Getting a panic from a production server made in Go - 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\n", 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. 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
}

Related

How to wrap API Endpoint with middleware

AM trying generate and appy the client credential accesstoken to wrap simple API endpoint. I got code that work well when all codes are in the main function. However, i woild like to have the API endpoint (/protected) outside the main function in a different directory together with the middleware.
How do I do this, so that middleware apply to every request to the server?
my main function in the parent directory
func main() {
log.Printf("Server started")
router := sw.NewRouter()
manager := manage.NewDefaultManager()
manager.SetAuthorizeCodeTokenCfg(manage.DefaultAuthorizeCodeTokenCfg)
manager.MustTokenStorage(store.NewMemoryTokenStore())
clientStore := store.NewClientStore()
manager.MapClientStorage(clientStore)
srv := server.NewDefaultServer(manager)
srv.SetAllowGetAccessRequest(true)
srv.SetClientInfoHandler(server.ClientFormHandler)
manager.SetRefreshTokenCfg(manage.DefaultRefreshTokenCfg)
srv.SetInternalErrorHandler(func(err error) (re *errors.Response) {
log.Println("Internal Error:", err.Error())
return
})
srv.SetResponseErrorHandler(func(re *errors.Response) {
log.Println("Response Error:", re.Error.Error())
})
router.HandleFunc("/oauth2/token", func(w http.ResponseWriter, r *http.Request) {
srv.HandleTokenRequest(w, r)
})
router.HandleFunc("/credentials", func(w http.ResponseWriter, r *http.Request) {
clientId := uuid.New().String()[:8]
clientSecret := uuid.New().String()[:8]
err := clientStore.Set(clientId, &models.Client{
ID: clientId,
Secret: clientSecret,
Domain: "http://localhost:9094",
})
if err != nil {
fmt.Println(err.Error())
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"CLIENT_ID": clientId, "CLIENT_SECRET": clientSecret})
})
log.Fatal(http.ListenAndServe(":8000", router))
}
middleware API endpoint and router in a sub directory
func protecteduri(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, I'm protected"))
}
func validateToken(f http.HandlerFunc, srv *server.Server) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, err := srv.ValidationBearerToken(r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
f.ServeHTTP(w, r)
})
}
router
type Route struct {
Name string
Method string
Pattern string
HandlerFunc http.HandlerFunc
}
type Routes []Route
func NewRouter() *mux.Router {
router := mux.NewRouter().StrictSlash(true)
for _, route := range routes {
var handler http.Handler
handler = route.HandlerFunc
handler = Logger(handler, route.Name)
router.
Methods(route.Method).
Path(route.Pattern).
Name(route.Name).
Handler(handler)
}
return router
}
func Index(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World!")
}
var routes = Routes{
{
"Index",
"GET",
"/",
Index,
},
{
"protecteduri",
strings.ToUpper("Get"),
"/protected",
protecteduri,
},
{
"AccessTokenRequest",
strings.ToUpper("Post"),
"/oauth2/token",
AccessTokenRequest,
},
}
Normally i add middleware in the router file as
hadler = middleware(handler)
and it apply to all the incoming request but in this case it does not
work.
The full working code is this
func main() {
manager := manage.NewDefaultManager()
manager.SetAuthorizeCodeTokenCfg(manage.DefaultAuthorizeCodeTokenCfg)
// token memory store
manager.MustTokenStorage(store.NewMemoryTokenStore())
// client memory store
clientStore := store.NewClientStore()
manager.MapClientStorage(clientStore)
srv := server.NewDefaultServer(manager)
srv.SetAllowGetAccessRequest(true)
srv.SetClientInfoHandler(server.ClientFormHandler)
manager.SetRefreshTokenCfg(manage.DefaultRefreshTokenCfg)
srv.SetInternalErrorHandler(func(err error) (re *errors.Response) {
log.Println("Internal Error:", err.Error())
return
})
srv.SetResponseErrorHandler(func(re *errors.Response) {
log.Println("Response Error:", re.Error.Error())
})
http.HandleFunc("/oauth2/token", func(w http.ResponseWriter, r *http.Request) {
srv.HandleTokenRequest(w, r)
})
http.HandleFunc("/credentials", func(w http.ResponseWriter, r *http.Request) {
clientId := uuid.New().String()[:8]
clientSecret := uuid.New().String()[:8]
err := clientStore.Set(clientId, &models.Client{
ID: clientId,
Secret: clientSecret,
Domain: "http://localhost:9094",
})
if err != nil {
fmt.Println(err.Error())
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"CLIENT_ID": clientId, "CLIENT_SECRET": clientSecret})
})
http.HandleFunc("/protected", validateToken(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, I'm protected"))
}, srv))
log.Fatal(http.ListenAndServe(":9096", nil))
}
//main function end here
//middleware function below
func validateToken(f http.HandlerFunc, srv *server.Server) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, err := srv.ValidationBearerToken(r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
f.ServeHTTP(w, r)
})
}
As you can see the accesstoken to request this handle function http.HandleFunc("/protected"... below is validated using the middleware function "validateToken".
http.HandleFunc("/protected", validateToken(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, I'm protected"))
}, srv))
What i want to do is to rewrite the main function by moving the route handle function (http.HandleFunc("/protected"...) outside the main function and still the the accesstoken get validated when i send request to the (http.HandleFunc("/protected", validateToken(...) function using the routes defined above. In the ealier code, it works but the token to the handlefunction
func protecteduri(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, I'm protected"))
}
is not validated.
i will be adding more handle function and do not want to put all of them in the main function.
http.HandleFunc("/protected", validateToken(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, I'm protected"))
}, srv))

Go Gorilla Mux MiddlewareFunc with r.Use and returning errors

How do you set up Gorilla Mux r.Use to return errors down the middleware chain? https://godoc.org/github.com/gorilla/mux#Router.Use
Main.go
r := mux.NewRouter()
r.Use(LoggingFunc)
r.Use(AuthFunc)
Basic middleware
Starts with logging middleware which can catch and handle errors from further down the chain
type HandlerFunc func(w http.ResponseWriter, r *http.Request) error
func LoggingFunc(next HandlerFunc) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Logging middleware
defer func() {
if err, ok := recover().(error); ok {
w.WriteHeader(http.StatusInternalServerError)
}
}()
err := next(w, r)
if err != nil {
// log error
}
})
}
The next middleware handles authentication and returns an error to the logging middleware.
func AuthFunc(next HandlerFunc) HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
if r.GET("JWT") == "" {
return fmt.Errorf("No JWT")
}
return next(w, r)
}
}
I keep getting errors like
cannot use AuthFunc (type func(handlers.HandlerFunc) http.Handler) as type mux.MiddlewareFunc in argument to r.Use
Thanks
According to the mux.Use doc its argument type is MiddlewareFunc which return type is http.Handler not error type. You have to define which return type is http.HandlerFunc
type Middleware func(http.HandlerFunc) http.HandlerFunc
func main() {
r := mux.NewRouter()
// execute middleware from right to left of the chain
chain := Chain(SayHello, AuthFunc(), LoggingFunc())
r.HandleFunc("/", chain)
println("server listening : 8000")
http.ListenAndServe(":8000", r)
}
// Chain applies middlewares to a http.HandlerFunc
func Chain(f http.HandlerFunc, middlewares ...Middleware) http.HandlerFunc {
for _, m := range middlewares {
f = m(f)
}
return f
}
func LoggingFunc() Middleware {
return func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Loggin middleware
defer func() {
if _, ok := recover().(error); ok {
w.WriteHeader(http.StatusInternalServerError)
}
}()
// Call next middleware/handler in chain
next(w, r)
}
}
}
func AuthFunc() Middleware {
return func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("JWT") == "" {
fmt.Errorf("No JWT")
return
}
next(w, r)
}
}
}
func SayHello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello client")
}
It will execute the LogginFunc then AuthFunc and then SayHello method which is your desire method after passing all those middlewares.

Gorilla/mux middleware not being hit

I am on go version go1.10.4 linux/amd64
I am registering middleware, but they don't seem to be hit.
package main
import (
"encoding/json"
"fmt"
"github.com/dgrijalva/jwt-go"
"github.com/gorilla/context"
"github.com/mitchellh/mapstructure"
"huru/migrations"
"huru/models"
"huru/models/person"
"huru/routes"
"net/http"
"os"
"github.com/gorilla/mux"
_ "github.com/lib/pq"
log "github.com/sirupsen/logrus"
)
func loggingMiddleware(next http.Handler) http.Handler {
log.Println("logging middleware registered");
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Do stuff here
log.Println("Here is the request URI:",r.RequestURI)
// Call the next handler, which can be another middleware in the chain, or the final handler.
next.ServeHTTP(w, r)
})
}
type Exception struct {
Message string `json:"message"`
}
func authMiddleware(next http.Handler) http.Handler {
log.Println("auth middleware registered");
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
params := r.URL.Query()
fmt.Println("the params are:", params);
token, _ := jwt.Parse(params["token"][0], func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("there was an error")
}
return []byte("secret"), nil
})
claims, ok := token.Claims.(jwt.MapClaims)
if ! (ok && token.Valid) {
json.NewEncoder(w).Encode(Exception{Message: "Invalid authorization token"})
return;
}
var user person.Model
mapstructure.Decode(claims, &user)
context.Set(r, "logged_in_user", user)
next.ServeHTTP(w, r)
})
}
func errorMiddleware(next http.Handler) http.Handler {
log.Println("error handling middleware registered");
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Error("Caught error in defer/recover middleware: ", err)
originalError := err.(struct{ OriginalError error }).OriginalError
if originalError != nil {
log.Error("Original error in defer/recover middleware: ", originalError)
}
statusCode := err.(struct{ StatusCode int }).StatusCode
if statusCode != 0 {
w.WriteHeader(statusCode)
} else {
w.WriteHeader(http.StatusInternalServerError)
}
message := err.(struct{ Message string }).Message
if message == "" {
message = "Unknown error message."
}
json.NewEncoder(w).Encode(struct {
ID string
}{
message,
})
}
}()
next.ServeHTTP(w, r)
})
}
func main() {
routerParent := mux.NewRouter()
routerParent.Use(loggingMiddleware)
routerParent.Use(errorMiddleware)
routerParent.Use(authMiddleware)
router := routerParent.PathPrefix("/api/v1").Subrouter();
router.Use(loggingMiddleware)
router.Use(errorMiddleware)
router.Use(authMiddleware)
// register and login
{
handler := routes.LoginHandler{}
subRouter := router.PathPrefix("/").Subrouter()
handler.Mount(subRouter, struct{}{});
}
{
handler := routes.RegisterHandler{}
subRouter := router.PathPrefix("/").Subrouter()
handler.Mount(subRouter, struct{}{})
}
{
// people
handler := routes.PersonHandler{}
subRouter := router.PathPrefix("/").Subrouter()
subRouter.Use(authMiddleware)
handler.Mount(subRouter, routes.PersonInjection{People: models.PersonInit()})
}
// ...
}
none of these get logged:
log.Println("error handling middleware registered");
log.Println("auth middleware registered");
log.Println("logging middleware registered");
and at runtime none of middleware routes seem to get hit, nothing is logged there. Anyone know why that may be?
Note that I don't expect to need all these:
routerParent := mux.NewRouter()
routerParent.Use(loggingMiddleware)
routerParent.Use(errorMiddleware)
routerParent.Use(authMiddleware)
router := routerParent.PathPrefix("/api/v1").Subrouter();
router.Use(loggingMiddleware)
router.Use(errorMiddleware)
router.Use(authMiddleware)
in reality I probably just want:
routerParent := mux.NewRouter()
router := routerParent.PathPrefix("/api/v1").Subrouter();
router.Use(loggingMiddleware)
router.Use(errorMiddleware)
router.Use(authMiddleware)
but it's just there to prove that something is off. At the end of the main func, I have this to start the server:
host := os.Getenv("huru_api_host")
port := os.Getenv("huru_api_port")
if host == "" {
host = "localhost"
}
if port == "" {
port = "80"
}
log.Info(fmt.Sprintf("Huru API server listening on port %s", port))
path := fmt.Sprintf("%s:%s", host, port)
log.Fatal(http.ListenAndServe(path, routerParent))

Unable to protect gorilla/mux Subroute with basic auth

I am trying to create routes using gorilla/mux, some of which should be protected by basic auth and others shouldn't. Specifically, every route under /v2 should require basic auth, but the routes under /health should be publicly accessible.
As you can see below, I can wrap each of my /v2 route handlers with BasicAuth(), but that's against the DRY principle, and also error prone, not to mention the security implications of forgetting to wrap one of those handlers.
I have the following output from curl. All but the last one is as I expect. One should not be able to GET /smallcat without authentication.
$ curl localhost:3000/health/ping
"PONG"
$ curl localhost:3000/health/ping/
404 page not found
$ curl localhost:3000/v2/bigcat
Unauthorised.
$ curl apiuser:apipass#localhost:3000/v2/bigcat
"Big MEOW"
$ curl localhost:3000/v2/smallcat
"Small Meow"
Here's the complete code. I believe I need to fix the v2Router definition somehow, but fail to see how.
package main
import (
"crypto/subtle"
"encoding/json"
"log"
"net/http"
"github.com/gorilla/mux"
)
func endAPICall(w http.ResponseWriter, httpStatus int, anyStruct interface{}) {
result, err := json.MarshalIndent(anyStruct, "", " ")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(httpStatus)
w.Write(result)
}
func BasicAuth(handler http.HandlerFunc, username, password, realm string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user, pass, ok := r.BasicAuth()
if !ok || subtle.ConstantTimeCompare([]byte(user), []byte(username)) != 1 || subtle.ConstantTimeCompare([]byte(pass), []byte(password)) != 1 {
w.Header().Set("WWW-Authenticate", `Basic realm="`+realm+`"`)
w.WriteHeader(401)
w.Write([]byte("Unauthorised.\n"))
return
}
handler(w, r)
}
}
func routers() *mux.Router {
username := "apiuser"
password := "apipass"
noopHandler := func(http.ResponseWriter, *http.Request) {}
topRouter := mux.NewRouter().StrictSlash(false)
healthRouter := topRouter.PathPrefix("/health/").Subrouter()
v2Router := topRouter.PathPrefix("/v2").HandlerFunc(BasicAuth(noopHandler, username, password, "Provide username and password")).Subrouter()
healthRouter.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
endAPICall(w, 200, "PONG")
})
v2Router.HandleFunc("/smallcat", func(w http.ResponseWriter, r *http.Request) {
endAPICall(w, 200, "Small Meow")
})
bigMeowFn := func(w http.ResponseWriter, r *http.Request) {
endAPICall(w, 200, "Big MEOW")
}
v2Router.HandleFunc("/bigcat", BasicAuth(bigMeowFn, username, password, "Provide username and password"))
return topRouter
}
func main() {
if r := routers(); r != nil {
log.Fatal("Server exited:", http.ListenAndServe(":3000", r))
}
}
I achieved the expected behavior by using negroni. If the BasicAuth() call fails, none of the route handlers under /v2 are invoked.
The working code is in a Gist (with revisions, for those interested) here: https://gist.github.com/gurjeet/13b2f69af6ac80c0357ab20ee24fa575
Per SO convention, though, here's the complete code:
package main
import (
"crypto/subtle"
"encoding/json"
"log"
"net/http"
"github.com/gorilla/mux"
"github.com/urfave/negroni"
)
func endAPICall(w http.ResponseWriter, httpStatus int, anyStruct interface{}) {
result, err := json.MarshalIndent(anyStruct, "", " ")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(httpStatus)
w.Write(result)
}
func BasicAuth(w http.ResponseWriter, r *http.Request, username, password, realm string) bool {
user, pass, ok := r.BasicAuth()
if !ok || subtle.ConstantTimeCompare([]byte(user), []byte(username)) != 1 || subtle.ConstantTimeCompare([]byte(pass), []byte(password)) != 1 {
w.Header().Set("WWW-Authenticate", `Basic realm="`+realm+`"`)
w.WriteHeader(401)
w.Write([]byte("Unauthorised.\n"))
return false
}
return true
}
func routers() *mux.Router {
username := "apiuser"
password := "apipass"
v2Path := "/v2"
healthPath := "/health"
topRouter := mux.NewRouter().StrictSlash(true)
healthRouter := mux.NewRouter().PathPrefix(healthPath).Subrouter().StrictSlash(true)
v2Router := mux.NewRouter().PathPrefix(v2Path).Subrouter().StrictSlash(true)
healthRouter.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
endAPICall(w, 200, "PONG")
})
v2Router.HandleFunc("/smallcat", func(w http.ResponseWriter, r *http.Request) {
endAPICall(w, 200, "Small Meow")
})
bigMeowFn := func(w http.ResponseWriter, r *http.Request) {
endAPICall(w, 200, "Big MEOW")
}
v2Router.HandleFunc("/bigcat", bigMeowFn)
topRouter.PathPrefix(healthPath).Handler(negroni.New(
/* Health-check routes are unprotected */
negroni.Wrap(healthRouter),
))
topRouter.PathPrefix(v2Path).Handler(negroni.New(
negroni.HandlerFunc(func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
if BasicAuth(w, r, username, password, "Provide user name and password") {
/* Call the next handler iff Basic-Auth succeeded */
next(w, r)
}
}),
negroni.Wrap(v2Router),
))
return topRouter
}
func main() {
if r := routers(); r != nil {
log.Fatal("Server exited:", http.ListenAndServe(":3000", r))
}
}

Golang + CORS. Global setting somehow?

I'm trying out this small Go example https://github.com/jakecoffman/golang-rest-bootstrap, and so far so good.
I'm trying to add CORS to allow my front-end app access.
Here is my Main.go
func main() {
var err error
session, err = r.Connect(r.ConnectOpts{
Address: "localhost:28015",
Database: "demo",
MaxOpen: 40,
})
if err != nil {
log.Fatalln(err.Error())
}
r := mux.NewRouter()
users.Init(r, session)
accounts.Init(r, session)
r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
})
port := "9999" // os.Getenv("PORT")
log.Println("Serving on", ":"+port)
http.ListenAndServe(":"+port, context.ClearHandler(r))
}
It allows CORS at the root url, but since the other routes are handled in the controllers I just can't seem to get CORS to work there as well.
Here is part of the AccountController
func NewAccountController(r *mux.Router, s AccountService) *AccountController {
cont := AccountController{s}
r.Handle("/accounts", cont)
r.Handle("/accounts/{id}", cont)
return &cont
}
And
func (a AccountController) ServeHTTP(w http.ResponseWriter, r *http.Request) {
code := http.StatusMethodNotAllowed
var data interface{}
defer func(c int) {
log.Println(r.URL, "-", r.Method, "-", code, r.RemoteAddr)
}(code)
if r.URL.Path == "/accounts" {
switch r.Method {
case "GET":
code, data = a.List(w, r)
case "POST":
code, data = a.Add(w, r)
default:
return
}
} else {
switch r.Method {
case "GET":
code, data = a.Get(w, r)
case "PUT":
code, data = a.Update(w, r)
case "DELETE":
code, data = a.Delete(w, r)
default:
return
}
}
w.WriteHeader(code)
w.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(data)
if err != nil {
log.Println("Failed to write data: ", err)
code = http.StatusInternalServerError
}
}
Any ideas or pointers would be great.
Thanks,
JB
You can make a simple middleware for that:
type CORSMiddleware struct {
http.Handler
}
func (cm CORSMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
cm.Handler.ServeHTTP(w, r)
}
Then, you can use it like that:
var h http.Handler = CORSMiddleware{cont}
r.Handle("/accounts", h)
r.Handle("/accounts/{id}", h)

Resources