opentakserver 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. opentakserver/EmailValidator.py +39 -0
  2. opentakserver/SocketServer.py +99 -0
  3. opentakserver/__init__.py +3 -0
  4. opentakserver/app.py +281 -0
  5. opentakserver/blueprints/__init__.py +0 -0
  6. opentakserver/blueprints/api.py +1044 -0
  7. opentakserver/blueprints/config.py +116 -0
  8. opentakserver/blueprints/marti.py +555 -0
  9. opentakserver/blueprints/ots_socketio.py +33 -0
  10. opentakserver/blueprints/scheduled_jobs.py +107 -0
  11. opentakserver/blueprints/scheduler_api.py +90 -0
  12. opentakserver/ca_config.py +90 -0
  13. opentakserver/certificate_authority.py +395 -0
  14. opentakserver/controllers/__init__.py +0 -0
  15. opentakserver/controllers/client_controller.py +234 -0
  16. opentakserver/controllers/cot_controller.py +731 -0
  17. opentakserver/defaultconfig.py +106 -0
  18. opentakserver/extensions.py +16 -0
  19. opentakserver/forms/MediaMTXGlobalConfig.py +77 -0
  20. opentakserver/forms/MediaMTXPathConfig.py +90 -0
  21. opentakserver/forms/__init__.py +0 -0
  22. opentakserver/functions.py +15 -0
  23. opentakserver/models/Alert.py +48 -0
  24. opentakserver/models/Base.py +5 -0
  25. opentakserver/models/CasEvac.py +161 -0
  26. opentakserver/models/Certificate.py +53 -0
  27. opentakserver/models/Chatrooms.py +30 -0
  28. opentakserver/models/ChatroomsUids.py +12 -0
  29. opentakserver/models/CoT.py +63 -0
  30. opentakserver/models/Config.py +11 -0
  31. opentakserver/models/DataPackage.py +57 -0
  32. opentakserver/models/EUD.py +78 -0
  33. opentakserver/models/GeoChat.py +38 -0
  34. opentakserver/models/Icon.py +35 -0
  35. opentakserver/models/Marker.py +89 -0
  36. opentakserver/models/Point.py +77 -0
  37. opentakserver/models/RBLine.py +104 -0
  38. opentakserver/models/Team.py +36 -0
  39. opentakserver/models/VideoRecording.py +59 -0
  40. opentakserver/models/VideoStream.py +115 -0
  41. opentakserver/models/WebAuthn.py +6 -0
  42. opentakserver/models/ZMIST.py +30 -0
  43. opentakserver/models/__init__.py +0 -0
  44. opentakserver/models/role.py +16 -0
  45. opentakserver/models/user.py +32 -0
  46. opentakserver/mumble/Murmur.ice +885 -0
  47. opentakserver/mumble/__init__.py +0 -0
  48. opentakserver/mumble/mumble_authenticator.py +105 -0
  49. opentakserver/mumble/mumble_ice_app.py +180 -0
  50. opentakserver-1.0.0.dist-info/LICENSE +674 -0
  51. opentakserver-1.0.0.dist-info/METADATA +113 -0
  52. opentakserver-1.0.0.dist-info/RECORD +53 -0
  53. opentakserver-1.0.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,39 @@
1
+ import tldextract
2
+ from flask_security import MailUtil
3
+ from extensions import logger
4
+
5
+
6
+ class EmailValidator(MailUtil):
7
+
8
+ def __init__(self, app):
9
+ super().__init__(app)
10
+ self.app = app
11
+
12
+ def validate(self, email: str) -> str:
13
+ domain_whitelist = self.app.config.get("OTS_EMAIL_DOMAIN_WHITELIST")
14
+ domain_blacklist = self.app.config.get("OTS_EMAIL_DOMAIN_BLACKLIST")
15
+ tld_whitelist = self.app.config.get("OTS_EMAIL_TLD_WHITELIST")
16
+ tld_blacklist = self.app.config.get("OTS_EMAIL_TLD_BLACKLIST")
17
+
18
+ domain = email.split("@")[-1]
19
+ parsed_domain = tldextract.extract(domain)
20
+ logger.debug("Got domain: {}".format(domain))
21
+
22
+ if domain_whitelist and domain not in domain_whitelist:
23
+ logger.error("Domain {} is not whitelisted".format(domain))
24
+ raise ValueError("Domain {} is not whitelisted".format(domain))
25
+
26
+ if domain_blacklist and domain in domain_blacklist:
27
+ logger.error("Domain {} is blacklisted".format(domain))
28
+ raise ValueError("Domain {} is blacklisted".format(domain))
29
+
30
+ if tld_whitelist and parsed_domain.suffix not in tld_whitelist:
31
+ logger.error("TLD {} not whitelisted".format(parsed_domain.suffix))
32
+ raise ValueError("TLD {} not whitelisted".format(parsed_domain.suffix))
33
+
34
+ if tld_blacklist and parsed_domain.suffix in tld_blacklist:
35
+ logger.error("TLD {} is blacklisted".format(parsed_domain.suffix))
36
+ raise ValueError("TLD {} is blacklisted".format(parsed_domain.suffix))
37
+
38
+ logger.info("Looks like {} is good".format(email))
39
+ return super().validate(email)
@@ -0,0 +1,99 @@
1
+ import os
2
+ import socket
3
+ import ssl
4
+ from threading import Thread
5
+
6
+ from opentakserver.controllers.client_controller import ClientController
7
+
8
+
9
+ class SocketServer(Thread):
10
+ def __init__(self, logger, app_context=None, port=8088, ssl_server=False):
11
+ super().__init__()
12
+
13
+ self.logger = logger
14
+ self.port = port
15
+ self.ssl = ssl_server
16
+ self.shutdown = False
17
+ self.daemon = True
18
+ self.socket = None
19
+ self.clients = []
20
+ self.app_context = app_context
21
+
22
+ def run(self):
23
+ if self.ssl:
24
+ self.socket = self.launch_ssl_server()
25
+ elif self.app_context.app.config.get("OTS_ENABLE_TCP_STREAMING_PORT"):
26
+ self.socket = self.launch_tcp_server()
27
+ else:
28
+ self.logger.info("TCP connections are disabled")
29
+ return
30
+
31
+ self.socket.settimeout(1.0)
32
+
33
+ while not self.shutdown:
34
+ try:
35
+ sock, addr = self.socket.accept()
36
+ if self.ssl:
37
+ self.logger.info("New SSL connection from {}".format(addr[0]))
38
+ else:
39
+ self.logger.info("New TCP connection from {}".format(addr[0]))
40
+
41
+ new_thread = ClientController(addr[0], addr[1], sock, self.logger, self.app_context.app, self.ssl)
42
+ new_thread.daemon = True
43
+ new_thread.start()
44
+ self.clients.append(new_thread)
45
+ except KeyboardInterrupt:
46
+ break
47
+ except TimeoutError:
48
+ if self.shutdown:
49
+ self.socket.shutdown(socket.SHUT_RDWR)
50
+ self.socket.close()
51
+
52
+ if self.ssl:
53
+ self.logger.info("SSL server has shut down")
54
+ else:
55
+ self.logger.info("TCP server has shut down")
56
+
57
+ def launch_tcp_server(self):
58
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
59
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
60
+ s.bind(('0.0.0.0', self.port))
61
+ s.listen(1)
62
+
63
+ return s
64
+
65
+ def launch_ssl_server(self):
66
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) as sock:
67
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
68
+
69
+ context = self.get_ssl_context()
70
+
71
+ sconn = context.wrap_socket(sock, server_side=True)
72
+ sconn.bind(('0.0.0.0', self.port))
73
+ sconn.listen(0)
74
+
75
+ return sconn
76
+
77
+ def stop(self):
78
+ if self.ssl:
79
+ self.logger.warning("Shutting down SSL server")
80
+ else:
81
+ self.logger.warning("Shutting down TCP server")
82
+
83
+ self.shutdown = True
84
+ for client in self.clients:
85
+ self.logger.debug('Attempting to stop client {}'.format(client.address))
86
+ client.stop()
87
+
88
+ def get_ssl_context(self):
89
+ context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
90
+
91
+ with self.app_context:
92
+ context.load_cert_chain(
93
+ os.path.join(self.app_context.app.config.get("OTS_CA_FOLDER"), "certs", "opentakserver", "opentakserver.pem"),
94
+ os.path.join(self.app_context.app.config.get("OTS_CA_FOLDER"), "certs", "opentakserver", "opentakserver.nopass.key"))
95
+
96
+ context.verify_mode = self.app_context.app.config.get("OTS_SSL_VERIFICATION_MODE")
97
+ context.load_verify_locations(cafile=os.path.join(self.app_context.app.config.get("OTS_CA_FOLDER"), 'ca.pem'))
98
+
99
+ return context
@@ -0,0 +1,3 @@
1
+ # These version placeholders will be replaced later during substitution.
2
+ __version__ = "1.0.0"
3
+ __version_tuple__ = (1, 0, 0)
opentakserver/app.py ADDED
@@ -0,0 +1,281 @@
1
+ import sys
2
+
3
+ import eventlet
4
+ try:
5
+ eventlet.monkey_patch()
6
+ except BaseException as e:
7
+ print("Failed to monkey_patch(): {}".format(e))
8
+
9
+ import platform
10
+ import requests
11
+ from sqlalchemy import insert
12
+ import sqlite3
13
+ from opentakserver.models.Icon import Icon
14
+
15
+ import yaml
16
+ from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
17
+
18
+ from opentakserver.EmailValidator import EmailValidator
19
+
20
+ import logging
21
+ import os
22
+
23
+ import colorlog
24
+ from werkzeug.middleware.proxy_fix import ProxyFix
25
+ from datetime import datetime
26
+ import sqlalchemy
27
+
28
+ import flask_wtf
29
+
30
+ import pika
31
+ from flask import Flask, jsonify
32
+ from flask_cors import CORS
33
+
34
+ from flask_security import Security, SQLAlchemyUserDatastore, hash_password, uia_username_mapper, uia_email_mapper
35
+ from flask_security.models import fsqla_v3 as fsqla
36
+ from flask_security.signals import user_registered
37
+
38
+ from opentakserver.extensions import logger, db, socketio, mail, apscheduler
39
+ from opentakserver.defaultconfig import DefaultConfig
40
+ from opentakserver.models.WebAuthn import WebAuthn
41
+
42
+ from opentakserver.controllers.cot_controller import CoTController
43
+ from opentakserver.certificate_authority import CertificateAuthority
44
+ from opentakserver.SocketServer import SocketServer
45
+ try:
46
+ from opentakserver.mumble.mumble_ice_app import MumbleIceDaemon
47
+ except ModuleNotFoundError:
48
+ print("Mumble auth not supported on this platform")
49
+
50
+
51
+ def init_extensions(app):
52
+ db.init_app(app)
53
+
54
+ # Handle config options that can't be serialized to yaml
55
+ app.config.update({"SCHEDULER_JOBSTORES": {'default': SQLAlchemyJobStore(url=app.config.get("SQLALCHEMY_DATABASE_URI"))}})
56
+ identity_attributes = [{"username": {"mapper": uia_username_mapper, "case_insensitive": False}}]
57
+
58
+ # Don't allow registration unless email is enabled
59
+ if app.config.get("OTS_ENABLE_EMAIL"):
60
+ identity_attributes.append({"email": {"mapper": uia_email_mapper, "case_insensitive": True}})
61
+ app.config.update({
62
+ "SECURITY_REGISTERABLE": True,
63
+ "SECURITY_CONFIRMABLE": True,
64
+ "SECURITY_RECOVERABLE": True,
65
+ "SECURITY_TWO_FACTOR_ENABLED_METHODS": ["authenticator", "email"]
66
+ })
67
+ else:
68
+ app.config.update({
69
+ "SECURITY_REGISTERABLE": False,
70
+ "SECURITY_CONFIRMABLE": False,
71
+ "SECURITY_RECOVERABLE": False,
72
+ "SECURITY_TWO_FACTOR_ENABLED_METHODS": ["authenticator"]
73
+ })
74
+ app.config.update({"SECURITY_USER_IDENTITY_ATTRIBUTES": identity_attributes})
75
+
76
+ ca = CertificateAuthority(logger, app)
77
+ ca.create_ca()
78
+
79
+ cors = CORS(app, resources={r"/api/*": {"origins": "*"}, r"/Marti/*": {"origins": "*"}, r"/*": {"origins": "*"}},
80
+ supports_credentials=True)
81
+ flask_wtf.CSRFProtect(app)
82
+
83
+ socketio.init_app(app, logger=logger)
84
+
85
+ rabbit_connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
86
+ channel = rabbit_connection.channel()
87
+ channel.exchange_declare('cot', durable=True, exchange_type='fanout')
88
+ channel.exchange_declare('dms', durable=True, exchange_type='direct')
89
+ channel.exchange_declare('chatrooms', durable=True, exchange_type='direct')
90
+ channel.queue_declare(queue='cot_controller')
91
+ channel.exchange_declare(exchange='cot_controller', exchange_type='fanout')
92
+
93
+ cot_thread = CoTController(app.app_context(), logger, db, socketio)
94
+ app.cot_thread = cot_thread
95
+
96
+ if not apscheduler.running:
97
+ apscheduler.init_app(app)
98
+ apscheduler.start(paused=True)
99
+
100
+ try:
101
+ fsqla.FsModels.set_db_info(db)
102
+ except sqlalchemy.exc.InvalidRequestError:
103
+ pass
104
+
105
+ from opentakserver.models.user import User
106
+ from opentakserver.models.role import Role
107
+
108
+ user_datastore = SQLAlchemyUserDatastore(db, User, Role, WebAuthn)
109
+ app.security = Security(app, user_datastore, mail_util_cls=EmailValidator)
110
+
111
+ mail.init_app(app)
112
+ with app.app_context():
113
+ db.create_all()
114
+
115
+
116
+ def setup_logging(app):
117
+ level = logging.INFO
118
+ if app.config.get("DEBUG"):
119
+ level = logging.DEBUG
120
+
121
+ if sys.stdout.isatty():
122
+ color_log_handler = colorlog.StreamHandler()
123
+ color_log_formatter = colorlog.ColoredFormatter(
124
+ '%(log_color)s[%(asctime)s] - OpenTAKServer[%(process)d] - %(module)s - %(levelname)s - %(message)s', datefmt="%Y-%m-%d %H:%M:%S")
125
+ color_log_handler.setFormatter(color_log_formatter)
126
+ logger.setLevel(level)
127
+ logger.addHandler(color_log_handler)
128
+ logger.info("Added color logger")
129
+
130
+ fh = logging.FileHandler(os.path.join(app.config.get("OTS_DATA_FOLDER"), 'opentakserver.log'))
131
+ fh.setLevel(level)
132
+ fh.setFormatter(logging.Formatter("[%(asctime)s] - OpenTAKServer[%(process)d] - %(module)s - %(levelname)s - %(message)s"))
133
+ logger.addHandler(fh)
134
+
135
+
136
+ def create_app():
137
+ app = Flask(__name__)
138
+ app.config.from_object(DefaultConfig)
139
+ setup_logging(app)
140
+
141
+ # Load config.yml if it exists
142
+ if os.path.exists(os.path.join(app.config.get("OTS_DATA_FOLDER"), "config.yml")):
143
+ app.config.from_file(os.path.join(app.config.get("OTS_DATA_FOLDER"), "config.yml"), load=yaml.safe_load)
144
+ else:
145
+ # First run, created config.yml based on default settings
146
+ logger.info("Creating config.yml")
147
+ with open(os.path.join(app.config.get("OTS_DATA_FOLDER"), "config.yml"), "w") as config:
148
+ conf = {}
149
+ for option in DefaultConfig.__dict__:
150
+ # Fix the sqlite DB path on Windows
151
+ if option == "SQLALCHEMY_DATABASE_URI" and platform.system() == "Windows" and DefaultConfig.__dict__[option].startswith("sqlite"):
152
+ conf[option] = DefaultConfig.__dict__[option].replace("////", "///").replace("\\", "/")
153
+ elif option.isupper():
154
+ conf[option] = DefaultConfig.__dict__[option]
155
+ config.write(yaml.safe_dump(conf))
156
+
157
+ # Try to set the MediaMTX token
158
+ try:
159
+ with open(os.path.join(app.config.get("OTS_DATA_FOLDER"), "mediamtx", "mediamtx.yml"), "r") as mediamtx_config:
160
+ conf = mediamtx_config.read()
161
+ conf = conf.replace("MTX_TOKEN", app.config.get("OTS_MEDIAMTX_TOKEN"))
162
+ with open(os.path.join(app.config.get("OTS_DATA_FOLDER"), "mediamtx", "mediamtx.yml"), "w") as mediamtx_config:
163
+ mediamtx_config.write(conf)
164
+ except BaseException as e:
165
+ logger.error("Failed to set MediaMTX token: {}".format(e))
166
+
167
+ init_extensions(app)
168
+
169
+ from opentakserver.blueprints.marti import marti_blueprint
170
+ app.register_blueprint(marti_blueprint)
171
+
172
+ from opentakserver.blueprints.api import api_blueprint
173
+ app.register_blueprint(api_blueprint)
174
+
175
+ from opentakserver.blueprints.ots_socketio import ots_socketio_blueprint
176
+ app.register_blueprint(ots_socketio_blueprint)
177
+
178
+ from opentakserver.blueprints.scheduled_jobs import scheduler_blueprint
179
+ app.register_blueprint(scheduler_blueprint)
180
+
181
+ from opentakserver.blueprints.scheduler_api import scheduler_api_blueprint
182
+ app.register_blueprint(scheduler_api_blueprint)
183
+
184
+ from opentakserver.blueprints.config import config_blueprint
185
+ app.register_blueprint(config_blueprint)
186
+
187
+ app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_host=1)
188
+
189
+ return app
190
+
191
+
192
+ app = create_app()
193
+
194
+
195
+ @app.route("/")
196
+ def home():
197
+ return jsonify([])
198
+
199
+
200
+ @app.after_request
201
+ def after_request_func(response):
202
+ response.direct_passthrough = False
203
+ return response
204
+
205
+
206
+ @user_registered.connect_via(app)
207
+ def user_registered_sighandler(app, user, confirmation_token, **kwargs):
208
+ default_role = app.security.datastore.find_or_create_role(
209
+ name="user", permissions={"user-read", "user-write"}
210
+ )
211
+ app.security.datastore.add_role_to_user(user, default_role)
212
+
213
+
214
+ if __name__ == '__main__':
215
+ with app.app_context():
216
+ logger.debug("Loading DB..")
217
+ db.create_all()
218
+
219
+ # Download the icon sets if they aren't already in the DB
220
+ icons = db.session.query(Icon).count()
221
+ if icons == 0:
222
+ logger.info("Downloading icons...")
223
+ try:
224
+ r = requests.get("https://github.com/brian7704/OpenTAKServer-Installer/raw/master/iconsets.sqlite", stream=True)
225
+ with open(os.path.join(app.config.get("OTS_DATA_FOLDER"), "icons.sqlite"), "wb") as f:
226
+ f.write(r.content)
227
+
228
+ def dict_factory(cursor, row):
229
+ d = {}
230
+ for idx, col in enumerate(cursor.description):
231
+ d[col[0]] = row[idx]
232
+ return d
233
+
234
+ con = sqlite3.connect(os.path.join(app.config.get("OTS_DATA_FOLDER"), "icons.sqlite"))
235
+ con.row_factory = dict_factory
236
+ cur = con.cursor()
237
+ rows = cur.execute("SELECT * FROM icons")
238
+ for row in rows:
239
+ db.session.execute(insert(Icon).values(**row))
240
+ db.session.commit()
241
+ except BaseException as e:
242
+ logger.error("Failed to download icons: {}".format(e))
243
+
244
+ if app.config.get("DEBUG"):
245
+ logger.debug("Starting in debug mode")
246
+ else:
247
+ logger.info("Starting in production mode")
248
+
249
+ app.security.datastore.find_or_create_role(
250
+ name="user", permissions={"user-read", "user-write"}
251
+ )
252
+
253
+ app.security.datastore.find_or_create_role(
254
+ name="administrator", permissions={"administrator"}
255
+ )
256
+
257
+ if not app.security.datastore.find_user(username="administrator"):
258
+ logger.info("Creating administrator account. The password is 'password'")
259
+ app.security.datastore.create_user(username="administrator",
260
+ password=hash_password("password"), roles=["administrator"])
261
+ db.session.commit()
262
+
263
+ tcp_thread = SocketServer(logger, app.app_context(), app.config.get("OTS_TCP_STREAMING_PORT"))
264
+ tcp_thread.start()
265
+ app.tcp_thread = tcp_thread
266
+
267
+ ssl_thread = SocketServer(logger, app.app_context(), app.config.get("OTS_SSL_STREAMING_PORT"), True)
268
+ ssl_thread.start()
269
+ app.ssl_thread = ssl_thread
270
+
271
+ if app.config.get("OTS_ENABLE_MUMBLE_AUTHENTICATION"):
272
+ logger.info("Starting mumble authentication handler")
273
+ mumble_daemon = MumbleIceDaemon(app, logger)
274
+ mumble_daemon.daemon = True
275
+ mumble_daemon.start()
276
+ else:
277
+ logger.info("Mumble authentication handler disabled")
278
+
279
+ app.start_time = datetime.now()
280
+
281
+ socketio.run(app, host="127.0.0.1", port=app.config.get("OTS_LISTENER_PORT"), debug=False, log_output=False)
File without changes