pomme/internal/api/zone.go
2023-05-28 01:23:18 -04:00

223 lines
5.2 KiB
Go

package api
import (
"errors"
"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
logger, ok := r.Context().Value(keyLoggerContextID).(*Responder)
if !ok {
return
}
_, 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.Response = Response{
Message: "File upload failed",
Status: http.StatusInternalServerError,
Err: err,
}
logger.newLogEntry().errorLogger(logger.Response)
logger.newLogEntry().apiError(logger.Response, w, r)
return
}
defer file.Close() //nolint: errcheck
b, err := io.ReadAll(file)
if err != nil {
logger.Response = Response{
Message: "internal server error",
Status: http.StatusInternalServerError,
Err: err,
}
logger.newLogEntry().errorLogger(logger.Response)
logger.newLogEntry().apiError(logger.Response, w, r)
return
}
ok = validateContentType(file)
if !ok {
logger.Response = Response{
Message: "file type must be text/plain",
Status: http.StatusUnsupportedMediaType,
}
logger.newLogEntry().infoLogger(logger.Response)
logger.newLogEntry().apiError(logger.Response, w, r)
return
}
user, ok := claims["username"].(string)
if !ok {
logger.Response = Response{
Message: "Expected username to be a string",
Status: http.StatusInternalServerError,
Err: errors.New("unable to assert string type to claims interface{}"),
}
logger.newLogEntry().errorLogger(logger.Response)
}
zoneFile := newDNSRequest(header.Filename, user, b)
if err := zoneFile.parse(); err != nil {
logger.Response = Response{
Message: "Unable to parse zonefile",
Status: http.StatusInternalServerError,
Err: err,
}
logger.newLogEntry().errorLogger(logger.Response)
logger.newLogEntry().apiError(logger.Response, w, r)
return
}
db, ok := r.Context().Value(keyPrincipalContextID).(*gorm.DB)
if !ok {
logger.Response = Response{
Message: "Unable to parse zonefile",
Status: http.StatusInternalServerError,
Err: errors.New("unable to connect to DB"),
}
logger.newLogEntry().errorLogger(logger.Response)
logger.newLogEntry().apiError(logger.Response, w, r)
return
}
// check if request is coming from user not in the DB but has a valid JWT
db.Where("username = ?", user).First(&result)
if result.Username == "" {
logger.Response = Response{
Message: "Internal server error",
Status: http.StatusInternalServerError,
Err: errors.New("user does not exist"),
}
logger.newLogEntry().errorLogger(logger.Response)
logger.newLogEntry().apiError(logger.Response, w, r)
return
}
db.Create(
&ZoneRequest{
User: user,
Zone: &Zone{
FileName: header.Filename,
},
})
if err := zoneFile.save(); err != nil {
logger.Response = Response{
Message: "Unable to save zonefile",
Status: http.StatusInternalServerError,
Err: err,
}
logger.newLogEntry().errorLogger(logger.Response)
logger.newLogEntry().apiError(logger.Response, w, r)
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
}
}