Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:

build-report-ui:
name: Build wiretap monitor UI
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.x]
Expand Down
98 changes: 71 additions & 27 deletions cmd/root_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import (

"github.com/pb33f/harhar"
"github.com/pb33f/libopenapi"
v3 "github.com/pb33f/libopenapi/datamodel/high/v3"
"github.com/pb33f/libopenapi/orderedmap"
"github.com/pb33f/wiretap/har"
"github.com/pb33f/wiretap/shared"
Expand All @@ -40,7 +39,8 @@ var (

configFlag, _ := cmd.Flags().GetString("config")

var spec string
specs := make([]string, 0)
var primarySpec string
var port string
var monitorPort string
var wsPort string
Expand Down Expand Up @@ -104,8 +104,19 @@ var (
}

specFlag, _ := cmd.Flags().GetString("spec")
if specFlag != "" {
spec = specFlag
if len(specFlag) != 0 && specFlag != "" {
specs = append(specs, specFlag)
if primarySpec == "" {
primarySpec = specFlag
}
}

specsFlag, _ := cmd.Flags().GetStringSlice("specs")
if len(specsFlag) > 0 && specsFlag[0] != "" {
specs = append(specs, specsFlag...)
if primarySpec == "" {
primarySpec = specsFlag[0]
}
}

monitorPortFlag, _ := cmd.Flags().GetString("monitor-port")
Expand Down Expand Up @@ -182,8 +193,18 @@ var (
if config.StaticIndex == "" {
config.StaticIndex = staticIndex
}
if len(config.Specs) != 0 {
for _, spec := range config.Specs {
if spec != "" {
specs = append(specs, spec)
}
}
}
if config.Spec != "" {
spec = config.Spec
specs = append(specs, config.Spec)
if primarySpec == "" {
primarySpec = config.Spec
}
}
if len(staticMockDir) != 0 {
if len(config.StaticMockDir) == 0 {
Expand Down Expand Up @@ -258,16 +279,26 @@ var (
config.HARPathAllowList = harWhiteList
}

if spec == "" {
// If no primary specification has been provided, then we'll default to the first specification in the list
// Priority for primary specification is (in order):
// 1. -spec option on the CLI
// 2. First -specs option on the CLI if defined
// 3. `contract` key in the yaml config
// 4. First specification in the list final specification list
if primarySpec == "" && len(specs) != 0 {
primarySpec = specs[0]
}

if len(specs) == 0 {
pterm.Println()
pterm.Warning.Println("No OpenAPI specification provided. " +
"Please provide a path to an OpenAPI specification using the --spec or -s flags. \n" +
"Please provide a path to at least one OpenAPI specification using the --spec or -s flags. \n" +
"Without an OpenAPI specification, wiretap will not be able to validate " +
"requests and responses")
pterm.Println()
}

if (mockMode || len(config.MockModeList) > 0) && spec == "" {
if (mockMode || len(config.MockModeList) > 0) && len(specs) == 0 {
pterm.Println()
pterm.Error.Println("Cannot enable mock mode, no OpenAPI specification provided!\n" +
"Please provide a path to an OpenAPI specification using the --spec or -s flags.\n" +
Expand Down Expand Up @@ -307,8 +338,9 @@ var (
redirectBasePath = parsedURL.Path
}

if spec != "" {
config.Contract = spec
if len(specs) != 0 {
config.Contracts = specs
config.PrimaryContract = primarySpec
}
config.RedirectURL = redirectURL
config.RedirectHost = redirectHost
Expand Down Expand Up @@ -511,8 +543,8 @@ var (

// check if we want to validate the HAR file against the OpenAPI spec.
// but only if we're not in mock mode and there is a spec provided
if config.HARValidate && !config.MockMode && config.Contract != "" {
pterm.Printf("🔍 Validating HAR file against OpenAPI specification: %s\n", pterm.LightMagenta(config.Contract))
if config.HARValidate && !config.MockMode && len(config.Contracts) != 0 {
pterm.Printf("🔍 Validating HAR file against OpenAPI specification(s): %s\n", pterm.LightMagenta(config.GetContractList()))

// check if whitelist is empty, if so, fail.
if len(config.HARPathAllowList) == 0 {
Expand All @@ -531,7 +563,7 @@ var (
}

// if there is no spec, print an error
if config.HARValidate && config.Contract == "" {
if config.HARValidate && len(config.Contracts) == 0 {
pterm.Error.Println("Cannot validate HAR file against OpenAPI specification, no specification provided, use '-s'")
pterm.Println()
return nil
Expand Down Expand Up @@ -562,18 +594,18 @@ var (
}

// load the openapi spec
var doc libopenapi.Document
var docModel *libopenapi.DocumentModel[v3.Document]
var err error
if config.Contract != "" {
doc, err = loadOpenAPISpec(config.Contract, config.Base)
var primaryDoc libopenapi.Document
docs := make([]shared.ApiDocument, 0)
docModels := make([]shared.ApiDocumentModel, 0)
for _, contract := range config.Contracts {
doc, err := loadOpenAPISpec(contract, config.Base)
if err != nil {
return err
}

// build a model
var errs []error
docModel, errs = doc.BuildV3Model()
docModel, errs := doc.BuildV3Model()
if len(errs) > 0 && docModel != nil {
pterm.Warning.Printf("OpenAPI Specification loaded, but there %s %d %s detected...\n",
shared.Pluralize(len(errs), "was", "were"),
Expand All @@ -587,16 +619,27 @@ var (
pterm.Error.Printf("Failed to load / read OpenAPI specification.")
return errors.Join(errs...)
}

docs = append(docs, shared.ApiDocument{Document: doc, DocumentName: contract})
docModels = append(docModels, shared.ApiDocumentModel{
DocumentName: contract,
DocumentModel: docModel,
})

if contract == config.PrimaryContract {
primaryDoc = doc
}
}

if doc != nil {
pterm.Info.Printf("OpenAPI Specification: '%s' parsed and read\n", config.Contract)
if len(docs) != 0 {
pterm.Info.Printf("OpenAPI Specification(s): '%s' parsed and read\n", config.GetContractList())
pterm.Info.Printf("Primary OpenAPI Specification: '%s'\n", config.PrimaryContract)
}

if !config.HARValidate {

// ready to boot, let's go!
_, pErr := runWiretapService(&config, doc)
_, pErr := runWiretapService(&config, docs, primaryDoc)

if pErr != nil {
pterm.Println()
Expand All @@ -618,15 +661,15 @@ var (
}
}

validationErrors := har.ValidateHAR(harFile, &docModel.Model, &config)
validationErrors := har.ValidateHAR(harFile, docModels, &config)
if len(validationErrors) > 0 {
pterm.Println()
pterm.Error.Printf("HAR file failed validation against OpenAPI specification: %s\n", config.Contract)
pterm.Error.Printf("HAR file failed validation against OpenAPI specification(s): %s\n", config.GetContractList())
pterm.Println()

for _, e := range validationErrors {

location := pterm.Sprintf("Violation location: %s:%d:%d", pterm.LightCyan(config.Contract), e.SpecLine, e.SpecCol)
location := pterm.Sprintf("Violation location: %s:%d:%d", pterm.LightCyan(e.SpecName), e.SpecLine, e.SpecCol)
var items []pterm.BulletListItem
items = append(items, pterm.BulletListItem{
Level: 0, Text: pterm.LightRed(e.Message),
Expand All @@ -644,7 +687,7 @@ var (
if e.SchemaValidationErrors[0].Line >= 1 {
items = append(items, pterm.BulletListItem{
Level: 3, Text: pterm.Sprintf("Schema violation Location: %s:%d:%d",
pterm.LightCyan(config.Contract), e.SchemaValidationErrors[0].Line,
pterm.LightCyan(e.SpecName), e.SchemaValidationErrors[0].Line,
e.SchemaValidationErrors[0].Column),
})
}
Expand Down Expand Up @@ -702,7 +745,8 @@ func Execute(version, commit, date string, fs embed.FS) {
rootCmd.Flags().StringP("monitor-port", "m", "", "Set port on which to serve the monitor UI (default is 9091)")
rootCmd.Flags().StringP("ws-port", "w", "", "Set port on which to serve the monitor UI websocket (default is 9092)")
rootCmd.Flags().StringP("ws-host", "v", "localhost", "Set the backend hostname for wiretap, for remotely deployed service")
rootCmd.Flags().StringP("spec", "s", "", "Set the path to the OpenAPI specification to use")
rootCmd.Flags().StringP("spec", "s", "", "List of paths to the OpenAPI specification to use")
rootCmd.Flags().StringSliceP("specs", "S", []string{}, "List of paths to the OpenAPI specification to use")
rootCmd.Flags().StringP("static", "t", "", "Set the path to a directory of static files to serve")
rootCmd.Flags().StringP("static-index", "i", "index.html", "Set the index filename for static file serving (default is index.html)")
rootCmd.Flags().StringP("cert", "n", "", "Set the path to the TLS certificate to use for TLS/HTTPS")
Expand Down
6 changes: 3 additions & 3 deletions cmd/run_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import (
staticMock "github.com/pb33f/wiretap/static-mock"
)

func runWiretapService(wiretapConfig *shared.WiretapConfiguration, doc libopenapi.Document) (server.PlatformServer, error) {
func runWiretapService(wiretapConfig *shared.WiretapConfiguration, docs []shared.ApiDocument, primaryDoc libopenapi.Document) (server.PlatformServer, error) {

var err error

Expand Down Expand Up @@ -65,7 +65,7 @@ func runWiretapService(wiretapConfig *shared.WiretapConfiguration, doc libopenap
platformServer := server.NewPlatformServer(ranchConfig)

// create wiretap service
wtService := daemon.NewWiretapService(doc, wiretapConfig)
wtService := daemon.NewWiretapService(docs, wiretapConfig)

// register wiretap service
if err = platformServer.RegisterService(wtService, daemon.WiretapServiceChan); err != nil {
Expand All @@ -83,7 +83,7 @@ func runWiretapService(wiretapConfig *shared.WiretapConfiguration, doc libopenap

// register spec service
if err = platformServer.RegisterService(
specs.NewSpecService(doc), specs.SpecServiceChan); err != nil {
specs.NewSpecService(primaryDoc), specs.SpecServiceChan); err != nil {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't add support for being able to specify which specification to retrieve by this service, instead implementing the idea of a "primary specification"

We may want to have a discussion on this.

Copy link
Contributor

@serranoio serranoio May 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To me, it seems like creating a primaryDoc for the SpecService does not fully finish the feature.

If I have multiple specs loaded, I would want to see those multiple specs in the UI

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But if we merge the specs into one document, this would not be a problem 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm just thinking about use cases.

The most likely scenario when I'm using the UI is probably to view a single specification.

The most likely scenario when having multiple specifications is when it's used in CI IMO.

I just don't think it's worth the effort to implement this at this time.

panic(err)
}

Expand Down
12 changes: 6 additions & 6 deletions daemon/dto.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
package daemon

import (
"github.com/pb33f/libopenapi-validator/errors"
"github.com/pb33f/wiretap/shared"
"net/textproto"
"time"
)
Expand Down Expand Up @@ -47,11 +47,11 @@ type HttpResponse struct {
}

type HttpTransaction struct {
Request *HttpRequest `json:"httpRequest,omitempty"`
RequestValidation []*errors.ValidationError `json:"requestValidation,omitempty"`
Response *HttpResponse `json:"httpResponse,omitempty"`
ResponseValidation []*errors.ValidationError `json:"responseValidation,omitempty"`
Id string `json:"id,omitempty"`
Request *HttpRequest `json:"httpRequest,omitempty"`
RequestValidation []*shared.WiretapValidationError `json:"requestValidation,omitempty"`
Response *HttpResponse `json:"httpResponse,omitempty"`
ResponseValidation []*shared.WiretapValidationError `json:"responseValidation,omitempty"`
Id string `json:"id,omitempty"`
}

type FormPart struct {
Expand Down
13 changes: 11 additions & 2 deletions daemon/handle_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,17 @@ func (ws *WiretapService) handleMockRequest(
}
}

// build a mock based on the request.
mock, mockStatus, mockErr := ws.mockEngine.GenerateResponse(request.HttpRequest)
var mock []byte
var mockStatus int
var mockErr error

docValidator := ws.getValidatorForRequest(request)
if docValidator != nil {
mock, mockStatus, mockErr = docValidator.mockEngine.GenerateResponse(request.HttpRequest)
} else {
mockStatus = http.StatusInternalServerError
mockErr = fmt.Errorf("mock engine has not been intialized; configure an OpenAPI specification to use this option")
}

// validate http request.
ws.ValidateRequest(request, newReq)
Expand Down
5 changes: 2 additions & 3 deletions daemon/handle_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import (

"github.com/gorilla/websocket"

"github.com/pb33f/libopenapi-validator/errors"
"github.com/pb33f/ranch/model"
configModel "github.com/pb33f/wiretap/config"
"github.com/pb33f/wiretap/shared"
Expand Down Expand Up @@ -133,8 +132,8 @@ func (ws *WiretapService) handleHttpRequest(request *model.Request) {
return
}

var requestErrors []*errors.ValidationError
var responseErrors []*errors.ValidationError
var requestErrors []*shared.WiretapValidationError
var responseErrors []*shared.WiretapValidationError

ws.config.Logger.Info("[wiretap] handling API request", "url", request.HttpRequest.URL.String())

Expand Down
4 changes: 2 additions & 2 deletions daemon/stream_output.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ package daemon
import (
"fmt"
jsoniter "github.com/json-iterator/go"
"github.com/pb33f/libopenapi-validator/errors"
"github.com/pb33f/wiretap/shared"
"github.com/pterm/pterm"
"os"
"sync"
)

func (ws *WiretapService) listenForValidationErrors() {

ws.streamViolations = []*errors.ValidationError{}
ws.streamViolations = []*shared.WiretapValidationError{}
var lock sync.RWMutex
json := jsoniter.ConfigCompatibleWithStandardLibrary

Expand Down
Loading