From 39bb0d50ecbdd6d9669a01c7d2c170ed07291f76 Mon Sep 17 00:00:00 2001 From: Jordan Gong Date: Sat, 3 Oct 2020 10:58:11 +0800 Subject: Add basic features - Subscription support - V2ray-core upgrade support --- vmess2json/vmesseditor.py | 217 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 vmess2json/vmesseditor.py (limited to 'vmess2json/vmesseditor.py') diff --git a/vmess2json/vmesseditor.py b/vmess2json/vmesseditor.py new file mode 100644 index 0000000..f32aa62 --- /dev/null +++ b/vmess2json/vmesseditor.py @@ -0,0 +1,217 @@ +#!/usr/bin/env python3 +import os +import sys +import json +import base64 +import pprint +import argparse +import random +import hashlib +import binascii +import traceback +import urllib.request +import urllib.parse +import tempfile + +vmscheme = "vmess://" +ssscheme = "ss://" + +def parseLink(link): + if link.startswith(ssscheme): + return parseSs(link) + elif link.startswith(vmscheme): + return parseVmess(link) + else: + print("ERROR: unsupported line: "+link) + return None + +def item2link(item): + if item["net"] == "shadowsocks": + auth = base64.b64encode("{method}:{password}".format(**item).encode()).decode() + addr = "{add}:{port}".format(**item) + sslink = "ss://{}@{}#{}".format(auth, addr, urllib.parse.quote(item["ps"])) + return sslink + else: + return "vmess://{}".format(base64.b64encode(json.dumps(item).encode()).decode()) + + +def parseSs(sslink): + if sslink.startswith(ssscheme): + ps = "" + info = sslink[len(ssscheme):] + + if info.rfind("#") > 0: + info, ps = info.split("#", 2) + ps = urllib.parse.unquote(ps) + + if info.find("@") < 0: + # old style link + #paddings + blen = len(info) + if blen % 4 > 0: + info += "=" * (4 - blen % 4) + + info = base64.b64decode(info).decode() + + atidx = info.rfind("@") + method, password = info[:atidx].split(":", 2) + addr, port = info[atidx+1:].split(":", 2) + else: + atidx = info.rfind("@") + addr, port = info[atidx+1:].split(":", 2) + + info = info[:atidx] + blen = len(info) + if blen % 4 > 0: + info += "=" * (4 - blen % 4) + + info = base64.b64decode(info).decode() + method, password = info.split(":", 2) + + return dict(net="shadowsocks", add=addr, port=port, method=method, password=password, ps=ps) + +def parseVmess(vmesslink): + if vmesslink.startswith(vmscheme): + bs = vmesslink[len(vmscheme):] + #paddings + blen = len(bs) + if blen % 4 > 0: + bs += "=" * (4 - blen % 4) + + vms = base64.b64decode(bs).decode() + return json.loads(vms) + else: + raise Exception("vmess link invalid") + + + +def menu_loop(lines): + vmesses = [] + menu_item = lambda x: "[{ps}] {add}:{port}/{net}".format(**x) + + for _v in lines: + _vinfo = parseLink(_v) + if _vinfo is not None: + vmesses.append({ + "menu": menu_item(_vinfo), + "link": _v, + "info": _vinfo + }) + + while True: + + print("==============================================================") + for i, item in enumerate(vmesses): + print("[{:^3}] - {}".format(i, item["menu"])) + + print("""============================================================== +Enter index digit XX to edit, +Other commands: Add(a), Delete XX(dXX), Sort by ps(s), Sort by ps desc(d), +Save Write(w), Quit without saving(q) +""") + + try: + sel = input("Choose >>>") + if sel.isdigit(): + idx = int(sel) + try: + _edited = edit_item(vmesses[idx]["info"]) + except json.decoder.JSONDecodeError: + print("Error: json syntax error") + else: + vmesses[idx] = { + "menu": menu_item(_edited), + "link": item2link(_edited), + "info": _edited + } + + elif sel == "a": + _v = input("input >>>") + _vinfo = parseLink(_v) + if _vinfo is not None: + vmesses.append({ + "menu": menu_item(_vinfo), + "link": _v, + "info": _vinfo + }) + elif sel == "s": + vmesses = sorted(vmesses, key=lambda i:i["info"]["ps"]) + elif sel == "d": + vmesses = sorted(vmesses, key=lambda i:i["info"]["ps"], reverse=True) + elif sel == "w": + output_item(vmesses) + return + elif sel == "q": + return + elif sel.startswith("d") and sel[1:].isdigit(): + idx = int(sel[1:]) + del vmesses[idx] + else: + print("Error: Unreconized command.") + except IndexError: + print("Error input: Out of range") + except EOFError: + return + + +def edit_item(item): + tfile = tempfile.NamedTemporaryFile(delete=False) + tfile.close() + with open(tfile.name, 'w') as f: + json.dump(item, f, indent=4) + + os.system("vim {}".format(tfile.name)) + + with open(tfile.name, 'r') as f: + try: + _in = json.load(f) + finally: + os.remove(tfile.name) + + return _in + +def output_item(vmesses): + links = map(lambda x:x["link"], vmesses) + with open(option.edit[0], "w") as f: + f.write("\n".join(links)) + +def edit_single_link(vmess): + _vinfo = parseLink(vmess) + if _vinfo is None: + return + + try: + _vedited = edit_item(_vinfo) + except json.decoder.JSONDecodeError as e: + print("JSON format error:", e) + return + + _link = item2link(_vedited) + print("Edited Link:") + print(_link) + +if __name__ == "__main__": + + parser = argparse.ArgumentParser(description="vmess subscribe file editor.") + parser.add_argument('edit', + nargs=1, + type=str, + help="a subscribe text file, base64 encoded or not, or a single vmess:// ss:// link") + + option = parser.parse_args() + arg = option.edit[0] + if os.path.exists(arg): + with open(arg) as f: + indata = f.read().strip() + try: + blen = len(indata) + if blen % 4 > 0: + indata += "=" * (4 - blen % 4) + lines = base64.b64decode(indata).decode().splitlines() + except (binascii.Error, UnicodeDecodeError): + lines = indata.splitlines() + finally: + menu_loop(lines) + + else: + edit_single_link(arg) -- cgit v1.2.3