166 lines
7.4 KiB
Python
Executable File
166 lines
7.4 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# web/build.sh serve: a local static server for web/dist with the headers the page
|
|
# needs. The build uses WebAssembly threads (SharedArrayBuffer), which browsers only
|
|
# enable on a cross-origin isolated page:
|
|
# Cross-Origin-Opener-Policy: same-origin
|
|
# Cross-Origin-Embedder-Policy: require-corp
|
|
# (web/deploy/nginx-snippet.conf is the same for the real host.)
|
|
#
|
|
# Two same-origin proxies for the firmware's own servers (web/compat/wada_http.h), so
|
|
# they need no CORS or CORP headers, and the identifying User-Agent OSM asks for is
|
|
# set here (a page cannot set one). Each only passes the paths the firmware asks for:
|
|
# /tiles/... -> the map tile server (DEFAULT_TILE_SERVER in
|
|
# src/helpers/esp32/TouchPrefsStore.cpp, or --tiles URL)
|
|
# /fw/apps/... -> the Store, http://firmware.wadamesh.com/apps (or --store URL)
|
|
# serve.py DIR [PORT] [--bind ADDR] [--tiles http://tiles.example] [--store http://x/apps] [--nt DIR]
|
|
# /nt/ serves the no-threads bundle (web/dist-nothreads, or --nt DIR) the page loads when
|
|
# it is not cross-origin isolated (any http:// origin other than localhost).
|
|
import http.server
|
|
import os
|
|
import re
|
|
import sys
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
UA = 'wadamesh-web (https://github.com/ALLFATHER-BV/wadamesh)'
|
|
# z/x/y.jpg (+ the topo style's /opentopo prefix) and the line-of-sight elevation query
|
|
TILE_PATH = re.compile(r'^/tiles(/(?:[a-z]+/)?\d{1,2}/\d{1,7}/\d{1,7}\.(?:jpg|png)|/elev\?locations=[0-9.,|%C-]{1,1400})$')
|
|
|
|
|
|
def default_tile_server():
|
|
src = os.path.join(HERE, '..', '..', 'src', 'helpers', 'esp32', 'TouchPrefsStore.cpp')
|
|
try:
|
|
m = re.search(r'DEFAULT_TILE_SERVER = "([^"]+)"', open(src, encoding='utf-8').read())
|
|
if m:
|
|
return m.group(1).rstrip('/')
|
|
except OSError:
|
|
pass
|
|
return 'http://tiles.wadamesh.com'
|
|
|
|
|
|
TILE_UPSTREAM = default_tile_server()
|
|
NT_DIR = None # the no-threads bundle served at /nt/ (main(): --nt DIR, else ../dist-nothreads)
|
|
STORE_UPSTREAM = 'http://firmware.wadamesh.com/apps'
|
|
# catalogs, <id>/<ver>/<file> (code, manifest, the app's own files), language files
|
|
STORE_PATH = re.compile(r'^/fw/apps/(apps\.json|langs\.json|[A-Za-z0-9_-]{1,20}/[0-9][0-9.]{0,7}/[A-Za-z0-9_-][A-Za-z0-9._-]{0,39}'
|
|
r'|lang/(?:[0-9][0-9.]{0,7}/)?[a-z]{2}(?:-[a-z]{2})?\.lang)$')
|
|
|
|
|
|
class Handler(http.server.SimpleHTTPRequestHandler):
|
|
extensions_map = {
|
|
**http.server.SimpleHTTPRequestHandler.extensions_map,
|
|
'.wasm': 'application/wasm',
|
|
'.js': 'text/javascript',
|
|
'.mjs': 'text/javascript',
|
|
'.svg': 'image/svg+xml',
|
|
'.webmanifest': 'application/manifest+json',
|
|
}
|
|
|
|
def translate_path(self, path):
|
|
# nt/: the no-threads bundle (web/dist-nothreads), as the site serves it at /nt/
|
|
if NT_DIR and path.split('?', 1)[0].startswith('/nt/'):
|
|
rel = path.split('?', 1)[0][len('/nt/'):]
|
|
if '..' in rel or rel.startswith('/'):
|
|
return os.path.join(NT_DIR, '__nope__')
|
|
return os.path.join(NT_DIR, rel)
|
|
return super().translate_path(path)
|
|
|
|
def do_GET(self):
|
|
if self.path.startswith('/tiles/'):
|
|
return self.proxy(TILE_PATH, TILE_UPSTREAM, '/tiles', 'public, max-age=1209600')
|
|
if self.path.startswith('/fw/apps/'):
|
|
# catalogs change with every app release; a version folder never changes
|
|
catalog = self.path.endswith('.json') and self.path.count('/') == 3
|
|
return self.proxy(STORE_PATH, STORE_UPSTREAM, '/fw/apps',
|
|
'no-cache' if catalog else 'public, max-age=604800')
|
|
return super().do_GET()
|
|
|
|
def proxy(self, allowed, upstream, prefix, cache):
|
|
if not allowed.match(self.path):
|
|
self.send_error(404)
|
|
return
|
|
url = upstream + self.path[len(prefix):]
|
|
req = urllib.request.Request(url, headers={'User-Agent': UA})
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=20) as r:
|
|
body, status, ctype = r.read(), r.status, r.headers.get('Content-Type', 'application/octet-stream')
|
|
except urllib.error.HTTPError as e:
|
|
body, status, ctype = b'', e.code, 'text/plain'
|
|
except (urllib.error.URLError, OSError):
|
|
self.send_error(502, 'upstream unreachable')
|
|
return
|
|
self.send_response(status)
|
|
self.send_header('Content-Type', ctype)
|
|
self.send_header('Content-Length', str(len(body)))
|
|
if status == 200:
|
|
self.send_header('Cache-Control', cache)
|
|
self._proxied = True
|
|
self.end_headers()
|
|
self.wfile.write(body)
|
|
|
|
def send_head(self):
|
|
# Serve the precompressed .br / .gz that build.sh writes, as nginx's
|
|
# brotli_static / gzip_static would (so local load times are realistic).
|
|
path = self.translate_path(self.path)
|
|
accept = self.headers.get('Accept-Encoding', '')
|
|
for enc, ext in (('br', '.br'), ('gzip', '.gz')):
|
|
if enc in accept and os.path.isfile(path + ext):
|
|
ctype = self.guess_type(path)
|
|
f = open(path + ext, 'rb')
|
|
size = os.fstat(f.fileno()).st_size
|
|
self.send_response(200)
|
|
self.send_header('Content-Type', ctype)
|
|
self.send_header('Content-Encoding', enc)
|
|
self.send_header('Content-Length', str(size))
|
|
self.send_header('Vary', 'Accept-Encoding')
|
|
self.end_headers()
|
|
return f
|
|
return super().send_head()
|
|
|
|
def end_headers(self):
|
|
self.send_header('Cross-Origin-Opener-Policy', 'same-origin')
|
|
self.send_header('Cross-Origin-Embedder-Policy', 'require-corp')
|
|
self.send_header('Cross-Origin-Resource-Policy', 'same-origin')
|
|
if not getattr(self, '_proxied', False):
|
|
self.send_header('Cache-Control', 'no-cache')
|
|
self._proxied = False
|
|
super().end_headers()
|
|
|
|
def log_message(self, fmt, *args):
|
|
if os.environ.get('WADA_SERVE_QUIET'):
|
|
return
|
|
super().log_message(fmt, *args)
|
|
|
|
|
|
def main():
|
|
args = [a for a in sys.argv[1:] if not a.startswith('--')]
|
|
bind = '127.0.0.1'
|
|
if '--bind' in sys.argv:
|
|
bind = sys.argv[sys.argv.index('--bind') + 1]
|
|
args = [a for a in args if a != bind]
|
|
global TILE_UPSTREAM, STORE_UPSTREAM
|
|
if '--tiles' in sys.argv:
|
|
TILE_UPSTREAM = sys.argv[sys.argv.index('--tiles') + 1].rstrip('/')
|
|
args = [a for a in args if a.rstrip('/') != TILE_UPSTREAM]
|
|
if '--store' in sys.argv:
|
|
STORE_UPSTREAM = sys.argv[sys.argv.index('--store') + 1].rstrip('/')
|
|
args = [a for a in args if a.rstrip('/') != STORE_UPSTREAM]
|
|
root = args[0] if args else '.'
|
|
global NT_DIR
|
|
nt = sys.argv[sys.argv.index('--nt') + 1] if '--nt' in sys.argv else os.path.join(root, '..', 'dist-nothreads')
|
|
args = [a for a in args if a != nt]
|
|
NT_DIR = os.path.abspath(nt) if os.path.isdir(nt) else None
|
|
port = int(args[1]) if len(args) > 1 else 8080
|
|
os.chdir(root)
|
|
srv = http.server.ThreadingHTTPServer((bind, port), Handler)
|
|
print(f'wadamesh web: http://{"localhost" if bind == "127.0.0.1" else bind}:{port}/ (serving {os.getcwd()}, map tiles from {TILE_UPSTREAM}, Store from {STORE_UPSTREAM}, Ctrl-C stops)', flush=True)
|
|
try:
|
|
srv.serve_forever()
|
|
except KeyboardInterrupt:
|
|
pass
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|