Mercurial > p > roundup > code
comparison roundup/scripts/roundup_server.py @ 1356:83f33642d220 maint-0.5
[[Metadata associated with this commit was garbled during conversion from CVS
to Subversion.]]
| author | Richard Jones <richard@users.sourceforge.net> |
|---|---|
| date | Thu, 09 Jan 2003 22:59:22 +0000 |
| parents | |
| children | 434708fc4c29 |
comparison
equal
deleted
inserted
replaced
| 1242:3d0158c8c32b | 1356:83f33642d220 |
|---|---|
| 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. | |
| 18 | |
| 19 $Id: roundup_server.py,v 1.16 2002-11-28 06:55:57 richard Exp $ | |
| 20 """ | |
| 21 | |
| 22 # python version check | |
| 23 from roundup import version_check | |
| 24 | |
| 25 import sys, os, urllib, StringIO, traceback, cgi, binascii, getopt, imp | |
| 26 import BaseHTTPServer | |
| 27 | |
| 28 # Roundup modules of use here | |
| 29 from roundup.cgi import cgitb, client | |
| 30 import roundup.instance | |
| 31 from roundup.i18n import _ | |
| 32 | |
| 33 # | |
| 34 ## Configuration | |
| 35 # | |
| 36 | |
| 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 } | |
| 45 | |
| 46 ROUNDUP_USER = None | |
| 47 | |
| 48 | |
| 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() | |
| 57 | |
| 58 # | |
| 59 ## end configuration | |
| 60 # | |
| 61 | |
| 62 class RoundupRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): | |
| 63 TRACKER_HOMES = TRACKER_HOMES | |
| 64 ROUNDUP_USER = ROUNDUP_USER | |
| 65 | |
| 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 | |
| 95 | |
| 96 do_GET = do_POST = do_HEAD = send_head = run_cgi | |
| 97 | |
| 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>')) | |
| 114 | |
| 115 def inner_run_cgi(self): | |
| 116 ''' This is the inner part of the CGI handling | |
| 117 ''' | |
| 118 | |
| 119 rest = self.path | |
| 120 i = rest.rfind('?') | |
| 121 if i >= 0: | |
| 122 rest, query = rest[:i], rest[i+1:] | |
| 123 else: | |
| 124 query = '' | |
| 125 | |
| 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 | |
| 136 | |
| 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 = '/' | |
| 142 | |
| 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'] | |
| 166 | |
| 167 decoded_query = query.replace('+', ' ') | |
| 168 | |
| 169 # do the roundup thang | |
| 170 c = tracker.Client(tracker, self, env) | |
| 171 c.main() | |
| 172 | |
| 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]* | |
| 178 | |
| 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 | |
| 183 | |
| 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) | |
| 195 | |
| 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 | |
| 200 | |
| 201 Write our new PID to the pidfile. | |
| 202 | |
| 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) | |
| 209 | |
| 210 # Create new session | |
| 211 os.setsid() | |
| 212 | |
| 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) | |
| 220 | |
| 221 os.chdir("/") | |
| 222 os.umask(0) | |
| 223 | |
| 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) | |
| 230 | |
| 231 def abspath(path): | |
| 232 ''' Make the given path an absolute path. | |
| 233 | |
| 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) | |
| 239 | |
| 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)) | |
| 253 | |
| 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() | |
| 262 | |
| 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') | |
| 277 | |
| 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!") | |
| 281 | |
| 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)) | |
| 297 | |
| 298 # we don't want the cgi module interpreting the command-line args ;) | |
| 299 sys.argv = sys.argv[:1] | |
| 300 address = (hostname, port) | |
| 301 | |
| 302 # fork? | |
| 303 if pidfile: | |
| 304 daemonize(pidfile) | |
| 305 | |
| 306 # redirect stdout/stderr to our logfile | |
| 307 if logfile: | |
| 308 sys.stdout = sys.stderr = open(logfile, 'a') | |
| 309 | |
| 310 httpd = BaseHTTPServer.HTTPServer(address, RoundupRequestHandler) | |
| 311 print _('Roundup server started on %(address)s')%locals() | |
| 312 try: | |
| 313 httpd.serve_forever() | |
| 314 except KeyboardInterrupt: | |
| 315 print 'Keyboard Interrupt: exiting' | |
| 316 | |
| 317 if __name__ == '__main__': | |
| 318 run() | |
| 319 | |
| 320 # vim: set filetype=python ts=4 sw=4 et si |
