Skip to content

Commit 5829d17

Browse files
author
Sergio Andres Virviescas Santana
committed
Add Atreugo.NewVirtualHost
1 parent 6b40c09 commit 5829d17

File tree

4 files changed

+269
-45
lines changed

4 files changed

+269
-45
lines changed

atreugo.go

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ func New(cfg Config) *Atreugo {
6161
}
6262

6363
server := &Atreugo{
64-
server: newFasthttpServer(cfg, r.router.Handler, log),
64+
server: newFasthttpServer(cfg, log),
6565
log: log,
6666
cfg: cfg,
6767
Router: r,
@@ -70,14 +70,9 @@ func New(cfg Config) *Atreugo {
7070
return server
7171
}
7272

73-
func newFasthttpServer(cfg Config, handler fasthttp.RequestHandler, log fasthttp.Logger) *fasthttp.Server {
74-
if cfg.Compress {
75-
handler = fasthttp.CompressHandler(handler)
76-
}
77-
73+
func newFasthttpServer(cfg Config, log fasthttp.Logger) *fasthttp.Server {
7874
return &fasthttp.Server{
7975
Name: cfg.Name,
80-
Handler: handler,
8176
HeaderReceived: cfg.HeaderReceived,
8277
Concurrency: cfg.Concurrency,
8378
DisableKeepalive: cfg.DisableKeepalive,
@@ -104,6 +99,28 @@ func newFasthttpServer(cfg Config, handler fasthttp.RequestHandler, log fasthttp
10499
}
105100
}
106101

102+
func (s *Atreugo) handler() fasthttp.RequestHandler {
103+
handler := s.router.Handler
104+
105+
if len(s.virtualHosts) > 0 {
106+
handler = func(ctx *fasthttp.RequestCtx) {
107+
hostname := gotils.B2S(ctx.URI().Host())
108+
109+
if h := s.virtualHosts[hostname]; h != nil {
110+
h(ctx)
111+
} else {
112+
s.router.Handler(ctx)
113+
}
114+
}
115+
}
116+
117+
if s.cfg.Compress {
118+
handler = fasthttp.CompressHandler(handler)
119+
}
120+
121+
return handler
122+
}
123+
107124
// SaveMatchedRoutePath if enabled, adds the matched route path onto the ctx.UserValue context
108125
// before invoking the handler.
109126
// The matched route path is only added to handlers of routes that were
@@ -184,6 +201,7 @@ func (s *Atreugo) Serve(ln net.Listener) error {
184201

185202
s.cfg.Addr = ln.Addr().String()
186203
s.cfg.Network = ln.Addr().Network()
204+
s.server.Handler = s.handler()
187205

188206
if gotils.StringSliceInclude(tcpNetworks, s.cfg.Network) {
189207
schema := "http"
@@ -207,3 +225,29 @@ func (s *Atreugo) Serve(ln net.Listener) error {
207225
func (s *Atreugo) SetLogOutput(output io.Writer) {
208226
s.log.SetOutput(output)
209227
}
228+
229+
// NewVirtualHost returns a new sub-router for running more than one web site
230+
// (such as company1.example.com and company2.example.com) on a single atreugo instance.
231+
// Virtual hosts can be "IP-based", meaning that you have a different IP address
232+
// for every web site, or "name-based", meaning that you have multiple names
233+
// running on each IP address.
234+
//
235+
// The fact that they are running on the same atreugo instance is not apparent to the end user.
236+
func (s *Atreugo) NewVirtualHost(hostname string) *Router {
237+
if s.virtualHosts == nil {
238+
s.virtualHosts = make(map[string]fasthttp.RequestHandler)
239+
}
240+
241+
vHost := newRouter(s.log, s.cfg.ErrorView)
242+
vHost.router.NotFound = s.router.NotFound
243+
vHost.router.MethodNotAllowed = s.router.MethodNotAllowed
244+
vHost.router.PanicHandler = s.router.PanicHandler
245+
246+
if s.virtualHosts[hostname] != nil {
247+
panicf("a router is already registered for virtual host '%s'", hostname)
248+
}
249+
250+
s.virtualHosts[hostname] = vHost.router.Handler
251+
252+
return vHost
253+
}

atreugo_test.go

Lines changed: 211 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ package atreugo
33
import (
44
"bytes"
55
"errors"
6+
"fmt"
7+
"math/rand"
8+
"net"
69
"reflect"
710
"testing"
811
"time"
@@ -160,75 +163,174 @@ func Test_New(t *testing.T) { //nolint:funlen,gocognit
160163
}
161164

162165
func Test_newFasthttpServer(t *testing.T) { //nolint:funlen
163-
type args struct {
164-
compress bool
166+
cfg := Config{
167+
Name: "test",
168+
HeaderReceived: func(header *fasthttp.RequestHeader) fasthttp.RequestConfig {
169+
return fasthttp.RequestConfig{}
170+
},
171+
Concurrency: rand.Int(), // nolint:gosec
172+
DisableKeepalive: true,
173+
ReadBufferSize: rand.Int(), // nolint:gosec
174+
WriteBufferSize: rand.Int(), // nolint:gosec
175+
ReadTimeout: time.Duration(rand.Int()), // nolint:gosec
176+
WriteTimeout: time.Duration(rand.Int()), // nolint:gosec
177+
IdleTimeout: time.Duration(rand.Int()), // nolint:gosec
178+
MaxConnsPerIP: rand.Int(), // nolint:gosec
179+
MaxRequestsPerConn: rand.Int(), // nolint:gosec
180+
MaxRequestBodySize: rand.Int(), // nolint:gosec
181+
ReduceMemoryUsage: true,
182+
GetOnly: true,
183+
DisablePreParseMultipartForm: true,
184+
LogAllErrors: true,
185+
DisableHeaderNamesNormalizing: true,
186+
SleepWhenConcurrencyLimitsExceeded: time.Duration(rand.Int()), // nolint:gosec
187+
NoDefaultServerHeader: true,
188+
NoDefaultDate: true,
189+
NoDefaultContentType: true,
190+
ConnState: func(net.Conn, fasthttp.ConnState) {},
191+
KeepHijackedConns: true,
165192
}
166193

167-
type want struct {
168-
compress bool
194+
srv := newFasthttpServer(cfg, testLog)
195+
196+
if srv == nil {
197+
t.Fatal("newFasthttpServer() == nil")
198+
}
199+
200+
fasthttpServerType := reflect.TypeOf(fasthttp.Server{})
201+
configType := reflect.TypeOf(Config{})
202+
203+
fasthttpServerValue := reflect.ValueOf(*srv) // nolint:govet
204+
configValue := reflect.ValueOf(cfg)
205+
206+
for i := 0; i < fasthttpServerType.NumField(); i++ {
207+
field := fasthttpServerType.Field(i)
208+
209+
if !unicode.IsUpper(rune(field.Name[0])) { // Check if the field is public
210+
continue
211+
} else if gotils.StringSliceInclude(notConfigFasthttpFields, field.Name) {
212+
continue
213+
}
214+
215+
_, exist := configType.FieldByName(field.Name)
216+
if !exist {
217+
t.Errorf("The field '%s' does not exist in atreugo.Config", field.Name)
218+
}
219+
220+
v1 := fmt.Sprint(fasthttpServerValue.FieldByName(field.Name).Interface())
221+
v2 := fmt.Sprint(configValue.FieldByName(field.Name).Interface())
222+
223+
if v1 != v2 {
224+
t.Errorf("fasthttp.Server.%s == %s, want %s", field.Name, v1, v2)
225+
}
226+
}
227+
228+
if srv.Handler != nil {
229+
t.Error("fasthttp.Server.Handler must be nil")
230+
}
231+
232+
if !isEqual(srv.Logger, testLog) {
233+
t.Errorf("fasthttp.Server.Logger == %p, want %p", srv.Logger, testLog)
234+
}
235+
}
236+
237+
func TestAtreugo_handler(t *testing.T) { // nolint:funlen,gocognit
238+
type args struct {
239+
cfg Config
240+
hosts []string
169241
}
170242

171243
tests := []struct {
172244
name string
173245
args args
174-
want want
175246
}{
176247
{
177-
name: "NotCompress",
248+
name: "Default",
178249
args: args{
179-
compress: false,
180-
},
181-
want: want{
182-
compress: false,
250+
cfg: Config{},
183251
},
184252
},
185253
{
186254
name: "Compress",
187255
args: args{
188-
compress: true,
256+
cfg: Config{Compress: true},
189257
},
190-
want: want{
191-
compress: true,
258+
},
259+
{
260+
name: "MultiHost",
261+
args: args{
262+
cfg: Config{},
263+
hosts: []string{"localhost", "example.com"},
264+
},
265+
},
266+
{
267+
name: "MultiHostCompress",
268+
args: args{
269+
cfg: Config{Compress: true},
270+
hosts: []string{"localhost", "example.com"},
192271
},
193272
},
194273
}
195274

196-
handler := func(ctx *fasthttp.RequestCtx) {}
197-
198275
for _, test := range tests {
199276
tt := test
200277

201278
t.Run(tt.name, func(t *testing.T) {
202-
cfg := Config{
203-
LogLevel: "fatal",
204-
Compress: tt.args.compress,
279+
testView := func(ctx *RequestCtx) error {
280+
return ctx.JSONResponse(JSON{"data": gotils.RandBytes(make([]byte, 300))})
205281
}
206-
srv := newFasthttpServer(cfg, handler, testLog)
282+
testPath := "/"
283+
284+
s := New(tt.args.cfg)
285+
s.GET(testPath, testView)
207286

208-
if (reflect.ValueOf(handler).Pointer() == reflect.ValueOf(srv.Handler).Pointer()) == tt.want.compress {
209-
t.Error("The handler has not been wrapped by compression handler")
287+
for _, hostname := range tt.args.hosts {
288+
vHost := s.NewVirtualHost(hostname)
289+
vHost.GET(testPath, testView)
210290
}
211-
})
212-
}
213-
}
214291

215-
func TestAtreugo_ConfigFasthttpFields(t *testing.T) {
216-
fasthttpServerType := reflect.TypeOf(fasthttp.Server{})
217-
configType := reflect.TypeOf(Config{})
292+
handler := s.handler()
218293

219-
for i := 0; i < fasthttpServerType.NumField(); i++ {
220-
field := fasthttpServerType.Field(i)
294+
if handler == nil {
295+
t.Errorf("handler is nil")
296+
}
221297

222-
if !unicode.IsUpper(rune(field.Name[0])) { // Check if the field is public
223-
continue
224-
} else if gotils.StringSliceInclude(notConfigFasthttpFields, field.Name) {
225-
continue
226-
}
298+
newHostname := string(gotils.RandBytes(make([]byte, 10))) + ".com"
227299

228-
_, exist := configType.FieldByName(field.Name)
229-
if !exist {
230-
t.Errorf("The field '%s' does not exist in atreugo.Config", field.Name)
231-
}
300+
hosts := tt.args.hosts
301+
hosts = append(hosts, newHostname)
302+
303+
for _, hostname := range hosts {
304+
for _, path := range []string{testPath, "/notfound"} {
305+
ctx := new(fasthttp.RequestCtx)
306+
ctx.Request.Header.Set(fasthttp.HeaderAcceptEncoding, "gzip")
307+
ctx.Request.Header.Set(fasthttp.HeaderHost, hostname)
308+
ctx.Request.URI().SetHost(hostname)
309+
ctx.Request.SetRequestURI(path)
310+
311+
handler(ctx)
312+
313+
statusCode := ctx.Response.StatusCode()
314+
wantStatusCode := fasthttp.StatusOK
315+
316+
if path != testPath {
317+
wantStatusCode = fasthttp.StatusNotFound
318+
}
319+
320+
if statusCode != wantStatusCode {
321+
t.Errorf("Host %s - Path %s, Status code == %d, want %d", hostname, path, statusCode, wantStatusCode)
322+
}
323+
324+
if wantStatusCode == fasthttp.StatusNotFound {
325+
continue
326+
}
327+
328+
if tt.args.cfg.Compress && len(ctx.Response.Header.Peek(fasthttp.HeaderContentEncoding)) == 0 {
329+
t.Errorf("The header '%s' is not setted", fasthttp.HeaderContentEncoding)
330+
}
331+
}
332+
}
333+
})
232334
}
233335
}
234336

@@ -359,6 +461,10 @@ func TestAtreugo_Serve(t *testing.T) {
359461
if s.cfg.Network != lnNetwork {
360462
t.Errorf("Atreugo.Config.Network = %s, want %s", s.cfg.Network, lnNetwork)
361463
}
464+
465+
if s.server.Handler == nil {
466+
t.Error("Atreugo.server.Handler is nil")
467+
}
362468
}
363469

364470
func TestAtreugo_SetLogOutput(t *testing.T) {
@@ -372,3 +478,70 @@ func TestAtreugo_SetLogOutput(t *testing.T) {
372478
t.Error("SetLogOutput() log output was not changed")
373479
}
374480
}
481+
482+
func TestAtreugo_NewVirtualHost(t *testing.T) {
483+
hostname := "localhost"
484+
s := New(testAtreugoConfig)
485+
486+
if s.virtualHosts != nil {
487+
t.Error("Atreugo.virtualHosts must be nil before register a new virtual host")
488+
}
489+
490+
vHost := s.NewVirtualHost(hostname)
491+
if vHost == nil {
492+
t.Fatal("Atreugo.NewVirtualHost() returned a nil router")
493+
}
494+
495+
if !isEqual(vHost.router.NotFound, s.router.NotFound) {
496+
t.Errorf("VirtualHost router.NotFound == %p, want %p", vHost.router.NotFound, s.router.NotFound)
497+
}
498+
499+
if !isEqual(vHost.router.MethodNotAllowed, s.router.MethodNotAllowed) {
500+
t.Errorf(
501+
"VirtualHost router.MethodNotAllowed == %p, want %p",
502+
vHost.router.MethodNotAllowed,
503+
s.router.MethodNotAllowed,
504+
)
505+
}
506+
507+
if !isEqual(vHost.router.PanicHandler, s.router.PanicHandler) {
508+
t.Errorf("VirtualHost router.PanicHandler == %p, want %p", vHost.router.PanicHandler, s.router.PanicHandler)
509+
}
510+
511+
if h := s.virtualHosts[hostname]; h == nil {
512+
t.Error("The new virtual host is not registeded")
513+
}
514+
515+
defer func() {
516+
err := recover()
517+
if err == nil {
518+
t.Error("Expected panic when a virtual host is duplicated")
519+
}
520+
521+
wantErrString := fmt.Sprintf("a router is already registered for virtual host '%s'", hostname)
522+
if err != wantErrString {
523+
t.Errorf("Error string == %s, want %s", err, wantErrString)
524+
}
525+
}()
526+
527+
// panic when a virtual host is duplicated
528+
s.NewVirtualHost(hostname)
529+
}
530+
531+
// Benchmarks.
532+
func Benchmark_Handler(b *testing.B) {
533+
s := New(testAtreugoConfig)
534+
s.GET("/", func(ctx *RequestCtx) error { return nil })
535+
536+
ctx := new(fasthttp.RequestCtx)
537+
ctx.Request.Header.SetMethod("GET")
538+
ctx.Request.SetRequestURI("/")
539+
540+
handler := s.handler()
541+
542+
b.ResetTimer()
543+
544+
for i := 0; i <= b.N; i++ {
545+
handler(ctx)
546+
}
547+
}

0 commit comments

Comments
 (0)