Restructure config and setup of handlers

- move initialization and dependency resolving to registration of protocol handlers
- replace file opening in HTTP mock with fs.FS - depends on having a FS that returns an io.ReadSeeker - could be worked around in the future
- update config for new routing rules to see how they look like in production
- extend grammar tests
This commit is contained in:
Peter 2021-04-26 22:35:28 +02:00
parent 45a6d11d5b
commit 9f53b01e49
Signed by: prskr
GPG key ID: C1DB5D2E8DB512F9
19 changed files with 248 additions and 215 deletions

View file

@ -10,27 +10,18 @@ import (
"gitlab.com/inetmock/inetmock/internal/app" "gitlab.com/inetmock/inetmock/internal/app"
"gitlab.com/inetmock/inetmock/internal/endpoint" "gitlab.com/inetmock/inetmock/internal/endpoint"
dns "gitlab.com/inetmock/inetmock/internal/endpoint/handler/dns/mock"
http "gitlab.com/inetmock/inetmock/internal/endpoint/handler/http/mock"
"gitlab.com/inetmock/inetmock/internal/endpoint/handler/http/proxy"
"gitlab.com/inetmock/inetmock/internal/endpoint/handler/metrics"
"gitlab.com/inetmock/inetmock/pkg/cert" "gitlab.com/inetmock/inetmock/pkg/cert"
) )
var ( var (
serverApp app.App serverApp app.App
cfg appConfig cfg appConfig
registrations = []endpoint.Registration{
http.AddHTTPMock,
dns.AddDNSMock,
proxy.AddHTTPProxy,
metrics.AddMetricsExporter,
}
) )
type Data struct { type Data struct {
PCAP string PCAP string
Audit string Audit string
FakeFiles string
} }
func (d *Data) setup() (err error) { func (d *Data) setup() (err error) {

View file

@ -1,10 +1,17 @@
package main package main
import ( import (
"io/fs"
"os"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"go.uber.org/zap" "go.uber.org/zap"
"gitlab.com/inetmock/inetmock/internal/endpoint" "gitlab.com/inetmock/inetmock/internal/endpoint"
dnsmock "gitlab.com/inetmock/inetmock/internal/endpoint/handler/dns/mock"
"gitlab.com/inetmock/inetmock/internal/endpoint/handler/http/mock"
"gitlab.com/inetmock/inetmock/internal/endpoint/handler/http/proxy"
"gitlab.com/inetmock/inetmock/internal/endpoint/handler/metrics"
"gitlab.com/inetmock/inetmock/internal/pcap" "gitlab.com/inetmock/inetmock/internal/pcap"
audit2 "gitlab.com/inetmock/inetmock/internal/pcap/consumers/audit" audit2 "gitlab.com/inetmock/inetmock/internal/pcap/consumers/audit"
"gitlab.com/inetmock/inetmock/internal/rpc" "gitlab.com/inetmock/inetmock/internal/rpc"
@ -39,12 +46,7 @@ func startINetMock(_ *cobra.Command, _ []string) error {
return err return err
} }
for _, registration := range registrations { fakeFileFS := os.DirFS(cfg.Data.FakeFiles)
if err = registration(registry); err != nil {
appLogger.Error("Failed to run registration", zap.Error(err))
return err
}
}
var certStore cert.Store var certStore cert.Store
if certStore, err = cert.NewDefaultStore(cfg.TLS, appLogger.Named("CertStore")); err != nil { if certStore, err = cert.NewDefaultStore(cfg.TLS, appLogger.Named("CertStore")); err != nil {
@ -57,8 +59,12 @@ func startINetMock(_ *cobra.Command, _ []string) error {
return err return err
} }
if err = setupEndpointHandlers(registry, appLogger, eventStream, certStore, fakeFileFS); err != nil {
appLogger.Error("Failed to run registration", zap.Error(err))
return err
}
var endpointOrchestrator = endpoint.NewOrchestrator( var endpointOrchestrator = endpoint.NewOrchestrator(
serverApp.Context(),
certStore, certStore,
registry, registry,
eventStream, eventStream,
@ -86,7 +92,7 @@ func startINetMock(_ *cobra.Command, _ []string) error {
} }
} }
errChan := endpointOrchestrator.StartEndpoints() errChan := endpointOrchestrator.StartEndpoints(serverApp.Context())
if err := rpcAPI.StartServer(); err != nil { if err := rpcAPI.StartServer(); err != nil {
serverApp.Shutdown() serverApp.Shutdown()
appLogger.Error( appLogger.Error(
@ -148,6 +154,22 @@ func setupEventStream(appLogger logging.Logger) (audit.EventStream, error) {
return evenStream, nil return evenStream, nil
} }
func setupEndpointHandlers(registry endpoint.HandlerRegistry, logger logging.Logger, emitter audit.Emitter, store cert.Store, fakeFileFS fs.FS) (err error) {
if err = mock.AddHTTPMock(registry, logger, emitter, fakeFileFS); err != nil {
return
}
if err = proxy.AddHTTPProxy(registry, logger, emitter, store); err != nil {
return
}
if err = dnsmock.AddDNSMock(registry, logger, emitter); err != nil {
return
}
if err = metrics.AddMetricsExporter(registry, logger); err != nil {
return
}
return nil
}
//nolint:deadcode //nolint:deadcode
func startAuditConsumer(eventStream audit.EventStream) error { func startAuditConsumer(eventStream audit.EventStream) error {
recorder := pcap.NewRecorder() recorder := pcap.NewRecorder()

View file

@ -1,45 +1,19 @@
x-response-rules: &httpResponseRules x-response-rules: &httpResponseRules
rules: rules:
- pattern: ".*\\.(?i)exe" - PathPattern("") => File("sample.exe")
matcher: Path - Header("Accept", "application/octet-stream") => File("sample.exe")
- pattern: "^application/octet-stream$" - Header("Accept", "image/jpeg") => File("default.jpg")
target: Accept - PathPattern(".*\\.(?i)(jpg|jpeg)") => File("default.jpg")
matcher: Header - Header("Accept", "image/png") => File("default.png")
response: ./assets/fakeFiles/sample.exe - PathPattern(".*\\.(?i)png") => File("default.png")
- pattern: "^image/jpeg$" - Header("Accept", "image/gif") => File("default.gif")
target: Accept - PathPattern(".*\\.(?i)gif") => File("default.gif")
matcher: Header - Header("Accept", "image/x-icon") => File("default.ico")
response: ./assets/fakeFiles/default.jpg - PathPattern(".*\\.(?i)ico") => File("default.ico")
- pattern: ".*\\.(?i)(jpg|jpeg)" - Header("Accept", "text/plain") => File("default.txt")
matcher: Path - PathPattern(".*\\.(?i)txt") => File("default.txt")
response: ./assets/fakeFiles/default.jpg - Header("Accept", "text/html") => File("default.html")
- pattern: "^image/png$" - PathPattern(".*") => File("default.html")
target: Accept
matcher: Header
response: ./assets/fakeFiles/default.png
- pattern: ".*\\.(?i)png"
matcher: Path
response: ./assets/fakeFiles/default.png
- pattern: ".*\\.(?i)gif"
matcher: Path
response: ./assets/fakeFiles/default.gif
- pattern: ".*\\.(?i)ico"
matcher: Path
response: ./assets/fakeFiles/default.ico
- pattern: "^text/plain$"
target: Accept
matcher: Header
response: ./assets/fakeFiles/default.txt
- pattern: ".*\\.(?i)txt"
matcher: Path
response: ./assets/fakeFiles/default.txt
- pattern: "^text/html$"
target: Accept
matcher: Header
response: ./assets/fakeFiles/default.html
- pattern: ".*"
matcher: Path
response: ./assets/fakeFiles/default.html
x-http-handlers: &httpHandlers x-http-handlers: &httpHandlers
endpoints: endpoints:
@ -57,6 +31,7 @@ x-http-handlers: &httpHandlers
data: data:
pcap: /var/lib/inetmock/data/pcap pcap: /var/lib/inetmock/data/pcap
audit: /var/lib/inetmock/data/audit audit: /var/lib/inetmock/data/audit
fakeFiles: ./assets/fakeFiles
api: api:
listen: unix:///var/run/inetmock.sock listen: unix:///var/run/inetmock.sock

View file

@ -6,24 +6,16 @@ import (
"context" "context"
"github.com/soheilhy/cmux" "github.com/soheilhy/cmux"
"gitlab.com/inetmock/inetmock/pkg/audit"
"gitlab.com/inetmock/inetmock/pkg/cert"
"gitlab.com/inetmock/inetmock/pkg/logging"
) )
type Lifecycle interface { type Lifecycle interface {
Name() string Name() string
Logger() logging.Logger
CertStore() cert.Store
Audit() audit.Emitter
Context() context.Context
Uplink() Uplink Uplink() Uplink
UnmarshalOptions(cfg interface{}) error UnmarshalOptions(cfg interface{}) error
} }
type ProtocolHandler interface { type ProtocolHandler interface {
Start(ctx Lifecycle) error Start(ctx context.Context, lifecycle Lifecycle) error
} }
type MultiplexHandler interface { type MultiplexHandler interface {

View file

@ -5,6 +5,8 @@ import (
"time" "time"
"go.uber.org/zap" "go.uber.org/zap"
"gitlab.com/inetmock/inetmock/pkg/logging"
) )
const ( const (
@ -17,19 +19,19 @@ type Endpoint struct {
uplink Uplink uplink Uplink
} }
func (e *Endpoint) Start(lifecycle Lifecycle) (err error) { func (e *Endpoint) Start(ctx context.Context, logger logging.Logger, lifecycle Lifecycle) (err error) {
startupResult := make(chan error) startupResult := make(chan error)
ctx, cancel := context.WithTimeout(lifecycle.Context(), startupTimeoutDuration) ctx, cancel := context.WithTimeout(ctx, startupTimeoutDuration)
defer cancel() defer cancel()
go func() { go func() {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
lifecycle.Logger().Fatal("Startup error recovered", zap.Any("recovered", r)) logger.Fatal("Startup error recovered", zap.Any("recovered", r))
} }
}() }()
startupResult <- e.Handler.Start(lifecycle) startupResult <- e.Handler.Start(ctx, lifecycle)
}() }()
select { select {

View file

@ -8,6 +8,7 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
"gitlab.com/inetmock/inetmock/internal/endpoint" "gitlab.com/inetmock/inetmock/internal/endpoint"
"gitlab.com/inetmock/inetmock/pkg/audit"
"gitlab.com/inetmock/inetmock/pkg/logging" "gitlab.com/inetmock/inetmock/pkg/logging"
) )
@ -15,17 +16,18 @@ const shutdownTimeout = 100 * time.Millisecond
type dnsHandler struct { type dnsHandler struct {
logger logging.Logger logger logging.Logger
emitter audit.Emitter
dnsServer *dns.Server dnsServer *dns.Server
} }
func (d *dnsHandler) Start(lifecycle endpoint.Lifecycle) error { func (d *dnsHandler) Start(ctx context.Context, lifecycle endpoint.Lifecycle) error {
var err error var err error
var options dnsOptions var options dnsOptions
if options, err = loadFromConfig(lifecycle); err != nil { if options, err = loadFromConfig(lifecycle); err != nil {
return err return err
} }
d.logger = lifecycle.Logger().With( d.logger = d.logger.With(
zap.String("handler_name", lifecycle.Name()), zap.String("handler_name", lifecycle.Name()),
zap.String("address", lifecycle.Uplink().Addr().String()), zap.String("address", lifecycle.Uplink().Addr().String()),
) )
@ -33,8 +35,8 @@ func (d *dnsHandler) Start(lifecycle endpoint.Lifecycle) error {
handler := &regexHandler{ handler := &regexHandler{
handlerName: lifecycle.Name(), handlerName: lifecycle.Name(),
fallback: options.Fallback, fallback: options.Fallback,
logger: lifecycle.Logger(), logger: d.logger,
auditEmitter: lifecycle.Audit(), auditEmitter: d.emitter,
} }
for _, rule := range options.Rules { for _, rule := range options.Rules {
@ -59,6 +61,7 @@ func (d *dnsHandler) Start(lifecycle endpoint.Lifecycle) error {
} }
go d.startServer() go d.startServer()
go d.shutdownOnEnd(ctx)
return nil return nil
} }

View file

@ -6,6 +6,8 @@ import (
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"gitlab.com/inetmock/inetmock/internal/endpoint" "gitlab.com/inetmock/inetmock/internal/endpoint"
"gitlab.com/inetmock/inetmock/pkg/audit"
"gitlab.com/inetmock/inetmock/pkg/logging"
"gitlab.com/inetmock/inetmock/pkg/metrics" "gitlab.com/inetmock/inetmock/pkg/metrics"
) )
@ -21,7 +23,7 @@ var (
initLock sync.Locker = new(sync.Mutex) initLock sync.Locker = new(sync.Mutex)
) )
func AddDNSMock(registry endpoint.HandlerRegistry) error { func AddDNSMock(registry endpoint.HandlerRegistry, logger logging.Logger, emitter audit.Emitter) error {
initLock.Lock() initLock.Lock()
defer initLock.Unlock() defer initLock.Unlock()
@ -61,7 +63,10 @@ func AddDNSMock(registry endpoint.HandlerRegistry) error {
} }
registry.RegisterHandler(name, func() endpoint.ProtocolHandler { registry.RegisterHandler(name, func() endpoint.ProtocolHandler {
return &dnsHandler{} return &dnsHandler{
logger: logger,
emitter: emitter,
}
}) })
return nil return nil

View file

@ -3,6 +3,7 @@ package mock
import ( import (
"context" "context"
"errors" "errors"
"io/fs"
"net" "net"
"net/http" "net/http"
@ -11,6 +12,7 @@ import (
"gitlab.com/inetmock/inetmock/internal/endpoint" "gitlab.com/inetmock/inetmock/internal/endpoint"
imHttp "gitlab.com/inetmock/inetmock/internal/endpoint/handler/http" imHttp "gitlab.com/inetmock/inetmock/internal/endpoint/handler/http"
"gitlab.com/inetmock/inetmock/pkg/audit"
"gitlab.com/inetmock/inetmock/pkg/logging" "gitlab.com/inetmock/inetmock/pkg/logging"
) )
@ -21,16 +23,18 @@ const (
) )
type httpHandler struct { type httpHandler struct {
logger logging.Logger logger logging.Logger
server *http.Server fakeFileFS fs.FS
server *http.Server
emitter audit.Emitter
} }
func (p *httpHandler) Matchers() []cmux.Matcher { func (p *httpHandler) Matchers() []cmux.Matcher {
return []cmux.Matcher{cmux.HTTP1()} return []cmux.Matcher{cmux.HTTP1()}
} }
func (p *httpHandler) Start(lifecycle endpoint.Lifecycle) error { func (p *httpHandler) Start(ctx context.Context, lifecycle endpoint.Lifecycle) error {
p.logger = lifecycle.Logger().With( p.logger = p.logger.With(
zap.String("protocol_handler", name), zap.String("protocol_handler", name),
) )
@ -45,9 +49,10 @@ func (p *httpHandler) Start(lifecycle endpoint.Lifecycle) error {
) )
router := &RegexHandler{ router := &RegexHandler{
logger: p.logger,
emitter: lifecycle.Audit(),
handlerName: lifecycle.Name(), handlerName: lifecycle.Name(),
logger: p.logger,
emitter: p.emitter,
fakeFileFS: p.fakeFileFS,
} }
p.server = &http.Server{ p.server = &http.Server{
Handler: router, Handler: router,
@ -59,7 +64,7 @@ func (p *httpHandler) Start(lifecycle endpoint.Lifecycle) error {
} }
go p.startServer(lifecycle.Uplink().Listener) go p.startServer(lifecycle.Uplink().Listener)
go p.shutdownOnCancel(lifecycle.Context()) go p.shutdownOnCancel(ctx)
return nil return nil
} }

View file

@ -1,9 +1,10 @@
package mock package mock
import ( import (
"io"
"io/fs"
"math/rand" "math/rand"
"net/http" "net/http"
"os"
"path" "path"
"strconv" "strconv"
"time" "time"
@ -27,13 +28,15 @@ type RegexHandler struct {
logger logging.Logger logger logging.Logger
routes []*route routes []*route
emitter audit.Emitter emitter audit.Emitter
fakeFileFS fs.FS
} }
func NewRegexHandler(name string, logger logging.Logger, emitter audit.Emitter) *RegexHandler { func NewRegexHandler(name string, logger logging.Logger, emitter audit.Emitter, fakeFileFS fs.FS) *RegexHandler {
return &RegexHandler{ return &RegexHandler{
handlerName: name, handlerName: name,
logger: logger, logger: logger,
emitter: emitter, emitter: emitter,
fakeFileFS: fakeFileFS,
} }
} }
@ -68,23 +71,32 @@ func (h *RegexHandler) AddRouteRule(rule TargetRule) {
h.Handler(rule, emittingFileHandler{ h.Handler(rule, emittingFileHandler{
emitter: h.emitter, emitter: h.emitter,
targetPath: rule.response, targetPath: rule.response,
fs: h.fakeFileFS,
}) })
} }
type emittingFileHandler struct { type emittingFileHandler struct {
emitter audit.Emitter emitter audit.Emitter
targetPath string targetPath string
fs fs.FS
} }
func (f emittingFileHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { func (f emittingFileHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
f.emitter.Emit(imHttp.EventFromRequest(request, v1.AppProtocol_APP_PROTOCOL_HTTP)) f.emitter.Emit(imHttp.EventFromRequest(request, v1.AppProtocol_APP_PROTOCOL_HTTP))
file, err := os.Open(f.targetPath) file, err := f.fs.Open(f.targetPath)
if err != nil { if err != nil {
http.Error(writer, err.Error(), 500) http.Error(writer, err.Error(), 500)
} }
var rs io.ReadSeeker
var ok bool
if rs, ok = file.(io.ReadSeeker); !ok {
http.Error(writer, "internal server error", 500)
}
defer func() { defer func() {
_ = file.Close() _ = file.Close()
}() }()
//nolint:gosec //nolint:gosec
http.ServeContent(writer, request, path.Base(request.RequestURI), time.Now().Add(-(time.Duration(rand.Int()) * time.Millisecond)), file) http.ServeContent(writer, request, path.Base(request.RequestURI), time.Now().Add(-(time.Duration(rand.Int()) * time.Millisecond)), rs)
} }

View file

@ -1,13 +1,14 @@
package mock_test package mock_test
import ( import (
"crypto/sha256"
"encoding/hex"
"io" "io"
"io/fs"
"net/http" "net/http"
"net/url" "net/url"
"path/filepath" "strings"
"testing" "testing"
"testing/fstest"
"time"
"github.com/golang/mock/gomock" "github.com/golang/mock/gomock"
"github.com/maxatome/go-testdeep/td" "github.com/maxatome/go-testdeep/td"
@ -19,6 +20,19 @@ import (
"gitlab.com/inetmock/inetmock/pkg/logging" "gitlab.com/inetmock/inetmock/pkg/logging"
) )
var (
defaultHTMLContent = `<html>
<head>
<title>INetSim default HTML page</title>
</head>
<body>
<p></p>
<p align="center">This is the default HTML page for INetMock HTTP mock handler.</p>
<p align="center">This file is an HTML document.</p>
</body>
</html>`
)
//nolint:funlen //nolint:funlen
func TestRegexpHandler_ServeHTTP(t *testing.T) { func TestRegexpHandler_ServeHTTP(t *testing.T) {
t.Parallel() t.Parallel()
@ -28,25 +42,32 @@ func TestRegexpHandler_ServeHTTP(t *testing.T) {
type fields struct { type fields struct {
rules []mock.TargetRule rules []mock.TargetRule
emitterSetup func(tb testing.TB, ctrl *gomock.Controller) audit.Emitter emitterSetup func(tb testing.TB, ctrl *gomock.Controller) audit.Emitter
fakeFileFS fs.FS
} }
type args struct { type args struct {
req *http.Request req *http.Request
} }
tests := []struct { tests := []struct {
name string name string
fields fields fields fields
args args args args
wantErr bool wantErr bool
wantStatus interface{} wantStatus interface{}
wantRespHash string want string
}{ }{
{ {
name: "GET /index.html", name: "GET /index.html",
fields: fields{ fields: fields{
rules: []mock.TargetRule{ rules: []mock.TargetRule{
mock.MustPathTargetRule(`\.(?i)(htm|html)$`, filepath.Join("testdata", "default.html")), mock.MustPathTargetRule(`\.(?i)(htm|html)$`, "default.html"),
}, },
emitterSetup: defaultEmitter, emitterSetup: defaultEmitter,
fakeFileFS: fstest.MapFS{
"default.html": &fstest.MapFile{
Data: []byte(defaultHTMLContent),
ModTime: time.Now().Add(-1337 * time.Second),
},
},
}, },
args: args{ args: args{
req: &http.Request{ req: &http.Request{
@ -54,16 +75,22 @@ func TestRegexpHandler_ServeHTTP(t *testing.T) {
Method: http.MethodGet, Method: http.MethodGet,
}, },
}, },
wantRespHash: "c2a3f8995831dd1e79cb753619a55752692168f6cf846b07405f2070492f481c", want: defaultHTMLContent,
wantStatus: td.Between(200, 299), wantStatus: td.Between(200, 299),
}, },
{ {
name: "GET /profile.htm", name: "GET /profile.htm",
fields: fields{ fields: fields{
rules: []mock.TargetRule{ rules: []mock.TargetRule{
mock.MustPathTargetRule(`\.(?i)(htm|html)$`, filepath.Join("testdata", "default.html")), mock.MustPathTargetRule(`\.(?i)(htm|html)$`, "default.html"),
}, },
emitterSetup: defaultEmitter, emitterSetup: defaultEmitter,
fakeFileFS: fstest.MapFS{
"default.html": &fstest.MapFile{
Data: []byte(defaultHTMLContent),
ModTime: time.Now().Add(-1337 * time.Second),
},
},
}, },
args: args{ args: args{
req: &http.Request{ req: &http.Request{
@ -71,17 +98,23 @@ func TestRegexpHandler_ServeHTTP(t *testing.T) {
Method: http.MethodGet, Method: http.MethodGet,
}, },
}, },
wantRespHash: "c2a3f8995831dd1e79cb753619a55752692168f6cf846b07405f2070492f481c", want: defaultHTMLContent,
wantStatus: td.Between(200, 299), wantStatus: td.Between(200, 299),
}, },
{ {
name: "GET with Accept: text/html", name: "GET with Accept: text/html",
fields: fields{ fields: fields{
rules: []mock.TargetRule{ rules: []mock.TargetRule{
mock.MustPathTargetRule(`\.(?i)(htm|html)$`, filepath.Join("testdata", "default.html")), mock.MustPathTargetRule(`\.(?i)(htm|html)$`, "default.html"),
mock.MustHeaderTargetRule("Accept", "(?i)text/html", filepath.Join("testdata", "default.html")), mock.MustHeaderTargetRule("Accept", "(?i)text/html", "default.html"),
}, },
emitterSetup: defaultEmitter, emitterSetup: defaultEmitter,
fakeFileFS: fstest.MapFS{
"default.html": &fstest.MapFile{
Data: []byte(defaultHTMLContent),
ModTime: time.Now().Add(-1337 * time.Second),
},
},
}, },
args: args{ args: args{
req: &http.Request{ req: &http.Request{
@ -92,8 +125,8 @@ func TestRegexpHandler_ServeHTTP(t *testing.T) {
Method: http.MethodGet, Method: http.MethodGet,
}, },
}, },
wantRespHash: "c2a3f8995831dd1e79cb753619a55752692168f6cf846b07405f2070492f481c", want: defaultHTMLContent,
wantStatus: td.Between(200, 299), wantStatus: td.Between(200, 299),
}, },
} }
for _, tc := range tests { for _, tc := range tests {
@ -102,7 +135,7 @@ func TestRegexpHandler_ServeHTTP(t *testing.T) {
t.Parallel() t.Parallel()
logger := logging.CreateTestLogger(t) logger := logging.CreateTestLogger(t)
ctrl := gomock.NewController(t) ctrl := gomock.NewController(t)
h := mock.NewRegexHandler(t.Name(), logger, tt.fields.emitterSetup(t, ctrl)) h := mock.NewRegexHandler(t.Name(), logger, tt.fields.emitterSetup(t, ctrl), tt.fields.fakeFileFS)
for _, rule := range tt.fields.rules { for _, rule := range tt.fields.rules {
h.AddRouteRule(rule) h.AddRouteRule(rule)
@ -121,11 +154,10 @@ func TestRegexpHandler_ServeHTTP(t *testing.T) {
td.Cmp(t, resp.StatusCode, tt.wantStatus) td.Cmp(t, resp.StatusCode, tt.wantStatus)
sha256Hash := sha256.New() builder := new(strings.Builder)
_, err = io.Copy(sha256Hash, resp.Body) _, err = io.Copy(builder, resp.Body)
td.CmpNoError(t, err) td.CmpNoError(t, err)
computedHash := hex.EncodeToString(sha256Hash.Sum(nil)) td.Cmp(t, builder.String(), tt.want)
td.Cmp(t, computedHash, tt.wantRespHash)
}) })
} }
} }

View file

@ -1,11 +1,14 @@
package mock package mock
import ( import (
"io/fs"
"sync" "sync"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"gitlab.com/inetmock/inetmock/internal/endpoint" "gitlab.com/inetmock/inetmock/internal/endpoint"
"gitlab.com/inetmock/inetmock/pkg/audit"
"gitlab.com/inetmock/inetmock/pkg/logging"
"gitlab.com/inetmock/inetmock/pkg/metrics" "gitlab.com/inetmock/inetmock/pkg/metrics"
) )
@ -46,13 +49,16 @@ func InitMetrics() error {
return nil return nil
} }
func AddHTTPMock(registry endpoint.HandlerRegistry) (err error) { func AddHTTPMock(registry endpoint.HandlerRegistry, logger logging.Logger, emitter audit.Emitter, fakeFileFS fs.FS) (err error) {
if err := InitMetrics(); err != nil { if err := InitMetrics(); err != nil {
return err return err
} }
registry.RegisterHandler(name, func() endpoint.ProtocolHandler { registry.RegisterHandler(name, func() endpoint.ProtocolHandler {
return &httpHandler{} return &httpHandler{
emitter: emitter,
logger: logger,
}
}) })
return return

View file

@ -12,6 +12,8 @@ import (
"gitlab.com/inetmock/inetmock/internal/endpoint" "gitlab.com/inetmock/inetmock/internal/endpoint"
imHttp "gitlab.com/inetmock/inetmock/internal/endpoint/handler/http" imHttp "gitlab.com/inetmock/inetmock/internal/endpoint/handler/http"
"gitlab.com/inetmock/inetmock/pkg/audit"
"gitlab.com/inetmock/inetmock/pkg/cert"
"gitlab.com/inetmock/inetmock/pkg/logging" "gitlab.com/inetmock/inetmock/pkg/logging"
) )
@ -20,16 +22,18 @@ const (
) )
type httpProxy struct { type httpProxy struct {
logger logging.Logger logger logging.Logger
proxy *goproxy.ProxyHttpServer proxy *goproxy.ProxyHttpServer
server *http.Server certStore cert.Store
emitter audit.Emitter
server *http.Server
} }
func (h *httpProxy) Matchers() []cmux.Matcher { func (h *httpProxy) Matchers() []cmux.Matcher {
return []cmux.Matcher{cmux.HTTP1()} return []cmux.Matcher{cmux.HTTP1()}
} }
func (h *httpProxy) Start(lifecycle endpoint.Lifecycle) error { func (h *httpProxy) Start(ctx context.Context, lifecycle endpoint.Lifecycle) error {
var opts httpProxyOptions var opts httpProxyOptions
if err := lifecycle.UnmarshalOptions(&opts); err != nil { if err := lifecycle.UnmarshalOptions(&opts); err != nil {
return err return err
@ -44,25 +48,25 @@ func (h *httpProxy) Start(lifecycle endpoint.Lifecycle) error {
zap.String("address", lifecycle.Uplink().Addr().String()), zap.String("address", lifecycle.Uplink().Addr().String()),
) )
tlsConfig := lifecycle.CertStore().TLSConfig() tlsConfig := h.certStore.TLSConfig()
proxyHandler := &proxyHTTPHandler{ proxyHandler := &proxyHTTPHandler{
handlerName: lifecycle.Name(), handlerName: lifecycle.Name(),
options: opts, options: opts,
logger: h.logger, logger: h.logger,
emitter: lifecycle.Audit(), emitter: h.emitter,
} }
proxyHTTPSHandler := &proxyHTTPSHandler{ proxyHTTPSHandler := &proxyHTTPSHandler{
options: opts, options: opts,
tlsConfig: tlsConfig, tlsConfig: tlsConfig,
emitter: lifecycle.Audit(), emitter: h.emitter,
} }
h.proxy.OnRequest().Do(proxyHandler) h.proxy.OnRequest().Do(proxyHandler)
h.proxy.OnRequest().HandleConnect(proxyHTTPSHandler) h.proxy.OnRequest().HandleConnect(proxyHTTPSHandler)
go h.startProxy(lifecycle.Uplink().Listener) go h.startProxy(lifecycle.Uplink().Listener)
go h.shutdownOnContextDone(lifecycle.Context()) go h.shutdownOnContextDone(ctx)
return nil return nil
} }

View file

@ -6,6 +6,8 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
"gitlab.com/inetmock/inetmock/internal/endpoint" "gitlab.com/inetmock/inetmock/internal/endpoint"
"gitlab.com/inetmock/inetmock/pkg/audit"
"gitlab.com/inetmock/inetmock/pkg/cert"
"gitlab.com/inetmock/inetmock/pkg/logging" "gitlab.com/inetmock/inetmock/pkg/logging"
"gitlab.com/inetmock/inetmock/pkg/metrics" "gitlab.com/inetmock/inetmock/pkg/metrics"
) )
@ -15,11 +17,7 @@ var (
requestDurationHistogram *prometheus.HistogramVec requestDurationHistogram *prometheus.HistogramVec
) )
func AddHTTPProxy(registry endpoint.HandlerRegistry) (err error) { func AddHTTPProxy(registry endpoint.HandlerRegistry, logger logging.Logger, emitter audit.Emitter, store cert.Store) (err error) {
var logger logging.Logger
if logger, err = logging.CreateLogger(); err != nil {
return
}
logger = logger.With( logger = logger.With(
zap.String("protocol_handler", name), zap.String("protocol_handler", name),
) )
@ -30,8 +28,10 @@ func AddHTTPProxy(registry endpoint.HandlerRegistry) (err error) {
registry.RegisterHandler(name, func() endpoint.ProtocolHandler { registry.RegisterHandler(name, func() endpoint.ProtocolHandler {
return &httpProxy{ return &httpProxy{
logger: logger, logger: logger,
proxy: goproxy.NewProxyHttpServer(), emitter: emitter,
certStore: store,
proxy: goproxy.NewProxyHttpServer(),
} }
}) })

View file

@ -1,6 +1,7 @@
package metrics package metrics
import ( import (
"context"
"errors" "errors"
"net/http" "net/http"
@ -20,7 +21,7 @@ type metricsExporter struct {
server *http.Server server *http.Server
} }
func (m *metricsExporter) Start(lifecycle endpoint.Lifecycle) error { func (m *metricsExporter) Start(ctx context.Context, lifecycle endpoint.Lifecycle) error {
var exporterOptions metricsExporterOptions var exporterOptions metricsExporterOptions
if err := lifecycle.UnmarshalOptions(&exporterOptions); err != nil { if err := lifecycle.UnmarshalOptions(&exporterOptions); err != nil {
return err return err
@ -47,7 +48,7 @@ func (m *metricsExporter) Start(lifecycle endpoint.Lifecycle) error {
}() }()
go func() { go func() {
<-lifecycle.Context().Done() <-ctx.Done()
if err := m.server.Close(); err != nil && !errors.Is(err, http.ErrServerClosed) { if err := m.server.Close(); err != nil && !errors.Is(err, http.ErrServerClosed) {
m.logger.Error("failed to stop metrics server", zap.Error(err)) m.logger.Error("failed to stop metrics server", zap.Error(err))
} }

View file

@ -7,11 +7,7 @@ import (
"gitlab.com/inetmock/inetmock/pkg/logging" "gitlab.com/inetmock/inetmock/pkg/logging"
) )
func AddMetricsExporter(registry endpoint.HandlerRegistry) (err error) { func AddMetricsExporter(registry endpoint.HandlerRegistry, logger logging.Logger) (err error) {
var logger logging.Logger
if logger, err = logging.CreateLogger(); err != nil {
return
}
logger = logger.With( logger = logger.With(
zap.String("protocol_handler", name), zap.String("protocol_handler", name),
) )

View file

@ -1,40 +1,22 @@
package endpoint package endpoint
import ( import (
"context"
"github.com/mitchellh/mapstructure" "github.com/mitchellh/mapstructure"
"gitlab.com/inetmock/inetmock/pkg/audit"
"gitlab.com/inetmock/inetmock/pkg/cert"
"gitlab.com/inetmock/inetmock/pkg/logging"
) )
type endpointLifecycle struct { type endpointLifecycle struct {
endpointName string endpointName string
ctx context.Context
logger logging.Logger
certStore cert.Store
emitter audit.Emitter
uplink Uplink uplink Uplink
opts map[string]interface{} opts map[string]interface{}
} }
func NewEndpointLifecycleFromContext( func NewEndpointLifecycle(
endpointName string, endpointName string,
ctx context.Context,
logger logging.Logger,
certStore cert.Store,
emitter audit.Emitter,
uplink Uplink, uplink Uplink,
opts map[string]interface{}, opts map[string]interface{},
) Lifecycle { ) Lifecycle {
return &endpointLifecycle{ return &endpointLifecycle{
endpointName: endpointName, endpointName: endpointName,
ctx: ctx,
logger: logger,
certStore: certStore,
emitter: emitter,
uplink: uplink, uplink: uplink,
opts: opts, opts: opts,
} }
@ -48,22 +30,6 @@ func (e *endpointLifecycle) Uplink() Uplink {
return e.uplink return e.uplink
} }
func (e *endpointLifecycle) Logger() logging.Logger {
return e.logger
}
func (e *endpointLifecycle) CertStore() cert.Store {
return e.certStore
}
func (e *endpointLifecycle) Audit() audit.Emitter {
return e.emitter
}
func (e *endpointLifecycle) Context() context.Context {
return e.ctx
}
func (e *endpointLifecycle) UnmarshalOptions(cfg interface{}) error { func (e *endpointLifecycle) UnmarshalOptions(cfg interface{}) error {
return mapstructure.Decode(e.opts, cfg) return mapstructure.Decode(e.opts, cfg)
} }

View file

@ -18,18 +18,16 @@ var (
type Orchestrator interface { type Orchestrator interface {
RegisterListener(spec ListenerSpec) error RegisterListener(spec ListenerSpec) error
StartEndpoints() (errChan chan error) StartEndpoints(ctx context.Context) (errChan chan error)
} }
func NewOrchestrator( func NewOrchestrator(
appCtx context.Context,
certStore cert.Store, certStore cert.Store,
registry HandlerRegistry, registry HandlerRegistry,
emitter audit.Emitter, emitter audit.Emitter,
logger logging.Logger, logger logging.Logger,
) Orchestrator { ) Orchestrator {
return &orchestrator{ return &orchestrator{
appCtx: appCtx,
registry: registry, registry: registry,
logger: logger, logger: logger,
certStore: certStore, certStore: certStore,
@ -38,7 +36,6 @@ func NewOrchestrator(
} }
type orchestrator struct { type orchestrator struct {
appCtx context.Context
registry HandlerRegistry registry HandlerRegistry
logger logging.Logger logger logging.Logger
certStore cert.Store certStore cert.Store
@ -68,24 +65,20 @@ func (e *orchestrator) RegisterListener(spec ListenerSpec) (err error) {
return return
} }
func (e *orchestrator) StartEndpoints() chan error { func (e *orchestrator) StartEndpoints(ctx context.Context) chan error {
var errChan = make(chan error) var errChan = make(chan error)
for _, epListener := range e.endpointListeners { for _, epListener := range e.endpointListeners {
endpointLogger := e.logger.With( endpointLogger := e.logger.With(
zap.String("epListener", epListener.name), zap.String("epListener", epListener.name),
) )
endpointLogger.Info("Starting epListener") endpointLogger.Info("Starting epListener")
lifecycle := NewEndpointLifecycleFromContext( lifecycle := NewEndpointLifecycle(
epListener.name, epListener.name,
e.appCtx,
e.logger.With(zap.String("epListener", epListener.name)),
e.certStore,
e.emitter,
epListener.uplink, epListener.uplink,
epListener.Options, epListener.Options,
) )
if err := epListener.Start(lifecycle); err == nil { if err := epListener.Start(ctx, e.logger.With(zap.String("epListener", epListener.name)), lifecycle); err == nil {
endpointLogger.Info("successfully started epListener") endpointLogger.Info("successfully started epListener")
} else { } else {
endpointLogger.Error("error occurred during epListener startup - will be skipped for now") endpointLogger.Error("error occurred during epListener startup - will be skipped for now")

View file

@ -2,43 +2,61 @@ package endpoint_test
import ( import (
"testing" "testing"
"testing/fstest"
"github.com/golang/mock/gomock"
"github.com/maxatome/go-testdeep/td" "github.com/maxatome/go-testdeep/td"
"gitlab.com/inetmock/inetmock/internal/endpoint" "gitlab.com/inetmock/inetmock/internal/endpoint"
dnsmock "gitlab.com/inetmock/inetmock/internal/endpoint/handler/dns/mock" dnsmock "gitlab.com/inetmock/inetmock/internal/endpoint/handler/dns/mock"
httpmock "gitlab.com/inetmock/inetmock/internal/endpoint/handler/http/mock" httpmock "gitlab.com/inetmock/inetmock/internal/endpoint/handler/http/mock"
audit_mock "gitlab.com/inetmock/inetmock/internal/mock/audit"
"gitlab.com/inetmock/inetmock/pkg/logging"
) )
func Test_handlerRegistry_AvailableHandlers(t *testing.T) { func Test_handlerRegistry_AvailableHandlers(t *testing.T) {
t.Parallel() t.Parallel()
tests := []struct { tests := []struct {
name string name string
handlerRegistry endpoint.HandlerRegistry handlerRegistrySetup func(tb testing.TB, ctrl *gomock.Controller) endpoint.HandlerRegistry
wantAvailableHandlers interface{} wantAvailableHandlers interface{}
}{ }{
{ {
name: "Empty registry", name: "Empty registry",
handlerRegistry: endpoint.NewHandlerRegistry(), handlerRegistrySetup: func(testing.TB, *gomock.Controller) endpoint.HandlerRegistry {
return endpoint.NewHandlerRegistry()
},
wantAvailableHandlers: td.Nil(), wantAvailableHandlers: td.Nil(),
}, },
{ {
name: "Single handler registered", name: "Single handler registered",
handlerRegistry: func() endpoint.HandlerRegistry { handlerRegistrySetup: func(tb testing.TB, ctrl *gomock.Controller) endpoint.HandlerRegistry {
tb.Helper()
registry := endpoint.NewHandlerRegistry() registry := endpoint.NewHandlerRegistry()
_ = httpmock.AddHTTPMock(registry) logger := logging.CreateTestLogger(tb)
emitter := audit_mock.NewMockEmitter(ctrl)
if err := httpmock.AddHTTPMock(registry, logger, emitter, new(fstest.MapFS)); err != nil {
tb.Fatalf("AddHTTPMock() error = %v", err)
}
return registry return registry
}(), },
wantAvailableHandlers: td.Set(endpoint.HandlerReference("http_mock")), wantAvailableHandlers: td.Set(endpoint.HandlerReference("http_mock")),
}, },
{ {
name: "Multiple handler registered", name: "Multiple handler registered",
handlerRegistry: func() endpoint.HandlerRegistry { handlerRegistrySetup: func(tb testing.TB, ctrl *gomock.Controller) endpoint.HandlerRegistry {
tb.Helper()
registry := endpoint.NewHandlerRegistry() registry := endpoint.NewHandlerRegistry()
_ = httpmock.AddHTTPMock(registry) logger := logging.CreateTestLogger(tb)
_ = dnsmock.AddDNSMock(registry) emitter := audit_mock.NewMockEmitter(ctrl)
if err := httpmock.AddHTTPMock(registry, logger, emitter, new(fstest.MapFS)); err != nil {
tb.Fatalf("AddHTTPMock() error = %v", err)
}
if err := dnsmock.AddDNSMock(registry, logger, emitter); err != nil {
tb.Fatalf("AddHTTPMock() error = %v", err)
}
return registry return registry
}(), },
wantAvailableHandlers: td.Set( wantAvailableHandlers: td.Set(
endpoint.HandlerReference("dns_mock"), endpoint.HandlerReference("dns_mock"),
endpoint.HandlerReference("http_mock"), endpoint.HandlerReference("http_mock"),
@ -49,7 +67,8 @@ func Test_handlerRegistry_AvailableHandlers(t *testing.T) {
tt := tc tt := tc
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
t.Parallel() t.Parallel()
gotAvailableHandlers := tt.handlerRegistry.AvailableHandlers() ctrl := gomock.NewController(t)
gotAvailableHandlers := tt.handlerRegistrySetup(t, ctrl).AvailableHandlers()
td.Cmp(t, gotAvailableHandlers, tt.wantAvailableHandlers) td.Cmp(t, gotAvailableHandlers, tt.wantAvailableHandlers)
}) })
} }
@ -58,26 +77,34 @@ func Test_handlerRegistry_AvailableHandlers(t *testing.T) {
func Test_handlerRegistry_HandlerForName(t *testing.T) { func Test_handlerRegistry_HandlerForName(t *testing.T) {
t.Parallel() t.Parallel()
tests := []struct { tests := []struct {
name string name string
handlerRegistry endpoint.HandlerRegistry handlerRegistrySetup func(tb testing.TB, ctrl *gomock.Controller) endpoint.HandlerRegistry
handlerRef endpoint.HandlerReference handlerRef endpoint.HandlerReference
wantInstance interface{} wantInstance interface{}
wantOk bool wantOk bool
}{ }{
{ {
name: "Empty registry", name: "Empty registry",
handlerRegistry: endpoint.NewHandlerRegistry(), handlerRegistrySetup: func(tb testing.TB, _ *gomock.Controller) endpoint.HandlerRegistry {
handlerRef: "http_mock", tb.Helper()
wantInstance: nil, return endpoint.NewHandlerRegistry()
wantOk: false, },
handlerRef: "http_mock",
wantInstance: nil,
wantOk: false,
}, },
{ {
name: "Registry with HTTP mock registered", name: "Registry with HTTP mock registered",
handlerRegistry: func() endpoint.HandlerRegistry { handlerRegistrySetup: func(tb testing.TB, ctrl *gomock.Controller) endpoint.HandlerRegistry {
tb.Helper()
registry := endpoint.NewHandlerRegistry() registry := endpoint.NewHandlerRegistry()
_ = httpmock.AddHTTPMock(registry) logger := logging.CreateTestLogger(tb)
emitter := audit_mock.NewMockEmitter(ctrl)
if err := httpmock.AddHTTPMock(registry, logger, emitter, new(fstest.MapFS)); err != nil {
tb.Fatalf("AddHTTPMock() error = %v", err)
}
return registry return registry
}(), },
handlerRef: "http_mock", handlerRef: "http_mock",
wantInstance: td.NotNil(), wantInstance: td.NotNil(),
wantOk: true, wantOk: true,
@ -87,7 +114,8 @@ func Test_handlerRegistry_HandlerForName(t *testing.T) {
tt := tc tt := tc
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
t.Parallel() t.Parallel()
gotInstance, gotOk := tt.handlerRegistry.HandlerForName(tt.handlerRef) ctrl := gomock.NewController(t)
gotInstance, gotOk := tt.handlerRegistrySetup(t, ctrl).HandlerForName(tt.handlerRef)
td.Cmp(t, gotInstance, tt.wantInstance) td.Cmp(t, gotInstance, tt.wantInstance)
td.Cmp(t, gotOk, tt.wantOk) td.Cmp(t, gotOk, tt.wantOk)
}) })

View file

@ -22,11 +22,11 @@ func TestParse(t *testing.T) {
{ {
name: "Terminator only - string argument", name: "Terminator only - string argument",
args: args{ args: args{
rule: `=> ReturnFile("default.html")`, rule: `=> File("default.html")`,
}, },
want: &Routing{ want: &Routing{
Terminator: &Method{ Terminator: &Method{
Name: "ReturnFile", Name: "File",
Params: []Param{ Params: []Param{
{ {
String: stringRef("default.html"), String: stringRef("default.html"),
@ -73,7 +73,7 @@ func TestParse(t *testing.T) {
{ {
name: "path pattern and terminator", name: "path pattern and terminator",
args: args{ args: args{
rule: `PathPattern("/index.html") => ReturnFile("default.html")`, rule: `PathPattern(".*\\.(?i)png") => ReturnFile("default.html")`,
}, },
want: &Routing{ want: &Routing{
Terminator: &Method{ Terminator: &Method{
@ -90,7 +90,7 @@ func TestParse(t *testing.T) {
Name: "PathPattern", Name: "PathPattern",
Params: []Param{ Params: []Param{
{ {
String: stringRef("/index.html"), String: stringRef(`.*\.(?i)png`),
}, },
}, },
}, },