Reintroduce zero width feature, JSON response fix

This commit is contained in:
vysion 2022-04-02 21:04:20 -07:00
parent ce6a5d101c
commit 5b4b5d4b87
9 changed files with 179 additions and 63 deletions

View File

@ -1,13 +1,16 @@
{
"Version": "13.7.0",
"Name": "Tytanium Example Configuration",
"Version": "13.5.0",
"DestinationType": "ImageUploader, TextUploader, FileUploader",
"RequestMethod": "POST",
"RequestURL": "https://yourdomainhere.com/upload",
"Parameters": {
"zerowidth": "$prompt:zero width (1=y, 0=n, 0 default)|0$"
},
"Headers": {
"Authorization": "your key here"
},
"Body": "MultipartFormData",
"FileFormName": "file",
"URL": "$response$"
"URL": "$json:data.uri$",
"ErrorMessage": "$json:message$"
}

24
init.go
View File

@ -14,8 +14,9 @@ import (
)
const (
mebibyte = 1 << 20
minute = 60000
mebibyte = 1 << 20
minute = 60000
characterTagLengthEncoded = 12
)
func init() {
@ -85,14 +86,17 @@ func initConfiguration() {
}
}
// TODO: what the fuck is this clean it up
// - ID length * 4 bytes,
// - extension length limit * 4 bytes,
// - 1 byte for the / character,
// - 4 bytes for the . character
// - X bytes for encryption key
constants.PathLengthLimitBytes = (global.Configuration.Storage.IDLength * 4) + (constants.ExtensionLengthLimit * 4) + 5 + global.Configuration.Encryption.EncryptionKeyLength
// Domain length + 1 byte for "/"
// ID length * 12 (%00%00%00%00)
// Extension length * 12
// 9 (?enc_key=) * 12
// Encryption key length * 12
constants.PathLengthLimitBytes =
(len(global.Configuration.Domain) + 1) +
(global.Configuration.Storage.IDLength * characterTagLengthEncoded) +
(constants.ExtensionLengthLimit * characterTagLengthEncoded) +
(9 * characterTagLengthEncoded) +
(global.Configuration.Encryption.EncryptionKeyLength * characterTagLengthEncoded)
log.Println("[init] Loaded configuration")
}

View File

@ -29,30 +29,37 @@ type JSONResponse struct {
}
// SendTextResponse sends a plaintext response to the client along with an HTTP status code.
func SendTextResponse(ctx *fasthttp.RequestCtx, msg string, code int) {
ctx.Response.Header.SetContentType(plainTextContentType)
if code == fasthttp.StatusInternalServerError {
log.Printf(fmt.Sprintf("Unhandled error!, %s", msg))
if global.Configuration.Logging.Enabled {
logger.ErrorLogger.Printf("500 response sent; error message: %s", msg)
}
}
ctx.SetStatusCode(code)
_, e := fmt.Fprint(ctx.Response.BodyWriter(), msg)
if e != nil {
log.Printf(fmt.Sprintf("Request failed to send! %v, status code %d", e, code))
if global.Configuration.Logging.Enabled {
logger.ErrorLogger.Printf("Failed to send response; error message: %s, status code: %d", e, code)
}
}
}
//func SendTextResponse(ctx *fasthttp.RequestCtx, msg string, code int) {
// ctx.Response.Header.SetContentType(plainTextContentType)
// if code == fasthttp.StatusInternalServerError {
// log.Printf(fmt.Sprintf("Unhandled error!, %s", msg))
// if global.Configuration.Logging.Enabled {
// logger.ErrorLogger.Printf("500 response sent; error message: %s", msg)
// }
// }
//
// ctx.SetStatusCode(code)
// _, e := fmt.Fprint(ctx.Response.BodyWriter(), msg)
// if e != nil {
// log.Printf(fmt.Sprintf("Request failed to send! %v, status code %d", e, code))
// if global.Configuration.Logging.Enabled {
// logger.ErrorLogger.Printf("Failed to send response; error message: %s, status code: %d", e, code)
// }
// }
//}
// SendJSONResponse sends a JSON encoded response to the client along with an HTTP status code of 200 OK.
func SendJSONResponse(ctx *fasthttp.RequestCtx, json interface{}, statusCode int) {
func SendJSONResponse(ctx *fasthttp.RequestCtx, j JSONResponse, statusCode int) {
if statusCode == RequestStatusInternalError {
log.Printf(fmt.Sprintf("Unhandled error!, %s", j.Message))
if global.Configuration.Logging.Enabled {
logger.ErrorLogger.Printf("500 response sent; error message: %v", j.Message)
}
}
ctx.SetContentType(jsonContentType)
ctx.SetStatusCode(statusCode)
e := json2.NewEncoder(ctx.Response.BodyWriter()).Encode(json)
e := json2.NewEncoder(ctx.Response.BodyWriter()).Encode(j)
if e != nil {
if global.Configuration.Logging.Enabled {
logger.ErrorLogger.Printf("Failed to send JSON response; error message: %v", e)

View File

@ -6,6 +6,7 @@ import (
"github.com/minio/sio"
"github.com/valyala/fasthttp"
"io"
"net/url"
"os"
"path"
"regexp"
@ -19,8 +20,11 @@ import (
"tytanium/utils"
)
const encryptionKeyParam = "enc_key"
const rawParam = "raw"
const (
paramEncryptionKey = "enc_key"
paramRaw = "raw"
ZeroWidthFirstByte = 37
)
// discordHTML represents what is sent back to any client which User-Agent contains the regex contained in
// discordBotRegex.
@ -43,9 +47,7 @@ var (
// ServeFile will serve the / endpoint. It gets the "id" variable from mux and tries to find the file's information in the database.
// If an ID is either not provided or not found, the function hands the request off to ServeNotFound.
func ServeFile(ctx *fasthttp.RequestCtx) {
pBytes := ctx.Request.URI().Path()
if len(pBytes) > constants.PathLengthLimitBytes {
if len(ctx.Request.RequestURI()) > constants.PathLengthLimitBytes {
response.SendJSONResponse(ctx, response.JSONResponse{
Status: response.RequestStatusError,
Data: nil,
@ -54,7 +56,7 @@ func ServeFile(ctx *fasthttp.RequestCtx) {
return
}
if len(pBytes) <= 1 {
if len(ctx.Request.RequestURI()) <= 1 {
response.SendJSONResponse(ctx, response.JSONResponse{
Status: response.RequestStatusError,
Data: nil,
@ -63,7 +65,31 @@ func ServeFile(ctx *fasthttp.RequestCtx) {
return
}
if len(ctx.QueryArgs().Peek(encryptionKeyParam)) == 0 {
// Convert entire request URI to a normal string if the first byte represents a URL-encoded string.
if ctx.Request.RequestURI()[1] == ZeroWidthFirstByte {
uriDecoded, err := url.QueryUnescape(string(ctx.Request.RequestURI()))
if err != nil {
response.SendJSONResponse(ctx, response.JSONResponse{
Status: response.RequestStatusError,
Data: nil,
Message: "Failed to decode string.",
}, fasthttp.StatusOK)
return
}
if len(uriDecoded) <= 1 {
response.SendJSONResponse(ctx, response.JSONResponse{
Status: response.RequestStatusError,
Data: nil,
Message: "Zero-width string is not long enough.",
}, fasthttp.StatusOK)
return
}
ctx.Request.SetRequestURI("/" + utils.ZeroWidthToString(uriDecoded[1:]))
}
if len(ctx.QueryArgs().Peek(paramEncryptionKey)) == 0 {
response.SendJSONResponse(ctx, response.JSONResponse{
Status: response.RequestStatusError,
Data: nil,
@ -72,9 +98,8 @@ func ServeFile(ctx *fasthttp.RequestCtx) {
return
}
p := string(pBytes[1:])
filePath := path.Join(global.Configuration.Storage.Directory, p)
pathNoLeadingSlash := string(ctx.Request.URI().Path()[1:])
filePath := path.Join(global.Configuration.Storage.Directory, pathNoLeadingSlash)
// we only need to know if it exists or not
fileInfo, err := os.Stat(filePath)
@ -130,7 +155,7 @@ func ServeFile(ctx *fasthttp.RequestCtx) {
_ = fileReader.Close()
}()
key, err := encryption.DeriveKey(ctx.QueryArgs().Peek(encryptionKeyParam), []byte(global.Configuration.Encryption.Nonce))
key, err := encryption.DeriveKey(ctx.QueryArgs().Peek(paramEncryptionKey), []byte(global.Configuration.Encryption.Nonce))
if err != nil {
response.SendJSONResponse(ctx, response.JSONResponse{
Status: response.RequestStatusInternalError,
@ -166,17 +191,17 @@ func ServeFile(ctx *fasthttp.RequestCtx) {
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-Disposition", fmt.Sprintf("inline; filename=\"%s\"", pathNoLeadingSlash))
ctx.Response.Header.Set("Content-Length", strconv.FormatInt(fileInfo.Size(), 10))
if discordBotRegex.Match(ctx.Request.Header.UserAgent()) && !ctx.QueryArgs().Has(rawParam) {
if discordBotRegex.Match(ctx.Request.Header.UserAgent()) && !ctx.QueryArgs().Has(paramRaw) {
if mimetype.EqualsAny(mimeType.String(), "image/png", "image/jpeg", "image/gif") {
ctx.Response.Header.SetContentType("text/html; charset=utf8")
ctx.Response.Header.Add("Cache-Control", "no-cache, no-store, must-revalidate")
ctx.Response.Header.Add("Pragma", "no-cache")
ctx.Response.Header.Add("Expires", "0")
u := fmt.Sprintf("%s/%s?%s=true&enc_key=%s", global.Configuration.Domain, p, rawParam, string(ctx.QueryArgs().Peek(encryptionKeyParam)))
u := fmt.Sprintf("%s/%s?%s=true&enc_key=%s", global.Configuration.Domain, pathNoLeadingSlash, paramRaw, string(ctx.QueryArgs().Peek(paramEncryptionKey)))
_, _ = fmt.Fprint(ctx.Response.BodyWriter(), strings.Replace(discordHTML, "{{.}}", u, 1))
return
}

View File

@ -7,5 +7,9 @@ import (
// ServeNotFound will always return an HTTP status code of 404 + error message text.
func ServeNotFound(ctx *fasthttp.RequestCtx) {
response.SendTextResponse(ctx, "Not found", fasthttp.StatusNotFound)
response.SendJSONResponse(ctx, response.JSONResponse{
Status: response.RequestStatusError,
Data: nil,
Message: "Not found.",
}, fasthttp.StatusNotFound)
}

View File

@ -19,11 +19,6 @@ import (
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) {
@ -209,17 +204,23 @@ func ServeUpload(ctx *fasthttp.RequestCtx) {
logger.InfoLogger.Printf("File %s was created, size: %d", fileName, f.Size)
}
var u string
if string(ctx.QueryArgs().Peek("omitdomain")) == "1" {
u = fileName
} else {
u = fmt.Sprintf("%s/%s?enc_key=%s", global.Configuration.Domain, fileName, masterKey)
targetPath := fmt.Sprintf("%s?enc_key=%s", fileName, masterKey)
if global.Configuration.ForceZeroWidth || string(ctx.QueryArgs().Peek("zerowidth")) == "1" {
targetPath = utils.StringToZeroWidth(targetPath)
}
response.SendJSONResponse(ctx, response.JSONResponse{
Status: response.RequestStatusOK,
Data: uploadResponse{
URI: u,
Data: struct {
URI string `json:"uri"`
Path string `json:"path"`
FileName string `json:"file_name"`
EncryptionKey string `json:"encryption_key"`
}{
URI: global.Configuration.Domain + "/" + targetPath,
Path: targetPath,
FileName: fileName,
EncryptionKey: masterKey,
},
Message: "",

View File

@ -10,7 +10,11 @@ import (
// HTTP status code 401 is returned.
func IsAuthorized(ctx *fasthttp.RequestCtx) bool {
if string(ctx.Request.Header.Peek("authorization")) != global.Configuration.Security.MasterKey {
response.SendTextResponse(ctx, "Not authorized.", fasthttp.StatusUnauthorized)
response.SendJSONResponse(ctx, response.JSONResponse{
Status: response.RequestStatusError,
Data: nil,
Message: "Not authorized to access that.",
}, fasthttp.StatusUnauthorized)
return false
}
return true

View File

@ -21,11 +21,19 @@ const (
// FilterSanitize means the file's Content-Type header returned to the client should be changed to text/plain.
func FilterCheck(ctx *fasthttp.RequestCtx, mimeType string) FilterStatus {
if len(global.Configuration.Filter.Blacklist) > 0 && mimetype.EqualsAny(mimeType, global.Configuration.Filter.Blacklist...) {
response.SendTextResponse(ctx, "File type is blacklisted.", fasthttp.StatusBadRequest)
response.SendJSONResponse(ctx, response.JSONResponse{
Status: response.RequestStatusError,
Data: nil,
Message: "This file type is blacklisted.",
}, fasthttp.StatusOK)
return FilterFail
}
if len(global.Configuration.Filter.Whitelist) > 0 && !mimetype.EqualsAny(mimeType, global.Configuration.Filter.Whitelist...) {
response.SendTextResponse(ctx, "File type is not whitelisted.", fasthttp.StatusBadRequest)
response.SendJSONResponse(ctx, response.JSONResponse{
Status: response.RequestStatusError,
Data: nil,
Message: "This file type is not whitelisted.",
}, fasthttp.StatusOK)
return FilterFail
}
if len(global.Configuration.Filter.Sanitize) > 0 && mimetype.EqualsAny(mimeType, global.Configuration.Filter.Sanitize...) {

View File

@ -1 +1,61 @@
package utils
import (
"strings"
)
var (
characterIndex = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.?=_"
characterReference = []string{
"\U000E0050", "\U000E0043", "\U000E0034", "\U000E0035",
"\U000E002D", "\U000E002A", "\U000E005D", "\U000E002E",
"\U000E0026", "\U000E0024", "\U000E0058", "\U000E004E",
"\U000E0037", "\U000E0049", "\U000E0051", "\U000E0041",
"\U000E0028", "\U000E0027", "\U000E004B", "\U000E005E",
"\U000E0044", "\U000E0040", "\U000E004D", "\U000E0056",
"\U000E0060", "\U000E0055", "\U000E0030", "\U000E0023",
"\U000E0039", "\U000E004F", "\U000E0052", "\U000E002B",
"\U000E0057", "\U000E003C", "\U000E0053", "\U000E005B",
"\U000E003F", "\U000E0021", "\U000E003B", "\U000E0046",
"\U000E0031", "\U000E0059", "\U000E003E", "\U000E0047",
"\U000E005C", "\U000E003D", "\U000E0054", "\U000E0048",
"\U000E005F", "\U000E0038", "\U000E003A", "\U000E002F",
"\U000E005A", "\U000E0020", "\U000E0042", "\U000E0033",
"\U000E0036", "\U000E004A", "\U000E0022", "\U000E0045",
"\U000E0032", "\U000E002C", "\U000E0029", "\U000E007B",
"\U000E007C", "\U000E007D", "\U000E007E",
}
)
func GetCharacterIndex(s string) int {
return strings.Index(characterIndex, s)
}
func ZeroWidthToString(encodedStr string) string {
var finalStr string
strAsRunes := []rune(encodedStr)
for _, v := range strAsRunes {
for pos, i := range characterReference {
if []rune(i)[0] == v {
finalStr += string(characterIndex[pos])
}
}
}
return finalStr
}
func StringToZeroWidth(baseStr string) string {
var finalStr string
splitStr := strings.Split(baseStr, "")
for _, r := range splitStr {
index := GetCharacterIndex(r)
finalStr += characterReference[index]
}
return finalStr
}