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
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,34 @@ wiretap -u https://api.pb33f.com
wiretap -u https://api.pb33f.com -s my-openapi-spec.yaml
```

## Multi-spec mode

Got more than one OpenAPI contract? Hand `wiretap` a list, or point it at a directory and let it discover them.

```shell
wiretap -u https://api.pb33f.com --specs ./users.yaml,./orders.yaml,./billing.yaml
```

```shell
wiretap -u https://api.pb33f.com --spec-dir ./contracts
```

`wiretap` routes each request to the spec that owns it, and reports any duplicate or ambiguous routes across your contracts at startup.

Use `--dry-run` to discover, analyze, and print the conflict report without starting the proxy. Exits non-zero on conflicts or load errors — perfect for CI.

```shell
wiretap --spec-dir ./contracts --dry-run
```

Full details: [Multi-spec mode](https://pb33f.io/wiretap/multi-spec-mode/).

# Documentation

- 🚀 [Quick Start](https://pb33f.io/wiretap/quickstart/) 🚀
- [Installing](https://pb33f.io/wiretap/quickstart/)
- [Configuring](https://pb33f.io/wiretap/configuring/)
- [Multi-spec mode](https://pb33f.io/wiretap/multi-spec-mode/)
- [Monitor UI](https://pb33f.io/wiretap/monitor/)
- [Serving static content](https://pb33f.io/wiretap/static-content/)
- [GiftShop example API](https://pb33f.io/wiretap/giftshop-api/)
Expand Down
52 changes: 24 additions & 28 deletions cmd/booted_message.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley
// SPDX-License-Identifier: AGPL
// SPDX-License-Identifier: BUSL-1.1

package cmd

import (
"fmt"

"github.com/pb33f/doctor/terminal"
"github.com/pb33f/ranch/bus"
"github.com/pb33f/ranch/model"
"github.com/pb33f/ranch/plank/pkg/server"
"github.com/pb33f/wiretap/shared"
"github.com/pterm/pterm"
)

func bootedMessage(wiretapConfig *shared.WiretapConfiguration, eventBus bus.EventBus) {
Expand All @@ -19,47 +21,41 @@ func bootedMessage(wiretapConfig *shared.WiretapConfiguration, eventBus bus.Even
handler.Handle(func(message *model.Message) {
if !seen {
seen = true
pterm.Println()
fmt.Println()

b1 := pterm.DefaultBox.WithTitle(pterm.LightMagenta("API Gateway")).Sprint(wiretapConfig.GetApiGateway())
b2 := pterm.DefaultBox.WithTitle(pterm.LightMagenta("Monitor UI")).Sprint(wiretapConfig.GetMonitorUI())
b3 := pterm.DefaultBox.WithTitle(pterm.LightMagenta("Static files served from")).Sprint(wiretapConfig.StaticDir)
b1 := terminal.RenderBox(wiretapConfig.GetApiGateway(), terminal.BoxOptions{Title: style.Secondary("API Gateway")})
b2 := terminal.RenderBox(wiretapConfig.GetMonitorUI(), terminal.BoxOptions{Title: style.Secondary("Monitor UI")})
b3 := terminal.RenderBox(wiretapConfig.StaticDir, terminal.BoxOptions{Title: style.Secondary("Static files served from")})

var pp *pterm.PanelPrinter
var panels string
if wiretapConfig.StaticDir != "" {
pp = pterm.DefaultPanel.WithPanels(pterm.Panels{
{{Data: b1}, {Data: b2}, {Data: b3}},
})
panels = terminal.RenderPanelGrid([][]string{{b1, b2, b3}}, terminal.PanelOptions{Gap: 2})
} else {
pp = pterm.DefaultPanel.WithPanels(pterm.Panels{
{{Data: b1}, {Data: b2}},
})
panels = terminal.RenderPanelGrid([][]string{{b1, b2}}, terminal.PanelOptions{Gap: 2})
}
panels, _ := pp.Srender()

pterm.DefaultBox.WithTitle(pterm.LightCyan("wiretap is online!")).
WithTitleTopLeft().
WithRightPadding(3).
WithTopPadding(1).
WithLeftPadding(3).
WithBottomPadding(0).
Println(panels)
fmt.Println(terminal.RenderBox(panels, terminal.BoxOptions{
Title: style.Primary("wiretap is online!"),
PaddingLeft: 3,
PaddingRight: 3,
PaddingTop: 1,
}))

pterm.Println()
fmt.Println()
if wiretapConfig.MockMode {
pterm.Info.Printf("Ⓜ️ Mock mode: wiretap is not proxying any traffic, all responses are %s.\n",
pterm.LightMagenta("generated mocks/simulations"))
cliLog.Info(fmt.Sprintf("Ⓜ️ Mock mode: wiretap is not proxying any traffic, all responses are %s.",
style.Secondary("generated mocks/simulations")))

} else {
if wiretapConfig.RedirectURL != "" {
pterm.Info.Printf("wiretap is proxying all traffic to '%s'\n",
pterm.LightMagenta(wiretapConfig.RedirectURL))
cliLog.Info(fmt.Sprintf("wiretap is proxying all traffic to '%s'",
style.Secondary(wiretapConfig.RedirectURL)))
} else {
pterm.Info.Printf("no redirect URL configured, wiretap is not operating as a proxy\n")
cliLog.Info("no redirect URL configured, wiretap is not operating as a proxy")
}
}

pterm.Println()
fmt.Println()
}
}, nil)
}()
Expand Down
7 changes: 3 additions & 4 deletions cmd/handle_http_traffic.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley
// SPDX-License-Identifier: AGPL
// SPDX-License-Identifier: BUSL-1.1

package cmd

Expand All @@ -13,7 +13,6 @@ import (
"github.com/pb33f/wiretap/daemon"
"github.com/pb33f/wiretap/shared"
staticMock "github.com/pb33f/wiretap/static-mock"
"github.com/pterm/pterm"
)

type HandleHttpTraffic struct {
Expand Down Expand Up @@ -66,7 +65,7 @@ func handleHttpTraffic(hht *HandleHttpTraffic) {
mux.HandleFunc(websocket, handleWebsocket)
}

pterm.Info.Println(pterm.LightMagenta(fmt.Sprintf("API Gateway UI booting on port %s...", wiretapConfig.Port)))
commandLogger(wiretapConfig).Info(fmt.Sprintf("API Gateway UI booting on port %s...", wiretapConfig.Port))

var httpErr error
if wiretapConfig.CertificateKey != "" && wiretapConfig.Certificate != "" {
Expand All @@ -79,7 +78,7 @@ func handleHttpTraffic(hht *HandleHttpTraffic) {
}

if httpErr != nil {
pterm.Error.Println(httpErr)
commandLogger(wiretapConfig).Error(httpErr.Error())
}
}()
}
53 changes: 45 additions & 8 deletions cmd/load_specification.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley
// SPDX-License-Identifier: AGPL
// SPDX-License-Identifier: BUSL-1.1

package cmd

import (
"fmt"

"github.com/pb33f/doctor/terminal"
"github.com/pb33f/libopenapi"
"github.com/pb33f/libopenapi/datamodel"
"github.com/pterm/pterm"
"github.com/pb33f/wiretap/shared"
"github.com/pb33f/wiretap/specs"
"io"
"log/slog"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
)

Expand All @@ -21,7 +25,7 @@ func loadOpenAPISpec(contract, base string) (libopenapi.Document, error) {

if strings.HasPrefix(contract, "http://") || strings.HasPrefix(contract, "https://") {
if docUrl, err := url.Parse(contract); err == nil {
pterm.Info.Printf("Fetching OpenAPI Specification from URL: '%s'\n", docUrl.String())
cliLog.Info(fmt.Sprintf("Fetching OpenAPI Specification from URL: '%s'", docUrl.String()))
resp, er := http.Get(docUrl.String())
if er != nil {
return nil, er
Expand Down Expand Up @@ -57,18 +61,51 @@ func loadOpenAPISpec(contract, base string) (libopenapi.Document, error) {
if strings.HasPrefix(base, "http") {
u, _ := url.Parse(base)
if u != nil {
pterm.Info.Printf("Setting OpenAPI reference base URL to: '%s'\n", u.String())
cliLog.Debug(fmt.Sprintf("Setting OpenAPI reference base URL to: '%s'", u.String()))
docConfig.BaseURL = u
}
} else {
pterm.Info.Printf("Setting OpenAPI reference base path to: '%s'\n", base)
cliLog.Debug(fmt.Sprintf("Setting OpenAPI reference base path to: '%s'", base))
docConfig.BasePath = base
}
}

handler := pterm.NewSlogHandler(&pterm.DefaultLogger)
docConfig.Logger = slog.New(handler)
pterm.DefaultLogger.Level = pterm.LogLevelError
docConfig.Logger = terminal.NewCLIPrettyLogger(os.Stdout, slog.LevelError)

return libopenapi.NewDocumentWithConfiguration(specBytes, docConfig)
}

func loadAllSpecs(paths []string, base string) ([]shared.ApiDocument, []specs.LoadError) {
docs := make([]shared.ApiDocument, 0, len(paths))
var loadErrors []specs.LoadError

for _, contract := range paths {
specBase := base
if specBase == "" && !strings.HasPrefix(contract, "http://") && !strings.HasPrefix(contract, "https://") {
specBase = filepath.Dir(contract)
}
doc, err := loadOpenAPISpec(contract, specBase)
if err != nil {
loadErrors = append(loadErrors, specs.LoadError{Spec: contract, Error: err})
continue
}

docModel, docErr := doc.BuildV3Model()
if docErr != nil && docModel != nil {
cliLog.Warn("OpenAPI Specification loaded, but there was an issue detected...")
cliLog.Warn(fmt.Sprintf("--> %s", docErr.Error()))
}
if docErr != nil && docModel == nil {
loadErrors = append(loadErrors, specs.LoadError{Spec: contract, Error: docErr})
continue
}

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

return docs, loadErrors
}
31 changes: 12 additions & 19 deletions cmd/print_banner.go
Original file line number Diff line number Diff line change
@@ -1,28 +1,21 @@
// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley
// SPDX-License-Identifier: AGPL
// SPDX-License-Identifier: BUSL-1.1

package cmd

import (
"fmt"
"github.com/pterm/pterm"
"os"

"github.com/pb33f/doctor/terminal"
)

func PrintBanner() {
text := `
@@@@@@@ @@@@@@@ @@@@@@ @@@@@@ @@@@@@@@
@@@@@@@@ @@@@@@@@ @@@@@@@ @@@@@@@ @@@@@@@@
@@! @@@ @@! @@@ @@@ @@@ @@!
!@! @!@ !@ @!@ @!@ @!@ !@!
@!@@!@! @!@!@!@ @!@!!@ @!@!!@ @!!!:!
!!@!!! !!!@!!!! !!@!@! !!@!@! !!!!!:
!!: !!: !!! !!: !!: !!:
:!: :!: !:! :!: :!: :!:
:: :: :::: :: :::: :: :::: ::
: :: : :: : : : : : : :`
pterm.DefaultBasicText.Println(pterm.LightMagenta(text))
pterm.Print(pterm.LightCyan(fmt.Sprintf("wiretap version: %s", Version)))
pterm.Println(pterm.LightMagenta(fmt.Sprintf(" | compiled: %s", Date)))
pterm.Println(pterm.LightCyan("Designed and built by Princess Beef Heavy Industries: https://pb33f.io/wiretap"))
pterm.Println()
terminal.PrintBanner(terminal.BannerOptions{
Writer: os.Stdout,
Palette: terminal.PaletteForTheme(terminal.ThemeDark),
ProductName: "wiretap",
ProductURL: "Designed and built by Princess Beef Heavy Industries, LLC (pb33f): https://pb33f.io/wiretap",
Version: Version,
Date: Date,
})
}
Loading
Loading