Compare commits

..

1 Commits

Author SHA1 Message Date
0d68d7d7b2 Bump json5 from 1.0.1 to 1.0.2 in /admin
Bumps [json5](https://github.com/json5/json5) from 1.0.1 to 1.0.2.
- [Release notes](https://github.com/json5/json5/releases)
- [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/json5/json5/compare/v1.0.1...v1.0.2)

---
updated-dependencies:
- dependency-name: json5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-08 04:10:07 +00:00
68 changed files with 14024 additions and 6958 deletions

View File

@ -5,7 +5,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
go: ["1.23", "1.22", "1.21"]
go: ["1.17", "1.16"]
name: Go ${{ matrix.go }} - Build
steps:
- uses: actions/checkout@v2
@ -34,7 +34,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
go: ["1.23", "1.22", "1.21"]
go: ["1.17", "1.16"]
name: Go ${{ matrix.go }} - Test
steps:
- uses: actions/checkout@v2

View File

@ -24,7 +24,7 @@ linters:
- maligned
- nilnil
- nlreturn
- scopelint
- scopelint
- testpackage
- varnamelen
- wrapcheck
@ -35,19 +35,19 @@ linters-settings:
godot:
capital: true
ireturn:
allow: "error,empty,anon,stdlib,.*(or|er)$,github.com/dstotijn/hetty/pkg/filter.Expression"
allow: "error,empty,anon,stdlib,.*(or|er)$,github.com/99designs/gqlgen/graphql.Marshaler,github.com/dstotijn/hetty/pkg/api.QueryResolver,github.com/dstotijn/hetty/pkg/filter.Expression"
issues:
exclude-rules:
- linters:
- gosec
- gosec
# Ignore SHA1 usage.
text: "G(401|505):"
- linters:
- wsl
- wsl
# Ignore cuddled defer statements.
text: "only one cuddle assignment allowed before defer statement"
- linters:
- nlreturn
- nlreturn
# Ignore `break` without leading blank line.
text: "break with no blank line before"
text: "break with no blank line before"

View File

@ -96,12 +96,12 @@ $ hetty --help
Usage:
hetty [flags] [subcommand] [flags]
Runs an HTTP server with (MITM) proxy and a web based admin interface.
Runs an HTTP server with (MITM) proxy, GraphQL service, and a web based admin interface.
Options:
--cert Path to root CA certificate. Creates file if it doesn't exist. (Default: "~/.hetty/hetty_cert.pem")
--key Path to root CA private key. Creates file if it doesn't exist. (Default: "~/.hetty/hetty_key.pem")
--db Database file path. Creates file if it doesn't exist. (Default: "~/.hetty/hetty.db")
--db Database directory path. (Default: "~/.hetty/db")
--addr TCP address for HTTP server to listen on, in the form \"host:port\". (Default: ":8080")
--chrome Launch Chrome with proxy settings applied and certificate errors ignored. (Default: false)
--verbose Enable verbose logging.
@ -146,10 +146,14 @@ Guidelines](CONTRIBUTING.md) for details.
## Sponsors
<p><a href="https://www.tines.com/?utm_source=oss&utm_medium=sponsorship&utm_campaign=hetty">
<img src="https://hetty.xyz/img/tines-sponsorship-badge.png" width="140" alt="Sponsored by Tines">
</a></p>
💖 Are you enjoying Hetty? You can [sponsor me](https://github.com/sponsors/dstotijn)!
## License
[MIT](LICENSE)
© 20192025 Hetty Software
© 2022 Hetty Software

View File

@ -3806,9 +3806,9 @@ json-to-pretty-yaml@^1.2.2:
remove-trailing-spaces "^1.0.6"
json5@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe"
integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==
version "1.0.2"
resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593"
integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==
dependencies:
minimist "^1.2.0"
@ -4242,9 +4242,9 @@ minimatch@^3.0.4, minimatch@^3.1.2:
brace-expansion "^1.1.7"
minimist@^1.2.0, minimist@^1.2.6, minimist@~1.2.5:
version "1.2.6"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
version "1.2.7"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18"
integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==
mkdirp@^1.0.4:
version "1.0.4"

View File

@ -1,10 +0,0 @@
version: v2
plugins:
- local: protoc-gen-go
out: pkg
opt: paths=source_relative
- local: protoc-gen-connect-go
out: pkg
opt:
- paths=source_relative
- package_suffix # Generate `*.connect.go` files next to `*.pb.go` files.

View File

@ -1,12 +0,0 @@
version: v2
modules:
- path: proto
lint:
use:
- STANDARD
except:
- PACKAGE_DIRECTORY_MATCH
- PACKAGE_VERSION_SUFFIX
breaking:
use:
- PACKAGE

View File

@ -16,14 +16,16 @@ import (
"strings"
"github.com/chromedp/chromedp"
badgerdb "github.com/dgraph-io/badger/v3"
"github.com/gorilla/mux"
"github.com/mitchellh/go-homedir"
"github.com/peterbourgon/ff/v3/ffcli"
"go.etcd.io/bbolt"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"github.com/dstotijn/hetty/pkg/api"
"github.com/dstotijn/hetty/pkg/chrome"
"github.com/dstotijn/hetty/pkg/db/bolt"
"github.com/dstotijn/hetty/pkg/db/badger"
"github.com/dstotijn/hetty/pkg/proj"
"github.com/dstotijn/hetty/pkg/proxy"
"github.com/dstotijn/hetty/pkg/proxy/intercept"
@ -44,12 +46,12 @@ var hettyUsage = `
Usage:
hetty [flags] [subcommand] [flags]
Runs an HTTP server with (MITM) proxy and a web based admin interface.
Runs an HTTP server with (MITM) proxy, GraphQL service, and a web based admin interface.
Options:
--cert Path to root CA certificate. Creates file if it doesn't exist. (Default: "~/.hetty/hetty_cert.pem")
--key Path to root CA private key. Creates file if it doesn't exist. (Default: "~/.hetty/hetty_key.pem")
--db Database file path. Creates file if it doesn't exist. (Default: "~/.hetty/hetty.db")
--db Database directory path. (Default: "~/.hetty/db")
--addr TCP address for HTTP server to listen on, in the form \"host:port\". (Default: ":8080")
--chrome Launch Chrome with proxy settings applied and certificate errors ignored. (Default: false)
--verbose Enable verbose logging.
@ -87,7 +89,7 @@ func NewHettyCommand() (*ffcli.Command, *Config) {
"Path to root CA certificate. Creates a new certificate if file doesn't exist.")
fs.StringVar(&cmd.key, "key", "~/.hetty/hetty_key.pem",
"Path to root CA private key. Creates a new private key if file doesn't exist.")
fs.StringVar(&cmd.db, "db", "~/.hetty/hetty.db", "Database file path. Creates file if it doesn't exist.")
fs.StringVar(&cmd.db, "db", "~/.hetty/db", "Database directory path.")
fs.StringVar(&cmd.addr, "addr", ":8080", "TCP address to listen on, in the form \"host:port\".")
fs.BoolVar(&cmd.chrome, "chrome", false, "Launch Chrome with proxy settings applied and certificate errors ignored.")
fs.BoolVar(&cmd.version, "version", false, "Output version.")
@ -152,21 +154,25 @@ func (cmd *HettyCommand) Exec(ctx context.Context, _ []string) error {
cmd.config.logger.Fatal("Failed to load or create CA key pair.", zap.Error(err))
}
dbLogger := cmd.config.logger.Named("boltdb").Sugar()
boltOpts := *bbolt.DefaultOptions
boltOpts.Logger = &bolt.Logger{SugaredLogger: dbLogger}
// BadgerDB logs some verbose entries with `INFO` level, so unless
// we're running in debug mode, bump the minimal level to `WARN`.
dbLogger := cmd.config.logger.Named("badgerdb").WithOptions(zap.IncreaseLevel(zapcore.WarnLevel))
boltDB, err := bolt.OpenDatabase(dbPath, &boltOpts)
dbSugaredLogger := dbLogger.Sugar()
badger, err := badger.OpenDatabase(
badgerdb.DefaultOptions(dbPath).WithLogger(badger.NewLogger(dbSugaredLogger)),
)
if err != nil {
cmd.config.logger.Fatal("Failed to open database.", zap.Error(err))
}
defer boltDB.Close()
defer badger.Close()
scope := &scope.Scope{}
reqLogService := reqlog.NewService(reqlog.Config{
Scope: scope,
Repository: boltDB,
Repository: badger,
Logger: cmd.config.logger.Named("reqlog").Sugar(),
})
@ -175,12 +181,12 @@ func (cmd *HettyCommand) Exec(ctx context.Context, _ []string) error {
})
senderService := sender.NewService(sender.Config{
Repository: boltDB,
Repository: badger,
ReqLogService: reqLogService,
})
projService, err := proj.NewService(proj.Config{
Repository: boltDB,
Repository: badger,
InterceptService: interceptService,
ReqLogService: reqLogService,
SenderService: senderService,
@ -228,11 +234,14 @@ func (cmd *HettyCommand) Exec(ctx context.Context, _ []string) error {
req.Method != http.MethodConnect && !strings.HasPrefix(req.RequestURI, "http://")
}).Subrouter().StrictSlash(true)
// Connect RPC server.
projPath, projHandler := proj.NewProjectServiceHandler(projService)
adminRouter.PathPrefix(projPath).Handler(projHandler)
reqlogPath, reqlogHandler := reqlog.NewHttpRequestLogServiceHandler(reqLogService)
adminRouter.PathPrefix(reqlogPath).Handler(reqlogHandler)
// GraphQL server.
gqlEndpoint := "/api/graphql/"
adminRouter.Path(gqlEndpoint).Handler(api.HTTPHandler(&api.Resolver{
ProjectService: projService,
RequestLogService: reqLogService,
InterceptService: interceptService,
SenderService: senderService,
}, gqlEndpoint))
// Admin interface.
adminRouter.PathPrefix("").Handler(adminHandler)

43
go.mod
View File

@ -1,34 +1,59 @@
module github.com/dstotijn/hetty
go 1.23
toolchain go1.23.4
go 1.17
require (
connectrpc.com/connect v1.18.1
github.com/99designs/gqlgen v0.14.0
github.com/chromedp/chromedp v0.7.8
github.com/google/go-cmp v0.5.9
github.com/dgraph-io/badger/v3 v3.2103.2
github.com/google/go-cmp v0.5.6
github.com/gorilla/mux v1.7.4
github.com/matryer/moq v0.2.5
github.com/mitchellh/go-homedir v1.1.0
github.com/oklog/ulid/v2 v2.1.0
github.com/oklog/ulid v1.3.1
github.com/peterbourgon/ff/v3 v3.1.2
github.com/smallstep/truststore v0.11.0
go.etcd.io/bbolt v1.4.0-beta.0
github.com/vektah/gqlparser/v2 v2.2.0
go.uber.org/zap v1.21.0
google.golang.org/protobuf v1.36.3
)
require (
github.com/agnivade/levenshtein v1.1.0 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.1.1 // indirect
github.com/chromedp/cdproto v0.0.0-20220217222649-d8c14a5c6edf // indirect
github.com/chromedp/sysutil v1.0.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect
github.com/dgraph-io/ristretto v0.1.0 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.1.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 // indirect
github.com/golang/protobuf v1.3.1 // indirect
github.com/golang/snappy v0.0.3 // indirect
github.com/google/flatbuffers v1.12.1 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/hashicorp/golang-lru v0.5.1 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/klauspost/compress v1.12.3 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mitchellh/mapstructure v1.1.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/russross/blackfriday/v2 v2.0.1 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/urfave/cli/v2 v2.1.1 // indirect
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e // indirect
go.opencensus.io v0.22.5 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/mod v0.4.2 // indirect
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 // indirect
golang.org/x/sys v0.0.0-20220209214540-3681064d5158 // indirect
golang.org/x/tools v0.1.5 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
gopkg.in/yaml.v2 v2.2.8 // indirect
howett.net/plist v0.0.0-20181124034731-591f970eefbb // indirect
)

200
go.sum
View File

@ -1,42 +1,120 @@
connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw=
connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/99designs/gqlgen v0.14.0 h1:Wg8aNYQUjMR/4v+W3xD+7SizOy6lSvVeQ06AobNQAXI=
github.com/99designs/gqlgen v0.14.0/go.mod h1:S7z4boV+Nx4VvzMUpVrY/YuHjFX4n7rDyuTqvAkuoRE=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
github.com/agnivade/levenshtein v1.1.0 h1:n6qGwyHG61v3ABce1rPVZklEYRT8NFpCMrpZdBUbYGM=
github.com/agnivade/levenshtein v1.1.0/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chromedp/cdproto v0.0.0-20220217222649-d8c14a5c6edf h1:1omDWNUsWxn2HpiMiMuyRmzjl9uG7RP3IE6GTlpgJWU=
github.com/chromedp/cdproto v0.0.0-20220217222649-d8c14a5c6edf/go.mod h1:At5TxYYdxkbQL0TSefRjhLE3Q0lgvqKKMSFUglJ7i1U=
github.com/chromedp/chromedp v0.7.8 h1:JFPIFb28LPjcx6l6mUUzLOTD/TgswcTtg7KrDn8S/2I=
github.com/chromedp/chromedp v0.7.8/go.mod h1:HcIUFBa5vA+u2QI3+xljiU59llUQ8lgGoLzYSCBfmUA=
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgraph-io/badger/v3 v3.2103.2 h1:dpyM5eCJAtQCBcMCZcT4UBZchuTJgCywerHHgmxfxM8=
github.com/dgraph-io/badger/v3 v3.2103.2/go.mod h1:RHo4/GmYcKKh5Lxu63wLEMHJ70Pac2JqZRYGhlyAo2M=
github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI=
github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g=
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA=
github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw=
github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.12.3 h1:G5AfA94pHPysR56qqrkO2pxEexdDzrpFJ6yt/VqWxVU=
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
github.com/matryer/moq v0.2.5 h1:BGQISyhl7Gc9W/gMYmAJONh9mT6AYeyeTjNupNPknMs=
github.com/matryer/moq v0.2.5/go.mod h1:9RtPYjTnH1bSBIkpvtHkFN7nbWAnO7oRpdJkEIn6UtE=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU=
github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/orisano/pixelmatch v0.0.0-20210112091706-4fa4c7ba91d5 h1:1SoBaSPudixRecmlHXb/GxmaD3fLMtHIDN13QujwQuc=
github.com/orisano/pixelmatch v0.0.0-20210112091706-4fa4c7ba91d5/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
github.com/peterbourgon/ff/v3 v3.1.2 h1:0GNhbRhO9yHA4CC27ymskOsuRpmX0YQxwxM9UPiP6JM=
github.com/peterbourgon/ff/v3 v3.1.2/go.mod h1:XNJLY8EIl6MjMVjBS4F0+G0LYoAqs0DTa4rmHHukKDE=
@ -45,16 +123,49 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
github.com/smallstep/truststore v0.11.0 h1:JUTkQ4oHr40jHTS/A2t0usEhteMWG+45CDD2iJA/dIk=
github.com/smallstep/truststore v0.11.0/go.mod h1:HwHKRcBi0RUxxw1LYDpTRhYC4jZUuxPpkHdVonlkoDM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k=
github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e h1:+w0Zm/9gaWpEAyDlU1eKOuk5twTjAjuevXqcJJw8hrg=
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U=
github.com/vektah/gqlparser/v2 v2.2.0 h1:bAc3slekAAJW6sZTi07aGq0OrfaCjj4jxARAaC7g2EM=
github.com/vektah/gqlparser/v2 v2.2.0/go.mod h1:i3mQIGIrbK2PD1RrCeMTlVbkF2FJ6WkU1KJlJlC+3F4=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.etcd.io/bbolt v1.4.0-beta.0 h1:U7Y9yH6ZojEo5/BDFMXDXD1RNx9L7iKxudzqR68jLaM=
go.etcd.io/bbolt v1.4.0-beta.0/go.mod h1:Qv5yHB6jkQESXT/uVfxJgUPMqgAyhL0GLxcQaz9bSec=
go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
@ -63,52 +174,95 @@ go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8=
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158 h1:rm+CHSpPEEW2IsXUib1ThaHIjuBVZjxNgSKmBLFfD4c=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200815165600-90abf76919f3/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
howett.net/plist v0.0.0-20181124034731-591f970eefbb h1:jhnBjNi9UFpfpl8YZhA9CrOqpnJdvzuiHsl/dnxl11M=
howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=
sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67/go.mod h1:L5q+DGLGOQFpo1snNEkLOJT2d1YTW66rWNzatr3He1k=

56
gqlgen.yml Normal file
View File

@ -0,0 +1,56 @@
# Where are all the schema files located? globs are supported eg src/**/*.graphqls
schema:
- pkg/api/schema.graphql
# Where should the generated server code go?
exec:
filename: pkg/api/generated.go
package: api
# Uncomment to enable federation
# federation:
# filename: graph/generated/federation.go
# package: generated
# Where should any generated models go?
model:
filename: pkg/api/models_gen.go
package: api
# Where should the resolver implementations go?
resolver:
layout: single-file
filename: pkg/api/resolvers.go
dir: pkg/api
package: api
# Optional: turn on use `gqlgen:"fieldName"` tags in your models
# struct_tag: json
# Optional: turn on to use []Thing instead of []*Thing
omit_slice_element_pointers: true
# Optional: set to speed up generation time by not performing a final validation pass.
# skip_validation: true
# gqlgen will search for any type names in the schema in these go packages
# if they match it will use them, otherwise it will generate them.
# autobind:
# - "github.com/dstotijn/hetty/graph/model"
# This section declares type mapping between the GraphQL and go type systems
#
# The first line in each type will be used as defaults for resolver arguments and
# modelgen, the others will be allowed when binding to fields. Configure them to
# your liking
models:
ID:
model:
- github.com/dstotijn/hetty/pkg/api.ULID
URL:
model:
- github.com/dstotijn/hetty/pkg/api.URL
# Int:
# model:
# - github.com/99designs/gqlgen/graphql.Int
# - github.com/99designs/gqlgen/graphql.Int64
# - github.com/99designs/gqlgen/graphql.Int32

8995
pkg/api/generated.go Normal file

File diff suppressed because it is too large Load Diff

21
pkg/api/http.go Normal file
View File

@ -0,0 +1,21 @@
package api
import (
"net/http"
"github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/playground"
"github.com/gorilla/mux"
)
func HTTPHandler(resolver *Resolver, gqlEndpoint string) http.Handler {
router := mux.NewRouter().SkipClean(true)
router.Methods("POST").Handler(
handler.NewDefaultServer(NewExecutableSchema(Config{
Resolvers: resolver,
})),
)
router.Methods("GET").Handler(playground.Handler("GraphQL Playground", gqlEndpoint))
return router
}

65
pkg/api/models.go Normal file
View File

@ -0,0 +1,65 @@
package api
import (
"fmt"
"io"
"net/url"
"strconv"
"github.com/99designs/gqlgen/graphql"
"github.com/oklog/ulid"
)
func MarshalULID(u ulid.ULID) graphql.Marshaler {
return graphql.WriterFunc(func(w io.Writer) {
fmt.Fprint(w, strconv.Quote(u.String()))
})
}
func UnmarshalULID(v interface{}) (ulid.ULID, error) {
rawULID, ok := v.(string)
if !ok {
return ulid.ULID{}, fmt.Errorf("ulid must be a string")
}
u, err := ulid.Parse(rawULID)
if err != nil {
return ulid.ULID{}, fmt.Errorf("failed to parse ULID: %w", err)
}
return u, nil
}
func MarshalURL(u *url.URL) graphql.Marshaler {
return graphql.WriterFunc(func(w io.Writer) {
fmt.Fprint(w, strconv.Quote(u.String()))
})
}
func UnmarshalURL(v interface{}) (*url.URL, error) {
rawURL, ok := v.(string)
if !ok {
return nil, fmt.Errorf("url must be a string")
}
u, err := url.Parse(rawURL)
if err != nil {
return nil, fmt.Errorf("failed to parse URL: %w", err)
}
return u, nil
}
type HTTPHeaders []HTTPHeader
func (h HTTPHeaders) Len() int {
return len(h)
}
func (h HTTPHeaders) Less(i, j int) bool {
return h[i].Key < h[j].Key
}
func (h HTTPHeaders) Swap(i, j int) {
h[i], h[j] = h[j], h[i]
}

301
pkg/api/models_gen.go Normal file
View File

@ -0,0 +1,301 @@
// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.
package api
import (
"fmt"
"io"
"net/url"
"strconv"
"time"
"github.com/oklog/ulid"
)
type CancelRequestResult struct {
Success bool `json:"success"`
}
type CancelResponseResult struct {
Success bool `json:"success"`
}
type ClearHTTPRequestLogResult struct {
Success bool `json:"success"`
}
type CloseProjectResult struct {
Success bool `json:"success"`
}
type DeleteProjectResult struct {
Success bool `json:"success"`
}
type DeleteSenderRequestsResult struct {
Success bool `json:"success"`
}
type HTTPHeader struct {
Key string `json:"key"`
Value string `json:"value"`
}
type HTTPHeaderInput struct {
Key string `json:"key"`
Value string `json:"value"`
}
type HTTPRequest struct {
ID ulid.ULID `json:"id"`
URL *url.URL `json:"url"`
Method HTTPMethod `json:"method"`
Proto HTTPProtocol `json:"proto"`
Headers []HTTPHeader `json:"headers"`
Body *string `json:"body"`
Response *HTTPResponse `json:"response"`
}
type HTTPRequestLog struct {
ID ulid.ULID `json:"id"`
URL string `json:"url"`
Method HTTPMethod `json:"method"`
Proto string `json:"proto"`
Headers []HTTPHeader `json:"headers"`
Body *string `json:"body"`
Timestamp time.Time `json:"timestamp"`
Response *HTTPResponseLog `json:"response"`
}
type HTTPRequestLogFilter struct {
OnlyInScope bool `json:"onlyInScope"`
SearchExpression *string `json:"searchExpression"`
}
type HTTPRequestLogFilterInput struct {
OnlyInScope *bool `json:"onlyInScope"`
SearchExpression *string `json:"searchExpression"`
}
type HTTPResponse struct {
// Will be the same ID as its related request ID.
ID ulid.ULID `json:"id"`
Proto HTTPProtocol `json:"proto"`
StatusCode int `json:"statusCode"`
StatusReason string `json:"statusReason"`
Body *string `json:"body"`
Headers []HTTPHeader `json:"headers"`
}
type HTTPResponseLog struct {
// Will be the same ID as its related request ID.
ID ulid.ULID `json:"id"`
Proto HTTPProtocol `json:"proto"`
StatusCode int `json:"statusCode"`
StatusReason string `json:"statusReason"`
Body *string `json:"body"`
Headers []HTTPHeader `json:"headers"`
}
type InterceptSettings struct {
RequestsEnabled bool `json:"requestsEnabled"`
ResponsesEnabled bool `json:"responsesEnabled"`
RequestFilter *string `json:"requestFilter"`
ResponseFilter *string `json:"responseFilter"`
}
type ModifyRequestInput struct {
ID ulid.ULID `json:"id"`
URL *url.URL `json:"url"`
Method HTTPMethod `json:"method"`
Proto HTTPProtocol `json:"proto"`
Headers []HTTPHeaderInput `json:"headers"`
Body *string `json:"body"`
ModifyResponse *bool `json:"modifyResponse"`
}
type ModifyRequestResult struct {
Success bool `json:"success"`
}
type ModifyResponseInput struct {
RequestID ulid.ULID `json:"requestID"`
Proto HTTPProtocol `json:"proto"`
Headers []HTTPHeaderInput `json:"headers"`
Body *string `json:"body"`
StatusCode int `json:"statusCode"`
StatusReason string `json:"statusReason"`
}
type ModifyResponseResult struct {
Success bool `json:"success"`
}
type Project struct {
ID ulid.ULID `json:"id"`
Name string `json:"name"`
IsActive bool `json:"isActive"`
Settings *ProjectSettings `json:"settings"`
}
type ProjectSettings struct {
Intercept *InterceptSettings `json:"intercept"`
}
type ScopeHeader struct {
Key *string `json:"key"`
Value *string `json:"value"`
}
type ScopeHeaderInput struct {
Key *string `json:"key"`
Value *string `json:"value"`
}
type ScopeRule struct {
URL *string `json:"url"`
Header *ScopeHeader `json:"header"`
Body *string `json:"body"`
}
type ScopeRuleInput struct {
URL *string `json:"url"`
Header *ScopeHeaderInput `json:"header"`
Body *string `json:"body"`
}
type SenderRequest struct {
ID ulid.ULID `json:"id"`
SourceRequestLogID *ulid.ULID `json:"sourceRequestLogID"`
URL *url.URL `json:"url"`
Method HTTPMethod `json:"method"`
Proto HTTPProtocol `json:"proto"`
Headers []HTTPHeader `json:"headers"`
Body *string `json:"body"`
Timestamp time.Time `json:"timestamp"`
Response *HTTPResponseLog `json:"response"`
}
type SenderRequestFilter struct {
OnlyInScope bool `json:"onlyInScope"`
SearchExpression *string `json:"searchExpression"`
}
type SenderRequestFilterInput struct {
OnlyInScope *bool `json:"onlyInScope"`
SearchExpression *string `json:"searchExpression"`
}
type SenderRequestInput struct {
ID *ulid.ULID `json:"id"`
URL *url.URL `json:"url"`
Method *HTTPMethod `json:"method"`
Proto *HTTPProtocol `json:"proto"`
Headers []HTTPHeaderInput `json:"headers"`
Body *string `json:"body"`
}
type UpdateInterceptSettingsInput struct {
RequestsEnabled bool `json:"requestsEnabled"`
ResponsesEnabled bool `json:"responsesEnabled"`
RequestFilter *string `json:"requestFilter"`
ResponseFilter *string `json:"responseFilter"`
}
type HTTPMethod string
const (
HTTPMethodGet HTTPMethod = "GET"
HTTPMethodHead HTTPMethod = "HEAD"
HTTPMethodPost HTTPMethod = "POST"
HTTPMethodPut HTTPMethod = "PUT"
HTTPMethodDelete HTTPMethod = "DELETE"
HTTPMethodConnect HTTPMethod = "CONNECT"
HTTPMethodOptions HTTPMethod = "OPTIONS"
HTTPMethodTrace HTTPMethod = "TRACE"
HTTPMethodPatch HTTPMethod = "PATCH"
)
var AllHTTPMethod = []HTTPMethod{
HTTPMethodGet,
HTTPMethodHead,
HTTPMethodPost,
HTTPMethodPut,
HTTPMethodDelete,
HTTPMethodConnect,
HTTPMethodOptions,
HTTPMethodTrace,
HTTPMethodPatch,
}
func (e HTTPMethod) IsValid() bool {
switch e {
case HTTPMethodGet, HTTPMethodHead, HTTPMethodPost, HTTPMethodPut, HTTPMethodDelete, HTTPMethodConnect, HTTPMethodOptions, HTTPMethodTrace, HTTPMethodPatch:
return true
}
return false
}
func (e HTTPMethod) String() string {
return string(e)
}
func (e *HTTPMethod) UnmarshalGQL(v interface{}) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("enums must be strings")
}
*e = HTTPMethod(str)
if !e.IsValid() {
return fmt.Errorf("%s is not a valid HttpMethod", str)
}
return nil
}
func (e HTTPMethod) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}
type HTTPProtocol string
const (
HTTPProtocolHTTP10 HTTPProtocol = "HTTP10"
HTTPProtocolHTTP11 HTTPProtocol = "HTTP11"
HTTPProtocolHTTP20 HTTPProtocol = "HTTP20"
)
var AllHTTPProtocol = []HTTPProtocol{
HTTPProtocolHTTP10,
HTTPProtocolHTTP11,
HTTPProtocolHTTP20,
}
func (e HTTPProtocol) IsValid() bool {
switch e {
case HTTPProtocolHTTP10, HTTPProtocolHTTP11, HTTPProtocolHTTP20:
return true
}
return false
}
func (e HTTPProtocol) String() string {
return string(e)
}
func (e *HTTPProtocol) UnmarshalGQL(v interface{}) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("enums must be strings")
}
*e = HTTPProtocol(str)
if !e.IsValid() {
return fmt.Errorf("%s is not a valid HttpProtocol", str)
}
return nil
}
func (e HTTPProtocol) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}

1005
pkg/api/resolvers.go Normal file

File diff suppressed because it is too large Load Diff

57
pkg/db/badger/badger.go Normal file
View File

@ -0,0 +1,57 @@
package badger
import (
"fmt"
"github.com/dgraph-io/badger/v3"
)
const (
// Key prefixes. Each prefix value should be unique.
projectPrefix = 0x00
reqLogPrefix = 0x01
resLogPrefix = 0x02
senderReqPrefix = 0x03
// Request log indices.
reqLogProjectIDIndex = 0x00
// Sender request indices.
senderReqProjectIDIndex = 0x00
)
// Database is used to store and retrieve data from an underlying Badger database.
type Database struct {
badger *badger.DB
}
// OpenDatabase opens a new Badger database.
func OpenDatabase(opts badger.Options) (*Database, error) {
db, err := badger.Open(opts)
if err != nil {
return nil, fmt.Errorf("badger: failed to open database: %w", err)
}
return &Database{badger: db}, nil
}
// Close closes the underlying Badger database.
func (db *Database) Close() error {
return db.badger.Close()
}
// DatabaseFromBadgerDB returns a Database with `db` set as the underlying
// Badger database.
func DatabaseFromBadgerDB(db *badger.DB) *Database {
return &Database{badger: db}
}
func entryKey(prefix, index byte, value []byte) []byte {
// Key consists of: | prefix (byte) | index (byte) | value
key := make([]byte, 2+len(value))
key[0] = prefix
key[1] = index
copy(key[2:len(value)+2], value)
return key
}

21
pkg/db/badger/logger.go Normal file
View File

@ -0,0 +1,21 @@
package badger
import (
"github.com/dgraph-io/badger/v3"
"go.uber.org/zap"
)
// Interface guard.
var _ badger.Logger = (*Logger)(nil)
type Logger struct {
*zap.SugaredLogger
}
func NewLogger(l *zap.SugaredLogger) *Logger {
return &Logger{l}
}
func (l *Logger) Warningf(template string, args ...interface{}) {
l.Warnf(template, args)
}

115
pkg/db/badger/proj.go Normal file
View File

@ -0,0 +1,115 @@
package badger
import (
"bytes"
"context"
"encoding/gob"
"errors"
"fmt"
"github.com/dgraph-io/badger/v3"
"github.com/oklog/ulid"
"github.com/dstotijn/hetty/pkg/proj"
)
func (db *Database) UpsertProject(ctx context.Context, project proj.Project) error {
buf := bytes.Buffer{}
err := gob.NewEncoder(&buf).Encode(project)
if err != nil {
return fmt.Errorf("badger: failed to encode project: %w", err)
}
err = db.badger.Update(func(txn *badger.Txn) error {
return txn.Set(entryKey(projectPrefix, 0, project.ID[:]), buf.Bytes())
})
if err != nil {
return fmt.Errorf("badger: failed to commit transaction: %w", err)
}
return nil
}
func (db *Database) FindProjectByID(ctx context.Context, projectID ulid.ULID) (project proj.Project, err error) {
err = db.badger.View(func(txn *badger.Txn) error {
item, err := txn.Get(entryKey(projectPrefix, 0, projectID[:]))
if err != nil {
return err
}
err = item.Value(func(rawProject []byte) error {
return gob.NewDecoder(bytes.NewReader(rawProject)).Decode(&project)
})
if err != nil {
return fmt.Errorf("failed to retrieve or parse project: %w", err)
}
return nil
})
if errors.Is(err, badger.ErrKeyNotFound) {
return proj.Project{}, proj.ErrProjectNotFound
}
if err != nil {
return proj.Project{}, fmt.Errorf("badger: failed to commit transaction: %w", err)
}
return project, nil
}
func (db *Database) DeleteProject(ctx context.Context, projectID ulid.ULID) error {
err := db.ClearRequestLogs(ctx, projectID)
if err != nil {
return fmt.Errorf("badger: failed to delete project request logs: %w", err)
}
err = db.DeleteSenderRequests(ctx, projectID)
if err != nil {
return fmt.Errorf("badger: failed to delete project sender requests: %w", err)
}
err = db.badger.Update(func(txn *badger.Txn) error {
return txn.Delete(entryKey(projectPrefix, 0, projectID[:]))
})
if err != nil {
return fmt.Errorf("badger: failed to delete project item: %w", err)
}
return nil
}
func (db *Database) Projects(ctx context.Context) ([]proj.Project, error) {
projects := make([]proj.Project, 0)
err := db.badger.View(func(txn *badger.Txn) error {
var rawProject []byte
prefix := entryKey(projectPrefix, 0, nil)
iterator := txn.NewIterator(badger.DefaultIteratorOptions)
defer iterator.Close()
for iterator.Seek(prefix); iterator.ValidForPrefix(prefix); iterator.Next() {
rawProject, err := iterator.Item().ValueCopy(rawProject)
if err != nil {
return fmt.Errorf("failed to copy value: %w", err)
}
var project proj.Project
err = gob.NewDecoder(bytes.NewReader(rawProject)).Decode(&project)
if err != nil {
return fmt.Errorf("failed to decode project: %w", err)
}
projects = append(projects, project)
}
return nil
})
if err != nil {
return nil, fmt.Errorf("badger: failed to commit transaction: %w", err)
}
return projects, nil
}

328
pkg/db/badger/proj_test.go Normal file
View File

@ -0,0 +1,328 @@
package badger
import (
"bytes"
"context"
"encoding/gob"
"errors"
"math/rand"
"regexp"
"testing"
"time"
badgerdb "github.com/dgraph-io/badger/v3"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/oklog/ulid"
"github.com/dstotijn/hetty/pkg/filter"
"github.com/dstotijn/hetty/pkg/proj"
"github.com/dstotijn/hetty/pkg/scope"
)
//nolint:gosec
var ulidEntropy = rand.New(rand.NewSource(time.Now().UnixNano()))
var regexpCompareOpt = cmp.Comparer(func(x, y *regexp.Regexp) bool {
switch {
case x == nil && y == nil:
return true
case x == nil || y == nil:
return false
default:
return x.String() == y.String()
}
})
func TestUpsertProject(t *testing.T) {
t.Parallel()
badgerDB, err := badgerdb.Open(badgerdb.DefaultOptions("").WithInMemory(true))
if err != nil {
t.Fatalf("failed to open badger database: %v", err)
}
database := DatabaseFromBadgerDB(badgerDB)
defer database.Close()
searchExpr, err := filter.ParseQuery("foo AND bar OR NOT baz")
if err != nil {
t.Fatalf("unexpected error (expected: nil, got: %v)", err)
}
exp := proj.Project{
ID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
Name: "foobar",
Settings: proj.Settings{
ReqLogBypassOutOfScope: true,
ReqLogOnlyFindInScope: true,
ReqLogSearchExpr: searchExpr,
ScopeRules: []scope.Rule{
{
URL: regexp.MustCompile("^https://(.*)example.com(.*)$"),
Header: scope.Header{
Key: regexp.MustCompile("^X-Foo(.*)$"),
Value: regexp.MustCompile("^foo(.*)$"),
},
Body: regexp.MustCompile("^foo(.*)"),
},
},
},
}
err = database.UpsertProject(context.Background(), exp)
if err != nil {
t.Fatalf("unexpected error storing project: %v", err)
}
var rawProject []byte
err = badgerDB.View(func(txn *badgerdb.Txn) error {
item, err := txn.Get(entryKey(projectPrefix, 0, exp.ID[:]))
if err != nil {
return err
}
rawProject, err = item.ValueCopy(nil)
return err
})
if err != nil {
t.Fatalf("unexpected error retrieving project from database: %v", err)
}
got := proj.Project{}
err = gob.NewDecoder(bytes.NewReader(rawProject)).Decode(&got)
if err != nil {
t.Fatalf("unexpected error decoding project: %v", err)
}
if diff := cmp.Diff(exp, got, regexpCompareOpt, cmpopts.IgnoreUnexported(proj.Project{})); diff != "" {
t.Fatalf("project not equal (-exp, +got):\n%v", diff)
}
}
func TestFindProjectByID(t *testing.T) {
t.Parallel()
t.Run("existing project", func(t *testing.T) {
t.Parallel()
badgerDB, err := badgerdb.Open(badgerdb.DefaultOptions("").WithInMemory(true))
if err != nil {
t.Fatalf("failed to open badger database: %v", err)
}
database := DatabaseFromBadgerDB(badgerDB)
defer database.Close()
exp := proj.Project{
ID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
Name: "foobar",
Settings: proj.Settings{},
}
buf := bytes.Buffer{}
err = gob.NewEncoder(&buf).Encode(exp)
if err != nil {
t.Fatalf("unexpected error encoding project: %v", err)
}
err = badgerDB.Update(func(txn *badgerdb.Txn) error {
return txn.Set(entryKey(projectPrefix, 0, exp.ID[:]), buf.Bytes())
})
if err != nil {
t.Fatalf("unexpected error setting project: %v", err)
}
got, err := database.FindProjectByID(context.Background(), exp.ID)
if err != nil {
t.Fatalf("unexpected error finding project: %v", err)
}
if diff := cmp.Diff(exp, got, cmpopts.IgnoreUnexported(proj.Project{})); diff != "" {
t.Fatalf("project not equal (-exp, +got):\n%v", diff)
}
})
t.Run("project not found", func(t *testing.T) {
t.Parallel()
database, err := OpenDatabase(badgerdb.DefaultOptions("").WithInMemory(true))
if err != nil {
t.Fatalf("failed to open badger database: %v", err)
}
defer database.Close()
projectID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
_, err = database.FindProjectByID(context.Background(), projectID)
if !errors.Is(err, proj.ErrProjectNotFound) {
t.Fatalf("expected `proj.ErrProjectNotFound`, got: %v", err)
}
})
}
func TestDeleteProject(t *testing.T) {
t.Parallel()
badgerDB, err := badgerdb.Open(badgerdb.DefaultOptions("").WithInMemory(true))
if err != nil {
t.Fatalf("failed to open badger database: %v", err)
}
database := DatabaseFromBadgerDB(badgerDB)
defer database.Close()
// Store fixtures.
projectID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
reqLogID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
senderReqID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
err = badgerDB.Update(func(txn *badgerdb.Txn) error {
// Project item.
if err := txn.Set(entryKey(projectPrefix, 0, projectID[:]), nil); err != nil {
return err
}
// Sender request items.
if err := txn.Set(entryKey(senderReqPrefix, 0, senderReqID[:]), nil); err != nil {
return err
}
if err := txn.Set(entryKey(resLogPrefix, 0, senderReqID[:]), nil); err != nil {
return err
}
err := txn.Set(entryKey(senderReqPrefix, senderReqProjectIDIndex, append(projectID[:], senderReqID[:]...)), nil)
if err != nil {
return err
}
// Request log items.
if err := txn.Set(entryKey(reqLogPrefix, 0, reqLogID[:]), nil); err != nil {
return err
}
if err := txn.Set(entryKey(resLogPrefix, 0, reqLogID[:]), nil); err != nil {
return err
}
err = txn.Set(entryKey(reqLogPrefix, reqLogProjectIDIndex, append(projectID[:], reqLogID[:]...)), nil)
if err != nil {
return err
}
return nil
})
if err != nil {
t.Fatalf("unexpected error creating fixtures: %v", err)
}
err = database.DeleteProject(context.Background(), projectID)
if err != nil {
t.Fatalf("unexpected error deleting project: %v", err)
}
// Assert project key was deleted.
err = badgerDB.View(func(txn *badgerdb.Txn) error {
_, err := txn.Get(entryKey(projectPrefix, 0, projectID[:]))
return err
})
if !errors.Is(err, badgerdb.ErrKeyNotFound) {
t.Fatalf("expected `badger.ErrKeyNotFound`, got: %v", err)
}
// Assert request log item was deleted.
err = badgerDB.View(func(txn *badgerdb.Txn) error {
_, err := txn.Get(entryKey(reqLogPrefix, 0, reqLogID[:]))
return err
})
if !errors.Is(err, badgerdb.ErrKeyNotFound) {
t.Fatalf("expected `badger.ErrKeyNotFound`, got: %v", err)
}
// Assert response log item related to request log was deleted.
err = badgerDB.View(func(txn *badgerdb.Txn) error {
_, err := txn.Get(entryKey(resLogPrefix, 0, reqLogID[:]))
return err
})
if !errors.Is(err, badgerdb.ErrKeyNotFound) {
t.Fatalf("expected `badger.ErrKeyNotFound`, got: %v", err)
}
// Assert request log project ID index key was deleted.
err = badgerDB.View(func(txn *badgerdb.Txn) error {
_, err := txn.Get(entryKey(reqLogPrefix, reqLogProjectIDIndex, append(projectID[:], reqLogID[:]...)))
return err
})
if !errors.Is(err, badgerdb.ErrKeyNotFound) {
t.Fatalf("expected `badger.ErrKeyNotFound`, got: %v", err)
}
// Assert sender request item was deleted.
err = badgerDB.View(func(txn *badgerdb.Txn) error {
_, err := txn.Get(entryKey(senderReqPrefix, 0, senderReqID[:]))
return err
})
if !errors.Is(err, badgerdb.ErrKeyNotFound) {
t.Fatalf("expected `badger.ErrKeyNotFound`, got: %v", err)
}
// Assert response log item related to sender request was deleted.
err = badgerDB.View(func(txn *badgerdb.Txn) error {
_, err := txn.Get(entryKey(resLogPrefix, 0, senderReqID[:]))
return err
})
if !errors.Is(err, badgerdb.ErrKeyNotFound) {
t.Fatalf("expected `badger.ErrKeyNotFound`, got: %v", err)
}
// Assert sender request project ID index key was deleted.
err = badgerDB.View(func(txn *badgerdb.Txn) error {
_, err := txn.Get(entryKey(senderReqPrefix, senderReqProjectIDIndex, append(projectID[:], senderReqID[:]...)))
return err
})
if !errors.Is(err, badgerdb.ErrKeyNotFound) {
t.Fatalf("expected `badger.ErrKeyNotFound`, got: %v", err)
}
}
func TestProjects(t *testing.T) {
t.Parallel()
database, err := OpenDatabase(badgerdb.DefaultOptions("").WithInMemory(true))
if err != nil {
t.Fatalf("failed to open badger database: %v", err)
}
defer database.Close()
exp := []proj.Project{
{
ID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
Name: "one",
},
{
ID: ulid.MustNew(ulid.Timestamp(time.Now())+100, ulidEntropy),
Name: "two",
},
}
// Store fixtures.
for _, project := range exp {
err = database.UpsertProject(context.Background(), project)
if err != nil {
t.Fatalf("unexpected error creating project fixture: %v", err)
}
}
got, err := database.Projects(context.Background())
if err != nil {
t.Fatalf("unexpected error finding projects: %v", err)
}
if len(exp) != len(got) {
t.Fatalf("expected %v projects, got: %v", len(exp), len(got))
}
if diff := cmp.Diff(exp, got, cmpopts.IgnoreUnexported(proj.Project{})); diff != "" {
t.Fatalf("projects not equal (-exp, +got):\n%v", diff)
}
}

260
pkg/db/badger/reqlog.go Normal file
View File

@ -0,0 +1,260 @@
package badger
import (
"bytes"
"context"
"encoding/gob"
"errors"
"fmt"
"github.com/dgraph-io/badger/v3"
"github.com/oklog/ulid"
"github.com/dstotijn/hetty/pkg/reqlog"
"github.com/dstotijn/hetty/pkg/scope"
)
func (db *Database) FindRequestLogs(
ctx context.Context,
filter reqlog.FindRequestsFilter,
scope *scope.Scope) ([]reqlog.RequestLog, error,
) {
if filter.ProjectID.Compare(ulid.ULID{}) == 0 {
return nil, reqlog.ErrProjectIDMustBeSet
}
txn := db.badger.NewTransaction(false)
defer txn.Discard()
reqLogIDs, err := findRequestLogIDsByProjectID(txn, filter.ProjectID)
if err != nil {
return nil, fmt.Errorf("badger: failed to find request log IDs: %w", err)
}
reqLogs := make([]reqlog.RequestLog, 0, len(reqLogIDs))
for _, reqLogID := range reqLogIDs {
reqLog, err := getRequestLogWithResponse(txn, reqLogID)
if err != nil {
return nil, fmt.Errorf("badger: failed to get request log (id: %v): %w", reqLogID.String(), err)
}
if filter.OnlyInScope {
if !reqLog.MatchScope(scope) {
continue
}
}
// Filter by search expression.
// TODO: Once pagination is introduced, this filter logic should be done
// as items are retrieved (e.g. when using a `badger.Iterator`).
if filter.SearchExpr != nil {
match, err := reqLog.Matches(filter.SearchExpr)
if err != nil {
return nil, fmt.Errorf(
"badger: failed to match search expression for request log (id: %v): %w",
reqLogID.String(), err,
)
}
if !match {
continue
}
}
reqLogs = append(reqLogs, reqLog)
}
return reqLogs, nil
}
func getRequestLogWithResponse(txn *badger.Txn, reqLogID ulid.ULID) (reqlog.RequestLog, error) {
item, err := txn.Get(entryKey(reqLogPrefix, 0, reqLogID[:]))
switch {
case errors.Is(err, badger.ErrKeyNotFound):
return reqlog.RequestLog{}, reqlog.ErrRequestNotFound
case err != nil:
return reqlog.RequestLog{}, fmt.Errorf("failed to lookup request log item: %w", err)
}
reqLog := reqlog.RequestLog{
ID: reqLogID,
}
err = item.Value(func(rawReqLog []byte) error {
err = gob.NewDecoder(bytes.NewReader(rawReqLog)).Decode(&reqLog)
if err != nil {
return fmt.Errorf("failed to decode request log: %w", err)
}
return nil
})
if err != nil {
return reqlog.RequestLog{}, fmt.Errorf("failed to retrieve or parse request log value: %w", err)
}
item, err = txn.Get(entryKey(resLogPrefix, 0, reqLogID[:]))
if errors.Is(err, badger.ErrKeyNotFound) {
return reqLog, nil
}
if err != nil {
return reqlog.RequestLog{}, fmt.Errorf("failed to get response log: %w", err)
}
err = item.Value(func(rawReslog []byte) error {
var resLog reqlog.ResponseLog
err = gob.NewDecoder(bytes.NewReader(rawReslog)).Decode(&resLog)
if err != nil {
return fmt.Errorf("failed to decode response log: %w", err)
}
reqLog.Response = &resLog
return nil
})
if err != nil {
return reqlog.RequestLog{}, fmt.Errorf("failed to retrieve or parse response log value: %w", err)
}
return reqLog, nil
}
func (db *Database) FindRequestLogByID(ctx context.Context, reqLogID ulid.ULID) (reqLog reqlog.RequestLog, err error) {
txn := db.badger.NewTransaction(false)
defer txn.Discard()
reqLog, err = getRequestLogWithResponse(txn, reqLogID)
if err != nil {
return reqlog.RequestLog{}, fmt.Errorf("badger: failed to get request log: %w", err)
}
return reqLog, nil
}
func (db *Database) StoreRequestLog(ctx context.Context, reqLog reqlog.RequestLog) error {
buf := bytes.Buffer{}
err := gob.NewEncoder(&buf).Encode(reqLog)
if err != nil {
return fmt.Errorf("badger: failed to encode request log: %w", err)
}
entries := []*badger.Entry{
// Request log itself.
{
Key: entryKey(reqLogPrefix, 0, reqLog.ID[:]),
Value: buf.Bytes(),
},
// Index by project ID.
{
Key: entryKey(reqLogPrefix, reqLogProjectIDIndex, append(reqLog.ProjectID[:], reqLog.ID[:]...)),
},
}
err = db.badger.Update(func(txn *badger.Txn) error {
for i := range entries {
err := txn.SetEntry(entries[i])
if err != nil {
return err
}
}
return nil
})
if err != nil {
return fmt.Errorf("badger: failed to commit transaction: %w", err)
}
return nil
}
func (db *Database) StoreResponseLog(ctx context.Context, reqLogID ulid.ULID, resLog reqlog.ResponseLog) error {
buf := bytes.Buffer{}
err := gob.NewEncoder(&buf).Encode(resLog)
if err != nil {
return fmt.Errorf("badger: failed to encode response log: %w", err)
}
err = db.badger.Update(func(txn *badger.Txn) error {
return txn.SetEntry(&badger.Entry{
Key: entryKey(resLogPrefix, 0, reqLogID[:]),
Value: buf.Bytes(),
})
})
if err != nil {
return fmt.Errorf("badger: failed to commit transaction: %w", err)
}
return nil
}
func (db *Database) ClearRequestLogs(ctx context.Context, projectID ulid.ULID) error {
// Note: this transaction is used just for reading; we use the `badger.WriteBatch`
// API to bulk delete items.
txn := db.badger.NewTransaction(false)
defer txn.Discard()
reqLogIDs, err := findRequestLogIDsByProjectID(txn, projectID)
if err != nil {
return fmt.Errorf("badger: failed to find request log IDs: %w", err)
}
writeBatch := db.badger.NewWriteBatch()
defer writeBatch.Cancel()
for _, reqLogID := range reqLogIDs {
// Delete request logs.
err := writeBatch.Delete(entryKey(reqLogPrefix, 0, reqLogID[:]))
if err != nil {
return fmt.Errorf("badger: failed to delete request log: %w", err)
}
// Delete related response log.
err = writeBatch.Delete(entryKey(resLogPrefix, 0, reqLogID[:]))
if err != nil {
return fmt.Errorf("badger: failed to delete request log: %w", err)
}
}
if err := writeBatch.Flush(); err != nil {
return fmt.Errorf("badger: failed to commit batch write: %w", err)
}
err = db.badger.DropPrefix(entryKey(reqLogPrefix, reqLogProjectIDIndex, projectID[:]))
if err != nil {
return fmt.Errorf("badger: failed to drop request log project ID index items: %w", err)
}
return nil
}
func findRequestLogIDsByProjectID(txn *badger.Txn, projectID ulid.ULID) ([]ulid.ULID, error) {
reqLogIDs := make([]ulid.ULID, 0)
opts := badger.DefaultIteratorOptions
opts.PrefetchValues = false
opts.Reverse = true
iterator := txn.NewIterator(opts)
defer iterator.Close()
var projectIndexKey []byte
prefix := entryKey(reqLogPrefix, reqLogProjectIDIndex, projectID[:])
for iterator.Seek(append(prefix, 255)); iterator.ValidForPrefix(prefix); iterator.Next() {
projectIndexKey = iterator.Item().KeyCopy(projectIndexKey)
var id ulid.ULID
// The request log ID starts *after* the first 2 prefix and index bytes
// and the 16 byte project ID.
if err := id.UnmarshalBinary(projectIndexKey[18:]); err != nil {
return nil, fmt.Errorf("failed to parse request log ID: %w", err)
}
reqLogIDs = append(reqLogIDs, id)
}
return reqLogIDs, nil
}

View File

@ -0,0 +1,127 @@
package badger
import (
"context"
"errors"
"net/http"
"net/url"
"testing"
"time"
badgerdb "github.com/dgraph-io/badger/v3"
"github.com/google/go-cmp/cmp"
"github.com/oklog/ulid"
"github.com/dstotijn/hetty/pkg/reqlog"
)
func TestFindRequestLogs(t *testing.T) {
t.Parallel()
t.Run("without project ID in filter", func(t *testing.T) {
t.Parallel()
database, err := OpenDatabase(badgerdb.DefaultOptions("").WithInMemory(true))
if err != nil {
t.Fatalf("failed to open badger database: %v", err)
}
defer database.Close()
filter := reqlog.FindRequestsFilter{}
_, err = database.FindRequestLogs(context.Background(), filter, nil)
if !errors.Is(err, reqlog.ErrProjectIDMustBeSet) {
t.Fatalf("expected `reqlog.ErrProjectIDMustBeSet`, got: %v", err)
}
})
t.Run("returns request logs and related response logs", func(t *testing.T) {
t.Parallel()
database, err := OpenDatabase(badgerdb.DefaultOptions("").WithInMemory(true))
if err != nil {
t.Fatalf("failed to open badger database: %v", err)
}
defer database.Close()
projectID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
fixtures := []reqlog.RequestLog{
{
ID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
ProjectID: projectID,
URL: mustParseURL(t, "https://example.com/foobar"),
Method: http.MethodPost,
Proto: "HTTP/1.1",
Header: http.Header{
"X-Foo": []string{"baz"},
},
Body: []byte("foo"),
Response: &reqlog.ResponseLog{
Proto: "HTTP/1.1",
Status: "200 OK",
StatusCode: 200,
Header: http.Header{
"X-Yolo": []string{"swag"},
},
Body: []byte("bar"),
},
},
{
ID: ulid.MustNew(ulid.Timestamp(time.Now())+100, ulidEntropy),
ProjectID: projectID,
URL: mustParseURL(t, "https://example.com/foo?bar=baz"),
Method: http.MethodGet,
Proto: "HTTP/1.1",
Header: http.Header{
"X-Foo": []string{"baz"},
},
},
}
// Store fixtures.
for _, reqLog := range fixtures {
err = database.StoreRequestLog(context.Background(), reqLog)
if err != nil {
t.Fatalf("unexpected error creating request log fixture: %v", err)
}
if reqLog.Response != nil {
err = database.StoreResponseLog(context.Background(), reqLog.ID, *reqLog.Response)
if err != nil {
t.Fatalf("unexpected error creating response log fixture: %v", err)
}
}
}
filter := reqlog.FindRequestsFilter{
ProjectID: projectID,
}
got, err := database.FindRequestLogs(context.Background(), filter, nil)
if err != nil {
t.Fatalf("unexpected error finding request logs: %v", err)
}
// We expect the found request logs are *reversed*, e.g. newest first.
exp := make([]reqlog.RequestLog, len(fixtures))
for i, j := 0, len(fixtures)-1; i < j; i, j = i+1, j-1 {
exp[i], exp[j] = fixtures[j], fixtures[i]
}
if diff := cmp.Diff(exp, got); diff != "" {
t.Fatalf("request logs not equal (-exp, +got):\n%v", diff)
}
})
}
func mustParseURL(t *testing.T, s string) *url.URL {
t.Helper()
u, err := url.Parse(s)
if err != nil {
panic(err)
}
return u
}

240
pkg/db/badger/sender.go Normal file
View File

@ -0,0 +1,240 @@
package badger
import (
"bytes"
"context"
"encoding/gob"
"errors"
"fmt"
"github.com/dgraph-io/badger/v3"
"github.com/oklog/ulid"
"github.com/dstotijn/hetty/pkg/reqlog"
"github.com/dstotijn/hetty/pkg/scope"
"github.com/dstotijn/hetty/pkg/sender"
)
func (db *Database) StoreSenderRequest(ctx context.Context, req sender.Request) error {
buf := bytes.Buffer{}
err := gob.NewEncoder(&buf).Encode(req)
if err != nil {
return fmt.Errorf("badger: failed to encode sender request: %w", err)
}
entries := []*badger.Entry{
// Sender request itself.
{
Key: entryKey(senderReqPrefix, 0, req.ID[:]),
Value: buf.Bytes(),
},
// Index by project ID.
{
Key: entryKey(senderReqPrefix, senderReqProjectIDIndex, append(req.ProjectID[:], req.ID[:]...)),
},
}
err = db.badger.Update(func(txn *badger.Txn) error {
for i := range entries {
err := txn.SetEntry(entries[i])
if err != nil {
return err
}
}
return nil
})
if err != nil {
return fmt.Errorf("badger: failed to commit transaction: %w", err)
}
return nil
}
func (db *Database) FindSenderRequestByID(ctx context.Context, senderReqID ulid.ULID) (sender.Request, error) {
txn := db.badger.NewTransaction(false)
defer txn.Discard()
req, err := getSenderRequestWithResponseLog(txn, senderReqID)
if err != nil {
return sender.Request{}, fmt.Errorf("badger: failed to get sender request: %w", err)
}
return req, nil
}
func (db *Database) FindSenderRequests(
ctx context.Context,
filter sender.FindRequestsFilter,
scope *scope.Scope,
) ([]sender.Request, error) {
if filter.ProjectID.Compare(ulid.ULID{}) == 0 {
return nil, sender.ErrProjectIDMustBeSet
}
txn := db.badger.NewTransaction(false)
defer txn.Discard()
senderReqIDs, err := findSenderRequestIDsByProjectID(txn, filter.ProjectID)
if err != nil {
return nil, fmt.Errorf("badger: failed to find sender request IDs: %w", err)
}
senderReqs := make([]sender.Request, 0, len(senderReqIDs))
for _, id := range senderReqIDs {
senderReq, err := getSenderRequestWithResponseLog(txn, id)
if err != nil {
return nil, fmt.Errorf("badger: failed to get sender request (id: %v): %w", id.String(), err)
}
if filter.OnlyInScope {
if !senderReq.MatchScope(scope) {
continue
}
}
// Filter by search expression.
// TODO: Once pagination is introduced, this filter logic should be done
// as items are retrieved (e.g. when using a `badger.Iterator`).
if filter.SearchExpr != nil {
match, err := senderReq.Matches(filter.SearchExpr)
if err != nil {
return nil, fmt.Errorf(
"badger: failed to match search expression for sender request (id: %v): %w",
id.String(), err,
)
}
if !match {
continue
}
}
senderReqs = append(senderReqs, senderReq)
}
return senderReqs, nil
}
func (db *Database) DeleteSenderRequests(ctx context.Context, projectID ulid.ULID) error {
// Note: this transaction is used just for reading; we use the `badger.WriteBatch`
// API to bulk delete items.
txn := db.badger.NewTransaction(false)
defer txn.Discard()
senderReqIDs, err := findSenderRequestIDsByProjectID(txn, projectID)
if err != nil {
return fmt.Errorf("badger: failed to find sender request IDs: %w", err)
}
writeBatch := db.badger.NewWriteBatch()
defer writeBatch.Cancel()
for _, senderReqID := range senderReqIDs {
// Delete sender requests.
err := writeBatch.Delete(entryKey(senderReqPrefix, 0, senderReqID[:]))
if err != nil {
return fmt.Errorf("badger: failed to delete sender requests: %w", err)
}
// Delete related response log.
err = writeBatch.Delete(entryKey(resLogPrefix, 0, senderReqID[:]))
if err != nil {
return fmt.Errorf("badger: failed to delete request log: %w", err)
}
}
if err := writeBatch.Flush(); err != nil {
return fmt.Errorf("badger: failed to commit batch write: %w", err)
}
err = db.badger.DropPrefix(entryKey(senderReqPrefix, senderReqProjectIDIndex, projectID[:]))
if err != nil {
return fmt.Errorf("badger: failed to drop sender request project ID index items: %w", err)
}
return nil
}
func getSenderRequestWithResponseLog(txn *badger.Txn, senderReqID ulid.ULID) (sender.Request, error) {
item, err := txn.Get(entryKey(senderReqPrefix, 0, senderReqID[:]))
switch {
case errors.Is(err, badger.ErrKeyNotFound):
return sender.Request{}, sender.ErrRequestNotFound
case err != nil:
return sender.Request{}, fmt.Errorf("failed to lookup sender request item: %w", err)
}
req := sender.Request{
ID: senderReqID,
}
err = item.Value(func(rawSenderReq []byte) error {
err = gob.NewDecoder(bytes.NewReader(rawSenderReq)).Decode(&req)
if err != nil {
return fmt.Errorf("failed to decode sender request: %w", err)
}
return nil
})
if err != nil {
return sender.Request{}, fmt.Errorf("failed to retrieve or parse sender request value: %w", err)
}
item, err = txn.Get(entryKey(resLogPrefix, 0, senderReqID[:]))
if errors.Is(err, badger.ErrKeyNotFound) {
return req, nil
}
if err != nil {
return sender.Request{}, fmt.Errorf("failed to get response log: %w", err)
}
err = item.Value(func(rawReslog []byte) error {
var resLog reqlog.ResponseLog
err = gob.NewDecoder(bytes.NewReader(rawReslog)).Decode(&resLog)
if err != nil {
return fmt.Errorf("failed to decode response log: %w", err)
}
req.Response = &resLog
return nil
})
if err != nil {
return sender.Request{}, fmt.Errorf("failed to retrieve or parse response log value: %w", err)
}
return req, nil
}
func findSenderRequestIDsByProjectID(txn *badger.Txn, projectID ulid.ULID) ([]ulid.ULID, error) {
senderReqIDs := make([]ulid.ULID, 0)
opts := badger.DefaultIteratorOptions
opts.PrefetchValues = false
opts.Reverse = true
iterator := txn.NewIterator(opts)
defer iterator.Close()
var projectIndexKey []byte
prefix := entryKey(senderReqPrefix, senderReqProjectIDIndex, projectID[:])
for iterator.Seek(append(prefix, 255)); iterator.ValidForPrefix(prefix); iterator.Next() {
projectIndexKey = iterator.Item().KeyCopy(projectIndexKey)
var id ulid.ULID
// The request log ID starts *after* the first 2 prefix and index bytes
// and the 16 byte project ID.
if err := id.UnmarshalBinary(projectIndexKey[18:]); err != nil {
return nil, fmt.Errorf("failed to parse sender request ID: %w", err)
}
senderReqIDs = append(senderReqIDs, id)
}
return senderReqIDs, nil
}

View File

@ -0,0 +1,204 @@
package badger_test
import (
"context"
"errors"
"math/rand"
"net/http"
"net/url"
"testing"
"time"
badgerdb "github.com/dgraph-io/badger/v3"
"github.com/google/go-cmp/cmp"
"github.com/oklog/ulid"
"github.com/dstotijn/hetty/pkg/db/badger"
"github.com/dstotijn/hetty/pkg/reqlog"
"github.com/dstotijn/hetty/pkg/sender"
)
//nolint:gosec
var ulidEntropy = rand.New(rand.NewSource(time.Now().UnixNano()))
var exampleURL = func() *url.URL {
u, err := url.Parse("https://example.com/foobar")
if err != nil {
panic(err)
}
return u
}()
func TestFindRequestByID(t *testing.T) {
t.Parallel()
database, err := badger.OpenDatabase(badgerdb.DefaultOptions("").WithInMemory(true))
if err != nil {
t.Fatalf("failed to open badger database: %v", err)
}
defer database.Close()
// See: https://go.dev/blog/subtests#cleaning-up-after-a-group-of-parallel-tests
t.Run("group", func(t *testing.T) {
t.Run("sender request not found", func(t *testing.T) {
t.Parallel()
_, err := database.FindSenderRequestByID(context.Background(), ulid.ULID{})
if !errors.Is(err, sender.ErrRequestNotFound) {
t.Fatalf("expected `sender.ErrRequestNotFound`, got: %v", err)
}
})
t.Run("sender request found", func(t *testing.T) {
t.Parallel()
exp := sender.Request{
ID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
ProjectID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
SourceRequestLogID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
URL: exampleURL,
Method: http.MethodGet,
Proto: sender.HTTPProto20,
Header: http.Header{
"X-Foo": []string{"bar"},
},
Body: []byte("foo"),
}
err := database.StoreSenderRequest(context.Background(), exp)
if err != nil {
t.Fatalf("unexpected error (expected: nil, got: %v)", err)
}
resLog := reqlog.ResponseLog{
Proto: "HTTP/2.0",
Status: "200 OK",
StatusCode: 200,
Header: http.Header{
"X-Yolo": []string{"swag"},
},
Body: []byte("bar"),
}
err = database.StoreResponseLog(context.Background(), exp.ID, resLog)
if err != nil {
t.Fatalf("unexpected error (expected: nil, got: %v)", err)
}
exp.Response = &resLog
got, err := database.FindSenderRequestByID(context.Background(), exp.ID)
if err != nil {
t.Fatalf("unexpected error (expected: nil, got: %v)", err)
}
if diff := cmp.Diff(exp, got); diff != "" {
t.Fatalf("sender request not equal (-exp, +got):\n%v", diff)
}
})
})
}
func TestFindSenderRequests(t *testing.T) {
t.Parallel()
t.Run("without project ID in filter", func(t *testing.T) {
t.Parallel()
database, err := badger.OpenDatabase(badgerdb.DefaultOptions("").WithInMemory(true))
if err != nil {
t.Fatalf("failed to open badger database: %v", err)
}
defer database.Close()
filter := sender.FindRequestsFilter{}
_, err = database.FindSenderRequests(context.Background(), filter, nil)
if !errors.Is(err, sender.ErrProjectIDMustBeSet) {
t.Fatalf("expected `sender.ErrProjectIDMustBeSet`, got: %v", err)
}
})
t.Run("returns sender requests and related response logs", func(t *testing.T) {
t.Parallel()
database, err := badger.OpenDatabase(badgerdb.DefaultOptions("").WithInMemory(true))
if err != nil {
t.Fatalf("failed to open badger database: %v", err)
}
defer database.Close()
projectID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
fixtures := []sender.Request{
{
ID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
ProjectID: projectID,
SourceRequestLogID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
URL: exampleURL,
Method: http.MethodPost,
Proto: "HTTP/1.1",
Header: http.Header{
"X-Foo": []string{"baz"},
},
Body: []byte("foo"),
Response: &reqlog.ResponseLog{
Proto: "HTTP/1.1",
Status: "200 OK",
StatusCode: 200,
Header: http.Header{
"X-Yolo": []string{"swag"},
},
Body: []byte("bar"),
},
},
{
ID: ulid.MustNew(ulid.Timestamp(time.Now())+100, ulidEntropy),
ProjectID: projectID,
SourceRequestLogID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
URL: exampleURL,
Method: http.MethodGet,
Proto: "HTTP/1.1",
Header: http.Header{
"X-Foo": []string{"baz"},
},
},
}
// Store fixtures.
for _, senderReq := range fixtures {
err = database.StoreSenderRequest(context.Background(), senderReq)
if err != nil {
t.Fatalf("unexpected error creating request log fixture: %v", err)
}
if senderReq.Response != nil {
err = database.StoreResponseLog(context.Background(), senderReq.ID, *senderReq.Response)
if err != nil {
t.Fatalf("unexpected error creating response log fixture: %v", err)
}
}
}
filter := sender.FindRequestsFilter{
ProjectID: projectID,
}
got, err := database.FindSenderRequests(context.Background(), filter, nil)
if err != nil {
t.Fatalf("unexpected error finding sender requests: %v", err)
}
// We expect the found sender requests are *reversed*, e.g. newest first.
exp := make([]sender.Request, len(fixtures))
for i, j := 0, len(fixtures)-1; i < j; i, j = i+1, j-1 {
exp[i], exp[j] = fixtures[j], fixtures[i]
}
if diff := cmp.Diff(exp, got); diff != "" {
t.Fatalf("sender requests not equal (-exp, +got):\n%v", diff)
}
})
}

View File

@ -1,60 +0,0 @@
package bolt
import (
"fmt"
bolt "go.etcd.io/bbolt"
)
// Database is used to store and retrieve data from an underlying Bolt database.
type Database struct {
bolt *bolt.DB
}
// OpenDatabase opens a new Bolt database.
func OpenDatabase(path string, opts *bolt.Options) (*Database, error) {
db, err := bolt.Open(path, 0o600, opts)
if err != nil {
return nil, fmt.Errorf("bolt: failed to open database: %w", err)
}
return DatabaseFromBoltDB(db)
}
// Close closes the underlying Bolt database.
func (db *Database) Close() error {
return db.bolt.Close()
}
// DatabaseFromBoltDB returns a Database with `db` set as the underlying Bolt
// database.
func DatabaseFromBoltDB(db *bolt.DB) (*Database, error) {
err := db.Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists(projectsBucketName)
if err != nil {
return err
}
return nil
})
if err != nil {
return nil, fmt.Errorf("bolt: failed to create projects bucket: %w", err)
}
return &Database{bolt: db}, nil
}
func createNestedBucket(tx *bolt.Tx, names ...[]byte) (b *bolt.Bucket, err error) {
for i, name := range names {
if b == nil {
b, err = tx.CreateBucketIfNotExists(name)
} else {
b, err = b.CreateBucketIfNotExists(name)
}
if err != nil {
return nil, fmt.Errorf("bolt: failed to create nested bucket %q: %w", names[:i+1], err)
}
}
return b, nil
}

View File

@ -1,23 +0,0 @@
package bolt
import (
bolt "go.etcd.io/bbolt"
"go.uber.org/zap"
)
// Interface guard.
var _ bolt.Logger = (*Logger)(nil)
type Logger struct {
*zap.SugaredLogger
}
// Warning implements bbolt.Logger.
func (l *Logger) Warning(v ...interface{}) {
l.Warn(v...)
}
// Warningf implements bbolt.Logger.
func (l *Logger) Warningf(format string, v ...interface{}) {
l.Warnf(format, v...)
}

View File

@ -1,174 +0,0 @@
package bolt
import (
"context"
"errors"
"fmt"
bolt "go.etcd.io/bbolt"
"google.golang.org/protobuf/proto"
"github.com/dstotijn/hetty/pkg/proj"
)
var (
ErrProjectsBucketNotFound = errors.New("bolt: projects bucket not found")
ErrProjectBucketNotFound = errors.New("bolt: project bucket not found")
)
var (
projectsBucketName = []byte("projects")
projectKey = []byte("project")
)
func projectsBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
b := tx.Bucket(projectsBucketName)
if b == nil {
return nil, ErrProjectsBucketNotFound
}
return b, nil
}
func projectBucket(tx *bolt.Tx, projectID string) (*bolt.Bucket, error) {
pb, err := projectsBucket(tx)
if err != nil {
return nil, err
}
b := pb.Bucket([]byte(projectID))
if b == nil {
return nil, ErrProjectBucketNotFound
}
return b, nil
}
func (db *Database) UpsertProject(ctx context.Context, project *proj.Project) error {
err := db.bolt.Update(func(tx *bolt.Tx) error {
b, err := createNestedBucket(tx, projectsBucketName, []byte(project.Id))
if err != nil {
return fmt.Errorf("bolt: failed to create project bucket: %w", err)
}
buf, err := proto.Marshal(project)
if err != nil {
return fmt.Errorf("bolt: failed to marshal project: %w", err)
}
err = b.Put(projectKey, buf)
if err != nil {
return fmt.Errorf("bolt: failed to upsert project: %w", err)
}
_, err = b.CreateBucketIfNotExists(reqLogsBucketName)
if err != nil {
return fmt.Errorf("bolt: failed to create request logs bucket: %w", err)
}
_, err = b.CreateBucketIfNotExists(senderReqsBucketName)
if err != nil {
return fmt.Errorf("bolt: failed to create sender requests bucket: %w", err)
}
return nil
})
if err != nil {
return fmt.Errorf("bolt: failed to commit transaction: %w", err)
}
return nil
}
func (db *Database) FindProjectByID(ctx context.Context, projectID string) (*proj.Project, error) {
project := &proj.Project{}
err := db.bolt.View(func(tx *bolt.Tx) error {
bucket, err := projectBucket(tx, projectID)
if errors.Is(err, ErrProjectsBucketNotFound) || errors.Is(err, ErrProjectBucketNotFound) {
return proj.ErrProjectNotFound
}
if err != nil {
return err
}
rawProject := bucket.Get(projectKey)
if rawProject == nil {
return proj.ErrProjectNotFound
}
err = proto.Unmarshal(rawProject, project)
if err != nil {
return fmt.Errorf("failed to unmarshal project: %w", err)
}
return nil
})
if err != nil {
return nil, fmt.Errorf("bolt: failed to commit transaction: %w", err)
}
return project, nil
}
func (db *Database) DeleteProject(ctx context.Context, projectID string) error {
err := db.bolt.Update(func(tx *bolt.Tx) error {
pb, err := projectsBucket(tx)
if err != nil {
return err
}
err = pb.DeleteBucket([]byte(projectID))
if err != nil {
return fmt.Errorf("failed to delete project bucket: %w", err)
}
return nil
})
if err != nil {
return fmt.Errorf("bolt: failed to commit transaction: %w", err)
}
return nil
}
func (db *Database) Projects(ctx context.Context) ([]*proj.Project, error) {
projects := make([]*proj.Project, 0)
err := db.bolt.View(func(tx *bolt.Tx) error {
pb, err := projectsBucket(tx)
if err != nil {
return err
}
err = pb.ForEachBucket(func(projectID []byte) error {
bucket, err := projectBucket(tx, string(projectID))
if err != nil {
return err
}
rawProject := bucket.Get(projectKey)
if rawProject == nil {
return proj.ErrProjectNotFound
}
project := &proj.Project{}
err = proto.Unmarshal(rawProject, project)
if err != nil {
return fmt.Errorf("failed to unmarshal project: %w", err)
}
projects = append(projects, project)
return nil
})
if err != nil {
return fmt.Errorf("failed to iterate over projects: %w", err)
}
return nil
})
if err != nil {
return nil, fmt.Errorf("bolt: failed to commit transaction: %w", err)
}
return projects, nil
}

View File

@ -1,233 +0,0 @@
package bolt_test
import (
"context"
"errors"
"testing"
"github.com/oklog/ulid/v2"
"go.etcd.io/bbolt"
"google.golang.org/protobuf/proto"
"github.com/dstotijn/hetty/pkg/db/bolt"
"github.com/dstotijn/hetty/pkg/proj"
"github.com/dstotijn/hetty/pkg/reqlog"
"github.com/dstotijn/hetty/pkg/scope"
"github.com/dstotijn/hetty/pkg/testutil"
)
func TestUpsertProject(t *testing.T) {
t.Parallel()
path := t.TempDir() + "bolt.db"
boltDB, err := bbolt.Open(path, 0o600, nil)
if err != nil {
t.Fatalf("failed to open bolt database: %v", err)
}
db, err := bolt.DatabaseFromBoltDB(boltDB)
if err != nil {
t.Fatalf("failed to create database: %v", err)
}
defer db.Close()
exp := &proj.Project{
Id: "foobar-project-id",
Name: "foobar",
ReqLogBypassOutOfScope: true,
ReqLogFilter: &reqlog.RequestLogsFilter{
OnlyInScope: true,
SearchExpr: "foo AND bar OR NOT baz",
},
ScopeRules: []*scope.ScopeRule{
{
UrlRegexp: "^https://(.*)example.com(.*)$",
HeaderKeyRegexp: "^X-Foo(.*)$",
HeaderValueRegexp: "^foo(.*)$",
BodyRegexp: "^foo(.*)",
},
},
}
err = db.UpsertProject(context.Background(), exp)
if err != nil {
t.Fatalf("unexpected error storing project: %v", err)
}
var rawProject []byte
err = boltDB.View(func(tx *bbolt.Tx) error {
rawProject = tx.Bucket([]byte("projects")).Bucket([]byte(exp.Id)).Get([]byte("project"))
return nil
})
if err != nil {
t.Fatalf("unexpected error retrieving project from database: %v", err)
}
if rawProject == nil {
t.Fatalf("expected raw project to be retrieved, got: nil")
}
got := &proj.Project{}
err = proto.Unmarshal(rawProject, got)
if err != nil {
t.Fatalf("unexpected error decoding project: %v", err)
}
testutil.ProtoDiff(t, "project not equal", exp, got, "id")
}
func TestFindProjectByID(t *testing.T) {
t.Parallel()
t.Run("existing project", func(t *testing.T) {
t.Parallel()
path := t.TempDir() + "bolt.db"
boltDB, err := bbolt.Open(path, 0o600, nil)
if err != nil {
t.Fatalf("failed to open bolt database: %v", err)
}
db, err := bolt.DatabaseFromBoltDB(boltDB)
if err != nil {
t.Fatalf("failed to create database: %v", err)
}
defer db.Close()
exp := &proj.Project{
Id: ulid.Make().String(),
}
buf, err := proto.Marshal(exp)
if err != nil {
t.Fatalf("unexpected error encoding project: %v", err)
}
err = boltDB.Update(func(tx *bbolt.Tx) error {
b, err := tx.Bucket([]byte("projects")).CreateBucket([]byte(exp.Id))
if err != nil {
return err
}
return b.Put([]byte("project"), buf)
})
if err != nil {
t.Fatalf("unexpected error setting project: %v", err)
}
got, err := db.FindProjectByID(context.Background(), exp.Id)
if err != nil {
t.Fatalf("unexpected error finding project: %v", err)
}
testutil.ProtoDiff(t, "project not equal", exp, got)
})
t.Run("project not found", func(t *testing.T) {
t.Parallel()
path := t.TempDir() + "bolt.db"
boltDB, err := bbolt.Open(path, 0o600, nil)
if err != nil {
t.Fatalf("failed to open bolt database: %v", err)
}
db, err := bolt.DatabaseFromBoltDB(boltDB)
if err != nil {
t.Fatalf("failed to create database: %v", err)
}
defer db.Close()
projectID := ulid.Make().String()
_, err = db.FindProjectByID(context.Background(), projectID)
if !errors.Is(err, proj.ErrProjectNotFound) {
t.Fatalf("expected `proj.ErrProjectNotFound`, got: %v", err)
}
})
}
func TestDeleteProject(t *testing.T) {
t.Parallel()
path := t.TempDir() + "bolt.db"
boltDB, err := bbolt.Open(path, 0o600, nil)
if err != nil {
t.Fatalf("failed to open bolt database: %v", err)
}
db, err := bolt.DatabaseFromBoltDB(boltDB)
if err != nil {
t.Fatalf("failed to create database: %v", err)
}
defer db.Close()
// Insert test fixture.
projectID := ulid.Make().String()
err = db.UpsertProject(context.Background(), &proj.Project{
Id: projectID,
})
if err != nil {
t.Fatalf("unexpected error storing project: %v", err)
}
err = db.DeleteProject(context.Background(), projectID)
if err != nil {
t.Fatalf("unexpected error deleting project: %v", err)
}
var got *bbolt.Bucket
_ = boltDB.View(func(tx *bbolt.Tx) error {
got = tx.Bucket([]byte("projects")).Bucket([]byte(projectID))
return nil
})
if got != nil {
t.Fatalf("expected bucket to be nil, got: %v", got)
}
}
func TestProjects(t *testing.T) {
t.Parallel()
path := t.TempDir() + "bolt.db"
boltDB, err := bbolt.Open(path, 0o600, nil)
if err != nil {
t.Fatalf("failed to open bolt database: %v", err)
}
db, err := bolt.DatabaseFromBoltDB(boltDB)
if err != nil {
t.Fatalf("failed to create database: %v", err)
}
defer db.Close()
exp := []*proj.Project{
{
Id: ulid.Make().String(),
Name: "one",
},
{
Id: ulid.Make().String(),
Name: "two",
},
}
// Store fixtures.
for _, project := range exp {
err = db.UpsertProject(context.Background(), project)
if err != nil {
t.Fatalf("unexpected error creating project fixture: %v", err)
}
}
got, err := db.Projects(context.Background())
if err != nil {
t.Fatalf("unexpected error finding projects: %v", err)
}
if len(exp) != len(got) {
t.Fatalf("expected %v projects, got: %v", len(exp), len(got))
}
testutil.ProtoSlicesDiff(t, "projects not equal", exp, got)
}

View File

@ -1,182 +0,0 @@
package bolt
import (
"context"
"errors"
"fmt"
bolt "go.etcd.io/bbolt"
"google.golang.org/protobuf/proto"
"github.com/dstotijn/hetty/pkg/http"
"github.com/dstotijn/hetty/pkg/reqlog"
)
var ErrRequestLogsBucketNotFound = errors.New("bolt: request logs bucket not found")
var reqLogsBucketName = []byte("request_logs")
func requestLogsBucket(tx *bolt.Tx, projectID string) (*bolt.Bucket, error) {
pb, err := projectBucket(tx, projectID)
if err != nil {
return nil, err
}
b := pb.Bucket(reqLogsBucketName)
if b == nil {
return nil, ErrRequestLogsBucketNotFound
}
return b, nil
}
func (db *Database) FindRequestLogs(ctx context.Context, projectID string, filterFn func(*reqlog.HttpRequestLog) (bool, error)) (reqLogs []*reqlog.HttpRequestLog, err error) {
tx, err := db.bolt.Begin(false)
if err != nil {
return nil, fmt.Errorf("bolt: failed to begin transaction: %w", err)
}
defer tx.Rollback()
b, err := requestLogsBucket(tx, projectID)
if err != nil {
return nil, fmt.Errorf("bolt: failed to get request logs bucket: %w", err)
}
err = b.ForEach(func(reqLogID, rawReqLog []byte) error {
var reqLog reqlog.HttpRequestLog
err = proto.Unmarshal(rawReqLog, &reqLog)
if err != nil {
return fmt.Errorf("failed to decode request log: %w", err)
}
if filterFn != nil {
match, err := filterFn(&reqLog)
if err != nil {
return fmt.Errorf("failed to filter request log: %w", err)
}
if !match {
return nil
}
}
reqLogs = append(reqLogs, &reqLog)
return nil
})
if err != nil {
return nil, fmt.Errorf("bolt: failed to iterate over request logs: %w", err)
}
// Reverse items, so newest requests appear first.
for i, j := 0, len(reqLogs)-1; i < j; i, j = i+1, j-1 {
reqLogs[i], reqLogs[j] = reqLogs[j], reqLogs[i]
}
return reqLogs, nil
}
func (db *Database) FindRequestLogByID(ctx context.Context, projectID, reqLogID string) (*reqlog.HttpRequestLog, error) {
reqLog := &reqlog.HttpRequestLog{}
err := db.bolt.View(func(tx *bolt.Tx) error {
b, err := requestLogsBucket(tx, projectID)
if err != nil {
return fmt.Errorf("bolt: failed to get request logs bucket: %w", err)
}
rawReqLog := b.Get([]byte(reqLogID))
if rawReqLog == nil {
return reqlog.ErrRequestLogNotFound
}
err = proto.Unmarshal(rawReqLog, reqLog)
if err != nil {
return fmt.Errorf("failed to unmarshal request log: %w", err)
}
return nil
})
if err != nil {
return nil, fmt.Errorf("bolt: failed to find request log by ID: %w", err)
}
return reqLog, nil
}
func (db *Database) StoreRequestLog(ctx context.Context, reqLog *reqlog.HttpRequestLog) error {
encReqLog, err := proto.Marshal(reqLog)
if err != nil {
return fmt.Errorf("bolt: failed to marshal request log: %w", err)
}
err = db.bolt.Update(func(txn *bolt.Tx) error {
b, err := requestLogsBucket(txn, reqLog.ProjectId)
if err != nil {
return fmt.Errorf("failed to get request logs bucket: %w", err)
}
err = b.Put([]byte(reqLog.Id), encReqLog)
if err != nil {
return fmt.Errorf("failed to put request log: %w", err)
}
return nil
})
if err != nil {
return fmt.Errorf("bolt: failed to commit transaction: %w", err)
}
return nil
}
func (db *Database) StoreResponseLog(ctx context.Context, projectID, reqLogID string, resLog *http.Response) error {
err := db.bolt.Update(func(txn *bolt.Tx) error {
b, err := requestLogsBucket(txn, projectID)
if err != nil {
return fmt.Errorf("failed to get request logs bucket: %w", err)
}
encReqLog := b.Get([]byte(reqLogID))
if encReqLog == nil {
return reqlog.ErrRequestLogNotFound
}
var reqLog reqlog.HttpRequestLog
err = proto.Unmarshal(encReqLog, &reqLog)
if err != nil {
return fmt.Errorf("failed to decode request log: %w", err)
}
reqLog.Response = resLog
encReqLog, err = proto.Marshal(&reqLog)
if err != nil {
return fmt.Errorf("failed to encode request log: %w", err)
}
err = b.Put([]byte(reqLogID), encReqLog)
if err != nil {
return fmt.Errorf("failed to put request log: %w", err)
}
return nil
})
if err != nil {
return fmt.Errorf("bolt: failed to commit transaction: %w", err)
}
return nil
}
func (db *Database) ClearRequestLogs(ctx context.Context, projectID string) error {
err := db.bolt.Update(func(txn *bolt.Tx) error {
pb, err := projectBucket(txn, projectID)
if err != nil {
return fmt.Errorf("failed to get project bucket: %w", err)
}
return pb.DeleteBucket(reqLogsBucketName)
})
if err != nil {
return fmt.Errorf("bolt: failed to commit transaction: %w", err)
}
return nil
}

View File

@ -1,110 +0,0 @@
package bolt_test
import (
"context"
"testing"
"github.com/oklog/ulid/v2"
"go.etcd.io/bbolt"
"github.com/dstotijn/hetty/pkg/db/bolt"
"github.com/dstotijn/hetty/pkg/http"
"github.com/dstotijn/hetty/pkg/proj"
"github.com/dstotijn/hetty/pkg/reqlog"
"github.com/dstotijn/hetty/pkg/testutil"
)
func TestFindRequestLogs(t *testing.T) {
t.Parallel()
t.Run("returns request logs and related response logs", func(t *testing.T) {
t.Parallel()
path := t.TempDir() + "bolt.db"
boltDB, err := bbolt.Open(path, 0o600, nil)
if err != nil {
t.Fatalf("failed to open bolt database: %v", err)
}
db, err := bolt.DatabaseFromBoltDB(boltDB)
if err != nil {
t.Fatalf("failed to create database: %v", err)
}
defer db.Close()
projectID := ulid.Make().String()
err = db.UpsertProject(context.Background(), &proj.Project{
Id: projectID,
})
if err != nil {
t.Fatalf("unexpected error upserting project: %v", err)
}
fixtures := []*reqlog.HttpRequestLog{
{
Id: ulid.Make().String(),
ProjectId: projectID,
Request: &http.Request{
Url: "https://example.com/foobar",
Method: http.Method_METHOD_POST,
Protocol: http.Protocol_PROTOCOL_HTTP11,
Headers: []*http.Header{
{Key: "X-Foo", Value: "baz"},
},
Body: []byte("foo"),
},
Response: &http.Response{
Status: "200 OK",
StatusCode: 200,
Headers: []*http.Header{
{Key: "X-Yolo", Value: "swag"},
},
Body: []byte("bar"),
},
},
{
Id: ulid.Make().String(),
ProjectId: projectID,
Request: &http.Request{
Url: "https://example.com/foo?bar=baz",
Method: http.Method_METHOD_GET,
Protocol: http.Protocol_PROTOCOL_HTTP11,
Headers: []*http.Header{
{Key: "X-Foo", Value: "baz"},
},
Body: []byte("foo"),
},
Response: &http.Response{
Status: "200 OK",
StatusCode: 200,
Headers: []*http.Header{
{Key: "X-Yolo", Value: "swag"},
},
Body: []byte("bar"),
},
},
}
// Store fixtures.
for _, reqLog := range fixtures {
err = db.StoreRequestLog(context.Background(), reqLog)
if err != nil {
t.Fatalf("unexpected error creating request log fixture: %v", err)
}
}
got, err := db.FindRequestLogs(context.Background(), projectID, nil)
if err != nil {
t.Fatalf("unexpected error finding request logs: %v", err)
}
// We expect the found request logs are *reversed*, e.g. newest first.
exp := make([]*reqlog.HttpRequestLog, len(fixtures))
for i, j := 0, len(fixtures)-1; i < j; i, j = i+1, j-1 {
exp[i], exp[j] = fixtures[j], fixtures[i]
}
testutil.ProtoSlicesDiff(t, "request logs not equal", exp, got)
})
}

View File

@ -1,153 +0,0 @@
package bolt
import (
"context"
"errors"
"fmt"
bolt "go.etcd.io/bbolt"
"google.golang.org/protobuf/proto"
"github.com/dstotijn/hetty/pkg/sender"
)
var ErrSenderRequestsBucketNotFound = errors.New("bolt: sender requests bucket not found")
var senderReqsBucketName = []byte("sender_requests")
func senderReqsBucket(tx *bolt.Tx, projectID string) (*bolt.Bucket, error) {
pb, err := projectBucket(tx, projectID)
if err != nil {
return nil, err
}
b := pb.Bucket(senderReqsBucketName)
if b == nil {
return nil, ErrSenderRequestsBucketNotFound
}
return b, nil
}
func (db *Database) StoreSenderRequest(ctx context.Context, req *sender.Request) error {
rawReq, err := proto.Marshal(req)
if err != nil {
return fmt.Errorf("bolt: failed to marshal sender request: %w", err)
}
err = db.bolt.Update(func(tx *bolt.Tx) error {
senderReqsBucket, err := senderReqsBucket(tx, req.ProjectId)
if err != nil {
return fmt.Errorf("failed to get sender requests bucket: %w", err)
}
err = senderReqsBucket.Put([]byte(req.Id), rawReq)
if err != nil {
return fmt.Errorf("failed to put sender request: %w", err)
}
return nil
})
if err != nil {
return fmt.Errorf("bolt: failed to commit transaction: %w", err)
}
return nil
}
func (db *Database) FindSenderRequestByID(ctx context.Context, projectID, senderReqID string) (req *sender.Request, err error) {
if projectID == "" {
return nil, sender.ErrProjectIDMustBeSet
}
err = db.bolt.View(func(tx *bolt.Tx) error {
senderReqsBucket, err := senderReqsBucket(tx, projectID)
if err != nil {
return fmt.Errorf("failed to get sender requests bucket: %w", err)
}
rawSenderReq := senderReqsBucket.Get([]byte(senderReqID))
if rawSenderReq == nil {
return sender.ErrRequestNotFound
}
req = &sender.Request{}
err = proto.Unmarshal(rawSenderReq, req)
if err != nil {
return fmt.Errorf("failed to unmarshal sender request: %w", err)
}
return nil
})
if err != nil {
return nil, fmt.Errorf("bolt: failed to commit transaction: %w", err)
}
return req, nil
}
func (db *Database) FindSenderRequests(ctx context.Context, projectID string, filterFn func(req *sender.Request) (bool, error)) (reqs []*sender.Request, err error) {
tx, err := db.bolt.Begin(false)
if err != nil {
return nil, fmt.Errorf("bolt: failed to begin transaction: %w", err)
}
defer tx.Rollback()
b, err := senderReqsBucket(tx, projectID)
if err != nil {
return nil, fmt.Errorf("failed to get sender requests bucket: %w", err)
}
err = b.ForEach(func(senderReqID, rawSenderReq []byte) error {
req := &sender.Request{}
err = proto.Unmarshal(rawSenderReq, req)
if err != nil {
return fmt.Errorf("failed to unmarshal sender request: %w", err)
}
if filterFn != nil {
match, err := filterFn(req)
if err != nil {
return fmt.Errorf("failed to filter sender request: %w", err)
}
if !match {
return nil
}
}
reqs = append(reqs, req)
return nil
})
if err != nil {
return nil, fmt.Errorf("bolt: failed to commit transaction: %w", err)
}
// Reverse items, so newest requests appear first.
for i, j := 0, len(reqs)-1; i < j; i, j = i+1, j-1 {
reqs[i], reqs[j] = reqs[j], reqs[i]
}
return reqs, nil
}
func (db *Database) DeleteSenderRequests(ctx context.Context, projectID string) error {
err := db.bolt.Update(func(tx *bolt.Tx) error {
senderReqsBucket, err := senderReqsBucket(tx, projectID)
if err != nil {
return fmt.Errorf("failed to get sender requests bucket: %w", err)
}
err = senderReqsBucket.DeleteBucket(senderReqsBucketName)
if err != nil {
return fmt.Errorf("failed to delete sender requests bucket: %w", err)
}
return nil
})
if err != nil {
return fmt.Errorf("bolt: failed to commit transaction: %w", err)
}
return nil
}

View File

@ -1,212 +0,0 @@
package bolt_test
import (
"context"
"errors"
"net/url"
"testing"
"github.com/oklog/ulid/v2"
"go.etcd.io/bbolt"
"github.com/dstotijn/hetty/pkg/db/bolt"
"github.com/dstotijn/hetty/pkg/http"
"github.com/dstotijn/hetty/pkg/proj"
"github.com/dstotijn/hetty/pkg/sender"
"github.com/dstotijn/hetty/pkg/testutil"
)
var exampleURL = func() *url.URL {
u, err := url.Parse("https://example.com/foobar")
if err != nil {
panic(err)
}
return u
}()
func TestFindRequestByID(t *testing.T) {
t.Parallel()
path := t.TempDir() + "bolt.db"
boltDB, err := bbolt.Open(path, 0o600, nil)
if err != nil {
t.Fatalf("failed to open bolt database: %v", err)
}
defer boltDB.Close()
db, err := bolt.DatabaseFromBoltDB(boltDB)
if err != nil {
t.Fatalf("failed to create database: %v", err)
}
defer db.Close()
projectID := "foobar-project-id"
reqID := "foobar-req-id"
err = db.UpsertProject(context.Background(), &proj.Project{
Id: projectID,
})
if err != nil {
t.Fatalf("unexpected error upserting project: %v", err)
}
// See: https://go.dev/blog/subtests#cleaning-up-after-a-group-of-parallel-tests
t.Run("group", func(t *testing.T) {
t.Run("sender request not found", func(t *testing.T) {
t.Parallel()
_, err := db.FindSenderRequestByID(context.Background(), projectID, reqID)
if !errors.Is(err, sender.ErrRequestNotFound) {
t.Fatalf("expected `sender.ErrRequestNotFound`, got: %v", err)
}
})
t.Run("sender request found", func(t *testing.T) {
t.Parallel()
exp := &sender.Request{
Id: "foobar-sender-req-id",
ProjectId: projectID,
SourceRequestLogId: "foobar-req-log-id",
HttpRequest: &http.Request{
Url: exampleURL.String(),
Method: http.Method_METHOD_GET,
Protocol: http.Protocol_PROTOCOL_HTTP20,
Headers: []*http.Header{
{
Key: "X-Foo",
Value: "bar",
},
},
Body: []byte("foo"),
},
HttpResponse: &http.Response{
Protocol: http.Protocol_PROTOCOL_HTTP20,
Status: "200 OK",
StatusCode: 200,
Headers: []*http.Header{
{
Key: "X-Yolo",
Value: "swag",
},
},
Body: []byte("bar"),
},
}
err := db.StoreSenderRequest(context.Background(), exp)
if err != nil {
t.Fatalf("unexpected error (expected: nil, got: %v)", err)
}
got, err := db.FindSenderRequestByID(context.Background(), projectID, exp.Id)
if err != nil {
t.Fatalf("unexpected error (expected: nil, got: %v)", err)
}
testutil.ProtoDiff(t, "sender request not equal", exp, got, "id")
})
})
}
func TestFindSenderRequests(t *testing.T) {
t.Parallel()
t.Run("returns sender requests and related response logs", func(t *testing.T) {
t.Parallel()
path := t.TempDir() + "bolt.db"
boltDB, err := bbolt.Open(path, 0o600, nil)
if err != nil {
t.Fatalf("failed to open bolt database: %v", err)
}
defer boltDB.Close()
db, err := bolt.DatabaseFromBoltDB(boltDB)
if err != nil {
t.Fatalf("failed to create database: %v", err)
}
defer db.Close()
projectID := "foobar-project-id"
err = db.UpsertProject(context.Background(), &proj.Project{
Id: projectID,
Name: "foobar",
})
if err != nil {
t.Fatalf("unexpected error creating project (expected: nil, got: %v)", err)
}
fixtures := []*sender.Request{
{
Id: ulid.Make().String(),
ProjectId: projectID,
SourceRequestLogId: "foobar-req-log-id-1",
HttpRequest: &http.Request{
Url: exampleURL.String(),
Method: http.Method_METHOD_POST,
Protocol: http.Protocol_PROTOCOL_HTTP11,
Headers: []*http.Header{
{
Key: "X-Foo",
Value: "baz",
},
},
Body: []byte("foo"),
},
HttpResponse: &http.Response{
Protocol: http.Protocol_PROTOCOL_HTTP11,
Status: "200 OK",
StatusCode: 200,
Headers: []*http.Header{
{
Key: "X-Yolo",
Value: "swag",
},
},
Body: []byte("bar"),
},
},
{
Id: ulid.Make().String(),
ProjectId: projectID,
SourceRequestLogId: "foobar-req-log-id-2",
HttpRequest: &http.Request{
Url: exampleURL.String(),
Method: http.Method_METHOD_GET,
Protocol: http.Protocol_PROTOCOL_HTTP11,
Headers: []*http.Header{
{
Key: "X-Foo",
Value: "baz",
},
},
Body: []byte("foo"),
},
},
}
// Store fixtures.
for _, senderReq := range fixtures {
err = db.StoreSenderRequest(context.Background(), senderReq)
if err != nil {
t.Fatalf("unexpected error creating request log fixture: %v", err)
}
}
got, err := db.FindSenderRequests(context.Background(), projectID, nil)
if err != nil {
t.Fatalf("unexpected error finding sender requests: %v", err)
}
// We expect the found sender requests are *reversed*, e.g. newest first.
exp := make([]*sender.Request, len(fixtures))
for i, j := 0, len(fixtures)-1; i < j; i, j = i+1, j-1 {
exp[i], exp[j] = fixtures[j], fixtures[i]
}
testutil.ProtoSlicesDiff(t, "sender requests not equal", exp, got)
})
}

View File

@ -3,11 +3,10 @@ package filter
import (
"errors"
"fmt"
"github.com/dstotijn/hetty/pkg/http"
"net/http"
)
func MatchHTTPHeaders(op TokenType, expr Expression, headers []*http.Header) (bool, error) {
func MatchHTTPHeaders(op TokenType, expr Expression, headers http.Header) (bool, error) {
if headers == nil {
return false, nil
}
@ -20,9 +19,11 @@ func MatchHTTPHeaders(op TokenType, expr Expression, headers []*http.Header) (bo
}
// Return `true` if at least one header (<key>: <value>) is equal to the string literal.
for _, header := range headers {
if strLiteral.Value == fmt.Sprintf("%v: %v", header.Key, header.Value) {
return true, nil
for key, values := range headers {
for _, value := range values {
if strLiteral.Value == fmt.Sprintf("%v: %v", key, value) {
return true, nil
}
}
}
@ -34,9 +35,11 @@ func MatchHTTPHeaders(op TokenType, expr Expression, headers []*http.Header) (bo
}
// Return `true` if none of the headers (<key>: <value>) are equal to the string literal.
for _, header := range headers {
if strLiteral.Value == fmt.Sprintf("%v: %v", header.Key, header.Value) {
return false, nil
for key, values := range headers {
for _, value := range values {
if strLiteral.Value == fmt.Sprintf("%v: %v", key, value) {
return false, nil
}
}
}
@ -48,9 +51,11 @@ func MatchHTTPHeaders(op TokenType, expr Expression, headers []*http.Header) (bo
}
// Return `true` if at least one header (<key>: <value>) matches the regular expression.
for _, header := range headers {
if re.Regexp.MatchString(fmt.Sprintf("%v: %v", header.Key, header.Value)) {
return true, nil
for key, values := range headers {
for _, value := range values {
if re.MatchString(fmt.Sprintf("%v: %v", key, value)) {
return true, nil
}
}
}
@ -62,9 +67,11 @@ func MatchHTTPHeaders(op TokenType, expr Expression, headers []*http.Header) (bo
}
// Return `true` if none of the headers (<key>: <value>) match the regular expression.
for _, header := range headers {
if re.Regexp.MatchString(fmt.Sprintf("%v: %v", header.Key, header.Value)) {
return false, nil
for key, values := range headers {
for _, value := range values {
if re.MatchString(fmt.Sprintf("%v: %v", key, value)) {
return false, nil
}
}
}

View File

@ -1,79 +0,0 @@
package http
import (
"fmt"
"io"
nethttp "net/http"
"strconv"
"strings"
)
var ProtoMap = map[string]Protocol{
"HTTP/1.0": Protocol_PROTOCOL_HTTP10,
"HTTP/1.1": Protocol_PROTOCOL_HTTP11,
"HTTP/2.0": Protocol_PROTOCOL_HTTP20,
}
var MethodMap = map[string]Method{
"GET": Method_METHOD_GET,
"POST": Method_METHOD_POST,
"PUT": Method_METHOD_PUT,
"DELETE": Method_METHOD_DELETE,
"CONNECT": Method_METHOD_CONNECT,
"OPTIONS": Method_METHOD_OPTIONS,
"TRACE": Method_METHOD_TRACE,
"PATCH": Method_METHOD_PATCH,
}
func ParseHeader(header nethttp.Header) []*Header {
headers := []*Header{}
for key, values := range header {
for _, value := range values {
headers = append(headers, &Header{Key: key, Value: value})
}
}
return headers
}
var ResponseSearchKeyFns = map[string]func(rl *Response) string{
"res.proto": func(rl *Response) string { return rl.GetProtocol().String() },
"res.status": func(rl *Response) string { return rl.GetStatus() },
"res.statusCode": func(rl *Response) string { return strconv.Itoa(int(rl.GetStatusCode())) },
"res.statusReason": func(rl *Response) string {
statusReasonSubs := strings.SplitN(rl.GetStatus(), " ", 2)
if len(statusReasonSubs) != 2 {
return ""
}
return statusReasonSubs[1]
},
"res.body": func(rl *Response) string { return string(rl.GetBody()) },
}
func ParseHTTPResponse(res *nethttp.Response) (*Response, error) {
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, fmt.Errorf("reqlog: could not read body: %w", err)
}
headers := []*Header{}
for k, v := range res.Header {
for _, vv := range v {
headers = append(headers, &Header{Key: k, Value: vv})
}
}
protocol, ok := ProtoMap[res.Proto]
if !ok {
return nil, fmt.Errorf("reqlog: invalid protocol %q", res.Proto)
}
return &Response{
Protocol: protocol,
Status: res.Status,
StatusCode: int32(res.StatusCode),
Headers: headers,
Body: body,
}, nil
}

View File

@ -1,476 +0,0 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.3
// protoc (unknown)
// source: http/http.proto
package http
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Method int32
const (
Method_METHOD_UNSPECIFIED Method = 0
Method_METHOD_GET Method = 1
Method_METHOD_HEAD Method = 2
Method_METHOD_POST Method = 3
Method_METHOD_PUT Method = 4
Method_METHOD_DELETE Method = 5
Method_METHOD_CONNECT Method = 6
Method_METHOD_OPTIONS Method = 7
Method_METHOD_TRACE Method = 8
Method_METHOD_PATCH Method = 9
)
// Enum value maps for Method.
var (
Method_name = map[int32]string{
0: "METHOD_UNSPECIFIED",
1: "METHOD_GET",
2: "METHOD_HEAD",
3: "METHOD_POST",
4: "METHOD_PUT",
5: "METHOD_DELETE",
6: "METHOD_CONNECT",
7: "METHOD_OPTIONS",
8: "METHOD_TRACE",
9: "METHOD_PATCH",
}
Method_value = map[string]int32{
"METHOD_UNSPECIFIED": 0,
"METHOD_GET": 1,
"METHOD_HEAD": 2,
"METHOD_POST": 3,
"METHOD_PUT": 4,
"METHOD_DELETE": 5,
"METHOD_CONNECT": 6,
"METHOD_OPTIONS": 7,
"METHOD_TRACE": 8,
"METHOD_PATCH": 9,
}
)
func (x Method) Enum() *Method {
p := new(Method)
*p = x
return p
}
func (x Method) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (Method) Descriptor() protoreflect.EnumDescriptor {
return file_http_http_proto_enumTypes[0].Descriptor()
}
func (Method) Type() protoreflect.EnumType {
return &file_http_http_proto_enumTypes[0]
}
func (x Method) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use Method.Descriptor instead.
func (Method) EnumDescriptor() ([]byte, []int) {
return file_http_http_proto_rawDescGZIP(), []int{0}
}
type Protocol int32
const (
Protocol_PROTOCOL_UNSPECIFIED Protocol = 0
Protocol_PROTOCOL_HTTP10 Protocol = 1
Protocol_PROTOCOL_HTTP11 Protocol = 2
Protocol_PROTOCOL_HTTP20 Protocol = 3
)
// Enum value maps for Protocol.
var (
Protocol_name = map[int32]string{
0: "PROTOCOL_UNSPECIFIED",
1: "PROTOCOL_HTTP10",
2: "PROTOCOL_HTTP11",
3: "PROTOCOL_HTTP20",
}
Protocol_value = map[string]int32{
"PROTOCOL_UNSPECIFIED": 0,
"PROTOCOL_HTTP10": 1,
"PROTOCOL_HTTP11": 2,
"PROTOCOL_HTTP20": 3,
}
)
func (x Protocol) Enum() *Protocol {
p := new(Protocol)
*p = x
return p
}
func (x Protocol) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (Protocol) Descriptor() protoreflect.EnumDescriptor {
return file_http_http_proto_enumTypes[1].Descriptor()
}
func (Protocol) Type() protoreflect.EnumType {
return &file_http_http_proto_enumTypes[1]
}
func (x Protocol) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use Protocol.Descriptor instead.
func (Protocol) EnumDescriptor() ([]byte, []int) {
return file_http_http_proto_rawDescGZIP(), []int{1}
}
type Request struct {
state protoimpl.MessageState `protogen:"open.v1"`
Method Method `protobuf:"varint,1,opt,name=method,proto3,enum=hetty.http.v1.Method" json:"method,omitempty"`
Protocol Protocol `protobuf:"varint,2,opt,name=protocol,proto3,enum=hetty.http.v1.Protocol" json:"protocol,omitempty"`
Url string `protobuf:"bytes,3,opt,name=url,proto3" json:"url,omitempty"`
Headers []*Header `protobuf:"bytes,4,rep,name=headers,proto3" json:"headers,omitempty"`
Body []byte `protobuf:"bytes,5,opt,name=body,proto3" json:"body,omitempty"`
Response *Response `protobuf:"bytes,6,opt,name=response,proto3" json:"response,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Request) Reset() {
*x = Request{}
mi := &file_http_http_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Request) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Request) ProtoMessage() {}
func (x *Request) ProtoReflect() protoreflect.Message {
mi := &file_http_http_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Request.ProtoReflect.Descriptor instead.
func (*Request) Descriptor() ([]byte, []int) {
return file_http_http_proto_rawDescGZIP(), []int{0}
}
func (x *Request) GetMethod() Method {
if x != nil {
return x.Method
}
return Method_METHOD_UNSPECIFIED
}
func (x *Request) GetProtocol() Protocol {
if x != nil {
return x.Protocol
}
return Protocol_PROTOCOL_UNSPECIFIED
}
func (x *Request) GetUrl() string {
if x != nil {
return x.Url
}
return ""
}
func (x *Request) GetHeaders() []*Header {
if x != nil {
return x.Headers
}
return nil
}
func (x *Request) GetBody() []byte {
if x != nil {
return x.Body
}
return nil
}
func (x *Request) GetResponse() *Response {
if x != nil {
return x.Response
}
return nil
}
type Response struct {
state protoimpl.MessageState `protogen:"open.v1"`
Protocol Protocol `protobuf:"varint,1,opt,name=protocol,proto3,enum=hetty.http.v1.Protocol" json:"protocol,omitempty"`
Status string `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"`
StatusCode int32 `protobuf:"varint,3,opt,name=status_code,json=statusCode,proto3" json:"status_code,omitempty"`
Headers []*Header `protobuf:"bytes,5,rep,name=headers,proto3" json:"headers,omitempty"`
Body []byte `protobuf:"bytes,6,opt,name=body,proto3" json:"body,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Response) Reset() {
*x = Response{}
mi := &file_http_http_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Response) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Response) ProtoMessage() {}
func (x *Response) ProtoReflect() protoreflect.Message {
mi := &file_http_http_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Response.ProtoReflect.Descriptor instead.
func (*Response) Descriptor() ([]byte, []int) {
return file_http_http_proto_rawDescGZIP(), []int{1}
}
func (x *Response) GetProtocol() Protocol {
if x != nil {
return x.Protocol
}
return Protocol_PROTOCOL_UNSPECIFIED
}
func (x *Response) GetStatus() string {
if x != nil {
return x.Status
}
return ""
}
func (x *Response) GetStatusCode() int32 {
if x != nil {
return x.StatusCode
}
return 0
}
func (x *Response) GetHeaders() []*Header {
if x != nil {
return x.Headers
}
return nil
}
func (x *Response) GetBody() []byte {
if x != nil {
return x.Body
}
return nil
}
type Header struct {
state protoimpl.MessageState `protogen:"open.v1"`
Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Header) Reset() {
*x = Header{}
mi := &file_http_http_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Header) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Header) ProtoMessage() {}
func (x *Header) ProtoReflect() protoreflect.Message {
mi := &file_http_http_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Header.ProtoReflect.Descriptor instead.
func (*Header) Descriptor() ([]byte, []int) {
return file_http_http_proto_rawDescGZIP(), []int{2}
}
func (x *Header) GetKey() string {
if x != nil {
return x.Key
}
return ""
}
func (x *Header) GetValue() string {
if x != nil {
return x.Value
}
return ""
}
var File_http_http_proto protoreflect.FileDescriptor
var file_http_http_proto_rawDesc = []byte{
0x0a, 0x0f, 0x68, 0x74, 0x74, 0x70, 0x2f, 0x68, 0x74, 0x74, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x12, 0x0d, 0x68, 0x65, 0x74, 0x74, 0x79, 0x2e, 0x68, 0x74, 0x74, 0x70, 0x2e, 0x76, 0x31,
0x22, 0xf9, 0x01, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2d, 0x0a, 0x06,
0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x68,
0x65, 0x74, 0x74, 0x79, 0x2e, 0x68, 0x74, 0x74, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x74,
0x68, 0x6f, 0x64, 0x52, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x33, 0x0a, 0x08, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x17, 0x2e,
0x68, 0x65, 0x74, 0x74, 0x79, 0x2e, 0x68, 0x74, 0x74, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72,
0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c,
0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75,
0x72, 0x6c, 0x12, 0x2f, 0x0a, 0x07, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20,
0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x68, 0x65, 0x74, 0x74, 0x79, 0x2e, 0x68, 0x74, 0x74, 0x70,
0x2e, 0x76, 0x31, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x52, 0x07, 0x68, 0x65, 0x61, 0x64,
0x65, 0x72, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28,
0x0c, 0x52, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x12, 0x33, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x68, 0x65, 0x74, 0x74,
0x79, 0x2e, 0x68, 0x74, 0x74, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xbd, 0x01, 0x0a,
0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x08, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x17, 0x2e, 0x68, 0x65,
0x74, 0x74, 0x79, 0x2e, 0x68, 0x74, 0x74, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x74,
0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x16,
0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06,
0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73,
0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x73, 0x74, 0x61,
0x74, 0x75, 0x73, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x2f, 0x0a, 0x07, 0x68, 0x65, 0x61, 0x64, 0x65,
0x72, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x68, 0x65, 0x74, 0x74, 0x79,
0x2e, 0x68, 0x74, 0x74, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x52,
0x07, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x62, 0x6f, 0x64, 0x79,
0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x22, 0x30, 0x0a, 0x06,
0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20,
0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75,
0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x2a, 0xc1,
0x01, 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x16, 0x0a, 0x12, 0x4d, 0x45, 0x54,
0x48, 0x4f, 0x44, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10,
0x00, 0x12, 0x0e, 0x0a, 0x0a, 0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x47, 0x45, 0x54, 0x10,
0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x48, 0x45, 0x41, 0x44,
0x10, 0x02, 0x12, 0x0f, 0x0a, 0x0b, 0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x50, 0x4f, 0x53,
0x54, 0x10, 0x03, 0x12, 0x0e, 0x0a, 0x0a, 0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x50, 0x55,
0x54, 0x10, 0x04, 0x12, 0x11, 0x0a, 0x0d, 0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x44, 0x45,
0x4c, 0x45, 0x54, 0x45, 0x10, 0x05, 0x12, 0x12, 0x0a, 0x0e, 0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44,
0x5f, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x10, 0x06, 0x12, 0x12, 0x0a, 0x0e, 0x4d, 0x45,
0x54, 0x48, 0x4f, 0x44, 0x5f, 0x4f, 0x50, 0x54, 0x49, 0x4f, 0x4e, 0x53, 0x10, 0x07, 0x12, 0x10,
0x0a, 0x0c, 0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0x08,
0x12, 0x10, 0x0a, 0x0c, 0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x50, 0x41, 0x54, 0x43, 0x48,
0x10, 0x09, 0x2a, 0x63, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x18,
0x0a, 0x14, 0x50, 0x52, 0x4f, 0x54, 0x4f, 0x43, 0x4f, 0x4c, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45,
0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x13, 0x0a, 0x0f, 0x50, 0x52, 0x4f, 0x54,
0x4f, 0x43, 0x4f, 0x4c, 0x5f, 0x48, 0x54, 0x54, 0x50, 0x31, 0x30, 0x10, 0x01, 0x12, 0x13, 0x0a,
0x0f, 0x50, 0x52, 0x4f, 0x54, 0x4f, 0x43, 0x4f, 0x4c, 0x5f, 0x48, 0x54, 0x54, 0x50, 0x31, 0x31,
0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x50, 0x52, 0x4f, 0x54, 0x4f, 0x43, 0x4f, 0x4c, 0x5f, 0x48,
0x54, 0x54, 0x50, 0x32, 0x30, 0x10, 0x03, 0x42, 0x24, 0x5a, 0x22, 0x67, 0x69, 0x74, 0x68, 0x75,
0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x64, 0x73, 0x74, 0x6f, 0x74, 0x69, 0x6a, 0x6e, 0x2f, 0x68,
0x65, 0x74, 0x74, 0x79, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x68, 0x74, 0x74, 0x70, 0x62, 0x06, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_http_http_proto_rawDescOnce sync.Once
file_http_http_proto_rawDescData = file_http_http_proto_rawDesc
)
func file_http_http_proto_rawDescGZIP() []byte {
file_http_http_proto_rawDescOnce.Do(func() {
file_http_http_proto_rawDescData = protoimpl.X.CompressGZIP(file_http_http_proto_rawDescData)
})
return file_http_http_proto_rawDescData
}
var file_http_http_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
var file_http_http_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
var file_http_http_proto_goTypes = []any{
(Method)(0), // 0: hetty.http.v1.Method
(Protocol)(0), // 1: hetty.http.v1.Protocol
(*Request)(nil), // 2: hetty.http.v1.Request
(*Response)(nil), // 3: hetty.http.v1.Response
(*Header)(nil), // 4: hetty.http.v1.Header
}
var file_http_http_proto_depIdxs = []int32{
0, // 0: hetty.http.v1.Request.method:type_name -> hetty.http.v1.Method
1, // 1: hetty.http.v1.Request.protocol:type_name -> hetty.http.v1.Protocol
4, // 2: hetty.http.v1.Request.headers:type_name -> hetty.http.v1.Header
3, // 3: hetty.http.v1.Request.response:type_name -> hetty.http.v1.Response
1, // 4: hetty.http.v1.Response.protocol:type_name -> hetty.http.v1.Protocol
4, // 5: hetty.http.v1.Response.headers:type_name -> hetty.http.v1.Header
6, // [6:6] is the sub-list for method output_type
6, // [6:6] is the sub-list for method input_type
6, // [6:6] is the sub-list for extension type_name
6, // [6:6] is the sub-list for extension extendee
0, // [0:6] is the sub-list for field type_name
}
func init() { file_http_http_proto_init() }
func file_http_http_proto_init() {
if File_http_http_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_http_http_proto_rawDesc,
NumEnums: 2,
NumMessages: 3,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_http_http_proto_goTypes,
DependencyIndexes: file_http_http_proto_depIdxs,
EnumInfos: file_http_http_proto_enumTypes,
MessageInfos: file_http_http_proto_msgTypes,
}.Build()
File_http_http_proto = out.File
file_http_http_proto_rawDesc = nil
file_http_http_proto_goTypes = nil
file_http_http_proto_depIdxs = nil
}

View File

@ -1,340 +0,0 @@
// Code generated by protoc-gen-connect-go. DO NOT EDIT.
//
// Source: proj/proj.proto
package proj
import (
connect "connectrpc.com/connect"
context "context"
errors "errors"
http "net/http"
strings "strings"
)
// This is a compile-time assertion to ensure that this generated file and the connect package are
// compatible. If you get a compiler error that this constant is not defined, this code was
// generated with a version of connect newer than the one compiled into your binary. You can fix the
// problem by either regenerating this code with an older version of connect or updating the connect
// version compiled into your binary.
const _ = connect.IsAtLeastVersion1_13_0
const (
// ProjectServiceName is the fully-qualified name of the ProjectService service.
ProjectServiceName = "hetty.proj.v1.ProjectService"
)
// These constants are the fully-qualified names of the RPCs defined in this package. They're
// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route.
//
// Note that these are different from the fully-qualified method names used by
// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to
// reflection-formatted method names, remove the leading slash and convert the remaining slash to a
// period.
const (
// ProjectServiceCreateProjectProcedure is the fully-qualified name of the ProjectService's
// CreateProject RPC.
ProjectServiceCreateProjectProcedure = "/hetty.proj.v1.ProjectService/CreateProject"
// ProjectServiceOpenProjectProcedure is the fully-qualified name of the ProjectService's
// OpenProject RPC.
ProjectServiceOpenProjectProcedure = "/hetty.proj.v1.ProjectService/OpenProject"
// ProjectServiceCloseProjectProcedure is the fully-qualified name of the ProjectService's
// CloseProject RPC.
ProjectServiceCloseProjectProcedure = "/hetty.proj.v1.ProjectService/CloseProject"
// ProjectServiceDeleteProjectProcedure is the fully-qualified name of the ProjectService's
// DeleteProject RPC.
ProjectServiceDeleteProjectProcedure = "/hetty.proj.v1.ProjectService/DeleteProject"
// ProjectServiceGetActiveProjectProcedure is the fully-qualified name of the ProjectService's
// GetActiveProject RPC.
ProjectServiceGetActiveProjectProcedure = "/hetty.proj.v1.ProjectService/GetActiveProject"
// ProjectServiceListProjectsProcedure is the fully-qualified name of the ProjectService's
// ListProjects RPC.
ProjectServiceListProjectsProcedure = "/hetty.proj.v1.ProjectService/ListProjects"
// ProjectServiceUpdateInterceptSettingsProcedure is the fully-qualified name of the
// ProjectService's UpdateInterceptSettings RPC.
ProjectServiceUpdateInterceptSettingsProcedure = "/hetty.proj.v1.ProjectService/UpdateInterceptSettings"
// ProjectServiceSetScopeRulesProcedure is the fully-qualified name of the ProjectService's
// SetScopeRules RPC.
ProjectServiceSetScopeRulesProcedure = "/hetty.proj.v1.ProjectService/SetScopeRules"
// ProjectServiceSetRequestLogsFilterProcedure is the fully-qualified name of the ProjectService's
// SetRequestLogsFilter RPC.
ProjectServiceSetRequestLogsFilterProcedure = "/hetty.proj.v1.ProjectService/SetRequestLogsFilter"
)
// ProjectServiceClient is a client for the hetty.proj.v1.ProjectService service.
type ProjectServiceClient interface {
CreateProject(context.Context, *connect.Request[CreateProjectRequest]) (*connect.Response[CreateProjectResponse], error)
OpenProject(context.Context, *connect.Request[OpenProjectRequest]) (*connect.Response[OpenProjectResponse], error)
CloseProject(context.Context, *connect.Request[CloseProjectRequest]) (*connect.Response[CloseProjectResponse], error)
DeleteProject(context.Context, *connect.Request[DeleteProjectRequest]) (*connect.Response[DeleteProjectResponse], error)
GetActiveProject(context.Context, *connect.Request[GetActiveProjectRequest]) (*connect.Response[GetActiveProjectResponse], error)
ListProjects(context.Context, *connect.Request[ListProjectsRequest]) (*connect.Response[ListProjectsResponse], error)
UpdateInterceptSettings(context.Context, *connect.Request[UpdateInterceptSettingsRequest]) (*connect.Response[UpdateInterceptSettingsResponse], error)
SetScopeRules(context.Context, *connect.Request[SetScopeRulesRequest]) (*connect.Response[SetScopeRulesResponse], error)
SetRequestLogsFilter(context.Context, *connect.Request[SetRequestLogsFilterRequest]) (*connect.Response[SetRequestLogsFilterResponse], error)
}
// NewProjectServiceClient constructs a client for the hetty.proj.v1.ProjectService service. By
// default, it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses,
// and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the
// connect.WithGRPC() or connect.WithGRPCWeb() options.
//
// The URL supplied here should be the base URL for the Connect or gRPC server (for example,
// http://api.acme.com or https://acme.com/grpc).
func NewProjectServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) ProjectServiceClient {
baseURL = strings.TrimRight(baseURL, "/")
projectServiceMethods := File_proj_proj_proto.Services().ByName("ProjectService").Methods()
return &projectServiceClient{
createProject: connect.NewClient[CreateProjectRequest, CreateProjectResponse](
httpClient,
baseURL+ProjectServiceCreateProjectProcedure,
connect.WithSchema(projectServiceMethods.ByName("CreateProject")),
connect.WithClientOptions(opts...),
),
openProject: connect.NewClient[OpenProjectRequest, OpenProjectResponse](
httpClient,
baseURL+ProjectServiceOpenProjectProcedure,
connect.WithSchema(projectServiceMethods.ByName("OpenProject")),
connect.WithClientOptions(opts...),
),
closeProject: connect.NewClient[CloseProjectRequest, CloseProjectResponse](
httpClient,
baseURL+ProjectServiceCloseProjectProcedure,
connect.WithSchema(projectServiceMethods.ByName("CloseProject")),
connect.WithClientOptions(opts...),
),
deleteProject: connect.NewClient[DeleteProjectRequest, DeleteProjectResponse](
httpClient,
baseURL+ProjectServiceDeleteProjectProcedure,
connect.WithSchema(projectServiceMethods.ByName("DeleteProject")),
connect.WithClientOptions(opts...),
),
getActiveProject: connect.NewClient[GetActiveProjectRequest, GetActiveProjectResponse](
httpClient,
baseURL+ProjectServiceGetActiveProjectProcedure,
connect.WithSchema(projectServiceMethods.ByName("GetActiveProject")),
connect.WithClientOptions(opts...),
),
listProjects: connect.NewClient[ListProjectsRequest, ListProjectsResponse](
httpClient,
baseURL+ProjectServiceListProjectsProcedure,
connect.WithSchema(projectServiceMethods.ByName("ListProjects")),
connect.WithClientOptions(opts...),
),
updateInterceptSettings: connect.NewClient[UpdateInterceptSettingsRequest, UpdateInterceptSettingsResponse](
httpClient,
baseURL+ProjectServiceUpdateInterceptSettingsProcedure,
connect.WithSchema(projectServiceMethods.ByName("UpdateInterceptSettings")),
connect.WithClientOptions(opts...),
),
setScopeRules: connect.NewClient[SetScopeRulesRequest, SetScopeRulesResponse](
httpClient,
baseURL+ProjectServiceSetScopeRulesProcedure,
connect.WithSchema(projectServiceMethods.ByName("SetScopeRules")),
connect.WithClientOptions(opts...),
),
setRequestLogsFilter: connect.NewClient[SetRequestLogsFilterRequest, SetRequestLogsFilterResponse](
httpClient,
baseURL+ProjectServiceSetRequestLogsFilterProcedure,
connect.WithSchema(projectServiceMethods.ByName("SetRequestLogsFilter")),
connect.WithClientOptions(opts...),
),
}
}
// projectServiceClient implements ProjectServiceClient.
type projectServiceClient struct {
createProject *connect.Client[CreateProjectRequest, CreateProjectResponse]
openProject *connect.Client[OpenProjectRequest, OpenProjectResponse]
closeProject *connect.Client[CloseProjectRequest, CloseProjectResponse]
deleteProject *connect.Client[DeleteProjectRequest, DeleteProjectResponse]
getActiveProject *connect.Client[GetActiveProjectRequest, GetActiveProjectResponse]
listProjects *connect.Client[ListProjectsRequest, ListProjectsResponse]
updateInterceptSettings *connect.Client[UpdateInterceptSettingsRequest, UpdateInterceptSettingsResponse]
setScopeRules *connect.Client[SetScopeRulesRequest, SetScopeRulesResponse]
setRequestLogsFilter *connect.Client[SetRequestLogsFilterRequest, SetRequestLogsFilterResponse]
}
// CreateProject calls hetty.proj.v1.ProjectService.CreateProject.
func (c *projectServiceClient) CreateProject(ctx context.Context, req *connect.Request[CreateProjectRequest]) (*connect.Response[CreateProjectResponse], error) {
return c.createProject.CallUnary(ctx, req)
}
// OpenProject calls hetty.proj.v1.ProjectService.OpenProject.
func (c *projectServiceClient) OpenProject(ctx context.Context, req *connect.Request[OpenProjectRequest]) (*connect.Response[OpenProjectResponse], error) {
return c.openProject.CallUnary(ctx, req)
}
// CloseProject calls hetty.proj.v1.ProjectService.CloseProject.
func (c *projectServiceClient) CloseProject(ctx context.Context, req *connect.Request[CloseProjectRequest]) (*connect.Response[CloseProjectResponse], error) {
return c.closeProject.CallUnary(ctx, req)
}
// DeleteProject calls hetty.proj.v1.ProjectService.DeleteProject.
func (c *projectServiceClient) DeleteProject(ctx context.Context, req *connect.Request[DeleteProjectRequest]) (*connect.Response[DeleteProjectResponse], error) {
return c.deleteProject.CallUnary(ctx, req)
}
// GetActiveProject calls hetty.proj.v1.ProjectService.GetActiveProject.
func (c *projectServiceClient) GetActiveProject(ctx context.Context, req *connect.Request[GetActiveProjectRequest]) (*connect.Response[GetActiveProjectResponse], error) {
return c.getActiveProject.CallUnary(ctx, req)
}
// ListProjects calls hetty.proj.v1.ProjectService.ListProjects.
func (c *projectServiceClient) ListProjects(ctx context.Context, req *connect.Request[ListProjectsRequest]) (*connect.Response[ListProjectsResponse], error) {
return c.listProjects.CallUnary(ctx, req)
}
// UpdateInterceptSettings calls hetty.proj.v1.ProjectService.UpdateInterceptSettings.
func (c *projectServiceClient) UpdateInterceptSettings(ctx context.Context, req *connect.Request[UpdateInterceptSettingsRequest]) (*connect.Response[UpdateInterceptSettingsResponse], error) {
return c.updateInterceptSettings.CallUnary(ctx, req)
}
// SetScopeRules calls hetty.proj.v1.ProjectService.SetScopeRules.
func (c *projectServiceClient) SetScopeRules(ctx context.Context, req *connect.Request[SetScopeRulesRequest]) (*connect.Response[SetScopeRulesResponse], error) {
return c.setScopeRules.CallUnary(ctx, req)
}
// SetRequestLogsFilter calls hetty.proj.v1.ProjectService.SetRequestLogsFilter.
func (c *projectServiceClient) SetRequestLogsFilter(ctx context.Context, req *connect.Request[SetRequestLogsFilterRequest]) (*connect.Response[SetRequestLogsFilterResponse], error) {
return c.setRequestLogsFilter.CallUnary(ctx, req)
}
// ProjectServiceHandler is an implementation of the hetty.proj.v1.ProjectService service.
type ProjectServiceHandler interface {
CreateProject(context.Context, *connect.Request[CreateProjectRequest]) (*connect.Response[CreateProjectResponse], error)
OpenProject(context.Context, *connect.Request[OpenProjectRequest]) (*connect.Response[OpenProjectResponse], error)
CloseProject(context.Context, *connect.Request[CloseProjectRequest]) (*connect.Response[CloseProjectResponse], error)
DeleteProject(context.Context, *connect.Request[DeleteProjectRequest]) (*connect.Response[DeleteProjectResponse], error)
GetActiveProject(context.Context, *connect.Request[GetActiveProjectRequest]) (*connect.Response[GetActiveProjectResponse], error)
ListProjects(context.Context, *connect.Request[ListProjectsRequest]) (*connect.Response[ListProjectsResponse], error)
UpdateInterceptSettings(context.Context, *connect.Request[UpdateInterceptSettingsRequest]) (*connect.Response[UpdateInterceptSettingsResponse], error)
SetScopeRules(context.Context, *connect.Request[SetScopeRulesRequest]) (*connect.Response[SetScopeRulesResponse], error)
SetRequestLogsFilter(context.Context, *connect.Request[SetRequestLogsFilterRequest]) (*connect.Response[SetRequestLogsFilterResponse], error)
}
// NewProjectServiceHandler builds an HTTP handler from the service implementation. It returns the
// path on which to mount the handler and the handler itself.
//
// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf
// and JSON codecs. They also support gzip compression.
func NewProjectServiceHandler(svc ProjectServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) {
projectServiceMethods := File_proj_proj_proto.Services().ByName("ProjectService").Methods()
projectServiceCreateProjectHandler := connect.NewUnaryHandler(
ProjectServiceCreateProjectProcedure,
svc.CreateProject,
connect.WithSchema(projectServiceMethods.ByName("CreateProject")),
connect.WithHandlerOptions(opts...),
)
projectServiceOpenProjectHandler := connect.NewUnaryHandler(
ProjectServiceOpenProjectProcedure,
svc.OpenProject,
connect.WithSchema(projectServiceMethods.ByName("OpenProject")),
connect.WithHandlerOptions(opts...),
)
projectServiceCloseProjectHandler := connect.NewUnaryHandler(
ProjectServiceCloseProjectProcedure,
svc.CloseProject,
connect.WithSchema(projectServiceMethods.ByName("CloseProject")),
connect.WithHandlerOptions(opts...),
)
projectServiceDeleteProjectHandler := connect.NewUnaryHandler(
ProjectServiceDeleteProjectProcedure,
svc.DeleteProject,
connect.WithSchema(projectServiceMethods.ByName("DeleteProject")),
connect.WithHandlerOptions(opts...),
)
projectServiceGetActiveProjectHandler := connect.NewUnaryHandler(
ProjectServiceGetActiveProjectProcedure,
svc.GetActiveProject,
connect.WithSchema(projectServiceMethods.ByName("GetActiveProject")),
connect.WithHandlerOptions(opts...),
)
projectServiceListProjectsHandler := connect.NewUnaryHandler(
ProjectServiceListProjectsProcedure,
svc.ListProjects,
connect.WithSchema(projectServiceMethods.ByName("ListProjects")),
connect.WithHandlerOptions(opts...),
)
projectServiceUpdateInterceptSettingsHandler := connect.NewUnaryHandler(
ProjectServiceUpdateInterceptSettingsProcedure,
svc.UpdateInterceptSettings,
connect.WithSchema(projectServiceMethods.ByName("UpdateInterceptSettings")),
connect.WithHandlerOptions(opts...),
)
projectServiceSetScopeRulesHandler := connect.NewUnaryHandler(
ProjectServiceSetScopeRulesProcedure,
svc.SetScopeRules,
connect.WithSchema(projectServiceMethods.ByName("SetScopeRules")),
connect.WithHandlerOptions(opts...),
)
projectServiceSetRequestLogsFilterHandler := connect.NewUnaryHandler(
ProjectServiceSetRequestLogsFilterProcedure,
svc.SetRequestLogsFilter,
connect.WithSchema(projectServiceMethods.ByName("SetRequestLogsFilter")),
connect.WithHandlerOptions(opts...),
)
return "/hetty.proj.v1.ProjectService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case ProjectServiceCreateProjectProcedure:
projectServiceCreateProjectHandler.ServeHTTP(w, r)
case ProjectServiceOpenProjectProcedure:
projectServiceOpenProjectHandler.ServeHTTP(w, r)
case ProjectServiceCloseProjectProcedure:
projectServiceCloseProjectHandler.ServeHTTP(w, r)
case ProjectServiceDeleteProjectProcedure:
projectServiceDeleteProjectHandler.ServeHTTP(w, r)
case ProjectServiceGetActiveProjectProcedure:
projectServiceGetActiveProjectHandler.ServeHTTP(w, r)
case ProjectServiceListProjectsProcedure:
projectServiceListProjectsHandler.ServeHTTP(w, r)
case ProjectServiceUpdateInterceptSettingsProcedure:
projectServiceUpdateInterceptSettingsHandler.ServeHTTP(w, r)
case ProjectServiceSetScopeRulesProcedure:
projectServiceSetScopeRulesHandler.ServeHTTP(w, r)
case ProjectServiceSetRequestLogsFilterProcedure:
projectServiceSetRequestLogsFilterHandler.ServeHTTP(w, r)
default:
http.NotFound(w, r)
}
})
}
// UnimplementedProjectServiceHandler returns CodeUnimplemented from all methods.
type UnimplementedProjectServiceHandler struct{}
func (UnimplementedProjectServiceHandler) CreateProject(context.Context, *connect.Request[CreateProjectRequest]) (*connect.Response[CreateProjectResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hetty.proj.v1.ProjectService.CreateProject is not implemented"))
}
func (UnimplementedProjectServiceHandler) OpenProject(context.Context, *connect.Request[OpenProjectRequest]) (*connect.Response[OpenProjectResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hetty.proj.v1.ProjectService.OpenProject is not implemented"))
}
func (UnimplementedProjectServiceHandler) CloseProject(context.Context, *connect.Request[CloseProjectRequest]) (*connect.Response[CloseProjectResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hetty.proj.v1.ProjectService.CloseProject is not implemented"))
}
func (UnimplementedProjectServiceHandler) DeleteProject(context.Context, *connect.Request[DeleteProjectRequest]) (*connect.Response[DeleteProjectResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hetty.proj.v1.ProjectService.DeleteProject is not implemented"))
}
func (UnimplementedProjectServiceHandler) GetActiveProject(context.Context, *connect.Request[GetActiveProjectRequest]) (*connect.Response[GetActiveProjectResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hetty.proj.v1.ProjectService.GetActiveProject is not implemented"))
}
func (UnimplementedProjectServiceHandler) ListProjects(context.Context, *connect.Request[ListProjectsRequest]) (*connect.Response[ListProjectsResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hetty.proj.v1.ProjectService.ListProjects is not implemented"))
}
func (UnimplementedProjectServiceHandler) UpdateInterceptSettings(context.Context, *connect.Request[UpdateInterceptSettingsRequest]) (*connect.Response[UpdateInterceptSettingsResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hetty.proj.v1.ProjectService.UpdateInterceptSettings is not implemented"))
}
func (UnimplementedProjectServiceHandler) SetScopeRules(context.Context, *connect.Request[SetScopeRulesRequest]) (*connect.Response[SetScopeRulesResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hetty.proj.v1.ProjectService.SetScopeRules is not implemented"))
}
func (UnimplementedProjectServiceHandler) SetRequestLogsFilter(context.Context, *connect.Request[SetRequestLogsFilterRequest]) (*connect.Response[SetRequestLogsFilterResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hetty.proj.v1.ProjectService.SetRequestLogsFilter is not implemented"))
}

View File

@ -4,11 +4,12 @@ import (
"context"
"errors"
"fmt"
"math/rand"
"regexp"
"sync"
"time"
connect "connectrpc.com/connect"
"github.com/oklog/ulid/v2"
"github.com/oklog/ulid"
"github.com/dstotijn/hetty/pkg/filter"
"github.com/dstotijn/hetty/pkg/proxy/intercept"
@ -17,16 +18,43 @@ import (
"github.com/dstotijn/hetty/pkg/sender"
)
type Service struct {
//nolint:gosec
var ulidEntropy = rand.New(rand.NewSource(time.Now().UnixNano()))
// Service is used for managing projects.
type Service interface {
CreateProject(ctx context.Context, name string) (Project, error)
OpenProject(ctx context.Context, projectID ulid.ULID) (Project, error)
CloseProject() error
DeleteProject(ctx context.Context, projectID ulid.ULID) error
ActiveProject(ctx context.Context) (Project, error)
IsProjectActive(projectID ulid.ULID) bool
Projects(ctx context.Context) ([]Project, error)
Scope() *scope.Scope
SetScopeRules(ctx context.Context, rules []scope.Rule) error
SetRequestLogFindFilter(ctx context.Context, filter reqlog.FindRequestsFilter) error
SetSenderRequestFindFilter(ctx context.Context, filter sender.FindRequestsFilter) error
UpdateInterceptSettings(ctx context.Context, settings intercept.Settings) error
}
type service struct {
repo Repository
interceptSvc *intercept.Service
reqLogSvc *reqlog.Service
senderSvc *sender.Service
reqLogSvc reqlog.Service
senderSvc sender.Service
scope *scope.Scope
activeProjectID string
activeProjectID ulid.ULID
mu sync.RWMutex
}
type Project struct {
ID ulid.ULID
Name string
Settings Settings
isActive bool
}
type Settings struct {
// Request log settings
ReqLogBypassOutOfScope bool
@ -59,14 +87,14 @@ var nameRegexp = regexp.MustCompile(`^[\w\d\s]+$`)
type Config struct {
Repository Repository
InterceptService *intercept.Service
ReqLogService *reqlog.Service
SenderService *sender.Service
ReqLogService reqlog.Service
SenderService sender.Service
Scope *scope.Scope
}
// NewService returns a new Service.
func NewService(cfg Config) (*Service, error) {
return &Service{
func NewService(cfg Config) (Service, error) {
return &service{
repo: cfg.Repository,
interceptSvc: cfg.InterceptService,
reqLogSvc: cfg.ReqLogService,
@ -75,324 +103,216 @@ func NewService(cfg Config) (*Service, error) {
}, nil
}
func (svc *Service) CreateProject(ctx context.Context, req *connect.Request[CreateProjectRequest]) (*connect.Response[CreateProjectResponse], error) {
if !nameRegexp.MatchString(req.Msg.Name) {
return nil, ErrInvalidName
func (svc *service) CreateProject(ctx context.Context, name string) (Project, error) {
if !nameRegexp.MatchString(name) {
return Project{}, ErrInvalidName
}
project := &Project{
Id: ulid.Make().String(),
Name: req.Msg.Name,
project := Project{
ID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
Name: name,
}
err := svc.repo.UpsertProject(ctx, project)
if err != nil {
return nil, fmt.Errorf("proj: could not create project: %w", err)
return Project{}, fmt.Errorf("proj: could not create project: %w", err)
}
return &connect.Response[CreateProjectResponse]{
Msg: &CreateProjectResponse{
Project: project,
},
}, nil
return project, nil
}
// CloseProject closes the currently open project (if there is one).
func (svc *Service) CloseProject(ctx context.Context, _ *connect.Request[CloseProjectRequest]) (*connect.Response[CloseProjectResponse], error) {
func (svc *service) CloseProject() error {
svc.mu.Lock()
defer svc.mu.Unlock()
if svc.activeProjectID == "" {
return nil, connect.NewError(connect.CodeFailedPrecondition, ErrNoProject)
if svc.activeProjectID.Compare(ulid.ULID{}) == 0 {
return nil
}
svc.activeProjectID = ""
svc.reqLogSvc.SetActiveProjectID("")
svc.activeProjectID = ulid.ULID{}
svc.reqLogSvc.SetActiveProjectID(ulid.ULID{})
svc.reqLogSvc.SetBypassOutOfScopeRequests(false)
svc.reqLogSvc.SetRequestLogsFilter(nil)
svc.reqLogSvc.SetFindReqsFilter(reqlog.FindRequestsFilter{})
svc.interceptSvc.UpdateSettings(intercept.Settings{
RequestsEnabled: false,
ResponsesEnabled: false,
RequestFilter: nil,
ResponseFilter: nil,
})
svc.senderSvc.SetActiveProjectID("")
svc.senderSvc.SetActiveProjectID(ulid.ULID{})
svc.senderSvc.SetFindReqsFilter(sender.FindRequestsFilter{})
svc.scope.SetRules(nil)
return &connect.Response[CloseProjectResponse]{}, nil
return nil
}
// DeleteProject removes a project from the repository.
func (svc *Service) DeleteProject(ctx context.Context, req *connect.Request[DeleteProjectRequest]) (*connect.Response[DeleteProjectResponse], error) {
if svc.activeProjectID == "" {
return nil, connect.NewError(connect.CodeFailedPrecondition, ErrNoProject)
func (svc *service) DeleteProject(ctx context.Context, projectID ulid.ULID) error {
if svc.activeProjectID.Compare(projectID) == 0 {
return fmt.Errorf("proj: project (%v) is active", projectID.String())
}
if err := svc.repo.DeleteProject(ctx, req.Msg.ProjectId); err != nil {
return nil, fmt.Errorf("proj: could not delete project: %w", err)
if err := svc.repo.DeleteProject(ctx, projectID); err != nil {
return fmt.Errorf("proj: could not delete project: %w", err)
}
return &connect.Response[DeleteProjectResponse]{}, nil
return nil
}
// OpenProject sets a project as the currently active project.
func (svc *Service) OpenProject(ctx context.Context, req *connect.Request[OpenProjectRequest]) (*connect.Response[OpenProjectResponse], error) {
func (svc *service) OpenProject(ctx context.Context, projectID ulid.ULID) (Project, error) {
svc.mu.Lock()
defer svc.mu.Unlock()
p, err := svc.repo.FindProjectByID(ctx, req.Msg.ProjectId)
if errors.Is(err, ErrProjectNotFound) {
return nil, connect.NewError(connect.CodeNotFound, ErrProjectNotFound)
}
project, err := svc.repo.FindProjectByID(ctx, projectID)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("proj: failed to find project: %w", err))
return Project{}, fmt.Errorf("proj: failed to get project: %w", err)
}
svc.activeProjectID = p.Id
interceptSettings := intercept.Settings{
RequestsEnabled: p.InterceptRequests,
ResponsesEnabled: p.InterceptResponses,
}
if p.InterceptRequestFilterExpr != "" {
expr, err := filter.ParseQuery(p.InterceptRequestFilterExpr)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("proj: failed to parse intercept request filter: %w", err))
}
interceptSettings.RequestFilter = expr
}
if p.InterceptResponseFilterExpr != "" {
expr, err := filter.ParseQuery(p.InterceptResponseFilterExpr)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("proj: failed to parse intercept response filter: %w", err))
}
interceptSettings.ResponseFilter = expr
}
svc.activeProjectID = project.ID
// Request log settings.
svc.reqLogSvc.SetActiveProjectID(p.Id)
svc.reqLogSvc.SetBypassOutOfScopeRequests(p.ReqLogBypassOutOfScope)
svc.reqLogSvc.SetRequestLogsFilter(p.ReqLogFilter)
svc.reqLogSvc.SetFindReqsFilter(reqlog.FindRequestsFilter{
ProjectID: project.ID,
OnlyInScope: project.Settings.ReqLogOnlyFindInScope,
SearchExpr: project.Settings.ReqLogSearchExpr,
})
svc.reqLogSvc.SetBypassOutOfScopeRequests(project.Settings.ReqLogBypassOutOfScope)
svc.reqLogSvc.SetActiveProjectID(project.ID)
// Intercept settings.
svc.interceptSvc.UpdateSettings(interceptSettings)
svc.interceptSvc.UpdateSettings(intercept.Settings{
RequestsEnabled: project.Settings.InterceptRequests,
ResponsesEnabled: project.Settings.InterceptResponses,
RequestFilter: project.Settings.InterceptRequestFilter,
ResponseFilter: project.Settings.InterceptResponseFilter,
})
// Sender settings.
svc.senderSvc.SetActiveProjectID(p.Id)
svc.senderSvc.SetRequestsFilter(&sender.RequestsFilter{
OnlyInScope: p.SenderOnlyFindInScope,
SearchExpr: p.SenderSearchExpr,
svc.senderSvc.SetActiveProjectID(project.ID)
svc.senderSvc.SetFindReqsFilter(sender.FindRequestsFilter{
ProjectID: project.ID,
OnlyInScope: project.Settings.SenderOnlyFindInScope,
SearchExpr: project.Settings.SenderSearchExpr,
})
// Scope settings.
scopeRules, err := p.ParseScopeRules()
if err != nil {
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("proj: failed to parse scope rules: %w", err))
}
svc.scope.SetRules(scopeRules)
p.IsActive = true
return &connect.Response[OpenProjectResponse]{
Msg: &OpenProjectResponse{
Project: p,
},
}, nil
}
func (svc *Service) GetActiveProject(ctx context.Context, _ *connect.Request[GetActiveProjectRequest]) (*connect.Response[GetActiveProjectResponse], error) {
project, err := svc.activeProject(ctx)
if errors.Is(err, ErrNoProject) {
return nil, connect.NewError(connect.CodeNotFound, err)
}
if err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
project.IsActive = true
return &connect.Response[GetActiveProjectResponse]{
Msg: &GetActiveProjectResponse{
Project: project,
},
}, nil
}
func (svc *Service) activeProject(ctx context.Context) (*Project, error) {
if svc.activeProjectID == "" {
return nil, ErrNoProject
}
project, err := svc.repo.FindProjectByID(ctx, svc.activeProjectID)
if err != nil {
return nil, fmt.Errorf("proj: failed to get active project: %w", err)
}
svc.scope.SetRules(project.Settings.ScopeRules)
return project, nil
}
func (svc *Service) ListProjects(ctx context.Context, _ *connect.Request[ListProjectsRequest]) (*connect.Response[ListProjectsResponse], error) {
func (svc *service) ActiveProject(ctx context.Context) (Project, error) {
activeProjectID := svc.activeProjectID
if activeProjectID.Compare(ulid.ULID{}) == 0 {
return Project{}, ErrNoProject
}
project, err := svc.repo.FindProjectByID(ctx, activeProjectID)
if err != nil {
return Project{}, fmt.Errorf("proj: failed to get active project: %w", err)
}
project.isActive = true
return project, nil
}
func (svc *service) Projects(ctx context.Context) ([]Project, error) {
projects, err := svc.repo.Projects(ctx)
if err != nil {
return nil, fmt.Errorf("proj: could not get projects: %w", err)
}
for _, project := range projects {
if svc.IsProjectActive(project.Id) {
project.IsActive = true
}
}
return &connect.Response[ListProjectsResponse]{
Msg: &ListProjectsResponse{
Projects: projects,
},
}, nil
return projects, nil
}
func (svc *Service) Scope() *scope.Scope {
func (svc *service) Scope() *scope.Scope {
return svc.scope
}
func (svc *Service) SetScopeRules(ctx context.Context, req *connect.Request[SetScopeRulesRequest]) (*connect.Response[SetScopeRulesResponse], error) {
p, err := svc.activeProject(ctx)
if errors.Is(err, ErrNoProject) {
return nil, connect.NewError(connect.CodeFailedPrecondition, err)
}
if err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
p.ScopeRules = req.Msg.Rules
err = svc.repo.UpsertProject(ctx, p)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("proj: failed to update project: %w", err))
}
scopeRules, err := p.ParseScopeRules()
if err != nil {
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("proj: failed to parse scope rules: %w", err))
}
svc.scope.SetRules(scopeRules)
return &connect.Response[SetScopeRulesResponse]{}, nil
}
func (p *Project) ParseScopeRules() ([]scope.Rule, error) {
var err error
scopeRules := make([]scope.Rule, len(p.ScopeRules))
for i, rule := range p.ScopeRules {
scopeRules[i] = scope.Rule{}
if rule.UrlRegexp != "" {
scopeRules[i].URL, err = regexp.Compile(rule.UrlRegexp)
if err != nil {
return nil, fmt.Errorf("failed to parse scope rule's URL field: %w", err)
}
}
if rule.HeaderKeyRegexp != "" {
scopeRules[i].Header.Key, err = regexp.Compile(rule.HeaderKeyRegexp)
if err != nil {
return nil, fmt.Errorf("failed to parse scope rule's header key field: %w", err)
}
}
if rule.HeaderValueRegexp != "" {
scopeRules[i].Header.Value, err = regexp.Compile(rule.HeaderValueRegexp)
if err != nil {
return nil, fmt.Errorf("failed to parse scope rule's header value field: %w", err)
}
}
if rule.BodyRegexp != "" {
scopeRules[i].Body, err = regexp.Compile(rule.BodyRegexp)
if err != nil {
return nil, fmt.Errorf("failed to parse scope rule's body field: %w", err)
}
}
}
return scopeRules, nil
}
func (svc *Service) SetRequestLogsFilter(ctx context.Context, req *connect.Request[SetRequestLogsFilterRequest]) (*connect.Response[SetRequestLogsFilterResponse], error) {
project, err := svc.activeProject(ctx)
if errors.Is(err, ErrNoProject) {
return nil, connect.NewError(connect.CodeFailedPrecondition, err)
}
if err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
project.ReqLogFilter = req.Msg.Filter
err = svc.repo.UpsertProject(ctx, project)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("proj: failed to update project: %w", err))
}
svc.reqLogSvc.SetRequestLogsFilter(req.Msg.Filter)
return &connect.Response[SetRequestLogsFilterResponse]{}, nil
}
func (svc *Service) SetSenderRequestFindFilter(ctx context.Context, filter *sender.RequestsFilter) error {
project, err := svc.activeProject(ctx)
func (svc *service) SetScopeRules(ctx context.Context, rules []scope.Rule) error {
project, err := svc.ActiveProject(ctx)
if err != nil {
return err
}
project.SenderOnlyFindInScope = filter.OnlyInScope
project.SenderSearchExpr = filter.SearchExpr
project.Settings.ScopeRules = rules
err = svc.repo.UpsertProject(ctx, project)
if err != nil {
return fmt.Errorf("proj: failed to update project: %w", err)
}
svc.senderSvc.SetRequestsFilter(filter)
svc.scope.SetRules(rules)
return nil
}
func (svc *Service) IsProjectActive(projectID string) bool {
return projectID == svc.activeProjectID
}
func (svc *Service) UpdateInterceptSettings(ctx context.Context, req *connect.Request[UpdateInterceptSettingsRequest]) (*connect.Response[UpdateInterceptSettingsResponse], error) {
project, err := svc.activeProject(ctx)
func (svc *service) SetRequestLogFindFilter(ctx context.Context, filter reqlog.FindRequestsFilter) error {
project, err := svc.ActiveProject(ctx)
if err != nil {
return nil, err
return err
}
project.InterceptRequests = req.Msg.RequestsEnabled
project.InterceptResponses = req.Msg.ResponsesEnabled
project.InterceptRequestFilterExpr = req.Msg.RequestFilterExpr
project.InterceptResponseFilterExpr = req.Msg.ResponseFilterExpr
filter.ProjectID = project.ID
project.Settings.ReqLogOnlyFindInScope = filter.OnlyInScope
project.Settings.ReqLogSearchExpr = filter.SearchExpr
err = svc.repo.UpsertProject(ctx, project)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("proj: failed to update project: %w", err))
return fmt.Errorf("proj: failed to update project: %w", err)
}
reqFilterExpr, err := filter.ParseQuery(req.Msg.RequestFilterExpr)
if err != nil {
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("proj: failed to parse intercept request filter: %w", err))
}
svc.reqLogSvc.SetFindReqsFilter(filter)
respFilterExpr, err := filter.ParseQuery(req.Msg.ResponseFilterExpr)
if err != nil {
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("proj: failed to parse intercept response filter: %w", err))
}
svc.interceptSvc.UpdateSettings(intercept.Settings{
RequestsEnabled: req.Msg.RequestsEnabled,
ResponsesEnabled: req.Msg.ResponsesEnabled,
RequestFilter: reqFilterExpr,
ResponseFilter: respFilterExpr,
})
return &connect.Response[UpdateInterceptSettingsResponse]{}, nil
return nil
}
func (svc *service) SetSenderRequestFindFilter(ctx context.Context, filter sender.FindRequestsFilter) error {
project, err := svc.ActiveProject(ctx)
if err != nil {
return err
}
filter.ProjectID = project.ID
project.Settings.SenderOnlyFindInScope = filter.OnlyInScope
project.Settings.SenderSearchExpr = filter.SearchExpr
err = svc.repo.UpsertProject(ctx, project)
if err != nil {
return fmt.Errorf("proj: failed to update project: %w", err)
}
svc.senderSvc.SetFindReqsFilter(filter)
return nil
}
func (svc *service) IsProjectActive(projectID ulid.ULID) bool {
return projectID.Compare(svc.activeProjectID) == 0
}
func (svc *service) UpdateInterceptSettings(ctx context.Context, settings intercept.Settings) error {
project, err := svc.ActiveProject(ctx)
if err != nil {
return err
}
project.Settings.InterceptRequests = settings.RequestsEnabled
project.Settings.InterceptResponses = settings.ResponsesEnabled
project.Settings.InterceptRequestFilter = settings.RequestFilter
project.Settings.InterceptResponseFilter = settings.ResponseFilter
err = svc.repo.UpsertProject(ctx, project)
if err != nil {
return fmt.Errorf("proj: failed to update project: %w", err)
}
svc.interceptSvc.UpdateSettings(settings)
return nil
}

File diff suppressed because it is too large Load Diff

View File

@ -2,12 +2,14 @@ package proj
import (
"context"
"github.com/oklog/ulid"
)
type Repository interface {
FindProjectByID(ctx context.Context, id string) (*Project, error)
UpsertProject(ctx context.Context, project *Project) error
DeleteProject(ctx context.Context, id string) error
Projects(ctx context.Context) ([]*Project, error)
FindProjectByID(ctx context.Context, id ulid.ULID) (Project, error)
UpsertProject(ctx context.Context, project Project) error
DeleteProject(ctx context.Context, id ulid.ULID) error
Projects(ctx context.Context) ([]Project, error)
Close() error
}

View File

@ -5,12 +5,12 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"strconv"
"strings"
"github.com/dstotijn/hetty/pkg/filter"
httppb "github.com/dstotijn/hetty/pkg/http"
"github.com/dstotijn/hetty/pkg/scope"
)
@ -34,7 +34,7 @@ var reqFilterKeyFns = map[string]func(req *http.Request) (string, error){
return "", err
}
req.Body = io.NopCloser(bytes.NewBuffer(body))
req.Body = ioutil.NopCloser(bytes.NewBuffer(body))
return string(body), nil
},
}
@ -61,7 +61,7 @@ var resFilterKeyFns = map[string]func(res *http.Response) (string, error){
return "", err
}
res.Body = io.NopCloser(bytes.NewBuffer(body))
res.Body = ioutil.NopCloser(bytes.NewBuffer(body))
return string(body), nil
},
@ -134,7 +134,7 @@ func matchReqInfixExpr(req *http.Request, expr filter.InfixExpression) (bool, er
}
if leftVal == "headers" {
match, err := filter.MatchHTTPHeaders(expr.Operator, expr.Right, httppb.ParseHeader(req.Header))
match, err := filter.MatchHTTPHeaders(expr.Operator, expr.Right, req.Header)
if err != nil {
return false, fmt.Errorf("failed to match request HTTP headers: %w", err)
}
@ -267,7 +267,7 @@ func MatchRequestScope(req *http.Request, s *scope.Scope) (bool, error) {
return false, fmt.Errorf("failed to read request body: %w", err)
}
req.Body = io.NopCloser(bytes.NewBuffer(body))
req.Body = ioutil.NopCloser(bytes.NewBuffer(body))
if matches := rule.Body.Match(body); matches {
return true, nil
@ -345,7 +345,7 @@ func matchResInfixExpr(res *http.Response, expr filter.InfixExpression) (bool, e
}
if leftVal == "headers" {
match, err := filter.MatchHTTPHeaders(expr.Operator, expr.Right, httppb.ParseHeader(res.Header))
match, err := filter.MatchHTTPHeaders(expr.Operator, expr.Right, res.Header)
if err != nil {
return false, fmt.Errorf("failed to match request HTTP headers: %w", err)
}

View File

@ -8,7 +8,7 @@ import (
"sort"
"sync"
"github.com/oklog/ulid/v2"
"github.com/oklog/ulid"
"github.com/dstotijn/hetty/pkg/filter"
"github.com/dstotijn/hetty/pkg/log"

View File

@ -3,19 +3,23 @@ package intercept_test
import (
"context"
"errors"
"math/rand"
"net/http"
"net/http/httptest"
"sync"
"testing"
"time"
"github.com/oklog/ulid/v2"
"github.com/oklog/ulid"
"go.uber.org/zap"
"github.com/dstotijn/hetty/pkg/proxy"
"github.com/dstotijn/hetty/pkg/proxy/intercept"
)
//nolint:gosec
var ulidEntropy = rand.New(rand.NewSource(time.Now().UnixNano()))
func TestRequestModifier(t *testing.T) {
t.Parallel()
@ -29,7 +33,7 @@ func TestRequestModifier(t *testing.T) {
ResponsesEnabled: false,
})
reqID := ulid.Make()
reqID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
err := svc.ModifyRequest(reqID, nil, nil)
if !errors.Is(err, intercept.ErrRequestNotFound) {
@ -51,7 +55,7 @@ func TestRequestModifier(t *testing.T) {
defer cancel()
req := httptest.NewRequest("GET", "https://example.com/foo", nil)
reqID := ulid.Make()
reqID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
*req = *req.WithContext(ctx)
*req = *req.WithContext(proxy.WithRequestID(req.Context(), reqID))
@ -78,7 +82,7 @@ func TestRequestModifier(t *testing.T) {
req := httptest.NewRequest("GET", "https://example.com/foo", nil)
req.Header.Set("X-Foo", "foo")
reqID := ulid.Make()
reqID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
*req = *req.WithContext(proxy.WithRequestID(req.Context(), reqID))
modReq := req.Clone(context.Background())
@ -139,7 +143,7 @@ func TestResponseModifier(t *testing.T) {
ResponsesEnabled: true,
})
reqID := ulid.Make()
reqID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
err := svc.ModifyResponse(reqID, nil)
if !errors.Is(err, intercept.ErrRequestNotFound) {
@ -161,7 +165,7 @@ func TestResponseModifier(t *testing.T) {
defer cancel()
req := httptest.NewRequest("GET", "https://example.com/foo", nil)
reqID := ulid.Make()
reqID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
*req = *req.WithContext(ctx)
*req = *req.WithContext(proxy.WithRequestID(req.Context(), reqID))
@ -208,7 +212,7 @@ func TestResponseModifier(t *testing.T) {
req := httptest.NewRequest("GET", "https://example.com/foo", nil)
req.Header.Set("X-Foo", "foo")
reqID := ulid.Make()
reqID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
*req = *req.WithContext(proxy.WithRequestID(req.Context(), reqID))
res := &http.Response{

View File

@ -7,17 +7,21 @@ import (
"crypto/x509"
"errors"
"fmt"
"math/rand"
"net"
"net/http"
"net/http/httputil"
"strings"
"time"
"github.com/oklog/ulid/v2"
"github.com/oklog/ulid"
"github.com/dstotijn/hetty/pkg/log"
)
//nolint:gosec
var ulidEntropy = rand.New(rand.NewSource(time.Now().UnixNano()))
type contextKey int
const reqIDKey contextKey = 0
@ -91,7 +95,7 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
reqID := ulid.Make()
reqID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
ctx := context.WithValue(r.Context(), reqIDKey, reqID)
*r = *r.WithContext(ctx)

View File

@ -3,13 +3,15 @@ package reqlog
import (
"context"
httppb "github.com/dstotijn/hetty/pkg/http"
"github.com/oklog/ulid"
"github.com/dstotijn/hetty/pkg/scope"
)
type Repository interface {
FindRequestLogs(ctx context.Context, projectID string, filterFn func(*HttpRequestLog) (bool, error)) ([]*HttpRequestLog, error)
FindRequestLogByID(ctx context.Context, projectID, id string) (*HttpRequestLog, error)
StoreRequestLog(ctx context.Context, reqLog *HttpRequestLog) error
StoreResponseLog(ctx context.Context, projectID, reqLogID string, resLog *httppb.Response) error
ClearRequestLogs(ctx context.Context, projectID string) error
FindRequestLogs(ctx context.Context, filter FindRequestsFilter, scope *scope.Scope) ([]RequestLog, error)
FindRequestLogByID(ctx context.Context, id ulid.ULID) (RequestLog, error)
StoreRequestLog(ctx context.Context, reqLog RequestLog) error
StoreResponseLog(ctx context.Context, reqLogID ulid.ULID, resLog ResponseLog) error
ClearRequestLogs(ctx context.Context, projectID ulid.ULID) error
}

View File

@ -0,0 +1,291 @@
// Code generated by moq; DO NOT EDIT.
// github.com/matryer/moq
package reqlog_test
import (
"context"
"github.com/dstotijn/hetty/pkg/reqlog"
"github.com/dstotijn/hetty/pkg/scope"
"github.com/oklog/ulid"
"sync"
)
// Ensure, that RepoMock does implement reqlog.Repository.
// If this is not the case, regenerate this file with moq.
var _ reqlog.Repository = &RepoMock{}
// RepoMock is a mock implementation of reqlog.Repository.
//
// func TestSomethingThatUsesRepository(t *testing.T) {
//
// // make and configure a mocked reqlog.Repository
// mockedRepository := &RepoMock{
// ClearRequestLogsFunc: func(ctx context.Context, projectID ulid.ULID) error {
// panic("mock out the ClearRequestLogs method")
// },
// FindRequestLogByIDFunc: func(ctx context.Context, id ulid.ULID) (reqlog.RequestLog, error) {
// panic("mock out the FindRequestLogByID method")
// },
// FindRequestLogsFunc: func(ctx context.Context, filter reqlog.FindRequestsFilter, scopeMoqParam *scope.Scope) ([]reqlog.RequestLog, error) {
// panic("mock out the FindRequestLogs method")
// },
// StoreRequestLogFunc: func(ctx context.Context, reqLog reqlog.RequestLog) error {
// panic("mock out the StoreRequestLog method")
// },
// StoreResponseLogFunc: func(ctx context.Context, reqLogID ulid.ULID, resLog reqlog.ResponseLog) error {
// panic("mock out the StoreResponseLog method")
// },
// }
//
// // use mockedRepository in code that requires reqlog.Repository
// // and then make assertions.
//
// }
type RepoMock struct {
// ClearRequestLogsFunc mocks the ClearRequestLogs method.
ClearRequestLogsFunc func(ctx context.Context, projectID ulid.ULID) error
// FindRequestLogByIDFunc mocks the FindRequestLogByID method.
FindRequestLogByIDFunc func(ctx context.Context, id ulid.ULID) (reqlog.RequestLog, error)
// FindRequestLogsFunc mocks the FindRequestLogs method.
FindRequestLogsFunc func(ctx context.Context, filter reqlog.FindRequestsFilter, scopeMoqParam *scope.Scope) ([]reqlog.RequestLog, error)
// StoreRequestLogFunc mocks the StoreRequestLog method.
StoreRequestLogFunc func(ctx context.Context, reqLog reqlog.RequestLog) error
// StoreResponseLogFunc mocks the StoreResponseLog method.
StoreResponseLogFunc func(ctx context.Context, reqLogID ulid.ULID, resLog reqlog.ResponseLog) error
// calls tracks calls to the methods.
calls struct {
// ClearRequestLogs holds details about calls to the ClearRequestLogs method.
ClearRequestLogs []struct {
// Ctx is the ctx argument value.
Ctx context.Context
// ProjectID is the projectID argument value.
ProjectID ulid.ULID
}
// FindRequestLogByID holds details about calls to the FindRequestLogByID method.
FindRequestLogByID []struct {
// Ctx is the ctx argument value.
Ctx context.Context
// ID is the id argument value.
ID ulid.ULID
}
// FindRequestLogs holds details about calls to the FindRequestLogs method.
FindRequestLogs []struct {
// Ctx is the ctx argument value.
Ctx context.Context
// Filter is the filter argument value.
Filter reqlog.FindRequestsFilter
// ScopeMoqParam is the scopeMoqParam argument value.
ScopeMoqParam *scope.Scope
}
// StoreRequestLog holds details about calls to the StoreRequestLog method.
StoreRequestLog []struct {
// Ctx is the ctx argument value.
Ctx context.Context
// ReqLog is the reqLog argument value.
ReqLog reqlog.RequestLog
}
// StoreResponseLog holds details about calls to the StoreResponseLog method.
StoreResponseLog []struct {
// Ctx is the ctx argument value.
Ctx context.Context
// ReqLogID is the reqLogID argument value.
ReqLogID ulid.ULID
// ResLog is the resLog argument value.
ResLog reqlog.ResponseLog
}
}
lockClearRequestLogs sync.RWMutex
lockFindRequestLogByID sync.RWMutex
lockFindRequestLogs sync.RWMutex
lockStoreRequestLog sync.RWMutex
lockStoreResponseLog sync.RWMutex
}
// ClearRequestLogs calls ClearRequestLogsFunc.
func (mock *RepoMock) ClearRequestLogs(ctx context.Context, projectID ulid.ULID) error {
if mock.ClearRequestLogsFunc == nil {
panic("RepoMock.ClearRequestLogsFunc: method is nil but Repository.ClearRequestLogs was just called")
}
callInfo := struct {
Ctx context.Context
ProjectID ulid.ULID
}{
Ctx: ctx,
ProjectID: projectID,
}
mock.lockClearRequestLogs.Lock()
mock.calls.ClearRequestLogs = append(mock.calls.ClearRequestLogs, callInfo)
mock.lockClearRequestLogs.Unlock()
return mock.ClearRequestLogsFunc(ctx, projectID)
}
// ClearRequestLogsCalls gets all the calls that were made to ClearRequestLogs.
// Check the length with:
// len(mockedRepository.ClearRequestLogsCalls())
func (mock *RepoMock) ClearRequestLogsCalls() []struct {
Ctx context.Context
ProjectID ulid.ULID
} {
var calls []struct {
Ctx context.Context
ProjectID ulid.ULID
}
mock.lockClearRequestLogs.RLock()
calls = mock.calls.ClearRequestLogs
mock.lockClearRequestLogs.RUnlock()
return calls
}
// FindRequestLogByID calls FindRequestLogByIDFunc.
func (mock *RepoMock) FindRequestLogByID(ctx context.Context, id ulid.ULID) (reqlog.RequestLog, error) {
if mock.FindRequestLogByIDFunc == nil {
panic("RepoMock.FindRequestLogByIDFunc: method is nil but Repository.FindRequestLogByID was just called")
}
callInfo := struct {
Ctx context.Context
ID ulid.ULID
}{
Ctx: ctx,
ID: id,
}
mock.lockFindRequestLogByID.Lock()
mock.calls.FindRequestLogByID = append(mock.calls.FindRequestLogByID, callInfo)
mock.lockFindRequestLogByID.Unlock()
return mock.FindRequestLogByIDFunc(ctx, id)
}
// FindRequestLogByIDCalls gets all the calls that were made to FindRequestLogByID.
// Check the length with:
// len(mockedRepository.FindRequestLogByIDCalls())
func (mock *RepoMock) FindRequestLogByIDCalls() []struct {
Ctx context.Context
ID ulid.ULID
} {
var calls []struct {
Ctx context.Context
ID ulid.ULID
}
mock.lockFindRequestLogByID.RLock()
calls = mock.calls.FindRequestLogByID
mock.lockFindRequestLogByID.RUnlock()
return calls
}
// FindRequestLogs calls FindRequestLogsFunc.
func (mock *RepoMock) FindRequestLogs(ctx context.Context, filter reqlog.FindRequestsFilter, scopeMoqParam *scope.Scope) ([]reqlog.RequestLog, error) {
if mock.FindRequestLogsFunc == nil {
panic("RepoMock.FindRequestLogsFunc: method is nil but Repository.FindRequestLogs was just called")
}
callInfo := struct {
Ctx context.Context
Filter reqlog.FindRequestsFilter
ScopeMoqParam *scope.Scope
}{
Ctx: ctx,
Filter: filter,
ScopeMoqParam: scopeMoqParam,
}
mock.lockFindRequestLogs.Lock()
mock.calls.FindRequestLogs = append(mock.calls.FindRequestLogs, callInfo)
mock.lockFindRequestLogs.Unlock()
return mock.FindRequestLogsFunc(ctx, filter, scopeMoqParam)
}
// FindRequestLogsCalls gets all the calls that were made to FindRequestLogs.
// Check the length with:
// len(mockedRepository.FindRequestLogsCalls())
func (mock *RepoMock) FindRequestLogsCalls() []struct {
Ctx context.Context
Filter reqlog.FindRequestsFilter
ScopeMoqParam *scope.Scope
} {
var calls []struct {
Ctx context.Context
Filter reqlog.FindRequestsFilter
ScopeMoqParam *scope.Scope
}
mock.lockFindRequestLogs.RLock()
calls = mock.calls.FindRequestLogs
mock.lockFindRequestLogs.RUnlock()
return calls
}
// StoreRequestLog calls StoreRequestLogFunc.
func (mock *RepoMock) StoreRequestLog(ctx context.Context, reqLog reqlog.RequestLog) error {
if mock.StoreRequestLogFunc == nil {
panic("RepoMock.StoreRequestLogFunc: method is nil but Repository.StoreRequestLog was just called")
}
callInfo := struct {
Ctx context.Context
ReqLog reqlog.RequestLog
}{
Ctx: ctx,
ReqLog: reqLog,
}
mock.lockStoreRequestLog.Lock()
mock.calls.StoreRequestLog = append(mock.calls.StoreRequestLog, callInfo)
mock.lockStoreRequestLog.Unlock()
return mock.StoreRequestLogFunc(ctx, reqLog)
}
// StoreRequestLogCalls gets all the calls that were made to StoreRequestLog.
// Check the length with:
// len(mockedRepository.StoreRequestLogCalls())
func (mock *RepoMock) StoreRequestLogCalls() []struct {
Ctx context.Context
ReqLog reqlog.RequestLog
} {
var calls []struct {
Ctx context.Context
ReqLog reqlog.RequestLog
}
mock.lockStoreRequestLog.RLock()
calls = mock.calls.StoreRequestLog
mock.lockStoreRequestLog.RUnlock()
return calls
}
// StoreResponseLog calls StoreResponseLogFunc.
func (mock *RepoMock) StoreResponseLog(ctx context.Context, reqLogID ulid.ULID, resLog reqlog.ResponseLog) error {
if mock.StoreResponseLogFunc == nil {
panic("RepoMock.StoreResponseLogFunc: method is nil but Repository.StoreResponseLog was just called")
}
callInfo := struct {
Ctx context.Context
ReqLogID ulid.ULID
ResLog reqlog.ResponseLog
}{
Ctx: ctx,
ReqLogID: reqLogID,
ResLog: resLog,
}
mock.lockStoreResponseLog.Lock()
mock.calls.StoreResponseLog = append(mock.calls.StoreResponseLog, callInfo)
mock.lockStoreResponseLog.Unlock()
return mock.StoreResponseLogFunc(ctx, reqLogID, resLog)
}
// StoreResponseLogCalls gets all the calls that were made to StoreResponseLog.
// Check the length with:
// len(mockedRepository.StoreResponseLogCalls())
func (mock *RepoMock) StoreResponseLogCalls() []struct {
Ctx context.Context
ReqLogID ulid.ULID
ResLog reqlog.ResponseLog
} {
var calls []struct {
Ctx context.Context
ReqLogID ulid.ULID
ResLog reqlog.ResponseLog
}
mock.lockStoreResponseLog.RLock()
calls = mock.calls.StoreResponseLog
mock.lockStoreResponseLog.RUnlock()
return calls
}

View File

@ -1,167 +0,0 @@
// Code generated by protoc-gen-connect-go. DO NOT EDIT.
//
// Source: reqlog/reqlog.proto
package reqlog
import (
connect "connectrpc.com/connect"
context "context"
errors "errors"
http "net/http"
strings "strings"
)
// This is a compile-time assertion to ensure that this generated file and the connect package are
// compatible. If you get a compiler error that this constant is not defined, this code was
// generated with a version of connect newer than the one compiled into your binary. You can fix the
// problem by either regenerating this code with an older version of connect or updating the connect
// version compiled into your binary.
const _ = connect.IsAtLeastVersion1_13_0
const (
// HttpRequestLogServiceName is the fully-qualified name of the HttpRequestLogService service.
HttpRequestLogServiceName = "hetty.reqlog.v1.HttpRequestLogService"
)
// These constants are the fully-qualified names of the RPCs defined in this package. They're
// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route.
//
// Note that these are different from the fully-qualified method names used by
// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to
// reflection-formatted method names, remove the leading slash and convert the remaining slash to a
// period.
const (
// HttpRequestLogServiceGetHttpRequestLogProcedure is the fully-qualified name of the
// HttpRequestLogService's GetHttpRequestLog RPC.
HttpRequestLogServiceGetHttpRequestLogProcedure = "/hetty.reqlog.v1.HttpRequestLogService/GetHttpRequestLog"
// HttpRequestLogServiceListHttpRequestLogsProcedure is the fully-qualified name of the
// HttpRequestLogService's ListHttpRequestLogs RPC.
HttpRequestLogServiceListHttpRequestLogsProcedure = "/hetty.reqlog.v1.HttpRequestLogService/ListHttpRequestLogs"
// HttpRequestLogServiceClearHttpRequestLogsProcedure is the fully-qualified name of the
// HttpRequestLogService's ClearHttpRequestLogs RPC.
HttpRequestLogServiceClearHttpRequestLogsProcedure = "/hetty.reqlog.v1.HttpRequestLogService/ClearHttpRequestLogs"
)
// HttpRequestLogServiceClient is a client for the hetty.reqlog.v1.HttpRequestLogService service.
type HttpRequestLogServiceClient interface {
GetHttpRequestLog(context.Context, *connect.Request[GetHttpRequestLogRequest]) (*connect.Response[GetHttpRequestLogResponse], error)
ListHttpRequestLogs(context.Context, *connect.Request[ListHttpRequestLogsRequest]) (*connect.Response[ListHttpRequestLogsResponse], error)
ClearHttpRequestLogs(context.Context, *connect.Request[ClearHttpRequestLogsRequest]) (*connect.Response[ClearHttpRequestLogsResponse], error)
}
// NewHttpRequestLogServiceClient constructs a client for the hetty.reqlog.v1.HttpRequestLogService
// service. By default, it uses the Connect protocol with the binary Protobuf Codec, asks for
// gzipped responses, and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply
// the connect.WithGRPC() or connect.WithGRPCWeb() options.
//
// The URL supplied here should be the base URL for the Connect or gRPC server (for example,
// http://api.acme.com or https://acme.com/grpc).
func NewHttpRequestLogServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) HttpRequestLogServiceClient {
baseURL = strings.TrimRight(baseURL, "/")
httpRequestLogServiceMethods := File_reqlog_reqlog_proto.Services().ByName("HttpRequestLogService").Methods()
return &httpRequestLogServiceClient{
getHttpRequestLog: connect.NewClient[GetHttpRequestLogRequest, GetHttpRequestLogResponse](
httpClient,
baseURL+HttpRequestLogServiceGetHttpRequestLogProcedure,
connect.WithSchema(httpRequestLogServiceMethods.ByName("GetHttpRequestLog")),
connect.WithClientOptions(opts...),
),
listHttpRequestLogs: connect.NewClient[ListHttpRequestLogsRequest, ListHttpRequestLogsResponse](
httpClient,
baseURL+HttpRequestLogServiceListHttpRequestLogsProcedure,
connect.WithSchema(httpRequestLogServiceMethods.ByName("ListHttpRequestLogs")),
connect.WithClientOptions(opts...),
),
clearHttpRequestLogs: connect.NewClient[ClearHttpRequestLogsRequest, ClearHttpRequestLogsResponse](
httpClient,
baseURL+HttpRequestLogServiceClearHttpRequestLogsProcedure,
connect.WithSchema(httpRequestLogServiceMethods.ByName("ClearHttpRequestLogs")),
connect.WithClientOptions(opts...),
),
}
}
// httpRequestLogServiceClient implements HttpRequestLogServiceClient.
type httpRequestLogServiceClient struct {
getHttpRequestLog *connect.Client[GetHttpRequestLogRequest, GetHttpRequestLogResponse]
listHttpRequestLogs *connect.Client[ListHttpRequestLogsRequest, ListHttpRequestLogsResponse]
clearHttpRequestLogs *connect.Client[ClearHttpRequestLogsRequest, ClearHttpRequestLogsResponse]
}
// GetHttpRequestLog calls hetty.reqlog.v1.HttpRequestLogService.GetHttpRequestLog.
func (c *httpRequestLogServiceClient) GetHttpRequestLog(ctx context.Context, req *connect.Request[GetHttpRequestLogRequest]) (*connect.Response[GetHttpRequestLogResponse], error) {
return c.getHttpRequestLog.CallUnary(ctx, req)
}
// ListHttpRequestLogs calls hetty.reqlog.v1.HttpRequestLogService.ListHttpRequestLogs.
func (c *httpRequestLogServiceClient) ListHttpRequestLogs(ctx context.Context, req *connect.Request[ListHttpRequestLogsRequest]) (*connect.Response[ListHttpRequestLogsResponse], error) {
return c.listHttpRequestLogs.CallUnary(ctx, req)
}
// ClearHttpRequestLogs calls hetty.reqlog.v1.HttpRequestLogService.ClearHttpRequestLogs.
func (c *httpRequestLogServiceClient) ClearHttpRequestLogs(ctx context.Context, req *connect.Request[ClearHttpRequestLogsRequest]) (*connect.Response[ClearHttpRequestLogsResponse], error) {
return c.clearHttpRequestLogs.CallUnary(ctx, req)
}
// HttpRequestLogServiceHandler is an implementation of the hetty.reqlog.v1.HttpRequestLogService
// service.
type HttpRequestLogServiceHandler interface {
GetHttpRequestLog(context.Context, *connect.Request[GetHttpRequestLogRequest]) (*connect.Response[GetHttpRequestLogResponse], error)
ListHttpRequestLogs(context.Context, *connect.Request[ListHttpRequestLogsRequest]) (*connect.Response[ListHttpRequestLogsResponse], error)
ClearHttpRequestLogs(context.Context, *connect.Request[ClearHttpRequestLogsRequest]) (*connect.Response[ClearHttpRequestLogsResponse], error)
}
// NewHttpRequestLogServiceHandler builds an HTTP handler from the service implementation. It
// returns the path on which to mount the handler and the handler itself.
//
// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf
// and JSON codecs. They also support gzip compression.
func NewHttpRequestLogServiceHandler(svc HttpRequestLogServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) {
httpRequestLogServiceMethods := File_reqlog_reqlog_proto.Services().ByName("HttpRequestLogService").Methods()
httpRequestLogServiceGetHttpRequestLogHandler := connect.NewUnaryHandler(
HttpRequestLogServiceGetHttpRequestLogProcedure,
svc.GetHttpRequestLog,
connect.WithSchema(httpRequestLogServiceMethods.ByName("GetHttpRequestLog")),
connect.WithHandlerOptions(opts...),
)
httpRequestLogServiceListHttpRequestLogsHandler := connect.NewUnaryHandler(
HttpRequestLogServiceListHttpRequestLogsProcedure,
svc.ListHttpRequestLogs,
connect.WithSchema(httpRequestLogServiceMethods.ByName("ListHttpRequestLogs")),
connect.WithHandlerOptions(opts...),
)
httpRequestLogServiceClearHttpRequestLogsHandler := connect.NewUnaryHandler(
HttpRequestLogServiceClearHttpRequestLogsProcedure,
svc.ClearHttpRequestLogs,
connect.WithSchema(httpRequestLogServiceMethods.ByName("ClearHttpRequestLogs")),
connect.WithHandlerOptions(opts...),
)
return "/hetty.reqlog.v1.HttpRequestLogService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case HttpRequestLogServiceGetHttpRequestLogProcedure:
httpRequestLogServiceGetHttpRequestLogHandler.ServeHTTP(w, r)
case HttpRequestLogServiceListHttpRequestLogsProcedure:
httpRequestLogServiceListHttpRequestLogsHandler.ServeHTTP(w, r)
case HttpRequestLogServiceClearHttpRequestLogsProcedure:
httpRequestLogServiceClearHttpRequestLogsHandler.ServeHTTP(w, r)
default:
http.NotFound(w, r)
}
})
}
// UnimplementedHttpRequestLogServiceHandler returns CodeUnimplemented from all methods.
type UnimplementedHttpRequestLogServiceHandler struct{}
func (UnimplementedHttpRequestLogServiceHandler) GetHttpRequestLog(context.Context, *connect.Request[GetHttpRequestLogRequest]) (*connect.Response[GetHttpRequestLogResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hetty.reqlog.v1.HttpRequestLogService.GetHttpRequestLog is not implemented"))
}
func (UnimplementedHttpRequestLogServiceHandler) ListHttpRequestLogs(context.Context, *connect.Request[ListHttpRequestLogsRequest]) (*connect.Response[ListHttpRequestLogsResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hetty.reqlog.v1.HttpRequestLogService.ListHttpRequestLogs is not implemented"))
}
func (UnimplementedHttpRequestLogServiceHandler) ClearHttpRequestLogs(context.Context, *connect.Request[ClearHttpRequestLogsRequest]) (*connect.Response[ClearHttpRequestLogsResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hetty.reqlog.v1.HttpRequestLogService.ClearHttpRequestLogs is not implemented"))
}

View File

@ -6,13 +6,13 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"connectrpc.com/connect"
"github.com/oklog/ulid/v2"
"github.com/oklog/ulid"
"github.com/dstotijn/hetty/pkg/filter"
httppb "github.com/dstotijn/hetty/pkg/http"
"github.com/dstotijn/hetty/pkg/log"
"github.com/dstotijn/hetty/pkg/proxy"
"github.com/dstotijn/hetty/pkg/scope"
@ -26,32 +26,71 @@ const (
)
var (
ErrRequestLogNotFound = errors.New("reqlog: request not found")
ErrRequestNotFound = errors.New("reqlog: request not found")
ErrProjectIDMustBeSet = errors.New("reqlog: project ID must be set")
)
type Service struct {
type RequestLog struct {
ID ulid.ULID
ProjectID ulid.ULID
URL *url.URL
Method string
Proto string
Header http.Header
Body []byte
Response *ResponseLog
}
type ResponseLog struct {
Proto string
StatusCode int
Status string
Header http.Header
Body []byte
}
type Service interface {
FindRequests(ctx context.Context) ([]RequestLog, error)
FindRequestLogByID(ctx context.Context, id ulid.ULID) (RequestLog, error)
ClearRequests(ctx context.Context, projectID ulid.ULID) error
RequestModifier(next proxy.RequestModifyFunc) proxy.RequestModifyFunc
ResponseModifier(next proxy.ResponseModifyFunc) proxy.ResponseModifyFunc
SetActiveProjectID(id ulid.ULID)
ActiveProjectID() ulid.ULID
SetBypassOutOfScopeRequests(bool)
BypassOutOfScopeRequests() bool
SetFindReqsFilter(filter FindRequestsFilter)
FindReqsFilter() FindRequestsFilter
}
type service struct {
bypassOutOfScopeRequests bool
reqsFilter *RequestLogsFilter
activeProjectID string
findReqsFilter FindRequestsFilter
activeProjectID ulid.ULID
scope *scope.Scope
repo Repository
logger log.Logger
}
type Config struct {
ActiveProjectID string
Scope *scope.Scope
Repository Repository
Logger log.Logger
type FindRequestsFilter struct {
ProjectID ulid.ULID
OnlyInScope bool
SearchExpr filter.Expression
}
func NewService(cfg Config) *Service {
s := &Service{
activeProjectID: cfg.ActiveProjectID,
repo: cfg.Repository,
scope: cfg.Scope,
logger: cfg.Logger,
type Config struct {
Scope *scope.Scope
Repository Repository
Logger log.Logger
}
func NewService(cfg Config) Service {
s := &service{
repo: cfg.Repository,
scope: cfg.Scope,
logger: cfg.Logger,
}
if s.logger == nil {
@ -61,95 +100,28 @@ func NewService(cfg Config) *Service {
return s
}
func (svc *Service) ListHttpRequestLogs(ctx context.Context, req *connect.Request[ListHttpRequestLogsRequest]) (*connect.Response[ListHttpRequestLogsResponse], error) {
projectID := svc.activeProjectID
if projectID == "" {
return nil, connect.NewError(connect.CodeFailedPrecondition, ErrProjectIDMustBeSet)
}
reqLogs, err := svc.repo.FindRequestLogs(ctx, projectID, svc.filterRequestLog)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("reqlog: failed to find request logs: %w", err))
}
return connect.NewResponse(&ListHttpRequestLogsResponse{
HttpRequestLogs: reqLogs,
}), nil
func (svc *service) FindRequests(ctx context.Context) ([]RequestLog, error) {
return svc.repo.FindRequestLogs(ctx, svc.findReqsFilter, svc.scope)
}
func (svc *Service) filterRequestLog(reqLog *HttpRequestLog) (bool, error) {
if svc.reqsFilter.GetOnlyInScope() && svc.scope != nil && !reqLog.MatchScope(svc.scope) {
return false, nil
}
var f filter.Expression
var err error
if expr := svc.reqsFilter.GetSearchExpr(); expr != "" {
f, err = filter.ParseQuery(expr)
if err != nil {
return false, fmt.Errorf("failed to parse search expression: %w", err)
}
}
if f == nil {
return true, nil
}
match, err := reqLog.Matches(f)
if err != nil {
return false, fmt.Errorf("failed to match search expression for request log (id: %v): %w", reqLog.Id, err)
}
return match, nil
func (svc *service) FindRequestLogByID(ctx context.Context, id ulid.ULID) (RequestLog, error) {
return svc.repo.FindRequestLogByID(ctx, id)
}
func (svc *Service) FindRequestLogByID(ctx context.Context, id string) (*HttpRequestLog, error) {
if svc.activeProjectID == "" {
return nil, connect.NewError(connect.CodeFailedPrecondition, ErrProjectIDMustBeSet)
}
return svc.repo.FindRequestLogByID(ctx, svc.activeProjectID, id)
func (svc *service) ClearRequests(ctx context.Context, projectID ulid.ULID) error {
return svc.repo.ClearRequestLogs(ctx, projectID)
}
// GetHttpRequestLog implements HttpRequestLogServiceHandler.
func (svc *Service) GetHttpRequestLog(ctx context.Context, req *connect.Request[GetHttpRequestLogRequest]) (*connect.Response[GetHttpRequestLogResponse], error) {
id, err := ulid.Parse(req.Msg.Id)
if err != nil {
return nil, connect.NewError(connect.CodeInvalidArgument, err)
}
reqLog, err := svc.repo.FindRequestLogByID(ctx, svc.activeProjectID, id.String())
if errors.Is(err, ErrRequestLogNotFound) {
return nil, connect.NewError(connect.CodeNotFound, err)
}
if err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
return connect.NewResponse(&GetHttpRequestLogResponse{
HttpRequestLog: reqLog,
}), nil
}
func (svc *Service) ClearHttpRequestLogs(ctx context.Context, req *connect.Request[ClearHttpRequestLogsRequest]) (*connect.Response[ClearHttpRequestLogsResponse], error) {
err := svc.repo.ClearRequestLogs(ctx, svc.activeProjectID)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("reqlog: failed to clear request logs: %w", err))
}
return connect.NewResponse(&ClearHttpRequestLogsResponse{}), nil
}
func (svc *Service) storeResponse(ctx context.Context, reqLogID string, res *http.Response) error {
respb, err := httppb.ParseHTTPResponse(res)
func (svc *service) storeResponse(ctx context.Context, reqLogID ulid.ULID, res *http.Response) error {
resLog, err := ParseHTTPResponse(res)
if err != nil {
return err
}
return svc.repo.StoreResponseLog(ctx, svc.activeProjectID, reqLogID, respb)
return svc.repo.StoreResponseLog(ctx, reqLogID, resLog)
}
func (svc *Service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestModifyFunc {
func (svc *service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestModifyFunc {
return func(req *http.Request) {
next(req)
@ -161,19 +133,19 @@ func (svc *Service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestM
// TODO: Use io.LimitReader.
var err error
body, err = io.ReadAll(req.Body)
body, err = ioutil.ReadAll(req.Body)
if err != nil {
svc.logger.Errorw("Failed to read request body for logging.",
"error", err)
return
}
req.Body = io.NopCloser(bytes.NewBuffer(body))
clone.Body = io.NopCloser(bytes.NewBuffer(body))
req.Body = ioutil.NopCloser(bytes.NewBuffer(body))
clone.Body = ioutil.NopCloser(bytes.NewBuffer(body))
}
// Bypass logging if no project is active.
if svc.activeProjectID == "" {
if svc.activeProjectID.Compare(ulid.ULID{}) == 0 {
ctx := context.WithValue(req.Context(), LogBypassedKey, true)
*req = *req.WithContext(ctx)
@ -201,37 +173,14 @@ func (svc *Service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestM
return
}
proto, ok := httppb.ProtoMap[clone.Proto]
if !ok {
svc.logger.Errorw("Bypassed logging: request has an invalid protocol.",
"proto", clone.Proto)
return
}
method, ok := httppb.MethodMap[clone.Method]
if !ok {
svc.logger.Errorw("Bypassed logging: request has an invalid method.",
"method", clone.Method)
return
}
headers := []*httppb.Header{}
for k, v := range clone.Header {
for _, vv := range v {
headers = append(headers, &httppb.Header{Key: k, Value: vv})
}
}
reqLog := &HttpRequestLog{
Id: reqID.String(),
ProjectId: svc.activeProjectID,
Request: &httppb.Request{
Url: clone.URL.String(),
Method: method,
Protocol: proto,
Headers: headers,
Body: body,
},
reqLog := RequestLog{
ID: reqID,
ProjectID: svc.activeProjectID,
Method: clone.Method,
URL: clone.URL,
Proto: clone.Proto,
Header: clone.Header,
Body: body,
}
err := svc.repo.StoreRequestLog(req.Context(), reqLog)
@ -242,15 +191,15 @@ func (svc *Service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestM
}
svc.logger.Debugw("Stored request log.",
"reqLogID", reqLog.Id,
"url", reqLog.Request.Url,
)
ctx := context.WithValue(req.Context(), ReqLogIDKey, reqID)
"reqLogID", reqLog.ID.String(),
"url", reqLog.URL.String())
ctx := context.WithValue(req.Context(), ReqLogIDKey, reqLog.ID)
*req = *req.WithContext(ctx)
}
}
func (svc *Service) ResponseModifier(next proxy.ResponseModifyFunc) proxy.ResponseModifyFunc {
func (svc *service) ResponseModifier(next proxy.ResponseModifyFunc) proxy.ResponseModifyFunc {
return func(res *http.Response) error {
if err := next(res); err != nil {
return err
@ -279,7 +228,7 @@ func (svc *Service) ResponseModifier(next proxy.ResponseModifyFunc) proxy.Respon
}
go func() {
if err := svc.storeResponse(context.Background(), reqLogID.String(), &clone); err != nil {
if err := svc.storeResponse(context.Background(), reqLogID, &clone); err != nil {
svc.logger.Errorw("Failed to store response log.",
"error", err)
} else {
@ -292,22 +241,41 @@ func (svc *Service) ResponseModifier(next proxy.ResponseModifyFunc) proxy.Respon
}
}
func (svc *Service) SetActiveProjectID(id string) {
func (svc *service) SetActiveProjectID(id ulid.ULID) {
svc.activeProjectID = id
}
func (svc *Service) ActiveProjectID() string {
func (svc *service) ActiveProjectID() ulid.ULID {
return svc.activeProjectID
}
func (svc *Service) SetRequestLogsFilter(filter *RequestLogsFilter) {
svc.reqsFilter = filter
func (svc *service) SetFindReqsFilter(filter FindRequestsFilter) {
svc.findReqsFilter = filter
}
func (svc *Service) SetBypassOutOfScopeRequests(bypass bool) {
func (svc *service) FindReqsFilter() FindRequestsFilter {
return svc.findReqsFilter
}
func (svc *service) SetBypassOutOfScopeRequests(bypass bool) {
svc.bypassOutOfScopeRequests = bypass
}
func (svc *Service) BypassOutOfScopeRequests() bool {
func (svc *service) BypassOutOfScopeRequests() bool {
return svc.bypassOutOfScopeRequests
}
func ParseHTTPResponse(res *http.Response) (ResponseLog, error) {
body, err := io.ReadAll(res.Body)
if err != nil {
return ResponseLog{}, fmt.Errorf("reqlog: could not read body: %w", err)
}
return ResponseLog{
Proto: res.Proto,
StatusCode: res.StatusCode,
Status: res.Status,
Header: res.Header,
Body: body,
}, nil
}

View File

@ -1,533 +0,0 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.3
// protoc (unknown)
// source: reqlog/reqlog.proto
package reqlog
import (
http "github.com/dstotijn/hetty/pkg/http"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type HttpRequestLog struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
ProjectId string `protobuf:"bytes,2,opt,name=project_id,json=projectId,proto3" json:"project_id,omitempty"`
RemoteIp string `protobuf:"bytes,3,opt,name=remote_ip,json=remoteIp,proto3" json:"remote_ip,omitempty"`
Request *http.Request `protobuf:"bytes,4,opt,name=request,proto3" json:"request,omitempty"`
Response *http.Response `protobuf:"bytes,5,opt,name=response,proto3" json:"response,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *HttpRequestLog) Reset() {
*x = HttpRequestLog{}
mi := &file_reqlog_reqlog_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *HttpRequestLog) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*HttpRequestLog) ProtoMessage() {}
func (x *HttpRequestLog) ProtoReflect() protoreflect.Message {
mi := &file_reqlog_reqlog_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use HttpRequestLog.ProtoReflect.Descriptor instead.
func (*HttpRequestLog) Descriptor() ([]byte, []int) {
return file_reqlog_reqlog_proto_rawDescGZIP(), []int{0}
}
func (x *HttpRequestLog) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *HttpRequestLog) GetProjectId() string {
if x != nil {
return x.ProjectId
}
return ""
}
func (x *HttpRequestLog) GetRemoteIp() string {
if x != nil {
return x.RemoteIp
}
return ""
}
func (x *HttpRequestLog) GetRequest() *http.Request {
if x != nil {
return x.Request
}
return nil
}
func (x *HttpRequestLog) GetResponse() *http.Response {
if x != nil {
return x.Response
}
return nil
}
type GetHttpRequestLogRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetHttpRequestLogRequest) Reset() {
*x = GetHttpRequestLogRequest{}
mi := &file_reqlog_reqlog_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetHttpRequestLogRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetHttpRequestLogRequest) ProtoMessage() {}
func (x *GetHttpRequestLogRequest) ProtoReflect() protoreflect.Message {
mi := &file_reqlog_reqlog_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetHttpRequestLogRequest.ProtoReflect.Descriptor instead.
func (*GetHttpRequestLogRequest) Descriptor() ([]byte, []int) {
return file_reqlog_reqlog_proto_rawDescGZIP(), []int{1}
}
func (x *GetHttpRequestLogRequest) GetId() string {
if x != nil {
return x.Id
}
return ""
}
type GetHttpRequestLogResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
HttpRequestLog *HttpRequestLog `protobuf:"bytes,1,opt,name=http_request_log,json=httpRequestLog,proto3" json:"http_request_log,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetHttpRequestLogResponse) Reset() {
*x = GetHttpRequestLogResponse{}
mi := &file_reqlog_reqlog_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetHttpRequestLogResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetHttpRequestLogResponse) ProtoMessage() {}
func (x *GetHttpRequestLogResponse) ProtoReflect() protoreflect.Message {
mi := &file_reqlog_reqlog_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetHttpRequestLogResponse.ProtoReflect.Descriptor instead.
func (*GetHttpRequestLogResponse) Descriptor() ([]byte, []int) {
return file_reqlog_reqlog_proto_rawDescGZIP(), []int{2}
}
func (x *GetHttpRequestLogResponse) GetHttpRequestLog() *HttpRequestLog {
if x != nil {
return x.HttpRequestLog
}
return nil
}
type ListHttpRequestLogsRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListHttpRequestLogsRequest) Reset() {
*x = ListHttpRequestLogsRequest{}
mi := &file_reqlog_reqlog_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListHttpRequestLogsRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListHttpRequestLogsRequest) ProtoMessage() {}
func (x *ListHttpRequestLogsRequest) ProtoReflect() protoreflect.Message {
mi := &file_reqlog_reqlog_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListHttpRequestLogsRequest.ProtoReflect.Descriptor instead.
func (*ListHttpRequestLogsRequest) Descriptor() ([]byte, []int) {
return file_reqlog_reqlog_proto_rawDescGZIP(), []int{3}
}
type ListHttpRequestLogsResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
HttpRequestLogs []*HttpRequestLog `protobuf:"bytes,1,rep,name=http_request_logs,json=httpRequestLogs,proto3" json:"http_request_logs,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListHttpRequestLogsResponse) Reset() {
*x = ListHttpRequestLogsResponse{}
mi := &file_reqlog_reqlog_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListHttpRequestLogsResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListHttpRequestLogsResponse) ProtoMessage() {}
func (x *ListHttpRequestLogsResponse) ProtoReflect() protoreflect.Message {
mi := &file_reqlog_reqlog_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListHttpRequestLogsResponse.ProtoReflect.Descriptor instead.
func (*ListHttpRequestLogsResponse) Descriptor() ([]byte, []int) {
return file_reqlog_reqlog_proto_rawDescGZIP(), []int{4}
}
func (x *ListHttpRequestLogsResponse) GetHttpRequestLogs() []*HttpRequestLog {
if x != nil {
return x.HttpRequestLogs
}
return nil
}
type RequestLogsFilter struct {
state protoimpl.MessageState `protogen:"open.v1"`
OnlyInScope bool `protobuf:"varint,1,opt,name=only_in_scope,json=onlyInScope,proto3" json:"only_in_scope,omitempty"`
SearchExpr string `protobuf:"bytes,2,opt,name=search_expr,json=searchExpr,proto3" json:"search_expr,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RequestLogsFilter) Reset() {
*x = RequestLogsFilter{}
mi := &file_reqlog_reqlog_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *RequestLogsFilter) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RequestLogsFilter) ProtoMessage() {}
func (x *RequestLogsFilter) ProtoReflect() protoreflect.Message {
mi := &file_reqlog_reqlog_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RequestLogsFilter.ProtoReflect.Descriptor instead.
func (*RequestLogsFilter) Descriptor() ([]byte, []int) {
return file_reqlog_reqlog_proto_rawDescGZIP(), []int{5}
}
func (x *RequestLogsFilter) GetOnlyInScope() bool {
if x != nil {
return x.OnlyInScope
}
return false
}
func (x *RequestLogsFilter) GetSearchExpr() string {
if x != nil {
return x.SearchExpr
}
return ""
}
type ClearHttpRequestLogsRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ClearHttpRequestLogsRequest) Reset() {
*x = ClearHttpRequestLogsRequest{}
mi := &file_reqlog_reqlog_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ClearHttpRequestLogsRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ClearHttpRequestLogsRequest) ProtoMessage() {}
func (x *ClearHttpRequestLogsRequest) ProtoReflect() protoreflect.Message {
mi := &file_reqlog_reqlog_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ClearHttpRequestLogsRequest.ProtoReflect.Descriptor instead.
func (*ClearHttpRequestLogsRequest) Descriptor() ([]byte, []int) {
return file_reqlog_reqlog_proto_rawDescGZIP(), []int{6}
}
type ClearHttpRequestLogsResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ClearHttpRequestLogsResponse) Reset() {
*x = ClearHttpRequestLogsResponse{}
mi := &file_reqlog_reqlog_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ClearHttpRequestLogsResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ClearHttpRequestLogsResponse) ProtoMessage() {}
func (x *ClearHttpRequestLogsResponse) ProtoReflect() protoreflect.Message {
mi := &file_reqlog_reqlog_proto_msgTypes[7]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ClearHttpRequestLogsResponse.ProtoReflect.Descriptor instead.
func (*ClearHttpRequestLogsResponse) Descriptor() ([]byte, []int) {
return file_reqlog_reqlog_proto_rawDescGZIP(), []int{7}
}
var File_reqlog_reqlog_proto protoreflect.FileDescriptor
var file_reqlog_reqlog_proto_rawDesc = []byte{
0x0a, 0x13, 0x72, 0x65, 0x71, 0x6c, 0x6f, 0x67, 0x2f, 0x72, 0x65, 0x71, 0x6c, 0x6f, 0x67, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0f, 0x68, 0x65, 0x74, 0x74, 0x79, 0x2e, 0x72, 0x65, 0x71,
0x6c, 0x6f, 0x67, 0x2e, 0x76, 0x31, 0x1a, 0x0f, 0x68, 0x74, 0x74, 0x70, 0x2f, 0x68, 0x74, 0x74,
0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xc3, 0x01, 0x0a, 0x0e, 0x48, 0x74, 0x74, 0x70,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4c, 0x6f, 0x67, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64,
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72,
0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09,
0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x65, 0x6d,
0x6f, 0x74, 0x65, 0x5f, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65,
0x6d, 0x6f, 0x74, 0x65, 0x49, 0x70, 0x12, 0x30, 0x0a, 0x07, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x68, 0x65, 0x74, 0x74, 0x79, 0x2e,
0x68, 0x74, 0x74, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52,
0x07, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x33, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x68, 0x65, 0x74,
0x74, 0x79, 0x2e, 0x68, 0x74, 0x74, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2a, 0x0a,
0x18, 0x47, 0x65, 0x74, 0x48, 0x74, 0x74, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4c,
0x6f, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18,
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x66, 0x0a, 0x19, 0x47, 0x65, 0x74,
0x48, 0x74, 0x74, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4c, 0x6f, 0x67, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x49, 0x0a, 0x10, 0x68, 0x74, 0x74, 0x70, 0x5f, 0x72,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x6c, 0x6f, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
0x32, 0x1f, 0x2e, 0x68, 0x65, 0x74, 0x74, 0x79, 0x2e, 0x72, 0x65, 0x71, 0x6c, 0x6f, 0x67, 0x2e,
0x76, 0x31, 0x2e, 0x48, 0x74, 0x74, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4c, 0x6f,
0x67, 0x52, 0x0e, 0x68, 0x74, 0x74, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4c, 0x6f,
0x67, 0x22, 0x1c, 0x0a, 0x1a, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x74, 0x74, 0x70, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22,
0x6a, 0x0a, 0x1b, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x74, 0x74, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b,
0x0a, 0x11, 0x68, 0x74, 0x74, 0x70, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x6c,
0x6f, 0x67, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x68, 0x65, 0x74, 0x74,
0x79, 0x2e, 0x72, 0x65, 0x71, 0x6c, 0x6f, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x74, 0x74, 0x70,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4c, 0x6f, 0x67, 0x52, 0x0f, 0x68, 0x74, 0x74, 0x70,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x22, 0x58, 0x0a, 0x11, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72,
0x12, 0x22, 0x0a, 0x0d, 0x6f, 0x6e, 0x6c, 0x79, 0x5f, 0x69, 0x6e, 0x5f, 0x73, 0x63, 0x6f, 0x70,
0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x6f, 0x6e, 0x6c, 0x79, 0x49, 0x6e, 0x53,
0x63, 0x6f, 0x70, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x5f, 0x65,
0x78, 0x70, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x65, 0x61, 0x72, 0x63,
0x68, 0x45, 0x78, 0x70, 0x72, 0x22, 0x1d, 0x0a, 0x1b, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x48, 0x74,
0x74, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x22, 0x1e, 0x0a, 0x1c, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x48, 0x74, 0x74,
0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x32, 0xf0, 0x02, 0x0a, 0x15, 0x48, 0x74, 0x74, 0x70, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x4c, 0x6f, 0x67, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x6c,
0x0a, 0x11, 0x47, 0x65, 0x74, 0x48, 0x74, 0x74, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x4c, 0x6f, 0x67, 0x12, 0x29, 0x2e, 0x68, 0x65, 0x74, 0x74, 0x79, 0x2e, 0x72, 0x65, 0x71, 0x6c,
0x6f, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x48, 0x74, 0x74, 0x70, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x4c, 0x6f, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a,
0x2e, 0x68, 0x65, 0x74, 0x74, 0x79, 0x2e, 0x72, 0x65, 0x71, 0x6c, 0x6f, 0x67, 0x2e, 0x76, 0x31,
0x2e, 0x47, 0x65, 0x74, 0x48, 0x74, 0x74, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4c,
0x6f, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x72, 0x0a, 0x13,
0x4c, 0x69, 0x73, 0x74, 0x48, 0x74, 0x74, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4c,
0x6f, 0x67, 0x73, 0x12, 0x2b, 0x2e, 0x68, 0x65, 0x74, 0x74, 0x79, 0x2e, 0x72, 0x65, 0x71, 0x6c,
0x6f, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x74, 0x74, 0x70, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x1a, 0x2c, 0x2e, 0x68, 0x65, 0x74, 0x74, 0x79, 0x2e, 0x72, 0x65, 0x71, 0x6c, 0x6f, 0x67, 0x2e,
0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x74, 0x74, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00,
0x12, 0x75, 0x0a, 0x14, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x48, 0x74, 0x74, 0x70, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x12, 0x2c, 0x2e, 0x68, 0x65, 0x74, 0x74, 0x79,
0x2e, 0x72, 0x65, 0x71, 0x6c, 0x6f, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x72,
0x48, 0x74, 0x74, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2d, 0x2e, 0x68, 0x65, 0x74, 0x74, 0x79, 0x2e, 0x72,
0x65, 0x71, 0x6c, 0x6f, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x48, 0x74,
0x74, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x26, 0x5a, 0x24, 0x67, 0x69, 0x74, 0x68, 0x75,
0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x64, 0x73, 0x74, 0x6f, 0x74, 0x69, 0x6a, 0x6e, 0x2f, 0x68,
0x65, 0x74, 0x74, 0x79, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x72, 0x65, 0x71, 0x6c, 0x6f, 0x67, 0x62,
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_reqlog_reqlog_proto_rawDescOnce sync.Once
file_reqlog_reqlog_proto_rawDescData = file_reqlog_reqlog_proto_rawDesc
)
func file_reqlog_reqlog_proto_rawDescGZIP() []byte {
file_reqlog_reqlog_proto_rawDescOnce.Do(func() {
file_reqlog_reqlog_proto_rawDescData = protoimpl.X.CompressGZIP(file_reqlog_reqlog_proto_rawDescData)
})
return file_reqlog_reqlog_proto_rawDescData
}
var file_reqlog_reqlog_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
var file_reqlog_reqlog_proto_goTypes = []any{
(*HttpRequestLog)(nil), // 0: hetty.reqlog.v1.HttpRequestLog
(*GetHttpRequestLogRequest)(nil), // 1: hetty.reqlog.v1.GetHttpRequestLogRequest
(*GetHttpRequestLogResponse)(nil), // 2: hetty.reqlog.v1.GetHttpRequestLogResponse
(*ListHttpRequestLogsRequest)(nil), // 3: hetty.reqlog.v1.ListHttpRequestLogsRequest
(*ListHttpRequestLogsResponse)(nil), // 4: hetty.reqlog.v1.ListHttpRequestLogsResponse
(*RequestLogsFilter)(nil), // 5: hetty.reqlog.v1.RequestLogsFilter
(*ClearHttpRequestLogsRequest)(nil), // 6: hetty.reqlog.v1.ClearHttpRequestLogsRequest
(*ClearHttpRequestLogsResponse)(nil), // 7: hetty.reqlog.v1.ClearHttpRequestLogsResponse
(*http.Request)(nil), // 8: hetty.http.v1.Request
(*http.Response)(nil), // 9: hetty.http.v1.Response
}
var file_reqlog_reqlog_proto_depIdxs = []int32{
8, // 0: hetty.reqlog.v1.HttpRequestLog.request:type_name -> hetty.http.v1.Request
9, // 1: hetty.reqlog.v1.HttpRequestLog.response:type_name -> hetty.http.v1.Response
0, // 2: hetty.reqlog.v1.GetHttpRequestLogResponse.http_request_log:type_name -> hetty.reqlog.v1.HttpRequestLog
0, // 3: hetty.reqlog.v1.ListHttpRequestLogsResponse.http_request_logs:type_name -> hetty.reqlog.v1.HttpRequestLog
1, // 4: hetty.reqlog.v1.HttpRequestLogService.GetHttpRequestLog:input_type -> hetty.reqlog.v1.GetHttpRequestLogRequest
3, // 5: hetty.reqlog.v1.HttpRequestLogService.ListHttpRequestLogs:input_type -> hetty.reqlog.v1.ListHttpRequestLogsRequest
6, // 6: hetty.reqlog.v1.HttpRequestLogService.ClearHttpRequestLogs:input_type -> hetty.reqlog.v1.ClearHttpRequestLogsRequest
2, // 7: hetty.reqlog.v1.HttpRequestLogService.GetHttpRequestLog:output_type -> hetty.reqlog.v1.GetHttpRequestLogResponse
4, // 8: hetty.reqlog.v1.HttpRequestLogService.ListHttpRequestLogs:output_type -> hetty.reqlog.v1.ListHttpRequestLogsResponse
7, // 9: hetty.reqlog.v1.HttpRequestLogService.ClearHttpRequestLogs:output_type -> hetty.reqlog.v1.ClearHttpRequestLogsResponse
7, // [7:10] is the sub-list for method output_type
4, // [4:7] is the sub-list for method input_type
4, // [4:4] is the sub-list for extension type_name
4, // [4:4] is the sub-list for extension extendee
0, // [0:4] is the sub-list for field type_name
}
func init() { file_reqlog_reqlog_proto_init() }
func file_reqlog_reqlog_proto_init() {
if File_reqlog_reqlog_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_reqlog_reqlog_proto_rawDesc,
NumEnums: 0,
NumMessages: 8,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_reqlog_reqlog_proto_goTypes,
DependencyIndexes: file_reqlog_reqlog_proto_depIdxs,
MessageInfos: file_reqlog_reqlog_proto_msgTypes,
}.Build()
File_reqlog_reqlog_proto = out.File
file_reqlog_reqlog_proto_rawDesc = nil
file_reqlog_reqlog_proto_goTypes = nil
file_reqlog_reqlog_proto_depIdxs = nil
}

View File

@ -1,119 +1,86 @@
package reqlog_test
//go:generate go run github.com/matryer/moq -out repo_mock_test.go -pkg reqlog_test . Repository:RepoMock
import (
"context"
"io"
"math/rand"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/oklog/ulid/v2"
"go.etcd.io/bbolt"
"github.com/google/go-cmp/cmp"
"github.com/oklog/ulid"
"github.com/dstotijn/hetty/pkg/db/bolt"
httppb "github.com/dstotijn/hetty/pkg/http"
"github.com/dstotijn/hetty/pkg/proj"
"github.com/dstotijn/hetty/pkg/proxy"
"github.com/dstotijn/hetty/pkg/reqlog"
"github.com/dstotijn/hetty/pkg/scope"
"github.com/dstotijn/hetty/pkg/testutil"
)
//nolint:gosec
var ulidEntropy = rand.New(rand.NewSource(time.Now().UnixNano()))
//nolint:paralleltest
func TestRequestModifier(t *testing.T) {
path := t.TempDir() + "bolt.db"
boltDB, err := bbolt.Open(path, 0o600, nil)
if err != nil {
t.Fatalf("failed to open bolt database: %v", err)
repoMock := &RepoMock{
StoreRequestLogFunc: func(_ context.Context, _ reqlog.RequestLog) error {
return nil
},
}
defer boltDB.Close()
db, err := bolt.DatabaseFromBoltDB(boltDB)
if err != nil {
t.Fatalf("failed to create database: %v", err)
}
defer db.Close()
projectID := ulid.Make().String()
err = db.UpsertProject(context.Background(), &proj.Project{
Id: projectID,
})
if err != nil {
t.Fatalf("unexpected error upserting project: %v", err)
}
svc := reqlog.NewService(reqlog.Config{
Repository: db,
Repository: repoMock,
Scope: &scope.Scope{},
})
svc.SetActiveProjectID(projectID)
svc.SetActiveProjectID(ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy))
next := func(req *http.Request) {
req.Body = io.NopCloser(strings.NewReader("modified body"))
}
reqModFn := svc.RequestModifier(next)
req := httptest.NewRequest("GET", "https://example.com/", strings.NewReader("bar"))
req.Header.Add("X-Yolo", "swag")
reqID := ulid.Make()
reqID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
req = req.WithContext(proxy.WithRequestID(req.Context(), reqID))
reqModFn(req)
t.Run("request log was stored in repository", func(t *testing.T) {
exp := &reqlog.HttpRequestLog{
Id: reqID.String(),
ProjectId: svc.ActiveProjectID(),
Request: &httppb.Request{
Url: "https://example.com/",
Method: httppb.Method_METHOD_GET,
Protocol: httppb.Protocol_PROTOCOL_HTTP11,
Headers: []*httppb.Header{
{
Key: "X-Yolo",
Value: "swag",
},
},
Body: []byte("modified body"),
},
gotCount := len(repoMock.StoreRequestLogCalls())
if expCount := 1; expCount != gotCount {
t.Fatalf("incorrect `proj.Service.AddRequestLog` calls (expected: %v, got: %v)", expCount, gotCount)
}
got, err := db.FindRequestLogByID(context.Background(), svc.ActiveProjectID(), reqID.String())
if err != nil {
t.Fatalf("failed to find request by id: %v", err)
exp := reqlog.RequestLog{
ID: ulid.ULID{}, // Empty value
ProjectID: svc.ActiveProjectID(),
Method: req.Method,
URL: req.URL,
Proto: req.Proto,
Header: req.Header,
Body: []byte("modified body"),
}
got := repoMock.StoreRequestLogCalls()[0].ReqLog
got.ID = ulid.ULID{} // Override to empty value so we can compare against expected value.
testutil.ProtoDiff(t, "request log not equal", exp, got)
if diff := cmp.Diff(exp, got); diff != "" {
t.Fatalf("request log not equal (-exp, +got):\n%v", diff)
}
})
}
//nolint:paralleltest
func TestResponseModifier(t *testing.T) {
path := t.TempDir() + "bolt.db"
boltDB, err := bbolt.Open(path, 0o600, nil)
if err != nil {
t.Fatalf("failed to open bolt database: %v", err)
repoMock := &RepoMock{
StoreResponseLogFunc: func(_ context.Context, _ ulid.ULID, _ reqlog.ResponseLog) error {
return nil
},
}
defer boltDB.Close()
db, err := bolt.DatabaseFromBoltDB(boltDB)
if err != nil {
t.Fatalf("failed to create database: %v", err)
}
defer db.Close()
projectID := "foobar-project-id"
err = db.UpsertProject(context.Background(), &proj.Project{
Id: projectID,
})
if err != nil {
t.Fatalf("unexpected error upserting project: %v", err)
}
svc := reqlog.NewService(reqlog.Config{
Repository: db,
Repository: repoMock,
})
svc.SetActiveProjectID(projectID)
svc.SetActiveProjectID(ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy))
next := func(res *http.Response) error {
res.Body = io.NopCloser(strings.NewReader("modified body"))
@ -122,44 +89,39 @@ func TestResponseModifier(t *testing.T) {
resModFn := svc.ResponseModifier(next)
req := httptest.NewRequest("GET", "https://example.com/", strings.NewReader("bar"))
reqLogID := ulid.Make()
reqLogID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
req = req.WithContext(context.WithValue(req.Context(), reqlog.ReqLogIDKey, reqLogID))
err = db.StoreRequestLog(context.Background(), &reqlog.HttpRequestLog{
Id: reqLogID.String(),
ProjectId: projectID,
})
if err != nil {
t.Fatalf("failed to store request log: %v", err)
}
res := &http.Response{
Request: req,
Proto: "HTTP/1.1",
Status: "200 OK",
StatusCode: 200,
Body: io.NopCloser(strings.NewReader("bar")),
Request: req,
Body: io.NopCloser(strings.NewReader("bar")),
}
if err := resModFn(res); err != nil {
t.Fatalf("unexpected error (expected: nil, got: %v)", err)
}
// Dirty (but simple) wait for other goroutine to finish calling repository.
time.Sleep(10 * time.Millisecond)
t.Run("request log was stored in repository", func(t *testing.T) {
// Dirty (but simple) wait for other goroutine to finish calling repository.
time.Sleep(10 * time.Millisecond)
got := len(repoMock.StoreResponseLogCalls())
if exp := 1; exp != got {
t.Fatalf("incorrect `proj.Service.AddResponseLog` calls (expected: %v, got: %v)", exp, got)
}
got, err := db.FindRequestLogByID(context.Background(), svc.ActiveProjectID(), reqLogID.String())
if err != nil {
t.Fatalf("failed to find request by id: %v", err)
}
t.Run("ran next modifier first, before calling repository", func(t *testing.T) {
got := repoMock.StoreResponseLogCalls()[0].ResLog.Body
if exp := "modified body"; exp != string(got) {
t.Fatalf("incorrect `ResponseLog.Body` value (expected: %v, got: %v)", exp, string(got))
}
})
exp := &httppb.Response{
Protocol: httppb.Protocol_PROTOCOL_HTTP11,
Status: "200 OK",
StatusCode: 200,
Headers: []*httppb.Header{},
Body: []byte("modified body"),
}
testutil.ProtoDiff(t, "response not equal", exp, got.GetResponse())
t.Run("called repository with request log id", func(t *testing.T) {
got := repoMock.StoreResponseLogCalls()[0].ReqLogID
if exp := reqLogID; exp.Compare(got) != 0 {
t.Fatalf("incorrect `reqLogID` argument for `Repository.AddResponseLogCalls` (expected: %v, got: %v)",
exp.String(), got.String())
}
})
})
}

View File

@ -3,34 +3,40 @@ package reqlog
import (
"errors"
"fmt"
"strconv"
"strings"
"github.com/oklog/ulid/v2"
"github.com/oklog/ulid"
"github.com/dstotijn/hetty/pkg/filter"
"github.com/dstotijn/hetty/pkg/http"
"github.com/dstotijn/hetty/pkg/scope"
)
var reqLogSearchKeyFns = map[string]func(rl *HttpRequestLog) string{
"req.id": func(rl *HttpRequestLog) string { return rl.GetId() },
"req.proto": func(rl *HttpRequestLog) string { return rl.GetRequest().GetProtocol().String() },
"req.url": func(rl *HttpRequestLog) string { return rl.GetRequest().GetUrl() },
"req.method": func(rl *HttpRequestLog) string { return rl.GetRequest().GetMethod().String() },
"req.body": func(rl *HttpRequestLog) string { return string(rl.GetRequest().GetBody()) },
"req.timestamp": func(rl *HttpRequestLog) string {
id, err := ulid.Parse(rl.GetId())
if err != nil {
var reqLogSearchKeyFns = map[string]func(rl RequestLog) string{
"req.id": func(rl RequestLog) string { return rl.ID.String() },
"req.proto": func(rl RequestLog) string { return rl.Proto },
"req.url": func(rl RequestLog) string {
if rl.URL == nil {
return ""
}
return ulid.Time(id.Time()).String()
return rl.URL.String()
},
"req.method": func(rl RequestLog) string { return rl.Method },
"req.body": func(rl RequestLog) string { return string(rl.Body) },
"req.timestamp": func(rl RequestLog) string { return ulid.Time(rl.ID.Time()).String() },
}
var ResLogSearchKeyFns = map[string]func(rl ResponseLog) string{
"res.proto": func(rl ResponseLog) string { return rl.Proto },
"res.statusCode": func(rl ResponseLog) string { return strconv.Itoa(rl.StatusCode) },
"res.statusReason": func(rl ResponseLog) string { return rl.Status },
"res.body": func(rl ResponseLog) string { return string(rl.Body) },
}
// TODO: Request and response headers search key functions.
// Matches returns true if the supplied search expression evaluates to true.
func (reqLog *HttpRequestLog) Matches(expr filter.Expression) (bool, error) {
func (reqLog RequestLog) Matches(expr filter.Expression) (bool, error) {
switch e := expr.(type) {
case filter.PrefixExpression:
return reqLog.matchPrefixExpr(e)
@ -43,7 +49,7 @@ func (reqLog *HttpRequestLog) Matches(expr filter.Expression) (bool, error) {
}
}
func (reqLog *HttpRequestLog) matchPrefixExpr(expr filter.PrefixExpression) (bool, error) {
func (reqLog RequestLog) matchPrefixExpr(expr filter.PrefixExpression) (bool, error) {
switch expr.Operator {
case filter.TokOpNot:
match, err := reqLog.Matches(expr.Right)
@ -57,7 +63,7 @@ func (reqLog *HttpRequestLog) matchPrefixExpr(expr filter.PrefixExpression) (boo
}
}
func (reqLog *HttpRequestLog) matchInfixExpr(expr filter.InfixExpression) (bool, error) {
func (reqLog RequestLog) matchInfixExpr(expr filter.InfixExpression) (bool, error) {
switch expr.Operator {
case filter.TokOpAnd:
left, err := reqLog.Matches(expr.Left)
@ -93,7 +99,7 @@ func (reqLog *HttpRequestLog) matchInfixExpr(expr filter.InfixExpression) (bool,
leftVal := reqLog.getMappedStringLiteral(left.Value)
if leftVal == "req.headers" {
match, err := filter.MatchHTTPHeaders(expr.Operator, expr.Right, reqLog.Request.Headers)
match, err := filter.MatchHTTPHeaders(expr.Operator, expr.Right, reqLog.Header)
if err != nil {
return false, fmt.Errorf("failed to match request HTTP headers: %w", err)
}
@ -102,7 +108,7 @@ func (reqLog *HttpRequestLog) matchInfixExpr(expr filter.InfixExpression) (bool,
}
if leftVal == "res.headers" && reqLog.Response != nil {
match, err := filter.MatchHTTPHeaders(expr.Operator, expr.Right, reqLog.Response.Headers)
match, err := filter.MatchHTTPHeaders(expr.Operator, expr.Right, reqLog.Response.Header)
if err != nil {
return false, fmt.Errorf("failed to match response HTTP headers: %w", err)
}
@ -153,7 +159,7 @@ func (reqLog *HttpRequestLog) matchInfixExpr(expr filter.InfixExpression) (bool,
}
}
func (reqLog *HttpRequestLog) getMappedStringLiteral(s string) string {
func (reqLog RequestLog) getMappedStringLiteral(s string) string {
switch {
case strings.HasPrefix(s, "req."):
fn, ok := reqLogSearchKeyFns[s]
@ -161,22 +167,28 @@ func (reqLog *HttpRequestLog) getMappedStringLiteral(s string) string {
return fn(reqLog)
}
case strings.HasPrefix(s, "res."):
fn, ok := http.ResponseSearchKeyFns[s]
if reqLog.Response == nil {
return ""
}
fn, ok := ResLogSearchKeyFns[s]
if ok {
return fn(reqLog.GetResponse())
return fn(*reqLog.Response)
}
}
return s
}
func (reqLog *HttpRequestLog) matchStringLiteral(strLiteral filter.StringLiteral) (bool, error) {
for _, header := range reqLog.GetRequest().GetHeaders() {
if strings.Contains(
strings.ToLower(fmt.Sprintf("%v: %v", header.Key, header.Value)),
strings.ToLower(strLiteral.Value),
) {
return true, nil
func (reqLog RequestLog) matchStringLiteral(strLiteral filter.StringLiteral) (bool, error) {
for key, values := range reqLog.Header {
for _, value := range values {
if strings.Contains(
strings.ToLower(fmt.Sprintf("%v: %v", key, value)),
strings.ToLower(strLiteral.Value),
) {
return true, nil
}
}
}
@ -189,19 +201,21 @@ func (reqLog *HttpRequestLog) matchStringLiteral(strLiteral filter.StringLiteral
}
}
if res := reqLog.GetResponse(); res != nil {
for _, header := range res.Headers {
if strings.Contains(
strings.ToLower(fmt.Sprintf("%v: %v", header.Key, header.Value)),
strings.ToLower(strLiteral.Value),
) {
return true, nil
if reqLog.Response != nil {
for key, values := range reqLog.Response.Header {
for _, value := range values {
if strings.Contains(
strings.ToLower(fmt.Sprintf("%v: %v", key, value)),
strings.ToLower(strLiteral.Value),
) {
return true, nil
}
}
}
for _, fn := range http.ResponseSearchKeyFns {
for _, fn := range ResLogSearchKeyFns {
if strings.Contains(
strings.ToLower(fn(reqLog.GetResponse())),
strings.ToLower(fn(*reqLog.Response)),
strings.ToLower(strLiteral.Value),
) {
return true, nil
@ -212,25 +226,29 @@ func (reqLog *HttpRequestLog) matchStringLiteral(strLiteral filter.StringLiteral
return false, nil
}
func (reqLog *HttpRequestLog) MatchScope(s *scope.Scope) bool {
func (reqLog RequestLog) MatchScope(s *scope.Scope) bool {
for _, rule := range s.Rules() {
if rule.URL != nil {
if matches := rule.URL.MatchString(reqLog.GetRequest().GetUrl()); matches {
if rule.URL != nil && reqLog.URL != nil {
if matches := rule.URL.MatchString(reqLog.URL.String()); matches {
return true
}
}
for _, header := range reqLog.GetRequest().GetHeaders() {
for key, values := range reqLog.Header {
var keyMatches, valueMatches bool
if matches := rule.Header.Key.MatchString(header.Key); matches {
keyMatches = true
if rule.Header.Key != nil {
if matches := rule.Header.Key.MatchString(key); matches {
keyMatches = true
}
}
if rule.Header.Value != nil {
if matches := rule.Header.Value.MatchString(header.Value); matches {
valueMatches = true
break
for _, value := range values {
if matches := rule.Header.Value.MatchString(value); matches {
valueMatches = true
break
}
}
}
// When only key or value is set, match on whatever is set.
@ -246,7 +264,7 @@ func (reqLog *HttpRequestLog) MatchScope(s *scope.Scope) bool {
}
if rule.Body != nil {
if matches := rule.Body.Match(reqLog.GetRequest().GetBody()); matches {
if matches := rule.Body.Match(reqLog.Body); matches {
return true
}
}

View File

@ -4,7 +4,6 @@ import (
"testing"
"github.com/dstotijn/hetty/pkg/filter"
"github.com/dstotijn/hetty/pkg/http"
"github.com/dstotijn/hetty/pkg/reqlog"
)
@ -14,17 +13,15 @@ func TestRequestLogMatch(t *testing.T) {
tests := []struct {
name string
query string
requestLog *reqlog.HttpRequestLog
requestLog reqlog.RequestLog
expectedMatch bool
expectedError error
}{
{
name: "infix expression, equal operator, match",
query: "req.body = foo",
requestLog: &reqlog.HttpRequestLog{
Request: &http.Request{
Body: []byte("foo"),
},
requestLog: reqlog.RequestLog{
Body: []byte("foo"),
},
expectedMatch: true,
expectedError: nil,
@ -32,10 +29,8 @@ func TestRequestLogMatch(t *testing.T) {
{
name: "infix expression, not equal operator, match",
query: "req.body != bar",
requestLog: &reqlog.HttpRequestLog{
Request: &http.Request{
Body: []byte("foo"),
},
requestLog: reqlog.RequestLog{
Body: []byte("foo"),
},
expectedMatch: true,
expectedError: nil,
@ -43,10 +38,8 @@ func TestRequestLogMatch(t *testing.T) {
{
name: "infix expression, greater than operator, match",
query: "req.body > a",
requestLog: &reqlog.HttpRequestLog{
Request: &http.Request{
Body: []byte("b"),
},
requestLog: reqlog.RequestLog{
Body: []byte("b"),
},
expectedMatch: true,
expectedError: nil,
@ -54,10 +47,8 @@ func TestRequestLogMatch(t *testing.T) {
{
name: "infix expression, less than operator, match",
query: "req.body < b",
requestLog: &reqlog.HttpRequestLog{
Request: &http.Request{
Body: []byte("a"),
},
requestLog: reqlog.RequestLog{
Body: []byte("a"),
},
expectedMatch: true,
expectedError: nil,
@ -65,10 +56,8 @@ func TestRequestLogMatch(t *testing.T) {
{
name: "infix expression, greater than or equal operator, match greater than",
query: "req.body >= a",
requestLog: &reqlog.HttpRequestLog{
Request: &http.Request{
Body: []byte("b"),
},
requestLog: reqlog.RequestLog{
Body: []byte("b"),
},
expectedMatch: true,
expectedError: nil,
@ -76,10 +65,8 @@ func TestRequestLogMatch(t *testing.T) {
{
name: "infix expression, greater than or equal operator, match equal",
query: "req.body >= a",
requestLog: &reqlog.HttpRequestLog{
Request: &http.Request{
Body: []byte("a"),
},
requestLog: reqlog.RequestLog{
Body: []byte("a"),
},
expectedMatch: true,
expectedError: nil,
@ -87,10 +74,8 @@ func TestRequestLogMatch(t *testing.T) {
{
name: "infix expression, less than or equal operator, match less than",
query: "req.body <= b",
requestLog: &reqlog.HttpRequestLog{
Request: &http.Request{
Body: []byte("a"),
},
requestLog: reqlog.RequestLog{
Body: []byte("a"),
},
expectedMatch: true,
expectedError: nil,
@ -98,10 +83,8 @@ func TestRequestLogMatch(t *testing.T) {
{
name: "infix expression, less than or equal operator, match equal",
query: "req.body <= b",
requestLog: &reqlog.HttpRequestLog{
Request: &http.Request{
Body: []byte("b"),
},
requestLog: reqlog.RequestLog{
Body: []byte("b"),
},
expectedMatch: true,
expectedError: nil,
@ -109,10 +92,8 @@ func TestRequestLogMatch(t *testing.T) {
{
name: "infix expression, regular expression operator, match",
query: `req.body =~ "^foo(.*)$"`,
requestLog: &reqlog.HttpRequestLog{
Request: &http.Request{
Body: []byte("foobar"),
},
requestLog: reqlog.RequestLog{
Body: []byte("foobar"),
},
expectedMatch: true,
expectedError: nil,
@ -120,10 +101,8 @@ func TestRequestLogMatch(t *testing.T) {
{
name: "infix expression, negate regular expression operator, match",
query: `req.body !~ "^foo(.*)$"`,
requestLog: &reqlog.HttpRequestLog{
Request: &http.Request{
Body: []byte("xoobar"),
},
requestLog: reqlog.RequestLog{
Body: []byte("xoobar"),
},
expectedMatch: true,
expectedError: nil,
@ -131,11 +110,9 @@ func TestRequestLogMatch(t *testing.T) {
{
name: "infix expression, and operator, match",
query: "req.body = bar AND res.body = yolo",
requestLog: &reqlog.HttpRequestLog{
Request: &http.Request{
Body: []byte("bar"),
},
Response: &http.Response{
requestLog: reqlog.RequestLog{
Body: []byte("bar"),
Response: &reqlog.ResponseLog{
Body: []byte("yolo"),
},
},
@ -145,11 +122,9 @@ func TestRequestLogMatch(t *testing.T) {
{
name: "infix expression, or operator, match",
query: "req.body = bar OR res.body = yolo",
requestLog: &reqlog.HttpRequestLog{
Request: &http.Request{
Body: []byte("foo"),
},
Response: &http.Response{
requestLog: reqlog.RequestLog{
Body: []byte("foo"),
Response: &reqlog.ResponseLog{
Body: []byte("yolo"),
},
},
@ -159,10 +134,8 @@ func TestRequestLogMatch(t *testing.T) {
{
name: "prefix expression, not operator, match",
query: "NOT (req.body = bar)",
requestLog: &reqlog.HttpRequestLog{
Request: &http.Request{
Body: []byte("foo"),
},
requestLog: reqlog.RequestLog{
Body: []byte("foo"),
},
expectedMatch: true,
expectedError: nil,
@ -170,10 +143,8 @@ func TestRequestLogMatch(t *testing.T) {
{
name: "string literal expression, match in request log",
query: "foo",
requestLog: &reqlog.HttpRequestLog{
Request: &http.Request{
Body: []byte("foo"),
},
requestLog: reqlog.RequestLog{
Body: []byte("foo"),
},
expectedMatch: true,
expectedError: nil,
@ -181,10 +152,8 @@ func TestRequestLogMatch(t *testing.T) {
{
name: "string literal expression, no match",
query: "foo",
requestLog: &reqlog.HttpRequestLog{
Request: &http.Request{
Body: []byte("bar"),
},
requestLog: reqlog.RequestLog{
Body: []byte("bar"),
},
expectedMatch: false,
expectedError: nil,
@ -192,8 +161,8 @@ func TestRequestLogMatch(t *testing.T) {
{
name: "string literal expression, match in response log",
query: "foo",
requestLog: &reqlog.HttpRequestLog{
Response: &http.Response{
requestLog: reqlog.RequestLog{
Response: &reqlog.ResponseLog{
Body: []byte("foo"),
},
},

View File

@ -1,159 +0,0 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.3
// protoc (unknown)
// source: scope/scope.proto
package scope
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type ScopeRule struct {
state protoimpl.MessageState `protogen:"open.v1"`
UrlRegexp string `protobuf:"bytes,1,opt,name=url_regexp,json=urlRegexp,proto3" json:"url_regexp,omitempty"`
HeaderKeyRegexp string `protobuf:"bytes,2,opt,name=header_key_regexp,json=headerKeyRegexp,proto3" json:"header_key_regexp,omitempty"`
HeaderValueRegexp string `protobuf:"bytes,3,opt,name=header_value_regexp,json=headerValueRegexp,proto3" json:"header_value_regexp,omitempty"`
BodyRegexp string `protobuf:"bytes,4,opt,name=body_regexp,json=bodyRegexp,proto3" json:"body_regexp,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ScopeRule) Reset() {
*x = ScopeRule{}
mi := &file_scope_scope_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ScopeRule) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ScopeRule) ProtoMessage() {}
func (x *ScopeRule) ProtoReflect() protoreflect.Message {
mi := &file_scope_scope_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ScopeRule.ProtoReflect.Descriptor instead.
func (*ScopeRule) Descriptor() ([]byte, []int) {
return file_scope_scope_proto_rawDescGZIP(), []int{0}
}
func (x *ScopeRule) GetUrlRegexp() string {
if x != nil {
return x.UrlRegexp
}
return ""
}
func (x *ScopeRule) GetHeaderKeyRegexp() string {
if x != nil {
return x.HeaderKeyRegexp
}
return ""
}
func (x *ScopeRule) GetHeaderValueRegexp() string {
if x != nil {
return x.HeaderValueRegexp
}
return ""
}
func (x *ScopeRule) GetBodyRegexp() string {
if x != nil {
return x.BodyRegexp
}
return ""
}
var File_scope_scope_proto protoreflect.FileDescriptor
var file_scope_scope_proto_rawDesc = []byte{
0x0a, 0x11, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x2f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x12, 0x0e, 0x68, 0x65, 0x74, 0x74, 0x79, 0x2e, 0x73, 0x63, 0x6f, 0x70, 0x65,
0x2e, 0x76, 0x31, 0x22, 0xa7, 0x01, 0x0a, 0x09, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x52, 0x75, 0x6c,
0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x75, 0x72, 0x6c, 0x5f, 0x72, 0x65, 0x67, 0x65, 0x78, 0x70, 0x18,
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x75, 0x72, 0x6c, 0x52, 0x65, 0x67, 0x65, 0x78, 0x70,
0x12, 0x2a, 0x0a, 0x11, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x72,
0x65, 0x67, 0x65, 0x78, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x68, 0x65, 0x61,
0x64, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x67, 0x65, 0x78, 0x70, 0x12, 0x2e, 0x0a, 0x13,
0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x72, 0x65, 0x67,
0x65, 0x78, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x68, 0x65, 0x61, 0x64, 0x65,
0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x67, 0x65, 0x78, 0x70, 0x12, 0x1f, 0x0a, 0x0b,
0x62, 0x6f, 0x64, 0x79, 0x5f, 0x72, 0x65, 0x67, 0x65, 0x78, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28,
0x09, 0x52, 0x0a, 0x62, 0x6f, 0x64, 0x79, 0x52, 0x65, 0x67, 0x65, 0x78, 0x70, 0x42, 0x25, 0x5a,
0x23, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x64, 0x73, 0x74, 0x6f,
0x74, 0x69, 0x6a, 0x6e, 0x2f, 0x68, 0x65, 0x74, 0x74, 0x79, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73,
0x63, 0x6f, 0x70, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_scope_scope_proto_rawDescOnce sync.Once
file_scope_scope_proto_rawDescData = file_scope_scope_proto_rawDesc
)
func file_scope_scope_proto_rawDescGZIP() []byte {
file_scope_scope_proto_rawDescOnce.Do(func() {
file_scope_scope_proto_rawDescData = protoimpl.X.CompressGZIP(file_scope_scope_proto_rawDescData)
})
return file_scope_scope_proto_rawDescData
}
var file_scope_scope_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_scope_scope_proto_goTypes = []any{
(*ScopeRule)(nil), // 0: hetty.scope.v1.ScopeRule
}
var file_scope_scope_proto_depIdxs = []int32{
0, // [0:0] is the sub-list for method output_type
0, // [0:0] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_scope_scope_proto_init() }
func file_scope_scope_proto_init() {
if File_scope_scope_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_scope_scope_proto_rawDesc,
NumEnums: 0,
NumMessages: 1,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_scope_scope_proto_goTypes,
DependencyIndexes: file_scope_scope_proto_depIdxs,
MessageInfos: file_scope_scope_proto_msgTypes,
}.Build()
File_scope_scope_proto = out.File
file_scope_scope_proto_rawDesc = nil
file_scope_scope_proto_goTypes = nil
file_scope_scope_proto_depIdxs = nil
}

View File

@ -2,11 +2,17 @@ package sender
import (
"context"
"github.com/oklog/ulid"
"github.com/dstotijn/hetty/pkg/reqlog"
"github.com/dstotijn/hetty/pkg/scope"
)
type Repository interface {
FindSenderRequestByID(ctx context.Context, projectID, id string) (*Request, error)
FindSenderRequests(ctx context.Context, projectID string, filterFn func(*Request) (bool, error)) ([]*Request, error)
StoreSenderRequest(ctx context.Context, req *Request) error
DeleteSenderRequests(ctx context.Context, projectID string) error
FindSenderRequestByID(ctx context.Context, id ulid.ULID) (Request, error)
FindSenderRequests(ctx context.Context, filter FindRequestsFilter, scope *scope.Scope) ([]Request, error)
StoreSenderRequest(ctx context.Context, req Request) error
StoreResponseLog(ctx context.Context, reqLogID ulid.ULID, resLog reqlog.ResponseLog) error
DeleteSenderRequests(ctx context.Context, projectID ulid.ULID) error
}

View File

@ -0,0 +1,292 @@
// Code generated by moq; DO NOT EDIT.
// github.com/matryer/moq
package sender_test
import (
"context"
"github.com/dstotijn/hetty/pkg/reqlog"
"github.com/dstotijn/hetty/pkg/scope"
"github.com/dstotijn/hetty/pkg/sender"
"github.com/oklog/ulid"
"sync"
)
// Ensure, that RepoMock does implement sender.Repository.
// If this is not the case, regenerate this file with moq.
var _ sender.Repository = &RepoMock{}
// RepoMock is a mock implementation of sender.Repository.
//
// func TestSomethingThatUsesRepository(t *testing.T) {
//
// // make and configure a mocked sender.Repository
// mockedRepository := &RepoMock{
// DeleteSenderRequestsFunc: func(ctx context.Context, projectID ulid.ULID) error {
// panic("mock out the DeleteSenderRequests method")
// },
// FindSenderRequestByIDFunc: func(ctx context.Context, id ulid.ULID) (sender.Request, error) {
// panic("mock out the FindSenderRequestByID method")
// },
// FindSenderRequestsFunc: func(ctx context.Context, filter sender.FindRequestsFilter, scopeMoqParam *scope.Scope) ([]sender.Request, error) {
// panic("mock out the FindSenderRequests method")
// },
// StoreResponseLogFunc: func(ctx context.Context, reqLogID ulid.ULID, resLog reqlog.ResponseLog) error {
// panic("mock out the StoreResponseLog method")
// },
// StoreSenderRequestFunc: func(ctx context.Context, req sender.Request) error {
// panic("mock out the StoreSenderRequest method")
// },
// }
//
// // use mockedRepository in code that requires sender.Repository
// // and then make assertions.
//
// }
type RepoMock struct {
// DeleteSenderRequestsFunc mocks the DeleteSenderRequests method.
DeleteSenderRequestsFunc func(ctx context.Context, projectID ulid.ULID) error
// FindSenderRequestByIDFunc mocks the FindSenderRequestByID method.
FindSenderRequestByIDFunc func(ctx context.Context, id ulid.ULID) (sender.Request, error)
// FindSenderRequestsFunc mocks the FindSenderRequests method.
FindSenderRequestsFunc func(ctx context.Context, filter sender.FindRequestsFilter, scopeMoqParam *scope.Scope) ([]sender.Request, error)
// StoreResponseLogFunc mocks the StoreResponseLog method.
StoreResponseLogFunc func(ctx context.Context, reqLogID ulid.ULID, resLog reqlog.ResponseLog) error
// StoreSenderRequestFunc mocks the StoreSenderRequest method.
StoreSenderRequestFunc func(ctx context.Context, req sender.Request) error
// calls tracks calls to the methods.
calls struct {
// DeleteSenderRequests holds details about calls to the DeleteSenderRequests method.
DeleteSenderRequests []struct {
// Ctx is the ctx argument value.
Ctx context.Context
// ProjectID is the projectID argument value.
ProjectID ulid.ULID
}
// FindSenderRequestByID holds details about calls to the FindSenderRequestByID method.
FindSenderRequestByID []struct {
// Ctx is the ctx argument value.
Ctx context.Context
// ID is the id argument value.
ID ulid.ULID
}
// FindSenderRequests holds details about calls to the FindSenderRequests method.
FindSenderRequests []struct {
// Ctx is the ctx argument value.
Ctx context.Context
// Filter is the filter argument value.
Filter sender.FindRequestsFilter
// ScopeMoqParam is the scopeMoqParam argument value.
ScopeMoqParam *scope.Scope
}
// StoreResponseLog holds details about calls to the StoreResponseLog method.
StoreResponseLog []struct {
// Ctx is the ctx argument value.
Ctx context.Context
// ReqLogID is the reqLogID argument value.
ReqLogID ulid.ULID
// ResLog is the resLog argument value.
ResLog reqlog.ResponseLog
}
// StoreSenderRequest holds details about calls to the StoreSenderRequest method.
StoreSenderRequest []struct {
// Ctx is the ctx argument value.
Ctx context.Context
// Req is the req argument value.
Req sender.Request
}
}
lockDeleteSenderRequests sync.RWMutex
lockFindSenderRequestByID sync.RWMutex
lockFindSenderRequests sync.RWMutex
lockStoreResponseLog sync.RWMutex
lockStoreSenderRequest sync.RWMutex
}
// DeleteSenderRequests calls DeleteSenderRequestsFunc.
func (mock *RepoMock) DeleteSenderRequests(ctx context.Context, projectID ulid.ULID) error {
if mock.DeleteSenderRequestsFunc == nil {
panic("RepoMock.DeleteSenderRequestsFunc: method is nil but Repository.DeleteSenderRequests was just called")
}
callInfo := struct {
Ctx context.Context
ProjectID ulid.ULID
}{
Ctx: ctx,
ProjectID: projectID,
}
mock.lockDeleteSenderRequests.Lock()
mock.calls.DeleteSenderRequests = append(mock.calls.DeleteSenderRequests, callInfo)
mock.lockDeleteSenderRequests.Unlock()
return mock.DeleteSenderRequestsFunc(ctx, projectID)
}
// DeleteSenderRequestsCalls gets all the calls that were made to DeleteSenderRequests.
// Check the length with:
// len(mockedRepository.DeleteSenderRequestsCalls())
func (mock *RepoMock) DeleteSenderRequestsCalls() []struct {
Ctx context.Context
ProjectID ulid.ULID
} {
var calls []struct {
Ctx context.Context
ProjectID ulid.ULID
}
mock.lockDeleteSenderRequests.RLock()
calls = mock.calls.DeleteSenderRequests
mock.lockDeleteSenderRequests.RUnlock()
return calls
}
// FindSenderRequestByID calls FindSenderRequestByIDFunc.
func (mock *RepoMock) FindSenderRequestByID(ctx context.Context, id ulid.ULID) (sender.Request, error) {
if mock.FindSenderRequestByIDFunc == nil {
panic("RepoMock.FindSenderRequestByIDFunc: method is nil but Repository.FindSenderRequestByID was just called")
}
callInfo := struct {
Ctx context.Context
ID ulid.ULID
}{
Ctx: ctx,
ID: id,
}
mock.lockFindSenderRequestByID.Lock()
mock.calls.FindSenderRequestByID = append(mock.calls.FindSenderRequestByID, callInfo)
mock.lockFindSenderRequestByID.Unlock()
return mock.FindSenderRequestByIDFunc(ctx, id)
}
// FindSenderRequestByIDCalls gets all the calls that were made to FindSenderRequestByID.
// Check the length with:
// len(mockedRepository.FindSenderRequestByIDCalls())
func (mock *RepoMock) FindSenderRequestByIDCalls() []struct {
Ctx context.Context
ID ulid.ULID
} {
var calls []struct {
Ctx context.Context
ID ulid.ULID
}
mock.lockFindSenderRequestByID.RLock()
calls = mock.calls.FindSenderRequestByID
mock.lockFindSenderRequestByID.RUnlock()
return calls
}
// FindSenderRequests calls FindSenderRequestsFunc.
func (mock *RepoMock) FindSenderRequests(ctx context.Context, filter sender.FindRequestsFilter, scopeMoqParam *scope.Scope) ([]sender.Request, error) {
if mock.FindSenderRequestsFunc == nil {
panic("RepoMock.FindSenderRequestsFunc: method is nil but Repository.FindSenderRequests was just called")
}
callInfo := struct {
Ctx context.Context
Filter sender.FindRequestsFilter
ScopeMoqParam *scope.Scope
}{
Ctx: ctx,
Filter: filter,
ScopeMoqParam: scopeMoqParam,
}
mock.lockFindSenderRequests.Lock()
mock.calls.FindSenderRequests = append(mock.calls.FindSenderRequests, callInfo)
mock.lockFindSenderRequests.Unlock()
return mock.FindSenderRequestsFunc(ctx, filter, scopeMoqParam)
}
// FindSenderRequestsCalls gets all the calls that were made to FindSenderRequests.
// Check the length with:
// len(mockedRepository.FindSenderRequestsCalls())
func (mock *RepoMock) FindSenderRequestsCalls() []struct {
Ctx context.Context
Filter sender.FindRequestsFilter
ScopeMoqParam *scope.Scope
} {
var calls []struct {
Ctx context.Context
Filter sender.FindRequestsFilter
ScopeMoqParam *scope.Scope
}
mock.lockFindSenderRequests.RLock()
calls = mock.calls.FindSenderRequests
mock.lockFindSenderRequests.RUnlock()
return calls
}
// StoreResponseLog calls StoreResponseLogFunc.
func (mock *RepoMock) StoreResponseLog(ctx context.Context, reqLogID ulid.ULID, resLog reqlog.ResponseLog) error {
if mock.StoreResponseLogFunc == nil {
panic("RepoMock.StoreResponseLogFunc: method is nil but Repository.StoreResponseLog was just called")
}
callInfo := struct {
Ctx context.Context
ReqLogID ulid.ULID
ResLog reqlog.ResponseLog
}{
Ctx: ctx,
ReqLogID: reqLogID,
ResLog: resLog,
}
mock.lockStoreResponseLog.Lock()
mock.calls.StoreResponseLog = append(mock.calls.StoreResponseLog, callInfo)
mock.lockStoreResponseLog.Unlock()
return mock.StoreResponseLogFunc(ctx, reqLogID, resLog)
}
// StoreResponseLogCalls gets all the calls that were made to StoreResponseLog.
// Check the length with:
// len(mockedRepository.StoreResponseLogCalls())
func (mock *RepoMock) StoreResponseLogCalls() []struct {
Ctx context.Context
ReqLogID ulid.ULID
ResLog reqlog.ResponseLog
} {
var calls []struct {
Ctx context.Context
ReqLogID ulid.ULID
ResLog reqlog.ResponseLog
}
mock.lockStoreResponseLog.RLock()
calls = mock.calls.StoreResponseLog
mock.lockStoreResponseLog.RUnlock()
return calls
}
// StoreSenderRequest calls StoreSenderRequestFunc.
func (mock *RepoMock) StoreSenderRequest(ctx context.Context, req sender.Request) error {
if mock.StoreSenderRequestFunc == nil {
panic("RepoMock.StoreSenderRequestFunc: method is nil but Repository.StoreSenderRequest was just called")
}
callInfo := struct {
Ctx context.Context
Req sender.Request
}{
Ctx: ctx,
Req: req,
}
mock.lockStoreSenderRequest.Lock()
mock.calls.StoreSenderRequest = append(mock.calls.StoreSenderRequest, callInfo)
mock.lockStoreSenderRequest.Unlock()
return mock.StoreSenderRequestFunc(ctx, req)
}
// StoreSenderRequestCalls gets all the calls that were made to StoreSenderRequest.
// Check the length with:
// len(mockedRepository.StoreSenderRequestCalls())
func (mock *RepoMock) StoreSenderRequestCalls() []struct {
Ctx context.Context
Req sender.Request
} {
var calls []struct {
Ctx context.Context
Req sender.Request
}
mock.lockStoreSenderRequest.RLock()
calls = mock.calls.StoreSenderRequest
mock.lockStoreSenderRequest.RUnlock()
return calls
}

View File

@ -0,0 +1,498 @@
// Code generated by moq; DO NOT EDIT.
// github.com/matryer/moq
package sender_test
import (
"context"
"github.com/dstotijn/hetty/pkg/proxy"
"github.com/dstotijn/hetty/pkg/reqlog"
"github.com/oklog/ulid"
"sync"
)
// Ensure, that ReqLogServiceMock does implement reqlog.Service.
// If this is not the case, regenerate this file with moq.
var _ reqlog.Service = &ReqLogServiceMock{}
// ReqLogServiceMock is a mock implementation of reqlog.Service.
//
// func TestSomethingThatUsesService(t *testing.T) {
//
// // make and configure a mocked reqlog.Service
// mockedService := &ReqLogServiceMock{
// ActiveProjectIDFunc: func() ulid.ULID {
// panic("mock out the ActiveProjectID method")
// },
// BypassOutOfScopeRequestsFunc: func() bool {
// panic("mock out the BypassOutOfScopeRequests method")
// },
// ClearRequestsFunc: func(ctx context.Context, projectID ulid.ULID) error {
// panic("mock out the ClearRequests method")
// },
// FindReqsFilterFunc: func() reqlog.FindRequestsFilter {
// panic("mock out the FindReqsFilter method")
// },
// FindRequestLogByIDFunc: func(ctx context.Context, id ulid.ULID) (reqlog.RequestLog, error) {
// panic("mock out the FindRequestLogByID method")
// },
// FindRequestsFunc: func(ctx context.Context) ([]reqlog.RequestLog, error) {
// panic("mock out the FindRequests method")
// },
// RequestModifierFunc: func(next proxy.RequestModifyFunc) proxy.RequestModifyFunc {
// panic("mock out the RequestModifier method")
// },
// ResponseModifierFunc: func(next proxy.ResponseModifyFunc) proxy.ResponseModifyFunc {
// panic("mock out the ResponseModifier method")
// },
// SetActiveProjectIDFunc: func(id ulid.ULID) {
// panic("mock out the SetActiveProjectID method")
// },
// SetBypassOutOfScopeRequestsFunc: func(b bool) {
// panic("mock out the SetBypassOutOfScopeRequests method")
// },
// SetFindReqsFilterFunc: func(filter reqlog.FindRequestsFilter) {
// panic("mock out the SetFindReqsFilter method")
// },
// }
//
// // use mockedService in code that requires reqlog.Service
// // and then make assertions.
//
// }
type ReqLogServiceMock struct {
// ActiveProjectIDFunc mocks the ActiveProjectID method.
ActiveProjectIDFunc func() ulid.ULID
// BypassOutOfScopeRequestsFunc mocks the BypassOutOfScopeRequests method.
BypassOutOfScopeRequestsFunc func() bool
// ClearRequestsFunc mocks the ClearRequests method.
ClearRequestsFunc func(ctx context.Context, projectID ulid.ULID) error
// FindReqsFilterFunc mocks the FindReqsFilter method.
FindReqsFilterFunc func() reqlog.FindRequestsFilter
// FindRequestLogByIDFunc mocks the FindRequestLogByID method.
FindRequestLogByIDFunc func(ctx context.Context, id ulid.ULID) (reqlog.RequestLog, error)
// FindRequestsFunc mocks the FindRequests method.
FindRequestsFunc func(ctx context.Context) ([]reqlog.RequestLog, error)
// RequestModifierFunc mocks the RequestModifier method.
RequestModifierFunc func(next proxy.RequestModifyFunc) proxy.RequestModifyFunc
// ResponseModifierFunc mocks the ResponseModifier method.
ResponseModifierFunc func(next proxy.ResponseModifyFunc) proxy.ResponseModifyFunc
// SetActiveProjectIDFunc mocks the SetActiveProjectID method.
SetActiveProjectIDFunc func(id ulid.ULID)
// SetBypassOutOfScopeRequestsFunc mocks the SetBypassOutOfScopeRequests method.
SetBypassOutOfScopeRequestsFunc func(b bool)
// SetFindReqsFilterFunc mocks the SetFindReqsFilter method.
SetFindReqsFilterFunc func(filter reqlog.FindRequestsFilter)
// calls tracks calls to the methods.
calls struct {
// ActiveProjectID holds details about calls to the ActiveProjectID method.
ActiveProjectID []struct {
}
// BypassOutOfScopeRequests holds details about calls to the BypassOutOfScopeRequests method.
BypassOutOfScopeRequests []struct {
}
// ClearRequests holds details about calls to the ClearRequests method.
ClearRequests []struct {
// Ctx is the ctx argument value.
Ctx context.Context
// ProjectID is the projectID argument value.
ProjectID ulid.ULID
}
// FindReqsFilter holds details about calls to the FindReqsFilter method.
FindReqsFilter []struct {
}
// FindRequestLogByID holds details about calls to the FindRequestLogByID method.
FindRequestLogByID []struct {
// Ctx is the ctx argument value.
Ctx context.Context
// ID is the id argument value.
ID ulid.ULID
}
// FindRequests holds details about calls to the FindRequests method.
FindRequests []struct {
// Ctx is the ctx argument value.
Ctx context.Context
}
// RequestModifier holds details about calls to the RequestModifier method.
RequestModifier []struct {
// Next is the next argument value.
Next proxy.RequestModifyFunc
}
// ResponseModifier holds details about calls to the ResponseModifier method.
ResponseModifier []struct {
// Next is the next argument value.
Next proxy.ResponseModifyFunc
}
// SetActiveProjectID holds details about calls to the SetActiveProjectID method.
SetActiveProjectID []struct {
// ID is the id argument value.
ID ulid.ULID
}
// SetBypassOutOfScopeRequests holds details about calls to the SetBypassOutOfScopeRequests method.
SetBypassOutOfScopeRequests []struct {
// B is the b argument value.
B bool
}
// SetFindReqsFilter holds details about calls to the SetFindReqsFilter method.
SetFindReqsFilter []struct {
// Filter is the filter argument value.
Filter reqlog.FindRequestsFilter
}
}
lockActiveProjectID sync.RWMutex
lockBypassOutOfScopeRequests sync.RWMutex
lockClearRequests sync.RWMutex
lockFindReqsFilter sync.RWMutex
lockFindRequestLogByID sync.RWMutex
lockFindRequests sync.RWMutex
lockRequestModifier sync.RWMutex
lockResponseModifier sync.RWMutex
lockSetActiveProjectID sync.RWMutex
lockSetBypassOutOfScopeRequests sync.RWMutex
lockSetFindReqsFilter sync.RWMutex
}
// ActiveProjectID calls ActiveProjectIDFunc.
func (mock *ReqLogServiceMock) ActiveProjectID() ulid.ULID {
if mock.ActiveProjectIDFunc == nil {
panic("ReqLogServiceMock.ActiveProjectIDFunc: method is nil but Service.ActiveProjectID was just called")
}
callInfo := struct {
}{}
mock.lockActiveProjectID.Lock()
mock.calls.ActiveProjectID = append(mock.calls.ActiveProjectID, callInfo)
mock.lockActiveProjectID.Unlock()
return mock.ActiveProjectIDFunc()
}
// ActiveProjectIDCalls gets all the calls that were made to ActiveProjectID.
// Check the length with:
// len(mockedService.ActiveProjectIDCalls())
func (mock *ReqLogServiceMock) ActiveProjectIDCalls() []struct {
} {
var calls []struct {
}
mock.lockActiveProjectID.RLock()
calls = mock.calls.ActiveProjectID
mock.lockActiveProjectID.RUnlock()
return calls
}
// BypassOutOfScopeRequests calls BypassOutOfScopeRequestsFunc.
func (mock *ReqLogServiceMock) BypassOutOfScopeRequests() bool {
if mock.BypassOutOfScopeRequestsFunc == nil {
panic("ReqLogServiceMock.BypassOutOfScopeRequestsFunc: method is nil but Service.BypassOutOfScopeRequests was just called")
}
callInfo := struct {
}{}
mock.lockBypassOutOfScopeRequests.Lock()
mock.calls.BypassOutOfScopeRequests = append(mock.calls.BypassOutOfScopeRequests, callInfo)
mock.lockBypassOutOfScopeRequests.Unlock()
return mock.BypassOutOfScopeRequestsFunc()
}
// BypassOutOfScopeRequestsCalls gets all the calls that were made to BypassOutOfScopeRequests.
// Check the length with:
// len(mockedService.BypassOutOfScopeRequestsCalls())
func (mock *ReqLogServiceMock) BypassOutOfScopeRequestsCalls() []struct {
} {
var calls []struct {
}
mock.lockBypassOutOfScopeRequests.RLock()
calls = mock.calls.BypassOutOfScopeRequests
mock.lockBypassOutOfScopeRequests.RUnlock()
return calls
}
// ClearRequests calls ClearRequestsFunc.
func (mock *ReqLogServiceMock) ClearRequests(ctx context.Context, projectID ulid.ULID) error {
if mock.ClearRequestsFunc == nil {
panic("ReqLogServiceMock.ClearRequestsFunc: method is nil but Service.ClearRequests was just called")
}
callInfo := struct {
Ctx context.Context
ProjectID ulid.ULID
}{
Ctx: ctx,
ProjectID: projectID,
}
mock.lockClearRequests.Lock()
mock.calls.ClearRequests = append(mock.calls.ClearRequests, callInfo)
mock.lockClearRequests.Unlock()
return mock.ClearRequestsFunc(ctx, projectID)
}
// ClearRequestsCalls gets all the calls that were made to ClearRequests.
// Check the length with:
// len(mockedService.ClearRequestsCalls())
func (mock *ReqLogServiceMock) ClearRequestsCalls() []struct {
Ctx context.Context
ProjectID ulid.ULID
} {
var calls []struct {
Ctx context.Context
ProjectID ulid.ULID
}
mock.lockClearRequests.RLock()
calls = mock.calls.ClearRequests
mock.lockClearRequests.RUnlock()
return calls
}
// FindReqsFilter calls FindReqsFilterFunc.
func (mock *ReqLogServiceMock) FindReqsFilter() reqlog.FindRequestsFilter {
if mock.FindReqsFilterFunc == nil {
panic("ReqLogServiceMock.FindReqsFilterFunc: method is nil but Service.FindReqsFilter was just called")
}
callInfo := struct {
}{}
mock.lockFindReqsFilter.Lock()
mock.calls.FindReqsFilter = append(mock.calls.FindReqsFilter, callInfo)
mock.lockFindReqsFilter.Unlock()
return mock.FindReqsFilterFunc()
}
// FindReqsFilterCalls gets all the calls that were made to FindReqsFilter.
// Check the length with:
// len(mockedService.FindReqsFilterCalls())
func (mock *ReqLogServiceMock) FindReqsFilterCalls() []struct {
} {
var calls []struct {
}
mock.lockFindReqsFilter.RLock()
calls = mock.calls.FindReqsFilter
mock.lockFindReqsFilter.RUnlock()
return calls
}
// FindRequestLogByID calls FindRequestLogByIDFunc.
func (mock *ReqLogServiceMock) FindRequestLogByID(ctx context.Context, id ulid.ULID) (reqlog.RequestLog, error) {
if mock.FindRequestLogByIDFunc == nil {
panic("ReqLogServiceMock.FindRequestLogByIDFunc: method is nil but Service.FindRequestLogByID was just called")
}
callInfo := struct {
Ctx context.Context
ID ulid.ULID
}{
Ctx: ctx,
ID: id,
}
mock.lockFindRequestLogByID.Lock()
mock.calls.FindRequestLogByID = append(mock.calls.FindRequestLogByID, callInfo)
mock.lockFindRequestLogByID.Unlock()
return mock.FindRequestLogByIDFunc(ctx, id)
}
// FindRequestLogByIDCalls gets all the calls that were made to FindRequestLogByID.
// Check the length with:
// len(mockedService.FindRequestLogByIDCalls())
func (mock *ReqLogServiceMock) FindRequestLogByIDCalls() []struct {
Ctx context.Context
ID ulid.ULID
} {
var calls []struct {
Ctx context.Context
ID ulid.ULID
}
mock.lockFindRequestLogByID.RLock()
calls = mock.calls.FindRequestLogByID
mock.lockFindRequestLogByID.RUnlock()
return calls
}
// FindRequests calls FindRequestsFunc.
func (mock *ReqLogServiceMock) FindRequests(ctx context.Context) ([]reqlog.RequestLog, error) {
if mock.FindRequestsFunc == nil {
panic("ReqLogServiceMock.FindRequestsFunc: method is nil but Service.FindRequests was just called")
}
callInfo := struct {
Ctx context.Context
}{
Ctx: ctx,
}
mock.lockFindRequests.Lock()
mock.calls.FindRequests = append(mock.calls.FindRequests, callInfo)
mock.lockFindRequests.Unlock()
return mock.FindRequestsFunc(ctx)
}
// FindRequestsCalls gets all the calls that were made to FindRequests.
// Check the length with:
// len(mockedService.FindRequestsCalls())
func (mock *ReqLogServiceMock) FindRequestsCalls() []struct {
Ctx context.Context
} {
var calls []struct {
Ctx context.Context
}
mock.lockFindRequests.RLock()
calls = mock.calls.FindRequests
mock.lockFindRequests.RUnlock()
return calls
}
// RequestModifier calls RequestModifierFunc.
func (mock *ReqLogServiceMock) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestModifyFunc {
if mock.RequestModifierFunc == nil {
panic("ReqLogServiceMock.RequestModifierFunc: method is nil but Service.RequestModifier was just called")
}
callInfo := struct {
Next proxy.RequestModifyFunc
}{
Next: next,
}
mock.lockRequestModifier.Lock()
mock.calls.RequestModifier = append(mock.calls.RequestModifier, callInfo)
mock.lockRequestModifier.Unlock()
return mock.RequestModifierFunc(next)
}
// RequestModifierCalls gets all the calls that were made to RequestModifier.
// Check the length with:
// len(mockedService.RequestModifierCalls())
func (mock *ReqLogServiceMock) RequestModifierCalls() []struct {
Next proxy.RequestModifyFunc
} {
var calls []struct {
Next proxy.RequestModifyFunc
}
mock.lockRequestModifier.RLock()
calls = mock.calls.RequestModifier
mock.lockRequestModifier.RUnlock()
return calls
}
// ResponseModifier calls ResponseModifierFunc.
func (mock *ReqLogServiceMock) ResponseModifier(next proxy.ResponseModifyFunc) proxy.ResponseModifyFunc {
if mock.ResponseModifierFunc == nil {
panic("ReqLogServiceMock.ResponseModifierFunc: method is nil but Service.ResponseModifier was just called")
}
callInfo := struct {
Next proxy.ResponseModifyFunc
}{
Next: next,
}
mock.lockResponseModifier.Lock()
mock.calls.ResponseModifier = append(mock.calls.ResponseModifier, callInfo)
mock.lockResponseModifier.Unlock()
return mock.ResponseModifierFunc(next)
}
// ResponseModifierCalls gets all the calls that were made to ResponseModifier.
// Check the length with:
// len(mockedService.ResponseModifierCalls())
func (mock *ReqLogServiceMock) ResponseModifierCalls() []struct {
Next proxy.ResponseModifyFunc
} {
var calls []struct {
Next proxy.ResponseModifyFunc
}
mock.lockResponseModifier.RLock()
calls = mock.calls.ResponseModifier
mock.lockResponseModifier.RUnlock()
return calls
}
// SetActiveProjectID calls SetActiveProjectIDFunc.
func (mock *ReqLogServiceMock) SetActiveProjectID(id ulid.ULID) {
if mock.SetActiveProjectIDFunc == nil {
panic("ReqLogServiceMock.SetActiveProjectIDFunc: method is nil but Service.SetActiveProjectID was just called")
}
callInfo := struct {
ID ulid.ULID
}{
ID: id,
}
mock.lockSetActiveProjectID.Lock()
mock.calls.SetActiveProjectID = append(mock.calls.SetActiveProjectID, callInfo)
mock.lockSetActiveProjectID.Unlock()
mock.SetActiveProjectIDFunc(id)
}
// SetActiveProjectIDCalls gets all the calls that were made to SetActiveProjectID.
// Check the length with:
// len(mockedService.SetActiveProjectIDCalls())
func (mock *ReqLogServiceMock) SetActiveProjectIDCalls() []struct {
ID ulid.ULID
} {
var calls []struct {
ID ulid.ULID
}
mock.lockSetActiveProjectID.RLock()
calls = mock.calls.SetActiveProjectID
mock.lockSetActiveProjectID.RUnlock()
return calls
}
// SetBypassOutOfScopeRequests calls SetBypassOutOfScopeRequestsFunc.
func (mock *ReqLogServiceMock) SetBypassOutOfScopeRequests(b bool) {
if mock.SetBypassOutOfScopeRequestsFunc == nil {
panic("ReqLogServiceMock.SetBypassOutOfScopeRequestsFunc: method is nil but Service.SetBypassOutOfScopeRequests was just called")
}
callInfo := struct {
B bool
}{
B: b,
}
mock.lockSetBypassOutOfScopeRequests.Lock()
mock.calls.SetBypassOutOfScopeRequests = append(mock.calls.SetBypassOutOfScopeRequests, callInfo)
mock.lockSetBypassOutOfScopeRequests.Unlock()
mock.SetBypassOutOfScopeRequestsFunc(b)
}
// SetBypassOutOfScopeRequestsCalls gets all the calls that were made to SetBypassOutOfScopeRequests.
// Check the length with:
// len(mockedService.SetBypassOutOfScopeRequestsCalls())
func (mock *ReqLogServiceMock) SetBypassOutOfScopeRequestsCalls() []struct {
B bool
} {
var calls []struct {
B bool
}
mock.lockSetBypassOutOfScopeRequests.RLock()
calls = mock.calls.SetBypassOutOfScopeRequests
mock.lockSetBypassOutOfScopeRequests.RUnlock()
return calls
}
// SetFindReqsFilter calls SetFindReqsFilterFunc.
func (mock *ReqLogServiceMock) SetFindReqsFilter(filter reqlog.FindRequestsFilter) {
if mock.SetFindReqsFilterFunc == nil {
panic("ReqLogServiceMock.SetFindReqsFilterFunc: method is nil but Service.SetFindReqsFilter was just called")
}
callInfo := struct {
Filter reqlog.FindRequestsFilter
}{
Filter: filter,
}
mock.lockSetFindReqsFilter.Lock()
mock.calls.SetFindReqsFilter = append(mock.calls.SetFindReqsFilter, callInfo)
mock.lockSetFindReqsFilter.Unlock()
mock.SetFindReqsFilterFunc(filter)
}
// SetFindReqsFilterCalls gets all the calls that were made to SetFindReqsFilter.
// Check the length with:
// len(mockedService.SetFindReqsFilterCalls())
func (mock *ReqLogServiceMock) SetFindReqsFilterCalls() []struct {
Filter reqlog.FindRequestsFilter
} {
var calls []struct {
Filter reqlog.FindRequestsFilter
}
mock.lockSetFindReqsFilter.RLock()
calls = mock.calls.SetFindReqsFilter
mock.lockSetFindReqsFilter.RUnlock()
return calls
}

View File

@ -5,32 +5,31 @@ import (
"fmt"
"strings"
"github.com/oklog/ulid/v2"
"github.com/oklog/ulid"
"github.com/dstotijn/hetty/pkg/filter"
"github.com/dstotijn/hetty/pkg/http"
"github.com/dstotijn/hetty/pkg/reqlog"
"github.com/dstotijn/hetty/pkg/scope"
)
var senderReqSearchKeyFns = map[string]func(req *Request) string{
"req.id": func(req *Request) string { return req.Id },
"req.proto": func(req *Request) string { return req.GetHttpRequest().GetProtocol().String() },
"req.url": func(req *Request) string { return req.GetHttpRequest().GetUrl() },
"req.method": func(req *Request) string { return req.GetHttpRequest().GetMethod().String() },
"req.body": func(req *Request) string { return string(req.GetHttpRequest().GetBody()) },
"req.timestamp": func(req *Request) string {
id, err := ulid.Parse(req.Id)
if err != nil {
var senderReqSearchKeyFns = map[string]func(req Request) string{
"req.id": func(req Request) string { return req.ID.String() },
"req.proto": func(req Request) string { return req.Proto },
"req.url": func(req Request) string {
if req.URL == nil {
return ""
}
return ulid.Time(id.Time()).String()
return req.URL.String()
},
"req.method": func(req Request) string { return req.Method },
"req.body": func(req Request) string { return string(req.Body) },
"req.timestamp": func(req Request) string { return ulid.Time(req.ID.Time()).String() },
}
// TODO: Request and response headers search key functions.
// Matches returns true if the supplied search expression evaluates to true.
func (req *Request) Matches(expr filter.Expression) (bool, error) {
func (req Request) Matches(expr filter.Expression) (bool, error) {
switch e := expr.(type) {
case filter.PrefixExpression:
return req.matchPrefixExpr(e)
@ -43,7 +42,7 @@ func (req *Request) Matches(expr filter.Expression) (bool, error) {
}
}
func (req *Request) matchPrefixExpr(expr filter.PrefixExpression) (bool, error) {
func (req Request) matchPrefixExpr(expr filter.PrefixExpression) (bool, error) {
switch expr.Operator {
case filter.TokOpNot:
match, err := req.Matches(expr.Right)
@ -57,7 +56,7 @@ func (req *Request) matchPrefixExpr(expr filter.PrefixExpression) (bool, error)
}
}
func (req *Request) matchInfixExpr(expr filter.InfixExpression) (bool, error) {
func (req Request) matchInfixExpr(expr filter.InfixExpression) (bool, error) {
switch expr.Operator {
case filter.TokOpAnd:
left, err := req.Matches(expr.Left)
@ -93,7 +92,7 @@ func (req *Request) matchInfixExpr(expr filter.InfixExpression) (bool, error) {
leftVal := req.getMappedStringLiteral(left.Value)
if leftVal == "req.headers" {
match, err := filter.MatchHTTPHeaders(expr.Operator, expr.Right, req.GetHttpRequest().GetHeaders())
match, err := filter.MatchHTTPHeaders(expr.Operator, expr.Right, req.Header)
if err != nil {
return false, fmt.Errorf("failed to match request HTTP headers: %w", err)
}
@ -101,8 +100,8 @@ func (req *Request) matchInfixExpr(expr filter.InfixExpression) (bool, error) {
return match, nil
}
if leftVal == "res.headers" && req.GetHttpResponse() != nil {
match, err := filter.MatchHTTPHeaders(expr.Operator, expr.Right, req.GetHttpResponse().GetHeaders())
if leftVal == "res.headers" && req.Response != nil {
match, err := filter.MatchHTTPHeaders(expr.Operator, expr.Right, req.Response.Header)
if err != nil {
return false, fmt.Errorf("failed to match response HTTP headers: %w", err)
}
@ -153,7 +152,7 @@ func (req *Request) matchInfixExpr(expr filter.InfixExpression) (bool, error) {
}
}
func (req *Request) getMappedStringLiteral(s string) string {
func (req Request) getMappedStringLiteral(s string) string {
switch {
case strings.HasPrefix(s, "req."):
fn, ok := senderReqSearchKeyFns[s]
@ -161,22 +160,28 @@ func (req *Request) getMappedStringLiteral(s string) string {
return fn(req)
}
case strings.HasPrefix(s, "res."):
fn, ok := http.ResponseSearchKeyFns[s]
if req.Response == nil {
return ""
}
fn, ok := reqlog.ResLogSearchKeyFns[s]
if ok {
return fn(req.GetHttpResponse())
return fn(*req.Response)
}
}
return s
}
func (req *Request) matchStringLiteral(strLiteral filter.StringLiteral) (bool, error) {
for _, header := range req.GetHttpRequest().GetHeaders() {
if strings.Contains(
strings.ToLower(fmt.Sprintf("%v: %v", header.Key, header.Value)),
strings.ToLower(strLiteral.Value),
) {
return true, nil
func (req Request) matchStringLiteral(strLiteral filter.StringLiteral) (bool, error) {
for key, values := range req.Header {
for _, value := range values {
if strings.Contains(
strings.ToLower(fmt.Sprintf("%v: %v", key, value)),
strings.ToLower(strLiteral.Value),
) {
return true, nil
}
}
}
@ -189,47 +194,54 @@ func (req *Request) matchStringLiteral(strLiteral filter.StringLiteral) (bool, e
}
}
for _, header := range req.GetHttpResponse().GetHeaders() {
if strings.Contains(
strings.ToLower(fmt.Sprintf("%v: %v", header.Key, header.Value)),
strings.ToLower(strLiteral.Value),
) {
return true, nil
if req.Response != nil {
for key, values := range req.Response.Header {
for _, value := range values {
if strings.Contains(
strings.ToLower(fmt.Sprintf("%v: %v", key, value)),
strings.ToLower(strLiteral.Value),
) {
return true, nil
}
}
}
}
for _, fn := range http.ResponseSearchKeyFns {
if strings.Contains(
strings.ToLower(fn(req.GetHttpResponse())),
strings.ToLower(strLiteral.Value),
) {
return true, nil
for _, fn := range reqlog.ResLogSearchKeyFns {
if strings.Contains(
strings.ToLower(fn(*req.Response)),
strings.ToLower(strLiteral.Value),
) {
return true, nil
}
}
}
return false, nil
}
func (req *Request) MatchScope(s *scope.Scope) bool {
func (req Request) MatchScope(s *scope.Scope) bool {
for _, rule := range s.Rules() {
if url := req.GetHttpRequest().GetUrl(); rule.URL != nil && url != "" {
if matches := rule.URL.MatchString(url); matches {
if rule.URL != nil && req.URL != nil {
if matches := rule.URL.MatchString(req.URL.String()); matches {
return true
}
}
for _, headers := range req.GetHttpRequest().GetHeaders() {
for key, values := range req.Header {
var keyMatches, valueMatches bool
if rule.Header.Key != nil {
if matches := rule.Header.Key.MatchString(headers.Key); matches {
if matches := rule.Header.Key.MatchString(key); matches {
keyMatches = true
}
}
if rule.Header.Value != nil {
if matches := rule.Header.Value.MatchString(headers.Value); matches {
valueMatches = true
for _, value := range values {
if matches := rule.Header.Value.MatchString(value); matches {
valueMatches = true
break
}
}
}
// When only key or value is set, match on whatever is set.
@ -245,7 +257,7 @@ func (req *Request) MatchScope(s *scope.Scope) bool {
}
if rule.Body != nil {
if matches := rule.Body.Match(req.GetHttpRequest().GetBody()); matches {
if matches := rule.Body.Match(req.Body); matches {
return true
}
}

View File

@ -4,7 +4,7 @@ import (
"testing"
"github.com/dstotijn/hetty/pkg/filter"
"github.com/dstotijn/hetty/pkg/http"
"github.com/dstotijn/hetty/pkg/reqlog"
"github.com/dstotijn/hetty/pkg/sender"
)
@ -14,17 +14,15 @@ func TestRequestLogMatch(t *testing.T) {
tests := []struct {
name string
query string
senderReq *sender.Request
senderReq sender.Request
expectedMatch bool
expectedError error
}{
{
name: "infix expression, equal operator, match",
query: "req.body = foo",
senderReq: &sender.Request{
HttpRequest: &http.Request{
Body: []byte("foo"),
},
senderReq: sender.Request{
Body: []byte("foo"),
},
expectedMatch: true,
expectedError: nil,
@ -32,10 +30,8 @@ func TestRequestLogMatch(t *testing.T) {
{
name: "infix expression, not equal operator, match",
query: "req.body != bar",
senderReq: &sender.Request{
HttpRequest: &http.Request{
Body: []byte("foo"),
},
senderReq: sender.Request{
Body: []byte("foo"),
},
expectedMatch: true,
expectedError: nil,
@ -43,10 +39,8 @@ func TestRequestLogMatch(t *testing.T) {
{
name: "infix expression, greater than operator, match",
query: "req.body > a",
senderReq: &sender.Request{
HttpRequest: &http.Request{
Body: []byte("b"),
},
senderReq: sender.Request{
Body: []byte("b"),
},
expectedMatch: true,
expectedError: nil,
@ -54,10 +48,8 @@ func TestRequestLogMatch(t *testing.T) {
{
name: "infix expression, less than operator, match",
query: "req.body < b",
senderReq: &sender.Request{
HttpRequest: &http.Request{
Body: []byte("a"),
},
senderReq: sender.Request{
Body: []byte("a"),
},
expectedMatch: true,
expectedError: nil,
@ -65,10 +57,8 @@ func TestRequestLogMatch(t *testing.T) {
{
name: "infix expression, greater than or equal operator, match greater than",
query: "req.body >= a",
senderReq: &sender.Request{
HttpRequest: &http.Request{
Body: []byte("b"),
},
senderReq: sender.Request{
Body: []byte("b"),
},
expectedMatch: true,
expectedError: nil,
@ -76,10 +66,8 @@ func TestRequestLogMatch(t *testing.T) {
{
name: "infix expression, greater than or equal operator, match equal",
query: "req.body >= a",
senderReq: &sender.Request{
HttpRequest: &http.Request{
Body: []byte("a"),
},
senderReq: sender.Request{
Body: []byte("a"),
},
expectedMatch: true,
expectedError: nil,
@ -87,10 +75,8 @@ func TestRequestLogMatch(t *testing.T) {
{
name: "infix expression, less than or equal operator, match less than",
query: "req.body <= b",
senderReq: &sender.Request{
HttpRequest: &http.Request{
Body: []byte("a"),
},
senderReq: sender.Request{
Body: []byte("a"),
},
expectedMatch: true,
expectedError: nil,
@ -98,10 +84,8 @@ func TestRequestLogMatch(t *testing.T) {
{
name: "infix expression, less than or equal operator, match equal",
query: "req.body <= b",
senderReq: &sender.Request{
HttpRequest: &http.Request{
Body: []byte("b"),
},
senderReq: sender.Request{
Body: []byte("b"),
},
expectedMatch: true,
expectedError: nil,
@ -109,10 +93,8 @@ func TestRequestLogMatch(t *testing.T) {
{
name: "infix expression, regular expression operator, match",
query: `req.body =~ "^foo(.*)$"`,
senderReq: &sender.Request{
HttpRequest: &http.Request{
Body: []byte("foobar"),
},
senderReq: sender.Request{
Body: []byte("foobar"),
},
expectedMatch: true,
expectedError: nil,
@ -120,10 +102,8 @@ func TestRequestLogMatch(t *testing.T) {
{
name: "infix expression, negate regular expression operator, match",
query: `req.body !~ "^foo(.*)$"`,
senderReq: &sender.Request{
HttpRequest: &http.Request{
Body: []byte("xoobar"),
},
senderReq: sender.Request{
Body: []byte("xoobar"),
},
expectedMatch: true,
expectedError: nil,
@ -131,11 +111,9 @@ func TestRequestLogMatch(t *testing.T) {
{
name: "infix expression, and operator, match",
query: "req.body = bar AND res.body = yolo",
senderReq: &sender.Request{
HttpRequest: &http.Request{
Body: []byte("bar"),
},
HttpResponse: &http.Response{
senderReq: sender.Request{
Body: []byte("bar"),
Response: &reqlog.ResponseLog{
Body: []byte("yolo"),
},
},
@ -145,11 +123,9 @@ func TestRequestLogMatch(t *testing.T) {
{
name: "infix expression, or operator, match",
query: "req.body = bar OR res.body = yolo",
senderReq: &sender.Request{
HttpRequest: &http.Request{
Body: []byte("foo"),
},
HttpResponse: &http.Response{
senderReq: sender.Request{
Body: []byte("foo"),
Response: &reqlog.ResponseLog{
Body: []byte("yolo"),
},
},
@ -159,10 +135,8 @@ func TestRequestLogMatch(t *testing.T) {
{
name: "prefix expression, not operator, match",
query: "NOT (req.body = bar)",
senderReq: &sender.Request{
HttpRequest: &http.Request{
Body: []byte("foo"),
},
senderReq: sender.Request{
Body: []byte("foo"),
},
expectedMatch: true,
expectedError: nil,
@ -170,10 +144,8 @@ func TestRequestLogMatch(t *testing.T) {
{
name: "string literal expression, match in request log",
query: "foo",
senderReq: &sender.Request{
HttpRequest: &http.Request{
Body: []byte("foo"),
},
senderReq: sender.Request{
Body: []byte("foo"),
},
expectedMatch: true,
expectedError: nil,
@ -181,10 +153,8 @@ func TestRequestLogMatch(t *testing.T) {
{
name: "string literal expression, no match",
query: "foo",
senderReq: &sender.Request{
HttpRequest: &http.Request{
Body: []byte("bar"),
},
senderReq: sender.Request{
Body: []byte("bar"),
},
expectedMatch: false,
expectedError: nil,
@ -192,8 +162,8 @@ func TestRequestLogMatch(t *testing.T) {
{
name: "string literal expression, match in response log",
query: "foo",
senderReq: &sender.Request{
HttpResponse: &http.Response{
senderReq: sender.Request{
Response: &reqlog.ResponseLog{
Body: []byte("foo"),
},
},

View File

@ -1,311 +0,0 @@
// Code generated by protoc-gen-connect-go. DO NOT EDIT.
//
// Source: sender/sender.proto
package sender
import (
connect "connectrpc.com/connect"
context "context"
errors "errors"
http "net/http"
strings "strings"
)
// This is a compile-time assertion to ensure that this generated file and the connect package are
// compatible. If you get a compiler error that this constant is not defined, this code was
// generated with a version of connect newer than the one compiled into your binary. You can fix the
// problem by either regenerating this code with an older version of connect or updating the connect
// version compiled into your binary.
const _ = connect.IsAtLeastVersion1_13_0
const (
// SenderServiceName is the fully-qualified name of the SenderService service.
SenderServiceName = "sender.SenderService"
)
// These constants are the fully-qualified names of the RPCs defined in this package. They're
// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route.
//
// Note that these are different from the fully-qualified method names used by
// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to
// reflection-formatted method names, remove the leading slash and convert the remaining slash to a
// period.
const (
// SenderServiceGetRequestByIDProcedure is the fully-qualified name of the SenderService's
// GetRequestByID RPC.
SenderServiceGetRequestByIDProcedure = "/sender.SenderService/GetRequestByID"
// SenderServiceListRequestsProcedure is the fully-qualified name of the SenderService's
// ListRequests RPC.
SenderServiceListRequestsProcedure = "/sender.SenderService/ListRequests"
// SenderServiceSetRequestsFilterProcedure is the fully-qualified name of the SenderService's
// SetRequestsFilter RPC.
SenderServiceSetRequestsFilterProcedure = "/sender.SenderService/SetRequestsFilter"
// SenderServiceGetRequestsFilterProcedure is the fully-qualified name of the SenderService's
// GetRequestsFilter RPC.
SenderServiceGetRequestsFilterProcedure = "/sender.SenderService/GetRequestsFilter"
// SenderServiceCreateOrUpdateRequestProcedure is the fully-qualified name of the SenderService's
// CreateOrUpdateRequest RPC.
SenderServiceCreateOrUpdateRequestProcedure = "/sender.SenderService/CreateOrUpdateRequest"
// SenderServiceCloneFromRequestLogProcedure is the fully-qualified name of the SenderService's
// CloneFromRequestLog RPC.
SenderServiceCloneFromRequestLogProcedure = "/sender.SenderService/CloneFromRequestLog"
// SenderServiceSendRequestProcedure is the fully-qualified name of the SenderService's SendRequest
// RPC.
SenderServiceSendRequestProcedure = "/sender.SenderService/SendRequest"
// SenderServiceDeleteRequestsProcedure is the fully-qualified name of the SenderService's
// DeleteRequests RPC.
SenderServiceDeleteRequestsProcedure = "/sender.SenderService/DeleteRequests"
)
// SenderServiceClient is a client for the sender.SenderService service.
type SenderServiceClient interface {
GetRequestByID(context.Context, *connect.Request[GetRequestByIDRequest]) (*connect.Response[GetRequestByIDResponse], error)
ListRequests(context.Context, *connect.Request[ListRequestsRequest]) (*connect.Response[ListRequestsResponse], error)
SetRequestsFilter(context.Context, *connect.Request[SetRequestsFilterRequest]) (*connect.Response[SetRequestsFilterResponse], error)
GetRequestsFilter(context.Context, *connect.Request[GetRequestsFilterRequest]) (*connect.Response[GetRequestsFilterResponse], error)
CreateOrUpdateRequest(context.Context, *connect.Request[CreateOrUpdateRequestRequest]) (*connect.Response[CreateOrUpdateRequestResponse], error)
CloneFromRequestLog(context.Context, *connect.Request[CloneFromRequestLogRequest]) (*connect.Response[CloneFromRequestLogResponse], error)
SendRequest(context.Context, *connect.Request[SendRequestRequest]) (*connect.Response[SendRequestResponse], error)
DeleteRequests(context.Context, *connect.Request[DeleteRequestsRequest]) (*connect.Response[DeleteRequestsResponse], error)
}
// NewSenderServiceClient constructs a client for the sender.SenderService service. By default, it
// uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends
// uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or
// connect.WithGRPCWeb() options.
//
// The URL supplied here should be the base URL for the Connect or gRPC server (for example,
// http://api.acme.com or https://acme.com/grpc).
func NewSenderServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) SenderServiceClient {
baseURL = strings.TrimRight(baseURL, "/")
senderServiceMethods := File_sender_sender_proto.Services().ByName("SenderService").Methods()
return &senderServiceClient{
getRequestByID: connect.NewClient[GetRequestByIDRequest, GetRequestByIDResponse](
httpClient,
baseURL+SenderServiceGetRequestByIDProcedure,
connect.WithSchema(senderServiceMethods.ByName("GetRequestByID")),
connect.WithClientOptions(opts...),
),
listRequests: connect.NewClient[ListRequestsRequest, ListRequestsResponse](
httpClient,
baseURL+SenderServiceListRequestsProcedure,
connect.WithSchema(senderServiceMethods.ByName("ListRequests")),
connect.WithClientOptions(opts...),
),
setRequestsFilter: connect.NewClient[SetRequestsFilterRequest, SetRequestsFilterResponse](
httpClient,
baseURL+SenderServiceSetRequestsFilterProcedure,
connect.WithSchema(senderServiceMethods.ByName("SetRequestsFilter")),
connect.WithClientOptions(opts...),
),
getRequestsFilter: connect.NewClient[GetRequestsFilterRequest, GetRequestsFilterResponse](
httpClient,
baseURL+SenderServiceGetRequestsFilterProcedure,
connect.WithSchema(senderServiceMethods.ByName("GetRequestsFilter")),
connect.WithClientOptions(opts...),
),
createOrUpdateRequest: connect.NewClient[CreateOrUpdateRequestRequest, CreateOrUpdateRequestResponse](
httpClient,
baseURL+SenderServiceCreateOrUpdateRequestProcedure,
connect.WithSchema(senderServiceMethods.ByName("CreateOrUpdateRequest")),
connect.WithClientOptions(opts...),
),
cloneFromRequestLog: connect.NewClient[CloneFromRequestLogRequest, CloneFromRequestLogResponse](
httpClient,
baseURL+SenderServiceCloneFromRequestLogProcedure,
connect.WithSchema(senderServiceMethods.ByName("CloneFromRequestLog")),
connect.WithClientOptions(opts...),
),
sendRequest: connect.NewClient[SendRequestRequest, SendRequestResponse](
httpClient,
baseURL+SenderServiceSendRequestProcedure,
connect.WithSchema(senderServiceMethods.ByName("SendRequest")),
connect.WithClientOptions(opts...),
),
deleteRequests: connect.NewClient[DeleteRequestsRequest, DeleteRequestsResponse](
httpClient,
baseURL+SenderServiceDeleteRequestsProcedure,
connect.WithSchema(senderServiceMethods.ByName("DeleteRequests")),
connect.WithClientOptions(opts...),
),
}
}
// senderServiceClient implements SenderServiceClient.
type senderServiceClient struct {
getRequestByID *connect.Client[GetRequestByIDRequest, GetRequestByIDResponse]
listRequests *connect.Client[ListRequestsRequest, ListRequestsResponse]
setRequestsFilter *connect.Client[SetRequestsFilterRequest, SetRequestsFilterResponse]
getRequestsFilter *connect.Client[GetRequestsFilterRequest, GetRequestsFilterResponse]
createOrUpdateRequest *connect.Client[CreateOrUpdateRequestRequest, CreateOrUpdateRequestResponse]
cloneFromRequestLog *connect.Client[CloneFromRequestLogRequest, CloneFromRequestLogResponse]
sendRequest *connect.Client[SendRequestRequest, SendRequestResponse]
deleteRequests *connect.Client[DeleteRequestsRequest, DeleteRequestsResponse]
}
// GetRequestByID calls sender.SenderService.GetRequestByID.
func (c *senderServiceClient) GetRequestByID(ctx context.Context, req *connect.Request[GetRequestByIDRequest]) (*connect.Response[GetRequestByIDResponse], error) {
return c.getRequestByID.CallUnary(ctx, req)
}
// ListRequests calls sender.SenderService.ListRequests.
func (c *senderServiceClient) ListRequests(ctx context.Context, req *connect.Request[ListRequestsRequest]) (*connect.Response[ListRequestsResponse], error) {
return c.listRequests.CallUnary(ctx, req)
}
// SetRequestsFilter calls sender.SenderService.SetRequestsFilter.
func (c *senderServiceClient) SetRequestsFilter(ctx context.Context, req *connect.Request[SetRequestsFilterRequest]) (*connect.Response[SetRequestsFilterResponse], error) {
return c.setRequestsFilter.CallUnary(ctx, req)
}
// GetRequestsFilter calls sender.SenderService.GetRequestsFilter.
func (c *senderServiceClient) GetRequestsFilter(ctx context.Context, req *connect.Request[GetRequestsFilterRequest]) (*connect.Response[GetRequestsFilterResponse], error) {
return c.getRequestsFilter.CallUnary(ctx, req)
}
// CreateOrUpdateRequest calls sender.SenderService.CreateOrUpdateRequest.
func (c *senderServiceClient) CreateOrUpdateRequest(ctx context.Context, req *connect.Request[CreateOrUpdateRequestRequest]) (*connect.Response[CreateOrUpdateRequestResponse], error) {
return c.createOrUpdateRequest.CallUnary(ctx, req)
}
// CloneFromRequestLog calls sender.SenderService.CloneFromRequestLog.
func (c *senderServiceClient) CloneFromRequestLog(ctx context.Context, req *connect.Request[CloneFromRequestLogRequest]) (*connect.Response[CloneFromRequestLogResponse], error) {
return c.cloneFromRequestLog.CallUnary(ctx, req)
}
// SendRequest calls sender.SenderService.SendRequest.
func (c *senderServiceClient) SendRequest(ctx context.Context, req *connect.Request[SendRequestRequest]) (*connect.Response[SendRequestResponse], error) {
return c.sendRequest.CallUnary(ctx, req)
}
// DeleteRequests calls sender.SenderService.DeleteRequests.
func (c *senderServiceClient) DeleteRequests(ctx context.Context, req *connect.Request[DeleteRequestsRequest]) (*connect.Response[DeleteRequestsResponse], error) {
return c.deleteRequests.CallUnary(ctx, req)
}
// SenderServiceHandler is an implementation of the sender.SenderService service.
type SenderServiceHandler interface {
GetRequestByID(context.Context, *connect.Request[GetRequestByIDRequest]) (*connect.Response[GetRequestByIDResponse], error)
ListRequests(context.Context, *connect.Request[ListRequestsRequest]) (*connect.Response[ListRequestsResponse], error)
SetRequestsFilter(context.Context, *connect.Request[SetRequestsFilterRequest]) (*connect.Response[SetRequestsFilterResponse], error)
GetRequestsFilter(context.Context, *connect.Request[GetRequestsFilterRequest]) (*connect.Response[GetRequestsFilterResponse], error)
CreateOrUpdateRequest(context.Context, *connect.Request[CreateOrUpdateRequestRequest]) (*connect.Response[CreateOrUpdateRequestResponse], error)
CloneFromRequestLog(context.Context, *connect.Request[CloneFromRequestLogRequest]) (*connect.Response[CloneFromRequestLogResponse], error)
SendRequest(context.Context, *connect.Request[SendRequestRequest]) (*connect.Response[SendRequestResponse], error)
DeleteRequests(context.Context, *connect.Request[DeleteRequestsRequest]) (*connect.Response[DeleteRequestsResponse], error)
}
// NewSenderServiceHandler builds an HTTP handler from the service implementation. It returns the
// path on which to mount the handler and the handler itself.
//
// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf
// and JSON codecs. They also support gzip compression.
func NewSenderServiceHandler(svc SenderServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) {
senderServiceMethods := File_sender_sender_proto.Services().ByName("SenderService").Methods()
senderServiceGetRequestByIDHandler := connect.NewUnaryHandler(
SenderServiceGetRequestByIDProcedure,
svc.GetRequestByID,
connect.WithSchema(senderServiceMethods.ByName("GetRequestByID")),
connect.WithHandlerOptions(opts...),
)
senderServiceListRequestsHandler := connect.NewUnaryHandler(
SenderServiceListRequestsProcedure,
svc.ListRequests,
connect.WithSchema(senderServiceMethods.ByName("ListRequests")),
connect.WithHandlerOptions(opts...),
)
senderServiceSetRequestsFilterHandler := connect.NewUnaryHandler(
SenderServiceSetRequestsFilterProcedure,
svc.SetRequestsFilter,
connect.WithSchema(senderServiceMethods.ByName("SetRequestsFilter")),
connect.WithHandlerOptions(opts...),
)
senderServiceGetRequestsFilterHandler := connect.NewUnaryHandler(
SenderServiceGetRequestsFilterProcedure,
svc.GetRequestsFilter,
connect.WithSchema(senderServiceMethods.ByName("GetRequestsFilter")),
connect.WithHandlerOptions(opts...),
)
senderServiceCreateOrUpdateRequestHandler := connect.NewUnaryHandler(
SenderServiceCreateOrUpdateRequestProcedure,
svc.CreateOrUpdateRequest,
connect.WithSchema(senderServiceMethods.ByName("CreateOrUpdateRequest")),
connect.WithHandlerOptions(opts...),
)
senderServiceCloneFromRequestLogHandler := connect.NewUnaryHandler(
SenderServiceCloneFromRequestLogProcedure,
svc.CloneFromRequestLog,
connect.WithSchema(senderServiceMethods.ByName("CloneFromRequestLog")),
connect.WithHandlerOptions(opts...),
)
senderServiceSendRequestHandler := connect.NewUnaryHandler(
SenderServiceSendRequestProcedure,
svc.SendRequest,
connect.WithSchema(senderServiceMethods.ByName("SendRequest")),
connect.WithHandlerOptions(opts...),
)
senderServiceDeleteRequestsHandler := connect.NewUnaryHandler(
SenderServiceDeleteRequestsProcedure,
svc.DeleteRequests,
connect.WithSchema(senderServiceMethods.ByName("DeleteRequests")),
connect.WithHandlerOptions(opts...),
)
return "/sender.SenderService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case SenderServiceGetRequestByIDProcedure:
senderServiceGetRequestByIDHandler.ServeHTTP(w, r)
case SenderServiceListRequestsProcedure:
senderServiceListRequestsHandler.ServeHTTP(w, r)
case SenderServiceSetRequestsFilterProcedure:
senderServiceSetRequestsFilterHandler.ServeHTTP(w, r)
case SenderServiceGetRequestsFilterProcedure:
senderServiceGetRequestsFilterHandler.ServeHTTP(w, r)
case SenderServiceCreateOrUpdateRequestProcedure:
senderServiceCreateOrUpdateRequestHandler.ServeHTTP(w, r)
case SenderServiceCloneFromRequestLogProcedure:
senderServiceCloneFromRequestLogHandler.ServeHTTP(w, r)
case SenderServiceSendRequestProcedure:
senderServiceSendRequestHandler.ServeHTTP(w, r)
case SenderServiceDeleteRequestsProcedure:
senderServiceDeleteRequestsHandler.ServeHTTP(w, r)
default:
http.NotFound(w, r)
}
})
}
// UnimplementedSenderServiceHandler returns CodeUnimplemented from all methods.
type UnimplementedSenderServiceHandler struct{}
func (UnimplementedSenderServiceHandler) GetRequestByID(context.Context, *connect.Request[GetRequestByIDRequest]) (*connect.Response[GetRequestByIDResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("sender.SenderService.GetRequestByID is not implemented"))
}
func (UnimplementedSenderServiceHandler) ListRequests(context.Context, *connect.Request[ListRequestsRequest]) (*connect.Response[ListRequestsResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("sender.SenderService.ListRequests is not implemented"))
}
func (UnimplementedSenderServiceHandler) SetRequestsFilter(context.Context, *connect.Request[SetRequestsFilterRequest]) (*connect.Response[SetRequestsFilterResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("sender.SenderService.SetRequestsFilter is not implemented"))
}
func (UnimplementedSenderServiceHandler) GetRequestsFilter(context.Context, *connect.Request[GetRequestsFilterRequest]) (*connect.Response[GetRequestsFilterResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("sender.SenderService.GetRequestsFilter is not implemented"))
}
func (UnimplementedSenderServiceHandler) CreateOrUpdateRequest(context.Context, *connect.Request[CreateOrUpdateRequestRequest]) (*connect.Response[CreateOrUpdateRequestResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("sender.SenderService.CreateOrUpdateRequest is not implemented"))
}
func (UnimplementedSenderServiceHandler) CloneFromRequestLog(context.Context, *connect.Request[CloneFromRequestLogRequest]) (*connect.Response[CloneFromRequestLogResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("sender.SenderService.CloneFromRequestLog is not implemented"))
}
func (UnimplementedSenderServiceHandler) SendRequest(context.Context, *connect.Request[SendRequestRequest]) (*connect.Response[SendRequestResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("sender.SenderService.SendRequest is not implemented"))
}
func (UnimplementedSenderServiceHandler) DeleteRequests(context.Context, *connect.Request[DeleteRequestsRequest]) (*connect.Response[DeleteRequestsResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("sender.SenderService.DeleteRequests is not implemented"))
}

View File

@ -5,19 +5,21 @@ import (
"context"
"errors"
"fmt"
"math/rand"
"net/http"
"net/url"
"time"
connect "connectrpc.com/connect"
"github.com/oklog/ulid/v2"
"google.golang.org/protobuf/proto"
"github.com/oklog/ulid"
"github.com/dstotijn/hetty/pkg/filter"
httppb "github.com/dstotijn/hetty/pkg/http"
"github.com/dstotijn/hetty/pkg/reqlog"
"github.com/dstotijn/hetty/pkg/scope"
)
//nolint:gosec
var ulidEntropy = rand.New(rand.NewSource(time.Now().UnixNano()))
var defaultHTTPClient = &http.Client{
Transport: &HTTPTransport{},
Timeout: 30 * time.Second,
@ -28,19 +30,37 @@ var (
ErrRequestNotFound = errors.New("sender: request not found")
)
type Service struct {
activeProjectID string
reqsFilter *RequestsFilter
type Service interface {
FindRequestByID(ctx context.Context, id ulid.ULID) (Request, error)
FindRequests(ctx context.Context) ([]Request, error)
CreateOrUpdateRequest(ctx context.Context, req Request) (Request, error)
CloneFromRequestLog(ctx context.Context, reqLogID ulid.ULID) (Request, error)
DeleteRequests(ctx context.Context, projectID ulid.ULID) error
SendRequest(ctx context.Context, id ulid.ULID) (Request, error)
SetActiveProjectID(ulid.ULID)
SetFindReqsFilter(filter FindRequestsFilter)
FindReqsFilter() FindRequestsFilter
}
type service struct {
activeProjectID ulid.ULID
findReqsFilter FindRequestsFilter
scope *scope.Scope
repo Repository
reqLogSvc *reqlog.Service
reqLogSvc reqlog.Service
httpClient *http.Client
}
type FindRequestsFilter struct {
ProjectID ulid.ULID
OnlyInScope bool
SearchExpr filter.Expression
}
type Config struct {
Scope *scope.Scope
Repository Repository
ReqLogService *reqlog.Service
ReqLogService reqlog.Service
HTTPClient *http.Client
}
@ -48,8 +68,8 @@ type SendError struct {
err error
}
func NewService(cfg Config) *Service {
svc := &Service{
func NewService(cfg Config) Service {
svc := &service{
repo: cfg.Repository,
reqLogSvc: cfg.ReqLogService,
httpClient: defaultHTTPClient,
@ -63,215 +83,163 @@ func NewService(cfg Config) *Service {
return svc
}
func (svc *Service) GetRequestByID(ctx context.Context, req *connect.Request[GetRequestByIDRequest]) (*connect.Response[GetRequestByIDResponse], error) {
if svc.activeProjectID == "" {
return nil, connect.NewError(connect.CodeFailedPrecondition, ErrProjectIDMustBeSet)
}
type Request struct {
ID ulid.ULID
ProjectID ulid.ULID
SourceRequestLogID ulid.ULID
senderReq, err := svc.repo.FindSenderRequestByID(ctx, svc.activeProjectID, req.Msg.RequestId)
if err != nil {
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("sender: failed to find request: %w", err))
}
URL *url.URL
Method string
Proto string
Header http.Header
Body []byte
return &connect.Response[GetRequestByIDResponse]{
Msg: &GetRequestByIDResponse{Request: senderReq},
}, nil
Response *reqlog.ResponseLog
}
func (svc *Service) ListRequests(ctx context.Context, req *connect.Request[ListRequestsRequest]) (*connect.Response[ListRequestsResponse], error) {
reqs, err := svc.repo.FindSenderRequests(ctx, svc.activeProjectID, svc.filterRequest)
func (svc *service) FindRequestByID(ctx context.Context, id ulid.ULID) (Request, error) {
req, err := svc.repo.FindSenderRequestByID(ctx, id)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("sender: failed to find requests: %w", err))
return Request{}, fmt.Errorf("sender: failed to find request: %w", err)
}
return &connect.Response[ListRequestsResponse]{
Msg: &ListRequestsResponse{Requests: reqs},
}, nil
return req, nil
}
func (svc *Service) filterRequest(req *Request) (bool, error) {
if svc.reqsFilter.OnlyInScope {
if svc.scope != nil && !req.MatchScope(svc.scope) {
return false, nil
}
}
if svc.reqsFilter.SearchExpr == "" {
return true, nil
}
expr, err := filter.ParseQuery(svc.reqsFilter.SearchExpr)
if err != nil {
return false, fmt.Errorf("failed to parse search expression: %w", err)
}
match, err := req.Matches(expr)
if err != nil {
return false, fmt.Errorf("failed to match search expression for sender request (id: %v): %w",
req.Id, err,
)
}
return match, nil
func (svc *service) FindRequests(ctx context.Context) ([]Request, error) {
return svc.repo.FindSenderRequests(ctx, svc.findReqsFilter, svc.scope)
}
func (svc *Service) CreateOrUpdateRequest(ctx context.Context, req *connect.Request[CreateOrUpdateRequestRequest]) (*connect.Response[CreateOrUpdateRequestResponse], error) {
if svc.activeProjectID == "" {
return nil, connect.NewError(connect.CodeFailedPrecondition, ErrProjectIDMustBeSet)
func (svc *service) CreateOrUpdateRequest(ctx context.Context, req Request) (Request, error) {
if svc.activeProjectID.Compare(ulid.ULID{}) == 0 {
return Request{}, ErrProjectIDMustBeSet
}
r := proto.Clone(req.Msg.Request).(*Request)
if r == nil {
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("sender: request is nil"))
if req.ID.Compare(ulid.ULID{}) == 0 {
req.ID = ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
}
if r.HttpRequest == nil {
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("sender: request.http_request is nil"))
req.ProjectID = svc.activeProjectID
if req.Method == "" {
req.Method = http.MethodGet
}
if r.Id == "" {
r.Id = ulid.Make().String()
if req.Proto == "" {
req.Proto = HTTPProto20
}
r.ProjectId = svc.activeProjectID
if r.HttpRequest.Method == httppb.Method_METHOD_UNSPECIFIED {
r.HttpRequest.Method = httppb.Method_METHOD_GET
if !isValidProto(req.Proto) {
return Request{}, fmt.Errorf("sender: unsupported HTTP protocol: %v", req.Proto)
}
if r.HttpRequest.Protocol == httppb.Protocol_PROTOCOL_UNSPECIFIED {
r.HttpRequest.Protocol = httppb.Protocol_PROTOCOL_HTTP20
}
err := svc.repo.StoreSenderRequest(ctx, r)
err := svc.repo.StoreSenderRequest(ctx, req)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("sender: failed to store request: %w", err))
return Request{}, fmt.Errorf("sender: failed to store request: %w", err)
}
return &connect.Response[CreateOrUpdateRequestResponse]{
Msg: &CreateOrUpdateRequestResponse{
Request: r,
},
}, nil
return req, nil
}
func (svc *Service) CloneFromRequestLog(ctx context.Context, req *connect.Request[CloneFromRequestLogRequest]) (*connect.Response[CloneFromRequestLogResponse], error) {
if svc.activeProjectID == "" {
return nil, connect.NewError(connect.CodeFailedPrecondition, ErrProjectIDMustBeSet)
func (svc *service) CloneFromRequestLog(ctx context.Context, reqLogID ulid.ULID) (Request, error) {
if svc.activeProjectID.Compare(ulid.ULID{}) == 0 {
return Request{}, ErrProjectIDMustBeSet
}
reqLog, err := svc.reqLogSvc.FindRequestLogByID(ctx, req.Msg.RequestLogId)
reqLog, err := svc.reqLogSvc.FindRequestLogByID(ctx, reqLogID)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("sender: failed to find request log: %w", err))
return Request{}, fmt.Errorf("sender: failed to find request log: %w", err)
}
clonedReqLog := proto.Clone(reqLog).(*reqlog.HttpRequestLog)
senderReq := &Request{
Id: ulid.Make().String(),
ProjectId: svc.activeProjectID,
SourceRequestLogId: clonedReqLog.Id,
HttpRequest: clonedReqLog.Request,
req := Request{
ID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
ProjectID: svc.activeProjectID,
SourceRequestLogID: reqLogID,
Method: reqLog.Method,
URL: reqLog.URL,
Proto: HTTPProto20, // Attempt HTTP/2.
Header: reqLog.Header,
Body: reqLog.Body,
}
err = svc.repo.StoreSenderRequest(ctx, senderReq)
err = svc.repo.StoreSenderRequest(ctx, req)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("sender: failed to store request: %w", err))
return Request{}, fmt.Errorf("sender: failed to store request: %w", err)
}
return &connect.Response[CloneFromRequestLogResponse]{Msg: &CloneFromRequestLogResponse{
Request: senderReq,
}}, nil
return req, nil
}
func (svc *Service) SetRequestsFilter(filter *RequestsFilter) {
svc.reqsFilter = filter
func (svc *service) SetFindReqsFilter(filter FindRequestsFilter) {
svc.findReqsFilter = filter
}
func (svc *Service) RequestsFilter() *RequestsFilter {
return svc.reqsFilter
func (svc *service) FindReqsFilter() FindRequestsFilter {
return svc.findReqsFilter
}
func (svc *Service) SendRequest(ctx context.Context, connReq *connect.Request[SendRequestRequest]) (*connect.Response[SendRequestResponse], error) {
req, err := svc.repo.FindSenderRequestByID(ctx, svc.activeProjectID, connReq.Msg.RequestId)
func (svc *service) SendRequest(ctx context.Context, id ulid.ULID) (Request, error) {
req, err := svc.repo.FindSenderRequestByID(ctx, id)
if err != nil {
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("sender: failed to find request: %w", err))
return Request{}, fmt.Errorf("sender: failed to find request: %w", err)
}
httpReq, err := parseHTTPRequest(ctx, req)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("sender: failed to parse HTTP request: %w", err))
return Request{}, fmt.Errorf("sender: failed to parse HTTP request: %w", err)
}
httpRes, err := svc.sendHTTPRequest(httpReq)
resLog, err := svc.sendHTTPRequest(httpReq)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("sender: could not send HTTP request: %w", err))
return Request{}, fmt.Errorf("sender: could not send HTTP request: %w", err)
}
req.HttpResponse = httpRes
err = svc.repo.StoreSenderRequest(ctx, req)
err = svc.repo.StoreResponseLog(ctx, id, resLog)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("sender: failed to store sender response log: %w", err))
return Request{}, fmt.Errorf("sender: failed to store sender response log: %w", err)
}
return &connect.Response[SendRequestResponse]{
Msg: &SendRequestResponse{
Request: req,
},
}, nil
req.Response = &resLog
return req, nil
}
func parseHTTPRequest(ctx context.Context, req *Request) (*http.Request, error) {
ctx = context.WithValue(ctx, protoCtxKey{}, req.GetHttpRequest().GetProtocol())
func parseHTTPRequest(ctx context.Context, req Request) (*http.Request, error) {
ctx = context.WithValue(ctx, protoCtxKey{}, req.Proto)
httpReq, err := http.NewRequestWithContext(ctx,
req.GetHttpRequest().GetMethod().String(),
req.GetHttpRequest().GetUrl(),
bytes.NewReader(req.GetHttpRequest().GetBody()),
)
httpReq, err := http.NewRequestWithContext(ctx, req.Method, req.URL.String(), bytes.NewReader(req.Body))
if err != nil {
return nil, fmt.Errorf("failed to construct HTTP request: %w", err)
}
for _, header := range req.GetHttpRequest().GetHeaders() {
httpReq.Header.Add(header.Key, header.Value)
if req.Header != nil {
httpReq.Header = req.Header
}
return httpReq, nil
}
func (svc *Service) sendHTTPRequest(httpReq *http.Request) (*httppb.Response, error) {
func (svc *service) sendHTTPRequest(httpReq *http.Request) (reqlog.ResponseLog, error) {
res, err := svc.httpClient.Do(httpReq)
if err != nil {
return nil, &SendError{err}
return reqlog.ResponseLog{}, &SendError{err}
}
defer res.Body.Close()
resLog, err := httppb.ParseHTTPResponse(res)
resLog, err := reqlog.ParseHTTPResponse(res)
if err != nil {
return nil, fmt.Errorf("failed to parse http response: %w", err)
return reqlog.ResponseLog{}, fmt.Errorf("failed to parse http response: %w", err)
}
return resLog, err
}
func (svc *Service) SetActiveProjectID(id string) {
func (svc *service) SetActiveProjectID(id ulid.ULID) {
svc.activeProjectID = id
}
func (svc *Service) DeleteRequests(ctx context.Context, req *connect.Request[DeleteRequestsRequest]) (*connect.Response[DeleteRequestsResponse], error) {
if svc.activeProjectID == "" {
return nil, connect.NewError(connect.CodeFailedPrecondition, ErrProjectIDMustBeSet)
}
err := svc.repo.DeleteSenderRequests(ctx, svc.activeProjectID)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("sender: failed to delete requests: %w", err))
}
return &connect.Response[DeleteRequestsResponse]{}, nil
func (svc *service) DeleteRequests(ctx context.Context, projectID ulid.ULID) error {
return svc.repo.DeleteSenderRequests(ctx, projectID)
}
func (e SendError) Error() string {

File diff suppressed because it is too large Load Diff

View File

@ -1,27 +1,38 @@
package sender_test
//go:generate go run github.com/matryer/moq -out reqlog_mock_test.go -pkg sender_test ../reqlog Service:ReqLogServiceMock
//go:generate go run github.com/matryer/moq -out repo_mock_test.go -pkg sender_test . Repository:RepoMock
import (
"context"
"errors"
"fmt"
http "net/http"
"math/rand"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"
connect "connectrpc.com/connect"
"go.etcd.io/bbolt"
"google.golang.org/protobuf/testing/protocmp"
"github.com/google/go-cmp/cmp"
"github.com/oklog/ulid"
"github.com/dstotijn/hetty/pkg/db/bolt"
httppb "github.com/dstotijn/hetty/pkg/http"
"github.com/dstotijn/hetty/pkg/proj"
"github.com/dstotijn/hetty/pkg/reqlog"
"github.com/dstotijn/hetty/pkg/sender"
"github.com/dstotijn/hetty/pkg/testutil"
"github.com/google/go-cmp/cmp"
)
//nolint:gosec
var ulidEntropy = rand.New(rand.NewSource(time.Now().UnixNano()))
var exampleURL = func() *url.URL {
u, err := url.Parse("https://example.com/foobar")
if err != nil {
panic(err)
}
return u
}()
func TestStoreRequest(t *testing.T) {
t.Parallel()
@ -30,16 +41,10 @@ func TestStoreRequest(t *testing.T) {
svc := sender.NewService(sender.Config{})
_, err := svc.CreateOrUpdateRequest(context.Background(), &connect.Request[sender.CreateOrUpdateRequestRequest]{
Msg: &sender.CreateOrUpdateRequestRequest{
Request: &sender.Request{
HttpRequest: &httppb.Request{
Url: "https://example.com/foobar",
Method: httppb.Method_METHOD_POST,
Body: []byte("foobar"),
},
},
},
_, err := svc.CreateOrUpdateRequest(context.Background(), sender.Request{
URL: exampleURL,
Method: http.MethodPost,
Body: []byte("foobar"),
})
if !errors.Is(err, sender.ErrProjectIDMustBeSet) {
t.Fatalf("expected `sender.ErrProjectIDMustBeSet`, got: %v", err)
@ -49,86 +54,74 @@ func TestStoreRequest(t *testing.T) {
t.Run("with active project", func(t *testing.T) {
t.Parallel()
path := t.TempDir() + "bolt.db"
boltDB, err := bbolt.Open(path, 0o600, nil)
if err != nil {
t.Fatalf("failed to open bolt database: %v", err)
repoMock := &RepoMock{
StoreSenderRequestFunc: func(ctx context.Context, req sender.Request) error {
return nil
},
}
defer boltDB.Close()
db, err := bolt.DatabaseFromBoltDB(boltDB)
if err != nil {
t.Fatalf("failed to create database: %v", err)
}
defer db.Close()
svc := sender.NewService(sender.Config{
Repository: db,
Repository: repoMock,
})
projectID := "foobar-project-id"
err = db.UpsertProject(context.Background(), &proj.Project{
Id: projectID,
Name: "foobar",
})
if err != nil {
t.Fatalf("unexpected error upserting project: %v", err)
}
projectID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
svc.SetActiveProjectID(projectID)
exp := &sender.Request{
ProjectId: projectID,
HttpRequest: &httppb.Request{
Method: httppb.Method_METHOD_POST,
Protocol: httppb.Protocol_PROTOCOL_HTTP20,
Url: "https://example.com/foobar",
Headers: []*httppb.Header{
{Key: "X-Foo", Value: "bar"},
},
Body: []byte("foobar"),
exp := sender.Request{
ProjectID: projectID,
URL: exampleURL,
Method: http.MethodPost,
Proto: "HTTP/1.1",
Header: http.Header{
"X-Foo": []string{"bar"},
},
Body: []byte("foobar"),
}
createRes, err := svc.CreateOrUpdateRequest(context.Background(), &connect.Request[sender.CreateOrUpdateRequestRequest]{
Msg: &sender.CreateOrUpdateRequestRequest{
Request: exp,
got, err := svc.CreateOrUpdateRequest(context.Background(), sender.Request{
URL: exampleURL,
Method: http.MethodPost,
Proto: "HTTP/1.1",
Header: http.Header{
"X-Foo": []string{"bar"},
},
Body: []byte("foobar"),
})
if err != nil {
t.Fatalf("unexpected error storing request: %v", err)
}
if createRes.Msg.Request.Id == "" {
if got.ID.Compare(ulid.ULID{}) == 0 {
t.Fatal("expected request ID to be non-empty value")
}
testutil.ProtoDiff(t, "request not equal", exp, createRes.Msg.Request, "id")
got, err := db.FindSenderRequestByID(context.Background(), projectID, createRes.Msg.Request.Id)
if err != nil {
t.Fatalf("failed to find request by ID: %v", err)
if len(repoMock.StoreSenderRequestCalls()) != 1 {
t.Fatal("expected `svc.repo.StoreSenderRequest()` to have been called 1 time")
}
testutil.ProtoDiff(t, "request not equal", exp, got, "id")
if diff := cmp.Diff(got, repoMock.StoreSenderRequestCalls()[0].Req); diff != "" {
t.Fatalf("repo call arg not equal (-exp, +got):\n%v", diff)
}
// Reset ID to make comparison with expected easier.
got.ID = ulid.ULID{}
if diff := cmp.Diff(exp, got); diff != "" {
t.Fatalf("request not equal (-exp, +got):\n%v", diff)
}
})
}
func TestCloneFromRequestLog(t *testing.T) {
t.Parallel()
reqLogID := "foobar-req-log-id"
reqLogID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
t.Run("without active project", func(t *testing.T) {
t.Parallel()
svc := sender.NewService(sender.Config{})
_, err := svc.CloneFromRequestLog(context.Background(), &connect.Request[sender.CloneFromRequestLogRequest]{
Msg: &sender.CloneFromRequestLogRequest{
RequestLogId: reqLogID,
},
})
_, err := svc.CloneFromRequestLog(context.Background(), reqLogID)
if !errors.Is(err, sender.ErrProjectIDMustBeSet) {
t.Fatalf("expected `sender.ErrProjectIDMustBeSet`, got: %v", err)
}
@ -137,103 +130,85 @@ func TestCloneFromRequestLog(t *testing.T) {
t.Run("with active project", func(t *testing.T) {
t.Parallel()
path := t.TempDir() + "bolt.db"
boltDB, err := bbolt.Open(path, 0o600, nil)
if err != nil {
t.Fatalf("failed to open bolt database: %v", err)
}
defer boltDB.Close()
db, err := bolt.DatabaseFromBoltDB(boltDB)
if err != nil {
t.Fatalf("failed to create database: %v", err)
}
defer db.Close()
projectID := "foobar-project-id"
err = db.UpsertProject(context.Background(), &proj.Project{
Id: projectID,
Name: "foobar",
})
if err != nil {
t.Fatalf("unexpected error upserting project: %v", err)
}
reqLog := &reqlog.HttpRequestLog{
Id: reqLogID,
ProjectId: projectID,
Request: &httppb.Request{
Url: "https://example.com/foobar",
Method: httppb.Method_METHOD_POST,
Body: []byte("foobar"),
Headers: []*httppb.Header{
{Key: "X-Foo", Value: "bar"},
},
reqLog := reqlog.RequestLog{
ID: reqLogID,
ProjectID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
URL: exampleURL,
Method: http.MethodPost,
Proto: "HTTP/1.1",
Header: http.Header{
"X-Foo": []string{"bar"},
},
Response: &httppb.Response{
Protocol: httppb.Protocol_PROTOCOL_HTTP20,
StatusCode: 200,
Status: "200 OK",
Body: []byte("foobar"),
Body: []byte("foobar"),
}
reqLogMock := &ReqLogServiceMock{
FindRequestLogByIDFunc: func(ctx context.Context, id ulid.ULID) (reqlog.RequestLog, error) {
return reqLog, nil
},
}
if err := db.StoreRequestLog(context.Background(), reqLog); err != nil {
t.Fatalf("failed to store request log: %v", err)
repoMock := &RepoMock{
StoreSenderRequestFunc: func(ctx context.Context, req sender.Request) error {
return nil
},
}
svc := sender.NewService(sender.Config{
ReqLogService: reqlog.NewService(reqlog.Config{
ActiveProjectID: projectID,
Repository: db,
}),
Repository: db,
ReqLogService: reqLogMock,
Repository: repoMock,
})
projectID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
svc.SetActiveProjectID(projectID)
exp := &sender.Request{
SourceRequestLogId: reqLogID,
ProjectId: projectID,
HttpRequest: &httppb.Request{
Url: "https://example.com/foobar",
Method: httppb.Method_METHOD_POST,
Body: []byte("foobar"),
Headers: []*httppb.Header{
{Key: "X-Foo", Value: "bar"},
},
exp := sender.Request{
SourceRequestLogID: reqLogID,
ProjectID: projectID,
URL: exampleURL,
Method: http.MethodPost,
Proto: sender.HTTPProto20,
Header: http.Header{
"X-Foo": []string{"bar"},
},
Body: []byte("foobar"),
}
got, err := svc.CloneFromRequestLog(context.Background(), &connect.Request[sender.CloneFromRequestLogRequest]{
Msg: &sender.CloneFromRequestLogRequest{
RequestLogId: reqLogID,
},
})
got, err := svc.CloneFromRequestLog(context.Background(), reqLogID)
if err != nil {
t.Fatalf("unexpected error cloning from request log: %v", err)
}
testutil.ProtoDiff(t, "request not equal", exp, got.Msg.Request, "id")
if len(reqLogMock.FindRequestLogByIDCalls()) != 1 {
t.Fatal("expected `svc.reqLogSvc.FindRequestLogByID()` to have been called 1 time")
}
if got := reqLogMock.FindRequestLogByIDCalls()[0].ID; reqLogID.Compare(got) != 0 {
t.Fatalf("reqlog service call arg `id` not equal (expected: %q, got: %q)", reqLogID, got)
}
if got.ID.Compare(ulid.ULID{}) == 0 {
t.Fatal("expected request ID to be non-empty value")
}
if len(repoMock.StoreSenderRequestCalls()) != 1 {
t.Fatal("expected `svc.repo.StoreSenderRequest()` to have been called 1 time")
}
if diff := cmp.Diff(got, repoMock.StoreSenderRequestCalls()[0].Req); diff != "" {
t.Fatalf("repo call arg not equal (-exp, +got):\n%v", diff)
}
// Reset ID to make comparison with expected easier.
got.ID = ulid.ULID{}
if diff := cmp.Diff(exp, got); diff != "" {
t.Fatalf("request not equal (-exp, +got):\n%v", diff)
}
})
}
func TestSendRequest(t *testing.T) {
t.Parallel()
path := t.TempDir() + "bolt.db"
boltDB, err := bbolt.Open(path, 0o600, nil)
if err != nil {
t.Fatalf("failed to open bolt database: %v", err)
}
defer boltDB.Close()
db, err := bolt.DatabaseFromBoltDB(boltDB)
if err != nil {
t.Fatalf("failed to create database: %v", err)
}
defer db.Close()
date := time.Now().Format(http.TimeFormat)
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@ -243,73 +218,72 @@ func TestSendRequest(t *testing.T) {
}))
defer ts.Close()
projectID := "foobar-project-id"
err = db.UpsertProject(context.Background(), &proj.Project{
Id: projectID,
Name: "foobar",
})
if err != nil {
t.Fatalf("unexpected error upserting project: %v", err)
tsURL, _ := url.Parse(ts.URL)
reqID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
req := sender.Request{
ID: reqID,
ProjectID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
URL: tsURL,
Method: http.MethodPost,
Proto: "HTTP/1.1",
Header: http.Header{
"X-Foo": []string{"bar"},
},
Body: []byte("foobar"),
}
reqID := "foobar-req-id"
req := &sender.Request{
Id: reqID,
ProjectId: projectID,
HttpRequest: &httppb.Request{
Url: ts.URL,
Method: httppb.Method_METHOD_POST,
Body: []byte("foobar"),
Headers: []*httppb.Header{
{Key: "X-Foo", Value: "bar"},
},
repoMock := &RepoMock{
FindSenderRequestByIDFunc: func(ctx context.Context, id ulid.ULID) (sender.Request, error) {
return req, nil
},
StoreResponseLogFunc: func(ctx context.Context, reqLogID ulid.ULID, resLog reqlog.ResponseLog) error {
return nil
},
}
if err := db.StoreSenderRequest(context.Background(), req); err != nil {
t.Fatalf("failed to store request: %v", err)
}
svc := sender.NewService(sender.Config{
ReqLogService: reqlog.NewService(reqlog.Config{
Repository: db,
}),
Repository: db,
Repository: repoMock,
})
svc.SetActiveProjectID(projectID)
exp := &httppb.Response{
Protocol: httppb.Protocol_PROTOCOL_HTTP11,
StatusCode: 200,
exp := reqlog.ResponseLog{
Proto: "HTTP/1.1",
StatusCode: http.StatusOK,
Status: "200 OK",
Headers: []*httppb.Header{
{Key: "Date", Value: date},
{Key: "Foobar", Value: "baz"},
{Key: "Content-Length", Value: "3"},
{Key: "Content-Type", Value: "text/plain; charset=utf-8"},
Header: http.Header{
"Content-Length": []string{"3"},
"Content-Type": []string{"text/plain; charset=utf-8"},
"Date": []string{date},
"Foobar": []string{"baz"},
},
Body: []byte("baz"),
}
got, err := svc.SendRequest(context.Background(), &connect.Request[sender.SendRequestRequest]{
Msg: &sender.SendRequestRequest{
RequestId: reqID,
},
})
got, err := svc.SendRequest(context.Background(), reqID)
if err != nil {
t.Fatalf("unexpected error sending request: %v", err)
}
opts := []cmp.Option{
protocmp.Transform(),
protocmp.SortRepeated(func(a, b *httppb.Header) bool {
if a.Key != b.Key {
return a.Key < b.Key
}
return a.Value < b.Value
}),
if len(repoMock.FindSenderRequestByIDCalls()) != 1 {
t.Fatal("expected `svc.repo.FindSenderRequestByID()` to have been called 1 time")
}
if diff := cmp.Diff(exp, got.Msg.Request.HttpResponse, opts...); diff != "" {
t.Fatalf("response not equal (-exp, +got):\n%v", diff)
if diff := cmp.Diff(reqID, repoMock.FindSenderRequestByIDCalls()[0].ID); diff != "" {
t.Fatalf("call arg `id` for `svc.repo.FindSenderRequestByID()` not equal (-exp, +got):\n%v", diff)
}
if len(repoMock.StoreResponseLogCalls()) != 1 {
t.Fatal("expected `svc.repo.StoreResponseLog()` to have been called 1 time")
}
if diff := cmp.Diff(reqID, repoMock.StoreResponseLogCalls()[0].ReqLogID); diff != "" {
t.Fatalf("call arg `reqLogID` for `svc.repo.StoreResponseLog()` not equal (-exp, +got):\n%v", diff)
}
if diff := cmp.Diff(exp, repoMock.StoreResponseLogCalls()[0].ResLog); diff != "" {
t.Fatalf("call arg `resLog` for `svc.repo.StoreResponseLog()` not equal (-exp, +got):\n%v", diff)
}
if diff := cmp.Diff(repoMock.StoreResponseLogCalls()[0].ResLog, *got.Response); diff != "" {
t.Fatalf("returned response log value and persisted value not equal (-exp, +got):\n%v", diff)
}
}

View File

@ -45,3 +45,7 @@ func (t *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) {
return http.DefaultTransport.RoundTrip(req)
}
func isValidProto(proto string) bool {
return proto == HTTPProto10 || proto == HTTPProto11 || proto == HTTPProto20
}

View File

@ -1,54 +0,0 @@
package testutil
import (
"testing"
"github.com/dstotijn/hetty/pkg/log"
"github.com/google/go-cmp/cmp"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/testing/protocmp"
)
func ProtoDiff[M proto.Message](t *testing.T, msg string, exp, got M, ignoreFields ...protoreflect.Name) {
t.Helper()
opts := []cmp.Option{
protocmp.Transform(),
protocmp.IgnoreFields(exp, ignoreFields...),
}
if diff := cmp.Diff(exp, got, opts...); diff != "" {
t.Fatalf("%v (-exp, +got):\n%v", msg, diff)
}
}
func ProtoSlicesDiff[M proto.Message](t *testing.T, msg string, exp, got []M, ignoreFields ...protoreflect.Name) {
t.Helper()
opts := []cmp.Option{
protocmp.Transform(),
}
if len(exp) > 0 {
opts = append(opts, protocmp.IgnoreFields(exp[0], ignoreFields...))
}
if diff := cmp.Diff(exp, got, opts...); diff != "" {
t.Fatalf("%v (-exp, +got):\n%v", msg, diff)
}
}
type testLogger struct {
log.NopLogger
t *testing.T
}
func (l *testLogger) Errorw(msg string, v ...interface{}) {
l.t.Helper()
l.t.Fatalf(msg+": %v", v...)
}
func NewLogger(t *testing.T) log.Logger {
t.Helper()
return &testLogger{t: t}
}

View File

@ -1,47 +0,0 @@
syntax = "proto3";
package hetty.http.v1;
option go_package = "github.com/dstotijn/hetty/pkg/http";
enum Method {
METHOD_UNSPECIFIED = 0;
METHOD_GET = 1;
METHOD_HEAD = 2;
METHOD_POST = 3;
METHOD_PUT = 4;
METHOD_DELETE = 5;
METHOD_CONNECT = 6;
METHOD_OPTIONS = 7;
METHOD_TRACE = 8;
METHOD_PATCH = 9;
}
enum Protocol {
PROTOCOL_UNSPECIFIED = 0;
PROTOCOL_HTTP10 = 1;
PROTOCOL_HTTP11 = 2;
PROTOCOL_HTTP20 = 3;
}
message Request {
Method method = 1;
Protocol protocol = 2;
string url = 3;
repeated Header headers = 4;
bytes body = 5;
Response response = 6;
}
message Response {
Protocol protocol = 1;
string status = 2;
int32 status_code = 3;
repeated Header headers = 5;
bytes body = 6;
}
message Header {
string key = 1;
string value = 2;
}

View File

@ -1,104 +0,0 @@
syntax = "proto3";
package hetty.proj.v1;
import "reqlog/reqlog.proto";
import "scope/scope.proto";
option go_package = "github.com/dstotijn/hetty/pkg/proj";
message Project {
string id = 1;
string name = 2;
bool is_active = 3;
// Request log settings
bool req_log_bypass_out_of_scope = 4;
// Request logs filter
hetty.reqlog.v1.RequestLogsFilter req_log_filter = 5;
// Intercept settings
bool intercept_requests = 6;
bool intercept_responses = 7;
string intercept_request_filter_expr = 8;
string intercept_response_filter_expr = 9;
// Sender settings
bool sender_only_find_in_scope = 10;
string sender_search_expr = 11;
// Scope settings
repeated hetty.scope.v1.ScopeRule scope_rules = 12;
}
message CreateProjectRequest {
string name = 1;
}
message CreateProjectResponse {
Project project = 1;
}
message OpenProjectRequest {
string project_id = 1;
}
message OpenProjectResponse {
Project project = 1;
}
message CloseProjectRequest {}
message CloseProjectResponse {}
message DeleteProjectRequest {
string project_id = 1;
}
message DeleteProjectResponse {}
message GetActiveProjectRequest {}
message GetActiveProjectResponse {
Project project = 1;
}
message ListProjectsRequest {}
message ListProjectsResponse {
repeated Project projects = 1;
}
message UpdateInterceptSettingsRequest {
bool requests_enabled = 1;
bool responses_enabled = 2;
string request_filter_expr = 3;
string response_filter_expr = 4;
}
message UpdateInterceptSettingsResponse {}
message SetScopeRulesRequest {
repeated hetty.scope.v1.ScopeRule rules = 1;
}
message SetScopeRulesResponse {}
message SetRequestLogsFilterRequest {
hetty.reqlog.v1.RequestLogsFilter filter = 1;
}
message SetRequestLogsFilterResponse {}
service ProjectService {
rpc CreateProject(CreateProjectRequest) returns (CreateProjectResponse) {}
rpc OpenProject(OpenProjectRequest) returns (OpenProjectResponse) {}
rpc CloseProject(CloseProjectRequest) returns (CloseProjectResponse) {}
rpc DeleteProject(DeleteProjectRequest) returns (DeleteProjectResponse) {}
rpc GetActiveProject(GetActiveProjectRequest) returns (GetActiveProjectResponse) {}
rpc ListProjects(ListProjectsRequest) returns (ListProjectsResponse) {}
rpc UpdateInterceptSettings(UpdateInterceptSettingsRequest) returns (UpdateInterceptSettingsResponse) {}
rpc SetScopeRules(SetScopeRulesRequest) returns (SetScopeRulesResponse) {}
rpc SetRequestLogsFilter(SetRequestLogsFilterRequest) returns (SetRequestLogsFilterResponse) {}
}

View File

@ -1,44 +0,0 @@
syntax = "proto3";
package hetty.reqlog.v1;
import "http/http.proto";
option go_package = "github.com/dstotijn/hetty/pkg/reqlog";
message HttpRequestLog {
string id = 1;
string project_id = 2;
string remote_ip = 3;
hetty.http.v1.Request request = 4;
hetty.http.v1.Response response = 5;
}
message GetHttpRequestLogRequest {
string id = 1;
}
message GetHttpRequestLogResponse {
HttpRequestLog http_request_log = 1;
}
message ListHttpRequestLogsRequest {}
message ListHttpRequestLogsResponse {
repeated HttpRequestLog http_request_logs = 1;
}
message RequestLogsFilter {
bool only_in_scope = 1;
string search_expr = 2;
}
message ClearHttpRequestLogsRequest {}
message ClearHttpRequestLogsResponse {}
service HttpRequestLogService {
rpc GetHttpRequestLog(GetHttpRequestLogRequest) returns (GetHttpRequestLogResponse) {}
rpc ListHttpRequestLogs(ListHttpRequestLogsRequest) returns (ListHttpRequestLogsResponse) {}
rpc ClearHttpRequestLogs(ClearHttpRequestLogsRequest) returns (ClearHttpRequestLogsResponse) {}
}

View File

@ -1,12 +0,0 @@
syntax = "proto3";
package hetty.scope.v1;
option go_package = "github.com/dstotijn/hetty/pkg/scope";
message ScopeRule {
string url_regexp = 1;
string header_key_regexp = 2;
string header_value_regexp = 3;
string body_regexp = 4;
}

View File

@ -1,85 +0,0 @@
syntax = "proto3";
package sender;
import "http/http.proto";
option go_package = "github.com/dstotijn/hetty/proto/sender";
message Request {
string id = 1;
string project_id = 2;
string source_request_log_id = 3;
hetty.http.v1.Request http_request = 4;
hetty.http.v1.Response http_response = 10;
}
message RequestsFilter {
bool only_in_scope = 1;
string search_expr = 2;
}
message GetRequestByIDRequest {
string request_id = 1;
}
message GetRequestByIDResponse {
Request request = 1;
}
message ListRequestsRequest {}
message ListRequestsResponse {
repeated Request requests = 1;
}
message CloneFromRequestLogRequest {
string request_log_id = 1;
}
message CloneFromRequestLogResponse {
Request request = 1;
}
message SendRequestRequest {
string request_id = 1;
}
message SendRequestResponse {
Request request = 1;
}
message DeleteRequestsRequest {}
message DeleteRequestsResponse {}
message CreateOrUpdateRequestRequest {
Request request = 1;
}
message CreateOrUpdateRequestResponse {
Request request = 1;
}
message SetRequestsFilterRequest {
RequestsFilter filter = 1;
}
message SetRequestsFilterResponse {}
message GetRequestsFilterRequest {}
message GetRequestsFilterResponse {
RequestsFilter filter = 1;
}
service SenderService {
rpc GetRequestByID(GetRequestByIDRequest) returns (GetRequestByIDResponse) {}
rpc ListRequests(ListRequestsRequest) returns (ListRequestsResponse) {}
rpc SetRequestsFilter(SetRequestsFilterRequest) returns (SetRequestsFilterResponse) {}
rpc GetRequestsFilter(GetRequestsFilterRequest) returns (GetRequestsFilterResponse) {}
rpc CreateOrUpdateRequest(CreateOrUpdateRequestRequest) returns (CreateOrUpdateRequestResponse) {}
rpc CloneFromRequestLog(CloneFromRequestLogRequest) returns (CloneFromRequestLogResponse) {}
rpc SendRequest(SendRequestRequest) returns (SendRequestResponse) {}
rpc DeleteRequests(DeleteRequestsRequest) returns (DeleteRequestsResponse) {}
}

9
tools.go Normal file
View File

@ -0,0 +1,9 @@
//go:build tools
// +build tools
package tools
import (
_ "github.com/99designs/gqlgen"
_ "github.com/matryer/moq"
)