Skip to content
This repository was archived by the owner on Nov 21, 2019. It is now read-only.

Commit 85aeaf6

Browse files
author
icymind
committed
add feature: import from uri
1 parent d528e7f commit 85aeaf6

File tree

3 files changed

+201
-28
lines changed

3 files changed

+201
-28
lines changed

html/index.html

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
</a>
1919
<!-- <a class="item" data-tab="mode"> -->
2020
<!-- <i class="filter icon"></i> -->
21-
<!-- 模式 -->
21+
<!-- 模式 -->
2222
<!-- </a> -->
2323
<a class="item" data-tab="system">
2424
<i class="linux icon"></i>
@@ -70,9 +70,19 @@
7070
</div>
7171

7272
<div id="proxies-tab" class="ui bottom attached tab segment" data-tab="proxies">
73-
<div class="ui teal icon labeled button" v-on:click="newProfile">
74-
<i class="ui plus icon"></i>
75-
新建配置
73+
<div id="add-profile" class="ui dropdown button left pointing labeled icon teal">
74+
<i class="add user icon"></i>
75+
添加配置
76+
<div class="menu">
77+
<div class="item" v-on:click="newProfile">
78+
<i class="ui plus icon"></i>
79+
新建
80+
</div>
81+
<div class="item" v-on:click="showImportProfileModal">
82+
<i class="ui paste icon"></i>
83+
导入
84+
</div>
85+
</div>
7686
</div>
7787
<div class="ui divider hidden"></div>
7888
<div id="profiles">
@@ -295,15 +305,19 @@ <h4 class="ui header teal">代理状态</h4>
295305
</div>
296306
</div>
297307

308+
<div id="importProfileModal" class="ui basic modal">
309+
<div class="ui action fluid input">
310+
<input type="text">
311+
<div class="ui button teal right labeled icon" @click="importProfile">
312+
<i class="copy icon"></i>
313+
导入URI
314+
</div>
315+
</div>
316+
<div class="ui pointing red basic label hidden">
317+
</div>
318+
</div>
298319
<div id="profileModal" class="ui modal" ref="profileModal">
299320
<div class="scrolling content">
300-
<div class="ui action input">
301-
<input type="text">
302-
<div class="ui button teal right labeled icon">
303-
<i class="copy icon"></i>
304-
导入URI
305-
</div>
306-
</div>
307321
<form class="ui form">
308322
<div class="field">
309323
<label>配置名称</label>
@@ -332,10 +346,6 @@ <h4 class="ui header teal">代理状态</h4>
332346
</div>
333347
</div>
334348
</div>
335-
<!-- <div class="ui icon labeled button" @click="importProfile"> -->
336-
<!-- <i class="ui paste icon"></i> -->
337-
<!-- 导入 -->
338-
<!-- </div> -->
339349
</div>
340350
<div class="ui divider hidden"></div>
341351
<form class="ui form">

js/manage.js

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,6 @@ const myApp = new Vue({
143143
this.errorMsg = err.toString()
144144
$(this.$refs.errorModal)
145145
.modal({
146-
blurring: true,
147146
dimmerSettings: {
148147
opacity: 0.8
149148
}
@@ -157,13 +156,27 @@ const myApp = new Vue({
157156
$(this.$refs.profileModal)
158157
.modal({
159158
dimmerSettings: {
160-
opacity: 0.2
159+
opacity: 0.8
161160
},
162161
detachable: false,
163162
closable: false
164163
})
165164
.modal(action)
166165
},
166+
showImportProfileModal () {
167+
document.querySelector('#importProfileModal input').value = ''
168+
const label = document.querySelector('#importProfileModal div.label')
169+
label.innerHTML = ''
170+
label.classList.add('hidden')
171+
$('#importProfileModal')
172+
.modal({
173+
dimmerSettings: {
174+
opacity: 0.8
175+
},
176+
detachable: false
177+
})
178+
.modal('show')
179+
},
167180
newProfile () {
168181
this.editingProfile = {
169182
'name': '新配置',
@@ -242,8 +255,16 @@ const myApp = new Vue({
242255
duration: 10
243256
})
244257
},
245-
async importProfile () {
246-
// TODO:
258+
importProfile () {
259+
const uri = document.querySelector('#importProfileModal input').value
260+
try {
261+
this.editingProfile = vrouter.parseProfileURI(uri)
262+
this.toggleProfileEditor('show')
263+
} catch (error) {
264+
const label = document.querySelector('#importProfileModal div.label')
265+
label.innerHTML = error
266+
label.classList.remove('hidden')
267+
}
247268
},
248269
async saveProfile () {
249270
// save: proxies, mode, BWList
@@ -476,7 +497,6 @@ const myApp = new Vue({
476497
$('*[data-content]').popup('hide')
477498
$(this.$refs.loginModal)
478499
.modal({
479-
blurring: true,
480500
dimmerSettings: {
481501
opacity: 0.8
482502
}
@@ -493,7 +513,6 @@ const myApp = new Vue({
493513
showAboutModal () {
494514
$(this.$refs.aboutModal)
495515
.modal({
496-
blurring: true,
497516
dimmerSettings: {
498517
opacity: 0.8
499518
}
@@ -612,16 +631,26 @@ document.addEventListener('DOMContentLoaded', async () => {
612631
$('.tabular.menu .item').tab()
613632
$('.dropdown').dropdown()
614633
$('*[data-content]').popup()
615-
$('#profileModal').modal({
616-
dimmerSettings: {
617-
opacity: 0.2
618-
},
619-
detachable: false,
620-
closable: false
621-
})
634+
// $('#profileModal').modal({
635+
// dimmerSettings: {
636+
// opacity: 0.2
637+
// },
638+
// detachable: false,
639+
// closable: false
640+
// })
641+
// $('#importProfileModal').modal({
642+
// dimmerSettings: {
643+
// opacity: 0.2
644+
// },
645+
// detachable: false
646+
// })
622647
$('#profiles .ui.message').dimmer({
623648
opacity: 0,
624649
on: 'hover',
625650
duration: 10
626651
})
652+
$('#add-profile')
653+
.dropdown({
654+
on: 'hover'
655+
})
627656
})

js/vrouter-local.js

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1727,6 +1727,140 @@ echo ""`
17271727
await this.installNwWatchdog()
17281728
}
17291729
}
1730+
parseProfileURI (uri) {
1731+
// ssr://dnBzLmljeW1pbmQuY29tOjk5OTk6YXV0aF9hZXMxMjhfbWQ1OmNoYWNoYTIwOnRsczEuMl90aWNrZXRfYXV0aDphR0Z3Y0hramMzTnlJekl3TVRjLz9vYmZzcGFyYW09JnByb3RvcGFyYW09TXpJJnJlbWFya3M9UVc1a2NtOXBaQ0JUVTFJZ1JHVm1ZWFZzZEEmZ3JvdXA9ZG5Ceg
1732+
1733+
// ss://Y2hhY2hhMjA6aGFwcHkjc3MjMjAxNw@vps.icymind.com:7979?plugin=kcptun%3Bnocomp%3Dtrue%3Bmode%3Dfast%3Bkey%3Dhappy%23kt%232017%3Bcrypt%3Dnone#kcptun
1734+
1735+
// ss://Y2hhY2hhMjA6aGFwcHkjc3MjMjAxNw@vps.icymind.com:8989#%E6%98%8E%E6%98%8E%E6%98%8E
1736+
let profile = {
1737+
'name': '配置xx',
1738+
'action': 'new',
1739+
'mode': 'whitelist',
1740+
'proxies': 'ss',
1741+
'relayUDP': false,
1742+
'enableTunnelDns': true,
1743+
'selectedBL': {'gfwDomains': true, 'extraBlackList': true},
1744+
'selectedWL': {'chinaIPs': true, 'lanNetworks': true, 'extraWhiteList': true},
1745+
'shadowsocks': {
1746+
'address': '123.123.123.123',
1747+
'port': '8989',
1748+
'password': 'demo-paswd',
1749+
'timeout': 300,
1750+
'method': 'chacha20',
1751+
'fastopen': false
1752+
},
1753+
'shadowsocksr': {
1754+
'address': '123.123.123.123',
1755+
'port': '9999',
1756+
'password': 'demo-paswd',
1757+
'timeout': 300,
1758+
'method': 'chacha20',
1759+
'protocol': 'auth_aes128_md5',
1760+
'protocol_param': '32',
1761+
'obfs': 'tls1.2_ticket_auth',
1762+
'obfs_param': '',
1763+
'others': '',
1764+
'fastopen': false
1765+
},
1766+
'kcptun': {
1767+
'address': '',
1768+
'port': '',
1769+
'key': 'demo-secret',
1770+
'crypt': 'aes-128',
1771+
'mode': 'fast2',
1772+
'others': 'sndwnd=256;rcvwnd=2048;nocomp=true'
1773+
}
1774+
}
1775+
let type = uri.substr(0, uri.indexOf(':'))
1776+
if (type === 'ssr') {
1777+
profile.proxies = 'ssr'
1778+
let decode = Buffer.from(uri.substr(6), 'base64').toString()
1779+
const separatorIndex = decode.indexOf('/?')
1780+
let config = decode.substr(0, separatorIndex).split(':')
1781+
config[config.length - 1] = Buffer.from(config[config.length - 1], 'base64').toString()
1782+
;[profile.shadowsocksr.address, profile.shadowsocksr.port, profile.shadowsocksr.protocol, profile.shadowsocksr.method, profile.shadowsocksr.obfs, profile.shadowsocksr.password] = config
1783+
1784+
config = decode.substr(separatorIndex + 2).split('&')
1785+
config.forEach((pair) => {
1786+
let [key, value] = pair.split('=')
1787+
value = Buffer.from(value, 'base64').toString()
1788+
switch (key) {
1789+
case 'obfsparam':
1790+
profile.shadowsocksr.obfs_param = value
1791+
break
1792+
case 'protoparam':
1793+
profile.shadowsocksr.protocol_param = value
1794+
break
1795+
case 'remarks':
1796+
profile.name = value
1797+
break
1798+
case 'group':
1799+
break
1800+
default:
1801+
profile.shadowsocksr.others += `${key}=${value};`
1802+
}
1803+
})
1804+
} else if (type === 'ss') {
1805+
profile.proxies = 'ss'
1806+
const nameIndex = uri.lastIndexOf('#')
1807+
if (nameIndex >= 0) {
1808+
profile.name = decodeURIComponent(uri.substr(nameIndex + 1))
1809+
}
1810+
const separatorIndex = uri.indexOf('@')
1811+
if (separatorIndex > 0) {
1812+
// https://shadowsocks.org/en/spec/SIP002-URI-Scheme.html
1813+
// ss://[email protected]:8888/?plugin=url-encoded-plugin-argument-value&unsupported-arguments=should-be-ignored#Dummy+profile+name
1814+
let decode = Buffer.from(uri.substr(5, separatorIndex - 5), 'base64').toString()
1815+
;[profile.shadowsocks.method, profile.shadowsocks.password] = decode.split(':')
1816+
1817+
const pluginIndex = uri.indexOf('?plugin')
1818+
if (pluginIndex < 0) {
1819+
// without plugin
1820+
decode = uri.substr(separatorIndex + 1, nameIndex < 0 ? undefined : nameIndex - separatorIndex - 1)
1821+
;[profile.shadowsocks.address, profile.shadowsocks.port] = decode.split(':')
1822+
} else {
1823+
// with plugin
1824+
decode = uri.substr(separatorIndex + 1, pluginIndex - separatorIndex - 1)
1825+
;[profile.shadowsocks.address, profile.shadowsocks.port] = decode.split(':')
1826+
1827+
let plugin = uri.substr(pluginIndex + '?plugin'.length + 1, nameIndex - 1 - pluginIndex - '?plugin'.length)
1828+
let config = decodeURIComponent(plugin).split(';')
1829+
if (config[0] !== 'kcptun') {
1830+
throw Error(`unsupported plugin: ${config[0]}`)
1831+
} else {
1832+
profile.proxies = 'ssKt'
1833+
let others = ''
1834+
config.slice(1).forEach((pair) => {
1835+
let [key, value] = pair.split('=')
1836+
switch (key) {
1837+
case 'mode':
1838+
case 'key':
1839+
case 'crypt':
1840+
profile.kcptun[key] = value
1841+
break
1842+
default:
1843+
others += `${pair};`
1844+
}
1845+
})
1846+
profile.kcptun.address = profile.kcptun.address || profile.shadowsocks.address
1847+
profile.kcptun.port = profile.kcptun.port || profile.shadowsocks.port
1848+
profile.kcptun.others = others === '' ? profile.kcptun.others : others
1849+
}
1850+
}
1851+
} else {
1852+
// https://shadowsocks.org/en/config/quick-guide.html
1853+
// ss://YmYtY2ZiOnRlc3RAMTkyLjE2OC4xMDAuMTo4ODg4Cg#example-server
1854+
let index = uri.indexOf('#')
1855+
let decode = Buffer.from(uri.substr(5, index < 0 ? undefined : nameIndex - 5), 'base64').toString()
1856+
let config = decode.split('@')
1857+
;[profile.shadowsocks.address, profile.shadowsocks.port, profile.shadowsocks.method, profile.shadowsocks.password] = [...config[1].split(':'), ...config[0].split(':')]
1858+
}
1859+
} else {
1860+
throw Error('unsupported URI')
1861+
}
1862+
return profile
1863+
}
17301864
async copyTemplate (fileName) {
17311865
const template = path.join(__dirname, '..', 'config', fileName)
17321866
const dest = path.join(this.config.host.configDir, fileName)

0 commit comments

Comments
 (0)