package main import ( "fmt" "io" "log" "net/http" "os" "path/filepath" "strconv" "strings" "time" admintemplates "git.yetaga.in/alazyreader/alpha/admin_templates" ) type SessionProvider interface { Create(user User, expr time.Duration) (string, error) Get(key string) (User, error) Refresh(key string, user User, expr time.Duration) error } type PageProvider interface { Page(key string) (*Page, error) Save(key string, page *Page) error } type UserProvider interface { Authenticate(name string, password string) (User, error) } type RootHandler struct { Sessions SessionProvider Pages PageProvider Users UserProvider Admin *AdminHandler StaticDir string } type AdminHandler struct { Sessions SessionProvider } func (h *AdminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Write([]byte("admin route")) } func (h *RootHandler) LoginHandler(w http.ResponseWriter, r *http.Request) { switch r.Method { case "GET": // serve login form w.Write([]byte(admintemplates.LoginPage)) case "POST": // handle login request r.ParseForm() username := r.PostForm.Get("username") password := r.PostForm.Get("password") if username == "" { h.ErrorHandle(http.StatusBadRequest, w) return } user, err := h.Users.Authenticate(username, password) if err != nil { h.ErrorHandle(http.StatusForbidden, w) return } sessionKey, err := h.Sessions.Create(user, time.Duration(time.Hour*24)) if err != nil { h.ErrorHandle(http.StatusInternalServerError, w) return } w.Header().Add("Set-Cookie", "alphasess="+sessionKey) http.Redirect(w, r, "/admin", http.StatusFound) default: // anything else doesn't do anything h.ErrorHandle(http.StatusMethodNotAllowed, w) } } func (h *RootHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // handle login route if r.URL.Path == "/login" { h.LoginHandler(w, r) return } // hand off to admin router if strings.HasPrefix(r.URL.Path, "/admin") { if user, ok := h.IsLoggedIn(r); !ok { http.Redirect(w, r, "/login", http.StatusFound) return } else if HasRole(user.Roles, "admin") { h.Admin.ServeHTTP(w, r) return } h.ErrorHandle(http.StatusForbidden, w) return } // attempt to serve from the managed pages page, err := h.Pages.Page(r.URL.Path) if err == nil { w.Write(page.Contents) return } // fall back to serving out of the static directory, but: // 1. prevent the generated indexes from rendering if strings.HasSuffix(r.URL.Path, "/") { h.ErrorHandle(http.StatusNotFound, w) return } // 2. prevent hidden paths from rendering for _, seg := range strings.Split(r.URL.Path, "/") { if strings.HasPrefix(seg, ".") { h.ErrorHandle(http.StatusNotFound, w) return } } // 3. catch files that would 404 and serve our own 404 page if !staticFileExists(filepath.Join(h.StaticDir, r.URL.Path)) { h.ErrorHandle(http.StatusNotFound, w) return } // finally, use the built-in fileserver to serve fs := http.FileServer(http.Dir(h.StaticDir)) fs.ServeHTTP(w, r) } func (h *RootHandler) ErrorHandle(status int, w http.ResponseWriter) { f, err := os.Open(filepath.Join(h.StaticDir, strconv.Itoa(status)+".html")) if err == nil { w.WriteHeader(status) _, err = io.Copy(w, f) if err != nil { fmt.Fprintf(w, "Internal Server Error while loading %d page\n", status) } return } w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.Header().Set("X-Content-Type-Options", "nosniff") w.WriteHeader(http.StatusNotFound) fmt.Fprintf(w, "%d\n", status) } func (h *RootHandler) IsLoggedIn(r *http.Request) (User, bool) { cookie, err := r.Cookie("alphasess") if err != nil { return User{}, false } user, err := h.Sessions.Get(cookie.Value) return user, err == nil } func HasRole(roles []string, role string) bool { for _, r := range roles { if r == role { return true } } return false } func staticFileExists(name string) bool { f, err := os.Open(name) if err != nil { return false } defer f.Close() _, err = f.Stat() return err == nil } func main() { sessions := &Sessions{} pages := &Index{} users := &Userstore{} users.Create("testuser", []string{"admin"}, "password") handler := &RootHandler{ Sessions: sessions, Pages: pages, Users: users, Admin: &AdminHandler{ Sessions: sessions, }, StaticDir: "./static", } pages.Save("foo", &Page{ Contents: []byte("foobar"), }) pages.Save("index", &Page{ Contents: []byte("root"), }) log.Printf("serving on http://localhost:8080") err := http.ListenAndServe(":8080", handler) log.Fatalf("server error: %v", err) }