1.2.0
This commit is contained in:
parent
e09830024d
commit
07ab3b8b92
49
README.md
49
README.md
|
@ -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
|
||||
|
||||
|
|
151
conf/example.yml
151
conf/example.yml
|
@ -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.
|
|
@ -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
|
||||
}
|
|
@ -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
88
main.go
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
13
response.go
13
response.go
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
55
structs.go
55
structs.go
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue