Replace GraphQL server with Connect RPC

This commit is contained in:
David Stotijn
2025-02-05 21:54:59 +01:00
parent 52c83a1989
commit 6889c9c183
53 changed files with 5875 additions and 11685 deletions

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/99designs/gqlgen/graphql.Marshaler,github.com/dstotijn/hetty/pkg/api.QueryResolver,github.com/dstotijn/hetty/pkg/filter.Expression"
allow: "error,empty,anon,stdlib,.*(or|er)$,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,7 +96,7 @@ $ hetty --help
Usage:
hetty [flags] [subcommand] [flags]
Runs an HTTP server with (MITM) proxy, GraphQL service, and a web based admin interface.
Runs an HTTP server with (MITM) proxy 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")

10
buf.gen.yaml Normal file
View File

@ -0,0 +1,10 @@
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.

12
buf.yaml Normal file
View File

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

View File

@ -22,7 +22,6 @@ import (
"go.etcd.io/bbolt"
"go.uber.org/zap"
"github.com/dstotijn/hetty/pkg/api"
"github.com/dstotijn/hetty/pkg/chrome"
"github.com/dstotijn/hetty/pkg/db/bolt"
"github.com/dstotijn/hetty/pkg/proj"
@ -45,7 +44,7 @@ var hettyUsage = `
Usage:
hetty [flags] [subcommand] [flags]
Runs an HTTP server with (MITM) proxy, GraphQL service, and a web based admin interface.
Runs an HTTP server with (MITM) proxy 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")
@ -229,14 +228,11 @@ func (cmd *HettyCommand) Exec(ctx context.Context, _ []string) error {
req.Method != http.MethodConnect && !strings.HasPrefix(req.RequestURI, "http://")
}).Subrouter().StrictSlash(true)
// GraphQL server.
gqlEndpoint := "/api/graphql/"
adminRouter.Path(gqlEndpoint).Handler(api.HTTPHandler(&api.Resolver{
ProjectService: projService,
RequestLogService: reqLogService,
InterceptService: interceptService,
SenderService: senderService,
}, gqlEndpoint))
// Connect RPC server.
projPath, projHandler := proj.NewProjectServiceHandler(projService)
adminRouter.PathPrefix(projPath).Handler(projHandler)
reqlogPath, reqlogHandler := reqlog.NewHttpRequestLogServiceHandler(reqLogService)
adminRouter.PathPrefix(reqlogPath).Handler(reqlogHandler)
// Admin interface.
adminRouter.PathPrefix("").Handler(adminHandler)

22
go.mod
View File

@ -5,44 +5,30 @@ go 1.23
toolchain go1.23.4
require (
github.com/99designs/gqlgen v0.14.0
connectrpc.com/connect v1.18.1
github.com/chromedp/chromedp v0.7.8
github.com/google/go-cmp v0.5.6
github.com/google/go-cmp v0.5.9
github.com/gorilla/mux v1.7.4
github.com/mitchellh/go-homedir v1.1.0
github.com/oklog/ulid v1.3.1
github.com/oklog/ulid/v2 v2.1.0
github.com/peterbourgon/ff/v3 v3.1.2
github.com/smallstep/truststore v0.11.0
github.com/vektah/gqlparser/v2 v2.2.0
go.etcd.io/bbolt v1.4.0-beta.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/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/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.1.0 // 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/mailru/easyjson v0.7.7 // indirect
github.com/matryer/moq v0.2.5 // 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.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
golang.org/x/mod v0.4.2 // indirect
golang.org/x/sys v0.26.0 // 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
)

101
go.sum
View File

@ -1,13 +1,6 @@
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=
connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw=
connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
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/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/chromedp/cdproto v0.0.0-20220217222649-d8c14a5c6edf h1:1omDWNUsWxn2HpiMiMuyRmzjl9uG7RP3IE6GTlpgJWU=
@ -16,61 +9,34 @@ github.com/chromedp/chromedp v0.7.8 h1:JFPIFb28LPjcx6l6mUUzLOTD/TgswcTtg7KrDn8S/
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/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/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/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/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
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/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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/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/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/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/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/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU=
github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
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.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=
@ -79,33 +45,13 @@ 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/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/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.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/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/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/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=
@ -119,32 +65,20 @@ 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-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/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-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/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/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-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-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-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -155,27 +89,20 @@ 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-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
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=
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=
@ -185,5 +112,3 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
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=

View File

@ -1,56 +0,0 @@
# 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

File diff suppressed because it is too large Load Diff

View File

@ -1,21 +0,0 @@
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
}

View File

@ -1,65 +0,0 @@
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]
}

View File

@ -1,301 +0,0 @@
// 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()))
}

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,12 @@
package bolt
import (
"bytes"
"context"
"encoding/gob"
"errors"
"fmt"
"github.com/oklog/ulid"
bolt "go.etcd.io/bbolt"
"google.golang.org/protobuf/proto"
"github.com/dstotijn/hetty/pkg/proj"
)
@ -32,13 +30,13 @@ func projectsBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
return b, nil
}
func projectBucket(tx *bolt.Tx, projectID []byte) (*bolt.Bucket, error) {
func projectBucket(tx *bolt.Tx, projectID string) (*bolt.Bucket, error) {
pb, err := projectsBucket(tx)
if err != nil {
return nil, err
}
b := pb.Bucket(projectID[:])
b := pb.Bucket([]byte(projectID))
if b == nil {
return nil, ErrProjectBucketNotFound
}
@ -46,21 +44,19 @@ func projectBucket(tx *bolt.Tx, projectID []byte) (*bolt.Bucket, error) {
return b, nil
}
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("bolt: failed to encode project: %w", err)
}
err = db.bolt.Update(func(tx *bolt.Tx) error {
b, err := createNestedBucket(tx, projectsBucketName, project.ID[:])
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)
}
err = b.Put(projectKey, buf.Bytes())
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)
}
@ -84,9 +80,11 @@ func (db *Database) UpsertProject(ctx context.Context, project proj.Project) err
return nil
}
func (db *Database) FindProjectByID(ctx context.Context, projectID ulid.ULID) (project proj.Project, err error) {
err = db.bolt.View(func(tx *bolt.Tx) error {
bucket, err := projectBucket(tx, projectID[:])
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
}
@ -99,28 +97,28 @@ func (db *Database) FindProjectByID(ctx context.Context, projectID ulid.ULID) (p
return proj.ErrProjectNotFound
}
err = gob.NewDecoder(bytes.NewReader(rawProject)).Decode(&project)
err = proto.Unmarshal(rawProject, project)
if err != nil {
return fmt.Errorf("failed to decode project: %w", err)
return fmt.Errorf("failed to unmarshal project: %w", err)
}
return nil
})
if err != nil {
return proj.Project{}, fmt.Errorf("bolt: failed to commit transaction: %w", err)
return nil, fmt.Errorf("bolt: failed to commit transaction: %w", err)
}
return project, nil
}
func (db *Database) DeleteProject(ctx context.Context, projectID ulid.ULID) error {
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(projectID[:])
err = pb.DeleteBucket([]byte(projectID))
if err != nil {
return fmt.Errorf("failed to delete project bucket: %w", err)
}
@ -134,8 +132,8 @@ func (db *Database) DeleteProject(ctx context.Context, projectID ulid.ULID) erro
return nil
}
func (db *Database) Projects(ctx context.Context) ([]proj.Project, error) {
projects := make([]proj.Project, 0)
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)
@ -144,7 +142,7 @@ func (db *Database) Projects(ctx context.Context) ([]proj.Project, error) {
}
err = pb.ForEachBucket(func(projectID []byte) error {
bucket, err := projectBucket(tx, projectID)
bucket, err := projectBucket(tx, string(projectID))
if err != nil {
return err
}
@ -154,16 +152,16 @@ func (db *Database) Projects(ctx context.Context) ([]proj.Project, error) {
return proj.ErrProjectNotFound
}
var project proj.Project
err = gob.NewDecoder(bytes.NewReader(rawProject)).Decode(&project)
project := &proj.Project{}
err = proto.Unmarshal(rawProject, project)
if err != nil {
return fmt.Errorf("bolt: failed to decode project: %w", err)
return fmt.Errorf("failed to unmarshal project: %w", err)
}
projects = append(projects, project)
return nil
})
if err != nil {
return fmt.Errorf("bolt: failed to iterate over projects: %w", err)
return fmt.Errorf("failed to iterate over projects: %w", err)
}
return nil

View File

@ -1,40 +1,21 @@
package bolt_test
import (
"bytes"
"context"
"encoding/gob"
"errors"
"math/rand"
"regexp"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/oklog/ulid"
"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/filter"
"github.com/dstotijn/hetty/pkg/proj"
"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()))
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()
@ -50,27 +31,20 @@ func TestUpsertProject(t *testing.T) {
}
defer db.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(.*)"),
},
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(.*)",
},
},
}
@ -83,7 +57,7 @@ func TestUpsertProject(t *testing.T) {
var rawProject []byte
err = boltDB.View(func(tx *bbolt.Tx) error {
rawProject = tx.Bucket([]byte("projects")).Bucket(exp.ID[:]).Get([]byte("project"))
rawProject = tx.Bucket([]byte("projects")).Bucket([]byte(exp.Id)).Get([]byte("project"))
return nil
})
if err != nil {
@ -93,16 +67,14 @@ func TestUpsertProject(t *testing.T) {
t.Fatalf("expected raw project to be retrieved, got: nil")
}
got := proj.Project{}
got := &proj.Project{}
err = gob.NewDecoder(bytes.NewReader(rawProject)).Decode(&got)
err = proto.Unmarshal(rawProject, 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)
}
testutil.ProtoDiff(t, "project not equal", exp, got, "id")
}
func TestFindProjectByID(t *testing.T) {
@ -123,36 +95,32 @@ func TestFindProjectByID(t *testing.T) {
}
defer db.Close()
exp := proj.Project{
ID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
exp := &proj.Project{
Id: ulid.Make().String(),
}
buf := bytes.Buffer{}
err = gob.NewEncoder(&buf).Encode(exp)
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(exp.ID[:])
b, err := tx.Bucket([]byte("projects")).CreateBucket([]byte(exp.Id))
if err != nil {
return err
}
return b.Put([]byte("project"), buf.Bytes())
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)
got, err := db.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)
}
testutil.ProtoDiff(t, "project not equal", exp, got)
})
t.Run("project not found", func(t *testing.T) {
@ -170,7 +138,7 @@ func TestFindProjectByID(t *testing.T) {
}
defer db.Close()
projectID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
projectID := ulid.Make().String()
_, err = db.FindProjectByID(context.Background(), projectID)
if !errors.Is(err, proj.ErrProjectNotFound) {
@ -195,9 +163,9 @@ func TestDeleteProject(t *testing.T) {
defer db.Close()
// Insert test fixture.
projectID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
err = db.UpsertProject(context.Background(), proj.Project{
ID: projectID,
projectID := ulid.Make().String()
err = db.UpsertProject(context.Background(), &proj.Project{
Id: projectID,
})
if err != nil {
t.Fatalf("unexpected error storing project: %v", err)
@ -209,8 +177,8 @@ func TestDeleteProject(t *testing.T) {
}
var got *bbolt.Bucket
err = boltDB.View(func(tx *bbolt.Tx) error {
got = tx.Bucket([]byte("projects")).Bucket(projectID[:])
_ = boltDB.View(func(tx *bbolt.Tx) error {
got = tx.Bucket([]byte("projects")).Bucket([]byte(projectID))
return nil
})
if got != nil {
@ -233,13 +201,13 @@ func TestProjects(t *testing.T) {
}
defer db.Close()
exp := []proj.Project{
exp := []*proj.Project{
{
ID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
Id: ulid.Make().String(),
Name: "one",
},
{
ID: ulid.MustNew(ulid.Timestamp(time.Now())+100, ulidEntropy),
Id: ulid.Make().String(),
Name: "two",
},
}
@ -261,7 +229,5 @@ func TestProjects(t *testing.T) {
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)
}
testutil.ProtoSlicesDiff(t, "projects not equal", exp, got)
}

View File

@ -1,25 +1,23 @@
package bolt
import (
"bytes"
"context"
"encoding/gob"
"errors"
"fmt"
"github.com/oklog/ulid"
bolt "go.etcd.io/bbolt"
"google.golang.org/protobuf/proto"
"github.com/dstotijn/hetty/pkg/http"
"github.com/dstotijn/hetty/pkg/reqlog"
"github.com/dstotijn/hetty/pkg/scope"
)
var ErrRequestLogsBucketNotFound = errors.New("bolt: request logs bucket not found")
var reqLogsBucketName = []byte("request_logs")
func requestLogsBucket(tx *bolt.Tx, projectID ulid.ULID) (*bolt.Bucket, error) {
pb, err := projectBucket(tx, projectID[:])
func requestLogsBucket(tx *bolt.Tx, projectID string) (*bolt.Bucket, error) {
pb, err := projectBucket(tx, projectID)
if err != nil {
return nil, err
}
@ -32,47 +30,36 @@ func requestLogsBucket(tx *bolt.Tx, projectID ulid.ULID) (*bolt.Bucket, error) {
return b, nil
}
func (db *Database) FindRequestLogs(ctx context.Context, filter reqlog.FindRequestsFilter, scope *scope.Scope) (reqLogs []reqlog.RequestLog, err error) {
if filter.ProjectID.Compare(ulid.ULID{}) == 0 {
return nil, reqlog.ErrProjectIDMustBeSet
}
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, filter.ProjectID)
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.RequestLog
err = gob.NewDecoder(bytes.NewReader(rawReqLog)).Decode(&reqLog)
var reqLog reqlog.HttpRequestLog
err = proto.Unmarshal(rawReqLog, &reqLog)
if err != nil {
return fmt.Errorf("failed to decode request log: %w", err)
}
if filter.OnlyInScope && !reqLog.MatchScope(scope) {
return nil
}
// Filter by search expression. TODO: Once pagination is introduced,
// this filter logic should be done as items are retrieved.
if filter.SearchExpr != nil {
match, err := reqLog.Matches(filter.SearchExpr)
if filterFn != nil {
match, err := filterFn(&reqLog)
if err != nil {
return fmt.Errorf("failed to match search expression for request log (id: %v): %w", reqLogID, err)
return fmt.Errorf("failed to filter request log: %w", err)
}
if !match {
return nil
}
}
reqLogs = append(reqLogs, reqLog)
reqLogs = append(reqLogs, &reqLog)
return nil
})
if err != nil {
@ -87,46 +74,45 @@ func (db *Database) FindRequestLogs(ctx context.Context, filter reqlog.FindReque
return reqLogs, nil
}
func (db *Database) FindRequestLogByID(ctx context.Context, projectID, reqLogID ulid.ULID) (reqLog reqlog.RequestLog, err error) {
err = db.bolt.View(func(tx *bolt.Tx) error {
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(reqLogID[:])
rawReqLog := b.Get([]byte(reqLogID))
if rawReqLog == nil {
return reqlog.ErrRequestNotFound
return reqlog.ErrRequestLogNotFound
}
err = gob.NewDecoder(bytes.NewReader(rawReqLog)).Decode(&reqLog)
err = proto.Unmarshal(rawReqLog, reqLog)
if err != nil {
return fmt.Errorf("failed to decode request log: %w", err)
return fmt.Errorf("failed to unmarshal request log: %w", err)
}
return nil
})
if err != nil {
return reqlog.RequestLog{}, fmt.Errorf("bolt: failed to find request log by ID: %w", err)
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.RequestLog) error {
buf := bytes.Buffer{}
err := gob.NewEncoder(&buf).Encode(reqLog)
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 encode request log: %w", err)
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)
b, err := requestLogsBucket(txn, reqLog.ProjectId)
if err != nil {
return fmt.Errorf("failed to get request logs bucket: %w", err)
}
err = b.Put(reqLog.ID[:], buf.Bytes())
err = b.Put([]byte(reqLog.Id), encReqLog)
if err != nil {
return fmt.Errorf("failed to put request log: %w", err)
}
@ -140,40 +126,32 @@ func (db *Database) StoreRequestLog(ctx context.Context, reqLog reqlog.RequestLo
return nil
}
func (db *Database) StoreResponseLog(ctx context.Context, projectID, reqLogID ulid.ULID, resLog reqlog.ResponseLog) error {
buf := bytes.Buffer{}
err := gob.NewEncoder(&buf).Encode(resLog)
if err != nil {
return fmt.Errorf("bolt: failed to encode response log: %w", err)
}
err = db.bolt.Update(func(txn *bolt.Tx) error {
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)
}
rawReqLog := b.Get(reqLogID[:])
if rawReqLog == nil {
return reqlog.ErrRequestNotFound
encReqLog := b.Get([]byte(reqLogID))
if encReqLog == nil {
return reqlog.ErrRequestLogNotFound
}
var reqLog reqlog.RequestLog
err = gob.NewDecoder(bytes.NewReader(rawReqLog)).Decode(&reqLog)
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
reqLog.Response = resLog
buf := bytes.Buffer{}
err = gob.NewEncoder(&buf).Encode(reqLog)
encReqLog, err = proto.Marshal(&reqLog)
if err != nil {
return fmt.Errorf("failed to encode request log: %w", err)
}
err = b.Put(reqLog.ID[:], buf.Bytes())
err = b.Put([]byte(reqLogID), encReqLog)
if err != nil {
return fmt.Errorf("failed to put request log: %w", err)
}
@ -187,9 +165,9 @@ func (db *Database) StoreResponseLog(ctx context.Context, projectID, reqLogID ul
return nil
}
func (db *Database) ClearRequestLogs(ctx context.Context, projectID ulid.ULID) error {
func (db *Database) ClearRequestLogs(ctx context.Context, projectID string) error {
err := db.bolt.Update(func(txn *bolt.Tx) error {
pb, err := projectBucket(txn, projectID[:])
pb, err := projectBucket(txn, projectID)
if err != nil {
return fmt.Errorf("failed to get project bucket: %w", err)
}

View File

@ -2,47 +2,21 @@ package bolt_test
import (
"context"
"errors"
"net/http"
"net/url"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/oklog/ulid"
"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("without project ID in filter", 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()
filter := reqlog.FindRequestsFilter{}
_, err = db.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()
@ -58,44 +32,56 @@ func TestFindRequestLogs(t *testing.T) {
}
defer db.Close()
projectID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
projectID := ulid.Make().String()
err = db.UpsertProject(context.Background(), proj.Project{
ID: projectID,
err = db.UpsertProject(context.Background(), &proj.Project{
Id: projectID,
})
if err != nil {
t.Fatalf("unexpected error upserting project: %v", err)
}
fixtures := []reqlog.RequestLog{
fixtures := []*reqlog.HttpRequestLog{
{
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"},
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"),
},
Body: []byte("foo"),
Response: &reqlog.ResponseLog{
Proto: "HTTP/1.1",
Response: &http.Response{
Status: "200 OK",
StatusCode: 200,
Header: http.Header{
"X-Yolo": []string{"swag"},
Headers: []*http.Header{
{Key: "X-Yolo", Value: "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"},
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"),
},
},
}
@ -108,34 +94,17 @@ func TestFindRequestLogs(t *testing.T) {
}
}
filter := reqlog.FindRequestsFilter{
ProjectID: projectID,
}
got, err := db.FindRequestLogs(context.Background(), filter, nil)
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.RequestLog, len(fixtures))
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]
}
if diff := cmp.Diff(exp, got); diff != "" {
t.Fatalf("request logs not equal (-exp, +got):\n%v", diff)
}
testutil.ProtoSlicesDiff(t, "request logs not equal", exp, got)
})
}
func mustParseURL(t *testing.T, s string) *url.URL {
t.Helper()
u, err := url.Parse(s)
if err != nil {
panic(err)
}
return u
}

View File

@ -1,16 +1,13 @@
package bolt
import (
"bytes"
"context"
"encoding/gob"
"errors"
"fmt"
"github.com/oklog/ulid"
bolt "go.etcd.io/bbolt"
"google.golang.org/protobuf/proto"
"github.com/dstotijn/hetty/pkg/scope"
"github.com/dstotijn/hetty/pkg/sender"
)
@ -18,8 +15,8 @@ var ErrSenderRequestsBucketNotFound = errors.New("bolt: sender requests bucket n
var senderReqsBucketName = []byte("sender_requests")
func senderReqsBucket(tx *bolt.Tx, projectID ulid.ULID) (*bolt.Bucket, error) {
pb, err := projectBucket(tx, projectID[:])
func senderReqsBucket(tx *bolt.Tx, projectID string) (*bolt.Bucket, error) {
pb, err := projectBucket(tx, projectID)
if err != nil {
return nil, err
}
@ -32,21 +29,19 @@ func senderReqsBucket(tx *bolt.Tx, projectID ulid.ULID) (*bolt.Bucket, error) {
return b, nil
}
func (db *Database) StoreSenderRequest(ctx context.Context, req sender.Request) error {
buf := bytes.Buffer{}
err := gob.NewEncoder(&buf).Encode(req)
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 encode sender request: %w", err)
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)
senderReqsBucket, err := senderReqsBucket(tx, req.ProjectId)
if err != nil {
return fmt.Errorf("failed to get sender requests bucket: %w", err)
}
err = senderReqsBucket.Put(req.ID[:], buf.Bytes())
err = senderReqsBucket.Put([]byte(req.Id), rawReq)
if err != nil {
return fmt.Errorf("failed to put sender request: %w", err)
}
@ -60,9 +55,9 @@ func (db *Database) StoreSenderRequest(ctx context.Context, req sender.Request)
return nil
}
func (db *Database) FindSenderRequestByID(ctx context.Context, projectID, senderReqID ulid.ULID) (req sender.Request, err error) {
if projectID.Compare(ulid.ULID{}) == 0 {
return sender.Request{}, sender.ErrProjectIDMustBeSet
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 {
@ -71,63 +66,49 @@ func (db *Database) FindSenderRequestByID(ctx context.Context, projectID, sender
return fmt.Errorf("failed to get sender requests bucket: %w", err)
}
rawSenderReq := senderReqsBucket.Get(senderReqID[:])
rawSenderReq := senderReqsBucket.Get([]byte(senderReqID))
if rawSenderReq == nil {
return sender.ErrRequestNotFound
}
err = gob.NewDecoder(bytes.NewReader(rawSenderReq)).Decode(&req)
req = &sender.Request{}
err = proto.Unmarshal(rawSenderReq, req)
if err != nil {
return fmt.Errorf("failed to decode sender request: %w", err)
return fmt.Errorf("failed to unmarshal sender request: %w", err)
}
return nil
})
if err != nil {
return sender.Request{}, fmt.Errorf("bolt: failed to commit transaction: %w", err)
return nil, fmt.Errorf("bolt: failed to commit transaction: %w", err)
}
return req, nil
}
func (db *Database) FindSenderRequests(ctx context.Context, filter sender.FindRequestsFilter, scope *scope.Scope) (reqs []sender.Request, err error) {
if filter.ProjectID.Compare(ulid.ULID{}) == 0 {
return nil, sender.ErrProjectIDMustBeSet
}
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, filter.ProjectID)
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 {
var req sender.Request
err = gob.NewDecoder(bytes.NewReader(rawSenderReq)).Decode(&req)
req := &sender.Request{}
err = proto.Unmarshal(rawSenderReq, req)
if err != nil {
return fmt.Errorf("failed to decode sender request: %w", err)
return fmt.Errorf("failed to unmarshal sender request: %w", err)
}
if filter.OnlyInScope {
if !req.MatchScope(scope) {
return nil
}
}
// Filter by search expression. TODO: Once pagination is introduced,
// this filter logic should be done as items are retrieved.
if filter.SearchExpr != nil {
match, err := req.Matches(filter.SearchExpr)
if filterFn != nil {
match, err := filterFn(req)
if err != nil {
return fmt.Errorf(
"bolt: failed to match search expression for sender request (id: %v): %w",
senderReqID, err,
)
return fmt.Errorf("failed to filter sender request: %w", err)
}
if !match {
@ -150,7 +131,7 @@ func (db *Database) FindSenderRequests(ctx context.Context, filter sender.FindRe
return reqs, nil
}
func (db *Database) DeleteSenderRequests(ctx context.Context, projectID ulid.ULID) error {
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 {

View File

@ -3,19 +3,17 @@ package bolt_test
import (
"context"
"errors"
"net/http"
"net/url"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/oklog/ulid"
"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/sender"
"github.com/dstotijn/hetty/pkg/testutil"
)
var exampleURL = func() *url.URL {
@ -43,11 +41,11 @@ func TestFindRequestByID(t *testing.T) {
}
defer db.Close()
projectID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
reqID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
projectID := "foobar-project-id"
reqID := "foobar-req-id"
err = db.UpsertProject(context.Background(), proj.Project{
ID: projectID,
err = db.UpsertProject(context.Background(), &proj.Project{
Id: projectID,
})
if err != nil {
t.Fatalf("unexpected error upserting project: %v", err)
@ -67,24 +65,31 @@ func TestFindRequestByID(t *testing.T) {
t.Run("sender request found", func(t *testing.T) {
t.Parallel()
exp := sender.Request{
ID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
ProjectID: projectID,
SourceRequestLogID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
URL: exampleURL,
Method: http.MethodGet,
Proto: sender.HTTPProto20,
Header: http.Header{
"X-Foo": []string{"bar"},
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"),
},
Body: []byte("foo"),
Response: &reqlog.ResponseLog{
Proto: "HTTP/2.0",
HttpResponse: &http.Response{
Protocol: http.Protocol_PROTOCOL_HTTP20,
Status: "200 OK",
StatusCode: 200,
Header: http.Header{
"X-Yolo": []string{"swag"},
Headers: []*http.Header{
{
Key: "X-Yolo",
Value: "swag",
},
},
Body: []byte("bar"),
},
@ -95,14 +100,12 @@ func TestFindRequestByID(t *testing.T) {
t.Fatalf("unexpected error (expected: nil, got: %v)", err)
}
got, err := db.FindSenderRequestByID(context.Background(), exp.ProjectID, exp.ID)
got, err := db.FindSenderRequestByID(context.Background(), projectID, 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)
}
testutil.ProtoDiff(t, "sender request not equal", exp, got, "id")
})
})
}
@ -110,30 +113,6 @@ func TestFindRequestByID(t *testing.T) {
func TestFindSenderRequests(t *testing.T) {
t.Parallel()
t.Run("without project ID in filter", 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()
filter := sender.FindRequestsFilter{}
_, err = db.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()
@ -150,48 +129,61 @@ func TestFindSenderRequests(t *testing.T) {
}
defer db.Close()
projectID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
projectID := "foobar-project-id"
err = db.UpsertProject(context.Background(), proj.Project{
ID: projectID,
Name: "foobar",
Settings: proj.Settings{},
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{
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"},
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"),
},
Body: []byte("foo"),
Response: &reqlog.ResponseLog{
Proto: "HTTP/1.1",
HttpResponse: &http.Response{
Protocol: http.Protocol_PROTOCOL_HTTP11,
Status: "200 OK",
StatusCode: 200,
Header: http.Header{
"X-Yolo": []string{"swag"},
Headers: []*http.Header{
{
Key: "X-Yolo",
Value: "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"},
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"),
},
},
}
@ -204,23 +196,17 @@ func TestFindSenderRequests(t *testing.T) {
}
}
filter := sender.FindRequestsFilter{
ProjectID: projectID,
}
got, err := db.FindSenderRequests(context.Background(), filter, nil)
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))
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)
}
testutil.ProtoSlicesDiff(t, "sender requests not equal", exp, got)
})
}

View File

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

79
pkg/http/http.go Normal file
View File

@ -0,0 +1,79 @@
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
}

476
pkg/http/http.pb.go Normal file
View File

@ -0,0 +1,476 @@
// 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
}

340
pkg/proj/proj.connect.go Normal file
View File

@ -0,0 +1,340 @@
// 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,12 +4,11 @@ import (
"context"
"errors"
"fmt"
"math/rand"
"regexp"
"sync"
"time"
"github.com/oklog/ulid"
connect "connectrpc.com/connect"
"github.com/oklog/ulid/v2"
"github.com/dstotijn/hetty/pkg/filter"
"github.com/dstotijn/hetty/pkg/proxy/intercept"
@ -18,27 +17,16 @@ import (
"github.com/dstotijn/hetty/pkg/sender"
)
//nolint:gosec
var ulidEntropy = rand.New(rand.NewSource(time.Now().UnixNano()))
type Service struct {
repo Repository
interceptSvc *intercept.Service
reqLogSvc *reqlog.Service
senderSvc *sender.Service
scope *scope.Scope
activeProjectID ulid.ULID
activeProjectID string
mu sync.RWMutex
}
type Project struct {
ID ulid.ULID
Name string
Settings Settings
isActive bool
}
type Settings struct {
// Request log settings
ReqLogBypassOutOfScope bool
@ -87,216 +75,324 @@ func NewService(cfg Config) (*Service, error) {
}, nil
}
func (svc *Service) CreateProject(ctx context.Context, name string) (Project, error) {
if !nameRegexp.MatchString(name) {
return Project{}, ErrInvalidName
func (svc *Service) CreateProject(ctx context.Context, req *connect.Request[CreateProjectRequest]) (*connect.Response[CreateProjectResponse], error) {
if !nameRegexp.MatchString(req.Msg.Name) {
return nil, ErrInvalidName
}
project := Project{
ID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
Name: name,
project := &Project{
Id: ulid.Make().String(),
Name: req.Msg.Name,
}
err := svc.repo.UpsertProject(ctx, project)
if err != nil {
return Project{}, fmt.Errorf("proj: could not create project: %w", err)
return nil, fmt.Errorf("proj: could not create project: %w", err)
}
return project, nil
return &connect.Response[CreateProjectResponse]{
Msg: &CreateProjectResponse{
Project: project,
},
}, nil
}
// CloseProject closes the currently open project (if there is one).
func (svc *Service) CloseProject() error {
func (svc *Service) CloseProject(ctx context.Context, _ *connect.Request[CloseProjectRequest]) (*connect.Response[CloseProjectResponse], error) {
svc.mu.Lock()
defer svc.mu.Unlock()
if svc.activeProjectID.Compare(ulid.ULID{}) == 0 {
return nil
if svc.activeProjectID == "" {
return nil, connect.NewError(connect.CodeFailedPrecondition, ErrNoProject)
}
svc.activeProjectID = ulid.ULID{}
svc.reqLogSvc.SetActiveProjectID(ulid.ULID{})
svc.activeProjectID = ""
svc.reqLogSvc.SetActiveProjectID("")
svc.reqLogSvc.SetBypassOutOfScopeRequests(false)
svc.reqLogSvc.SetFindReqsFilter(reqlog.FindRequestsFilter{})
svc.reqLogSvc.SetRequestLogsFilter(nil)
svc.interceptSvc.UpdateSettings(intercept.Settings{
RequestsEnabled: false,
ResponsesEnabled: false,
RequestFilter: nil,
ResponseFilter: nil,
})
svc.senderSvc.SetActiveProjectID(ulid.ULID{})
svc.senderSvc.SetFindReqsFilter(sender.FindRequestsFilter{})
svc.senderSvc.SetActiveProjectID("")
svc.scope.SetRules(nil)
return nil
return &connect.Response[CloseProjectResponse]{}, nil
}
// DeleteProject removes a project from the repository.
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())
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)
}
if err := svc.repo.DeleteProject(ctx, projectID); err != nil {
return fmt.Errorf("proj: could not delete project: %w", err)
if err := svc.repo.DeleteProject(ctx, req.Msg.ProjectId); err != nil {
return nil, fmt.Errorf("proj: could not delete project: %w", err)
}
return nil
return &connect.Response[DeleteProjectResponse]{}, nil
}
// OpenProject sets a project as the currently active project.
func (svc *Service) OpenProject(ctx context.Context, projectID ulid.ULID) (Project, error) {
func (svc *Service) OpenProject(ctx context.Context, req *connect.Request[OpenProjectRequest]) (*connect.Response[OpenProjectResponse], error) {
svc.mu.Lock()
defer svc.mu.Unlock()
project, err := svc.repo.FindProjectByID(ctx, projectID)
p, err := svc.repo.FindProjectByID(ctx, req.Msg.ProjectId)
if errors.Is(err, ErrProjectNotFound) {
return nil, connect.NewError(connect.CodeNotFound, ErrProjectNotFound)
}
if err != nil {
return Project{}, fmt.Errorf("proj: failed to get project: %w", err)
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("proj: failed to find project: %w", err))
}
svc.activeProjectID = project.ID
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
}
// Request log settings.
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)
svc.reqLogSvc.SetActiveProjectID(p.Id)
svc.reqLogSvc.SetBypassOutOfScopeRequests(p.ReqLogBypassOutOfScope)
svc.reqLogSvc.SetRequestLogsFilter(p.ReqLogFilter)
// Intercept settings.
svc.interceptSvc.UpdateSettings(intercept.Settings{
RequestsEnabled: project.Settings.InterceptRequests,
ResponsesEnabled: project.Settings.InterceptResponses,
RequestFilter: project.Settings.InterceptRequestFilter,
ResponseFilter: project.Settings.InterceptResponseFilter,
})
svc.interceptSvc.UpdateSettings(interceptSettings)
// Sender settings.
svc.senderSvc.SetActiveProjectID(project.ID)
svc.senderSvc.SetFindReqsFilter(sender.FindRequestsFilter{
ProjectID: project.ID,
OnlyInScope: project.Settings.SenderOnlyFindInScope,
SearchExpr: project.Settings.SenderSearchExpr,
svc.senderSvc.SetActiveProjectID(p.Id)
svc.senderSvc.SetRequestsFilter(&sender.RequestsFilter{
OnlyInScope: p.SenderOnlyFindInScope,
SearchExpr: p.SenderSearchExpr,
})
// Scope settings.
svc.scope.SetRules(project.Settings.ScopeRules)
return project, nil
}
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)
scopeRules, err := p.ParseScopeRules()
if err != nil {
return Project{}, fmt.Errorf("proj: failed to get active project: %w", err)
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
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)
}
return project, nil
}
func (svc *Service) Projects(ctx context.Context) ([]Project, error) {
func (svc *Service) ListProjects(ctx context.Context, _ *connect.Request[ListProjectsRequest]) (*connect.Response[ListProjectsResponse], error) {
projects, err := svc.repo.Projects(ctx)
if err != nil {
return nil, fmt.Errorf("proj: could not get projects: %w", err)
}
return projects, nil
for _, project := range projects {
if svc.IsProjectActive(project.Id) {
project.IsActive = true
}
}
return &connect.Response[ListProjectsResponse]{
Msg: &ListProjectsResponse{
Projects: projects,
},
}, nil
}
func (svc *Service) Scope() *scope.Scope {
return svc.scope
}
func (svc *Service) SetScopeRules(ctx context.Context, rules []scope.Rule) error {
project, err := svc.ActiveProject(ctx)
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)
if err != nil {
return err
}
project.Settings.ScopeRules = rules
project.SenderOnlyFindInScope = filter.OnlyInScope
project.SenderSearchExpr = filter.SearchExpr
err = svc.repo.UpsertProject(ctx, project)
if err != nil {
return fmt.Errorf("proj: failed to update project: %w", err)
}
svc.scope.SetRules(rules)
svc.senderSvc.SetRequestsFilter(filter)
return nil
}
func (svc *Service) SetRequestLogFindFilter(ctx context.Context, filter reqlog.FindRequestsFilter) error {
project, err := svc.ActiveProject(ctx)
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)
if err != nil {
return err
return nil, err
}
filter.ProjectID = project.ID
project.Settings.ReqLogOnlyFindInScope = filter.OnlyInScope
project.Settings.ReqLogSearchExpr = filter.SearchExpr
project.InterceptRequests = req.Msg.RequestsEnabled
project.InterceptResponses = req.Msg.ResponsesEnabled
project.InterceptRequestFilterExpr = req.Msg.RequestFilterExpr
project.InterceptResponseFilterExpr = req.Msg.ResponseFilterExpr
err = svc.repo.UpsertProject(ctx, project)
if err != nil {
return fmt.Errorf("proj: failed to update project: %w", err)
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("proj: failed to update project: %w", err))
}
svc.reqLogSvc.SetFindReqsFilter(filter)
return nil
}
func (svc *Service) SetSenderRequestFindFilter(ctx context.Context, filter sender.FindRequestsFilter) error {
project, err := svc.ActiveProject(ctx)
reqFilterExpr, err := filter.ParseQuery(req.Msg.RequestFilterExpr)
if err != nil {
return err
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("proj: failed to parse intercept request filter: %w", err))
}
filter.ProjectID = project.ID
project.Settings.SenderOnlyFindInScope = filter.OnlyInScope
project.Settings.SenderSearchExpr = filter.SearchExpr
err = svc.repo.UpsertProject(ctx, project)
respFilterExpr, err := filter.ParseQuery(req.Msg.ResponseFilterExpr)
if err != nil {
return fmt.Errorf("proj: failed to update project: %w", err)
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("proj: failed to parse intercept response filter: %w", err))
}
svc.senderSvc.SetFindReqsFilter(filter)
svc.interceptSvc.UpdateSettings(intercept.Settings{
RequestsEnabled: req.Msg.RequestsEnabled,
ResponsesEnabled: req.Msg.ResponsesEnabled,
RequestFilter: reqFilterExpr,
ResponseFilter: respFilterExpr,
})
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
return &connect.Response[UpdateInterceptSettingsResponse]{}, nil
}

1181
pkg/proj/proj.pb.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -2,14 +2,12 @@ package proj
import (
"context"
"github.com/oklog/ulid"
)
type Repository interface {
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)
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)
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 = ioutil.NopCloser(bytes.NewBuffer(body))
req.Body = io.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 = ioutil.NopCloser(bytes.NewBuffer(body))
res.Body = io.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, req.Header)
match, err := filter.MatchHTTPHeaders(expr.Operator, expr.Right, httppb.ParseHeader(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 = ioutil.NopCloser(bytes.NewBuffer(body))
req.Body = io.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, res.Header)
match, err := filter.MatchHTTPHeaders(expr.Operator, expr.Right, httppb.ParseHeader(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"
"github.com/oklog/ulid/v2"
"github.com/dstotijn/hetty/pkg/filter"
"github.com/dstotijn/hetty/pkg/log"

View File

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

View File

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

View File

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

View File

@ -0,0 +1,167 @@
// 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"
"github.com/oklog/ulid"
"connectrpc.com/connect"
"github.com/oklog/ulid/v2"
"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,48 +26,21 @@ const (
)
var (
ErrRequestNotFound = errors.New("reqlog: request not found")
ErrRequestLogNotFound = errors.New("reqlog: request not found")
ErrProjectIDMustBeSet = errors.New("reqlog: project ID must be set")
)
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 struct {
bypassOutOfScopeRequests bool
findReqsFilter FindRequestsFilter
activeProjectID ulid.ULID
reqsFilter *RequestLogsFilter
activeProjectID string
scope *scope.Scope
repo Repository
logger log.Logger
}
type FindRequestsFilter struct {
ProjectID ulid.ULID
OnlyInScope bool
SearchExpr filter.Expression
}
type Config struct {
ActiveProjectID ulid.ULID
ActiveProjectID string
Scope *scope.Scope
Repository Repository
Logger log.Logger
@ -88,25 +61,92 @@ func NewService(cfg Config) *Service {
return s
}
func (svc *Service) FindRequests(ctx context.Context) ([]RequestLog, error) {
return svc.repo.FindRequestLogs(ctx, svc.findReqsFilter, svc.scope)
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) FindRequestLogByID(ctx context.Context, id ulid.ULID) (RequestLog, error) {
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 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) storeResponse(ctx context.Context, reqLogID ulid.ULID, res *http.Response) error {
resLog, err := ParseHTTPResponse(res)
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)
if err != nil {
return err
}
return svc.repo.StoreResponseLog(ctx, svc.activeProjectID, reqLogID, resLog)
return svc.repo.StoreResponseLog(ctx, svc.activeProjectID, reqLogID, respb)
}
func (svc *Service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestModifyFunc {
@ -121,19 +161,19 @@ func (svc *Service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestM
// TODO: Use io.LimitReader.
var err error
body, err = ioutil.ReadAll(req.Body)
body, err = io.ReadAll(req.Body)
if err != nil {
svc.logger.Errorw("Failed to read request body for logging.",
"error", err)
return
}
req.Body = ioutil.NopCloser(bytes.NewBuffer(body))
clone.Body = ioutil.NopCloser(bytes.NewBuffer(body))
req.Body = io.NopCloser(bytes.NewBuffer(body))
clone.Body = io.NopCloser(bytes.NewBuffer(body))
}
// Bypass logging if no project is active.
if svc.activeProjectID.Compare(ulid.ULID{}) == 0 {
if svc.activeProjectID == "" {
ctx := context.WithValue(req.Context(), LogBypassedKey, true)
*req = *req.WithContext(ctx)
@ -161,14 +201,37 @@ func (svc *Service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestM
return
}
reqLog := RequestLog{
ID: reqID,
ProjectID: svc.activeProjectID,
Method: clone.Method,
URL: clone.URL,
Proto: clone.Proto,
Header: clone.Header,
Body: body,
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,
},
}
err := svc.repo.StoreRequestLog(req.Context(), reqLog)
@ -179,10 +242,10 @@ func (svc *Service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestM
}
svc.logger.Debugw("Stored request log.",
"reqLogID", reqLog.ID.String(),
"url", reqLog.URL.String())
ctx := context.WithValue(req.Context(), ReqLogIDKey, reqLog.ID)
"reqLogID", reqLog.Id,
"url", reqLog.Request.Url,
)
ctx := context.WithValue(req.Context(), ReqLogIDKey, reqID)
*req = *req.WithContext(ctx)
}
}
@ -216,7 +279,7 @@ func (svc *Service) ResponseModifier(next proxy.ResponseModifyFunc) proxy.Respon
}
go func() {
if err := svc.storeResponse(context.Background(), reqLogID, &clone); err != nil {
if err := svc.storeResponse(context.Background(), reqLogID.String(), &clone); err != nil {
svc.logger.Errorw("Failed to store response log.",
"error", err)
} else {
@ -229,20 +292,16 @@ func (svc *Service) ResponseModifier(next proxy.ResponseModifyFunc) proxy.Respon
}
}
func (svc *Service) SetActiveProjectID(id ulid.ULID) {
func (svc *Service) SetActiveProjectID(id string) {
svc.activeProjectID = id
}
func (svc *Service) ActiveProjectID() ulid.ULID {
func (svc *Service) ActiveProjectID() string {
return svc.activeProjectID
}
func (svc *Service) SetFindReqsFilter(filter FindRequestsFilter) {
svc.findReqsFilter = filter
}
func (svc *Service) FindReqsFilter() FindRequestsFilter {
return svc.findReqsFilter
func (svc *Service) SetRequestLogsFilter(filter *RequestLogsFilter) {
svc.reqsFilter = filter
}
func (svc *Service) SetBypassOutOfScopeRequests(bypass bool) {
@ -252,18 +311,3 @@ func (svc *Service) SetBypassOutOfScopeRequests(bypass 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
}

533
pkg/reqlog/reqlog.pb.go Normal file
View File

@ -0,0 +1,533 @@
// 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

@ -3,27 +3,24 @@ package reqlog_test
import (
"context"
"io"
"math/rand"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/oklog/ulid"
"github.com/oklog/ulid/v2"
"go.etcd.io/bbolt"
"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"
@ -39,9 +36,9 @@ func TestRequestModifier(t *testing.T) {
}
defer db.Close()
projectID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
err = db.UpsertProject(context.Background(), proj.Project{
ID: projectID,
projectID := ulid.Make().String()
err = db.UpsertProject(context.Background(), &proj.Project{
Id: projectID,
})
if err != nil {
t.Fatalf("unexpected error upserting project: %v", err)
@ -58,34 +55,39 @@ func TestRequestModifier(t *testing.T) {
}
reqModFn := svc.RequestModifier(next)
req := httptest.NewRequest("GET", "https://example.com/", strings.NewReader("bar"))
reqID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
req.Header.Add("X-Yolo", "swag")
reqID := ulid.Make()
req = req.WithContext(proxy.WithRequestID(req.Context(), reqID))
reqModFn(req)
t.Run("request log was stored in repository", func(t *testing.T) {
exp := reqlog.RequestLog{
ID: reqID,
ProjectID: svc.ActiveProjectID(),
Method: req.Method,
URL: req.URL,
Proto: req.Proto,
Header: req.Header,
Body: []byte("modified body"),
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"),
},
}
got, err := svc.FindRequestLogByID(context.Background(), reqID)
got, err := db.FindRequestLogByID(context.Background(), svc.ActiveProjectID(), reqID.String())
if err != nil {
t.Fatalf("failed to find request by id: %v", err)
}
if diff := cmp.Diff(exp, got); diff != "" {
t.Fatalf("request log not equal (-exp, +got):\n%v", diff)
}
testutil.ProtoDiff(t, "request log not equal", exp, got)
})
}
//nolint:paralleltest
func TestResponseModifier(t *testing.T) {
path := t.TempDir() + "bolt.db"
boltDB, err := bbolt.Open(path, 0o600, nil)
@ -100,9 +102,9 @@ func TestResponseModifier(t *testing.T) {
}
defer db.Close()
projectID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
err = db.UpsertProject(context.Background(), proj.Project{
ID: projectID,
projectID := "foobar-project-id"
err = db.UpsertProject(context.Background(), &proj.Project{
Id: projectID,
})
if err != nil {
t.Fatalf("unexpected error upserting project: %v", err)
@ -120,39 +122,44 @@ func TestResponseModifier(t *testing.T) {
resModFn := svc.ResponseModifier(next)
req := httptest.NewRequest("GET", "https://example.com/", strings.NewReader("bar"))
reqLogID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
reqLogID := ulid.Make()
req = req.WithContext(context.WithValue(req.Context(), reqlog.ReqLogIDKey, reqLogID))
err = db.StoreRequestLog(context.Background(), reqlog.RequestLog{
ID: reqLogID,
ProjectID: projectID,
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,
Body: io.NopCloser(strings.NewReader("bar")),
Request: req,
Proto: "HTTP/1.1",
Status: "200 OK",
StatusCode: 200,
Body: io.NopCloser(strings.NewReader("bar")),
}
if err := resModFn(res); err != nil {
t.Fatalf("unexpected error (expected: nil, got: %v)", err)
}
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)
// Dirty (but simple) wait for other goroutine to finish calling repository.
time.Sleep(10 * time.Millisecond)
got, err := svc.FindRequestLogByID(context.Background(), reqLogID)
if err != nil {
t.Fatalf("failed to find request by id: %v", err)
}
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) {
if exp := "modified body"; exp != string(got.Response.Body) {
t.Fatalf("incorrect `ResponseLog.Body` value (expected: %v, got: %v)", exp, string(got.Response.Body))
}
})
})
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())
}

View File

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

View File

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

159
pkg/scope/scope.pb.go Normal file
View File

@ -0,0 +1,159 @@
// 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,15 +2,11 @@ package sender
import (
"context"
"github.com/oklog/ulid"
"github.com/dstotijn/hetty/pkg/scope"
)
type Repository interface {
FindSenderRequestByID(ctx context.Context, projectID, id ulid.ULID) (Request, error)
FindSenderRequests(ctx context.Context, filter FindRequestsFilter, scope *scope.Scope) ([]Request, error)
StoreSenderRequest(ctx context.Context, req Request) error
DeleteSenderRequests(ctx context.Context, projectID ulid.ULID) error
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
}

View File

@ -5,31 +5,32 @@ import (
"fmt"
"strings"
"github.com/oklog/ulid"
"github.com/oklog/ulid/v2"
"github.com/dstotijn/hetty/pkg/filter"
"github.com/dstotijn/hetty/pkg/reqlog"
"github.com/dstotijn/hetty/pkg/http"
"github.com/dstotijn/hetty/pkg/scope"
)
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 {
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 {
return ""
}
return req.URL.String()
return ulid.Time(id.Time()).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)
@ -42,7 +43,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)
@ -56,7 +57,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)
@ -92,7 +93,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.Header)
match, err := filter.MatchHTTPHeaders(expr.Operator, expr.Right, req.GetHttpRequest().GetHeaders())
if err != nil {
return false, fmt.Errorf("failed to match request HTTP headers: %w", err)
}
@ -100,8 +101,8 @@ func (req Request) matchInfixExpr(expr filter.InfixExpression) (bool, error) {
return match, nil
}
if leftVal == "res.headers" && req.Response != nil {
match, err := filter.MatchHTTPHeaders(expr.Operator, expr.Right, req.Response.Header)
if leftVal == "res.headers" && req.GetHttpResponse() != nil {
match, err := filter.MatchHTTPHeaders(expr.Operator, expr.Right, req.GetHttpResponse().GetHeaders())
if err != nil {
return false, fmt.Errorf("failed to match response HTTP headers: %w", err)
}
@ -152,7 +153,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]
@ -160,28 +161,22 @@ func (req Request) getMappedStringLiteral(s string) string {
return fn(req)
}
case strings.HasPrefix(s, "res."):
if req.Response == nil {
return ""
}
fn, ok := reqlog.ResLogSearchKeyFns[s]
fn, ok := http.ResponseSearchKeyFns[s]
if ok {
return fn(*req.Response)
return fn(req.GetHttpResponse())
}
}
return s
}
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
}
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
}
}
@ -194,54 +189,47 @@ func (req Request) matchStringLiteral(strLiteral filter.StringLiteral) (bool, er
}
}
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 _, 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
}
}
for _, fn := range reqlog.ResLogSearchKeyFns {
if strings.Contains(
strings.ToLower(fn(*req.Response)),
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
}
}
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 rule.URL != nil && req.URL != nil {
if matches := rule.URL.MatchString(req.URL.String()); matches {
if url := req.GetHttpRequest().GetUrl(); rule.URL != nil && url != "" {
if matches := rule.URL.MatchString(url); matches {
return true
}
}
for key, values := range req.Header {
for _, headers := range req.GetHttpRequest().GetHeaders() {
var keyMatches, valueMatches bool
if rule.Header.Key != nil {
if matches := rule.Header.Key.MatchString(key); matches {
if matches := rule.Header.Key.MatchString(headers.Key); matches {
keyMatches = true
}
}
if rule.Header.Value != nil {
for _, value := range values {
if matches := rule.Header.Value.MatchString(value); matches {
valueMatches = true
break
}
if matches := rule.Header.Value.MatchString(headers.Value); matches {
valueMatches = true
}
}
// When only key or value is set, match on whatever is set.
@ -257,7 +245,7 @@ func (req Request) MatchScope(s *scope.Scope) bool {
}
if rule.Body != nil {
if matches := rule.Body.Match(req.Body); matches {
if matches := rule.Body.Match(req.GetHttpRequest().GetBody()); matches {
return true
}
}

View File

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

View File

@ -0,0 +1,311 @@
// 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,21 +5,19 @@ import (
"context"
"errors"
"fmt"
"math/rand"
"net/http"
"net/url"
"time"
"github.com/oklog/ulid"
connect "connectrpc.com/connect"
"github.com/oklog/ulid/v2"
"google.golang.org/protobuf/proto"
"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,
@ -31,20 +29,14 @@ var (
)
type Service struct {
activeProjectID ulid.ULID
findReqsFilter FindRequestsFilter
activeProjectID string
reqsFilter *RequestsFilter
scope *scope.Scope
repo Repository
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
@ -71,165 +63,215 @@ func NewService(cfg Config) *Service {
return svc
}
type Request struct {
ID ulid.ULID
ProjectID ulid.ULID
SourceRequestLogID ulid.ULID
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)
}
URL *url.URL
Method string
Proto string
Header http.Header
Body []byte
Response *reqlog.ResponseLog
}
func (svc *Service) FindRequestByID(ctx context.Context, id ulid.ULID) (Request, error) {
req, err := svc.repo.FindSenderRequestByID(ctx, svc.activeProjectID, id)
senderReq, err := svc.repo.FindSenderRequestByID(ctx, svc.activeProjectID, req.Msg.RequestId)
if err != nil {
return Request{}, fmt.Errorf("sender: failed to find request: %w", err)
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("sender: failed to find request: %w", err))
}
return req, nil
return &connect.Response[GetRequestByIDResponse]{
Msg: &GetRequestByIDResponse{Request: senderReq},
}, 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 Request) (Request, error) {
if svc.activeProjectID.Compare(ulid.ULID{}) == 0 {
return Request{}, ErrProjectIDMustBeSet
}
if req.ID.Compare(ulid.ULID{}) == 0 {
req.ID = ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
}
req.ProjectID = svc.activeProjectID
if req.Method == "" {
req.Method = http.MethodGet
}
if req.Proto == "" {
req.Proto = HTTPProto20
}
if !isValidProto(req.Proto) {
return Request{}, fmt.Errorf("sender: unsupported HTTP protocol: %v", req.Proto)
}
err := svc.repo.StoreSenderRequest(ctx, req)
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)
if err != nil {
return Request{}, fmt.Errorf("sender: failed to store request: %w", err)
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("sender: failed to find requests: %w", err))
}
return req, nil
return &connect.Response[ListRequestsResponse]{
Msg: &ListRequestsResponse{Requests: reqs},
}, nil
}
func (svc *Service) CloneFromRequestLog(ctx context.Context, reqLogID ulid.ULID) (Request, error) {
if svc.activeProjectID.Compare(ulid.ULID{}) == 0 {
return Request{}, ErrProjectIDMustBeSet
func (svc *Service) filterRequest(req *Request) (bool, error) {
if svc.reqsFilter.OnlyInScope {
if svc.scope != nil && !req.MatchScope(svc.scope) {
return false, nil
}
}
reqLog, err := svc.reqLogSvc.FindRequestLogByID(ctx, reqLogID)
if svc.reqsFilter.SearchExpr == "" {
return true, nil
}
expr, err := filter.ParseQuery(svc.reqsFilter.SearchExpr)
if err != nil {
return Request{}, fmt.Errorf("sender: failed to find request log: %w", err)
return false, fmt.Errorf("failed to parse search expression: %w", err)
}
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, req)
match, err := req.Matches(expr)
if err != nil {
return Request{}, fmt.Errorf("sender: failed to store request: %w", err)
return false, fmt.Errorf("failed to match search expression for sender request (id: %v): %w",
req.Id, err,
)
}
return req, nil
return match, nil
}
func (svc *Service) SetFindReqsFilter(filter FindRequestsFilter) {
svc.findReqsFilter = filter
}
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) FindReqsFilter() FindRequestsFilter {
return svc.findReqsFilter
}
r := proto.Clone(req.Msg.Request).(*Request)
func (svc *Service) SendRequest(ctx context.Context, id ulid.ULID) (Request, error) {
req, err := svc.repo.FindSenderRequestByID(ctx, svc.activeProjectID, id)
if r == nil {
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("sender: request is nil"))
}
if r.HttpRequest == nil {
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("sender: request.http_request is nil"))
}
if r.Id == "" {
r.Id = ulid.Make().String()
}
r.ProjectId = svc.activeProjectID
if r.HttpRequest.Method == httppb.Method_METHOD_UNSPECIFIED {
r.HttpRequest.Method = httppb.Method_METHOD_GET
}
if r.HttpRequest.Protocol == httppb.Protocol_PROTOCOL_UNSPECIFIED {
r.HttpRequest.Protocol = httppb.Protocol_PROTOCOL_HTTP20
}
err := svc.repo.StoreSenderRequest(ctx, r)
if err != nil {
return Request{}, fmt.Errorf("sender: failed to find request: %w", err)
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("sender: failed to store request: %w", err))
}
return &connect.Response[CreateOrUpdateRequestResponse]{
Msg: &CreateOrUpdateRequestResponse{
Request: r,
},
}, 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)
}
reqLog, err := svc.reqLogSvc.FindRequestLogByID(ctx, req.Msg.RequestLogId)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, 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,
}
err = svc.repo.StoreSenderRequest(ctx, senderReq)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("sender: failed to store request: %w", err))
}
return &connect.Response[CloneFromRequestLogResponse]{Msg: &CloneFromRequestLogResponse{
Request: senderReq,
}}, nil
}
func (svc *Service) SetRequestsFilter(filter *RequestsFilter) {
svc.reqsFilter = filter
}
func (svc *Service) RequestsFilter() *RequestsFilter {
return svc.reqsFilter
}
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)
if err != nil {
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("sender: failed to find request: %w", err))
}
httpReq, err := parseHTTPRequest(ctx, req)
if err != nil {
return Request{}, fmt.Errorf("sender: failed to parse HTTP request: %w", err)
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("sender: failed to parse HTTP request: %w", err))
}
resLog, err := svc.sendHTTPRequest(httpReq)
httpRes, err := svc.sendHTTPRequest(httpReq)
if err != nil {
return Request{}, fmt.Errorf("sender: could not send HTTP request: %w", err)
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("sender: could not send HTTP request: %w", err))
}
req.Response = &resLog
req.HttpResponse = httpRes
err = svc.repo.StoreSenderRequest(ctx, req)
if err != nil {
return Request{}, fmt.Errorf("sender: failed to store sender response log: %w", err)
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("sender: failed to store sender response log: %w", err))
}
req.Response = &resLog
return req, nil
return &connect.Response[SendRequestResponse]{
Msg: &SendRequestResponse{
Request: req,
},
}, nil
}
func parseHTTPRequest(ctx context.Context, req Request) (*http.Request, error) {
ctx = context.WithValue(ctx, protoCtxKey{}, req.Proto)
func parseHTTPRequest(ctx context.Context, req *Request) (*http.Request, error) {
ctx = context.WithValue(ctx, protoCtxKey{}, req.GetHttpRequest().GetProtocol())
httpReq, err := http.NewRequestWithContext(ctx, req.Method, req.URL.String(), bytes.NewReader(req.Body))
httpReq, err := http.NewRequestWithContext(ctx,
req.GetHttpRequest().GetMethod().String(),
req.GetHttpRequest().GetUrl(),
bytes.NewReader(req.GetHttpRequest().GetBody()),
)
if err != nil {
return nil, fmt.Errorf("failed to construct HTTP request: %w", err)
}
if req.Header != nil {
httpReq.Header = req.Header
for _, header := range req.GetHttpRequest().GetHeaders() {
httpReq.Header.Add(header.Key, header.Value)
}
return httpReq, nil
}
func (svc *Service) sendHTTPRequest(httpReq *http.Request) (reqlog.ResponseLog, error) {
func (svc *Service) sendHTTPRequest(httpReq *http.Request) (*httppb.Response, error) {
res, err := svc.httpClient.Do(httpReq)
if err != nil {
return reqlog.ResponseLog{}, &SendError{err}
return nil, &SendError{err}
}
defer res.Body.Close()
resLog, err := reqlog.ParseHTTPResponse(res)
resLog, err := httppb.ParseHTTPResponse(res)
if err != nil {
return reqlog.ResponseLog{}, fmt.Errorf("failed to parse http response: %w", err)
return nil, fmt.Errorf("failed to parse http response: %w", err)
}
return resLog, err
}
func (svc *Service) SetActiveProjectID(id ulid.ULID) {
func (svc *Service) SetActiveProjectID(id string) {
svc.activeProjectID = id
}
func (svc *Service) DeleteRequests(ctx context.Context, projectID ulid.ULID) error {
return svc.repo.DeleteSenderRequests(ctx, projectID)
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 (e SendError) Error() string {

1040
pkg/sender/sender.pb.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -4,36 +4,24 @@ import (
"context"
"errors"
"fmt"
"math/rand"
"net/http"
http "net/http"
"net/http/httptest"
"net/url"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/oklog/ulid"
connect "connectrpc.com/connect"
"go.etcd.io/bbolt"
"google.golang.org/protobuf/testing/protocmp"
"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()
@ -42,10 +30,16 @@ func TestStoreRequest(t *testing.T) {
svc := sender.NewService(sender.Config{})
_, err := svc.CreateOrUpdateRequest(context.Background(), sender.Request{
URL: exampleURL,
Method: http.MethodPost,
Body: []byte("foobar"),
_, 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"),
},
},
},
})
if !errors.Is(err, sender.ErrProjectIDMustBeSet) {
t.Fatalf("expected `sender.ErrProjectIDMustBeSet`, got: %v", err)
@ -72,75 +66,69 @@ func TestStoreRequest(t *testing.T) {
Repository: db,
})
projectID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
err = db.UpsertProject(context.Background(), proj.Project{
ID: projectID,
Name: "foobar",
Settings: proj.Settings{},
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)
}
svc.SetActiveProjectID(projectID)
svc.SetActiveProjectID(projectID)
exp := sender.Request{
ProjectID: projectID,
URL: exampleURL,
Method: http.MethodPost,
Proto: "HTTP/1.1",
Header: http.Header{
"X-Foo": []string{"bar"},
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"),
},
Body: []byte("foobar"),
}
got, err := svc.CreateOrUpdateRequest(context.Background(), sender.Request{
URL: exampleURL,
Method: http.MethodPost,
Proto: "HTTP/1.1",
Header: http.Header{
"X-Foo": []string{"bar"},
createRes, err := svc.CreateOrUpdateRequest(context.Background(), &connect.Request[sender.CreateOrUpdateRequestRequest]{
Msg: &sender.CreateOrUpdateRequestRequest{
Request: exp,
},
Body: []byte("foobar"),
})
if err != nil {
t.Fatalf("unexpected error storing request: %v", err)
}
if got.ID.Compare(ulid.ULID{}) == 0 {
if createRes.Msg.Request.Id == "" {
t.Fatal("expected request ID to be non-empty value")
}
diff := cmp.Diff(exp, got, cmpopts.IgnoreFields(sender.Request{}, "ID"))
if diff != "" {
t.Fatalf("request not equal (-exp, +got):\n%v", diff)
}
testutil.ProtoDiff(t, "request not equal", exp, createRes.Msg.Request, "id")
got, err = db.FindSenderRequestByID(context.Background(), projectID, got.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)
}
diff = cmp.Diff(exp, got, cmpopts.IgnoreFields(sender.Request{}, "ID"))
if diff != "" {
t.Fatalf("request not equal (-exp, +got):\n%v", diff)
}
testutil.ProtoDiff(t, "request not equal", exp, got, "id")
})
}
func TestCloneFromRequestLog(t *testing.T) {
t.Parallel()
reqLogID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
reqLogID := "foobar-req-log-id"
t.Run("without active project", func(t *testing.T) {
t.Parallel()
svc := sender.NewService(sender.Config{})
_, err := svc.CloneFromRequestLog(context.Background(), reqLogID)
_, err := svc.CloneFromRequestLog(context.Background(), &connect.Request[sender.CloneFromRequestLogRequest]{
Msg: &sender.CloneFromRequestLogRequest{
RequestLogId: reqLogID,
},
})
if !errors.Is(err, sender.ErrProjectIDMustBeSet) {
t.Fatalf("expected `sender.ErrProjectIDMustBeSet`, got: %v", err)
}
@ -162,24 +150,32 @@ func TestCloneFromRequestLog(t *testing.T) {
}
defer db.Close()
projectID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
err = db.UpsertProject(context.Background(), proj.Project{
ID: projectID,
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.RequestLog{
ID: reqLogID,
ProjectID: projectID,
URL: exampleURL,
Method: http.MethodPost,
Proto: "HTTP/1.1",
Header: http.Header{
"X-Foo": []string{"bar"},
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"},
},
},
Response: &httppb.Response{
Protocol: httppb.Protocol_PROTOCOL_HTTP20,
StatusCode: 200,
Status: "200 OK",
Body: []byte("foobar"),
},
Body: []byte("foobar"),
}
if err := db.StoreRequestLog(context.Background(), reqLog); err != nil {
@ -196,27 +192,29 @@ func TestCloneFromRequestLog(t *testing.T) {
svc.SetActiveProjectID(projectID)
exp := sender.Request{
SourceRequestLogID: reqLogID,
ProjectID: projectID,
URL: exampleURL,
Method: http.MethodPost,
Proto: sender.HTTPProto20,
Header: http.Header{
"X-Foo": []string{"bar"},
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"},
},
},
Body: []byte("foobar"),
}
got, err := svc.CloneFromRequestLog(context.Background(), reqLogID)
got, err := svc.CloneFromRequestLog(context.Background(), &connect.Request[sender.CloneFromRequestLogRequest]{
Msg: &sender.CloneFromRequestLogRequest{
RequestLogId: reqLogID,
},
})
if err != nil {
t.Fatalf("unexpected error cloning from request log: %v", err)
}
diff := cmp.Diff(exp, got, cmpopts.IgnoreFields(sender.Request{}, "ID"))
if diff != "" {
t.Fatalf("request not equal (-exp, +got):\n%v", diff)
}
testutil.ProtoDiff(t, "request not equal", exp, got.Msg.Request, "id")
})
}
@ -245,28 +243,27 @@ func TestSendRequest(t *testing.T) {
}))
defer ts.Close()
tsURL, _ := url.Parse(ts.URL)
projectID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
err = db.UpsertProject(context.Background(), proj.Project{
ID: projectID,
Settings: proj.Settings{},
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)
}
reqID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
req := sender.Request{
ID: reqID,
ProjectID: projectID,
URL: tsURL,
Method: http.MethodPost,
Proto: "HTTP/1.1",
Header: http.Header{
"X-Foo": []string{"bar"},
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"},
},
},
Body: []byte("foobar"),
}
if err := db.StoreSenderRequest(context.Background(), req); err != nil {
@ -281,26 +278,38 @@ func TestSendRequest(t *testing.T) {
})
svc.SetActiveProjectID(projectID)
exp := &reqlog.ResponseLog{
Proto: "HTTP/1.1",
StatusCode: http.StatusOK,
exp := &httppb.Response{
Protocol: httppb.Protocol_PROTOCOL_HTTP11,
StatusCode: 200,
Status: "200 OK",
Header: http.Header{
"Content-Length": []string{"3"},
"Content-Type": []string{"text/plain; charset=utf-8"},
"Date": []string{date},
"Foobar": []string{"baz"},
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"},
},
Body: []byte("baz"),
}
got, err := svc.SendRequest(context.Background(), reqID)
got, err := svc.SendRequest(context.Background(), &connect.Request[sender.SendRequestRequest]{
Msg: &sender.SendRequestRequest{
RequestId: reqID,
},
})
if err != nil {
t.Fatalf("unexpected error sending request: %v", err)
}
diff := cmp.Diff(exp, got.Response, cmpopts.IgnoreFields(sender.Request{}, "ID"))
if diff != "" {
t.Fatalf("request not equal (-exp, +got):\n%v", diff)
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 diff := cmp.Diff(exp, got.Msg.Request.HttpResponse, opts...); diff != "" {
t.Fatalf("response not equal (-exp, +got):\n%v", diff)
}
}

View File

@ -45,7 +45,3 @@ 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
}

54
pkg/testutil/testutil.go Normal file
View File

@ -0,0 +1,54 @@
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}
}

47
proto/http/http.proto Normal file
View File

@ -0,0 +1,47 @@
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;
}

104
proto/proj/proj.proto Normal file
View File

@ -0,0 +1,104 @@
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) {}
}

44
proto/reqlog/reqlog.proto Normal file
View File

@ -0,0 +1,44 @@
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) {}
}

12
proto/scope/scope.proto Normal file
View File

@ -0,0 +1,12 @@
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;
}

85
proto/sender/sender.proto Normal file
View File

@ -0,0 +1,85 @@
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) {}
}

View File

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