1
0
mirror of git://f0xx.org/android_cast synced 2026-07-29 03:38:52 +03:00

url shortener addons

This commit is contained in:
Anton Afanasyeu
2026-06-11 21:43:18 +02:00
parent 66d66e6b46
commit f70d7814c8
10 changed files with 570 additions and 2 deletions

View File

@@ -0,0 +1,41 @@
# URL shortener — deploy (FE → BE)
**Topology:** same as [INFRA.md](../../docs/INFRA.md) — TLS on Gentoo FE, app on Alpine BE `:80`.
## Prerequisites (confirmed)
| Item | Status |
|------|--------|
| DNS `s.f0xx.org` → FE | PO |
| FE TLS cert | `/etc/letsencrypt/live/s.f0xx.org/` |
| Git remote | `git://f0xx.org/androicast_project/url-shortener` |
| MariaDB schema `url_shortener` | `sql/schema.mariadb.sql` |
## FE (Gentoo)
Add `deploy/nginx.fe-s.f0xx.org.snippet` inside a new `server { listen 443 ssl; server_name s.f0xx.org; }` block
(mirror `apps.f0xx.org` TLS paths; cert paths above).
```bash
nginx -t && rc-service nginx reload
```
## BE (Alpine)
1. Sync this tree to docroot (suggested): `/var/www/localhost/htdocs/apps/s/`
2. Include `deploy/nginx.be-url-shortener.fragment` in `apps.conf` **or** dedicated `server_name` on BE
3. Apply SQL migration
4. `rc-service php-fpm81 restart` if needed
```bash
nginx -t && rc-service nginx reload
```
## Verify
```bash
curl -sSI https://s.f0xx.org/api/v1/health
./scripts/smoke_shorten.sh
```
Not added to `validate_be_services.sh` until first production deploy (SPEC).

View File

@@ -0,0 +1,42 @@
# URL shortener (`s.f0xx.org`)
Standalone opt-in service — **not** part of `examples/crash_reporter/backend/`.
| Doc | Path |
|-----|------|
| SPEC | [docs/specs/20100611_3_url_shortener.md](../../docs/specs/20100611_3_url_shortener.md) |
| DR | [docs/DRs/20100611_3_url_shortener.md](../../docs/DRs/20100611_3_url_shortener.md) |
| Deploy | [DEPLOY.md](DEPLOY.md) |
**Status:** scaffold (OpenAPI + SQL + nginx fragments + smoke scripts). PHP handlers TODO.
**Git remote (PO confirmed):** `git://f0xx.org/androicast_project/url-shortener`
**Public URL:** `https://s.f0xx.org`
**FE TLS (PO confirmed):** `/etc/letsencrypt/live/s.f0xx.org/{fullchain.pem,privkey.pem}`
## Quick smoke (when deployed)
```bash
export BASE=https://s.f0xx.org
export BEARER=demo-bearer-replace-me
./scripts/demo_curl.sh
```
## Local dev (future)
```bash
# MariaDB: apply sql/schema.mariadb.sql
# php -S 127.0.0.1:8091 -t public
```
## Layout
```text
backend/url-shortener/
openapi.yaml # API contract (source of truth with SPEC §6)
deploy/ # FE + BE nginx fragments
sql/schema.mariadb.sql
scripts/demo_curl.sh
scripts/smoke_shorten.sh
public/index.php # stub router (health only until impl)
```

View File

@@ -0,0 +1,29 @@
# BE Alpine — url-shortener vhost fragment
# Include from /etc/nginx/conf.d/apps.conf or separate file.
#
# Docroot: /var/www/localhost/htdocs/apps/s/public
# PHP-FPM: same socket as crash reporter unless load warrants a pool.
server {
listen 80;
server_name s.f0xx.org;
root /var/www/localhost/htdocs/apps/s/public;
index index.php;
# API + QR
location ^~ /api/ {
try_files $uri /index.php?$query_string;
}
# Short slug redirect (1216 hex chars)
location ~ ^/[a-f0-9]{6,16}$ {
try_files $uri /index.php?$query_string;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass unix:/run/php-fpm81.sock;
}
}

View File

@@ -0,0 +1,30 @@
# FE Gentoo — server block for s.f0xx.org (URL shortener)
# File: tmp/FE_gentoo/etc/nginx/nginx.conf OR conf.d/s.f0xx.org.conf
#
# TLS cert (confirmed on FE):
# ssl_certificate /etc/letsencrypt/live/s.f0xx.org/fullchain.pem;
# ssl_certificate_key /etc/letsencrypt/live/s.f0xx.org/privkey.pem;
server {
listen 80;
server_name s.f0xx.org;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name s.f0xx.org;
ssl_certificate /etc/letsencrypt/live/s.f0xx.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/s.f0xx.org/privkey.pem;
# All paths → BE :80 (PHP-FPM url-shortener vhost)
location / {
proxy_pass http://artc0.intra.raptor.org:80;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

View File

@@ -0,0 +1,253 @@
openapi: 3.0.3
info:
title: Android Cast URL Shortener
description: |
Standalone service at https://s.f0xx.org — SPEC docs/specs/20100611_3_url_shortener.md
version: 0.1.0
servers:
- url: https://s.f0xx.org
description: Production (FE TLS → BE :80)
paths:
/api/v1/health:
get:
summary: Service health
operationId: health
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
status:
type: string
service:
type: string
/api/v1/shorten:
get:
summary: Shorten URL (query params)
description: Prefer POST for long URLs or sensitive query strings.
operationId: shortenGet
parameters:
- $ref: '#/components/parameters/url'
- $ref: '#/components/parameters/bearer'
- $ref: '#/components/parameters/signer'
- $ref: '#/components/parameters/ttl'
responses:
'200':
$ref: '#/components/responses/ShortenSuccess'
'400':
$ref: '#/components/responses/ShortenError'
'401':
$ref: '#/components/responses/ShortenError'
'403':
$ref: '#/components/responses/ShortenError'
'409':
$ref: '#/components/responses/ShortenError'
'429':
$ref: '#/components/responses/ShortenError'
post:
summary: Shorten URL (JSON body) — preferred
operationId: shortenPost
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ShortenRequest'
responses:
'200':
$ref: '#/components/responses/ShortenSuccess'
'400':
$ref: '#/components/responses/ShortenError'
'401':
$ref: '#/components/responses/ShortenError'
'403':
$ref: '#/components/responses/ShortenError'
'409':
$ref: '#/components/responses/ShortenError'
'429':
$ref: '#/components/responses/ShortenError'
/api/v1/qr/{slug}.png:
get:
summary: QR code PNG for short URL
operationId: qrPng
parameters:
- name: slug
in: path
required: true
schema:
type: string
pattern: '^[a-f0-9]{6,16}$'
- name: size
in: query
schema:
type: integer
default: 256
minimum: 128
maximum: 512
responses:
'200':
description: PNG image
content:
image/png:
schema:
type: string
format: binary
'404':
description: Unknown slug
'410':
description: Expired slug
/{slug}:
get:
summary: Redirect to original URL
operationId: redirect
parameters:
- name: slug
in: path
required: true
schema:
type: string
pattern: '^[a-f0-9]{6,16}$'
responses:
'302':
description: Found — Location is original URL
headers:
Location:
schema:
type: string
format: uri
'404':
description: Unknown slug
'410':
description: Expired slug
head:
summary: Redirect probe (no body)
operationId: redirectHead
parameters:
- name: slug
in: path
required: true
schema:
type: string
responses:
'302':
description: Found
'404':
description: Unknown
'410':
description: Expired
components:
parameters:
url:
name: url
in: query
required: true
schema:
type: string
format: uri
maxLength: 2048
bearer:
name: bearer
in: query
required: true
schema:
type: string
minLength: 8
signer:
name: signer
in: query
required: false
schema:
type: string
ttl:
name: ttl
in: query
required: false
description: Seconds until expiry; 0 = permanent (signer required)
schema:
type: integer
minimum: 0
default: 86400
schemas:
ShortenRequest:
type: object
required: [url, bearer]
properties:
url:
type: string
format: uri
maxLength: 2048
bearer:
type: string
signer:
type: string
ttl:
type: integer
minimum: 0
default: 86400
ShortenSuccess:
type: object
required: [code, url, u, bearer, ttl]
properties:
code:
type: string
enum: ['0']
url:
type: string
format: uri
u:
type: string
format: uri
description: Short URL
bearer:
type: string
ttl:
type: string
description: Remaining seconds as string; "0" if permanent
qr:
type: string
format: uri
description: Optional v1.1 — URL to QR PNG
ShortenError:
type: object
required: [code, desc]
properties:
code:
type: string
enum:
- ACCESS_DENIED
- MISSING_BEARER
- INVALID_URL
- TTL_EXPIRED
- SIGNER_REQUIRED
- RATE_LIMITED
- INTERNAL
- NOT_IMPLEMENTED
url:
type: string
bearer:
type: string
desc:
type: string
responses:
ShortenSuccess:
description: Short URL created or cached
content:
application/json:
schema:
$ref: '#/components/schemas/ShortenSuccess'
ShortenError:
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ShortenError'

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
/**
* URL shortener front controller (stub).
* Routes: /api/v1/health, /api/v1/shorten, /api/v1/qr/{slug}.png, /{slug}
* Full implementation tracked in docs/specs/20100611_3_url_shortener.md
*/
header('Content-Type: application/json; charset=utf-8');
$uri = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';
if ($uri === '/api/v1/health') {
echo json_encode([
'status' => 'stub',
'service' => 'url-shortener',
'message' => 'PHP handlers not deployed yet — see backend/url-shortener/README.md',
], JSON_UNESCAPED_SLASHES);
exit;
}
http_response_code(501);
echo json_encode([
'code' => 'NOT_IMPLEMENTED',
'desc' => 'url-shortener API stub — deploy implementation from git://f0xx.org/androicast_project/url-shortener',
], JSON_UNESCAPED_SLASHES);

View File

@@ -0,0 +1,55 @@
#!/usr/bin/env bash
# End-to-end curl cookbook — SPEC appendix A.
# Usage:
# export BASE=https://s.f0xx.org
# export BEARER=your-trusted-token
# export SIGNER=optional-for-ttl0
# ./scripts/demo_curl.sh
set -euo pipefail
BASE="${BASE:-https://s.f0xx.org}"
BEARER="${BEARER:?set BEARER to a trusted API token}"
SIGNER="${SIGNER:-}"
TARGET="${TARGET:-https://defender.net/very/long/link?demo=1}"
TTL="${TTL:-3600}"
echo "==> 1) Health"
curl -fsS "$BASE/api/v1/health" | head -c 200
echo
echo "==> 2) Shorten (POST preferred)"
SHORT_JSON="$(curl -fsS -X POST "$BASE/api/v1/shorten" \
-H 'Content-Type: application/json' \
-d "$(jq -nc --arg url "$TARGET" --arg bearer "$BEARER" --argjson ttl "$TTL" \
'{url:$url,bearer:$bearer,ttl:$ttl}')")"
echo "$SHORT_JSON" | jq .
SHORT="$(echo "$SHORT_JSON" | jq -r '.u // empty')"
if [[ -z "$SHORT" ]]; then
echo "shorten failed (no .u in response)" >&2
exit 1
fi
SLUG="${SHORT##*/}"
echo "==> 3) Shorten cache hit (same bearer + url)"
curl -fsS -X POST "$BASE/api/v1/shorten" \
-H 'Content-Type: application/json' \
-d "$(jq -nc --arg url "$TARGET" --arg bearer "$BEARER" --argjson ttl "$TTL" \
'{url:$url,bearer:$bearer,ttl:$ttl}')" | jq .
echo "==> 4) Redirect (HEAD — no body)"
curl -sSI "$SHORT" | grep -E '^(HTTP|Location:)'
echo "==> 5) QR PNG"
curl -fsS -o "/tmp/url-shortener-qr-${SLUG}.png" "$BASE/api/v1/qr/${SLUG}.png?size=256"
file "/tmp/url-shortener-qr-${SLUG}.png"
if [[ -n "$SIGNER" ]]; then
echo "==> 6) Permanent link (ttl=0, signer required)"
curl -fsS -X POST "$BASE/api/v1/shorten" \
-H 'Content-Type: application/json' \
-d "$(jq -nc --arg url "$TARGET&perm=1" --arg bearer "$BEARER" --arg signer "$SIGNER" \
'{url:$url,bearer:$bearer,signer:$signer,ttl:0}')" | jq .
fi
echo "==> Done"

View File

@@ -0,0 +1,29 @@
#!/usr/bin/env bash
# Minimal deploy smoke — exits non-zero on failure.
set -euo pipefail
BASE="${BASE:-https://s.f0xx.org}"
BEARER="${BEARER:-}"
echo "==> health $BASE/api/v1/health"
code="$(curl -sS -o /tmp/url-shortener-health.json -w '%{http_code}' "$BASE/api/v1/health" || true)"
if [[ "$code" != "200" ]]; then
echo "FAIL health HTTP $code" >&2
cat /tmp/url-shortener-health.json 2>/dev/null || true
exit 1
fi
cat /tmp/url-shortener-health.json
echo
if [[ -z "$BEARER" ]]; then
echo "SKIP shorten (set BEARER to run full smoke)"
exit 0
fi
echo "==> shorten POST"
curl -fsS -X POST "$BASE/api/v1/shorten" \
-H 'Content-Type: application/json' \
-d '{"url":"https://example.com/smoke","bearer":"'"$BEARER"'","ttl":300}' \
| jq -e '.code == "0" and (.u | length) > 0' >/dev/null
echo "OK"

View File

@@ -0,0 +1,62 @@
-- URL shortener — MariaDB schema (SPEC docs/specs/20100611_3_url_shortener.md §9)
CREATE DATABASE IF NOT EXISTS url_shortener
CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE url_shortener;
CREATE TABLE IF NOT EXISTS bearer_tokens (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
token_hash CHAR(64) NOT NULL,
label VARCHAR(128) NOT NULL DEFAULT '',
trusted TINYINT(1) NOT NULL DEFAULT 0,
can_temporary TINYINT(1) NOT NULL DEFAULT 1,
can_permanent TINYINT(1) NOT NULL DEFAULT 0,
rate_limit_per_hour INT UNSIGNED NOT NULL DEFAULT 1000,
revoked_at DATETIME NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uq_bearer_token_hash (token_hash)
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS signer_keys (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
key_hash CHAR(64) NOT NULL,
label VARCHAR(128) NOT NULL DEFAULT '',
can_permanent TINYINT(1) NOT NULL DEFAULT 1,
revoked_at DATETIME NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uq_signer_key_hash (key_hash)
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS links (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
bearer_id INT UNSIGNED NOT NULL,
signer_id INT UNSIGNED NULL,
slug VARCHAR(16) NOT NULL,
origin TEXT NOT NULL,
origin_hash CHAR(64) NOT NULL,
ttl_seconds INT UNSIGNED NOT NULL DEFAULT 86400,
expires_at DATETIME NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
accessed_at DATETIME NULL,
access_count BIGINT UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (id),
UNIQUE KEY uq_slug (slug),
KEY idx_bearer_origin (bearer_id, origin_hash),
CONSTRAINT fk_links_bearer FOREIGN KEY (bearer_id) REFERENCES bearer_tokens (id),
CONSTRAINT fk_links_signer FOREIGN KEY (signer_id) REFERENCES signer_keys (id)
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS audit_log (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
event VARCHAR(32) NOT NULL,
bearer_id INT UNSIGNED NULL,
slug VARCHAR(16) NULL,
ip VARCHAR(45) NOT NULL DEFAULT '',
detail TEXT,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY idx_audit_created (created_at)
) ENGINE=InnoDB;

View File

@@ -92,7 +92,7 @@ endobj
endobj
13 0 obj
<<
/Author (Android Cast project) /CreationDate (D:20260521190501+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260521190501+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Author (Android Cast project) /CreationDate (D:20260611185343+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260611185343+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Android Cast \204 Next-gen egress review) /Trapped /False
>>
endobj
@@ -153,7 +153,7 @@ xref
trailer
<<
/ID
[<69103a44ba79ad224f4c4d9612cf06ea><69103a44ba79ad224f4c4d9612cf06ea>]
[<7980b3b39b2662662624213f08f5e4c7><7980b3b39b2662662624213f08f5e4c7>]
% ReportLab generated PDF document -- digest (opensource)
/Info 13 0 R