# -*- coding: iso-8859-1 -*- # # Copyright (C) 2003-2005 Edgewall Software # Copyright (C) 2003-2005 Jonas Borgström # Copyright (C) 2005 Matthew Good # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. The terms # are also available at http://trac.edgewall.com/license.html. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://projects.edgewall.com/trac/. # # Author: Jonas Borgström # Matthew Good # # Todo: # - External auth using mod_proxy / squid. from trac import util, __version__ from trac.env import open_environment from trac.web.api import Request from trac.web.cgi_frontend import TracFieldStorage from trac.web.main import dispatch_request, get_environment, \ send_pretty_error, send_project_index from trac.util import md5crypt import os import re import sys import md5 import time import socket, errno import urllib import urllib2 from SocketServer import ThreadingMixIn from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler try: from base64 import b64decode except ImportError: from base64 import decodestring as b64decode class BasicAuth: def __init__(self, htpasswd, realm): self.hash = {} self.realm = realm try: import crypt self.crypt = crypt.crypt except ImportError: self.crypt = None self.load(htpasswd) def load(self, filename): fd = open(filename, 'r') for line in fd: u, h = line.strip().split(':') if '$' in h or self.crypt: self.hash[u] = h else: print >>sys.stderr, 'Warning: cannot parse password for ' \ 'user "%s" without the "crypt" module' % u if self.hash == {}: print >> sys.stderr, "Warning: found no users in file:", filename def test(self, user, password): the_hash = self.hash.get(user) if the_hash is None: return False if not '$' in the_hash: return self.crypt(password, the_hash[:2]) == the_hash magic, salt = the_hash[1:].split('$')[:2] magic = '$' + magic + '$' return md5crypt(password, salt, magic) == the_hash def send_auth_request(self, req): req.send_response(401) req.send_header('WWW-Authenticate', 'Basic realm="%s"' % self.realm) req.end_headers() def do_auth(self, req): if not 'Authorization' in req.headers or \ not req.headers['Authorization'].startswith('Basic'): self.send_auth_request(req) return None auth = req.headers['Authorization'][len('Basic')+1:] auth = b64decode(auth).split(':') if len(auth) != 2: self.send_auth_request(req) return None user, password = auth if not self.test(user, password): self.send_auth_request(req) return None return user class DigestAuth: """A simple HTTP DigestAuth implementation (rfc2617)""" MAX_NONCES = 100 def __init__(self, htdigest, realm): self.active_nonces = [] self.hash = {} self.realm = realm self.load_htdigest(htdigest, realm) def load_htdigest(self, filename, realm): """ Load account information from apache style htdigest files, only users from the specified realm are used """ fd = open(filename, 'r') for line in fd.readlines(): u, r, a1 = line.strip().split(':') if r == realm: self.hash[u] = a1 if self.hash == {}: print >> sys.stderr, "Warning: found no users in realm:", realm def parse_auth_header(self, authorization): values = {} for value in urllib2.parse_http_list(authorization): n, v = value.split('=', 1) if v[0] == '"' and v[-1] == '"': values[n] = v[1:-1] else: values[n] = v return values def send_auth_request(self, req, stale='false'): """ Send a digest challange to the browser. Record used nonces to avoid replay attacks. """ nonce = util.hex_entropy() self.active_nonces.append(nonce) if len(self.active_nonces) > DigestAuth.MAX_NONCES: self.active_nonces = self.active_nonces[-DigestAuth.MAX_NONCES:] req.send_response(401) req.send_header('WWW-Authenticate', 'Digest realm="%s", nonce="%s", qop="auth", stale="%s"' % (self.realm, nonce, stale)) req.end_headers() def do_auth(self, req): if not 'Authorization' in req.headers or \ not req.headers['Authorization'].startswith('Digest'): self.send_auth_request(req) return None auth = self.parse_auth_header(req.headers['Authorization'][7:]) required_keys = ['username', 'realm', 'nonce', 'uri', 'response', 'nc', 'cnonce'] # Invalid response? for key in required_keys: if not auth.has_key(key): self.send_auth_request(req) return None # Unknown user? if not self.hash.has_key(auth['username']): self.send_auth_request(req) return None kd = lambda x: md5.md5(':'.join(x)).hexdigest() a1 = self.hash[auth['username']] a2 = kd([req.command, auth['uri']]) # Is the response correct? correct = kd([a1, auth['nonce'], auth['nc'], auth['cnonce'], auth['qop'], a2]) if auth['response'] != correct: self.send_auth_request(req) return None # Is the nonce active, if not ask the client to use a new one if not auth['nonce'] in self.active_nonces: self.send_auth_request(req, stale='true') return None self.active_nonces.remove(auth['nonce']) return auth['username'] class TracHTTPServer(ThreadingMixIn, HTTPServer): projects = None def __init__(self, server_address, env_parent_dir, env_paths, auths): HTTPServer.__init__(self, server_address, TracHTTPRequestHandler) if self.server_port == 80: self.http_host = self.server_name else: self.http_host = '%s:%d' % (self.server_name, self.server_port) self.env_parent_dir = env_parent_dir and {'TRAC_ENV_PARENT_DIR': env_parent_dir} self.auths = auths self.projects = {} for env_path in env_paths: # Remove trailing slashes while env_path and not os.path.split(env_path)[1]: env_path = os.path.split(env_path)[0] project = os.path.split(env_path)[1] if self.projects.has_key(project): print >>sys.stderr, 'Warning: Ignoring project "%s" since ' \ 'it conflicts with project "%s"' \ % (env_path, self.projects[project]) else: self.projects[project] = env_path def get_env_opts(self, project=None): if self.env_parent_dir: opts = self.env_parent_dir.items() else: opts = [('TRAC_ENV', self.projects[project])] return dict(opts + os.environ.items()) def send_project_index(self, req): if self.env_parent_dir: return send_project_index(req, self.get_env_opts()) else: return send_project_index(req, os.environ, self.projects.values()) class TracHTTPRequestHandler(BaseHTTPRequestHandler): server_version = 'tracd/' + __version__ url_re = re.compile('/(?P[^/\?]+)' '(?P/?[^\?]*)?' '(?:\?(?P.*))?') env = None log = None project_name = None def finish(self): """We need to help the garbage collector a little.""" BaseHTTPRequestHandler.finish(self) self.wfile = None self.rfile = None def do_POST(self): self._do_trac_req() def do_HEAD(self): self.do_GET() def do_GET(self): self._do_trac_req() def _do_trac_req(self): if self.path == '/': path_info = '/' req = TracHTTPRequest(self, '', '') self.server.send_project_index(req) return m = self.url_re.findall(self.path) if not m: self.send_error(400, 'Bad Request') return project_name, path_info, query_string = m[0] project_name = urllib.unquote(project_name) path_info = urllib.unquote(path_info) req = TracHTTPRequest(self, project_name, query_string) try: opts = self.server.get_env_opts(project_name) except KeyError: # unrecognized project self.server.send_project_index(req) return env = get_environment(req, opts) if not env: self.server.send_project_index(req) return req.remote_user = None if path_info == '/login': auth = self.server.auths.get(project_name) or \ self.server.auths.get('*') if not auth: raise util.TracError('Authentication not enabled. ' 'Please use the tracd --auth option.\n') req.remote_user = auth.do_auth(self) if not req.remote_user: return try: start = time.time() dispatch_request(path_info, req, env) env.log.debug('Total request time: %f s', time.time() - start) except socket.error, (code, msg): if code == errno.EPIPE or code == 10053: # Windows env.log.info('Lost connection to client: %s' % self.address_string()) else: raise except Exception, e: try: send_pretty_error(e, env, req) except socket.error, (code, msg): if code == errno.EPIPE or code == 10053: # Windows env.log.info('Lost connection to client: %s' % self.address_string()) else: raise class TracHTTPRequest(Request): def __init__(self, handler, project_name, query_string): Request.__init__(self) self.__handler = handler self.__status_sent = False self.scheme = 'http' self.method = self.__handler.command self.remote_addr = str(self.__handler.client_address[0]) self.server_name = self.__handler.server.server_name self.server_port = self.__handler.server.server_port if self.__handler.headers.has_key('Cookie'): self.incookie.load(self.__handler.headers['Cookie']) self.cgi_location = '/' + project_name self.idx_location = '/' environ = {'REQUEST_METHOD': self.method, 'QUERY_STRING': query_string} headers = self.__handler.headers if self.method in ('GET', 'HEAD'): headers = None self.args = TracFieldStorage(self.__handler.rfile, environ=environ, headers=headers, keep_blank_values=1) def read(self, size=None): return self.__handler.rfile.read(size) def write(self, data): self.__handler.wfile.write(data) def get_header(self, name): return self.__handler.headers.get(name) def send_response(self, code): self.__handler.send_response(code) self.__status_sent = True def send_header(self, name, value): if not self.__status_sent: self._headers.append((name, value)) else: self.__handler.send_header(name, value) def end_headers(self): self.__handler.end_headers()