#include #include #include #include #include #include #include #include #include #define ARENA_IMPL #define STR_IMPL #include "dynarr.h" #include "arena.h" #include "str.h" /* utils */ Str str_fmt(Arena *a, const char *fmt, ...) { va_list ap; va_start(ap, fmt); int n = vsnprintf(NULL, 0, fmt, ap); va_end(ap); va_start(ap, fmt); char *s = new_arr(a, char, n); vsprintf(s, fmt, ap); va_end(ap); return (Str) { s, n }; } Str uint_to_str(unsigned src, Arena *a) { Str r = { 0 }; do { str_catc(&r, (src % 10) + '0', a); src /= 10; } while(src); for (int i = 0, j = r.n - 1; i < j; i++, j--) { char t = r.s[i]; r.s[i] = r.s[j]; r.s[j] = t; } return r; } /* address cache */ #define ADDR_MAP_MAX 128 #define ADDR_HOST_MAX 128 typedef struct { char host[ADDR_HOST_MAX]; int hostn, port; struct addrinfo *ai; } AddrMap; static AddrMap addr_mapv[ADDR_MAP_MAX]; static int addr_mapc = 0; struct addrinfo * addr_get(Str host, int port, Arena *scratch) { if (host.n > ADDR_HOST_MAX) { fprintf(stderr, "hostname too long\n"); return NULL; } /* TODO: maybe move fetched addr to front of table? */ for (int i = 0; i < addr_mapc; i++) { AddrMap *m = &addr_mapv[i]; if (m->port != port) continue; if (str_eql((Str) { m->host, m->hostn }, host)) return m->ai; } puts("lookup"); struct addrinfo *ai; int e = getaddrinfo(str_to_cstr(host, scratch), str_to_cstr(uint_to_str(port, scratch), scratch), &(struct addrinfo) { .ai_family = AF_INET, .ai_socktype = SOCK_STREAM, .ai_protocol = IPPROTO_TCP, .ai_flags = AI_NUMERICSERV }, &ai); if (e) { fprintf(stderr, "bad address: %s\n", gai_strerror(e)); return NULL; } if (addr_mapc == ADDR_MAP_MAX) { freeaddrinfo(addr_mapv[0].ai); for (int i = 1; i < ADDR_MAP_MAX; i++) addr_mapv[i-1] = addr_mapv[i]; addr_mapc--; } AddrMap *m = &addr_mapv[addr_mapc++]; memcpy(m->host, host.s, host.n); m->hostn = host.n; m->port = port; m->ai = ai; return ai; } void addr_init(void) { /* do nothing */ } void addr_fini(void) { for (int i = 0; i < addr_mapc; i++) freeaddrinfo(addr_mapv[i].ai); } int addr_conn(Str host, int port, Arena *scratch) { struct addrinfo *ai = addr_get(host, port, scratch); if (!ai) return -1; int s = socket(AF_INET, SOCK_STREAM, 0); if (s == -1) { fprintf(stderr, "couldn't create socket\n"); return -1; } if (connect(s, ai->ai_addr, ai->ai_addrlen) < 0) { fprintf(stderr, "couldn't connect to host\n"); close(s); return -1; } return s; } /* url parsing */ #define PROT_DEFAULT PROT_GOPHER typedef enum { PROT_UNKNOWN, PROT_GOPHER, PROT_FILE, } Proto; typedef enum { GI_TEXT, GI_MENU, GI_SEARCH, GI_UNSUPPORTED, } GopherItemType; typedef struct { Str host, path; Proto proto; int port; } Request; int parse_port(Str s) { int x = 0; for (int i = 0; i < s.n; i++) { if (s.s[i] < '0' || s.s[i] > '9') return -1; if (x >= INT_MAX / 10 - 1) return -1; x = (x * 10) + (s.s[i] - '0'); } return x; } int parse_request(Str url, Request *req) { int i; static struct { Str s; Proto p; int port; } prot_tbl[] = { { Ss("gopher"), PROT_GOPHER, 70 }, { Ss("file"), PROT_FILE, 0 }, }; /* protocol */ req->proto = PROT_UNKNOWN; Cut cprot = str_cuts(url, S("://")); if (cprot.tail.n > 0) { for (i = 0; i < sizeof prot_tbl / sizeof *prot_tbl; i++) { if (str_eql(cprot.head, prot_tbl[i].s)) { req->proto = prot_tbl[i].p; req->port = prot_tbl[i].port; } } url = cprot.tail; switch (req->proto) { case PROT_UNKNOWN: fprintf(stderr, "bad protocol\n"); return -1; case PROT_FILE: req->path = url; return 0; default: break; } } else { req->proto = PROT_DEFAULT; } /* host & port */ Cut chost = str_cut(url, '/'); Cut cport = str_cut(chost.head, ':'); req->path = chost.tail; req->host = cport.head; if (cport.tail.n > 0) { req->port = parse_port(cport.tail); if (req->port == -1) { fprintf(stderr, "bad port\n"); return -1; } } return 0; } /* documents */ typedef enum { DOC_TEXT, DOC_GOPHERMAP, DOC_ERROR, DOC_UNKNOWN } DocType; typedef struct DocLine { struct DocLine *next, *prev; Str s; int link; } DocLine; typedef DYNARR(Str) DocLinks; typedef struct { Arena arena; DocType type; Str src; DocLine *head, *tail; DocLinks links; } Doc; /* document parsing */ DocLine * doc_push_line(Doc *d, Str s) { DocLine *n = new(&d->arena, DocLine); n->s = s; n->prev = d->tail; if (d->tail) d->tail->next = n; if (!d->head) d->head = n; d->tail = n; return n; } void doc_link(Doc *d, DocLine *l, Str url) { DA_APUSH(&d->links, &d->arena, url); l->link = d->links.n; } int parse_linewise(Doc *d, int (*fn)(Doc *, Str)) { Str s = d->src; while (s.n > 0) { Cut c = str_cut(s, '\n'); if (fn(d, str_trim(c.head))) return -1; s = c.tail; } return 0; } int parse_text_line(Doc *d, Str s) { doc_push_line(d, s); return 0; } int parse_gophermap_line(Doc *d, Str s) { if (!s.n) { doc_push_line(d, s); return 0; } if (str_eql(s, S("."))) return 0; char item = s.s[0]; s = str_skip(s, 1); Cut cdsp = str_cut(s, '\t'); Str dsp = cdsp.head; s = cdsp.tail; Cut csel = str_cut(s, '\t'); Str sel = csel.head; s = csel.tail; Cut chost = str_cut(s, '\t'); Str host = chost.head; int port = parse_port(chost.tail); if (port == -1) { fprintf(stderr, "bad port '%.*s'\n", (int)chost.tail.n, chost.tail.s); return -1; } DocLine *l = doc_push_line(d, dsp); if (item != 'i') { Str url = str_fmt(&d->arena, "=> gopher://%.*s:%d/%.*s", (int)host.n, host.s, port, (int)sel.n, sel.s); doc_link(d, l, url); } return 0; } int parse_doc(Doc *d) { switch (d->type) { case DOC_TEXT: return parse_linewise(d, parse_text_line); case DOC_GOPHERMAP: return parse_linewise(d, parse_gophermap_line); case DOC_ERROR: doc_push_line(d, S("Error")); return -1; default: break; } doc_push_line(d, S("Unknown document type")); return -1; } /* document fetching */ ssize_t write_str(int fd, Str s) { return write(fd, s.s, s.n); } Str read_all(int fd, Arena *a) { DYNARR(char) r = { 0 }; for (;;) { DA_AFIT(&r, a, r.n + 1024); ssize_t n = read(fd, r.v + r.n, r.c_ - r.n); if (n <= 0) break; r.n += n; } return (Str) { r.v, r.n }; } DocType fetch_gopher(Str *buf, Request req, Arena *perm, Arena *scratch) { DocType t = DOC_UNKNOWN; if (req.path.n == 0) { t = DOC_GOPHERMAP; } else { switch (req.path.s[0]) { case '0': t = DOC_TEXT; break; case '1': t = DOC_GOPHERMAP; break; default: /* don't bother fetching */ *buf = S(""); return DOC_UNKNOWN; } req.path = str_skip(req.path, 1); } int s = addr_conn(req.host, req.port, scratch); if (s == -1) return DOC_ERROR; write_str(s, req.path); write_str(s, S("\r\n")); *buf = read_all(s, perm); close(s); return t; } DocType fetch_file(Str *buf, Request req, Arena *perm, Arena *scratch) { int f = open(str_to_cstr(req.path, scratch), O_RDONLY); if (f == -1) return DOC_ERROR; *buf = read_all(f, perm); close(f); return DOC_TEXT; } DocType fetch(Str *buf, Request req, Arena *perm, Arena *scratch) { switch (req.proto) { case PROT_GOPHER: return fetch_gopher(buf, req, perm, scratch); case PROT_FILE: return fetch_file(buf, req, perm, scratch); default: *buf = S(""); fprintf(stderr, "unknown type to fetch file\n"); return DOC_ERROR; } } void term_size(int *rows, int *cols) { struct winsize sz = { 0 }; ioctl(STDOUT_FILENO, TIOCGWINSZ, &sz); if (rows) *rows = sz.ws_row; if (cols) *cols = sz.ws_col; } int main(void) { Arena scratch = { 0 }; Request req = { 0 }; addr_init(); printf("parse_request() -> %d\n", parse_request(S("gopher://tilde.town/1~wrmr"), &req)); printf("host = %.*s\n", (int)req.host.n, req.host.s); printf("path = %.*s\n", (int)req.path.n, req.path.s); printf("prot = %d\n", req.proto); printf("port = %d\n", req.port); Str buf = { 0 }; DocType t = fetch(&buf, req, &scratch, &scratch); printf("fetch() -> %d\n", t); Doc doc = { .arena = scratch, .src = buf, .type = t }; printf("parse() -> %d\n", parse_doc(&doc)); DocLine *l = doc.head; for (;;) { int ln; term_size(&ln, NULL); for (int i = 0; i < ln - 1; i++) { if (l->link > 0) printf("[%d]", l->link); printf("\t%.*s\n", (int)l->s.n, l->s.s); if (!l->next) break; l = l->next; } char buf[1024] = { 0 }; if (l->next) printf("MORE"); printf("* "); fflush(stdout); fgets(buf, 1023, stdin); if (buf[0] == 'q') break; if (buf[0] == 'b') { for (int i = 0; l->prev && i < 2 * (ln - 1); i++) l = l->prev; } } puts("Goodbye!"); addr_fini(); return 0; }