Skip to content

Instantly share code, notes, and snippets.

@tonyseek
Created August 23, 2016 10:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tonyseek/63491ba1802710a3e7447fa579bf7bcc to your computer and use it in GitHub Desktop.
Save tonyseek/63491ba1802710a3e7447fa579bf7bcc to your computer and use it in GitHub Desktop.
Migrate the Surge configuration to ShadowsocksX because the Surge Mac 2.0 restricts device numbers.
#!/usr/bin/env python3
"""ShadowsocksX Migration.
This script generates ShadowsocksX profiles from the Surge configuration.
"""
import os
import json
import time
import shutil
import logging
import argparse
import plistlib
import configparser
logger = logging.getLogger(__name__)
DEFAULT_SSX_PLIST = '~/Library/Preferences/clowwindy.ShadowsocksX.plist'
def parse_surge_config(filename):
parser = configparser.ConfigParser(allow_no_value=True)
parser.read([filename])
assert parser.has_section('Proxy'), '[Proxy] section is required.'
for option in parser.options('Proxy'):
# extract value
value = parser.get('Proxy', option, raw=True)
if isinstance(value, list):
value = value[0]
value = value.lower()
# skip useless value
if value == 'direct':
continue
# parse value
fragments = [fragment.strip() for fragment in value.split(',')]
try:
proto, host, port, method, password, _ = fragments[:6]
except ValueError:
logger.warning('Unable to parse %r' % fragments)
continue
if proto != 'custom':
logger.info('Skip unsupported proto %r' % fragments)
yield {
'method': method,
'password': password,
'remarks': option,
'server': host,
'server_port': int(port),
}
def merge_shadowsocks_profile(ssx_config, new_config):
config = dict(ssx_config)
config.setdefault('current', 0)
config.setdefault('profiles', []).extend(new_config)
return config
def migrate(surge_config, input_plist, output_plist, print_only):
# loads and parse surge config
new_config = parse_surge_config(surge_config)
# loads and merge the plist file
plist = plistlib.load(open(input_plist, 'rb'))
ssx_config = json.loads(plist['config'].decode('utf-8'))
merged_config = merge_shadowsocks_profile(ssx_config, new_config)
plist['config'] = json.dumps(merged_config).encode('utf-8')
if print_only:
print(plistlib.dumps(plist, fmt=plistlib.FMT_XML).decode('utf-8'))
return
# backup if overrides
if input_plist == output_plist:
timestamp = int(time.time() * 100)
shutil.copy(output_plist, '{0}.{1}.bak'.format(input_plist, timestamp))
# write the output file
with open(output_plist, 'wb') as plist_file:
plistlib.dump(plist, plist_file, fmt=plistlib.FMT_BINARY)
def main():
parser = argparse.ArgumentParser()
parser.add_argument(
'--surge-config', help='The input path of surge configuration.',
required=True)
parser.add_argument(
'--input-ssx-plist', help='The input path of ShadowsocksX plist.',
default=DEFAULT_SSX_PLIST)
parser.add_argument(
'--output-ssx-plist', help='The output path of ShadowsocksX plist.',
default=DEFAULT_SSX_PLIST)
parser.add_argument(
'-p', '--print-only', help='Print only.', action='store_true',
default=False)
args = parser.parse_args()
migrate(
os.path.expanduser(args.surge_config),
os.path.expanduser(args.input_ssx_plist),
os.path.expanduser(args.output_ssx_plist),
args.print_only,
)
if not args.print_only:
print('Now you can import the plist file:')
print('\tdefaults import clowwindy.ShadowsocksX "{0}"'.format(
args.output_ssx_plist))
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment