httpserver: Add support for WSGI apps

For example, with Flask:

```
from supybot import utils, plugins, ircutils, callbacks, httpserver
from supybot.commands import *
from supybot.i18n import PluginInternationalization

_ = PluginInternationalization('TestWsgi')

from flask import Flask, request, render_template

app = Flask(__name__)

@app.route("/")
def hello_world():
    return "<p>Hello, World!</p>"

@app.route('/login/', methods=['POST', 'GET'])
def login():
    error = None
    if request.method == 'POST':
        if request.form['username'] == 'root' \
                and request.form['password'] == 'admin':
            return 'Hello root'
        else:
            return 'Error: Invalid username/password'
    # the code below is executed if the request method
    # was GET or the credentials were invalid
    return '''
        <form method="POST" action=".">
            <label for="username">Username:
                <input name="username" id="username" />
            </label>
            <label for="password">Password:
                <input name="password" id="password" type="password" />
            </label>
            <input type="Submit" />
        </form>
    '''

class TestWsgi(callbacks.Plugin):
    """Test Flask"""
    def __init__(self, irc):
        self.__parent = super(TestWsgi, self)
        callbacks.Plugin.__init__(self, irc)

        httpserver.hook('testwsgi', app)

    def die(self):
        self.__parent.die()
        httpserver.unhook('testwsgi')

Class = TestWsgi
```
This commit is contained in:
Valentin Lorentz 2022-10-31 22:13:34 +01:00
parent 4da1291876
commit ffb2e8488f

View File

@ -34,7 +34,10 @@ An embedded and centralized HTTP server for Supybot's plugins.
import os import os
import cgi import cgi
import socket import socket
import sys
from threading import Thread from threading import Thread
from wsgiref.handlers import BaseHandler, SimpleHandler
from wsgiref.simple_server import WSGIRequestHandler
import supybot.log as log import supybot.log as log
import supybot.conf as conf import supybot.conf as conf
@ -181,6 +184,22 @@ class SupyHTTPRequestHandler(BaseHTTPRequestHandler):
except KeyError: except KeyError:
callback = Supy404() callback = Supy404()
if not isinstance(callback, SupyHTTPServerCallback):
# WSGI-based callback
environ = WSGIRequestHandler.get_environ(self)
environ.update({
'SERVER_NAME': '0.0.0.0', # TODO
'SERVER_PORT': '80', # TODO
'PATH_INFO': '/' + environ['PATH_INFO'].split('/', 2)[2]
})
SimpleHandler(
stdin=self.rfile, stdout=self.wfile, stderr=sys.stderr, environ=environ,
multithread=False
).run(callback)
return
# BaseHTTPRequestHandler-based callback
# Some shortcuts # Some shortcuts
for name in ('send_response', 'send_header', 'end_headers', 'rfile', for name in ('send_response', 'send_header', 'end_headers', 'rfile',
'wfile', 'headers'): 'wfile', 'headers'):
@ -189,6 +208,22 @@ class SupyHTTPRequestHandler(BaseHTTPRequestHandler):
path = self.path path = self.path
if not callback.fullpath: if not callback.fullpath:
path = '/' + path.split('/', 2)[-1] path = '/' + path.split('/', 2)[-1]
if callback == 'doPost':
if 'Content-Type' not in self.headers:
self.headers['Content-Type'] = 'application/x-www-form-urlencoded'
if self.headers['Content-Type'] == 'application/x-www-form-urlencoded':
form = cgi.FieldStorage(
fp=self.rfile,
headers=self.headers,
environ={'REQUEST_METHOD':'POST',
'CONTENT_TYPE':self.headers['Content-Type'],
})
else:
content_length = int(self.headers.get('Content-Length', '0'))
form = self.rfile.read(content_length)
kwargs['form'] = form
getattr(callback, callbackMethod)(self, path, getattr(callback, callbackMethod)(self, path,
*args, **kwargs) *args, **kwargs)
@ -196,19 +231,7 @@ class SupyHTTPRequestHandler(BaseHTTPRequestHandler):
self.do_X('doGet') self.do_X('doGet')
def do_POST(self): def do_POST(self):
if 'Content-Type' not in self.headers: self.do_X('doPost')
self.headers['Content-Type'] = 'application/x-www-form-urlencoded'
if self.headers['Content-Type'] == 'application/x-www-form-urlencoded':
form = cgi.FieldStorage(
fp=self.rfile,
headers=self.headers,
environ={'REQUEST_METHOD':'POST',
'CONTENT_TYPE':self.headers['Content-Type'],
})
else:
content_length = int(self.headers.get('Content-Length', '0'))
form = self.rfile.read(content_length)
self.do_X('doPost', form=form)
def do_HEAD(self): def do_HEAD(self):
self.do_X('doHead') self.do_X('doHead')
@ -312,7 +335,7 @@ class SupyIndex(SupyHTTPServerCallback):
plugins = [ plugins = [
(name, cb) (name, cb)
for (name, cb) in handler.server.callbacks.items() for (name, cb) in handler.server.callbacks.items()
if cb.public] if getattr(cb, 'public', True)]
if plugins == []: if plugins == []:
plugins = _('No plugins available.') plugins = _('No plugins available.')
else: else:
@ -417,6 +440,9 @@ class RealSupyHTTPServer(HTTPServer):
timeout = 0.5 timeout = 0.5
running = False running = False
base_environ = {}
def __init__(self, address, protocol, callback): def __init__(self, address, protocol, callback):
self.protocol = protocol self.protocol = protocol
if protocol == 4: if protocol == 4:
@ -441,10 +467,11 @@ class RealSupyHTTPServer(HTTPServer):
'reloaded the plugin and it didn\'t properly unhook. ' 'reloaded the plugin and it didn\'t properly unhook. '
'Forced unhook.') % subdir) 'Forced unhook.') % subdir)
self.callbacks[subdir] = callback self.callbacks[subdir] = callback
callback.doHook(self, subdir) if hasattr(callback, 'doHook'):
callback.doHook(self, subdir)
def unhook(self, subdir): def unhook(self, subdir):
callback = self.callbacks.pop(subdir, None) callback = self.callbacks.pop(subdir, None)
if callback: if callback and hasattr(callback, 'doUnhook'):
callback.doUnhook(self) callback.doUnhook(self)
return callback return callback
@ -452,6 +479,8 @@ class RealSupyHTTPServer(HTTPServer):
return 'server at %s %i' % self.server_address[0:2] return 'server at %s %i' % self.server_address[0:2]
class TestSupyHTTPServer(RealSupyHTTPServer): class TestSupyHTTPServer(RealSupyHTTPServer):
base_environ = {}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.callbacks = {} self.callbacks = {}
self.server_address = ("0.0.0.0", 0) self.server_address = ("0.0.0.0", 0)