1
0
Fork 0

Compare commits

..

No commits in common. "master" and "v23.17.0" have entirely different histories.

32 changed files with 758 additions and 428 deletions

3
.gitignore vendored
View File

@ -1,5 +1,4 @@
bin/*
!bin/.keep
.vscode
*.jade.go
jade.go
*.jade.go

View File

@ -5,44 +5,52 @@ SYSDDIR_=${shell pkg-config systemd --variable=systemdsystemunitdir}
SYSDDIR=${SYSDDIR_:/%=%}
DESTDIR=/
VERSION=23.34.0
VERSION=23.17.0
FLAGS=-trimpath -mod=readonly -modcacherw
LDFLAGS=-ldflags "-s -w -X main.version=${VERSION}" -tags osusergo,netgo
all: web/*.jade.go ${TARGET}
all: ${TARGET}
.PHONY: ${TARGET}
${TARGET}:
go build -o bin/$@ ${FLAGS} ${LDFLAGS} cmd/$@/main.go
go build -o bin/$@-clean ${FLAGS} ${LDFLAGS} cmd/$@-clean/main.go
web/*.jade.go: web/templates/*.jade
go install github.com/Joker/jade/cmd/jade@latest
go generate web/web.go
go build -o bin/$@ ${LDFLAGS} cmd/$@/main.go
go build -o bin/$@-clean ${LDFLAGS} cmd/$@-clean/main.go
install-jade:
go install github.com/Joker/jade/cmd/jade@latest
run:
bin/${TARGET} -conf configs/config.yaml
run-clean:
bin/${TARGET}-clean -conf configs/config.yaml
install:
install -Dm 0755 bin/${TARGET} ${DESTDIR}usr/bin/${TARGET}
install -Dm 0755 bin/${TARGET}-clean ${DESTDIR}usr/bin/${TARGET}-clean
install -Dm 0755 tools/gen-salt.sh ${DESTDIR}usr/bin/${TARGET}-gen-salt
install -Dm 0644 configs/config.yaml ${DESTDIR}etc/dwelling/upload.yaml
install -Dm 0644 configs/logrotate ${DESTDIR}etc/logrotate.d/${TARGET}
install -Dm 0644 LICENSE ${DESTDIR}usr/share/licenses/${TARGET}/LICENSE
install -Dm 0644 init/systemd/${TARGET}.service ${DESTDIR}${SYSDDIR}/${TARGET}.service
install -Dm 0644 init/systemd/${TARGET}-clean.timer ${DESTDIR}${SYSDDIR}/${TARGET}-clean.timer
install -Dm 0644 init/systemd/${TARGET}-clean.service ${DESTDIR}${SYSDDIR}/${TARGET}-clean.service
install -Dm 0644 build/dwelling-upload.conf ${DESTDIR}usr/lib/sysusers.d/dwelling-upload.conf
uninstall:
rm ${DESTDIR}usr/bin/${TARGET}
rm ${DESTDIR}usr/bin/${TARGET}-clean
rm ${DESTDIR}usr/bin/${TARGET}-gen-salt
rm ${DESTDIR}etc/logrotate.d/${TARGET}
rm ${DESTDIR}usr/share/licenses/${TARGET}/LICENSE
rm ${DESTDIR}${SYSDDIR}/${TARGET}.service
rm ${DESTDIR}${SYSDDIR}/${TARGET}-clean.timer
rm ${DESTDIR}${SYSDDIR}/${TARGET}-clean.service
rm ${DESTDIR}usr/lib/sysusers.d/dwelling-upload.conf
clean:
rm -f web/*.jade.go
go clean

View File

@ -1,6 +1,6 @@
# Maintainer: Alexander "Arav" Andreev <me@arav.su>
pkgname=dwelling-upload
pkgver=23.34.0
pkgver=23.17.0
pkgrel=1
pkgdesc="Arav's Dwelling / Upload"
arch=('i686' 'x86_64' 'arm' 'armv6h' 'armv7h' 'aarch64')
@ -9,16 +9,19 @@ license=('MIT')
makedepends=('go>=1.17')
provides=('dwelling-upload')
conflicts=('dwelling-upload')
backup=('etc/dwelling/upload.yaml')
source=("https://git.arav.su/Arav/dwelling-upload/archive/v${pkgver}.tar.gz")
md5sums=('SKIP')
build() {
cd "$srcdir/$pkgname"
export GOPATH="$srcdir"/gopath
export CGO_CPPFLAGS="${CPPFLAGS}"
export CGO_CFLAGS="${CFLAGS}"
export CGO_CXXFLAGS="${CXXFLAGS}"
export CGO_LDFLAGS="${LDFLAGS}"
if [ ! -f "$(go env GOPATH)/bin/jade" ]; then
make VERSION=$pkgver DESTDIR="$pkgdir/" install-jade
fi
make VERSION=$pkgver DESTDIR="$pkgdir/"
}

View File

@ -0,0 +1,4 @@
# sysusers.d
g dwupload - -
u dwupload - -
m dwupload dwupload

View File

@ -1,48 +1,70 @@
package main
import (
"dwelling-upload/internal/configuration"
"dwelling-upload/pkg/logging"
"dwelling-upload/pkg/utils"
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"path"
"time"
)
var (
uploadDir = flag.String("dir", "/srv/upload", "path to a directory where uploaded files are stored")
expiry = flag.Duration("expiry", 36*time.Hour, "keep files for this much hours")
showVersion = flag.Bool("v", false, "show version")
)
var configPath *string = flag.String("conf", "config.yaml", "path to configuration file")
var showVersion *bool = flag.Bool("v", false, "show version")
var version string
func main() {
flag.Parse()
log.SetFlags(log.Llongfile)
if *showVersion {
fmt.Println("dwelling-upload-clean Ver. ", version, "\nCopyright (c) 2022,2023 Alexander \"Arav\" Andreev <me@arav.su>")
return
}
uploadsDir, err := os.ReadDir(*uploadDir)
config, err := configuration.LoadConfiguration(*configPath)
if err != nil {
log.Fatalf("failed to open a directory %s: %s\n", *uploadDir, err)
log.Fatalln(err)
}
for _, entry := range uploadsDir {
file, err := os.Stat(path.Join(*uploadDir, entry.Name()))
if err != nil {
log.Printf("failed to stat a file %s: %s", entry.Name(), err)
continue
}
logErr, err := logging.New(config.Log.CleanError)
if err != nil {
log.Fatalln("failed to open error logger:", err)
}
defer logErr.Close()
if time.Now().UTC().Sub(file.ModTime().UTC()) >= *expiry {
if err := os.Remove(path.Join(*uploadDir, entry.Name())); err != nil {
log.Printf("failed to remove a file %s: %s", entry.Name(), err)
logClean, err := logging.New(config.Log.Clean)
if err != nil {
log.Fatalln("failed to open error logger:", err)
}
defer logClean.Close()
uploadsDir, err := ioutil.ReadDir(config.Uploads.Directory)
if err != nil {
logErr.Fatalf("failed to open directory %s: %s\n", config.Uploads.Directory, err)
}
var deletedCount int64 = 0
var deletedSize int64 = 0
for _, entry := range uploadsDir {
if time.Now().UTC().Sub(entry.ModTime().UTC()) >= time.Duration(config.Uploads.Limits.KeepForHours)*time.Hour {
if err := os.Remove(path.Join(config.Uploads.Directory, entry.Name())); err != nil {
logErr.Println("failed to remove file ", entry.Name(), ": ", err)
} else {
deletedSize += entry.Size()
deletedCount++
}
}
}
_, _, cFSz := utils.ConvertFileSize(deletedSize)
if deletedCount > 0 {
logClean.Printf("%d %s", deletedCount, cFSz)
}
}

View File

@ -1,120 +1,128 @@
package main
import (
duihttp "dwelling-upload/internal/http"
"dwelling-upload/internal/configuration"
"dwelling-upload/internal/http"
"dwelling-upload/pkg/logging"
"dwelling-upload/pkg/utils"
"dwelling-upload/pkg/watcher"
"dwelling-upload/web"
"flag"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"path"
"syscall"
"git.arav.su/Arav/httpr"
)
var (
listenAddress = flag.String("listen", "/var/run/dwelling-upload/sock", "listen address (ip:port|unix_path)")
uploadDir = flag.String("dir", "/srv/upload", "directory where uploaded files are stored")
expiry = flag.Int("expiry", 36, "keep files for this much hours")
storageSize = flag.Int64("storage", 102400, "storage size in MiB for uploads")
fileSize = flag.Int64("file", 128, "max. size in MiB for files")
showVersion *bool = flag.Bool("v", false, "show version")
)
var configPath *string = flag.String("conf", "config.yaml", "path to configuration file")
var showVersion *bool = flag.Bool("v", false, "show version")
var version string
func main() {
flag.Parse()
log.SetFlags(log.Llongfile)
if *showVersion {
fmt.Println("dwelling-upload Ver. ", version, "\nCopyright (c) 2022,2023 Alexander \"Arav\" Andreev <me@arav.su>")
return
}
hashSalt, err := os.ReadFile(path.Join(os.Getenv("CREDENTIALS_DIRECTORY"), "salt"))
config, err := configuration.LoadConfiguration(*configPath)
if err != nil {
log.Fatalln("failed to read hash salt file:", err)
log.Fatalln(err)
}
logFilePath := path.Join(os.Getenv("LOGS_DIRECTORY"), "file.log")
logFileFd, err := os.OpenFile(logFilePath, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0660)
if err != nil {
log.Fatalln("failed to open file.log:", err)
}
defer logFileFd.Close()
logFile := log.New(logFileFd, "", log.LstdFlags)
uploadDirSize, err := utils.DirectorySize(*uploadDir)
if err != nil {
log.Fatalf("failed to get initial size of %s: %s", *uploadDir, err)
}
hand := duihttp.NewUploadHandlers(logFile, *uploadDir, &uploadDirSize, string(hashSalt),
*expiry, *storageSize, *fileSize)
r := httpr.New()
r.NotFoundHandler = func(w http.ResponseWriter, r *http.Request) {
duihttp.Error(w, r, "", http.StatusNotFound)
}
r.Handler(http.MethodGet, "/", hand.Index)
r.Handler(http.MethodPost, "/", hand.Upload)
r.Handler(http.MethodGet, "/:hash/:name", hand.Download)
r.Handler(http.MethodPost, "/delete", hand.Delete)
r.Handler(http.MethodDelete, "/:hash", hand.Delete)
r.ServeStatic("/assets/*filepath", web.Assets())
r.Handler(http.MethodGet, "/robots.txt", duihttp.RobotsTxt)
r.Handler(http.MethodGet, "/favicon.svg", duihttp.Favicon)
srv := duihttp.NewHttpServer(r)
if err := srv.Start(*listenAddress); err != nil {
log.Fatalln("failed to start a server:", err)
}
defer func() {
if err := srv.Stop(); err != nil {
log.Fatalln("failed to properly shutdown a server:", err)
if typ, addr := config.SplitNetworkAddress(); typ == "unix" {
os.Remove(addr)
}
}()
doneSignal := make(chan os.Signal, 1)
signal.Notify(doneSignal, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
logErr, err := logging.New(config.Log.Error)
if err != nil {
log.Fatalln("error logger:", err)
}
defer logErr.Close()
logUpload, err := logging.New(config.Log.Upload)
if err != nil {
log.Fatalln("upload logger:", err)
}
defer logUpload.Close()
logDownload, err := logging.New(config.Log.Download)
if err != nil {
log.Fatalln("download logger:", err)
}
defer logDownload.Close()
logDelete, err := logging.New(config.Log.Delete)
if err != nil {
log.Fatalln("delete logger:", err)
}
defer logDelete.Close()
watcha, err := watcher.NewInotifyWatcher()
if err != nil {
log.Fatalln(err)
logErr.Fatalln(err)
}
defer watcha.Close()
if err := watcha.AddWatch(*uploadDir, watcher.CrDelMask); err != nil {
log.Fatalln(err)
if err := watcha.AddWatch(config.Uploads.Directory, watcher.CrDelMask); err != nil {
logErr.Fatalln(err)
}
uploadDirNotify := make(chan uint32)
go watcha.WatchForMask(uploadDirNotify, watcher.CrDelMask)
uploadDirSize, err := utils.DirectorySize(config.Uploads.Directory)
if err != nil {
logErr.Fatalf("failed to get initial size of %s: %s", config.Uploads.Directory, err)
}
watcha.WatchForMask(uploadDirNotify, watcher.CrDelMask)
hand := http.NewUploadHandlers(config, logErr, logUpload, logDownload, logDelete, &uploadDirSize)
srv := http.NewHttpServer()
srv.SetNotFoundHandler(hand.NotFound)
srv.ServeStatic("/assets/*filepath", hand.AssetsFS())
srv.GET("/", hand.Index)
srv.POST("/", hand.Upload)
srv.POST("/delete", hand.Delete)
srv.GET("/f/:hash/:name", hand.Download)
srv.DELETE("/:hash", hand.Delete)
if err := srv.Start(config.SplitNetworkAddress()); err != nil {
logErr.Fatalln("failed to start a server:", err)
}
doneSignal := make(chan os.Signal, 1)
signal.Notify(doneSignal, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
logReopenSignal := make(chan os.Signal, 1)
signal.Notify(logReopenSignal, syscall.SIGHUP)
go func() {
for {
select {
case <-logReopenSignal:
logErr.Reopen(config.Log.Error)
logUpload.Reopen(config.Log.Upload)
logDownload.Reopen(config.Log.Download)
case <-uploadDirNotify:
uploadDirSize, err = utils.DirectorySize(*uploadDir)
sz, err := utils.DirectorySize(config.Uploads.Directory)
if err != nil {
log.Println("failed to get uploads directory size:", err)
logErr.Println("failed to get uploads directory size:", err)
}
if sz > 0 {
uploadDirSize = sz
}
}
}
}()
<-doneSignal
if err := srv.Stop(); err != nil {
logErr.Fatalln("failed to properly shutdown a server:", err)
}
}

27
configs/config.yaml Normal file
View File

@ -0,0 +1,27 @@
# Sets network type (could be tcp{,4,6}, unix)
# and address:port or /path/to/unix.sock to
# listen on.
listen_on: "unix /var/run/dwelling-upload/sock"
# Salt for hash of uploaded files.
# Aim is to make links bruteforcing useless.
hash_salt: "iyP3oZWHI3xO3XBF7s78Vg"
# Logging options.
log:
# dwelling-upload logs.
error: "/var/log/dwelling-upload/error.log"
upload: "/var/log/dwelling-upload/upload.log"
download: "/var/log/dwelling-upload/download.log"
delete: "/var/log/dwelling-upload/delete.log"
# dwelling-upload-clean logs.
clean: "/var/log/dwelling-upload/clean.log"
clean_error: "/var/log/dwelling-upload/clean_error.log"
uploads:
# Path where to put uploaded files.
directory: "/srv/uploads"
limits:
# Maximum size of a file in MiB.
file_size: 128
# Amount of hours file will be keeped for.
keep_for_hours: 48
# Maximum size of a whole storage in MiB.
storage: 204800

View File

@ -1,12 +1,15 @@
/var/log/dwelling-upload/*log {
nocreate
copytruncate
size 1M
compress
compresscmd /usr/bin/zstd
compressext .zst
compressoptions -T0 --long -15
uncompresscmd /usr/bin/unzstd
sharedscripts
missingok
notifempty
postrotate
/bin/pkill -HUP dwelling-upload
endscript
}

View File

@ -1,22 +1,22 @@
server {
listen 443 ssl http2;
listen 8094; # Tor I2P
# listen 8094; # Tor
listen 127.0.0.1:8114; # I2P
listen [300:a98d:d6d0:8a08::c]:80; # Yggdrasil
server_name upload.arav.su upload.arav.i2p 4usftbmjpfexkr2x5xbp5ukmygpmg4fgrnx2wbifsexqctooz5hmviyd.onion;
server_name upload.arav.su upload.arav.i2p;
access_log /var/log/nginx/dwelling/upload.log main if=$nolog;
ssl_certificate /etc/letsencrypt/live/arav.su/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/arav.su/privkey.pem;
add_header Content-Security-Policy "default-src 'self'; script-src 'none'; style-src 'self'; img-src 'self'; media-src 'self'; object-src 'none'; frame-src 'none'; frame-ancestors 'none'; font-src 'self'; form-action 'self'";
add_header Content-Security-Policy "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self'; media-src 'self'; object-src 'none'; frame-src 'none'; frame-ancestors 'none'; font-src 'self'; form-action 'self'";
add_header X-Frame-Options "DENY";
add_header X-Content-Type-Options "nosniff";
add_header X-XSS-Protection "1; mode=block";
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload";
add_header Onion-Location "http://4usftbmjpfexkr2x5xbp5ukmygpmg4fgrnx2wbifsexqctooz5hmviyd.onion$request_uri";
# add_header Onion-Location "http://.onion$request_uri";
location / {

8
go.mod
View File

@ -2,6 +2,8 @@ module dwelling-upload
go 1.17
require github.com/pkg/errors v0.9.1
require git.arav.su/Arav/httpr v0.3.1
require (
github.com/julienschmidt/httprouter v1.3.0
github.com/pkg/errors v0.9.1
gopkg.in/yaml.v3 v3.0.1
)

8
go.sum
View File

@ -1,4 +1,8 @@
git.arav.su/Arav/httpr v0.3.1 h1:8ba90SJ4XYUWfIlC3V0Zuw3+CcOb9IYVkOZ/2mB9JO0=
git.arav.su/Arav/httpr v0.3.1/go.mod h1:z0SVYwe5dBReeVuFU9QH2PmBxICJwchxqY5OfZbeVzU=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -3,8 +3,9 @@ Description=dwelling-upload-clean
[Service]
Type=oneshot
DynamicUser=yes
ExecStart=/usr/bin/dwelling-upload-clean -dir /srv/upload -expiry 36h
User=dwupload
Group=dwupload
ExecStart=/usr/bin/dwelling-upload-clean -conf /etc/dwelling/upload.yaml
ReadOnlyPaths=/
# Set here path to directory where uploads are stored.
@ -12,6 +13,8 @@ ReadWritePaths=/srv/upload
NoExecPaths=/
ExecPaths=/usr/bin/dwelling-upload-clean
LogsDirectory=dwelling-upload
AmbientCapabilities=
CapabilityBoundingSet=
@ -19,33 +22,18 @@ LockPersonality=true
MemoryDenyWriteExecute=true
NoNewPrivileges=true
PrivateDevices=true
PrivateTmp=true
PrivateUsers=true
ProcSubset=pid
ProtectClock=true
ProtectControlGroups=true
ProtectHome=true
ProtectHostname=true
ProtectKernelLogs=true
ProtectKernelModules=true
ProtectKernelTunables=true
ProtectProc=noaccess
ProtectSystem=strict
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
RestrictAddressFamilies=
RestrictNamespaces=true
RestrictRealtime=true
RestrictSUIDSGID=true
SystemCallArchitectures=native
SystemCallFilter=~@clock
SystemCallFilter=~@cpu-emulation
SystemCallFilter=~@debug
SystemCallFilter=~@module
SystemCallFilter=~@mount
SystemCallFilter=~@obsolete
SystemCallFilter=~@privileged
SystemCallFilter=~@raw-io
SystemCallFilter=~@reboot
SystemCallFilter=~@swap
[Install]
WantedBy=multi-user.target

View File

@ -5,9 +5,9 @@ After=network.target
[Service]
Type=simple
Restart=on-failure
DynamicUser=yes
ExecStart=/usr/bin/dwelling-upload -listen /var/run/dwelling-upload/sock \
-dir /srv/upload -expiry 36 -storage 102400 -file 128
User=dwupload
Group=dwupload
ExecStart=/usr/bin/dwelling-upload -conf /etc/dwelling/upload.yaml
ReadOnlyPaths=/
# Set here path to directory where uploads are stored.
@ -18,9 +18,6 @@ ExecPaths=/usr/bin/dwelling-upload
RuntimeDirectory=dwelling-upload
LogsDirectory=dwelling-upload
# Use gen-salt.sh to generate salt! It will create / append to an override.conf.
SetCredentialEncrypted=
AmbientCapabilities=
CapabilityBoundingSet=
@ -28,33 +25,18 @@ LockPersonality=true
MemoryDenyWriteExecute=true
NoNewPrivileges=true
PrivateDevices=true
PrivateTmp=true
PrivateUsers=true
ProcSubset=pid
ProtectClock=true
ProtectControlGroups=true
ProtectHome=true
ProtectHostname=true
ProtectKernelLogs=true
ProtectKernelModules=true
ProtectKernelTunables=true
ProtectProc=noaccess
ProtectSystem=strict
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
RestrictNamespaces=true
RestrictRealtime=true
RestrictSUIDSGID=true
SystemCallArchitectures=native
SystemCallFilter=~@clock
SystemCallFilter=~@cpu-emulation
SystemCallFilter=~@debug
SystemCallFilter=~@module
SystemCallFilter=~@mount
SystemCallFilter=~@obsolete
SystemCallFilter=~@privileged
SystemCallFilter=~@raw-io
SystemCallFilter=~@reboot
SystemCallFilter=~@swap
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,56 @@
package configuration
import (
"os"
"strings"
"github.com/pkg/errors"
"gopkg.in/yaml.v3"
)
// Configuration holds a list of process names to be tracked and a listen address.
type Configuration struct {
ListenOn string `yaml:"listen_on"`
HashSalt string `yaml:"hash_salt"`
User string `yaml:"user"`
Log struct {
Error string `yaml:"error"`
Upload string `yaml:"upload"`
Download string `yaml:"download"`
Delete string `yaml:"delete"`
Clean string `yaml:"clean"`
CleanError string `yaml:"clean_error"`
} `yaml:"log"`
Uploads struct {
Directory string `yaml:"directory"`
Limits struct {
FileSize int64 `yaml:"file_size"`
KeepForHours int `yaml:"keep_for_hours"`
Storage int64 `yaml:"storage"`
} `yaml:"limits"`
} `yaml:"uploads"`
}
func LoadConfiguration(path string) (*Configuration, error) {
configFile, err := os.Open(path)
if err != nil {
return nil, errors.Wrap(err, "failed to open configuration file")
}
defer configFile.Close()
config := &Configuration{}
if err := yaml.NewDecoder(configFile).Decode(config); err != nil {
return nil, errors.Wrap(err, "failed to parse configuration file")
}
return config, nil
}
// SplitNetworkAddress splits ListenOn option and returns as two strings
// network type (e.g. tcp, unix, udp) and address:port or /path/to/prog.socket
// to listen on.
func (c *Configuration) SplitNetworkAddress() (string, string) {
s := strings.Split(c.ListenOn, " ")
return s[0], s[1]
}

View File

@ -2,109 +2,111 @@ package http
import (
"crypto/sha256"
"dwelling-upload/internal/configuration"
"dwelling-upload/pkg/logging"
"dwelling-upload/pkg/utils"
"dwelling-upload/web"
"encoding/base64"
"encoding/hex"
"fmt"
"io"
"log"
"net/http"
"net/url"
"os"
"path"
"strings"
"time"
"git.arav.su/Arav/httpr"
)
type UploadHandlers struct {
logFile *log.Logger
conf *configuration.Configuration
logErr *logging.Logger
logUpload *logging.Logger
logDownload *logging.Logger
logDelete *logging.Logger
uploadDir string
uploadDirSize *int64
hashSalt string
keepForHours int
limitStorage int64
limitFileSize int64
}
func NewUploadHandlers(lFile *log.Logger, uploadDir string, uploadDirSize *int64,
hashSalt string, keepForHours int, limStorage, limFileSz int64) *UploadHandlers {
func NewUploadHandlers(conf *configuration.Configuration, lErr, lUp, lDown, lDel *logging.Logger, uploadDirSize *int64) *UploadHandlers {
return &UploadHandlers{
logFile: lFile,
uploadDir: uploadDir,
uploadDirSize: uploadDirSize,
hashSalt: hashSalt,
keepForHours: keepForHours,
limitStorage: limStorage,
limitFileSize: limFileSz}
conf: conf,
logErr: lErr,
logUpload: lUp,
logDownload: lDown,
logDelete: lDel,
uploadDirSize: uploadDirSize}
}
func (*UploadHandlers) AssetsFS() http.FileSystem {
return web.Assets()
}
func (h *UploadHandlers) Index(w http.ResponseWriter, r *http.Request) {
var storCapacity int64 = h.limitStorage << 20
var fMaxSize int64 = h.limitFileSize << 20
var storCapacity int64 = h.conf.Uploads.Limits.Storage << 20
var fMaxSize int64 = h.conf.Uploads.Limits.FileSize << 20
_, _, capStr := utils.ConvertFileSize(storCapacity)
_, _, usedStr := utils.ConvertFileSize(*h.uploadDirSize)
_, _, availStr := utils.ConvertFileSize(storCapacity - *h.uploadDirSize)
_, _, fMaxSzStr := utils.ConvertFileSize(fMaxSize)
web.Index(utils.MainSite(r.Host), h.keepForHours, fMaxSzStr, availStr, w)
web.Index(utils.MainSite(r.Host), storCapacity, *h.uploadDirSize, h.conf.Uploads.Limits.KeepForHours, fMaxSzStr, usedStr, capStr, availStr, w)
}
func (h *UploadHandlers) Upload(w http.ResponseWriter, r *http.Request) {
var fMaxSizeBytes int64 = h.limitFileSize << 20
var storCapacity int64 = h.limitStorage << 20
var fMaxSizeBytes int64 = h.conf.Uploads.Limits.FileSize << 20
var storCapacity int64 = h.conf.Uploads.Limits.Storage << 20
r.Body = http.MaxBytesReader(w, r.Body, fMaxSizeBytes)
if err := r.ParseMultipartForm(fMaxSizeBytes); err != nil {
log.Println("failed to parse upload form:", err)
Error(w, r, "Failed to parse upload form.", http.StatusExpectationFailed)
h.logErr.Println("failed to parse form:", err)
http.Error(w, err.Error(), http.StatusExpectationFailed)
return
}
f, fHandler, err := r.FormFile("file")
if err != nil {
log.Println("failed to open incoming file:", err)
Error(w, r, "Error reading an incoming file.", http.StatusInternalServerError)
h.logErr.Println("failed to open incoming file:", err)
w.WriteHeader(http.StatusInternalServerError)
http.Error(w, "cannot read incoming file", http.StatusInternalServerError)
return
}
defer os.Remove(fHandler.Filename)
defer f.Close()
defer func() {
os.Remove(fHandler.Filename)
f.Close()
}()
var leftSpace int64 = storCapacity - *h.uploadDirSize
if leftSpace < fHandler.Size {
log.Println("not enough space left in storage, only", leftSpace>>20, "MiB left")
Error(w, r, "Not enough space left, sorry.", http.StatusInternalServerError)
return
h.logErr.Println("not enough space left in storage, only", leftSpace>>20, "MiB left")
w.WriteHeader(http.StatusInternalServerError)
web.ErrorNoSpace(utils.MainSite(r.Host), w)
}
s256 := sha256.New()
if _, err := io.Copy(s256, f); err != nil {
log.Println("failed to compute a SHA-256 hash:", err)
Error(w, r, "A hash for the file cannot be computed.", http.StatusInternalServerError)
h.logErr.Println("failed to compute SHA-256 hash:", err)
http.Error(w, "cannot compute hash for a file", http.StatusInternalServerError)
return
}
fHash := hex.EncodeToString(s256.Sum(nil))
s256.Write([]byte(h.hashSalt))
s256.Write([]byte(time.Now().String()))
s256.Write([]byte(h.conf.HashSalt))
fSaltedHash := base64.RawURLEncoding.EncodeToString(s256.Sum(nil))
f.Seek(0, io.SeekStart)
fPath := path.Join(h.uploadDir, fSaltedHash)
fPath := path.Join(h.conf.Uploads.Directory, fSaltedHash)
_, err = os.Stat(fPath)
if os.IsNotExist(err) {
fDst, err := os.Create(fPath)
if err != nil {
log.Println("failed to open file for writing", err)
Error(w, r, "File cannot be written.", http.StatusInternalServerError)
h.logErr.Println("failed to open file for writing", err)
http.Error(w, "cannot create your file", http.StatusInternalServerError)
return
}
defer fDst.Close()
@ -117,20 +119,17 @@ func (h *UploadHandlers) Upload(w http.ResponseWriter, r *http.Request) {
fDst.Write([]byte{0})
fDst.Seek(0, io.SeekStart)
if _, err = io.Copy(fDst, f); err != nil {
log.Println("failed to copy uploaded file to destination:", err)
Error(w, r, "Failed to copy uploaded file to the storage.", http.StatusInternalServerError)
_, err = io.Copy(fDst, f)
if err != nil {
h.logErr.Println("failed to copy uploaded file to destination:", err)
http.Error(w, "cannot copy file's content", http.StatusInternalServerError)
return
}
typ, _ := utils.NetworkType(r.Host)
ip := r.Header.Get("X-Real-IP")
if typ != "www" && typ != "ygg" {
ip = ""
}
h.logFile.Printf("| up | %s | %s | %s | SHA256 %s | %s | %d | %s",
ip, typ, fHandler.Filename, fHash, fSaltedHash, fHandler.Size, r.UserAgent())
h.logUpload.Printf("| %s | %s | %s | SHA256 %s | %s | %d | %s", r.Header.Get("X-Real-IP"), typ,
fHandler.Filename, fHash, fSaltedHash, fHandler.Size, r.UserAgent())
w.WriteHeader(http.StatusCreated)
} else {
@ -138,50 +137,46 @@ func (h *UploadHandlers) Upload(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusFound)
}
downloadURL := path.Join("/", fSaltedHash, fHandler.Filename)
downloadURL := path.Join("/f", fSaltedHash, fHandler.Filename)
downloadURLParsed, _ := url.Parse(downloadURL)
_, scheme := utils.NetworkType(r.Host)
site := scheme + "://" + r.Host
if strings.Contains(r.UserAgent(), "curl") || strings.Contains(r.UserAgent(), "Wget") {
fmt.Fprintln(w, site+downloadURLParsed.String(), "will be kept for", h.keepForHours)
} else {
web.Uploaded(utils.MainSite(r.Host), site, downloadURLParsed.String(), h.keepForHours, w)
}
}
func (h *UploadHandlers) Download(w http.ResponseWriter, r *http.Request) {
saltedHash := httpr.Param(r, "hash")
path := path.Join(h.uploadDir, saltedHash)
stat, err := os.Stat(path)
if os.IsNotExist(err) {
Error(w, r, "", http.StatusNotFound)
if strings.Contains(r.UserAgent(), "curl") {
w.Write([]byte(site + downloadURLParsed.String() + "\r\n"))
return
}
name := httpr.Param(r, "name")
web.Uploaded(utils.MainSite(r.Host), site, downloadURLParsed.String(), h.conf.Uploads.Limits.KeepForHours, w)
}
func (h *UploadHandlers) Download(w http.ResponseWriter, r *http.Request) {
saltedHash := GetURLParam(r, "hash")
path := path.Join(h.conf.Uploads.Directory, saltedHash)
stat, err := os.Stat(path)
if os.IsNotExist(err) {
h.NotFound(w, r)
return
}
name := GetURLParam(r, "name")
w.Header().Add("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", name))
fd, err := os.Open(path)
if err != nil {
log.Println("failed to open file to read:", err)
Error(w, r, "Failed to open file to read.", http.StatusInternalServerError)
h.logErr.Println("failed to open file to read:", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
defer fd.Close()
typ, _ := utils.NetworkType(r.Host)
ip := r.Header.Get("X-Real-IP")
if typ != "www" && typ != "ygg" {
ip = ""
}
netTyp, _ := utils.NetworkType(r.Host)
h.logFile.Printf("| dw | %s | %s | %s | %s | %s",
ip, typ, name, saltedHash, r.UserAgent())
h.logDownload.Printf("| %s | %s | %s | %s | %s", r.Header.Get("X-Real-IP"), netTyp, name, saltedHash, r.UserAgent())
http.ServeContent(w, r, path, stat.ModTime(), fd)
}
@ -189,57 +184,39 @@ func (h *UploadHandlers) Download(w http.ResponseWriter, r *http.Request) {
func (h *UploadHandlers) Delete(w http.ResponseWriter, r *http.Request) {
var saltedHash string
if r.Method == "DELETE" {
saltedHash = httpr.Param(r, "hash")
saltedHash = GetURLParam(r, "hash")
} else {
r.ParseForm()
saltedHash = r.FormValue("hash")
}
path := path.Join(h.uploadDir, saltedHash)
path := path.Join(h.conf.Uploads.Directory, saltedHash)
if _, err := os.Stat(path); os.IsNotExist(err) {
Error(w, r, "", http.StatusNotFound)
_, err := os.Stat(path)
if os.IsNotExist(err) {
h.NotFound(w, r)
return
}
if err := os.Remove(path); err != nil {
log.Println("failed to remove a file:", err)
Error(w, r, "Failed to remove a file.", http.StatusInternalServerError)
err = os.Remove(path)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, err)
return
}
typ, _ := utils.NetworkType(r.Host)
ip := r.Header.Get("X-Real-IP")
if typ != "www" && typ != "ygg" {
ip = ""
}
netTyp, _ := utils.NetworkType(r.Host)
h.logFile.Printf("| dt | %s | %s | %s | %s",
ip, typ, saltedHash, r.UserAgent())
h.logDelete.Printf("| %s | %s | %s | %s", r.Header.Get("X-Real-IP"), netTyp, saltedHash, r.UserAgent())
if strings.Contains(r.UserAgent(), "curl") || strings.Contains(r.UserAgent(), "Wget") {
fmt.Fprintln(w, "File was successfully deleted.")
w.WriteHeader(http.StatusNoContent)
}
func (h *UploadHandlers) NotFound(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
if !strings.Contains(r.UserAgent(), "curl") {
web.Error404(utils.MainSite(r.Host), w)
} else {
web.Deleted(utils.MainSite(r.Host), w)
fmt.Fprintln(w, "deleted")
}
}
func Error(w http.ResponseWriter, r *http.Request, reason string, code int) {
if strings.Contains(r.UserAgent(), "curl") || strings.Contains(r.UserAgent(), "Wget") {
http.Error(w, reason, code)
return
}
w.WriteHeader(code)
web.ErrorXXX(utils.MainSite(r.Host), code, reason, w)
}
func RobotsTxt(w http.ResponseWriter, r *http.Request) {
data, _ := web.AssetsGetFile("robots.txt")
w.Write(data)
}
func Favicon(w http.ResponseWriter, r *http.Request) {
data, _ := web.AssetsGetFile("img/favicon.svg")
w.Write(data)
}

View File

@ -5,41 +5,55 @@ import (
"log"
"net"
"net/http"
"net/netip"
"os"
"strings"
"time"
"github.com/julienschmidt/httprouter"
)
type HttpServer struct {
s http.Server
addr net.Addr
server *http.Server
router *httprouter.Router
}
func NewHttpServer(r http.Handler) *HttpServer {
return &HttpServer{s: http.Server{
ReadTimeout: 3 * time.Second,
WriteTimeout: 3 * time.Second,
Handler: r}}
}
func (s *HttpServer) Start(address string) error {
var network string
if !strings.ContainsRune(address, ':') {
network = "unix"
} else {
ap, err := netip.ParseAddrPort(address)
if err != nil {
return err
}
if ap.Addr().Is4() {
network = "tcp4"
} else if ap.Addr().Is6() {
network = "tcp6"
}
func NewHttpServer() *HttpServer {
r := httprouter.New()
return &HttpServer{
server: &http.Server{
ReadTimeout: 3 * time.Second,
WriteTimeout: 3 * time.Second,
Handler: r,
},
router: r,
}
}
func (s *HttpServer) GET(path string, handler http.HandlerFunc) {
s.router.Handler(http.MethodGet, path, handler)
}
func (s *HttpServer) POST(path string, handler http.HandlerFunc) {
s.router.Handler(http.MethodPost, path, handler)
}
func (s *HttpServer) DELETE(path string, handler http.HandlerFunc) {
s.router.Handler(http.MethodDelete, path, handler)
}
func (s *HttpServer) ServeStatic(path string, fsys http.FileSystem) {
s.router.ServeFiles(path, fsys)
}
func (s *HttpServer) SetNotFoundHandler(handler http.HandlerFunc) {
s.router.NotFound = handler
}
// GetURLParam wrapper around underlying router for getting URL parameters.
func GetURLParam(r *http.Request, param string) string {
return httprouter.ParamsFromContext(r.Context()).ByName(param)
}
func (s *HttpServer) Start(network, address string) error {
listener, err := net.Listen(network, address)
if err != nil {
return err
@ -49,10 +63,8 @@ func (s *HttpServer) Start(address string) error {
os.Chmod(address, 0777)
}
s.addr = listener.Addr()
go func() {
if err = s.s.Serve(listener); err != nil && err != http.ErrServerClosed {
if err = s.server.Serve(listener); err != nil && err != http.ErrServerClosed {
log.Fatalln(err)
}
}()
@ -64,11 +76,7 @@ func (s *HttpServer) Stop() error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if s.addr.Network() == "unix" {
defer os.Remove(s.addr.String())
}
if err := s.s.Shutdown(ctx); err != nil {
if err := s.server.Shutdown(ctx); err != nil {
return err
}

96
pkg/logging/logger.go Normal file
View File

@ -0,0 +1,96 @@
package logging
import (
"fmt"
"io"
"os"
"strings"
"sync"
"time"
"github.com/pkg/errors"
)
type Logger struct {
sync.Mutex
file io.WriteCloser
}
// New creates a Logger instance with a given filename
func New(path string) (*Logger, error) {
f, err := os.OpenFile(path, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0660)
if err != nil {
return nil, errors.Wrap(err, "failed to open log file")
}
return &Logger{file: f}, nil
}
func (l *Logger) Reopen(path string) error {
l.Lock()
defer l.Unlock()
f, err := os.OpenFile(path, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0660)
if err != nil {
return err
}
l.file.Close()
l.file = f
return nil
}
func (l *Logger) Println(v ...interface{}) {
l.Lock()
defer l.Unlock()
nowStr := time.Now().UTC().Format(time.RFC3339)
fmt.Fprintln(l.file, nowStr, v)
}
func (l *Logger) Printf(format string, v ...interface{}) {
l.Lock()
defer l.Unlock()
// Ensure a new line will be written
if !strings.HasSuffix(format, "\n") {
format += "\n"
}
nowStr := time.Now().UTC().Format(time.RFC3339)
fmt.Fprintf(l.file, nowStr+" "+format, v...)
}
func (l *Logger) Fatalln(v ...interface{}) {
l.Lock()
nowStr := time.Now().UTC().Format(time.RFC3339)
fmt.Fprintln(l.file, nowStr, v)
l.file.Close()
os.Exit(1)
}
func (l *Logger) Fatalf(format string, v ...interface{}) {
l.Lock()
// Ensure a new line will be written
if !strings.HasSuffix(format, "\n") {
format += "\n"
}
nowStr := time.Now().UTC().Format(time.RFC3339)
fmt.Fprintf(l.file, nowStr+" "+format, v...)
l.file.Close()
os.Exit(1)
}
func (l *Logger) Close() error {
return l.file.Close()
}

View File

@ -25,8 +25,6 @@ func NetworkType(host string) (string, string) {
return "i2p", "http"
} else if strings.Contains(host, "onion") {
return "tor", "http"
} else if strings.Contains(host, "[300:") {
return "ygg", "http"
} else {
return "www", "https"
}

View File

@ -1,8 +1,8 @@
package utils
import (
"os"
"path"
"io/fs"
"path/filepath"
"strconv"
"strings"
@ -14,10 +14,10 @@ var sizeSuffixes = [...]string{"B", "KiB", "MiB", "GiB", "TiB"}
// ConvertFileSize converts size in bytes down to biggest units it represents.
// Returns converted size, unit and, a concatenation of size and unit
func ConvertFileSize(size int64) (float64, string, string) {
idx := 0
fSize := float64(size)
var idx int
var fSize float64 = float64(size)
for ; fSize >= 1024; fSize /= 1024 {
for idx = 0; fSize >= 1024; fSize /= 1024 {
idx++
}
@ -25,23 +25,23 @@ func ConvertFileSize(size int64) (float64, string, string) {
fSizeStr = strings.TrimRight(fSizeStr, "0")
fSizeStr = strings.TrimSuffix(fSizeStr, ".")
return fSize, sizeSuffixes[idx],
strings.Join([]string{fSizeStr, sizeSuffixes[idx]}, " ")
return fSize, sizeSuffixes[idx], strings.Join([]string{fSizeStr, sizeSuffixes[idx]}, " ")
}
func DirectorySize(dirPath string) (dirSz int64, err error) {
dir, err := os.ReadDir(dirPath)
if err != nil {
return 0, errors.Wrapf(err, "failed to compute %s directory size", dirPath)
}
for _, entry := range dir {
file, err := os.Stat(path.Join(dirPath, entry.Name()))
func DirectorySize(path string) (dirSz int64, err error) {
err = filepath.Walk(path, func(_ string, info fs.FileInfo, err error) error {
if err != nil {
return 0, errors.Wrapf(err, "failed to stat a file %s", entry.Name())
return errors.Wrapf(err, "failed to compute %s directory size", path)
}
dirSz += file.Size()
dirSz += info.Size()
return nil
})
if err != nil {
return 0, err
}
return dirSz, nil
return dirSz - 4096, nil
}

View File

@ -54,24 +54,26 @@ func (w *InotifyWatcher) AddWatch(path string, mask uint32) error {
// WatchForMask checks for events specified in `mask` and sends them
// to channel `fired`. See `man inotify` for events.
func (w *InotifyWatcher) WatchForMask(fired chan uint32, mask uint32) {
for !w.closed {
buffer := make([]byte, syscall.SizeofInotifyEvent*inotifyCount)
n, err := syscall.Read(w.fd, buffer)
if err != nil {
break
}
go func() {
for !w.closed {
buffer := make([]byte, syscall.SizeofInotifyEvent*inotifyCount)
n, err := syscall.Read(w.fd, buffer)
if err != nil {
break
}
if n < syscall.SizeofInotifyEvent {
continue
}
if n < syscall.SizeofInotifyEvent {
continue
}
for offset := 0; offset < len(buffer); offset += syscall.SizeofInotifyEvent {
event := (*syscall.InotifyEvent)(unsafe.Pointer(&buffer[offset]))
if event.Mask&mask > 0 {
fired <- event.Mask
for offset := 0; offset < len(buffer); offset += syscall.SizeofInotifyEvent {
event := (*syscall.InotifyEvent)(unsafe.Pointer(&buffer[offset]))
if event.Mask&mask > 0 {
fired <- event.Mask
}
}
}
}
}()
}
// Close removes all watchers, closes inotify descriptor and stops all

View File

@ -1,26 +0,0 @@
#!/usr/bin/sh
if [[ $EUID -ne 0 ]]; then
echo "Must run as root!" >&2;
exit 1
fi
# It will create a new encription key if it doesn't exists.
systemd-creds setup
if [[ $1 ]]; then
char_num=$1;
else
char_num=64;
fi
service_override=$(pkg-config systemd --variable=systemdsystemconfdir)/dwelling-upload.service.d/override.conf
if [ ! -f $service_override ]; then
echo "[Service]" > $service_override;
fi
cat /dev/urandom | tr -dc 'a-zA-Z0-9!@#$%^&*' | fold -w $char_num | head -n 1 \
| systemd-creds encrypt -qp --name=salt - - 2> /dev/null >> $service_override;
systemctl daemon-reload

View File

@ -33,8 +33,6 @@
background-color: var(--secondary-color);
color: var(--background-color); }
.center { text-align: center; }
a,
button {
color: var(--primary-color);
@ -52,8 +50,8 @@ input[type="file"] {
color: var(--text-color);
font: inherit; }
button,
input[type="file"]::file-selector-button {
input[type="file"]::file-selector-button,
button {
background: none;
border: none;
color: var(--primary-color);
@ -93,6 +91,18 @@ h2 {
small { font-size: .8rem; }
progress {
background-color: var(--secondary-color);
border: none;
color: var(--primary-color);
height: 1.1rem;
width: 30%; }
progress::-moz-progress-bar {
background-color: var(--primary-color); }
.center { text-align: center; }
html { margin-left: calc(100vw - 100%); }
body {
@ -109,22 +119,24 @@ header {
flex-wrap: wrap;
justify-content: space-between; }
header svg { width: 360px; }
#logo {
display: block;
width: 360px; }
header svg text { fill: var(--text-color); }
#logo text { fill: var(--text-color); }
header svg text:first-child {
#logo .logo {
font-size: 2rem;
font-variant-caps: small-caps;
font-weight: bold; }
header svg text:last-child { font-size: .88rem; }
@media screen and (-webkit-min-device-pixel-ratio:0) {
header svg text:first-child { font-size: 2.082rem; } }
#logo .logo { font-size: 2.082rem; } }
@-moz-document url-prefix() {
header svg text:first-child { font-size: 2rem; } }
#logo .logo { font-size: 2rem; } }
#logo .under { font-size: .88rem; }
nav { margin-top: .5rem; }
@ -136,13 +148,7 @@ nav h1 {
section { margin-top: 1rem; }
#error {
font-size: 3.5rem;
line-height: 5rem;
text-align: center;
margin: 6rem 0; }
#error h1 { font-size: 8rem; }
#used-space div span { margin: 0 .2rem; }
footer {
font-size: .8rem;
@ -152,12 +158,11 @@ footer {
@media screen and (max-width: 640px) {
header { display: block; }
header svg {
#logo {
margin: 0 auto;
width: 100%; }
nav {
width: 100%;
text-align: center; }
input[type="text"] { width: 100%; } }

View File

@ -1,3 +0,0 @@
User-agent: *
Allow: /$
Disallow: /

141
web/jade.go Normal file
View File

@ -0,0 +1,141 @@
// Code generated by "jade.go"; DO NOT EDIT.
package web
import (
"bytes"
"io"
"strconv"
)
var (
escaped = []byte{'<', '>', '"', '\'', '&'}
replacing = []string{"&lt;", "&gt;", "&#34;", "&#39;", "&amp;"}
)
func WriteEscString(st string, buffer *WriterAsBuffer) {
for i := 0; i < len(st); i++ {
if n := bytes.IndexByte(escaped, st[i]); n >= 0 {
buffer.WriteString(replacing[n])
} else {
buffer.WriteByte(st[i])
}
}
}
type WriterAsBuffer struct {
io.Writer
}
func (w *WriterAsBuffer) WriteString(s string) (n int, err error) {
n, err = w.Write([]byte(s))
return
}
func (w *WriterAsBuffer) WriteByte(b byte) (err error) {
_, err = w.Write([]byte{b})
return
}
type stringer interface {
String() string
}
func WriteAll(a interface{}, escape bool, buffer *WriterAsBuffer) {
switch v := a.(type) {
case string:
if escape {
WriteEscString(v, buffer)
} else {
buffer.WriteString(v)
}
case int:
WriteInt(int64(v), buffer)
case int8:
WriteInt(int64(v), buffer)
case int16:
WriteInt(int64(v), buffer)
case int32:
WriteInt(int64(v), buffer)
case int64:
WriteInt(v, buffer)
case uint:
WriteUint(uint64(v), buffer)
case uint8:
WriteUint(uint64(v), buffer)
case uint16:
WriteUint(uint64(v), buffer)
case uint32:
WriteUint(uint64(v), buffer)
case uint64:
WriteUint(v, buffer)
case float32:
buffer.WriteString(strconv.FormatFloat(float64(v), 'f', -1, 64))
case float64:
buffer.WriteString(strconv.FormatFloat(v, 'f', -1, 64))
case bool:
WriteBool(v, buffer)
case stringer:
if escape {
WriteEscString(v.String(), buffer)
} else {
buffer.WriteString(v.String())
}
default:
buffer.WriteString("\n<<< unprinted type, fmt.Stringer implementation needed >>>\n")
}
}
func ternary(condition bool, iftrue, iffalse interface{}) interface{} {
if condition {
return iftrue
} else {
return iffalse
}
}
// Used part of go source:
// https://github.com/golang/go/blob/master/src/strconv/itoa.go
func WriteUint(u uint64, buffer *WriterAsBuffer) {
var a [64 + 1]byte
i := len(a)
if ^uintptr(0)>>32 == 0 {
for u > uint64(^uintptr(0)) {
q := u / 1e9
us := uintptr(u - q*1e9)
for j := 9; j > 0; j-- {
i--
qs := us / 10
a[i] = byte(us - qs*10 + '0')
us = qs
}
u = q
}
}
us := uintptr(u)
for us >= 10 {
i--
q := us / 10
a[i] = byte(us - q*10 + '0')
us = q
}
i--
a[i] = byte(us + '0')
buffer.Write(a[i:])
}
func WriteInt(i int64, buffer *WriterAsBuffer) {
if i < 0 {
buffer.WriteByte('-')
i = -i
}
WriteUint(uint64(i), buffer)
}
func WriteBool(b bool, buffer *WriterAsBuffer) {
if b {
buffer.WriteString("true")
return
}
buffer.WriteString("false")
}

View File

@ -12,12 +12,12 @@ html(lang="en")
block head
body
header
svg(viewBox="0 -25 216 40")
text Arav's dwelling
text(y="11") Welcome to my sacred place, wanderer
svg#logo(viewBox="0 -25 216 40")
text.logo Arav's dwelling
text.under(y="11") Welcome to my sacred place, wanderer
nav
a(href=mainSite title="Arav's dwelling") Back to main website
block header
block body
footer
| 2022,2023 Alexander "Arav" Andreev &lt;#[a(href="mailto:me@arav.su") me@arav.su]&gt; #[a(href=mainSite+'/privacy') Privacy statements]
| 2022,2023 Alexander "Arav" Andreev &lt;#[a(href="mailto:me@arav.su") me@arav.su]&gt;

View File

@ -1,11 +0,0 @@
extends base.jade
block header
h1 Deleted
block body
:go:func Deleted(mainSite string)
section
h2 File was successfully deleted.
center
a(href="/") Back to index page

View File

@ -0,0 +1,20 @@
extends base.jade
block head
:go:func Error404(mainSite string)
style(type="text/css").
#error {
font-size: 3.5rem;
line-height: 5rem;
text-align: center;
margin: 6rem 0; }
#error h1 { font-size: 8rem; }
block header
h1 404
block body
section#error
h1 404
| Nod Found xD

View File

@ -1,14 +0,0 @@
extends base.jade
block header
h1 Еггог
block body
:go:func ErrorXXX(mainSite string, code int, errorMsg string)
section#error
h1 #{code}
| #{http.StatusText(code)}
if errorMsg != ""
p #{errorMsg}
center
a(href="/") Back to index page

View File

@ -4,25 +4,37 @@ block header
h1 Upload
block body
:go:func Index(mainSite string, keepForHours int, fileMaxSize, storageAvailableStr string)
section
:go:func Index(mainSite string, storageCapacity, storageUsed int64, keepForHours int, fileMaxSize, storageUsedStr, storageCapacityStr, storageAvailableStr string)
section#rules.center
h2 Rules
p Maximum file size is #[b #{fileMaxSize}] and it will be kept for #[b #{keepForHours}] hours.
p Content you upload should comply with Russian Federation's law. Generally speaking, anything illegal, like CP, extremist literature, and so on is forbidden.
section.center
p Maximum file size is #{fileMaxSize} and it will be kept for #{keepForHours} hours.
p Content you upload should comply with with Russian Federation's law. Generally speaking, anything illegal, like CP, extremist literature, and so on is forbidden.
section#used-space.center
h2 Free space
div
span #{storageUsedStr}
progress(value=storageUsed max=storageCapacity)
span #{storageCapacityStr}
div
| #{storageAvailableStr}
section#upload.center
h2 Upload
form(action="/" method="POST" enctype="multipart/form-data")
input(type="file" name="file" multiple=false)
button(type="submit") Upload
p.center #[b #{storageAvailableStr}] left.
section
p Using cURL: #[code curl -F 'file=@somefile.ext' https://upload.arav.su]
p Also works under other networks (I2P, Tor, Yggdrasil). For Tor and I2P you'll need to add a #[code --proxy] option for cURL. Same for deletion.
p A resulted link has the following structure: #[code &lt;site&gt;/&lt;hash&gt;/&lt;file&gt;.&lt;ext&gt;].
section.center
p You can use cURL to upload a file: #[code curl -F 'file=@somefile.ext' https://upload.arav.su]
p Over I2P: #[code curl --proxy 127.0.0.1:4444 -F 'file=@somefile.ext' http://upload.arav.i2p]
p A resulted link looks like this: #[code /f/base64rawURL(salted SHA-256)/filename.ext].
section.center
h2 Delete
form(action="/delete" method="POST")
input(type="text", name="hash" placeholder="File hash goes here" minlength="43" maxlength="43" size="43" required="")
button(type="submit") Delete
section.center
p You can delete a file using cURL: #[code curl -XDELETE https://upload.arav.su/&lt;hash&gt;]
p Over I2P: #[code curl --proxy 127.0.0.1:4444 -XDELETE http://upload.arav.i2p/&lt;hash&gt;]
section
p Using cURL: #[code curl -XDELETE https://upload.arav.su/&lt;hash&gt;]
h2 Privacy statements
p Any abuses should be sent to #[a(href="mailto:admin@arav.su") admin@arav.su]. I WILL cooperate with law enforcements and provide them with logs.
p Logs include: access time, IP-address, file name it was uploaded/downloaded with, a SHA-256 hash of it, size of it, and User-Agent.

View File

@ -0,0 +1,19 @@
extends base.jade
block head
:go:func ErrorNoSpace(mainSite string)
style(type="text/css").
#error {
font-size: 3.5rem;
line-height: 5rem;
text-align: center;
margin: 6rem 0; }
#error h1 { font-size: 6rem; }
block header
h1 :(
block body
section#error
h1 Not enough space left

View File

@ -5,7 +5,7 @@ block header
block body
:go:func Uploaded(mainSite, site, downloadLink string, keepForHours int)
section
section#file
h2 Your link
center
a(href=downloadLink) #{site}#{downloadLink}

View File

@ -6,10 +6,10 @@ import (
"net/http"
)
//go:generate $GOPATH/bin/jade -pkg=web -writer templates/index.jade
//go:generate $GOPATH/bin/jade -pkg=web -writer templates/deleted.jade
//go:generate $GOPATH/bin/jade -pkg=web -writer templates/uploaded.jade
//go:generate $GOPATH/bin/jade -pkg=web -writer templates/errorXXX.jade
//go:generate $GOPATH/bin/jade -pkg=web -stdbuf -stdlib -writer templates/index.jade
//go:generate $GOPATH/bin/jade -pkg=web -stdbuf -stdlib -writer templates/nospace.jade
//go:generate $GOPATH/bin/jade -pkg=web -stdbuf -stdlib -writer templates/uploaded.jade
//go:generate $GOPATH/bin/jade -pkg=web -stdbuf -stdlib -writer templates/error404.jade
//go:embed assets
var assetsDir embed.FS