Path: blob/dev/pkg/input/formats/openapi/downloader.go
2844 views
package openapi12import (3"encoding/json"4"fmt"5"io"6"net/http"7"net/url"8"os"9"path/filepath"10"strings"11"time"1213"github.com/pkg/errors"14"github.com/projectdiscovery/nuclei/v3/pkg/input/formats"15"github.com/projectdiscovery/retryablehttp-go"16)1718// OpenAPIDownloader implements the SpecDownloader interface for OpenAPI 3.0 specs19type OpenAPIDownloader struct{}2021// NewDownloader creates a new OpenAPI downloader22func NewDownloader() formats.SpecDownloader {23return &OpenAPIDownloader{}24}2526// This function downloads an OpenAPI 3.0 spec from the given URL and saves it to tmpDir27func (d *OpenAPIDownloader) Download(urlStr, tmpDir string, httpClient *retryablehttp.Client) (string, error) {28// Validate URL format, OpenAPI 3.0 specs are typically JSON29if !strings.HasSuffix(urlStr, ".json") {30return "", fmt.Errorf("URL does not appear to be an OpenAPI JSON spec")31}3233const maxSpecSizeBytes = 10 * 1024 * 1024 // 10MB3435// Use provided httpClient or create a fallback36var client *http.Client37if httpClient != nil {38client = httpClient.HTTPClient39} else {40// Fallback to simple client if no httpClient provided41client = &http.Client{Timeout: 30 * time.Second}42}4344resp, err := client.Get(urlStr)45if err != nil {46return "", errors.Wrap(err, "failed to download OpenAPI spec")47}4849defer func() {50_ = resp.Body.Close()51}()5253if resp.StatusCode != http.StatusOK {54return "", fmt.Errorf("HTTP %d when downloading OpenAPI spec", resp.StatusCode)55}5657bodyBytes, err := io.ReadAll(io.LimitReader(resp.Body, maxSpecSizeBytes))58if err != nil {59return "", errors.Wrap(err, "failed to read response body")60}6162// Validate it's a valid JSON and has OpenAPI structure63var spec map[string]interface{}64if err := json.Unmarshal(bodyBytes, &spec); err != nil {65return "", fmt.Errorf("downloaded content is not valid JSON: %w", err)66}6768// Check if it's an OpenAPI 3.0 spec69if openapi, exists := spec["openapi"]; exists {70if openapiStr, ok := openapi.(string); ok && strings.HasPrefix(openapiStr, "3.") {71// Valid OpenAPI 3.0 spec72} else {73return "", fmt.Errorf("not a valid OpenAPI 3.0 spec (found version: %v)", openapi)74}75} else {76return "", fmt.Errorf("not an OpenAPI spec (missing 'openapi' field)")77}7879// Extract host from URL for server configuration80parsedURL, err := url.Parse(urlStr)81if err != nil {82return "", errors.Wrap(err, "failed to parse URL")83}84host := parsedURL.Host85scheme := parsedURL.Scheme86if scheme == "" {87scheme = "https"88}8990// Add servers section if missing or empty91servers, exists := spec["servers"]92if !exists || servers == nil {93spec["servers"] = []map[string]interface{}{{"url": scheme + "://" + host}}94} else if serverList, ok := servers.([]interface{}); ok && len(serverList) == 0 {95spec["servers"] = []map[string]interface{}{{"url": scheme + "://" + host}}96}9798// Marshal back to JSON99modifiedJSON, err := json.Marshal(spec)100if err != nil {101return "", errors.Wrap(err, "failed to marshal modified spec")102}103104// Create output directory105openapiDir := filepath.Join(tmpDir, "openapi")106if err := os.MkdirAll(openapiDir, 0755); err != nil {107return "", errors.Wrap(err, "failed to create openapi directory")108}109110// Generate filename111filename := fmt.Sprintf("openapi-spec-%d.json", time.Now().Unix())112filePath := filepath.Join(openapiDir, filename)113114// Write file115file, err := os.Create(filePath)116if err != nil {117return "", fmt.Errorf("failed to create file: %w", err)118}119120defer func() {121_ = file.Close()122}()123124if _, writeErr := file.Write(modifiedJSON); writeErr != nil {125_ = os.Remove(filePath)126return "", errors.Wrap(writeErr, "failed to write OpenAPI spec to file")127}128129return filePath, nil130}131132// SupportedExtensions returns the list of supported file extensions for OpenAPI133func (d *OpenAPIDownloader) SupportedExtensions() []string {134return []string{".json"}135}136137138