How to set up web.py under IIS 7 using CGI (with nice URLs)

I’ve been using python to create maintenance scripts for our servers and wanted to explore creating a simple web interface for running these scripts. It was pretty simple to create a python script to output some HTML and serve the output of the script as a webpage using IIS. However, using print statements to output HTML is pretty messy; I came across web.py, which is a simple web framework that seemed to do exactly what I wanted: allow me to using python for server side scripting but use templates to store the HTML.

I first attempted to install web.py on IIS6. I got to a point where I kept getting the error, “CGI Error The specified CGI application misbehaved by not returning a complete set of HTTP headers.” I then moved on to IIS7, this time I got a similar error but IIS7 returned more information, showing an exception that python was throwing with the call stack. I imagine this would work fine on IIS6 but troubleshooting problems along the way is more difficult. Below I will show  how to set up serve a web page from IIS7 first just using python and then using web.py (both use CGI) and finally we’ll set up IIS to allow us to use pretty URLs with web.py.

How to serve a webpage from IIS7 using python

  1. Install Python
  2. Create a file test.py with the following, c:\myapp\
    #!/usr/bin/env python
    # -*- coding: UTF-8 -*-
    
    # enable debugging
    
    print "Content-Type: text/plain;charset=utf-8"
    print
    print "Hello World!"
    
  3. Go to a command prompt and run test.py. It should output the code for the simple hello world page.
  4. Open IIS, right click on your website and click “Add Application”. Give your application the alias myapp and point the path to c:\myapp\.
  5. Click on your newly created application and double click “Handler Mappings”.  Click “Add Script Map”. Add *.py as the request path and the path to your python executable for executable, i.e. C:\Python27\Python.exe %s %s. Enter a name, like “python”. Click ok and then when it asks you to add and allow an entry to the ISAPI/CGI restriction list click yes. This tells IIS that if it receives any requests for files with the extension .py, it should execute python, pass the file as a parameter and then return the output. The script will be expected to output a valid webpage.
  6. Now you should be able to go to a web browser and navigate to localhost/myapp/test.py and see your hello world webpage.

Congratulations! You’ve created a website using Python! Now let’s create one using web.py.

How to serve a webpage from IIS7 using web.py

  1. Download web.py, extract it and then in a command prompt from that directory run “python setup.py install”.
  2. Create a new file in c:\myapp named test2.py with the following contents:
    
    import web
    
    urls = (
     '/myapp/test2\.py.*', 'index'
    )
    
    class index:
     def GET(self):
     return "Hello, world!"
    
    if __name__ == "__main__":
     app = web.application(urls, globals())
     app.run()
    

    The urls variable uses a regular expression to tell the script what to do depending on the url the browser requests. This says send anything that starts with with the text “my/app/test2.py” to the class “index”. The class index then says, on a GET request return “Hello, world!”

  3. Now go to a command prompt and run python c:\myapp\test2.py. You should see output of http://0.0.0.0:8080/. The script is now running a mini web server on port 8080. If you point your browser to http://localhost:8080/myapp/test2.py you should see hello world. If you navigate to http://localhost:8080/ you will see “not found”, as that URL does not match the URL pattern we specified. Close the command prompt to stop the script.
  4. We’re not done. If you try and use web.py through IIS now by navigating to localhost/myapp/test2.py you’ll get an error that ends with:
    File "C:\Python27\lib\site-packages\web\wsgi.py", line 16, in runfcgi import flup.server.fcgi as flups ImportError: No module named flup.server.fcgi "
    
  5. We need to install flup (flup may want you to first install python-setuptools, if so google it and install latest version). Download flup but don’t install yet. If you do install you’ll get the following error:
    File "build\bdist.win32\egg\flup\server\fcgi_base.py", line 978, in _setupSocket AttributeError: 'module' object has no attribute 'fromfd'
    
  6. This site had the solution, in the extracted flup directory find and edit the file fcgi_base.py. Comment out following lines: (make sure you comment out every line, including both lines of statements that span multiple lines)
    
    #isFCGI = True
    
    #sock = socket.fromfd(FCGI_LISTENSOCK_FILENO, socket.AF_INET,
     # socket.SOCK_STREAM)
     #try:
     #sock.getpeername()
     #except socket.error, e:
     # if e[0] == errno.ENOTSOCK:
     # Not a socket, assume CGI context.
     # isFCGI = False
     # elif e[0] != errno.ENOTCONN:
     # raise
    

    And right on top of the commented out lines add the line: isFCGI = False

  7. Now install flup by navigating to it’s directory in the command prompt and running  “python setup.py install”.
  8. Now you should be able to navigate to localhost/myapp/test2.py and see helloworld!

Congratulations! You can now use web.py under IIS. However, the URL structure is annoying. In order for our script to run we have to request /myapp/test2.py. We can set up patterns in our script to serve different pages for /myapp/test2.py/home and /myapp/test2.py/about but what if we want our site to respond to /myapp/home and /myapp/about? If we navigate to those pages in IIS now IIS will not even use python, let alone call our script test2.py. Currently IIS is sending anything that ends in .py to python, so when we request test2.py, it executes test2.py passing it the URL. The solution I came up with was to tell IIS to send all requests directly to test2.py, regardless of whether the URL specifies test2.py.

Nice URLs with IIS and web.py

For this example I’m just going to tell IIS to send anything that ends in .test directly to my web.py script. This could easily be modified to send all requests.

  1. In IIS, under navigate to myapp, “Handler Mappings”. Click “Add Script Map”. Under request path but *.test, under executable put C:\Python27\python.exe C:\Myapp\test2.py %s (the first part should be the path to your python executable). For name, put test. Click ok and then when it asks you to add and allow an entry to the ISAPI/CGI restriction list click yes. We’ve now told IIS to send any request ending in .test directly to test2.py.
  2. Modify c:\myapp\test2.py so it looks like this:
    import web
    
    urls = (
     '.*\.test', 'test',
     '/myapp/test2\.py.*', 'index'
    )
    
    class index:
     def GET(self):
     return "Hello, world!"
    
    class test:
     def GET(self):
     return "Hello, world! I'm loving my pretty URL!"
    
    if __name__ == "__main__":
     app = web.application(urls, globals())
     app.run()
    
    

    We’ve added two things to this file. The url will still send anything that starts with /myapp/test.py to index but now it will send anything that ends with .test to the class test. We’ve also added the class test, which will output a different string.

  3. Now you can navigate to localhost/app/home.test and you should see “Hello, world! I’m loving my pretty URL!”. You can now create url patterns and classes for /app/home.test and /app/about.test, or more likely tell IIS to send all requests to your page and remove the .test part.

In this example we’ve still outputted the page contents directly from a string in python but on the web.py website you can find very simple and easy to implement examples of using templates, which will allow you separate out the html into it’s own template page.