From a720b0ee41f301edfa693f2017cea7c29bf9241f Mon Sep 17 00:00:00 2001 From: Peter Kurfer Date: Wed, 1 Apr 2020 04:08:21 +0200 Subject: [PATCH] Initial working version * supports HTTP * support TLS interception e.g. for HTTPS * support CA generation via cli * first draft of plugin API * support commands from plugins * includes Dockerfile * includes basic configuration --- .dockerignore | 18 ++ .gitattributes | 1 + .gitignore | 17 ++ Dockerfile | 43 ++++ Makefile | 64 ++++++ assets/fakeFiles/default.gif | 3 + assets/fakeFiles/default.html | 3 + assets/fakeFiles/default.ico | 3 + assets/fakeFiles/default.jpg | 3 + assets/fakeFiles/default.png | 3 + assets/fakeFiles/default.txt | 3 + config.yaml | 41 ++++ go.mod | 12 + go.sum | 187 ++++++++++++++++ internal/cmd/init.go | 46 ++++ internal/cmd/plugins.go | 22 ++ internal/cmd/root.go | 111 +++++++++ internal/config/constants.go | 6 + internal/config/handler_config.go | 49 ++++ internal/config/loading.go | 51 +++++ internal/plugins/loading.go | 78 +++++++ main.go | 19 ++ pkg/api/protocol_handler.go | 16 ++ pkg/logging/factory.go | 42 ++++ pkg/path/helpers.go | 19 ++ pkg/plugins/http_mock/Makefile | 43 ++++ pkg/plugins/http_mock/main.go | 116 ++++++++++ pkg/plugins/http_mock/protocol_options.go | 33 +++ pkg/plugins/http_mock/regex_router.go | 34 +++ pkg/plugins/tls_interceptor/Makefile | 43 ++++ pkg/plugins/tls_interceptor/addr_utils.go | 20 ++ .../tls_interceptor/addr_utils_test.go | 44 ++++ pkg/plugins/tls_interceptor/cert_store.go | 203 +++++++++++++++++ .../tls_interceptor/cert_store_test.go | 211 ++++++++++++++++++ pkg/plugins/tls_interceptor/certs.go | 106 +++++++++ pkg/plugins/tls_interceptor/certs_test.go | 74 ++++++ .../tls_interceptor/generate_ca_cmd.go | 83 +++++++ pkg/plugins/tls_interceptor/init.go | 23 ++ pkg/plugins/tls_interceptor/main.go | 160 +++++++++++++ .../tls_interceptor/protocol_options.go | 78 +++++++ pkg/plugins/tls_interceptor/proxy.go | 51 +++++ pkg/plugins/tls_interceptor/proxy_conn.go | 22 ++ pkg/plugins/tls_interceptor/test_setup.go | 55 +++++ pkg/plugins/tls_interceptor/time_source.go | 18 ++ 44 files changed, 2277 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 assets/fakeFiles/default.gif create mode 100644 assets/fakeFiles/default.html create mode 100644 assets/fakeFiles/default.ico create mode 100644 assets/fakeFiles/default.jpg create mode 100644 assets/fakeFiles/default.png create mode 100644 assets/fakeFiles/default.txt create mode 100644 config.yaml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/cmd/init.go create mode 100644 internal/cmd/plugins.go create mode 100644 internal/cmd/root.go create mode 100644 internal/config/constants.go create mode 100644 internal/config/handler_config.go create mode 100644 internal/config/loading.go create mode 100644 internal/plugins/loading.go create mode 100644 main.go create mode 100644 pkg/api/protocol_handler.go create mode 100644 pkg/logging/factory.go create mode 100644 pkg/path/helpers.go create mode 100644 pkg/plugins/http_mock/Makefile create mode 100644 pkg/plugins/http_mock/main.go create mode 100644 pkg/plugins/http_mock/protocol_options.go create mode 100644 pkg/plugins/http_mock/regex_router.go create mode 100644 pkg/plugins/tls_interceptor/Makefile create mode 100644 pkg/plugins/tls_interceptor/addr_utils.go create mode 100644 pkg/plugins/tls_interceptor/addr_utils_test.go create mode 100644 pkg/plugins/tls_interceptor/cert_store.go create mode 100644 pkg/plugins/tls_interceptor/cert_store_test.go create mode 100644 pkg/plugins/tls_interceptor/certs.go create mode 100644 pkg/plugins/tls_interceptor/certs_test.go create mode 100644 pkg/plugins/tls_interceptor/generate_ca_cmd.go create mode 100644 pkg/plugins/tls_interceptor/init.go create mode 100644 pkg/plugins/tls_interceptor/main.go create mode 100644 pkg/plugins/tls_interceptor/protocol_options.go create mode 100644 pkg/plugins/tls_interceptor/proxy.go create mode 100644 pkg/plugins/tls_interceptor/proxy_conn.go create mode 100644 pkg/plugins/tls_interceptor/test_setup.go create mode 100644 pkg/plugins/tls_interceptor/time_source.go diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4a827a5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,18 @@ +############### +# Directories # +############### + +.git/ +plugins/ + +######### +# Files # +######### + +*.out +main +inetmock +README.md +.dockerignore +.gitignore +Dockerfile \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8defa90 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +assets/fakeFiles/* filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eb56052 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +######### +# files # +######### + +**/cov.out +**/cov-raw.out +**/*.so +*.key +*.pem +inetmock +main + +############### +# directories # +############### + +.idea/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ed9a6d1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,43 @@ +FROM golang:1.14-buster as build + +# Create appuser. +ARG USER=inetmock +ARG USER_ID=10001 + +ENV CGO_ENABLED=0 + +# Prepare build stage - can be cached +WORKDIR /work +RUN apt-get update && \ + apt-get install -y --no-install-recommends make gcc && \ + adduser \ + --disabled-password \ + --gecos "" \ + --home "/nonexistent" \ + --shell "/sbin/nologin" \ + --no-create-home \ + --uid "${USER_ID}" \ + "${USER}" + +# Fetch dependencies +COPY Makefile go.mod go.sum ./ +RUN go mod download + +COPY ./ ./ + +# Build binary and plugins +RUN make CONTAINER=yes + +# Runtime layer + +FROM scratch + +WORKDIR /app + +COPY --from=build /etc/passwd /etc/group /etc/ +COPY --from=build --chown=$USER /work/inetmock ./ +COPY --from=build --chown=$USER /work/plugins/ ./plugins/ + +USER $USER:$USER + +ENTRYPOINT ["/app/inetmock"] \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..80d4892 --- /dev/null +++ b/Makefile @@ -0,0 +1,64 @@ +VERSION = $(shell git describe --dirty --tags --always) +DIR = $(dir $(realpath $(firstword $(MAKEFILE_LIST)))) +BUILD_PATH = $(DIR)/main.go +PKGS = $(shell go list ./...) +TEST_PKGS = $(shell find . -type f -name "*_test.go" -printf '%h\n' | sort -u) +GOARGS = GOOS=linux GOARCH=amd64 +GO_BUILD_ARGS = -ldflags="-w -s" +GO_CONTAINER_BUILD_ARGS = -ldflags="-w -s" -a -installsuffix cgo +GO_DEBUG_BUILD_ARGS = -gcflags "all=-N -l" +BINARY_NAME = inetmock +PLUGINS = $(wildcard $(DIR)pkg/plugins/*/.) +DEBUG_PORT = 2345 +DEBUG_ARGS?= --development-logs=true +INETMOCK_PLUGINS_DIRECTORY = $(DIR)plugins + +.PHONY: clean all format deps compile debug test cli-cover-report html-cover-report plugins $(PLUGINS) + +all: clean format compile test plugins + +clean: + @find $(DIR) -type f \( -name "*.out" -or -name "*.so" \) -exec rm -f {} \; + @rm -rf $(DIR)plugins + @rm -f $(DIR)$(BINARY_NAME) $(DIR)main + +format: + @go fmt $(PKGS) + +deps: + @go mod tidy + @go build -v $(BUILD_PATH) + +compile: deps +ifdef DEBUG + @echo 'Compiling for debugging...' + @$(GOARGS) go build $(GO_DEBUG_BUILD_ARGS) -o $(DIR)$(BINARY_NAME) $(BUILD_PATH) +else ifdef CONTAINER + @echo 'Compiling for container usage...' + @$(GOARGS) go build $(GO_CONTAINER_BUILD_ARGS) -o $(DIR)$(BINARY_NAME) $(BUILD_PATH) +else + @echo 'Compiling for normal Linux env...' + @$(GOARGS) go build $(GO_BUILD_ARGS) -o $(DIR)$(BINARY_NAME) $(BUILD_PATH) +endif + +debug: + @export INETMOCK_PLUGINS_DIRECTORY + @dlv exec $(DIR)$(BINARY_NAME) \ + --headless \ + --listen=:2345 \ + --api-version=2 \ + --accept-multiclient \ + -- $(DEBUG_ARGS) +test: + @go test -coverprofile=./cov-raw.out -v $(TEST_PKGS) + @cat ./cov-raw.out | grep -v "generated" > ./cov.out + +cli-cover-report: + @go tool cover -func=cov.out + +html-cover-report: + @go tool cover -html=cov.out -o .coverage.html + +plugins: $(PLUGINS) +$(PLUGINS): + $(MAKE) -C $@ \ No newline at end of file diff --git a/assets/fakeFiles/default.gif b/assets/fakeFiles/default.gif new file mode 100644 index 0000000..299f6f0 --- /dev/null +++ b/assets/fakeFiles/default.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4951b3d218405caac8b3698658ad8680df908884d0b9d5a17654b1e0c240844b +size 25053 diff --git a/assets/fakeFiles/default.html b/assets/fakeFiles/default.html new file mode 100644 index 0000000..1a64e75 --- /dev/null +++ b/assets/fakeFiles/default.html @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2a3f8995831dd1e79cb753619a55752692168f6cf846b07405f2070492f481c +size 235 diff --git a/assets/fakeFiles/default.ico b/assets/fakeFiles/default.ico new file mode 100644 index 0000000..272443c --- /dev/null +++ b/assets/fakeFiles/default.ico @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb3f33cb0c7bae881d13be647cb928aa7fec9e9fcd6ec758751374bf7436d41a +size 70614 diff --git a/assets/fakeFiles/default.jpg b/assets/fakeFiles/default.jpg new file mode 100644 index 0000000..4e77e32 --- /dev/null +++ b/assets/fakeFiles/default.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:511560ec3481a9089bb5fe0854b41dd9c0413fb8b7694805f50bff4b8fedbb13 +size 36275 diff --git a/assets/fakeFiles/default.png b/assets/fakeFiles/default.png new file mode 100644 index 0000000..25baa0a --- /dev/null +++ b/assets/fakeFiles/default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b5ab03ea66bd52ddae299f172a3aef5c8b0b8d84a1c777c22e62358cbb634741 +size 200147 diff --git a/assets/fakeFiles/default.txt b/assets/fakeFiles/default.txt new file mode 100644 index 0000000..674c898 --- /dev/null +++ b/assets/fakeFiles/default.txt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:136c41a07ec1ee2e3437a636bf48ced65e46e8bbc9c759fee820b5c3bf5b772e +size 92 diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..85b12dd --- /dev/null +++ b/config.yaml @@ -0,0 +1,41 @@ +endpoints: + plainHttp: + handler: http_mock + listenAddress: 0.0.0.0 + port: 80 + options: + rules: + - pattern: ".*\\.(?i)exe" + target: ./assets/fakeFiles/sample.exe + - pattern: ".*\\.(?i)(jpg|jpeg)" + target: ./assets/fakeFiles/default.jpg + - pattern: ".*\\.(?i)png" + target: ./assets/fakeFiles/default.png + - pattern: ".*\\.(?i)gif" + target: ./assets/fakeFiles/default.gif + - pattern: ".*\\.(?i)ico" + target: ./assets/fakeFiles/default.ico + - pattern: ".*\\.(?i)txt" + target: ./assets/fakeFiles/default.txt + - pattern: ".*" + target: ./assets/fakeFiles/default.html + httpsDowngrade: + handler: tls_interceptor + listenAddress: 0.0.0.0 + port: 443 + options: + ecdsaCurve: P256 + validity: + ca: + notBeforeRelative: 17520h + notAfterRelative: 17520h + domain: + notBeforeRelative: 168h + notAfterRelative: 168h + rootCaCert: + publicKey: ./ca.pem + privateKey: ./ca.key + certCachePath: /tmp/inetmock/ + target: + ipAddress: 127.0.0.1 + port: 80 \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1a673e6 --- /dev/null +++ b/go.mod @@ -0,0 +1,12 @@ +module github.com/baez90/inetmock + +go 1.13 + +require ( + github.com/spf13/cobra v0.0.6 + github.com/spf13/pflag v1.0.5 + github.com/spf13/viper v1.4.0 + go.uber.org/zap v1.14.1 + golang.org/x/tools v0.0.0-20191127201027-ecd32218bd7f // indirect + gopkg.in/yaml.v2 v2.2.8 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7439ac0 --- /dev/null +++ b/go.sum @@ -0,0 +1,187 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0/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/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +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/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.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/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.6 h1:breEStsVwemnKh2/s6gMvSdMEkwW0sK8vGStnlVBMCs= +github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.14.1 h1:nYDKopTbvAPq/NrUVZwT15y2lpROBiLLyoRTbXOYWOo= +go.uber.org/zap v1.14.1/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529 h1:iMGN4xG0cnqj3t+zOM8wUB0BiPKHEwSxEZCvzcbZuvk= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191127201027-ecd32218bd7f h1:3MlESg/jvTr87F4ttA/q4B+uhe/q6qleC9/DP+IwQmY= +golang.org/x/tools v0.0.0-20191127201027-ecd32218bd7f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +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.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= diff --git a/internal/cmd/init.go b/internal/cmd/init.go new file mode 100644 index 0000000..478bc33 --- /dev/null +++ b/internal/cmd/init.go @@ -0,0 +1,46 @@ +package cmd + +import ( + "github.com/baez90/inetmock/internal/plugins" + "github.com/baez90/inetmock/pkg/logging" + "github.com/baez90/inetmock/pkg/path" + "github.com/spf13/viper" + "go.uber.org/zap" +) + +var ( + appIsInitialized = false +) + +func initApp() (err error) { + if appIsInitialized { + return + } + appIsInitialized = true + logging.ConfigureLogging( + logging.ParseLevel(logLevel), + developmentLogs, + map[string]interface{}{"cwd": path.WorkingDirectory()}, + ) + logger, _ = logging.CreateLogger() + registry := plugins.Registry() + if err = appConfig.ReadConfig(configFilePath); err != nil { + logger.Error( + "unrecoverable error occurred during reading the config file", + zap.Error(err), + ) + return + } + + viperInst := viper.GetViper() + pluginDir := viperInst.GetString("plugins-directory") + if err = registry.LoadPlugins(pluginDir); err != nil { + logger.Error("Failed to load plugins", + zap.Error(err), + ) + } + + pluginsCmd.AddCommand(registry.PluginCommands()...) + + return +} diff --git a/internal/cmd/plugins.go b/internal/cmd/plugins.go new file mode 100644 index 0000000..09a1fbe --- /dev/null +++ b/internal/cmd/plugins.go @@ -0,0 +1,22 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +var ( + pluginsCmd *cobra.Command +) + +func init() { + pluginsCmd = &cobra.Command{ + Use: "plugins", + Short: "Use the plugins prefix to interact with commands that are provided by plugins", + Long: ` +The plugin prefix can be used to interact with commands that are provided by plugins. +The easiest way to explore what commands are available is to start with 'inetmock plugins' - like you did! +This help page contains a list of available sub-commands starting with the name of the plugin as a prefix. +`, + } + rootCmd.AddCommand(pluginsCmd) +} diff --git a/internal/cmd/root.go b/internal/cmd/root.go new file mode 100644 index 0000000..ae5f2c6 --- /dev/null +++ b/internal/cmd/root.go @@ -0,0 +1,111 @@ +package cmd + +import ( + "github.com/baez90/inetmock/internal/config" + "github.com/baez90/inetmock/internal/plugins" + "github.com/baez90/inetmock/pkg/api" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "go.uber.org/zap" + "os" + "os/signal" + "strings" + "sync" + "syscall" +) + +var ( + logger *zap.Logger + rootCmd = cobra.Command{ + Use: "", + Short: "INetMock is lightweight internet mock", + Run: startInetMock, + } + + configFilePath string + logLevel string + developmentLogs bool + handlers []api.ProtocolHandler + appConfig = config.CreateConfig() +) + +func init() { + rootCmd.PersistentFlags().String("plugins-directory", "", "Directory where plugins should be loaded from") + rootCmd.PersistentFlags().StringVar(&configFilePath, "config", "", "Path to config file that should be used") + rootCmd.PersistentFlags().StringVar(&logLevel, "log-level", "info", "logging level to use") + rootCmd.PersistentFlags().BoolVar(&developmentLogs, "development-logs", false, "Enable development mode logs") + + appConfig.InitConfig(rootCmd.PersistentFlags()) +} + +func startInetMock(cmd *cobra.Command, args []string) { + registry := plugins.Registry() + var wg sync.WaitGroup + + //todo introduce endpoint type and move startup and shutdown to this type + + for key, val := range viper.GetStringMap(config.EndpointsKey) { + handlerSubConfig := viper.Sub(strings.Join([]string{config.EndpointsKey, key, config.OptionsKey}, ".")) + pluginConfig := config.CreateHandlerConfig(val, handlerSubConfig) + logger.Info(key, zap.Any("value", pluginConfig)) + + if handler, ok := registry.HandlerForName(pluginConfig.HandlerName()); ok { + handlers = append(handlers, handler) + go startEndpoint(handler, pluginConfig, logger) + wg.Add(1) + } else { + logger.Warn( + "no matching handler registered", + zap.String("handler", pluginConfig.HandlerName()), + ) + } + } + + signalChannel := make(chan os.Signal, 1) + signal.Notify(signalChannel, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) + + // block until canceled + s := <-signalChannel + + logger.Info( + "got signal to quit", + zap.String("signal", s.String()), + ) + + for _, handler := range handlers { + go shutdownEndpoint(handler, &wg, logger) + } + + wg.Wait() +} + +func startEndpoint(handler api.ProtocolHandler, config config.HandlerConfig, logger *zap.Logger) { + defer func() { + if r := recover(); r != nil { + logger.Fatal( + "recovered panic during startup of endpoint", + zap.Any("recovered", r), + ) + } + }() + handler.Run(config) +} + +func shutdownEndpoint(handler api.ProtocolHandler, wg *sync.WaitGroup, logger *zap.Logger) { + defer func() { + if r := recover(); r != nil { + logger.Fatal( + "recovered panic during shutdown of endpoint", + zap.Any("recovered", r), + ) + } + }() + handler.Shutdown(wg) +} + +func ExecuteRootCommand() error { + if err := initApp(); err != nil { + return err + } + return rootCmd.Execute() +} diff --git a/internal/config/constants.go b/internal/config/constants.go new file mode 100644 index 0000000..a645a7c --- /dev/null +++ b/internal/config/constants.go @@ -0,0 +1,6 @@ +package config + +const ( + EndpointsKey = "endpoints" + OptionsKey = "options" +) diff --git a/internal/config/handler_config.go b/internal/config/handler_config.go new file mode 100644 index 0000000..27bf5a3 --- /dev/null +++ b/internal/config/handler_config.go @@ -0,0 +1,49 @@ +package config + +import "github.com/spf13/viper" + +const ( + pluginConfigKey = "handler" + listenAddressConfigKey = "listenaddress" + portConfigKey = "port" +) + +type handlerConfig struct { + pluginName string + port uint16 + listenAddress string + options *viper.Viper +} + +func (h handlerConfig) HandlerName() string { + return h.pluginName +} + +func (h handlerConfig) ListenAddress() string { + return h.listenAddress +} + +func (h handlerConfig) Port() uint16 { + return h.port +} + +func (h handlerConfig) Options() *viper.Viper { + return h.options +} + +type HandlerConfig interface { + HandlerName() string + ListenAddress() string + Port() uint16 + Options() *viper.Viper +} + +func CreateHandlerConfig(configMap interface{}, subConfig *viper.Viper) HandlerConfig { + underlyingMap := configMap.(map[string]interface{}) + return &handlerConfig{ + pluginName: underlyingMap[pluginConfigKey].(string), + listenAddress: underlyingMap[listenAddressConfigKey].(string), + port: uint16(underlyingMap[portConfigKey].(int)), + options: subConfig, + } +} diff --git a/internal/config/loading.go b/internal/config/loading.go new file mode 100644 index 0000000..32fd973 --- /dev/null +++ b/internal/config/loading.go @@ -0,0 +1,51 @@ +package config + +import ( + "github.com/baez90/inetmock/pkg/logging" + "github.com/baez90/inetmock/pkg/path" + "github.com/spf13/pflag" + "github.com/spf13/viper" + "go.uber.org/zap" + "strings" +) + +func CreateConfig() Config { + logger, _ := logging.CreateLogger() + return &config{ + logger: logger.Named("Config"), + } +} + +type Config interface { + InitConfig(flags *pflag.FlagSet) + ReadConfig(configFilePath string) error +} + +type config struct { + logger *zap.Logger +} + +func (c config) InitConfig(flags *pflag.FlagSet) { + viper.SetConfigName("config") + viper.SetConfigType("yaml") + viper.AddConfigPath("/etc/inetmock/") + viper.AddConfigPath("$HOME/.inetmock") + viper.AddConfigPath(".") + viper.SetEnvPrefix("INetMock") + _ = viper.BindPFlags(flags) + viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) + viper.AutomaticEnv() +} + +func (c *config) ReadConfig(configFilePath string) (err error) { + if configFilePath != "" && path.FileExists(configFilePath) { + viper.SetConfigFile(configFilePath) + } + if err = viper.ReadInConfig(); err != nil { + if _, ok := err.(viper.ConfigFileNotFoundError); ok { + err = nil + c.logger.Warn("failed to load config") + } + } + return +} diff --git a/internal/plugins/loading.go b/internal/plugins/loading.go new file mode 100644 index 0000000..c933976 --- /dev/null +++ b/internal/plugins/loading.go @@ -0,0 +1,78 @@ +package plugins + +import ( + "fmt" + "github.com/baez90/inetmock/pkg/api" + "github.com/spf13/cobra" + "path/filepath" + "plugin" +) + +var ( + registry HandlerRegistry +) + +type HandlerRegistry interface { + LoadPlugins(pluginsPath string) error + RegisterHandler(handlerName string, handlerProvider api.PluginInstanceFactory, subCommands ...*cobra.Command) + HandlerForName(handlerName string) (api.ProtocolHandler, bool) + PluginCommands() []*cobra.Command +} + +type handlerRegistry struct { + handlers map[string]api.PluginInstanceFactory + pluginCommands []*cobra.Command +} + +func (h handlerRegistry) PluginCommands() []*cobra.Command { + return h.pluginCommands +} + +func (h *handlerRegistry) HandlerForName(handlerName string) (instance api.ProtocolHandler, ok bool) { + var provider api.PluginInstanceFactory + if provider, ok = h.handlers[handlerName]; ok { + instance = provider() + } + return +} + +func (h *handlerRegistry) RegisterHandler(handlerName string, handlerProvider api.PluginInstanceFactory, subCommands ...*cobra.Command) { + if _, exists := h.handlers[handlerName]; exists { + panic(fmt.Sprintf("plugin %s already registered - there's something strange...in the neighborhood")) + } + h.handlers[handlerName] = handlerProvider + + if len(subCommands) > 0 { + pluginCmds := &cobra.Command{ + Use: handlerName, + } + pluginCmds.AddCommand(subCommands...) + h.pluginCommands = append(h.pluginCommands, pluginCmds) + } +} + +func (h *handlerRegistry) LoadPlugins(pluginsPath string) (err error) { + var plugins []string + if plugins, err = filepath.Glob(fmt.Sprintf("%s%c*.so", pluginsPath, filepath.Separator)); err != nil { + return + } + + for _, pluginSo := range plugins { + if _, err = plugin.Open(pluginSo); err != nil { + return + } + } + + err = nil + return +} + +func Registry() HandlerRegistry { + return registry +} + +func init() { + registry = &handlerRegistry{ + handlers: make(map[string]api.PluginInstanceFactory), + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..206a226 --- /dev/null +++ b/main.go @@ -0,0 +1,19 @@ +package main + +import ( + "github.com/baez90/inetmock/internal/cmd" + "go.uber.org/zap" + "os" +) + +func main() { + logger, _ := zap.NewProduction() + defer logger.Sync() + + if err := cmd.ExecuteRootCommand(); err != nil { + logger.Error("Failed to run inetmock", + zap.Error(err), + ) + os.Exit(1) + } +} diff --git a/pkg/api/protocol_handler.go b/pkg/api/protocol_handler.go new file mode 100644 index 0000000..98c2aa2 --- /dev/null +++ b/pkg/api/protocol_handler.go @@ -0,0 +1,16 @@ +package api + +import ( + "github.com/baez90/inetmock/internal/config" + "go.uber.org/zap" + "sync" +) + +type PluginInstanceFactory func() ProtocolHandler + +type LoggingFactory func() (*zap.Logger, error) + +type ProtocolHandler interface { + Run(config config.HandlerConfig) + Shutdown(wg *sync.WaitGroup) +} diff --git a/pkg/logging/factory.go b/pkg/logging/factory.go new file mode 100644 index 0000000..dc671ec --- /dev/null +++ b/pkg/logging/factory.go @@ -0,0 +1,42 @@ +package logging + +import ( + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "strings" +) + +var ( + loggingConfig = zap.NewProductionConfig() +) + +func ConfigureLogging( + level zap.AtomicLevel, + developmentLogging bool, + initialFields map[string]interface{}, +) { + loggingConfig.Level = level + loggingConfig.Development = developmentLogging + loggingConfig.InitialFields = initialFields +} + +func ParseLevel(levelString string) zap.AtomicLevel { + switch strings.ToLower(levelString) { + case "debug": + return zap.NewAtomicLevelAt(zapcore.DebugLevel) + case "info": + return zap.NewAtomicLevelAt(zapcore.InfoLevel) + case "warn": + return zap.NewAtomicLevelAt(zapcore.WarnLevel) + case "error": + return zap.NewAtomicLevelAt(zapcore.ErrorLevel) + case "fatal": + return zap.NewAtomicLevelAt(zapcore.FatalLevel) + default: + return zap.NewAtomicLevelAt(zapcore.InfoLevel) + } +} + +func CreateLogger() (*zap.Logger, error) { + return loggingConfig.Build() +} diff --git a/pkg/path/helpers.go b/pkg/path/helpers.go new file mode 100644 index 0000000..d0e428c --- /dev/null +++ b/pkg/path/helpers.go @@ -0,0 +1,19 @@ +package path + +import ( + "os" + "path/filepath" +) + +func WorkingDirectory() (cwd string) { + cwd, _ = filepath.Abs(filepath.Dir(os.Args[0])) + return +} + +func FileExists(filename string) bool { + info, err := os.Stat(filename) + if os.IsNotExist(err) { + return false + } + return !info.IsDir() +} diff --git a/pkg/plugins/http_mock/Makefile b/pkg/plugins/http_mock/Makefile new file mode 100644 index 0000000..d03d9ad --- /dev/null +++ b/pkg/plugins/http_mock/Makefile @@ -0,0 +1,43 @@ +VERSION = $(shell git describe --dirty --tags --always) +DIR = $(dir $(realpath $(firstword $(MAKEFILE_LIST)))) +PKGS = $(shell go list ./...) +TEST_PKGS = $(shell find . -type f -name "*_test.go" -printf '%h\n' | sort -u) +GOARGS = GOOS=linux GOARCH=amd64 +GO_BUILD_ARGS = -buildmode=plugin -ldflags="-w -s" +GO_CONTAINER_BUILD_ARGS = -buildmode=plugin -ldflags="-w -s" -a -installsuffix cgo +GO_DEBUG_BUILD_ARGS = -buildmode=plugin -gcflags "all=-N -l" +PLUGIN_NAME = http_mock.so +OUT_DIR = $(DIR)../../../plugins +DEBUG_PORT = 2345 + +.PHONY: deps format compile test cli-cover-report html-cover-report + +all: compile test + +deps: + @go mod tidy + @go build -buildmode=plugin -v $(DIR)... + +format: + @go fmt $(PKGS) + +compile: deps + @mkdir -p $(OUT_DIR) +ifdef DEBUG + @echo 'Compiling for debugging...' + @$(GOARGS) go build $(GO_DEBUG_BUILD_ARGS) -o $(OUT_DIR)/$(PLUGIN_NAME) $(DIR) +else ifdef CONTAINER + @$(GOARGS) go build $(GO_CONTAINER_BUILD_ARGS) -o $(OUT_DIR)/$(PLUGIN_NAME) $(DIR) +else + @$(GOARGS) go build $(GO_BUILD_ARGS) -o $(OUT_DIR)/$(PLUGIN_NAMEs) $(DIR) +endif + +test: + @go test -coverprofile=./cov-raw.out -v $(TEST_PKGS) + @cat ./cov-raw.out | grep -v "generated" > ./cov.out + +cli-cover-report: + @go tool cover -func=cov.out + +html-cover-report: + @go tool cover -html=cov.out -o .coverage.html \ No newline at end of file diff --git a/pkg/plugins/http_mock/main.go b/pkg/plugins/http_mock/main.go new file mode 100644 index 0000000..10f4075 --- /dev/null +++ b/pkg/plugins/http_mock/main.go @@ -0,0 +1,116 @@ +package main + +import ( + "bytes" + "fmt" + "github.com/baez90/inetmock/internal/config" + "github.com/baez90/inetmock/internal/plugins" + "github.com/baez90/inetmock/pkg/api" + "github.com/baez90/inetmock/pkg/logging" + "github.com/baez90/inetmock/pkg/path" + "go.uber.org/zap" + "net/http" + "path/filepath" + "regexp" + "sync" +) + +const ( + name = "http_mock" +) + +type httpPlugin struct { + logger *zap.Logger + router *RegexpHandler + server *http.Server +} + +func (p *httpPlugin) Run(config config.HandlerConfig) { + options := loadFromConfig(config.Options()) + addr := fmt.Sprintf("%s:%d", config.ListenAddress(), config.Port()) + p.server = &http.Server{Addr: addr, Handler: p.router} + p.logger = p.logger.With( + zap.String("address", addr), + ) + + for _, rule := range options.Rules { + p.setupRoute(rule) + } + + go p.startServer() +} + +func (p *httpPlugin) Shutdown(wg *sync.WaitGroup) { + if err := p.server.Close(); err != nil { + p.logger.Error( + "failed to shutdown HTTP server", + zap.Error(err), + ) + } + + wg.Done() +} + +func (p *httpPlugin) startServer() { + if err := p.server.ListenAndServe(); err != nil { + p.logger.Error( + "failed to start http listener", + zap.Error(err), + ) + } +} + +func (p *httpPlugin) setupRoute(rule targetRule) { + var compiled *regexp.Regexp + var err error + if compiled, err = regexp.Compile(rule.pattern); err != nil { + p.logger.Warn( + "failed to parse route - skipping", + zap.String("route", rule.pattern), + zap.Error(err), + ) + return + } + p.logger.Info( + "setup routing", + zap.String("route", compiled.String()), + zap.String("target", rule.target), + ) + + p.router.Handler(compiled, createHandlerForTarget(p.logger, rule.target)) +} + +func createHandlerForTarget(logger *zap.Logger, targetPath string) http.Handler { + return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + targetFilePath := filepath.Join(path.WorkingDirectory(), targetPath) + + headerWriter := &bytes.Buffer{} + request.Header.Write(headerWriter) + + logger.Info( + "Handling request", + zap.String("source", request.RemoteAddr), + zap.String("host", request.Host), + zap.String("method", request.Method), + zap.String("protocol", request.Proto), + zap.String("path", request.RequestURI), + zap.String("target", targetFilePath), + zap.Reflect("headers", request.Header), + ) + + http.ServeFile(writer, request, targetFilePath) + }) +} + +func init() { + logger, _ := logging.CreateLogger() + logger = logger.With( + zap.String("ProtocolHandler", name), + ) + plugins.Registry().RegisterHandler(name, func() api.ProtocolHandler { + return &httpPlugin{ + logger: logger, + router: &RegexpHandler{}, + } + }) +} diff --git a/pkg/plugins/http_mock/protocol_options.go b/pkg/plugins/http_mock/protocol_options.go new file mode 100644 index 0000000..5e33e42 --- /dev/null +++ b/pkg/plugins/http_mock/protocol_options.go @@ -0,0 +1,33 @@ +package main + +import "github.com/spf13/viper" + +const ( + rulesConfigKey = "rules" + patternConfigKey = "pattern" + targetConfigKey = "target" +) + +type targetRule struct { + pattern string + target string +} + +type httpOptions struct { + Rules []targetRule +} + +func loadFromConfig(config *viper.Viper) httpOptions { + options := httpOptions{} + anonRules := config.Get(rulesConfigKey).([]interface{}) + + for _, i := range anonRules { + innerData := i.(map[interface{}]interface{}) + options.Rules = append(options.Rules, targetRule{ + pattern: innerData[patternConfigKey].(string), + target: innerData[targetConfigKey].(string), + }) + } + + return options +} diff --git a/pkg/plugins/http_mock/regex_router.go b/pkg/plugins/http_mock/regex_router.go new file mode 100644 index 0000000..bfa4a24 --- /dev/null +++ b/pkg/plugins/http_mock/regex_router.go @@ -0,0 +1,34 @@ +package main + +import ( + "net/http" + "regexp" +) + +type route struct { + pattern *regexp.Regexp + handler http.Handler +} + +type RegexpHandler struct { + routes []*route +} + +func (h *RegexpHandler) Handler(pattern *regexp.Regexp, handler http.Handler) { + h.routes = append(h.routes, &route{pattern, handler}) +} + +func (h *RegexpHandler) HandleFunc(pattern *regexp.Regexp, handler func(http.ResponseWriter, *http.Request)) { + h.routes = append(h.routes, &route{pattern, http.HandlerFunc(handler)}) +} + +func (h *RegexpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + for _, route := range h.routes { + if route.pattern.MatchString(r.URL.Path) { + route.handler.ServeHTTP(w, r) + return + } + } + // no pattern matched; send 404 response + http.NotFound(w, r) +} diff --git a/pkg/plugins/tls_interceptor/Makefile b/pkg/plugins/tls_interceptor/Makefile new file mode 100644 index 0000000..9e5e4f4 --- /dev/null +++ b/pkg/plugins/tls_interceptor/Makefile @@ -0,0 +1,43 @@ +VERSION = $(shell git describe --dirty --tags --always) +DIR = $(dir $(realpath $(firstword $(MAKEFILE_LIST)))) +PKGS = $(shell go list ./...) +TEST_PKGS = $(shell find . -type f -name "*_test.go" -printf '%h\n' | sort -u) +GOARGS = GOOS=linux GOARCH=amd64 +GO_BUILD_ARGS = -buildmode=plugin -ldflags="-w -s" +GO_CONTAINER_BUILD_ARGS = -buildmode=plugin -ldflags="-w -s" -a -installsuffix cgo +GO_DEBUG_BUILD_ARGS = -buildmode=plugin -gcflags "all=-N -l" +PLUGIN_NAME = tls_interceptor.so +OUT_DIR = $(DIR)../../../plugins +DEBUG_PORT = 2345 + +.PHONY: deps format compile test cli-cover-report html-cover-report + +all: compile test + +deps: + @go mod tidy + @go build -buildmode=plugin -v $(DIR)... + +format: + @go fmt $(PKGS) + +compile: deps + @mkdir -p $(OUT_DIR) +ifdef DEBUG + @echo 'Compiling for debugging...' + @$(GOARGS) go build $(GO_DEBUG_BUILD_ARGS) -o $(OUT_DIR)/$(PLUGIN_NAME) $(DIR) +else ifdef CONTAINER + @$(GOARGS) go build $(GO_CONTAINER_BUILD_ARGS) -o $(OUT_DIR)/$(PLUGIN_NAME) $(DIR) +else + @$(GOARGS) go build $(GO_BUILD_ARGS) -o $(OUT_DIR)/$(PLUGIN_NAME) $(DIR) +endif + +test: + @go test -coverprofile=./cov-raw.out -v $(TEST_PKGS) + @cat ./cov-raw.out | grep -v "generated" > ./cov.out + +cli-cover-report: + @go tool cover -func=cov.out + +html-cover-report: + @go tool cover -html=cov.out -o .coverage.html \ No newline at end of file diff --git a/pkg/plugins/tls_interceptor/addr_utils.go b/pkg/plugins/tls_interceptor/addr_utils.go new file mode 100644 index 0000000..db39271 --- /dev/null +++ b/pkg/plugins/tls_interceptor/addr_utils.go @@ -0,0 +1,20 @@ +package main + +import ( + "fmt" + "regexp" + "strings" +) + +var ( + ipExtractionRegex = regexp.MustCompile(`(.+):\d{1,5}$`) +) + +func extractIPFromAddress(addr string) (string, error) { + matches := ipExtractionRegex.FindAllStringSubmatch(addr, -1) + if len(matches) > 0 && len(matches[0]) >= 1 { + return strings.Trim(matches[0][1], "[]"), nil + } else { + return "", fmt.Errorf("failed to extract IP address from addr %s", addr) + } +} diff --git a/pkg/plugins/tls_interceptor/addr_utils_test.go b/pkg/plugins/tls_interceptor/addr_utils_test.go new file mode 100644 index 0000000..4a224aa --- /dev/null +++ b/pkg/plugins/tls_interceptor/addr_utils_test.go @@ -0,0 +1,44 @@ +package main + +import "testing" + +func Test_extractIPFromAddress(t *testing.T) { + type args struct { + addr string + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "Get address for IPv4 address", + want: "127.0.0.1", + wantErr: false, + args: args{ + addr: "127.0.0.1:23492", + }, + }, + { + name: "Get address for IPv6 address", + want: "::1", + wantErr: false, + args: args{ + addr: "[::1]:23492", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := extractIPFromAddress(tt.args.addr) + if (err != nil) != tt.wantErr { + t.Errorf("extractIPFromAddress() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("extractIPFromAddress() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/plugins/tls_interceptor/cert_store.go b/pkg/plugins/tls_interceptor/cert_store.go new file mode 100644 index 0000000..807674e --- /dev/null +++ b/pkg/plugins/tls_interceptor/cert_store.go @@ -0,0 +1,203 @@ +package main + +import ( + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "fmt" + "github.com/baez90/inetmock/pkg/path" + "go.uber.org/zap" + "math/big" + "net" + "path/filepath" +) + +type keyProvider func() (key interface{}, err error) + +type certStore struct { + options *tlsOptions + keyProvider keyProvider + caCert *x509.Certificate + caPrivateKey interface{} + certCache map[string]*tls.Certificate + timeSourceInstance timeSource + logger *zap.Logger +} + +func (cs *certStore) timeSource() timeSource { + if cs.timeSourceInstance == nil { + cs.timeSourceInstance = createTimeSource() + } + return cs.timeSourceInstance +} + +func (cs *certStore) initCaCert() (err error) { + if path.FileExists(cs.options.rootCaCert.publicKeyPath) && path.FileExists(cs.options.rootCaCert.privateKeyPath) { + var tlsCert tls.Certificate + if tlsCert, err = tls.LoadX509KeyPair(cs.options.rootCaCert.publicKeyPath, cs.options.rootCaCert.privateKeyPath); err != nil { + return + } else if len(tlsCert.Certificate) > 0 { + cs.caPrivateKey = tlsCert.PrivateKey + cs.caCert, err = x509.ParseCertificate(tlsCert.Certificate[0]) + } + } else { + cs.caCert, cs.caPrivateKey, err = cs.generateCaCert() + } + return +} + +func (cs *certStore) initProvider() { + if cs.keyProvider == nil { + cs.keyProvider = func() (key interface{}, err error) { + return privateKeyForCurve(cs.options) + } + } +} + +func (cs *certStore) generateCaCert() (pubKey *x509.Certificate, privateKey interface{}, err error) { + cs.initProvider() + timeSource := cs.timeSource() + var serialNumber *big.Int + if serialNumber, err = generateSerialNumber(); err != nil { + return + } + + if privateKey, err = cs.keyProvider(); err != nil { + return + } + + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{"INetMock"}, + Country: []string{"US"}, + Province: []string{""}, + Locality: []string{"San Francisco"}, + StreetAddress: []string{"Golden Gate Bridge"}, + PostalCode: []string{"94016"}, + }, + IsCA: true, + NotBefore: timeSource.UTCNow().Add(-cs.options.validity.ca.notBeforeRelative), + NotAfter: timeSource.UTCNow().Add(cs.options.validity.ca.notAfterRelative), + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + } + + var derBytes []byte + if derBytes, err = x509.CreateCertificate(rand.Reader, &template, &template, publicKey(privateKey), privateKey); err != nil { + return + } + + pubKey, err = x509.ParseCertificate(derBytes) + + if err = writePublicKey(cs.options.rootCaCert.publicKeyPath, derBytes); err != nil { + return + } + + var privateKeyBytes []byte + if privateKeyBytes, err = x509.MarshalPKCS8PrivateKey(privateKey); err != nil { + return + } + + if err = writePrivateKey(cs.options.rootCaCert.privateKeyPath, privateKeyBytes); err != nil { + return + } + + return +} + +func (cs *certStore) getCertificate(serverName string, ip string) (cert *tls.Certificate, err error) { + if cert, ok := cs.certCache[serverName]; ok { + return cert, nil + } + + certPath := filepath.Join(cs.options.certCachePath, fmt.Sprintf("%s.pem", serverName)) + keyPath := filepath.Join(cs.options.certCachePath, fmt.Sprintf("%s.key", serverName)) + + if path.FileExists(certPath) && path.FileExists(keyPath) { + if tlsCert, loadErr := tls.LoadX509KeyPair(certPath, keyPath); loadErr == nil { + cs.certCache[serverName] = &tlsCert + x509Cert, err := x509.ParseCertificate(tlsCert.Certificate[0]) + if err == nil && !certShouldBeRenewed(cs.timeSource(), x509Cert) { + return &tlsCert, nil + } + } + } + + if cert, err = cs.generateDomainCert(serverName, ip); err == nil { + cs.certCache[serverName] = cert + } + + return +} + +func (cs *certStore) generateDomainCert( + serverName string, + localIp string, +) (cert *tls.Certificate, err error) { + cs.initProvider() + + if cs.caCert == nil { + if err = cs.initCaCert(); err != nil { + return + } + } + + var serialNumber *big.Int + if serialNumber, err = generateSerialNumber(); err != nil { + return + } + + var privateKey interface{} + if privateKey, err = cs.keyProvider(); err != nil { + return + } + + notBefore := cs.timeSource().UTCNow().Add(-cs.options.validity.domain.notBeforeRelative) + notAfter := cs.timeSource().UTCNow().Add(cs.options.validity.domain.notAfterRelative) + + cs.logger.Info( + "generate domain certificate", + zap.Time("notBefore", notBefore), + zap.Time("notAfter", notAfter), + ) + + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{"INetMock"}, + }, + IPAddresses: []net.IP{net.ParseIP(localIp)}, + DNSNames: []string{serverName}, + NotBefore: notBefore, + NotAfter: notAfter, + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + } + + var derBytes []byte + if derBytes, err = x509.CreateCertificate(rand.Reader, &template, cs.caCert, publicKey(privateKey), cs.caPrivateKey); err != nil { + return + } + + if err = writePublicKey(filepath.Join(cs.options.certCachePath, fmt.Sprintf("%s.pem", serverName)), derBytes); err != nil { + return + } + + var privateKeyBytes []byte + if privateKeyBytes, err = x509.MarshalPKCS8PrivateKey(privateKey); err != nil { + return + } + + if cert, err = parseCert(derBytes, privateKeyBytes); err != nil { + return + } + + if err = writePrivateKey(filepath.Join(cs.options.certCachePath, fmt.Sprintf("%s.key", serverName)), privateKeyBytes); err != nil { + return + } + + return +} diff --git a/pkg/plugins/tls_interceptor/cert_store_test.go b/pkg/plugins/tls_interceptor/cert_store_test.go new file mode 100644 index 0000000..a4de478 --- /dev/null +++ b/pkg/plugins/tls_interceptor/cert_store_test.go @@ -0,0 +1,211 @@ +package main + +import ( + "crypto/tls" + "crypto/x509" + "go.uber.org/zap" + "io/ioutil" + "os" + "path/filepath" + "reflect" + "testing" + "time" +) + +func Test_generateCaCert(t *testing.T) { + tmpDir, err := ioutil.TempDir(os.TempDir(), "*-inetmock") + if err != nil { + t.Errorf("failed to create temp dir %v", err) + return + } + + options := &tlsOptions{ + ecdsaCurve: "P256", + rootCaCert: cert{ + publicKeyPath: filepath.Join(tmpDir, "localhost.pem"), + privateKeyPath: filepath.Join(tmpDir, "localhost.key"), + }, + validity: validity{ + ca: certValidity{ + notBeforeRelative: time.Hour * 24 * 30, + notAfterRelative: time.Hour * 24 * 30, + }, + }, + } + + certStore := certStore{ + options: options, + } + + defer func() { + _ = os.Remove(tmpDir) + }() + + _, _, err = certStore.generateCaCert() + + if err != nil { + t.Errorf("failed to generate CA cert %v", err) + } + + if _, err = os.Stat(options.rootCaCert.publicKeyPath); err != nil { + t.Errorf("cert file was not created") + } + + if _, err = os.Stat(options.rootCaCert.privateKeyPath); err != nil { + t.Errorf("cert file was not created") + } +} + +func Test_generateDomainCert(t *testing.T) { + + tmpDir, err := ioutil.TempDir(os.TempDir(), "*-inetmock") + if err != nil { + t.Errorf("failed to create temp dir %v", err) + return + } + defer func() { + _ = os.Remove(tmpDir) + }() + + caTlsCert, _ := loadPEMCert(testCaCrt, testCaKey) + caCert, _ := x509.ParseCertificate(caTlsCert.Certificate[0]) + + options := &tlsOptions{ + ecdsaCurve: "P256", + certCachePath: tmpDir, + validity: validity{ + domain: certValidity{ + notAfterRelative: time.Hour * 24 * 30, + notBeforeRelative: time.Hour * 24 * 30, + }, + ca: certValidity{ + notAfterRelative: time.Hour * 24 * 30, + notBeforeRelative: time.Hour * 24 * 30, + }, + }, + } + + logger, _ := zap.NewDevelopment() + + certStore := certStore{ + options: options, + caCert: caCert, + logger: logger, + caPrivateKey: caTlsCert.PrivateKey, + } + + type args struct { + domain string + ip string + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Test create google.com cert", + args: args{ + domain: "google.com", + ip: "127.0.0.1", + }, + wantErr: false, + }, + { + name: "Test create golem.de cert", + args: args{ + domain: "golem.de", + ip: "127.0.0.1", + }, + wantErr: false, + }, + { + name: "Test create golem.de cert with any IP address", + args: args{ + domain: "golem.de", + ip: "10.10.0.10", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if domainCert, err := certStore.generateDomainCert( + tt.args.domain, + tt.args.ip, + ); (err != nil) != tt.wantErr || reflect.DeepEqual(domainCert, tls.Certificate{}) { + t.Errorf("generateDomainCert() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_certStore_initCaCert(t *testing.T) { + + tmpDir, err := ioutil.TempDir(os.TempDir(), "*-inetmock") + if err != nil { + t.Errorf("failed to create temp dir %v", err) + return + } + defer func() { + _ = os.Remove(tmpDir) + }() + + caCertPath := filepath.Join(tmpDir, "cacert.pem") + caKeyPath := filepath.Join(tmpDir, "cacert.key") + + if err := ioutil.WriteFile(caCertPath, testCaCrt, 0600); err != nil { + t.Errorf("failed to write cacert.pem %v", err) + return + } + if err := ioutil.WriteFile(caKeyPath, testCaKey, 0600); err != nil { + t.Errorf("failed to write cacert.key %v", err) + return + } + + type fields struct { + options *tlsOptions + caCert *x509.Certificate + } + tests := []struct { + name string + fields fields + wantErr bool + }{ + { + name: "Init CA cert from file", + wantErr: false, + fields: fields{ + options: &tlsOptions{ + rootCaCert: cert{ + publicKeyPath: caCertPath, + privateKeyPath: caKeyPath, + }, + }, + }, + }, + { + name: "Init CA with new cert", + wantErr: false, + fields: fields{ + options: &tlsOptions{ + rootCaCert: cert{ + publicKeyPath: filepath.Join(tmpDir, "nonexistent.pem"), + privateKeyPath: filepath.Join(tmpDir, "nonexistent.key"), + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cs := &certStore{ + options: tt.fields.options, + caCert: tt.fields.caCert, + } + if err := cs.initCaCert(); (err != nil) != tt.wantErr || cs.caCert == nil { + t.Errorf("initCaCert() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/pkg/plugins/tls_interceptor/certs.go b/pkg/plugins/tls_interceptor/certs.go new file mode 100644 index 0000000..17b309e --- /dev/null +++ b/pkg/plugins/tls_interceptor/certs.go @@ -0,0 +1,106 @@ +package main + +import ( + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "math/big" + "os" +) + +type curveType string + +const ( + certificateBlockType = "CERTIFICATE" + privateKeyBlockType = "PRIVATE KEY" + + curveTypeP224 curveType = "P224" + curveTypeP256 curveType = "P256" + curveTypeP384 curveType = "P384" + curveTypeP521 curveType = "P521" + curveTypeED25519 curveType = "ED25519" +) + +func loadPEMCert(certPEMBytes []byte, keyPEMBytes []byte) (*tls.Certificate, error) { + cert, err := tls.X509KeyPair(certPEMBytes, keyPEMBytes) + return &cert, err +} + +func parseCert(derBytes []byte, privateKeyBytes []byte) (*tls.Certificate, error) { + pemEncodedPublicKey := pem.EncodeToMemory(&pem.Block{Type: certificateBlockType, Bytes: derBytes}) + pemEncodedPrivateKey := pem.EncodeToMemory(&pem.Block{Type: privateKeyBlockType, Bytes: privateKeyBytes}) + cert, err := tls.X509KeyPair(pemEncodedPublicKey, pemEncodedPrivateKey) + return &cert, err +} + +func writePublicKey(crtOutPath string, derBytes []byte) (err error) { + var certOut *os.File + if certOut, err = os.Create(crtOutPath); err != nil { + return + } + if err = pem.Encode(certOut, &pem.Block{Type: certificateBlockType, Bytes: derBytes}); err != nil { + return + } + err = certOut.Close() + return +} + +func writePrivateKey(keyOutPath string, privateKeyBytes []byte) (err error) { + var keyOut *os.File + if keyOut, err = os.OpenFile(keyOutPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600); err != nil { + return + } + + if err = pem.Encode(keyOut, &pem.Block{Type: privateKeyBlockType, Bytes: privateKeyBytes}); err != nil { + return + } + + err = keyOut.Close() + return +} + +func certShouldBeRenewed(timeSource timeSource, cert *x509.Certificate) bool { + lifetime := cert.NotAfter.Sub(cert.NotBefore) + // if the cert is closer to the end of the lifetime than lifetime/2 it should be renewed + if cert.NotAfter.Sub(timeSource.UTCNow()) < lifetime/4 { + return true + } + return false +} + +func generateSerialNumber() (*big.Int, error) { + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + return rand.Int(rand.Reader, serialNumberLimit) +} + +func privateKeyForCurve(options *tlsOptions) (privateKey interface{}, err error) { + switch options.ecdsaCurve { + case "P224": + privateKey, err = ecdsa.GenerateKey(elliptic.P224(), rand.Reader) + case "P256": + privateKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + case "P384": + privateKey, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + case "P521": + privateKey, err = ecdsa.GenerateKey(elliptic.P521(), rand.Reader) + default: + _, privateKey, err = ed25519.GenerateKey(rand.Reader) + } + + return +} + +func publicKey(privateKey interface{}) interface{} { + switch k := privateKey.(type) { + case *ecdsa.PrivateKey: + return &k.PublicKey + case ed25519.PrivateKey: + return k.Public().(ed25519.PublicKey) + default: + return nil + } +} diff --git a/pkg/plugins/tls_interceptor/certs_test.go b/pkg/plugins/tls_interceptor/certs_test.go new file mode 100644 index 0000000..8d28bdc --- /dev/null +++ b/pkg/plugins/tls_interceptor/certs_test.go @@ -0,0 +1,74 @@ +package main + +import ( + "crypto/x509" + "testing" + "time" +) + +type testTimeSource struct { + nowValue time.Time +} + +func (t testTimeSource) UTCNow() time.Time { + return t.nowValue +} + +func Test_certShouldBeRenewed(t *testing.T) { + type args struct { + timeSource timeSource + cert *x509.Certificate + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "Detect cert is expired", + want: true, + args: args{ + cert: &x509.Certificate{ + NotAfter: time.Now().UTC().Add(1 * time.Hour), + NotBefore: time.Now().UTC().Add(-1 * time.Hour), + }, + timeSource: testTimeSource{ + nowValue: time.Now().UTC().Add(2 * time.Hour), + }, + }, + }, + { + name: "Detect cert should be renewed", + want: true, + args: args{ + cert: &x509.Certificate{ + NotAfter: time.Now().UTC().Add(1 * time.Hour), + NotBefore: time.Now().UTC().Add(-1 * time.Hour), + }, + timeSource: testTimeSource{ + nowValue: time.Now().UTC().Add(45 * time.Minute), + }, + }, + }, + { + name: "Detect cert shouldn't be renewed", + want: false, + args: args{ + cert: &x509.Certificate{ + NotAfter: time.Now().UTC().Add(1 * time.Hour), + NotBefore: time.Now().UTC().Add(-1 * time.Hour), + }, + timeSource: testTimeSource{ + nowValue: time.Now().UTC().Add(25 * time.Minute), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := certShouldBeRenewed(tt.args.timeSource, tt.args.cert); got != tt.want { + t.Errorf("certShouldBeRenewed() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/plugins/tls_interceptor/generate_ca_cmd.go b/pkg/plugins/tls_interceptor/generate_ca_cmd.go new file mode 100644 index 0000000..74d61d9 --- /dev/null +++ b/pkg/plugins/tls_interceptor/generate_ca_cmd.go @@ -0,0 +1,83 @@ +package main + +import ( + "github.com/spf13/cobra" + "go.uber.org/zap" +) + +const ( + generateCACertOutPath = "cert-out" + generateCAKeyOutPath = "key-out" + generateCACurveName = "curve" +) + +func generateCACmd(logger *zap.Logger) *cobra.Command { + cmd := &cobra.Command{ + Use: "generate-ca", + Short: "Generate a new CA certificate and corresponding key", + Long: ``, + Run: runGenerateCA(logger), + } + + cmd.Flags().String(generateCACertOutPath, "", "Path where CA cert file should be stored") + cmd.Flags().String(generateCAKeyOutPath, "", "Path where CA key file should be stored") + cmd.Flags().String(generateCACurveName, "", "Name of the curve to use, if empty ED25519 is used, other valid values are [P224, P256,P384,P521]") + + return cmd +} + +func runGenerateCA(logger *zap.Logger) func(cmd *cobra.Command, args []string) { + return func(cmd *cobra.Command, args []string) { + var certOutPath, keyOutPath, curveName string + var err error + if certOutPath, err = cmd.Flags().GetString(generateCACertOutPath); err != nil { + logger.Error( + "failed to parse parse flag", + zap.String("flag", generateCACertOutPath), + zap.Error(err), + ) + return + } + if keyOutPath, err = cmd.Flags().GetString(generateCAKeyOutPath); err != nil { + logger.Error( + "failed to parse parse flag", + zap.String("flag", generateCAKeyOutPath), + zap.Error(err), + ) + return + } + if curveName, err = cmd.Flags().GetString(generateCACurveName); err != nil { + logger.Error( + "failed to parse parse flag", + zap.String("flag", generateCACurveName), + zap.Error(err), + ) + return + } + + logger := logger.With( + zap.String(generateCACurveName, curveName), + zap.String(generateCACertOutPath, certOutPath), + zap.String(generateCAKeyOutPath, keyOutPath), + ) + + certStore := certStore{ + options: &tlsOptions{ + ecdsaCurve: curveType(curveName), + rootCaCert: cert{ + publicKeyPath: certOutPath, + privateKeyPath: keyOutPath, + }, + }, + } + + if _, _, err := certStore.generateCaCert(); err != nil { + logger.Error( + "failed to generate CA cert", + zap.Error(err), + ) + } else { + logger.Info("Successfully generated CA cert") + } + } +} diff --git a/pkg/plugins/tls_interceptor/init.go b/pkg/plugins/tls_interceptor/init.go new file mode 100644 index 0000000..a03b56d --- /dev/null +++ b/pkg/plugins/tls_interceptor/init.go @@ -0,0 +1,23 @@ +package main + +import ( + "github.com/baez90/inetmock/internal/plugins" + "github.com/baez90/inetmock/pkg/api" + "github.com/baez90/inetmock/pkg/logging" + "go.uber.org/zap" + "sync" +) + +func init() { + logger, _ := logging.CreateLogger() + logger = logger.With( + zap.String("ProtocolHandler", name), + ) + + plugins.Registry().RegisterHandler(name, func() api.ProtocolHandler { + return &tlsInterceptor{ + logger: logger, + currentConnectionsCount: &sync.WaitGroup{}, + } + }, generateCACmd(logger)) +} diff --git a/pkg/plugins/tls_interceptor/main.go b/pkg/plugins/tls_interceptor/main.go new file mode 100644 index 0000000..0ec98d9 --- /dev/null +++ b/pkg/plugins/tls_interceptor/main.go @@ -0,0 +1,160 @@ +package main + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "github.com/baez90/inetmock/internal/config" + "go.uber.org/zap" + "net" + "sync" + "time" +) + +const ( + name = "tls_interceptor" +) + +type tlsInterceptor struct { + logger *zap.Logger + listener net.Listener + certStore *certStore + options *tlsOptions + shutdownRequested bool + currentConnectionsCount *sync.WaitGroup + currentConnections []*proxyConn +} + +func (t *tlsInterceptor) Run(config config.HandlerConfig) { + var err error + t.options = loadFromConfig(config.Options()) + addr := fmt.Sprintf("%s:%d", config.ListenAddress(), config.Port()) + + t.logger = t.logger.With( + zap.String("address", addr), + zap.String("target", t.options.redirectionTarget.address()), + ) + + t.certStore = &certStore{ + options: t.options, + certCache: make(map[string]*tls.Certificate), + logger: t.logger, + } + + if err = t.certStore.initCaCert(); err != nil { + t.logger.Error( + "failed to initialize CA cert", + zap.Error(err), + ) + } + + rootCaPool := x509.NewCertPool() + rootCaPool.AddCert(t.certStore.caCert) + + tlsConfig := &tls.Config{ + GetCertificate: t.getCert, + RootCAs: rootCaPool, + } + + if t.listener, err = tls.Listen("tcp", addr, tlsConfig); err != nil { + t.logger.Fatal( + "failed to create tls listener", + zap.Error(err), + ) + return + } + + go t.startListener() +} + +func (t *tlsInterceptor) Shutdown(wg *sync.WaitGroup) { + t.shutdownRequested = true + done := make(chan struct{}) + go func() { + t.currentConnectionsCount.Wait() + close(done) + }() + + select { + case <-done: + wg.Done() + case <-time.After(5 * time.Second): + for _, proxyConn := range t.currentConnections { + if err := proxyConn.Close(); err != nil { + t.logger.Error( + "error while closing remaining proxy connections", + zap.Error(err), + ) + } + } + wg.Done() + } +} + +func (t *tlsInterceptor) startListener() { + for !t.shutdownRequested { + conn, err := t.listener.Accept() + if err != nil { + t.logger.Error( + "error during accept", + zap.Error(err), + ) + continue + } + + t.currentConnectionsCount.Add(1) + go t.proxyConn(conn) + } +} + +func (t *tlsInterceptor) getCert(info *tls.ClientHelloInfo) (cert *tls.Certificate, err error) { + var localIp string + if localIp, err = extractIPFromAddress(info.Conn.LocalAddr().String()); err != nil { + localIp = "127.0.0.1" + } + if cert, err = t.certStore.getCertificate(info.ServerName, localIp); err != nil { + t.logger.Error( + "error while resolving certificate", + zap.String("serverName", info.ServerName), + zap.String("localAddr", localIp), + zap.Error(err), + ) + } + + return +} + +func (t *tlsInterceptor) proxyConn(conn net.Conn) { + defer conn.Close() + + rAddr, err := net.ResolveTCPAddr("tcp", t.options.redirectionTarget.address()) + if err != nil { + t.logger.Error( + "failed to resolve proxy target", + zap.Error(err), + ) + } + + targetConn, err := net.DialTCP("tcp", nil, rAddr) + if err != nil { + t.logger.Error( + "failed to connect to proxy target", + zap.Error(err), + ) + return + } + defer targetConn.Close() + + t.currentConnections = append(t.currentConnections, &proxyConn{ + source: conn, + target: targetConn, + }) + + Pipe(conn, targetConn) + + t.currentConnectionsCount.Done() + t.logger.Info( + "connection closed", + zap.String("remoteAddr", conn.RemoteAddr().String()), + ) +} diff --git a/pkg/plugins/tls_interceptor/protocol_options.go b/pkg/plugins/tls_interceptor/protocol_options.go new file mode 100644 index 0000000..c13eedf --- /dev/null +++ b/pkg/plugins/tls_interceptor/protocol_options.go @@ -0,0 +1,78 @@ +package main + +import ( + "fmt" + "github.com/spf13/viper" + "time" +) + +const ( + certCachePathConfigKey = "certCachePath" + ecdsaCurveConfigKey = "ecdsaCurve" + targetIpAddressConfigKey = "target.ipAddress" + targetPortConfigKey = "target.port" + publicKeyConfigKey = "rootCaCert.publicKey" + privateKeyPathConfigKey = "rootCaCert.privateKey" + caCertValidityNotBeforeKey = "validity.ca.notBeforeRelative" + caCertValidityNotAfterKey = "validity.ca.notAfterRelative" + domainCertValidityNotBeforeKey = "validity.domain.notBeforeRelative" + domainCertValidityNotAfterKey = "validity.domain.notAfterRelative" +) + +type cert struct { + publicKeyPath string + privateKeyPath string +} + +type certValidity struct { + notBeforeRelative time.Duration + notAfterRelative time.Duration +} + +type validity struct { + ca certValidity + domain certValidity +} + +type redirectionTarget struct { + ipAddress string + port uint16 +} + +func (rt redirectionTarget) address() string { + return fmt.Sprintf("%s:%d", rt.ipAddress, rt.port) +} + +type tlsOptions struct { + rootCaCert cert + certCachePath string + redirectionTarget redirectionTarget + ecdsaCurve curveType + validity validity +} + +func loadFromConfig(config *viper.Viper) *tlsOptions { + + return &tlsOptions{ + certCachePath: config.GetString(certCachePathConfigKey), + ecdsaCurve: curveType(config.GetString(ecdsaCurveConfigKey)), + redirectionTarget: redirectionTarget{ + ipAddress: config.GetString(targetIpAddressConfigKey), + port: uint16(config.GetInt(targetPortConfigKey)), + }, + validity: validity{ + ca: certValidity{ + notBeforeRelative: config.GetDuration(caCertValidityNotBeforeKey), + notAfterRelative: config.GetDuration(caCertValidityNotAfterKey), + }, + domain: certValidity{ + notBeforeRelative: config.GetDuration(domainCertValidityNotBeforeKey), + notAfterRelative: config.GetDuration(domainCertValidityNotAfterKey), + }, + }, + rootCaCert: cert{ + publicKeyPath: config.GetString(publicKeyConfigKey), + privateKeyPath: config.GetString(privateKeyPathConfigKey), + }, + } +} diff --git a/pkg/plugins/tls_interceptor/proxy.go b/pkg/plugins/tls_interceptor/proxy.go new file mode 100644 index 0000000..8d74f14 --- /dev/null +++ b/pkg/plugins/tls_interceptor/proxy.go @@ -0,0 +1,51 @@ +package main + +import ( + "net" +) + +func chanFromConn(conn net.Conn) chan []byte { + c := make(chan []byte) + + go func() { + b := make([]byte, 1024) + + for { + n, err := conn.Read(b) + if n > 0 { + res := make([]byte, n) + // Copy the buffer so it doesn't get changed while read by the recipient. + copy(res, b[:n]) + c <- res + } + if err != nil { + c <- nil + break + } + } + }() + + return c +} + +func Pipe(conn1 net.Conn, conn2 net.Conn) { + chan1 := chanFromConn(conn1) + chan2 := chanFromConn(conn2) + + for { + select { + case b1 := <-chan1: + if b1 == nil { + return + } else { + conn2.Write(b1) + } + case b2 := <-chan2: + if b2 == nil { + return + } else { + conn1.Write(b2) + } + } + } +} diff --git a/pkg/plugins/tls_interceptor/proxy_conn.go b/pkg/plugins/tls_interceptor/proxy_conn.go new file mode 100644 index 0000000..92d650b --- /dev/null +++ b/pkg/plugins/tls_interceptor/proxy_conn.go @@ -0,0 +1,22 @@ +package main + +import ( + "fmt" + "net" +) + +type proxyConn struct { + source net.Conn + target net.Conn +} + +func (p *proxyConn) Close() error { + var err error + if targetErr := p.target.Close(); targetErr != nil { + err = fmt.Errorf("error while closing target conn: %w", targetErr) + } + if sourceErr := p.source.Close(); sourceErr != nil { + err = fmt.Errorf("error while closing source conn: %w", err) + } + return err +} diff --git a/pkg/plugins/tls_interceptor/test_setup.go b/pkg/plugins/tls_interceptor/test_setup.go new file mode 100644 index 0000000..bc90685 --- /dev/null +++ b/pkg/plugins/tls_interceptor/test_setup.go @@ -0,0 +1,55 @@ +package main + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "time" +) + +var ( + testCaCrt []byte + testCaKey []byte +) + +func init() { + tmpDir, err := ioutil.TempDir(os.TempDir(), "*-inetmock") + if err != nil { + panic(fmt.Sprintf("failed to create temp dir %v", err)) + } + + options := &tlsOptions{ + ecdsaCurve: "P256", + rootCaCert: cert{ + publicKeyPath: filepath.Join(tmpDir, "localhost.pem"), + privateKeyPath: filepath.Join(tmpDir, "localhost.key"), + }, + validity: validity{ + ca: certValidity{ + notBeforeRelative: time.Hour * 24 * 30, + notAfterRelative: time.Hour * 24 * 30, + }, + }, + } + + certStore := certStore{ + options: options, + keyProvider: func() (key interface{}, err error) { + return privateKeyForCurve(options) + }, + } + + defer func() { + _ = os.Remove(tmpDir) + }() + + _, _, err = certStore.generateCaCert() + + testCaCrt, _ = ioutil.ReadFile(options.rootCaCert.publicKeyPath) + testCaKey, _ = ioutil.ReadFile(options.rootCaCert.privateKeyPath) + + if err != nil { + panic(fmt.Sprintf("failed to generate CA cert %v", err)) + } +} diff --git a/pkg/plugins/tls_interceptor/time_source.go b/pkg/plugins/tls_interceptor/time_source.go new file mode 100644 index 0000000..ed7c7ff --- /dev/null +++ b/pkg/plugins/tls_interceptor/time_source.go @@ -0,0 +1,18 @@ +package main + +import "time" + +type timeSource interface { + UTCNow() time.Time +} + +func createTimeSource() timeSource { + return &defaultTimeSource{} +} + +type defaultTimeSource struct { +} + +func (d defaultTimeSource) UTCNow() time.Time { + return time.Now().UTC() +}