api: implement preflight request

* closes #442
This commit is contained in:
Nicola 2025-03-03 19:50:38 -05:00
parent b535d94910
commit 97dd9aab58
No known key found for this signature in database
GPG key ID: A08DEF00BA54E806
3 changed files with 98 additions and 23 deletions

View file

@ -62,6 +62,17 @@ To run the API, follow these steps from the root directory of the project:
The web server will be accessible at [http://localhost:8081/](http://localhost:8081/).
!!! warning "CORS Configuration"
When running a client web application on a different domain than the REST API server,
do not forget to set the environment variable `CAIMIRA_ALLOWED_ORIGINS`:
`export CAIMIRA_ALLOWED_ORIGINS="https://myclientapp.org"`
If you need to allow multiple domains, input a comma-separated list of domains:
`export CAIMIRA_ALLOWED_ORIGINS="https://myclientapp.org,https://myclientapp2.org"`
### API Endpoints
Currently, the REST API contains two routing categories that provide the generation of results for the main CAiMIRA outputs:

View file

@ -1,13 +1,36 @@
import os
import tornado.web
class BaseRequestHandler(tornado.web.RequestHandler):
class CorsHandler(tornado.web.RequestHandler):
"""Handler to manage CORS (Cross-Origin Resource Sharing) configuration.
This handler implements CORS support by setting appropriate headers for cross-origin requests.
It checks allowed origins against environment variable CAIMIRA_ALLOWED_ORIGINS and enables
CORS for matching origins.
"""
def set_default_headers(self):
self.set_header("Access-Control-Allow-Origin", "*")
self.set_header("Access-Control-Allow-Headers", "x-requested-with")
self.set_header("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
allowed_origins = os.environ.get("CAIMIRA_ALLOWED_ORIGINS", None)
request_origin = self.request.headers.get("Origin", None) # can have value `null`
if allowed_origins and request_origin:
allowed_origins = [origin.lower().strip() for origin in allowed_origins.split(",")]
if request_origin.lower() in allowed_origins:
self.set_header("Access-Control-Allow-Origin", request_origin)
def options(self, *args):
self.set_header("Access-Control-Allow-Methods", "POST, OPTIONS")
self.set_header("Access-Control-Allow-Headers", "Content-Type")
self.set_status(204) # No Content
class BaseRequestHandler(CorsHandler):
"""Base handler for HTTP requests extending CorsHandler.
This class provides base functionality for handling HTTP requests with CORS support.
It includes error handling capabilities by overriding the write_error method.
"""
def write_error(self, status_code, **kwargs):
self.set_status(status_code)
self.write({"message": kwargs.get('exc_info')[1].__str__()})

View file

@ -0,0 +1,41 @@
import os
from tornado.testing import AsyncHTTPTestCase
from caimira.api.app import Application
class TestAPIApp(AsyncHTTPTestCase):
def get_app(self):
return Application(debug=True)
def test_api_app(self):
response = self.fetch("/")
assert response.code == 200
def test_cors(self):
# test unset env
response = self.fetch("/", method="OPTIONS", headers={"Origin": "http://example.com"})
assert response.code == 204
assert "Access-Control-Allow-Origin" not in response.headers
# test None and empty value
os.environ["CAIMIRA_ALLOWED_ORIGINS"] = ""
response = self.fetch("/", method="OPTIONS", headers={"Origin": "http://example.com"})
assert response.code == 204
assert "Access-Control-Allow-Origin" not in response.headers
# test allowing single domain
os.environ["CAIMIRA_ALLOWED_ORIGINS"] = "http://example.com"
response = self.fetch("/", method="OPTIONS", headers={"Origin": "http://example.com"})
assert response.code == 204
assert response.headers["Access-Control-Allow-Origin"] == "http://example.com"
# test allowing multiple domains
os.environ["CAIMIRA_ALLOWED_ORIGINS"] = "http://example.com, http://example2.com"
response = self.fetch("/", method="OPTIONS", headers={"Origin": "http://example2.com"})
assert response.code == 204
assert response.headers["Access-Control-Allow-Origin"] == "http://example2.com"
# test `null` value for Origin header
os.environ["CAIMIRA_ALLOWED_ORIGINS"] = "http://example.com"
response = self.fetch("/", method="OPTIONS", headers={"Origin": "null"})
assert response.code == 204
assert "Access-Control-Allow-Origin" not in response.headers