ref: eeb8ba921c8be60f7f29caba428e89e65ec66737
dir: /castor.c/
#include <u.h>
#include <libc.h>
#include <libsec.h>
#include <String.h>
#include <regexp.h>
#include <draw.h>
#include <event.h>
#include <keyboard.h>
#include <panel.h>
#include <bio.h>
#include <stdio.h>
#include <ctype.h>
#include <plumb.h>
#include "castor.h"
Panel *root;
Panel *backp;
Panel *fwdp;
Panel *entryp;
Panel *urlp;
Panel *textp;
Panel *statusp;
Panel *popup;
Url *current_base_url;
Mouse *mouse;
Hist *hist = nil;
int preformatted = 0;
enum
{
Mback,
Mforward,
Msearch,
Mexit,
};
char *menu3[] = {
"back",
"forward",
"search",
"exit",
0
};
char* replace_char(char* str, char find, char replace){
char *current_pos = strchr(str,find);
while (current_pos){
*current_pos = replace;
current_pos = strchr(current_pos,find);
}
return str;
}
char *
cleanup(char *line)
{
if(line=="" || line==NULL)
return line;
char *src, *dst;
for (src=dst=line; *src != '\0'; src++) {
*dst = *src;
if (*dst != '\r' && *dst != '\n') dst++;
}
*dst = '\0';
replace_char(line, '\t', ' ');
return line;
}
void
set_current_base_url(Url *url)
{
freeurl(current_base_url);
current_base_url = url;
}
void
show(Ctx *c)
{
plinittextview(textp, PACKE|EXPAND, ZP, c->text, texthit);
pldraw(textp, screen);
plinitlabel(urlp, PACKN|FILLX, c->url->raw);
pldraw(urlp, screen);
message("Castor9");
}
void
plumburl(Url *u)
{
int fd;
fd = plumbopen("send", OWRITE|OCEXEC);
if(fd<0)
return;
plumbsendtext(fd, "castor9", nil, nil, u->raw);
close(fd);
freeurl(u);
}
void
page(Url *u)
{
int fd;
char tmp[32] = "/tmp/castor9XXXXXXXXXXX", *cmd;
fd = request(u);
if(fd < 0)
sysfatal("dial: %r");
fprint(fd, "%s\r\n", u->raw);
switch(rfork(RFFDG|RFPROC|RFMEM|RFREND|RFNOWAIT|RFNOTEG)){
case -1:
fprint(2, "Can't fork!");
break;
case 0:
mktemp(tmp);
cmd = smprint("tail -n +2 >%s >[2]/dev/null; page -w %s; rm %s", tmp, tmp, tmp);
dup(fd, 0);
close(fd);
execl("/bin/rc", "rc", "-c", cmd, nil);
}
}
char *
protocol(char *link)
{
if(strstr(link, "http://") != nil) {
return " [WWW]";
} else if(strstr(link, "https://") != nil) {
return " [WWW]";
} else if(strstr(link, "gopher://") != nil) {
return " [GOPHER]";
} else if(strstr(link, "finger://") != nil) {
return " [FINGER]";
} else {
return "";
}
}
char *
symbol(char *link)
{
if(strstr(link, "http://") != nil) {
return "⇄";
} else if(strstr(link, "https://") != nil) {
return "⇄";
} else if(strstr(link, "gopher://") != nil) {
return "⇒";
} else if(strstr(link, "finger://") != nil) {
return "⇒";
} else {
return "→";
}
}
void
handle_status(char *status, Response *r)
{
int code;
char *meta;
code = atoi(strtok(status, " \t"));
if(code == 0)
message("STATUS: %s\n", status);
meta = strtok(NULL, "\n");
r->status = code;
r->meta = cleanup(meta);
}
void
render_text(Ctx *c, char *line)
{
char *base, *right_margin;
int length, width;
length = strlen(strdup(line));
base = strdup(line);
width = 80;
char *preformatted_marker = "```";
if(strncmp(line, preformatted_marker, strlen(preformatted_marker)) == 0){
if(preformatted==0){
preformatted=1;
}else{
preformatted=0;
}
return;
}
while(*base)
{
if(preformatted==1)
{
plrtstr(&c->text, 1000000, 8, 0, font, strdup(cleanup(base)), PL_HEAD, 0);
break;
}
if((length <= width))
{
plrtstr(&c->text, 1000000, 8, 0, font, strdup(cleanup(base)), 0, 0);
break;
}
right_margin = base + width;
while(!isspace(*right_margin))
{
right_margin--;
if(right_margin == base)
{
right_margin += width;
while(!isspace(*right_margin))
{
if(*right_margin == '\0')
break;
right_margin++;
}
}
}
*right_margin = '\0';
plrtstr(&c->text, 1000000, 8, 0, font, strdup(cleanup(base)), 0, 0);
length -= right_margin - base + 1; /* +1 for the space */
base = right_margin + 1;
}
}
void
render_link(Ctx *c, char *line)
{
char *copy = strdup(cleanup(line + 2)); /* bypass => */
char *link = strtok(copy, " ");
char *rest = strtok(NULL, "\0");
char *label;
if(rest != NULL)
{
while(isspace(*rest))
rest++;
label = smprint("%s %s%s", symbol(link), rest, protocol(link));
}else{
label = smprint("%s %s%s", symbol(link), link, protocol(link));
}
plrtstr(&c->text, 1000000, 8, 0, font, strdup(label), PL_HOT, estrdup(link));
}
Url *
base_url(Url *url)
{
char *base_url, *path, *ptr;
if(url->path == "/" || url->path == NULL){
path = "/";
}else{
path = estrdup(url->path);
ptr = strrchr(path, '/');
if(path[strlen(path)-1] != '/')
strcpy(ptr, "/");
}
base_url = smprint("gemini://%s%s", url->host, path);
return urlparse(nil, base_url);
}
int
request(Url *url)
{
Thumbprint *th;
TLSconn conn;
int fd;
char *port;
if(url->port == NULL){
port = "1965";
}else{
port = url->port;
}
char *naddr = netmkaddr(url->host, "tcp", port);
fd = dial(naddr, 0, 0, 0);
if(fd < 0){
message("unable to connect to %s:%s: %r", url->host, url->port);
return -1;
}
conn.serverName = url->host;
memset(&conn, 0, sizeof(conn));
fd = tlsClient(fd, &conn);
th = initThumbprints("/sys/lib/ssl/gemini", nil, "x509");
if(th != nil){
okCertificate(conn.cert, conn.certlen, th);
freeThumbprints(th);
free(conn.cert);
}
return fd;
}
void
gemini_get(Url *url)
{
int fd;
char *line;
Biobuf body;
Ctx *c;
c = malloc(sizeof *c);
if(c==nil)
sysfatal("malloc: %r");
c->text = nil;
Response *r;
r = malloc(sizeof *r);
if(r == nil)
sysfatal("malloc: %r");
r->url = url;
Hist *h;
h = malloc(sizeof *h);
if(h == nil)
sysfatal("malloc: %r");
plrtstr(&c->text, 1000000, 0, 0, font, strdup(" "), 0, 0);
message("loading %s...", url->raw);
fd = request(url);
fprint(fd, "%s\r\n", url->raw);
Binit(&body, fd, OREAD);
char *status = Brdstr(&body, '\n', '0');
handle_status(status, r);
if(r->status == 20){
c->url = url;
set_current_base_url(base_url(url));
if(r->meta != NULL && strncmp(r->meta, "text/", strlen("text/")) != 0)
{
Bflush(&body);
close(fd);
page(url);
message("Castor9");
}else{
while((line = Brdstr(&body, '\n', 0)) != nil)
{
if(strncmp(line, "=>", strlen("=>")) == 0)
{
render_link(c, line);
}else{
render_text(c, line);
}
free(line);
}
Bflush(&body);
close(fd);
h->p = hist;
h->n = nil;
h->c = c;
hist = h;
show(c);
}
} else if(r->status == 31) {
Url *redirect = urlparse(nil, r->meta);
gemini_get(redirect);
}
}
void
backhit(Panel *p, int b)
{
USED(p);
if(b!=1)
return;
if(hist==nil || hist->p==nil)
return;
hist->p->n = hist;
hist = hist->p;
set_current_base_url(base_url(hist->c->url));
show(hist->c);
}
void
nexthit(Panel *p, int b)
{
USED(p);
if(b!=1)
return;
if(hist==nil || hist->n==nil)
return;
hist = hist->n;
set_current_base_url(base_url(hist->c->url));
show(hist->c);
}
void
menuhit(int button, int item)
{
USED(button);
switch(item){
case Mback:
backhit(backp, 1);
break;
case Mforward:
nexthit(fwdp, 1);
break;
case Msearch:
//search();
break;
case Mexit:
exits(nil);
break;
}
}
void
entryhit(Panel *p, char *t)
{
USED(p);
switch(strlen(t)){
case 0:
return;
case 1:
switch(*t){
case 'b':
//backhit(backp, 1);
break;
case 'n':
//nexthit(fwdp, 1);
break;
case 'q':
exits(nil);
break;
default:
message("unknown command %s", t);
break;
}
break;
default:
if(strstr(t, "gemini://") == NULL)
t = smprint("gemini://%s", t);
gemini_get(urlparse(nil, t));
}
plinitentry(entryp, PACKN|FILLX, 0, "", entryhit);
pldraw(root, screen);
}
void
texthit(Panel *p, int b, Rtext *rt)
{
char *n;
Url *next_url;
char *link = rt->user;
USED(p);
if(b!=1)
return;
if(link==nil)
return;
if (strstr(link, "gemini://") != nil || strstr(link, "://") != nil){
next_url = urlparse(nil, link);
} else {
/* assuming relative URL */
if(*link == '/'){
n = smprint("%s%s", urlparse(current_base_url, link)->raw, estrdup(link)+1);
}else{
n = smprint("%s%s", urlparse(current_base_url, link)->raw, estrdup(link));
}
next_url = urlparse(nil, n);
}
if(strcmp(next_url->scheme, "gemini") == 0){
gemini_get(next_url);
} else {
message("Plumbing %s", next_url->raw);
plumburl(next_url);
}
}
void
message(char *s, ...)
{
static char buf[1024];
char *out;
va_list args;
va_start(args, s);
out = buf + vsnprint(buf, sizeof(buf), s, args);
va_end(args);
*out='\0';
plinitlabel(statusp, PACKN|FILLX, buf);
pldraw(statusp, screen);
flushimage(display, 1);
}
void
mkpanels(void)
{
Panel *p, *ybar, *xbar, *m;
m = plmenu(0, 0, menu3, PACKN|FILLX, menuhit);
root = plpopup(0, EXPAND, 0, 0, m);
p = plgroup(root, PACKN|FILLX);
statusp = pllabel(p, PACKN|FILLX, "Castor!");
plplacelabel(statusp, PLACEW);
pllabel(p, PACKW, "Go: ");
entryp = plentry(p, PACKN|FILLX, 0, "", entryhit);
p = plgroup(root, PACKN|FILLX);
urlp = pllabel(p, PACKN|FILLX, "");
plplacelabel(urlp, PLACEW);
p = plgroup(root, PACKN|EXPAND);
ybar = plscrollbar(p, PACKW|USERFL);
xbar = plscrollbar(p, IGNORE);
textp = pltextview(p, PACKE|EXPAND, ZP, nil, nil);
plscroll(textp, xbar, ybar);
plgrabkb(entryp);
}
void
eresized(int new)
{
if(new && getwindow(display, Refnone)<0)
sysfatal("cannot reattach: %r");
plpack(root, screen->r);
pldraw(root, screen);
}
void scrolltext(int dy, int whence)
{
Scroll s;
s = plgetscroll(textp);
switch(whence){
case 0:
s.pos.y = dy;
break;
case 1:
s.pos.y += dy;
break;
case 2:
s.pos.y = s.size.y+dy;
break;
}
if(s.pos.y > s.size.y)
s.pos.y = s.size.y;
if(s.pos.y < 0)
s.pos.y = 0;
plsetscroll(textp, s);
/* BUG: there is a redraw issue when scrolling
This fixes the issue albeit not properly */
pldraw(textp, screen);
}
void
main(int argc, char *argv[])
{
Event e;
Url *url;
if(argc == 2)
if(strcmp(argv[1], "gemini://") != 0){
url = urlparse(nil, smprint("gemini://%s", argv[1]));
}else{
url = urlparse(nil, argv[1]);
}
else
url = urlparse(nil, "gemini://gemini.circumlunar.space/capcom/");
quotefmtinstall();
if(initdraw(nil, nil, "gemini")<0)
sysfatal("initdraw: %r");
einit(Emouse|Ekeyboard);
plinit(screen->depth);
mkpanels();
gemini_get(url);
eresized(0);
for(;;){
switch(event(&e)){
case Ekeyboard:
switch(e.kbdc){
default:
plgrabkb(entryp);
plkeyboard(e.kbdc);
break;
case Khome:
scrolltext(0, 0);
break;
case Kup:
scrolltext(-textp->size.y/4, 1);
break;
case Kpgup:
scrolltext(-textp->size.y/2, 1);
break;
case Kdown:
scrolltext(textp->size.y/4, 1);
break;
case Kpgdown:
scrolltext(textp->size.y/2, 1);
break;
case Kend:
scrolltext(-textp->size.y, 2);
break;
case Kdel:
exits(nil);
break;
}
break;
case Emouse:
mouse = &e.mouse;
if(mouse->buttons & (8|16) && ptinrect(mouse->xy, textp->r)){
if(mouse->buttons & 8)
scrolltext(textp->r.min.y - mouse->xy.y, 1);
else
scrolltext(mouse->xy.y - textp->r.min.y, 1);
break;
}
plmouse(root, mouse);
/* BUG: there is a redraw issue when scrolling
This fixes the issue albeit not properly */
//pldraw(textp, screen);
break;
}
}
}
// //////////////////////////
enum {
Domlen = 256,
};
static char reserved[] = "%:/?#[]@!$&'()*+,;=";
static int
dhex(char c)
{
if('0' <= c && c <= '9')
return c-'0';
if('a' <= c && c <= 'f')
return c-'a'+10;
if('A' <= c && c <= 'F')
return c-'A'+10;
return 0;
}
static char*
unescape(char *s, char *spec)
{
char *r, *w;
uchar x;
if(s == nil)
return s;
for(r=w=s; x = *r; r++){
if(x == '%' && isxdigit(r[1]) && isxdigit(r[2])){
x = (dhex(r[1])<<4)|dhex(r[2]);
if(spec && strchr(spec, x)){
*w++ = '%';
*w++ = toupper(r[1]);
*w++ = toupper(r[2]);
}
else
*w++ = x;
r += 2;
continue;
}
*w++ = x;
}
*w = 0;
return s;
}
int
Efmt(Fmt *f)
{
char *s, *spec;
Str2 s2;
s2 = va_arg(f->args, Str2);
s = s2.s1;
spec = s2.s2;
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(".-_~!$&'()*,;=", *s) || strchr(spec, *s))
fmtprint(f, "%c", *s);
else
fmtprint(f, "%%%.2X", *s & 0xff);
return 0;
}
int
Nfmt(Fmt *f)
{
char d[Domlen], *s;
s = va_arg(f->args, char*);
if(utf2idn(s, d, sizeof(d)) >= 0)
s = d;
fmtprint(f, "%s", s);
return 0;
}
int
Mfmt(Fmt *f)
{
char *s = va_arg(f->args, char*);
fmtprint(f, (*s != '[' && strchr(s, ':') != nil)? "[%s]" : "%s", s);
return 0;
}
int
Ufmt(Fmt *f)
{
char *s;
Url *u;
if((u = va_arg(f->args, Url*)) == nil)
return fmtprint(f, "nil");
if(u->scheme)
fmtprint(f, "%s:", u->scheme);
if(u->user || u->host)
fmtprint(f, "//");
if(u->user){
fmtprint(f, "%E", (Str2){u->user, ""});
if(u->pass)
fmtprint(f, ":%E", (Str2){u->pass, ""});
fmtprint(f, "@");
}
if(u->host){
fmtprint(f, "%]", u->host);
if(u->port)
fmtprint(f, ":%s", u->port);
}
if(s = Upath(u))
fmtprint(f, "%E", (Str2){s, "/:@+"});
if(u->query)
fmtprint(f, "?%E", (Str2){u->query, "/:@"});
if(u->fragment)
fmtprint(f, "#%E", (Str2){u->fragment, "/:@?+"});
return 0;
}
char*
Upath(Url *u)
{
if(u){
if(u->path)
return u->path;
if(u->user || u->host)
return "/";
}
return nil;
}
static char*
remdot(char *s)
{
char *b, *d, *p;
int dir, n;
dir = 1;
b = d = s;
if(*s == '/')
s++;
for(; s; s = p){
if(p = strchr(s, '/'))
*p++ = 0;
if(*s == '.' && ((s[1] == 0) || (s[1] == '.' && s[2] == 0))){
if(s[1] == '.')
while(d > b)
if(*--d == '/')
break;
dir = 1;
continue;
} else
dir = (p != nil);
if((n = strlen(s)) > 0)
memmove(d+1, s, n);
*d++ = '/';
d += n;
}
if(dir)
*d++ = '/';
*d = 0;
return b;
}
static char*
abspath(char *s, char *b)
{
char *x, *a;
if(b && *b){
if(s == nil || *s == 0)
return estrdup(b);
if(*s != '/' && (x = strrchr(b, '/'))){
a = emalloc((x - b) + strlen(s) + 4);
sprint(a, "%.*s/%s", utfnlen(b, x - b), b, s);
return remdot(a);
}
}
if(s && *s){
if(*s != '/')
return estrdup(s);
a = emalloc(strlen(s) + 4);
sprint(a, "%s", s);
return remdot(a);
}
return nil;
}
static void
pstrdup(char **p)
{
if(p == nil || *p == nil)
return;
if(**p == 0){
*p = nil;
return;
}
*p = estrdup(*p);
}
static char*
mklowcase(char *s)
{
char *cp;
Rune r;
if(s == nil)
return s;
cp = s;
while(*cp != 0){
chartorune(&r, cp);
r = tolowerrune(r);
cp += runetochar(cp, &r);
}
return s;
}
static Url *
saneurl(Url *u)
{
if(u == nil || u->scheme == nil || u->host == nil || Upath(u) == nil){
freeurl(u);
return nil;
}
if(u->port){
print("PORT: %s\n", u->port);
/* remove default ports */
switch(atoi(u->port)){
case 21: if(!strcmp(u->scheme, "ftp")) goto Defport; break;
case 70: if(!strcmp(u->scheme, "gopher")) goto Defport; break;
case 80: if(!strcmp(u->scheme, "http")) goto Defport; break;
case 443: if(!strcmp(u->scheme, "https")) goto Defport; break;
case 1965: if(!strcmp(u->scheme, "gemini")) goto Defport; break;
default: if(!strcmp(u->scheme, u->port)) goto Defport; break;
Defport:
free(u->port);
u->port = nil;
}
}
return u;
}
Url*
urlparse(Url *b, char *s)
{
char *t, *p, *x, *y;
Url *u;
if(s == nil)
s = "";
t = nil;
s = p = estrdup(s);
u = emalloc(sizeof(*u));
u->raw = estrdup(s);
for(; *p; p++){
if(*p == ':'){
if(p == s)
break;
*p++ = 0;
u->scheme = s;
b = nil;
goto Abs;
}
if(!isalpha(*p))
if((p == s) || ((!isdigit(*p) && strchr("+-.", *p) == nil)))
break;
}
p = s;
if(b){
switch(*p){
case 0:
memmove(u, b, sizeof(*u));
goto Out;
case '#':
memmove(u, b, sizeof(*u));
u->fragment = p+1;
goto Out;
case '?':
memmove(u, b, sizeof(*u));
u->fragment = u->query = nil;
break;
case '/':
if(p[1] == '/'){
u->scheme = b->scheme;
b = nil;
break;
}
default:
memmove(u, b, sizeof(*u));
u->fragment = u->query = u->path = nil;
break;
}
}
Abs:
if(x = strchr(p, '#')){
*x = 0;
u->fragment = x+1;
}
if(x = strchr(p, '?')){
*x = 0;
u->query = x+1;
}
if(p[0] == '/' && p[1] == '/'){
p += 2;
if(x = strchr(p, '/')){
u->path = t = abspath(x, Upath(b));
*x = 0;
}
if(x = strchr(p, '@')){
*x = 0;
if(y = strchr(p, ':')){
*y = 0;
u->pass = y+1;
}
u->user = p;
p = x+1;
}
if((x = strrchr(p, ']')) == nil)
x = p;
if(x = strrchr(x, ':')){
*x = 0;
u->port = x+1;
}
if(x = strchr(p, '[')){
p = x+1;
if(y = strchr(p, ']'))
*y = 0;
}
u->host = p;
} else {
u->path = t = abspath(p, Upath(b));
}
Out:
pstrdup(&u->scheme);
pstrdup(&u->user);
pstrdup(&u->pass);
pstrdup(&u->host);
pstrdup(&u->port);
pstrdup(&u->path);
pstrdup(&u->query);
pstrdup(&u->fragment);
free(s);
free(t);
/* the + character encodes space only in query part */
if(s = u->query)
while(s = strchr(s, '+'))
*s++ = ' ';
if(s = u->host){
t = emalloc(Domlen);
if(idn2utf(s, t, Domlen) >= 0){
u->host = estrdup(t);
free(s);
}
free(t);
}
unescape(u->user, nil);
unescape(u->pass, nil);
unescape(u->path, reserved);
unescape(u->query, reserved);
unescape(u->fragment, reserved);
mklowcase(u->scheme);
mklowcase(u->host);
mklowcase(u->port);
if((u = saneurl(u)) != nil)
u->full = smprint("%U", u);
return u;
}
int
matchurl(Url *u, Url *s)
{
if(u){
char *a, *b;
if(s == nil)
return 0;
if(u->scheme && (s->scheme == nil || strcmp(u->scheme, s->scheme)))
return 0;
if(u->user && (s->user == nil || strcmp(u->user, s->user)))
return 0;
if(u->host && (s->host == nil || strcmp(u->host, s->host)))
return 0;
if(u->port && (s->port == nil || strcmp(u->port, s->port)))
return 0;
if(a = Upath(u)){
b = Upath(s);
if(b == nil || strncmp(a, b, strlen(a)))
return 0;
}
}
return 1;
}
void
freeurl(Url *u)
{
if(u == nil)
return;
free(u->full);
free(u->scheme);
free(u->user);
free(u->pass);
free(u->host);
free(u->port);
free(u->path);
free(u->query);
free(u->fragment);
free(u->raw);
free(u);
}
// ///////////////
void *
emalloc(int n)
{
void *v;
if((v = malloc(n)) == nil) {
fprint(2, "out of memory allocating %d\n", n);
sysfatal("mem");
}
setmalloctag(v, getcallerpc(&n));
memset(v, 0, n);
return v;
}
char *
estrdup(char *s)
{
char *t;
if((t = strdup(s)) == nil) {
fprint(2, "out of memory in strdup(%.10s)\n", s);
sysfatal("mem");
}
setmalloctag(t, getcallerpc(&t));
return t;
}