17 Commits

Author SHA1 Message Date
Adnan Maolood
e5cf345273 Update README.md 2021-03-24 13:30:46 -04:00
Adnan Maolood
c68ce57488 mux: Add copyright notice 2021-03-24 13:09:53 -04:00
Adnan Maolood
2b161650fe Split LICENSE into two files 2021-03-24 13:08:32 -04:00
Adnan Maolood
dbbef1fb6d Revert "Require Go 1.16"
This reverts commit 0e87d64ffc.
2021-03-23 22:05:12 -04:00
Adnan Maolood
90518a01a8 Revert "Replace uses of ioutil with io"
This reverts commit 19f1d6693e.
2021-03-23 22:02:32 -04:00
Adnan Maolood
056e55abbb response: Remove unnecessary length check 2021-03-20 18:29:40 -04:00
Adnan Maolood
72d437c82e response: Limit response header size 2021-03-20 14:01:45 -04:00
Adnan Maolood
3dca29eb41 response: Don't use bufReadCloser 2021-03-20 13:41:53 -04:00
Adnan Maolood
a40b5dcd0b fs: Fix empty media type for directory index pages 2021-03-20 13:33:15 -04:00
Adnan Maolood
fffe86680e client: Only get cert if TrustCertificate is set 2021-03-20 12:54:41 -04:00
Adnan Maolood
d5af32e121 client: Close connection on error 2021-03-20 12:49:27 -04:00
Adnan Maolood
5141eaafaa Tweak request and response parsing 2021-03-20 12:27:20 -04:00
Adnan Maolood
e5c0afa013 response: Treat empty meta as invalid 2021-03-20 12:07:24 -04:00
Adnan Maolood
4c7c200f92 Remove unused field 2021-03-20 12:05:21 -04:00
Adnan Maolood
0a709da439 Remove charset=utf-8 from default media type 2021-03-20 12:04:42 -04:00
Adnan Maolood
1fdef9b608 Rename ServeMux to Mux 2021-03-15 15:44:35 -04:00
Adnan Maolood
2144e2c2f2 status: Reintroduce StatusSensitiveInput 2021-03-15 15:19:43 -04:00
19 changed files with 161 additions and 160 deletions

64
LICENSE
View File

@@ -1,51 +1,19 @@
go-gemini is available under the terms of the MIT license: Copyright (c) 2020 Adnan Maolood
Copyright (c) 2020 Adnan Maolood Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
Permission is hereby granted, free of charge, to any person obtaining a copy The above copyright notice and this permission notice shall be included in all
of this software and associated documentation files (the "Software"), to deal copies or substantial portions of the Software.
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
copies or substantial portions of the Software. IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER SOFTWARE.
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Portions of this program were taken from Go:
Copyright (c) 2009 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

27
LICENSE-GO Normal file
View File

@@ -0,0 +1,27 @@
Copyright (c) 2009 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -2,9 +2,9 @@
[![godocs.io](https://godocs.io/git.sr.ht/~adnano/go-gemini?status.svg)](https://godocs.io/git.sr.ht/~adnano/go-gemini) [![builds.sr.ht status](https://builds.sr.ht/~adnano/go-gemini.svg)](https://builds.sr.ht/~adnano/go-gemini?) [![godocs.io](https://godocs.io/git.sr.ht/~adnano/go-gemini?status.svg)](https://godocs.io/git.sr.ht/~adnano/go-gemini) [![builds.sr.ht status](https://builds.sr.ht/~adnano/go-gemini.svg)](https://builds.sr.ht/~adnano/go-gemini?)
Package gemini implements the [Gemini protocol](https://gemini.circumlunar.space) in Go. Package gemini implements the [Gemini protocol](https://gemini.circumlunar.space)
in Go. It provides an API similar to that of net/http to facilitate the
It provides an API similar to that of net/http to make it easy to develop Gemini clients and servers. development of Gemini clients and servers.
Compatible with version v0.14.3 of the Gemini specification. Compatible with version v0.14.3 of the Gemini specification.
@@ -12,6 +12,9 @@ Compatible with version v0.14.3 of the Gemini specification.
import "git.sr.ht/~adnano/go-gemini" import "git.sr.ht/~adnano/go-gemini"
Note that some filesystem-related functionality is only available on Go 1.16
or later as it relies on the io/fs package.
## Examples ## Examples
There are a few examples provided in the examples directory. There are a few examples provided in the examples directory.
@@ -24,3 +27,9 @@ To run an example:
Send patches and questions to [~adnano/go-gemini-devel](https://lists.sr.ht/~adnano/go-gemini-devel). Send patches and questions to [~adnano/go-gemini-devel](https://lists.sr.ht/~adnano/go-gemini-devel).
Subscribe to release announcements on [~adnano/go-gemini-announce](https://lists.sr.ht/~adnano/go-gemini-announce). Subscribe to release announcements on [~adnano/go-gemini-announce](https://lists.sr.ht/~adnano/go-gemini-announce).
## License
go-gemini is licensed under the terms of the MIT license (see LICENSE).
Portions of this library were adapted from Go and are governed by a BSD-style
license (see LICENSE-GO). Those files are marked accordingly.

View File

@@ -131,6 +131,9 @@ func (c *Client) Do(ctx context.Context, req *Request) (*Response, error) {
conn.Close() conn.Close()
return nil, ctx.Err() return nil, ctx.Err()
case r := <-res: case r := <-res:
if r.err != nil {
conn.Close()
}
return r.resp, r.err return r.resp, r.err
} }
} }
@@ -174,9 +177,9 @@ func (c *Client) dialContext(ctx context.Context, network, addr string) (net.Con
} }
func (c *Client) verifyConnection(cs tls.ConnectionState, hostname string) error { func (c *Client) verifyConnection(cs tls.ConnectionState, hostname string) error {
cert := cs.PeerCertificates[0]
// See if the client trusts the certificate // See if the client trusts the certificate
if c.TrustCertificate != nil { if c.TrustCertificate != nil {
cert := cs.PeerCertificates[0]
return c.TrustCertificate(hostname, cert) return c.TrustCertificate(hostname, cert)
} }
return nil return nil

6
doc.go
View File

@@ -29,10 +29,10 @@ Servers should be configured with certificates:
} }
server.GetCertificate = certificates.Get server.GetCertificate = certificates.Get
ServeMux is a Gemini request multiplexer. Mux is a Gemini request multiplexer.
ServeMux can handle requests for multiple hosts and schemes. Mux can handle requests for multiple hosts and schemes.
mux := &gemini.ServeMux{} mux := &gemini.Mux{}
mux.HandleFunc("example.com", func(ctx context.Context, w gemini.ResponseWriter, r *gemini.Request) { mux.HandleFunc("example.com", func(ctx context.Context, w gemini.ResponseWriter, r *gemini.Request) {
fmt.Fprint(w, "Welcome to example.com") fmt.Fprint(w, "Welcome to example.com")
}) })

View File

@@ -30,7 +30,7 @@ func main() {
log.Fatal(err) log.Fatal(err)
} }
mux := &gemini.ServeMux{} mux := &gemini.Mux{}
mux.HandleFunc("/", profile) mux.HandleFunc("/", profile)
mux.HandleFunc("/username", changeUsername) mux.HandleFunc("/username", changeUsername)

View File

@@ -22,7 +22,7 @@ func main() {
log.Fatal(err) log.Fatal(err)
} }
mux := &gemini.ServeMux{} mux := &gemini.Mux{}
mux.Handle("/", gemini.FileServer(os.DirFS("/var/www"))) mux.Handle("/", gemini.FileServer(os.DirFS("/var/www")))
server := &gemini.Server{ server := &gemini.Server{

View File

@@ -21,7 +21,7 @@ func main() {
log.Fatal(err) log.Fatal(err)
} }
mux := &gemini.ServeMux{} mux := &gemini.Mux{}
mux.HandleFunc("/", stream) mux.HandleFunc("/", stream)
server := &gemini.Server{ server := &gemini.Server{

5
fs.go
View File

@@ -1,3 +1,5 @@
// +build go1.16
package gemini package gemini
import ( import (
@@ -151,7 +153,8 @@ func serveFile(w ResponseWriter, r *Request, fsys fs.FS, name string, redirect b
} }
// Use contents of index.gmi if present // Use contents of index.gmi if present
index, err := fsys.Open(path.Join(name, indexPage)) name = path.Join(name, indexPage)
index, err := fsys.Open(name)
if err == nil { if err == nil {
defer index.Close() defer index.Close()
istat, err := index.Stat() istat, err := index.Stat()

View File

@@ -11,8 +11,6 @@ func init() {
mime.AddExtensionType(".gemini", "text/gemini") mime.AddExtensionType(".gemini", "text/gemini")
} }
var crlf = []byte("\r\n")
// Errors. // Errors.
var ( var (
ErrInvalidRequest = errors.New("gemini: invalid request") ErrInvalidRequest = errors.New("gemini: invalid request")
@@ -22,3 +20,15 @@ var (
// when the response status code does not permit a body. // when the response status code does not permit a body.
ErrBodyNotAllowed = errors.New("gemini: response status code does not allow body") ErrBodyNotAllowed = errors.New("gemini: response status code does not allow body")
) )
var crlf = []byte("\r\n")
func trimCRLF(b []byte) ([]byte, bool) {
// Check for CR
if len(b) < 2 || b[len(b)-2] != '\r' {
return nil, false
}
// Trim CRLF
b = b[:len(b)-2]
return b, true
}

2
go.mod
View File

@@ -1,5 +1,5 @@
module git.sr.ht/~adnano/go-gemini module git.sr.ht/~adnano/go-gemini
go 1.16 go 1.15
require golang.org/x/net v0.0.0-20210119194325-5f4716e94777 require golang.org/x/net v0.0.0-20210119194325-5f4716e94777

28
io.go
View File

@@ -1,7 +1,6 @@
package gemini package gemini
import ( import (
"bufio"
"context" "context"
"io" "io"
) )
@@ -75,30 +74,3 @@ func (nopReadCloser) Read(p []byte) (int, error) {
func (nopReadCloser) Close() error { func (nopReadCloser) Close() error {
return nil return nil
} }
type bufReadCloser struct {
br *bufio.Reader // used until empty
io.ReadCloser
}
func newBufReadCloser(br *bufio.Reader, rc io.ReadCloser) io.ReadCloser {
body := &bufReadCloser{ReadCloser: rc}
if br.Buffered() != 0 {
body.br = br
}
return body
}
func (b *bufReadCloser) Read(p []byte) (n int, err error) {
if b.br != nil {
if n := b.br.Buffered(); len(p) > n {
p = p[:n]
}
n, err = b.br.Read(p)
if b.br.Buffered() == 0 {
b.br = nil
}
return n, err
}
return b.ReadCloser.Read(p)
}

30
mux.go
View File

@@ -1,3 +1,7 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE-GO file.
package gemini package gemini
import ( import (
@@ -10,7 +14,7 @@ import (
"sync" "sync"
) )
// ServeMux is a Gemini request multiplexer. // Mux is a Gemini request multiplexer.
// It matches the URL of each incoming request against a list of registered // It matches the URL of each incoming request against a list of registered
// patterns and calls the handler for the pattern that // patterns and calls the handler for the pattern that
// most closely matches the URL. // most closely matches the URL.
@@ -55,17 +59,17 @@ import (
// scheme of "gemini". // scheme of "gemini".
// //
// If a subtree has been registered and a request is received naming the // If a subtree has been registered and a request is received naming the
// subtree root without its trailing slash, ServeMux redirects that // subtree root without its trailing slash, Mux redirects that
// request to the subtree root (adding the trailing slash). This behavior can // request to the subtree root (adding the trailing slash). This behavior can
// be overridden with a separate registration for the path without // be overridden with a separate registration for the path without
// the trailing slash. For example, registering "/images/" causes ServeMux // the trailing slash. For example, registering "/images/" causes Mux
// to redirect a request for "/images" to "/images/", unless "/images" has // to redirect a request for "/images" to "/images/", unless "/images" has
// been registered separately. // been registered separately.
// //
// ServeMux also takes care of sanitizing the URL request path and // Mux also takes care of sanitizing the URL request path and
// redirecting any request containing . or .. elements or repeated slashes // redirecting any request containing . or .. elements or repeated slashes
// to an equivalent, cleaner URL. // to an equivalent, cleaner URL.
type ServeMux struct { type Mux struct {
mu sync.RWMutex mu sync.RWMutex
m map[muxKey]Handler m map[muxKey]Handler
es []muxEntry // slice of entries sorted from longest to shortest es []muxEntry // slice of entries sorted from longest to shortest
@@ -106,7 +110,7 @@ func cleanPath(p string) string {
// Find a handler on a handler map given a path string. // Find a handler on a handler map given a path string.
// Most-specific (longest) pattern wins. // Most-specific (longest) pattern wins.
func (mux *ServeMux) match(key muxKey) Handler { func (mux *Mux) match(key muxKey) Handler {
// Check for exact match first. // Check for exact match first.
if r, ok := mux.m[key]; ok { if r, ok := mux.m[key]; ok {
return r return r
@@ -134,7 +138,7 @@ func (mux *ServeMux) match(key muxKey) Handler {
// This occurs when a handler for path + "/" was already registered, but // This occurs when a handler for path + "/" was already registered, but
// not for path itself. If the path needs appending to, it creates a new // not for path itself. If the path needs appending to, it creates a new
// URL, setting the path to u.Path + "/" and returning true to indicate so. // URL, setting the path to u.Path + "/" and returning true to indicate so.
func (mux *ServeMux) redirectToPathSlash(key muxKey, u *url.URL) (*url.URL, bool) { func (mux *Mux) redirectToPathSlash(key muxKey, u *url.URL) (*url.URL, bool) {
mux.mu.RLock() mux.mu.RLock()
shouldRedirect := mux.shouldRedirectRLocked(key) shouldRedirect := mux.shouldRedirectRLocked(key)
mux.mu.RUnlock() mux.mu.RUnlock()
@@ -146,8 +150,8 @@ func (mux *ServeMux) redirectToPathSlash(key muxKey, u *url.URL) (*url.URL, bool
// shouldRedirectRLocked reports whether the given path and host should be redirected to // shouldRedirectRLocked reports whether the given path and host should be redirected to
// path+"/". This should happen if a handler is registered for path+"/" but // path+"/". This should happen if a handler is registered for path+"/" but
// not path -- see comments at ServeMux. // not path -- see comments at Mux.
func (mux *ServeMux) shouldRedirectRLocked(key muxKey) bool { func (mux *Mux) shouldRedirectRLocked(key muxKey) bool {
if _, exist := mux.m[key]; exist { if _, exist := mux.m[key]; exist {
return false return false
} }
@@ -177,7 +181,7 @@ func getWildcard(hostname string) (string, bool) {
// the path is not in its canonical form, the handler will be an // the path is not in its canonical form, the handler will be an
// internally-generated handler that redirects to the canonical path. If the // internally-generated handler that redirects to the canonical path. If the
// host contains a port, it is ignored when matching handlers. // host contains a port, it is ignored when matching handlers.
func (mux *ServeMux) Handler(r *Request) Handler { func (mux *Mux) Handler(r *Request) Handler {
scheme := r.URL.Scheme scheme := r.URL.Scheme
host := r.URL.Hostname() host := r.URL.Hostname()
path := cleanPath(r.URL.Path) path := cleanPath(r.URL.Path)
@@ -212,14 +216,14 @@ func (mux *ServeMux) Handler(r *Request) Handler {
// ServeGemini dispatches the request to the handler whose // ServeGemini dispatches the request to the handler whose
// pattern most closely matches the request URL. // pattern most closely matches the request URL.
func (mux *ServeMux) ServeGemini(ctx context.Context, w ResponseWriter, r *Request) { func (mux *Mux) ServeGemini(ctx context.Context, w ResponseWriter, r *Request) {
h := mux.Handler(r) h := mux.Handler(r)
h.ServeGemini(ctx, w, r) h.ServeGemini(ctx, w, r)
} }
// Handle registers the handler for the given pattern. // Handle registers the handler for the given pattern.
// If a handler already exists for pattern, Handle panics. // If a handler already exists for pattern, Handle panics.
func (mux *ServeMux) Handle(pattern string, handler Handler) { func (mux *Mux) Handle(pattern string, handler Handler) {
if pattern == "" { if pattern == "" {
panic("gemini: invalid pattern") panic("gemini: invalid pattern")
} }
@@ -294,6 +298,6 @@ func appendSorted(es []muxEntry, e muxEntry) []muxEntry {
} }
// HandleFunc registers the handler function for the given pattern. // HandleFunc registers the handler function for the given pattern.
func (mux *ServeMux) HandleFunc(pattern string, handler HandlerFunc) { func (mux *Mux) HandleFunc(pattern string, handler HandlerFunc) {
mux.Handle(pattern, handler) mux.Handle(pattern, handler)
} }

View File

@@ -10,7 +10,7 @@ type nopHandler struct{}
func (*nopHandler) ServeGemini(context.Context, ResponseWriter, *Request) {} func (*nopHandler) ServeGemini(context.Context, ResponseWriter, *Request) {}
func TestServeMuxMatch(t *testing.T) { func TestMuxMatch(t *testing.T) {
type Match struct { type Match struct {
URL string URL string
Ok bool Ok bool
@@ -292,7 +292,7 @@ func TestServeMuxMatch(t *testing.T) {
for i, test := range tests { for i, test := range tests {
h := &nopHandler{} h := &nopHandler{}
var mux ServeMux var mux Mux
mux.Handle(test.Pattern, h) mux.Handle(test.Pattern, h)
for _, match := range tests[i].Matches { for _, match := range tests[i].Matches {

View File

@@ -51,26 +51,25 @@ func NewRequest(rawurl string) (*Request, error) {
// for specialized applications; most code should use the Server // for specialized applications; most code should use the Server
// to read requests and handle them via the Handler interface. // to read requests and handle them via the Handler interface.
func ReadRequest(r io.Reader) (*Request, error) { func ReadRequest(r io.Reader) (*Request, error) {
// Read URL // Limit request size
r = io.LimitReader(r, 1026) r = io.LimitReader(r, 1026)
br := bufio.NewReaderSize(r, 1026) br := bufio.NewReaderSize(r, 1026)
rawurl, err := br.ReadString('\r') b, err := br.ReadBytes('\n')
if err != nil { if err != nil {
if err == io.EOF {
return nil, ErrInvalidRequest
}
return nil, err return nil, err
} }
// Read terminating line feed // Read URL
if b, err := br.ReadByte(); err != nil { rawurl, ok := trimCRLF(b)
return nil, err if !ok {
} else if b != '\n' {
return nil, ErrInvalidRequest return nil, ErrInvalidRequest
} }
// Trim carriage return if len(rawurl) == 0 {
rawurl = rawurl[:len(rawurl)-1]
// Validate URL
if len(rawurl) > 1024 {
return nil, ErrInvalidRequest return nil, ErrInvalidRequest
} }
u, err := url.Parse(rawurl) u, err := url.Parse(string(rawurl))
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -2,7 +2,6 @@ package gemini
import ( import (
"bufio" "bufio"
"io"
"net/url" "net/url"
"strings" "strings"
"testing" "testing"
@@ -36,25 +35,25 @@ func TestReadRequest(t *testing.T) {
}, },
{ {
Raw: "\r\n", Raw: "\r\n",
URL: &url.URL{}, Err: ErrInvalidRequest,
}, },
{ {
Raw: "gemini://example.com\n", Raw: "gemini://example.com\n",
Err: io.EOF, Err: ErrInvalidRequest,
}, },
{ {
Raw: "gemini://example.com", Raw: "gemini://example.com",
Err: io.EOF, Err: ErrInvalidRequest,
}, },
{ {
// 1030 bytes // 1030 bytes
Raw: maxURL + "xxxxxx", Raw: maxURL + "xxxxxx",
Err: io.EOF, Err: ErrInvalidRequest,
}, },
{ {
// 1027 bytes // 1027 bytes
Raw: maxURL + "x" + "\r\n", Raw: maxURL + "x" + "\r\n",
Err: io.EOF, Err: ErrInvalidRequest,
}, },
{ {
// 1024 bytes // 1024 bytes

View File

@@ -10,7 +10,7 @@ import (
) )
// The default media type for responses. // The default media type for responses.
const defaultMediaType = "text/gemini; charset=utf-8" const defaultMediaType = "text/gemini"
// Response represents the response from a Gemini request. // Response represents the response from a Gemini request.
// //
@@ -44,52 +44,56 @@ type Response struct {
// ReadResponse reads a Gemini response from the provided io.ReadCloser. // ReadResponse reads a Gemini response from the provided io.ReadCloser.
func ReadResponse(r io.ReadCloser) (*Response, error) { func ReadResponse(r io.ReadCloser) (*Response, error) {
resp := &Response{} resp := &Response{}
br := bufio.NewReader(r)
// Read the status // Limit response header size
statusB := make([]byte, 2) lr := io.LimitReader(r, 1029)
if _, err := br.Read(statusB); err != nil { // Wrap the reader to remove the limit later on
wr := &struct{ io.Reader }{lr}
br := bufio.NewReader(wr)
// Read response header
b, err := br.ReadBytes('\n')
if err != nil {
if err == io.EOF {
return nil, ErrInvalidResponse
}
return nil, err return nil, err
} }
status, err := strconv.Atoi(string(statusB)) if len(b) < 3 {
return nil, ErrInvalidResponse
}
// Read the status
status, err := strconv.Atoi(string(b[:2]))
if err != nil { if err != nil {
return nil, ErrInvalidResponse return nil, ErrInvalidResponse
} }
resp.Status = Status(status) resp.Status = Status(status)
// Read one space // Read one space
if b, err := br.ReadByte(); err != nil { if b[2] != ' ' {
return nil, err
} else if b != ' ' {
return nil, ErrInvalidResponse return nil, ErrInvalidResponse
} }
// Read the meta // Read the meta
meta, err := br.ReadString('\r') meta, ok := trimCRLF(b[3:])
if err != nil { if !ok {
return nil, err
}
// Trim carriage return
meta = meta[:len(meta)-1]
// Ensure meta is less than or equal to 1024 bytes
if len(meta) > 1024 {
return nil, ErrInvalidResponse return nil, ErrInvalidResponse
} }
if resp.Status.Class() == StatusSuccess && meta == "" { if len(meta) == 0 {
// Use default media type
meta = defaultMediaType
}
resp.Meta = meta
// Read terminating newline
if b, err := br.ReadByte(); err != nil {
return nil, err
} else if b != '\n' {
return nil, ErrInvalidResponse return nil, ErrInvalidResponse
} }
resp.Meta = string(meta)
if resp.Status.Class() == StatusSuccess { if resp.Status.Class() == StatusSuccess {
resp.Body = newBufReadCloser(br, r) // Use unlimited reader
wr.Reader = r
type readCloser struct {
io.Reader
io.Closer
}
resp.Body = readCloser{br, r}
} else { } else {
resp.Body = nopReadCloser{} resp.Body = nopReadCloser{}
r.Close() r.Close()
@@ -142,8 +146,8 @@ func (r *Response) WriteTo(w io.Writer) (int64, error) {
// has returned. // has returned.
type ResponseWriter interface { type ResponseWriter interface {
// SetMediaType sets the media type that will be sent by Write for a // SetMediaType sets the media type that will be sent by Write for a
// successful response. If no media type is set, a default of // successful response. If no media type is set, a default media type of
// "text/gemini; charset=utf-8" will be used. // "text/gemini" will be used.
// //
// Setting the media type after a call to Write or WriteHeader has // Setting the media type after a call to Write or WriteHeader has
// no effect. // no effect.
@@ -154,7 +158,7 @@ type ResponseWriter interface {
// If WriteHeader has not yet been called, Write calls WriteHeader with // If WriteHeader has not yet been called, Write calls WriteHeader with
// StatusSuccess and the media type set in SetMediaType before writing the data. // StatusSuccess and the media type set in SetMediaType before writing the data.
// If no media type was set, Write uses a default media type of // If no media type was set, Write uses a default media type of
// "text/gemini; charset=utf-8". // "text/gemini".
Write([]byte) (int, error) Write([]byte) (int, error)
// WriteHeader sends a Gemini response header with the provided // WriteHeader sends a Gemini response header with the provided
@@ -175,7 +179,6 @@ type ResponseWriter interface {
type responseWriter struct { type responseWriter struct {
bw *bufio.Writer bw *bufio.Writer
cl io.Closer
mediatype string mediatype string
wroteHeader bool wroteHeader bool
bodyAllowed bool bodyAllowed bool

View File

@@ -2,6 +2,7 @@ package gemini
import ( import (
"io" "io"
"io/ioutil"
"strings" "strings"
"testing" "testing"
) )
@@ -65,15 +66,15 @@ func TestReadWriteResponse(t *testing.T) {
}, },
{ {
Raw: "", Raw: "",
Err: io.EOF, Err: ErrInvalidResponse,
}, },
{ {
Raw: "10 Search query", Raw: "10 Search query",
Err: io.EOF, Err: ErrInvalidResponse,
}, },
{ {
Raw: "20 text/gemini\nHello, world!", Raw: "20 text/gemini\nHello, world!",
Err: io.EOF, Err: ErrInvalidResponse,
}, },
{ {
Raw: "20 text/gemini\rHello, world!", Raw: "20 text/gemini\rHello, world!",
@@ -81,7 +82,7 @@ func TestReadWriteResponse(t *testing.T) {
}, },
{ {
Raw: "20 text/gemini\r", Raw: "20 text/gemini\r",
Err: io.EOF, Err: ErrInvalidResponse,
}, },
{ {
Raw: "abcdefghijklmnopqrstuvwxyz", Raw: "abcdefghijklmnopqrstuvwxyz",
@@ -91,7 +92,7 @@ func TestReadWriteResponse(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Logf("%#v", test.Raw) t.Logf("%#v", test.Raw)
resp, err := ReadResponse(io.NopCloser(strings.NewReader(test.Raw))) resp, err := ReadResponse(ioutil.NopCloser(strings.NewReader(test.Raw)))
if err != test.Err { if err != test.Err {
t.Errorf("expected err = %v, got %v", test.Err, err) t.Errorf("expected err = %v, got %v", test.Err, err)
} }
@@ -105,7 +106,7 @@ func TestReadWriteResponse(t *testing.T) {
if resp.Meta != test.Meta { if resp.Meta != test.Meta {
t.Errorf("expected meta = %s, got %s", test.Meta, resp.Meta) t.Errorf("expected meta = %s, got %s", test.Meta, resp.Meta)
} }
b, _ := io.ReadAll(resp.Body) b, _ := ioutil.ReadAll(resp.Body)
body := string(b) body := string(b)
if body != test.Body { if body != test.Body {
t.Errorf("expected body = %#v, got %#v", test.Body, body) t.Errorf("expected body = %#v, got %#v", test.Body, body)

View File

@@ -6,6 +6,7 @@ type Status int
// Gemini status codes. // Gemini status codes.
const ( const (
StatusInput Status = 10 StatusInput Status = 10
StatusSensitiveInput Status = 11
StatusSuccess Status = 20 StatusSuccess Status = 20
StatusRedirect Status = 30 StatusRedirect Status = 30
StatusPermanentRedirect Status = 31 StatusPermanentRedirect Status = 31
@@ -36,6 +37,8 @@ func (s Status) String() string {
switch s { switch s {
case StatusInput: case StatusInput:
return "Input" return "Input"
case StatusSensitiveInput:
return "Sensitive input"
case StatusSuccess: case StatusSuccess:
return "Success" return "Success"
case StatusRedirect: case StatusRedirect: