diff --git a/core/errors/errors.go b/core/errors/errors.go index d68c2e57..fb3665bb 100644 --- a/core/errors/errors.go +++ b/core/errors/errors.go @@ -11,7 +11,6 @@ import ( var ( // Prefix the error prefix, applies to each error's message. - // Should not be changed. Prefix = "" // NewLine adds a new line to the end of each error's message // defaults to true diff --git a/core/errors/errors_test.go b/core/errors/errors_test.go new file mode 100644 index 00000000..69794b70 --- /dev/null +++ b/core/errors/errors_test.go @@ -0,0 +1,98 @@ +// black-box testing +package errors_test + +import ( + "fmt" + "testing" + + "github.com/kataras/iris/core/errors" +) + +var errMessage = "User with mail: %s already exists" +var errUserAlreadyExists = errors.New(errMessage) +var userMail = "user1@mail.go" +var expectedUserAlreadyExists = "User with mail: user1@mail.go already exists" + +func getNewLine() string { + if errors.NewLine { + return "\n" + } + return "" +} + +func ExampleError() { + fmt.Print(errUserAlreadyExists.Format(userMail)) + // first output first Output line + fmt.Print(errUserAlreadyExists.Format(userMail).Append("Please change your mail addr")) + // second output second and third Output lines + + // Output: + // User with mail: user1@mail.go already exists + // User with mail: user1@mail.go already exists + // Please change your mail addr +} + +func do(method string, testErr *errors.Error, expectingMsg string, t *testing.T) { + formattedErr := func() error { + return testErr.Format(userMail) + }() + + if formattedErr.Error() != expectingMsg { + t.Fatalf("error %s failed, expected:\n%s got:\n%s", method, expectingMsg, formattedErr.Error()) + } +} + +func TestFormat(t *testing.T) { + expected := errors.Prefix + expectedUserAlreadyExists + getNewLine() + do("Format Test", errUserAlreadyExists, expected, t) +} + +func TestAppendErr(t *testing.T) { + errors.NewLine = true + errors.Prefix = "error: " + + errChangeMailMsg := "Please change your mail addr" + errChangeMail := fmt.Errorf(errChangeMailMsg) // test go standard error + expectedErrorMessage := errUserAlreadyExists.Format(userMail).Error() + errChangeMailMsg + getNewLine() // first Prefix and last newline lives inside do + errAppended := errUserAlreadyExists.AppendErr(errChangeMail) + do("Append Test Standard error type", &errAppended, expectedErrorMessage, t) +} + +func TestAppendError(t *testing.T) { + errors.NewLine = true + errors.Prefix = "error: " + + errChangeMailMsg := "Please change your mail addr" + errChangeMail := errors.New(errChangeMailMsg) // test Error struct + expectedErrorMessage := errUserAlreadyExists.Format(userMail).Error() + errChangeMail.Error() + getNewLine() // first Prefix and last newline lives inside do + errAppended := errUserAlreadyExists.AppendErr(errChangeMail) + do("Append Test Error type", &errAppended, expectedErrorMessage, t) +} + +func TestAppend(t *testing.T) { + errors.NewLine = true + errors.Prefix = "error: " + + errChangeMailMsg := "Please change your mail addr" + expectedErrorMessage := errUserAlreadyExists.Format(userMail).Error() + errChangeMailMsg + getNewLine() // first Prefix and last newline lives inside do + errAppended := errUserAlreadyExists.Append(errChangeMailMsg) + do("Append Test string Message", &errAppended, expectedErrorMessage, t) +} + +func TestNewLine(t *testing.T) { + errors.NewLine = false + + errNoNewLine := errors.New(errMessage) + expected := errors.Prefix + expectedUserAlreadyExists + do("NewLine Test", errNoNewLine, expected, t) + + errors.NewLine = true +} + +func TestPrefix(t *testing.T) { + errors.Prefix = "MyPrefix: " + + errUpdatedPrefix := errors.New(errMessage) + expected := errors.Prefix + expectedUserAlreadyExists + "\n" + do("Prefix Test with "+errors.Prefix, errUpdatedPrefix, expected, t) +} diff --git a/core/handlerconv/from_std_test.go b/core/handlerconv/from_std_test.go new file mode 100644 index 00000000..238c6e32 --- /dev/null +++ b/core/handlerconv/from_std_test.go @@ -0,0 +1,66 @@ +// black-box testing +package handlerconv_test + +import ( + "net/http" + "testing" + + "github.com/kataras/iris" + "github.com/kataras/iris/context" + "github.com/kataras/iris/core/handlerconv" + "github.com/kataras/iris/httptest" +) + +func TestFromStd(t *testing.T) { + expected := "ok" + std := func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(expected)) + } + + h := handlerconv.FromStd(http.HandlerFunc(std)) + + hFunc := handlerconv.FromStd(std) + + app := iris.New() + app.Get("/handler", h) + app.Get("/func", hFunc) + + e := httptest.New(app, t) + + e.GET("/handler"). + Expect().Status(iris.StatusOK).Body().Equal(expected) + + e.GET("/func"). + Expect().Status(iris.StatusOK).Body().Equal(expected) +} + +func TestFromStdWithNext(t *testing.T) { + + basicauth := "secret" + passed := "ok" + + stdWNext := func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + if username, password, ok := r.BasicAuth(); ok && + username == basicauth && password == basicauth { + next.ServeHTTP(w, r) + return + } + w.WriteHeader(iris.StatusForbidden) + } + + h := handlerconv.FromStdWithNext(stdWNext) + next := func(ctx context.Context) { + ctx.WriteString(passed) + } + + app := iris.New() + app.Get("/handlerwithnext", h, next) + + e := httptest.New(app, t) + + e.GET("/handlerwithnext"). + Expect().Status(iris.StatusForbidden) + + e.GET("/handlerwithnext").WithBasicAuth(basicauth, basicauth). + Expect().Status(iris.StatusOK).Body().Equal(passed) +} diff --git a/core/host/proxy.go b/core/host/proxy.go index 0f89887a..9ac19674 100644 --- a/core/host/proxy.go +++ b/core/host/proxy.go @@ -5,10 +5,13 @@ package host import ( + "crypto/tls" "net/http" "net/http/httputil" "net/url" "strings" + + "github.com/kataras/iris/core/nettools" ) func singleJoiningSlash(a, b string) string { @@ -29,7 +32,6 @@ func singleJoiningSlash(a, b string) string { // the target request will be for /base/dir. // // Relative to httputil.NewSingleHostReverseProxy with some additions. -// Used for the deprecated `LETSENCRYPT`. func ProxyHandler(target *url.URL) *httputil.ReverseProxy { targetQuery := target.RawQuery director := func(req *http.Request) { @@ -42,8 +44,22 @@ func ProxyHandler(target *url.URL) *httputil.ReverseProxy { } else { req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery } + + if _, ok := req.Header["User-Agent"]; !ok { + // explicitly disable User-Agent so it's not set to default value + req.Header.Set("User-Agent", "") + } } - return &httputil.ReverseProxy{Director: director} + p := &httputil.ReverseProxy{Director: director} + + if nettools.IsLoopbackHost(target.Host) { + transport := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + p.Transport = transport + } + + return p } // NewProxy returns a new host (server supervisor) which @@ -56,7 +72,6 @@ func ProxyHandler(target *url.URL) *httputil.ReverseProxy { // proxy.ListenAndServe() // use of proxy.Shutdown to close the proxy server. func NewProxy(hostAddr string, target *url.URL) *Supervisor { proxyHandler := ProxyHandler(target) - proxy := New(&http.Server{ Addr: hostAddr, Handler: proxyHandler, diff --git a/core/host/proxy_test.go b/core/host/proxy_test.go new file mode 100644 index 00000000..cecb8e54 --- /dev/null +++ b/core/host/proxy_test.go @@ -0,0 +1,60 @@ +// black-box testing +package host_test + +import ( + "net" + "net/url" + "testing" + + "github.com/kataras/iris" + "github.com/kataras/iris/context" + "github.com/kataras/iris/core/host" + "github.com/kataras/iris/httptest" +) + +func TestProxy(t *testing.T) { + expectedIndex := "ok /" + expectedAbout := "ok /about" + unexpectedRoute := "unexpected" + + // proxySrv := iris.New() + u, err := url.Parse("https://localhost") + if err != nil { + t.Fatalf("%v while parsing url", err) + } + + // p := host.ProxyHandler(u) + // transport := &http.Transport{ + // TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + // } + // p.Transport = transport + // proxySrv.Downgrade(p.ServeHTTP) + // go proxySrv.Run(iris.Addr(":80"), iris.WithoutBanner, iris.WithoutInterruptHandler) + + go host.NewProxy(":80", u).ListenAndServe() + + app := iris.New() + app.Get("/", func(ctx context.Context) { + ctx.WriteString(expectedIndex) + }) + + app.Get("/about", func(ctx context.Context) { + ctx.WriteString(expectedAbout) + }) + + app.OnErrorCode(iris.StatusNotFound, func(ctx context.Context) { + ctx.WriteString(unexpectedRoute) + }) + + l, err := net.Listen("tcp", "localhost:443") + if err != nil { + t.Fatalf("%v while creating tcp4 listener for new tls local test listener", err) + } + // main server + go app.Run(iris.Listener(httptest.NewLocalTLSListener(l)), iris.WithoutBanner) + + e := httptest.NewInsecure(t, httptest.URL("http://localhost")) + e.GET("/").Expect().Status(iris.StatusOK).Body().Equal(expectedIndex) + e.GET("/about").Expect().Status(iris.StatusOK).Body().Equal(expectedAbout) + e.GET("/notfound").Expect().Status(iris.StatusNotFound).Body().Equal(unexpectedRoute) +} diff --git a/core/host/scheduler.go b/core/host/scheduler.go index acce9102..68cae279 100644 --- a/core/host/scheduler.go +++ b/core/host/scheduler.go @@ -22,13 +22,23 @@ func (t *task) isCanceled() bool { return atomic.LoadInt32(&t.alreadyCanceled) != 0 } +// Scheduler is a type of an event emmiter. +// Can register a specific task for a specific event +// when host is starting the server or host is interrupted by CTRL+C/CMD+C. +// It's being used internally on host supervisor. type Scheduler struct { onServeTasks []*task onInterruptTasks []*task } +// TaskCancelFunc cancels a Task when called. type TaskCancelFunc func() +// Schedule schedule/registers a Task, +// it will be executed/run to when host starts the server +// or when host is interrupted by CTRL+C/CMD+C based on the TaskRunner type. +// +// See `OnInterrupt` and `ScheduleFunc` too. func (s *Scheduler) Schedule(runner TaskRunner) TaskCancelFunc { t := new(task) @@ -50,6 +60,11 @@ func (s *Scheduler) Schedule(runner TaskRunner) TaskCancelFunc { } } +// ScheduleFunc schedule/registers a task function, +// it will be executed/run to when host starts the server +// or when host is interrupted by CTRL+C/CMD+C based on the TaskRunner type. +// +// See `OnInterrupt` and `ScheduleFunc` too. func (s *Scheduler) ScheduleFunc(runner func(TaskProcess)) TaskCancelFunc { return s.Schedule(TaskRunnerFunc(runner)) } @@ -63,10 +78,14 @@ func cancelTasks(tasks []*task) { } } +// CancelOnServeTasks cancels all tasks that are scheduled to run when +// host is starting the server, when the server is alive and online. func (s *Scheduler) CancelOnServeTasks() { cancelTasks(s.onServeTasks) } +// CancelOnInterruptTasks cancels all tasks that are scheduled to run when +// host is being interrupted by CTRL+C/CMD+C, when the server is alive and online as well. func (s *Scheduler) CancelOnInterruptTasks() { cancelTasks(s.onInterruptTasks) } @@ -124,6 +143,8 @@ func (s *Scheduler) notifyErr(err error) { }) } +// CopyTo copies all tasks from "s" to "to" Scheduler. +// It doesn't care about anything else. func (s *Scheduler) CopyTo(to *Scheduler) { s.visit(func(t *task) { rnner := t.runner diff --git a/core/host/scheduler_test.go b/core/host/scheduler_test.go index 4f50017c..7e2b1a99 100644 --- a/core/host/scheduler_test.go +++ b/core/host/scheduler_test.go @@ -1,7 +1,4 @@ -// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - +// white-box testing package host import ( diff --git a/core/host/supervisor.go b/core/host/supervisor.go index 4356fcfc..517fbbcc 100644 --- a/core/host/supervisor.go +++ b/core/host/supervisor.go @@ -36,6 +36,13 @@ type Supervisor struct { mu sync.Mutex } +// New returns a new host supervisor +// based on a native net/http "srv". +// +// It contains all native net/http's Server methods. +// Plus you can add tasks on specific events. +// It has its own flow, which means that you can prevent +// to return and exit and restore the flow too. func New(srv *http.Server) *Supervisor { return &Supervisor{ server: srv, @@ -45,10 +52,23 @@ func New(srv *http.Server) *Supervisor { } } +// DeferFlow defers the flow of the exeuction, +// i.e: when server should return error and exit +// from app, a DeferFlow call inside a Task +// can wait for a `RestoreFlow` to exit or not exit if +// host's server is "fixed". +// +// See `RestoreFlow` too. func (su *Supervisor) DeferFlow() { atomic.StoreInt32(&su.shouldWait, 1) } +// RestoreFlow restores the flow of the execution, +// if called without a `DeferFlow` call before +// then it does nothing. +// See tests to understand how that can be useful on specific cases. +// +// See `DeferFlow` too. func (su *Supervisor) RestoreFlow() { if su.isWaiting() { atomic.StoreInt32(&su.shouldWait, 0) @@ -162,10 +182,25 @@ func (su *Supervisor) newListener() (net.Listener, error) { return l, nil } +// Serve accepts incoming connections on the Listener l, creating a +// new service goroutine for each. The service goroutines read requests and +// then call su.server.Handler to reply to them. +// +// For HTTP/2 support, server.TLSConfig should be initialized to the +// provided listener's TLS Config before calling Serve. If +// server.TLSConfig is non-nil and doesn't include the string "h2" in +// Config.NextProtos, HTTP/2 support is not enabled. +// +// Serve always returns a non-nil error. After Shutdown or Close, the +// returned error is http.ErrServerClosed. func (su *Supervisor) Serve(l net.Listener) error { return su.supervise(func() error { return su.server.Serve(l) }) } +// ListenAndServe listens on the TCP network address addr +// and then calls Serve with handler to handle requests +// on incoming connections. +// Accepted connections are configured to enable TCP keep-alives. func (su *Supervisor) ListenAndServe() error { l, err := su.newListener() if err != nil { @@ -178,6 +213,11 @@ func setupHTTP2(cfg *tls.Config) { cfg.NextProtos = append(cfg.NextProtos, "h2") // HTTP2 } +// ListenAndServeTLS acts identically to ListenAndServe, except that it +// expects HTTPS connections. Additionally, files containing a certificate and +// matching private key for the server must be provided. If the certificate +// is signed by a certificate authority, the certFile should be the concatenation +// of the server's certificate, any intermediates, and the CA's certificate. func (su *Supervisor) ListenAndServeTLS(certFile string, keyFile string) error { if certFile == "" || keyFile == "" { return errors.New("certFile or keyFile missing") @@ -195,6 +235,9 @@ func (su *Supervisor) ListenAndServeTLS(certFile string, keyFile string) error { return su.ListenAndServe() } +// ListenAndServeAutoTLS acts identically to ListenAndServe, except that it +// expects HTTPS connections. server's certificates are auto generated from LETSENCRYPT using +// the golang/x/net/autocert package. func (su *Supervisor) ListenAndServeAutoTLS() error { autoTLSManager := autocert.Manager{ Prompt: autocert.AcceptTOS, diff --git a/core/host/supervisor_test.go b/core/host/supervisor_test.go index b63f8747..2c01f721 100644 --- a/core/host/supervisor_test.go +++ b/core/host/supervisor_test.go @@ -1,6 +1,4 @@ -// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. +// white-box testing package host diff --git a/core/host/task.go b/core/host/task.go index 8c9f2435..0697dfc1 100644 --- a/core/host/task.go +++ b/core/host/task.go @@ -12,16 +12,21 @@ import ( "context" "github.com/kataras/iris/core/nettools" "net/http" - "os" ) type ( + // FlowController exports the `DeferFlow` + // and `RestoreFlow` capabilities. + // Read more at Supervisor. FlowController interface { DeferFlow() RestoreFlow() } ) +// TaskHost contains all the necessary information +// about the host supervisor, its server +// and the exports the whole flow controller of it. type TaskHost struct { su *Supervisor // Supervisor with access fields when server is running, i.e restrict access to "Schedule" @@ -35,14 +40,17 @@ type TaskHost struct { errChan chan error } +// Done filled when server was shutdown. func (h TaskHost) Done() <-chan struct{} { return h.doneChan } +// Err filled when server received an error. func (h TaskHost) Err() <-chan error { return h.errChan } +// Serve can (re)run the server with the latest known configuration. func (h TaskHost) Serve() error { // the underline server's serve, using the "latest known" listener from the supervisor. l, err := h.su.newListener() @@ -69,24 +77,40 @@ func (h TaskHost) Hostname() string { return nettools.ResolveHostname(h.su.server.Addr) } +// Shutdown gracefully shuts down the server without interrupting any +// active connections. Shutdown works by first closing all open +// listeners, then closing all idle connections, and then waiting +// indefinitely for connections to return to idle and then shut down. +// If the provided context expires before the shutdown is complete, +// then the context's error is returned. +// +// Shutdown does not attempt to close nor wait for hijacked +// connections such as WebSockets. The caller of Shutdown should +// separately notify such long-lived connections of shutdown and wait +// for them to close, if desired. func (h TaskHost) Shutdown(ctx context.Context) error { // the underline server's Shutdown (otherwise we will cancel all tasks and do cycles) return h.su.server.Shutdown(ctx) } -func (h TaskHost) PID() int { - return h.pid -} - +// TaskProcess is the context of the Task runner. +// Contains the host's information and actions +// and its self cancelation emmiter. type TaskProcess struct { canceledChan chan struct{} host TaskHost } +// Done filled when this task is canceled. func (p TaskProcess) Done() <-chan struct{} { return p.canceledChan } +// Host returns the TaskHost. +// +// TaskHost contains all the necessary information +// about the host supervisor, its server +// and the exports the whole flow controller of it. func (p TaskProcess) Host() TaskHost { return p.host } @@ -97,7 +121,6 @@ func createTaskHost(su *Supervisor) TaskHost { FlowController: su, doneChan: make(chan struct{}), errChan: make(chan error), - pid: os.Getpid(), } return host @@ -121,11 +144,21 @@ func newTaskProcess(host TaskHost) TaskProcess { // A Task is considered to be a lightweight process because it runs within the context of a Supervisor // and takes advantage of resources allocated for that Supervisor and its Server. type TaskRunner interface { + // Run runs the task based on its TaskProcess which contains + // all the necessary information and actions to control the host supervisor + // and its server. Run(TaskProcess) } +// TaskRunnerFunc "converts" a func(TaskProcess) to a complete TaskRunner. +// Its functionality is exactly the same as TaskRunner. +// +// See `TaskRunner` too. type TaskRunnerFunc func(TaskProcess) +// Run runs the task based on its TaskProcess which contains +// all the necessary information and actions to control the host supervisor +// and its server. func (s TaskRunnerFunc) Run(proc TaskProcess) { s(proc) } diff --git a/core/host/task_banner.go b/core/host/task_banner.go index eecee5d8..750760e4 100644 --- a/core/host/task_banner.go +++ b/core/host/task_banner.go @@ -10,6 +10,10 @@ import ( "runtime" ) +// WriteBannerTask is a task which accepts a logger(io.Writer) +// and a "banner" text to write to following +// by a generated message based on the host supervisor's server and writes it to the "w". +// This task runs on serve. func WriteBannerTask(w io.Writer, banner string) TaskRunnerFunc { return func(proc TaskProcess) { listeningURI := proc.Host().HostURL() diff --git a/core/host/task_example_test.go b/core/host/task_example_test.go index 499e3942..a8f25065 100644 --- a/core/host/task_example_test.go +++ b/core/host/task_example_test.go @@ -1,7 +1,4 @@ -// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - +// white-box testing package host import ( diff --git a/core/host/task_interrupt.go b/core/host/task_interrupt.go index e95cdb87..ad96ed93 100644 --- a/core/host/task_interrupt.go +++ b/core/host/task_interrupt.go @@ -13,6 +13,7 @@ import ( // value(Task) when an OS interrupt/kill signal received. type OnInterrupt TaskRunnerFunc +// Run runs the interrupt task and completes the TaskRunner interface. func (t OnInterrupt) Run(proc TaskProcess) { t(proc) } diff --git a/core/logger/logger_test.go b/core/logger/logger_test.go new file mode 100644 index 00000000..6f424cc4 --- /dev/null +++ b/core/logger/logger_test.go @@ -0,0 +1,18 @@ +// black-box testing +package logger_test + +import ( + "bytes" + "testing" + + "github.com/kataras/iris/core/logger" +) + +func TestLog(t *testing.T) { + msg := "Hello this is me" + l := &bytes.Buffer{} + logger.Log(l, msg) + if expected, got := msg, l.String(); expected != got { + t.Fatalf("expected %s but got %s", expected, got) + } +} diff --git a/core/memstore/memstore_test.go b/core/memstore/memstore_test.go index 4290dd6e..68d89719 100644 --- a/core/memstore/memstore_test.go +++ b/core/memstore/memstore_test.go @@ -1,3 +1,4 @@ +// white-box testing package memstore import ( @@ -91,3 +92,21 @@ func TestImmutable(t *testing.T) { t.Fatalf("expected objp to be immutable but caller was able to change its value") } } + +func TestImmutableSetOnlyWithSetImmutable(t *testing.T) { + var p Store + + p.SetImmutable("objp", &myTestObject{"value"}) + + p.Set("objp", &myTestObject{"modified"}) + vObjP := p.Get("objp").(myTestObject) + if vObjP.name == "modified" { + t.Fatalf("caller should not be able to change the immutable entry with a simple `Set`") + } + + p.SetImmutable("objp", &myTestObject{"value with SetImmutable"}) + vvObjP := p.Get("objp").(myTestObject) + if vvObjP.name != "value with SetImmutable" { + t.Fatalf("caller should be able to change the immutable entry with a `SetImmutable`") + } +} diff --git a/core/nettools/addr.go b/core/nettools/addr.go index 3fb5f8d7..0d21033b 100644 --- a/core/nettools/addr.go +++ b/core/nettools/addr.go @@ -57,6 +57,10 @@ var IsLoopbackHost = func(requestHost string) bool { // would probably not want to reach the server with different Application.Config.Addr than // he/she declared. portOrPathIdx := strings.LastIndexByte(requestHost, ':') + + if portOrPathIdx == 0 { // 0.0.0.0:[...]/localhost:[...]/127.0.0.1:[...]/ipv6 local... + return true + } // this will not catch ipv6 loopbacks like subdomain.0000:0:0000::01.1:8080 // but, again, is for developers only, is hard to try to navigate with something like this, // and if that happened, I provide a way to override the whole "algorithm" to a custom one via "IsLoopbackHost". diff --git a/core/router/router.go b/core/router/router.go index cf11967e..94d7fe9f 100644 --- a/core/router/router.go +++ b/core/router/router.go @@ -91,6 +91,11 @@ func (router *Router) Downgrade(newMainHandler http.HandlerFunc) { router.mu.Unlock() } +// Downgraded returns true if this router is downgraded. +func (router *Router) Downgraded() bool { + return router.mainHandler != nil && router.requestHandler == nil +} + // WrapRouter adds a wrapper on the top of the main router. // Usually it's useful for third-party middleware // when need to wrap the entire application with a middleware like CORS. diff --git a/httptest/httptest.go b/httptest/httptest.go index 7b37d19a..10476eb0 100644 --- a/httptest/httptest.go +++ b/httptest/httptest.go @@ -10,7 +10,6 @@ import ( "testing" "github.com/iris-contrib/httpexpect" - "github.com/kataras/iris" ) diff --git a/httptest/netutils.go b/httptest/netutils.go new file mode 100644 index 00000000..9b22b61a --- /dev/null +++ b/httptest/netutils.go @@ -0,0 +1,97 @@ +package httptest + +import ( + "crypto/tls" + "net" +) + +// copied from net/http/httptest/internal + +// LocalhostCert is a PEM-encoded TLS cert with SAN IPs +// "127.0.0.1" and "[::1]", expiring at Jan 29 16:00:00 2084 GMT. +// generated from src/crypto/tls: +// go run generate_cert.go --rsa-bits 1024 --host 127.0.0.1,::1,example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h +// note: these are not the net/http/httptest/internal contents but doesn't matter. +var LocalhostCert = []byte(`-----BEGIN CERTIFICATE----- +MIIDAzCCAeugAwIBAgIJAP0pWSuIYyQCMA0GCSqGSIb3DQEBBQUAMBgxFjAUBgNV +BAMMDWxvY2FsaG9zdDozMzEwHhcNMTYxMjI1MDk1OTI3WhcNMjYxMjIzMDk1OTI3 +WjAYMRYwFAYDVQQDDA1sb2NhbGhvc3Q6MzMxMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEA5vETjLa+8W856rWXO1xMF/CLss9vn5xZhPXKhgz+D7ogSAXm +mWP53eeBUGC2r26J++CYfVqwOmfJEu9kkGUVi8cGMY9dHeIFPfxD31MYX175jJQe +tu0WeUII7ciNsSUDyBMqsl7yi1IgN7iLONM++1+QfbbmNiEbghRV6icEH6M+bWlz +3YSAMEdpK3mg2gsugfLKMwJkaBKEehUNMySRlIhyLITqt1exYGaggRd1zjqUpqpD +sL2sRVHJ3qHGkSh8nVy8MvG8BXiFdYQJP3mCQDZzruCyMWj5/19KAyu7Cto3Bcvu +PgujnwRtU+itt8WhZUVtU1n7Ivf6lMJTBcc4OQIDAQABo1AwTjAdBgNVHQ4EFgQU +MXrBvbILQmiwjUj19aecF2N+6IkwHwYDVR0jBBgwFoAUMXrBvbILQmiwjUj19aec +F2N+6IkwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEA4zbFml1t9KXJ +OijAV8gALePR8v04DQwJP+jsRxXw5zzhc8Wqzdd2hjUd07mfRWAvmyywrmhCV6zq +OHznR+aqIqHtm0vV8OpKxLoIQXavfBd6axEXt3859RDM4xJNwIlxs3+LWGPgINud +wjJqjyzSlhJpQpx4YZ5Da+VMiqAp8N1UeaZ5lBvmSDvoGh6HLODSqtPlWMrldRW9 +AfsXVxenq81MIMeKW2fSOoPnWZ4Vjf1+dSlbJE/DD4zzcfbyfgY6Ep/RrUltJ3ag +FQbuNTQlgKabe21dSL9zJ2PengVKXl4Trl+4t/Kina9N9Jw535IRCSwinD6a/2Ca +m7DnVXFiVA== +-----END CERTIFICATE----- +`) + +// LocalhostKey is the private key for localhostCert. +var LocalhostKey = []byte(`-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA5vETjLa+8W856rWXO1xMF/CLss9vn5xZhPXKhgz+D7ogSAXm +mWP53eeBUGC2r26J++CYfVqwOmfJEu9kkGUVi8cGMY9dHeIFPfxD31MYX175jJQe +tu0WeUII7ciNsSUDyBMqsl7yi1IgN7iLONM++1+QfbbmNiEbghRV6icEH6M+bWlz +3YSAMEdpK3mg2gsugfLKMwJkaBKEehUNMySRlIhyLITqt1exYGaggRd1zjqUpqpD +sL2sRVHJ3qHGkSh8nVy8MvG8BXiFdYQJP3mCQDZzruCyMWj5/19KAyu7Cto3Bcvu +PgujnwRtU+itt8WhZUVtU1n7Ivf6lMJTBcc4OQIDAQABAoIBAQCTLE0eHpPevtg0 ++FaRUMd5diVA5asoF3aBIjZXaU47bY0G+SO02x6wSMmDFK83a4Vpy/7B3Bp0jhF5 +DLCUyKaLdmE/EjLwSUq37ty+JHFizd7QtNBCGSN6URfpmSabHpCjX3uVQqblHIhF +mki3BQCdJ5CoXPemxUCHjDgYSZb6JVNIPJExjekc0+4A2MYWMXV6Wr86C7AY3659 +KmveZpC3gOkLA/g/IqDQL/QgTq7/3eloHaO+uPBihdF56do4eaOO0jgFYpl8V7ek +PZhHfhuPZV3oq15+8Vt77ngtjUWVI6qX0E3ilh+V5cof+03q0FzHPVe3zBUNXcm0 +OGz19u/FAoGBAPSm4Aa4xs/ybyjQakMNix9rak66ehzGkmlfeK5yuQ/fHmTg8Ac+ +ahGs6A3lFWQiyU6hqm6Qp0iKuxuDh35DJGCWAw5OUS/7WLJtu8fNFch6iIG29rFs +s+Uz2YLxJPebpBsKymZUp7NyDRgEElkiqsREmbYjLrc8uNKkDy+k14YnAoGBAPGn +ZlN0Mo5iNgQStulYEP5pI7WOOax9KOYVnBNguqgY9c7fXVXBxChoxt5ebQJWG45y +KPG0hB0bkA4YPu4bTRf5acIMpjFwcxNlmwdc4oCkT4xqAFs9B/AKYZgkf4IfKHqW +P9PD7TbUpkaxv25bPYwUSEB7lPa+hBtRyN9Wo6qfAoGAPBkeISiU1hJE0i7YW55h +FZfKZoqSYq043B+ywo+1/Dsf+UH0VKM1ZSAnZPpoVc/hyaoW9tAb98r0iZ620wJl +VkCjgYklknbY5APmw/8SIcxP6iVq1kzQqDYjcXIRVa3rEyWEcLzM8VzL8KFXbIQC +lPIRHFfqKuMEt+HLRTXmJ7MCgYAHGvv4QjdmVl7uObqlG9DMGj1RjlAF0VxNf58q +NrLmVG2N2qV86wigg4wtZ6te4TdINfUcPkmQLYpLz8yx5Z2bsdq5OPP+CidoD5nC +WqnSTIKGR2uhQycjmLqL5a7WHaJsEFTqHh2wego1k+5kCUzC/KmvM7MKmkl6ICp+ +3qZLUwKBgQCDOhKDwYo1hdiXoOOQqg/LZmpWOqjO3b4p99B9iJqhmXN0GKXIPSBh +5nqqmGsG8asSQhchs7EPMh8B80KbrDTeidWskZuUoQV27Al1UEmL6Zcl83qXD6sf +k9X9TwWyZtp5IL1CAEd/Il9ZTXFzr3lNaN8LCFnU+EIsz1YgUW8LTg== +-----END RSA PRIVATE KEY----- +`) + +// NewLocalListener returns a new ipv4 "127.0.0.1:0" +// or tcp6 "[::1]:0" tcp listener. +func NewLocalListener() net.Listener { + l, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + if l, err = net.Listen("tcp6", "[::1]:0"); err != nil { + panic(err) + } + } + return l +} + +// NewLocalTLSListener returns a new tls listener +// based on the "tcpListener", if "tcpListener" is nil +// it make use of the `NewLocalListener`. +// Cert and Key are `LocalhostCert` and `LocalhostKey` respectfully. +func NewLocalTLSListener(tcpListener net.Listener) net.Listener { + if tcpListener == nil { + tcpListener = NewLocalListener() + } + + cert, err := tls.X509KeyPair(LocalhostCert, LocalhostKey) + if err != nil { + panic(err) + } + + cfg := new(tls.Config) + cfg.NextProtos = []string{"http/1.1"} + cfg.Certificates = []tls.Certificate{cert} + cfg.InsecureSkipVerify = true + return tls.NewListener(tcpListener, cfg) +} diff --git a/iris.go b/iris.go index 3b0d416c..9bc81766 100644 --- a/iris.go +++ b/iris.go @@ -6,6 +6,7 @@ package iris import ( // std packages + stdContext "context" "io" "log" "net" @@ -79,6 +80,9 @@ type Application struct { sessions sessions.Sessions // used for build once sync.Once + + mu sync.Mutex + Shutdown func(stdContext.Context) error } // New creates and returns a fresh empty Iris *Application instance. @@ -149,14 +153,17 @@ func (app *Application) Build() (err error) { return // if view engine loading failed then don't continue } - var routerHandler router.RequestHandler - // router - // create the request handler, the default routing handler - routerHandler = router.NewDefaultHandler() + if !app.Router.Downgraded() { + var routerHandler router.RequestHandler + // router + // create the request handler, the default routing handler + routerHandler = router.NewDefaultHandler() + + err = app.Router.BuildRouter(app.ContextPool, routerHandler, app.APIBuilder) + // re-build of the router from outside can be done with; + // app.RefreshRouter() + } - err = app.Router.BuildRouter(app.ContextPool, routerHandler, app.APIBuilder) - // re-build of the router from outside can be done with; - // app.RefreshRouter() }) return @@ -166,6 +173,9 @@ func (app *Application) Build() (err error) { // completes the necessary missing parts of that "srv" // and returns a new, ready-to-use, host (supervisor). func (app *Application) NewHost(srv *http.Server) *host.Supervisor { + app.mu.Lock() + defer app.mu.Unlock() + // set the server's handler to the framework's router if srv.Handler == nil { srv.Handler = app.Router @@ -216,6 +226,10 @@ func (app *Application) NewHost(srv *http.Server) *host.Supervisor { su.Schedule(host.ShutdownOnInterruptTask(shutdownTimeout)) } + if app.Shutdown == nil { + app.Shutdown = su.Shutdown + } + return su } diff --git a/iris_deprecated.go b/iris_deprecated.go index c88ae781..bd775809 100644 --- a/iris_deprecated.go +++ b/iris_deprecated.go @@ -102,20 +102,12 @@ func (app *Application) ListenTLS(addr string, certFile, keyFile string) { func (app *Application) ListenLETSENCRYPT(addr string, cacheDirOptional ...string) { l, err := nettools.LETSENCRYPT(addr, addr, cacheDirOptional...) CheckErr(err) - - targetURL := nettools.SchemeHTTPS + "://" + nettools.ResolveVHost(addr) - target, err := url.Parse(targetURL) + // create the redirect server to redirect http://... to https://... + hostname := nettools.ResolveHostname(addr) + proxyAddr := hostname + ":80" + target, err := url.Parse("https://" + hostname) CheckErr(err) - // create the reverse proxy to redirect http://... to https://... - proxyAddr := nettools.ResolveHostname(addr) + ":80" - proxySrv := host.NewProxy(proxyAddr, target) - - go func() { - if err := proxySrv.ListenAndServe(); err != nil { - // don't panic here, just log the proxy's error. - app.Log("proxy error: %v", err) - } - }() + go host.NewProxy(proxyAddr, target).ListenAndServe() CheckErr(app.Run(Listener(l))) }