diff --git a/auth/auth.go b/auth/auth.go new file mode 100644 index 0000000..2580d90 --- /dev/null +++ b/auth/auth.go @@ -0,0 +1,30 @@ +package auth + +import ( + "net/http" + + "github.com/ishanjain28/pogo/common" +) + +func RequireAuthorization() common.Handler { + return func(rc *common.RouterContext, w http.ResponseWriter, r *http.Request) *common.HTTPError { + if usr := DecryptSession(r); usr != nil { + rc.User = usr + return nil + } + return &common.HTTPError{ + Message: "Unauthorized!", + StatusCode: http.StatusUnauthorized, + } + } +} + +func CreateSession() common.Handler { + return func(rc *common.RouterContext, w http.ResponseWriter, r *http.Request) *common.HTTPError { + return nil + } +} + +func DecryptSession(r *http.Request) *common.User { + return nil +} diff --git a/common/common.go b/common/common.go new file mode 100644 index 0000000..68bccb9 --- /dev/null +++ b/common/common.go @@ -0,0 +1,66 @@ +package common + +import ( + "fmt" + "io" + "log" + "net/http" + "os" + "strconv" +) + +// Handler is the signature of HTTP Handler that is passed to Handle function +type Handler func(rc *RouterContext, w http.ResponseWriter, r *http.Request) *HTTPError + +// HTTPError is any error that occurs in middlewares or the code that handles HTTP Frontend of application +// Message is logged to console and Status Code is sent in response +// If a Middleware sends an HTTPError, No middlewares further up in chain are executed +type HTTPError struct { + // Message to log in console + Message string + // Status code that'll be sent in response + StatusCode int +} + +// RouterContext contains any information to be shared with middlewares. +type RouterContext struct { + User *User +} + +// User struct denotes the data is stored in the cookie +type User struct { + Name string `json:"name"` +} + +// ReadAndServeFile reads the file from specified location and sends it in response +func ReadAndServeFile(name string, w http.ResponseWriter) *HTTPError { + f, err := os.Open(name) + if err != nil { + + if os.IsNotExist(err) { + return &HTTPError{ + Message: fmt.Sprintf("%s not found", name), + StatusCode: http.StatusNotFound, + } + } + + return &HTTPError{ + Message: fmt.Sprintf("error in reading %s: %v\n", name, err), + StatusCode: http.StatusInternalServerError, + } + } + + defer f.Close() + stats, err := f.Stat() + if err != nil { + log.Printf("error in fetching %s's stats: %v\n", name, err) + } else { + w.Header().Add("Content-Length", strconv.FormatInt(stats.Size(), 10)) + } + + _, err = io.Copy(w, f) + if err != nil { + log.Printf("error in copying %s to response: %v\n", name, err) + } + return nil +} diff --git a/router/router.go b/router/router.go new file mode 100644 index 0000000..1cc79d8 --- /dev/null +++ b/router/router.go @@ -0,0 +1,148 @@ +package router + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "strings" + + "github.com/gorilla/mux" + "github.com/ishanjain28/pogo/auth" + "github.com/ishanjain28/pogo/common" +) + +type NewConfig struct { + Name string + Host string + Email string + Description string + Image string + PodcastURL string +} + +// Handle takes multiple Handler and executes them in a serial order starting from first to last. +// In case, Any middle ware returns an error, The error is logged to console and sent to the user, Middlewares further up in chain are not executed. +func Handle(handlers ...common.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + rc := &common.RouterContext{} + for _, handler := range handlers { + err := handler(rc, w, r) + if err != nil { + log.Printf("%v", err) + + w.Write([]byte(http.StatusText(err.StatusCode))) + + return + } + } + }) +} + +func Init() *mux.Router { + + r := mux.NewRouter() + + // "Static" paths + r.PathPrefix("/assets/").Handler(http.StripPrefix("/assets/", http.FileServer(http.Dir("assets/web/static")))) + r.PathPrefix("/download/").Handler(http.StripPrefix("/download/", http.FileServer(http.Dir("podcasts")))) + + // Paths that require specific handlers + r.Handle("/", Handle( + rootHandler(), + )).Methods("GET") + + r.Handle("/rss", Handle( + rootHandler(), + )).Methods("GET") + + r.Handle("/json", Handle( + rootHandler(), + )).Methods("GET") + + // Authenticated endpoints should be passed to BasicAuth() + // first + r.Handle("/admin", Handle( + auth.RequireAuthorization(), + adminHandler(), + )) + // r.HandleFunc("/admin/publish", BasicAuth(CreateEpisode)) + // r.HandleFunc("/admin/delete", BasicAuth(RemoveEpisode)) + // r.HandleFunc("/admin/css", BasicAuth(CustomCss)) + + r.Handle("/setup", Handle( + serveSetup(), + )).Methods("GET", "POST") + + return r +} + +// Handles /, /feed and /json endpoints +func rootHandler() common.Handler { + return func(rc *common.RouterContext, w http.ResponseWriter, r *http.Request) *common.HTTPError { + + var file string + switch r.URL.Path { + case "/rss": + w.Header().Set("Content-Type", "application/rss+xml") + file = "assets/web/feed.rss" + case "/json": + w.Header().Set("Content-Type", "application/json") + file = "assets/web/feed.json" + case "/": + w.Header().Set("Content-Type", "text/html") + file = "assets/web/index.html" + default: + return &common.HTTPError{ + Message: fmt.Sprintf("%s: Not Found", r.URL.Path), + StatusCode: http.StatusNotFound, + } + } + + return common.ReadAndServeFile(file, w) + } +} + +func adminHandler() common.Handler { + return func(rc *common.RouterContext, w http.ResponseWriter, r *http.Request) *common.HTTPError { + return common.ReadAndServeFile("assets/web/admin.html", w) + } +} + +// Serve setup.html and config parameters +func serveSetup() common.Handler { + return func(rc *common.RouterContext, w http.ResponseWriter, r *http.Request) *common.HTTPError { + if r.Method == "GET" { + return common.ReadAndServeFile("assets/web/setup.html", w) + } + r.ParseMultipartForm(32 << 20) + + // Parse form and convert to JSON + cnf := NewConfig{ + strings.Join(r.Form["podcastname"], ""), // Podcast name + strings.Join(r.Form["podcasthost"], ""), // Podcast host + strings.Join(r.Form["podcastemail"], ""), // Podcast host email + "", // Podcast image + "", // Podcast location + "", // Podcast location + } + + b, err := json.Marshal(cnf) + if err != nil { + panic(err) + } + + ioutil.WriteFile("assets/config/config.json", b, 0644) + w.Write([]byte("Done")) + return nil + } +} + +func redirectHandler() common.Handler { + return func(rc *common.RouterContext, w http.ResponseWriter, r *http.Request) *common.HTTPError { + + return nil + } +} diff --git a/setup.go b/setup.go deleted file mode 100644 index d2a0032..0000000 --- a/setup.go +++ /dev/null @@ -1,50 +0,0 @@ -package main - -import ( - "io/ioutil" - "net/http" - "encoding/json" - "strings" -) - -type NewConfig struct { - Name string - Host string - Email string - Description string - Image string - PodcastUrl string -} - -// Serve setup.html and config parameters -func ServeSetup(w http.ResponseWriter, r *http.Request) { - if r.Method == "GET" { - data, err := ioutil.ReadFile("assets/web/setup.html") - - if err != nil { - panic(err) - } - w.Write(data) - } - if r.Method == "POST" { - r.ParseMultipartForm(32 << 20) - - // Parse form and convert to JSON - cnf := NewConfig{ - strings.Join(r.Form["podcastname"], ""), // Podcast name - strings.Join(r.Form["podcasthost"], ""), // Podcast host - strings.Join(r.Form["podcastemail"], ""), // Podcast host email - strings.Join(r.Form["podcastdescription"], ""), // Podcast Description - "", // Podcast image - "", // Podcast location - } - - b, err := json.Marshal(cnf) - if err != nil { - panic(err) - } - - ioutil.WriteFile("assets/config/config.json", b, 0644) - w.Write([]byte("Done")) - } -} diff --git a/webserver.go b/webserver.go index f85e7e2..ff79afb 100644 --- a/webserver.go +++ b/webserver.go @@ -8,49 +8,12 @@ package main import ( "fmt" - "io/ioutil" "log" "net/http" - "github.com/gorilla/mux" + "github.com/ishanjain28/pogo/router" ) -// Prints out contents of feed.rss -func RssHandler(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/rss+xml") - w.WriteHeader(http.StatusOK) - data, err := ioutil.ReadFile("assets/web/feed.rss") - if err != nil { - panic(err) - } - w.Header().Set("Content-Length", fmt.Sprint(len(data))) - fmt.Fprint(w, string(data)) -} - -// Does the same as above method, only with the JSON feed data -func JsonHandler(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - data, err := ioutil.ReadFile("assets/web/feed.json") - if err != nil { - panic(err) - } - w.Header().Set("Content-Length", fmt.Sprint(len(data))) - fmt.Fprint(w, string(data)) -} - -// Serve up homepage -func HomeHandler(w http.ResponseWriter, r *http.Request) { - data, err := ioutil.ReadFile("assets/web/index.html") - - if err == nil { - w.Write(data) - } else { - w.WriteHeader(500) - w.Write([]byte("Error500 - " + http.StatusText(500))) - } -} - // Authenticate user using basic webserver authentication // @TODO: Replace this with a different for of _more secure_ // authentication that we can POST to instead. @@ -58,63 +21,43 @@ func HomeHandler(w http.ResponseWriter, r *http.Request) { * Code from stackoverflow by user Timmmm * https://stackoverflow.com/questions/21936332/idiomatic-way-of-requiring-http-basic-auth-in-go/39591234#39591234 */ -func BasicAuth(handler http.HandlerFunc) http.HandlerFunc { +// func BasicAuth(handler http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - realm := "Login to Pogo admin interface" - user, pass, ok := r.BasicAuth() +// return func(w http.ResponseWriter, r *http.Request) { +// realm := "Login to Pogo admin interface" +// user, pass, ok := r.BasicAuth() - if !AuthUser(user,pass) || !ok { - w.Header().Set("WWW-Authenticate", `Basic realm="`+realm+`"`) - w.WriteHeader(401) - w.Write([]byte("Unauthorised.\n")) - return - } - handler(w, r) - } -} +// if !AuthUser(user, pass) || !ok { +// w.Header().Set("WWW-Authenticate", `Basic realm="`+realm+`"`) +// w.WriteHeader(401) +// w.Write([]byte("Unauthorised.\n")) +// return +// } +// handler(w, r) +// } +// } -// Handler for serving up admin page -func AdminHandler(w http.ResponseWriter, r *http.Request) { - data, err := ioutil.ReadFile("assets/web/admin.html") +// // Handler for serving up admin page +// func AdminHandler(w http.ResponseWriter, r *http.Request) { +// data, err := ioutil.ReadFile("assets/web/admin.html") - if err == nil { - w.Write(data) - } else { - w.WriteHeader(500) - w.Write([]byte("500 Something went wrong - " + http.StatusText(500))) - } -} +// if err == nil { +// w.Write(data) +// } else { +// w.WriteHeader(500) +// w.Write([]byte("500 Something went wrong - " + http.StatusText(500))) +// } +// } // Main function that defines routes func main() { // Start the watch() function in generate_rss.go, which // watches for file changes and regenerates the feed - go watch() + // go watch() // Define routes - r := mux.NewRouter() - - // "Static" paths - r.PathPrefix("/assets/").Handler(http.StripPrefix("/assets/", http.FileServer(http.Dir("assets/web/static")))) - r.PathPrefix("/download/").Handler(http.StripPrefix("/download/", http.FileServer(http.Dir("podcasts")))) - - // Paths that require specific handlers - http.Handle("/", r) // Pass everything to gorilla mux - r.HandleFunc("/", HomeHandler) - r.HandleFunc("/rss", RssHandler) - r.HandleFunc("/json", JsonHandler) - - // Authenticated endpoints should be passed to BasicAuth() - // first - r.HandleFunc("/admin", BasicAuth(AdminHandler)) - r.HandleFunc("/admin/publish", BasicAuth(CreateEpisode)) - r.HandleFunc("/admin/delete", BasicAuth(RemoveEpisode)) - r.HandleFunc("/admin/css", BasicAuth(CustomCss)) - - r.HandleFunc("/setup", ServeSetup) - - // We're live! - fmt.Println("Listening on port :8000") - log.Fatal(http.ListenAndServe(":8000", r)) + // We're live + r := router.Init() + fmt.Println("Listening on port :3000") + log.Fatal(http.ListenAndServe(":3000", r)) }