@@ -2,6 +2,7 @@ package http
22
33import (
44 "bytes"
5+ "encoding/json"
56 "fmt"
67 "io"
78 "net/http"
@@ -274,6 +275,135 @@ func addPathOp(m *map[string]*openapi3.Operation, op *openapi3.Operation, name s
274275 }
275276}
276277
278+ // collectParameters merges path-level and operation-level parameters.
279+ // Operation-level parameters override path-level ones with the same (in,name).
280+ func collectParameters (pathItem * openapi3.PathItem , op * openapi3.Operation ) []* openapi3.Parameter {
281+ var result []* openapi3.Parameter
282+ // index to handle overrides
283+ key := func (p * openapi3.Parameter ) string { return p .In + "\x00 " + p .Name }
284+ seen := map [string ]bool {}
285+
286+ if pathItem != nil {
287+ for _ , pref := range pathItem .Parameters {
288+ if pref == nil || pref .Value == nil {
289+ continue
290+ }
291+ p := pref .Value
292+ result = append (result , p )
293+ seen [key (p )] = true
294+ }
295+ }
296+
297+ if op != nil {
298+ for _ , pref := range op .Parameters {
299+ if pref == nil || pref .Value == nil {
300+ continue
301+ }
302+ p := pref .Value
303+ k := key (p )
304+ if seen [k ] {
305+ for i := range result {
306+ // override by replacing prior entry
307+ if key (result [i ]) == k {
308+ result [i ] = p
309+ // OpenAPI params are unique per operation by (in,name). An op-level param
310+ // overrides at most one path-level entry, so replace once and stop.
311+ break
312+ }
313+ }
314+ } else {
315+ result = append (result , p )
316+ seen [k ] = true
317+ }
318+ }
319+ }
320+
321+ return result
322+ }
323+
324+ func paramStringForSchema (sref * openapi3.SchemaRef ) string {
325+ if sref == nil || sref .Value == nil {
326+ return "x"
327+ }
328+ s := sref .Value
329+
330+ if len (s .Enum ) > 0 {
331+ if v , ok := s .Enum [0 ].(string ); ok {
332+ return v
333+ }
334+ return fmt .Sprint (s .Enum [0 ])
335+ }
336+
337+ switch s .Type {
338+ case "integer" , "number" :
339+ return "1"
340+ case "boolean" :
341+ return "true"
342+ case "array" :
343+ return paramStringForSchema (s .Items )
344+ case "object" :
345+ return "x"
346+ default :
347+ return "x"
348+ }
349+ }
350+
351+ func substitutePathParams (apiPath string , params []* openapi3.Parameter ) string {
352+ if ! strings .Contains (apiPath , "{" ) {
353+ return apiPath
354+ }
355+
356+ for _ , p := range params {
357+ if p == nil || p .In != "path" {
358+ continue
359+ }
360+ placeholder := "{" + p .Name + "}"
361+ if strings .Contains (apiPath , placeholder ) {
362+ apiPath = strings .ReplaceAll (apiPath , placeholder , url .PathEscape (paramStringForSchema (p .Schema )))
363+ }
364+ }
365+
366+ // fallback: strip any remaining braces
367+ if strings .Contains (apiPath , "{" ) {
368+ apiPath = strings .ReplaceAll (apiPath , "{" , "" )
369+ apiPath = strings .ReplaceAll (apiPath , "}" , "" )
370+ }
371+
372+ return apiPath
373+ }
374+
375+ func buildQueryAndHeaders (params []* openapi3.Parameter ) (string , map [string ]string ) {
376+ var parts []string
377+ headers := make (map [string ]string )
378+ for _ , p := range params {
379+ if p == nil {
380+ continue
381+ }
382+
383+ getStringValue := func (pref * openapi3.SchemaRef ) string {
384+ if pref != nil && pref .Value != nil && pref .Value .Type == "object" {
385+ // generate a small object and JSON-stringify it
386+ obj , _ := genSchemaObject (pref .Value , false )
387+ if data , err := json .Marshal (obj ); err == nil {
388+ return string (data )
389+ }
390+ return "{}"
391+ }
392+ return paramStringForSchema (pref )
393+ }
394+
395+ switch p .In {
396+ case "query" :
397+ v := getStringValue (p .Schema )
398+ parts = append (parts , url .QueryEscape (p .Name )+ "=" + url .QueryEscape (v ))
399+ case "header" :
400+ v := getStringValue (p .Schema )
401+ headers [p .Name ] = v
402+ }
403+ }
404+ return strings .Join (parts , "&" ), headers
405+ }
406+
277407func genSchemaObject (schema * openapi3.Schema , minimal bool ) (interface {}, bool ) {
278408 //todo: also need 'max' as a param to generate as many fields as possible
279409
@@ -330,7 +460,7 @@ func genSchemaObject(schema *openapi3.Schema, minimal bool) (interface{}, bool)
330460 stringVal , _ = schema .Example .(string )
331461 } else if schema .Default != nil {
332462 stringVal , _ = schema .Default .(string )
333- } else if schema . Enum != nil && len (schema .Enum ) > 0 {
463+ } else if len (schema .Enum ) > 0 {
334464 stringVal , _ = schema .Enum [0 ].(string )
335465 }
336466
@@ -393,16 +523,9 @@ func (p *CustomProbe) probeAPISpecEndpoints(proto, targetHost, port, prefix stri
393523 }
394524
395525 for apiPath , pathInfo := range spec .Paths {
396- //very primitive way to set the path params (will break for numeric values)
397- if strings .Contains (apiPath , "{" ) {
398- apiPath = strings .ReplaceAll (apiPath , "{" , "" )
526+ rawRoute := apiPath
527+ // Path param substitution is handled per operation below.
399528
400- if strings .Contains (apiPath , "}" ) {
401- apiPath = strings .ReplaceAll (apiPath , "}" , "" )
402- }
403- }
404-
405- endpoint := fmt .Sprintf ("%s%s%s" , addr , prefix , apiPath )
406529 ops := pathOps (pathInfo )
407530 for apiMethod , apiInfo := range ops {
408531 if apiInfo == nil {
@@ -412,6 +535,19 @@ func (p *CustomProbe) probeAPISpecEndpoints(proto, targetHost, port, prefix stri
412535 continue
413536 }
414537
538+ // Build endpoint for this operation using dummy path/query params
539+ params := collectParameters (pathInfo , apiInfo )
540+ finalPath := substitutePathParams (rawRoute , params )
541+ endpoint := fmt .Sprintf ("%s%s%s" , addr , prefix , finalPath )
542+ qstr , hdrs := buildQueryAndHeaders (params )
543+ if qstr != "" {
544+ if strings .Contains (endpoint , "?" ) {
545+ endpoint = endpoint + "&" + qstr
546+ } else {
547+ endpoint = endpoint + "?" + qstr
548+ }
549+ }
550+
415551 var bodyBytes []byte
416552 var contentType string
417553 var formFieldName string
@@ -510,8 +646,8 @@ func (p *CustomProbe) probeAPISpecEndpoints(proto, targetHost, port, prefix stri
510646 }
511647 }
512648
513- //make a call (no params for now)
514- if p .apiSpecEndpointCall (httpClient , endpoint , apiMethod , contentType , bodyBytes ) {
649+ //make a call
650+ if p .apiSpecEndpointCall (httpClient , endpoint , apiMethod , contentType , bodyBytes , hdrs ) {
515651 if formFieldName != "" {
516652 //trying again with a different generated body (simple hacky version)
517653 //retrying only for form data for now
@@ -532,7 +668,7 @@ func (p *CustomProbe) probeAPISpecEndpoints(proto, targetHost, port, prefix stri
532668 "op" : op ,
533669 }).Debug ("retrying.form.submit.image p.apiSpecEndpointCall" )
534670
535- if p .apiSpecEndpointCall (httpClient , endpoint , apiMethod , contentType , bodyForm .Bytes ()) &&
671+ if p .apiSpecEndpointCall (httpClient , endpoint , apiMethod , contentType , bodyForm .Bytes (), hdrs ) &&
536672 formFieldName != "" {
537673 strBody := strings .NewReader (data .DefaultTextJSON )
538674 var bodyForm * bytes.Buffer
@@ -545,7 +681,7 @@ func (p *CustomProbe) probeAPISpecEndpoints(proto, targetHost, port, prefix stri
545681 log .WithFields (log.Fields {
546682 "op" : op ,
547683 }).Debug ("retrying.form.submit.json p.apiSpecEndpointCall" )
548- p .apiSpecEndpointCall (httpClient , endpoint , apiMethod , contentType , bodyForm .Bytes ())
684+ p .apiSpecEndpointCall (httpClient , endpoint , apiMethod , contentType , bodyForm .Bytes (), hdrs )
549685 }
550686 }
551687 }
@@ -561,7 +697,7 @@ func (p *CustomProbe) probeAPISpecEndpoints(proto, targetHost, port, prefix stri
561697 "data" : string (bodyBytes ),
562698 }).Debug ("generatedSchemaObject(true)/retrying.post p.apiSpecEndpointCall" )
563699
564- p .apiSpecEndpointCall (httpClient , endpoint , apiMethod , contentType , bodyBytes )
700+ p .apiSpecEndpointCall (httpClient , endpoint , apiMethod , contentType , bodyBytes , hdrs )
565701 }
566702 }
567703 }
@@ -574,8 +710,8 @@ func (p *CustomProbe) apiSpecEndpointCall(
574710 method string ,
575711 contentType string ,
576712 bodyBytes []byte ,
713+ headers map [string ]string ,
577714) bool {
578- const op = "probe.http.CustomProbe.apiSpecEndpointCall"
579715 maxRetryCount := p .retryCount ()
580716
581717 notReadyErrorWait := time .Duration (16 )
@@ -605,7 +741,14 @@ func (p *CustomProbe) apiSpecEndpointCall(
605741 req .Header .Set (HeaderContentType , contentType )
606742 }
607743
608- //no request headers and no credentials for now
744+ for hname , hvalue := range headers {
745+ if strings .EqualFold (hname , HeaderContentType ) {
746+ continue
747+ }
748+ req .Header .Add (hname , hvalue )
749+ }
750+
751+ //no credentials for now
609752 res , err := client .Do (req )
610753 p .CallCount .Inc ()
611754
0 commit comments