From 4d6fdfca123a832ed34e9f7aa42662d51f338811 Mon Sep 17 00:00:00 2001 From: Cedric Staub Date: Thu, 11 Dec 2025 20:05:11 -0800 Subject: [PATCH 01/11] Add new distroless image (w/o pkcs11 support) --- .github/workflows/compile.yml | 2 ++ .github/workflows/docker.yml | 6 ++++++ Dockerfile-alpine | 2 +- Dockerfile-debian | 2 +- Dockerfile-distroless | 27 +++++++++++++++++++++++++++ main.go | 2 +- 6 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 Dockerfile-distroless diff --git a/.github/workflows/compile.yml b/.github/workflows/compile.yml index 7dd6855047..c867fd09ab 100644 --- a/.github/workflows/compile.yml +++ b/.github/workflows/compile.yml @@ -25,6 +25,8 @@ jobs: run: 'docker buildx build -f Dockerfile-alpine .' - name: Build container (debian) run: 'docker buildx build -f Dockerfile-debian .' + - name: Build container (distroless) + run: 'docker buildx build -f Dockerfile-distroless .' build-linux: name: Build (Linux) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 7421c3d57e..0ecd21dc41 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -33,9 +33,15 @@ jobs: - name: Build container (debian/latest) if: ${{ github.ref == 'refs/heads/master' }} run: 'docker buildx build -f Dockerfile-debian --push -t ghostunnel/ghostunnel:latest-debian --platform linux/amd64,linux/arm64,linux/arm/v7 .' + - name: Build container (distroless/latest) + if: ${{ github.ref == 'refs/heads/master' }} + run: 'docker buildx build -f Dockerfile-distroless --push -t ghostunnel/ghostunnel:latest-distroless --platform linux/amd64,linux/arm64,linux/arm/v7 .' - name: Build container (alpine/tagged) if: ${{ github.ref != 'refs/heads/master' }} run: 'docker buildx build -f Dockerfile-alpine --push -t ghostunnel/ghostunnel:$(git describe --tags --abbrev=0) -t ghostunnel/ghostunnel:$(git describe --tags --abbrev=0)-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 .' - name: Build container (debian/tagged) if: ${{ github.ref != 'refs/heads/master' }} run: 'docker buildx build -f Dockerfile-debian --push -t ghostunnel/ghostunnel:$(git describe --tags --abbrev=0)-debian --platform linux/amd64,linux/arm64,linux/arm/v7 .' + - name: Build container (distroless/tagged) + if: ${{ github.ref != 'refs/heads/master' }} + run: 'docker buildx build -f Dockerfile-distroless --push -t ghostunnel/ghostunnel:$(git describe --tags --abbrev=0)-debian --platform linux/amd64,linux/arm64,linux/arm/v7 .' diff --git a/Dockerfile-alpine b/Dockerfile-alpine index 551c93cbdb..16380af026 100644 --- a/Dockerfile-alpine +++ b/Dockerfile-alpine @@ -16,7 +16,7 @@ COPY . /go/src/github.com/ghostunnel/ghostunnel # Build RUN cd /go/src/github.com/ghostunnel/ghostunnel && \ - GO111MODULE=on make clean ghostunnel && \ + make clean ghostunnel && \ cp ghostunnel /usr/bin/ghostunnel # Create a multi-stage build with the binary diff --git a/Dockerfile-debian b/Dockerfile-debian index 5bf728a2ed..dfc4e7f66a 100644 --- a/Dockerfile-debian +++ b/Dockerfile-debian @@ -16,7 +16,7 @@ COPY . /go/src/github.com/ghostunnel/ghostunnel # Build RUN cd /go/src/github.com/ghostunnel/ghostunnel && \ - GO111MODULE=on make clean ghostunnel && \ + make clean ghostunnel && \ cp ghostunnel /usr/bin/ghostunnel # Create a multi-stage build with the binary diff --git a/Dockerfile-distroless b/Dockerfile-distroless new file mode 100644 index 0000000000..259d49e561 --- /dev/null +++ b/Dockerfile-distroless @@ -0,0 +1,27 @@ +# Dockerfile for ghostunnel/ghostunnel built on Debian. +# +# To build this image: +# docker build -t ghostunnel/ghostunnel . +# +# To run ghostunnel from the image (for example): +# docker run --rm ghostunnel/ghostunnel --version + +FROM golang:1.25-bookworm AS build + +# Dependencies +RUN apt update && apt install -y gcc make git + +# Copy source +COPY . /go/src/github.com/ghostunnel/ghostunnel + +# Build +RUN cd /go/src/github.com/ghostunnel/ghostunnel && \ + GOFLAGS="-tags=nopkcs11" make clean ghostunnel && \ + cp ghostunnel /usr/bin/ghostunnel + +# Create a multi-stage build with the binary +FROM gcr.io/distroless/base-nossl:nonroot + +COPY --from=build /usr/bin/ghostunnel /usr/bin/ghostunnel + +ENTRYPOINT ["/usr/bin/ghostunnel"] diff --git a/main.go b/main.go index f07ac6eac6..99821ecefb 100644 --- a/main.go +++ b/main.go @@ -416,7 +416,7 @@ func main() { func run(args []string) error { runtime.GOMAXPROCS(runtime.NumCPU()) - app.Version(fmt.Sprintf("rev %s built with %s", version, runtime.Version())) + app.Version(fmt.Sprintf("rev %s built with %s (pkcs11: %v, keychain: %v)", version, runtime.Version(), certloader.SupportsPKCS11(), certloader.SupportsKeychain())) app.Validate(validateFlags) app.UsageTemplate(kingpin.LongHelpTemplate) command := kingpin.MustParse(app.Parse(args)) From 86ac0f405507ed72e5d87d8b3500bc277b41febf Mon Sep 17 00:00:00 2001 From: Cedric Staub Date: Fri, 12 Dec 2025 11:58:06 -0800 Subject: [PATCH 02/11] Migrate from make to mage --- .github/workflows/compile.yml | 46 +- .github/workflows/docker.yml | 36 +- .github/workflows/release.yml | 33 +- .github/workflows/test.yml | 67 ++- .gitignore | 2 + CONTRIBUTING.md | 2 +- Dockerfile-alpine | 8 +- Dockerfile-debian | 8 +- Dockerfile-distroless | 8 +- Dockerfile-test | 15 +- Makefile | 70 --- README.md | 34 +- go.mod | 3 + go.sum | 2 + magefile.go | 461 ++++++++++++++++++ test-keys/Makefile | 38 -- test-keys/README.md | 6 - test-keys/cacert.pem | 17 - test-keys/client-cert.pem | 19 - test-keys/client-combined.pem | 46 -- test-keys/client-key.pem | 27 - test-keys/client-keystore.p12 | Bin 2381 -> 0 bytes test-keys/client-pkcs8.pem | 28 -- test-keys/openssl.ext | 9 - test-keys/root-cert.pem | 17 - test-keys/root-combined.pem | 44 -- test-keys/root-key.pem | 27 - test-keys/server-cert.pem | 19 - test-keys/server-combined.pem | 46 -- test-keys/server-key.pem | 27 - test-keys/server-keystore.p12 | Bin 2381 -> 0 bytes test-keys/server-pkcs8.pem | 28 -- vendor/github.com/wadey/gocovmerge/LICENSE | 22 + vendor/github.com/wadey/gocovmerge/README.md | 16 + .../github.com/wadey/gocovmerge/gocovmerge.go | 111 +++++ vendor/golang.org/x/tools/cover/profile.go | 266 ++++++++++ vendor/modules.txt | 4 + 37 files changed, 1044 insertions(+), 568 deletions(-) delete mode 100644 Makefile create mode 100644 magefile.go delete mode 100644 test-keys/Makefile delete mode 100644 test-keys/README.md delete mode 100644 test-keys/cacert.pem delete mode 100644 test-keys/client-cert.pem delete mode 100644 test-keys/client-combined.pem delete mode 100644 test-keys/client-key.pem delete mode 100644 test-keys/client-keystore.p12 delete mode 100644 test-keys/client-pkcs8.pem delete mode 100644 test-keys/openssl.ext delete mode 100644 test-keys/root-cert.pem delete mode 100644 test-keys/root-combined.pem delete mode 100644 test-keys/root-key.pem delete mode 100644 test-keys/server-cert.pem delete mode 100644 test-keys/server-combined.pem delete mode 100644 test-keys/server-key.pem delete mode 100644 test-keys/server-keystore.p12 delete mode 100644 test-keys/server-pkcs8.pem create mode 100644 vendor/github.com/wadey/gocovmerge/LICENSE create mode 100644 vendor/github.com/wadey/gocovmerge/README.md create mode 100644 vendor/github.com/wadey/gocovmerge/gocovmerge.go create mode 100644 vendor/golang.org/x/tools/cover/profile.go diff --git a/.github/workflows/compile.yml b/.github/workflows/compile.yml index c867fd09ab..5a1475e426 100644 --- a/.github/workflows/compile.yml +++ b/.github/workflows/compile.yml @@ -21,12 +21,19 @@ jobs: uses: docker/setup-buildx-action@v3 with: install: true - - name: Build container (alpine) - run: 'docker buildx build -f Dockerfile-alpine .' - - name: Build container (debian) - run: 'docker buildx build -f Dockerfile-debian .' - - name: Build container (distroless) - run: 'docker buildx build -f Dockerfile-distroless .' + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version: '1.25.x' + - name: Set up Mage + uses: magefile/mage-action@v3 + with: + install-only: true + version: v1.14.0 + - name: Build containers + run: mage -v docker:build + env: + GITHUB_REF: ${{ github.ref }} build-linux: name: Build (Linux) @@ -50,12 +57,16 @@ jobs: with: fetch-depth: 100 fetch-tags: true + - name: Set up Mage + uses: magefile/mage-action@v3 + with: + install-only: true + version: v1.14.0 - name: Build binary run: | - CGO_ENABLED=1 GOARCH=amd64 make ghostunnel + CGO_ENABLED=1 GOARCH=amd64 mage -v go:build mv ghostunnel ghostunnel-linux-amd64 - make clean - CGO_ENABLED=1 GOARCH=arm64 CC=aarch64-linux-gnu-gcc make ghostunnel + CGO_ENABLED=1 GOARCH=arm64 CC=aarch64-linux-gnu-gcc mage -v go:build mv ghostunnel ghostunnel-linux-arm64 - name: Upload artifact uses: actions/upload-artifact@v5 @@ -85,12 +96,16 @@ jobs: with: fetch-depth: 100 fetch-tags: true + - name: Set up Mage + uses: magefile/mage-action@v3 + with: + install-only: true + version: v1.14.0 - name: Build binary run: | - CGO_ENABLED=1 GOARCH=amd64 make ghostunnel + CGO_ENABLED=1 GOARCH=amd64 mage -v go:build mv ghostunnel ghostunnel-darwin-amd64 - make clean - CGO_ENABLED=1 GOARCH=arm64 make ghostunnel + CGO_ENABLED=1 GOARCH=arm64 mage -v go:build mv ghostunnel ghostunnel-darwin-arm64 lipo -create -output ghostunnel-darwin-universal ghostunnel-darwin-amd64 ghostunnel-darwin-arm64 - name: Upload artifact @@ -126,8 +141,13 @@ jobs: with: fetch-depth: 100 fetch-tags: true + - name: Set up Mage + uses: magefile/mage-action@v3 + with: + install-only: true + version: v1.14.0 - name: Build binary - run: make ghostunnel + run: mage -v go:build - name: Upload artifact uses: actions/upload-artifact@v5 with: diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 0ecd21dc41..0cb5368747 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -1,5 +1,7 @@ --- name: Docker +permissions: + contents: read on: push: @@ -8,7 +10,7 @@ on: jobs: buildx: - name: Build container + name: Build and publish containers runs-on: ubuntu-24.04 steps: - name: Checkout @@ -22,26 +24,22 @@ jobs: uses: docker/setup-buildx-action@v3 with: install: true + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version: '1.25.x' - name: Login to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: Build container (alpine/latest) - if: ${{ github.ref == 'refs/heads/master' }} - run: 'docker buildx build -f Dockerfile-alpine --push -t ghostunnel/ghostunnel:latest -t ghostunnel/ghostunnel:latest-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 .' - - name: Build container (debian/latest) - if: ${{ github.ref == 'refs/heads/master' }} - run: 'docker buildx build -f Dockerfile-debian --push -t ghostunnel/ghostunnel:latest-debian --platform linux/amd64,linux/arm64,linux/arm/v7 .' - - name: Build container (distroless/latest) - if: ${{ github.ref == 'refs/heads/master' }} - run: 'docker buildx build -f Dockerfile-distroless --push -t ghostunnel/ghostunnel:latest-distroless --platform linux/amd64,linux/arm64,linux/arm/v7 .' - - name: Build container (alpine/tagged) - if: ${{ github.ref != 'refs/heads/master' }} - run: 'docker buildx build -f Dockerfile-alpine --push -t ghostunnel/ghostunnel:$(git describe --tags --abbrev=0) -t ghostunnel/ghostunnel:$(git describe --tags --abbrev=0)-alpine --platform linux/amd64,linux/arm64,linux/arm/v7 .' - - name: Build container (debian/tagged) - if: ${{ github.ref != 'refs/heads/master' }} - run: 'docker buildx build -f Dockerfile-debian --push -t ghostunnel/ghostunnel:$(git describe --tags --abbrev=0)-debian --platform linux/amd64,linux/arm64,linux/arm/v7 .' - - name: Build container (distroless/tagged) - if: ${{ github.ref != 'refs/heads/master' }} - run: 'docker buildx build -f Dockerfile-distroless --push -t ghostunnel/ghostunnel:$(git describe --tags --abbrev=0)-debian --platform linux/amd64,linux/arm64,linux/arm/v7 .' + - name: Set up Mage + uses: magefile/mage-action@v3 + with: + install-only: true + version: v1.14.0 + - name: Build and publish containers + run: mage -v docker:push + env: + GITHUB_REF: ${{ github.ref }} + DOCKER_PLATFORMS: "linux/amd64,linux/arm64,linux/arm/v7" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3e1b4fd0b1..a3fcc4d181 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,12 +26,18 @@ jobs: uses: actions/checkout@v6 with: fetch-depth: 0 + - name: Set up Mage + uses: magefile/mage-action@v3 + with: + install-only: true + version: v1.14.0 - name: Build binary + env: + VERSION: ${{ github.ref_name }} run: | - CGO_ENABLED=1 GOARCH=amd64 make VERSION=$GITHUB_REF_NAME ghostunnel + CGO_ENABLED=1 GOARCH=amd64 mage -v go:build mv ghostunnel ghostunnel-linux-amd64 - make clean - CGO_ENABLED=1 GOARCH=arm64 CC=aarch64-linux-gnu-gcc make VERSION=$GITHUB_REF_NAME ghostunnel + CGO_ENABLED=1 GOARCH=arm64 CC=aarch64-linux-gnu-gcc mage -v go:build mv ghostunnel ghostunnel-linux-arm64 - name: Upload artifact uses: actions/upload-artifact@v5 @@ -60,12 +66,18 @@ jobs: uses: actions/checkout@v6 with: fetch-depth: 0 + - name: Set up Mage + uses: magefile/mage-action@v3 + with: + install-only: true + version: v1.14.0 - name: Build binary + env: + VERSION: ${{ github.ref_name }} run: | - CGO_ENABLED=1 GOARCH=amd64 make VERSION=$GITHUB_REF_NAME ghostunnel + CGO_ENABLED=1 GOARCH=amd64 mage -v go:build mv ghostunnel ghostunnel-darwin-amd64 - make clean - CGO_ENABLED=1 GOARCH=arm64 make VERSION=$GITHUB_REF_NAME ghostunnel + CGO_ENABLED=1 GOARCH=arm64 mage -v go:build mv ghostunnel ghostunnel-darwin-arm64 lipo -create -output ghostunnel-darwin-universal ghostunnel-darwin-amd64 ghostunnel-darwin-arm64 - name: Upload artifact @@ -100,9 +112,16 @@ jobs: uses: actions/checkout@v6 with: fetch-depth: 0 + - name: Set up Mage + uses: magefile/mage-action@v3 + with: + install-only: true + version: v1.14.0 - name: Build binary + env: + VERSION: ${{ github.ref_name }} run: | - make VERSION=$GITHUB_REF_NAME ghostunnel + mage -v go:build mv ghostunnel ghostunnel-windows-amd64.exe - name: Upload artifact uses: actions/upload-artifact@v5 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 84327348d9..9bb5f8a6fa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,5 +1,7 @@ --- name: Test +permissions: + contents: read on: push: @@ -8,8 +10,8 @@ on: branches: [ master ] jobs: - build: - name: Unit tests + test: + name: Tests strategy: matrix: version: [1.25.x] @@ -23,64 +25,57 @@ jobs: id: go - name: Checkout uses: actions/checkout@v6 - - name: Build binary - run: make ghostunnel + - name: Set up Python + if: matrix.os != 'windows-latest' + uses: actions/setup-python@v6 + with: + python-version: '3.11.x' + - name: Set up Mage + uses: magefile/mage-action@v3 + with: + install-only: true + version: v1.14.0 - name: Run tests - run: make unit - - integration-linux: - name: Integration tests (Linux) - strategy: - matrix: - version: [1.25] - os: [ubuntu-22.04, ubuntu-24.04] - runs-on: ${{ matrix.os }} - steps: - - name: Checkout - uses: actions/checkout@v6 - - name: Run tests - run: GO_VERSION=${{ matrix.version }} make docker-test + run: mage -v test:all - name: Codecov upload + if: ${{ github.actor != 'dependabot[bot]' && matrix.os != 'windows-latest' }} uses: codecov/codecov-action@v5 - if: ${{ github.actor != 'dependabot[bot]' }} with: files: ./coverage/all.profile - flags: linux + flags: ${{ contains(matrix.os, 'ubuntu') && 'linux' || (contains(matrix.os, 'macos') && 'darwin' || 'windows') }} fail_ci_if_error: true verbose: true token: ${{ secrets.CODECOV_TOKEN }} env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - - integration-darwin: - name: Integration tests (Darwin) + + test-docker: + name: Tests (PKCS#11/SoftHSM) strategy: matrix: - version: [1.25.x] - os: [macos-latest] + version: [1.25] + os: [ubuntu-24.04] runs-on: ${{ matrix.os }} steps: + - name: Checkout + uses: actions/checkout@v6 - name: Set up Go uses: actions/setup-go@v6 with: go-version: ${{ matrix.version }} - id: go - - name: Set up Python - uses: actions/setup-python@v6 + - name: Set up Mage + uses: magefile/mage-action@v3 with: - python-version: '3.11.x' - - name: Install gocovmerge - run: go install github.com/wadey/gocovmerge@latest - - name: Checkout - uses: actions/checkout@v6 - - name: Run tests - run: make test + install-only: true + version: v1.14.0 + - name: Run tests in Docker + run: GO_VERSION=${{ matrix.version }} mage -v test:docker - name: Codecov upload uses: codecov/codecov-action@v5 if: ${{ github.actor != 'dependabot[bot]' }} with: files: ./coverage/all.profile - flags: darwin + flags: linux fail_ci_if_error: true verbose: true token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.gitignore b/.gitignore index d03d319944..15f73bddf8 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ *.crt *.out ghostunnel +ghostunnel.man ghostunnel.exe ghostunnel.test ghostunnel.certstore @@ -20,3 +21,4 @@ dist/ .idea bazel-* coverage/ +test-keys/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b174cd5de2..9f463986d7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,7 +5,7 @@ by forking the repository and sending a pull request. When submitting code, please make efforts to follow existing conventions and style in order to keep the code as readable as possible. Please also make sure -all tests pass by running `make test`, and format your code with `go fmt`. +all tests pass by running `mage test`, and format your code with `go fmt`. Note that ghostunnel relies heavily on integration tests written in Python that run checks on a live instance. If you are adding new features or changing diff --git a/Dockerfile-alpine b/Dockerfile-alpine index 16380af026..d16d98086a 100644 --- a/Dockerfile-alpine +++ b/Dockerfile-alpine @@ -9,14 +9,16 @@ FROM golang:1.25-alpine AS build # Dependencies -RUN apk add --no-cache --update gcc musl-dev libtool make git +RUN apk add --no-cache --update gcc musl-dev libtool git # Copy source COPY . /go/src/github.com/ghostunnel/ghostunnel -# Build +# Install mage and build RUN cd /go/src/github.com/ghostunnel/ghostunnel && \ - make clean ghostunnel && \ + go install github.com/magefile/mage@latest && \ + export PATH=$PATH:$(go env GOPATH)/bin && \ + mage -v go:build && \ cp ghostunnel /usr/bin/ghostunnel # Create a multi-stage build with the binary diff --git a/Dockerfile-debian b/Dockerfile-debian index dfc4e7f66a..0c6546457e 100644 --- a/Dockerfile-debian +++ b/Dockerfile-debian @@ -9,14 +9,16 @@ FROM golang:1.25-bookworm AS build # Dependencies -RUN apt update && apt install -y gcc libtool make git +RUN apt update && apt install -y gcc libtool git # Copy source COPY . /go/src/github.com/ghostunnel/ghostunnel -# Build +# Install mage and build RUN cd /go/src/github.com/ghostunnel/ghostunnel && \ - make clean ghostunnel && \ + go install github.com/magefile/mage@latest && \ + export PATH=$PATH:$(go env GOPATH)/bin && \ + mage -v go:build && \ cp ghostunnel /usr/bin/ghostunnel # Create a multi-stage build with the binary diff --git a/Dockerfile-distroless b/Dockerfile-distroless index 259d49e561..54c7c73991 100644 --- a/Dockerfile-distroless +++ b/Dockerfile-distroless @@ -9,14 +9,16 @@ FROM golang:1.25-bookworm AS build # Dependencies -RUN apt update && apt install -y gcc make git +RUN apt update && apt install -y gcc git # Copy source COPY . /go/src/github.com/ghostunnel/ghostunnel -# Build +# Install mage and build RUN cd /go/src/github.com/ghostunnel/ghostunnel && \ - GOFLAGS="-tags=nopkcs11" make clean ghostunnel && \ + go install github.com/magefile/mage@latest && \ + export PATH=$PATH:$(go env GOPATH)/bin && \ + GOFLAGS="-tags=nopkcs11" mage -v go:build && \ cp ghostunnel /usr/bin/ghostunnel # Create a multi-stage build with the binary diff --git a/Dockerfile-test b/Dockerfile-test index 2fd890eb70..1a8713e7f1 100644 --- a/Dockerfile-test +++ b/Dockerfile-test @@ -12,15 +12,19 @@ FROM golang:${GO_VERSION} # Install build dependencies RUN apt-get update && \ - apt-get install -y build-essential python3-minimal netcat-traditional softhsm2 rsyslog git $@ - -# Man page for docs -docs/MANPAGE.md: ghostunnel - ./ghostunnel --help-custom-man | pandoc --wrap=preserve --from man --to markdown > $@ - -# Test binary with coverage instrumentation -ghostunnel.test: $(SOURCE_FILES) - go test -c -covermode=count -coverpkg .,./auth,./certloader,./proxy,./wildcard,./socket - -# Clean build output -clean: - rm -rf ghostunnel coverage ghostunnel.test tests/__pycache__ -.PHONY: clean - -# Run all tests (unit + integration tests) -test: unit integration - gocovmerge coverage/*.profile | grep -v "internal/test" > coverage/all.profile - @echo "PASS" -.PHONY: test - -# Run unit tests -unit: - @mkdir -p coverage - go test -v -covermode=count -coverprofile=coverage/unit-test.profile ./... -.PHONY: unit - -integration: $(INTEGRATION_TESTS) -.PHONY: integration - -# Run integration tests -$(INTEGRATION_TESTS): ghostunnel.test - @mkdir -p coverage - @cd tests && ./runner.py $@ -.PHONY: $(INTEGRATION_TESTS) - -# Import test keys into SoftHSM (v2) -softhsm-import: - softhsm2-util --init-token --slot 0 \ - --label ${GHOSTUNNEL_TEST_PKCS11_LABEL} \ - --so-pin ${GHOSTUNNEL_TEST_PKCS11_PIN} \ - --pin ${GHOSTUNNEL_TEST_PKCS11_PIN} - softhsm2-util --id 01 \ - --token ${GHOSTUNNEL_TEST_PKCS11_LABEL} \ - --label ${GHOSTUNNEL_TEST_PKCS11_LABEL} \ - --so-pin ${GHOSTUNNEL_TEST_PKCS11_PIN} \ - --pin ${GHOSTUNNEL_TEST_PKCS11_PIN} \ - --import test-keys/server-pkcs8.pem -.PHONY: softhsm-import - -# Build Docker image -docker-build: - docker build -t ghostunnel/ghostunnel . -.PHONY: docker-build - -# Run unit and integration tests in Docker container -docker-test: - docker build -t ghostunnel/ghostunnel-test -f Dockerfile-test . - docker run -v ${PWD}:/go/src/github.com/ghostunnel/ghostunnel ghostunnel/ghostunnel-test -.PHONY: docker-test diff --git a/README.md b/README.md index 755f468f93..84af296349 100644 --- a/README.md +++ b/README.md @@ -60,9 +60,14 @@ Getting Started To get started and play around with the Ghostunnel you will need X.509 client and server certificates. If you don't already maintain a PKI, a good way to get started is to use a package like [cloudflare/cfssl](https://github.com/cloudflare/cfssl). -If you only need some test certificates for playing around with the tunnel you -can find some pre-generated ones in the `test-keys` directory (alongside instructions -on how to generate new ones with OpenSSL). + +For testing and development purposes, you can generate test certificates using: + + # Generate test certificates and keys + mage testKeys + +This will create a `test-keys` directory with all the necessary certificates and keys +for testing. **Note: These are test certificates only and should NOT be used in production.** ### Install @@ -70,20 +75,23 @@ Ghostunnel is available through [GitHub releases][rel] and through [Docker Hub][ Please note that the official release binaries are best effort, and are usually built directly via Github Actions on the latest available images. If you need -compatibility for specific OS versions, we recommend building yourself. +compatibility for specific OS versions we recommend building yourself. -To build Ghostunnel, simply run: +Ghostunnel uses the [mage][mage] build system, a make/rake-like build tool using +Go. You can build Ghostunnel with the [mage][mage] commands shown below. # Compile binary - make ghostunnel + mage go:build - # Generate man page - make ghostunnel.man + # Build containers + mage docker:build -Note that Ghostunnel requires Go 1.22 or later to build, and CGO is required. +You can also run `mage -l` to view all build targets and add `-v` to mage commands +to get more verbose output. [rel]: https://github.com/ghostunnel/ghostunnel/releases [hub]: https://hub.docker.com/r/ghostunnel/ghostunnel +[mage]: https://magefile.org ### Develop @@ -94,11 +102,11 @@ modules][gomod] for managing vendored dependencies. To run tests: # Option 1: run unit & integration tests locally - make test + mage test:all # Option 2: run unit & integration tests in a Docker container # This also runs PKCS#11 integration tests using SoftHSM in the container - make docker-test + mage test:docker # Open coverage information in browser go tool cover -html coverage/all.profile @@ -153,6 +161,10 @@ any of the flags matches. See [ACCESS-FLAGS](docs/ACCESS-FLAGS.md) for more information. In this example, we assume that the CN of the client cert we want to accept connections from is `client`. +**Note:** Before running the examples below, make sure you have generated the test +certificates by running `mage testKeys` (see the [Getting Started](#getting-started) +section above). + Start a backend server: nc -l localhost 8080 diff --git a/go.mod b/go.mod index 875323a08c..7229c64397 100644 --- a/go.mod +++ b/go.mod @@ -89,6 +89,7 @@ require ( github.com/ulikunitz/xz v0.5.15 // indirect github.com/valyala/fastjson v1.6.4 // indirect github.com/vektah/gqlparser/v2 v2.5.31 // indirect + github.com/wadey/gocovmerge v0.0.0-20160331181800-b5bfa59ec0ad // indirect github.com/weppos/publicsuffix-go v0.50.1 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect @@ -121,3 +122,5 @@ require ( ) go 1.24.6 + +tool github.com/wadey/gocovmerge diff --git a/go.sum b/go.sum index 2bc0988fe1..51df6ff74e 100644 --- a/go.sum +++ b/go.sum @@ -281,6 +281,8 @@ github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXV github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= github.com/vektah/gqlparser/v2 v2.5.31 h1:YhWGA1mfTjID7qJhd1+Vxhpk5HTgydrGU9IgkWBTJ7k= github.com/vektah/gqlparser/v2 v2.5.31/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts= +github.com/wadey/gocovmerge v0.0.0-20160331181800-b5bfa59ec0ad h1:W0LEBv82YCGEtcmPA3uNZBI33/qF//HAAs3MawDjRa0= +github.com/wadey/gocovmerge v0.0.0-20160331181800-b5bfa59ec0ad/go.mod h1:Hy8o65+MXnS6EwGElrSRjUzQDLXreJlzYLlWiHtt8hM= github.com/weppos/publicsuffix-go v0.13.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k= github.com/weppos/publicsuffix-go v0.40.3-0.20250127173806-e489a31678ca/go.mod h1:43Dfyxu2dpmLg56at26Q4k9gwf3yWSUiwk8kGnwzULk= github.com/weppos/publicsuffix-go v0.50.1 h1:elrBHeSkS/eIb169+DnLrknqmdP4AjT0Q0tEdytz1Og= diff --git a/magefile.go b/magefile.go new file mode 100644 index 0000000000..761e24d2c7 --- /dev/null +++ b/magefile.go @@ -0,0 +1,461 @@ +//go:build mage + +package main + +import ( + "bytes" + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "time" + + "github.com/magefile/mage/mg" + "github.com/magefile/mage/sh" +) + +type Go mg.Namespace +type Git mg.Namespace +type Test mg.Namespace +type Docker mg.Namespace + +var Default = Go.Build + +// printf prints the given format and args if verbose mode is enabled. +func printf(format string, args ...interface{}) { + if mg.Verbose() { + fmt.Printf(format, args...) + } +} + +// runCommands executes a list of commands, stopping on the first error. +// Checks the context for cancellation before running each command. +func runCommands(ctx context.Context, cmds [][]string) error { + for _, cmd := range cmds { + if err := ctx.Err(); err != nil { + return fmt.Errorf("context cancelled: %w", err) + } + if err := sh.Run(cmd[0], cmd[1:]...); err != nil { + return err + } + } + return nil +} + +// Builds builds the Ghostunnel binary. +func (Go) Build(ctx context.Context) error { + version := os.Getenv("VERSION") + if version == "" { + version = getVersion() + } + + return sh.Run("go", "build", "-ldflags", fmt.Sprintf("-X main.version=%s", version), "-o", "ghostunnel", ".") +} + +// Man generates the Ghostunnel man page from the built binary. +// Also generates docs/MANPAGE.md from the man page using pandoc. +func (Go) Man(ctx context.Context) error { + mg.CtxDeps(ctx, Go.Build) + + output, err := sh.Output("./ghostunnel", "--help-custom-man") + if err != nil { + return fmt.Errorf("failed to generate man page: %w", err) + } + + if err := os.WriteFile("ghostunnel.man", []byte(output), 0644); err != nil { + return fmt.Errorf("failed to write ghostunnel.man: %w", err) + } + + // Generate docs/MANPAGE.md from the man page using pandoc + if err := sh.Run("pandoc", "-f", "man", "-t", "markdown", "ghostunnel.man", "-o", "docs/MANPAGE.md"); err != nil { + return fmt.Errorf("failed to generate docs/MANPAGE.md: %w", err) + } + + return nil +} + +// Clean removes build artifacts. +func (Git) Clean(ctx context.Context) error { + return sh.Run("git", "clean", "-Xdf") +} + +// build builds the *test* binary with coverage instrumentation. +func (Test) build() error { + return sh.Run("go", "test", "-c", "-covermode=count", "-coverpkg", ".,./auth,./certloader,./proxy,./wildcard,./socket") +} + +// All runs both unit and integration tests, then merges coverage. +func (Test) All(ctx context.Context) error { + mg.CtxDeps(ctx, Test.Unit, Test.Integration) + mg.CtxDeps(ctx, Test.Coverage) + return nil +} + +// Unit runs the unit tests. +func (Test) Unit(ctx context.Context) error { + printf("Running unit tests...\n") + + if err := os.MkdirAll("coverage", 0755); err != nil { + return fmt.Errorf("failed to create coverage directory: %w", err) + } + + return sh.Run("go", "test", "-v", "-covermode=count", "-coverprofile=coverage/unit-test.profile", "./...") +} + +// Integration runs the integration tests. +func (Test) Integration(ctx context.Context) error { + mg.CtxDeps(ctx, Test.build) + + if err := os.MkdirAll("coverage", 0755); err != nil { + return fmt.Errorf("failed to create coverage directory: %w", err) + } + + // Skip integration tests on Windows + if runtime.GOOS == "windows" { + fmt.Fprintf(os.Stderr, "Integration tests are not supported on Windows\n") + return nil + } + + // Run integration tests + testFiles, err := filepath.Glob("tests/test-*.py") + if err != nil || len(testFiles) == 0 { + return fmt.Errorf("failed to find test files: %w", err) + } + + // Run each integration test directly + printf("Running integration tests...\n") + for _, testFile := range testFiles { + if err := ctx.Err(); err != nil { + return fmt.Errorf("context cancelled: %w", err) + } + + testName := strings.TrimSuffix(filepath.Base(testFile), ".py") + printf("=== RUN %s\n", testName) + + // Run the Python test file directly from tests directory + start := time.Now() + testFileName := filepath.Base(testFile) + cmd := exec.CommandContext(ctx, "python3", testFileName) + cmd.Dir = "tests" + + // Capture stdout and stderr + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + err := cmd.Run() + duration := time.Since(start) + elapsed := duration.Seconds() + + if err == nil { + printf("=== PASS: %s (%.2fs)\n", testName, elapsed) + continue + } + + // Test failed - output captured stdout/stderr and failure message + os.Stdout.Write(stdout.Bytes()) + os.Stderr.Write(stderr.Bytes()) + printf("=== FAIL: %s (%.2fs)\n", testName, elapsed) + + // Get exit code if available + if exitError, ok := err.(*exec.ExitError); ok { + return fmt.Errorf("integration test %s failed with exit code %d", testName, exitError.ExitCode()) + } + + return fmt.Errorf("integration test %s failed: %w", testName, err) + } + + return nil +} + +// Coverage merges the coverage files into a single file. +func (Test) Coverage(ctx context.Context) error { + mg.CtxDeps(ctx, Test.Unit, Test.Integration) + + // Get all coverage profile files + coverageFiles, err := filepath.Glob("coverage/*.profile") + if err != nil || len(coverageFiles) == 0 { + return fmt.Errorf("failed to find coverage files: %w", err) + } + + // Merge coverage files, excluding internal/test + args := []string{"tool", "gocovmerge"} + args = append(args, coverageFiles...) + + mergeOutput, err := sh.Output("go", args...) + if err != nil { + return fmt.Errorf("failed to merge coverage: %w", err) + } + + // Filter out internal/test lines (same as Makefile's grep -v) + lines := strings.Split(string(mergeOutput), "\n") + var filtered []string + for _, line := range lines { + if !strings.Contains(line, "internal/test") { + filtered = append(filtered, line) + } + } + + // Write merged coverage + return os.WriteFile("coverage/all.profile", []byte(strings.Join(filtered, "\n")), 0644) +} + +// Keys generates test certificates and keys for development/testing purposes. +// These should NOT be used in production. The keys are generated in the test-keys directory. +func (Test) Keys(ctx context.Context) error { + // Create test-keys directory + if err := os.MkdirAll("test-keys", 0755); err != nil { + return err + } + + // Write openssl.ext configuration file + opensslExt := `[root] +keyUsage=critical, keyCertSign +basicConstraints=critical, CA:TRUE, pathlen:0 +[server] +extendedKeyUsage = serverAuth +subjectAltName = IP:127.0.0.1,IP:::1,DNS:localhost,URI:spiffe://ghostunnel/server +[client] +extendedKeyUsage = clientAuth +subjectAltName = IP:127.0.0.1,IP:::1,DNS:localhost,URI:spiffe://ghostunnel/client +` + if err := os.WriteFile("test-keys/openssl.ext", []byte(opensslExt), 0644); err != nil { + return err + } + + // Root CA generation commands + rootCommands := [][]string{ + {"openssl", "genrsa", "-out", "test-keys/root-key.pem", "2048"}, + {"openssl", "req", "-new", "-key", "test-keys/root-key.pem", "-out", "test-keys/root-csr.pem", "-subj", "/CN=root"}, + {"openssl", "x509", "-req", "-sha256", "-in", "test-keys/root-csr.pem", "-signkey", "test-keys/root-key.pem", "-out", "test-keys/root-cert.pem", "-days", "5000", "-extfile", "test-keys/openssl.ext", "-extensions", "root"}, + } + if err := runCommands(ctx, rootCommands); err != nil { + return err + } + + // Create root combined file and cacert.pem + rootCert, rootCertErr := os.ReadFile("test-keys/root-cert.pem") + rootKey, rootKeyErr := os.ReadFile("test-keys/root-key.pem") + if rootCertErr != nil || rootKeyErr != nil { + return fmt.Errorf("failed to read root certificate or key: %v / %v", rootCertErr, rootKeyErr) + } + rootCombined := append(rootCert, rootKey...) + if err := os.WriteFile("test-keys/root-combined.pem", rootCombined, 0644); err != nil { + return err + } + if err := os.WriteFile("test-keys/cacert.pem", rootCert, 0644); err != nil { + return err + } + + // Generate server and client keys + for _, name := range []string{"server", "client"} { + if err := generateEntityKeys(ctx, name); err != nil { + return err + } + } + + printf("Test keys generated successfully in test-keys/ directory\n") + return nil +} + +// generateEntityKeys generates all keys and certificates for a given entity (server or client). +func generateEntityKeys(ctx context.Context, name string) error { + commands := [][]string{ + {"openssl", "genrsa", "-out", fmt.Sprintf("test-keys/%s-key.pem", name), "2048"}, + {"openssl", "req", "-new", "-key", fmt.Sprintf("test-keys/%s-key.pem", name), "-out", fmt.Sprintf("test-keys/%s-csr.pem", name), "-subj", fmt.Sprintf("/CN=%s", name)}, + {"openssl", "x509", "-req", "-sha256", "-in", fmt.Sprintf("test-keys/%s-csr.pem", name), "-CA", "test-keys/root-combined.pem", "-CAkey", "test-keys/root-combined.pem", "-CAcreateserial", "-out", fmt.Sprintf("test-keys/%s-cert.pem", name), "-days", "5000", "-extfile", "test-keys/openssl.ext", "-extensions", name}, + } + if err := runCommands(ctx, commands); err != nil { + return err + } + + // Create combined file + cert, certErr := os.ReadFile(fmt.Sprintf("test-keys/%s-cert.pem", name)) + key, keyErr := os.ReadFile(fmt.Sprintf("test-keys/%s-key.pem", name)) + if certErr != nil || keyErr != nil { + return fmt.Errorf("failed to read certificate or key: %v / %v", certErr, keyErr) + } + combined := append(cert, key...) + if err := os.WriteFile(fmt.Sprintf("test-keys/%s-combined.pem", name), combined, 0644); err != nil { + return err + } + + // Generate PKCS#12 keystore and PKCS#8 key + keystoreCommands := [][]string{ + {"openssl", "pkcs12", "-export", "-out", fmt.Sprintf("test-keys/%s-keystore.p12", name), "-in", fmt.Sprintf("test-keys/%s-combined.pem", name), "-inkey", fmt.Sprintf("test-keys/%s-combined.pem", name), "-passout", "pass:"}, + {"openssl", "pkcs8", "-topk8", "-inform", "PEM", "-outform", "PEM", "-in", fmt.Sprintf("test-keys/%s-key.pem", name), "-out", fmt.Sprintf("test-keys/%s-pkcs8.pem", name), "-nocrypt"}, + } + if err := runCommands(ctx, keystoreCommands); err != nil { + return err + } + + return nil +} + +// SoftHSMImport initializes a SoftHSM token and imports the test server key. +// Automatically generates test keys if they don't exist (via Test.Keys dependency). +// Environment variables can be used to configure SoftHSM: +// - GHOSTUNNEL_TEST_PKCS11_LABEL: Token label (default: "ghostunnel-pkcs11-test") +// - GHOSTUNNEL_TEST_PKCS11_PIN: Token PIN (default: "1234") +// - SOFTHSM2_CONF: SoftHSM config file path (default: "/etc/softhsm/softhsm2.conf") +func (Test) SoftHSMImport(ctx context.Context) error { + mg.CtxDeps(ctx, Test.Keys) + + // Get configuration from environment variables + label := os.Getenv("GHOSTUNNEL_TEST_PKCS11_LABEL") + pin := os.Getenv("GHOSTUNNEL_TEST_PKCS11_PIN") + if label == "" || pin == "" { + return fmt.Errorf("GHOSTUNNEL_TEST_PKCS11_LABEL and GHOSTUNNEL_TEST_PKCS11_PIN must be set") + } + + printf("Initializing SoftHSM token with label: %s\n", label) + + // Initialize and import into SoftHSM token + softhsmCommands := [][]string{ + {"softhsm2-util", "--init-token", "--slot", "0", "--label", label, "--so-pin", pin, "--pin", pin}, + {"softhsm2-util", "--id", "01", "--token", label, "--label", label, "--so-pin", pin, "--pin", pin, "--import", "test-keys/server-pkcs8.pem"}, + } + if err := runCommands(ctx, softhsmCommands); err != nil { + return fmt.Errorf("failed to configure SoftHSM: %w", err) + } + + printf("SoftHSM token initialized and key imported successfully\n") + return nil +} + +// Docker builds and runs tests in a Docker container. +// Output is streamed in real-time as the container runs. +func (Test) Docker(ctx context.Context) error { + args := []string{"build", "-t", "ghostunnel/ghostunnel-test", "-f", "Dockerfile-test"} + if !mg.Verbose() { + args = append(args, "--quiet") + } + args = append(args, ".") + if err := sh.Run("docker", args...); err != nil { + return fmt.Errorf("failed to build test image: %w", err) + } + + pwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get current directory: %w", err) + } + + args = []string{"run", "-v", fmt.Sprintf("%s:/go/src/github.com/ghostunnel/ghostunnel", pwd), "ghostunnel/ghostunnel-test", "--"} + if mg.Verbose() { + args = append(args, "-v") + } + args = append(args, "test:softhsmimport", "test:all") + return sh.Run("docker", args...) +} + +// Build builds and tags all Docker containers. +// Uses docker buildx for multi-platform builds. Does not push images. +func (Docker) Build(ctx context.Context) error { + return buildDocker(ctx, false) +} + +// Push builds and publishes all Docker containers to Docker Hub. +// Uses docker buildx for multi-platform builds and pushes images. +func (Docker) Push(ctx context.Context) error { + return buildDocker(ctx, true) +} + +// buildDocker builds and tags all Docker containers, optionally pushing them to Docker Hub. +func buildDocker(ctx context.Context, push bool) error { + // Determine base tag (latest for master, version tag otherwise) + baseTag, err := getDockerTag() + if err != nil { + return err + } + + builds := map[string][]string{ + "Dockerfile-alpine": []string{ + fmt.Sprintf("ghostunnel/ghostunnel:%s", baseTag), + fmt.Sprintf("ghostunnel/ghostunnel:%s-alpine", baseTag), + }, + "Dockerfile-debian": []string{fmt.Sprintf("ghostunnel/ghostunnel:%s-debian", baseTag)}, + "Dockerfile-distroless": []string{fmt.Sprintf("ghostunnel/ghostunnel:%s-distroless", baseTag)}, + } + + for dockerfile, tags := range builds { + if err := ctx.Err(); err != nil { + return fmt.Errorf("context cancelled: %w", err) + } + if err := buildDockerImage(dockerfile, tags, push); err != nil { + return fmt.Errorf("failed to build image: %w", err) + } + } + + return nil +} + +// buildDockerImage builds a Docker image using buildx with the given Dockerfile, tags, and platforms. +// If push is true, it will push the image to the registry. +func buildDockerImage(dockerfile string, tags []string, push bool) error { + args := []string{"buildx", "build", "-f", dockerfile} + + platforms := os.Getenv("DOCKER_PLATFORMS") + if platforms != "" { + args = append(args, "--platform", platforms) + } + for _, tag := range tags { + args = append(args, "-t", tag) + } + if !mg.Verbose() { + args = append(args, "--quiet") + } + if push { + args = append(args, "--push") + } + + // Add build context & run + args = append(args, ".") + return sh.Run("docker", args...) +} + +// getVersion gets the version from git describe. +func getVersion() string { + output, err := sh.Output("git", "describe", "--always", "--dirty") + if err != nil { + return "unknown" + } + return strings.TrimSpace(output) +} + +// getDockerTag determines the Docker tag to use based on git state. +// Returns "latest" if on master branch, otherwise returns the most recent tag. +func getDockerTag() (string, error) { + // Check if we're on a tag (for GitHub Actions when triggered by tag push) + // In GitHub Actions, GITHUB_REF will be set, but locally we check git + githubRef := os.Getenv("GITHUB_REF") + if githubRef != "" { + // GitHub Actions: refs/heads/master or refs/tags/v1.2.3 + if strings.HasPrefix(githubRef, "refs/heads/master") { + return "latest", nil + } + if strings.HasPrefix(githubRef, "refs/tags/") { + tag := strings.TrimPrefix(githubRef, "refs/tags/") + return tag, nil + } + } + + // Check current branch + branch, err := sh.Output("git", "rev-parse", "--abbrev-ref", "HEAD") + if err != nil { + return "", fmt.Errorf("failed to determine git ref: %w", err) + } + if strings.TrimSpace(branch) == "master" { + return "latest", nil + } + + // Not on master, get the most recent tag + tag, err := sh.Output("git", "describe", "--tags", "--abbrev=0") + if err != nil { + return "", fmt.Errorf("failed to get git tag: %w", err) + } + + return strings.TrimSpace(tag), nil +} diff --git a/test-keys/Makefile b/test-keys/Makefile deleted file mode 100644 index b17b9d3ea9..0000000000 --- a/test-keys/Makefile +++ /dev/null @@ -1,38 +0,0 @@ -.PHONY: all -all: root-combined.pem root-key.pem server-combined.pem server-keystore.p12 client-combined.pem client-keystore.p12 server-pkcs8.pem server-key.pem client-pkcs8.pem client-key.pem cacert.pem - rm -f *.srl *-csr.pem - -%-key.pem: - openssl genrsa -out $@ 2048 - -%-csr.pem: %-key.pem - openssl req -new -key $< -out $@ -subj /CN=$(@:-csr.pem=) - -## -## NOTE: At the following rules, CSR should be the 1st, and key/certificate/CA -## the 2nd prerequisite -## - -root-cert.pem: root-csr.pem root-key.pem openssl.ext - openssl x509 -req -sha256 -in $< -signkey $(word 2,$^) -out $@ -days 5000 -extfile openssl.ext -extensions root - -cacert.pem: root-cert.pem - cp $< $@ - -server-cert.pem: server-csr.pem root-combined.pem openssl.ext - openssl x509 -req -sha256 -in $< -CA $(word 2,$^) -CAkey $(word 2,$^) -CAcreateserial -out $@ -days 5000 -extfile openssl.ext -extensions server - -client-cert.pem: client-csr.pem root-combined.pem openssl.ext - openssl x509 -req -sha256 -in $< -CA $(word 2,$^) -CAkey $(word 2,$^) -CAcreateserial -out $@ -days 5000 -extfile openssl.ext -extensions client - -# Combined certificate/key file -%-combined.pem: %-cert.pem %-key.pem - cat $^ > $@ - -# Keystore in PKCS#12 format -%-keystore.p12: %-combined.pem - openssl pkcs12 -export -out $@ -in $< -inkey $< -password pass: - -# Private key in PKCS#8 format (for SoftHSM testing) -%-pkcs8.pem: %-key.pem - openssl pkcs8 -topk8 -inform PEM -outform PEM -in $< -out $@ -nocrypt diff --git a/test-keys/README.md b/test-keys/README.md deleted file mode 100644 index 3363a4a562..0000000000 --- a/test-keys/README.md +++ /dev/null @@ -1,6 +0,0 @@ -Test keys -========= - -The certificates and keys contained in this directory have been generated for -test/development purposes only. Do not use these files in production -deployments! You can regenerate them anytime by running `make all`. diff --git a/test-keys/cacert.pem b/test-keys/cacert.pem deleted file mode 100644 index 5519e8be0a..0000000000 --- a/test-keys/cacert.pem +++ /dev/null @@ -1,17 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICxzCCAa+gAwIBAgIJAOVOC+xQP1L0MA0GCSqGSIb3DQEBCwUAMA8xDTALBgNV -BAMMBHJvb3QwHhcNMTkwNjE4MTkwNjA0WhcNMzMwMjI0MTkwNjA0WjAPMQ0wCwYD -VQQDDARyb290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ts+OiJP -hFYyks33N3VVMFYAHXmBThx9tu2nIBspwXGDj4kUtVbTtsw4+FP4zdOqVJAjcd17 -sFvQmIkCtNQriE1zbFZZ1wDUMGCrw60JM0sjbWwlgOlio5iY/Zf/Xuh7zN/CNgyM -s6hqF7Rwy+Vw9BvOG3/A2Xu0LzaRmNmzfd5KzMfqkMqunC84CKSGy+jcwu9w2gcm -j0Nbf7AcH9pNb4cdmuP2fnq//IFdDxesfMhZq+CI1WiO3JtUzg3oxPr/RYcofI9P -UkzRzbjn8u9j2gDJ0umCFDKOawhTmj51/rqMXU8IcdsV3T0Iftq1DdweF4k85P6A -oHKOAkRIaTsTlwIDAQABoyYwJDAOBgNVHQ8BAf8EBAMCAgQwEgYDVR0TAQH/BAgw -BgEB/wIBADANBgkqhkiG9w0BAQsFAAOCAQEAHAx0mgmvlqp4KfCQ/WP8YCblQ8v1 -qWIC4sZk6oGpLMAVa3jMWY2gelrfA8WByOFVzyOnNF4vxidSCPVhBrXNMrWJSq3J -9udQEuREA7v7sElf0sseCtSp8Jp+ase0V+i+fZDPI5ezvGdZgMoOim9K1QcoU911 -4Yj0UBaB2avmktNDAo6D0o4Ph/sTAWoaqllDtql3ovvpIhlQ/3ZgsOrGxJQJrhMO -whK5pvnU0yDH37pRWrcguv/dTTbT5kc1BPmz7oIynhaczVrYOXii9XqqSl0wGYUm -dpj6fVObgA5UrTytEnGLKvPmvYfJgN4CzX0SOm+lC89LWAp4u2xvgRE3HQ== ------END CERTIFICATE----- diff --git a/test-keys/client-cert.pem b/test-keys/client-cert.pem deleted file mode 100644 index 974293b549..0000000000 --- a/test-keys/client-cert.pem +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDBDCCAeygAwIBAgIJAJG2NWsnKE4bMA0GCSqGSIb3DQEBCwUAMA8xDTALBgNV -BAMMBHJvb3QwHhcNMTkwNjE4MTkwNjA0WhcNMzMwMjI0MTkwNjA0WjARMQ8wDQYD -VQQDDAZjbGllbnQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDiqQT6 -+lMjyPHaTIILqndhhuQlrNbGwdCeUC/ATnV0svAbojfAysOspJGoTmTP6v+SHnyp -kODJJoYYPVn7jY4EbSk8NqkSBr1/Ood7DLwCgflh1lKmuzJ1J5PYLew8bDlwD3Wb -bgto5y+NPC+LsM85+GlN5UE1rJkzRpKYa8TEoQ1Kt/nfOjkKLiynJfSL8gzJHKjA -qyFI8fTWaZ0naH5fG8ZMQezkhw/imhvSHW5oZIgQieZqe5vM0B9/BEUjSgblLaak -/snByo86RtH2Ga3QA4g9TiZzs9ou3x4hqqoY7dv8hzBxUHW686ZKiRi5C5F3ieKc -iyFf7GHBTIWUXrl5AgMBAAGjYTBfMBMGA1UdJQQMMAoGCCsGAQUFBwMCMEgGA1Ud -EQRBMD+HBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAGCCWxvY2FsaG9zdIYac3BpZmZl -Oi8vZ2hvc3R1bm5lbC9jbGllbnQwDQYJKoZIhvcNAQELBQADggEBAMYM1iOjzjKh -f1cudHrG462KqDlLO/C3kTkboBBVPNEZFGQ+nXIAdrE5Di88NiLLnhlXhpvq5AGt -8WSm9qB411pHZHUPu4x7e0pAhVh46XBe3dh7ICCpBRBOJJXvBpRUP9dLIL8hMOoA -lFUIO4ivHLZ/5bbn25xcMmy9m7BiyehBDmx/VnH4nvlS22D9MGc1wGnKoY89w8Tm -wwnwANgEYV26SUhSylwi7TaB/K43toOmfHOM+l+dreTmKQCtHdRt/4ZX5r7UNxvR -MbhhirlVramubA4VeCzwqT7CWv8CQpi4Sh1/cztNNHo0Rf3sU8VMs7TRNGRqqyvg -MZh1Oyc57pw= ------END CERTIFICATE----- diff --git a/test-keys/client-combined.pem b/test-keys/client-combined.pem deleted file mode 100644 index 0caedabe46..0000000000 --- a/test-keys/client-combined.pem +++ /dev/null @@ -1,46 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDBDCCAeygAwIBAgIJAJG2NWsnKE4bMA0GCSqGSIb3DQEBCwUAMA8xDTALBgNV -BAMMBHJvb3QwHhcNMTkwNjE4MTkwNjA0WhcNMzMwMjI0MTkwNjA0WjARMQ8wDQYD -VQQDDAZjbGllbnQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDiqQT6 -+lMjyPHaTIILqndhhuQlrNbGwdCeUC/ATnV0svAbojfAysOspJGoTmTP6v+SHnyp -kODJJoYYPVn7jY4EbSk8NqkSBr1/Ood7DLwCgflh1lKmuzJ1J5PYLew8bDlwD3Wb -bgto5y+NPC+LsM85+GlN5UE1rJkzRpKYa8TEoQ1Kt/nfOjkKLiynJfSL8gzJHKjA -qyFI8fTWaZ0naH5fG8ZMQezkhw/imhvSHW5oZIgQieZqe5vM0B9/BEUjSgblLaak -/snByo86RtH2Ga3QA4g9TiZzs9ou3x4hqqoY7dv8hzBxUHW686ZKiRi5C5F3ieKc -iyFf7GHBTIWUXrl5AgMBAAGjYTBfMBMGA1UdJQQMMAoGCCsGAQUFBwMCMEgGA1Ud -EQRBMD+HBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAGCCWxvY2FsaG9zdIYac3BpZmZl -Oi8vZ2hvc3R1bm5lbC9jbGllbnQwDQYJKoZIhvcNAQELBQADggEBAMYM1iOjzjKh -f1cudHrG462KqDlLO/C3kTkboBBVPNEZFGQ+nXIAdrE5Di88NiLLnhlXhpvq5AGt -8WSm9qB411pHZHUPu4x7e0pAhVh46XBe3dh7ICCpBRBOJJXvBpRUP9dLIL8hMOoA -lFUIO4ivHLZ/5bbn25xcMmy9m7BiyehBDmx/VnH4nvlS22D9MGc1wGnKoY89w8Tm -wwnwANgEYV26SUhSylwi7TaB/K43toOmfHOM+l+dreTmKQCtHdRt/4ZX5r7UNxvR -MbhhirlVramubA4VeCzwqT7CWv8CQpi4Sh1/cztNNHo0Rf3sU8VMs7TRNGRqqyvg -MZh1Oyc57pw= ------END CERTIFICATE----- ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEA4qkE+vpTI8jx2kyCC6p3YYbkJazWxsHQnlAvwE51dLLwG6I3 -wMrDrKSRqE5kz+r/kh58qZDgySaGGD1Z+42OBG0pPDapEga9fzqHewy8AoH5YdZS -prsydSeT2C3sPGw5cA91m24LaOcvjTwvi7DPOfhpTeVBNayZM0aSmGvExKENSrf5 -3zo5Ci4spyX0i/IMyRyowKshSPH01mmdJ2h+XxvGTEHs5IcP4pob0h1uaGSIEInm -anubzNAffwRFI0oG5S2mpP7JwcqPOkbR9hmt0AOIPU4mc7PaLt8eIaqqGO3b/Icw -cVB1uvOmSokYuQuRd4ninIshX+xhwUyFlF65eQIDAQABAoIBAHDANXsH9T2y4yR9 -tJ9LADHioTFgpkKe/UETkH1wShtwO+LzMhrUgrwp7U81GA8ZzmKIiejr6fYGFOSP -+GgbRY9MIhwS8M3HzpIwsl5yuj/hGgYiUGEic/o1YuVCCucPyw7EkfNsrX5UqqHu -U5SAssUann+iUGr73gXU0G3EBlDtAPHyLks/ejnyEmiCdvzdMUwyOdhPIliy/Wxx -i7BrHqwOQ2Ze8r7RYAFsFDQZu17JvWz89Amyq+gsArDdG1O0pgT1PaJvGmimsADg -rTbsx6CSReqsdMMHVP59W/neYjT4tnj7Tqa9SoLw98J3get7iD1/qEWNZMfweYFG -Mu5N0MUCgYEA9jmuAca4yXDQaXpMyl3mi/93kG9XQzQpJfSJi4h/In3v5maml5wZ -nl+RrRBLbknCaJosybmezArCwNqRtMbv3ROEtlt148UVyLE/nQ1t5SZoVGmqQcuQ -33w8CHmiJXxpFJF8QGlsMwPBkeu0GKWadtHgDhUquPC6ZO8wrVdySKsCgYEA66iB -VVlQcrydJywmghgWfs5IGOcFNiYEnqwb+4YncLH6bnpct1rZPIBGqrOrqyrBuEJC -X8EOhWdbNHesjaMsnPCk18Iu1MrDbFY3jc1yRoceaAfbRgo4wxJrWAAivq2RLrZr -uQoNMZ5szZWb7tj77lPHM06dBNiY/lKSCgjjDmsCgYBVUZVHDkn3mzzMX9F+Cd2a -QzJ1/O663bcobLzAedK76/GV35n4TOHfq/P5iPzqQWq4/jtoxwYMY+oqE1KL1qdE -7r98xOJrw14SBcEqNX4P+igKn/acB+KyXgUbVrJl1N9Afk0UsLMKVlHbs1XbeH62 -j3DY0GLWZKvhz7QJTJTQMwKBgQCmR+DyezHzs7jj4hhN5XdCBIYE5EXeHldWKrUX -9mIv97VwbPCtA/KS6SRz4JE6FtJYFJDHBe8cHIDTdboQutmI55g5EbB/v47DC9bp -PNTshPzkhN6o/h9tLbsGfhoGF2yA3IEIMNp8b3/wVCeSEhM56G1/e3H58GeL1HFt -mTW1fwKBgFSvhnIgwPrrshHKzrHrLwJowtQwOaEVLmYhloV0B4pkUV9Cn55QPqTt -EtCbP00HzeTJN472lknOvZ744cecc3pjqMsCPKc4ThJ8IxonFiiyC9ZDlUdTbUEQ -BSbORtj0VBIpiZFylhSYsw+i+GeI8EFgNMJvjfGVduv0MxrQKxan ------END RSA PRIVATE KEY----- diff --git a/test-keys/client-key.pem b/test-keys/client-key.pem deleted file mode 100644 index 4339e3aca6..0000000000 --- a/test-keys/client-key.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEA4qkE+vpTI8jx2kyCC6p3YYbkJazWxsHQnlAvwE51dLLwG6I3 -wMrDrKSRqE5kz+r/kh58qZDgySaGGD1Z+42OBG0pPDapEga9fzqHewy8AoH5YdZS -prsydSeT2C3sPGw5cA91m24LaOcvjTwvi7DPOfhpTeVBNayZM0aSmGvExKENSrf5 -3zo5Ci4spyX0i/IMyRyowKshSPH01mmdJ2h+XxvGTEHs5IcP4pob0h1uaGSIEInm -anubzNAffwRFI0oG5S2mpP7JwcqPOkbR9hmt0AOIPU4mc7PaLt8eIaqqGO3b/Icw -cVB1uvOmSokYuQuRd4ninIshX+xhwUyFlF65eQIDAQABAoIBAHDANXsH9T2y4yR9 -tJ9LADHioTFgpkKe/UETkH1wShtwO+LzMhrUgrwp7U81GA8ZzmKIiejr6fYGFOSP -+GgbRY9MIhwS8M3HzpIwsl5yuj/hGgYiUGEic/o1YuVCCucPyw7EkfNsrX5UqqHu -U5SAssUann+iUGr73gXU0G3EBlDtAPHyLks/ejnyEmiCdvzdMUwyOdhPIliy/Wxx -i7BrHqwOQ2Ze8r7RYAFsFDQZu17JvWz89Amyq+gsArDdG1O0pgT1PaJvGmimsADg -rTbsx6CSReqsdMMHVP59W/neYjT4tnj7Tqa9SoLw98J3get7iD1/qEWNZMfweYFG -Mu5N0MUCgYEA9jmuAca4yXDQaXpMyl3mi/93kG9XQzQpJfSJi4h/In3v5maml5wZ -nl+RrRBLbknCaJosybmezArCwNqRtMbv3ROEtlt148UVyLE/nQ1t5SZoVGmqQcuQ -33w8CHmiJXxpFJF8QGlsMwPBkeu0GKWadtHgDhUquPC6ZO8wrVdySKsCgYEA66iB -VVlQcrydJywmghgWfs5IGOcFNiYEnqwb+4YncLH6bnpct1rZPIBGqrOrqyrBuEJC -X8EOhWdbNHesjaMsnPCk18Iu1MrDbFY3jc1yRoceaAfbRgo4wxJrWAAivq2RLrZr -uQoNMZ5szZWb7tj77lPHM06dBNiY/lKSCgjjDmsCgYBVUZVHDkn3mzzMX9F+Cd2a -QzJ1/O663bcobLzAedK76/GV35n4TOHfq/P5iPzqQWq4/jtoxwYMY+oqE1KL1qdE -7r98xOJrw14SBcEqNX4P+igKn/acB+KyXgUbVrJl1N9Afk0UsLMKVlHbs1XbeH62 -j3DY0GLWZKvhz7QJTJTQMwKBgQCmR+DyezHzs7jj4hhN5XdCBIYE5EXeHldWKrUX -9mIv97VwbPCtA/KS6SRz4JE6FtJYFJDHBe8cHIDTdboQutmI55g5EbB/v47DC9bp -PNTshPzkhN6o/h9tLbsGfhoGF2yA3IEIMNp8b3/wVCeSEhM56G1/e3H58GeL1HFt -mTW1fwKBgFSvhnIgwPrrshHKzrHrLwJowtQwOaEVLmYhloV0B4pkUV9Cn55QPqTt -EtCbP00HzeTJN472lknOvZ744cecc3pjqMsCPKc4ThJ8IxonFiiyC9ZDlUdTbUEQ -BSbORtj0VBIpiZFylhSYsw+i+GeI8EFgNMJvjfGVduv0MxrQKxan ------END RSA PRIVATE KEY----- diff --git a/test-keys/client-keystore.p12 b/test-keys/client-keystore.p12 deleted file mode 100644 index 67d59dac495bb73a3e1f3e105f6e7c95d7b341df..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2381 zcmV-T39|Muf(c0i0Ru3C2@eJdDuzgg_YDCD0ic2j00e>v{4jzD_%MP4uLcP!hDe6@ z4FLxRpn?OSFoFY|0s#Opf&-NX2`Yw2hW8Bt2LUh~1_~;MNQUPrYG<(uMR~BLzw11Yu2sxSDkb9D_*kl_OkjHW-GA}0bm-XT(`Vb zllAt%6bXiLWvf>IIzgp7YJ>Ni@xb^HQe^FXtd^0oRCNe(QI#Va7|N49ZIWHi5OoaC2Fc!=U&k(rqKny?H7LH9z!1DR} zmam=jA96BzbIq!uMp~32b?jDJe20*?nyS~T&XR?K0$k=ip93irYIba>nxT#7X>Gp> zuIQBOw%aSTQ$xK9%y#haP*~g5asI0KsnmK(mt)ogcw75_8%I?D2q`Y8b!DSDCZpdUaZLfQg2g-+z=OpE1nmrE~ z&1q$+C25@U3S0MS#vGRPXc9*W`%Fbx$OXDWJsPbxo4r9b(wu-jql#)xLG(aYVKX@zgOl|T!%lOOB&zO{`XRKvAq>t=fm>-?%?BjB> zKaPc$U=YL*V8li7t=&ZGAPh_5s3LA8nu47#yrucVg0g%73>Z9l)(Yi(JWaWl?WuV6 z#`jyz=w1XogLga}W7LWi*4f7DGs3*(cjDam8Is`|`J_XYnGCIE zcWczugG@wU^hXALmOhk!p*EE^i|=Vx5rI>e!KVC-kxaA|tsJBUemLjf%wtkOK)M5; zFoFd^1_>&LNQU8RwicW4mT|V$^Qf}6!XWa5YKY>8#Ga9cG6|3MRb1gtSo`6`Q9M4Yhx>#VKZpWv@*vh2}>Ca&D5KN`o^RK ztS$PHhtpC7yerw08TGeldgy}w5@i4T&9$5fK{{GNCX!wo%GNZ>M^9R%=7#?&mAN&X zWGh%XrfPJi2q+48@lM4={DkCKO)VLCtPP!fyHqq}B}wfNE-=IDCX+PEt}%1G6_VRy z4mBu>B(VjUDh7M*b{9izJoNlpbIvNgy~xCW3*eG(1H_`qUf9`m`bW-AeYN-y_! z5aP{astknFQk&Ieer+H)p@DoEhKBpdZ&K$8*F)OkqF+yrB6=LSINmB*_>Px&J|VRb zyk<=1?xwUjKSI5DM)vah3`(KT##|3z5QI^!k!v#j_ivp_?zuj;%A723V=#ABdd)?{ zCRVb@MFOoU>HuJT10#6RFE6{fxh%4J~m3LS1tBbq!byd`>={Tk>u zhY+mqg;jTVCVt_tkSZLmx`cA7<$ zp37l<6GWcI<=${{_hsn2+cv)r+<^SfMWxqtb>;zeaVLkb@n=oxiTQ90%xZgM3I-0< zP@xU9L9cdT&fn(2JyY0-I`6_Vb2ra(IuA#JK4$b12d4zCM1|{neNB@efg8n~u>^`+ zOpqj?mIWNsr|=ihy8T2pw9@}^H8vC+LB+Q}1X-1aVtvVINSO(}kD1ByF3j`+w6Vz8 zXJf~ylT!)hJATXyT279_k$@v!EwpsWTS<#I-c=ULYbl{AYSXW)ywkQ_KeCC;(aW=a zHT>9>-2SB3V8-u zufAw{CusDBtmicp1l@h{#6g~ITtSx4ui;F18#wa>Q;y5o*UjpS@dm$7tB6=wTAJ^_ z1yf-=GN0UL&VyOvI3r+%AqO48f9s@_T{o8zd>`$%`#t+>4fa;`eLyg2sloh@_|k&0 z5>bDf=WQB}tO6*K!a9v~Oyj)WNG zgi^_tje%Gk(J?V4Fe3&DDuzgg_YDCF6)_eB6r1P=D%p?hZFyC}=k&l1)`p`>f-o^K zAutIB1uG5%0vZJX1QZGp)nYIR)}ul5M`{%=4Z=@+t*`_L{@9~~N%l_w0s;sC#@TA6 diff --git a/test-keys/client-pkcs8.pem b/test-keys/client-pkcs8.pem deleted file mode 100644 index 579065875e..0000000000 --- a/test-keys/client-pkcs8.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDiqQT6+lMjyPHa -TIILqndhhuQlrNbGwdCeUC/ATnV0svAbojfAysOspJGoTmTP6v+SHnypkODJJoYY -PVn7jY4EbSk8NqkSBr1/Ood7DLwCgflh1lKmuzJ1J5PYLew8bDlwD3Wbbgto5y+N -PC+LsM85+GlN5UE1rJkzRpKYa8TEoQ1Kt/nfOjkKLiynJfSL8gzJHKjAqyFI8fTW -aZ0naH5fG8ZMQezkhw/imhvSHW5oZIgQieZqe5vM0B9/BEUjSgblLaak/snByo86 -RtH2Ga3QA4g9TiZzs9ou3x4hqqoY7dv8hzBxUHW686ZKiRi5C5F3ieKciyFf7GHB -TIWUXrl5AgMBAAECggEAcMA1ewf1PbLjJH20n0sAMeKhMWCmQp79QROQfXBKG3A7 -4vMyGtSCvCntTzUYDxnOYoiJ6Ovp9gYU5I/4aBtFj0wiHBLwzcfOkjCyXnK6P+Ea -BiJQYSJz+jVi5UIK5w/LDsSR82ytflSqoe5TlICyxRqef6JQavveBdTQbcQGUO0A -8fIuSz96OfISaIJ2/N0xTDI52E8iWLL9bHGLsGserA5DZl7yvtFgAWwUNBm7Xsm9 -bPz0CbKr6CwCsN0bU7SmBPU9om8aaKawAOCtNuzHoJJF6qx0wwdU/n1b+d5iNPi2 -ePtOpr1KgvD3wneB63uIPX+oRY1kx/B5gUYy7k3QxQKBgQD2Oa4BxrjJcNBpekzK -XeaL/3eQb1dDNCkl9ImLiH8ife/mZqaXnBmeX5GtEEtuScJomizJuZ7MCsLA2pG0 -xu/dE4S2W3XjxRXIsT+dDW3lJmhUaapBy5DffDwIeaIlfGkUkXxAaWwzA8GR67QY -pZp20eAOFSq48Lpk7zCtV3JIqwKBgQDrqIFVWVByvJ0nLCaCGBZ+zkgY5wU2JgSe -rBv7hidwsfpuely3Wtk8gEaqs6urKsG4QkJfwQ6FZ1s0d6yNoyyc8KTXwi7UysNs -VjeNzXJGhx5oB9tGCjjDEmtYACK+rZEutmu5Cg0xnmzNlZvu2PvuU8czTp0E2Jj+ -UpIKCOMOawKBgFVRlUcOSfebPMxf0X4J3ZpDMnX87rrdtyhsvMB50rvr8ZXfmfhM -4d+r8/mI/OpBarj+O2jHBgxj6ioTUovWp0Tuv3zE4mvDXhIFwSo1fg/6KAqf9pwH -4rJeBRtWsmXU30B+TRSwswpWUduzVdt4fraPcNjQYtZkq+HPtAlMlNAzAoGBAKZH -4PJ7MfOzuOPiGE3ld0IEhgTkRd4eV1YqtRf2Yi/3tXBs8K0D8pLpJHPgkToW0lgU -kMcF7xwcgNN1uhC62YjnmDkRsH+/jsML1uk81OyE/OSE3qj+H20tuwZ+GgYXbIDc -gQgw2nxvf/BUJ5ISEznobX97cfnwZ4vUcW2ZNbV/AoGAVK+GciDA+uuyEcrOsesv -AmjC1DA5oRUuZiGWhXQHimRRX0KfnlA+pO0S0Js/TQfN5Mk3jvaWSc69nvjhx5xz -emOoywI8pzhOEnwjGicWKLIL1kOVR1NtQRAFJs5G2PRUEimJkXKWFJizD6L4Z4jw -QWA0wm+N8ZV26/QzGtArFqc= ------END PRIVATE KEY----- diff --git a/test-keys/openssl.ext b/test-keys/openssl.ext deleted file mode 100644 index d4627db0ce..0000000000 --- a/test-keys/openssl.ext +++ /dev/null @@ -1,9 +0,0 @@ -[root] -keyUsage=critical, keyCertSign -basicConstraints=critical, CA:TRUE, pathlen:0 -[server] -extendedKeyUsage = serverAuth -subjectAltName = IP:127.0.0.1,IP:::1,DNS:localhost,URI:spiffe://ghostunnel/server -[client] -extendedKeyUsage = clientAuth -subjectAltName = IP:127.0.0.1,IP:::1,DNS:localhost,URI:spiffe://ghostunnel/client diff --git a/test-keys/root-cert.pem b/test-keys/root-cert.pem deleted file mode 100644 index 5519e8be0a..0000000000 --- a/test-keys/root-cert.pem +++ /dev/null @@ -1,17 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICxzCCAa+gAwIBAgIJAOVOC+xQP1L0MA0GCSqGSIb3DQEBCwUAMA8xDTALBgNV -BAMMBHJvb3QwHhcNMTkwNjE4MTkwNjA0WhcNMzMwMjI0MTkwNjA0WjAPMQ0wCwYD -VQQDDARyb290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ts+OiJP -hFYyks33N3VVMFYAHXmBThx9tu2nIBspwXGDj4kUtVbTtsw4+FP4zdOqVJAjcd17 -sFvQmIkCtNQriE1zbFZZ1wDUMGCrw60JM0sjbWwlgOlio5iY/Zf/Xuh7zN/CNgyM -s6hqF7Rwy+Vw9BvOG3/A2Xu0LzaRmNmzfd5KzMfqkMqunC84CKSGy+jcwu9w2gcm -j0Nbf7AcH9pNb4cdmuP2fnq//IFdDxesfMhZq+CI1WiO3JtUzg3oxPr/RYcofI9P -UkzRzbjn8u9j2gDJ0umCFDKOawhTmj51/rqMXU8IcdsV3T0Iftq1DdweF4k85P6A -oHKOAkRIaTsTlwIDAQABoyYwJDAOBgNVHQ8BAf8EBAMCAgQwEgYDVR0TAQH/BAgw -BgEB/wIBADANBgkqhkiG9w0BAQsFAAOCAQEAHAx0mgmvlqp4KfCQ/WP8YCblQ8v1 -qWIC4sZk6oGpLMAVa3jMWY2gelrfA8WByOFVzyOnNF4vxidSCPVhBrXNMrWJSq3J -9udQEuREA7v7sElf0sseCtSp8Jp+ase0V+i+fZDPI5ezvGdZgMoOim9K1QcoU911 -4Yj0UBaB2avmktNDAo6D0o4Ph/sTAWoaqllDtql3ovvpIhlQ/3ZgsOrGxJQJrhMO -whK5pvnU0yDH37pRWrcguv/dTTbT5kc1BPmz7oIynhaczVrYOXii9XqqSl0wGYUm -dpj6fVObgA5UrTytEnGLKvPmvYfJgN4CzX0SOm+lC89LWAp4u2xvgRE3HQ== ------END CERTIFICATE----- diff --git a/test-keys/root-combined.pem b/test-keys/root-combined.pem deleted file mode 100644 index 0e45b1332f..0000000000 --- a/test-keys/root-combined.pem +++ /dev/null @@ -1,44 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICxzCCAa+gAwIBAgIJAOVOC+xQP1L0MA0GCSqGSIb3DQEBCwUAMA8xDTALBgNV -BAMMBHJvb3QwHhcNMTkwNjE4MTkwNjA0WhcNMzMwMjI0MTkwNjA0WjAPMQ0wCwYD -VQQDDARyb290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ts+OiJP -hFYyks33N3VVMFYAHXmBThx9tu2nIBspwXGDj4kUtVbTtsw4+FP4zdOqVJAjcd17 -sFvQmIkCtNQriE1zbFZZ1wDUMGCrw60JM0sjbWwlgOlio5iY/Zf/Xuh7zN/CNgyM -s6hqF7Rwy+Vw9BvOG3/A2Xu0LzaRmNmzfd5KzMfqkMqunC84CKSGy+jcwu9w2gcm -j0Nbf7AcH9pNb4cdmuP2fnq//IFdDxesfMhZq+CI1WiO3JtUzg3oxPr/RYcofI9P -UkzRzbjn8u9j2gDJ0umCFDKOawhTmj51/rqMXU8IcdsV3T0Iftq1DdweF4k85P6A -oHKOAkRIaTsTlwIDAQABoyYwJDAOBgNVHQ8BAf8EBAMCAgQwEgYDVR0TAQH/BAgw -BgEB/wIBADANBgkqhkiG9w0BAQsFAAOCAQEAHAx0mgmvlqp4KfCQ/WP8YCblQ8v1 -qWIC4sZk6oGpLMAVa3jMWY2gelrfA8WByOFVzyOnNF4vxidSCPVhBrXNMrWJSq3J -9udQEuREA7v7sElf0sseCtSp8Jp+ase0V+i+fZDPI5ezvGdZgMoOim9K1QcoU911 -4Yj0UBaB2avmktNDAo6D0o4Ph/sTAWoaqllDtql3ovvpIhlQ/3ZgsOrGxJQJrhMO -whK5pvnU0yDH37pRWrcguv/dTTbT5kc1BPmz7oIynhaczVrYOXii9XqqSl0wGYUm -dpj6fVObgA5UrTytEnGLKvPmvYfJgN4CzX0SOm+lC89LWAp4u2xvgRE3HQ== ------END CERTIFICATE----- ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEA0ts+OiJPhFYyks33N3VVMFYAHXmBThx9tu2nIBspwXGDj4kU -tVbTtsw4+FP4zdOqVJAjcd17sFvQmIkCtNQriE1zbFZZ1wDUMGCrw60JM0sjbWwl -gOlio5iY/Zf/Xuh7zN/CNgyMs6hqF7Rwy+Vw9BvOG3/A2Xu0LzaRmNmzfd5KzMfq -kMqunC84CKSGy+jcwu9w2gcmj0Nbf7AcH9pNb4cdmuP2fnq//IFdDxesfMhZq+CI -1WiO3JtUzg3oxPr/RYcofI9PUkzRzbjn8u9j2gDJ0umCFDKOawhTmj51/rqMXU8I -cdsV3T0Iftq1DdweF4k85P6AoHKOAkRIaTsTlwIDAQABAoIBAH24w62d75OUYasu -q4yhpR2g6YipffO5ASwlH1UBXTA+IpdewL4u+yUvN3i5eMwgvJqXJsspqCpLVGDe -sIJpT1uB8qRSCFct41bFDSUq8yVmU8VWijYG8g1hWzc5fcZ1D/vkHsRjTzF/5WIk -8GxibarfOVEkJzSFkbXk74MXqvIMloeohYhHBEkuelv4cE79UhArYm//tYNQn6qJ -8x7yu6tgZa3L5YlAXT162LXr4jz5eqnvYuEypjQUcFNsNGWAg50y05+lM855KQXZ -t1qVYXeKqoH6QAgXJCS/ETD8uVf6llryOjEtxLfhKn6zQ9Pkid1ui3iSWrtitSfF -l4HZdoECgYEA/sjGEjlT8MgGtZHFyjiGs5QBuPWDmMnF8yOTEs53zedxflOjgc0T -qp195QT1NP8v+N3KKPxViqWJ3zQIeKSIBSXKV+CVrY3mqF0aOTKVSiUHXJtswPcj -dva5fPToxbg6DslabfQ2U5gabwpnv33oC9F5FIGO6UwVK+y6Uvt9z3ECgYEA09zP -aw9lQkTtjPTmQXd9gymm3kLkNySZ7P2UhZqd1gw80YabIXUQxeDVkanTe5lmpIy3 -xI0fBb5L8nHfBatiRuT4HLg4FTQvSoszSQUwRr90oXdK2KFtdJ10nSalQkl2rKiu -Goiz1zk5v9i5snS5QUyjUjEVXuXmWtyGN1AEH4cCgYAoGNzjPVZyjGhR5vEj1CWO -2Xoz65+cShT3IOAik4/TPdtksDEJWob/0O1hP5h/kLGyDuWj8aJcwZSjJgM3SV2G -wd3IWrXzrGNil9RqsAVCt/Uio+tHqx1PuaCTg8+mXkn0zceHimmKWmO+mfqd7mi4 -g2z0xzcAjFLAqO7h5GZ2YQKBgQCIGBj5qfse9c4vNQZQWaS7w97YednF/nIOhdqJ -dIgdfuD2q2QtutiadFJc7t3WUPVZkLdfwFOESfjZdgKQzsYjymQDduc337zdQswg -BQA9AjG2oz3mKNR6C8dkR/XyveRJB1ZH3za/c5hAP8UR+N8kLknfu34B5ubxySVC -lNkBMwKBgDzeYdoblhseC3FNU0f0rgs3gemoqXOswv31gEULKxjKdRVhYc0CNZrJ -3Rf+cp5yjWitc0eLFNM3W0JblDacR4p27+hBheXv4VfdXswju6V/IwYOPsNb2Xe6 -6P3h7grmtV2H4wSXUv+hi5VWQfGj3DJWB6PiPyYQ4wzwy6Xw/4op ------END RSA PRIVATE KEY----- diff --git a/test-keys/root-key.pem b/test-keys/root-key.pem deleted file mode 100644 index a146568d87..0000000000 --- a/test-keys/root-key.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEA0ts+OiJPhFYyks33N3VVMFYAHXmBThx9tu2nIBspwXGDj4kU -tVbTtsw4+FP4zdOqVJAjcd17sFvQmIkCtNQriE1zbFZZ1wDUMGCrw60JM0sjbWwl -gOlio5iY/Zf/Xuh7zN/CNgyMs6hqF7Rwy+Vw9BvOG3/A2Xu0LzaRmNmzfd5KzMfq -kMqunC84CKSGy+jcwu9w2gcmj0Nbf7AcH9pNb4cdmuP2fnq//IFdDxesfMhZq+CI -1WiO3JtUzg3oxPr/RYcofI9PUkzRzbjn8u9j2gDJ0umCFDKOawhTmj51/rqMXU8I -cdsV3T0Iftq1DdweF4k85P6AoHKOAkRIaTsTlwIDAQABAoIBAH24w62d75OUYasu -q4yhpR2g6YipffO5ASwlH1UBXTA+IpdewL4u+yUvN3i5eMwgvJqXJsspqCpLVGDe -sIJpT1uB8qRSCFct41bFDSUq8yVmU8VWijYG8g1hWzc5fcZ1D/vkHsRjTzF/5WIk -8GxibarfOVEkJzSFkbXk74MXqvIMloeohYhHBEkuelv4cE79UhArYm//tYNQn6qJ -8x7yu6tgZa3L5YlAXT162LXr4jz5eqnvYuEypjQUcFNsNGWAg50y05+lM855KQXZ -t1qVYXeKqoH6QAgXJCS/ETD8uVf6llryOjEtxLfhKn6zQ9Pkid1ui3iSWrtitSfF -l4HZdoECgYEA/sjGEjlT8MgGtZHFyjiGs5QBuPWDmMnF8yOTEs53zedxflOjgc0T -qp195QT1NP8v+N3KKPxViqWJ3zQIeKSIBSXKV+CVrY3mqF0aOTKVSiUHXJtswPcj -dva5fPToxbg6DslabfQ2U5gabwpnv33oC9F5FIGO6UwVK+y6Uvt9z3ECgYEA09zP -aw9lQkTtjPTmQXd9gymm3kLkNySZ7P2UhZqd1gw80YabIXUQxeDVkanTe5lmpIy3 -xI0fBb5L8nHfBatiRuT4HLg4FTQvSoszSQUwRr90oXdK2KFtdJ10nSalQkl2rKiu -Goiz1zk5v9i5snS5QUyjUjEVXuXmWtyGN1AEH4cCgYAoGNzjPVZyjGhR5vEj1CWO -2Xoz65+cShT3IOAik4/TPdtksDEJWob/0O1hP5h/kLGyDuWj8aJcwZSjJgM3SV2G -wd3IWrXzrGNil9RqsAVCt/Uio+tHqx1PuaCTg8+mXkn0zceHimmKWmO+mfqd7mi4 -g2z0xzcAjFLAqO7h5GZ2YQKBgQCIGBj5qfse9c4vNQZQWaS7w97YednF/nIOhdqJ -dIgdfuD2q2QtutiadFJc7t3WUPVZkLdfwFOESfjZdgKQzsYjymQDduc337zdQswg -BQA9AjG2oz3mKNR6C8dkR/XyveRJB1ZH3za/c5hAP8UR+N8kLknfu34B5ubxySVC -lNkBMwKBgDzeYdoblhseC3FNU0f0rgs3gemoqXOswv31gEULKxjKdRVhYc0CNZrJ -3Rf+cp5yjWitc0eLFNM3W0JblDacR4p27+hBheXv4VfdXswju6V/IwYOPsNb2Xe6 -6P3h7grmtV2H4wSXUv+hi5VWQfGj3DJWB6PiPyYQ4wzwy6Xw/4op ------END RSA PRIVATE KEY----- diff --git a/test-keys/server-cert.pem b/test-keys/server-cert.pem deleted file mode 100644 index 1a0995c7af..0000000000 --- a/test-keys/server-cert.pem +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDBDCCAeygAwIBAgIJAJG2NWsnKE4aMA0GCSqGSIb3DQEBCwUAMA8xDTALBgNV -BAMMBHJvb3QwHhcNMTkwNjE4MTkwNjA0WhcNMzMwMjI0MTkwNjA0WjARMQ8wDQYD -VQQDDAZzZXJ2ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC0btZV -oXR6FVQuu6ou01zGZanM/Rf42a6dtPm5oUohbbHDTsfOrrZ6p6HdBn1CE4rrnUVF -Gd9N9gGMRGjHeF2MuF0E3PFyLmjHFZ8LvhoJqVrbvzO+SZsa5KqN+jiM/F6vjuFD -YnA9IC4bwWg88SoXYvdzBlpBjrZ+jXmTTu7jHkmGjwlTj4BFyFmXBcBY5drD8yAx -KY8h7c0vday+r54oUuNast4b+da6qbyTOvSkaGpefSVpvifZ5Ujxyj7+U5EpjxSq -9bi8z7eqZ/NXMrLqfBwXavLqitdZbIz+KsdRJs4GERa8FX35sGkt9fvHlc1IOwi/ -ujbRteiLQE9Lky33AgMBAAGjYTBfMBMGA1UdJQQMMAoGCCsGAQUFBwMBMEgGA1Ud -EQRBMD+HBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAGCCWxvY2FsaG9zdIYac3BpZmZl -Oi8vZ2hvc3R1bm5lbC9zZXJ2ZXIwDQYJKoZIhvcNAQELBQADggEBAKgmfGFidjjt -47+ut7gOKQG4//tclqk5gGQeC0a1dZ8oh+62OlFo+lZdm8seZYE/6sRI0RlQpBGU -CxFcyIGUIE2+wcNdW+rSG6PkZlx9/2RyqwzsFLkSiMBCJYMRMvBJ+0y9C9UrW3Y2 -NPTcGNbTdz3aIFFKo6+cXcji6e+vna9J2a6D6HlkdXCMhWn8zLJu1Dk1EHYPTrgC -52z70BOkGpwWEpAv4Ad4etQkTzO+FfcXi3FFWyURbHpYMG1tGAAI23JE+AZ8Oosp -5MudVntBQGcSKtpbWmKq/jU23C6HqOAhnP/890sv+ZN1AnemSHiyxDaLM6kAy/yf -zWpdG5th+QA= ------END CERTIFICATE----- diff --git a/test-keys/server-combined.pem b/test-keys/server-combined.pem deleted file mode 100644 index 3b1689b93f..0000000000 --- a/test-keys/server-combined.pem +++ /dev/null @@ -1,46 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDBDCCAeygAwIBAgIJAJG2NWsnKE4aMA0GCSqGSIb3DQEBCwUAMA8xDTALBgNV -BAMMBHJvb3QwHhcNMTkwNjE4MTkwNjA0WhcNMzMwMjI0MTkwNjA0WjARMQ8wDQYD -VQQDDAZzZXJ2ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC0btZV -oXR6FVQuu6ou01zGZanM/Rf42a6dtPm5oUohbbHDTsfOrrZ6p6HdBn1CE4rrnUVF -Gd9N9gGMRGjHeF2MuF0E3PFyLmjHFZ8LvhoJqVrbvzO+SZsa5KqN+jiM/F6vjuFD -YnA9IC4bwWg88SoXYvdzBlpBjrZ+jXmTTu7jHkmGjwlTj4BFyFmXBcBY5drD8yAx -KY8h7c0vday+r54oUuNast4b+da6qbyTOvSkaGpefSVpvifZ5Ujxyj7+U5EpjxSq -9bi8z7eqZ/NXMrLqfBwXavLqitdZbIz+KsdRJs4GERa8FX35sGkt9fvHlc1IOwi/ -ujbRteiLQE9Lky33AgMBAAGjYTBfMBMGA1UdJQQMMAoGCCsGAQUFBwMBMEgGA1Ud -EQRBMD+HBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAGCCWxvY2FsaG9zdIYac3BpZmZl -Oi8vZ2hvc3R1bm5lbC9zZXJ2ZXIwDQYJKoZIhvcNAQELBQADggEBAKgmfGFidjjt -47+ut7gOKQG4//tclqk5gGQeC0a1dZ8oh+62OlFo+lZdm8seZYE/6sRI0RlQpBGU -CxFcyIGUIE2+wcNdW+rSG6PkZlx9/2RyqwzsFLkSiMBCJYMRMvBJ+0y9C9UrW3Y2 -NPTcGNbTdz3aIFFKo6+cXcji6e+vna9J2a6D6HlkdXCMhWn8zLJu1Dk1EHYPTrgC -52z70BOkGpwWEpAv4Ad4etQkTzO+FfcXi3FFWyURbHpYMG1tGAAI23JE+AZ8Oosp -5MudVntBQGcSKtpbWmKq/jU23C6HqOAhnP/890sv+ZN1AnemSHiyxDaLM6kAy/yf -zWpdG5th+QA= ------END CERTIFICATE----- ------BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEAtG7WVaF0ehVULruqLtNcxmWpzP0X+NmunbT5uaFKIW2xw07H -zq62eqeh3QZ9QhOK651FRRnfTfYBjERox3hdjLhdBNzxci5oxxWfC74aCala278z -vkmbGuSqjfo4jPxer47hQ2JwPSAuG8FoPPEqF2L3cwZaQY62fo15k07u4x5Jho8J -U4+ARchZlwXAWOXaw/MgMSmPIe3NL3Wsvq+eKFLjWrLeG/nWuqm8kzr0pGhqXn0l -ab4n2eVI8co+/lORKY8UqvW4vM+3qmfzVzKy6nwcF2ry6orXWWyM/irHUSbOBhEW -vBV9+bBpLfX7x5XNSDsIv7o20bXoi0BPS5Mt9wIDAQABAoIBAQCXW2Lw8j+DKeE0 -UriwDx4ET8Pg8W7qkbCpGudhkKte32X/MFbsSiNJQNXHU2o/w6UFgShajxmDjJo/ -0CMVMSNIqF9fdPgVkFjUqI6CizXMZQSS0hHVzmkOZkOsVfqaShojqvfMTwdYP2Zu -Yg/PD37/n0V0eB++Xrk37/XMXKqIWbcmScR+YZHmuVcPVy+HST0irSYS5bCPp0CY -6fn2tYlGnv9zbEGxX2vYwJkLYZZ+w7zJP92l6Wsb+UesF3oVeHrqZwqR3H0UUbBl -XyXtiVKSrswsMGgAbPwygCYKWvrsYpNUqjGAfKc0aegLAK/zugBKD+pMYf5A4NT7 -IcobZzIBAoGBANwBwoIqy4fq0D1u+qfRFZAfVz11/ysVmW1MXWJbzRZkRJ7uCwgz -VlyCR/1RzEAKgDaudvS2jWPbADrxhMXZCZH6JvnjVoDvp/tH45VUlcXacbJzTX6T -485toMHZtTe3pCQc6Da/OxFRbjZrpM1BUaIMi5olMC17qzbgz4P9rPQNAoGBANHz -qJD0u4y7LXdPdvwTnoBhW3ju92nCm4ahCLSgpu0cYWMI6b+3m+QN2xuWGGtvd0XO -cBrEvaZ3bHz9JWg/R4lN8dd1yEZ5o/0Y1WBV1r3YTetz2gH4FQUyvfhkh2ropYiS -vsAy4MvLpLXwnmSFLUbuH9cTxsiAz+HQoVjgmBUTAoGAQKb3vP7Mfu2fMl55pHVK -C+dZ4MLbMJMRWlA1pSDrHOhsTfQQI+HKQDc85pFyH2O2l9sfM/ytgimqYKf255lH -ObG0YHzRP+Stjadrlsebl+AAx3sgy9C0AkavFihfG7eIseAY0XXS6tzuMWnirTrD -vYZIRA0nokYUY3UfJl+WwsUCgYA4xtbQFI7fmQLLoB7CPp//gdSV2LHp1OnwN34+ -Mq/RsXoYqSFlakbrHmAhjq6d5y/vHcutQYU0Dlm1V1QVY/95fy2mocB+ZojVejDB -85S2FFfGE7diDu/ITxeWo8EHRVD2pRCWs2udUT4CwcZj0qwS4XSU2lFIiaUuIRhx -C5O1JwKBgQDE3hwprPAunMNlfJD4nWXDn9aLF+4kP+vxKxJ95GeG0lpFYYnMEb5R -xp3wPS8u6mC0f90REe28/Y6s9HRXc9VOwa+/jA3Ye6MexiQimDYW1ClnOp2gRoq8 -KM4xtWWjzEVkEKIEtZKn+X78iP03+U/TAzUdff2ZNrUcOr6VhYs3pg== ------END RSA PRIVATE KEY----- diff --git a/test-keys/server-key.pem b/test-keys/server-key.pem deleted file mode 100644 index 3b190ddcb0..0000000000 --- a/test-keys/server-key.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEAtG7WVaF0ehVULruqLtNcxmWpzP0X+NmunbT5uaFKIW2xw07H -zq62eqeh3QZ9QhOK651FRRnfTfYBjERox3hdjLhdBNzxci5oxxWfC74aCala278z -vkmbGuSqjfo4jPxer47hQ2JwPSAuG8FoPPEqF2L3cwZaQY62fo15k07u4x5Jho8J -U4+ARchZlwXAWOXaw/MgMSmPIe3NL3Wsvq+eKFLjWrLeG/nWuqm8kzr0pGhqXn0l -ab4n2eVI8co+/lORKY8UqvW4vM+3qmfzVzKy6nwcF2ry6orXWWyM/irHUSbOBhEW -vBV9+bBpLfX7x5XNSDsIv7o20bXoi0BPS5Mt9wIDAQABAoIBAQCXW2Lw8j+DKeE0 -UriwDx4ET8Pg8W7qkbCpGudhkKte32X/MFbsSiNJQNXHU2o/w6UFgShajxmDjJo/ -0CMVMSNIqF9fdPgVkFjUqI6CizXMZQSS0hHVzmkOZkOsVfqaShojqvfMTwdYP2Zu -Yg/PD37/n0V0eB++Xrk37/XMXKqIWbcmScR+YZHmuVcPVy+HST0irSYS5bCPp0CY -6fn2tYlGnv9zbEGxX2vYwJkLYZZ+w7zJP92l6Wsb+UesF3oVeHrqZwqR3H0UUbBl -XyXtiVKSrswsMGgAbPwygCYKWvrsYpNUqjGAfKc0aegLAK/zugBKD+pMYf5A4NT7 -IcobZzIBAoGBANwBwoIqy4fq0D1u+qfRFZAfVz11/ysVmW1MXWJbzRZkRJ7uCwgz -VlyCR/1RzEAKgDaudvS2jWPbADrxhMXZCZH6JvnjVoDvp/tH45VUlcXacbJzTX6T -485toMHZtTe3pCQc6Da/OxFRbjZrpM1BUaIMi5olMC17qzbgz4P9rPQNAoGBANHz -qJD0u4y7LXdPdvwTnoBhW3ju92nCm4ahCLSgpu0cYWMI6b+3m+QN2xuWGGtvd0XO -cBrEvaZ3bHz9JWg/R4lN8dd1yEZ5o/0Y1WBV1r3YTetz2gH4FQUyvfhkh2ropYiS -vsAy4MvLpLXwnmSFLUbuH9cTxsiAz+HQoVjgmBUTAoGAQKb3vP7Mfu2fMl55pHVK -C+dZ4MLbMJMRWlA1pSDrHOhsTfQQI+HKQDc85pFyH2O2l9sfM/ytgimqYKf255lH -ObG0YHzRP+Stjadrlsebl+AAx3sgy9C0AkavFihfG7eIseAY0XXS6tzuMWnirTrD -vYZIRA0nokYUY3UfJl+WwsUCgYA4xtbQFI7fmQLLoB7CPp//gdSV2LHp1OnwN34+ -Mq/RsXoYqSFlakbrHmAhjq6d5y/vHcutQYU0Dlm1V1QVY/95fy2mocB+ZojVejDB -85S2FFfGE7diDu/ITxeWo8EHRVD2pRCWs2udUT4CwcZj0qwS4XSU2lFIiaUuIRhx -C5O1JwKBgQDE3hwprPAunMNlfJD4nWXDn9aLF+4kP+vxKxJ95GeG0lpFYYnMEb5R -xp3wPS8u6mC0f90REe28/Y6s9HRXc9VOwa+/jA3Ye6MexiQimDYW1ClnOp2gRoq8 -KM4xtWWjzEVkEKIEtZKn+X78iP03+U/TAzUdff2ZNrUcOr6VhYs3pg== ------END RSA PRIVATE KEY----- diff --git a/test-keys/server-keystore.p12 b/test-keys/server-keystore.p12 deleted file mode 100644 index a24b53cd42ffca65c88eb3a0251cf3d698bdfdb8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2381 zcmV-T39|Muf(c0i0Ru3C2@eJdDuzgg_YDCD0ic2j00e>v{4jzD_%MP4uLcP!hDe6@ z4FLxRpn?OSFoFY|0s#Opf&-NX2`Yw2hW8Bt2LUh~1_~;MNQUPRdCf@aRxF1dHcE<#x&TCA{jF^4M^L=Lgd6ZzJYRVSZ;rem6w;j z&o}9du@lbptXyx_(N7TXXp$$=>%~lW;gb8$OL~TDuOB4LF+lUYBk&(W)_FL` zzZ!mlo8rNduR9h7BueW&I_hcKElNu$TR-Yuc?pQsW<~I4p%Gdm{;!-1U`KjXW#+2{ zT;_`jd>^aHCB7VE$*vgVF@`9+VP46;-yZmf_^Peo2u(jE!-pjK)^2%{HwiDl8Rh;j z1-+ls3G)q7d29-CT@-!de3d6~bON9$a83hC2Moy9SYr$Qq{G3`+H#Q6OgT23pCa!e z^k>H2JfrE&Sv_r>nB}cCyUqBH@ha|#qp&=FUlF07auDD#F9|<48``+n!+sH|1^+p` zX0sdf7-`A#^ip^6Enk8=bYenj8JgN}asj|J;iW2tfB~vsEWvB%AZFap3Ds2^n&*3e zQ9vU1kPlc4HLGl)72HfgzT&HH_VrbOu=S~P8@w3tENwqrrZU+fJS@hz38z%&Q;vS+ zDnb=50ISh>0Q*dF8-8I5kN0=o2-k5T{SjZau5SgUX|&89WQ2LP<9Xx$h(gaK6BI12 zjh5Pt0@Y}^)qxeFdJm3uy8dS@wLyju>DqK@%Z4pM?)u$u+9r#il%;-*eI0)md-8Yd z8O;CrBY)7hTaCE%i>==>_;uTftQQBAZoA6$DC&va10MrXvg%1|-dMS-bTuYt_~em< zE>C-7jIL~@tjDc$W$0}Zxlj$c+x9NjD3aL^-DMhgvkVX6)tV9AJX$F zdiaBl?{EZHrJlRVe^)Djs(?Mn`$YUmOS?y)M>+gAl9%%ph15HL*acwjx!4)hiMu1~ zp$^g)Dx?Z0Ej4N#We@U~PV?txT5=}~5EBD#UsDMEiZ|xrbW#Ia6jJUvYQ81M9g=B>j#Ab-b zFoFd^1_>&LNQUW7;118lhWR!chv zHLnlugTo7uK@l0nBoR@eg0N;5_i2W$a;mQUAlvEN*&tR0$cyN-DCvsV#B`~@0eons zn1~HRfS)Tms>n*3yKQEqej2CHYGU^PkaR$D1=d(wLXQkF~D>%CE*ThbC?a>1Zl7sa0 z3)wX4@>ii%lRlz}TyuqScd|Xk3Fru^Ccd6hF|CMxM{4-AIoH)!z6#aL~^TA=w-kjg9QMQ`jvriPz`3m%n$hp3T=h~+c4I_R+ zUAl*Ipzuv;7~n9T(CKlt3Oa!~A>$8Sd1CqRvSG2rD5xDV1nq31?`xEd(!{FKhpR3! z1ukn&x=DZg4fJmh&|Pl-*b2RTh>Qyo(muy??G$}g;i~`%${a0RfEBBALTjAW@>r5Kw-1s38t9O$DBlvB}lWoMV)AP@K7&3i^1^n@rao%?z8lN z_9X!J2>I^w99=cD9+P(UZbHzTB%1LzP*TZ@k(#lSGlZqr4b4tkVwa|d;pU@=z+#Q_ zZH^jl487do8TL5~`v= pb.StartLine && (pi.StartLine != pb.StartLine || pi.StartCol >= pb.StartCol) + } + + i := 0 + if sortFunc(i) != true { + i = sort.Search(len(p.Blocks)-startIndex, sortFunc) + } + i += startIndex + if i < len(p.Blocks) && p.Blocks[i].StartLine == pb.StartLine && p.Blocks[i].StartCol == pb.StartCol { + if p.Blocks[i].EndLine != pb.EndLine || p.Blocks[i].EndCol != pb.EndCol { + log.Fatalf("OVERLAP MERGE: %v %v %v", p.FileName, p.Blocks[i], pb) + } + switch p.Mode { + case "set": + p.Blocks[i].Count |= pb.Count + case "count", "atomic": + p.Blocks[i].Count += pb.Count + default: + log.Fatalf("unsupported covermode: '%s'", p.Mode) + } + } else { + if i > 0 { + pa := p.Blocks[i-1] + if pa.EndLine >= pb.EndLine && (pa.EndLine != pb.EndLine || pa.EndCol > pb.EndCol) { + log.Fatalf("OVERLAP BEFORE: %v %v %v", p.FileName, pa, pb) + } + } + if i < len(p.Blocks)-1 { + pa := p.Blocks[i+1] + if pa.StartLine <= pb.StartLine && (pa.StartLine != pb.StartLine || pa.StartCol < pb.StartCol) { + log.Fatalf("OVERLAP AFTER: %v %v %v", p.FileName, pa, pb) + } + } + p.Blocks = append(p.Blocks, cover.ProfileBlock{}) + copy(p.Blocks[i+1:], p.Blocks[i:]) + p.Blocks[i] = pb + } + return i + 1 +} + +func addProfile(profiles []*cover.Profile, p *cover.Profile) []*cover.Profile { + i := sort.Search(len(profiles), func(i int) bool { return profiles[i].FileName >= p.FileName }) + if i < len(profiles) && profiles[i].FileName == p.FileName { + mergeProfiles(profiles[i], p) + } else { + profiles = append(profiles, nil) + copy(profiles[i+1:], profiles[i:]) + profiles[i] = p + } + return profiles +} + +func dumpProfiles(profiles []*cover.Profile, out io.Writer) { + if len(profiles) == 0 { + return + } + fmt.Fprintf(out, "mode: %s\n", profiles[0].Mode) + for _, p := range profiles { + for _, b := range p.Blocks { + fmt.Fprintf(out, "%s:%d.%d,%d.%d %d %d\n", p.FileName, b.StartLine, b.StartCol, b.EndLine, b.EndCol, b.NumStmt, b.Count) + } + } +} + +func main() { + flag.Parse() + + var merged []*cover.Profile + + for _, file := range flag.Args() { + profiles, err := cover.ParseProfiles(file) + if err != nil { + log.Fatalf("failed to parse profiles: %v", err) + } + for _, p := range profiles { + merged = addProfile(merged, p) + } + } + + dumpProfiles(merged, os.Stdout) +} diff --git a/vendor/golang.org/x/tools/cover/profile.go b/vendor/golang.org/x/tools/cover/profile.go new file mode 100644 index 0000000000..47a9a54116 --- /dev/null +++ b/vendor/golang.org/x/tools/cover/profile.go @@ -0,0 +1,266 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package cover provides support for parsing coverage profiles +// generated by "go test -coverprofile=cover.out". +package cover // import "golang.org/x/tools/cover" + +import ( + "bufio" + "errors" + "fmt" + "io" + "math" + "os" + "sort" + "strconv" + "strings" +) + +// Profile represents the profiling data for a specific file. +type Profile struct { + FileName string + Mode string + Blocks []ProfileBlock +} + +// ProfileBlock represents a single block of profiling data. +type ProfileBlock struct { + StartLine, StartCol int + EndLine, EndCol int + NumStmt, Count int +} + +type byFileName []*Profile + +func (p byFileName) Len() int { return len(p) } +func (p byFileName) Less(i, j int) bool { return p[i].FileName < p[j].FileName } +func (p byFileName) Swap(i, j int) { p[i], p[j] = p[j], p[i] } + +// ParseProfiles parses profile data in the specified file and returns a +// Profile for each source file described therein. +func ParseProfiles(fileName string) ([]*Profile, error) { + pf, err := os.Open(fileName) + if err != nil { + return nil, err + } + defer pf.Close() + return ParseProfilesFromReader(pf) +} + +// ParseProfilesFromReader parses profile data from the Reader and +// returns a Profile for each source file described therein. +func ParseProfilesFromReader(rd io.Reader) ([]*Profile, error) { + // First line is "mode: foo", where foo is "set", "count", or "atomic". + // Rest of file is in the format + // encoding/base64/base64.go:34.44,37.40 3 1 + // where the fields are: name.go:line.column,line.column numberOfStatements count + files := make(map[string]*Profile) + s := bufio.NewScanner(rd) + mode := "" + for s.Scan() { + line := s.Text() + if mode == "" { + const p = "mode: " + if !strings.HasPrefix(line, p) || line == p { + return nil, fmt.Errorf("bad mode line: %v", line) + } + mode = line[len(p):] + continue + } + fn, b, err := parseLine(line) + if err != nil { + return nil, fmt.Errorf("line %q doesn't match expected format: %v", line, err) + } + p := files[fn] + if p == nil { + p = &Profile{ + FileName: fn, + Mode: mode, + } + files[fn] = p + } + p.Blocks = append(p.Blocks, b) + } + if err := s.Err(); err != nil { + return nil, err + } + for _, p := range files { + sort.Sort(blocksByStart(p.Blocks)) + // Merge samples from the same location. + j := 1 + for i := 1; i < len(p.Blocks); i++ { + b := p.Blocks[i] + last := p.Blocks[j-1] + if b.StartLine == last.StartLine && + b.StartCol == last.StartCol && + b.EndLine == last.EndLine && + b.EndCol == last.EndCol { + if b.NumStmt != last.NumStmt { + return nil, fmt.Errorf("inconsistent NumStmt: changed from %d to %d", last.NumStmt, b.NumStmt) + } + if mode == "set" { + p.Blocks[j-1].Count |= b.Count + } else { + p.Blocks[j-1].Count += b.Count + } + continue + } + p.Blocks[j] = b + j++ + } + p.Blocks = p.Blocks[:j] + } + // Generate a sorted slice. + profiles := make([]*Profile, 0, len(files)) + for _, profile := range files { + profiles = append(profiles, profile) + } + sort.Sort(byFileName(profiles)) + return profiles, nil +} + +// parseLine parses a line from a coverage file. +// It is equivalent to the regex +// ^(.+):([0-9]+)\.([0-9]+),([0-9]+)\.([0-9]+) ([0-9]+) ([0-9]+)$ +// +// However, it is much faster: https://golang.org/cl/179377 +func parseLine(l string) (fileName string, block ProfileBlock, err error) { + end := len(l) + + b := ProfileBlock{} + b.Count, end, err = seekBack(l, ' ', end, "Count") + if err != nil { + return "", b, err + } + b.NumStmt, end, err = seekBack(l, ' ', end, "NumStmt") + if err != nil { + return "", b, err + } + b.EndCol, end, err = seekBack(l, '.', end, "EndCol") + if err != nil { + return "", b, err + } + b.EndLine, end, err = seekBack(l, ',', end, "EndLine") + if err != nil { + return "", b, err + } + b.StartCol, end, err = seekBack(l, '.', end, "StartCol") + if err != nil { + return "", b, err + } + b.StartLine, end, err = seekBack(l, ':', end, "StartLine") + if err != nil { + return "", b, err + } + fn := l[0:end] + if fn == "" { + return "", b, errors.New("a FileName cannot be blank") + } + return fn, b, nil +} + +// seekBack searches backwards from end to find sep in l, then returns the +// value between sep and end as an integer. +// If seekBack fails, the returned error will reference what. +func seekBack(l string, sep byte, end int, what string) (value int, nextSep int, err error) { + // Since we're seeking backwards and we know only ASCII is legal for these values, + // we can ignore the possibility of non-ASCII characters. + for start := end - 1; start >= 0; start-- { + if l[start] == sep { + i, err := strconv.Atoi(l[start+1 : end]) + if err != nil { + return 0, 0, fmt.Errorf("couldn't parse %q: %v", what, err) + } + if i < 0 { + return 0, 0, fmt.Errorf("negative values are not allowed for %s, found %d", what, i) + } + return i, start, nil + } + } + return 0, 0, fmt.Errorf("couldn't find a %s before %s", string(sep), what) +} + +type blocksByStart []ProfileBlock + +func (b blocksByStart) Len() int { return len(b) } +func (b blocksByStart) Swap(i, j int) { b[i], b[j] = b[j], b[i] } +func (b blocksByStart) Less(i, j int) bool { + bi, bj := b[i], b[j] + return bi.StartLine < bj.StartLine || bi.StartLine == bj.StartLine && bi.StartCol < bj.StartCol +} + +// Boundary represents the position in a source file of the beginning or end of a +// block as reported by the coverage profile. In HTML mode, it will correspond to +// the opening or closing of a tag and will be used to colorize the source +type Boundary struct { + Offset int // Location as a byte offset in the source file. + Start bool // Is this the start of a block? + Count int // Event count from the cover profile. + Norm float64 // Count normalized to [0..1]. + Index int // Order in input file. +} + +// Boundaries returns a Profile as a set of Boundary objects within the provided src. +func (p *Profile) Boundaries(src []byte) (boundaries []Boundary) { + // Find maximum count. + max := 0 + for _, b := range p.Blocks { + if b.Count > max { + max = b.Count + } + } + // Divisor for normalization. + divisor := math.Log(float64(max)) + + // boundary returns a Boundary, populating the Norm field with a normalized Count. + index := 0 + boundary := func(offset int, start bool, count int) Boundary { + b := Boundary{Offset: offset, Start: start, Count: count, Index: index} + index++ + if !start || count == 0 { + return b + } + if max <= 1 { + b.Norm = 0.8 // Profile is in"set" mode; we want a heat map. Use cov8 in the CSS. + } else if count > 0 { + b.Norm = math.Log(float64(count)) / divisor + } + return b + } + + line, col := 1, 2 // TODO: Why is this 2? + for si, bi := 0, 0; si < len(src) && bi < len(p.Blocks); { + b := p.Blocks[bi] + if b.StartLine == line && b.StartCol == col { + boundaries = append(boundaries, boundary(si, true, b.Count)) + } + if b.EndLine == line && b.EndCol == col || line > b.EndLine { + boundaries = append(boundaries, boundary(si, false, 0)) + bi++ + continue // Don't advance through src; maybe the next block starts here. + } + if src[si] == '\n' { + line++ + col = 0 + } + col++ + si++ + } + sort.Sort(boundariesByPos(boundaries)) + return +} + +type boundariesByPos []Boundary + +func (b boundariesByPos) Len() int { return len(b) } +func (b boundariesByPos) Swap(i, j int) { b[i], b[j] = b[j], b[i] } +func (b boundariesByPos) Less(i, j int) bool { + if b[i].Offset == b[j].Offset { + // Boundaries at the same offset should be ordered according to + // their original position. + return b[i].Index < b[j].Index + } + return b[i].Offset < b[j].Offset +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 70a6f9c1e8..8e3abc0992 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -431,6 +431,9 @@ github.com/vektah/gqlparser/v2/parser github.com/vektah/gqlparser/v2/validator github.com/vektah/gqlparser/v2/validator/core github.com/vektah/gqlparser/v2/validator/rules +# github.com/wadey/gocovmerge v0.0.0-20160331181800-b5bfa59ec0ad +## explicit +github.com/wadey/gocovmerge # github.com/weppos/publicsuffix-go v0.50.1 ## explicit; go 1.24.0 github.com/weppos/publicsuffix-go/publicsuffix @@ -595,6 +598,7 @@ golang.org/x/text/unicode/bidi golang.org/x/text/unicode/norm # golang.org/x/tools v0.39.0 ## explicit; go 1.24.0 +golang.org/x/tools/cover golang.org/x/tools/go/ast/edge golang.org/x/tools/go/ast/inspector golang.org/x/tools/go/gcexportdata From 6462041c1e26ca6e4710293aa15099316619388f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Jan 2026 04:05:42 +0000 Subject: [PATCH 03/11] Bump actions/download-artifact from 6 to 7 Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 6 to 7. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3e1b4fd0b1..f0c29545a5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -151,7 +151,7 @@ jobs: with: fetch-depth: 0 - name: Download artifact - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 with: name: ghostunnel-${{ matrix.target.os }}-${{ matrix.target.arch }} path: dist @@ -179,7 +179,7 @@ jobs: with: fetch-depth: 0 - name: Download artifact - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 with: name: ghostunnel-${{ matrix.target.os }}-${{ matrix.target.arch }}.exe path: dist From 1098325e4c98f1444bf0a6facc1c0d37c096f179 Mon Sep 17 00:00:00 2001 From: Cedric Staub Date: Wed, 7 Jan 2026 16:06:50 -0800 Subject: [PATCH 04/11] Make mage a tool dependency --- .github/workflows/compile.yml | 32 +- .github/workflows/docker.yml | 7 +- .github/workflows/release.yml | 25 +- .github/workflows/test.yml | 14 +- go.mod | 9 +- go.sum | 2 - .../github.com/magefile/mage/.gitattributes | 2 + vendor/github.com/magefile/mage/.gitignore | 38 + .../github.com/magefile/mage/.goreleaser.yml | 53 ++ .../magefile/mage/CODE_OF_CONDUCT.md | 85 ++ .../github.com/magefile/mage/CONTRIBUTING.md | 42 + vendor/github.com/magefile/mage/README.md | 81 ++ .../github.com/magefile/mage/internal/run.go | 115 +++ .../magefile/mage/mage/command_string.go | 16 + .../magefile/mage/mage/magefile_tmpl.go | 47 + vendor/github.com/magefile/mage/mage/main.go | 779 ++++++++++++++++ .../github.com/magefile/mage/mage/template.go | 488 ++++++++++ vendor/github.com/magefile/mage/magefile.go | 121 +++ vendor/github.com/magefile/mage/main.go | 11 + .../github.com/magefile/mage/parse/parse.go | 832 ++++++++++++++++++ vendor/modules.txt | 4 + 21 files changed, 2734 insertions(+), 69 deletions(-) create mode 100644 vendor/github.com/magefile/mage/.gitattributes create mode 100644 vendor/github.com/magefile/mage/.gitignore create mode 100644 vendor/github.com/magefile/mage/.goreleaser.yml create mode 100644 vendor/github.com/magefile/mage/CODE_OF_CONDUCT.md create mode 100644 vendor/github.com/magefile/mage/CONTRIBUTING.md create mode 100644 vendor/github.com/magefile/mage/README.md create mode 100644 vendor/github.com/magefile/mage/internal/run.go create mode 100644 vendor/github.com/magefile/mage/mage/command_string.go create mode 100644 vendor/github.com/magefile/mage/mage/magefile_tmpl.go create mode 100644 vendor/github.com/magefile/mage/mage/main.go create mode 100644 vendor/github.com/magefile/mage/mage/template.go create mode 100644 vendor/github.com/magefile/mage/magefile.go create mode 100644 vendor/github.com/magefile/mage/main.go create mode 100644 vendor/github.com/magefile/mage/parse/parse.go diff --git a/.github/workflows/compile.yml b/.github/workflows/compile.yml index 5a1475e426..64db4eb9f3 100644 --- a/.github/workflows/compile.yml +++ b/.github/workflows/compile.yml @@ -25,13 +25,8 @@ jobs: uses: actions/setup-go@v6 with: go-version: '1.25.x' - - name: Set up Mage - uses: magefile/mage-action@v3 - with: - install-only: true - version: v1.14.0 - name: Build containers - run: mage -v docker:build + run: go tool mage -v docker:build env: GITHUB_REF: ${{ github.ref }} @@ -57,16 +52,11 @@ jobs: with: fetch-depth: 100 fetch-tags: true - - name: Set up Mage - uses: magefile/mage-action@v3 - with: - install-only: true - version: v1.14.0 - name: Build binary run: | - CGO_ENABLED=1 GOARCH=amd64 mage -v go:build + CGO_ENABLED=1 GOARCH=amd64 go tool mage -v go:build mv ghostunnel ghostunnel-linux-amd64 - CGO_ENABLED=1 GOARCH=arm64 CC=aarch64-linux-gnu-gcc mage -v go:build + CGO_ENABLED=1 GOARCH=arm64 CC=aarch64-linux-gnu-gcc go tool mage -v go:build mv ghostunnel ghostunnel-linux-arm64 - name: Upload artifact uses: actions/upload-artifact@v5 @@ -96,16 +86,11 @@ jobs: with: fetch-depth: 100 fetch-tags: true - - name: Set up Mage - uses: magefile/mage-action@v3 - with: - install-only: true - version: v1.14.0 - name: Build binary run: | - CGO_ENABLED=1 GOARCH=amd64 mage -v go:build + CGO_ENABLED=1 GOARCH=amd64 go tool mage -v go:build mv ghostunnel ghostunnel-darwin-amd64 - CGO_ENABLED=1 GOARCH=arm64 mage -v go:build + CGO_ENABLED=1 GOARCH=arm64 go tool mage -v go:build mv ghostunnel ghostunnel-darwin-arm64 lipo -create -output ghostunnel-darwin-universal ghostunnel-darwin-amd64 ghostunnel-darwin-arm64 - name: Upload artifact @@ -141,13 +126,8 @@ jobs: with: fetch-depth: 100 fetch-tags: true - - name: Set up Mage - uses: magefile/mage-action@v3 - with: - install-only: true - version: v1.14.0 - name: Build binary - run: mage -v go:build + run: go tool mage -v go:build - name: Upload artifact uses: actions/upload-artifact@v5 with: diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 0cb5368747..2e887477a3 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -33,13 +33,8 @@ jobs: with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: Set up Mage - uses: magefile/mage-action@v3 - with: - install-only: true - version: v1.14.0 - name: Build and publish containers - run: mage -v docker:push + run: go tool mage -v docker:push env: GITHUB_REF: ${{ github.ref }} DOCKER_PLATFORMS: "linux/amd64,linux/arm64,linux/arm/v7" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a3fcc4d181..2a3331e6e2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,18 +26,13 @@ jobs: uses: actions/checkout@v6 with: fetch-depth: 0 - - name: Set up Mage - uses: magefile/mage-action@v3 - with: - install-only: true - version: v1.14.0 - name: Build binary env: VERSION: ${{ github.ref_name }} run: | - CGO_ENABLED=1 GOARCH=amd64 mage -v go:build + CGO_ENABLED=1 GOARCH=amd64 go tool mage -v go:build mv ghostunnel ghostunnel-linux-amd64 - CGO_ENABLED=1 GOARCH=arm64 CC=aarch64-linux-gnu-gcc mage -v go:build + CGO_ENABLED=1 GOARCH=arm64 CC=aarch64-linux-gnu-gcc go tool mage -v go:build mv ghostunnel ghostunnel-linux-arm64 - name: Upload artifact uses: actions/upload-artifact@v5 @@ -66,18 +61,13 @@ jobs: uses: actions/checkout@v6 with: fetch-depth: 0 - - name: Set up Mage - uses: magefile/mage-action@v3 - with: - install-only: true - version: v1.14.0 - name: Build binary env: VERSION: ${{ github.ref_name }} run: | - CGO_ENABLED=1 GOARCH=amd64 mage -v go:build + CGO_ENABLED=1 GOARCH=amd64 go tool mage -v go:build mv ghostunnel ghostunnel-darwin-amd64 - CGO_ENABLED=1 GOARCH=arm64 mage -v go:build + CGO_ENABLED=1 GOARCH=arm64 go tool mage -v go:build mv ghostunnel ghostunnel-darwin-arm64 lipo -create -output ghostunnel-darwin-universal ghostunnel-darwin-amd64 ghostunnel-darwin-arm64 - name: Upload artifact @@ -112,16 +102,11 @@ jobs: uses: actions/checkout@v6 with: fetch-depth: 0 - - name: Set up Mage - uses: magefile/mage-action@v3 - with: - install-only: true - version: v1.14.0 - name: Build binary env: VERSION: ${{ github.ref_name }} run: | - mage -v go:build + go tool mage -v go:build mv ghostunnel ghostunnel-windows-amd64.exe - name: Upload artifact uses: actions/upload-artifact@v5 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9bb5f8a6fa..466df8830f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,13 +30,8 @@ jobs: uses: actions/setup-python@v6 with: python-version: '3.11.x' - - name: Set up Mage - uses: magefile/mage-action@v3 - with: - install-only: true - version: v1.14.0 - name: Run tests - run: mage -v test:all + run: go tool mage -v test:all - name: Codecov upload if: ${{ github.actor != 'dependabot[bot]' && matrix.os != 'windows-latest' }} uses: codecov/codecov-action@v5 @@ -63,13 +58,8 @@ jobs: uses: actions/setup-go@v6 with: go-version: ${{ matrix.version }} - - name: Set up Mage - uses: magefile/mage-action@v3 - with: - install-only: true - version: v1.14.0 - name: Run tests in Docker - run: GO_VERSION=${{ matrix.version }} mage -v test:docker + run: GO_VERSION=${{ matrix.version }} go tool mage -v test:docker - name: Codecov upload uses: codecov/codecov-action@v5 if: ${{ github.actor != 'dependabot[bot]' }} diff --git a/go.mod b/go.mod index 7229c64397..67f1821744 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/kavu/go_reuseport v1.5.0 github.com/landlock-lsm/go-landlock v0.0.0-20251103212306-430f8e5cd97c github.com/letsencrypt/pkcs11key/v4 v4.0.0 + github.com/magefile/mage v1.15.0 github.com/mholt/acmez v1.2.0 github.com/open-policy-agent/opa v1.11.0 github.com/pires/go-proxyproto v0.8.1 @@ -26,6 +27,7 @@ require ( github.com/wrouesnel/go.connect-proxy-scheme v0.0.0-20240822095422-f6d0c8f327b9 golang.org/x/net v0.47.0 golang.org/x/sync v0.18.0 + golang.org/x/sys v0.38.0 google.golang.org/grpc v1.77.0 google.golang.org/protobuf v1.36.10 ) @@ -64,7 +66,6 @@ require ( github.com/lestrrat-go/option v1.0.1 // indirect github.com/lestrrat-go/option/v2 v2.0.0 // indirect github.com/libdns/libdns v1.1.1 // indirect - github.com/magefile/mage v1.15.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mholt/acmez/v3 v3.1.4 // indirect @@ -111,7 +112,6 @@ require ( golang.org/x/crypto v0.45.0 // indirect golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect golang.org/x/mod v0.30.0 // indirect - golang.org/x/sys v0.38.0 // indirect golang.org/x/text v0.31.0 // indirect golang.org/x/tools v0.39.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 // indirect @@ -123,4 +123,7 @@ require ( go 1.24.6 -tool github.com/wadey/gocovmerge +tool ( + github.com/magefile/mage + github.com/wadey/gocovmerge +) diff --git a/go.sum b/go.sum index 51df6ff74e..e6c8ac5df8 100644 --- a/go.sum +++ b/go.sum @@ -314,8 +314,6 @@ github.com/zmap/zcrypto v0.0.0-20201128221613-3719af1573cf/go.mod h1:aPM7r+JOkfL github.com/zmap/zcrypto v0.0.0-20201211161100-e54a5822fb7e/go.mod h1:aPM7r+JOkfL+9qSB4KbYjtoEzJqUK50EXkkJabeNJDQ= github.com/zmap/zcrypto v0.0.0-20250129210703-03c45d0bae98 h1:Qp98bmMm9JHPPOaLi2Nb6oWoZ+1OyOMWI7PPeJrirI0= github.com/zmap/zcrypto v0.0.0-20250129210703-03c45d0bae98/go.mod h1:YTUyN/U1oJ7RzCEY5hUweYxbVUu7X+11wB7OXZT15oE= -github.com/zmap/zcrypto v0.0.0-20251114214934-bb32b590b717 h1:4x89hS1LZY4YctWc+XOj4BEz08bUNRTraXtHQRThsmI= -github.com/zmap/zcrypto v0.0.0-20251114214934-bb32b590b717/go.mod h1:NBtLpB/eitQk7/yXV7mHbZ/Gtmigw0Un9H9DEnrl+Zg= github.com/zmap/zlint/v3 v3.0.0/go.mod h1:paGwFySdHIBEMJ61YjoqT4h7Ge+fdYG4sUQhnTb1lJ8= github.com/zmap/zlint/v3 v3.6.8 h1:ZvdfwSgqPxeD2Sb0yhH/7jzrpFHBKAjPhzKr1vVev9c= github.com/zmap/zlint/v3 v3.6.8/go.mod h1:Tm0qwwaO629pgJ/En7M9U9Edx4+rQRuoeXVpXvgVHhA= diff --git a/vendor/github.com/magefile/mage/.gitattributes b/vendor/github.com/magefile/mage/.gitattributes new file mode 100644 index 0000000000..571c1ad444 --- /dev/null +++ b/vendor/github.com/magefile/mage/.gitattributes @@ -0,0 +1,2 @@ +site/* linguist-documentation +vendor/* linguist-vendored \ No newline at end of file diff --git a/vendor/github.com/magefile/mage/.gitignore b/vendor/github.com/magefile/mage/.gitignore new file mode 100644 index 0000000000..8d101a18d1 --- /dev/null +++ b/vendor/github.com/magefile/mage/.gitignore @@ -0,0 +1,38 @@ +# Binaries for programs and plugins +*.exe +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 +.glide/ + +# Magefile output +mage_output_file.go + +# VScode +.vscode + +# stupid osx +.DS_Store + +# Goland +.idea +*.iml + +# Vim +*.sw[op] +Session.vim +*~ + +# GNU Screen +.screenrc + +# Hugo build lock +.hugo_build.lock diff --git a/vendor/github.com/magefile/mage/.goreleaser.yml b/vendor/github.com/magefile/mage/.goreleaser.yml new file mode 100644 index 0000000000..4123b2dc2e --- /dev/null +++ b/vendor/github.com/magefile/mage/.goreleaser.yml @@ -0,0 +1,53 @@ +project_name: mage +release: + github: + owner: magefile + name: mage + draft: true +build: + binary: mage + main: . + ldflags: -s -w -X github.com/magefile/mage/mage.timestamp={{.Date}} -X github.com/magefile/mage/mage.commitHash={{.Commit}} -X github.com/magefile/mage/mage.gitTag={{.Version}} + goos: + - darwin + - linux + - windows + - freebsd + - netbsd + - openbsd + - dragonfly + goarch: + - amd64 + - arm + - arm64 + ignore: + - goos: openbsd + goarch: arm + goarm: 6 + env: + - CGO_ENABLED=0 +archives: +- + name_template: "{{.Binary}}_{{.Version}}_{{.Os}}-{{.Arch}}" + replacements: + amd64: 64bit + 386: 32bit + arm: ARM + arm64: ARM64 + darwin: macOS + linux: Linux + windows: Windows + openbsd: OpenBSD + netbsd: NetBSD + freebsd: FreeBSD + dragonfly: DragonFlyBSD + format: tar.gz + format_overrides: + - goos: windows + format: zip + files: + - LICENSE +snapshot: + name_template: SNAPSHOT-{{ .Commit }} +checksum: + name_template: '{{ .ProjectName }}_{{ .Version }}_checksums.txt' diff --git a/vendor/github.com/magefile/mage/CODE_OF_CONDUCT.md b/vendor/github.com/magefile/mage/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000..10b331a51d --- /dev/null +++ b/vendor/github.com/magefile/mage/CODE_OF_CONDUCT.md @@ -0,0 +1,85 @@ +# Mage Community Code of Conduct + +This is a copy of the [go code of conduct](https://golang.org/conduct). It applies to all areas of the magefile github organization, the #mage and #mage-dev slack channels on gopher slack, and the mage google group (https://groups.google.com/forum/#!forum/magefile). + +Reports may be directed to Nate Finch, the Mage Project Steward at nate.finch@gmail.com, or Carmen Andoh and Van Riper, the Go Project Stewards at conduct@golang.org. + +## About +Online communities include people from many different backgrounds. The Go contributors are committed to providing a friendly, safe and welcoming environment for all, regardless of gender identity and expression, sexual orientation, disabilities, neurodiversity, physical appearance, body size, ethnicity, nationality, race, age, religion, or similar personal characteristics. + +The first goal of the Code of Conduct is to specify a baseline standard of behavior so that people with different social values and communication styles can talk about Go effectively, productively, and respectfully. + +The second goal is to provide a mechanism for resolving conflicts in the community when they arise. + +The third goal of the Code of Conduct is to make our community welcoming to people from different backgrounds. Diversity is critical to the project; for Go to be successful, it needs contributors and users from all backgrounds. (See Go, Open Source, Community.) + +We believe that healthy debate and disagreement are essential to a healthy project and community. However, it is never ok to be disrespectful. We value diverse opinions, but we value respectful behavior more. + +## Gopher values +These are the values to which people in the Go community (“Gophers”) should aspire. + +## Be friendly and welcoming +* Be patient + * Remember that people have varying communication styles and that not everyone is using their native language. (Meaning and tone can be lost in translation.) +* Be thoughtful + * Productive communication requires effort. Think about how your words will be interpreted. + * Remember that sometimes it is best to refrain entirely from commenting. +* Be respectful + * In particular, respect differences of opinion. +* Be charitable + * Interpret the arguments of others in good faith, do not seek to disagree. + * When we do disagree, try to understand why. +* Avoid destructive behavior: + * Derailing: stay on topic; if you want to talk about something else, start a new conversation. + * Unconstructive criticism: don't merely decry the current state of affairs; offer—or at least solicit—suggestions as to how things may be improved. + * Snarking (pithy, unproductive, sniping comments) + * Discussing potentially offensive or sensitive issues; this all too often leads to unnecessary conflict. + * Microaggressions: brief and commonplace verbal, behavioral and environmental indignities that communicate hostile, derogatory or negative slights and insults to a person or group. +People are complicated. You should expect to be misunderstood and to misunderstand others; when this inevitably occurs, resist the urge to be defensive or assign blame. Try not to take offense where no offense was intended. Give people the benefit of the doubt. Even if the intent was to provoke, do not rise to it. It is the responsibility of all parties to de-escalate conflict when it arises. + +## Code of Conduct +### Our Pledge +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. + +### Our Standards +Examples of behavior that contributes to creating a positive environment include: + +Using welcoming and inclusive language +Being respectful of differing viewpoints and experiences +Gracefully accepting constructive criticism +Focusing on what is best for the community +Showing empathy towards other community members +Examples of unacceptable behavior by participants include: + +The use of sexualized language or imagery and unwelcome sexual attention or advances +Trolling, insulting/derogatory comments, and personal or political attacks +Public or private harassment +Publishing others’ private information, such as a physical or electronic address, without explicit permission +Other conduct which could reasonably be considered inappropriate in a professional setting +Our Responsibilities +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +### Scope +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +This Code of Conduct also applies outside the project spaces when the Project Stewards have a reasonable belief that an individual’s behavior may have a negative impact on the project or its community. + +### Conflict Resolution +We do not believe that all conflict is bad; healthy debate and disagreement often yield positive results. However, it is never okay to be disrespectful or to engage in behavior that violates the project’s code of conduct. + +If you see someone violating the code of conduct, you are encouraged to address the behavior directly with those involved. Many issues can be resolved quickly and easily, and this gives people more control over the outcome of their dispute. If you are unable to resolve the matter for any reason, or if the behavior is threatening or harassing, report it. We are dedicated to providing an environment where participants feel welcome and safe. + +Reports should be directed to Carmen Andoh and Van Riper, the Go Project Stewards, at conduct@golang.org \[or Nate Finch at nate.finch@gmail.com\]. It is the Project Stewards’ duty to receive and address reported violations of the code of conduct. They will then work with a committee consisting of representatives from the Open Source Programs Office and the Google Open Source Strategy team. If for any reason you are uncomfortable reaching out the Project Stewards, please email the Google Open Source Programs Office at opensource@google.com. + +We will investigate every complaint, but you may not receive a direct response. We will use our discretion in determining when and how to follow up on reported incidents, which may range from not taking action to permanent expulsion from the project and project-sponsored spaces. We will notify the accused of the report and provide them an opportunity to discuss it before any action is taken. The identity of the reporter will be omitted from the details of the report supplied to the accused. In potentially harmful situations, such as ongoing harassment or threats to anyone’s safety, we may take action without notice. + +### Attribution +This Code of Conduct is adapted from the Contributor Covenant, version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +## Summary +* Treat everyone with respect and kindness. +* Be thoughtful in how you communicate. +* Don’t be destructive or inflammatory. +* If you encounter an issue, please mail conduct@golang.org. diff --git a/vendor/github.com/magefile/mage/CONTRIBUTING.md b/vendor/github.com/magefile/mage/CONTRIBUTING.md new file mode 100644 index 0000000000..e1394d2045 --- /dev/null +++ b/vendor/github.com/magefile/mage/CONTRIBUTING.md @@ -0,0 +1,42 @@ +# Contributing + +Of course, contributions are more than welcome. Please read these guidelines for +making the process as painless as possible. + +## Discussion + +Development discussion should take place on the #mage channel of [gopher +slack](https://gophers.slack.com/). + +There is a separate #mage-dev channel that has the github app to post github +activity to the channel, to make it easy to follow. + +## Issues + +If there's an issue you'd like to work on, please comment on it, so we can +discuss approach, etc. and make sure no one else is currently working on that +issue. + +Please always create an issue before sending a PR unless it's an obvious typo +or other trivial change. + +## Dependency Management + +Currently mage has no dependencies(!) outside the standard libary. Let's keep +it that way. Since it's likely that mage will be vendored into a project, +adding dependencies to mage adds dependencies to every project that uses mage. + +## Versions + +Please avoid using features of go and the stdlib that prevent mage from being +buildable with older versions of Go. The CI tests currently check that mage is +buildable with go 1.7 and later. You may build with whatever version you like, +but CI has the final say. + +## Testing + +Please write tests for any new features. Tests must use the normal go testing +package. + +Tests must pass the race detector (run `go test -race ./...`). + diff --git a/vendor/github.com/magefile/mage/README.md b/vendor/github.com/magefile/mage/README.md new file mode 100644 index 0000000000..d96fd025a7 --- /dev/null +++ b/vendor/github.com/magefile/mage/README.md @@ -0,0 +1,81 @@ +[![Built with Mage](https://magefile.org/badge.svg)](https://magefile.org) +[![Build Status](https://travis-ci.org/magefile/mage.svg?branch=master)](https://travis-ci.org/magefile/mage) [![Build status](https://ci.appveyor.com/api/projects/status/n6h146y79xgxkidl/branch/master?svg=true)](https://ci.appveyor.com/project/natefinch/mage/branch/master) + +

+ +## About + +Mage is a make-like build tool using Go. You write plain-old go functions, +and Mage automatically uses them as Makefile-like runnable targets. + +## Installation + +Mage has no dependencies outside the Go standard library, and builds with Go 1.7 +and above (possibly even lower versions, but they're not regularly tested). + +**Using GOPATH** + +``` +go get -u -d github.com/magefile/mage +cd $GOPATH/src/github.com/magefile/mage +go run bootstrap.go +``` + +**Using Go Modules** + +``` +git clone https://github.com/magefile/mage +cd mage +go run bootstrap.go +``` + +This will download the code and then run the bootstrap script to build mage with +version infomation embedded in it. A normal `go get` (without -d) or `go +install` will build the binary correctly, but no version info will be embedded. +If you've done this, no worries, just go to `$GOPATH/src/github.com/magefile/mage` +and run `mage install` or `go run bootstrap.go` and a new binary will be created +with the correct version information. + +The mage binary will be created in your $GOPATH/bin directory. + +You may also install a binary release from our +[releases](https://github.com/magefile/mage/releases) page. + +## Demo + +[![Mage Demo](https://img.youtube.com/vi/GOqbD0lF-iA/maxresdefault.jpg)](https://www.youtube.com/watch?v=GOqbD0lF-iA) + +## Discussion + +Join the `#mage` channel on [gophers slack](https://gophers.slack.com/messages/general/) +or post on the [magefile google group](https://groups.google.com/forum/#!forum/magefile) +for discussion of usage, development, etc. + +# Documentation + +See [magefile.org](https://magefile.org) for full documentation. + +See [pkg.go.dev/github.com/magefile/mage/mage](https://pkg.go.dev/github.com/magefile/mage/mage) +for instructions on how to use Mage as a library. + +# Why? + +Makefiles are hard to read and hard to write. Mostly because makefiles are +essentially fancy bash scripts with significant white space and additional +make-related syntax. + +Mage lets you have multiple magefiles, name your magefiles whatever you want, +and they're easy to customize for multiple operating systems. Mage has no +dependencies (aside from go) and runs just fine on all major operating systems, +whereas make generally uses bash which is not well supported on Windows. Go is +superior to bash for any non-trivial task involving branching, looping, anything +that's not just straight line execution of commands. And if your project is +written in Go, why introduce another language as idiosyncratic as bash? Why not +use the language your contributors are already comfortable with? + +# Thanks + +If you use mage and like it, or any of my other software, and you'd like to show your appreciation, you can do so on my patreon: + +[](https://www.patreon.com/join/natefinch?) + diff --git a/vendor/github.com/magefile/mage/internal/run.go b/vendor/github.com/magefile/mage/internal/run.go new file mode 100644 index 0000000000..79b4f049ac --- /dev/null +++ b/vendor/github.com/magefile/mage/internal/run.go @@ -0,0 +1,115 @@ +package internal + +import ( + "bytes" + "fmt" + "io/ioutil" + "log" + "os" + "os/exec" + "runtime" + "strings" +) + +var debug *log.Logger = log.New(ioutil.Discard, "", 0) + +func SetDebug(l *log.Logger) { + debug = l +} + +func RunDebug(cmd string, args ...string) error { + env, err := EnvWithCurrentGOOS() + if err != nil { + return err + } + buf := &bytes.Buffer{} + errbuf := &bytes.Buffer{} + debug.Println("running", cmd, strings.Join(args, " ")) + c := exec.Command(cmd, args...) + c.Env = env + c.Stderr = errbuf + c.Stdout = buf + if err := c.Run(); err != nil { + debug.Print("error running '", cmd, strings.Join(args, " "), "': ", err, ": ", errbuf) + return err + } + debug.Println(buf) + return nil +} + +func OutputDebug(cmd string, args ...string) (string, error) { + env, err := EnvWithCurrentGOOS() + if err != nil { + return "", err + } + buf := &bytes.Buffer{} + errbuf := &bytes.Buffer{} + debug.Println("running", cmd, strings.Join(args, " ")) + c := exec.Command(cmd, args...) + c.Env = env + c.Stderr = errbuf + c.Stdout = buf + if err := c.Run(); err != nil { + errMsg := strings.TrimSpace(errbuf.String()) + debug.Print("error running '", cmd, strings.Join(args, " "), "': ", err, ": ", errMsg) + return "", fmt.Errorf("error running \"%s %s\": %s\n%s", cmd, strings.Join(args, " "), err, errMsg) + } + return strings.TrimSpace(buf.String()), nil +} + +// SplitEnv takes the results from os.Environ() (a []string of foo=bar values) +// and makes a map[string]string out of it. +func SplitEnv(env []string) (map[string]string, error) { + out := map[string]string{} + + for _, s := range env { + parts := strings.SplitN(s, "=", 2) + if len(parts) != 2 { + return nil, fmt.Errorf("badly formatted environment variable: %v", s) + } + out[parts[0]] = parts[1] + } + return out, nil +} + +// joinEnv converts the given map into a list of foo=bar environment variables, +// such as that outputted by os.Environ(). +func joinEnv(env map[string]string) []string { + vals := make([]string, 0, len(env)) + for k, v := range env { + vals = append(vals, k+"="+v) + } + return vals +} + +// EnvWithCurrentGOOS returns a copy of os.Environ with the GOOS and GOARCH set +// to runtime.GOOS and runtime.GOARCH. +func EnvWithCurrentGOOS() ([]string, error) { + vals, err := SplitEnv(os.Environ()) + if err != nil { + return nil, err + } + vals["GOOS"] = runtime.GOOS + vals["GOARCH"] = runtime.GOARCH + return joinEnv(vals), nil +} + +// EnvWithGOOS retuns the os.Environ() values with GOOS and/or GOARCH either set +// to their runtime value, or the given value if non-empty. +func EnvWithGOOS(goos, goarch string) ([]string, error) { + env, err := SplitEnv(os.Environ()) + if err != nil { + return nil, err + } + if goos == "" { + env["GOOS"] = runtime.GOOS + } else { + env["GOOS"] = goos + } + if goarch == "" { + env["GOARCH"] = runtime.GOARCH + } else { + env["GOARCH"] = goarch + } + return joinEnv(env), nil +} diff --git a/vendor/github.com/magefile/mage/mage/command_string.go b/vendor/github.com/magefile/mage/mage/command_string.go new file mode 100644 index 0000000000..fcdaf9f949 --- /dev/null +++ b/vendor/github.com/magefile/mage/mage/command_string.go @@ -0,0 +1,16 @@ +// Code generated by "stringer -type=Command"; DO NOT EDIT. + +package mage + +import "strconv" + +const _Command_name = "NoneVersionInitCleanCompileStatic" + +var _Command_index = [...]uint8{0, 4, 11, 15, 20, 33} + +func (i Command) String() string { + if i < 0 || i >= Command(len(_Command_index)-1) { + return "Command(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _Command_name[_Command_index[i]:_Command_index[i+1]] +} diff --git a/vendor/github.com/magefile/mage/mage/magefile_tmpl.go b/vendor/github.com/magefile/mage/mage/magefile_tmpl.go new file mode 100644 index 0000000000..1df9703792 --- /dev/null +++ b/vendor/github.com/magefile/mage/mage/magefile_tmpl.go @@ -0,0 +1,47 @@ +package mage + +var mageTpl = `//go:build mage +// +build mage + +package main + +import ( + "fmt" + "os" + "os/exec" + + "github.com/magefile/mage/mg" // mg contains helpful utility functions, like Deps +) + +// Default target to run when none is specified +// If not set, running mage will list available targets +// var Default = Build + +// A build step that requires additional params, or platform specific steps for example +func Build() error { + mg.Deps(InstallDeps) + fmt.Println("Building...") + cmd := exec.Command("go", "build", "-o", "MyApp", ".") + return cmd.Run() +} + +// A custom install step if you need your bin someplace other than go/bin +func Install() error { + mg.Deps(Build) + fmt.Println("Installing...") + return os.Rename("./MyApp", "/usr/bin/MyApp") +} + +// Manage your deps, or running package managers. +func InstallDeps() error { + fmt.Println("Installing Deps...") + cmd := exec.Command("go", "get", "github.com/stretchr/piglatin") + return cmd.Run() +} + +// Clean up after yourself +func Clean() { + fmt.Println("Cleaning...") + os.RemoveAll("MyApp") +} +` diff --git a/vendor/github.com/magefile/mage/mage/main.go b/vendor/github.com/magefile/mage/mage/main.go new file mode 100644 index 0000000000..0062bd35aa --- /dev/null +++ b/vendor/github.com/magefile/mage/mage/main.go @@ -0,0 +1,779 @@ +package mage + +import ( + "bytes" + "crypto/sha1" + "errors" + "flag" + "fmt" + "go/build" + "io" + "io/ioutil" + "log" + "os" + "os/exec" + "os/signal" + "path/filepath" + "regexp" + "runtime" + "sort" + "strings" + "syscall" + "text/template" + "time" + + "github.com/magefile/mage/internal" + "github.com/magefile/mage/mg" + "github.com/magefile/mage/parse" + "github.com/magefile/mage/sh" +) + +// magicRebuildKey is used when hashing the output binary to ensure that we get +// a new binary even if nothing in the input files or generated mainfile has +// changed. This can be used when we change how we parse files, or otherwise +// change the inputs to the compiling process. +const magicRebuildKey = "v0.3" + +// (Aaaa)(Bbbb) -> aaaaBbbb +var firstWordRx = regexp.MustCompile(`^([[:upper:]][^[:upper:]]+)([[:upper:]].*)$`) + +// (AAAA)(Bbbb) -> aaaaBbbb +var firstAbbrevRx = regexp.MustCompile(`^([[:upper:]]+)([[:upper:]][^[:upper:]].*)$`) + +func lowerFirstWord(s string) string { + if match := firstWordRx.FindStringSubmatch(s); match != nil { + return strings.ToLower(match[1]) + match[2] + } + if match := firstAbbrevRx.FindStringSubmatch(s); match != nil { + return strings.ToLower(match[1]) + match[2] + } + return strings.ToLower(s) +} + +var mainfileTemplate = template.Must(template.New("").Funcs(map[string]interface{}{ + "lower": strings.ToLower, + "lowerFirst": func(s string) string { + parts := strings.Split(s, ":") + for i, t := range parts { + parts[i] = lowerFirstWord(t) + } + return strings.Join(parts, ":") + }, +}).Parse(mageMainfileTplString)) +var initOutput = template.Must(template.New("").Parse(mageTpl)) + +const ( + mainfile = "mage_output_file.go" + initFile = "magefile.go" +) + +var debug = log.New(ioutil.Discard, "DEBUG: ", log.Ltime|log.Lmicroseconds) + +// set by ldflags when you "mage build" +var ( + commitHash = "" + timestamp = "" + gitTag = "" +) + +//go:generate stringer -type=Command + +// Command tracks invocations of mage that run without targets or other flags. +type Command int + +// The various command types +const ( + None Command = iota + Version // report the current version of mage + Init // create a starting template for mage + Clean // clean out old compiled mage binaries from the cache + CompileStatic // compile a static binary of the current directory +) + +// Main is the entrypoint for running mage. It exists external to mage's main +// function to allow it to be used from other programs, specifically so you can +// go run a simple file that run's mage's Main. +func Main() int { + return ParseAndRun(os.Stdout, os.Stderr, os.Stdin, os.Args[1:]) +} + +// Invocation contains the args for invoking a run of Mage. +type Invocation struct { + Debug bool // turn on debug messages + Dir string // directory to read magefiles from + WorkDir string // directory where magefiles will run + Force bool // forces recreation of the compiled binary + Verbose bool // tells the magefile to print out log statements + List bool // tells the magefile to print out a list of targets + Help bool // tells the magefile to print out help for a specific target + Keep bool // tells mage to keep the generated main file after compiling + Timeout time.Duration // tells mage to set a timeout to running the targets + CompileOut string // tells mage to compile a static binary to this path, but not execute + GOOS string // sets the GOOS when producing a binary with -compileout + GOARCH string // sets the GOARCH when producing a binary with -compileout + Ldflags string // sets the ldflags when producing a binary with -compileout + Stdout io.Writer // writer to write stdout messages to + Stderr io.Writer // writer to write stderr messages to + Stdin io.Reader // reader to read stdin from + Args []string // args to pass to the compiled binary + GoCmd string // the go binary command to run + CacheDir string // the directory where we should store compiled binaries + HashFast bool // don't rely on GOCACHE, just hash the magefiles +} + +// MagefilesDirName is the name of the default folder to look for if no directory was specified, +// if this folder exists it will be assumed mage package lives inside it. +const MagefilesDirName = "magefiles" + +// UsesMagefiles returns true if we are getting our mage files from a magefiles directory. +func (i Invocation) UsesMagefiles() bool { + return filepath.Base(i.Dir) == MagefilesDirName +} + +// ParseAndRun parses the command line, and then compiles and runs the mage +// files in the given directory with the given args (do not include the command +// name in the args). +func ParseAndRun(stdout, stderr io.Writer, stdin io.Reader, args []string) int { + errlog := log.New(stderr, "", 0) + out := log.New(stdout, "", 0) + inv, cmd, err := Parse(stderr, stdout, args) + inv.Stderr = stderr + inv.Stdin = stdin + if err == flag.ErrHelp { + return 0 + } + if err != nil { + errlog.Println("Error:", err) + return 2 + } + + switch cmd { + case Version: + out.Println("Mage Build Tool", gitTag) + out.Println("Build Date:", timestamp) + out.Println("Commit:", commitHash) + out.Println("built with:", runtime.Version()) + return 0 + case Init: + if err := generateInit(inv.Dir); err != nil { + errlog.Println("Error:", err) + return 1 + } + out.Println(initFile, "created") + return 0 + case Clean: + if err := removeContents(inv.CacheDir); err != nil { + out.Println("Error:", err) + return 1 + } + out.Println(inv.CacheDir, "cleaned") + return 0 + case CompileStatic: + return Invoke(inv) + case None: + return Invoke(inv) + default: + panic(fmt.Errorf("Unknown command type: %v", cmd)) + } +} + +// Parse parses the given args and returns structured data. If parse returns +// flag.ErrHelp, the calling process should exit with code 0. +func Parse(stderr, stdout io.Writer, args []string) (inv Invocation, cmd Command, err error) { + inv.Stdout = stdout + fs := flag.FlagSet{} + fs.SetOutput(stdout) + + // options flags + + fs.BoolVar(&inv.Force, "f", false, "force recreation of compiled magefile") + fs.BoolVar(&inv.Debug, "debug", mg.Debug(), "turn on debug messages") + fs.BoolVar(&inv.Verbose, "v", mg.Verbose(), "show verbose output when running mage targets") + fs.BoolVar(&inv.Help, "h", false, "show this help") + fs.DurationVar(&inv.Timeout, "t", 0, "timeout in duration parsable format (e.g. 5m30s)") + fs.BoolVar(&inv.Keep, "keep", false, "keep intermediate mage files around after running") + fs.StringVar(&inv.Dir, "d", "", "directory to read magefiles from") + fs.StringVar(&inv.WorkDir, "w", "", "working directory where magefiles will run") + fs.StringVar(&inv.GoCmd, "gocmd", mg.GoCmd(), "use the given go binary to compile the output") + fs.StringVar(&inv.GOOS, "goos", "", "set GOOS for binary produced with -compile") + fs.StringVar(&inv.GOARCH, "goarch", "", "set GOARCH for binary produced with -compile") + fs.StringVar(&inv.Ldflags, "ldflags", "", "set ldflags for binary produced with -compile") + + // commands below + + fs.BoolVar(&inv.List, "l", false, "list mage targets in this directory") + var showVersion bool + fs.BoolVar(&showVersion, "version", false, "show version info for the mage binary") + var mageInit bool + fs.BoolVar(&mageInit, "init", false, "create a starting template if no mage files exist") + var clean bool + fs.BoolVar(&clean, "clean", false, "clean out old generated binaries from CACHE_DIR") + var compileOutPath string + fs.StringVar(&compileOutPath, "compile", "", "output a static binary to the given path") + + fs.Usage = func() { + fmt.Fprint(stdout, ` +mage [options] [target] + +Mage is a make-like command runner. See https://magefile.org for full docs. + +Commands: + -clean clean out old generated binaries from CACHE_DIR + -compile + output a static binary to the given path + -h show this help + -init create a starting template if no mage files exist + -l list mage targets in this directory + -version show version info for the mage binary + +Options: + -d + directory to read magefiles from (default "." or "magefiles" if exists) + -debug turn on debug messages + -f force recreation of compiled magefile + -goarch sets the GOARCH for the binary created by -compile (default: current arch) + -gocmd + use the given go binary to compile the output (default: "go") + -goos sets the GOOS for the binary created by -compile (default: current OS) + -ldflags sets the ldflags for the binary created by -compile (default: "") + -h show description of a target + -keep keep intermediate mage files around after running + -t + timeout in duration parsable format (e.g. 5m30s) + -v show verbose output when running mage targets + -w + working directory where magefiles will run (default -d value) +`[1:]) + } + err = fs.Parse(args) + if err == flag.ErrHelp { + // parse will have already called fs.Usage() + return inv, cmd, err + } + if err == nil && inv.Help && len(fs.Args()) == 0 { + fs.Usage() + // tell upstream, to just exit + return inv, cmd, flag.ErrHelp + } + + numCommands := 0 + switch { + case mageInit: + numCommands++ + cmd = Init + case compileOutPath != "": + numCommands++ + cmd = CompileStatic + inv.CompileOut = compileOutPath + inv.Force = true + case showVersion: + numCommands++ + cmd = Version + case clean: + numCommands++ + cmd = Clean + if fs.NArg() > 0 { + // Temporary dupe of below check until we refactor the other commands to use this check + return inv, cmd, errors.New("-h, -init, -clean, -compile and -version cannot be used simultaneously") + } + } + if inv.Help { + numCommands++ + } + + if inv.Debug { + debug.SetOutput(stderr) + } + + inv.CacheDir = mg.CacheDir() + + if numCommands > 1 { + debug.Printf("%d commands defined", numCommands) + return inv, cmd, errors.New("-h, -init, -clean, -compile and -version cannot be used simultaneously") + } + + if cmd != CompileStatic && (inv.GOARCH != "" || inv.GOOS != "") { + return inv, cmd, errors.New("-goos and -goarch only apply when running with -compile") + } + + inv.Args = fs.Args() + if inv.Help && len(inv.Args) > 1 { + return inv, cmd, errors.New("-h can only show help for a single target") + } + + if len(inv.Args) > 0 && cmd != None { + return inv, cmd, fmt.Errorf("unexpected arguments to command: %q", inv.Args) + } + inv.HashFast = mg.HashFast() + return inv, cmd, err +} + +const dotDirectory = "." + +// Invoke runs Mage with the given arguments. +func Invoke(inv Invocation) int { + errlog := log.New(inv.Stderr, "", 0) + if inv.GoCmd == "" { + inv.GoCmd = "go" + } + if inv.Dir == "" { + inv.Dir = dotDirectory + } + if inv.WorkDir == "" { + inv.WorkDir = inv.Dir + } + magefilesDir := filepath.Join(inv.Dir, MagefilesDirName) + // . will be default unless we find a mage folder. + mfSt, err := os.Stat(magefilesDir) + if err == nil { + if mfSt.IsDir() { + stderrBuf := &bytes.Buffer{} + originalDir := inv.Dir + inv.Dir = magefilesDir // preemptive assignment + // TODO: Remove this fallback and the above Magefiles invocation when the bw compatibility is removed. + files, err := Magefiles(originalDir, inv.GOOS, inv.GOARCH, inv.GoCmd, stderrBuf, false, inv.Debug) + if err == nil { + if len(files) != 0 { + errlog.Println("[WARNING] You have both a magefiles directory and mage files in the " + + "current directory, in future versions the files will be ignored in favor of the directory") + inv.Dir = originalDir + } + } + } + } + + if inv.CacheDir == "" { + inv.CacheDir = mg.CacheDir() + } + + files, err := Magefiles(inv.Dir, inv.GOOS, inv.GOARCH, inv.GoCmd, inv.Stderr, inv.UsesMagefiles(), inv.Debug) + if err != nil { + errlog.Println("Error determining list of magefiles:", err) + return 1 + } + + if len(files) == 0 { + errlog.Println("No .go files marked with the mage build tag in this directory.") + return 1 + } + debug.Printf("found magefiles: %s", strings.Join(files, ", ")) + exePath := inv.CompileOut + if inv.CompileOut == "" { + exePath, err = ExeName(inv.GoCmd, inv.CacheDir, files) + if err != nil { + errlog.Println("Error getting exe name:", err) + return 1 + } + } + debug.Println("output exe is ", exePath) + + useCache := false + if inv.HashFast { + debug.Println("user has set MAGEFILE_HASHFAST, so we'll ignore GOCACHE") + } else { + s, err := internal.OutputDebug(inv.GoCmd, "env", "GOCACHE") + if err != nil { + errlog.Printf("failed to run %s env GOCACHE: %s", inv.GoCmd, err) + return 1 + } + + // if GOCACHE exists, always rebuild, so we catch transitive + // dependencies that have changed. + if s != "" { + debug.Println("go build cache exists, will ignore any compiled binary") + useCache = true + } + } + + if !useCache { + _, err = os.Stat(exePath) + switch { + case err == nil: + if inv.Force { + debug.Println("ignoring existing executable") + } else { + debug.Println("Running existing exe") + return RunCompiled(inv, exePath, errlog) + } + case os.IsNotExist(err): + debug.Println("no existing exe, creating new") + default: + debug.Printf("error reading existing exe at %v: %v", exePath, err) + debug.Println("creating new exe") + } + } + + // parse wants dir + filenames... arg + fnames := make([]string, 0, len(files)) + for i := range files { + fnames = append(fnames, filepath.Base(files[i])) + } + if inv.Debug { + parse.EnableDebug() + } + debug.Println("parsing files") + info, err := parse.PrimaryPackage(inv.GoCmd, inv.Dir, fnames) + if err != nil { + errlog.Println("Error parsing magefiles:", err) + return 1 + } + + // reproducible output for deterministic builds + sort.Sort(info.Funcs) + sort.Sort(info.Imports) + + main := filepath.Join(inv.Dir, mainfile) + binaryName := "mage" + if inv.CompileOut != "" { + binaryName = filepath.Base(inv.CompileOut) + } + + err = GenerateMainfile(binaryName, main, info) + if err != nil { + errlog.Println("Error:", err) + return 1 + } + if !inv.Keep { + defer os.RemoveAll(main) + } + files = append(files, main) + if err := Compile(inv.GOOS, inv.GOARCH, inv.Ldflags, inv.Dir, inv.GoCmd, exePath, files, inv.Debug, inv.Stderr, inv.Stdout); err != nil { + errlog.Println("Error:", err) + return 1 + } + if !inv.Keep { + // move aside this file before we run the compiled version, in case the + // compiled file screws things up. Yes this doubles up with the above + // defer, that's ok. + os.RemoveAll(main) + } else { + debug.Print("keeping mainfile") + } + + if inv.CompileOut != "" { + return 0 + } + + return RunCompiled(inv, exePath, errlog) +} + +type mainfileTemplateData struct { + Description string + Funcs []*parse.Function + DefaultFunc parse.Function + Aliases map[string]*parse.Function + Imports []*parse.Import + BinaryName string +} + +// listGoFiles returns a list of all .go files in a given directory, +// matching the provided tag +func listGoFiles(magePath, goCmd, tag string, envStr []string) ([]string, error) { + origMagePath := magePath + if !filepath.IsAbs(magePath) { + cwd, err := os.Getwd() + if err != nil { + return nil, fmt.Errorf("can't get current working directory: %v", err) + } + magePath = filepath.Join(cwd, magePath) + } + + env, err := internal.SplitEnv(envStr) + if err != nil { + return nil, fmt.Errorf("error parsing environment variables: %v", err) + } + + bctx := build.Default + bctx.BuildTags = []string{tag} + + if _, ok := env["GOOS"]; ok { + bctx.GOOS = env["GOOS"] + } + + if _, ok := env["GOARCH"]; ok { + bctx.GOARCH = env["GOARCH"] + } + + pkg, err := bctx.Import(".", magePath, 0) + if err != nil { + if _, ok := err.(*build.NoGoError); ok { + return []string{}, nil + } + + // Allow multiple packages in the same directory + if _, ok := err.(*build.MultiplePackageError); !ok { + return nil, fmt.Errorf("failed to parse go source files: %v", err) + } + } + + goFiles := make([]string, len(pkg.GoFiles)) + for i := range pkg.GoFiles { + goFiles[i] = filepath.Join(origMagePath, pkg.GoFiles[i]) + } + + debug.Printf("found %d go files with build tag %s (files: %v)", len(goFiles), tag, goFiles) + return goFiles, nil +} + +// Magefiles returns the list of magefiles in dir. +func Magefiles(magePath, goos, goarch, goCmd string, stderr io.Writer, isMagefilesDirectory, isDebug bool) ([]string, error) { + start := time.Now() + defer func() { + debug.Println("time to scan for Magefiles:", time.Since(start)) + }() + + env, err := internal.EnvWithGOOS(goos, goarch) + if err != nil { + return nil, err + } + + debug.Println("getting all files including those with mage tag in", magePath) + mageFiles, err := listGoFiles(magePath, goCmd, "mage", env) + if err != nil { + return nil, fmt.Errorf("listing mage files: %v", err) + } + + if isMagefilesDirectory { + // For the magefiles directory, we always use all go files, both with + // and without the mage tag, as per normal go build tag rules. + debug.Println("using all go files in magefiles directory", magePath) + return mageFiles, nil + } + + // For folders other than the magefiles directory, we only consider files + // that have the mage build tag and ignore those that don't. + + debug.Println("getting all files without mage tag in", magePath) + nonMageFiles, err := listGoFiles(magePath, goCmd, "", env) + if err != nil { + return nil, fmt.Errorf("listing non-mage files: %v", err) + } + + // convert non-Mage list to a map of files to exclude. + exclude := map[string]bool{} + for _, f := range nonMageFiles { + if f != "" { + debug.Printf("marked file as non-mage: %q", f) + exclude[f] = true + } + } + + // filter out the non-mage files from the mage files. + var files []string + for _, f := range mageFiles { + if f != "" && !exclude[f] { + files = append(files, f) + } + } + + return files, nil +} + +// Compile uses the go tool to compile the files into an executable at path. +func Compile(goos, goarch, ldflags, magePath, goCmd, compileTo string, gofiles []string, isDebug bool, stderr, stdout io.Writer) error { + debug.Println("compiling to", compileTo) + debug.Println("compiling using gocmd:", goCmd) + if isDebug { + internal.RunDebug(goCmd, "version") + internal.RunDebug(goCmd, "env") + } + environ, err := internal.EnvWithGOOS(goos, goarch) + if err != nil { + return err + } + // strip off the path since we're setting the path in the build command + for i := range gofiles { + gofiles[i] = filepath.Base(gofiles[i]) + } + buildArgs := []string{"build", "-o", compileTo} + if ldflags != "" { + buildArgs = append(buildArgs, "-ldflags", ldflags) + } + args := append(buildArgs, gofiles...) + + debug.Printf("running %s %s", goCmd, strings.Join(args, " ")) + c := exec.Command(goCmd, args...) + c.Env = environ + c.Stderr = stderr + c.Stdout = stdout + c.Dir = magePath + start := time.Now() + err = c.Run() + debug.Println("time to compile Magefile:", time.Since(start)) + if err != nil { + return errors.New("error compiling magefiles") + } + return nil +} + +// GenerateMainfile generates the mage mainfile at path. +func GenerateMainfile(binaryName, path string, info *parse.PkgInfo) error { + debug.Println("Creating mainfile at", path) + + f, err := os.Create(path) + if err != nil { + return fmt.Errorf("error creating generated mainfile: %v", err) + } + defer f.Close() + data := mainfileTemplateData{ + Description: info.Description, + Funcs: info.Funcs, + Aliases: info.Aliases, + Imports: info.Imports, + BinaryName: binaryName, + } + + if info.DefaultFunc != nil { + data.DefaultFunc = *info.DefaultFunc + } + + debug.Println("writing new file at", path) + if err := mainfileTemplate.Execute(f, data); err != nil { + return fmt.Errorf("can't execute mainfile template: %v", err) + } + if err := f.Close(); err != nil { + return fmt.Errorf("error closing generated mainfile: %v", err) + } + // we set an old modtime on the generated mainfile so that the go tool + // won't think it has changed more recently than the compiled binary. + longAgo := time.Now().Add(-time.Hour * 24 * 365 * 10) + if err := os.Chtimes(path, longAgo, longAgo); err != nil { + return fmt.Errorf("error setting old modtime on generated mainfile: %v", err) + } + return nil +} + +// ExeName reports the executable filename that this version of Mage would +// create for the given magefiles. +func ExeName(goCmd, cacheDir string, files []string) (string, error) { + var hashes []string + for _, s := range files { + h, err := hashFile(s) + if err != nil { + return "", err + } + hashes = append(hashes, h) + } + // hash the mainfile template to ensure if it gets updated, we make a new + // binary. + hashes = append(hashes, fmt.Sprintf("%x", sha1.Sum([]byte(mageMainfileTplString)))) + sort.Strings(hashes) + ver, err := internal.OutputDebug(goCmd, "version") + if err != nil { + return "", err + } + hash := sha1.Sum([]byte(strings.Join(hashes, "") + magicRebuildKey + ver)) + filename := fmt.Sprintf("%x", hash) + + out := filepath.Join(cacheDir, filename) + if runtime.GOOS == "windows" { + out += ".exe" + } + return out, nil +} + +func hashFile(fn string) (string, error) { + f, err := os.Open(fn) + if err != nil { + return "", fmt.Errorf("can't open input file for hashing: %#v", err) + } + defer f.Close() + + h := sha1.New() + if _, err := io.Copy(h, f); err != nil { + return "", fmt.Errorf("can't write data to hash: %v", err) + } + return fmt.Sprintf("%x", h.Sum(nil)), nil +} + +func generateInit(dir string) error { + debug.Println("generating default magefile in", dir) + f, err := os.Create(filepath.Join(dir, initFile)) + if err != nil { + return fmt.Errorf("could not create mage template: %v", err) + } + defer f.Close() + + if err := initOutput.Execute(f, nil); err != nil { + return fmt.Errorf("can't execute magefile template: %v", err) + } + + return nil +} + +// RunCompiled runs an already-compiled mage command with the given args, +func RunCompiled(inv Invocation, exePath string, errlog *log.Logger) int { + debug.Println("running binary", exePath) + c := exec.Command(exePath, inv.Args...) + c.Stderr = inv.Stderr + c.Stdout = inv.Stdout + c.Stdin = inv.Stdin + c.Dir = inv.Dir + if inv.WorkDir != inv.Dir { + c.Dir = inv.WorkDir + } + // intentionally pass through unaltered os.Environ here.. your magefile has + // to deal with it. + c.Env = os.Environ() + if inv.Verbose { + c.Env = append(c.Env, "MAGEFILE_VERBOSE=1") + } + if inv.List { + c.Env = append(c.Env, "MAGEFILE_LIST=1") + } + if inv.Help { + c.Env = append(c.Env, "MAGEFILE_HELP=1") + } + if inv.Debug { + c.Env = append(c.Env, "MAGEFILE_DEBUG=1") + } + if inv.GoCmd != "" { + c.Env = append(c.Env, fmt.Sprintf("MAGEFILE_GOCMD=%s", inv.GoCmd)) + } + if inv.Timeout > 0 { + c.Env = append(c.Env, fmt.Sprintf("MAGEFILE_TIMEOUT=%s", inv.Timeout.String())) + } + debug.Print("running magefile with mage vars:\n", strings.Join(filter(c.Env, "MAGEFILE"), "\n")) + // catch SIGINT to allow magefile to handle them + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT) + defer signal.Stop(sigCh) + err := c.Run() + if !sh.CmdRan(err) { + errlog.Printf("failed to run compiled magefile: %v", err) + } + return sh.ExitStatus(err) +} + +func filter(list []string, prefix string) []string { + var out []string + for _, s := range list { + if strings.HasPrefix(s, prefix) { + out = append(out, s) + } + } + return out +} + +// removeContents removes all files but not any subdirectories in the given +// directory. +func removeContents(dir string) error { + debug.Println("removing all files in", dir) + files, err := ioutil.ReadDir(dir) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + for _, f := range files { + if f.IsDir() { + continue + } + err = os.Remove(filepath.Join(dir, f.Name())) + if err != nil { + return err + } + } + return nil +} diff --git a/vendor/github.com/magefile/mage/mage/template.go b/vendor/github.com/magefile/mage/mage/template.go new file mode 100644 index 0000000000..af822f0f2d --- /dev/null +++ b/vendor/github.com/magefile/mage/mage/template.go @@ -0,0 +1,488 @@ +package mage + +// this template uses the "data" + +// var only for tests +var mageMainfileTplString = `// +build ignore + +package main + +import ( + "context" + _flag "flag" + _fmt "fmt" + _ioutil "io/ioutil" + _log "log" + "os" + "os/signal" + _filepath "path/filepath" + _sort "sort" + "strconv" + _strings "strings" + "syscall" + _tabwriter "text/tabwriter" + "time" + {{range .Imports}}{{.UniqueName}} "{{.Path}}" + {{end}} +) + +func main() { + // Use local types and functions in order to avoid name conflicts with additional magefiles. + type arguments struct { + Verbose bool // print out log statements + List bool // print out a list of targets + Help bool // print out help for a specific target + Timeout time.Duration // set a timeout to running the targets + Args []string // args contain the non-flag command-line arguments + } + + parseBool := func(env string) bool { + val := os.Getenv(env) + if val == "" { + return false + } + b, err := strconv.ParseBool(val) + if err != nil { + _log.Printf("warning: environment variable %s is not a valid bool value: %v", env, val) + return false + } + return b + } + + parseDuration := func(env string) time.Duration { + val := os.Getenv(env) + if val == "" { + return 0 + } + d, err := time.ParseDuration(val) + if err != nil { + _log.Printf("warning: environment variable %s is not a valid duration value: %v", env, val) + return 0 + } + return d + } + args := arguments{} + fs := _flag.FlagSet{} + fs.SetOutput(os.Stdout) + + // default flag set with ExitOnError and auto generated PrintDefaults should be sufficient + fs.BoolVar(&args.Verbose, "v", parseBool("MAGEFILE_VERBOSE"), "show verbose output when running targets") + fs.BoolVar(&args.List, "l", parseBool("MAGEFILE_LIST"), "list targets for this binary") + fs.BoolVar(&args.Help, "h", parseBool("MAGEFILE_HELP"), "print out help for a specific target") + fs.DurationVar(&args.Timeout, "t", parseDuration("MAGEFILE_TIMEOUT"), "timeout in duration parsable format (e.g. 5m30s)") + fs.Usage = func() { + _fmt.Fprintf(os.Stdout, ` + "`" + ` +%s [options] [target] + +Commands: + -l list targets in this binary + -h show this help + +Options: + -h show description of a target + -t + timeout in duration parsable format (e.g. 5m30s) + -v show verbose output when running targets + ` + "`" + `[1:], _filepath.Base(os.Args[0])) + } + if err := fs.Parse(os.Args[1:]); err != nil { + // flag will have printed out an error already. + return + } + args.Args = fs.Args() + if args.Help && len(args.Args) == 0 { + fs.Usage() + return + } + + // color is ANSI color type + type color int + + // If you add/change/remove any items in this constant, + // you will need to run "stringer -type=color" in this directory again. + // NOTE: Please keep the list in an alphabetical order. + const ( + black color = iota + red + green + yellow + blue + magenta + cyan + white + brightblack + brightred + brightgreen + brightyellow + brightblue + brightmagenta + brightcyan + brightwhite + ) + + // AnsiColor are ANSI color codes for supported terminal colors. + var ansiColor = map[color]string{ + black: "\u001b[30m", + red: "\u001b[31m", + green: "\u001b[32m", + yellow: "\u001b[33m", + blue: "\u001b[34m", + magenta: "\u001b[35m", + cyan: "\u001b[36m", + white: "\u001b[37m", + brightblack: "\u001b[30;1m", + brightred: "\u001b[31;1m", + brightgreen: "\u001b[32;1m", + brightyellow: "\u001b[33;1m", + brightblue: "\u001b[34;1m", + brightmagenta: "\u001b[35;1m", + brightcyan: "\u001b[36;1m", + brightwhite: "\u001b[37;1m", + } + + const _color_name = "blackredgreenyellowbluemagentacyanwhitebrightblackbrightredbrightgreenbrightyellowbrightbluebrightmagentabrightcyanbrightwhite" + + var _color_index = [...]uint8{0, 5, 8, 13, 19, 23, 30, 34, 39, 50, 59, 70, 82, 92, 105, 115, 126} + + colorToLowerString := func (i color) string { + if i < 0 || i >= color(len(_color_index)-1) { + return "color(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _color_name[_color_index[i]:_color_index[i+1]] + } + + // ansiColorReset is an ANSI color code to reset the terminal color. + const ansiColorReset = "\033[0m" + + // defaultTargetAnsiColor is a default ANSI color for colorizing targets. + // It is set to Cyan as an arbitrary color, because it has a neutral meaning + var defaultTargetAnsiColor = ansiColor[cyan] + + getAnsiColor := func(color string) (string, bool) { + colorLower := _strings.ToLower(color) + for k, v := range ansiColor { + colorConstLower := colorToLowerString(k) + if colorConstLower == colorLower { + return v, true + } + } + return "", false + } + + // Terminals which don't support color: + // TERM=vt100 + // TERM=cygwin + // TERM=xterm-mono + var noColorTerms = map[string]bool{ + "vt100": false, + "cygwin": false, + "xterm-mono": false, + } + + // terminalSupportsColor checks if the current console supports color output + // + // Supported: + // linux, mac, or windows's ConEmu, Cmder, putty, git-bash.exe, pwsh.exe + // Not supported: + // windows cmd.exe, powerShell.exe + terminalSupportsColor := func() bool { + envTerm := os.Getenv("TERM") + if _, ok := noColorTerms[envTerm]; ok { + return false + } + return true + } + + // enableColor reports whether the user has requested to enable a color output. + enableColor := func() bool { + b, _ := strconv.ParseBool(os.Getenv("MAGEFILE_ENABLE_COLOR")) + return b + } + + // targetColor returns the ANSI color which should be used to colorize targets. + targetColor := func() string { + s, exists := os.LookupEnv("MAGEFILE_TARGET_COLOR") + if exists == true { + if c, ok := getAnsiColor(s); ok == true { + return c + } + } + return defaultTargetAnsiColor + } + + // store the color terminal variables, so that the detection isn't repeated for each target + var enableColorValue = enableColor() && terminalSupportsColor() + var targetColorValue = targetColor() + + printName := func(str string) string { + if enableColorValue { + return _fmt.Sprintf("%s%s%s", targetColorValue, str, ansiColorReset) + } else { + return str + } + } + + list := func() error { + {{with .Description}}_fmt.Println(` + "`{{.}}\n`" + `) + {{- end}} + {{- $default := .DefaultFunc}} + targets := map[string]string{ + {{- range .Funcs}} + "{{lowerFirst .TargetName}}{{if and (eq .Name $default.Name) (eq .Receiver $default.Receiver)}}*{{end}}": {{printf "%q" .Synopsis}}, + {{- end}} + {{- range .Imports}}{{$imp := .}} + {{- range .Info.Funcs}} + "{{lowerFirst .TargetName}}{{if and (eq .Name $default.Name) (eq .Receiver $default.Receiver)}}*{{end}}": {{printf "%q" .Synopsis}}, + {{- end}} + {{- end}} + } + + keys := make([]string, 0, len(targets)) + for name := range targets { + keys = append(keys, name) + } + _sort.Strings(keys) + + _fmt.Println("Targets:") + w := _tabwriter.NewWriter(os.Stdout, 0, 4, 4, ' ', 0) + for _, name := range keys { + _fmt.Fprintf(w, " %v\t%v\n", printName(name), targets[name]) + } + err := w.Flush() + {{- if .DefaultFunc.Name}} + if err == nil { + _fmt.Println("\n* default target") + } + {{- end}} + return err + } + + var ctx context.Context + ctxCancel := func(){} + + // by deferring in a closure, we let the cancel function get replaced + // by the getContext function. + defer func() { + ctxCancel() + }() + + getContext := func() (context.Context, func()) { + if ctx == nil { + if args.Timeout != 0 { + ctx, ctxCancel = context.WithTimeout(context.Background(), args.Timeout) + } else { + ctx, ctxCancel = context.WithCancel(context.Background()) + } + } + + return ctx, ctxCancel + } + + runTarget := func(logger *_log.Logger, fn func(context.Context) error) interface{} { + var err interface{} + ctx, cancel := getContext() + d := make(chan interface{}) + go func() { + defer func() { + err := recover() + d <- err + }() + err := fn(ctx) + d <- err + }() + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT) + select { + case <-sigCh: + logger.Println("cancelling mage targets, waiting up to 5 seconds for cleanup...") + cancel() + cleanupCh := time.After(5 * time.Second) + + select { + // target exited by itself + case err = <-d: + return err + // cleanup timeout exceeded + case <-cleanupCh: + return _fmt.Errorf("cleanup timeout exceeded") + // second SIGINT received + case <-sigCh: + logger.Println("exiting mage") + return _fmt.Errorf("exit forced") + } + case <-ctx.Done(): + cancel() + e := ctx.Err() + _fmt.Printf("ctx err: %v\n", e) + return e + case err = <-d: + // we intentionally don't cancel the context here, because + // the next target will need to run with the same context. + return err + } + } + // This is necessary in case there aren't any targets, to avoid an unused + // variable error. + _ = runTarget + + handleError := func(logger *_log.Logger, err interface{}) { + if err != nil { + logger.Printf("Error: %+v\n", err) + type code interface { + ExitStatus() int + } + if c, ok := err.(code); ok { + os.Exit(c.ExitStatus()) + } + os.Exit(1) + } + } + _ = handleError + + // Set MAGEFILE_VERBOSE so mg.Verbose() reflects the flag value. + if args.Verbose { + os.Setenv("MAGEFILE_VERBOSE", "1") + } else { + os.Setenv("MAGEFILE_VERBOSE", "0") + } + + _log.SetFlags(0) + if !args.Verbose { + _log.SetOutput(_ioutil.Discard) + } + logger := _log.New(os.Stderr, "", 0) + if args.List { + if err := list(); err != nil { + _log.Println(err) + os.Exit(1) + } + return + } + + if args.Help { + if len(args.Args) < 1 { + logger.Println("no target specified") + os.Exit(2) + } + switch _strings.ToLower(args.Args[0]) { + {{range .Funcs -}} + case "{{lower .TargetName}}": + {{if ne .Comment "" -}} + _fmt.Println({{printf "%q" .Comment}}) + _fmt.Println() + {{end}} + _fmt.Print("Usage:\n\n\t{{$.BinaryName}} {{lower .TargetName}}{{range .Args}} <{{.Name}}>{{end}}\n\n") + var aliases []string + {{- $name := .Name -}} + {{- $recv := .Receiver -}} + {{range $alias, $func := $.Aliases}} + {{if and (eq $name $func.Name) (eq $recv $func.Receiver)}}aliases = append(aliases, "{{$alias}}"){{end -}} + {{- end}} + if len(aliases) > 0 { + _fmt.Printf("Aliases: %s\n\n", _strings.Join(aliases, ", ")) + } + return + {{end -}} + {{range .Imports -}} + {{range .Info.Funcs -}} + case "{{lower .TargetName}}": + {{if ne .Comment "" -}} + _fmt.Println({{printf "%q" .Comment}}) + _fmt.Println() + {{end}} + _fmt.Print("Usage:\n\n\t{{$.BinaryName}} {{lower .TargetName}}{{range .Args}} <{{.Name}}>{{end}}\n\n") + var aliases []string + {{- $name := .Name -}} + {{- $recv := .Receiver -}} + {{range $alias, $func := $.Aliases}} + {{if and (eq $name $func.Name) (eq $recv $func.Receiver)}}aliases = append(aliases, "{{$alias}}"){{end -}} + {{- end}} + if len(aliases) > 0 { + _fmt.Printf("Aliases: %s\n\n", _strings.Join(aliases, ", ")) + } + return + {{end -}} + {{end -}} + default: + logger.Printf("Unknown target: %q\n", args.Args[0]) + os.Exit(2) + } + } + if len(args.Args) < 1 { + {{- if .DefaultFunc.Name}} + ignoreDefault, _ := strconv.ParseBool(os.Getenv("MAGEFILE_IGNOREDEFAULT")) + if ignoreDefault { + if err := list(); err != nil { + logger.Println("Error:", err) + os.Exit(1) + } + return + } + {{.DefaultFunc.ExecCode}} + handleError(logger, ret) + return + {{- else}} + if err := list(); err != nil { + logger.Println("Error:", err) + os.Exit(1) + } + return + {{- end}} + } + for x := 0; x < len(args.Args); { + target := args.Args[x] + x++ + + // resolve aliases + switch _strings.ToLower(target) { + {{range $alias, $func := .Aliases}} + case "{{lower $alias}}": + target = "{{$func.TargetName}}" + {{- end}} + } + + switch _strings.ToLower(target) { + {{range .Funcs }} + case "{{lower .TargetName}}": + expected := x + {{len .Args}} + if expected > len(args.Args) { + // note that expected and args at this point include the arg for the target itself + // so we subtract 1 here to show the number of args without the target. + logger.Printf("not enough arguments for target \"{{.TargetName}}\", expected %v, got %v\n", expected-1, len(args.Args)-1) + os.Exit(2) + } + if args.Verbose { + logger.Println("Running target:", "{{.TargetName}}") + } + {{.ExecCode}} + handleError(logger, ret) + {{- end}} + {{range .Imports}} + {{$imp := .}} + {{range .Info.Funcs }} + case "{{lower .TargetName}}": + expected := x + {{len .Args}} + if expected > len(args.Args) { + // note that expected and args at this point include the arg for the target itself + // so we subtract 1 here to show the number of args without the target. + logger.Printf("not enough arguments for target \"{{.TargetName}}\", expected %v, got %v\n", expected-1, len(args.Args)-1) + os.Exit(2) + } + if args.Verbose { + logger.Println("Running target:", "{{.TargetName}}") + } + {{.ExecCode}} + handleError(logger, ret) + {{- end}} + {{- end}} + default: + logger.Printf("Unknown target specified: %q\n", target) + os.Exit(2) + } + } +} + + + + +` diff --git a/vendor/github.com/magefile/mage/magefile.go b/vendor/github.com/magefile/mage/magefile.go new file mode 100644 index 0000000000..c08ffa3769 --- /dev/null +++ b/vendor/github.com/magefile/mage/magefile.go @@ -0,0 +1,121 @@ +//go:build mage +// +build mage + +// This is the build script for Mage. The install target is all you really need. +// The release target is for generating official releases and is really only +// useful to project admins. +package main + +import ( + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "regexp" + "runtime" + "strings" + "time" + + "github.com/magefile/mage/mg" + "github.com/magefile/mage/sh" +) + +var Aliases = map[string]interface{}{ + "Speak": Say, +} + +// Say says something. +func Say(msg string, i int, b bool, d time.Duration) error { + _, err := fmt.Printf("%v(%T) %v(%T) %v(%T) %v(%T)\n", msg, msg, i, i, b, b, d, d) + return err +} + +// Runs "go install" for mage. This generates the version info the binary. +func Install() error { + name := "mage" + if runtime.GOOS == "windows" { + name += ".exe" + } + + gocmd := mg.GoCmd() + // use GOBIN if set in the environment, otherwise fall back to first path + // in GOPATH environment string + bin, err := sh.Output(gocmd, "env", "GOBIN") + if err != nil { + return fmt.Errorf("can't determine GOBIN: %v", err) + } + if bin == "" { + gopath, err := sh.Output(gocmd, "env", "GOPATH") + if err != nil { + return fmt.Errorf("can't determine GOPATH: %v", err) + } + paths := strings.Split(gopath, string([]rune{os.PathListSeparator})) + bin = filepath.Join(paths[0], "bin") + } + // specifically don't mkdirall, if you have an invalid gopath in the first + // place, that's not on us to fix. + if err := os.Mkdir(bin, 0700); err != nil && !os.IsExist(err) { + return fmt.Errorf("failed to create %q: %v", bin, err) + } + path := filepath.Join(bin, name) + + // we use go build here because if someone built with go get, then `go + // install` turns into a no-op, and `go install -a` fails on people's + // machines that have go installed in a non-writeable directory (such as + // normal OS installs in /usr/bin) + return sh.RunV(gocmd, "build", "-o", path, "-ldflags="+flags(), "github.com/magefile/mage") +} + +var releaseTag = regexp.MustCompile(`^v1\.[0-9]+\.[0-9]+$`) + +// Generates a new release. Expects a version tag in v1.x.x format. +func Release(tag string) (err error) { + if _, err := exec.LookPath("goreleaser"); err != nil { + return fmt.Errorf("can't find goreleaser: %w", err) + } + if !releaseTag.MatchString(tag) { + return errors.New("TAG environment variable must be in semver v1.x.x format, but was " + tag) + } + + if err := sh.RunV("git", "tag", "-a", tag, "-m", tag); err != nil { + return err + } + if err := sh.RunV("git", "push", "origin", tag); err != nil { + return err + } + defer func() { + if err != nil { + sh.RunV("git", "tag", "--delete", tag) + sh.RunV("git", "push", "--delete", "origin", tag) + } + }() + return sh.RunV("goreleaser") +} + +// Remove the temporarily generated files from Release. +func Clean() error { + return sh.Rm("dist") +} + +func flags() string { + timestamp := time.Now().Format(time.RFC3339) + hash := hash() + tag := tag() + if tag == "" { + tag = "dev" + } + return fmt.Sprintf(`-X "github.com/magefile/mage/mage.timestamp=%s" -X "github.com/magefile/mage/mage.commitHash=%s" -X "github.com/magefile/mage/mage.gitTag=%s"`, timestamp, hash, tag) +} + +// tag returns the git tag for the current branch or "" if none. +func tag() string { + s, _ := sh.Output("git", "describe", "--tags") + return s +} + +// hash returns the git hash for the current repo or "" if none. +func hash() string { + hash, _ := sh.Output("git", "rev-parse", "--short", "HEAD") + return hash +} diff --git a/vendor/github.com/magefile/mage/main.go b/vendor/github.com/magefile/mage/main.go new file mode 100644 index 0000000000..d596ac7f06 --- /dev/null +++ b/vendor/github.com/magefile/mage/main.go @@ -0,0 +1,11 @@ +package main + +import ( + "os" + + "github.com/magefile/mage/mage" +) + +func main() { + os.Exit(mage.Main()) +} diff --git a/vendor/github.com/magefile/mage/parse/parse.go b/vendor/github.com/magefile/mage/parse/parse.go new file mode 100644 index 0000000000..48cf1eced1 --- /dev/null +++ b/vendor/github.com/magefile/mage/parse/parse.go @@ -0,0 +1,832 @@ +package parse + +import ( + "errors" + "fmt" + "go/ast" + "go/doc" + "go/parser" + "go/token" + "io/ioutil" + "log" + "os" + "sort" + "strings" + "time" + + "github.com/magefile/mage/internal" +) + +const importTag = "mage:import" + +var debug = log.New(ioutil.Discard, "DEBUG: ", log.Ltime|log.Lmicroseconds) + +// EnableDebug turns on debug logging. +func EnableDebug() { + debug.SetOutput(os.Stderr) +} + +// PkgInfo contains inforamtion about a package of files according to mage's +// parsing rules. +type PkgInfo struct { + AstPkg *ast.Package + DocPkg *doc.Package + Description string + Funcs Functions + DefaultFunc *Function + Aliases map[string]*Function + Imports Imports +} + +// Function represented a job function from a mage file +type Function struct { + PkgAlias string + Package string + ImportPath string + Name string + Receiver string + IsError bool + IsContext bool + Synopsis string + Comment string + Args []Arg +} + +var _ sort.Interface = (Functions)(nil) + +// Functions implements sort interface to optimize compiled output with +// deterministic generated mainfile. +type Functions []*Function + +func (s Functions) Len() int { + return len(s) +} + +func (s Functions) Less(i, j int) bool { + return s[i].TargetName() < s[j].TargetName() +} + +func (s Functions) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +// Arg is an argument to a Function. +type Arg struct { + Name, Type string +} + +// ID returns user-readable information about where this function is defined. +func (f Function) ID() string { + path := "" + if f.ImportPath != "" { + path = f.ImportPath + } + receiver := "" + if f.Receiver != "" { + receiver = f.Receiver + "." + } + return fmt.Sprintf("%s.%s%s", path, receiver, f.Name) +} + +// TargetName returns the name of the target as it should appear when used from +// the mage cli. It is always lowercase. +func (f Function) TargetName() string { + var names []string + + for _, s := range []string{f.PkgAlias, f.Receiver, f.Name} { + if s != "" { + names = append(names, s) + } + } + return strings.Join(names, ":") +} + +// ExecCode returns code for the template switch to run the target. +// It wraps each target call to match the func(context.Context) error that +// runTarget requires. +func (f Function) ExecCode() string { + name := f.Name + if f.Receiver != "" { + name = f.Receiver + "{}." + name + } + if f.Package != "" { + name = f.Package + "." + name + } + + var parseargs string + for x, arg := range f.Args { + switch arg.Type { + case "string": + parseargs += fmt.Sprintf(` + arg%d := args.Args[x] + x++`, x) + case "int": + parseargs += fmt.Sprintf(` + arg%d, err := strconv.Atoi(args.Args[x]) + if err != nil { + logger.Printf("can't convert argument %%q to int\n", args.Args[x]) + os.Exit(2) + } + x++`, x) + case "bool": + parseargs += fmt.Sprintf(` + arg%d, err := strconv.ParseBool(args.Args[x]) + if err != nil { + logger.Printf("can't convert argument %%q to bool\n", args.Args[x]) + os.Exit(2) + } + x++`, x) + case "time.Duration": + parseargs += fmt.Sprintf(` + arg%d, err := time.ParseDuration(args.Args[x]) + if err != nil { + logger.Printf("can't convert argument %%q to time.Duration\n", args.Args[x]) + os.Exit(2) + } + x++`, x) + } + } + + out := parseargs + ` + wrapFn := func(ctx context.Context) error { + ` + if f.IsError { + out += "return " + } + out += name + "(" + var args []string + if f.IsContext { + args = append(args, "ctx") + } + for x := 0; x < len(f.Args); x++ { + args = append(args, fmt.Sprintf("arg%d", x)) + } + out += strings.Join(args, ", ") + out += ")" + if !f.IsError { + out += ` + return nil` + } + out += ` + } + ret := runTarget(logger, wrapFn)` + return out +} + +// PrimaryPackage parses a package. If files is non-empty, it will only parse the files given. +func PrimaryPackage(gocmd, path string, files []string) (*PkgInfo, error) { + info, err := Package(path, files) + if err != nil { + return nil, err + } + + if err := setImports(gocmd, info); err != nil { + return nil, err + } + + setDefault(info) + setAliases(info) + return info, nil +} + +func checkDupes(info *PkgInfo, imports []*Import) error { + funcs := map[string][]*Function{} + for _, f := range info.Funcs { + funcs[strings.ToLower(f.TargetName())] = append(funcs[strings.ToLower(f.TargetName())], f) + } + for _, imp := range imports { + for _, f := range imp.Info.Funcs { + target := strings.ToLower(f.TargetName()) + funcs[target] = append(funcs[target], f) + } + } + for alias, f := range info.Aliases { + if len(funcs[alias]) != 0 { + var ids []string + for _, f := range funcs[alias] { + ids = append(ids, f.ID()) + } + return fmt.Errorf("alias %q duplicates existing target(s): %s\n", alias, strings.Join(ids, ", ")) + } + funcs[alias] = append(funcs[alias], f) + } + var dupes []string + for target, list := range funcs { + if len(list) > 1 { + dupes = append(dupes, target) + } + } + if len(dupes) == 0 { + return nil + } + errs := make([]string, 0, len(dupes)) + for _, d := range dupes { + var ids []string + for _, f := range funcs[d] { + ids = append(ids, f.ID()) + } + sort.Strings(ids) + errs = append(errs, fmt.Sprintf("%q target has multiple definitions: %s\n", d, strings.Join(ids, ", "))) + } + sort.Strings(errs) + return errors.New(strings.Join(errs, "\n")) +} + +// Package compiles information about a mage package. +func Package(path string, files []string) (*PkgInfo, error) { + start := time.Now() + defer func() { + debug.Println("time parse Magefiles:", time.Since(start)) + }() + fset := token.NewFileSet() + pkg, err := getPackage(path, files, fset) + if err != nil { + return nil, err + } + p := doc.New(pkg, "./", 0) + pi := &PkgInfo{ + AstPkg: pkg, + DocPkg: p, + Description: toOneLine(p.Doc), + } + + setNamespaces(pi) + setFuncs(pi) + + hasDupes, names := checkDupeTargets(pi) + if hasDupes { + msg := "Build targets must be case insensitive, thus the following targets conflict:\n" + for _, v := range names { + if len(v) > 1 { + msg += " " + strings.Join(v, ", ") + "\n" + } + } + return nil, errors.New(msg) + } + + return pi, nil +} + +func getNamedImports(gocmd string, pkgs map[string]string) ([]*Import, error) { + var imports []*Import + for pkg, alias := range pkgs { + debug.Printf("getting import package %q, alias %q", pkg, alias) + imp, err := getImport(gocmd, pkg, alias) + if err != nil { + return nil, err + } + imports = append(imports, imp) + } + return imports, nil +} + +// getImport returns the metadata about a package that has been mage:import'ed. +func getImport(gocmd, importpath, alias string) (*Import, error) { + out, err := internal.OutputDebug(gocmd, "list", "-f", "{{.Dir}}||{{.Name}}", importpath) + if err != nil { + return nil, err + } + parts := strings.Split(out, "||") + if len(parts) != 2 { + return nil, fmt.Errorf("incorrect data from go list: %s", out) + } + dir, name := parts[0], parts[1] + debug.Printf("parsing imported package %q from dir %q", importpath, dir) + + // we use go list to get the list of files, since go/parser doesn't differentiate between + // go files with build tags etc, and go list does. This prevents weird problems if you + // have more than one package in a folder because of build tags. + out, err = internal.OutputDebug(gocmd, "list", "-f", `{{join .GoFiles "||"}}`, importpath) + if err != nil { + return nil, err + } + files := strings.Split(out, "||") + + info, err := Package(dir, files) + if err != nil { + return nil, err + } + for i := range info.Funcs { + debug.Printf("setting alias %q and package %q on func %v", alias, name, info.Funcs[i].Name) + info.Funcs[i].PkgAlias = alias + info.Funcs[i].ImportPath = importpath + } + return &Import{Alias: alias, Name: name, Path: importpath, Info: *info}, nil +} + +// Import represents the data about a mage:import package +type Import struct { + Alias string + Name string + UniqueName string // a name unique across all imports + Path string + Info PkgInfo +} + +var _ sort.Interface = (Imports)(nil) + +// Imports implements sort interface to optimize compiled output with +// deterministic generated mainfile. +type Imports []*Import + +func (s Imports) Len() int { + return len(s) +} + +func (s Imports) Less(i, j int) bool { + return s[i].UniqueName < s[j].UniqueName +} + +func (s Imports) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +func setFuncs(pi *PkgInfo) { + for _, f := range pi.DocPkg.Funcs { + if f.Recv != "" { + debug.Printf("skipping method %s.%s", f.Recv, f.Name) + // skip methods + continue + } + if !ast.IsExported(f.Name) { + debug.Printf("skipping non-exported function %s", f.Name) + // skip non-exported functions + continue + } + fn, err := funcType(f.Decl.Type) + if err != nil { + debug.Printf("skipping function with invalid signature func %s: %v", f.Name, err) + continue + } + debug.Printf("found target %v", f.Name) + fn.Name = f.Name + fn.Comment = toOneLine(f.Doc) + fn.Synopsis = sanitizeSynopsis(f) + pi.Funcs = append(pi.Funcs, fn) + } +} + +func setNamespaces(pi *PkgInfo) { + for _, t := range pi.DocPkg.Types { + if !isNamespace(t) { + continue + } + debug.Printf("found namespace %s %s", pi.DocPkg.ImportPath, t.Name) + for _, f := range t.Methods { + if !ast.IsExported(f.Name) { + continue + } + fn, err := funcType(f.Decl.Type) + if err != nil { + debug.Printf("skipping invalid namespace method %s %s.%s: %v", pi.DocPkg.ImportPath, t.Name, f.Name, err) + continue + } + debug.Printf("found namespace method %s %s.%s", pi.DocPkg.ImportPath, t.Name, f.Name) + fn.Name = f.Name + fn.Comment = toOneLine(f.Doc) + fn.Synopsis = sanitizeSynopsis(f) + fn.Receiver = t.Name + + pi.Funcs = append(pi.Funcs, fn) + } + } +} + +func setImports(gocmd string, pi *PkgInfo) error { + importNames := map[string]string{} + rootImports := []string{} + for _, f := range pi.AstPkg.Files { + for _, d := range f.Decls { + gen, ok := d.(*ast.GenDecl) + if !ok || gen.Tok != token.IMPORT { + continue + } + for j := 0; j < len(gen.Specs); j++ { + spec := gen.Specs[j] + impspec := spec.(*ast.ImportSpec) + if len(gen.Specs) == 1 && gen.Lparen == token.NoPos && impspec.Doc == nil { + impspec.Doc = gen.Doc + } + name, alias, ok := getImportPath(impspec) + if !ok { + continue + } + if alias != "" { + debug.Printf("found %s: %s (%s)", importTag, name, alias) + importNames[name] = alias + } else { + debug.Printf("found %s: %s", importTag, name) + rootImports = append(rootImports, name) + } + } + } + } + imports, err := getNamedImports(gocmd, importNames) + if err != nil { + return err + } + for _, s := range rootImports { + imp, err := getImport(gocmd, s, "") + if err != nil { + return err + } + imports = append(imports, imp) + } + if err := checkDupes(pi, imports); err != nil { + return err + } + + // have to set unique package names on imports + used := map[string]bool{} + for _, imp := range imports { + unique := imp.Name + "_mageimport" + x := 1 + for used[unique] { + unique = fmt.Sprintf("%s_mageimport%d", imp.Name, x) + x++ + } + used[unique] = true + imp.UniqueName = unique + for _, f := range imp.Info.Funcs { + f.Package = unique + } + } + pi.Imports = imports + return nil +} + +func getImportPath(imp *ast.ImportSpec) (path, alias string, ok bool) { + if imp.Doc == nil || len(imp.Doc.List) == 9 { + return "", "", false + } + // import is always the last comment + s := imp.Doc.List[len(imp.Doc.List)-1].Text + + // trim comment start and normalize for anyone who has spaces or not between + // "//"" and the text + vals := strings.Fields(strings.ToLower(s[2:])) + if len(vals) == 0 { + return "", "", false + } + if vals[0] != importTag { + return "", "", false + } + path, ok = lit2string(imp.Path) + if !ok { + return "", "", false + } + + switch len(vals) { + case 1: + // just the import tag, this is a root import + return path, "", true + case 2: + // also has an alias + return path, vals[1], true + default: + log.Println("warning: ignoring malformed", importTag, "for import", path) + return "", "", false + } +} + +func isNamespace(t *doc.Type) bool { + if len(t.Decl.Specs) != 1 { + return false + } + id, ok := t.Decl.Specs[0].(*ast.TypeSpec) + if !ok { + return false + } + sel, ok := id.Type.(*ast.SelectorExpr) + if !ok { + return false + } + ident, ok := sel.X.(*ast.Ident) + if !ok { + return false + } + return ident.Name == "mg" && sel.Sel.Name == "Namespace" +} + +// checkDupeTargets checks a package for duplicate target names. +func checkDupeTargets(info *PkgInfo) (hasDupes bool, names map[string][]string) { + names = map[string][]string{} + lowers := map[string]bool{} + for _, f := range info.Funcs { + low := strings.ToLower(f.Name) + if f.Receiver != "" { + low = strings.ToLower(f.Receiver) + ":" + low + } + if lowers[low] { + hasDupes = true + } + lowers[low] = true + names[low] = append(names[low], f.Name) + } + return hasDupes, names +} + +// sanitizeSynopsis sanitizes function Doc to create a summary. +func sanitizeSynopsis(f *doc.Func) string { + synopsis := doc.Synopsis(f.Doc) + + // If the synopsis begins with the function name, remove it. This is done to + // not repeat the text. + // From: + // clean Clean removes the temporarily generated files + // To: + // clean removes the temporarily generated files + if syns := strings.Split(synopsis, " "); strings.EqualFold(f.Name, syns[0]) { + return strings.Join(syns[1:], " ") + } + + return synopsis +} + +func setDefault(pi *PkgInfo) { + for _, v := range pi.DocPkg.Vars { + for x, name := range v.Names { + if name != "Default" { + continue + } + spec := v.Decl.Specs[x].(*ast.ValueSpec) + if len(spec.Values) != 1 { + log.Println("warning: default declaration has multiple values") + } + + f, err := getFunction(spec.Values[0], pi) + if err != nil { + log.Println("warning, default declaration malformed:", err) + return + } + pi.DefaultFunc = f + return + } + } +} + +func lit2string(l *ast.BasicLit) (string, bool) { + if !strings.HasPrefix(l.Value, `"`) || !strings.HasSuffix(l.Value, `"`) { + return "", false + } + return strings.Trim(l.Value, `"`), true +} + +func setAliases(pi *PkgInfo) { + for _, v := range pi.DocPkg.Vars { + for x, name := range v.Names { + if name != "Aliases" { + continue + } + spec, ok := v.Decl.Specs[x].(*ast.ValueSpec) + if !ok { + log.Println("warning: aliases declaration is not a value") + return + } + if len(spec.Values) != 1 { + log.Println("warning: aliases declaration has multiple values") + } + comp, ok := spec.Values[0].(*ast.CompositeLit) + if !ok { + log.Println("warning: aliases declaration is not a map") + return + } + pi.Aliases = map[string]*Function{} + for _, elem := range comp.Elts { + kv, ok := elem.(*ast.KeyValueExpr) + if !ok { + log.Printf("warning: alias declaration %q is not a map element", elem) + continue + } + k, ok := kv.Key.(*ast.BasicLit) + if !ok || k.Kind != token.STRING { + log.Printf("warning: alias key is not a string literal %q", elem) + continue + } + + alias, ok := lit2string(k) + if !ok { + log.Println("warning: malformed name for alias", elem) + continue + } + f, err := getFunction(kv.Value, pi) + if err != nil { + log.Printf("warning, alias malformed: %v", err) + continue + } + pi.Aliases[alias] = f + } + return + } + } +} + +func getFunction(exp ast.Expr, pi *PkgInfo) (*Function, error) { + // selector expressions are in LIFO format. + // So, in foo.bar.baz the first selector.Name is + // actually "baz", the second is "bar", and the last is "foo" + + var pkg, receiver, funcname string + switch v := exp.(type) { + case *ast.Ident: + // "foo" : Bar + funcname = v.Name + case *ast.SelectorExpr: + // need to handle + // namespace.Func + // import.Func + // import.namespace.Func + + // "foo" : ?.bar + funcname = v.Sel.Name + switch x := v.X.(type) { + case *ast.Ident: + // "foo" : baz.bar + // this is either a namespace or package + firstname := x.Name + for _, f := range pi.Funcs { + if firstname == f.Receiver && funcname == f.Name { + return f, nil + } + } + // not a namespace, let's try imported packages + for _, imp := range pi.Imports { + if firstname == imp.Name { + for _, f := range imp.Info.Funcs { + if funcname == f.Name { + return f, nil + } + } + break + } + } + return nil, fmt.Errorf("%q is not a known target", exp) + case *ast.SelectorExpr: + // "foo" : bar.Baz.Bat + // must be package.Namespace.Func + sel, ok := v.X.(*ast.SelectorExpr) + if !ok { + return nil, fmt.Errorf("%q is must denote a target function but was %T", exp, v.X) + } + receiver = sel.Sel.Name + id, ok := sel.X.(*ast.Ident) + if !ok { + return nil, fmt.Errorf("%q is must denote a target function but was %T", exp, v.X) + } + pkg = id.Name + default: + return nil, fmt.Errorf("%q is not valid", exp) + } + default: + return nil, fmt.Errorf("target %s is not a function", exp) + } + if pkg == "" { + for _, f := range pi.Funcs { + if f.Name == funcname && f.Receiver == receiver { + return f, nil + } + } + return nil, fmt.Errorf("unknown function %s.%s", receiver, funcname) + } + for _, imp := range pi.Imports { + if imp.Name == pkg { + for _, f := range imp.Info.Funcs { + if f.Name == funcname && f.Receiver == receiver { + return f, nil + } + } + return nil, fmt.Errorf("unknown function %s.%s.%s", pkg, receiver, funcname) + } + } + return nil, fmt.Errorf("unknown package for function %q", exp) +} + +// getPackage returns the importable package at the given path. +func getPackage(path string, files []string, fset *token.FileSet) (*ast.Package, error) { + var filter func(f os.FileInfo) bool + if len(files) > 0 { + fm := make(map[string]bool, len(files)) + for _, f := range files { + fm[f] = true + } + + filter = func(f os.FileInfo) bool { + return fm[f.Name()] + } + } + + pkgs, err := parser.ParseDir(fset, path, filter, parser.ParseComments) + if err != nil { + return nil, fmt.Errorf("failed to parse directory: %v", err) + } + + switch len(pkgs) { + case 1: + var pkg *ast.Package + for _, pkg = range pkgs { + } + return pkg, nil + case 0: + return nil, fmt.Errorf("no importable packages found in %s", path) + default: + var names []string + for name := range pkgs { + names = append(names, name) + } + return nil, fmt.Errorf("multiple packages found in %s: %v", path, strings.Join(names, ", ")) + } +} + +// hasContextParams returns whether or not the first parameter is a context.Context. If it +// determines that hte first parameter makes this function invalid for mage, it'll return a non-nil +// error. +func hasContextParam(ft *ast.FuncType) (bool, error) { + if ft.Params.NumFields() < 1 { + return false, nil + } + param := ft.Params.List[0] + sel, ok := param.Type.(*ast.SelectorExpr) + if !ok { + return false, nil + } + pkg, ok := sel.X.(*ast.Ident) + if !ok { + return false, nil + } + if pkg.Name != "context" { + return false, nil + } + if sel.Sel.Name != "Context" { + return false, nil + } + if len(param.Names) > 1 { + // something like foo, bar context.Context + return false, errors.New("ETOOMANYCONTEXTS") + } + return true, nil +} + +func hasVoidReturn(ft *ast.FuncType) bool { + res := ft.Results + return res.NumFields() == 0 +} + +func hasErrorReturn(ft *ast.FuncType) (bool, error) { + res := ft.Results + if res.NumFields() == 0 { + // void return is ok + return false, nil + } + if res.NumFields() > 1 { + return false, errors.New("ETOOMANYRETURNS") + } + ret := res.List[0] + if len(ret.Names) > 1 { + return false, errors.New("ETOOMANYERRORS") + } + if fmt.Sprint(ret.Type) == "error" { + return true, nil + } + return false, errors.New("EBADRETURNTYPE") +} + +func funcType(ft *ast.FuncType) (*Function, error) { + var err error + f := &Function{} + f.IsContext, err = hasContextParam(ft) + if err != nil { + return nil, err + } + f.IsError, err = hasErrorReturn(ft) + if err != nil { + return nil, err + } + x := 0 + if f.IsContext { + x++ + } + for ; x < len(ft.Params.List); x++ { + param := ft.Params.List[x] + t := fmt.Sprint(param.Type) + typ, ok := argTypes[t] + if !ok { + return nil, fmt.Errorf("unsupported argument type: %s", t) + } + // support for foo, bar string + for _, name := range param.Names { + f.Args = append(f.Args, Arg{Name: name.Name, Type: typ}) + } + } + return f, nil +} + +func toOneLine(s string) string { + return strings.TrimSpace(strings.Replace(s, "\n", " ", -1)) +} + +var argTypes = map[string]string{ + "string": "string", + "int": "int", + "&{time Duration}": "time.Duration", + "bool": "bool", +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 8e3abc0992..a9d94d4535 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -204,7 +204,11 @@ github.com/letsencrypt/pkcs11key/v4 github.com/libdns/libdns # github.com/magefile/mage v1.15.0 ## explicit; go 1.12 +github.com/magefile/mage +github.com/magefile/mage/internal +github.com/magefile/mage/mage github.com/magefile/mage/mg +github.com/magefile/mage/parse github.com/magefile/mage/sh github.com/magefile/mage/target # github.com/mattn/go-colorable v0.1.14 From 058f2704e32c062aaa026590a771699587015b0c Mon Sep 17 00:00:00 2001 From: Cedric Staub Date: Thu, 8 Jan 2026 22:09:36 -0800 Subject: [PATCH 05/11] Compile mage before we set GOARCH --- .github/workflows/compile.yml | 10 ++++++---- .github/workflows/release.yml | 10 ++++++---- .gitignore | 1 + 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/.github/workflows/compile.yml b/.github/workflows/compile.yml index 64db4eb9f3..05abb85d81 100644 --- a/.github/workflows/compile.yml +++ b/.github/workflows/compile.yml @@ -54,9 +54,10 @@ jobs: fetch-tags: true - name: Build binary run: | - CGO_ENABLED=1 GOARCH=amd64 go tool mage -v go:build + go tool mage -compile ./mage-bin + CGO_ENABLED=1 GOARCH=amd64 ./mage-bin -v go:build mv ghostunnel ghostunnel-linux-amd64 - CGO_ENABLED=1 GOARCH=arm64 CC=aarch64-linux-gnu-gcc go tool mage -v go:build + CGO_ENABLED=1 GOARCH=arm64 CC=aarch64-linux-gnu-gcc ./mage-bin -v go:build mv ghostunnel ghostunnel-linux-arm64 - name: Upload artifact uses: actions/upload-artifact@v5 @@ -88,9 +89,10 @@ jobs: fetch-tags: true - name: Build binary run: | - CGO_ENABLED=1 GOARCH=amd64 go tool mage -v go:build + go tool mage -compile ./mage-bin + CGO_ENABLED=1 GOARCH=amd64 ./mage-bin -v go:build mv ghostunnel ghostunnel-darwin-amd64 - CGO_ENABLED=1 GOARCH=arm64 go tool mage -v go:build + CGO_ENABLED=1 GOARCH=arm64 ./mage-bin -v go:build mv ghostunnel ghostunnel-darwin-arm64 lipo -create -output ghostunnel-darwin-universal ghostunnel-darwin-amd64 ghostunnel-darwin-arm64 - name: Upload artifact diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2a3331e6e2..e1f1dc7c54 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,9 +30,10 @@ jobs: env: VERSION: ${{ github.ref_name }} run: | - CGO_ENABLED=1 GOARCH=amd64 go tool mage -v go:build + go tool mage -compile ./mage-bin + CGO_ENABLED=1 GOARCH=amd64 ./mage-bin -v go:build mv ghostunnel ghostunnel-linux-amd64 - CGO_ENABLED=1 GOARCH=arm64 CC=aarch64-linux-gnu-gcc go tool mage -v go:build + CGO_ENABLED=1 GOARCH=arm64 CC=aarch64-linux-gnu-gcc ./mage-bin -v go:build mv ghostunnel ghostunnel-linux-arm64 - name: Upload artifact uses: actions/upload-artifact@v5 @@ -65,9 +66,10 @@ jobs: env: VERSION: ${{ github.ref_name }} run: | - CGO_ENABLED=1 GOARCH=amd64 go tool mage -v go:build + go tool mage -compile ./mage-bin + CGO_ENABLED=1 GOARCH=amd64 ./mage-bin -v go:build mv ghostunnel ghostunnel-darwin-amd64 - CGO_ENABLED=1 GOARCH=arm64 go tool mage -v go:build + CGO_ENABLED=1 GOARCH=arm64 ./mage-bin -v go:build mv ghostunnel ghostunnel-darwin-arm64 lipo -create -output ghostunnel-darwin-universal ghostunnel-darwin-amd64 ghostunnel-darwin-arm64 - name: Upload artifact diff --git a/.gitignore b/.gitignore index 15f73bddf8..fb67fdf8eb 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ *.out ghostunnel ghostunnel.man +mage-bin ghostunnel.exe ghostunnel.test ghostunnel.certstore From 20c9f4e3cd13401e9d67b828caec56293c47e12c Mon Sep 17 00:00:00 2001 From: Cedric Staub Date: Fri, 9 Jan 2026 21:07:35 -0800 Subject: [PATCH 06/11] Fix README test keys example --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 84af296349..e5250abe08 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ started is to use a package like [cloudflare/cfssl](https://github.com/cloudflar For testing and development purposes, you can generate test certificates using: # Generate test certificates and keys - mage testKeys + mage test:keys This will create a `test-keys` directory with all the necessary certificates and keys for testing. **Note: These are test certificates only and should NOT be used in production.** From 5afddc8e08846c236f815d4dfa64e9048f53a957 Mon Sep 17 00:00:00 2001 From: Cedric Staub Date: Fri, 9 Jan 2026 21:22:11 -0800 Subject: [PATCH 07/11] Replace fakeca package with pure Go implementation, fix Windows unit tests for certstore --- certstore/certstore_test.go | 27 +- certstore/main_test.go | 12 +- certstore/testca_test.go | 156 ++++++++++++ go.mod | 3 +- go.sum | 9 - vendor/github.com/github/smimesign/LICENSE.md | 21 -- .../github/smimesign/fakeca/LICENSE.md | 21 -- .../github/smimesign/fakeca/README.md | 44 ---- .../github/smimesign/fakeca/configuration.go | 240 ------------------ .../github/smimesign/fakeca/identity.go | 144 ----------- vendor/modules.txt | 3 - 11 files changed, 164 insertions(+), 516 deletions(-) create mode 100644 certstore/testca_test.go delete mode 100644 vendor/github.com/github/smimesign/LICENSE.md delete mode 100644 vendor/github.com/github/smimesign/fakeca/LICENSE.md delete mode 100644 vendor/github.com/github/smimesign/fakeca/README.md delete mode 100644 vendor/github.com/github/smimesign/fakeca/configuration.go delete mode 100644 vendor/github.com/github/smimesign/fakeca/identity.go diff --git a/certstore/certstore_test.go b/certstore/certstore_test.go index 38de7a88b7..c6a97705c7 100644 --- a/certstore/certstore_test.go +++ b/certstore/certstore_test.go @@ -12,10 +12,7 @@ import ( "crypto/sha256" "crypto/sha512" "crypto/x509" - "runtime" "testing" - - "github.com/github/smimesign/fakeca" ) func TestImportDeleteRSA(t *testing.T) { @@ -27,12 +24,7 @@ func TestImportDeleteECDSA(t *testing.T) { } // ImportDeleteHelper is an abstraction for testing identity Import()/Delete(). -func ImportDeleteHelper(t *testing.T, i *fakeca.Identity) { - if runtime.GOOS == "windows" { - t.Skip("FIXME: Windows runners in Github fail this test due to issues with their OpenSSL installation") - return - } - +func ImportDeleteHelper(t *testing.T, i *identity) { withStore(t, func(store Store) { // Import an identity if err := store.Import(i.PFX("asdf"), "asdf"); err != nil { @@ -99,11 +91,6 @@ func ImportDeleteHelper(t *testing.T, i *fakeca.Identity) { } func TestSignerRSA(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("FIXME: Windows runners in Github fail this test due to issues with their OpenSSL installation") - return - } - rsaPriv, ok := leafRSA.PrivateKey.(*rsa.PrivateKey) if !ok { t.Fatal("expected priv to be an RSA private key") @@ -234,11 +221,6 @@ func TestSignerRSA(t *testing.T) { } func TestSignerECDSA(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("FIXME: Windows runners in Github fail this test due to issues with their OpenSSL installation") - return - } - ecPriv, ok := leafEC.PrivateKey.(*ecdsa.PrivateKey) if !ok { t.Fatal("expected priv to be an ECDSA private key") @@ -320,12 +302,7 @@ func TestCertificateEC(t *testing.T) { CertificateHelper(t, leafEC) } -func CertificateHelper(t *testing.T, leaf *fakeca.Identity) { - if runtime.GOOS == "windows" { - t.Skip("FIXME: Windows runners in Github fail this test due to issues with their OpenSSL installation") - return - } - +func CertificateHelper(t *testing.T, leaf *identity) { withIdentity(t, root, func(caIdent Identity) { withIdentity(t, intermediate, func(interIdent Identity) { withIdentity(t, leaf, func(leafIdent Identity) { diff --git a/certstore/main_test.go b/certstore/main_test.go index 0838d8ab75..5eba7c0769 100644 --- a/certstore/main_test.go +++ b/certstore/main_test.go @@ -13,29 +13,27 @@ import ( "log" "os" "testing" - - "github.com/github/smimesign/fakeca" ) var ( - root = fakeca.New(fakeca.IsCA, fakeca.Subject(pkix.Name{ + root = newIdentity(withIsCA, withSubject(pkix.Name{ Organization: []string{"certstore"}, CommonName: "root", })) - intermediate = root.Issue(fakeca.IsCA, fakeca.Subject(pkix.Name{ + intermediate = root.Issue(withIsCA, withSubject(pkix.Name{ Organization: []string{"certstore"}, CommonName: "intermediate", })) leafKeyRSA, _ = rsa.GenerateKey(rand.Reader, 2048) - leafRSA = intermediate.Issue(fakeca.PrivateKey(leafKeyRSA), fakeca.Subject(pkix.Name{ + leafRSA = intermediate.Issue(withPrivateKey(leafKeyRSA), withSubject(pkix.Name{ Organization: []string{"certstore"}, CommonName: "leaf-rsa", })) leafKeyEC, _ = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - leafEC = intermediate.Issue(fakeca.PrivateKey(leafKeyEC), fakeca.Subject(pkix.Name{ + leafEC = intermediate.Issue(withPrivateKey(leafKeyEC), withSubject(pkix.Name{ Organization: []string{"certstore"}, CommonName: "leaf-ec", })) @@ -57,7 +55,7 @@ func withStore(t *testing.T, cb func(Store)) { cb(store) } -func withIdentity(t *testing.T, i *fakeca.Identity, cb func(Identity)) { +func withIdentity(t *testing.T, i *identity, cb func(Identity)) { withStore(t, func(store Store) { // Import an identity if err := store.Import(i.PFX("asdf"), "asdf"); err != nil { diff --git a/certstore/testca_test.go b/certstore/testca_test.go new file mode 100644 index 0000000000..730a223fe5 --- /dev/null +++ b/certstore/testca_test.go @@ -0,0 +1,156 @@ +//go:build !linux +// +build !linux + +package certstore + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "math/big" + "time" + + "software.sslmate.com/src/go-pkcs12" +) + +// identity is a certificate and private key for testing. +type identity struct { + Certificate *x509.Certificate + PrivateKey crypto.Signer + Issuer *identity + nextSN int64 +} + +// identityOption configures identity creation. +type identityOption func(*identityConfig) + +type identityConfig struct { + subject *pkix.Name + issuer *identity + priv crypto.Signer + isCA bool +} + +// withIsCA marks the identity as a certificate authority. +var withIsCA identityOption = func(c *identityConfig) { + c.isCA = true +} + +// withSubject sets the certificate subject. +func withSubject(name pkix.Name) identityOption { + return func(c *identityConfig) { + c.subject = &name + } +} + +// withPrivateKey sets a custom private key. +func withPrivateKey(key crypto.Signer) identityOption { + return func(c *identityConfig) { + c.priv = key + } +} + +// withIssuer sets the issuing identity. +func withIssuer(issuer *identity) identityOption { + return func(c *identityConfig) { + c.issuer = issuer + } +} + +// newIdentity creates a new identity (root CA or issued certificate). +func newIdentity(opts ...identityOption) *identity { + cfg := &identityConfig{} + for _, opt := range opts { + opt(cfg) + } + + // Generate private key if not provided + priv := cfg.priv + if priv == nil { + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + panic(err) + } + priv = key + } + + // Build certificate template + subject := pkix.Name{CommonName: "test"} + if cfg.subject != nil { + subject = *cfg.subject + } + + sn, err := rand.Int(rand.Reader, big.NewInt(1<<62)) + if err != nil { + panic(err) + } + + tmpl := &x509.Certificate{ + SerialNumber: sn, + Subject: subject, + NotBefore: time.Unix(0, 0), + NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour), + IsCA: cfg.isCA, + BasicConstraintsValid: true, + } + + if cfg.isCA { + tmpl.KeyUsage = x509.KeyUsageCertSign | x509.KeyUsageCRLSign + } + + // Determine parent cert and signing key + var parent *x509.Certificate + var signingKey crypto.Signer + var issuer *identity + + if cfg.issuer != nil { + parent = cfg.issuer.Certificate + signingKey = cfg.issuer.PrivateKey + issuer = cfg.issuer + tmpl.SerialNumber = big.NewInt(cfg.issuer.incrementSN()) + } else { + parent = tmpl + signingKey = priv + } + + // Create certificate + der, err := x509.CreateCertificate(rand.Reader, tmpl, parent, priv.Public(), signingKey) + if err != nil { + panic(err) + } + + cert, err := x509.ParseCertificate(der) + if err != nil { + panic(err) + } + + return &identity{ + Certificate: cert, + PrivateKey: priv, + Issuer: issuer, + nextSN: 1, + } +} + +// Issue creates a new identity signed by this one. +func (id *identity) Issue(opts ...identityOption) *identity { + opts = append(opts, withIssuer(id)) + return newIdentity(opts...) +} + +// PFX returns the identity as PKCS#12 data encrypted with password. +func (id *identity) PFX(password string) []byte { + pfxData, err := pkcs12.Legacy.Encode(id.PrivateKey, id.Certificate, nil, password) + if err != nil { + panic(err) + } + return pfxData +} + +func (id *identity) incrementSN() int64 { + sn := id.nextSN + id.nextSN++ + return sn +} diff --git a/go.mod b/go.mod index 875323a08c..711adda567 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,6 @@ require ( github.com/coreos/go-systemd/v22 v22.6.0 github.com/cyberdelia/go-metrics-graphite v0.0.0-20161219230853-39f87cc3b432 github.com/deathowl/go-metrics-prometheus v0.0.0-20221009205350-f2a1482ba35b - github.com/github/smimesign v0.2.0 github.com/go-jose/go-jose/v4 v4.1.3 github.com/hashicorp/go-syslog v1.0.0 github.com/kavu/go_reuseport v1.5.0 @@ -28,6 +27,7 @@ require ( golang.org/x/sync v0.18.0 google.golang.org/grpc v1.77.0 google.golang.org/protobuf v1.36.10 + software.sslmate.com/src/go-pkcs12 v0.6.0 ) require ( @@ -117,7 +117,6 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect kernel.org/pub/linux/libs/security/libcap/psx v1.2.77 // indirect sigs.k8s.io/yaml v1.6.0 // indirect - software.sslmate.com/src/go-pkcs12 v0.6.0 // indirect ) go 1.24.6 diff --git a/go.sum b/go.sum index 2bc0988fe1..f3e30cf4fc 100644 --- a/go.sum +++ b/go.sum @@ -32,7 +32,6 @@ github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+Y github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= -github.com/certifi/gocertifi v0.0.0-20180118203423-deb3ae2ef261/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo= @@ -70,8 +69,6 @@ github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7Dlme github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/github/smimesign v0.2.0 h1:Hho4YcX5N1I9XNqhq0fNx0Sts8MhLonHd+HRXVGNjvk= -github.com/github/smimesign v0.2.0/go.mod h1:iZiiwNT4HbtGRVqCQu7uJPEZCuEE5sfSSttcnePkDl4= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= @@ -196,7 +193,6 @@ github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWk github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/open-policy-agent/opa v1.11.0 h1:eOd/jJrbavakiX477yT4LrXZfUWViAot/AsKsjsfe7o= github.com/open-policy-agent/opa v1.11.0/go.mod h1:QimuJO4T3KYxWzrmAymqlFvsIanCjKrGjmmC8GgAdgE= -github.com/pborman/getopt v0.0.0-20180811024354-2b5b3bfb099b/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM= @@ -204,7 +200,6 @@ github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0= github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -312,8 +307,6 @@ github.com/zmap/zcrypto v0.0.0-20201128221613-3719af1573cf/go.mod h1:aPM7r+JOkfL github.com/zmap/zcrypto v0.0.0-20201211161100-e54a5822fb7e/go.mod h1:aPM7r+JOkfL+9qSB4KbYjtoEzJqUK50EXkkJabeNJDQ= github.com/zmap/zcrypto v0.0.0-20250129210703-03c45d0bae98 h1:Qp98bmMm9JHPPOaLi2Nb6oWoZ+1OyOMWI7PPeJrirI0= github.com/zmap/zcrypto v0.0.0-20250129210703-03c45d0bae98/go.mod h1:YTUyN/U1oJ7RzCEY5hUweYxbVUu7X+11wB7OXZT15oE= -github.com/zmap/zcrypto v0.0.0-20251114214934-bb32b590b717 h1:4x89hS1LZY4YctWc+XOj4BEz08bUNRTraXtHQRThsmI= -github.com/zmap/zcrypto v0.0.0-20251114214934-bb32b590b717/go.mod h1:NBtLpB/eitQk7/yXV7mHbZ/Gtmigw0Un9H9DEnrl+Zg= github.com/zmap/zlint/v3 v3.0.0/go.mod h1:paGwFySdHIBEMJ61YjoqT4h7Ge+fdYG4sUQhnTb1lJ8= github.com/zmap/zlint/v3 v3.6.8 h1:ZvdfwSgqPxeD2Sb0yhH/7jzrpFHBKAjPhzKr1vVev9c= github.com/zmap/zlint/v3 v3.6.8/go.mod h1:Tm0qwwaO629pgJ/En7M9U9Edx4+rQRuoeXVpXvgVHhA= @@ -353,7 +346,6 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= @@ -457,7 +449,6 @@ golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxb golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4 h1:Pw6WnI9W/LIdRxqK7T6XGugGbHIRl5Q7q3BssH6xk4s= diff --git a/vendor/github.com/github/smimesign/LICENSE.md b/vendor/github.com/github/smimesign/LICENSE.md deleted file mode 100644 index fe6ededa81..0000000000 --- a/vendor/github.com/github/smimesign/LICENSE.md +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2017 GitHub, Inc. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/vendor/github.com/github/smimesign/fakeca/LICENSE.md b/vendor/github.com/github/smimesign/fakeca/LICENSE.md deleted file mode 100644 index 7800c58b40..0000000000 --- a/vendor/github.com/github/smimesign/fakeca/LICENSE.md +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2017 Ben Toews. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/vendor/github.com/github/smimesign/fakeca/README.md b/vendor/github.com/github/smimesign/fakeca/README.md deleted file mode 100644 index 6a7db21341..0000000000 --- a/vendor/github.com/github/smimesign/fakeca/README.md +++ /dev/null @@ -1,44 +0,0 @@ -# fakeca [![PkgGoDev](https://pkg.go.dev/badge/github.com/github/smimesign/fakeca?tab=doc)](https://pkg.go.dev/github.com/github/smimesign/fakeca?tab=doc) - -This is a package for creating fake certificate authorities for test fixtures. - -## Example - -```go -package main - -import ( - "crypto/x509/pkix" - - "github.com/github/smimesign/fakeca" -) - -func main() { - // Change defaults for cert subjects. - fakeca.DefaultProvince = []string{"CO"} - fakeca.DefaultLocality = []string{"Denver"} - - // Create a root CA. - root := fakeca.New(fakeca.IsCA, fakeca.Subject(pkix.Name{ - CommonName: "root.myorg.com", - })) - - // Create an intermediate CA under the root. - intermediate := root.Issue(fakeca.IsCA, fakeca.Subject(pkix.Name{ - CommonName: "intermediate.myorg.com", - })) - - // Create a leaf certificate under the intermediate. - leaf := intermediate.Issue(fakeca.Subject(pkix.Name{ - CommonName: "leaf.myorg.com", - })) - - // Get PFX (PKCS12) blob containing certificate and encrypted private key. - leafPFX := leaf.PFX("pa55w0rd") - - // Get an *x509.CertPool containing certificate chain from CA to leaf for use - // with Go's TLS libraries. - leafPool := leaf.ChainPool() -} - -``` diff --git a/vendor/github.com/github/smimesign/fakeca/configuration.go b/vendor/github.com/github/smimesign/fakeca/configuration.go deleted file mode 100644 index 7e1be3a75c..0000000000 --- a/vendor/github.com/github/smimesign/fakeca/configuration.go +++ /dev/null @@ -1,240 +0,0 @@ -package fakeca - -import ( - "crypto" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "crypto/x509/pkix" - "fmt" - "math" - "math/big" - "time" -) - -type configuration struct { - subject *pkix.Name - issuer *Identity - nextSN *int64 - priv *crypto.Signer - isCA bool - notBefore *time.Time - notAfter *time.Time - issuingCertificateURL []string - ocspServer []string - keyUsage x509.KeyUsage -} - -func (c *configuration) generate() *Identity { - templ := &x509.Certificate{ - Subject: c.getSubject(), - IsCA: c.isCA, - BasicConstraintsValid: true, - NotAfter: c.getNotAfter(), - NotBefore: c.getNotBefore(), - IssuingCertificateURL: c.issuingCertificateURL, - OCSPServer: c.ocspServer, - KeyUsage: c.keyUsage, - } - - var ( - parent *x509.Certificate - thisPriv = c.getPrivateKey() - priv crypto.Signer - ) - - if c.issuer != nil { - parent = c.issuer.Certificate - templ.SerialNumber = big.NewInt(c.issuer.IncrementSN()) - priv = c.issuer.PrivateKey - } else { - parent = templ - templ.SerialNumber = randSN() - priv = thisPriv - } - - der, err := x509.CreateCertificate(rand.Reader, templ, parent, thisPriv.Public(), priv) - if err != nil { - panic(err) - } - - cert, err := x509.ParseCertificate(der) - if err != nil { - panic(err) - } - - return &Identity{ - Certificate: cert, - PrivateKey: thisPriv, - Issuer: c.issuer, - NextSN: c.getNextSN(), - } -} - -var ( - // DefaultCountry is the default subject Country. - DefaultCountry = []string{"US"} - - // DefaultProvince is the default subject Province. - DefaultProvince = []string{"CA"} - - // DefaultLocality is the default subject Locality. - DefaultLocality = []string{"San Francisco"} - - // DefaultStreetAddress is the default subject StreetAddress. - DefaultStreetAddress = []string(nil) - - // DefaultPostalCode is the default subject PostalCode. - DefaultPostalCode = []string(nil) - - // DefaultCommonName is the default subject CommonName. - DefaultCommonName = "fakeca" - - cnCounter int64 -) - -func (c *configuration) getSubject() pkix.Name { - if c.subject != nil { - return *c.subject - } - - var cn string - if cnCounter == 0 { - cn = DefaultCommonName - } else { - cn = fmt.Sprintf("%s #%d", DefaultCommonName, cnCounter) - } - cnCounter++ - - return pkix.Name{ - Country: DefaultCountry, - Province: DefaultProvince, - Locality: DefaultLocality, - StreetAddress: DefaultStreetAddress, - PostalCode: DefaultPostalCode, - CommonName: cn, - } -} - -func (c *configuration) getNextSN() int64 { - if c.nextSN == nil { - sn := randSN().Int64() - c.nextSN = &sn - } - - return *c.nextSN -} - -func randSN() *big.Int { - i, err := rand.Int(rand.Reader, big.NewInt(int64(math.MaxInt64))) - if err != nil { - panic(err) - } - - return i -} - -func (c *configuration) getPrivateKey() crypto.Signer { - if c.priv == nil { - priv, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - panic(err) - } - - signer := crypto.Signer(priv) - - c.priv = &signer - } - - return *c.priv -} - -func (c *configuration) getNotBefore() time.Time { - if c.notBefore == nil { - return time.Unix(0, 0) - } - - return *c.notBefore -} - -func (c *configuration) getNotAfter() time.Time { - if c.notAfter == nil { - return time.Now().Add(time.Hour * 24 * 365 * 10) - } - - return *c.notAfter -} - -// Option is an option that can be passed to New(). -type Option option -type option func(c *configuration) - -// Subject is an Option that sets a identity's subject field. -func Subject(value pkix.Name) Option { - return func(c *configuration) { - c.subject = &value - } -} - -// NextSerialNumber is an Option that determines the SN of the next issued -// certificate. -func NextSerialNumber(value int64) Option { - return func(c *configuration) { - c.nextSN = &value - } -} - -// PrivateKey is an Option for setting the identity's private key. -func PrivateKey(value crypto.Signer) Option { - return func(c *configuration) { - c.priv = &value - } -} - -// Issuer is an Option for setting the identity's issuer. -func Issuer(value *Identity) Option { - return func(c *configuration) { - c.issuer = value - } -} - -// NotBefore is an Option for setting the identity's certificate's NotBefore. -func NotBefore(value time.Time) Option { - return func(c *configuration) { - c.notBefore = &value - } -} - -// NotAfter is an Option for setting the identity's certificate's NotAfter. -func NotAfter(value time.Time) Option { - return func(c *configuration) { - c.notAfter = &value - } -} - -// IssuingCertificateURL is an Option for setting the identity's certificate's -// IssuingCertificateURL. -func IssuingCertificateURL(value ...string) Option { - return func(c *configuration) { - c.issuingCertificateURL = append(c.issuingCertificateURL, value...) - } -} - -// OCSPServer is an Option for setting the identity's certificate's OCSPServer. -func OCSPServer(value ...string) Option { - return func(c *configuration) { - c.ocspServer = append(c.ocspServer, value...) - } -} - -// KeyUsage is an Option for setting the identity's certificate's KeyUsage. -func KeyUsage(ku x509.KeyUsage) Option { - return func(c *configuration) { - c.keyUsage = ku - } -} - -// IsCA is an Option for making an identity a certificate authority. -var IsCA Option = func(c *configuration) { - c.isCA = true -} diff --git a/vendor/github.com/github/smimesign/fakeca/identity.go b/vendor/github.com/github/smimesign/fakeca/identity.go deleted file mode 100644 index 19ec9a4d6f..0000000000 --- a/vendor/github.com/github/smimesign/fakeca/identity.go +++ /dev/null @@ -1,144 +0,0 @@ -package fakeca - -import ( - "bytes" - "crypto" - "crypto/ecdsa" - "crypto/rsa" - "crypto/x509" - "encoding/pem" - "errors" - "fmt" - "os/exec" -) - -// Identity is a certificate and private key. -type Identity struct { - Issuer *Identity - PrivateKey crypto.Signer - Certificate *x509.Certificate - NextSN int64 -} - -// New creates a new CA. -func New(opts ...Option) *Identity { - c := &configuration{} - - for _, opt := range opts { - option(opt)(c) - } - - return c.generate() -} - -// Issue issues a new Identity with this one as its parent. -func (id *Identity) Issue(opts ...Option) *Identity { - opts = append(opts, Issuer(id)) - return New(opts...) -} - -// PFX wraps the certificate and private key in an encrypted PKCS#12 packet. The -// provided password must be alphanumeric. -func (id *Identity) PFX(password string) []byte { - return toPFX(id.Certificate, id.PrivateKey, password) -} - -// Chain builds a slice of *x509.Certificate from this CA and its issuers. -func (id *Identity) Chain() []*x509.Certificate { - chain := []*x509.Certificate{} - for this := id; this != nil; this = this.Issuer { - chain = append(chain, this.Certificate) - } - - return chain -} - -// ChainPool builds an *x509.CertPool from this CA and its issuers. -func (id *Identity) ChainPool() *x509.CertPool { - chain := x509.NewCertPool() - for this := id; this != nil; this = this.Issuer { - chain.AddCert(this.Certificate) - } - - return chain -} - -// IncrementSN returns the next serial number. -func (id *Identity) IncrementSN() int64 { - defer func() { - id.NextSN++ - }() - - return id.NextSN -} - -func toPFX(cert *x509.Certificate, priv interface{}, password string) []byte { - // only allow alphanumeric passwords - for _, c := range password { - switch { - case c >= 'a' && c <= 'z': - case c >= 'A' && c <= 'Z': - case c >= '0' && c <= '9': - default: - panic("password must be alphanumeric") - } - } - - passout := fmt.Sprintf("pass:%s", password) - cmd := exec.Command("openssl", "pkcs12", "-export", "-passout", passout) - - cmd.Stdin = bytes.NewReader(append(append(toPKCS8(priv), '\n'), toPEM(cert)...)) - - out := new(bytes.Buffer) - cmd.Stdout = out - - if err := cmd.Run(); err != nil { - panic(err) - } - - return out.Bytes() -} - -func toPEM(cert *x509.Certificate) []byte { - buf := new(bytes.Buffer) - if err := pem.Encode(buf, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}); err != nil { - panic(err) - } - - return buf.Bytes() -} - -func toDER(priv interface{}) []byte { - var ( - der []byte - err error - ) - switch p := priv.(type) { - case *rsa.PrivateKey: - der = x509.MarshalPKCS1PrivateKey(p) - case *ecdsa.PrivateKey: - der, err = x509.MarshalECPrivateKey(p) - default: - err = errors.New("unknown key type") - } - if err != nil { - panic(err) - } - - return der -} - -func toPKCS8(priv interface{}) []byte { - cmd := exec.Command("openssl", "pkcs8", "-topk8", "-nocrypt", "-inform", "DER") - - cmd.Stdin = bytes.NewReader(toDER(priv)) - - out := new(bytes.Buffer) - cmd.Stdout = out - - if err := cmd.Run(); err != nil { - panic(err) - } - - return out.Bytes() -} diff --git a/vendor/modules.txt b/vendor/modules.txt index 70a6f9c1e8..9d3c6ec3db 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -66,9 +66,6 @@ github.com/dsnet/compress/internal/prefix # github.com/fatih/color v1.18.0 ## explicit; go 1.17 github.com/fatih/color -# github.com/github/smimesign v0.2.0 -## explicit; go 1.12 -github.com/github/smimesign/fakeca # github.com/go-ini/ini v1.67.0 ## explicit github.com/go-ini/ini From ffd2a0ab7f673914d30a57a2a9a48f65cf385781 Mon Sep 17 00:00:00 2001 From: Cedric Staub Date: Fri, 9 Jan 2026 21:37:20 -0800 Subject: [PATCH 08/11] Fix lint by removing +build tag line --- certstore/testca_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/certstore/testca_test.go b/certstore/testca_test.go index 730a223fe5..e0fc3c9ebe 100644 --- a/certstore/testca_test.go +++ b/certstore/testca_test.go @@ -1,5 +1,4 @@ //go:build !linux -// +build !linux package certstore From 4f4bd61e44b922bc0841daced331feb686dcc83f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 11 Jan 2026 22:38:32 +0000 Subject: [PATCH 09/11] Bump actions/upload-artifact from 5 to 6 Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/compile.yml | 12 ++++++------ .github/workflows/release.yml | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/compile.yml b/.github/workflows/compile.yml index 05abb85d81..ef509ba489 100644 --- a/.github/workflows/compile.yml +++ b/.github/workflows/compile.yml @@ -60,12 +60,12 @@ jobs: CGO_ENABLED=1 GOARCH=arm64 CC=aarch64-linux-gnu-gcc ./mage-bin -v go:build mv ghostunnel ghostunnel-linux-arm64 - name: Upload artifact - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: ghostunnel-linux-amd64-${{ matrix.os }} path: ghostunnel-linux-amd64 - name: Upload artifact - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: ghostunnel-linux-arm64-${{ matrix.os }} path: ghostunnel-linux-arm64 @@ -96,17 +96,17 @@ jobs: mv ghostunnel ghostunnel-darwin-arm64 lipo -create -output ghostunnel-darwin-universal ghostunnel-darwin-amd64 ghostunnel-darwin-arm64 - name: Upload artifact - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: ghostunnel-darwin-amd64 path: ghostunnel-darwin-amd64 - name: Upload artifact - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: ghostunnel-darwin-arm64 path: ghostunnel-darwin-arm64 - name: Upload artifact - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: ghostunnel-darwin-universal path: ghostunnel-darwin-universal @@ -131,7 +131,7 @@ jobs: - name: Build binary run: go tool mage -v go:build - name: Upload artifact - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: ghostunnel-windows-amd64 path: ghostunnel diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e1f1dc7c54..83d089ca8a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -36,12 +36,12 @@ jobs: CGO_ENABLED=1 GOARCH=arm64 CC=aarch64-linux-gnu-gcc ./mage-bin -v go:build mv ghostunnel ghostunnel-linux-arm64 - name: Upload artifact - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: ghostunnel-linux-amd64 path: ghostunnel-linux-amd64 - name: Upload artifact - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: ghostunnel-linux-arm64 path: ghostunnel-linux-arm64 @@ -73,17 +73,17 @@ jobs: mv ghostunnel ghostunnel-darwin-arm64 lipo -create -output ghostunnel-darwin-universal ghostunnel-darwin-amd64 ghostunnel-darwin-arm64 - name: Upload artifact - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: ghostunnel-darwin-amd64 path: ghostunnel-darwin-amd64 - name: Upload artifact - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: ghostunnel-darwin-arm64 path: ghostunnel-darwin-arm64 - name: Upload artifact - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: ghostunnel-darwin-universal path: ghostunnel-darwin-universal @@ -111,7 +111,7 @@ jobs: go tool mage -v go:build mv ghostunnel ghostunnel-windows-amd64.exe - name: Upload artifact - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: ghostunnel-windows-amd64.exe path: ghostunnel-windows-amd64.exe From 15a2c7ca0b45629ddef87bd3a234666b9f3d96be Mon Sep 17 00:00:00 2001 From: Cedric Staub Date: Sat, 10 Jan 2026 15:33:35 -0800 Subject: [PATCH 10/11] Add more unit tests to increase test coverage, add golangci-lint config file --- .golangci.yml | 22 ++++ certloader/certtlsconfig_test.go | 160 +++++++++++++++++++++++++++ certloader/spiffe_tls_config_test.go | 45 ++++++++ main_test.go | 43 +++++++ proxy/semaphore_test.go | 47 ++++++++ socket/net_test.go | 50 +++++++++ 6 files changed, 367 insertions(+) create mode 100644 .golangci.yml create mode 100644 certloader/certtlsconfig_test.go create mode 100644 proxy/semaphore_test.go diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000000..82d257a2bb --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,22 @@ +version: "2" + +linters: + # Use default linters (errcheck, govet, ineffassign, staticcheck, unused) + default: standard + # Enable human-readable exclusion presets including std-error-handling + # which excludes common error handling patterns like Close() + exclusions: + presets: + - std-error-handling + - common-false-positives + rules: + # Additional exclusions for os.Remove() in tests - cleanup that's best-effort + - linters: + - errcheck + text: "os.Remove" + path: "_test\\.go" + settings: + errcheck: + # Additional functions to exclude from checking + exclude-functions: + - (*github.com/ghostunnel/ghostunnel/certloader.spiffeTLSConfigSource).Close diff --git a/certloader/certtlsconfig_test.go b/certloader/certtlsconfig_test.go new file mode 100644 index 0000000000..79c8be08cf --- /dev/null +++ b/certloader/certtlsconfig_test.go @@ -0,0 +1,160 @@ +/*- + * Copyright 2025 Ghostunnel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package certloader + +import ( + "crypto/tls" + "crypto/x509" + "io" + "log" + "testing" + + "github.com/stretchr/testify/assert" +) + +// mockCertificateNoPrivateKey simulates a certificate without a private key +type mockCertificateNoPrivateKey struct{} + +func (m *mockCertificateNoPrivateKey) Reload() error { + return nil +} + +func (m *mockCertificateNoPrivateKey) GetIdentifier() string { + return "mock-cert-no-key" +} + +func (m *mockCertificateNoPrivateKey) GetCertificate(*tls.ClientHelloInfo) (*tls.Certificate, error) { + // Return a certificate without a private key + return &tls.Certificate{ + Certificate: [][]byte{}, + PrivateKey: nil, // No private key + }, nil +} + +func (m *mockCertificateNoPrivateKey) GetClientCertificate(*tls.CertificateRequestInfo) (*tls.Certificate, error) { + return &tls.Certificate{ + Certificate: [][]byte{}, + PrivateKey: nil, + }, nil +} + +func (m *mockCertificateNoPrivateKey) GetTrustStore() *x509.CertPool { + return nil +} + +// mockCertificateWithPrivateKey simulates a certificate with a private key +type mockCertificateWithPrivateKey struct{} + +func (m *mockCertificateWithPrivateKey) Reload() error { + return nil +} + +func (m *mockCertificateWithPrivateKey) GetIdentifier() string { + return "mock-cert-with-key" +} + +func (m *mockCertificateWithPrivateKey) GetCertificate(*tls.ClientHelloInfo) (*tls.Certificate, error) { + // Return a minimal certificate with a non-nil private key placeholder + // In practice, we just need PrivateKey to be non-nil for CanServe to return true + return &tls.Certificate{ + Certificate: [][]byte{}, + PrivateKey: struct{}{}, // Non-nil placeholder + }, nil +} + +func (m *mockCertificateWithPrivateKey) GetClientCertificate(*tls.CertificateRequestInfo) (*tls.Certificate, error) { + return &tls.Certificate{ + Certificate: [][]byte{}, + PrivateKey: struct{}{}, + }, nil +} + +func (m *mockCertificateWithPrivateKey) GetTrustStore() *x509.CertPool { + return x509.NewCertPool() +} + +func TestCertTLSConfigSourceGetServerConfigCannotServe(t *testing.T) { + cert := &mockCertificateNoPrivateKey{} + source := TLSConfigSourceFromCertificate(cert, log.New(io.Discard, "", 0)) + + _, err := source.GetServerConfig(nil) + assert.NotNil(t, err, "should fail when certificate cannot serve (no private key)") + assert.Contains(t, err.Error(), "cannot be used as a server") +} + +func TestCertTLSConfigSourceCanServeWithPrivateKey(t *testing.T) { + cert := &mockCertificateWithPrivateKey{} + source := TLSConfigSourceFromCertificate(cert, log.New(io.Discard, "", 0)) + + assert.True(t, source.CanServe(), "should be able to serve with private key") +} + +func TestCertTLSConfigSourceCanServeWithoutPrivateKey(t *testing.T) { + cert := &mockCertificateNoPrivateKey{} + source := TLSConfigSourceFromCertificate(cert, log.New(io.Discard, "", 0)) + + assert.False(t, source.CanServe(), "should not be able to serve without private key") +} + +func TestCertTLSConfigSourceGetClientConfig(t *testing.T) { + cert := &mockCertificateWithPrivateKey{} + source := TLSConfigSourceFromCertificate(cert, log.New(io.Discard, "", 0)) + + config, err := source.GetClientConfig(nil) + assert.Nil(t, err, "should succeed getting client config") + assert.NotNil(t, config, "client config should not be nil") + + tlsConfig := config.GetClientConfig() + assert.NotNil(t, tlsConfig, "TLS config should not be nil") +} + +func TestCertTLSConfigSourceGetServerConfig(t *testing.T) { + cert := &mockCertificateWithPrivateKey{} + source := TLSConfigSourceFromCertificate(cert, log.New(io.Discard, "", 0)) + + config, err := source.GetServerConfig(nil) + assert.Nil(t, err, "should succeed getting server config with private key") + assert.NotNil(t, config, "server config should not be nil") + + tlsConfig := config.GetServerConfig() + assert.NotNil(t, tlsConfig, "TLS config should not be nil") +} + +func TestNewCertTLSConfigNilBase(t *testing.T) { + cert := &mockCertificateWithPrivateKey{} + config := newCertTLSConfig(cert, nil) + + assert.NotNil(t, config.base, "base should be initialized to non-nil config when nil passed") +} + +func TestNewCertTLSConfigWithBase(t *testing.T) { + cert := &mockCertificateWithPrivateKey{} + base := &tls.Config{ + MinVersion: tls.VersionTLS13, + } + config := newCertTLSConfig(cert, base) + + assert.Equal(t, base, config.base, "base should be set to provided config") +} + +func TestCertTLSConfigSourceReload(t *testing.T) { + cert := &mockCertificateWithPrivateKey{} + source := TLSConfigSourceFromCertificate(cert, log.New(io.Discard, "", 0)) + + err := source.Reload() + assert.Nil(t, err, "reload should succeed") +} diff --git a/certloader/spiffe_tls_config_test.go b/certloader/spiffe_tls_config_test.go index c59620e172..9379fd7ac6 100644 --- a/certloader/spiffe_tls_config_test.go +++ b/certloader/spiffe_tls_config_test.go @@ -139,3 +139,48 @@ func countVerifyPeerCertificate(callCount *int32) func(rawCerts [][]byte, verifi return nil } } + +func TestSpiffeTLSConfigSourceReload(t *testing.T) { + td := spiffeid.RequireTrustDomainFromString("example.org") + ca := spiffetest.NewCA(t, td) + + svid := ca.CreateX509SVID(spiffeid.RequireFromPath(td, "/foo")) + + workloadAPI := spiffetest.New(t) + workloadAPI.SetX509SVIDResponse( + &spiffetest.X509SVIDResponse{ + Bundle: ca.X509Bundle(), + SVIDs: []*x509svid.SVID{svid}, + }) + defer workloadAPI.Stop() + + source, err := TLSConfigSourceFromWorkloadAPI(workloadAPI.Addr(), false, log.Default()) + require.NoError(t, err) + defer func() { _ = source.(*spiffeTLSConfigSource).Close() }() + + // Reload should be a no-op for SPIFFE (workload API maintains itself) + err = source.Reload() + require.NoError(t, err, "Reload should not return an error") +} + +func TestSpiffeTLSConfigSourceCanServe(t *testing.T) { + td := spiffeid.RequireTrustDomainFromString("example.org") + ca := spiffetest.NewCA(t, td) + + svid := ca.CreateX509SVID(spiffeid.RequireFromPath(td, "/foo")) + + workloadAPI := spiffetest.New(t) + workloadAPI.SetX509SVIDResponse( + &spiffetest.X509SVIDResponse{ + Bundle: ca.X509Bundle(), + SVIDs: []*x509svid.SVID{svid}, + }) + defer workloadAPI.Stop() + + source, err := TLSConfigSourceFromWorkloadAPI(workloadAPI.Addr(), false, log.Default()) + require.NoError(t, err) + defer func() { _ = source.(*spiffeTLSConfigSource).Close() }() + + // CanServe should always return true for SPIFFE source + require.True(t, source.CanServe(), "CanServe should return true") +} diff --git a/main_test.go b/main_test.go index ae297e59a5..f86b0422b4 100644 --- a/main_test.go +++ b/main_test.go @@ -17,6 +17,7 @@ package main import ( + "crypto/tls" "encoding/json" "errors" "net" @@ -28,6 +29,7 @@ import ( "testing" "time" + "github.com/ghostunnel/ghostunnel/certloader" "github.com/ghostunnel/ghostunnel/proxy" "github.com/stretchr/testify/assert" ) @@ -391,3 +393,44 @@ func TestProxyLoggingFlags(t *testing.T) { assert.Equal(t, proxyLoggerFlags([]string{"conn-errs", "handshake-errs"}), proxy.LogConnections) assert.Equal(t, proxyLoggerFlags([]string{"conns", "conn-errs"}), proxy.LogHandshakeErrors) } + +// failingTLSConfigSource is a mock TLSConfigSource that always returns errors +type failingTLSConfigSource struct{} + +func (f *failingTLSConfigSource) Reload() error { + return nil +} + +func (f *failingTLSConfigSource) CanServe() bool { + return false +} + +func (f *failingTLSConfigSource) GetClientConfig(base *tls.Config) (certloader.TLSClientConfig, error) { + return nil, errors.New("test error: GetClientConfig failed") +} + +func (f *failingTLSConfigSource) GetServerConfig(base *tls.Config) (certloader.TLSServerConfig, error) { + return nil, errors.New("test error: GetServerConfig failed") +} + +func TestMustGetServerConfigPanicsOnError(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Error("expected panic when GetServerConfig fails") + } + }() + + source := &failingTLSConfigSource{} + mustGetServerConfig(source, nil) +} + +func TestMustGetClientConfigPanicsOnError(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Error("expected panic when GetClientConfig fails") + } + }() + + source := &failingTLSConfigSource{} + mustGetClientConfig(source, nil) +} diff --git a/proxy/semaphore_test.go b/proxy/semaphore_test.go new file mode 100644 index 0000000000..2c65e26d84 --- /dev/null +++ b/proxy/semaphore_test.go @@ -0,0 +1,47 @@ +/*- + * Copyright 2025 Ghostunnel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package proxy + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestUnlimitedSemaphoreAcquireWithBackground(t *testing.T) { + sem := &unlimitedSemaphore{} + err := sem.Acquire(context.Background(), 1) + assert.Nil(t, err, "Acquire should return nil for background context") +} + +func TestUnlimitedSemaphoreAcquireWithCanceledContext(t *testing.T) { + sem := &unlimitedSemaphore{} + ctx, cancel := context.WithCancel(context.Background()) + cancel() + err := sem.Acquire(ctx, 1) + assert.NotNil(t, err, "Acquire should return context error when canceled") + assert.Equal(t, context.Canceled, err) +} + +func TestUnlimitedSemaphoreRelease(t *testing.T) { + sem := &unlimitedSemaphore{} + // Should not panic - Release is a no-op for unlimited semaphore + sem.Release(1) + sem.Release(100) + sem.Release(0) +} diff --git a/socket/net_test.go b/socket/net_test.go index 0eda44b309..b0304a78e6 100644 --- a/socket/net_test.go +++ b/socket/net_test.go @@ -17,6 +17,7 @@ package socket import ( + "path/filepath" "testing" "github.com/stretchr/testify/assert" @@ -105,3 +106,52 @@ func TestParseHTTPAddress(t *testing.T) { t.Errorf("unexpected address: %s", address) } } + +func TestOpenTCPSocket(t *testing.T) { + ln, err := Open("tcp", "127.0.0.1:0") + assert.Nil(t, err, "should be able to open TCP socket on random port") + defer func() { _ = ln.Close() }() + assert.NotNil(t, ln.Addr(), "listener should have an address") +} + +func TestOpenUnixSocket(t *testing.T) { + tmpDir := t.TempDir() + sockPath := filepath.Join(tmpDir, "test.sock") + ln, err := Open("unix", sockPath) + assert.Nil(t, err, "should be able to open Unix socket") + defer func() { _ = ln.Close() }() + assert.NotNil(t, ln.Addr(), "listener should have an address") +} + +func TestParseAndOpenTCPSuccess(t *testing.T) { + ln, err := ParseAndOpen("127.0.0.1:0") + assert.Nil(t, err, "should be able to parse and open TCP address") + defer func() { _ = ln.Close() }() +} + +func TestParseAndOpenUnixSuccess(t *testing.T) { + tmpDir := t.TempDir() + sockPath := filepath.Join(tmpDir, "test.sock") + ln, err := ParseAndOpen("unix:" + sockPath) + assert.Nil(t, err, "should be able to parse and open Unix socket") + defer func() { _ = ln.Close() }() +} + +func TestParseAndOpenInvalidAddress(t *testing.T) { + _, err := ParseAndOpen("invalid-no-port") + assert.NotNil(t, err, "should fail to parse invalid address") +} + +func TestParseAndOpenUnresolvable(t *testing.T) { + _, err := ParseAndOpen("nonexistent.invalid.domain.test:8080") + assert.NotNil(t, err, "should fail to resolve nonexistent domain") +} + +func TestParseAddressWithSkipResolve(t *testing.T) { + // With skipResolve=true, should not fail on unresolvable address + network, address, host, err := ParseAddress("nonexistent.invalid.domain.test:8080", true) + assert.Nil(t, err, "should succeed with skipResolve=true") + assert.Equal(t, "tcp", network) + assert.Equal(t, "nonexistent.invalid.domain.test:8080", address) + assert.Equal(t, "nonexistent.invalid.domain.test", host) +} From 3d7bf6153f1b3430d4f13be36b066bde4f0704d0 Mon Sep 17 00:00:00 2001 From: Cedric Staub Date: Sun, 11 Jan 2026 15:26:12 -0800 Subject: [PATCH 11/11] Fix golangci-lint govet issues - Move WaitGroup.Add before goroutine in proxy.go to fix race condition - Use net.JoinHostPort for IPv6 compatibility in status_test.go - Add nolint directives for CGo pointer conversions in certstore_darwin.go Co-Authored-By: Claude Opus 4.5 --- certstore/certstore_darwin.go | 4 ++-- proxy/proxy.go | 2 +- status_test.go | 3 +-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/certstore/certstore_darwin.go b/certstore/certstore_darwin.go index c57d232292..0149d77fcb 100644 --- a/certstore/certstore_darwin.go +++ b/certstore/certstore_darwin.go @@ -445,8 +445,8 @@ func mapToCFDictionary(gomap map[C.CFTypeRef]C.CFTypeRef) C.CFDictionaryRef { ) for k, v := range gomap { - keys = append(keys, unsafe.Pointer(k)) - values = append(values, unsafe.Pointer(v)) + keys = append(keys, unsafe.Pointer(k)) //nolint:govet // CGo pointer conversion is safe here + values = append(values, unsafe.Pointer(v)) //nolint:govet // CGo pointer conversion is safe here } return C.CFDictionaryCreate(nilCFAllocatorRef, &keys[0], &values[0], C.CFIndex(n), nil, nil) diff --git a/proxy/proxy.go b/proxy/proxy.go index aa285a8346..c15df94b78 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -191,8 +191,8 @@ func (p *Proxy) Accept() { continue } + p.handlers.Add(1) go connTimer.Time(func() { - p.handlers.Add(1) openCounter.Inc(1) totalCounter.Inc(1) diff --git a/status_test.go b/status_test.go index 600c984386..0cd00aa44e 100644 --- a/status_test.go +++ b/status_test.go @@ -20,7 +20,6 @@ import ( "context" "encoding/json" "errors" - "fmt" "io" "net" "net/http" @@ -253,7 +252,7 @@ func statusTargetWithResponseStatusCode(code int) (statusResponse, int) { return nil, errors.New("simulating error when talking to backend") } u, _ := url.Parse(statusTarget.URL) // NOTE: I tried using statusTarget.Config.Addr instead, but it wasn't set. - return net.Dial("tcp", fmt.Sprintf("%s:%s", u.Hostname(), u.Port())) + return net.Dial("tcp", net.JoinHostPort(u.Hostname(), u.Port())) }, "", "", "", statusTarget.URL) req := httptest.NewRequest(http.MethodGet, "/not-empty", nil)