package api import ( "encoding/json" "io" "net/http" "path" "strconv" "strings" "time" "git.t-juice.club/torjus/gpaste" "git.t-juice.club/torjus/gpaste/files" "git.t-juice.club/torjus/gpaste/users" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/google/uuid" "go.uber.org/zap" ) type HTTPServer struct { Files files.FileStore Users users.UserStore Auth *gpaste.AuthService config *gpaste.ServerConfig Logger *zap.SugaredLogger AccessLogger *zap.SugaredLogger http.Server } func NewHTTPServer(cfg *gpaste.ServerConfig) *HTTPServer { srv := &HTTPServer{ config: cfg, Logger: zap.NewNop().Sugar(), AccessLogger: zap.NewNop().Sugar(), } srv.Files = files.NewMemoryFileStore() srv.Users = users.NewMemoryUserStore() srv.Auth = gpaste.NewAuthService(srv.Users, []byte(srv.config.SigningSecret)) // Create initial user // TODO: Do properly user := &users.User{Username: "admin", Role: users.RoleAdmin} user.SetPassword("admin") srv.Users.Store(user) r := chi.NewRouter() r.Use(middleware.RealIP) r.Use(middleware.RequestID) r.Use(srv.MiddlewareAccessLogger) r.Use(srv.MiddlewareAuthentication) r.Get("/", srv.HandlerIndex) r.Post("/api/file", srv.HandlerAPIFilePost) r.Get("/api/file/{id}", srv.HandlerAPIFileGet) r.Delete("/api/file/{id}", srv.HandlerAPIFileDelete) r.Post("/api/login", srv.HandlerAPILogin) r.Post("/api/user", srv.HandlerAPIUserCreate) srv.Handler = r return srv } func (s *HTTPServer) HandlerIndex(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("index")) } func (s *HTTPServer) HandlerAPIFilePost(w http.ResponseWriter, r *http.Request) { reqID := middleware.GetReqID(r.Context()) // Check if multipart form ct := r.Header.Get("Content-Type") if strings.Contains(ct, "multipart/form-data") { s.processMultiPartFormUpload(w, r) return } f := fileFromParams(r) f.ID = uuid.NewString() f.Body = r.Body err := s.Files.Store(f) if err != nil { w.WriteHeader(http.StatusInternalServerError) s.Logger.Warnw("Error storing file.", "req_id", reqID, "error", err, "id", f.ID, "remote_addr", r.RemoteAddr) return } s.Logger.Infow("Stored file.", "req_id", reqID, "id", f.ID, "remote_addr", r.RemoteAddr) fileURL := path.Join(s.config.URL, "/api/file", f.ID) var resp = struct { Message string `json:"message"` ID string `json:"id"` URL string `json:"url"` }{ Message: "OK", ID: f.ID, URL: fileURL, } w.WriteHeader(http.StatusAccepted) encoder := json.NewEncoder(w) if err := encoder.Encode(&resp); err != nil { s.Logger.Warnw("Error encoding response to client.", "req_id", reqID, "error", err, "remote_addr", r.RemoteAddr) } } func (s *HTTPServer) HandlerAPIFileGet(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") if id == "" { w.WriteHeader(http.StatusBadRequest) return } f, err := s.Files.Get(id) if err != nil { // TODO: LOG w.WriteHeader(http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) if _, err := io.Copy(w, f.Body); err != nil { reqID := middleware.GetReqID(r.Context()) s.Logger.Warnw("Error writing file to client.", "req_id", reqID, "error", err, "remote_addr", r.RemoteAddr) } } func (s *HTTPServer) HandlerAPIFileDelete(w http.ResponseWriter, r *http.Request) { // TODO: Require auth id := chi.URLParam(r, "id") if id == "" { w.WriteHeader(http.StatusBadRequest) return } err := s.Files.Delete(id) if err != nil { w.WriteHeader(http.StatusBadRequest) return } reqID := middleware.GetReqID(r.Context()) s.Logger.Infow("Deleted file", "id", id, "req_id", reqID) } func (s *HTTPServer) processMultiPartFormUpload(w http.ResponseWriter, r *http.Request) { reqID := middleware.GetReqID(r.Context()) var responses []ResponseAPIFilePost if err := r.ParseMultipartForm(1024 * 1024 * 10); err != nil { s.Logger.Warnw("Error parsing multipart form.", "req_id", reqID, "err", err) } for k := range r.MultipartForm.File { ff, fh, err := r.FormFile(k) if err != nil { s.Logger.Warnw("Error reading file from multipart form.", "req_id", reqID, "error", err) return } f := fileFromParams(r) f.ID = uuid.NewString() f.OriginalFilename = fh.Filename f.Body = ff if err := s.Files.Store(f); err != nil { w.WriteHeader(http.StatusInternalServerError) s.Logger.Warnw("Error storing file.", "req_id", reqID, "error", err, "id", f.ID, "remote_addr", r.RemoteAddr) return } s.Logger.Infow("Stored file.", "req_id", reqID, "id", f.ID, "filename", f.OriginalFilename, "remote_addr", r.RemoteAddr) fileURL := path.Join(s.config.URL, "/api/file", f.ID) responses = append(responses, ResponseAPIFilePost{Message: "OK", ID: f.ID, URL: fileURL}) } w.WriteHeader(http.StatusAccepted) encoder := json.NewEncoder(w) if err := encoder.Encode(&responses); err != nil { s.Logger.Warnw("Error encoding response to client.", "req_id", reqID, "error", err, "remote_addr", r.RemoteAddr) } } func (s *HTTPServer) HandlerAPILogin(w http.ResponseWriter, r *http.Request) { reqID := middleware.GetReqID(r.Context()) var expectedRequest RequestAPILogin decoder := json.NewDecoder(r.Body) defer r.Body.Close() if err := decoder.Decode(&expectedRequest); err != nil { w.WriteHeader(http.StatusBadRequest) return } token, err := s.Auth.Login(expectedRequest.Username, expectedRequest.Password) if err != nil { w.WriteHeader(http.StatusUnauthorized) return } response := ResponseAPILogin{ Token: token, } s.Logger.Infow("User logged in.", "req_id", reqID, "username", expectedRequest.Username) encoder := json.NewEncoder(w) if err := encoder.Encode(&response); err != nil { s.Logger.Infow("Error encoding json response to client.", "req_id", reqID, "error", err, "remote_addr", r.RemoteAddr) } } func (s *HTTPServer) HandlerAPIUserCreate(w http.ResponseWriter, r *http.Request) { reqID := middleware.GetReqID(r.Context()) defer r.Body.Close() role, err := RoleFromRequest(r) if err != nil || role != users.RoleAdmin { w.WriteHeader(http.StatusUnauthorized) return } var req RequestAPIUserCreate decoder := json.NewDecoder(r.Body) if err := decoder.Decode(&req); err != nil { s.Logger.Debugw("Error parsing request.", "req_id", reqID, "error", err, "remote_addr", r.RemoteAddr) w.WriteHeader(http.StatusBadRequest) return } // TODO: Ensure user does not already exist user := &users.User{Username: req.Username, Role: users.RoleUser} if err := user.SetPassword(req.Password); err != nil { s.Logger.Warnw("Error setting user password.", "req_id", reqID, "error", err, "remote_addr", r.RemoteAddr) w.WriteHeader(http.StatusBadRequest) return } if err := s.Users.Store(user); err != nil { s.Logger.Warnw("Error setting user password.", "req_id", reqID, "error", err, "remote_addr", r.RemoteAddr) w.WriteHeader(http.StatusInternalServerError) return } w.WriteHeader(http.StatusAccepted) s.Logger.Infow("Created user.", "req_id", reqID, "remote_addr", r.RemoteAddr, "username", req.Username) } func (s *HTTPServer) HandlerAPIUserList(w http.ResponseWriter, r *http.Request) { reqID := middleware.GetReqID(r.Context()) l, err := s.Users.List() if err != nil { s.Logger.Warnw("Error listing users.", "req_id", reqID, "error", err) w.WriteHeader(http.StatusInternalServerError) return } encoder := json.NewEncoder(w) if err := encoder.Encode(l); err != nil { s.Logger.Warnw("Error encoding response.", "req_id", "error", err) } } func fileFromParams(r *http.Request) *files.File { const ( keyMaxViews = "max_views" keyExpiresOn = "exp" ) var f files.File q := r.URL.Query() if q.Has(keyMaxViews) { views, err := strconv.ParseUint(q.Get(keyMaxViews), 10, 64) if err == nil { f.MaxViews = uint(views) } } if q.Has(keyExpiresOn) { exp, err := time.Parse(time.RFC3339, q.Get(keyExpiresOn)) if err == nil { f.ExpiresOn = exp } } return &f }