diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index a6370b22..84e3c8d9 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -130,18 +130,3 @@ jobs: run: | docker tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }} ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest - scan: - runs-on: ubuntu-latest - needs: - - merge - steps: - - name: Scan Image with Trivy - uses: aquasecurity/trivy-action@0.20.0 - with: - image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest - format: "sarif" - output: "trivy-results.sarif" - - name: Upload Trivy SARIF Report - uses: github/codeql-action/upload-sarif@v3 - with: - sarif_file: "trivy-results.sarif" diff --git a/.gitignore b/.gitignore index 1b8c3100..c2e8386f 100755 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,7 @@ compose.yml config*/ certs*/ bin/ - -templates/codemirror/ +error_pages/ logs/ log/ @@ -13,7 +12,8 @@ log/ go.work.sum -!src/**/ +!cmd/**/ +!internal/**/ todo.md diff --git a/Dockerfile b/Dockerfile index 83d966e2..a82fb68d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ RUN apk add --no-cache tzdata WORKDIR /src # Only copy go.mod and go.sum initially for better caching -COPY src/go.mod src/go.sum ./ +COPY go.mod go.sum /src # Utilize build cache RUN --mount=type=cache,target="/go/pkg/mod" \ @@ -16,8 +16,10 @@ ENV GOCACHE=/root/.cache/go-build # Build the application with better caching RUN --mount=type=cache,target="/go/pkg/mod" \ --mount=type=cache,target="/root/.cache/go-build" \ - --mount=type=bind,src=src,dst=/src \ - CGO_ENABLED=0 GOOS=linux go build -ldflags '-w -s' -pgo=auto -o /go-proxy . + --mount=type=bind,src=cmd,dst=/src/cmd \ + --mount=type=bind,src=internal,dst=/src/internal \ + CGO_ENABLED=0 GOOS=linux go build -ldflags '-w -s' -pgo=auto -o /app/go-proxy ./cmd && \ + mkdir /app/error_pages /app/certs # Stage 2: Final image FROM scratch @@ -28,7 +30,7 @@ LABEL maintainer="yusing@6uo.me" COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo # copy binary -COPY --from=builder /go-proxy /app/ +COPY --from=builder /app / # copy schema directory COPY schema/ /app/schema/ diff --git a/Makefile b/Makefile index 11e77848..717d6bad 100755 --- a/Makefile +++ b/Makefile @@ -1,3 +1,5 @@ +BUILD_FLAG ?= -s -w + .PHONY: all setup build test up restart logs get debug run archive repush rapid-crash debug-list-containers all: debug @@ -10,10 +12,10 @@ setup: build: mkdir -p bin CGO_ENABLED=0 GOOS=linux \ - go build -ldflags '${BUILD_FLAG}' -pgo=auto -o bin/go-proxy github.com/yusing/go-proxy + go build -ldflags '${BUILD_FLAG}' -pgo=auto -o bin/go-proxy ./cmd test: - go test ./src/... + go test ./internal/... up: docker compose up -d @@ -25,13 +27,13 @@ logs: docker compose logs -f get: - cd src && go get -u && go mod tidy && cd .. + cd cmd && go get -u && go mod tidy && cd .. debug: - make build && sudo GOPROXY_DEBUG=1 bin/go-proxy + make BUILD_FLAG="" build && sudo GOPROXY_DEBUG=1 bin/go-proxy run: - BUILD_FLAG="-s -w" make build && sudo bin/go-proxy + make build && sudo bin/go-proxy archive: git archive HEAD -o ../go-proxy-$$(date +"%Y%m%d%H%M").zip diff --git a/README.md b/README.md index ce2ca6fd..b42b1ba0 100755 --- a/README.md +++ b/README.md @@ -40,6 +40,8 @@ A lightweight, easy-to-use, and [performant](docs/benchmark_result.md) reverse p - Auto hot-reload on container state / config file changes - **idlesleeper**: stop containers on idle, wake it up on traffic _(optional, see [showcase](#idlesleeper))_ - HTTP(s) reserve proxy +- [HTTP middleware support](docs/middlewares.md) _(experimental)_ +- [Custom error pages support](docs/middlewares.md#custom-error-pages) - TCP and UDP port forwarding - Web UI for configuration and monitoring (See [screenshots](https://github.com/yusing/go-proxy-frontend?tab=readme-ov-file#screenshots)) - Supports linux/amd64, linux/arm64, linux/arm/v7, linux/arm/v6 multi-platform diff --git a/src/main.go b/cmd/main.go similarity index 87% rename from src/main.go rename to cmd/main.go index 79de91fc..5e2a94bc 100755 --- a/src/main.go +++ b/cmd/main.go @@ -16,23 +16,24 @@ import ( "time" "github.com/sirupsen/logrus" - "github.com/yusing/go-proxy/api" - apiUtils "github.com/yusing/go-proxy/api/v1/utils" - "github.com/yusing/go-proxy/common" - "github.com/yusing/go-proxy/config" - "github.com/yusing/go-proxy/docker" - "github.com/yusing/go-proxy/docker/idlewatcher" - E "github.com/yusing/go-proxy/error" - R "github.com/yusing/go-proxy/route" - "github.com/yusing/go-proxy/server" - F "github.com/yusing/go-proxy/utils/functional" + "github.com/yusing/go-proxy/internal" + "github.com/yusing/go-proxy/internal/api" + apiUtils "github.com/yusing/go-proxy/internal/api/v1/utils" + "github.com/yusing/go-proxy/internal/common" + "github.com/yusing/go-proxy/internal/config" + "github.com/yusing/go-proxy/internal/docker" + "github.com/yusing/go-proxy/internal/docker/idlewatcher" + E "github.com/yusing/go-proxy/internal/error" + R "github.com/yusing/go-proxy/internal/route" + "github.com/yusing/go-proxy/internal/server" + F "github.com/yusing/go-proxy/internal/utils/functional" ) func main() { args := common.GetArgs() if args.Command == common.CommandSetup { - Setup() + internal.Setup() return } diff --git a/docs/middlewares.md b/docs/middlewares.md index 6a1fbb46..959c85c6 100644 --- a/docs/middlewares.md +++ b/docs/middlewares.md @@ -8,6 +8,7 @@ - [Table of content](#table-of-content) - [Available middlewares](#available-middlewares) - [Redirect http](#redirect-http) + - [Custom error pages](#custom-error-pages) - [Modify request or response](#modify-request-or-response) - [Set headers](#set-headers) - [Add headers](#add-headers) @@ -48,6 +49,56 @@ server { [🔼Back to top](#table-of-content) +### Custom error pages + +To enable custom error pages, mount a folder containing your `html`, `js`, `css` files to `/app/error_pages` of _go-proxy_ container **(subfolders are ignored, please place all files in root directory)** + +Any path under `error_pages` directory (e.g. `href` tag), should starts with `/$gperrorpage/` + +Example: +```html + + + 404 Not Found + + + + ... + + +``` + +Hot-reloading is **supported**, you can **edit**, **rename** or **delete** files **without restarting**. Changes will be reflected after page reload + +Error page will be served if: +- status code is not in range of 200 to 300 +- content type is `text/html`, `application/xhtml+xml` or `text/plain` + +Error page will be served: + +- from file `.html` if exists +- otherwise from `404.html` +- if they don't exist, original response will be served + +```yaml +# docker labels +proxy.app1.middlewares.custom_error_page: + +# include file +app1: + middlewares: + custom_error_page: +``` + +nginx equivalent: +```nginx +location / { + try_files $uri $uri/ /error_pages/404.html =404; +} +``` + +[🔼Back to top](#table-of-content) + ### Modify request or response ```yaml @@ -89,6 +140,8 @@ location / { } ``` +[🔼Back to top](#table-of-content) + #### Add headers ```yaml @@ -114,6 +167,8 @@ location / { } ``` +[🔼Back to top](#table-of-content) + #### Hide headers ```yaml @@ -171,6 +226,8 @@ app1: set_x_forwarded: ``` +[🔼Back to top](#table-of-content) + ### Forward Authorization header (experimental) Fields: @@ -226,6 +283,8 @@ http: - session_id ``` +[🔼Back to top](#table-of-content) + ## Examples ### Authentik @@ -242,4 +301,6 @@ services: proxy.authentik.middlewares.set_x_forwarded: proxy.authentik.middlewares.modify_request.add_headers: | Strict-Transport-Security: "max-age=63072000" always -``` \ No newline at end of file +``` + +[🔼Back to top](#table-of-content) \ No newline at end of file diff --git a/src/go.mod b/go.mod similarity index 94% rename from src/go.mod rename to go.mod index c273d718..d8432571 100644 --- a/src/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( require ( github.com/Microsoft/go-winio v0.6.2 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect - github.com/cloudflare/cloudflare-go v0.105.0 // indirect + github.com/cloudflare/cloudflare-go v0.106.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-connections v0.5.0 // indirect @@ -39,9 +39,9 @@ require ( github.com/pkg/errors v0.9.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect go.opentelemetry.io/otel v1.30.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0 // indirect go.opentelemetry.io/otel/metric v1.30.0 // indirect - go.opentelemetry.io/otel/sdk v1.24.0 // indirect + go.opentelemetry.io/otel/sdk v1.30.0 // indirect go.opentelemetry.io/otel/trace v1.30.0 // indirect golang.org/x/crypto v0.27.0 // indirect golang.org/x/mod v0.21.0 // indirect diff --git a/src/go.sum b/go.sum similarity index 86% rename from src/go.sum rename to go.sum index 2e7394cc..cb8f5356 100644 --- a/src/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/cloudflare/cloudflare-go v0.105.0 h1:yu2IatITLZ4dw7/byzRrlE5DfUvtub0k9CHZ5zBlj90= -github.com/cloudflare/cloudflare-go v0.105.0/go.mod h1:pfUQ4PIG4ISI0/Mmc21Bp86UnFU0ktmPf3iTgbSL+cM= +github.com/cloudflare/cloudflare-go v0.106.0 h1:q41gC5Wc1nfi0D1ZhSHokWcd9mGMbqC7RE7qiP+qE00= +github.com/cloudflare/cloudflare-go v0.106.0/go.mod h1:pfUQ4PIG4ISI0/Mmc21Bp86UnFU0ktmPf3iTgbSL+cM= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -43,8 +43,10 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc= github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -91,18 +93,18 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 h1:ZIg3ZT/ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0/go.mod h1:DQAwmETtZV00skUwgD6+0U89g80NKsJE3DCKeLLPQMI= go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts= go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 h1:Xw8U6u2f8DK2XAkGRFV7BBLENgnTGX9i4rQRxJf+/vs= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0 h1:lsInsfvhVIfOI6qHVyysXMNDnjO9Npvl7tlDPJFBVd4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0/go.mod h1:KQsVNh4OjgjTG0G6EiNi1jVpnaeeKsKMRwbLN+f1+8M= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0 h1:umZgi92IyxfXd/l4kaDhnKgY8rnN/cZcF1LKc6I8OQ8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0/go.mod h1:4lVs6obhSVRb1EW5FhOuBTyiQhtRtAnnva9vD3yRfq8= go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w= go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ= -go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= -go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= +go.opentelemetry.io/otel/sdk v1.30.0 h1:cHdik6irO49R5IysVhdn8oaiR9m8XluDaJAs4DfOrYE= +go.opentelemetry.io/otel/sdk v1.30.0/go.mod h1:p14X4Ok8S+sygzblytT1nqG98QG2KYKv++HE0LY/mhg= go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc= go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o= -go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI= -go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -148,14 +150,14 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY= -google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2 h1:rIo7ocm2roD9DcFIX67Ym8icoGCKSARAiPljFhh5suQ= -google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2/go.mod h1:O1cOfN1Cy6QEYr7VxtjOyP5AdAuR0aJ/MYZaaof623Y= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= -google.golang.org/grpc v1.63.1 h1:pNClQmvdlyNUiwFETOux/PYqfhmA7BrswEdGRnib1fA= -google.golang.org/grpc v1.63.1/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc= +google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.66.1 h1:hO5qAXR19+/Z44hmvIM4dQFMSYX9XcWsByfoxutBpAM= +google.golang.org/grpc v1.66.1/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/go.work b/go.work deleted file mode 100644 index aad4eaa8..00000000 --- a/go.work +++ /dev/null @@ -1,5 +0,0 @@ -go 1.22.0 - -toolchain go1.23.1 - -use ./src diff --git a/internal/api/handler.go b/internal/api/handler.go new file mode 100644 index 00000000..4139842c --- /dev/null +++ b/internal/api/handler.go @@ -0,0 +1,57 @@ +package api + +import ( + "fmt" + "net/http" + + v1 "github.com/yusing/go-proxy/internal/api/v1" + "github.com/yusing/go-proxy/internal/api/v1/error_page" + . "github.com/yusing/go-proxy/internal/api/v1/utils" + "github.com/yusing/go-proxy/internal/common" + "github.com/yusing/go-proxy/internal/config" +) + +type ServeMux struct{ *http.ServeMux } + +func NewServeMux() ServeMux { + return ServeMux{http.NewServeMux()} +} + +func (mux ServeMux) HandleFunc(method, endpoint string, handler http.HandlerFunc) { + mux.ServeMux.HandleFunc(fmt.Sprintf("%s %s", method, endpoint), checkHost(handler)) +} + +func NewHandler(cfg *config.Config) http.Handler { + mux := NewServeMux() + mux.HandleFunc("GET", "/v1", v1.Index) + mux.HandleFunc("GET", "/v1/checkhealth", wrap(cfg, v1.CheckHealth)) + mux.HandleFunc("HEAD", "/v1/checkhealth", wrap(cfg, v1.CheckHealth)) + mux.HandleFunc("POST", "/v1/reload", wrap(cfg, v1.Reload)) + mux.HandleFunc("GET", "/v1/list", wrap(cfg, v1.List)) + mux.HandleFunc("GET", "/v1/list/{what}", wrap(cfg, v1.List)) + mux.HandleFunc("GET", "/v1/file", v1.GetFileContent) + mux.HandleFunc("GET", "/v1/file/{filename}", v1.GetFileContent) + mux.HandleFunc("POST", "/v1/file/{filename}", v1.SetFileContent) + mux.HandleFunc("PUT", "/v1/file/{filename}", v1.SetFileContent) + mux.HandleFunc("GET", "/v1/stats", wrap(cfg, v1.Stats)) + mux.HandleFunc("GET", "/v1/error_page", error_page.GetHandleFunc()) + return mux +} + +// allow only requests to API server with host matching common.APIHTTPAddr +func checkHost(f http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.Host != common.APIHTTPAddr { + Logger.Warnf("invalid request to API server with host: %s, expected: %s", r.Host, common.APIHTTPAddr) + w.WriteHeader(http.StatusNotFound) + return + } + f(w, r) + } +} + +func wrap(cfg *config.Config, f func(cfg *config.Config, w http.ResponseWriter, r *http.Request)) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + f(cfg, w, r) + } +} diff --git a/src/api/v1/checkhealth.go b/internal/api/v1/checkhealth.go similarity index 85% rename from src/api/v1/checkhealth.go rename to internal/api/v1/checkhealth.go index 19476d45..6a2b483f 100644 --- a/src/api/v1/checkhealth.go +++ b/internal/api/v1/checkhealth.go @@ -5,9 +5,9 @@ import ( "net/http" "strings" - U "github.com/yusing/go-proxy/api/v1/utils" - "github.com/yusing/go-proxy/config" - R "github.com/yusing/go-proxy/route" + U "github.com/yusing/go-proxy/internal/api/v1/utils" + "github.com/yusing/go-proxy/internal/config" + R "github.com/yusing/go-proxy/internal/route" ) func CheckHealth(cfg *config.Config, w http.ResponseWriter, r *http.Request) { diff --git a/internal/api/v1/error_page/error_page.go b/internal/api/v1/error_page/error_page.go new file mode 100644 index 00000000..a2a46e31 --- /dev/null +++ b/internal/api/v1/error_page/error_page.go @@ -0,0 +1,88 @@ +package error_page + +import ( + "context" + "fmt" + "os" + "path" + "sync" + + api "github.com/yusing/go-proxy/internal/api/v1/utils" + "github.com/yusing/go-proxy/internal/common" + U "github.com/yusing/go-proxy/internal/utils" + F "github.com/yusing/go-proxy/internal/utils/functional" + W "github.com/yusing/go-proxy/internal/watcher" + "github.com/yusing/go-proxy/internal/watcher/events" +) + +const errPagesBasePath = common.ErrorPagesBasePath + +var setup = sync.OnceFunc(func() { + dirWatcher = W.NewDirectoryWatcher(context.Background(), errPagesBasePath) + loadContent() + go watchDir() +}) + +func GetStaticFile(filename string) ([]byte, bool) { + return fileContentMap.Load(filename) +} + +// try .html -> 404.html -> not ok +func GetErrorPageByStatus(statusCode int) (content []byte, ok bool) { + content, ok = fileContentMap.Load(fmt.Sprintf("%d.html", statusCode)) + if !ok && statusCode != 404 { + return fileContentMap.Load("404.html") + } + return +} + +func loadContent() { + files, err := U.ListFiles(errPagesBasePath, 0) + if err != nil { + api.Logger.Error(err) + return + } + for _, file := range files { + if fileContentMap.Has(file) { + continue + } + content, err := os.ReadFile(file) + if err != nil { + api.Logger.Errorf("failed to read error page resource %s: %s", file, err) + continue + } + file = path.Base(file) + api.Logger.Infof("error page resource %s loaded", file) + fileContentMap.Store(file, content) + } +} + +func watchDir() { + eventCh, errCh := dirWatcher.Events(context.Background()) + for { + select { + case event, ok := <-eventCh: + if !ok { + return + } + filename := event.ActorName + switch event.Action { + case events.ActionFileWritten: + fileContentMap.Delete(filename) + loadContent() + case events.ActionFileDeleted: + fileContentMap.Delete(filename) + api.Logger.Infof("error page resource %s deleted", filename) + case events.ActionFileRenamed: + api.Logger.Infof("error page resource %s deleted", filename) + fileContentMap.Delete(filename) + loadContent() + } + case err := <-errCh: + api.Logger.Errorf("error watching error page directory: %s", err) + } + } +} + +var dirWatcher W.Watcher +var fileContentMap = F.NewMapOf[string, []byte]() diff --git a/internal/api/v1/error_page/http_handler.go b/internal/api/v1/error_page/http_handler.go new file mode 100644 index 00000000..826fd9db --- /dev/null +++ b/internal/api/v1/error_page/http_handler.go @@ -0,0 +1,25 @@ +package error_page + +import "net/http" + +func GetHandleFunc() http.HandlerFunc { + setup() + return serveHTTP +} + +func serveHTTP(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + if r.URL.Path == "/" { + http.Error(w, "invalid path", http.StatusNotFound) + return + } + content, ok := fileContentMap.Load(r.URL.Path) + if !ok { + http.Error(w, "404 not found", http.StatusNotFound) + return + } + w.Write(content) +} diff --git a/src/api/v1/file.go b/internal/api/v1/file.go similarity index 66% rename from src/api/v1/file.go rename to internal/api/v1/file.go index a0adc094..ed17b0d4 100644 --- a/src/api/v1/file.go +++ b/internal/api/v1/file.go @@ -6,10 +6,11 @@ import ( "os" "path" - U "github.com/yusing/go-proxy/api/v1/utils" - "github.com/yusing/go-proxy/common" - "github.com/yusing/go-proxy/config" - "github.com/yusing/go-proxy/proxy/provider" + U "github.com/yusing/go-proxy/internal/api/v1/utils" + "github.com/yusing/go-proxy/internal/common" + "github.com/yusing/go-proxy/internal/config" + E "github.com/yusing/go-proxy/internal/error" + "github.com/yusing/go-proxy/internal/proxy/provider" ) func GetFileContent(w http.ResponseWriter, r *http.Request) { @@ -37,14 +38,15 @@ func SetFileContent(w http.ResponseWriter, r *http.Request) { return } + var validateErr E.NestedError if filename == common.ConfigFileName { - err = config.Validate(content).Error() + validateErr = config.Validate(content) } else { - err = provider.Validate(content).Error() + validateErr = provider.Validate(content) } - if err != nil { - U.HandleErr(w, r, err, http.StatusBadRequest) + if validateErr != nil { + U.RespondJson(w, validateErr.JSONObject(), http.StatusBadRequest) return } diff --git a/src/api/v1/index.go b/internal/api/v1/index.go similarity index 100% rename from src/api/v1/index.go rename to internal/api/v1/index.go diff --git a/src/api/v1/list.go b/internal/api/v1/list.go similarity index 84% rename from src/api/v1/list.go rename to internal/api/v1/list.go index 66bf9c06..48a8744b 100644 --- a/src/api/v1/list.go +++ b/internal/api/v1/list.go @@ -5,10 +5,9 @@ import ( "net/http" "os" - "github.com/yusing/go-proxy/common" - "github.com/yusing/go-proxy/config" - - U "github.com/yusing/go-proxy/api/v1/utils" + U "github.com/yusing/go-proxy/internal/api/v1/utils" + "github.com/yusing/go-proxy/internal/common" + "github.com/yusing/go-proxy/internal/config" ) func List(cfg *config.Config, w http.ResponseWriter, r *http.Request) { @@ -38,7 +37,7 @@ func listRoutes(cfg *config.Config, w http.ResponseWriter, r *http.Request) { } } - if err := U.RespondJson(routes, w); err != nil { + if err := U.RespondJson(w, routes); err != nil { U.HandleErr(w, r, err) } } diff --git a/internal/api/v1/reload.go b/internal/api/v1/reload.go new file mode 100644 index 00000000..c62c61ea --- /dev/null +++ b/internal/api/v1/reload.go @@ -0,0 +1,16 @@ +package v1 + +import ( + "net/http" + + U "github.com/yusing/go-proxy/internal/api/v1/utils" + "github.com/yusing/go-proxy/internal/config" +) + +func Reload(cfg *config.Config, w http.ResponseWriter, r *http.Request) { + if err := cfg.Reload(); err != nil { + U.RespondJson(w, err.JSONObject(), http.StatusInternalServerError) + } else { + w.WriteHeader(http.StatusOK) + } +} diff --git a/src/api/v1/stats.go b/internal/api/v1/stats.go similarity index 52% rename from src/api/v1/stats.go rename to internal/api/v1/stats.go index 154fe01a..1d6bf35c 100644 --- a/src/api/v1/stats.go +++ b/internal/api/v1/stats.go @@ -3,10 +3,10 @@ package v1 import ( "net/http" - U "github.com/yusing/go-proxy/api/v1/utils" - "github.com/yusing/go-proxy/config" - "github.com/yusing/go-proxy/server" - "github.com/yusing/go-proxy/utils" + U "github.com/yusing/go-proxy/internal/api/v1/utils" + "github.com/yusing/go-proxy/internal/config" + "github.com/yusing/go-proxy/internal/server" + "github.com/yusing/go-proxy/internal/utils" ) func Stats(cfg *config.Config, w http.ResponseWriter, r *http.Request) { @@ -14,7 +14,7 @@ func Stats(cfg *config.Config, w http.ResponseWriter, r *http.Request) { "proxies": cfg.Statistics(), "uptime": utils.FormatDuration(server.GetProxyServer().Uptime()), } - if err := U.RespondJson(stats, w); err != nil { + if err := U.RespondJson(w, stats); err != nil { U.HandleErr(w, r, err) } } diff --git a/src/api/v1/utils/error.go b/internal/api/v1/utils/error.go similarity index 85% rename from src/api/v1/utils/error.go rename to internal/api/v1/utils/error.go index 3f4dc925..1a9f58c4 100644 --- a/src/api/v1/utils/error.go +++ b/internal/api/v1/utils/error.go @@ -6,12 +6,14 @@ import ( "net/http" "github.com/sirupsen/logrus" - E "github.com/yusing/go-proxy/error" + E "github.com/yusing/go-proxy/internal/error" ) +var Logger = logrus.WithField("module", "api") + func HandleErr(w http.ResponseWriter, r *http.Request, origErr error, code ...int) { err := E.From(origErr).Subjectf("%s %s", r.Method, r.URL) - logrus.WithField("module", "api").Error(err) + Logger.Error(err) if len(code) > 0 { http.Error(w, err.String(), code[0]) return diff --git a/internal/api/v1/utils/health_check.go b/internal/api/v1/utils/health_check.go new file mode 100644 index 00000000..a03f9c89 --- /dev/null +++ b/internal/api/v1/utils/health_check.go @@ -0,0 +1,33 @@ +package utils + +import ( + "net" + "net/http" + + "github.com/yusing/go-proxy/internal/common" +) + +func IsSiteHealthy(url string) bool { + // try HEAD first + // if HEAD is not allowed, try GET + resp, err := httpClient.Head(url) + if resp != nil { + resp.Body.Close() + } + if err != nil && resp != nil && resp.StatusCode == http.StatusMethodNotAllowed { + _, err = httpClient.Get(url) + } + if resp != nil { + resp.Body.Close() + } + return err == nil +} + +func IsStreamHealthy(scheme, address string) bool { + conn, err := net.DialTimeout(scheme, address, common.DialTimeout) + if err != nil { + return false + } + conn.Close() + return true +} diff --git a/internal/api/v1/utils/http_client.go b/internal/api/v1/utils/http_client.go new file mode 100644 index 00000000..bcacbf37 --- /dev/null +++ b/internal/api/v1/utils/http_client.go @@ -0,0 +1,23 @@ +package utils + +import ( + "crypto/tls" + "net" + "net/http" + + "github.com/yusing/go-proxy/internal/common" +) + +var httpClient = &http.Client{ + Timeout: common.ConnectionTimeout, + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DisableKeepAlives: true, + ForceAttemptHTTP2: true, + DialContext: (&net.Dialer{ + Timeout: common.DialTimeout, + KeepAlive: common.KeepAlive, // this is different from DisableKeepAlives + }).DialContext, + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + }, +} diff --git a/internal/api/v1/utils/localhost.go b/internal/api/v1/utils/localhost.go new file mode 100644 index 00000000..4c5dde1a --- /dev/null +++ b/internal/api/v1/utils/localhost.go @@ -0,0 +1,31 @@ +package utils + +import ( + "fmt" + "io" + "net/http" + + "github.com/yusing/go-proxy/internal/common" + E "github.com/yusing/go-proxy/internal/error" +) + +func ReloadServer() E.NestedError { + resp, err := httpClient.Post(fmt.Sprintf("%s/v1/reload", common.APIHTTPURL), "", nil) + if err != nil { + return E.From(err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + failure := E.Failure("server reload").Extraf("status code: %v", resp.StatusCode) + b, err := io.ReadAll(resp.Body) + if err != nil { + return failure.Extraf("unable to read response body: %s", err) + } + reloadErr, ok := E.FromJSON(b) + if ok { + return E.Join("reload success, but server returned error", reloadErr) + } + return failure.Extraf("unable to read response body") + } + return nil +} diff --git a/src/api/v1/utils/utils.go b/internal/api/v1/utils/utils.go similarity index 51% rename from src/api/v1/utils/utils.go rename to internal/api/v1/utils/utils.go index a7501b18..da4199ad 100644 --- a/src/api/v1/utils/utils.go +++ b/internal/api/v1/utils/utils.go @@ -5,9 +5,12 @@ import ( "net/http" ) -func RespondJson(data any, w http.ResponseWriter) error { +func RespondJson(w http.ResponseWriter, data any, code ...int) error { + if len(code) > 0 { + w.WriteHeader(code[0]) + } w.Header().Set("Content-Type", "application/json") - j, err := json.Marshal(data) + j, err := json.MarshalIndent(data, "", " ") if err != nil { return err } else { diff --git a/src/autocert/config.go b/internal/autocert/config.go similarity index 93% rename from src/autocert/config.go rename to internal/autocert/config.go index 4143e8b7..93f873fd 100644 --- a/src/autocert/config.go +++ b/internal/autocert/config.go @@ -7,8 +7,8 @@ import ( "github.com/go-acme/lego/v4/certcrypto" "github.com/go-acme/lego/v4/lego" - E "github.com/yusing/go-proxy/error" - M "github.com/yusing/go-proxy/models" + E "github.com/yusing/go-proxy/internal/error" + M "github.com/yusing/go-proxy/internal/models" ) type Config M.AutoCertConfig diff --git a/src/autocert/constants.go b/internal/autocert/constants.go similarity index 100% rename from src/autocert/constants.go rename to internal/autocert/constants.go diff --git a/src/autocert/dummy.go b/internal/autocert/dummy.go similarity index 100% rename from src/autocert/dummy.go rename to internal/autocert/dummy.go diff --git a/src/autocert/provider.go b/internal/autocert/provider.go similarity index 97% rename from src/autocert/provider.go rename to internal/autocert/provider.go index 97fb8025..b7217f10 100644 --- a/src/autocert/provider.go +++ b/internal/autocert/provider.go @@ -14,9 +14,9 @@ import ( "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/lego" "github.com/go-acme/lego/v4/registration" - E "github.com/yusing/go-proxy/error" - M "github.com/yusing/go-proxy/models" - U "github.com/yusing/go-proxy/utils" + E "github.com/yusing/go-proxy/internal/error" + M "github.com/yusing/go-proxy/internal/models" + U "github.com/yusing/go-proxy/internal/utils" ) type Provider struct { diff --git a/src/autocert/provider_test/ovh_test.go b/internal/autocert/provider_test/ovh_test.go similarity index 92% rename from src/autocert/provider_test/ovh_test.go rename to internal/autocert/provider_test/ovh_test.go index 032c0a42..9463f1d8 100644 --- a/src/autocert/provider_test/ovh_test.go +++ b/internal/autocert/provider_test/ovh_test.go @@ -4,8 +4,8 @@ import ( "testing" "github.com/go-acme/lego/v4/providers/dns/ovh" - U "github.com/yusing/go-proxy/utils" - . "github.com/yusing/go-proxy/utils/testing" + U "github.com/yusing/go-proxy/internal/utils" + . "github.com/yusing/go-proxy/internal/utils/testing" "gopkg.in/yaml.v3" ) diff --git a/src/autocert/setup.go b/internal/autocert/setup.go similarity index 91% rename from src/autocert/setup.go rename to internal/autocert/setup.go index e8d1ca2c..2b44f943 100644 --- a/src/autocert/setup.go +++ b/internal/autocert/setup.go @@ -4,7 +4,7 @@ import ( "context" "os" - E "github.com/yusing/go-proxy/error" + E "github.com/yusing/go-proxy/internal/error" ) func (p *Provider) Setup(ctx context.Context) (err E.NestedError) { diff --git a/src/autocert/state.go b/internal/autocert/state.go similarity index 100% rename from src/autocert/state.go rename to internal/autocert/state.go diff --git a/src/autocert/user.go b/internal/autocert/user.go similarity index 100% rename from src/autocert/user.go rename to internal/autocert/user.go diff --git a/src/common/args.go b/internal/common/args.go similarity index 95% rename from src/common/args.go rename to internal/common/args.go index e84d6f93..cdc48f4e 100644 --- a/src/common/args.go +++ b/internal/common/args.go @@ -4,7 +4,7 @@ import ( "flag" "github.com/sirupsen/logrus" - E "github.com/yusing/go-proxy/error" + E "github.com/yusing/go-proxy/internal/error" ) type Args struct { diff --git a/src/common/constants.go b/internal/common/constants.go similarity index 90% rename from src/common/constants.go rename to internal/common/constants.go index 156f5454..69bed7a2 100644 --- a/src/common/constants.go +++ b/internal/common/constants.go @@ -7,7 +7,7 @@ import ( const ( ConnectionTimeout = 5 * time.Second DialTimeout = 3 * time.Second - KeepAlive = 5 * time.Second + KeepAlive = 60 * time.Second ) // file, folder structure @@ -30,6 +30,10 @@ const ( ComposeExampleFileName = "compose.example.yml" ) +const ( + ErrorPagesBasePath = "error_pages" +) + const DockerHostFromEnv = "$DOCKER_HOST" const ( diff --git a/internal/common/env.go b/internal/common/env.go new file mode 100644 index 00000000..de2f714e --- /dev/null +++ b/internal/common/env.go @@ -0,0 +1,55 @@ +package common + +import ( + "fmt" + "net" + "os" + + "github.com/sirupsen/logrus" + U "github.com/yusing/go-proxy/internal/utils" +) + +var ( + NoSchemaValidation = GetEnvBool("GOPROXY_NO_SCHEMA_VALIDATION") + IsDebug = GetEnvBool("GOPROXY_DEBUG") + + ProxyHTTPAddr, + ProxyHTTPHost, + ProxyHTTPPort, + ProxyHTTPURL = GetAddrEnv("GOPROXY_HTTP_ADDR", ":80", "http") + + ProxyHTTPSAddr, + ProxyHTTPSHost, + ProxyHTTPSPort, + ProxyHTTPSURL = GetAddrEnv("GOPROXY_HTTPS_ADDR", ":443", "https") + + APIHTTPAddr, + APIHTTPHost, + APIHTTPPort, + APIHTTPURL = GetAddrEnv("GOPROXY_API_ADDR", "127.0.0.1:8888", "http") +) + +func GetEnvBool(key string) bool { + return U.ParseBool(os.Getenv(key)) +} + +func GetEnv(key, defaultValue string) string { + value, ok := os.LookupEnv(key) + if !ok { + value = defaultValue + } + return value +} + +func GetAddrEnv(key, defaultValue, scheme string) (addr, host, port, fullURL string) { + addr = GetEnv(key, defaultValue) + host, port, err := net.SplitHostPort(addr) + if err != nil { + logrus.Fatalf("Invalid address: %s", addr) + } + if host == "" { + host = "localhost" + } + fullURL = fmt.Sprintf("%s://%s:%s", scheme, host, port) + return +} diff --git a/src/common/http.go b/internal/common/http.go similarity index 100% rename from src/common/http.go rename to internal/common/http.go diff --git a/src/common/ports.go b/internal/common/ports.go similarity index 100% rename from src/common/ports.go rename to internal/common/ports.go diff --git a/src/config/config.go b/internal/config/config.go similarity index 87% rename from src/config/config.go rename to internal/config/config.go index ef7b1ca4..26052f84 100644 --- a/src/config/config.go +++ b/internal/config/config.go @@ -5,16 +5,16 @@ import ( "os" "github.com/sirupsen/logrus" - "github.com/yusing/go-proxy/autocert" - "github.com/yusing/go-proxy/common" - E "github.com/yusing/go-proxy/error" - M "github.com/yusing/go-proxy/models" - PR "github.com/yusing/go-proxy/proxy/provider" - R "github.com/yusing/go-proxy/route" - U "github.com/yusing/go-proxy/utils" - F "github.com/yusing/go-proxy/utils/functional" - W "github.com/yusing/go-proxy/watcher" - "github.com/yusing/go-proxy/watcher/events" + "github.com/yusing/go-proxy/internal/autocert" + "github.com/yusing/go-proxy/internal/common" + E "github.com/yusing/go-proxy/internal/error" + M "github.com/yusing/go-proxy/internal/models" + PR "github.com/yusing/go-proxy/internal/proxy/provider" + R "github.com/yusing/go-proxy/internal/route" + U "github.com/yusing/go-proxy/internal/utils" + F "github.com/yusing/go-proxy/internal/utils/functional" + W "github.com/yusing/go-proxy/internal/watcher" + "github.com/yusing/go-proxy/internal/watcher/events" "gopkg.in/yaml.v3" ) @@ -45,7 +45,7 @@ func Load() E.NestedError { value: M.DefaultConfig(), proxyProviders: F.NewMapOf[string, *PR.Provider](), l: logrus.WithField("module", "config"), - watcher: W.NewFileWatcher(common.ConfigFileName), + watcher: W.NewConfigFileWatcher(common.ConfigFileName), reloadReq: make(chan struct{}, 1), } return instance.load() @@ -116,8 +116,9 @@ func (cfg *Config) WatchChanges() { case <-cfg.watcherCtx.Done(): return case event := <-eventCh: - if event.Action == events.ActionFileDeleted { - cfg.stopProviders() + if event.Action == events.ActionFileDeleted || event.Action == events.ActionFileRenamed { + cfg.l.Error("config file deleted or renamed, ignoring...") + continue } else { cfg.reloadReq <- struct{}{} } diff --git a/src/config/query.go b/internal/config/query.go similarity index 87% rename from src/config/query.go rename to internal/config/query.go index 47e15e51..d3195f5b 100644 --- a/src/config/query.go +++ b/internal/config/query.go @@ -1,11 +1,11 @@ package config import ( - M "github.com/yusing/go-proxy/models" - PR "github.com/yusing/go-proxy/proxy/provider" - R "github.com/yusing/go-proxy/route" - U "github.com/yusing/go-proxy/utils" - F "github.com/yusing/go-proxy/utils/functional" + M "github.com/yusing/go-proxy/internal/models" + PR "github.com/yusing/go-proxy/internal/proxy/provider" + R "github.com/yusing/go-proxy/internal/route" + U "github.com/yusing/go-proxy/internal/utils" + F "github.com/yusing/go-proxy/internal/utils/functional" ) func (cfg *Config) DumpEntries() map[string]*M.RawEntry { diff --git a/src/docker/client.go b/internal/docker/client.go similarity index 95% rename from src/docker/client.go rename to internal/docker/client.go index 89c16f5c..41328572 100644 --- a/src/docker/client.go +++ b/internal/docker/client.go @@ -8,9 +8,9 @@ import ( "github.com/docker/cli/cli/connhelper" "github.com/docker/docker/client" "github.com/sirupsen/logrus" - "github.com/yusing/go-proxy/common" - E "github.com/yusing/go-proxy/error" - F "github.com/yusing/go-proxy/utils/functional" + "github.com/yusing/go-proxy/internal/common" + E "github.com/yusing/go-proxy/internal/error" + F "github.com/yusing/go-proxy/internal/utils/functional" ) type Client struct { diff --git a/src/docker/client_info.go b/internal/docker/client_info.go similarity index 96% rename from src/docker/client_info.go rename to internal/docker/client_info.go index c09190a9..8446393e 100644 --- a/src/docker/client_info.go +++ b/internal/docker/client_info.go @@ -8,7 +8,7 @@ import ( "github.com/docker/docker/api/types/container" "github.com/docker/docker/client" - E "github.com/yusing/go-proxy/error" + E "github.com/yusing/go-proxy/internal/error" ) type ClientInfo struct { diff --git a/src/docker/container.go b/internal/docker/container.go similarity index 98% rename from src/docker/container.go rename to internal/docker/container.go index 956223e9..0e492e42 100644 --- a/src/docker/container.go +++ b/internal/docker/container.go @@ -6,7 +6,7 @@ import ( "strings" "github.com/docker/docker/api/types" - U "github.com/yusing/go-proxy/utils" + U "github.com/yusing/go-proxy/internal/utils" ) type Container struct { diff --git a/src/docker/homepage_label.go b/internal/docker/homepage_label.go similarity index 100% rename from src/docker/homepage_label.go rename to internal/docker/homepage_label.go diff --git a/src/docker/idlewatcher/html/loading_page.html b/internal/docker/idlewatcher/html/loading_page.html similarity index 100% rename from src/docker/idlewatcher/html/loading_page.html rename to internal/docker/idlewatcher/html/loading_page.html diff --git a/src/docker/idlewatcher/http.go b/internal/docker/idlewatcher/http.go similarity index 92% rename from src/docker/idlewatcher/http.go rename to internal/docker/idlewatcher/http.go index c2203660..f2b46056 100644 --- a/src/docker/idlewatcher/http.go +++ b/internal/docker/idlewatcher/http.go @@ -19,13 +19,7 @@ type templateData struct { //go:embed html/loading_page.html var loadingPage []byte -var loadingPageTmpl = func() *template.Template { - tmpl, err := template.New("loading").Parse(string(loadingPage)) - if err != nil { - panic(err) - } - return tmpl -}() +var loadingPageTmpl = template.Must(template.New("loading_page").Parse(string(loadingPage))) const ( htmlContentType = "text/html; charset=utf-8" diff --git a/src/docker/idlewatcher/round_trip.go b/internal/docker/idlewatcher/round_trip.go similarity index 96% rename from src/docker/idlewatcher/round_trip.go rename to internal/docker/idlewatcher/round_trip.go index b1d8c2a3..738bfc62 100644 --- a/src/docker/idlewatcher/round_trip.go +++ b/internal/docker/idlewatcher/round_trip.go @@ -18,7 +18,10 @@ func (rt roundTripper) RoundTrip(req *http.Request) (*http.Response, error) { func (w *watcher) roundTrip(origRoundTrip roundTripFunc, req *http.Request) (*http.Response, error) { // wake the container - w.wakeCh <- struct{}{} + select { + case w.wakeCh <- struct{}{}: + default: + } // target site is ready, passthrough if w.ready.Load() { diff --git a/src/docker/idlewatcher/watcher.go b/internal/docker/idlewatcher/watcher.go similarity index 96% rename from src/docker/idlewatcher/watcher.go rename to internal/docker/idlewatcher/watcher.go index af17f008..4fa5cd98 100644 --- a/src/docker/idlewatcher/watcher.go +++ b/internal/docker/idlewatcher/watcher.go @@ -9,11 +9,11 @@ import ( "github.com/docker/docker/api/types/container" "github.com/sirupsen/logrus" - D "github.com/yusing/go-proxy/docker" - E "github.com/yusing/go-proxy/error" - P "github.com/yusing/go-proxy/proxy" - PT "github.com/yusing/go-proxy/proxy/fields" - W "github.com/yusing/go-proxy/watcher" + D "github.com/yusing/go-proxy/internal/docker" + E "github.com/yusing/go-proxy/internal/error" + P "github.com/yusing/go-proxy/internal/proxy" + PT "github.com/yusing/go-proxy/internal/proxy/fields" + W "github.com/yusing/go-proxy/internal/watcher" ) type ( diff --git a/src/docker/inspect.go b/internal/docker/inspect.go similarity index 88% rename from src/docker/inspect.go rename to internal/docker/inspect.go index ae7f8f5c..7ee8e5f5 100644 --- a/src/docker/inspect.go +++ b/internal/docker/inspect.go @@ -4,7 +4,7 @@ import ( "context" "time" - E "github.com/yusing/go-proxy/error" + E "github.com/yusing/go-proxy/internal/error" ) func (c Client) Inspect(containerID string) (Container, E.NestedError) { diff --git a/src/docker/label.go b/internal/docker/label.go similarity index 95% rename from src/docker/label.go rename to internal/docker/label.go index 39b288ff..8326622a 100644 --- a/src/docker/label.go +++ b/internal/docker/label.go @@ -4,9 +4,9 @@ import ( "reflect" "strings" - E "github.com/yusing/go-proxy/error" - U "github.com/yusing/go-proxy/utils" - F "github.com/yusing/go-proxy/utils/functional" + E "github.com/yusing/go-proxy/internal/error" + U "github.com/yusing/go-proxy/internal/utils" + F "github.com/yusing/go-proxy/internal/utils/functional" ) /* diff --git a/src/docker/label_parser.go b/internal/docker/label_parser.go similarity index 97% rename from src/docker/label_parser.go rename to internal/docker/label_parser.go index 91b1c348..e9769c94 100644 --- a/src/docker/label_parser.go +++ b/internal/docker/label_parser.go @@ -3,7 +3,7 @@ package docker import ( "strings" - E "github.com/yusing/go-proxy/error" + E "github.com/yusing/go-proxy/internal/error" "gopkg.in/yaml.v3" ) diff --git a/src/docker/label_parser_test.go b/internal/docker/label_parser_test.go similarity index 96% rename from src/docker/label_parser_test.go rename to internal/docker/label_parser_test.go index 4596f902..1fd9ce80 100644 --- a/src/docker/label_parser_test.go +++ b/internal/docker/label_parser_test.go @@ -4,8 +4,8 @@ import ( "fmt" "testing" - E "github.com/yusing/go-proxy/error" - . "github.com/yusing/go-proxy/utils/testing" + E "github.com/yusing/go-proxy/internal/error" + . "github.com/yusing/go-proxy/internal/utils/testing" ) func makeLabel(namespace string, alias string, field string) string { diff --git a/src/docker/label_test.go b/internal/docker/label_test.go similarity index 95% rename from src/docker/label_test.go rename to internal/docker/label_test.go index 045001ab..f8fe7aac 100644 --- a/src/docker/label_test.go +++ b/internal/docker/label_test.go @@ -4,8 +4,8 @@ import ( "fmt" "testing" - U "github.com/yusing/go-proxy/utils" - . "github.com/yusing/go-proxy/utils/testing" + U "github.com/yusing/go-proxy/internal/utils" + . "github.com/yusing/go-proxy/internal/utils/testing" ) func TestNestedLabel(t *testing.T) { diff --git a/src/docker/labels.go b/internal/docker/labels.go similarity index 100% rename from src/docker/labels.go rename to internal/docker/labels.go diff --git a/src/docker/proxy_properties.go b/internal/docker/proxy_properties.go similarity index 100% rename from src/docker/proxy_properties.go rename to internal/docker/proxy_properties.go diff --git a/src/error/builder.go b/internal/error/builder.go similarity index 100% rename from src/error/builder.go rename to internal/error/builder.go diff --git a/src/error/builder_test.go b/internal/error/builder_test.go similarity index 95% rename from src/error/builder_test.go rename to internal/error/builder_test.go index afd33ae7..de7ce06e 100644 --- a/src/error/builder_test.go +++ b/internal/error/builder_test.go @@ -3,7 +3,7 @@ package error import ( "testing" - . "github.com/yusing/go-proxy/utils/testing" + . "github.com/yusing/go-proxy/internal/utils/testing" ) func TestBuilderEmpty(t *testing.T) { diff --git a/src/error/error.go b/internal/error/error.go similarity index 80% rename from src/error/error.go rename to internal/error/error.go index fc2b246d..6286c6e5 100644 --- a/src/error/error.go +++ b/internal/error/error.go @@ -1,6 +1,7 @@ package error import ( + "encoding/json" "errors" "fmt" "strings" @@ -13,6 +14,11 @@ type ( err error extras []nestedError } + jsonNestedError struct { + Subject string + Err string + Extras []jsonNestedError + } ) func From(err error) NestedError { @@ -22,6 +28,29 @@ func From(err error) NestedError { return &nestedError{err: err} } +func FromJSON(data []byte) (NestedError, bool) { + var j jsonNestedError + if err := json.Unmarshal(data, &j); err != nil { + return nil, false + } + if j.Err == "" { + return nil, false + } + extras := make([]nestedError, len(j.Extras)) + for i, e := range j.Extras { + extra, ok := fromJSONObject(e) + if !ok { + return nil, false + } + extras[i] = *extra + } + return &nestedError{ + subject: j.Subject, + err: errors.New(j.Err), + extras: extras, + }, true +} + // Check is a helper function that // convert (T, error) to (T, NestedError). func Check[T any](obj T, err error) (T, NestedError) { @@ -157,6 +186,23 @@ func (ne NestedError) Subjectf(format string, args ...any) NestedError { return ne } +func (ne NestedError) JSONObject() jsonNestedError { + extras := make([]jsonNestedError, len(ne.extras)) + for i, e := range ne.extras { + extras[i] = e.JSONObject() + } + return jsonNestedError{ + Subject: ne.subject, + Err: ne.err.Error(), + Extras: extras, + } +} + +func (ne NestedError) JSON() []byte { + b, _ := json.MarshalIndent(ne.JSONObject(), "", " ") + return b +} + func (ne NestedError) NoError() bool { return ne == nil } @@ -169,6 +215,14 @@ func errorf(format string, args ...any) NestedError { return From(fmt.Errorf(format, args...)) } +func fromJSONObject(obj jsonNestedError) (NestedError, bool) { + data, err := json.Marshal(obj) + if err != nil { + return nil, false + } + return FromJSON(data) +} + func (ne NestedError) withError(err NestedError) NestedError { if ne != nil && err != nil { ne.extras = append(ne.extras, *err) diff --git a/src/error/error_test.go b/internal/error/error_test.go similarity index 96% rename from src/error/error_test.go rename to internal/error/error_test.go index b46cb18c..4c08a3f8 100644 --- a/src/error/error_test.go +++ b/internal/error/error_test.go @@ -4,8 +4,8 @@ import ( "errors" "testing" - . "github.com/yusing/go-proxy/error" - . "github.com/yusing/go-proxy/utils/testing" + . "github.com/yusing/go-proxy/internal/error" + . "github.com/yusing/go-proxy/internal/utils/testing" ) func TestErrorIs(t *testing.T) { diff --git a/src/error/errors.go b/internal/error/errors.go similarity index 100% rename from src/error/errors.go rename to internal/error/errors.go diff --git a/internal/http/content_type.go b/internal/http/content_type.go new file mode 100644 index 00000000..826f020e --- /dev/null +++ b/internal/http/content_type.go @@ -0,0 +1,32 @@ +package http + +import ( + "mime" + "net/http" +) + +type ContentType string + +func GetContentType(h http.Header) ContentType { + ct := h.Get("Content-Type") + if ct == "" { + return "" + } + ct, _, err := mime.ParseMediaType(ct) + if err != nil { + return "" + } + return ContentType(ct) +} + +func (ct ContentType) IsHTML() bool { + return ct == "text/html" || ct == "application/xhtml+xml" +} + +func (ct ContentType) IsJSON() bool { + return ct == "application/json" +} + +func (ct ContentType) IsPlainText() bool { + return ct == "text/plain" +} diff --git a/src/route/middleware/headers.go b/internal/http/header_utils.go similarity index 64% rename from src/route/middleware/headers.go rename to internal/http/header_utils.go index b12b134a..ab736589 100644 --- a/src/route/middleware/headers.go +++ b/internal/http/header_utils.go @@ -1,15 +1,13 @@ -package middleware +package http import ( "net/http" "slices" - - gpHTTP "github.com/yusing/go-proxy/http" ) -func removeHop(h Header) { - reqUpType := gpHTTP.UpgradeType(h) - gpHTTP.RemoveHopByHopHeaders(h) +func RemoveHop(h http.Header) { + reqUpType := UpgradeType(h) + RemoveHopByHopHeaders(h) if reqUpType != "" { h.Set("Connection", "Upgrade") @@ -19,7 +17,7 @@ func removeHop(h Header) { } } -func copyHeader(dst, src Header) { +func CopyHeader(dst, src http.Header) { for k, vv := range src { for _, v := range vv { dst.Add(k, v) @@ -27,7 +25,7 @@ func copyHeader(dst, src Header) { } } -func filterHeaders(h Header, allowed []string) { +func FilterHeaders(h http.Header, allowed []string) { if allowed == nil { return } diff --git a/src/http/modify_response_writer.go b/internal/http/modify_response_writer.go similarity index 100% rename from src/http/modify_response_writer.go rename to internal/http/modify_response_writer.go diff --git a/src/http/reverse_proxy_mod.go b/internal/http/reverse_proxy_mod.go similarity index 92% rename from src/http/reverse_proxy_mod.go rename to internal/http/reverse_proxy_mod.go index 43810a3a..57d018f5 100644 --- a/src/http/reverse_proxy_mod.go +++ b/internal/http/reverse_proxy_mod.go @@ -10,6 +10,7 @@ package http // Copyright (c) 2024 yusing import ( + "bytes" "context" "errors" "fmt" @@ -129,20 +130,13 @@ type ReverseProxy struct { // Response from the backend. It is called if the backend // returns a response at all, with any HTTP status code. // If the backend is unreachable, the optional ErrorHandler is - // called without any call to ModifyResponse. + // called before ModifyResponse. // // If ModifyResponse returns an error, ErrorHandler is called // with its error value. If ErrorHandler is nil, its default // implementation is used. ModifyResponse func(*http.Response) error - // ErrorHandler is an optional function that handles errors - // reaching the backend or errors from ModifyResponse. - // - // If nil, the default is to log the provided error and return - // a 502 Status Bad Gateway response. - ErrorHandler func(http.ResponseWriter, *http.Request, error) - ServeHTTP http.HandlerFunc } @@ -255,9 +249,11 @@ func copyHeader(dst, src http.Header) { } } -func (p *ReverseProxy) errorHandler(rw http.ResponseWriter, _ *http.Request, err error) { - logger.Errorf("http: proxy error: %s", err) - rw.WriteHeader(http.StatusBadGateway) +func (p *ReverseProxy) errorHandler(rw http.ResponseWriter, r *http.Request, err error, writeHeader bool) { + logger.Errorf("http proxy to %s failed: %s", r.URL.String(), err) + if writeHeader { + rw.WriteHeader(http.StatusBadGateway) + } } // modifyResponse conditionally runs the optional ModifyResponse hook @@ -268,7 +264,7 @@ func (p *ReverseProxy) modifyResponse(rw http.ResponseWriter, res *http.Response } if err := p.ModifyResponse(res); err != nil { res.Body.Close() - p.errorHandler(rw, req, err) + p.errorHandler(rw, req, err, true) return false } return true @@ -324,7 +320,7 @@ func (p *ReverseProxy) serveHTTP(rw http.ResponseWriter, req *http.Request) { reqUpType := UpgradeType(outreq.Header) if !IsPrint(reqUpType) { - p.errorHandler(rw, req, fmt.Errorf("client tried to switch to invalid protocol %q", reqUpType)) + p.errorHandler(rw, req, fmt.Errorf("client tried to switch to invalid protocol %q", reqUpType), true) return } @@ -384,8 +380,20 @@ func (p *ReverseProxy) serveHTTP(rw http.ResponseWriter, req *http.Request) { res, err := transport.RoundTrip(outreq) if err != nil { - p.errorHandler(rw, outreq, err) - return + p.errorHandler(rw, outreq, err, false) + errMsg := err.Error() + res = &http.Response{ + Status: http.StatusText(http.StatusBadGateway), + StatusCode: http.StatusBadGateway, + Proto: outreq.Proto, + ProtoMajor: outreq.ProtoMajor, + ProtoMinor: outreq.ProtoMinor, + Header: make(http.Header), + Body: io.NopCloser(bytes.NewReader([]byte(errMsg))), + Request: outreq, + ContentLength: int64(len(errMsg)), + TLS: outreq.TLS, + } } // Deal with 101 Switching Protocols responses: (WebSocket, h2c, etc) @@ -418,16 +426,9 @@ func (p *ReverseProxy) serveHTTP(rw http.ResponseWriter, req *http.Request) { _, err = io.Copy(rw, res.Body) if err != nil { - defer res.Body.Close() - // note: removed - // Since we're streaming the response, if we run into an error all we can do - // is abort the request. Issue 23643: ReverseProxy should use ErrAbortHandler - // on read error while copying body. - // if !shouldPanicOnCopyError(req) { - // p.logf("suppressing panic for copyResponse error in test; copy error: %s", err) - // return - // } - panic(http.ErrAbortHandler) + p.errorHandler(rw, req, err, true) + res.Body.Close() + return } res.Body.Close() // close now, instead of defer, to populate res.Trailer @@ -480,23 +481,23 @@ func (p *ReverseProxy) handleUpgradeResponse(rw http.ResponseWriter, req *http.R reqUpType := UpgradeType(req.Header) resUpType := UpgradeType(res.Header) if !IsPrint(resUpType) { // We know reqUpType is ASCII, it's checked by the caller. - p.errorHandler(rw, req, fmt.Errorf("backend tried to switch to invalid protocol %q", resUpType)) + p.errorHandler(rw, req, fmt.Errorf("backend tried to switch to invalid protocol %q", resUpType), true) } if !strings.EqualFold(reqUpType, resUpType) { - p.errorHandler(rw, req, fmt.Errorf("backend tried to switch protocol %q when %q was requested", resUpType, reqUpType)) + p.errorHandler(rw, req, fmt.Errorf("backend tried to switch protocol %q when %q was requested", resUpType, reqUpType), true) return } backConn, ok := res.Body.(io.ReadWriteCloser) if !ok { - p.errorHandler(rw, req, fmt.Errorf("internal error: 101 switching protocols response with non-writable body")) + p.errorHandler(rw, req, fmt.Errorf("internal error: 101 switching protocols response with non-writable body"), true) return } rc := http.NewResponseController(rw) conn, brw, hijackErr := rc.Hijack() if errors.Is(hijackErr, http.ErrNotSupported) { - p.errorHandler(rw, req, fmt.Errorf("can't switch protocols using non-Hijacker ResponseWriter type %T", rw)) + p.errorHandler(rw, req, fmt.Errorf("can't switch protocols using non-Hijacker ResponseWriter type %T", rw), true) return } @@ -513,7 +514,7 @@ func (p *ReverseProxy) handleUpgradeResponse(rw http.ResponseWriter, req *http.R defer close(backConnCloseCh) if hijackErr != nil { - p.errorHandler(rw, req, fmt.Errorf("hijack failed on protocol switch: %w", hijackErr)) + p.errorHandler(rw, req, fmt.Errorf("hijack failed on protocol switch: %w", hijackErr), true) return } defer conn.Close() @@ -523,11 +524,11 @@ func (p *ReverseProxy) handleUpgradeResponse(rw http.ResponseWriter, req *http.R res.Header = rw.Header() res.Body = nil // so res.Write only writes the headers; we have res.Body in backConn above if err := res.Write(brw); err != nil { - p.errorHandler(rw, req, fmt.Errorf("response write: %s", err)) + p.errorHandler(rw, req, fmt.Errorf("response write: %s", err), true) return } if err := brw.Flush(); err != nil { - p.errorHandler(rw, req, fmt.Errorf("response flush: %s", err)) + p.errorHandler(rw, req, fmt.Errorf("response flush: %s", err), true) return } errc := make(chan error, 1) diff --git a/internal/http/status_code.go b/internal/http/status_code.go new file mode 100644 index 00000000..db8002ca --- /dev/null +++ b/internal/http/status_code.go @@ -0,0 +1,7 @@ +package http + +import "net/http" + +func IsSuccess(status int) bool { + return status >= http.StatusOK && status < http.StatusMultipleChoices +} diff --git a/src/models/autocert_config.go b/internal/models/autocert_config.go similarity index 100% rename from src/models/autocert_config.go rename to internal/models/autocert_config.go diff --git a/src/models/config.go b/internal/models/config.go similarity index 100% rename from src/models/config.go rename to internal/models/config.go diff --git a/src/models/proxy_providers.go b/internal/models/proxy_providers.go similarity index 100% rename from src/models/proxy_providers.go rename to internal/models/proxy_providers.go diff --git a/src/models/raw_entry.go b/internal/models/raw_entry.go similarity index 95% rename from src/models/raw_entry.go rename to internal/models/raw_entry.go index e5971c2a..8647a980 100644 --- a/src/models/raw_entry.go +++ b/internal/models/raw_entry.go @@ -5,9 +5,9 @@ import ( "strconv" "strings" - . "github.com/yusing/go-proxy/common" - D "github.com/yusing/go-proxy/docker" - F "github.com/yusing/go-proxy/utils/functional" + . "github.com/yusing/go-proxy/internal/common" + D "github.com/yusing/go-proxy/internal/docker" + F "github.com/yusing/go-proxy/internal/utils/functional" ) type ( diff --git a/src/proxy/entry.go b/internal/proxy/entry.go similarity index 93% rename from src/proxy/entry.go rename to internal/proxy/entry.go index c752e796..3d0faacd 100644 --- a/src/proxy/entry.go +++ b/internal/proxy/entry.go @@ -5,10 +5,10 @@ import ( "net/url" "time" - D "github.com/yusing/go-proxy/docker" - E "github.com/yusing/go-proxy/error" - M "github.com/yusing/go-proxy/models" - T "github.com/yusing/go-proxy/proxy/fields" + D "github.com/yusing/go-proxy/internal/docker" + E "github.com/yusing/go-proxy/internal/error" + M "github.com/yusing/go-proxy/internal/models" + T "github.com/yusing/go-proxy/internal/proxy/fields" ) type ( diff --git a/src/proxy/fields/alias.go b/internal/proxy/fields/alias.go similarity index 100% rename from src/proxy/fields/alias.go rename to internal/proxy/fields/alias.go diff --git a/src/proxy/fields/headers.go b/internal/proxy/fields/headers.go similarity index 87% rename from src/proxy/fields/headers.go rename to internal/proxy/fields/headers.go index fd1483ed..86a9837f 100644 --- a/src/proxy/fields/headers.go +++ b/internal/proxy/fields/headers.go @@ -4,7 +4,7 @@ import ( "net/http" "strings" - E "github.com/yusing/go-proxy/error" + E "github.com/yusing/go-proxy/internal/error" ) func ValidateHTTPHeaders(headers map[string]string) (http.Header, E.NestedError) { diff --git a/src/proxy/fields/host.go b/internal/proxy/fields/host.go similarity index 77% rename from src/proxy/fields/host.go rename to internal/proxy/fields/host.go index 08399719..ca4a0f1b 100644 --- a/src/proxy/fields/host.go +++ b/internal/proxy/fields/host.go @@ -1,7 +1,7 @@ package fields import ( - E "github.com/yusing/go-proxy/error" + E "github.com/yusing/go-proxy/internal/error" ) type Host string diff --git a/src/proxy/fields/path_mode.go b/internal/proxy/fields/path_mode.go similarity index 87% rename from src/proxy/fields/path_mode.go rename to internal/proxy/fields/path_mode.go index f4d4889d..4f8f4dab 100644 --- a/src/proxy/fields/path_mode.go +++ b/internal/proxy/fields/path_mode.go @@ -1,7 +1,7 @@ package fields import ( - E "github.com/yusing/go-proxy/error" + E "github.com/yusing/go-proxy/internal/error" ) type PathMode string diff --git a/src/proxy/fields/path_pattern.go b/internal/proxy/fields/path_pattern.go similarity index 94% rename from src/proxy/fields/path_pattern.go rename to internal/proxy/fields/path_pattern.go index a95b0ce9..071b6773 100644 --- a/src/proxy/fields/path_pattern.go +++ b/internal/proxy/fields/path_pattern.go @@ -3,7 +3,7 @@ package fields import ( "regexp" - E "github.com/yusing/go-proxy/error" + E "github.com/yusing/go-proxy/internal/error" ) type PathPattern string diff --git a/src/proxy/fields/path_pattern_test.go b/internal/proxy/fields/path_pattern_test.go similarity index 88% rename from src/proxy/fields/path_pattern_test.go rename to internal/proxy/fields/path_pattern_test.go index 76d70bcb..0d2444ee 100644 --- a/src/proxy/fields/path_pattern_test.go +++ b/internal/proxy/fields/path_pattern_test.go @@ -3,8 +3,8 @@ package fields import ( "testing" - E "github.com/yusing/go-proxy/error" - U "github.com/yusing/go-proxy/utils/testing" + E "github.com/yusing/go-proxy/internal/error" + U "github.com/yusing/go-proxy/internal/utils/testing" ) var validPatterns = []string{ diff --git a/src/proxy/fields/port.go b/internal/proxy/fields/port.go similarity index 93% rename from src/proxy/fields/port.go rename to internal/proxy/fields/port.go index 10872376..a0e34ced 100644 --- a/src/proxy/fields/port.go +++ b/internal/proxy/fields/port.go @@ -3,7 +3,7 @@ package fields import ( "strconv" - E "github.com/yusing/go-proxy/error" + E "github.com/yusing/go-proxy/internal/error" ) type Port int diff --git a/src/proxy/fields/scheme.go b/internal/proxy/fields/scheme.go similarity index 91% rename from src/proxy/fields/scheme.go rename to internal/proxy/fields/scheme.go index 44e0a8e1..457b9cd7 100644 --- a/src/proxy/fields/scheme.go +++ b/internal/proxy/fields/scheme.go @@ -1,7 +1,7 @@ package fields import ( - E "github.com/yusing/go-proxy/error" + E "github.com/yusing/go-proxy/internal/error" ) type Scheme string diff --git a/src/proxy/fields/signal.go b/internal/proxy/fields/signal.go similarity index 84% rename from src/proxy/fields/signal.go rename to internal/proxy/fields/signal.go index c2bae6c4..0083b006 100644 --- a/src/proxy/fields/signal.go +++ b/internal/proxy/fields/signal.go @@ -1,7 +1,7 @@ package fields import ( - E "github.com/yusing/go-proxy/error" + E "github.com/yusing/go-proxy/internal/error" ) type Signal string diff --git a/src/proxy/fields/stop_method.go b/internal/proxy/fields/stop_method.go similarity index 89% rename from src/proxy/fields/stop_method.go rename to internal/proxy/fields/stop_method.go index 6d4e7471..bac9ad48 100644 --- a/src/proxy/fields/stop_method.go +++ b/internal/proxy/fields/stop_method.go @@ -1,7 +1,7 @@ package fields import ( - E "github.com/yusing/go-proxy/error" + E "github.com/yusing/go-proxy/internal/error" ) type StopMethod string diff --git a/src/proxy/fields/stream_port.go b/internal/proxy/fields/stream_port.go similarity index 92% rename from src/proxy/fields/stream_port.go rename to internal/proxy/fields/stream_port.go index a1418fdd..881d18e7 100644 --- a/src/proxy/fields/stream_port.go +++ b/internal/proxy/fields/stream_port.go @@ -3,8 +3,8 @@ package fields import ( "strings" - "github.com/yusing/go-proxy/common" - E "github.com/yusing/go-proxy/error" + "github.com/yusing/go-proxy/internal/common" + E "github.com/yusing/go-proxy/internal/error" ) type StreamPort struct { diff --git a/src/proxy/fields/stream_port_test.go b/internal/proxy/fields/stream_port_test.go similarity index 87% rename from src/proxy/fields/stream_port_test.go rename to internal/proxy/fields/stream_port_test.go index 39def288..fce707cf 100644 --- a/src/proxy/fields/stream_port_test.go +++ b/internal/proxy/fields/stream_port_test.go @@ -3,8 +3,8 @@ package fields import ( "testing" - E "github.com/yusing/go-proxy/error" - . "github.com/yusing/go-proxy/utils/testing" + E "github.com/yusing/go-proxy/internal/error" + . "github.com/yusing/go-proxy/internal/utils/testing" ) var validPorts = []string{ diff --git a/src/proxy/fields/stream_scheme.go b/internal/proxy/fields/stream_scheme.go similarity index 95% rename from src/proxy/fields/stream_scheme.go rename to internal/proxy/fields/stream_scheme.go index 318ad2e8..67684600 100644 --- a/src/proxy/fields/stream_scheme.go +++ b/internal/proxy/fields/stream_scheme.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - E "github.com/yusing/go-proxy/error" + E "github.com/yusing/go-proxy/internal/error" ) type StreamScheme struct { diff --git a/src/proxy/fields/timeout.go b/internal/proxy/fields/timeout.go similarity index 86% rename from src/proxy/fields/timeout.go rename to internal/proxy/fields/timeout.go index 4f70f4bb..b299beae 100644 --- a/src/proxy/fields/timeout.go +++ b/internal/proxy/fields/timeout.go @@ -3,7 +3,7 @@ package fields import ( "time" - E "github.com/yusing/go-proxy/error" + E "github.com/yusing/go-proxy/internal/error" ) func ValidateDurationPostitive(value string) (time.Duration, E.NestedError) { diff --git a/src/proxy/provider/docker.go b/internal/proxy/provider/docker.go similarity index 95% rename from src/proxy/provider/docker.go rename to internal/proxy/provider/docker.go index d16f764d..e12ca93d 100755 --- a/src/proxy/provider/docker.go +++ b/internal/proxy/provider/docker.go @@ -7,11 +7,11 @@ import ( "strings" "github.com/sirupsen/logrus" - D "github.com/yusing/go-proxy/docker" - E "github.com/yusing/go-proxy/error" - M "github.com/yusing/go-proxy/models" - R "github.com/yusing/go-proxy/route" - W "github.com/yusing/go-proxy/watcher" + D "github.com/yusing/go-proxy/internal/docker" + E "github.com/yusing/go-proxy/internal/error" + M "github.com/yusing/go-proxy/internal/models" + R "github.com/yusing/go-proxy/internal/route" + W "github.com/yusing/go-proxy/internal/watcher" ) type DockerProvider struct { diff --git a/src/proxy/provider/docker_test.go b/internal/proxy/provider/docker_test.go similarity index 96% rename from src/proxy/provider/docker_test.go rename to internal/proxy/provider/docker_test.go index 9e0185ae..45bfae29 100644 --- a/src/proxy/provider/docker_test.go +++ b/internal/proxy/provider/docker_test.go @@ -5,13 +5,13 @@ import ( "testing" "github.com/docker/docker/api/types" - "github.com/yusing/go-proxy/common" - D "github.com/yusing/go-proxy/docker" - E "github.com/yusing/go-proxy/error" - P "github.com/yusing/go-proxy/proxy" - T "github.com/yusing/go-proxy/proxy/fields" + "github.com/yusing/go-proxy/internal/common" + D "github.com/yusing/go-proxy/internal/docker" + E "github.com/yusing/go-proxy/internal/error" + P "github.com/yusing/go-proxy/internal/proxy" + T "github.com/yusing/go-proxy/internal/proxy/fields" - . "github.com/yusing/go-proxy/utils/testing" + . "github.com/yusing/go-proxy/internal/utils/testing" ) var dummyNames = []string{"/a"} diff --git a/src/proxy/provider/file.go b/internal/proxy/provider/file.go similarity index 84% rename from src/proxy/provider/file.go rename to internal/proxy/provider/file.go index 9216c3aa..a227fa71 100644 --- a/src/proxy/provider/file.go +++ b/internal/proxy/provider/file.go @@ -5,12 +5,12 @@ import ( "os" "path" - "github.com/yusing/go-proxy/common" - E "github.com/yusing/go-proxy/error" - M "github.com/yusing/go-proxy/models" - R "github.com/yusing/go-proxy/route" - U "github.com/yusing/go-proxy/utils" - W "github.com/yusing/go-proxy/watcher" + "github.com/yusing/go-proxy/internal/common" + E "github.com/yusing/go-proxy/internal/error" + M "github.com/yusing/go-proxy/internal/models" + R "github.com/yusing/go-proxy/internal/route" + U "github.com/yusing/go-proxy/internal/utils" + W "github.com/yusing/go-proxy/internal/watcher" ) type FileProvider struct { @@ -94,5 +94,5 @@ func (p *FileProvider) LoadRoutesImpl() (routes R.Routes, res E.NestedError) { } func (p *FileProvider) NewWatcher() W.Watcher { - return W.NewFileWatcher(p.fileName) + return W.NewConfigFileWatcher(p.fileName) } diff --git a/src/proxy/provider/provider.go b/internal/proxy/provider/provider.go similarity index 96% rename from src/proxy/provider/provider.go rename to internal/proxy/provider/provider.go index 3f4cafc7..e3882613 100644 --- a/src/proxy/provider/provider.go +++ b/internal/proxy/provider/provider.go @@ -5,9 +5,9 @@ import ( "path" "github.com/sirupsen/logrus" - E "github.com/yusing/go-proxy/error" - R "github.com/yusing/go-proxy/route" - W "github.com/yusing/go-proxy/watcher" + E "github.com/yusing/go-proxy/internal/error" + R "github.com/yusing/go-proxy/internal/route" + W "github.com/yusing/go-proxy/internal/watcher" ) type ( diff --git a/src/route/constants.go b/internal/route/constants.go similarity index 100% rename from src/route/constants.go rename to internal/route/constants.go diff --git a/src/route/http.go b/internal/route/http.go similarity index 85% rename from src/route/http.go rename to internal/route/http.go index 8fa20ca8..8c3710e7 100755 --- a/src/route/http.go +++ b/internal/route/http.go @@ -1,7 +1,9 @@ package route import ( + "encoding/json" "fmt" + "slices" "sync" "net/http" @@ -9,14 +11,14 @@ import ( "strings" "github.com/sirupsen/logrus" - "github.com/yusing/go-proxy/common" - "github.com/yusing/go-proxy/docker/idlewatcher" - E "github.com/yusing/go-proxy/error" - . "github.com/yusing/go-proxy/http" - P "github.com/yusing/go-proxy/proxy" - PT "github.com/yusing/go-proxy/proxy/fields" - "github.com/yusing/go-proxy/route/middleware" - F "github.com/yusing/go-proxy/utils/functional" + "github.com/yusing/go-proxy/internal/common" + "github.com/yusing/go-proxy/internal/docker/idlewatcher" + E "github.com/yusing/go-proxy/internal/error" + . "github.com/yusing/go-proxy/internal/http" + P "github.com/yusing/go-proxy/internal/proxy" + PT "github.com/yusing/go-proxy/internal/proxy/fields" + "github.com/yusing/go-proxy/internal/route/middleware" + F "github.com/yusing/go-proxy/internal/utils/functional" ) type ( @@ -170,10 +172,21 @@ func (u *URL) MarshalText() (text []byte, err error) { func ProxyHandler(w http.ResponseWriter, r *http.Request) { mux, err := findMuxFunc(r.Host) if err != nil { - http.Error(w, err.Error(), http.StatusNotFound) logrus.Error(E.Failure("request"). Subjectf("%s %s", r.Method, r.URL.String()). With(err)) + acceptTypes := r.Header.Values("Accept") + switch { + case slices.Contains(acceptTypes, "text/html"): + + case slices.Contains(acceptTypes, "application/json"): + w.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(map[string]string{ + "error": err.Error(), + }) + default: + http.Error(w, err.Error(), http.StatusNotFound) + } return } mux.ServeHTTP(w, r) diff --git a/internal/route/middleware/custom_error_page.go b/internal/route/middleware/custom_error_page.go new file mode 100644 index 00000000..e42d505b --- /dev/null +++ b/internal/route/middleware/custom_error_page.go @@ -0,0 +1,70 @@ +package middleware + +import ( + "bytes" + "fmt" + "io" + "net/http" + "path/filepath" + "strings" + + "github.com/sirupsen/logrus" + "github.com/yusing/go-proxy/internal/api/v1/error_page" + gpHTTP "github.com/yusing/go-proxy/internal/http" +) + +const staticFilePathPrefix = "/$gperrorpage/" + +var CustomErrorPage = &Middleware{ + before: func(next http.Handler, w ResponseWriter, r *Request) { + path := r.URL.Path + if path != "" && path[0] != '/' { + path = "/" + path + } + if strings.HasPrefix(path, staticFilePathPrefix) { + filename := path[len(staticFilePathPrefix):] + file, ok := error_page.GetStaticFile(filename) + if !ok { + http.NotFound(w, r) + errPageLogger.Errorf("unable to load resource %s", filename) + } else { + ext := filepath.Ext(filename) + switch ext { + case ".html": + w.Header().Set("Content-Type", "text/html; charset=utf-8") + case ".js": + w.Header().Set("Content-Type", "application/javascript; charset=utf-8") + case ".css": + w.Header().Set("Content-Type", "text/css; charset=utf-8") + default: + errPageLogger.Errorf("unexpected file type %q for %s", ext, filename) + } + w.Write(file) + } + return + } + next.ServeHTTP(w, r) + }, + modifyResponse: func(resp *Response) error { + // only handles non-success status code and html/plain content type + contentType := gpHTTP.GetContentType(resp.Header) + if !gpHTTP.IsSuccess(resp.StatusCode) && (contentType.IsHTML() || contentType.IsPlainText()) { + errorPage, ok := error_page.GetErrorPageByStatus(resp.StatusCode) + if ok { + errPageLogger.Debugf("error page for status %d loaded", resp.StatusCode) + io.Copy(io.Discard, resp.Body) // drain the original body + resp.Body.Close() + resp.Body = io.NopCloser(bytes.NewReader(errorPage)) + resp.ContentLength = int64(len(errorPage)) + resp.Header.Set("Content-Length", fmt.Sprint(len(errorPage))) + resp.Header.Set("Content-Type", "text/html; charset=utf-8") + } else { + errPageLogger.Errorf("unable to load error page for status %d", resp.StatusCode) + } + return nil + } + return nil + }, +} + +var errPageLogger = logrus.WithField("middleware", "error_page") diff --git a/src/route/middleware/forward_auth.go b/internal/route/middleware/forward_auth.go similarity index 92% rename from src/route/middleware/forward_auth.go rename to internal/route/middleware/forward_auth.go index f7e80e7c..6232bd11 100644 --- a/src/route/middleware/forward_auth.go +++ b/internal/route/middleware/forward_auth.go @@ -14,11 +14,11 @@ import ( "time" "github.com/sirupsen/logrus" - "github.com/yusing/go-proxy/common" - D "github.com/yusing/go-proxy/docker" - E "github.com/yusing/go-proxy/error" - gpHTTP "github.com/yusing/go-proxy/http" - U "github.com/yusing/go-proxy/utils" + "github.com/yusing/go-proxy/internal/common" + D "github.com/yusing/go-proxy/internal/docker" + E "github.com/yusing/go-proxy/internal/error" + gpHTTP "github.com/yusing/go-proxy/internal/http" + U "github.com/yusing/go-proxy/internal/utils" ) type ( @@ -91,7 +91,7 @@ func newForwardAuth() (fa *forwardAuth) { } func (fa *forwardAuth) forward(next http.Handler, w ResponseWriter, req *Request) { - removeHop(req.Header) + gpHTTP.RemoveHop(req.Header) faReq, err := http.NewRequestWithContext( req.Context(), @@ -105,10 +105,10 @@ func (fa *forwardAuth) forward(next http.Handler, w ResponseWriter, req *Request return } - copyHeader(faReq.Header, req.Header) - removeHop(faReq.Header) + gpHTTP.CopyHeader(faReq.Header, req.Header) + gpHTTP.RemoveHop(faReq.Header) - filterHeaders(faReq.Header, fa.AuthResponseHeaders) + gpHTTP.FilterHeaders(faReq.Header, fa.AuthResponseHeaders) fa.setAuthHeaders(req, faReq) faResp, err := fa.client.Do(faReq) @@ -127,8 +127,8 @@ func (fa *forwardAuth) forward(next http.Handler, w ResponseWriter, req *Request } if faResp.StatusCode < http.StatusOK || faResp.StatusCode >= http.StatusMultipleChoices { - copyHeader(w.Header(), faResp.Header) - removeHop(w.Header()) + gpHTTP.CopyHeader(w.Header(), faResp.Header) + gpHTTP.RemoveHop(w.Header()) redirectURL, err := faResp.Location() if err != nil { diff --git a/src/route/middleware/middleware.go b/internal/route/middleware/middleware.go similarity index 81% rename from src/route/middleware/middleware.go rename to internal/route/middleware/middleware.go index fae2d0c6..e02cb801 100644 --- a/src/route/middleware/middleware.go +++ b/internal/route/middleware/middleware.go @@ -3,9 +3,9 @@ package middleware import ( "net/http" - D "github.com/yusing/go-proxy/docker" - E "github.com/yusing/go-proxy/error" - gpHTTP "github.com/yusing/go-proxy/http" + D "github.com/yusing/go-proxy/internal/docker" + E "github.com/yusing/go-proxy/internal/error" + gpHTTP "github.com/yusing/go-proxy/internal/http" ) type ( @@ -21,7 +21,7 @@ type ( BeforeFunc func(next http.Handler, w ResponseWriter, r *Request) RewriteFunc func(req *ProxyRequest) - ModifyResponseFunc func(res *Response) error + ModifyResponseFunc func(resp *Response) error CloneWithOptFunc func(opts OptionsRaw, rp *ReverseProxy) (*Middleware, E.NestedError) OptionsRaw = map[string]any @@ -68,7 +68,7 @@ func (m *Middleware) WithOptionsClone(optsRaw OptionsRaw, rp *ReverseProxy) (*Mi func PatchReverseProxy(rp *ReverseProxy, middlewares map[string]OptionsRaw) (res E.NestedError) { befores := make([]BeforeFunc, 0, len(middlewares)) rewrites := make([]RewriteFunc, 0, len(middlewares)) - modifyResponses := make([]ModifyResponseFunc, 0, len(middlewares)) + modResps := make([]ModifyResponseFunc, 0, len(middlewares)) invalidM := E.NewBuilder("invalid middlewares") invalidOpts := E.NewBuilder("invalid options") @@ -96,7 +96,7 @@ func PatchReverseProxy(rp *ReverseProxy, middlewares map[string]OptionsRaw) (res rewrites = append(rewrites, m.rewrite) } if m.modifyResponse != nil { - modifyResponses = append(modifyResponses, m.modifyResponse) + modResps = append(modResps, m.modifyResponse) } } @@ -118,29 +118,26 @@ func PatchReverseProxy(rp *ReverseProxy, middlewares map[string]OptionsRaw) (res } if len(rewrites) > 0 { - origRewrite := rp.Rewrite + if rp.Rewrite != nil { + rewrites = append([]RewriteFunc{rp.Rewrite}, rewrites...) + } rp.Rewrite = func(req *ProxyRequest) { - if origRewrite != nil { - origRewrite(req) - } for _, rewrite := range rewrites { rewrite(req) } } } - if len(modifyResponses) > 0 { - origModifyResponse := rp.ModifyResponse + if len(modResps) > 0 { + if rp.ModifyResponse != nil { + modResps = append([]ModifyResponseFunc{rp.ModifyResponse}, modResps...) + } rp.ModifyResponse = func(res *Response) error { - if origModifyResponse != nil { - return origModifyResponse(res) + b := E.NewBuilder("errors in middleware ModifyResponse") + for _, mr := range modResps { + b.AddE(mr(res)) } - for _, modifyResponse := range modifyResponses { - if err := modifyResponse(res); err != nil { - return err - } - } - return nil + return b.Build().Error() } } diff --git a/src/route/middleware/middlewares.go b/internal/route/middleware/middlewares.go similarity index 64% rename from src/route/middleware/middlewares.go rename to internal/route/middleware/middlewares.go index ab21daa5..31b8c68e 100644 --- a/src/route/middleware/middlewares.go +++ b/internal/route/middleware/middlewares.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - D "github.com/yusing/go-proxy/docker" + D "github.com/yusing/go-proxy/internal/docker" ) var middlewares map[string]*Middleware @@ -14,15 +14,17 @@ func Get(name string) (middleware *Middleware, ok bool) { return } -// initialize middleware names +// initialize middleware names and label parsers func init() { middlewares = map[string]*Middleware{ - "set_x_forwarded": SetXForwarded, - "add_x_forwarded": AddXForwarded, - "redirect_http": RedirectHTTP, - "forward_auth": ForwardAuth.m, - "modify_response": ModifyResponse.m, - "modify_request": ModifyRequest.m, + "set_x_forwarded": SetXForwarded, + "add_x_forwarded": AddXForwarded, + "redirect_http": RedirectHTTP, + "forward_auth": ForwardAuth.m, + "modify_response": ModifyResponse.m, + "modify_request": ModifyRequest.m, + "error_page": CustomErrorPage, + "custom_error_page": CustomErrorPage, } names := make(map[*Middleware][]string) for name, m := range middlewares { diff --git a/src/route/middleware/modify_request.go b/internal/route/middleware/modify_request.go similarity index 90% rename from src/route/middleware/modify_request.go rename to internal/route/middleware/modify_request.go index 28994959..a722c7f8 100644 --- a/src/route/middleware/modify_request.go +++ b/internal/route/middleware/modify_request.go @@ -1,9 +1,9 @@ package middleware import ( - D "github.com/yusing/go-proxy/docker" - E "github.com/yusing/go-proxy/error" - U "github.com/yusing/go-proxy/utils" + D "github.com/yusing/go-proxy/internal/docker" + E "github.com/yusing/go-proxy/internal/error" + U "github.com/yusing/go-proxy/internal/utils" ) type ( diff --git a/src/route/middleware/modify_request_test.go b/internal/route/middleware/modify_request_test.go similarity index 95% rename from src/route/middleware/modify_request_test.go rename to internal/route/middleware/modify_request_test.go index c925464f..1ed4c5c0 100644 --- a/src/route/middleware/modify_request_test.go +++ b/internal/route/middleware/modify_request_test.go @@ -4,7 +4,7 @@ import ( "slices" "testing" - . "github.com/yusing/go-proxy/utils/testing" + . "github.com/yusing/go-proxy/internal/utils/testing" ) func TestSetModifyRequest(t *testing.T) { diff --git a/src/route/middleware/modify_response.go b/internal/route/middleware/modify_response.go similarity index 90% rename from src/route/middleware/modify_response.go rename to internal/route/middleware/modify_response.go index 1509b82a..ea1aafb2 100644 --- a/src/route/middleware/modify_response.go +++ b/internal/route/middleware/modify_response.go @@ -3,9 +3,9 @@ package middleware import ( "net/http" - D "github.com/yusing/go-proxy/docker" - E "github.com/yusing/go-proxy/error" - U "github.com/yusing/go-proxy/utils" + D "github.com/yusing/go-proxy/internal/docker" + E "github.com/yusing/go-proxy/internal/error" + U "github.com/yusing/go-proxy/internal/utils" ) type ( diff --git a/src/route/middleware/modify_response_test.go b/internal/route/middleware/modify_response_test.go similarity index 95% rename from src/route/middleware/modify_response_test.go rename to internal/route/middleware/modify_response_test.go index c63a1eb8..9c7a7a87 100644 --- a/src/route/middleware/modify_response_test.go +++ b/internal/route/middleware/modify_response_test.go @@ -4,7 +4,7 @@ import ( "slices" "testing" - . "github.com/yusing/go-proxy/utils/testing" + . "github.com/yusing/go-proxy/internal/utils/testing" ) func TestSetModifyResponse(t *testing.T) { diff --git a/src/route/middleware/redirect_http.go b/internal/route/middleware/redirect_http.go similarity index 88% rename from src/route/middleware/redirect_http.go rename to internal/route/middleware/redirect_http.go index e50685ed..a0e72e15 100644 --- a/src/route/middleware/redirect_http.go +++ b/internal/route/middleware/redirect_http.go @@ -3,7 +3,7 @@ package middleware import ( "net/http" - "github.com/yusing/go-proxy/common" + "github.com/yusing/go-proxy/internal/common" ) var RedirectHTTP = &Middleware{ diff --git a/src/route/middleware/redirect_http_test.go b/internal/route/middleware/redirect_http_test.go similarity index 85% rename from src/route/middleware/redirect_http_test.go rename to internal/route/middleware/redirect_http_test.go index 18b77b1b..6d2b2f65 100644 --- a/src/route/middleware/redirect_http_test.go +++ b/internal/route/middleware/redirect_http_test.go @@ -4,8 +4,8 @@ import ( "net/http" "testing" - "github.com/yusing/go-proxy/common" - . "github.com/yusing/go-proxy/utils/testing" + "github.com/yusing/go-proxy/internal/common" + . "github.com/yusing/go-proxy/internal/utils/testing" ) func TestRedirectToHTTPs(t *testing.T) { diff --git a/src/route/middleware/test_data/sample_headers.json b/internal/route/middleware/test_data/sample_headers.json similarity index 100% rename from src/route/middleware/test_data/sample_headers.json rename to internal/route/middleware/test_data/sample_headers.json diff --git a/src/route/middleware/test_utils.go b/internal/route/middleware/test_utils.go similarity index 96% rename from src/route/middleware/test_utils.go rename to internal/route/middleware/test_utils.go index f19dbd25..bb49e70a 100644 --- a/src/route/middleware/test_utils.go +++ b/internal/route/middleware/test_utils.go @@ -9,8 +9,8 @@ import ( "net/http/httptest" "net/url" - E "github.com/yusing/go-proxy/error" - gpHTTP "github.com/yusing/go-proxy/http" + E "github.com/yusing/go-proxy/internal/error" + gpHTTP "github.com/yusing/go-proxy/internal/http" ) //go:embed test_data/sample_headers.json diff --git a/src/route/middleware/x_forwarded.go b/internal/route/middleware/x_forwarded.go similarity index 100% rename from src/route/middleware/x_forwarded.go rename to internal/route/middleware/x_forwarded.go diff --git a/src/route/route.go b/internal/route/route.go similarity index 88% rename from src/route/route.go rename to internal/route/route.go index 01402892..5aaf6a5d 100755 --- a/src/route/route.go +++ b/internal/route/route.go @@ -4,10 +4,10 @@ import ( "fmt" "net/url" - E "github.com/yusing/go-proxy/error" - M "github.com/yusing/go-proxy/models" - P "github.com/yusing/go-proxy/proxy" - F "github.com/yusing/go-proxy/utils/functional" + E "github.com/yusing/go-proxy/internal/error" + M "github.com/yusing/go-proxy/internal/models" + P "github.com/yusing/go-proxy/internal/proxy" + F "github.com/yusing/go-proxy/internal/utils/functional" ) type ( diff --git a/src/route/stream.go b/internal/route/stream.go similarity index 96% rename from src/route/stream.go rename to internal/route/stream.go index 8fe6668c..af1446cb 100755 --- a/src/route/stream.go +++ b/internal/route/stream.go @@ -9,8 +9,8 @@ import ( "time" "github.com/sirupsen/logrus" - E "github.com/yusing/go-proxy/error" - P "github.com/yusing/go-proxy/proxy" + E "github.com/yusing/go-proxy/internal/error" + P "github.com/yusing/go-proxy/internal/proxy" ) type StreamRoute struct { diff --git a/src/route/tcp.go b/internal/route/tcp.go similarity index 93% rename from src/route/tcp.go rename to internal/route/tcp.go index 48da2539..4ce5901c 100755 --- a/src/route/tcp.go +++ b/internal/route/tcp.go @@ -7,8 +7,8 @@ import ( "sync" "time" - T "github.com/yusing/go-proxy/proxy/fields" - U "github.com/yusing/go-proxy/utils" + T "github.com/yusing/go-proxy/internal/proxy/fields" + U "github.com/yusing/go-proxy/internal/utils" ) const tcpDialTimeout = 5 * time.Second diff --git a/src/route/udp.go b/internal/route/udp.go similarity index 94% rename from src/route/udp.go rename to internal/route/udp.go index 41b11e2a..071d660e 100755 --- a/src/route/udp.go +++ b/internal/route/udp.go @@ -5,9 +5,9 @@ import ( "io" "net" - T "github.com/yusing/go-proxy/proxy/fields" - U "github.com/yusing/go-proxy/utils" - F "github.com/yusing/go-proxy/utils/functional" + T "github.com/yusing/go-proxy/internal/proxy/fields" + U "github.com/yusing/go-proxy/internal/utils" + F "github.com/yusing/go-proxy/internal/utils/functional" ) type ( diff --git a/src/server/instance.go b/internal/server/instance.go similarity index 100% rename from src/server/instance.go rename to internal/server/instance.go diff --git a/src/server/server.go b/internal/server/server.go similarity index 98% rename from src/server/server.go rename to internal/server/server.go index 57276dd1..d29830b9 100644 --- a/src/server/server.go +++ b/internal/server/server.go @@ -7,7 +7,7 @@ import ( "time" "github.com/sirupsen/logrus" - "github.com/yusing/go-proxy/autocert" + "github.com/yusing/go-proxy/internal/autocert" "golang.org/x/net/context" ) diff --git a/src/setup.go b/internal/setup.go similarity index 97% rename from src/setup.go rename to internal/setup.go index 4aaa95d8..aa17dd47 100644 --- a/src/setup.go +++ b/internal/setup.go @@ -1,4 +1,4 @@ -package main +package internal import ( "fmt" @@ -9,7 +9,7 @@ import ( "os" "path" - . "github.com/yusing/go-proxy/common" + . "github.com/yusing/go-proxy/internal/common" ) var branch = GetEnv("GOPROXY_BRANCH", "v0.5") diff --git a/src/utils/format.go b/internal/utils/format.go similarity index 100% rename from src/utils/format.go rename to internal/utils/format.go diff --git a/internal/utils/fs.go b/internal/utils/fs.go new file mode 100644 index 00000000..09855dfd --- /dev/null +++ b/internal/utils/fs.go @@ -0,0 +1,34 @@ +package utils + +import ( + "fmt" + "os" + "path" +) + +// Recursively lists all files in a directory until `maxDepth` is reached +// Returns a slice of file paths relative to `dir` +func ListFiles(dir string, maxDepth int) ([]string, error) { + entries, err := os.ReadDir(dir) + if err != nil { + return nil, fmt.Errorf("error listing directory %s: %w", dir, err) + } + files := make([]string, 0) + for _, entry := range entries { + if entry.IsDir() { + if maxDepth <= 0 { + continue + } + subEntries, err := ListFiles(path.Join(dir, entry.Name()), maxDepth-1) + if err != nil { + return nil, err + } + for _, subEntry := range subEntries { + files = append(files, path.Join(dir, entry.Name(), subEntry)) + } + } else { + files = append(files, path.Join(dir, entry.Name())) + } + } + return files, nil +} diff --git a/src/utils/functional/functional.go b/internal/utils/functional/functional.go similarity index 100% rename from src/utils/functional/functional.go rename to internal/utils/functional/functional.go diff --git a/src/utils/functional/map.go b/internal/utils/functional/map.go similarity index 97% rename from src/utils/functional/map.go rename to internal/utils/functional/map.go index 812cbe6b..e0d3284f 100644 --- a/src/utils/functional/map.go +++ b/internal/utils/functional/map.go @@ -4,7 +4,7 @@ import ( "github.com/puzpuzpuz/xsync/v3" "gopkg.in/yaml.v3" - E "github.com/yusing/go-proxy/error" + E "github.com/yusing/go-proxy/internal/error" ) type Map[KT comparable, VT any] struct { diff --git a/src/utils/functional/map_test.go b/internal/utils/functional/map_test.go similarity index 91% rename from src/utils/functional/map_test.go rename to internal/utils/functional/map_test.go index 031993e6..c9382a38 100644 --- a/src/utils/functional/map_test.go +++ b/internal/utils/functional/map_test.go @@ -3,8 +3,8 @@ package functional_test import ( "testing" - . "github.com/yusing/go-proxy/utils/functional" - . "github.com/yusing/go-proxy/utils/testing" + . "github.com/yusing/go-proxy/internal/utils/functional" + . "github.com/yusing/go-proxy/internal/utils/testing" ) func TestNewMapFrom(t *testing.T) { diff --git a/src/utils/functional/map_utils.go b/internal/utils/functional/map_utils.go similarity index 100% rename from src/utils/functional/map_utils.go rename to internal/utils/functional/map_utils.go diff --git a/src/utils/functional/slice.go b/internal/utils/functional/slice.go similarity index 100% rename from src/utils/functional/slice.go rename to internal/utils/functional/slice.go diff --git a/src/utils/io.go b/internal/utils/io.go similarity index 98% rename from src/utils/io.go rename to internal/utils/io.go index cd0ff84a..4274de06 100644 --- a/src/utils/io.go +++ b/internal/utils/io.go @@ -8,7 +8,7 @@ import ( "os" "syscall" - E "github.com/yusing/go-proxy/error" + E "github.com/yusing/go-proxy/internal/error" ) // TODO: move to "utils/io" diff --git a/internal/utils/must.go b/internal/utils/must.go new file mode 100644 index 00000000..897c561b --- /dev/null +++ b/internal/utils/must.go @@ -0,0 +1,8 @@ +package utils + +func Must[T any](v T, err error) T { + if err != nil { + panic(err) + } + return v +} diff --git a/src/utils/schema.go b/internal/utils/schema.go similarity index 100% rename from src/utils/schema.go rename to internal/utils/schema.go diff --git a/src/utils/serialization.go b/internal/utils/serialization.go similarity index 99% rename from src/utils/serialization.go rename to internal/utils/serialization.go index bfff55e1..b5267526 100644 --- a/src/utils/serialization.go +++ b/internal/utils/serialization.go @@ -8,7 +8,7 @@ import ( "strings" "github.com/santhosh-tekuri/jsonschema" - E "github.com/yusing/go-proxy/error" + E "github.com/yusing/go-proxy/internal/error" "gopkg.in/yaml.v3" ) diff --git a/src/utils/string.go b/internal/utils/string.go similarity index 100% rename from src/utils/string.go rename to internal/utils/string.go diff --git a/src/utils/testing/testing.go b/internal/utils/testing/testing.go similarity index 100% rename from src/utils/testing/testing.go rename to internal/utils/testing/testing.go diff --git a/internal/watcher/config_file_watcher.go b/internal/watcher/config_file_watcher.go new file mode 100644 index 00000000..ddb3ea82 --- /dev/null +++ b/internal/watcher/config_file_watcher.go @@ -0,0 +1,21 @@ +package watcher + +import ( + "context" + "sync" + + "github.com/yusing/go-proxy/internal/common" +) + +var configDirWatcher *dirWatcher +var configDirWatcherMu sync.Mutex + +// create a new file watcher for file under ConfigBasePath +func NewConfigFileWatcher(filename string) Watcher { + configDirWatcherMu.Lock() + defer configDirWatcherMu.Unlock() + if configDirWatcher == nil { + configDirWatcher = NewDirectoryWatcher(context.Background(), common.ConfigBasePath) + } + return configDirWatcher.Add(filename) +} diff --git a/internal/watcher/directory_watcher.go b/internal/watcher/directory_watcher.go new file mode 100644 index 00000000..acbd7b3f --- /dev/null +++ b/internal/watcher/directory_watcher.go @@ -0,0 +1,146 @@ +package watcher + +import ( + "context" + "errors" + "strings" + "sync" + + "github.com/fsnotify/fsnotify" + "github.com/sirupsen/logrus" + E "github.com/yusing/go-proxy/internal/error" + F "github.com/yusing/go-proxy/internal/utils/functional" + "github.com/yusing/go-proxy/internal/watcher/events" +) + +type dirWatcher struct { + dir string + w *fsnotify.Watcher + + fwMap F.Map[string, *fileWatcher] + mu sync.Mutex + + eventCh chan Event + errCh chan E.NestedError + + ctx context.Context +} + +func NewDirectoryWatcher(ctx context.Context, dirPath string) *dirWatcher { + //! subdirectories are not watched + w, err := fsnotify.NewWatcher() + if err != nil { + logrus.Panicf("unable to create fs watcher: %s", err) + } + if err = w.Add(dirPath); err != nil { + logrus.Panicf("unable to create fs watcher: %s", err) + } + helper := &dirWatcher{ + dir: dirPath, + w: w, + fwMap: F.NewMapOf[string, *fileWatcher](), + eventCh: make(chan Event), + errCh: make(chan E.NestedError), + ctx: ctx, + } + go helper.start() + return helper +} + +func (h *dirWatcher) Events(_ context.Context) (<-chan Event, <-chan E.NestedError) { + return h.eventCh, h.errCh +} + +func (h *dirWatcher) Add(relPath string) *fileWatcher { + h.mu.Lock() + defer h.mu.Unlock() + + // check if the watcher already exists + s, ok := h.fwMap.Load(relPath) + if ok { + return s + } + s = &fileWatcher{ + relPath: relPath, + eventCh: make(chan Event), + errCh: make(chan E.NestedError), + } + go func() { + defer func() { + close(s.eventCh) + close(s.errCh) + }() + for { + select { + case <-h.ctx.Done(): + return + case _, ok := <-h.eventCh: + if !ok { // directory watcher closed + return + } + } + } + }() + h.fwMap.Store(relPath, s) + return s +} + +func (h *dirWatcher) start() { + defer close(h.eventCh) + defer h.w.Close() + + for { + select { + case <-h.ctx.Done(): + return + case fsEvent, ok := <-h.w.Events: + if !ok { + return + } + // retrieve the watcher + relPath := strings.TrimPrefix(fsEvent.Name, h.dir) + relPath = strings.TrimPrefix(relPath, "/") + + msg := Event{ + Type: events.EventTypeFile, + ActorName: relPath, + } + switch { + case fsEvent.Has(fsnotify.Write): + msg.Action = events.ActionFileWritten + case fsEvent.Has(fsnotify.Create): + msg.Action = events.ActionFileCreated + case fsEvent.Has(fsnotify.Remove): + msg.Action = events.ActionFileDeleted + case fsEvent.Has(fsnotify.Rename): + msg.Action = events.ActionFileRenamed + default: // ignore other events + continue + } + + // send event to directory watcher + select { + case h.eventCh <- msg: + default: + } + + // send event to file watcher too + w, ok := h.fwMap.Load(relPath) + if ok { + select { + case w.eventCh <- msg: + default: + } + } + case err := <-h.w.Errors: + if errors.Is(err, fsnotify.ErrClosed) { + // closed manually? + return + } + select { + case h.errCh <- E.From(err): + default: + } + } + } +} diff --git a/src/watcher/docker_watcher.go b/internal/watcher/docker_watcher.go similarity index 96% rename from src/watcher/docker_watcher.go rename to internal/watcher/docker_watcher.go index c3c686ee..d9c6a999 100644 --- a/src/watcher/docker_watcher.go +++ b/internal/watcher/docker_watcher.go @@ -8,9 +8,9 @@ import ( docker_events "github.com/docker/docker/api/types/events" "github.com/docker/docker/api/types/filters" "github.com/sirupsen/logrus" - D "github.com/yusing/go-proxy/docker" - E "github.com/yusing/go-proxy/error" - "github.com/yusing/go-proxy/watcher/events" + D "github.com/yusing/go-proxy/internal/docker" + E "github.com/yusing/go-proxy/internal/error" + "github.com/yusing/go-proxy/internal/watcher/events" ) type ( diff --git a/src/watcher/events/events.go b/internal/watcher/events/events.go similarity index 84% rename from src/watcher/events/events.go rename to internal/watcher/events/events.go index a892f890..87de55d1 100644 --- a/src/watcher/events/events.go +++ b/internal/watcher/events/events.go @@ -9,9 +9,9 @@ import ( type ( Event struct { Type EventType - ActorName string - ActorID string - ActorAttributes map[string]string + ActorName string // docker: container name, file: relative file path + ActorID string // docker: container id, file: empty + ActorAttributes map[string]string // docker: container labels, file: empty Action Action } Action uint16 @@ -19,9 +19,10 @@ type ( ) const ( - ActionFileModified Action = (1 << iota) + ActionFileWritten Action = (1 << iota) ActionFileCreated ActionFileDeleted + ActionFileRenamed ActionContainerCreate ActionContainerStart diff --git a/internal/watcher/file_watcher.go b/internal/watcher/file_watcher.go new file mode 100644 index 00000000..4bd310b8 --- /dev/null +++ b/internal/watcher/file_watcher.go @@ -0,0 +1,17 @@ +package watcher + +import ( + "context" + + E "github.com/yusing/go-proxy/internal/error" +) + +type fileWatcher struct { + relPath string + eventCh chan Event + errCh chan E.NestedError +} + +func (fw *fileWatcher) Events(ctx context.Context) (<-chan Event, <-chan E.NestedError) { + return fw.eventCh, fw.errCh +} diff --git a/src/watcher/watcher.go b/internal/watcher/watcher.go similarity index 61% rename from src/watcher/watcher.go rename to internal/watcher/watcher.go index 17b70dd8..fa39951d 100644 --- a/src/watcher/watcher.go +++ b/internal/watcher/watcher.go @@ -3,8 +3,8 @@ package watcher import ( "context" - E "github.com/yusing/go-proxy/error" - "github.com/yusing/go-proxy/watcher/events" + E "github.com/yusing/go-proxy/internal/error" + "github.com/yusing/go-proxy/internal/watcher/events" ) type Event = events.Event diff --git a/src/api/handler.go b/src/api/handler.go deleted file mode 100644 index d25d47da..00000000 --- a/src/api/handler.go +++ /dev/null @@ -1,30 +0,0 @@ -package api - -import ( - "net/http" - - v1 "github.com/yusing/go-proxy/api/v1" - "github.com/yusing/go-proxy/config" -) - -func NewHandler(cfg *config.Config) http.Handler { - mux := http.NewServeMux() - mux.HandleFunc("GET /v1", v1.Index) - mux.HandleFunc("GET /v1/checkhealth", wrap(cfg, v1.CheckHealth)) - mux.HandleFunc("HEAD /v1/checkhealth", wrap(cfg, v1.CheckHealth)) - mux.HandleFunc("POST /v1/reload", wrap(cfg, v1.Reload)) - mux.HandleFunc("GET /v1/list", wrap(cfg, v1.List)) - mux.HandleFunc("GET /v1/list/{what}", wrap(cfg, v1.List)) - mux.HandleFunc("GET /v1/file", v1.GetFileContent) - mux.HandleFunc("GET /v1/file/{filename}", v1.GetFileContent) - mux.HandleFunc("POST /v1/file/{filename}", v1.SetFileContent) - mux.HandleFunc("PUT /v1/file/{filename}", v1.SetFileContent) - mux.HandleFunc("GET /v1/stats", wrap(cfg, v1.Stats)) - return mux -} - -func wrap(cfg *config.Config, f func(cfg *config.Config, w http.ResponseWriter, r *http.Request)) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - f(cfg, w, r) - } -} diff --git a/src/api/v1/reload.go b/src/api/v1/reload.go deleted file mode 100644 index 44efaf59..00000000 --- a/src/api/v1/reload.go +++ /dev/null @@ -1,16 +0,0 @@ -package v1 - -import ( - "net/http" - - U "github.com/yusing/go-proxy/api/v1/utils" - "github.com/yusing/go-proxy/config" -) - -func Reload(cfg *config.Config, w http.ResponseWriter, r *http.Request) { - if err := cfg.Reload().Error(); err != nil { - U.HandleErr(w, r, err) - return - } - w.WriteHeader(http.StatusOK) -} diff --git a/src/api/v1/utils/net.go b/src/api/v1/utils/net.go deleted file mode 100644 index a9c7743d..00000000 --- a/src/api/v1/utils/net.go +++ /dev/null @@ -1,62 +0,0 @@ -package utils - -import ( - "crypto/tls" - "fmt" - "net" - "net/http" - - "github.com/yusing/go-proxy/common" - E "github.com/yusing/go-proxy/error" -) - -func IsSiteHealthy(url string) bool { - // try HEAD first - // if HEAD is not allowed, try GET - resp, err := HttpClient.Head(url) - if resp != nil { - resp.Body.Close() - } - if err != nil && resp != nil && resp.StatusCode == http.StatusMethodNotAllowed { - _, err = HttpClient.Get(url) - } - if resp != nil { - resp.Body.Close() - } - return err == nil -} - -func IsStreamHealthy(scheme, address string) bool { - conn, err := net.DialTimeout(scheme, address, common.DialTimeout) - if err != nil { - return false - } - conn.Close() - return true -} - -func ReloadServer() E.NestedError { - resp, err := HttpClient.Post(fmt.Sprintf("http://localhost%v/reload", common.APIHTTPAddr), "", nil) - if err != nil { - return E.From(err) - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return E.Failure("server reload").Subjectf("status code: %v", resp.StatusCode) - } - return nil -} - -var HttpClient = &http.Client{ - Timeout: common.ConnectionTimeout, - Transport: &http.Transport{ - Proxy: http.ProxyFromEnvironment, - DisableKeepAlives: true, - ForceAttemptHTTP2: true, - DialContext: (&net.Dialer{ - Timeout: common.DialTimeout, - KeepAlive: common.KeepAlive, // this is different from DisableKeepAlives - }).DialContext, - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - }, -} diff --git a/src/common/env.go b/src/common/env.go deleted file mode 100644 index ed9ac593..00000000 --- a/src/common/env.go +++ /dev/null @@ -1,41 +0,0 @@ -package common - -import ( - "net" - "os" - - "github.com/sirupsen/logrus" - U "github.com/yusing/go-proxy/utils" -) - -var ( - NoSchemaValidation = GetEnvBool("GOPROXY_NO_SCHEMA_VALIDATION") - IsDebug = GetEnvBool("GOPROXY_DEBUG") - ProxyHTTPAddr = GetEnv("GOPROXY_HTTP_ADDR", ":80") - ProxyHTTPSAddr = GetEnv("GOPROXY_HTTPS_ADDR", ":443") - APIHTTPAddr = GetEnv("GOPROXY_API_ADDR", "127.0.0.1:8888") - - ProxyHTTPPort = getPort(ProxyHTTPAddr) - ProxyHTTPSPort = getPort(ProxyHTTPSAddr) - ProxyAPIPort = getPort(APIHTTPAddr) -) - -func GetEnvBool(key string) bool { - return U.ParseBool(os.Getenv(key)) -} - -func GetEnv(key string, defaultValue string) string { - value, ok := os.LookupEnv(key) - if !ok { - value = defaultValue - } - return value -} - -func getPort(addr string) string { - _, port, err := net.SplitHostPort(addr) - if err != nil { - logrus.Fatalf("Invalid address: %s", addr) - } - return port -} diff --git a/src/watcher/file_watcher.go b/src/watcher/file_watcher.go deleted file mode 100644 index 2ce79398..00000000 --- a/src/watcher/file_watcher.go +++ /dev/null @@ -1,29 +0,0 @@ -package watcher - -import ( - "context" - "path" - - "github.com/yusing/go-proxy/common" - E "github.com/yusing/go-proxy/error" -) - -type fileWatcher struct { - filename string -} - -func NewFileWatcher(filename string) Watcher { - if path.Base(filename) != filename { - panic("filename must be a relative path") - } - return &fileWatcher{filename: filename} -} - -func (f *fileWatcher) Events(ctx context.Context) (<-chan Event, <-chan E.NestedError) { - if fwHelper == nil { - fwHelper = newFileWatcherHelper(common.ConfigBasePath) - } - return fwHelper.Add(ctx, f) -} - -var fwHelper *fileWatcherHelper diff --git a/src/watcher/file_watcher_helper.go b/src/watcher/file_watcher_helper.go deleted file mode 100644 index 351b7035..00000000 --- a/src/watcher/file_watcher_helper.go +++ /dev/null @@ -1,124 +0,0 @@ -package watcher - -import ( - "context" - "errors" - "path" - "sync" - - "github.com/fsnotify/fsnotify" - "github.com/sirupsen/logrus" - E "github.com/yusing/go-proxy/error" - "github.com/yusing/go-proxy/watcher/events" -) - -type fileWatcherHelper struct { - w *fsnotify.Watcher - m map[string]*fileWatcherStream - wg sync.WaitGroup - mu sync.Mutex -} - -type fileWatcherStream struct { - *fileWatcher - stopped chan struct{} - eventCh chan Event - errCh chan E.NestedError -} - -func newFileWatcherHelper(dirPath string) *fileWatcherHelper { - w, err := fsnotify.NewWatcher() - if err != nil { - logrus.Panicf("unable to create fs watcher: %s", err) - } - if err = w.Add(dirPath); err != nil { - logrus.Panicf("unable to create fs watcher: %s", err) - } - helper := &fileWatcherHelper{ - w: w, - m: make(map[string]*fileWatcherStream), - } - go helper.start() - return helper -} - -func (h *fileWatcherHelper) Add(ctx context.Context, w *fileWatcher) (<-chan Event, <-chan E.NestedError) { - h.mu.Lock() - defer h.mu.Unlock() - - // check if the watcher already exists - s, ok := h.m[w.filename] - if ok { - return s.eventCh, s.errCh - } - s = &fileWatcherStream{ - fileWatcher: w, - stopped: make(chan struct{}), - eventCh: make(chan Event), - errCh: make(chan E.NestedError), - } - go func() { - for { - select { - case <-ctx.Done(): - s.stopped <- struct{}{} - case <-s.stopped: - h.mu.Lock() - defer h.mu.Unlock() - close(s.eventCh) - close(s.errCh) - delete(h.m, w.filename) - return - } - } - }() - h.m[w.filename] = s - return s.eventCh, s.errCh -} - -func (h *fileWatcherHelper) start() { - defer h.wg.Done() - - for { - select { - case fsEvent, ok := <-h.w.Events: - if !ok { - // closed manually? - fsLogger.Error("channel closed") - return - } - // retrieve the watcher - w, ok := h.m[path.Base(fsEvent.Name)] - if !ok { - // watcher for this file does not exist - continue - } - - msg := Event{ - Type: events.EventTypeFile, - ActorName: w.filename, - } - switch { - case fsEvent.Has(fsnotify.Create): - msg.Action = events.ActionFileCreated - case fsEvent.Has(fsnotify.Write): - msg.Action = events.ActionFileModified - case fsEvent.Has(fsnotify.Remove), fsEvent.Has(fsnotify.Rename): - msg.Action = events.ActionFileDeleted - default: // ignore other events - continue - } - - // send event - w.eventCh <- msg - case err := <-h.w.Errors: - if errors.Is(err, fsnotify.ErrClosed) { - // closed manually? - return - } - fsLogger.Error(err) - } - } -} - -var fsLogger = logrus.WithField("module", "fsnotify")