diff --git a/.github/workflows/check_doc.yaml b/.github/workflows/check_doc.yaml new file mode 100644 index 000000000..5fea9809c --- /dev/null +++ b/.github/workflows/check_doc.yaml @@ -0,0 +1,63 @@ +name: Check Documentation + +on: + pull_request: + branches: + - '*' + paths: + - '.github/workflows/check_doc.yaml' + - 'docs/**' + +jobs: + + docs: + name: lint, build and verify + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Install markdownlint + run: | + npm install --global markdownlint@0.29.0 markdownlint-cli@0.35.0 + + - name: Lint + run: ./docs/scripts/lint.sh docs + + - name: Setup python + uses: actions/setup-python@v6 + with: + python-version: '3.12' + cache: 'pip' + cache-dependency-path: "./docs/requirements.txt" + + - name: Build documentation + working-directory: ./docs + run: | + pip install -r requirements.txt + mkdocs build --strict + + - name: Setup ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.4' + + - name: Install html-proofer + run: | + gem install nokogiri --version 1.18.6 --no-document -- --use-system-libraries + gem install html-proofer --version 5.0.10 --no-document -- --use-system-libraries + env: + NOKOGIRI_USE_SYSTEM_LIBRARIES: "true" + + # Comes from https://github.com/gjtorikian/html-proofer?tab=readme-ov-file#caching-with-continuous-integration + - name: Cache HTMLProofer + uses: actions/cache@v4 + with: + path: tmp/.htmlproofer + key: ${{ runner.os }}-htmlproofer + + - name: Verify + run: ./docs/scripts/verify.sh docs/site diff --git a/.github/workflows/check_doc.yml b/.github/workflows/check_doc.yml deleted file mode 100644 index 48afa3298..000000000 --- a/.github/workflows/check_doc.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Check Documentation - -on: - pull_request: - branches: - - '*' - -jobs: - - docs: - name: Check, verify and build documentation - runs-on: ubuntu-latest - - steps: - - name: Check out code - uses: actions/checkout@v5 - with: - fetch-depth: 0 - - - name: Check documentation - run: make docs-pull-images docs - env: - # These variables are not passed to workflows that are triggered by a pull request from a fork. - DOCS_VERIFY_SKIP: ${{ vars.DOCS_VERIFY_SKIP }} - DOCS_LINT_SKIP: ${{ vars.DOCS_LINT_SKIP }} diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yaml similarity index 100% rename from .github/workflows/documentation.yml rename to .github/workflows/documentation.yaml diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 157c551f9..9f26be973 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -2,18 +2,19 @@ site_name: Traefik site_description: Traefik Documentation site_author: traefik.io site_url: https://doc.traefik.io/traefik -dev_addr: 0.0.0.0:8000 +dev_addr: localhost:8000 repo_name: 'GitHub' repo_url: 'https://github.com/traefik/traefik' docs_dir: 'content' -product: proxy -# https://squidfunk.github.io/mkdocs-material/ +# Use custom version of mkdocs-material +# See https://github.com/traefik/mkdocs-material theme: name: 'traefik-labs' + product: proxy language: en include_sidebar: true favicon: assets/img/traefikproxy-icon-color.png diff --git a/docs/readme.md b/docs/readme.md index de1d8b677..822c1cd6a 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -16,3 +16,15 @@ [pymdown-extensions]: https://facelessuser.github.io/pymdown-extensions "PyMdown Extensions" [pymdown-extensions-src]: https://github.com/facelessuser/pymdown-extensions "PyMdown Extensions - Sources" + +## Build locally without docker + +```sh +# Pre-requisite: python3, pip and virtualenv +DOCS="/tmp/traefik-docs" +mkdir "$DOCS" +virtualenv "$DOCS" +source "$DOCS/bin/activate" +pip install -r requirements.txt +mkdocs serve # or mkdocs build +``` diff --git a/docs/scripts/lint.sh b/docs/scripts/lint.sh index 28f7716ed..89035b002 100755 --- a/docs/scripts/lint.sh +++ b/docs/scripts/lint.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash # This script will run a couple of linter on the documentation set -eu @@ -6,17 +6,17 @@ set -eu # We want to run all linters before returning success (exit 0) or failure (exit 1) # So this variable holds the global exit code EXIT_CODE=0 -readonly BASE_DIR=/app +readonly BASE_DIR="${1:-/app}" # Run YAML linter for Kubernetes multi-resource files -/lint-yaml.sh "${BASE_DIR}" || EXIT_CODE=1 +./docs/scripts/lint-yaml.sh "${BASE_DIR}" || EXIT_CODE=1 echo "== Linting Markdown" # Uses the file ".markdownlint.json" for setup cd "${BASE_DIR}" || exit 1 -LINTER_EXCLUSIONS="$(find "${BASE_DIR}/content" -type f -name '.markdownlint.json')" -GLOBAL_LINT_OPTIONS="--config ${BASE_DIR}/.markdownlint.json" +LINTER_EXCLUSIONS="$(find "content" -type f -name '.markdownlint.json')" +GLOBAL_LINT_OPTIONS="--config .markdownlint.json" # Lint the specific folders (containing linter specific rulesets) for LINTER_EXCLUSION in ${LINTER_EXCLUSIONS} @@ -27,6 +27,6 @@ do done # Lint all the content, excluding the previously done` -eval markdownlint "${GLOBAL_LINT_OPTIONS}" "${BASE_DIR}/content/**/*.md" || EXIT_CODE=1 +eval markdownlint "${GLOBAL_LINT_OPTIONS}" "content/**/*.md" || EXIT_CODE=1 exit "${EXIT_CODE}" diff --git a/pkg/server/router/tcp/router.go b/pkg/server/router/tcp/router.go index 0f5c8f843..283f1364c 100644 --- a/pkg/server/router/tcp/router.go +++ b/pkg/server/router/tcp/router.go @@ -3,6 +3,7 @@ package tcp import ( "bufio" "bytes" + "context" "crypto/tls" "errors" "io" @@ -222,7 +223,17 @@ func (r *Router) acmeTLSALPNHandler() tcp.Handler { } return tcp.HandlerFunc(func(conn tcp.WriteCloser) { - _ = tls.Server(conn, r.httpsTLSConfig).Handshake() + tlsConn := tls.Server(conn, r.httpsTLSConfig) + defer tlsConn.Close() + + // This avoids stale connections when validating the ACME challenge, + // as we expect a validation request to complete in a short period of time. + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + if err := tlsConn.HandshakeContext(ctx); err != nil { + log.Debug().Err(err).Msg("Error during ACME-TLS/1 handshake") + } }) } diff --git a/pkg/server/router/tcp/router_test.go b/pkg/server/router/tcp/router_test.go index 1c2875776..768a78ea9 100644 --- a/pkg/server/router/tcp/router_test.go +++ b/pkg/server/router/tcp/router_test.go @@ -697,6 +697,64 @@ func Test_Routing(t *testing.T) { } } +func Test_Router_acmeTLSALPNHandlerTimeout(t *testing.T) { + router, err := NewRouter() + require.NoError(t, err) + + router.httpsTLSConfig = &tls.Config{} + + listener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + + acceptCh := make(chan struct{}, 1) + go func() { + close(acceptCh) + + conn, err := listener.Accept() + require.NoError(t, err) + + defer listener.Close() + + router.acmeTLSALPNHandler(). + ServeTCP(conn.(*net.TCPConn)) + }() + + <-acceptCh + + conn, err := net.DialTimeout("tcp", listener.Addr().String(), 2*time.Second) + require.NoError(t, err) + + // This is a minimal truncated Client Hello message + // to simulate a hanging connection during TLS handshake. + clientHello := []byte{ + // TLS Record Header + 0x16, // Content Type: Handshake + 0x03, 0x01, // Version: TLS 1.0 (for compatibility) + 0x00, 0x50, // Length: 80 bytes + } + + _, err = conn.Write(clientHello) + require.NoError(t, err) + + errCh := make(chan error, 1) + go func() { + // This will return an EOF as the acmeTLSALPNHandler will close the connection + // after a timeout during the TLS handshake. + b := make([]byte, 256) + _, err = conn.Read(b) + + errCh <- err + }() + + select { + case err := <-errCh: + assert.ErrorIs(t, err, io.EOF) + + case <-time.After(3 * time.Second): + t.Fatal("Error: Timeout waiting for acmeTLSALPNHandler to close the connection") + } +} + // routerTCPCatchAll configures a TCP CatchAll No TLS - HostSNI(`*`) router. func routerTCPCatchAll(conf *runtime.Configuration) { conf.TCPRouters["tcp-catchall"] = &runtime.TCPRouterInfo{