package server import ( "encoding/json" "fmt" "net/http" "strings" "time" "git.t-juice.club/torjus/ezshare/store" "github.com/go-chi/chi/v5" "go.uber.org/zap" ) type HTTPServer struct { Logger *zap.SugaredLogger store store.FileStore binaryStore store.BinaryStore serverGRPCCert []byte grpcEndpoint string http.Server } type MetadataResponse struct { GRPCEndpoint string `json:"grpc_endpoint"` } func NewHTTPSever(store store.FileStore, binaryStore store.BinaryStore, certBytes []byte, grpcEndpoint string) *HTTPServer { srv := &HTTPServer{ Logger: zap.NewNop().Sugar(), store: store, binaryStore: binaryStore, serverGRPCCert: certBytes, grpcEndpoint: grpcEndpoint, } r := chi.NewRouter() r.Get("/server.pem", srv.ServerCertHandler) r.Get("/metadata", srv.MetadataHandler) r.Get("/files/{id}", srv.FileHandler) r.Get("/b", srv.BinaryIndexHandler) r.Get("/b/{filename}", srv.BinaryHandler) srv.Handler = r return srv } func (s *HTTPServer) ServerCertHandler(w http.ResponseWriter, r *http.Request) { if _, err := w.Write(s.serverGRPCCert); err != nil { s.Logger.Warnw("Error sending server certificate.", "error", err, "remote_addr", r.RemoteAddr) } s.Logger.Infow("Sent server certificate.", "remote_addr", r.RemoteAddr) } func (s *HTTPServer) BinaryIndexHandler(w http.ResponseWriter, r *http.Request) { binaries, err := s.binaryStore.List() if err != nil { WriteErrorResponse(w, http.StatusInternalServerError, "error listing binaries") return } for _, bin := range binaries { w.Write([]byte(fmt.Sprintf("%s\n", bin))) } } func (s *HTTPServer) BinaryHandler(w http.ResponseWriter, r *http.Request) { filename := chi.URLParam(r, "filename") split := strings.Split(filename, "-") if len(split) != 4 { WriteErrorResponse(w, http.StatusBadRequest, "invalid filename") return } version := split[1][1:] operatingSystem := split[2] arch := split[3] release, err := s.binaryStore.GetBinary(version, operatingSystem, arch) if err != nil { WriteErrorResponse(w, http.StatusNotFound, "release not found") return } if operatingSystem == "windows" { filename = fmt.Sprintf("%s.exe", filename) } w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename)) w.Write(release.Data) } func (s *HTTPServer) MetadataHandler(w http.ResponseWriter, r *http.Request) { md := &MetadataResponse{ GRPCEndpoint: s.grpcEndpoint, } encoder := json.NewEncoder(w) if err := encoder.Encode(md); err != nil { s.Logger.Warnw("Error encoding or sending metadata.", "error", err, "remote_addr", r.RemoteAddr) } s.Logger.Infow("Wrote server cert.", "remote_addr", r.RemoteAddr) } func (s *HTTPServer) FileHandler(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") f, err := s.store.GetFile(id) if err != nil { if err == store.ErrNoSuchItem { s.Logger.Debugw("Tried to get non-existing file.", "remote_addr", r.RemoteAddr) WriteErrorResponse(w, http.StatusNotFound, "file not found") return } s.Logger.Warnw("Error getting file from store.", "error", err, "remote_addr", r.RemoteAddr) WriteErrorResponse(w, http.StatusInternalServerError, fmt.Sprintf("error: %s", err)) return } // Check if expired, passcode required, or other reasons for not sending file // To many views if f.Metadata.MaxViews != 0 { if f.Metadata.CurrentViews >= f.Metadata.MaxViews { // File has been viewed max amount of times - delete s.Logger.Infow("File has been viewed too many times.", "id", id) if err := s.store.DeleteFile(id); err != nil { s.Logger.Warnw("Error deleting file.", "id", id) } WriteErrorResponse(w, http.StatusNotFound, "file viewed too many times") return } } // Expired if f.Metadata.ExpiresOn != nil { if f.Metadata.ExpiresOn.AsTime().Before(time.Now()) { // File has expired s.Logger.Infow("File has been expired.", "id", id) if err := s.store.DeleteFile(id); err != nil { s.Logger.Warnw("Error deleting file.", "id", id) } WriteErrorResponse(w, http.StatusNotFound, "file not found") return } } // Passcode required if f.Metadata.Passcode != "" { if f.Metadata.Passcode != r.URL.Query().Get("passcode") { WriteErrorResponse(w, http.StatusUnauthorized, "wrong passcode") return } } w.Header().Add("Content-Type", http.DetectContentType(f.Data)) if _, err := w.Write(f.Data); err != nil { s.Logger.Warnw("Error sending file.", "error", err, "remote_addr", r.RemoteAddr) } s.Logger.Infow("Sent file.", "remote_addr", r.RemoteAddr) f.Metadata.CurrentViews++ if _, err := s.store.StoreFile(f); err != nil { s.Logger.Warnw("Error storing updated file.", "error", err) } } func WriteErrorResponse(w http.ResponseWriter, status int, message string) { errMessage := struct { Error string `json:"error"` }{ Error: message, } w.Header().Add("Content-Type", "application/json") w.WriteHeader(status) encoder := json.NewEncoder(w) encoder.Encode(&errMessage) }