You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
174 lines
6.3 KiB
174 lines
6.3 KiB
#!python3.*
|
|
|
|
"""update server.
|
|
"""
|
|
|
|
import argparse
|
|
import hashlib
|
|
import http.server
|
|
import json
|
|
import logging
|
|
import os
|
|
import re
|
|
import socket
|
|
import time
|
|
import urllib.parse
|
|
|
|
class UpdateHttpServer:
|
|
def __init__(self, port, bind, catalog_dir):
|
|
def handler(*args):
|
|
UpdateHTTPRequestHandler(catalog_dir, *args)
|
|
httpd = http.server.HTTPServer((bind, port), handler)
|
|
sa = httpd.socket.getsockname()
|
|
logger.info('http server starting {0}:{1} {2}'.format(sa[0], sa[1], catalog_dir))
|
|
try:
|
|
httpd.serve_forever()
|
|
except KeyboardInterrupt:
|
|
logger.info('Shutting down...')
|
|
httpd.socket.close()
|
|
|
|
|
|
class UpdateHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
|
|
def __init__(self, catalog_dir, *args):
|
|
self.catalog_dir = catalog_dir
|
|
http.server.BaseHTTPRequestHandler.__init__(self, *args)
|
|
|
|
def do_GET(self):
|
|
request_path = urllib.parse.urlparse(self.path)
|
|
if request_path.path == '/_catalog':
|
|
err = ''
|
|
query = urllib.parse.urlparse(self.path).query
|
|
try:
|
|
op = urllib.parse.parse_qs(query)['op'][0]
|
|
if op == 'list':
|
|
try:
|
|
path = urllib.parse.parse_qs(query)['path'][0]
|
|
except KeyError:
|
|
path = '.'
|
|
self.__send_dir(path)
|
|
result = True
|
|
else:
|
|
err = '{0} unknown operation'.format(op)
|
|
result = False
|
|
except KeyError:
|
|
err = '{0} invaid catalog request'.format(self.path)
|
|
result = False
|
|
if not result:
|
|
logger.info(err)
|
|
self.send_response(http.HTTPStatus.FORBIDDEN, err)
|
|
self.end_headers()
|
|
else:
|
|
self.__send_file(self.path)
|
|
|
|
def __check_header(self):
|
|
ex_headers_templ = ['x-*-STA-MAC', 'x-*-AP-MAC', 'x-*-FREE-SPACE', 'x-*-SKETCH-SIZE', 'x-*-SKETCH-MD5', 'x-*-CHIP-SIZE', 'x-*-SDK-VERSION']
|
|
ex_headers = []
|
|
ua = re.match('(ESP8266|ESP32)-http-Update', self.headers.get('User-Agent'))
|
|
if ua:
|
|
arch = ua.group().split('-')[0]
|
|
ex_headers = list(map(lambda x: x.replace('*', arch), ex_headers_templ))
|
|
else:
|
|
logger.info('User-Agent {0} is not HTTPUpdate'.format(ua))
|
|
return False
|
|
for ex_header in ex_headers:
|
|
if ex_header not in self.headers:
|
|
logger.info('Missing header {0} to identify a legitimate request'.format(ex_header))
|
|
return False
|
|
return True
|
|
|
|
def __send_file(self, path):
|
|
if not self.__check_header():
|
|
self.send_response(http.HTTPStatus.FORBIDDEN, 'The request available only from ESP8266 or ESP32 http updater.')
|
|
self.end_headers()
|
|
return
|
|
|
|
filename = os.path.join(self.catalog_dir, path.lstrip('/'))
|
|
logger.debug('Request file:{0}'.format(filename))
|
|
try:
|
|
fsize = os.path.getsize(filename)
|
|
self.send_response(http.HTTPStatus.OK)
|
|
self.send_header('Content-Type', 'application/octet-stream')
|
|
self.send_header('Content-Disposition', 'attachment; filename=' + os.path.basename(filename))
|
|
self.send_header('Content-Length', fsize)
|
|
self.send_header('x-MD5', get_MD5(filename))
|
|
self.end_headers()
|
|
f = open(filename, 'rb')
|
|
self.wfile.write(f.read())
|
|
f.close()
|
|
except Exception as e:
|
|
err = str(e)
|
|
logger.error(err)
|
|
self.send_response(http.HTTPStatus.INTERNAL_SERVER_ERROR, err)
|
|
self.end_headers()
|
|
|
|
def __send_dir(self, path):
|
|
content = dir_json(path)
|
|
d = json.dumps(content).encode('UTF-8', 'replace')
|
|
logger.debug(d)
|
|
self.send_response(http.HTTPStatus.OK)
|
|
self.send_header('Content-Type', 'application/json')
|
|
self.send_header('Content-Length', str(len(d)))
|
|
self.end_headers()
|
|
self.wfile.write(d)
|
|
|
|
|
|
def dir_json(path):
|
|
d = list()
|
|
for entry in os.listdir(path):
|
|
e = {'name': entry}
|
|
if os.path.isdir(entry):
|
|
e['type'] = "directory"
|
|
else:
|
|
e['type'] = "file"
|
|
if os.path.splitext(entry)[1] == '.bin':
|
|
fn = os.path.join(path, entry)
|
|
try:
|
|
f = open(fn, 'rb')
|
|
c = f.read(1)
|
|
f.close()
|
|
except Exception as e:
|
|
logger.info(str(e))
|
|
c = b'\x00'
|
|
if c == b'\xe9':
|
|
e['type'] = "bin"
|
|
mtime = os.path.getmtime(fn);
|
|
e['date'] = time.strftime('%x', time.localtime(mtime))
|
|
e['time'] = time.strftime('%X', time.localtime(mtime))
|
|
e['size'] = os.path.getsize(fn)
|
|
d.append(e)
|
|
return d
|
|
|
|
|
|
def get_MD5(filename):
|
|
try:
|
|
f = open(filename, 'rb')
|
|
bin_file = f.read()
|
|
f.close()
|
|
md5 = hashlib.md5(bin_file).hexdigest()
|
|
return md5
|
|
except Exception as e:
|
|
logger.error(str(e))
|
|
return None
|
|
|
|
|
|
def run(port=8000, bind='127.0.0.1', catalog_dir='', log_level=logging.INFO):
|
|
logging.basicConfig(level=log_level)
|
|
UpdateHttpServer(port, bind, catalog_dir)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument('--port', '-p', action='store', default=8000, type=int,
|
|
help='Port number [default:8000]')
|
|
parser.add_argument('--bind', '-b', action='store', default=socket.gethostbyname(socket.gethostname()),
|
|
help='Specifies address to which it should bind. [default:HOST IP or 127.0.0.1]')
|
|
parser.add_argument('--catalog', '-d', action='store', default=os.getcwd(),
|
|
help='Catalog directory')
|
|
parser.add_argument('--log', '-l', action='store', default='INFO',
|
|
help='Logging level')
|
|
args = parser.parse_args()
|
|
loglevel = getattr(logging, args.log.upper(), None)
|
|
if not isinstance(loglevel, int):
|
|
raise ValueError('Invalid log level: %s' % args.log)
|
|
logger = logging.getLogger(__name__)
|
|
run(args.port, args.bind, args.catalog, loglevel)
|
|
|