2019-07-09 12:21:32 +02:00
|
|
|
#!/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:
|
2019-07-10 03:56:01 +02:00
|
|
|
print("ERROR: unsupported line: "+link)
|
2019-07-09 12:21:32 +02:00
|
|
|
return None
|
|
|
|
|
|
|
|
def item2link(item):
|
|
|
|
if item["net"] == "shadowsocks":
|
2019-07-10 03:56:01 +02:00
|
|
|
auth = base64.b64encode("{method}:{password}".format(**item).encode()).decode()
|
|
|
|
addr = "{add}:{port}".format(**item)
|
|
|
|
sslink = "ss://{}@{}#{}".format(auth, addr, urllib.parse.quote(item["ps"]))
|
2019-07-09 12:21:32 +02:00
|
|
|
return sslink
|
|
|
|
else:
|
|
|
|
return "vmess://{}".format(base64.b64encode(json.dumps(item).encode()).decode())
|
|
|
|
|
|
|
|
|
|
|
|
def parseSs(sslink):
|
|
|
|
if sslink.startswith(ssscheme):
|
2019-07-10 03:56:01 +02:00
|
|
|
ps = ""
|
2019-07-09 12:21:32 +02:00
|
|
|
info = sslink[len(ssscheme):]
|
|
|
|
|
|
|
|
if info.rfind("#") > 0:
|
2019-07-10 03:56:01 +02:00
|
|
|
info, ps = info.split("#", 2)
|
2019-07-11 03:59:18 +02:00
|
|
|
ps = urllib.parse.unquote(ps)
|
2019-07-09 12:21:32 +02:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
2019-07-10 03:56:01 +02:00
|
|
|
return dict(net="shadowsocks", add=addr, port=port, method=method, password=password, ps=ps)
|
2019-07-09 12:21:32 +02:00
|
|
|
|
|
|
|
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")
|
|
|
|
|
|
|
|
|
|
|
|
|
2019-07-10 03:56:01 +02:00
|
|
|
def menu_loop(lines):
|
2019-07-09 12:21:32 +02:00
|
|
|
vmesses = []
|
2019-07-11 03:59:18 +02:00
|
|
|
menu_item = lambda x: "[{ps}] {add}:{port}/{net}".format(**x)
|
|
|
|
|
2019-07-09 12:21:32 +02:00
|
|
|
for _v in lines:
|
|
|
|
_vinfo = parseLink(_v)
|
|
|
|
if _vinfo is not None:
|
|
|
|
vmesses.append({
|
2019-07-10 03:56:01 +02:00
|
|
|
"menu": menu_item(_vinfo),
|
|
|
|
"link": _v,
|
2019-07-09 12:21:32 +02:00
|
|
|
"info": _vinfo
|
|
|
|
})
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
|
|
|
print("==============================================================")
|
|
|
|
for i, item in enumerate(vmesses):
|
2019-07-10 03:56:01 +02:00
|
|
|
print("[{:^3}] - {}".format(i, item["menu"]))
|
2019-07-09 12:21:32 +02:00
|
|
|
|
2019-07-10 03:56:01 +02:00
|
|
|
print("""==============================================================
|
2019-07-11 07:43:47 +02:00
|
|
|
Enter index digit XX to edit,
|
|
|
|
Other commands: Add(a), Delete XX(dXX), Sort by ps(s), Sort by ps desc(d),
|
2019-07-11 03:22:33 +02:00
|
|
|
Save Write(w), Quit without saving(q)
|
2019-07-10 03:56:01 +02:00
|
|
|
""")
|
2019-07-09 12:21:32 +02:00
|
|
|
|
2019-07-10 03:56:01 +02:00
|
|
|
try:
|
2019-07-11 03:22:33 +02:00
|
|
|
sel = input("Choose >>>")
|
2019-07-09 12:21:32 +02:00
|
|
|
if sel.isdigit():
|
2019-07-10 03:56:01 +02:00
|
|
|
idx = int(sel)
|
2019-07-11 03:59:18 +02:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2019-07-11 03:22:33 +02:00
|
|
|
elif sel == "a":
|
|
|
|
_v = input("input >>>")
|
|
|
|
_vinfo = parseLink(_v)
|
|
|
|
if _vinfo is not None:
|
|
|
|
vmesses.append({
|
|
|
|
"menu": menu_item(_vinfo),
|
|
|
|
"link": _v,
|
|
|
|
"info": _vinfo
|
|
|
|
})
|
2019-07-11 07:43:47 +02:00
|
|
|
elif sel == "s":
|
|
|
|
vmesses = sorted(vmesses, key=lambda i:i["info"]["ps"])
|
2019-07-09 12:21:32 +02:00
|
|
|
elif sel == "d":
|
2019-07-11 07:43:47 +02:00
|
|
|
vmesses = sorted(vmesses, key=lambda i:i["info"]["ps"], reverse=True)
|
2019-07-09 12:21:32 +02:00
|
|
|
elif sel == "w":
|
|
|
|
output_item(vmesses)
|
2019-07-10 03:56:01 +02:00
|
|
|
return
|
2019-07-09 12:21:32 +02:00
|
|
|
elif sel == "q":
|
2019-07-10 03:56:01 +02:00
|
|
|
return
|
2019-07-09 12:21:32 +02:00
|
|
|
elif sel.startswith("d") and sel[1:].isdigit():
|
2019-07-10 03:56:01 +02:00
|
|
|
idx = int(sel[1:])
|
2019-07-09 12:21:32 +02:00
|
|
|
del vmesses[idx]
|
|
|
|
else:
|
2019-07-10 03:56:01 +02:00
|
|
|
print("Error: Unreconized command.")
|
|
|
|
except IndexError:
|
|
|
|
print("Error input: Out of range")
|
|
|
|
except EOFError:
|
|
|
|
return
|
|
|
|
|
2019-07-09 12:21:32 +02:00
|
|
|
|
2019-07-11 03:59:18 +02:00
|
|
|
def edit_item(item):
|
2019-07-10 03:56:01 +02:00
|
|
|
tfile = tempfile.NamedTemporaryFile(delete=False)
|
|
|
|
tfile.close()
|
|
|
|
with open(tfile.name, 'w') as f:
|
2019-07-09 12:21:32 +02:00
|
|
|
json.dump(item, f, indent=4)
|
|
|
|
|
2019-07-10 03:56:01 +02:00
|
|
|
os.system("vim {}".format(tfile.name))
|
|
|
|
|
|
|
|
with open(tfile.name, 'r') as f:
|
|
|
|
try:
|
2019-07-11 03:59:18 +02:00
|
|
|
_in = json.load(f)
|
|
|
|
finally:
|
|
|
|
os.remove(tfile.name)
|
|
|
|
|
|
|
|
return _in
|
2019-07-09 12:21:32 +02:00
|
|
|
|
|
|
|
def output_item(vmesses):
|
2019-07-10 03:56:01 +02:00
|
|
|
links = map(lambda x:x["link"], vmesses)
|
2019-07-09 12:21:32 +02:00
|
|
|
with open(option.edit[0].name, "w") as f:
|
|
|
|
f.write("\n".join(links))
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
|
|
parser = argparse.ArgumentParser(description="vmess subscribe file editor.")
|
|
|
|
parser.add_argument('edit',
|
|
|
|
nargs=1,
|
|
|
|
type=argparse.FileType('r'),
|
|
|
|
help="a subscribe text file, base64 encoded or not")
|
|
|
|
|
|
|
|
option = parser.parse_args()
|
2019-07-11 08:31:37 +02:00
|
|
|
indata = option.edit[0].read().strip()
|
2019-07-10 03:56:01 +02:00
|
|
|
option.edit[0].close()
|
2019-07-09 12:21:32 +02:00
|
|
|
|
|
|
|
try:
|
2019-07-11 08:31:37 +02:00
|
|
|
blen = len(indata)
|
|
|
|
if blen % 4 > 0:
|
|
|
|
indata += "=" * (4 - blen % 4)
|
2019-07-09 12:21:32 +02:00
|
|
|
lines = base64.b64decode(indata).decode().splitlines()
|
|
|
|
except (binascii.Error, UnicodeDecodeError):
|
|
|
|
lines = indata.splitlines()
|
|
|
|
finally:
|
2019-07-10 03:56:01 +02:00
|
|
|
menu_loop(lines)
|