#!/usr/bin/env python

# -*- coding: utf-8; mode: python -*-
#
# Cherokee-admin
#
# Authors:
#      Alvaro Lopez Ortega <alvaro@alobbs.com>
#
# Copyright (C) 2001-2010 Alvaro Lopez Ortega
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
# License as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.
#

import re
import os
import sys
import time
import fcntl
import signal
import urllib2
import threading
import subprocess

from select import select


# Constants
ADMIN_HOST           = "localhost"
ADMIN_PORT           = 9090
ADMIN_LAUNCH_TIMEOUT = 15

# Paths
cherokee_admin_path = 'cherokee-admin'
runner              = None
exiting             = False

# Defaults
PATH_PREFIXES  = ['/usr', '/usr/local', '/opt/local', '/usr/gnu']
DEFAULT_PATHS  = [os.path.join (x,'sbin') for x in PATH_PREFIXES]
DEFAULT_PATHS += [os.path.join (x,'bin')  for x in PATH_PREFIXES]


def set_non_blocking (fd):
    fl = fcntl.fcntl (fd, fcntl.F_GETFL)
    fcntl.fcntl (fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)


def report_error (description, title="ERROR"):
    # MacOS X
    osascript_bin = bin_in_path ("osascript")
    if osascript_bin:
        dialog  = "display dialog \\\"%(description)s\\\" with title \\\"%(title)s\\\" buttons \\\"Close\\\" with icon stop" %(locals())
        command = "osascript -e 'tell application \"Finder\"' -e \"activate\" -e \"%(dialog)s\" -e 'end tell'" %(locals())
        os.system (command)

    # Text mode
    print '[%(title)s] - %(description)s'%(locals())


def build_command_as_root (cmd):
    assert type(cmd) == list

    command = cmd[:]
    env     = os.environ.copy()

    # If not root
    uid = os.getuid()
    if uid != 0:
        if not os.getenv ('SUDO_ASKPASS'):
            gaskpass_bin  = bin_in_path ('gaskpass')
            askpass_bin   = bin_in_path ("cherokee-macos-askpass")
            osascript_bin = bin_in_path ("osascript")
            X_display     = os.getenv ("DISPLAY")

            # GNOME: gaskpass
            if gaskpass_bin and X_display:
                env['SUDO_ASKPASS'] = gaskpass_bin

            # MacOS X: osascript
            elif osascript_bin and askpass_bin:
                env['SUDO_ASKPASS'] = askpass_bin

            # Error
            else:
                report_error ("Did not find a suitable SUDO_ASKPASS application")

            # Add sudo
            use_askpass = (env.get('SUDO_ASKPASS') != None)
            if use_askpass:
                command = [bin_in_path('sudo'), '-A'] + command

    return (command, env)


def run_as_root (cmd):
    cmd_env = build_command_as_root (cmd)
    command, env = cmd_env
    return subprocess.call (command, env=env, shell=False, close_fds=True)


class Admin_Runner (threading.Thread):
   def __init__ (self):
      threading.Thread.__init__ (self)
      self.daemon = True

      self.url        = ''
      self.user       = ''
      self.password   = ''
      self.command    = [cherokee_admin_path] + sys.argv[1:]
      self.needs_auth = True
      self.popen      = None

      self.launching      = True
      self.launching_lock = threading.Lock()
      self.launching_lock.acquire()

      # Detect -u, --unsecure, -<something>u
      self._check_auth()

   def _check_auth (self):
      # Detects --unsecure
      self.needs_auth = not ('--unsecure' in sys.argv)

      # Detects -u and -<something>u
      for arg in sys.argv:
          if len(arg) >= 2 and arg[0] == '-' and arg[1] != '-':
              if 'u' in arg:
                  self.needs_auth = False

   def run (self):
       cmd_env = build_command_as_root (self.command)
       command, environment = cmd_env

       p = subprocess.Popen (command,
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE,
                             env=environment, close_fds=True)
       self.popen = p

       stdout_f,  stderr_f  = (p.stdout, p.stderr)
       stdout_fd, stderr_fd = stdout_f.fileno(), stderr_f.fileno()
       stdout,    stderr    = '', ''

       set_non_blocking (stdout_fd)
       set_non_blocking (stderr_fd)

       global exiting
       while not exiting:
           # Poll I/O activity
           r,w,e = select([stdout_fd, stderr_fd], [], [stdout_fd, stderr_fd], 1)

           if e:
               break

           # Read output
           new_line = False

           if stdout_fd in r:
               data = stdout_f.read(1024)
               if not data: break
               os.write (sys.stdout.fileno(), data)
               stdout += data
               new_line = '\n' in data

           if stderr_fd in r:
               data = stderr_f.read(1024)
               if not data: break
               os.write (sys.stderr.fileno(), data)
               stderr += data
               new_line = '\n' in data

           # Launching status
           if not self.launching:
               continue

           got_info = bool(self.url)
           if self.needs_auth:
               got_info &= bool(self.user) and bool(self.password)

           if got_info:
               self.launching = False
               self.launching_lock.release()

               stdout = ''
               stderr = ''
               continue

           # Parse connection info
           if new_line:
               tmp = re.findall (r'\s+URL:\s+(http.+)\n', stdout)
               if tmp:
                   self.url = tmp[0]

               tmp = re.findall (r'\s+User:\s+(\w+)', stdout)
               if tmp:
                   self.user = tmp[0]

               tmp = re.findall (r'\s+One-time Password:\s+(\w+)', stdout)
               if tmp:
                   self.password = tmp[0]

       # Error or process exited
       exiting = True


def http_GET_admin (host=ADMIN_HOST, port=ADMIN_PORT, req='/'):
    URI = "http://%s:%s%s" %(host, port, req)
    try:
        resp = urllib2.urlopen (URI)
    except urllib2.URLError, e:
        description = str(e)
        for key in ('61,', '111,', 'connection refused'):
            if key in description.lower():
                return False
        return description
    except Exception, e:
        return str(e)

    content = resp.read()
    return content


_bin_in_path_cache = {}
def bin_in_path (bin):
    global _bin_in_path_cache

    # Cache hit
    if bin in _bin_in_path_cache:
        return _bin_in_path_cache[bin]

    # Cache miss: check it out
    paths = os.getenv('PATH').split(':') + DEFAULT_PATHS

    for e in paths:
        fp = os.path.join (e, bin)
        if os.access (fp, os.X_OK):
            _bin_in_path_cache[bin] = fp
            return fp

    _bin_in_path_cache[bin] = False


def launch_browser (url, user, password):
   if user and password:
      host = re.findall (r'http://(.+)/', url)[0]
      URI = 'http://%(user)s:%(password)s@%(host)s/' %(locals())
   else:
      URI = url

   # MacOS X
   if os.access ("/usr/bin/open", os.X_OK):
      os.system ("open '%(URI)s'" %(locals()))
   # LSB
   elif bin_in_path ('xdg-open'):
      os.system ("xdg-open '%(URI)s'" %(locals()))
   # KDE
   elif bin_in_path ('kfmclient'):
      os.system ("kfmclient openURL '%(URI)s'" %(locals()))
   # Gnome
   elif bin_in_path ('gnome-open'):
      os.system ("gnome-open '%(URI)s'" %(locals()))

   # Error
   else:
       report_error ("Did not find a way to open: %(url)s" %(locals()))


def find_cherokee_admin():
   global cherokee_admin_path

   path = os.path.abspath (os.path.realpath (__file__) + '/../cherokee-admin')
   if os.path.exists (path):
      cherokee_admin_path = path
      return

   path = os.path.abspath (os.path.realpath (__file__) + '/../../sbin/cherokee-admin')
   if os.path.exists (path):
      cherokee_admin_path = path
      return

   report_error ("Could not find cherokee-admin")


def quit_signal (num, stack):
    global exiting
    exiting = True
    print
    print "Cherokee-admin-launcher exiting.."


def main():
   # Find cherokee-admin
   find_cherokee_admin()

   # Ensure port is empty
   print "Checking TCP port %(ADMIN_PORT)s availability.."%(globals()),
   response = http_GET_admin()
   if not response:
       print "OK"
   else:
       print
       report_error ("The 9090 port is already in use.")
       raise SystemExit

   # Launch Cherokee-admin
   global runner
   runner = Admin_Runner()
   runner.start()

   print "Launching %s.." %(' '.join (runner.command))
   runner.launching_lock.acquire()

   # Wait for it to be available
   print "Connecting..",
   wait_timeout = time.time() + ADMIN_LAUNCH_TIMEOUT

   while True:
       response = http_GET_admin()
       if response:
           print "OK"
           break
       if time.time() < wait_timeout:
           time.sleep(0.3)
       else:
           print "Timeout"
           return

   # Launching browser
   print "Launching browser..",
   launch_browser (runner.url, runner.user, runner.password)
   print "OK"

   # Wait for it to finish
   while not exiting:
       time.sleep (.1)

   # Exiting
   if runner and runner.popen and runner.popen.pid:
       kill_command = ['/bin/sh', '-c', 'kill %s'%(runner.popen.pid)]
       run_as_root (kill_command)
       try:
           os.waitpid (runner.popen.pid, 0)
       except OSError:
           pass

if __name__ == '__main__':
   if '--help' in sys.argv:
      os.system ('%(cherokee_admin_path)s --help' %(globals()))
      raise SystemExit

   # Set signal handlers
   signal.signal (signal.SIGINT,  quit_signal)
   signal.signal (signal.SIGTERM, quit_signal)

   # The real deal
   main()
