summaryrefslogtreecommitdiff
path: root/vmess2json/vmesseditor.py
diff options
context:
space:
mode:
Diffstat (limited to 'vmess2json/vmesseditor.py')
-rw-r--r--vmess2json/vmesseditor.py217
1 files changed, 217 insertions, 0 deletions
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)