Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
projectdiscovery
GitHub Repository: projectdiscovery/nuclei
Path: blob/dev/pkg/input/formats/openapi/downloader.go
2844 views
1
package openapi
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
)
18
19
// OpenAPIDownloader implements the SpecDownloader interface for OpenAPI 3.0 specs
20
type OpenAPIDownloader struct{}
21
22
// NewDownloader creates a new OpenAPI downloader
23
func NewDownloader() formats.SpecDownloader {
24
return &OpenAPIDownloader{}
25
}
26
27
// This function downloads an OpenAPI 3.0 spec from the given URL and saves it to tmpDir
28
func (d *OpenAPIDownloader) Download(urlStr, tmpDir string, httpClient *retryablehttp.Client) (string, error) {
29
// Validate URL format, OpenAPI 3.0 specs are typically JSON
30
if !strings.HasSuffix(urlStr, ".json") {
31
return "", fmt.Errorf("URL does not appear to be an OpenAPI JSON spec")
32
}
33
34
const maxSpecSizeBytes = 10 * 1024 * 1024 // 10MB
35
36
// Use provided httpClient or create a fallback
37
var client *http.Client
38
if httpClient != nil {
39
client = httpClient.HTTPClient
40
} else {
41
// Fallback to simple client if no httpClient provided
42
client = &http.Client{Timeout: 30 * time.Second}
43
}
44
45
resp, err := client.Get(urlStr)
46
if err != nil {
47
return "", errors.Wrap(err, "failed to download OpenAPI spec")
48
}
49
50
defer func() {
51
_ = resp.Body.Close()
52
}()
53
54
if resp.StatusCode != http.StatusOK {
55
return "", fmt.Errorf("HTTP %d when downloading OpenAPI spec", resp.StatusCode)
56
}
57
58
bodyBytes, err := io.ReadAll(io.LimitReader(resp.Body, maxSpecSizeBytes))
59
if err != nil {
60
return "", errors.Wrap(err, "failed to read response body")
61
}
62
63
// Validate it's a valid JSON and has OpenAPI structure
64
var spec map[string]interface{}
65
if err := json.Unmarshal(bodyBytes, &spec); err != nil {
66
return "", fmt.Errorf("downloaded content is not valid JSON: %w", err)
67
}
68
69
// Check if it's an OpenAPI 3.0 spec
70
if openapi, exists := spec["openapi"]; exists {
71
if openapiStr, ok := openapi.(string); ok && strings.HasPrefix(openapiStr, "3.") {
72
// Valid OpenAPI 3.0 spec
73
} else {
74
return "", fmt.Errorf("not a valid OpenAPI 3.0 spec (found version: %v)", openapi)
75
}
76
} else {
77
return "", fmt.Errorf("not an OpenAPI spec (missing 'openapi' field)")
78
}
79
80
// Extract host from URL for server configuration
81
parsedURL, err := url.Parse(urlStr)
82
if err != nil {
83
return "", errors.Wrap(err, "failed to parse URL")
84
}
85
host := parsedURL.Host
86
scheme := parsedURL.Scheme
87
if scheme == "" {
88
scheme = "https"
89
}
90
91
// Add servers section if missing or empty
92
servers, exists := spec["servers"]
93
if !exists || servers == nil {
94
spec["servers"] = []map[string]interface{}{{"url": scheme + "://" + host}}
95
} else if serverList, ok := servers.([]interface{}); ok && len(serverList) == 0 {
96
spec["servers"] = []map[string]interface{}{{"url": scheme + "://" + host}}
97
}
98
99
// Marshal back to JSON
100
modifiedJSON, err := json.Marshal(spec)
101
if err != nil {
102
return "", errors.Wrap(err, "failed to marshal modified spec")
103
}
104
105
// Create output directory
106
openapiDir := filepath.Join(tmpDir, "openapi")
107
if err := os.MkdirAll(openapiDir, 0755); err != nil {
108
return "", errors.Wrap(err, "failed to create openapi directory")
109
}
110
111
// Generate filename
112
filename := fmt.Sprintf("openapi-spec-%d.json", time.Now().Unix())
113
filePath := filepath.Join(openapiDir, filename)
114
115
// Write file
116
file, err := os.Create(filePath)
117
if err != nil {
118
return "", fmt.Errorf("failed to create file: %w", err)
119
}
120
121
defer func() {
122
_ = file.Close()
123
}()
124
125
if _, writeErr := file.Write(modifiedJSON); writeErr != nil {
126
_ = os.Remove(filePath)
127
return "", errors.Wrap(writeErr, "failed to write OpenAPI spec to file")
128
}
129
130
return filePath, nil
131
}
132
133
// SupportedExtensions returns the list of supported file extensions for OpenAPI
134
func (d *OpenAPIDownloader) SupportedExtensions() []string {
135
return []string{".json"}
136
}
137
138