17 Commits
Author SHA1 Message Date
Hide_D dc0e1a6da9 feat: store editor images in upload bind 2026-08-06 15:52:54 +00:00
Hide_D 9046a131f2 feat: add signed bind-backed user icon uploads 2026-08-06 15:40:55 +00:00
Hide_D a0e1271c88 Merge branch 'master' into feature/image-sync-trigger-20260806 2026-08-06 14:58:46 +00:00
Hide_D 2fb618aa5c feat: add scoped image sync endpoint 2026-08-06 14:58:41 +00:00
Hide_D 69f0fedc20 change local 2026-08-06 14:34:27 +00:00
Hide_D 5078f6f3b1 Generalize reverse proxy source allowlist 2026-08-06 11:32:40 +00:00
Hide_D 87cc2977d5 Disable legacy HTTP image hooks by default 2026-08-06 11:25:39 +00:00
Hide_D f04b81c606 Add secure Node image webhook service 2026-08-06 11:21:54 +00:00
Hide_D 24073326b3 Add canonical name-based general icons 2026-07-26 05:21:13 +00:00
Hide_D 785937addc 강서유서월드 추가 이미지 2025-06-27 03:38:03 +09:00
Hide_D 7a5e1950ec 다병종 이벤트 깃수 병종 이미지 2025-01-23 20:39:11 +09:00
Hide_D 5447189129 병종명 변경경 2024-12-28 20:48:03 +09:00
Hide_D 58c50c6655 버퍼 추가 2024-12-28 20:44:35 +09:00
Hide_D 5d1bf46ccd 병종 이미지 수정
- 이후 리뉴얼 때 큰 카드로 사용
2024-12-28 20:01:54 +09:00
Hide_D 2bed18136f 롤시나리오 2024-12-10 02:51:08 +09:00
Hide_D e0030cc390 강서유서월드 이미지 업데이트 2024-12-07 15:11:42 +09:00
Hide_D 885ab4502e 강서유서월드 전콘 2024-12-07 00:14:29 +09:00
1204 changed files with 1897 additions and 2 deletions
+23
View File
@@ -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
+6
View File
@@ -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/
+212
View File
@@ -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.
+112
View File
@@ -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}
+9
View File
@@ -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"]
+29
View File
@@ -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; }
}
}
+17
View File
@@ -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 "$@"
+69
View File
@@ -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
+23
View File
@@ -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)."
Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 548 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 387 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 577 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 468 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 397 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 600 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 503 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 617 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 457 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 427 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 387 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 495 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 500 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 495 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 420 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 433 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 467 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 453 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 425 KiB

Some files were not shown because too many files have changed in this diff Show More