From ffb2e8488f703dd145ddcfd5d8ec39d5aba91ad6 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Mon, 31 Oct 2022 22:13:34 +0100 Subject: [PATCH 1/3] 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 "

Hello, World!

" @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 '''
''' 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 ``` --- src/httpserver.py | 61 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 45 insertions(+), 16 deletions(-) diff --git a/src/httpserver.py b/src/httpserver.py index 3b150fc11e..0fe1f02f48 100644 --- a/src/httpserver.py +++ b/src/httpserver.py @@ -34,7 +34,10 @@ import os import cgi import socket +import sys from threading import Thread +from wsgiref.handlers import BaseHandler, SimpleHandler +from wsgiref.simple_server import WSGIRequestHandler import supybot.log as log import supybot.conf as conf @@ -181,6 +184,22 @@ def do_X(self, callbackMethod, *args, **kwargs): except KeyError: 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 for name in ('send_response', 'send_header', 'end_headers', 'rfile', 'wfile', 'headers'): @@ -189,6 +208,22 @@ def do_X(self, callbackMethod, *args, **kwargs): path = self.path if not callback.fullpath: 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, *args, **kwargs) @@ -196,19 +231,7 @@ def do_GET(self): self.do_X('doGet') def do_POST(self): - 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) - self.do_X('doPost', form=form) + self.do_X('doPost') def do_HEAD(self): self.do_X('doHead') @@ -312,7 +335,7 @@ def doGetOrHead(self, handler, path, write_content): plugins = [ (name, cb) for (name, cb) in handler.server.callbacks.items() - if cb.public] + if getattr(cb, 'public', True)] if plugins == []: plugins = _('No plugins available.') else: @@ -417,6 +440,9 @@ class RealSupyHTTPServer(HTTPServer): timeout = 0.5 running = False + + base_environ = {} + def __init__(self, address, protocol, callback): self.protocol = protocol if protocol == 4: @@ -441,10 +467,11 @@ def hook(self, subdir, callback): 'reloaded the plugin and it didn\'t properly unhook. ' 'Forced unhook.') % subdir) self.callbacks[subdir] = callback - callback.doHook(self, subdir) + if hasattr(callback, 'doHook'): + callback.doHook(self, subdir) def unhook(self, subdir): callback = self.callbacks.pop(subdir, None) - if callback: + if callback and hasattr(callback, 'doUnhook'): callback.doUnhook(self) return callback @@ -452,6 +479,8 @@ def __str__(self): return 'server at %s %i' % self.server_address[0:2] class TestSupyHTTPServer(RealSupyHTTPServer): + base_environ = {} + def __init__(self, *args, **kwargs): self.callbacks = {} self.server_address = ("0.0.0.0", 0) From e1b2d8f26e0bc531494096b7a8716eb136750f18 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Tue, 1 Nov 2022 16:48:53 +0100 Subject: [PATCH 2/3] Set SERVER_NAME and SERVER_PORT --- src/httpserver.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/httpserver.py b/src/httpserver.py index 0fe1f02f48..2e00da762f 100644 --- a/src/httpserver.py +++ b/src/httpserver.py @@ -188,8 +188,6 @@ def do_X(self, callbackMethod, *args, **kwargs): # 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( @@ -440,9 +438,6 @@ class RealSupyHTTPServer(HTTPServer): timeout = 0.5 running = False - - base_environ = {} - def __init__(self, address, protocol, callback): self.protocol = protocol if protocol == 4: @@ -451,7 +446,15 @@ def __init__(self, address, protocol, callback): self.address_family = socket.AF_INET6 else: raise AssertionError(protocol) + HTTPServer.__init__(self, address, callback) + + host, port = self.server_address[:2] + self.base_environ = { + 'SERVER_NAME': socket.getfqdn(host), + 'SERVER_PORT': str(port), + } + self.callbacks = DEFAULT_CALLBACKS.copy() def server_bind(self): From 7486ae271e4ada49dbb070c7f5bf14ea9aeefb9f Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Tue, 1 Nov 2022 18:32:07 +0100 Subject: [PATCH 3/3] Fix crash on /pluginname path --- src/httpserver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/httpserver.py b/src/httpserver.py index 2e00da762f..fc0f492893 100644 --- a/src/httpserver.py +++ b/src/httpserver.py @@ -188,7 +188,7 @@ def do_X(self, callbackMethod, *args, **kwargs): # WSGI-based callback environ = WSGIRequestHandler.get_environ(self) environ.update({ - 'PATH_INFO': '/' + environ['PATH_INFO'].split('/', 2)[2] + 'PATH_INFO': '/' + (environ['PATH_INFO'].split('/', 2) + [''])[2] }) SimpleHandler( stdin=self.rfile, stdout=self.wfile, stderr=sys.stderr, environ=environ,