diff --git a/.dockerignore b/.dockerignore index cb2153d..c39d238 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,11 @@ +/.dockerignore +/Dockerfile +/Dockerfile.buildkit /.git/ -/keys/ +/.github/ +/.gitignore +/deploy/ +!/deploy/pubkeys/.gitkeep +/dist/ /sish +/.vscode/ \ No newline at end of file diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 53f52fe..b9998df 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -9,7 +9,7 @@ jobs: - uses: actions/checkout@v1 - name: Lint the codebase run: | - docker run --rm -v $(pwd):/app -w /app golangci/golangci-lint:v1.21.0 golangci-lint run -E goimports + docker run --rm -v $(pwd):/app -w /app golangci/golangci-lint:v1.27.0 golangci-lint run -E goimports - name: Set up Docker Buildx uses: crazy-max/ghaction-docker-buildx@v1 with: @@ -29,7 +29,10 @@ jobs: if [[ ${GITHUB_REF} =~ ^refs\/tags\/v.*$ ]] then REF="${GITHUB_REF//refs\/tags\/}" - OTHER_TAGS="${OTHER_TAGS} -t ${GITHUB_REPOSITORY}:latest" + if ! [[ ${GITHUB_REF} =~ ^refs\/tags\/v.*-.*$ ]] + then + OTHER_TAGS="${OTHER_TAGS} -t ${GITHUB_REPOSITORY}:latest" + fi fi docker buildx build \ @@ -42,4 +45,5 @@ jobs: --build-arg DATE=${DATE} \ -t ${GITHUB_REPOSITORY}:${GITHUB_SHA} \ -t ${GITHUB_REPOSITORY}:${REF} \ + -f Dockerfile.buildkit \ ${OTHER_TAGS} . diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index a40fdd2..aabf052 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -9,17 +9,12 @@ jobs: - uses: actions/checkout@v1 - name: Lint the codebase run: | - docker run --rm -v $(pwd):/app -w /app golangci/golangci-lint:v1.21.0 golangci-lint run -E goimports - - name: Set up Docker Buildx - uses: crazy-max/ghaction-docker-buildx@v1 - with: - version: latest + docker run --rm -v $(pwd):/app -w /app golangci/golangci-lint:v1.27.0 golangci-lint run -E goimports - name: Build and the Docker images run: | DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" - docker buildx build \ - --platform linux/arm/v7,linux/arm64,linux/amd64 \ + docker build \ --cache-from ${GITHUB_REPOSITORY}-cache \ --build-arg VERSION=${GITHUB_SHA} \ --build-arg COMMIT=${GITHUB_SHA} \ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f59d22d..d5c2f36 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v1 with: - go-version: 1.13 + go-version: 1.14 - name: Run GoReleaser uses: goreleaser/goreleaser-action@v1 with: diff --git a/.gitignore b/.gitignore index dd1261f..31c6c2c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,4 @@ -keys/* -!keys/.gitkeep -pubkeys/* -!pubkeys/.gitkeep -ssl/* -!ssl/.gitkeep -sish -deploy/* -!deploy/docker-compose.yml +deploy/ dist/ +sish __debug_bin \ No newline at end of file diff --git a/.goreleaser.yml b/.goreleaser.yml index a20dd15..2764e98 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -3,8 +3,10 @@ before: - go mod tidy - go generate ./... builds: -- env: - - CGO_ENABLED=0 +- ldflags: + - -s -w -X github.com/antoniomika/sish/cmd.Version={{ .Version }} -X github.com/antoniomika/sish/cmd.Commit={{ .Commit }} -X github.com/antoniomika/sish/cmd.Date={{ .Date }} + env: + - CGO_ENABLED=0 goos: - linux - win @@ -31,5 +33,5 @@ archives: files: - LICENSE* - README* - - pubkeys/ + - deploy/pubkeys/ - templates/**/* diff --git a/.vscode/launch.json b/.vscode/launch.json index 70a565e..1f55fbc 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -12,20 +12,24 @@ "program": "${workspaceFolder}", "env": {}, "args": [ - "-sish.auth=false", - "-sish.debug=true", - "-sish.subdomainlen=3", - "-sish.httpsenabled=false", - "-sish.http=localhost:8081", - "-sish.addr=localhost:2222", - "-sish.domain=testing.ssi.sh", - "-sish.forcerandomsubdomain=false", - "-sish.bindrandom=false", - "-sish.tcpalias=true", - "-sish.proxyprotoenabled=false", - "-sish.logtoclient=true", - "-sish.adminenabled=true", - "-sish.serviceconsoleenabled=true" + "--debug=true", + "--bind-random-subdomains-length=3", + "--http-address=:8081", + "--ssh-address=:2222", + "--domain=testing.ssi.sh", + "--bind-random-subdomains=false", + "--bind-random-ports=false", + "--https=false", + "--authentication=false", + "--tcp-aliases=true", + "--proxy-protocol=false", + "--log-to-client=true", + "--admin-console=true", + "--service-console=true", + "--verify-ssl=false", + "--http-load-balancer=true", + "--tcp-load-balancer=true", + "--alias-load-balancer=true" ] } ] diff --git a/Dockerfile b/Dockerfile index 8da5487..a9a6800 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.13.2-alpine as builder +FROM golang:1.14-alpine as builder LABEL maintainer="Antonio Mika " ENV GOCACHE /gocache @@ -6,7 +6,7 @@ ENV CGO_ENABLED 0 WORKDIR /app -RUN apk add --no-cache git +RUN apk add --no-cache git ca-certificates COPY go.mod . COPY go.sum . @@ -19,7 +19,7 @@ ARG VERSION=dev ARG COMMIT=none ARG DATE=unknown -RUN go install -ldflags="-s -w -X main.version=${VERSION} -X main.commit=${COMMIT} -X main.date=${DATE}" +RUN go install -ldflags="-s -w -X github.com/antoniomika/sish/cmd.Version=${VERSION} -X github.com/antoniomika/sish/cmd.Commit=${COMMIT} -X github.com/antoniomika/sish/cmd.Date=${DATE}" RUN go test -i ./... FROM scratch @@ -28,7 +28,8 @@ LABEL maintainer="Antonio Mika " WORKDIR /app COPY --from=builder /tmp /tmp -COPY --from=builder /app/pubkeys /app/pubkeys +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +COPY --from=builder /app/deploy/pubkeys /app/deploy/pubkeys COPY --from=builder /app/templates /app/templates COPY --from=builder /go/bin/sish /app/sish diff --git a/Dockerfile.buildkit b/Dockerfile.buildkit new file mode 100644 index 0000000..4214dfe --- /dev/null +++ b/Dockerfile.buildkit @@ -0,0 +1,40 @@ +FROM --platform=$BUILDPLATFORM golang:1.14-alpine as builder +LABEL maintainer="Antonio Mika " + +ENV GOCACHE /gocache +ENV CGO_ENABLED 0 + +WORKDIR /app + +RUN apk add --no-cache git ca-certificates + +COPY go.mod . +COPY go.sum . + +RUN go mod download + +COPY . . + +ARG VERSION=dev +ARG COMMIT=none +ARG DATE=unknown + +ARG TARGETOS +ARG TARGETARCH + +ENV GOOS=${TARGETOS} GOARCH=${TARGETARCH} + +RUN go build -o /go/bin/sish -ldflags="-s -w -X github.com/antoniomika/sish/cmd.Version=${VERSION} -X github.com/antoniomika/sish/cmd.Commit=${COMMIT} -X github.com/antoniomika/sish/cmd.Date=${DATE}" + +FROM scratch as release +LABEL maintainer="Antonio Mika " + +WORKDIR /app + +COPY --from=builder /tmp /tmp +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +COPY --from=builder /app/deploy/pubkeys /app/deploy/pubkeys +COPY --from=builder /app/templates /app/templates +COPY --from=builder /go/bin/sish /app/sish + +ENTRYPOINT ["/app/sish"] diff --git a/README.md b/README.md index 53990e1..148c961 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,16 @@ -sish -==== +# sish An open source serveo/ngrok alternative. -Deploy ------- +## Deploy -Builds are made automatically on Google Cloud Build and Dockerhub. Feel free to either use the automated binaries or to build your own. If you submit a PR and would like access to Google Cloud Build's output (including pre-made PR binaries), feel free to let me know. +Builds are made automatically for each commit to the repo and are pushed to Dockerhub. Builds are +tagged using a commit sha, branch name, tag, latest if released on master. +You can find a list [here](https://hub.docker.com/r/antoniomika/sish/tags). +Each release builds separate `sish` binaries that can be downloaded from +[here](https://github.com/antoniomika/sish/releases) for various OS/archs. +Feel free to either use the automated binaries or to build your own. If you submit a PR, images are +not built by default and will require a retag from a maintainer to be built. 1. Pull the Docker image - `docker pull antoniomika/sish:latest` @@ -18,49 +22,137 @@ Builds are made automatically on Google Cloud Build and Dockerhub. Feel free to -v ~/sish/keys:/keys \ -v ~/sish/pubkeys:/pubkeys \ --net=host antoniomika/sish:latest \ - -sish.addr=:22 \ - -sish.https=:443 \ - -sish.http=:80 \ - -sish.httpsenabled=true \ - -sish.httpspems=/ssl \ - -sish.keysdir=/pubkeys \ - -sish.pkloc=/keys/ssh_key \ - -sish.bindrandom=false + --ssh-address=:22 \ + --http-address=:80 \ + --https-address=:443 \ + --https=true \ + --https-certificate-directory=/ssl \ + --authentication-keys-directory=/pubkeys \ + --private-key-location=/keys/ssh_key \ + --bind-random-ports=false ``` 3. SSH to your host to communicate with sish - `ssh -p 2222 -R 80:localhost:8080 ssi.sh` -Docker Compose --------------- +## Docker Compose -You can also use Docker Compose to setup your sish instance. This includes taking care of SSL via Let's Encrypt for you. This uses the [adferrand/docker-letsencrypt-dns](https://github.com/adferrand/docker-letsencrypt-dns) container to handle issuing wildcard certifications over DNS. For more information on how to use this, head to that link above. Generally, you can deploy your service like so: +You can also use Docker Compose to setup your sish instance. This includes taking +care of SSL via Let's Encrypt for you. This uses the +[adferrand/dnsrobocert](https://github.com/adferrand/dnsrobocert) container to handle issuing wildcard +certifications over DNS. For more information on how to use this, head to that link above. Generally, you +can deploy your service like so: ```bash -DOMAIN=yourdomain.com \ -LETSENCRYPT_USER_MAIL=you@yourdomain.com \ -LEXICON_PROVIDER=cloudflare \ -LEXICON_PROVIDER_OPTIONS="--auth-username=you@yourdomain.com --auth-token=your-auth-token" \ docker-compose -f deploy/docker-compose.yml up -d ``` -How it works ------------- +The domain and DNS auth info in `deploy/docker-compose.yml` and `deploy/le-config.yml` should be updated +to reflect your needs. You will also need to create a symlink in the `./ssl` directory that points to the +Let's Encrypt certificates like: -SSH can normally forward local and remote ports. This service implements an SSH server that only does that and nothing else. The service supports multiplexing connections over HTTP/HTTPS with WebSocket support. Just assign a remote port as port `80` to proxy HTTP traffic and `443` to proxy HTTPS traffic. If you use any other remote port, the server will listen to the port for connections, but only if that port is available. +```bash +ln -s /etc/letsencrypt/live/ssi.sh/fullchain.pem deploy/ssl/ssi.sh.crt +ln -s /etc/letsencrypt/live/ssi.sh/privkey.pem deploy/ssl/ssi.sh.key +``` + +I use these files in my deployment of `ssi.sh` and have included them here for consistency. + +## How it works + +SSH can normally forward local and remote ports. This service implements +an SSH server that only handles forwarding and nothing else. The service supports +multiplexing connections over HTTP/HTTPS with WebSocket support. Just assign a +remote port as port `80` to proxy HTTP traffic and `443` to proxy HTTPS traffic. +If you use any other remote port, the server will listen to the port for TCP connections, +but only if that port is available. You can choose your own subdomain instead of relying on a randomly assigned one -by setting the `-sish.forcerandomsubdomain` option to `false` and then selecting a +by setting the `--bind-random-subdomains` option to `false` and then selecting a subdomain by prepending it to the remote port specifier: `ssh -p 2222 -R foo:80:localhost:8080 ssi.sh` If the selected subdomain is not taken, it will be assigned to your connection. -Authentication --------------- +## Supported forwarding types + +### HTTP forwarding + +sish can forward any number of HTTP connections through SSH. It also provides logging the connections +to the connected client that has forwarded the connection and a web interface to see full request and +responses made to each forwarded connection. Each webinterface can be unique to the forwarded connection or +use a unified access token. To make use of HTTP forwarding, ports `[80, 443]` are used to tell sish that a +HTTP connection is being forwarded and that HTTP virtualhosting should be defined for the service. For +example, let's say I'm +developing a HTTP webservice on my laptop at port `8080` that uses websockets and I want to show one of my +coworkers who is not near me. I can forward the connection like so: + +```bash +ssh -R hereiam:80:localhost:8080 ssi.sh +``` + +And then share the link `https://hereiam.ssi.sh` with my coworker. They should be able to access the service +seamlessly over HTTPS, with full websocket support working fine. Let's say `hereiam.ssi.sh` isn't available, +then sish will generate a random subdomain and give that to me. + +### TCP forwarding + +Any TCP based service can be used with sish for TCP and alias forwarding. TCP forwarding +will establish a remote port on the server that you deploy sish to and will forward all connections +to that port through the SSH connection and to your local device. For example, if I was to run +a SSH server on my laptop with port `22` and want to be able to access it from anywhere at `ssi.sh:2222`, +I can use an SSH command on my laptop like so to forward the connection: -If you want to use this service privately, it supports both public key and password authentication. To enable authentication, set `-sish.auth=true` as one of your CLI options and be sure to configure `-sish.password` or `-sish.keysdir` to your liking. The directory provided by `-sish.keysdir` is watched for changes and will reload the authorized keys automatically. The authorized cert index is regenerated on directory modification, so removed public keys will also automatically be removed. Files in this directory can either be single key per file, or multiple keys per file separated by newlines, similar to `authorized_keys`. Password auth can be disabled by setting `-sish.password=""` as a CLI option. +```bash +ssh -R 2222:localhost:22 ssi.sh +``` + +I can use the forwarded connection to then access my laptop from anywhere: + +```bash +ssh -p 2222 ssi.sh +``` + +### TCP alias forwarding + +Let's say instead I don't want the service to be accessible by the rest of the world, you can then use a TCP +alias. A TCP alias is a type of forwarded TCP connection that only exists inside of sish. You can gain access +to the alias by using SSH with the `-W` flag, which will forwarding the SSH process' stdin/stdout to the +fowarded TCP connection. In combination with authentication, this will guarantee your remote service is safe +from the rest of the world because you need to login to sish before you can access it. Changing the example +above for this would mean running the following command on my laptop: + +```bash +ssh -R mylaptop:22:localhost:22 ssi.sh +``` + +sish won't publish port 22 or 2222 to the rest of the world anymore, instead it'll retain a pointer saying +that TCP connections made from within SSH after a user has authenticated to `mylaptop:22` should be +forwarded to the forwarded TCP tunnel. Then I can use the forwarded connection access my laptop from +anywhere using: + +```bash +ssh -o ProxyCommand="ssh -W %h:%p ssi.sh" mylaptop +``` + +Shorthand for which is this with newer SSH versions: + +```bash +ssh -J ssi.sh mylaptop +``` + +## Authentication + +If you want to use this service privately, it supports both public key and password +authentication. To enable authentication, set `--authentication=true` as one of your CLI +options and be sure to configure `--authentication-password` or `--authentication-keys-directory` to your +liking. The directory provided by `--authentication-keys-directory` is watched for changes and will reload +the authorized keys automatically. The authorized cert index is regenerated on directory +modification, so removed public keys will also automatically be removed. Files in this +directory can either be single key per file, or multiple keys per file separated by newlines, +similar to `authorized_keys`. Password auth can be disabled by setting `--authentication-password=""` as a +CLI option. One of my favorite ways of using this for authentication is like so: @@ -68,124 +160,177 @@ One of my favorite ways of using this for authentication is like so: sish@sish0:~/sish/pubkeys# curl https://github.com/antoniomika.keys > antoniomika ``` -This will load my public keys from GitHub, place them in the directory that sish is watching, and then load the pubkey. As soon as this command is run, I can SSH normally and it will authorize me. +This will load my public keys from GitHub, place them in the directory that sish is watching, +and then load the pubkey. As soon as this command is run, I can SSH normally and it will authorize me. + +## Custom domains + +sish supports allowing users to bring custom domains to the service, but SSH key auth is required to be +enabled. To use this feature, you must setup TXT and CNAME/A records for the domain/subdomain you would +like to use for your forwarded connection. The CNAME/A record must point to the domain or IP that is hosting +sish. The TXT record must be be a `key=val` string that looks like: + +```text +sish=SSHKEYFINGERPRINT +``` -Whitelisting IPs ----------------- +Where `SSHKEYFINGERPRINT` is the fingerprint of the key used for logging into the server. You can set +multiple TXT records and sish will check all of them to ensure at least one is a match. You can retrieve +your key fingerprint by running: + +```bash +ssh-keygen -lf ~/.ssh/id_rsa | awk '{print $2}' +``` + +If you trust the users connecting to sish and would like to allow any domain to be used with sish +(bypassing verification), there are a few added flags to aid in this. This is especially useful when +adding multiple wildcard certificates to sish in order to not need to automatically provision Let's +Encrypt certs. To disable verfication, set `--bind-any-host=true`, which will allow and subdomain/domain +combination to be used. To only allow subdomains of a certain subset of domains, you can set `--bind-hosts` +to a comma separated list of domains that are allowed to be bound. + +To add certficates for sish to use, configure the `--https-certificate-directory` flag to point to a dir +that is accessible by sish. In the directory, sish will look for a combination of files that look like +`name.crt` and `name.key`. `name` can be arbitrary in either case, it just needs to be unique to the cert +and key pair to allow them to be loaded into sish. + +## Load balancing + +sish can load balance any type of forwarded connection, but this needs to be enabled when starting sish +using the `--http-load-balancer`, +`--tcp-load-balancer`, and `--alias-load-balancer` flags. Let's say you have a few edge nodes +(raspberry pis) that are running a service internally but you want to be able to balance load across these +devices from the outside world. By enabling load balancing in sish, this happens automatically when a +device with the same forwarded TCP port, alias, or HTTP subdomain connects to sish. Connections will then be +evenly distributed to whatever nodes are connected to sish that match the forwarded connection. + +## Whitelisting IPs Whitelisting IP ranges or countries is also possible. Whole CIDR ranges can be -specified with the `-sish.whitelistedips` option that accepts a comma-separated string like "192.30.252.0/22,185.199.108.0/22". If you want to whitelist a single +specified with the `--whitelisted-ips` option that accepts a comma-separated +string like "192.30.252.0/22,185.199.108.0/22". If you want to whitelist a single IP, use the `/32` range. -To whitelist countries, use `sish.whitelistedcountries` with a comma-separated +To whitelist countries, use `--whitelisted-countries` with a comma-separated string of countries in ISO format (for example, "pt" for Portugal). You'll also -need to set `-sish.usegeodb` to `true`. +need to set `--geodb` to `true`. -Demo - At this time, the demo instance has been set to require auth due to abuse ----- +## Demo - At this time, the demo instance has been set to require auth due to abuse -There is a demo service (and my private instance) currently running on `ssi.sh` that doesn't require any authentication. This service provides default logging (errors, connection IP/username, and pubkey fingerprint). I do not log any of the password authentication data or the data sent within the service/tunnels. My deploy uses the exact deploy steps that are listed above. This instance is for testing and educational purposes only. You can deploy this extremely easily on any host (Google Cloud Platform provides an always-free instance that this should run perfectly on). If the service begins to accrue a lot of traffic, I will enable authentication and then you can reach out to me to get your SSH key whitelisted (make sure it's on GitHub and you provide me with your GitHub username). +There is a demo service (and my private instance) currently running on `ssi.sh` that +doesn't require any authentication. This service provides default logging +(errors, connection IP/username, and pubkey fingerprint). I do not log any of the password +authentication data or the data sent within the service/tunnels. My deploy uses the exact +deploy steps that are listed above. This instance is for testing and educational purposes only. +You can deploy this extremely easily on any host (Google Cloud Platform provides an always-free +instance that this should run perfectly on). If the service begins to accrue a lot of traffic, +I will enable authentication and then you can reach out to me to get your SSH key whitelisted +(make sure it's on GitHub and you provide me with your GitHub username). -Notes ------ +## Notes -1. This is by no means production ready in any way. This was hacked together and solves a fairly specific use case. +1. This is by no means production ready in any way. This was hacked together and solves a fairly specific +use case. - You can help it get production ready by submitting PRs/reviewing code/writing tests/etc -2. This is a fairly simple implementation, I've intentionally cut corners in some places to make it easier to write. -3. If you have any questions or comments, feel free to reach out via email [me@antoniomika.me](mailto:me@antoniomika.me) or on [freenode IRC #sish](https://kiwiirc.com/client/chat.freenode.net:6697/#sish) +2. This is a fairly simple implementation, I've intentionally cut corners in some places to make it easier +to write. +3. If you have any questions or comments, feel free to reach out via email +[me@antoniomika.me](mailto:me@antoniomika.me) +or on [freenode IRC #sish](https://kiwiirc.com/client/chat.freenode.net:6697/#sish) + +## Upgrading to v1.0 -CLI Flags ---------- +There are numerous breaking changes in sish between pre-1.0 and post-1.0 versions. The largest changes are +found in the mapping of command flags and configuration params. Those have changed drastically, but it should be easy +to find the new counterpart. The other change is SSH keys that are supported for host key auth. sish +continues to support most modern keys, but by default if a host key is not found, it will create an OpenSSH +ED25519 key to use. Previous versions of sish would aes encrypt the pem block of this private key, but we +have since moved to using the native +[OpenSSH private key format](https://github.com/openssh/openssh-portable/blob/master/sshkey.c) to allow for +easy interop between OpenSSH tools. For this reason, you will either have to manually convert an AES +encrypted key or generate a new one. + +## CLI Flags ```text -sh-3.2# ./sish -h -Usage of ./sish: - -sish.addr string - The address to listen for SSH connections (default "localhost:2222") - -sish.adminenabled - Whether or not to enable the admin console - -sish.admintoken string - The token to use for admin access (default "S3Cr3tP4$$W0rD") - -sish.appendusertosubdomain - Whether or not to append the user to the subdomain - -sish.auth - Whether or not to require auth on the SSH service - -sish.bannedcountries string - A comma separated list of banned countries - -sish.bannedips string - A comma separated list of banned ips - -sish.bannedsubdomains string - A comma separated list of banned subdomains (default "localhost") - -sish.bindrandom - Bind ports randomly (OS chooses) (default true) - -sish.bindrange string - Ports that are allowed to be bound (default "0,1024-65535") - -sish.cleanupunbound - Whether or not to cleanup unbound (forwarded) SSH connections (default true) - -sish.debug - Whether or not to print debug information - -sish.domain string - The domain for HTTP(S) multiplexing (default "ssi.sh") - -sish.forcerandomsubdomain - Whether or not to force a random subdomain (default true) - -sish.http string - The address to listen for HTTP connections (default "localhost:80") - -sish.httpport int - The port to use for http command output - -sish.https string - The address to listen for HTTPS connections (default "localhost:443") - -sish.httpsenabled - Whether or not to listen for HTTPS connections - -sish.httpspems string - The location of pem files for HTTPS (fullchain.pem and privkey.pem) (default "ssl/") - -sish.httpsport int - The port to use for https command output - -sish.idletimeout int - Number of seconds to wait for activity before closing a connection (default 5) - -sish.connecttimeout int - Number of seconds the ssh login process is allowed before closing a connection (default 5) - -sish.keysdir string - Directory for public keys for pubkey auth (default "pubkeys/") - -sish.logtoclient - Whether or not to log http requests to the client - -sish.password string - Password to use for password auth (default "S3Cr3tP4$$W0rD") - -sish.pingclient - Whether or not ping the client. (default true) - -sish.pingclientinterval int - Interval in seconds to ping a client to ensure it is up. (default 10) - -sish.pkloc string - SSH server private key (default "keys/ssh_key") - -sish.pkpass string - Passphrase to use for the server private key (default "S3Cr3tP4$$phrAsE") - -sish.proxyprotoenabled - Whether or not to enable the use of the proxy protocol - -sish.proxyprotoversion string - What version of the proxy protocol to use. Can either be 1, 2, or userdefined. If userdefined, the user needs to add a command to SSH called proxyproto:version (ie proxyproto:1) (default "1") - -sish.redirectroot - Whether or not to redirect the root domain (default true) - -sish.redirectrootlocation string - Where to redirect the root domain to (default "https://github.com/antoniomika/sish") - -sish.serviceconsoleenabled - Whether or not to enable the admin console for each service and send the info to users - -sish.serviceconsoletoken string - The token to use for service access. Auto generated if empty. - -sish.subdomainlen int - The length of the random subdomain to generate (default 3) - -sish.tcpalias - Whether or not to allow the use of TCP aliasing - -sish.usegeodb - Whether or not to use the maxmind geodb - -sish.usersubdomainseparator - Separator to use when appending username to subdomain (default "-") - -sish.verifyorigin - Whether or not to verify origin on websocket connection (default true) - -sish.verifyssl - Whether or not to verify SSL on proxy connection (default true) - -sish.version - Print version and exit - -sish.whitelistedcountries string - A comma separated list of whitelisted countries - -sish.whitelistedips string - A comma separated list of whitelisted ips +sish is a command line utility that implements an SSH server that can handle HTTP(S)/WS(S)/TCP multiplexing, forwarding and load balancing. +It can handle multiple vhosting and reverse tunneling endpoints for a large number of clients. + +Usage: + sish [flags] + +Flags: + --admin-console Enable the admin console accessible at http(s)://domain/_sish/console?x-authorization=admin-console-token + -j, --admin-console-token string The token to use for admin console access if it's enabled (default "S3Cr3tP4$$W0rD") + --alias-load-balancer Enable the alias load balancer (multiple clients can bind the same alias) + --append-user-to-subdomain Append the SSH user to the subdomain. This is useful in multitenant environments + --append-user-to-subdomain-separator string The token to use for separating username and subdomain selection in a virtualhost (default "-") + --authentication Require authentication for the SSH service + -k, --authentication-keys-directory string Directory where public keys for public key authentication are stored. + sish will watch this directory and automatically load new keys and remove keys + from the authentication list (default "deploy/pubkeys/") + -u, --authentication-password string Password to use for ssh server password authentication (default "S3Cr3tP4$$W0rD") + -o, --banned-countries string A comma separated list of banned countries. Applies to HTTP, TCP, and SSH connections + -x, --banned-ips string A comma separated list of banned ips that are unable to access the service. Applies to HTTP, TCP, and SSH connections + -b, --banned-subdomains string A comma separated list of banned subdomains that users are unable to bind (default "localhost") + --bind-any-host Bind any host when accepting an HTTP listener + --bind-hosts string A comma separated list of other hosts a user can bind. Requested hosts should be subdomains of a host in this list + --bind-random-ports Force TCP tunnels to bind a random port, where the kernel will randomly assign it (default true) + --bind-random-subdomains Force bound HTTP tunnels to use random subdomains instead of user provided ones (default true) + --bind-random-subdomains-length int The length of the random subdomain to generate if a subdomain is unavailable or if random subdomains are enforced (default 3) + --cleanup-unbound Cleanup unbound (unforwarded) SSH connections after a set timeout (default true) + --cleanup-unbound-timeout duration Duration to wait before cleaning up an unbound (unforwarded) connection (default 5s) + -c, --config string Config file (default "config.yml") + --debug Enable debugging information + -d, --domain string The root domain for HTTP(S) multiplexing that will be appended to subdomains (default "ssi.sh") + --geodb Use a geodb to verify country IP address association for IP filtering + -h, --help help for sish + -i, --http-address string The address to listen for HTTP connections (default "localhost:80") + --http-load-balancer Enable the HTTP load balancer (multiple clients can bind the same domain) + --http-port-override int The port to use for http command output. This does not effect ports used for connecting, it's for cosmetic use only + --https Listen for HTTPS connections. Requires a correct --https-certificate-directory + -t, --https-address string The address to listen for HTTPS connections (default "localhost:443") + -s, --https-certificate-directory string The directory containing HTTPS certificate files (name.crt and name.key). There can be many crt/key pairs (default "deploy/ssl/") + --https-ondemand-certificate Enable retrieving certificates on demand via Let's Encrypt + --https-ondemand-certificate-accept-terms Accept the Let's Encrypt terms + --https-ondemand-certificate-email string The email to use with Let's Encrypt for cert notifications. Can be left blank + --https-port-override int The port to use for https command output. This does not effect ports used for connecting, it's for cosmetic use only + --idle-connection Enable connection idle timeouts for reads and writes (default true) + --idle-connection-timeout duration Duration to wait for activity before closing a connection for all reads and writes (default 5s) + --load-templates Load HTML templates. This is required for admin/service consoles (default true) + --load-templates-directory string The directory and glob parameter for templates that should be loaded (default "templates/*") + --localhost-as-all Enable forcing localhost to mean all interfaces for tcp listeners (default true) + --log-to-client Enable logging HTTP and TCP requests to the client + --log-to-file Enable writing log output to file, specified by log-to-file-path + --log-to-file-compress Enable compressing log output files + --log-to-file-max-age int The maxium number of days to store log output in a file (default 28) + --log-to-file-max-backups int The maxium number of rotated logs files to keep (default 3) + --log-to-file-max-size int The maximum size of outputed log files in megabytes (default 500) + --log-to-file-path string The file to write log output to (default "/tmp/sish.log") + --log-to-stdout Enable writing log output to stdout (default true) + --ping-client Send ping requests to the underlying SSH client. + This is useful to ensure that SSH connections are kept open or close cleanly (default true) + --ping-client-interval duration Duration representing an interval to ping a client to ensure it is up (default 5s) + --ping-client-timeout duration Duration to wait for activity before closing a connection after sending a ping to a client (default 5s) + -n, --port-bind-range string Ports or port ranges that sish will allow to be bound when a user attempts to use TCP forwarding (default "0,1024-65535") + -l, --private-key-location string The location of the SSH server private key. sish will create a private key here if + it doesn't exist using the --private-key-passphrase to encrypt it if supplied (default "deploy/keys/ssh_key") + -p, --private-key-passphrase string Passphrase to use to encrypt the server private key (default "S3Cr3tP4$$phrAsE") + --proxy-protocol Use the proxy-protocol while proxying connections in order to pass-on IP address and port information + -q, --proxy-protocol-version string What version of the proxy protocol to use. Can either be 1, 2, or userdefined. + If userdefined, the user needs to add a command to SSH called proxyproto:version (ie proxyproto:1) (default "1") + --redirect-root Redirect the root domain to the location defined in --redirect-root-location (default true) + -r, --redirect-root-location string The location to redirect requests to the root domain + to instead of responding with a 404 (default "https://github.com/antoniomika/sish") + --service-console Enable the service console for each service and send the info to connected clients + -m, --service-console-token string The token to use for service console access. Auto generated if empty for each connected tunnel + -a, --ssh-address string The address to listen for SSH connections (default "localhost:2222") + --tcp-aliases Enable the use of TCP aliasing + --tcp-load-balancer Enable the TCP load balancer (multiple clients can bind the same port) + --time-format string The time format to use for both HTTP and general log messages (default "2006/01/02 - 15:04:05") + --verify-dns Verify DNS information for hosts and ensure it matches a connecting users sha256 key fingerprint (default true) + --verify-ssl Verify SSL certificates made on proxied HTTP connections (default true) + -v, --version version for sish + -y, --whitelisted-countries string A comma separated list of whitelisted countries. Applies to HTTP, TCP, and SSH connections + -w, --whitelisted-ips string A comma separated list of whitelisted ips. Applies to HTTP, TCP, and SSH connections ``` diff --git a/cmd/sish.go b/cmd/sish.go new file mode 100644 index 0000000..b05e232 --- /dev/null +++ b/cmd/sish.go @@ -0,0 +1,193 @@ +// Package cmd implements the sish CLI command. +package cmd + +import ( + "fmt" + "io" + "log" + "os" + "time" + + "github.com/antoniomika/sish/sshmuxer" + "github.com/antoniomika/sish/utils" + "github.com/fsnotify/fsnotify" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "gopkg.in/natefinch/lumberjack.v2" +) + +var ( + // Version describes the version of the current build. + Version = "dev" + + // Commit describes the commit of the current build. + Commit = "none" + + // Date describes the date of the current build. + Date = "unknown" + + // configFile holds the location of the config file from CLI flags. + configFile string + + // rootCmd is the root cobra command. + rootCmd = &cobra.Command{ + Use: "sish", + Short: "The sish command initializes and runs the sish ssh multiplexer", + Long: "sish is a command line utility that implements an SSH server that can handle HTTP(S)/WS(S)/TCP multiplexing, forwarding and load balancing.\nIt can handle multiple vhosting and reverse tunneling endpoints for a large number of clients.", + Run: runCommand, + Version: Version, + } +) + +// init initializes flags used by the root command. +func init() { + cobra.OnInitialize(initConfig) + + rootCmd.SetVersionTemplate(fmt.Sprintf("Version: %v\nCommit: %v\nDate: %v\n", Version, Commit, Date)) + + rootCmd.PersistentFlags().StringVarP(&configFile, "config", "c", "config.yml", "Config file") + + rootCmd.PersistentFlags().StringP("ssh-address", "a", "localhost:2222", "The address to listen for SSH connections") + rootCmd.PersistentFlags().StringP("http-address", "i", "localhost:80", "The address to listen for HTTP connections") + rootCmd.PersistentFlags().StringP("https-address", "t", "localhost:443", "The address to listen for HTTPS connections") + rootCmd.PersistentFlags().StringP("redirect-root-location", "r", "https://github.com/antoniomika/sish", "The location to redirect requests to the root domain\nto instead of responding with a 404") + rootCmd.PersistentFlags().StringP("https-certificate-directory", "s", "deploy/ssl/", "The directory containing HTTPS certificate files (name.crt and name.key). There can be many crt/key pairs") + rootCmd.PersistentFlags().StringP("https-ondemand-certificate-email", "", "", "The email to use with Let's Encrypt for cert notifications. Can be left blank") + rootCmd.PersistentFlags().StringP("domain", "d", "ssi.sh", "The root domain for HTTP(S) multiplexing that will be appended to subdomains") + rootCmd.PersistentFlags().StringP("banned-subdomains", "b", "localhost", "A comma separated list of banned subdomains that users are unable to bind") + rootCmd.PersistentFlags().StringP("banned-ips", "x", "", "A comma separated list of banned ips that are unable to access the service. Applies to HTTP, TCP, and SSH connections") + rootCmd.PersistentFlags().StringP("banned-countries", "o", "", "A comma separated list of banned countries. Applies to HTTP, TCP, and SSH connections") + rootCmd.PersistentFlags().StringP("whitelisted-ips", "w", "", "A comma separated list of whitelisted ips. Applies to HTTP, TCP, and SSH connections") + rootCmd.PersistentFlags().StringP("whitelisted-countries", "y", "", "A comma separated list of whitelisted countries. Applies to HTTP, TCP, and SSH connections") + rootCmd.PersistentFlags().StringP("private-key-passphrase", "p", "S3Cr3tP4$$phrAsE", "Passphrase to use to encrypt the server private key") + rootCmd.PersistentFlags().StringP("private-key-location", "l", "deploy/keys/ssh_key", "The location of the SSH server private key. sish will create a private key here if\nit doesn't exist using the --private-key-passphrase to encrypt it if supplied") + rootCmd.PersistentFlags().StringP("authentication-password", "u", "S3Cr3tP4$$W0rD", "Password to use for ssh server password authentication") + rootCmd.PersistentFlags().StringP("authentication-keys-directory", "k", "deploy/pubkeys/", "Directory where public keys for public key authentication are stored.\nsish will watch this directory and automatically load new keys and remove keys\nfrom the authentication list") + rootCmd.PersistentFlags().StringP("port-bind-range", "n", "0,1024-65535", "Ports or port ranges that sish will allow to be bound when a user attempts to use TCP forwarding") + rootCmd.PersistentFlags().StringP("proxy-protocol-version", "q", "1", "What version of the proxy protocol to use. Can either be 1, 2, or userdefined.\nIf userdefined, the user needs to add a command to SSH called proxyproto:version (ie proxyproto:1)") + rootCmd.PersistentFlags().StringP("admin-console-token", "j", "S3Cr3tP4$$W0rD", "The token to use for admin console access if it's enabled") + rootCmd.PersistentFlags().StringP("service-console-token", "m", "", "The token to use for service console access. Auto generated if empty for each connected tunnel") + rootCmd.PersistentFlags().StringP("append-user-to-subdomain-separator", "", "-", "The token to use for separating username and subdomain selection in a virtualhost") + rootCmd.PersistentFlags().StringP("time-format", "", "2006/01/02 - 15:04:05", "The time format to use for both HTTP and general log messages") + rootCmd.PersistentFlags().StringP("log-to-file-path", "", "/tmp/sish.log", "The file to write log output to") + rootCmd.PersistentFlags().StringP("bind-hosts", "", "", "A comma separated list of other hosts a user can bind. Requested hosts should be subdomains of a host in this list") + rootCmd.PersistentFlags().StringP("load-templates-directory", "", "templates/*", "The directory and glob parameter for templates that should be loaded") + + rootCmd.PersistentFlags().BoolP("bind-random-subdomains", "", true, "Force bound HTTP tunnels to use random subdomains instead of user provided ones") + rootCmd.PersistentFlags().BoolP("verify-ssl", "", true, "Verify SSL certificates made on proxied HTTP connections") + rootCmd.PersistentFlags().BoolP("verify-dns", "", true, "Verify DNS information for hosts and ensure it matches a connecting users sha256 key fingerprint") + rootCmd.PersistentFlags().BoolP("cleanup-unbound", "", true, "Cleanup unbound (unforwarded) SSH connections after a set timeout") + rootCmd.PersistentFlags().BoolP("bind-random-ports", "", true, "Force TCP tunnels to bind a random port, where the kernel will randomly assign it") + rootCmd.PersistentFlags().BoolP("append-user-to-subdomain", "", false, "Append the SSH user to the subdomain. This is useful in multitenant environments") + rootCmd.PersistentFlags().BoolP("debug", "", false, "Enable debugging information") + rootCmd.PersistentFlags().BoolP("ping-client", "", true, "Send ping requests to the underlying SSH client.\nThis is useful to ensure that SSH connections are kept open or close cleanly") + rootCmd.PersistentFlags().BoolP("geodb", "", false, "Use a geodb to verify country IP address association for IP filtering") + rootCmd.PersistentFlags().BoolP("authentication", "", false, "Require authentication for the SSH service") + rootCmd.PersistentFlags().BoolP("proxy-protocol", "", false, "Use the proxy-protocol while proxying connections in order to pass-on IP address and port information") + rootCmd.PersistentFlags().BoolP("https", "", false, "Listen for HTTPS connections. Requires a correct --https-certificate-directory") + rootCmd.PersistentFlags().BoolP("redirect-root", "", true, "Redirect the root domain to the location defined in --redirect-root-location") + rootCmd.PersistentFlags().BoolP("admin-console", "", false, "Enable the admin console accessible at http(s)://domain/_sish/console?x-authorization=admin-console-token") + rootCmd.PersistentFlags().BoolP("service-console", "", false, "Enable the service console for each service and send the info to connected clients") + rootCmd.PersistentFlags().BoolP("tcp-aliases", "", false, "Enable the use of TCP aliasing") + rootCmd.PersistentFlags().BoolP("log-to-client", "", false, "Enable logging HTTP and TCP requests to the client") + rootCmd.PersistentFlags().BoolP("idle-connection", "", true, "Enable connection idle timeouts for reads and writes") + rootCmd.PersistentFlags().BoolP("http-load-balancer", "", false, "Enable the HTTP load balancer (multiple clients can bind the same domain)") + rootCmd.PersistentFlags().BoolP("tcp-load-balancer", "", false, "Enable the TCP load balancer (multiple clients can bind the same port)") + rootCmd.PersistentFlags().BoolP("alias-load-balancer", "", false, "Enable the alias load balancer (multiple clients can bind the same alias)") + rootCmd.PersistentFlags().BoolP("localhost-as-all", "", true, "Enable forcing localhost to mean all interfaces for tcp listeners") + rootCmd.PersistentFlags().BoolP("log-to-stdout", "", true, "Enable writing log output to stdout") + rootCmd.PersistentFlags().BoolP("log-to-file", "", false, "Enable writing log output to file, specified by log-to-file-path") + rootCmd.PersistentFlags().BoolP("log-to-file-compress", "", false, "Enable compressing log output files") + rootCmd.PersistentFlags().BoolP("https-ondemand-certificate", "", false, "Enable retrieving certificates on demand via Let's Encrypt") + rootCmd.PersistentFlags().BoolP("https-ondemand-certificate-accept-terms", "", false, "Accept the Let's Encrypt terms") + rootCmd.PersistentFlags().BoolP("bind-any-host", "", false, "Bind any host when accepting an HTTP listener") + rootCmd.PersistentFlags().BoolP("load-templates", "", true, "Load HTML templates. This is required for admin/service consoles") + + rootCmd.PersistentFlags().IntP("http-port-override", "", 0, "The port to use for http command output. This does not effect ports used for connecting, it's for cosmetic use only") + rootCmd.PersistentFlags().IntP("https-port-override", "", 0, "The port to use for https command output. This does not effect ports used for connecting, it's for cosmetic use only") + rootCmd.PersistentFlags().IntP("bind-random-subdomains-length", "", 3, "The length of the random subdomain to generate if a subdomain is unavailable or if random subdomains are enforced") + rootCmd.PersistentFlags().IntP("log-to-file-max-size", "", 500, "The maximum size of outputed log files in megabytes") + rootCmd.PersistentFlags().IntP("log-to-file-max-backups", "", 3, "The maxium number of rotated logs files to keep") + rootCmd.PersistentFlags().IntP("log-to-file-max-age", "", 28, "The maxium number of days to store log output in a file") + + rootCmd.PersistentFlags().DurationP("idle-connection-timeout", "", 5*time.Second, "Duration to wait for activity before closing a connection for all reads and writes") + rootCmd.PersistentFlags().DurationP("ping-client-interval", "", 5*time.Second, "Duration representing an interval to ping a client to ensure it is up") + rootCmd.PersistentFlags().DurationP("ping-client-timeout", "", 5*time.Second, "Duration to wait for activity before closing a connection after sending a ping to a client") + rootCmd.PersistentFlags().DurationP("cleanup-unbound-timeout", "", 5*time.Second, "Duration to wait before cleaning up an unbound (unforwarded) connection") +} + +// initConfig initializes the configuration and loads needed +// values. It initializes logging and other vars. +func initConfig() { + viper.SetConfigFile(configFile) + + err := viper.BindPFlags(rootCmd.PersistentFlags()) + if err != nil { + log.Println("Unable to bind pflags:", err) + } + + viper.AutomaticEnv() + + if err := viper.ReadInConfig(); err == nil { + log.Println("Using config file:", viper.ConfigFileUsed()) + } + + viper.WatchConfig() + + writers := []io.Writer{} + + if viper.GetBool("log-to-stdout") { + writers = append(writers, os.Stdout) + } + + if viper.GetBool("log-to-file") { + writers = append(writers, &lumberjack.Logger{ + Filename: viper.GetString("log-to-file-path"), + MaxSize: viper.GetInt("log-to-file-max-size"), + MaxBackups: viper.GetInt("log-to-file-max-backups"), + MaxAge: viper.GetInt("log-to-file-max-age"), + Compress: viper.GetBool("log-to-file-compress"), + }) + } + + multiWriter := io.MultiWriter(writers...) + + viper.OnConfigChange(func(e fsnotify.Event) { + log.Println("Reloaded configuration file.") + + log.SetFlags(0) + log.SetOutput(utils.LogWriter{ + TimeFmt: viper.GetString("time-format"), + MultiWriter: multiWriter, + }) + + if viper.GetBool("debug") { + logrus.SetLevel(logrus.DebugLevel) + } + }) + + log.SetFlags(0) + log.SetOutput(utils.LogWriter{ + TimeFmt: viper.GetString("time-format"), + MultiWriter: multiWriter, + }) + + if viper.GetBool("debug") { + logrus.SetLevel(logrus.DebugLevel) + } + + logrus.SetOutput(multiWriter) + + utils.Setup(multiWriter) +} + +// Execute executes the root command. +func Execute() error { + return rootCmd.Execute() +} + +// runCommand is used to start the root muxer. +func runCommand(cmd *cobra.Command, args []string) { + sshmuxer.Start() +} diff --git a/config.example.yml b/config.example.yml new file mode 100644 index 0000000..5250f03 --- /dev/null +++ b/config.example.yml @@ -0,0 +1,65 @@ +admin-console: false +admin-console-token: S3Cr3tP4$$W0rD +alias-load-balancer: false +append-user-to-subdomain: false +append-user-to-subdomain-separator: '-' +authentication: false +authentication-keys-directory: deploy/pubkeys/ +authentication-password: S3Cr3tP4$$W0rD +banned-countries: "" +banned-ips: "" +banned-subdomains: localhost +bind-any-host: false +bind-hosts: "" +bind-random-ports: true +bind-random-subdomains: true +bind-random-subdomains-length: 3 +cleanup-unbound: true +cleanup-unbound-timeout: 5s +config: config.yml +debug: false +domain: ssi.sh +geodb: false +http-address: localhost:80 +http-load-balancer: false +http-port-override: 0 +https: false +https-address: localhost:443 +https-certificate-directory: deploy/ssl/ +https-ondemand-certificate: false +https-ondemand-certificate-accept-terms: false +https-ondemand-certificate-email: "" +https-port-override: 0 +idle-connection: true +idle-connection-timeout: 5s +load-templates: true +load-templates-directory: templates/* +localhost-as-all: true +log-to-client: false +log-to-file: false +log-to-file-compress: false +log-to-file-max-age: 28 +log-to-file-max-backups: 3 +log-to-file-max-size: 500 +log-to-file-path: /tmp/sish.log +log-to-stdout: true +ping-client: true +ping-client-interval: 5s +ping-client-timeout: 5s +port-bind-range: 0,1024-65535 +private-key-location: deploy/keys/ssh_key +private-key-passphrase: S3Cr3tP4$$phrAsE +proxy-protocol: false +proxy-protocol-version: "1" +redirect-root: true +redirect-root-location: https://github.com/antoniomika/sish +service-console: false +service-console-token: "" +ssh-address: localhost:2222 +tcp-aliases: false +tcp-load-balancer: false +time-format: 2006/01/02 - 15:04:05 +verify-dns: true +verify-ssl: true +whitelisted-countries: "" +whitelisted-ips: "" diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 72bc14f..1d3c3f4 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -1,24 +1,13 @@ version: '3.7' services: - busybox: - image: busybox - volumes: - - ./letsencrypt:/etc/letsencrypt - command: /bin/sh -c "echo '$DOMAIN *.$DOMAIN autorestart-containers=sish' > /etc/letsencrypt/domains.conf" letsencrypt: - image: adferrand/letsencrypt-dns:latest + image: adferrand/dnsrobocert:latest container_name: letsencrypt-dns - depends_on: - - busybox - environment: - - VERSION=latest - - LETSENCRYPT_USER_MAIL - - LEXICON_PROVIDER - - LEXICON_PROVIDER_OPTIONS volumes: - /var/run/docker.sock:/var/run/docker.sock - ./letsencrypt:/etc/letsencrypt + - ./le-config.yml:/etc/dnsrobocert/config.yml restart: always sish: image: antoniomika/sish:latest @@ -29,16 +18,17 @@ services: - ./letsencrypt:/etc/letsencrypt - ./pubkeys:/pubkeys - ./keys:/keys + - ./ssl:/ssl command: | - -sish.addr=:22 - -sish.https=:443 - -sish.http=:80 - -sish.httpsenabled=true - -sish.httpspems=/etc/letsencrypt/live/$DOMAIN - -sish.keysdir=/pubkeys - -sish.pkloc=/keys/ssh_key - -sish.bindrandom=false - -sish.domain=$DOMAIN - -sish.forcerandomsubdomain=false + --ssh-address=:22 + --http-address=:80 + --https-address=:443 + --https=true + --https-certificate-directory=/ssl + --authentication-keys-directory=/pubkeys + --private-key-location=/keys/ssh_key + --bind-random-ports=false + --bind-random-subdomains=false + --domain=ssi.sh network_mode: host restart: always diff --git a/keys/.gitkeep b/deploy/keys/.gitkeep similarity index 100% rename from keys/.gitkeep rename to deploy/keys/.gitkeep diff --git a/deploy/le-config.yml b/deploy/le-config.yml new file mode 100644 index 0000000..a978e9c --- /dev/null +++ b/deploy/le-config.yml @@ -0,0 +1,17 @@ +acme: + email_account: AUTH_EMAIL +certificates: +- autorestart: + - containers: + - sish + domains: + - ssi.sh + - '*.ssi.sh' + name: ssi.sh + profile: cloudflare +profiles: +- name: cloudflare + provider: cloudflare + provider_options: + auth_token: AUTH_TOKEN + auth_username: AUTH_EMAIL \ No newline at end of file diff --git a/pubkeys/.gitkeep b/deploy/pubkeys/.gitkeep similarity index 100% rename from pubkeys/.gitkeep rename to deploy/pubkeys/.gitkeep diff --git a/ssl/.gitkeep b/deploy/ssl/.gitkeep similarity index 100% rename from ssl/.gitkeep rename to deploy/ssl/.gitkeep diff --git a/go.mod b/go.mod index 9853c84..d360c4c 100644 --- a/go.mod +++ b/go.mod @@ -1,23 +1,39 @@ module github.com/antoniomika/sish require ( - github.com/fsnotify/fsnotify v1.4.7 - github.com/gin-contrib/sse v0.1.0 // indirect - github.com/gin-gonic/gin v1.4.0 - github.com/golang/protobuf v1.3.2 // indirect - github.com/gorilla/websocket v1.4.1 - github.com/jpillora/ipfilter v1.0.0 - github.com/json-iterator/go v1.1.8 // indirect - github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c - github.com/logrusorgru/aurora v0.0.0-20191116043053-66b7ad493a23 - github.com/mattn/go-isatty v0.0.10 // indirect + github.com/ScaleFT/sshkeys v0.0.0-20200327173127-6142f742bca5 + github.com/antoniomika/oxy v1.1.1-0.20200517194743-bedd7c62c77e + github.com/caddyserver/certmagic v0.10.13 + github.com/cenkalti/backoff/v4 v4.0.2 // indirect + github.com/fsnotify/fsnotify v1.4.9 + github.com/gin-gonic/gin v1.6.3 + github.com/go-playground/validator/v10 v10.3.0 // indirect + github.com/golang/protobuf v1.4.2 // indirect + github.com/gorilla/websocket v1.4.2 + github.com/jpillora/ipfilter v1.2.1 + github.com/klauspost/cpuid v1.2.4 // indirect + github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 + github.com/mailgun/timetools v0.0.0-20170619190023-f3a7b8ffff47 // indirect + github.com/miekg/dns v1.1.29 // indirect github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a - github.com/oschwald/maxminddb-golang v1.5.0 // indirect - github.com/pires/go-proxyproto v0.0.0-20190615163442-2c19fd512994 - github.com/ugorji/go v1.1.7 // indirect - golang.org/x/crypto v0.0.0-20191108234033-bd318be0434a - golang.org/x/sys v0.0.0-20191105231009-c1f44814a5cd // indirect - gopkg.in/yaml.v2 v2.2.5 // indirect + github.com/mitchellh/mapstructure v1.3.1 // indirect + github.com/pelletier/go-toml v1.8.0 // indirect + github.com/phuslu/geoip v1.0.20200411 // indirect + github.com/pires/go-proxyproto v0.1.3 + github.com/sirupsen/logrus v1.6.0 + github.com/spf13/afero v1.2.2 // indirect + github.com/spf13/cast v1.3.1 // indirect + github.com/spf13/cobra v1.0.0 + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/viper v1.7.0 + golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 + golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2 // indirect + golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 // indirect + gopkg.in/ini.v1 v1.56.0 // indirect + gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.0.0 + gopkg.in/square/go-jose.v2 v2.5.1 // indirect ) -go 1.13 +go 1.14 diff --git a/go.sum b/go.sum index fb1a0e3..9f1c222 100644 --- a/go.sum +++ b/go.sum @@ -1,81 +1,703 @@ -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +contrib.go.opencensus.io/exporter/ocagent v0.4.12/go.mod h1:450APlNTSR6FrvC3CTRqYosuDstRB9un7SOx2k/9ckA= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Azure/azure-sdk-for-go v32.4.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/go-autorest/autorest v0.1.0/go.mod h1:AKyIcETwSUFxIcs/Wnq/C+kwCtlEYGUVd7FPNb2slmg= +github.com/Azure/go-autorest/autorest v0.5.0/go.mod h1:9HLKlQjVBH6U3oDfsXOeVc56THsLPw1L03yban4xThw= +github.com/Azure/go-autorest/autorest/adal v0.1.0/go.mod h1:MeS4XhScH55IST095THyTxElntu7WqB7pNbZo8Q5G3E= +github.com/Azure/go-autorest/autorest/adal v0.2.0/go.mod h1:MeS4XhScH55IST095THyTxElntu7WqB7pNbZo8Q5G3E= +github.com/Azure/go-autorest/autorest/azure/auth v0.1.0/go.mod h1:Gf7/i2FUpyb/sGBLIFxTBzrNzBo7aPXXE3ZVeDRwdpM= +github.com/Azure/go-autorest/autorest/azure/cli v0.1.0/go.mod h1:Dk8CUAt/b/PzkfeRsWzVG9Yj3ps8mS8ECztu43rdU8U= +github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= +github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/to v0.2.0/go.mod h1:GunWKJp1AEqgMaGLV+iocmRAJWqST1wQYhyyjXJ3SJc= +github.com/Azure/go-autorest/autorest/validation v0.1.0/go.mod h1:Ha3z/SqBeaalWQvokg3NZAlQTalVMtOIAs1aGK7G6u8= +github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= +github.com/Azure/go-autorest/tracing v0.1.0/go.mod h1:ROEEAFwXycQw7Sn3DXNtEedEvdeRAgDr0izn4z5Ij88= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87/go.mod h1:iGLljf5n9GjT6kc0HBvyI1nOKnGQbNB66VzSNbK5iks= +github.com/ScaleFT/sshkeys v0.0.0-20200327173127-6142f742bca5 h1:VauE2GcJNZFun2Och6tIT2zJZK1v6jxALQDA9BIji/E= +github.com/ScaleFT/sshkeys v0.0.0-20200327173127-6142f742bca5/go.mod h1:gxOHeajFfvGQh/fxlC8oOKBe23xnnJTif00IFFbiT+o= +github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= +github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/akamai/AkamaiOPEN-edgegrid-golang v0.9.8/go.mod h1:aVvklgKsPENRkl29bNwrHISa1F+YLGTHArMxZMBqWM8= +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/aliyun/alibaba-cloud-sdk-go v1.61.112/go.mod h1:pUKYbK5JQ+1Dfxk80P0qxGqe5dkxDoabbZS7zOcouyA= +github.com/antoniomika/oxy v1.1.1-0.20200517194743-bedd7c62c77e h1:II47DJPovACM4IndMelUd39qcOMDkZjvxJM+Ed5gojk= +github.com/antoniomika/oxy v1.1.1-0.20200517194743-bedd7c62c77e/go.mod h1:3+QhU5rjR3nA+fbjvrafIeU9Vix+j9MtLvnBZjF/N00= +github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aws/aws-sdk-go v1.30.20/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= +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/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/caddyserver/certmagic v0.10.13 h1:wfyYpXVXSSYMS1ZFpSr7HptwsC+j7elda5PUERrHtRc= +github.com/caddyserver/certmagic v0.10.13/go.mod h1:Yz6cSRUdddGy6Ut5JfrvcqBwrm1BqXxJRqJq2TwjPnA= +github.com/cenkalti/backoff/v4 v4.0.0 h1:6VeaLF9aI+MAUQ95106HwWzYZgJJpZ4stumjj6RFYAU= +github.com/cenkalti/backoff/v4 v4.0.0/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8Bog/87DQnVg= +github.com/cenkalti/backoff/v4 v4.0.2 h1:JIufpQLbh4DkbQoii76ItQIUFzevQSqOLZca4eamEDs= +github.com/cenkalti/backoff/v4 v4.0.2/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8Bog/87DQnVg= +github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/cloudflare-go v0.10.2/go.mod h1:qhVI5MKwBGhdNU89ZRz2plgYutcJ5PCekLxXn56w6SY= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd h1:qMd81Ts1T2OTKmB4acZcyKaMtRnY5Y44NuXGX2GFJ1w= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= +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/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.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/cpu/goacmedns v0.0.2/go.mod h1:4MipLkI+qScwqtVxcNO6okBhbgRrr7/tKXUSgSL0teQ= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +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/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a h1:saTgr5tMLFnmy/yg3qDTft4rE5DY2uJ/cCxCe3q0XTU= +github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a/go.mod h1:Bw9BbhOJVNR+t0jCqx2GC6zv0TGBsShs56Y3gfSCvl0= +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/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= +github.com/dnaeon/go-vcr v0.0.0-20180814043457-aafff18a5cc2/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= +github.com/dnsimple/dnsimple-go v0.60.0/go.mod h1:O5TJ0/U6r7AfT8niYNlmohpLbCSG+c71tQlGr9SeGrg= +github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/exoscale/egoscale v0.18.1/go.mod h1:Z7OOdzzTOz1Q1PjQXumlz9Wn/CddH0zSYdCF3rnBKXE= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.4.0 h1:3tMoCCfM7ppqsR0ptz/wi1impNpT7/9wQtMZ8lr1mCQ= -github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= +github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/go-acme/lego/v3 v3.7.0 h1:qC5/8/CbltyAE8fGLE6bGlqucj7pXc/vBxiLwLOsmAQ= +github.com/go-acme/lego/v3 v3.7.0/go.mod h1:4eDjjYkAsDXyNcwN8IhhZAwxz9Ltiks1Zmpv0q20J7A= +github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +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-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/go-playground/validator/v10 v10.3.0 h1:nZU+7q+yJoFmwvNgv/LnPUkwPal62+b2xXj0AU1Es7o= +github.com/go-playground/validator/v10 v10.3.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= +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/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +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/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +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/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= -github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/jpillora/ipfilter v1.0.0 h1:iOdQcx4MEesxmU9IjOZ/yPlXAHpvX/NgG9QUNr8TOpc= -github.com/jpillora/ipfilter v1.0.0/go.mod h1:wuysWh3ibOyGQXDBthc4PMZKTKa/8s4RbaOtkTKOOhs= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gophercloud/gophercloud v0.3.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gravitational/trace v0.0.0-20190726142706-a535a178675f/go.mod h1:RvdOUHE4SHqR3oXlFFKnGzms8a5dugHygGw1bqDstYI= +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.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +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/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df/go.mod h1:QMZY7/J/KSQEhKWFeDesPjMj+wCHReeknARU3wqlyN4= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jpillora/ipfilter v1.2.1 h1:K5zGPjyjgf2MPB+iTULZ7Hl4zXPWOb4JwgxMdogKq20= +github.com/jpillora/ipfilter v1.2.1/go.mod h1:B1/L0Go28JK2UD/qUkmoMNN8akI+XK26b2ZB7PaV1I8= +github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok= -github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c h1:N7A4JCA2G+j5fuFxCsJqjFU/sZe0mj8H0sSoSwbaikw= -github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c/go.mod h1:Nn5wlyECw3iJrzi0AhIWg+AJUb4PlRQVW4/3XHH1LZA= -github.com/logrusorgru/aurora v0.0.0-20191116043053-66b7ad493a23 h1:Wp7NjqGKGN9te9N/rvXYRhlVcrulGdxnz8zadXWs7fc= -github.com/logrusorgru/aurora v0.0.0-20191116043053-66b7ad493a23/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= -github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10= -github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +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/klauspost/cpuid v1.2.3 h1:CCtW0xUnWGVINKvE/WWOYKdsPV6mawAtvQuSl8guwQs= +github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/cpuid v1.2.4 h1:EBfaK0SWSwk+fgk6efYFWdzl8MwRWoOO1gkmiaTXPW4= +github.com/klauspost/cpuid v1.2.4/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/kolo/xmlrpc v0.0.0-20190717152603-07c4ee3fd181/go.mod h1:o03bZfuBwAXHetKXuInt4S7omeXUu62/A845kiycsSQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/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/labbsr0x/bindman-dns-webhook v1.0.2/go.mod h1:p6b+VCXIR8NYKpDr8/dg1HKfQoRHCdcsROXKvmoehKA= +github.com/labbsr0x/goh v1.0.1/go.mod h1:8K2UhVoaWXcCU7Lxoa2omWnC8gyW8px7/lmO61c027w= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/linode/linodego v0.10.0/go.mod h1:cziNP7pbvE3mXIPneHj0oRY8L1WtGEIKlZ8LANE4eXA= +github.com/liquidweb/liquidweb-go v1.6.0/go.mod h1:UDcVnAMDkZxpw4Y7NOHkqoeiGacVLEIG/i5J9cyixzQ= +github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 h1:bqDmpDG49ZRnB5PcgP0RXtQvnMSgIF14M7CBd2shtXs= +github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailgun/minheap v0.0.0-20170619185613-3dbe6c6bf55f/go.mod h1:V3EvCedtJTvUYzJF2GZMRB0JMlai+6cBu3VCTQz33GQ= +github.com/mailgun/multibuf v0.0.0-20150714184110-565402cd71fb/go.mod h1:E0vRBBIQUHcRtmL/oR6w/jehh4FJqJFxe86gBnw9gXc= +github.com/mailgun/timetools v0.0.0-20141028012446-7e6055773c51/go.mod h1:RYmqHbhWwIz3z9eVmQ2rx82rulEMG0t+Q1bzfc9DYN4= +github.com/mailgun/timetools v0.0.0-20170619190023-f3a7b8ffff47 h1:jlyJPTyctWqANbaxi/nXRrxX4WeeAGMPaHPj9XlO0Rw= +github.com/mailgun/timetools v0.0.0-20170619190023-f3a7b8ffff47/go.mod h1:RYmqHbhWwIz3z9eVmQ2rx82rulEMG0t+Q1bzfc9DYN4= +github.com/mailgun/ttlmap v0.0.0-20170619185759-c1c17f74874f/go.mod h1:8heskWJ5c0v5J9WH89ADhyal1DOZcayll8fSbhB+/9A= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-tty v0.0.0-20180219170247-931426f7535a/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/miekg/dns v1.1.27 h1:aEH/kqUzUxGJ/UHcEKdJY+ugH6WEzsEBBSPa8zuy1aM= +github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= +github.com/miekg/dns v1.1.29 h1:xHBEhR+t5RzcFJjBLJlax2daXOrTYtr9z4WdKEfWFzg= +github.com/miekg/dns v1.1.29/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a h1:eU8j/ClY2Ty3qdHnn0TyW3ivFoPC/0F1gQZz8yTxbbE= github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a/go.mod h1:v8eSC2SMp9/7FTKUncp7fH9IwPfw+ysMObcEz5FWheQ= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-vnc v0.0.0-20150629162542-723ed9867aed/go.mod h1:3rdaFaCv4AyBgu5ALFM0+tSuHrBh6v692nyQe3ikrq0= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.3.1 h1:cCBH2gTD2K0OtLlv/Y5H01VQCqmlDxz30kS5Y5bqfLA= +github.com/mitchellh/mapstructure v1.3.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/oschwald/maxminddb-golang v1.4.0/go.mod h1:3jhIUymTJ5VREKyIhWm66LJiQt04F0UCDdodShpjWsY= -github.com/oschwald/maxminddb-golang v1.5.0 h1:rmyoIV6z2/s9TCJedUuDiKht2RN12LWJ1L7iRGtWY64= -github.com/oschwald/maxminddb-golang v1.5.0/go.mod h1:3jhIUymTJ5VREKyIhWm66LJiQt04F0UCDdodShpjWsY= -github.com/pires/go-proxyproto v0.0.0-20190615163442-2c19fd512994 h1:3ssKn22MN6oLH+l2iimsBdCliSgELXTBWWR+yooB2lQ= -github.com/pires/go-proxyproto v0.0.0-20190615163442-2c19fd512994/go.mod h1:6/gX3+E/IYGa0wMORlSMla999awQFdbaeQCHjSMKIzY= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY4wsPvgQH/jsuJi4XO2ssZbdsIizr4CVC8= +github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= +github.com/nrdcg/auroradns v1.0.1/go.mod h1:y4pc0i9QXYlFCWrhWrUSIETnZgrf4KuwjDIWmmXo3JI= +github.com/nrdcg/dnspod-go v0.4.0/go.mod h1:vZSoFSFeQVm2gWLMkyX61LZ8HI3BaqtHZWgPTGKr6KQ= +github.com/nrdcg/goinwx v0.6.1/go.mod h1:XPiut7enlbEdntAqalBIqcYcTEVhpv/dKWgDCX2SwKQ= +github.com/nrdcg/namesilo v0.2.1/go.mod h1:lwMvfQTyYq+BbjJd30ylEG4GPSS6PII0Tia4rRpRiyw= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= +github.com/oracle/oci-go-sdk v7.0.0+incompatible/go.mod h1:VQb79nF8Z2cwLkLS35ukwStZIg5F66tcBccjip/j888= +github.com/ovh/go-ovh v0.0.0-20181109152953-ba5adb4cf014/go.mod h1:joRatxRJaZBsY3JAOEMcoOp05CnZzsx4scTxi95DHyQ= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.8.0 h1:Keo9qb7iRJs2voHvunFtuuYFsbWeOBh8/P9v/kVMFtw= +github.com/pelletier/go-toml v1.8.0/go.mod h1:D6yutnOGMveHEPV7VQOuvI/gXY61bv+9bAOTRnLElKs= +github.com/phuslu/geoip v1.0.20200217/go.mod h1:2z3izHYc+bwW7j5pyvtXG8N+I9Q87XnZfULO6lN9NhA= +github.com/phuslu/geoip v1.0.20200411 h1:AfadsXTn7dr/oRLusL7GMK7j+QxvfNYkqRRgHVH3m5U= +github.com/phuslu/geoip v1.0.20200411/go.mod h1:2z3izHYc+bwW7j5pyvtXG8N+I9Q87XnZfULO6lN9NhA= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pires/go-proxyproto v0.1.3 h1:2XEuhsQluSNA5QIQkiUv8PfgZ51sNYIQkq/yFquiSQM= +github.com/pires/go-proxyproto v0.1.3/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2/go.mod h1:7tZKcyumwBO6qip7RNQ5r77yrssm9bfCowcLEBcU5IA= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +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/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sacloud/libsacloud v1.26.1/go.mod h1:79ZwATmHLIFZIMd7sxA3LwzVy/B77uj3LDoToVTxDoQ= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/skratchdot/open-golang v0.0.0-20160302144031-75fb7ed4208c/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +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/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= +github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +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/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/spf13/viper v1.7.0 h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM= +github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +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/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/timewasted/linode v0.0.0-20160829202747-37e84520dcf7/go.mod h1:imsgLplxEC/etjIhdr3dNzV3JeT27LbVu5pYWm0JCBY= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc= github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4= +github.com/transip/gotransip/v6 v6.0.2/go.mod h1:pQZ36hWWRahCUXkFWlx9Hs711gLd8J4qdgLdRzmtY+g= +github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/vulcand/oxy v1.1.0 h1:DbBijGo1+6cFqR9jarkMxasdj0lgWwrrFtue6ijek4Q= +github.com/vulcand/oxy v1.1.0/go.mod h1:ADiMYHi8gkGl2987yQIzDRoXZilANF4WtKaQ92OppKY= +github.com/vulcand/predicate v1.1.0/go.mod h1:mlccC5IRBoc2cIFmCB8ZM62I3VDb6p2GXESMHa3CnZg= +github.com/vultr/govultr v0.1.4/go.mod h1:9H008Uxr/C4vFNGLqKx232C206GL0PBHzOP0809bGNA= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +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.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/ratelimit v0.0.0-20180316092928-c15da0234277/go.mod h1:2X8KaoNd1J0lZV+PxJk/5+DGbO/tpwLR1m++a7FnB/Y= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180621125126-a49355c7e3f8/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/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-20191108234033-bd318be0434a h1:R/qVym5WAxsZWQqZCwDY/8sdVKV1m1WgU4/S5IRQAzc= -golang.org/x/crypto v0.0.0-20191108234033-bd318be0434a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 h1:cg5LA/zNPRzIXIWSCxQW10Rvpy94aQh3LT/ShoCpkHw= +golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/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-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180611182652-db08ff08e862/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/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-20181201002055-351d144fa1fc/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-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/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-20190503192946-f4e77d36d62c h1:uOCk1iQW6Vc18bnC13MfzScl+wdKBmM9Y9kU7Z83/lw= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190930134127-c5a3c61f89f3/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2 h1:eDrdRpKgkcCqKZQwyZRyeFZgfqt37SL7Kv3tok06cKE= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +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-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180622082034-63fc586f45fe/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +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-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/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-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190907184412-d223b2b6db03/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191105231009-c1f44814a5cd h1:3x5uuvBgE6oaXJjCOvpCC1IpgJogqQ+PqGGU3ZxAgII= -golang.org/x/sys v0.0.0-20191105231009-c1f44814a5cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200219091948-cb0a6d8edb6c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 h1:DYfZAGf2WMFjMxbgTjaC+2HC7NkNAQs+6Q8b9WEB/F4= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190921001708-c4c64cad1fd0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/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-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/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-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +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/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= -gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= -gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ= -gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +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/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE= +gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.56.0 h1:DPMeDvGTM54DXbPkVIZsp19fp/I2K7zwA/itHYHKo8Y= +gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw= +gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= +gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/ns1/ns1-go.v2 v2.0.0-20190730140822-b51389932cbc/go.mod h1:VV+3haRsgDiVLxyifmMBrBIuCWFBPYKbRssXB9z67Hw= +gopkg.in/resty.v1 v1.9.1/go.mod h1:vo52Hzryw9PnPHcJfPsBiFW62XhNx5OczbV9y+IMpgc= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/square/go-jose.v2 v2.3.1 h1:SK5KegNXmKmqE342YYN2qPHEnUYeoMiXXl1poUlI+o4= +gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w= +gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +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.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +launchpad.net/gocheck v0.0.0-20140225173054-000000000087/go.mod h1:hj7XX3B/0A+80Vse0e+BUHsHMTEhd0O4cpUHr/e/BUM= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/http.go b/http.go deleted file mode 100644 index e446ff3..0000000 --- a/http.go +++ /dev/null @@ -1,252 +0,0 @@ -package main - -import ( - "bytes" - "compress/gzip" - "crypto/tls" - "encoding/base64" - "encoding/json" - "fmt" - "io/ioutil" - "log" - "net" - "net/http" - "net/http/httputil" - "path/filepath" - "strings" - "time" - - "github.com/gorilla/websocket" - "github.com/koding/websocketproxy" - - "github.com/gin-gonic/gin" -) - -// ProxyHolder holds proxy and connection info -type ProxyHolder struct { - ProxyHost string - ProxyTo string - Scheme string - SSHConn *SSHConnection -} - -func startHTTPHandler(state *State) { - releaseMode := gin.ReleaseMode - if *debug { - releaseMode = gin.DebugMode - } - gin.SetMode(releaseMode) - - gin.ForceConsoleColor() - - r := gin.New() - r.LoadHTMLGlob("templates/*") - r.Use(func(c *gin.Context) { - c.Set("startTime", time.Now()) - clientIPAddr, _, err := net.SplitHostPort(c.Request.RemoteAddr) - if state.IPFilter.Blocked(c.ClientIP()) || state.IPFilter.Blocked(clientIPAddr) || err != nil { - c.AbortWithStatus(http.StatusForbidden) - return - } - c.Next() - }, gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string { - var statusColor, methodColor, resetColor string - if param.IsOutputColor() { - statusColor = param.StatusCodeColor() - methodColor = param.MethodColor() - resetColor = param.ResetColor() - } - - if param.Latency > time.Minute { - // Truncate in a golang < 1.8 safe way - param.Latency = param.Latency - param.Latency%time.Second - } - - if *adminToken != "" && strings.Contains(param.Path, *adminToken) { - param.Path = strings.Replace(param.Path, *adminToken, "[REDACTED]", 1) - } - - if *serviceConsoleToken != "" && strings.Contains(param.Path, *serviceConsoleToken) { - param.Path = strings.Replace(param.Path, *serviceConsoleToken, "[REDACTED]", 1) - } - - logLine := fmt.Sprintf("%v | %s |%s %3d %s| %13v | %15s |%s %-7s %s %s\n%s", - param.TimeStamp.Format("2006/01/02 - 15:04:05"), - param.Request.Host, - statusColor, param.StatusCode, resetColor, - param.Latency, - param.ClientIP, - methodColor, param.Method, resetColor, - param.Path, - param.ErrorMessage, - ) - - if *logToClient { - hostname := strings.Split(param.Request.Host, ":")[0] - loc, ok := state.HTTPListeners.Load(hostname) - if ok { - proxyHolder := loc.(*ProxyHolder) - sendMessage(proxyHolder.SSHConn, strings.TrimSpace(logLine), true) - } - } - - return logLine - }), gin.Recovery(), func(c *gin.Context) { - hostname := strings.Split(c.Request.Host, ":")[0] - hostIsRoot := hostname == *rootDomain - - if (*adminEnabled || *serviceConsoleEnabled) && strings.HasPrefix(c.Request.URL.Path, "/_sish/") { - state.Console.HandleRequest(hostname, hostIsRoot, c) - return - } - - if hostIsRoot && *redirectRoot { - c.Redirect(http.StatusFound, *redirectRootLocation) - return - } - - loc, ok := state.HTTPListeners.Load(hostname) - if !ok { - err := c.AbortWithError(http.StatusNotFound, fmt.Errorf("cannot find connection for host: %s", hostname)) - if err != nil { - log.Println("Aborting with error", err) - } - return - } - - reqBody, err := ioutil.ReadAll(c.Request.Body) - if err != nil { - log.Println("Error reading request body:", err) - return - } - - c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(reqBody)) - - requestedScheme := "http" - - if c.Request.TLS != nil { - requestedScheme = "https" - } - - c.Request.Header.Set("X-Forwarded-Proto", requestedScheme) - - proxyHolder := loc.(*ProxyHolder) - - url := *c.Request.URL - url.Host = "local" - url.Path = "" - url.RawQuery = "" - url.Fragment = "" - url.Scheme = proxyHolder.Scheme - - dialer := func(network, addr string) (net.Conn, error) { - return net.Dial("unix", proxyHolder.ProxyTo) - } - - tlsConfig := &tls.Config{ - InsecureSkipVerify: !*verifySSL, - } - - if c.IsWebsocket() { - scheme := "ws" - if url.Scheme == "https" { - scheme = "wss" - } - - var checkOrigin func(r *http.Request) bool - if !*verifyOrigin { - checkOrigin = func(r *http.Request) bool { - return true - } - } - - url.Scheme = scheme - wsProxy := websocketproxy.NewProxy(&url) - wsProxy.Upgrader = &websocket.Upgrader{ - ReadBufferSize: 1024, - WriteBufferSize: 1024, - CheckOrigin: checkOrigin, - } - wsProxy.Dialer = &websocket.Dialer{ - NetDial: dialer, - TLSClientConfig: tlsConfig, - } - gin.WrapH(wsProxy)(c) - return - } - - proxy := httputil.NewSingleHostReverseProxy(&url) - proxy.Transport = &http.Transport{ - Dial: dialer, - TLSClientConfig: tlsConfig, - } - - if *adminEnabled || *serviceConsoleEnabled { - proxy.ModifyResponse = func(response *http.Response) error { - resBody, err := ioutil.ReadAll(response.Body) - if err != nil { - log.Println("error reading response for webconsole:", err) - } - - response.Body = ioutil.NopCloser(bytes.NewBuffer(resBody)) - - startTime := c.GetTime("startTime") - currentTime := time.Now() - diffTime := currentTime.Sub(startTime) - - roundTime := 10 * time.Microsecond - if diffTime > time.Second { - roundTime = 10 * time.Millisecond - } - - if response.Header.Get("Content-Encoding") == "gzip" { - gzData := bytes.NewBuffer(resBody) - gzReader, err := gzip.NewReader(gzData) - if err != nil { - log.Println("error reading gzip data:", err) - } - - resBody, err = ioutil.ReadAll(gzReader) - if err != nil { - log.Println("error reading gzip data:", err) - } - } - - requestHeaders := c.Request.Header.Clone() - requestHeaders.Add("Host", hostname) - - data, err := json.Marshal(map[string]interface{}{ - "startTime": startTime, - "currentTime": currentTime, - "requestIP": c.ClientIP(), - "requestTime": diffTime.Round(roundTime).String(), - "requestMethod": c.Request.Method, - "requestUrl": c.Request.URL, - "requestHeaders": requestHeaders, - "requestBody": base64.StdEncoding.EncodeToString(reqBody), - "responseHeaders": response.Header, - "responseCode": response.StatusCode, - "responseStatus": response.Status, - "responseBody": base64.StdEncoding.EncodeToString(resBody), - }) - - if err != nil { - log.Println("error marshaling json for webconsole:", err) - } - - state.Console.BroadcastRoute(hostname, data) - - return nil - } - } - - gin.WrapH(proxy)(c) - }) - - if *httpsEnabled { - go func() { - log.Fatal(r.RunTLS(*httpsAddr, filepath.Join(*httpsPems, "fullchain.pem"), filepath.Join(*httpsPems, "privkey.pem"))) - }() - } - log.Fatal(r.Run(*httpAddr)) -} diff --git a/httpmuxer/httpmuxer.go b/httpmuxer/httpmuxer.go new file mode 100644 index 0000000..26f8f2c --- /dev/null +++ b/httpmuxer/httpmuxer.go @@ -0,0 +1,237 @@ +// Package httpmuxer handles all of the HTTP connections made +// to sish. This implements the http multiplexing necessary for +// sish's core feature. +package httpmuxer + +import ( + "bytes" + "fmt" + "io/ioutil" + "log" + "net" + "net/http" + "path/filepath" + "strings" + "time" + + "github.com/antoniomika/oxy/forward" + "github.com/antoniomika/sish/utils" + "github.com/caddyserver/certmagic" + "github.com/pires/go-proxyproto" + "github.com/spf13/viper" + + "github.com/gin-gonic/gin" +) + +// Start initializes the HTTP service. +func Start(state *utils.State) { + releaseMode := gin.ReleaseMode + if viper.GetBool("debug") { + releaseMode = gin.DebugMode + } + gin.SetMode(releaseMode) + gin.DefaultWriter = state.LogWriter + gin.ForceConsoleColor() + + r := gin.New() + + if viper.GetBool("load-templates") { + r.LoadHTMLGlob(viper.GetString("load-templates-directory")) + } + + r.Use(func(c *gin.Context) { + // startTime is used for calculating latencies. + c.Set("startTime", time.Now()) + + // Here is where we check whether or not an IP is blocked. + clientIPAddr, _, err := net.SplitHostPort(c.Request.RemoteAddr) + if state.IPFilter.Blocked(c.ClientIP()) || state.IPFilter.Blocked(clientIPAddr) || err != nil { + c.AbortWithStatus(http.StatusForbidden) + return + } + c.Next() + }, gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string { + // Here is the logger we use to format each incoming request. + var statusColor, methodColor, resetColor string + if param.IsOutputColor() { + statusColor = param.StatusCodeColor() + methodColor = param.MethodColor() + resetColor = param.ResetColor() + } + + if param.Latency > time.Minute { + // Truncate in a golang < 1.8 safe way + param.Latency = param.Latency - param.Latency%time.Second + } + + if viper.GetString("admin-console-token") != "" && strings.Contains(param.Path, viper.GetString("admin-console-token")) { + param.Path = strings.Replace(param.Path, viper.GetString("admin-console-token"), "[REDACTED]", 1) + } + + if viper.GetString("service-console-token") != "" && strings.Contains(param.Path, viper.GetString("service-console-token")) { + param.Path = strings.Replace(param.Path, viper.GetString("service-console-token"), "[REDACTED]", 1) + } + + logLine := fmt.Sprintf("%v | %s |%s %3d %s| %13v | %15s |%s %-7s %s %s\n%s", + param.TimeStamp.Format(viper.GetString("time-format")), + param.Request.Host, + statusColor, param.StatusCode, resetColor, + param.Latency, + param.ClientIP, + methodColor, param.Method, resetColor, + param.Path, + param.ErrorMessage, + ) + + if viper.GetBool("log-to-client") { + hostname := strings.Split(param.Request.Host, ":")[0] + loc, ok := state.HTTPListeners.Load(hostname) + if ok { + proxyHolder := loc.(*utils.HTTPHolder) + sshConnTmp, ok := proxyHolder.SSHConnections.Load(param.Keys["proxySocket"]) + if ok { + sshConn := sshConnTmp.(*utils.SSHConnection) + sshConn.SendMessage(strings.TrimSpace(logLine), true) + } else { + proxyHolder.SSHConnections.Range(func(key, val interface{}) bool { + sshConn := val.(*utils.SSHConnection) + sshConn.SendMessage(strings.TrimSpace(logLine), true) + return true + }) + } + } + } + + return logLine + }), gin.Recovery(), func(c *gin.Context) { + hostname := strings.Split(c.Request.Host, ":")[0] + hostIsRoot := hostname == viper.GetString("domain") + + // Return a 404 for the favicon. + if hostIsRoot && strings.HasPrefix(c.Request.URL.Path, "/favicon.ico") { + c.AbortWithStatus(http.StatusNotFound) + return + } + + if (viper.GetBool("admin-console") || viper.GetBool("service-console")) && strings.HasPrefix(c.Request.URL.Path, "/_sish/") { + state.Console.HandleRequest(hostname, hostIsRoot, c) + return + } + + if hostIsRoot && viper.GetBool("redirect-root") { + c.Redirect(http.StatusFound, viper.GetString("redirect-root-location")) + return + } + + loc, ok := state.HTTPListeners.Load(hostname) + if !ok { + err := c.AbortWithError(http.StatusNotFound, fmt.Errorf("cannot find connection for host: %s", hostname)) + if err != nil { + log.Println("Aborting with error", err) + } + return + } + + reqBody, err := ioutil.ReadAll(c.Request.Body) + if err != nil { + log.Println("Error reading request body:", err) + return + } + + c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(reqBody)) + + proxyHolder := loc.(*utils.HTTPHolder) + + err = forward.ResponseModifier(ResponseModifier(state, hostname, reqBody, c))(proxyHolder.Forward) + if err != nil { + log.Println("Unable to set response modifier:", err) + } + + gin.WrapH(proxyHolder.Balancer)(c) + }) + + // If HTTPS is enabled, setup certmagic to allow us to provision HTTPS certs on the fly. + // You can use sish without a wildcard cert, but you really should. If you get a lot of clients + // with many random subdomains, you'll burn through your Let's Encrypt quota. Be careful! + if viper.GetBool("https") { + certManager := certmagic.NewDefault() + + acmeManager := certmagic.NewACMEManager(certManager, certmagic.DefaultACME) + + acmeManager.Agreed = viper.GetBool("https-ondemand-certificate-accept-terms") + acmeManager.Email = viper.GetString("https-ondemand-certificate-email") + + certManager.Issuer = acmeManager + + certManager.Storage = &certmagic.FileStorage{ + Path: filepath.Join(viper.GetString("https-certificate-directory"), "certmagic"), + } + + certManager.OnDemand = &certmagic.OnDemandConfig{ + DecisionFunc: func(name string) error { + if !viper.GetBool("https-ondemand-certificate") { + return fmt.Errorf("ondemand certificate retrieval is not enabled") + } + + _, ok := state.HTTPListeners.Load(name) + if !ok { + return fmt.Errorf("cannot find connection for host: %s", name) + } + + log.Println("Requesting certificate for host:", name) + return nil + }, + } + + certFiles, err := filepath.Glob(filepath.Join(viper.GetString("https-certificate-directory"), "*.crt")) + if err != nil { + log.Println("Error loading unmanaged certificates:", err) + } + + for _, v := range certFiles { + err := certManager.CacheUnmanagedCertificatePEMFile(v, fmt.Sprintf("%s.key", strings.TrimSuffix(v, ".crt")), []string{}) + if err != nil { + log.Println("Error loading unmanaged certificate:", err) + } + } + + httpsServer := &http.Server{ + Addr: viper.GetString("https-address"), + TLSConfig: certManager.TLSConfig(), + Handler: r, + } + + go func() { + l, err := net.Listen("tcp", httpsServer.Addr) + if err != nil { + log.Fatalf("couldn't listen to %q: %q\n", httpsServer.Addr, err.Error()) + } + + httpsListener := &proxyproto.Listener{ + Listener: l, + } + + defer httpsListener.Close() + + log.Fatal(httpsServer.ServeTLS(httpsListener, "", "")) + }() + } + + httpServer := &http.Server{ + Addr: viper.GetString("http-address"), + Handler: r, + } + + l, err := net.Listen("tcp", httpServer.Addr) + if err != nil { + log.Fatalf("couldn't listen to %q: %q\n", httpServer.Addr, err.Error()) + } + + httpsListener := &proxyproto.Listener{ + Listener: l, + } + + defer httpsListener.Close() + + log.Fatal(httpServer.Serve(httpsListener)) +} diff --git a/httpmuxer/proxy.go b/httpmuxer/proxy.go new file mode 100644 index 0000000..2c81172 --- /dev/null +++ b/httpmuxer/proxy.go @@ -0,0 +1,115 @@ +package httpmuxer + +import ( + "bytes" + "compress/gzip" + "crypto/tls" + "encoding/base64" + "encoding/json" + "io/ioutil" + "log" + "net" + "net/http" + "strings" + "time" + + "github.com/antoniomika/sish/utils" + "github.com/gin-gonic/gin" + "github.com/spf13/viper" +) + +// RoundTripper returns the specific handler for unix connections. This +// will allow us to use our created sockets cleanly. +func RoundTripper() *http.Transport { + dialer := func(network, addr string) (net.Conn, error) { + realAddr, err := base64.StdEncoding.DecodeString(strings.Split(addr, ":")[0]) + if err != nil { + log.Println("Unable to parse socket:", err) + } + + return net.Dial("unix", string(realAddr)) + } + + tlsConfig := &tls.Config{ + InsecureSkipVerify: !viper.GetBool("verify-ssl"), + } + + return &http.Transport{ + Dial: dialer, + TLSClientConfig: tlsConfig, + } +} + +// ResponseModifier implements a response modifier for the specified request. +// We don't actually modify any requests, but we do want to record the request +// so we can send it to the web console. +func ResponseModifier(state *utils.State, hostname string, reqBody []byte, c *gin.Context) func(*http.Response) error { + return func(response *http.Response) error { + if viper.GetBool("admin-console") || viper.GetBool("service-console") { + resBody, err := ioutil.ReadAll(response.Body) + if err != nil { + log.Println("Error reading response for webconsole:", err) + } + + response.Body = ioutil.NopCloser(bytes.NewBuffer(resBody)) + + startTime := c.GetTime("startTime") + currentTime := time.Now() + diffTime := currentTime.Sub(startTime) + + roundTime := 10 * time.Microsecond + if diffTime > time.Second { + roundTime = 10 * time.Millisecond + } + + if response.Header.Get("Content-Encoding") == "gzip" { + gzData := bytes.NewBuffer(resBody) + gzReader, err := gzip.NewReader(gzData) + if err != nil { + log.Println("Error reading gzip data:", err) + } + + resBody, err = ioutil.ReadAll(gzReader) + if err != nil { + log.Println("Error reading gzip data:", err) + } + } + + requestHeaders := c.Request.Header.Clone() + requestHeaders.Add("Host", hostname) + + data, err := json.Marshal(map[string]interface{}{ + "startTime": startTime, + "startTimePretty": startTime.Format(viper.GetString("time-format")), + "currentTime": currentTime, + "requestIP": c.ClientIP(), + "requestTime": diffTime.Round(roundTime).String(), + "requestMethod": c.Request.Method, + "requestUrl": c.Request.URL, + "requestHeaders": requestHeaders, + "requestBody": base64.StdEncoding.EncodeToString(reqBody), + "responseHeaders": response.Header, + "responseCode": response.StatusCode, + "responseStatus": response.Status, + "responseBody": base64.StdEncoding.EncodeToString(resBody), + }) + + if err != nil { + log.Println("Error marshaling json for webconsole:", err) + } + + if response.Request != nil { + hostLocation, err := base64.StdEncoding.DecodeString(response.Request.URL.Host) + if err != nil { + log.Println("Error loading proxy info from request", err) + } + + c.Set("proxySocket", string(hostLocation)) + } + + state.Console.BroadcastRoute(hostname, data) + } + + return nil + } +} diff --git a/main.go b/main.go index e873e80..84121b0 100644 --- a/main.go +++ b/main.go @@ -1,365 +1,16 @@ +// Package main represents the main entrypoint of the sish application. package main import ( - "flag" "log" - "net" - "os" - "os/signal" - "runtime" - "strconv" - "strings" - "sync" - "time" - "github.com/jpillora/ipfilter" - - "golang.org/x/crypto/ssh" -) - -// SSHConnection handles state for a SSHConnection -type SSHConnection struct { - SSHConn *ssh.ServerConn - Listeners *sync.Map - Close chan bool - Messages chan string - ProxyProto byte - Session chan bool - CleanupHandler bool -} - -// State handles overall state -type State struct { - Console *WebConsole - SSHConnections *sync.Map - Listeners *sync.Map - HTTPListeners *sync.Map - TCPListeners *sync.Map - IPFilter *ipfilter.IPFilter -} - -var ( - version = "dev" - commit = "none" - date = "unknown" - httpPort int - httpsPort int - serverAddr = flag.String("sish.addr", "localhost:2222", "The address to listen for SSH connections") - httpAddr = flag.String("sish.http", "localhost:80", "The address to listen for HTTP connections") - httpPortOverride = flag.Int("sish.httpport", 0, "The port to use for http command output") - httpsAddr = flag.String("sish.https", "localhost:443", "The address to listen for HTTPS connections") - httpsPortOverride = flag.Int("sish.httpsport", 0, "The port to use for https command output") - verifyOrigin = flag.Bool("sish.verifyorigin", true, "Whether or not to verify origin on websocket connection") - verifySSL = flag.Bool("sish.verifyssl", true, "Whether or not to verify SSL on proxy connection") - httpsEnabled = flag.Bool("sish.httpsenabled", false, "Whether or not to listen for HTTPS connections") - redirectRoot = flag.Bool("sish.redirectroot", true, "Whether or not to redirect the root domain") - redirectRootLocation = flag.String("sish.redirectrootlocation", "https://github.com/antoniomika/sish", "Where to redirect the root domain to") - httpsPems = flag.String("sish.httpspems", "ssl/", "The location of pem files for HTTPS (fullchain.pem and privkey.pem)") - rootDomain = flag.String("sish.domain", "ssi.sh", "The domain for HTTP(S) multiplexing") - domainLen = flag.Int("sish.subdomainlen", 3, "The length of the random subdomain to generate") - forceRandomSubdomain = flag.Bool("sish.forcerandomsubdomain", true, "Whether or not to force a random subdomain") - bannedSubdomains = flag.String("sish.bannedsubdomains", "localhost", "A comma separated list of banned subdomains") - bannedIPs = flag.String("sish.bannedips", "", "A comma separated list of banned ips") - bannedCountries = flag.String("sish.bannedcountries", "", "A comma separated list of banned countries") - whitelistedIPs = flag.String("sish.whitelistedips", "", "A comma separated list of whitelisted ips") - whitelistedCountries = flag.String("sish.whitelistedcountries", "", "A comma separated list of whitelisted countries") - useGeoDB = flag.Bool("sish.usegeodb", false, "Whether or not to use the maxmind geodb") - pkPass = flag.String("sish.pkpass", "S3Cr3tP4$$phrAsE", "Passphrase to use for the server private key") - pkLoc = flag.String("sish.pkloc", "keys/ssh_key", "SSH server private key") - authEnabled = flag.Bool("sish.auth", false, "Whether or not to require auth on the SSH service") - authPassword = flag.String("sish.password", "S3Cr3tP4$$W0rD", "Password to use for password auth") - authKeysDir = flag.String("sish.keysdir", "pubkeys/", "Directory for public keys for pubkey auth") - bindRange = flag.String("sish.bindrange", "0,1024-65535", "Ports that are allowed to be bound") - cleanupUnbound = flag.Bool("sish.cleanupunbound", true, "Whether or not to cleanup unbound (forwarded) SSH connections") - bindRandom = flag.Bool("sish.bindrandom", true, "Bind ports randomly (OS chooses)") - proxyProtoEnabled = flag.Bool("sish.proxyprotoenabled", false, "Whether or not to enable the use of the proxy protocol") - proxyProtoVersion = flag.String("sish.proxyprotoversion", "1", "What version of the proxy protocol to use. Can either be 1, 2, or userdefined. If userdefined, the user needs to add a command to SSH called proxyproto:version (ie proxyproto:1)") - debug = flag.Bool("sish.debug", false, "Whether or not to print debug information") - versionCheck = flag.Bool("sish.version", false, "Print version and exit") - tcpAlias = flag.Bool("sish.tcpalias", false, "Whether or not to allow the use of TCP aliasing") - logToClient = flag.Bool("sish.logtoclient", false, "Whether or not to log http requests to the client") - idleTimeout = flag.Int("sish.idletimeout", 5, "Number of seconds to wait for activity before closing a connection") - connectTimeout = flag.Int("sish.connecttimeout", 5, "Number of seconds the ssh login process is allowed before closing a connection") - appendUserToSubdomain = flag.Bool("sish.appendusertosubdomain", false, "Whether or not to append the user to the subdomain") - userSubdomainSeparator = flag.String("sish.usersubdomainseparator", "-", "Separator to use when appending username to subdomain") - adminEnabled = flag.Bool("sish.adminenabled", false, "Whether or not to enable the admin console") - adminToken = flag.String("sish.admintoken", "S3Cr3tP4$$W0rD", "The token to use for admin access") - serviceConsoleEnabled = flag.Bool("sish.serviceconsoleenabled", false, "Whether or not to enable the admin console for each service and send the info to users") - serviceConsoleToken = flag.String("sish.serviceconsoletoken", "", "The token to use for service access. Auto generated if empty.") - pingClient = flag.Bool("sish.pingclient", true, "Whether or not ping the client.") - pingClientInterval = flag.Int("sish.pingclientinterval", 10, "Interval in seconds to ping a client to ensure it is up.") - bannedSubdomainList = []string{""} - filter *ipfilter.IPFilter + "github.com/antoniomika/sish/cmd" ) +// main will start the sish command lifecycle and spawn the sish services. func main() { - flag.Parse() - - _, httpPortString, err := net.SplitHostPort(*httpAddr) - if err != nil { - log.Fatalln("Error parsing address:", err) - } - - _, httpsPortString, err := net.SplitHostPort(*httpsAddr) - if err != nil { - log.Fatalln("Error parsing address:", err) - } - - httpPort, err = strconv.Atoi(httpPortString) - if err != nil { - log.Fatalln("Error parsing address:", err) - } - - httpsPort, err = strconv.Atoi(httpsPortString) - if err != nil { - log.Fatalln("Error parsing address:", err) - } - - if *httpPortOverride != 0 { - httpPort = *httpPortOverride - } - - if *httpsPortOverride != 0 { - httpsPort = *httpsPortOverride - } - - if *versionCheck { - log.Printf("\nVersion: %v\nCommit: %v\nDate: %v\n", version, commit, date) - os.Exit(0) - } - - commaSplitFields := func(c rune) bool { - return c == ',' - } - - bannedSubdomainList = append(bannedSubdomainList, strings.FieldsFunc(*bannedSubdomains, commaSplitFields)...) - for k, v := range bannedSubdomainList { - bannedSubdomainList[k] = strings.ToLower(strings.TrimSpace(v) + "." + *rootDomain) - } - - upperList := func(stringList string) []string { - list := strings.FieldsFunc(stringList, commaSplitFields) - for k, v := range list { - list[k] = strings.ToUpper(v) - } - - return list - } - - whitelistedCountriesList := upperList(*whitelistedCountries) - whitelistedIPList := strings.FieldsFunc(*whitelistedIPs, commaSplitFields) - - ipfilterOpts := ipfilter.Options{ - BlockedCountries: upperList(*bannedCountries), - AllowedCountries: whitelistedCountriesList, - BlockedIPs: strings.FieldsFunc(*bannedIPs, commaSplitFields), - AllowedIPs: whitelistedIPList, - BlockByDefault: len(whitelistedIPList) > 0 || len(whitelistedCountriesList) > 0, - } - - if *useGeoDB { - filter = ipfilter.NewLazy(ipfilterOpts) - } else { - filter = ipfilter.NewNoDB(ipfilterOpts) - } - - watchCerts() - - state := &State{ - SSHConnections: &sync.Map{}, - Listeners: &sync.Map{}, - HTTPListeners: &sync.Map{}, - TCPListeners: &sync.Map{}, - IPFilter: filter, - Console: NewWebConsole(), - } - - state.Console.State = state - - go startHTTPHandler(state) - - if *debug { - go func() { - for { - log.Println("=======Start=========") - log.Println("===Goroutines=====") - log.Println(runtime.NumGoroutine()) - log.Println("===Listeners======") - state.Listeners.Range(func(key, value interface{}) bool { - log.Println(key, value) - return true - }) - log.Println("===Clients========") - state.SSHConnections.Range(func(key, value interface{}) bool { - log.Println(key, value) - return true - }) - log.Println("===HTTP Clients===") - state.HTTPListeners.Range(func(key, value interface{}) bool { - log.Println(key, value) - return true - }) - log.Println("===TCP Aliases====") - state.TCPListeners.Range(func(key, value interface{}) bool { - log.Println(key, value) - return true - }) - log.Println("===Web Console Routes====") - state.Console.Clients.Range(func(key, value interface{}) bool { - log.Println(key, value) - return true - }) - log.Println("===Web Console Tokens====") - state.Console.RouteTokens.Range(func(key, value interface{}) bool { - log.Println(key, value) - return true - }) - log.Print("========End==========\n\n") - - time.Sleep(2 * time.Second) - } - }() - } - - log.Println("Starting SSH service on address:", *serverAddr) - - sshConfig := getSSHConfig() - - listener, err := net.Listen("tcp", *serverAddr) + err := cmd.Execute() if err != nil { - log.Fatal(err) + log.Println("Unable to execute root command:", err) } - - state.Listeners.Store(listener.Addr(), listener) - - defer func() { - listener.Close() - state.Listeners.Delete(listener.Addr()) - }() - - c := make(chan os.Signal, 1) - signal.Notify(c, os.Interrupt) - go func() { - for range c { - os.Exit(0) - } - }() - - for { - conn, err := listener.Accept() - if err != nil { - log.Println(err) - continue - } - - clientRemote, _, err := net.SplitHostPort(conn.RemoteAddr().String()) - - if err != nil || filter.Blocked(clientRemote) { - conn.Close() - continue - } - - clientLoggedIn := false - - if *cleanupUnbound { - go func() { - <-time.After(time.Duration(*connectTimeout) * time.Second) - if !clientLoggedIn { - conn.Close() - } - }() - } - - log.Println("Accepted SSH connection for:", conn.RemoteAddr()) - - go func() { - sshConn, chans, reqs, err := ssh.NewServerConn(conn, sshConfig) - clientLoggedIn = true - if err != nil { - conn.Close() - log.Println(err) - return - } - - holderConn := &SSHConnection{ - SSHConn: sshConn, - Listeners: &sync.Map{}, - Close: make(chan bool), - Messages: make(chan string), - Session: make(chan bool), - } - - state.SSHConnections.Store(sshConn.RemoteAddr(), holderConn) - - go func() { - err := sshConn.Wait() - if err != nil && *debug { - log.Println("Closing SSH connection:", err) - } - - select { - case <-holderConn.Close: - break - default: - holderConn.CleanUp(state) - } - }() - - go handleRequests(reqs, holderConn, state) - go handleChannels(chans, holderConn, state) - - if *cleanupUnbound { - go func() { - select { - case <-time.After(1 * time.Second): - count := 0 - holderConn.Listeners.Range(func(key, value interface{}) bool { - count++ - return true - }) - - if count == 0 { - sendMessage(holderConn, "No forwarding requests sent. Closing connection.", true) - time.Sleep(1 * time.Millisecond) - holderConn.CleanUp(state) - } - case <-holderConn.Close: - return - } - }() - } - - if *pingClient { - go func() { - tickDuration := time.Duration(*pingClientInterval) * time.Second - ticker := time.Tick(tickDuration) - for { - err := conn.SetDeadline(time.Now().Add(tickDuration).Add(time.Duration(*idleTimeout) * time.Second)) - if err != nil { - log.Println("Unable to set deadline") - } - - select { - case <-ticker: - _, _, err := sshConn.SendRequest("keepalive@sish", true, nil) - if err != nil { - log.Println("Error retrieving keepalive response") - return - } - case <-holderConn.Close: - return - } - } - }() - } - }() - } -} - -// CleanUp closes all allocated resources and cleans them up -func (s *SSHConnection) CleanUp(state *State) { - close(s.Close) - s.SSHConn.Close() - state.SSHConnections.Delete(s.SSHConn.RemoteAddr()) - log.Println("Closed SSH connection for:", s.SSHConn.RemoteAddr(), "user:", s.SSHConn.User()) } diff --git a/requests.go b/requests.go deleted file mode 100644 index ae389b2..0000000 --- a/requests.go +++ /dev/null @@ -1,323 +0,0 @@ -package main - -import ( - "fmt" - "io" - "io/ioutil" - "log" - "net" - "os" - "strconv" - "time" - - "github.com/logrusorgru/aurora" - "github.com/pires/go-proxyproto" - "golang.org/x/crypto/ssh" -) - -type channelForwardMsg struct { - Addr string - Rport uint32 -} - -type forwardedTCPPayload struct { - Addr string - Port uint32 - OriginAddr string - OriginPort uint32 -} - -func handleRemoteForward(newRequest *ssh.Request, sshConn *SSHConnection, state *State) { - check := &channelForwardMsg{} - - err := ssh.Unmarshal(newRequest.Payload, check) - if err != nil { - log.Println("Error unmarshaling remote forward payload:", err) - } - - bindPort := check.Rport - - handleTCPAliasing := false - if bindPort != uint32(80) && bindPort != uint32(443) { - if *tcpAlias && check.Addr != "localhost" { - handleTCPAliasing = true - } else { - checkedPort, err := checkPort(check.Rport, *bindRange) - if err != nil && !*bindRandom { - err = newRequest.Reply(false, nil) - if err != nil { - log.Println("Error replying to socket request:", err) - } - return - } - - bindPort = checkedPort - if *bindRandom { - bindPort = 0 - - if *bindRange != "" { - bindPort = getRandomPortInRange(*bindRange) - } - } - } - } - - stringPort := strconv.FormatUint(uint64(bindPort), 10) - listenAddr := ":" + stringPort - listenType := "tcp" - - tmpfile, err := ioutil.TempFile("", sshConn.SSHConn.RemoteAddr().String()+":"+stringPort) - if err != nil { - err = newRequest.Reply(false, nil) - if err != nil { - log.Println("Error replying to socket request:", err) - } - return - } - os.Remove(tmpfile.Name()) - - if stringPort == "80" || stringPort == "443" || handleTCPAliasing { - listenType = "unix" - listenAddr = tmpfile.Name() - } - - chanListener, err := net.Listen(listenType, listenAddr) - if err != nil { - err = newRequest.Reply(false, nil) - if err != nil { - log.Println("Error replying to socket request:", err) - } - return - } - - state.Listeners.Store(chanListener.Addr(), chanListener) - sshConn.Listeners.Store(chanListener.Addr(), chanListener) - - cleanupChanListener := func() { - chanListener.Close() - state.Listeners.Delete(chanListener.Addr()) - sshConn.Listeners.Delete(chanListener.Addr()) - os.Remove(tmpfile.Name()) - } - - defer cleanupChanListener() - - go func() { - <-sshConn.Close - cleanupChanListener() - }() - - connType := "tcp" - if stringPort == "80" { - connType = "http" - } else if stringPort == "443" { - connType = "https" - } - - requestMessages := fmt.Sprintf("Starting SSH Forwarding service for %s. Forwarded connections can be accessed via the following methods:\r\n", aurora.Sprintf(aurora.Green("%s:%s"), connType, stringPort)) - - if stringPort == "80" || stringPort == "443" { - scheme := "http" - if stringPort == "443" { - scheme = "https" - } - - host := getOpenHost(check.Addr, state, sshConn) - - pH := &ProxyHolder{ - ProxyHost: host, - ProxyTo: chanListener.Addr().String(), - Scheme: scheme, - SSHConn: sshConn, - } - - state.HTTPListeners.Store(host, pH) - defer state.HTTPListeners.Delete(host) - - if *adminEnabled || *serviceConsoleEnabled { - routeToken := *serviceConsoleToken - sendToken := false - if routeToken == "" { - sendToken = true - routeToken = RandStringBytesMaskImprSrc(20) - } - - state.Console.AddRoute(host, routeToken) - defer state.Console.RemoveRoute(host) - - if *serviceConsoleEnabled && sendToken { - scheme := "http" - portString := "" - if httpPort != 80 { - portString = fmt.Sprintf(":%d", httpPort) - } - - if *httpsEnabled { - scheme = "https" - if httpsPort != 443 { - portString = fmt.Sprintf(":%d", httpsPort) - } - } - - consoleURL := fmt.Sprintf("%s://%s%s", scheme, host, portString) - - requestMessages += fmt.Sprintf("Service console can be accessed here: %s/_sish/console?x-authorization=%s\r\n", consoleURL, routeToken) - } - } - - httpPortString := "" - if httpPort != 80 { - httpPortString = fmt.Sprintf(":%d", httpPort) - } - - requestMessages += fmt.Sprintf("%s: http://%s%s\r\n", aurora.BgBlue("HTTP"), host, httpPortString) - log.Printf("%s forwarding started: http://%s%s -> %s for client: %s\n", aurora.BgBlue("HTTP"), host, httpPortString, chanListener.Addr().String(), sshConn.SSHConn.RemoteAddr().String()) - - if *httpsEnabled { - httpsPortString := "" - if httpsPort != 443 { - httpsPortString = fmt.Sprintf(":%d", httpsPort) - } - - requestMessages += fmt.Sprintf("%s: https://%s%s\r\n", aurora.BgBlue("HTTPS"), host, httpsPortString) - log.Printf("%s forwarding started: https://%s%s -> %s for client: %s\n", aurora.BgBlue("HTTPS"), host, httpPortString, chanListener.Addr().String(), sshConn.SSHConn.RemoteAddr().String()) - } - } else { - if handleTCPAliasing { - validAlias := getOpenAlias(check.Addr, stringPort, state, sshConn) - - state.TCPListeners.Store(validAlias, chanListener.Addr().String()) - defer state.TCPListeners.Delete(validAlias) - - requestMessages += fmt.Sprintf("%s: %s\r\n", aurora.BgBlue("TCP Alias"), validAlias) - log.Printf("%s forwarding started: %s -> %s for client: %s\n", aurora.BgBlue("TCP Alias"), validAlias, chanListener.Addr().String(), sshConn.SSHConn.RemoteAddr().String()) - } else { - requestMessages += fmt.Sprintf("%s: %s:%d\r\n", aurora.BgBlue("TCP"), *rootDomain, chanListener.Addr().(*net.TCPAddr).Port) - log.Printf("%s forwarding started: %s:%d -> %s for client: %s\n", aurora.BgBlue("TCP"), *rootDomain, chanListener.Addr().(*net.TCPAddr).Port, chanListener.Addr().String(), sshConn.SSHConn.RemoteAddr().String()) - } - } - - sendMessage(sshConn, requestMessages, false) - - for { - cl, err := chanListener.Accept() - if err != nil { - break - } - - defer cl.Close() - - if connType == "tcp" { - logLine := fmt.Sprintf("Accepted connection from %s -> %s", cl.RemoteAddr().String(), sshConn.SSHConn.RemoteAddr().String()) - log.Println(logLine) - - if *logToClient { - sendMessage(sshConn, logLine, true) - } - } - - resp := &forwardedTCPPayload{ - Addr: check.Addr, - Port: check.Rport, - OriginAddr: check.Addr, - OriginPort: check.Rport, - } - - newChan, newReqs, err := sshConn.SSHConn.OpenChannel("forwarded-tcpip", ssh.Marshal(resp)) - if err != nil { - sendMessage(sshConn, err.Error(), true) - cl.Close() - continue - } - - defer newChan.Close() - - if sshConn.ProxyProto != 0 && (listenType != "unix" || handleTCPAliasing) { - var sourceInfo *net.TCPAddr - var destInfo *net.TCPAddr - if _, ok := cl.RemoteAddr().(*net.TCPAddr); !ok { - sourceInfo = sshConn.SSHConn.RemoteAddr().(*net.TCPAddr) - destInfo = sshConn.SSHConn.LocalAddr().(*net.TCPAddr) - } else { - sourceInfo = cl.RemoteAddr().(*net.TCPAddr) - destInfo = cl.LocalAddr().(*net.TCPAddr) - } - - proxyProtoHeader := proxyproto.Header{ - Version: sshConn.ProxyProto, - Command: proxyproto.ProtocolVersionAndCommand(proxyproto.PROXY), - TransportProtocol: proxyproto.AddressFamilyAndProtocol(proxyproto.TCPv4), - SourceAddress: sourceInfo.IP, - DestinationAddress: destInfo.IP, - SourcePort: uint16(sourceInfo.Port), - DestinationPort: uint16(destInfo.Port), - } - - _, err := proxyProtoHeader.WriteTo(newChan) - if err != nil && *debug { - log.Println("Error writing to channel:", err) - } - } - - go copyBoth(cl, newChan) - go ssh.DiscardRequests(newReqs) - } -} - -// IdleTimeoutConn handles the connection with a context deadline -// code adapted from https://qiita.com/kwi/items/b38d6273624ad3f6ae79 -type IdleTimeoutConn struct { - Conn net.Conn -} - -// Read is needed to implement the reader part -func (i IdleTimeoutConn) Read(buf []byte) (int, error) { - err := i.Conn.SetReadDeadline(time.Now().Add(time.Duration(*idleTimeout) * time.Second)) - if err != nil { - return 0, err - } - - return i.Conn.Read(buf) -} - -// Write is needed to implement the writer part -func (i IdleTimeoutConn) Write(buf []byte) (int, error) { - err := i.Conn.SetWriteDeadline(time.Now().Add(time.Duration(*idleTimeout) * time.Second)) - if err != nil { - return 0, err - } - - return i.Conn.Write(buf) -} - -func copyBoth(writer net.Conn, reader ssh.Channel) { - closeBoth := func() { - reader.Close() - writer.Close() - } - - tcon := IdleTimeoutConn{ - Conn: writer, - } - - copyToReader := func() { - _, err := io.Copy(reader, tcon) - if err != nil && *debug { - log.Println("Error copying to reader:", err) - } - - closeBoth() - } - - copyToWriter := func() { - _, err := io.Copy(tcon, reader) - if err != nil && *debug { - log.Println("Error copying to writer:", err) - } - - closeBoth() - } - - go copyToReader() - copyToWriter() -} diff --git a/sshmuxer/aliashandler.go b/sshmuxer/aliashandler.go new file mode 100644 index 0000000..8014f01 --- /dev/null +++ b/sshmuxer/aliashandler.go @@ -0,0 +1,52 @@ +package sshmuxer + +import ( + "encoding/base64" + "fmt" + "log" + "net/url" + "sync" + + "github.com/antoniomika/oxy/roundrobin" + "github.com/antoniomika/sish/utils" + "github.com/logrusorgru/aurora" +) + +// handleAliasListener handles the creation of the aliasHandler +// (or addition for load balancing) and set's up the underlying listeners. +func handleAliasListener(check *channelForwardMsg, stringPort string, requestMessages string, listenerHolder *utils.ListenerHolder, state *utils.State, sshConn *utils.SSHConnection) (*utils.AliasHolder, *url.URL, string, string, error) { + validAlias, aH := utils.GetOpenAlias(check.Addr, stringPort, state, sshConn) + + if aH == nil { + lb, err := roundrobin.New(nil) + + if err != nil { + log.Println("Error initializing alias balancer:", err) + return nil, nil, "", "", err + } + + aH = &utils.AliasHolder{ + AliasHost: validAlias, + SSHConnections: &sync.Map{}, + Balancer: lb, + } + + state.AliasListeners.Store(validAlias, aH) + } + + aH.SSHConnections.Store(listenerHolder.Addr().String(), sshConn) + + serverURL := &url.URL{ + Host: base64.StdEncoding.EncodeToString([]byte(listenerHolder.Addr().String())), + } + + err := aH.Balancer.UpsertServer(serverURL) + if err != nil { + log.Println("Unable to add server to balancer") + } + + requestMessages += fmt.Sprintf("%s: %s\r\n", aurora.BgBlue("TCP Alias"), validAlias) + log.Printf("%s forwarding started: %s -> %s for client: %s\n", aurora.BgBlue("TCP Alias"), validAlias, listenerHolder.Addr().String(), sshConn.SSHConn.RemoteAddr().String()) + + return aH, serverURL, validAlias, requestMessages, nil +} diff --git a/channels.go b/sshmuxer/channels.go similarity index 53% rename from channels.go rename to sshmuxer/channels.go index c1df504..9814a40 100644 --- a/channels.go +++ b/sshmuxer/channels.go @@ -1,26 +1,33 @@ -package main +package sshmuxer import ( + "encoding/base64" "fmt" "io" "log" "net" "strings" + "github.com/antoniomika/sish/utils" "github.com/logrusorgru/aurora" + "github.com/spf13/viper" "golang.org/x/crypto/ssh" ) +// proxyProtoPrefix is used when deciding what proxy protocol +// version to use. var proxyProtoPrefix = "proxyproto:" -func handleSession(newChannel ssh.NewChannel, sshConn *SSHConnection, state *State) { +// handleSession handles the channel when a user requests a session. +// This is how we send console messages. +func handleSession(newChannel ssh.NewChannel, sshConn *utils.SSHConnection, state *utils.State) { connection, requests, err := newChannel.Accept() if err != nil { sshConn.CleanUp(state) return } - if *debug { + if viper.GetBool("debug") { log.Println("Handling session for connection:", connection) } @@ -71,14 +78,14 @@ func handleSession(newChannel ssh.NewChannel, sshConn *SSHConnection, state *Sta } case "exec": payloadString := string(req.Payload[4:]) - if strings.HasPrefix(payloadString, proxyProtoPrefix) && *proxyProtoEnabled { + if strings.HasPrefix(payloadString, proxyProtoPrefix) && viper.GetBool("proxy-protocol") { sshConn.ProxyProto = getProxyProtoVersion(strings.TrimPrefix(payloadString, proxyProtoPrefix)) if sshConn.ProxyProto != 0 { - sendMessage(sshConn, fmt.Sprintf("Proxy protocol enabled for TCP connections. Using protocol version %d", int(sshConn.ProxyProto)), true) + sshConn.SendMessage(fmt.Sprintf("Proxy protocol enabled for TCP connections. Using protocol version %d", int(sshConn.ProxyProto)), true) } } default: - if *debug { + if viper.GetBool("debug") { log.Println("Sub Channel Type", req.Type, req.WantReply, string(req.Payload)) } } @@ -86,7 +93,8 @@ func handleSession(newChannel ssh.NewChannel, sshConn *SSHConnection, state *Sta }() } -func handleAlias(newChannel ssh.NewChannel, sshConn *SSHConnection, state *State) { +// handleAlias is used when handling a SSH connection to attach to an alias listener. +func handleAlias(newChannel ssh.NewChannel, sshConn *utils.SSHConnection, state *utils.State) { connection, requests, err := newChannel.Accept() if err != nil { sshConn.CleanUp(state) @@ -95,7 +103,7 @@ func handleAlias(newChannel ssh.NewChannel, sshConn *SSHConnection, state *State go ssh.DiscardRequests(requests) - if *debug { + if viper.GetBool("debug") { log.Println("Handling alias connection for:", connection) } @@ -108,23 +116,64 @@ func handleAlias(newChannel ssh.NewChannel, sshConn *SSHConnection, state *State } tcpAliasToConnect := fmt.Sprintf("%s:%d", check.Addr, check.Port) - loc, ok := state.TCPListeners.Load(tcpAliasToConnect) + loc, ok := state.AliasListeners.Load(tcpAliasToConnect) if !ok { log.Println("Unable to load tcp alias:", tcpAliasToConnect) sshConn.CleanUp(state) return } - conn, err := net.Dial("unix", loc.(string)) + aH := loc.(*utils.AliasHolder) + + connectionLocation, err := aH.Balancer.NextServer() + if err != nil { + log.Println("Unable to load connection location:", err) + sshConn.CleanUp(state) + return + } + + host, err := base64.StdEncoding.DecodeString(connectionLocation.Host) + if err != nil { + log.Println("Unable to decode connection location:", err) + sshConn.CleanUp(state) + return + } + + aliasAddr := string(host) + + logLine := fmt.Sprintf("Accepted connection from %s -> %s", sshConn.SSHConn.RemoteAddr().String(), tcpAliasToConnect) + log.Println(logLine) + + if viper.GetBool("log-to-client") { + aH.SSHConnections.Range(func(key, val interface{}) bool { + sshConn := val.(*utils.SSHConnection) + + sshConn.Listeners.Range(func(key, val interface{}) bool { + listenerAddr := key.(string) + + if listenerAddr == aliasAddr { + sshConn.SendMessage(logLine, true) + + return false + } + + return true + }) + + return true + }) + } + + conn, err := net.Dial("unix", aliasAddr) if err != nil { log.Println("Error connecting to alias:", err) sshConn.CleanUp(state) return } - sshConn.Listeners.Store(conn.RemoteAddr(), conn) + sshConn.Listeners.Store(aliasAddr, conn) - copyBoth(conn, connection) + utils.CopyBoth(conn, connection) select { case <-sshConn.Close: @@ -134,16 +183,18 @@ func handleAlias(newChannel ssh.NewChannel, sshConn *SSHConnection, state *State } } +// writeToSession is where we write to the underlying session channel. func writeToSession(connection ssh.Channel, c string) { _, err := connection.Write(append([]byte(c), []byte{'\r', '\n'}...)) - if err != nil && *debug { + if err != nil && viper.GetBool("debug") { log.Println("Error trying to write message to socket:", err) } } +// getProxyProtoVersion returns the proxy proto version selected by the client. func getProxyProtoVersion(proxyProtoUserVersion string) byte { - if *proxyProtoVersion != "userdefined" { - proxyProtoUserVersion = *proxyProtoVersion + if viper.GetString("proxy-protocol-version") != "userdefined" { + proxyProtoUserVersion = viper.GetString("proxy-protocol-version") } realProtoVersion := 0 diff --git a/handle.go b/sshmuxer/handle.go similarity index 61% rename from handle.go rename to sshmuxer/handle.go index cee90c8..e6fec52 100644 --- a/handle.go +++ b/sshmuxer/handle.go @@ -1,23 +1,27 @@ -package main +package sshmuxer import ( "fmt" "log" "time" + "github.com/antoniomika/sish/utils" + "github.com/spf13/viper" "golang.org/x/crypto/ssh" ) -func handleRequests(reqs <-chan *ssh.Request, sshConn *SSHConnection, state *State) { +// handleRequests handles incoming requests from an SSH connection. +func handleRequests(reqs <-chan *ssh.Request, sshConn *utils.SSHConnection, state *utils.State) { for req := range reqs { - if *debug { + if viper.GetBool("debug") { log.Println("Main Request Info", req.Type, req.WantReply, string(req.Payload)) } go handleRequest(req, sshConn, state) } } -func handleRequest(newRequest *ssh.Request, sshConn *SSHConnection, state *State) { +// handleRequest handles a incoming request from a SSH connection. +func handleRequest(newRequest *ssh.Request, sshConn *utils.SSHConnection, state *utils.State) { switch req := newRequest.Type; req { case "tcpip-forward": go checkSession(newRequest, sshConn, state) @@ -35,7 +39,8 @@ func handleRequest(newRequest *ssh.Request, sshConn *SSHConnection, state *State } } -func checkSession(newRequest *ssh.Request, sshConn *SSHConnection, state *State) { +// checkSession will check a session to see that it has a session. +func checkSession(newRequest *ssh.Request, sshConn *utils.SSHConnection, state *utils.State) { if sshConn.CleanupHandler { return } @@ -53,16 +58,18 @@ func checkSession(newRequest *ssh.Request, sshConn *SSHConnection, state *State) } } -func handleChannels(chans <-chan ssh.NewChannel, sshConn *SSHConnection, state *State) { +// handleChannels handles a SSH connection's channel requests. +func handleChannels(chans <-chan ssh.NewChannel, sshConn *utils.SSHConnection, state *utils.State) { for newChannel := range chans { - if *debug { + if viper.GetBool("debug") { log.Println("Main Channel Info", newChannel.ChannelType(), string(newChannel.ExtraData())) } go handleChannel(newChannel, sshConn, state) } } -func handleChannel(newChannel ssh.NewChannel, sshConn *SSHConnection, state *State) { +// handleChannel handles a SSH connection's channel request. +func handleChannel(newChannel ssh.NewChannel, sshConn *utils.SSHConnection, state *utils.State) { switch channel := newChannel.ChannelType(); channel { case "session": close(sshConn.Session) diff --git a/sshmuxer/httphandler.go b/sshmuxer/httphandler.go new file mode 100644 index 0000000..ba469c9 --- /dev/null +++ b/sshmuxer/httphandler.go @@ -0,0 +1,130 @@ +package sshmuxer + +import ( + "encoding/base64" + "fmt" + "log" + "net/url" + "sync" + + "github.com/antoniomika/oxy/forward" + "github.com/antoniomika/oxy/roundrobin" + "github.com/antoniomika/sish/httpmuxer" + "github.com/antoniomika/sish/utils" + "github.com/logrusorgru/aurora" + "github.com/spf13/viper" +) + +// handleHTTPListener handles the creation of the httpHandler +// (or addition for load balancing) and set's up the underlying listeners. +func handleHTTPListener(check *channelForwardMsg, stringPort string, requestMessages string, listenerHolder *utils.ListenerHolder, state *utils.State, sshConn *utils.SSHConnection) (*utils.HTTPHolder, *url.URL, string, string, error) { + scheme := "http" + if stringPort == "443" { + scheme = "https" + } + + host, pH := utils.GetOpenHost(check.Addr, state, sshConn) + + if pH == nil { + rT := httpmuxer.RoundTripper() + + fwd, err := forward.New( + forward.PassHostHeader(true), + forward.RoundTripper(rT), + forward.WebsocketRoundTripper(rT), + ) + + if err != nil { + log.Println("Error initializing HTTP forwarder:", err) + return nil, nil, "", "", err + } + + lb, err := roundrobin.New(fwd) + + if err != nil { + log.Println("Error initializing HTTP balancer:", err) + return nil, nil, "", "", err + } + + pH = &utils.HTTPHolder{ + HTTPHost: host, + Scheme: scheme, + SSHConnections: &sync.Map{}, + Forward: fwd, + Balancer: lb, + } + + state.HTTPListeners.Store(host, pH) + } + + pH.SSHConnections.Store(listenerHolder.Addr().String(), sshConn) + + serverURL := &url.URL{ + Host: base64.StdEncoding.EncodeToString([]byte(listenerHolder.Addr().String())), + Scheme: pH.Scheme, + } + + err := pH.Balancer.UpsertServer(serverURL) + if err != nil { + log.Println("Unable to add server to balancer") + } + + if viper.GetBool("admin-console") || viper.GetBool("service-console") { + routeToken := viper.GetString("service-console-token") + sendToken := false + routeExists := state.Console.RouteExists(host) + + if routeToken == "" { + sendToken = true + + if routeExists { + routeToken, _ = state.Console.RouteToken(host) + } else { + routeToken = utils.RandStringBytesMaskImprSrc(20) + } + } + + if !routeExists { + state.Console.AddRoute(host, routeToken) + } + + if viper.GetBool("service-console") && sendToken { + scheme := "http" + portString := "" + if httpPort != 80 { + portString = fmt.Sprintf(":%d", httpPort) + } + + if viper.GetBool("https") { + scheme = "https" + if httpsPort != 443 { + portString = fmt.Sprintf(":%d", httpsPort) + } + } + + consoleURL := fmt.Sprintf("%s://%s%s", scheme, host, portString) + + requestMessages += fmt.Sprintf("Service console can be accessed here: %s/_sish/console?x-authorization=%s\r\n", consoleURL, routeToken) + } + } + + httpPortString := "" + if httpPort != 80 { + httpPortString = fmt.Sprintf(":%d", httpPort) + } + + requestMessages += fmt.Sprintf("%s: http://%s%s\r\n", aurora.BgBlue("HTTP"), host, httpPortString) + log.Printf("%s forwarding started: http://%s%s -> %s for client: %s\n", aurora.BgBlue("HTTP"), host, httpPortString, listenerHolder.Addr().String(), sshConn.SSHConn.RemoteAddr().String()) + + if viper.GetBool("https") { + httpsPortString := "" + if httpsPort != 443 { + httpsPortString = fmt.Sprintf(":%d", httpsPort) + } + + requestMessages += fmt.Sprintf("%s: https://%s%s\r\n", aurora.BgBlue("HTTPS"), host, httpsPortString) + log.Printf("%s forwarding started: https://%s%s -> %s for client: %s\n", aurora.BgBlue("HTTPS"), host, httpPortString, listenerHolder.Addr().String(), sshConn.SSHConn.RemoteAddr().String()) + } + + return pH, serverURL, host, requestMessages, nil +} diff --git a/sshmuxer/requests.go b/sshmuxer/requests.go new file mode 100644 index 0000000..60a8e53 --- /dev/null +++ b/sshmuxer/requests.go @@ -0,0 +1,234 @@ +package sshmuxer + +import ( + "fmt" + "io/ioutil" + "log" + "net" + "os" + "strconv" + + "github.com/antoniomika/sish/utils" + "github.com/logrusorgru/aurora" + "github.com/pires/go-proxyproto" + "github.com/spf13/viper" + "golang.org/x/crypto/ssh" +) + +// channelForwardMsg is the message sent by SSH +// to init a forwarded connection. +type channelForwardMsg struct { + Addr string + Rport uint32 +} + +// forwardedTCPPayload is the payload sent by SSH +// to init a forwarded connection. +type forwardedTCPPayload struct { + Addr string + Port uint32 + OriginAddr string + OriginPort uint32 +} + +// handleRemoteForward will handle a remote forward request +// and stand up the relevant listeners. +func handleRemoteForward(newRequest *ssh.Request, sshConn *utils.SSHConnection, state *utils.State) { + check := &channelForwardMsg{} + + err := ssh.Unmarshal(newRequest.Payload, check) + if err != nil { + log.Println("Error unmarshaling remote forward payload:", err) + } + + bindPort := check.Rport + stringPort := strconv.FormatUint(uint64(bindPort), 10) + + listenerType := utils.HTTPListener + if bindPort != uint32(80) && bindPort != uint32(443) { + testAddr := net.ParseIP(check.Addr) + if viper.GetBool("tcp-aliases") && check.Addr != "localhost" && testAddr == nil { + listenerType = utils.AliasListener + } else if check.Addr == "localhost" || testAddr != nil { + listenerType = utils.TCPListener + } + } + + tmpfile, err := ioutil.TempFile("", sshConn.SSHConn.RemoteAddr().String()+":"+stringPort) + if err != nil { + err = newRequest.Reply(false, nil) + if err != nil { + log.Println("Error replying to socket request:", err) + } + return + } + os.Remove(tmpfile.Name()) + + listenAddr := tmpfile.Name() + + chanListener, err := net.Listen("unix", listenAddr) + if err != nil { + err = newRequest.Reply(false, nil) + if err != nil { + log.Println("Error replying to socket request:", err) + } + return + } + + listenerHolder := &utils.ListenerHolder{ + ListenAddr: listenAddr, + Listener: chanListener, + Type: listenerType, + SSHConn: sshConn, + } + + state.Listeners.Store(listenAddr, listenerHolder) + sshConn.Listeners.Store(listenAddr, listenerHolder) + + cleanupChanListener := func() { + listenerHolder.Close() + state.Listeners.Delete(listenAddr) + sshConn.Listeners.Delete(listenAddr) + os.Remove(listenAddr) + } + + defer cleanupChanListener() + + go func() { + <-sshConn.Close + cleanupChanListener() + }() + + connType := "tcp" + if stringPort == "80" { + connType = "http" + } else if stringPort == "443" { + connType = "https" + } + + mainRequestMessages := fmt.Sprintf("Starting SSH Forwarding service for %s. Forwarded connections can be accessed via the following methods:\r\n", aurora.Sprintf(aurora.Green("%s:%s"), connType, stringPort)) + + switch listenerType { + case utils.HTTPListener: + pH, serverURL, host, requestMessages, err := handleHTTPListener(check, stringPort, mainRequestMessages, listenerHolder, state, sshConn) + if err != nil { + return + } + + mainRequestMessages = requestMessages + + defer func() { + err := pH.Balancer.RemoveServer(serverURL) + if err != nil { + log.Println("Unable to add server to balancer") + } + + pH.SSHConnections.Delete(listenerHolder.Addr().String()) + + if len(pH.Balancer.Servers()) == 0 { + state.HTTPListeners.Delete(host) + + if viper.GetBool("admin-console") || viper.GetBool("service-console") { + state.Console.RemoveRoute(host) + } + } + }() + case utils.AliasListener: + aH, serverURL, validAlias, requestMessages, err := handleAliasListener(check, stringPort, mainRequestMessages, listenerHolder, state, sshConn) + if err != nil { + return + } + + mainRequestMessages = requestMessages + + defer func() { + err := aH.Balancer.RemoveServer(serverURL) + if err != nil { + log.Println("Unable to add server to balancer") + } + + aH.SSHConnections.Delete(listenerHolder.Addr().String()) + + if len(aH.Balancer.Servers()) == 0 { + state.AliasListeners.Delete(validAlias) + } + }() + case utils.TCPListener: + tH, serverURL, tcpAddr, requestMessages, err := handleTCPListener(check, bindPort, mainRequestMessages, listenerHolder, state, sshConn) + if err != nil { + return + } + + mainRequestMessages = requestMessages + + go tH.Handle(state) + + defer func() { + err := tH.Balancer.RemoveServer(serverURL) + if err != nil { + log.Println("Unable to add server to balancer") + } + + tH.SSHConnections.Delete(listenerHolder.Addr().String()) + + if len(tH.Balancer.Servers()) == 0 { + tH.Listener.Close() + state.Listeners.Delete(tcpAddr) + state.TCPListeners.Delete(tcpAddr) + } + }() + } + + sshConn.SendMessage(mainRequestMessages, false) + + for { + cl, err := listenerHolder.Accept() + if err != nil { + break + } + + resp := &forwardedTCPPayload{ + Addr: check.Addr, + Port: check.Rport, + OriginAddr: check.Addr, + OriginPort: check.Rport, + } + + newChan, newReqs, err := sshConn.SSHConn.OpenChannel("forwarded-tcpip", ssh.Marshal(resp)) + if err != nil { + sshConn.SendMessage(err.Error(), true) + cl.Close() + continue + } + + if sshConn.ProxyProto != 0 && listenerType == utils.TCPListener { + var sourceInfo *net.TCPAddr + var destInfo *net.TCPAddr + if _, ok := cl.RemoteAddr().(*net.TCPAddr); !ok { + sourceInfo = sshConn.SSHConn.RemoteAddr().(*net.TCPAddr) + destInfo = sshConn.SSHConn.LocalAddr().(*net.TCPAddr) + } else { + sourceInfo = cl.RemoteAddr().(*net.TCPAddr) + destInfo = cl.LocalAddr().(*net.TCPAddr) + } + + proxyProtoHeader := proxyproto.Header{ + Version: sshConn.ProxyProto, + Command: proxyproto.ProtocolVersionAndCommand(proxyproto.PROXY), + TransportProtocol: proxyproto.AddressFamilyAndProtocol(proxyproto.TCPv4), + SourceAddress: sourceInfo.IP, + DestinationAddress: destInfo.IP, + SourcePort: uint16(sourceInfo.Port), + DestinationPort: uint16(destInfo.Port), + } + + _, err := proxyProtoHeader.WriteTo(newChan) + if err != nil && viper.GetBool("debug") { + log.Println("Error writing to channel:", err) + } + } + + go utils.CopyBoth(cl, newChan) + go ssh.DiscardRequests(newReqs) + } +} diff --git a/sshmuxer/sshmuxer.go b/sshmuxer/sshmuxer.go new file mode 100644 index 0000000..0befe88 --- /dev/null +++ b/sshmuxer/sshmuxer.go @@ -0,0 +1,250 @@ +// Package sshmuxer handles the underlying SSH server +// and multiplexing forwarding sessions. +package sshmuxer + +import ( + "log" + "net" + "os" + "os/signal" + "runtime" + "strconv" + "sync" + "time" + + "github.com/antoniomika/sish/httpmuxer" + "github.com/antoniomika/sish/utils" + "github.com/pires/go-proxyproto" + "github.com/spf13/viper" + "golang.org/x/crypto/ssh" +) + +var ( + // httpPort is used as a string override for the used HTTP port. + httpPort int + + // httpsPort is used as a string override for the used HTTPS port. + httpsPort int +) + +// Start initializes the ssh muxer service. It will start necessary components +// and begin listening for SSH connections. +func Start() { + _, httpPortString, err := net.SplitHostPort(viper.GetString("http-address")) + if err != nil { + log.Fatalln("Error parsing address:", err) + } + + _, httpsPortString, err := net.SplitHostPort(viper.GetString("https-address")) + if err != nil { + log.Fatalln("Error parsing address:", err) + } + + httpPort, err = strconv.Atoi(httpPortString) + if err != nil { + log.Fatalln("Error parsing address:", err) + } + + httpsPort, err = strconv.Atoi(httpsPortString) + if err != nil { + log.Fatalln("Error parsing address:", err) + } + + if viper.GetInt("http-port-override") != 0 { + httpPort = viper.GetInt("http-port-override") + } + + if viper.GetInt("https-port-override") != 0 { + httpsPort = viper.GetInt("https-port-override") + } + + utils.WatchCerts() + + state := utils.NewState() + state.Console.State = state + + go httpmuxer.Start(state) + + if viper.GetBool("debug") { + go func() { + for { + log.Println("=======Start=========") + log.Println("===Goroutines=====") + log.Println(runtime.NumGoroutine()) + log.Println("===Listeners======") + state.Listeners.Range(func(key, value interface{}) bool { + log.Println(key, value) + return true + }) + log.Println("===Clients========") + state.SSHConnections.Range(func(key, value interface{}) bool { + log.Println(key, value) + return true + }) + log.Println("===HTTP Clients===") + state.HTTPListeners.Range(func(key, value interface{}) bool { + log.Println(key, value) + return true + }) + log.Println("===TCP Aliases====") + state.AliasListeners.Range(func(key, value interface{}) bool { + log.Println(key, value) + return true + }) + log.Println("===Web Console Routes====") + state.Console.Clients.Range(func(key, value interface{}) bool { + log.Println(key, value) + return true + }) + log.Println("===Web Console Tokens====") + state.Console.RouteTokens.Range(func(key, value interface{}) bool { + log.Println(key, value) + return true + }) + log.Print("========End==========\n") + + time.Sleep(2 * time.Second) + } + }() + } + + log.Println("Starting SSH service on address:", viper.GetString("ssh-address")) + + sshConfig := utils.GetSSHConfig() + + l, err := net.Listen("tcp", viper.GetString("ssh-address")) + if err != nil { + log.Fatal(err) + } + + listener := &proxyproto.Listener{ + Listener: l, + } + + state.Listeners.Store(viper.GetString("ssh-address"), listener) + + defer func() { + listener.Close() + state.Listeners.Delete(viper.GetString("ssh-address")) + }() + + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + go func() { + for range c { + os.Exit(0) + } + }() + + for { + conn, err := listener.Accept() + if err != nil { + log.Println(err) + continue + } + + clientRemote, _, err := net.SplitHostPort(conn.RemoteAddr().String()) + + if err != nil || state.IPFilter.Blocked(clientRemote) { + conn.Close() + continue + } + + clientLoggedIn := false + + if viper.GetBool("cleanup-unbound") { + go func() { + <-time.After(viper.GetDuration("cleanup-unbound-timeout")) + if !clientLoggedIn { + conn.Close() + } + }() + } + + log.Println("Accepted SSH connection for:", conn.RemoteAddr()) + + go func() { + sshConn, chans, reqs, err := ssh.NewServerConn(conn, sshConfig) + clientLoggedIn = true + if err != nil { + conn.Close() + log.Println(err) + return + } + + holderConn := &utils.SSHConnection{ + SSHConn: sshConn, + Listeners: &sync.Map{}, + Closed: &sync.Once{}, + Close: make(chan bool), + Messages: make(chan string), + Session: make(chan bool), + } + + state.SSHConnections.Store(sshConn.RemoteAddr().String(), holderConn) + + go func() { + err := sshConn.Wait() + if err != nil && viper.GetBool("debug") { + log.Println("Closing SSH connection:", err) + } + + select { + case <-holderConn.Close: + break + default: + holderConn.CleanUp(state) + } + }() + + go handleRequests(reqs, holderConn, state) + go handleChannels(chans, holderConn, state) + + if viper.GetBool("cleanup-unbound") { + go func() { + select { + case <-time.After(viper.GetDuration("cleanup-unbound-timeout")): + count := 0 + holderConn.Listeners.Range(func(key, value interface{}) bool { + count++ + return true + }) + + if count == 0 { + holderConn.SendMessage("No forwarding requests sent. Closing connection.", true) + time.Sleep(1 * time.Millisecond) + holderConn.CleanUp(state) + } + case <-holderConn.Close: + return + } + }() + } + + if viper.GetBool("ping-client") { + go func() { + tickDuration := viper.GetDuration("ping-client-interval") + ticker := time.NewTicker(tickDuration) + + for { + err := conn.SetDeadline(time.Now().Add(tickDuration).Add(viper.GetDuration("ping-client-timeout"))) + if err != nil { + log.Println("Unable to set deadline") + } + + select { + case <-ticker.C: + _, _, err := sshConn.SendRequest("keepalive@sish", true, nil) + if err != nil { + log.Println("Error retrieving keepalive response:", err) + return + } + case <-holderConn.Close: + return + } + } + }() + } + }() + } +} diff --git a/sshmuxer/tcphandler.go b/sshmuxer/tcphandler.go new file mode 100644 index 0000000..f844286 --- /dev/null +++ b/sshmuxer/tcphandler.go @@ -0,0 +1,69 @@ +package sshmuxer + +import ( + "encoding/base64" + "fmt" + "log" + "net" + "net/url" + "sync" + + "github.com/antoniomika/oxy/roundrobin" + "github.com/antoniomika/sish/utils" + "github.com/logrusorgru/aurora" + "github.com/pires/go-proxyproto" + "github.com/spf13/viper" +) + +// handleTCPListener handles the creation of the tcpHandler +// (or addition for load balancing) and set's up the underlying listeners. +func handleTCPListener(check *channelForwardMsg, bindPort uint32, requestMessages string, listenerHolder *utils.ListenerHolder, state *utils.State, sshConn *utils.SSHConnection) (*utils.TCPHolder, *url.URL, string, string, error) { + tcpAddr, _, tH := utils.GetOpenPort(check.Addr, bindPort, state, sshConn) + + if tH == nil { + lb, err := roundrobin.New(nil) + + if err != nil { + log.Println("Error initializing tcp balancer:", err) + return nil, nil, "", "", err + } + + tH = &utils.TCPHolder{ + TCPHost: tcpAddr, + SSHConnections: &sync.Map{}, + Balancer: lb, + } + + l, err := net.Listen("tcp", tcpAddr) + if err != nil { + log.Println("Error listening on addr:", err) + return nil, nil, "", "", err + } + + ln := &proxyproto.Listener{ + Listener: l, + } + + tH.Listener = ln + + state.Listeners.Store(tcpAddr, ln) + state.TCPListeners.Store(tcpAddr, tH) + } + + tH.SSHConnections.Store(listenerHolder.Addr().String(), sshConn) + + serverURL := &url.URL{ + Host: base64.StdEncoding.EncodeToString([]byte(listenerHolder.Addr().String())), + } + + err := tH.Balancer.UpsertServer(serverURL) + if err != nil { + log.Println("Unable to add server to balancer") + } + + listenPort := tH.Listener.Addr().(*net.TCPAddr).Port + requestMessages += fmt.Sprintf("%s: %s:%d\r\n", aurora.BgBlue("TCP"), viper.GetString("domain"), listenPort) + log.Printf("%s forwarding started: %s:%d -> %s for client: %s\n", aurora.BgBlue("TCP"), viper.GetString("domain"), listenPort, listenerHolder.Addr().String(), sshConn.SSHConn.RemoteAddr().String()) + + return tH, serverURL, tcpAddr, requestMessages, nil +} diff --git a/templates/console.tmpl b/templates/console.tmpl index d23ae3e..bb6709a 100644 --- a/templates/console.tmpl +++ b/templates/console.tmpl @@ -1,46 +1,60 @@ {{ define "console" }} {{ template "header" .}}
-
+
+ + - + - + + + + + + + + +
DateClient IP Method Path StatusTimeDuration
-
+

Request Info

Headers

+

                 

Body

+

                 

-
+

Response Info:

Headers

+

                 

Body

+

                 
@@ -48,51 +62,62 @@
diff --git a/templates/header.tmpl b/templates/header.tmpl index 12a2477..e107c4b 100644 --- a/templates/header.tmpl +++ b/templates/header.tmpl @@ -17,6 +17,10 @@ + + + + + + @@ -41,15 +47,19 @@ -
+
{{ end }} \ No newline at end of file diff --git a/templates/routes.tmpl b/templates/routes.tmpl index 8aac3b6..b97af54 100644 --- a/templates/routes.tmpl +++ b/templates/routes.tmpl @@ -1,7 +1,7 @@ {{ define "routes" }} {{ template "header" .}}
-
+

Clients

@@ -10,19 +10,29 @@ + - + + + + + + + + + +
Username SSH Version SSH Session IDSSH Pubkey Fingerprint Listeners Disconnect
Disconnect
-
+

Routes

- + @@ -32,82 +42,91 @@ - + + + + + + + +
TypeDisconnect
+ + + Disconnect
{{ template "footer" .}} {{ end }} \ No newline at end of file diff --git a/utils.go b/utils.go deleted file mode 100644 index 0d23d44..0000000 --- a/utils.go +++ /dev/null @@ -1,419 +0,0 @@ -package main - -import ( - "bytes" - "crypto/ed25519" - "crypto/rand" - "crypto/x509" - "encoding/pem" - "fmt" - "io/ioutil" - "log" - mathrand "math/rand" - "net" - "os" - "os/signal" - "path/filepath" - "strconv" - "strings" - "sync" - "time" - - "github.com/fsnotify/fsnotify" - "github.com/logrusorgru/aurora" - "github.com/mikesmitty/edkey" - "golang.org/x/crypto/ssh" -) - -var ( - certHolder = make([]ssh.PublicKey, 0) - holderLock = sync.Mutex{} -) - -func getRandomPortInRange(portRange string) uint32 { - var bindPort uint32 - - ranges := strings.Split(strings.TrimSpace(portRange), ",") - possible := [][]uint64{} - for _, r := range ranges { - ends := strings.Split(strings.TrimSpace(r), "-") - - if len(ends) == 1 { - ui, err := strconv.ParseUint(ends[0], 0, 64) - if err != nil { - return 0 - } - - possible = append(possible, []uint64{uint64(ui)}) - } else if len(ends) == 2 { - ui1, err := strconv.ParseUint(ends[0], 0, 64) - if err != nil { - return 0 - } - - ui2, err := strconv.ParseUint(ends[1], 0, 64) - if err != nil { - return 0 - } - - possible = append(possible, []uint64{uint64(ui1), uint64(ui2)}) - } - } - - mathrand.Seed(time.Now().UnixNano()) - locHolder := mathrand.Intn(len(possible)) - - if len(possible[locHolder]) == 1 { - bindPort = uint32(possible[locHolder][0]) - } else if len(possible[locHolder]) == 2 { - bindPort = uint32(mathrand.Intn(int(possible[locHolder][1]-possible[locHolder][0])) + int(possible[locHolder][0])) - } - - ln, err := net.Listen("tcp", fmt.Sprintf(":%d", bindPort)) - if err != nil { - return getRandomPortInRange(portRange) - } - - ln.Close() - - return bindPort -} - -func checkPort(port uint32, portRanges string) (uint32, error) { - ranges := strings.Split(strings.TrimSpace(portRanges), ",") - checks := false - for _, r := range ranges { - ends := strings.Split(strings.TrimSpace(r), "-") - - if len(ends) == 1 { - ui, err := strconv.ParseUint(ends[0], 0, 64) - if err != nil { - return 0, err - } - - if uint64(ui) == uint64(port) { - checks = true - continue - } - } else if len(ends) == 2 { - ui1, err := strconv.ParseUint(ends[0], 0, 64) - if err != nil { - return 0, err - } - - ui2, err := strconv.ParseUint(ends[1], 0, 64) - if err != nil { - return 0, err - } - - if uint64(port) >= ui1 && uint64(port) <= ui2 { - checks = true - continue - } - } - } - - if checks { - return port, nil - } - - return 0, fmt.Errorf("not a safe port") -} - -func watchCerts() { - loadCerts() - watcher, err := fsnotify.NewWatcher() - if err != nil { - log.Fatal(err) - } - - go func() { - c := make(chan os.Signal, 1) - signal.Notify(c, os.Interrupt) - go func() { - for range c { - watcher.Close() - os.Exit(0) - } - }() - - for { - select { - case _, ok := <-watcher.Events: - if !ok { - return - } - loadCerts() - case _, ok := <-watcher.Errors: - if !ok { - return - } - } - } - }() - - err = watcher.Add(*authKeysDir) - if err != nil { - log.Fatal(err) - } -} - -func loadCerts() { - tmpCertHolder := make([]ssh.PublicKey, 0) - - files, err := ioutil.ReadDir(*authKeysDir) - if err != nil { - log.Fatal(err) - } - - parseKey := func(keyBytes []byte, fileInfo os.FileInfo) { - keyHandle := func(keyBytes []byte, fileInfo os.FileInfo) []byte { - key, _, _, rest, e := ssh.ParseAuthorizedKey(keyBytes) - if e != nil { - log.Printf("Can't load file %s as public key: %s\n", fileInfo.Name(), e) - } - - if key != nil { - tmpCertHolder = append(tmpCertHolder, key) - } - return rest - } - - for ok := true; ok; ok = len(keyBytes) > 0 { - keyBytes = keyHandle(keyBytes, fileInfo) - } - } - - for _, f := range files { - i, e := ioutil.ReadFile(filepath.Join(*authKeysDir, f.Name())) - if e == nil && len(i) > 0 { - parseKey(i, f) - } - } - - holderLock.Lock() - defer holderLock.Unlock() - certHolder = tmpCertHolder -} - -func getSSHConfig() *ssh.ServerConfig { - sshConfig := &ssh.ServerConfig{ - NoClientAuth: !*authEnabled, - PasswordCallback: func(c ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) { - log.Printf("Login attempt: %s, user %s", c.RemoteAddr(), c.User()) - - if string(password) == *authPassword && *authPassword != "" { - return nil, nil - } - - return nil, fmt.Errorf("password doesn't match") - }, - PublicKeyCallback: func(c ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) { - log.Printf("Login attempt: %s, user %s key: %s", c.RemoteAddr(), c.User(), string(ssh.MarshalAuthorizedKey(key))) - - holderLock.Lock() - defer holderLock.Unlock() - for _, i := range certHolder { - if bytes.Equal(key.Marshal(), i.Marshal()) { - return nil, nil - } - } - - return nil, fmt.Errorf("public key doesn't match") - }, - } - sshConfig.AddHostKey(loadPrivateKey(*pkPass)) - return sshConfig -} - -func generatePrivateKey(passphrase string) []byte { - _, pk, err := ed25519.GenerateKey(rand.Reader) - if err != nil { - log.Fatal(err) - } - - log.Println("Generated RSA Keypair") - - pemBlock := &pem.Block{ - Type: "OPENSSH PRIVATE KEY", - Bytes: edkey.MarshalED25519PrivateKey(pk), - } - - var pemData []byte - - if passphrase != "" { - encBlock, err := x509.EncryptPEMBlock(rand.Reader, pemBlock.Type, pemBlock.Bytes, []byte(passphrase), x509.PEMCipherAES256) - if err != nil { - log.Fatal(err) - } - - pemData = pem.EncodeToMemory(encBlock) - } else { - pemData = pem.EncodeToMemory(pemBlock) - } - - err = ioutil.WriteFile(*pkLoc, pemData, 0644) - if err != nil { - log.Println("Error writing to file:", err) - } - - return pemData -} - -// ParsePrivateKey pareses the PrivateKey into a ssh.Signer and let's it be used by CASigner -func loadPrivateKey(passphrase string) ssh.Signer { - var signer ssh.Signer - - pk, err := ioutil.ReadFile(*pkLoc) - if err != nil { - pk = generatePrivateKey(passphrase) - } - - if passphrase != "" { - signer, err = ssh.ParsePrivateKeyWithPassphrase(pk, []byte(passphrase)) - if err != nil { - log.Fatal(err) - } - } else { - signer, err = ssh.ParsePrivateKey(pk) - if err != nil { - log.Fatal(err) - } - } - - return signer -} - -func inBannedList(host string, bannedList []string) bool { - for _, v := range bannedList { - if strings.TrimSpace(v) == host { - return true - } - } - - return false -} - -func getOpenHost(addr string, state *State, sshConn *SSHConnection) string { - getUnusedHost := func() string { - first := true - - hostExtension := "" - if *appendUserToSubdomain { - hostExtension = *userSubdomainSeparator + sshConn.SSHConn.User() - } - - host := strings.ToLower(addr + hostExtension + "." + *rootDomain) - - getRandomHost := func() string { - return strings.ToLower(RandStringBytesMaskImprSrc(*domainLen) + "." + *rootDomain) - } - reportUnavailable := func(unavailable bool) { - if first && unavailable { - sendMessage(sshConn, aurora.Sprintf("The subdomain %s is unavailable. Assigning a random subdomain.", aurora.Red(host)), true) - } - } - - checkHost := func(checkHost string) bool { - if *forceRandomSubdomain || !first || inBannedList(host, bannedSubdomainList) { - reportUnavailable(true) - host = getRandomHost() - } - - _, ok := state.HTTPListeners.Load(host) - reportUnavailable(ok) - - first = false - return ok - } - - for checkHost(host) { - } - - return host - } - - return getUnusedHost() -} - -func getOpenAlias(addr string, port string, state *State, sshConn *SSHConnection) string { - getUnusedAlias := func() string { - first := true - alias := fmt.Sprintf("%s:%s", strings.ToLower(addr), port) - getRandomAlias := func() string { - return fmt.Sprintf("%s:%s", strings.ToLower(RandStringBytesMaskImprSrc(*domainLen)), port) - } - reportUnavailable := func(unavailable bool) { - if first && unavailable { - sendMessage(sshConn, aurora.Sprintf("The alias %s is unavaible. Assigning a random alias.", aurora.Red(alias)), true) - } - } - - checkAlias := func(checkAlias string) bool { - if *forceRandomSubdomain || !first || inBannedList(alias, bannedSubdomainList) { - reportUnavailable(true) - alias = getRandomAlias() - } - - _, ok := state.TCPListeners.Load(alias) - reportUnavailable(ok) - - first = false - return ok - } - - for checkAlias(alias) { - } - - return alias - } - - return getUnusedAlias() -} - -// RandStringBytesMaskImprSrc creates a random string of length n -// https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-golang -func RandStringBytesMaskImprSrc(n int) string { - const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - const ( - letterIdxBits = 6 // 6 bits to represent a letter index - letterIdxMask = 1<= 0; { - if remain == 0 { - cache, remain = src.Int63(), letterIdxMax - } - if idx := int(cache & letterIdxMask); idx < len(letterBytes) { - b[i] = letterBytes[idx] - i-- - } - cache >>= letterIdxBits - remain-- - } - - return string(b) -} - -func sendMessage(sshConn *SSHConnection, message string, block bool) { - if block { - sshConn.Messages <- message - return - } - - for i := 0; i < 5; { - select { - case <-sshConn.Close: - return - case sshConn.Messages <- message: - return - default: - time.Sleep(100 * time.Millisecond) - i++ - } - } -} diff --git a/utils/conn.go b/utils/conn.go new file mode 100644 index 0000000..9a9f306 --- /dev/null +++ b/utils/conn.go @@ -0,0 +1,123 @@ +package utils + +import ( + "io" + "log" + "net" + "sync" + "time" + + "github.com/spf13/viper" + "golang.org/x/crypto/ssh" +) + +// SSHConnection handles state for a SSHConnection. It wraps an ssh.ServerConn +// and allows us to pass other state around the application. +// Listeners is a map[string]net.Listener +type SSHConnection struct { + SSHConn *ssh.ServerConn + Listeners *sync.Map + Closed *sync.Once + Close chan bool + Messages chan string + ProxyProto byte + Session chan bool + CleanupHandler bool +} + +// SendMessage sends a console message to the connection. If block is true, it +// will block until the message is sent. If it is false, it will try to send the +// message 5 times, waiting 100ms each time. +func (s *SSHConnection) SendMessage(message string, block bool) { + if block { + s.Messages <- message + return + } + + for i := 0; i < 5; { + select { + case <-s.Close: + return + case s.Messages <- message: + return + default: + time.Sleep(100 * time.Millisecond) + i++ + } + } +} + +// CleanUp closes all allocated resources for a SSH session and cleans them up. +func (s *SSHConnection) CleanUp(state *State) { + s.Closed.Do(func() { + close(s.Close) + s.SSHConn.Close() + state.SSHConnections.Delete(s.SSHConn.RemoteAddr().String()) + log.Println("Closed SSH connection for:", s.SSHConn.RemoteAddr().String(), "user:", s.SSHConn.User()) + }) +} + +// IdleTimeoutConn handles the connection with a context deadline. +// code adapted from https://qiita.com/kwi/items/b38d6273624ad3f6ae79 +type IdleTimeoutConn struct { + Conn net.Conn +} + +// Read is needed to implement the reader part +func (i IdleTimeoutConn) Read(buf []byte) (int, error) { + err := i.Conn.SetReadDeadline(time.Now().Add(viper.GetDuration("idle-connection-timeout"))) + if err != nil { + return 0, err + } + + return i.Conn.Read(buf) +} + +// Write is needed to implement the writer part. +func (i IdleTimeoutConn) Write(buf []byte) (int, error) { + err := i.Conn.SetWriteDeadline(time.Now().Add(viper.GetDuration("idle-connection-timeout"))) + if err != nil { + return 0, err + } + + return i.Conn.Write(buf) +} + +// CopyBoth copies betwen a reader and writer and will cleanup each. +func CopyBoth(writer net.Conn, reader io.ReadWriteCloser) { + closeBoth := func() { + reader.Close() + writer.Close() + } + + var tcon io.ReadWriter + + if viper.GetBool("idle-connection") { + tcon = IdleTimeoutConn{ + Conn: writer, + } + } else { + tcon = writer + } + + copyToReader := func() { + _, err := io.Copy(reader, tcon) + if err != nil && viper.GetBool("debug") { + log.Println("Error copying to reader:", err) + } + + closeBoth() + } + + copyToWriter := func() { + _, err := io.Copy(tcon, reader) + if err != nil && viper.GetBool("debug") { + log.Println("Error copying to writer:", err) + } + + closeBoth() + } + + go copyToReader() + copyToWriter() +} diff --git a/console.go b/utils/console.go similarity index 52% rename from console.go rename to utils/console.go index 7000df7..4586ef8 100644 --- a/console.go +++ b/utils/console.go @@ -1,23 +1,26 @@ -package main +package utils import ( + "encoding/base64" "fmt" "log" - "net" "net/http" "strings" "sync" "github.com/gin-gonic/gin" "github.com/gorilla/websocket" + "github.com/spf13/viper" ) +// upgrader is the default WS upgrader that we use for webconsole clients. var upgrader = websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, } -// WebClient represents a primitive web console client +// WebClient represents a primitive web console client. It maintains +// references that allow us to communicate and track a client connection. type WebClient struct { Conn *websocket.Conn Console *WebConsole @@ -25,15 +28,16 @@ type WebClient struct { Route string } -// WebConsole represents the data structure that stores web console client information -// Clients is a map[string][]*WebClient +// WebConsole represents the data structure that stores web console client information. +// Clients is a map[string][]*WebClient. +// RouteTokens is a map[string]string. type WebConsole struct { Clients *sync.Map RouteTokens *sync.Map State *State } -// NewWebConsole set's up the WebConsole +// NewWebConsole sets up the WebConsole. func NewWebConsole() *WebConsole { return &WebConsole{ Clients: &sync.Map{}, @@ -41,11 +45,11 @@ func NewWebConsole() *WebConsole { } } -// HandleRequest handles an incoming WS request +// HandleRequest handles an incoming web request, handles auth, and then routes it. func (c *WebConsole) HandleRequest(hostname string, hostIsRoot bool, g *gin.Context) { userAuthed := false userIsAdmin := false - if (*adminEnabled && *adminToken != "") && (g.Request.URL.Query().Get("x-authorization") == *adminToken || g.Request.Header.Get("x-authorization") == *adminToken) { + if (viper.GetBool("admin-console") && viper.GetString("admin-console-token") != "") && (g.Request.URL.Query().Get("x-authorization") == viper.GetString("admin-console-token") || g.Request.Header.Get("x-authorization") == viper.GetString("admin-console-token")) { userIsAdmin = true userAuthed = true } @@ -53,7 +57,7 @@ func (c *WebConsole) HandleRequest(hostname string, hostIsRoot bool, g *gin.Cont tokenInterface, ok := c.RouteTokens.Load(hostname) if ok { routeToken, ok := tokenInterface.(string) - if *serviceConsoleEnabled && ok && (g.Request.URL.Query().Get("x-authorization") == routeToken || g.Request.Header.Get("x-authorization") == routeToken) { + if viper.GetBool("service-console") && ok && (g.Request.URL.Query().Get("x-authorization") == routeToken || g.Request.Header.Get("x-authorization") == routeToken) { userAuthed = true } } @@ -70,19 +74,13 @@ func (c *WebConsole) HandleRequest(hostname string, hostIsRoot bool, g *gin.Cont } else if strings.HasPrefix(g.Request.URL.Path, "/_sish/api/disconnectroute/") && userIsAdmin { c.HandleDisconnectRoute(hostname, g) return - } else if strings.HasPrefix(g.Request.URL.Path, "/_sish/api/routes") && hostIsRoot && userIsAdmin { - c.HandleRoutes(hostname, g) - return - } else if strings.HasPrefix(g.Request.URL.Path, "/_sish/api/allroutes") && hostIsRoot && userIsAdmin { - c.HandleAllRoutes(hostname, g) - return } else if strings.HasPrefix(g.Request.URL.Path, "/_sish/api/clients") && hostIsRoot && userIsAdmin { c.HandleClients(hostname, g) return } } -// HandleTemplate handles rendering the console template +// HandleTemplate handles rendering the console templates. func (c *WebConsole) HandleTemplate(hostname string, hostIsRoot bool, userIsAdmin bool, g *gin.Context) { if hostIsRoot && userIsAdmin { g.HTML(http.StatusOK, "routes", nil) @@ -120,14 +118,14 @@ func (c *WebConsole) HandleWebSocket(hostname string, g *gin.Context) { go client.Handle() } -// HandleDisconnectClient handles the disconnection request for a client +// HandleDisconnectClient handles the disconnection request for a SSH client. func (c *WebConsole) HandleDisconnectClient(hostname string, g *gin.Context) { client := strings.TrimPrefix(g.Request.URL.Path, "/_sish/api/disconnectclient/") c.State.SSHConnections.Range(func(key interface{}, val interface{}) bool { - clientName := key.(*net.TCPAddr) + clientName := key.(string) - if clientName.String() == client { + if clientName == client { holderConn := val.(*SSHConnection) holderConn.CleanUp(c.State) @@ -144,95 +142,43 @@ func (c *WebConsole) HandleDisconnectClient(hostname string, g *gin.Context) { g.JSON(http.StatusOK, data) } -// HandleDisconnectRoute handles the disconnection request for a route +// HandleDisconnectRoute handles the disconnection request for a forwarded route. func (c *WebConsole) HandleDisconnectRoute(hostname string, g *gin.Context) { route := strings.Split(strings.TrimPrefix(g.Request.URL.Path, "/_sish/api/disconnectroute/"), "/") - routeType := route[0] - routeName := route[1] - - var listenerAddr string - - switch routeType { - case "tcpalias": - c.State.TCPListeners.Range(func(key interface{}, val interface{}) bool { - tcpAlias := key.(string) - if routeName == tcpAlias { - listenerAddr = val.(string) - } - - return true - }) - case "httplistener": - c.State.HTTPListeners.Range(func(key interface{}, val interface{}) bool { - httpListener := key.(string) - if routeName == httpListener { - listenerAddrTmp := val.(*ProxyHolder) - listenerAddr = listenerAddrTmp.ProxyTo - } - - return true - }) - } + encRouteName := route[1] - if listenerAddr == "" { - listenerAddr = routeName - } + decRouteName, err := base64.StdEncoding.DecodeString(encRouteName) + if err != nil { + log.Println("Error decoding route name:", err) + err := g.AbortWithError(http.StatusInternalServerError, err) - c.State.Listeners.Range(func(key interface{}, val interface{}) bool { - var tcpListener *net.TCPAddr - unixListener, ok := key.(*net.UnixAddr) - if !ok { - tcpListener = key.(*net.TCPAddr) + if err != nil { + log.Println("Error aborting with error:", err) } + return + } - var name string + routeName := string(decRouteName) - if unixListener != nil { - name = unixListener.String() - } else { - name = tcpListener.String() - } + listenerTmp, ok := c.State.Listeners.Load(routeName) + if ok { + listener, ok := listenerTmp.(*ListenerHolder) - if listenerAddr == name { - if unixListener != nil { - actualUnixListener := val.(*net.UnixListener) - actualUnixListener.Close() - } else { - actualTCPListener := val.(*net.TCPListener) - actualTCPListener.Close() - } + if ok { + listener.Close() } - - return true - }) - - data := map[string]interface{}{ - "status": true, } - g.JSON(http.StatusOK, data) -} - -// HandleRoutes handles returning available http routes to join -func (c *WebConsole) HandleRoutes(hostname string, g *gin.Context) { data := map[string]interface{}{ "status": true, } - routes := []string{} - c.Clients.Range(func(key interface{}, val interface{}) bool { - routeName := key.(string) - routes = append(routes, routeName) - - return true - }) - - data["routes"] = routes - g.JSON(http.StatusOK, data) } -// HandleClients handles returning all connected clients +// HandleClients handles returning all connected SSH clients. This will +// also go through all of the forwarded connections for the SSH client and +// return them. func (c *WebConsole) HandleClients(hostname string, g *gin.Context) { data := map[string]interface{}{ "status": true, @@ -240,84 +186,89 @@ func (c *WebConsole) HandleClients(hostname string, g *gin.Context) { clients := map[string]map[string]interface{}{} c.State.SSHConnections.Range(func(key interface{}, val interface{}) bool { - clientName := key.(*net.TCPAddr) + clientName := key.(string) sshConn := val.(*SSHConnection) listeners := []string{} - routeListeners := map[string]map[string]string{} + routeListeners := map[string]map[string]interface{}{} sshConn.Listeners.Range(func(key interface{}, val interface{}) bool { - var tcpListener *net.TCPAddr - unixListener, ok := key.(*net.UnixAddr) - if !ok { - tcpListener = key.(*net.TCPAddr) - } - - var name string - - if ok { - name = unixListener.String() - } else { - name = tcpListener.String() - } + name, ok := key.(string) - altName, ok := val.(string) if ok { - name = altName + listeners = append(listeners, name) } - listeners = append(listeners, name) - return true }) - tcpAliases := map[string]string{} - c.State.TCPListeners.Range(func(key interface{}, val interface{}) bool { + tcpAliases := map[string]interface{}{} + c.State.AliasListeners.Range(func(key interface{}, val interface{}) bool { tcpAlias := key.(string) - aliasAddress := val.(string) + aliasHolder := val.(*AliasHolder) for _, v := range listeners { - if v == aliasAddress { - tcpAliases[tcpAlias] = aliasAddress + for _, server := range aliasHolder.Balancer.Servers() { + serverAddr, err := base64.StdEncoding.DecodeString(server.Host) + if err != nil { + log.Println("Error decoding server host:", err) + continue + } + + aliasAddress := string(serverAddr) + + if v == aliasAddress { + tcpAliases[tcpAlias] = aliasAddress + } } } return true }) - listenerParts := map[string]string{} - c.State.Listeners.Range(func(key interface{}, val interface{}) bool { - var tcpListener *net.TCPAddr - unixListener, ok := key.(*net.UnixAddr) - if !ok { - tcpListener = key.(*net.TCPAddr) - } - - var addr string - if unixListener != nil { - addr = unixListener.String() - } else { - addr = tcpListener.String() - } + listenerParts := map[string]interface{}{} + c.State.TCPListeners.Range(func(key interface{}, val interface{}) bool { + tcpAlias := key.(string) + aliasHolder := val.(*TCPHolder) for _, v := range listeners { - if v == addr { - listenerParts[addr] = addr + for _, server := range aliasHolder.Balancer.Servers() { + serverAddr, err := base64.StdEncoding.DecodeString(server.Host) + if err != nil { + log.Println("Error decoding server host:", err) + continue + } + + aliasAddress := string(serverAddr) + + if v == aliasAddress { + listenerParts[tcpAlias] = aliasAddress + } } } return true }) - httpListeners := map[string]string{} + httpListeners := map[string]interface{}{} c.State.HTTPListeners.Range(func(key interface{}, val interface{}) bool { httpListener := key.(string) - aliasAddress := val.(*ProxyHolder) + aliasAddress := val.(*HTTPHolder) - for _, v := range listeners { - if v == aliasAddress.ProxyTo { - httpListeners[httpListener] = aliasAddress.ProxyTo + listenerHandlers := []string{} + aliasAddress.SSHConnections.Range(func(key interface{}, val interface{}) bool { + aliasAddr := key.(string) + + for _, v := range listeners { + if v == aliasAddr { + listenerHandlers = append(listenerHandlers, aliasAddr) + } } + return true + }) + + if len(listenerHandlers) > 0 { + httpListeners[httpListener] = listenerHandlers } return true @@ -327,13 +278,24 @@ func (c *WebConsole) HandleClients(hostname string, g *gin.Context) { routeListeners["listeners"] = listenerParts routeListeners["httpListeners"] = httpListeners - clients[clientName.String()] = map[string]interface{}{ - "remoteAddr": sshConn.SSHConn.RemoteAddr().String(), - "user": sshConn.SSHConn.User(), - "version": string(sshConn.SSHConn.ClientVersion()), - "session": sshConn.SSHConn.SessionID(), - "listeners": listeners, - "routeListeners": routeListeners, + pubKey := "" + pubKeyFingerprint := "" + if sshConn.SSHConn.Permissions != nil { + if _, ok := sshConn.SSHConn.Permissions.Extensions["pubKey"]; ok { + pubKey = sshConn.SSHConn.Permissions.Extensions["pubKey"] + pubKeyFingerprint = sshConn.SSHConn.Permissions.Extensions["pubKeyFingerprint"] + } + } + + clients[clientName] = map[string]interface{}{ + "remoteAddr": sshConn.SSHConn.RemoteAddr().String(), + "user": sshConn.SSHConn.User(), + "version": string(sshConn.SSHConn.ClientVersion()), + "session": sshConn.SSHConn.SessionID(), + "pubKey": pubKey, + "pubKeyFingerprint": pubKeyFingerprint, + "listeners": listeners, + "routeListeners": routeListeners, } return true @@ -344,65 +306,31 @@ func (c *WebConsole) HandleClients(hostname string, g *gin.Context) { g.JSON(http.StatusOK, data) } -// HandleAllRoutes handles returning all connected routes (tunnels) -func (c *WebConsole) HandleAllRoutes(hostname string, g *gin.Context) { - data := map[string]interface{}{ - "status": true, - } - - tcpAliases := []string{} - c.State.TCPListeners.Range(func(key interface{}, val interface{}) bool { - tcpAlias := key.(string) - tcpAliases = append(tcpAliases, tcpAlias) - - return true - }) +// RouteToken returns the route token for a specific route. +func (c *WebConsole) RouteToken(route string) (string, bool) { + token, ok := c.RouteTokens.Load(route) + routeToken := "" - listeners := []string{} - c.State.Listeners.Range(func(key interface{}, val interface{}) bool { - var tcpListener *net.TCPAddr - unixListener, ok := key.(*net.UnixAddr) - if !ok { - tcpListener = key.(*net.TCPAddr) - } - - if unixListener != nil { - listeners = append(listeners, unixListener.String()) - } else { - listeners = append(listeners, tcpListener.String()) - } - - return true - }) - - httpListeners := []string{} - c.State.HTTPListeners.Range(func(key interface{}, val interface{}) bool { - httpListener := key.(string) - httpListeners = append(httpListeners, httpListener) - - return true - }) - - data["tcpAliases"] = tcpAliases - data["listeners"] = listeners - data["httpListeners"] = httpListeners + if ok { + routeToken = token.(string) + } - g.JSON(http.StatusOK, data) + return routeToken, ok } -// RouteExists check if a route exists +// RouteExists check if a route token exists. func (c *WebConsole) RouteExists(route string) bool { - _, ok := c.Clients.Load(route) + _, ok := c.RouteToken(route) return ok } -// AddRoute adds a route to the console +// AddRoute adds a route token to the console. func (c *WebConsole) AddRoute(route string, token string) { c.Clients.LoadOrStore(route, []*WebClient{}) c.RouteTokens.Store(route, token) } -// RemoveRoute adds a route to the console +// RemoveRoute removes a route token from the console. func (c *WebConsole) RemoveRoute(route string) { data, ok := c.Clients.Load(route) @@ -424,7 +352,7 @@ func (c *WebConsole) RemoveRoute(route string) { c.RouteTokens.Delete(route) } -// AddClient adds a client to the console +// AddClient adds a client to the console route. func (c *WebConsole) AddClient(route string, w *WebClient) { data, ok := c.Clients.Load(route) @@ -443,7 +371,7 @@ func (c *WebConsole) AddClient(route string, w *WebClient) { c.Clients.Store(route, clients) } -// RemoveClient removes a client from the console +// RemoveClient removes a client from the console route. func (c *WebConsole) RemoveClient(route string, w *WebClient) { data, ok := c.Clients.Load(route) @@ -473,7 +401,7 @@ func (c *WebConsole) RemoveClient(route string, w *WebClient) { } } -// BroadcastRoute sends a message to all clients on a route +// BroadcastRoute sends a message to all clients on a route. func (c *WebConsole) BroadcastRoute(route string, message []byte) { data, ok := c.Clients.Load(route) @@ -492,7 +420,7 @@ func (c *WebConsole) BroadcastRoute(route string, message []byte) { } } -// Handle is the only place socket reads and writes happen +// Handle is the only place socket reads and writes happen. func (c *WebClient) Handle() { defer func() { c.Conn.Close() @@ -517,6 +445,6 @@ func (c *WebClient) Handle() { err := c.Conn.WriteMessage(websocket.CloseMessage, []byte{}) if err != nil { - log.Println("error writing to websocket:", err) + log.Println("Error writing to websocket:", err) } } diff --git a/utils/state.go b/utils/state.go new file mode 100644 index 0000000..2d2c969 --- /dev/null +++ b/utils/state.go @@ -0,0 +1,177 @@ +package utils + +import ( + "encoding/base64" + "fmt" + "io" + "log" + "net" + "sync" + "time" + + "github.com/antoniomika/oxy/forward" + "github.com/antoniomika/oxy/roundrobin" + "github.com/jpillora/ipfilter" + "github.com/spf13/viper" +) + +// ListenerType represents any listener sish supports. +type ListenerType int + +const ( + // AliasListener represents a tcp alias. + AliasListener ListenerType = iota + + // HTTPListener represents a HTTP proxy. + HTTPListener + + // TCPListener represents a generic tcp listener. + TCPListener + + // ProcessListener represents a process specific listener. + ProcessListener +) + +// LogWriter represents a writer that is used for writing logs in multiple locations. +type LogWriter struct { + TimeFmt string + MultiWriter io.Writer +} + +// Write implements the write function for the LogWriter. It will add a time in a +// specific format to logs. +func (w LogWriter) Write(bytes []byte) (int, error) { + return fmt.Fprintf(w.MultiWriter, "%v | %s", time.Now().Format(w.TimeFmt), string(bytes)) +} + +// ListenerHolder represents a generic listener. +type ListenerHolder struct { + net.Listener + ListenAddr string + Type ListenerType + SSHConn *SSHConnection +} + +// HTTPHolder holds proxy and connection info. +// SSHConnections is a map[string]*SSHConnection. +type HTTPHolder struct { + HTTPHost string + Scheme string + SSHConnections *sync.Map + Forward *forward.Forwarder + Balancer *roundrobin.RoundRobin +} + +// AliasHolder holds alias and connection info. +// SSHConnections is a map[string]*SSHConnection. +type AliasHolder struct { + AliasHost string + SSHConnections *sync.Map + Balancer *roundrobin.RoundRobin +} + +// TCPHolder holds proxy and connection info. +// SSHConnections is a map[string]*SSHConnection. +type TCPHolder struct { + TCPHost string + Listener net.Listener + SSHConnections *sync.Map + Balancer *roundrobin.RoundRobin +} + +// Handle will copy connections from one handler to a roundrobin server. +func (tH *TCPHolder) Handle(state *State) { + for { + cl, err := tH.Listener.Accept() + if err != nil { + break + } + + clientRemote, _, err := net.SplitHostPort(cl.RemoteAddr().String()) + + if err != nil || state.IPFilter.Blocked(clientRemote) { + cl.Close() + continue + } + + connectionLocation, err := tH.Balancer.NextServer() + if err != nil { + log.Println("Unable to load connection location:", err) + cl.Close() + continue + } + + host, err := base64.StdEncoding.DecodeString(connectionLocation.Host) + if err != nil { + log.Println("Unable to decode connection location:", err) + cl.Close() + continue + } + + hostAddr := string(host) + + logLine := fmt.Sprintf("Accepted connection from %s -> %s", cl.RemoteAddr().String(), cl.LocalAddr().String()) + log.Println(logLine) + + if viper.GetBool("log-to-client") { + tH.SSHConnections.Range(func(key, val interface{}) bool { + sshConn := val.(*SSHConnection) + + sshConn.Listeners.Range(func(key, val interface{}) bool { + listenerAddr := key.(string) + + if listenerAddr == hostAddr { + sshConn.SendMessage(logLine, true) + + return false + } + + return true + }) + + return true + }) + } + + conn, err := net.Dial("unix", hostAddr) + if err != nil { + log.Println("Error connecting to tcp balancer:", err) + cl.Close() + continue + } + + go CopyBoth(conn, cl) + } +} + +// State handles overall state. It retains mutexed maps for various +// datastructures and shared objects. +// SSHConnections is a map[string]*SSHConnection. +// Listeners is a map[string]net.Listener. +// HTTPListeners is a map[string]HTTPHolder. +// AliasListeners is a map[string]AliasHolder. +// TCPListeners is a map[string]TCPHolder. +type State struct { + Console *WebConsole + SSHConnections *sync.Map + Listeners *sync.Map + HTTPListeners *sync.Map + AliasListeners *sync.Map + TCPListeners *sync.Map + IPFilter *ipfilter.IPFilter + LogWriter io.Writer +} + +// NewState returns a new State struct. +func NewState() *State { + return &State{ + SSHConnections: &sync.Map{}, + Listeners: &sync.Map{}, + HTTPListeners: &sync.Map{}, + AliasListeners: &sync.Map{}, + TCPListeners: &sync.Map{}, + IPFilter: Filter, + Console: NewWebConsole(), + LogWriter: multiWriter, + } +} diff --git a/utils/utils.go b/utils/utils.go new file mode 100644 index 0000000..d5491d1 --- /dev/null +++ b/utils/utils.go @@ -0,0 +1,621 @@ +// Package utils implements utilities used across different +// areas of the sish application. There are utility functions +// that help with overall state management and are core to the application. +package utils + +import ( + "bytes" + "crypto/ed25519" + "crypto/rand" + "encoding/pem" + "fmt" + "io" + "io/ioutil" + "log" + mathrand "math/rand" + "net" + "os" + "os/signal" + "path/filepath" + "strconv" + "strings" + "sync" + "time" + + "github.com/ScaleFT/sshkeys" + "github.com/fsnotify/fsnotify" + "github.com/jpillora/ipfilter" + "github.com/logrusorgru/aurora" + "github.com/mikesmitty/edkey" + "github.com/spf13/viper" + "golang.org/x/crypto/ssh" +) + +const ( + // sishDNSPrefix is the prefix used for DNS TXT records. + sishDNSPrefix = "sish=" +) + +var ( + // Filter is the IPFilter used to block connections. + Filter *ipfilter.IPFilter + + // certHolder is a slice of publickeys for auth. + certHolder = make([]ssh.PublicKey, 0) + + // holderLock is the mutex used to update the certHolder slice. + holderLock = sync.Mutex{} + + // bannedSubdomainList is a list of subdomains that cannot be bound. + bannedSubdomainList = []string{""} + + // multiWriter is the writer that can be used for writing to multiple locations. + multiWriter io.Writer +) + +// Setup main utils. This initializes, whitelists, blacklists, +// and log writers. +func Setup(logWriter io.Writer) { + multiWriter = logWriter + + upperList := func(stringList string) []string { + list := strings.FieldsFunc(stringList, CommaSplitFields) + for k, v := range list { + list[k] = strings.ToUpper(v) + } + + return list + } + + whitelistedCountriesList := upperList(viper.GetString("whitelisted-countries")) + whitelistedIPList := strings.FieldsFunc(viper.GetString("whitelisted-ips"), CommaSplitFields) + + ipfilterOpts := ipfilter.Options{ + BlockedCountries: upperList(viper.GetString("banned-countries")), + AllowedCountries: whitelistedCountriesList, + BlockedIPs: strings.FieldsFunc(viper.GetString("banned-ips"), CommaSplitFields), + AllowedIPs: whitelistedIPList, + BlockByDefault: len(whitelistedIPList) > 0 || len(whitelistedCountriesList) > 0, + } + + if viper.GetBool("geodb") { + Filter = ipfilter.NewLazy(ipfilterOpts) + } else { + Filter = ipfilter.NewNoDB(ipfilterOpts) + } + + bannedSubdomainList = append(bannedSubdomainList, strings.FieldsFunc(viper.GetString("banned-subdomains"), CommaSplitFields)...) + for k, v := range bannedSubdomainList { + bannedSubdomainList[k] = strings.ToLower(strings.TrimSpace(v) + "." + viper.GetString("domain")) + } +} + +// CommaSplitFields is a function used by strings.FieldsFunc to split around commas. +func CommaSplitFields(c rune) bool { + return c == ',' +} + +// GetRandomPortInRange returns a random port in the provided range. +// The port range is a comma separated list of ranges or ports. +func GetRandomPortInRange(portRange string) uint32 { + var bindPort uint32 + + ranges := strings.Split(strings.TrimSpace(portRange), ",") + possible := [][]uint64{} + for _, r := range ranges { + ends := strings.Split(strings.TrimSpace(r), "-") + + if len(ends) == 1 { + ui, err := strconv.ParseUint(ends[0], 0, 64) + if err != nil { + return 0 + } + + possible = append(possible, []uint64{uint64(ui)}) + } else if len(ends) == 2 { + ui1, err := strconv.ParseUint(ends[0], 0, 64) + if err != nil { + return 0 + } + + ui2, err := strconv.ParseUint(ends[1], 0, 64) + if err != nil { + return 0 + } + + possible = append(possible, []uint64{uint64(ui1), uint64(ui2)}) + } + } + + mathrand.Seed(time.Now().UnixNano()) + locHolder := mathrand.Intn(len(possible)) + + if len(possible[locHolder]) == 1 { + bindPort = uint32(possible[locHolder][0]) + } else if len(possible[locHolder]) == 2 { + bindPort = uint32(mathrand.Intn(int(possible[locHolder][1]-possible[locHolder][0])) + int(possible[locHolder][0])) + } + + ln, err := net.Listen("tcp", fmt.Sprintf(":%d", bindPort)) + if err != nil { + return GetRandomPortInRange(portRange) + } + + ln.Close() + + return bindPort +} + +// CheckPort verifies if a port exists within the port range. +// It will return 0 and an error if not (0 allows the kernel to select) +// the port. +func CheckPort(port uint32, portRanges string) (uint32, error) { + ranges := strings.Split(strings.TrimSpace(portRanges), ",") + checks := false + for _, r := range ranges { + ends := strings.Split(strings.TrimSpace(r), "-") + + if len(ends) == 1 { + ui, err := strconv.ParseUint(ends[0], 0, 64) + if err != nil { + return 0, err + } + + if uint64(ui) == uint64(port) { + checks = true + continue + } + } else if len(ends) == 2 { + ui1, err := strconv.ParseUint(ends[0], 0, 64) + if err != nil { + return 0, err + } + + ui2, err := strconv.ParseUint(ends[1], 0, 64) + if err != nil { + return 0, err + } + + if uint64(port) >= ui1 && uint64(port) <= ui2 { + checks = true + continue + } + } + } + + if checks { + return port, nil + } + + return 0, fmt.Errorf("not a safe port") +} + +// WatchCerts watches ssh keys for changes and will load them. +func WatchCerts() { + loadCerts() + watcher, err := fsnotify.NewWatcher() + if err != nil { + log.Fatal(err) + } + + go func() { + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + go func() { + for range c { + watcher.Close() + os.Exit(0) + } + }() + + for { + select { + case _, ok := <-watcher.Events: + if !ok { + return + } + loadCerts() + case _, ok := <-watcher.Errors: + if !ok { + return + } + } + } + }() + + err = watcher.Add(viper.GetString("authentication-keys-directory")) + if err != nil { + log.Fatal(err) + } +} + +// loadCerts loads public keys from the keys directory into a slice that is used +// authenticating a user. +func loadCerts() { + tmpCertHolder := make([]ssh.PublicKey, 0) + + files, err := ioutil.ReadDir(viper.GetString("authentication-keys-directory")) + if err != nil { + log.Fatal(err) + } + + parseKey := func(keyBytes []byte, fileInfo os.FileInfo) { + keyHandle := func(keyBytes []byte, fileInfo os.FileInfo) []byte { + key, _, _, rest, e := ssh.ParseAuthorizedKey(keyBytes) + if e != nil { + log.Printf("Can't load file %s as public key: %s\n", fileInfo.Name(), e) + } + + if key != nil { + tmpCertHolder = append(tmpCertHolder, key) + } + return rest + } + + for ok := true; ok; ok = len(keyBytes) > 0 { + keyBytes = keyHandle(keyBytes, fileInfo) + } + } + + for _, f := range files { + i, e := ioutil.ReadFile(filepath.Join(viper.GetString("authentication-keys-directory"), f.Name())) + if e == nil && len(i) > 0 { + parseKey(i, f) + } + } + + holderLock.Lock() + defer holderLock.Unlock() + certHolder = tmpCertHolder +} + +// GetSSHConfig Returns an SSH config for the ssh muxer. +// It handles auth and storing user connection information. +func GetSSHConfig() *ssh.ServerConfig { + sshConfig := &ssh.ServerConfig{ + NoClientAuth: !viper.GetBool("authentication"), + PasswordCallback: func(c ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) { + log.Printf("Login attempt: %s, user %s", c.RemoteAddr(), c.User()) + + if string(password) == viper.GetString("authentication-password") && viper.GetString("authentication-password") != "" { + return nil, nil + } + + return nil, fmt.Errorf("password doesn't match") + }, + PublicKeyCallback: func(c ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) { + log.Printf("Login attempt: %s, user %s key: %s", c.RemoteAddr(), c.User(), string(ssh.MarshalAuthorizedKey(key))) + + holderLock.Lock() + defer holderLock.Unlock() + for _, i := range certHolder { + if bytes.Equal(key.Marshal(), i.Marshal()) { + permssionsData := &ssh.Permissions{ + Extensions: map[string]string{ + "pubKey": string(key.Marshal()), + "pubKeyFingerprint": ssh.FingerprintSHA256(key), + }, + } + + return permssionsData, nil + } + } + + return nil, fmt.Errorf("public key doesn't match") + }, + } + sshConfig.AddHostKey(loadPrivateKey(viper.GetString("private-key-passphrase"))) + return sshConfig +} + +// generatePrivateKey creates a new ed25519 private key to be used by the +// the SSH server as the host key. +func generatePrivateKey(passphrase string) []byte { + _, pk, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + log.Fatal(err) + } + + log.Println("Generated ED25519 Keypair") + + // In an effort to guarantee that keys can still be loaded by OpenSSH + // we adopt branching logic here for passphrase encrypted keys. + // I wrote a module that handled both, but ultimately decided this + // is likely cleaner and less specialized. + var pemData []byte + if passphrase != "" { + pemData, err = sshkeys.Marshal(pk, &sshkeys.MarshalOptions{ + Passphrase: []byte(passphrase), + Format: sshkeys.FormatOpenSSHv1, + }) + + if err != nil { + log.Fatal(err) + } + } else { + pemBlock := &pem.Block{ + Type: "OPENSSH PRIVATE KEY", + Bytes: edkey.MarshalED25519PrivateKey(pk), + } + + pemData = pem.EncodeToMemory(pemBlock) + } + + err = ioutil.WriteFile(viper.GetString("private-key-location"), pemData, 0600) + if err != nil { + log.Println("Error writing to file:", err) + } + + return pemData +} + +// ParsePrivateKey parses the PrivateKey into a ssh.Signer and +// let's it be used by the SSH server. +func loadPrivateKey(passphrase string) ssh.Signer { + var signer ssh.Signer + + pk, err := ioutil.ReadFile(viper.GetString("private-key-location")) + if err != nil { + log.Println("Error loading private key, generating a new one:", err) + pk = generatePrivateKey(passphrase) + } + + if passphrase != "" { + signer, err = ssh.ParsePrivateKeyWithPassphrase(pk, []byte(passphrase)) + if err != nil { + log.Fatal(err) + } + } else { + signer, err = ssh.ParsePrivateKey(pk) + if err != nil { + log.Fatal(err) + } + } + + return signer +} + +// inList is used to scan whether or not something exists +// in a slice of data. +func inList(host string, bannedList []string) bool { + for _, v := range bannedList { + if strings.TrimSpace(v) == host { + return true + } + } + + return false +} + +// verifyDNS will verify that a specific domain/subdomain combo matches +// the specific TXT entry that exists for the domain. It will check that the +// publickey used for auth is at least included in the TXT records for the domain. +func verifyDNS(addr string, sshConn *SSHConnection) (bool, string, error) { + if !viper.GetBool("verify-dns") || sshConn.SSHConn.Permissions == nil { + return false, "", nil + } + + if _, ok := sshConn.SSHConn.Permissions.Extensions["pubKeyFingerprint"]; !ok { + return false, "", nil + } + + records, err := net.LookupTXT(addr) + + for _, v := range records { + if strings.HasPrefix(v, sishDNSPrefix) { + dnsPubKeyFingerprint := strings.TrimSpace(strings.TrimPrefix(v, sishDNSPrefix)) + + match := sshConn.SSHConn.Permissions.Extensions["pubKeyFingerprint"] == dnsPubKeyFingerprint + if match { + return match, dnsPubKeyFingerprint, err + } + } + } + + return false, "", nil +} + +// GetOpenPort returns open ports that can be bound. It verifies the host to +// bind the port to and attempts to listen to the port to ensure it is open. +// If load balancing is enabled, it will return the port if used. +func GetOpenPort(addr string, port uint32, state *State, sshConn *SSHConnection) (string, uint32, *TCPHolder) { + getUnusedPort := func() (string, uint32, *TCPHolder) { + var tH *TCPHolder + + first := true + bindPort := port + bindAddr := addr + listenAddr := "" + + if bindAddr == "localhost" && viper.GetBool("localhost-as-all") { + bindAddr = "" + } + + reportUnavailable := func(unavailable bool) { + if first && unavailable { + sshConn.SendMessage(aurora.Sprintf("The TCP port %s is unavaible. Assigning a random port.", aurora.Red(listenAddr)), true) + } + } + + checkPort := func(checkerAddr string, checkerPort uint32) bool { + listenAddr = fmt.Sprintf("%s:%d", bindAddr, bindPort) + checkedPort, err := CheckPort(checkerPort, viper.GetString("port-bind-range")) + if err == nil && !viper.GetBool("tcp-load-balancer") { + ln, listenErr := net.Listen("tcp", fmt.Sprintf(":%d", port)) + if listenErr != nil { + err = listenErr + } else { + ln.Close() + } + } + + if viper.GetBool("bind-random-ports") || !first || err != nil { + reportUnavailable(true) + + if viper.GetString("port-bind-range") != "" { + bindPort = GetRandomPortInRange(viper.GetString("port-bind-range")) + } else { + bindPort = 0 + } + } else { + bindPort = checkedPort + } + + listenAddr = fmt.Sprintf("%s:%d", bindAddr, bindPort) + holder, ok := state.TCPListeners.Load(listenAddr) + if ok && viper.GetBool("tcp-load-balancer") { + tH = holder.(*TCPHolder) + ok = false + } + + reportUnavailable(ok) + + first = false + return ok + } + + for checkPort(bindAddr, bindPort) { + } + + return listenAddr, bindPort, tH + } + + return getUnusedPort() +} + +// GetOpenHost returns an open host or a random host if that one is unavailable. +// If load balancing is enabled, it will return the requested domain. +func GetOpenHost(addr string, state *State, sshConn *SSHConnection) (string, *HTTPHolder) { + dnsMatch, _, err := verifyDNS(addr, sshConn) + if err != nil && viper.GetBool("debug") { + log.Println("Error looking up txt records for domain:", addr) + } + + getUnusedHost := func() (string, *HTTPHolder) { + var pH *HTTPHolder + + first := true + hostExtension := "" + + if viper.GetBool("append-user-to-subdomain") { + hostExtension = viper.GetString("append-user-to-subdomain-separator") + sshConn.SSHConn.User() + } + + proposedHost := addr + hostExtension + "." + viper.GetString("domain") + domainParts := strings.Join(strings.Split(addr, ".")[1:], ".") + if dnsMatch || viper.GetBool("bind-any-host") || inList(domainParts, strings.FieldsFunc(viper.GetString("bind-hosts"), CommaSplitFields)) { + proposedHost = addr + } + + host := strings.ToLower(proposedHost) + + getRandomHost := func() string { + return strings.ToLower(RandStringBytesMaskImprSrc(viper.GetInt("bind-random-subdomains-length")) + "." + viper.GetString("domain")) + } + + reportUnavailable := func(unavailable bool) { + if first && unavailable { + sshConn.SendMessage(aurora.Sprintf("The subdomain %s is unavailable. Assigning a random subdomain.", aurora.Red(host)), true) + } + } + + checkHost := func(checkHost string) bool { + if viper.GetBool("bind-random-subdomains") || !first || inList(host, bannedSubdomainList) { + reportUnavailable(true) + host = getRandomHost() + } + + holder, ok := state.HTTPListeners.Load(host) + if ok && viper.GetBool("http-load-balancer") { + pH = holder.(*HTTPHolder) + ok = false + } + + reportUnavailable(ok) + + first = false + return ok + } + + for checkHost(host) { + } + + return host, pH + } + + return getUnusedHost() +} + +// GetOpenAlias returns open aliases or a random one if it is not enabled. +// If load balancing is enabled, it will return the requested alias. +func GetOpenAlias(addr string, port string, state *State, sshConn *SSHConnection) (string, *AliasHolder) { + getUnusedAlias := func() (string, *AliasHolder) { + var aH *AliasHolder + + first := true + alias := fmt.Sprintf("%s:%s", strings.ToLower(addr), port) + + getRandomAlias := func() string { + return fmt.Sprintf("%s:%s", strings.ToLower(RandStringBytesMaskImprSrc(viper.GetInt("bind-random-subdomains-length"))), port) + } + + reportUnavailable := func(unavailable bool) { + if first && unavailable { + sshConn.SendMessage(aurora.Sprintf("The alias %s is unavaible. Assigning a random alias.", aurora.Red(alias)), true) + } + } + + checkAlias := func(checkAlias string) bool { + if viper.GetBool("bind-random-subdomains") || !first || inList(alias, bannedSubdomainList) { + reportUnavailable(true) + alias = getRandomAlias() + } + + holder, ok := state.AliasListeners.Load(alias) + if ok && viper.GetBool("alias-load-balancer") { + aH = holder.(*AliasHolder) + ok = false + } + + reportUnavailable(ok) + + first = false + return ok + } + + for checkAlias(alias) { + } + + return alias, aH + } + + return getUnusedAlias() +} + +// RandStringBytesMaskImprSrc creates a random string of length n +// https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-golang +func RandStringBytesMaskImprSrc(n int) string { + const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + const ( + letterIdxBits = 6 // 6 bits to represent a letter index + letterIdxMask = 1<= 0; { + if remain == 0 { + cache, remain = src.Int63(), letterIdxMax + } + if idx := int(cache & letterIdxMask); idx < len(letterBytes) { + b[i] = letterBytes[idx] + i-- + } + cache >>= letterIdxBits + remain-- + } + + return string(b) +}