Initial commit
This commit is contained in:
parent
62bca3c42d
commit
3d853e8fe9
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -94,3 +94,5 @@ ENV/
|
|||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
.data/
|
||||
certs/
|
||||
|
|
0
__init__.py
Normal file
0
__init__.py
Normal file
19
config.json
Normal file
19
config.json
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"save_path": ".data",
|
||||
"log_file": "",
|
||||
"socket": {
|
||||
"ip": "0.0.0.0",
|
||||
"port": 7766
|
||||
},
|
||||
"ws": {
|
||||
"ip": "0.0.0.0",
|
||||
"port": 7767,
|
||||
"cert": "",
|
||||
"key": ""
|
||||
},
|
||||
"logging": {
|
||||
"level": "DEBUG",
|
||||
"format": "%(asctime)s.%(msecs)03d %(name)s - %(levelname)s - %(message)s",
|
||||
"datefmt": "%d-%b-%y %H:%M:%S"
|
||||
}
|
||||
}
|
0
handlers/__init__.py
Normal file
0
handlers/__init__.py
Normal file
261
handlers/mainHandler.py
Normal file
261
handlers/mainHandler.py
Normal file
|
@ -0,0 +1,261 @@
|
|||
import asyncio
|
||||
import logging
|
||||
import struct
|
||||
|
||||
from time import time
|
||||
|
||||
from objects import glob
|
||||
|
||||
flights = {}
|
||||
#flight_data_cache = b""
|
||||
clients = set()
|
||||
|
||||
STRUCTS = {
|
||||
"start": b"IB24s",
|
||||
"end": b"",
|
||||
"flight_data": b"?BHhHff",
|
||||
"server_open": b"",
|
||||
"server_close": b""
|
||||
}
|
||||
|
||||
class Client:
|
||||
def __init__(self, ws):
|
||||
self.ws = ws
|
||||
self.subscriptions = set()
|
||||
self.get_flight_changes = False
|
||||
|
||||
async def broadcast_flight_change(uuid, active):
|
||||
logging.info("[%d] Broadcasting flight running: %r" % (uuid, active))
|
||||
for cli in [cli for cli in clients if cli.get_flight_changes or uuid in cli.subscriptions]:
|
||||
await cli.ws.send(struct.pack(b"<BI?",
|
||||
4, # packet_id
|
||||
uuid,
|
||||
active
|
||||
))
|
||||
|
||||
async def broadcast_flight_data(uuid, data):
|
||||
logging.info("[%d] Broadcasting flight data on %d bytes" % (uuid, len(data)))
|
||||
for cli in [cli for cli in clients if uuid in cli.subscriptions]:
|
||||
await cli.ws.send(struct.pack(b"<BI%ds" % len(data),
|
||||
5, # packet_id
|
||||
uuid,
|
||||
data
|
||||
))
|
||||
|
||||
async def handle(ws, path):
|
||||
if len(path):
|
||||
path = path[1:]
|
||||
# Register new client
|
||||
cli = Client(ws)
|
||||
clients.add(cli)
|
||||
logging.info("<ws://%s:%d/%s> Connected." % (*ws.remote_address, path))
|
||||
|
||||
async def send_error(msg):
|
||||
logging.debug("<ws://%s:%d/%s> -> ERROR: %s" % (*ws.remote_address, path, msg.decode()))
|
||||
await cli.ws.send(struct.pack(b"<BH%ds" % len(msg),
|
||||
0xff, # packet_id: Error
|
||||
len(msg),
|
||||
msg
|
||||
))
|
||||
|
||||
async def send_all_flights(_):
|
||||
logging.info("<ws://%s:%d/%s> -> All active flights" % (*ws.remote_address, path))
|
||||
await cli.ws.send(struct.pack(b"<2B%dI" % len(flights),
|
||||
0, # packet_id
|
||||
len(flights),
|
||||
*flights
|
||||
))
|
||||
|
||||
async def subscribe_flight_changes(data):
|
||||
if not len(data):
|
||||
return await send_error(b"Invalid data")
|
||||
|
||||
(cli.get_flight_changes,) = struct.unpack(b"<?", data[0:1])
|
||||
logging.info("<ws://%s:%d/%s> -> %subscribed to all flight changes" % (*ws.remote_address, path, cli.get_flight_changes and "S" or "Uns"))
|
||||
await cli.ws.send(struct.pack(b"<B?",
|
||||
1, # packet_id
|
||||
cli.get_flight_changes
|
||||
))
|
||||
|
||||
async def subscribe_flight(data):
|
||||
if len(data) < 2:
|
||||
return await send_error(b"Invalid data")
|
||||
|
||||
(uuid, subscribe) = struct.unpack(b"<I?", data[0:5])
|
||||
if subscribe:
|
||||
if uuid not in flights.keys():
|
||||
await send_error(b"%d not found" % uuid)
|
||||
else:
|
||||
cli.subscriptions.add(uuid)
|
||||
elif uuid in cli.subscriptions:
|
||||
cli.subscriptions.remove(uuid)
|
||||
logging.info("<ws://%s:%d/%s> -> %subscribed to %d" % (*ws.remote_address, path, uuid in cli.subscriptions and "S" or "Uns", uuid))
|
||||
await cli.ws.send(struct.pack(b"<BI?",
|
||||
2, # packet_id
|
||||
uuid,
|
||||
uuid in cli.subscriptions
|
||||
))
|
||||
|
||||
async def fetch_data(data):
|
||||
if not len(data):
|
||||
return await send_error(b"Invalid data")
|
||||
|
||||
(uuid,) = struct.unpack(b"<I", data[0:4])
|
||||
if uuid not in flights.keys():
|
||||
await send_error(b"Flight not found")
|
||||
else:
|
||||
data = flights[uuid].get_all()
|
||||
await cli.ws.send(struct.pack(b"<B%ds" % len(data),
|
||||
3, # packet_id
|
||||
data
|
||||
))
|
||||
|
||||
async def default(data):
|
||||
await send_error(b"Unimplemented function")
|
||||
logging.error("Invalid packet received: %s" % data)
|
||||
|
||||
async def query(data):
|
||||
if not len(data):
|
||||
return await send_error(b"Invalid data")
|
||||
|
||||
((packet_id,), data) = (struct.unpack(b"<B", data[0:1]), data[1:])
|
||||
|
||||
logging.debug("Received packetID: %d" % packet_id)
|
||||
|
||||
switch = {
|
||||
0: send_all_flights, # fetch all active flights
|
||||
1: subscribe_flight_changes, # next byte is a boolean to subscribe to flight changes (start & end)
|
||||
2: subscribe_flight, # next uint is uuid to subscribe to (toggle)
|
||||
3: fetch_data # fetch all stored timeline data
|
||||
}
|
||||
await switch.get(packet_id, default)(data)
|
||||
|
||||
try:
|
||||
if len(path):
|
||||
await subscribe_flight(struct.pack(b"<I?", int(path), True))
|
||||
while not cli.ws.closed:
|
||||
async for data in cli.ws:
|
||||
data = data.encode() # For some reason or another... websockets module returns a string instead of bytes?
|
||||
await query(data)
|
||||
|
||||
await asyncio.sleep(.1)
|
||||
finally:
|
||||
# Unregister current client
|
||||
clients.remove(cli)
|
||||
logging.info("<ws://%s:%d/%s> Disconnected." % (*ws.remote_address, path))
|
||||
|
||||
class Flight:
|
||||
def __init__(self, data):
|
||||
(self.uuid, self.playername_len, self.playername) = struct.unpack(b"<" + STRUCTS["start"], data[:29])
|
||||
self.active = True
|
||||
|
||||
self.last_timeline_values = [None] * 7
|
||||
self.timeline = b""
|
||||
|
||||
logging.info("[%d] Flight started" % self.uuid)
|
||||
asyncio.ensure_future( broadcast_flight_change(self.uuid, self.active) )
|
||||
|
||||
def get_all(self):
|
||||
return struct.pack(b"<" + STRUCTS["start"],
|
||||
self.uuid,
|
||||
self.playername_len,
|
||||
self.playername,
|
||||
) + self.timeline
|
||||
|
||||
def add(self, data):
|
||||
data = [*struct.unpack(b"<" + STRUCTS["flight_data"], data[:16])]
|
||||
for i in range(len(data)):
|
||||
if data[i] == self.last_timeline_values[i]:
|
||||
data[i] = None
|
||||
else:
|
||||
self.last_timeline_values[i] = data[i]
|
||||
|
||||
frame = self.format_flight_data(data)
|
||||
if not frame:
|
||||
logging.debug("[%d] Empty frame (skipping)" % self.uuid)
|
||||
return
|
||||
|
||||
logging.debug("[%d] New frame: %s" % (self.uuid, frame))
|
||||
asyncio.ensure_future( broadcast_flight_data(self.uuid, frame) )
|
||||
|
||||
self.timeline += frame
|
||||
|
||||
@staticmethod
|
||||
def format_flight_data(data):
|
||||
ret = b""
|
||||
flag = 0
|
||||
for i in range(len(data)):
|
||||
if data[i] != None:
|
||||
flag |= 1<<i
|
||||
if i != 0: # We dont include the paused bool (?) in the data as we keep it in the flag instead
|
||||
ret += struct.pack(b"<" + bytes([STRUCTS["flight_data"][i]]), data[i])
|
||||
|
||||
if flag == 0: # Empty frame; Nothing changed so we dont even add a timestamp
|
||||
return None
|
||||
|
||||
return struct.pack(b"<IB", int(time()), flag) + ret
|
||||
|
||||
def end(self):
|
||||
self.active = False
|
||||
|
||||
logging.info("[%d] Flight ended" % self.uuid)
|
||||
asyncio.ensure_future( broadcast_flight_change(self.uuid, self.active) )
|
||||
|
||||
with open("%s/%s.flt" % (glob.config["save_path"], self.uuid), "wb") as f:
|
||||
f.write(self.get_all())
|
||||
|
||||
class DiscoveryProtocol(asyncio.DatagramProtocol):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
def connection_made(self, transport):
|
||||
self.transport = transport
|
||||
def datagram_received(self, data, addr):
|
||||
logging.info("Received data from %s:%d" % addr)
|
||||
|
||||
if len(data) < 4: # Not even a header... smh
|
||||
logging.debug("Noise received: %b" % data)
|
||||
return
|
||||
|
||||
(magic, packet_id) = struct.unpack(b"<3sB", data[:4])
|
||||
if magic != b"FLY" or packet_id > 5:
|
||||
logging.error("Invalid packet header: %b" % data[:4])
|
||||
return
|
||||
|
||||
switch = {
|
||||
0: self.unimplemented, # ping?
|
||||
1: self.handle_start,
|
||||
2: self.handle_flight_data,
|
||||
3: self.handle_end,
|
||||
4: self.handle_server_open,
|
||||
5: self.handle_server_close
|
||||
}
|
||||
switch.get(packet_id, self.unimplemented)(data[4:])
|
||||
|
||||
@staticmethod
|
||||
def unimplemented(data):
|
||||
logging.warning("Unimplemented: %b" % data)
|
||||
|
||||
@staticmethod
|
||||
def handle_start(data):
|
||||
flight = Flight(data)
|
||||
flights[flight.uuid] = flight
|
||||
logging.debug("Added %d into flights dict" % flight.uuid)
|
||||
|
||||
@staticmethod
|
||||
def handle_end(data):
|
||||
(uuid, data) = (*struct.unpack(b"<I", data[:4]), data[4:])
|
||||
flights[uuid].end()
|
||||
|
||||
@staticmethod
|
||||
def handle_flight_data(data):
|
||||
(uuid, data) = (*struct.unpack(b"<I", data[:4]), data[4:])
|
||||
flights[uuid].add(data)
|
||||
|
||||
@staticmethod
|
||||
def handle_server_open(_):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def handle_server_close(_):
|
||||
pass
|
||||
|
43
main.py
Normal file
43
main.py
Normal file
|
@ -0,0 +1,43 @@
|
|||
import websockets
|
||||
import asyncio
|
||||
import pathlib
|
||||
import logging
|
||||
|
||||
from objects import glob
|
||||
from handlers.mainHandler import handle, DiscoveryProtocol
|
||||
|
||||
enable_ssl = len(glob.config["ws"]["cert"]) != 0
|
||||
ws_addr = (glob.config["ws"]["ip"], glob.config["ws"]["port"])
|
||||
sock_addr = (glob.config["socket"]["ip"], glob.config["socket"]["port"])
|
||||
ws_kwargs = {}
|
||||
|
||||
if enable_ssl:
|
||||
import ssl
|
||||
logging.debug("Loading cert")
|
||||
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||
ssl_context.load_cert_chain(glob.config["ws"]["cert"], keyfile = glob.config["ws"]["key"])
|
||||
ws_kwargs["ssl"] = ssl_context
|
||||
else:
|
||||
logging.debug("Skipping ssl (disabled)")
|
||||
|
||||
logging.debug("Create websocket instance")
|
||||
srvr = websockets.serve(
|
||||
handle,
|
||||
*ws_addr,
|
||||
**ws_kwargs
|
||||
)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
logging.debug("Start main loop")
|
||||
|
||||
logging.info("Starting websocket on %s:%d (SSL: %s)" % (*ws_addr, enable_ssl and "ENABLED" or "DISABLED"))
|
||||
try:
|
||||
#asyncio.ensure_future(wait())
|
||||
loop.run_until_complete(loop.create_datagram_endpoint(DiscoveryProtocol, local_addr = sock_addr))
|
||||
loop.run_until_complete(srvr)
|
||||
loop.run_forever()
|
||||
except KeyboardInterrupt:
|
||||
print("Exit")
|
||||
|
||||
logging.info("Stopped")
|
0
objects/__init__.py
Normal file
0
objects/__init__.py
Normal file
31
objects/glob.py
Normal file
31
objects/glob.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
import os
|
||||
import sys
|
||||
import json
|
||||
import logging
|
||||
|
||||
with open("config.json", "r") as f:
|
||||
config = json.load(f)
|
||||
|
||||
# fix for logging level
|
||||
default_logging_fallback = False
|
||||
if type(config["logging"]["level"]) is not int:
|
||||
try:
|
||||
config["logging"]["level"] = getattr(logging, config["logging"]["level"])
|
||||
except:
|
||||
config["logging"]["level"] = logging.INFO
|
||||
default_logging_fallback = True
|
||||
|
||||
# Setup logging settings
|
||||
logging_handlers = [logging.StreamHandler(sys.stdout)]
|
||||
if len(config["log_file"]):
|
||||
logging_handlers.append(logging.FileHandler(config["log_file"]))
|
||||
logging.basicConfig(**config["logging"], handlers = logging_handlers)
|
||||
logging.debug("glob.configured logger")
|
||||
|
||||
if default_logging_fallback:
|
||||
logging.warning("Invalid logging value in config! Defaulting to logging level INFO.")
|
||||
logging.info("Logging level: %d" % config["logging"]["level"])
|
||||
|
||||
# Create save_path if not exists
|
||||
if not os.path.exists(config["save_path"]):
|
||||
os.makedirs(config["save_path"], 0o770)
|
1
requirements.txt
Normal file
1
requirements.txt
Normal file
|
@ -0,0 +1 @@
|
|||
websockets
|
42
test.html
Normal file
42
test.html
Normal file
|
@ -0,0 +1,42 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>WebSocket test</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
function hex(str)
|
||||
{
|
||||
var arr1 = [];
|
||||
for (var n = 0, l = str.length; n < l; n ++)
|
||||
{
|
||||
var hex = Number(str.charCodeAt(n)).toString(16);
|
||||
arr1.push(hex);
|
||||
}
|
||||
return arr1.join(' ');
|
||||
}
|
||||
|
||||
var ws = new WebSocket("ws://127.0.0.1:7767/"),
|
||||
messages = document.createElement('ul');
|
||||
ws.onmessage = function (event) {
|
||||
var messages = document.getElementsByTagName('ul')[0],
|
||||
message = document.createElement('li'),
|
||||
res = new Response(event.data).arrayBuffer().then(
|
||||
function(buff) {
|
||||
last = buff;
|
||||
data = new DataView(buff);
|
||||
|
||||
data_str = "";
|
||||
for (let i = 0; i < data.byteLength; i++)
|
||||
data_str += String.fromCharCode(data.getInt8(i));
|
||||
|
||||
content = document.createTextNode(data_str + " | " + hex(data_str));
|
||||
message.appendChild(content);
|
||||
messages.appendChild(message);
|
||||
}
|
||||
);
|
||||
};
|
||||
document.body.appendChild(messages);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue
Block a user