diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..301f34b --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Runtime state and secrets generated by OpenTAKServer on first boot. +# None of this belongs in version control — the tracked content under ots/ is +# limited to configs/ and mediamtx/ templates. + +# PKI: CA signing key, issued client keys, .p12 bundles, CRL state. +# ca-do-not-share.key signs every cert the server trusts. +# Note the trailing /* rather than /: excluding the directory itself would make +# the !negation below impossible to honour. +/ots/ca/* +!/ots/ca/.gitignore + +# Postgres data directory +/ots/pgdata/ + +# Logs, uploads, and generated runtime config (holds broker credentials) +/ots/logs/ +/ots/uploads/ +/ots/config.yml +/ots/icons.sqlite + +# Certbot / Let's Encrypt material +/ots/configs/nginx/letsencrypt/ + +# macOS +.DS_Store + +ots_config.env + +# Symlink to ots_config.env so compose can interpolate ${OTS_DB_PORT}. +.env diff --git a/Dockerfile b/Dockerfile index 431f09a..e32c59f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,8 +13,15 @@ RUN chown -R ots:ots /app RUN python -m venv /app/venv ENV PATH="/app/venv/bin:$PATH" -# TODO: Install from PyPI -RUN pip install git+https://github.com/brian7704/OpenTAKServer.git +# Pinned to a tagged release. Unpinned git HEAD ships mid-development +# snapshots; 1.7.11 is the highest version with matching ghcr handler images. +RUN pip install opentakserver==1.7.11 + +# Fix two upstream 1.7.11 bugs that stop CoT reaching clients. See the script +# for details; it fails the build if either anchor is missing, so a version bump +# cannot silently drop a patch. +COPY --chown=ots:ots patches/fix_eud_handler.py /tmp/fix_eud_handler.py +RUN python3 /tmp/fix_eud_handler.py RUN /app/venv/bin/flask --app /app/venv/lib/python3.13/site-packages/opentakserver/app.py ots create-ca #RUN /app/venv/bin/flask --app /app/venv/lib/python3.13/site-packages/opentakserver/app.py db upgrade diff --git a/README.md b/README.md index 69dc0f7..93bbc0a 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,101 @@ # OpenTAKServer-Docker -This repo contains the docker-compose.yml and config files needed to run OpenTAKServer in a docker container. -To use it, install docker on your system, clone this repo, and run `docker compose up -d` or `sudo docker compose up -d` -depending on your platform. +This repo contains the `docker-compose.yml` and config files needed to run OpenTAKServer in a +docker container. -This repo is new and not quite complete. Full documentation will be available on https://docs.opentakserver.io when it's ready -for use. CloudTAK will also be incorporated into docker-compose.yml. \ No newline at end of file +This repo is new and not quite complete. Full documentation will be available at +https://docs.opentakserver.io when it's ready for use. CloudTAK will also be incorporated into +`docker-compose.yml`. + +## Table of contents + +- [Getting started](#getting-started) +- [Configuration](#configuration) + - [Host ports](#host-ports) +- [Scripts](#scripts) +- [Usage with Colata API](#usage-with-colata-api) + +## Getting started + +To use it, install docker on your system, clone this repo. + +Create config file: + +```sh +cp ots_config.env.template ots_config.env +``` + +In `ots_config.env`: + +```diff ++ OTS_FQDN= ++ OTS_MEDIAMTX_TOKEN= +``` + +For `OTS_FQDN` set server's host IP: + +``` +ipconfig getifaddr en0 +``` + +The `.env` symlink is required. `env_file:` in `docker-compose.yml` passes +`ots_config.env` *into the containers*, but Compose resolves `${...}` placeholders in the +compose file itself only from the shell environment or a `.env` file. Without the symlink +the port variables below are silently ignored and those host ports fall back to their +defaults. + +```sh +ln -s ots_config.env .env +``` + +Run `docker compose up -d` (or `sudo docker compose up -d`, depending on your platform). + +A successful run opens the TAK admin panel at `http://localhost:80`. + +## Configuration + +### Host ports + +Every published host port is configurable, so this stack can coexist with other local +projects. In each case only the **host** side moves; the container side is fixed. + +| Variable | Default | Published for | +| --- | --- | --- | +| `OTS_DB_PORT` | `5433` | Postgres | +| `OTS_WEB_HTTP_PORT` | `80` | HTTP Web UI | +| `OTS_WEB_HTTPS_PORT` | `443` | HTTPS Web UI | +| `OTS_API_HTTP_PORT` | `18080` | HTTP API (nginx-proxy) | +| `OTS_API_HTTPS_PORT` | `8443` | HTTPS API (nginx-proxy) | +| `OTS_API_INTERNAL_PORT` | `8081` | Internal Flask API server | +| `OTS_CERT_ENROLLMENT_PORT` | `8446` | Certificate enrollment | +| `OTS_MQTT_PORT` | `8883` | MQTT / Meshtastic | + +`OTS_DB_PORT`'s container side stays 5432 because OTS reaches its database via +`SQLALCHEMY_DATABASE_URI=...@ots-db/ots`, which carries no port and resolves over the +compose network. + +The `nginx-proxy` container sides are fixed because each is a hardcoded `listen` directive +in `ots/configs/nginx/templates` — remap the host side and nginx still listens where it +always did, but change the container side and nothing is listening there at all. + +Two caveats when moving the web ports: + +- Clients must then include the port explicitly. `markers.sh` and anything else calling + `https:///api/...` assumes `443`. +- `OTS_FQDN` carries no port, so certificate enrollment and any TAK client profile deriving + URLs from it still expect the defaults. + +## Scripts + +To manage markers manually use the `bin/markers.sh` or `bin/remove_all_markers.sh` script. See docs in +script comments. + +## Usage with Colata API + +This setup was created to work specially with [Colata API](https://github.com/codequest-eu/drone-detection-api). + +1. Run TAK server before the API to receive initial detector events. +2. Make sure the TAK client (like Android's `ATAK`) is properly connected to the TAK server via + TCP (`8080` port) using the IP provided in `ots_config.env`. ⚠️ The TAK server and client have + to be on the same network for this to work. +3. Run the API with the `atak-worker` service. diff --git a/bin/markers.sh b/bin/markers.sh new file mode 100755 index 0000000..79652eb --- /dev/null +++ b/bin/markers.sh @@ -0,0 +1,175 @@ +#!/usr/bin/env bash +# +# Create a marker on the OpenTAKServer map via POST /api/markers. +# The marker is stored in the DB and broadcast to connected TAK clients. +# +# ------------------------------------------------------------------------------ +# EXAMPLES +# ------------------------------------------------------------------------------ +# +# Create an unknown-ground marker (type defaults to a-u-G): +# ./markers.sh -n "Waypoint 1" -a 52.2297 -o 21.0122 +# +# Create a friendly marker (blue icon on the client): +# ./markers.sh -n "OP North" -a 52.2297 -o 21.0122 -t a-f-G-U-C +# +# Create a hostile marker (red icon): +# ./markers.sh -n "Contact" -a 52.3100 -o 21.1100 -t a-h-G +# +# List every marker currently on the map (paginated, 10 per page): +# ./markers.sh -l +# +# Move or rename an existing marker by reusing its UID. This updates the +# marker in place rather than creating a duplicate: +# ./markers.sh -n "OP North" -a 52.4000 -o 21.0122 -t a-f-G-U-C \ +# -u 10e99852-26f2-454f-9acb-86da78f46cf9 +# +# Delete a marker: +# ./markers.sh -d 10e99852-26f2-454f-9acb-86da78f46cf9 +# +# Target a different server, or use different credentials, without editing +# this file: +# OTS_HOST=10.0.0.5 ./markers.sh -n Rally -a 52.1 -o 21.0 +# OTS_USER=alice OTS_PASS=hunter2 ./markers.sh -n Rally -a 52.1 -o 21.0 +# +# Drop a row of markers from a loop: +# for i in 1 2 3; do +# ./markers.sh -n "Checkpoint $i" -a "52.2$i" -o 21.01 +# done +# +# Print the full option reference: +# ./markers.sh -h +# +# ------------------------------------------------------------------------------ +# NOTES +# ------------------------------------------------------------------------------ +# +# - UIDs must be UUID4; the script generates one unless you pass -u. +# - Markers go stale on clients 24h after creation (server-side default). +# - A client must be connected when the marker is created to receive it; +# replay-on-connect for pre-existing markers is untested. +# +set -euo pipefail + +OTS_HOST="${OTS_HOST:-$(ipconfig getifaddr en0)}" +OTS_USER="${OTS_USER:-administrator}" +OTS_PASS="${OTS_PASS:-password}" + +usage() { + cat <&2; usage ;; + \?) echo "Error: unknown option -$OPTARG" >&2; usage ;; + esac +done + +# Log in and grab a bearer token. Flask-Security returns it under +# response.user.authentication_token when include_auth_token is set. +login() { + local body + body=$(curl -sk --fail-with-body \ + -X POST "https://${OTS_HOST}/api/login?include_auth_token" \ + -H "Content-Type: application/json" \ + -d "{\"username\":\"${OTS_USER}\",\"password\":\"${OTS_PASS}\"}") || { + echo "Error: login failed for ${OTS_USER}@${OTS_HOST}" >&2 + exit 1 + } + python3 -c " +import json, sys +try: + print(json.load(sys.stdin)['response']['user']['authentication_token']) +except (KeyError, ValueError): + sys.exit('Error: no auth token in login response') +" <<<"$body" +} + +token=$(login) + +if [[ "$action" == "list" ]]; then + curl -sk "https://${OTS_HOST}/api/markers" \ + -H "Authentication-Token: ${token}" | python3 -m json.tool + exit 0 +fi + +if [[ "$action" == "delete" ]]; then + curl -sk -X DELETE "https://${OTS_HOST}/api/markers?uid=${del_uid}" \ + -H "Authentication-Token: ${token}" + echo + exit 0 +fi + +[[ -n "$name" && -n "$lat" && -n "$lon" ]] || { + echo "Error: -n, -a and -o are required" >&2 + usage +} + +# The API rejects anything that is not UUID4, so generate rather than improvise. +if [[ -z "$muid" ]]; then + muid=$(python3 -c "import uuid; print(uuid.uuid4())") +fi + +payload=$(python3 -c " +import json, sys +print(json.dumps({ + 'uid': sys.argv[1], + 'name': sys.argv[2], + 'latitude': float(sys.argv[3]), + 'longitude': float(sys.argv[4]), + 'type': sys.argv[5], + 'fov': 90 +})) +" "$muid" "$name" "$lat" "$lon" "$cot_type") + +response=$(curl -sk -X POST "https://${OTS_HOST}/api/markers" \ + -H "Content-Type: application/json" \ + -H "Authentication-Token: ${token}" \ + -d "$payload") + +if [[ "$response" == *'"success":true'* ]]; then + echo "Created '${name}' (${cot_type}) at ${lat},${lon}" + echo " uid: ${muid}" +else + echo "Failed: ${response}" >&2 + exit 1 +fi diff --git a/bin/remove_all_markers.sh b/bin/remove_all_markers.sh new file mode 100755 index 0000000..7babed6 --- /dev/null +++ b/bin/remove_all_markers.sh @@ -0,0 +1,55 @@ +#!/bin/sh +# +# Remove all markers from the OpenTAKServer map +# + +set -eu + +OTS_HOST="${OTS_HOST:-$(ipconfig getifaddr en0)}" +OTS_USER="${OTS_USER:-administrator}" +OTS_PASS="${OTS_PASS:-password}" + +# Get script directory +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +# List all markers and extract UIDs, then delete each one +echo "Fetching markers from ${OTS_HOST}..." + +marker_uids=$(OTS_HOST="${OTS_HOST}" OTS_USER="${OTS_USER}" OTS_PASS="${OTS_PASS}" "$SCRIPT_DIR/markers.sh" -l 2>/dev/null | python3 -c " +import json, sys +try: + data = json.load(sys.stdin) + # Try both possible response formats + markers = data.get('response', {}).get('markers', []) or data.get('results', []) + for m in markers: + print(m.get('uid')) +except Exception as e: + print(f'Error: {e}', file=sys.stderr) + sys.exit(1) +") + +if [ -z "$marker_uids" ]; then + echo "No markers found on the map." + exit 0 +fi + +count=$(echo "$marker_uids" | wc -l) +echo "Found $count item(s). Attempting deletion..." + +deleted=0 +failed=0 + +echo "$marker_uids" | while IFS= read -r uid; do + if [ -n "$uid" ]; then + result=$(OTS_HOST="${OTS_HOST}" OTS_USER="${OTS_USER}" OTS_PASS="${OTS_PASS}" "$SCRIPT_DIR/markers.sh" -d "$uid" 2>&1) + if echo "$result" | grep -q '"success":true'; then + echo "✓ Deleted: $uid" + deleted=$((deleted + 1)) + else + failed=$((failed + 1)) + fi + fi +done + +echo "" +echo "Done! Markers have been deleted." diff --git a/bin/send_cot.py b/bin/send_cot.py new file mode 100755 index 0000000..512a6c9 --- /dev/null +++ b/bin/send_cot.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +""" +Send XML CoT events to OpenTAKServer via TCP port 8088. + +This script properly handles the OpenTAKServer protocol: +1. Sends initial device identification CoT +2. Sends actual data CoT events +3. Publishes via the cot_parser to be stored in the database + +Usage: + ./send_cot.py --callsign "Test Device" --lat 52.2297 --lon 21.0122 +""" + +import uuid +import socket +import time +from datetime import datetime, timedelta +from xml.etree.ElementTree import Element, SubElement, tostring + +def create_device_cot(device_uid, callsign): + """Create device identification CoT (first event after connecting).""" + now = datetime.utcnow() + stale = now + timedelta(hours=24) + now_str = now.isoformat() + 'Z' + stale_str = stale.isoformat() + 'Z' + + xml = f""" + + + + +""" + + return xml + + +def create_marker_cot(uid, callsign, latitude, longitude, cot_type='a-f-G', seconds=2): + """Create a marker CoT event.""" + now = datetime.utcnow() + stale = now + timedelta(seconds=seconds) + now_str = now.isoformat() + 'Z' + stale_str = stale.isoformat() + 'Z' + + xml = f""" + + + + +""" + + return xml + + +def send_cot_via_tcp(host, port, cot_xml): + """Send CoT XML via TCP.""" + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect((host, port)) + sock.sendall(cot_xml.encode('utf-8')) + time.sleep(0.1) + sock.close() + return True + except Exception as e: + print(f"✗ Error sending CoT: {e}") + return False + + +def main(): + import argparse + + parser = argparse.ArgumentParser(description='Send CoT events to OpenTAKServer') + parser.add_argument('--host', default='localhost', help='OpenTAKServer host') + parser.add_argument('--port', type=int, default=8088, help='OpenTAKServer TCP CoT port') + parser.add_argument('--callsign', required=True, help='Callsign for the device and marker') + parser.add_argument('--lat', type=float, required=True, help='Latitude') + parser.add_argument('--lon', type=float, required=True, help='Longitude') + parser.add_argument('--type', default='a-f-G', help='CoT type (default: a-f-G friendly ground)') + parser.add_argument('--ttl', type=int, default=10, help='Time-to-live in hours') + parser.add_argument('--device-uid', help='Device UID (generated if not provided)') + + args = parser.parse_args() + + # Generate a device UID (should be consistent for this device) + device_uid = args.device_uid or str(uuid.uuid4()) + + print(f"Connecting to {args.host}:{args.port}...") + + # Step 1: Send device identification + print(f"1. Registering device '{args.callsign}'...") + # device_cot = create_device_cot(device_uid, args.callsign) + # if send_cot_via_tcp(args.host, args.port, device_cot): + # print(" ✓ Device registered") + time.sleep(1.0) # Wait for device to be registered in DB + + # Step 2: Send marker CoT (use device UID so it's associated with the registered device) + print(f"2. Sending marker '{args.callsign}' at {args.lat}, {args.lon}...") + marker_cot = create_marker_cot(device_uid, args.callsign, args.lat, args.lon, args.type, args.ttl) + if send_cot_via_tcp(args.host, args.port, marker_cot): + print(" ✓ Marker CoT sent") + + print(f"\n✓ Complete!") + print(f"Device UID: {device_uid}") + print(f"Type: {args.type}") + print(f"Expires in: {args.ttl} hours") + print(f"\nTo send more markers from this device:") + print(f" python3 send_cot.py --device-uid {device_uid} --callsign 'New Marker' --lat 52.5 --lon 21.5") + + +if __name__ == '__main__': + main() diff --git a/docker-compose.yml b/docker-compose.yml index c55351e..867c133 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,12 +3,13 @@ services: build: context: . dockerfile: Dockerfile + image: ots-local:1.7.11 container_name: opentakserver hostname: opentakserver restart: unless-stopped tty: true ports: - - "8081:8081" # OTS listens on this port on the loopback interface for HTTP(S) requests + - "${OTS_API_INTERNAL_PORT:-8081}:8081" # OTS listens on this port for HTTP(S) requests volumes: - "./ots:/app/ots:rw" #- "./.opentakserver_venv:/app/venv/:rw" @@ -26,12 +27,18 @@ services: env_file: ots_config.env ots_cot_parser: - image: ghcr.io/brian7704/ots_cot_parser:master + # The ghcr ots_* images are stale 1.5.14 builds regardless of their tag, and + # ship the old client_controller.py that binds a 'cot' exchange the 1.7.x core + # no longer creates. Use the locally-built image so handlers match the core. + build: + context: . + dockerfile: Dockerfile + image: ots-local:1.7.11 container_name: ots_cot_parser hostname: ots-cot_parser restart: unless-stopped tty: true - command: python3 /app/venv/bin/cot_parser + entrypoint: ["python3", "/app/venv/bin/cot_parser"] healthcheck: disable: true volumes: @@ -42,12 +49,15 @@ services: env_file: ots_config.env ots_eud_handler: - image: ghcr.io/brian7704/ots_eud_handler:master + build: + context: . + dockerfile: Dockerfile + image: ots-local:1.7.11 container_name: ots_eud_handler hostname: ots_eud_handler restart: unless-stopped tty: true - command: python3 /app/venv/bin/eud_handler + entrypoint: ["python3", "/app/venv/bin/eud_handler"] ports: - "0.0.0.0:8088:8088" # TCP CoT streaming port healthcheck: @@ -57,12 +67,15 @@ services: env_file: ots_config.env ots_eud_handler_ssl: - image: ghcr.io/brian7704/ots_eud_handler_ssl:master + build: + context: . + dockerfile: Dockerfile + image: ots-local:1.7.11 container_name: ots_eud_handler_ssl hostname: ots_eud_handler_ssl restart: unless-stopped tty: true - command: python3 /app/venv/bin/eud_handler --ssl + entrypoint: ["python3", "/app/venv/bin/eud_handler", "--ssl"] ports: - "0.0.0.0:8089:8089" # SSL CoT streaming port healthcheck: @@ -76,6 +89,7 @@ services: ots-webui: image: ghcr.io/brian7704/opentakserver-ui:master + platform: linux/amd64 # published for amd64 only, runs emulated container_name: ots-webui hostname: opentakserver-webui @@ -87,12 +101,17 @@ services: hostname: nginx-proxy restart: unless-stopped ports: - - "0.0.0.0:80:80" # HTTP Web UI - - "0.0.0.0:443:443" # HTTPS Web UI - - "0.0.0.0:8080:8080" # HTTP API requests to OpenTAKServer port 8081 - - "0.0.0.0:8443:8443" # HTTPS API requests to OpenTAKServer port 8081 - - "0.0.0.0:8446:8446" # Proxy for certificate enrollment to OpenTAKServer port 8081 - - "0.0.0.0:8883:8883" # Proxy for MQTT / Meshtastic to Rabbitmq port 1883 + # Host sides are configurable to avoid colliding with other local projects; + # set the OTS_*_PORT vars in ots_config.env. Container sides must stay as + # they are: each is hardcoded as a `listen` directive in + # ots/configs/nginx/templates, so nginx would not be listening on a + # changed port. + - "0.0.0.0:${OTS_WEB_HTTP_PORT:-80}:80" # HTTP Web UI + - "0.0.0.0:${OTS_WEB_HTTPS_PORT:-443}:443" # HTTPS Web UI + - "0.0.0.0:${OTS_API_HTTP_PORT:-8080}:8080" # HTTP API requests to OpenTAKServer port 8081 + - "0.0.0.0:${OTS_API_HTTPS_PORT:-8443}:8443" # HTTPS API requests to OpenTAKServer port 8081 + - "0.0.0.0:${OTS_CERT_ENROLLMENT_PORT:-8446}:8446" # Proxy for certificate enrollment to OpenTAKServer port 8081 + - "0.0.0.0:${OTS_MQTT_PORT:-8883}:8883" # Proxy for MQTT / Meshtastic to Rabbitmq port 1883 volumes: - "./ots/ca:/app/ots/ca:ro" - "./ots/configs/nginx/templates:/etc/nginx/templates:ro" @@ -146,7 +165,9 @@ services: - "./ots/mediamtx/mediamtx.yml:/mediamtx.yml" ots-db: - image: postgis/postgis:18-3.6 + # postgis/postgis publishes amd64 only; imresamu/postgis is the PostGIS + # project's multi-arch companion build of the same recipe. + image: imresamu/postgis:18-3.6 container_name: ots-db hostname: ots-db restart: unless-stopped @@ -159,4 +180,8 @@ services: timeout: 5s retries: 10 ports: - - "5432:5432" \ No newline at end of file + # Host side is configurable to avoid colliding with other local projects' + # Postgres; set OTS_DB_PORT in ots_config.env. Container side must stay + # 5432: OTS connects with SQLALCHEMY_DATABASE_URI=...@ots-db/ots, which + # has no explicit port and resolves over the compose network. + - "${OTS_DB_PORT:-5433}:5432" \ No newline at end of file diff --git a/ots/.gitignore b/ots/.gitignore index 30eb3da..369e50c 100644 --- a/ots/.gitignore +++ b/ots/.gitignore @@ -1,7 +1,7 @@ -# Ignore everything in this directory -ca -# But do not ignore this .gitignore file -!ca +# "ca" was listed here and then immediately re-included with "!ca", which left +# the CA private key unignored. Certificate material is handled by the root +# .gitignore now; a negation here would override it, since deeper .gitignore +# files win over shallower ones. !mediamtx !.gitignore !configs \ No newline at end of file diff --git a/ots_config.env b/ots_config.env deleted file mode 100644 index e89aa31..0000000 --- a/ots_config.env +++ /dev/null @@ -1,9 +0,0 @@ -SQLALCHEMY_DATABASE_URI=postgresql+psycopg://ots:password@ots-db/ots -OTS_FQDN=_ -OTS_RABBITMQ_SERVER_ADDRESS=rabbitmq -OTS_LISTENER_ADDRESS=0.0.0.0 -OTS_MEDIAMTX_API_ADDRESS=http://mediamtx:9997 -POSTGRES_PASSWORD=password -POSTGRES_USER=ots -POSTGRES_DB=ots -PGUSER=ots \ No newline at end of file diff --git a/ots_config.env.template b/ots_config.env.template new file mode 100644 index 0000000..83da14a --- /dev/null +++ b/ots_config.env.template @@ -0,0 +1,37 @@ +# Required, your MacOS local network IP: +OTS_FQDN= + +# Required, random, alphanumeric string: +OTS_MEDIAMTX_TOKEN= + +SQLALCHEMY_DATABASE_URI=postgresql+psycopg://ots:password@ots-db/ots +OTS_RABBITMQ_SERVER_ADDRESS=rabbitmq +OTS_LISTENER_ADDRESS=0.0.0.0 +OTS_MEDIAMTX_API_ADDRESS=http://mediamtx:9997 +POSTGRES_PASSWORD=password +POSTGRES_USER=ots +POSTGRES_DB=ots +PGUSER=ots + +# Host port for the Postgres container. The container side stays 5432. +OTS_DB_PORT=5433 + +# Host ports published by nginx-proxy. Container sides are fixed by the `listen` +# directives in ots/configs/nginx/templates, so only these host mappings are +# configurable. Changing OTS_WEB_HTTPS_PORT away from 443 means clients must +# include the port explicitly (markers.sh and any https:///api/... caller +# assume the default). +OTS_WEB_HTTP_PORT=80 +OTS_WEB_HTTPS_PORT=443 + +# Host port for the OTS HTTP API. Moved off default 8080 so the drone-detection-api +# `api` service can bind it. Container side stays 8080. +OTS_API_HTTP_PORT=18080 + +OTS_API_HTTPS_PORT=8443 +OTS_CERT_ENROLLMENT_PORT=8446 +OTS_MQTT_PORT=8883 + +# Host port for the OTS internal API server (Flask/gunicorn). Used by clients connecting +# to the OpenTAKServer for REST API calls and WebSocket connections. Container side stays 8081. +OTS_API_INTERNAL_PORT=8081 diff --git a/patches/fix_eud_handler.py b/patches/fix_eud_handler.py new file mode 100644 index 0000000..22160d8 --- /dev/null +++ b/patches/fix_eud_handler.py @@ -0,0 +1,74 @@ +"""Patch upstream OpenTAKServer 1.7.11 bugs in the installed package. + +Both fixes are idempotent and abort the build if their anchor is missing, so a +version bump can never silently drop them. Upstream sources ship with CRLF line +endings, so anchors are matched with \r?\n tolerance. + +Run as: python3 fix_eud_handler.py [site-packages/opentakserver] +""" + +import re +import sys +from pathlib import Path + +PKG = Path( + sys.argv[1] + if len(sys.argv) > 1 + else "/app/venv/lib/python3.13/site-packages/opentakserver" +) + + +def patch(path: Path, pattern: str, replacement: str, applied: str, label: str) -> None: + # read_text() applies universal newlines, so the upstream CRLF is normalised + # to LF here and written back as LF. Harmless for Python sources. + text = path.read_text() + if re.search(applied, text, flags=re.M): + print(f"[skip] {label}: already applied") + return + new_text, count = re.subn(pattern, replacement, text, count=1, flags=re.M) + if count != 1: + sys.exit(f"[FAIL] {label}: anchor not found in {path}") + path.write_text(new_text) + print(f"[ok] {label}") + + +# 1. eud_handler.py imports the EudHandler *module* where the class is required, +# so it gets passed as RequestHandlerClass and every plain-TCP (8088) +# connection dies with "TypeError: 'module' object is not callable". +# EudHandlerSSL.py already uses the correct form, which is exactly why only +# the TCP listener was affected. +patch( + PKG / "eud_handler" / "eud_handler.py", + r"^from opentakserver\.eud_handler import EudHandler[ \t]*$", + "from opentakserver.eud_handler.EudHandler import EudHandler", + r"^from opentakserver\.eud_handler\.EudHandler import EudHandler[ \t]*$", + "eud_handler.py: import the class, not the module", +) + +# 2. setup() opens the RabbitMQ channel asynchronously via pika.SelectConnection, +# but handle_cot() registers the client only on the FIRST CoT +# ("if event and not self.uid"). parse_device_info() then guards its +# queue_bind/basic_consume block with "if self.rabbit_channel and ...", so a +# client whose self-report arrives before the channel opens is silently +# skipped and stays unbound for the entire connection -- no crash, no retry. +# ATAK sends its self-report immediately on connect, so it loses this race +# routinely. Block in setup() (which runs before handle()) until the channel +# is ready, capped at 5s so a dead broker can't wedge the connection. +patch( + PKG / "eud_handler" / "EudHandler.py", + r"^([ \t]*)self\.iothread\.start\(\)([ \t]*)$", + ( + r"\1self.iothread.start()\2\n" + r"\1# Wait for the async channel before handle() reads the first CoT.\n" + r"\1# Registration runs once, so losing the race leaves us unbound.\n" + r"\1import time as _ots_time\n" + r"\1for _ in range(100):\n" + r"\1 if self.rabbit_channel and self.rabbit_channel.is_open:\n" + r"\1 break\n" + r"\1 _ots_time.sleep(0.05)" + ), + r"_ots_time", + "EudHandler.py: await RabbitMQ channel in setup()", +) + +print("patches complete")