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