nodemcu-firmware/docs/modules/httpd.md

12 KiB

LEDC Module

Since Origin / Contributor Maintainer Source
2021-11-07 Johny Mattsson Johny Mattsson httpd.c

This module provides an interface to Espressif's web server component.

HTTPD Overview

The httpd module implements support for both static file serving and dynamic content generation. For static files, all files need to reside under a common prefix (the "webroot") in the (virtual) filesystem. The module does not care whether the underlying file system supports directories or not, so files may be served from SPIFFS, FAT filesystems, or whatever else may be mounted. If you wish to include the static website contents within the firmware image itself, considering using the EROMFS module.

Unlike the default behaviour of the Espressif web server, this module serves static files based on file extensions primarily. Static routes are typically defined as a file extension (e.g. *.html) and the Content-Type such files should be served as. A number of file extensions are included by default and should cover the basic needs:

  • *.html (text/html)
  • *.css (text/css)
  • *.js (text/javascript)
  • *.json (application/json)
  • *.gif (image/gif)
  • *.jpg (image/jpeg)
  • *.jpeg (image/jpeg)
  • *.png (image/png)
  • *.svg (image/svg+xml)
  • *.ttf (font/ttf)

The native Espressif approach may also be used if you prefer, but is harder to work with. Both schemes can coexist in most cases without issues. When using the native approach, URI wildcard matching is supported.

Dynamic routes may be registered, which when accessed by a client will result in a Lua function being invoked. This function may then generate whatever content is applicable, for example obtaining a sensor value and returning it.

Note that if you are writing sensor data to files and serving those files statically you will be susceptible to race conditions where the file contents may not be available from the outside. This is due to the web server running in its own FreeRTOS thread and serving files directly from that thread concurrently with the Lua VM running as usual. It is therefore safer to instead serve such content on a dynamic route, even if all that route does is reads the file and serves that.

An example of such a setup:

function handler(req)
  local f = io.open('/path/to/mysensordata.csv', 'r')
  return {
    status = "200 OK",
    type = "text/plain",
    getbody = function()
      local data = f:read(512) -- pick a suiteable chunk size here
      if not data then f:close() end
      return data
    end,
  }
end

httpd.dynamic(httpd.GET, "/mysensordata", handler)

httpd.start()

Starts the web server. The server has to be started before routes can be configured.

Syntax

httpd.start({
  webroot = "<static file prefix>",
  max_handlers = 20,
  auto_index = httpd.INDEX_ALL,
})

Parameters

A single configuration table is provided, with the following possible fields:

  • webroot (mandatory) This sets the prefix used when serving static files. For example, with webroot set to "web", a HTTP request for "/index.html" will result in the httpd module trying to serve the file "web/index.html" from the file system. Do NOT set this to the empty string, as that would provide remote access to your entire virtual file system, including special files such as virtual device files (e.g. "/dev/uart1") which would likely present a serious security issue.
  • max_handlers (optional) Configures the maximum number of route handlers the server will support. Default value is 20, which includes both the standard static file extension handlers and any user-provided handlers. Raising this will result in a bit of additional memory being used. Adjust if and when necessary.
  • auto_index Sets the indexer mode to be used. Most web servers automatically go looking for an "index.html" file when a directory is requested. For example, when pointing your web browser to a web site for the first time, e.g. http://www.example.com/ the actual request will come through for "/", which in turn commonly gets translated to "/index.html" on the server. This behaviour can be enabled in this module as well. There are three modes provided:
    • httpd.INDEX_NONE No automatic translation to "index.html" is provided.
    • httpd.INDEX_ROOT Only the root ("/") is translated to "/index.html".
    • httpd.INDEX_ALL Any path ending with a "/" has "index.html" appended. For example, a request for "subdir/" would become "subdir/index.html", which in turn might result in the file "web/subdir/index.html" being served (if the webroot was set to "web"). The default value is httpd.INDEX_ROOT.

Returns

nil

Example

httpd.start({ webroot = "web", auto_index = httpd.INDEX_ALL })

httpd.stop()

Stops the web server. All registered route handlers are removed.

Syntax

httpd.stop()

Parameters

None.

Returns

nil

httpd.static()

Registers a static route handler.

Syntax

httpd.static(route, content_type)

Parameters

  • route The route prefix. Typically in the form of *.ext to serve all files with the ".ext" extension statically. Refer to the Espressif documentation if you wish to use the native Espressif style of static routes instead.
  • content_type The value to send in the Content-Type header for this file type.

Returns

An error code on failure, or nil on success. The error code is the value returned from the httpd_register_uri_handler() function.

Example

httpd.start({ webroot = "web" })
httpd.static("*.csv", "text/csv") -- Serve CSV files under web/

httpd.dynamic()

Registers a dynamic route handler.

Syntax

httpd.dynamic(method, route, handler)

Parameters

  • method The HTTP method this route applies to. One of:
    • httpd.GET
    • httpd.HEAD
    • httpd.PUT
    • httpd.POST
    • httpd.DELETE
  • route The route prefix. Be mindful of any trailing "/" as that may interact with the auto_index functionality.
  • handler The route handler function - handler(req). The provided request object req has the following fields/functions:
    • method The request method. Same as the method parameter above. If the same function is registered for several methods, this field can be used to determine the method the request used.
    • uri The requested URI. Includes both path and query string (if applicable).
    • query The query string on its own. Not decoded.
    • headers A table-like object in which request headers may be looked up. Note that due to the Espressif API not providing a way to iterate over all headers this table will appear empty if fed to pairs().
    • getbody() A function which may be called to read in the request body incrementally. The size of each chunk is set via the Kconfig option "Receive body chunk size". When this function returns nil the end of the body has been reached. May raise an error if reading the body fails for some reason (e.g. timeout, network error).

Note that the provided req object is only valid within the scope of this single invocation of the handler. Attempts to store away the request and use it later will fail.

Returns

A table with the response data to send to the requesting client:

{
  status = "200 OK",
  type = "text/plain",
  headers = {
    ['X-Extra'] = "My custom header value"
  },
  body = "Hello, Lua!",
  getbody = dynamic_content_generator_func,
}

Supported fields:

  • status The status code and string to send. If not included "200 OK" is used. Other common strings would be "404 Not Found", "400 Bad Request" and everybody's favourite "500 Internal Server Error".
  • type The value for the Content-Type header. The Espressif web server component handles this header specially, which is why it's provided here and not within the headers table.
  • body The full content body to send.
  • getbody A function to source the body content from, similar to the way the request body is read in. This function will be called repeatedly and the returned string from each invocation will be sent as a chunk to the client. Once this function returns nil the body is deemed to be complete and no further calls to the function will be made. It is guaranteed that the function will be called until it returns nil even if the sending of the content encounters an error. This ensures that any resource cleanup necessary will still take place in such circumstances (e.g. file closing).

Only one of body and getbody should be specified.

Example

httpd.start({ webroot = "web" })

function put_foo(req)
  local body_len = tonumber(req.headers['content-length']) or 0
  if body_len < 4096
  then
    local f = io.open("/upload/foo.txt", "w")
    local body = req.getbody()
    while body
    do
      f:write(body)
      body = req.getbody()
    end
    f:close()
    return { status = "201 Created" }
  else
    return { status = "400 Bad Request" }
  end
end

httpd.dynamic(httpd.PUT, "/foo", put_foo)

httpd.websocket()

Registers a websocket route handler. This is optional, and must be selected explicitly in the configuration.

Syntax

httpd.websocket(route, handler)

Parameters

  • route The route prefix. Be mindful of any trailing "/" as that may interact with the auto_index functionality.
  • handler The route handler function - handler(req, ws). The req object is the same as for a regular dynamic route. The provided websocket object ws has the following fields/functions:
    • on This allows registration of handlers when data is received. This is invoked with two arguments -- the name of the event and the handler for that event. The allowable names are:
      • text The handler is called with a single string argument whenever a text message is received.
      • binary The handler is called with a single string argument whenever a binary message is received.
      • close The handler is called when the client wants to close the connection.
    • text This can be called with a string argument and it sends a text message.
    • binary This can be called with a string argument and it sends a binary message.
    • close The connection to the client is closed.

Returns

nil

Example

httpd.start({
  webroot = "<static file prefix>",
  max_handlers = 20,
  auto_index = httpd.INDEX_ALL,
})

function echo_ws(req, ws)
  ws:on('text', function(data) print(data) ws:text(data) end)
end

httpd.websocket("/echo", echo_ws)

function tick(ws, n) 
  ws:text("tick: " .. n)
  n = n + 1
  if n < 6 then
    tmr.create():alarm(1000, tmr.ALARM_SINGLE, function () 
       tick(ws ,n)
    end)
  else
    ws:close()
  end
end

function heartbeat(req, ws) 
  tick(ws, 0)
end

httpd.websocket("/beat", heartbeat)

httpd.unregister()

Unregisters a previously registered handler. The default handlers may be unregistered.

Syntax

httpd.unregister(method, route)

Parameters

  • method The method the route was registered for. One of:
    • httpd.GET
    • httpd.HEAD
    • httpd.PUT
    • httpd.POST
    • httpd.DELETE
  • route The route prefix.

Returns

1 on success, nil on failure (including if the route was not registered).

Example

Unregistering one of the default static handlers:

httpd.start({ webroot = "web" })
httpd.unregister(httpd.GET, "*.jpeg")