Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0b70acf39 | ||
|
|
ffd9327e3e | ||
|
|
40b81a5dc8 | ||
|
|
dc5582aa04 | ||
|
|
643ced23e9 | ||
|
|
64eee4ebc5 | ||
|
|
5212f586c7 |
12
.dockerignore
Normal file
12
.dockerignore
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
.git
|
||||||
|
data/
|
||||||
|
scripts/
|
||||||
|
branding/
|
||||||
|
README.md
|
||||||
|
LICENSE
|
||||||
|
nginx-security-headers.conf
|
||||||
|
docker-compose.yml
|
||||||
|
.dockerignore
|
||||||
|
.gitignore
|
||||||
|
app.js
|
||||||
|
i18n.js
|
||||||
98
.github/workflows/docker.yml
vendored
Normal file
98
.github/workflows/docker.yml
vendored
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
name: Build & Push Docker Image
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
env:
|
||||||
|
IMAGE_NAME: xmrpay
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment: DOCKER
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Extract version from tag
|
||||||
|
id: version
|
||||||
|
run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Inject version into source
|
||||||
|
run: |
|
||||||
|
sed -i "s|VERSION = '[^']*'|VERSION = '${{ steps.version.outputs.version }}'|" i18n.js
|
||||||
|
sed -i -E "s|(<span class=\"version\">v)[^<]*(</span>)|\1${{ steps.version.outputs.version }}\2|" index.html
|
||||||
|
|
||||||
|
- name: Minify JS
|
||||||
|
run: |
|
||||||
|
npm i -g terser
|
||||||
|
terser app.js -c -m -o app.min.js
|
||||||
|
terser i18n.js -c -m -o i18n.min.js
|
||||||
|
|
||||||
|
- name: Update SRI hashes
|
||||||
|
run: |
|
||||||
|
sri() { echo "sha384-$(openssl dgst -sha384 -binary "$1" | openssl base64 -A)"; }
|
||||||
|
|
||||||
|
H_STYLE=$(sri style.css)
|
||||||
|
H_QRCODE=$(sri lib/qrcode.min.js)
|
||||||
|
H_I18N=$(sri i18n.min.js)
|
||||||
|
H_JSPDF=$(sri lib/jspdf.min.js)
|
||||||
|
H_CRYPTO=$(sri lib/xmr-crypto.bundle.js)
|
||||||
|
|
||||||
|
# Update dynamic SRI in app.js and re-minify
|
||||||
|
sed -i -E \
|
||||||
|
-e "s|(jspdf\.min\.js.*integrity\s*=\s*')sha384-[A-Za-z0-9+/=]+|\1${H_JSPDF}|" \
|
||||||
|
-e "s|(xmr-crypto\.bundle\.js.*integrity\s*=\s*')sha384-[A-Za-z0-9+/=]+|\1${H_CRYPTO}|" \
|
||||||
|
app.js
|
||||||
|
terser app.js -c -m -o app.min.js
|
||||||
|
H_APP=$(sri app.min.js)
|
||||||
|
|
||||||
|
# Update index.html
|
||||||
|
sed -i -E \
|
||||||
|
-e "s|(style\.css[^\"]*\"\s+integrity=\")sha384-[A-Za-z0-9+/=]+|\1${H_STYLE}|" \
|
||||||
|
-e "s|(qrcode\.min\.js[^\"]*\"\s+integrity=\")sha384-[A-Za-z0-9+/=]+|\1${H_QRCODE}|" \
|
||||||
|
-e "s|(i18n\.min\.js[^\"]*\"\s+integrity=\")sha384-[A-Za-z0-9+/=]+|\1${H_I18N}|" \
|
||||||
|
-e "s|(app\.min\.js[^\"]*\"\s+integrity=\")sha384-[A-Za-z0-9+/=]+|\1${H_APP}|" \
|
||||||
|
index.html
|
||||||
|
|
||||||
|
# Update privacy.html
|
||||||
|
sed -i -E \
|
||||||
|
-e "s|(style\.css[^\"]*\"\s+integrity=\")sha384-[A-Za-z0-9+/=]+|\1${H_STYLE}|" \
|
||||||
|
privacy.html
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
tags: |
|
||||||
|
schmidt1024/${{ env.IMAGE_NAME }}:latest
|
||||||
|
schmidt1024/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }}
|
||||||
|
ghcr.io/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}:latest
|
||||||
|
ghcr.io/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
24
Caddyfile
Normal file
24
Caddyfile
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{$DOMAIN:localhost} {
|
||||||
|
root * /srv
|
||||||
|
encode gzip
|
||||||
|
|
||||||
|
# Security headers
|
||||||
|
header {
|
||||||
|
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||||
|
X-Content-Type-Options "nosniff"
|
||||||
|
X-Frame-Options "DENY"
|
||||||
|
Referrer-Policy "no-referrer"
|
||||||
|
Permissions-Policy "geolocation=(), microphone=(), camera=()"
|
||||||
|
Content-Security-Policy "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self'; form-action 'none'; frame-ancestors 'none'; base-uri 'none'"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Short URL rewrite: /s/CODE -> s.php?c=CODE
|
||||||
|
@shorturl path_regexp short ^/s/([a-zA-Z0-9]+)$
|
||||||
|
rewrite @shorturl /s.php?c={re.short.1}
|
||||||
|
|
||||||
|
# PHP via FPM
|
||||||
|
php_fastcgi 127.0.0.1:9000
|
||||||
|
|
||||||
|
# Static files
|
||||||
|
file_server
|
||||||
|
}
|
||||||
38
Dockerfile
Normal file
38
Dockerfile
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
FROM php:8.3-fpm-alpine AS base
|
||||||
|
|
||||||
|
# Install PHP curl extension (needed for API proxies)
|
||||||
|
RUN apk add --no-cache caddy curl-dev \
|
||||||
|
&& docker-php-ext-install curl \
|
||||||
|
&& rm -rf /var/cache/apk/*
|
||||||
|
|
||||||
|
# PHP-FPM tuning for low-memory VPS
|
||||||
|
RUN { \
|
||||||
|
echo '[www]'; \
|
||||||
|
echo 'pm = ondemand'; \
|
||||||
|
echo 'pm.max_children = 8'; \
|
||||||
|
echo 'pm.process_idle_timeout = 60s'; \
|
||||||
|
} > /usr/local/etc/php-fpm.d/zz-tuning.conf
|
||||||
|
|
||||||
|
# App files
|
||||||
|
COPY index.html privacy.html style.css sw.js favicon.svg s.php /srv/
|
||||||
|
COPY app.min.js /srv/app.min.js
|
||||||
|
COPY i18n.min.js /srv/i18n.min.js
|
||||||
|
COPY api/ /srv/api/
|
||||||
|
COPY lib/ /srv/lib/
|
||||||
|
COPY fonts/ /srv/fonts/
|
||||||
|
|
||||||
|
# Writable data directory
|
||||||
|
RUN mkdir -p /srv/data && chown www-data:www-data /srv/data
|
||||||
|
|
||||||
|
# Caddyfile
|
||||||
|
COPY Caddyfile /etc/caddy/Caddyfile
|
||||||
|
|
||||||
|
# Entrypoint: start PHP-FPM + Caddy
|
||||||
|
COPY docker-entrypoint.sh /usr/local/bin/
|
||||||
|
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
||||||
|
|
||||||
|
EXPOSE 80 443
|
||||||
|
|
||||||
|
VOLUME ["/srv/data", "/data/caddy"]
|
||||||
|
|
||||||
|
ENTRYPOINT ["docker-entrypoint.sh"]
|
||||||
@@ -145,7 +145,7 @@ xmrpay.link/
|
|||||||
## Self-Hosting
|
## Self-Hosting
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://gitea.schmidt.eco/schmidt1024/xmrpay.link.git
|
git clone https://github.com/schmidt1024/xmrpay.git
|
||||||
cd xmrpay.link
|
cd xmrpay.link
|
||||||
# Serve with any web server that supports PHP
|
# Serve with any web server that supports PHP
|
||||||
# No build tools, no npm, no database required
|
# No build tools, no npm, no database required
|
||||||
|
|||||||
@@ -14,13 +14,19 @@ function send_security_headers(): void {
|
|||||||
|
|
||||||
// ── Origin verification ───────────────────────────────────────────────────────
|
// ── Origin verification ───────────────────────────────────────────────────────
|
||||||
function verify_origin(): void {
|
function verify_origin(): void {
|
||||||
$allowed = [
|
|
||||||
'https://xmrpay.link',
|
|
||||||
'http://mc6wfeaqc7oijgdcudrr5zsotmwok3jzk3tu2uezzyjisn7nzzjjizyd.onion',
|
|
||||||
];
|
|
||||||
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
|
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
|
||||||
// Allow same-origin (no Origin header from direct same-origin requests)
|
// Allow same-origin (no Origin header from direct same-origin requests)
|
||||||
if ($origin === '') return;
|
if ($origin === '') return;
|
||||||
|
|
||||||
|
// Dynamically allow the host this instance runs on
|
||||||
|
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
|
||||||
|
$self_origin = $scheme . '://' . ($_SERVER['HTTP_HOST'] ?? '');
|
||||||
|
|
||||||
|
$allowed = [
|
||||||
|
$self_origin,
|
||||||
|
'https://xmrpay.link',
|
||||||
|
'http://mc6wfeaqc7oijgdcudrr5zsotmwok3jzk3tu2uezzyjisn7nzzjjizyd.onion',
|
||||||
|
];
|
||||||
if (!in_array($origin, $allowed, true)) {
|
if (!in_array($origin, $allowed, true)) {
|
||||||
http_response_code(403);
|
http_response_code(403);
|
||||||
echo json_encode(['error' => 'Origin not allowed']);
|
echo json_encode(['error' => 'Origin not allowed']);
|
||||||
|
|||||||
27
docker-compose.yml
Normal file
27
docker-compose.yml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
services:
|
||||||
|
xmrpay:
|
||||||
|
image: ${XMRPAY_IMAGE:-schmidt1024/xmrpay:latest}
|
||||||
|
container_name: xmrpay
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
environment:
|
||||||
|
- DOMAIN=${DOMAIN:-localhost}
|
||||||
|
volumes:
|
||||||
|
- xmrpay-data:/srv/data
|
||||||
|
- caddy-data:/data/caddy
|
||||||
|
|
||||||
|
watchtower:
|
||||||
|
image: containrrr/watchtower
|
||||||
|
container_name: watchtower
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- WATCHTOWER_CLEANUP=true
|
||||||
|
- WATCHTOWER_POLL_INTERVAL=21600
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
xmrpay-data:
|
||||||
|
caddy-data:
|
||||||
8
docker-entrypoint.sh
Normal file
8
docker-entrypoint.sh
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Start PHP-FPM in background
|
||||||
|
php-fpm &
|
||||||
|
|
||||||
|
# Run Caddy in foreground
|
||||||
|
exec caddy run --config /etc/caddy/Caddyfile --adapter caddyfile
|
||||||
2
i18n.js
2
i18n.js
@@ -13,7 +13,7 @@ var I18n = (function () {
|
|||||||
|
|
||||||
var VERSION = '1.0.0';
|
var VERSION = '1.0.0';
|
||||||
|
|
||||||
var footer = 'Open Source · No Tracking · No KYC<br /><a href="https://gitea.schmidt.eco/schmidt1024/xmrpay.link" target="_blank" rel="noopener noreferrer">Source</a> · <a href="http://mc6wfeaqc7oijgdcudrr5zsotmwok3jzk3tu2uezzyjisn7nzzjjizyd.onion" title="Tor Hidden Service">Onion</a> · <a href="/privacy.html">Privacy & Terms</a><br /><span class="version">v' + VERSION + '</span>';
|
var footer = 'Open Source · No Tracking · No KYC<br /><a href="https://github.com/schmidt1024/xmrpay" target="_blank" rel="noopener noreferrer">Source</a> · <a href="http://mc6wfeaqc7oijgdcudrr5zsotmwok3jzk3tu2uezzyjisn7nzzjjizyd.onion" title="Tor Hidden Service">Onion</a> · <a href="/privacy.html">Privacy & Terms</a><br /><span class="version">v' + VERSION + '</span>';
|
||||||
|
|
||||||
var translations = {
|
var translations = {
|
||||||
en: {
|
en: {
|
||||||
|
|||||||
@@ -115,7 +115,7 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<p data-i18n-html="footer">Open Source · No Tracking · No KYC<br /><a href="https://gitea.schmidt.eco/schmidt1024/xmrpay.link" target="_blank" rel="noopener noreferrer">Source</a> · <a href="http://mc6wfeaqc7oijgdcudrr5zsotmwok3jzk3tu2uezzyjisn7nzzjjizyd.onion" title="Tor Hidden Service">Onion</a> · <a href="/privacy.html">Privacy & Terms</a><br /><span class="version">v1.0.0</span></p>
|
<p data-i18n-html="footer">Open Source · No Tracking · No KYC<br /><a href="https://github.com/schmidt1024/xmrpay" target="_blank" rel="noopener noreferrer">Source</a> · <a href="http://mc6wfeaqc7oijgdcudrr5zsotmwok3jzk3tu2uezzyjisn7nzzjjizyd.onion" title="Tor Hidden Service">Onion</a> · <a href="/privacy.html">Privacy & Terms</a><br /><span class="version">v1.0.0</span></p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<div class="lang-picker" id="langPicker">
|
<div class="lang-picker" id="langPicker">
|
||||||
|
|||||||
60
install.sh
Normal file
60
install.sh
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# xmrpay.link — Self-hosting installer
|
||||||
|
# Usage: curl -sL https://xmrpay.link/install.sh | sh -s your-domain.com
|
||||||
|
|
||||||
|
DOMAIN="${1:-}"
|
||||||
|
INSTALL_DIR="/opt/xmrpay"
|
||||||
|
IMAGE="schmidt1024/xmrpay:latest"
|
||||||
|
COMPOSE_URL="https://raw.githubusercontent.com/schmidt1024/xmrpay/master/docker-compose.yml"
|
||||||
|
|
||||||
|
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
info() { printf '\033[1;34m→\033[0m %s\n' "$1"; }
|
||||||
|
ok() { printf '\033[1;32m✓\033[0m %s\n' "$1"; }
|
||||||
|
fail() { printf '\033[1;31m✗\033[0m %s\n' "$1" >&2; exit 1; }
|
||||||
|
|
||||||
|
# ── Preflight ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[ "$(id -u)" -eq 0 ] || fail "Run as root: curl -sL https://xmrpay.link/install.sh | sudo sh -s $DOMAIN"
|
||||||
|
[ -n "$DOMAIN" ] || fail "Usage: curl -sL https://xmrpay.link/install.sh | sh -s YOUR-DOMAIN.COM"
|
||||||
|
|
||||||
|
# ── Install Docker if missing ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
if ! command -v docker >/dev/null 2>&1; then
|
||||||
|
info "Installing Docker..."
|
||||||
|
curl -fsSL https://get.docker.com | sh
|
||||||
|
systemctl enable --now docker
|
||||||
|
ok "Docker installed"
|
||||||
|
else
|
||||||
|
ok "Docker found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Set up xmrpay ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
info "Setting up xmrpay in $INSTALL_DIR..."
|
||||||
|
mkdir -p "$INSTALL_DIR"
|
||||||
|
cd "$INSTALL_DIR"
|
||||||
|
|
||||||
|
curl -fsSL "$COMPOSE_URL" -o docker-compose.yml
|
||||||
|
|
||||||
|
cat > .env <<EOF
|
||||||
|
DOMAIN=$DOMAIN
|
||||||
|
XMRPAY_IMAGE=$IMAGE
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# ── Start ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
info "Starting xmrpay..."
|
||||||
|
docker compose pull
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
ok "xmrpay is running!"
|
||||||
|
echo ""
|
||||||
|
echo " https://$DOMAIN"
|
||||||
|
echo ""
|
||||||
|
echo " Watchtower checks for updates every 6 hours."
|
||||||
|
echo " Data stored in Docker volume: xmrpay-data"
|
||||||
|
echo " Config: $INSTALL_DIR/.env"
|
||||||
|
echo ""
|
||||||
@@ -198,7 +198,7 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<p data-i18n-html="footer">Open Source · No Tracking · No KYC · <a href="https://gitea.schmidt.eco/schmidt1024/xmrpay.link" target="_blank" rel="noopener noreferrer">Source</a> · <a href="http://mc6wfeaqc7oijgdcudrr5zsotmwok3jzk3tu2uezzyjisn7nzzjjizyd.onion" title="Tor Hidden Service">Onion</a> · <a href="/privacy.html">Privacy & Terms</a></p>
|
<p data-i18n-html="footer">Open Source · No Tracking · No KYC · <a href="https://github.com/schmidt1024/xmrpay" target="_blank" rel="noopener noreferrer">Source</a> · <a href="http://mc6wfeaqc7oijgdcudrr5zsotmwok3jzk3tu2uezzyjisn7nzzjjizyd.onion" title="Tor Hidden Service">Onion</a> · <a href="/privacy.html">Privacy & Terms</a></p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<div class="lang-picker" id="langPicker">
|
<div class="lang-picker" id="langPicker">
|
||||||
|
|||||||
2
s.php
2
s.php
@@ -1,5 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
$pathInfo = isset($_SERVER['PATH_INFO']) && is_string($_SERVER['PATH_INFO']) ? $_SERVER['PATH_INFO'] : null;
|
$pathInfo = isset($_SERVER['PATH_INFO']) && is_string($_SERVER['PATH_INFO']) && $_SERVER['PATH_INFO'] !== '' ? $_SERVER['PATH_INFO'] : null;
|
||||||
$queryCode = isset($_GET['c']) && is_string($_GET['c']) ? $_GET['c'] : '';
|
$queryCode = isset($_GET['c']) && is_string($_GET['c']) ? $_GET['c'] : '';
|
||||||
$code = trim($pathInfo ?? $queryCode, '/');
|
$code = trim($pathInfo ?? $queryCode, '/');
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,15 @@ else
|
|||||||
echo "Skipping pre-deploy backup (DEPLOY_BACKUP_ENABLE=0)."
|
echo "Skipping pre-deploy backup (DEPLOY_BACKUP_ENABLE=0)."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ── Inject version from git tags ──────────────────────────────────────────────
|
||||||
|
GIT_VERSION=$(git describe --tags --always 2>/dev/null || echo "dev")
|
||||||
|
# Turn v1.0.0-3-gabc1234 into 1.0.0+3
|
||||||
|
VERSION=$(echo "$GIT_VERSION" | sed -E 's/^v//; s/-([0-9]+)-g[0-9a-f]+$/+\1/')
|
||||||
|
echo "Version: $VERSION"
|
||||||
|
|
||||||
|
sed -i -E "s|VERSION = '[^']*'|VERSION = '${VERSION}'|" i18n.js
|
||||||
|
sed -i -E "s|(<span class=\"version\">v)[^<]*(</span>)|\1${VERSION}\2|" index.html
|
||||||
|
|
||||||
# ── Minify & update SRI hashes ────────────────────────────────────────────────
|
# ── Minify & update SRI hashes ────────────────────────────────────────────────
|
||||||
echo "Minifying JS..."
|
echo "Minifying JS..."
|
||||||
TERSER="${TERSER:-terser}"
|
TERSER="${TERSER:-terser}"
|
||||||
|
|||||||
Reference in New Issue
Block a user