Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc0e1a6da9 | ||
|
|
9046a131f2 | ||
|
|
a0e1271c88 | ||
|
|
2fb618aa5c | ||
|
|
69f0fedc20 | ||
|
|
5078f6f3b1 | ||
|
|
87cc2977d5 | ||
|
|
f04b81c606 | ||
|
|
24073326b3 | ||
|
|
785937addc | ||
|
|
7a5e1950ec | ||
|
|
5447189129 | ||
|
|
58c50c6655 | ||
|
|
5d1bf46ccd | ||
|
|
2bed18136f | ||
|
|
e0030cc390 | ||
|
|
885ab4502e |
@@ -0,0 +1,23 @@
|
||||
# Same-host native reverse proxies should use the loopback-only published port.
|
||||
IMAGE_BIND_ADDRESS=127.0.0.1
|
||||
IMAGE_PORT=8191
|
||||
IMAGE_REPOSITORY_PATH=/home/letrhee/sam_rebuild/image
|
||||
IMAGE_UID=1000
|
||||
IMAGE_GID=1000
|
||||
# Source CIDRs observed inside image-web. A host-native proxy normally appears
|
||||
# as this Compose network's Docker bridge gateway, not as 127.0.0.1.
|
||||
# Confirm after startup with: docker compose logs image-web
|
||||
TRUSTED_PROXY_CIDRS=172.24.0.1/32
|
||||
|
||||
IMAGE_REMOTE_URL=https://gitea.hided.net/devsam/image.git
|
||||
IMAGE_REPOSITORY_FULL_NAME=devsam/image
|
||||
IMAGE_DEFAULT_BRANCH=master
|
||||
IMAGE_ALLOWED_BRANCHES=master
|
||||
IMAGE_PUBLIC_BASES=https://sam.hided.net/image,https://sam-image.hided.net
|
||||
|
||||
GITEA_WEBHOOK_SECRET_FILE=./secrets/gitea_webhook_secret
|
||||
IMAGE_ADMIN_SECRET_FILE=./secrets/image_admin_secret
|
||||
IMAGE_SYNC_CORE_SECRET_FILE=./secrets/image_sync_core_secret
|
||||
IMAGE_SYNC_CORE2026_SECRET_FILE=./secrets/image_sync_core2026_secret
|
||||
IMAGE_UPLOAD_CORE_SECRET_FILE=./secrets/image_upload_core_secret
|
||||
IMAGE_UPLOAD_CORE2026_SECRET_FILE=./secrets/image_upload_core2026_secret
|
||||
@@ -2,3 +2,9 @@
|
||||
/hook/logs.txt
|
||||
/hook/list.json
|
||||
/hook/HashKey.php
|
||||
/hook/legacy-enabled
|
||||
/hook/inventory.v2.json
|
||||
/.env
|
||||
/secrets/*
|
||||
!/secrets/.gitkeep
|
||||
/runtime-data/
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
# Shared image repository
|
||||
|
||||
`icons/` is served as `/image/icons/`. General portraits follow these rules:
|
||||
|
||||
- `icons/장수/<장수명>.<확장자>` is the canonical shared path.
|
||||
- Scenario-specific collections keep a separate directory such as
|
||||
`icons/걸그룹/` or `icons/롤시나리오/`.
|
||||
- Root-level numeric files such as `icons/1047.jpg` are compatibility aliases
|
||||
for deployed legacy versions. Do not delete or replace them while those
|
||||
versions are in service.
|
||||
- A general name must map to one canonical file. If two records intentionally
|
||||
need different portraits, disambiguate the scenario name or use an explicit
|
||||
scenario-specific path.
|
||||
|
||||
The core2026 repository owns `resources/general-icons.json` and
|
||||
`tools/manage-general-icons.mjs`. Use that tool with this repository as
|
||||
`--image-root` to synchronize aliases, scenario paths, and verify that every
|
||||
catalog source exists and has identical bytes.
|
||||
|
||||
## Public URLs
|
||||
|
||||
The same tracked files are exposed through both URL contracts:
|
||||
|
||||
- `https://sam.hided.net/image/game/...` and `/image/icons/...`
|
||||
- `https://sam-image.hided.net/game/...` and `/icons/...`
|
||||
- `https://sam-image.hided.net/image/...` is a compatibility alias.
|
||||
|
||||
Do not redirect the old `/image/*` URLs to the dedicated domain. Existing PHP
|
||||
and core2026 clients use same-origin relative paths and can move independently.
|
||||
The image server never exposes the repository root, `.git`, PHP sources, or
|
||||
directory listings.
|
||||
|
||||
## Node webhook deployment service
|
||||
|
||||
`compose.yaml` runs a read-only Nginx static edge and an internal Node service.
|
||||
Only the edge publishes port 8191. The Node service verifies the raw Gitea
|
||||
`X-Gitea-Signature`, accepts push events for `devsam/image`, fetches the exact
|
||||
remote branch tip, rejects dirty or non-fast-forward updates, and serializes all
|
||||
Git changes. The initial and only allowed branch is `master` unless
|
||||
`IMAGE_ALLOWED_BRANCHES` is explicitly expanded.
|
||||
|
||||
The service is reverse-proxy agnostic. Caddy, Nginx, HAProxy, or another proxy
|
||||
can use the same port and path contract.
|
||||
|
||||
For a native reverse proxy running on the same host as Docker, publish port
|
||||
8191 on loopback only and proxy to `127.0.0.1:8191`:
|
||||
|
||||
```dotenv
|
||||
IMAGE_BIND_ADDRESS=127.0.0.1
|
||||
IMAGE_PORT=8191
|
||||
TRUSTED_PROXY_CIDRS=172.24.0.1/32
|
||||
```
|
||||
|
||||
These two addresses intentionally describe different hops. The native proxy
|
||||
connects to the host loopback address, while the Nginx process inside
|
||||
`image-web` normally observes the connection as coming from that Compose
|
||||
network's Docker bridge gateway (`172.24.0.1` in the example). Use the source
|
||||
shown in the `image-web` access/error log, then recreate `image-web` whenever
|
||||
`TRUSTED_PROXY_CIDRS` changes. Do not replace it with a Cloudflare CIDR: the
|
||||
direct peer of `image-web` remains the host-native reverse proxy path.
|
||||
|
||||
If the reverse proxy is on another host, bind an explicit private interface
|
||||
instead of loopback and apply the host `DOCKER-USER` (or equivalent) firewall
|
||||
rule. Keep every source CIDR observed by `image-web` in the untracked
|
||||
`TRUSTED_PROXY_CIDRS` value, separated by commas or spaces. Avoid `0.0.0.0`
|
||||
unless the firewall has already been verified.
|
||||
|
||||
### Prepare
|
||||
|
||||
```sh
|
||||
cp .env.example .env
|
||||
./deploy/scripts/init-secrets.sh
|
||||
docker compose config --quiet
|
||||
docker compose build
|
||||
```
|
||||
|
||||
Only for a proxy on another host, apply and inspect the dedicated Docker
|
||||
ingress chain with root privileges:
|
||||
|
||||
```sh
|
||||
sudo env TRUSTED_PROXY_CIDRS=192.0.2.10/32 IMAGE_PORT=8191 \
|
||||
./deploy/scripts/firewall-8191.sh apply
|
||||
sudo ./deploy/scripts/firewall-8191.sh check
|
||||
```
|
||||
|
||||
The script only owns the `SAM_IMAGE_INGRESS` chain and its port-8191 jump from
|
||||
`DOCKER-USER`; it does not flush shared firewall chains.
|
||||
|
||||
For the same-host native-proxy case, point the proxy upstream at loopback. A
|
||||
minimal Caddy site is:
|
||||
|
||||
```caddyfile
|
||||
sam-image.hided.net {
|
||||
reverse_proxy 127.0.0.1:8191
|
||||
}
|
||||
```
|
||||
|
||||
The equivalent Nginx/HAProxy configuration should use the same upstream. The
|
||||
application does not depend on Caddy-specific request behavior.
|
||||
|
||||
Secret values are generated under ignored `secrets/` files with mode 0600 and
|
||||
are never printed. Put the contents of `secrets/gitea_webhook_secret` into the
|
||||
Gitea webhook configuration. Configure a JSON push webhook targeting:
|
||||
|
||||
```text
|
||||
https://sam-image.hided.net/v1/hooks/gitea
|
||||
```
|
||||
|
||||
Use branch filter `master`. Disable the old PHP webhook before enabling the new
|
||||
writer. The legacy PHP files remain in `hook/` for an explicit rollback, but
|
||||
PHP and Node must never mutate the checkout concurrently.
|
||||
|
||||
### Fallback sync callers
|
||||
|
||||
If Gitea webhook delivery is missed, Core and Core2026 can request a restricted
|
||||
reconciliation through `POST /v1/sync`. This endpoint cannot select or change a
|
||||
branch: it only fetches the current active branch and applies the same clean
|
||||
worktree and fast-forward checks as a webhook deployment.
|
||||
|
||||
Each caller has a separate secret:
|
||||
|
||||
- `secrets/image_sync_core_secret` for legacy Core
|
||||
- `secrets/image_sync_core2026_secret` for Core2026
|
||||
|
||||
The caller sends its name, a timestamp, a unique request ID, and an HMAC-SHA256
|
||||
signature over `timestamp.request-id.<exact JSON body>`. Requests expire after
|
||||
five minutes and successful request IDs are persisted for replay protection.
|
||||
The body is either `{}` or `{ "commit": "<full-image-commit-sha>" }`; all other
|
||||
fields are rejected. The optional commit asserts the expected remote tip and
|
||||
does not grant checkout selection.
|
||||
|
||||
Distribute only the matching caller secret through an ignored secret file.
|
||||
Never give either caller `image_admin_secret`, which also authorizes explicit
|
||||
branch changes. This fallback handles webhook delivery outages; if the image
|
||||
service itself is stopped, restore it and run the caller command again.
|
||||
|
||||
### Short-lived user-icon uploads
|
||||
|
||||
Core and Core2026 can store validated account icons and editor attachments
|
||||
through this service with
|
||||
`PUT /v1/uploads/<user-icons|content>/<client>/<random-32-hex>.<extension>`. Each game
|
||||
server validates the authenticated user and image first, then sends the raw
|
||||
image body with `X-Image-Client`, `X-Image-Expires`, `X-Image-Request-Id`, and
|
||||
`X-Image-Signature` headers.
|
||||
|
||||
The signature is HMAC-SHA256 over
|
||||
`expires.requestId.pathname.contentType.sha256(body)`. Expiry may be at most
|
||||
five minutes in the future, so a grant cannot be reused for another path,
|
||||
content type, body, or later upload. The service also checks the image magic,
|
||||
caller scope, and request replay before it writes one immutable file below the
|
||||
host bind directory `runtime-data/uploads`. User uploads are deliberately not
|
||||
added to Git; Nginx exposes that bind read-only at `/icons/users/` for account
|
||||
icons and `/uploads/` for editor content. User icons retain the 50KB limit;
|
||||
editor content retains the existing 1MB limit.
|
||||
|
||||
Create separate upload secrets with `deploy/scripts/init-secrets.sh`. Mount only
|
||||
the matching `image_upload_core_secret` or `image_upload_core2026_secret` on the
|
||||
game server. The shared secrets stay server-side in Docker secrets; they are
|
||||
not returned to browsers or forwarded to Cloudflare.
|
||||
|
||||
Run `deploy/scripts/init-secrets.sh` before the first Compose start so
|
||||
`runtime-data/uploads` exists with permissions that allow the hook container to
|
||||
write and the Nginx container to read. Back up this directory independently of
|
||||
the Git repository when moving servers.
|
||||
|
||||
Legacy HTTP mutation is disabled by default. An emergency PHP rollback must
|
||||
first stop `image-hook`, then create the ignored `hook/legacy-enabled` sentinel
|
||||
in the legacy checkout before restoring its Caddy/Gitea route. Remove the
|
||||
sentinel before Node is started again. CLI execution of `hook/git_pull.php`
|
||||
from the `hook/` directory remains available for local recovery without
|
||||
exposing the HTTP endpoint.
|
||||
|
||||
### Start and verify
|
||||
|
||||
```sh
|
||||
docker compose up -d --build
|
||||
docker compose ps
|
||||
curl -fsS https://sam-image.hided.net/healthz
|
||||
curl -fsS https://sam-image.hided.net/v1/status
|
||||
curl -fsS https://sam-image.hided.net/game/back.jpg -o /dev/null
|
||||
curl -fsS https://sam-image.hided.net/image/icons/default.jpg -o /dev/null
|
||||
```
|
||||
|
||||
Nginx serves only `game/`, `icons/`, `hook/list.json`, and
|
||||
`hook/inventory.v2.json`. The API inventory additionally reports the deployed
|
||||
branch, full commit, generation time, asset list, and both public base URLs.
|
||||
|
||||
### Explicit branch deployment
|
||||
|
||||
Automatic pushes only update the active branch. Add a branch to
|
||||
`IMAGE_ALLOWED_BRANCHES`, recreate `image-hook`, and switch it with the internal
|
||||
signed administration command:
|
||||
|
||||
```sh
|
||||
./deploy/scripts/admin-deploy.sh <branch> [expected-commit]
|
||||
```
|
||||
|
||||
The administration route is not exposed through the public reverse proxy.
|
||||
|
||||
### Tests and rollback
|
||||
|
||||
```sh
|
||||
docker build -t sam-image-hook:test node-hook
|
||||
docker run --rm sam-image-hook:test npm test
|
||||
docker compose exec -T image-web nginx -t -c /tmp/nginx.conf
|
||||
```
|
||||
|
||||
Stopping `image-hook` does not remove the last checked-out files from the
|
||||
static service. For a full rollback, stop Node mutation first, restore the old
|
||||
Caddy/PHP webhook route, and only then enable the legacy writer. Preserve both
|
||||
checkouts until public file hashes and representative browser requests have
|
||||
been verified.
|
||||
@@ -0,0 +1,112 @@
|
||||
name: sam-image
|
||||
|
||||
services:
|
||||
image-hook:
|
||||
build:
|
||||
context: ./node-hook
|
||||
image: sam-image-hook:1.2.0
|
||||
restart: unless-stopped
|
||||
user: "${IMAGE_UID:-1000}:${IMAGE_GID:-1000}"
|
||||
read_only: true
|
||||
init: true
|
||||
environment:
|
||||
PORT: "8081"
|
||||
IMAGE_REPOSITORY_PATH: /data/image
|
||||
IMAGE_REMOTE_URL: ${IMAGE_REMOTE_URL:-https://gitea.hided.net/devsam/image.git}
|
||||
IMAGE_REPOSITORY_FULL_NAME: ${IMAGE_REPOSITORY_FULL_NAME:-devsam/image}
|
||||
IMAGE_DEFAULT_BRANCH: ${IMAGE_DEFAULT_BRANCH:-master}
|
||||
IMAGE_ALLOWED_BRANCHES: ${IMAGE_ALLOWED_BRANCHES:-master}
|
||||
IMAGE_PUBLIC_BASES: ${IMAGE_PUBLIC_BASES:-https://sam.hided.net/image,https://sam-image.hided.net}
|
||||
IMAGE_STATE_PATH: /var/lib/image-hook/state.json
|
||||
IMAGE_UPLOAD_ROOT: /var/lib/image-hook/uploads
|
||||
IMAGE_UPLOAD_STATE_PATH: /var/lib/image-hook/upload-state.json
|
||||
GITEA_WEBHOOK_SECRET_FILE: /run/secrets/gitea_webhook_secret
|
||||
IMAGE_ADMIN_SECRET_FILE: /run/secrets/image_admin_secret
|
||||
IMAGE_SYNC_CLIENT_SECRET_FILES: core=/run/secrets/image_sync_core_secret,core2026=/run/secrets/image_sync_core2026_secret
|
||||
IMAGE_UPLOAD_CLIENT_SECRET_FILES: core=/run/secrets/image_upload_core_secret,core2026=/run/secrets/image_upload_core2026_secret
|
||||
MAX_UPLOAD_BYTES: "51200"
|
||||
MAX_CONTENT_UPLOAD_BYTES: "1048576"
|
||||
volumes:
|
||||
- type: bind
|
||||
source: ${IMAGE_REPOSITORY_PATH:-.}
|
||||
target: /data/image
|
||||
- ./runtime-data:/var/lib/image-hook
|
||||
secrets:
|
||||
- gitea_webhook_secret
|
||||
- image_admin_secret
|
||||
- image_sync_core_secret
|
||||
- image_sync_core2026_secret
|
||||
- image_upload_core_secret
|
||||
- image_upload_core2026_secret
|
||||
tmpfs:
|
||||
- /tmp:size=16m,mode=1777
|
||||
cap_drop: [ALL]
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
pids_limit: 64
|
||||
mem_limit: 256m
|
||||
cpus: 1.0
|
||||
healthcheck:
|
||||
test: [CMD, node, -e, "fetch('http://127.0.0.1:8081/healthz').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 6
|
||||
start_period: 10s
|
||||
networks: [image-internal, image-egress]
|
||||
|
||||
image-web:
|
||||
build:
|
||||
context: ./deploy/nginx
|
||||
image: sam-image-web:1.2.0
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
image-hook:
|
||||
condition: service_healthy
|
||||
read_only: true
|
||||
environment:
|
||||
TRUSTED_PROXY_CIDRS: ${TRUSTED_PROXY_CIDRS:?Set TRUSTED_PROXY_CIDRS to the direct reverse-proxy source CIDR list}
|
||||
ports:
|
||||
- "${IMAGE_BIND_ADDRESS:-0.0.0.0}:${IMAGE_PORT:-8191}:8080"
|
||||
volumes:
|
||||
- type: bind
|
||||
source: ${IMAGE_REPOSITORY_PATH:-.}
|
||||
target: /srv/image
|
||||
read_only: true
|
||||
- type: bind
|
||||
source: ./runtime-data/uploads
|
||||
target: /srv/uploads
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /tmp:size=8m,mode=1777
|
||||
cap_drop: [ALL]
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
pids_limit: 32
|
||||
mem_limit: 64m
|
||||
cpus: 0.5
|
||||
healthcheck:
|
||||
test: [CMD, wget, -q, -O, /dev/null, http://127.0.0.1:8080/healthz]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 6
|
||||
networks: [image-internal, image-edge]
|
||||
|
||||
networks:
|
||||
image-internal:
|
||||
internal: true
|
||||
image-egress:
|
||||
image-edge:
|
||||
|
||||
secrets:
|
||||
gitea_webhook_secret:
|
||||
file: ${GITEA_WEBHOOK_SECRET_FILE:-./secrets/gitea_webhook_secret}
|
||||
image_admin_secret:
|
||||
file: ${IMAGE_ADMIN_SECRET_FILE:-./secrets/image_admin_secret}
|
||||
image_sync_core_secret:
|
||||
file: ${IMAGE_SYNC_CORE_SECRET_FILE:-./secrets/image_sync_core_secret}
|
||||
image_sync_core2026_secret:
|
||||
file: ${IMAGE_SYNC_CORE2026_SECRET_FILE:-./secrets/image_sync_core2026_secret}
|
||||
image_upload_core_secret:
|
||||
file: ${IMAGE_UPLOAD_CORE_SECRET_FILE:-./secrets/image_upload_core_secret}
|
||||
image_upload_core2026_secret:
|
||||
file: ${IMAGE_UPLOAD_CORE2026_SECRET_FILE:-./secrets/image_upload_core2026_secret}
|
||||
@@ -0,0 +1,9 @@
|
||||
FROM nginx:1.29.5-alpine@sha256:1eff5a5f3fcf8431a0abb7eddf5471fec24e5e1905a2581aeacdb07a4479b92b
|
||||
|
||||
COPY templates/default.conf.template /etc/image/default.conf.template
|
||||
COPY entrypoint.sh /usr/local/bin/image-web-entrypoint
|
||||
RUN chmod 0555 /usr/local/bin/image-web-entrypoint
|
||||
|
||||
USER nginx
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["/usr/local/bin/image-web-entrypoint"]
|
||||
@@ -0,0 +1,29 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
trusted_proxy_cidrs=${TRUSTED_PROXY_CIDRS:-${CADDY_SOURCE_CIDR:-}}
|
||||
|
||||
if [ -z "$trusted_proxy_cidrs" ]; then
|
||||
echo "TRUSTED_PROXY_CIDRS is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
: > /tmp/trusted-proxy-allow.conf
|
||||
for trusted_proxy_cidr in $(printf '%s' "$trusted_proxy_cidrs" | tr ',' ' '); do
|
||||
case "$trusted_proxy_cidr" in
|
||||
*[!0-9A-Fa-f:./]*)
|
||||
echo "Invalid trusted proxy CIDR: $trusted_proxy_cidr" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
printf 'allow %s;\n' "$trusted_proxy_cidr" >> /tmp/trusted-proxy-allow.conf
|
||||
done
|
||||
|
||||
if [ ! -s /tmp/trusted-proxy-allow.conf ]; then
|
||||
echo "TRUSTED_PROXY_CIDRS must contain at least one CIDR" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
cp /etc/image/default.conf.template /tmp/nginx.conf
|
||||
|
||||
exec nginx -c /tmp/nginx.conf -g 'daemon off;'
|
||||
@@ -0,0 +1,137 @@
|
||||
worker_processes auto;
|
||||
pid /tmp/nginx.pid;
|
||||
error_log /dev/stderr notice;
|
||||
|
||||
events {
|
||||
worker_connections 256;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
access_log /dev/stdout combined;
|
||||
client_body_temp_path /tmp/client_body;
|
||||
proxy_temp_path /tmp/proxy;
|
||||
fastcgi_temp_path /tmp/fastcgi;
|
||||
uwsgi_temp_path /tmp/uwsgi;
|
||||
scgi_temp_path /tmp/scgi;
|
||||
|
||||
server {
|
||||
listen 8080;
|
||||
server_name _;
|
||||
server_tokens off;
|
||||
root /srv/image;
|
||||
disable_symlinks on;
|
||||
|
||||
allow 127.0.0.1;
|
||||
allow ::1;
|
||||
include /tmp/trusted-proxy-allow.conf;
|
||||
deny all;
|
||||
|
||||
location = /healthz {
|
||||
proxy_pass http://image-hook:8081/healthz;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
|
||||
location = /v1/status {
|
||||
proxy_pass http://image-hook:8081/v1/status;
|
||||
proxy_set_header Host $host;
|
||||
add_header Access-Control-Allow-Origin "*" always;
|
||||
}
|
||||
|
||||
location = /v1/inventory {
|
||||
proxy_pass http://image-hook:8081/v1/inventory;
|
||||
proxy_set_header Host $host;
|
||||
proxy_buffering off;
|
||||
add_header Access-Control-Allow-Origin "*" always;
|
||||
}
|
||||
|
||||
location = /v1/hooks/gitea {
|
||||
limit_except POST { deny all; }
|
||||
client_max_body_size 1m;
|
||||
proxy_pass http://image-hook:8081/v1/hooks/gitea;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Gitea-Signature $http_x_gitea_signature;
|
||||
proxy_set_header X-Gitea-Event $http_x_gitea_event;
|
||||
proxy_set_header X-Gitea-Delivery $http_x_gitea_delivery;
|
||||
proxy_request_buffering on;
|
||||
}
|
||||
|
||||
location = /v1/sync {
|
||||
limit_except POST { deny all; }
|
||||
client_max_body_size 4k;
|
||||
proxy_pass http://image-hook:8081/v1/sync;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Image-Client $http_x_image_client;
|
||||
proxy_set_header X-Image-Timestamp $http_x_image_timestamp;
|
||||
proxy_set_header X-Image-Request-Id $http_x_image_request_id;
|
||||
proxy_set_header X-Image-Signature $http_x_image_signature;
|
||||
proxy_request_buffering on;
|
||||
}
|
||||
|
||||
location ^~ /v1/uploads/ {
|
||||
limit_except PUT { deny all; }
|
||||
client_max_body_size 1m;
|
||||
proxy_pass http://image-hook:8081;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Image-Client $http_x_image_client;
|
||||
proxy_set_header X-Image-Expires $http_x_image_expires;
|
||||
proxy_set_header X-Image-Request-Id $http_x_image_request_id;
|
||||
proxy_set_header X-Image-Signature $http_x_image_signature;
|
||||
proxy_request_buffering on;
|
||||
}
|
||||
|
||||
location ^~ /v1/admin/ { return 404; }
|
||||
location = /image { return 404; }
|
||||
location = /image/ { return 404; }
|
||||
location ^~ /image/ { rewrite ^/image/(.*)$ /$1 last; }
|
||||
|
||||
location ^~ /game/ {
|
||||
try_files $uri =404;
|
||||
add_header Access-Control-Allow-Origin "*" always;
|
||||
add_header X-Content-Type-Options nosniff always;
|
||||
}
|
||||
|
||||
location ~ "^/icons/users/([a-z0-9][a-z0-9_-]{1,31})/([a-f0-9]{32}\.(?:avif|webp|jpe?g|png|gif))$" {
|
||||
alias /srv/uploads/user-icons/$1/$2;
|
||||
etag on;
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable" always;
|
||||
add_header Access-Control-Allow-Origin "*" always;
|
||||
add_header X-Content-Type-Options nosniff always;
|
||||
}
|
||||
|
||||
location /icons/users/ { return 404; }
|
||||
|
||||
location ~ "^/uploads/([a-z0-9][a-z0-9_-]{1,31})/([a-f0-9]{32}\.(?:avif|webp|jpe?g|png|gif))$" {
|
||||
alias /srv/uploads/content/$1/$2;
|
||||
etag on;
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable" always;
|
||||
add_header Access-Control-Allow-Origin "*" always;
|
||||
add_header X-Content-Type-Options nosniff always;
|
||||
}
|
||||
|
||||
location /uploads/ { return 404; }
|
||||
|
||||
location /icons/ {
|
||||
try_files $uri =404;
|
||||
add_header Access-Control-Allow-Origin "*" always;
|
||||
add_header X-Content-Type-Options nosniff always;
|
||||
}
|
||||
|
||||
location = /hook/list.json {
|
||||
try_files $uri =404;
|
||||
add_header Access-Control-Allow-Origin "*" always;
|
||||
}
|
||||
|
||||
location = /hook/inventory.v2.json {
|
||||
try_files $uri =404;
|
||||
add_header Access-Control-Allow-Origin "*" always;
|
||||
}
|
||||
|
||||
location ~ (^|/)\. { return 404; }
|
||||
location ~ \.php$ { return 404; }
|
||||
location / { return 404; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
if [ "$#" -lt 1 ] || [ "$#" -gt 2 ]; then
|
||||
echo "Usage: $0 <branch> [commit]" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
repository_dir=$(CDPATH= cd -- "$script_dir/../.." && pwd)
|
||||
branch=$1
|
||||
commit=${2:-}
|
||||
set -- deploy --branch "$branch"
|
||||
if [ -n "$commit" ]; then
|
||||
set -- "$@" --commit "$commit"
|
||||
fi
|
||||
exec docker compose --project-directory "$repository_dir" exec -T image-hook node src/admin-cli.mjs "$@"
|
||||
@@ -0,0 +1,69 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
action=${1:-check}
|
||||
trusted_proxy_cidrs=${TRUSTED_PROXY_CIDRS:-${CADDY_SOURCE_CIDR:-}}
|
||||
image_port=${IMAGE_PORT:-8191}
|
||||
chain=SAM_IMAGE_INGRESS
|
||||
|
||||
case "$image_port" in
|
||||
''|*[!0-9]*) echo "IMAGE_PORT must be numeric" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
require_root() {
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "Run this action as root." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
case "$action" in
|
||||
check)
|
||||
iptables -S DOCKER-USER 2>/dev/null | grep -F "$chain" || true
|
||||
iptables -S "$chain" 2>/dev/null || true
|
||||
;;
|
||||
apply)
|
||||
require_root
|
||||
if [ -z "$trusted_proxy_cidrs" ]; then
|
||||
echo "TRUSTED_PROXY_CIDRS is required" >&2
|
||||
exit 2
|
||||
fi
|
||||
rule_count=0
|
||||
for trusted_proxy_cidr in $(printf '%s' "$trusted_proxy_cidrs" | tr ',' ' '); do
|
||||
case "$trusted_proxy_cidr" in
|
||||
*[!0-9A-Fa-f:./]*)
|
||||
echo "Invalid trusted proxy CIDR: $trusted_proxy_cidr" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
rule_count=$((rule_count + 1))
|
||||
done
|
||||
if [ "$rule_count" -eq 0 ]; then
|
||||
echo "TRUSTED_PROXY_CIDRS must contain at least one CIDR" >&2
|
||||
exit 2
|
||||
fi
|
||||
iptables -n -L DOCKER-USER >/dev/null
|
||||
iptables -n -L "$chain" >/dev/null 2>&1 || iptables -N "$chain"
|
||||
iptables -F "$chain"
|
||||
iptables -A "$chain" -j DROP
|
||||
for trusted_proxy_cidr in $(printf '%s' "$trusted_proxy_cidrs" | tr ',' ' '); do
|
||||
iptables -I "$chain" 1 -s "$trusted_proxy_cidr" -j ACCEPT
|
||||
done
|
||||
iptables -C DOCKER-USER -p tcp -m conntrack --ctorigdstport "$image_port" -j "$chain" 2>/dev/null \
|
||||
|| iptables -I DOCKER-USER 1 -p tcp -m conntrack --ctorigdstport "$image_port" -j "$chain"
|
||||
;;
|
||||
remove)
|
||||
require_root
|
||||
while iptables -C DOCKER-USER -p tcp -m conntrack --ctorigdstport "$image_port" -j "$chain" 2>/dev/null; do
|
||||
iptables -D DOCKER-USER -p tcp -m conntrack --ctorigdstport "$image_port" -j "$chain"
|
||||
done
|
||||
if iptables -n -L "$chain" >/dev/null 2>&1; then
|
||||
iptables -F "$chain"
|
||||
iptables -X "$chain"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 [check|apply|remove]" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,23 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
repository_dir=$(CDPATH= cd -- "$script_dir/../.." && pwd)
|
||||
secret_dir="$repository_dir/secrets"
|
||||
state_dir="$repository_dir/runtime-data"
|
||||
|
||||
umask 077
|
||||
mkdir -p "$secret_dir"
|
||||
mkdir -p "$state_dir"
|
||||
mkdir -p "$state_dir/uploads"
|
||||
for name in gitea_webhook_secret image_admin_secret image_sync_core_secret image_sync_core2026_secret image_upload_core_secret image_upload_core2026_secret; do
|
||||
path="$secret_dir/$name"
|
||||
if [ ! -e "$path" ]; then
|
||||
openssl rand -hex 32 > "$path"
|
||||
fi
|
||||
chmod 600 "$path"
|
||||
done
|
||||
chmod 700 "$state_dir"
|
||||
chmod 755 "$state_dir/uploads"
|
||||
|
||||
echo "Secret files are ready in $secret_dir (values not printed)."
|
||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 548 KiB |
|
After Width: | Height: | Size: 387 KiB |
|
After Width: | Height: | Size: 461 KiB |
|
After Width: | Height: | Size: 446 KiB |
|
After Width: | Height: | Size: 405 KiB |
|
After Width: | Height: | Size: 360 KiB |
|
After Width: | Height: | Size: 378 KiB |
|
After Width: | Height: | Size: 577 KiB |
|
After Width: | Height: | Size: 409 KiB |
|
After Width: | Height: | Size: 468 KiB |
|
After Width: | Height: | Size: 397 KiB |
|
After Width: | Height: | Size: 460 KiB |
|
After Width: | Height: | Size: 600 KiB |
|
After Width: | Height: | Size: 358 KiB |
|
After Width: | Height: | Size: 503 KiB |
|
After Width: | Height: | Size: 617 KiB |
|
After Width: | Height: | Size: 459 KiB |
|
After Width: | Height: | Size: 398 KiB |
|
After Width: | Height: | Size: 457 KiB |
|
After Width: | Height: | Size: 427 KiB |
|
After Width: | Height: | Size: 550 KiB |
|
After Width: | Height: | Size: 387 KiB |
|
After Width: | Height: | Size: 495 KiB |
|
After Width: | Height: | Size: 500 KiB |
|
After Width: | Height: | Size: 495 KiB |
|
After Width: | Height: | Size: 420 KiB |
|
After Width: | Height: | Size: 433 KiB |
|
After Width: | Height: | Size: 436 KiB |
|
After Width: | Height: | Size: 362 KiB |
|
After Width: | Height: | Size: 467 KiB |
|
After Width: | Height: | Size: 453 KiB |
|
After Width: | Height: | Size: 425 KiB |