1import sys 2import os 3import time 4import codecs 5import socket 6from scgi import passfd 7 8 9def log(msg): 10 timestamp = time.strftime('%Y-%m-%d %H:%M:%S') 11 sys.stderr.write('[%s] %s\n' % (timestamp, msg)) 12 13 14# netstring utility functions 15def ns_read_size(input): 16 size = b'' 17 while 1: 18 c = input.read(1) 19 if c == b':': 20 break 21 elif not c: 22 raise IOError('short netstring read') 23 size = size + c 24 return int(size) 25 26 27def ns_reads(input): 28 size = ns_read_size(input) 29 data = b'' 30 while size > 0: 31 s = input.read(size) 32 if not s: 33 raise IOError('short netstring read') 34 data = data + s 35 size -= len(s) 36 if input.read(1) != b',': 37 raise IOError('missing netstring terminator') 38 return data 39 40 41# string encoding for evironmental variables 42HEADER_ENCODING = 'iso-8859-1' 43 44 45def parse_env(headers): 46 items = headers.split(b'\0') 47 items = items[:-1] 48 if len(items) % 2 != 0: 49 raise ValueError('malformed headers') 50 env = {} 51 for i in range(0, len(items), 2): 52 k = items[i].decode(HEADER_ENCODING) 53 v = items[i+1].decode(HEADER_ENCODING) 54 env[k] = v 55 return env 56 57 58def read_env(input): 59 headers = ns_reads(input) 60 return parse_env(headers) 61 62 63class SCGIHandler: 64 65 # Subclasses should normally override the produce method. 66 67 def __init__(self, parent_fd): 68 self.parent_fd = parent_fd 69 70 def serve(self): 71 while 1: 72 try: 73 os.write(self.parent_fd, b'1') # indicates that child is ready 74 fd = passfd.recvfd(self.parent_fd) 75 except (IOError, OSError): 76 # parent probably exited (EPIPE comes thru as OSError) 77 raise SystemExit 78 conn = socket.fromfd(fd, socket.AF_INET, socket.SOCK_STREAM) 79 # Make sure the socket is blocking. Apparently, on FreeBSD the 80 # socket is non-blocking. I think that's an OS bug but I don't 81 # have the resources to track it down. 82 conn.setblocking(1) 83 os.close(fd) 84 try: 85 self.handle_connection(conn) 86 finally: 87 try: 88 conn.shutdown(socket.SHUT_RDWR) 89 except OSError: 90 pass 91 conn.close() 92 93 def read_env(self, input): 94 return read_env(input) 95 96 def handle_connection(self, conn): 97 """Handle an incoming request. This used to be the function to 98 override in your own handler class, and doing so will still work. 99 It will be easier (and therefore probably safer) to override 100 produce() or produce_cgilike() instead. 101 """ 102 input = conn.makefile("rb") 103 output = conn.makefile("wb") 104 env = self.read_env(input) 105 bodysize = int(env.get('CONTENT_LENGTH', 0)) 106 try: 107 self.produce(env, bodysize, input, output) 108 finally: 109 output.close() 110 input.close() 111 112 def produce(self, env, bodysize, input, output): 113 """This is the function you normally override to run your 114 application. It is called once for every incoming request that 115 this process is expected to handle. 116 117 Parameters: 118 119 env - a dict mapping CGI parameter names to their values. 120 121 bodysize - an integer giving the length of the request body, in 122 bytes (or zero if there is none). 123 124 input - a file allowing you to read the request body, if any, 125 over a socket. The body is exactly bodysize bytes long; don't 126 try to read more than bodysize bytes. This parameter is taken 127 from the CONTENT_LENGTH CGI parameter. 128 129 output - a file allowing you to write your page over a socket 130 back to the client. Before writing the page's contents, you 131 must write an http header, e.g. "Content-Type: text/plain\\r\\n" 132 133 The default implementation of this function sets up a CGI-like 134 environment, calls produce_cgilike(), and then restores the 135 original environment for the next request. It is probably 136 faster and cleaner to override produce(), but produce_cgilike() 137 may be more convenient. 138 """ 139 140 # Preserve current system environment 141 stdin = sys.stdin 142 stdout = sys.stdout 143 environ = os.environ 144 145 # Set up CGI-like environment for produce_cgilike() 146 sys.stdin = codecs.getreader('utf-8')(input) 147 sys.stdout = codecs.getwriter('utf-8')(output) 148 os.environ = env 149 150 # Call CGI-like version of produce() function 151 try: 152 self.produce_cgilike(env, bodysize) 153 finally: 154 # Restore original environment no matter what happens 155 sys.stdin.close() 156 sys.stdout.close() 157 sys.stdin = stdin 158 sys.stdout = stdout 159 os.environ = environ 160 161 162 def produce_cgilike(self, env, bodysize): 163 """A CGI-like version of produce. Override this function instead 164 of produce() if you want a CGI-like environment: CGI parameters 165 are added to your environment variables, the request body can be 166 read on standard input, and the resulting page is written to 167 standard output. 168 169 The CGI parameters are also passed as env, and the size of the 170 request body in bytes is passed as bodysize (or zero if there is 171 no body). 172 173 Default implementation is to produce a text page listing the 174 request's CGI parameters, which can be useful for debugging. 175 """ 176 print("Content-Type: text/plain; charset=utf-8") 177 print() 178 for k, v in env.items(): 179 print("%s: %r" % (k, v)) 180 181 182