Add HTTP/2 Example and Websocket wss:// too in the same time :)

Former-commit-id: fd4c12043d6ed739770236e014ccd2f0f4f5a84c
This commit is contained in:
Gerasimos (Makis) Maropoulos 2017-02-17 06:49:54 +02:00
parent 5055546a38
commit 48b470f5da
6 changed files with 353 additions and 54 deletions

View File

@ -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. 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! - 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) - Router (two lines to add, new features)
- Template engines (two lines to add, same features as before, except their easier configuration) - 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) - 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)

View File

@ -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())
}

View File

@ -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($("<div><center><h3>Disconnected</h3></center></div>"));
});
w.On("chat", function (message) {
appendMessage($("<div>" + message + "</div>"));
});
$("#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;
}
}

View File

@ -0,0 +1,24 @@
<html>
<head>
<title>{{ .Title}}</title>
</head>
<body>
<div id="messages"
style="border-width: 1px; border-style: solid; height: 400px; width: 375px;">
</div>
<input type="text" id="messageTxt" />
<button type="button" id="sendBtn">Send</button>
<script type="text/javascript">
var HOST = {{.Host}}
</script>
<script src="/js/vendor/jquery-2.2.3.min.js"></script>
<!-- This is auto-serving by the Iris, you don't need to have this file in your disk-->
<script src="/iris-ws.js"></script>
<!-- -->
<script src="/js/chat.js"></script>
</body>
</html>

123
iris.go
View File

@ -100,8 +100,6 @@ type Framework struct {
// These are setted by user's call to .Adapt // These are setted by user's call to .Adapt
policies Policies policies Policies
ln net.Listener // setted on Listten/Serve funcions, available after 'Boot'
// TLSNextProto optionally specifies a function to take over // TLSNextProto optionally specifies a function to take over
// ownership of the provided TLS connection when an NPN/ALPN // ownership of the provided TLS connection when an NPN/ALPN
// protocol upgrade has occurred. The map key is the protocol // 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) { s.Adapt(EventPolicy{Boot: func(s *Framework) {
// set the host and scheme // set the host and scheme
if s.Config.VHost == "" { // if not setted by Listen functions if s.Config.VHost == "" { // if not setted by Listen functions
if s.ln != nil { // but user called .Serve s.Config.VHost = DefaultServerAddr
// 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
}
} }
// 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 s.Config.VScheme == "" {
// if :443 or :https then returns https:// otherwise http://
s.Config.VScheme = ParseScheme(s.Config.VHost) s.Config.VScheme = ParseScheme(s.Config.VHost)
} }
}}) }})
{ {
@ -436,28 +430,21 @@ func (s *Framework) Boot() (firstTime bool) {
return return
} }
// Serve serves incoming connections from the given listener. func (s *Framework) setupServe() (srv *http.Server, deferFn func()) {
//
// 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
s.closedManually = false s.closedManually = false
s.Boot() s.Boot()
// post any panics to the user defined logger. deferFn = func() {
defer func() { // post any panics to the user defined logger.
if rerr := recover(); rerr != nil { if rerr := recover(); rerr != nil {
if err, ok := rerr.(error); ok { if err, ok := rerr.(error); ok {
s.handlePanic(err) s.handlePanic(err)
} }
} }
}() }
srv := &http.Server{ srv = &http.Server{
ReadTimeout: s.Config.ReadTimeout, ReadTimeout: s.Config.ReadTimeout,
WriteTimeout: s.Config.WriteTimeout, WriteTimeout: s.Config.WriteTimeout,
MaxHeaderBytes: s.Config.MaxHeaderBytes, MaxHeaderBytes: s.Config.MaxHeaderBytes,
@ -467,21 +454,38 @@ func (s *Framework) Serve(ln net.Listener) error {
ErrorLog: s.policies.LoggerPolicy.ToLogger(log.LstdFlags), ErrorLog: s.policies.LoggerPolicy.ToLogger(log.LstdFlags),
Handler: s.Router, Handler: s.Router,
} }
// Set the grace shutdown, it's just a func no need to make things complicated // Set the grace shutdown, it's just a func no need to make things complicated
// all are managed by net/http now. // all are managed by net/http now.
s.Shutdown = func(ctx context.Context) error { s.Shutdown = func(ctx context.Context) error {
// order matters, look s.handlePanic // order matters, look s.handlePanic
s.closedManually = true s.closedManually = true
err := srv.Shutdown(ctx) err := srv.Shutdown(ctx)
s.ln = nil
return err 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 // print the banner and wait for system channel interrupt
go s.postServe() go s.postServe()
// finally return the error or block here, remember, return srv.Serve(ln)
// until go1.8 these are our best options.
return srv.Serve(s.ln)
} }
func (s *Framework) postServe() { func (s *Framework) postServe() {
@ -507,14 +511,15 @@ func (s *Framework) postServe() {
// If you need to manually monitor any error please use `.Serve` instead. // If you need to manually monitor any error please use `.Serve` instead.
func (s *Framework) Listen(addr string) { func (s *Framework) Listen(addr string) {
addr = ParseHost(addr) 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 { if portIdx := strings.IndexByte(addr, ':'); portIdx < 0 {
// missing port part, add it // missing port part, add it
addr = addr + ":80" 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. // If you need to manually monitor any error please use `.Serve` instead.
func (s *Framework) ListenTLS(addr string, certFile, keyFile string) { func (s *Framework) ListenTLS(addr string, certFile, keyFile string) {
addr = ParseHost(addr) 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) srv, fn := s.setupServe()
if err != nil { // We are doing the same parts as .Serve does but instead we run srv.ListenAndServeTLS
s.handlePanic(err) // because of un-exported net/http.server.go:setupHTTP2_ListenAndServeTLS function which
} // broke our previous flow but no problem :)
s.Must(s.Serve(ln)) 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 // 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 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 "" // 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, // example: https://github.com/iris-contrib/examples/blob/master/letsencrypt/main.go
// NOTE: if you are ready for production then use `$app.Serve(iris.LETSENCRYPTPROD("mydomain.com"))` instead
func (s *Framework) ListenLETSENCRYPT(addr string, cacheFileOptional ...string) { func (s *Framework) ListenLETSENCRYPT(addr string, cacheFileOptional ...string) {
addr = ParseHost(addr) 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...) ln, err := LETSENCRYPT(addr, cacheFileOptional...)
if err != nil { if err != nil {
s.handlePanic(err) s.handlePanic(err)

View File

@ -607,9 +607,9 @@ func (router *Router) StaticHandler(reqPath string, systemPath string, showList
// second parameter: the system directory // second parameter: the system directory
// third OPTIONAL parameter: the exception routes // third OPTIONAL parameter: the exception routes
// (= give priority to these routes instead of the static handler) // (= 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 // As a special case, the returned file server redirects any request
// ending in "/index.html" to the same path, without the final // 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 ). // StaticWeb calls the StaticHandler(reqPath, systemPath, listingDirectories: false, gzip: false ).
func (router *Router) StaticWeb(reqPath string, systemPath string, exceptRoutes ...RouteInfo) RouteInfo { func (router *Router) StaticWeb(reqPath string, systemPath string, exceptRoutes ...RouteInfo) RouteInfo {
h := router.StaticHandler(reqPath, systemPath, false, false, exceptRoutes...) h := router.StaticHandler(reqPath, systemPath, false, false, exceptRoutes...)
routePath := validateWildcard(reqPath, "file") paramName := "file"
return router.registerResourceRoute(routePath, h) 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 // Layout oerrides the parent template layout with a more specific layout for this Party