10 Commits

Author SHA1 Message Date
Adnan Maolood
21ad3a2ded server: Disallow ServeConn usage after Shutdown 2021-02-24 19:25:52 -05:00
Adnan Maolood
2d7f28e152 Update examples/client.go 2021-02-24 19:21:31 -05:00
Adnan Maolood
1764e02d1e Remove ResponseWriter.Close method 2021-02-24 19:00:09 -05:00
Adnan Maolood
1bc5c68c3f response: Revert to using fields instead of methods 2021-02-24 18:50:40 -05:00
Adnan Maolood
867074d81b examples/client: Fix display of response status 2021-02-24 16:16:42 -05:00
Adnan Maolood
1da23ba07b Revert "Replace uses of ioutil with io"
This reverts commit 48c67bcead.
2021-02-24 14:45:57 -05:00
Adnan Maolood
cbfbeb6c22 Don't require Go 1.16 2021-02-24 14:29:29 -05:00
Adnan Maolood
c3418fdfed Add missing import 2021-02-24 14:28:47 -05:00
Adnan Maolood
6181751e8d Move mimetype registration to gemini.go 2021-02-24 14:27:49 -05:00
Adnan Maolood
48c67bcead Replace uses of ioutil with io 2021-02-24 11:11:10 -05:00
10 changed files with 69 additions and 97 deletions

2
doc.go
View File

@@ -9,7 +9,7 @@ Client is a Gemini client.
if err != nil {
// handle error
}
defer resp.Close()
defer resp.Body.Close()
// ...
Server is a Gemini server.

View File

@@ -103,9 +103,9 @@ func do(req *gemini.Request, via []*gemini.Request) (*gemini.Response, error) {
return resp, err
}
switch resp.Status().Class() {
switch resp.Status.Class() {
case gemini.StatusInput:
input, ok := getInput(resp.Meta(), resp.Status() == gemini.StatusSensitiveInput)
input, ok := getInput(resp.Meta, resp.Status == gemini.StatusSensitiveInput)
if !ok {
break
}
@@ -119,7 +119,7 @@ func do(req *gemini.Request, via []*gemini.Request) (*gemini.Response, error) {
return resp, errors.New("too many redirects")
}
target, err := url.Parse(resp.Meta())
target, err := url.Parse(resp.Meta)
if err != nil {
return resp, err
}
@@ -153,11 +153,11 @@ func main() {
fmt.Println(err)
os.Exit(1)
}
defer resp.Close()
defer resp.Body.Close()
// Handle response
if resp.Status().Class() == gemini.StatusSuccess {
_, err := io.Copy(os.Stdout, resp)
if resp.Status.Class() == gemini.StatusSuccess {
_, err := io.Copy(os.Stdout, resp.Body)
if err != nil {
log.Fatal(err)
}

View File

@@ -102,7 +102,3 @@ func (w *logResponseWriter) WriteHeader(status gemini.Status, meta string) {
func (w *logResponseWriter) Flush() error {
return nil
}
func (w *logResponseWriter) Close() error {
return nil
}

8
fs.go
View File

@@ -1,3 +1,5 @@
// +build go1.16
package gemini
import (
@@ -13,12 +15,6 @@ import (
"strings"
)
func init() {
// Add Gemini mime types
mime.AddExtensionType(".gmi", "text/gemini")
mime.AddExtensionType(".gemini", "text/gemini")
}
// FileServer returns a handler that serves Gemini requests with the contents
// of the provided file system.
//

View File

@@ -2,8 +2,15 @@ package gemini
import (
"errors"
"mime"
)
func init() {
// Add Gemini mime types
mime.AddExtensionType(".gmi", "text/gemini")
mime.AddExtensionType(".gemini", "text/gemini")
}
var crlf = []byte("\r\n")
// Errors.

2
go.mod
View File

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

View File

@@ -14,11 +14,10 @@ import (
// ServeGemini should write the response header and data to the ResponseWriter
// and then return. Returning signals that the request is finished; it is not
// valid to use the ResponseWriter after or concurrently with the completion
// of the ServeGemini call. Handlers may also call ResponseWriter.Close to
// manually close the connection.
// of the ServeGemini call.
//
// The provided context is canceled when the client's connection is closed,
// when ResponseWriter.Close is called, or when the ServeGemini method returns.
// The provided context is canceled when the client's connection is closed
// or the ServeGemini method returns.
//
// Handlers should not modify the provided Request.
type Handler interface {
@@ -100,7 +99,7 @@ func (t *timeoutHandler) ServeGemini(ctx context.Context, w ResponseWriter, r *R
buf := &bytes.Buffer{}
tw := &timeoutWriter{
wc: &contextWriter{
wr: &contextWriter{
ctx: ctx,
cancel: cancel,
done: ctx.Done(),
@@ -124,7 +123,7 @@ func (t *timeoutHandler) ServeGemini(ctx context.Context, w ResponseWriter, r *R
}
type timeoutWriter struct {
wc io.WriteCloser
wr io.Writer
status Status
meta string
mediatype string
@@ -139,7 +138,7 @@ func (w *timeoutWriter) Write(b []byte) (int, error) {
if !w.wroteHeader {
w.WriteHeader(StatusSuccess, w.mediatype)
}
return w.wc.Write(b)
return w.wr.Write(b)
}
func (w *timeoutWriter) WriteHeader(status Status, meta string) {
@@ -154,7 +153,3 @@ func (w *timeoutWriter) WriteHeader(status Status, meta string) {
func (w *timeoutWriter) Flush() error {
return nil
}
func (w *timeoutWriter) Close() error {
return w.wc.Close()
}

View File

@@ -15,24 +15,29 @@ const defaultMediaType = "text/gemini; charset=utf-8"
//
// The Client returns Responses from servers once the response
// header has been received. The response body is streamed on demand
// as the response is read. If the network connection fails or the server
// terminates the response, Read calls return an error.
//
// It is the caller's responsibility to close the response.
// as the Body field is read.
type Response struct {
status Status
meta string
body io.ReadCloser
conn net.Conn
}
// Status is the response status code.
Status Status
// NewResponse returns a new response with the provided status, meta, and body.
func NewResponse(status Status, meta string, body io.ReadCloser) *Response {
return &Response{
status: status,
meta: meta,
body: body,
}
// Meta returns the response meta.
// For successful responses, the meta should contain the media type of the response.
// For failure responses, the meta should contain a short description of the failure.
Meta string
// Body represents the response body.
//
// The response body is streamed on demand as the Body field
// is read. If the network connection fails or the server
// terminates the response, Body.Read calls return an error.
//
// The Gemini client guarantees that Body is always
// non-nil, even on responses without a body or responses with
// a zero-length body. It is the caller's responsibility to
// close Body.
Body io.ReadCloser
conn net.Conn
}
// ReadResponse reads a Gemini response from the provided io.ReadCloser.
@@ -49,7 +54,7 @@ func ReadResponse(r io.ReadCloser) (*Response, error) {
if err != nil {
return nil, ErrInvalidResponse
}
resp.status = Status(status)
resp.Status = Status(status)
// Read one space
if b, err := br.ReadByte(); err != nil {
@@ -69,11 +74,11 @@ func ReadResponse(r io.ReadCloser) (*Response, error) {
if len(meta) > 1024 {
return nil, ErrInvalidResponse
}
if resp.status.Class() == StatusSuccess && meta == "" {
if resp.Status.Class() == StatusSuccess && meta == "" {
// Use default media type
meta = defaultMediaType
}
resp.meta = meta
resp.Meta = meta
// Read terminating newline
if b, err := br.ReadByte(); err != nil {
@@ -82,38 +87,15 @@ func ReadResponse(r io.ReadCloser) (*Response, error) {
return nil, ErrInvalidResponse
}
if resp.status.Class() == StatusSuccess {
resp.body = newBufReadCloser(br, r)
if resp.Status.Class() == StatusSuccess {
resp.Body = newBufReadCloser(br, r)
} else {
resp.body = nopReadCloser{}
resp.Body = nopReadCloser{}
r.Close()
}
return resp, nil
}
// Status returns the response status code.
func (r *Response) Status() Status {
return r.status
}
// Meta returns the response meta.
// For successful responses, the meta should contain the media type of the response.
// For failure responses, the meta should contain a short description of the failure.
func (r *Response) Meta() string {
return r.meta
}
// Read reads data from the response body.
// The response body is streamed on demand as Read is called.
func (r *Response) Read(p []byte) (n int, err error) {
return r.body.Read(p)
}
// Close closes the response body.
func (r *Response) Close() error {
return r.body.Close()
}
// Conn returns the network connection on which the response was received.
func (r *Response) Conn() net.Conn {
return r.conn
@@ -165,10 +147,6 @@ type ResponseWriter interface {
// Flush sends any buffered data to the client.
Flush() error
// Close closes the connection.
// Any blocked Write operations will be unblocked and return errors.
Close() error
}
type responseWriter struct {
@@ -179,10 +157,9 @@ type responseWriter struct {
bodyAllowed bool
}
func newResponseWriter(w io.WriteCloser) *responseWriter {
func newResponseWriter(w io.Writer) *responseWriter {
return &responseWriter{
bw: bufio.NewWriter(w),
cl: w,
}
}
@@ -228,7 +205,3 @@ func (w *responseWriter) Flush() error {
// Write errors from WriteHeader will be returned here.
return w.bw.Flush()
}
func (w *responseWriter) Close() error {
return w.cl.Close()
}

View File

@@ -91,13 +91,13 @@ func TestReadWriteResponse(t *testing.T) {
// No response
continue
}
if resp.status != test.Status {
t.Errorf("expected status = %d, got %d", test.Status, resp.status)
if resp.Status != test.Status {
t.Errorf("expected status = %d, got %d", test.Status, resp.Status)
}
if resp.meta != test.Meta {
t.Errorf("expected meta = %s, got %s", test.Meta, resp.meta)
if resp.Meta != test.Meta {
t.Errorf("expected meta = %s, got %s", test.Meta, resp.Meta)
}
b, _ := ioutil.ReadAll(resp.body)
b, _ := ioutil.ReadAll(resp.Body)
body := string(b)
if body != test.Body {
t.Errorf("expected body = %#v, got %#v", test.Body, body)

View File

@@ -282,14 +282,17 @@ func (srv *Server) serve(ctx context.Context, l net.Listener) error {
return err
}
tempDelay = 0
go srv.ServeConn(ctx, rw)
go srv.serveConn(ctx, rw, false)
}
}
func (srv *Server) trackConn(conn *net.Conn, cancel context.CancelFunc) bool {
func (srv *Server) trackConn(conn *net.Conn, cancel context.CancelFunc, external bool) bool {
srv.mu.Lock()
defer srv.mu.Unlock()
if srv.closed && !srv.shutdown {
// Reject the connection under the following conditions:
// - Shutdown or Close has been called and conn is external (from ServeConn)
// - Close (not Shutdown) has been called and conn is internal (from Serve)
if srv.closed && (external || !srv.shutdown) {
return false
}
if srv.conns == nil {
@@ -309,15 +312,17 @@ func (srv *Server) deleteConn(conn *net.Conn) {
// It closes the connection when the response has been completed.
// If the provided context expires before the response has completed,
// ServeConn closes the connection and returns the context's error.
//
// Note that ServeConn can be used during a Shutdown.
func (srv *Server) ServeConn(ctx context.Context, conn net.Conn) error {
return srv.serveConn(ctx, conn, true)
}
func (srv *Server) serveConn(ctx context.Context, conn net.Conn, external bool) error {
defer conn.Close()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
if !srv.trackConn(&conn, cancel) {
if !srv.trackConn(&conn, cancel, external) {
return context.Canceled
}
defer srv.tryCloseDone()
@@ -332,7 +337,7 @@ func (srv *Server) ServeConn(ctx context.Context, conn net.Conn) error {
errch := make(chan error, 1)
go func() {
errch <- srv.serveConn(ctx, conn)
errch <- srv.goServeConn(ctx, conn)
}()
select {
@@ -343,7 +348,7 @@ func (srv *Server) ServeConn(ctx context.Context, conn net.Conn) error {
}
}
func (srv *Server) serveConn(ctx context.Context, conn net.Conn) error {
func (srv *Server) goServeConn(ctx context.Context, conn net.Conn) error {
ctx, cancel := context.WithCancel(ctx)
done := ctx.Done()
cw := &contextWriter{