Fixed keep-alive header and broken connection handling
This commit is contained in:
parent
9dd87a62ad
commit
8cc028764d
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,2 +1,2 @@
|
|||
settings.py
|
||||
settings.py
|
||||
*.pyc
|
44
README.md
44
README.md
|
@ -1,23 +1,23 @@
|
|||
# Sanic
|
||||
|
||||
Python 3.5+ web server that's written to go fast
|
||||
|
||||
▄▄▄▄▄
|
||||
▀▀▀██████▄▄▄ _______________
|
||||
▄▄▄▄▄ █████████▄ / \
|
||||
▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! |
|
||||
▀▀█████▄▄ ▀██████▄██ | _________________/
|
||||
▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/
|
||||
▀▀▀▄ ▀▀███ ▀ ▄▄
|
||||
▄███▀▀██▄████████▄ ▄▀▀▀▀▀▀█▌
|
||||
██▀▄▄▄██▀▄███▀ ▀▀████ ▄██
|
||||
▄▀▀▀▄██▄▀▀▌████▒▒▒▒▒▒███ ▌▄▄▀
|
||||
▌ ▐▀████▐███▒▒▒▒▒▐██▌
|
||||
▀▄▄▄▄▀ ▀▀████▒▒▒▒▄██▀
|
||||
▀▀█████████▀
|
||||
▄▄██▀██████▀█
|
||||
▄██▀ ▀▀▀ █
|
||||
▄█ ▐▌
|
||||
▄▄▄▄█▌ ▀█▄▄▄▄▀▀▄
|
||||
▌ ▐ ▀▀▄▄▄▀
|
||||
# Sanic
|
||||
|
||||
Python 3.5+ web server that's written to go fast
|
||||
|
||||
▄▄▄▄▄
|
||||
▀▀▀██████▄▄▄ _______________
|
||||
▄▄▄▄▄ █████████▄ / \
|
||||
▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! |
|
||||
▀▀█████▄▄ ▀██████▄██ | _________________/
|
||||
▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/
|
||||
▀▀▀▄ ▀▀███ ▀ ▄▄
|
||||
▄███▀▀██▄████████▄ ▄▀▀▀▀▀▀█▌
|
||||
██▀▄▄▄██▀▄███▀ ▀▀████ ▄██
|
||||
▄▀▀▀▄██▄▀▀▌████▒▒▒▒▒▒███ ▌▄▄▀
|
||||
▌ ▐▀████▐███▒▒▒▒▒▐██▌
|
||||
▀▄▄▄▄▀ ▀▀████▒▒▒▒▄██▀
|
||||
▀▀█████████▀
|
||||
▄▄██▀██████▀█
|
||||
▄██▀ ▀▀▀ █
|
||||
▄█ ▐▌
|
||||
▄▄▄▄█▌ ▀█▄▄▄▄▀▀▄
|
||||
▌ ▐ ▀▀▄▄▄▀
|
||||
▀▀▄▄▀
|
3
requirements.txt
Normal file
3
requirements.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
uvloop
|
||||
httptools
|
||||
ujson
|
|
@ -1,2 +1 @@
|
|||
from .sanic import Sanic
|
||||
from .server import Response
|
||||
from .sanic import Sanic
|
|
@ -1,21 +1,21 @@
|
|||
LOGO = """
|
||||
▄▄▄▄▄
|
||||
▀▀▀██████▄▄▄ _______________
|
||||
▄▄▄▄▄ █████████▄ / \\
|
||||
▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! |
|
||||
▀▀█████▄▄ ▀██████▄██ | _________________/
|
||||
▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/
|
||||
▀▀▀▄ ▀▀███ ▀ ▄▄
|
||||
▄███▀▀██▄████████▄ ▄▀▀▀▀▀▀█▌
|
||||
██▀▄▄▄██▀▄███▀ ▀▀████ ▄██
|
||||
▄▀▀▀▄██▄▀▀▌████▒▒▒▒▒▒███ ▌▄▄▀
|
||||
▌ ▐▀████▐███▒▒▒▒▒▐██▌
|
||||
▀▄▄▄▄▀ ▀▀████▒▒▒▒▄██▀
|
||||
▀▀█████████▀
|
||||
▄▄██▀██████▀█
|
||||
▄██▀ ▀▀▀ █
|
||||
▄█ ▐▌
|
||||
▄▄▄▄█▌ ▀█▄▄▄▄▀▀▄
|
||||
▌ ▐ ▀▀▄▄▄▀
|
||||
▀▀▄▄▀
|
||||
LOGO = """
|
||||
▄▄▄▄▄
|
||||
▀▀▀██████▄▄▄ _______________
|
||||
▄▄▄▄▄ █████████▄ / \\
|
||||
▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! |
|
||||
▀▀█████▄▄ ▀██████▄██ | _________________/
|
||||
▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/
|
||||
▀▀▀▄ ▀▀███ ▀ ▄▄
|
||||
▄███▀▀██▄████████▄ ▄▀▀▀▀▀▀█▌
|
||||
██▀▄▄▄██▀▄███▀ ▀▀████ ▄██
|
||||
▄▀▀▀▄██▄▀▀▌████▒▒▒▒▒▒███ ▌▄▄▀
|
||||
▌ ▐▀████▐███▒▒▒▒▒▐██▌
|
||||
▀▄▄▄▄▀ ▀▀████▒▒▒▒▄██▀
|
||||
▀▀█████████▀
|
||||
▄▄██▀██████▀█
|
||||
▄██▀ ▀▀▀ █
|
||||
▄█ ▐▌
|
||||
▄▄▄▄█▌ ▀█▄▄▄▄▀▀▄
|
||||
▌ ▐ ▀▀▄▄▄▀
|
||||
▀▀▄▄▀
|
||||
"""
|
|
@ -1,4 +1,4 @@
|
|||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s: %(levelname)s: %(message)s")
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s: %(levelname)s: %(message)s")
|
||||
log = logging.getLogger(__name__)
|
|
@ -1,6 +1,47 @@
|
|||
import ujson
|
||||
|
||||
from .server import Response
|
||||
|
||||
def json(input):
|
||||
return Response(ujson.dumps(input), content_type="application/json")
|
||||
import ujson
|
||||
|
||||
STATUS_CODES = {
|
||||
200: 'OK',
|
||||
404: 'Not Found'
|
||||
}
|
||||
class HTTPResponse:
|
||||
__slots__ = ('body', 'status', 'content_type')
|
||||
|
||||
def __init__(self, body='', status=200, content_type='text/plain'):
|
||||
self.content_type = 'text/plain'
|
||||
self.body = body
|
||||
self.status = status
|
||||
|
||||
@property
|
||||
def body_bytes(self):
|
||||
body_type = type(self.body)
|
||||
if body_type is str:
|
||||
body = self.body.encode('utf-8')
|
||||
elif body_type is bytes:
|
||||
body = self.body
|
||||
else:
|
||||
body = b'Unable to interpret body'
|
||||
|
||||
return body
|
||||
|
||||
def output(self, version="1.1", keep_alive=False):
|
||||
body = self.body_bytes
|
||||
return b''.join([
|
||||
'HTTP/{} {} {}\r\n'.format(version, self.status, STATUS_CODES.get(self.status, 'FAIL')).encode('latin-1'),
|
||||
'Content-Type: {}\r\n'.format(self.content_type).encode('latin-1'),
|
||||
'Content-Length: {}\r\n'.format(len(body)).encode('latin-1'),
|
||||
'Connection: {}\r\n'.format('keep-alive' if keep_alive else 'close').encode('latin-1'),
|
||||
b'\r\n',
|
||||
body,
|
||||
#b'\r\n'
|
||||
])
|
||||
|
||||
|
||||
def error_404(request, *args):
|
||||
return HTTPResponse("404!", status=404)
|
||||
error_404.is_async = False
|
||||
|
||||
def json(input):
|
||||
return HTTPResponse(ujson.dumps(input), content_type="application/json")
|
||||
def text(input):
|
||||
return HTTPResponse(input, content_type="text/plain")
|
|
@ -1,15 +1,19 @@
|
|||
from .log import log
|
||||
|
||||
class Router():
|
||||
routes = None
|
||||
default = None
|
||||
|
||||
def __init__(self, default=None):
|
||||
self.routes = {}
|
||||
self.default=default
|
||||
|
||||
def add(self, route, handler):
|
||||
self.routes[route] = handler
|
||||
|
||||
def get(self, uri):
|
||||
return self.routes.get(uri.decode('utf-8'), self.default)
|
||||
from .log import log
|
||||
|
||||
class Router():
|
||||
routes = None
|
||||
default = None
|
||||
|
||||
def __init__(self, default=None):
|
||||
self.routes = {}
|
||||
self.default=default
|
||||
|
||||
def add(self, route, handler):
|
||||
self.routes[route] = handler
|
||||
|
||||
def get(self, uri):
|
||||
handler = self.routes.get(uri.decode('utf-8'), self.default)
|
||||
if handler:
|
||||
return handler
|
||||
else:
|
||||
return self.default
|
|
@ -1,26 +1,24 @@
|
|||
import inspect
|
||||
from .router import Router
|
||||
from .server import Response, serve
|
||||
from .log import log
|
||||
|
||||
class Sanic:
|
||||
name = None
|
||||
routes = []
|
||||
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.router = Router(default=self.handler_default)
|
||||
|
||||
def route(self, *args, **kwargs):
|
||||
def response(handler):
|
||||
handler.is_async = inspect.iscoroutinefunction(handler)
|
||||
self.router.add(*args, **kwargs, handler=handler)
|
||||
return handler
|
||||
|
||||
return response
|
||||
|
||||
def run(self, host="127.0.0.1", port=8000, debug=False):
|
||||
return serve(router=self.router, host=host, port=port, debug=debug)
|
||||
|
||||
def handler_default(self, request, *args):
|
||||
return Response("404!", status=404)
|
||||
import inspect
|
||||
from .router import Router
|
||||
from .response import HTTPResponse, error_404
|
||||
from .server import serve
|
||||
from .log import log
|
||||
|
||||
class Sanic:
|
||||
name = None
|
||||
routes = []
|
||||
|
||||
def __init__(self, name, router=None):
|
||||
self.name = name
|
||||
self.router = router or Router(default=error_404)
|
||||
|
||||
def route(self, *args, **kwargs):
|
||||
def response(handler):
|
||||
handler.is_async = inspect.iscoroutinefunction(handler)
|
||||
self.router.add(*args, **kwargs, handler=handler)
|
||||
return handler
|
||||
|
||||
return response
|
||||
|
||||
def run(self, host="127.0.0.1", port=8000, debug=False):
|
||||
return serve(router=self.router, host=host, port=port, debug=debug)
|
374
sanic/server.py
374
sanic/server.py
|
@ -1,196 +1,180 @@
|
|||
import argparse
|
||||
import sys
|
||||
import asyncio
|
||||
import signal
|
||||
import functools
|
||||
import httptools
|
||||
import logging
|
||||
|
||||
import httptools
|
||||
try:
|
||||
import uvloop as async_loop
|
||||
except:
|
||||
async_loop = asyncio
|
||||
|
||||
from socket import *
|
||||
|
||||
from .log import log
|
||||
from .config import LOGO
|
||||
|
||||
PRINT = 0
|
||||
|
||||
class Request:
|
||||
__slots__ = ('protocol', 'url', 'headers', 'version', 'method')
|
||||
|
||||
def __init__(self, protocol, url, headers, version, method):
|
||||
self.protocol = protocol
|
||||
self.url = url
|
||||
self.headers = headers
|
||||
self.version = version
|
||||
self.method = method
|
||||
|
||||
STATUS_CODES = {
|
||||
200: 'OK',
|
||||
404: 'Not Found'
|
||||
}
|
||||
class Response:
|
||||
__slots__ = ('body', 'status', 'content_type')
|
||||
|
||||
def __init__(self, body='', status=200, content_type='text/plain'):
|
||||
self.content_type = 'text/plain'
|
||||
self.body = body
|
||||
self.status = status
|
||||
|
||||
@property
|
||||
def body_bytes(self):
|
||||
body_type = type(self.body)
|
||||
if body_type is str:
|
||||
body = self.body.encode('utf-8')
|
||||
elif body_type is bytes:
|
||||
body = self.body
|
||||
else:
|
||||
body = b'Unable to interpret body'
|
||||
|
||||
return body
|
||||
|
||||
def output(self, version):
|
||||
body = self.body_bytes
|
||||
return b''.join([
|
||||
'HTTP/{} {} {}\r\n'.format(version, self.status, STATUS_CODES.get(self.status, 'FAIL')).encode('latin-1'),
|
||||
'Content-Type: {}\r\n'.format(self.content_type).encode('latin-1'),
|
||||
'Content-Length: {}\r\n'.format(len(body)).encode('latin-1'),
|
||||
b'\r\n',
|
||||
body
|
||||
])
|
||||
|
||||
|
||||
class HttpProtocol(asyncio.Protocol):
|
||||
|
||||
__slots__ = ('loop',
|
||||
'transport', 'request', 'parser',
|
||||
'url', 'headers', 'router')
|
||||
|
||||
def __init__(self, *, router, loop):
|
||||
self.loop = loop
|
||||
self.transport = None
|
||||
self.request = None
|
||||
self.parser = None
|
||||
self.url = None
|
||||
self.headers = None
|
||||
self.router = router
|
||||
|
||||
# -------------------------------------------- #
|
||||
# Connection
|
||||
# -------------------------------------------- #
|
||||
|
||||
def connection_made(self, transport):
|
||||
self.transport = transport
|
||||
# TCP Nodelay
|
||||
# I have no evidence to support this makes anything faster
|
||||
# So I'll leave it commented out for now
|
||||
|
||||
# sock = transport.get_extra_info('socket')
|
||||
# try:
|
||||
# sock.setsockopt(IPPROTO_TCP, TCP_NODELAY, 1)
|
||||
# except (OSError, NameError):
|
||||
# pass
|
||||
|
||||
def connection_lost(self, exc):
|
||||
self.request = self.parser = None
|
||||
|
||||
# -------------------------------------------- #
|
||||
# Parsing
|
||||
# -------------------------------------------- #
|
||||
|
||||
def data_received(self, data):
|
||||
if self.parser is None:
|
||||
assert self.request is None
|
||||
self.headers = []
|
||||
self.parser = httptools.HttpRequestParser(self)
|
||||
|
||||
self.parser.feed_data(data)
|
||||
|
||||
def on_url(self, url):
|
||||
self.url = url
|
||||
|
||||
def on_header(self, name, value):
|
||||
self.headers.append((name, value))
|
||||
|
||||
def on_headers_complete(self):
|
||||
self.request = Request(
|
||||
protocol=self,
|
||||
url=self.url,
|
||||
headers=dict(self.headers),
|
||||
version=self.parser.get_http_version(),
|
||||
method=self.parser.get_method()
|
||||
)
|
||||
self.loop.call_soon(self.handle, self.request)
|
||||
|
||||
# -------------------------------------------- #
|
||||
# Responding
|
||||
# -------------------------------------------- #
|
||||
|
||||
def handle(self, request):
|
||||
handler = self.router.get(request.url)
|
||||
if handler.is_async:
|
||||
future = asyncio.Future()
|
||||
self.loop.create_task(self.handle_response(future, handler, request))
|
||||
future.add_done_callback(self.handle_result)
|
||||
else:
|
||||
response = handler(request)
|
||||
self.write_response(response)
|
||||
|
||||
def write_response(self, response):
|
||||
self.transport.write(response.output(request.version))
|
||||
|
||||
if not self.parser.should_keep_alive():
|
||||
self.transport.close()
|
||||
self.parser = None
|
||||
self.request = None
|
||||
|
||||
# -------------------------------------------- #
|
||||
# Async
|
||||
# -------------------------------------------- #
|
||||
|
||||
async def handle_response(self, future, handler, request):
|
||||
result = await handler(request)
|
||||
future.set_result(result)
|
||||
|
||||
def handle_result(self, future):
|
||||
response = future.result()
|
||||
self.write_response(response)
|
||||
|
||||
|
||||
def abort(msg):
|
||||
log.info(msg, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def serve(router, host, port, debug=False):
|
||||
# Create Event Loop
|
||||
loop = async_loop.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
loop.set_debug(debug)
|
||||
|
||||
# Add signal handlers
|
||||
def ask_exit(signame):
|
||||
log.debug("Exiting, received signal %s" % signame)
|
||||
loop.stop()
|
||||
|
||||
for signame in ('SIGINT', 'SIGTERM'):
|
||||
loop.add_signal_handler(getattr(signal, signame), functools.partial(ask_exit, signame))
|
||||
|
||||
if debug:
|
||||
log.setLevel(logging.DEBUG)
|
||||
log.debug(LOGO)
|
||||
|
||||
# Serve
|
||||
log.info('Goin\' Fast @ {}:{}'.format(host, port))
|
||||
|
||||
server_coroutine = loop.create_server(lambda: HttpProtocol(loop=loop, router=router), host, port)
|
||||
server_loop = loop.run_until_complete(server_coroutine)
|
||||
try:
|
||||
loop.run_forever()
|
||||
finally:
|
||||
server_loop.close()
|
||||
import argparse
|
||||
import sys
|
||||
import asyncio
|
||||
import signal
|
||||
import functools
|
||||
import httptools
|
||||
import logging
|
||||
|
||||
import httptools
|
||||
try:
|
||||
import uvloop as async_loop
|
||||
except:
|
||||
async_loop = asyncio
|
||||
|
||||
from socket import *
|
||||
|
||||
from .log import log
|
||||
from .config import LOGO
|
||||
from .response import HTTPResponse
|
||||
|
||||
PRINT = 0
|
||||
|
||||
class Request:
|
||||
__slots__ = ('protocol', 'url', 'headers', 'version', 'method')
|
||||
|
||||
def __init__(self, protocol, url, headers, version, method):
|
||||
self.protocol = protocol
|
||||
self.url = url
|
||||
self.headers = headers
|
||||
self.version = version
|
||||
self.method = method
|
||||
|
||||
class HttpProtocol(asyncio.Protocol):
|
||||
|
||||
__slots__ = ('loop',
|
||||
'transport', 'request', 'parser',
|
||||
'url', 'headers', 'router')
|
||||
|
||||
def __init__(self, *, router, loop):
|
||||
self.loop = loop
|
||||
self.transport = None
|
||||
self.request = None
|
||||
self.parser = None
|
||||
self.url = None
|
||||
self.headers = None
|
||||
self.router = router
|
||||
|
||||
# -------------------------------------------- #
|
||||
# Connection
|
||||
# -------------------------------------------- #
|
||||
|
||||
def connection_made(self, transport):
|
||||
self.transport = transport
|
||||
#TODO: handle keep-alive/connection timeout
|
||||
|
||||
# TCP Nodelay
|
||||
# I have no evidence to support this makes anything faster
|
||||
# So I'll leave it commented out for now
|
||||
|
||||
# sock = transport.get_extra_info('socket')
|
||||
# try:
|
||||
# sock.setsockopt(IPPROTO_TCP, TCP_NODELAY, 1)
|
||||
# except (OSError, NameError):
|
||||
# pass
|
||||
|
||||
def connection_lost(self, exc):
|
||||
self.request = self.parser = None
|
||||
|
||||
# -------------------------------------------- #
|
||||
# Parsing
|
||||
# -------------------------------------------- #
|
||||
|
||||
def data_received(self, data):
|
||||
if self.parser is None:
|
||||
assert self.request is None
|
||||
self.headers = []
|
||||
self.parser = httptools.HttpRequestParser(self)
|
||||
|
||||
try:
|
||||
#print(data)
|
||||
self.parser.feed_data(data)
|
||||
except httptools.parser.errors.HttpParserError:
|
||||
#log.error("Invalid request data, connection closed")
|
||||
self.transport.close()
|
||||
|
||||
def on_url(self, url):
|
||||
self.url = url
|
||||
|
||||
def on_header(self, name, value):
|
||||
self.headers.append((name, value))
|
||||
|
||||
def on_headers_complete(self):
|
||||
self.request = Request(
|
||||
protocol=self,
|
||||
url=self.url,
|
||||
headers=dict(self.headers),
|
||||
version=self.parser.get_http_version(),
|
||||
method=self.parser.get_method()
|
||||
)
|
||||
global n
|
||||
n += 1
|
||||
self.n = n
|
||||
#print("res {} - {}".format(n, self.request))
|
||||
self.loop.call_soon(self.handle, self.request)
|
||||
|
||||
# -------------------------------------------- #
|
||||
# Responding
|
||||
# -------------------------------------------- #
|
||||
|
||||
def handle(self, request):
|
||||
handler = self.router.get(request.url)
|
||||
if handler.is_async:
|
||||
future = asyncio.Future()
|
||||
self.loop.create_task(self.handle_response(future, handler, request))
|
||||
future.add_done_callback(self.handle_result)
|
||||
else:
|
||||
response = handler(request)
|
||||
self.write_response(request, response)
|
||||
|
||||
def write_response(self, request, response):
|
||||
#print("response - {} - {}".format(self.n, self.request))
|
||||
try:
|
||||
keep_alive = self.parser.should_keep_alive()
|
||||
self.transport.write(response.output(request.version, keep_alive))
|
||||
#print("KA - {}".format(self.parser.should_keep_alive()))
|
||||
if not keep_alive:
|
||||
self.transport.close()
|
||||
except:
|
||||
log.error("Writing request failed, connection closed")
|
||||
self.transport.close()
|
||||
|
||||
self.parser = None
|
||||
self.request = None
|
||||
|
||||
# -------------------------------------------- #
|
||||
# Async
|
||||
# -------------------------------------------- #
|
||||
|
||||
async def handle_response(self, future, handler, request):
|
||||
response = await handler(request)
|
||||
future.set_result((request, response))
|
||||
|
||||
def handle_result(self, future):
|
||||
request, response = future.result()
|
||||
self.write_response(request, response)
|
||||
|
||||
|
||||
def abort(msg):
|
||||
log.info(msg, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def serve(router, host, port, debug=False):
|
||||
# Create Event Loop
|
||||
loop = async_loop.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
loop.set_debug(debug)
|
||||
|
||||
# Add signal handlers
|
||||
def ask_exit(signame):
|
||||
log.debug("Exiting, received signal %s" % signame)
|
||||
loop.stop()
|
||||
|
||||
for signame in ('SIGINT', 'SIGTERM'):
|
||||
loop.add_signal_handler(getattr(signal, signame), functools.partial(ask_exit, signame))
|
||||
|
||||
if debug:
|
||||
log.setLevel(logging.DEBUG)
|
||||
log.debug(LOGO)
|
||||
|
||||
# Serve
|
||||
log.info('Goin\' Fast @ {}:{}'.format(host, port))
|
||||
|
||||
server_coroutine = loop.create_server(lambda: HttpProtocol(loop=loop, router=router), host, port)
|
||||
server_loop = loop.run_until_complete(server_coroutine)
|
||||
try:
|
||||
loop.run_forever()
|
||||
finally:
|
||||
server_loop.close()
|
||||
loop.close()
|
15
test.go
Normal file
15
test.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func handler(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:])
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/", handler)
|
||||
http.ListenAndServe(":8000", nil)
|
||||
}
|
Loading…
Reference in New Issue
Block a user