userland: 268 applets, an editor, names that resolve, and a web server
Four complaints, and only one of them was the thing it looked like. **"toybox is too small."** Half true, and the fix cost no licensing at all: 236 of toybox's applets are what defconfig builds, and the rest live in toys/pending/ -- awk, bc, diff, more, expr, tr, hexdump, stty among them. Same 0BSD tarball, switched off by a config default. 26 of them are on now, 236 -> 268. Not everything: strace wants ptrace, syslogd wants BSD's facilitynames[], git wants OpenSSL, and those three break the link. The toybox build also works again on this machine. Its scripts start #!/bin/bash, which does not exist on NixOS, so the binary in the tree was one nobody could reproduce; the shebangs are rewritten to /usr/bin/env bash on the way in. Pending applets are enabled by editing .config directly, because `make oldconfig` is the obvious way and undoes it -- every pending symbol is `default n`. **BusyBox was asked for and is not here.** It is GPL-2.0-only, and `make ramroot` links the root image *into the kernel binary*, so it would put GPLv2 inside an Apache-2.0 executable -- which is the exact case ports/Makefile's NOTPORTS filter and docs/licensing.md §4 exist for. nano is the same answer one version later. Both recorded in licensing.md with what was done instead, since "no" without an alternative is how a rule gets quietly broken later. **An editor that is not vi.** kilo, BSD-2-Clause, one 1300-line file, no dependencies beyond termios and TIOCGWINSZ, both of which zsh already needed. Installed as /bin/edit as well as /bin/kilo, because somebody with a file to fix will type `edit`. **"wget doesn't work."** Not a userland problem, and BusyBox's wget would have failed identically: nothing on this system could turn a name into an address, so every program taking a hostname failed in its own way and each looked like a separate bug. There is still no DNS client in SchOS and there does not need to be -- musl has a complete resolver and it works over ordinary UDP sockets. The one missing fact was which nameserver, which the kernel has had all along from DHCP and never published. /proc/resolv.conf publishes it in resolv.conf's exact syntax; cmd/resolvconf decides whether it becomes /etc/resolv.conf, and will not overwrite a hand-written one without -f. # wget -O - http://example.com/ | head -1 <!doctype html><html lang="en"><head><title>Example Domain</title> **Something to host with.** cmd/httpd: GET and HEAD, static files, index.html for a directory and a listing otherwise, a child per connection. No CGI, no keep-alive, no TLS, no config file. The path check is the only security-relevant code in it and it refuses rather than normalises -- no component may be `..`, and percent-decoding happens first, because %2e%2e is `..` and a check before decoding does not see it. Both forms answer 403. And the disk is 100 MB, from 32. It is sparse, so the image is 21 MB on the host. RAMDISK_BLOCKS went 6000 -> 8000 with it: the ramdisk was at 86% and the next thing added would have broken `make ramroot`, which nobody would have noticed until they used it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
This commit is contained in:
parent
bc39b73969
commit
b08432b2c3
17 changed files with 994 additions and 21 deletions
460
cmd/httpd/main.c
Normal file
460
cmd/httpd/main.c
Normal file
|
|
@ -0,0 +1,460 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// httpd -- serve a directory over HTTP.
|
||||
//
|
||||
// SchOS could fetch a page before it could serve one, which is a strange shape
|
||||
// for a system with a TCP stack, a filesystem and no web server. This is the
|
||||
// other half.
|
||||
//
|
||||
// httpd /srv on port 80, anywhere
|
||||
// httpd -r /tmp -p 8080 somewhere else, some other port
|
||||
// httpd -a 127.0.0.1 loopback only
|
||||
//
|
||||
// GET and HEAD, static files, `index.html` for a directory and a listing when
|
||||
// there is none. It is deliberately a *file server* and not a web server:
|
||||
// there is no CGI, no scripting, no configuration file, no virtual hosts, no
|
||||
// keep-alive and no TLS. Every one of those is a decision somebody should make
|
||||
// on purpose, and none of them is needed to answer the question this exists to
|
||||
// answer -- whether this machine can put a file on the network.
|
||||
//
|
||||
// **The path check is the only security-relevant code here**, and it is done
|
||||
// by rejecting rather than by normalising. See resolve() below.
|
||||
|
||||
#include "types.h"
|
||||
#include "user.h"
|
||||
#include "fcntl.h"
|
||||
#include "stat.h"
|
||||
#include "socket.h"
|
||||
#include "posix.h"
|
||||
|
||||
#define DEFAULT_PORT 80
|
||||
#define DEFAULT_ROOT "/srv"
|
||||
|
||||
static char root[128] = DEFAULT_ROOT;
|
||||
|
||||
static int
|
||||
slen(const char *s)
|
||||
{
|
||||
int n = 0;
|
||||
while (s[n]) n++;
|
||||
return n;
|
||||
}
|
||||
|
||||
static void
|
||||
puts_(const char *s)
|
||||
{
|
||||
write(1, (void *)s, slen(s));
|
||||
}
|
||||
|
||||
static void
|
||||
putn(int v)
|
||||
{
|
||||
char b[12];
|
||||
int i = 0;
|
||||
if (v == 0) { write(1, "0", 1); return; }
|
||||
if (v < 0) { write(1, "-", 1); v = -v; }
|
||||
while (v > 0 && i < 11) { b[i++] = '0' + (v % 10); v /= 10; }
|
||||
while (i > 0) { i--; write(1, &b[i], 1); }
|
||||
}
|
||||
|
||||
static int
|
||||
atoi_(const char *s)
|
||||
{
|
||||
int v = 0;
|
||||
while (*s >= '0' && *s <= '9') v = v * 10 + (*s++ - '0');
|
||||
return v;
|
||||
}
|
||||
|
||||
static int
|
||||
scat(char *dst, int at, int cap, const char *src)
|
||||
{
|
||||
while (*src && at < cap - 1) dst[at++] = *src++;
|
||||
dst[at] = 0;
|
||||
return at;
|
||||
}
|
||||
|
||||
static int
|
||||
ucat(char *dst, int at, int cap, unsigned long long v)
|
||||
{
|
||||
char b[24];
|
||||
int i = 0;
|
||||
if (v == 0) b[i++] = '0';
|
||||
while (v > 0 && i < 23) { b[i++] = '0' + (int)(v % 10); v /= 10; }
|
||||
while (i > 0 && at < cap - 1) dst[at++] = b[--i];
|
||||
dst[at] = 0;
|
||||
return at;
|
||||
}
|
||||
|
||||
static unsigned int
|
||||
parse_ip(const char *s, int *ok)
|
||||
{
|
||||
unsigned int b[4] = { 0, 0, 0, 0 };
|
||||
int i;
|
||||
|
||||
*ok = 0;
|
||||
for (i = 0; i < 4; i++) {
|
||||
int any = 0;
|
||||
while (*s >= '0' && *s <= '9') { b[i] = b[i] * 10 + (*s++ - '0'); any = 1; }
|
||||
if (!any || b[i] > 255) return 0;
|
||||
if (i < 3) { if (*s != '.') return 0; s++; }
|
||||
}
|
||||
if (*s) return 0;
|
||||
*ok = 1;
|
||||
return b[0] | (b[1] << 8) | (b[2] << 16) | (b[3] << 24);
|
||||
}
|
||||
|
||||
// --- what a file is called, and what that means -----------------------------
|
||||
|
||||
static const struct { const char *ext, *type; } mime[] = {
|
||||
{ ".html", "text/html" },
|
||||
{ ".htm", "text/html" },
|
||||
{ ".css", "text/css" },
|
||||
{ ".js", "text/javascript" },
|
||||
{ ".json", "application/json" },
|
||||
{ ".txt", "text/plain" },
|
||||
{ ".md", "text/plain" },
|
||||
{ ".c", "text/plain" },
|
||||
{ ".h", "text/plain" },
|
||||
{ ".png", "image/png" },
|
||||
{ ".jpg", "image/jpeg" },
|
||||
{ ".jpeg", "image/jpeg" },
|
||||
{ ".gif", "image/gif" },
|
||||
{ ".svg", "image/svg+xml" },
|
||||
{ ".ico", "image/x-icon" },
|
||||
{ ".pdf", "application/pdf" },
|
||||
{ ".gz", "application/gzip" },
|
||||
{ ".tar", "application/x-tar" },
|
||||
};
|
||||
|
||||
// Unknown types are octet-stream rather than text/plain. A browser handed the
|
||||
// wrong text type renders a binary as mojibake; handed octet-stream it offers
|
||||
// to save it, which is the behaviour you want for the file you did not think
|
||||
// of when writing this table.
|
||||
static const char *
|
||||
type_of(const char *path)
|
||||
{
|
||||
int n = slen(path), i, j;
|
||||
|
||||
for (i = 0; i < (int)(sizeof(mime) / sizeof(mime[0])); i++) {
|
||||
int e = slen(mime[i].ext);
|
||||
if (n < e) continue;
|
||||
for (j = 0; j < e; j++) {
|
||||
char a = path[n - e + j], b = mime[i].ext[j];
|
||||
if (a >= 'A' && a <= 'Z') a += 32;
|
||||
if (a != b) break;
|
||||
}
|
||||
if (j == e) return mime[i].type;
|
||||
}
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
// --- the path ----------------------------------------------------------------
|
||||
|
||||
static int
|
||||
hexval(char c)
|
||||
{
|
||||
if (c >= '0' && c <= '9') return c - '0';
|
||||
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
||||
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Turn a request target into a path under `root`, or refuse.
|
||||
//
|
||||
// **Refusing rather than normalising** is the whole design. The tempting
|
||||
// version resolves `..` and collapses slashes and then checks the result, and
|
||||
// it is wrong in a way that is hard to see: every such normaliser has to agree
|
||||
// exactly with the kernel's, and when it does not, the difference is a file
|
||||
// outside the root. So no component may be `..`, full stop -- there is nothing
|
||||
// to get subtly wrong, and no legitimate request contains one.
|
||||
//
|
||||
// Percent-decoding happens first, because `%2e%2e` is `..` and a check that
|
||||
// runs before decoding does not see it.
|
||||
//
|
||||
// Returns 0 on success, -1 on a malformed target, -2 on one that escapes.
|
||||
static int
|
||||
resolve(const char *target, char *out, int cap)
|
||||
{
|
||||
char dec[512];
|
||||
int i = 0, n = 0, at;
|
||||
|
||||
// Decode, stopping at the query string -- there is nothing here that reads
|
||||
// one, and carrying it into a filename would look for a file named after
|
||||
// it.
|
||||
while (target[i] && target[i] != '?' && target[i] != '#' && n < (int)sizeof(dec) - 1) {
|
||||
if (target[i] == '%') {
|
||||
int h = hexval(target[i + 1]), l = h < 0 ? -1 : hexval(target[i + 2]);
|
||||
if (l < 0) return -1;
|
||||
dec[n++] = (char)((h << 4) | l);
|
||||
i += 3;
|
||||
} else {
|
||||
dec[n++] = target[i++];
|
||||
}
|
||||
}
|
||||
dec[n] = 0;
|
||||
|
||||
if (dec[0] != '/')
|
||||
return -1;
|
||||
// A NUL anywhere in the decoded target truncates the path for the kernel
|
||||
// but not for any length check above it. Nothing legitimate contains one.
|
||||
for (i = 0; i < n; i++)
|
||||
if (dec[i] == 0)
|
||||
return -1;
|
||||
|
||||
// No component may be `..`, and none may be empty-with-a-dot.
|
||||
for (i = 0; i < n; i++) {
|
||||
if (dec[i] != '/')
|
||||
continue;
|
||||
if (dec[i + 1] == '.' && dec[i + 2] == '.' &&
|
||||
(dec[i + 3] == '/' || dec[i + 3] == 0))
|
||||
return -2;
|
||||
}
|
||||
|
||||
at = scat(out, 0, cap, root);
|
||||
at = scat(out, at, cap, dec);
|
||||
if (at >= cap - 1)
|
||||
return -1;
|
||||
// Trailing slash off, so that stat() and the listing agree about the name.
|
||||
while (at > 1 && out[at - 1] == '/')
|
||||
out[--at] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// --- responses ---------------------------------------------------------------
|
||||
|
||||
static void
|
||||
head(int fd, const char *status, const char *type, long long len)
|
||||
{
|
||||
char h[256];
|
||||
int at = 0;
|
||||
|
||||
at = scat(h, at, sizeof(h), "HTTP/1.0 ");
|
||||
at = scat(h, at, sizeof(h), status);
|
||||
at = scat(h, at, sizeof(h), "\r\nServer: SchOS httpd\r\nConnection: close\r\n"
|
||||
"Content-Type: ");
|
||||
at = scat(h, at, sizeof(h), type);
|
||||
if (len >= 0) {
|
||||
at = scat(h, at, sizeof(h), "\r\nContent-Length: ");
|
||||
at = ucat(h, at, sizeof(h), (unsigned long long)len);
|
||||
}
|
||||
at = scat(h, at, sizeof(h), "\r\n\r\n");
|
||||
write(fd, h, at);
|
||||
}
|
||||
|
||||
static void
|
||||
fail(int fd, const char *status, const char *msg)
|
||||
{
|
||||
char b[256];
|
||||
int at = 0;
|
||||
|
||||
at = scat(b, at, sizeof(b), "<!doctype html><title>");
|
||||
at = scat(b, at, sizeof(b), status);
|
||||
at = scat(b, at, sizeof(b), "</title><h1>");
|
||||
at = scat(b, at, sizeof(b), status);
|
||||
at = scat(b, at, sizeof(b), "</h1><p>");
|
||||
at = scat(b, at, sizeof(b), msg);
|
||||
at = scat(b, at, sizeof(b), "\n");
|
||||
|
||||
head(fd, status, "text/html", at);
|
||||
write(fd, b, at);
|
||||
}
|
||||
|
||||
static void
|
||||
send_file(int fd, const char *path, int body)
|
||||
{
|
||||
char buf[4096];
|
||||
struct stat st;
|
||||
int in, n;
|
||||
|
||||
in = open((char *)path, O_RDONLY);
|
||||
if (in < 0) { fail(fd, "404 Not Found", "No such file."); return; }
|
||||
if (fstat(in, &st) < 0) { close(in); fail(fd, "500 Internal Server Error", "stat failed."); return; }
|
||||
|
||||
head(fd, "200 OK", type_of(path), (long long)st.size);
|
||||
if (!body) { close(in); return; }
|
||||
|
||||
while ((n = read(in, buf, sizeof(buf))) > 0)
|
||||
if (write(fd, buf, n) != n)
|
||||
break; // the client went away; not an error
|
||||
close(in);
|
||||
}
|
||||
|
||||
// A directory with no index.html. Generated straight into the socket, so a
|
||||
// directory with ten thousand entries does not need ten thousand entries'
|
||||
// worth of memory -- which means no Content-Length, which is why every
|
||||
// response here is HTTP/1.0 with Connection: close.
|
||||
static void
|
||||
send_listing(int fd, const char *path, const char *target, int body)
|
||||
{
|
||||
char dbuf[2048], line[512];
|
||||
int d, n, at;
|
||||
|
||||
d = open((char *)path, O_RDONLY);
|
||||
if (d < 0) { fail(fd, "404 Not Found", "No such directory."); return; }
|
||||
|
||||
head(fd, "200 OK", "text/html", -1);
|
||||
if (!body) { close(d); return; }
|
||||
|
||||
at = scat(line, 0, sizeof(line), "<!doctype html><meta charset=utf-8><title>");
|
||||
at = scat(line, at, sizeof(line), target);
|
||||
at = scat(line, at, sizeof(line), "</title><h1>");
|
||||
at = scat(line, at, sizeof(line), target);
|
||||
at = scat(line, at, sizeof(line), "</h1><ul>\n");
|
||||
write(fd, line, at);
|
||||
|
||||
while ((n = getdents64(d, dbuf, sizeof(dbuf))) > 0) {
|
||||
int off = 0;
|
||||
while (off < n) {
|
||||
struct dirent64 *e = (struct dirent64 *)(dbuf + off);
|
||||
const char *nm = e->d_name;
|
||||
|
||||
off += e->d_reclen;
|
||||
if (nm[0] == '.' && (nm[1] == 0 || (nm[1] == '.' && nm[2] == 0)))
|
||||
continue;
|
||||
|
||||
at = scat(line, 0, sizeof(line), "<li><a href=\"");
|
||||
at = scat(line, at, sizeof(line), nm);
|
||||
if (e->d_type == DT_DIR)
|
||||
at = scat(line, at, sizeof(line), "/");
|
||||
at = scat(line, at, sizeof(line), "\">");
|
||||
at = scat(line, at, sizeof(line), nm);
|
||||
if (e->d_type == DT_DIR)
|
||||
at = scat(line, at, sizeof(line), "/");
|
||||
at = scat(line, at, sizeof(line), "</a>\n");
|
||||
write(fd, line, at);
|
||||
}
|
||||
}
|
||||
write(fd, "</ul>\n", 6);
|
||||
close(d);
|
||||
}
|
||||
|
||||
// --- one request -------------------------------------------------------------
|
||||
|
||||
static void
|
||||
serve(int fd)
|
||||
{
|
||||
char req[1024], path[640], target[512];
|
||||
struct stat st;
|
||||
int n = 0, i, body = 1;
|
||||
|
||||
// The request line is all this reads. Headers are skipped rather than
|
||||
// parsed: nothing here varies on one, and a parser for something nobody
|
||||
// consults is a parser that can only be wrong.
|
||||
while (n < (int)sizeof(req) - 1) {
|
||||
int r = read(fd, req + n, 1);
|
||||
if (r <= 0) return;
|
||||
if (req[n] == '\n') break;
|
||||
n++;
|
||||
}
|
||||
req[n] = 0;
|
||||
while (n > 0 && (req[n - 1] == '\r' || req[n - 1] == ' '))
|
||||
req[--n] = 0;
|
||||
|
||||
if (req[0] == 'G' && req[1] == 'E' && req[2] == 'T' && req[3] == ' ')
|
||||
i = 4, body = 1;
|
||||
else if (req[0] == 'H' && req[1] == 'E' && req[2] == 'A' && req[3] == 'D' && req[4] == ' ')
|
||||
i = 5, body = 0;
|
||||
else {
|
||||
fail(fd, "501 Not Implemented", "GET and HEAD only.");
|
||||
return;
|
||||
}
|
||||
|
||||
{ int j = 0;
|
||||
while (req[i] && req[i] != ' ' && j < (int)sizeof(target) - 1)
|
||||
target[j++] = req[i++];
|
||||
target[j] = 0; }
|
||||
|
||||
switch (resolve(target, path, sizeof(path))) {
|
||||
case -1: fail(fd, "400 Bad Request", "That is not a path this serves."); return;
|
||||
case -2: fail(fd, "403 Forbidden", "The path leaves the document root."); return;
|
||||
}
|
||||
|
||||
puts_("httpd: "); puts_(body ? "GET " : "HEAD "); puts_(path); puts_("\n");
|
||||
|
||||
if (stat(path, &st) < 0) { fail(fd, "404 Not Found", "No such file."); return; }
|
||||
|
||||
if (st.type == T_DIR) {
|
||||
char idx[700];
|
||||
int at = scat(idx, 0, sizeof(idx), path);
|
||||
at = scat(idx, at, sizeof(idx), "/index.html");
|
||||
if (stat(idx, &st) == 0 && st.type != T_DIR)
|
||||
send_file(fd, idx, body);
|
||||
else
|
||||
send_listing(fd, path, target, body);
|
||||
return;
|
||||
}
|
||||
send_file(fd, path, body);
|
||||
}
|
||||
|
||||
int
|
||||
main(int argc, char *argv[])
|
||||
{
|
||||
struct sockaddr_in sa;
|
||||
unsigned int addr = 0; // 0.0.0.0
|
||||
int port = DEFAULT_PORT, lfd, i;
|
||||
|
||||
for (i = 1; i < argc; i++) {
|
||||
if (argv[i][0] != '-' || argv[i][2] != 0 || i + 1 >= argc)
|
||||
goto usage;
|
||||
switch (argv[i][1]) {
|
||||
case 'p': port = atoi_(argv[++i]); break;
|
||||
case 'r': { int at = scat(root, 0, sizeof(root), argv[++i]);
|
||||
while (at > 1 && root[at - 1] == '/') root[--at] = 0; } break;
|
||||
case 'a': { int ok; addr = parse_ip(argv[++i], &ok);
|
||||
if (!ok) { puts_("httpd: -a wants an address\n"); return 1; } } break;
|
||||
default:
|
||||
usage:
|
||||
puts_("usage: httpd [-r root] [-p port] [-a address]\n"
|
||||
" serves " DEFAULT_ROOT " on port 80, from anywhere, by default.\n");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
if (port <= 0 || port > 65535) { puts_("httpd: that is not a port\n"); return 1; }
|
||||
|
||||
{ struct stat st;
|
||||
if (stat(root, &st) < 0 || st.type != T_DIR) {
|
||||
puts_("httpd: no such directory: "); puts_(root); puts_("\n");
|
||||
return 1;
|
||||
} }
|
||||
|
||||
lfd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (lfd < 0) { puts_("httpd: no socket\n"); return 1; }
|
||||
|
||||
sa.sin_family = AF_INET;
|
||||
sa.sin_port = (unsigned short)(((port & 0xff) << 8) | ((port >> 8) & 0xff));
|
||||
sa.sin_addr = addr;
|
||||
for (i = 0; i < 8; i++) sa.sin_zero[i] = 0;
|
||||
|
||||
if (bind(lfd, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
|
||||
puts_("httpd: cannot bind port "); putn(port);
|
||||
puts_(port < 1024 ? " (ports below 1024 may be reserved)\n" : "\n");
|
||||
return 1;
|
||||
}
|
||||
if (listen(lfd, 8) < 0) { puts_("httpd: cannot listen\n"); return 1; }
|
||||
|
||||
puts_("httpd: serving "); puts_(root);
|
||||
puts_(" on port "); putn(port); puts_("\n");
|
||||
|
||||
for (;;) {
|
||||
int fd = accept(lfd, 0, 0);
|
||||
if (fd < 0)
|
||||
continue;
|
||||
|
||||
// A child per connection, so that one client that stops reading cannot
|
||||
// stop the server. The parent reaps without blocking: a zombie per
|
||||
// request would fill the process table on a busy machine, and blocking
|
||||
// to reap would undo the fork.
|
||||
int pid = fork();
|
||||
if (pid == 0) {
|
||||
close(lfd);
|
||||
serve(fd);
|
||||
close(fd);
|
||||
exit();
|
||||
}
|
||||
close(fd);
|
||||
if (pid < 0)
|
||||
puts_("httpd: cannot fork; dropped a connection\n");
|
||||
while (waitpid(-1, 0, WNOHANG) > 0)
|
||||
;
|
||||
}
|
||||
}
|
||||
136
cmd/resolvconf/main.c
Normal file
136
cmd/resolvconf/main.c
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// resolvconf -- put the nameserver where the resolver will look.
|
||||
//
|
||||
// SchOS has no DNS client, and the interesting part is that it does not need
|
||||
// one. musl contains a complete resolver: `getaddrinfo` reads
|
||||
// /etc/resolv.conf, sends a query over an ordinary UDP socket and parses the
|
||||
// reply, and every one of those pieces already worked here. The single missing
|
||||
// fact was *which* nameserver -- which arrives in a DHCP reply the kernel
|
||||
// handled and never told anyone about.
|
||||
//
|
||||
// That is what made `wget` look broken. It was not wget, and it was not the
|
||||
// core utilities: nothing on this system could turn a name into an address, so
|
||||
// every program that took a hostname failed the same way and each one looked
|
||||
// like its own separate bug.
|
||||
//
|
||||
// resolvconf wait for a nameserver, write it, exit
|
||||
// resolvconf -n write whatever there is now, do not wait
|
||||
// resolvconf -p print what the kernel has, change nothing
|
||||
//
|
||||
// The kernel publishes /proc/resolv.conf and stops there, deliberately: it
|
||||
// renders resolv.conf's exact syntax so this is a copy rather than a
|
||||
// translation, and *whether* /etc/resolv.conf should be overwritten is a
|
||||
// policy question. A machine with a hand-written one should keep it, which is
|
||||
// why -f exists and why it is not the default.
|
||||
|
||||
#include "types.h"
|
||||
#include "user.h"
|
||||
#include "fcntl.h"
|
||||
|
||||
#define SRC "/proc/resolv.conf"
|
||||
#define DST "/etc/resolv.conf"
|
||||
|
||||
// DHCP takes a few seconds on a quiet network and longer on a busy one. Two
|
||||
// minutes is past any of that and short enough that a machine with no DHCP
|
||||
// server does not hold a boot job open for ever.
|
||||
#define WAIT_SECONDS 120
|
||||
|
||||
static void
|
||||
puts_(const char *s)
|
||||
{
|
||||
int n = 0;
|
||||
while (s[n]) n++;
|
||||
write(1, (void *)s, n);
|
||||
}
|
||||
|
||||
static int
|
||||
slurp(const char *path, char *buf, int cap)
|
||||
{
|
||||
int fd = open((char *)path, O_RDONLY);
|
||||
int n, len = 0;
|
||||
|
||||
if (fd < 0)
|
||||
return -1;
|
||||
while (len < cap - 1 && (n = read(fd, buf + len, cap - 1 - len)) > 0)
|
||||
len += n;
|
||||
close(fd);
|
||||
buf[len] = 0;
|
||||
return len;
|
||||
}
|
||||
|
||||
int
|
||||
main(int argc, char *argv[])
|
||||
{
|
||||
char buf[512];
|
||||
int i, n, fd, wait = 1, force = 0, print = 0;
|
||||
|
||||
for (i = 1; i < argc; i++) {
|
||||
if (argv[i][0] != '-' || argv[i][2] != 0)
|
||||
goto usage;
|
||||
switch (argv[i][1]) {
|
||||
case 'n': wait = 0; break;
|
||||
case 'f': force = 1; break;
|
||||
case 'p': print = 1; wait = 0; break;
|
||||
default:
|
||||
usage:
|
||||
puts_("usage: resolvconf [-n] [-f] [-p]\n"
|
||||
" -n do not wait for DHCP\n"
|
||||
" -f overwrite an existing " DST "\n"
|
||||
" -p print what the kernel has and exit\n");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Wait by polling rather than by blocking on the file: /proc has no
|
||||
// readiness to report and inventing one for this would be a worse
|
||||
// interface than a loop that ends.
|
||||
for (i = 0; ; i++) {
|
||||
n = slurp(SRC, buf, sizeof(buf));
|
||||
if (n < 0) {
|
||||
puts_("resolvconf: no " SRC " -- this kernel has no network\n");
|
||||
return 1;
|
||||
}
|
||||
if (n > 0 || !wait || i >= WAIT_SECONDS)
|
||||
break;
|
||||
sleep(100); // one second, at 100 Hz
|
||||
}
|
||||
|
||||
if (print) {
|
||||
if (n > 0) write(1, buf, n);
|
||||
else puts_("resolvconf: the kernel has no nameserver yet\n");
|
||||
return n > 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
if (n == 0) {
|
||||
puts_("resolvconf: no nameserver after ");
|
||||
puts_(wait ? "two minutes" : "no wait");
|
||||
puts_(" -- is there a DHCP server?\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// An existing file is somebody's decision until told otherwise.
|
||||
if (!force) {
|
||||
char old[512];
|
||||
if (slurp(DST, old, sizeof(old)) > 0) {
|
||||
puts_("resolvconf: " DST " already exists; -f to replace it\n");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
fd = open(DST, O_WRONLY | O_CREAT | O_TRUNC);
|
||||
if (fd < 0) {
|
||||
puts_("resolvconf: cannot write " DST "\n");
|
||||
return 1;
|
||||
}
|
||||
if (write(fd, buf, n) != n) {
|
||||
puts_("resolvconf: short write to " DST "\n");
|
||||
close(fd);
|
||||
return 1;
|
||||
}
|
||||
close(fd);
|
||||
|
||||
puts_("resolvconf: ");
|
||||
write(1, buf, n);
|
||||
return 0;
|
||||
}
|
||||
Loading…
Reference in a new issue