ref: 291e2955f505e3167a725e8b00371e4ef597ecef
author: Sigrid Haflínudóttir <ftrvxmtrx@gmail.com>
date: Sun May 17 09:07:27 EDT 2020
a very basic client
--- /dev/null
+++ b/LICENSE
@@ -1,0 +1,1 @@
+Public domain.
--- /dev/null
+++ b/README.md
@@ -1,0 +1,11 @@
+# gemnine
+
+WIP.
+
+Gemini browser for Plan 9.
+
+WTH is Gemini??? See https://gemini.circumlunar.space/
+
+As of now some basics are implemented to handle errors and print the
+body to stdout, the plan is to add a separate GUI program to display
+the pages in a better way.
--- /dev/null
+++ b/main.c
@@ -1,0 +1,201 @@
+#include <u.h>
+#include <libc.h>
+#include <libsec.h>
+#include <bio.h>
+#include <ctype.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;
+ Biobuf body;
+ int fd;
+};
+
+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;
+ TLSconn conn;
+ int 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)
+ 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)
+ 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);
+ Binit(&r->body, r->fd, OREAD);
+ if((s = Brdstr(&r->body, '\n', 1)) == nil){
+ werrstr("EOF");
+ goto err;
+ }
+ if((len = Blinelen(&r->body)) > 0)
+ s[len-1] = 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 */
+ freeresponse(r);
+ r = request(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:
+ freeresponse(r);
+ return nil;
+}
+
+void
+main(int argc, char **argv)
+{
+ Response *r;
+ char *s;
+ int len;
+
+ if(argc < 2){
+ fprint(2, "usage: gemnine URL\n");
+ exits("usage");
+ }
+
+ quotefmtinstall();
+
+ if((r = request(argv[1])) != nil){
+ if(r->mime != nil && strncmp(r->mime, "text/", 5) != 0){
+ /* FIXME handle in a better way */
+ if(r->mime != nil)
+ fprint(2, "MIME %s\n", r->mime);
+ }else if(r->prompt != nil){
+ /* FIXME no idea */
+ fprint(2, "INPUT %s\n", r->prompt);
+ }else{
+ while((s = Brdstr(&r->body, '\n', 1)) != nil){
+ if((len = Blinelen(&r->body)) > 0)
+ s[len] = 0;
+ print("%s\n", s);
+ free(s);
+ }
+ }
+ freeresponse(r);
+ }else{
+ fprint(2, "%s: %r\n", argv[1]);
+ exits("failed");
+ }
+
+ exits(nil);
+}
--- /dev/null
+++ b/mkfile
@@ -1,0 +1,17 @@
+</$objtype/mkfile
+
+TARG=gemnine
+
+BIN=/$objtype/bin
+
+OFILES=\
+ main.$O\
+
+UPDATE=\
+ $HFILES\
+ ${OFILES:%.$O=%.c}\
+ mkfile\
+
+default:V: all
+
+</sys/src/cmd/mkone