Simple client functionality and an example.
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
This commit is contained in:
parent
e46658d475
commit
cc0c7e6eb5
4
TODO.md
4
TODO.md
|
@ -1,6 +1,8 @@
|
|||
- [x] server
|
||||
- [x] TLS configuration from cert+key files
|
||||
- [ ] client
|
||||
- [x] client
|
||||
- [ ] follow redirects
|
||||
- [ ] reject non-gemini requests
|
||||
- [x] contrib - filesystem handling
|
||||
- [x] serving files
|
||||
- [x] directory index files
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"tildegit.org/tjp/gus/gemini"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Fprintf(os.Stderr, "usage: %s <gemini url>\n", os.Args[0])
|
||||
}
|
||||
|
||||
certfile, keyfile := envConfig()
|
||||
|
||||
// build a client
|
||||
var client gemini.Client
|
||||
if certfile != "" && keyfile != "" {
|
||||
tlsConf, err := gemini.FileTLS(certfile, keyfile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
client = gemini.NewClient(tlsConf)
|
||||
}
|
||||
|
||||
// parse the URL and build the request
|
||||
request := &gemini.Request{URL: buildURL()}
|
||||
|
||||
// fetch the response
|
||||
response, err := client.RoundTrip(request)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if response.Status != gemini.StatusSuccess {
|
||||
log.Fatalf("%d %s\n", response.Status, response.Meta)
|
||||
}
|
||||
|
||||
//io.Copy(os.Stdout, response)
|
||||
buf, err := io.ReadAll(response)
|
||||
fmt.Printf("response: %s\n", buf)
|
||||
}
|
||||
|
||||
func envConfig() (string, string) {
|
||||
return os.Getenv("SERVER_CERTIFICATE"), os.Getenv("SERVER_PRIVATEKEY")
|
||||
}
|
||||
|
||||
func buildURL() *url.URL {
|
||||
raw := os.Args[1]
|
||||
if strings.HasPrefix(raw, "//") {
|
||||
raw = "gemini:" + raw
|
||||
}
|
||||
|
||||
u, err := url.Parse(raw)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
return u
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
package gemini
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
)
|
||||
|
||||
// Client is used for sending gemini requests and parsing gemini responses.
|
||||
//
|
||||
// It carries no state and is usable and reusable simultaneously by multiple goroutines.
|
||||
// The only reason you might create more than one Client is to support separate TLS-cert
|
||||
// driven identities.
|
||||
//
|
||||
// The zero value of Client is usable, it simply has no client TLS cert.
|
||||
type Client struct {
|
||||
tlsConf *tls.Config
|
||||
}
|
||||
|
||||
// Create a gemini Client with the given TLS configuration.
|
||||
func NewClient(tlsConf *tls.Config) Client {
|
||||
return Client{tlsConf: tlsConf}
|
||||
}
|
||||
|
||||
// RoundTrip sends a single gemini request to the correct server and returns its response.
|
||||
//
|
||||
// It also populates the TLSState and RemoteAddr fields on the request - the only field
|
||||
// it needs populated beforehand is the URL.
|
||||
func (client Client) RoundTrip(request *Request) (*Response, error) {
|
||||
if request.Scheme != "gemini" && request.Scheme != "" {
|
||||
return nil, errors.New("non-gemini protocols not supported")
|
||||
}
|
||||
|
||||
host := request.Host
|
||||
if _, port, _ := net.SplitHostPort(host); port == "" {
|
||||
host += ":1965"
|
||||
}
|
||||
|
||||
conn, err := tls.Dial("tcp", host, client.tlsConf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
request.RemoteAddr = conn.RemoteAddr()
|
||||
st := conn.ConnectionState()
|
||||
request.TLSState = &st
|
||||
|
||||
if _, err := conn.Write([]byte(request.URL.String() + "\r\n")); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response, err := ParseResponse(conn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// read and store the request body in full or we may miss doing so before the
|
||||
// connection gets closed.
|
||||
bodybuf, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.Body = bytes.NewBuffer(bodybuf)
|
||||
|
||||
return response, nil
|
||||
}
|
|
@ -283,6 +283,9 @@ func ParseResponse(rdr io.Reader) (*Response, error) {
|
|||
if hdrLine[len(hdrLine)-2] != '\r' {
|
||||
return nil, InvalidResponseLineEnding
|
||||
}
|
||||
if hdrLine[2] != ' ' {
|
||||
return nil, InvalidResponseHeaderLine
|
||||
}
|
||||
hdrLine = hdrLine[:len(hdrLine)-2]
|
||||
|
||||
status, err := strconv.Atoi(string(hdrLine[:2]))
|
||||
|
@ -292,7 +295,7 @@ func ParseResponse(rdr io.Reader) (*Response, error) {
|
|||
|
||||
return &Response{
|
||||
Status: Status(status),
|
||||
Meta: string(hdrLine[2:]),
|
||||
Meta: string(hdrLine[3:]),
|
||||
Body: bufrdr,
|
||||
}, nil
|
||||
}
|
||||
|
|
Reference in New Issue