fix issue with unauthorized message, add zero-width image url option
This commit is contained in:
parent
baad27a4a4
commit
8d41c9805a
|
@ -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.
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
5
main.go
5
main.go
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue