diff options
Diffstat (limited to 'libmproxy')
-rw-r--r-- | libmproxy/console/flowlist.py | 8 | ||||
-rw-r--r-- | libmproxy/console/grideditor.py | 16 | ||||
-rw-r--r-- | libmproxy/console/help.py | 10 | ||||
-rw-r--r-- | libmproxy/console/searchable.py | 4 | ||||
-rw-r--r-- | libmproxy/console/window.py | 2 | ||||
-rw-r--r-- | libmproxy/filt.py | 21 | ||||
-rw-r--r-- | libmproxy/flow.py | 27 | ||||
-rw-r--r-- | libmproxy/protocol/http.py | 88 | ||||
-rw-r--r-- | libmproxy/proxy/config.py | 18 | ||||
-rw-r--r-- | libmproxy/proxy/server.py | 12 | ||||
-rw-r--r-- | libmproxy/script.py | 86 | ||||
-rw-r--r-- | libmproxy/version.py | 4 | ||||
-rw-r--r-- | libmproxy/web/app.py | 3 |
13 files changed, 179 insertions, 120 deletions
diff --git a/libmproxy/console/flowlist.py b/libmproxy/console/flowlist.py index bb23df75..46cd0de1 100644 --- a/libmproxy/console/flowlist.py +++ b/libmproxy/console/flowlist.py @@ -50,9 +50,9 @@ class EventListBox(urwid.ListBox): self.master.clear_events() key = None elif key == "G": - self.set_focus(0) - elif key == "g": self.set_focus(len(self.master.eventlist) - 1) + elif key == "g": + self.set_focus(0) return urwid.ListBox.keypress(self, size, key) @@ -338,10 +338,10 @@ class FlowListBox(urwid.ListBox): self.master.clear_flows() elif key == "e": self.master.toggle_eventlog() - elif key == "G": + elif key == "g": self.master.state.set_focus(0) signals.flowlist_change.send(self) - elif key == "g": + elif key == "G": self.master.state.set_focus(self.master.state.flow_count()) signals.flowlist_change.send(self) elif key == "l": diff --git a/libmproxy/console/grideditor.py b/libmproxy/console/grideditor.py index b20e54e4..d32ce5b4 100644 --- a/libmproxy/console/grideditor.py +++ b/libmproxy/console/grideditor.py @@ -5,9 +5,11 @@ import re import os import urwid +from netlib import odict +from netlib.http import user_agents + from . import common, signals from .. import utils, filt, script -from netlib import http_uastrings, http_cookies, odict FOOTER = [ @@ -416,9 +418,9 @@ class GridEditor(urwid.WidgetWrap): res.append(i[0]) self.callback(self.data_out(res), *self.cb_args, **self.cb_kwargs) signals.pop_view_state.send(self) - elif key == "G": - self.walker.set_focus(0) elif key == "g": + self.walker.set_focus(0) + elif key == "G": self.walker.set_focus(len(self.walker.lst) - 1) elif key in ["h", "left"]: self.walker.left() @@ -516,7 +518,7 @@ class HeaderEditor(GridEditor): return text def set_user_agent(self, k): - ua = http_uastrings.get_by_shortcut(k) + ua = user_agents.get_by_shortcut(k) if ua: self.walker.add_value( [ @@ -529,7 +531,7 @@ class HeaderEditor(GridEditor): if key == "U": signals.status_prompt_onekey.send( prompt = "Add User-Agent header:", - keys = [(i[0], i[1]) for i in http_uastrings.UASTRINGS], + keys = [(i[0], i[1]) for i in user_agents.UASTRINGS], callback = self.set_user_agent, ) return True @@ -592,7 +594,7 @@ class SetHeadersEditor(GridEditor): return text def set_user_agent(self, k): - ua = http_uastrings.get_by_shortcut(k) + ua = user_agents.get_by_shortcut(k) if ua: self.walker.add_value( [ @@ -606,7 +608,7 @@ class SetHeadersEditor(GridEditor): if key == "U": signals.status_prompt_onekey.send( prompt = "Add User-Agent header:", - keys = [(i[0], i[1]) for i in http_uastrings.UASTRINGS], + keys = [(i[0], i[1]) for i in user_agents.UASTRINGS], callback = self.set_user_agent, ) return True diff --git a/libmproxy/console/help.py b/libmproxy/console/help.py index 4e81a566..ba87348d 100644 --- a/libmproxy/console/help.py +++ b/libmproxy/console/help.py @@ -28,7 +28,7 @@ class HelpView(urwid.ListBox): keys = [ ("j, k", "down, up"), ("h, l", "left, right (in some contexts)"), - ("g, G", "go to end, beginning"), + ("g, G", "go to beginning, end"), ("space", "page down"), ("pg up/down", "page up/down"), ("arrows", "up, down, left, right"), @@ -42,12 +42,12 @@ class HelpView(urwid.ListBox): text.append(urwid.Text([("head", "\n\nGlobal keys:\n")])) keys = [ - ("c", "client replay"), + ("c", "client replay of HTTP requests"), ("i", "set interception pattern"), ("o", "options"), ("q", "quit / return to previous page"), ("Q", "quit without confirm prompt"), - ("S", "server replay"), + ("S", "server replay of HTTP responses"), ] text.extend( common.format_keyvals(keys, key="key", val="text", indent=4) @@ -108,8 +108,8 @@ class HelpView(urwid.ListBox): return None elif key == "?": key = None - elif key == "G": - self.set_focus(0) elif key == "g": + self.set_focus(0) + elif key == "G": self.set_focus(len(self.body.contents)) return urwid.ListBox.keypress(self, size, key) diff --git a/libmproxy/console/searchable.py b/libmproxy/console/searchable.py index 627d595d..dea0ac7f 100644 --- a/libmproxy/console/searchable.py +++ b/libmproxy/console/searchable.py @@ -33,10 +33,10 @@ class Searchable(urwid.ListBox): self.find_next(False) elif key == "N": self.find_next(True) - elif key == "G": + elif key == "g": self.set_focus(0) self.walker._modified() - elif key == "g": + elif key == "G": self.set_focus(len(self.walker) - 1) self.walker._modified() else: diff --git a/libmproxy/console/window.py b/libmproxy/console/window.py index 8754ed57..69d5e242 100644 --- a/libmproxy/console/window.py +++ b/libmproxy/console/window.py @@ -23,7 +23,7 @@ class Window(urwid.Frame): if not k: if args[1] == "mouse drag": signals.status_message.send( - message = "Hold down alt or ctrl to select text.", + message = "Hold down shift, alt or ctrl to select text.", expire = 1 ) elif args[1] == "mouse press" and args[2] == 4: diff --git a/libmproxy/filt.py b/libmproxy/filt.py index 1983586b..bd17a807 100644 --- a/libmproxy/filt.py +++ b/libmproxy/filt.py @@ -241,6 +241,19 @@ class FUrl(_Rex): def __call__(self, f): return re.search(self.expr, f.request.url) +class FSrc(_Rex): + code = "src" + help = "Match source address" + + def __call__(self, f): + return f.client_conn and re.search(self.expr, repr(f.client_conn.address)) + +class FDst(_Rex): + code = "dst" + help = "Match destination address" + + def __call__(self, f): + return f.server_conn and re.search(self.expr, repr(f.server_conn.address)) class _Int(_Action): def __init__(self, num): @@ -313,6 +326,8 @@ filt_rex = [ FRequestContentType, FResponseContentType, FContentType, + FSrc, + FDst, ] filt_int = [ FCode @@ -324,7 +339,7 @@ def _make(): # ones. parts = [] for klass in filt_unary: - f = pp.Literal("~%s" % klass.code) + f = pp.Literal("~%s" % klass.code) + pp.WordEnd() f.setParseAction(klass.make) parts.append(f) @@ -333,12 +348,12 @@ def _make(): pp.QuotedString("\"", escChar='\\') |\ pp.QuotedString("'", escChar='\\') for klass in filt_rex: - f = pp.Literal("~%s" % klass.code) + rex.copy() + f = pp.Literal("~%s" % klass.code) + pp.WordEnd() + rex.copy() f.setParseAction(klass.make) parts.append(f) for klass in filt_int: - f = pp.Literal("~%s" % klass.code) + pp.Word(pp.nums) + f = pp.Literal("~%s" % klass.code) + pp.WordEnd() + pp.Word(pp.nums) f.setParseAction(klass.make) parts.append(f) diff --git a/libmproxy/flow.py b/libmproxy/flow.py index 59312ceb..4b725ae5 100644 --- a/libmproxy/flow.py +++ b/libmproxy/flow.py @@ -158,7 +158,7 @@ class StreamLargeBodies(object): def run(self, flow, is_request): r = flow.request if is_request else flow.response code = flow.response.code if flow.response else None - expected_size = netlib.http.expected_http_body_size( + expected_size = netlib.http.http1.HTTP1Protocol.expected_http_body_size( r.headers, is_request, flow.request.method, code ) if not (0 <= expected_size <= self.max_size): @@ -663,9 +663,12 @@ class FlowMaster(controller.Master): for s in self.scripts[:]: self.unload_script(s) - def unload_script(self, script): - script.unload() - self.scripts.remove(script) + def unload_script(self, script_obj): + try: + script_obj.unload() + except script.ScriptError as e: + self.add_event("Script error:\n" + str(e), "error") + self.scripts.remove(script_obj) def load_script(self, command): """ @@ -678,16 +681,16 @@ class FlowMaster(controller.Master): return v.args[0] self.scripts.append(s) - def run_single_script_hook(self, script, name, *args, **kwargs): - if script and not self.pause_scripts: - ret = script.run(name, *args, **kwargs) - if not ret[0] and ret[1]: - e = "Script error:\n" + ret[1][1] - self.add_event(e, "error") + def _run_single_script_hook(self, script_obj, name, *args, **kwargs): + if script_obj and not self.pause_scripts: + try: + script_obj.run(name, *args, **kwargs) + except script.ScriptError as e: + self.add_event("Script error:\n" + str(e), "error") def run_script_hook(self, name, *args, **kwargs): - for script in self.scripts: - self.run_single_script_hook(script, name, *args, **kwargs) + for script_obj in self.scripts: + self._run_single_script_hook(script_obj, name, *args, **kwargs) def get_ignore_filter(self): return self.server.config.check_ignore.patterns diff --git a/libmproxy/protocol/http.py b/libmproxy/protocol/http.py index 2bb1f528..f2ac5acc 100644 --- a/libmproxy/protocol/http.py +++ b/libmproxy/protocol/http.py @@ -1,14 +1,16 @@ from __future__ import absolute_import import Cookie +import copy +import threading +import time import urllib import urlparse -import time -import copy from email.utils import parsedate_tz, formatdate, mktime_tz -import threading -from netlib import http, tcp, http_status, http_cookies -import netlib.utils -from netlib import odict + +import netlib +from netlib import http, tcp, odict, utils +from netlib.http import cookies, http1 + from .tcp import TCPHandler from .primitives import KILL, ProtocolHandler, Flow, Error from ..proxy.connection import ServerConnection @@ -303,6 +305,10 @@ class HTTPRequest(HTTPMessage): is_replay=bool ) + @property + def body(self): + return self.content + @classmethod def from_state(cls, state): f = cls( @@ -354,11 +360,10 @@ class HTTPRequest(HTTPMessage): if hasattr(rfile, "reset_timestamps"): rfile.reset_timestamps() - req = http.read_request( - rfile, + protocol = http1.HTTP1Protocol(rfile=rfile, wfile=wfile) + req = protocol.read_request( include_body = include_body, body_size_limit = body_size_limit, - wfile = wfile ) if hasattr(rfile, "first_byte_timestamp"): @@ -375,7 +380,7 @@ class HTTPRequest(HTTPMessage): req.path, req.httpversion, req.headers, - req.content, + req.body, timestamp_start, timestamp_end ) @@ -642,7 +647,7 @@ class HTTPRequest(HTTPMessage): """ ret = odict.ODict() for i in self.headers["cookie"]: - ret.extend(http_cookies.parse_cookie_header(i)) + ret.extend(cookies.parse_cookie_header(i)) return ret def set_cookies(self, odict): @@ -650,7 +655,7 @@ class HTTPRequest(HTTPMessage): Takes an netlib.odict.ODict object. Over-writes any existing Cookie headers. """ - v = http_cookies.format_cookie_header(odict) + v = cookies.format_cookie_header(odict) self.headers["Cookie"] = [v] def replace(self, pattern, repl, *args, **kwargs): @@ -724,6 +729,12 @@ class HTTPResponse(HTTPMessage): msg=str ) + + @property + def body(self): + return self.content + + @classmethod def from_state(cls, state): f = cls(None, None, None, None, None) @@ -760,11 +771,12 @@ class HTTPResponse(HTTPMessage): if hasattr(rfile, "reset_timestamps"): rfile.reset_timestamps() - httpversion, code, msg, headers, content = http.read_response( - rfile, + protocol = http1.HTTP1Protocol(rfile=rfile) + resp = protocol.read_response( request_method, body_size_limit, - include_body=include_body) + include_body=include_body + ) if hasattr(rfile, "first_byte_timestamp"): # more accurate timestamp_start @@ -776,11 +788,11 @@ class HTTPResponse(HTTPMessage): timestamp_end = None return HTTPResponse( - httpversion, - code, - msg, - headers, - content, + resp.httpversion, + resp.status_code, + resp.msg, + resp.headers, + resp.body, timestamp_start, timestamp_end ) @@ -894,7 +906,7 @@ class HTTPResponse(HTTPMessage): """ ret = [] for header in self.headers["set-cookie"]: - v = http_cookies.parse_set_cookie_header(header) + v = http.cookies.parse_set_cookie_header(header) if v: name, value, attrs = v ret.append([name, [value, attrs]]) @@ -910,7 +922,7 @@ class HTTPResponse(HTTPMessage): values = [] for i in odict.lst: values.append( - http_cookies.format_set_cookie_header( + http.cookies.format_set_cookie_header( i[0], i[1][0], i[1][1] @@ -1044,7 +1056,8 @@ class HTTPHandler(ProtocolHandler): self.c.server_conn.send(request_raw) # Only get the headers at first... flow.response = HTTPResponse.from_stream( - self.c.server_conn.rfile, flow.request.method, + self.c.server_conn.rfile, + flow.request.method, body_size_limit=self.c.config.body_size_limit, include_body=False ) @@ -1081,10 +1094,13 @@ class HTTPHandler(ProtocolHandler): if flow.response.stream: flow.response.content = CONTENT_MISSING else: - flow.response.content = http.read_http_body( - self.c.server_conn.rfile, flow.response.headers, + protocol = http1.HTTP1Protocol(rfile=self.c.server_conn.rfile) + flow.response.content = protocol.read_http_body( + flow.response.headers, self.c.config.body_size_limit, - flow.request.method, flow.response.code, False + flow.request.method, + flow.response.code, + False ) flow.response.timestamp_end = utils.timestamp() @@ -1231,7 +1247,7 @@ class HTTPHandler(ProtocolHandler): pass def send_error(self, code, message, headers): - response = http_status.RESPONSES.get(code, "Unknown") + response = http.status_codes.RESPONSES.get(code, "Unknown") html_content = """ <html> <head> @@ -1285,6 +1301,7 @@ class HTTPHandler(ProtocolHandler): if not request.host and flow.server_conn: request.host, request.port = flow.server_conn.address.host, flow.server_conn.address.port + # Now we can process the request. if request.form_in == "authority": if self.c.client_conn.ssl_established: @@ -1363,7 +1380,7 @@ class HTTPHandler(ProtocolHandler): # We provide a mostly unified API to the user, which needs to be # unfiddled here # ( See also: https://github.com/mitmproxy/mitmproxy/issues/337 ) - address = netlib.tcp.Address((flow.request.host, flow.request.port)) + address = tcp.Address((flow.request.host, flow.request.port)) ssl = (flow.request.scheme == "https") @@ -1417,8 +1434,8 @@ class HTTPHandler(ProtocolHandler): h = flow.response._assemble_head(preserve_transfer_encoding=True) self.c.client_conn.send(h) - chunks = http.read_http_body_chunked( - self.c.server_conn.rfile, + protocol = http1.HTTP1Protocol(rfile=self.c.server_conn.rfile) + chunks = protocol.read_http_body_chunked( flow.response.headers, self.c.config.body_size_limit, flow.request.method, @@ -1440,15 +1457,18 @@ class HTTPHandler(ProtocolHandler): semantics. Returns True, if so. """ close_connection = ( - http.connection_close( + http1.HTTP1Protocol.connection_close( flow.request.httpversion, - flow.request.headers) or http.connection_close( + flow.request.headers + ) or http1.HTTP1Protocol.connection_close( flow.response.httpversion, - flow.response.headers) or http.expected_http_body_size( + flow.response.headers + ) or http1.HTTP1Protocol.expected_http_body_size( flow.response.headers, False, flow.request.method, - flow.response.code) == -1) + flow.response.code) == -1 + ) if close_connection: if flow.request.form_in == "authority" and flow.response.code == 200: # Workaround for diff --git a/libmproxy/proxy/config.py b/libmproxy/proxy/config.py index c5306b4a..ec91a6e0 100644 --- a/libmproxy/proxy/config.py +++ b/libmproxy/proxy/config.py @@ -2,7 +2,11 @@ from __future__ import absolute_import import os import re from OpenSSL import SSL -from netlib import http_auth, certutils, tcp + +import netlib +from netlib import http, certutils, tcp +from netlib.http import authentication + from .. import utils, platform, version from .primitives import RegularProxyMode, SpoofMode, SSLSpoofMode, TransparentProxyMode, UpstreamProxyMode, ReverseProxyMode, Socks5ProxyMode @@ -103,7 +107,7 @@ class ProxyConfig: self.openssl_method_server = ssl_version_server else: self.openssl_method_server = tcp.SSL_VERSIONS[ssl_version_server] - + if ssl_verify_upstream_cert: self.openssl_verification_mode_server = SSL.VERIFY_PEER else: @@ -164,18 +168,18 @@ def process_proxy_options(parser, options): return parser.error( "Invalid single-user specification. Please use the format username:password") username, password = options.auth_singleuser.split(':') - password_manager = http_auth.PassManSingleUser(username, password) + password_manager = authentication.PassManSingleUser(username, password) elif options.auth_nonanonymous: - password_manager = http_auth.PassManNonAnon() + password_manager = authentication.PassManNonAnon() elif options.auth_htpasswd: try: - password_manager = http_auth.PassManHtpasswd( + password_manager = authentication.PassManHtpasswd( options.auth_htpasswd) except ValueError as v: return parser.error(v.message) - authenticator = http_auth.BasicProxyAuth(password_manager, "mitmproxy") + authenticator = authentication.BasicProxyAuth(password_manager, "mitmproxy") else: - authenticator = http_auth.NullProxyAuth(None) + authenticator = authentication.NullProxyAuth(None) certs = [] for i in options.certs: diff --git a/libmproxy/proxy/server.py b/libmproxy/proxy/server.py index 2711bd0e..e77439fb 100644 --- a/libmproxy/proxy/server.py +++ b/libmproxy/proxy/server.py @@ -167,12 +167,12 @@ class ConnectionHandler: self.channel.tell("serverdisconnect", self) self.server_conn = None - def set_server_address(self, address): + def set_server_address(self, addr): """ Sets a new server address with the given priority. Does not re-establish either connection or SSL handshake. """ - address = tcp.Address.wrap(address) + address = tcp.Address.wrap(addr) # Don't reconnect to the same destination. if self.server_conn and self.server_conn.address == address: @@ -309,15 +309,15 @@ class ConnectionHandler: self.client_conn.finish() def log(self, msg, level, subs=()): - msg = [ + full_msg = [ "%s:%s: %s" % (self.client_conn.address.host, self.client_conn.address.port, msg)] for i in subs: - msg.append(" -> " + i) - msg = "\n".join(msg) - self.channel.tell("log", Log(msg, level)) + full_msg.append(" -> " + i) + full_msg = "\n".join(full_msg) + self.channel.tell("log", Log(full_msg, level)) def find_cert(self): host = self.server_conn.address.host diff --git a/libmproxy/script.py b/libmproxy/script.py index 46edb86b..e13f0e2b 100644 --- a/libmproxy/script.py +++ b/libmproxy/script.py @@ -3,7 +3,7 @@ import os import traceback import threading import shlex -from . import controller +import sys class ScriptError(Exception): @@ -55,21 +55,17 @@ class ScriptContext: class Script: """ - The instantiator should do something along this vein: - - s = Script(argv, master) - s.load() + Script object representing an inline script. """ def __init__(self, command, master): - self.command = command - self.argv = self.parse_command(command) + self.args = self.parse_command(command) self.ctx = ScriptContext(master) self.ns = None self.load() @classmethod - def parse_command(klass, command): + def parse_command(cls, command): if not command or not command.strip(): raise ScriptError("Empty script command.") if os.name == "nt": # Windows: escape all backslashes in the path. @@ -89,54 +85,66 @@ class Script: def load(self): """ - Loads a module. + Loads an inline script. + + Returns: + The return value of self.run("start", ...) - Raises ScriptError on failure, with argument equal to an error - message that may be a formatted traceback. + Raises: + ScriptError on failure """ + if self.ns is not None: + self.unload() ns = {} + script_dir = os.path.dirname(os.path.abspath(self.args[0])) + sys.path.append(script_dir) try: - execfile(self.argv[0], ns, ns) - except Exception as v: - raise ScriptError(traceback.format_exc(v)) + execfile(self.args[0], ns, ns) + except Exception as e: + # Python 3: use exception chaining, https://www.python.org/dev/peps/pep-3134/ + raise ScriptError(traceback.format_exc(e)) + sys.path.pop() self.ns = ns - r = self.run("start", self.argv) - if not r[0] and r[1]: - raise ScriptError(r[1][1]) + return self.run("start", self.args) def unload(self): - return self.run("done") + ret = self.run("done") + self.ns = None + return ret def run(self, name, *args, **kwargs): """ - Runs a plugin method. + Runs an inline script hook. Returns: + The return value of the method. + None, if the script does not provide the method. - (True, retval) on success. - (False, None) on nonexistent method. - (False, (exc, traceback string)) if there was an exception. + Raises: + ScriptError if there was an exception. """ f = self.ns.get(name) if f: try: - return (True, f(self.ctx, *args, **kwargs)) - except Exception as v: - return (False, (v, traceback.format_exc(v))) + return f(self.ctx, *args, **kwargs) + except Exception as e: + raise ScriptError(traceback.format_exc(e)) else: - return (False, None) + return None class ReplyProxy(object): - def __init__(self, original_reply): - self._ignore_calls = 1 - self.lock = threading.Lock() + def __init__(self, original_reply, script_thread): self.original_reply = original_reply + self.script_thread = script_thread + self._ignore_call = True + self.lock = threading.Lock() def __call__(self, *args, **kwargs): with self.lock: - if self._ignore_calls > 0: - self._ignore_calls -= 1 + if self._ignore_call: + self.script_thread.start() + self._ignore_call = False return self.original_reply(*args, **kwargs) @@ -145,16 +153,19 @@ class ReplyProxy(object): def _handle_concurrent_reply(fn, o, *args, **kwargs): - # Make first call to o.reply a no op - - reply_proxy = ReplyProxy(o.reply) - o.reply = reply_proxy + # Make first call to o.reply a no op and start the script thread. + # We must not start the script thread before, as this may lead to a nasty race condition + # where the script thread replies a different response before the normal reply, which then gets swallowed. def run(): fn(*args, **kwargs) # If the script did not call .reply(), we have to do it now. reply_proxy() - ScriptThread(target=run).start() + + script_thread = ScriptThread(target=run) + + reply_proxy = ReplyProxy(o.reply, script_thread) + o.reply = reply_proxy class ScriptThread(threading.Thread): @@ -171,6 +182,7 @@ def concurrent(fn): "clientdisconnect"): def _concurrent(ctx, obj): _handle_concurrent_reply(fn, obj, ctx, obj) + return _concurrent raise NotImplementedError( - "Concurrent decorator not supported for this method.") + "Concurrent decorator not supported for '%s' method." % fn.func_name) diff --git a/libmproxy/version.py b/libmproxy/version.py index 7836c849..0af60af5 100644 --- a/libmproxy/version.py +++ b/libmproxy/version.py @@ -1,4 +1,6 @@ -IVERSION = (0, 12, 2) +from __future__ import (absolute_import, print_function, division) + +IVERSION = (0, 13, 1) VERSION = ".".join(str(i) for i in IVERSION) MINORVERSION = ".".join(str(i) for i in IVERSION[:2]) NAME = "mitmproxy" diff --git a/libmproxy/web/app.py b/libmproxy/web/app.py index 29ae9e7a..d6082ee2 100644 --- a/libmproxy/web/app.py +++ b/libmproxy/web/app.py @@ -81,7 +81,8 @@ class WebSocketEventBroadcaster(tornado.websocket.WebSocketHandler): @classmethod def broadcast(cls, **kwargs): - message = json.dumps(kwargs) + message = json.dumps(kwargs, ensure_ascii=False) + for conn in cls.connections: try: conn.write_message(message) |