1 # Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
2 # This module is free software, and you may redistribute it and/or modify
3 # under the same terms as Python, so long as this copyright message and
4 # disclaimer are retained in their original form.
5 #
6 # IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
7 # DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
8 # OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
9 # POSSIBILITY OF SUCH DAMAGE.
10 #
11 # BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
12 # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
13 # FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
14 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
15 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
16 #
17 """ HTTP Server that serves roundup.
19 $Id: roundup_server.py,v 1.23 2003-04-24 07:19:02 richard Exp $
20 """
22 # python version check
23 from roundup import version_check
25 import sys, os, urllib, StringIO, traceback, cgi, binascii, getopt, imp
26 import BaseHTTPServer
28 # Roundup modules of use here
29 from roundup.cgi import cgitb, client
30 import roundup.instance
31 from roundup.i18n import _
33 #
34 ## Configuration
35 #
37 # This indicates where the Roundup trackers live. They're given as NAME ->
38 # TRACKER_HOME, where the NAME part is used in the URL to select the
39 # appropriate reacker.
40 # Make sure the NAME part doesn't include any url-unsafe characters like
41 # spaces, as these confuse the cookie handling in browsers like IE.
42 TRACKER_HOMES = {
43 'bar': '/tmp/bar',
44 }
46 ROUNDUP_USER = None
49 # Where to log debugging information to. Use an instance of DevNull if you
50 # don't want to log anywhere.
51 # TODO: actually use this stuff
52 #class DevNull:
53 # def write(self, info):
54 # pass
55 #LOG = open('/var/log/roundup.cgi.log', 'a')
56 #LOG = DevNull()
58 #
59 ## end configuration
60 #
62 import zlib, base64
63 favico = zlib.decompress(base64.decodestring('''
64 eJyVUk2IQVEUfn4yaRYjibdQZiVba/ZE2djIUmHWFjaKGVmIlY2iFMVG2ViQhXqFSP6iFFJvw4uF
65 LGdWd743mpeMn+a88917Oue7955z3qEoET6FQkHx8iahKDV2A8B7XgERRf/EKMSUzyf8ypbbnnQy
66 mWBdr9eVSkVw3tJGoxGNRpvNZigUyufzWPv9Pvwcx0UiERj7/V4g73Y7j8fTarWMRmO73U4kEkKI
67 YZhardbr9eLxuOD0+/2ZTMZisYjFYpqmU6kU799uN5tNMBg8HA7ZbPY8GaTh8/mEipRKpclk0ul0
68 NpvNarUmk0mWZS/yr9frcrmc+iMOh+NWydPp1Ov1SiSSc344HL7fKKfTiSN2u12tVqOcxWJxn6/V
69 ag0GAwxkrlKp5vP5fT7ulMlk6XRar9dLpVIUXi6Xb5Hxa1wul0ajKZVKsVjM7XYXCoVOp3OVPJvN
70 AoFAtVo1m825XO7hSODOYrH4kHbxxGAwwODBGI/H6DBs5LNara7yl8slGjIcDsHpdrunU6PRCAP2
71 r3fPdUcIYeyEfLSAJ0LeAUZHCAt8Al/8/kLIEWDB5YDj0wm8fAP6fVfo
72 '''.strip()))
74 class RoundupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
75 TRACKER_HOMES = TRACKER_HOMES
76 ROUNDUP_USER = ROUNDUP_USER
78 def run_cgi(self):
79 """ Execute the CGI command. Wrap an innner call in an error
80 handler so all errors can be caught.
81 """
82 save_stdin = sys.stdin
83 sys.stdin = self.rfile
84 try:
85 self.inner_run_cgi()
86 except client.NotFound:
87 self.send_error(404, self.path)
88 except client.Unauthorised:
89 self.send_error(403, self.path)
90 except:
91 # it'd be nice to be able to detect if these are going to have
92 # any effect...
93 self.send_response(400)
94 self.send_header('Content-Type', 'text/html')
95 self.end_headers()
96 try:
97 reload(cgitb)
98 self.wfile.write(cgitb.breaker())
99 self.wfile.write(cgitb.html())
100 except:
101 s = StringIO.StringIO()
102 traceback.print_exc(None, s)
103 self.wfile.write("<pre>")
104 self.wfile.write(cgi.escape(s.getvalue()))
105 self.wfile.write("</pre>\n")
106 sys.stdin = save_stdin
108 do_GET = do_POST = run_cgi
110 def index(self):
111 ''' Print up an index of the available trackers
112 '''
113 self.send_response(200)
114 self.send_header('Content-Type', 'text/html')
115 self.end_headers()
116 w = self.wfile.write
117 w(_('<html><head><title>Roundup trackers index</title></head>\n'))
118 w(_('<body><h1>Roundup trackers index</h1><ol>\n'))
119 keys = self.TRACKER_HOMES.keys()
120 keys.sort()
121 for tracker in keys:
122 w(_('<li><a href="%(tracker_url)s/index">%(tracker_name)s</a>\n')%{
123 'tracker_url': urllib.quote(tracker),
124 'tracker_name': cgi.escape(tracker)})
125 w(_('</ol></body></html>'))
127 def inner_run_cgi(self):
128 ''' This is the inner part of the CGI handling
129 '''
130 rest = self.path
132 if rest == '/favicon.ico':
133 raise client.NotFound
134 # self.send_response(200)
135 # self.send_header('Content-Type', 'image/x-ico')
136 # self.end_headers()
137 # self.wfile.write(favicon)
138 # return
140 i = rest.rfind('?')
141 if i >= 0:
142 rest, query = rest[:i], rest[i+1:]
143 else:
144 query = ''
146 # no tracker - spit out the index
147 if rest == '/':
148 return self.index()
150 # figure the tracker
151 l_path = rest.split('/')
152 tracker_name = urllib.unquote(l_path[1])
154 # handle missing trailing '/'
155 if len(l_path) == 2:
156 self.send_response(301)
157 # redirect - XXX https??
158 protocol = 'http'
159 url = '%s://%s%s/'%(protocol, self.headers['host'], self.path)
160 self.send_header('Location', url)
161 self.end_headers()
162 self.wfile.write('Moved Permanently')
163 return
165 if self.TRACKER_HOMES.has_key(tracker_name):
166 tracker_home = self.TRACKER_HOMES[tracker_name]
167 tracker = roundup.instance.open(tracker_home)
168 else:
169 raise client.NotFound
171 # figure out what the rest of the path is
172 if len(l_path) > 2:
173 rest = '/'.join(l_path[2:])
174 else:
175 rest = '/'
177 # Set up the CGI environment
178 env = {}
179 env['TRACKER_NAME'] = tracker_name
180 env['REQUEST_METHOD'] = self.command
181 env['PATH_INFO'] = urllib.unquote(rest)
182 if query:
183 env['QUERY_STRING'] = query
184 host = self.address_string()
185 if self.headers.typeheader is None:
186 env['CONTENT_TYPE'] = self.headers.type
187 else:
188 env['CONTENT_TYPE'] = self.headers.typeheader
189 length = self.headers.getheader('content-length')
190 if length:
191 env['CONTENT_LENGTH'] = length
192 co = filter(None, self.headers.getheaders('cookie'))
193 if co:
194 env['HTTP_COOKIE'] = ', '.join(co)
195 env['HTTP_AUTHORIZATION'] = self.headers.getheader('authorization')
196 env['SCRIPT_NAME'] = ''
197 env['SERVER_NAME'] = self.server.server_name
198 env['SERVER_PORT'] = str(self.server.server_port)
199 env['HTTP_HOST'] = self.headers['host']
201 decoded_query = query.replace('+', ' ')
203 # do the roundup thang
204 c = tracker.Client(tracker, self, env)
205 c.main()
207 def usage(message=''):
208 if message:
209 message = _('Error: %(error)s\n\n')%{'error': message}
210 print _('''%(message)sUsage:
211 roundup-server [-n hostname] [-p port] [-l file] [-d file] [name=tracker home]*
213 -n: sets the host name
214 -p: sets the port to listen on
215 -l: sets a filename to log to (instead of stdout)
216 -d: daemonize, and write the server's PID to the nominated file
218 name=tracker home
219 Sets the tracker home(s) to use. The name is how the tracker is
220 identified in the URL (it's the first part of the URL path). The
221 tracker home is the directory that was identified when you did
222 "roundup-admin init". You may specify any number of these name=home
223 pairs on the command-line. For convenience, you may edit the
224 TRACKER_HOMES variable in the roundup-server file instead.
225 Make sure the name part doesn't include any url-unsafe characters like
226 spaces, as these confuse the cookie handling in browsers like IE.
227 ''')%locals()
228 sys.exit(0)
230 def daemonize(pidfile):
231 ''' Turn this process into a daemon.
232 - make sure the sys.std(in|out|err) are completely cut off
233 - make our parent PID 1
235 Write our new PID to the pidfile.
237 From A.M. Kuuchling (possibly originally Greg Ward) with
238 modification from Oren Tirosh, and finally a small mod from me.
239 '''
240 # Fork once
241 if os.fork() != 0:
242 os._exit(0)
244 # Create new session
245 os.setsid()
247 # Second fork to force PPID=1
248 pid = os.fork()
249 if pid:
250 pidfile = open(pidfile, 'w')
251 pidfile.write(str(pid))
252 pidfile.close()
253 os._exit(0)
255 os.chdir("/")
256 os.umask(0)
258 # close off sys.std(in|out|err), redirect to devnull so the file
259 # descriptors can't be used again
260 devnull = os.open('/dev/null', 0)
261 os.dup2(devnull, 0)
262 os.dup2(devnull, 1)
263 os.dup2(devnull, 2)
265 def abspath(path):
266 ''' Make the given path an absolute path.
268 Code from Zope-Coders posting of 2002-10-06 by GvR.
269 '''
270 if not os.path.isabs(path):
271 path = os.path.join(os.getcwd(), path)
272 return os.path.normpath(path)
274 def run():
275 ''' Script entry point - handle args and figure out what to to.
276 '''
277 # time out after a minute if we can
278 import socket
279 if hasattr(socket, 'setdefaulttimeout'):
280 socket.setdefaulttimeout(60)
282 hostname = ''
283 port = 8080
284 pidfile = None
285 logfile = None
286 try:
287 # handle the command-line args
288 try:
289 optlist, args = getopt.getopt(sys.argv[1:], 'n:p:u:d:l:h')
290 except getopt.GetoptError, e:
291 usage(str(e))
293 user = ROUNDUP_USER
294 for (opt, arg) in optlist:
295 if opt == '-n': hostname = arg
296 elif opt == '-p': port = int(arg)
297 elif opt == '-u': user = arg
298 elif opt == '-d': pidfile = abspath(arg)
299 elif opt == '-l': logfile = abspath(arg)
300 elif opt == '-h': usage()
302 if hasattr(os, 'getuid'):
303 # if root, setuid to the running user
304 if not os.getuid() and user is not None:
305 try:
306 import pwd
307 except ImportError:
308 raise ValueError, _("Can't change users - no pwd module")
309 try:
310 uid = pwd.getpwnam(user)[2]
311 except KeyError:
312 raise ValueError, _("User %(user)s doesn't exist")%locals()
313 os.setuid(uid)
314 elif os.getuid() and user is not None:
315 print _('WARNING: ignoring "-u" argument, not root')
317 # People can remove this check if they're really determined
318 if not os.getuid() and user is None:
319 raise ValueError, _("Can't run as root!")
321 # handle tracker specs
322 if args:
323 d = {}
324 for arg in args:
325 try:
326 name, home = arg.split('=')
327 except ValueError:
328 raise ValueError, _("Instances must be name=home")
329 d[name] = home
330 RoundupRequestHandler.TRACKER_HOMES = d
331 except SystemExit:
332 raise
333 except:
334 exc_type, exc_value = sys.exc_info()[:2]
335 usage('%s: %s'%(exc_type, exc_value))
337 # we don't want the cgi module interpreting the command-line args ;)
338 sys.argv = sys.argv[:1]
339 address = (hostname, port)
341 # fork?
342 if pidfile:
343 if not hasattr(os, 'fork'):
344 print "Sorry, you can't run the server as a daemon on this" \
345 'Operating System'
346 sys.exit(0)
347 daemonize(pidfile)
349 # redirect stdout/stderr to our logfile
350 if logfile:
351 # appending, unbuffered
352 sys.stdout = sys.stderr = open(logfile, 'a', 0)
354 httpd = BaseHTTPServer.HTTPServer(address, RoundupRequestHandler)
355 print _('Roundup server started on %(address)s')%locals()
356 try:
357 httpd.serve_forever()
358 except KeyboardInterrupt:
359 print 'Keyboard Interrupt: exiting'
361 if __name__ == '__main__':
362 run()
364 # vim: set filetype=python ts=4 sw=4 et si