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:
Peter 2020-04-01 04:08:21 +02:00
parent 6012f104af
commit a720b0ee41
Signed by: prskr
GPG key ID: C1DB5D2E8DB512F9
44 changed files with 2277 additions and 0 deletions

18
.dockerignore Normal file
View file

@ -0,0 +1,18 @@
###############
# Directories #
###############
.git/
plugins/
#########
# Files #
#########
*.out
main
inetmock
README.md
.dockerignore
.gitignore
Dockerfile

1
.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
assets/fakeFiles/* filter=lfs diff=lfs merge=lfs -text

17
.gitignore vendored Normal file
View file

@ -0,0 +1,17 @@
#########
# files #
#########
**/cov.out
**/cov-raw.out
**/*.so
*.key
*.pem
inetmock
main
###############
# directories #
###############
.idea/

43
Dockerfile Normal file
View 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
View 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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

BIN
assets/fakeFiles/default.txt (Stored with Git LFS) Normal file

Binary file not shown.

41
config.yaml Normal file
View 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
View 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
View 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
View 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
View 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
View 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()
}

View file

@ -0,0 +1,6 @@
package config
const (
EndpointsKey = "endpoints"
OptionsKey = "options"
)

View 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,
}
}

View 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
}

View 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
View 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)
}
}

View 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
View 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
View 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()
}

View 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

View 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{},
}
})
}

View 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
}

View 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)
}

View 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

View 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)
}
}

View 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)
}
})
}
}

View 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
}

View 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)
}
})
}
}

View 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
}
}

View 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)
}
})
}
}

View 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")
}
}
}

View 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))
}

View 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()),
)
}

View 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),
},
}
}

View 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)
}
}
}
}

View 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
}

View 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))
}
}

View 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()
}