Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21ad3a2ded | ||
|
|
2d7f28e152 | ||
|
|
1764e02d1e | ||
|
|
1bc5c68c3f | ||
|
|
867074d81b | ||
|
|
1da23ba07b | ||
|
|
cbfbeb6c22 | ||
|
|
c3418fdfed | ||
|
|
6181751e8d | ||
|
|
48c67bcead |
2
doc.go
2
doc.go
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
8
fs.go
@@ -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.
|
||||
//
|
||||
|
||||
@@ -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
2
go.mod
@@ -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
|
||||
|
||||
17
handler.go
17
handler.go
@@ -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()
|
||||
}
|
||||
|
||||
83
response.go
83
response.go
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
21
server.go
21
server.go
@@ -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{
|
||||
|
||||
Reference in New Issue
Block a user