package main import ( "fmt" "log" "os" "io" "path" "path/filepath" "sort" "net/http" "html/template" "flag" "database/sql" "encoding/json" "encoding/base64" "time" "archive/zip" "crypto/rand" "crypto/cipher" "crypto/aes" "embed" ) var store = b64decodeAndInitNonce(os.Getenv("SESSION_KEY")) var nonce []byte = nil var db *sql.DB const baseDocDir string = "docs" const NOTFOUND string = "Not found" const UNAUTH string = "Unauthorized" //go:embed templates var tmplContent embed.FS type Doc struct { Name string Size string ModTime time.Time Link string } var fmap template.FuncMap = template.FuncMap { "add": func(i, j int) int { return i + j }, "formatmodtime": func(i time.Time) string { return i.Format("2006-01-02") }, } func b64decodeAndInitNonce(b string) []byte { dst := make([]byte, base64.StdEncoding.DecodedLen(len(b))) n, err := base64.StdEncoding.Decode(dst, []byte(b)) if err != nil { panic(err) } dst = dst[:n] if nonce == nil { nonce = make([]byte, 12) if _, err := io.ReadFull(rand.Reader, nonce); err != nil { panic(err) } } return dst } func encrypt(b []byte) []byte { blk, err := aes.NewCipher(store) if err != nil { panic(err) } aesgcm, err := cipher.NewGCM(blk) if err != nil { panic(err) } return aesgcm.Seal(nil, nonce, b, nil) } func decrypt(b []byte) ([]byte, error) { blk, err := aes.NewCipher(store) if err != nil { return nil, err } aesgcm, err := cipher.NewGCM(blk) if err != nil { return nil, err } plain, err := aesgcm.Open(nil, nonce, b, nil) if err != nil { return nil, err } return plain, nil } func serveTemplate(w http.ResponseWriter, r *http.Request, data interface{}, view ...string) { var nav string = "templates/nav.html" d := struct { Data interface{} User *User } { data, nil, } if u, err := checkSession(w, r); u != nil && err == nil { d.User = u nav = "templates/nav_logged.html" } views := []string {"templates/base.html", nav} views = append(views, view...) //t, err := template.New("base.html").Funcs(fmap).ParseFiles(views...) t, err := template.New("base.html").Funcs(fmap).ParseFS(tmplContent, views...) if err != nil { log.Fatal(err) } if err := t.Execute(w, d); err != nil { log.Fatal(err) } } func serveSimple(w http.ResponseWriter, r *http.Request, data interface{}, view string, xviews ...string) { d := struct { Data interface{} User *User } { data, nil, } views := []string {view} views = append(views, xviews...) fp := filepath.Base(views[len(views)-1]) t, err := template.New(fp).Funcs(fmap).ParseFS(tmplContent, views...) if err != nil { log.Fatal(err) } if err := t.Execute(w, d); err != nil { log.Fatal(err) } } func checkSession(w http.ResponseWriter, r *http.Request) (*User, error) { cookie, err := r.Cookie("session") if err != nil { if err == http.ErrNoCookie { return nil, nil } else { return nil, err } } ub64, err := base64.StdEncoding.DecodeString(cookie.Value) if err != nil { return nil, err } plain, err := decrypt(ub64) if err != nil { return nil, err } var user User err = json.Unmarshal(plain, &user) if err != nil { return nil, err } return &user, nil } func sendError(w http.ResponseWriter, r *http.Request, s string, status int) { log.Println(s) w.WriteHeader(status) w.Write([]byte(s)) //http.Error(w, s, status) //view := fmt.Sprintf("templates/%d.html", status) //t, err := template.ParseFiles("templates/base.html", view) //if err != nil { // log.Fatal(err) //} //if err := t.Execute(w, nil); err != nil { // log.Fatal(err) //} } func serveLogin(w http.ResponseWriter, r *http.Request, errorMsg string) { err := errorMsg if errorMsg == "" { err = consumeFlash(w, r, "error") } data := struct { Error string }{ err, } serveTemplate(w, r, data, "templates/login.html") } func unauthorized(w http.ResponseWriter, r *http.Request) { sendFlash(w, r, "redirect", r.URL.String()) w.WriteHeader(http.StatusUnauthorized) serveLogin(w, r, "") } func sendInvalidMethod(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusMethodNotAllowed) } func humanize(i int64) string { var sizes [4]string = [4]string {"O", "K", "M", "G"} j := i s := 0 for j > 1024 && s < len(sizes) { j = j / 1024.0 s++ } return fmt.Sprintf("%v%s", j, sizes[s]) } func index(w http.ResponseWriter, r *http.Request) { u, err := checkSession(w, r) if u != nil && err == nil { userImpersonation := r.URL.Query().Get("user") if r.URL.Path != "/" { data := struct { Msg string UserImpersonation string }{ NOTFOUND, userImpersonation, } w.WriteHeader(http.StatusNotFound) serveTemplate(w, r, data, "templates/msg.html") return } username := u.Username if u.IsAdmin { if userImpersonation != "" { username = userImpersonation } } userDocPath := filepath.Join(baseDocDir, username) err := os.Mkdir(userDocPath, 0750) if err != nil && !os.IsExist(err) { sendError(w, r, err.Error(), http.StatusInternalServerError) } files, err := os.ReadDir(filepath.Join(baseDocDir, username)) if err != nil { sendError(w, r, err.Error(), http.StatusInternalServerError) } var docs []Doc sort.Slice(files, func(i, j int) bool { info1, err := files[i].Info() if err != nil { return false } info2, err := files[j].Info() if err != nil { return false } return info1.ModTime().After(info2.ModTime()) }) for _, file := range files { info, err := file.Info() if err != nil { sendError(w, r, err.Error(), http.StatusInternalServerError) return } docs = append(docs, Doc { file.Name(), humanize(info.Size()), info.ModTime(), path.Join(baseDocDir, username, file.Name()), }) } flasherr := consumeFlash(w, r, "error") data := struct { Docs []Doc Error string UserImpersonation string }{ docs, flasherr, userImpersonation, } serveTemplate(w, r, data, "templates/user.html") return } else if err != nil { log.Println(err) } if r.URL.Path != "/" { w.WriteHeader(http.StatusNotFound) serveLogin(w, r, "") return } unauthorized(w, r) } func admin(w http.ResponseWriter, r *http.Request) { u, err := checkSession(w, r) if u != nil && err == nil && u.IsAdmin { serveTemplate(w, r, nil, "templates/admin.html") } else if u!= nil && !u.IsAdmin { data := struct { Msg string UserImpersonation string }{ UNAUTH, "", } w.WriteHeader(http.StatusUnauthorized) serveTemplate(w, r, data, "templates/msg.html") } else if err != nil { //sendError(w, r, err.Error(), http.StatusInternalServerError) log.Println(err) unauthorized(w, r) } else { unauthorized(w, r) } } func adminUsers(w http.ResponseWriter, r *http.Request) { u, err := checkSession(w, r) if u != nil && err == nil && u.IsAdmin { users, err := GetUsers(db) if err != nil { sendError(w, r, err.Error(), http.StatusInternalServerError) return } serveTemplate(w, r, struct { Users []User UserImpersonation string }{ users, "", }, "templates/admin/users.html") } else if u!= nil && !u.IsAdmin { data := struct { Msg string UserImpersonation string }{ UNAUTH, "", } w.WriteHeader(http.StatusUnauthorized) serveTemplate(w, r, data, "templates/msg.html") } else if err != nil { //sendError(w, r, err.Error(), http.StatusInternalServerError) log.Println(err) unauthorized(w, r) } else { //sendError(w, r, "Unauthorized", http.StatusUnauthorized) unauthorized(w, r) } } func createuser(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: err := consumeFlash(w, r, "error") data := struct { Error string }{ err, } serveTemplate(w, r, data, "templates/createuser.html") case http.MethodPost: u := r.FormValue("user") email := r.FormValue("email") pass := r.FormValue("pass") cpass := r.FormValue("cpass") isadmin := r.FormValue("isadmin") if len(pass) < 10 { sendFlash(w, r, "error", "Le mot de passe doit avoir une longeur supérieure ou égale à 10 caractères.") http.Redirect(w, r, "/createuser", http.StatusSeeOther) return } if pass != cpass { sendFlash(w, r, "error", "Le mot de passe et la confirmation du mot de passe ne sont pas les mêmes.") http.Redirect(w, r, "/createuser", http.StatusSeeOther) return } user := User{-1, u, email, pass, isadmin == "on"} user, err := CreateUser(db, user) if err != nil { sendError(w, r, err.Error(), http.StatusInternalServerError) return } http.Redirect(w, r, "/", http.StatusSeeOther) default: sendInvalidMethod(w, r) } } func consumeFlash(w http.ResponseWriter, r *http.Request, name string) string { cookie, err := r.Cookie(name) if err != nil { if err == http.ErrNoCookie { return "" } else { return err.Error() } } http.SetCookie(w, &http.Cookie{ Name: name, Value: "", MaxAge: -1, }) s, err := base64.StdEncoding.DecodeString(cookie.Value) if err != nil { return "" } return string(s) } func sendFlash(w http.ResponseWriter, r *http.Request, name, s string) { str := base64.StdEncoding.EncodeToString([]byte(s)) cookie := http.Cookie { Name: name, Value: str, MaxAge: 0, Path: "/", // Only https on qutebrowser Secure: true, HttpOnly: true, SameSite: http.SameSiteStrictMode, } http.SetCookie(w, &cookie) } func logout(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: u, err := checkSession(w, r) if u != nil && err == nil { http.SetCookie(w, &http.Cookie{ Name: "session", Value: "", MaxAge: -1, }) } http.Redirect(w, r, "/login", http.StatusSeeOther) default: sendInvalidMethod(w, r) } } func login(w http.ResponseWriter, r *http.Request) { u, err := checkSession(w, r) if u != nil && err == nil { http.Redirect(w, r, "/", http.StatusSeeOther) return } switch r.Method { case http.MethodGet: serveLogin(w, r, "") case http.MethodPost: u := r.FormValue("user") pass := r.FormValue("pass") user, err := CheckUserPass(db, User{-1, u, "", pass, false}) if err != nil { w.WriteHeader(http.StatusForbidden) serveLogin(w, r, "Incorrect login credentials") return } user.Pass = "" jsonData, err := json.Marshal(user) if err != nil { sendError(w, r, err.Error(), http.StatusInternalServerError) return } ciphertext := encrypt(jsonData) bStr := base64.StdEncoding.EncodeToString(ciphertext) cookie := http.Cookie { Name: "session", Value: bStr, MaxAge: 0, // Only https on qutebrowser Secure: true, HttpOnly: true, SameSite: http.SameSiteStrictMode, } http.SetCookie(w, &cookie) redirectflash := consumeFlash(w, r, "redirect") http.Redirect(w, r, redirectflash, http.StatusSeeOther) default: sendInvalidMethod(w, r) } } func handleFileServer(dir, prefix string) http.HandlerFunc { fs := http.FileServer(http.Dir(baseDocDir)) hdlr := http.StripPrefix(prefix, fs).ServeHTTP return func(w http.ResponseWriter, r *http.Request) { u, err := checkSession(w, r) if u != nil && err == nil { dir := filepath.Dir(r.URL.Path) username := filepath.Base(dir) if u.Username == username || u.IsAdmin { hdlr(w, r) return } } unauthorized(w, r) } } func download(w http.ResponseWriter, r *http.Request) { u, err := checkSession(w, r) if u != nil && err == nil { username := u.Username if u.IsAdmin { name := r.URL.Query().Get("user") if name != "" { username = name } } switch r.Method { case http.MethodPost: r.ParseForm() selection := r.Form["selection"] if len(selection) == 0 { sendFlash(w, r, "error", "Aucun fichier sélectionné") http.Redirect(w, r, "/", http.StatusSeeOther) return } contentDisposition := fmt.Sprintf("attachment; filename=\"Documents.zip\"") w.Header().Set("Content-Disposition", contentDisposition) wr := zip.NewWriter(w) defer wr.Close() for _, sel := range selection { if filepath.Base(filepath.Dir(sel)) == username { wrc, err := wr.Create(filepath.Base(sel)) if err != nil { sendError(w, r, err.Error(), http.StatusInternalServerError) return } f, err := os.Open(sel) if err != nil { sendError(w, r, err.Error(), http.StatusInternalServerError) return } io.Copy(wrc, f) } } case http.MethodGet: contentDisposition := fmt.Sprintf("attachment; filename=\"Documents.zip\"") w.Header().Set("Content-Disposition", contentDisposition) wr := zip.NewWriter(w) defer wr.Close() files, err := os.ReadDir(filepath.Join(baseDocDir, username)) if err != nil { sendError(w, r, err.Error(), http.StatusInternalServerError) return } for _, file := range files { filePath := path.Join(baseDocDir, username, file.Name()) wrc, err := wr.Create(filepath.Base(filePath)) if err != nil { sendError(w, r, err.Error(), http.StatusInternalServerError) return } f, err := os.Open(filePath) if err != nil { sendError(w, r, err.Error(), http.StatusInternalServerError) return } io.Copy(wrc, f) } default: sendInvalidMethod(w, r) } } else { unauthorized(w, r) } } func upload(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodPost: u, err := checkSession(w, r) if u != nil && err == nil { userImpersonation := false username := u.Username if u.IsAdmin { name := r.URL.Query().Get("user") if name != "" { username = name userImpersonation = true } } userDocPath := filepath.Join(baseDocDir, username) err := os.Mkdir(userDocPath, 0750) if err != nil && !os.IsExist(err) { sendError(w, r, err.Error(), http.StatusInternalServerError) } rd, err := r.MultipartReader() if err != nil { sendError(w, r, err.Error(), http.StatusInternalServerError) } for { part, err := rd.NextPart() if err == io.EOF { break } else if err != nil { sendError(w, r, err.Error(), http.StatusInternalServerError) } docPath := filepath.Join(userDocPath, part.FileName()) file, err := os.Create(docPath) if err != nil { sendError(w, r, err.Error(), http.StatusInternalServerError) } defer file.Close() _, err = io.Copy(file, part) if err != nil { sendError(w, r, err.Error(), http.StatusInternalServerError) } } if userImpersonation { http.Redirect(w, r, fmt.Sprintf("/?user=%s", username), http.StatusSeeOther) } else { http.Redirect(w, r, "/", http.StatusSeeOther) } } else { unauthorized(w, r) } default: sendInvalidMethod(w, r) } } func main() { p := flag.Int("p", 8080, "the port to bind to") dbPath := flag.String("d", "./db/test.db", "the db to connect to") flag.Parse() var err error log.Printf("Connecting to db: %s\n", *dbPath) db, err = InitAndGetDB("sqlite3", *dbPath) if err != nil { panic(err) } defer db.Close() http.HandleFunc("/docs/", handleFileServer(baseDocDir, "/docs/")) //http.Handle("/docs/", http.StripPrefix("/docs/", http.FileServer(http.Dir("docs")))) http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) http.HandleFunc("/", index) //http.HandleFunc("/createuser", createuser) http.HandleFunc("/login", login) http.HandleFunc("/logout", logout) http.HandleFunc("/upload", upload) http.HandleFunc("/download", download) http.HandleFunc("/imgs", imgs) http.HandleFunc("/robots.txt", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./robots.txt") }) http.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./favicon.ico") }) http.HandleFunc("/admin", admin) http.HandleFunc("/admin/users", adminUsers) log.Printf("Serving http://localhost:%d\n", *p) log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *p), nil)) }