shithub: gemnine

ref: 78425573e39c42b2624834ad832f804996aa32d4
dir: gemnine/main.c

View raw version
#include <u.h>
#include <libc.h>
#include <libsec.h>
#include <bio.h>
#include <ctype.h>
#include <plumb.h>

typedef struct Url Url;
typedef struct Response Response;

struct Url {
	char *url;
	char *server;
	char *port;
};

struct Response {
	Url *url;
	char *mime;
	char *prompt;
	int status;
	int fd;
};

#pragma varargck type "E" char*

char *
urlto(Url *url, char *u)
{
	char *e, *trail;
	int len;

	if((len = strlen(u)) < 1)
		return "";
	trail = (len > 1 && u[len-1] == '/') ? "/" : "";

	if(*u == '/'){
		if(u[1] == '/') /* no protocol */
			return smprint("gemini://%s%s", cleanname(u+2), trail);

		/* absolute url, no scheme */
		return strcmp(url->port, "1965") == 0 ?
			smprint("gemini://%s%s%s", url->server, cleanname(u), trail) :
			smprint("gemini://%s:%s%s%s", url->server, url->port, cleanname(u), trail);
	}

	/* with scheme */
	if((e = strpbrk(u, ":/")) != nil && e[0] == ':' && e[1] == '/' && e[2] == '/'){
		e[2] = 0;
		e = cleanname(e+3);
		return smprint("%s/%s%s", u, e, trail);
	}

	/* chars not allowed */
	if(strpbrk(u, ":") != nil)
		return strdup(u);

	/* relative, no scheme */
	len = strlen(url->url);
	if(url->url[len-1] == '/'){ /* easy */
		u = smprint("%s%s%s", url->url, u, trail);
	}else{
		/* replace the last element */
		if((e = strrchr(url->url, '/')) != nil && e[-1] != '/')
			len = e - url->url;
		u = smprint("%.*s/%s%s", len, url->url, u, trail);
	}
	if((e = strchr(strchr(u, ':') + 3, '/')) != nil)
		cleanname(e);
	return u;
}

Url *
parseurl(char *url)
{
	char *server, *port, *s, *e;
	Url *u;

	url = strdup(url);
	if((s = strpbrk(url, ":/")) != nil && s[0] == ':' && s[1] == '/' && s[2] == '/'){
		server = s + 3;
	}else{
		s = smprint("gemini://%s", url);
		free(url);
		url = s;
		server = s + 9;
	}

	port = strdup("1965");
	if((e = strpbrk(server, ":/")) != nil){
		s = mallocz(e-server+1, 1);
		memmove(s, server, e-server);
		server = s;
		if(*e == ':'){
			port = strdup(e+1);
			if((e = strchr(port, '/')) != nil)
				*e = 0;
		}
	}else{
		server = strdup(server);
	}

	u = calloc(1, sizeof(*u));
	u->url = url;
	u->server = server;
	u->port = port;

	return u;
}

void
freeurl(Url *u)
{
	if(u != nil){
		free(u->url);
		free(u->server);
		free(u->port);
		free(u);
	}
}

void
freeresponse(Response *r)
{
	if(r != nil){
		close(r->fd);
		freeurl(r->url);
		free(r->mime);
		free(r->prompt);
		free(r);
	}
}

Response *
request(char *url)
{
	Thumbprint *th;
	Response *r;
	char *s, buf[1024];
	TLSconn conn;
	int i, ok, len, oldfd;

	r = calloc(1, sizeof(*r));
	r->fd = -1;
	if((r->url = parseurl(url)) == nil)
		goto err;

	if((r->fd = dial(netmkaddr(r->url->server, "tcp", r->url->port), nil, nil, nil)) < 0){
		werrstr("dial: %r");
		goto err;
	}
	th = initThumbprints("/sys/lib/ssl/gemini", nil, "x509");
	memset(&conn, 0, sizeof(conn));
	conn.serverName = r->url->server;
	oldfd = r->fd;
	r->fd = tlsClient(oldfd, &conn);
	close(oldfd);
	if(r->fd < 0){
		werrstr("tls: %r");
		goto err;
	}

	/* FIXME find a way to trust on the first run */
	if(th != nil){
		ok = okCertificate(conn.cert, conn.certlen, th);
		freeThumbprints(th);
		if(!ok){
			//fprint(2, "echo 'x509 %r server=%s' >>/sys/lib/ssl/gemini\n", r->url->server);
			//werrstr("untrusted cert");
			//goto err;
		}
	}

	fprint(r->fd, "%s\r\n", r->url->url);
	for(len = 0; len < sizeof(buf)-1; len++){
		if((i = read(r->fd, buf+len, 1)) < 0){
			werrstr("read: %r");
			goto err;
		}
		if(i == 0 || buf[len] == '\n')
			break;
	}

	s = buf;
	s[len] = 0;
	for(len--; len >= 0 && (s[len] == '\r' || s[len] == '\n'); len--)
		s[len] = 0;
	if(s[0] < '0' || s[0] > '9' || s[1] < '0' || s[1] > '9'){
		werrstr("invalid status");
		goto err;
	}
	r->status = 10*(int)(s[0]-'0') + s[1] - '0';
	s += 2;
	while(isspace(*s))
		s++;

	if(r->status >= 10 && r->status < 20){ /* input */
		r->prompt = strdup(s);
	}else if(r->status >= 20 && r->status < 30){ /* success */
		r->mime = strdup(s[0] ? s : "text/gemini");
	}else if(r->status >= 30 && r->status < 40){ /* redirect */
		s = urlto(r->url, s);
		freeresponse(r);
		r = request(s);
		free(s);
	}else if(r->status >= 40 && r->status < 50){
		werrstr("temporary failure: %s", s);
		goto err;
	}else if(r->status >= 50 && r->status < 60){
		werrstr("permanent failure: %s", s);
		goto err;
	}else if(r->status >= 60 && r->status < 70){
		werrstr("client cert required: %s", s);
		goto err;
	}

	return r;

err:
	if(r != nil && r->url != nil)
		werrstr("%q: %r", r->url->url);
	freeresponse(r);
	return nil;
}

int
Efmt(Fmt *f)
{
	char *s;

	s = va_arg(f->args, char*);
	for(; *s; s++){
		if(*s == '%' && isxdigit(s[1]) && isxdigit(s[2])){
			fmtprint(f, "%%%c%c", toupper(s[1]), toupper(s[2]));
			s += 2;
		}else if(isalnum(*s) || strchr(".-_~!$&'()*,;=/:@ \n", *s) == nil){
			fmtprint(f, "%c", *s);
		}else{
			fmtprint(f, "%%%.2X", *s & 0xff);
		}
	}
	return 0;
}

char *
readall(int fd)
{
	char *s;
	int n, sz, bufsz;

	bufsz = 1023;
	s = nil;
	for(sz = 0;; sz += n){
		if(bufsz-sz < 1024){
			bufsz *= 2;
			s = realloc(s, bufsz);
		}
		if((n = read(fd, s+sz, bufsz-sz-1)) < 1)
			break;
	}
	s[sz] = 0;
	if(sz > 1 && s[sz-1] == '\n')
		s[sz-1] = 0;

	return s;
}

void
page(Response *r)
{
	if(rfork(RFPROC|RFFDG|RFNOTEG|RFNOWAIT) == 0){
		char tmp[32] = "/tmp/gem9XXXXXXXXXXX", *cmd;
		mktemp(tmp);
		cmd = smprint("cat >%s >[2]/dev/null; page -w %s; rm %s", tmp, tmp, tmp);
		dup(r->fd, 0); close(r->fd);
		execl("/bin/rc", "rc", "-c", cmd, nil);
	}
}

void
play(Response *r)
{
	int wfd;
	char *wsys, tmp[64];

	if(rfork(RFPROC|RFFDG|RFNOTEG|RFNOWAIT) == 0){
		snprint(tmp, sizeof(tmp), "new -pid %d -dx %d -dy %d", getpid(), 640, 480);
		if ((wsys = getenv("wsys")) == nil)
			exits("no wsys");
		if ((wfd = open(wsys, ORDWR)) < 0 ||
		    mount(wfd, -1, "/mnt/wsys", MREPL, tmp) < 0 ||
		    bind("/mnt/wsys", "/dev", MBEFORE) < 0){
			exits("wsys: %r");
		}
		dup(r->fd, 0); close(r->fd);
		execl("/bin/play", "play", nil);
	}
}

void
main(int argc, char **argv)
{
	Response *r;
	char *s, *t, *u, *url;
	int len, wait, pl, fd;
	Plumbmsg *m;
	Biobuf out, body;

	wait = 0;
	ARGBEGIN{
	case 'w':
		wait = 1;
		break;
	}ARGEND;

	if(!wait && argc < 1){
		fprint(2, "usage: gemnine [-w] [URL]\n");
		exits("usage");
	}

	fmtinstall('E', Efmt);
	quotefmtinstall();
	Binit(&out, 1, OWRITE);
	pl = -1;

nexturl:
	url = nil;
	if(wait){
		if(pl >= 0 || (pl = plumbopen("gemini", OREAD)) >= 0){
			if((m = plumbrecv(pl)) != nil){
				url = strdup(m->data);
				plumbfree(m);
			}else{
				exits(nil);
			}
		}else{
			sysfatal("plumbopen: %r");
		}
	}else{
		url = strdup(argv[0]);
	}

nextreq:
	if((r = request(url)) != nil){
		if(r->mime != nil && strncmp(r->mime, "text/", 5) != 0){
			if(strncmp(r->mime, "image/", 6) == 0 || strcmp(r->mime, "application/pdf") == 0)
				page(r);
			else if(strncmp(r->mime, "audio/", 6) == 0)
				play(r);
			else
				fprint(2, "unsupported MIME %q\n", r->mime);
		}else if(r->prompt != nil){
			if(wait)
				close(open("/dev/text", OWRITE|OTRUNC));
			if((fd = open("/dev/consctl", OWRITE)) >= 0){
				write(fd, "holdon", 6);
				print("%s\n", r->prompt);
				s = readall(0);
				free(url);
				url = smprint("%s?%E", r->url->url, s);
				free(s);
				freeresponse(r);
				close(fd);
				goto nextreq;
			}else{
				fprint(2, "%r\n");
			}
		}else{
			if(wait)
				close(open("/dev/text", OWRITE|OTRUNC));
			Binit(&body, r->fd, OREAD);
			while((s = Brdstr(&body, '\n', 1)) != nil){
				if((len = Blinelen(&body)) > 0)
					s[len] = 0;
				for(len--; len >= 0 && (s[len] == '\r' || s[len] == '\n'); len--)
					s[len] = 0;
				if(s[0] == '=' && s[1] == '>'){
					u = s + 2;
					while(isspace(*u))
						u++;
					if((t = strpbrk(u, " \t")) != nil)
						*t++ = 0;
					else
						t = "";
					u = urlto(r->url, u);
					Bprint(&out, "→ %s %s\n", u, t);
					free(u);
				}else{
					Bprint(&out, "%s\n", s);
				}
				free(s);
			}
		}
		freeresponse(r);
	}else{
		fprint(2, "%r\n");
		if(!wait)
			exits("failed");
	}

	Bflush(&out);
	if(wait)
		goto nexturl;

	exits(nil);
}