/* See LICENSE file for copyright and license details. */
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <locale.h>
#include <netdb.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <wchar.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "common.h"
#include "config.h"
static char *mainurl;
static Item *mainentry;
static int devnullfd;
static int parent = 1;
static int interactive;
void
die(const char *fmt, ...)
{
va_list arg;
va_start(arg, fmt);
vfprintf(stderr, fmt, arg);
va_end(arg);
fputc('\n', stderr);
exit(1);
}
#ifndef asprintf
int
asprintf(char **s, const char *fmt, ...)
{
va_list ap;
int n;
va_start(ap, fmt);
n = vsnprintf(NULL, 0, fmt, ap);
va_end(ap);
if (n == INT_MAX || !(*s = malloc(++n)))
return -1;
va_start(ap, fmt);
vsnprintf(*s, n, fmt, ap);
va_end(ap);
return n;
}
#endif /* asprintf */
/* print `len' columns of characters. */
size_t
mbsprint(const char *s, size_t len)
{
wchar_t wc;
size_t col = 0, i, slen;
int rl, w;
if (!len)
return col;
slen = strlen(s);
for (i = 0; i < slen; i += rl) {
if ((rl = mbtowc(&wc, s + i, slen - i < 4 ? slen - i : 4)) <= 0)
break;
if ((w = wcwidth(wc)) == -1)
continue;
if (col + w > len || (col + w == len && s[i + rl])) {
fputs("\xe2\x80\xa6", stdout);
col++;
break;
}
fwrite(s + i, 1, rl, stdout);
col += w;
}
return col;
}
static void *
xreallocarray(void *m, const size_t n, const size_t s)
{
void *nm;
if (n == 0 || s == 0) {
free(m);
return NULL;
}
if (s && n > (size_t)-1/s)
die("realloc: overflow");
if (!(nm = realloc(m, n * s)))
die("realloc: %s", strerror(errno));
return nm;
}
static void *
xmalloc(const size_t n)
{
void *m = malloc(n);
if (!m)
die("malloc: %s\n", strerror(errno));
return m;
}
static void *
xcalloc(size_t n)
{
char *m = xmalloc(n);
while (n)
m[--n] = 0;
return m;
}
static char *
xstrdup(const char *str)
{
char *s;
if (!(s = strdup(str)))
die("strdup: %s\n", strerror(errno));
return s;
}
static void
usage(void)
{
die("usage: sacc URL");
}
static void
clearitem(Item *item)
{
Dir *dir;
Item *items;
char *tag;
size_t i;
if (!item)
return;
if (dir = item->dat) {
items = dir->items;
for (i = 0; i < dir->nitems; ++i)
clearitem(&items[i]);
free(items);
clear(&item->dat);
}
if (parent && (tag = item->tag) &&
!strncmp(tag, tmpdir, strlen(tmpdir)))
unlink(tag);
clear(&item->tag);
clear(&item->raw);
}
const char *
typedisplay(char t)
{
switch (t) {
case '0':
return "Text+";
case '1':
return "Dir +";
case '2':
return "CSO |";
case '3':
return "Err |";
case '4':
return "Macf+";
case '5':
return "DOSf+";
case '6':
return "UUEf+";
case '7':
return "Find+";
case '8':
return "Tlnt|";
case '9':
return "Binf+";
case '+':
return "Mirr+";
case 'T':
return "IBMt|";
case 'g':
return "GIF +";
case 'I':
return "Img +";
case 'h':
return "HTML+";
case 'i':
return " |";
case 's':
return "Snd |";
default:
return "! |";
}
}
static void
printdir(Item *item)
{
Dir *dir;
Item *items;
size_t i, nitems;
if (!item || !(dir = item->dat))
return;
items = dir->items;
nitems = dir->nitems;
for (i = 0; i < nitems; ++i) {
printf("%s%s\n",
typedisplay(items[i].type), items[i].username);
}
}
static void
displaytextitem(Item *item)
{
FILE *pagerin;
int pid, wpid;
uicleanup();
switch (pid = fork()) {
case -1:
uistatus("Couldn't fork.");
return;
case 0:
parent = 0;
pagerin = popen("$PAGER", "we");
fputs(item->raw, pagerin);
exit(pclose(pagerin));
default:
while ((wpid = wait(NULL)) >= 0 && wpid != pid)
;
}
uisetup();
}
static char *
pickfield(char **raw, char sep)
{
char *c, *f = *raw;
for (c = *raw; *c && *c != sep; ++c)
;
*c = '\0';
*raw = c+1;
return f;
}
static char *
invaliditem(char *raw)
{
char c;
int tabs;
for (tabs = 0; (c = *raw) && c != '\n'; ++raw) {
if (c == '\t')
++tabs;
}
if (c)
*raw++ = '\0';
return (tabs == 3) ? NULL : raw;
}
static void
molditem(Item *item, char **raw)
{
char *next;
if (!*raw)
return;
if ((next = invaliditem(*raw))) {
item->username = *raw;
*raw = next;
return;
}
item->type = *raw[0]++;
item->username = pickfield(raw, '\t');
item->selector = pickfield(raw, '\t');
item->host = pickfield(raw, '\t');
item->port = pickfield(raw, '\r');
if (!*raw[0])
++*raw;
}
static Dir *
molddiritem(char *raw)
{
Item *items = NULL;
char *s, *nl, *p;
Dir *dir;
size_t i, nitems;
for (s = nl = raw, nitems = 0; p = strchr(nl, '\n'); ++nitems) {
s = nl;
nl = p+1;
}
if (!strcmp(s, ".\r\n") || !strcmp(s, ".\n"))
--nitems;
if (!nitems) {
uistatus("Couldn't parse dir item");
return NULL;
}
dir = xmalloc(sizeof(Dir));
items = xreallocarray(items, nitems, sizeof(Item));
memset(items, 0, nitems * sizeof(Item));
for (i = 0; i < nitems; ++i)
molditem(&items[i], &raw);
dir->items = items;
dir->nitems = nitems;
dir->printoff = dir->curline = 0;
return dir;
}
static char *
getrawitem(int sock)
{
char *raw, *buf;
size_t bn, bs;
ssize_t n;
raw = buf = NULL;
bn = bs = n = 0;
do {
bs -= n;
buf += n;
if (bs < 1) {
raw = xreallocarray(raw, ++bn, BUFSIZ);
buf = raw + (bn-1) * BUFSIZ;
bs = BUFSIZ;
}
} while ((n = read(sock, buf, bs)) > 0);
*buf = '\0';
if (n < 0) {
uistatus("Can't read socket: %s", strerror(errno));
clear(&raw);
}
return raw;
}
static int
sendselector(int sock, const char *selector)
{
char *msg, *p;
size_t ln;
ssize_t n;
ln = strlen(selector) + 3;
msg = p = xmalloc(ln);
snprintf(msg, ln--, "%s\r\n", selector);
while ((n = write(sock, p, ln)) != -1 && n != 0) {
ln -= n;
p += n;
}
free(msg);
if (n == -1)
uistatus("Can't send message: %s", strerror(errno));
return n;
}
static int
connectto(const char *host, const char *port)
{
static const struct addrinfo hints = {
.ai_family = AF_UNSPEC,
.ai_socktype = SOCK_STREAM,
.ai_protocol = IPPROTO_TCP,
};
struct addrinfo *addrs, *addr;
int sock, r;
if (r = getaddrinfo(host, port, &hints, &addrs)) {
uistatus("Can't resolve hostname \"%s\": %s",
host, gai_strerror(r));
return -1;
}
for (addr = addrs; addr; addr = addr->ai_next) {
if ((sock = socket(addr->ai_family, addr->ai_socktype,
addr->ai_protocol)) < 0)
continue;
if ((r = connect(sock, addr->ai_addr, addr->ai_addrlen)) < 0) {
close(sock);
continue;
}
break;
}
if (sock < 0) {
uistatus("Can't open socket: %s", strerror(errno));
return -1;
}
if (r < 0) {
uistatus("Can't connect to: %s:%s: %s",
host, port, strerror(errno));
return -1;
}
freeaddrinfo(addrs);
return sock;
}
static int
download(Item *item, int dest)
{
char buf[BUFSIZ];
ssize_t r, w;
int src;
if (!item->tag) {
if ((src = connectto(item->host, item->port)) < 0 ||
sendselector(src, item->selector) < 0)
return 0;
} else if ((src = open(item->tag, O_RDONLY)) < 0) {
printf("Can't open source file %s: %s",
item->tag, strerror(errno));
errno = 0;
return 0;
}
while ((r = read(src, buf, BUFSIZ)) > 0) {
while ((w = write(dest, buf, r)) > 0)
r -= w;
}
if (r < 0 || w < 0) {
printf("Error downloading file %s: %s",
item->selector, strerror(errno));
errno = 0;
}
close(src);
return (r == 0 && w == 0);
}
static void
downloaditem(Item *item)
{
char *file, *path, *tag;
mode_t mode = S_IRUSR|S_IWUSR|S_IRGRP;
int dest;
if (file = strrchr(item->selector, '/'))
++file;
else
file = item->selector;
if (!(path = uiprompt("Download to [%s] (^D cancel): ", file)))
return;
if (!path[0])
path = xstrdup(file);
if (tag = item->tag) {
if (access(tag, R_OK) < 0) {
clear(&item->tag);
} else if (!strcmp(tag, path)) {
goto cleanup;
}
}
if ((dest = open(path, O_WRONLY|O_CREAT|O_EXCL, mode)) < 0) {
uistatus("Can't open destination file %s: %s",
path, strerror(errno));
errno = 0;
goto cleanup;
}
if (!download(item, dest))
goto cleanup;
if (!item->tag)
item->tag = path;
return;
cleanup:
free(path);
return;
}
static int
fetchitem(Item *item)
{
int sock;
if ((sock = connectto(item->host, item->port)) < 0 ||
sendselector(sock, item->selector) < 0)
return 0;
item->raw = getrawitem(sock);
close(sock);
if (item->raw && !*item->raw) {
uistatus("Empty response from server");
clear(&item->raw);
}
return (item->raw != NULL);
}
static void
plumb(char *url)
{
switch (fork()) {
case -1:
uistatus("Couldn't fork.");
return;
case 0:
parent = 0;
dup2(devnullfd, 1);
dup2(devnullfd, 2);
if (execlp(plumber, plumber, url, NULL) < 0)
uistatus("execlp: plumb(%s): %s", url, strerror(errno));
}
uistatus("Plumbed \"%s\"", url);
}
static void
plumbitem(Item *item)
{
char *file, *path, *tag;
mode_t mode = S_IRUSR|S_IWUSR|S_IRGRP;
int n, dest, plumbitem;
if (file = strrchr(item->selector, '/'))
++file;
else
file = item->selector;
path = uiprompt("Download %s to (^D cancel, <empty> plumb): ",
file);
if (!path)
return;
if ((tag = item->tag) && access(tag, R_OK) < 0) {
clear(&item->tag);
tag = NULL;
}
plumbitem = path[0] ? 0 : 1;
if (!path[0]) {
clear(&path);
if (!tag) {
if (asprintf(&path, "%s/%s", tmpdir, file) < 0)
die("Can't generate tmpdir path: ",
strerror(errno));
}
}
if (path && (!tag || strcmp(tag, path))) {
if ((dest = open(path, O_WRONLY|O_CREAT|O_EXCL, mode)) < 0) {
uistatus("Can't open destination file %s: %s",
path, strerror(errno));
errno = 0;
goto cleanup;
}
if (!download(item, dest) || tag)
goto cleanup;
}
if (!tag)
item->tag = path;
if (plumbitem)
plumb(item->tag);
return;
cleanup:
free(path);
return;
}
static int
dig(Item *entry, Item *item)
{
char *plumburi = NULL;
if (item->raw) /* already in cache */
return item->type;
if (!item->entry)
item->entry = entry ? entry : item;
switch (item->type) {
case 'h': /* fallthrough */
if (!strncmp(item->selector, "URL:", 4)) {
plumb(item->selector+4);
return 0;
}
case '0':
if (!fetchitem(item))
return 0;
break;
case '1':
case '+':
case '7':
if (!fetchitem(item) || !(item->dat = molddiritem(item->raw)))
return 0;
break;
case '4':
case '5':
case '6':
case '9':
downloaditem(item);
return 0;
case '8':
if (asprintf(&plumburi, "telnet://%s@%s:%s", item->selector,
item->host, item->port) < 0)
return 0;
plumb(plumburi);
free(plumburi);
return 0;
case 'g':
case 'I':
plumbitem(item);
return 0;
case 'T':
if (asprintf(&plumburi, "tn3270://%s@%s:%s", item->selector,
item->host, item->port) < 0)
return 0;
plumb(plumburi);
free(plumburi);
return 0;
default:
uistatus("Type %c (%s) not supported",
item->type, typedisplay(item->type));
return 0;
}
return item->type;
}
static char *
searchselector(Item *item)
{
char *pexp, *exp, *tag, *selector = item->selector;
size_t n = strlen(selector);
if ((tag = item->tag) && !strncmp(tag, selector, n))
pexp = tag + n+1;
else
pexp = "";
if (!(exp = uiprompt("Enter search string (^D cancel) [%s]: ", pexp)))
return NULL;
if (exp[0] && strcmp(exp, pexp)) {
n += strlen(exp) + 2;
tag = xmalloc(n);
snprintf(tag, n, "%s\t%s", selector, exp);
}
free(exp);
return tag;
}
static int
searchitem(Item *entry, Item *item)
{
char *sel, *selector;
if (!(sel = searchselector(item)))
return 0;
if (sel != item->tag) {
clearitem(item);
selector = item->selector;
item->selector = item->tag = sel;
dig(entry, item);
item->selector = selector;
}
return (item->dat != NULL);
}
static void
printout(Item *hole)
{
if (!hole)
return;
switch (hole->type) {
case '0':
if (dig(hole, hole))
fputs(hole->raw, stdout);
return;
case '1':
case '+':
if (dig(hole, hole))
printdir(hole);
default:
return;
}
}
static void
delve(Item *hole)
{
Item *entry = NULL;
while (hole) {
switch (hole->type) {
case 'h':
case '0':
if (dig(entry, hole))
displaytextitem(hole);
break;
case '1':
case '+':
if (dig(entry, hole) && hole->dat)
entry = hole;
break;
case '7':
if (searchitem(entry, hole))
entry = hole;
break;
case '4':
case '5':
case '6': /* TODO decode? */
case '8':
case '9':
case 'g':
case 'I':
case 'T':
dig(entry, hole);
break;
case 0:
uistatus("Couldn't get %s:%s/%c%s", hole->host,
hole->port, hole->type, hole->selector);
}
if (!entry)
return;
do {
uidisplay(entry);
hole = uiselectitem(entry);
} while (hole == entry);
}
}
static Item *
moldentry(char *url)
{
Item *entry;
char *p, *host = url, *port = "70", *gopherpath = "1";
int parsed, ipv6;
if (p = strstr(url, "://")) {
if (strncmp(url, "gopher", p - url))
die("Protocol not supported: %.*s", p - url, url);
host = p + 3;
}
if (*host == '[') {
ipv6 = 1;
++host;
} else {
ipv6 = 0;
}
for (parsed = 0, p = host; !parsed && *p; ++p) {
switch (*p) {
case ']':
if (ipv6) {
*p = '\0';
ipv6 = 0;
}
continue;
case ':':
if (!ipv6) {
*p = '\0';
port = p+1;
}
continue;
case '/':
*p = '\0';
parsed = 1;
continue;
}
}
if (*host == '\0' || *port == '\0' || ipv6)
die("Can't parse url");
if (*p != '\0')
gopherpath = p;
entry = xcalloc(sizeof(Item));
entry->type = gopherpath[0];
entry->username = entry->selector = ++gopherpath;
entry->host = host;
entry->port = port;
entry->entry = entry;
return entry;
}
static void
cleanup(void)
{
clearitem(mainentry);
if (parent)
rmdir(tmpdir);
free(mainentry);
free(mainurl);
if (interactive)
uicleanup();
}
static void
setup(void)
{
struct sigaction sa;
int fd;
setlocale(LC_CTYPE, "");
setenv("PAGER", "more", 0);
atexit(cleanup);
/* reopen stdin in case we're reading from a pipe */
if ((fd = open("/dev/tty", O_RDONLY)) < 0)
die("open: /dev/tty: %s", strerror(errno));
if (dup2(fd, 0) < 0)
die("dup2: /dev/tty, stdin: %s", strerror(errno));
close(fd);
if ((devnullfd = open("/dev/null", O_WRONLY)) < 0)
die("open: /dev/null: %s", strerror(errno));
if (mkdir(tmpdir, S_IRWXU) < 0 && errno != EEXIST)
die("mkdir: %s: %s", tmpdir, strerror(errno));
if(interactive = isatty(1)) {
uisetup();
sigemptyset(&sa.sa_mask);
sa.sa_handler = uisigwinch;
sa.sa_flags = SA_RESTART;
sigaction(SIGWINCH, &sa, NULL);
}
}
int
main(int argc, char *argv[])
{
if (argc != 2)
usage();
setup();
mainurl = xstrdup(argv[1]);
mainentry = moldentry(mainurl);
if (interactive)
delve(mainentry);
else
printout(mainentry);
exit(0);
}