Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3ddfebcd24 | |||
| 5db166c2b9 | |||
| 23df3a1aa4 | |||
| 9865b14f2c | |||
| f9b561e6e7 | |||
| 99bbae5b94 | |||
| f0d33eb150 | |||
| 7fa8dae48f | |||
| a79b0235e7 | |||
| bac7eaeffe | |||
| 09ac2e3e36 | |||
| c9f9891489 | |||
| f98aa04bba | |||
| 6a9d1c8eaf | |||
| a14562ae50 | |||
| 0ec0a19b9d | |||
| 3e891b87f5 | |||
| 0714176d6e | |||
| 6eb1ee50ac | |||
| ccaccd27d9 | |||
| 61e4038bd4 | |||
| 28dcdc72ab | |||
| e6e87a16ac | |||
|
|
bbbdcd650c | ||
|
|
9c4d7df1fc | ||
|
|
ff4065a5d3 | ||
|
|
c5bc33f50a | ||
|
|
90c525b324 | ||
|
|
cabca4963c | ||
|
|
a4cd80b34d | ||
|
|
448c35c0ef | ||
|
|
c56646025a | ||
|
|
07d0882c65 | ||
|
|
016ffd9fa4 | ||
|
|
95c3ac899f | ||
|
|
3bc8446a6d | ||
|
|
aabe581148 |
11
.dockerignore
Normal file
11
.dockerignore
Normal file
@@ -0,0 +1,11 @@
|
||||
.git/
|
||||
.gitignore
|
||||
.dockerignore
|
||||
Dockerfile
|
||||
LICENSE
|
||||
Makefile
|
||||
*.md
|
||||
*.jpg
|
||||
docker-compose.yaml
|
||||
prosody-filer-s3
|
||||
!cmd/prosody-filer-s3
|
||||
77
.gitea/workflows/main.yaml
Normal file
77
.gitea/workflows/main.yaml
Normal file
@@ -0,0 +1,77 @@
|
||||
---
|
||||
name: Main workflow
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
tags-ignore:
|
||||
- '**'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run unit tests
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 5
|
||||
container:
|
||||
image: alpine:edge
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
apk add -U \
|
||||
curl \
|
||||
git \
|
||||
go \
|
||||
minio \
|
||||
nodejs
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Start minio
|
||||
run: |
|
||||
minio server /data &
|
||||
while ! curl -s -f http://localhost:9000/minio/health/live; do
|
||||
sleep 1
|
||||
done
|
||||
|
||||
- name: Run unit tests
|
||||
run: go test -v ./...
|
||||
|
||||
build:
|
||||
name: Build docker image
|
||||
needs: test
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 5
|
||||
container:
|
||||
image: alpine:edge
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
apk add -U \
|
||||
docker-cli \
|
||||
docker-cli-buildx \
|
||||
git \
|
||||
nodejs
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Login to registry
|
||||
uses: actions/docker-login@v2
|
||||
with:
|
||||
registry: git.mug.lv
|
||||
username: galen
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Setup buildx
|
||||
uses: actions/docker-setup-buildx@v2
|
||||
|
||||
- name: Build image
|
||||
uses: actions/docker-build-push@v4
|
||||
# TODO: remove once fixed in upstream
|
||||
env:
|
||||
ACTIONS_RUNTIME_TOKEN: ""
|
||||
with:
|
||||
push: ${{ gitea.ref_name == 'main' }}
|
||||
tags: git.mug.lv/galen/prosody-filer-s3:${{ gitea.ref_name == 'main' && 'latest' || gitea.ref_name }}
|
||||
47
.gitea/workflows/tag.yaml
Normal file
47
.gitea/workflows/tag.yaml
Normal file
@@ -0,0 +1,47 @@
|
||||
---
|
||||
name: Main workflow
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- '**'
|
||||
tags:
|
||||
- '**'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build docker image
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 5
|
||||
container:
|
||||
image: alpine:edge
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
apk add -U \
|
||||
docker-cli \
|
||||
docker-cli-buildx \
|
||||
git \
|
||||
nodejs
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Login to registry
|
||||
uses: actions/docker-login@v2
|
||||
with:
|
||||
registry: git.mug.lv
|
||||
username: galen
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Setup buildx
|
||||
uses: actions/docker-setup-buildx@v2
|
||||
|
||||
- name: Build image
|
||||
uses: actions/docker-build-push@v4
|
||||
# TODO: remove once fixed in upstream
|
||||
env:
|
||||
ACTIONS_RUNTIME_TOKEN: ""
|
||||
with:
|
||||
push: true
|
||||
tags: git.mug.lv/galen/prosody-filer-s3:${{ gitea.ref_name }}
|
||||
18
.gitignore
vendored
18
.gitignore
vendored
@@ -1,16 +1,2 @@
|
||||
config.toml
|
||||
main
|
||||
prosody-filer
|
||||
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, build with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
prosody-filer-s3
|
||||
!cmd/prosody-filer-s3
|
||||
|
||||
19
Dockerfile
Normal file
19
Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
||||
FROM golang:1.20-alpine@sha256:e7cc33118f807c67d9f2dfc811cc2cc8b79b3687d0b4ac891dd59bb2a5e4a8d3 AS build
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
COPY . /build
|
||||
|
||||
RUN go build ./...
|
||||
|
||||
FROM alpine:3.18@sha256:82d1e9d7ed48a7523bdebc18cf6290bdb97b82302a8a9c27d4fe885949ea94d1
|
||||
|
||||
RUN apk add -U --no-cache ca-certificates tini && \
|
||||
addgroup -g 1000 prosody-filer-s3 && \
|
||||
adduser -h /var/lib/empty -G prosody-filer-s3 -s /sbin/nologin -D -H -u 1000 prosody-filer-s3
|
||||
|
||||
COPY --from=build /build/prosody-filer-s3 /usr/local/bin/prosody-filer-s3
|
||||
|
||||
USER 1000
|
||||
|
||||
ENTRYPOINT ["/sbin/tini", "--", "/usr/local/bin/prosody-filer-s3"]
|
||||
12
Makefile
Normal file
12
Makefile
Normal file
@@ -0,0 +1,12 @@
|
||||
.PHONY: build
|
||||
build:
|
||||
go build ./...
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -f prosody-filer-s3
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
docker compose up -d
|
||||
go test ./... -count 1
|
||||
44
README.md
44
README.md
@@ -1,3 +1,42 @@
|
||||
# Prosody Filer S3 fork (fork)
|
||||
|
||||
Originally forked from https://github.com/Wilm0r/prosody-filer-s3.
|
||||
|
||||
A simple XMPP upload server (tested with Prosody only so far) that relies on S3 API compatible storage (tested against my own Ceph and [Scaleway (free 75GB)](https://www.scaleway.com/en/object-storage/)) instead of local disk, so that you can run it without local state on Kubernetes/Docker.
|
||||
|
||||
Like the original version, it aims to be thin and simple. It streams PUT operations directly to S3, and fetch operations will by default redirect to a signed request to S3, instead of sitting in between as an unnecessary bottleneck. (If you do prefer proxying, it can be enabled using the `ProxyMode` setting.)
|
||||
|
||||
If you want automatic purging, just set [lifecycle policies](https://docs.aws.amazon.com/AmazonS3/latest/dev/lifecycle-configuration-examples.html) on your S3 bucket.
|
||||
|
||||
## `config.toml` example
|
||||
|
||||
```ini
|
||||
### IP address and port to listen to, e.g. "[::]:5050"
|
||||
ListenPort = "0.0.0.0:5280"
|
||||
### Secret (must match the one in prosody.conf.lua!)
|
||||
Secret =
|
||||
### Subdirectory for HTTP upload / download requests (usually "upload/",
|
||||
### NO to LEADING slash, YES to trailing!)
|
||||
UploadSubDir = "upload/"
|
||||
|
||||
### Hostname of S3 compatible endpoint
|
||||
S3Endpoint = "xmpp-filer.s3.nl-ams.scw.cloud"
|
||||
### HTTPS. True by default obviously, set to false if you must.
|
||||
S3TLS = true
|
||||
### Credentials. Use AWS_ACCESS_KEY_ID environment variable if you prefer.
|
||||
S3AccessKey = "..."
|
||||
### Or AWS_SECRET_ACCESS_KEY environment variable.
|
||||
S3Secret = "..."
|
||||
### Our S3 bucket name.
|
||||
S3Bucket = "xmpp-filer"
|
||||
|
||||
### If your client doesn't deal well with the 302 redirects or signed URLs,
|
||||
### enable this setting so Filer will proxy the data for you.
|
||||
ProxyMode = false
|
||||
```
|
||||
|
||||
The rest of this manual covers the local-storage original Filer. The majority of it (except for Prosody/Ejabberd configuration) shouldn't apply to you if you're planning to run this Filer on k8s.
|
||||
|
||||
# Prosody Filer
|
||||
|
||||
A simple file server for handling XMPP http_upload requests. This server is meant to be used with the Prosody [mod_http_upload_external](https://modules.prosody.im/mod_http_upload_external.html) module.
|
||||
@@ -81,7 +120,7 @@ Copy
|
||||
|
||||
to ```/home/prosody-filer/```. Rename the configuration to ```config.toml```.
|
||||
|
||||
Make sure the `prosody-filer` binary is executable:
|
||||
Make sure the `prosody-filer` binary is executable:
|
||||
|
||||
```
|
||||
chmod u+x prosody-filer
|
||||
@@ -245,6 +284,7 @@ server {
|
||||
}
|
||||
|
||||
root /home/prosody-filer;
|
||||
autoindex off;
|
||||
client_max_body_size 51m;
|
||||
client_body_buffer_size 51m;
|
||||
try_files $uri $uri/ @prosodyfiler;
|
||||
@@ -308,7 +348,7 @@ server {
|
||||
|
||||
Prosody Filer has no immediate knowlegde over all the stored files and the time they were uploaded, since no database exists for that. Also Prosody is not capable to do auto deletion if *mod_http_upload_external* is used. Therefore the suggested way of purging the uploads directory is to execute a purge command via a cron job:
|
||||
|
||||
@daily find /home/prosody-filer/upload/* -maxdepth 0 -type d -mtime +28 | xargs rm -rf
|
||||
@daily find /home/prosody-filer/upload/ -mindepth 1 -type d -mtime +28 -print0 | xargs -0 -- rm -rf
|
||||
|
||||
This will delete uploads older than 28 days.
|
||||
|
||||
|
||||
7
build.sh
7
build.sh
@@ -1,7 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
##
|
||||
## Builds static prosody-filer binary
|
||||
##
|
||||
|
||||
CGO_ENABLED=0 GOOS=linux go build -a -ldflags '-extldflags "-static"' .
|
||||
341
cmd/prosody-filer-s3/main.go
Normal file
341
cmd/prosody-filer-s3/main.go
Normal file
@@ -0,0 +1,341 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"flag"
|
||||
"fmt"
|
||||
"mime"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
|
||||
minio "github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
)
|
||||
|
||||
/*
|
||||
* Configuration of this server
|
||||
*/
|
||||
type Config struct {
|
||||
Address string `toml:"address"`
|
||||
Secret string `toml:"secret"`
|
||||
UploadSubDir string `toml:"upload_sub_dir"`
|
||||
ProxyMode bool `toml:"proxy_mode"`
|
||||
S3Endpoint string `toml:"s3_endpoint"`
|
||||
S3AccessKey string `toml:"s3_access_key"`
|
||||
S3SecretKey string `toml:"s3_secret_key"`
|
||||
S3TLS bool `toml:"s3_tls"`
|
||||
S3Bucket string `toml:"s3_bucket"`
|
||||
S3Region string `toml:"s3_region"`
|
||||
}
|
||||
|
||||
const ALLOWED_METHODS = "OPTIONS, HEAD, GET, PUT"
|
||||
|
||||
// How long presigned S3 will remain valid
|
||||
const PRESIGNED_URL_EXPIRY_TIME = 5 * time.Minute
|
||||
|
||||
var mimeTypeRegex = regexp.MustCompile("((audio|image|video)/.*|text/plain)")
|
||||
|
||||
func setContentHeaders(h http.Header, filename string) {
|
||||
mimeType := mime.TypeByExtension(filepath.Ext(filename))
|
||||
h.Set("Content-Type", mimeType)
|
||||
if mimeTypeRegex.MatchString(mimeType) {
|
||||
h.Set("Content-Disposition", "inline")
|
||||
} else {
|
||||
h.Set("Content-Disposition", "attachment")
|
||||
}
|
||||
}
|
||||
|
||||
func handleHead(
|
||||
w http.ResponseWriter,
|
||||
r *http.Request,
|
||||
config *Config,
|
||||
client *minio.Client,
|
||||
filename string,
|
||||
) {
|
||||
info, err := client.StatObject(
|
||||
r.Context(),
|
||||
config.S3Bucket,
|
||||
filename,
|
||||
minio.StatObjectOptions{},
|
||||
)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to get file stats from s3")
|
||||
http.Error(w, "Service Unavailable", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
|
||||
setContentHeaders(w.Header(), filename)
|
||||
w.Header().Set("Content-Length", strconv.FormatInt(info.Size, 10))
|
||||
}
|
||||
|
||||
func handleGet(
|
||||
w http.ResponseWriter,
|
||||
r *http.Request,
|
||||
config *Config,
|
||||
client *minio.Client,
|
||||
filename string,
|
||||
) {
|
||||
if config.ProxyMode {
|
||||
obj, err := client.GetObject(
|
||||
r.Context(),
|
||||
config.S3Bucket,
|
||||
filename,
|
||||
minio.GetObjectOptions{},
|
||||
)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to get file from s3")
|
||||
http.Error(w, "Service Unavailable", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
defer obj.Close()
|
||||
|
||||
setContentHeaders(w.Header(), filename)
|
||||
http.ServeContent(w, r, filename, time.Now(), obj)
|
||||
} else {
|
||||
objUrl, err := client.PresignedGetObject(
|
||||
r.Context(),
|
||||
config.S3Bucket,
|
||||
filename,
|
||||
PRESIGNED_URL_EXPIRY_TIME,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to get file from s3")
|
||||
http.Error(w, "Service Unavailable", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
|
||||
setContentHeaders(w.Header(), filename)
|
||||
w.Header().Set("Location", objUrl.String())
|
||||
w.WriteHeader(http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
func requestHasValidHmac(r *http.Request, filename, secret string) bool {
|
||||
var actual string
|
||||
mac := hmac.New(sha256.New, []byte(secret))
|
||||
|
||||
params := r.URL.Query()
|
||||
if params.Has("v2") {
|
||||
actual = params.Get("v2")
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
if contentType == "" {
|
||||
return false
|
||||
}
|
||||
contentLength := strconv.FormatInt(r.ContentLength, 10)
|
||||
mac.Write([]byte(filename + "\x00" + contentLength + "\x00" + contentType))
|
||||
} else if params.Has("v") {
|
||||
actual = params.Get("v")
|
||||
contentLength := strconv.FormatInt(r.ContentLength, 10)
|
||||
mac.Write([]byte(filename + " " + contentLength))
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
expected := hex.EncodeToString(mac.Sum(nil))
|
||||
|
||||
return hmac.Equal([]byte(expected), []byte(actual))
|
||||
}
|
||||
|
||||
func handlePut(
|
||||
w http.ResponseWriter,
|
||||
r *http.Request,
|
||||
config *Config,
|
||||
client *minio.Client,
|
||||
filename string,
|
||||
) {
|
||||
log.Info().
|
||||
Str("filename", filename).
|
||||
Int64("size", r.ContentLength).
|
||||
Msg("uploading file")
|
||||
if !requestHasValidHmac(r, filename, config.Secret) {
|
||||
log.Warn().Msgf("invalid or missing hmac")
|
||||
http.Error(w, "Invalid HMAC", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := client.StatObject(
|
||||
context.Background(),
|
||||
config.S3Bucket,
|
||||
filename,
|
||||
minio.StatObjectOptions{},
|
||||
)
|
||||
if err == nil {
|
||||
log.Warn().Str("filename", filename).Msg("file already exists")
|
||||
http.Error(w, "File already exists", http.StatusConflict)
|
||||
return
|
||||
} else if minio.ToErrorResponse(err).Code != "NoSuchKey" {
|
||||
log.Error().Err(err).Msg("failed to stat file")
|
||||
http.Error(w, "Service Unavailable", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
|
||||
h := make(http.Header)
|
||||
setContentHeaders(h, filename)
|
||||
opts := minio.PutObjectOptions{
|
||||
ContentType: h.Get("Content-Type"),
|
||||
ContentDisposition: h.Get("Content-Disposition"),
|
||||
}
|
||||
info, err := client.PutObject(
|
||||
r.Context(),
|
||||
config.S3Bucket,
|
||||
filename,
|
||||
r.Body,
|
||||
r.ContentLength,
|
||||
opts,
|
||||
)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to upload file to s3")
|
||||
http.Error(w, "Service Unavailable", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
|
||||
log.Info().
|
||||
Str("filename", filename).
|
||||
Str("etag", info.ETag).
|
||||
Msg("file uploaded")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
}
|
||||
|
||||
func newHandler(config *Config, client *minio.Client) http.HandlerFunc {
|
||||
var subPath string
|
||||
if config.UploadSubDir == "" || config.UploadSubDir == "/" {
|
||||
subPath = "/"
|
||||
} else {
|
||||
subPath = "/" + strings.Trim(config.UploadSubDir, "/") + "/"
|
||||
}
|
||||
getFilename := func(r *http.Request) string {
|
||||
return strings.TrimPrefix(r.URL.Path, subPath)
|
||||
}
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
log.Info().Str("method", r.Method).Str("url", r.URL.String()).Msg("")
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodHead:
|
||||
handleHead(w, r, config, client, getFilename(r))
|
||||
case http.MethodGet:
|
||||
handleGet(w, r, config, client, getFilename(r))
|
||||
case http.MethodOptions:
|
||||
w.Header().Set("Allow", ALLOWED_METHODS)
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", ALLOWED_METHODS)
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
|
||||
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
w.Header().Set("Access-Control-Max-Age", "7200")
|
||||
case http.MethodPut:
|
||||
handlePut(w, r, config, client, getFilename(r))
|
||||
default:
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func loadConfig(filename string) (*Config, error) {
|
||||
config := &Config{S3TLS: true}
|
||||
if filename != "" {
|
||||
if _, err := toml.DecodeFile(filename, config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Environment vars override config file.
|
||||
for envVar, configVar := range map[string]interface{}{
|
||||
"PROSODY_FILER_ADDRESS": &config.Address,
|
||||
"PROSODY_FILER_SECRET": &config.Secret,
|
||||
"PROSODY_FILER_UPLOAD_SUB_DIR": &config.UploadSubDir,
|
||||
"PROSODY_FILER_PROXY_MODE": &config.ProxyMode,
|
||||
"PROSODY_FILER_S3_ENDPOINT": &config.S3Endpoint,
|
||||
"AWS_ACCESS_KEY_ID": &config.S3AccessKey,
|
||||
"PROSODY_FILER_S3_ACCESS_KEY": &config.S3AccessKey,
|
||||
"AWS_SECRET_ACCESS_KEY": &config.S3SecretKey,
|
||||
"PROSODY_FILER_S3_SECRET_KEY": &config.S3SecretKey,
|
||||
"PROSODY_FILER_S3_BUCKET": &config.S3Bucket,
|
||||
"PROSODY_FILER_S3_REGION": &config.S3Region,
|
||||
"PROSODY_FILER_S3_TLS": &config.S3TLS,
|
||||
} {
|
||||
if v, ok := os.LookupEnv(envVar); ok {
|
||||
switch t := configVar.(type) {
|
||||
case *string:
|
||||
*t = v
|
||||
case *bool:
|
||||
*t = strings.ToLower(v) == "true"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for name, value := range map[string]string{
|
||||
"Address": config.Address,
|
||||
"Secret": config.Secret,
|
||||
"S3Endpoint": config.S3Endpoint,
|
||||
"S3AccessKey": config.S3AccessKey,
|
||||
"S3SecretKey": config.S3SecretKey,
|
||||
"S3Bucket": config.S3Bucket,
|
||||
} {
|
||||
if value == "" {
|
||||
return nil, fmt.Errorf("missing required config value %s", name)
|
||||
}
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func initS3Client(config *Config) (*minio.Client, error) {
|
||||
client, err := minio.New(config.S3Endpoint, &minio.Options{
|
||||
Creds: credentials.NewStaticV4(config.S3AccessKey, config.S3SecretKey, ""),
|
||||
Region: config.S3Region,
|
||||
Secure: config.S3TLS,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
exists, err := client.BucketExists(context.Background(), config.S3Bucket)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exists {
|
||||
// Some servers don't correctly report this.
|
||||
log.Warn().Msg("bucket does not seem to exist")
|
||||
}
|
||||
|
||||
return client, err
|
||||
}
|
||||
|
||||
func main() {
|
||||
configPath := flag.String("config", "", "path to config file")
|
||||
flag.Parse()
|
||||
|
||||
config, err := loadConfig(*configPath)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed to load config")
|
||||
}
|
||||
|
||||
client, err := initS3Client(config)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed to initialize s3 client")
|
||||
}
|
||||
|
||||
var pattern string
|
||||
if config.UploadSubDir == "" || config.UploadSubDir == "/" {
|
||||
pattern = "/"
|
||||
} else {
|
||||
pattern = "/" + strings.TrimPrefix(config.UploadSubDir, "/")
|
||||
}
|
||||
http.HandleFunc(pattern, newHandler(config, client))
|
||||
log.Info().Msgf("starting server on %s", config.Address)
|
||||
if err := http.ListenAndServe(config.Address, nil); err != nil {
|
||||
log.Fatal().Err(err).Msg("failed to start server")
|
||||
}
|
||||
}
|
||||
347
cmd/prosody-filer-s3/main_test.go
Normal file
347
cmd/prosody-filer-s3/main_test.go
Normal file
@@ -0,0 +1,347 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func setupBucket(t *testing.T, config *Config, client *minio.Client) {
|
||||
err := client.MakeBucket(
|
||||
context.Background(),
|
||||
config.S3Bucket,
|
||||
minio.MakeBucketOptions{},
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func teardownBucket(t *testing.T, config *Config, client *minio.Client) {
|
||||
err := client.RemoveBucketWithOptions(
|
||||
context.Background(),
|
||||
config.S3Bucket,
|
||||
minio.RemoveBucketOptions{ForceDelete: true},
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func uploadFile(t *testing.T, config *Config, client *minio.Client) {
|
||||
f, err := os.Open("../../catmetal.jpg")
|
||||
assert.NoError(t, err)
|
||||
defer f.Close()
|
||||
|
||||
info, err := f.Stat()
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = client.PutObject(
|
||||
context.Background(),
|
||||
config.S3Bucket,
|
||||
"catmetal.jpg",
|
||||
f,
|
||||
info.Size(),
|
||||
minio.PutObjectOptions{
|
||||
ContentType: "image/jpeg",
|
||||
ContentDisposition: "inline",
|
||||
},
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func testConfig() *Config {
|
||||
return &Config{
|
||||
Secret: "mysecret",
|
||||
UploadSubDir: "/upload",
|
||||
S3Endpoint: "localhost:9000",
|
||||
S3AccessKey: "minioadmin",
|
||||
S3SecretKey: "minioadmin",
|
||||
S3TLS: false,
|
||||
S3Bucket: "xmpp",
|
||||
S3Region: "default",
|
||||
}
|
||||
}
|
||||
|
||||
const config = `address = "[::]:5050"
|
||||
secret = "mysecret"
|
||||
upload_sub_dir = "/upload"
|
||||
proxy_mode = true
|
||||
s3_endpoint = "s3.com"
|
||||
s3_access_key = "1234567890"
|
||||
s3_secret_key = "abcdefghijklmnopqrstuvwxyz"
|
||||
s3_tls = false
|
||||
s3_bucket = "mybucket"
|
||||
s3_region = "mordor"`
|
||||
|
||||
func TestConfigFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
configPath := filepath.Join(dir, "config.toml")
|
||||
|
||||
err := os.WriteFile(configPath, []byte(config), 0o600)
|
||||
assert.NoError(t, err)
|
||||
|
||||
config, err := loadConfig(configPath)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, &Config{
|
||||
Address: "[::]:5050",
|
||||
Secret: "mysecret",
|
||||
UploadSubDir: "/upload",
|
||||
ProxyMode: true,
|
||||
S3Endpoint: "s3.com",
|
||||
S3AccessKey: "1234567890",
|
||||
S3SecretKey: "abcdefghijklmnopqrstuvwxyz",
|
||||
S3TLS: false,
|
||||
S3Bucket: "mybucket",
|
||||
S3Region: "mordor",
|
||||
}, config)
|
||||
}
|
||||
|
||||
func TestConfigEnv(t *testing.T) {
|
||||
t.Setenv("PROSODY_FILER_ADDRESS", "[::]:5050")
|
||||
t.Setenv("PROSODY_FILER_SECRET", "mysecret")
|
||||
t.Setenv("PROSODY_FILER_UPLOAD_SUB_DIR", "/upload")
|
||||
t.Setenv("PROSODY_FILER_PROXY_MODE", "true")
|
||||
t.Setenv("PROSODY_FILER_S3_ENDPOINT", "s3.com")
|
||||
t.Setenv("PROSODY_FILER_S3_ACCESS_KEY", "1234567890")
|
||||
t.Setenv("PROSODY_FILER_S3_SECRET_KEY", "abcdefghijklmnopqrstuvwxyz")
|
||||
t.Setenv("PROSODY_FILER_S3_BUCKET", "mybucket")
|
||||
t.Setenv("PROSODY_FILER_S3_REGION", "mordor")
|
||||
t.Setenv("PROSODY_FILER_S3_TLS", "false")
|
||||
|
||||
config, err := loadConfig("")
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, &Config{
|
||||
Address: "[::]:5050",
|
||||
Secret: "mysecret",
|
||||
UploadSubDir: "/upload",
|
||||
ProxyMode: true,
|
||||
S3Endpoint: "s3.com",
|
||||
S3AccessKey: "1234567890",
|
||||
S3SecretKey: "abcdefghijklmnopqrstuvwxyz",
|
||||
S3TLS: false,
|
||||
S3Bucket: "mybucket",
|
||||
S3Region: "mordor",
|
||||
}, config)
|
||||
}
|
||||
|
||||
func TestUploadValid(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
authVersion string
|
||||
hmac string
|
||||
}{
|
||||
{authVersion: "v", hmac: "dde65647d5e4726adf6ff4282cd3a0648df84436b5387039dd6aea28d04c4917"},
|
||||
{authVersion: "v2", hmac: "26a2b6e27b451bf695f769c8cbb23f8856e80a1fb5963a831f1c79a19873365d"},
|
||||
} {
|
||||
t.Run(fmt.Sprintf("upload with auth %s", tc.authVersion), func(t *testing.T) {
|
||||
config := testConfig()
|
||||
client, err := initS3Client(config)
|
||||
assert.NoError(t, err)
|
||||
|
||||
setupBucket(t, config, client)
|
||||
defer teardownBucket(t, config, client)
|
||||
|
||||
f, err := os.ReadFile("../../catmetal.jpg")
|
||||
assert.NoError(t, err)
|
||||
|
||||
req, err := http.NewRequest("PUT", "/upload/catmetal.jpg", bytes.NewBuffer(f))
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Content-Type", "image/jpeg")
|
||||
|
||||
q := req.URL.Query()
|
||||
q.Set(tc.authVersion, tc.hmac)
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(newHandler(config, client))
|
||||
handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusCreated, rr.Code)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadValidNoPrefix(t *testing.T) {
|
||||
config := testConfig()
|
||||
config.UploadSubDir = ""
|
||||
client, err := initS3Client(config)
|
||||
assert.NoError(t, err)
|
||||
|
||||
setupBucket(t, config, client)
|
||||
defer teardownBucket(t, config, client)
|
||||
|
||||
f, err := os.ReadFile("../../catmetal.jpg")
|
||||
assert.NoError(t, err)
|
||||
|
||||
req, err := http.NewRequest("PUT", "/catmetal.jpg", bytes.NewBuffer(f))
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Content-Type", "image/jpeg")
|
||||
|
||||
q := req.URL.Query()
|
||||
q.Set("v2", "26a2b6e27b451bf695f769c8cbb23f8856e80a1fb5963a831f1c79a19873365d")
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(newHandler(config, client))
|
||||
handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusCreated, rr.Code)
|
||||
}
|
||||
|
||||
func TestUploadMissingMAC(t *testing.T) {
|
||||
f, err := os.ReadFile("../../catmetal.jpg")
|
||||
assert.NoError(t, err)
|
||||
|
||||
req, err := http.NewRequest("PUT", "/upload/catmetal.jpg", bytes.NewBuffer(f))
|
||||
assert.NoError(t, err)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(newHandler(testConfig(), nil))
|
||||
handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusForbidden, rr.Code)
|
||||
}
|
||||
|
||||
func TestUploadInvalidMAC(t *testing.T) {
|
||||
for _, version := range []string{"v", "v2"} {
|
||||
t.Run(version, func(t *testing.T) {
|
||||
f, err := os.ReadFile("../../catmetal.jpg")
|
||||
assert.NoError(t, err)
|
||||
|
||||
req, err := http.NewRequest("PUT", "/upload/catmetal.jpg", bytes.NewBuffer(f))
|
||||
assert.NoError(t, err)
|
||||
|
||||
q := req.URL.Query()
|
||||
q.Set(version, "abc")
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(newHandler(testConfig(), nil))
|
||||
handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusForbidden, rr.Code)
|
||||
})
|
||||
}
|
||||
}
|
||||
func TestUploadInvalidV2WithoutContentType(t *testing.T) {
|
||||
config := testConfig()
|
||||
client, err := initS3Client(config)
|
||||
assert.NoError(t, err)
|
||||
|
||||
setupBucket(t, config, client)
|
||||
defer teardownBucket(t, config, client)
|
||||
|
||||
f, err := os.ReadFile("../../catmetal.jpg")
|
||||
assert.NoError(t, err)
|
||||
|
||||
req, err := http.NewRequest("PUT", "/upload/catmetal.jpg", bytes.NewBuffer(f))
|
||||
assert.NoError(t, err)
|
||||
|
||||
q := req.URL.Query()
|
||||
q.Set("v2", "26a2b6e27b451bf695f769c8cbb23f8856e80a1fb5963a831f1c79a19873365d")
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(newHandler(config, client))
|
||||
handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusForbidden, rr.Code)
|
||||
}
|
||||
|
||||
func TestUploadInvalidMethod(t *testing.T) {
|
||||
f, err := os.ReadFile("../../catmetal.jpg")
|
||||
assert.NoError(t, err)
|
||||
|
||||
req, err := http.NewRequest("POST", "/upload/catmetal.jpg", bytes.NewBuffer(f))
|
||||
assert.NoError(t, err)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(newHandler(testConfig(), nil))
|
||||
handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusMethodNotAllowed, rr.Code)
|
||||
}
|
||||
|
||||
func TestUploadDuplicate(t *testing.T) {
|
||||
config := testConfig()
|
||||
client, err := initS3Client(config)
|
||||
assert.NoError(t, err)
|
||||
|
||||
setupBucket(t, config, client)
|
||||
defer teardownBucket(t, config, client)
|
||||
|
||||
uploadFile(t, config, client)
|
||||
|
||||
f, err := os.ReadFile("../../catmetal.jpg")
|
||||
assert.NoError(t, err)
|
||||
|
||||
req, err := http.NewRequest("PUT", "/upload/catmetal.jpg", bytes.NewBuffer(f))
|
||||
assert.NoError(t, err)
|
||||
|
||||
q := req.URL.Query()
|
||||
q.Add("v", "dde65647d5e4726adf6ff4282cd3a0648df84436b5387039dd6aea28d04c4917")
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(newHandler(config, client))
|
||||
handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusConflict, rr.Code)
|
||||
}
|
||||
|
||||
func TestDownloadOK(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
method string
|
||||
proxy bool
|
||||
expectedStatus int
|
||||
}{
|
||||
{method: http.MethodHead, proxy: true, expectedStatus: http.StatusOK},
|
||||
{method: http.MethodHead, proxy: false, expectedStatus: http.StatusOK},
|
||||
{method: http.MethodGet, proxy: true, expectedStatus: http.StatusOK},
|
||||
{method: http.MethodGet, proxy: false, expectedStatus: http.StatusFound},
|
||||
} {
|
||||
t.Run(fmt.Sprintf("method %s with proxy %t", tc.method, tc.proxy), func(t *testing.T) {
|
||||
config := testConfig()
|
||||
config.ProxyMode = tc.proxy
|
||||
client, err := initS3Client(config)
|
||||
assert.NoError(t, err)
|
||||
|
||||
setupBucket(t, config, client)
|
||||
defer teardownBucket(t, config, client)
|
||||
|
||||
uploadFile(t, config, client)
|
||||
|
||||
req, err := http.NewRequest(tc.method, "/upload/catmetal.jpg", nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(newHandler(config, client))
|
||||
handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, tc.expectedStatus, rr.Code)
|
||||
assert.Equal(t, "image/jpeg", rr.Header().Get("Content-Type"))
|
||||
assert.Equal(t, "inline", rr.Header().Get("Content-Disposition"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloadValidNoPrefix(t *testing.T) {
|
||||
config := testConfig()
|
||||
config.ProxyMode = true
|
||||
config.UploadSubDir = ""
|
||||
client, err := initS3Client(config)
|
||||
assert.NoError(t, err)
|
||||
|
||||
setupBucket(t, config, client)
|
||||
defer teardownBucket(t, config, client)
|
||||
|
||||
uploadFile(t, config, client)
|
||||
|
||||
req, err := http.NewRequest("GET", "/catmetal.jpg", nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(newHandler(config, client))
|
||||
handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, http.StatusOK, rr.Code)
|
||||
assert.Equal(t, "image/jpeg", rr.Header().Get("Content-Type"))
|
||||
assert.Equal(t, "inline", rr.Header().Get("Content-Disposition"))
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
###
|
||||
### Server configuration
|
||||
### (rename this file to "config.toml"!)
|
||||
|
||||
### IP address and port to listen to, e.g. "[::]:5050" to listen to ipv6 and ipv4 addresses
|
||||
listenport = "[::]:5050"
|
||||
|
||||
### Secret (must match the one in prosody.conf.lua!)
|
||||
secret = "mysecret"
|
||||
|
||||
### Where to store the uploaded files
|
||||
storeDir = "./uploads/"
|
||||
|
||||
### Subdirectory for HTTP upload / download requests (usually "upload/")
|
||||
uploadSubDir = "upload/"
|
||||
10
docker-compose.yaml
Normal file
10
docker-compose.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
minio:
|
||||
image: minio/minio:latest
|
||||
command:
|
||||
- server
|
||||
- /data
|
||||
ports:
|
||||
- "9000:9000"
|
||||
34
go.mod
Normal file
34
go.mod
Normal file
@@ -0,0 +1,34 @@
|
||||
module git.mug.lv/galen/prosody-filer-s3
|
||||
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.3.2
|
||||
github.com/minio/minio-go/v7 v7.0.56
|
||||
github.com/rs/zerolog v1.29.1
|
||||
github.com/stretchr/testify v1.8.4
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.16.5 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/minio/md5-simd v1.1.2 // indirect
|
||||
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rs/xid v1.5.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.2 // indirect
|
||||
golang.org/x/crypto v0.10.0 // indirect
|
||||
golang.org/x/net v0.11.0 // indirect
|
||||
golang.org/x/sys v0.9.0 // indirect
|
||||
golang.org/x/text v0.10.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
73
go.sum
Normal file
73
go.sum
Normal file
@@ -0,0 +1,73 @@
|
||||
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
|
||||
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
|
||||
github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
||||
github.com/minio/minio-go/v7 v7.0.56 h1:pkZplIEHu8vinjkmhsexcXpWth2tjVLphrTZx6fBVZY=
|
||||
github.com/minio/minio-go/v7 v7.0.56/go.mod h1:NUDy4A4oXPq1l2yK6LTSvCEzAMeIcoz9lcj5dbzSrRE=
|
||||
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
|
||||
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc=
|
||||
github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU=
|
||||
github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y=
|
||||
github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
|
||||
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
|
||||
golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
|
||||
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
|
||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
|
||||
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
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/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
198
main.go
198
main.go
@@ -1,198 +0,0 @@
|
||||
/*
|
||||
* This module allows upload via mod_http_upload_external
|
||||
* Also see: https://modules.prosody.im/mod_http_upload_external.html
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
)
|
||||
|
||||
/*
|
||||
* Configuration of this server
|
||||
*/
|
||||
type Config struct {
|
||||
Listenport string
|
||||
Secret string
|
||||
Storedir string
|
||||
UploadSubDir string
|
||||
}
|
||||
|
||||
var conf Config
|
||||
|
||||
/*
|
||||
* Sets CORS headers
|
||||
*/
|
||||
func addCORSheaders(w http.ResponseWriter) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "OPTIONS, HEAD, GET, PUT")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
|
||||
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
w.Header().Set("Access-Control-Max-Age", "7200")
|
||||
}
|
||||
|
||||
/*
|
||||
* Request handler
|
||||
* Is activated when a clients requests the file, file information or an upload
|
||||
*/
|
||||
func handleRequest(w http.ResponseWriter, r *http.Request) {
|
||||
log.Println("Incoming request:", r.Method, r.URL.String())
|
||||
|
||||
// Parse URL and args
|
||||
u, err := url.Parse(r.URL.String())
|
||||
if err != nil {
|
||||
log.Println("Failed to parse URL:", err)
|
||||
}
|
||||
|
||||
a, err := url.ParseQuery(u.RawQuery)
|
||||
if err != nil {
|
||||
log.Println("Failed to parse URL query params:", err)
|
||||
}
|
||||
|
||||
fileStorePath := strings.TrimPrefix(u.Path, "/"+conf.UploadSubDir)
|
||||
|
||||
// Add CORS headers
|
||||
addCORSheaders(w)
|
||||
|
||||
if r.Method == "PUT" {
|
||||
// Check if MAC is attached to URL
|
||||
if a["v"] == nil {
|
||||
log.Println("Error: No HMAC attached to URL.")
|
||||
http.Error(w, "409 Conflict", 409)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("MAC sent: ", a["v"][0])
|
||||
|
||||
/*
|
||||
* Check if the request is valid
|
||||
*/
|
||||
mac := hmac.New(sha256.New, []byte(conf.Secret))
|
||||
log.Println("fileStorePath:", fileStorePath)
|
||||
log.Println("ContentLength:", strconv.FormatInt(r.ContentLength, 10))
|
||||
mac.Write([]byte(fileStorePath + " " + strconv.FormatInt(r.ContentLength, 10)))
|
||||
macString := hex.EncodeToString(mac.Sum(nil))
|
||||
|
||||
/*
|
||||
* Check whether calculated (expected) MAC is the MAC that client send in "v" URL parameter
|
||||
*/
|
||||
if hmac.Equal([]byte(macString), []byte(a["v"][0])) {
|
||||
// Make sure the path exists
|
||||
os.MkdirAll(filepath.Dir(conf.Storedir+fileStorePath), os.ModePerm)
|
||||
|
||||
file, err := os.OpenFile(conf.Storedir+fileStorePath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0755)
|
||||
defer file.Close()
|
||||
if err != nil {
|
||||
log.Println("Creating new file failed:", err)
|
||||
http.Error(w, "409 Conflict", 409)
|
||||
return
|
||||
}
|
||||
|
||||
n, err := io.Copy(file, r.Body)
|
||||
if err != nil {
|
||||
log.Println("Writing to new file failed:", err)
|
||||
http.Error(w, "500 Internal Server Error", 500)
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("Successfully written", n, "bytes to file", fileStorePath)
|
||||
} else {
|
||||
log.Println("Invalid MAC.")
|
||||
http.Error(w, "403 Forbidden", 403)
|
||||
return
|
||||
}
|
||||
} else if r.Method == "HEAD" {
|
||||
fileinfo, err := os.Stat(conf.Storedir + fileStorePath)
|
||||
if err != nil {
|
||||
log.Println("Getting file information failed:", err)
|
||||
http.Error(w, "404 Not Found", 404)
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
* Find out the content type to sent correct header. There is a Go function for retrieving the
|
||||
* MIME content type, but this does not work with encrypted files (=> OMEMO). Therefore we're just
|
||||
* relying on file extensions.
|
||||
*/
|
||||
contentType := mime.TypeByExtension(filepath.Ext(fileStorePath))
|
||||
w.Header().Set("Content-Length", strconv.FormatInt(fileinfo.Size(), 10))
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
} else if r.Method == "GET" {
|
||||
contentType := mime.TypeByExtension(filepath.Ext(fileStorePath))
|
||||
if f, err := os.Stat(conf.Storedir + fileStorePath); err != nil || f.IsDir() {
|
||||
log.Println("Directory listing forbidden!")
|
||||
http.Error(w, "403 Forbidden", 403)
|
||||
return
|
||||
}
|
||||
if contentType == "" {
|
||||
contentType = "application/octet-stream"
|
||||
}
|
||||
http.ServeFile(w, r, conf.Storedir+fileStorePath)
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
} else {
|
||||
log.Println("Invalid method", r.Method, "for access to ", conf.UploadSubDir)
|
||||
http.Error(w, "405 Method Not Allowed", 405)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func readConfig(configfilename string, conf *Config) error {
|
||||
log.Println("Reading configuration ...")
|
||||
|
||||
configdata, err := ioutil.ReadFile(configfilename)
|
||||
if err != nil {
|
||||
log.Fatal("Configuration file config.toml cannot be read:", err, "...Exiting.")
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := toml.Decode(string(configdata), conf); err != nil {
|
||||
log.Fatal("Config file config.toml is invalid:", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
* Main function
|
||||
*/
|
||||
func main() {
|
||||
/*
|
||||
* Read startup arguments
|
||||
*/
|
||||
var argConfigFile = flag.String("config", "./config.toml", "Path to configuration file \"config.toml\".")
|
||||
flag.Parse()
|
||||
|
||||
/*
|
||||
* Read config file
|
||||
*/
|
||||
err := readConfig(*argConfigFile, &conf)
|
||||
if err != nil {
|
||||
log.Println("There was an error while reading the configuration file:", err)
|
||||
}
|
||||
|
||||
/*
|
||||
* Start HTTP server
|
||||
*/
|
||||
log.Println("Starting up XMPP HTTP upload server ...")
|
||||
http.HandleFunc("/"+conf.UploadSubDir, handleRequest)
|
||||
log.Printf("Server started on port %s. Waiting for requests.\n", conf.Listenport)
|
||||
http.ListenAndServe(conf.Listenport, nil)
|
||||
}
|
||||
265
main_test.go
265
main_test.go
@@ -1,265 +0,0 @@
|
||||
package main
|
||||
|
||||
/*
|
||||
* Manual testing with CURL
|
||||
* Send with:
|
||||
* curl -X PUT "http://localhost:5050/upload/thomas/abc/catmetal.jpg?v=e17531b1e88bc9a5cbf816eca8a82fc09969c9245250f3e1b2e473bb564e4be0" --data-binary '@catmetal.jpg'
|
||||
* HMAC: e17531b1e88bc9a5cbf816eca8a82fc09969c9245250f3e1b2e473bb564e4be0
|
||||
*/
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func mockUpload() {
|
||||
os.MkdirAll(filepath.Dir(conf.Storedir+"thomas/abc/"), os.ModePerm)
|
||||
from, err := os.Open("./catmetal.jpg")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer from.Close()
|
||||
|
||||
to, err := os.OpenFile(conf.Storedir+"thomas/abc/catmetal.jpg", os.O_RDWR|os.O_CREATE, 0660)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer to.Close()
|
||||
|
||||
_, err = io.Copy(to, from)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func cleanup() {
|
||||
// Clean up
|
||||
if _, err := os.Stat(conf.Storedir); err == nil {
|
||||
// Delete existing catmetal picture
|
||||
err := os.RemoveAll(conf.Storedir)
|
||||
if err != nil {
|
||||
log.Println("Error while cleaning up:", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadConfig(t *testing.T) {
|
||||
// Set config
|
||||
err := readConfig("config.toml", &conf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadValid(t *testing.T) {
|
||||
// Set config
|
||||
readConfig("config.toml", &conf)
|
||||
|
||||
// Read catmetal file
|
||||
catmetalfile, err := ioutil.ReadFile("catmetal.jpg")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create request
|
||||
req, err := http.NewRequest("PUT", "/upload/thomas/abc/catmetal.jpg", bytes.NewBuffer(catmetalfile))
|
||||
q := req.URL.Query()
|
||||
q.Add("v", "e17531b1e88bc9a5cbf816eca8a82fc09969c9245250f3e1b2e473bb564e4be0")
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(handleRequest)
|
||||
|
||||
// Send request and record response
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
// Check status code
|
||||
if status := rr.Code; status != http.StatusOK {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v. HTTP body: %s", status, http.StatusOK, rr.Body.String())
|
||||
}
|
||||
|
||||
// clean up
|
||||
cleanup()
|
||||
}
|
||||
|
||||
func TestUploadMissingMAC(t *testing.T) {
|
||||
// Set config
|
||||
readConfig("config.toml", &conf)
|
||||
|
||||
// Read catmetal file
|
||||
catmetalfile, err := ioutil.ReadFile("catmetal.jpg")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create request
|
||||
req, err := http.NewRequest("PUT", "/upload/thomas/abc/catmetal.jpg", bytes.NewBuffer(catmetalfile))
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(handleRequest)
|
||||
|
||||
// Send request and record response
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
// Check status code
|
||||
if status := rr.Code; status != http.StatusConflict {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v. HTTP body: %s", status, http.StatusConflict, rr.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadInvalidMAC(t *testing.T) {
|
||||
// Set config
|
||||
readConfig("config.toml", &conf)
|
||||
|
||||
// Read catmetal file
|
||||
catmetalfile, err := ioutil.ReadFile("catmetal.jpg")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create request
|
||||
req, err := http.NewRequest("PUT", "/upload/thomas/abc/catmetal.jpg", bytes.NewBuffer(catmetalfile))
|
||||
q := req.URL.Query()
|
||||
q.Add("v", "abc")
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(handleRequest)
|
||||
|
||||
// Send request and record response
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
// Check status code
|
||||
if status := rr.Code; status != http.StatusForbidden {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v. HTTP body: %s", status, http.StatusForbidden, rr.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadInvalidMethod(t *testing.T) {
|
||||
// Set config
|
||||
readConfig("config.toml", &conf)
|
||||
|
||||
// Read catmetal file
|
||||
catmetalfile, err := ioutil.ReadFile("catmetal.jpg")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create request
|
||||
req, err := http.NewRequest("POST", "/upload/thomas/abc/catmetal.jpg", bytes.NewBuffer(catmetalfile))
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(handleRequest)
|
||||
|
||||
// Send request and record response
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
// Check status code
|
||||
if status := rr.Code; status != http.StatusMethodNotAllowed {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v. HTTP body: %s", status, http.StatusMethodNotAllowed, rr.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloadHead(t *testing.T) {
|
||||
// Set config
|
||||
readConfig("config.toml", &conf)
|
||||
|
||||
// Mock upload
|
||||
mockUpload()
|
||||
|
||||
// Create request
|
||||
req, err := http.NewRequest("HEAD", "/upload/thomas/abc/catmetal.jpg", nil)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(handleRequest)
|
||||
|
||||
// Send request and record response
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
// Check status code
|
||||
if status := rr.Code; status != http.StatusOK {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v. HTTP body: %s", status, http.StatusOK, rr.Body.String())
|
||||
}
|
||||
|
||||
// cleanup
|
||||
cleanup()
|
||||
}
|
||||
|
||||
func TestDownloadGet(t *testing.T) {
|
||||
// Set config
|
||||
readConfig("config.toml", &conf)
|
||||
|
||||
// moch upload
|
||||
mockUpload()
|
||||
|
||||
// Create request
|
||||
req, err := http.NewRequest("GET", "/upload/thomas/abc/catmetal.jpg", nil)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(handleRequest)
|
||||
|
||||
// Send request and record response
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
// Check status code
|
||||
if status := rr.Code; status != http.StatusOK {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v. HTTP body: %s", status, http.StatusOK, rr.Body.String())
|
||||
}
|
||||
|
||||
// cleanup
|
||||
cleanup()
|
||||
}
|
||||
|
||||
func TestEmptyGet(t *testing.T) {
|
||||
// Set config
|
||||
readConfig("config.toml", &conf)
|
||||
|
||||
// Create request
|
||||
req, err := http.NewRequest("GET", "", nil)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(handleRequest)
|
||||
|
||||
// Send request and record response
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
// Check status code
|
||||
if status := rr.Code; status != http.StatusForbidden {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v. HTTP body: %s", status, http.StatusForbidden, rr.Body.String())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user