Metadata-Version: 2.4
Name: ray-slide-captcha
Version: 1.0.0
Summary: Slide puzzle captcha Python backend library (framework-agnostic core service, FastAPI router optional)
License: MIT
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Security
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: Pillow>=10.0.0
Provides-Extra: fastapi
Requires-Dist: fastapi>=0.110.0; extra == "fastapi"
Requires-Dist: pydantic>=2.5.0; extra == "fastapi"
Provides-Extra: dev
Requires-Dist: uvicorn[standard]>=0.27.0; extra == "dev"
Requires-Dist: fastapi>=0.110.0; extra == "dev"
Requires-Dist: pydantic>=2.5.0; extra == "dev"
Dynamic: license-file

# ray-slide-captcha

Slide puzzle captcha Python backend library.

[简体中文](./README.zh-CN.md) | English

Companion frontend: [ray-captcha-react](https://github.com/ray-wzy/ray-captcha-react)

## Features

- Core service `CaptchaService` has **zero framework dependencies** — works with any Python project (Flask / Django / Starlette / FastAPI, etc.)
- FastAPI router shipped as an **optional adapter**, ready out of the box
- Background pre-generated asset pool, response < 10ms
- IP rate limiting + failure penalty + one-time token
- HMAC-SHA256 signed token, consumed once by the business layer
- Multi-source background image downloader with automatic retry on failure

## Installation

```bash
# Core library (only depends on Pillow, works with any Python framework)
pip install ray-slide-captcha

# With FastAPI router adapter
pip install ray-slide-captcha[fastapi]
```

## Quick Start

### Any Python framework

```python
from ray_slide_captcha import CaptchaService

service = CaptchaService(secret_key="your-secret-key-at-least-32-chars")
challenge = service.create_challenge(client_ip="1.2.3.4")
# Write your own routes to handle challenge / verify / consume_token
```

### FastAPI

```python
from fastapi import FastAPI
from ray_slide_captcha import CaptchaService
from ray_slide_captcha.fastapi import create_captcha_router

app = FastAPI()
service = CaptchaService(secret_key="your-secret-key-at-least-32-chars")
app.include_router(create_captcha_router(service), prefix="/api")

# Consume token in your business endpoint
@app.post("/login")
def login(token: str):
    if not service.consume_token(token):
        raise HTTPException(400, "Invalid captcha")
    # ...
```

### Run the example server

```bash
pip install -e .[dev]
cd examples
python main.py
# Server runs at http://localhost:8000
```

## CaptchaService Configuration

| Parameter | Default | Description |
| --- | --- | --- |
| `secret_key` | required | HMAC signing key, length >= 32 |
| `canvas_width` | 320 | Canvas width |
| `canvas_height` | 180 | Canvas height |
| `puzzle_width` | 60 | Puzzle piece width |
| `puzzle_height` | 60 | Puzzle piece height |
| `position_tolerance` | 8 | Position tolerance (px) |
| `challenge_ttl` | 120 | Challenge validity (seconds) |
| `token_ttl` | 300 | Token validity (seconds) |
| `pool_size` | 50 | Pre-generated pool size |

## API Endpoints

### `GET /captcha/challenge`

Fetch a new captcha challenge.

**Response:**

```json
{
  "code": 200,
  "msg": "Captcha challenge generated",
  "data": {
    "id": "uuid",
    "bgUrl": "data:image/png;base64,...",
    "puzzleUrl": "data:image/png;base64,...",
    "expires_in": 120
  }
}
```

> **Note**: All `msg` values are English by default. To localize, modify the `ERR_MESSAGES` dict in `fastapi.py` or pass a custom `msg` to `ok()` / `fail()`.

### `POST /captcha/verify`

Verify the drag position and return a one-time token.

**Request body:**

```json
{
  "id": "uuid",
  "x_position": 150,
  "drag_duration_ms": 800
}
```

**Response:**

```json
{
  "code": 200,
  "msg": "Verification successful",
  "data": {
    "success": true,
    "token": "eyJjbGllbnRJZCI6IC...",
    "expires_in": 300
  }
}
```

### `CaptchaService.consume_token(token)`

Called by the business layer to consume a token once (invalidated after consumption).

```python
from ray_slide_captcha import CaptchaService

service = CaptchaService(secret_key="...")

if not service.consume_token(token):
    raise Exception("Invalid or expired captcha")
```

## Anti-Abuse Mechanisms

| Mechanism | Threshold | Description |
| --- | --- | --- |
| IP rate limit | 60 / min | challenge endpoint |
| Failure penalty | 6 consecutive fails | 5s cooldown |
| Drag duration | 180ms - 60s | Too fast / too slow are rejected |
| Position tolerance | ±8px | Bot brute-force hit rate < 3% |
| Token consumption | one-time | HMAC signed, invalidated on consumption |

## License

MIT
