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