Path: blob/dev/pkg/input/formats/swagger/downloader.go
2851 views
package swagger12import (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"gopkg.in/yaml.v3"17)1819// SwaggerDownloader implements the SpecDownloader interface for Swagger 2.0 specs20type SwaggerDownloader struct{}2122// NewDownloader creates a new Swagger downloader23func NewDownloader() formats.SpecDownloader {24return &SwaggerDownloader{}25}2627// This function downloads a Swagger 2.0 spec from the given URL and saves it to tmpDir28func (d *SwaggerDownloader) Download(urlStr, tmpDir string, httpClient *retryablehttp.Client) (string, error) {29// Swagger can be JSON or YAML30supportedExts := d.SupportedExtensions()31isSupported := false32for _, ext := range supportedExts {33if strings.HasSuffix(urlStr, ext) {34isSupported = true35break36}37}38if !isSupported {39return "", fmt.Errorf("URL does not appear to be a Swagger spec (supported: %v)", supportedExts)40}4142const maxSpecSizeBytes = 10 * 1024 * 1024 // 10MB4344// Use provided httpClient or create a fallback45var client *http.Client46if httpClient != nil {47client = httpClient.HTTPClient48} else {49// Fallback to simple client if no httpClient provided50client = &http.Client{Timeout: 30 * time.Second}51}5253resp, err := client.Get(urlStr)54if err != nil {55return "", errors.Wrap(err, "failed to download Swagger spec")56}5758defer func() {59_ = resp.Body.Close()60}()6162if resp.StatusCode != http.StatusOK {63return "", fmt.Errorf("HTTP %d when downloading Swagger spec", resp.StatusCode)64}6566bodyBytes, err := io.ReadAll(io.LimitReader(resp.Body, maxSpecSizeBytes))67if err != nil {68return "", errors.Wrap(err, "failed to read response body")69}7071// Determine format and parse72var spec map[string]interface{}73var isYAML bool7475// Try JSON first76if err := json.Unmarshal(bodyBytes, &spec); err != nil {77// Then try YAML78if err := yaml.Unmarshal(bodyBytes, &spec); err != nil {79return "", fmt.Errorf("downloaded content is neither valid JSON nor YAML: %w", err)80}81isYAML = true82}8384// Validate it's a Swagger 2.0 spec85if swagger, exists := spec["swagger"]; exists {86if swaggerStr, ok := swagger.(string); ok && strings.HasPrefix(swaggerStr, "2.") {87// Valid Swagger 2.0 spec88} else {89return "", fmt.Errorf("not a valid Swagger 2.0 spec (found version: %v)", swagger)90}91} else {92return "", fmt.Errorf("not a Swagger spec (missing 'swagger' field)")93}9495// Extract host from URL for host configuration96parsedURL, err := url.Parse(urlStr)97if err != nil {98return "", errors.Wrap(err, "failed to parse URL")99}100101host := parsedURL.Host102scheme := parsedURL.Scheme103if scheme == "" {104scheme = "https"105}106107// Add host if missing108if _, exists := spec["host"]; !exists {109spec["host"] = host110}111112// Add schemes if missing113if _, exists := spec["schemes"]; !exists {114spec["schemes"] = []string{scheme}115}116117// Create output directory118swaggerDir := filepath.Join(tmpDir, "swagger")119if err := os.MkdirAll(swaggerDir, 0755); err != nil {120return "", errors.Wrap(err, "failed to create swagger directory")121}122123// Generate filename and content based on original format124var filename string125var content []byte126127if isYAML {128filename = fmt.Sprintf("swagger-spec-%d.yaml", time.Now().Unix())129content, err = yaml.Marshal(spec)130if err != nil {131return "", errors.Wrap(err, "failed to marshal modified YAML spec")132}133} else {134filename = fmt.Sprintf("swagger-spec-%d.json", time.Now().Unix())135content, err = json.Marshal(spec)136if err != nil {137return "", errors.Wrap(err, "failed to marshal modified JSON spec")138}139}140141filePath := filepath.Join(swaggerDir, filename)142143// Write file144file, err := os.Create(filePath)145if err != nil {146return "", errors.Wrap(err, "failed to create file")147}148149defer func() {150_ = file.Close()151}()152153if _, writeErr := file.Write(content); writeErr != nil {154_ = os.Remove(filePath)155return "", errors.Wrap(writeErr, "failed to write file")156}157158return filePath, nil159}160161// SupportedExtensions returns the list of supported file extensions for Swagger162func (d *SwaggerDownloader) SupportedExtensions() []string {163return []string{".json", ".yaml", ".yml"}164}165166167