dongmiyu8979 2018-12-07 00:59
浏览 39
已采纳

从Go制造的生产服务器中惊慌失措

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.

  • 写回答

1条回答 默认 最新

  • duanfen1992 2018-12-07 01:23
    关注

    1. The Problem

    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.

    2. Testing

    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
    

    3. Solution

    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
    }
    
    本回答被题主选为最佳回答 , 对您是否有帮助呢?
    评论

报告相同问题?

悬赏问题

  • ¥15 2020长安杯与连接网探
  • ¥15 关于#matlab#的问题:在模糊控制器中选出线路信息,在simulink中根据线路信息生成速度时间目标曲线(初速度为20m/s,15秒后减为0的速度时间图像)我想问线路信息是什么
  • ¥15 banner广告展示设置多少时间不怎么会消耗用户价值
  • ¥16 mybatis的代理对象无法通过@Autowired装填
  • ¥15 可见光定位matlab仿真
  • ¥15 arduino 四自由度机械臂
  • ¥15 wordpress 产品图片 GIF 没法显示
  • ¥15 求三国群英传pl国战时间的修改方法
  • ¥15 matlab代码代写,需写出详细代码,代价私
  • ¥15 ROS系统搭建请教(跨境电商用途)