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.17 2003-01-13 02:44:42 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 class RoundupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
63 TRACKER_HOMES = TRACKER_HOMES
64 ROUNDUP_USER = ROUNDUP_USER
66 def run_cgi(self):
67 """ Execute the CGI command. Wrap an innner call in an error
68 handler so all errors can be caught.
69 """
70 save_stdin = sys.stdin
71 sys.stdin = self.rfile
72 try:
73 self.inner_run_cgi()
74 except client.NotFound:
75 self.send_error(404, self.path)
76 except client.Unauthorised:
77 self.send_error(403, self.path)
78 except:
79 # it'd be nice to be able to detect if these are going to have
80 # any effect...
81 self.send_response(400)
82 self.send_header('Content-Type', 'text/html')
83 self.end_headers()
84 try:
85 reload(cgitb)
86 self.wfile.write(cgitb.breaker())
87 self.wfile.write(cgitb.html())
88 except:
89 self.wfile.write("<pre>")
90 s = StringIO.StringIO()
91 traceback.print_exc(None, s)
92 self.wfile.write(cgi.escape(s.getvalue()))
93 self.wfile.write("</pre>\n")
94 sys.stdin = save_stdin
96 do_GET = do_POST = do_HEAD = send_head = run_cgi
98 def index(self):
99 ''' Print up an index of the available trackers
100 '''
101 self.send_response(200)
102 self.send_header('Content-Type', 'text/html')
103 self.end_headers()
104 w = self.wfile.write
105 w(_('<html><head><title>Roundup trackers index</title></head>\n'))
106 w(_('<body><h1>Roundup trackers index</h1><ol>\n'))
107 keys = self.TRACKER_HOMES.keys()
108 keys.sort()
109 for tracker in keys:
110 w(_('<li><a href="%(tracker_url)s/index">%(tracker_name)s</a>\n')%{
111 'tracker_url': urllib.quote(tracker),
112 'tracker_name': cgi.escape(tracker)})
113 w(_('</ol></body></html>'))
115 def inner_run_cgi(self):
116 ''' This is the inner part of the CGI handling
117 '''
119 rest = self.path
120 i = rest.rfind('?')
121 if i >= 0:
122 rest, query = rest[:i], rest[i+1:]
123 else:
124 query = ''
126 # figure the tracker
127 if rest == '/':
128 return self.index()
129 l_path = rest.split('/')
130 tracker_name = urllib.unquote(l_path[1])
131 if self.TRACKER_HOMES.has_key(tracker_name):
132 tracker_home = self.TRACKER_HOMES[tracker_name]
133 tracker = roundup.instance.open(tracker_home)
134 else:
135 raise client.NotFound
137 # figure out what the rest of the path is
138 if len(l_path) > 2:
139 rest = '/'.join(l_path[2:])
140 else:
141 rest = '/'
143 # Set up the CGI environment
144 env = {}
145 env['TRACKER_NAME'] = tracker_name
146 env['REQUEST_METHOD'] = self.command
147 env['PATH_INFO'] = urllib.unquote(rest)
148 if query:
149 env['QUERY_STRING'] = query
150 host = self.address_string()
151 if self.headers.typeheader is None:
152 env['CONTENT_TYPE'] = self.headers.type
153 else:
154 env['CONTENT_TYPE'] = self.headers.typeheader
155 length = self.headers.getheader('content-length')
156 if length:
157 env['CONTENT_LENGTH'] = length
158 co = filter(None, self.headers.getheaders('cookie'))
159 if co:
160 env['HTTP_COOKIE'] = ', '.join(co)
161 env['HTTP_AUTHORIZATION'] = self.headers.getheader('authorization')
162 env['SCRIPT_NAME'] = ''
163 env['SERVER_NAME'] = self.server.server_name
164 env['SERVER_PORT'] = str(self.server.server_port)
165 env['HTTP_HOST'] = self.headers['host']
167 decoded_query = query.replace('+', ' ')
169 # do the roundup thang
170 c = tracker.Client(tracker, self, env)
171 c.main()
173 def usage(message=''):
174 if message:
175 message = _('Error: %(error)s\n\n')%{'error': message}
176 print _('''%(message)sUsage:
177 roundup-server [-n hostname] [-p port] [-l file] [-d file] [name=tracker home]*
179 -n: sets the host name
180 -p: sets the port to listen on
181 -l: sets a filename to log to (instead of stdout)
182 -d: daemonize, and write the server's PID to the nominated file
184 name=tracker home
185 Sets the tracker home(s) to use. The name is how the tracker is
186 identified in the URL (it's the first part of the URL path). The
187 tracker home is the directory that was identified when you did
188 "roundup-admin init". You may specify any number of these name=home
189 pairs on the command-line. For convenience, you may edit the
190 TRACKER_HOMES variable in the roundup-server file instead.
191 Make sure the name part doesn't include any url-unsafe characters like
192 spaces, as these confuse the cookie handling in browsers like IE.
193 ''')%locals()
194 sys.exit(0)
196 def daemonize(pidfile):
197 ''' Turn this process into a daemon.
198 - make sure the sys.std(in|out|err) are completely cut off
199 - make our parent PID 1
201 Write our new PID to the pidfile.
203 From A.M. Kuuchling (possibly originally Greg Ward) with
204 modification from Oren Tirosh, and finally a small mod from me.
205 '''
206 # Fork once
207 if os.fork() != 0:
208 os._exit(0)
210 # Create new session
211 os.setsid()
213 # Second fork to force PPID=1
214 pid = os.fork()
215 if pid:
216 pidfile = open(pidfile, 'w')
217 pidfile.write(str(pid))
218 pidfile.close()
219 os._exit(0)
221 os.chdir("/")
222 os.umask(0)
224 # close off sys.std(in|out|err), redirect to devnull so the file
225 # descriptors can't be used again
226 devnull = os.open('/dev/null', 0)
227 os.dup2(devnull, 0)
228 os.dup2(devnull, 1)
229 os.dup2(devnull, 2)
231 def abspath(path):
232 ''' Make the given path an absolute path.
234 Code from Zope-Coders posting of 2002-10-06 by GvR.
235 '''
236 if not os.path.isabs(path):
237 path = os.path.join(os.getcwd(), path)
238 return os.path.normpath(path)
240 def run():
241 ''' Script entry point - handle args and figure out what to to.
242 '''
243 hostname = ''
244 port = 8080
245 pidfile = None
246 logfile = None
247 try:
248 # handle the command-line args
249 try:
250 optlist, args = getopt.getopt(sys.argv[1:], 'n:p:u:d:l:')
251 except getopt.GetoptError, e:
252 usage(str(e))
254 user = ROUNDUP_USER
255 for (opt, arg) in optlist:
256 if opt == '-n': hostname = arg
257 elif opt == '-p': port = int(arg)
258 elif opt == '-u': user = arg
259 elif opt == '-d': pidfile = abspath(arg)
260 elif opt == '-l': logfile = abspath(arg)
261 elif opt == '-h': usage()
263 if hasattr(os, 'getuid'):
264 # if root, setuid to the running user
265 if not os.getuid() and user is not None:
266 try:
267 import pwd
268 except ImportError:
269 raise ValueError, _("Can't change users - no pwd module")
270 try:
271 uid = pwd.getpwnam(user)[2]
272 except KeyError:
273 raise ValueError, _("User %(user)s doesn't exist")%locals()
274 os.setuid(uid)
275 elif os.getuid() and user is not None:
276 print _('WARNING: ignoring "-u" argument, not root')
278 # People can remove this check if they're really determined
279 if not os.getuid() and user is None:
280 raise ValueError, _("Can't run as root!")
282 # handle tracker specs
283 if args:
284 d = {}
285 for arg in args:
286 try:
287 name, home = arg.split('=')
288 except ValueError:
289 raise ValueError, _("Instances must be name=home")
290 d[name] = home
291 RoundupRequestHandler.TRACKER_HOMES = d
292 except SystemExit:
293 raise
294 except:
295 exc_type, exc_value = sys.exc_info()[:2]
296 usage('%s: %s'%(exc_type, exc_value))
298 # we don't want the cgi module interpreting the command-line args ;)
299 sys.argv = sys.argv[:1]
300 address = (hostname, port)
302 # fork?
303 if pidfile:
304 daemonize(pidfile)
306 # redirect stdout/stderr to our logfile
307 if logfile:
308 # appending, unbuffered
309 sys.stdout = sys.stderr = open(logfile, 'a', 0)
311 httpd = BaseHTTPServer.HTTPServer(address, RoundupRequestHandler)
312 print _('Roundup server started on %(address)s')%locals()
313 try:
314 httpd.serve_forever()
315 except KeyboardInterrupt:
316 print 'Keyboard Interrupt: exiting'
318 if __name__ == '__main__':
319 run()
321 # vim: set filetype=python ts=4 sw=4 et si