Metadata-Version: 2.4
Name: xms-dns
Version: 0.2.3
Summary: Cliente Python para gestionar DNS en el panel XMS (Digital Value). CLI, biblioteca y servidor dyndns2/checkip compatible con ddclient. A, AAAA y TXT.
Project-URL: Homepage, https://github.com/soukron/xms-dns
Project-URL: Repository, https://github.com/soukron/xms-dns
Project-URL: Issues, https://github.com/soukron/xms-dns/issues
Author-email: Sergio Garcia <soukron@gmbros.net>
Maintainer-email: Sergio Garcia <soukron@gmbros.net>
License-Expression: CC-BY-NC-4.0
License-File: LICENSE
Keywords: ddclient,digitalvalue,dns,dyndns,xms
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: System Administrators
Classifier: License :: Free for non-commercial use
Classifier: Operating System :: OS Independent
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 :: Name Service (DNS)
Classifier: Topic :: System :: Networking
Requires-Python: >=3.9
Requires-Dist: beautifulsoup4>=4.12
Requires-Dist: fastapi>=0.100
Requires-Dist: httpx>=0.27
Requires-Dist: lxml>=5.0
Requires-Dist: pyyaml>=6.0
Requires-Dist: typing-extensions>=4.0; python_version < '3.11'
Requires-Dist: uvicorn>=0.23
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == 'dev'
Description-Content-Type: text/markdown

# xms-dns

[PyPI](https://pypi.org/project/xms-dns/)
[Python](https://pypi.org/project/xms-dns/)
[License: CC BY-NC 4.0](https://creativecommons.org/licenses/by-nc/4.0/)

Cliente Python para gestionar DNS en el panel XMS (Digital Value). Incluye CLI (`xms-dns`), biblioteca y servidor dyndns2/checkip (`xms-dns-server`) compatible con ddclient. Soporta registros A, AAAA y TXT.

## Setup

```bash
pip install xms-dns
```

For development:

```bash
git clone https://github.com/soukron/xms-dns.git
cd xms-dns
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
```



### Config file (optional, no secrets)

Create `~/.config/xms-dns/config` with non-sensitive settings only:

```bash
mkdir -p ~/.config/xms-dns
cat > ~/.config/xms-dns/config <<'EOF'
XMS_BASE_URL=https://xms.digitalvalue.es
XMS_LOGIN=admin#example.test
EOF
chmod 600 ~/.config/xms-dns/config
```

Passwords are **not** read from this file. Use `xms-dns login` instead.

Environment variables override the config file. Override paths with `XMS_CONFIG_DIR` or `XMS_CACHE_DIR`.

### Login

```bash
xms-dns login
```

Resolves settings in this order:

1. Environment variables (`XMS_BASE_URL`, `XMS_LOGIN`, `XMS_PASSWORD`)
2. Config file (`XMS_BASE_URL`, `XMS_LOGIN` only)
3. Interactive prompts for anything still missing (password always prompted unless in env)

Connection type is always `siempre` (permanent session). On success, the session cookie is saved to `~/.cache/xms-dns/auth.cache`.

### HTTP calls

Login and operations are separate executions:

- `xms-dns login` — authenticates against the panel and saves the session (its own HTTP calls; not counted against other commands).
- **Any other command** — exactly **one** HTTP request to the panel, using the cached session. Credentials are never sent again.

Example: `xms-dns domains` is a single `GET` to `Operacion=Dominios`. `xms-dns list` is a single `GET` to load the zone.

The only exception is `upsert` **when the value changes**: XMS has no inline edit, so the client deletes and recreates the record (two or three requests).

## CLI

Global flags:

- `-o` / `--output` — output format: `table` (default), `csv`, `json`, or `yaml`
- `-v` / `--verbose` — log HTTP requests to stderr
- `--debug` — log requests and response bodies to stderr (includes verbose)

```bash
xms-dns login
xms-dns version
xms-dns whoami
xms-dns list gmbros.net
xms-dns -o csv list gmbros.net
xms-dns -o json domains
xms-dns -v list gmbros.net
xms-dns --debug upsert gmbros.net dyndns-test A 203.0.113.10
```

### `version` and `whoami`

```bash
xms-dns version              # versión instalada (texto plano)
xms-dns -o json version      # {"version": "0.2.2"}

xms-dns whoami               # config cargada + validez de la sesión cacheada
xms-dns -o json whoami
# {"xms_base_url": "...", "xms_login": "admin#example.test", "session_valid": "yes"}
```

`whoami` lee `XMS_BASE_URL` y `XMS_LOGIN` desde entorno o config (no pide contraseña) y comprueba la sesión cacheada con una petición al panel.

DNS commands use the cached session only. If not logged in or session expired:

```
error: Not logged in. Run: xms-dns login
```



## Server (dyndns2 + checkip)

Start the dynamic DNS server:

```bash
xms-dns-server
```

Environment variables:


| Variable          | Default                       | Purpose                                                                           |
| ----------------- | ----------------------------- | --------------------------------------------------------------------------------- |
| `XMS_BASE_URL`    | `https://xms.digitalvalue.es` | XMS panel URL                                                                     |
| `XMS_SERVER_HOST` | `127.0.0.1`                   | Bind address                                                                      |
| `XMS_SERVER_PORT` | `8080`                        | Bind port                                                                         |
| `XMS_TRUST_PROXY` | unset                         | Set to `1` to trust `X-Forwarded-For` / `X-Real-Ip` for client IP and access logs |
| `XMS_CACHE_DIR`   | `~/.cache/xms-dns`            | Session cache directory                                                           |


> **Reverse proxy (production):** if `xms-dns-server` runs behind nginx, Caddy, Traefik, etc., set `XMS_TRUST_PROXY=1`. Otherwise `/checkip` and updates without `myip` see the proxy IP (`127.0.0.1`) instead of the client. Only enable this when a trusted proxy overwrites `X-Forwarded-For` — do not expose the server directly to the internet with this flag on.



### Authentication

The server does **not** use `xms-dns login`. Each dyndns2 client sends XMS credentials via HTTP Basic auth:

- `username` → XMS login (e.g. `admin#example.test`)
- `password` → XMS password

The session cookie is cached per user in `~/.cache/xms-dns/server/{sha256}.cache` (separate from the CLI `auth.cache`). If the cached session expires, the server re-authenticates with the credentials from the current request.

### Endpoints

#### dyndns2

- `GET /nic/update?hostname=home.example.test&myip=...` — actualización dyndns2. Respuesta en texto plano: `good {value}`, `nochg {value}`, `badauth`, `nohost`, etc. Siempre HTTP 200 (los clientes leen el cuerpo, no el código).
- Tipos de registro: **A** (por defecto), **AAAA**, **TXT**.
  - A / AAAA: dirección en `myip`, o se usa la IP del cliente (`X-Forwarded-For` con `XMS_TRUST_PROXY=1`).
  - AAAA: `type=AAAA` o IPv6 en `myip` (auto-detectado).
  - TXT: `type=TXT` y valor en `txt=` (o `value=`).

Credenciales XMS vía HTTP Basic auth (`username` = login XMS, p. ej. `admin#dominio.test`).

Ejemplo ddclient:

```
protocol=dyndns2
server=dyndns.example.test
login=admin#example.test
password=your-xms-password
home.example.test
```

Synology (proveedor DDNS personalizado):

```
https://__USERNAME__:__PASSWORD__@dyndns.example.test/nic/update?hostname=__HOSTNAME__&myip=__MYIP__
```

#### checkip (compatible con [ifconfig.me](https://ifconfig.me/))

| Ruta | Respuesta |
|------|-----------|
| `GET /checkip` | IPv4 del cliente (texto plano) |
| `GET /checkip?format=pfsense` | `ip=…` (legacy pfSense) |
| `GET /checkip/ip` | IPv4 |
| `GET /checkip/ua` | cabecera `User-Agent` |
| `GET /checkip/lang` | `Accept-Language` |
| `GET /checkip/encoding` | `Accept-Encoding` |
| `GET /checkip/mime` | `Accept` |
| `GET /checkip/charset` | `Accept-Charset` |
| `GET /checkip/forwarded` | `X-Forwarded-For` (raw) |
| `GET /checkip/all` | todos los campos, una línea `key: value` por campo |
| `GET /checkip/all.json` | mismo contenido en JSON |

```bash
curl https://dyndns.example.test/checkip
curl https://dyndns.example.test/checkip/ua
curl https://dyndns.example.test/checkip/all
curl https://dyndns.example.test/checkip/all.json
```

Campos en `/all` y `/all.json`: `ip_addr`, `remote_host` (siempre `unavailable`), `user_agent`, `port`, `language`, `referer`, `connection`, `keep_alive`, `method`, `encoding`, `mime`, `charset`, `via`, `forwarded`.

En un **navegador** (cabecera `Accept: text/html`), la raíz de checkip (`/checkip` o `/` vía host dedicado) muestra una página HTML con la tabla de conexión y ejemplos CLI. `curl` y clientes API siguen recibiendo texto plano.

### Access log

El servidor escribe una línea de access log por petición (formato uvicorn), con la **IP real del cliente** cuando `XMS_TRUST_PROXY=1` (lee `X-Forwarded-For` / `X-Real-Ip`).

Ejemplos:

```
INFO:     170.253.60.198:42188 - "GET /checkip (ip=170.253.60.198) HTTP/1.1" 200 OK
INFO:     170.253.60.198:42188 - "GET /nic/update?hostname=home.example.test&myip=… (good type=A value=170.253.60.198) HTTP/1.1" 200 OK
WARNING:  170.253.60.198:42188 - "GET /nic/update?hostname=… (badauth type=A reason=XMS authentication failed) HTTP/1.1" 200 OK
```

Los logs de dyndns2 **no incluyen** login ni otras credenciales.

### Production deployment (Docker + Traefik)

Ejemplo de despliegue con Traefik: un contenedor `xms-dns-server` (PyPI o build local), TLS en el edge y rate limit por IP.

Variables típicas (`config.env`):

```bash
TRAEFIK_HOST=dyndns.example.test
TRAEFIK_CHECKIP_HOST=checkip.example.test   # HTTPS (websecure) + HTTP (web, puerto 80)
TRAEFIK_ENTRYPOINT=websecure
TRAEFIK_ENTRYPOINT_HTTP=web
XMS_TRUST_PROXY=1
XMS_CACHE_DIR=/data/cache
```

**checkip en HTTP y HTTPS:** Traefik expone `checkip.*` en el puerto 443 (TLS) y también en el 80 (sin redirect). `dyndns.*` redirige HTTP→HTTPS por router. El edge Traefik no debe tener redirect global en el entrypoint `web` (solo redirects por servicio).

**Host dedicado para checkip (opcional):** Traefik puede reescribir rutas sin redirección HTTP, apuntando al mismo backend:

| Petición pública | Backend |
|------------------|---------|
| `https://checkip.example.test/` | `/checkip` |
| `https://checkip.example.test/ua` | `/checkip/ua` |
| `https://checkip.example.test/all.json` | `/checkip/all.json` |

Labels Traefik (extracto):

```yaml
# Raíz → /checkip
traefik.http.middlewares.xms-checkip-root.replacepath.path: /checkip
traefik.http.routers.xms-checkip-root.rule: Host(`checkip.example.test`) && Path(`/`)
traefik.http.routers.xms-checkip-root.middlewares: xms-checkip-root@docker,…

# /ua, /all, … → /checkip/…
traefik.http.middlewares.xms-checkip-rewrite.replacepathregex.regex: ^/(.*)
traefik.http.middlewares.xms-checkip-rewrite.replacepathregex.replacement: /checkip/$1
traefik.http.routers.xms-checkip.rule: Host(`checkip.example.test`)
traefik.http.routers.xms-checkip.middlewares: xms-checkip-rewrite@docker,…
```

Requiere registro DNS para `checkip.example.test` (misma IP que el edge). Let's Encrypt vía Traefik.

If `myip` is omitted on dyndns2 updates, the client IP is used (see `X-Forwarded-For` when `XMS_TRUST_PROXY=1`).

Put TLS termination on a reverse proxy in production; bind locally by default. Remember `XMS_TRUST_PROXY=1` on the server process.

## Library

```python
from xms_dns import XmsClient

# Login once (CLI does this via `xms-dns login`)
with XmsClient(base_url="...", login="...", password="...") as client:
    client.authenticate()

# Later operations reuse auth.cache
with XmsClient.from_env() as client:
    client.ensure_session()
    client.upsert_record("gmbros.net", "home", "A", "203.0.113.10")
```



## Tests

All default tests use mocked HTTP responses and **do not** contact the live XMS panel:

```bash
pytest
```



## Releasing

CI runs on every push/PR to `main` (tests only). **PyPI publish runs on tags**, not on every push.

The package version comes from the **git tag** (`hatch-vcs`), not from `pyproject.toml`:

```bash
# 1. Update CHANGELOG.md
# 2. Commit and push
git commit -am "Release 0.1.2"
git push origin main

# 3. Tag and push — version 0.1.2 is taken from v0.1.2
git tag v0.1.2
git push origin v0.1.2
```



### One-time PyPI setup (Trusted Publishing)

No API token in GitHub secrets. Configure once in PyPI:

1. [https://pypi.org/manage/project/xms-dns/settings/publishing/](https://pypi.org/manage/project/xms-dns/settings/publishing/)
2. **Add a new pending publisher** → GitHub
3. Owner: `soukron`, repository: `xms-dns`, workflow: `publish.yml`, environment: `pypi`
4. In GitHub: repo **Settings → Environments → New environment** → name it `pypi` (no secrets needed)



## Live smoke test

```bash
xms-dns login   # or set XMS_PASSWORD for one-shot login inside the script
XMS_LIVE=1 python scripts/smoke_test.py
```



## Security

- Do not store passwords in the config file
- Keep `~/.cache/xms-dns/auth.cache` private (`chmod 600`)
- Keep server session caches private too: `~/.cache/xms-dns/server/*.cache` (`chmod 600`)



## License

Copyright (c) 2026 Sergio Garcia.

Licensed under [CC BY-NC 4.0](https://creativecommons.org/licenses/by-nc/4.0/): non-commercial use with attribution required.