Flask app that routes to multiple top level and sub domains - python

I have an app that requires multiple domains pointing to the same app with different data being displayed, but I also have the same "admin" subdomain across all domains which also displays different data depending on the domain.
An example would be:
pinetree.com - displays information about pine trees
oaktree.com - displays information about oak trees
admin.pinetree.com - displays admin for managing pine trees
admin.oaktree.com - displays admin for managing oak trees
So far, I've found that you need to write the SERVER_NAME (domain name) in the Flask config in order to use subdomains with Flask, but since I have many different types of trees with unique domains, and new trees are added all the time, I don't see how I could use that functionality.
Also, I have seen that GAE flexible doesn't have multitenancy, which is what I had first thought would be the way to manage multiple domains on GAE.

Subdomain matching, explained in another answer, should be used if you have one base domain with several subdomains. It's more straightforward since Flask can infer more about the URLs it's matching.
However, if you have multiple base domains, you should use host matching instead. You must set host_matching=True on the app object, as well as setting static_host so the static route knows which host to to serve from. Unlike subdomains, you do not set SERVER_NAME. Then pass the host option to routes. This matches against the full domain, and so it requires writing out the full domain each time, rather than just the subdomain.
Unfortunately, matching the full host means matching the port as well. Under the dev server, the port will be 5000 by default, but in production the port may be 80, 443, or something else. You can write a small helper to set the port to 5000 when running in development mode (or whatever configuration logic you need for your deployment).
from flask.helpers import get_env
def p(host):
if get_env() == "development":
return host + ":5000"
return host
# p("example.com") -> "example.com:5000"
This example shows routing to any host of the form {tree}tree.com and admin.{tree}tree.com, with pinetree.com as the static host.
from flask import Flask
app = Flask(__name__, host_matching=True, static_host=p("pinetree.com"))
#app.route("/", host=p("<tree>tree.com"))
def index(tree):
return f"{tree} tree"
Blueprint does not accept a host option yet, so you'll need to specify the host for each route. You can simplify this a bit using partial.
from functools import partial
from flask import Blueprint
admin = Blueprint("admin", __name__)
admin_route = partial(admin.route, host=p("admin.<tree>tree.com"))
#admin_route("/")
def index(tree):
return f"admin for {tree} tree"
app.register_blueprint(admin)
Note that the host can take URL parameters just like the path in the route. It will be passed to views just like path parameters. This allows for dynamic hosts and subdomains. You can use #app.url_defaults and #app.url_value_preprocessor to extract this into g instead of writing it as an argument for each view.
from flask import g
#app.url_value_preprocessor
def extract_tree(endpoint, values):
g.tree = values.pop("tree")
#app.url_defaults
def inject_tree(endpoint, values):
values.setdefault("tree", g.tree)
#app.route("/")
def index()
return f"{g.tree} tree"
During development, add the hosts your hosts file (/etc/hosts on Unix so they route to localhost.
127.0.0.1 localhost pinetree.com admin.pinetree.com oaktree.com admin.oaktree.com
And run with:
export FLASK_DEBUG=1
flask run

Related

Can't serve swagger on remotely hosted server with different url

I'm trying to serve some simple service using flask and flask_restx (a forked project of flask-restplus, that would be eventually served on AWS.
When it is served, I want to generate swagger page for others to test it easily.
from flask import Flask
from flask_restx import Api
from my_service import service_namespace
app = Flask(__name__)
api = Api(app, version='1.0')
api.add_namespace(service_namespace)
if __name__ == '__main__':
app.run(debug=True)
When I test it locally (e.g. localhost:5000), it works just fine. Problem is, when it is hosted on AWS, because it has a specific domain (gets redirected?) (e.g. my-company.com/chris-service to a container), the document page is unable to find its required files like css and so:
What I've looked and tried
Python (Flask + Swagger) Flasgger throwing 404 error
flask python creating swagger document error
404 error in Flask
Also tried adding Blueprint (albeit without knowing exactly what it does):
app = Flask(__name__)
blueprint = Blueprint("api", __name__,
root_path="/chris-service",
# url_prefix="/chris-service", # doesn't work
)
api = Api(blueprint)
app.register_blueprint(blueprint)
...
And still no luck.
Update
So here's more information as per the comments (pseudo, but technically identical)
Access point for the swagger is my-company.com/chris (with or without http:// or https:// doesn't make difference)
When connecting to the above address, the request URL for the assets are my-company.com/swaggerui/swagger-ui.css
You can access the asset in my-company.com/chris/swaggerui/swagger-ui.css
So I my resolution (which didn't work) was to somehow change the root_path (not even sure if it's the correct wording), as shown in What I've looked and tried.
I've spent about a week to solve this but can't find a way.
Any help will be greatful :) Thanks
Swagger parameters defined at apidoc.py file. Default apidoc object also created in this file. So if you want to customize it you have change it before app and api initialization.
In your case url_prefix should be changed (I recommend to use environment variables to be able set url_prefix flexibly):
$ export URL_PREFIX='/chris'
from os import environ
from flask import Flask
from flask_restx import Api, apidoc
if (url_prefix := environ.get('URL_PREFIX', None)) is not None:
apidoc.apidoc.url_prefix = url_prefix
app = Flask(__name__)
api = Api(app)
...
if __name__ == '__main__':
app.run()
Always very frustrating when stuff is working locally but not when deployed to AWS. Reading this github issue, these 404 errors on swagger assets are probably caused by:
Missing javascript swagger packages
Probably not the case, since flask-restx does this for you. And running it locally should also not work in this case.
Missing gunicorn settings
Make sure that you are also setting gunicorn up correctly as well with
--forwarded-allow-ips if deploying with it (you should be). If you are in a kubernetes cluster you can set this to *
https://docs.gunicorn.org/en/stable/settings.html#forwarded-allow-ips
According to this post, you also have to explicitly set
settings.FLASK_SERVER_NAME to something like http://ec2-10-221-200-56.us-west-2.compute.amazonaws.com:5000
If that does not work, try to deploy a flask-restx example, that should definetely work. This rules out any errors on your end.

Using Flask with subdomain breaks Google Task Queue routing

I set SERVER_NAME in my Flask app to start using subdomains so I can have e.g. frontend and backend on two different subdomains:
frontend.domain.com
backend.domain.com
I set Flask like this:
app.config['SERVER_NAME'] = 'domain.com'
app.url_map.default_subdomain = "frontend"
The app is published using Google App Engine, everything works fine, except the default App Engine domain https://PROJECT_ID.REGION_ID.r.appspot.com now returns a 404 because I understand Flask is not recognising any matching route.
I thought it was fine since I never used https://PROJECT_ID.REGION_ID.r.appspot.com, now I know I was wrong...
https://PROJECT_ID.REGION_ID.r.appspot.com is used by Google Task Cloud to route tasks and e.g. myapp.ey.r.appspot.com/my_task_worker, which is called by Cloud Tasks create_task, now hits a Not Found 404 while it worked before I set SERVER_NAME
How do I fix this? Do I have to hardcode myapp.ey.r.appspot.com in my Flask app somehow?
Here's an extract of my app.yaml, adapted:
runtime: python37
handlers:
- url: /.*
secure: always
redirect_http_response_code: 301
script: auto
env_variables:
DEBUG: False
SERVER_NAME: 'domain.com'
DEFAULT_SUBDOMAIN: 'frontend'
GCP_PROJECT: 'myapp'
CLOUD_TASK_LOCATION: 'europe-west3'
CLOUD_TASK_QUEUE: 'default'
GOOGLE_CLOUD_PLATFORM_API_KEY: 'xxxxxxxx'
...
Do I have to hardcode myapp.ey.r.appspot.com in my Flask app somehow?
Yes. The problem here is that you're managing the redirection from your App instead of leaving App engine to do it. Although this isn't a bad practice by its own, it leaves many of the App Engine features out and most important, as you already mentioned, other GCP products like Cloud Tasks expect a specific behaviour in order to work properly.
How do I fix this?
Under your current architecture you would have to add a routing to the default URL, however as far as I know Flask doesn't allow to route more than one domain, so you would have to switch the 'SERVER_NAME' to the default app engine or change into something like Django that supports multiple domains.
My suggestion is to map your subdomains to App Engine services (one for your frontend and one for your backend) and leave the routing to GCP (and remove the 'SERVER_NAME'). You can make use of the dispatch.yaml to do the routing, you can for example create the next routes:
dispatch:
# Default service serves the typical web resources and all static resources.
- url: "myapp.ey.r.appspot.com/*"
service: default
- url: "frontend.domain.com/*"
service: frontend
- url: "backend.domain.com/*"
service: backend

Get the URL of a server

I am using flask and flask email for sending an email. When I work on I used the localhost as a base url. I deployed it on the server and email sent is still showing with localhost address.
For example
base_url = "http://localhost:0.0.0.6465"
url=base_url+'/login'
I sent an email(i am using flask-mail) with the url and I can login with the url.
With the same script when I deployed on the server I am getting with same localhost address.I need the server URL should be the base url.
To debug this I tried
url=request.base_url+'/login'
I am getting 404 error in the browser if I use this. I dont want to change the initializtion of base_url because I have to use both in the local as well as in the server.
How can I do that?
You can get the URL to the currently running app through request.host_url. But what you really want to to do to get an external URL to a specific part of your application, is to use url_for as you'd do when referencing your regular endpoints, but with the parameter _external=True:
Given that you have:
#app.route('/login')
def login():
....
You can generate an external URL by using:
from flask import (
url_for,
)
...
url = url_for('login', _external=True)
This will also take into account any proxies in front of your application if you need that, as long as you've used the ProxyFix middleware when setting up your app object.
Since this uses the same mechanism as Flask uses when generating URLs between different pages, it should behave just as you want - i.e. it'll work both on localhost and on the remote host.

(Nginx) + Gunicorn + Flask + Bokeh on Azure with AD

(Apologies: I'm a webapp novice.)
REQUIREMENTS:
Azure Application Services Web App
Need Azure AD to authenticate users
Python Bokeh app to display data webapp once user is authenticated
ATTEMPT:
I tried wrapping a Flask app around Bokeh using this example After many iterations I can't get all traffic to flow to bokeh after authenticating. I've tried flask redirect(), and my latest iteration involves using requests' get() from the example in the link.
.
...
from requests import get
...
#app.route('/')
def bkapp_page():
script = server_document('https://myapp.azurewebsites.net/bkapp')
return render_template("embed.html", script=script, template="Flask")
#app.route('/bkapp', defaults={'path': ''})
#app.route('/bkapp/<path:path>")
def bkapp(path):
return get(f'http://localhost:5006/bkapp/{path}').content
...
PROBLEM:
- Despite many iterations, I can't get the template bokeh renders (which points to javascript the server serves) to point to a path that successfully renders the page. The best I have done is to get the /bkapp endpoint to not give me a 404 error.
- Python Bokeh runs on port 5006 and flask is running on 8000. I need to forward all traffic destined to bokeh (after authentication) to bokeh through flask. (Azure App Services appear to only allow one open port unless you customize a container, which I'd like to avoid.)
ALTERNATIVES?:
Sit nginx in front of gunicorn and use a 'reverse proxy' to send all traffic to bokeh. (Can I do this securely?) But I will have to create a custom container. I'd rather do this with just gunicorn + flask + bokeh, if possible.
Feel free to propose anything else. I'd like to use just gunicorn + flask + bokeh, but if I need a different method, I'll result to that.
Thanks!

Routes With Custom Domains Using Flask

My web app assigns a subdomain to users and optionally allows them to use a custom domain. This works except when the user visits their custom domain for a route without including a trailing slash.
GET requests to this url works as expected: http://user.example.com:5000/book/12345/
GET requests to this url works as expected: http://custom.com:5000/book/12345/
GET requests to this url attempt to redirect, but fail: http://custom.com:5000/book/12345
Flask ends up redirecting the browser to this url which, of course, doesn't work: http://<invalid>.example.com:5000/book/12345/
Is there a different way that I should handle custom domains? Here's a complete minimal example to reproduce this. I have set custom.com, example.com. and user.example.com to point to 127.0.0.1 in my /etc/hosts file in my development environment so that Flask receives the request.
from flask import Flask
app = Flask(__name__)
server = app.config['SERVER_NAME'] = 'example.com:5000'
#app.route('/', subdomain="<subdomain>")
#app.route('/')
def index(subdomain=None):
return ("index")
#app.route('/book/<book_id>/', subdomain="<subdomain>")
#app.route('/book/<book_id>/')
def posts(post_id, subdomain=None):
return (book_id)
if __name__ == '__main__':
app.run(host='example.com', debug=True)
I'm not sure that's possible. host matching and subdomain matching are mutually exclusive (look at host matching parameter).
I'd love to be wrong though.
One way around this issue that I can think of is to use something in front of Flask (say nginx) that points custom.com to custom.com._custom.example.com or something like that. In your code you could create a custom url_for function that would recognize this as a custom domain. I would ask on the Flask mailing list as they would be able to give you a solid answer.

Categories