package api import ( "fmt" "io" "log" "net/http" "strings" "dns.froth.zone/pomme/internal" "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 a multipart form file as user input. It must not exceed 1 mb and must be of text/plain content type. // @Description If a file uploads successfully it will be saved locally and parsed. // @Description If parsing is successful pomme will save the file to your ZoneDir defined in your config. // @Description 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) { var result internal.User _, 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 { logger := newResponder(Response[any]{ Message: "File upload failed", Status: http.StatusInternalServerError, Err: err.Error(), }) logger.apiError(w, r) logger.writeLogEntry() return } defer file.Close() //nolint: errcheck b, err := io.ReadAll(file) if err != nil { logger := newResponder(Response[any]{ Message: "internal server error", Status: http.StatusInternalServerError, Err: err.Error(), }) logger.apiError(w, r) logger.writeLogEntry() return } ok := validateContentType(file) if !ok { logger := newResponder(Response[any]{ Message: "file type must be text/plain", Status: http.StatusUnsupportedMediaType, }) logger.apiError(w, r) logger.writeLogEntry() return } zoneFile := newDNSRequest(header.Filename, claims["username"].(string), b) if err := zoneFile.parse(); err != nil { logger := newResponder(Response[any]{ Message: "Unable to parse zonefile", Status: http.StatusInternalServerError, Err: err.Error(), }) logger.apiError(w, r) logger.writeLogEntry() return } db, ok := r.Context().Value(keyPrincipalContextID).(*gorm.DB) if !ok { logger := newResponder(Response[any]{ Message: "internal server error", Status: http.StatusInternalServerError, Err: "unable to connect to DB", }) logger.apiError(w, r) logger.writeLogEntry() return } // check if request is coming from user not in the DB but has a valid JWT db.Where("username = ?", claims["username"].(string)).First(&result) if result.Username == "" { logger := newResponder(Response[any]{ Message: "user does not exist", Status: http.StatusInternalServerError, }) logger.apiError(w, r) return } db.Create( &ZoneRequest{ User: claims["username"].(string), Zone: &Zone{ FileName: header.Filename, }, }) if err := zoneFile.save(); err != nil { logger := newResponder(Response[any]{ Message: "Unable to save zonefile", Status: http.StatusInternalServerError, Err: err.Error(), }) logger.apiError(w, r) logger.writeLogEntry() return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) resp := internal.Response{ Message: "Successfully uploaded zonefile", } render.JSON(w, r, resp) } func newDNSRequest(filename string, user string, dat []byte) ndr { return &ZoneRequest{ User: user, Zone: &Zone{ FileName: filename, Body: dat, }, } } func (zone *ZoneRequest) parse() error { zp := dns.NewZoneParser(strings.NewReader(string(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 { return makeLocal(zone) } 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 } }