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.34 2003-11-12 23:29:17 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, socket, errno
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 # 'example': '/path/to/example',
44 }
46 ROUNDUP_USER = None
49 #
50 ## end configuration
51 #
53 import zlib, base64
54 favico = zlib.decompress(base64.decodestring('''
55 eJyVUk2IQVEUfn4yaRYjibdQZiVba/ZE2djIUmHWFjaKGVmIlY2iFMVG2ViQhXqFSP6iFFJvw4uF
56 LGdWd743mpeMn+a88917Oue7955z3qEoET6FQkHx8iahKDV2A8B7XgERRf/EKMSUzyf8ypbbnnQy
57 mWBdr9eVSkVw3tJGoxGNRpvNZigUyufzWPv9Pvwcx0UiERj7/V4g73Y7j8fTarWMRmO73U4kEkKI
58 YZhardbr9eLxuOD0+/2ZTMZisYjFYpqmU6kU799uN5tNMBg8HA7ZbPY8GaTh8/mEipRKpclk0ul0
59 NpvNarUmk0mWZS/yr9frcrmc+iMOh+NWydPp1Ov1SiSSc344HL7fKKfTiSN2u12tVqOcxWJxn6/V
60 ag0GAwxkrlKp5vP5fT7ulMlk6XRar9dLpVIUXi6Xb5Hxa1wul0ajKZVKsVjM7XYXCoVOp3OVPJvN
61 AoFAtVo1m825XO7hSODOYrH4kHbxxGAwwODBGI/H6DBs5LNara7yl8slGjIcDsHpdrunU6PRCAP2
62 r3fPdUcIYeyEfLSAJ0LeAUZHCAt8Al/8/kLIEWDB5YDj0wm8fAP6fVfo
63 '''.strip()))
65 class RoundupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
66 TRACKER_HOMES = TRACKER_HOMES
67 ROUNDUP_USER = ROUNDUP_USER
69 def run_cgi(self):
70 """ Execute the CGI command. Wrap an innner call in an error
71 handler so all errors can be caught.
72 """
73 save_stdin = sys.stdin
74 sys.stdin = self.rfile
75 try:
76 self.inner_run_cgi()
77 except client.NotFound:
78 self.send_error(404, self.path)
79 except client.Unauthorised:
80 self.send_error(403, self.path)
81 except socket.timeout:
82 s = StringIO.StringIO()
83 traceback.print_exc(None, s)
84 self.log_message('%s', s.getvalue())
85 except:
86 # it'd be nice to be able to detect if these are going to have
87 # any effect...
88 self.send_response(400)
89 self.send_header('Content-Type', 'text/html')
90 self.end_headers()
91 try:
92 reload(cgitb)
93 self.wfile.write(cgitb.breaker())
94 self.wfile.write(cgitb.html())
95 except:
96 s = StringIO.StringIO()
97 traceback.print_exc(None, s)
98 self.wfile.write("<pre>")
99 self.wfile.write(cgi.escape(s.getvalue()))
100 self.wfile.write("</pre>\n")
101 sys.stdin = save_stdin
103 do_GET = do_POST = run_cgi
105 def index(self):
106 ''' Print up an index of the available trackers
107 '''
108 self.send_response(200)
109 self.send_header('Content-Type', 'text/html')
110 self.end_headers()
111 w = self.wfile.write
112 w(_('<html><head><title>Roundup trackers index</title></head>\n'))
113 w(_('<body><h1>Roundup trackers index</h1><ol>\n'))
114 keys = self.TRACKER_HOMES.keys()
115 keys.sort()
116 for tracker in keys:
117 w(_('<li><a href="%(tracker_url)s/index">%(tracker_name)s</a>\n')%{
118 'tracker_url': urllib.quote(tracker),
119 'tracker_name': cgi.escape(tracker)})
120 w(_('</ol></body></html>'))
122 def inner_run_cgi(self):
123 ''' This is the inner part of the CGI handling
124 '''
125 rest = self.path
127 if rest == '/favicon.ico':
128 raise client.NotFound
130 i = rest.rfind('?')
131 if i >= 0:
132 rest, query = rest[:i], rest[i+1:]
133 else:
134 query = ''
136 # no tracker - spit out the index
137 if rest == '/':
138 return self.index()
140 # figure the tracker
141 l_path = rest.split('/')
142 tracker_name = urllib.unquote(l_path[1])
144 # handle missing trailing '/'
145 if len(l_path) == 2:
146 self.send_response(301)
147 # redirect - XXX https??
148 protocol = 'http'
149 url = '%s://%s%s/'%(protocol, self.headers['host'], self.path)
150 self.send_header('Location', url)
151 self.end_headers()
152 self.wfile.write('Moved Permanently')
153 return
155 if self.TRACKER_HOMES.has_key(tracker_name):
156 tracker_home = self.TRACKER_HOMES[tracker_name]
157 tracker = roundup.instance.open(tracker_home)
158 else:
159 raise client.NotFound
161 # figure out what the rest of the path is
162 if len(l_path) > 2:
163 rest = '/'.join(l_path[2:])
164 else:
165 rest = '/'
167 # Set up the CGI environment
168 env = {}
169 env['TRACKER_NAME'] = tracker_name
170 env['REQUEST_METHOD'] = self.command
171 env['PATH_INFO'] = urllib.unquote(rest)
172 if query:
173 env['QUERY_STRING'] = query
174 host = self.address_string()
175 if self.headers.typeheader is None:
176 env['CONTENT_TYPE'] = self.headers.type
177 else:
178 env['CONTENT_TYPE'] = self.headers.typeheader
179 length = self.headers.getheader('content-length')
180 if length:
181 env['CONTENT_LENGTH'] = length
182 co = filter(None, self.headers.getheaders('cookie'))
183 if co:
184 env['HTTP_COOKIE'] = ', '.join(co)
185 env['HTTP_AUTHORIZATION'] = self.headers.getheader('authorization')
186 env['SCRIPT_NAME'] = ''
187 env['SERVER_NAME'] = self.server.server_name
188 env['SERVER_PORT'] = str(self.server.server_port)
189 env['HTTP_HOST'] = self.headers['host']
191 decoded_query = query.replace('+', ' ')
193 # do the roundup thang
194 c = tracker.Client(tracker, self, env)
195 c.main()
197 LOG_IPADDRESS = 1
198 def address_string(self):
199 if self.LOG_IPADDRESS:
200 return self.client_address[0]
201 else:
202 host, port = self.client_address
203 return socket.getfqdn(host)
205 def error():
206 exc_type, exc_value = sys.exc_info()[:2]
207 return _('Error: %s: %s' % (exc_type, exc_value))
209 def usage(message=''):
210 print _('''%(message)s
212 Usage:
213 roundup-server [options] [name=tracker home]*
215 options:
216 -n: sets the host name
217 -p: sets the port to listen on
218 -u: sets the uid to this user after listening on the port
219 -g: sets the gid to this group after listening on the port
220 -l: sets a filename to log to (instead of stdout)
221 -d: sets a filename to write server PID to. This option causes the server
222 to run in the background. Note: on Windows the PID argument is needed,
223 but ignored. The -l option *must* be specified if this option is.
224 -N: log client machine names in access log instead of IP addresses (much
225 slower)
227 name=tracker home:
228 Sets the tracker home(s) to use. The name is how the tracker is
229 identified in the URL (it's the first part of the URL path). The
230 tracker home is the directory that was identified when you did
231 "roundup-admin init". You may specify any number of these name=home
232 pairs on the command-line. For convenience, you may edit the
233 TRACKER_HOMES variable in the roundup-server file instead.
234 Make sure the name part doesn't include any url-unsafe characters like
235 spaces, as these confuse the cookie handling in browsers like IE.
236 ''')%locals()
237 sys.exit(0)
239 def daemonize(pidfile):
240 ''' Turn this process into a daemon.
241 - make sure the sys.std(in|out|err) are completely cut off
242 - make our parent PID 1
244 Write our new PID to the pidfile.
246 From A.M. Kuuchling (possibly originally Greg Ward) with
247 modification from Oren Tirosh, and finally a small mod from me.
248 '''
249 # Fork once
250 if os.fork() != 0:
251 os._exit(0)
253 # Create new session
254 os.setsid()
256 # Second fork to force PPID=1
257 pid = os.fork()
258 if pid:
259 pidfile = open(pidfile, 'w')
260 pidfile.write(str(pid))
261 pidfile.close()
262 os._exit(0)
264 os.chdir("/")
265 os.umask(0)
267 # close off sys.std(in|out|err), redirect to devnull so the file
268 # descriptors can't be used again
269 devnull = os.open('/dev/null', 0)
270 os.dup2(devnull, 0)
271 os.dup2(devnull, 1)
272 os.dup2(devnull, 2)
274 def run(port=8080, success_message=None):
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 pidfile = None
284 logfile = None
285 try:
286 # handle the command-line args
287 try:
288 optlist, args = getopt.getopt(sys.argv[1:], 'n:p:u:d:l:hN')
289 except getopt.GetoptError, e:
290 usage(str(e))
292 user = ROUNDUP_USER
293 group = None
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 == '-g': group = arg
299 elif opt == '-d': pidfile = os.path.abspath(arg)
300 elif opt == '-l': logfile = os.path.abspath(arg)
301 elif opt == '-h': usage()
302 elif opt == '-N': RoundupRequestHandler.LOG_IPADDRESS = 0
304 if pidfile and not logfile:
305 raise ValueError, _("logfile *must* be specified if pidfile is")
307 # obtain server before changing user id - allows to use port <
308 # 1024 if started as root
309 address = (hostname, port)
310 try:
311 httpd = BaseHTTPServer.HTTPServer(address, RoundupRequestHandler)
312 except socket.error, e:
313 if e[0] == errno.EADDRINUSE:
314 raise socket.error, \
315 _("Unable to bind to port %s, port already in use." % port)
316 raise
318 if group is not None and hasattr(os, 'getgid'):
319 # if root, setgid to the running user
320 if not os.getgid() and user is not None:
321 try:
322 import pwd
323 except ImportError:
324 raise ValueError, _("Can't change groups - no pwd module")
325 try:
326 gid = pwd.getpwnam(user)[3]
327 except KeyError:
328 raise ValueError,_("Group %(group)s doesn't exist")%locals()
329 os.setgid(gid)
330 elif os.getgid() and user is not None:
331 print _('WARNING: ignoring "-g" argument, not root')
333 if hasattr(os, 'getuid'):
334 # if root, setuid to the running user
335 if not os.getuid() and user is not None:
336 try:
337 import pwd
338 except ImportError:
339 raise ValueError, _("Can't change users - no pwd module")
340 try:
341 uid = pwd.getpwnam(user)[2]
342 except KeyError:
343 raise ValueError, _("User %(user)s doesn't exist")%locals()
344 os.setuid(uid)
345 elif os.getuid() and user is not None:
346 print _('WARNING: ignoring "-u" argument, not root')
348 # People can remove this check if they're really determined
349 if not os.getuid() and user is None:
350 raise ValueError, _("Can't run as root!")
352 # handle tracker specs
353 if args:
354 d = {}
355 for arg in args:
356 try:
357 name, home = arg.split('=')
358 except ValueError:
359 raise ValueError, _("Instances must be name=home")
360 d[name] = home
361 RoundupRequestHandler.TRACKER_HOMES = d
362 except SystemExit:
363 raise
364 except ValueError:
365 usage(error())
366 except:
367 print error()
368 sys.exit(1)
370 # we don't want the cgi module interpreting the command-line args ;)
371 sys.argv = sys.argv[:1]
373 if pidfile:
374 if not hasattr(os, 'fork'):
375 print "Sorry, you can't run the server as a daemon on this" \
376 'Operating System'
377 sys.exit(0)
378 else:
379 daemonize(pidfile)
381 # redirect stdout/stderr to our logfile
382 if logfile:
383 # appending, unbuffered
384 sys.stdout = sys.stderr = open(logfile, 'a', 0)
386 if success_message:
387 print success_message
388 else:
389 print _('Roundup server started on %(address)s')%locals()
391 try:
392 httpd.serve_forever()
393 except KeyboardInterrupt:
394 print 'Keyboard Interrupt: exiting'
396 if __name__ == '__main__':
397 run()
399 # vim: set filetype=python ts=4 sw=4 et si