package api import ( "fmt" "io" "log" "net/http" "os" "path/filepath" "strings" "git.freecumextremist.com/grumbulon/pomme/internal" "git.freecumextremist.com/grumbulon/pomme/internal/util" "github.com/go-chi/jwtauth/v5" "github.com/go-chi/render" "github.com/miekg/dns" "gorm.io/gorm" ) // Upload godoc // // @Summary upload a zonefile // @Description upload takes files from the user and stores it locally to be parsed. Uploads are associated with a specific user. // // @Description Rate limited: 10 requests every 10 second // @Description you must specify "Bearer" before entering your token // // @Tags DNS // @Accept mpfd // @Produce json // @Param file formData file true "Zonefile to upload" // @Success 200 {object} internal.SwaggerGenericResponse[internal.Response] // @Failure 500 {object} internal.SwaggerGenericResponse[internal.Response] "internalServerError is a 500 server error with a logged error call back" // @Param Authorization header string true "Bearer Token" // // @Security Bearer // // @Router /api/upload [post] func ReceiveFile(w http.ResponseWriter, r *http.Request) { _, claims, _ := jwtauth.FromContext(r.Context()) r.Body = http.MaxBytesReader(w, r.Body, 1*1024*1024) // approx 1 mb max upload file, header, err := r.FormFile("file") if err != nil { APIError(w, r, genericResponseFields{"message": "File upload failed", "status": http.StatusInternalServerError, "error": err.Error()}) return } defer file.Close() //nolint: errcheck b, err := io.ReadAll(file) if err != nil { APIError(w, r, genericResponseFields{"message": "internal server error", "status": http.StatusInternalServerError, "error": err.Error()}) return } ok := validateContentType(file) if !ok { APIError(w, r, genericResponseFields{"message": "file must be text/plain", "status": http.StatusUnsupportedMediaType}) return } name := strings.Split(header.Filename, ".") if err = util.MakeLocal(name[0], claims["username"].(string), b); err != nil { APIError(w, r, genericResponseFields{"message": "internal server error", "status": http.StatusInternalServerError, "error": err.Error()}) return } db, ok := r.Context().Value(keyPrincipalContextID).(*gorm.DB) if !ok { APIError(w, r, genericResponseFields{"message": "internal server error", "status": http.StatusInternalServerError, "error": "unable to connect to DB"}) return } db.Create( &ZoneRequest{ User: claims["username"].(string), Zone: &Zone{ FileName: fmt.Sprintf("tmpfile-%s-%s", name[0], claims["username"].(string)), RawFileName: name[0], }, }) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) resp := internal.Response{ Message: "Successfully uploaded zonefile", } render.JSON(w, r, resp) } // Parse godoc // // @Summary parse your zonefile // @Description parse your zonefile // // @Description Rate limited: 10 requests every 10 second // @Description you must specify "Bearer" before entering your token // // @Tags DNS // @Accept mpfd // @Produce json // @Param filename query string true "Zonefile name" // @Success 200 {object} internal.SwaggerGenericResponse[internal.Response] // @Failure 500 {object} internal.SwaggerGenericResponse[internal.Response] "internalServerError is a 500 server error with a logged error call back" // @Param Authorization header string true "Bearer Token" // // @Security Bearer // // @Router /api/parse [post] func ParseZoneFiles(w http.ResponseWriter, r *http.Request) { var result internal.ZoneRequest _, claims, _ := jwtauth.FromContext(r.Context()) err := r.ParseForm() if err != nil { APIError(w, r, genericResponseFields{"message": "internal server error", "status": http.StatusInternalServerError, "error": err.Error()}) return } filename := r.Form.Get("filename") if filename == "" { APIError(w, r, genericResponseFields{"message": "no filename provided", "status": http.StatusInternalServerError}) return } db, ok := r.Context().Value(keyPrincipalContextID).(*gorm.DB) if !ok { APIError(w, r, genericResponseFields{"message": "internal server error", "status": http.StatusInternalServerError, "error": "unable to connect to DB"}) return } db.Where(ZoneRequest{ Zone: &Zone{ RawFileName: filename, }, User: claims["username"].(string), }).First(&result) if result == (internal.ZoneRequest{}) { APIError(w, r, genericResponseFields{"message": "internal server error", "status": http.StatusInternalServerError}) return } zoneFile := newNDSRequest(result.RawFileName, claims["username"].(string)) if err := zoneFile.Parse(); err != nil { APIError(w, r, genericResponseFields{"message": "Unable to parse zonefile", "status": http.StatusInternalServerError, "error": err.Error()}) return } w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(http.StatusCreated) resp := internal.Response{ Message: "Successfully parsed zonefile", } render.JSON(w, r, resp) } func SaveZoneFiles(w http.ResponseWriter, r *http.Request) { var result internal.ZoneRequest _, claims, _ := jwtauth.FromContext(r.Context()) err := r.ParseForm() if err != nil { APIError(w, r, genericResponseFields{"message": "internal server error", "status": http.StatusInternalServerError, "error": err.Error()}) return } filename := r.Form.Get("filename") if filename == "" { APIError(w, r, genericResponseFields{"message": "no filename provided", "status": http.StatusInternalServerError}) return } db, ok := r.Context().Value(keyPrincipalContextID).(*gorm.DB) if !ok { APIError(w, r, genericResponseFields{"message": "internal server error", "status": http.StatusInternalServerError, "error": "unable to connect to DB"}) return } db.Where(ZoneRequest{ Zone: &Zone{ RawFileName: filename, }, User: claims["username"].(string), }).First(&result) if result == (internal.ZoneRequest{}) { APIError(w, r, genericResponseFields{"message": "internal server error", "status": http.StatusInternalServerError}) return } zoneFile := newNDSRequest(result.RawFileName, claims["username"].(string)) if err := zoneFile.Save(); err != nil { APIError(w, r, genericResponseFields{"message": "Unable to save zonefile", "status": http.StatusInternalServerError, "error": err.Error()}) return } w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(http.StatusCreated) resp := internal.Response{ Message: "Successfully saved zonefile", } render.JSON(w, r, resp) } func newNDSRequest(filename string, user string) ndr { dat, err := os.ReadFile(fmt.Sprintf("/tmp/tmpfile-%s-%s", filename, user)) if err != nil { return &ZoneRequest{} } return &ZoneRequest{ User: user, Zone: &Zone{ FileName: fmt.Sprintf("tmpfile-%s-%s", filename, user), RawFileName: filename, Body: string(dat), }, } } // Parse will be used to parse zonefiles. func (zone *ZoneRequest) Parse() error { zp := dns.NewZoneParser(strings.NewReader(zone.Body), "", "") for rr, ok := zp.Next(); ok; rr, ok = zp.Next() { log.Println(rr) } if err := zp.Err(); err != nil { return fmt.Errorf("unable to parse Zonefile: %w", err) } return nil } func (zone *ZoneRequest) Save() error { c, err := internal.ReadConfig() if err != nil { logHandler(genericResponseFields{"error": err.Error(), "message": "no config file defined"}) return fmt.Errorf("unable to parse directory: %w", err) } var path string = fmt.Sprintf("%s/%s/", c.ZoneDir, zone.RawFileName) var tmpPath string = fmt.Sprintf("/tmp/tmpfile-%s-%s", zone.RawFileName, zone.User) if err = os.MkdirAll(path, 0o750); err != nil { logHandler(genericResponseFields{"error": err.Error(), "message": "unable to make directory for zone files"}) return fmt.Errorf("unable to make zone directory: %w", err) } if _, err = os.Create(filepath.Clean(path + zone.RawFileName)); err != nil { logHandler(genericResponseFields{"error": err.Error(), "message": "unable to save zonefile to directory"}) return fmt.Errorf("unable to save zonefile to directory: %w", err) } f, err := os.Open(filepath.Clean(tmpPath)) if err != nil { logHandler(genericResponseFields{"error": err.Error(), "message": "unable to save zonefile to directory"}) return fmt.Errorf("unable to save zonefile to directory: %w", err) } defer func() { if err = f.Close(); err != nil { logHandler(genericResponseFields{"message": "Error closing file", "error": err.Error()}) } if err = os.Remove(filepath.Clean(tmpPath)); err != nil { logHandler(genericResponseFields{"message": "Error removing tmp file", "error": err.Error()}) } }() b, err := io.ReadAll(f) if err != nil { logHandler(genericResponseFields{"error": err.Error(), "message": "unable to save zonefile to directory"}) return fmt.Errorf("unable to save zonefile to directory: %w", err) } if err = os.WriteFile(path+zone.RawFileName, b, 0o600); err != nil { logHandler(genericResponseFields{"error": err.Error(), "message": "unable to save zonefile to directory"}) return fmt.Errorf("unable to save zonefile to directory: %w", err) } return nil } func validateContentType(file io.Reader) bool { bytes, err := io.ReadAll(file) if err != nil { return false } mimeType := http.DetectContentType(bytes) mime := strings.Contains(mimeType, "text/plain") switch mime { case true: return true default: return false } }