broad

This series has been abandoned for a long time, I don’t know how many people still remember ๐Ÿ˜‚. If you remember, I’m trying to find a time to replace the previous weather station, otherwise it’s going to pass.

Today brings the third article in this series, which mainly uses two modules (NET, file) and covers a wide range of topics. But I’m going to finish it in one article.

Before we start, let’s talk about how it works. When accessing a domain name, the APP sends a DNS request to the server for the IP address of the domain name. It then makes an HTTP request for the desired content.

How to implement

As you can see from the above principle, you need to implement a DNS server and a TCP server, and also need to polish HTML to implement an ugly page. The following three steps will be used to implement the function.

The DNS server

The first thing you need to know is that DNS uses UDP and uses port 53. The main job of this DNS server is to respond to a DNS response with the IP address of the device, regardless of what happens.

Before implementing this DNS server, you need to understand the DNS protocol. Only after you know what the DNS data frame looks like can you construct a reply packet. Here is a more clear and clear article, interested can read. Or look at part 4 here (message).

The DNS protocol frame looks like this, including the header, the question (which is the domain name), and the answer (which is the IP).

Here are the header details,

The thing to know about the header is,

  • 1. The frame format of DNS request and response data is the same.
  • 2. The ID of the response header is that of the direct replication request header.
  • 3. The header takes up 12 bytes, meaning that question begins at the 13th byte.

Next, the details of question and answer

    1. Qname and name have the same content, namely domain name;
    1. The length of qname and name is indeterminate (for different domains) and ends in 0x00. The position starts at the 13th byte.
    1. Rdata is an IP address, four bytes long.

For more details, please refer to the wireShark documentation, which is also a good choice: ๐Ÿ˜. This will help you understand DNS.

As can be seen from the above analysis results, the core function of this DNS server is to resolve the Qname in the replication request frame. This is not too hard to do, just copy the contents of the first 0x00 from the 13th byte of the request frame.

Let’s start live coding!

module = {}

local dns_ip=wifi.ap.getip()
local i1,i2,i3,i4=dns_ip:match("(%d+)%.(%d+)%.(%d+)%.(%d+)")
local x00=string.char(0)
local x01=string.char(1)
local dns_str1=string.char(128).. x00.. x00.. x01.. x00.. x01.. x00.. x00.. x00.. x00localdns_str2=x00.. x01.. x00.. x01..string.char(192)..string.char(12).. x00.. x01.. x00.. x01.. x00.. x00..string.char(3).. x00.. x00..string.char(4)
local dns_strIP=string.char(i1)..string.char(i2)..string.char(i3)..string.char(i4)

local dnsServer = nil
Copy the code

See the table variable (module) at the beginning, the DNS server part of the code will eventually be packaged into a module, available to other files or module calls.

The advantage of modularity is that it encapsulates and privates variables, both to decouple and maintain code.

A bunch of variables in the middle that are used to build response frames later. The last variable is used to store the DNS server created. Because an instantiated NET Server module can listen only once. Encapsulate it so you don’t get any errors.

Now comes the core code, which parses the request frame to find the Qname. By the way, let’s say the domain name is WWW.1234.COM. So the format 3 WWW 4 1234 3 COM that’s stored in qname, dot ยท is not going to be written in qname.

-- get the question
local function decodeQuery(payload)
  local len = #payload
  local pos = 13
  local char = ""
  while string.byte(payload, pos) ~= 0 do
    pos = pos + 1
  end
  return string.sub(payload, 13, pos)
end
Copy the code

And then the code to create a DNS server,

--start the dns server
function module.startdnsServer(a)
  if dnsServer == nil then
    dnsServer = net.createUDPSocket()
    dnsServer:on("receive".function(sck, data, port, ip)
      local id = string.sub(data, 1.2)
      local query = decodeQuery(data)
      localresponse = id.. dns_str1.. query.. dns_str2.. dns_strIP-- print(string.byte(query, 1, #query))
  -- print(string.byte(response, 1, #response))
      sck:send(port, ip, response)
    end)
    
    dnsServer:listen(53)
    print("dns server start, heap = "..node.heap())
  end
  return true
end
Copy the code

Determine if dnsServer is nil before creating a UDPSocket instance. If so, create the instance and listen on port 53. Also add a callback for the receive event. When the request frame is received, the data frame is parsed, the response frame is packaged, and the response frame is finally returned. Note that starting port listening comes last.

Finally, shut down the DNS service and return module. The functions to start and close the service are in the table, and code elsewhere can use these functions by accessing the key in the table.

--stop the dns server
function module.stopdnsServer(a)
  if dnsServer ~= nil then
    dnsServer:close()
    dnsServer = nil
  end
  return true
end

return module
Copy the code

At this point, the DNS server is done. All APP DNS requests will receive a response packet with the IP address of the device. The APP will then make an HTTP request to this IP address.

TCP server

To be able to respond to HTTP requests, you need to create a TCP instance using the NET module. Unless otherwise specified, port 80 is used. So, just create a TCP instance that listens on port 80. Ignore requests that are not port 80.

The TCP server’s job is simple: when it listens for a request from port 80, it sends back the HTML file. You don’t need to know what the other party’s request is.

module = {}

local server = nil
local f = nil

okHeader = "HTTP/1.0 200 OK\r\nServer: NodeMCU on ESP8266\r\ nContent-type: text/ HTML \r\n\r\n"

local function serverOnSent(sck, payload)
  local content = f.read(500)
-- print(content)
  if content then
    sck:send(content)
  else
    sck:close()
    sent = false
  end
end
Copy the code

Similar to DNS modularization, you don’t need to add a local keyword to a known variable. OkHeader is a variable that stores the HTTP response header. The serverOnSent function is a callback to the SENT event. It simply reads the file and sends it in 500 bytes (TCP has a maximum frame length, so it needs to send it in 500 bytes).

In addition to the sent event, there is also the receive event. Here is the corresponding code

local function serverOnReceive(sck, payload, callback)
  local _, _, method, path, query = string.find(payload, "([A-Z]+) (.+)? (.+) HTTP")
  if method == nil then
    _, _, method, path = string.find(payload, "([A-Z]+) (.+) HTTP")
  end
  callback(sck, method, path, query)
  if method ~= nil then
    if f then
      f.seek("set".0)
    end
    sck:send(okHeader)
  end
end
Copy the code

The function begins by parsing the request header, more on that later. This is followed by a callback function. Finally, there is a mindless reply that sends back a response header to trigger the SENT event. Of course, to avoid being too mindless, a simple filter is made. Only the received data contains the request header will respond.

The reason for using callbacks here is that serverOnReceive is an internal function and the inclusion of callbacks makes it easier for external functions to extend specific functionality.

Finally, there are the start function and the close function, which is easy to close.

function module.startServer(callback, path, p)
  local port = p or 80
  local exists = file.exists(path or "index.html")
  if server == nil then
    server = net.createServer()
    if server == nil then return false."server create failed" end
    server:listen(80.function(sck)
      sck:on("receive".function(sck, payload)
        serverOnReceive(sck, payload, callback)
      end)
      sck:on("sent".function(sck, payload)
        serverOnSent(sck, payload)
      end)
    end)
  end
  if exists ~= true then return false."file not exist" end
  f = file.open(path or "index.html")
  if f == nil then return false."file open failed" end
  print("html server start, heap = "..node.heap())
  return true
end

function module.stopServer(a)
  if server ~= nil then
    server:close()
    server = nil
  end
  return result 
end
Copy the code

Start the function with default parameters for path and p. So, when these two arguments are nil, give them the default value.

Then create a TCP instance and listen on port 80. The LISTEN function of a TCP instance brings back calls. This is different from UDP.

In addition, there are some validity judgments in the function. Error flags and error messages are returned if validation is not passed. Lua supports multiple parameter returns. The specific usage is

local result, msg = startServer()
Copy the code

Start the service

Both servers are written, write a file to start it. The code is fairly simple

local htmlServer = require "server"
local dnsServer = require "dnsServer"

dnsServer.startdnsServer()
htmlServer.startServer(function (sck, method, path, query)
  print(method, path, query)
end)
Copy the code

Start by importing the two modules using the require keyword and rename them. The import prerequisite is to store the above two files in nodemcu with the names server.lua and dnsserver. lau respectively.

Replace print(method, path, query) with something else, and you can do anything you can think of.

Short of HTML

In fact, the above is not complete. Because I still need an HTML file. The content of this file is also simple. However involved in the content of the front end, not going into detail. See the full code here.

Just a quick word about XHR requests

    function connect() {
      let url = '/setwifi? ssid=' + encodeURIComponent($('#ssid').value) + '&pwd=' + encodeURIComponent($('#pwd').value);
      let xhr = new XMLHttpRequest();

      xhr.onloadend = function () {$('#success').style.display = 'inline';
      }
      xhr.open('GET', url, true);
      xhr.send();
    }
Copy the code

Here XHR is used to submit a GET request. The request header looks something like GET /setwifi? ssid=X&pwd=Y HTTP… . The receive callback resolves this header to GET GET /setwifi SSId =X&pwd=Y, stored in three variables.

If you’ve read the previous article, you might be under the impression that you didn’t need to use XHR to submit requests. Instead, you parse the URL that the browser accesses. This time it is different, because the TCP received request header is initiated by APP, so it does not know what it looks like. If you don’t know the content, you can’t do the next one. However, with XHR you can initiate a known request.

Welcome to star

At this point, the project of mandatory portal authentication is complete. The code for the project can be found on GitHub. If other new articles are updated later, the code will be updated to the above

Anyway, just welcome Star

Series of articles first convenient in Jane books