Compare commits
94 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| db368bbfd4 | |||
| 79c0529823 | |||
| e1b2d99b23 | |||
| 6175a9857c | |||
| 7441ce0a83 | |||
| 1dc5a2b56b | |||
| 2eaa0ada05 | |||
| 1e67af6bba | |||
| db8529e560 | |||
| 114edffa0a | |||
| 5732fb7aa0 | |||
| 45c80c1686 | |||
| 40e13b716c | |||
| 3079f15783 | |||
| fd26ac21af | |||
| 3a90641e40 | |||
| 757e38bc32 | |||
| 3d821b7844 | |||
| 31bf2fb3f6 | |||
| 008cceed07 | |||
| 8a9eb0f5ea | |||
| be7c5a3164 | |||
| e15ae6070f | |||
| d5330dcfbc | |||
| fad4b5fd34 | |||
| fc0e2d8fdd | |||
| 6794b64658 | |||
| c6358b3b6c | |||
| 5f8b6c5ba4 | |||
| 3dec173a40 | |||
| 9d1eb48368 | |||
| 321c3515f3 | |||
| 7870d7b3f2 | |||
| c18f91270e | |||
| 19efe356ba | |||
| dc4bd52fac | |||
| bce638e2bc | |||
| 6146fd6b1b | |||
| d206f845b1 | |||
| 17e4a4e1f1 | |||
| f8dfc345f0 | |||
| 87e5dbbc81 | |||
| 605b81b307 | |||
| dc6258e386 | |||
| 79651834b4 | |||
| a81fce0eb9 | |||
| 85ca8771c3 | |||
| 7e2f59de50 | |||
| ad184314d7 | |||
| 33848cbbd2 | |||
| 4dfa67af5d | |||
| 6d744ada2a | |||
| e88fbf2aa9 | |||
| e0c1df7043 | |||
| ce9ce97837 | |||
| ac0313cee3 | |||
| 1ada753fcb | |||
| 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 |
2
.dockerignore
Normal file
2
.dockerignore
Normal file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!bin/
|
||||
60
.github/workflows/build.yaml
vendored
Normal file
60
.github/workflows/build.yaml
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
---
|
||||
name: Build image
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
push:
|
||||
description: Whether to push the image
|
||||
type: boolean
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build docker image
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Login to registry
|
||||
if: ${{ inputs.push }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: git.mug.lv
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Setup buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Setup go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
- name: Build binary
|
||||
run: make build
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
|
||||
- name: Tag image
|
||||
uses: docker/metadata-action@v5
|
||||
id: meta
|
||||
with:
|
||||
images: |
|
||||
git.mug.lv/${{ github.repository }}
|
||||
flavor: |
|
||||
latest=false
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=ref,event=tag
|
||||
|
||||
- name: Build image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: ${{ inputs.push }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
46
.github/workflows/main.yaml
vendored
Normal file
46
.github/workflows/main.yaml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
---
|
||||
name: Main workflow
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
tags-ignore:
|
||||
- '**'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run unit tests
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
- name: Run minio
|
||||
run: |
|
||||
wget -q https://dl.min.io/server/minio/release/linux-amd64/minio
|
||||
chmod +x minio
|
||||
./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 ./...
|
||||
|
||||
- name: Stop minio
|
||||
if: always()
|
||||
run: pkill -f minio
|
||||
|
||||
build:
|
||||
needs: test
|
||||
uses: ./.github/workflows/build.yaml
|
||||
with:
|
||||
push: ${{ github.ref_name == 'main' }}
|
||||
secrets: inherit
|
||||
16
.github/workflows/tag.yaml
vendored
Normal file
16
.github/workflows/tag.yaml
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
name: Main workflow
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- '**'
|
||||
tags:
|
||||
- '**'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: ./.github/workflows/build.yaml
|
||||
with:
|
||||
push: true
|
||||
secrets: inherit
|
||||
17
.gitignore
vendored
17
.gitignore
vendored
@@ -1,16 +1 @@
|
||||
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
|
||||
bin/
|
||||
|
||||
11
.renovaterc.json5
Normal file
11
.renovaterc.json5
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"dependencyDashboard": true,
|
||||
"extends": [
|
||||
"group:allNonMajor",
|
||||
"schedule:monthly",
|
||||
],
|
||||
"enabledManagers": ["github-actions", "gomod"],
|
||||
"osvVulnerabilityAlerts": true,
|
||||
"postUpdateOptions": ["gomodTidy"]
|
||||
}
|
||||
11
Dockerfile
Normal file
11
Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
||||
FROM alpine:3.21
|
||||
|
||||
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 bin/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 -o bin/ ./cmd/prosody-filer-s3
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -f prosody-filer-s3
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
docker compose up -d
|
||||
go test ./... -count 1
|
||||
50
README.md
50
README.md
@@ -1,3 +1,48 @@
|
||||
# Prosody Filer S3
|
||||
|
||||
Originally forked from https://github.com/Wilm0r/prosody-filer-s3.
|
||||
|
||||
Refactored to cleanup code base & fix some issues with the original implementation (e.g. HEAD requests not returning the file size). The `v2` upload API has also been implemented.
|
||||
|
||||
See [config.toml](./config.toml) for an example configuration file. It's also now possible to [configure using environment variables](./cmd/prosody-filer-s3/main.go#L255).
|
||||
|
||||
# Prosody Filer S3 fork
|
||||
|
||||
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 +126,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 +290,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 +354,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
config.toml
Normal file
10
config.toml
Normal file
@@ -0,0 +1,10 @@
|
||||
address = "[::]:5050"
|
||||
secret = "mysecret"
|
||||
upload_sub_dir = ""
|
||||
proxy_mode = true
|
||||
s3_endpoint = "localhost:9000"
|
||||
s3_access_key = "minioadmin"
|
||||
s3_secret_key = "minioadmin"
|
||||
s3_tls = false
|
||||
s3_bucket = "bucket"
|
||||
s3_region = ""
|
||||
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"
|
||||
35
go.mod
Normal file
35
go.mod
Normal file
@@ -0,0 +1,35 @@
|
||||
module git.mug.lv/galen/prosody-filer-s3
|
||||
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.25.4
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.5.0
|
||||
github.com/minio/minio-go/v7 v7.0.97
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/go-ini/ini v1.67.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.11 // indirect
|
||||
github.com/klauspost/crc32 v1.3.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/minio/crc64nvme v1.1.0 // indirect
|
||||
github.com/minio/md5-simd v1.1.2 // indirect
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rs/xid v1.6.0 // indirect
|
||||
github.com/tinylib/msgp v1.3.0 // indirect
|
||||
golang.org/x/crypto v0.39.0 // indirect
|
||||
golang.org/x/net v0.41.0 // indirect
|
||||
golang.org/x/sys v0.34.0 // indirect
|
||||
golang.org/x/text v0.26.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
59
go.sum
Normal file
59
go.sum
Normal file
@@ -0,0 +1,59 @@
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
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/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
||||
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU=
|
||||
github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM=
|
||||
github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw=
|
||||
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.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/minio/crc64nvme v1.1.0 h1:e/tAguZ+4cw32D+IO/8GSf5UVr9y+3eJcxZI2WOO/7Q=
|
||||
github.com/minio/crc64nvme v1.1.0/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
|
||||
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.97 h1:lqhREPyfgHTB/ciX8k2r8k0D93WaFqxbJX36UZq5occ=
|
||||
github.com/minio/minio-go/v7 v7.0.97/go.mod h1:re5VXuo0pwEtoNLsNuSr0RrLfT/MBtohwdaSmPPSRSk=
|
||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
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.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww=
|
||||
github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
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.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
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=
|
||||
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