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)
- Reading gives the next line; with none waiting, a read waits for one.
cat /dev/internet0shows requests as they end (Ctrl+C stops it). The card keeps only the last 8 lines, so a reader sees what is current, and they go when the computer stops. - Writing is refused: opening it for writing fails with
EACCES. - poll:
INwhile a line of the log waits;PRIwhile a request has news (its response came, a piece of its body, its end or its failure), until the program next asks the card or reads its file. A program waiting for a request polls forPRI, then asks again.
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"))
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)
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)
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