Skip to content

Commit 1e796a3

Browse files
committed
standalone@2.0.13: better ratelimiting & docs
1 parent 563ab48 commit 1e796a3

File tree

9 files changed

+215
-164
lines changed

9 files changed

+215
-164
lines changed

docs/.vitepress/config.mjs

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -159,25 +159,23 @@ export default withMermaid({
159159
sidebar: [
160160
{ text: "Quickstart", link: "/guide/index.md" },
161161
{ text: "Feature comparison", link: "/guide/alternatives.md" },
162-
{ text: "Community libraries", link: "/guide/community.md" },
163162
{
164-
text: "Libraries",
163+
text: "Standalone",
165164
collapsed: false,
166165
items: [
167-
{ text: "Server", link: "/guide/server.md" },
168-
{ text: "Widget", link: "/guide/widget.md" },
169-
{ text: "Solver", link: "/guide/solver.md" },
166+
{ text: "Quickstart", link: "/guide/standalone/index.md" },
167+
{ text: "API", link: "/guide/standalone/api.md" },
168+
{ text: "Options", link: "/guide/standalone/options.md" },
170169
],
171170
},
172171
{
173-
text: "Standalone",
172+
text: "Libraries",
174173
collapsed: true,
175174
items: [
176-
{ text: "About", link: "/guide/standalone/index.md" },
177-
{ text: "Installation", link: "/guide/standalone/installation.md" },
178-
{ text: "Usage", link: "/guide/standalone/usage.md" },
179-
{ text: "API", link: "/guide/standalone/api.md" },
180-
{ text: "Options", link: "/guide/standalone/options.md" },
175+
{ text: "Server", link: "/guide/server.md" },
176+
{ text: "Widget", link: "/guide/widget.md" },
177+
{ text: "Solver", link: "/guide/solver.md" },
178+
{ text: "Community libraries", link: "/guide/community.md" },
181179
],
182180
},
183181
{

docs/guide/standalone/index.md

Lines changed: 105 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,111 @@
22

33
Cap Standalone is a self-hosted version of Cap's backend that allows you to spin up a server to validate and create challenges so you can use it with languages other than JS.
44

5-
It's simple yet amazingly powerful, allowing you to use Cap in any language that can make HTTP requests. It's mostly compatible with reCAPTCHA and hCaptcha's siteverify enpoints, so you can use it as a drop-in replacement for them.
5+
It's simple yet powerful, allowing you to use Cap in any language that can make HTTP requests. It's mostly compatible with reCAPTCHA and hCaptcha's siteverify enpoints, so you can use it as a drop-in replacement for them.
66

7-
It also offers API key support, a built-in assets server, a dashboard with statistics, and more. To get started, follow [the instructions to install it on your server](installation.md).
7+
It also offers API key support, a built-in assets server, a dashboard with statistics, and more.
88

99
![Screenshot of Cap's standalone mode](/standalone_screenshot.png)
10+
11+
## Installation
12+
13+
### Requirements
14+
15+
You'll need to have [Docker Engine 20.10 or higher](https://docs.docker.com/get-docker/) installed on your server. Both `x86_64` (amd64) and `arm64` architectures are supported.
16+
17+
---
18+
19+
Run the following command to pull the Cap Standalone Docker image from Docker Hub:
20+
21+
```bash
22+
docker pull tiago2/cap:latest
23+
```
24+
25+
Then, to run the server, use the following command:
26+
27+
```bash
28+
docker run -d \
29+
-p 3000:3000 \
30+
-v cap-data:/usr/src/app/.data \
31+
-e ADMIN_KEY=your_secret_password \
32+
--name cap-standalone \
33+
tiago2/cap:latest
34+
```
35+
36+
Make sure to replace `your_secret_password` with a strong password, as anyone with it will be able to log into the dashboard and create keys. It'll need to be at least 30 characters long.
37+
38+
Then, you can access the dashboard at `http://localhost:3000`, log in, and create a key. You'll get a site key and a secret key which you'll be able to use on your widget.
39+
40+
On Debian and other OSes that don't use `iptables`, if you can't open the dashboard, try setting `--network=host` in the run command. Thanks to [Boro Vukovic](https://github.com/tiagozip/cap/issues/70#issuecomment-3086464282) for letting me know about this.
41+
42+
You'll also need to make the server publicly accessible from the internet, as the widget needs to be able to reach it. If you're using a reverse proxy, make sure to check [the options guide](/guide/standalone/options.md) to configure rate-limiting properly.
43+
44+
## Usage
45+
46+
### Client-side
47+
48+
Let's configure your widget to use your self-hosted Cap Standalone server. To do this, set the widget's API endpoint option to:
49+
50+
```
51+
https://<instance_url>/<site_key>/
52+
```
53+
54+
Make sure to replace:
55+
56+
- `<instance_url>`: The actual URL where your Cap Standalone instance is running. This URL must be publicly accessible from the internet.
57+
- `<site_key>`: Your site key from this dashboard.
58+
59+
Example:
60+
61+
```html
62+
<cap-widget
63+
data-cap-api-endpoint="https://cap.example.com/d9256640cb53/"
64+
></cap-widget>
65+
```
66+
67+
### Server-side
68+
69+
After a user completes the CAPTCHA on your site, your backend needs to verify their token using this server's API.
70+
71+
You can do this by sending a `POST` request from your server to the following endpoint:
72+
73+
```
74+
https://<instance_url>/<site_key>/siteverify
75+
```
76+
77+
Your request needs to include the following data:
78+
79+
- `secret`: Your key secret from this dashboard. This is **not** the admin key, but rather your site key's secret.
80+
81+
- `response`: The CAPTCHA token generated by the widget on the client-side
82+
83+
Example using `curl`:
84+
85+
```bash
86+
curl "https://<instance_url>/<site_key>/siteverify" \
87+
-X POST \
88+
-H "Content-Type: application/json" \
89+
-d '{ "secret": "<key_secret>", "response": "<captcha_token>" }'
90+
```
91+
92+
The response should look like this:
93+
94+
```json
95+
{
96+
"success": true
97+
}
98+
```
99+
100+
Or, if the captcha token is invalid or expired, it will return:
101+
102+
```json
103+
{
104+
"success": false
105+
}
106+
```
107+
108+
If `success` is true, you can proceed with your app logic.
109+
110+
### Client-side library storage
111+
112+
Cap Standalone can also serve the widget and floating client-side library files. [Learn more](options.md#asset-server).
Lines changed: 1 addition & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,3 @@
11
# Installation
22

3-
### Requirements
4-
5-
You'll need to have [Docker Engine 20.10 or higher](https://docs.docker.com/get-docker/) installed on your server. Both `x86_64` (amd64) and `arm64` architectures are supported.
6-
7-
---
8-
9-
Run the following command to pull the Cap Standalone Docker image from Docker Hub:
10-
11-
```bash
12-
docker pull tiago2/cap:latest
13-
```
14-
15-
Then, to run the server, use the following command:
16-
17-
```bash
18-
docker run -d \
19-
-p 3000:3000 \
20-
-v cap-data:/usr/src/app/.data \
21-
-e ADMIN_KEY=your_secret_password \
22-
--name cap-standalone \
23-
tiago2/cap:latest
24-
```
25-
26-
Make sure to replace `your_secret_password` with a strong password, as anyone with it will be able to log into the dashboard and create keys. It'll need to be at least 30 characters long.
27-
28-
Then, you can access the dashboard at `http://localhost:3000`, log in, and create a key. You'll get a site key and a secret key which you'll be able to use on your widget.
29-
30-
On Debian and other OSes that don't use `iptables`, if you can't open the dashboard, try setting `--network=host` in the run command. Thanks to [Boro Vukovic](https://github.com/tiagozip/cap/issues/70#issuecomment-3086464282) for letting me know about this.
31-
32-
You'll also need to make the server publicly accessible from the internet, as the widget needs to be able to reach it.
33-
34-
Done! Now, read the [usage guide](/guide/standalone/usage.md) to learn how to use it.
3+
This section has moved to [Standalone](/guide/standalone/index.md).

docs/guide/standalone/options.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ By default, Standalone will use Elysia's built-in `server.requestIP` function to
4343

4444
If so, you can change the IP extraction logic to simply read from a header set in `RATELIMIT_IP_HEADER` in your env. For example, if you were using Cloudflare, you might set `RATELIMIT_IP_HEADER` to `cf-connecting-ip`. On most setups, this is `x-forwarded-for`.
4545

46+
The `/siteverify` endpoint is intended for server-to-server use, so each request checks if your key's secret is valid. If it is, the request is allowed. If not, the request is denied and the client's IP is temporarily blocked for a few hundred milliseconds. This prevents database strain without affecting legitimate requests.
47+
4648
If you're interested in an option to fully disable ratelimiting, let us know using GitHub issues.
4749

4850
## Custom data path
49-
If you would like the data to be stored in a custom path inside the container, you can set the `DATA_PATH` environment variable to the desired path. Note that this only works in Standalone 2.0.9 or above. Added by [#100](https://github.com/tiagozip/cap/issues/100)
51+
If you would like the data to be stored in a custom path inside the container, you can set the `DATA_PATH` environment variable to the desired path. Note that this only works in Standalone 2.0.9 or above.

docs/guide/standalone/usage.md

Lines changed: 2 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,3 @@
1-
# Usage
1+
# Installation
22

3-
First, make sure Cap Standalone is installed and running by following [the installation guide](installation.md).
4-
5-
Then, you can get to using Cap Standalone:
6-
7-
### Client-side
8-
9-
Let's configure your widget to use your self-hosted Cap Standalone server. To do this, set the widget's API endpoint option to:
10-
11-
```
12-
https://<instance_url>/<site_key>/
13-
```
14-
15-
Make sure to replace:
16-
17-
- `<instance_url>`: The actual URL where your Cap Standalone instance is running. This URL must be publicly accessible from the internet.
18-
- `<site_key>`: Your site key from this dashboard.
19-
20-
Example:
21-
22-
```html
23-
<cap-widget
24-
data-cap-api-endpoint="https://cap.example.com/d9256640cb53/"
25-
></cap-widget>
26-
```
27-
28-
### Server-side
29-
30-
After a user completes the CAPTCHA on your site, your backend needs to verify their token using this server's API.
31-
32-
You can do this by sending a `POST` request from your server to the following endpoint:
33-
34-
```
35-
https://<instance_url>/<site_key>/siteverify
36-
```
37-
38-
Your request needs to include the following data:
39-
40-
- `secret`: Your key secret from this dashboard. This is **not** the admin key, but rather your site key's secret.
41-
42-
- `response`: The CAPTCHA token generated by the widget on the client-side
43-
44-
Example using `curl`:
45-
46-
```bash
47-
curl "https://<instance_url>/<site_key>/siteverify" \
48-
-X POST \
49-
-H "Content-Type: application/json" \
50-
-d '{ "secret": "<key_secret>", "response": "<captcha_token>" }'
51-
```
52-
53-
The response should look like this:
54-
55-
```json
56-
{
57-
"success": true
58-
}
59-
```
60-
61-
Or, if the captcha token is invalid or expired, it will return:
62-
63-
```json
64-
{
65-
"success": false
66-
}
67-
```
68-
69-
If `success` is true, you can proceed with your app logic.
70-
71-
### Client-side library storage
72-
73-
To learn more about using the client-side library, check out the [guide on it](options.md#asset-server).
3+
This section has moved to [Standalone](/guide/standalone/index.md).

standalone/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "cap-standalone",
3-
"version": "2.0.12",
3+
"version": "2.0.13",
44
"scripts": {
55
"test": "echo \"Error: no test specified\" && exit 1",
66
"dev": "bun run --watch src/index.js",

standalone/src/cap.js

Lines changed: 0 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,6 @@ import { ratelimitGenerator } from "./ratelimit.js";
99
const getSitekeyConfigQuery = db.query(
1010
`SELECT (config) FROM keys WHERE siteKey = ?`,
1111
);
12-
const getSitekeyWithSecretQuery = db.query(
13-
`SELECT * FROM keys WHERE siteKey = ?`,
14-
);
1512

1613
const insertChallengeQuery = db.query(`
1714
INSERT INTO challenges (siteKey, token, data, expires)
@@ -28,12 +25,6 @@ const insertTokenQuery = db.query(`
2825
INSERT INTO tokens (siteKey, token, expires)
2926
VALUES (?, ?, ?)
3027
`);
31-
const getTokenQuery = db.query(`
32-
SELECT * FROM tokens WHERE siteKey = ? AND token = ?
33-
`);
34-
const deleteTokenQuery = db.query(`
35-
DELETE FROM tokens WHERE siteKey = ? AND token = ?
36-
`);
3728

3829
const upsertSolutionQuery = db.query(`
3930
INSERT INTO solutions (siteKey, bucket, count)
@@ -138,40 +129,4 @@ export const capServer = new Elysia({
138129
token,
139130
expires,
140131
};
141-
})
142-
.post("/:siteKey/siteverify", async ({ body, set, params }) => {
143-
const sitekey = params.siteKey;
144-
const { secret, response } = body;
145-
146-
if (!sitekey || !secret || !response) {
147-
set.status = 400;
148-
return { error: "Missing required parameters" };
149-
}
150-
151-
const keyHash = getSitekeyWithSecretQuery.get(sitekey)?.secretHash;
152-
if (!keyHash) {
153-
set.status = 404;
154-
return { error: "Invalid site key or secret" };
155-
}
156-
157-
if (!(await Bun.password.verify(secret, keyHash))) {
158-
set.status = 403;
159-
return { error: "Invalid site key or secret" };
160-
}
161-
162-
const token = getTokenQuery.get(params.siteKey, response);
163-
164-
if (!token) {
165-
set.status = 404;
166-
return { error: "Token not found" };
167-
}
168-
169-
if (token.expires < Date.now()) {
170-
deleteTokenQuery.run(params.siteKey, response);
171-
set.status = 403;
172-
return { error: "Token expired" };
173-
}
174-
175-
deleteTokenQuery.run(params.siteKey, response);
176-
return { success: true };
177132
});

standalone/src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { assetsServer } from "./assets.js";
55
import { auth } from "./auth.js";
66
import { capServer } from "./cap.js";
77
import { server } from "./server.js";
8+
import { siteverifyServer } from "./siteverify.js";
89

910
const serverPort = process.env.SERVER_PORT || 3000;
1011
const serverHostname = process.env.SERVER_HOSTNAME || "0.0.0.0";
@@ -71,6 +72,7 @@ new Elysia({
7172
.use(server)
7273
.use(assetsServer)
7374
.use(capServer)
75+
.use(siteverifyServer)
7476
.listen(serverPort);
7577

7678
console.log(`🧢 Cap running on http://${serverHostname}:${serverPort}`);

0 commit comments

Comments
 (0)