pomme/internal/api/zone.go
2023-01-30 23:23:35 -05:00

245 lines
5.3 KiB
Go

package api
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"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"
)
// ZoneRequest represents a Zone file request.
type ZoneRequest struct {
*Zone
User string `json:"user,omitempty" gorm:"foreignKey:username;references:User"`
}
// Zone struct represents a zonefile.
type Zone struct {
gorm.Model
FileName string `json:"name"`
RawFileName string `json:"rawname"`
Body string `json:"body,omitempty"`
}
// 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} httpSuccess
// @Failure 500 {object} httpInternalServerError
// @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())
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")
if err != nil {
internalServerError(w, r, fmt.Sprintf("file upload failed: %v", err))
return
}
defer file.Close() //nolint: errcheck
ok := validateContentType(file)
if !ok {
http.Error(w, "file must be text/plain", http.StatusUnsupportedMediaType)
return
}
name := strings.Split(header.Filename, ".")
if _, err = io.Copy(&buf, file); err != nil {
internalServerError(w, r, "internal server error")
return
}
if err = util.MakeLocal(name[0], claims["username"].(string), buf); err != nil {
internalServerError(w, r, err.Error())
return
}
db, ok := r.Context().Value(keyPrincipalContextID).(*gorm.DB)
if !ok {
internalServerError(w, r, "internal server error")
return
}
db.Create(
&ZoneRequest{
User: claims["username"].(string),
Zone: &Zone{
FileName: fmt.Sprintf("tmpfile-%s-%s", name[0], claims["username"].(string)),
RawFileName: name[0],
},
})
buf.Reset()
w.WriteHeader(http.StatusCreated)
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(
internal.Response{
HTTPResponse: http.StatusCreated,
Message: "Successfully uploaded zonefile",
})
if err != nil {
internalServerError(w, r, "internal server error")
return
}
}
// 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} httpSuccess
// @Failure 500 {object} httpInternalServerError
// @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 {
internalServerError(w, r, "unable to parse request")
return
}
filename := r.Form.Get("filename")
if filename == "" {
internalServerError(w, r, "no filename parsed")
return
}
db, ok := r.Context().Value(keyPrincipalContextID).(*gorm.DB)
if !ok {
internalServerError(w, r, "internal server error")
return
}
db.Where(ZoneRequest{
Zone: &Zone{
RawFileName: filename,
},
User: claims["username"].(string),
}).First(&result)
if result == (internal.ZoneRequest{}) {
internalServerError(w, r, "internal server error")
return
}
zoneFile := newZoneRequest(result.RawFileName, claims["username"].(string))
if err := zoneFile.Parse(); err != nil {
internalServerError(w, r, fmt.Sprintf("unable to parse zonefile: %v", err))
return
}
resp := internal.Response{
Message: "Successfully parsed zonefile",
HTTPResponse: http.StatusOK,
}
render.JSON(w, r, resp)
}
func newZoneRequest(filename string, user string) *ZoneRequest {
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 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
}
}