diff --git a/.travis.yml b/.travis.yml index d6f89492..c5f03596 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,6 @@ language: go go: - - go1.7 + - go1.7 + +go_import_path: gopkg.in/kataras/iris.v6 diff --git a/HISTORY.md b/HISTORY.md index 9e471d8b..5ddf0a2f 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -14,7 +14,7 @@ to adapt the new changes to your application, it contains an overview of the new - Template engines (two lines to add, same features as before, except their easier configuration) - Basic middleware, that have been written by me, are transfared to the main repository[/middleware](https://github.com/kataras/iris/tree/master/middleware) with a lot of improvements to the `recover middleware` (see the next) - `func(http.ResponseWriter, r *http.Request, next http.HandlerFunc)` signature is fully compatible using `iris.ToHandler` helper wrapper func, without any need of custom boilerplate code. So all net/http middleware out there are supported, no need to re-invert the world here, search to the internet and you'll find a suitable to your case. - +- Developers can use a `yaml` files for the configuration using the `iris.YAML` function: `app := iris.New(iris.YAML("myconfiguration.yaml"))` Fixes: diff --git a/configuration.go b/configuration.go index 5bad68ac..af5c77be 100644 --- a/configuration.go +++ b/configuration.go @@ -1,13 +1,13 @@ package iris import ( - "crypto/tls" - "net" - "net/http" + "io/ioutil" + "path/filepath" "strconv" "time" "github.com/imdario/mergo" + "gopkg.in/yaml.v2" ) type ( @@ -39,6 +39,39 @@ func (o OptionSet) Set(c *Configuration) { o(c) } +// YAML reads Configuration from a file.yml. +// +// Accepts the absolute path of the file.yml. +// An error will be shown to the user via panic with the error message. +// Error may occur when the file.yml doesn't exists or is not formatted correctly. +// +// Usage: +// 1. `app := iris.New(YAML("myfile.yml"))` +// 2. `app.Set(YAML("myfile.yml"))` +func YAML(filename string) Configuration { + c := DefaultConfiguration() + + // get the abs + // which will try to find the 'filename' from current workind dir too. + yamlAbsPath, err := filepath.Abs(filename) + if err != nil { + panic("FATAL ERROR .yml.filename to absolute: " + err.Error()) + } + + // read the raw contents of the file + data, err := ioutil.ReadFile(yamlAbsPath) + if err != nil { + panic("FATAL ERROR .yml.ReadFile: " + err.Error()) + } + + // put the file's contents as yaml to the default configuration(c) + if err := yaml.Unmarshal(data, &c); err != nil { + panic("FATAL ERROR .yml.Unmarshal: " + err.Error()) + } + + return c +} + // Configuration the whole configuration for an Iris station instance // these can be passed via options also, look at the top of this file(configuration.go). // Configuration is a valid OptionSetter. @@ -57,7 +90,7 @@ type Configuration struct { // listening to the $instance.Handler after the manually-called $instance.Build // // Default comes from iris.Default.Listen/.Serve with iris' listeners (iris.TCP4/UNIX/TLS/LETSENCRYPT). - VHost string + VHost string `yaml:"vHost"` // VScheme is the scheme (http:// or https://) putted at the template function '{{url }}' // It's an optional field, @@ -68,34 +101,20 @@ type Configuration struct { // addr only(http://) but the nginx mapper is listening to https:// // // Default comes from iris.Default.Listen/.Serve with iris' listeners (TCP4,UNIX,TLS,LETSENCRYPT). - VScheme string + VScheme string `yaml:"vScheme"` // ReadTimeout is the maximum duration before timing out read of the request. - ReadTimeout time.Duration + ReadTimeout time.Duration `yaml:"readTimeout"` + // WriteTimeout is the maximum duration before timing out write of the response. - WriteTimeout time.Duration + WriteTimeout time.Duration `yaml:"writeTimeout"` // MaxHeaderBytes controls the maximum number of bytes the // server will read parsing the request header's keys and // values, including the request line. It does not limit the // size of the request body. // If zero, DefaultMaxHeaderBytes is used. - MaxHeaderBytes int - - // TLSNextProto optionally specifies a function to take over - // ownership of the provided TLS connection when an NPN/ALPN - // protocol upgrade has occurred. The map key is the protocol - // name negotiated. The Handler argument should be used to - // handle HTTP requests and will initialize the Request's TLS - // and RemoteAddr if not already set. The connection is - // automatically closed when the function returns. - // If TLSNextProto is nil, HTTP/2 support is enabled automatically. - TLSNextProto map[string]func(*http.Server, *tls.Conn, http.Handler) - - // ConnState specifies an optional callback function that is - // called when a client connection changes state. See the - // ConnState type and associated constants for details. - ConnState func(net.Conn, http.ConnState) + MaxHeaderBytes int `yaml:"maxHeaderBytes"` // CheckForUpdates will try to search for newer version of Iris based on the https://github.com/kataras/iris/releases // If a newer version found then the app will ask the he dev/user if want to update the 'x' version @@ -111,7 +130,7 @@ type Configuration struct { // Usage: app := iris.New(iris.Configuration{CheckForUpdates: true}) // // Defaults to false. - CheckForUpdates bool + CheckForUpdates bool `yaml:"checkForUpdates"` // DisablePathCorrection corrects and redirects the requested path to the registered path // for example, if /home/ path is requested but no handler for this Route found, @@ -119,7 +138,7 @@ type Configuration struct { // (permant)redirects the client to the correct path /home // // Defaults to false. - DisablePathCorrection bool + DisablePathCorrection bool `yaml:"disablePathCorrection"` // EnablePathEscape when is true then its escapes the path, the named parameters (if any). // Change to false it if you want something like this https://github.com/kataras/iris/issues/135 to work @@ -132,17 +151,17 @@ type Configuration struct { // projectName, _ := url.QueryUnescape(c.Param("project"). // // Defaults to false. - EnablePathEscape bool + EnablePathEscape bool `yaml:"enablePathEscape"` // FireMethodNotAllowed if it's true router checks for StatusMethodNotAllowed(405) and // fires the 405 error instead of 404 // Defaults to false. - FireMethodNotAllowed bool + FireMethodNotAllowed bool `yaml:"fireMethodNotAllowed"` // DisableBanner outputs the iris banner at startup // // Defaults to false. - DisableBanner bool + DisableBanner bool `yaml:"disableBanner"` // DisableBodyConsumptionOnUnmarshal manages the reading behavior of the context's body readers/binders. // If setted to true then it @@ -152,34 +171,36 @@ type Configuration struct { // if this field setted to true then a new buffer will be created to read from and the request body. // The body will not be changed and existing data before the // context.UnmarshalBody/ReadJSON/ReadXML will be not consumed. - DisableBodyConsumptionOnUnmarshal bool + DisableBodyConsumptionOnUnmarshal bool `yaml:"disableBodyConsumptionOnUnmarshal"` // TimeFormat time format for any kind of datetime parsing // Defauls to "Mon, 02 Jan 2006 15:04:05 GMT". - TimeFormat string + TimeFormat string `yaml:"timeFormat"` // Charset character encoding for various rendering // used for templates and the rest of the responses // Defaults to "UTF-8". - Charset string + Charset string `yaml:"charset"` // Gzip enables gzip compression on your Render actions, this includes any type of render, // templates and pure/raw content // If you don't want to enable it globally, you could just use the third parameter // on context.Render("myfileOrResponse", structBinding{}, iris.RenderOptions{"gzip": true}) // Defaults to false. - Gzip bool + Gzip bool `yaml:"gzip"` // Other are the custom, dynamic options, can be empty. // This field used only by you to set any app's options you want // or by custom adaptors, it's a way to simple communicate between your adaptors (if any) // Defaults to a non-nil empty map. - Other map[string]interface{} + Other map[string]interface{} `yaml:"other"` } // Set implements the OptionSetter func (c Configuration) Set(main *Configuration) { - mergo.MergeWithOverwrite(main, c) + if err := mergo.MergeWithOverwrite(main, c); err != nil { + panic("FATAL ERROR .Configuration as OptionSetter: " + err.Error()) + } } // All options starts with "Option" preffix in order to be easier to find what dev searching for @@ -245,29 +266,6 @@ var ( } } - // TLSNextProto optionally specifies a function to take over - // ownership of the provided TLS connection when an NPN/ALPN - // protocol upgrade has occurred. The map key is the protocol - // name negotiated. The Handler argument should be used to - // handle HTTP requests and will initialize the Request's TLS - // and RemoteAddr if not already set. The connection is - // automatically closed when the function returns. - // If TLSNextProto is nil, HTTP/2 support is enabled automatically. - OptionTLSNextProto = func(val map[string]func(*http.Server, *tls.Conn, http.Handler)) OptionSet { - return func(c *Configuration) { - c.TLSNextProto = val - } - } - - // ConnState specifies an optional callback function that is - // called when a client connection changes state. See the - // ConnState type and associated constants for details. - OptionConnState = func(val func(net.Conn, http.ConnState)) OptionSet { - return func(c *Configuration) { - c.ConnState = val - } - } - // OptionCheckForUpdates will try to search for newer version of Iris based on the https://github.com/kataras/iris/releases // If a newer version found then the app will ask the he dev/user if want to update the 'x' version // if 'y' is pressed then the updater will try to install the latest version @@ -407,8 +405,8 @@ const ( ) // DefaultConfiguration returns the default configuration for an Iris station, fills the main Configuration -func DefaultConfiguration() *Configuration { - return &Configuration{ +func DefaultConfiguration() Configuration { + return Configuration{ VHost: "", VScheme: "", ReadTimeout: DefaultReadTimeout, diff --git a/configuration_test.go b/configuration_test.go new file mode 100644 index 00000000..2d4bc8a5 --- /dev/null +++ b/configuration_test.go @@ -0,0 +1,198 @@ +// Black-box Testing +package iris_test + +import ( + "io/ioutil" + "os" + "reflect" + "testing" + "time" + + . "gopkg.in/kataras/iris.v6" +) + +// go test -v -run TestConfiguration* + +func TestConfigurationStatic(t *testing.T) { + def := DefaultConfiguration() + + app := New(def) + afterNew := *app.Config + + if !reflect.DeepEqual(def, afterNew) { + t.Fatalf("Default configuration is not the same after NewFromConfig expected:\n %#v \ngot:\n %#v", def, afterNew) + } + + afterNew.Charset = "changed" + + if reflect.DeepEqual(def, afterNew) { + t.Fatalf("Configuration should be not equal, got: %#v", afterNew) + } + + app = New(Configuration{DisableBanner: true}) + + afterNew = *app.Config + + if app.Config.DisableBanner == false { + t.Fatalf("Passing a Configuration field as Option fails, expected DisableBanner to be true but was false") + } + + app = New() // empty , means defaults so + if !reflect.DeepEqual(def, *app.Config) { + t.Fatalf("Default configuration is not the same after NewFromConfig expected:\n %#v \ngot:\n %#v", def, *app.Config) + } +} + +func TestConfigurationOptions(t *testing.T) { + charset := "MYCHARSET" + disableBanner := true + + app := New(OptionCharset(charset), OptionDisableBanner(disableBanner)) + + if got := app.Config.Charset; got != charset { + t.Fatalf("Expected configuration Charset to be: %s but got: %s", charset, got) + } + + if got := app.Config.DisableBanner; got != disableBanner { + t.Fatalf("Expected configuration DisableBanner to be: %#v but got: %#v", disableBanner, got) + } + + // now check if other default values are setted (should be setted automatically) + + expected := DefaultConfiguration() + expected.Charset = charset + expected.DisableBanner = disableBanner + + has := *app.Config + if !reflect.DeepEqual(has, expected) { + t.Fatalf("Default configuration is not the same after New expected:\n %#v \ngot:\n %#v", expected, has) + } +} + +func TestConfigurationOptionsDeep(t *testing.T) { + charset := "MYCHARSET" + disableBanner := true + vhost := "mydomain.com" + // first charset,disableBanner and profilepath, no canonical order. + app := New(OptionCharset(charset), OptionDisableBanner(disableBanner), OptionVHost(vhost)) + + expected := DefaultConfiguration() + expected.Charset = charset + expected.DisableBanner = disableBanner + expected.VHost = vhost + + has := *app.Config + + if !reflect.DeepEqual(has, expected) { + t.Fatalf("DEEP configuration is not the same after New expected:\n %#v \ngot:\n %#v", expected, has) + } +} + +func TestConfigurationYAML(t *testing.T) { + // create the key and cert files on the fly, and delete them when this test finished + yamlFile, ferr := ioutil.TempFile("", "configuration.yml") + + if ferr != nil { + t.Fatal(ferr) + } + + defer func() { + yamlFile.Close() + time.Sleep(50 * time.Millisecond) + os.Remove(yamlFile.Name()) + }() + + yamlConfigurationContents := ` + vHost: iris-go.com + + vScheme: https:// + + readTimeout: 0 + + writeTimeout: 5s + + maxHeaderBytes: 8096 + + checkForUpdates: true + + disablePathCorrection: false + + enablePathEscape: false + + fireMethodNotAllowed: true + + disableBanner: true + + disableBodyConsumptionOnUnmarshal: true + + timeFormat: Mon, 01 Jan 2006 15:04:05 GMT + + charset: UTF-8 + + gzip: true + + ` + yamlFile.WriteString(yamlConfigurationContents) + filename := yamlFile.Name() + app := New(YAML(filename)) + + c := app.Config + + if expected := "iris-go.com"; c.VHost != expected { + t.Fatalf("error on TestConfigurationYAML: Expected VHost %s but got %s", expected, c.VHost) + } + + if expected := "https://"; c.VScheme != expected { + t.Fatalf("error on TestConfigurationYAML: Expected VScheme %s but got %s", expected, c.VScheme) + } + + if expected := 0; c.ReadTimeout != time.Duration(expected) { + t.Fatalf("error on TestConfigurationYAML: Expected ReadTimeout %s but got %s", expected, c.ReadTimeout) + } + + if expected := time.Duration(5 * time.Second); c.WriteTimeout != expected { + t.Fatalf("error on TestConfigurationYAML: Expected WriteTimeout %s but got %s", expected, c.WriteTimeout) + } + + if expected := 8096; c.MaxHeaderBytes != expected { + t.Fatalf("error on TestConfigurationYAML: Expected MaxHeaderBytes %s but got %s", expected, c.MaxHeaderBytes) + } + + if expected := true; c.CheckForUpdates != expected { + t.Fatalf("error on TestConfigurationYAML: Expected checkForUpdates %v but got %v", expected, c.CheckForUpdates) + } + + if expected := false; c.DisablePathCorrection != expected { + t.Fatalf("error on TestConfigurationYAML: Expected DisablePathCorrection %v but got %v", expected, c.DisablePathCorrection) + } + + if expected := false; c.EnablePathEscape != expected { + t.Fatalf("error on TestConfigurationYAML: Expected EnablePathEscape %v but got %v", expected, c.EnablePathEscape) + } + + if expected := true; c.FireMethodNotAllowed != expected { + t.Fatalf("error on TestConfigurationYAML: Expected FireMethodNotAllowed %v but got %v", expected, c.FireMethodNotAllowed) + } + + if expected := true; c.DisableBanner != expected { + t.Fatalf("error on TestConfigurationYAML: Expected DisableBanner %v but got %v", expected, c.DisableBanner) + } + + if expected := true; c.DisableBodyConsumptionOnUnmarshal != expected { + t.Fatalf("error on TestConfigurationYAML: Expected DisableBodyConsumptionOnUnmarshal %v but got %v", + expected, c.DisableBodyConsumptionOnUnmarshal) + } + + if expected := "Mon, 01 Jan 2006 15:04:05 GMT"; c.TimeFormat != expected { + t.Fatalf("error on TestConfigurationYAML: Expected TimeFormat %s but got %s", expected, c.TimeFormat) + } + + if expected := "UTF-8"; c.Charset != expected { + t.Fatalf("error on TestConfigurationYAML: Expected Charset %s but got %s", expected, c.Charset) + } + + if expected := true; c.Gzip != expected { + t.Fatalf("error on TestConfigurationYAML: Expected != %v but got %v", expected, c.Gzip) + } + +} diff --git a/iris.go b/iris.go index bbe8ae47..946b99e4 100644 --- a/iris.go +++ b/iris.go @@ -9,6 +9,7 @@ package iris import ( + "crypto/tls" "fmt" "io" "log" @@ -66,7 +67,21 @@ type Framework struct { // HTTP Server runtime fields is the iris' defined main server, developer can use unlimited number of servers // note: they're available after .Build, and .Serve/Listen/ListenTLS/ListenLETSENCRYPT/ListenUNIX - ln net.Listener + ln net.Listener + // TLSNextProto optionally specifies a function to take over + // ownership of the provided TLS connection when an NPN/ALPN + // protocol upgrade has occurred. The map key is the protocol + // name negotiated. The Handler argument should be used to + // handle HTTP requests and will initialize the Request's TLS + // and RemoteAddr if not already set. The connection is + // automatically closed when the function returns. + // If TLSNextProto is nil, HTTP/2 support is enabled automatically. + TLSNextProto map[string]func(*http.Server, *tls.Conn, http.Handler) + // ConnState specifies an optional callback function that is + // called when a client connection changes state. See the + // ConnState type and associated constants for details. + ConnState func(net.Conn, http.ConnState) + closedManually bool once sync.Once @@ -92,13 +107,14 @@ func DevLogger() LoggerPolicy { // New creates and returns a fresh Iris *Framework instance // with the default configuration if no 'setters' parameters passed. func New(setters ...OptionSetter) *Framework { - s := &Framework{Config: DefaultConfiguration()} + cfg := DefaultConfiguration() + s := &Framework{Config: &cfg} + // +------------------------------------------------------------+ // | Set the config passed from setters | // | or use the default one | // +------------------------------------------------------------+ s.Set(setters...) - { // +------------------------------------------------------------+ // | Module Name: Logger | @@ -389,19 +405,13 @@ func (s *Framework) Serve(ln net.Listener) error { ReadTimeout: s.Config.ReadTimeout, WriteTimeout: s.Config.WriteTimeout, MaxHeaderBytes: s.Config.MaxHeaderBytes, - TLSNextProto: s.Config.TLSNextProto, - ConnState: s.Config.ConnState, + TLSNextProto: s.TLSNextProto, + ConnState: s.ConnState, Addr: s.Config.VHost, ErrorLog: s.policies.LoggerPolicy.ToLogger(log.LstdFlags), Handler: s.Router, } - if s.Config.TLSNextProto != nil { - srv.TLSNextProto = s.Config.TLSNextProto - } - if s.Config.ConnState != nil { - srv.ConnState = s.Config.ConnState - } // print the banner and wait for system channel interrupt go s.postServe() // finally return the error or block here, remember, @@ -535,7 +545,9 @@ func (s *Framework) isRunning() bool { // Close is not working propetly but it releases the host:port. func (s *Framework) Close() error { + if s.isRunning() { + s.closedManually = true ///TODO: // This code below doesn't works without custom net listener which will work in a stop channel which will cost us performance. // This will work on go v1.8 BUT FOR NOW make unexported reserve/reboot/restart in order to be non confusual for the user.