diff options
author | Mike Crute <mcrute@gmail.com> | 2016-08-16 19:35:56 -0700 |
---|---|---|
committer | Mike Crute <mcrute@gmail.com> | 2016-08-16 19:35:56 -0700 |
commit | b89f5ffb3730c6813dfc864a8045c6b99635604c (patch) | |
tree | c5b6bcdef35c19a164e40e2c9da5dd4e44b7770a /python | |
parent | 244e353bb6a788e5f76ed999abf3149704dea754 (diff) | |
download | ubntmfi-b89f5ffb3730c6813dfc864a8045c6b99635604c.tar.bz2 ubntmfi-b89f5ffb3730c6813dfc864a8045c6b99635604c.tar.xz ubntmfi-b89f5ffb3730c6813dfc864a8045c6b99635604c.zip |
Remove non-core files and cleanup
Diffstat (limited to 'python')
-rw-r--r-- | python/inform.py | 211 | ||||
-rw-r--r-- | python/noop_server.py | 45 |
2 files changed, 256 insertions, 0 deletions
diff --git a/python/inform.py b/python/inform.py new file mode 100644 index 0000000..ee47a69 --- /dev/null +++ b/python/inform.py | |||
@@ -0,0 +1,211 @@ | |||
1 | import json | ||
2 | import copy | ||
3 | import struct | ||
4 | import binascii | ||
5 | from Crypto.Cipher import AES | ||
6 | from cStringIO import StringIO | ||
7 | |||
8 | |||
9 | class BinaryDataStream(object): | ||
10 | """Directional binary data stream | ||
11 | |||
12 | Reads and writes binary data from any stream-like object. This object is | ||
13 | not bi-directional. Does no interpertation just unpacking and packing. | ||
14 | """ | ||
15 | |||
16 | def __init__(self, data): | ||
17 | self.data = data | ||
18 | |||
19 | @classmethod | ||
20 | def for_output(cls): | ||
21 | return cls(StringIO()) | ||
22 | |||
23 | def read_int(self): | ||
24 | return struct.unpack(">i", self.data.read(4))[0] | ||
25 | |||
26 | def read_short(self): | ||
27 | return struct.unpack(">h", self.data.read(2))[0] | ||
28 | |||
29 | def read_string(self, length): | ||
30 | return self.data.read(length) | ||
31 | |||
32 | def write_int(self, data): | ||
33 | self.data.write(struct.pack(">i", data)) | ||
34 | |||
35 | def write_short(self, data): | ||
36 | self.data.write(struct.pack(">h", data)) | ||
37 | |||
38 | def write_string(self, data): | ||
39 | self.data.write(data) | ||
40 | |||
41 | def get_output(self): | ||
42 | return self.data.getvalue() | ||
43 | |||
44 | |||
45 | class Cryptor(object): | ||
46 | """AES encryption strategy | ||
47 | |||
48 | Handles AES crypto by wrapping pycrypto. Does padding and un-padding as | ||
49 | well as key conversions when needed. | ||
50 | """ | ||
51 | |||
52 | def __init__(self, key, iv): | ||
53 | self.iv = iv | ||
54 | self.key = key | ||
55 | self.cipher = AES.new(key.decode("hex"), AES.MODE_CBC, iv) | ||
56 | |||
57 | @staticmethod | ||
58 | def unpad(s): | ||
59 | return s[0:-ord(s[-1])] | ||
60 | |||
61 | @staticmethod | ||
62 | def pad(s, BS=16): | ||
63 | return s + (BS - len(s) % BS) * chr(BS - len(s) % BS) | ||
64 | |||
65 | def decrypt(self, payload): | ||
66 | return self.unpad(self.cipher.decrypt(payload)) | ||
67 | |||
68 | def encrypt(self, payload): | ||
69 | return self.cipher.encrypt(self.pad(payload)) | ||
70 | |||
71 | |||
72 | class InformPacket(object): | ||
73 | """Inform model object | ||
74 | |||
75 | Holds basic, parsed, inform packet data. Does some interpertation for | ||
76 | fields like flags. Can be passed to and from the serialiser. This class | ||
77 | only fully supports version 1 of the inform data protocol. Version 0 | ||
78 | payload parsing is not supported. | ||
79 | """ | ||
80 | |||
81 | ENCRYPTED_FLAG = 0x1 | ||
82 | COMPRESSED_FLAG = 0x2 | ||
83 | |||
84 | def __init__(self): | ||
85 | self.magic_number = None | ||
86 | self.version = None | ||
87 | self.mac_addr = None | ||
88 | self.flags = None | ||
89 | self.iv = None | ||
90 | self.data_version = None | ||
91 | self.data_length = None | ||
92 | self.raw_payload = None | ||
93 | self._used_key = None | ||
94 | |||
95 | def response_copy(self): | ||
96 | """Copy object for use in response | ||
97 | |||
98 | Generates a deep copy of the object and removes the payload so that it | ||
99 | can be used to respond to the station that send this inform request. | ||
100 | """ | ||
101 | new_obj = copy.deepcopy(self) | ||
102 | new_obj.raw_payload = None | ||
103 | return new_obj | ||
104 | |||
105 | @staticmethod | ||
106 | def _format_mac_addr(mac_bytes): | ||
107 | return ":".join([binascii.hexlify(i) for i in mac_bytes]) | ||
108 | |||
109 | def _has_flag(self, flag): | ||
110 | return self.flags & flag != 0 | ||
111 | |||
112 | @property | ||
113 | def formatted_mac_addr(self): | ||
114 | return self._format_mac_addr(self.mac_addr) | ||
115 | |||
116 | @property | ||
117 | def is_encrypted(self): | ||
118 | return self._has_flag(self.ENCRYPTED_FLAG) | ||
119 | |||
120 | @property | ||
121 | def is_compressed(self): | ||
122 | return self._has_flag(self.COMPRESSED_FLAG) | ||
123 | |||
124 | @property | ||
125 | def payload(self): | ||
126 | if self.data_version == 1: | ||
127 | return json.loads(self.raw_payload.decode("latin-1")) | ||
128 | else: | ||
129 | return self.raw_payload | ||
130 | |||
131 | @payload.setter | ||
132 | def payload(self, value): | ||
133 | self.raw_payload = json.dumps(value) | ||
134 | |||
135 | |||
136 | class InformSerializer(object): | ||
137 | """Inform protocol version 1 parser/serializer | ||
138 | |||
139 | Handles the parsing of the inform binary protocol to python objects and | ||
140 | seralization of python objects to inform binary protocol. Handles | ||
141 | cryptography and data formats. Compatible only with version 1 of the data | ||
142 | format. | ||
143 | """ | ||
144 | |||
145 | MASTER_KEY = "ba86f2bbe107c7c57eb5f2690775c712" | ||
146 | PROTOCOL_MAGIC = 1414414933 | ||
147 | MAX_VERSION = 1 | ||
148 | |||
149 | def __init__(self, key=None, key_bag=None): | ||
150 | self.key = key | ||
151 | self.key_bag = key_bag or {} | ||
152 | |||
153 | def _decrypt_payload(self, packet): | ||
154 | i = 0 | ||
155 | key = self.key_bag.get(packet.formatted_mac_addr) | ||
156 | |||
157 | for key in (key, self.key, self.MASTER_KEY): | ||
158 | if key is None: | ||
159 | continue | ||
160 | |||
161 | decrypted = Cryptor(key, packet.iv).decrypt(packet.raw_payload) | ||
162 | |||
163 | json.loads(decrypted.decode("latin-1")) | ||
164 | packet.raw_payload = decrypted | ||
165 | packet._used_key = key | ||
166 | break | ||
167 | |||
168 | def parse(self, input): | ||
169 | input_stream = BinaryDataStream(input) | ||
170 | |||
171 | packet = InformPacket() | ||
172 | |||
173 | packet.magic_number = input_stream.read_int() | ||
174 | assert packet.magic_number == self.PROTOCOL_MAGIC | ||
175 | |||
176 | packet.version = input_stream.read_int() | ||
177 | assert packet.version < self.MAX_VERSION | ||
178 | |||
179 | packet.mac_addr = input_stream.read_string(6) | ||
180 | packet.flags = input_stream.read_short() | ||
181 | packet.iv = input_stream.read_string(16) | ||
182 | packet.data_version = input_stream.read_int() | ||
183 | packet.data_length = input_stream.read_int() | ||
184 | |||
185 | packet.raw_payload = input_stream.read_string(packet.data_length) | ||
186 | |||
187 | if packet.is_encrypted: | ||
188 | self._decrypt_payload(packet) | ||
189 | |||
190 | return packet | ||
191 | |||
192 | def _encrypt_payload(self, packet): | ||
193 | if packet.data_version != 1: | ||
194 | raise ValueError("Can no encrypt contents of pre 1.0 packets") | ||
195 | |||
196 | key = packet._used_key if packet._used_key else self.MASTER_KEY | ||
197 | return Cryptor(key, packet.iv).encrypt(json.dumps(packet.payload)) | ||
198 | |||
199 | def serialize(self, packet): | ||
200 | output = BinaryDataStream.for_output() | ||
201 | |||
202 | output.write_int(packet.magic_number) | ||
203 | output.write_int(packet.version) | ||
204 | output.write_string(packet.mac_addr) | ||
205 | output.write_short(packet.flags) | ||
206 | output.write_string(packet.iv) | ||
207 | output.write_int(packet.data_version) | ||
208 | output.write_int(packet.data_length) | ||
209 | output.write_string(self._encrypt_payload(packet)) | ||
210 | |||
211 | return output.get_output() | ||
diff --git a/python/noop_server.py b/python/noop_server.py new file mode 100644 index 0000000..ebe3bda --- /dev/null +++ b/python/noop_server.py | |||
@@ -0,0 +1,45 @@ | |||
1 | #!/usr/bin/python | ||
2 | |||
3 | from StringIO import StringIO | ||
4 | from SocketServer import TCPServer | ||
5 | from inform import InformSerializer | ||
6 | from ConfigParser import SafeConfigParser | ||
7 | from SimpleHTTPServer import SimpleHTTPRequestHandler | ||
8 | |||
9 | |||
10 | class Handler(SimpleHTTPRequestHandler): | ||
11 | |||
12 | def __init__(self, *args, **kwargs): | ||
13 | super(SimpleHTTPServer, self).__init__(*args, **kwargs) | ||
14 | self.parser = InformSerializer(key_bag=get_keys("inform.cfg")) | ||
15 | |||
16 | def _get_keys(self, filename): | ||
17 | cfg = SafeConfigParser() | ||
18 | cfg.read(filename) | ||
19 | |||
20 | return dict((sect, cfg.get(sect, "authkey")) | ||
21 | for sect in cfg.sections()) | ||
22 | |||
23 | def do_POST(self): | ||
24 | length = int(self.headers['content-length']) | ||
25 | body = StringIO(self.rfile.read(length)) | ||
26 | packet = self.parser.parse(body) | ||
27 | |||
28 | noop_packet = packet.response_copy() | ||
29 | noop_packet.payload = { "_type": "noop", "interval": 10 } | ||
30 | |||
31 | buffer = StringIO(self.parser.serialize(noop_packet)) | ||
32 | |||
33 | self.send_response(200) | ||
34 | self.send_header("Content-type", "application/x-binary") | ||
35 | self.send_header("Connection", "close") | ||
36 | self.send_header("User-Agent", "Unifi Controller") | ||
37 | self.send_header("Content-Length", buffer.len) | ||
38 | self.end_headers() | ||
39 | |||
40 | return buffer | ||
41 | |||
42 | |||
43 | httpd = TCPServer(("", 9966), Handler) | ||
44 | print "serving on 9966" | ||
45 | httpd.serve_forever() | ||