Thursday, October 14, 2010

Python webservice that executes local commands

There are a few different options when it comes to managing server-side scripting on a web site. Usually folks use php or perl and even python in many occasions.

This blog post is about using python to execute code locally on the server in response to http GET requests.

So far you are thinking so what? You are already crafting your comment and it is saying something like, "Google mod_python" or "Google mod_perl". You are right, the best way to do CGI is via mod_perl, mod_php or mod_perl. The problem is user access and chroot.

Apache will execute server side scripts as the user / group defined in the main httpd.conf. In my case: apache / apache.
Apache will also assume a document root of /var/www/ for scripts (on a Centos 5.5 box) even if the userdir module is in use.

My problem was: How to get apache to execute scripts as dave:dave on doc root = /home/dave/. It was critical to get this working because the scripts in question interact with the .gnupg/pubkeyring and .gnupg/seckeyring files under /home/dave/.gnupg/.

Basically, I was making some kind of web based PGP key server. A web based gui for remote users to manage keys.

In the end I settled for python and the BaseHTTPServer.

First of all a simple class that will accept a shell command, execute it and return the stdout.


import popen2

class MyShellCommand:

"""execute a command, capture stdout and return it."""
def __callShellCmd(self, cmd):

stdout, stdin = popen2.popen2(command)
data = ""
while True:
c = stdout.read(1)
if( c ):
data += c
else:
break
return data

"""concrete example"""
def getPublicGPGKey(self, keyid):

"""TODO: Add logic to validate key id..."""
command = "gpg -a --export '%s'" % keyid
return self.__callShellCmd(command)


Now that we have a utility to retrieve public keys from the gpg keyring, lets call it from a webservice that is owned and operated by user:group, dave:dave.


import MyShellCommand
import time
import BaseHTTPServer, cgi

"""Configure host ip and port to listen on. Use high port for non root users."""
HOST_NAME = '127.0.0.1'
PORT_NUMBER = 8080

class MyHandler(BaseHTTPServer.BaseHTTPRequestHandler):

def do_HEAD(s):
s.send_response(200)
s.send_header("Content-type", "text/html")
s.end_headers()

def do_GET(s):
s.send_response(200)
s.send_header("Content-type", "text/html")
s.end_headers()

"""Get the path and find the parameters"""
path, query_string = s.path.split('?', 1)
params = dict(cgi.parse_qsl(query_string))

"""create a shell object"""
shell = MyShellCommand()

"""Validate the call being made"""
if path == '/publickey':
"""Validate the parameters"""
if params.has_key('id'):
s.wfile.write('%s' % shell.getPublicGPGKey(params['id']))

"""I dont like descriptive errors."""
else: s.wfile.write('An error occurred.')
else: s.wfile.write('An error occurred.')



if __name__ == '__main__':
server_class = BaseHTTPServer.HTTPServer
httpd = server_class((HOST_NAME, PORT_NUMBER), MyHandler)
print time.asctime(), "Server Starts - %s:%s" % (HOST_NAME, PORT_NUMBER)
try:
httpd.serve_forever()
except KeyboardInterrupt:
pass
httpd.server_close()
print time.asctime(), "Server Stops - %s:%s" % (HOST_NAME, PORT_NUMBER)


If you execute the above script, you will have a working webservice that responds nicely to only one specific set of GET data. Call it with a URL like this:


http://127.0.0.1:8080/publickey?id=mykeyname
Post a Comment