"Fossies" - the Fresh Open Source Software Archive

Member "viewvc-1.2.1/bin/standalone.py" (26 Mar 2020, 19550 Bytes) of package /linux/misc/viewvc-1.2.1.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "standalone.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 1.1.28_vs_1.2.1.

    1 #!/usr/bin/env python
    2 # -*-python-*-
    3 #
    4 # Copyright (C) 1999-2020 The ViewCVS Group. All Rights Reserved.
    5 #
    6 # By using this file, you agree to the terms and conditions set forth in
    7 # the LICENSE.html file which can be found at the top level of the ViewVC
    8 # distribution or at http://viewvc.org/license-1.html.
    9 #
   10 # For more information, visit http://viewvc.org/
   11 #
   12 # -----------------------------------------------------------------------
   13 #
   14 # This program originally written by Peter Funk <pf@artcom-gmbh.de>, with
   15 # contributions by Ka-Ping Yee.
   16 #
   17 # -----------------------------------------------------------------------
   18 
   19 #
   20 # INSTALL-TIME CONFIGURATION
   21 #
   22 # These values will be set during the installation process. During
   23 # development, they will remain None.
   24 #
   25 
   26 LIBRARY_DIR = None
   27 CONF_PATHNAME = None
   28 
   29 import sys
   30 import os
   31 import os.path
   32 import stat
   33 import string
   34 import urllib
   35 import rfc822
   36 import socket
   37 import select
   38 import base64
   39 import BaseHTTPServer
   40 
   41 if LIBRARY_DIR:
   42   sys.path.insert(0, LIBRARY_DIR)
   43 else:
   44   sys.path.insert(0, os.path.abspath(os.path.join(sys.argv[0], "../../lib")))
   45 
   46 import sapi
   47 import viewvc
   48 
   49 
   50 # The 'crypt' module is only available on Unix platforms.  We'll try
   51 # to use 'fcrypt' if it's available (for more information, see
   52 # http://carey.geek.nz/code/python-fcrypt/).
   53 has_crypt = False
   54 try:
   55   import crypt
   56   has_crypt = True
   57   def _check_passwd(user_passwd, real_passwd):
   58     return real_passwd == crypt.crypt(user_passwd, real_passwd[:2])
   59 except ImportError:
   60   try:
   61     import fcrypt
   62     has_crypt = True
   63     def _check_passwd(user_passwd, real_passwd):
   64       return real_passwd == fcrypt.crypt(user_passwd, real_passwd[:2])
   65   except ImportError:
   66     def _check_passwd(user_passwd, real_passwd):
   67       return False
   68 
   69 
   70 class Options:
   71   port = 49152      # default TCP/IP port used for the server
   72   repositories = {} # use default repositories specified in config
   73   host = sys.platform == 'mac' and '127.0.0.1' or 'localhost'
   74   script_alias = 'viewvc'
   75   config_file = None
   76   htpasswd_file = None
   77 
   78 
   79 class StandaloneServer(sapi.CgiServer):
   80   """Custom sapi interface that uses a BaseHTTPRequestHandler HANDLER
   81   to generate output."""
   82   
   83   def __init__(self, handler):
   84     sapi.CgiServer.__init__(self, inheritableOut = sys.platform != "win32")
   85     self.handler = handler
   86 
   87   def header(self, content_type='text/html', status=None):
   88     if not self.headerSent:
   89       self.headerSent = 1
   90       if status is None:
   91         statusCode = 200
   92         statusText = 'OK'       
   93       else:        
   94         p = status.find(' ')
   95         if p < 0:
   96           statusCode = int(status)
   97           statusText = ''
   98         else:
   99           statusCode = int(status[:p])
  100           statusText = status[p+1:]
  101       self.handler.send_response(statusCode, statusText)
  102       self.handler.send_header("Content-type", content_type)
  103       for (name, value) in self.headers:
  104         self.handler.send_header(name, value)
  105       self.handler.end_headers()
  106 
  107 
  108 class NotViewVCLocationException(Exception):
  109   """The request location was not aimed at ViewVC."""
  110   pass
  111 
  112 
  113 class AuthenticationException(Exception):
  114   """Authentication requirements have not been met."""
  115   pass
  116 
  117 
  118 class ViewVCHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
  119   """Custom HTTP request handler for ViewVC."""
  120   
  121   def do_GET(self):
  122     """Serve a GET request."""
  123     self.handle_request('GET')
  124 
  125   def do_POST(self):
  126     """Serve a POST request."""
  127     self.handle_request('POST')
  128 
  129   def handle_request(self, method):
  130     """Handle a request of type METHOD."""
  131     try:
  132       self.run_viewvc()
  133     except NotViewVCLocationException:
  134       # If the request was aimed at the server root, but there's a
  135       # non-empty script_alias, automatically redirect to the
  136       # script_alias.  Otherwise, just return a 404 and shrug.
  137       if (not self.path or self.path == "/") and options.script_alias:
  138         new_url = self.server.url + options.script_alias + '/'
  139         self.send_response(301, "Moved Permanently")
  140         self.send_header("Content-type", "text/html")
  141         self.send_header("Location", new_url)
  142         self.end_headers()
  143         self.wfile.write("""<html>
  144 <head>
  145 <meta http-equiv="refresh" content="10; url=%s" />
  146 <title>Moved Temporarily</title>
  147 </head>
  148 <body>
  149 <h1>Redirecting to ViewVC</h1>
  150 <p>You will be automatically redirected to <a href="%s">ViewVC</a>.
  151    If this doesn't work, please click on the link above.</p>
  152 </body>
  153 </html>
  154 """ % (new_url, new_url))
  155       else:
  156         self.send_error(404)
  157     except IOError: # ignore IOError: [Errno 32] Broken pipe
  158       pass
  159     except AuthenticationException:
  160       self.send_response(401, "Unauthorized")
  161       self.send_header("WWW-Authenticate", 'Basic realm="ViewVC"')
  162       self.send_header("Content-type", "text/html")
  163       self.end_headers()
  164       self.wfile.write("""<html>
  165 <head>
  166 <title>Authentication failed</title>
  167 </head>
  168 <body>
  169 <h1>Authentication failed</h1>
  170 <p>Authentication has failed.  Please retry with the correct username
  171    and password.</p>
  172 </body>
  173 </html>""")
  174       
  175   def is_viewvc(self):
  176     """Check whether self.path is, or is a child of, the ScriptAlias"""
  177     if not options.script_alias:
  178       return 1
  179     if self.path == '/' + options.script_alias:
  180       return 1
  181     alias_len = len(options.script_alias)
  182     if self.path[:alias_len+2] == '/' + options.script_alias + '/':
  183       return 1
  184     if self.path[:alias_len+2] == '/' + options.script_alias + '?':
  185       return 1
  186     return 0
  187 
  188   def validate_password(self, htpasswd_file, username, password):
  189     """Compare USERNAME and PASSWORD against HTPASSWD_FILE."""
  190     try:
  191       lines = open(htpasswd_file, 'r').readlines()
  192       for line in lines:
  193         file_user, file_pass = line.rstrip().split(':', 1)
  194         if username == file_user:
  195           return _check_passwd(password, file_pass)
  196     except:
  197       pass
  198     return False
  199     
  200   def run_viewvc(self):
  201     """Run ViewVC to field a single request."""
  202 
  203     ### Much of this is adapter from Python's standard library
  204     ### module CGIHTTPServer.
  205 
  206     # Is this request even aimed at ViewVC?  If not, complain.
  207     if not self.is_viewvc():
  208       raise NotViewVCLocationException()
  209 
  210     # If htpasswd authentication is enabled, try to authenticate the user.
  211     self.username = None
  212     if options.htpasswd_file:
  213       authn = self.headers.get('authorization')
  214       if not authn:
  215         raise AuthenticationException()
  216       try:
  217         kind, data = authn.split(' ', 1)
  218         if kind == 'Basic':
  219           data = base64.b64decode(data)
  220           username, password = data.split(':', 1)
  221       except:
  222         raise AuthenticationException()
  223       if not self.validate_password(options.htpasswd_file, username, password):
  224         raise AuthenticationException()
  225       self.username = username
  226 
  227     # Setup the environment in preparation of executing ViewVC's core code.
  228     env = os.environ  
  229     
  230     scriptname = options.script_alias and '/' + options.script_alias or ''
  231 
  232     viewvc_url = self.server.url[:-1] + scriptname
  233     rest = self.path[len(scriptname):]
  234     i = rest.rfind('?')
  235     if i >= 0:
  236       rest, query = rest[:i], rest[i+1:]
  237     else:
  238       query = ''
  239 
  240     # Since we're going to modify the env in the parent, provide empty
  241     # values to override previously set values
  242     for k in env.keys():
  243       if k[:5] == 'HTTP_':
  244         del env[k]
  245     for k in ('QUERY_STRING', 'REMOTE_HOST', 'CONTENT_LENGTH',
  246               'HTTP_USER_AGENT', 'HTTP_COOKIE'):
  247       if env.has_key(k): 
  248         env[k] = ""
  249 
  250     # XXX Much of the following could be prepared ahead of time!
  251     env['SERVER_SOFTWARE'] = self.version_string()
  252     env['SERVER_NAME'] = self.server.server_name
  253     env['GATEWAY_INTERFACE'] = 'CGI/1.1'
  254     env['SERVER_PROTOCOL'] = self.protocol_version
  255     env['SERVER_PORT'] = str(self.server.server_port)
  256     env['REQUEST_METHOD'] = self.command
  257     uqrest = urllib.unquote(rest)
  258     env['PATH_INFO'] = uqrest
  259     env['SCRIPT_NAME'] = scriptname
  260     if query:
  261       env['QUERY_STRING'] = query
  262     env['HTTP_HOST'] = self.server.address[0]
  263     host = self.address_string()
  264     if host != self.client_address[0]:
  265       env['REMOTE_HOST'] = host
  266     env['REMOTE_ADDR'] = self.client_address[0]
  267     if self.username:
  268       env['REMOTE_USER'] = self.username
  269     if self.headers.typeheader is None:
  270       env['CONTENT_TYPE'] = self.headers.type
  271     else:
  272       env['CONTENT_TYPE'] = self.headers.typeheader
  273     length = self.headers.getheader('content-length')
  274     if length:
  275       env['CONTENT_LENGTH'] = length
  276     accept = []
  277     for line in self.headers.getallmatchingheaders('accept'):
  278       if line[:1] in string.whitespace:
  279         accept.append(line.strip())
  280       else:
  281         accept = accept + line[7:].split(',')
  282     env['HTTP_ACCEPT'] = ','.join(accept)
  283     ua = self.headers.getheader('user-agent')
  284     if ua:
  285       env['HTTP_USER_AGENT'] = ua
  286     modified = self.headers.getheader('if-modified-since')
  287     if modified:
  288       env['HTTP_IF_MODIFIED_SINCE'] = modified
  289     etag = self.headers.getheader('if-none-match')
  290     if etag:
  291       env['HTTP_IF_NONE_MATCH'] = etag
  292     # AUTH_TYPE
  293     # REMOTE_IDENT
  294     # XXX Other HTTP_* headers
  295       
  296     # Preserve state, because we execute script in current process:
  297     save_argv = sys.argv
  298     save_stdin = sys.stdin
  299     save_stdout = sys.stdout
  300     save_stderr = sys.stderr
  301     # For external tools like enscript we also need to redirect
  302     # the real stdout file descriptor.
  303     #
  304     # FIXME:  This code used to carry the following comment:
  305     #
  306     #   (On windows, reassigning the sys.stdout variable is sufficient
  307     #   because pipe_cmds makes it the standard output for child
  308     #   processes.)
  309     #
  310     # But we no longer use pipe_cmds.  So at the very least, the
  311     # comment is stale.  Is the code okay, though?
  312     if sys.platform != "win32":
  313       save_realstdout = os.dup(1) 
  314     try:
  315       try:
  316         sys.argv = []
  317         sys.stdout = self.wfile
  318         if sys.platform != "win32":
  319           os.dup2(self.wfile.fileno(), 1)
  320         sys.stdin = self.rfile
  321         viewvc.main(StandaloneServer(self), cfg)
  322       finally:
  323         sys.argv = save_argv
  324         sys.stdin = save_stdin
  325         sys.stdout.flush()
  326         if sys.platform != "win32":
  327           os.dup2(save_realstdout, 1)
  328           os.close(save_realstdout)
  329         sys.stdout = save_stdout
  330         sys.stderr = save_stderr
  331     except SystemExit, status:
  332       self.log_error("ViewVC exit status %s", str(status))
  333     else:
  334       self.log_error("ViewVC exited ok")
  335 
  336 
  337 class ViewVCHTTPServer(BaseHTTPServer.HTTPServer):
  338   """Customized HTTP server for ViewVC."""
  339   
  340   def __init__(self, host, port, callback):
  341     self.address = (host, port)
  342     self.url = 'http://%s:%d/' % (host, port)
  343     self.callback = callback
  344     BaseHTTPServer.HTTPServer.__init__(self, self.address, self.handler)
  345 
  346   def serve_until_quit(self):
  347     self.quit = 0
  348     while not self.quit:
  349       rd, wr, ex = select.select([self.socket.fileno()], [], [], 1)
  350       if rd:
  351         self.handle_request()
  352 
  353   def server_activate(self):
  354     BaseHTTPServer.HTTPServer.server_activate(self)
  355     if self.callback:
  356       self.callback(self)
  357 
  358   def server_bind(self):
  359     # set SO_REUSEADDR (if available on this platform)
  360     if hasattr(socket, 'SOL_SOCKET') and hasattr(socket, 'SO_REUSEADDR'):
  361       self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  362     BaseHTTPServer.HTTPServer.server_bind(self)
  363 
  364   def handle_error(self, request, client_address):
  365     """Handle an error gracefully. use stderr instead of stdout
  366     to avoid double fault.
  367     """
  368     sys.stderr.write('-'*40 + '\n')
  369     sys.stderr.write('Exception happened during processing of request from '
  370                      '%s\n' % str(client_address))
  371     import traceback
  372     traceback.print_exc()
  373     sys.stderr.write('-'*40 + '\n')
  374 
  375 
  376 def serve(host, port, callback=None):
  377   """Start an HTTP server for HOST on PORT.  Call CALLBACK function
  378   when the server is ready to serve."""
  379 
  380   ViewVCHTTPServer.handler = ViewVCHTTPRequestHandler
  381 
  382   try:
  383     # XXX Move this code out of this function.
  384     # Early loading of configuration here.  Used to allow tinkering
  385     # with some configuration settings:
  386     handle_config(options.config_file)
  387     if options.repositories:
  388       cfg.general.default_root = "Development"
  389       for repo_name in options.repositories.keys():
  390         repo_path = os.path.normpath(options.repositories[repo_name])
  391         if os.path.exists(os.path.join(repo_path, "CVSROOT", "config")):
  392           cfg.general.cvs_roots[repo_name] = repo_path
  393         elif os.path.exists(os.path.join(repo_path, "format")):
  394           cfg.general.svn_roots[repo_name] = repo_path
  395     elif cfg.general.cvs_roots.has_key("Development") and \
  396          not os.path.isdir(cfg.general.cvs_roots["Development"]):
  397       sys.stderr.write("*** No repository found. Please use the -r option.\n")
  398       sys.stderr.write("   Use --help for more info.\n")
  399       raise KeyboardInterrupt # Hack!
  400     os.close(0) # To avoid problems with shell job control
  401 
  402     # always use default docroot location
  403     cfg.options.docroot = None
  404 
  405     # if cvsnt isn't found, fall back to rcs
  406     if (cfg.conf_path is None and cfg.utilities.cvsnt):
  407       import popen
  408       cvsnt_works = 0
  409       try:
  410         fp = popen.popen(cfg.utilities.cvsnt, ['--version'], 'rt')
  411         try:
  412           while 1:
  413             line = fp.readline()
  414             if not line:
  415               break
  416             if line.find("Concurrent Versions System (CVSNT)") >= 0:
  417               cvsnt_works = 1
  418               while fp.read(4096):
  419                 pass
  420               break
  421         finally:
  422           fp.close()
  423       except:
  424         pass
  425       if not cvsnt_works:
  426         cfg.utilities.cvsnt = None
  427 
  428     ViewVCHTTPServer(host, port, callback).serve_until_quit()
  429   except (KeyboardInterrupt, select.error):
  430     pass
  431   print 'server stopped'
  432 
  433 
  434 def handle_config(config_file):
  435   global cfg
  436   cfg = viewvc.load_config(config_file or CONF_PATHNAME)
  437 
  438 
  439 def usage():
  440   clean_options = Options()
  441   cmd = os.path.basename(sys.argv[0])
  442   port = clean_options.port
  443   host = clean_options.host
  444   script_alias = clean_options.script_alias
  445   sys.stderr.write("""Usage: %(cmd)s [OPTIONS]
  446 
  447 Run a simple, standalone HTTP server configured to serve up ViewVC requests.
  448 
  449 Options:
  450 
  451   --config-file=FILE (-c)    Read configuration options from FILE.  If not
  452                              specified, ViewVC will look for a configuration
  453                              file in its installation tree, falling back to
  454                              built-in default values.
  455                              
  456   --daemon (-d)              Background the server process.
  457 
  458   --help                     Show this usage message and exit.
  459   
  460   --host=HOSTNAME (-h)       Listen on HOSTNAME.  Required for access from a
  461                              remote machine.  [default: %(host)s]
  462 
  463   --htpasswd-file=FILE       Authenticate incoming requests, validating against
  464                              against FILE, which is an Apache HTTP Server
  465                              htpasswd file.  (CRYPT only; no DIGEST support.)
  466 
  467   --port=PORT (-p)           Listen on PORT.  [default: %(port)d]
  468 
  469   --repository=PATH (-r)     Serve the Subversion or CVS repository located
  470                              at PATH.  This option may be used more than once.
  471 
  472   --script-alias=PATH (-s)   Use PATH as the virtual script location (similar
  473                              to Apache HTTP Server's ScriptAlias directive).
  474                              For example, "--script-alias=repo/view" will serve
  475                              ViewVC at "http://HOSTNAME:PORT/repo/view".
  476                              [default: %(script_alias)s]
  477 """ % locals())
  478   sys.exit(0)
  479 
  480 
  481 def badusage(errstr):
  482   cmd = os.path.basename(sys.argv[0])
  483   sys.stderr.write("ERROR: %s\n\n"
  484                    "Try '%s --help' for detailed usage information.\n"
  485                    % (errstr, cmd))
  486   sys.exit(1)
  487 
  488 
  489 def main(argv):
  490   """Command-line interface (looks at argv to decide what to do)."""
  491   import getopt
  492 
  493   short_opts = ''.join(['c:',
  494                         'd',
  495                         'h:',
  496                         'p:',
  497                         'r:',
  498                         's:',
  499                         ])
  500   long_opts = ['daemon',
  501                'config-file=',
  502                'help',
  503                'host=',
  504                'htpasswd-file=',
  505                'port=',
  506                'repository=',
  507                'script-alias=',
  508                ]
  509     
  510   opt_daemon = False
  511   opt_host = None
  512   opt_port = None
  513   opt_htpasswd_file = None
  514   opt_config_file = None
  515   opt_script_alias = None
  516   opt_repositories = []
  517 
  518   # Parse command-line options.
  519   try:  
  520     opts, args = getopt.getopt(argv[1:], short_opts, long_opts)
  521     for opt, val in opts:
  522       if opt in ['--help']:
  523         usage()
  524       elif opt in ['-r', '--repository']: # may be used more than once
  525         opt_repositories.append(val)
  526       elif opt in ['-d', '--daemon']:
  527         opt_daemon = 1
  528       elif opt in ['-p', '--port']:
  529         opt_port = val
  530       elif opt in ['-h', '--host']:
  531         opt_host = val
  532       elif opt in ['-s', '--script-alias']:
  533         opt_script_alias = val
  534       elif opt in ['-c', '--config-file']:
  535         opt_config_file = val
  536       elif opt in ['--htpasswd-file']:
  537         opt_htpasswd_file = val
  538   except getopt.error, err:
  539     badusage(str(err))
  540 
  541   # Validate options that need validating.
  542   class BadUsage(Exception): pass
  543   try:
  544     if opt_port is not None:
  545       try:
  546         options.port = int(opt_port)
  547       except ValueError:
  548         raise BadUsage("Port '%s' is not a valid port number" % (opt_port))
  549       if not options.port:
  550         raise BadUsage("You must supply a valid port.")
  551     if opt_htpasswd_file is not None:
  552       if not os.path.isfile(opt_htpasswd_file):
  553         raise BadUsage("'%s' does not appear to be a valid htpasswd file."
  554                        % (opt_htpasswd_file))
  555       if not has_crypt:
  556         raise BadUsage("Unable to locate suitable `crypt' module for use "
  557                        "with --htpasswd-file option.  If your Python "
  558                        "distribution does not include this module (as is "
  559                        "the case on many non-Unix platforms), consider "
  560                        "installing the `fcrypt' module instead (see "
  561                        "http://carey.geek.nz/code/python-fcrypt/).")
  562       options.htpasswd_file = opt_htpasswd_file
  563     if opt_config_file is not None:
  564       if not os.path.isfile(opt_config_file):
  565         raise BadUsage("'%s' does not appear to be a valid configuration file."
  566                        % (opt_config_file))
  567       options.config_file = opt_config_file
  568     if opt_host is not None:
  569       options.host = opt_host
  570     if opt_script_alias is not None:
  571       options.script_alias = '/'.join(filter(None, opt_script_alias.split('/')))
  572     for repository in opt_repositories:
  573       if not options.repositories.has_key('Development'):
  574         rootname = 'Development'
  575       else:
  576         rootname = 'Repository%d' % (len(options.repositories.keys()) + 1)
  577       options.repositories[rootname] = repository
  578   except BadUsage, err:
  579     badusage(str(err))
  580       
  581   # Fork if we're in daemon mode.
  582   if opt_daemon:
  583     pid = os.fork()
  584     if pid != 0:
  585       sys.exit()
  586 
  587   # Finaly, start the server.
  588   def ready(server):
  589     print 'server ready at %s%s' % (server.url, options.script_alias)
  590   serve(options.host, options.port, ready)
  591 
  592 
  593 if __name__ == '__main__':
  594   options = Options()
  595   main(sys.argv)