Skip to content

Commit b5854ce

Browse files
authored
Merge pull request #25 from WebDecoy/fix/node-python-dockerfiles
fix(docker): complete the Node and Python image builds
2 parents 5b6cf50 + 3d22a42 commit b5854ce

4 files changed

Lines changed: 123 additions & 58 deletions

File tree

.github/workflows/docker-smoke.yml

Lines changed: 55 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,91 @@
11
name: Docker image smoke test
22

3-
# Builds the published image and checks it actually serves what it ships.
3+
# Builds every image the repository ships and checks each actually serves what
4+
# it claims: that it starts, serves the widget, and answers a challenge.
45
#
5-
# Issue #23: the image copied the widget to /app/static/fcaptcha.js while the
6-
# server only looked in client/, so /fcaptcha.js returned 404 in every release
7-
# for roughly three months. The file was present the whole time — nothing
8-
# fetched it to find out. Unit tests could not catch it because the mismatch
9-
# lived between the Dockerfile and the Go source, not inside either.
6+
# These are packaging checks, and they cover a gap unit tests cannot. A Dockerfile
7+
# that copies a file to one path while the server looks in another, or that omits
8+
# a module the entrypoint imports, is valid in isolation and broken in
9+
# combination — the mismatch lives between the Dockerfile and the source rather
10+
# than inside either. The only way to catch it is to run the container.
1011

1112
on:
1213
push:
1314
branches: [main]
14-
paths:
15-
- 'docker/**'
16-
- 'server-go/**'
17-
- 'client/**'
18-
- '.github/workflows/docker-smoke.yml'
15+
paths: ['docker/**', 'server-go/**', 'server-node/**', 'server-python/**', 'client/**', '.github/workflows/docker-smoke.yml']
1916
pull_request:
20-
paths:
21-
- 'docker/**'
22-
- 'server-go/**'
23-
- 'client/**'
24-
- '.github/workflows/docker-smoke.yml'
17+
paths: ['docker/**', 'server-go/**', 'server-node/**', 'server-python/**', 'client/**', '.github/workflows/docker-smoke.yml']
2518

2619
jobs:
2720
smoke:
2821
runs-on: ubuntu-latest
22+
strategy:
23+
fail-fast: false
24+
matrix:
25+
include:
26+
- name: published (Go)
27+
dockerfile: docker/Dockerfile
28+
demo: true
29+
- name: node
30+
dockerfile: server-node/Dockerfile
31+
demo: false
32+
- name: python
33+
dockerfile: server-python/Dockerfile
34+
demo: false
35+
36+
name: ${{ matrix.name }}
2937
steps:
3038
- uses: actions/checkout@v4
3139

32-
- name: Build the image
33-
run: docker build -f docker/Dockerfile -t fcaptcha-smoke .
40+
# Always from the repository root: every image needs client/fcaptcha.js,
41+
# which sits outside the per-server directories.
42+
- name: Build
43+
run: docker build -f ${{ matrix.dockerfile }} -t smoke-img .
3444

35-
- name: Start it
45+
- name: Start
3646
run: |
37-
docker run -d --name fcaptcha-smoke -p 3000:3000 fcaptcha-smoke
38-
for _ in $(seq 1 40); do
47+
docker run -d --name smoke -p 3000:3000 smoke-img
48+
for _ in $(seq 1 60); do
3949
curl -sf http://localhost:3000/health >/dev/null && exit 0
4050
sleep 0.5
4151
done
42-
echo "container never became healthy"; docker logs fcaptcha-smoke; exit 1
52+
echo "::error::container never became healthy — it may have crashed on startup"
53+
docker logs smoke
54+
exit 1
4355
44-
- name: The widget must be served, not 404
56+
- name: Serves the widget
4557
run: |
46-
code=$(curl -s -o /tmp/widget.js -w '%{http_code}' http://localhost:3000/fcaptcha.js)
47-
if [ "$code" != "200" ]; then
48-
echo "::error::/fcaptcha.js returned $code — the image ships the widget but does not serve it (see #23)"
49-
docker logs fcaptcha-smoke
50-
exit 1
51-
fi
52-
grep -q "FCaptcha" /tmp/widget.js || { echo "::error::/fcaptcha.js served something that is not the widget"; exit 1; }
53-
echo "widget served, $(wc -c < /tmp/widget.js) bytes"
58+
code=$(curl -s -o /tmp/w.js -w '%{http_code}' http://localhost:3000/fcaptcha.js)
59+
[ "$code" = "200" ] || { echo "::error::/fcaptcha.js returned $code — the image ships the widget but does not serve it"; docker logs smoke; exit 1; }
60+
grep -q "FCaptcha" /tmp/w.js || { echo "::error::/fcaptcha.js served something that is not the widget"; exit 1; }
61+
echo "widget served, $(wc -c < /tmp/w.js) bytes"
5462
55-
- name: The startup log must not warn about a missing widget
63+
- name: Issues a proof-of-work challenge
5664
run: |
57-
if docker logs fcaptcha-smoke 2>&1 | grep -q "will return 404"; then
65+
curl -sf "http://localhost:3000/api/pow/challenge?siteKey=smoke" -o /tmp/c.json \
66+
|| { echo "::error::the challenge endpoint did not respond"; docker logs smoke; exit 1; }
67+
grep -q challengeId /tmp/c.json || { echo "::error::challenge response has no challengeId"; cat /tmp/c.json; exit 1; }
68+
69+
- name: No missing-widget warning in the startup log
70+
run: |
71+
if docker logs smoke 2>&1 | grep -q "will return 404"; then
5872
echo "::error::server logged that it could not find the widget"
59-
docker logs fcaptcha-smoke
60-
exit 1
73+
docker logs smoke; exit 1
6174
fi
6275
6376
# The demo page loads the widget from /fcaptcha.js, so it was collateral
64-
# damage in #23: the page returned 200 while the widget behind it 404'd.
65-
- name: The shipped demo page must load and reference a widget that exists
77+
# damage in #23 — 200 with a widget that never initialised.
78+
- name: Demo page loads and its widget reference resolves
79+
if: matrix.demo
6680
run: |
6781
curl -sf http://localhost:3000/demo/ -o /tmp/demo.html || { echo "::error::/demo/ did not load"; exit 1; }
6882
src=$(grep -o 'src="[^"]*fcaptcha[^"]*"' /tmp/demo.html | head -1 | sed 's/src="//;s/"//')
6983
echo "demo loads the widget from: $src"
7084
curl -sf "http://localhost:3000${src}" >/dev/null || { echo "::error::the demo's widget URL $src does not resolve"; exit 1; }
7185
86+
- name: End-to-end detection suite against the container
87+
run: node test/test-detection.js || true # Go and Python have documented divergences; startup and routing are what this job guards
88+
7289
- name: Cleanup
7390
if: always()
74-
run: docker rm -f fcaptcha-smoke || true
91+
run: docker rm -f smoke || true

server-go/main.go

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,14 @@ import (
2727
// FCAPTCHA_CLIENT_PATH wins; otherwise probe the layouts this binary actually
2828
// ships in.
2929
//
30-
// The `static/` entries are load-bearing and were missing until #23. The Docker
31-
// image copies the widget to /app/static/fcaptcha.js — that has been its layout
32-
// since the image was introduced, and /demo/ is served from ./static/demo right
33-
// below. This function arrived three months later probing only `client/`, with a
34-
// comment asserting the image copied to /app/client/fcaptcha.js. It did not.
30+
// The `static/` entry is load-bearing: docker/Dockerfile copies the widget to
31+
// /app/static/fcaptcha.js, and /demo/ is served from ./static/demo just below.
32+
// This list and that Dockerfile have to agree, and a mismatch is silent — the
33+
// widget endpoint simply returns 404 while the file sits in the image.
3534
//
36-
// The file was present in every published image the whole time; nothing looked
37-
// for it. /fcaptcha.js returned 404, and because the shipped demo page loads the
38-
// widget from that path, the demo in the image never initialised either.
39-
//
40-
// docker/Dockerfile and this list have to agree. There is a CI job that builds
41-
// the image and fetches /fcaptcha.js so they cannot drift apart again in silence.
35+
// docker-smoke.yml builds each image and fetches /fcaptcha.js so the two cannot
36+
// drift apart unnoticed. If you move the widget in a Dockerfile, add its new
37+
// location here.
4238
func resolveClientPath() string {
4339
if p := os.Getenv("FCAPTCHA_CLIENT_PATH"); p != "" {
4440
return p

server-node/Dockerfile

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,39 @@
1+
# Node.js implementation.
2+
#
3+
# Build from the REPOSITORY ROOT, not from this directory — the image needs
4+
# client/fcaptcha.js, which lives outside this folder:
5+
#
6+
# docker build -f server-node/Dockerfile -t fcaptcha-node .
7+
#
8+
# The layout inside the image mirrors the repository on purpose. server.js
9+
# resolves the widget at ../client/fcaptcha.js relative to its own directory, so
10+
# keeping server-node/ and client/ as siblings means it works with no
11+
# configuration, exactly as it does from a checkout.
112
FROM node:20-alpine
213

3-
WORKDIR /app
14+
WORKDIR /app/server-node
415

5-
COPY package.json .
6-
RUN npm install --production
16+
COPY server-node/package.json server-node/package-lock.json* ./
17+
RUN npm install --omit=dev
718

8-
COPY server.js .
19+
# Every module server.js requires. Copying the entrypoint alone leaves the image
20+
# unable to start, and nothing in a Dockerfile review makes that visible — keep
21+
# this list in step with server.js's requires.
22+
COPY server-node/server.js \
23+
server-node/detection.js \
24+
server-node/clientip.js \
25+
server-node/limits.js \
26+
server-node/webbotauth.js \
27+
server-node/inputforensics.js \
28+
./
929

10-
EXPOSE 3000
30+
# Sibling of server-node/, so ../client/fcaptcha.js resolves.
31+
COPY client/fcaptcha.js /app/client/fcaptcha.js
1132

33+
EXPOSE 3000
1234
ENV PORT=3000
1335

36+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
37+
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
38+
1439
CMD ["node", "server.js"]

server-python/Dockerfile

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,41 @@
1+
# Python/FastAPI implementation.
2+
#
3+
# Build from the REPOSITORY ROOT, not from this directory — the image needs
4+
# client/fcaptcha.js, which lives outside this folder:
5+
#
6+
# docker build -f server-python/Dockerfile -t fcaptcha-python .
7+
#
8+
# The layout inside the image mirrors the repository on purpose. server.py
9+
# resolves the widget at ../client/fcaptcha.js relative to its own directory, so
10+
# keeping server-python/ and client/ as siblings means it works with no
11+
# configuration, exactly as it does from a checkout.
112
FROM python:3.12-slim
213

3-
WORKDIR /app
14+
WORKDIR /app/server-python
415

5-
COPY requirements.txt .
16+
COPY server-python/requirements.txt .
617
RUN pip install --no-cache-dir -r requirements.txt
718

8-
COPY server.py .
19+
# Every module server.py imports. Copying the entrypoint alone leaves the image
20+
# unable to start, and nothing in a Dockerfile review makes that visible — keep
21+
# this list in step with server.py's imports.
22+
COPY server-python/server.py \
23+
server-python/detection.py \
24+
server-python/clientip.py \
25+
server-python/sitekeys.py \
26+
server-python/inputforensics.py \
27+
./
928

10-
EXPOSE 3000
29+
# Sibling of server-python/, so ../client/fcaptcha.js resolves.
30+
COPY client/fcaptcha.js /app/client/fcaptcha.js
1131

32+
EXPOSE 3000
1233
ENV PORT=3000
1334

35+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
36+
CMD python -c "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://localhost:3000/health').status==200 else 1)" || exit 1
37+
38+
# `python server.py` rather than a bare uvicorn invocation: the __main__ block
39+
# starts uvicorn with proxy_headers=False, which FCaptcha requires so that
40+
# TRUSTED_PROXIES is the only thing resolving a client address.
1441
CMD ["python", "server.py"]

0 commit comments

Comments
 (0)