Skip to content

Instantly share code, notes, and snippets.

@alexaandru
Last active July 31, 2017 11:16
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save alexaandru/eb13b0608eb5d6d34d2d to your computer and use it in GitHub Desktop.
Save alexaandru/eb13b0608eb5d6d34d2d to your computer and use it in GitHub Desktop.
Minimal Golang web stack.

Mini Go Web Stack

Description

This represents a proof of concept, minimal web stack. I used http://nicolasmerouze.com/build-web-framework-golang/ series of articles as a starting point and the goal was to keep the stack as minimal as possible while still being useful for practical purposes. And of course, use the standard library as much as possible and only integrate components compatible with it.

Right now the stack includes:

package main
import (
"fmt"
"net/http"
"strings"
au "github.com/alexaandru/elastic_guardian/authentication"
az "github.com/alexaandru/elastic_guardian/authorization"
"github.com/justinas/alice"
)
const AppEnv = "development"
const Realm = "Secret Area"
const AuthHdr = "X-Authenticated-User"
type Routes map[string]http.HandlerFunc
// the /dev/null equivalent for HTTP - where all requests we don't care about end up.
func httpNullGen(routes Routes) alice.Constructor {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if _, exists := routes[r.URL.Path]; exists {
next.ServeHTTP(w, r)
return
}
if r.URL.Path == "/favicon.ico" {
r.Header.Set("X-Silence-Logging", "yes")
}
errHandler(w, r, http.StatusNotFound)
})
}
}
func errHandler(w http.ResponseWriter, r *http.Request, status int, msgs ...string) {
if status == http.StatusInternalServerError {
msgs = append(msgs, r.Header.Get("X-Panic"))
}
msg := strings.Join(msgs, "; ")
if AppEnv != "production" && len(msg) > 0 {
msg = "\n\n(" + msg + ")"
}
http.Error(w, http.StatusText(status)+msg, status)
}
func dieHard(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
r.Header.Set("X-Panic", fmt.Sprintf("%v", err))
errHandler(w, r, http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
func auHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
status, user := au.BasicAuthPassed(r.Header.Get("Authorization"))
if status == au.Passed {
r.Header.Set(AuthHdr, user)
next.ServeHTTP(w, r)
} else if status == au.NotAttempted {
w.Header().Set("WWW-Authenticate", "Basic realm=\""+Realm+"\"")
errHandler(w, r, http.StatusUnauthorized)
} else {
errHandler(w, r, http.StatusForbidden, "authentication")
}
})
}
func azHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if az.AuthorizationPassed(r.Header.Get(AuthHdr), r.Method, r.URL.Path) {
next.ServeHTTP(w, r)
} else {
errHandler(w, r, http.StatusForbidden, "authorization")
}
})
}
func init() {
au.LoadCredentials(au.CredentialsStore{
"john": au.Hash("secret"),
})
az.LoadAuthorizations(az.AuthorizationStore{
"john": az.AuthorizationRules{az.Deny, []string{"GET /about"}},
})
}
// Credits go to: https://gist.github.com/cespare/3985516
package main
import (
"fmt"
"io"
"net/http"
"strings"
"time"
)
const ApacheFormatPattern = "%s - - [%s] \"%s %d %d\" %.4fms \"%s\"\n"
type ApacheLogRecord struct {
http.ResponseWriter
ip string
time time.Time
method, uri, protocol string
status int
responseBytes int64
elapsedTime time.Duration
}
func (r *ApacheLogRecord) Log(out io.Writer, origHdr http.Header) {
timeFormatted := r.time.Format("02/Jan/2006 03:04:05")
requestLine := fmt.Sprintf("%s %s %s", r.method, r.uri, r.protocol)
fmt.Fprintf(out, ApacheFormatPattern, r.ip, timeFormatted, requestLine, r.status,
r.responseBytes, r.elapsedTime.Seconds()*1000, origHdr.Get("X-Panic"))
}
func (r *ApacheLogRecord) Write(p []byte) (int, error) {
written, err := r.ResponseWriter.Write(p)
r.responseBytes += int64(written)
return written, err
}
func (r *ApacheLogRecord) WriteHeader(status int) {
r.status = status
r.ResponseWriter.WriteHeader(status)
}
type ApacheLoggingHandler struct {
handler http.Handler
out io.Writer
}
func NewApacheLoggingHandler(handler http.Handler, out io.Writer) http.Handler {
return &ApacheLoggingHandler{
handler: handler,
out: out,
}
}
func (h *ApacheLoggingHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
clientIP := r.RemoteAddr
if colon := strings.LastIndex(clientIP, ":"); colon != -1 {
clientIP = clientIP[:colon]
}
record := &ApacheLogRecord{
ResponseWriter: rw,
ip: clientIP,
time: time.Time{},
method: r.Method,
uri: r.RequestURI,
protocol: r.Proto,
status: http.StatusOK,
elapsedTime: time.Duration(0),
}
startTime := time.Now()
h.handler.ServeHTTP(record, r)
finishTime := time.Now()
record.time = finishTime.UTC()
record.elapsedTime = finishTime.Sub(startTime)
if r.Header.Get("X-Silence-Logging") != "yes" {
record.Log(h.out, r.Header)
}
}
package main
import (
"fmt"
"net/http"
"os"
"github.com/justinas/alice"
)
func HomePage(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Home Page")
}
func AboutPage(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "About Page")
}
func main() {
routes := Routes{
"/": HomePage,
"/about": AboutPage,
}
mux := http.DefaultServeMux
httpNull := httpNullGen(routes)
commonHandlers := alice.New(httpNull, dieHard, auHandler, azHandler)
loggingHandler := NewApacheLoggingHandler(mux, os.Stderr)
for route, fn := range routes {
mux.Handle(route, commonHandlers.ThenFunc(fn))
}
server := &http.Server{Addr: ":8089", Handler: loggingHandler}
fmt.Println(server.ListenAndServe())
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment