import base64 import logging import os import re import time from http.cookies import SimpleCookie 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) 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\d+)/(?P\d+)/(?P-?\d+)/(?P-?\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] application = TileServer()