reworked zws handling, added ForceZeroWidth to config, updated readme

This commit is contained in:
vysion 2021-09-12 13:18:32 -07:00
parent 07e7eea2cf
commit d2857a21dd
11 changed files with 50 additions and 43 deletions

View File

@ -6,23 +6,24 @@ A simple file host server, intended for personal or small group use.
## Features
- Configure and tune the server to how you want with extensive customization
- Tune the server to exactly how you want with extensive customization options
- Built with [fasthttp](https://github.com/vayala/fasthttp) for performance and built-in anti-DoS features
- Whitelist/blacklist file types, and check them based on the file header, not the extension
- Whitelist/blacklist checks done via file headers rather than extensions
- Sanitize files to prevent against phishing attacks (Change their Content-Type to text/plain)
- ~~Public/private mode (private by default)~~ (private only)
- Zero-width file IDs in URLs - paste invisible but functional links!
- File ID collision checking
- Works well with image capture suites, such as ShareX/MagicCap
- Not written in Javascript!
### Setup
- Download the binary or build this program
- Rename `example.yml` to `config.yml` and set the values you want
- Optional: Replace `routes/favicon.ico` with your own favicon
- Start the binary
- Done
- **Optional:** You can use the [Size Checker](https://github.com/vysiondev/size-checker) program to make the `/stats` path produce values other than 0 for file count and total size used. Just tell it to check your files directory. You can run it as a cron job or run it manually whenever you want to update it. (If you choose not to use it, `/stats` will always return 0 for every field.)
- **Optional stuff**
- You can use the [Size Checker](https://github.com/vysiondev/size-checker) program to make the `/stats` path produce values other than 0 for file count and total size used. Just tell it to check your files directory. You can run it as a cron job or run it manually whenever you want to update it. (If you choose not to use it, `/stats` will always return 0 for every field.)
- If you want to change the favicon, replace `routes/favicon.ico` with your own image.
### How to Upload

View File

@ -2,13 +2,14 @@ package api
// Configuration is the configuration structure used by the program.
type Configuration struct {
Storage storageConfig
RateLimit rateLimitConfig
Filter filterConfig
Security securityConfig
Server serverConfig
Redis redisConfig
MoreStats bool
Storage storageConfig
RateLimit rateLimitConfig
Filter filterConfig
Security securityConfig
Server serverConfig
Redis redisConfig
MoreStats bool
ForceZeroWidth bool
}
type storageConfig struct {

View File

@ -88,4 +88,8 @@ Redis: # Configure the Redis connection. The values in this section are self-exp
DB: 0
# A boolean. Set this value to true if you want to have /stats show more data, including memory usage.
MoreStats:
MoreStats:
# A boolean. Set this value to true if zero width URLs will always be used, regardless of whether the user sets
# the zerowidth parameter in the request URL to 0 or not.
ForceZeroWidth:

View File

@ -1,9 +1,9 @@
{
"Version": "13.5.0",
"Version": "13.6.1",
"Name": "Tytanium Example Configuration",
"DestinationType": "ImageUploader, TextUploader, FileUploader",
"RequestMethod": "POST",
"RequestURL": "https://example.com/upload",
"RequestURL": "https://yourdomainhere.com/upload",
"Headers": {
"Authorization": "your key here"
},

View File

@ -13,5 +13,5 @@ var RedisClient *redis.Client
const (
// Version is the current version of the server.
Version = "1.3.0"
Version = "1.3.1"
)

View File

@ -48,7 +48,7 @@ func initConfiguration() {
viper.SetDefault("RateLimit.Bandwidth.Upload", 1000*mebibyte)
viper.SetDefault("Server.Port", 3030)
viper.SetDefault("server.concurrency", 128*4)
viper.SetDefault("Server.Concurrency", 128*4)
err := viper.Unmarshal(&global.Configuration)
if err != nil {

View File

@ -16,7 +16,8 @@ import (
func main() {
s := &fasthttp.Server{
ErrorHandler: nil,
ErrorHandler: nil,
// yo what da fuck
Handler: middleware.HandleCORS(middleware.LimitPath(middleware.HandleHTTPRequest)),
HeaderReceived: nil,
ContinueHandler: nil,

View File

@ -9,7 +9,6 @@ import (
"github.com/vysiondev/tytanium/security"
"github.com/vysiondev/tytanium/utils"
"io"
"net/url"
"os"
"path"
"regexp"
@ -40,24 +39,19 @@ 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) {
id := string(ctx.Request.URI().LastPathSegment())
if len(id) == 0 {
p := string(ctx.Request.URI().Path())
if len(p) == 0 {
ServeNotFound(ctx)
return
}
decoded := url.QueryEscape(id)
// Most likely a zero-with URL but we can check for that
if strings.HasPrefix(decoded, "%") {
id = utils.ZWSToString(id)
if len(id) == 0 {
response.SendTextResponse(ctx, "The path segment could not be converted to a string. This is an invalid zero-width URL.", fasthttp.StatusBadRequest)
return
}
}
// Convert all zero-width characters to normal string
p = utils.ZWSToString(p)
filePath := path.Join(global.Configuration.Storage.Directory, p)
// we only need to know if it exists or not
fileInfo, err := os.Stat(path.Join(global.Configuration.Storage.Directory, id))
fileInfo, err := os.Stat(filePath)
if err != nil {
if os.IsNotExist(err) {
ServeNotFound(ctx)
@ -67,8 +61,13 @@ func ServeFile(ctx *fasthttp.RequestCtx) {
return
}
if fileInfo.IsDir() {
response.SendTextResponse(ctx, "This is a directory, not a file", fasthttp.StatusBadRequest)
return
}
// We don't need a limited reader because mimetype.DetectReader automatically caps it
fileReader, e := os.OpenFile(path.Join(global.Configuration.Storage.Directory, id), os.O_RDONLY, 0644)
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)
return
@ -102,7 +101,7 @@ 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), id, rawParam)
u := fmt.Sprintf("%s/%s?%s=true", utils.GetServerRoot(ctx), p, rawParam)
_, _ = fmt.Fprint(ctx.Response.BodyWriter(), strings.Replace(discordHTML, "{{.}}", u, 1))
return
}
@ -110,13 +109,14 @@ func ServeFile(ctx *fasthttp.RequestCtx) {
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\"", id))
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)

View File

@ -29,7 +29,7 @@ func ServeUpload(ctx *fasthttp.RequestCtx) {
response.SendTextResponse(ctx, "No multipart form was in the request.", fasthttp.StatusBadRequest)
return
}
response.SendTextResponse(ctx, fmt.Sprintf("The form couldn't be parsed. %v", e), fasthttp.StatusBadRequest)
response.SendTextResponse(ctx, fmt.Sprintf("The multipart form couldn't be parsed. %v", e), fasthttp.StatusBadRequest)
return
}
defer ctx.Request.RemoveMultipartFormFiles()
@ -68,8 +68,10 @@ func ServeUpload(ctx *fasthttp.RequestCtx) {
status := security.FilterCheck(ctx, mimeType.String())
if status == security.FilterFail {
// response already sent if filter check failed, so no need to send anything here
return
}
_, 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)
@ -127,7 +129,7 @@ func ServeUpload(ctx *fasthttp.RequestCtx) {
return
}
if string(ctx.QueryArgs().Peek("zerowidth")) == "1" {
if global.Configuration.ForceZeroWidth || string(ctx.QueryArgs().Peek("zerowidth")) == "1" {
fileName = utils.StringToZWS(fileName)
}

View File

@ -16,6 +16,7 @@ const (
var src = rand.NewSource(time.Now().UnixNano())
// RandBytes gets a set of random bytes. Useful for random strings
// note: i have no idea where i found this code lol but whatever
func RandBytes(n int, c chan<- string, onExit func()) {
defer onExit()
b := make([]byte, n)

View File

@ -1,8 +1,6 @@
package utils
import (
"strings"
)
import "strings"
// hey yeah if it works and its stupid it aint stupid
const characterIndex = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789."
@ -24,8 +22,7 @@ var (
"\U000E005F", "\U000E0038", "\U000E003A", "\U000E002F",
"\U000E005A", "\U000E0020", "\U000E0042", "\U000E0033",
"\U000E0036", "\U000E004A", "\U000E0022", "\U000E0045",
"\U000E0032", "\U000E002C", "\U000E0029", "\U000E0025",
"\U000E004C",
"\U000E0032", "\U000E002C", "\U000E0029",
}
)
@ -55,7 +52,7 @@ func ZWSToString(encodedStr string) string {
rL := []rune(encodedStr)
var finalStr string
for _, r := range rL {
for ind, r := range rL {
match := false
for i, v := range characterReference {
if []rune(v)[0] == r {
@ -65,7 +62,7 @@ func ZWSToString(encodedStr string) string {
}
}
if !match {
return ""
finalStr += string(encodedStr[ind])
}
}