package server import ( "bytes" "encoding/base64" "encoding/json" "fmt" "log" "net" "net/http" "os" "time" "github.com/gobuffalo/packr" "github.com/gorilla/handlers" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/gorilla/mux" "github.com/gorilla/sessions" "github.com/pkg/errors" "golang.org/x/oauth2" "github.com/nsheridan/cashier/lib" "github.com/nsheridan/cashier/server/auth/github" "github.com/nsheridan/cashier/server/config" "github.com/nsheridan/cashier/server/metrics" "github.com/nsheridan/cashier/server/signer" "github.com/sid77/drop" ) // Run the server. func Run(conf *config.Config) { var err error laddr := fmt.Sprintf("%s:%d", conf.Server.Addr, conf.Server.Port) l, err := net.Listen("tcp", laddr) if err != nil { log.Fatal(errors.Wrapf(err, "unable to listen on %s:%d", conf.Server.Addr, conf.Server.Port)) } if conf.Server.User != "" { log.Print("Dropping privileges...") if err := drop.DropPrivileges(conf.Server.User); err != nil { log.Fatal(errors.Wrap(err, "unable to drop privileges")) } } // Unprivileged section metrics.Register() authprovider, err := github.New(conf.Github) if err != nil { log.Fatal(errors.Wrap(err, "unable to setup github auth provider")) } keysigner, err := signer.New(conf.SSH) if err != nil { log.Fatal(err) } ctx := &app{ cookiestore: sessions.NewCookieStore([]byte(conf.Server.CookieSecret)), keysigner: keysigner, authprovider: authprovider, config: conf.Server, router: mux.NewRouter(), } ctx.cookiestore.Options = &sessions.Options{ MaxAge: 900, Path: "/", Secure: conf.Server.SecureCookie, HttpOnly: true, } logfile := os.Stderr if conf.Server.HTTPLogFile != "" { logfile, err = os.OpenFile(conf.Server.HTTPLogFile, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0640) if err != nil { log.Printf("error opening log: %v. logging to stdout", err) } } ctx.routes() ctx.router.Use(mwVersion) ctx.router.Use(handlers.CompressHandler) ctx.router.Use(handlers.RecoveryHandler()) r := handlers.LoggingHandler(logfile, ctx.router) s := &http.Server{ Handler: r, ReadTimeout: 20 * time.Second, WriteTimeout: 20 * time.Second, IdleTimeout: 120 * time.Second, } log.Printf("Starting server on %s", laddr) s.Serve(l) } // mwVersion is middleware to add a X-Cashier-Version header to the response. func mwVersion(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("X-Cashier-Version", lib.Version) next.ServeHTTP(w, r) }) } func encodeString(s string) string { var buffer bytes.Buffer chunkSize := 70 runes := []rune(base64.StdEncoding.EncodeToString([]byte(s))) for i := 0; i < len(runes); i += chunkSize { end := i + chunkSize if end > len(runes) { end = len(runes) } buffer.WriteString(string(runes[i:end])) buffer.WriteString("\n") } buffer.WriteString(".\n") return buffer.String() } // app contains local context - cookiestore, authsession etc. type app struct { cookiestore *sessions.CookieStore authprovider *github.Config keysigner *signer.KeySigner router *mux.Router config *config.Server } func (a *app) routes() { // login required a.router.Methods("GET").Path("/").Handler(a.authed(http.HandlerFunc(a.index))) // no login required a.router.Methods("GET").Path("/auth/login").HandlerFunc(a.auth) a.router.Methods("GET").Path("/auth/callback").HandlerFunc(a.auth) a.router.Methods("POST").Path("/sign").HandlerFunc(a.sign) a.router.Methods("GET").Path("/health").HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) fmt.Fprintf(w, "ok") }) a.router.Methods("GET").Path("/metrics").Handler(promhttp.Handler()) box := packr.NewBox("static") a.router.PathPrefix("/static/").Handler(http.StripPrefix("/static", http.FileServer(box))) } func (a *app) getAuthToken(r *http.Request) *oauth2.Token { token := &oauth2.Token{} marshalled := a.getSessionVariable(r, "token") json.Unmarshal([]byte(marshalled), token) return token } func (a *app) setAuthToken(w http.ResponseWriter, r *http.Request, token *oauth2.Token) { v, _ := json.Marshal(token) a.setSessionVariable(w, r, "token", string(v)) } func (a *app) getSessionVariable(r *http.Request, key string) string { session, _ := a.cookiestore.Get(r, "session") v, ok := session.Values[key].(string) if !ok { v = "" } return v } func (a *app) setSessionVariable(w http.ResponseWriter, r *http.Request, key, value string) { session, _ := a.cookiestore.Get(r, "session") session.Values[key] = value session.Save(r, w) } func (a *app) authed(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t := a.getAuthToken(r) if !t.Valid() || !a.authprovider.Valid(t) { a.setSessionVariable(w, r, "origin_url", r.URL.EscapedPath()) http.Redirect(w, r, "/auth/login", http.StatusSeeOther) return } next.ServeHTTP(w, r) }) }