ref: dfc5c880042c1c61b88a2f3f2eaf344e8fe88684
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(strncmp(line, "#", strlen("#")) == 0) { plrtstr(&c->text, 1000000, 8, 0, font, strdup(cleanup(base)), PL_SEL, 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 { 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){ /* 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; }