Nodejs实现内网穿透服务

2022-04-15 0 1,199
目录
  • 1. 局域网内代理
  • 2. 内网穿透
    • 什么是内网穿透?
    • bridge
    • proxyServe
  • 总结
    • proxyServe源码

      也许你很难从网上找到一篇从代码层面讲解内网穿透的文章,我曾搜过,未果,遂成此文。

      1. 局域网内代理

      我们先来回顾上篇,如何实现一个局域网内的服务代理?因为这个非常简单,所以,直接上代码。

      const net = require('net')
      
      const proxy = net.createServer(socket => {
        const localServe = new net.Socket()
        localServe.connect(5502, '192.168.31.130') // 局域网内的服务端口及ip。
      
        socket.pipe(localServe).pipe(socket)
      })
      
      proxy.listen(80)
      
      

      这就是一个非常简单的服务端代理,代码简单清晰明了,如果有疑问的话,估计就是管道(pipe)这里,简单说下。socket是一个全双工流,也就是既可读又可写的数据流。代码中,当socket接收到客户端数据的时候,它会把数据写入localSever,当localSever有数据的时候,它会把数据写入socket,socket再把数据发送给客户端。

      2. 内网穿透

      局域网代理简单,内网穿透就没这么简单了,但是,它却是核心的代码,需要在其上做相当的逻辑处理。具体实现之前,我们先梳理一下内网穿透。

      什么是内网穿透?

      简单来说,就是公网客户端,可以访问局域网内的服务。比如,本地启动的服务。公网客户端怎么会知道本地启的serve呢?这里必然要借助公网服务端。那么公网服务端又怎么知道本地服务呢?这就需要本地和服务端建立socket链接了。

      四个角色

      通过上面的描述,我们引出四个角色。

      1. 公网客户端,我们取名叫client。
      2. 公网服务端,因为有代理的作用,我们取名叫proxyServe。
      3. 本地服务,取名localServe。
      4. 本地与服务端的socket长连接,它是proxyServe与localServe之前的桥梁,负责数据的中转,我们取名叫bridge。

      其中,client和localServe不需要我们关心,因为client可以是浏览器或者其它,localServe就是一个普通的本地服务。我们只需要关心proxyServe和bridge就可以了。我们这里介绍的依然是最简单的实现方式,提供一种思路与思考,那我们先从最简单的开始。

      bridge

      我们从四个角色一节知道, bridge是一个与proxyServe之间socket连接,且是数据的中转,上代码捋捋思路。

      const net = require('net')
      
      const proxyServe = '10.253.107.245'
      
      const bridge = new net.Socket()
      bridge.connect(80, proxyServe, _ => {
        bridge.write('GET /regester?key=sq HTTP/1.1\r\n\r\n')
      })
      
      bridge.on('data', data => {
        const localServer = new net.Socket()
        localServer.connect(8088, 'localhost', _ => {
          localServer.write(data)
          localServer.on('data', res => bridge.write(res))
        })
      })
      
      

      代码清晰可读,甚至朗朗上口。引入net库,声明公网地址,创建bridge,使bridge连接proxyServe,成功之后,向proxyServe注册本地服务,接着,bridge监听数据,有请求到达时,创建与本地服务的连接,成功之后,把请求数据发送给localServe,同时监听响应数据,把响应流写入到bridge。

      其余没什么好解释的了,毕竟这只是示例代码。不过示例代码中有段/regester?key=sq,这个key可是有大作用的,在这里key=sq。那么角色client通过代理服务访问本地服务的是,需要在路径上加上这个key,proxyServe才能对应的上bridge,从而对应上localServe。

      例如:lcoalServe是:http://localhost:8088 ,rpoxyServe是example.com ,注册的key是sq。那么要想通过prxoyServe访问到localServe,需要如下写法:example.com/sq 。为什么要这样写?当然只是一个定义而已,你读懂这篇文章的代码之后,可以修改这样的约定。

      那么,且看以下关键代码:

      proxyServe

      这里的proxyServe虽然是一个简化后的示例代码,讲起来依然有些复杂,要想彻底弄懂,并结合自己的业务做成可用代码,是要下一番功夫的。这里我把代码拆分成一块一块,试着把它讲明白,我们给代码块取个名字,方便讲解。
      代码块一:createServe

      该块的主要功能是创建代理服务,与client和bridge建立socket链接,socket监听数据请求,在回调函数里做逻辑处理,具体代码如下:

      const net = require('net')
      
      const bridges = {} // 当有bridge建立socket连接时,缓存在这里
      const clients = {} // 当有client建立socket连接时,缓存在这里,具体数据结构看源代码
      
      net.createServer(socket => {
        socket.on('data', data => {
          const request = data.toString()
          const url = request.match(/.+ (?<url>.+) /)?.groups?.url
          
          if (!url) return
      
          if (isBridge(url)) {
            regesterBridge(socket, url)
            return
          }
      
          const { bridge, key } = findBridge(request, url)
          if (!bridge) return
      
          cacheClientRequest(bridge, key, socket, request, url)
      
          sendRequestToBridgeByKey(key)
        })
      }).listen(80)
      
      

      看一下数据监听里的代码逻辑:

      1. 把请求数据转换成字符串。
      2. 从请求里查找URL,找不到URL直接结束本次请求。
      3. 通过URL判断是不是bridge,如果是,注册这个bridge,否者,认为是一个client请求。
      4. 查看client请求有没有已经注册过的bridge — 记住,这是一个代理服务,没有已经注册的bridge,就认为请求无效。
      5. 缓存这次请求。
      6. 接着再把请求发送给bridge。

      结合代码及逻辑梳理,应该能看得懂,但是,对5或许有疑问,接下来一一梳理。

      代码块二:isBridge

      判断是不是一个bridge的注册请求,这里写的很简单,不过,真实业务,或许可以定义更加确切的数据。

      function isBridge (url) {
        return url.startsWith('/regester?')
      }
      

      代码块三:regesterBridge
      简单,看代码再说明:

      function regesterBridge (socket, url) {
        const key = url.match(/(^|&|\?)key=(?<key>[^&]*)(&|$)/)?.groups?.key
        bridges[key] = socket
        socket.removeAllListeners('data')
      }
      
      1. 通过URL查找要注册的bridge的key。
      2. 把改socket连接缓存起来。
      3. 移除bridge的数据监听 — 代码块一里每个socket都有默认的数据监听回调函说,如果不移除,会导致后续数据混乱。

      代码块四:findBridge

      逻辑走到代码块4的时候,说明这已经是一个client请求了,那么,需要先找到它对应的bridge,没有bridge,就需要先注册bridge,然后需要用户稍后再发起client请求。代码如下:

      function findBridge (request, url) {
        let key = url.match(/\/(?<key>[^\/\?]*)(\/|\?|$)/)?.groups?.key
        let bridge = bridges[key]
        if (bridge) return { bridge, key }
      
        const referer = request.match(/\r\nReferer: (?<referer>.+)\r\n/)?.groups?.referer
        if (!referer) return {}
      
        key = referer.split('//')[1].split('/')[1]
        bridge = bridges[key]
        if (bridge) return { bridge, key }
      
        return {}
      }
      
      
      • 从URL中匹配出要代理的bridge的key,找到就返回对应的bridge及key。
      • 找不到再从请求头里的referer里找,找到就返回bridge及key。
      • 都找不到,我们知道在代码块一里会结束掉本次请求。

      代码块五:cacheClientRequest

      代码执行到这里,说明已经是一个client请求了,我们先把这个请求缓存起来,缓存的时候,我们一并把请求对应的bridge、key绑定一起缓存,方便后续操作。

      为什么要缓存client请求?

      在目前的方案里,我们希望请求和响应都是成对有序的。我们知道网络传输都是分片传输的,目前来看,如果我们不在应用层控制请求和响应成对且有序,会导致数据包之间的混乱现象。暂且这样,后续如果有更好方案,可以不在应用层强制控制数据的请求响应有序,可以信赖tcp/ip层。
      讲完原因,我们先来看缓存代码,这里比较简单,复杂的在于逐个取出请求并有序返回整个响应。

      function cacheClientRequest (bridge, key, socket, request, url) {
        if (clients[key]) {
          clients[key].requests.push({bridge, key, socket, request, url})
        } else {
          clients[key] = {}
          clients[key].requests = [{bridge, key, socket, request, url}]
        }
      }
      

      我们先判断该bridge对应的key下是不是已经有client的请求缓存了,如果有,就push进去。

      如果没有,我们就创建一个对象,把本次请求初始化进去。

      接下来就是最复杂的,取出请求缓存,发送给bridge,监听bridge的响应,直到本次响应结束,在删除bridge的数据监听,再试着取出下一个请求,重复上面的动作,直到处理完client的所有请求。

      代码块六:sendRequestToBridgeByKey

      在代码块五的最后,对该块做了概括性的说明。可以先稍作理解,在看下面代码,因为代码里会有一些响应完整性的判断,去除这一些,代码就好理解一些。整个方案,我们没有对请求完整性进行处理,原因是,一个请求的基本都在一份数据包大小内,除非是文件上传接口,我们暂不处理,不然,代码又会复杂一些。

      function sendRequestToBridgeByKey (key) {
        const client = clients[key]
        if (client.isSending) return
      
        const requests = client.requests
        if (requests.length <= 0) return
      
        client.isSending = true
        client.contentLength = 0
        client.received = 0
      
        const {bridge, socket, request, url} = requests.shift()
      
        const newUrl = url.replace(key, '')
        const newRequest = request.replace(url, newUrl)
      
        bridge.write(newRequest)
        bridge.on('data', data => {
          const response = data.toString()
      
          let code = response.match(/^HTTP[S]*\/[1-9].[0-9] (?<code>[0-9]{3}).*\r\n/)?.groups?.code
          if (code) {
            code = parseInt(code)
            if (code === 200) {
              let contentLength = response.match(/\r\nContent-Length: (?<contentLength>.+)\r\n/)?.groups?.contentLength
              if (contentLength) {
                contentLength = parseInt(contentLength)
                client.contentLength = contentLength
                client.received = Buffer.from(response.split('\r\n\r\n')[1]).length
              }
            } else {
              socket.write(data)
              client.isSending = false
              bridge.removeAllListeners('data')
              sendRequestToBridgeByKey(key)
              return
            }
          } else {
            client.received += data.length
          }
      
          socket.write(data)
      
          if (client.contentLength <= client.received) {
            client.isSending = false
            bridge.removeAllListeners('data')
            sendRequestToBridgeByKey(key)
          }
        })
      }
      
      

      从clients里取出bridge key对应的client。
      判断该client是不是有请求正在发送,如果有,结束执行。如果没有,继续。
      判断该client下是否有请求,如果有,继续,没有,结束执行。
      从队列中取出第一个,它包含请求的socket及缓存的bridge。
      替换掉约定的数据,把最终的请求数据发送给bridge。
      监听bridge的数据响应。

      • 获取响应code
        • 如果响应是200,我们从中获取content length,如果有,我们对本次请求做一些初始化的操作。设置请求长度,设置已经发送的请求长度。
        • 如果不是200,我们把数据发送给client,并且结束本次请求,移除本次数据监听,递归调用sendRequestToBridgeByKey
      • 如果没有获取的code,我们认为本次响应非第一次,于是,把其长度累加到已发送字段上。
      • 我们接着发送该数据到client。
      • 再判断响应的长度是否和已经发送的过的数据长度一致,如果一致,设置client的数据发送状态为false,移除数据监听,递归调用递归调用sendRequestToBridgeByKey。

      至此,核心代码逻辑已经全部结束。

      总结

      理解这套代码之后,就可以在其上做扩展,丰富代码,为你所用。理解完这套代码,你能想到,它还有哪些使用场景吗?是不是这个思路也可以用在远程控制上,如果你要控制客户端时,从这段代码找找,是不是会有灵感。
      这套代码或许会有难点,可能要对tcp/ip所有了解,也需要对http有所了解,并且知道一些关键的请求头,知道一些关键的响应信息,当然,对于http了解的越多越好。
      如果有什么需要交流,欢迎留言。

      proxyServe源码

      const net = require('net')
      
      const bridges = {}
      const clients = {}
      
      net.createServer(socket => {
        socket.on('data', data => {
          const request = data.toString()
          const url = request.match(/.+ (?<url>.+) /)?.groups?.url
          
          if (!url) return
      
          if (isBridge(url)) {
            regesterBridge(socket, url)
            return
          }
      
          const { bridge, key } = findBridge(request, url)
          if (!bridge) return
      
          cacheClientRequest(bridge, key, socket, request, url)
      
          sendRequestToBridgeByKey(key)
        })
      }).listen(80)
      
      function isBridge (url) {
        return url.startsWith('/regester?')
      }
      
      function regesterBridge (socket, url) {
        const key = url.match(/(^|&|\?)key=(?<key>[^&]*)(&|$)/)?.groups?.key
        bridges[key] = socket
        socket.removeAllListeners('data')
      }
      
      function findBridge (request, url) {
        let key = url.match(/\/(?<key>[^\/\?]*)(\/|\?|$)/)?.groups?.key
        let bridge = bridges[key]
        if (bridge) return { bridge, key }
      
        const referer = request.match(/\r\nReferer: (?<referer>.+)\r\n/)?.groups?.referer
        if (!referer) return {}
      
        key = referer.split('//')[1].split('/')[1]
        bridge = bridges[key]
        if (bridge) return { bridge, key }
      
        return {}
      }
      
      function cacheClientRequest (bridge, key, socket, request, url) {
        if (clients[key]) {
          clients[key].requests.push({bridge, key, socket, request, url})
        } else {
          clients[key] = {}
          clients[key].requests = [{bridge, key, socket, request, url}]
        }
      }
      
      function sendRequestToBridgeByKey (key) {
        const client = clients[key]
        if (client.isSending) return
      
        const requests = client.requests
        if (requests.length <= 0) return
      
        client.isSending = true
        client.contentLength = 0
        client.received = 0
      
        const {bridge, socket, request, url} = requests.shift()
      
        const newUrl = url.replace(key, '')
        const newRequest = request.replace(url, newUrl)
      
        bridge.write(newRequest)
        bridge.on('data', data => {
          const response = data.toString()
      
          let code = response.match(/^HTTP[S]*\/[1-9].[0-9] (?<code>[0-9]{3}).*\r\n/)?.groups?.code
          if (code) {
            code = parseInt(code)
            if (code === 200) {
              let contentLength = response.match(/\r\nContent-Length: (?<contentLength>.+)\r\n/)?.groups?.contentLength
              if (contentLength) {
                contentLength = parseInt(contentLength)
                client.contentLength = contentLength
                client.received = Buffer.from(response.split('\r\n\r\n')[1]).length
              }
            } else {
              socket.write(data)
              client.isSending = false
              bridge.removeAllListeners('data')
              sendRequestToBridgeByKey(key)
              return
            }
          } else {
            client.received += data.length
          }
      
          socket.write(data)
      
          if (client.contentLength <= client.received) {
            client.isSending = false
            bridge.removeAllListeners('data')
            sendRequestToBridgeByKey(key)
          }
        })
      }
      
      

      到此这篇关于Nodejs实现内网穿透服务的文章就介绍到这了,更多相关Node 内网穿透内容请搜索NICE源码以前的文章或继续浏览下面的相关文章希望大家以后多多支持NICE源码!

      免责声明:
      1、本网站所有发布的源码、软件和资料均为收集各大资源网站整理而来;仅限用于学习和研究目的,您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。 不得使用于非法商业用途,不得违反国家法律。否则后果自负!

      2、本站信息来自网络,版权争议与本站无关。一切关于该资源商业行为与www.niceym.com无关。
      如果您喜欢该程序,请支持正版源码、软件,购买注册,得到更好的正版服务。
      如有侵犯你版权的,请邮件与我们联系处理(邮箱:skknet@qq.com),本站将立即改正。

      NICE源码网 JavaScript Nodejs实现内网穿透服务 https://www.niceym.com/27630.html