├── .DS_Store ├── README.md ├── WX20220328.png └── proxy-code ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── base64.js ├── client.js ├── config.js ├── getCache.js ├── package-lock.json ├── package.json ├── request.txt ├── response.txt ├── server.js ├── setCache.js ├── snowflake.js ├── sock5Client.js ├── sock5Server.js ├── sock5ZClientProxy.js └── test └── testCache.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/traceless/awkward-proxy/HEAD/.DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # awkward-proxy 2 | 基于公司代理的白名单域名上访问互联网,解决部分访问互联网的限制,也就是内网办公的限制。 3 | 4 | ## 1、背景 5 | 目前有一些传统但又打着互联网的公司经常会做一些愚蠢且无聊的事情。已经2020年了,还是有不少公司是内网办公的,传统的银行,金融,安全类的公司就是这样操作的,不给员工上外网,这个还能理解。但是一些号称大厂,IT一线公司,还是不给上外网,那么怎么解决这个问题?解决的前提是你得能访问一些代理白名单的外网,一般这样的公司都会预留一些域名白名单的来允许你访问部分的网站,比如允许你访问CSDN,访问微信开发者中心等一些常用的技术网站,如果这些都没有,那么就不用看下去了。 6 | 7 | ## 2、实现的思路 8 | 如图: 9 | ![image](https://github.com/traceless/awkward-proxy/blob/master/WX20220328.png) 10 | 11 | 1. 把内网pc的请求信息存放到白名单的互联网文章A中,通常请求信息有url, param/body, header等 12 | 2. 外网pc定时从文章A拿到请求信息体,然后通过外网请求链接获取内容 13 | 3. 将获取到的内容写入到文章B中,通常就是包含header,body信息等 14 | 4. 内网pc定时从文章B获取到响应信息,然后返回给浏览器 15 | 16 | ## 3、具体实现细节 17 | 18 | ### 客户端client.js 19 | - 所有浏览器请求先存放到requestArray中,然后定时把请求信息列表写入文章req中 20 | - 定时获取文章res内容,把内容存放到responseArray 21 | - 然后等待轮询responseArray, 根据reqId匹配对应的响应进行处理,主要是把html的url全部替换成http,返回给浏览器 22 | 23 | ### 服务端server.js 24 | - 定时获取文章req内容,解析得到requestArray,遍历requestArray,请求url链接的内容,并且存放到resultResponse数组中 25 | - 定时把resultResponse内容写入到文章res中,客户端会定时拉取这些响应 26 | 27 | ## 4、配置信息 28 | 1. 例子中使用的是微信公众平台的文章,所以需要公众号账号,登录后获取cookie和token,然后需要素材文章的appmsgid 2个,一个请求,一个响应。 29 | 2. 测试时候可以使用本地的文件作为中间交互的载体 30 | 3. 设置浏览器的代理服务器为本地的client开发的端口 31 | 4. 避免被中间人挟持,可以配置Base64的secret。当然也可以使用对称加密后再转标准base64字符,自由修改函数的实现,这里提供的是实现的思路。 32 | 33 | ### 运行 34 | 1. npm i 安装必要的模块 35 | 2. 内网pc执行 node client.js (然后电脑的浏览器配置代理的使用client.js暴露的端口) 36 | 3. 外网pc执行 node server.js (也可以使用在手机上执行) 37 | 38 | ## 5、注意 39 | 1. 例子中使用的是微信公众平台的文章作为中间存放信息交互的载体。目前还不支持https代理,不过已经做了折中的方式,如果要访问https的网站,请变通比如 http://www.baidu.com/_sssss, 给域名添加后缀来解决。因为https的代理比较复杂,我这里就不做太复杂的实现了。按思路理论上可以实现sock5代理,目前实现的sock5代理效果不行,不过它能用,还能看视频,大家可以本地测试下,很慢20s+,慢的离谱,大家可以自行优化一下。。。 40 | 2. 目前看来实现http就已经很慢了,假如2秒定时轮询的话,访问一个网页大概需要10+秒加载完成,反正总比没有要强把,理论上比正常上网慢4-6秒。如果外网域名有暴露推送的接口,那么速度会快很多,思路都差不多,自行实现。里面有不少的bug,主要是一些网站的js会做加密,无法解析替换https,小问题还是挺多的,整体可用。你也可以把一些域名限制,默认所有链接默认走https,但还是无法解决js加密问题,sock5全代理则无这样的问题。 41 | 3. 暂时没实现上传的支持,看看源码请自行实现,不难。下载文件理论上可以的,还没测试过。不过按文章存放的字数限制来说,测试过2.5M文件转base64能存到单个文章中(其实也可以放到多个文章中,之前测试过可以,大家自行实现即可),更大的10M就挂了,其他大小没测试了,反正当下载没啥必要。 42 | 4. BTW: base64是比较方便的编码设计,这次案例中学到了编码的姿势。 43 | -------------------------------------------------------------------------------- /WX20220328.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/traceless/awkward-proxy/HEAD/WX20220328.png -------------------------------------------------------------------------------- /proxy-code/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /proxy-code/.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | config/*.js 3 | -------------------------------------------------------------------------------- /proxy-code/.eslintrc.js: -------------------------------------------------------------------------------- 1 | // http://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | root: true, 5 | parser: 'babel-eslint', 6 | parserOptions: { 7 | sourceType: 'module' 8 | }, 9 | env: { 10 | browser: false, 11 | node: true, 12 | es6: true 13 | }, 14 | // https://github.com/standard/standard/blob/master/docs/RULES-en.md 15 | extends: 'standard', 16 | // required to lint *.vue files 17 | plugins: [ 18 | 'html' 19 | ], 20 | // add your custom rules here 21 | 'rules': { 22 | 'space-before-blocks': [2, 'always'], 23 | 'space-before-function-paren': [2, 'never'], 24 | 'space-in-parens': [2, 'never'], 25 | // allow paren-less arrow functions 26 | 'arrow-parens': 0, 27 | // allow async-await 28 | 'generator-star-spacing': 0, 29 | // allow debugger during development 30 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 31 | }, 32 | globals: { 33 | App: true, 34 | Page: true, 35 | wx: true, 36 | swan: true, 37 | tt: true, 38 | my: true, 39 | getApp: true, 40 | getPage: true, 41 | requirePlugin: true, 42 | mpvue: true, 43 | mpvuePlatform: true 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /proxy-code/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Editor directories and files 9 | .idea 10 | *.suo 11 | *.ntvs* 12 | *.njsproj 13 | *.sln 14 | -------------------------------------------------------------------------------- /proxy-code/base64.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // 自定义的base64加密 3 | 4 | function Base64(_secret) { 5 | const secret = _secret || 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=' 6 | const chars = secret.split('') 7 | const mapChars = {} 8 | chars.forEach((e, index) => { 9 | mapChars[e] = index 10 | }) 11 | // 编码加密 12 | this.encodeBase64 = function encodeBase64(bufferOrStr, encoding = 'utf-8') { 13 | const buffer = bufferOrStr instanceof Buffer ? bufferOrStr : Buffer.from(bufferOrStr, encoding) 14 | let result = '' 15 | let arr = [], bt = [], char 16 | for (let i = 0; i < buffer.length; i += 3) { 17 | if (i + 3 > buffer.length) { 18 | arr = buffer.slice(i, buffer.length) 19 | break 20 | } 21 | bt = buffer.slice(i, i + 3) 22 | char = chars[bt[0] >> 2] + chars[((bt[0] & 3) << 4) | (bt[1] >> 4)] + chars[((bt[1] & 15) << 2) | (bt[2] >> 6)] + chars[bt[2] & 63] 23 | result += char 24 | } 25 | if (buffer.length % 3 === 1) { 26 | char = chars[arr[0] >> 2] + chars[((arr[0] & 3) << 4)] + chars[64] + chars[64] 27 | result += char 28 | } else if (buffer.length % 3 === 2) { 29 | char = chars[arr[0] >> 2] + chars[((arr[0] & 3) << 4) | (arr[1] >> 4)] + chars[((arr[1] & 15) << 2)] + chars[64] 30 | result += char 31 | } 32 | return result 33 | } 34 | // 编码解密 35 | this.decodeBase64 = function decodeBase64(base64Str) { 36 | let size = base64Str.length / 4 * 3 37 | let j = 0 38 | if (~base64Str.indexOf(chars[64] + '' + chars[64])) { 39 | size -= 2 40 | } else if (~base64Str.indexOf(chars[64])) { 41 | size -= 1 42 | } 43 | let buffer = Buffer.alloc(size) 44 | let enc1, enc2, enc3, enc4, i = 0; 45 | while (i < base64Str.length) { 46 | enc1 = mapChars[base64Str.charAt(i++)] 47 | enc2 = mapChars[base64Str.charAt(i++)] 48 | enc3 = mapChars[base64Str.charAt(i++)] 49 | enc4 = mapChars[base64Str.charAt(i++)] 50 | buffer.writeUInt8(enc1 << 2 | enc2 >> 4, j++) 51 | if (enc3 !== 64) { 52 | buffer.writeUInt8(((enc2 & 15) << 4) | (enc3 >> 2), j++) 53 | } 54 | if (enc4 !== 64) { 55 | buffer.writeUInt8(((enc3 & 3) << 6) | enc4, j++) 56 | } 57 | } 58 | return buffer 59 | } 60 | 61 | } 62 | 63 | Base64.randomSecret = function () { 64 | // 不能使用 = 号,url穿参数不支持 65 | const source = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789,*$' 66 | let chars = source.split('') 67 | const newChars = [] 68 | while (chars.length > 0) { 69 | let index = Math.floor(Math.random() * chars.length); 70 | newChars.push(chars[index]) 71 | chars.splice(index, 1); 72 | } 73 | return newChars.join('') 74 | } 75 | 76 | module.exports = Base64 77 | function test() { 78 | let secret = Base64.randomSecret() 79 | console.log('secret:', secret) 80 | let mybase64 = new Base64(secret) 81 | let test = "test123456" 82 | let encodeStr = mybase64.encodeBase64(test) 83 | let bufferData = mybase64.decodeBase64(encodeStr) 84 | let encodeFromBuffer = mybase64.encodeBase64(bufferData) 85 | console.log(encodeStr, encodeFromBuffer, bufferData.toString()) 86 | } 87 | // test() -------------------------------------------------------------------------------- /proxy-code/client.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const Koa = require('koa') 3 | const http = require('http') 4 | const uuid = require('uuid') 5 | 6 | const { setRequestCache } = require('./setCache') 7 | const { getResponseCache } = require('./getCache') 8 | const { snowflake } = require('./snowflake') 9 | const config = require('./config') 10 | const base64 = require('./base64') 11 | const mybase64 = new base64(config.secret) 12 | const app = new Koa() 13 | // 等待 14 | async function sleep(time) { 15 | return new Promise((resolve) => { 16 | setTimeout(() => { 17 | resolve(); 18 | }, time || 1000); 19 | }); 20 | } 21 | const httpsFlag = '_sssss' 22 | // 判断链接 23 | function matchHttpsUrl(html) { 24 | // 匹配https开始的,结尾是单引号或者双引号的连接 25 | let match = /(https:\/\/)+(www\.)?[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+(:\d+)?[^"\']*/g 26 | let result 27 | while (result = match.exec(html)) { 28 | const newUrl = result[0].replace('https:', 'http:') + httpsFlag 29 | html = html.replace(result[0], newUrl) 30 | // console.log(" ======match url========:", result[0], newUrl) 31 | } 32 | return html 33 | } 34 | 35 | const requestArray = [] // {url, method, headers, body } 36 | let responseArray = [] // { headers, body } 37 | // 把请求进行封装 38 | app.use(async (ctx, next) => { 39 | // 可以设置图片就不请求 40 | const url = ctx.request.url 41 | if (~url.indexOf('.png') || ~url.indexOf('.jpg') || ~url.indexOf('.ico') || ~url.indexOf('.gif')) { 42 | ctx.state = 404 43 | // return 44 | } 45 | const data = [] 46 | ctx.req.on('data', chunk => { 47 | data.push(chunk) 48 | }) 49 | // 不管什么内容,一律转base64,交给代理服务器server.js 处理 50 | ctx.request.body = await new Promise((resolve, reject) => { 51 | ctx.req.on('end', () => { 52 | resolve(mybase64.encodeBase64(Buffer.concat(data))) 53 | }) 54 | }) 55 | // console.log('browser headers', ctx.request.headers) 56 | const { method, headers, body } = ctx.request 57 | let myurl = url 58 | if (url.indexOf(httpsFlag)) { 59 | myurl = url.replace(httpsFlag, '').replace('http:', 'https:') 60 | } 61 | console.log("======= myurl=======", myurl) 62 | const requestData = { method, url: myurl, headers, body } 63 | requestData.createTime = snowflake.shortNextId() 64 | requestData.reqId = uuid.v1() 65 | // 代理服务响应的时候,就需要这个字段值去匹配是哪个请求的响应 66 | ctx.request.reqId = requestData.reqId 67 | requestArray.push(requestData) 68 | await next() 69 | }) 70 | 71 | // 下一步等待响应 72 | app.use(async ctx => { 73 | const reqId = ctx.request.reqId 74 | // 轮询查看是否有数据回来了,轮训N次 75 | for (let i = 0; i < 30; i++) { 76 | // 睡眠等待1s,下次再看看是否已经返回 77 | await sleep(1000) 78 | const index = responseArray.findIndex(res => res.reqId === reqId) 79 | if (~index) { 80 | // 组装headers 81 | for (const field in responseArray[index].headers) { 82 | ctx.append(field, responseArray[index].headers[field]) 83 | } 84 | if (~responseArray[index].headers['content-type'].indexOf('image')) { 85 | ctx.body = Buffer.from(responseArray[index].body, 'base64') 86 | } else if (responseArray[index].headers['accept-ranges'] === 'bytes') { 87 | ctx.body = Buffer.from(responseArray[index].body, 'base64') 88 | } else { 89 | ctx.body = Buffer.from(responseArray[index].body, 'base64').toString() 90 | } 91 | // 把html页面中的https改成http, 这样js,png那些链接才能继续请求 92 | if (~responseArray[index].headers['content-type'].indexOf('text/html')) { 93 | ctx.body = matchHttpsUrl(ctx.body) 94 | // 可以强制全部走https,比如某些域名下全部走https,这里交给大家去实现了 95 | // ctx.body = ctx.body.replace(/https:\/\//g, 'http://') 96 | } 97 | ctx.state = responseArray[index].state 98 | console.log('client get the response:', ctx.state) 99 | responseArray.splice(index, 1) 100 | // 删除已响应的请求 101 | const reqIndex = requestArray.findIndex(req => req.reqId === reqId) 102 | requestArray.splice(reqIndex, 1) 103 | return 104 | } 105 | } 106 | // 等待30次,还没有响应数据,那么就说明超时了,先不管了 107 | const reqIndex = requestArray.findIndex(req => req.reqId === reqId) 108 | requestArray.splice(reqIndex, 1) 109 | console.log(' ###request time out ', ctx.request.url) 110 | }) 111 | 112 | // let reqCache = '{}' 113 | let allreadyRead = true 114 | let lastResponseCache = '{}' 115 | 116 | // 轮询发送请求列表, 每次发送请求后,重置 allreadyRead = false 117 | let lastRequestCache = '[]' 118 | setInterval(async () => { 119 | // 发送请求,发送新请求 120 | const current = JSON.stringify(requestArray) 121 | if (lastRequestCache === current) { 122 | console.log('no request') 123 | return 124 | } 125 | lastRequestCache = current 126 | const res = await setRequestCache(JSON.stringify({ requestArray, allreadyRead })) 127 | console.log('=============== set request ===============', res) 128 | allreadyRead = false 129 | }, 2000) 130 | 131 | // 轮询获取响应 132 | setInterval(async () => { 133 | try { 134 | const responseCache = await getResponseCache() 135 | allreadyRead = true 136 | if (lastResponseCache === responseCache) { 137 | console.log('no response') 138 | return 139 | } 140 | lastResponseCache = responseCache 141 | const res = JSON.parse(responseCache) 142 | responseArray = responseArray.concat(res) 143 | } catch (err) { 144 | console.log(err) 145 | } 146 | }, 2100) 147 | const port = 9000 148 | console.log('proxy-port:', port) 149 | http.createServer(app.callback()).listen(port) 150 | -------------------------------------------------------------------------------- /proxy-code/config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // wx 公众号平台的cookie 3 | exports.cookie = 'appmsglist_action_3202820990=card; pgv_pvid=435555' 4 | 5 | // wx 公众号平台的参数 6 | exports.token = 1681347482 7 | // 填写你公司的代码,如果有公司外网白名单的代理,请设置代理 8 | exports.proxy = 'http://proxy.company.com:8081' 9 | exports.proxy = false 10 | // 使用本地文件测试 11 | exports.useFile = true 12 | // base64 使用加密,可以使用 base64.randomSecret() 重新生成一个 13 | exports.secret = 'QutyBmoRZpVxFzH4cXA3DGEdkKYi*vhlO90a2b,TsjnN5PC$eIU1gLJqfr76SWwM8' 14 | exports.requestAppmsgid = 100000035 15 | exports.responseAppmsgid = 100000036 16 | -------------------------------------------------------------------------------- /proxy-code/getCache.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const request = require('request') 3 | const config = require('./config') 4 | const fs = require('fs') 5 | const path = require('path') 6 | const base64 = require('./base64') 7 | const mybase64 = new base64(config.secret) 8 | const opts = { 9 | url: `https://mp.weixin.qq.com/cgi-bin/appmsg?t=media/appmsg_edit&action=edit&lang=zh_CN&token=${config.token}&type=10&appmsgid=${config.responseAppmsgid}&fromview=list`, 10 | proxy: config.proxy || null, 11 | headers: { 12 | cookie: config.cookie, 13 | referer: 'https://mp.weixin.qq.com/cgi-bin/cgi-bin/appmsgtemplate?action=edit&appmsgid=100000020', 14 | Host: 'mp.weixin.qq.com', 15 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36', 16 | Accept: 'text/html', 17 | Origin: 'https://mp.weixin.qq.com' 18 | } 19 | } 20 | 21 | const { gunzipSync } = require('zlib') 22 | function getCache(appmsgid) { 23 | opts.url = `https://mp.weixin.qq.com/cgi-bin/appmsg?t=media/appmsg_edit&action=edit&lang=zh_CN&token=${config.token}&type=10&appmsgid=${appmsgid}&fromview=list` 24 | return new Promise((resolve, reject) => { 25 | // 转为base64 26 | request(opts, (err, res) => { 27 | if (err) { 28 | reject(err) 29 | return 30 | } 31 | const starStr = '@*@' 32 | const start = res.body.indexOf(starStr) 33 | const end = res.body.indexOf('@_@') 34 | let data = res.body.substring(start + starStr.length, end) 35 | try { 36 | data = gunzipSync(mybase64.decodeBase64(data)).toString() 37 | } catch (err) { 38 | console.log('@@@@', data) 39 | reject(err) 40 | } 41 | resolve(data) 42 | }) 43 | }) 44 | } 45 | // 获取响应内容 46 | let getResponseCache = async function() { 47 | return getCache(config.responseAppmsgid) 48 | } 49 | // 获取请求内容 50 | let getRequestCache = async function() { 51 | return getCache(config.requestAppmsgid) 52 | } 53 | 54 | // 调试模式,使用本地文件进行调试,作为数据交换介质 55 | if (config.useFile) { 56 | getRequestCache = async function() { 57 | return fs.readFileSync(path.resolve(__dirname, 'request.txt')).toString() 58 | } 59 | getResponseCache = function() { 60 | return fs.readFileSync(path.resolve(__dirname, 'response.txt')).toString() 61 | } 62 | } 63 | 64 | module.exports = { getResponseCache, getRequestCache } 65 | -------------------------------------------------------------------------------- /proxy-code/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "proxy", 3 | "version": "1.0.0", 4 | "description": "test", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "koa": "^2.11.0", 13 | "koa-router": "^8.0.8", 14 | "node-uuid": "^1.4.8", 15 | "request": "^2.88.2", 16 | "uuid": "^7.0.3" 17 | }, 18 | "devDependencies": { 19 | "babel-core": "^6.22.1", 20 | "babel-eslint": "^8.2.3", 21 | "babel-loader": "^7.1.1", 22 | "babel-plugin-transform-runtime": "^6.22.0", 23 | "babel-preset-env": "^1.3.2", 24 | "babel-preset-stage-2": "^6.22.0", 25 | "babel-register": "^6.22.0", 26 | "chalk": "^2.4.0", 27 | "connect-history-api-fallback": "^1.3.0", 28 | "copy-webpack-plugin": "^4.5.1", 29 | "css-loader": "^0.28.11", 30 | "cssnano": "^3.10.0", 31 | "eslint": "^4.19.1", 32 | "eslint-config-standard": "^11.0.0", 33 | "eslint-friendly-formatter": "^4.0.1", 34 | "eslint-loader": "^2.0.0", 35 | "eslint-plugin-html": "^4.0.3", 36 | "eslint-plugin-import": "^2.11.0", 37 | "eslint-plugin-node": "^6.0.1", 38 | "eslint-plugin-promise": "^3.4.0", 39 | "eslint-plugin-standard": "^3.0.1", 40 | "eventsource-polyfill": "^0.9.6", 41 | "express": "^4.16.3", 42 | "extract-text-webpack-plugin": "^3.0.2", 43 | "file-loader": "^1.1.11", 44 | "friendly-errors-webpack-plugin": "^1.7.0", 45 | "glob": "^7.1.2", 46 | "html-webpack-plugin": "^3.2.0", 47 | "http-proxy-middleware": "^0.18.0", 48 | "mkdirp": "^0.5.1", 49 | "mptoast": "^1.3.0", 50 | "mpvue-loader": "^2.0.0", 51 | "mpvue-template-compiler": "^2.0.0", 52 | "mpvue-webpack-target": "^1.0.3", 53 | "optimize-css-assets-webpack-plugin": "^3.2.0", 54 | "ora": "^2.0.0", 55 | "portfinder": "^1.0.13", 56 | "postcss-loader": "^2.1.4", 57 | "postcss-mpvue-wxss": "^1.0.0", 58 | "prettier": "~1.12.1", 59 | "px2rpx-loader": "^0.1.10", 60 | "relative": "^3.0.2", 61 | "rimraf": "^2.6.0", 62 | "semver": "^5.3.0", 63 | "shelljs": "^0.8.1", 64 | "uglifyjs-webpack-plugin": "^1.2.5", 65 | "url-loader": "^1.0.1", 66 | "vue-style-loader": "^4.1.0", 67 | "webpack": "^3.11.0", 68 | "webpack-bundle-analyzer": "^3.3.2", 69 | "webpack-dev-middleware-hard-disk": "^1.12.0", 70 | "webpack-merge": "^4.1.0", 71 | "webpack-mpvue-asset-plugin": "^2.0.0", 72 | "webpack-mpvue-vendor-plugin": "^2.0.0" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /proxy-code/request.txt: -------------------------------------------------------------------------------- 1 | {"requestArray":[],"allreadyRead":false} -------------------------------------------------------------------------------- /proxy-code/response.txt: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /proxy-code/server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const request = require('request') 3 | const { getRequestCache } = require('./getCache') 4 | const { setResponseCache } = require('./setCache') 5 | const { snowflake } = require('./snowflake') 6 | const config = require('./config') 7 | const base64 = require('./base64') 8 | const mybase64 = new base64(config.secret) 9 | // 睡眠,交出执行权 10 | async function sleep(time) { 11 | return new Promise((resolve) => { 12 | setTimeout(() => { 13 | resolve(); 14 | }, time || 1000); 15 | }); 16 | } 17 | 18 | // ############################## 外网pc端服务 ################################## 19 | async function executeRequest(requestObj) { 20 | const opts = Object.assign({}, requestObj) 21 | opts.proxy = config.proxy || null 22 | // 使用解压 23 | opts.gzip = true 24 | return new Promise((resolve, reject) => { 25 | request(opts, (err, res) => { 26 | if (err) { 27 | console.log('err', err) 28 | reject(err) 29 | return 30 | } 31 | res.reqId = requestObj.reqId 32 | res.createTime = requestObj.createTime 33 | resolve(res) 34 | }).encoding = 'base64' 35 | }) 36 | } 37 | 38 | let writeable = false 39 | let resultResponse = [] 40 | let lastTime = snowflake.shortNextId() 41 | // 定时轮训,去获取client存放的requestArray 42 | setInterval(async () => { 43 | try { 44 | const reqCache = await getRequestCache() 45 | const { requestArray = [], allreadyRead = false } = JSON.parse(reqCache) 46 | // 设置是否可写,当客户端client(浏览器)读取过响应后后,server才能重新写入响应到repsonseCache中 47 | writeable = allreadyRead 48 | console.log('you request allreadyRead:', allreadyRead, requestArray.length) 49 | if (requestArray.length === 0) { 50 | return 51 | } 52 | // 请求url 把响应放到 队列 53 | for (let i = 0; i < requestArray.length; i++) { 54 | const request = requestArray[i] 55 | // 根据时间线来判断是否已经处理过这个请求了,因为浏览器的代理,不会一直更新这个请求缓存区的。它要等对应的响应到了,才会删除requestArray的数据。 56 | if (request.createTime <= lastTime || !request.url) { 57 | continue 58 | } 59 | lastTime = request.createTime 60 | // 如果是上传文件或者图片,那么就改成字节,这里没有实现,可以判断Content-Type="multipart/form-data" 61 | // 进行设置 request multipart参数 https://github.com/request/request 62 | request.body = mybase64.decodeBase64(request.body).toString() 63 | console.log('get you requestUrl', request.url) 64 | try { 65 | executeRequest(request).then(res => { 66 | const responseObj = {} 67 | // console.log('resbody headers', res.headers) 68 | responseObj.state = res.statusCode 69 | responseObj.body = res.body 70 | // 这里client已经解压过了,就不要设置gzip了 71 | delete res.headers['content-encoding'] 72 | delete res.headers['transfer-encoding'] 73 | responseObj.headers = res.headers 74 | // 异步,在req 赋值了 75 | responseObj.createTime = res.createTime 76 | responseObj.reqId = res.reqId 77 | console.log('get you response reqId', res.reqId) 78 | resultResponse.push(responseObj) 79 | }) 80 | } catch (err) { 81 | console.log(err) 82 | } 83 | if (i % 10 === 0) { 84 | await sleep(1000) 85 | } 86 | } 87 | } catch (err) { 88 | console.log('err: getRequestCache', err) 89 | } 90 | }, 2000) 91 | 92 | // 轮询 写入响应 93 | setInterval(async () => { 94 | if (!writeable) { 95 | console.info('it can not write now', writeable) 96 | return 97 | } 98 | writeable = false 99 | if (resultResponse.length === 0) { 100 | return 101 | } 102 | // 设置响应后,后置为空(不用等待响应是否成功写入,不然数据不同步,也可以在失败后恢复数组) 103 | const copyRespArray = resultResponse.concat([]) 104 | resultResponse = [] 105 | const res = await setResponseCache(JSON.stringify(copyRespArray)) 106 | if (res && !~res.toString().indexOf('"ret":0')) { 107 | console.log('===============err===============', res) 108 | } 109 | }, 2200) 110 | -------------------------------------------------------------------------------- /proxy-code/setCache.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const fs = require('fs') 3 | const path = require('path') 4 | const request = require('request') 5 | const config = require('./config') 6 | const base64 = require('./base64') 7 | const mybase64 = new base64(config.secret) 8 | const opts = { 9 | url: `https://mp.weixin.qq.com/cgi-bin/operate_appmsg?t=ajax-response&sub=update&type=10&token=${config.token}&lang=zh_CN`, 10 | proxy: config.proxy || null, 11 | headers: { 12 | cookie: config.cookie, 13 | // referer: 'https://mp.weixin.qq.com/cgi-bin/cgi-bin/appmsgtemplate?action=edit&appmsgid=100000020', 14 | Host: 'mp.weixin.qq.com', 15 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36', 16 | Accept: 'application/json', 17 | Origin: 'https://mp.weixin.qq.com', 18 | // 'Accept-Encoding': 'gzip, deflate', // 不需要压缩 19 | 'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2', 20 | 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 21 | 'X-Requested-With': 'XMLHttpRequest', 22 | 'Referer': 'https://mp.weixin.qq.com/cgi-bin/appmsg?t=media/appmsg_edit&action=edit&type=10&appmsgid=100000028&token=1681347483&lang=zh_CN' 23 | } 24 | } 25 | // 请求参数,每次都需要 26 | function createBody(content, appmsgid, dataSeq) { 27 | content = '@*@' + content + '@_@' 28 | const body = `token=${config.token}&lang=zh_CN&f=json&ajax=1&random=0.8267605760212564&AppMsgId=${appmsgid}&count=1&data_seq=${dataSeq}&operate_from=Firefox&isnew=0&ad_video_transition0=&can_reward0=0&reward_reply_id0=&related_video0=&is_video_recommend0=-1&title0=cache_${appmsgid}&author0=22doctor&writerid0=0&fileid0=&digest0=response-content&auto_gen_digest0=1&content0=%3Cp%3E${content}%3Cbr%3E%3C%2Fp%3E&sourceurl0=&need_open_comment0=0&only_fans_can_comment0=0&only_fans_days_can_comment0=0&cdn_url0=&cdn_235_1_url0=&cdn_1_1_url0=&cdn_url_back0=&crop_list0=&music_id0=&video_id0=&voteid0=&voteismlt0=&supervoteid0=&cardid0=&cardquantity0=&cardlimit0=&vid_type0=&show_cover_pic0=0&shortvideofileid0=©right_type0=0&releasefirst0=&platform0=&reprint_permit_type0=&allow_reprint0=&allow_reprint_modify0=&original_article_type0=&ori_white_list0=&video_ori_status0=&hit_nickname0=&free_content0=&fee0=0&ad_id0=&guide_words0=&is_share_copyright0=0&share_copyright_url0=&source_article_type0=&reprint_recommend_title0=&reprint_recommend_content0=&share_page_type0=0&share_imageinfo0=%7B%22list%22%3A%5B%5D%7D&share_video_id0=&dot0=%7B%7D&share_voice_id0=&insert_ad_mode0=&categories_list0=%5B%5D&compose_info0=%7B%22list%22%3A%5B%7B%22blockIdx%22%3A1%2C%22content%22%3A%22%3Cp+style%3D%5C%22overflow-y%3A+hidden%3B%5C%22%3Etest-content%3Cmpchecktext+id%3D%5C%221589426622468_0.7019304859993029%5C%22+contenteditable%3D%5C%22false%5C%22%3E%3C%2Fmpchecktext%3E%3Cbr%3E%3C%2Fp%3E%22%2C%22width%22%3A578%2C%22height%22%3A27%2C%22topMargin%22%3A0%2C%22marginBottom%22%3A0%2C%22contentEditable%22%3Anull%2C%22blockType%22%3A9%2C%22background%22%3A%22rgba(0%2C+0%2C+0%2C+0)%22%2C%22text%22%3A%22test-content%22%2C%22textColor%22%3A%22rgb(51%2C+51%2C+51)%22%2C%22textFontSize%22%3A%2217px%22%2C%22textBackGround%22%3A%22rgba(0%2C+0%2C+0%2C+0)%22%7D%5D%7D&is_pay_subscribe0=0&pay_fee0=&pay_preview_percent0=&pay_desc0=&appmsg_album0=&remind_flag=` 29 | // const body = `token=${config.token}&lang=zh_CN&f=json&ajax=1&random=0.4266195415809638&AppMsgId=${appmsgid}&count=1&data_seq=${dataSeq}&operate_from=Firefox&isnew=0&ad_video_transition0=&can_reward0=0&reward_reply_id0=&related_video0=&is_video_recommend0=0&title0=cache_${appmsgid}&author0=doctor&writerid0=0&fileid0=&digest0=***&auto_gen_digest0=0&content0=%3Cp%3E%40%40%40%3Cbr%3E%3C%2Fp%3E&sourceurl0=&need_open_comment0=0&only_fans_can_comment0=0&only_fans_days_can_comment0=0&cdn_url0=&cdn_235_1_url0=&cdn_1_1_url0=&cdn_url_back0=&crop_list0=&music_id0=&video_id0=&voteid0=&voteismlt0=&supervoteid0=&cardid0=&cardquantity0=&cardlimit0=&vid_type0=&show_cover_pic0=0&shortvideofileid0=©right_type0=0&releasefirst0=&platform0=&reprint_permit_type0=&allow_reprint0=&allow_reprint_modify0=&original_article_type0=&ori_white_list0=&video_ori_status0=&hit_nickname0=&free_content0=&fee0=0&ad_id0=&guide_words0=&is_share_copyright0=0&share_copyright_url0=&source_article_type0=&reprint_recommend_title0=&reprint_recommend_content0=&share_page_type0=0&share_imageinfo0=%7B%22list%22%3A%5B%5D%7D&share_video_id0=&dot0=%7B%7D&share_voice_id0=&insert_ad_mode0=&categories_list0=%5B%5D&compose_info0=%7B%22list%22%3A%5B%7B%22blockIdx%22%3A1%2C%22content%22%3A%22%3Cp+style%3D%5C%22overflow-y%3A+hidden%3B%5C%22%3E%40%40%40%3Cbr%3E%3C%2Fp%3E%22%2C%22width%22%3A578%2C%22height%22%3A27%2C%22topMargin%22%3A0%2C%22marginBottom%22%3A0%2C%22contentEditable%22%3Anull%2C%22blockType%22%3A9%2C%22background%22%3A%22rgba(0%2C+0%2C+0%2C+0)%22%2C%22text%22%3A%22%40%40%40%22%2C%22textColor%22%3A%22rgb(51%2C+51%2C+51)%22%2C%22textFontSize%22%3A%2217px%22%2C%22textBackGround%22%3A%22rgba(0%2C+0%2C+0%2C+0)%22%7D%5D%7D&is_pay_subscribe0=0&pay_fee0=&pay_preview_percent0=&pay_desc0=&appmsg_album0=&remind_flag=` 30 | return body 31 | } 32 | 33 | // 这里需要自己重新实现的方法 34 | const { gzipSync } = require('zlib') 35 | let setCache = function(requestCache, appmsgid, dataSeq = '1339542083857891328') { 36 | return new Promise((resolve, reject) => { 37 | // // 发送前先压缩 转为base64 38 | let data = mybase64.encodeBase64(gzipSync(requestCache)) 39 | opts.body = createBody(data, appmsgid, dataSeq) 40 | request.post(opts, (err, res) => { 41 | if (err) { 42 | reject(err) 43 | return 44 | } 45 | resolve(res.body) 46 | }) 47 | }) 48 | } 49 | 50 | const dataSeqData = {} 51 | // 设置响应信息 52 | let setResponseCache = async function(data) { 53 | const resData = await setCache(data, config.responseAppmsgid, dataSeqData[config.responseAppmsgid]) 54 | const res = JSON.parse(resData) 55 | dataSeqData[config.responseAppmsgid] = res.data_seq 56 | return res 57 | } 58 | 59 | // 设置请求信息 60 | let setRequestCache = async function(data) { 61 | const resData = await setCache(data, config.requestAppmsgid, dataSeqData[config.requestAppmsgid]) 62 | const res = JSON.parse(resData) 63 | dataSeqData[config.requestAppmsgid] = res.data_seq 64 | return res 65 | } 66 | 67 | // 使用本地文件测试 68 | if (config.useFile) { 69 | setRequestCache = function(data) { 70 | fs.writeFileSync(path.resolve(__dirname, 'request.txt'), data) 71 | } 72 | setResponseCache = function(data) { 73 | fs.writeFileSync(path.resolve(__dirname, 'response.txt'), data) 74 | } 75 | } else { 76 | // 这里避免第一次请求报错,简单处理 77 | setRequestCache('{"data": "this is RequestCache"}') 78 | setResponseCache('{"data": "this is ResponseCache"}') 79 | } 80 | 81 | module.exports = { setResponseCache, setRequestCache } 82 | -------------------------------------------------------------------------------- /proxy-code/snowflake.js: -------------------------------------------------------------------------------- 1 | var Snowflake = (function() { 2 | function Snowflake(_workerId, _dataCenterId, _sequence) { 3 | this.twepoch = 1288834974657n; 4 | //this.twepoch = 0n; 5 | this.workerIdBits = 5n; 6 | this.dataCenterIdBits = 5n; 7 | this.maxWrokerId = -1n ^ (-1n << this.workerIdBits); // 值为:31 8 | this.maxDataCenterId = -1n ^ (-1n << this.dataCenterIdBits); // 值为:31 9 | this.sequenceBits = 12n; 10 | this.workerIdShift = this.sequenceBits; // 值为:12 11 | this.dataCenterIdShift = this.sequenceBits + this.workerIdBits; // 值为:17 12 | this.timestampLeftShift = this.sequenceBits + this.workerIdBits + this.dataCenterIdBits; // 值为:22 13 | this.sequenceMask = -1n ^ (-1n << this.sequenceBits); // 值为:4095 14 | this.lastTimestamp = -1n; 15 | //设置默认值,从环境变量取 16 | this.workerId = 1n; 17 | this.dataCenterId = 1n; 18 | this.sequence = 0n; 19 | if (this.workerId > this.maxWrokerId || this.workerId < 0) { 20 | throw new Error('_workerId must max than 0 and small than maxWrokerId-[' + this.maxWrokerId + ']'); 21 | } 22 | if (this.dataCenterId > this.maxDataCenterId || this.dataCenterId < 0) { 23 | throw new Error('_dataCenterId must max than 0 and small than maxDataCenterId-[' + this.maxDataCenterId + ']'); 24 | } 25 | 26 | this.workerId = BigInt(_workerId); 27 | this.dataCenterId = BigInt(_dataCenterId); 28 | this.sequence = BigInt(_sequence); 29 | } 30 | Snowflake.prototype.tilNextMillis = function(lastTimestamp) { 31 | var timestamp = this.timeGen(); 32 | while (timestamp <= lastTimestamp) { 33 | timestamp = this.timeGen(); 34 | } 35 | return BigInt(timestamp); 36 | }; 37 | Snowflake.prototype.timeGen = function() { 38 | return BigInt(Date.now()); 39 | }; 40 | Snowflake.prototype.nextId = function() { 41 | var timestamp = this.timeGen(); 42 | if (timestamp < this.lastTimestamp) { 43 | throw new Error('Clock moved backwards. Refusing to generate id for ' + 44 | (this.lastTimestamp - timestamp)); 45 | } 46 | if (this.lastTimestamp === timestamp) { 47 | this.sequence = (this.sequence + 1n) & this.sequenceMask; 48 | if (this.sequence === 0n) { 49 | timestamp = this.tilNextMillis(this.lastTimestamp); 50 | } 51 | } else { 52 | this.sequence = 0n; 53 | } 54 | this.lastTimestamp = timestamp; 55 | return ((timestamp - this.twepoch) << this.timestampLeftShift) | 56 | (this.dataCenterId << this.dataCenterIdShift) | 57 | (this.workerId << this.workerIdShift) | 58 | this.sequence; 59 | }; 60 | Snowflake.prototype.shortNextId = function() { 61 | var timestamp = this.timeGen(); 62 | if (timestamp < this.lastTimestamp) { 63 | throw new Error('Clock moved backwards. Refusing to generate id for ' + 64 | (this.lastTimestamp - timestamp)); 65 | } 66 | if (this.lastTimestamp === timestamp) { 67 | this.sequence = (this.sequence + 1n) & this.sequenceMask; 68 | if (this.sequence === 0n) { 69 | timestamp = this.tilNextMillis(this.lastTimestamp); 70 | } 71 | } else { 72 | this.sequence = 0n; 73 | } 74 | this.lastTimestamp = timestamp; 75 | const id = ((timestamp - this.twepoch) << this.workerIdShift) | 76 | this.sequence; 77 | return parseInt(id) 78 | }; 79 | return Snowflake; 80 | }()); 81 | 82 | let snowflake = new Snowflake(1,1,0) 83 | module.exports = { snowflake } 84 | 85 | -------------------------------------------------------------------------------- /proxy-code/sock5Client.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const net = require('net') 3 | const uuid = require('uuid') 4 | const { setRequestCache } = require('./setCache') 5 | const { getResponseCache } = require('./getCache') 6 | const { snowflake } = require('./snowflake') 7 | class RequestInfo { 8 | constructor({ socketId, remoteAddr, remotePort, body, createTime }) { 9 | this.socketId = socketId 10 | this.remoteAddr = remoteAddr 11 | this.remotePort = remotePort 12 | this.body = body 13 | this.createTime = createTime 14 | } 15 | } 16 | const requestArray = [] // {url, method, headers, body } 17 | 18 | const sockMap = {} 19 | let server = net.createServer(socket => { 20 | const socketId = uuid.v1() 21 | socket.once('data', data => { 22 | if (!data || data[0] !== 0x05) return socket.destroy() 23 | socket.write(Buffer.from([5, 0]), err => { 24 | if (err) socket.destroy() 25 | let addrtype = 0 26 | let remoteAddr = null 27 | let remotePort = null 28 | let addrLen = 0 29 | socket.once('data', (data) => { 30 | // 只支持 CONNECT 31 | if (data.length < 7 || data[1] !== 0x01) { 32 | delete sockMap[socketId] 33 | return socket.destroy() 34 | } 35 | try { 36 | addrtype = data[3]// ADDRESS_TYPE 目标服务器地址类型 37 | if (addrtype === 3) { // 0x03 域名地址(没有打错,就是没有0x02), 38 | addrLen = data[4] // 域名地址的第1个字节为域名长度,剩下字节为域名名称字节数组 39 | } else if (addrtype !== 1 && addrtype !== 4) { 40 | delete sockMap[socketId] 41 | return socket.destroy() 42 | } 43 | remotePort = data.readUInt16BE(data.length - 2)// 最后两位为端口值 44 | if (addrtype === 1) { // 0x01 IP V4地址 45 | remoteAddr = data.slice(4, 8).join('.') 46 | } else if (addrtype === 4) { // 0x04 IP V6地址 47 | return socket.write(Buffer.from([0x05, 0x08, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) // 不支持IP V6 48 | } else { // 0x03 域名地址(没有打错,就是没有0x02),域名地址的第1个字节为域名长度,剩下字节为域名名称字节数组 49 | remoteAddr = data.slice(5, 5 + addrLen).toString('binary') 50 | } 51 | console.log(`ready to connecting target : ${remoteAddr}:${remotePort}`) 52 | // 告诉浏览器,可以进行传输数据了 53 | socket.write(Buffer.from([0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), (err) => { 54 | if (err) { 55 | console.error(`socket error:${err.message}`) 56 | delete sockMap[socketId] 57 | return socket.destroy() 58 | } 59 | // 监听把浏览器请求的信息,封装请求对象,然后定时写入到缓冲区 60 | sockMap[socketId] = socket 61 | socket.on('data', data => { 62 | console.log('socket data', data.length) 63 | const body = data.toString('base64') 64 | const requestData = new RequestInfo({ reqId: uuid.v1(), socketId, remoteAddr, remotePort, body, createTime: snowflake.shortNextId() }) 65 | requestArray.push(requestData) 66 | }) 67 | }) 68 | } catch (e) { 69 | delete sockMap[socketId] 70 | console.error(e) 71 | } 72 | }) 73 | }) 74 | }) 75 | 76 | socket.on('error', err => { console.error(`error:${err.message}`) }) 77 | }) 78 | const port = 11100 79 | console.log('listen sock5 port:', port) 80 | server.listen(port) 81 | 82 | // 轮询发送请求列表, 每次发送请求后,重置 allreadyRead = false 83 | let allreadyRead = true 84 | let lastRequestCache = '[]' 85 | setInterval(async () => { 86 | // 删除那些过期消息 87 | const now = new Date().getTime() 88 | for (let i = 0; i < requestArray.length; i++) { 89 | if (requestArray[i].createTime + 1000 * 10 < now) { 90 | requestArray.splice(i, 1) 91 | i-- 92 | } 93 | } 94 | console.log('requestArray length ', requestArray.length) 95 | // 发送请求,发送新请求 96 | const current = JSON.stringify(requestArray) 97 | if (lastRequestCache === current) { 98 | console.log('no request') 99 | return 100 | } 101 | lastRequestCache = current 102 | const res = await setRequestCache(JSON.stringify({ requestArray, allreadyRead })) 103 | console.log('=============== set request ===============', res) 104 | allreadyRead = false 105 | }, 1000) 106 | 107 | let lastResponseCache = '{}' 108 | // 轮询获取响应 109 | setInterval(async () => { 110 | try { 111 | const responseCache = await getResponseCache() 112 | allreadyRead = true 113 | if (lastResponseCache === responseCache) { 114 | console.log('no response') 115 | return 116 | } 117 | lastResponseCache = responseCache 118 | const respList = JSON.parse(responseCache) 119 | respList.forEach(respInfo => { 120 | const reqIndex = requestArray.findIndex(req => req.reqId < respInfo.reqId) 121 | if (~reqIndex) { 122 | requestArray.splice(reqIndex, 1) 123 | } 124 | if (sockMap[respInfo.socketId]) { 125 | const data = Buffer.from(respInfo.body, 'base64') 126 | sockMap[respInfo.socketId].write(data) 127 | } 128 | }) 129 | console.log('requestArray length ', requestArray.length) 130 | // responseArray = responseArray.concat(res) 131 | } catch (err) { 132 | console.log(err) 133 | } 134 | }, 1100) 135 | -------------------------------------------------------------------------------- /proxy-code/sock5Server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const net = require('net') 3 | const { getRequestCache } = require('./getCache') 4 | const { setResponseCache } = require('./setCache') 5 | const { snowflake } = require('./snowflake') 6 | 7 | class RequestInfo { 8 | constructor({ socketId, remoteAddr, remotePort, body, createTime }) { 9 | this.socketId = socketId 10 | this.remoteAddr = remoteAddr 11 | this.remotePort = remotePort 12 | this.body = body 13 | this.createTime = createTime 14 | } 15 | } 16 | 17 | const sockMap = {} 18 | function remoteConnect({ remoteAddr, remotePort, body, socketId, createTime }) { 19 | console.log('socketId ======socketId:', socketId) 20 | if (sockMap[socketId] != null) { 21 | console.log('remoteConnect body', socketId) 22 | // 转为二进制 23 | const data = Buffer.from(body, 'base64') 24 | sockMap[socketId].write(data) 25 | return 26 | } 27 | // 新的socketId,那么服务也是要创建新的连接 28 | let remote = net.connect(remotePort, remoteAddr, () => { 29 | console.log(` remoteConnect connecting : ${remoteAddr}:${remotePort}`) 30 | const data = Buffer.from(body, 'base64') 31 | sockMap[socketId].write(data) 32 | }) 33 | remote.on('data', data => { 34 | console.log('remotedaaaaa', data.length) 35 | // 把response信息写入到列表,会被定时写入到缓冲区 36 | const body = data.toString('base64') 37 | const responseObj = { createTime, socketId, body } 38 | resultResponse.push(responseObj) 39 | }) 40 | remote.on('error', (err) => { 41 | console.error(`连接到远程服务器 ${remoteAddr}:${remoteAddr} 失败,失败信息:${err.message}`) 42 | remote.destroy() 43 | // 删除引用 44 | delete sockMap[socketId] 45 | }) 46 | // 保持引用 47 | sockMap[socketId] = remote 48 | if (sockMap[socketId]) { 49 | console.log('socketId ======socketId', socketId) 50 | } 51 | } 52 | 53 | // 这里从请求缓冲区获取 请求的报文 54 | let writeable = false 55 | let resultResponse = [] 56 | let lastTime = snowflake.shortNextId() 57 | setInterval(async () => { 58 | try { 59 | const reqCache = await getRequestCache() 60 | const { requestArray = [], allreadyRead = false } = JSON.parse(reqCache) 61 | // 判断是否可写,当客户端读取过响应后后,才能重新写入到repsonseCache中 62 | writeable = allreadyRead 63 | console.log('you request allreadyRead:', allreadyRead, requestArray.length) 64 | if (requestArray.length === 0) { 65 | return 66 | } 67 | // 请求url 把响应放到 队列 68 | for (let i = 0; i < requestArray.length; i++) { 69 | const request = new RequestInfo(requestArray[i]) 70 | // 根据时间线是否过期的请求,createTime是增序的 71 | if (request.createTime <= lastTime || !request.socketId) { 72 | continue 73 | } 74 | lastTime = request.createTime 75 | remoteConnect(request) 76 | } 77 | } catch (err) { 78 | console.log('err: getRequestCache', err) 79 | } 80 | }, 1000) 81 | 82 | // 轮询 写入响应 83 | setInterval(async () => { 84 | if (!writeable) { 85 | console.info('not writeable') 86 | return 87 | } 88 | writeable = false 89 | if (resultResponse.length === 0) { 90 | return 91 | } 92 | // 返回新数组副本 93 | const copyResponseArr = resultResponse.concat([]) 94 | // 设置响应后,后置为空(不用等待响应是否成功写入,不然数据不同步,也可以在失败后恢复数组) 95 | resultResponse = [] 96 | const res = await setResponseCache(JSON.stringify(copyResponseArr)) 97 | if (res && !~res.toString().indexOf('"ret":0')) { 98 | console.log('===============err===============', res) 99 | } 100 | }, 1200) 101 | -------------------------------------------------------------------------------- /proxy-code/sock5ZClientProxy.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const net = require('net') 3 | 4 | function remoteClient(remoteAddr, remotePort, socket) { 5 | let remote = net.connect(remotePort, remoteAddr, () => { 6 | console.log(` remoteClient connecting : ${remoteAddr}:${remotePort}`) 7 | socket.write(Buffer.from([0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), (err) => { 8 | if (err) { 9 | console.error(`error:${err.message}`) 10 | return socket.destroy() 11 | } 12 | // remote.pipe(socket) 13 | // socket.pipe(remote) 14 | }) 15 | }) 16 | socket.on('data', data => { 17 | console.log('socketsdaaaaa', data.length) 18 | remote.write(data) 19 | }) 20 | remote.on('data', data => { 21 | console.log('remotedaaaaa', data.length) 22 | socket.write(data) 23 | }) 24 | remote.on('error', (err) => { 25 | console.error(`连接到远程服务器 ${remoteAddr}:${remoteAddr} 失败,失败信息:${err.message}`) 26 | remote.destroy() 27 | socket.destroy() 28 | }) 29 | } 30 | let server = net.createServer(socket => { 31 | socket.once('data', data => { 32 | if (!data || data[0] !== 0x05) return socket.destroy() 33 | socket.write(Buffer.from([5, 0]), err => { 34 | if (err) socket.destroy() 35 | let addrtype = 0 36 | let remoteAddr = null 37 | let remotePort = null 38 | let addrLen = 0 39 | socket.once('data', (data) => { 40 | // 只支持 CONNECT 41 | if (data.length < 7 || data[1] !== 0x01) { 42 | return socket.destroy() 43 | } 44 | try { 45 | addrtype = data[3]// ADDRESS_TYPE 目标服务器地址类型 46 | if (addrtype === 3) { // 0x03 域名地址(没有打错,就是没有0x02), 47 | addrLen = data[4] // 域名地址的第1个字节为域名长度,剩下字节为域名名称字节数组 48 | } else if (addrtype !== 1 && addrtype !== 4) { 49 | return socket.destroy() 50 | } 51 | remotePort = data.readUInt16BE(data.length - 2)// 最后两位为端口值 52 | if (addrtype === 1) { // 0x01 IP V4地址 53 | remoteAddr = data.slice(4, 8).join('.') 54 | } else if (addrtype === 4) { // 0x04 IP V6地址 55 | return socket.write(Buffer.from([0x05, 0x08, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) // 不支持IP V6 56 | } else { // 0x03 域名地址(没有打错,就是没有0x02),域名地址的第1个字节为域名长度,剩下字节为域名名称字节数组 57 | remoteAddr = data.slice(5, 5 + addrLen).toString('binary') 58 | } 59 | console.log(`target connecting : ${remoteAddr}:${remotePort}`) 60 | remoteClient(remoteAddr, remotePort, socket) 61 | } catch (e) { 62 | console.error(e) 63 | } 64 | }) 65 | }) 66 | }) 67 | 68 | socket.on('error', err => { console.error(`error:${err.message}`) }) 69 | }) 70 | const port = 11100 71 | console.log('port:', port) 72 | server.listen(11100) 73 | -------------------------------------------------------------------------------- /proxy-code/test/testCache.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const { setRequestCache, setResponseCache } = require('../setCache') 3 | const { getResponseCache, getRequestCache } = require('../getCache') 4 | setTimeout(() => { 5 | // 延迟执行 6 | setResponseCache('{"data": "this is ResponseCache again"}').then(data => { 7 | console.log('setResponseCache success? ', data) 8 | }) 9 | setRequestCache('{"data": "this is RequestCache again"}').then(data => { 10 | console.log('RequestCache success? ', data) 11 | }) 12 | }, 2500) 13 | 14 | setTimeout(() => { 15 | // 延迟执行 16 | getResponseCache().then(data => { 17 | console.log('getResponseCache:', data) 18 | }) 19 | 20 | getRequestCache().then(data => { 21 | console.log('getRequestCache:', data) 22 | }) 23 | }, 4000) 24 | --------------------------------------------------------------------------------