You may not be able to find an article on the web that explains Intranet penetration at the code level, but I did a search and found no results.

1. Intra-lan proxy

Let’s review the previous article, how to implement a LAN service proxy? Because this is pretty simple, I’ll just go to the code.

const net = require('net')

const proxy = net.createServer(socket= > {
  const localServe = new net.Socket()
  localServe.connect(5502.'192.168.31.130') // Service port and IP address of the LAN.

  socket.pipe(localServe).pipe(socket)
})

proxy.listen(80)
Copy the code

This is a very simple server-side proxy. The code is simple and clear. If in doubt, it is probably a pipe. A socket is a full-duplex stream, that is, a data stream that is both readable and writable. In this code, when the socket receives data from the client, it writes the data to localSever. When the localSever has data, it writes the data to the socket, and the socket sends the data to the client.

2. Intranet penetration

LAN proxy is simple, Intranet penetration is not so simple, but it is the core code, need to do quite logical processing on it. Before the concrete implementation, we first comb through the Intranet penetration.

What is Intranet penetration?

In simple terms, it is a public network client that can access services on the LAN. For example, locally started services. How can the public network client know about the local server? The public network server must be used. How does the public network server know about the local service? This requires the local and server to establish a socket link.

Four roles

From the above description, we introduce four characters.

  1. Public network client, let’s call it client.
  2. Public network server, because of the role of proxy, we named proxyServe.
  3. The local service is named localServe.
  4. It is the bridge between proxyServe and localServe. It is responsible for data transfer. We call it Bridge.

Client and localServe don’t need us to care, because the client can be a browser or something, localServe is just a normal local service. We only need to care about proxyServe and Bridge. We are still introduced here is the simplest way to implement, to provide a way of thinking and thinking, that we start from the simplest.

bridge

The bridge is a socket connection to proxyServe, and it is a data transfer.

const net = require('net')

const proxyServe = '10.253.107.245'

const bridge = new net.Socket()
bridge.connect(80, proxyServe, _= > {
  bridge.write('GET /regester? Key = HTTP / 1.1 sq \ 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))
  })
})
Copy the code

The code is clear and readable, even catchy. Net library is introduced, public network address is declared, bridge is created, and bridge is connected to proxyServe. After success, local service is registered with proxyServe. Then, Bridge listens for data and creates connection with local service when request arrives. The request data is sent to localServe, while listening for the response data and writing the response stream to the Bridge.

There’s nothing else to explain; after all, this is just sample code. But there is a /regester in the sample code? Key =sq, this key is very useful, key=sq. If the role client accesses the local service through the proxy service, the key needs to be added to the path, so that proxyServe can correspond to the upper bridge, and thus corresponds to localServe.

For example, lcoalServe is http://localhost:8088, rpoxyServe is example.com, and the registered key is SQ. To access localServe via prxoyServe, write: example.com/sq. Why do you write that? Of course, it’s just a definition, and you can modify the convention after you read the code in this article.

So, here’s the key code:

proxyServe

Although proxyServe here is a simplified example code, it is still a bit complicated to talk about. It will take some effort to thoroughly understand it and make it usable code with your own business. So I’m going to break it up into pieces and try to make sense of it, but let’s give it a name just to make it easier.

Block 1: createServe

The main function of this block is to create proxy service, establish socket link with client and bridge, socket listens for data request, do logic processing in callback function, specific code is as follows:

const net = require('net')

const bridges = {} // When a bridge establishes a socket connection, the cache is stored here
const clients = {} // When a client establishes a socket connection, the cache is stored here

net.createServer(socket= > {
  socket.on('data'.data= > {
    const request = data.toString()
    const url = request.match(/. + (? 
      
       .+) /
      )? .groups? .urlif(! 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)
Copy the code

Take a look at the code logic in the data listener:

  1. Convert the request data to a string.
  2. Search the URL in the request. If no URL is found, end the request.
  3. Check whether the URL is a bridge. If so, register the bridge. If not, consider it a client request.
  4. Check if the client request has a registered bridge — remember, this is a proxy service, and without a registered bridge, the request is considered invalid.
  5. Cache this request.
  6. The request is then sent to bridge.

Combined with the code and logic comb, should be able to understand, but, 5 May have questions, next one comb.

Block 2: isBridge

It’s easy to determine if it’s a bridge registration request, but a real business might be able to define more precise data.

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

Code block 3: regesterBridge

Simple, look at the code to illustrate:

function regesterBridge (socket, url) {
  const key = url.match(/ (^ | & | \? key=(? 
      
       [^&]*)(&|$)/
      )? .groups? .key bridges[key] = socket socket.removeAllListeners('data')}Copy the code
  1. Find the key of the bridge to be registered through the URL.
  2. Cache the changed socket connections.
  3. Remove bridge listener – Each socket in block 1 has a default listener callback saying that if not removed, subsequent data will be corrupted.

Block 4: findBridge

When the logic reaches block 4, it is already a client request, so it needs to find its corresponding bridge first. If there is no bridge, it needs to register the bridge first, and then the user needs to make a client request later. The code is as follows:

function findBridge (request, url) {
  let key = url.match(/ / /? 
      
       [^\/\?] (*) \ / | \? | $) /
      )? .groups? .keylet bridge = bridges[key]
  if (bridge) return { bridge, key }

  const referer = request.match(/\r\nReferer: (? 
      
       .+)\r\n/
      )? .groups? .refererif(! referer)return {}

  key = referer.split('/ /') [1].split('/') [1]
  bridge = bridges[key]
  if (bridge) return { bridge, key }

  return{}}Copy the code
  1. Match the key of the bridge to be proxied from the URL, and return the corresponding bridge and key if found.
  2. If the bridge and key are not found, refer to the referer in the request header.
  3. None. We know that block 1 will terminate the request.

Block 5: cacheClientRequest

When the code is executed here, it indicates that it has already been a client request. We will cache this request first. During caching, we will bind the corresponding bridge and key together with the cache for the convenience of subsequent operations.

Why cache client requests?

In the current scenario, we want requests and responses to be ordered in pairs. We know that network traffic is fragmented, and at present, if we do not control the request and response in pairs and order at the application layer, it will lead to chaos between packets. For the time being, if there is a better solution in the future, we can not force the application layer to control the data request and response order, and can rely on the TCP/IP layer.

After that, let’s take a look at the caching code, which is a little bit simpler here, but the tricky part is taking requests one by one and returning the entire response in an orderly fashion.

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}]
  }
}
Copy the code
  1. We first check whether the key corresponding to the bridge already has the client’s request cache. If so, we push the request into the key.
  2. If not, we create an object that initializes the request.

The next step is the most complicated one: fetch the request cache, send it to the bridge, listen for the bridge’s response until the end of the response, delete the bridge’s data listening, and try to fetch the next request, repeat the above action until all the client’s requests are processed.

Block 6: sendRequestToBridgeByKey

At the end of block 5, a general description of the block is given. You can understand it a little bit by looking at the code below, because there are some judgments about response integrity in the code, and it’s a little bit easier to understand if you take those away. In the whole scheme, we did not deal with the request integrity, because a request is basically within the size of a packet, unless it is a file upload interface, we do not deal with it temporarily, otherwise, the code will be more complicated.

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] (? [0-9]{3}).*\r\n/)? .groups? .codeif (code) {
      code = parseInt(code)
      if (code === 200) {
        let contentLength = response.match(/\r\nContent-Length: (? 
      
       .+)\r\n/
      )? .groups? .contentLengthif (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)
    }
  })
}
Copy the code
  1. Retrieve bridge key from clients.
  2. Check whether the client is sending requests. If yes, no further action is required. If not, continue.
  3. Check whether the client has a request. If yes, continue. If no, end the execution.
  4. Fetch the first one from the queue, which contains the requested socket and the cached bridge.
  5. Replace the agreed data and send the final request data to the Bridge.
  6. Listen for the bridge’s data response.
    • Get the response code
      • If the response is 200, we get the Content Length from it. If so, we do some initialization for the request. Set request Length. Sets the length of the sent request.
      • If it is not 200, we send the data to the client and end the request, remove the data listening and recursively call sendRequestToBridgeByKey
    • If we don’t get the code, we assume that this response is not the first one, so we add its length to the sent field.
    • We then send that data to the client.
    • Then check whether the length of the response is consistent with the length of the sent data. If so, set the data sending status of the client to false, remove the data listening, and recursively call sendRequestToBridgeByKey.

At this point, the core code logic is complete.

conclusion

Once you understand the code, you can expand on it and enrich it for your own use. Now that you understand this code, can you think of any other scenarios in which it can be used? I wonder if the same idea can be applied to remote control. If you want to control the client, look for inspiration in this code.

This code might be a bit tricky, you might want to know everything about TCP/IP, you might want to know something about HTTP, you might want to know some key headers, you might want to know some key response information, and of course, the more you know about HTTP, the better.

If you have anything to communicate, please leave a message.

ProxyServe source

const net = require('net')

const bridges = {}
const clients = {}

net.createServer(socket= > {
  socket.on('data'.data= > {
    const request = data.toString()
    const url = request.match(/. + (? 
      
       .+) /
      )? .groups? .urlif(! 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=(? 
      
       [^&]*)(&|$)/
      )? .groups? .key bridges[key] = socket socket.removeAllListeners('data')}function findBridge (request, url) {
  let key = url.match(/ / /? 
      
       [^\/\?] (*) \ / | \? | $) /
      )? .groups? .keylet bridge = bridges[key]
  if (bridge) return { bridge, key }

  const referer = request.match(/\r\nReferer: (? 
      
       .+)\r\n/
      )? .groups? .refererif(! 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] (? [0-9]{3}).*\r\n/)? .groups? .codeif (code) {
      code = parseInt(code)
      if (code === 200) {
        let contentLength = response.match(/\r\nContent-Length: (? 
      
       .+)\r\n/
      )? .groups? .contentLengthif (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)
    }
  })
}
Copy the code