Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d78052ce08 | ||
|
|
1f2888c54a | ||
|
|
41d5f8d31b | ||
|
|
24026422b2 | ||
|
|
5e977250ec | ||
|
|
d8c5da1c7c | ||
|
|
d01d50ff1a | ||
|
|
3ed39e62d8 | ||
|
|
f2921a396f | ||
|
|
efef44c2f9 | ||
|
|
c8626bae17 |
@@ -1,6 +1,6 @@
|
||||
# go-gemini
|
||||
|
||||
[](https://godoc.org/git.sr.ht/~adnano/go-gemini)
|
||||
[](https://godocs.io/git.sr.ht/~adnano/go-gemini)
|
||||
|
||||
Package gemini implements the [Gemini protocol](https://gemini.circumlunar.space) in Go.
|
||||
|
||||
|
||||
15
client.go
15
client.go
@@ -6,6 +6,7 @@ import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -74,12 +75,22 @@ func (c *Client) Do(req *Request) (*Response, error) {
|
||||
conn := tls.Client(netConn, config)
|
||||
// Set connection deadline
|
||||
if c.Timeout != 0 {
|
||||
conn.SetDeadline(time.Now().Add(c.Timeout))
|
||||
err := conn.SetDeadline(time.Now().Add(c.Timeout))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"failed to set connection deadline: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Write the request
|
||||
w := bufio.NewWriter(conn)
|
||||
req.Write(w)
|
||||
|
||||
err = req.Write(w)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"failed to write request data: %w", err)
|
||||
}
|
||||
|
||||
if err := w.Flush(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
50
doc.go
Normal file
50
doc.go
Normal file
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
Package gemini implements the Gemini protocol.
|
||||
|
||||
Client is a Gemini client.
|
||||
|
||||
client := &gemini.Client{}
|
||||
resp, err := client.Get("gemini://example.com")
|
||||
if err != nil {
|
||||
// handle error
|
||||
}
|
||||
if resp.Body != nil {
|
||||
defer resp.Body.Close()
|
||||
// ...
|
||||
}
|
||||
// ...
|
||||
|
||||
Server is a Gemini server.
|
||||
|
||||
server := &gemini.Server{
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
Servers should be configured with certificates:
|
||||
|
||||
err := server.Certificates.Load("/var/lib/gemini/certs")
|
||||
if err != nil {
|
||||
// handle error
|
||||
}
|
||||
|
||||
Servers can accept requests for multiple hosts and schemes:
|
||||
|
||||
server.RegisterFunc("example.com", func(w *gemini.ResponseWriter, r *gemini.Request) {
|
||||
fmt.Fprint(w, "Welcome to example.com")
|
||||
})
|
||||
server.RegisterFunc("example.org", func(w *gemini.ResponseWriter, r *gemini.Request) {
|
||||
fmt.Fprint(w, "Welcome to example.org")
|
||||
})
|
||||
server.RegisterFunc("http://example.net", func(w *gemini.ResponseWriter, r *gemini.Request) {
|
||||
fmt.Fprint(w, "Proxied content from http://example.net")
|
||||
})
|
||||
|
||||
To start the server, call ListenAndServe:
|
||||
|
||||
err := server.ListenAndServe()
|
||||
if err != nil {
|
||||
// handle error
|
||||
}
|
||||
*/
|
||||
package gemini
|
||||
@@ -17,11 +17,12 @@ import (
|
||||
"time"
|
||||
|
||||
"git.sr.ht/~adnano/go-gemini"
|
||||
"git.sr.ht/~adnano/go-gemini/tofu"
|
||||
"git.sr.ht/~adnano/go-xdg"
|
||||
)
|
||||
|
||||
var (
|
||||
hosts gemini.KnownHostsFile
|
||||
hosts tofu.KnownHostsFile
|
||||
scanner *bufio.Scanner
|
||||
)
|
||||
|
||||
@@ -46,7 +47,7 @@ Otherwise, this should be safe to trust.
|
||||
=> `
|
||||
|
||||
func trustCertificate(hostname string, cert *x509.Certificate) error {
|
||||
fingerprint := gemini.NewFingerprint(cert.Raw, cert.NotAfter)
|
||||
fingerprint := tofu.NewFingerprint(cert.Raw, cert.NotAfter)
|
||||
knownHost, ok := hosts.Lookup(hostname)
|
||||
if ok && time.Now().Before(knownHost.Expires) {
|
||||
// Check fingerprint
|
||||
@@ -146,7 +147,7 @@ func main() {
|
||||
}
|
||||
fmt.Print(string(body))
|
||||
} else {
|
||||
fmt.Printf("%d %s: %s\n", resp.Status, resp.Status.Message(), resp.Meta)
|
||||
fmt.Printf("%d %s\n", resp.Status, resp.Meta)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509/pkix"
|
||||
"fmt"
|
||||
@@ -35,10 +36,35 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// stream writes an infinite stream to w.
|
||||
func stream(w *gemini.ResponseWriter, r *gemini.Request) {
|
||||
ch := make(chan string)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
go func(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
ch <- fmt.Sprint(time.Now().UTC())
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
// Close channel when finished.
|
||||
// In this example this will never be reached.
|
||||
close(ch)
|
||||
}(ctx)
|
||||
|
||||
for {
|
||||
fmt.Fprintln(w, time.Now().UTC())
|
||||
w.Flush()
|
||||
time.Sleep(time.Second)
|
||||
s, ok := <-ch
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
fmt.Fprintln(w, s)
|
||||
if err := w.Flush(); err != nil {
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
22
fs.go
22
fs.go
@@ -1,6 +1,7 @@
|
||||
package gemini
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"os"
|
||||
@@ -9,8 +10,13 @@ import (
|
||||
|
||||
func init() {
|
||||
// Add Gemini mime types
|
||||
mime.AddExtensionType(".gmi", "text/gemini")
|
||||
mime.AddExtensionType(".gemini", "text/gemini")
|
||||
if err := mime.AddExtensionType(".gmi", "text/gemini"); err != nil {
|
||||
panic(fmt.Errorf("failed to register .gmi extension mimetype: %w", err))
|
||||
}
|
||||
|
||||
if err := mime.AddExtensionType(".gemini", "text/gemini"); err != nil {
|
||||
panic(fmt.Errorf("failed to register .gemini extension mimetype: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
// FileServer takes a filesystem and returns a Responder which uses that filesystem.
|
||||
@@ -27,15 +33,15 @@ func (fsh fsHandler) Respond(w *ResponseWriter, r *Request) {
|
||||
p := path.Clean(r.URL.Path)
|
||||
f, err := fsh.Open(p)
|
||||
if err != nil {
|
||||
w.WriteStatus(StatusNotFound)
|
||||
w.Status(StatusNotFound)
|
||||
return
|
||||
}
|
||||
// Detect mimetype
|
||||
ext := path.Ext(p)
|
||||
mimetype := mime.TypeByExtension(ext)
|
||||
w.SetMediaType(mimetype)
|
||||
w.Meta(mimetype)
|
||||
// Copy file to response writer
|
||||
io.Copy(w, f)
|
||||
_, _ = io.Copy(w, f)
|
||||
}
|
||||
|
||||
// TODO: replace with io/fs.FS when available
|
||||
@@ -66,15 +72,15 @@ func (d Dir) Open(name string) (File, error) {
|
||||
func ServeFile(w *ResponseWriter, fs FS, name string) {
|
||||
f, err := fs.Open(name)
|
||||
if err != nil {
|
||||
w.WriteStatus(StatusNotFound)
|
||||
w.Status(StatusNotFound)
|
||||
return
|
||||
}
|
||||
// Detect mimetype
|
||||
ext := path.Ext(name)
|
||||
mimetype := mime.TypeByExtension(ext)
|
||||
w.SetMediaType(mimetype)
|
||||
w.Meta(mimetype)
|
||||
// Copy file to response writer
|
||||
io.Copy(w, f)
|
||||
_, _ = io.Copy(w, f)
|
||||
}
|
||||
|
||||
func openFile(p string) (File, error) {
|
||||
|
||||
49
gemini.go
49
gemini.go
@@ -1,52 +1,3 @@
|
||||
/*
|
||||
Package gemini implements the Gemini protocol.
|
||||
|
||||
Client is a Gemini client.
|
||||
|
||||
client := &gemini.Client{}
|
||||
resp, err := client.Get("gemini://example.com")
|
||||
if err != nil {
|
||||
// handle error
|
||||
}
|
||||
if resp.Status.Class() == gemini.StatusClassSucess {
|
||||
defer resp.Body.Close()
|
||||
// ...
|
||||
}
|
||||
// ...
|
||||
|
||||
Server is a Gemini server.
|
||||
|
||||
server := &gemini.Server{
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
Servers should be configured with certificates:
|
||||
|
||||
err := server.Certificates.Load("/var/lib/gemini/certs")
|
||||
if err != nil {
|
||||
// handle error
|
||||
}
|
||||
|
||||
Servers can accept requests for multiple hosts and schemes:
|
||||
|
||||
server.RegisterFunc("example.com", func(w *gemini.ResponseWriter, r *gemini.Request) {
|
||||
fmt.Fprint(w, "Welcome to example.com")
|
||||
})
|
||||
server.RegisterFunc("example.org", func(w *gemini.ResponseWriter, r *gemini.Request) {
|
||||
fmt.Fprint(w, "Welcome to example.org")
|
||||
})
|
||||
server.RegisterFunc("http://example.net", func(w *gemini.ResponseWriter, r *gemini.Request) {
|
||||
fmt.Fprint(w, "Proxied content from http://example.net")
|
||||
})
|
||||
|
||||
To start the server, call ListenAndServe:
|
||||
|
||||
err := server.ListenAndServe()
|
||||
if err != nil {
|
||||
// handle error
|
||||
}
|
||||
*/
|
||||
package gemini
|
||||
|
||||
import (
|
||||
|
||||
6
mux.go
6
mux.go
@@ -138,14 +138,14 @@ func (mux *ServeMux) Respond(w *ResponseWriter, r *Request) {
|
||||
// If the given path is /tree and its handler is not registered,
|
||||
// redirect for /tree/.
|
||||
if u, ok := mux.redirectToPathSlash(path, r.URL); ok {
|
||||
w.WriteHeader(StatusRedirect, u.String())
|
||||
w.Header(StatusRedirect, u.String())
|
||||
return
|
||||
}
|
||||
|
||||
if path != r.URL.Path {
|
||||
u := *r.URL
|
||||
u.Path = path
|
||||
w.WriteHeader(StatusRedirect, u.String())
|
||||
w.Header(StatusRedirect, u.String())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ func (mux *ServeMux) Respond(w *ResponseWriter, r *Request) {
|
||||
|
||||
resp := mux.match(path)
|
||||
if resp == nil {
|
||||
w.WriteStatus(StatusNotFound)
|
||||
w.Status(StatusNotFound)
|
||||
return
|
||||
}
|
||||
resp.Respond(w, r)
|
||||
|
||||
@@ -21,7 +21,6 @@ type Request struct {
|
||||
Host string
|
||||
|
||||
// Certificate specifies the TLS certificate to use for the request.
|
||||
// Request certificates take precedence over client certificates.
|
||||
//
|
||||
// On the server side, if the client provided a certificate then
|
||||
// Certificate.Leaf is guaranteed to be non-nil.
|
||||
|
||||
93
response.go
93
response.go
@@ -13,7 +13,7 @@ type Response struct {
|
||||
Status Status
|
||||
|
||||
// Meta contains more information related to the response status.
|
||||
// For successful responses, Meta should contain the mimetype of the response.
|
||||
// For successful responses, Meta should contain the media type of the response.
|
||||
// For failure responses, Meta should contain a short description of the failure.
|
||||
// Meta should not be longer than 1024 bytes.
|
||||
Meta string
|
||||
@@ -82,6 +82,8 @@ func ReadResponse(rc io.ReadCloser) (*Response, error) {
|
||||
|
||||
if resp.Status.Class() == StatusClassSuccess {
|
||||
resp.Body = newReadCloserBody(br, rc)
|
||||
} else {
|
||||
rc.Close()
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
@@ -112,3 +114,92 @@ func (b *readCloserBody) Read(p []byte) (n int, err error) {
|
||||
}
|
||||
return b.ReadCloser.Read(p)
|
||||
}
|
||||
|
||||
// ResponseWriter is used to construct a Gemini response.
|
||||
type ResponseWriter struct {
|
||||
b *bufio.Writer
|
||||
status Status
|
||||
meta string
|
||||
setHeader bool
|
||||
wroteHeader bool
|
||||
bodyAllowed bool
|
||||
}
|
||||
|
||||
// NewResponseWriter returns a ResponseWriter that uses the provided io.Writer.
|
||||
func NewResponseWriter(w io.Writer) *ResponseWriter {
|
||||
return &ResponseWriter{
|
||||
b: bufio.NewWriter(w),
|
||||
}
|
||||
}
|
||||
|
||||
// Header sets the response header.
|
||||
func (w *ResponseWriter) Header(status Status, meta string) {
|
||||
w.status = status
|
||||
w.meta = meta
|
||||
}
|
||||
|
||||
// Status sets the response status code.
|
||||
// It also sets the response meta to status.Meta().
|
||||
func (w *ResponseWriter) Status(status Status) {
|
||||
w.status = status
|
||||
w.meta = status.Meta()
|
||||
}
|
||||
|
||||
// Meta sets the response meta.
|
||||
//
|
||||
// For successful responses, meta should contain the media type of the response.
|
||||
// For failure responses, meta should contain a short description of the failure.
|
||||
// The response meta should not be greater than 1024 bytes.
|
||||
func (w *ResponseWriter) Meta(meta string) {
|
||||
w.meta = meta
|
||||
}
|
||||
|
||||
// Write writes data to the connection as part of the response body.
|
||||
// If the response status does not allow for a response body, Write returns
|
||||
// ErrBodyNotAllowed.
|
||||
//
|
||||
// Write writes the response header if it has not already been written.
|
||||
// It writes a successful status code if one is not set.
|
||||
func (w *ResponseWriter) Write(b []byte) (int, error) {
|
||||
if !w.wroteHeader {
|
||||
w.writeHeader(StatusSuccess)
|
||||
}
|
||||
if !w.bodyAllowed {
|
||||
return 0, ErrBodyNotAllowed
|
||||
}
|
||||
return w.b.Write(b)
|
||||
}
|
||||
|
||||
func (w *ResponseWriter) writeHeader(defaultStatus Status) {
|
||||
status := w.status
|
||||
if status == 0 {
|
||||
status = defaultStatus
|
||||
}
|
||||
|
||||
meta := w.meta
|
||||
if status.Class() == StatusClassSuccess {
|
||||
w.bodyAllowed = true
|
||||
|
||||
if meta == "" {
|
||||
meta = "text/gemini"
|
||||
}
|
||||
}
|
||||
|
||||
w.b.WriteString(strconv.Itoa(int(status)))
|
||||
w.b.WriteByte(' ')
|
||||
w.b.WriteString(meta)
|
||||
w.b.Write(crlf)
|
||||
w.wroteHeader = true
|
||||
}
|
||||
|
||||
// Flush writes any buffered data to the underlying io.Writer.
|
||||
//
|
||||
// Flush writes the response header if it has not already been written.
|
||||
// It writes a failure status code if one is not set.
|
||||
func (w *ResponseWriter) Flush() error {
|
||||
if !w.wroteHeader {
|
||||
w.writeHeader(StatusTemporaryFailure)
|
||||
}
|
||||
// Write errors from writeHeader will be returned here.
|
||||
return w.b.Flush()
|
||||
}
|
||||
|
||||
121
server.go
121
server.go
@@ -1,13 +1,10 @@
|
||||
package gemini
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -176,39 +173,43 @@ func (s *Server) getCertificateFor(hostname string) (*tls.Certificate, error) {
|
||||
func (s *Server) respond(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
if d := s.ReadTimeout; d != 0 {
|
||||
conn.SetReadDeadline(time.Now().Add(d))
|
||||
_ = conn.SetReadDeadline(time.Now().Add(d))
|
||||
}
|
||||
if d := s.WriteTimeout; d != 0 {
|
||||
conn.SetWriteDeadline(time.Now().Add(d))
|
||||
_ = conn.SetWriteDeadline(time.Now().Add(d))
|
||||
}
|
||||
|
||||
w := NewResponseWriter(conn)
|
||||
defer w.b.Flush()
|
||||
defer func() {
|
||||
_ = w.Flush()
|
||||
}()
|
||||
|
||||
req, err := ReadRequest(conn)
|
||||
if err != nil {
|
||||
w.WriteStatus(StatusBadRequest)
|
||||
} else {
|
||||
// Store information about the TLS connection
|
||||
if tlsConn, ok := conn.(*tls.Conn); ok {
|
||||
req.TLS = tlsConn.ConnectionState()
|
||||
if len(req.TLS.PeerCertificates) > 0 {
|
||||
peerCert := req.TLS.PeerCertificates[0]
|
||||
// Store the TLS certificate
|
||||
req.Certificate = &tls.Certificate{
|
||||
Certificate: [][]byte{peerCert.Raw},
|
||||
Leaf: peerCert,
|
||||
}
|
||||
w.Status(StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Store information about the TLS connection
|
||||
if tlsConn, ok := conn.(*tls.Conn); ok {
|
||||
req.TLS = tlsConn.ConnectionState()
|
||||
if len(req.TLS.PeerCertificates) > 0 {
|
||||
peerCert := req.TLS.PeerCertificates[0]
|
||||
// Store the TLS certificate
|
||||
req.Certificate = &tls.Certificate{
|
||||
Certificate: [][]byte{peerCert.Raw},
|
||||
Leaf: peerCert,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resp := s.responder(req)
|
||||
if resp != nil {
|
||||
resp.Respond(w, req)
|
||||
} else {
|
||||
w.WriteStatus(StatusNotFound)
|
||||
if resp == nil {
|
||||
w.Status(StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
resp.Respond(w, req)
|
||||
}
|
||||
|
||||
func (s *Server) responder(r *Request) Responder {
|
||||
@@ -232,82 +233,6 @@ func (s *Server) logf(format string, args ...interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
// ResponseWriter is used by a Gemini handler to construct a Gemini response.
|
||||
type ResponseWriter struct {
|
||||
b *bufio.Writer
|
||||
bodyAllowed bool
|
||||
wroteHeader bool
|
||||
mediatype string
|
||||
}
|
||||
|
||||
// NewResponseWriter returns a ResponseWriter that uses the provided io.Writer.
|
||||
func NewResponseWriter(w io.Writer) *ResponseWriter {
|
||||
return &ResponseWriter{
|
||||
b: bufio.NewWriter(w),
|
||||
}
|
||||
}
|
||||
|
||||
// WriteHeader writes the response header.
|
||||
// If the header has already been written, WriteHeader does nothing.
|
||||
//
|
||||
// Meta contains more information related to the response status.
|
||||
// For successful responses, Meta should contain the mimetype of the response.
|
||||
// For failure responses, Meta should contain a short description of the failure.
|
||||
// Meta should not be longer than 1024 bytes.
|
||||
func (w *ResponseWriter) WriteHeader(status Status, meta string) {
|
||||
if w.wroteHeader {
|
||||
return
|
||||
}
|
||||
w.b.WriteString(strconv.Itoa(int(status)))
|
||||
w.b.WriteByte(' ')
|
||||
w.b.WriteString(meta)
|
||||
w.b.Write(crlf)
|
||||
|
||||
// Only allow body to be written on successful status codes.
|
||||
if status.Class() == StatusClassSuccess {
|
||||
w.bodyAllowed = true
|
||||
}
|
||||
w.wroteHeader = true
|
||||
}
|
||||
|
||||
// WriteStatus writes the response header with the given status code.
|
||||
//
|
||||
// WriteStatus is equivalent to WriteHeader(status, status.Message())
|
||||
func (w *ResponseWriter) WriteStatus(status Status) {
|
||||
w.WriteHeader(status, status.Message())
|
||||
}
|
||||
|
||||
// SetMediaType sets the media type that will be written for a successful response.
|
||||
// If the mimetype is not set, it will default to "text/gemini".
|
||||
func (w *ResponseWriter) SetMediaType(mediatype string) {
|
||||
w.mediatype = mediatype
|
||||
}
|
||||
|
||||
// Write writes data to the connection as part of the response body.
|
||||
// If the response status does not allow for a response body, Write returns
|
||||
// ErrBodyNotAllowed.
|
||||
//
|
||||
// If the response header has not yet been written, Write calls WriteHeader
|
||||
// with StatusSuccess and the mimetype set in SetMimetype.
|
||||
func (w *ResponseWriter) Write(b []byte) (int, error) {
|
||||
if !w.wroteHeader {
|
||||
mediatype := w.mediatype
|
||||
if mediatype == "" {
|
||||
mediatype = "text/gemini"
|
||||
}
|
||||
w.WriteHeader(StatusSuccess, mediatype)
|
||||
}
|
||||
if !w.bodyAllowed {
|
||||
return 0, ErrBodyNotAllowed
|
||||
}
|
||||
return w.b.Write(b)
|
||||
}
|
||||
|
||||
// Flush writes any buffered data to the underlying io.Writer.
|
||||
func (w *ResponseWriter) Flush() error {
|
||||
return w.b.Flush()
|
||||
}
|
||||
|
||||
// A Responder responds to a Gemini request.
|
||||
type Responder interface {
|
||||
// Respond accepts a Request and constructs a Response.
|
||||
|
||||
16
status.go
16
status.go
@@ -41,19 +41,11 @@ func (s Status) Class() StatusClass {
|
||||
return StatusClass(s / 10)
|
||||
}
|
||||
|
||||
// Message returns a status message corresponding to this status code.
|
||||
func (s Status) Message() string {
|
||||
// Meta returns a description of the status code appropriate for use in a response.
|
||||
//
|
||||
// Meta returns an empty string for input, success, and redirect status codes.
|
||||
func (s Status) Meta() string {
|
||||
switch s {
|
||||
case StatusInput:
|
||||
return "Input"
|
||||
case StatusSensitiveInput:
|
||||
return "Sensitive input"
|
||||
case StatusSuccess:
|
||||
return "Success"
|
||||
case StatusRedirect:
|
||||
return "Redirect"
|
||||
case StatusPermanentRedirect:
|
||||
return "Permanent redirect"
|
||||
case StatusTemporaryFailure:
|
||||
return "Temporary failure"
|
||||
case StatusServerUnavailable:
|
||||
|
||||
10
text.go
10
text.go
@@ -88,17 +88,17 @@ func (l LineText) line() {}
|
||||
type Text []Line
|
||||
|
||||
// ParseText parses Gemini text from the provided io.Reader.
|
||||
func ParseText(r io.Reader) Text {
|
||||
func ParseText(r io.Reader) (Text, error) {
|
||||
var t Text
|
||||
ParseLines(r, func(line Line) {
|
||||
err := ParseLines(r, func(line Line) {
|
||||
t = append(t, line)
|
||||
})
|
||||
return t
|
||||
return t, err
|
||||
}
|
||||
|
||||
// ParseLines parses Gemini text from the provided io.Reader.
|
||||
// It calls handler with each line that it parses.
|
||||
func ParseLines(r io.Reader, handler func(Line)) {
|
||||
func ParseLines(r io.Reader, handler func(Line)) error {
|
||||
const spacetab = " \t"
|
||||
var pre bool
|
||||
scanner := bufio.NewScanner(r)
|
||||
@@ -149,6 +149,8 @@ func ParseLines(r io.Reader, handler func(Line)) {
|
||||
}
|
||||
handler(line)
|
||||
}
|
||||
|
||||
return scanner.Err()
|
||||
}
|
||||
|
||||
// String writes the Gemini text response to a string and returns it.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
package gemini
|
||||
// Package tofu implements trust on first use using hosts and fingerprints.
|
||||
package tofu
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -52,12 +53,17 @@ func (k *KnownHostsFile) Lookup(hostname string) (Fingerprint, bool) {
|
||||
}
|
||||
|
||||
// Write writes a known hosts entry to the configured output.
|
||||
func (k *KnownHostsFile) Write(hostname string, fingerprint Fingerprint) {
|
||||
func (k *KnownHostsFile) Write(hostname string, fingerprint Fingerprint) error {
|
||||
k.mu.RLock()
|
||||
defer k.mu.RUnlock()
|
||||
if k.out != nil {
|
||||
k.writeKnownHost(k.out, hostname, fingerprint)
|
||||
_, err := k.writeKnownHost(k.out, hostname, fingerprint)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write to known host file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteAll writes all of the known hosts to the provided io.Writer.
|
||||
Reference in New Issue
Block a user