Compare commits
10 Commits
v1.0.0-rc3
...
v1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22c2c4f702 | ||
|
|
437df5266b | ||
|
|
30eca2478c | ||
|
|
1f4d6d89dc | ||
|
|
7db86c67b1 | ||
|
|
671ce83693 | ||
|
|
0ced49e323 | ||
|
|
b5dc800922 | ||
|
|
51ec5a44b6 | ||
|
|
4a5735c463 |
68
README.md
68
README.md
@@ -1,6 +1,6 @@
|
||||
# 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.
|
||||
|
||||
*(This module can also be used with future versions of Ejabberd: https://github.com/processone/ejabberd/commit/fface33d54f24c777dbec96fda6bd00e665327fe)*
|
||||
|
||||
@@ -21,9 +21,9 @@ A simple file server for handling XMPP http_upload requests. This server is meat
|
||||
* Go is very good at serving HTTP requests.
|
||||
|
||||
|
||||
## 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)
|
||||
@@ -32,15 +32,15 @@ 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
|
||||
|
||||
and switch to the new directory:
|
||||
and switch to the new directory:
|
||||
|
||||
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
|
||||
@@ -54,7 +54,7 @@ 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
|
||||
|
||||
@@ -62,10 +62,10 @@ Switch to the new user:
|
||||
|
||||
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```.
|
||||
|
||||
@@ -80,7 +80,7 @@ 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
|
||||
|
||||
@@ -96,7 +96,7 @@ listenport = "127.0.0.1:5050"
|
||||
secret = "mysecret"
|
||||
|
||||
### Where to store the uploaded files
|
||||
storeDir = "./uploads/"
|
||||
storeDir = "./upload/"
|
||||
|
||||
### Subdirectory for HTTP upload / download requests (usually "upload/")
|
||||
uploadSubDir = "upload/"
|
||||
@@ -105,6 +105,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```
|
||||
@@ -119,11 +123,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
|
||||
@@ -156,7 +161,7 @@ Create a new config file ```/etc/nginx/sites-available/uploads.myserver.tld```:
|
||||
}
|
||||
}
|
||||
|
||||
Enable the new config:
|
||||
Enable the new config:
|
||||
|
||||
ln -s /etc/nginx/sites-available/uploads.myserver.tld /etc/nginx/sites-enabled/
|
||||
|
||||
@@ -168,15 +173,44 @@ Reload Nginx:
|
||||
|
||||
systemctl reload nginx
|
||||
|
||||
#### Configuration for letting nginx serve the uploaded files
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name xmppserver.tld;
|
||||
|
||||
# ...
|
||||
|
||||
location /upload/ {
|
||||
root /home/prosody-filer;
|
||||
client_max_body_size 51m;
|
||||
client_body_buffer_size 51m;
|
||||
try_files $uri $uri/ @prosodyfiler;
|
||||
}
|
||||
location @prosodyfiler {
|
||||
proxy_pass http://127.0.0.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;
|
||||
}
|
||||
|
||||
# ...
|
||||
}
|
||||
```
|
||||
|
||||
## 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 /home/prosody-filer/uploads -maxdepth 0 -type d -mtime +28 | xargs rm -rf
|
||||
@daily find /home/prosody-filer/upload/* -maxdepth 0 -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
|
||||
@@ -186,5 +220,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.
|
||||
|
||||
|
||||
|
||||
21
main.go
21
main.go
@@ -37,6 +37,18 @@ type Config struct {
|
||||
|
||||
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
|
||||
@@ -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,7 +109,7 @@ 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
|
||||
}
|
||||
|
||||
|
||||
57
main_test.go
57
main_test.go
@@ -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,6 +236,9 @@ 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) {
|
||||
|
||||
Reference in New Issue
Block a user