From ae488eb7d2eaa71a4dda7c03b29841dc80010b6c Mon Sep 17 00:00:00 2001 From: grumbulon Date: Sat, 4 Feb 2023 17:15:20 -0500 Subject: [PATCH 01/14] add ndr interface for handling dns reqs, add zonedir to config, add /save route to save zfs to zonedir, fix make local, and other small changes --- internal/api/api.go | 1 + internal/api/helpers.go | 2 +- internal/api/types.go | 11 +++- internal/api/zone.go | 123 +++++++++++++++++++++++++++++++++++----- internal/types.go | 1 + internal/util/fs.go | 7 +-- 6 files changed, 123 insertions(+), 22 deletions(-) diff --git a/internal/api/api.go b/internal/api/api.go index e0026ef..50cd295 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -35,6 +35,7 @@ func API() http.Handler { api.Use(jwtauth.Authenticator) api.With(setDBMiddleware).Post("/upload", ReceiveFile) api.With(setDBMiddleware).Post("/parse", ParseZoneFiles) + api.With(setDBMiddleware).Post("/save", SaveZoneFiles) }) // Open routes diff --git a/internal/api/helpers.go b/internal/api/helpers.go index cd26208..12e20f2 100644 --- a/internal/api/helpers.go +++ b/internal/api/helpers.go @@ -48,7 +48,7 @@ func setDBMiddleware(next http.Handler) http.Handler { }) } -func APIError[T map[string]any](w http.ResponseWriter, r *http.Request, v map[string]any) { +func APIError[T ~map[string]any](w http.ResponseWriter, r *http.Request, v T) { w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("Content-Type", "application/json; charset=utf-8") diff --git a/internal/api/types.go b/internal/api/types.go index a56a1cc..7293855 100644 --- a/internal/api/types.go +++ b/internal/api/types.go @@ -6,8 +6,6 @@ const ( keyPrincipalContextID key = iota ) -type genericResponseFields map[string]any - type key int // ZoneRequest represents a Zone file request. @@ -28,3 +26,12 @@ type Zone struct { type GenericResponse[T map[string]any] struct { Response map[string]any `json:"response,omitempty"` } + +type genericResponseFields map[string]any + +type ndr interface { + Parse() error + Save() error +} + +var _ ndr = (*ZoneRequest)(nil) diff --git a/internal/api/zone.go b/internal/api/zone.go index 489d719..b517576 100644 --- a/internal/api/zone.go +++ b/internal/api/zone.go @@ -1,7 +1,6 @@ package api import ( - "bytes" "fmt" "io" "log" @@ -39,8 +38,6 @@ import ( func ReceiveFile(w http.ResponseWriter, r *http.Request) { _, claims, _ := jwtauth.FromContext(r.Context()) - var buf bytes.Buffer - r.Body = http.MaxBytesReader(w, r.Body, 1*1024*1024) // approx 1 mb max upload file, header, err := r.FormFile("file") @@ -52,6 +49,11 @@ func ReceiveFile(w http.ResponseWriter, r *http.Request) { defer file.Close() //nolint: errcheck + b, err := io.ReadAll(file) + if err != nil { + log.Fatalln(err) + } + ok := validateContentType(file) if !ok { http.Error(w, "file must be text/plain", http.StatusUnsupportedMediaType) @@ -61,13 +63,7 @@ func ReceiveFile(w http.ResponseWriter, r *http.Request) { name := strings.Split(header.Filename, ".") - if _, err = io.Copy(&buf, file); err != nil { - APIError(w, r, genericResponseFields{"message": "internal server error", "status": http.StatusInternalServerError, "error": err.Error()}) - - return - } - - if err = util.MakeLocal(name[0], claims["username"].(string), buf); err != nil { + 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 @@ -89,8 +85,6 @@ func ReceiveFile(w http.ResponseWriter, r *http.Request) { }, }) - buf.Reset() - w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) @@ -161,7 +155,7 @@ func ParseZoneFiles(w http.ResponseWriter, r *http.Request) { return } - zoneFile := newZoneRequest(result.RawFileName, claims["username"].(string)) + 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()}) @@ -180,7 +174,66 @@ func ParseZoneFiles(w http.ResponseWriter, r *http.Request) { render.JSON(w, r, resp) } -func newZoneRequest(filename string, user string) *ZoneRequest { +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{} @@ -211,6 +264,48 @@ func (zone *ZoneRequest) Parse() error { 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 os.MkdirAll(path, 0755); 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(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(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 f.Close() + + 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 os.WriteFile(path+zone.RawFileName, b, 0666); 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 { diff --git a/internal/types.go b/internal/types.go index 5d049e8..97e99c9 100644 --- a/internal/types.go +++ b/internal/types.go @@ -35,6 +35,7 @@ type Config struct { Port string DB string TestDB string + ZoneDir string } // SwaggerGenericResponse[T] diff --git a/internal/util/fs.go b/internal/util/fs.go index c46b092..14e6403 100644 --- a/internal/util/fs.go +++ b/internal/util/fs.go @@ -1,18 +1,15 @@ package util import ( - "bytes" "fmt" "os" ) -func MakeLocal(filename, username string, buf bytes.Buffer) error { +func MakeLocal(filename, username string, buf []byte) error { if _, err := os.Stat(fmt.Sprintf("/tmp/tmpfile-%s-%s", filename, username)); !os.IsNotExist(err) { return fmt.Errorf("file %s already exists: %w", filename, err) } - defer buf.Reset() - f, err := os.Create("/tmp/tmpfile-" + filename + "-" + username) //nolint: gosec // this is set to nolint because I am doing everything os.CreateTemp does but here since I don't like some of the limitations if err != nil { @@ -26,7 +23,7 @@ func MakeLocal(filename, username string, buf bytes.Buffer) error { } }() - err = os.WriteFile(f.Name(), buf.Bytes(), 0o600) + err = os.WriteFile(f.Name(), buf, 0o600) if err != nil { return fmt.Errorf("failed to write file locally: %w", err) From 653670e6fa5896aed015588b8dd632cb72c4677a Mon Sep 17 00:00:00 2001 From: grumbulon Date: Sat, 4 Feb 2023 17:18:18 -0500 Subject: [PATCH 02/14] return error if file being saved to /tmp is empty --- internal/util/fs.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/util/fs.go b/internal/util/fs.go index 14e6403..6c04f8f 100644 --- a/internal/util/fs.go +++ b/internal/util/fs.go @@ -10,6 +10,10 @@ func MakeLocal(filename, username string, buf []byte) error { return fmt.Errorf("file %s already exists: %w", filename, err) } + if len(buf) == 0 { + return fmt.Errorf("will not save empty file: %s to FS", filename) + } + f, err := os.Create("/tmp/tmpfile-" + filename + "-" + username) //nolint: gosec // this is set to nolint because I am doing everything os.CreateTemp does but here since I don't like some of the limitations if err != nil { From 08eab3e6cb567ca2c58bd53e33c6a2af9617f16a Mon Sep 17 00:00:00 2001 From: grumbulon Date: Sat, 4 Feb 2023 17:29:08 -0500 Subject: [PATCH 03/14] lint --- internal/api/zone.go | 28 +++++++++++++++++++++------- internal/util/fs.go | 5 ++++- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/internal/api/zone.go b/internal/api/zone.go index b517576..f28ac83 100644 --- a/internal/api/zone.go +++ b/internal/api/zone.go @@ -6,6 +6,7 @@ import ( "log" "net/http" "os" + "path/filepath" "strings" "git.freecumextremist.com/grumbulon/pomme/internal" @@ -51,12 +52,14 @@ func ReceiveFile(w http.ResponseWriter, r *http.Request) { b, err := io.ReadAll(file) if err != nil { - log.Fatalln(err) + APIError(w, r, genericResponseFields{"message": "internal server error", "status": http.StatusInternalServerError, "error": err.Error()}) + + return } ok := validateContentType(file) if !ok { - http.Error(w, "file must be text/plain", http.StatusUnsupportedMediaType) + APIError(w, r, genericResponseFields{"message": "file must be text/plain", "status": http.StatusUnsupportedMediaType}) return } @@ -268,38 +271,49 @@ 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 os.MkdirAll(path, 0755); err != nil { + 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(path + zone.RawFileName); err != nil { + 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(tmpPath) + 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 f.Close() + defer func() { + if err = f.Close(); err != nil { + logHandler(genericResponseFields{"message": "Error closing 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 os.WriteFile(path+zone.RawFileName, b, 0666); err != nil { + 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) } diff --git a/internal/util/fs.go b/internal/util/fs.go index 6c04f8f..d2048e4 100644 --- a/internal/util/fs.go +++ b/internal/util/fs.go @@ -1,17 +1,20 @@ package util import ( + "errors" "fmt" "os" ) +var errEmptyFile = errors.New("will not save empty file to FS") + func MakeLocal(filename, username string, buf []byte) error { if _, err := os.Stat(fmt.Sprintf("/tmp/tmpfile-%s-%s", filename, username)); !os.IsNotExist(err) { return fmt.Errorf("file %s already exists: %w", filename, err) } if len(buf) == 0 { - return fmt.Errorf("will not save empty file: %s to FS", filename) + return errEmptyFile } f, err := os.Create("/tmp/tmpfile-" + filename + "-" + username) //nolint: gosec From a572f6841fdbd99b81651b4bac3c23ecd858ad18 Mon Sep 17 00:00:00 2001 From: grumbulon Date: Sat, 4 Feb 2023 17:32:36 -0500 Subject: [PATCH 04/14] remove tmp file after saving to permanent directory --- internal/api/zone.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/api/zone.go b/internal/api/zone.go index f28ac83..db607d7 100644 --- a/internal/api/zone.go +++ b/internal/api/zone.go @@ -302,6 +302,10 @@ func (zone *ZoneRequest) Save() error { 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) From a83ae1bff3d3ed8bb9867571b86c2bc1ea813b4e Mon Sep 17 00:00:00 2001 From: grumbulon Date: Sat, 4 Feb 2023 17:34:08 -0500 Subject: [PATCH 05/14] update sample config --- config.sample.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config.sample.yaml b/config.sample.yaml index 94d008c..2875fb6 100644 --- a/config.sample.yaml +++ b/config.sample.yaml @@ -2,4 +2,5 @@ server: example.com # does nothing yet hashingsecret: PasswordHashingSecret port: 3008 # port the server runs on db: pomme.sqlite -testdb: test.sqlite \ No newline at end of file +testdb: test.sqlite +zonedir: /home/user/pomme/nds/pomme/zones \ No newline at end of file From 2426a8b393c5ebe7596c92d871975248029107cf Mon Sep 17 00:00:00 2001 From: grumbulon Date: Sat, 4 Feb 2023 18:48:00 -0500 Subject: [PATCH 06/14] turn into one route --- internal/api/api.go | 2 - internal/api/types.go | 4 +- internal/api/zone.go | 165 +++++------------------------------------- 3 files changed, 22 insertions(+), 149 deletions(-) diff --git a/internal/api/api.go b/internal/api/api.go index 50cd295..f257a6e 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -34,8 +34,6 @@ func API() http.Handler { api.Use(jwtauth.Authenticator) api.With(setDBMiddleware).Post("/upload", ReceiveFile) - api.With(setDBMiddleware).Post("/parse", ParseZoneFiles) - api.With(setDBMiddleware).Post("/save", SaveZoneFiles) }) // Open routes diff --git a/internal/api/types.go b/internal/api/types.go index 7293855..2efe5e3 100644 --- a/internal/api/types.go +++ b/internal/api/types.go @@ -30,8 +30,8 @@ type GenericResponse[T map[string]any] struct { type genericResponseFields map[string]any type ndr interface { - Parse() error - Save() error + parse() error + save() error } var _ ndr = (*ZoneRequest)(nil) diff --git a/internal/api/zone.go b/internal/api/zone.go index db607d7..378d9f1 100644 --- a/internal/api/zone.go +++ b/internal/api/zone.go @@ -64,9 +64,7 @@ func ReceiveFile(w http.ResponseWriter, r *http.Request) { return } - name := strings.Split(header.Filename, ".") - - if err = util.MakeLocal(name[0], claims["username"].(string), b); err != nil { + if err = util.MakeLocal(header.Filename, claims["username"].(string), b); err != nil { APIError(w, r, genericResponseFields{"message": "internal server error", "status": http.StatusInternalServerError, "error": err.Error()}) return @@ -83,11 +81,25 @@ func ReceiveFile(w http.ResponseWriter, r *http.Request) { &ZoneRequest{ User: claims["username"].(string), Zone: &Zone{ - FileName: fmt.Sprintf("tmpfile-%s-%s", name[0], claims["username"].(string)), - RawFileName: name[0], + FileName: fmt.Sprintf("tmpfile-%s-%s", header.Filename, claims["username"].(string)), + RawFileName: header.Filename, }, }) + zoneFile := newDNSRequest(header.Filename, 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 + } + + 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") w.WriteHeader(http.StatusCreated) @@ -99,144 +111,7 @@ func ReceiveFile(w http.ResponseWriter, r *http.Request) { 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 { +func newDNSRequest(filename string, user string) ndr { dat, err := os.ReadFile(fmt.Sprintf("/tmp/tmpfile-%s-%s", filename, user)) if err != nil { return &ZoneRequest{} @@ -253,7 +128,7 @@ func newNDSRequest(filename string, user string) ndr { } // Parse will be used to parse zonefiles. -func (zone *ZoneRequest) Parse() error { +func (zone *ZoneRequest) parse() error { zp := dns.NewZoneParser(strings.NewReader(zone.Body), "", "") for rr, ok := zp.Next(); ok; rr, ok = zp.Next() { @@ -267,7 +142,7 @@ func (zone *ZoneRequest) Parse() error { return nil } -func (zone *ZoneRequest) Save() error { +func (zone *ZoneRequest) save() error { c, err := internal.ReadConfig() if err != nil { logHandler(genericResponseFields{"error": err.Error(), "message": "no config file defined"}) From 92100e27a03eaf26bfc6a1a1bb9d531e3d00a7fc Mon Sep 17 00:00:00 2001 From: grumbulon Date: Sat, 4 Feb 2023 20:41:16 -0500 Subject: [PATCH 07/14] add fs.go to internal, make makeLocal do the majority of lifting for tmp and perm files, made save() call makeLocal --- internal/api/fs.go | 69 +++++++++++++++++++++++++++++++++++++++ internal/api/types.go | 9 ++++- internal/api/zone.go | 76 ++++++------------------------------------- internal/util/fs.go | 40 ----------------------- 4 files changed, 87 insertions(+), 107 deletions(-) create mode 100644 internal/api/fs.go delete mode 100644 internal/util/fs.go diff --git a/internal/api/fs.go b/internal/api/fs.go new file mode 100644 index 0000000..492dd24 --- /dev/null +++ b/internal/api/fs.go @@ -0,0 +1,69 @@ +package api + +import ( + "errors" + "fmt" + "os" + "path/filepath" + + "git.freecumextremist.com/grumbulon/pomme/internal" +) + +var errEmptyFile = errors.New("will not save empty file to FS") + +// makeLocal takes a type path and then saves a zone file to either tmp or a permanent location +func makeLocal(where path, zone *ZoneRequest) error { + var ( + path string + file string + c *internal.Config + err error + ) + + if _, err := os.Stat(fmt.Sprintf(zone.FileName, zone.User)); !os.IsNotExist(err) { + return fmt.Errorf("file %s already exists: %w", zone.FileName, err) + } + + if len([]byte(zone.Body)) == 0 { + return errEmptyFile + } + + switch where { + case Tmp: + path = fmt.Sprintf("/tmp/tmpfile-%s-%s", zone.RawFileName, zone.User) + case Perm: + 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) + } + path = fmt.Sprintf("%s/%s/", c.ZoneDir, zone.RawFileName) + file = zone.RawFileName + 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) + } + } + + f, err := os.Create(filepath.Clean(path + file)) //nolint: gosec + if err != nil { + return fmt.Errorf("failed to write file locally: %w", err) + } + + // close and remove the temporary file at the end of the program + defer func() { + if err = f.Close(); err != nil { + return + } + }() + + err = os.WriteFile(f.Name(), []byte(zone.Body), 0o600) + + if err != nil { + return fmt.Errorf("failed to write file locally: %w", err) + } + + return nil +} diff --git a/internal/api/types.go b/internal/api/types.go index 2efe5e3..8e40922 100644 --- a/internal/api/types.go +++ b/internal/api/types.go @@ -6,8 +6,15 @@ const ( keyPrincipalContextID key = iota ) +const ( + Tmp path = iota + Perm +) + type key int +type path int + // ZoneRequest represents a Zone file request. type ZoneRequest struct { *Zone @@ -31,7 +38,7 @@ type genericResponseFields map[string]any type ndr interface { parse() error - save() error + save(where path) error } var _ ndr = (*ZoneRequest)(nil) diff --git a/internal/api/zone.go b/internal/api/zone.go index 378d9f1..8cf9d8e 100644 --- a/internal/api/zone.go +++ b/internal/api/zone.go @@ -10,7 +10,6 @@ import ( "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" @@ -64,8 +63,10 @@ func ReceiveFile(w http.ResponseWriter, r *http.Request) { return } - if err = util.MakeLocal(header.Filename, claims["username"].(string), b); err != nil { - APIError(w, r, genericResponseFields{"message": "internal server error", "status": http.StatusInternalServerError, "error": err.Error()}) + zoneFile := newDNSRequest(header.Filename, claims["username"].(string), b) + + if err := zoneFile.save(Tmp); err != nil { + APIError(w, r, genericResponseFields{"message": "Unable to save zonefile", "status": http.StatusInternalServerError, "error": err.Error()}) return } @@ -86,15 +87,13 @@ func ReceiveFile(w http.ResponseWriter, r *http.Request) { }, }) - zoneFile := newDNSRequest(header.Filename, 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 } - if err := zoneFile.save(); err != nil { + if err := zoneFile.save(Perm); err != nil { APIError(w, r, genericResponseFields{"message": "Unable to save zonefile", "status": http.StatusInternalServerError, "error": err.Error()}) return @@ -111,12 +110,7 @@ func ReceiveFile(w http.ResponseWriter, r *http.Request) { render.JSON(w, r, resp) } -func newDNSRequest(filename string, user string) ndr { - dat, err := os.ReadFile(fmt.Sprintf("/tmp/tmpfile-%s-%s", filename, user)) - if err != nil { - return &ZoneRequest{} - } - +func newDNSRequest(filename string, user string, dat []byte) ndr { return &ZoneRequest{ User: user, Zone: &Zone{ @@ -142,61 +136,11 @@ func (zone *ZoneRequest) parse() error { return nil } -func (zone *ZoneRequest) save() error { - c, err := internal.ReadConfig() - if err != nil { - logHandler(genericResponseFields{"error": err.Error(), "message": "no config file defined"}) +func (zone *ZoneRequest) save(path) error { + // clean up the tmp file + defer os.Remove(filepath.Clean("/tmp/" + zone.FileName)) - 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 + return makeLocal(Perm, zone) } func validateContentType(file io.Reader) bool { diff --git a/internal/util/fs.go b/internal/util/fs.go deleted file mode 100644 index d2048e4..0000000 --- a/internal/util/fs.go +++ /dev/null @@ -1,40 +0,0 @@ -package util - -import ( - "errors" - "fmt" - "os" -) - -var errEmptyFile = errors.New("will not save empty file to FS") - -func MakeLocal(filename, username string, buf []byte) error { - if _, err := os.Stat(fmt.Sprintf("/tmp/tmpfile-%s-%s", filename, username)); !os.IsNotExist(err) { - return fmt.Errorf("file %s already exists: %w", filename, err) - } - - if len(buf) == 0 { - return errEmptyFile - } - - f, err := os.Create("/tmp/tmpfile-" + filename + "-" + username) //nolint: gosec - // this is set to nolint because I am doing everything os.CreateTemp does but here since I don't like some of the limitations - if err != nil { - return fmt.Errorf("failed to write file locally: %w", err) - } - - // close and remove the temporary file at the end of the program - defer func() { - if err = f.Close(); err != nil { - return - } - }() - - err = os.WriteFile(f.Name(), buf, 0o600) - - if err != nil { - return fmt.Errorf("failed to write file locally: %w", err) - } - - return nil -} From ae3e7a1ae6a854e4cfdc02d47a058bd3f0fc1477 Mon Sep 17 00:00:00 2001 From: grumbulon Date: Sat, 4 Feb 2023 20:46:28 -0500 Subject: [PATCH 08/14] capture error on defer, doc string, lint --- internal/api/fs.go | 8 ++++++-- internal/api/zone.go | 7 ++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/internal/api/fs.go b/internal/api/fs.go index 492dd24..3df8e1d 100644 --- a/internal/api/fs.go +++ b/internal/api/fs.go @@ -11,7 +11,7 @@ import ( var errEmptyFile = errors.New("will not save empty file to FS") -// makeLocal takes a type path and then saves a zone file to either tmp or a permanent location +// makeLocal takes a type path and then saves a zone file to either tmp or a permanent location. func makeLocal(where path, zone *ZoneRequest) error { var ( path string @@ -20,7 +20,7 @@ func makeLocal(where path, zone *ZoneRequest) error { err error ) - if _, err := os.Stat(fmt.Sprintf(zone.FileName, zone.User)); !os.IsNotExist(err) { + if _, err = os.Stat(fmt.Sprintf(zone.FileName, zone.User)); !os.IsNotExist(err) { return fmt.Errorf("file %s already exists: %w", zone.FileName, err) } @@ -33,13 +33,17 @@ func makeLocal(where path, zone *ZoneRequest) error { path = fmt.Sprintf("/tmp/tmpfile-%s-%s", zone.RawFileName, zone.User) case Perm: 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) } + path = fmt.Sprintf("%s/%s/", c.ZoneDir, zone.RawFileName) + file = zone.RawFileName + if err = os.MkdirAll(path, 0o750); err != nil { logHandler(genericResponseFields{"error": err.Error(), "message": "unable to make directory for zone files"}) diff --git a/internal/api/zone.go b/internal/api/zone.go index 8cf9d8e..577f6f2 100644 --- a/internal/api/zone.go +++ b/internal/api/zone.go @@ -138,7 +138,12 @@ func (zone *ZoneRequest) parse() error { func (zone *ZoneRequest) save(path) error { // clean up the tmp file - defer os.Remove(filepath.Clean("/tmp/" + zone.FileName)) + defer func() (err error) { + if err = os.Remove(filepath.Clean("/tmp/" + zone.FileName)); err != nil { + return + } + return + }() return makeLocal(Perm, zone) } From f7f0a4951270ae8d5a1fe76a58704908b040b1f8 Mon Sep 17 00:00:00 2001 From: grumbulon Date: Sat, 4 Feb 2023 21:05:39 -0500 Subject: [PATCH 09/14] update swagger docs --- docs/docs.go | 52 +------------------------------------------- docs/swagger.json | 52 +------------------------------------------- docs/swagger.yaml | 41 ++++------------------------------ internal/api/zone.go | 5 ++++- 4 files changed, 10 insertions(+), 140 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 9c7572d..aeb6391 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -62,56 +62,6 @@ const docTemplate = `{ } } }, - "/api/parse": { - "post": { - "security": [ - { - "Bearer": [] - } - ], - "description": "parse your zonefile\nRate limited: 10 requests every 10 second\nyou must specify \"Bearer\" before entering your token", - "consumes": [ - "multipart/form-data" - ], - "produces": [ - "application/json" - ], - "tags": [ - "DNS" - ], - "summary": "parse your zonefile", - "parameters": [ - { - "type": "string", - "description": "Zonefile name", - "name": "filename", - "in": "query", - "required": true - }, - { - "type": "string", - "description": "Bearer Token", - "name": "Authorization", - "in": "header", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/internal.SwaggerGenericResponse-internal_Response" - } - }, - "500": { - "description": "internalServerError is a 500 server error with a logged error call back", - "schema": { - "$ref": "#/definitions/internal.SwaggerGenericResponse-internal_Response" - } - } - } - } - }, "/api/upload": { "post": { "security": [ @@ -119,7 +69,7 @@ const docTemplate = `{ "Bearer": [] } ], - "description": "upload takes files from the user and stores it locally to be parsed. Uploads are associated with a specific user.\nRate limited: 10 requests every 10 second\nyou must specify \"Bearer\" before entering your token", + "description": "Upload takes a multipart form file as user input. It must not exceede 1 mb and must be of text/plain content type.\nIf a file uploads successfully it will be saved locally and parsed.\nIf parsing is successful pomme will save the file to your ZoneDir defined in your config.\nUploads are associated with a specific user.\nRate limited: 10 requests every 10 second\nyou must specify \"Bearer\" before entering your token", "consumes": [ "multipart/form-data" ], diff --git a/docs/swagger.json b/docs/swagger.json index be72e9e..fc04d4b 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -53,56 +53,6 @@ } } }, - "/api/parse": { - "post": { - "security": [ - { - "Bearer": [] - } - ], - "description": "parse your zonefile\nRate limited: 10 requests every 10 second\nyou must specify \"Bearer\" before entering your token", - "consumes": [ - "multipart/form-data" - ], - "produces": [ - "application/json" - ], - "tags": [ - "DNS" - ], - "summary": "parse your zonefile", - "parameters": [ - { - "type": "string", - "description": "Zonefile name", - "name": "filename", - "in": "query", - "required": true - }, - { - "type": "string", - "description": "Bearer Token", - "name": "Authorization", - "in": "header", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/internal.SwaggerGenericResponse-internal_Response" - } - }, - "500": { - "description": "internalServerError is a 500 server error with a logged error call back", - "schema": { - "$ref": "#/definitions/internal.SwaggerGenericResponse-internal_Response" - } - } - } - } - }, "/api/upload": { "post": { "security": [ @@ -110,7 +60,7 @@ "Bearer": [] } ], - "description": "upload takes files from the user and stores it locally to be parsed. Uploads are associated with a specific user.\nRate limited: 10 requests every 10 second\nyou must specify \"Bearer\" before entering your token", + "description": "Upload takes a multipart form file as user input. It must not exceede 1 mb and must be of text/plain content type.\nIf a file uploads successfully it will be saved locally and parsed.\nIf parsing is successful pomme will save the file to your ZoneDir defined in your config.\nUploads are associated with a specific user.\nRate limited: 10 requests every 10 second\nyou must specify \"Bearer\" before entering your token", "consumes": [ "multipart/form-data" ], diff --git a/docs/swagger.yaml b/docs/swagger.yaml index ea7667b..7a4e7b2 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -50,48 +50,15 @@ paths: summary: authenticate as a regular user tags: - accounts - /api/parse: - post: - consumes: - - multipart/form-data - description: |- - parse your zonefile - Rate limited: 10 requests every 10 second - you must specify "Bearer" before entering your token - parameters: - - description: Zonefile name - in: query - name: filename - required: true - type: string - - description: Bearer Token - in: header - name: Authorization - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/internal.SwaggerGenericResponse-internal_Response' - "500": - description: internalServerError is a 500 server error with a logged error - call back - schema: - $ref: '#/definitions/internal.SwaggerGenericResponse-internal_Response' - security: - - Bearer: [] - summary: parse your zonefile - tags: - - DNS /api/upload: post: consumes: - multipart/form-data description: |- - upload takes files from the user and stores it locally to be parsed. Uploads are associated with a specific user. + Upload takes a multipart form file as user input. It must not exceede 1 mb and must be of text/plain content type. + If a file uploads successfully it will be saved locally and parsed. + If parsing is successful pomme will save the file to your ZoneDir defined in your config. + Uploads are associated with a specific user. Rate limited: 10 requests every 10 second you must specify "Bearer" before entering your token parameters: diff --git a/internal/api/zone.go b/internal/api/zone.go index 577f6f2..8e516db 100644 --- a/internal/api/zone.go +++ b/internal/api/zone.go @@ -19,7 +19,10 @@ import ( // 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 Upload takes a multipart form file as user input. It must not exceede 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 From bca521eb4e8de6e3916d4a72d7d03447b58bf164 Mon Sep 17 00:00:00 2001 From: grumbulon Date: Sat, 4 Feb 2023 21:49:23 -0500 Subject: [PATCH 10/14] wow woah, we don't need to save locally actually --- internal/api/fs.go | 25 ++++++++++--------------- internal/api/zone.go | 24 ++++-------------------- 2 files changed, 14 insertions(+), 35 deletions(-) diff --git a/internal/api/fs.go b/internal/api/fs.go index 3df8e1d..be9a3ae 100644 --- a/internal/api/fs.go +++ b/internal/api/fs.go @@ -28,27 +28,22 @@ func makeLocal(where path, zone *ZoneRequest) error { return errEmptyFile } - switch where { - case Tmp: - path = fmt.Sprintf("/tmp/tmpfile-%s-%s", zone.RawFileName, zone.User) - case Perm: - c, err = internal.ReadConfig() + c, err = internal.ReadConfig() - if err != nil { - logHandler(genericResponseFields{"error": err.Error(), "message": "no config file defined"}) + if err != nil { + logHandler(genericResponseFields{"error": err.Error(), "message": "no config file defined"}) - return fmt.Errorf("unable to parse directory: %w", err) - } + return fmt.Errorf("unable to parse directory: %w", err) + } - path = fmt.Sprintf("%s/%s/", c.ZoneDir, zone.RawFileName) + path = fmt.Sprintf("%s/%s/", c.ZoneDir, zone.RawFileName) - file = zone.RawFileName + file = zone.RawFileName - if err = os.MkdirAll(path, 0o750); err != nil { - logHandler(genericResponseFields{"error": err.Error(), "message": "unable to make directory for zone files"}) + 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) - } + return fmt.Errorf("unable to make zone directory: %w", err) } f, err := os.Create(filepath.Clean(path + file)) //nolint: gosec diff --git a/internal/api/zone.go b/internal/api/zone.go index 8e516db..d4e599a 100644 --- a/internal/api/zone.go +++ b/internal/api/zone.go @@ -5,8 +5,6 @@ import ( "io" "log" "net/http" - "os" - "path/filepath" "strings" "git.freecumextremist.com/grumbulon/pomme/internal" @@ -68,8 +66,8 @@ func ReceiveFile(w http.ResponseWriter, r *http.Request) { zoneFile := newDNSRequest(header.Filename, claims["username"].(string), b) - if err := zoneFile.save(Tmp); err != nil { - APIError(w, r, genericResponseFields{"message": "Unable to save zonefile", "status": http.StatusInternalServerError, "error": err.Error()}) + if err := zoneFile.parse(); err != nil { + APIError(w, r, genericResponseFields{"message": "Unable to parse zonefile", "status": http.StatusInternalServerError, "error": err.Error()}) return } @@ -90,12 +88,6 @@ func ReceiveFile(w http.ResponseWriter, r *http.Request) { }, }) - if err := zoneFile.parse(); err != nil { - APIError(w, r, genericResponseFields{"message": "Unable to parse zonefile", "status": http.StatusInternalServerError, "error": err.Error()}) - - return - } - if err := zoneFile.save(Perm); err != nil { APIError(w, r, genericResponseFields{"message": "Unable to save zonefile", "status": http.StatusInternalServerError, "error": err.Error()}) @@ -139,16 +131,8 @@ func (zone *ZoneRequest) parse() error { return nil } -func (zone *ZoneRequest) save(path) error { - // clean up the tmp file - defer func() (err error) { - if err = os.Remove(filepath.Clean("/tmp/" + zone.FileName)); err != nil { - return - } - return - }() - - return makeLocal(Perm, zone) +func (zone *ZoneRequest) save(p path) error { + return makeLocal(p, zone) } func validateContentType(file io.Reader) bool { From cf1071b3890852f2b49200df914d2794752ff394 Mon Sep 17 00:00:00 2001 From: grumbulon Date: Sat, 4 Feb 2023 23:02:21 -0500 Subject: [PATCH 11/14] remove rawFileName from Zone db schema as no longer necessary, removed not needed code, and documentation. Made tests work with new zone file route consolidation. --- internal/api/api_test.go | 34 ---------------------------------- internal/api/fs.go | 21 +++++---------------- internal/api/types.go | 35 ++++++++++++++++++++++------------- internal/api/zone.go | 17 +++++++---------- internal/types.go | 5 ++--- 5 files changed, 36 insertions(+), 76 deletions(-) diff --git a/internal/api/api_test.go b/internal/api/api_test.go index e894ea9..9500dc7 100644 --- a/internal/api/api_test.go +++ b/internal/api/api_test.go @@ -313,40 +313,6 @@ func (a *accountTest) TestUpload(t *testing.T) { assert.Equal(t, tc.expected.response, resp.StatusCode) } - - if tc.name == "Should upload a valid file" { - parse(t, f.Name(), a) - } }) } } - -func parse(t *testing.T, fname string, a *accountTest) { - var target response - - client := http.Client{} - - form := url.Values{} - - form.Add("filename", filepath.Clean(fname)) - - if req, err := http.NewRequest(http.MethodPost, a.url+`/api/parse`, strings.NewReader(form.Encode())); err == nil { - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - req.Header.Add("Authorization", `Bearer:`+a.token) - req.Header.Add("User-Agent", "pomme-api-test-slave") - - resp, err := client.Do(req) - if err != nil { - assert.NotNil(t, err) - } - - respBody, _ := io.ReadAll(resp.Body) - - err = json.Unmarshal(respBody, &target) - if err != nil { - assert.NotNil(t, err) - } - - assert.Equal(t, http.StatusCreated, resp.StatusCode) - } -} diff --git a/internal/api/fs.go b/internal/api/fs.go index be9a3ae..c0eaa23 100644 --- a/internal/api/fs.go +++ b/internal/api/fs.go @@ -12,15 +12,8 @@ import ( var errEmptyFile = errors.New("will not save empty file to FS") // makeLocal takes a type path and then saves a zone file to either tmp or a permanent location. -func makeLocal(where path, zone *ZoneRequest) error { - var ( - path string - file string - c *internal.Config - err error - ) - - if _, err = os.Stat(fmt.Sprintf(zone.FileName, zone.User)); !os.IsNotExist(err) { +func makeLocal(zone *ZoneRequest) error { + if _, err := os.Stat(fmt.Sprintf(zone.FileName, zone.User)); !os.IsNotExist(err) { return fmt.Errorf("file %s already exists: %w", zone.FileName, err) } @@ -28,25 +21,21 @@ func makeLocal(where path, zone *ZoneRequest) error { return errEmptyFile } - c, err = internal.ReadConfig() - + 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) } - path = fmt.Sprintf("%s/%s/", c.ZoneDir, zone.RawFileName) - - file = zone.RawFileName - + path := fmt.Sprintf("%s/%s/", c.ZoneDir, zone.FileName) 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) } - f, err := os.Create(filepath.Clean(path + file)) //nolint: gosec + f, err := os.Create(filepath.Clean(path + zone.FileName)) //nolint: gosec if err != nil { return fmt.Errorf("failed to write file locally: %w", err) } diff --git a/internal/api/types.go b/internal/api/types.go index 8e40922..4efa452 100644 --- a/internal/api/types.go +++ b/internal/api/types.go @@ -6,39 +6,48 @@ const ( keyPrincipalContextID key = iota ) -const ( - Tmp path = iota - Perm -) - type key int -type path int - -// ZoneRequest represents a Zone file request. +// ZoneRequest represents a new zone file request. +// +// Inside it is a pointer to the zone struct, which contains zone file information, and a User field to keep track of whom owns the file/request. type ZoneRequest struct { *Zone User string `json:"user,omitempty" gorm:"foreignKey:username;references:User"` } -// Zone struct represents a zonefile. +// Zone struct represents a zone file. type Zone struct { gorm.Model - FileName string `json:"name"` - RawFileName string `json:"rawname"` - Body string `json:"body,omitempty"` + + // FileName is the file name for an uploaded zone file how it is expected to show up on the filesystem + FileName string `json:"name"` + + // Body is the bytes array of a zone files body for copying and moving it around + Body []byte `json:"body,omitempty"` } +// GenericResponse is a generics wrapper to send responses for API Errors. type GenericResponse[T map[string]any] struct { Response map[string]any `json:"response,omitempty"` } +// instead of calling map[string]any{...} you can call genericResponseFields{...} when making a generic response above. type genericResponseFields map[string]any +// ndr is an interface for new DNS requests. It's methods can be used with a ZoneRequest object. type ndr interface { + // parse() is a wrapper around miekg's NewZoneParser, which is used to validate uploaded zone files + // + // if no error is raised the zone file can be saved. parse() error - save(where path) error + + // save() is a wrapper around internal.makeLocal() which will save a valid non-empty zone file to the filesystem + // + // the file is saved to a location the user sets up in their config.yaml file, + // once saved it can be consumed by something like NSD. + save() error } var _ ndr = (*ZoneRequest)(nil) diff --git a/internal/api/zone.go b/internal/api/zone.go index d4e599a..8283c65 100644 --- a/internal/api/zone.go +++ b/internal/api/zone.go @@ -83,12 +83,11 @@ func ReceiveFile(w http.ResponseWriter, r *http.Request) { &ZoneRequest{ User: claims["username"].(string), Zone: &Zone{ - FileName: fmt.Sprintf("tmpfile-%s-%s", header.Filename, claims["username"].(string)), - RawFileName: header.Filename, + FileName: header.Filename, }, }) - if err := zoneFile.save(Perm); err != nil { + if err := zoneFile.save(); err != nil { APIError(w, r, genericResponseFields{"message": "Unable to save zonefile", "status": http.StatusInternalServerError, "error": err.Error()}) return @@ -109,16 +108,14 @@ func newDNSRequest(filename string, user string, dat []byte) ndr { return &ZoneRequest{ User: user, Zone: &Zone{ - FileName: fmt.Sprintf("tmpfile-%s-%s", filename, user), - RawFileName: filename, - Body: string(dat), + FileName: filename, + Body: dat, }, } } -// Parse will be used to parse zonefiles. func (zone *ZoneRequest) parse() error { - zp := dns.NewZoneParser(strings.NewReader(zone.Body), "", "") + zp := dns.NewZoneParser(strings.NewReader(string(zone.Body)), "", "") for rr, ok := zp.Next(); ok; rr, ok = zp.Next() { log.Println(rr) @@ -131,8 +128,8 @@ func (zone *ZoneRequest) parse() error { return nil } -func (zone *ZoneRequest) save(p path) error { - return makeLocal(p, zone) +func (zone *ZoneRequest) save() error { + return makeLocal(zone) } func validateContentType(file io.Reader) bool { diff --git a/internal/types.go b/internal/types.go index 97e99c9..e54431c 100644 --- a/internal/types.go +++ b/internal/types.go @@ -24,9 +24,8 @@ type ZoneRequest struct { // Zone struct represents a zonefile in the database. type Zone struct { gorm.Model - FileName string `json:"name"` - RawFileName string `json:"rawfilename"` - Body string `json:"body"` + FileName string `json:"name"` + Body string `json:"body"` } type Config struct { From 25229f9c81ba67ae320aaff3ec1eb9adf8ac8466 Mon Sep 17 00:00:00 2001 From: grumbulon Date: Sat, 4 Feb 2023 23:17:08 -0500 Subject: [PATCH 12/14] removed unnecessary byte slice conversions --- internal/api/fs.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/api/fs.go b/internal/api/fs.go index c0eaa23..3385ab7 100644 --- a/internal/api/fs.go +++ b/internal/api/fs.go @@ -17,7 +17,7 @@ func makeLocal(zone *ZoneRequest) error { return fmt.Errorf("file %s already exists: %w", zone.FileName, err) } - if len([]byte(zone.Body)) == 0 { + if len(zone.Body) == 0 { return errEmptyFile } @@ -47,7 +47,7 @@ func makeLocal(zone *ZoneRequest) error { } }() - err = os.WriteFile(f.Name(), []byte(zone.Body), 0o600) + err = os.WriteFile(f.Name(), zone.Body, 0o600) if err != nil { return fmt.Errorf("failed to write file locally: %w", err) From b8d4af58c89e39ae83090afe427c5eff4f0bb3ed Mon Sep 17 00:00:00 2001 From: grumbulon Date: Sun, 5 Feb 2023 14:23:45 -0500 Subject: [PATCH 13/14] adding sys.go and using /x/sys/unix for killing pomme, because syscall package is deprecated. Added small wrapper for killPomme(). --- go.mod | 2 +- go.sum | 2 -- internal/api/helpers.go | 3 +-- internal/sys.go | 15 +++++++++++++++ 4 files changed, 17 insertions(+), 5 deletions(-) create mode 100644 internal/sys.go diff --git a/go.mod b/go.mod index 59a8587..125c3a7 100644 --- a/go.mod +++ b/go.mod @@ -53,7 +53,7 @@ require ( github.com/swaggo/http-swagger v1.3.3 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect golang.org/x/net v0.5.0 // indirect - golang.org/x/sys v0.4.0 // indirect + golang.org/x/sys v0.4.0 golang.org/x/tools v0.1.12 // indirect gopkg.in/yaml.v3 v3.0.1 modernc.org/libc v1.21.5 // indirect diff --git a/go.sum b/go.sum index b13400f..118962c 100644 --- a/go.sum +++ b/go.sum @@ -185,8 +185,6 @@ gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/gorm v1.24.2/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= -gorm.io/gorm v1.24.3 h1:WL2ifUmzR/SLp85CSURAfybcHnGZ+yLSGSxgYXlFBHg= -gorm.io/gorm v1.24.3/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= gorm.io/gorm v1.24.5 h1:g6OPREKqqlWq4kh/3MCQbZKImeB9e6Xgc4zD+JgNZGE= gorm.io/gorm v1.24.5/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= diff --git a/internal/api/helpers.go b/internal/api/helpers.go index 12e20f2..1918f47 100644 --- a/internal/api/helpers.go +++ b/internal/api/helpers.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "net/http" - "syscall" "time" "git.freecumextremist.com/grumbulon/pomme/internal" @@ -35,7 +34,7 @@ func setDBMiddleware(next http.Handler) http.Handler { if err != nil && !ok { logHandler(genericResponseFields{"error": err.Error(), "message": "Error initializing DB"}) - err = syscall.Kill(syscall.Getpid(), syscall.SIGINT) + err = internal.SysKill() if err != nil { panic("idk what to do with this but syscall.Kill errored out.") } diff --git a/internal/sys.go b/internal/sys.go new file mode 100644 index 0000000..943b7d0 --- /dev/null +++ b/internal/sys.go @@ -0,0 +1,15 @@ +package internal + +import "golang.org/x/sys/unix" + +func SysKill() (err error) { + err = killPomme() + + return +} + +func killPomme() (err error) { + err = unix.Kill(unix.Getpid(), unix.SIGINT) + + return +} From 12b219bf45bf65aed5b575721ae3369409b1d2f0 Mon Sep 17 00:00:00 2001 From: Sam Therapy Date: Mon, 6 Feb 2023 15:34:17 +0100 Subject: [PATCH 14/14] Small nitpicks gitignore: add default zones directory upload: fix typo in swagger docs config: simplify default save location Signed-off-by: Sam Therapy --- .gitignore | 4 +++- config.sample.yaml | 2 +- docs/docs.go | 5 ++--- docs/swagger.json | 2 +- docs/swagger.yaml | 2 +- internal/api/zone.go | 2 +- 6 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 6c5e775..c2f184e 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,6 @@ test.db test.sqlite pomme.sqlite .dccache -config.yaml \ No newline at end of file +config.yaml + +zones/* \ No newline at end of file diff --git a/config.sample.yaml b/config.sample.yaml index 2875fb6..15c7b66 100644 --- a/config.sample.yaml +++ b/config.sample.yaml @@ -3,4 +3,4 @@ hashingsecret: PasswordHashingSecret port: 3008 # port the server runs on db: pomme.sqlite testdb: test.sqlite -zonedir: /home/user/pomme/nds/pomme/zones \ No newline at end of file +zonedir: zones \ No newline at end of file diff --git a/docs/docs.go b/docs/docs.go index aeb6391..22f2570 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1,5 +1,4 @@ -// Package docs GENERATED BY SWAG; DO NOT EDIT -// This file was generated by swaggo/swag +// Code generated by swaggo/swag. DO NOT EDIT package docs import "github.com/swaggo/swag" @@ -69,7 +68,7 @@ const docTemplate = `{ "Bearer": [] } ], - "description": "Upload takes a multipart form file as user input. It must not exceede 1 mb and must be of text/plain content type.\nIf a file uploads successfully it will be saved locally and parsed.\nIf parsing is successful pomme will save the file to your ZoneDir defined in your config.\nUploads are associated with a specific user.\nRate limited: 10 requests every 10 second\nyou must specify \"Bearer\" before entering your token", + "description": "Upload takes a multipart form file as user input. It must not exceed 1 mb and must be of text/plain content type.\nIf a file uploads successfully it will be saved locally and parsed.\nIf parsing is successful pomme will save the file to your ZoneDir defined in your config.\nUploads are associated with a specific user.\nRate limited: 10 requests every 10 second\nyou must specify \"Bearer\" before entering your token", "consumes": [ "multipart/form-data" ], diff --git a/docs/swagger.json b/docs/swagger.json index fc04d4b..3a83f84 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -60,7 +60,7 @@ "Bearer": [] } ], - "description": "Upload takes a multipart form file as user input. It must not exceede 1 mb and must be of text/plain content type.\nIf a file uploads successfully it will be saved locally and parsed.\nIf parsing is successful pomme will save the file to your ZoneDir defined in your config.\nUploads are associated with a specific user.\nRate limited: 10 requests every 10 second\nyou must specify \"Bearer\" before entering your token", + "description": "Upload takes a multipart form file as user input. It must not exceed 1 mb and must be of text/plain content type.\nIf a file uploads successfully it will be saved locally and parsed.\nIf parsing is successful pomme will save the file to your ZoneDir defined in your config.\nUploads are associated with a specific user.\nRate limited: 10 requests every 10 second\nyou must specify \"Bearer\" before entering your token", "consumes": [ "multipart/form-data" ], diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 7a4e7b2..3e804b4 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -55,7 +55,7 @@ paths: consumes: - multipart/form-data description: |- - Upload takes a multipart form file as user input. It must not exceede 1 mb and must be of text/plain content type. + Upload takes a multipart form file as user input. It must not exceed 1 mb and must be of text/plain content type. If a file uploads successfully it will be saved locally and parsed. If parsing is successful pomme will save the file to your ZoneDir defined in your config. Uploads are associated with a specific user. diff --git a/internal/api/zone.go b/internal/api/zone.go index 8283c65..99b3e2b 100644 --- a/internal/api/zone.go +++ b/internal/api/zone.go @@ -17,7 +17,7 @@ import ( // Upload godoc // // @Summary upload a zonefile -// @Description Upload takes a multipart form file as user input. It must not exceede 1 mb and must be of text/plain content type. +// @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.