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.35 2003-12-04 02:43:07 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:
82 exc, val, tb = sys.exc_info()
83 if hasattr(socket, 'timeout') and exc == socket.timeout:
84 s = StringIO.StringIO()
85 traceback.print_exc(None, s)
86 self.log_message(str(s.getvalue()))
87 else:
88 # it'd be nice to be able to detect if these are going to have
89 # any effect...
90 self.send_response(400)
91 self.send_header('Content-Type', 'text/html')
92 self.end_headers()
93 try:
94 reload(cgitb)
95 self.wfile.write(cgitb.breaker())
96 self.wfile.write(cgitb.html())
97 except:
98 s = StringIO.StringIO()
99 traceback.print_exc(None, s)
100 self.wfile.write("<pre>")
101 self.wfile.write(cgi.escape(s.getvalue()))
102 self.wfile.write("</pre>\n")
103 sys.stdin = save_stdin
105 do_GET = do_POST = run_cgi
107 def index(self):
108 ''' Print up an index of the available trackers
109 '''
110 self.send_response(200)
111 self.send_header('Content-Type', 'text/html')
112 self.end_headers()
113 w = self.wfile.write
114 w(_('<html><head><title>Roundup trackers index</title></head>\n'))
115 w(_('<body><h1>Roundup trackers index</h1><ol>\n'))
116 keys = self.TRACKER_HOMES.keys()
117 keys.sort()
118 for tracker in keys:
119 w(_('<li><a href="%(tracker_url)s/index">%(tracker_name)s</a>\n')%{
120 'tracker_url': urllib.quote(tracker),
121 'tracker_name': cgi.escape(tracker)})
122 w(_('</ol></body></html>'))
124 def inner_run_cgi(self):
125 ''' This is the inner part of the CGI handling
126 '''
127 rest = self.path
129 if rest == '/favicon.ico':
130 raise client.NotFound
132 i = rest.rfind('?')
133 if i >= 0:
134 rest, query = rest[:i], rest[i+1:]
135 else:
136 query = ''
138 # no tracker - spit out the index
139 if rest == '/':
140 return self.index()
142 # figure the tracker
143 l_path = rest.split('/')
144 tracker_name = urllib.unquote(l_path[1])
146 # handle missing trailing '/'
147 if len(l_path) == 2:
148 self.send_response(301)
149 # redirect - XXX https??
150 protocol = 'http'
151 url = '%s://%s%s/'%(protocol, self.headers['host'], self.path)
152 self.send_header('Location', url)
153 self.end_headers()
154 self.wfile.write('Moved Permanently')
155 return
157 if self.TRACKER_HOMES.has_key(tracker_name):
158 tracker_home = self.TRACKER_HOMES[tracker_name]
159 tracker = roundup.instance.open(tracker_home)
160 else:
161 raise client.NotFound
163 # figure out what the rest of the path is
164 if len(l_path) > 2:
165 rest = '/'.join(l_path[2:])
166 else:
167 rest = '/'
169 # Set up the CGI environment
170 env = {}
171 env['TRACKER_NAME'] = tracker_name
172 env['REQUEST_METHOD'] = self.command
173 env['PATH_INFO'] = urllib.unquote(rest)
174 if query:
175 env['QUERY_STRING'] = query
176 host = self.address_string()
177 if self.headers.typeheader is None:
178 env['CONTENT_TYPE'] = self.headers.type
179 else:
180 env['CONTENT_TYPE'] = self.headers.typeheader
181 length = self.headers.getheader('content-length')
182 if length:
183 env['CONTENT_LENGTH'] = length
184 co = filter(None, self.headers.getheaders('cookie'))
185 if co:
186 env['HTTP_COOKIE'] = ', '.join(co)
187 env['HTTP_AUTHORIZATION'] = self.headers.getheader('authorization')
188 env['SCRIPT_NAME'] = ''
189 env['SERVER_NAME'] = self.server.server_name
190 env['SERVER_PORT'] = str(self.server.server_port)
191 env['HTTP_HOST'] = self.headers['host']
193 decoded_query = query.replace('+', ' ')
195 # do the roundup thang
196 c = tracker.Client(tracker, self, env)
197 c.main()
199 LOG_IPADDRESS = 1
200 def address_string(self):
201 if self.LOG_IPADDRESS:
202 return self.client_address[0]
203 else:
204 host, port = self.client_address
205 return socket.getfqdn(host)
207 def error():
208 exc_type, exc_value = sys.exc_info()[:2]
209 return _('Error: %s: %s' % (exc_type, exc_value))
211 def usage(message=''):
212 print _('''%(message)s
214 Usage:
215 roundup-server [options] [name=tracker home]*
217 options:
218 -n: sets the host name
219 -p: sets the port to listen on
220 -u: sets the uid to this user after listening on the port
221 -g: sets the gid to this group after listening on the port
222 -l: sets a filename to log to (instead of stdout)
223 -d: sets a filename to write server PID to. This option causes the server
224 to run in the background. Note: on Windows the PID argument is needed,
225 but ignored. The -l option *must* be specified if this option is.
226 -N: log client machine names in access log instead of IP addresses (much
227 slower)
229 name=tracker home:
230 Sets the tracker home(s) to use. The name is how the tracker is
231 identified in the URL (it's the first part of the URL path). The
232 tracker home is the directory that was identified when you did
233 "roundup-admin init". You may specify any number of these name=home
234 pairs on the command-line. For convenience, you may edit the
235 TRACKER_HOMES variable in the roundup-server file instead.
236 Make sure the name part doesn't include any url-unsafe characters like
237 spaces, as these confuse the cookie handling in browsers like IE.
238 ''')%locals()
239 sys.exit(0)
241 def daemonize(pidfile):
242 ''' Turn this process into a daemon.
243 - make sure the sys.std(in|out|err) are completely cut off
244 - make our parent PID 1
246 Write our new PID to the pidfile.
248 From A.M. Kuuchling (possibly originally Greg Ward) with
249 modification from Oren Tirosh, and finally a small mod from me.
250 '''
251 # Fork once
252 if os.fork() != 0:
253 os._exit(0)
255 # Create new session
256 os.setsid()
258 # Second fork to force PPID=1
259 pid = os.fork()
260 if pid:
261 pidfile = open(pidfile, 'w')
262 pidfile.write(str(pid))
263 pidfile.close()
264 os._exit(0)
266 os.chdir("/")
267 os.umask(0)
269 # close off sys.std(in|out|err), redirect to devnull so the file
270 # descriptors can't be used again
271 devnull = os.open('/dev/null', 0)
272 os.dup2(devnull, 0)
273 os.dup2(devnull, 1)
274 os.dup2(devnull, 2)
276 def run(port=8080, success_message=None):
277 ''' Script entry point - handle args and figure out what to to.
278 '''
279 # time out after a minute if we can
280 import socket
281 if hasattr(socket, 'setdefaulttimeout'):
282 socket.setdefaulttimeout(60)
284 hostname = ''
285 pidfile = None
286 logfile = None
287 try:
288 # handle the command-line args
289 try:
290 optlist, args = getopt.getopt(sys.argv[1:], 'n:p:u:d:l:hN')
291 except getopt.GetoptError, e:
292 usage(str(e))
294 user = ROUNDUP_USER
295 group = None
296 for (opt, arg) in optlist:
297 if opt == '-n': hostname = arg
298 elif opt == '-p': port = int(arg)
299 elif opt == '-u': user = arg
300 elif opt == '-g': group = arg
301 elif opt == '-d': pidfile = os.path.abspath(arg)
302 elif opt == '-l': logfile = os.path.abspath(arg)
303 elif opt == '-h': usage()
304 elif opt == '-N': RoundupRequestHandler.LOG_IPADDRESS = 0
306 if pidfile and not logfile:
307 raise ValueError, _("logfile *must* be specified if pidfile is")
309 # obtain server before changing user id - allows to use port <
310 # 1024 if started as root
311 address = (hostname, port)
312 try:
313 httpd = BaseHTTPServer.HTTPServer(address, RoundupRequestHandler)
314 except socket.error, e:
315 if e[0] == errno.EADDRINUSE:
316 raise socket.error, \
317 _("Unable to bind to port %s, port already in use." % port)
318 raise
320 if group is not None and hasattr(os, 'getgid'):
321 # if root, setgid to the running user
322 if not os.getgid() and user is not None:
323 try:
324 import pwd
325 except ImportError:
326 raise ValueError, _("Can't change groups - no pwd module")
327 try:
328 gid = pwd.getpwnam(user)[3]
329 except KeyError:
330 raise ValueError,_("Group %(group)s doesn't exist")%locals()
331 os.setgid(gid)
332 elif os.getgid() and user is not None:
333 print _('WARNING: ignoring "-g" argument, not root')
335 if hasattr(os, 'getuid'):
336 # if root, setuid to the running user
337 if not os.getuid() and user is not None:
338 try:
339 import pwd
340 except ImportError:
341 raise ValueError, _("Can't change users - no pwd module")
342 try:
343 uid = pwd.getpwnam(user)[2]
344 except KeyError:
345 raise ValueError, _("User %(user)s doesn't exist")%locals()
346 os.setuid(uid)
347 elif os.getuid() and user is not None:
348 print _('WARNING: ignoring "-u" argument, not root')
350 # People can remove this check if they're really determined
351 if not os.getuid() and user is None:
352 raise ValueError, _("Can't run as root!")
354 # handle tracker specs
355 if args:
356 d = {}
357 for arg in args:
358 try:
359 name, home = arg.split('=')
360 except ValueError:
361 raise ValueError, _("Instances must be name=home")
362 d[name] = home
363 RoundupRequestHandler.TRACKER_HOMES = d
364 except SystemExit:
365 raise
366 except ValueError:
367 usage(error())
368 except:
369 print error()
370 sys.exit(1)
372 # we don't want the cgi module interpreting the command-line args ;)
373 sys.argv = sys.argv[:1]
375 if pidfile:
376 if not hasattr(os, 'fork'):
377 print "Sorry, you can't run the server as a daemon on this" \
378 'Operating System'
379 sys.exit(0)
380 else:
381 daemonize(pidfile)
383 # redirect stdout/stderr to our logfile
384 if logfile:
385 # appending, unbuffered
386 sys.stdout = sys.stderr = open(logfile, 'a', 0)
388 if success_message:
389 print success_message
390 else:
391 print _('Roundup server started on %(address)s')%locals()
393 try:
394 httpd.serve_forever()
395 except KeyboardInterrupt:
396 print 'Keyboard Interrupt: exiting'
398 if __name__ == '__main__':
399 run()
401 # vim: set filetype=python ts=4 sw=4 et si