You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
713 lines
11 KiB
713 lines
11 KiB
/* See LICENSE file for copyright and license details. */
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <netdb.h>
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#if defined(__OpenBSD__) || defined(__NetBSD__) || defined(__DragonFly__) || \
|
|
defined(___FreeBSD__)
|
|
#include <sys/ioctl.h>
|
|
#else
|
|
#include <stropts.h>
|
|
#endif
|
|
#include <unistd.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 parent = 1;
|
|
|
|
void
|
|
die(const char *fmt, ...)
|
|
{
|
|
va_list arg;
|
|
|
|
va_start(arg, fmt);
|
|
vfprintf(stderr, fmt, arg);
|
|
va_end(arg);
|
|
fputc('\n', stderr);
|
|
|
|
exit(1);
|
|
}
|
|
|
|
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[i]);
|
|
}
|
|
free(items);
|
|
clear(&item->dat);
|
|
}
|
|
|
|
if (parent && (tag = item->tag) &&
|
|
!strncmp(tag, "/tmp/sacc/img-", 14) && strlen(tag) == 20)
|
|
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
|
|
displaytextitem(Item *item)
|
|
{
|
|
FILE *pagerin;
|
|
int pid, wpid, status;
|
|
|
|
uicleanup();
|
|
switch (pid = fork()) {
|
|
case -1:
|
|
fprintf(stderr, "Couldn't fork.\n");
|
|
return;
|
|
case 0:
|
|
parent = 0;
|
|
pagerin = popen("$PAGER", "we");
|
|
fputs(item->raw, pagerin);
|
|
status = pclose(pagerin);
|
|
fputs("[ Press Enter to continue ]", stdout);
|
|
fflush(stdout);
|
|
getchar();
|
|
exit(status);
|
|
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 Item *
|
|
molditem(char **raw)
|
|
{
|
|
Item *item;
|
|
char *next;
|
|
|
|
if (!*raw)
|
|
return NULL;
|
|
|
|
item = xcalloc(sizeof(Item));
|
|
|
|
if ((next = invaliditem(*raw))) {
|
|
item->username = *raw;
|
|
*raw = next;
|
|
return item;
|
|
}
|
|
|
|
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;
|
|
|
|
return item;
|
|
}
|
|
|
|
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)
|
|
return NULL;
|
|
|
|
dir = xmalloc(sizeof(Dir));
|
|
items = xreallocarray(items, nitems, sizeof(Item*));
|
|
|
|
for (i = 0; i < nitems; ++i)
|
|
items[i] = molditem(&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);
|
|
|
|
if (n < 0)
|
|
die("Can't read socket: %s", strerror(errno));
|
|
|
|
*buf = '\0';
|
|
|
|
return raw;
|
|
}
|
|
|
|
static void
|
|
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)
|
|
die("Can't send message: %s", strerror(errno));
|
|
}
|
|
|
|
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))
|
|
die("Can't resolve hostname “%s”: %s", host, gai_strerror(r));
|
|
|
|
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)
|
|
die("Can't open socket: %s", strerror(errno));
|
|
if (r < 0)
|
|
die("Can't connect to: %s:%s: %s", host, port, strerror(errno));
|
|
|
|
freeaddrinfo(addrs);
|
|
|
|
return sock;
|
|
}
|
|
|
|
static int
|
|
download(Item *item, int dest)
|
|
{
|
|
char buf[BUFSIZ];
|
|
ssize_t r, w;
|
|
int sock;
|
|
|
|
sock = connectto(item->host, item->port);
|
|
sendselector(sock, item->selector);
|
|
|
|
while ((r = read(sock, buf, BUFSIZ)) > 0) {
|
|
while ((w = write(dest, buf, r)) > 0)
|
|
r -= w;
|
|
}
|
|
|
|
if (r < 0 || w < 0) {
|
|
printf("Error downloading file %s: %s\n",
|
|
item->selector, strerror(errno));
|
|
errno = 0;
|
|
}
|
|
|
|
close(sock);
|
|
|
|
return (r == 0 && w == 0);
|
|
}
|
|
|
|
static void
|
|
downloaditem(Item *item)
|
|
{
|
|
char *file, *path;
|
|
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[1])
|
|
path[strlen(path)-1] = '\0';
|
|
else
|
|
path = xstrdup(file);
|
|
|
|
if ((dest = open(path, O_WRONLY|O_CREAT|O_EXCL, mode)) < 0) {
|
|
printf("Can't open destination file %s: %s\n",
|
|
path, strerror(errno));
|
|
errno = 0;
|
|
goto cleanup;
|
|
}
|
|
|
|
if (!download(item, dest))
|
|
goto cleanup;
|
|
|
|
item->tag = path;
|
|
return;
|
|
cleanup:
|
|
free(path);
|
|
return;
|
|
}
|
|
|
|
static int
|
|
fetchitem(Item *item)
|
|
{
|
|
int sock;
|
|
|
|
sock = connectto(item->host, item->port);
|
|
sendselector(sock, item->selector);
|
|
item->raw = getrawitem(sock);
|
|
close(sock);
|
|
|
|
if (!*item->raw) {
|
|
fputs("Empty response from server\n", stderr);
|
|
free(item->raw);
|
|
item->raw = NULL;
|
|
return 0;
|
|
}
|
|
|
|
return (item->raw != NULL);
|
|
}
|
|
|
|
static char *
|
|
searchselector(Item *item)
|
|
{
|
|
char *input, *selector = NULL;
|
|
size_t n;
|
|
|
|
if (input = uiprompt("Enter search string: ")) {
|
|
selector = item->selector;
|
|
n = strlen(selector) + 1 + strlen(input);
|
|
item->selector = xmalloc(n);
|
|
snprintf(item->selector, n, "%s\t%s", selector, input);
|
|
free(input);
|
|
}
|
|
|
|
return selector;
|
|
}
|
|
|
|
static void
|
|
plumb(char *url)
|
|
{
|
|
switch (fork()) {
|
|
case -1:
|
|
fprintf(stderr, "Couldn't fork.\n");
|
|
return;
|
|
case 0:
|
|
parent = 0;
|
|
if (execlp(plumber, plumber, url, NULL) < 0)
|
|
die("execlp: %s", strerror(errno));
|
|
}
|
|
}
|
|
|
|
static void
|
|
plumbitem(Item *item)
|
|
{
|
|
char *file, *path;
|
|
mode_t mode = S_IRUSR|S_IWUSR|S_IRGRP;
|
|
int dest;
|
|
|
|
if (!item->tag) {
|
|
if (file = strrchr(item->selector, '/'))
|
|
++file;
|
|
else
|
|
file = item->selector;
|
|
|
|
path = uiprompt("Download %s to (^D cancel, <empty> plumb): ",
|
|
file);
|
|
if (!path)
|
|
return;
|
|
if (path[1]) {
|
|
path[strlen(path)-1] = '\0';
|
|
dest = open(path, O_WRONLY|O_CREAT|O_EXCL, mode);
|
|
if (dest < 0) {
|
|
printf("Can't open destination file %s: %s\n",
|
|
path, strerror(errno));
|
|
errno = 0;
|
|
goto cleanup;
|
|
}
|
|
} else {
|
|
path = xstrdup("/tmp/sacc/img-XXXXXX");
|
|
|
|
if ((dest = mkstemp(path)) < 0)
|
|
die("mkstemp: %s: %s", path, strerror(errno));
|
|
}
|
|
|
|
if (!download(item, dest))
|
|
goto cleanup;
|
|
|
|
item->tag = path;
|
|
}
|
|
|
|
plumb(item->tag);
|
|
return;
|
|
|
|
cleanup:
|
|
free(path);
|
|
return;
|
|
}
|
|
|
|
static int
|
|
dig(Item *entry, Item *item)
|
|
{
|
|
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))) {
|
|
fputs("Couldn't parse dir item\n", stderr);
|
|
return 0;
|
|
}
|
|
break;
|
|
case '4':
|
|
case '5':
|
|
case '6':
|
|
case '9':
|
|
downloaditem(item);
|
|
return 0;
|
|
case 'g':
|
|
case 'I':
|
|
plumbitem(item);
|
|
return 0;
|
|
default:
|
|
fprintf(stderr, "Type %c (%s) not supported\n",
|
|
item->type, typedisplay(item->type));
|
|
return 0;
|
|
}
|
|
|
|
return item->type;
|
|
}
|
|
|
|
static void
|
|
delve(Item *hole)
|
|
{
|
|
Item *entry = NULL;
|
|
char *selector;
|
|
|
|
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 (selector = searchselector(hole)) {
|
|
clear(&hole->raw);
|
|
if (dig(entry, hole) && hole->dat)
|
|
entry = hole;
|
|
free(hole->selector);
|
|
hole->selector = selector;
|
|
}
|
|
break;
|
|
case '4':
|
|
case '5':
|
|
case '6': /* TODO decode? */
|
|
case '9':
|
|
case 'g':
|
|
case 'I':
|
|
dig(entry, hole);
|
|
break;
|
|
case 0:
|
|
fprintf(stderr, "Couldn't get %s:%s/%c%s\n", hole->host,
|
|
hole->port, hole->type, hole->selector);
|
|
}
|
|
|
|
if (!entry)
|
|
return;
|
|
|
|
do {
|
|
display(entry);
|
|
hole = selectitem(entry);
|
|
} while (hole == entry);
|
|
}
|
|
}
|
|
|
|
static Item *
|
|
moldentry(char *url)
|
|
{
|
|
Item *entry;
|
|
char *p, *host = url, *port = "gopher", *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';
|
|
gopherpath = p+1;
|
|
parsed = 1;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (*host == '\0' || *port == '\0' || ipv6)
|
|
die("Can't parse url");
|
|
|
|
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("/tmp/sacc");
|
|
free(mainentry);
|
|
free(mainurl);
|
|
uicleanup();
|
|
}
|
|
|
|
static void
|
|
setup(void)
|
|
{
|
|
setenv("PAGER", "more", 0);
|
|
atexit(cleanup);
|
|
if (mkdir("/tmp/sacc", S_IRWXU) < 0 && errno != EEXIST)
|
|
die("mkdir: %s: %s", "/tmp/sacc", errno);
|
|
uisetup();
|
|
}
|
|
|
|
int
|
|
main(int argc, char *argv[])
|
|
{
|
|
if (argc != 2)
|
|
usage();
|
|
|
|
setup();
|
|
|
|
mainurl = xstrdup(argv[1]);
|
|
|
|
mainentry = moldentry(mainurl);
|
|
delve(mainentry);
|
|
|
|
exit(0);
|
|
}
|
|
|