Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
projectdiscovery
GitHub Repository: projectdiscovery/nuclei
Path: blob/dev/pkg/input/formats/swagger/downloader.go
2851 views
1
package swagger
2
3
import (
4
"encoding/json"
5
"fmt"
6
"io"
7
"net/http"
8
"net/url"
9
"os"
10
"path/filepath"
11
"strings"
12
"time"
13
14
"github.com/pkg/errors"
15
"github.com/projectdiscovery/nuclei/v3/pkg/input/formats"
16
"github.com/projectdiscovery/retryablehttp-go"
17
"gopkg.in/yaml.v3"
18
)
19
20
// SwaggerDownloader implements the SpecDownloader interface for Swagger 2.0 specs
21
type SwaggerDownloader struct{}
22
23
// NewDownloader creates a new Swagger downloader
24
func NewDownloader() formats.SpecDownloader {
25
return &SwaggerDownloader{}
26
}
27
28
// This function downloads a Swagger 2.0 spec from the given URL and saves it to tmpDir
29
func (d *SwaggerDownloader) Download(urlStr, tmpDir string, httpClient *retryablehttp.Client) (string, error) {
30
// Swagger can be JSON or YAML
31
supportedExts := d.SupportedExtensions()
32
isSupported := false
33
for _, ext := range supportedExts {
34
if strings.HasSuffix(urlStr, ext) {
35
isSupported = true
36
break
37
}
38
}
39
if !isSupported {
40
return "", fmt.Errorf("URL does not appear to be a Swagger spec (supported: %v)", supportedExts)
41
}
42
43
const maxSpecSizeBytes = 10 * 1024 * 1024 // 10MB
44
45
// Use provided httpClient or create a fallback
46
var client *http.Client
47
if httpClient != nil {
48
client = httpClient.HTTPClient
49
} else {
50
// Fallback to simple client if no httpClient provided
51
client = &http.Client{Timeout: 30 * time.Second}
52
}
53
54
resp, err := client.Get(urlStr)
55
if err != nil {
56
return "", errors.Wrap(err, "failed to download Swagger spec")
57
}
58
59
defer func() {
60
_ = resp.Body.Close()
61
}()
62
63
if resp.StatusCode != http.StatusOK {
64
return "", fmt.Errorf("HTTP %d when downloading Swagger spec", resp.StatusCode)
65
}
66
67
bodyBytes, err := io.ReadAll(io.LimitReader(resp.Body, maxSpecSizeBytes))
68
if err != nil {
69
return "", errors.Wrap(err, "failed to read response body")
70
}
71
72
// Determine format and parse
73
var spec map[string]interface{}
74
var isYAML bool
75
76
// Try JSON first
77
if err := json.Unmarshal(bodyBytes, &spec); err != nil {
78
// Then try YAML
79
if err := yaml.Unmarshal(bodyBytes, &spec); err != nil {
80
return "", fmt.Errorf("downloaded content is neither valid JSON nor YAML: %w", err)
81
}
82
isYAML = true
83
}
84
85
// Validate it's a Swagger 2.0 spec
86
if swagger, exists := spec["swagger"]; exists {
87
if swaggerStr, ok := swagger.(string); ok && strings.HasPrefix(swaggerStr, "2.") {
88
// Valid Swagger 2.0 spec
89
} else {
90
return "", fmt.Errorf("not a valid Swagger 2.0 spec (found version: %v)", swagger)
91
}
92
} else {
93
return "", fmt.Errorf("not a Swagger spec (missing 'swagger' field)")
94
}
95
96
// Extract host from URL for host configuration
97
parsedURL, err := url.Parse(urlStr)
98
if err != nil {
99
return "", errors.Wrap(err, "failed to parse URL")
100
}
101
102
host := parsedURL.Host
103
scheme := parsedURL.Scheme
104
if scheme == "" {
105
scheme = "https"
106
}
107
108
// Add host if missing
109
if _, exists := spec["host"]; !exists {
110
spec["host"] = host
111
}
112
113
// Add schemes if missing
114
if _, exists := spec["schemes"]; !exists {
115
spec["schemes"] = []string{scheme}
116
}
117
118
// Create output directory
119
swaggerDir := filepath.Join(tmpDir, "swagger")
120
if err := os.MkdirAll(swaggerDir, 0755); err != nil {
121
return "", errors.Wrap(err, "failed to create swagger directory")
122
}
123
124
// Generate filename and content based on original format
125
var filename string
126
var content []byte
127
128
if isYAML {
129
filename = fmt.Sprintf("swagger-spec-%d.yaml", time.Now().Unix())
130
content, err = yaml.Marshal(spec)
131
if err != nil {
132
return "", errors.Wrap(err, "failed to marshal modified YAML spec")
133
}
134
} else {
135
filename = fmt.Sprintf("swagger-spec-%d.json", time.Now().Unix())
136
content, err = json.Marshal(spec)
137
if err != nil {
138
return "", errors.Wrap(err, "failed to marshal modified JSON spec")
139
}
140
}
141
142
filePath := filepath.Join(swaggerDir, filename)
143
144
// Write file
145
file, err := os.Create(filePath)
146
if err != nil {
147
return "", errors.Wrap(err, "failed to create file")
148
}
149
150
defer func() {
151
_ = file.Close()
152
}()
153
154
if _, writeErr := file.Write(content); writeErr != nil {
155
_ = os.Remove(filePath)
156
return "", errors.Wrap(writeErr, "failed to write file")
157
}
158
159
return filePath, nil
160
}
161
162
// SupportedExtensions returns the list of supported file extensions for Swagger
163
func (d *SwaggerDownloader) SupportedExtensions() []string {
164
return []string{".json", ".yaml", ".yml"}
165
}
166
167