Skip to content

Commit 4ab505c

Browse files
authored
Merge pull request #8 from ninedraft/add-serve-tls
Add serve tls
2 parents 0cd33d3 + 7645b45 commit 4ab505c

File tree

8 files changed

+171
-58
lines changed

8 files changed

+171
-58
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,5 @@
1313

1414
# Dependency directories (remove the comment below to include it)
1515
# vendor/
16+
17+
.vscode/

client.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ func (client *Client) dial(ctx context.Context, host string, cfg *tls.Config) (n
7878
}
7979
var tlsDialer = &tls.Dialer{
8080
NetDialer: &net.Dialer{},
81+
Config: cfg,
8182
}
8283
return tlsDialer.DialContext(ctx, "tcp", host)
8384
}

internal/bufwriter/bufwriter.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Package bufwriter provides a buffered writer gadget.
2+
package bufwriter
3+
4+
import (
5+
"bufio"
6+
"errors"
7+
"io"
8+
9+
"github.com/ninedraft/gemax/internal/multierr"
10+
)
11+
12+
// Writer is a buffered io.Writer wrapper.
13+
type Writer struct {
14+
isClosed bool
15+
closer io.Closer
16+
*writer
17+
}
18+
19+
// DefaultBufferSize is used if buffers size is <= 0.
20+
const DefaultBufferSize = 16 << 10 // 16 Kb
21+
22+
// New creates a new buffered writer.
23+
// If bufSize <= 0, then DefaultBufferSize is used as internal buffer size.
24+
func New(w io.WriteCloser, bufSize int) *Writer {
25+
if bufSize <= 0 {
26+
bufSize = DefaultBufferSize
27+
}
28+
return &Writer{
29+
closer: w,
30+
writer: bufio.NewWriterSize(w, bufSize),
31+
}
32+
}
33+
34+
type writer = bufio.Writer
35+
36+
// Reset buffer and sets new write target.
37+
func (wr *Writer) Reset(w io.WriteCloser) {
38+
wr.isClosed = false
39+
wr.closer = w
40+
wr.writer.Reset(w)
41+
}
42+
43+
var errAlreadyClosed = errors.New("already closed")
44+
45+
// Close flushes and closes underlying writer.
46+
func (wr *Writer) Close() error {
47+
if wr.isClosed {
48+
return errAlreadyClosed
49+
}
50+
wr.isClosed = true
51+
return multierr.Combine(
52+
wr.Flush(),
53+
wr.closer.Close(),
54+
)
55+
}

response.go

Lines changed: 15 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
package gemax
22

33
import (
4-
"bufio"
54
"errors"
65
"fmt"
76
"io"
87
"sync"
98

10-
"github.com/ninedraft/gemax/internal/multierr"
11-
9+
"github.com/ninedraft/gemax/internal/bufwriter"
1210
"github.com/ninedraft/gemax/status"
1311
)
1412

@@ -22,13 +20,12 @@ type responseWriter struct {
2220
status status.Code
2321
statusWritten bool
2422
isClosed bool
25-
dst *bufferedWriter
23+
writer *bufwriter.Writer
2624
}
2725

2826
func newResponseWriter(wr io.WriteCloser) *responseWriter {
2927
return &responseWriter{
30-
dst: newBufferedWriter(wr),
31-
isClosed: false,
28+
writer: newBufferedWriter(wr),
3229
}
3330
}
3431

@@ -39,7 +36,7 @@ func (rw *responseWriter) WriteStatus(code status.Code, meta string) {
3936
if code == status.Success && meta == "" {
4037
meta = MIMEGemtext
4138
}
42-
_, _ = fmt.Fprintf(rw.dst, "%d %s\r\n", code, meta)
39+
_, _ = fmt.Fprintf(rw.writer, "%d %s\r\n", code, meta)
4340
rw.status = code
4441
rw.statusWritten = true
4542
}
@@ -49,15 +46,7 @@ func (rw *responseWriter) Write(data []byte) (int, error) {
4946
return 0, io.ErrNoProgress
5047
}
5148
rw.WriteStatus(status.Success, MIMEGemtext)
52-
return rw.dst.Write(data)
53-
}
54-
55-
func (rw *responseWriter) WriteString(s string) (int, error) {
56-
if rw.isClosed {
57-
return 0, io.ErrNoProgress
58-
}
59-
rw.WriteStatus(status.Success, MIMEGemtext)
60-
return rw.dst.WriteString(s)
49+
return rw.writer.Write(data)
6150
}
6251

6352
var errAlreadyClosed = errors.New("already closed")
@@ -68,45 +57,27 @@ func (rw *responseWriter) Close() error {
6857
}
6958
rw.WriteStatus(status.Success, MIMEGemtext)
7059
rw.isClosed = true
71-
var errClose = rw.dst.Close()
72-
putBufferedWriter(rw.dst)
73-
rw.dst = nil
60+
var errClose = rw.writer.Close()
61+
putBufferedWriter(rw.writer)
62+
rw.writer = nil
7463
return errClose
7564
}
7665

77-
type bufferedWriter struct {
78-
closer io.Closer
79-
*bufio.Writer
80-
}
81-
82-
func (wr *bufferedWriter) Close() error {
83-
return multierr.Combine(
84-
wr.Writer.Flush(),
85-
wr.closer.Close(),
86-
)
87-
}
88-
8966
const writeBufferSize = 4 * 1024
9067

9168
var bufioWriterPool = &sync.Pool{
9269
New: func() interface{} {
93-
return bufio.NewWriterSize(nil, writeBufferSize)
70+
return bufwriter.New(nil, writeBufferSize)
9471
},
9572
}
9673

97-
func newBufferedWriter(wr io.WriteCloser) *bufferedWriter {
98-
var bwr = bufioWriterPool.Get().(*bufio.Writer)
74+
func newBufferedWriter(wr io.WriteCloser) *bufwriter.Writer {
75+
var bwr = bufioWriterPool.Get().(*bufwriter.Writer)
9976
bwr.Reset(wr)
100-
return &bufferedWriter{
101-
closer: wr,
102-
Writer: bwr,
103-
}
77+
return bwr
10478
}
10579

106-
func putBufferedWriter(bwr *bufferedWriter) {
107-
var wr = bwr.Writer
108-
bwr.Writer = nil
109-
bwr.closer = nil
110-
wr.Reset(nil)
111-
bufioWriterPool.Put(wr)
80+
func putBufferedWriter(bwr *bufwriter.Writer) {
81+
bwr.Reset(nil)
82+
bufioWriterPool.Put(bwr)
11283
}

server.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package gemax
22

33
import (
44
"context"
5+
"crypto/tls"
56
"fmt"
67
"net"
78
"sync"
@@ -14,6 +15,7 @@ type Handler func(ctx context.Context, rw ResponseWriter, req IncomingRequest)
1415

1516
// Server is gemini protocol server.
1617
type Server struct {
18+
Addr string
1719
Handler Handler
1820
ConnContext func(ctx context.Context, conn net.Conn) context.Context
1921
Logf func(format string, args ...interface{})
@@ -23,14 +25,30 @@ type Server struct {
2325
listeners map[net.Listener]struct{}
2426
}
2527

26-
// Serve starts server on provided listener. Provided context will be passed to handlers.
27-
func (server *Server) Serve(ctx context.Context, listener net.Listener) error {
28+
func (server *Server) init() {
2829
if server.conns == nil {
2930
server.conns = map[*connTrack]struct{}{}
3031
}
3132
if server.listeners == nil {
3233
server.listeners = map[net.Listener]struct{}{}
3334
}
35+
}
36+
37+
// ListenAndServe starts a TLS gemini server at specified server.
38+
func (server *Server) ListenAndServe(ctx context.Context, tlsCfg *tls.Config) error {
39+
server.init()
40+
var listener, errListener = tls.Listen("tcp", server.Addr, tlsCfg)
41+
if errListener != nil {
42+
return fmt.Errorf("creating listener: %w", errListener)
43+
}
44+
server.addListener(listener)
45+
defer ignoreErr(listener.Close)
46+
return server.Serve(ctx, listener)
47+
}
48+
49+
// Serve starts server on provided listener. Provided context will be passed to handlers.
50+
func (server *Server) Serve(ctx context.Context, listener net.Listener) error {
51+
server.init()
3452
server.addListener(listener)
3553
for {
3654
var conn, errAccept = listener.Accept()

server_test.go

Lines changed: 63 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package gemax_test
22

33
import (
44
"context"
5+
"crypto/tls"
56
"errors"
67
"fmt"
78
"io"
@@ -15,23 +16,81 @@ import (
1516
)
1617

1718
func TestServerSuccess(test *testing.T) {
18-
var listener = setupEchoServer(test)
19+
var listener, server = setupEchoServer(test)
1920
defer func() { _ = listener.Close() }()
21+
var ctx, cancel = context.WithCancel(context.Background())
22+
test.Cleanup(cancel)
23+
runTask(test, func() {
24+
var err = server.Serve(ctx, listener)
25+
if err != nil {
26+
test.Logf("test server: Serve: %v", err)
27+
}
28+
})
2029

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

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

2635
func TestServerBadRequest(test *testing.T) {
27-
var listener = setupEchoServer(test)
36+
var listener, server = setupEchoServer(test)
2837
defer func() { _ = listener.Close() }()
38+
var ctx, cancel = context.WithCancel(context.Background())
39+
test.Cleanup(cancel)
40+
runTask(test, func() {
41+
var err = server.Serve(ctx, listener)
42+
if err != nil {
43+
test.Logf("test server: Serve: %v", err)
44+
}
45+
})
2946

3047
var resp = listener.next(test.Name(), strings.NewReader("invalid URL"))
48+
3149
expectResponse(test, resp, "59 "+status.Text(status.BadRequest)+"\r\n")
3250
}
3351

34-
func setupEchoServer(t *testing.T) *fakeListener {
52+
func TestListenAndServe(test *testing.T) {
53+
var server = &gemax.Server{
54+
Addr: "localhost:40423",
55+
Logf: test.Logf,
56+
Handler: func(ctx context.Context, rw gemax.ResponseWriter, req gemax.IncomingRequest) {
57+
_, _ = io.WriteString(rw, "example text")
58+
},
59+
}
60+
test.Logf("loading test certs")
61+
var cert, errCert = tls.LoadX509KeyPair("testdata/cert.pem", "testdata/key.pem")
62+
if errCert != nil {
63+
test.Fatal(errCert)
64+
}
65+
var cfg = &tls.Config{
66+
MinVersion: tls.VersionTLS12,
67+
Certificates: []tls.Certificate{cert},
68+
}
69+
var ctx, cancel = context.WithCancel(context.Background())
70+
test.Cleanup(cancel)
71+
test.Logf("starting test server")
72+
go func() {
73+
test.Logf("test server: listening on %q", server.Addr)
74+
var err = server.ListenAndServe(ctx, cfg)
75+
if err != nil {
76+
test.Logf("test server: Serve: %v", err)
77+
}
78+
}()
79+
time.Sleep(time.Second)
80+
var client = &gemax.Client{}
81+
var resp, errFetch = client.Fetch(ctx, "gemini://"+server.Addr)
82+
if errFetch != nil {
83+
test.Error("fetching: ", errFetch)
84+
return
85+
}
86+
defer func() { _ = resp.Close() }()
87+
88+
expectResponse(test, resp, "example text")
89+
var data, errRead = io.ReadAll(resp)
90+
test.Logf("%s / %v", data, errRead)
91+
}
92+
93+
func setupEchoServer(t *testing.T) (*fakeListener, *gemax.Server) {
3594
t.Helper()
3695
var server = &gemax.Server{
3796
Logf: t.Logf,
@@ -40,15 +99,7 @@ func setupEchoServer(t *testing.T) *fakeListener {
4099
},
41100
}
42101
var listener = newListener(t.Name())
43-
var ctx, cancel = context.WithCancel(context.Background())
44-
t.Cleanup(cancel)
45-
runTask(t, func() {
46-
var err = server.Serve(ctx, listener)
47-
if err != nil {
48-
t.Logf("test server: Serve: %v", err)
49-
}
50-
})
51-
return listener
102+
return listener, server
52103
}
53104

54105
func expectResponse(t *testing.T, got io.Reader, want string) {

testdata/cert.pem

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIBWzCCAQCgAwIBAgIBATAKBggqhkjOPQQDBDAQMQ4wDAYDVQQKEwVnZW1heDAe
3+
Fw0yMTAzMTYxNDA0MDJaFw0zMTAzMTQxNDA0MDJaMBAxDjAMBgNVBAoTBWdlbWF4
4+
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEyNzg8+oClgjoilsvQaDfaPtn8lXJ
5+
57mUJzS92B4HrT5MXwS+6RzjzrOaXezk9oWHepsfyfLE7zNyGKzkb663RqNLMEkw
6+
DgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQC
7+
MAAwFAYDVR0RBA0wC4IJbG9jYWxob3N0MAoGCCqGSM49BAMEA0kAMEYCIQDHPwlp
8+
MX2OA3yrqT5V6HlXtDlwy75WAqi50tXJAnJPygIhAICFq1OKKTxsWFlCQwWwss+/
9+
F+EomW3F92wTWhxpyELh
10+
-----END CERTIFICATE-----

testdata/key.pem

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-----BEGIN EC PRIVATE KEY-----
2+
MHcCAQEEIDLP4kH0EwzuMHcXdvtuV4IXFyBbPH6leoicqS5gLKO3oAoGCCqGSM49
3+
AwEHoUQDQgAEyNzg8+oClgjoilsvQaDfaPtn8lXJ57mUJzS92B4HrT5MXwS+6Rzj
4+
zrOaXezk9oWHepsfyfLE7zNyGKzkb663Rg==
5+
-----END EC PRIVATE KEY-----

0 commit comments

Comments
 (0)