Metadata-Version: 2.4
Name: python-peerjs-server
Version: 1.0.0b1
Summary: Python asyncio port of the PeerJS signalling server
Author: Kaundur
License: Copyright 2026 Kaundur
        
        Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
        
Project-URL: Homepage, https://github.com/Kaundur/python-peerjs-server
Project-URL: Repository, https://github.com/Kaundur/python-peerjs-server
Project-URL: Bug Tracker, https://github.com/Kaundur/python-peerjs-server/issues
Keywords: peerjs,webrtc,websocket,signalling,signaling,asyncio
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.13
Classifier: Framework :: AsyncIO
Classifier: Topic :: Internet
Classifier: Topic :: System :: Networking
Classifier: Typing :: Typed
Requires-Python: >=3.13
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: websockets>=14
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
Requires-Dist: ruff; extra == "dev"
Requires-Dist: mypy; extra == "dev"
Requires-Dist: bandit; extra == "dev"
Dynamic: license-file

# python-peerjs-server

An `asyncio`-based Python port of the [PeerJS signalling server](https://github.com/peers/peerjs-server), the WebSocket
broker that PeerJS clients use to discover each other and exchange WebRTC offers/answers/candidates.

It's feature-complete and wire-compatible with the original: WebSocket session handling, reconnect, and the HTTP API
are all implemented, plus a few deployment conveniences, like rate limiting,
proxied-IP support, and bounded queues.

## Features

- WebSocket signalling at `{path}/peerjs` with `id`/`token`/`key` query-param auth
- Reconnect support (same `id` + `token` re-attaches the socket and drains queued messages)
- Per-peer message queueing for offline destinations, with periodic `EXPIRE` sweeps
- Periodic dead-connection reaping based on heartbeat pings
- HTTP API: `GET {path}{key}/id` (fresh peer id) and `GET {path}{key}/peers` (peer list, opt-in via `allow_discovery`)
- CORS header support
- Fixed-window per-IP rate limiting on the HTTP API
- Optional `X-Forwarded-For` trust for real client IPs when running behind a reverse proxy

## Requirements

- Python 3.13+
- `websockets>=14` (the only runtime dependency, installed automatically below)

## Installation

```bash
pip install -e .
```

## Usage

Run as a console script:

```bash
peerjs-server --port 9000 --key peerjs
```

or as a module:

```bash
python -m python_peerjs_server --port 9000 --key peerjs
```

### CLI options

| Flag                | Env var                  | Default  | Description                        |
|---------------------|--------------------------|----------|------------------------------------|
| `--host`            | `PEERJS_HOST`            | `::`     | Bind host                          |
| `--port`            | `PEERJS_PORT`            | `9000`   | Bind port                          |
| `--key`             | `PEERJS_KEY`             | `peerjs` | Access key clients must supply     |
| `--path`            | `PEERJS_PATH`            | `/`      | Path prefix for HTTP/WS routes     |
| `--allow-discovery` | `PEERJS_ALLOW_DISCOVERY` | off      | Expose `GET /peers`                |
| `--cors-origin`     | `PEERJS_CORS_ORIGIN`     | unset    | CORS origin for HTTP API responses |
| `--log-level`       | `PEERJS_LOG_LEVEL`       | `INFO`   | Logging level                      |

Other server behaviour (timeouts, rate limits, queue caps, proxy trust) is configured via
`python_peerjs_server.config.Config` when embedding the server programmatically; see
[`src/python_peerjs_server/config.py`](src/python_peerjs_server/config.py) for the full set of fields and their
defaults.

### Embedding in your own asyncio app

```python
import asyncio
from python_peerjs_server.config import Config
from python_peerjs_server.main import peerjs_serve

asyncio.run(peerjs_serve(Config(port=9000, key="peerjs")))
```

### Logging

If you're running via the CLI, `--log-level`/`PEERJS_LOG_LEVEL` already configures this for you (it calls
`logging.basicConfig`); the rest of this section is for embedding `python_peerjs_server` in your own app.

Every module logs under the `python_peerjs_server` namespace (`logging.getLogger(__name__)`), so that one logger name
is the knob to turn:

```python
import logging

# See python_peerjs_server logs at INFO+ on stderr
logging.getLogger("python_peerjs_server").setLevel(logging.INFO)
logging.getLogger("python_peerjs_server").addHandler(logging.StreamHandler())

# Or, if your app already configured the root logger, just set the level;
# propagation does the rest
logging.getLogger("python_peerjs_server").setLevel(logging.DEBUG)

# Silence a noisy sub-logger specifically
logging.getLogger("python_peerjs_server.services.web_socket_session").setLevel(logging.WARNING)
```

A `NullHandler` is attached by default, so the library stays silent unless logging is configured (either on
`python_peerjs_server`
or higher up).

## Connecting a PeerJS client

This server only implements the signalling protocol; clients connect using the [PeerJS](https://peerjs.com/) client
library ([docs](https://peerjs.com/docs/), [GitHub](https://github.com/peers/peerjs)):

```js
const peer = new Peer("some-id", {
    host: "localhost",
    port: 9000,
    path: "/",
    key: "peerjs",
});
```

Runnable versions of the standalone and FastAPI embedding patterns live in [`examples/`](examples/):
[`standalone.py`](examples/standalone.py) and [`fastapi_app.py`](examples/fastapi_app.py).

### CORS

The PeerJS client calls the HTTP API (`GET {path}{key}/id`) before it opens the WebSocket. If your page is served
from a different origin than the broker (e.g. a Vite/CRA dev server on `localhost:5173` talking to a broker on
`localhost:9000`), the browser will block that request with a CORS error unless you set `--cors-origin` /
`PEERJS_CORS_ORIGIN` / `Config.cors_origin` to the *exact* origin (scheme + host + port) serving the page:

```bash
peerjs-server --port 9000 --key peerjs --cors-origin http://localhost:5173
```

It's unset by default (no `Access-Control-Allow-Origin` header sent), which is fine for same-origin deployments.

## Running alongside another web framework

`peerjs_serve()` is just a coroutine; you can run it inside whatever process already hosts your app instead of standing
up a separate service. Pass `handle_signals=False` so it doesn't fight your app for `SIGINT`/`SIGTERM` handling, and
pick a `port` distinct from your main app's.

Runnable versions of both patterns below live in [`examples/`](examples/): [`tornado_app.py`](examples/tornado_app.py)
and [`flask_app.py`](examples/flask_app.py).

### Tornado

Tornado runs on the standard `asyncio` event loop, so the broker can simply be scheduled as another task on it:

```python
import asyncio
import tornado.ioloop
import tornado.web

from python_peerjs_server.config import Config
from python_peerjs_server.main import peerjs_serve


class MainHandler(tornado.web.RequestHandler):
    def get(self) -> None:
        self.write("hello from tornado")


async def main() -> None:
    app = tornado.web.Application([(r"/", MainHandler)])
    app.listen(8888)

    asyncio.create_task(peerjs_serve(Config(port=9000, key="peerjs"), handle_signals=False))

    await asyncio.Event().wait()  # run forever


if __name__ == "__main__":
    asyncio.run(main())
```

### Flask

Flask's dev server is sync/WSGI and blocks its own thread, so the broker has to run on an event loop in a separate
thread:

```python
import asyncio
import threading

from flask import Flask

from python_peerjs_server.config import Config
from python_peerjs_server.main import peerjs_serve

app = Flask(__name__)


@app.get("/")
def index() -> str:
    return "hello from flask"


def run_python_peerjs_server() -> None:
    asyncio.run(peerjs_serve(Config(port=9000, key="peerjs"), handle_signals=False))


if __name__ == "__main__":
    threading.Thread(target=run_python_peerjs_server, daemon=True).start()
    app.run(port=8888)
```

For an ASGI app (FastAPI, Starlette) you can instead start it from a lifespan/startup hook with `asyncio.create_task`,
the same way as the Tornado example: both share a loop with no extra thread needed.

## Development

```bash
pip install -e ".[dev]"  # install with dev/test/lint dependencies
```

```bash
pytest                 # run tests
ruff check src         # lint
ruff format src        # format
mypy src                # type check
bandit -r src           # security scan
```

## License

MIT, see [`LICENSE`](LICENSE).
