1 # SPDX-License-Identifier: GPL-2.0-only
3 # This file is part of Nominatim. (https://nominatim.org)
5 # Copyright (C) 2023 by the Nominatim developer community.
6 # For a full list of authors see the git log.
8 Server implementation using the falcon webserver framework.
10 from typing import Optional, Mapping, cast, Any
11 from pathlib import Path
14 from falcon.asgi import App, Request, Response
16 from nominatim.api import NominatimAPIAsync
17 import nominatim.api.v1 as api_impl
18 from nominatim.config import Configuration
20 class HTTPNominatimError(Exception):
21 """ A special exception class for errors raised during processing.
23 def __init__(self, msg: str, status: int, content_type: str) -> None:
26 self.content_type = content_type
29 async def nominatim_error_handler(req: Request, resp: Response, #pylint: disable=unused-argument
30 exception: HTTPNominatimError,
32 """ Special error handler that passes message and content type as
35 resp.status = exception.status
36 resp.text = exception.msg
37 resp.content_type = exception.content_type
40 class ParamWrapper(api_impl.ASGIAdaptor):
41 """ Adaptor class for server glue to Falcon framework.
44 def __init__(self, req: Request, resp: Response,
45 config: Configuration) -> None:
51 def get(self, name: str, default: Optional[str] = None) -> Optional[str]:
52 return cast(Optional[str], self.request.get_param(name, default=default))
55 def get_header(self, name: str, default: Optional[str] = None) -> Optional[str]:
56 return cast(Optional[str], self.request.get_header(name, default=default))
59 def error(self, msg: str, status: int = 400) -> HTTPNominatimError:
60 return HTTPNominatimError(msg, status, self.content_type)
63 def create_response(self, status: int, output: str, num_results: int) -> None:
64 self.response.context.num_results = num_results
65 self.response.status = status
66 self.response.text = output
67 self.response.content_type = self.content_type
70 def base_uri(self) -> str:
71 return cast (str, self.request.forwarded_prefix)
73 def config(self) -> Configuration:
77 class EndpointWrapper:
78 """ Converter for server glue endpoint functions to Falcon request handlers.
81 def __init__(self, name: str, func: api_impl.EndpointFunc, api: NominatimAPIAsync) -> None:
87 async def on_get(self, req: Request, resp: Response) -> None:
88 """ Implementation of the endpoint.
90 await self.func(self.api, ParamWrapper(req, resp, self.api.config))
93 class FileLoggingMiddleware:
94 """ Middleware to log selected requests into a file.
97 def __init__(self, file_name: str):
98 self.fd = open(file_name, 'a', buffering=1, encoding='utf8') # pylint: disable=R1732
101 async def process_request(self, req: Request, _: Response) -> None:
102 """ Callback before the request starts timing.
104 req.context.start = dt.datetime.now(tz=dt.timezone.utc)
107 async def process_response(self, req: Request, resp: Response,
108 resource: Optional[EndpointWrapper],
109 req_succeeded: bool) -> None:
110 """ Callback after requests writes to the logfile. It only
111 writes logs for sucessful requests for search, reverse and lookup.
113 if not req_succeeded or resource is None or resp.status != 200\
114 or resource.name not in ('reverse', 'search', 'lookup'):
117 finish = dt.datetime.now(tz=dt.timezone.utc)
118 duration = (finish - req.context.start).total_seconds()
119 params = req.scope['query_string'].decode('utf8')
120 start = req.context.start.replace(tzinfo=None)\
121 .isoformat(sep=' ', timespec='milliseconds')
123 self.fd.write(f"[{start}] "
124 f"{duration:.4f} {getattr(resp.context, 'num_results', 0)} "
125 f'{resource.name} "{params}"\n')
128 def get_application(project_dir: Path,
129 environ: Optional[Mapping[str, str]] = None) -> App:
130 """ Create a Nominatim Falcon ASGI application.
132 api = NominatimAPIAsync(project_dir, environ)
134 middleware: Optional[object] = None
135 log_file = api.config.LOG_FILE
137 middleware = FileLoggingMiddleware(log_file)
139 app = App(cors_enable=api.config.get_bool('CORS_NOACCESSCONTROL'),
140 middleware=middleware)
141 app.add_error_handler(HTTPNominatimError, nominatim_error_handler)
143 legacy_urls = api.config.get_bool('SERVE_LEGACY_URLS')
144 for name, func in api_impl.ROUTES:
145 endpoint = EndpointWrapper(name, func, api)
146 app.add_route(f"/{name}", endpoint)
148 app.add_route(f"/{name}.php", endpoint)
153 def run_wsgi() -> App:
154 """ Entry point for uvicorn.
156 Make sure uvicorn is run from the project directory.
158 return get_application(Path('.'))