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:

Windows

 

Linux:

Linux

OS X:OSX

And because it’s possible, Android:

 

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).