Initial implementation.

This commit is contained in:
Solderpunk 2019-10-29 19:45:36 +02:00
parent 65c546e3ea
commit 11f9d440bf
3 changed files with 166 additions and 2 deletions

View File

@ -1,4 +1,4 @@
Copyright (c) <year> <owner> . All rights reserved.
Copyright (c) <2019> <solderpunk@sdf.org> . All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

View File

@ -1,3 +1,44 @@
# gemini-demo-3
Minimal but usable interactive Gemini client in almost < 100 LOC of Go
Minimal but usable interactive Gemini client in not quite < 100 LOC
of Go.
## Rationale
One of the original design criteria for the Gemini protocol was that
"a basic but usable (not ultra-spartan) client should fit comfortably
within 50 or so lines of code in a modern high-level language.
Certainly not more than 100". This client was written to gauge how
close to (or far from!) that goal the initial rough specification is.
## Capabilities
This crude but functional client:
* Has a minimal interactive interface for "Gemini maps"
* Will print plain text in UTF-8
* Will NOT prompt for user input
* Will NOT follow redirects
* Will report errors
* Does NOT DO ANY validation of TLS certificates
Non-text files are not yet handled.
This is less functional than the Python demo (gemini-demo-1) or Lua
demo (gemini-demo-2). Further, it's the only one of these demo
clients which is not currently below 100 lines. This may be as much
due to my inexperience with Go as with the language's unavoidable
verbosity. Improvements welcome.
## Usage
Run the client and you'll get a prompt. Type a Gemini URL (the scheme
is implied, so simply entering e.g. `gemini.conman.org` will work) to
visit a Gemini location.
If a Gemini menu is visited, you'll see numeric indices for links, ala
VF-1 or AV-98. Type a number to visit that link.
There is very crude history: you can type `b` to go "back".
Type `q` to quit.

123
gemini-demo.go Normal file
View File

@ -0,0 +1,123 @@
package main
import (
"bufio"
"crypto/tls"
"fmt"
"io/ioutil"
"net/url"
"os"
"strconv"
"strings"
)
func main() {
stdinReader := bufio.NewReader(os.Stdin)
var u string // URL
links := make([]string, 0, 100)
history := make([]string, 0, 100)
for {
fmt.Print("> ")
cmd, _ := stdinReader.ReadString('\n')
cmd = strings.TrimSpace(cmd)
// Command dispatch
switch strings.ToLower(cmd) {
case "": // Nothing
continue
case "q": // Quit
fmt.Println("Bye!")
os.Exit(0)
case "b": // Back
if len(history) < 2 {
fmt.Println("No history yet!")
continue
}
u = history[len(history)-2]
history = history[0 : len(history)-2]
default:
index, err := strconv.Atoi(cmd)
if err != nil {
// Treat this as a URL
u = cmd
if !strings.HasPrefix(u, "gemini://") {
u = "gemini://" + u
}
} else {
// Treat this as a menu lookup
u = links[index-1]
}
}
// Parse URL
parsed, err := url.Parse(u)
if err != nil {
fmt.Println("Error parsing URL!")
continue
}
// Connect to server
conn, err := tls.Dial("tcp", parsed.Host+":1965", &tls.Config{InsecureSkipVerify: true})
if err != nil {
fmt.Println("Failed to connect: " + err.Error())
continue
}
defer conn.Close()
// Send request
conn.Write([]byte(u + "\r\n"))
// Receive and parse response header
reader := bufio.NewReader(conn)
responseHeader, err := reader.ReadString('\n')
parts := strings.Fields(responseHeader)
status, err := strconv.Atoi(parts[0][0:1])
meta := parts[1]
// Switch on status code
switch status {
case 1, 3, 6:
// No input, redirects or client certs
fmt.Println("Unsupported feature!")
case 2:
// Successful transaction
// text/* content only
if !strings.HasPrefix(meta, "text/") {
fmt.Println("Unsupported type " + meta)
continue
}
// Read everything
bodyBytes, err := ioutil.ReadAll(reader)
if err != nil {
fmt.Println("Error reading body")
continue
}
body := string(bodyBytes)
if meta == "text/gemini" {
// Handle Gemini map
links = make([]string, 0, 100)
for _, line := range strings.Split(body, "\n") {
if strings.HasPrefix(line, "=>") {
line = line[2:]
bits := strings.Fields(line)
parsedLink, err := url.Parse(bits[0])
if err != nil {
continue
}
link := parsed.ResolveReference(parsedLink).String()
var label string
if len(bits) == 1 {
label = link
} else {
label = strings.Join(bits[1:], " ")
}
links = append(links, link)
fmt.Printf("[%d] %s\n", len(links), label)
} else {
fmt.Println(line)
}
}
} else {
// Just print any other kind of text
fmt.Print(body)
}
history = append(history, u)
case 4, 5:
fmt.Println("ERROR: " + meta)
}
}
}