├── .editorconfig
├── .eslintrc
├── .gitattributes
├── .gitignore
├── .travis.yml
├── Dockerfile
├── LICENSE
├── README.md
├── nginx.conf
├── package.json
├── public
├── index.html
└── manifest.json
├── src
├── App.css
├── App.js
├── Footbar.js
├── Navbar.js
├── PingCard.js
├── base64.js
├── index.css
├── index.js
├── serverList.json
└── utils.js
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*]
2 | end_of_line = lf
3 | indent_size = 4
4 | indent_style = space
5 | insert_final_newline = true
6 | charset = utf-8
7 | trim_trailing_whitespace = true
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "babel",
3 | "parserOptions": {
4 | "ecmaVersion": 7,
5 | "sourceType": "module"
6 | },
7 | "plugins": [
8 | "react"
9 | ],
10 | "ecmaFeatures": {
11 | "jsx": true
12 | },
13 | "env": {
14 | "jest": true
15 | },
16 | "rules": {
17 | "react/jsx-uses-vars": [ 2 ],
18 | "react/jsx-uses-react": [ 2 ],
19 | "quotes": [
20 | "error",
21 | "single"
22 | ],
23 | "indent": [
24 | "error",
25 | 4,
26 | {
27 | "SwitchCase": 1
28 | }
29 | ],
30 | "comma-dangle": [
31 | "error",
32 | "always-multiline"
33 | ],
34 | "newline-after-var": "error",
35 | "semi": [
36 | "error",
37 | "never"
38 | ],
39 | "eqeqeq": [
40 | "error",
41 | "always"
42 | ]
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.js -crlf
2 | *.json -crlf
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 | .idea/
19 |
20 | npm-debug.log*
21 | yarn-debug.log*
22 | yarn-error.log*
23 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "node"
4 | - "lts/*"
5 |
6 | install:
7 | - yarn
8 |
9 | script: 'true'
10 |
11 | cache:
12 | directories:
13 | - "node_modules"
14 |
15 | after_success:
16 | - yarn build
17 | - echo "torch.njs.app" > build/CNAME
18 |
19 | deploy:
20 | provider: pages
21 | skip_cleanup: true
22 | github_token: $GITHUB_TOKEN
23 | target_branch: gh-pages
24 | email: jiduye@gmail.com
25 | local-dir: build
26 | name: Travis Automatically Bot
27 | on:
28 | branch: master
29 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM alpine:3.8 as builder
2 | ADD . /app
3 | RUN apk --no-cache add nodejs yarn && \
4 | cd /app && yarn && yarn build
5 |
6 | FROM alpine:3.8
7 | RUN apk --no-cache add ca-certificates nginx
8 | COPY --from=builder /app/build /app
9 | COPY nginx.conf /etc/nginx/nginx.conf
10 |
11 | EXPOSE 80
12 | WORKDIR /app
13 |
14 | CMD nginx -g "daemon off;"
15 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Indexyz
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
Torch-Web
2 |
3 | > Simple and useful tcping tools
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | ## Quick start
15 | ```bash
16 | # Install deps
17 | yarn
18 |
19 | # Run it
20 | yarn run start
21 | ```
22 |
23 | ## Build
24 |
25 | * Replace `src/serverList.json` first if necessary
26 |
27 | ```
28 | cd Torch-Web
29 | docker build torch-web .
30 | ```
31 |
32 | ## License
33 |
34 | [MIT](https://github.com/Indexyz/Torch-Web/blob/master/LICENSE)
35 |
--------------------------------------------------------------------------------
/nginx.conf:
--------------------------------------------------------------------------------
1 | worker_processes auto;
2 | pid /var/run/nginx.pid;
3 |
4 | events {
5 | worker_connections 4096;
6 | use epoll;
7 | }
8 |
9 | http {
10 | sendfile on;
11 | tcp_nopush on;
12 | tcp_nodelay on;
13 | access_log /dev/stderr;
14 | server_tokens off;
15 | keepalive_timeout 15;
16 | keepalive_requests 100;
17 | keepalive_disable msie6;
18 | include /etc/nginx/mime.types;
19 | default_type application/octet-stream;
20 | error_log /dev/stderr error;
21 | gzip on;
22 | gzip_comp_level 6;
23 | gzip_min_length 512;
24 | gzip_buffers 4 8k;
25 | gzip_proxied any;
26 | gzip_vary on;
27 | gzip_disable "msie6";
28 | gzip_types
29 | text/css
30 | text/javascript
31 | text/xml
32 | text/plain
33 | text/x-component
34 | application/javascript
35 | application/x-javascript
36 | application/json
37 | application/xml
38 | application/rss+xml
39 | application/vnd.ms-fontobject
40 | font/truetype
41 | font/opentype
42 | image/svg+xml
43 | image/jpg
44 | image/png
45 | image/webp;
46 |
47 | server {
48 | listen 80;
49 | root /app;
50 | charset utf-8;
51 | index index.html;
52 | client_max_body_size 1000m;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "torch-web",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "antd": "3.19.2",
7 | "axios": "^0.19.0",
8 | "react": "^16.8.6",
9 | "react-dom": "^16.8.6",
10 | "react-flexbox-grid": "^2.1.2",
11 | "react-helmet": "^5.2.1",
12 | "react-scripts": "3.0.1",
13 | "uuid": "^3.3.2"
14 | },
15 | "scripts": {
16 | "start": "react-scripts start",
17 | "build": "react-scripts build",
18 | "eject": "react-scripts eject",
19 | "lint": "eslint src"
20 | },
21 | "devDependencies": {
22 | "eslint": "^5.16.0",
23 | "eslint-config-babel": "^9.0.0",
24 | "eslint-plugin-flowtype": "^3.9.1",
25 | "eslint-plugin-react": "^7.13.0"
26 | },
27 | "browserslist": {
28 | "production": [
29 | ">0.2%",
30 | "not dead",
31 | "not op_mini all"
32 | ],
33 | "development": [
34 | "last 1 chrome version",
35 | "last 1 firefox version",
36 | "last 1 safari version"
37 | ]
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | Torch
23 |
24 |
25 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Torch",
3 | "name": "Simple and useful tcping tools",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | @import '~antd/dist/antd.css';
2 |
3 | .siteContext {
4 | min-height: calc(100vh - 64px - 69px);
5 | }
6 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import Navbar from './Navbar'
3 | import Footbar from './Footbar'
4 | import PingCard from './PingCard'
5 | import { Row, Col } from 'react-flexbox-grid'
6 | import { parseLink } from './utils'
7 | import {
8 | Layout,
9 | Input,
10 | Button,
11 | InputNumber,
12 | Modal,
13 | message } from 'antd'
14 | import uuid from 'uuid'
15 | import './App.css'
16 | import { Helmet } from 'react-helmet'
17 |
18 | const { TextArea } = Input
19 |
20 | class App extends Component {
21 | state = {
22 | hosts: [],
23 | input: {
24 | host: 'www.baidu.com',
25 | port: 443,
26 | },
27 | text: '',
28 | displayModal: false,
29 | }
30 |
31 | addHost = (docs) => {
32 | this.setState(pre => {
33 | pre.hosts = [Object.assign({}, docs, {
34 | uuid: uuid.v4(),
35 | }), ...pre.hosts]
36 | return pre
37 | })
38 | }
39 |
40 | updateHost(event) {
41 | const value = event.target.value
42 |
43 | this.setState(pre => {
44 | pre.input.host = value
45 | return pre
46 | })
47 | }
48 |
49 | updateText(event) {
50 | const value = event.target.value
51 |
52 | this.setState(pre => {
53 | pre.text = value
54 | return pre
55 | })
56 | }
57 |
58 | showModal = () => {
59 | this.setState({
60 | displayModal: true,
61 | })
62 | }
63 |
64 | reTest = () => {
65 | const action = async () => {
66 | const oldTests = this.state.hosts
67 | let counter = 0
68 |
69 | this.setState(state => {
70 | state.hosts = []
71 | return state
72 | })
73 |
74 | for (const item of oldTests.reverse()) {
75 | counter += 1
76 | item.uuid = null
77 | this.addHost(item)
78 | message.loading(`Processing ${counter} of ${oldTests.length}`, 0.9)
79 | await new Promise(resolve => setTimeout(resolve, 1000))
80 | }
81 | }
82 |
83 | action().catch(err => { console.log(err) })
84 | }
85 |
86 | handleCancel = () => {
87 | this.setState({
88 | displayModal: false,
89 | })
90 | }
91 |
92 | handleMultiAdd = () => {
93 | this.handleCancel()
94 |
95 | message.info('Processing links, please wait')
96 |
97 | const action = async () => {
98 | let counter = 0
99 | const lines = (await Promise.all(this.state.text.split('\n')
100 | .map(item => item.trim())
101 | .map(parseLink)))
102 | .reduce((previous, current) => {
103 | console.log(current)
104 | if (current === null) {
105 | message.error('在解析部分链接时出现问题,请检查键入信息', 5)
106 | }
107 | if (Array.isArray(current)) {
108 | return [...current, ...previous]
109 | }
110 | return [current, ...previous]
111 | }, [])
112 | .filter(link => link !== null)
113 |
114 | for (const line of lines) {
115 | counter += 1
116 | this.addHost(line)
117 | message.loading(`Processing ${counter} of ${lines.length}`, 0.9)
118 | await new Promise(resolve => setTimeout(resolve, 1000))
119 | }
120 | message.success('节点添加完毕!部分检测可能尚未完成,请耐心等待……', 3)
121 | }
122 |
123 | action()
124 | }
125 |
126 | updatePort(value) {
127 | this.setState(pre => {
128 | pre.input.port = value
129 | return pre
130 | })
131 | }
132 |
133 | render() {
134 | window.dataLayer = window.dataLayer || []
135 | function gtag(){
136 | window.dataLayer.push(arguments)
137 | }
138 | gtag('js', new Date())
139 | gtag('config', 'UA-97074604-2')
140 |
141 | return (
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
155 |
160 |
165 |
166 |
167 |
168 |
174 |
180 |
181 |
182 | {this.state.hosts.map(item => {
183 | return (
184 |
191 |
192 |
193 | )
194 | })}
195 |
196 |
197 |
198 |
204 |
208 | 目前支持以下链接格式:
209 |
210 |
211 |
212 |
213 |
214 | sub:订阅地址 (sub:https://ADDRESS)
215 |
216 | arn
217 |
218 |
219 | 一行一个链接,一次可混合多种链接检测
220 |
221 |
222 |
223 |
224 | )
225 | }
226 | }
227 |
228 | export default App
229 |
--------------------------------------------------------------------------------
/src/Footbar.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { Layout } from 'antd'
3 |
4 | class Footbar extends Component {
5 | render() {
6 | return (
7 |
8 |
9 | Torch
10 | , ©2018 Created by Indexyz
11 |
12 |
13 | )
14 | }
15 | }
16 |
17 | export default Footbar
18 |
--------------------------------------------------------------------------------
/src/Navbar.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { Menu, Icon, Layout } from 'antd'
3 |
4 | class Navbar extends Component {
5 | render() {
6 | return (
7 |
8 |
9 |
18 |
19 |
20 | )
21 | }
22 | }
23 |
24 | export default Navbar
25 |
--------------------------------------------------------------------------------
/src/PingCard.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import {Card, Tag, Popover} from 'antd'
3 | import {Row, Col} from 'react-flexbox-grid'
4 | import serverList from './serverList.json'
5 | import axios from 'axios'
6 |
7 | async function ping(apiRoot, host, port) {
8 | const res = await axios.get(`${apiRoot}/${host}/${port}`)
9 |
10 | return res.data
11 | }
12 |
13 | function getInitState() {
14 | const state = {}
15 |
16 | for (const item of serverList) {
17 | state[item.name] = {
18 | 'type': 'loading',
19 | 'name': item.name,
20 | }
21 | }
22 | return state
23 | }
24 |
25 | class PingCard extends Component {
26 | state = getInitState()
27 |
28 | async componentDidMount() {
29 | for (const item of serverList) {
30 | ping(item.address, this.props.host, this.props.port)
31 | .then(data => {
32 | this.setState({
33 | [item.name]: {
34 | 'type': data.status === true ? data.time : 'offline',
35 | 'name': item.name,
36 | },
37 | })
38 | })
39 | .catch(() => {
40 | this.setState({
41 | [item.name]: {
42 | 'type': 'error',
43 | 'name': item.name,
44 | },
45 | })
46 | })
47 | }
48 | }
49 |
50 | getTag(status) {
51 | if (status.type === 'loading') {
52 | return {status.name}: Loading
53 | } else if (status.type === 'offline') {
54 | return {status.name}: Down
55 | } else if (status.type === 'error') {
56 | return {status.name}: Error
57 | }
58 | return
59 | {status.name}: {status.type === null ? '???' : status.type.toFixed(2)} ms
60 |
61 | }
62 |
63 | render() {
64 | return (
65 | {
67 | if (this.props.title === undefined) {
68 | return this.props.host + ':' + this.props.port
69 | }
70 | return this.props.title
71 | })()}
72 | >
73 |
74 | {serverList.map(item =>
75 |
76 |
77 | {this.getTag(this.state[item.name])}
78 |
79 |
80 | )}
81 |
82 |
83 |
84 | )
85 | }
86 | }
87 |
88 | export default PingCard
89 |
--------------------------------------------------------------------------------
/src/base64.js:
--------------------------------------------------------------------------------
1 |
2 | /**
3 | *
4 | * Base64 encode / decode
5 | * http://www.webtoolkit.info/
6 | *
7 | **/
8 |
9 | const Base64 = {
10 |
11 | // private property
12 | _keyStr: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=',
13 |
14 | // public method for encoding
15 | encode: function (input) {
16 | let output = ''
17 | let chr1, chr2, chr3, enc1, enc2, enc3, enc4
18 | let i = 0
19 |
20 | input = Base64._utf8_encode(input)
21 |
22 | while (i < input.length) {
23 |
24 | chr1 = input.charCodeAt(i++)
25 | chr2 = input.charCodeAt(i++)
26 | chr3 = input.charCodeAt(i++)
27 |
28 | enc1 = chr1 >> 2
29 | enc2 = ((chr1 & 3) << 4) | (chr2 >> 4)
30 | enc3 = ((chr2 & 15) << 2) | (chr3 >> 6)
31 | enc4 = chr3 & 63
32 |
33 | if (isNaN(chr2)) {
34 | enc3 = enc4 = 64
35 | } else if (isNaN(chr3)) {
36 | enc4 = 64
37 | }
38 |
39 | output = output +
40 | this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) +
41 | this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4)
42 |
43 | }
44 |
45 | return output
46 | },
47 |
48 | // public method for decoding
49 | decode: function (input) {
50 | let output = ''
51 | let chr1, chr2, chr3
52 | let enc1, enc2, enc3, enc4
53 | let i = 0
54 |
55 | input = input.replace(/[^A-Za-z0-9+/=]/g, '')
56 |
57 | while (i < input.length) {
58 |
59 | enc1 = this._keyStr.indexOf(input.charAt(i++))
60 | enc2 = this._keyStr.indexOf(input.charAt(i++))
61 | enc3 = this._keyStr.indexOf(input.charAt(i++))
62 | enc4 = this._keyStr.indexOf(input.charAt(i++))
63 |
64 | chr1 = (enc1 << 2) | (enc2 >> 4)
65 | chr2 = ((enc2 & 15) << 4) | (enc3 >> 2)
66 | chr3 = ((enc3 & 3) << 6) | enc4
67 |
68 | output = output + String.fromCharCode(chr1)
69 |
70 | if (enc3 !== 64) {
71 | output = output + String.fromCharCode(chr2)
72 | }
73 | if (enc4 !== 64) {
74 | output = output + String.fromCharCode(chr3)
75 | }
76 |
77 | }
78 |
79 | output = Base64._utf8_decode(output)
80 |
81 | return output
82 |
83 | },
84 |
85 | // private method for UTF-8 encoding
86 | _utf8_encode: function (string) {
87 | string = string.replace(/\r\n/g, '\n')
88 | let utftext = ''
89 |
90 | for (let n = 0; n < string.length; n++) {
91 |
92 | const c = string.charCodeAt(n)
93 |
94 | if (c < 128) {
95 | utftext += String.fromCharCode(c)
96 | }
97 | else if ((c > 127) && (c < 2048)) {
98 | utftext += String.fromCharCode((c >> 6) | 192)
99 | utftext += String.fromCharCode((c & 63) | 128)
100 | }
101 | else {
102 | utftext += String.fromCharCode((c >> 12) | 224)
103 | utftext += String.fromCharCode(((c >> 6) & 63) | 128)
104 | utftext += String.fromCharCode((c & 63) | 128)
105 | }
106 |
107 | }
108 |
109 | return utftext
110 | },
111 |
112 | // private method for UTF-8 decoding
113 | _utf8_decode: function (utftext) {
114 | let string = ''
115 | let i = 0
116 | let c = 0,
117 | c3 = 0,
118 | c2 = 0
119 |
120 | while (i < utftext.length) {
121 |
122 | c = utftext.charCodeAt(i)
123 |
124 | if (c < 128) {
125 | string += String.fromCharCode(c)
126 | i++
127 | }
128 | else if ((c > 191) && (c < 224)) {
129 | c2 = utftext.charCodeAt(i + 1)
130 | string += String.fromCharCode(((c & 31) << 6) | (c2 & 63))
131 | i += 2
132 | }
133 | else {
134 | c2 = utftext.charCodeAt(i + 1)
135 | c3 = utftext.charCodeAt(i + 2)
136 | string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63))
137 | i += 3
138 | }
139 |
140 | }
141 |
142 | return string
143 | },
144 |
145 | }
146 |
147 | export default Base64
148 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | }
6 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import './index.css'
4 | import App from './App'
5 |
6 | ReactDOM.render(, document.getElementById('root'))
7 |
--------------------------------------------------------------------------------
/src/serverList.json:
--------------------------------------------------------------------------------
1 | [{
2 | "name": "USA-LOS",
3 | "address": "https://usa-los-tcping.torch.njs.app",
4 | "info": "Indexyz"
5 | }, {
6 | "name": "China-上海阿里云",
7 | "address": "https://torch.biu.best",
8 | "info": "Biu https://t.me/MCT_boom"
9 | }, {
10 | "name": "China-多点同测",
11 | "address": "https://ping.regend.xyz",
12 | "info": "小灰 https://bio.regend.xyz"
13 | },{
14 | "name": "China-江苏联通",
15 | "address": "https://jiangsu.otto23.ga",
16 | "info": "小绿 https://xlego.xyz/"
17 | }]
18 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | import base64 from './base64'
2 | import axios from 'axios'
3 |
4 | /**
5 | *
6 | * @param {string} origin
7 | */
8 | function fillMissingPadding(origin) {
9 | if (origin.length % 4 !== 0) {
10 | let padding = ''
11 |
12 | for (let i = 0; i < origin.length % 4; i++) {
13 | padding += '='
14 | }
15 |
16 | return origin + padding
17 | }
18 | return origin
19 | }
20 |
21 | /**
22 | * URL-Safe Base64 Decode
23 | *
24 | * @param {string} encoded
25 | */
26 | function urlSafeBase64Decode(encoded) {
27 | return base64.decode(
28 | fillMissingPadding(encoded.replace(/_/g, '/')
29 | .replace(/-/g, '+'))
30 | )
31 | }
32 |
33 | /**
34 | * https://shadowsocks.org/en/config/quick-guide.html
35 | * Origin ss://BASE64-ENCODED-STRING-WITHOUT-PADDING#TAG
36 | * Plain text ss://method:password@hostname:port
37 | *
38 | * @param link
39 | * @returns {Promise<{title: *, host: *, port: number}>}
40 | */
41 | async function parseSSLink(link) {
42 | const split = link.split('#')
43 | const originLink = urlSafeBase64Decode(split[0].substr(5))
44 | const a = originLink.split(':')
45 | const port = Number(a[2])
46 | const host = a[1].split('@')[1]
47 |
48 | if (port >= 1 && port <= 65535 && host) {
49 | return {
50 | title: split[1] === '' ? host : split[1],
51 | host: host,
52 | port: Number(port),
53 | }
54 | }
55 |
56 | return null
57 | }
58 |
59 | function parseV2rayLink(link) {
60 | const jsonData = JSON.parse(urlSafeBase64Decode(link.substr(8)))
61 |
62 | return {
63 | title: jsonData['ps'],
64 | host: jsonData['add'],
65 | port: Number(jsonData['port']),
66 | }
67 | }
68 |
69 | /**
70 | *
71 | * @param {string} link
72 | * @returns {object} parsed object
73 | */
74 | function parseSSRLink(link) {
75 | let originLink = urlSafeBase64Decode(link.substr(6))
76 | const decoded = {}
77 |
78 | const keys = ['server', 'server_port', 'portocol', 'method', 'obfs', 'password']
79 |
80 | for (const key of keys) {
81 | decoded[key] = originLink.substr(0, originLink.indexOf(':'))
82 | originLink = originLink.substr(originLink.indexOf(':') + 1)
83 | }
84 | const qs = JSON.parse(
85 | '{"' +
86 | decodeURI(originLink.substr(2))
87 | .replace(/"/g, '\\"')
88 | .replace(/&/g, '","')
89 | .replace(/=/g, '":"') + '"}')
90 |
91 | return {
92 | title: urlSafeBase64Decode(qs['remarks']),
93 | host: decoded['server'],
94 | port: Number(decoded['server_port']),
95 | }
96 | }
97 |
98 | /**
99 | * Parse normal link like google.com:443
100 | *
101 | * @param {string} origin
102 | */
103 | function parseNormalLink(origin) {
104 | const splited = origin.split(':')
105 |
106 | if (splited.length === 2) {
107 | return {
108 | host: splited[0],
109 | port: Number(splited[1]),
110 | }
111 | } else if (origin.trim() === '') {
112 | return null
113 | }
114 | return {
115 | host: origin,
116 | port: 22,
117 | }
118 | }
119 |
120 | const avalibleSubMap = [
121 | 'ssr://',
122 | 'vmess://',
123 | ]
124 |
125 | function inSubMap(link) {
126 | return avalibleSubMap.filter(it => link.startsWith(it)).length > 0
127 | }
128 |
129 | /**
130 | * Test Subscription link
131 | *
132 | * @param {string} origin
133 | */
134 | async function parseSubscription(origin) {
135 | const subscriptionLink = origin.substr(4)
136 | const reqURL = `https://cors-anywhere.herokuapp.com/${subscriptionLink}`
137 | const resp = await axios.get(reqURL)
138 |
139 | const r = urlSafeBase64Decode(fillMissingPadding(resp.data))
140 | .split('\n')
141 | .map(item => item.trim())
142 | .filter(inSubMap)
143 |
144 | return Promise.all(r.map(parseLink))
145 | }
146 |
147 | /**
148 | * Get link parsed object
149 | *
150 | * @param {string} link
151 | * @returns {object} parsed object
152 | */
153 | async function parseLink(link) {
154 | try {
155 | if (link.startsWith('ss://')) {
156 | return parseSSLink(link)
157 | }
158 | if (link.startsWith('ssr://')) {
159 | return parseSSRLink(link)
160 | }
161 | if (link.startsWith('sub:')) {
162 | return await parseSubscription(link)
163 | }
164 | if (link.startsWith('vmess://')) {
165 | return parseV2rayLink(link)
166 | }
167 | return parseNormalLink(link)
168 | } catch (e) {
169 | /**
170 | * Not need handle here
171 | * Paser error will skip this node
172 | */
173 | }
174 | return null
175 | }
176 |
177 | export {
178 | parseLink,
179 | }
180 |
--------------------------------------------------------------------------------