diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..03846a7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +# TODO: change this to -alpine later and add gcc and other required tools +FROM python:3.8.10-alpine +RUN mkdir /app +WORKDIR /app + +# ADD requirements.txt /app +ADD airdocs-webserver.py /app/ +ADD compare_signatures.py /app/ +ADD utils.py /app/ + +ARG port +RUN echo Port exposed: ${port} +EXPOSE ${port} + +# RUN pip3 install -r requirements.txt +RUN pip3 install numpy scipy sklearn +ENV airdocs_port=${port} +# CMD ["python3", "airdocs-webserver.py", "-l", "0.0.0.0", "-p 8081"] +CMD ["sh", "-c", "python3 airdocs-webserver.py -l 0.0.0.0 -p ${airdocs_port}"] + +# TODO: +# HTTPS, limitare dimensiune documente -> Sa nu poti pune Gigs of data diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f1f4895 --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +PORT=80 + +container: + docker build --build-arg port=$(PORT) --tag $(TAG) --file ./Dockerfile ./ + +docker-run: + docker run -p $(EXTPORT):$(PORT) $(TAG) \ No newline at end of file diff --git a/airdocs-webserver.py b/airdocs-webserver.py index 15179c8..8f9adfe 100755 --- a/airdocs-webserver.py +++ b/airdocs-webserver.py @@ -1,7 +1,8 @@ #!/usr/bin/env python import argparse -from http.server import HTTPServer, BaseHTTPRequestHandler +from http.server import HTTPServer, BaseHTTPRequestHandler, SimpleHTTPRequestHandler +from http import HTTPStatus import json, base64 from datetime import datetime from os import listdir @@ -12,12 +13,26 @@ import zlib import uuid import shutil +from signal import * +import sys +import socket, os +from socketserver import BaseServer +# from SimpleHTTPServer import SimpleHTTPRequestHandler +from OpenSSL import SSL +import ssl +import string + +import requests +from requests.exceptions import HTTPError + db_name = "airdocs" +global has_db_been_closed +db = None class S(BaseHTTPRequestHandler): - def _set_headers(self): - self.send_response(200) + def _set_headers(self, code): + self.send_response(code) self.send_header("Content-type", "text/html") self.end_headers() @@ -30,11 +45,11 @@ def _html(self, message): return content.encode("utf8") # NOTE: must return a bytes object! def do_GET(self): - self._set_headers() + self._set_headers(HTTPStatus.OK) self.wfile.write(self._html("hi!")) def do_HEAD(self): - self._set_headers() + self._set_headers(HTTPStatus.OK) def docname(self, signature): # CRC32 produces 32 bit of the entire signature(devID, wifi, gps, ble) @@ -46,21 +61,25 @@ def docname(self, signature): return key def do_POST(self): - db = shelve.open(db_name, writeback=True) + global has_db_been_closed + global db + if has_db_been_closed: + db = shelve.open(db_name, writeback=True) content_len = int(self.headers.get('Content-Length')) post_body = self.rfile.read(content_len) parsed = json.loads(post_body) # print_json(parsed) # here we display the whole message # write_json_to_file(parsed["fingerprints"]) #write only fingerprints to file - self._set_headers() # Test if (parsed["type"] == "TEST"): + self._set_headers(HTTPStatus.OK) self.wfile.write(self._html("Successful Testing")) print(parsed["fingerprints"]) # Delete 1 file if (parsed["type"] == "DELFILE"): + self._set_headers(HTTPStatus.OK) self.wfile.write(self._html("Successful Deleting")) id = parsed["id"] devId = db[id]["signature"]["devId"] @@ -76,6 +95,7 @@ def do_POST(self): # Delete all files from a certain device if (parsed["type"] == "DELALL"): + self._set_headers(HTTPStatus.OK) self.wfile.write(self._html("Successful Deleting")) for d in db: if d != "count": @@ -94,28 +114,109 @@ def do_POST(self): # Add document to Database if (parsed["type"] == "POST"): #TODO - do something with the received file - parsed["documentURL"] & fingerprints - parsed["fingerprints"] - self.wfile.write(self._html("Successful Sending")) + # print("parsed: %s" % parsed) signature = parsed["fingerprints"] + # print("signature: %s" % signature) filetype = parsed["filetype"] + # print("filetype: %s" % filetype) + signature = signature[list(signature.keys())[0]] document = os.path.basename(parsed["document"]) key = self.docname(signature) if not os.path.exists("storage"): os.makedirs("storage") if ('file' in parsed): - os.makedirs("storage/" + key) - doc_name = "storage/"+ key + "/" + document - with open(doc_name, "wb") as fp: + # os.makedirs("storage/" + key) + # doc_name = "storage/"+ key + "/" + document + # with open(doc_name, "wb") as fp: + # content = base64.b64decode(parsed['file']) # fails on invalid file: "\/9j\/4AAQSkZJRgABAQAAAQABAAD\/CxABn13\/W" -> search, then post, then anything else + # fp.write(content) + try: # check whether or not the file is in the correct format - otherwise an exception may occur and break the application content = base64.b64decode(parsed['file']) - fp.write(content) + # print(content) + except Exception as e: + return cleanup_db(self, db, str(e)) + else: + os.makedirs("storage/" + key) + doc_name = "storage/"+ key + "/" + document + with open(doc_name, "wb") as fp: + fp.write(content) try: db[key] = {"document": document, "filetype": filetype, "signature": signature} + # print("db[%s]: " % key) + # print(db[key]) # db["document"+str(db['count'])] = {"document": document, "signature": signature} finally: + self._set_headers(HTTPStatus.OK) + self.wfile.write(self._html("Successful Sending")) db["count"] += 1 + # TODO: send to IndoorAtlas {coordinates, key, documentname } + # # indoorAtlasURL = 'https://positioning-api.indooratlas.com/v1/venues/4a3a0540-9fca-11ed-912a-d149562ef888?key=01e2ffb8-fba6-4ef7-a9da-deabc075e063' + # indoorAtlasURL = 'https://app.indooratlas.com/web-api/venue-meta/4a3a0540-9fca-11ed-912a-d149562ef888/geofences' + # accessToken = "JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhcGlrZXkiOnsiaWQiOiIwYjQ3Y2QxOC0xZGE5LTQzZDctYjQ1Mi00ZjQwNTBkNzM2ODQifSwidXNlciI6eyJpZCI6ImE1ZmI2MzViLTlkOGMtNDQyNS1hOWZhLWI0Yjg1ZmIyMWQ3MiIsInVzZXJuYW1lIjoidmxhZGFsZXgwMiIsImVtYWlsIjoiY2F0YW5vaXUudmxhZEB5YWhvby5jb20iLCJ0b3NWZXJzaW9uIjozfSwiaWF0IjoxNjc4OTk2NTk4LCJleHAiOjE2NzkwODI5OTh9.A79fmQ4Sz48WLXKoGuPXUpz2_v2USpwnzGICX9gNPn0" # TODO: find a way to generate this + # response = requests.get(indoorAtlasURL, headers={'Authorization': accessToken, 'Content-Type': 'application/json;charset=UTF-8'}) + + # jsonResponse = response.json() + + # payload_features = jsonResponse["features"] + + # indoorAtlasDefault = { + # "AR": { + # "image": { + # "name": "AirDoc", + # "sha256": "2df6a3c13587b2dff4e07eb053a26d8329874e67c3224e34644d86a3740d9a74", + # "url": "https://ida-poi-images-prod.s3.eu-west-1.amazonaws.com/a5fb635b-9d8c-4425-a9fa-b4b85fb21d72/images/2df6a3c13587b2dff4e07eb053a26d8329874e67c3224e34644d86a3740d9a74" + # } + # } + # } + + # characters = string.digits + 'abcdef' + # id_1 = ''.join(random.choice(characters) for i in range(8)) + # id_2 = ''.join(random.choice(characters) for i in range(4)) + # id_3 = ''.join(random.choice(characters) for i in range(4)) + # id_4 = ''.join(random.choice(characters) for i in range(4)) + # id_5 = ''.join(random.choice(characters) for i in range(12)) + + # new_feature = { + # "type": "Feature", + # # "id": id_1+'-'+id_2+'-'+id_3+'-'+id_4+'-'+id_5, # TODO + # "id": "0baaed80-a0a6-11ed"+'-'+id_4+'-'+id_5, # TODO + # "properties": { + # "floor": signature["floor"], + # "description": document, + # "payload": { + # "IndoorAtlas": indoorAtlasDefault, + # "AirdocsKey": key + # } + # }, + # "geometry": { + # "type": "Point", + # "coordinates": [ + # signature["longitude"], # TODO + # signature["latitude"] # TODO + # ] + # } + # } + + # payload_features.append(new_feature) + # payload = { + # "type": "FeatureCollection", + # "features": payload_features + # } + # # print("PAYLOAD:") + # json_string = json.dumps(payload) + # # print(json_string) + + # response = requests.put(indoorAtlasURL, headers={'Authorization': accessToken, 'Content-Type': 'application/json;charset=UTF-8'}, data=json_string) + + # print("Status Code", response.status_code) + # print("JSON Response ", response.json()) + + # Search for documents if (parsed["type"] == "SEARCH"): + self._set_headers(HTTPStatus.OK) #TODO - use fingerprints - parsed["fingerprints"] to search for file and return the url here response = [] #self.wfile.write(self._html("New document URL here111")) @@ -123,7 +224,7 @@ def do_POST(self): q_signature = q_signature[list(q_signature.keys())[0]] q_sim_threshold = float(parsed["threshold"]) - if q_sim_threshold > 1: + if q_sim_threshold > 1: # this should not be included. q_sim_threshold = 1 precalculate_fingerprints(q_signature) @@ -131,9 +232,18 @@ def do_POST(self): if d != "count": precalculate_fingerprints(db[d]["signature"]) similarity = compare_fingerprints(q_signature, db[d]["signature"]) + print("similarity", similarity) if similarity < q_sim_threshold: document = db[d]["document"] + latitude = db[d]["signature"]["latitude"] + longitude = db[d]["signature"]["longitude"] + altitude = 0 + if "altitude" in db[d]["signature"]: + altitude = db[d]["signature"]["altitude"] + + print(altitude) description = db[d]["signature"]["comment"] + # -> devID? maybe roles? TODO: Discussions filetype = db[d]["filetype"] full_path = "storage/" + d + "/" + document if os.path.exists(full_path): @@ -145,6 +255,9 @@ def do_POST(self): "id" : d, "document" : document, "description": description, + "latitude": latitude, + "longitude": longitude, + "altitude": altitude, "file": file_string, "filetype": filetype}) else: @@ -152,29 +265,99 @@ def do_POST(self): "id" : d, "document" : document, "description": description, + "latitude": latitude, + "longitude": longitude, + "altitude": altitude, "filetype": filetype}) response = sorted(response, key = lambda i: i["similarity"]) - #print(response) + # print(response) self.wfile.write(json.dumps(response).encode(encoding='utf_8')) - db.close() + cleanup_db(self, db, "") +def cleanup_db(self, db, err_msg): + if len(err_msg) > 0: + self.send_response(HTTPStatus.BAD_REQUEST) + self.send_header("Content-type", "text/html") + self.end_headers() + self.wfile.write(self._html("Cannot process request data: " + err_msg)) + global has_db_been_closed + db.close() + print("", flush=True) # h@ck! for docker not printing stdout + has_db_been_closed = True + + +# def open_db(db_name, writeback = True): +# global has_db_been_closed +# global db +# if has_db_been_closed: +# db = shelve.open(db_name, writeback) +# return db -def run(server_class=HTTPServer, handler_class=S, addr="localhost", port=8000, filename=None, dir=None, config=None): +def handler(signal_received, frame): + # Handle any cleanup here + global has_db_been_closed + global db + print('Got signal %d. Exiting gracefully.' % (signal_received)) + if db is not None and has_db_been_closed == False: + db.close() + has_db_been_closed = True + exit(1) + +# class SecureHTTPServer(HTTPServer): +# def __init__(self, server_address, HandlerClass): +# BaseServer.__init__(self, server_address, HandlerClass) +# ctx = SSL.Context(SSL.SSLv23_METHOD) +# #server.pem's location (containing the server private key and +# #the server certificate). +# fpem = './server.pem' +# ctx.use_privatekey_file (fpem) +# ctx.use_certificate_file(fpem) +# self.socket = SSL.Connection(ctx, socket.socket(self.address_family, +# self.socket_type)) +# self.server_bind() +# self.server_activate() + +# class SecureHTTPRequestHandler(BaseHTTPRequestHandler): +# def setup(self): +# self.connection = self.request +# self.rfile = socket._fileobject(self.request, "rb", self.rbufsize) +# self.wfile = socket._fileobject(self.request, "wb", self.wbufsize) + +def run(server_class=HTTPServer, handler_class=S, addr="localhost", port=8000, filename=None, dir=None, config=None, cert=None): + global db + global has_db_been_closed + has_db_been_closed = False db = shelve.open(db_name, writeback = True) + if not 'count' in db: db['count'] = 0 db.close() + has_db_been_closed = True + if filename!=None: open_json(filename) if dir!=None: open_dir(dir) if config!=None: open_config(config) + # server_address = ('', 443) server_address = (addr, port) httpd = server_class(server_address, handler_class) + # httpd = HTTPServer(('localhost', 1443), SimpleHTTPRequestHandler) + # sslctx = ssl.SSLContext() + # sslctx.check_hostname = False # If set to True, only the hostname that matches the certificate will be accepted + # sslctx.load_cert_chain(certfile='certificate.pem', keyfile="private.pem") + # httpd.socket = sslctx.wrap_socket(httpd.socket, server_side=True) + + # httpd = HTTPServer(server_address, SimpleHTTPRequestHandler) + # TODO: if TLS -> cert as arg + if cert!=None: + print("Using cert", cert) + httpd.socket = ssl.wrap_socket(httpd.socket, certfile=cert, server_side=True) + print("Starting httpd server on {addr}:{port}") httpd.serve_forever() @@ -217,6 +400,9 @@ def open_config(config): if __name__ == "__main__": + signal(SIGINT, handler) + signal(SIGTERM, handler) + parser = argparse.ArgumentParser(description="Run a simple HTTP server") parser.add_argument( "-l", @@ -237,6 +423,11 @@ def open_config(config): help="Specify the json file", required=False, ) + parser.add_argument( + "--cert", + help="Specify the TLS cert file", + required=False, + ) parser.add_argument( "-d", "--dir", @@ -250,4 +441,4 @@ def open_config(config): required=False, ) args = parser.parse_args() - run(addr=args.listen, port=args.port, filename=args.file, dir=args.dir, config=args.config) + run(addr=args.listen, port=args.port, filename=args.file, dir=args.dir, config=args.config, cert=args.cert) diff --git a/initial-commit b/initial-commit new file mode 100644 index 0000000..d8f9dc6 --- /dev/null +++ b/initial-commit @@ -0,0 +1 @@ +# Initial commit \ No newline at end of file diff --git a/server.pem b/server.pem new file mode 100644 index 0000000..d8094e5 --- /dev/null +++ b/server.pem @@ -0,0 +1,49 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDaH6D8nGQC3DfM +FoUGxwZ7tI4sYTT4l4hGL6QIjMKOlAYuREqPLRT6COxalDTzf01Xo6wHNLB8l3bz +WwtLSYQZkCDCCTIIcTwp2d+wSSylewGHInCF/PqucBbNF+QCspQVvF0p9uOgSmMH +Go4vxDyK0jjKqTDIzfcrq4Di9S8tUGjveOHk19YYwKXSs/Bt3owhTNMEg8HtNuGo +infeGXnTJoEIYBaYu5qh6juN4NmPQjYF42Acsa4vSTqIYSTB5tZ2CUQY70sPECxZ +034CWOhGgDFae1JO/O0CWEagmYgDoGNW0SR5hXO9pdU1DeN3+GPBdf8+5fkOYtrI +lNQwhnl9AgMBAAECggEARNn9CCAZlamL11k4bnY+fqf0xN9+SAYVFwqMvcIo27av +6MTXwDAb0f0lFkZ4HHCPql1YvQGNwBZTjuerPf2bvDH5lSYv6+hKdG94o33FoQqd +5dkiKbk8K/nnyJ+E8rEJ+3507f0nxv4UGjbwsPVBoxROpvIXGxugrOE7EukQFj4Z +W08zk/Z1bAJ+lmqSmelAJz7gjcIg3IRDmkhpkr2db5YbBSXt6f5hieQip2bLDZwz +l98XkTanegh/a156+bHGDgLqskIQboW5hZq4jMxe4VgqtY+l8h86St4Tbgve8CLO +KDh9XCQw40/uZvinUQKLQICkXX3uTPbWsjUAMMYPRQKBgQDxBmqLEvB6/3SBBzKt +E+dJ0OVFDKFVhuxgm4LT8SggLPEi/QCOJvVHf/KYHvVarUS90SZ0++xx/kYbuker +czpt0uo+6yzrYNrdCZkv9xDiT32LOkeO5tH6Wi5maqu/1Hq62R2kPmH30meDTFoL +EyBEWX/uJXkrhZGJ8XvhXz4cXwKBgQDnrPTcTmHzDptGIlNNays4dR7auNm9rrUf +/NS0+e8iBYBD58vAO0WbSZiK8vRsuUktQzhzrnXnQP0QOqQkmUwfYVx3H/bVuuDT +X93cvsfStiPy/WMaffhg+mcabuMKbVJm+lVYZpSEJ4YfbtI8Cnv5zFK4LxCpadEV +uYNE1143owKBgH7Mr1SHHDi5F+Ohk8l2RSGSYmuXH3FNUCHq7kLuBfwXaKzNiXM/ +j56T816QOuf/a8CovaChwFygNuXzdC0Aq+aHJXEAbFtQUxQiLpMLWbiVtSh/+pF1 +YVZiBzMeP0jmUcLm4i88GnI0IPz0OBLHdxw+MY1p7Xs2A8EcZ93DK3vlAoGAbjRo ++g9TKrKtDCyx4ab0FVQpTDQYZ9GemUvExx4JMe7Z9/Ngid3oG5ljtd0ihkGRXLWY +OfcoYMwaVUj/4eUGP/spaAVNfpf58LKusB4UodhAu4rrRKPdrgwPVZSf7crZIuE8 +DeQmC8NdWsB1w1fdZ5NZWgIRc/VBH4tytSGZ0O8CgYAOiLHUQeRXUdjKG9DiPEbi +4qhR7e6h3c/jFFtTQD1Tv6So69z910traQhWWAqSC0OzoZ8hlRMNy4WJP5+IHz6m +rxeVDfbhojbaG0sc0vOBS20hItslXi0zMuE8kXjo+o7Gbo5m1dKlZQ7N3r0N6pxW +XcMCpDArJ0rUAu9hoAwN0A== +-----END PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIIDazCCAlOgAwIBAgIUX0ja/zX7d6pt3GIBraGRoiFzUpkwDQYJKoZIhvcNAQEL +BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMjA1MDUyMDE4NDhaFw0yMzA1 +MDUyMDE4NDhaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw +HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQDaH6D8nGQC3DfMFoUGxwZ7tI4sYTT4l4hGL6QIjMKO +lAYuREqPLRT6COxalDTzf01Xo6wHNLB8l3bzWwtLSYQZkCDCCTIIcTwp2d+wSSyl +ewGHInCF/PqucBbNF+QCspQVvF0p9uOgSmMHGo4vxDyK0jjKqTDIzfcrq4Di9S8t +UGjveOHk19YYwKXSs/Bt3owhTNMEg8HtNuGoinfeGXnTJoEIYBaYu5qh6juN4NmP +QjYF42Acsa4vSTqIYSTB5tZ2CUQY70sPECxZ034CWOhGgDFae1JO/O0CWEagmYgD +oGNW0SR5hXO9pdU1DeN3+GPBdf8+5fkOYtrIlNQwhnl9AgMBAAGjUzBRMB0GA1Ud +DgQWBBR6k9+/MNbUqaZGBJWkdk5Yo0hF8jAfBgNVHSMEGDAWgBR6k9+/MNbUqaZG +BJWkdk5Yo0hF8jAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBh +rClp6VJDU0uTZP9h6yEYDvTr0Vam/3zvqA417gYB0kE2J5I/ODWqmb0RFhQxuj+4 +1i7Cbt0LQR+9hVlUXksiybzrBpeykmWjYvKtkbdLslgTJaRnvsm8rzqdum3qgCw8 +/V7D7MZURykAu3qlzeiqdcmr+WIQezrlSkiheEZK3k+8nq2Tyng/AuQGYxf61rni +Rg6sUvkOLHv04nySuU50VTPV/FUcXM6e8S/t1J0PfwtMSjsiBJakE8RbI+C6xO9/ +gtH8Pd9Oww+8hmnkLyf+wTBbS0n1IsrDYBosqAXAQwLvWEMAbTCRpY0r1QcskAeB +F4x4kgIid+xpa3jjG0ia +-----END CERTIFICATE----- diff --git a/tmp-file b/tmp-file new file mode 100644 index 0000000..1c2f433 --- /dev/null +++ b/tmp-file @@ -0,0 +1 @@ +tmp \ No newline at end of file