diff --git a/client.go b/client.go index de8aed5..566894a 100644 --- a/client.go +++ b/client.go @@ -12,6 +12,7 @@ import ( "tildegit.org/tjp/sliderule/gemini" "tildegit.org/tjp/sliderule/gopher" "tildegit.org/tjp/sliderule/internal/types" + "tildegit.org/tjp/sliderule/nex" "tildegit.org/tjp/sliderule/spartan" ) @@ -50,6 +51,7 @@ func NewClient(tlsConf *tls.Config) Client { "spartan": spartan.NewClient(), "http": hc, "https": hc, + "nex": nex.Client{}, }, MaxRedirects: DefaultMaxRedirects, } diff --git a/contrib/cgi/nex.go b/contrib/cgi/nex.go new file mode 100644 index 0000000..60d487d --- /dev/null +++ b/contrib/cgi/nex.go @@ -0,0 +1,10 @@ +package cgi + +import ( + "tildegit.org/tjp/sliderule" + "tildegit.org/tjp/sliderule/nex" +) + +func NexCGIDirectory(fsroot, urlroot, cmd string) sliderule.Handler { + return cgiDirectory(nex.ServerProtocol, fsroot, urlroot, cmd) +} diff --git a/contrib/fs/nex.go b/contrib/fs/nex.go new file mode 100644 index 0000000..47eb83e --- /dev/null +++ b/contrib/fs/nex.go @@ -0,0 +1,48 @@ +package fs + +import ( + "text/template" + + sr "tildegit.org/tjp/sliderule" + "tildegit.org/tjp/sliderule/nex" +) + +// NexFileHandler builds a handler which serves up files from a file system. +// +// It only serves responses for paths which correspond to files, not directories. +func NexFileHandler(fsroot, urlroot string) sr.Handler { + return fileHandler(nex.ServerProtocol, fsroot, urlroot) +} + +// NexDirectoryDefault serves up default files for directory path requests. +// +// If any of the supported filenames are found in the requested directory, the +// contents of that file is returned as the nex response. +// +// It returns nil for any paths which don't correspond to a directory. +func NexDirectoryDefault(fsroot, urlroot string, filenames ...string) sr.Handler { + return directoryDefault(nex.ServerProtocol, fsroot, urlroot, false, filenames...) +} + +// NexDirectoryListing produces a listing of the contents of any requested directories. +// +// It returns a nil response for any paths which don't correspond to a filesystem directory. +// +// When it encounters a directory path which doesn't end in a trailing slash (/) it returns +// a nil response. Trailing slashes are necessary for relative links to work properly. +// +// The template may be nil, in which case DefaultNexDirectoryList is used instead. The +// template is then processed with RenderDirectoryListing. +func NexDirectoryListing(fsroot, urlroot string, tmpl *template.Template) sr.Handler { + if tmpl == nil { + tmpl = DefaultNexDirectoryList + } + return directoryListing(nex.ServerProtocol, fsroot, urlroot, "", false, tmpl) +} + +var DefaultNexDirectoryList = template.Must(template.New("nex_dirlist").Parse(` +{{ .DirName }} directory: +{{ range .Entries }} +=> ./{{ .Name }}{{ if .IsDir }}/{{ end -}} +{{ end }} +`[1:])) diff --git a/nex/client.go b/nex/client.go new file mode 100644 index 0000000..5f53746 --- /dev/null +++ b/nex/client.go @@ -0,0 +1,61 @@ +package nex + +import ( + "bytes" + "errors" + "io" + "net" + neturl "net/url" + + "tildegit.org/tjp/sliderule/internal/types" +) + +// Client is used for sending nex requests and reading responses. +// +// It carries no state and is reusable simultaneously by multiple goroutines. +// +// The zero value is immediately usable. +type Client struct{} + +// RoundTrip sends a single nex request and returns its response. +func (c Client) RoundTrip(request *types.Request) (*types.Response, error) { + if request.Scheme != "nex" && request.Scheme != "" { + return nil, errors.New("non-nex protocols not supported") + } + + host := request.Host + if _, port, _ := net.SplitHostPort(host); port == "" { + host = net.JoinHostPort(host, "1900") + } + + conn, err := net.Dial("tcp", host) + if err != nil { + return nil, err + } + defer conn.Close() + + request.RemoteAddr = conn.RemoteAddr() + request.TLSState = nil + + if _, err := conn.Write([]byte(request.Path + "\n")); err != nil { + return nil, err + } + + response, err := io.ReadAll(conn) + if err != nil { + return nil, err + } + + return &types.Response{Body: bytes.NewBuffer(response)}, nil +} + +// Fetch builds and sends a nex request, and returns the response. +func (c Client) Fetch(url string) (*types.Response, error) { + u, err := neturl.Parse(url) + if err != nil { + return nil, err + } + return c.RoundTrip(&types.Request{URL: u}) +} + +func (c Client) IsRedirect(response *types.Response) bool { return false } diff --git a/nex/protocol.go b/nex/protocol.go new file mode 100644 index 0000000..0fad3a2 --- /dev/null +++ b/nex/protocol.go @@ -0,0 +1,23 @@ +package nex + +import ( + "io" + "net/url" + + "tildegit.org/tjp/sliderule/internal/types" +) + +type proto struct{} + +func (p proto) TemporaryRedirect(u *url.URL) *types.Response { return nil } +func (p proto) PermanentRedirect(u *url.URL) *types.Response { return nil } + +func (p proto) TemporaryServerError(err error) *types.Response { return StringResponse(err.Error()) } +func (p proto) PermanentServerError(err error) *types.Response { return StringResponse(err.Error()) } +func (p proto) CGIFailure(err error) *types.Response { return StringResponse(err.Error()) } + +func (p proto) Success(_ string, body io.Reader) *types.Response { return Response(body) } + +func (p proto) ParseResponse(input io.Reader) (*types.Response, error) { return Response(input), nil } + +var ServerProtocol types.ServerProtocol = proto{} diff --git a/nex/request.go b/nex/request.go new file mode 100644 index 0000000..290d55d --- /dev/null +++ b/nex/request.go @@ -0,0 +1,28 @@ +package nex + +import ( + "bufio" + "io" + "net/url" + "strings" + + "tildegit.org/tjp/sliderule/internal/types" +) + +// ParseRequest reads a nex request from an io.Reader. +func ParseRequest(rdr io.Reader) (*types.Request, error) { + line, err := bufio.NewReader(rdr).ReadString('\n') + if err != nil { + return nil, err + } + line = strings.TrimSuffix(line, "\n") + line = strings.TrimSuffix(line, "\r") + + return &types.Request{ + URL: &url.URL{ + Scheme: "nex", + Path: line, + OmitHost: true, + }, + }, nil +} diff --git a/nex/response.go b/nex/response.go new file mode 100644 index 0000000..165f161 --- /dev/null +++ b/nex/response.go @@ -0,0 +1,18 @@ +package nex + +import ( + "bytes" + "io" + + "tildegit.org/tjp/sliderule/internal/types" +) + +// Response builds a nex Response from an io.Reader. +func Response(body io.Reader) *types.Response { + return &types.Response{Body: body} +} + +// StringResponse builds a nex Response from a string. +func StringResponse(body string) *types.Response { + return Response(bytes.NewBufferString(body)) +} diff --git a/nex/serve.go b/nex/serve.go new file mode 100644 index 0000000..7e103e3 --- /dev/null +++ b/nex/serve.go @@ -0,0 +1,91 @@ +package nex + +import ( + "bytes" + "context" + "crypto/tls" + "fmt" + "io" + "net" + + "tildegit.org/tjp/sliderule/internal" + "tildegit.org/tjp/sliderule/internal/types" + "tildegit.org/tjp/sliderule/logging" +) + +type nexServer struct { + internal.Server + handler types.Handler +} + +func (ns nexServer) Protocol() string { return "NEX" } + +// NewServer builds a new nex server +func NewServer( + ctx context.Context, + hostname string, + network string, + address string, + handler types.Handler, + baseLog logging.Logger, +) (types.Server, error) { + ns := &nexServer{handler: handler} + + hostname = internal.JoinDefaultPort(hostname, "1900") + address = internal.JoinDefaultPort(address, "1900") + + var err error + ns.Server, err = internal.NewServer(ctx, hostname, network, address, baseLog, ns.handleConn) + if err != nil { + return nil, err + } + + return ns, err +} + +func NewTLSServer( + ctx context.Context, + hostname string, + network string, + address string, + handler types.Handler, + baseLog logging.Logger, + tlsConfig *tls.Config, +) (types.Server, error) { + ns, err := NewServer(ctx, hostname, network, address, handler, baseLog) + if err != nil { + return nil, err + } + + ns.(*nexServer).Listener = tls.NewListener(ns.(*nexServer).Listener, tlsConfig) + return ns, nil +} + +func (ns *nexServer) handleConn(conn net.Conn) { + request, err := ParseRequest(conn) + if err != nil { + _, _ = fmt.Fprint(conn, err.Error()+"\n") + } + + request.Server = ns + request.RemoteAddr = conn.RemoteAddr() + + if tlsconn, ok := conn.(*tls.Conn); ok { + state := tlsconn.ConnectionState() + request.TLSState = &state + } + + defer func() { + if r := recover(); r != nil { + _ = ns.LogError("msg", "panic in handler", "err", r) + _, _ = fmt.Fprint(conn, "Error handling request.\n") + } + }() + response := ns.handler.Handle(ns.Ctx, request) + if response == nil { + response = Response(bytes.NewBufferString("Document not found.")) + } + + defer response.Close() + _, _ = io.Copy(conn, response.Body) +}