Path: blob/dev/pkg/input/formats/openapi/generator.go
2070 views
package openapi12import (3"bytes"4"fmt"5"io"6"mime/multipart"7"net/http"8"net/http/httputil"9"net/url"10"os"11"strings"1213"github.com/clbanning/mxj/v2"14"github.com/getkin/kin-openapi/openapi3"15"github.com/pkg/errors"16"github.com/projectdiscovery/gologger"17"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"18"github.com/projectdiscovery/nuclei/v3/pkg/input/formats"19httpTypes "github.com/projectdiscovery/nuclei/v3/pkg/input/types"20"github.com/projectdiscovery/nuclei/v3/pkg/types"21"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"22"github.com/projectdiscovery/utils/errkit"23"github.com/projectdiscovery/utils/generic"24mapsutil "github.com/projectdiscovery/utils/maps"25"github.com/valyala/fasttemplate"26)2728const (29globalAuth = "globalAuth"30DEFAULT_HTTP_SCHEME_HEADER = "Authorization"31)3233// GenerateRequestsFromSchema generates http requests from an OpenAPI 3.0 document object34func GenerateRequestsFromSchema(schema *openapi3.T, opts formats.InputFormatOptions, callback formats.ParseReqRespCallback) error {35if len(schema.Servers) == 0 {36return errors.New("no servers found in openapi schema")37}3839// new set of globalParams obtained from security schemes40globalParams := openapi3.NewParameters()4142if len(schema.Security) > 0 {43params, err := GetGlobalParamsForSecurityRequirement(schema, &schema.Security)44if err != nil {45return err46}47globalParams = append(globalParams, params...)48}4950// validate global param requirements51for _, param := range globalParams {52if val, ok := opts.Variables[param.Value.Name]; ok {53param.Value.Example = val54} else {55// if missing check for validation56if opts.SkipFormatValidation {57gologger.Verbose().Msgf("openapi: skipping all requests due to missing global auth parameter: %s\n", param.Value.Name)58return nil59} else {60// fatal error61gologger.Fatal().Msgf("openapi: missing global auth parameter: %s\n", param.Value.Name)62}63}64}6566missingVarMap := make(map[string]struct{})67optionalVarMap := make(map[string]struct{})68missingParamValueCallback := func(param *openapi3.Parameter, opts *generateReqOptions) {69if !param.Required {70optionalVarMap[param.Name] = struct{}{}71return72}73missingVarMap[param.Name] = struct{}{}74}7576for _, serverURL := range schema.Servers {77pathURL := serverURL.URL78// Split the server URL into baseURL and serverPath79u, err := url.Parse(pathURL)80if err != nil {81return errors.Wrap(err, "could not parse server url")82}83baseURL := fmt.Sprintf("%s://%s", u.Scheme, u.Host)84serverPath := u.Path8586for path, v := range schema.Paths.Map() {87// a path item can have parameters88ops := v.Operations()89requestPath := path90if serverPath != "" {91requestPath = serverPath + path92}93for method, ov := range ops {94if err := generateRequestsFromOp(&generateReqOptions{95requiredOnly: opts.RequiredOnly,96method: method,97pathURL: baseURL,98requestPath: requestPath,99op: ov,100schema: schema,101globalParams: globalParams,102reqParams: v.Parameters,103opts: opts,104callback: callback,105missingParamValueCallback: missingParamValueCallback,106}); err != nil {107gologger.Warning().Msgf("Could not generate requests from op: %s\n", err)108}109}110}111}112113if len(missingVarMap) > 0 && !opts.SkipFormatValidation {114gologger.Error().Msgf("openapi: Found %d missing parameters, use -skip-format-validation flag to skip requests or update missing parameters generated in %s file,you can also specify these vars using -var flag in (key=value) format\n", len(missingVarMap), formats.DefaultVarDumpFileName)115gologger.Verbose().Msgf("openapi: missing params: %+v", mapsutil.GetSortedKeys(missingVarMap))116if config.CurrentAppMode == config.AppModeCLI {117// generate var dump file118vars := &formats.OpenAPIParamsCfgFile{}119for k := range missingVarMap {120vars.Var = append(vars.Var, k+"=")121}122vars.OptionalVars = mapsutil.GetSortedKeys(optionalVarMap)123if err := formats.WriteOpenAPIVarDumpFile(vars); err != nil {124gologger.Error().Msgf("openapi: could not write params file: %s\n", err)125}126// exit with status code 1127os.Exit(1)128}129}130131return nil132}133134type generateReqOptions struct {135// requiredOnly specifies whether to generate only required fields136requiredOnly bool137// method is the http method to use138method string139// pathURL is the base url to use140pathURL string141// requestPath is the path to use142requestPath string143// schema is the openapi schema to use144schema *openapi3.T145// op is the operation to use146op *openapi3.Operation147// post request generation callback148callback formats.ParseReqRespCallback149150// global parameters151globalParams openapi3.Parameters152// requestparams map153reqParams openapi3.Parameters154// global var map155opts formats.InputFormatOptions156// missingVar Callback157missingParamValueCallback func(param *openapi3.Parameter, opts *generateReqOptions)158}159160// generateRequestsFromOp generates requests from an operation and some other data161// about an OpenAPI Schema Path and Method object.162//163// It also accepts an optional requiredOnly flag which if specified, only returns the fields164// of the structure that are required. If false, all fields are returned.165func generateRequestsFromOp(opts *generateReqOptions) error {166req, err := http.NewRequest(opts.method, opts.pathURL+opts.requestPath, nil)167if err != nil {168return errors.Wrap(err, "could not make request")169}170171reqParams := opts.reqParams172if reqParams == nil {173reqParams = openapi3.NewParameters()174}175// add existing req params176reqParams = append(reqParams, opts.op.Parameters...)177// check for endpoint specific auth178if opts.op.Security != nil {179params, err := GetGlobalParamsForSecurityRequirement(opts.schema, opts.op.Security)180if err != nil {181return err182}183reqParams = append(reqParams, params...)184} else {185reqParams = append(reqParams, opts.globalParams...)186}187188query := url.Values{}189for _, parameter := range reqParams {190value := parameter.Value191192if value.Schema == nil || value.Schema.Value == nil {193continue194}195196// paramValue or default value to use197var paramValue interface{}198199// accept override from global variables200if val, ok := opts.opts.Variables[value.Name]; ok {201paramValue = val202} else if value.Schema.Value.Default != nil {203paramValue = value.Schema.Value.Default204} else if value.Schema.Value.Example != nil {205paramValue = value.Schema.Value.Example206} else if len(value.Schema.Value.Enum) > 0 {207paramValue = value.Schema.Value.Enum[0]208} else {209if !opts.opts.SkipFormatValidation {210if opts.missingParamValueCallback != nil {211opts.missingParamValueCallback(value, opts)212}213// skip request if param in path else skip this param only214if value.Required {215// gologger.Verbose().Msgf("skipping request [%s] %s due to missing value (%v)\n", opts.method, opts.requestPath, value.Name)216return nil217} else {218// if it is in path then remove it from path219opts.requestPath = strings.ReplaceAll(opts.requestPath, fmt.Sprintf("{%s}", value.Name), "")220if !opts.opts.RequiredOnly {221gologger.Verbose().Msgf("openapi: skipping optional param (%s) in (%v) in request [%s] %s due to missing value (%v)\n", value.Name, value.In, opts.method, opts.requestPath, value.Name)222}223continue224}225}226exampleX, err := generateExampleFromSchema(value.Schema.Value)227if err != nil {228// when failed to generate example229// skip request if param in path else skip this param only230if value.Required {231gologger.Verbose().Msgf("openapi: skipping request [%s] %s due to missing value (%v)\n", opts.method, opts.requestPath, value.Name)232return nil233} else {234// if it is in path then remove it from path235opts.requestPath = strings.ReplaceAll(opts.requestPath, fmt.Sprintf("{%s}", value.Name), "")236if !opts.opts.RequiredOnly {237gologger.Verbose().Msgf("openapi: skipping optional param (%s) in (%v) in request [%s] %s due to missing value (%v)\n", value.Name, value.In, opts.method, opts.requestPath, value.Name)238}239continue240}241}242paramValue = exampleX243}244if opts.requiredOnly && !value.Required {245// remove them from path if any246opts.requestPath = strings.ReplaceAll(opts.requestPath, fmt.Sprintf("{%s}", value.Name), "")247continue // Skip this parameter if it is not required and we want only required ones248}249250switch value.In {251case "query":252query.Set(value.Name, types.ToString(paramValue))253case "header":254req.Header.Set(value.Name, types.ToString(paramValue))255case "path":256opts.requestPath = fasttemplate.ExecuteStringStd(opts.requestPath, "{", "}", map[string]interface{}{257value.Name: types.ToString(paramValue),258})259case "cookie":260req.AddCookie(&http.Cookie{Name: value.Name, Value: types.ToString(paramValue)})261}262}263req.URL.RawQuery = query.Encode()264req.URL.Path = opts.requestPath265266if opts.op.RequestBody != nil {267for content, value := range opts.op.RequestBody.Value.Content {268cloned := req.Clone(req.Context())269270var val interface{}271272if value.Schema == nil || value.Schema.Value == nil {273val = generateEmptySchemaValue(content)274} else {275var err error276277val, err = generateExampleFromSchema(value.Schema.Value)278if err != nil {279continue280}281}282283// var body string284switch content {285case "application/json":286if marshalled, err := json.Marshal(val); err == nil {287// body = string(marshalled)288cloned.Body = io.NopCloser(bytes.NewReader(marshalled))289cloned.ContentLength = int64(len(marshalled))290cloned.Header.Set("Content-Type", "application/json")291}292case "application/xml":293values := mxj.Map(val.(map[string]interface{}))294295if marshalled, err := values.Xml(); err == nil {296// body = string(marshalled)297cloned.Body = io.NopCloser(bytes.NewReader(marshalled))298cloned.ContentLength = int64(len(marshalled))299cloned.Header.Set("Content-Type", "application/xml")300} else {301gologger.Warning().Msgf("openapi: could not encode xml")302}303case "application/x-www-form-urlencoded":304if values, ok := val.(map[string]interface{}); ok {305cloned.Form = url.Values{}306for k, v := range values {307cloned.Form.Set(k, types.ToString(v))308}309encoded := cloned.Form.Encode()310cloned.ContentLength = int64(len(encoded))311// body = encoded312cloned.Body = io.NopCloser(strings.NewReader(encoded))313cloned.Header.Set("Content-Type", "application/x-www-form-urlencoded")314}315case "multipart/form-data":316if values, ok := val.(map[string]interface{}); ok {317buffer := &bytes.Buffer{}318multipartWriter := multipart.NewWriter(buffer)319for k, v := range values {320// This is a file if format is binary, otherwise field321if property, ok := value.Schema.Value.Properties[k]; ok && property.Value.Format == "binary" {322if writer, err := multipartWriter.CreateFormFile(k, k); err == nil {323_, _ = writer.Write([]byte(types.ToString(v)))324}325} else {326_ = multipartWriter.WriteField(k, types.ToString(v))327}328}329_ = multipartWriter.Close()330// body = buffer.String()331cloned.Body = io.NopCloser(buffer)332cloned.ContentLength = int64(len(buffer.Bytes()))333cloned.Header.Set("Content-Type", multipartWriter.FormDataContentType())334}335case "text/plain":336str := types.ToString(val)337// body = str338cloned.Body = io.NopCloser(strings.NewReader(str))339cloned.ContentLength = int64(len(str))340cloned.Header.Set("Content-Type", "text/plain")341case "application/octet-stream":342str := types.ToString(val)343if str == "" {344// use two strings345str = "string1\nstring2"346}347if value.Schema != nil && generic.EqualsAny(value.Schema.Value.Format, "bindary", "byte") {348cloned.Body = io.NopCloser(bytes.NewReader([]byte(str)))349cloned.ContentLength = int64(len(str))350cloned.Header.Set("Content-Type", "application/octet-stream")351} else {352// use string placeholder353cloned.Body = io.NopCloser(strings.NewReader(str))354cloned.ContentLength = int64(len(str))355cloned.Header.Set("Content-Type", "text/plain")356}357default:358gologger.Verbose().Msgf("openapi: no correct content type found for body: %s\n", content)359// LOG: return errors.New("no correct content type found for body")360continue361}362363dumped, err := httputil.DumpRequestOut(cloned, true)364if err != nil {365return errors.Wrap(err, "could not dump request")366}367368rr, err := httpTypes.ParseRawRequestWithURL(string(dumped), cloned.URL.String())369if err != nil {370return errors.Wrap(err, "could not parse raw request")371}372opts.callback(rr)373continue374}375}376if opts.op.RequestBody != nil {377return nil378}379380dumped, err := httputil.DumpRequestOut(req, true)381if err != nil {382return errors.Wrap(err, "could not dump request")383}384385rr, err := httpTypes.ParseRawRequestWithURL(string(dumped), req.URL.String())386if err != nil {387return errors.Wrap(err, "could not parse raw request")388}389opts.callback(rr)390return nil391}392393// GetGlobalParamsForSecurityRequirement returns the global parameters for a security requirement394func GetGlobalParamsForSecurityRequirement(schema *openapi3.T, requirement *openapi3.SecurityRequirements) ([]*openapi3.ParameterRef, error) {395globalParams := openapi3.NewParameters()396if len(schema.Components.SecuritySchemes) == 0 {397return nil, errkit.Newf("security requirements (%+v) without any security schemes found in openapi file", schema.Security)398}399found := false400// this api is protected for each security scheme pull its corresponding scheme401schemaLabel:402for _, security := range *requirement {403for name := range security {404if scheme, ok := schema.Components.SecuritySchemes[name]; ok {405found = true406param, err := GenerateParameterFromSecurityScheme(scheme)407if err != nil {408return nil, err409410}411globalParams = append(globalParams, &openapi3.ParameterRef{Value: param})412continue schemaLabel413}414}415if !found && len(security) > 1 {416// if this is case then both security schemes are required417return nil, errkit.Newf("security requirement (%+v) not found in openapi file", security)418}419}420if !found {421return nil, errkit.Newf("security requirement (%+v) not found in openapi file", requirement)422}423424return globalParams, nil425}426427// GenerateParameterFromSecurityScheme generates an example from a schema object428func GenerateParameterFromSecurityScheme(scheme *openapi3.SecuritySchemeRef) (*openapi3.Parameter, error) {429if !generic.EqualsAny(scheme.Value.Type, "http", "apiKey") {430return nil, errkit.Newf("unsupported security scheme type (%s) found in openapi file", scheme.Value.Type)431}432if scheme.Value.Type == "http" {433// check scheme434if !generic.EqualsAny(scheme.Value.Scheme, "basic", "bearer") {435return nil, errkit.Newf("unsupported security scheme (%s) found in openapi file", scheme.Value.Scheme)436}437// HTTP authentication schemes basic or bearer use the Authorization header438headerName := scheme.Value.Name439if headerName == "" {440headerName = DEFAULT_HTTP_SCHEME_HEADER441}442// create parameters using the scheme443switch scheme.Value.Scheme {444case "basic":445h := openapi3.NewHeaderParameter(headerName)446h.Required = true447h.Description = globalAuth // differentiator for normal variables and global auth448return h, nil449case "bearer":450h := openapi3.NewHeaderParameter(headerName)451h.Required = true452h.Description = globalAuth // differentiator for normal variables and global auth453return h, nil454}455456}457if scheme.Value.Type == "apiKey" {458// validate name and in459if scheme.Value.Name == "" {460return nil, errkit.Newf("security scheme (%s) name is empty", scheme.Value.Type)461}462if !generic.EqualsAny(scheme.Value.In, "query", "header", "cookie") {463return nil, errkit.Newf("unsupported security scheme (%s) in (%s) found in openapi file", scheme.Value.Type, scheme.Value.In)464}465// create parameters using the scheme466switch scheme.Value.In {467case "query":468q := openapi3.NewQueryParameter(scheme.Value.Name)469q.Required = true470q.Description = globalAuth // differentiator for normal variables and global auth471return q, nil472case "header":473h := openapi3.NewHeaderParameter(scheme.Value.Name)474h.Required = true475h.Description = globalAuth // differentiator for normal variables and global auth476return h, nil477case "cookie":478c := openapi3.NewCookieParameter(scheme.Value.Name)479c.Required = true480c.Description = globalAuth // differentiator for normal variables and global auth481return c, nil482}483}484return nil, errkit.Newf("unsupported security scheme type (%s) found in openapi file", scheme.Value.Type)485}486487488