19 Commits

Author SHA1 Message Date
david_bai ffa9f84c4a fix(sendString):Avoid using the file fragmentation parameter to prevent transmission failure 2025-10-11 10:59:42 +08:00
david_bai 1e22481a00 docs(docker): align commands and access guidance with latest deployment model
- Use "bash ./deploy.sh" consistently across docs
- Prefer "docker compose" (Compose v2) and update examples accordingly
- Public mode: recommend "--with-nginx" for same-origin gateway
- Access: document Nginx (same-origin) vs direct ports; update HTTPS endpoints (lan-tls 8443, full 443)
- Health checks: add same-origin /api examples
- Add notes on NEXT_IMAGE_UNOPTIMIZED in Docker and same-origin behavior when --with-nginx is enabled
- Fix bare-metal docs cross-links to Docker guides
2025-10-10 23:25:12 +08:00
david_bai f0c4364dcd fix(config): lan-tls without --enable-web-https must still use same-origin when Nginx is enabled
- generate-config.sh: in lan-tls without HTTPS, set NEXT_PUBLIC_API_URL empty when WITH_NGINX=true so frontend uses relative /api and /socket.io; widen CORS origins to include http://localhost and http://<LOCAL_IP>
- deploy.sh: pass --with-nginx to config generator for consistency
2025-10-10 20:59:20 +08:00
david_bai 8ef43029d5 fix(deploy+docker+frontend): enforce same-origin via Nginx, disable Next Image optimization in Docker, allow Socket.IO polling fallback, and improve health checks and access info
- generate-config.sh: add --with-nginx flag handling; when enabled, set NEXT_PUBLIC_API_URL empty to use same-origin /api and /socket.io; add BACKEND_INTERNAL_URL for SSR/internal fetch; adjust lan-tls HTTPS (8443) and TLS generation policy
- deploy.sh: show only valid access URLs when Nginx is enabled (gateway URLs), avoid misleading :3002/:3001 entries
- frontend (env/webrtc): return mutable transports [websocket,polling]; use empty signaling server for same-origin; comments in English
- frontend (next.config): support NEXT_IMAGE_UNOPTIMIZED to turn off image optimization in Docker
- frontend (health): prefer BACKEND_INTERNAL_URL for internal health checks, fallback to public URL/localhost
- docker-compose + Dockerfile(frontend): pass NEXT_IMAGE_UNOPTIMIZED and BACKEND_INTERNAL_URL envs
2025-10-10 20:49:17 +08:00
david_bai 975f6e74ad docs: clarify LAN TLS (self-signed) usage — import CA in browser, correct HTTPS endpoints (8443), CORS guidance; fix deploy hints to only show reachable Nginx URLs. Also: trust CA in frontend container and align HTTPS port mapping. 2025-10-09 21:46:03 +08:00
david_bai dec59a12ec docs(readme): align Quick Start and Modes with new deployment model 2025-10-09 15:42:11 +08:00
david_bai 8590eda2c2 refactor(scripts): simplify modes and harden cert automation
- New modes: lan-http, lan-tls (self-signed), public, full
- Add flags: --no-sni443, --enable-web-https (lan-tls), --test-renewal
- generate-config: lan-tls HTTPS on 8443 only when explicitly enabled; HSTS only in full; SNI 443 default in full
- detect-environment: remove interactive prompt; adjust public description to 'HTTP + TURN'
- deploy.sh: pass new flags, add certbot scheduler (systemd timer or cron fallback), add dry-run renewal test
- Docs (EN/zh-CN): update quick start, modes overview, LAN TLS guidance, LE auto-issue/renew section
2025-10-08 23:17:19 +08:00
david_bai 1f4522eeb2 docs+scripts: move domain quick tips to docs; add modes overview; clarify full+SNI defaults; add self-signed domain guidance
- deploy.sh: replace verbose public domain test instructions with a single docs link
- docker/scripts/generate-config.sh: remove 'Intranet with TURN quick tip' from help; add docs pointers
- docs(zh/EN): add 'Modes Overview', add 'Private LAN + TURN' quick start example, add 'Domain + Self-signed' and 'Public Domain Quick Test' sections; note LE auto-issue/renew and SNI 443 default in full mode
2025-10-08 23:07:34 +08:00
david_bai 663082efe1 chore(doc): Replace Chinese comments with English comments 2025-10-08 15:59:50 +08:00
david_bai 2bd09835b1 docs(docker): elevate Docker one-click to top, add LE automation + SNI443, update flags and compose v2 commands
- DEPLOYMENT_docker.md/zh-CN: Add top Quick Start (private/public/full), Let’s Encrypt auto issue/renew (webroot, zero downtime), SNI 443 default for full+domain, common flags (--with-sni443, --turn-port-range, --le-email), replace docker-compose with docker compose.
- README.md/zh-CN: Promote Docker one-click section to top and link to docs.
- DEPLOYMENT.md/zh-CN: Add audience/scope notice; point to Docker docs for recommended path.
- ROADMAP.md/zh-CN: Record recently completed (Docker, LE, SNI, TURN).
2025-10-07 22:48:26 +08:00
david_bai 7809373f88 feat(nginx,sni): enable SNI-based 443 multiplexing (turns:443) and wiring
- docker/scripts/generate-config.sh
	- Add --enable-sni443/--no-sni443 flags; default enable in full+domain.
	- Generate Nginx stream{} with ssl_preread SNI routing: turn.<domain> -> coturn:5349; others -> web:8443.
	- When SNI is enabled, serve HTTPS on 8443 (http layer); otherwise keep 443.
- deploy.sh:
	- Add --with-sni443 and propagate to config generation and LE provisioning.
	- No compose changes required; 8443 remains internal.
- Notes:
	- Backward compatible. SNI is auto-enabled for full+domain, can be toggled with flags.
	- Leverages existing LE automation and TURN cert reuse.
2025-10-07 21:51:28 +08:00
david_bai 85baa97804 feat(ssl,deploy): safe cleanup, TURN auto-start, and HTTPS checks
- deploy.sh:
      - copy real cert lineage (handles -0001) into docker/ssl
      - Ensure TURN auto-start when requested; enhance clean (stop→rm, network fallback)
      - Add HTTPS post-deploy health check
      - Build fallback: parallel → serial on failure
      - Early-exit on clean-only mode
2025-10-07 21:22:51 +08:00
david_bai 246eff196e feat(deploy,ssl): automate Let’s Encrypt (webroot), preserve SSL, and auto-enable HTTPS
- generate-config.sh
      - Add flags: --no-clean, --reset-ssl, --ssl-mode (letsencrypt|self-signed|provided)
      - Stop deleting docker/ssl by default; only wipe on explicit --reset-ssl
      - Inject ACME webroot route into HTTP (80) server; create docker/letsencrypt-www
      - Default SSL_MODE: full=letsencrypt, private/public=self-signed
      - Add enable_https_if_cert_present: append 443 server only when server-cert.pem/server-key.pem exist
      - Keep self-signed path generating HTTPS immediately (non-basic)
  - docker-compose.yml
      - Mount ./docker/letsencrypt-www:/var/www/certbot:ro for Nginx ACME challenges
  - deploy.sh
      - Add --le-email for Let’s Encrypt account email
      - Auto-install certbot once (apt-get) and enable systemd timer if available
      - Install deploy hook at /etc/letsencrypt/renewal-hooks/deploy/privydrop-reload.sh to:
          - Copy renewed certs into docker/ssl
          - Hot-reload Nginx; HUP or restart coturn
      - First-time issuance (webroot) for <domain> and turn.<domain> after Nginx:80 is up; copy certs
      - Re-run generate-config with --no-clean --ssl-mode letsencrypt to enable 443, then reload Nginx
  - Behavior changes
      - Full mode prefers Let’s Encrypt by default; HTTPS gets enabled as soon as certs exist
      - docker/ssl is no longer wiped by config generation
  - Notes
      - SNI-based turns:443 is not implemented yet (planned)
      - Backward compatible with private/public (self-signed)
2025-10-05 12:43:56 +08:00
david_bai a498cc4799 chore(deploy): public output polish and public/full config fixes
- deploy.sh: show public endpoints (domain/public IP only); add TURN info (domain/public IP); prepend logs chmod 777; append HTTPS+Nginx quick-test tips.
- generate-config.sh: fix public/full CORS and NEXT_PUBLIC_API_URL; prefer PUBLIC_IP for TURN host when no domain; update help text.
2025-10-02 22:45:42 +08:00
david_bai 200fc65617 build(docker): Intranet deployment is successfully tested using turn
- Switch all CLI examples to Docker Compose V2 (docker compose) for consistency.
  - Add explicit instruction to grant write permissions to the host logs/ directory (chmod 777 -R logs) to fix coturn/nginx bind-mount logging errors.
  - Parameterize TURN UDP port range via TURN_MIN_PORT/TURN_MAX_PORT and set a safer default 49152-49252 to reduce startup/cleanup overhead and port
  conflicts.
  - Update troubleshooting with coturn log write failure guidance and port conflict hints.
  - Clarify that LAN IP is auto-detected in private mode; --local-ip is no longer needed by default but remains as an override for edge cases.
2025-09-30 14:01:30 +08:00
david_bai 2ee6961634 build(docker): Private mode deployment test successful
Test steps:
bash docker/scripts/generate-config.sh --mode private [--local-ip 192.168.0.113]
bash ./deploy.sh --mode private

Front-end directly inlines NEXT_PUBLIC_API_URL, directly connecting to the backend.
CORS (production) supports comma-separated multiple origins, with localhost and local network IPs included by default.
2025-09-29 18:27:12 +08:00
david_bai cfcd60145a build: refresh docker deployment workflow 2025-09-26 14:02:55 +08:00
david_bai 67b46d0b30 Merge branch 'fea/docker2' into fea/docker 2025-09-21 22:10:30 +08:00
david_bai 158433bb0b chore:Initial addition of Docker related content 2025-09-11 06:46:04 +08:00
38 changed files with 5247 additions and 222 deletions
+56
View File
@@ -0,0 +1,56 @@
# Git相关
.git
.gitignore
# 环境变量文件
.env*
!.env.docker.example
# 日志文件
logs/
*.log
# 依赖目录
node_modules/
frontend/node_modules/
backend/node_modules/
# 构建目录
.next/
dist/
build/
# 缓存目录
.npm
.pnpm-store
# 临时文件
*.tmp
*.temp
# 编辑器配置
.vscode/
.idea/
*.swp
*.swo
# 操作系统文件
.DS_Store
Thumbs.db
# Docker相关
Dockerfile
.dockerignore
docker-compose*.yml
# 文档文件
*.md
docs/
# 测试文件
coverage/
.nyc_output/
# 其他
.eslintcache
tsconfig.tsbuildinfo
+7 -1
View File
@@ -60,6 +60,12 @@ next-env.d.ts
# Build output
dist/
# Generated docker assets
docker/ssl/
docker/nginx/
docker/coturn/
logs/
# Temporary files
.temp/
.tmp/
.tmp/
+33 -1
View File
@@ -35,7 +35,39 @@ We believe everyone should have control over their own data. PrivyDrop was creat
- **Backend**: Node.js, Express.js, TypeScript
- **Real-time Communication**: WebRTC, Socket.IO
- **Data Storage**: Redis
- **Deployment**: PM2, Nginx, Docker [WIP]
- **Deployment**: PM2, Nginx, Docker
## 🐳 Docker One-Click Deployment (Recommended)
Deploy in minutes with zero manual configuration. Supports private/public networks and auto HTTPS (Lets Encrypt).
```bash
# Private LAN (no domain/public IP)
bash ./deploy.sh --mode lan-http
# Private LAN + TURN (for complex NAT/LAN)
bash ./deploy.sh --mode lan-http --with-turn
# LAN HTTPS (self-signed; dev/managed env; explicitly enable 8443)
bash ./deploy.sh --mode lan-tls --enable-web-https --with-nginx
# Public IP without domain (with TURN; recommended with Nginx for same-origin)
bash ./deploy.sh --mode public --with-turn --with-nginx
# Public domain (HTTPS + Nginx + TURN + SNI 443, auto-issue/renew)
bash ./deploy.sh --mode full --domain your-domain.com --with-nginx --with-turn --le-email you@domain.com
```
See [Docker Deployment Guide](./docs/DEPLOYMENT_docker.md) (Modes Overview, LAN TLS limitations, Lets Encrypt auto-issue/renew)
Heads-up (LAN TLS, self-signed)
- Import the CA certificate into your browser (or system trust store) on first use: `docker/ssl/ca-cert.pem`. Otherwise the browser shows “certificate not valid/untrusted”.
- Access endpoints (by default):
- Nginx: `http://localhost`
- HTTPS: `https://localhost:8443`, `https://<your LAN IP>:8443`
- Frontend dev ports (optional): `http://localhost:3002`, `http://<your LAN IP>:3002`
- When `--with-nginx` is enabled, the frontend and API are same-origin (`/api`, `/socket.io`) for stability; direct ports `:3002/:3001` are for debugging only and may cause CORS or 404.
- With `--enable-web-https` and the CA trusted, same-origin HTTPS (8443) avoids CORS; common dev origins (`localhost`, `:3002`) are allowed by default.
## 🚀 Quick Start (Full-Stack Local Development)
+45 -2
View File
@@ -35,9 +35,52 @@ PrivyDrop (原 SecureShare) 是一个基于 WebRTC 的开源点对点(P2P)
- **后端**: Node.js, Express.js, TypeScript
- **实时通信**: WebRTC, Socket.IO
- **数据存储**: Redis
- **部署**: PM2, Nginx, Docker[暂未支持]
- **部署**: PM2, Nginx, Docker
## 🚀 快速上手 (本地全栈开发)
## 🚀 快速上手
### 🐳 Docker 一键部署 (推荐)
**零配置,一条命令完成部署!支持内网/公网/域名,自动签发/续期 HTTPS。**
```bash
# 内网(无域名/无公网IP
bash ./deploy.sh --mode lan-http
# 内网 + TURN(推荐用于复杂内网/NAT)
bash ./deploy.sh --mode lan-http --with-turn
# 内网 HTTPS(自签,开发/受管环境,显式开启 8443)
bash ./deploy.sh --mode lan-tls --enable-web-https --with-nginx
# 公网IP(无域名),含 TURN(推荐同源经 Nginx)
bash ./deploy.sh --mode public --with-turn --with-nginx
# 公网域名(HTTPS + Nginx + TURN + SNI 443 分流,自动申请/续期证书)
bash ./deploy.sh --mode full --domain your-domain.com --with-nginx --with-turn --le-email you@domain.com
```
完整说明见: docs/DEPLOYMENT_docker.zh-CN.md(模式一览、LAN TLS、自签限制、Lets Encrypt 自动签发/续期)
提示(lan-tls 自签 HTTPS
- 首次访问需导入 CA 证书:`docker/ssl/ca-cert.pem` 到浏览器(或系统信任),否则浏览器会提示“证书无效/不受信任”。
- 访问方式(默认):
- Nginx: `http://localhost`
- HTTPS: `https://localhost:8443``https://<你的局域网IP>:8443`
- 前端开发口(可选): `http://localhost:3002``http://<你的局域网IP>:3002`
- 启用 `--with-nginx` 时,前端与 API 同源(/api、/socket.io)更稳定;直连 `:3002/:3001` 仅用于调试,可能导致 CORS 或 404。
- 若启用了 `--enable-web-https` 并导入 CA,浏览器与 API 走同源 HTTPS(8443)可避免 CORS;我们已默认放开常见开发来源(`localhost``:3002` 等)。
**部署优势**:
- ✅ 部署时间: 60 分钟 → 5 分钟
- ✅ 技术门槛: Linux 运维 → 会用 Docker 即可
- ✅ 环境要求: 公网 IP → 内网即可使用
- ✅ 成功率: 70% → 95%+
详见: [Docker 部署指南](./docs/DEPLOYMENT_docker.zh-CN.md)
### 💻 本地开发环境
在开始之前,请确保你的开发环境已安装 [Node.js](https://nodejs.org/) (v18+), [npm](https://www.npmjs.com/) 以及一个正在运行的 [Redis](https://redis.io/) 实例。
+11 -1
View File
@@ -8,12 +8,22 @@ This roadmap is a living document. We welcome community feedback and contributio
## ✅ Completed
### Architecture optimization
- **Core Architecture Refactor (Q3 2025)**: Successfully refactored the entire frontend codebase to a modern, layered architecture.
- Implemented a framework-agnostic **Service Layer** (`webrtcService`) to encapsulate all WebRTC and business logic.
- Introduced **Zustand** for centralized, predictable state management (`fileTransferStore`).
- Decoupled UI components from business logic, establishing a clear, unidirectional data flow.
- **Resumable File Transfers (Q3 2025):** Implemented robust logic for resuming transfers from the point of interruption. This is enabled by setting a save directory, which allows the receiver to check for partially downloaded files and request only the missing chunks.
### Deployment and Operation
- Docker one-click deployment (Q4 20252)
- Unified container health checks (node health-check.js)
- Lets Encrypt automation (webroot) with zero-downtime renewals and deploy-hook
- TURN improvements (env port range; default 49152-49252)
- SNI 443 multiplexing (turns:443 via Nginx stream; enabled by default in full+domain)
---
## Short-Term Goals (Next 1-3 Months)
@@ -60,4 +70,4 @@ Your contributions are vital to making this roadmap a reality!
2. **Start a Discussion:** If you're interested in a roadmap item, start a discussion to share your ideas.
3. **Submit a PR:** Fork the repo, create a feature branch, and submit a Pull Request.
Thank you for being part of the PrivyDrop community! Let's build the future of private sharing, together.
Thank you for being part of the PrivyDrop community! Let's build the future of private sharing, together.
+13 -3
View File
@@ -8,11 +8,21 @@
## ✅ 已完成
- **核心架构重构 (2025年Q3)**: 成功地将整个前端代码库重构为现代化的分层架构。
### 架构优化
- **核心架构重构 (2025 年 Q3)**: 成功地将整个前端代码库重构为现代化的分层架构。
- 实现了一个与框架无关的**服务层** (`webrtcService`),用于封装所有 WebRTC 和业务逻辑。
- 引入 **Zustand** (`fileTransferStore`) 进行中心化的、可预测的状态管理。
- 将 UI 组件与业务逻辑解耦,建立了清晰的单向数据流。
- **文件断点续传 (2025Q3):** 实现了稳健的断点续传逻辑。通过设置保存目录,接收方能够检查已部分下载的文件,并仅请求缺失的数据块,极大地提升了大文件和不稳定网络下的传输成功率。
- **文件断点续传 (2025Q3):** 实现了稳健的断点续传逻辑。通过设置保存目录,接收方能够检查已部分下载的文件,并仅请求缺失的数据块,极大地提升了大文件和不稳定网络下的传输成功率。
### 部署与运维
- Docker 一键部署(2025 年 Q4
- 容器健康检查统一(node health-check.js
- Lets Encryptwebroot)自动化与续期 deploy-hook(无停机)
- TURN 端口段变量化与默认缩小(49152-49252
- SNI 443 分流(Nginx streamfull+domain 默认开启)
---
@@ -61,4 +71,4 @@
2. **发起讨论:** 如果你对路线图中某个项目感兴趣,欢迎发起一个讨论来分享你的想法。
3. **提交代码:** Fork 仓库,创建你的功能分支,然后提交 Pull Request。
感谢你成为 PrivyDrop 社区的一员!让我们一起共创私人分享的未来。
感谢你成为 PrivyDrop 社区的一员!让我们一起共创私人分享的未来。
+13
View File
@@ -0,0 +1,13 @@
node_modules
npm-debug.log*
.npm
.env*
.git
.gitignore
README.md
Dockerfile
.dockerignore
coverage
.nyc_output
logs
*.log
+41
View File
@@ -0,0 +1,41 @@
# Build stage
FROM node:18-alpine AS builder
ARG HTTP_PROXY
ARG HTTPS_PROXY
ARG NO_PROXY
ENV http_proxy ${HTTP_PROXY} \
https_proxy ${HTTPS_PROXY} \
no_proxy ${NO_PROXY}
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Run stage
FROM node:18-alpine AS runtime
WORKDIR /app
# Copy prebuilt artifacts
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
COPY health-check.js ./
# Create user and set permissions
RUN addgroup -g 1001 -S nodejs && \
adduser -S backend -u 1001 -G nodejs && \
chown -R backend:nodejs /app
USER backend
EXPOSE 3001
# Use a Node.js script for health checks (instead of curl)
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD node health-check.js
CMD ["node", "dist/server.js"]
+2 -2
View File
@@ -88,7 +88,7 @@ server {
}
# Next.js Image Optimization Service (usually handled by the Next.js application)
location /_next/image {
proxy_pass http://localhost:3000; # Point to the Next.js application
proxy_pass http://localhost:3002; # Point to the Next.js application
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
@@ -110,7 +110,7 @@ server {
}
# Named location, used to proxy requests to the Next.js application
location @nextjs_app {
proxy_pass http://localhost:3000; # Point to the Next.js application
proxy_pass http://localhost:3002; # Point to the Next.js application
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env node
const http = require('http');
const options = {
host: 'localhost',
port: process.env.BACKEND_PORT || 3001,
path: '/health',
timeout: 2000,
method: 'GET'
};
const req = http.request(options, (res) => {
if (res.statusCode === 200) {
console.log('Health check passed');
process.exit(0);
} else {
console.log(`Health check failed with status: ${res.statusCode}`);
process.exit(1);
}
});
req.on('error', (err) => {
console.log(`Health check failed: ${err.message}`);
process.exit(1);
});
req.on('timeout', () => {
console.log('Health check timeout');
req.destroy();
process.exit(1);
});
req.end();
+156 -156
View File
@@ -19,7 +19,7 @@ importers:
version: 4.21.2
ioredis:
specifier: ^5.4.1
version: 5.7.0
version: 5.8.0
socket.io:
specifier: ^4.8.1
version: 4.8.1
@@ -32,7 +32,7 @@ importers:
version: 5.0.3
'@types/node':
specifier: ^22.13.4
version: 22.17.2
version: 22.18.6
cross-env:
specifier: ^7.0.3
version: 7.0.3
@@ -41,10 +41,10 @@ importers:
version: 6.0.1
ts-node:
specifier: ^10.9.2
version: 10.9.2(@types/node@22.17.2)(typescript@5.9.2)
version: 10.9.2(@types/node@22.18.6)(typescript@5.9.2)
tsx:
specifier: ^4.19.2
version: 4.20.4
version: 4.20.5
typescript:
specifier: ^5.7.3
version: 5.9.2
@@ -55,164 +55,164 @@ packages:
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'}
'@esbuild/aix-ppc64@0.25.9':
resolution: {integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==}
'@esbuild/aix-ppc64@0.25.10':
resolution: {integrity: sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.25.9':
resolution: {integrity: sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==}
'@esbuild/android-arm64@0.25.10':
resolution: {integrity: sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.25.9':
resolution: {integrity: sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==}
'@esbuild/android-arm@0.25.10':
resolution: {integrity: sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.25.9':
resolution: {integrity: sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==}
'@esbuild/android-x64@0.25.10':
resolution: {integrity: sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.25.9':
resolution: {integrity: sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==}
'@esbuild/darwin-arm64@0.25.10':
resolution: {integrity: sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.25.9':
resolution: {integrity: sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==}
'@esbuild/darwin-x64@0.25.10':
resolution: {integrity: sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.25.9':
resolution: {integrity: sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==}
'@esbuild/freebsd-arm64@0.25.10':
resolution: {integrity: sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.25.9':
resolution: {integrity: sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==}
'@esbuild/freebsd-x64@0.25.10':
resolution: {integrity: sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.25.9':
resolution: {integrity: sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==}
'@esbuild/linux-arm64@0.25.10':
resolution: {integrity: sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.25.9':
resolution: {integrity: sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==}
'@esbuild/linux-arm@0.25.10':
resolution: {integrity: sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.25.9':
resolution: {integrity: sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==}
'@esbuild/linux-ia32@0.25.10':
resolution: {integrity: sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.25.9':
resolution: {integrity: sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==}
'@esbuild/linux-loong64@0.25.10':
resolution: {integrity: sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.25.9':
resolution: {integrity: sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==}
'@esbuild/linux-mips64el@0.25.10':
resolution: {integrity: sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.25.9':
resolution: {integrity: sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==}
'@esbuild/linux-ppc64@0.25.10':
resolution: {integrity: sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.25.9':
resolution: {integrity: sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==}
'@esbuild/linux-riscv64@0.25.10':
resolution: {integrity: sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.25.9':
resolution: {integrity: sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==}
'@esbuild/linux-s390x@0.25.10':
resolution: {integrity: sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.25.9':
resolution: {integrity: sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==}
'@esbuild/linux-x64@0.25.10':
resolution: {integrity: sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-arm64@0.25.9':
resolution: {integrity: sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==}
'@esbuild/netbsd-arm64@0.25.10':
resolution: {integrity: sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-x64@0.25.9':
resolution: {integrity: sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==}
'@esbuild/netbsd-x64@0.25.10':
resolution: {integrity: sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-arm64@0.25.9':
resolution: {integrity: sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==}
'@esbuild/openbsd-arm64@0.25.10':
resolution: {integrity: sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-x64@0.25.9':
resolution: {integrity: sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==}
'@esbuild/openbsd-x64@0.25.10':
resolution: {integrity: sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/openharmony-arm64@0.25.9':
resolution: {integrity: sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==}
'@esbuild/openharmony-arm64@0.25.10':
resolution: {integrity: sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openharmony]
'@esbuild/sunos-x64@0.25.9':
resolution: {integrity: sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==}
'@esbuild/sunos-x64@0.25.10':
resolution: {integrity: sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.25.9':
resolution: {integrity: sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==}
'@esbuild/win32-arm64@0.25.10':
resolution: {integrity: sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.25.9':
resolution: {integrity: sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==}
'@esbuild/win32-ia32@0.25.10':
resolution: {integrity: sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.25.9':
resolution: {integrity: sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==}
'@esbuild/win32-x64@0.25.10':
resolution: {integrity: sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
'@ioredis/commands@1.3.0':
resolution: {integrity: sha512-M/T6Zewn7sDaBQEqIZ8Rb+i9y8qfGmq+5SDFSf9sA2lUZTmdDLVdOiQaeDp+Q4wElZ9HG1GAX5KhDaidp6LQsQ==}
'@ioredis/commands@1.4.0':
resolution: {integrity: sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==}
'@isaacs/balanced-match@4.0.1':
resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
@@ -272,8 +272,8 @@ packages:
'@types/mime@1.3.5':
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
'@types/node@22.17.2':
resolution: {integrity: sha512-gL6z5N9Jm9mhY+U2KXZpteb+09zyffliRkZyZOHODGATyC5B1Jt/7TzuuiLkFsSUMLbS1OLmlj/E+/3KF4Q/4w==}
'@types/node@22.18.6':
resolution: {integrity: sha512-r8uszLPpeIWbNKtvWRt/DbVi5zbqZyj1PTmhRMqBMvDnaz1QpmSKujUtJLrqGZeoM8v72MfYggDceY4K1itzWQ==}
'@types/qs@6.14.0':
resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==}
@@ -304,16 +304,16 @@ packages:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
ansi-regex@6.2.0:
resolution: {integrity: sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==}
ansi-regex@6.2.2:
resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
engines: {node: '>=12'}
ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
ansi-styles@6.2.1:
resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
ansi-styles@6.2.3:
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
engines: {node: '>=12'}
arg@4.1.3:
@@ -405,8 +405,8 @@ packages:
supports-color:
optional: true
debug@4.4.1:
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
@@ -478,8 +478,8 @@ packages:
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
engines: {node: '>= 0.4'}
esbuild@0.25.9:
resolution: {integrity: sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==}
esbuild@0.25.10:
resolution: {integrity: sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==}
engines: {node: '>=18'}
hasBin: true
@@ -557,8 +557,8 @@ packages:
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
ioredis@5.7.0:
resolution: {integrity: sha512-NUcA93i1lukyXU+riqEyPtSEkyFq8tX90uL659J+qpCZ3rEdViB/APC58oAhIh3+bJln2hzdlZbBZsGNrlsR8g==}
ioredis@5.8.0:
resolution: {integrity: sha512-AUXbKn9gvo9hHKvk6LbZJQSKn/qIfkWXrnsyL9Yrf+oeXmla9Nmf6XEumOddyhM8neynpK5oAV6r9r99KBuwzA==}
engines: {node: '>=12.22.0'}
ipaddr.js@1.9.1:
@@ -582,8 +582,8 @@ packages:
lodash.isarguments@3.1.0:
resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==}
lru-cache@11.1.0:
resolution: {integrity: sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==}
lru-cache@11.2.1:
resolution: {integrity: sha512-r8LA6i4LP4EeWOhqBaZZjDWwehd1xUJPCJd9Sv300H0ZmcUER4+JPh7bqqZeqs1o5pgtgvXm+d9UGrB5zZGDiQ==}
engines: {node: 20 || >=22}
make-error@1.3.6:
@@ -772,8 +772,8 @@ packages:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
strip-ansi@7.1.0:
resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
strip-ansi@7.1.2:
resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==}
engines: {node: '>=12'}
toidentifier@1.0.1:
@@ -794,8 +794,8 @@ packages:
'@swc/wasm':
optional: true
tsx@4.20.4:
resolution: {integrity: sha512-yyxBKfORQ7LuRt/BQKBXrpcq59ZvSW0XxwfjAt3w2/8PmdxaFzijtMhTawprSHhpzeM5BgU2hXHG3lklIERZXg==}
tsx@4.20.5:
resolution: {integrity: sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==}
engines: {node: '>=18.0.0'}
hasBin: true
@@ -861,85 +861,85 @@ snapshots:
dependencies:
'@jridgewell/trace-mapping': 0.3.9
'@esbuild/aix-ppc64@0.25.9':
'@esbuild/aix-ppc64@0.25.10':
optional: true
'@esbuild/android-arm64@0.25.9':
'@esbuild/android-arm64@0.25.10':
optional: true
'@esbuild/android-arm@0.25.9':
'@esbuild/android-arm@0.25.10':
optional: true
'@esbuild/android-x64@0.25.9':
'@esbuild/android-x64@0.25.10':
optional: true
'@esbuild/darwin-arm64@0.25.9':
'@esbuild/darwin-arm64@0.25.10':
optional: true
'@esbuild/darwin-x64@0.25.9':
'@esbuild/darwin-x64@0.25.10':
optional: true
'@esbuild/freebsd-arm64@0.25.9':
'@esbuild/freebsd-arm64@0.25.10':
optional: true
'@esbuild/freebsd-x64@0.25.9':
'@esbuild/freebsd-x64@0.25.10':
optional: true
'@esbuild/linux-arm64@0.25.9':
'@esbuild/linux-arm64@0.25.10':
optional: true
'@esbuild/linux-arm@0.25.9':
'@esbuild/linux-arm@0.25.10':
optional: true
'@esbuild/linux-ia32@0.25.9':
'@esbuild/linux-ia32@0.25.10':
optional: true
'@esbuild/linux-loong64@0.25.9':
'@esbuild/linux-loong64@0.25.10':
optional: true
'@esbuild/linux-mips64el@0.25.9':
'@esbuild/linux-mips64el@0.25.10':
optional: true
'@esbuild/linux-ppc64@0.25.9':
'@esbuild/linux-ppc64@0.25.10':
optional: true
'@esbuild/linux-riscv64@0.25.9':
'@esbuild/linux-riscv64@0.25.10':
optional: true
'@esbuild/linux-s390x@0.25.9':
'@esbuild/linux-s390x@0.25.10':
optional: true
'@esbuild/linux-x64@0.25.9':
'@esbuild/linux-x64@0.25.10':
optional: true
'@esbuild/netbsd-arm64@0.25.9':
'@esbuild/netbsd-arm64@0.25.10':
optional: true
'@esbuild/netbsd-x64@0.25.9':
'@esbuild/netbsd-x64@0.25.10':
optional: true
'@esbuild/openbsd-arm64@0.25.9':
'@esbuild/openbsd-arm64@0.25.10':
optional: true
'@esbuild/openbsd-x64@0.25.9':
'@esbuild/openbsd-x64@0.25.10':
optional: true
'@esbuild/openharmony-arm64@0.25.9':
'@esbuild/openharmony-arm64@0.25.10':
optional: true
'@esbuild/sunos-x64@0.25.9':
'@esbuild/sunos-x64@0.25.10':
optional: true
'@esbuild/win32-arm64@0.25.9':
'@esbuild/win32-arm64@0.25.10':
optional: true
'@esbuild/win32-ia32@0.25.9':
'@esbuild/win32-ia32@0.25.10':
optional: true
'@esbuild/win32-x64@0.25.9':
'@esbuild/win32-x64@0.25.10':
optional: true
'@ioredis/commands@1.3.0': {}
'@ioredis/commands@1.4.0': {}
'@isaacs/balanced-match@4.0.1': {}
@@ -951,7 +951,7 @@ snapshots:
dependencies:
string-width: 5.1.2
string-width-cjs: string-width@4.2.3
strip-ansi: 7.1.0
strip-ansi: 7.1.2
strip-ansi-cjs: strip-ansi@6.0.1
wrap-ansi: 8.1.0
wrap-ansi-cjs: wrap-ansi@7.0.0
@@ -978,19 +978,19 @@ snapshots:
'@types/body-parser@1.19.6':
dependencies:
'@types/connect': 3.4.38
'@types/node': 22.17.2
'@types/node': 22.18.6
'@types/connect@3.4.38':
dependencies:
'@types/node': 22.17.2
'@types/node': 22.18.6
'@types/cors@2.8.19':
dependencies:
'@types/node': 22.17.2
'@types/node': 22.18.6
'@types/express-serve-static-core@5.0.7':
dependencies:
'@types/node': 22.17.2
'@types/node': 22.18.6
'@types/qs': 6.14.0
'@types/range-parser': 1.2.7
'@types/send': 0.17.5
@@ -1005,7 +1005,7 @@ snapshots:
'@types/mime@1.3.5': {}
'@types/node@22.17.2':
'@types/node@22.18.6':
dependencies:
undici-types: 6.21.0
@@ -1016,12 +1016,12 @@ snapshots:
'@types/send@0.17.5':
dependencies:
'@types/mime': 1.3.5
'@types/node': 22.17.2
'@types/node': 22.18.6
'@types/serve-static@1.15.8':
dependencies:
'@types/http-errors': 2.0.5
'@types/node': 22.17.2
'@types/node': 22.18.6
'@types/send': 0.17.5
accepts@1.3.8:
@@ -1037,13 +1037,13 @@ snapshots:
ansi-regex@5.0.1: {}
ansi-regex@6.2.0: {}
ansi-regex@6.2.2: {}
ansi-styles@4.3.0:
dependencies:
color-convert: 2.0.1
ansi-styles@6.2.1: {}
ansi-styles@6.2.3: {}
arg@4.1.3: {}
@@ -1125,7 +1125,7 @@ snapshots:
dependencies:
ms: 2.1.3
debug@4.4.1:
debug@4.4.3:
dependencies:
ms: 2.1.3
@@ -1162,7 +1162,7 @@ snapshots:
engine.io@6.6.4:
dependencies:
'@types/cors': 2.8.19
'@types/node': 22.17.2
'@types/node': 22.18.6
accepts: 1.3.8
base64id: 2.0.0
cookie: 0.7.2
@@ -1183,34 +1183,34 @@ snapshots:
dependencies:
es-errors: 1.3.0
esbuild@0.25.9:
esbuild@0.25.10:
optionalDependencies:
'@esbuild/aix-ppc64': 0.25.9
'@esbuild/android-arm': 0.25.9
'@esbuild/android-arm64': 0.25.9
'@esbuild/android-x64': 0.25.9
'@esbuild/darwin-arm64': 0.25.9
'@esbuild/darwin-x64': 0.25.9
'@esbuild/freebsd-arm64': 0.25.9
'@esbuild/freebsd-x64': 0.25.9
'@esbuild/linux-arm': 0.25.9
'@esbuild/linux-arm64': 0.25.9
'@esbuild/linux-ia32': 0.25.9
'@esbuild/linux-loong64': 0.25.9
'@esbuild/linux-mips64el': 0.25.9
'@esbuild/linux-ppc64': 0.25.9
'@esbuild/linux-riscv64': 0.25.9
'@esbuild/linux-s390x': 0.25.9
'@esbuild/linux-x64': 0.25.9
'@esbuild/netbsd-arm64': 0.25.9
'@esbuild/netbsd-x64': 0.25.9
'@esbuild/openbsd-arm64': 0.25.9
'@esbuild/openbsd-x64': 0.25.9
'@esbuild/openharmony-arm64': 0.25.9
'@esbuild/sunos-x64': 0.25.9
'@esbuild/win32-arm64': 0.25.9
'@esbuild/win32-ia32': 0.25.9
'@esbuild/win32-x64': 0.25.9
'@esbuild/aix-ppc64': 0.25.10
'@esbuild/android-arm': 0.25.10
'@esbuild/android-arm64': 0.25.10
'@esbuild/android-x64': 0.25.10
'@esbuild/darwin-arm64': 0.25.10
'@esbuild/darwin-x64': 0.25.10
'@esbuild/freebsd-arm64': 0.25.10
'@esbuild/freebsd-x64': 0.25.10
'@esbuild/linux-arm': 0.25.10
'@esbuild/linux-arm64': 0.25.10
'@esbuild/linux-ia32': 0.25.10
'@esbuild/linux-loong64': 0.25.10
'@esbuild/linux-mips64el': 0.25.10
'@esbuild/linux-ppc64': 0.25.10
'@esbuild/linux-riscv64': 0.25.10
'@esbuild/linux-s390x': 0.25.10
'@esbuild/linux-x64': 0.25.10
'@esbuild/netbsd-arm64': 0.25.10
'@esbuild/netbsd-x64': 0.25.10
'@esbuild/openbsd-arm64': 0.25.10
'@esbuild/openbsd-x64': 0.25.10
'@esbuild/openharmony-arm64': 0.25.10
'@esbuild/sunos-x64': 0.25.10
'@esbuild/win32-arm64': 0.25.10
'@esbuild/win32-ia32': 0.25.10
'@esbuild/win32-x64': 0.25.10
escape-html@1.0.3: {}
@@ -1331,11 +1331,11 @@ snapshots:
inherits@2.0.4: {}
ioredis@5.7.0:
ioredis@5.8.0:
dependencies:
'@ioredis/commands': 1.3.0
'@ioredis/commands': 1.4.0
cluster-key-slot: 1.1.2
debug: 4.4.1
debug: 4.4.3
denque: 2.1.0
lodash.defaults: 4.2.0
lodash.isarguments: 3.1.0
@@ -1359,7 +1359,7 @@ snapshots:
lodash.isarguments@3.1.0: {}
lru-cache@11.1.0: {}
lru-cache@11.2.1: {}
make-error@1.3.6: {}
@@ -1407,7 +1407,7 @@ snapshots:
path-scurry@2.0.0:
dependencies:
lru-cache: 11.1.0
lru-cache: 11.2.1
minipass: 7.1.2
path-to-regexp@0.1.12: {}
@@ -1556,26 +1556,26 @@ snapshots:
dependencies:
eastasianwidth: 0.2.0
emoji-regex: 9.2.2
strip-ansi: 7.1.0
strip-ansi: 7.1.2
strip-ansi@6.0.1:
dependencies:
ansi-regex: 5.0.1
strip-ansi@7.1.0:
strip-ansi@7.1.2:
dependencies:
ansi-regex: 6.2.0
ansi-regex: 6.2.2
toidentifier@1.0.1: {}
ts-node@10.9.2(@types/node@22.17.2)(typescript@5.9.2):
ts-node@10.9.2(@types/node@22.18.6)(typescript@5.9.2):
dependencies:
'@cspotcode/source-map-support': 0.8.1
'@tsconfig/node10': 1.0.11
'@tsconfig/node12': 1.0.11
'@tsconfig/node14': 1.0.3
'@tsconfig/node16': 1.0.4
'@types/node': 22.17.2
'@types/node': 22.18.6
acorn: 8.15.0
acorn-walk: 8.3.4
arg: 4.1.3
@@ -1586,9 +1586,9 @@ snapshots:
v8-compile-cache-lib: 3.0.1
yn: 3.1.1
tsx@4.20.4:
tsx@4.20.5:
dependencies:
esbuild: 0.25.9
esbuild: 0.25.10
get-tsconfig: 4.10.1
optionalDependencies:
fsevents: 2.3.3
@@ -1622,9 +1622,9 @@ snapshots:
wrap-ansi@8.1.0:
dependencies:
ansi-styles: 6.2.1
ansi-styles: 6.2.3
string-width: 5.1.2
strip-ansi: 7.1.0
strip-ansi: 7.1.2
ws@8.17.1: {}
+19 -6
View File
@@ -3,17 +3,30 @@ import { CONFIG } from "./env";
// Define the sources allowed in the development environment
const DEV_ORIGINS = [
CONFIG.CORS_ORIGIN, // http://localhost:3002
'http://localhost:3000', // alternate port
/^http:\/\/192\.168\.\d+\.\d+:3000$/, // LAN addresses
/^http:\/\/192\.168\.\d+\.\d+:3002$/ // LAN addresses with new port
CONFIG.CORS_ORIGIN, // http://localhost:3002
"http://localhost:3002", // alternate port
/^http:\/\/192\.168\.\d+\.\d+:3002$/, // LAN addresses
/^http:\/\/192\.168\.\d+\.\d+:3002$/, // LAN addresses with new port
];
// Parse multi-origin config in production (comma-separated)
const parseProdOrigins = (): string | RegExp | (string | RegExp)[] => {
const v = CONFIG.CORS_ORIGIN?.trim();
if (!v) return DEV_ORIGINS; // Fallback to the development whitelist
if (v.includes(",")) {
return v
.split(",")
.map((s) => s.trim())
.filter(Boolean);
}
return v;
};
// Configure CORS
export const corsOptions: CorsOptions =
CONFIG.NODE_ENV === "production"
? {
origin: CONFIG.CORS_ORIGIN,
origin: parseProdOrigins(),
methods: ["GET", "POST", "OPTIONS"],
credentials: true,
allowedHeaders: ["Content-Type", "Authorization"],
@@ -28,7 +41,7 @@ export const corsOptions: CorsOptions =
export const corsWSOptions =
CONFIG.NODE_ENV === "production"
? {
origin: CONFIG.CORS_ORIGIN, // Allowed origin, replace with your Next.js application's URL
origin: parseProdOrigins(),
methods: ["GET", "POST"],
credentials: true,
}
+191
View File
@@ -0,0 +1,191 @@
import { Router, Request, Response } from 'express';
import { redis } from '../services/redis';
import { CONFIG } from '../config/env';
const router = Router();
// 应用启动时间
const startTime = Date.now();
// 基础健康检查
router.get('/health', async (req: Request, res: Response) => {
try {
const health = {
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: Math.floor((Date.now() - startTime) / 1000),
service: 'privydrop-backend',
version: process.env.npm_package_version || '1.0.0',
environment: CONFIG.NODE_ENV
};
res.status(200).json(health);
} catch (error) {
console.error('Health check error:', error);
res.status(503).json({
status: 'unhealthy',
timestamp: new Date().toISOString(),
service: 'privydrop-backend',
error: error instanceof Error ? error.message : 'Unknown error'
});
}
});
// API路径的健康检查 (兼容性)
router.get('/api/health', async (req: Request, res: Response) => {
try {
const health = {
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: Math.floor((Date.now() - startTime) / 1000),
service: 'privydrop-backend',
version: process.env.npm_package_version || '1.0.0',
environment: CONFIG.NODE_ENV
};
res.status(200).json(health);
} catch (error) {
console.error('Health check error:', error);
res.status(503).json({
status: 'unhealthy',
timestamp: new Date().toISOString(),
service: 'privydrop-backend',
error: error instanceof Error ? error.message : 'Unknown error'
});
}
});
// 详细健康检查
router.get('/health/detailed', async (req: Request, res: Response) => {
const errors: string[] = [];
let status = 'healthy';
try {
// 基础信息
const basicHealth = {
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: Math.floor((Date.now() - startTime) / 1000),
service: 'privydrop-backend',
version: process.env.npm_package_version || '1.0.0',
environment: CONFIG.NODE_ENV
};
// 检查Redis连接
const redisHealth = await checkRedisHealth();
if (redisHealth.status !== 'connected') {
errors.push('Redis connection failed');
status = 'unhealthy';
}
// 检查Socket.IO状态
const io = req.app.get('io');
const socketHealth = {
status: io ? 'running' : 'not_initialized',
connections: io ? io.engine.clientsCount : 0
};
// 获取系统资源信息
const systemInfo = getSystemInfo();
// 检查系统资源
if (systemInfo.memory.percent > 90) {
errors.push('High memory usage (>90%)');
status = status === 'healthy' ? 'degraded' : status;
}
if (systemInfo.cpu.percent > 80) {
errors.push('High CPU usage (>80%)');
status = status === 'healthy' ? 'degraded' : status;
}
const detailedHealth = {
...basicHealth,
status,
dependencies: {
redis: redisHealth,
socketio: socketHealth
},
system: systemInfo,
...(errors.length > 0 && { errors })
};
const httpStatus = status === 'healthy' ? 200 : 503;
res.status(httpStatus).json(detailedHealth);
} catch (error) {
console.error('Detailed health check error:', error);
res.status(503).json({
status: 'unhealthy',
timestamp: new Date().toISOString(),
service: 'privydrop-backend',
error: error instanceof Error ? error.message : 'Unknown error'
});
}
});
// Redis健康检查函数
async function checkRedisHealth() {
try {
const start = Date.now();
await redis.ping();
const responseTime = Date.now() - start;
return {
status: 'connected',
responseTime,
host: CONFIG.REDIS.HOST,
port: CONFIG.REDIS.PORT
};
} catch (error) {
return {
status: 'disconnected',
error: error instanceof Error ? error.message : 'Unknown error',
host: CONFIG.REDIS.HOST,
port: CONFIG.REDIS.PORT
};
}
}
// 系统信息获取函数
function getSystemInfo() {
const memUsage = process.memoryUsage();
const totalMem = memUsage.heapTotal;
const usedMem = memUsage.heapUsed;
const freeMem = totalMem - usedMem;
return {
memory: {
used: formatBytes(usedMem),
free: formatBytes(freeMem),
total: formatBytes(totalMem),
percent: Math.round((usedMem / totalMem) * 100)
},
cpu: {
percent: getCpuUsage()
},
uptime: process.uptime(),
platform: process.platform,
arch: process.arch,
nodeVersion: process.version
};
}
// 格式化字节数
function formatBytes(bytes: number): string {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
}
// 简化的CPU使用率 (基于进程CPU时间)
function getCpuUsage(): number {
const cpuUsage = process.cpuUsage();
const totalCpuTime = (cpuUsage.user + cpuUsage.system) / 1000000; // 转换为秒
const uptime = process.uptime();
return Math.round((totalCpuTime / uptime) * 100);
}
export default router;
+5
View File
@@ -5,6 +5,7 @@ import { Server } from "socket.io"; // socket.io: A library for real-time web ap
import { CONFIG } from "./config/env";
import { corsOptions, corsWSOptions } from "./config/server";
import apiRouter from "./routes/api";
import healthRouter from "./routes/health";
import { setupSocketHandlers } from "./socket/handlers";
const app = express(); // Create an Express application
@@ -19,6 +20,10 @@ setupSocketHandlers(io);
// Make io instance available to routes
app.set('io', io);
// Register health check routes first (for Docker health checks)
app.use(healthRouter);
// Register API routes
app.use(apiRouter);
server.listen(CONFIG.BACKEND_PORT, () => {
+762
View File
@@ -0,0 +1,762 @@
#!/bin/bash
set -e # Exit immediately on error
# Color definitions
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# Script directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DOCKER_SCRIPTS_DIR="$SCRIPT_DIR/docker/scripts"
# Logging helpers
log_info() {
echo -e "${BLUE}$1${NC}"
}
log_success() {
echo -e "${GREEN}$1${NC}"
}
log_warning() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
log_error() {
echo -e "${RED}$1${NC}"
}
# Show help
show_help() {
cat << EOF
PrivyDrop Docker Deployment Script
Usage: $0 [options]
Options:
--domain DOMAIN Specify domain (for HTTPS deployments)
--mode MODE Deployment mode: lan-http|lan-tls|public|full
lan-http: Intranet HTTP (fast start; no TLS)
lan-tls: Intranet HTTPS (self-signed; dev/managed env only)
public: Public HTTP + TURN server
full: Domain + HTTPS (Lets Encrypt) + TURN server
--with-nginx Enable Nginx reverse proxy
--with-turn Enable TURN server
--with-sni443 Force enable 443 SNI routing (full; default enabled)
--no-sni443 Disable 443 SNI routing (full; web listens directly on 443)
--enable-web-https In lan-tls mode, enable self-signed HTTPS on 8443 (no HSTS)
--le-email EMAIL Email for Let's Encrypt (recommended in full mode)
--test-renewal Run 'certbot renew --dry-run' and reload services (verification)
--clean Clean existing containers and data
--help Show help
Examples:
$0 --mode lan-http # LAN HTTP quick start
$0 --mode lan-http --with-turn # LAN HTTP with TURN (NAT-friendly)
$0 --mode lan-tls --enable-web-https # LAN HTTPS (self-signed) on 8443 (dev/managed)
$0 --mode public --with-turn # Public deployment + TURN server (no domain)
$0 --mode full --domain example.com \\
--with-nginx --with-turn --le-email you@domain.com # Full HTTPS deployment (LE auto-issue/renew)
$0 --clean # Clean deployment
Requirements:
- Docker Engine and Docker Compose V2 (command `docker compose`)
EOF
}
# Parse command-line arguments
parse_arguments() {
DOMAIN_NAME=""
DEPLOYMENT_MODE=""
WITH_NGINX=false
WITH_TURN=false
CLEAN_MODE=false
LE_EMAIL=""
WITH_SNI443=false
DISABLE_SNI443=false
ENABLE_WEB_HTTPS=false
TEST_RENEWAL=false
while [[ $# -gt 0 ]]; do
case $1 in
--domain)
DOMAIN_NAME="$2"
shift 2
;;
--mode)
DEPLOYMENT_MODE="$2"
shift 2
;;
--with-nginx)
WITH_NGINX=true
shift
;;
--with-turn)
WITH_TURN=true
shift
;;
--with-sni443)
WITH_SNI443=true
shift
;;
--no-sni443)
DISABLE_SNI443=true
shift
;;
--enable-web-https)
ENABLE_WEB_HTTPS=true
shift
;;
--le-email)
LE_EMAIL="$2"
shift 2
;;
--test-renewal)
TEST_RENEWAL=true
shift
;;
--clean)
CLEAN_MODE=true
shift
;;
--help)
show_help
exit 0
;;
*)
log_error "Unknown argument: $1"
show_help
exit 1
;;
esac
done
# Export variables for other scripts
export DOMAIN_NAME
export DEPLOYMENT_MODE
export WITH_NGINX
export WITH_TURN
}
# Check dependencies
check_dependencies() {
log_info "Checking dependencies..."
local missing_deps=()
if ! command -v docker &> /dev/null; then
missing_deps+=("docker")
fi
if ! docker compose version &> /dev/null; then
missing_deps+=("docker compose (V2)")
fi
if ! command -v curl &> /dev/null; then
missing_deps+=("curl")
fi
if ! command -v openssl &> /dev/null; then
missing_deps+=("openssl")
fi
if [[ ${#missing_deps[@]} -gt 0 ]]; then
log_error "Missing dependencies: ${missing_deps[*]}"
echo ""
echo "Please install the missing dependencies:"
for dep in "${missing_deps[@]}"; do
case $dep in
docker)
echo " Docker: https://docs.docker.com/get-docker/"
;;
"docker compose (V2)")
echo " Docker Compose V2 plugin: https://docs.docker.com/compose/install/"
;;
curl)
echo " curl: sudo apt-get install curl (Ubuntu/Debian)"
;;
openssl)
echo " openssl: sudo apt-get install openssl (Ubuntu/Debian)"
;;
esac
done
exit 1
fi
log_success "Dependency checks passed"
}
# Install and prepare Let's Encrypt (certbot)
ensure_certbot() {
if command -v certbot >/dev/null 2>&1; then
return 0
fi
log_info "Installing certbot (requires sudo)..."
if command -v apt-get >/dev/null 2>&1; then
sudo apt-get update -y && sudo apt-get install -y certbot
else
log_error "apt-get not found. Please install certbot manually or run on a supported system"
exit 1
fi
}
# Write certbot deploy hook: copy certs and hot-reload services after renewal
install_certbot_deploy_hook() {
local repo_dir="$SCRIPT_DIR"
local hook_dir="/etc/letsencrypt/renewal-hooks/deploy"
local hook_file="$hook_dir/privydrop-reload.sh"
local compose_file="$repo_dir/docker-compose.yml"
sudo mkdir -p "$hook_dir"
sudo bash -c "cat > '$hook_file'" << EOF
#!/bin/bash
set -e
REPO_DIR="$repo_dir"
COMPOSE_FILE="$compose_file"
# RENEWED_LINEAGE is provided by certbot and points to live/<domain>
if [[ -z "\$RENEWED_LINEAGE" ]]; then
exit 0
fi
cp "\$RENEWED_LINEAGE/fullchain.pem" "\$REPO_DIR/docker/ssl/server-cert.pem"
cp "\$RENEWED_LINEAGE/privkey.pem" "\$REPO_DIR/docker/ssl/server-key.pem"
chmod 600 "\$REPO_DIR/docker/ssl/server-key.pem" || true
# Hot-reload nginx; restart if it fails
docker compose -f "\$COMPOSE_FILE" exec -T nginx nginx -s reload 2>/dev/null || \
docker compose -f "\$COMPOSE_FILE" restart nginx || true
# Prefer sending HUP to coturn; restart if needed (ignore if disabled)
docker compose -f "\$COMPOSE_FILE" exec -T coturn sh -c 'kill -HUP 1' 2>/dev/null || \
docker compose -f "\$COMPOSE_FILE" restart coturn || true
EOF
sudo chmod +x "$hook_file"
# Attempt to enable systemd timer
if command -v systemctl >/dev/null 2>&1; then
sudo systemctl enable --now certbot.timer 2>/dev/null || true
fi
}
# Ensure renewal is scheduled daily: prefer systemd timer; fallback to cron
ensure_renewal_scheduler() {
if command -v systemctl >/dev/null 2>&1; then
sudo systemctl enable --now certbot.timer 2>/dev/null || true
return 0
fi
# Fallback: cron job every 12 hours
if [ -w /etc/cron.d ] || sudo test -d /etc/cron.d; then
sudo bash -c 'cat > /etc/cron.d/certbot' << 'EOF'
# Auto-renew Let's Encrypt certificates for PrivyDrop
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
0 */12 * * * root certbot renew -q
EOF
fi
}
# Dry-run renewal test
test_renewal() {
log_info "Running certbot dry-run renewal test..."
if ! command -v certbot >/dev/null 2>&1; then
log_error "certbot not installed. Run full mode issuance first or install certbot manually"
return 1
fi
sudo certbot renew --dry-run || {
log_error "certbot dry-run failed"
return 1
}
# Attempt hot-reload of nginx and coturn similar to deploy-hook
docker compose exec -T nginx nginx -s reload 2>/dev/null || docker compose restart nginx || true
docker compose exec -T coturn sh -c 'kill -HUP 1' 2>/dev/null || docker compose restart coturn || true
log_success "Dry-run renewal completed; services reloaded"
}
# Issue via webroot and enable 443 config
provision_letsencrypt_cert() {
# Only in full mode with nginx enabled and domain set
if [[ "$DEPLOYMENT_MODE" != "full" || "$WITH_NGINX" != "true" ]]; then
return 0
fi
if [[ -z "$DOMAIN_NAME" ]]; then
log_warning "Full mode without --domain; skipping Let's Encrypt"
return 0
fi
if [[ -z "$LE_EMAIL" ]]; then
log_warning "No --le-email specified; using --register-unsafely-without-email"
fi
ensure_certbot
install_certbot_deploy_hook
ensure_renewal_scheduler
mkdir -p docker/letsencrypt-www docker/ssl
# If certificates already exist (including -0001 lineage), skip issuance
if [[ -f "/etc/letsencrypt/live/$DOMAIN_NAME/fullchain.pem" ]] || ls -1d /etc/letsencrypt/live/${DOMAIN_NAME}* >/dev/null 2>&1; then
log_info "Detected existing certificates/lineage; skipping initial issuance"
else
log_info "Issuing Let's Encrypt certificate via webroot..."
local email_args="--email $LE_EMAIL"
if [[ -z "$LE_EMAIL" ]]; then
email_args="--register-unsafely-without-email"
fi
# Requires port 80 reachable and nginx running
sudo certbot certonly --webroot -w "$(pwd)/docker/letsencrypt-www" \
-d "$DOMAIN_NAME" -d "turn.$DOMAIN_NAME" \
$email_args --agree-tos --non-interactive || {
log_error "Certificate issuance failed; please check certbot output"
return 1
}
fi
# Resolve lineage directory (supports -0001/-0002 suffixes) and copy to docker/ssl
local lineage_dir
lineage_dir=$(readlink -f "/etc/letsencrypt/live/$DOMAIN_NAME" 2>/dev/null || true)
if [[ -z "$lineage_dir" || ! -d "$lineage_dir" ]]; then
lineage_dir=$(ls -1d /etc/letsencrypt/live/${DOMAIN_NAME}* 2>/dev/null | sort | tail -1)
fi
if [[ -z "$lineage_dir" || ! -f "$lineage_dir/fullchain.pem" ]]; then
log_error "No valid certificate lineage directory found. Check /etc/letsencrypt/live/${DOMAIN_NAME}*"
return 1
fi
sudo cp "$lineage_dir/fullchain.pem" docker/ssl/server-cert.pem
sudo cp "$lineage_dir/privkey.pem" docker/ssl/server-key.pem
sudo chmod 600 docker/ssl/server-key.pem || true
# Enable 443 config (certs ready): append only; pass SNI flag (enabled by default in full)
local gen_args=(--mode full --domain "$DOMAIN_NAME" --no-clean --ssl-mode letsencrypt)
[[ "$WITH_SNI443" == "true" ]] && gen_args+=(--enable-sni443)
[[ "$DISABLE_SNI443" == "true" ]] && gen_args+=(--no-sni443)
bash "$DOCKER_SCRIPTS_DIR/generate-config.sh" "${gen_args[@]}" || true
# Hot-reload nginx to enable 443
docker compose exec -T nginx nginx -s reload || docker compose restart nginx
}
# Clean existing deployment
clean_deployment() {
if [[ "$CLEAN_MODE" == "true" ]]; then
log_warning "Cleaning existing deployment..."
# Stop and remove containers
if [[ -f "docker-compose.yml" ]]; then
docker compose down -v --remove-orphans 2>/dev/null || true
fi
# After graceful stop, force-clean named containers as fallback
docker stop -t 10 privydrop-nginx privydrop-coturn 2>/dev/null || true
docker rm -f privydrop-nginx privydrop-coturn 2>/dev/null || true
# Fallback: remove project network (if present)
docker network rm privydrop_privydrop-network 2>/dev/null || true
# Remove images
docker images | grep privydrop | awk '{print $3}' | xargs -r docker rmi -f 2>/dev/null || true
# Clean configuration files
rm -rf docker/nginx/conf.d/*.conf docker/ssl/* logs/* .env 2>/dev/null || true
log_success "Cleanup complete"
if [[ $# -eq 1 ]]; then # If only --clean parameter
exit 0
fi
fi
}
# Ensure TURN service starts when requested (--with-turn)
ensure_turn_running() {
if [[ "$WITH_TURN" != "true" ]]; then
return 0
fi
# If not running, start coturn via profile
if ! docker compose ps | grep -q "privydrop-coturn"; then
log_info "Starting TURN service (profile: turn)..."
docker compose --profile turn up -d coturn || true
fi
}
# Environment detection and configuration generation
setup_environment() {
log_info "Setting up environment..."
# Ensure scripts are executable
chmod +x "$DOCKER_SCRIPTS_DIR"/*.sh 2>/dev/null || true
# Run environment detection
local detect_args=""
[[ -n "$DOMAIN_NAME" ]] && detect_args="--domain $DOMAIN_NAME"
[[ -n "$DEPLOYMENT_MODE" ]] && detect_args="$detect_args --mode $DEPLOYMENT_MODE"
[[ "$WITH_NGINX" == "true" ]] && detect_args="$detect_args --with-nginx"
[[ "$WITH_SNI443" == "true" ]] && detect_args="$detect_args --enable-sni443"
[[ "$DISABLE_SNI443" == "true" ]] && detect_args="$detect_args --no-sni443"
[[ "$ENABLE_WEB_HTTPS" == "true" ]] && detect_args="$detect_args --enable-web-https"
if ! bash "$DOCKER_SCRIPTS_DIR/detect-environment.sh" $detect_args; then
log_error "Environment detection failed"
exit 1
fi
# Generate configuration files
if ! bash "$DOCKER_SCRIPTS_DIR/generate-config.sh" $detect_args; then
log_error "Configuration generation failed"
exit 1
fi
log_success "Environment setup complete"
}
# Build and start services
deploy_services() {
log_info "Building and starting services..."
# Ensure log directories exist and relax permissions so containers (coturn/nginx etc.) can write logs
mkdir -p logs logs/nginx logs/backend logs/frontend logs/coturn 2>/dev/null || true
chmod 777 -R logs 2>/dev/null || true
log_info "Log directories prepared and permissions set: ./logs (mode 777)"
# Stop existing services
if docker compose ps | grep -q "Up"; then
log_info "Stopping existing services..."
docker compose down
fi
# Determine enabled services (Compose V2 requires --profile before the subcommand)
local profiles=""
if [[ "$WITH_NGINX" == "true" ]]; then
profiles="$profiles --profile nginx"
fi
if [[ "$WITH_TURN" == "true" ]]; then
profiles="$profiles --profile turn"
fi
# Build images (parallel first, fall back to serial on failure)
log_info "Building Docker images..."
set +e
docker compose build --parallel
local build_status=$?
set -e
if [[ $build_status -ne 0 ]]; then
log_warning "Parallel build failed; falling back to serial build..."
docker compose build
fi
# Start services (--profile must precede up)
log_info "Starting services..."
# shellcheck disable=SC2086
docker compose $profiles up -d
log_success "Services started"
}
# Wait for services to be ready
wait_for_services() {
log_info "Waiting for services to be ready..."
local max_attempts=60
local attempt=0
local services_ready=false
while [[ $attempt -lt $max_attempts ]]; do
local backend_ready=false
local frontend_ready=false
# Check backend health
if curl -f http://localhost:3001/health &> /dev/null; then
backend_ready=true
fi
# Check frontend health
if curl -f http://localhost:3002/api/health &> /dev/null; then
frontend_ready=true
fi
if [[ "$backend_ready" == "true" ]] && [[ "$frontend_ready" == "true" ]]; then
services_ready=true
break
fi
attempt=$((attempt + 1))
echo -n "."
sleep 2
done
echo ""
if [[ "$services_ready" == "true" ]]; then
log_success "All services are ready"
return 0
else
log_error "Service startup timed out"
log_info "View service status: docker compose ps"
log_info "View service logs: docker compose logs -f"
return 1
fi
}
# Run post-deployment checks
post_deployment_checks() {
log_info "Running post-deployment checks..."
# Check container status
log_info "Checking container status..."
docker compose ps
# In full+nginx, add HTTPS health check (if domain defined)
if [[ -f ".env" ]]; then
local dep_mode="$(grep "DEPLOYMENT_MODE=" .env | cut -d'=' -f2)"
local dname="$(grep "DOMAIN_NAME=" .env | cut -d'=' -f2)"
if [[ "$dep_mode" == "full" && -n "$dname" ]]; then
log_info "Test: HTTPS health check https://$dname/api/health"
if curl -fsS "https://$dname/api/health" >/dev/null; then
log_success "HTTPS health check passed"
else
log_warning "HTTPS health check failed. If the certificate was just issued, wait a bit or run: bash docker/scripts/generate-config.sh --mode full --domain $dname --no-clean && docker compose exec -T nginx nginx -s reload"
fi
fi
fi
# Run health-check tests
if [[ -f "test-health-apis.sh" ]]; then
log_info "Running health-check tests..."
if bash test-health-apis.sh; then
log_success "Health-check tests passed"
else
log_warning "Health-check tests failed, but services may still be working"
fi
fi
log_success "Post-deployment checks complete"
}
# Show deployment results
show_deployment_info() {
echo ""
echo -e "${GREEN}🎉 PrivyDrop deployment complete!${NC}"
echo ""
# Read configuration
local local_ip=""
local public_ip=""
local frontend_port=""
local backend_port=""
local https_port=""
local deployment_mode=""
local network_mode=""
local domain_name=""
local turn_enabled_env=""
if [[ -f ".env" ]]; then
local_ip=$(grep "LOCAL_IP=" .env | cut -d'=' -f2)
public_ip=$(grep "PUBLIC_IP=" .env | cut -d'=' -f2)
frontend_port=$(grep "FRONTEND_PORT=" .env | cut -d'=' -f2)
backend_port=$(grep "BACKEND_PORT=" .env | cut -d'=' -f2)
https_port=$(grep "HTTPS_PORT=" .env | cut -d'=' -f2)
deployment_mode=$(grep "DEPLOYMENT_MODE=" .env | cut -d'=' -f2)
network_mode=$(grep "NETWORK_MODE=" .env | cut -d'=' -f2)
domain_name=$(grep "DOMAIN_NAME=" .env | cut -d'=' -f2)
turn_enabled_env=$(grep "TURN_ENABLED=" .env | cut -d'=' -f2)
fi
echo -e "${BLUE}📋 Access Info:${NC}"
# Determine if public scenario (public/full)
local is_public="false"
if [[ "$deployment_mode" == "public" || "$deployment_mode" == "full" || "$network_mode" == "public" ]]; then
is_public="true"
fi
if [[ "$is_public" == "true" ]]; then
# For public scenarios, prefer domain, then public IP
if [[ -n "$domain_name" ]]; then
if [[ "$WITH_NGINX" == "true" || "$deployment_mode" == "full" ]]; then
echo " Public access: https://$domain_name"
echo " API: https://$domain_name"
else
echo " Public access: http://$domain_name:${frontend_port:-3002}"
echo " API: http://$domain_name:${backend_port:-3001}"
fi
elif [[ -n "$public_ip" ]]; then
if [[ "$WITH_NGINX" == "true" ]]; then
echo " Public access: http://$public_ip"
echo " API: http://$public_ip"
else
echo " Public access: http://$public_ip:${frontend_port:-3002}"
echo " API: http://$public_ip:${backend_port:-3001}"
fi
else
# Fallback: show LAN and localhost if public IP is unavailable
echo " Frontend: http://localhost:${frontend_port:-3002}"
echo " Backend API: http://localhost:${backend_port:-3001}"
fi
else
# Private/basic: localhost + LAN
if [[ "$WITH_NGINX" == "true" ]]; then
# When Nginx is enabled and frontend uses same-origin API, prefer the gateway as the primary entry
echo " Frontend: http://localhost"
echo " API: http://localhost"
else
echo " Frontend: http://localhost:${frontend_port:-3002}"
echo " Backend API: http://localhost:${backend_port:-3001}"
fi
if [[ -n "$local_ip" ]] && [[ "$local_ip" != "127.0.0.1" ]]; then
echo ""
echo -e "${BLUE}🌐 LAN Access:${NC}"
if [[ "$WITH_NGINX" == "true" ]]; then
echo " Frontend: http://$local_ip"
echo " API: http://$local_ip"
else
echo " Frontend: http://$local_ip:${frontend_port:-3002}"
echo " Backend API: http://$local_ip:${backend_port:-3001}"
fi
fi
fi
if [[ "$WITH_NGINX" == "true" ]]; then
echo ""
echo -e "${BLUE}🔀 Nginx Proxy:${NC}"
if [[ -n "$domain_name" ]]; then
echo " HTTP: http://$domain_name"
if [[ -f "docker/ssl/server-cert.pem" ]]; then
echo " HTTPS: https://$domain_name"
fi
elif [[ -n "$public_ip" ]]; then
echo " HTTP: http://$public_ip"
if [[ -f "docker/ssl/server-cert.pem" ]]; then
# In non-domain cases, show HTTPS with explicit port (e.g., lan-tls uses 8443)
if [[ -n "$https_port" && "$https_port" != "443" ]]; then
echo " HTTPS: https://$public_ip:$https_port"
else
echo " HTTPS: https://$public_ip"
fi
fi
else
echo " HTTP: http://localhost"
if [[ -f "docker/ssl/server-cert.pem" ]]; then
# Show correct HTTPS endpoint based on configured port
if [[ -n "$https_port" && "$https_port" != "443" ]]; then
echo " HTTPS: https://localhost:$https_port"
if [[ -n "$local_ip" && "$local_ip" != "127.0.0.1" ]]; then
echo " HTTPS (LAN): https://$local_ip:$https_port"
fi
else
echo " HTTPS: https://localhost"
fi
fi
fi
fi
echo ""
echo -e "${BLUE}🔧 Management Commands:${NC}"
echo " Status: docker compose ps"
echo " Logs: docker compose logs -f [service]"
echo " Restart: docker compose restart [service]"
echo " Stop: docker compose down"
echo " Full cleanup: $0 --clean"
if [[ -f "docker/ssl/ca-cert.pem" ]]; then
echo ""
echo -e "${BLUE}🔒 SSL Certificates:${NC}"
echo " CA certificate: docker/ssl/ca-cert.pem"
echo " To trust HTTPS, import the CA certificate into your browser"
fi
if [[ "$WITH_TURN" == "true" || "$turn_enabled_env" == "true" ]]; then
local turn_username=""
local turn_realm=""
if [[ -f ".env" ]]; then
turn_username=$(grep "TURN_USERNAME=" .env | cut -d'=' -f2)
turn_realm=$(grep "TURN_REALM=" .env | cut -d'=' -f2)
fi
echo ""
echo -e "${BLUE}🔄 TURN Server:${NC}"
# Prefer domain for TURN info; otherwise show public IP
if [[ -n "$domain_name" ]]; then
echo " STUN: stun:${domain_name}:3478"
echo " TURN (UDP): turn:${domain_name}:3478"
echo " TURN (TLS): turns:turn.${domain_name}:443 (if 443 SNI split is configured)"
elif [[ -n "$public_ip" ]]; then
echo " STUN: stun:${public_ip}:3478"
echo " TURN: turn:${public_ip}:3478"
else
echo " STUN: stun:${local_ip}:3478"
echo " TURN: turn:${local_ip}:3478"
fi
echo " Username: ${turn_username:-privydrop}"
echo " Password: (stored in .env)"
fi
echo ""
echo -e "${YELLOW}💡 Tips:${NC}"
echo " - First run may take several minutes to download and build images"
echo " - If issues occur, check logs: docker compose logs -f"
echo " - More help: $0 --help"
echo ""
# Public scenario: for domain + HTTPS setup steps, see docs
if [[ "$is_public" == "true" && -z "$domain_name" ]]; then
echo -e "${BLUE}🌍 Domain + HTTPS guide:${NC} see docs/DEPLOYMENT_docker.md or docs/DEPLOYMENT_docker.zh-CN.md"
fi
}
# Main function
main() {
echo -e "${BLUE}=== PrivyDrop Docker One-Click Deployment ===${NC}"
echo ""
# Parse command-line arguments
parse_arguments "$@"
# Check dependencies
check_dependencies
echo ""
# If only testing renewal, run and exit
if [[ "$TEST_RENEWAL" == "true" ]]; then
test_renewal && exit 0 || exit 1
fi
# Clean mode
clean_deployment
# If only cleaning (no other args), exit early to skip env detection
if [[ "$CLEAN_MODE" == "true" && -z "$DEPLOYMENT_MODE" && "$WITH_NGINX" == "false" && "$WITH_TURN" == "false" && -z "$DOMAIN_NAME" ]]; then
log_success "Cleanup complete (clean-only mode). Exiting."
exit 0
fi
# Environment setup
setup_environment
echo ""
# Deploy services
deploy_services
echo ""
# If full + nginx, automatically issue certs and enable 443
provision_letsencrypt_cert || true
# Ensure TURN is running (when requested with --with-turn)
ensure_turn_running || true
# Wait for services to be ready
if wait_for_services; then
echo ""
post_deployment_checks
show_deployment_info
else
log_error "Deployment failed. Please check logs: docker compose logs"
exit 1
fi
}
# Trap interrupt signals
trap 'log_warning "Deployment interrupted"; exit 1' INT TERM
# Run main function
main "$@"
+144
View File
@@ -0,0 +1,144 @@
services:
# Redis cache service
redis:
image: redis:7-alpine
container_name: privydrop-redis
restart: unless-stopped
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
volumes:
- redis_data:/data
networks:
- privydrop-network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
start_period: 5s
# Backend signaling service
backend:
build:
context: ./backend
dockerfile: Dockerfile
args:
- HTTP_PROXY=${HTTP_PROXY}
- HTTPS_PROXY=${HTTPS_PROXY}
- NO_PROXY=${NO_PROXY}
container_name: privydrop-backend
restart: unless-stopped
environment:
- NODE_ENV=production
- BACKEND_PORT=3001
- REDIS_HOST=redis
- REDIS_PORT=6379
- CORS_ORIGIN=${CORS_ORIGIN:-http://localhost}
ports:
- "${BACKEND_PORT:-3001}:3001"
depends_on:
redis:
condition: service_healthy
networks:
- privydrop-network
volumes:
- ./logs:/app/logs
healthcheck:
test: ["CMD", "node", "health-check.js"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
# Frontend app
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
args:
- HTTP_PROXY=${HTTP_PROXY}
- HTTPS_PROXY=${HTTPS_PROXY}
- NO_PROXY=${NO_PROXY}
- NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
- NEXT_PUBLIC_TURN_HOST=${NEXT_PUBLIC_TURN_HOST}
- NEXT_PUBLIC_TURN_USERNAME=${NEXT_PUBLIC_TURN_USERNAME}
- NEXT_PUBLIC_TURN_PASSWORD=${NEXT_PUBLIC_TURN_PASSWORD}
- NEXT_IMAGE_UNOPTIMIZED=${NEXT_IMAGE_UNOPTIMIZED:-true}
container_name: privydrop-frontend
restart: unless-stopped
environment:
- NODE_ENV=production
- NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL:-http://localhost:3001}
- BACKEND_INTERNAL_URL=${BACKEND_INTERNAL_URL:-http://backend:3001}
- NEXT_IMAGE_UNOPTIMIZED=${NEXT_IMAGE_UNOPTIMIZED:-true}
- PORT=3002
- HOSTNAME=0.0.0.0
- NODE_EXTRA_CA_CERTS=/opt/privydrop/ssl/ca-cert.pem
ports:
- "${FRONTEND_PORT:-3002}:3002"
depends_on:
backend:
condition: service_healthy
networks:
- privydrop-network
volumes:
- ./docker/ssl:/opt/privydrop/ssl:ro
healthcheck:
test: ["CMD", "node", "health-check.js"]
interval: 30s
timeout: 10s
retries: 3
start_period: 120s
# Nginx reverse proxy
nginx:
image: nginx:alpine
container_name: privydrop-nginx
restart: unless-stopped
ports:
- "${HTTP_PORT:-80}:80"
- "${HTTPS_PORT:-443}:${DOCKER_HTTPS_CONTAINER_PORT:-443}"
volumes:
- ./docker/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./docker/nginx/conf.d:/etc/nginx/conf.d:ro
- ./docker/letsencrypt-www:/var/www/certbot:ro
- ./docker/ssl:/etc/nginx/ssl:ro
- ./logs/nginx:/var/log/nginx
depends_on:
- frontend
- backend
networks:
- privydrop-network
profiles:
- nginx
# TURN/STUN server (optional, for NAT traversal)
coturn:
image: coturn/coturn:4.6.2
container_name: privydrop-coturn
restart: unless-stopped
ports:
- "3478:3478/tcp"
- "3478:3478/udp"
- "5349:5349/tcp"
- "5349:5349/udp"
- "${TURN_MIN_PORT:-49152}-${TURN_MAX_PORT:-49252}:${TURN_MIN_PORT:-49152}-${TURN_MAX_PORT:-49252}/udp"
volumes:
- ./docker/coturn/turnserver.conf:/etc/coturn/turnserver.conf:ro
- ./docker/ssl:/etc/ssl/certs:ro
- ./logs/coturn:/var/log
networks:
- privydrop-network
profiles:
- turn
command: ["-c", "/etc/coturn/turnserver.conf"]
networks:
privydrop-network:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
volumes:
redis_data:
driver: local
+487
View File
@@ -0,0 +1,487 @@
#!/bin/bash
# Color definitions
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Global variables
NETWORK_MODE=""
LOCAL_IP=""
PUBLIC_IP=""
DEPLOYMENT_MODE="basic"
FORCED_MODE=""
LOCAL_IP_OVERRIDE=""
declare -a IP_CANDIDATES=()
declare -A __SEEN_IPS=()
add_ip_candidate() {
local ip="$1"
[[ -z "$ip" ]] && return
[[ "$ip" == "127."* ]] && return
[[ "$ip" == "0.0.0.0" ]] && return
if [[ -z "${__SEEN_IPS[$ip]}" ]]; then
IP_CANDIDATES+=("$ip")
__SEEN_IPS[$ip]=1
fi
}
is_rfc1918_ip() {
local ip="$1"
case "$ip" in
10.*|192.168.*|172.1[6-9].*|172.2[0-9].*|172.3[0-1].*)
return 0
;;
*)
return 1
;;
esac
}
is_cgnat_ip() {
local ip="$1"
case "$ip" in
100.*)
return 0
;;
*)
return 1
;;
esac
}
is_reserved_benchmark_ip() {
local ip="$1"
case "$ip" in
198.18.*|198.19.*)
return 0
;;
*)
return 1
;;
esac
}
is_link_local_ip() {
local ip="$1"
case "$ip" in
169.254.*)
return 0
;;
*)
return 1
;;
esac
}
is_routable_public_ip() {
local ip="$1"
if [[ -z "$ip" ]]; then
return 1
fi
if is_rfc1918_ip "$ip"; then
return 1
fi
if is_cgnat_ip "$ip"; then
return 1
fi
if is_reserved_benchmark_ip "$ip"; then
return 1
fi
case "$ip" in
127.*|169.254.*)
return 1
;;
*)
return 0
;;
esac
}
collect_ip_candidates() {
IP_CANDIDATES=()
unset __SEEN_IPS
declare -A __SEEN_IPS=()
if command -v hostname >/dev/null 2>&1; then
local host_ips
host_ips=$(hostname -I 2>/dev/null || true)
for ip in $host_ips; do
add_ip_candidate "$ip"
done
fi
if command -v ip >/dev/null 2>&1; then
while IFS= read -r ip; do
add_ip_candidate "$ip"
done < <(ip -o -4 addr show scope global 2>/dev/null | awk '{print $4}' | cut -d/ -f1)
fi
if command -v ifconfig >/dev/null 2>&1; then
while IFS= read -r ip; do
add_ip_candidate "$ip"
done < <(ifconfig 2>/dev/null | awk '/inet / {print $2}' | grep -E '^[0-9]+(\.[0-9]+){3}$')
fi
if command -v ip >/dev/null 2>&1; then
local route_ip
route_ip=$(ip route get 1.1.1.1 2>/dev/null | awk '{for (i=1;i<=NF;i++) if ($i=="src") {print $(i+1); exit}}')
add_ip_candidate "$route_ip"
fi
if [[ ${#IP_CANDIDATES[@]} -eq 0 ]]; then
local fallback
fallback=$(hostname -I 2>/dev/null | awk '{print $1}')
add_ip_candidate "$fallback"
fi
}
resolve_local_ip() {
if [[ -n "$LOCAL_IP_OVERRIDE" ]]; then
LOCAL_IP="$LOCAL_IP_OVERRIDE"
return
fi
collect_ip_candidates
if [[ ${#IP_CANDIDATES[@]} -eq 0 ]]; then
LOCAL_IP=""
return
fi
local ip
for ip in "${IP_CANDIDATES[@]}"; do
if is_rfc1918_ip "$ip"; then
LOCAL_IP="$ip"
return
fi
done
for ip in "${IP_CANDIDATES[@]}"; do
if is_cgnat_ip "$ip"; then
LOCAL_IP="$ip"
return
fi
done
for ip in "${IP_CANDIDATES[@]}"; do
if is_reserved_benchmark_ip "$ip"; then
continue
fi
if is_link_local_ip "$ip"; then
continue
fi
LOCAL_IP="$ip"
return
done
LOCAL_IP="${IP_CANDIDATES[0]}"
}
# Logging helpers
log_info() {
echo -e "${BLUE}$1${NC}"
}
log_success() {
echo -e "${GREEN}$1${NC}"
}
log_warning() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
log_error() {
echo -e "${RED}$1${NC}"
}
# Detect network environment
detect_network_environment() {
log_info "Detecting network environment..."
resolve_local_ip
if [[ -z "$LOCAL_IP" ]]; then
LOCAL_IP="127.0.0.1"
log_warning "Unable to detect host IP; using default: $LOCAL_IP"
fi
if [[ "$FORCED_MODE" == "private" ]]; then
NETWORK_MODE="private"
PUBLIC_IP=""
log_info "Network mode set via parameters: $NETWORK_MODE"
echo " Local IP: $LOCAL_IP"
return 0
fi
local mode_guess="private"
local printed_prompt_info="false"
PUBLIC_IP=""
if curl -s --connect-timeout 5 --max-time 10 ifconfig.me > /dev/null 2>&1; then
PUBLIC_IP=$(curl -s --connect-timeout 5 --max-time 10 ifconfig.me 2>/dev/null || echo "")
if [[ -n "$PUBLIC_IP" ]]; then
if is_routable_public_ip "$PUBLIC_IP"; then
mode_guess="public"
else
log_warning "Public IP is test/reserved range; treating as private"
fi
else
log_warning "Public connectivity unstable; treating as private"
fi
fi
if [[ -z "$FORCED_MODE" ]]; then
if [[ "$mode_guess" == "public" ]]; then
NETWORK_MODE="public"
else
NETWORK_MODE="private"
fi
else
NETWORK_MODE="$FORCED_MODE"
if [[ "$FORCED_MODE" == "public" && -z "$PUBLIC_IP" ]]; then
log_warning "Could not detect public IP; continuing as public mode. Please verify network config"
fi
fi
if [[ "$NETWORK_MODE" != "public" ]]; then
PUBLIC_IP=""
fi
if [[ "$FORCED_MODE" == "public" ]]; then
log_info "Network mode set via parameters: $NETWORK_MODE"
elif [[ "$NETWORK_MODE" == "public" ]]; then
log_success "Public network detected"
else
log_success "Private network detected"
fi
if [[ "$printed_prompt_info" == "false" ]]; then
echo " Local IP: $LOCAL_IP"
if [[ "$NETWORK_MODE" == "public" && -n "$PUBLIC_IP" ]]; then
echo " Public IP: $PUBLIC_IP"
fi
fi
}
# Check system resources
check_system_resources() {
log_info "Checking system resources..."
local warnings=0
# Check memory
if command -v free >/dev/null 2>&1; then
TOTAL_MEM=$(free -m | awk 'NR==2{print $2}')
if [[ $TOTAL_MEM -lt 512 ]]; then
log_error "Insufficient memory: ${TOTAL_MEM}MB (512MB+ recommended)"
return 1
elif [[ $TOTAL_MEM -lt 1024 ]]; then
log_warning "Low memory: ${TOTAL_MEM}MB (1GB+ recommended)"
warnings=$((warnings + 1))
else
log_success "Memory OK: ${TOTAL_MEM}MB"
fi
else
log_warning "Unable to read memory usage"
warnings=$((warnings + 1))
fi
# Check disk usage
DISK_USAGE=$(df -h / | awk 'NR==2{print $5}' | sed 's/%//')
if [[ $DISK_USAGE -gt 95 ]]; then
log_error "Insufficient disk space: ${DISK_USAGE}% used"
return 1
elif [[ $DISK_USAGE -gt 80 ]]; then
log_warning "Disk space tight: ${DISK_USAGE}% used"
warnings=$((warnings + 1))
else
log_success "Disk space OK: ${DISK_USAGE}% used"
fi
# Check available disk space
AVAILABLE_SPACE=$(df -BG / | awk 'NR==2{print $4}' | sed 's/G//')
if [[ $AVAILABLE_SPACE -lt 2 ]]; then
log_error "Not enough free disk space: ${AVAILABLE_SPACE}GB (2GB+ recommended)"
return 1
fi
if [[ $warnings -gt 0 ]]; then
log_warning "System resource check passed with $warnings warning(s)"
else
log_success "System resource check passed"
fi
return 0
}
# Validate Docker environment
verify_docker_installation() {
log_info "Checking Docker environment..."
if ! command -v docker &> /dev/null; then
log_error "Docker is not installed"
echo "Please install Docker: https://docs.docker.com/get-docker/"
return 1
fi
if ! command -v docker-compose &> /dev/null && ! docker compose version &> /dev/null; then
log_error "Docker Compose is not installed"
echo "Please install Docker Compose: https://docs.docker.com/compose/install/"
return 1
fi
# Check Docker service status
if ! docker info &> /dev/null; then
log_error "Docker service is not running"
echo "Please start the Docker service"
return 1
fi
# Check Docker version
DOCKER_VERSION=$(docker --version | grep -oE '[0-9]+\.[0-9]+' | head -1)
log_success "Docker version: $DOCKER_VERSION"
# Check Docker Compose version
if command -v docker-compose &> /dev/null; then
COMPOSE_VERSION=$(docker-compose --version | grep -oE '[0-9]+\.[0-9]+' | head -1)
log_success "Docker Compose version: $COMPOSE_VERSION"
else
COMPOSE_VERSION=$(docker compose version --short 2>/dev/null || echo "built-in")
log_success "Docker Compose version: $COMPOSE_VERSION"
fi
return 0
}
# Check port usage
check_port_availability() {
local ports="$1"
log_info "Checking port usage..."
local occupied_ports=()
IFS=',' read -ra PORT_ARRAY <<< "$ports"
for port in "${PORT_ARRAY[@]}"; do
port=$(echo "$port" | xargs) # Trim spaces
if command -v ss >/dev/null 2>&1; then
if ss -tuln | grep -q ":$port "; then
occupied_ports+=("$port")
fi
elif command -v netstat >/dev/null 2>&1; then
if netstat -tuln 2>/dev/null | grep -q ":$port "; then
occupied_ports+=("$port")
fi
else
log_warning "Unable to check port usage (missing ss and netstat)"
return 0
fi
done
if [[ ${#occupied_ports[@]} -gt 0 ]]; then
log_warning "Ports in use: ${occupied_ports[*]}"
log_info "Change ports in .env, or run './deploy.sh --clean' / 'docker-compose down' to clean old containers"
else
log_success "All ports available"
fi
}
# Detect deployment mode
detect_deployment_mode() {
log_info "Determining deployment mode..."
if [[ "$NETWORK_MODE" == "public" ]] && [[ -n "$DOMAIN_NAME" ]]; then
DEPLOYMENT_MODE="full"
log_success "Deployment mode: full (HTTPS + TURN server)"
elif [[ "$NETWORK_MODE" == "public" ]]; then
DEPLOYMENT_MODE="public"
log_success "Deployment mode: public (HTTP + TURN)"
else
DEPLOYMENT_MODE="basic"
log_success "Deployment mode: basic (intranet HTTP)"
fi
}
# Main function
main() {
echo -e "${BLUE}=== PrivyDrop Docker Environment Check ===${NC}\n"
# Parse command-line arguments
while [[ $# -gt 0 ]]; do
case $1 in
--domain)
DOMAIN_NAME="$2"
shift 2
;;
--mode)
DEPLOYMENT_MODE="$2"
case "$2" in
private|basic)
FORCED_MODE="private"
;;
public|full)
FORCED_MODE="public"
;;
*)
FORCED_MODE=""
;;
esac
shift 2
;;
--local-ip)
LOCAL_IP_OVERRIDE="$2"
shift 2
;;
*)
shift
;;
esac
done
# Run checks
detect_network_environment
echo ""
if ! check_system_resources; then
log_error "System resource check failed; resolve resource issues and retry"
exit 1
fi
echo ""
if ! verify_docker_installation; then
log_error "Docker environment check failed; please install and start Docker"
exit 1
fi
echo ""
check_port_availability "80,443,3002,3001,3478,5349,6379"
echo ""
detect_deployment_mode
echo ""
log_success "Environment check complete!"
echo -e "${BLUE}Results:${NC}"
echo " Network mode: $NETWORK_MODE"
echo " Local IP: $LOCAL_IP"
[[ -n "$PUBLIC_IP" ]] && echo " Public IP: $PUBLIC_IP"
echo " Deployment mode: $DEPLOYMENT_MODE"
# Export env vars for other scripts
export NETWORK_MODE
export LOCAL_IP
export PUBLIC_IP
export DEPLOYMENT_MODE
export DOMAIN_NAME
export LOCAL_IP_OVERRIDE
return 0
}
# If the script is executed directly
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi
+1086
View File
File diff suppressed because it is too large Load Diff
+420
View File
@@ -0,0 +1,420 @@
#!/bin/bash
# PrivyDrop Docker deployment test script
# Validate deployment integrity and functionality
# Color definitions
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# Test result counters
TESTS_PASSED=0
TESTS_FAILED=0
TOTAL_TESTS=0
# Logging helpers
log_info() {
echo -e "${BLUE}$1${NC}"
}
log_success() {
echo -e "${GREEN}$1${NC}"
TESTS_PASSED=$((TESTS_PASSED + 1))
}
log_error() {
echo -e "${RED}$1${NC}"
TESTS_FAILED=$((TESTS_FAILED + 1))
}
log_warning() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
# Test functions
run_test() {
local test_name="$1"
local test_command="$2"
TOTAL_TESTS=$((TOTAL_TESTS + 1))
log_info "Test: $test_name"
if eval "$test_command" >/dev/null 2>&1; then
log_success "$test_name"
return 0
else
log_error "$test_name"
return 1
fi
}
# Docker environment tests
test_docker_environment() {
echo -e "${BLUE}=== Docker Environment Tests ===${NC}"
run_test "Docker installed" "command -v docker"
run_test "Docker daemon running" "docker info"
run_test "Docker Compose available" "docker-compose --version || docker compose version"
echo ""
}
# Container status tests
test_container_status() {
echo -e "${BLUE}=== Container Status Tests ===${NC}"
# Check if containers exist and are running
local containers=("privydrop-redis" "privydrop-backend" "privydrop-frontend")
for container in "${containers[@]}"; do
run_test "Container $container is running" "docker ps | grep -q $container"
done
# Check container health
for container in "${containers[@]}"; do
if docker ps --format "table {{.Names}}\t{{.Status}}" | grep -q "$container.*healthy"; then
log_success "Container $container health OK"
TESTS_PASSED=$((TESTS_PASSED + 1))
else
log_warning "Container $container health unknown or unhealthy"
fi
TOTAL_TESTS=$((TOTAL_TESTS + 1))
done
echo ""
}
# Network connectivity tests
test_network_connectivity() {
echo -e "${BLUE}=== Network Connectivity Tests ===${NC}"
# Test port connectivity
local ports=("3002:Frontend" "3001:Backend" "6379:Redis")
for port_info in "${ports[@]}"; do
local port=$(echo "$port_info" | cut -d':' -f1)
local service=$(echo "$port_info" | cut -d':' -f2)
run_test "$service port $port reachable" "nc -z localhost $port"
done
# Test inter-container networking
run_test "Backend can connect to Redis" "docker-compose exec -T backend sh -c 'nc -z redis 6379'"
run_test "Frontend can reach backend" "curl -f http://localhost:3001/health"
echo ""
}
# API functionality tests
test_api_functionality() {
echo -e "${BLUE}=== API Functionality Tests ===${NC}"
# Health check APIs
run_test "Backend health check API" "curl -f http://localhost:3001/health"
run_test "Frontend health check API" "curl -f http://localhost:3002/api/health"
# Backend detailed health check
if curl -f http://localhost:3001/health/detailed >/dev/null 2>&1; then
local redis_status=$(curl -s http://localhost:3001/health/detailed | jq -r '.dependencies.redis.status' 2>/dev/null)
if [[ "$redis_status" == "connected" ]]; then
log_success "Redis connection OK"
TESTS_PASSED=$((TESTS_PASSED + 1))
else
log_error "Redis connection issue"
TESTS_FAILED=$((TESTS_FAILED + 1))
fi
else
log_error "Detailed health check API unavailable"
TESTS_FAILED=$((TESTS_FAILED + 1))
fi
TOTAL_TESTS=$((TOTAL_TESTS + 1))
# Application API tests
run_test "Get room API" "curl -f http://localhost:3001/api/get_room"
run_test "Create room API" "curl -f -X POST -H 'Content-Type: application/json' -d '{\"roomId\":\"test123\"}' http://localhost:3001/api/create_room"
echo ""
}
# WebRTC functionality tests
test_webrtc_functionality() {
echo -e "${BLUE}=== WebRTC Functionality Tests ===${NC}"
# Test frontend page load
if curl -f http://localhost:3002 >/dev/null 2>&1; then
log_success "Frontend page reachable"
TESTS_PASSED=$((TESTS_PASSED + 1))
else
log_error "Frontend page not reachable"
TESTS_FAILED=$((TESTS_FAILED + 1))
fi
TOTAL_TESTS=$((TOTAL_TESTS + 1))
# Test Socket.IO connection (basic)
if curl -f http://localhost:3001/socket.io/socket.io.js >/dev/null 2>&1; then
log_success "Socket.IO client script reachable"
TESTS_PASSED=$((TESTS_PASSED + 1))
else
log_error "Socket.IO client script not reachable"
TESTS_FAILED=$((TESTS_FAILED + 1))
fi
TOTAL_TESTS=$((TOTAL_TESTS + 1))
echo ""
}
# Performance tests
test_performance() {
echo -e "${BLUE}=== Performance Tests ===${NC}"
# Memory usage test
local backend_memory=$(docker stats --no-stream --format "table {{.Container}}\t{{.MemUsage}}" | grep privydrop-backend | awk '{print $2}' | cut -d'/' -f1)
local frontend_memory=$(docker stats --no-stream --format "table {{.Container}}\t{{.MemUsage}}" | grep privydrop-frontend | awk '{print $2}' | cut -d'/' -f1)
if [[ -n "$backend_memory" ]]; then
log_info "Backend memory usage: $backend_memory"
fi
if [[ -n "$frontend_memory" ]]; then
log_info "Frontend memory usage: $frontend_memory"
fi
# Response time test
local response_time=$(curl -o /dev/null -s -w '%{time_total}' http://localhost:3001/health)
if (( $(echo "$response_time < 1.0" | bc -l) )); then
log_success "API response time OK: ${response_time}s"
TESTS_PASSED=$((TESTS_PASSED + 1))
else
log_warning "API response time slow: ${response_time}s"
fi
TOTAL_TESTS=$((TOTAL_TESTS + 1))
echo ""
}
# Security tests
test_security() {
echo -e "${BLUE}=== Security Tests ===${NC}"
# Check container users
local backend_user=$(docker-compose exec -T backend whoami 2>/dev/null || echo "unknown")
local frontend_user=$(docker-compose exec -T frontend whoami 2>/dev/null || echo "unknown")
if [[ "$backend_user" != "root" ]]; then
log_success "Backend container uses non-root user: $backend_user"
TESTS_PASSED=$((TESTS_PASSED + 1))
else
log_warning "Backend container runs as root"
fi
TOTAL_TESTS=$((TOTAL_TESTS + 1))
if [[ "$frontend_user" != "root" ]]; then
log_success "Frontend container uses non-root user: $frontend_user"
TESTS_PASSED=$((TESTS_PASSED + 1))
else
log_warning "Frontend container runs as root"
fi
TOTAL_TESTS=$((TOTAL_TESTS + 1))
# Check for sensitive info leakage
if curl -s http://localhost:3001/health/detailed | grep -q "password\|secret\|key" >/dev/null 2>&1; then
log_warning "Health check API may leak sensitive info"
else
log_success "Health check API does not leak sensitive info"
TESTS_PASSED=$((TESTS_PASSED + 1))
fi
TOTAL_TESTS=$((TOTAL_TESTS + 1))
echo ""
}
# Logging tests
test_logging() {
echo -e "${BLUE}=== Logging Tests ===${NC}"
# Check log directories
if [[ -d "logs" ]]; then
log_success "Log directory exists"
TESTS_PASSED=$((TESTS_PASSED + 1))
else
log_warning "Log directory does not exist"
fi
TOTAL_TESTS=$((TOTAL_TESTS + 1))
# Check log files
local log_files=("logs/backend" "logs/frontend")
for log_dir in "${log_files[@]}"; do
if [[ -d "$log_dir" ]]; then
log_success "Log directory $log_dir exists"
TESTS_PASSED=$((TESTS_PASSED + 1))
else
log_info "Log directory $log_dir not found (may be normal)"
fi
TOTAL_TESTS=$((TOTAL_TESTS + 1))
done
echo ""
}
# Configuration file tests
test_configuration() {
echo -e "${BLUE}=== Configuration File Tests ===${NC}"
# Check env file
if [[ -f ".env" ]]; then
log_success ".env file exists"
TESTS_PASSED=$((TESTS_PASSED + 1))
# Check key configuration entries
local required_vars=("LOCAL_IP" "CORS_ORIGIN" "NEXT_PUBLIC_API_URL")
for var in "${required_vars[@]}"; do
if grep -q "^$var=" .env; then
log_success "Config $var is set"
TESTS_PASSED=$((TESTS_PASSED + 1))
else
log_error "Config $var is not set"
TESTS_FAILED=$((TESTS_FAILED + 1))
fi
TOTAL_TESTS=$((TOTAL_TESTS + 1))
done
else
log_error ".env file not found"
TESTS_FAILED=$((TESTS_FAILED + 1))
fi
TOTAL_TESTS=$((TOTAL_TESTS + 1))
# Check Docker Compose file
if [[ -f "docker-compose.yml" ]]; then
log_success "docker-compose.yml exists"
TESTS_PASSED=$((TESTS_PASSED + 1))
else
log_error "docker-compose.yml not found"
TESTS_FAILED=$((TESTS_FAILED + 1))
fi
TOTAL_TESTS=$((TOTAL_TESTS + 1))
echo ""
}
# Cleanup tests
test_cleanup() {
echo -e "${BLUE}=== Cleanup Tests ===${NC}"
# Verify cleanup commands work
if [[ -f "deploy.sh" ]]; then
log_success "Deployment script exists"
TESTS_PASSED=$((TESTS_PASSED + 1))
# Test help command
if bash deploy.sh --help >/dev/null 2>&1; then
log_success "Deployment script help works"
TESTS_PASSED=$((TESTS_PASSED + 1))
else
log_error "Deployment script help fails"
TESTS_FAILED=$((TESTS_FAILED + 1))
fi
else
log_error "Deployment script not found"
TESTS_FAILED=$((TESTS_FAILED + 1))
fi
TOTAL_TESTS=$((TOTAL_TESTS + 2))
echo ""
}
# Generate test report
generate_report() {
echo -e "${BLUE}=== Test Report ===${NC}"
echo ""
echo "📊 Test stats:"
echo " Total tests: $TOTAL_TESTS"
echo -e " Passed: ${GREEN}$TESTS_PASSED${NC}"
echo -e " Failed: ${RED}$TESTS_FAILED${NC}"
local success_rate=$((TESTS_PASSED * 100 / TOTAL_TESTS))
echo " Success rate: $success_rate%"
echo ""
echo "📋 System info:"
echo " Docker version: $(docker --version)"
echo " Docker Compose version: $(docker-compose --version 2>/dev/null || docker compose version 2>/dev/null || echo 'unknown')"
echo " OS: $(uname -s) $(uname -r)"
echo " Test time: $(date)"
echo ""
if [[ $TESTS_FAILED -eq 0 ]]; then
echo -e "${GREEN}🎉 All tests passed! PrivyDrop deployment successful!${NC}"
echo ""
echo "🔗 Access links:"
echo " Frontend: http://localhost:3002"
echo " Backend API: http://localhost:3001"
# Show LAN access addresses
if [[ -f ".env" ]]; then
local local_ip=$(grep "LOCAL_IP=" .env | cut -d'=' -f2)
if [[ -n "$local_ip" && "$local_ip" != "127.0.0.1" ]]; then
echo ""
echo "🌐 LAN access:"
echo " Frontend: http://$local_ip:3002"
echo " Backend API: http://$local_ip:3001"
fi
fi
return 0
else
echo -e "${RED}$TESTS_FAILED test(s) failed${NC}"
echo ""
echo "🔧 Troubleshooting tips:"
echo " 1. View container status: docker-compose ps"
echo " 2. View container logs: docker-compose logs -f"
echo " 3. Redeploy: bash deploy.sh"
echo " 4. Clean and redeploy: bash deploy.sh --clean"
return 1
fi
}
# Main function
main() {
echo -e "${BLUE}=== PrivyDrop Docker Deployment Tests ===${NC}"
echo ""
# Check required tools
local missing_tools=()
for tool in curl jq bc nc; do
if ! command -v "$tool" >/dev/null 2>&1; then
missing_tools+=("$tool")
fi
done
if [[ ${#missing_tools[@]} -gt 0 ]]; then
log_warning "Missing test tools: ${missing_tools[*]}"
log_info "Suggested install: sudo apt-get install curl jq bc netcat"
echo ""
fi
# Run all tests
test_docker_environment
test_container_status
test_network_connectivity
test_api_functionality
test_webrtc_functionality
test_performance
test_security
test_logging
test_configuration
test_cleanup
# Generate report
generate_report
}
# Trap interrupt signals
trap 'echo -e "\n${YELLOW}Tests interrupted${NC}"; exit 1' INT TERM
# Run main function
main "$@"
+10 -1
View File
@@ -1,4 +1,8 @@
# PrivyDrop Deployment Guide
# PrivyDrop Deployment Guide (Bare-Metal)
> Audience & Scope: This guide targets developers/operators who prefer a non-container (bare-metal) setup.
>
> Recommended: Prefer the one-click Docker deployment for simplicity and robustness, including auto HTTPS and TURN. See [Docker Deployment Guide](./DEPLOYMENT_docker.md).
This guide provides comprehensive instructions for deploying the full-stack PrivyDrop application, including setting up Redis, a TURN server, the backend service, the frontend application, and configuring Nginx as a reverse proxy.
@@ -30,6 +34,7 @@ sudo bash backend/docker/env_install.sh
```
This script will automatically install:
- **Node.js v20** - Runtime environment
- **Redis Server** - Used for room management and caching
- **Coturn** - TURN/STUN server (optional, for NAT traversal)
@@ -38,6 +43,7 @@ This script will automatically install:
- **Certbot** - SSL certificate management
After installation, you can verify the services:
```bash
# Verify Node.js version
node -v
@@ -53,11 +59,13 @@ sudo systemctl status coturn
```
**Configuration Notes:**
- **Redis Configuration:** Default listening on `127.0.0.1:6379`, ensure your backend `.env` file includes correct `REDIS_HOST` and `REDIS_PORT`
- **TURN Service:** Optional configuration, PrivyDrop uses public STUN servers by default, only needed for extremely high NAT traversal requirements
- **Nginx:** Script installs official version and verifies stream module support
**TURN Server Firewall Configuration (if configuring TURN service):**
```bash
# Enable the Coturn service
sudo sed -i 's/#TURNSERVER_ENABLED=1/TURNSERVER_ENABLED=1/' /etc/default/coturn
@@ -68,6 +76,7 @@ sudo ufw reload
```
The ports seen via `sudo ufw app info Turnserver` are as follows:
- `3478,3479,5349,5350,49152:65535/tcp`
- `3478,3479,5349,5350,49152:65535/udp`
+15 -6
View File
@@ -1,4 +1,8 @@
# Privydrop 部署指南
# Privydrop 部署指南(裸机部署)
> 说明与定位:本指南面向具备 Linux 运维能力的开发者,介绍“裸机(非容器)”部署方式。
>
> 推荐方案:优先使用“一键 Docker 部署”,更简单、更稳健,支持自动签发/续期证书与 TURN。详见 [Docker 部署指南](./DEPLOYMENT_docker.zh-CN.md)。
本指南提供部署 Privydrop 全栈应用的全面说明,包括设置 Redis、TURN 服务器、后端服务、前端应用以及配置 Nginx 作为反向代理。
@@ -30,14 +34,16 @@ sudo bash backend/docker/env_install.sh
```
该脚本将自动安装:
- **Node.js v20** - 运行环境
- **Redis Server** - 用于房间管理和缓存
- **Coturn** - TURN/STUN 服务器(可选,用于NAT穿透)
- **Redis Server** - 用于房间管理和缓存
- **Coturn** - TURN/STUN 服务器(可选,用于 NAT 穿透)
- **Nginx** - Web 服务器和反向代理(使用官方仓库)
- **PM2** - Node.js 进程管理器
- **Certbot** - SSL 证书管理
安装完成后,可以验证各服务状态:
```bash
# 验证 Node.js 版本
node -v
@@ -53,11 +59,13 @@ sudo systemctl status coturn
```
**注意事项:**
- **Redis配置:** 默认监听 `127.0.0.1:6379`,请确保后端 `.env` 文件中包含正确的 `REDIS_HOST``REDIS_PORT`
- **TURN服务:** 为可选配置,Privydrop 默认使用公共 STUN 服务器,只有对 NAT 穿透有极高要求时才需要配置
- **Redis 配置:** 默认监听 `127.0.0.1:6379`,请确保后端 `.env` 文件中包含正确的 `REDIS_HOST``REDIS_PORT`
- **TURN 服务:** 为可选配置,Privydrop 默认使用公共 STUN 服务器,只有对 NAT 穿透有极高要求时才需要配置
- **Nginx** 脚本安装官方版本并验证 stream 模块支持
**TURN服务器防火墙配置(如果需要配置TURN服务):**
**TURN 服务器防火墙配置(如果需要配置 TURN 服务):**
```bash
# 启用 Coturn 服务
sudo sed -i 's/#TURNSERVER_ENABLED=1/TURNSERVER_ENABLED=1/' /etc/default/coturn
@@ -68,6 +76,7 @@ sudo ufw reload
```
通过 `sudo ufw app info Turnserver` 看到的端口如下:
- `3478,3479,5349,5350,49152:65535/tcp`
- `3478,3479,5349,5350,49152:65535/udp`
+564
View File
@@ -0,0 +1,564 @@
# PrivyDrop Docker One-Click Deployment (Recommended)
This guide provides a one-click Docker deployment for PrivyDrop. It supports both private and public networks, automates config/build/start, and provisions HTTPS certificates.
## 🚀 Quick Start (Top)
```bash
# Private LAN (no domain/public IP)
bash ./deploy.sh --mode lan-http
# Private LAN + TURN (for complex NAT/LAN)
bash ./deploy.sh --mode lan-http --with-turn
# LAN HTTPS (self-signed; dev/managed env; explicitly enable 8443)
bash ./deploy.sh --mode lan-tls --enable-web-https --with-nginx
# Public IP without domain (with TURN)
bash ./deploy.sh --mode public --with-turn
# Public domain (HTTPS + Nginx + TURN + SNI 443, auto-issue/renew certs)
bash ./deploy.sh --mode full --domain your-domain.com --with-nginx --with-turn --le-email you@domain.com
```
- Requires Docker Compose v2 (command `docker compose`).
- In full mode, Lets Encrypt (webroot) is auto-issued and auto-renewed (no downtime); SNI 443 multiplexing is enabled by default (`turn.your-domain.com` → coturn:5349; others → web:8443).
## Modes Overview
- lan-http: Intranet HTTP; fastest to start; no TLS
- lan-tls: Intranet HTTPS (self-signed; dev/managed env); 8443 disabled by default; enable via `--enable-web-https`; HSTS disabled; turns:443 not guaranteed
- public: Public HTTP + TURN; works without a domain (no HTTPS/turns:443)
- full: Domain + HTTPS (Lets Encrypt auto-issue/renew) + TURN; SNI 443 split enabled by default (use `--no-sni443` to disable)
## 🎯 Deployment Advantages
Compared to traditional deployment methods, Docker deployment offers the following advantages:
| Comparison | Traditional Deployment | Docker Deployment |
| ---------------------------- | ------------------------------- | ------------------------------ |
| **Deploy Time** | 30-60 minutes | 5 minutes |
| **Technical Requirements** | Linux ops experience | Basic Docker knowledge |
| **Environment Requirements** | Public IP + Domain | Works on private networks |
| **Configuration Complexity** | 10+ manual steps | One-click auto configuration |
| **Success Rate** | ~70% | >95% |
| **Maintenance Difficulty** | Manual multi-service management | Automatic container management |
## 📋 System Requirements
### Minimum Configuration
- **CPU**: 1 core
- **Memory**: 512MB
- **Disk**: 2GB available space
- **Network**: Any network environment (private/public)
### Recommended Configuration
- **CPU**: 2+ cores
- **Memory**: 1GB+
- **Disk**: 5GB+ available space
- **Network**: 100Mbps+
### Software Dependencies
- Docker 20.10+
- Docker Compose 2.x (command `docker compose`)
- curl (for health checks, optional)
- openssl (cert tools; the script auto-installs certbot)
## 🚀 Quick Start
### 1. Get the Code
```bash
# Clone the project
git clone https://github.com/david-bai00/PrivyDrop.git
cd PrivyDrop
```
### 2. One-Click Deployment
```bash
# Basic deployment (recommended for beginners)
bash ./deploy.sh
```
That's it! 🎉
## 📚 Deployment Modes
### LAN HTTP (lan-http)
**Use Case**: Private network file transfer, personal use, testing environment
```bash
bash ./deploy.sh --mode lan-http
```
**Features**:
- ✅ HTTP access
- ✅ Private network P2P transfer
- ✅ Uses public STUN servers
- ✅ Zero configuration startup
### Public Mode
**Use Case**: Servers with public IP but no domain
```bash
bash ./deploy.sh --mode public --with-turn --with-nginx
```
**Features**:
- ✅ HTTP access
- ✅ Built-in TURN server
- ✅ Supports complex network environments
- ✅ Automatic NAT traversal configuration
### Full Mode (full)
**Use Case**: Production environment, public servers with domain
```bash
bash ./deploy.sh --mode full --domain your-domain.com --with-nginx --with-turn --le-email you@domain.com
```
**Features**:
- ✅ HTTPS secure access (Lets Encrypt auto-issue/renew, zero downtime)
- ✅ Nginx reverse proxy
- ✅ Built-in TURN server (default port range 49152-49252/udp)
- ✅ SNI 443 multiplexing (turn.<domain> → coturn:5349; others → web:8443)
- ✅ Complete production setup
> Tip: The script no longer auto-detects the deployment mode; always pass `--mode lan-http|lan-tls|public|full`. If the detected LAN IP is not the one you expect, add `--local-ip 192.168.x.x` to override.
## 🔧 Advanced Configuration
### Custom Ports
```bash
# Modify .env file
FRONTEND_PORT=8080
BACKEND_PORT=8081
HTTP_PORT=8000
```
### Build-Time Proxy (optional)
Set the following variables in `.env` (or export them before running `deploy.sh`) when the build needs to go through a proxy. The configuration generator now preserves these fields on subsequent runs.
```bash
HTTP_PROXY=http://your-proxy:7890
HTTPS_PROXY=http://your-proxy:7890
NO_PROXY=localhost,127.0.0.1,backend,frontend,redis,coturn
```
`docker compose` passes these values as build args; the Dockerfiles expose them as environment variables so `npm`/`pnpm` automatically reuse the proxy. Leave them blank if you don't need a proxy.
### Common Flags
```bash
# Enable only Nginx reverse proxy
bash ./deploy.sh --with-nginx
# Enable TURN (recommended in public/full)
bash ./deploy.sh --with-turn
# Explicitly enable SNI 443 (auto-enabled in full+domain; use --no-sni443 to disable)
bash ./deploy.sh --with-sni443
# Adjust TURN port range (default 49152-49252/udp)
bash ./deploy.sh --mode full --with-turn --turn-port-range 55000-55100
```
## 🌐 Access Methods
- With Nginx (recommended, same-origin gateway)
- lan-http/public: `http://localhost` (or `http://<public IP>`)
- lan-tls (with `--enable-web-https`): `https://localhost:8443` (or `https://<LAN IP>:8443`)
- full (with domain): `https://<your-domain>` (443)
- Health checks: `curl -fsS http://localhost/api/health` (lan-http/public), `curl -kfsS https://localhost:8443/api/health` (lan-tls+https), `curl -fsS https://<domain>/api/health` (full)
- Without Nginx (direct ports, for debugging only)
- Frontend: `http://localhost:3002` (or `http://<LAN IP>:3002`)
- API: `http://localhost:3001` (or `http://<LAN IP>:3001`)
- Note: direct ports may cause CORS or 404 in production/public setups and are not recommended for public access.
### HTTPS Access (lan-tls/full)
- lan-tls: with `--enable-web-https`, access via `https://localhost:8443` (certs in `docker/ssl/`). Import `docker/ssl/ca-cert.pem` into your browser or trust store on first use.
- full: after Lets Encrypt issuance, access via `https://<your-domain>` (443). Certs auto-issue/renew; hot-reload is handled via deploy hook.
## 🔍 Management Commands
### View Service Status
```bash
docker compose ps
```
### View Service Logs
```bash
# View all service logs
docker compose logs -f
# View specific service logs
docker compose logs -f backend
docker compose logs -f frontend
docker compose logs -f redis
```
### Restart Services
```bash
# Restart all services
docker compose restart
# Restart specific service
docker compose restart backend
```
### Stop Services
```bash
# Stop services but keep data
docker compose stop
# Stop services and remove containers
docker compose down
```
### Complete Cleanup
```bash
# Clean all containers, images and data
bash ./deploy.sh --clean
```
## 🛠️ Troubleshooting
### Common Issues
#### 1. Port Already in Use
**Symptom**: Deployment shows port occupation warning
```
⚠️ The following ports are already in use: 3002, 3001
```
**Solution**:
```bash
# First try cleaning previous containers
bash ./deploy.sh --clean # or docker compose down
# If the port is still occupied, locate the process
sudo ss -tulpn | grep :3002
sudo kill -9 <PID>
# Finally, adjust the exposed ports in .env if necessary
vim .env # Update FRONTEND_PORT / BACKEND_PORT
```
#### 2. Insufficient Memory
**Symptom**: Containers fail to start or restart frequently
**Solution**:
```bash
# Check memory usage
free -h
# Add swap space (temporary solution)
sudo fallocate -l 1G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
```
#### 3. Docker Permission Issues
**Symptom**: Permission denied errors
**Solution**:
```bash
# Add user to docker group
sudo usermod -aG docker $USER
# Re-login or refresh group permissions
newgrp docker
```
#### 4. Service Inaccessible
**Symptom**: Browser cannot open pages
**Solution**:
```bash
# 1. Check service status
docker compose ps
# 2. Check health status
curl http://localhost:3001/health
curl http://localhost:3002/api/health
# 3. View detailed logs
docker compose logs -f
# 4. Check firewall
sudo ufw status
```
#### 5. WebRTC Connection Failure
**Symptom**: Cannot establish P2P connections
**Solution**:
```bash
# Enable TURN server
bash ./deploy.sh --with-turn
# Check network connectivity
curl -I http://localhost:3001/api/get_room
```
### Health Checks
The project provides comprehensive health check functionality:
```bash
# Run health check tests
bash test-health-apis.sh
# Manual service checks
curl http://localhost:3001/health # Backend basic check
curl http://localhost:3001/health/detailed # Backend detailed check
curl http://localhost:3002/api/health # Frontend check
```
### Performance Monitoring
```bash
# View container resource usage
docker stats
# View disk usage
docker system df
# Clean unused resources
docker system prune -f
```
## 📊 Performance Optimization
### Production Environment Optimization
1. **Enable Nginx Caching**:
```bash
bash deploy.sh --with-nginx
```
2. **Configure Resource Limits**:
```yaml
# Add to docker-compose.yml
services:
backend:
deploy:
resources:
limits:
memory: 256M
reservations:
memory: 128M
```
3. **Enable Log Rotation**:
```bash
# Configure log size limits
echo '{"log-driver":"json-file","log-opts":{"max-size":"10m","max-file":"3"}}' | sudo tee /etc/docker/daemon.json
sudo systemctl restart docker
```
### Network Optimization
1. **Use Dedicated Network**:
```yaml
networks:
privydrop-network:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
```
2. **Enable HTTP/2**:
```bash
# Auto-enabled (requires HTTPS)
bash deploy.sh --mode full --with-nginx
```
## 🔒 Security Configuration
### LAN HTTPS (lan-tls, self-signed, dev/managed env)
- 8443 is disabled by default; explicitly enable with:
```bash
bash ./deploy.sh --mode lan-tls --enable-web-https --with-nginx
```
- For development or managed devices only (internal CA trusted fleet-wide); HSTS disabled; `turns:443` not guaranteed. For restricted networks (443-only), use full (domain + trusted cert + SNI 443).
Usage (strongly recommended)
1) Import the self-signed CA (required)
- Location: `docker/ssl/ca-cert.pem`
- Browser import:
- Chrome/Edge: Settings → Privacy & Security → Security → Manage certificates → “Trusted Root Certification Authorities” → Import `ca-cert.pem`
- macOS: Keychain Access → System → Certificates → Import `ca-cert.pem` → set to “Always Trust”
- Linux (system-wide):
- `sudo cp docker/ssl/ca-cert.pem /usr/local/share/ca-certificates/privydrop-ca.crt`
- `sudo update-ca-certificates`
- Without trusting the CA, browser HTTPS will show untrusted cert warnings and API requests will fail.
2) Access endpoints (default ports and paths)
- Nginx reverse proxy: `http://localhost`
- HTTPS (Web): `https://localhost:8443`, `https://<LAN IP>:8443`
- Frontend direct (optional): `http://localhost:3002`, `http://<LAN IP>:3002`
- Note: In lan-tls, 443 is not open; HTTPS uses 8443.
3) CORS
- For convenience, common dev origins are allowed by default: `https://<LAN IP>:8443`, `https://localhost:8443`, `http://localhost`, `http://<LAN IP>`, `http://localhost:3002`, `http://<LAN IP>:3002`.
- To minimize allowed origins, edit `CORS_ORIGIN` in `.env` and then `docker compose restart backend`.
4) Health checks
- `curl -kfsS https://localhost:8443/api/health` → 200
- `bash ./test-health-apis.sh` → all tests should pass (frontend container trusts the self-signed CA).
5) Deployment hints
- The script prints only reachable Nginx endpoints; in lan-tls it will show `https://localhost:8443` (and `https://<LAN IP>:8443` if available).
### Public Domain Deployment (HTTPS + Nginx) — Quick Test
1) Point your domain A record to the server IP (optional: also `turn.<your-domain>` to the same IP)
2) Run:
```bash
./deploy.sh --mode full --domain <your-domain> --with-nginx --with-turn --le-email you@domain.com
```
3) Open ports: `80`, `443`, `3478/udp`, `5349/tcp`, `5349/udp`
4) Verify: visit `https://<your-domain>`, `/api/health` returns 200; open `chrome://webrtc-internals` and check for `relay` candidates (TURN)
### SSL/TLS Automation (Lets Encrypt)
In full mode, certificates are auto-issued and auto-renewed:
- Initial issuance: webroot (no downtime); system certs live under `/etc/letsencrypt/live/<domain>/`; copied to `docker/ssl/` and 443 is enabled.
- Renewal: `certbot.timer` or `/etc/cron.d/certbot` runs daily; the deploy-hook copies new certs to `docker/ssl/` and hot-reloads Nginx/Coturn.
- Lineage suffixes (-0001/-0002) are handled automatically.
### Network Security
1. **Firewall Configuration**:
```bash
# Ubuntu/Debian
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 3478/udp # TURN server
```
2. **Container Network Isolation**:
- All services run in isolated networks
- Only necessary ports exposed
- Internal services communicate using container names
## 📈 Monitoring and Logging
### Log Management
All service logs are centrally stored in the `logs/` directory:
```
logs/
├── nginx/ # Nginx access and error logs
├── backend/ # Backend application logs
├── frontend/ # Frontend application logs
└── coturn/ # TURN server logs
```
## 🔄 Updates and Maintenance
### Update Application
```bash
# Pull latest code
git pull origin main
# Redeploy
bash deploy.sh
```
### Data Backup
```bash
# Backup Redis data
docker compose exec redis redis-cli BGSAVE
# Backup SSL certificates
tar -czf ssl-backup.tar.gz docker/ssl/
# Backup configuration files
cp .env .env.backup
```
### Regular Maintenance
```bash
# Clean unused images and containers
docker system prune -f
# Update base images
docker compose pull
docker compose up -d
```
## 🆘 Getting Help
### Command Line Help
```bash
bash ./deploy.sh --help
### Additional Notes
- In Docker environments, Next.js Image optimization is disabled by default (`NEXT_IMAGE_UNOPTIMIZED=true`) to avoid container loopback fetch failures on `/_next/image`. To enable it, set the variable to `false` and rebuild.
- With `--with-nginx`, the frontend is built to use same-origin API (`/api`, `/socket.io/`). Use the gateway URLs printed by the script; direct ports `:3002/:3001` are not recommended in production.
```
### Online Resources
- [Project Homepage](https://github.com/david-bai00/PrivyDrop)
- [Live Demo](https://www.privydrop.app/)
- [Issue Reporting](https://github.com/david-bai00/PrivyDrop/issues)
### Community Support
- GitHub Issues: Technical questions and bug reports
+566
View File
@@ -0,0 +1,566 @@
# PrivyDrop Docker 一键部署(推荐)
本指南提供 PrivyDrop 的 Docker 一键部署方案,支持内网与公网,一次命令完成配置、构建、启动与证书自动化。
## 🚀 快速开始(置顶)
```bash
# 内网(无域名/无公网IP
bash ./deploy.sh --mode lan-http
# 内网 + TURN(推荐用于复杂内网/NAT)
bash ./deploy.sh --mode lan-http --with-turn
# 内网 HTTPS(自签,开发/受管环境,需显式开启 8443)
bash ./deploy.sh --mode lan-tls --enable-web-https --with-nginx
# 公网IP(无域名),含 TURN
bash ./deploy.sh --mode public --with-turn
# 公网域名(HTTPS + Nginx + TURN + SNI 443 分流,自动申请/续期证书)
bash ./deploy.sh --mode full --domain your-domain.com --with-nginx --with-turn --le-email you@domain.com
```
- 使用 Docker Compose V2(命令 `docker compose`)。
- full 模式自动申请 Lets Encrypt 证书(webroot,无停机)并自动续期;默认启用 SNI 443 分流(`turn.your-domain.com` → coturn:5349,其余 → web:8443)。
## 模式一览
- lan-http:内网 HTTP;最快上手,默认不启用 TLS
- lan-tls:内网 HTTPS(自签,仅开发/受管环境);默认不启 8443,需 `--enable-web-https` 显式开启;禁用 HSTS;不保证 turns:443
- public:公网 HTTP;开启 TURN;无域名也可使用(不提供 HTTPS/turns:443
- full:域名 + HTTPSLets Encrypt 自动签发/续期)+ TURN;默认启用 SNI 443 分流(可 `--no-sni443` 关闭)
## 🎯 部署优势
相比传统部署方式,Docker 部署具有以下优势:
| 对比项目 | 传统部署 | Docker 部署 |
| -------------- | -------------------- | ---------------- |
| **部署时间** | 30-60 分钟 | 5 分钟 |
| **技术要求** | Linux 运维经验 | 会用 Docker 即可 |
| **环境要求** | 公网 IP + 域名 | 内网即可使用 |
| **配置复杂度** | 10+个手动步骤 | 一键自动配置 |
| **成功率** | ~70% | >95% |
| **维护难度** | 需要手动管理多个服务 | 容器自动管理 |
## 📋 系统要求
### 最低配置
- **CPU**: 1 核
- **内存**: 512MB
- **磁盘**: 2GB 可用空间
- **网络**: 任意网络环境(内网/公网均可)
### 推荐配置
- **CPU**: 2 核及以上
- **内存**: 1GB 及以上
- **磁盘**: 5GB 及以上可用空间
- **网络**: 100Mbps 及以上
### 软件依赖
- Docker 20.10+
- Docker Compose 2.x(命令 `docker compose`
- curl(用于健康检查,可选)
- openssl(用于证书工具,脚本会自动安装 certbot)
## 🚀 快速开始
### 1. 获取代码
```bash
# 克隆项目
git clone https://github.com/david-bai00/PrivyDrop.git
cd PrivyDrop
```
### 2. 一键部署(示例)
```bash
# 示例:公网域名(HTTPS + Nginx + TURN
bash ./deploy.sh --mode full --domain your-domain.com --with-nginx --with-turn --le-email you@domain.com
```
## 📚 部署模式详解
### lan-http(内网 HTTP
**适用场景**: 内网文件传输、个人使用、测试环境
```bash
bash ./deploy.sh --mode lan-http
```
**特性**:
- ✅ HTTP 访问
- ✅ 内网 P2P 传输
- ✅ 使用公共 STUN 服务器
- ✅ 零配置启动
### 公网模式
**适用场景**: 有公网 IP 但无域名的服务器
```bash
bash ./deploy.sh --mode public --with-turn --with-nginx
```
**特性**:
- ✅ HTTP 访问
- ✅ 内置 TURN 服务器
- ✅ 支持复杂网络环境
- ✅ 自动配置 NAT 穿透
### 完整模式(full
**适用场景**: 生产环境、有域名的公网服务器
```bash
bash ./deploy.sh --mode full --domain your-domain.com --with-nginx --with-turn --le-email you@domain.com
```
**特性**:
- ✅ HTTPS 安全访问(Lets Encrypt 自动签发/续期,无停机)
- ✅ Nginx 反向代理
- ✅ 内置 TURN 服务器(默认端口段 49152-49252/udp,可覆盖)
- ✅ SNI 443 分流(turn.<domain> → coturn:5349,其余 → web:8443
- ✅ 完整生产环境配置
> 提示:脚本不再自动判断部署模式,请显式传递 `--mode lan-http|lan-tls|public|full`。若自动检测到的局域网 IP 与预期不符,可使用 `--local-ip 192.168.x.x` 进行覆盖。
## 🔧 高级配置
### 自定义端口
```bash
# 修改 .env 文件
FRONTEND_PORT=8080
BACKEND_PORT=8081
HTTP_PORT=8000
```
### 构建阶段代理(可选)
若需要在 Docker 构建时走网络代理,可在 `.env` 中设置以下变量,或者在执行 `deploy.sh` 之前通过环境变量导出。重新运行配置脚本时,这些字段会被保留:
```bash
HTTP_PROXY=http://你的代理:7890
HTTPS_PROXY=http://你的代理:7890
NO_PROXY=localhost,127.0.0.1,backend,frontend,redis,coturn
```
`docker compose` 会把这些变量作为 build args 传递给前后端镜像,Dockerfile 中会自动设置为环境变量,从而让 `npm`/`pnpm` 使用代理。若无需代理,保持为空即可。
### 常用开关
```bash
# 仅启用 Nginx
bash ./deploy.sh --with-nginx
# 启用 TURNpublic/full 建议)
bash ./deploy.sh --with-turn
# 显式启用 SNI 443full+domain 默认开启,可用 --no-sni443 关闭)
bash ./deploy.sh --with-sni443
# 调整 TURN 端口段(默认 49152-49252/udp
bash ./deploy.sh --mode full --with-turn --turn-port-range 55000-55100
```
## 🌐 访问方式
- 启用 Nginx(推荐,同源网关)
- lan-http/public`http://localhost`(或 `http://<公网IP>`
- lan-tls(启用 `--enable-web-https`):`https://localhost:8443`(或 `https://<LAN IP>:8443`
- full(有域名):`https://<your-domain>`443
- 健康检查:`curl -fsS http://localhost/api/health`lan-http/public),`curl -kfsS https://localhost:8443/api/health`lan-tls+https),`curl -fsS https://<domain>/api/health`full
- 未启用 Nginx(直连端口,仅调试)
- 前端:`http://localhost:3002`(或 `http://<LAN IP>:3002`
- API`http://localhost:3001`(或 `http://<LAN IP>:3001`
- 注意:直连端口在生产/公网可能导致 CORS 或 404,不推荐对外使用。
### HTTPS 访问(lan-tls/full
- lan-tls:开启 `--enable-web-https` 后通过 `https://localhost:8443` 访问(证书在 `docker/ssl/`)。首次访问需导入 `docker/ssl/ca-cert.pem` 到浏览器或系统信任。
- full:签发 Lets Encrypt 后通过 `https://<your-domain>` 访问(443)。
## 🔍 管理命令
### 查看服务状态
```bash
docker compose ps
```
### 查看服务日志
```bash
# 查看所有服务日志
docker compose logs -f
# 查看特定服务日志
docker compose logs -f backend
docker compose logs -f frontend
docker compose logs -f redis
```
### 重启服务
```bash
# 重启所有服务
docker compose restart
# 重启特定服务
docker compose restart backend
```
### 停止服务
```bash
# 停止服务但保留数据
docker compose stop
# 停止服务并删除容器
docker compose down
```
### 完全清理
```bash
# 清理所有容器、镜像和数据
bash ./deploy.sh --clean
```
## 🛠️ 故障排除
### 常见问题
#### 1. 端口被占用
**现象**: 部署时提示端口已被占用
```
⚠️ 以下端口已被占用: 3002, 3001
```
**解决方案**:
```bash
# 方法1: 清理旧容器
bash ./deploy.sh --clean # 或 docker compose down
# 方法2: 查找并结束占用进程
sudo ss -tulpn | grep :3002
sudo kill -9 <PID>
# 方法3: 如仍冲突,再调整端口
vim .env # 修改 FRONTEND_PORT / BACKEND_PORT
```
#### 2. 内存不足
**现象**: 容器启动失败或频繁重启
**解决方案**:
```bash
# 检查内存使用
free -h
# 添加交换空间 (临时解决)
sudo fallocate -l 1G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
```
#### 3. Docker 权限问题
**现象**: 提示权限不足
**解决方案**:
```bash
# 将用户添加到docker组
sudo usermod -aG docker $USER
# 重新登录或刷新组权限
newgrp docker
```
#### 4. 服务无法访问
**现象**: 浏览器无法打开页面
**解决方案**:
```bash
# 1. 检查服务状态
docker compose ps
# 2. 检查健康状态
curl http://localhost:3001/health
curl http://localhost:3002/api/health
# 3. 查看详细日志
docker compose logs -f
# 4. 检查防火墙
sudo ufw status
```
#### 5. WebRTC 连接失败
**现象**: 无法建立 P2P 连接
**解决方案**:
```bash
# 启用TURN服务器
bash ./deploy.sh --with-turn
# 检查网络连接
curl -I http://localhost:3001/api/get_room
```
### 健康检查
项目提供了完整的健康检查功能:
```bash
# 运行健康检查测试
bash ./test-health-apis.sh
# 手动检查各服务(直连端口)
curl http://localhost:3001/health # 后端基础检查(直连)
curl http://localhost:3001/health/detailed # 后端详细检查(直连)
curl http://localhost:3002/api/health # 前端检查(直连)
# 同源网关(启用 Nginx
curl -fsS http://localhost/api/health # lan-http/public
curl -kfsS https://localhost:8443/api/health # lan-tls (https)
```
### 性能监控
```bash
# 查看容器资源使用
docker stats
# 查看磁盘使用
docker system df
# 清理未使用的资源
docker system prune -f
```
## 📊 性能优化
### 生产环境优化
1. **启用 Nginx 缓存**:
```bash
bash ./deploy.sh --with-nginx
```
2. **配置资源限制**:
```yaml
# 在 docker-compose.yml 中添加
services:
backend:
deploy:
resources:
limits:
memory: 256M
reservations:
memory: 128M
```
3. **启用日志轮转**:
```bash
# 配置日志大小限制
echo '{"log-driver":"json-file","log-opts":{"max-size":"10m","max-file":"3"}}' | sudo tee /etc/docker/daemon.json
sudo systemctl restart docker
```
### 网络优化
1. **使用专用网络**:
```yaml
networks:
privydrop-network:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
```
2. **启用 HTTP/2**:
```bash
# 自动启用 (需要 HTTPS)
bash ./deploy.sh --mode full --with-nginx
```
## 🔒 HTTPS 与安全
### 内网 HTTPSlan-tls,自签,开发/受管环境)
- 默认不启 8443;需 `--enable-web-https` 显式开启:
```bash
bash ./deploy.sh --mode lan-tls --enable-web-https --with-nginx
```
- 仅用于开发或受管终端(全员导入内部 CA);禁用 HSTS;不保证 `turns:443`;受限网络(仅 443 出口)应使用 full(域名 + 受信证书 + SNI 443)。
使用说明(强烈建议逐条完成)
1) 导入自签 CA 证书(必做)
- 证书位置:`docker/ssl/ca-cert.pem`
- 浏览器导入:
- Chrome/Edge:设置 → 隐私与安全 → 安全 → 管理证书 → “受信任的根证书颁发机构” → 导入 `ca-cert.pem`
- macOS:钥匙串访问 → System → 证书 → 导入 `ca-cert.pem` → 设置“始终信任”
- Linux(系统层):
- `sudo cp docker/ssl/ca-cert.pem /usr/local/share/ca-certificates/privydrop-ca.crt`
- `sudo update-ca-certificates`
- 未导入时,浏览器访问 HTTPS 会提示“证书无效/不受信任”,API 请求也会失败。
2) 访问方式(默认端口与路径)
- Nginx 反代:`http://localhost`
- HTTPSWeb):`https://localhost:8443``https://<局域网IP>:8443`
- 前端直连(可选):`http://localhost:3002``http://<局域网IP>:3002`
- 说明:lan-tls 下未开启 443HTTPS 统一走 8443。
3) 跨域(CORS)说明
- 为方便开发与调试,脚本已默认放开常见来源:`https://<局域网IP>:8443``https://localhost:8443``http://localhost``http://<局域网IP>``http://localhost:3002``http://<局域网IP>:3002`
- 若仍需最小化来源,请在 `.env` 中精准收敛 `CORS_ORIGIN`,并 `docker compose restart backend`
4) 健康检查
- `curl -kfsS https://localhost:8443/api/health` → 200
- `bash ./test-health-apis.sh` → 所有测试应通过(前端 detailed 健康已在容器内信任自签 CA)。
5) 部署提示
- 脚本会只显示可访问的 Nginx 入口;lan-tls 下将显示明确的 `https://localhost:8443`(如存在局域网 IP 也将显示 `https://<IP>:8443`)。
### 公网域名部署(HTTPS + Nginx)快速测试
1) 将域名 A 记录解析至服务器 IP(可选:`turn.<your-domain>` 指向相同 IP
2) 运行:
```bash
./deploy.sh --mode full --domain <your-domain> --with-nginx --with-turn --le-email you@domain.com
```
3) 放行端口:`80`, `443`, `3478/udp`, `5349/tcp`, `5349/udp`
4) 验证:访问 `https://<your-domain>``/api/health` 返回 200;打开浏览器 `webrtc-internals` 观察是否出现 `relay` 候选(TURN
### 证书自动化(Lets Encrypt
full 模式自动申请并续期证书:
- 首次签发:webroot 模式(无停机),系统证书在 `/etc/letsencrypt/live/<domain>/`,脚本复制到 `docker/ssl/` 并启用 443
- 续期:`certbot.timer``/etc/cron.d/certbot` 每日尝试 `certbot renew`deploy-hook 自动复制新证书并热重载 Nginx/Coturn
- 证书谱系(-0001/-0002)已自动适配,无需手动处理。
### 网络安全
1. **防火墙配置**:
```bash
# Ubuntu/Debian
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 3478/udp # TURN服务器
```
2. **容器网络隔离**:
- 所有服务运行在独立网络中
- 仅暴露必要端口
- 内部服务使用容器名通信
## 📈 监控和日志
### 日志管理
所有服务日志统一存储在 `logs/` 目录:
```
logs/
├── nginx/ # Nginx访问和错误日志
├── backend/ # 后端应用日志
├── frontend/ # 前端应用日志
└── coturn/ # TURN服务器日志
```
## 🔄 更新和维护
### 更新应用
```bash
# 拉取最新代码
git pull origin main
# 重新部署
bash ./deploy.sh
```
### 数据备份
```bash
# 备份Redis数据
docker compose exec redis redis-cli BGSAVE
# 备份SSL证书
tar -czf ssl-backup.tar.gz docker/ssl/
# 备份配置文件
cp .env .env.backup
```
### 定期维护
```bash
# 清理未使用的镜像和容器
docker system prune -f
# 更新基础镜像
docker compose pull
docker compose up -d
```
## 🆘 获取帮助
### 命令行帮助
```bash
bash ./deploy.sh --help
```
### 在线资源
- [项目主页](https://github.com/david-bai00/PrivyDrop)
- [在线演示](https://www.privydrop.app/)
- [问题反馈](https://github.com/david-bai00/PrivyDrop/issues)
### 社区支持
- GitHub Issues: 技术问题和 bug 报告
### 其他提示
- Docker 环境默认关闭 Next.js Image 优化(`NEXT_IMAGE_UNOPTIMIZED=true`),以避免容器内对公网 IP 回环抓取导致 `/_next/image` 502;如需开启,将其设为 `false` 并重新构建。
- 启用 `--with-nginx` 时,前端会构建为同源 API(相对路径 `/api``/socket.io/`);请优先使用脚本输出的网关入口,不要直接使用 `:3002/:3001` 对外访问,否则可能触发 CORS 或 404。
+15
View File
@@ -0,0 +1,15 @@
node_modules
.next
.git
.gitignore
README.md
Dockerfile
.dockerignore
.env*
npm-debug.log*
.npm
coverage
.nyc_output
*.log
public/sw.js
public/workbox-*.js
+91
View File
@@ -0,0 +1,91 @@
# Multi-stage build — build stage
FROM node:18-alpine AS builder
ARG HTTP_PROXY
ARG HTTPS_PROXY
ARG NO_PROXY
ENV http_proxy ${HTTP_PROXY} \
https_proxy ${HTTPS_PROXY} \
no_proxy ${NO_PROXY}
WORKDIR /app
# Copy package files
COPY package*.json ./
COPY pnpm-lock.yaml ./
# Install pnpm
RUN npm install -g pnpm --no-audit --no-fund
# Install dependencies
RUN pnpm install --frozen-lockfile
# Copy source code
COPY . .
# Declare and use build-time public vars after deps installation to avoid cache invalidation when only API/TURN change
ARG NEXT_PUBLIC_API_URL
ARG NEXT_PUBLIC_TURN_HOST
ARG NEXT_PUBLIC_TURN_USERNAME
ARG NEXT_PUBLIC_TURN_PASSWORD
ARG NEXT_IMAGE_UNOPTIMIZED
# Inject public env vars during frontend build (for client direct access to backend and TURN)
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
ENV NEXT_PUBLIC_TURN_HOST=${NEXT_PUBLIC_TURN_HOST}
ENV NEXT_PUBLIC_TURN_USERNAME=${NEXT_PUBLIC_TURN_USERNAME}
ENV NEXT_PUBLIC_TURN_PASSWORD=${NEXT_PUBLIC_TURN_PASSWORD}
ENV NEXT_IMAGE_UNOPTIMIZED=${NEXT_IMAGE_UNOPTIMIZED}
# Set environment variables
ENV NEXT_TELEMETRY_DISABLED 1
ENV NODE_ENV production
# Build the app
RUN pnpm build
# Production stage
FROM node:18-alpine AS runner
WORKDIR /app
# Create a non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nextjs -u 1001 -G nodejs
# Copy build artifacts
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY health-check.js ./
# Set environment variables
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1
ENV PORT 3002
ENV HOSTNAME "0.0.0.0"
USER nextjs
# Expose ports
EXPOSE 3002
# Use a Node.js script for health checks (instead of curl)
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD node health-check.js
# Start the app
CMD ["node", "server.js"]
# Keep public env vars at runtime (optional; helps SSR read them)
# Re-declare ARGs in this stage so they can expand into ENV
ARG NEXT_PUBLIC_API_URL
ARG NEXT_PUBLIC_TURN_HOST
ARG NEXT_PUBLIC_TURN_USERNAME
ARG NEXT_PUBLIC_TURN_PASSWORD
ARG NEXT_IMAGE_UNOPTIMIZED
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
ENV NEXT_PUBLIC_TURN_HOST=${NEXT_PUBLIC_TURN_HOST}
ENV NEXT_PUBLIC_TURN_USERNAME=${NEXT_PUBLIC_TURN_USERNAME}
ENV NEXT_PUBLIC_TURN_PASSWORD=${NEXT_PUBLIC_TURN_PASSWORD}
ENV NEXT_IMAGE_UNOPTIMIZED=${NEXT_IMAGE_UNOPTIMIZED}
+118
View File
@@ -0,0 +1,118 @@
import { NextRequest, NextResponse } from 'next/server';
const startTime = Date.now();
export async function GET(request: NextRequest) {
try {
const errors: string[] = [];
let status = 'healthy';
// 基础健康信息
const basicHealth = {
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: Math.floor((Date.now() - startTime) / 1000),
service: 'privydrop-frontend',
version: process.env.npm_package_version || '1.0.0',
environment: process.env.NODE_ENV || 'development'
};
// Check backend API connectivity
const backendHealth = await checkBackendHealth();
if (backendHealth.status !== 'connected') {
errors.push('Backend API connection failed');
status = 'degraded';
}
// System info snapshot
const systemInfo = {
runtime: process.env.NEXT_RUNTIME || 'nodejs',
nextjs: {
version: process.version,
platform: process.platform,
arch: process.arch
},
memory: process.memoryUsage ? {
used: formatBytes(process.memoryUsage().heapUsed),
total: formatBytes(process.memoryUsage().heapTotal),
percent: Math.round((process.memoryUsage().heapUsed / process.memoryUsage().heapTotal) * 100)
} : null
};
const detailedHealth = {
...basicHealth,
status,
dependencies: {
backend: backendHealth
},
system: systemInfo,
...(errors.length > 0 && { errors })
};
const httpStatus = status === 'healthy' ? 200 : 503;
return NextResponse.json(detailedHealth, { status: httpStatus });
} catch (error) {
console.error('Detailed frontend health check error:', error);
return NextResponse.json({
status: 'unhealthy',
timestamp: new Date().toISOString(),
service: 'privydrop-frontend',
error: error instanceof Error ? error.message : 'Unknown error'
}, { status: 503 });
}
}
// Check backend API health
async function checkBackendHealth() {
try {
// Prefer container-internal URL, then public URL, then localhost fallback
const backendUrl = process.env.BACKEND_INTERNAL_URL || process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
const start = Date.now();
const response = await fetch(`${backendUrl}/health`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
// Timeout to avoid long-hanging connections in degraded networks
signal: AbortSignal.timeout(5000)
});
const responseTime = Date.now() - start;
if (response.ok) {
const data = await response.json();
return {
status: 'connected',
responseTime,
backendUrl,
backendService: data.service || 'unknown'
};
} else {
return {
status: 'error',
responseTime,
backendUrl,
httpStatus: response.status,
error: `HTTP ${response.status}`
};
}
} catch (error) {
return {
status: 'disconnected',
backendUrl: process.env.BACKEND_INTERNAL_URL || process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001',
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}
// 格式化字节数
function formatBytes(bytes: number): string {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
}
+30
View File
@@ -0,0 +1,30 @@
import { NextRequest, NextResponse } from 'next/server';
const startTime = Date.now();
export async function GET(request: NextRequest) {
try {
const health = {
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: Math.floor((Date.now() - startTime) / 1000),
service: 'privydrop-frontend',
version: process.env.npm_package_version || '1.0.0',
environment: process.env.NODE_ENV || 'development',
nextjs: {
version: process.env.NEXT_RUNTIME || 'nodejs'
}
};
return NextResponse.json(health, { status: 200 });
} catch (error) {
console.error('Frontend health check error:', error);
return NextResponse.json({
status: 'unhealthy',
timestamp: new Date().toISOString(),
service: 'privydrop-frontend',
error: error instanceof Error ? error.message : 'Unknown error'
}, { status: 503 });
}
}
+8 -8
View File
@@ -1,3 +1,5 @@
import { ManagerOptions, SocketOptions } from "socket.io-client";
export const config = {
API_URL: process.env.NEXT_PUBLIC_API_URL!,
USE_HTTPS: process.env.NODE_ENV !== "development",
@@ -54,14 +56,12 @@ export const getIceServers = () => {
return iceServers;
};
export const getSocketOptions = () => {
return config.USE_HTTPS
? {
secure: true,
path: "/socket.io/",
transports: ["websocket"],
}
: undefined;
export const getSocketOptions = (): Partial<ManagerOptions & SocketOptions> => {
// Allow polling fallback; do not force "secure" here — protocol will be inferred
return {
path: "/socket.io/",
transports: ["websocket", "polling"],
};
};
export const getFetchOptions = (options: RequestInit = {}): RequestInit => {
+2 -2
View File
@@ -49,7 +49,7 @@ const ClipboardApp = () => {
handleDownloadFile,
} = useFileTransferHandler({ messages, putMessageInMs });
// 简化的 WebRTC 连接初始化
// Simplified WebRTC connection initialization
const {
requestFile,
requestFolder,
@@ -60,7 +60,7 @@ const ClipboardApp = () => {
putMessageInMs,
});
// 大大简化的房间管理 - 不再需要传递任何 WebRTC 依赖
// Greatly simplified room management - No longer need to pass any WebRTC dependencies
const {
processRoomIdInput,
joinRoom,
@@ -225,7 +225,7 @@ const FileListDisplay: React.FC<FileListDisplayProps> = ({
}
return updated;
});
}, 3000);
}, 3002);
}
}
});
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env node
const http = require('http');
const options = {
host: 'localhost',
port: process.env.PORT || 3002,
path: '/api/health',
timeout: 2000,
method: 'GET'
};
const req = http.request(options, (res) => {
if (res.statusCode === 200) {
console.log('Frontend health check passed');
process.exit(0);
} else {
console.log(`Frontend health check failed with status: ${res.statusCode}`);
process.exit(1);
}
});
req.on('error', (err) => {
console.log(`Frontend health check failed: ${err.message}`);
process.exit(1);
});
req.on('timeout', () => {
console.log('Frontend health check timeout');
req.destroy();
process.exit(1);
});
req.end();
+13 -5
View File
@@ -27,7 +27,7 @@ export class ReceptionConfig {
// Network and timing
static readonly NETWORK_CONFIG = {
FIREFOX_COMPATIBILITY_DELAY: 10, // ms delay for Firefox compatibility
FINALIZATION_TIMEOUT: 30000, // 30s timeout for file finalization
FINALIZATION_TIMEOUT: 30020, // 30s timeout for file finalization
GRACEFUL_SHUTDOWN_TIMEOUT: 5000, // 5s timeout for graceful shutdown
};
@@ -54,7 +54,10 @@ export class ReceptionConfig {
/**
* Calculate expected chunks count for file size and offset
*/
static calculateExpectedChunks(fileSize: number, startOffset: number = 0): number {
static calculateExpectedChunks(
fileSize: number,
startOffset: number = 0
): number {
return Math.ceil((fileSize - startOffset) / this.FILE_CONFIG.CHUNK_SIZE);
}
@@ -68,7 +71,12 @@ export class ReceptionConfig {
/**
* Check if file should be saved to disk
*/
static shouldSaveToDisk(fileSize: number, hasSaveDirectory: boolean): boolean {
return hasSaveDirectory || fileSize >= this.FILE_CONFIG.LARGE_FILE_THRESHOLD;
static shouldSaveToDisk(
fileSize: number,
hasSaveDirectory: boolean
): boolean {
return (
hasSaveDirectory || fileSize >= this.FILE_CONFIG.LARGE_FILE_THRESHOLD
);
}
}
}
@@ -80,7 +80,7 @@ export class FileTransferOrchestrator implements MessageHandlerDelegate {
* 🎯 Send string content
*/
public async sendString(content: string, peerId: string): Promise<void> {
const chunkSize = TransferConfig.FILE_CONFIG.CHUNK_SIZE;
const chunkSize = 65000;
const chunks: string[] = [];
for (let i = 0; i < content.length; i += chunkSize) {
+4 -1
View File
@@ -18,10 +18,13 @@ class WebRTCService {
private static instance: WebRTCService;
private constructor() {
const apiUrl = (config.API_URL || "").trim();
// Use same-origin when API_URL is empty string — socket.io accepts empty string for same-origin
const signalingServer: string = apiUrl.length > 0 ? apiUrl : "";
const webRTCConfig = {
iceServers: getIceServers(),
socketOptions: getSocketOptions() || {},
signalingServer: config.API_URL,
signalingServer,
};
this.sender = new WebRTC_Initiator(webRTCConfig);
+9 -2
View File
@@ -12,6 +12,8 @@ const withMDX = createMDX({
const nextConfig = {
pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'],
images: {
// Disable optimization inside Docker to avoid container loopback fetch failures (502)
unoptimized: process.env.NEXT_IMAGE_UNOPTIMIZED === 'true',
remotePatterns: [
{
protocol: 'https',
@@ -20,7 +22,12 @@ const nextConfig = {
},
]
},
// 启用standalone输出模式,用于Docker部署
output: 'standalone',
// 禁用telemetry
experimental: {
instrumentationHook: true,
},
}
export default withMDX(nextConfig);
export default withMDX(nextConfig);
+10 -10
View File
@@ -5,7 +5,7 @@
"scripts": {
"dev": "next dev -H 0.0.0.0 -p 3002",
"build": "next build",
"start": "next start -p 3000",
"start": "next start -p 3002",
"lint": "next lint"
},
"dependencies": {
@@ -24,8 +24,8 @@
"@types/hast": "^3.0.4",
"@types/mdast": "^4.0.4",
"@types/negotiator": "^0.6.3",
"@types/node": "^20",
"@types/react-dom": "^18",
"@types/node": "^20.14.13",
"@types/react-dom": "^18.3.0",
"@types/unist": "^3.0.3",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
@@ -39,8 +39,8 @@
"next-mdx-remote": "^5.0.0",
"next-themes": "^0.3.0",
"qrcode.react": "^4.0.1",
"react": "^18",
"react-dom": "^18",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-intersection-observer": "^9.16.0",
"remark-gfm": "^4.0.0",
"sharp": "^0.33.5",
@@ -52,11 +52,11 @@
"zustand": "^5.0.7"
},
"devDependencies": {
"@types/react": "^18.3.18",
"eslint": "^8",
"@types/react": "^18.3.22",
"eslint": "^8.57.0",
"eslint-config-next": "14.2.5",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"typescript": "^5"
"postcss": "^8.4.40",
"tailwindcss": "^3.4.7",
"typescript": "^5.5.4"
}
}
Generated Executable → Regular
+13 -13
View File
@@ -54,10 +54,10 @@ importers:
specifier: ^0.6.3
version: 0.6.3
'@types/node':
specifier: ^20
specifier: ^20.14.13
version: 20.14.13
'@types/react-dom':
specifier: ^18
specifier: ^18.3.0
version: 18.3.0
'@types/unist':
specifier: ^3.0.3
@@ -99,10 +99,10 @@ importers:
specifier: ^4.0.1
version: 4.0.1(react@18.3.1)
react:
specifier: ^18
specifier: ^18.3.1
version: 18.3.1
react-dom:
specifier: ^18
specifier: ^18.3.1
version: 18.3.1(react@18.3.1)
react-intersection-observer:
specifier: ^9.16.0
@@ -133,22 +133,22 @@ importers:
version: 5.0.7(@types/react@18.3.22)(react@18.3.1)
devDependencies:
'@types/react':
specifier: ^18.3.18
specifier: ^18.3.22
version: 18.3.22
eslint:
specifier: ^8
specifier: ^8.57.0
version: 8.57.0
eslint-config-next:
specifier: 14.2.5
version: 14.2.5(eslint@8.57.0)(typescript@5.5.4)
postcss:
specifier: ^8
specifier: ^8.4.40
version: 8.4.40
tailwindcss:
specifier: ^3.4.1
specifier: ^3.4.7
version: 3.4.7
typescript:
specifier: ^5
specifier: ^5.5.4
version: 5.5.4
packages:
@@ -1367,8 +1367,8 @@ packages:
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
engines: {node: '>= 6'}
caniuse-lite@1.0.30001735:
resolution: {integrity: sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==}
caniuse-lite@1.0.30001745:
resolution: {integrity: sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ==}
ccount@2.0.1:
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
@@ -4730,7 +4730,7 @@ snapshots:
camelcase-css@2.0.1: {}
caniuse-lite@1.0.30001735: {}
caniuse-lite@1.0.30001745: {}
ccount@2.0.1: {}
@@ -6552,7 +6552,7 @@ snapshots:
'@next/env': 14.2.5
'@swc/helpers': 0.5.5
busboy: 1.6.0
caniuse-lite: 1.0.30001735
caniuse-lite: 1.0.30001745
graceful-fs: 4.2.11
postcss: 8.4.31
react: 18.3.1
+218
View File
@@ -0,0 +1,218 @@
#!/bin/bash
# Color definitions
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Test result counters
TESTS_PASSED=0
TESTS_FAILED=0
TOTAL_TESTS=0
# Logging helpers
log_info() {
echo -e "${BLUE}$1${NC}"
}
log_success() {
echo -e "${GREEN}$1${NC}"
TESTS_PASSED=$((TESTS_PASSED + 1))
}
log_error() {
echo -e "${RED}$1${NC}"
TESTS_FAILED=$((TESTS_FAILED + 1))
}
log_warning() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
# Test functions
test_api() {
local url="$1"
local description="$2"
local expected_status="${3:-200}"
TOTAL_TESTS=$((TOTAL_TESTS + 1))
echo ""
log_info "Test: $description"
log_info "URL: $url"
# Send request and capture response
response=$(curl -s -w "\n%{http_code}" "$url" 2>/dev/null)
if [ $? -ne 0 ]; then
log_error "Request failed - unable to connect to service"
return 1
fi
# Split response body and status code
http_code=$(echo "$response" | tail -n1)
response_body=$(echo "$response" | head -n -1)
# Check HTTP status code
if [ "$http_code" -eq "$expected_status" ]; then
log_success "HTTP status code OK: $http_code"
else
log_error "HTTP status code mismatch: expected $expected_status, got $http_code"
return 1
fi
# Validate JSON format
if echo "$response_body" | jq . >/dev/null 2>&1; then
log_success "Response is valid JSON"
# Pretty-print JSON response
echo -e "${BLUE}Response body:${NC}"
echo "$response_body" | jq .
# Verify required fields
status=$(echo "$response_body" | jq -r '.status // empty')
service=$(echo "$response_body" | jq -r '.service // empty')
timestamp=$(echo "$response_body" | jq -r '.timestamp // empty')
if [ -n "$status" ] && [ -n "$service" ] && [ -n "$timestamp" ]; then
log_success "Contains required fields: status, service, timestamp"
else
log_error "Missing required fields"
return 1
fi
else
log_error "Response is not valid JSON"
echo "Response body: $response_body"
return 1
fi
return 0
}
# Check if service is running
check_service() {
local port="$1"
local service_name="$2"
if nc -z localhost "$port" 2>/dev/null; then
log_success "$service_name is running (port $port)"
return 0
else
log_error "$service_name is not running (port $port)"
return 1
fi
}
# Wait for service to start
wait_for_service() {
local port="$1"
local service_name="$2"
local max_attempts=30
local attempt=0
log_info "Waiting for $service_name to start..."
while [ $attempt -lt $max_attempts ]; do
if nc -z localhost "$port" 2>/dev/null; then
log_success "$service_name started"
return 0
fi
attempt=$((attempt + 1))
echo -n "."
sleep 2
done
log_error "$service_name startup timed out"
return 1
}
# Main test function
main() {
echo -e "${BLUE}=== PrivyDrop Health Check API Tests ===${NC}"
echo ""
# Check required tools
if ! command -v curl &> /dev/null; then
log_error "curl is not installed; please install curl"
exit 1
fi
if ! command -v jq &> /dev/null; then
log_error "jq is not installed; please install jq for JSON parsing"
exit 1
fi
if ! command -v nc &> /dev/null; then
log_error "netcat is not installed; please install nc for port checks"
exit 1
fi
# Check service status
echo -e "${BLUE}=== Check Service Status ===${NC}"
backend_running=false
frontend_running=false
if check_service 3001 "Backend"; then
backend_running=true
fi
if check_service 3002 "Frontend"; then
frontend_running=true
fi
# Show startup hints if services are not running
if [ "$backend_running" = false ]; then
echo ""
log_warning "Backend is not running; please start it:"
echo " cd backend && npm run dev"
echo ""
fi
if [ "$frontend_running" = false ]; then
echo ""
log_warning "Frontend is not running; please start it:"
echo " cd frontend && pnpm dev"
echo ""
fi
# Test backend health check APIs
if [ "$backend_running" = true ]; then
echo -e "${BLUE}=== Test Backend Health Check APIs ===${NC}"
test_api "http://localhost:3001/health" "Backend basic health check"
test_api "http://localhost:3001/api/health" "Backend API path health check"
test_api "http://localhost:3001/health/detailed" "Backend detailed health check"
fi
# Test frontend health check APIs
if [ "$frontend_running" = true ]; then
echo -e "${BLUE}=== Test Frontend Health Check APIs ===${NC}"
test_api "http://localhost:3002/api/health" "Frontend basic health check"
test_api "http://localhost:3002/api/health/detailed" "Frontend detailed health check"
fi
# Test results summary
echo ""
echo -e "${BLUE}=== Test Results Summary ===${NC}"
echo "Total tests: $TOTAL_TESTS"
echo -e "Passed: ${GREEN}$TESTS_PASSED${NC}"
echo -e "Failed: ${RED}$TESTS_FAILED${NC}"
if [ $TESTS_FAILED -eq 0 ]; then
echo -e "${GREEN}🎉 All tests passed!${NC}"
exit 0
else
echo -e "${RED}$TESTS_FAILED test(s) failed${NC}"
exit 1
fi
}
# Trap interrupt signals
trap 'echo -e "\n${YELLOW}Tests interrupted${NC}"; exit 1' INT TERM
# Run main function
main "$@"