Skip to content

Commit

Permalink
Merge pull request #8 from ninedraft/add-serve-tls
Browse files Browse the repository at this point in the history
Add serve tls
  • Loading branch information
ninedraft authored Mar 29, 2021
2 parents 0cd33d3 + 7645b45 commit 4ab505c
Show file tree
Hide file tree
Showing 8 changed files with 171 additions and 58 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@

# Dependency directories (remove the comment below to include it)
# vendor/

.vscode/
1 change: 1 addition & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ func (client *Client) dial(ctx context.Context, host string, cfg *tls.Config) (n
}
var tlsDialer = &tls.Dialer{
NetDialer: &net.Dialer{},
Config: cfg,
}
return tlsDialer.DialContext(ctx, "tcp", host)
}
Expand Down
55 changes: 55 additions & 0 deletions internal/bufwriter/bufwriter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Package bufwriter provides a buffered writer gadget.
package bufwriter

import (
"bufio"
"errors"
"io"

"github.com/ninedraft/gemax/internal/multierr"
)

// Writer is a buffered io.Writer wrapper.
type Writer struct {
isClosed bool
closer io.Closer
*writer
}

// DefaultBufferSize is used if buffers size is <= 0.
const DefaultBufferSize = 16 << 10 // 16 Kb

// New creates a new buffered writer.
// If bufSize <= 0, then DefaultBufferSize is used as internal buffer size.
func New(w io.WriteCloser, bufSize int) *Writer {
if bufSize <= 0 {
bufSize = DefaultBufferSize
}
return &Writer{
closer: w,
writer: bufio.NewWriterSize(w, bufSize),
}
}

type writer = bufio.Writer

// Reset buffer and sets new write target.
func (wr *Writer) Reset(w io.WriteCloser) {
wr.isClosed = false
wr.closer = w
wr.writer.Reset(w)
}

var errAlreadyClosed = errors.New("already closed")

// Close flushes and closes underlying writer.
func (wr *Writer) Close() error {
if wr.isClosed {
return errAlreadyClosed
}
wr.isClosed = true
return multierr.Combine(
wr.Flush(),
wr.closer.Close(),
)
}
59 changes: 15 additions & 44 deletions response.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
package gemax

import (
"bufio"
"errors"
"fmt"
"io"
"sync"

"github.com/ninedraft/gemax/internal/multierr"

"github.com/ninedraft/gemax/internal/bufwriter"
"github.com/ninedraft/gemax/status"
)

Expand All @@ -22,13 +20,12 @@ type responseWriter struct {
status status.Code
statusWritten bool
isClosed bool
dst *bufferedWriter
writer *bufwriter.Writer
}

func newResponseWriter(wr io.WriteCloser) *responseWriter {
return &responseWriter{
dst: newBufferedWriter(wr),
isClosed: false,
writer: newBufferedWriter(wr),
}
}

Expand All @@ -39,7 +36,7 @@ func (rw *responseWriter) WriteStatus(code status.Code, meta string) {
if code == status.Success && meta == "" {
meta = MIMEGemtext
}
_, _ = fmt.Fprintf(rw.dst, "%d %s\r\n", code, meta)
_, _ = fmt.Fprintf(rw.writer, "%d %s\r\n", code, meta)
rw.status = code
rw.statusWritten = true
}
Expand All @@ -49,15 +46,7 @@ func (rw *responseWriter) Write(data []byte) (int, error) {
return 0, io.ErrNoProgress
}
rw.WriteStatus(status.Success, MIMEGemtext)
return rw.dst.Write(data)
}

func (rw *responseWriter) WriteString(s string) (int, error) {
if rw.isClosed {
return 0, io.ErrNoProgress
}
rw.WriteStatus(status.Success, MIMEGemtext)
return rw.dst.WriteString(s)
return rw.writer.Write(data)
}

var errAlreadyClosed = errors.New("already closed")
Expand All @@ -68,45 +57,27 @@ func (rw *responseWriter) Close() error {
}
rw.WriteStatus(status.Success, MIMEGemtext)
rw.isClosed = true
var errClose = rw.dst.Close()
putBufferedWriter(rw.dst)
rw.dst = nil
var errClose = rw.writer.Close()
putBufferedWriter(rw.writer)
rw.writer = nil
return errClose
}

type bufferedWriter struct {
closer io.Closer
*bufio.Writer
}

func (wr *bufferedWriter) Close() error {
return multierr.Combine(
wr.Writer.Flush(),
wr.closer.Close(),
)
}

const writeBufferSize = 4 * 1024

var bufioWriterPool = &sync.Pool{
New: func() interface{} {
return bufio.NewWriterSize(nil, writeBufferSize)
return bufwriter.New(nil, writeBufferSize)
},
}

func newBufferedWriter(wr io.WriteCloser) *bufferedWriter {
var bwr = bufioWriterPool.Get().(*bufio.Writer)
func newBufferedWriter(wr io.WriteCloser) *bufwriter.Writer {
var bwr = bufioWriterPool.Get().(*bufwriter.Writer)
bwr.Reset(wr)
return &bufferedWriter{
closer: wr,
Writer: bwr,
}
return bwr
}

func putBufferedWriter(bwr *bufferedWriter) {
var wr = bwr.Writer
bwr.Writer = nil
bwr.closer = nil
wr.Reset(nil)
bufioWriterPool.Put(wr)
func putBufferedWriter(bwr *bufwriter.Writer) {
bwr.Reset(nil)
bufioWriterPool.Put(bwr)
}
22 changes: 20 additions & 2 deletions server.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package gemax

import (
"context"
"crypto/tls"
"fmt"
"net"
"sync"
Expand All @@ -14,6 +15,7 @@ type Handler func(ctx context.Context, rw ResponseWriter, req IncomingRequest)

// Server is gemini protocol server.
type Server struct {
Addr string
Handler Handler
ConnContext func(ctx context.Context, conn net.Conn) context.Context
Logf func(format string, args ...interface{})
Expand All @@ -23,14 +25,30 @@ type Server struct {
listeners map[net.Listener]struct{}
}

// Serve starts server on provided listener. Provided context will be passed to handlers.
func (server *Server) Serve(ctx context.Context, listener net.Listener) error {
func (server *Server) init() {
if server.conns == nil {
server.conns = map[*connTrack]struct{}{}
}
if server.listeners == nil {
server.listeners = map[net.Listener]struct{}{}
}
}

// ListenAndServe starts a TLS gemini server at specified server.
func (server *Server) ListenAndServe(ctx context.Context, tlsCfg *tls.Config) error {
server.init()
var listener, errListener = tls.Listen("tcp", server.Addr, tlsCfg)
if errListener != nil {
return fmt.Errorf("creating listener: %w", errListener)
}
server.addListener(listener)
defer ignoreErr(listener.Close)
return server.Serve(ctx, listener)
}

// Serve starts server on provided listener. Provided context will be passed to handlers.
func (server *Server) Serve(ctx context.Context, listener net.Listener) error {
server.init()
server.addListener(listener)
for {
var conn, errAccept = listener.Accept()
Expand Down
75 changes: 63 additions & 12 deletions server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package gemax_test

import (
"context"
"crypto/tls"
"errors"
"fmt"
"io"
Expand All @@ -15,23 +16,81 @@ import (
)

func TestServerSuccess(test *testing.T) {
var listener = setupEchoServer(test)
var listener, server = setupEchoServer(test)
defer func() { _ = listener.Close() }()
var ctx, cancel = context.WithCancel(context.Background())
test.Cleanup(cancel)
runTask(test, func() {
var err = server.Serve(ctx, listener)
if err != nil {
test.Logf("test server: Serve: %v", err)
}
})

var resp = listener.next(test.Name(), strings.NewReader("gemini://example.com/path"))

expectResponse(test, resp, "20 text/gemini\r\ngemini://example.com/path")
}

func TestServerBadRequest(test *testing.T) {
var listener = setupEchoServer(test)
var listener, server = setupEchoServer(test)
defer func() { _ = listener.Close() }()
var ctx, cancel = context.WithCancel(context.Background())
test.Cleanup(cancel)
runTask(test, func() {
var err = server.Serve(ctx, listener)
if err != nil {
test.Logf("test server: Serve: %v", err)
}
})

var resp = listener.next(test.Name(), strings.NewReader("invalid URL"))

expectResponse(test, resp, "59 "+status.Text(status.BadRequest)+"\r\n")
}

func setupEchoServer(t *testing.T) *fakeListener {
func TestListenAndServe(test *testing.T) {
var server = &gemax.Server{
Addr: "localhost:40423",
Logf: test.Logf,
Handler: func(ctx context.Context, rw gemax.ResponseWriter, req gemax.IncomingRequest) {
_, _ = io.WriteString(rw, "example text")
},
}
test.Logf("loading test certs")
var cert, errCert = tls.LoadX509KeyPair("testdata/cert.pem", "testdata/key.pem")
if errCert != nil {
test.Fatal(errCert)
}
var cfg = &tls.Config{
MinVersion: tls.VersionTLS12,
Certificates: []tls.Certificate{cert},
}
var ctx, cancel = context.WithCancel(context.Background())
test.Cleanup(cancel)
test.Logf("starting test server")
go func() {
test.Logf("test server: listening on %q", server.Addr)
var err = server.ListenAndServe(ctx, cfg)
if err != nil {
test.Logf("test server: Serve: %v", err)
}
}()
time.Sleep(time.Second)
var client = &gemax.Client{}
var resp, errFetch = client.Fetch(ctx, "gemini://"+server.Addr)
if errFetch != nil {
test.Error("fetching: ", errFetch)
return
}
defer func() { _ = resp.Close() }()

expectResponse(test, resp, "example text")
var data, errRead = io.ReadAll(resp)
test.Logf("%s / %v", data, errRead)
}

func setupEchoServer(t *testing.T) (*fakeListener, *gemax.Server) {
t.Helper()
var server = &gemax.Server{
Logf: t.Logf,
Expand All @@ -40,15 +99,7 @@ func setupEchoServer(t *testing.T) *fakeListener {
},
}
var listener = newListener(t.Name())
var ctx, cancel = context.WithCancel(context.Background())
t.Cleanup(cancel)
runTask(t, func() {
var err = server.Serve(ctx, listener)
if err != nil {
t.Logf("test server: Serve: %v", err)
}
})
return listener
return listener, server
}

func expectResponse(t *testing.T, got io.Reader, want string) {
Expand Down
10 changes: 10 additions & 0 deletions testdata/cert.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-----BEGIN CERTIFICATE-----
MIIBWzCCAQCgAwIBAgIBATAKBggqhkjOPQQDBDAQMQ4wDAYDVQQKEwVnZW1heDAe
Fw0yMTAzMTYxNDA0MDJaFw0zMTAzMTQxNDA0MDJaMBAxDjAMBgNVBAoTBWdlbWF4
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEyNzg8+oClgjoilsvQaDfaPtn8lXJ
57mUJzS92B4HrT5MXwS+6RzjzrOaXezk9oWHepsfyfLE7zNyGKzkb663RqNLMEkw
DgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQC
MAAwFAYDVR0RBA0wC4IJbG9jYWxob3N0MAoGCCqGSM49BAMEA0kAMEYCIQDHPwlp
MX2OA3yrqT5V6HlXtDlwy75WAqi50tXJAnJPygIhAICFq1OKKTxsWFlCQwWwss+/
F+EomW3F92wTWhxpyELh
-----END CERTIFICATE-----
5 changes: 5 additions & 0 deletions testdata/key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIDLP4kH0EwzuMHcXdvtuV4IXFyBbPH6leoicqS5gLKO3oAoGCCqGSM49
AwEHoUQDQgAEyNzg8+oClgjoilsvQaDfaPtn8lXJ57mUJzS92B4HrT5MXwS+6Rzj
zrOaXezk9oWHepsfyfLE7zNyGKzkb663Rg==
-----END EC PRIVATE KEY-----

0 comments on commit 4ab505c

Please sign in to comment.