diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5835dd6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,40 @@ +# Use an official Python slim image +FROM python:3.10-slim + +# Set a non-root user and group +RUN addgroup --system appgroup && adduser --system --group --home /app appuser + +# Set working directory +WORKDIR /app/opentakserver + +# Copy the contents of the current directory (i.e. the opentakserver folder) to /app +COPY . /app/ + +# Set ownership of the /app directory to the non-root user +RUN chown -R appuser:appgroup /app + +# Install required system packages (if needed) +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Create a Python virtual environment +RUN python -m venv /app/venv +ENV PATH="/app/venv/bin:$PATH" + +# Upgrade pip and install OpenTAKServer along with any requirements +RUN pip install --upgrade pip && pip install poetry + +RUN poetry config virtualenvs.create false \ + && poetry lock \ + && poetry install --no-interaction --no-ansi + +# Set the non-root user for running the container +USER appuser + +# Expose the port the application listens on +EXPOSE 8081 +EXPOSE 8089 + +# Start OpenTAKServer when the container runs +CMD ["/bin/sh", "-c", "DOCKER_WORKAROUND=true flask db upgrade && opentakserver"] \ No newline at end of file diff --git a/helm/Chart.yaml b/helm/Chart.yaml new file mode 100644 index 0000000..9508afd --- /dev/null +++ b/helm/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: opentakserver +description: A Helm chart for deploying OpenTAKServer +type: application +version: 0.1.0 +appVersion: "latest" \ No newline at end of file diff --git a/helm/README.md b/helm/README.md new file mode 100644 index 0000000..80d0aee --- /dev/null +++ b/helm/README.md @@ -0,0 +1,23 @@ +# OpenTAKServer Helm Chart + +## Build + +1. **Validate chart** + helm lint helm/ + +2. **Build chart** + helm package helm/ + +## Installation + +1. **Update values.yaml** + Adjust values to fit your environment. + +2. **Install the Chart** + helm install opentakserver ./opentakserver-helm + +3. **Upgrade the Chart** + helm upgrade opentakserver ./opentakserver-helm + +4. **Uninstall** + helm uninstall opentakserver diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml new file mode 100644 index 0000000..d0e6eb6 --- /dev/null +++ b/helm/templates/deployment.yaml @@ -0,0 +1,46 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Release.Name }} + labels: + app: {{ .Release.Name }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + app: {{ .Release.Name }} + template: + metadata: + labels: + app: {{ .Release.Name }} + spec: + containers: + - name: opentakserver-api + image: "{{ .Values.image.api.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - containerPort: 8081 + env: + {{- range .Values.env }} + - name: {{ .name }} + value: {{ .value | quote }} + {{- end }} + {{- range $key, $value := .Values.secretEnv }} + - name: {{ $key }} + valueFrom: + secretKeyRef: + name: opentakserver-secret + key: {{ $key }} + {{- end }} + volumeMounts: + - name: {{ .Values.volume.name }} + mountPath: {{ .Values.volume.mountPath }} + - name: opentakserver-ui + image: "{{ .Values.image.ui.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - containerPort: 8080 + volumes: + - name: {{ .Values.volume.name }} + persistentVolumeClaim: + claimName: {{ .Release.Name }}-pvc \ No newline at end of file diff --git a/helm/templates/ingressroute.yaml b/helm/templates/ingressroute.yaml new file mode 100644 index 0000000..20fcb94 --- /dev/null +++ b/helm/templates/ingressroute.yaml @@ -0,0 +1,31 @@ +{{- if .Values.ingress.enabled -}} +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: {{ .Release.Name }}-ingress + namespace: {{ .Release.Namespace }} + labels: + app.kubernetes.io/name: {{ .Release.Name }} + app.kubernetes.io/instance: {{ .Release.Name }} +spec: + entryPoints: + - {{ .Values.ingress.entryPoint }} + routes: + - match: Host(`{{ .Values.ingress.host }}`) && (PathPrefix(`/api`) || PathPrefix(`/socket.io`)) + kind: Rule + services: + - name: opentakserver-api + port: {{ .Values.service.api.port }} + - match: Host(`{{ .Values.ingress.host }}`) && PathPrefix(`/Marti`) + kind: Rule + services: + - name: opentakserver-api + port: {{ .Values.service.api.port }} + - match: Host(`{{ .Values.ingress.host }}`) && PathPrefix(`/`) + kind: Rule + services: + - name: opentakserver-ui + port: {{ .Values.service.ui.port }} + tls: + certResolver: {{ .Values.ingress.certResolver }} +{{- end }} \ No newline at end of file diff --git a/helm/templates/pvc.yaml b/helm/templates/pvc.yaml new file mode 100644 index 0000000..a4718f0 --- /dev/null +++ b/helm/templates/pvc.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ .Release.Name }}-pvc +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.volume.size }} \ No newline at end of file diff --git a/helm/templates/secrets.yaml b/helm/templates/secrets.yaml new file mode 100644 index 0000000..4df1ee8 --- /dev/null +++ b/helm/templates/secrets.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: opentakserver-secret +type: Opaque +data: + {{- range .Values.secretEnv }} + {{ .name }}: {{ .value | b64enc }} + {{- end }} \ No newline at end of file diff --git a/helm/templates/service.yaml b/helm/templates/service.yaml new file mode 100644 index 0000000..121ee41 --- /dev/null +++ b/helm/templates/service.yaml @@ -0,0 +1,27 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }}-api +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.api.port }} + targetPort: 8081 + protocol: TCP + name: http + selector: + app: {{ .Release.Name }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ .Release.Name }}-ui +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.ui.port }} + targetPort: 8080 + protocol: TCP + name: http + selector: + app: {{ .Release.Name }} \ No newline at end of file diff --git a/helm/values.yaml b/helm/values.yaml new file mode 100644 index 0000000..6ffbb39 --- /dev/null +++ b/helm/values.yaml @@ -0,0 +1,44 @@ +replicaCount: 1 + +image: + api: + repository: server/opentakserver + ui: + repository: server/opentakserver-ui + tag: latest + pullPolicy: Always + +service: + type: ClusterIP + api: + port: 5000 + ui: + port: 5001 + +volume: + name: opentakserver-storage + size: 1Gi + mountPath: /app/data + +env: + - name: OTS_DATA_FOLDER + value: /app/data + - name: OTS_RABBITMQ_SERVER_ADDRESS + value: rabbitmq + - name: OTS_RABBITMQ_USERNAME + value: user + - name: OTS_LISTENER_ADDRESS + value: 0.0.0.0 + +ingress: + enabled: true + host: "myopentakserver.instance.com" + entryPoint: "websecure" + certResolver: "myresolver" + +secretEnv: + - name: OTS_RABBITMQ_PASSWORD + value: password + +nodeSelector: {} +tolerations: [] \ No newline at end of file diff --git a/opentakserver/app.py b/opentakserver/app.py index fe8bf36..bf19653 100644 --- a/opentakserver/app.py +++ b/opentakserver/app.py @@ -53,10 +53,11 @@ from opentakserver.SocketServer import SocketServer from pyfiglet import Figlet -try: - from opentakserver.mumble.mumble_ice_app import MumbleIceDaemon -except ModuleNotFoundError: - print("Mumble auth not supported on this platform") +if os.getenv('DOCKER_WORKAROUND', 'false').lower() != 'true': + try: + from opentakserver.mumble.mumble_ice_app import MumbleIceDaemon + except ModuleNotFoundError: + print("Mumble auth not supported on this platform") def init_extensions(app): @@ -114,14 +115,17 @@ def init_extensions(app): socketio_logger = logger socketio.init_app(app, logger=socketio_logger) - rabbit_connection = pika.BlockingConnection(pika.ConnectionParameters(app.config.get("OTS_RABBITMQ_SERVER_ADDRESS"))) - channel = rabbit_connection.channel() - channel.exchange_declare('cot', durable=True, exchange_type='fanout') - channel.exchange_declare('dms', durable=True, exchange_type='direct') - channel.exchange_declare('chatrooms', durable=True, exchange_type='direct') - channel.queue_declare(queue='cot_controller') - channel.exchange_declare(exchange='cot_controller', exchange_type='fanout') - channel.exchange_declare("missions", durable=True, exchange_type='topic') # For Data Sync mission feeds + if os.getenv('DOCKER_WORKAROUND', 'false').lower() != 'true': + rabbit_credentials = pika.PlainCredentials(app.config.get("OTS_RABBITMQ_USERNAME"), app.config.get("OTS_RABBITMQ_PASSWORD")) + rabbit_host = app.config.get("OTS_RABBITMQ_SERVER_ADDRESS") + rabbit_connection = pika.BlockingConnection(pika.ConnectionParameters(host=rabbit_host, credentials=rabbit_credentials)) + channel = rabbit_connection.channel() + channel.exchange_declare('cot', durable=True, exchange_type='fanout') + channel.exchange_declare('dms', durable=True, exchange_type='direct') + channel.exchange_declare('chatrooms', durable=True, exchange_type='direct') + channel.queue_declare(queue='cot_controller') + channel.exchange_declare(exchange='cot_controller', exchange_type='fanout') + channel.exchange_declare("missions", durable=True, exchange_type='topic') # For Data Sync mission feeds cot_thread = CoTController(app.app_context(), logger, db, socketio) app.cot_thread = cot_thread diff --git a/opentakserver/blueprints/marti_api/mission_marti_api.py b/opentakserver/blueprints/marti_api/mission_marti_api.py index eaefd38..947e5e4 100644 --- a/opentakserver/blueprints/marti_api/mission_marti_api.py +++ b/opentakserver/blueprints/marti_api/mission_marti_api.py @@ -349,7 +349,9 @@ def put_mission(mission_name: str): event = generate_new_mission_cot(mission) - rabbit_connection = pika.BlockingConnection(pika.ConnectionParameters(app.config.get("OTS_RABBITMQ_SERVER_ADDRESS"))) + rabbit_credentials = pika.PlainCredentials(app.config.get("OTS_RABBITMQ_USERNAME"), app.config.get("OTS_RABBITMQ_PASSWORD")) + rabbit_host = app.config.get("OTS_RABBITMQ_SERVER_ADDRESS") + rabbit_connection = pika.BlockingConnection(pika.ConnectionParameters(host=rabbit_host, credentials=rabbit_credentials)) channel = rabbit_connection.channel() channel.basic_publish(exchange="missions", routing_key="missions", body=json.dumps( @@ -437,7 +439,9 @@ def delete_mission(mission_name: str): db.session.delete(mission) db.session.commit() - rabbit_connection = pika.BlockingConnection(pika.ConnectionParameters(app.config.get("OTS_RABBITMQ_SERVER_ADDRESS"))) + rabbit_credentials = pika.PlainCredentials(app.config.get("OTS_RABBITMQ_USERNAME"), app.config.get("OTS_RABBITMQ_PASSWORD")) + rabbit_host = app.config.get("OTS_RABBITMQ_SERVER_ADDRESS") + rabbit_connection = pika.BlockingConnection(pika.ConnectionParameters(host=rabbit_host, credentials=rabbit_credentials)) channel = rabbit_connection.channel() channel.basic_publish(exchange="missions", routing_key="missions", body=json.dumps({'uid': app.config.get("OTS_NODE_ID"), @@ -531,7 +535,9 @@ def invite(mission_name: str, invitation_type: str, invitee: str): db.session.commit() event = generate_invitation_cot(mission, invitee) - rabbit_connection = pika.BlockingConnection(pika.ConnectionParameters(app.config.get("OTS_RABBITMQ_SERVER_ADDRESS"))) + rabbit_credentials = pika.PlainCredentials(app.config.get("OTS_RABBITMQ_USERNAME"), app.config.get("OTS_RABBITMQ_PASSWORD")) + rabbit_host = app.config.get("OTS_RABBITMQ_SERVER_ADDRESS") + rabbit_connection = pika.BlockingConnection(pika.ConnectionParameters(host=rabbit_host, credentials=rabbit_credentials)) channel = rabbit_connection.channel() channel.basic_publish(exchange="dms", routing_key=invitee, body=json.dumps({"uid": app.config.get("OTS_NODE_ID"), "cot": tostring(event).decode('utf-8')})) @@ -590,7 +596,9 @@ def invite_json(mission_name: str): return jsonify({'success': False, 'error': f"Mission not found: {mission_name}"}), 404 mission = mission[0] - rabbit_connection = pika.BlockingConnection(pika.ConnectionParameters(app.config.get("OTS_RABBITMQ_SERVER_ADDRESS"))) + rabbit_credentials = pika.PlainCredentials(app.config.get("OTS_RABBITMQ_USERNAME"), app.config.get("OTS_RABBITMQ_PASSWORD")) + rabbit_host = app.config.get("OTS_RABBITMQ_SERVER_ADDRESS") + rabbit_connection = pika.BlockingConnection(pika.ConnectionParameters(host=rabbit_host, credentials=rabbit_credentials)) channel = rabbit_connection.channel() for invitee in invitees: @@ -712,8 +720,9 @@ def change_eud_role(mission_name: str): event = generate_invitation_cot(mission, role.clientUid) body = {'uid': app.config.get("OTS_NODE_ID"), 'cot': tostring(event).decode('utf-8')} - rabbit_connection = pika.BlockingConnection( - pika.ConnectionParameters(app.config.get("OTS_RABBITMQ_SERVER_ADDRESS"))) + rabbit_credentials = pika.PlainCredentials(app.config.get("OTS_RABBITMQ_USERNAME"), app.config.get("OTS_RABBITMQ_PASSWORD")) + rabbit_host = app.config.get("OTS_RABBITMQ_SERVER_ADDRESS") + rabbit_connection = pika.BlockingConnection(pika.ConnectionParameters(host=rabbit_host, credentials=rabbit_credentials)) channel = rabbit_connection.channel() channel.basic_publish(exchange="dms", routing_key=client_uid, body=json.dumps(body)) @@ -727,8 +736,9 @@ def change_eud_role(mission_name: str): event = generate_invitation_cot(mission, client_uid, 't-x-m-r', delete=True) body = {'uid': app.config.get("OTS_NODE_ID"), 'cot': tostring(event).decode('utf-8')} - rabbit_connection = pika.BlockingConnection( - pika.ConnectionParameters(app.config.get("OTS_RABBITMQ_SERVER_ADDRESS"))) + rabbit_credentials = pika.PlainCredentials(app.config.get("OTS_RABBITMQ_USERNAME"), app.config.get("OTS_RABBITMQ_PASSWORD")) + rabbit_host = app.config.get("OTS_RABBITMQ_SERVER_ADDRESS") + rabbit_connection = pika.BlockingConnection(pika.ConnectionParameters(host=rabbit_host, credentials=rabbit_credentials)) channel = rabbit_connection.channel() channel.basic_publish(exchange="dms", routing_key=client_uid, body=json.dumps(body)) @@ -888,7 +898,9 @@ def mission_subscribe(mission_name: str = None, mission_guid: str = None): "role": role.to_json()['role'], } - rabbit_connection = pika.BlockingConnection(pika.ConnectionParameters(app.config.get("OTS_RABBITMQ_SERVER_ADDRESS"))) + rabbit_credentials = pika.PlainCredentials(app.config.get("OTS_RABBITMQ_USERNAME"), app.config.get("OTS_RABBITMQ_PASSWORD")) + rabbit_host = app.config.get("OTS_RABBITMQ_SERVER_ADDRESS") + rabbit_connection = pika.BlockingConnection(pika.ConnectionParameters(host=rabbit_host, credentials=rabbit_credentials)) channel = rabbit_connection.channel() channel.queue_bind(queue=uid, exchange="missions", routing_key=f"missions.{mission_name}") @@ -926,7 +938,9 @@ def mission_unsubscribe(mission_name: str): db.session.delete(role[0]) db.session.commit() - rabbit_connection = pika.BlockingConnection(pika.ConnectionParameters(app.config.get("OTS_RABBITMQ_SERVER_ADDRESS"))) + rabbit_credentials = pika.PlainCredentials(app.config.get("OTS_RABBITMQ_USERNAME"), app.config.get("OTS_RABBITMQ_PASSWORD")) + rabbit_host = app.config.get("OTS_RABBITMQ_SERVER_ADDRESS") + rabbit_connection = pika.BlockingConnection(pika.ConnectionParameters(host=rabbit_host, credentials=rabbit_credentials)) channel = rabbit_connection.channel() channel.queue_unbind(queue=eud_uid, exchange="missions", routing_key=f"missions.{mission_name}") @@ -995,7 +1009,9 @@ def create_log_entry(): change_cot = generate_mission_change_cot(log_entry.mission_name, mission[0], mission_change, cot_type='t-x-m-c-l') body = json.dumps({'uid': log_entry.creator_uid, 'cot': tostring(change_cot).decode('utf-8')}) - rabbit_connection = pika.BlockingConnection(pika.ConnectionParameters(app.config.get("OTS_RABBITMQ_SERVER_ADDRESS"))) + rabbit_credentials = pika.PlainCredentials(app.config.get("OTS_RABBITMQ_USERNAME"), app.config.get("OTS_RABBITMQ_PASSWORD")) + rabbit_host = app.config.get("OTS_RABBITMQ_SERVER_ADDRESS") + rabbit_connection = pika.BlockingConnection(pika.ConnectionParameters(host=rabbit_host, credentials=rabbit_credentials)) channel = rabbit_connection.channel() channel.basic_publish("missions", routing_key=f"missions.{log_entry.mission_name}", body=body) @@ -1158,7 +1174,9 @@ def mission_contents(mission_name: str): event = generate_mission_change_cot(mission_name, mission, mission_change, content=content) body = json.dumps({'uid': mission_change.creator_uid, 'cot': tostring(event).decode('utf-8')}) - rabbit_connection = pika.BlockingConnection(pika.ConnectionParameters(app.config.get("OTS_RABBITMQ_SERVER_ADDRESS"))) + rabbit_credentials = pika.PlainCredentials(app.config.get("OTS_RABBITMQ_USERNAME"), app.config.get("OTS_RABBITMQ_PASSWORD")) + rabbit_host = app.config.get("OTS_RABBITMQ_SERVER_ADDRESS") + rabbit_connection = pika.BlockingConnection(pika.ConnectionParameters(host=rabbit_host, credentials=rabbit_credentials)) channel = rabbit_connection.channel() channel.basic_publish("missions", routing_key=f"missions.{mission_name}", body=body) @@ -1232,8 +1250,9 @@ def mission_contents(mission_name: str): event = generate_mission_change_cot(mission_name, mission, mission_change, mission_uid=mission_uid) body = json.dumps({'uid': mission_change.creator_uid, 'cot': tostring(event).decode('utf-8')}) - rabbit_connection = pika.BlockingConnection( - pika.ConnectionParameters(app.config.get("OTS_RABBITMQ_SERVER_ADDRESS"))) + rabbit_credentials = pika.PlainCredentials(app.config.get("OTS_RABBITMQ_USERNAME"), app.config.get("OTS_RABBITMQ_PASSWORD")) + rabbit_host = app.config.get("OTS_RABBITMQ_SERVER_ADDRESS") + rabbit_connection = pika.BlockingConnection(pika.ConnectionParameters(host=rabbit_host, credentials=rabbit_credentials)) channel = rabbit_connection.channel() channel.basic_publish("missions", routing_key=f"missions.{mission_name}", body=body) @@ -1310,8 +1329,9 @@ def delete_content(mission_name: str): event = generate_mission_change_cot(eud_uid, mission, mission_change, content=content, mission_uid=mission_uid, cot_event=cot_event) body = {'uid': app.config.get("OTS_NODE_ID"), 'cot': tostring(event).decode('utf-8')} - rabbit_connection = pika.BlockingConnection( - pika.ConnectionParameters(app.config.get("OTS_RABBITMQ_SERVER_ADDRESS"))) + rabbit_credentials = pika.PlainCredentials(app.config.get("OTS_RABBITMQ_USERNAME"), app.config.get("OTS_RABBITMQ_PASSWORD")) + rabbit_host = app.config.get("OTS_RABBITMQ_SERVER_ADDRESS") + rabbit_connection = pika.BlockingConnection(pika.ConnectionParameters(host=rabbit_host, credentials=rabbit_credentials)) channel = rabbit_connection.channel() channel.basic_publish(exchange="missions", routing_key=f"missions.{mission_name}", body=json.dumps(body)) diff --git a/opentakserver/blueprints/ots_api/casevac_api.py b/opentakserver/blueprints/ots_api/casevac_api.py index b3de2b2..019df33 100644 --- a/opentakserver/blueprints/ots_api/casevac_api.py +++ b/opentakserver/blueprints/ots_api/casevac_api.py @@ -107,8 +107,9 @@ def add_casevac(): db.session.commit() logger.debug(f"Updated CasEvac {casevac.uid}") - rabbit_connection = pika.BlockingConnection( - pika.ConnectionParameters(app.config.get("OTS_RABBITMQ_SERVER_ADDRESS"))) + rabbit_credentials = pika.PlainCredentials(app.config.get("OTS_RABBITMQ_USERNAME"), app.config.get("OTS_RABBITMQ_PASSWORD")) + rabbit_host = app.config.get("OTS_RABBITMQ_SERVER_ADDRESS") + rabbit_connection = pika.BlockingConnection(pika.ConnectionParameters(host=rabbit_host, credentials=rabbit_credentials)) channel = rabbit_connection.channel() channel.basic_publish(exchange='cot', routing_key='', body=json.dumps({'cot': cot.xml, 'uid': app.config['OTS_NODE_ID']}), properties=pika.BasicProperties(expiration=app.config.get("OTS_RABBITMQ_TTL"))) @@ -153,8 +154,9 @@ def delete_casevac(): SubElement(detail, '_flow-tags_', {'TAK-Server-f1a8159ef7804f7a8a32d8efc4b773d0': iso8601_string_from_datetime(now)}) - rabbit_connection = pika.BlockingConnection( - pika.ConnectionParameters(app.config.get("OTS_RABBITMQ_SERVER_ADDRESS"))) + rabbit_credentials = pika.PlainCredentials(app.config.get("OTS_RABBITMQ_USERNAME"), app.config.get("OTS_RABBITMQ_PASSWORD")) + rabbit_host = app.config.get("OTS_RABBITMQ_SERVER_ADDRESS") + rabbit_connection = pika.BlockingConnection(pika.ConnectionParameters(host=rabbit_host, credentials=rabbit_credentials)) channel = rabbit_connection.channel() channel.basic_publish(exchange='cot', routing_key='', body=json.dumps( {'cot': tostring(event).decode('utf-8'), 'uid': app.config['OTS_NODE_ID']}), diff --git a/opentakserver/blueprints/ots_api/marker_api.py b/opentakserver/blueprints/ots_api/marker_api.py index 2c9ce9c..9e06553 100644 --- a/opentakserver/blueprints/ots_api/marker_api.py +++ b/opentakserver/blueprints/ots_api/marker_api.py @@ -125,8 +125,9 @@ def add_marker(): sensor.set("fovRed", "1.0") sensor.set("range", "100.0") - rabbit_connection = pika.BlockingConnection( - pika.ConnectionParameters(app.config.get("OTS_RABBITMQ_SERVER_ADDRESS"))) + rabbit_credentials = pika.PlainCredentials(app.config.get("OTS_RABBITMQ_USERNAME"), app.config.get("OTS_RABBITMQ_PASSWORD")) + rabbit_host = app.config.get("OTS_RABBITMQ_SERVER_ADDRESS") + rabbit_connection = pika.BlockingConnection(pika.ConnectionParameters(host=rabbit_host, credentials=rabbit_credentials)) channel = rabbit_connection.channel() channel.basic_publish(exchange='cot', routing_key='', body=json.dumps( {'cot': ET.tostring(event).decode('utf-8'), 'uid': app.config['OTS_NODE_ID']}), @@ -202,8 +203,9 @@ def delete_marker(): ET.SubElement(detail, 'link', {'relation': 'p-p', 'uid': marker.uid, 'type': marker.cot.type}) ET.SubElement(detail, '_flow-tags_', {'TAK-Server-f1a8159ef7804f7a8a32d8efc4b773d0': iso8601_string_from_datetime(now)}) - rabbit_connection = pika.BlockingConnection( - pika.ConnectionParameters(app.config.get("OTS_RABBITMQ_SERVER_ADDRESS"))) + rabbit_credentials = pika.PlainCredentials(app.config.get("OTS_RABBITMQ_USERNAME"), app.config.get("OTS_RABBITMQ_PASSWORD")) + rabbit_host = app.config.get("OTS_RABBITMQ_SERVER_ADDRESS") + rabbit_connection = pika.BlockingConnection(pika.ConnectionParameters(host=rabbit_host, credentials=rabbit_credentials)) channel = rabbit_connection.channel() channel.basic_publish(exchange='cot', routing_key='', body=json.dumps( {'cot': ET.tostring(event).decode('utf-8'), 'uid': app.config['OTS_NODE_ID']}), diff --git a/opentakserver/blueprints/ots_api/mission_api.py b/opentakserver/blueprints/ots_api/mission_api.py index d2c23ee..296b6ad 100644 --- a/opentakserver/blueprints/ots_api/mission_api.py +++ b/opentakserver/blueprints/ots_api/mission_api.py @@ -87,8 +87,9 @@ def create_edit_mission(): logger.debug(mission.serialize()) return jsonify({'success': False, 'error': f"Failed to add mission: {e}"}), 400 - rabbit_connection = pika.BlockingConnection( - pika.ConnectionParameters(app.config.get("OTS_RABBITMQ_SERVER_ADDRESS"))) + rabbit_credentials = pika.PlainCredentials(app.config.get("OTS_RABBITMQ_USERNAME"), app.config.get("OTS_RABBITMQ_PASSWORD")) + rabbit_host = app.config.get("OTS_RABBITMQ_SERVER_ADDRESS") + rabbit_connection = pika.BlockingConnection(pika.ConnectionParameters(host=rabbit_host, credentials=rabbit_credentials)) channel = rabbit_connection.channel() channel.basic_publish(exchange="missions", routing_key="missions", @@ -140,8 +141,9 @@ def delete_mission(): db.session.delete(mission) db.session.commit() - rabbit_connection = pika.BlockingConnection( - pika.ConnectionParameters(app.config.get("OTS_RABBITMQ_SERVER_ADDRESS"))) + rabbit_credentials = pika.PlainCredentials(app.config.get("OTS_RABBITMQ_USERNAME"), app.config.get("OTS_RABBITMQ_PASSWORD")) + rabbit_host = app.config.get("OTS_RABBITMQ_SERVER_ADDRESS") + rabbit_connection = pika.BlockingConnection(pika.ConnectionParameters(host=rabbit_host, credentials=rabbit_credentials)) channel = rabbit_connection.channel() channel.basic_publish(exchange="missions", routing_key="missions", body=json.dumps({'uid': app.config.get("OTS_NODE_ID"), diff --git a/opentakserver/blueprints/scheduled_jobs.py b/opentakserver/blueprints/scheduled_jobs.py index 5ce4539..e72e5ad 100644 --- a/opentakserver/blueprints/scheduled_jobs.py +++ b/opentakserver/blueprints/scheduled_jobs.py @@ -50,7 +50,9 @@ def get_airplanes_live_data(): app.config["OTS_AIRPLANES_LIVE_RADIUS"])) logger.info(r.text) if r.status_code == 200: - rabbit_connection = pika.BlockingConnection(pika.ConnectionParameters(app.config.get("OTS_RABBITMQ_SERVER_ADDRESS"))) + rabbit_credentials = pika.PlainCredentials(app.config.get("OTS_RABBITMQ_USERNAME"), app.config.get("OTS_RABBITMQ_PASSWORD")) + rabbit_host = app.config.get("OTS_RABBITMQ_SERVER_ADDRESS") + rabbit_connection = pika.BlockingConnection(pika.ConnectionParameters(host=rabbit_host, credentials=rabbit_credentials)) channel = rabbit_connection.channel() for craft in r.json()['ac']: @@ -150,7 +152,9 @@ def get_aishub_data(): logger.error(f"Failed to get AIS data: {r.text}") return - rabbit_connection = pika.BlockingConnection(pika.ConnectionParameters(app.config.get("OTS_RABBITMQ_SERVER_ADDRESS"))) + rabbit_credentials = pika.PlainCredentials(app.config.get("OTS_RABBITMQ_USERNAME"), app.config.get("OTS_RABBITMQ_PASSWORD")) + rabbit_host = app.config.get("OTS_RABBITMQ_SERVER_ADDRESS") + rabbit_connection = pika.BlockingConnection(pika.ConnectionParameters(host=rabbit_host, credentials=rabbit_credentials)) channel = rabbit_connection.channel() for vessel in r.json()[1]: event = aiscot.ais_to_cot(vessel, None, None) @@ -178,8 +182,9 @@ def delete_old_data(): days=app.config.get("OTS_DELETE_OLD_DATA_DAYS"), weeks=app.config.get("OTS_DELETE_OLD_DATA_WEEKS")) - rabbit_connection = pika.BlockingConnection( - pika.ConnectionParameters(app.config.get("OTS_RABBITMQ_SERVER_ADDRESS"))) + rabbit_credentials = pika.PlainCredentials(app.config.get("OTS_RABBITMQ_USERNAME"), app.config.get("OTS_RABBITMQ_PASSWORD")) + rabbit_host = app.config.get("OTS_RABBITMQ_SERVER_ADDRESS") + rabbit_connection = pika.BlockingConnection(pika.ConnectionParameters(host=rabbit_host, credentials=rabbit_credentials)) channel = rabbit_connection.channel() # I wish I hadn't made the marker's timestamp field a string... diff --git a/opentakserver/controllers/client_controller.py b/opentakserver/controllers/client_controller.py index 3db6c50..2d9222d 100644 --- a/opentakserver/controllers/client_controller.py +++ b/opentakserver/controllers/client_controller.py @@ -74,8 +74,17 @@ def __init__(self, address, port, sock, logger, app, is_ssl): # RabbitMQ try: - self.rabbit_connection = pika.SelectConnection(pika.ConnectionParameters(self.app.config.get("OTS_RABBITMQ_SERVER_ADDRESS")), - self.on_connection_open) + rabbit_credentials = pika.PlainCredentials(app.config.get("OTS_RABBITMQ_USERNAME"), app.config.get("OTS_RABBITMQ_PASSWORD")) + rabbit_host = app.config.get("OTS_RABBITMQ_SERVER_ADDRESS") + connection_params = pika.ConnectionParameters( + host=rabbit_host, + credentials=rabbit_credentials + ) + self.rabbit_connection = pika.SelectConnection( + connection_params, + self.on_connection_open + ) + self.rabbit_channel: Channel | None = None self.iothread = Thread(target=self.rabbit_connection.ioloop.start) self.iothread.daemon = True diff --git a/opentakserver/controllers/rabbitmq_client.py b/opentakserver/controllers/rabbitmq_client.py index 01a90c3..9f0aa5e 100644 --- a/opentakserver/controllers/rabbitmq_client.py +++ b/opentakserver/controllers/rabbitmq_client.py @@ -18,8 +18,16 @@ def __init__(self, context, logger, db, socketio): self.exchanges = [] try: - self.rabbit_connection = pika.SelectConnection(pika.ConnectionParameters(self.context.app.config.get("OTS_RABBITMQ_SERVER_ADDRESS")), - self.on_connection_open) + rabbit_credentials = pika.PlainCredentials(self.context.app.config.get("OTS_RABBITMQ_USERNAME"), self.context.app.config.get("OTS_RABBITMQ_PASSWORD")) + rabbit_host = self.context.app.config.get("OTS_RABBITMQ_SERVER_ADDRESS") + connection_params = pika.ConnectionParameters( + host=rabbit_host, + credentials=rabbit_credentials + ) + self.rabbit_connection = pika.SelectConnection( + connection_params, + self.on_connection_open + ) self.rabbit_channel: Channel = None self.iothread = Thread(target=self.rabbit_connection.ioloop.start) self.iothread.daemon = True diff --git a/opentakserver/defaultconfig.py b/opentakserver/defaultconfig.py index 606eab4..75e3ffd 100644 --- a/opentakserver/defaultconfig.py +++ b/opentakserver/defaultconfig.py @@ -10,146 +10,120 @@ class DefaultConfig: - SECRET_KEY = secrets.token_hex() - DEBUG = False - - OTS_DATA_FOLDER = os.path.join(Path.home(), 'ots') - OTS_LISTENER_ADDRESS = "127.0.0.1" - OTS_LISTENER_PORT = 8081 # OTS will listen for HTTP requests on this port. Nginx will listen on ports 80, 443, - # 8080, 8443, and 8446 and proxy requests to OTS_LISTENER_PORT - OTS_MARTI_HTTP_PORT = 8080 - OTS_MARTI_HTTPS_PORT = 8443 - OTS_ENABLE_TCP_STREAMING_PORT = True - OTS_TCP_STREAMING_PORT = 8088 - OTS_SSL_STREAMING_PORT = 8089 - OTS_BACKUP_COUNT = 7 - OTS_ENABLE_CHANNELS = True - OTS_RABBITMQ_SERVER_ADDRESS = "127.0.0.1" - OTS_MEDIAMTX_ENABLE = True - OTS_MEDIAMTX_API_ADDRESS = "http://localhost:9997" - OTS_MEDIAMTX_TOKEN = secrets.token_urlsafe(30 * 3 // 4) - OTS_SSL_VERIFICATION_MODE = 2 # Equivalent to ssl.CERT_REQUIRED. https://docs.python.org/3/library/ssl.html#ssl.SSLContext.verify_mode - OTS_SSL_CERT_HEADER = 'X-Ssl-Cert' - OTS_NODE_ID = ''.join(random.choices(string.ascii_lowercase + string.digits, k=32)) - OTS_CA_NAME = 'OpenTAKServer-CA' - OTS_CA_FOLDER = os.path.join(OTS_DATA_FOLDER, 'ca') - OTS_CA_PASSWORD = 'atakatak' - OTS_CA_EXPIRATION_TIME = 3650 # In days, defaults to 10 years - OTS_CA_COUNTRY = 'WW' - OTS_CA_STATE = 'XX' - OTS_CA_CITY = 'YY' - OTS_CA_ORGANIZATION = 'ZZ' - OTS_CA_ORGANIZATIONAL_UNIT = 'OpenTAKServer' - OTS_CA_SUBJECT = '/C={}/ST={}/L={}/O={}/OU={}'.format(OTS_CA_COUNTRY, OTS_CA_STATE, OTS_CA_CITY, - OTS_CA_ORGANIZATION, OTS_CA_ORGANIZATIONAL_UNIT) - OTS_FIGLET_WIDTH = 100 - OTS_FIGLET_FONTS = ["slant", "thin", "stampatello", "rectangles", "bell", "doom", "banner", "banner3-D", "banner3", - "mini", "marquee", "big", "chunky", "poison", "pepper", "computer", "puffy", "cosmic", "script", - "sblood", "epic", "speed", "trek", "rev", "larry3d", "3-d", "5lineoblique", "lean", "cursive", - "gothic"] - # Messages queued in RabbitMQ will auto-delete after 1 day if not consumed https://www.rabbitmq.com/docs/ttl - # Set to '0' to disable auto-deletion - OTS_RABBITMQ_TTL = '86400000' - - # See https://docs.python.org/3/library/logging.handlers.html#logging.handlers.TimedRotatingFileHandler - OTS_LOG_ROTATE_WHEN = 'midnight' - OTS_LOG_ROTATE_INTERVAL = 0 - - # ADS-B Settings - OTS_AIRPLANES_LIVE_LAT = 40.744213 - OTS_AIRPLANES_LIVE_LON = -73.986939 - OTS_AIRPLANES_LIVE_RADIUS = 10 - - # AIS Settings - OTS_AISHUB_USERNAME = None - OTS_AISHUB_SOUTH_LAT = None - OTS_AISHUB_WEST_LON = None - OTS_AISHUB_NORTH_LAT = None - OTS_AISHUB_EAST_LON = None - OTS_AISHUB_MMSI_LIST = "" - OTS_AISHUB_IMO_LIST = "" - - OTS_PROFILE_MAP_SOURCES = True - - OTS_ENABLE_MUMBLE_AUTHENTICATION = False + SECRET_KEY = os.getenv("SECRET_KEY", secrets.token_hex()) + DEBUG = os.getenv("DEBUG", "False").lower() in ["true", "1", "yes"] + + OTS_DATA_FOLDER = os.getenv("OTS_DATA_FOLDER", os.path.join(Path.home(), "ots")) + OTS_LISTENER_ADDRESS = os.getenv("OTS_LISTENER_ADDRESS", "127.0.0.1") + OTS_LISTENER_PORT = int(os.getenv("OTS_LISTENER_PORT", 8081)) + OTS_MARTI_HTTP_PORT = int(os.getenv("OTS_MARTI_HTTP_PORT", 8080)) + OTS_MARTI_HTTPS_PORT = int(os.getenv("OTS_MARTI_HTTPS_PORT", 8443)) + OTS_ENABLE_TCP_STREAMING_PORT = os.getenv("OTS_ENABLE_TCP_STREAMING_PORT", "True").lower() in ["true", "1", "yes"] + OTS_TCP_STREAMING_PORT = int(os.getenv("OTS_TCP_STREAMING_PORT", 8088)) + OTS_SSL_STREAMING_PORT = int(os.getenv("OTS_SSL_STREAMING_PORT", 8089)) + OTS_BACKUP_COUNT = int(os.getenv("OTS_BACKUP_COUNT", 7)) + OTS_ENABLE_CHANNELS = os.getenv("OTS_ENABLE_CHANNELS", "True").lower() in ["true", "1", "yes"] + OTS_RABBITMQ_SERVER_ADDRESS = os.getenv("OTS_RABBITMQ_SERVER_ADDRESS", "172.17.0.2") + OTS_RABBITMQ_USERNAME = os.getenv("OTS_RABBITMQ_USERNAME", "") + OTS_RABBITMQ_PASSWORD = os.getenv("OTS_RABBITMQ_PASSWORD", "") + OTS_MEDIAMTX_ENABLE = os.getenv("OTS_MEDIAMTX_ENABLE", "True").lower() in ["true", "1", "yes"] + OTS_MEDIAMTX_API_ADDRESS = os.getenv("OTS_MEDIAMTX_API_ADDRESS", "http://localhost:9997") + OTS_MEDIAMTX_TOKEN = os.getenv("OTS_MEDIAMTX_TOKEN", secrets.token_urlsafe(30 * 3 // 4)) + OTS_SSL_VERIFICATION_MODE = int(os.getenv("OTS_SSL_VERIFICATION_MODE", 2)) + OTS_SSL_CERT_HEADER = os.getenv("OTS_SSL_CERT_HEADER", "X-Ssl-Cert") + OTS_NODE_ID = os.getenv("OTS_NODE_ID", ''.join(random.choices(string.ascii_lowercase + string.digits, k=32))) + + # Certificate Authority Settings + OTS_CA_NAME = os.getenv("OTS_CA_NAME", "OpenTAKServer-CA") + OTS_CA_FOLDER = os.getenv("OTS_CA_FOLDER", os.path.join(OTS_DATA_FOLDER, "ca")) + OTS_CA_PASSWORD = os.getenv("OTS_CA_PASSWORD", "atakatak") + OTS_CA_EXPIRATION_TIME = int(os.getenv("OTS_CA_EXPIRATION_TIME", 3650)) + OTS_CA_COUNTRY = os.getenv("OTS_CA_COUNTRY", "WW") + OTS_CA_STATE = os.getenv("OTS_CA_STATE", "XX") + OTS_CA_CITY = os.getenv("OTS_CA_CITY", "YY") + OTS_CA_ORGANIZATION = os.getenv("OTS_CA_ORGANIZATION", "ZZ") + OTS_CA_ORGANIZATIONAL_UNIT = os.getenv("OTS_CA_ORGANIZATIONAL_UNIT", "OpenTAKServer") + OTS_CA_SUBJECT = os.getenv( + "OTS_CA_SUBJECT", + f"/C={OTS_CA_COUNTRY}/ST={OTS_CA_STATE}/L={OTS_CA_CITY}/O={OTS_CA_ORGANIZATION}/OU={OTS_CA_ORGANIZATIONAL_UNIT}" + ) + + # Flask-SQLAlchemy settings + SQLALCHEMY_DATABASE_URI = os.getenv("SQLALCHEMY_DATABASE_URI", f"sqlite:///{os.path.join(OTS_DATA_FOLDER, 'ots.db')}") + SQLALCHEMY_ECHO = os.getenv("SQLALCHEMY_ECHO", "False").lower() in ["true", "1", "yes"] + SQLALCHEMY_ENGINE_OPTIONS = {"pool_pre_ping": True} + SQLALCHEMY_TRACK_MODIFICATIONS = os.getenv("SQLALCHEMY_TRACK_MODIFICATIONS", "False").lower() in ["true", "1", "yes"] + SQLALCHEMY_RECORD_QUERIES = os.getenv("SQLALCHEMY_RECORD_QUERIES", "False").lower() in ["true", "1", "yes"] + + # Email Settings + OTS_ENABLE_EMAIL = os.getenv("OTS_ENABLE_EMAIL", "False").lower() in ["true", "1", "yes"] + MAIL_SERVER = os.getenv("MAIL_SERVER", "smtp.gmail.com") + MAIL_PORT = int(os.getenv("MAIL_PORT", 587)) + MAIL_USE_SSL = os.getenv("MAIL_USE_SSL", "False").lower() in ["true", "1", "yes"] + MAIL_USE_TLS = os.getenv("MAIL_USE_TLS", "True").lower() in ["true", "1", "yes"] + MAIL_USERNAME = os.getenv("MAIL_USERNAME", None) + MAIL_PASSWORD = os.getenv("MAIL_PASSWORD", None) - # Meshtastic settings - OTS_ENABLE_MESHTASTIC = False - OTS_MESHTASTIC_TOPIC = "opentakserver" - OTS_MESHTASTIC_PUBLISH_INTERVAL = 30 - OTS_MESHTASTIC_DOWNLINK_CHANNELS = [] + # Flask-Security-Too + SECURITY_PASSWORD_SALT = os.getenv("SECURITY_PASSWORD_SALT", str(secrets.SystemRandom().getrandbits(128))) + SECURITY_REGISTERABLE = os.getenv("SECURITY_REGISTERABLE", str(OTS_ENABLE_EMAIL)).lower() in ["true", "1", "yes"] + SECURITY_CONFIRMABLE = os.getenv("SECURITY_CONFIRMABLE", str(OTS_ENABLE_EMAIL)).lower() in ["true", "1", "yes"] + SECURITY_RECOVERABLE = os.getenv("SECURITY_RECOVERABLE", str(OTS_ENABLE_EMAIL)).lower() in ["true", "1", "yes"] - # Gmail settings - OTS_ENABLE_EMAIL = False - OTS_EMAIL_DOMAIN_WHITELIST = [] - OTS_EMAIL_DOMAIN_BLACKLIST = [] - OTS_EMAIL_TLD_WHITELIST = [] - OTS_EMAIL_TLD_BLACKLIST = [] - MAIL_SERVER = 'smtp.gmail.com' - MAIL_PORT = 587 - MAIL_USE_SSL = False - MAIL_USE_TLS = True - MAIL_DEBUG = False - MAIL_DEFAULT_SENDER = None - MAIL_MAX_EMAILS = None - MAIL_SUPPRESS_SEND = False - MAIL_ASCII_ATTACHMENTS = False - MAIL_USERNAME = None - MAIL_PASSWORD = None + # TOTP Settings + SECURITY_TOTP_SECRETS = {1: os.getenv("SECURITY_TOTP_SECRET", pyotp.random_base32())} + SECURITY_TOTP_ISSUER = os.getenv("SECURITY_TOTP_ISSUER", "OpenTAKServer") - OTS_DELETE_OLD_DATA_SECONDS = 0 - OTS_DELETE_OLD_DATA_MINUTES = 0 - OTS_DELETE_OLD_DATA_HOURS = 0 - OTS_DELETE_OLD_DATA_DAYS = 0 - OTS_DELETE_OLD_DATA_WEEKS = 1 + # Log Rotation + OTS_LOG_ROTATE_WHEN = os.getenv("OTS_LOG_ROTATE_WHEN", "midnight") + OTS_LOG_ROTATE_INTERVAL = int(os.getenv("OTS_LOG_ROTATE_INTERVAL", 0)) - # flask-sqlalchemy - SQLALCHEMY_DATABASE_URI = "sqlite:////{}".format(os.path.join(OTS_DATA_FOLDER, 'ots.db')) - SQLALCHEMY_ECHO = False - SQLALCHEMY_ENGINE_OPTIONS = {"pool_pre_ping": True} - SQLALCHEMY_TRACK_MODIFICATIONS = False - SQLALCHEMY_RECORD_QUERIES = False + # Data Cleanup + OTS_DELETE_OLD_DATA_DAYS = int(os.getenv("OTS_DELETE_OLD_DATA_DAYS", 0)) - ALLOWED_EXTENSIONS = ['zip', 'xml', 'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif', 'kml', 'kmz'] + # Allowed Upload Extensions + ALLOWED_EXTENSIONS = os.getenv("ALLOWED_EXTENSIONS", "zip,xml,txt,pdf,png,jpg,jpeg,gif,kml,kmz").split(",") - UPLOAD_FOLDER = os.path.join(OTS_DATA_FOLDER, 'uploads') + # Upload Folder + UPLOAD_FOLDER = os.getenv("UPLOAD_FOLDER", os.path.join(OTS_DATA_FOLDER, "uploads")) if not os.path.exists(UPLOAD_FOLDER): os.makedirs(UPLOAD_FOLDER) - # Flask-Security-Too - SECURITY_PASSWORD_SALT = str(secrets.SystemRandom().getrandbits(128)) - REMEMBER_COOKIE_SAMESITE = "strict" - SESSION_COOKIE_SAMESITE = "strict" - SECURITY_USERNAME_ENABLE = True - SECURITY_USERNAME_REQUIRED = True - SECURITY_TRACKABLE = True - SECURITY_CSRF_COOKIE_NAME = "XSRF-TOKEN" - WTF_CSRF_TIME_LIMIT = None - SECURITY_CSRF_IGNORE_UNAUTH_ENDPOINTS = True - WTF_CSRF_CHECK_DEFAULT = False - SECURITY_RETURN_GENERIC_RESPONSES = True - SECURITY_URL_PREFIX = "/api" - SECURITY_CHANGEABLE = True - SECURITY_CHANGE_URL = "/password/change" - SECURITY_RESET_URL = "/password/reset" - SECURITY_PASSWORD_LENGTH_MIN = 8 - SECURITY_REGISTERABLE = OTS_ENABLE_EMAIL - SECURITY_CONFIRMABLE = OTS_ENABLE_EMAIL - SECURITY_RECOVERABLE = OTS_ENABLE_EMAIL - SECURITY_TWO_FACTOR = True - SECURITY_TOTP_SECRETS = {1: pyotp.random_base32()} - SECURITY_TOTP_ISSUER = "OpenTAKServer" - SECURITY_TWO_FACTOR_ENABLED_METHODS = ["authenticator", "email"] - SECURITY_TWO_FACTOR_RESCUE_MAIL = MAIL_USERNAME - SECURITY_TWO_FACTOR_ALWAYS_VALIDATE = False - SECURITY_CSRF_PROTECT_MECHANISMS = ["session", "basic"] - SECURITY_LOGIN_WITHOUT_CONFIRMATION = True - SECURITY_POST_CONFIRM_VIEW = "/login" - SECURITY_REDIRECT_BEHAVIOR = 'spa' - SECURITY_RESET_VIEW = '/reset' - SECURITY_USERNAME_MIN_LENGTH = 1 - - SCHEDULER_API_ENABLED = False + # Flask Security + SECURITY_PASSWORD_SALT = os.getenv("SECURITY_PASSWORD_SALT", str(secrets.SystemRandom().getrandbits(128))) + REMEMBER_COOKIE_SAMESITE = os.getenv("REMEMBER_COOKIE_SAMESITE", "strict") + SESSION_COOKIE_SAMESITE = os.getenv("SESSION_COOKIE_SAMESITE", "strict") + SECURITY_USERNAME_ENABLE = os.getenv("SECURITY_USERNAME_ENABLE", "True").lower() in ["true", "1", "yes"] + SECURITY_USERNAME_REQUIRED = os.getenv("SECURITY_USERNAME_REQUIRED", "True").lower() in ["true", "1", "yes"] + SECURITY_TRACKABLE = os.getenv("SECURITY_TRACKABLE", "True").lower() in ["true", "1", "yes"] + SECURITY_CSRF_COOKIE_NAME = os.getenv("SECURITY_CSRF_COOKIE_NAME", "XSRF-TOKEN") + WTF_CSRF_TIME_LIMIT = os.getenv("WTF_CSRF_TIME_LIMIT", None) + SECURITY_CSRF_IGNORE_UNAUTH_ENDPOINTS = os.getenv("SECURITY_CSRF_IGNORE_UNAUTH_ENDPOINTS", "True").lower() in ["true", "1", "yes"] + WTF_CSRF_CHECK_DEFAULT = os.getenv("WTF_CSRF_CHECK_DEFAULT", "False").lower() in ["true", "1", "yes"] + SECURITY_RETURN_GENERIC_RESPONSES = os.getenv("SECURITY_RETURN_GENERIC_RESPONSES", "True").lower() in ["true", "1", "yes"] + SECURITY_URL_PREFIX = os.getenv("SECURITY_URL_PREFIX", "/api") + SECURITY_CHANGEABLE = os.getenv("SECURITY_CHANGEABLE", "True").lower() in ["true", "1", "yes"] + SECURITY_CHANGE_URL = os.getenv("SECURITY_CHANGE_URL", "/password/change") + SECURITY_RESET_URL = os.getenv("SECURITY_RESET_URL", "/password/reset") + SECURITY_PASSWORD_LENGTH_MIN = int(os.getenv("SECURITY_PASSWORD_LENGTH_MIN", 8)) + SECURITY_REGISTERABLE = os.getenv("SECURITY_REGISTERABLE", str(OTS_ENABLE_EMAIL)).lower() in ["true", "1", "yes"] + SECURITY_CONFIRMABLE = os.getenv("SECURITY_CONFIRMABLE", str(OTS_ENABLE_EMAIL)).lower() in ["true", "1", "yes"] + SECURITY_RECOVERABLE = os.getenv("SECURITY_RECOVERABLE", str(OTS_ENABLE_EMAIL)).lower() in ["true", "1", "yes"] + SECURITY_TWO_FACTOR = os.getenv("SECURITY_TWO_FACTOR", "True").lower() in ["true", "1", "yes"] + SECURITY_TOTP_SECRETS = {1: os.getenv("SECURITY_TOTP_SECRET", pyotp.random_base32())} + SECURITY_TOTP_ISSUER = os.getenv("SECURITY_TOTP_ISSUER", "OpenTAKServer") + SECURITY_TWO_FACTOR_ENABLED_METHODS = os.getenv("SECURITY_TWO_FACTOR_ENABLED_METHODS", "authenticator,email").split(",") + SECURITY_TWO_FACTOR_RESCUE_MAIL = os.getenv("SECURITY_TWO_FACTOR_RESCUE_MAIL", MAIL_USERNAME) + SECURITY_TWO_FACTOR_ALWAYS_VALIDATE = os.getenv("SECURITY_TWO_FACTOR_ALWAYS_VALIDATE", "False").lower() in ["true", "1", "yes"] + SECURITY_CSRF_PROTECT_MECHANISMS = os.getenv("SECURITY_CSRF_PROTECT_MECHANISMS", "session,basic").split(",") + SECURITY_LOGIN_WITHOUT_CONFIRMATION = os.getenv("SECURITY_LOGIN_WITHOUT_CONFIRMATION", "True").lower() in ["true", "1", "yes"] + SECURITY_POST_CONFIRM_VIEW = os.getenv("SECURITY_POST_CONFIRM_VIEW", "/login") + SECURITY_REDIRECT_BEHAVIOR = os.getenv("SECURITY_REDIRECT_BEHAVIOR", "spa") + SECURITY_RESET_VIEW = os.getenv("SECURITY_RESET_VIEW", "/reset") + SECURITY_USERNAME_MIN_LENGTH = int(os.getenv("SECURITY_USERNAME_MIN_LENGTH", 1)) + + # Job Scheduler + SCHEDULER_API_ENABLED = os.getenv("SCHEDULER_API_ENABLED", "False").lower() in ["true", "1", "yes"] JOBS = [ { "id": "get_airplanes_live_data", diff --git a/pyproject.toml b/pyproject.toml index a1ec668..2adf429 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ PyJWT = "2.9.0" pytak = "6.3.2" pytest = "8.3.3" pytest-cov = "6.0.0" -python = ">=3.10" +python = ">=3.10,<3.14" python-socketio = {extras = ["client", "websocket_client", "asyncio_client"], version = "5.11.4"} pyOpenSSL = "24.2.1" pyotp = "2.9.0"