Initial implementation.
This commit is contained in:
parent
65c546e3ea
commit
11f9d440bf
2
LICENSE
2
LICENSE
|
@ -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,
|
Redistribution and use in source and binary forms, with or without modification,
|
||||||
are permitted provided that the following conditions are met:
|
are permitted provided that the following conditions are met:
|
||||||
|
|
43
README.md
43
README.md
|
@ -1,3 +1,44 @@
|
||||||
# gemini-demo-3
|
# 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.
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue