diff --git a/HISTORY.md b/HISTORY.md index dc9dc268..6b123095 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -11,7 +11,8 @@ Users already notified for some breaking-changes, this section will help you to adapt the new changes to your application, it contains an overview of the new features too. - Shutdown with `app.Shutdown(context.Context) error`, no need for any third-parties, with `EventPolicy.Interrupted` and Go's 1.8 Gracefully Shutdown feature you're ready to go! -- HTTP/2 Go 1.8 `context.Push(target string, opts *http.PushOptions) error` is supported +- HTTP/2 Go 1.8 `context.Push(target string, opts *http.PushOptions) error` is supported, example can be found [here](https://github.com/kataras/iris.v6/blob/master/adaptors/websocket/_examples/webocket_secure/main.go) + - Router (two lines to add, new features) - 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) diff --git a/adaptors/websocket/_examples/websocket_secure/main.go b/adaptors/websocket/_examples/websocket_secure/main.go new file mode 100644 index 00000000..2ceee5ec --- /dev/null +++ b/adaptors/websocket/_examples/websocket_secure/main.go @@ -0,0 +1,197 @@ +package main + +import ( + "fmt" // optional + "io/ioutil" // optional + "os" // optional + "time" // optional + + "gopkg.in/kataras/iris.v6" + "gopkg.in/kataras/iris.v6/adaptors/httprouter" + "gopkg.in/kataras/iris.v6/adaptors/view" + "gopkg.in/kataras/iris.v6/adaptors/websocket" +) + +type clientPage struct { + Title string + Host string +} + +func main() { + app := iris.New() + app.Adapt(iris.DevLogger()) // enable all (error) logs + app.Adapt(httprouter.New()) // select the httprouter as the servemux + app.Adapt(view.HTML("./templates", ".html")) // select the html engine to serve templates + + ws := websocket.New(websocket.Config{ + // the path which the websocket client should listen/registed to, + Endpoint: "/my_endpoint", + // the client-side javascript static file path + // which will be served by Iris. + // default is /iris-ws.js + // if you change that you have to change the bottom of templates/client.html + // script tag: + ClientSourcePath: "/iris-ws.js", + // + // Set the timeouts, 0 means no timeout + // websocket has more configuration, go to ../../config.go for more: + // WriteTimeout: 0, + // ReadTimeout: 0, + // by-default all origins are accepted, you can change this behavior by setting: + // CheckOrigin: (r *http.Request ) bool {}, + // + // + // IDGenerator used to create (and later on, set) + // an ID for each incoming websocket connections (clients). + // The request is an argument which you can use to generate the ID (from headers for example). + // If empty then the ID is generated by DefaultIDGenerator: randomString(64): + // IDGenerator func(ctx *iris.Context) string {}, + }) + + app.Adapt(ws) // adapt the websocket server, you can adapt more than one with different Endpoint + + app.StaticWeb("/js", "./static/js") // static route to serve our javascript files + + app.Get("/", func(ctx *iris.Context) { + // send our custom javascript source file before client really asks for that + // using the new go v1.8's HTTP/2 Push. + // Note that you have to listen using ListenTLS/ListenLETSENCRYPT in order this to work. + if err := ctx.ResponseWriter.Push("/js/chat.js", nil); err != nil { + app.Log(iris.DevMode, err.Error()) + } + ctx.Render("client.html", clientPage{"Client Page", ctx.Host()}) + }) + + var myChatRoom = "room1" + + ws.OnConnection(func(c websocket.Connection) { + // Context returns the (upgraded) *iris.Context of this connection + // avoid using it, you normally don't need it, + // websocket has everything you need to authenticate the user BUT if it's necessary + // then you use it to receive user information, for example: from headers. + + // ctx := c.Context() + + // join to a room (optional) + c.Join(myChatRoom) + + c.On("chat", func(message string) { + if message == "leave" { + c.Leave(myChatRoom) + c.To(myChatRoom).Emit("chat", "Client with ID: "+c.ID()+" left from the room and cannot send or receive message to/from this room.") + c.Emit("chat", "You have left from the room: "+myChatRoom+" you cannot send or receive any messages from others inside that room.") + return + } + // to all except this connection -> + // c.To(websocket.Broadcast).Emit("chat", "Message from: "+c.ID()+"-> "+message) + // to all connected clients: c.To(websocket.All) + + // to the client itself -> + //c.Emit("chat", "Message from myself: "+message) + + //send the message to the whole room, + //all connections are inside this room will receive this message + c.To(myChatRoom).Emit("chat", "From: "+c.ID()+": "+message) + }) + + // or create a new leave event + // c.On("leave", func() { + // c.Leave(myChatRoom) + // }) + + c.OnDisconnect(func() { + fmt.Printf("Connection with ID: %s has been disconnected!\n", c.ID()) + }) + }) + + listenTLS(app) + +} + +// a test listenTLS for our localhost +func listenTLS(app *iris.Framework) { + + const ( + testTLSCert = `-----BEGIN CERTIFICATE----- +MIIDBTCCAe2gAwIBAgIJAOYzROngkH6NMA0GCSqGSIb3DQEBBQUAMBkxFzAVBgNV +BAMMDmxvY2FsaG9zdDo4MDgwMB4XDTE3MDIxNzAzNDM1NFoXDTI3MDIxNTAzNDM1 +NFowGTEXMBUGA1UEAwwObG9jYWxob3N0OjgwODAwggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQCfsiVHO14FpKsi0pvBv68oApQm2MO+dCvq87sDU4E0QJhG +KV1RCUmQVypChEqdLlUQsopcXSyKwbWoyg1/KNHYO3DHMfePb4bC1UD2HENq7Ph2 +8QJTEi/CJvUB9hqke/YCoWYdjFiI3h3Hw8q5whGO5XR3R23z69vr5XxoNlcF2R+O +TdkzArd0CWTZS27vbgdnyi9v3Waydh/rl+QRtPUgEoCEqOOkMSMldXO6Z9GlUk9b +FQHwIuEnlSoVFB5ot5cqebEjJnWMLLP83KOCQekJeHZOyjeTe8W0Fy1DGu5fvFNh +xde9e/7XlFE//00vT7nBmJAUV/2CXC8U5lsjLEqdAgMBAAGjUDBOMB0GA1UdDgQW +BBQOfENuLn/t0Z4ZY1+RPWaz7RBH+TAfBgNVHSMEGDAWgBQOfENuLn/t0Z4ZY1+R +PWaz7RBH+TAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQBG7AEEuIq6 +rWCE5I2t4IXz0jN7MilqEhUWDbUajl1paYf6Ikx5QhMsFx21p6WEWYIYcnWAKZe2 +chAgnnGojuxdx0qjiaH4N4xWGHsWhaesnIF1xJepLlX3kJZQURvRxM4wlljlQPIb +9tqzKP131K1HDqplAtp7nWQ72m3J0ZfzH0mYIUxuaS/uQIVtgKqdilwy/VE5dRZ9 +QFIb4G9TnNThXMqgTLjfNr33jVbTuv6fzKHYNbCkP3L10ydEs/ddlREmtsn9nE8Q +XCTIYXzA2kr5kWk7d3LkUiSvu3g2S1Ol1YaIKaOQyRveseCGwR4xohLT+dPUW9dL +3hDVLlwE3mB3 +-----END CERTIFICATE----- + +` + testTLSKey = `-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAn7IlRzteBaSrItKbwb+vKAKUJtjDvnQr6vO7A1OBNECYRild +UQlJkFcqQoRKnS5VELKKXF0sisG1qMoNfyjR2DtwxzH3j2+GwtVA9hxDauz4dvEC +UxIvwib1AfYapHv2AqFmHYxYiN4dx8PKucIRjuV0d0dt8+vb6+V8aDZXBdkfjk3Z +MwK3dAlk2Utu724HZ8ovb91msnYf65fkEbT1IBKAhKjjpDEjJXVzumfRpVJPWxUB +8CLhJ5UqFRQeaLeXKnmxIyZ1jCyz/NyjgkHpCXh2Tso3k3vFtBctQxruX7xTYcXX +vXv+15RRP/9NL0+5wZiQFFf9glwvFOZbIyxKnQIDAQABAoIBAEzBx4ExW8PCni8i +o5LAm2PTuXniflMwa1uGwsCahmOjGI3AnAWzPRSPkNRf2a0q8+AOsMosTphy+umi +FFKmQBZ6m35i2earaE6FSbABbbYbKGGi/ccH2sSrDOBgdfXRTzF8eiSBrJw8hnvZ +87rNOLtCNnSOdJ7lItODfgRo+fLo4uQenJ8VONYwtwm1ejn8qLXq8O5zF66IYUD6 +gAzqOiAWumgZL0tEmndeQ+noe4STpJZlOjiCsA12NiJaKDDeDIn5A/pXce+bYNfJ +k4yoroyq/JXBkhyuZDvX9vYp5AA+Q68h8/KmsKkifUgSGSHun5/80lYyT/f60TLX +PxT9GYECgYEA0s8qck7L29nBBTQ6IPF3GHGmqiRdfH+qhP/Jn4NtoW3XuVe4A15i +REq1L8WAiOUIBnBaD8HzbeioqJJYx1pu7x9h/GCNDhdBfwhTjnBe+JjfLqvJKnc0 +HUT5wj4DVqattxKzUW8kTRBSWtVremzeffDo+EL6dnR7Bc02Ibs4WpUCgYEAwe34 +Uqhie+/EFr4HjYRUNZSNgYNAJkKHVxk4qGzG5VhvjPafnHUbo+Kk/0QW7eIB+kvR +FDO8oKh9wTBrWZEcLJP4jDIKh4y8hZTo9B8EjxFONXVxZlOSYuGjheL8AiLzE7L9 +C1spaKMM/MyxAXDRHpG/NeEgXM7Kn6kUGwJdNekCgYAshLNiEGHcu8+XWcAs1NFh +yB56L9PORuerzpi1pvuv65JzAaNKktQNt/krbXoHbtaTBYb/bOYLf+aeMsmsz9w9 +g1MeCQXAxAiA2zFKE1D7Ds2S/ZQt8559z+MusgnicrCcyMY1nFL+M0QxCoD4CaWy +0v1f8EUUXuTcBMo5tV/hQQKBgDoBBW8jsiFDu7DZscSgOde00QZVzZAkAfsJLisi +LfNXGjZdZawUUuoX1iYLpZgNK25D0wtp1hdvjf2Ej/dAMd8bexHjvcaBT7ncqjiq +NmDcWjofIIXspTIyLwjStXGmJnJT7N/CqoYDjtTmHGND7Shpi3mAFn/r0isjFUJm +2J5RAoGALuGXxzmSRWmkIp11F/Qr3PBFWBWkrRWaH2TRLMhrU/wO8kCsSyo4PmAZ +ltOfD7InpDiCu43hcDPQ/29FUbDnmAhvMnmIQuHXGgPF/LhqEhbKPA/o/eZdQVCK +QG+tmveBBIYMed5YbWstZu/95lIHF+u8Hl+Z6xgveozfE5yqiUA= +-----END RSA PRIVATE KEY----- + + ` + ) + + // create the key and cert files on the fly, and delete them when this test finished + certFile, ferr := ioutil.TempFile("", "cert") + + if ferr != nil { + panic(ferr) + } + + keyFile, ferr := ioutil.TempFile("", "key") + if ferr != nil { + panic(ferr) + } + + certFile.WriteString(testTLSCert) + keyFile.WriteString(testTLSKey) + + // add an event when control+C pressed, to remove the temp cert and key files. + app.Adapt(iris.EventPolicy{ + Interrupted: func(*iris.Framework) { + certFile.Close() + time.Sleep(50 * time.Millisecond) + os.Remove(certFile.Name()) + + keyFile.Close() + time.Sleep(50 * time.Millisecond) + os.Remove(keyFile.Name()) + }, + }) + + // https://localhost + app.ListenTLS("localhost:443", certFile.Name(), keyFile.Name()) +} diff --git a/adaptors/websocket/_examples/websocket_secure/static/js/chat.js b/adaptors/websocket/_examples/websocket_secure/static/js/chat.js new file mode 100644 index 00000000..1bef06bd --- /dev/null +++ b/adaptors/websocket/_examples/websocket_secure/static/js/chat.js @@ -0,0 +1,38 @@ +var messageTxt; +var messages; + +$(function () { + + messageTxt = $("#messageTxt"); + messages = $("#messages"); + + /* secure wss because we ListenTLS */ + w = new Ws("wss://" + HOST + "/my_endpoint"); + w.OnConnect(function () { + console.log("Websocket connection established"); + }); + + w.OnDisconnect(function () { + appendMessage($("

Disconnected

")); + }); + + w.On("chat", function (message) { + appendMessage($("
" + message + "
")); + }); + + $("#sendBtn").click(function () { + w.Emit("chat", messageTxt.val().toString()); + messageTxt.val(""); + }); + +}) + + +function appendMessage(messageDiv) { + var theDiv = messages[0]; + var doScroll = theDiv.scrollTop == theDiv.scrollHeight - theDiv.clientHeight; + messageDiv.appendTo(messages); + if (doScroll) { + theDiv.scrollTop = theDiv.scrollHeight - theDiv.clientHeight; + } +} diff --git a/adaptors/websocket/_examples/websocket_secure/templates/client.html b/adaptors/websocket/_examples/websocket_secure/templates/client.html new file mode 100644 index 00000000..03387567 --- /dev/null +++ b/adaptors/websocket/_examples/websocket_secure/templates/client.html @@ -0,0 +1,24 @@ + + + +{{ .Title}} + + + +
+ +
+ + + + + + + + + + + diff --git a/iris.go b/iris.go index 7f35e202..96facc91 100644 --- a/iris.go +++ b/iris.go @@ -100,8 +100,6 @@ type Framework struct { // These are setted by user's call to .Adapt policies Policies - ln net.Listener // setted on Listten/Serve funcions, available after 'Boot' - // 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 @@ -210,19 +208,15 @@ func New(setters ...OptionSetter) *Framework { s.Adapt(EventPolicy{Boot: func(s *Framework) { // set the host and scheme if s.Config.VHost == "" { // if not setted by Listen functions - if s.ln != nil { // but user called .Serve - // then take the listener's addr - s.Config.VHost = s.ln.Addr().String() - } else { - // if no .Serve or .Listen called, then the user should set the VHost manually, - // however set it to a default value here for any case - s.Config.VHost = DefaultServerAddr - } + s.Config.VHost = DefaultServerAddr } - // if user didn't specified a scheme then get it from the VHost, which is already setted at before statements + // if user didn't specified a scheme then get it from the VHost, + // which is already setted at before statements if s.Config.VScheme == "" { + // if :443 or :https then returns https:// otherwise http:// s.Config.VScheme = ParseScheme(s.Config.VHost) } + }}) { @@ -436,28 +430,21 @@ func (s *Framework) Boot() (firstTime bool) { return } -// Serve serves incoming connections from the given listener. -// -// Serve blocks until the given listener returns permanent error. -func (s *Framework) Serve(ln net.Listener) error { - if s.ln != nil { - return errors.New("server is already started and listening") - } - - s.ln = ln +func (s *Framework) setupServe() (srv *http.Server, deferFn func()) { s.closedManually = false + s.Boot() - // post any panics to the user defined logger. - defer func() { + deferFn = func() { + // post any panics to the user defined logger. if rerr := recover(); rerr != nil { if err, ok := rerr.(error); ok { s.handlePanic(err) } } - }() + } - srv := &http.Server{ + srv = &http.Server{ ReadTimeout: s.Config.ReadTimeout, WriteTimeout: s.Config.WriteTimeout, MaxHeaderBytes: s.Config.MaxHeaderBytes, @@ -467,21 +454,38 @@ func (s *Framework) Serve(ln net.Listener) error { ErrorLog: s.policies.LoggerPolicy.ToLogger(log.LstdFlags), Handler: s.Router, } + // Set the grace shutdown, it's just a func no need to make things complicated // all are managed by net/http now. s.Shutdown = func(ctx context.Context) error { // order matters, look s.handlePanic s.closedManually = true err := srv.Shutdown(ctx) - s.ln = nil return err } + return +} + +// Serve serves incoming connections from the given listener. +// +// Serve blocks until the given listener returns permanent error. +func (s *Framework) Serve(ln net.Listener) error { + if ln == nil { + return errors.New("nil net.Listener on Serve") + } + + // if user called .Serve and doesn't uses any nginx-like balancers. + if s.Config.VHost == "" { + s.Config.VHost = ParseHost(ln.Addr().String()) + } // Scheme will be checked from Boot state. + + srv, fn := s.setupServe() + defer fn() + // print the banner and wait for system channel interrupt go s.postServe() - // finally return the error or block here, remember, - // until go1.8 these are our best options. - return srv.Serve(s.ln) + return srv.Serve(ln) } func (s *Framework) postServe() { @@ -507,14 +511,15 @@ func (s *Framework) postServe() { // If you need to manually monitor any error please use `.Serve` instead. func (s *Framework) Listen(addr string) { addr = ParseHost(addr) - if s.Config.VHost == "" { - s.Config.VHost = addr - // this will be set as the front-end listening addr - } - // only here, other Listen functions should throw an error if port is missing. - // User should know how to fix them on ListenUNIX/ListenTLS/ListenLETSENCRYPT/Serve, - // they are used by more 'advanced' devs, mostly. + // if .Listen called normally and VHost is not setted, + // so it's Host is the Real listening addr and user-given + if s.Config.VHost == "" { + s.Config.VHost = addr // as it is + // this will be set as the front-end listening addr + } // VScheme will be checked on Boot. + + // this check, only here, other Listen functions should throw an error if port is missing. if portIdx := strings.IndexByte(addr, ':'); portIdx < 0 { // missing port part, add it addr = addr + ":80" @@ -538,16 +543,27 @@ func (s *Framework) Listen(addr string) { // If you need to manually monitor any error please use `.Serve` instead. func (s *Framework) ListenTLS(addr string, certFile, keyFile string) { addr = ParseHost(addr) - if s.Config.VHost == "" { - s.Config.VHost = addr - // this will be set as the front-end listening addr + + { + // set it before Boot, be-careful VHost and VScheme are used by nginx users too + // we don't want to alt them. + if s.Config.VHost == "" { + s.Config.VHost = addr + // this will be set as the front-end listening addr + } + if s.Config.VScheme == "" { + s.Config.VScheme = SchemeHTTPS + } } - ln, err := TLS(addr, certFile, keyFile) - if err != nil { - s.handlePanic(err) - } - s.Must(s.Serve(ln)) + srv, fn := s.setupServe() + // We are doing the same parts as .Serve does but instead we run srv.ListenAndServeTLS + // because of un-exported net/http.server.go:setupHTTP2_ListenAndServeTLS function which + // broke our previous flow but no problem :) + defer fn() + // print the banner and wait for system channel interrupt + go s.postServe() + s.Must(srv.ListenAndServeTLS(certFile, keyFile)) } // ListenLETSENCRYPT starts a server listening at the specific nat address @@ -557,16 +573,25 @@ func (s *Framework) ListenTLS(addr string, certFile, keyFile string) { // if you skip the second parameter then the cache file is "./letsencrypt.cache" // if you want to disable cache then simple pass as second argument an empty empty string "" // -// example: https://github.com/iris-contrib/examples/blob/master/letsencrypt/main.go +// Note: HTTP/2 Push is not working with LETSENCRYPT, you have to use ListenTLS to enable HTTP/2 +// Because net/http's author didn't exported the functions to tell the server that is using HTTP/2... // -// supports localhost domains for testing, -// NOTE: if you are ready for production then use `$app.Serve(iris.LETSENCRYPTPROD("mydomain.com"))` instead +// example: https://github.com/iris-contrib/examples/blob/master/letsencrypt/main.go func (s *Framework) ListenLETSENCRYPT(addr string, cacheFileOptional ...string) { addr = ParseHost(addr) - if s.Config.VHost == "" { - s.Config.VHost = addr - // this will be set as the front-end listening addr + + { + // set it before Boot, be-careful VHost and VScheme are used by nginx users too + // we don't want to alt them. + if s.Config.VHost == "" { + s.Config.VHost = addr + // this will be set as the front-end listening addr + } + if s.Config.VScheme == "" { + s.Config.VScheme = SchemeHTTPS + } } + ln, err := LETSENCRYPT(addr, cacheFileOptional...) if err != nil { s.handlePanic(err) diff --git a/router.go b/router.go index fb9e33ce..4360bd68 100644 --- a/router.go +++ b/router.go @@ -607,9 +607,9 @@ func (router *Router) StaticHandler(reqPath string, systemPath string, showList // second parameter: the system directory // third OPTIONAL parameter: the exception routes // (= give priority to these routes instead of the static handler) -// for more options look iris.StaticHandler. +// for more options look router.StaticHandler. // -// iris.StaticWeb("/static", "./static") +// router.StaticWeb("/static", "./static") // // As a special case, the returned file server redirects any request // ending in "/index.html" to the same path, without the final @@ -618,8 +618,22 @@ func (router *Router) StaticHandler(reqPath string, systemPath string, showList // StaticWeb calls the StaticHandler(reqPath, systemPath, listingDirectories: false, gzip: false ). func (router *Router) StaticWeb(reqPath string, systemPath string, exceptRoutes ...RouteInfo) RouteInfo { h := router.StaticHandler(reqPath, systemPath, false, false, exceptRoutes...) - routePath := validateWildcard(reqPath, "file") - return router.registerResourceRoute(routePath, h) + paramName := "file" + routePath := validateWildcard(reqPath, paramName) + handler := func(ctx *Context) { + h(ctx) + if fname := ctx.Param(paramName); fname != "" { + cType := fs.TypeByExtension(fname) + if cType != contentBinary && !strings.Contains(cType, "charset") { + cType += "; charset=" + ctx.framework.Config.Charset + } + + ctx.SetContentType(cType) + } + + } + + return router.registerResourceRoute(routePath, handler) } // Layout oerrides the parent template layout with a more specific layout for this Party