├── .github ├── dependabot.yml └── workflows │ └── build-and-push-docker.yml ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── Dockerfile ├── Readme.md ├── go.mod ├── go.sum ├── idle_proxy.go ├── main.go └── wait_for_port.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" 9 | directory: "/" 10 | schedule: 11 | interval: "monthly" 12 | - package-ecosystem: "docker" 13 | directory: "/" 14 | schedule: 15 | interval: "monthly" 16 | - package-ecosystem: "github-actions" 17 | directory: "/" 18 | schedule: 19 | interval: "weekly" 20 | -------------------------------------------------------------------------------- /.github/workflows/build-and-push-docker.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docker image 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | tags: 8 | - '*' 9 | 10 | jobs: 11 | push_to_registry: 12 | name: Push Docker image to Docker Hub 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Check out the repo 16 | uses: actions/checkout@v3 17 | 18 | - name: Log in to Docker Hub 19 | uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a 20 | with: 21 | username: ${{ secrets.DOCKER_USERNAME }} 22 | password: ${{ secrets.DOCKER_PASSWORD }} 23 | 24 | - name: Extract metadata (tags, labels) for Docker 25 | id: meta 26 | uses: docker/metadata-action@507c2f2dc502c992ad446e3d7a5dfbe311567a96 27 | with: 28 | images: tiesv/tired-proxy 29 | 30 | - name: Build and push Docker image 31 | uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671 32 | with: 33 | context: . 34 | push: true 35 | tags: ${{ steps.meta.outputs.tags }} 36 | labels: ${{ steps.meta.outputs.labels }} 37 | build-args: VERSION=${{ github.ref_name }}${{ (github.ref_type == 'branch' && format('-{0}', github.sha)) || '' }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/go,visualstudiocode 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=go,visualstudiocode 3 | 4 | ### Go ### 5 | # If you prefer the allow list template instead of the deny list, see community template: 6 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 7 | # 8 | # Binaries for programs and plugins 9 | *.exe 10 | *.exe~ 11 | *.dll 12 | *.so 13 | *.dylib 14 | 15 | # Test binary, built with `go test -c` 16 | *.test 17 | 18 | # Output of the go coverage tool, specifically when used with LiteIDE 19 | *.out 20 | 21 | # Dependency directories (remove the comment below to include it) 22 | # vendor/ 23 | 24 | # Go workspace file 25 | go.work 26 | 27 | ### VisualStudioCode ### 28 | .vscode/* 29 | !.vscode/settings.json 30 | !.vscode/tasks.json 31 | !.vscode/launch.json 32 | !.vscode/extensions.json 33 | !.vscode/*.code-snippets 34 | 35 | # Local History for Visual Studio Code 36 | .history/ 37 | 38 | # Built Visual Studio Code Extensions 39 | *.vsix 40 | 41 | ### VisualStudioCode Patch ### 42 | # Ignore all local history of files 43 | .history 44 | .ionide 45 | 46 | # End of https://www.toptal.com/developers/gitignore/api/go,visualstudiocode 47 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch Package", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "auto", 12 | "program": "${fileDirname}" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "Debugf", 4 | "Debugln", 5 | "Infof", 6 | "inuse", 7 | "logrus", 8 | "shellwords", 9 | "stdlog" 10 | ] 11 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | FROM golang:1.19-alpine as build 3 | 4 | WORKDIR /app 5 | 6 | COPY go.mod ./ 7 | COPY go.sum ./ 8 | 9 | RUN go mod download 10 | 11 | COPY *.go ./ 12 | 13 | ARG VERSION 14 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-X main.Version=${VERSION}" -o /tired-proxy 15 | 16 | 17 | FROM scratch 18 | 19 | COPY --from=build /tired-proxy / 20 | 21 | CMD [ "/tired-proxy" ] -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Tired proxy 2 | 3 | [![Docker image size](https://img.shields.io/docker/image-size/tiesv/tired-proxy?sort=date "Docker image size") ](https://hub.docker.com/r/tiesv/tired-proxy) 4 | 5 | _Altered version of superfly/tired-proxy_ 6 | 7 | An HTTP proxy that exits after a settable idle time. Useful for creating _scale to zero_ Docker containers. Includes the feature to wait for the port of the origin server to be in use. 8 | 9 | ## Usage 10 | Inside your container script, start your web application and fork the process with Tired proxy. 11 | ```bash 12 | ./my-web-server & /tired-proxy --origin=http://localhost:3000 13 | ``` 14 | The proxy is now served at port 8080 15 | 16 | ### CLI options 17 | 18 | |Option|type|default|description| 19 | |---|---|---|---| 20 | |`idle-time`|int|60|idle time in seconds after which the application shuts down, if no requests where received| 21 | |`origin`|string|http://localhost|the origin host to which the requests are forwarded| 22 | |`port`|string|8080|port at which the proxy server listens for requests| 23 | |`wait-for-port`|int|0|maximum time in seconds to wait before the origin servers port is in use before starting the proxy server| 24 | |`verbose`|bool|false|verbose logging output| 25 | 26 | ## Get the docker image 27 | ```bash 28 | docker pull tiesv/tired-proxy:latest 29 | ``` -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module lubien/tired-proxy 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/sirupsen/logrus v1.9.0 7 | github.com/x-cray/logrus-prefixed-formatter v0.5.2 8 | ) 9 | 10 | require ( 11 | github.com/mattn/go-colorable v0.1.13 // indirect 12 | github.com/mattn/go-isatty v0.0.16 // indirect 13 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect 14 | github.com/onsi/ginkgo v1.16.5 // indirect 15 | github.com/onsi/gomega v1.26.0 // indirect 16 | golang.org/x/crypto v0.5.0 // indirect 17 | golang.org/x/sys v0.4.0 // indirect 18 | golang.org/x/term v0.4.0 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 2 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 3 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 8 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 9 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 10 | github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 11 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 12 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 13 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 14 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 15 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 16 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 17 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 18 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 19 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 20 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 21 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 22 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 23 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 24 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 25 | github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 26 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 27 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 28 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 29 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 30 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 31 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 32 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 33 | github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= 34 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 35 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= 36 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 37 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 38 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 39 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 40 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 41 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 42 | github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= 43 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 44 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 45 | github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= 46 | github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU= 47 | github.com/onsi/ginkgo/v2 v2.1.6/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= 48 | github.com/onsi/ginkgo/v2 v2.3.0/go.mod h1:Eew0uilEqZmIEZr8JrvYlvOM7Rr6xzTmMV8AyFNU9d0= 49 | github.com/onsi/ginkgo/v2 v2.4.0/go.mod h1:iHkDK1fKGcBoEHT5W7YBq4RFWaQulw+caOMkAt4OrFo= 50 | github.com/onsi/ginkgo/v2 v2.5.0/go.mod h1:Luc4sArBICYCS8THh8v3i3i5CuSZO+RaQRaJoeNwomw= 51 | github.com/onsi/ginkgo/v2 v2.7.0/go.mod h1:yjiuMwPokqY1XauOgju45q3sJt6VzQ/Fict1LFVcsAo= 52 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 53 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 54 | github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= 55 | github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= 56 | github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo= 57 | github.com/onsi/gomega v1.21.1/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc= 58 | github.com/onsi/gomega v1.22.1/go.mod h1:x6n7VNe4hw0vkyYUM4mjIXx3JbLiPaBPNgB7PRQ1tuM= 59 | github.com/onsi/gomega v1.24.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg= 60 | github.com/onsi/gomega v1.24.1/go.mod h1:3AOiACssS3/MajrniINInwbfOOtfZvplPzuRSmvt1jM= 61 | github.com/onsi/gomega v1.26.0 h1:03cDLK28U6hWvCAns6NeydX3zIm4SF3ci69ulidS32Q= 62 | github.com/onsi/gomega v1.26.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM= 63 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 64 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 65 | github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= 66 | github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 67 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 68 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 69 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 70 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 71 | github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg= 72 | github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE= 73 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 74 | github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 75 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 76 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 77 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 78 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 79 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 80 | golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= 81 | golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= 82 | golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= 83 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 84 | golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= 85 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 86 | golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= 87 | golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 88 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 89 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 90 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 91 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 92 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 93 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 94 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= 95 | golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 96 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 97 | golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 98 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 99 | golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= 100 | golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= 101 | golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= 102 | golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= 103 | golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= 104 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 105 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 106 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 107 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 108 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 109 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 110 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 111 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 112 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 113 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 114 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 115 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 116 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 117 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 118 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 119 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 120 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 121 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 122 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 123 | golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 124 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 125 | golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 126 | golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 127 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 128 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 129 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 130 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 131 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 132 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 133 | golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 134 | golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= 135 | golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 136 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 137 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 138 | golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 139 | golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= 140 | golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= 141 | golang.org/x/term v0.4.0 h1:O7UWfv5+A2qiuulQk30kVinPoMtoIPeVaKLEgLpVkvg= 142 | golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= 143 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 144 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 145 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 146 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 147 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 148 | golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 149 | golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= 150 | golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 151 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 152 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 153 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 154 | golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= 155 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 156 | golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= 157 | golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= 158 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 159 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 160 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 161 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 162 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 163 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 164 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 165 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 166 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 167 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 168 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 169 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 170 | google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 171 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 172 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 173 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 174 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 175 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 176 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 177 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 178 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 179 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 180 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 181 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 182 | -------------------------------------------------------------------------------- /idle_proxy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | stdlog "log" 8 | "net/http" 9 | "net/http/httputil" 10 | "net/url" 11 | "time" 12 | 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | type IdleProxy struct { 17 | idleTime time.Duration 18 | timer *time.Timer 19 | server *http.Server 20 | proxy *httputil.ReverseProxy 21 | chanFinish chan error 22 | } 23 | 24 | func StartIdleProxy(ctx context.Context, originUrl *url.URL, port string, idleTime time.Duration) *IdleProxy { 25 | log.Infof("Setup proxy server for origin %s", originUrl) 26 | httpErrorLogWriter := log.WriterLevel(logrus.ErrorLevel) 27 | idleProxy := &IdleProxy{ 28 | idleTime: idleTime, 29 | timer: time.NewTimer(idleTime), 30 | server: &http.Server{Addr: fmt.Sprintf(":%s", port), ErrorLog: stdlog.New(httpErrorLogWriter, "", 0)}, 31 | proxy: httputil.NewSingleHostReverseProxy(originUrl), 32 | chanFinish: make(chan error, 1), 33 | } 34 | 35 | idleProxy.server.Handler = idleProxy 36 | idleProxy.proxy.ErrorLog = stdlog.New(httpErrorLogWriter, "", 0) 37 | 38 | go func() { 39 | // wait for the idleTimer to expire, or the context to cancel 40 | select { 41 | case <-idleProxy.TimerDone(): 42 | log.Infof("Idle time (%s) expired, shutting down proxy...", idleProxy.idleTime.String()) 43 | case <-ctx.Done(): 44 | log.Info("Shutting down proxy...") 45 | } 46 | ctx, cancelShutdown := context.WithTimeout(context.Background(), 1*time.Second) 47 | defer cancelShutdown() 48 | if err := idleProxy.server.Shutdown(ctx); err != nil { 49 | idleProxy.chanFinish <- fmt.Errorf("error while shutting down proxy server: %w", err) 50 | } 51 | }() 52 | 53 | // start proxy 54 | go func() { 55 | defer httpErrorLogWriter.Close() 56 | log.Infof("Start proxy server, serving at http://localhost%s", idleProxy.server.Addr) 57 | // ignore ErrServerClosed as this one will be fired when the other goroutine shuts down the server 58 | if err := idleProxy.server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { 59 | idleProxy.chanFinish <- fmt.Errorf("error during handling proxy request: %w", err) 60 | } 61 | // Proxy stopped 62 | log.Info("Proxy server shut down") 63 | close(idleProxy.chanFinish) 64 | }() 65 | 66 | return idleProxy 67 | } 68 | 69 | // Proxy request handler that also resets the idle timer 70 | func (p *IdleProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { 71 | p.timer.Reset(p.idleTime) 72 | log.Infof("%s %s", r.Method, r.URL) 73 | p.proxy.ServeHTTP(w, r) 74 | } 75 | 76 | // Returns a channel that returns the current time when the timer expires 77 | func (p *IdleProxy) TimerDone() <-chan time.Time { 78 | return p.timer.C 79 | } 80 | 81 | // Channel that is closed when the proxy server is shut down. 82 | // If any error occurred during start or shut down of the proxy server, it is sent through the channel. 83 | func (p *IdleProxy) Done() <-chan error { 84 | return p.chanFinish 85 | } 86 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | stdlog "log" 8 | "net/url" 9 | "os" 10 | "os/signal" 11 | "syscall" 12 | "time" 13 | 14 | "github.com/sirupsen/logrus" 15 | prefixed "github.com/x-cray/logrus-prefixed-formatter" 16 | ) 17 | 18 | var Version string 19 | var log *logrus.Entry 20 | 21 | func init() { 22 | base := logrus.New() 23 | base.Formatter = new(prefixed.TextFormatter) 24 | log = base.WithFields(logrus.Fields{"prefix": "tired-proxy"}) 25 | // make all other packages also log with logrus 26 | 27 | stdlog.SetOutput(log.Writer()) 28 | } 29 | 30 | func main() { 31 | fmt.Printf("Tired proxy - version %s\n", Version) 32 | ctx, cancel := context.WithCancel(context.Background()) 33 | defer cancel() 34 | var origin = flag.String("origin", "http://localhost", "the origin host to which the requests are forwarded") 35 | var port = flag.String("port", "8080", "port at which the proxy server listens for requests") 36 | var idleTime = flag.Int("idle-time", 60, "idle time in seconds after which the application shuts down, if no requests where received") 37 | var waitFortPortTime = flag.Int("wait-for-port", 0, "maximum time in seconds to wait before the origin servers port is in use before starting the proxy server") 38 | var verbose = flag.Bool("verbose", false, "verbose logging output") 39 | flag.Parse() 40 | 41 | if *verbose { 42 | log.Logger.SetLevel(logrus.DebugLevel) 43 | } 44 | 45 | originUrl, err := url.Parse(*origin) 46 | if err != nil { 47 | log.Panicf("Invalid url given as host parameter: %s", err) 48 | } 49 | 50 | // Check if we need to wait for the origin server to be online 51 | if *waitFortPortTime > 0 { 52 | log.Infof("Waiting %d seconds for upstream host to come online", *waitFortPortTime) 53 | wfp := NewWaitForPortCmd(originUrl, PortInUse, *waitFortPortTime) 54 | if err := wfp.Wait(); err != nil { 55 | log.Panicf("error while waiting for upstream host to come online: %s", err) 56 | } 57 | log.Info("Upstream host came online") 58 | } 59 | 60 | log.Debug("About to start proxy server...") 61 | 62 | proxy := StartIdleProxy(ctx, originUrl, *port, time.Duration(*idleTime)*time.Second) 63 | 64 | // Setting up signal capturing 65 | stop := make(chan os.Signal, 1) 66 | signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM) 67 | 68 | // wait for either: the proxy to stop or the application to exit 69 | select { 70 | case err := <-proxy.Done(): 71 | if err != nil { 72 | log.Errorf("Proxy error: %s", err) 73 | } 74 | log.Debug("Proxy finished") 75 | case sig := <-stop: 76 | log.Infof("Received signal '%s', shutdown application...", sig) 77 | cancel() 78 | err := <-proxy.Done() // wait for the proxy to exit 79 | if err != nil { 80 | log.Errorf("Error: %s", err) 81 | } 82 | } 83 | log.Info("Tired proxy - exit") 84 | } 85 | -------------------------------------------------------------------------------- /wait_for_port.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // 4 | // Original code from https://github.com/bitnami/wait-for-port 5 | // Minor adaptions done to make it easier to integrate into tired-proxy. 6 | // Did not import the original code because it was not a lib 7 | 8 | import ( 9 | "context" 10 | "fmt" 11 | "net" 12 | "net/url" 13 | "os" 14 | "strconv" 15 | "syscall" 16 | "time" 17 | ) 18 | 19 | type PortStatus string 20 | 21 | const ( 22 | // PortInUse defines the state of a port in use 23 | PortInUse PortStatus = "inuse" 24 | // PortFree defines the state of a free port 25 | PortFree PortStatus = "free" 26 | ) 27 | 28 | // WaitForPortCmd allows checking a port state 29 | type WaitForPortCmd struct { 30 | Host string 31 | State PortStatus 32 | Timeout int 33 | Port int 34 | } 35 | 36 | // NewWaitForPortCmd returns a WaitForPortCmd with given parameters 37 | func NewWaitForPortCmd(remote *url.URL, portStatus PortStatus, timeout int) *WaitForPortCmd { 38 | host, port, err := net.SplitHostPort(remote.Host) 39 | if err != nil { 40 | log.Debugln("No port found in origin host url, guess a port depending on protocol...") 41 | // no port in the host, guess for a port 42 | switch remote.Scheme { 43 | case "http": 44 | port = "80" 45 | case "https": 46 | port = "443" 47 | default: 48 | panic(fmt.Sprintf("unable to determine port for url %#v, please specify one explicitly", remote.String())) 49 | } 50 | host = remote.Host 51 | log.Debugf("Use %d as port for origin") 52 | } 53 | portNr, err := strconv.Atoi(port) 54 | if err != nil { 55 | panic(fmt.Sprintf("unexpected non numeric port discovered %#v", port)) 56 | } 57 | return &WaitForPortCmd{ 58 | State: portStatus, 59 | Host: host, 60 | Timeout: timeout, 61 | Port: portNr, 62 | } 63 | } 64 | 65 | // Execute performs the port check 66 | func (c *WaitForPortCmd) Wait() error { 67 | var checkPortState func(ctx context.Context, host string, port int) bool 68 | switch c.State { 69 | case PortInUse: 70 | checkPortState = portIsInUse 71 | case PortFree: 72 | checkPortState = func(ctx context.Context, host string, port int) bool { 73 | return !portIsInUse(ctx, host, port) 74 | } 75 | default: 76 | return fmt.Errorf("unknown state %q", c.State) 77 | } 78 | if err := validatePort(c.Port); err != nil { 79 | return err 80 | } 81 | ctx, cancel := context.WithTimeout(context.Background(), time.Duration(c.Timeout)*time.Second) 82 | defer cancel() 83 | if err := validateHost(ctx, c.Host); err != nil { 84 | return err 85 | } 86 | 87 | for !checkPortState(ctx, c.Host, c.Port) { 88 | select { 89 | case <-ctx.Done(): 90 | return fmt.Errorf("timeout reached before the port went into state %q", c.State) 91 | case <-time.After(500 * time.Millisecond): 92 | } 93 | } 94 | return nil 95 | } 96 | 97 | func validatePort(port int) error { 98 | if port <= 0 { 99 | return fmt.Errorf("port out of range: port must be greater than zero") 100 | } else if port > 65535 { 101 | return fmt.Errorf("port out of range: port must be <= 65535") 102 | } 103 | return nil 104 | } 105 | 106 | func validateHost(ctx context.Context, host string) error { 107 | // An empty host is perfectly fine for us but net.LookupHost will fail 108 | if host == "" { 109 | return nil 110 | } 111 | if _, err := net.DefaultResolver.LookupHost(ctx, host); err != nil { 112 | return fmt.Errorf("cannot resolve host %q: %v", host, err) 113 | } 114 | return nil 115 | } 116 | 117 | func isAddrInUseError(err error) bool { 118 | if err, ok := err.(*net.OpError); ok { 119 | if err, ok := err.Err.(*os.SyscallError); ok { 120 | return err.Err == syscall.EADDRINUSE 121 | } 122 | } 123 | return false 124 | } 125 | 126 | func canConnectToPort(ctx context.Context, host string, port int) bool { 127 | d := net.Dialer{Timeout: 60 * time.Second} 128 | conn, err := d.DialContext(ctx, "tcp", net.JoinHostPort(host, fmt.Sprintf("%d", port))) 129 | if err == nil { 130 | defer conn.Close() 131 | return true 132 | } 133 | return false 134 | } 135 | 136 | // portIsInUse allows checking if a port is in use in the specified host. 137 | func portIsInUse(ctx context.Context, host string, port int) bool { 138 | log.Debugf("Check if port %d on %s is in use...", port, host) 139 | // If we can connect, is in use 140 | if canConnectToPort(ctx, host, port) { 141 | log.Debugln("Can connect to to port via tcp. Return port in use") 142 | return true 143 | } 144 | 145 | // If we are trying to check a remote host, we cannot do more, so we consider it not in use 146 | if host != "" { 147 | log.Debugln("Unable to check if a port is in use in any other way for remote hosts. Return port not in use") 148 | return false 149 | } 150 | 151 | // If we are checking locally, try to listen 152 | listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) 153 | log.Debugln("Attempt to listen to port locally...") 154 | if err == nil { 155 | listener.Close() 156 | log.Debugln("Able to listen. Return port not in use") 157 | return false 158 | } else if isAddrInUseError(err) { 159 | log.Debugln("Unable to listen with AddrInUseError. Return port is use") 160 | return true 161 | } 162 | // We could not connect to the port, and we cannot listen on it, the safest thing 163 | // we can assume in localhost is that is not in use (binding to a privileged port, for example) 164 | log.Debugln("Unable to connect or listen on port, assume not in use. Return port not in use") 165 | return false 166 | } 167 | --------------------------------------------------------------------------------