luci-app-shadowsocks-libev: add server links import feature
authorRichard Yu <yurichard3839@gmail.com>
Sat, 9 Nov 2019 15:43:00 +0000 (23:43 +0800)
committerYousong Zhou <yszhou4tech@gmail.com>
Fri, 15 Nov 2019 11:03:34 +0000 (19:03 +0800)
Signed-off-by: Richard Yu <yurichard3839@gmail.com>
applications/luci-app-shadowsocks-libev/htdocs/luci-static/resources/shadowsocks-libev.js
applications/luci-app-shadowsocks-libev/htdocs/luci-static/resources/view/shadowsocks-libev/servers.js

index 3aaaa50121209fd66dc2b239bf63a2fbf2aa7e03..3d111d57917bb6106317b42c5cd5262a5caab3bf 100644 (file)
@@ -238,5 +238,71 @@ return L.Class.extend({
                        window.open(L.url('admin/system/opkg') +
                                '?query=' + opkg_package, '_blank', 'noopener');
                };
+       },
+       parse_uri: function(uri) {
+               var scheme = 'ss://';
+               if (uri && uri.indexOf(scheme) === 0) {
+                       var atPos = uri.indexOf('@'), hashPos = uri.lastIndexOf('#'), tag;
+                       if (hashPos === -1) {
+                               hashPos = undefined;
+                       } else {
+                               tag = uri.slice(hashPos + 1);
+                       }
+
+                       if (atPos !== -1) { // SIP002 format https://shadowsocks.org/en/spec/SIP002-URI-Scheme.html
+                               var colonPos = uri.indexOf(':', atPos + 1), slashPos = uri.indexOf('/', colonPos + 1);
+                               if (colonPos === -1) return null;
+                               if (slashPos === -1) slashPos = undefined;
+
+                               var userinfo = atob(uri.slice(scheme.length, atPos)
+                                       .replace(/-/g, '+').replace(/_/g, '/')),
+                                       i = userinfo.indexOf(':');
+                               if (i === -1) return null;
+
+                               var config = {
+                                       server: uri.slice(atPos + 1, colonPos),
+                                       server_port: uri.slice(colonPos + 1, slashPos ? slashPos : hashPos),
+                                       password: userinfo.slice(i + 1),
+                                       method: userinfo.slice(0, i)
+                               };
+
+                               if (slashPos) {
+                                       var search = uri.slice(slashPos + 1, hashPos);
+                                       if (search[0] === '?') search = search.slice(1);
+                                       search.split('&').forEach(function(s) {
+                                               var j = s.indexOf('=');
+                                               if (j !== -1) {
+                                                       var k = s.slice(0, j), v = s.slice(j + 1);
+                                                       if (k === 'plugin') {
+                                                               v = decodeURIComponent(v);
+                                                               var k = v.indexOf(';');
+                                                               if (k !== -1) {
+                                                                       config['plugin'] = v.slice(0, k);
+                                                                       config['plugin_opts'] = v.slice(k + 1);
+                                                               }
+                                                       }
+                                               }
+                                       });
+                               }
+                               return [config, tag];
+                       } else { // Legacy format https://shadowsocks.org/en/config/quick-guide.html
+                               var plain = atob(uri.slice(scheme.length, hashPos)),
+                                       firstColonPos = plain.indexOf(':'),
+                                       lastColonPos = plain.lastIndexOf(':'),
+                                       atPos = plain.lastIndexOf('@', lastColonPos);
+                               if (firstColonPos === -1 ||
+                                       lastColonPos === -1 ||
+                                       atPos === -1) return null;
+
+                               var config = {
+                                       server: plain.slice(atPos + 1, lastColonPos),
+                                       server_port: plain.slice(lastColonPos + 1),
+                                       password: plain.slice(firstColonPos + 1, atPos),
+                                       method: plain.slice(0, firstColonPos)
+                               };
+                               return [config, tag];
+                       }
+               }
+               return null;
        }
 });
index d46bfb0aa78f13716e286cc69b0b035598b6112c..5951e92e51773b552c27d3fc7110a44beae995b1 100644 (file)
@@ -1,21 +1,65 @@
 'use strict';
 'require form';
+'require uci';
+'require ui';
 'require shadowsocks-libev as ss';
 
-function startsWith(str, search) {
-       return str.substring(0, search.length) === search;
-}
+var conf = 'shadowsocks-libev';
 
 return L.view.extend({
        render: function() {
                var m, s, o;
 
-               m = new form.Map('shadowsocks-libev', _('Remote Servers'),
+               m = new form.Map(conf, _('Remote Servers'),
                        _('Definition of remote shadowsocks servers.  \
                                Disable any of them will also disable instances referring to it.'));
 
                s = m.section(form.GridSection, 'server');
                s.addremove = true;
+               s.handleLinkImport = function() {
+                       var textarea = new ui.Textarea();
+                       ui.showModal(_('Import Links'), [
+                               textarea.render(),
+                               E('div', { class: 'right' }, [
+                                       E('button', {
+                                               class: 'btn',
+                                               click: ui.hideModal
+                                       }, [ _('Cancel') ]),
+                                       ' ',
+                                       E('button', {
+                                               class: 'btn cbi-button-action',
+                                               click: ui.createHandlerFn(this, function() {
+                                                       textarea.getValue().split('\n').forEach(function(s) {
+                                                               var config = ss.parse_uri(s);
+                                                               if (config) {
+                                                                       var tag = config[1];
+                                                                       if (tag && !tag.match(/^[a-zA-Z0-9_]+$/)) tag = null;
+                                                                       var sid = uci.add(conf, 'server', tag);
+                                                                       config = config[0];
+                                                                       Object.keys(config).forEach(function(k) {
+                                                                               uci.set(conf, sid, k, config[k]);
+                                                                       });
+                                                               }
+                                                       });
+                                                       return uci.save()
+                                                               .then(L.bind(this.map.load, this.map))
+                                                               .then(L.bind(this.map.reset, this.map))
+                                                               .then(L.ui.hideModal)
+                                                               .catch(function() {});
+                                               })
+                                       }, [ _('Import') ])
+                               ])
+                       ]);
+               };
+               s.renderSectionAdd = function(extra_class) {
+                       var el = form.GridSection.prototype.renderSectionAdd.apply(this, arguments);
+                       el.appendChild(E('button', {
+                               'class': 'cbi-button cbi-button-add',
+                               'title': _('Import Links'),
+                               'click': ui.createHandlerFn(this, 'handleLinkImport')
+                       }, [ _('Import Links') ]));
+                       return el;
+               };
 
                o = s.option(form.Flag, 'disabled', _('Disable'));
                o.editable = true;
@@ -26,7 +70,7 @@ return L.view.extend({
        },
        addFooter: function() {
                var p = '#edit=';
-               if (startsWith(location.hash, p)) {
+               if (location.hash.indexOf(p) === 0) {
                        var section_id = location.hash.substring(p.length);
                        var editBtn = document.querySelector('#cbi-shadowsocks-libev-' + section_id + ' button.cbi-button-edit');
                        if (editBtn)