Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
name: Deploy to Cloudflare

on:
push:
branches: [main, uat, release*]
workflow_dispatch:

env:
DATABASE_NAME: cfwarden-data

jobs:
deploy:
name: Deploy Cloudflare Worker
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Install Wrangler CLI
run: npm install -g wrangler

- name: Setup Rust toolchain
run: |
rustup toolchain install stable --profile minimal
rustup target add wasm32-unknown-unknown

- name: Install system dependencies
run: sudo apt install -y lld clang gcc llvm

- name: Install worker-build
run: cargo install worker-build

- name: Authenticate with Cloudflare
run: echo "Authenticated with Cloudflare"
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

- name: Create D1 database
id: create_db
run: |
# Create D1 database and capture output
DB_OUTPUT=$(wrangler d1 create $DATABASE_NAME)
echo "$DB_OUTPUT"

# Extract database_id from output
DATABASE_ID=$(echo "$DB_OUTPUT" | grep -oP 'database_id: \"\\K[^\"]+' || echo "")

if [ -n "$DATABASE_ID" ]; then
echo "DATABASE_ID=$DATABASE_ID" >> $GITHUB_OUTPUT
echo "Database created with ID: $DATABASE_ID"
else
echo "Warning: Could not extract database_id from output"
echo "DATABASE_ID=${{ secrets.D1_DATABASE_ID }}" >> $GITHUB_OUTPUT
fi
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

- name: Update wrangler.jsonc
run: |
cat <<EOF > wrangler.jsonc
{
"name": "cfwarden-work",
"main": "build/index.js",
"compatibility_date": "2025-09-19",
"account_id": "${{ secrets.CLOUDFLARE_ACCOUNT_ID }}",
"d1_databases": [
{
"binding": "cfwarden_data",
"database_name": "cfwarden-data",
"database_id": "${{ steps.create_db.outputs.DATABASE_ID }}",
"remote": true
}
],
"vars": {
"JWT_SECRET": "${{ secrets.JWT_SECRET }}",
"JWT_REFRESH_SECRET": "${{ secrets.JWT_REFRESH_SECRET }}",
"ALLOWED_EMAILS": "${{ secrets.ALLOWED_EMAILS }}",
"TWO_FACTOR_ENC_KEY": "${{ secrets.TWO_FACTOR_ENC_KEY }}"
},
"observability": {
"logs": {
"enabled": true,
"head_sampling_rate": 1,
"invocation_logs": true,
"persist": true
}
},
"build": {
"command": "worker-build --release"
}
}
EOF

- name: Initialize database schema
run: wrangler d1 execute $DATABASE_NAME --remote --file=sql/schema_full.sql
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

- name: Build worker
run: worker-build --release

- name: Deploy to Cloudflare
run: wrangler deploy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
86 changes: 86 additions & 0 deletions GITHUB.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Cloudflare Worker 部署指南

本文档说明如何使用GitHub Actions自动部署Cloudflare Worker。

## 前置条件

在运行GitHub Action之前,您需要在GitHub仓库中配置以下环境变量:

### 必需的环境变量

1. **CLOUDFLARE_API_TOKEN**
- 描述:Cloudflare API令牌,用于认证和部署
- 获取方式:在Cloudflare仪表板中创建API令牌,需要包含以下权限:
- Account: Workers Scripts: Edit
- Account: Workers KV Storage: Edit
- Account: D1: Edit
- User: User Details: Read

2. **CLOUDFLARE_ACCOUNT_ID**
- 描述:您的Cloudflare账户ID
- 获取方式:在Cloudflare仪表板的Overview页面找到Account ID

3. **JWT_SECRET**
- 描述:JWT访问令牌签名密钥
- 要求:至少32字符的随机字符串

4. **JWT_REFRESH_SECRET**
- 描述:JWT刷新令牌签名密钥
- 要求:至少32字符的随机字符串,与JWT_SECRET不同

5. **ALLOWED_EMAILS**
- 描述:注册白名单,多个邮箱用英文逗号分隔
- 示例:`user1@example.com,user2@example.com`
- 特殊值:使用`*`表示允许所有邮箱注册

### 可选的环境变量

6. **TWO_FACTOR_ENC_KEY**(可选)
- 描述:Base64编码的32字节密钥,用于加密存储TOTP秘钥
- 如果不设置,TOTP秘钥将以明文形式存储(不推荐)

7. **D1_DATABASE_ID**(可选)
- 描述:现有的D1数据库ID
- 如果已存在数据库,可以设置此变量跳过数据库创建步骤

## 配置步骤

### 1. 在GitHub仓库中设置环境变量

1. 进入您的GitHub仓库
2. 点击 Settings → Secrets and variables → Actions
3. 点击 "New repository secret"
4. 为每个必需的环境变量添加对应的值

### 2. 手动触发部署

配置完成后,您可以:

1. 推送代码到main分支(自动触发)
2. 或者在GitHub仓库的Actions标签页中手动运行工作流

## 工作流执行流程

GitHub Action将按以下顺序执行:

1. **环境准备**:安装Node.js、Rust、Wrangler CLI等必要工具
2. **数据库创建**:创建新的D1数据库(如果D1_DATABASE_ID未设置)
3. **数据库初始化**:执行sql/schema_full.sql初始化数据库结构
4. **密钥配置**:配置JWT密钥、白名单等环境变量
5. **构建部署**:构建Worker并部署到Cloudflare

## 注意事项

- 首次部署时会创建新的D1数据库,后续部署会重用现有数据库
- sql/schema_full.sql会清空现有数据,仅适用于全新部署
- 如需保留数据,请使用sql/schema.sql进行增量更新
- 确保所有环境变量都已正确设置,否则部署会失败

## 故障排除

如果部署失败,请检查:

1. Cloudflare API令牌权限是否足够
2. 环境变量值是否正确
3. Cloudflare账户是否有效
4. 网络连接是否正常
27 changes: 22 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,24 @@ Warden Worker 是一个运行在 Cloudflare Workers 上的轻量级 Bitwarden
- 核心能力:注册/登录、同步、密码项(Cipher)增删改、文件夹、TOTP(Authenticator)二步验证
- 官方安卓兼容:支持 `/api/devices/knowndevice` 与 remember-device(twoFactorProvider=5)流程

## 快速部署(Cloudflare)
## 自动部署(Github Action)

### 0. 前置条件
- Cloudflare 账号 + R2数据库
- Github 账号 + Action构建权

### 1. 部署仓库

1. 点击[Fork本项目](https://github.com/PIKACHUIM/warden-worker/fork),克隆到你的Github账号
![QQ20260127-153440.jpg](images/QQ20260127-153440.jpg)

2.
2.
![QQ20260127-150742.jpg](images/QQ20260127-150742.jpg)



## 手动部署(Cloudflare)

### 0. 前置条件

Expand All @@ -26,7 +43,7 @@ Warden Worker 是一个运行在 Cloudflare Workers 上的轻量级 Bitwarden
### 1. 创建 D1 数据库

```bash
wrangler d1 create vault1
wrangler d1 create cfwarden-db
```

把输出的 `database_id` 写入 `wrangler.jsonc` 的 `d1_databases`。
Expand All @@ -36,7 +53,7 @@ wrangler d1 create vault1
注意:`sql/schema_full.sql` 会 `DROP TABLE`,仅用于全新部署(会清空数据)。

```bash
wrangler d1 execute vault1 --remote --file=sql/schema_full.sql
wrangler d1 execute cfwarden-db --remote --file=sql/schema_full.sql
```

`sql/schema.sql` 仅保留为历史/兼容用途;推荐新部署直接使用 `sql/schema_full.sql`。
Expand All @@ -52,7 +69,7 @@ wrangler secret put TWO_FACTOR_ENC_KEY

- JWT_SECRET:访问令牌签名密钥
- JWT_REFRESH_SECRET:刷新令牌签名密钥
- ALLOWED_EMAILS:首个账号注册白名单(仅在“数据库还没有任何用户”时启用),多个邮箱用英文逗号分隔
- ALLOWED_EMAILS:注册白名单,多个邮箱用英文逗号分隔;可使用 `*` 表示允许所有邮箱注册
- TWO_FACTOR_ENC_KEY:可选,Base64 的 32 字节密钥;用于加密存储 TOTP 秘钥(不设置则以 `plain:` 形式存储)

### 4. 部署
Expand Down Expand Up @@ -81,7 +98,7 @@ wrangler deploy
## 本地开发

```bash
wrangler d1 execute vault1 --local --file=sql/schema_full.sql
wrangler d1 execute cfwarden-db --local --file=sql/schema_full.sql
wrangler dev
```

Expand Down
Binary file added images/QQ20260127-150742.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/QQ20260127-153440.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 36 additions & 0 deletions scripts/rate_limit_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const url = "https://warden.2x.nz/identity/connect/token";

async function main() {
const ip = "203.0.113.10";
for (let i = 1; i <= 35; i++) {
const body =
"grant_type=password" +
"&username=none%40example.com" +
"&password=x" +
"&deviceIdentifier=d" +
"&deviceName=n" +
"&deviceType=0" +
"&scope=api+offline_access" +
"&client_id=mobile";

const r = await fetch(url, {
method: "POST",
headers: {
"content-type": "application/x-www-form-urlencoded",
"x-forwarded-for": ip,
},
body,
});

const t = await r.text();
if ([1, 30, 31, 35].includes(i)) {
console.log(i, r.status, t);
}
}
}

main().catch((e) => {
console.error(e);
process.exit(1);
});

6 changes: 6 additions & 0 deletions sql/schema_full.sql
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ CREATE TABLE IF NOT EXISTS devices (
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

CREATE TABLE IF NOT EXISTS login_rate_limits (
key TEXT PRIMARY KEY NOT NULL,
count INTEGER NOT NULL,
reset_at INTEGER NOT NULL
);

CREATE INDEX IF NOT EXISTS idx_ciphers_user_id ON ciphers(user_id);
CREATE INDEX IF NOT EXISTS idx_ciphers_folder_id ON ciphers(folder_id);
CREATE INDEX IF NOT EXISTS idx_folders_user_id ON folders(user_id);
Expand Down
8 changes: 6 additions & 2 deletions src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use axum::{
extract::FromRequestParts,
http::{header, request::Parts},
};
use jsonwebtoken::{decode, DecodingKey, Validation};
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use worker::Env;
Expand Down Expand Up @@ -45,7 +45,11 @@ impl FromRequestParts<Arc<Env>> for Claims

// Decode and validate the token
let decoding_key = DecodingKey::from_secret(secret.to_string().as_ref());
let token_data = decode::<Claims>(&token, &decoding_key, &Validation::default())
let mut validation = Validation::new(Algorithm::HS256);
validation.validate_nbf = true;
validation.leeway = 60;

let token_data = decode::<Claims>(&token, &decoding_key, &validation)
.map_err(|_| AppError::Unauthorized("Invalid token".to_string()))?;

Ok(token_data.claims)
Expand Down
2 changes: 1 addition & 1 deletion src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ use std::sync::Arc;
use worker::{D1Database, Env};

pub fn get_db(env: &Arc<Env>) -> Result<D1Database, AppError> {
env.d1("vault1").map_err(AppError::Worker)
env.d1("cfwarden_data").map_err(AppError::Worker)
}
4 changes: 4 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ pub enum AppError {
#[error("Unauthorized: {0}")]
Unauthorized(String),

#[error("Too many requests: {0}")]
TooManyRequests(String),

#[error("Cryptography error: {0}")]
Crypto(String),

Expand All @@ -45,6 +48,7 @@ impl IntoResponse for AppError {
AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg),
AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg),
AppError::Unauthorized(msg) => (StatusCode::UNAUTHORIZED, msg),
AppError::TooManyRequests(msg) => (StatusCode::TOO_MANY_REQUESTS, msg),
AppError::Crypto(msg) => (
StatusCode::INTERNAL_SERVER_ERROR,
format!("Crypto error: {}", msg),
Expand Down
Loading