Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
projectdiscovery
GitHub Repository: projectdiscovery/nuclei
Path: blob/dev/pkg/input/formats/openapi/generator.go
2070 views
1
package openapi
2
3
import (
4
"bytes"
5
"fmt"
6
"io"
7
"mime/multipart"
8
"net/http"
9
"net/http/httputil"
10
"net/url"
11
"os"
12
"strings"
13
14
"github.com/clbanning/mxj/v2"
15
"github.com/getkin/kin-openapi/openapi3"
16
"github.com/pkg/errors"
17
"github.com/projectdiscovery/gologger"
18
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
19
"github.com/projectdiscovery/nuclei/v3/pkg/input/formats"
20
httpTypes "github.com/projectdiscovery/nuclei/v3/pkg/input/types"
21
"github.com/projectdiscovery/nuclei/v3/pkg/types"
22
"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
23
"github.com/projectdiscovery/utils/errkit"
24
"github.com/projectdiscovery/utils/generic"
25
mapsutil "github.com/projectdiscovery/utils/maps"
26
"github.com/valyala/fasttemplate"
27
)
28
29
const (
30
globalAuth = "globalAuth"
31
DEFAULT_HTTP_SCHEME_HEADER = "Authorization"
32
)
33
34
// GenerateRequestsFromSchema generates http requests from an OpenAPI 3.0 document object
35
func GenerateRequestsFromSchema(schema *openapi3.T, opts formats.InputFormatOptions, callback formats.ParseReqRespCallback) error {
36
if len(schema.Servers) == 0 {
37
return errors.New("no servers found in openapi schema")
38
}
39
40
// new set of globalParams obtained from security schemes
41
globalParams := openapi3.NewParameters()
42
43
if len(schema.Security) > 0 {
44
params, err := GetGlobalParamsForSecurityRequirement(schema, &schema.Security)
45
if err != nil {
46
return err
47
}
48
globalParams = append(globalParams, params...)
49
}
50
51
// validate global param requirements
52
for _, param := range globalParams {
53
if val, ok := opts.Variables[param.Value.Name]; ok {
54
param.Value.Example = val
55
} else {
56
// if missing check for validation
57
if opts.SkipFormatValidation {
58
gologger.Verbose().Msgf("openapi: skipping all requests due to missing global auth parameter: %s\n", param.Value.Name)
59
return nil
60
} else {
61
// fatal error
62
gologger.Fatal().Msgf("openapi: missing global auth parameter: %s\n", param.Value.Name)
63
}
64
}
65
}
66
67
missingVarMap := make(map[string]struct{})
68
optionalVarMap := make(map[string]struct{})
69
missingParamValueCallback := func(param *openapi3.Parameter, opts *generateReqOptions) {
70
if !param.Required {
71
optionalVarMap[param.Name] = struct{}{}
72
return
73
}
74
missingVarMap[param.Name] = struct{}{}
75
}
76
77
for _, serverURL := range schema.Servers {
78
pathURL := serverURL.URL
79
// Split the server URL into baseURL and serverPath
80
u, err := url.Parse(pathURL)
81
if err != nil {
82
return errors.Wrap(err, "could not parse server url")
83
}
84
baseURL := fmt.Sprintf("%s://%s", u.Scheme, u.Host)
85
serverPath := u.Path
86
87
for path, v := range schema.Paths.Map() {
88
// a path item can have parameters
89
ops := v.Operations()
90
requestPath := path
91
if serverPath != "" {
92
requestPath = serverPath + path
93
}
94
for method, ov := range ops {
95
if err := generateRequestsFromOp(&generateReqOptions{
96
requiredOnly: opts.RequiredOnly,
97
method: method,
98
pathURL: baseURL,
99
requestPath: requestPath,
100
op: ov,
101
schema: schema,
102
globalParams: globalParams,
103
reqParams: v.Parameters,
104
opts: opts,
105
callback: callback,
106
missingParamValueCallback: missingParamValueCallback,
107
}); err != nil {
108
gologger.Warning().Msgf("Could not generate requests from op: %s\n", err)
109
}
110
}
111
}
112
}
113
114
if len(missingVarMap) > 0 && !opts.SkipFormatValidation {
115
gologger.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)
116
gologger.Verbose().Msgf("openapi: missing params: %+v", mapsutil.GetSortedKeys(missingVarMap))
117
if config.CurrentAppMode == config.AppModeCLI {
118
// generate var dump file
119
vars := &formats.OpenAPIParamsCfgFile{}
120
for k := range missingVarMap {
121
vars.Var = append(vars.Var, k+"=")
122
}
123
vars.OptionalVars = mapsutil.GetSortedKeys(optionalVarMap)
124
if err := formats.WriteOpenAPIVarDumpFile(vars); err != nil {
125
gologger.Error().Msgf("openapi: could not write params file: %s\n", err)
126
}
127
// exit with status code 1
128
os.Exit(1)
129
}
130
}
131
132
return nil
133
}
134
135
type generateReqOptions struct {
136
// requiredOnly specifies whether to generate only required fields
137
requiredOnly bool
138
// method is the http method to use
139
method string
140
// pathURL is the base url to use
141
pathURL string
142
// requestPath is the path to use
143
requestPath string
144
// schema is the openapi schema to use
145
schema *openapi3.T
146
// op is the operation to use
147
op *openapi3.Operation
148
// post request generation callback
149
callback formats.ParseReqRespCallback
150
151
// global parameters
152
globalParams openapi3.Parameters
153
// requestparams map
154
reqParams openapi3.Parameters
155
// global var map
156
opts formats.InputFormatOptions
157
// missingVar Callback
158
missingParamValueCallback func(param *openapi3.Parameter, opts *generateReqOptions)
159
}
160
161
// generateRequestsFromOp generates requests from an operation and some other data
162
// about an OpenAPI Schema Path and Method object.
163
//
164
// It also accepts an optional requiredOnly flag which if specified, only returns the fields
165
// of the structure that are required. If false, all fields are returned.
166
func generateRequestsFromOp(opts *generateReqOptions) error {
167
req, err := http.NewRequest(opts.method, opts.pathURL+opts.requestPath, nil)
168
if err != nil {
169
return errors.Wrap(err, "could not make request")
170
}
171
172
reqParams := opts.reqParams
173
if reqParams == nil {
174
reqParams = openapi3.NewParameters()
175
}
176
// add existing req params
177
reqParams = append(reqParams, opts.op.Parameters...)
178
// check for endpoint specific auth
179
if opts.op.Security != nil {
180
params, err := GetGlobalParamsForSecurityRequirement(opts.schema, opts.op.Security)
181
if err != nil {
182
return err
183
}
184
reqParams = append(reqParams, params...)
185
} else {
186
reqParams = append(reqParams, opts.globalParams...)
187
}
188
189
query := url.Values{}
190
for _, parameter := range reqParams {
191
value := parameter.Value
192
193
if value.Schema == nil || value.Schema.Value == nil {
194
continue
195
}
196
197
// paramValue or default value to use
198
var paramValue interface{}
199
200
// accept override from global variables
201
if val, ok := opts.opts.Variables[value.Name]; ok {
202
paramValue = val
203
} else if value.Schema.Value.Default != nil {
204
paramValue = value.Schema.Value.Default
205
} else if value.Schema.Value.Example != nil {
206
paramValue = value.Schema.Value.Example
207
} else if len(value.Schema.Value.Enum) > 0 {
208
paramValue = value.Schema.Value.Enum[0]
209
} else {
210
if !opts.opts.SkipFormatValidation {
211
if opts.missingParamValueCallback != nil {
212
opts.missingParamValueCallback(value, opts)
213
}
214
// skip request if param in path else skip this param only
215
if value.Required {
216
// gologger.Verbose().Msgf("skipping request [%s] %s due to missing value (%v)\n", opts.method, opts.requestPath, value.Name)
217
return nil
218
} else {
219
// if it is in path then remove it from path
220
opts.requestPath = strings.ReplaceAll(opts.requestPath, fmt.Sprintf("{%s}", value.Name), "")
221
if !opts.opts.RequiredOnly {
222
gologger.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)
223
}
224
continue
225
}
226
}
227
exampleX, err := generateExampleFromSchema(value.Schema.Value)
228
if err != nil {
229
// when failed to generate example
230
// skip request if param in path else skip this param only
231
if value.Required {
232
gologger.Verbose().Msgf("openapi: skipping request [%s] %s due to missing value (%v)\n", opts.method, opts.requestPath, value.Name)
233
return nil
234
} else {
235
// if it is in path then remove it from path
236
opts.requestPath = strings.ReplaceAll(opts.requestPath, fmt.Sprintf("{%s}", value.Name), "")
237
if !opts.opts.RequiredOnly {
238
gologger.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)
239
}
240
continue
241
}
242
}
243
paramValue = exampleX
244
}
245
if opts.requiredOnly && !value.Required {
246
// remove them from path if any
247
opts.requestPath = strings.ReplaceAll(opts.requestPath, fmt.Sprintf("{%s}", value.Name), "")
248
continue // Skip this parameter if it is not required and we want only required ones
249
}
250
251
switch value.In {
252
case "query":
253
query.Set(value.Name, types.ToString(paramValue))
254
case "header":
255
req.Header.Set(value.Name, types.ToString(paramValue))
256
case "path":
257
opts.requestPath = fasttemplate.ExecuteStringStd(opts.requestPath, "{", "}", map[string]interface{}{
258
value.Name: types.ToString(paramValue),
259
})
260
case "cookie":
261
req.AddCookie(&http.Cookie{Name: value.Name, Value: types.ToString(paramValue)})
262
}
263
}
264
req.URL.RawQuery = query.Encode()
265
req.URL.Path = opts.requestPath
266
267
if opts.op.RequestBody != nil {
268
for content, value := range opts.op.RequestBody.Value.Content {
269
cloned := req.Clone(req.Context())
270
271
var val interface{}
272
273
if value.Schema == nil || value.Schema.Value == nil {
274
val = generateEmptySchemaValue(content)
275
} else {
276
var err error
277
278
val, err = generateExampleFromSchema(value.Schema.Value)
279
if err != nil {
280
continue
281
}
282
}
283
284
// var body string
285
switch content {
286
case "application/json":
287
if marshalled, err := json.Marshal(val); err == nil {
288
// body = string(marshalled)
289
cloned.Body = io.NopCloser(bytes.NewReader(marshalled))
290
cloned.ContentLength = int64(len(marshalled))
291
cloned.Header.Set("Content-Type", "application/json")
292
}
293
case "application/xml":
294
values := mxj.Map(val.(map[string]interface{}))
295
296
if marshalled, err := values.Xml(); err == nil {
297
// body = string(marshalled)
298
cloned.Body = io.NopCloser(bytes.NewReader(marshalled))
299
cloned.ContentLength = int64(len(marshalled))
300
cloned.Header.Set("Content-Type", "application/xml")
301
} else {
302
gologger.Warning().Msgf("openapi: could not encode xml")
303
}
304
case "application/x-www-form-urlencoded":
305
if values, ok := val.(map[string]interface{}); ok {
306
cloned.Form = url.Values{}
307
for k, v := range values {
308
cloned.Form.Set(k, types.ToString(v))
309
}
310
encoded := cloned.Form.Encode()
311
cloned.ContentLength = int64(len(encoded))
312
// body = encoded
313
cloned.Body = io.NopCloser(strings.NewReader(encoded))
314
cloned.Header.Set("Content-Type", "application/x-www-form-urlencoded")
315
}
316
case "multipart/form-data":
317
if values, ok := val.(map[string]interface{}); ok {
318
buffer := &bytes.Buffer{}
319
multipartWriter := multipart.NewWriter(buffer)
320
for k, v := range values {
321
// This is a file if format is binary, otherwise field
322
if property, ok := value.Schema.Value.Properties[k]; ok && property.Value.Format == "binary" {
323
if writer, err := multipartWriter.CreateFormFile(k, k); err == nil {
324
_, _ = writer.Write([]byte(types.ToString(v)))
325
}
326
} else {
327
_ = multipartWriter.WriteField(k, types.ToString(v))
328
}
329
}
330
_ = multipartWriter.Close()
331
// body = buffer.String()
332
cloned.Body = io.NopCloser(buffer)
333
cloned.ContentLength = int64(len(buffer.Bytes()))
334
cloned.Header.Set("Content-Type", multipartWriter.FormDataContentType())
335
}
336
case "text/plain":
337
str := types.ToString(val)
338
// body = str
339
cloned.Body = io.NopCloser(strings.NewReader(str))
340
cloned.ContentLength = int64(len(str))
341
cloned.Header.Set("Content-Type", "text/plain")
342
case "application/octet-stream":
343
str := types.ToString(val)
344
if str == "" {
345
// use two strings
346
str = "string1\nstring2"
347
}
348
if value.Schema != nil && generic.EqualsAny(value.Schema.Value.Format, "bindary", "byte") {
349
cloned.Body = io.NopCloser(bytes.NewReader([]byte(str)))
350
cloned.ContentLength = int64(len(str))
351
cloned.Header.Set("Content-Type", "application/octet-stream")
352
} else {
353
// use string placeholder
354
cloned.Body = io.NopCloser(strings.NewReader(str))
355
cloned.ContentLength = int64(len(str))
356
cloned.Header.Set("Content-Type", "text/plain")
357
}
358
default:
359
gologger.Verbose().Msgf("openapi: no correct content type found for body: %s\n", content)
360
// LOG: return errors.New("no correct content type found for body")
361
continue
362
}
363
364
dumped, err := httputil.DumpRequestOut(cloned, true)
365
if err != nil {
366
return errors.Wrap(err, "could not dump request")
367
}
368
369
rr, err := httpTypes.ParseRawRequestWithURL(string(dumped), cloned.URL.String())
370
if err != nil {
371
return errors.Wrap(err, "could not parse raw request")
372
}
373
opts.callback(rr)
374
continue
375
}
376
}
377
if opts.op.RequestBody != nil {
378
return nil
379
}
380
381
dumped, err := httputil.DumpRequestOut(req, true)
382
if err != nil {
383
return errors.Wrap(err, "could not dump request")
384
}
385
386
rr, err := httpTypes.ParseRawRequestWithURL(string(dumped), req.URL.String())
387
if err != nil {
388
return errors.Wrap(err, "could not parse raw request")
389
}
390
opts.callback(rr)
391
return nil
392
}
393
394
// GetGlobalParamsForSecurityRequirement returns the global parameters for a security requirement
395
func GetGlobalParamsForSecurityRequirement(schema *openapi3.T, requirement *openapi3.SecurityRequirements) ([]*openapi3.ParameterRef, error) {
396
globalParams := openapi3.NewParameters()
397
if len(schema.Components.SecuritySchemes) == 0 {
398
return nil, errkit.Newf("security requirements (%+v) without any security schemes found in openapi file", schema.Security)
399
}
400
found := false
401
// this api is protected for each security scheme pull its corresponding scheme
402
schemaLabel:
403
for _, security := range *requirement {
404
for name := range security {
405
if scheme, ok := schema.Components.SecuritySchemes[name]; ok {
406
found = true
407
param, err := GenerateParameterFromSecurityScheme(scheme)
408
if err != nil {
409
return nil, err
410
411
}
412
globalParams = append(globalParams, &openapi3.ParameterRef{Value: param})
413
continue schemaLabel
414
}
415
}
416
if !found && len(security) > 1 {
417
// if this is case then both security schemes are required
418
return nil, errkit.Newf("security requirement (%+v) not found in openapi file", security)
419
}
420
}
421
if !found {
422
return nil, errkit.Newf("security requirement (%+v) not found in openapi file", requirement)
423
}
424
425
return globalParams, nil
426
}
427
428
// GenerateParameterFromSecurityScheme generates an example from a schema object
429
func GenerateParameterFromSecurityScheme(scheme *openapi3.SecuritySchemeRef) (*openapi3.Parameter, error) {
430
if !generic.EqualsAny(scheme.Value.Type, "http", "apiKey") {
431
return nil, errkit.Newf("unsupported security scheme type (%s) found in openapi file", scheme.Value.Type)
432
}
433
if scheme.Value.Type == "http" {
434
// check scheme
435
if !generic.EqualsAny(scheme.Value.Scheme, "basic", "bearer") {
436
return nil, errkit.Newf("unsupported security scheme (%s) found in openapi file", scheme.Value.Scheme)
437
}
438
// HTTP authentication schemes basic or bearer use the Authorization header
439
headerName := scheme.Value.Name
440
if headerName == "" {
441
headerName = DEFAULT_HTTP_SCHEME_HEADER
442
}
443
// create parameters using the scheme
444
switch scheme.Value.Scheme {
445
case "basic":
446
h := openapi3.NewHeaderParameter(headerName)
447
h.Required = true
448
h.Description = globalAuth // differentiator for normal variables and global auth
449
return h, nil
450
case "bearer":
451
h := openapi3.NewHeaderParameter(headerName)
452
h.Required = true
453
h.Description = globalAuth // differentiator for normal variables and global auth
454
return h, nil
455
}
456
457
}
458
if scheme.Value.Type == "apiKey" {
459
// validate name and in
460
if scheme.Value.Name == "" {
461
return nil, errkit.Newf("security scheme (%s) name is empty", scheme.Value.Type)
462
}
463
if !generic.EqualsAny(scheme.Value.In, "query", "header", "cookie") {
464
return nil, errkit.Newf("unsupported security scheme (%s) in (%s) found in openapi file", scheme.Value.Type, scheme.Value.In)
465
}
466
// create parameters using the scheme
467
switch scheme.Value.In {
468
case "query":
469
q := openapi3.NewQueryParameter(scheme.Value.Name)
470
q.Required = true
471
q.Description = globalAuth // differentiator for normal variables and global auth
472
return q, nil
473
case "header":
474
h := openapi3.NewHeaderParameter(scheme.Value.Name)
475
h.Required = true
476
h.Description = globalAuth // differentiator for normal variables and global auth
477
return h, nil
478
case "cookie":
479
c := openapi3.NewCookieParameter(scheme.Value.Name)
480
c.Required = true
481
c.Description = globalAuth // differentiator for normal variables and global auth
482
return c, nil
483
}
484
}
485
return nil, errkit.Newf("unsupported security scheme type (%s) found in openapi file", scheme.Value.Type)
486
}
487
488