The Summer Server Project – Part 3: It lives…and serves.
….it lives.
After some time hacking away at the code and ultimately learning a few things about Python, I’ve got a basic workable server up and running. Here are some of the basic features:
- It serves basic static content (HTML, JavaScript and CSS). The bulk of this is done with the help of Python’s built in support for basic web servers.
- If an index1 page isn’t found, a default index is generated and served.
- There is a small config file that gives the user some flexibility with what the server does. This includes the port that the server runs on, how much logging is done, how the IP address is collected (this was done to account for some odd cross-platform differences in how the IP is reported which still hasn’t been entirely fixed yet), the document root (where the server looks for files to host) and the location of custom default “it works” page (page to load if the server works but no files are being served). This last configuration option is no longer used for some reasons detailed below.
Before I continue however, pasted below is the code for the server itself thus far.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 |
#!/usr/bin/python import BaseHTTPServer import ConfigParser import logging import os import signal import SimpleHTTPServer import SocketServer import socket import string import sys PORT = 8888 SSP_VERSION = "0.1" DOCROOT = "." ITWORKS = "html/index.html" LOGFILE = "ssp.log" IP = "0.0.0.0" # This is the default page for "it works!" (ie. the server is successfully loading content). &version& is replaced with the current version of ssp running. WORKSPAGE = """<!DOCTYPE html5> <html> <head> <style> body { background-color: #eee; font-family: 'Verdana', Geneva, sans-serif; margin: 10%; } .subtext { color: #B0B0B0; padding-left: 5em; } </style> </head> <body> <h3>It works!</h3> <p></p> <span class='subtext'>SSP version: &version&</span> <p></p> <span class='subtext'>To get started, place an index file (index.html) into your docroot.</span> </body> </html> """ # http://stackoverflow.com/a/25375077 class SSPHTTPHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): # Set the server version SimpleHTTPServer.SimpleHTTPRequestHandler.server_version = SSP_VERSION # ConfigParser for the Handler class config = ConfigParser.RawConfigParser(allow_no_value=True) def log_message(self, format, *args): """ Format the log messages generated by requests to the server. """ DETAILED = self.config.get("setup", "detailed") # Check to see if the user wants detailed logging. if DETAILED == "True": # Print log messages based on the response code. Each time a request is sent to the server, it responds with a three digit code. print("[%s]: %s ==> %s" % (self.log_date_time_string(), self.client_address[0], format%args)) else: # The codes are stored in the second argument of the args array that includes response log messages. code = args[1] # 200 is OK - this is what we're looking for. if code == "200": code = "OK (200)" print("[%s]: %s" % (self.log_date_time_string(), code)) # https://wiki.python.org/moin/BaseHttpServer def do_HEAD(self): # Send a 200 (OK) - request succeeded. self.send_response(200) # Set the content to html. self.send_header("Content-type", "text/html") # End the headers. self.end_headers() # https://wiki.python.org/moin/BaseHttpServer def do_GET(self): # self.send_response(200) # self.send_header("Content-type", "text/html") # self.end_headers() # Load the configuration file. self.config.read("ssp.config") # "It Works" page. itworks = self.config.get("content", "itworks") # Create the headers. self.do_HEAD() # Log that headers were sent logging.info("headers") # Check to see if an index.html or index.htm already exists. If it doesn't, use one set by the user in the config file. if os.path.isfile("index.html") == False: # This loads the default index file that the user configures. #f = open(itworks, "r") #self.wfile.write(f.read()) #f.close() default_page = WORKSPAGE.replace("&version&", SSP_VERSION) self.wfile.write(default_page) # If there is an index.html available, use that. elif os.path.isfile("index.html") == True: f = open("index.html", "r") self.wfile.write(f.read()) f.close() class sspserver(): def __init__(self): """ Constructor for main server class. """ # Setup the configuration parser. self.config = ConfigParser.RawConfigParser(allow_no_value=True) # Load the configuration file. self.config.read("ssp.config") # Set the log file. LOGFILE = self.config.get("setup", "logfile") # Setup the logger. # http://stackoverflow.com/q/11581794 - formatting help self.logger = logging.basicConfig(filename=LOGFILE, level=logging.DEBUG, format="[%(asctime)s]: %(levelname)s: %(message)s") # Log that things got started logging.info("ssp started.") # Set the port based on the config file. PORT = int(self.config.get("setup", "port")) # Set the version based on the config file. # SSP_VERSION = "ssp/" + self.config.get("setup", "ssp_version") # Set the docroot based on the config file. DOCROOT = self.config.get("content", "docroot") # Set the location of the "It Works" page (the default index.html page). # This is now written into the server itself for the purposes of simplfying things. # ITWORKS = self.config.get("content", "itworks") # Change the working directory to the one specified for the docroot. This ensures that we are serving content out of the docroot directory. os.chdir(DOCROOT) usehost = self.config.get("setup", "usehostname") # Thank to http://stackoverflow.com/questions/166506/finding-local-ip-addresses-using-pythons-stdlib for the IP tips. if usehost == False: IP = socket.gethostbyname(socket.getfqdn()) else: IP = socket.gethostbyname(socket.gethostname()) try: # Set up the http handler. This does the "grunt" work. The more fine grained details are handled in the SSPHTTPHandler class. Handler = SSPHTTPHandler # This creates a tcp server using the Handler. As I understand it, this creates a standard TCP server and then handles connections to it using the SSPHTTPHandler class. httpd = SocketServer.TCPServer(("", PORT), Handler) # Print the version of ssp. print("=> ssp/" + SSP_VERSION) # Print the port that the server will pipe content through. print(" ==> Serving on port " + str(PORT)) # Print the IP address of the server. print(" ==> Serving on IP " + str(IP)) # If the document root config option is set to ., serve content out of the current working directory. if DOCROOT == ".": print(" ==> Serving out of " + os.getcwd()) else: print(" ==> Serving out of " + DOCROOT) print("\nLog:") # Serve content "forever" until a KeyBoardInterrupt is issued (Control-C). httpd.serve_forever() except KeyboardInterrupt: # If Control-C is pressed, kill the server. sys.exit(0) if __name__ == "__main__": s = sspserver() |
The Cross-Platform Experience
As you may remember, one of my criteria for this server was that it run across platforms. Initially, I had imagined that this would includes Windows, OS X and Linux. Well, here it is running on Windows:
Linux:
And because it’s possible, Android:
However simple the server is designed right now, it works on four separate platforms with absolutely no modifications. Getting it to this point wasn’t overly difficult as, by its very nature, Python is largely platform agnostic so setting this up really only required me to stick with Python’s standard library (stuff that comes with Python).
Difficulties
The coding has been fairly kind to me. Thus far, not much has prevented me from progressing in any serious manner. One thing that has yet to be implemented is a simpler logging function which will require knowledge of HTTP codes (I’ve only got code 200 accounted for right now which is basically the code for “everything looks good”). Another thing that requires some work is the reporting back to the user the IP address. As you can see in the Android screenshot above, despite running on a device with an IP allocated by my router, it still reports back that the device’s IP is 127.0.0.1 which, although not technically incorrect (I could browser to that on the Android device and it would work), is inaccessible beyond the tablet. Right now, I’ve got the server trying to get the IP address two different ways: through the hostname or the FQDN (fully qualified domain name) so it is possible that a simple configuration change may solve that. However, I hope to develop some sort of system where the server recognizes that one won’t work and then uses the other automatically.
I was also having some difficulties with using the default “it works” page when I was simply opening it from a separate document and serving that. I’m not sure why that is so I’ve hard coded the HTML response into the server which I don’t really like. I’ll, at some point, come up with a much more elegant solution.
What’s Next
There are two things planned now. First, I plan to start testing this server. Given that I’ve got an extra Android tablet laying around that can run ssp, I may use that as my web server to test how well it handles connections and logs requests. The second thing to do is improve the logging. Right now, it basically dumps information into a log file and I’d like to clean that up.
Notes:
1 Web servers often look for a file called index.<extension> as the first page that is opened. In other words, when you navigate to www.mac-forums.com, your browser is opening www.mac-forums.com/index.php (I believe it’s a PHP file).