Add: API request lib

This commit is contained in:
Dreamacro 2018-10-06 15:29:52 +08:00
parent fc4c58ae92
commit b60ebfe75f
11 changed files with 804 additions and 696 deletions

1221
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -26,19 +26,20 @@
"start-dev": "webpack-dev-server --config=configs/webpack/dev.js" "start-dev": "webpack-dev-server --config=configs/webpack/dev.js"
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "^7.1.0", "@babel/cli": "^7.1.2",
"@babel/core": "^7.1.0", "@babel/core": "^7.1.2",
"@babel/preset-env": "^7.1.0", "@babel/preset-env": "^7.1.0",
"@babel/preset-react": "^7.0.0", "@babel/preset-react": "^7.0.0",
"@types/node": "^10.10.3", "@types/node": "^10.11.4",
"@types/react": "^16.4.14", "@types/react": "^16.4.15",
"@types/react-dom": "^16.0.7", "@types/react-dom": "^16.0.8",
"@types/react-i18next": "^7.8.2", "@types/react-i18next": "^7.8.2",
"@types/react-router-dom": "^4.3.1", "@types/react-router-dom": "^4.3.1",
"@types/react-sortable-hoc": "^0.6.4", "@types/react-sortable-hoc": "^0.6.4",
"@types/yaml": "^1.0.0",
"autoprefixer": "^9.1.5", "autoprefixer": "^9.1.5",
"awesome-typescript-loader": "^5.2.1", "awesome-typescript-loader": "^5.2.1",
"babel-loader": "^8.0.2", "babel-loader": "^8.0.4",
"css-loader": "^1.0.0", "css-loader": "^1.0.0",
"file-loader": "^2.0.0", "file-loader": "^2.0.0",
"html-webpack-plugin": "^3.2.0", "html-webpack-plugin": "^3.2.0",
@ -49,28 +50,29 @@
"react-hot-loader": "^4.3.11", "react-hot-loader": "^4.3.11",
"sass-loader": "^7.1.0", "sass-loader": "^7.1.0",
"style-loader": "^0.23.0", "style-loader": "^0.23.0",
"stylelint": "^9.5.0", "stylelint": "^9.6.0",
"stylelint-config-standard": "^18.2.0", "stylelint-config-standard": "^18.2.0",
"stylelint-webpack-plugin": "^0.10.5", "stylelint-webpack-plugin": "^0.10.5",
"tslint": "^5.11.0", "tslint": "^5.11.0",
"tslint-config-standard": "^8.0.1", "tslint-config-standard": "^8.0.1",
"tslint-loader": "^3.6.0", "tslint-loader": "^3.6.0",
"uglifyjs-webpack-plugin": "^2.0.1", "uglifyjs-webpack-plugin": "^2.0.1",
"webpack": "^4.19.1", "webpack": "^4.20.2",
"webpack-cli": "^3.1.1", "webpack-cli": "^3.1.2",
"webpack-dev-middleware": "^3.3.0", "webpack-dev-middleware": "^3.4.0",
"webpack-dev-server": "^3.1.8", "webpack-dev-server": "^3.1.9",
"webpack-merge": "^4.1.4", "webpack-merge": "^4.1.4",
"webpack-pwa-manifest": "^3.7.1" "webpack-pwa-manifest": "^3.7.1"
}, },
"dependencies": { "dependencies": {
"axios": "^0.18.0",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"dayjs": "^1.7.5", "dayjs": "^1.7.7",
"i18next": "^11.9.0", "i18next": "^11.9.0",
"i18next-browser-languagedetector": "^2.2.3", "i18next-browser-languagedetector": "^2.2.3",
"immer": "^1.7.2", "immer": "^1.7.2",
"ini": "^1.3.5", "ini": "^1.3.5",
"mobx": "^5.1.2", "mobx": "^5.5.0",
"mobx-react": "^5.2.8", "mobx-react": "^5.2.8",
"mobx-react-router": "^4.0.5", "mobx-react-router": "^4.0.5",
"node-sass": "^4.9.3", "node-sass": "^4.9.3",
@ -79,6 +81,7 @@
"react-i18next": "^7.12.0", "react-i18next": "^7.12.0",
"react-router-dom": "^4.3.1", "react-router-dom": "^4.3.1",
"react-sortable-hoc": "^0.8.3", "react-sortable-hoc": "^0.8.3",
"typescript": "^3.0.3" "typescript": "^3.1.1",
"yaml": "^1.0.0"
} }
} }

View File

@ -1,5 +1,5 @@
export function getLocalStorageItem (key: string) { export function getLocalStorageItem (key: string, defaultValue = '') {
return window.localStorage.getItem(key) return window.localStorage.getItem(key) || defaultValue
} }
export function setLocalStorageItem (key: string, value: string) { export function setLocalStorageItem (key: string, value: string) {
@ -22,3 +22,5 @@ export async function to <T, E = Error> (promise: any): Promise<[T, E]> {
return [null as T, e] return [null as T, e]
} }
} }
export type Partial<T> = { [P in keyof T]?: T[P] }

View File

@ -1,36 +0,0 @@
const sectionExpr = /^\[(.*)\]/
const lineBreak = /\r?\n/g
const isSectionLine = (line: string) => sectionExpr.test(line)
const formatSection = (text: string) =>
text.split(lineBreak)
.map(t => t.trim())
.filter(t => t && t[0] !== ';')
.map(t => t.split('=', 2))
.filter(pair => pair.length === 2)
.reduce((map, [key, value]) => map.set(key.trim(), value.trim()), new Map<string, string>())
const iniParser = (text = '') => {
const section = new Map<string, string>()
if (text.length === 0) return
const lines = text.split(lineBreak)
let content: string[] = []
let sectionName = ''
for (const line of lines) {
if (isSectionLine(line)) {
if (sectionName !== '') {
section.set(sectionName, content.join('\n'))
}
content = []
const match = line.match(sectionExpr)
sectionName = match && match[1]
} else {
content.push(line)
}
}
if (sectionName !== '') {
section.set(sectionName, content.join('\n'))
}
return section
}

View File

@ -25,7 +25,7 @@ export interface JsBridgeAPI {
/** /**
* Call a native handle * Call a native handle
*/ */
callHandler: (handleName: string, data?: any, responseCallback?: (responseData: any) => void) => void callHandler: <T>(handleName: string, data?: any, responseCallback?: (responseData: T) => void) => void
/** /**
* Who knows * Who knows
@ -115,8 +115,8 @@ export class JsBridge {
setTimeout(() => document.documentElement.removeChild(WVJBIframe), 0) setTimeout(() => document.documentElement.removeChild(WVJBIframe), 0)
} }
public callHandler (handleName: string, data?: any) { public callHandler<T> (handleName: string, data?: any) {
return new Promise(resolve => { return new Promise<T>((resolve) => {
this.instance.callHandler( this.instance.callHandler(
handleName, handleName,
data || undefined, data || undefined,
@ -130,11 +130,11 @@ export class JsBridge {
} }
public readConfigString () { public readConfigString () {
return this.callHandler('readConfigString') return this.callHandler<string>('readConfigString')
} }
public getPasteboard () { public getPasteboard () {
return this.callHandler('getPasteboard') return this.callHandler<string>('getPasteboard')
} }
public setPasteboard (data: string) { public setPasteboard (data: string) {

99
src/lib/request.ts Normal file
View File

@ -0,0 +1,99 @@
import axios, { AxiosInstance } from 'axios'
import { Partial, getLocalStorageItem } from '@lib/helper'
import { isClashX } from '@lib/jsBridge'
import { rootStores } from '@lib/createStore'
let instance: Request
export interface Config {
port: number
'socket-port': number
'redir-port': number
'allow-lan': boolean
mode: string
'log-level': string
}
export interface Rules {
rules: { name: string, payload: string }[]
}
export interface Proxies {
proxies: {
[key: string]: Proxy
}
}
export interface Proxy {
type: 'Direct' | 'Selector' | 'Reject' | 'URLTest' | 'Shadowsocks' | 'Vmess' | 'Socks' | 'Fallback'
now?: string
all?: string[]
}
export class Request {
protected instance: AxiosInstance
constructor (host: string, secret?: string) {
this.instance = axios.create({
baseURL: host,
headers: secret ? { Authorization: `Bearer ${secret}` } : {}
})
}
getConfig () {
return this.instance.get<Config>('configs')
}
updateConfig (config: Partial<Config>) {
return this.instance.put<void>('configs', config)
}
getRules () {
return this.instance.get<Rules>('rules')
}
updateRules () {
return this.instance.put<void>('rules')
}
getProxies () {
return this.instance.get<Proxies>('proxies')
}
getProxy (name: string) {
return this.instance.get<Proxy>('proxies/:name', { params: { name } })
}
getProxyDelay (name: string) {
return this.instance.get<{ delay: number }>('proxies/:name/delay', { params: { name } })
}
changeProxySelected (name: string, select: string) {
return this.instance.get<void>('proxies/:name', { params: { name }, data: { name: select } })
}
}
export async function Instance () {
if (instance) {
return instance
}
if (isClashX()) {
await rootStores.config.fetchAndParseConfig()
const general = rootStores.config.config.general
instance = new Request(
`http://${general.externalControllerAddr}:${general.externalControllerPort}`,
general.secret
)
return instance
}
const hostname = getLocalStorageItem('externalControllerAddr', '')
const port = getLocalStorageItem('externalControllerPort', '')
const secret = getLocalStorageItem('secret', '')
if (!hostname || !port) {
throw new Error('can\'t get hostname or port')
}
instance = new Request(`http://${hostname}:${port}`, secret)
return instance
}

View File

@ -19,11 +19,40 @@ export interface Config {
*/ */
socksPort?: number socksPort?: number
/**
* redir proxy port
*/
redirPort?: number
/**
* proxy is allow lan
*/
allowLan?: boolean
/** /**
* controller port * controller port
*/ */
externalController: number externalControllerPort?: number
/**
* controller address
*/
externalControllerAddr?: string
/**
* controller secret
*/
secret?: string
/**
* clash proxy mode
*/
mode?: string
/**
* clash tty log level
*/
logLevel?: string
} }
proxy?: Proxy[] proxy?: Proxy[]

View File

@ -30,10 +30,14 @@ export interface ShadowsocksProxy {
port?: number port?: number
cipter?: string cipher?: string
password?: string password?: string
obfs?: string
'obfs-host'?: string
} }
export interface VmessProxy { export interface VmessProxy {
@ -48,7 +52,7 @@ export interface VmessProxy {
alterid?: number alterid?: number
cipter?: string cipher?: string
tls?: boolean tls?: boolean
@ -75,7 +79,7 @@ export interface ProxyGroup {
* configs of proxy server * configs of proxy server
* now support select and url-test * now support select and url-test
*/ */
config?: SelectProxyGroup | UrlTestProxyGroup config?: SelectProxyGroup | UrlTestProxyGroup | FallbackProxyGroup
} }
@ -87,6 +91,18 @@ export interface SelectProxyGroup {
} }
export interface FallbackProxyGroup {
type?: 'fallback'
proxies?: string[] // proxy names
url?: string
interval?: number // second
}
export interface UrlTestProxyGroup { export interface UrlTestProxyGroup {
type?: 'url-test' type?: 'url-test'

View File

@ -2,9 +2,9 @@ export interface Rule {
type?: RuleType type?: RuleType
value?: string payload?: string
use?: string // proxy or proxy group name proxy?: string // proxy or proxy group name
} }

View File

@ -1,12 +1,12 @@
import { observable, action, runInAction } from 'mobx' import { observable, action, runInAction } from 'mobx'
import { parse } from 'ini' import * as yaml from 'yaml'
import { Config } from '@models' import * as Models from '@models'
import { jsBridge } from '@lib/jsBridge' import { jsBridge } from '@lib/jsBridge'
export class ConfigStore { export class ConfigStore {
@observable @observable
config: Config = {} config: Models.Config = {}
@observable @observable
public state: 'pending' | 'ok' | 'error' = 'pending' public state: 'pending' | 'ok' | 'error' = 'pending'
@ -26,11 +26,37 @@ export class ConfigStore {
} }
// otherwise parse ini // otherwise parse ini
const config = parse(rawConfig) const config = yaml.parse(rawConfig)
console.log(config) const externalController = config['external-controller'] as string || ''
const host = externalController.split(':')
this.config = config const proxies = config.Proxy as any[] || []
const proxy: Models.Proxy[] = proxies
.filter(p => ['vmess', 'ss', 'socks5'].includes(p.type))
.map(p => ({ name: p.name, config: p }))
const proxyGroups = config['Proxy Group'] as any[] || []
const proxyGroup: Models.ProxyGroup[] = proxyGroups
.filter(p => ['url-test', 'select', 'fallback'].includes(p.type))
.map(p => ({ name: p.name, config: p }))
this.config = {
general: {
port: config.port || 0,
socksPort: config['socks-port'] || 0,
redirPort: config['redir-port'] || 0,
allowLan: config['allow-lan'] || false,
externalControllerAddr: host[0] || '',
externalControllerPort: parseInt(host[1], 10) || 0,
secret: config.secret || '',
logLevel: config['log-level'] || 'info',
mode: config.mode || 'Rule'
},
proxy,
proxyGroup,
rules: config['Rule'] || []
}
this.state = 'ok' this.state = 'ok'
}) })
} }

View File

@ -7,7 +7,7 @@
"module": "commonjs", "module": "commonjs",
"target": "es5", "target": "es5",
"jsx": "react", "jsx": "react",
"lib": ["es5", "es6", "dom"], "lib": ["es5", "es6", "dom", "es2017"],
"experimentalDecorators": true, "experimentalDecorators": true,
"downlevelIteration": true, "downlevelIteration": true,
"baseUrl": ".", "baseUrl": ".",