29 Commits

Author SHA1 Message Date
Thomas Leister
07d0882c65 Introduces version string in log message
Adds a version string to enable the user to distinguish between
multiple Prosody-Filer versions. The version information is "baked in"
via the linker in build.sh
2020-04-20 21:20:39 +02:00
Thomas Leister
016ffd9fa4 Merge branch 'weiss-return-201' into develop 2020-01-12 10:47:18 +01:00
Holger Weiß
95c3ac899f Return status code 201 on PUT success
XEP-0363 (version 0.9.0) says:

| An HTTP status code of 201 means that the server is now ready to serve
| the file via the provided GET URL.

Some clients assume the upload failed when receiving a status code of
200, so we now return 201 instead.
2020-01-11 18:08:28 +01:00
Thomas Leister
3bc8446a6d Updates README: Fixes tidy up command 2020-01-08 20:19:08 +01:00
Thomas Leister
aabe581148 Fix directory listing issue also in alternative Nginx config 2020-01-08 20:11:14 +01:00
Thomas Leister
7dff0209f5 Fixes #14: Prevents directory listing in subdirectories
This commit prevents directory listings in every directory level,
not just on root level ("upload/").

In earlier versions, an attacker could get a list of past uploads
if he ever received a valid file download path, removed the file name and
descendet in parent directories.
2020-01-08 19:38:58 +01:00
Thomas Leister
65ca295289 Updates README.md 2019-08-07 20:08:32 +02:00
Thomas Leister
3e06fb8d3d Adds ipv6 support to doc and examples 2019-05-15 17:41:18 +02:00
Thomas Leister
6e287dbe56 Fixes Nginx config mistake in README.md 2019-05-11 12:13:07 +02:00
Thomas Leister
37234aadd5 Fixes #10 - Adds CORS OPTIONS support 2019-05-10 19:16:37 +02:00
Thomas Leister
75a5bf2316 Merge branch 'master' of github.com:ThomasLeister/prosody-filer 2019-04-29 20:54:45 +02:00
Thomas Leister
669e4c89e0 Readme.de: Add section about Ejabberd, general improvements 2019-04-29 20:54:28 +02:00
Thomas Leister
6a2e8cde1f Adds Apache config 2019-02-07 17:40:38 +01:00
Thomas Leister
22c2c4f702 Adds CORS headers 2019-01-01 18:56:22 +01:00
Thomas Leister
437df5266b Fixes #11 2018-12-30 14:22:06 +01:00
Thomas Leister
30eca2478c Fixes error code on copy fail 2018-07-29 16:43:56 +02:00
Thomas Leister
1f4d6d89dc Adds curl command 2018-07-29 16:40:50 +02:00
Thomas Leister
7db86c67b1 Opens files differently. Error on exiting file 2018-07-29 16:38:34 +02:00
Thomas Leister
671ce83693 Fix typo 2018-07-11 14:46:57 +02:00
Thomas Leister
0ced49e323 Fixes #7 2018-07-11 13:41:12 +02:00
Thomas Leister
b5dc800922 Merge pull request #6 from benediktg/nginx-config
Let files be delivered by nginx directly
2018-07-09 21:16:50 +02:00
Benedikt Geissler
51ec5a44b6 Add additonal nginx configuration as an alternative 2018-07-09 13:02:59 +02:00
Benedikt Geissler
4a5735c463 Let files be delivered by nginx directly 2018-07-08 14:31:45 +02:00
Thomas Leister
45c8da4111 Merge pull request #2 from xamanu/master
Avoid serving overview of main directory
2018-07-08 11:28:14 +02:00
Felix Delattre
9054a5db4c Avoid serving overview of main directory 2018-07-07 02:49:12 +02:00
Thomas Leister
819d418fb1 Adds sets proxy buffer to off 2018-07-04 15:50:23 +02:00
Thomas Leister
8c3743a450 Adds hint about Ejabberd 2018-07-04 08:07:51 +02:00
Thomas Leister
6470a3579b Fix mistake in cronjob command 2018-07-02 14:15:32 +02:00
Thomas Leister
e50279a564 Updates README: Adds statement of Matthew Wild 2018-07-02 13:01:03 +02:00
5 changed files with 306 additions and 56 deletions

236
README.md
View File

@@ -1,17 +1,31 @@
# Prosody Filer
A simple file server for handling XMPP http_upload requests. This server is meat to be used with the Prosody [mod_http_upload_external](https://modules.prosody.im/mod_http_upload_external.html) module.
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.
**Why should I use this server?**
**Despite the name, this server is also compatible with Ejabberd and Ejabberd's http_upload module!**
* Prosody's integrated http_upload server seems to be memory leaking.
---
## Why should I use this server?
* Prosody developers recommend using http_upload_external instead of http_upload (Matthew Wild on the question if http_upload is memory leaking):
> "BTW, I am not aware of any memory leaks in the HTTP upload code. However it is known to be very inefficient.
> That's why it has a very low upload limit, and **we encourage people to use mod_http_upload_external instead**.
> We set out to write a good XMPP server, not HTTP server (of which many good ones already exist), so our HTTP server is optimised for small bits of data, like BOSH and websocket.
> Handling large uploads and downloads was not a goal (and implementing a great HTTP server is not a high priority for the project compared to other things).
> **Our HTTP code buffers the entire upload into memory.
> More, it does it in an inefficient way that can use up to 4x the actual size of the data (if the data is large).
> So uploading a 10MB file can in theory use 40MB RAM.**
> But it's not a leak, the RAM is later cleared and reused. [...]
> The GC will free the memory at some point, but the OS may still report that Prosody is using that memory due to the way the libc allocator works.
> Most long lived processes behave this way (only increasing RAM, rarely decreasing)."
* This server works without any script interpreters or additional dependencies. It is delivered as a binary.
* Go is very good at serving HTTP requests.
* Go is very good at serving HTTP requests and "made for this task".
## Download
## Download
If you are using regular x86_64 Linux, you can download a finished binary for your system on the [release page](https://github.com/ThomasLeister/prosody-filer/releases). **No need to compile this application yourself**.
If you are using regular x86_64 Linux, you can download a finished binary for your system on the [release page](https://github.com/ThomasLeister/prosody-filer/releases). **No need to compile this application yourself**.
## Build (optional)
@@ -20,21 +34,27 @@ If you're using something different than a x64 Linux, you need to compile this a
To compile the server, you need a full Golang development environment. This can be set up quickly: https://golang.org/doc/install#install
Then checkout this repo:
Then checkout this repo:
go get github.com/ThomasLeister/prosody-filer
```sh
go get github.com/ThomasLeister/prosody-filer
```
and switch to the new directory:
and switch to the new directory:
cd $GOPATH/src/github.com/ThomasLeister/prosody-filer
```sh
cd $GOPATH/src/github.com/ThomasLeister/prosody-filer
```
The application can now be build:
The application can now be build:
### Build static binary
./build.sh
```sh
### Build static binary
./build.sh
### OR regular Go build
go build main.go
### OR regular Go build
go build main.go
```
## Set up / configuration
@@ -42,49 +62,72 @@ The application can now be build:
### Setup Prosody Filer environment
Create a new user for Prosody Filer to run as:
Create a new user for Prosody Filer to run as:
adduser --disabled-login --disabled-password prosody-filer
```sh
adduser --disabled-login --disabled-password prosody-filer
```
Switch to the new user:
su - prosody-filer
```sh
su - prosody-filer
```
Copy
Copy
* the binary ```prosody-filer``` and
* config ```config.example.toml```
* the binary ```prosody-filer``` and
* config ```config.example.toml```
to ```/home/prosody-filer/```. Rename the configuration to ```config.toml```.
Make sure the `prosody-filer` binary is executable:
```
chmod u+x prosody-filer
```
### Configure Prosody
Back in your root shell make sure ```mod_http_upload``` is **dis**abled and ```mod_http_upload_external``` is **en**abled! Then configure the external upload module:
```
```lua
http_upload_external_base_url = "https://uploads.myserver.tld/upload/"
http_upload_external_secret = "mysecret"
http_upload_external_file_size_limit = 50000000 -- 50 MB
```
Restart Prosody when you are finished:
Restart Prosody when you are finished:
systemctl restart prosody
### Alternative: Configure Ejabberd
Although this tool is named after Prosody, it can be used with Ejabberd, too! Make sure you have a Ejabberd configuration similar to this:
```yaml
mod_http_upload:
put_url: "https://uploads.@HOST@/upload"
external_secret: "mysecret"
max_size: 52428800
```
### Configure Prosody Filer
Prosody Filer configuration is done via the config.toml file in TOML syntax. There's not much to be configured:
```
### IP address and port to listen to, e.g. "127.0.0.1:5050"
listenport = "127.0.0.1:5050"
```toml
### IP address and port to listen to, e.g. "[::]:5050"
listenport = "[::1]:5050"
### Secret (must match the one in prosody.conf.lua!)
secret = "mysecret"
### Where to store the uploaded files
storeDir = "./uploads/"
storeDir = "./upload/"
### Subdirectory for HTTP upload / download requests (usually "upload/")
uploadSubDir = "upload/"
@@ -93,6 +136,10 @@ uploadSubDir = "upload/"
Make sure ```mysecret``` matches the secret defined in your mod_http_upload_external settings!
In addition to that, make sure that the nginx user or group can read the files uploaded
via prosody-filer if you want to have them served by nginx directly.
### Systemd service file
Create a new Systemd service file: ```/etc/systemd/system/prosody-filer.service```
@@ -107,11 +154,12 @@ Create a new Systemd service file: ```/etc/systemd/system/prosody-filer.service`
WorkingDirectory=/home/prosody-filer
User=prosody-filer
Group=prosody-filer
# Group=nginx # if the files should get served by nginx directly:
[Install]
WantedBy=multi-user.target
Reload the service definitions, enable the service and start it:
Reload the service definitions, enable the service and start it:
systemctl daemon-reload
systemctl enable prosody-filer
@@ -125,25 +173,38 @@ Done! Prosody Filer is now listening on the specified port and waiting for reque
Create a new config file ```/etc/nginx/sites-available/uploads.myserver.tld```:
server {
listen 80;
listen [::]:80;
listen 443 ssl;
listen [::]:443 ssl;
```nginx
server {
listen 80;
listen [::]:80;
listen 443 ssl;
listen [::]:443 ssl;
server_name uploads.myserver.tld;
server_name uploads.myserver.tld;
ssl_certificate /etc/letsencrypt/live/uploads.myserver.tld/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/uploads.myserver.tld/privkey.pem;
ssl_certificate /etc/letsencrypt/live/uploads.myserver.tld/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/uploads.myserver.tld/privkey.pem;
client_max_body_size 50m;
client_max_body_size 50m;
location /upload/ {
proxy_pass http://127.0.0.1:5050/upload/;
location /upload/ {
if ( $request_method = OPTIONS ) {
add_header Access-Control-Allow-Origin '*';
add_header Access-Control-Allow-Methods 'PUT, GET, OPTIONS, HEAD';
add_header Access-Control-Allow-Headers 'Authorization, Content-Type';
add_header Access-Control-Allow-Credentials 'true';
add_header Content-Length 0;
add_header Content-Type text/plain;
return 200;
}
}
Enable the new config:
proxy_pass http://[::]:5050/upload/;
proxy_request_buffering off;
}
}
```
Enable the new config:
ln -s /etc/nginx/sites-available/uploads.myserver.tld /etc/nginx/sites-enabled/
@@ -156,14 +217,101 @@ Reload Nginx:
systemctl reload nginx
#### Alternative configuration for letting Nginx serve the uploaded files
*(not officially supported - user contribution!)*
```nginx
server {
listen 80;
listen [::]:80;
listen 443 ssl;
listen [::]:443 ssl;
server_name uploads.myserver.tld;
ssl_certificate /etc/letsencrypt/live/uploads.myserver.tld/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/uploads.myserver.tld/privkey.pem;
location /upload/ {
if ( $request_method = OPTIONS ) {
add_header Access-Control-Allow-Origin '*';
add_header Access-Control-Allow-Methods 'PUT, GET, OPTIONS, HEAD';
add_header Access-Control-Allow-Headers 'Authorization, Content-Type';
add_header Access-Control-Allow-Credentials 'true';
add_header Content-Length 0;
add_header Content-Type text/plain;
return 200;
}
root /home/prosody-filer;
autoindex off;
client_max_body_size 51m;
client_body_buffer_size 51m;
try_files $uri $uri/ @prosodyfiler;
}
location @prosodyfiler {
proxy_pass http://[::1]:5050;
proxy_buffering off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host:$server_port;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-For $remote_addr;
}
}
```
## apache2 configuration (alternative to Nginx)
*(This configuration was provided by a user and has never been tested by the author of Prosody Filer. It might be outdated and might not work anymore)*
```
<VirtualHost *:80>
ServerName upload.example.eu
RedirectPermanent / https://upload.example.eu/
</VirtualHost>
<VirtualHost *:443>
ServerName upload.example.eu
SSLEngine on
SSLCertificateFile "Path to the ca file"
SSLCertificateKeyFile "Path to the key file"
Header always set Public-Key-Pins: ''
Header always set Strict-Transport-Security "max-age=63072000; includeSubdomains; preload"
H2Direct on
<Location /upload>
Header always set Access-Control-Allow-Origin "*"
Header always set Access-Control-Allow-Headers "Content-Type"
Header always set Access-Control-Allow-Methods "OPTIONS, PUT, GET"
RewriteEngine On
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule ^(.*)$ $1 [R=200,L]
</Location>
SSLProxyEngine on
ProxyPreserveHost On
ProxyRequests Off
ProxyPass / http://localhost:5050/
ProxyPassReverse / http://localhost:5050/
</VirtualHost>
```
## Automatic purge
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 /var/lib/prosody/uploads -maxdepth 0 -type d -mtime +28 | xargs rm -rf
@daily find /home/prosody-filer/upload/ -type d -mtime +28 | xargs rm -rf
This will delete uploads older than 28 days.
This will delete uploads older than 28 days.
## Check if it works
@@ -173,5 +321,3 @@ Get the log via
journalctl -f -u prosody-filer
If your XMPP clients uploads or downloads any file, there should be some log messages on the screen.

View File

@@ -1,7 +1,10 @@
#!/bin/sh
#!/bin/bash
##
## Builds static prosody-filer binary
##
### get VERSIONSTRING
VERSIONSTRING="$(git describe --tags --exact-match || git rev-parse --short HEAD)"
echo "Building version ${VERSIONSTRING} of Prosody-Filer ..."
### Compile and link statically
CGO_ENABLED=0 GOOS=linux go build -a -ldflags "-extldflags '-static' -w -s -X main.versionString=${VERSIONSTRING}" prosody-filer.go
CGO_ENABLED=0 GOOS=linux go build -a -ldflags '-extldflags "-static"' .

View File

@@ -2,8 +2,8 @@
### Server configuration
### (rename this file to "config.toml"!)
### IP address and port to listen to, e.g. "127.0.0.1:5050"
listenport = "127.0.0.1:5050"
### 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"

View File

@@ -36,6 +36,18 @@ type Config struct {
}
var conf Config
var versionString string = "0.0.0"
/*
* 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
@@ -55,7 +67,10 @@ func handleRequest(w http.ResponseWriter, r *http.Request) {
log.Println("Failed to parse URL query params:", err)
}
fileStorePath := strings.TrimLeft(u.Path, conf.UploadSubDir)
fileStorePath := strings.TrimPrefix(u.Path, "/"+conf.UploadSubDir)
// Add CORS headers
addCORSheaders(w)
if r.Method == "PUT" {
// Check if MAC is attached to URL
@@ -83,7 +98,7 @@ func handleRequest(w http.ResponseWriter, r *http.Request) {
// Make sure the path exists
os.MkdirAll(filepath.Dir(conf.Storedir+fileStorePath), os.ModePerm)
file, err := os.Create(conf.Storedir + fileStorePath)
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)
@@ -94,11 +109,12 @@ func handleRequest(w http.ResponseWriter, r *http.Request) {
n, err := io.Copy(file, r.Body)
if err != nil {
log.Println("Writing to new file failed:", err)
http.Error(w, "409 Conflict", 409)
http.Error(w, "500 Internal Server Error", 500)
return
}
log.Println("Successfully written", n, "bytes to file", fileStorePath)
w.WriteHeader(http.StatusCreated)
} else {
log.Println("Invalid MAC.")
http.Error(w, "403 Forbidden", 403)
@@ -122,6 +138,11 @@ func handleRequest(w http.ResponseWriter, r *http.Request) {
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"
}
@@ -172,7 +193,7 @@ func main() {
/*
* Start HTTP server
*/
log.Println("Starting up XMPP HTTP upload server ...")
log.Println("Starting Prosody-Filer", versionString, "...")
http.HandleFunc("/"+conf.UploadSubDir, handleRequest)
log.Printf("Server started on port %s. Waiting for requests.\n", conf.Listenport)
http.ListenAndServe(conf.Listenport, nil)

View File

@@ -1,13 +1,55 @@
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)
@@ -46,6 +88,9 @@ func TestUploadValid(t *testing.T) {
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) {
@@ -142,6 +187,9 @@ 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)
@@ -159,12 +207,18 @@ func TestDownloadHead(t *testing.T) {
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)
@@ -182,4 +236,30 @@ func TestDownloadGet(t *testing.T) {
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())
}
}