Skip to content

Commit f512359

Browse files
authored
Merge pull request #175 from vforvideo/admin
feat: httpadmin protocol for external control
2 parents 09d4752 + b153dae commit f512359

File tree

3 files changed

+82
-3
lines changed

3 files changed

+82
-3
lines changed

pproxy/admin.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import json
2+
import asyncio
3+
config = {}
4+
5+
6+
async def reply_http(reply, ver, code, content):
7+
await reply(code, f'{ver} {code}\r\nConnection: close\r\nContent-Type: text/plain\r\nCache-Control: max-age=900\r\nContent-Length: {len(content)}\r\n\r\n'.encode(), content, True)
8+
9+
10+
async def status_handler(reply, **kwarg):
11+
method = kwarg.get('method')
12+
if method == 'GET':
13+
data = {"status": "ok"}
14+
value = json.dumps(data).encode()
15+
ver = kwarg.get('ver')
16+
await reply_http(reply, ver, '200 OK', value)
17+
18+
19+
async def configs_handler(reply, **kwarg):
20+
method = kwarg.get('method')
21+
ver = kwarg.get('ver')
22+
23+
if method == 'GET':
24+
data = {"argv": config['argv']}
25+
value = json.dumps(data).encode()
26+
await reply_http(reply, ver, '200 OK', value)
27+
elif method == 'POST':
28+
config['argv'] = kwarg.get('content').decode().split(' ')
29+
config['reload'] = True
30+
data = {"result": 'ok'}
31+
value = json.dumps(data).encode()
32+
await reply_http(reply, ver, '200 OK', value)
33+
raise KeyboardInterrupt
34+
35+
36+
httpget = {
37+
'/status': status_handler,
38+
'/configs': configs_handler,
39+
}

pproxy/proto.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import asyncio, socket, urllib.parse, time, re, base64, hmac, struct, hashlib, io, os
2-
2+
from . import admin
33
HTTP_LINE = re.compile('([^ ]+) +(.+?) +(HTTP/[^ ]+)$')
44
packstr = lambda s, n=1: len(s).to_bytes(n, 'big') + s
55

@@ -404,6 +404,38 @@ async def connect(self, reader_remote, writer_remote, rauth, host_name, port, my
404404
class H3(H2):
405405
pass
406406

407+
408+
class HTTPAdmin(HTTP):
409+
async def accept(self, reader, user, writer, **kw):
410+
lines = await reader.read_until(b'\r\n\r\n')
411+
headers = lines[:-4].decode().split('\r\n')
412+
method, path, ver = HTTP_LINE.match(headers.pop(0)).groups()
413+
lines = '\r\n'.join(i for i in headers if not i.startswith('Proxy-'))
414+
headers = dict(i.split(': ', 1) for i in headers if ': ' in i)
415+
async def reply(code, message, body=None, wait=False):
416+
writer.write(message)
417+
if body:
418+
writer.write(body)
419+
if wait:
420+
await writer.drain()
421+
422+
content_length = int(headers.get('Content-Length','0'))
423+
content = ''
424+
if content_length > 0:
425+
content = await reader.read_n(content_length)
426+
427+
url = urllib.parse.urlparse(path)
428+
if url.hostname is not None:
429+
raise Exception(f'HTTP Admin Unsupported hostname')
430+
if method in ["GET", "POST", "PUT", "PATCH", "DELETE"]:
431+
for path, handler in admin.httpget.items():
432+
if path == url.path:
433+
await handler(reply=reply, ver=ver, method=method, headers=headers, lines=lines, content=content)
434+
raise Exception('Connection closed')
435+
raise Exception(f'404 {method} {url.path}')
436+
raise Exception(f'405 {method} not allowed')
437+
438+
407439
class SSH(BaseProtocol):
408440
async def connect(self, reader_remote, writer_remote, rauth, host_name, port, myhost, **kw):
409441
pass
@@ -567,7 +599,7 @@ def udp_accept(protos, data, **kw):
567599
return (proto,) + ret
568600
raise Exception(f'Unsupported protocol {data[:10]}')
569601

570-
MAPPINGS = dict(direct=Direct, http=HTTP, httponly=HTTPOnly, ssh=SSH, socks5=Socks5, socks4=Socks4, socks=Socks5, ss=SS, ssr=SSR, redir=Redir, pf=Pf, tunnel=Tunnel, echo=Echo, ws=WS, trojan=Trojan, h2=H2, h3=H3, ssl='', secure='', quic='')
602+
MAPPINGS = dict(direct=Direct, http=HTTP, httponly=HTTPOnly, httpadmin=HTTPAdmin, ssh=SSH, socks5=Socks5, socks4=Socks4, socks=Socks5, ss=SS, ssr=SSR, redir=Redir, pf=Pf, tunnel=Tunnel, echo=Echo, ws=WS, trojan=Trojan, h2=H2, h3=H3, ssl='', secure='', quic='')
571603
MAPPINGS['in'] = ''
572604

573605
def get_protos(rawprotos):

pproxy/server.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
import argparse, time, re, asyncio, functools, base64, random, urllib.parse, socket
1+
import argparse, time, re, asyncio, functools, base64, random, urllib.parse, socket, sys
22
from . import proto
3+
from . import admin
4+
35
from .__doc__ import *
46

57
SOCKET_TIMEOUT = 60
@@ -892,6 +894,8 @@ def print_server_started(option, server, print_fn):
892894
print_fn(option, bind)
893895

894896
def main(args = None):
897+
origin_argv = sys.argv[1:] if args is None else args
898+
895899
parser = argparse.ArgumentParser(description=__description__+'\nSupported protocols: http,socks4,socks5,shadowsocks,shadowsocksr,redirect,pf,tunnel', epilog=f'Online help: <{__url__}>')
896900
parser.add_argument('-l', dest='listen', default=[], action='append', type=proxies_by_uri, help='tcp server uri (default: http+socks4+socks5://:8080/)')
897901
parser.add_argument('-r', dest='rserver', default=[], action='append', type=proxies_by_uri, help='tcp remote server uri (default: direct)')
@@ -953,6 +957,7 @@ def main(args = None):
953957
from . import verbose
954958
verbose.setup(loop, args)
955959
servers = []
960+
admin.config.update({'argv': origin_argv, 'servers': servers, 'args': args, 'loop': loop})
956961
def print_fn(option, bind=None):
957962
print('Serving on', (bind or option.bind), 'by', ",".join(i.name for i in option.protos) + ('(SSL)' if option.sslclient else ''), '({}{})'.format(option.cipher.name, ' '+','.join(i.name() for i in option.cipher.plugins) if option.cipher and option.cipher.plugins else '') if option.cipher else '')
958963
for option in args.listen:
@@ -1004,6 +1009,9 @@ def print_fn(option, bind=None):
10041009
if hasattr(server, 'wait_closed'):
10051010
loop.run_until_complete(server.wait_closed())
10061011
loop.run_until_complete(loop.shutdown_asyncgens())
1012+
if admin.config.get('reload', False):
1013+
admin.config['reload'] = False
1014+
main(admin.config['argv'])
10071015
loop.close()
10081016

10091017
if __name__ == '__main__':

0 commit comments

Comments
 (0)