commit a12d13aa001ff4b15f91e6ee5d4b8ac3b3b50fe4 Author: nemunaire Date: Sat Jun 25 19:51:24 2016 +0200 Initial commit diff --git a/api.go b/api.go new file mode 100644 index 0000000..45c1fb4 --- /dev/null +++ b/api.go @@ -0,0 +1,85 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "log" + "net/http" + "strings" +) + +type DispatchFunction func(*User, []string, io.ReadCloser) (interface{}, error) + +var apiRoutes = map[string]*(map[string]struct{AuthFunction;DispatchFunction}){ + "version": &ApiVersionRouting, +} + +type apiHandler struct { + Authenticate func(*http.Request) (*User) +} + +func ApiHandler(Authenticate func(*http.Request) (*User)) (apiHandler) { + return apiHandler{Authenticate} +} + +func (a apiHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + user := a.Authenticate(r) + + log.Printf("Handling %s API request from %s: %s %s [%s]\n", r.Method, r.RemoteAddr, r.URL.Path, user, r.UserAgent()) + + // Extract URL arguments + var sURL = strings.Split(r.URL.Path[1:], "/") + if sURL[len(sURL)-1] == "" && len(sURL) > 0 { + // Remove trailing / + sURL = sURL[:len(sURL)-1] + } + + w.Header().Set("Content-Type", "application/json") + + var ret interface{} + var err error = nil + + // Refuse too large requests + if r.ContentLength < 0 || r.ContentLength > 10485760 || ((r.Method == "DELETE" || r.Method == "GET" || r.Method == "HEAD") && r.ContentLength > 0) { + http.Error(w, "{errmsg:\"Request too large or request size unknown\"}", http.StatusRequestEntityTooLarge) + return + } + + // Route request + if len(sURL) == 0 { + err = errors.New(fmt.Sprintf("No action provided")) + } else if h, ok := apiRoutes[sURL[0]]; ok { + if f, ok := (*h)[r.Method]; ok { + if f.AuthFunction(user, sURL[1:]) { + ret, err = f.DispatchFunction(user, sURL[1:], r.Body) + } else { + w.Header().Set("WWW-Authenticate", "Basic realm=\"YouP0m API\"") + http.Error(w, "{errmsg:\"You are not allowed to do this request.\"}", http.StatusUnauthorized) + return + } + } else { + err = errors.New(fmt.Sprintf("Invalid action (%s) provided for %s.", r.Method, sURL[0])) + } + } + + // Format response + resStatus := http.StatusOK + if err != nil { + ret = map[string]string{"errmsg": err.Error()} + resStatus = http.StatusBadRequest + } + + if ret == nil { + ret = map[string]string{"errmsg": "Page not found"} + resStatus = http.StatusNotFound + } + + if j, err := json.Marshal(ret); err != nil { + http.Error(w, fmt.Sprintf("{errmsg:\"%q\"}", err), http.StatusInternalServerError) + } else { + w.WriteHeader(resStatus) + w.Write(j) + } +} diff --git a/auth.go b/auth.go new file mode 100644 index 0000000..96fe98d --- /dev/null +++ b/auth.go @@ -0,0 +1,78 @@ +package main + +import ( + "bufio" + "net/http" + "os" + "strings" + + "github.com/nyarla/go-crypt" +) + +type User struct { + Username string `json:"username"` +} + +type Htpasswd struct { + entries map[string]string +} + +func NewHtpasswd(path string) (*Htpasswd, error) { + if fd, err := os.Open(path); err != nil { + return nil, err + } else { + defer fd.Close() + + htpasswd := Htpasswd{ + map[string]string{}, + } + + scanner := bufio.NewScanner(fd) + for scanner.Scan() { + line := strings.SplitN(strings.TrimSpace(scanner.Text()), ":", 2) + if len(line) == 2 && len(line[1]) > 2 { + htpasswd.entries[line[0]] = line[1] + } + } + if err := scanner.Err(); err != nil { + return nil, err + } + return &htpasswd, nil + } +} + +func (h Htpasswd) Authenticate(username, password string) *User { + if hash, ok := h.entries[username]; !ok { + return nil + } else if crypt.Crypt(password, hash[:2]) != hash { + return nil + } else { + var u = User{username} + return &u + } +} + + +/// Request authentication + +func Authenticate(htpasswd Htpasswd, r *http.Request) (*User) { + // Authenticate the user if any + if username, password, ok := r.BasicAuth(); ok { + return htpasswd.Authenticate(username, password) + } else { + return nil + } +} + + +/// Page rules + +type AuthFunction func(*User, []string) bool + +func PublicPage(u *User, args []string) bool { + return true +} + +func PrivatePage(u *User, args []string) bool { + return u != nil +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..920e948 --- /dev/null +++ b/main.go @@ -0,0 +1,54 @@ +package main + +import ( + "flag" + "log" + "net/http" + "os" +) + +var PublishedImgDir string +var NextImgDir string +var ThumbsDir string + +func main() { + bind := flag.String("bind", "0.0.0.0:8080", "Bind port/socket") + htpasswd_file := flag.String("htpasswd", "", "Admin passwords file, Apache htpasswd format") + flag.StringVar(&PublishedImgDir, "publishedimgdir", "./images/published/", "Directory where save published pictures") + flag.StringVar(&NextImgDir, "nextimgdir", "./images/next/", "Directory where save pictures to review") + flag.StringVar(&ThumbsDir, "thumbsdir", "./images/thumbs/", "Directory where generate thumbs") + flag.Parse() + + htpasswd := &Htpasswd{} + + if htpasswd_file != nil && *htpasswd_file != "" { + log.Println("Reading htpasswd file...") + var err error + if htpasswd, err = NewHtpasswd(*htpasswd_file); htpasswd == nil { + log.Fatal("Unable to parse htpasswd:", err) + } + } + + log.Println("Checking paths...") + if _, err := os.Stat(PublishedImgDir); os.IsNotExist(err) { + log.Fatal(err) + } + if _, err := os.Stat(NextImgDir); os.IsNotExist(err) { + log.Fatal(err) + } + if _, err := os.Stat(ThumbsDir); os.IsNotExist(err) { + log.Fatal(err) + } + + log.Println("Registering handlers...") + authFunc := func (r *http.Request) (*User){ return Authenticate(*htpasswd, r) } + + mux := http.NewServeMux() + mux.Handle("/api/", http.StripPrefix("/api", ApiHandler(authFunc))) + mux.Handle("/", http.FileServer(http.Dir("./static/"))) + + log.Println("Ready, listening on", *bind) + if err := http.ListenAndServe(*bind, mux); err != nil { + log.Fatal("Unable to listen and serve: ", err) + } +} diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..e1d7b02 --- /dev/null +++ b/static/index.html @@ -0,0 +1 @@ +

Welcome on YouP0m!

diff --git a/version.go b/version.go new file mode 100644 index 0000000..85f9f6c --- /dev/null +++ b/version.go @@ -0,0 +1,19 @@ +package main + +import ( + "io" +) + +var ApiVersionRouting = map[string]struct{AuthFunction; DispatchFunction}{ + "GET": {PublicPage, showVersion}, +} + +func showVersion(u *User, args []string, body io.ReadCloser) (interface{}, error) { + m := map[string]interface{}{"version": 0.1} + + if u != nil { + m["youare"] = *u + } + + return m, nil +}