Modular Computers the manual GitHub

Internet Card

An internet card makes HTTP and HTTPS requests to the real internet. The card speaks HTTP itself, because a program’s Lua cannot do TLS.

Facts

| | | |—|—| | Type | internet | | Address | An expansion slot path such as exp:0 | | Interrupt | Raised while a request has news for the program | | Power | 4 W at rest; a request costs a tick of a running CPU, and each piece of a body read a tenth of that | | Limits | 4 requests open per card, 64 on the server; 16 pieces of a body wait unread per request; a request body of at most 1 MiB | | Info | kind (internet) |

What it can reach

A card is a program running on the server’s network, so it is kept to the public internet. Requests to loopback, private, link-local (cloud metadata), CGNAT, multicast and reserved ranges are refused, in IPv4 and IPv6. Every address a name resolves to is judged, and the connection opens to the address that was judged (so a name cannot change its answer between the check and the connection). Redirects are the program’s to follow, so each hop is judged again.

A request cannot set the headers that frame it (Host, Content-Length, Connection and the like), cannot carry line breaks in a header, and cannot put credentials in its URL. The card sends no proxies, cookies or server details; it names itself ModularComputers.

A server’s operator can turn it off (internet_enabled) and allow or deny hosts and ranges (internet_allow, internet_deny). When it is off, request fails with the internet is turned off on this server.

Timeouts: 10 seconds to connect, 30 with nothing read, 10 minutes in all. The status line and headers together are at most 64 KiB. The methods are GET, HEAD, POST, PUT, DELETE, PATCH and OPTIONS.

In mcos

The card is used as any device: through its file, /dev/internet0 (internet1 for the next card), whose ioctls are its methods by name (below), in any open mode:

local sys = require("sys")
local card = assert(sys.open("/dev/internet0"))
local handle = assert(sys.ioctl(card, "request", "https://example.com/"))

Reading the file gives its access log, a line for each request as it ended, as a web server’s: the method, the URL, the status (or - and why it failed), how long it took and the bytes it brought.

GET https://example.com/ 200 143ms 1256
POST https://example.com/paste 201 310ms 42
GET http://10.0.0.1/ - 2ms 0 refused: a private address (the server's network)

An ioctl’s answer is the method’s (true for close, which answers nothing). A method’s failure is nil, its message and EINVAL; a card that has gone is ENODEV.

wget and curl are built on these methods, through the http library (require("http"), see the sys library): http.request(url, options) makes a request (options.method, headers, body, follow to follow redirects, up to 20, and device for a card’s file other than the first), and gives the response {status, message, headers, url} and a reader for its body (reader:read(n), "" at its end; reader:close()), or nil and why.

Methods

request(url[, method[, headers[, body]]]) → handle

Starts a request and gives its handle (a number) at once. The method defaults to GET, or POST when there is a body. headers is a table of names and values. The body is a string. The URL must be http or https with a host. It fails with a message when the request cannot be made: not an http or https URL, not a method it sends, a header the card writes itself, a body over 1 MiB, too many requests open (4), or the server's internet is busy: try again soon. Whether the address is allowed is judged as the request runs, and a refusal is the request’s failure (refused: ...). A request costs power.

response(handle) → {status, message, headers} or false

The status line and headers once they have come (headers by name, in lower case); false while they have not. It fails with why, when the request did (refused: ..., unknown host, timed out).

read(handle[, n]) → piece, “” or false

The next piece of the body, at most n bytes (a piece is at most 4096); "" at its end; false while nothing more has come. What a piece had past n is the next read’s. A failed request fails its reads with why, after what had come. Each piece costs power.

close(handle) → nothing

Stops a request: its connection closes and what it had waiting goes. A program closes every request it makes, finished or not: a card has only 4.

pull() → entry or nil

The access log’s oldest entry, what the file reads as a line: {method, url, status, millis, bytes, why} (status is 0 and why says why when it failed).

response, read and close fail with no such request: N for a handle that is not open.

Waiting

The interrupt is raised when a request has news (its response came, a piece of its body, its end or its failure) and stays raised until the program asks about it. In mcos, the card’s file is ready for sys.poll’s PRI while it has news, so a program asks again after each poll.

Each request holds at most 16 pieces of its body unread. A request whose next piece finds no room waits for it, so nothing is dropped: a big download goes at the program’s pace. Everything stops when the computer loses power or no program runs: requests close.

Examples

Fetch a page into a file from a shell:

curl -s https://example.com/ > /home/page.html
wget https://example.com/file.txt

Each program below is complete: run it with lua on a computer with an internet card.

Download a file

The methods by ioctl, waiting for news with sys.poll: lua internet-fetch.lua https://example.com/ /home/page.html.

-- Download a URL into a file, through the internet card's own methods.
--
-- The card's methods are ioctls on its file: request starts a request and gives its handle at
-- once; response gives the status and headers once they've come, and read the body a piece at a
-- time, both answering false while there's nothing yet; close ends it. While a request waits, the
-- card's file is ready for sys.poll's PRI when it has news, so the program asks again only then.
--
-- usage: lua internet-fetch.lua URL FILE
local sys = require("sys")

local CARD = "/dev/internet0"
local PIECE = 4096 -- the most one read of the body takes

local url, path = arg[1], arg[2]
if not (url and path) then
  io.stderr:write("usage: internet-fetch URL FILE\n")
  os.exit(2)
end

local card = assert(sys.open(CARD))

-- Asks a method until it answers something other than "not yet" (false), waiting for the card's
-- news between asks. A failure is nil and why, as the method's.
local function await(method, ...)
  while true do
    local answer, why = sys.ioctl(card, method, ...)
    if answer ~= false then
      return answer, why
    end
    sys.poll({[card] = {PRI = true}})
  end
end

local handle = assert(sys.ioctl(card, "request", url))
local response, why = await("response", handle)
if not response then
  io.stderr:write("internet-fetch: ", why, "\n") -- refused, an unknown host, timed out
  os.exit(1)
elseif response.status ~= 200 then
  io.stderr:write("internet-fetch: ", response.status, " ", response.message, "\n")
  sys.ioctl(card, "close", handle)
  os.exit(1)
end

local out = assert(io.open(path, "w"))
local size = 0
while true do
  local piece = assert(await("read", handle, PIECE))
  if piece == "" then
    break -- the end of the body
  end
  out:write(piece)
  size = size + #piece
end
out:close()
sys.ioctl(card, "close", handle)
print(string.format("%s: %d bytes (%s)", path, size, response.headers["content-type"] or "no type"))

Download internet-fetch.lua

Mirror several files at once

Up to 4 requests open, all waited on by one poll: lua internet-mirror.lua https://example.com/files /home/mirror a.txt b.txt c.txt.

-- Mirror a list of files from a web server into a folder, several downloads at once.
--
-- A card has up to 4 requests open at a time. Each is started with the request method and its
-- handle kept; then one loop waits for the card's news (its file's PRI) and asks every open
-- request for what it has: its response first (false: not yet), then pieces of its body
-- (false: none yet; "": its end). A download whose file is full of unread pieces waits for the
-- program, so nothing is lost however slowly it goes.
--
-- usage: lua internet-mirror.lua BASE-URL FOLDER NAME...
local sys = require("sys")

local CARD = "/dev/internet0"
local OPEN = 4 -- requests open at once: the card's most
local PIECE = 4096 -- the most one read of a body takes

local base, folder = arg[1], arg[2]
local queue = table.move(arg, 3, #arg, 1, {}) -- the names still to start
if not (base and folder and queue[1]) then
  io.stderr:write("usage: internet-mirror BASE-URL FOLDER NAME...\n")
  os.exit(2)
end
base = base:gsub("/*$", "/")

local card = assert(sys.open(CARD))
local open = {} -- each request's handle, to {name, out (its file, once the response came)}
local failed = 0

-- Starts requests from the queue while there's room.
local function start()
  while queue[1] and #open < OPEN do
    local name = table.remove(queue, 1)
    local handle, why = sys.ioctl(card, "request", base .. name)
    if handle then
      open[#open + 1] = {handle = handle, name = name}
    else
      io.stderr:write(name, ": ", why, "\n")
      failed = failed + 1
    end
  end
end

-- Ends a download: closes its request and its file.
local function finish(index, why)
  local download = table.remove(open, index)
  sys.ioctl(card, "close", download.handle)
  if download.out then
    download.out:close()
  end
  if why then
    io.stderr:write(download.name, ": ", why, "\n")
    failed = failed + 1
  end
end

-- What a download has now: true when it's done (or failed), false when it waits for more.
local function step(download)
  if not download.out then
    local response, why = sys.ioctl(card, "response", download.handle)
    if response == false then
      return false
    elseif not response then
      return true, why
    elseif response.status ~= 200 then
      return true, response.status .. " " .. response.message
    end
    download.out = assert(io.open(folder .. "/" .. download.name, "w"))
  end
  while true do
    local piece, why = sys.ioctl(card, "read", download.handle, PIECE)
    if piece == false then
      return false
    elseif not piece then
      return true, why
    elseif piece == "" then
      return true
    end
    download.out:write(piece)
  end
end

start()
while open[1] do
  sys.poll({[card] = {PRI = true}}) -- blocks until a request has news
  for index = #open, 1, -1 do
    local done, why = step(open[index])
    if done then
      finish(index, why)
    end
  end
  start()
end
sys.close(card)
os.exit(failed == 0 and 0 or 1)

Download internet-mirror.lua

Post a file

With the http library, following redirects: lua internet-post.lua https://example.com/paste /home/note.txt.

-- Send a file to a web service (a paste site, a webhook, your own server) and write its answer
-- out, with the http library that wget and curl use.
--
-- http.request makes the request through the internet card's methods, waits for its answer, and
-- follows redirects when asked (each hop a new request, judged by the card again). It gives the
-- response (status, message, headers, and the URL it came from) and a reader for the body, whose
-- read gives a piece at a time and "" at its end. The body goes out as a POST with its own
-- Content-Type; the card writes the headers that frame a request (Host, Content-Length) itself.
--
-- usage: lua internet-post.lua URL FILE [content-type]
local sys = require("sys")
local http = require("http")

local PIECE = 4096 -- the most one read of the answer takes

local url, path = arg[1], arg[2]
local kind = arg[3] or "text/plain"
if not (url and path) then
  io.stderr:write("usage: internet-post URL FILE [content-type]\n")
  os.exit(2)
end

local input = assert(io.open(path))
local body = input:read("a")
input:close()

local response, reply = http.request(url, {
  method = "POST",
  headers = {["Content-Type"] = kind, Accept = "text/plain"},
  body = body,
  follow = true,
})
if not response then
  io.stderr:write("internet-post: ", reply, "\n")
  os.exit(1)
end

while true do
  local piece, why = reply:read(PIECE)
  if not piece then
    io.stderr:write("internet-post: ", why, "\n")
    os.exit(1)
  elseif piece == "" then
    break
  end
  io.write(piece)
end
reply:close()
io.stderr:write(response.url, ": ", response.status, " ", response.message, "\n")
os.exit(response.status < 300 and 0 or 1)

Download internet-post.lua

Keep the access log

Through the card’s file, every request any program makes goes into a log: lua internet-log.lua &.

-- Keep the internet card's access log in a file: every request any program makes through it
-- (wget, curl, your own), as it ends. Run it in the background with &.
--
-- The card's file, /dev/internet0, is its access log, a line a request as a web server's:
-- "GET https://example.com/ 200 143ms 1256" (the method, the URL, the status or "-" and why it
-- failed, how long it took, the bytes it brought). The card keeps only its last 8, so a program
-- that wants them all reads as they come. sys.poll waits (IN) until a line is there, so the
-- program uses no time between requests.
--
-- usage: lua internet-log.lua [log]
local sys = require("sys")

local CARD = "/dev/internet0"
local LOG = arg[1] or "/home/internet.log"
local READ_SIZE = 4096 -- the most one read takes

local card = assert(sys.open(CARD))
local watch = {[card] = {IN = true}} -- IN: a line of the log is there to read
local pending = "" -- what has been read of a line not yet ended
while true do
  sys.poll(watch) -- blocks until a request ends
  pending = pending .. assert(sys.read(card, READ_SIZE))
  for line in pending:gmatch("([^\n]*)\n") do
    local log = assert(io.open(LOG, "a"))
    log:write(line, "\n")
    log:close() -- (on the disk at once)
  end
  pending = pending:match("[^\n]*$")
end

Download internet-log.lua