python-3.xsslfastapiuvicornhttp-redirect

uvicorn [fastapi] python run both HTTP and HTTPS


I'm trying to run a fastapi app with SSL.

I am running the app with uvicorn.

I can run the server on port 80 with HTTP,

if __name__ == '__main__':
    uvicorn.run("main:app", port=80, host='0.0.0.0', reload = True, reload_dirs = ["html_files"])

To run the port with HTTPS, I do the following,

if __name__ == '__main__':
    uvicorn.run("main:app", port=443, host='0.0.0.0', reload = True, reload_dirs = ["html_files"], ssl_keyfile="/etc/letsencrypt/live/my_domain/privkey.pem", ssl_certfile="/etc/letsencrypt/live/my_domain/fullchain.pem")

How can I run both or simply integrate https redirect?

N.B: This is a setup on a server where I don't want to use nginx, I know how to use nginx to implement https redirect.


Solution

  • You could use the HTTPSRedirectMiddleware. This would enforce "that all incoming requests must either be https or wss. Any incoming requests to http or ws will be redirected to the secure scheme instead`.

    Once both the apps below are running, the URL http://127.0.0.1, for instance, will be redirected to https://127.0.0.1.

    If you wish disabling the Swagger UI and ReDoc API autodcos in production, you could instead use the following line in the examples below:

    app = FastAPI(docs_url=None, redoc_url=None, openapi_url=None)
    

    If you would like having the https_redirect app starting up automatically when running the main app, you might do that using the subprocess module (inside main.py), as shown below:

    if __name__ == "__main__":
        import subprocess
        subprocess.Popen(['python', '-m', 'https_redirect']) 
        uvicorn.run(
            "main:app",
            ...
        )
    

    Working Example 1

    https_redirect.py

    from fastapi import FastAPI
    from fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware
    import uvicorn
    
    app = FastAPI()
    app.add_middleware(HTTPSRedirectMiddleware)
    
    
    if __name__ == "__main__":
        uvicorn.run(app, host="0.0.0.0", port=80)
    

    main.py

    from fastapi import FastAPI
    import uvicorn
    
    app = FastAPI()
    
    
    @app.get("/")
    async def main():
        return {"message": "Hello World"}
    
    
    if __name__ == "__main__":
        uvicorn.run(
            "main:app",
            host="0.0.0.0",
            port=443,
            ssl_keyfile="./key.pem",
            ssl_certfile="./cert.pem",
        )
    

    Note that, as shown in the implementation of HTTPSRedirectMiddleware, the redirection should only take place when the apps are listening on ports 80 and 443, respectively. If a different port is used (e.g., 8000 for both the apps—that should actually be a single main app, having the HTTPSRedirectMiddleware mounted as well), even though the HTTPSRedirectMiddleware, as shown in its implementation, would change the scheme to https and keep the same port number used in the original request URL when returning the RedirectResponse, a redirection would not actually take place, as you can't really have both HTTP and HTTPS apps listening on the same port number (when the SSL files are included in the app, it should only be listening for SSL connections). In that case, you should use two different port numbers, as demonstrated in the example earlier. If, however, the ports one has chosen differ from 80 and 443, a customized middleware could be used, as shown in the following example (the apps below, for demo purposes, are listening on ports 8000 and 8443, respectively).

    Working Example 2

    https_redirect.py

    from fastapi import FastAPI
    from starlette.datastructures import URL
    from starlette.responses import RedirectResponse
    from starlette.types import ASGIApp, Receive, Scope, Send
    import uvicorn
    
    
    class HTTPSRedirectMiddleware:
        def __init__(self, app: ASGIApp) -> None:
            self.app = app
    
        async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
            if scope["type"] in ("http", "websocket") and scope["scheme"] in ("http", "ws"):
                url = URL(scope=scope)
                redirect_scheme = {"http": "https", "ws": "wss"}[url.scheme]
                url = url.replace(scheme=redirect_scheme, port=8443)
                response = RedirectResponse(url, status_code=307)
                await response(scope, receive, send)
            else:
                await self.app(scope, receive, send)
    
    
    app = FastAPI()
    app.add_middleware(HTTPSRedirectMiddleware)
     
    
    if __name__ == "__main__":
        uvicorn.run(app, host="0.0.0.0", port=8000)  # HTTP port set to 8000
    

    main.py

    # ... same code implementation here as in "Working Example 1"
    
    
    if __name__ == "__main__":
        uvicorn.run(
            "main:app",
            host="0.0.0.0",
            port=8443, # HTTPS port set to 8443
            ssl_keyfile="./key.pem",
            ssl_certfile="./cert.pem",
        )
    

    Using a reverse proxy

    Alternative solutions include using a reverse proxy server, such as Nginx, and let it catch-all port 80 (HTTP) requests and redirect them to port 443 (HTTPS)—on which the main app will be listening for SSL connections only:

    server {
        listen 80 default_server;
        server_name _;
        return 301 https://$host$request_uri;
    }
    

    The server_name is set to _, which matches any hostname used. It will return 301 redirect to the HTTPS version of whatever URI was requested.

    You could also redirect only specific sites, which is convenient when you have multiple apps/sites that not all of them should be forced to use HTTPS connections. Additionally, instead of configuring the SSL certificate and key in your app, one may set this configuration on the reverse proxy server, including the app/site that needs to listen on port 443 for SSL connection:

    server {
        listen 80;
        server_name yourdomain.com;
        return 301 https://$host$request_uri;
    }
    
    server {
        listen 443 ssl;
        server_name yourdomain.com;
        ssl_certificate /path/to/your/cert.pem;
        ssl_certificate_key /path/to/your/key.pem;
        
        location / {
            proxy_pass http://localhost:8000;  # if you have the app running on port 8000
        }
    }
    

    For further reverse proxy configurations, you may have a look at the bottom of this answer.