team-3/src/c3nav/tileserver/wsgi.py

146 lines
5.5 KiB
Python
Raw Normal View History

2017-11-20 23:05:27 +01:00
import base64
import logging
import os
import re
import time
from http.cookies import SimpleCookie
2017-11-20 23:05:27 +01:00
from io import BytesIO
import requests
from c3nav.mapdata.utils.cache import CachePackage
from c3nav.mapdata.utils.tiles import (build_access_cache_key, build_base_cache_key, build_tile_etag, get_tile_bounds,
parse_tile_access_cookie)
2017-11-20 23:05:27 +01:00
logging.basicConfig(level=logging.DEBUG if os.environ.get('C3NAV_DEBUG') else logging.INFO,
format='[%(asctime)s] [%(process)s] [%(levelname)s] %(name)s: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S %z')
logger = logging.getLogger('c3nav')
class TileServer:
regex = re.compile(r'^/(?P<level>\d+)/(?P<zoom>\d+)/(?P<x>-?\d+)/(?P<y>-?\d+).png$')
def __init__(self):
try:
self.upstream_base = os.environ['C3NAV_UPSTREAM_BASE'].strip('/')
except KeyError:
raise Exception('C3NAV_UPSTREAM_BASE needs to be set.')
self.tile_secret = os.environ.get('C3NAV_TILE_SECRET', None)
if not self.tile_secret:
tile_secret_file = None
try:
tile_secret_file = os.environ['C3NAV_TILE_SECRET_FILE']
self.tile_secret = open(tile_secret_file).read().strip()
except KeyError:
raise Exception('C3NAV_TILE_SECRET or C3NAV_TILE_SECRET_FILE need to be set.')
except FileNotFoundError:
raise Exception('The C3NAV_TILE_SECRET_FILE (%s) does not exist.' % tile_secret_file)
self.auth_headers = {'X-Tile-Secret': base64.b64encode(self.tile_secret.encode())}
self.cache_package = None
self.cache_package_etag = None
wait = 1
while True:
success = self.load_cache_package()
if success:
logger.info('Cache package successfully loaded.')
break
logger.info('Retrying after %s seconds...' % wait)
time.sleep(wait)
wait = min(2, wait*2)
def load_cache_package(self):
logger.debug('Downloading cache package from upstream...')
try:
headers = self.auth_headers.copy()
if self.cache_package_etag is not None:
headers['If-None-Match'] = self.cache_package_etag
r = requests.get(self.upstream_base+'/map/cache/package.tar.xz', headers=headers)
if r.status_code == 403:
logger.error('Rejected cache package download with Error 403. Tile secret is probably incorrect.')
return False
if r.status_code == 304:
if self.cache_package is not None:
logger.debug('Not modified.')
return True
logger.error('Unexpected not modified.')
return False
r.raise_for_status()
except Exception as e:
logger.error('Cache package download failed: %s' % e)
return False
self.cache_package = CachePackage.read(BytesIO(r.content))
self.cache_package_etag = r.headers.get('ETag', None)
return True
def not_found(self, start_response, text):
start_response('404 Not Found', [('Content-Type', 'text/plain')])
return [text]
def __call__(self, env, start_response):
match = self.regex.match(env['PATH_INFO'])
if match is None:
return self.not_found(start_response, b'invalid tile path.')
zoom = int(match.group('zoom'))
if not (0 <= zoom <= 10):
return self.not_found(start_response, b'zoom out of bounds.')
# do this to be thread safe
cache_package = self.cache_package
# check if bounds are valid
x = int(match.group('x'))
y = int(match.group('y'))
minx, miny, maxx, maxy = get_tile_bounds(zoom, x, y)
if not cache_package.bounds_valid(minx, miny, maxx, maxy):
return self.not_found(start_response, b'coordinates out of bounds.')
# get level
level = int(match.group('level'))
level_data = cache_package.levels.get(level)
if level_data is None:
return self.not_found(start_response, b'invalid level.')
# decode access permissions
try:
cookie = SimpleCookie(env['HTTP_COOKIE'])['c3nav_tile_access'].value
except KeyError:
access_permissions = set()
else:
access_permissions = parse_tile_access_cookie(cookie, self.tile_secret)
# only access permissions that are affecting this tile
access_permissions &= set(level_data.restrictions[minx:miny, maxx:maxy])
# build cache keys
last_update = level_data.history.last_update(minx, miny, maxx, maxy)
base_cache_key = build_base_cache_key(last_update)
access_cache_key = build_access_cache_key(access_permissions)
# check browser cache
tile_etag = build_tile_etag(level, zoom, x, y, base_cache_key, access_cache_key, self.tile_secret)
if_none_match = env.get('HTTP_IF_NONE_MATCH')
if if_none_match == tile_etag:
start_response('304 Not Modified', [('Content-Type', 'text/plain'), ('ETag', tile_etag)])
return [b'']
r = requests.get('%s/map/%d/%d/%d/%d/%s.png' % (self.upstream_base, level, zoom, x, y, access_cache_key),
headers=self.auth_headers)
start_response('%d %s' % (r.status_code, r.reason),
[('Content-Type', r.headers['Content-Type']),
('ETag', r.headers['ETag'])])
return [r.content]
2017-11-20 23:05:27 +01:00
application = TileServer()