mirror of
https://git.freecumextremist.com/grumbulon/pomme.git
synced 2024-12-23 12:10:44 +00:00
342 lines
9.3 KiB
Go
342 lines
9.3 KiB
Go
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
|
|
}
|
|
}
|