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