From 5b4b5d4b8794a7a9f566d4752f9fe7b2f8f95b56 Mon Sep 17 00:00:00 2001 From: vysion <77179954+vysiondev@users.noreply.github.com> Date: Sat, 2 Apr 2022 21:04:20 -0700 Subject: [PATCH] Reintroduce zero width feature, JSON response fix --- example/tytanium.sxcu | 9 ++++-- init.go | 24 +++++++++------- response/response.go | 47 +++++++++++++++++------------- routes/serve_file.go | 53 +++++++++++++++++++++++++--------- routes/serve_not_found.go | 6 +++- routes/serve_upload.go | 25 ++++++++-------- security/auth.go | 6 +++- security/filter.go | 12 ++++++-- utils/zero_width.go | 60 +++++++++++++++++++++++++++++++++++++++ 9 files changed, 179 insertions(+), 63 deletions(-) diff --git a/example/tytanium.sxcu b/example/tytanium.sxcu index de065b9..7221941 100644 --- a/example/tytanium.sxcu +++ b/example/tytanium.sxcu @@ -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$" } \ No newline at end of file diff --git a/init.go b/init.go index eec0889..834a7fb 100644 --- a/init.go +++ b/init.go @@ -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") } diff --git a/response/response.go b/response/response.go index 10e250a..787a9a2 100644 --- a/response/response.go +++ b/response/response.go @@ -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) diff --git a/routes/serve_file.go b/routes/serve_file.go index 460c128..f3abed5 100644 --- a/routes/serve_file.go +++ b/routes/serve_file.go @@ -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 } diff --git a/routes/serve_not_found.go b/routes/serve_not_found.go index 8c107a6..6485f2e 100644 --- a/routes/serve_not_found.go +++ b/routes/serve_not_found.go @@ -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) } diff --git a/routes/serve_upload.go b/routes/serve_upload.go index c37cfb6..842438f 100644 --- a/routes/serve_upload.go +++ b/routes/serve_upload.go @@ -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: "", diff --git a/security/auth.go b/security/auth.go index b78f67a..dc927fc 100644 --- a/security/auth.go +++ b/security/auth.go @@ -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 diff --git a/security/filter.go b/security/filter.go index ed3fc75..29d7ffb 100644 --- a/security/filter.go +++ b/security/filter.go @@ -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...) { diff --git a/utils/zero_width.go b/utils/zero_width.go index d4b585b..61468ad 100644 --- a/utils/zero_width.go +++ b/utils/zero_width.go @@ -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 +}