I am getting a panic which I try to understand but I am not really sure why it panics. The error looks like this:
main.HTTPSNonWWWRedirect.func1(0x9a5a20, 0xc42015c2a0, 0xc420441400)
/srv/www/go/src/srorapp.no/handler.go:119 +0x1ef
net/http.HandlerFunc.ServeHTTP(0xc4200c5f20, 0x9a5a20, 0xc42015c2a0, 0xc420441400)
/usr/local/go/src/net/http/server.go:1918 +0x44
net/http.serverHandler.ServeHTTP(0xc4200696c0, 0x9a5a20, 0xc42015c2a0, 0xc420441400)
/usr/local/go/src/net/http/server.go:2619 +0xb4
net/http.(*conn).serve(0xc42006d180, 0x9a5fa0, 0xc42031e840)
/usr/local/go/src/net/http/server.go:1801 +0x71d
created by net/http.(*Server).Serve
/usr/local/go/src/net/http/server.go:2720 +0x288
It looks like it is being fired from a function called HTTPSNonWWWRedirect. Which is a http middleware that I have created:
// HTTPSNonWWWRedirect redirects http requests to https non www.
func HTTPSNonWWWRedirect(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.TLS != nil {
// If already using HTTPS then continue.
next.ServeHTTP(w, r)
return
}
u := *r.URL
u.Scheme = "https"
if r.Host[:3] != "www" {
u.Host = r.Host
http.Redirect(w, r, u.String(), http.StatusMovedPermanently)
return
}
u.Host = r.Host[4:]
http.Redirect(w, r, u.String(), http.StatusMovedPermanently)
})
}
This function is being used alongside with:
// NonWWWRedirect redirects www requests to non www.
func NonWWWRedirect(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Host[:3] != "www" {
// If already non www, then continue.
next.ServeHTTP(w, r)
return
}
u := *r.URL
u.Host = r.Host[4:]
u.Scheme = utils.Scheme(r)
http.Redirect(w, r, u.String(), http.StatusMovedPermanently)
})
}
And then I have two functions that serves from port 80 and 443.
func serveHTTP(h http.Handler) {
log.Fatal(http.ListenAndServe(":80", h))
}
func serveHTTPS(h http.Handler) {
log.Fatal(http.ListenAndServeTLS(":443", cfg.TLSCertFile, cfg.TLSKeyFile, h))
}
I have made a wrapper around julienschmidt httprouter to make things more convenient:
// https://gist.github.com/nmerouze/5ed810218c661b40f5c4
type router struct {
r *httprouter.Router
}
func newRouter() *router {
return &router{r: httprouter.New()}
}
In main I have something like this:
func main() {
router := newRouter()
recover := alice.New(recoverHandler)
// ....
redirect := alice.New(HTTPSNonWWWRedirect, NonWWWRedirect)
handler := redirect.Then(router.r)
go serveHTTP(handler)
serveHTTPS(handler)
}
Here are the contents of handler.go
package main
import (
"context"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/julienschmidt/httprouter"
cfg "srorapp.no/config"
"srorapp.no/user"
"srorapp.no/utils"
)
// https://gist.github.com/nmerouze/5ed810218c661b40f5c4
type router struct {
r *httprouter.Router
}
func newRouter() *router {
return &router{r: httprouter.New()}
}
var paramsKey utils.CtxKey = "params"
func paramsHandler(h http.Handler) httprouter.Handle {
return httprouter.Handle(func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
ctx := context.WithValue(r.Context(), paramsKey, ps)
h.ServeHTTP(w, r.WithContext(ctx))
})
}
// https://www.calhoun.io/pitfalls-of-context-values-and-how-to-avoid-or-mitigate-them/
func params(r *http.Request) httprouter.Params {
// https://blog.golang.org/context
// "Value allows a Context to carry request-scoped data.
// That data must be safe for simultaneous use by multiple goroutines."
// http://stackoverflow.com/questions/42893937/do-i-need-mutex-read-lock-when-retrieving-slice-values-with-context-in-go?noredirect=1#comment72889988_42893937
// Do not need a mutex here since I will access it in a simple way and not concurrently.
value := r.Context().Value(paramsKey)
if ps, ok := value.(httprouter.Params); ok {
return ps
}
return httprouter.Params{}
}
func (r *router) GET(path string, handler http.Handler) {
r.r.GET(path, paramsHandler(handler))
}
func (r *router) POST(path string, handler http.Handler) {
r.r.POST(path, paramsHandler(handler))
}
// -------------------------------------------------------------------------------------------
type errorHandlerFunc func(http.ResponseWriter, *http.Request) error
// http://stackoverflow.com/questions/42871194/how-can-i-combine-go-middleware-pattern-with-error-returning-request-handlers/42876307#42876307
func errorHandler(h errorHandlerFunc) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Set Content-Type to plain text when sending http.Error.
w.Header().Set("Content-Type", "application/json")
if err := h(w, r); err != nil {
// w.Header().Set("Content-Type", "text/html; charset=utf-8")
// http.Error(w, err.Error(), http.StatusBadRequest)
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
log.Println(err)
}
})
}
// -------------------------------------------------------------------------------------------
// https://github.com/labstack/echo/blob/master/middleware/redirect.go
// http://stackoverflow.com/questions/42916952/do-you-have-to-return-after-a-http-redirect-if-you-want-the-code-after-to-stop-e
// https://play.golang.org/p/uk0S1hCPhu
// HTTPSRedirect redirects HTTP to HTTPS.
func HTTPSRedirect(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.TLS != nil {
// If already using HTTPS then continue.
next.ServeHTTP(w, r)
return
}
http.Redirect(w, r, "https://"+r.Host+r.RequestURI, http.StatusMovedPermanently)
})
}
// HTTPSWWWRedirect redirects http requests to https www.
func HTTPSWWWRedirect(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.TLS != nil {
// If already using HTTPS then continue.
next.ServeHTTP(w, r)
return
}
u := *r.URL // Dereference *url.URL to make a copy.
u.Scheme = "https"
u.Host = "www." + strings.TrimPrefix(r.Host, "www.")
http.Redirect(w, r, u.String(), http.StatusMovedPermanently)
})
}
// HTTPSNonWWWRedirect redirects http requests to https non www.
func HTTPSNonWWWRedirect(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.TLS != nil {
// If already using HTTPS then continue.
next.ServeHTTP(w, r)
return
}
u := *r.URL
u.Scheme = "https"
if r.Host[:3] != "www" {
u.Host = r.Host
http.Redirect(w, r, u.String(), http.StatusMovedPermanently)
return
}
u.Host = r.Host[4:]
http.Redirect(w, r, u.String(), http.StatusMovedPermanently)
})
}
// WWWRedirect redirects non www requests to www.
func WWWRedirect(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Host[:3] == "www" {
// If already www, then continue.
next.ServeHTTP(w, r)
return
}
u := *r.URL
u.Host = "www." + r.Host
u.Scheme = utils.Scheme(r)
http.Redirect(w, r, u.String(), http.StatusMovedPermanently)
})
}
// NonWWWRedirect redirects www requests to non www.
func NonWWWRedirect(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Host[:3] != "www" {
// If already non www, then continue.
next.ServeHTTP(w, r)
return
}
u := *r.URL
u.Host = r.Host[4:]
u.Scheme = utils.Scheme(r)
http.Redirect(w, r, u.String(), http.StatusMovedPermanently)
})
}
func canServeGzip(r *http.Request) bool {
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
// If for some weird reason client does not understand gzip.
return false
}
path := filepath.FromSlash(filepath.Join(cfg.PublicHTML, r.URL.Path))
if _, err := os.Stat(path); os.IsNotExist(err) {
// If file or folder does not exists.
return false
}
fileExt := filepath.Ext(r.URL.Path)
if !utils.StringInSlice(cfg.GzipFileExt, fileExt) {
// This file should not be served as gzipped content.
return false
}
// Only serve gzipped file if it already exists.
if _, err := os.Stat(path + ".gz"); os.IsNotExist(err) {
// TODO: Create the gzipped file.
// http://stackoverflow.com/questions/16890648/how-can-i-use-golangs-compress-gzip-package-to-gzip-a-file
return false
}
return true
}
func gzipHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer next.ServeHTTP(w, r)
if !canServeGzip(r) {
// fmt.Println("as original", r.URL.Path)
return
}
w.Header().Add("Content-Encoding", "gzip")
w.Header().Add("Content-Type", contentType(filepath.Ext(r.URL.Path)))
r.URL.Path = r.URL.Path + ".gz"
// fmt.Println("as gzip", r.URL.Path)
})
}
func recoverHandler(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("panic: %+v
", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
}()
// time.Sleep(time.Millisecond * 500)
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
var userIDKey utils.CtxKey = "userID"
func authHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userID, err := user.IsLoggedIn(r)
if err != nil {
log.Printf("main authHandler() %v", err)
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), userIDKey, userID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func adminHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// time.Sleep(time.Millisecond * 600)
isLoggedInAsAdmin, err := user.IsLoggedInAsAdmin(r)
if err != nil || !isLoggedInAsAdmin {
if !isLoggedInAsAdmin {
log.Printf("main adminHandler() User is not logged in as admin %v", err)
} else {
log.Printf("main adminHandler() %v", err)
}
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), userIDKey, 1)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// -------------------------------------------------------------------------------------------
func serveDevHTTP(h http.Handler) {
log.Fatal(http.ListenAndServe(":8080", h))
// log.Fatal(http.ListenAndServe(":80", h))
}
func serveHTTP(h http.Handler) {
log.Fatal(http.ListenAndServe(":80", h))
}
func serveHTTPS(h http.Handler) {
log.Fatal(http.ListenAndServeTLS(":443", cfg.TLSCertFile, cfg.TLSKeyFile, h))
}
I am not sure how to debug this panic.
The handler.go:119
contains an if
statement. You tried to get the first three characters from the Host
header.
if r.Host[:3] != "www" {
u.Host = r.Host
http.Redirect(w, r, u.String(), http.StatusMovedPermanently)
return
}
Normally, the r.Host
will store the request URL information. Unless the Host
header explicitly changed on the request. From the net/http package doc:
type Request struct {
// ...
// For server requests Host specifies the host on which the URL
// is sought. Per RFC 7230, section 5.4, this is either the value
// of the "Host" header or the host name given in the URL itself.
// It may be of the form "host:port". For international domain
// names, Host may be in Punycode or Unicode form. Use
// golang.org/x/net/idna to convert it to either format if
// needed.
// To prevent DNS rebinding attacks, server Handlers should
// validate that the Host header has a value for which the
// Handler considers itself authoritative. The included
// ServeMux supports patterns registered to particular host
// names and thus protects its registered Handlers.
//
// For client requests Host optionally overrides the Host
// header to send. If empty, the Request.Write method uses
// the value of URL.Host. Host may contain an international
// domain name.
Host string
// ...
}
So the panic is occurring because r.Host
filled with empty string, or some string that the character count is lower than 3.
I created very simple web application using go that prints the value of r.Host[:3]
. I tested it using curl, with Host
header set to empty.
curl --verbose --header 'Host: ' http://localhost:8080
It triggers a panic, and I'm pretty sure it's the same panic error like what you get.
2018/12/07 08:11:54 http: panic serving 127.0.0.1:50889: runtime error: slice bounds out of range
goroutine 37 [running]:
net/http.(*conn).serve.func1(0xc0001380a0)
/usr/local/opt/go/libexec/src/net/http/server.go:1746 +0xd0
panic(0x125c0c0, 0x14964d0)
/usr/local/opt/go/libexec/src/runtime/panic.go:513 +0x1b9
main.main.func1(0x12efa80, 0xc00014c2a0, 0xc000162300)
/Users/novalagung/Desktop/test.go:11 +0x13d
net/http.HandlerFunc.ServeHTTP(0x12bcd98, 0x12efa80, 0xc00014c2a0, 0xc000162300)
/usr/local/opt/go/libexec/src/net/http/server.go:1964 +0x44
net/http.(*ServeMux).ServeHTTP(0x14a17a0, 0x12efa80, 0xc00014c2a0, 0xc000162300)
/usr/local/opt/go/libexec/src/net/http/server.go:2361 +0x127
net/http.serverHandler.ServeHTTP(0xc000093110, 0x12efa80, 0xc00014c2a0, 0xc000162300)
/usr/local/opt/go/libexec/src/net/http/server.go:2741 +0xab
net/http.(*conn).serve(0xc0001380a0, 0x12efc80, 0xc000146100)
/usr/local/opt/go/libexec/src/net/http/server.go:1847 +0x646
created by net/http.(*Server).Serve
/usr/local/opt/go/libexec/src/net/http/server.go:2851 +0x2f5
The solution is quite simple, just make sure that the r.Host
value is not an empty string and the length is greater than 2. Better use strings.HasPrefix()
to get this done.
if strings.HasPrefix(r.Host, "www") {
u.Host = r.Host
http.Redirect(w, r, u.String(), http.StatusMovedPermanently)
return
}