Python3 Http Web Server: virtual hosts - python

I am writing an rather simple http web server in python3. The web server needs to be simple - only basic reading from config files, etc. I am using only standard libraries and for now it works rather ok.
There is only one requirement for this project, which I can't implement on my own - virtual hosts. I need to have at least two virtual hosts, defined in config files. The problem is, that I can't find a way how can I implement them in python. Does anyone have any guides, articles, maybe some simple implementation how can this be done?
I would be grateful for any help.

Virtual hosts work by obeying the Host: header in the HTTP request.
Just read the headers of the request, and take action based on the value of the Host: header

For a simple HTTP web server, you can start with the WSGI reference implementation:
wsgiref is a reference implementation of the WSGI specification that can be used to add WSGI support to a web server or framework. It provides utilities for manipulating WSGI environment variables and response headers, base classes for implementing WSGI servers, a demo HTTP server that serves WSGI applications,...
Modifying the example server to check the HTTP_HOST header, here is a simple app that responds, depending on the virtual host, with a different text. (Extending the example to use a configuration file is left as an exercise).
import wsgiref
from wsgiref.simple_server import make_server
def my_app(environ,start_response):
from io import StringIO
stdout = StringIO()
host = environ["HTTP_HOST"].split(":")[0]
if host == "127.0.0.1":
print("This is virtual host 1", file=stdout)
elif host == "localhost":
print("This is virtual host 2", file=stdout)
else:
print("Unknown virtual host", file=stdout)
print("Hello world!", file=stdout)
print(file=stdout)
start_response(b"200 OK", [(b'Content-Type',b'text/plain; charset=utf-8')])
return [stdout.getvalue().encode("utf-8")]
def test1():
httpd = make_server('', 8000, my_app)
print("Serving HTTP on port 8000...")
# Respond to requests until process is killed
httpd.serve_forever()

Related

Correctly configure Python webserver service in Linux

I have a web user that can read my cheroot + Flask webserver files and can bind to ports 80 and 443 (through authbind). The systemd service is configured to run python through authbind as the web user. Problem is, cheroot takes both the SSL certificate and and key file paths to load them, and the only way I can think to make this work is to make them both readable by the web user. This, and the fact that the web user can bind to the two ports, rings alarms bells in me.
From what I understand after some research, the appropriate way to handle this would be to have another user (let's say web-starter for example) that can bind to the ports and read my private key file start the webserver and then "hand over" control to the web user.
How, and at what point do I do this? Can I do this in Python?
Example code:
webserver.service
...
User=web
Environment="FLASK_ENV=production"
ExecStart=/usr/bin/authbind --deep /usr/bin/python3 [PATH_TO_WEBAPP]/main.py
...
main.py
#! /usr/bin/python3
from cheroot.wsgi import Server as WSGIServer
from cheroot.ssl.builtin import BuiltinSSLAdapter as SSLAdapter
from ssl import TLSVersion
from myflaskapp import create_app
from os import environ
# if environment is 'development', use port 5000 and no ssl
DEVEL = environ.get('FLASK_ENV') == 'development'
if __name__ == '__main__':
port = 5000 if DEVEL else 443
server = WSGIServer(('0.0.0.0', port), create_app())
if not DEVEL:
server.ssl_adapter = SSLAdapter('ssl.crt', 'ssl.key')
server.ssl_adapter.context.minimum_version = TLSVersion.TLSv1_2
try:
server.start()
except KeyboardInterrupt:
server.stop()
PS: Also, is the use of environmental variables safe here? In other words, can a compromised web user affect them, or will systemd manage them?
ssl.key
jk ;)
Also, is there a way to protect writable files, such as SQLite databases?

How to redirect HTTP requests on a Python App on Bluemix to HTTPS only?

I have python app on Bluemix and would like to make it accessible over https only. By default I can connect both via http and https. Want to restrict access via https only. So what is the best way to disable http access, or redirect request to https only?
As ralphearle mentioned in his answer, Bluemix proxy server terminates the SSL, so you can look into the X-Forwarded-Proto header to find out if request comes from http or https.
See below a simple example based on the Python starter code from Bluemix. I added the RedirectHandler class to check for the X-Forwarded-Proto header value and redirects the request to https if it not https.
import os
try:
from SimpleHTTPServer import SimpleHTTPRequestHandler as Handler
from SocketServer import TCPServer as Server
except ImportError:
from http.server import SimpleHTTPRequestHandler as Handler
from http.server import HTTPServer as Server
class RedirectHandler(Handler):
def do_HEAD(self):
if ('X-Forwarded-Proto' in self.headers and
self.headers['X-Forwarded-Proto'] != 'https'):
self.send_response(301)
self.send_header("Location", 'https://' + self.headers['Host'] + self.path)
self.end_headers()
def do_GET(self):
self.do_HEAD()
Handler.do_GET(self)
# Read port selected by the cloud for our application
PORT = int(os.getenv('PORT', 8000))
# Change current directory to avoid exposure of control files
os.chdir('static')
httpd = Server(("", PORT), RedirectHandler)
try:
print("Start serving at port %i" % PORT)
httpd.serve_forever()
except KeyboardInterrupt:
pass
httpd.server_close()
The Bluemix proxy server terminates the SSL, so that all traffic looks like HTTP to your app. However, the proxy also adds a special HTTP header named $WSSC with a value that can be either http or https. Check this header and, if the value is set to http, then change it to https.
As Adam pointed out in his comment, the IBM forum has further discussion of this approach: https://developer.ibm.com/answers/questions/16016/how-do-i-enforce-ssl-for-my-bluemix-application.html

Hosting CGI and WSGI servers on the same port?

I've got two python servers locally hosting both the dynamic and static pages of a site.
I'm showing the dynamic page in an iframe on a static page, and want to send data to the parent static page using the javascript parent.functionx(data) approach, but I get error:
Uncaught SecurityError: Blocked a frame with origin "http://localhost:8001" from accessing a frame with origin "http://localhost:8000". Protocols, domains, and ports must match.
Can I host these both on the same port, and ideally run a single script to launch both static and dynamic pages?
A WSGI server on port 8001 to host a dynamic python page, accessed by the path /meas
cgiserver.py
import subprocess
from cherrypy import wsgiserver
def application(env, start_response):
if '/meas' in env['PATH_INFO']:
start_response('200 OK', [('Content-Type', 'text/html')])
pathargs = env['PATH_INFO']
args = pathargs.split('/')
participantid = args[2]
studyid = args[3]
repeatid = args[4]
proc = subprocess.Popen(['python3', 'cgi-bin/script-meas.py', participantid, studyid, repeatid], stdout=subprocess.PIPE)
line = proc.stdout.readline()
while line:
yield line
line = proc.stdout.readline()
wsgi_server = wsgiserver.CherryPyWSGIServer(('0.0.0.0', 8001), application)
wsgi_server.start()
A CGI server on port 8000 to host static files (of which there are many and complex folder structures)
wsgiserver.py
from http.server import CGIHTTPRequestHandler, HTTPServer
handler = CGIHTTPRequestHandler
handler.cgi_directories = ['/cgi-bin', '/htbin'] # this is the default
server = HTTPServer(('localhost', 8000), handler)
print('Serving on localhost:8000');
server.serve_forever()
Thank you for clarifying your needs.
In your use-case to have multiple processes using the same IP/port, you should put them behind a third process (apache/nginx/lighttpd/...).
Considering your question, I suppose you are in a developpment environment and it might be hard to setup and configure a webserver, so you could use a HTTP proxy (found SimpleHTTPProxy and ProxyHTTPServer and the related post "seriously simple python HTTP proxy?", but check the web for more); overall, this solution would not be easier...
The general idea is to configure the third program to listen to a port (ie: 80) and to serve the request to either of the backends depending of the requested path; ie:
When the process (say apache) listening to a port (let's say port 80) get a request to /meas it will redirect it to the wsgi server (either on a Unix socket or on the port 8081), if it is a request to /cgi-bin or /htbin it will redirect it to the CGI handler (actually Apache have a cgi-bin module and you could remove this backend).

How to get CherryPy to listen only on a specific host

I have a flask app that I want to deploy using CherryPy's built in server. I chose CherryPy so that the app can be deployed without having to reverse proxy (ie. nginx in front).
I'm having trouble getting CherryPy to listen for requests on just a single hostname.
Say I'm serving 2 sites: test1.com and test2.com (and have them set in my hosts file to point back to localhost).
My /etc/hosts file:
127.0.0.1 test1.com test2.com
CherryPy is serving test1.com, test2.com doesn't have anything serving it.
My cherrypy file is as follows:
import cherrypy
from my_test_flask_app import app
if __name__ == '__main__':
cherrypy.tree.graft(app, "/")
cherrypy.server.unsubscribe()
server = cherrypy._cpserver.Server()
server.socket_host = "test1.com"
server.socket_port = 8030
server.thread_pool = 30
server.subscribe()
cherrypy.engine.start()
cherrypy.engine.block()
Set up this way, I go to test1.com:8030 on my browser and it works as expected.
But when I go to test2.com:8030, the same app is served. I expected it not to serve anything, since CherryPy isn't set up to listen for test2.com.
To me, it seems that CherryPy is just listening for everything on the given port (8030), and treating the socket_host part as if its 0.0.0.0
Am I missing something here? I've looked through lots of docs and tutorials, but all things suggest that this code snippet should be working as I expected.
Thanks
Here's how you can setup what you want...
root = Root()
RootApp = cherrypy.Application(root)
Domain2App = cherrypy.Application(root)
SecureApp = cherrypy.Application(Secure())
vhost = cherrypy._cpwsgi.VirtualHost(RootApp,
domains={'www.domain2.example': Domain2App,
'www.domain2.example:443': SecureApp,
})
cherrypy.tree.graft(vhost)
https://cherrypy.readthedocs.org/en/3.3.0/refman/_cpwsgi.html#classes
Hope this helps!
You misunderstand the socket listen address - they are IP addresses only, not on DNS names. Set this way, CherryPy listens to the localhost (127.0.0.1) only - try using your Ethernet/Wlan local address and you should get connection refused.
Also, you can wrap your application with a WSGI middleware that checks the Host header for the proper domain, or use CherryPy virtual host facility to check the host header.

How do I build an WSGI apps built with CherryPyWSGIServer that support both HTTP and HTTPS?

I built a WSGI app and created a standalone wrapper using CherryPyWSGIServer. I see that CherryPyWSGIServer supports HTTPS but I am not sure how to support both HTTP and HTTPS together as it looks like the first server.start() blocks. How would I create two servers, one HTTP and one HTTPS, and start them both?
Here is what I have now:
server = CherryPyWSGIServer( (http_ip, http_port), web_app )
try:
server.start()
except KeyboardInterrupt:
server.stop()
Whenever I have done this in the past, I have used Apache or Nginx in front of the webserver, and let those handle the https.
Use a batch file to run your normal HTTP server on port 80 and a separate HTTPS instance on port 443.
Maybe this helps:
http://docs.cherrypy.org/stable/refman/process/servers.html#multiple-servers-ports

Categories