reworked zws handling, added ForceZeroWidth to config, updated readme
This commit is contained in:
parent
07e7eea2cf
commit
d2857a21dd
11
README.md
11
README.md
|
@ -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
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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:
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
2
init.go
2
init.go
|
@ -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 {
|
||||
|
|
3
main.go
3
main.go
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
11
utils/zws.go
11
utils/zws.go
|
@ -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])
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue