This commit is contained in:
vysion 2021-07-24 10:01:51 -04:00
parent e09830024d
commit 07ab3b8b92
13 changed files with 341 additions and 249 deletions

View File

@ -2,51 +2,26 @@
# Tytanium
A file host server which puts security first. Intended for private/small group use, and for things like screenshots (though you could use it for something else as well).
A file host server in Go which puts security first. **This server is not intended for large-scale use, but rather, private/small group use.** Effective with ShareX/MagicCap/other image capture suites.
## Features
- Excellent compatibility with image capture suites like ShareX/MagicCap/etc.
- Lots of configuration options
- Configure and tune the server to how you want with extensive customization
- 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 their headers, not the extension
- Sanitize files to prevent against phishing attacks
- Public/private mode (private by default)
- Zero-width file IDs in URLs
- Whitelist/blacklist file types, and check them based on the file header, not the extension
- 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
- Not written in Javascript!
## Setup
### Setup
Make sure you have a Redis instance set up somewhere; preferably in the same environment as the file hoster.
- Put your config in `conf/` as `config.yml` using `conf/example.yml` as a reference.
- Optionally, you can replace `favicon.ico` with your own icon! (It must have the same name)
- If you're using this with ShareX, check `example/tytanium.sxcu` for a template sxcu file.
Now you can choose to either run Tytanium with Docker or as a service on your system.
### Option 1: systemd/other service manager (Recommended)
- Download the binary to the same directory where `conf/` is located.
- Alternatively you can build it.
- Mark it as executable with `chmod 0744 <binary file>`.
- Copy `example/tytanium.service` to `/lib/systemd/system` (if it's not there already).
- Edit the WorkingDirectory and ExecFile to match the locations of the binary file.
- Run `systemctl daemon-reload`.
At this point, you can run the program using `systemctl start tytanium`. You can check on its status by running `systemctl status tytanium`.
If anything goes wrong, you can check `journalctl -u tytanium` and find out what happened.
### Option 2: Docker
**Note that you will have to attach external volumes yourself should you use this method.**
- Build the image with `docker build -t tytanium .`
- Make sure to bind the port you choose (default is `3030`) to other ports on your system. Here's an example of how you would run it, after building the image.
`docker container run -d -p 127.0.0.1:3030:3030 tytanium`
- Download the binary or build this program
- Rename `example.yml` to `config.yml` and set the values you want
- 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.)
### How to Upload

View File

@ -1,84 +1,89 @@
# -----------------------
# EXAMPLE CONFIGURATION
# Rename this file to "config.yml" before you start Tytanium.
# -----------------------
storage:
# Rename this file to "config2.yml" before you start Tytanium,
# or copy the contents of this file to a file named "config2.yml"
# that you made beforehand.
storage: # Configure options relating to file storage.
# If there is another directory you want to save files to (instead of "files" in the executable's
# directory), then specify an absolute path here.
directory:
security:
# The key to upload.
masterkey:
# Max size (in bytes) a file can be. This is 50 MiB.
# The size (in bytes) a file can be. This is 50 MiB.
# There will always be 2048 bytes added on top of this so that the server can respond. If it were 0 bytes,
# the server would not be able to respond at all LOL
maxsizebytes: 52428800
ratelimit:
# When to reset the limit, in milliseconds.
# The default value here is 1 minute.
resetafter: 60000
# Handles /upload path alone.
# This default value, for example, allows 10 uploads per minute.
# If not specified there will be no rate limit for the upload path.
upload: 10
# This is the GLOBAL rate limit.
# All requests to the server are limited to this number, per minute.
# If a per-route rate limit exceeds this number it will be overridden by this number.
# If not specified a rate limit will not exist.
global: 20
bandwidthlimit:
# When to reset the limit, in milliseconds.
# Wait 5 minutes before resetting the limit.
resetafter: 300000
# How many bytes an IP address can download/upload in the above resetafter time.
# These default values are download 50 MB in 5 minutes or upload 250 MB in 5 minutes.
# If they're not specified it will be ignored.
download: 52428800
upload: 262144000
# A subset of mime type lists which change how files can be displayed.
filter:
# A list of mime types to block from loading completely.
# If no values are given, a blacklist will not be set.
# This default list blocks unknown file types and unwanted files (typically), mainly applications.
blacklist:
- application/octet-stream
- application/vnd.microsoft.portable-executable
- application/x-msdos-program
- application/x-executable
- application/x-sqlite3
- application/x-object
- application/x-elf
# If this list is populated, then Tytanium will ONLY allow these mime types to be displayed.
# If a mime type from this list is also in the sanitize list, it will be sanitized.
# If a mime type from this list is also in the blacklist, it will be blacklisted anyway.
# If no values are given, a whitelist will not be set.
whitelist:
# A list of mime types to render as text/plain. Effective against documents like HTML to prevent phishing.
# If no values are given, no mime types will be sanitized.
sanitize:
- text/html
- text/x-php
- application/javascript
- application/x-python
- text/x-lua
- text/x-perl
server:
# What port to listen on. Default is 3030
port: 3030
# ID length to use. (e.g: xxxxx.png has an ID length of 5)
# If an ID collision occurs the most recent file will override.
# This default value gives 62^8 possible values, and should be good enough.
maxsize: 52428800
# The ID length to use. (e.g: xxxxx.png has an ID length of 5).
# IDs are composed of alphanumeric characters only (A-Z, a-z, 0-9).
# This default value gives 62^8 possible values, and should be good enough for most settings.
idlen: 8
# 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
# How many times an ID should be checked to see if a duplicate exists.
# If 0, and a duplicate ID is generated, the file will not upload.
collisioncheckattempts: 3
net:
# Self-explanatory Redis values.
redis:
uri:
password:
db: 0
ratelimit: # Limit the amount of requests users are allowed to make.
# When to reset the rate limit imposed on a user, in milliseconds. The default value here is 1 minute.
resetafter: 60000
# Handles the /upload path alone. This means that the rate limit for this path is exclusive to the path only.
# This default value, for example, allows 10 uploads per minute. If it is exceeded, the user can still access
# the rest of the site, but will not be allowed to upload until the rate limit is reset.
# If not specified there will be no rate limit for the upload path.
upload: 10
# This is the GLOBAL rate limit.
# All requests to the server are limited to this number, per minute.
# If a per-route rate limit exceeds this number it will be overridden by this number.
# If not specified a rate limit will not exist.
global: 20
bandwidth: # Limit the amount of data IPs can download/upload.
# When to reset the limit, in milliseconds. This default value is 5 minutes.
resetafter: 300000
# How many bytes an IP address can download/upload in the allotted time frame (specified by resetafter).
# These default values are download 50 MB in 5 minutes or upload 250 MB in 5 minutes.
# If they are not specified there will be no restrictions on bandwidth.
download: 52428800
upload: 262144000
filter: # Configure which file types are allowed to be uploaded.
# A list of mime types to block from loading completely.
# If no values are given, a blacklist will not be set.
# This default list blocks unknown file types and unwanted files (typically), mainly applications.
blacklist:
- application/octet-stream
- application/vnd.microsoft.portable-executable
- application/x-msdos-program
- application/x-executable
- application/x-sqlite3
- application/x-object
- application/x-elf
# If this list is populated, then Tytanium will ONLY allow these mime types to be displayed.
# If a mime type from this list is also in the sanitize list, it will be sanitized.
# If a mime type from this list is also in the blacklist, it will be blacklisted anyway.
# If no values are given, a whitelist will not be set.
whitelist:
# A list of mime types to render as text/plain. Effective against documents like HTML to prevent phishing.
# If no values are given, no mime types will be sanitized. It is highly recommended you at least keep text/html.
sanitize:
- text/html
- text/x-php
- application/javascript
- application/x-python
- text/x-lua
- text/x-perl
security: # Options relating to security and authorization.
# The key to upload. Do not share it with people you do not know and trust.
masterkey:
server: # Configure the way the HTTP server runs.
# What port to listen on. The default is 3030.
port:
# How many TOTAL requests the server can handle at once.
# Requests will not be served to ANYONE if the # of simultaneous connections is above this number.
concurrency: 512
redis: # Configure the Redis connection.
uri:
password:
db: 0
morestats: # A boolean for if you want to have /stats show more data, including memory usage.

52
config_struct.go Normal file
View File

@ -0,0 +1,52 @@
package main
type Configuration struct {
Storage storageConfig
RateLimit rateLimitConfig
Filter filterConfig
Security securityConfig
Server serverConfig
Redis redisConfig
MoreStats bool
}
type storageConfig struct {
Directory string
MaxSize int64
IDLen int64
CollisionCheckAttempts int64
}
type rateLimitConfig struct {
ResetAfter int64
Upload int64
Global int64
Bandwidth rateLimitBandwidthConfig
}
type rateLimitBandwidthConfig struct {
ResetAfter int64
Download int64
Upload int64
}
type filterConfig struct {
Blacklist []string
Whitelist []string
Sanitize []string
}
type securityConfig struct {
MasterKey string
}
type serverConfig struct {
Port int64
Concurrency int64
}
type redisConfig struct {
URI string
Password string
Db int64
}

View File

@ -18,15 +18,15 @@ const (
// FilterFail means a response was already returned, and the caller should terminate its function.
// FilterSanitize means the file's Content-Type header returned to the client should be changed to text/plain.
func (b *BaseHandler) FilterCheck(ctx *fasthttp.RequestCtx, mimeType string) FilterStatus {
if len(b.Config.Security.Filter.Blacklist) > 0 && mimetype.EqualsAny(mimeType, b.Config.Security.Filter.Blacklist...) {
if len(b.Config.Filter.Blacklist) > 0 && mimetype.EqualsAny(mimeType, b.Config.Filter.Blacklist...) {
SendTextResponse(ctx, "File type is blacklisted.", fasthttp.StatusBadRequest)
return FilterFail
}
if len(b.Config.Security.Filter.Whitelist) > 0 && !mimetype.EqualsAny(mimeType, b.Config.Security.Filter.Whitelist...) {
if len(b.Config.Filter.Whitelist) > 0 && !mimetype.EqualsAny(mimeType, b.Config.Filter.Whitelist...) {
SendTextResponse(ctx, "File type is not whitelisted.", fasthttp.StatusBadRequest)
return FilterFail
}
if len(b.Config.Security.Filter.Sanitize) > 0 && mimetype.EqualsAny(mimeType, b.Config.Security.Filter.Sanitize...) {
if len(b.Config.Filter.Sanitize) > 0 && mimetype.EqualsAny(mimeType, b.Config.Filter.Sanitize...) {
return FilterSanitize
}
return FilterPass

88
main.go
View File

@ -8,6 +8,9 @@ import (
"github.com/valyala/fasthttp"
"log"
"os"
"os/signal"
"runtime"
"strconv"
"time"
)
@ -15,7 +18,7 @@ import (
var Favicon []byte
const (
Version = "1.1.0"
Version = "1.2.0"
)
func main() {
@ -28,19 +31,25 @@ func main() {
var configuration Configuration
if err := viper.ReadInConfig(); err != nil {
log.Fatalf("Error reading config file, %s", err)
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
log.Fatalf("No config file set: %v", err)
} else {
log.Fatalf("Error reading config file: %v", err)
}
}
// Set undefined variables
viper.SetDefault("storage.directory", "files")
viper.SetDefault("server.port", "3030")
viper.SetDefault("net.redis.db", 0)
viper.SetDefault("server.idlen", 5)
viper.SetDefault("storage.idlen", 5)
viper.SetDefault("storage.collisioncheckattempts", 3)
viper.SetDefault("server.port", 3030)
viper.SetDefault("server.concurrency", 128*4)
viper.SetDefault("server.collisioncheckattempts", 3)
viper.SetDefault("security.maxsizebytes", 52428800)
viper.SetDefault("security.ratelimit.resetafter", 60000)
viper.SetDefault("security.bandwidthlimit.resetafter", 60000*5)
viper.SetDefault("ratelimit.resetafter", 60000)
viper.SetDefault("ratelimit.bandwidth.resetafter", 60000*5)
viper.SetDefault("security.maxsize", 52428800)
viper.SetDefault("redis.db", 0)
viper.SetDefault("morestats", false)
err := viper.Unmarshal(&configuration)
if err != nil {
log.Fatalf("Unable to decode into struct, %v", err)
@ -66,19 +75,20 @@ func main() {
}
log.Println("Saving all incoming files to directory", configuration.Storage.Directory)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
redisClient := redis.NewClient(&redis.Options{
Addr: configuration.Net.Redis.URI,
Password: configuration.Net.Redis.Password,
DB: configuration.Net.Redis.Db,
Addr: configuration.Redis.URI,
Password: configuration.Redis.Password,
DB: int(configuration.Redis.Db),
})
status := redisClient.Ping(ctx).Err()
if status != nil {
log.Fatal("Could not ping Redis database: " + status.Error())
cancel()
log.Fatalf("Could not ping Redis database, %v", status.Error())
}
cancel()
log.Println("Redis connection established")
b := NewBaseHandler(redisClient, configuration)
@ -88,13 +98,12 @@ func main() {
Handler: handleCORS(b.limitPath(b.handleHTTPRequest)),
HeaderReceived: nil,
ContinueHandler: nil,
Concurrency: configuration.Server.Concurrency,
Concurrency: int(configuration.Server.Concurrency),
DisableKeepalive: false,
ReadTimeout: 30 * time.Minute,
WriteTimeout: 30 * time.Minute,
ReadTimeout: 3 * time.Second,
TCPKeepalive: false,
TCPKeepalivePeriod: 0,
MaxRequestBodySize: configuration.Security.MaxSizeBytes + 2048,
MaxRequestBodySize: int(configuration.Storage.MaxSize) + 2048,
ReduceMemoryUsage: false,
GetOnly: false,
DisablePreParseMultipartForm: false,
@ -106,9 +115,44 @@ func main() {
KeepHijackedConns: false,
}
log.Println(">> Listening for new requests on port " + b.Config.Server.Port)
if err = s.ListenAndServe(":" + b.Config.Server.Port); err != nil {
log.Fatalf("Listen error: %s\n", err)
portAsString := strconv.Itoa(int(b.Config.Server.Port))
log.Println("Will listen for new requests on port " + portAsString)
stop := make(chan os.Signal)
signal.Notify(stop, os.Interrupt)
go func() {
if err = s.ListenAndServe(":" + portAsString); err != nil {
log.Fatalf("Listen error: %v\n", err)
}
}()
if b.Config.MoreStats {
// collect stats every 30 seconds
go func() {
for {
var m runtime.MemStats
runtime.ReadMemStats(&m)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
e := redisClient.Set(ctx, "ty_mem_usage", m.Alloc, 0).Err()
if e != nil {
log.Printf("Failed to write metrics! %v", e)
}
cancel()
time.Sleep(time.Second * 30)
}
}()
}
<-stop
log.Println("Shutting down")
if err := s.Shutdown(); err != nil {
log.Fatalf("Failed to shutdown gracefully: %v\n", err)
}
log.Println("Shut down")
os.Exit(0)
}

View File

@ -21,24 +21,24 @@ const (
func (b *BaseHandler) limitPath(h fasthttp.RequestHandler) fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) {
ip := utils.GetIP(ctx)
if b.Config.Security.RateLimit.ResetAfter <= 0 {
if b.Config.RateLimit.ResetAfter <= 0 {
h(ctx)
} else {
p := string(ctx.Request.URI().Path())
pathType := LimitGeneralPath
reqLimit := b.Config.Security.RateLimit.Global
reqLimit := b.Config.RateLimit.Global
switch strings.ToLower(p) {
case "/upload":
pathType = LimitUploadPath
reqLimit = b.Config.Security.RateLimit.Upload
reqLimit = b.Config.RateLimit.Upload
}
if reqLimit <= 0 {
h(ctx)
} else {
rlString := ""
// Check the global rate limit
isGlobalRateLimitOk, err := Try(ctx, b.RedisClient, fmt.Sprintf("G_%s", ip), b.Config.Security.RateLimit.Global, b.Config.Security.RateLimit.ResetAfter, 1)
isGlobalRateLimitOk, err := Try(ctx, b.RedisClient, fmt.Sprintf("G_%s", ip), b.Config.RateLimit.Global, b.Config.RateLimit.ResetAfter, 1)
if err != nil {
SendTextResponse(ctx, "Failed to call Try() to get information on global rate limit. "+err.Error(), fasthttp.StatusInternalServerError)
return
@ -49,7 +49,7 @@ func (b *BaseHandler) limitPath(h fasthttp.RequestHandler) fasthttp.RequestHandl
if pathType != LimitGeneralPath {
// Check the route exclusive rate limit
isPathOk, err := Try(ctx, b.RedisClient, fmt.Sprintf("%d_%s", pathType, ip), reqLimit, b.Config.Security.RateLimit.ResetAfter, 1)
isPathOk, err := Try(ctx, b.RedisClient, fmt.Sprintf("%d_%s", pathType, ip), reqLimit, b.Config.RateLimit.ResetAfter, 1)
if err != nil {
SendTextResponse(ctx, "Failed to call Try() to get information on path-specific rate limit. "+err.Error(), fasthttp.StatusInternalServerError)
return
@ -91,11 +91,11 @@ func (b *BaseHandler) handleHTTPRequest(ctx *fasthttp.RequestCtx) {
case "/favicon.ico":
ServeFavicon(ctx)
break
case "/checkauth":
b.ServeCheckAuth(ctx)
case "/check_auth":
b.ServeAuthCheck(ctx)
break
case "/ping":
b.ServePing(ctx)
case "/stats":
b.ServeStats(ctx)
break
default:
if !ctx.IsGet() {

View File

@ -15,18 +15,23 @@ func SendTextResponse(ctx *fasthttp.RequestCtx, msg string, code int) {
}
ctx.SetStatusCode(code)
_, _ = fmt.Fprint(ctx.Response.BodyWriter(), msg)
return
_, e := fmt.Fprint(ctx.Response.BodyWriter(), msg)
if e != nil {
log.Printf(fmt.Sprintf("Request failed to send! %v, 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{}) {
ctx.SetContentType("application/json")
_ = json2.NewEncoder(ctx.Response.BodyWriter()).Encode(json)
e := json2.NewEncoder(ctx.Response.BodyWriter()).Encode(json)
if e != nil {
log.Printf(fmt.Sprintf("JSON failed to send! %v", e))
}
}
// SendNothing sends 204 No Content.
func SendNothing(ctx *fasthttp.RequestCtx) {
ctx.SetStatusCode(fasthttp.StatusNoContent)
return
}

View File

@ -4,8 +4,8 @@ import (
"github.com/valyala/fasthttp"
)
// ServeCheckAuth validates either the standard or master key.
func (b *BaseHandler) ServeCheckAuth(ctx *fasthttp.RequestCtx) {
// ServeAuthCheck validates either the master key.
func (b *BaseHandler) ServeAuthCheck(ctx *fasthttp.RequestCtx) {
if !b.IsAuthorized(ctx) {
return
}

View File

@ -46,7 +46,7 @@ func (b *BaseHandler) ServeFile(ctx *fasthttp.RequestCtx) {
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)
SendTextResponse(ctx, "The path segment could not be converted to a string. This is an invalid zero-width URL.", fasthttp.StatusBadRequest)
return
}
}
@ -58,24 +58,24 @@ func (b *BaseHandler) ServeFile(ctx *fasthttp.RequestCtx) {
b.ServeNotFound(ctx)
return
}
SendTextResponse(ctx, "There was a problem calling stat on the file. "+err.Error(), fasthttp.StatusInternalServerError)
SendTextResponse(ctx, fmt.Sprintf("os.Stat() could not be called on the file. %v", err), fasthttp.StatusInternalServerError)
return
}
// We don't need a limited reader because mimetype.DetectReader automatically caps it
fileReader, e := os.OpenFile(path.Join(b.Config.Storage.Directory, id), os.O_RDONLY, 0644)
if e != nil {
SendTextResponse(ctx, "There was a problem reading the file. "+e.Error(), fasthttp.StatusInternalServerError)
SendTextResponse(ctx, fmt.Sprintf("The file could not be opened. %v", err), fasthttp.StatusInternalServerError)
return
}
defer func() {
_ = fileReader.Close()
}()
if b.Config.Security.BandwidthLimit.Download > 0 && b.Config.Security.BandwidthLimit.ResetAfter > 0 {
isBandwidthLimitNotReached, err := Try(ctx, b.RedisClient, fmt.Sprintf("BW_DN_%s", utils.GetIP(ctx)), b.Config.Security.BandwidthLimit.Download, b.Config.Security.RateLimit.ResetAfter, fileInfo.Size())
if b.Config.RateLimit.Bandwidth.Download > 0 && b.Config.RateLimit.Bandwidth.ResetAfter > 0 {
isBandwidthLimitNotReached, err := Try(ctx, b.RedisClient, fmt.Sprintf("BW_DN_%s", utils.GetIP(ctx)), b.Config.RateLimit.Bandwidth.Download, b.Config.RateLimit.Bandwidth.ResetAfter, fileInfo.Size())
if err != nil {
SendTextResponse(ctx, "There was a problem checking bandwidth limits. "+err.Error(), fasthttp.StatusInternalServerError)
SendTextResponse(ctx, fmt.Sprintf("Bandwidth limit couldn't be checked. %v", err), fasthttp.StatusInternalServerError)
return
}
if !isBandwidthLimitNotReached {
@ -86,7 +86,7 @@ func (b *BaseHandler) ServeFile(ctx *fasthttp.RequestCtx) {
mimeType, e := mimetype.DetectReader(fileReader)
if e != nil {
SendTextResponse(ctx, "Cannot detect the mime type of this file retrieved from server. It might be corrupted.", fasthttp.StatusBadRequest)
SendTextResponse(ctx, fmt.Sprintf("Cannot detect the mime type of this file retrieved from server. It might be corrupted. %v", e), fasthttp.StatusBadRequest)
return
}
@ -116,13 +116,13 @@ func (b *BaseHandler) ServeFile(ctx *fasthttp.RequestCtx) {
_, e = fileReader.Seek(0, io.SeekStart)
if e != nil {
SendTextResponse(ctx, "Failed to reset the reader to 0.", fasthttp.StatusInternalServerError)
SendTextResponse(ctx, fmt.Sprintf("Reader could not be reset to its initial position. %v", e), fasthttp.StatusInternalServerError)
return
}
_, copyErr := io.Copy(ctx.Response.BodyWriter(), fileReader)
if copyErr != nil {
SendTextResponse(ctx, "Could not write file to client. "+copyErr.Error(), fasthttp.StatusInternalServerError)
SendTextResponse(ctx, fmt.Sprintf("File wasn't written to the client successfully. %v", copyErr), fasthttp.StatusInternalServerError)
return
}
}

View File

@ -1,18 +0,0 @@
package main
import (
"github.com/valyala/fasthttp"
)
type PingResponse struct {
Public bool `json:"public"`
ServerVersion string `json:"version"`
MaxSize int `json:"max_size"`
}
func (b *BaseHandler) ServePing(ctx *fasthttp.RequestCtx) {
SendJSONResponse(ctx, &PingResponse{
ServerVersion: Version,
MaxSize: b.Config.Security.MaxSizeBytes,
})
}

84
serve_stats.go Normal file
View File

@ -0,0 +1,84 @@
package main
import (
"fmt"
"github.com/go-redis/redis/v8"
"github.com/valyala/fasthttp"
"runtime"
"strconv"
)
type GeneralStats struct {
ServerVersion string `json:"server_version"`
RuntimeVersion string `json:"runtime_version,omitempty"`
MemoryUsage int64 `json:"memory_usage,omitempty"`
SizeStats StatsFromSizeChecker `json:"size_stats"`
}
type StatsFromSizeChecker struct {
TotalSize int64 `json:"total_size"`
FileCount int64 `json:"file_count"`
TimeToComplete int64 `json:"time_to_complete"`
LastUpdated int64 `json:"last_updated"`
}
// ServeStats serves stats. StatsFromSizeChecker are populated into redis by https://github.com/vysiondev/size-checker.
func (b *BaseHandler) ServeStats(ctx *fasthttp.RequestCtx) {
var stats GeneralStats
stats.ServerVersion = Version
totalSize, err := getStatValueFromRedis(ctx, b.RedisClient, "sc_total_size")
if err != nil {
SendTextResponse(ctx, fmt.Sprintf("An error occurred while trying to get sc_total_size from Redis: %v", err), fasthttp.StatusInternalServerError)
return
}
stats.SizeStats.TotalSize = totalSize
fileCount, err := getStatValueFromRedis(ctx, b.RedisClient, "sc_file_count")
if err != nil {
SendTextResponse(ctx, fmt.Sprintf("An error occurred while trying to get sc_file_count from Redis: %v", err), fasthttp.StatusInternalServerError)
return
}
stats.SizeStats.FileCount = fileCount
timeToComplete, err := getStatValueFromRedis(ctx, b.RedisClient, "sc_time_to_complete")
if err != nil {
SendTextResponse(ctx, fmt.Sprintf("An error occurred while trying to get sc_time_to_complete from Redis: %v", err), fasthttp.StatusInternalServerError)
return
}
stats.SizeStats.TimeToComplete = timeToComplete
lastUpdated, err := getStatValueFromRedis(ctx, b.RedisClient, "sc_last_updated")
if err != nil {
SendTextResponse(ctx, fmt.Sprintf("An error occurred while trying to get sc_last_updated from Redis: %v", err), fasthttp.StatusInternalServerError)
return
}
stats.SizeStats.LastUpdated = lastUpdated
if b.Config.MoreStats {
memUsage, err := getStatValueFromRedis(ctx, b.RedisClient, "ty_mem_usage")
if err != nil {
SendTextResponse(ctx, fmt.Sprintf("An error occurred while trying to get ty_mem_usage from Redis: %v", err), fasthttp.StatusInternalServerError)
return
}
stats.MemoryUsage = memUsage
stats.RuntimeVersion = runtime.Version()
}
SendJSONResponse(ctx, &stats)
}
func getStatValueFromRedis(ctx *fasthttp.RequestCtx, c *redis.Client, key string) (int64, error) {
v, e := c.Get(ctx, key).Result()
if e != nil {
if e == redis.Nil {
return 0, nil
}
return 0, e
}
i, e := strconv.ParseInt(v, 10, 64)
if e != nil {
return 0, e
}
return i, nil
}

View File

@ -13,10 +13,6 @@ import (
const fileHandler = "file"
func (b *BaseHandler) GetValidFileID() {
}
// ServeUpload handles all incoming POST requests to /upload. It will take a multipart form, parse the file, then write it to disk.
// The file's information will also be inserted into the database.
func (b *BaseHandler) ServeUpload(ctx *fasthttp.RequestCtx) {
@ -27,23 +23,23 @@ func (b *BaseHandler) ServeUpload(ctx *fasthttp.RequestCtx) {
mp, e := ctx.Request.MultipartForm()
if e != nil {
if e == fasthttp.ErrNoMultipartForm {
SendTextResponse(ctx, "Multipart form not sent.", fasthttp.StatusBadRequest)
SendTextResponse(ctx, "You didn't send a multipart form.", fasthttp.StatusBadRequest)
return
}
SendTextResponse(ctx, "There was a problem parsing the form. "+e.Error(), fasthttp.StatusBadRequest)
SendTextResponse(ctx, fmt.Sprintf("The form couldn't be parsed. %v", e), fasthttp.StatusBadRequest)
return
}
defer ctx.Request.RemoveMultipartFormFiles()
if len(mp.File[fileHandler]) == 0 {
SendTextResponse(ctx, "No files were uploaded.", fasthttp.StatusBadRequest)
SendTextResponse(ctx, "No files were sent.", fasthttp.StatusBadRequest)
return
}
f := mp.File[fileHandler][0]
if b.Config.Security.BandwidthLimit.Upload > 0 && b.Config.Security.BandwidthLimit.ResetAfter > 0 {
isUploadBandwidthLimitNotReached, err := Try(ctx, b.RedisClient, fmt.Sprintf("BW_UP_%s", utils.GetIP(ctx)), b.Config.Security.BandwidthLimit.Upload, b.Config.Security.RateLimit.ResetAfter, f.Size)
if b.Config.RateLimit.Bandwidth.Upload > 0 && b.Config.RateLimit.Bandwidth.ResetAfter > 0 {
isUploadBandwidthLimitNotReached, err := Try(ctx, b.RedisClient, fmt.Sprintf("BW_UP_%s", utils.GetIP(ctx)), b.Config.RateLimit.Bandwidth.Upload, b.Config.RateLimit.Bandwidth.ResetAfter, f.Size)
if err != nil {
SendTextResponse(ctx, "There was a problem checking bandwidth limits for uploading. "+err.Error(), fasthttp.StatusInternalServerError)
SendTextResponse(ctx, fmt.Sprintf("Bandwidth limit couldn't be checked. %v", err), fasthttp.StatusInternalServerError)
return
}
if !isUploadBandwidthLimitNotReached {
@ -54,7 +50,7 @@ func (b *BaseHandler) ServeUpload(ctx *fasthttp.RequestCtx) {
openedFile, e := f.Open()
if e != nil {
SendTextResponse(ctx, "Failed to open file from request: "+e.Error(), fasthttp.StatusInternalServerError)
SendTextResponse(ctx, fmt.Sprintf("File failed to open. %v", e), fasthttp.StatusInternalServerError)
return
}
defer func() {
@ -73,7 +69,7 @@ func (b *BaseHandler) ServeUpload(ctx *fasthttp.RequestCtx) {
}
_, e = openedFile.Seek(0, io.SeekStart)
if e != nil {
SendTextResponse(ctx, "Failed to reset the reader to 0.", fasthttp.StatusInternalServerError)
SendTextResponse(ctx, fmt.Sprintf("Reader could not be reset to its initial position. %v", e), fasthttp.StatusInternalServerError)
return
}
@ -86,7 +82,7 @@ func (b *BaseHandler) ServeUpload(ctx *fasthttp.RequestCtx) {
randomStringChan := make(chan string, 1)
go func() {
wg.Add(1)
utils.RandBytes(b.Config.Server.IDLen, randomStringChan, func() { wg.Done() })
utils.RandBytes(int(b.Config.Storage.IDLen), randomStringChan, func() { wg.Done() })
}()
wg.Wait()
fileId := <-randomStringChan
@ -94,7 +90,7 @@ func (b *BaseHandler) ServeUpload(ctx *fasthttp.RequestCtx) {
i, e := os.Stat(path.Join(b.Config.Storage.Directory, fileName))
if e != nil {
if os.IsNotExist(e) {
if os.IsNotExist(e) || e == os.ErrNotExist {
break
}
}
@ -102,7 +98,7 @@ func (b *BaseHandler) ServeUpload(ctx *fasthttp.RequestCtx) {
break
}
attempts++
if attempts >= b.Config.Server.CollisionCheckAttempts {
if attempts >= int(b.Config.Storage.CollisionCheckAttempts) {
SendTextResponse(ctx, "Tried too many times to find a valid file ID to use. Consider increasing the ID length.", fasthttp.StatusInternalServerError)
return
}
@ -114,13 +110,17 @@ func (b *BaseHandler) ServeUpload(ctx *fasthttp.RequestCtx) {
}()
if err != nil {
SendTextResponse(ctx, "There was a problem creating this file. "+err.Error(), fasthttp.StatusInternalServerError)
if err == os.ErrPermission {
SendTextResponse(ctx, fmt.Sprintf("Permission to create a file was denied. %v", err), fasthttp.StatusInternalServerError)
return
}
SendTextResponse(ctx, fmt.Sprintf("Could not create the file. %v", err), fasthttp.StatusInternalServerError)
return
}
_, writeErr := io.Copy(fsFile, openedFile)
if writeErr != nil {
SendTextResponse(ctx, "There was a problem writing this file to disk. "+writeErr.Error(), fasthttp.StatusInternalServerError)
SendTextResponse(ctx, fmt.Sprintf("The file failed to write to disk. %v", e), fasthttp.StatusInternalServerError)
return
}

View File

@ -1,55 +0,0 @@
package main
type Configuration struct {
Security SecurityConfig
Server ServerConfig
Net NetConfig
Storage StorageConfig
}
type StorageConfig struct {
Directory string
}
type SecurityConfig struct {
MasterKey string
MaxSizeBytes int
RateLimit RateLimitConfig
BandwidthLimit BandwidthLimitConfig
Filter FilterConfig
}
type BandwidthLimitConfig struct {
ResetAfter int64
Download int64
Upload int64
}
type RateLimitConfig struct {
ResetAfter int64
Upload int64
Global int64
}
type FilterConfig struct {
Blacklist []string
Whitelist []string
Sanitize []string
}
type ServerConfig struct {
Port string
Concurrency int
IDLen int
CollisionCheckAttempts int
}
type NetConfig struct {
Redis RedisConfig
}
type RedisConfig struct {
URI string
Password string
Db int
}