Metadata-Version: 2.4
Name: porkbun_ddns
Version: 1.2.0
Summary: A unofficial DDNS-Client for Porkbun domains
Author-email: Nils Stein <github.nstein@mailbox.org>
License: MIT License
        
        Copyright (c) Nils Stein
        
        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: repository, https://github.com/mietzen/porkbun-ddns
Keywords: porkbun,ddns
Classifier: Development Status :: 4 - Beta
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: xdg-base-dirs~=6.0.2
Requires-Dist: jinja2
Provides-Extra: test
Requires-Dist: jsonschema; extra == "test"
Dynamic: license-file

# Disclaimer

**This package is not related to or developed by Porkbun. No relationship between the developer of this package and Porkbun exists.**

**All trademarks, logos and brand names are the property of their respective owners. All company, product and service names used in this package are for identification purposes only. Use of these names,trademarks and brands does not imply endorsement.**

# Porkbun DDNS

`porkbun-ddns` is a unofficial DDNS-Client for Porkbun Domains.
This library will only update the records if the IP(s) have changed or the dns entry didn't exist before, it will also set/update A (IPv4) and AAAA (IPv6) records.


Since [porkbun-dynamic-dns-python](https://github.com/porkbundomains/porkbun-dynamic-dns-python) is deprecated I took it into my own hands to code a decent DDNS Client for Porkbun.
Inspired by [con-f-use](https://github.com/con-f-use) [pull request](https://github.com/porkbundomains/porkbun-dynamic-dns-python/pull/6), I built a pip Package and a docker container.

As alternative to cert-bun use my [lego-certbot](https://github.com/mietzen/lego-certbot) image.

## Setup on Porkbun

Make sure that any domain you use this client with has API access enabled. See the below picture for reference.

![API Access Enabled](API_Access_Enabled.png)

If this is not enabled, you'll see an error about your API keys being invalid, despite them being correct.

# CLI

**Minimum required python version: 3.10**

## Install via pip

```shell
pip install porkbun-ddns
```

## Usage

```Shell
usage: porkbun-ddns [-h] [-c CONFIG] [-e ENDPOINT] [-pk APIKEY] [-sk SECRETAPIKEY] [--retry-count RETRY_COUNT] [--retry-delay RETRY_DELAY] [--webhook-url WEBHOOK_URL] [--webhook-template WEBHOOK_TEMPLATE] [--webhook-template-file WEBHOOK_TEMPLATE_FILE] [--log-level LOG_LEVEL] [-i [PUBLIC_IPS ...]] [-f FRITZBOX] [-4 | -6] [-v] [--env_only] domain [subdomains ...]

positional arguments:
  domain                Domain to be updated
  subdomains            Subdomain(s)

options:
  -h, --help            show this help message and exit
  -c CONFIG, --config CONFIG
                        Path to config file (default: ~/.config/porkbun-ddns-config.json)
  -e ENDPOINT, --endpoint ENDPOINT
                        The endpoint
  -pk APIKEY, --apikey APIKEY
                        The Porkbun-API-key
  -sk SECRETAPIKEY, --secretapikey SECRETAPIKEY
                        The secret API-key
  --retry-count RETRY_COUNT
                        Number of attempts for transient API failures (default: 3)
  --retry-delay RETRY_DELAY
                        Seconds to wait between retry attempts (default: 5)
  --webhook-url WEBHOOK_URL
                        Webhook URL to notify when IPs change
  --webhook-template WEBHOOK_TEMPLATE
                        Jinja2 template for the webhook payload
  --webhook-template-file WEBHOOK_TEMPLATE_FILE
                        Path to a file containing the Jinja2 webhook template (takes precedence over --webhook-template)
  --log-level LOG_LEVEL
                        Set log verbosity (DEBUG, INFO, WARNING, ERROR, CRITICAL)
  -i [PUBLIC_IPS ...], --public-ips [PUBLIC_IPS ...]
                        Public IPs (v4 and or v6)
  -f FRITZBOX, --fritzbox FRITZBOX
                        IP or Domain of your Fritz!Box
  -4, --ipv4-only       Only set/update IPv4 A Records
  -6, --ipv6-only       Only set/update IPv6 AAAA Records
  -v, --verbose         Show Debug Output
  --env_only            Don't use any config, get all variables from the environment
```

### The parameter *endpoint*, *apikey*, *secretapikey*

These parameter are required for each run of the program. The program will take the values for these (in this order) from:

1. The command-line-arguments (`-pk pk1_xxx`)
2. The environment-variables (`export PORKBUN_APIKEY='pk1_xxx'`)
3. The config-file (`apikey="pk_xxx"`)

In Docker use the `API_ENDPOINT` environment-variable instead.

So if a value is set through the CLI and in the file, the CLI-value will be used. This allows for a default-configuration in the config-file, whose settings can be selectively overridden through enviromnment-variables or CLI-arguments.

### The parameter *retry_count*, *retry_delay*

Transient API failures (unreachable endpoint, timeouts, HTTP 5xx) are retried automatically, HTTP 4xx errors (e.g. invalid API keys) fail immediately. Default is 3 attempts with a 5 seconds delay between them.

The program will take the values for these (in this order) from:

1. The command-line-arguments (`--retry-count 3`)
2. The environment-variables (`export PORKBUN_RETRY_COUNT='3'`)
3. The config-file (`retry_count="3"`)

### The parameter *webhook_url*, *webhook_template*, *webhook_template_file*

When the IP(s) of your records change, an aggregated webhook-notification can be POSTed to a URL of your choice. This works out of the box with Slack, MS Teams, Mattermost and Google Chat.

The program will take the values for these (in this order) from:

1. The command-line-arguments (`--webhook-url 'https://...'`)
2. The environment-variables (`export PORKBUN_WEBHOOK_URL='https://...'`)
3. The config-file (`webhook_url="https://..."`)

In Docker use the `WEBHOOK_URL`, `WEBHOOK_TEMPLATE` and `WEBHOOK_TEMPLATE_FILE` environment-variables instead.

The payload can be customized with an inline Jinja2-template (`--webhook-template`) or a template-file (`--webhook-template-file`), where the file takes precedence over the inline one. If neither is set, the following Slack-compatible default is used:

```json
{"text": "IP changed: {{ old_ips | join(', ') }} -> {{ new_ips | join(', ') }} ({{ domain }})"}
```

The following context-variables are available in templates: `changes` (list of changes, each `{record_type, fqdn, old_ip|None, new_ip}`), `old_ips` (previous IPs), `new_ips` (new IPs), `domain` (the updated domain) and `timestamp` (ISO-8601 UTC timestamp of the notification).

One notification is sent per run, after all records have been updated. Notifications are fire-and-forget: a failure to deliver never crashes the update-loop.

### The parameter *log_level*

Controls the verbosity of the logs. Accepts standard logging level names, case-insensitively: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` (default `INFO`). Set it via `--log-level WARNING` on the CLI or `LOG_LEVEL=WARNING` in Docker.

When both the legacy `--verbose`/`DEBUG` and `LOG_LEVEL` are set, `LOG_LEVEL` wins. An invalid value logs a warning and falls back to `INFO`, it never crashes.

### Examples

```shell
# using the default config-file in ~/.config/porkbun-ddns-config.json:
$ porkbun-ddns domain.com my_subdomain

# Using only environment variables:
# PORKBUN_APIKEY
# PORKBUN_SECRETAPIKEY
# PORKBUN_ENDPOINT (Optional)
$ porkbun-ddns domain.com my_subdomain --env_only

# Specific config-file:
$ porkbun-ddns domain.com my_subdomain -c "./config.json"

# Multiple subdomains:
$ porkbun-ddns domain.com my_subdomain_1 my_subdomain_2 my_subdomain_3

# Set root and subdomains:
$ porkbun-ddns domain.com @ my_subdomain_1 my_subdomain_2 my_subdomain_3

# Set wildcard domain:
$ porkbun-ddns domain.com '*'

# Set IP's explicit
$ porkbun-ddns domain.com my_subdomain -i '1.2.3.4' '1234:abcd:0:4567::8900'

# Use Fritz!Box to obtain IP's and set IPv4 A Record only
$ porkbun-ddns "./config.json" domain.com my_subdomain -f fritz.box -4
```

You can set up a cron job get the full path to porkbun-ddns with `which porkbun-ddns`, then execute `crontab -e` and add the following line:

```
*/30 * * * * <PORKBUN-DDNS-PATH>/porkbun-ddns "<YOUR-PATH>/config.json" domain.com my.subdomain >/dev/null 2>&1
```

`config.json` example:

```
{
  "endpoint":"https://api.porkbun.com/api/json/v3",
  "apikey": "pk1_xxx",
  "secretapikey": "sk1_xxx",
  "retry_count": "3",
  "retry_delay": "5",
  "webhook_url": "https://hooks.slack.com/services/..."
}
```

# Docker compose

```yaml
services:
  porkbun-ddns:
    image: "mietzen/porkbun-ddns:latest"
    container_name: porkbun-ddns
    environment:
      DOMAIN: "domain.com" # Your Porkbun domain
      SUBDOMAINS: "my_subdomain,my_other_subdomain,my_subsubdomain.my_subdomain" # Subdomains comma spreaded
      SECRETAPIKEY: "<YOUR-SECRETAPIKEY>" # Your Porkbun Secret-API-Key
      APIKEY: "<YOUR-APIKEY>" # Your Porkbun API-Key
      # API_ENDPOINT: "https://api.porkbun.com/api/json/v3" # Override the Porkbun API endpoint (e.g. a mirror/proxy)
      # PUBLIC_IPS: "1.2.3.4,2001:043e::1" # Set if you got static IP's
      # FRITZBOX: "192.168.178.1" # Use Fritz!BOX to obtain Public IP's
      # SLEEP: "300" # Seconds to sleep between DynDNS runs
      # IPV4: "TRUE" # Set IPv4 address
      # IPV6: "TRUE" # Set IPv6 address
      # DEBUG: "FALSE" # DEBUG LOGGING
      # LOG_LEVEL: "WARNING" # Set log verbosity (DEBUG, INFO, WARNING, ERROR, CRITICAL)
      # RETRY_COUNT: "3" # Number of attempts for transient API failures
      # RETRY_DELAY: "5" # Seconds to wait between retry attempts
      # WEBHOOK_URL: "https://hooks.slack.com/services/..." # POST an IP-change notification to this URL (Slack, MS Teams, Mattermost, Google Chat compatible by default)
      # WEBHOOK_TEMPLATE: '{"text": "IP changed: {{ old_ips | join(", ") }} -> {{ new_ips | join(", ") }} ({{ domain }})"}' # Optional custom Jinja2 template
      # WEBHOOK_TEMPLATE_FILE: "/path/to/template.j2" # Optional Jinja2 template file (takes precedence over WEBHOOK_TEMPLATE)
    restart: unless-stopped

# # Uncomment below to let it detect ipv6 address:
#     networks:
#       - ipv6_enabled

# networks:
#   ipv6_enabled:
#     enable_ipv6: true

```

# Docker run

```shell
docker run -d \
  -e DOMAIN="domain.com" \
  -e SUBDOMAINS="my_subdomain,my_other_subdomain,my_subsubdomain.my_subdomain" \
  -e SECRETAPIKEY="<YOUR-SECRETAPIKEY>" \
  -e APIKEY="<YOUR-APIKEY>" \
  -e LOG_LEVEL="WARNING" \
  -e WEBHOOK_URL="https://hooks.slack.com/services/..." \
  --name porkbun-ddns \
  --restart unless-stopped \
  mietzen/porkbun-ddns:latest
```

# Python

**Minimum required python version: 3.10**

```python
from pathlib import Path
from porkbun_ddns import PorkbunDDNS
from porkbun_ddns.config import Config, DEFAULT_ENDPOINT, extract_config


config = Config(DEFAULT_ENDPOINT, "YOUR-APIKEY", "YOUR-SECRETAPIKEY")
porkbun_ddns = PorkbunDDNS(config, 'domain.com')
# config = extract_config(Path("./config.json"))
# porkbun_ddns = PorkbunDDNS(config, 'domain.com')
# porkbun_ddns_ip = PorkbunDDNS(config, 'domain.com', public_ips=['1.2.3.4','1234:abcd:0:4567::8900'])
# porkbun_ddns_fritz = PorkbunDDNS(config, 'domain.com', fritzbox_ip='fritz.box', ipv6=False)

porkbun_ddns.set_subdomain('my_subdomain')
porkbun_ddns.update_records()
```
