Update routes to return structured JSON

This commit is contained in:
vysion 2022-03-30 12:15:45 -07:00
parent e539043714
commit 418c21139a
6 changed files with 252 additions and 114 deletions

View File

@ -2,7 +2,7 @@ package routes
import (
"github.com/valyala/fasthttp"
"github.com/vysiondev/tytanium/security"
"tytanium/security"
)
// ServeAuthCheck validates the master key by calling IsAuthorized.

View File

@ -4,9 +4,9 @@ import (
"bytes"
_ "embed"
"github.com/valyala/fasthttp"
"github.com/vysiondev/tytanium/global"
"github.com/vysiondev/tytanium/logger"
"io"
"tytanium/global"
"tytanium/logger"
)
//go:embed favicon.ico

View File

@ -3,24 +3,24 @@ package routes
import (
"fmt"
"github.com/gabriel-vasile/mimetype"
"github.com/minio/sio"
"github.com/valyala/fasthttp"
"github.com/vysiondev/tytanium/constants"
"github.com/vysiondev/tytanium/global"
"github.com/vysiondev/tytanium/response"
"github.com/vysiondev/tytanium/security"
"github.com/vysiondev/tytanium/utils"
"io"
"os"
"path"
"regexp"
"strconv"
"strings"
"tytanium/constants"
"tytanium/encryption"
"tytanium/global"
"tytanium/response"
"tytanium/security"
"tytanium/utils"
)
const (
rawParam = "raw"
ZeroWidthCharacterFirstByte = 243
)
const encryptionKeyParam = "enc_key"
const rawParam = "raw"
// discordHTML represents what is sent back to any client which User-Agent contains the regex contained in
// discordBotRegex.
@ -46,24 +46,33 @@ func ServeFile(ctx *fasthttp.RequestCtx) {
pBytes := ctx.Request.URI().Path()
if len(pBytes) > constants.PathLengthLimitBytes {
response.SendTextResponse(ctx, "Path is too long.", fasthttp.StatusBadRequest)
response.SendJSONResponse(ctx, response.JSONResponse{
Status: response.RequestStatusError,
Data: nil,
Message: "Path is too long.",
}, fasthttp.StatusOK)
return
}
if len(pBytes) <= 1 {
response.SendTextResponse(ctx, "Path is too short.", fasthttp.StatusBadRequest)
response.SendJSONResponse(ctx, response.JSONResponse{
Status: response.RequestStatusError,
Data: nil,
Message: "Path is too short.",
}, fasthttp.StatusOK)
return
}
if len(ctx.QueryArgs().Peek(encryptionKeyParam)) == 0 {
response.SendJSONResponse(ctx, response.JSONResponse{
Status: response.RequestStatusError,
Data: nil,
Message: "No encryption key was provided. (enc_key)",
}, fasthttp.StatusOK)
return
}
p := string(pBytes[1:])
// Convert entire path to normal string if a zero-width character is detected at the beginning.
if pBytes[1] == ZeroWidthCharacterFirstByte {
p = utils.StringToZeroWidthCharacters(p)
if len(p) == 0 {
response.SendTextResponse(ctx, "Malformed zero-width URL path.", fasthttp.StatusBadRequest)
return
}
}
filePath := path.Join(global.Configuration.Storage.Directory, p)
@ -74,43 +83,92 @@ func ServeFile(ctx *fasthttp.RequestCtx) {
ServeNotFound(ctx)
return
}
response.SendTextResponse(ctx, fmt.Sprintf("os.Stat() could not be called on the file. %v", err), fasthttp.StatusInternalServerError)
response.SendJSONResponse(ctx, response.JSONResponse{
Status: response.RequestStatusInternalError,
Data: nil,
Message: fmt.Sprintf("os.Stat() could not be called on the file. %v", err),
}, fasthttp.StatusOK)
return
}
if fileInfo.IsDir() {
response.SendTextResponse(ctx, "This is a directory, not a file", fasthttp.StatusBadRequest)
ServeNotFound(ctx)
return
}
if global.Configuration.RateLimit.Bandwidth.Download > 0 && global.Configuration.RateLimit.Bandwidth.ResetAfter > 0 {
isBandwidthLimitNotReached, err := security.Try(ctx, global.RedisClient, fmt.Sprintf("%s_%s", constants.RateLimitBandwidthDownload, utils.GetIP(ctx)), int64(global.Configuration.RateLimit.Bandwidth.Download), int64(global.Configuration.RateLimit.Bandwidth.ResetAfter), fileInfo.Size())
if err != nil {
response.SendJSONResponse(ctx, response.JSONResponse{
Status: response.RequestStatusInternalError,
Data: nil,
Message: fmt.Sprintf("Bandwidth limit couldn't be checked. %v", err),
}, fasthttp.StatusOK)
return
}
if !isBandwidthLimitNotReached {
response.SendJSONResponse(ctx, response.JSONResponse{
Status: response.RequestStatusError,
Data: nil,
Message: "Download bandwidth limit reached; try again later.",
}, fasthttp.StatusTooManyRequests)
return
}
}
// We don't need a limited reader because mimetype.DetectReader automatically caps it
fileReader, e := os.OpenFile(filePath, os.O_RDONLY, 0644)
if e != nil {
response.SendTextResponse(ctx, fmt.Sprintf("The file could not be opened. %v", err), fasthttp.StatusInternalServerError)
response.SendJSONResponse(ctx, response.JSONResponse{
Status: response.RequestStatusInternalError,
Data: nil,
Message: fmt.Sprintf("The file could not be opened. %v", err),
}, fasthttp.StatusOK)
return
}
defer func() {
_ = fileReader.Close()
}()
if global.Configuration.RateLimit.Bandwidth.Download > 0 && global.Configuration.RateLimit.Bandwidth.ResetAfter > 0 {
isBandwidthLimitNotReached, err := security.Try(ctx, global.RedisClient, fmt.Sprintf("%s_%s", constants.RateLimitBandwidthDownload, utils.GetIP(ctx)), int64(global.Configuration.RateLimit.Bandwidth.Download), int64(global.Configuration.RateLimit.Bandwidth.ResetAfter), fileInfo.Size())
if err != nil {
response.SendTextResponse(ctx, fmt.Sprintf("Bandwidth limit couldn't be checked. %v", err), fasthttp.StatusInternalServerError)
return
}
if !isBandwidthLimitNotReached {
response.SendTextResponse(ctx, "Download bandwidth limit reached; try again later.", fasthttp.StatusTooManyRequests)
return
}
}
mimeType, e := mimetype.DetectReader(fileReader)
if e != nil {
response.SendTextResponse(ctx, fmt.Sprintf("Cannot detect the mime type of this file retrieved from server. It might be corrupted. %v", e), fasthttp.StatusBadRequest)
key, err := encryption.DeriveKey(ctx.QueryArgs().Peek(encryptionKeyParam), []byte(global.Configuration.Encryption.Nonce))
if err != nil {
response.SendJSONResponse(ctx, response.JSONResponse{
Status: response.RequestStatusInternalError,
Data: nil,
Message: fmt.Sprintf("Failed to generate encryption key. %v", err),
}, fasthttp.StatusOK)
return
}
decryptedReader, err := sio.DecryptReader(fileReader, sio.Config{Key: key[:]})
if err != nil {
response.SendJSONResponse(ctx, response.JSONResponse{
Status: response.RequestStatusInternalError,
Data: nil,
Message: fmt.Sprintf("Failed to create a decrypted reader for mime type inspection. %v", e),
}, fasthttp.StatusOK)
return
}
mimeType, e := mimetype.DetectReader(decryptedReader)
if e != nil {
response.SendInvalidEncryptionKeyResponse(ctx)
return
}
filterStatus := security.FilterCheck(ctx, mimeType.String())
if filterStatus == security.FilterFail {
// already sent a response if filter check failed
return
} else if filterStatus == security.FilterSanitize {
ctx.Response.Header.Set("Content-Type", "text/plain; charset=utf8")
} else {
ctx.Response.Header.Set("Content-Type", mimeType.String())
}
ctx.Response.Header.Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", p))
ctx.Response.Header.Set("Content-Length", strconv.FormatInt(fileInfo.Size(), 10))
if discordBotRegex.Match(ctx.Request.Header.UserAgent()) && !ctx.QueryArgs().Has(rawParam) {
if mimetype.EqualsAny(mimeType.String(), "image/png", "image/jpeg", "image/gif") {
ctx.Response.Header.SetContentType("text/html; charset=utf8")
@ -118,33 +176,31 @@ func ServeFile(ctx *fasthttp.RequestCtx) {
ctx.Response.Header.Add("Pragma", "no-cache")
ctx.Response.Header.Add("Expires", "0")
u := fmt.Sprintf("%s/%s?%s=true", utils.GetServerRoot(ctx), p, rawParam)
u := fmt.Sprintf("%s/%s?%s=true&enc_key=%s", global.Configuration.Domain, p, rawParam, string(ctx.QueryArgs().Peek(encryptionKeyParam)))
_, _ = fmt.Fprint(ctx.Response.BodyWriter(), strings.Replace(discordHTML, "{{.}}", u, 1))
return
}
}
filterStatus := security.FilterCheck(ctx, mimeType.String())
if filterStatus == security.FilterFail {
// already sent a response if filter check failed
return
} else if filterStatus == security.FilterSanitize {
ctx.Response.Header.Set("Content-Type", "text/plain")
} else {
ctx.Response.Header.Set("Content-Type", mimeType.String())
}
ctx.Response.Header.Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", p))
ctx.Response.Header.Set("Content-Length", strconv.FormatInt(fileInfo.Size(), 10))
_, e = fileReader.Seek(0, io.SeekStart)
if e != nil {
response.SendTextResponse(ctx, fmt.Sprintf("Reader could not be reset to its initial position. %v", e), fasthttp.StatusInternalServerError)
_, err = fileReader.Seek(0, io.SeekStart)
if err != nil {
response.SendJSONResponse(ctx, response.JSONResponse{
Status: response.RequestStatusInternalError,
Data: nil,
Message: fmt.Sprintf("Failed to reset file reader to 0. %v", err),
}, fasthttp.StatusOK)
return
}
_, copyErr := io.Copy(ctx.Response.BodyWriter(), fileReader)
if copyErr != nil {
response.SendTextResponse(ctx, fmt.Sprintf("File wasn't written to the client successfully. %v", copyErr), fasthttp.StatusInternalServerError)
return
if _, err = sio.Decrypt(ctx.Response.BodyWriter(), fileReader, sio.Config{Key: key[:]}); err != nil {
if _, ok := err.(sio.Error); ok {
response.SendInvalidEncryptionKeyResponse(ctx)
return
}
response.SendJSONResponse(ctx, response.JSONResponse{
Status: response.RequestStatusInternalError,
Data: nil,
Message: fmt.Sprintf("Failed to write decrypted file to the response body. %v", err),
}, fasthttp.StatusOK)
}
}

View File

@ -2,7 +2,7 @@ package routes
import (
"github.com/valyala/fasthttp"
"github.com/vysiondev/tytanium/response"
"tytanium/response"
)
// ServeNotFound will always return an HTTP status code of 404 + error message text.

View File

@ -4,18 +4,17 @@ import (
"fmt"
"github.com/go-redis/redis/v8"
"github.com/valyala/fasthttp"
"github.com/vysiondev/tytanium/constants"
"github.com/vysiondev/tytanium/global"
"github.com/vysiondev/tytanium/response"
"runtime"
"strconv"
"tytanium/constants"
"tytanium/global"
"tytanium/response"
)
// GeneralStats represent all stats returned when making a GET request to /stats.
type GeneralStats struct {
ServerVersion string `json:"server_version"`
RuntimeVersion string `json:"runtime_version,omitempty"`
MemoryUsage int64 `json:"memory_usage,omitempty"`
SizeStats StatsFromSizeChecker `json:"size_stats"`
}
@ -34,43 +33,57 @@ func ServeStats(ctx *fasthttp.RequestCtx) {
totalSize, err := getStatValueFromRedis(ctx, global.RedisClient, "sc_total_size")
if err != nil {
response.SendTextResponse(ctx, fmt.Sprintf("An error occurred while trying to get sc_total_size from Redis: %v", err), fasthttp.StatusInternalServerError)
response.SendJSONResponse(ctx, response.JSONResponse{
Status: response.RequestStatusInternalError,
Data: nil,
Message: fmt.Sprintf("An error occurred while trying to get sc_total_size from Redis: %v", err),
}, fasthttp.StatusOK)
return
}
stats.SizeStats.TotalSize = totalSize
fileCount, err := getStatValueFromRedis(ctx, global.RedisClient, "sc_file_count")
if err != nil {
response.SendTextResponse(ctx, fmt.Sprintf("An error occurred while trying to get sc_file_count from Redis: %v", err), fasthttp.StatusInternalServerError)
response.SendJSONResponse(ctx, response.JSONResponse{
Status: response.RequestStatusInternalError,
Data: nil,
Message: fmt.Sprintf("An error occurred while trying to get sc_file_count from Redis: %v", err),
}, fasthttp.StatusOK)
return
}
stats.SizeStats.FileCount = fileCount
timeToComplete, err := getStatValueFromRedis(ctx, global.RedisClient, "sc_time_to_complete")
if err != nil {
response.SendTextResponse(ctx, fmt.Sprintf("An error occurred while trying to get sc_time_to_complete from Redis: %v", err), fasthttp.StatusInternalServerError)
response.SendJSONResponse(ctx, response.JSONResponse{
Status: response.RequestStatusInternalError,
Data: nil,
Message: fmt.Sprintf("An error occurred while trying to get sc_time_to_complete from Redis: %v", err),
}, fasthttp.StatusOK)
return
}
stats.SizeStats.TimeToComplete = timeToComplete
lastUpdated, err := getStatValueFromRedis(ctx, global.RedisClient, "sc_last_updated")
if err != nil {
response.SendTextResponse(ctx, fmt.Sprintf("An error occurred while trying to get sc_last_updated from Redis: %v", err), fasthttp.StatusInternalServerError)
response.SendJSONResponse(ctx, response.JSONResponse{
Status: response.RequestStatusInternalError,
Data: nil,
Message: fmt.Sprintf("An error occurred while trying to get sc_last_updated from Redis: %v", err),
}, fasthttp.StatusOK)
return
}
stats.SizeStats.LastUpdated = lastUpdated
if global.Configuration.MoreStats {
memUsage, err := getStatValueFromRedis(ctx, global.RedisClient, "ty_mem_usage")
if err != nil {
response.SendTextResponse(ctx, fmt.Sprintf("An error occurred while trying to get ty_mem_usage from Redis: %v", err), fasthttp.StatusInternalServerError)
return
}
stats.MemoryUsage = memUsage
stats.RuntimeVersion = runtime.Version()
}
response.SendJSONResponse(ctx, &stats)
response.SendJSONResponse(ctx, response.JSONResponse{
Status: response.RequestStatusOK,
Data: &stats,
Message: "",
}, fasthttp.StatusOK)
}
func getStatValueFromRedis(ctx *fasthttp.RequestCtx, c *redis.Client, key string) (int64, error) {

View File

@ -3,21 +3,27 @@ package routes
import (
"fmt"
"github.com/gabriel-vasile/mimetype"
"github.com/minio/sio"
"github.com/valyala/fasthttp"
"github.com/vysiondev/tytanium/constants"
"github.com/vysiondev/tytanium/global"
"github.com/vysiondev/tytanium/logger"
"github.com/vysiondev/tytanium/response"
"github.com/vysiondev/tytanium/security"
"github.com/vysiondev/tytanium/utils"
"io"
"os"
"path"
"sync"
"tytanium/constants"
"tytanium/encryption"
"tytanium/global"
"tytanium/logger"
"tytanium/response"
"tytanium/security"
"tytanium/utils"
)
const fileHandler = "file"
type uploadResponse struct {
URI string `json:"uri"`
EncryptionKey string `json:"encryption_key"`
}
// ServeUpload handles all incoming POST requests to /upload. It will take a multipart form, parse the file,
// then write it to disk.
func ServeUpload(ctx *fasthttp.RequestCtx) {
@ -28,15 +34,27 @@ func ServeUpload(ctx *fasthttp.RequestCtx) {
mp, e := ctx.Request.MultipartForm()
if e != nil {
if e == fasthttp.ErrNoMultipartForm {
response.SendTextResponse(ctx, "No multipart form was in the request.", fasthttp.StatusBadRequest)
response.SendJSONResponse(ctx, response.JSONResponse{
Status: response.RequestStatusError,
Data: nil,
Message: "No multipart form was present in the request.",
}, fasthttp.StatusOK)
return
}
response.SendTextResponse(ctx, fmt.Sprintf("The multipart form couldn't be parsed. %v", e), fasthttp.StatusBadRequest)
response.SendJSONResponse(ctx, response.JSONResponse{
Status: response.RequestStatusError,
Data: nil,
Message: fmt.Sprintf("The multipart form couldn't be parsed. %v", e),
}, fasthttp.StatusOK)
return
}
defer ctx.Request.RemoveMultipartFormFiles()
if mp.File == nil || len(mp.File[fileHandler]) == 0 {
response.SendTextResponse(ctx, "No files were sent.", fasthttp.StatusBadRequest)
response.SendJSONResponse(ctx, response.JSONResponse{
Status: response.RequestStatusError,
Data: nil,
Message: "No files were sent.",
}, fasthttp.StatusOK)
return
}
f := mp.File[fileHandler][0]
@ -44,11 +62,19 @@ func ServeUpload(ctx *fasthttp.RequestCtx) {
if global.Configuration.RateLimit.Bandwidth.Upload > 0 && global.Configuration.RateLimit.Bandwidth.ResetAfter > 0 {
isUploadBandwidthLimitNotReached, err := security.Try(ctx, global.RedisClient, fmt.Sprintf("%s_%s", constants.RateLimitBandwidthUpload, utils.GetIP(ctx)), int64(global.Configuration.RateLimit.Bandwidth.Upload), int64(global.Configuration.RateLimit.Bandwidth.ResetAfter), f.Size)
if err != nil {
response.SendTextResponse(ctx, fmt.Sprintf("Bandwidth limit couldn't be checked. %v", err), fasthttp.StatusInternalServerError)
response.SendJSONResponse(ctx, response.JSONResponse{
Status: response.RequestStatusInternalError,
Data: nil,
Message: fmt.Sprintf("Bandwidth limit could not be checked. %v", err),
}, fasthttp.StatusOK)
return
}
if !isUploadBandwidthLimitNotReached {
response.SendTextResponse(ctx, "Upload bandwidth limit reached; try again later.", fasthttp.StatusTooManyRequests)
response.SendJSONResponse(ctx, response.JSONResponse{
Status: response.RequestStatusError,
Data: nil,
Message: "Upload bandwidth limit reached; try again later.",
}, fasthttp.StatusTooManyRequests)
return
}
}
@ -56,13 +82,21 @@ func ServeUpload(ctx *fasthttp.RequestCtx) {
ext := path.Ext(f.Filename)
if len(ext) > constants.ExtensionLengthLimit {
response.SendTextResponse(ctx, "The file extension is too long.", fasthttp.StatusBadRequest)
response.SendJSONResponse(ctx, response.JSONResponse{
Status: response.RequestStatusError,
Data: nil,
Message: "File extension is too long.",
}, fasthttp.StatusOK)
return
}
openedFile, e := f.Open()
if e != nil {
response.SendTextResponse(ctx, fmt.Sprintf("File failed to open. %v", e), fasthttp.StatusInternalServerError)
response.SendJSONResponse(ctx, response.JSONResponse{
Status: response.RequestStatusInternalError,
Data: nil,
Message: fmt.Sprintf("File could not be opened. %v", e),
}, fasthttp.StatusOK)
return
}
defer func() {
@ -71,7 +105,11 @@ func ServeUpload(ctx *fasthttp.RequestCtx) {
mimeType, e := mimetype.DetectReader(openedFile)
if e != nil {
response.SendTextResponse(ctx, "Cannot detect the mime type of this file.", fasthttp.StatusBadRequest)
response.SendJSONResponse(ctx, response.JSONResponse{
Status: response.RequestStatusInternalError,
Data: nil,
Message: "Cannot detect the mime type of this file.",
}, fasthttp.StatusOK)
return
}
@ -83,7 +121,11 @@ func ServeUpload(ctx *fasthttp.RequestCtx) {
_, e = openedFile.Seek(0, io.SeekStart)
if e != nil {
response.SendTextResponse(ctx, fmt.Sprintf("Reader could not be reset to its initial position. %v", e), fasthttp.StatusInternalServerError)
response.SendJSONResponse(ctx, response.JSONResponse{
Status: response.RequestStatusInternalError,
Data: nil,
Message: fmt.Sprintf("Reader could not be reset to its initial position. %v", e),
}, fasthttp.StatusOK)
return
}
@ -92,15 +134,8 @@ func ServeUpload(ctx *fasthttp.RequestCtx) {
// loop until an unoccupied id is found
for {
var wg sync.WaitGroup
randomStringChan := make(chan string, 1)
go func() {
wg.Add(1)
utils.RandBytes(global.Configuration.Storage.IDLength, randomStringChan, func() { wg.Done() })
}()
wg.Wait()
fileId := <-randomStringChan
fileId := utils.RandString(global.Configuration.Storage.IDLength)
fileName = fileId + ext
i, e := os.Stat(path.Join(global.Configuration.Storage.Directory, fileName))
@ -114,28 +149,59 @@ func ServeUpload(ctx *fasthttp.RequestCtx) {
}
attempts++
if attempts >= global.Configuration.Storage.CollisionCheckAttempts {
response.SendTextResponse(ctx, "Tried too many times to find a valid file ID to use. Consider increasing the ID length.", fasthttp.StatusInternalServerError)
response.SendJSONResponse(ctx, response.JSONResponse{
Status: response.RequestStatusError,
Data: nil,
Message: "Tried too many times to find a valid file ID to use. Consider increasing the ID length.",
}, fasthttp.StatusOK)
return
}
}
fsFile, err := os.Create(path.Join(global.Configuration.Storage.Directory, fileName))
destFile, err := os.Create(path.Join(global.Configuration.Storage.Directory, fileName))
defer func() {
_ = fsFile.Close()
_ = destFile.Close()
}()
if err != nil {
if err == os.ErrPermission {
response.SendTextResponse(ctx, fmt.Sprintf("Permission to create a file was denied. %v", err), fasthttp.StatusInternalServerError)
response.SendJSONResponse(ctx, response.JSONResponse{
Status: response.RequestStatusInternalError,
Data: nil,
Message: fmt.Sprintf("Permission to create the file was denied. %v", err),
}, fasthttp.StatusOK)
return
}
response.SendTextResponse(ctx, fmt.Sprintf("Could not create the file. %v", err), fasthttp.StatusInternalServerError)
response.SendJSONResponse(ctx, response.JSONResponse{
Status: response.RequestStatusInternalError,
Data: nil,
Message: fmt.Sprintf("Failed to create the file. %v", err),
}, fasthttp.StatusOK)
return
}
_, writeErr := io.Copy(fsFile, openedFile)
if writeErr != nil {
response.SendTextResponse(ctx, fmt.Sprintf("The file failed to write to disk. %v", e), fasthttp.StatusInternalServerError)
masterKey := utils.RandString(global.Configuration.Encryption.EncryptionKeyLength)
key, err := encryption.DeriveKey([]byte(masterKey), []byte(global.Configuration.Encryption.Nonce))
if err != nil {
response.SendJSONResponse(ctx, response.JSONResponse{
Status: response.RequestStatusInternalError,
Data: nil,
Message: fmt.Sprintf("Failed to generate encryption key. %v", err),
}, fasthttp.StatusOK)
return
}
if _, err = sio.Encrypt(destFile, openedFile, sio.Config{Key: key[:]}); err != nil {
if _, ok := err.(sio.Error); ok {
response.SendInvalidEncryptionKeyResponse(ctx)
return
}
response.SendJSONResponse(ctx, response.JSONResponse{
Status: response.RequestStatusInternalError,
Data: nil,
Message: fmt.Sprintf("Failed to write encrypted file to disk. %v", err),
}, fasthttp.StatusOK)
return
}
@ -143,16 +209,19 @@ func ServeUpload(ctx *fasthttp.RequestCtx) {
logger.InfoLogger.Printf("File %s was created, size: %d", fileName, f.Size)
}
if global.Configuration.ForceZeroWidth || string(ctx.QueryArgs().Peek("zerowidth")) == "1" {
fileName = utils.ZeroWidthCharactersToString(fileName)
}
var u string
if string(ctx.QueryArgs().Peek("omitdomain")) == "1" {
u = fileName
} else {
u = fmt.Sprintf("%s/%s", utils.GetServerRoot(ctx), fileName)
u = fmt.Sprintf("%s/%s?enc_key=%s", global.Configuration.Domain, fileName, masterKey)
}
response.SendTextResponse(ctx, u, fasthttp.StatusOK)
response.SendJSONResponse(ctx, response.JSONResponse{
Status: response.RequestStatusOK,
Data: uploadResponse{
URI: u,
EncryptionKey: masterKey,
},
Message: "",
}, fasthttp.StatusOK)
}