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
This commit is contained in:
parent
6012f104af
commit
a720b0ee41
44 changed files with 2277 additions and 0 deletions
18
.dockerignore
Normal file
18
.dockerignore
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
###############
|
||||||
|
# Directories #
|
||||||
|
###############
|
||||||
|
|
||||||
|
.git/
|
||||||
|
plugins/
|
||||||
|
|
||||||
|
#########
|
||||||
|
# Files #
|
||||||
|
#########
|
||||||
|
|
||||||
|
*.out
|
||||||
|
main
|
||||||
|
inetmock
|
||||||
|
README.md
|
||||||
|
.dockerignore
|
||||||
|
.gitignore
|
||||||
|
Dockerfile
|
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
assets/fakeFiles/* filter=lfs diff=lfs merge=lfs -text
|
17
.gitignore
vendored
Normal file
17
.gitignore
vendored
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
#########
|
||||||
|
# files #
|
||||||
|
#########
|
||||||
|
|
||||||
|
**/cov.out
|
||||||
|
**/cov-raw.out
|
||||||
|
**/*.so
|
||||||
|
*.key
|
||||||
|
*.pem
|
||||||
|
inetmock
|
||||||
|
main
|
||||||
|
|
||||||
|
###############
|
||||||
|
# directories #
|
||||||
|
###############
|
||||||
|
|
||||||
|
.idea/
|
43
Dockerfile
Normal file
43
Dockerfile
Normal file
|
@ -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"]
|
64
Makefile
Normal file
64
Makefile
Normal file
|
@ -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 $@
|
BIN
assets/fakeFiles/default.gif
(Stored with Git LFS)
Normal file
BIN
assets/fakeFiles/default.gif
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/fakeFiles/default.html
(Stored with Git LFS)
Normal file
BIN
assets/fakeFiles/default.html
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/fakeFiles/default.ico
(Stored with Git LFS)
Normal file
BIN
assets/fakeFiles/default.ico
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/fakeFiles/default.jpg
(Stored with Git LFS)
Normal file
BIN
assets/fakeFiles/default.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/fakeFiles/default.png
(Stored with Git LFS)
Normal file
BIN
assets/fakeFiles/default.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/fakeFiles/default.txt
(Stored with Git LFS)
Normal file
BIN
assets/fakeFiles/default.txt
(Stored with Git LFS)
Normal file
Binary file not shown.
41
config.yaml
Normal file
41
config.yaml
Normal file
|
@ -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
|
12
go.mod
Normal file
12
go.mod
Normal file
|
@ -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
|
||||||
|
)
|
187
go.sum
Normal file
187
go.sum
Normal file
|
@ -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=
|
46
internal/cmd/init.go
Normal file
46
internal/cmd/init.go
Normal file
|
@ -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
|
||||||
|
}
|
22
internal/cmd/plugins.go
Normal file
22
internal/cmd/plugins.go
Normal file
|
@ -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)
|
||||||
|
}
|
111
internal/cmd/root.go
Normal file
111
internal/cmd/root.go
Normal file
|
@ -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()
|
||||||
|
}
|
6
internal/config/constants.go
Normal file
6
internal/config/constants.go
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
const (
|
||||||
|
EndpointsKey = "endpoints"
|
||||||
|
OptionsKey = "options"
|
||||||
|
)
|
49
internal/config/handler_config.go
Normal file
49
internal/config/handler_config.go
Normal file
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
51
internal/config/loading.go
Normal file
51
internal/config/loading.go
Normal file
|
@ -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
|
||||||
|
}
|
78
internal/plugins/loading.go
Normal file
78
internal/plugins/loading.go
Normal file
|
@ -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),
|
||||||
|
}
|
||||||
|
}
|
19
main.go
Normal file
19
main.go
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
16
pkg/api/protocol_handler.go
Normal file
16
pkg/api/protocol_handler.go
Normal file
|
@ -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)
|
||||||
|
}
|
42
pkg/logging/factory.go
Normal file
42
pkg/logging/factory.go
Normal file
|
@ -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()
|
||||||
|
}
|
19
pkg/path/helpers.go
Normal file
19
pkg/path/helpers.go
Normal file
|
@ -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()
|
||||||
|
}
|
43
pkg/plugins/http_mock/Makefile
Normal file
43
pkg/plugins/http_mock/Makefile
Normal file
|
@ -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
|
116
pkg/plugins/http_mock/main.go
Normal file
116
pkg/plugins/http_mock/main.go
Normal file
|
@ -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{},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
33
pkg/plugins/http_mock/protocol_options.go
Normal file
33
pkg/plugins/http_mock/protocol_options.go
Normal file
|
@ -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
|
||||||
|
}
|
34
pkg/plugins/http_mock/regex_router.go
Normal file
34
pkg/plugins/http_mock/regex_router.go
Normal file
|
@ -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)
|
||||||
|
}
|
43
pkg/plugins/tls_interceptor/Makefile
Normal file
43
pkg/plugins/tls_interceptor/Makefile
Normal file
|
@ -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
|
20
pkg/plugins/tls_interceptor/addr_utils.go
Normal file
20
pkg/plugins/tls_interceptor/addr_utils.go
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
44
pkg/plugins/tls_interceptor/addr_utils_test.go
Normal file
44
pkg/plugins/tls_interceptor/addr_utils_test.go
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
203
pkg/plugins/tls_interceptor/cert_store.go
Normal file
203
pkg/plugins/tls_interceptor/cert_store.go
Normal file
|
@ -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
|
||||||
|
}
|
211
pkg/plugins/tls_interceptor/cert_store_test.go
Normal file
211
pkg/plugins/tls_interceptor/cert_store_test.go
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
106
pkg/plugins/tls_interceptor/certs.go
Normal file
106
pkg/plugins/tls_interceptor/certs.go
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
74
pkg/plugins/tls_interceptor/certs_test.go
Normal file
74
pkg/plugins/tls_interceptor/certs_test.go
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
83
pkg/plugins/tls_interceptor/generate_ca_cmd.go
Normal file
83
pkg/plugins/tls_interceptor/generate_ca_cmd.go
Normal file
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
23
pkg/plugins/tls_interceptor/init.go
Normal file
23
pkg/plugins/tls_interceptor/init.go
Normal file
|
@ -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))
|
||||||
|
}
|
160
pkg/plugins/tls_interceptor/main.go
Normal file
160
pkg/plugins/tls_interceptor/main.go
Normal file
|
@ -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()),
|
||||||
|
)
|
||||||
|
}
|
78
pkg/plugins/tls_interceptor/protocol_options.go
Normal file
78
pkg/plugins/tls_interceptor/protocol_options.go
Normal file
|
@ -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),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
51
pkg/plugins/tls_interceptor/proxy.go
Normal file
51
pkg/plugins/tls_interceptor/proxy.go
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
22
pkg/plugins/tls_interceptor/proxy_conn.go
Normal file
22
pkg/plugins/tls_interceptor/proxy_conn.go
Normal file
|
@ -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
|
||||||
|
}
|
55
pkg/plugins/tls_interceptor/test_setup.go
Normal file
55
pkg/plugins/tls_interceptor/test_setup.go
Normal file
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
18
pkg/plugins/tls_interceptor/time_source.go
Normal file
18
pkg/plugins/tls_interceptor/time_source.go
Normal file
|
@ -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()
|
||||||
|
}
|
Loading…
Reference in a new issue