fix issue with unauthorized message, add zero-width image url option

This commit is contained in:
vysion 2021-02-24 02:14:56 -08:00
parent baad27a4a4
commit 8d41c9805a
8 changed files with 104 additions and 20 deletions

View File

@ -18,6 +18,7 @@ A file host server with server security in mind. Intended for private use.
- Whitelist/blacklist file types, and check them based on their headers, not the extension
- Sanitize files to prevent against phishing attacks
- Public/private mode (private by default)
- Zero-width image URLs that aren't absurdly long
## Setup
@ -54,4 +55,8 @@ If anything goes wrong, you can check `journalctl -u tytanium` and find out what
### How to Upload
Create a POST request to `/upload` with a file in the field "file". You can also set `omitdomain` to 1 if you don't want the host's original domain appended before the file name in the response. E.g: `a.png` instead of `https://a.com/a.png`
Create a POST request to `/upload` with a file in the field "file". Put the key in `Authorization` header
Set `?omitdomain=1`, if you don't want the host's original domain appended before the file name in the response. E.g: `a.png` instead of `https://a.com/a.png`
Add `?zerowidth=1` and set it to `1` to make your image URLs appear "zero-width". If you don't get what that means, try it, and see what happens.

View File

@ -69,9 +69,6 @@ server:
# If an ID collision occurs the most recent file will override.
# This default value gives 62^8 possible values, and should be good enough.
idlen: 8
# How many requests to handle at once, per IP.
# This default value of 16 means that an IP can only have 16 ongoing uploads/downloads with the server.
maxconnsperip: 16
# How many TOTAL requests the server can handle at once.
# Requests will not be served to ANYONE if the # of connections everywhere is above this number.
concurrency: 512

View File

@ -36,7 +36,7 @@ func Try(ctx context.Context, redisClient *redis.Client, id string, max int64, r
if err := pipeliner.IncrBy(ctx, id, incrBy).Err(); err != nil {
return err
}
return pipeliner.Expire(ctx, id, time.Duration(resetAfter) * time.Millisecond).Err()
return pipeliner.Expire(ctx, id, time.Duration(resetAfter)*time.Millisecond).Err()
})
return err

View File

@ -12,7 +12,7 @@ import (
)
const (
Version = "1.13.1"
Version = "1.13.2"
GCSKeyLoc = "./conf/key.json"
)
@ -34,7 +34,6 @@ func main() {
viper.SetDefault("net.redis.db", 0)
viper.SetDefault("server.idlen", 5)
viper.SetDefault("server.concurrency", 128*4)
viper.SetDefault("server.maxconnsperip", 16)
viper.SetDefault("security.maxsizebytes", 52428800)
viper.SetDefault("security.publicmode", false)
viper.SetDefault("security.ratelimit.resetafter", 60000)
@ -77,12 +76,10 @@ func main() {
Handler: b.limitPath(handleCORS(b.handleHTTPRequest)),
HeaderReceived: nil,
ContinueHandler: nil,
Name: "Tytanium " + Version,
Concurrency: configuration.Server.Concurrency,
DisableKeepalive: false,
ReadTimeout: 30 * time.Minute,
WriteTimeout: 30 * time.Minute,
MaxConnsPerIP: configuration.Server.MaxConnsPerIP,
TCPKeepalive: false,
TCPKeepalivePeriod: 0,
MaxRequestBodySize: configuration.Security.MaxSizeBytes + 2048,

View File

@ -10,6 +10,7 @@ import (
"github.com/valyala/fasthttp"
"github.com/vysiondev/httputil/net"
"io"
"net/url"
"regexp"
"strconv"
"strings"
@ -33,13 +34,23 @@ 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 (b *BaseHandler) ServeFile(ctx *fasthttp.RequestCtx) {
id := ctx.Request.URI().LastPathSegment()
id := string(ctx.Request.URI().LastPathSegment())
if len(id) == 0 {
b.ServeNotFound(ctx)
return
}
decoded := url.QueryEscape(id)
wc := b.GCSClient.Bucket(b.Config.Net.GCS.BucketName).Object(string(id)).Key(b.Key)
// Most likely a zero-with URL but we can check for that
if strings.HasPrefix(decoded, "%") {
id = ZWSToString(id)
if len(id) == 0 {
SendTextResponse(ctx, "There was a problem converting the path segment to a string.", fasthttp.StatusBadRequest)
return
}
}
wc := b.GCSClient.Bucket(b.Config.Net.GCS.BucketName).Object(id).Key(b.Key)
// We don't need a limited reader because mimetype.DetectReader automatically caps it
readBase, e := wc.NewReader(ctx)
if e != nil {
@ -79,8 +90,8 @@ func (b *BaseHandler) ServeFile(ctx *fasthttp.RequestCtx) {
ctx.Response.Header.Add("Pragma", "no-cache")
ctx.Response.Header.Add("Expires", "0")
url := fmt.Sprintf("%s/%s?%s=true", net.GetRoot(ctx), id, rawParam)
_, _ = fmt.Fprint(ctx.Response.BodyWriter(), strings.Replace(discordHTML, "{{.}}", url, 1))
u := fmt.Sprintf("%s/%s?%s=true", net.GetRoot(ctx), id, rawParam)
_, _ = fmt.Fprint(ctx.Response.BodyWriter(), strings.Replace(discordHTML, "{{.}}", u, 1))
return
}
}
@ -93,7 +104,7 @@ func (b *BaseHandler) ServeFile(ctx *fasthttp.RequestCtx) {
} else {
ctx.Response.Header.Set("Content-Type", mimeType.String())
}
ctx.Response.Header.Set("Content-Disposition", "inline")
ctx.Response.Header.Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", id))
ctx.Response.Header.Set("Content-Length", strconv.FormatInt(readBase.Attrs.Size, 10))
readBase.Close()

View File

@ -18,7 +18,6 @@ const fileHandler = "file"
func (b *BaseHandler) ServeUpload(ctx *fasthttp.RequestCtx) {
auth := b.IsAuthorized(ctx)
if !auth && !b.Config.Security.PublicMode {
SendTextResponse(ctx, "Not authorized to upload.", fasthttp.StatusUnauthorized)
return
}
mp, e := ctx.Request.MultipartForm()
@ -91,8 +90,12 @@ func (b *BaseHandler) ServeUpload(ctx *fasthttp.RequestCtx) {
return
}
if string(ctx.QueryArgs().Peek("zerowidth")) == "1" {
fileName = StringToZWS(fileName)
}
var u string
if mp.Value["omitdomain"] != nil && len(mp.Value["omitdomain"]) > 0 && mp.Value["omitdomain"][0] == "1" {
if string(ctx.QueryArgs().Peek("omitdomain")) == "1" {
u = fileName
} else {
u = fmt.Sprintf("%s/%s", net.GetRoot(ctx), fileName)

View File

@ -34,10 +34,9 @@ type FilterConfig struct {
}
type ServerConfig struct {
Port string
Concurrency int
MaxConnsPerIP int
IDLen int
Port string
Concurrency int
IDLen int
}
type NetConfig struct {

72
zws.go Normal file
View File

@ -0,0 +1,72 @@
package main
import (
"strings"
)
const characterIndex = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789."
var (
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", "\U000E0025",
"\U000E004C",
}
)
func GetCharacterIndex(s string) int {
return strings.Index(characterIndex, s)
}
func StringToZWS(baseStr string) string {
var completedStr string
for i := 0; i < len(baseStr); i++ {
r := characterReference[GetCharacterIndex(string(baseStr[i]))]
if len(r) == 0 {
// means we're trying to create a string without a character in the reference
return ""
}
completedStr += r
}
return completedStr
}
// what else am i supposed to call it dumbass
func ZWSToString(encodedStr string) string {
rL := []rune(encodedStr)
var finalStr string
for _, r := range rL {
match := false
for i, v := range characterReference {
if []rune(v)[0] == r {
match = true
finalStr += string(characterIndex[i])
break
}
}
if !match {
return ""
}
}
return finalStr
}