Procházet zdrojové kódy

- Allow "web handler" (the handler which receives http requests from
browsers visiting the web UI of supervisor) to deal with POST requests.

Chris McDonough před 17 roky
rodič
revize
70bd7a239a

+ 3 - 0
CHANGES.txt

@@ -184,6 +184,9 @@ Next Release
    memmon.py, which works on both Linux and Mac OS.  This script is
    now a console script named "memmon".
 
+ - Allow "web handler" (the handler which receives http requests from 
+   browsers visiting the web UI of supervisor) to deal with POST requests.
+
 3.0a3
 
   - Supervisorctl now reports a better error message when the main

+ 6 - 2
src/supervisor/http.py

@@ -26,6 +26,7 @@ from medusa import http_date
 from medusa import http_server
 from medusa import producers
 from medusa import filesys
+from medusa import default_handler
 
 from medusa.auth_handler import auth_handler
 
@@ -807,10 +808,11 @@ def make_http_servers(options, supervisord):
         xmlrpchandler = supervisor_xmlrpc_handler(supervisord, subinterfaces)
         tailhandler = logtail_handler(supervisord)
         maintailhandler = mainlogtail_handler(supervisord)
+        uihandler = supervisor_ui_handler(supervisord)
         here = os.path.abspath(os.path.dirname(__file__))
         templatedir = os.path.join(here, 'ui')
         filesystem = filesys.os_filesystem(templatedir)
-        uihandler = supervisor_ui_handler(filesystem, supervisord)
+        defaulthandler = default_handler.default_handler(filesystem)
 
         username = config['username']
         password = config['password']
@@ -823,12 +825,14 @@ def make_http_servers(options, supervisord):
             tailhandler = supervisor_auth_handler(users, tailhandler)
             maintailhandler = supervisor_auth_handler(users, maintailhandler)
             uihandler = supervisor_auth_handler(users, uihandler)
+            defaulthandler = supervisor_auth_handler(users, defaulthandler)
         else:
             options.logger.critical(
                 'Server %r running without any HTTP '
                 'authentication checking' % config['section'])
-        # uihandler must be consulted last as its match method matches
+        # defaulthandler must be consulted last as its match method matches
         # everything, so it's first here (indicating last checked)
+        hs.install_handler(defaulthandler)
         hs.install_handler(uihandler)
         hs.install_handler(maintailhandler)
         hs.install_handler(tailhandler)

+ 12 - 1
src/supervisor/tests/base.py

@@ -526,13 +526,17 @@ class DummyRequest:
     _error = None
     _done = False
     version = '1.0'
-    def __init__(self, path, params, query, fragment):
+    def __init__(self, path, params, query, fragment, env=None):
         self.args = path, params, query, fragment
         self.producers = []
         self.headers = {}
         self.header = []
         self.outgoing = []
         self.channel = DummyMedusaChannel()
+        if env is None:
+            self.env = {}
+        else:
+            self.env = env
 
     def split_uri(self):
         return self.args
@@ -558,6 +562,13 @@ class DummyRequest:
     def log(self, *arg, **kw):
         pass
 
+    def cgi_environment(self):
+        return self.env
+
+    def get_server_url(self):
+        return 'http://example.com'
+        
+
 class DummyRPCInterfaceFactory:
     def __init__(self, supervisord, **config):
         self.supervisord = supervisord

+ 9 - 4
src/supervisor/tests/test_http.py

@@ -333,14 +333,19 @@ class TopLevelFunctionTests(unittest.TestCase):
         inetdata = servers[0]
         self.assertEqual(inetdata[0], inet)
         server = inetdata[1]
-        paths = ['/RPC2', '/logtail', '/mainlogtail', '']
-        self.assertEqual([x.path for x in server.handlers], paths)
+        idents = [
+            'Supervisor XML-RPC Handler',
+            'Logtail HTTP Request Handler',
+            'Main Logtail HTTP Request Handler',
+            'Supervisor Web UI HTTP Request Handler',
+            'Default HTTP Request Handler'
+            ]
+        self.assertEqual([x.IDENT for x in server.handlers], idents)
 
         unixdata = servers[1]
         self.assertEqual(unixdata[0], unix)
         server = unixdata[1]
-        paths = ['/RPC2', '/logtail', '/mainlogtail', '']
-        self.assertEqual([x.path for x in server.handlers], paths)
+        self.assertEqual([x.IDENT for x in server.handlers], idents)
 
     def test_make_http_servers_withauth(self):
         socketfile = tempfile.mktemp()

+ 36 - 51
src/supervisor/tests/test_web.py

@@ -76,43 +76,19 @@ class UIHandlerTests(unittest.TestCase):
         return supervisor_ui_handler
 
     def _makeOne(self):
-        filesystem = DummyFilesystem()
         supervisord = DummySupervisor()
-        handler = self._getTargetClass()(filesystem, supervisord)
+        handler = self._getTargetClass()(supervisord)
         return handler
 
-    def test_get_view_index_html(self):
-        request = DummyRequest('/index.html', [], '', '')
-        handler = self._makeOne()
-        viewdata = handler.get_view(request)
-        from supervisor.web import StatusView
-        self.assertEqual(viewdata['template'], 'ui/status.html')
-        self.assertEqual(viewdata['view'], StatusView)
-
-    def test_get_view_tail_html(self):
-        request = DummyRequest('/tail.html', [], '', '')
-        handler = self._makeOne()
-        viewdata = handler.get_view(request)
-        from supervisor.web import TailView
-        self.assertEqual(viewdata['template'], 'ui/tail.html')
-        self.assertEqual(viewdata['view'], TailView)
-
-    def test_get_view_default(self):
-        request = DummyRequest('/', [], '', '')
-        handler = self._makeOne()
-        viewdata = handler.get_view(request)
-        from supervisor.web import StatusView
-        self.assertEqual(viewdata['template'], 'ui/status.html')
-        self.assertEqual(viewdata['view'], StatusView)
-
     def test_handle_request_no_view_method(self):
-        request = DummyRequest('/foo.css', [], '', '')
+        request = DummyRequest('/foo.css', [], '', '', {'PATH_INFO':'/foo.css'})
         handler = self._makeOne()
         data = handler.handle_request(request)
         self.assertEqual(data, None)
         
-    def test_handle_request_view_method(self):
-        request = DummyRequest('/index.html', [], '', '')
+    def test_handle_request_default(self):
+        request = DummyRequest('/index.html', [], '', '',
+                               {'PATH_INFO':'/index.html'})
         handler = self._makeOne()
         data = handler.handle_request(request)
         self.assertEqual(data, None)
@@ -120,14 +96,36 @@ class UIHandlerTests(unittest.TestCase):
         from supervisor.web import StatusView
         self.assertEqual(request.channel.producer.callback.__class__,StatusView)
 
-    def test_do_view_request(self):
-        request = DummyRequest('/index.html', [], '', '')
+    def test_handle_request_index_html(self):
+        request = DummyRequest('/index.html', [], '', '',
+                               {'PATH_INFO':'/index.html'})
         handler = self._makeOne()
+        data = handler.handle_request(request)
         from supervisor.web import StatusView
-        viewdata = {'template':'ui/status.html', 'view':StatusView}
-        handler.do_view_request(viewdata, request)
-        self.assertEqual(request.channel.producer.request, request)
-        self.assertEqual(request.channel.producer.callback.__class__,StatusView)
+        view = request.channel.producer.callback
+        self.assertEqual(view.__class__, StatusView)
+        self.assertEqual(view.context.template, 'ui/status.html')
+
+    def test_handle_request_tail_html(self):
+        request = DummyRequest('/tail.html', [], '', '',
+                               {'PATH_INFO':'/tail.html'})
+        handler = self._makeOne()
+        data = handler.handle_request(request)
+        from supervisor.web import TailView
+        view = request.channel.producer.callback
+        self.assertEqual(view.__class__, TailView)
+        self.assertEqual(view.context.template, 'ui/tail.html')
+
+    def test_handle_request_ok_html(self):
+        request = DummyRequest('/tail.html', [], '', '',
+                               {'PATH_INFO':'/ok.html'})
+        handler = self._makeOne()
+        data = handler.handle_request(request)
+        from supervisor.web import OKView
+        view = request.channel.producer.callback
+        self.assertEqual(view.__class__, OKView)
+        self.assertEqual(view.context.template, None)
+
 
 class StatusViewTests(unittest.TestCase):
     def _getTargetClass(self):
@@ -142,6 +140,7 @@ class StatusViewTests(unittest.TestCase):
         context = DummyContext()
         context.supervisord = DummySupervisor()
         context.template = 'ui/status.html'
+        context.form = {}
         view = self._makeOne(context)
         self.assertRaises(ValueError, view.make_callback, 'process', None)
 
@@ -150,6 +149,7 @@ class StatusViewTests(unittest.TestCase):
         context.supervisord = DummySupervisor()
         context.template = 'ui/status.html'
         context.request = DummyRequest('/foo', [], '', '')
+        context.form = {}
         context.response = {}
         view = self._makeOne(context)
         data = view.render()
@@ -159,8 +159,8 @@ class StatusViewTests(unittest.TestCase):
         context = DummyContext()
         context.supervisord = DummySupervisor()
         context.template = 'ui/status.html'
-        context.request = DummyRequest('/foo', [], '?action=refresh', '')
         context.response = {}
+        context.form = {'action':'refresh'}
         view = self._makeOne(context)
         data = view.render()
         from supervisor.http import NOT_DONE_YET
@@ -169,21 +169,6 @@ class StatusViewTests(unittest.TestCase):
 class DummyContext:
     pass
 
-class DummyFilesystem:
-    def __init__(self):
-        self.stat_return = (16877, 12515682L, 234881026L, 24, 501, 501,
-                            816L, 1187846057, 1187819244, 1187819244)
-        import StringIO
-        self.file = StringIO.StringIO()
-    def isdir(self, path):
-        return False
-    def isfile(self, path):
-        return True
-    def stat(self, path):
-        return self.stat_return
-    def open(self, *arg, **kw):
-        return self.file
-
 def test_suite():
     return unittest.findTestCases(sys.modules[__name__])
 

+ 8 - 4
src/supervisor/ui/status.html

@@ -17,10 +17,14 @@
     <div class="hidden" meld:id="statusmessage">#</div>
 
     <form meld:id="form" action="index.html" method="post">
-      <ul id="buttons" class="clr">
-        <li id="refresh"><a href="/index.html?action=refresh">&amp;nbsp;</a></li>
-        <li id="restart_all"><a href="/index.html?action=restartall">&amp;nbsp;</a></li>
-        <li id="stop_all"><a href="/index.html?action=stopall">&amp;nbsp;</a></li>
+
+      <ul class="clr" id="buttons">
+        <li id="refresh"><button type="submit" name="action"
+                  value="refresh"/>&amp;nbsp;</li>
+        <li id="restart_all"><button type="submit" name="action"
+                  value="restartall"/>&amp;nbsp;</li>
+        <li id="stop_all"><button type="submit" name="action"
+                 value="stopall"/>&amp;nbsp;</li>
       </ul>
 
       <table cellspacing="0" meld:id="statustable">

+ 20 - 14
src/supervisor/ui/stylesheets/supervisor.css

@@ -89,6 +89,7 @@ a:hover {
   padding: 10px 0 13px 0;
   background: url("../images/rule.gif") left bottom repeat-x;
 }
+
 .status_msg {
   padding: 5px 10px;
   border: 1px solid #919191;
@@ -99,39 +100,44 @@ a:hover {
 #buttons {
   margin: 13px 0;
 }
+
 #buttons li {
   float: left;
   display: block;
   margin: 0 7px 0 0;
 }
-#buttons a {
-  float: left;
-  display: block;
-  padding: 1px 0 0 0;
-}
-#buttons a, #buttons a:link {
-  text-decoration: none;
+
+#buttons button {
+   float: left;
+   display: block;
+   padding: 1px 0 0 0 0;
+   text-decoration: none;
 }
 
-#refresh a {
+#refresh button {
   width: 62px;
   background: url("../images/button_refresh.gif") 0 0 no-repeat;
 }
-#refresh a:hover {
+
+#refresh button:hover {
   background-position: -62px;
 }
-#restart_all a {
+
+#restart_all button {
   width: 84px;
   background: url("../images/button_restart.gif") 0 0 no-repeat;
 }
-#restart_all a:hover {
+
+#restart_all button:hover {
   background-position: -84px;
 }
-#stop_all a {
-  width: 65px;
+
+#stop_all button {
+  width: 64px;
   background: url("../images/button_stop.gif") 0 0 no-repeat;
 }
-#stop_all a:hover {
+
+#stop_all button:hover {
   background-position: -65px;
 }
 

+ 61 - 44
src/supervisor/web.py

@@ -21,10 +21,10 @@ import urllib
 import datetime
 import StringIO
 
-from medusa import default_handler
 from medusa import producers
 from medusa.http_server import http_date
 from medusa.http_server import get_header
+from medusa.xmlrpc_handler import collector
 
 import meld3
 
@@ -194,18 +194,13 @@ class MeldView:
 class TailView(MeldView):
     def render(self):
         supervisord = self.context.supervisord
-        request = self.context.request
+        form = self.context.form
 
-        path, params, query, fragment = request.split_uri()
-
-        if not query:
+        if not 'processname' in form:
             tail = 'No process name found'
             processname = None
         else:
-            while query.startswith('?'):
-                query = query[1:]
-            params = cgi.parse_qs(query)
-            processname = params.get('processname',[None])[0]
+            processname = form['processname']
 
             if not processname:
                 tail = 'No process name found'
@@ -390,18 +385,11 @@ class StatusView(MeldView):
         raise ValueError(action)
     
     def render(self):
-        request = self.context.request
+        form = self.context.form
         response = self.context.response
-        path, params, query, fragment = request.split_uri()
-
-        if query:
-            while query.startswith('?'):
-                query = query[1:]
-
-        qparams = cgi.parse_qs(query or '')
-        processname = qparams.get('processname',[None])[0]
-        action = qparams.get('action', [None])[0]
-        message = qparams.get('message', [None])[0]
+        processname = form.get('processname')
+        action = form.get('action')
+        message = form.get('message')
 
         if action:
             if not self.callback:
@@ -413,7 +401,7 @@ class StatusView(MeldView):
                 if message is NOT_DONE_YET:
                     return NOT_DONE_YET
                 if message is not None:
-                    server_url = request.get_server_url()
+                    server_url = form['SERVER_URL']
                     location = server_url + '?message=%s' % urllib.quote(
                         message)
                     response['headers']['Location'] = location
@@ -523,49 +511,78 @@ VIEWS = {
     }
 
 
-class supervisor_ui_handler(default_handler.default_handler):
+class supervisor_ui_handler:
     IDENT = 'Supervisor Web UI HTTP Request Handler'
-    path = ''
 
-    def __init__(self, filesystem, supervisord):
+    def __init__(self, supervisord):
         self.supervisord = supervisord
-        default_handler.default_handler.__init__(self, filesystem)
 
     def match(self, request):
-        return request.uri.startswith(self.path)
+        if request.command not in ('POST', 'GET'):
+            return False
 
-    def get_view(self, request):
         path, params, query, fragment = request.split_uri()
 
-        if '%' in path:
-            path = cgi.unquote(path)
-
-        # strip off all leading slashes
-        while path and path[0] == '/':
+        while path.startswith('/'):
             path = path[1:]
 
         if not path:
             path = 'index.html'
-
-        viewdata = VIEWS.get(path)
-        return viewdata
+            
+        for viewname in VIEWS.keys():
+            if viewname == path:
+                return True
 
     def handle_request(self, request):
-        viewdata = self.get_view(request)
-        if viewdata:
-            self.do_view_request(viewdata, request)
+        if request.command == 'POST':
+            request.collector = collector(self, request)
         else:
-            return default_handler.default_handler.handle_request(self, request)
+            self.continue_request('', request)
+
+    def continue_request (self, data, request):
+        form = {}
+        cgi_env = request.cgi_environment()
+        form.update(cgi_env)
+        if not form.has_key('QUERY_STRING'):
+            form['QUERY_STRING'] = ''
+
+        query = form['QUERY_STRING']
+
+        # we only handle x-www-form-urlencoded values from POSTs
+        form_urlencoded = cgi.parse_qsl(data)
+        query_data = cgi.parse_qs(query)
+
+        for k, v in query_data.items():
+            # ignore dupes
+            form[k] = v[0]
+
+        for k, v in form_urlencoded:
+            # ignore dupes
+            form[k] = v
+
+        form['SERVER_URL'] = request.get_server_url()
+
+        path = form['PATH_INFO']
+        # strip off all leading slashes
+        while path and path[0] == '/':
+            path = path[1:]
+        if not path:
+            path = 'index.html'
+
+        viewinfo = VIEWS.get(path)
+        if viewinfo is None:
+            # this should never happen if our match method works
+            return
 
-    def do_view_request(self, viewdata, request):
         response = {}
         response['headers'] = {}
 
-        viewclass = viewdata['view']
-        viewtemplate = viewdata['template']
+        viewclass = viewinfo['view']
+        viewtemplate = viewinfo['template']
         context = ViewContext(template=viewtemplate,
-                              request=request,
-                              response=response,
+                              request = request,
+                              form = form,
+                              response = response,
                               supervisord=self.supervisord)
         view = viewclass(context)
         pushproducer = request.channel.push_with_producer

+ 1 - 0
src/supervisor/xmlrpc.py

@@ -317,6 +317,7 @@ class RootRPCInterface:
 
 class supervisor_xmlrpc_handler(xmlrpc_handler):
     path = '/RPC2'
+    IDENT = 'Supervisor XML-RPC Handler'
     def __init__(self, supervisord, subinterfaces):
         self.rpcinterface = RootRPCInterface(subinterfaces)
         self.supervisord = supervisord