This repo holds a simple caddyserver v2 upload handler
|
Warning
|
You have to secure the upload URL as the nature of this handler/plugin is to save uploaded artifacts on the disc. |
In this handler are the following modules in use.
-
templates
-
file_server
-
upload :-)
|
Important
|
The relative paths are relative to the current web root
directory http.vars.root!
|
| Name | Description |
|---|---|
APPPORT |
The Port on which Caddyserver should listen. The format depends which Configuration file format you use. See the official Caddyserver documentation for the format Network addresses |
SKIP_LOG |
With this variable can you control if the health check will be logged or not. This feature was implemented as part of the vars handler. |
| Name | Description |
|---|---|
dest_dir |
The directory in which the files will be uploaded |
root_dir |
The root directory for this module. When not set will the root
directory be evaluated at runtime from this variable |
file_field_name |
The field name for the multi-part form upload |
fixed_file_name |
When set, all uploaded files will be renamed to the provided file name. |
response_template |
The response template after a upload was successfully |
max_form_buffer |
The maximum buffer size for ParseMultipartForm. It accepts all size values supported by go-humanize. Here can also be used a Placeholder The default size is 1G. |
max_form_buffer_int |
The maximum buffer size for ParseMultipartForm. |
max_filesize |
is the maximum size in bytes allowed for the upload.
It accepts all size values supported by go-humanize. Reads of more bytes will return an error with HTTP status 413. Here can also be used a Placeholder The default size is 1G. |
max_filesize_int |
is the maximum size in bytes allowed for the upload. Reads of more bytes will return an error with HTTP status 413. |
notify_url |
After a successful upload will this URL be called. The only supported schema is https. |
notify_method |
The default method is |
insecure |
This boolean flag configure the |
capath |
This is the Parameter where you can define the CA filename for the notify_url. |
create_uuid_dir |
If set to |
dest_dir_field_name |
If set, the dest_dir will be set from the specified form value. |
pass_thru |
If set to |
allowed_extensions |
List of allowed file extensions, e.g. |
allowed_mime_types |
List of allowed MIME types, e.g. |
on_abort |
What to do when an event handler aborts the |
The following Variables will be added to the request flow
| Name | Description |
|---|---|
http.upload.filename |
The uploaded filename |
http.upload.filesize |
The uploaded filesize |
http.upload.directory |
The directory where the file is uploaded |
http.upload.uuiddir |
The name of the random directory created if |
Because I prefer the JSON Config will I write here the configuration snipplet in JSON Syntax.
"handle": [
{
"MyTlsSetting": {},
"dest_dir": "upload", (1)
"handler": "upload",
"max_filesize": "5GB", (2)
"max_form_buffer_int": 1000000, (3)
"response_template": "upload-resp.txt" (4)
}
]-
Destination Directory on the Server site
-
Maximal possible upload size
-
Maximal buffer for uploading
-
the response template which will be used for response after upload
A full working example is in
docker-files/opt/webroot/config/Caddyfile-upload.json
Here a example Caddyfile which expects that the environment variable
APPPORT is set.
{
order upload before file_server
log {
level DEBUG
}
}
{$APPPORT} {
root .
file_server browse
templates
@mypost method POST
upload @mypost {
dest_dir tmp-upl
max_form_buffer 1G
max_filesize 4MB
response_template templates/upload-resp-template.txt
}
log {
output file access.log
}
}After a file was written successfully the upload handler emits the
Caddy event upload_finished via the built-in
events app.
Subscribed event handlers can use the following placeholders.
| Name | Description |
|---|---|
{event.data.filename} |
The file name (sent by the client, or |
{event.data.filesize} |
The file size in bytes |
{event.data.directory} |
The directory where the file is stored |
{event.data.filepath} |
The path of the stored file ( |
{event.data.uuiddir} |
The name of the UUID directory, empty without |
{event.data.requuid} |
The |
The container image contains the events.handlers.exec
module (experimental) to run a program for an event.
docker-files/opt/webroot/config/Caddyfile-events is a working demo
which runs docker-files/opt/webroot/scripts/on-upload.sh after every upload.
{
order upload before file_server
events {
on upload_finished exec /opt/webroot/scripts/on-upload.sh {event.data.filepath} {event.data.filesize} {event.data.filename} {event.data.directory} {event.data.uuiddir} {event.data.requuid} {event.id} {event.name} {event.time} {event.time_unix} {event.module}
}
}By default exec runs the program in the background, the HTTP response does
not wait for it and its exit code is ignored by the upload handler.
Caddy logs background command failed when the program exits with a non-zero
code or is killed after the timeout (default 30s), a successful run is not logged.
The output of the program goes to the stdout/stderr of Caddy.
|
Note
|
Programs running in the background survive config reloads, only the
exec timeout ends them. Caddy does not wait for them when it exits, so a
stop or restart of Caddy (or of its container) can end them in the middle of
their work.
|
The program runs once per upload, in parallel and without any limit on the number of running instances. It must be robust and short-lived:
-
Finish fast. Best is a small "write to queue" program, which only hands over the file (e.g. moves it into a spool directory or pushes a job to a message queue) and leaves long running tasks (scan, conversion, transfer to other systems, …) to a service outside of Caddy.
-
Limit the runtime yourself. When the
exectimeout (default 30s) hits, only the program itself is killed, not the processes it started. A shell script killed this way leaves its running commands behind, they keep running as orphans. Wrap slow commands withtimeout <seconds>below theexectimeout (as the demo script does), and useexec <command>for the last command of a shell script. -
Don’t start background processes (
&,nohup, …), they escape theexectimeout as well. -
Handle errors. A failing background program is only logged by Caddy (
background command failed), nobody retries it. In the foreground, decide whether an error rejects the upload (fail closed, like the demo script) or not. -
In a container, run Caddy with an init process (e.g.
podman run --init,docker run --initortini), so that orphaned processes are reaped.
To reject an upload, e.g. after a virus scan, the program must run in the
foreground with abort_codes, and the upload handler needs on_abort abort.
The Caddyfile syntax of exec only supports the command and its arguments,
so this needs the JSON config:
"events": {
"subscriptions": [
{
"events": ["upload_finished"],
"handlers": [
{
"handler": "exec",
"command": "/opt/webroot/scripts/on-upload.sh",
"args": ["{event.data.filepath}", "{event.data.filesize}", "{event.data.filename}", "{event.data.directory}", "{event.data.uuiddir}", "{event.data.requuid}", "{event.id}", "{event.name}", "{event.time}", "{event.time_unix}", "{event.module}"],
"foreground": true,
"abort_codes": [3]
}
]
}
]
}The demo script exits with code 3 when the file contains the EICAR test string. The upload then gets HTTP status 422 and the file is deleted.
In the foreground the HTTP response waits for the program. During this time
no data is sent to the client, so keep the runtime well below the smallest
timeout between client and Caddy. Caddy itself has no write timeout by default,
but reverse proxies and load balancers in front of it often have one, e.g.
OpenShift routes 30s, nginx proxy_read_timeout 60s, Cloudflare 100s.
If such a timeout hits, the client gets an error (e.g. 504) while Caddy
continues: the file may be stored anyway, or a 422 never reaches the client.
|
Warning
|
When exec kills the program after its timeout (default 30s), the
exit code is not one of the abort_codes and the event is not aborted:
the upload is accepted. To fail closed, limit the runtime inside the program
below the exec timeout and exit with an abort code when the limit is reached.
The demo script does this with timeout and the environment variable
SCAN_TIMEOUT (default 20s). SCAN_DELAY simulates a slow scan for testing.
|
Security notes:
-
The file name is chosen by the client. Pass it only as a separate argument and never into a shell like
sh -c "… {event.data.filename}", this would allow command injection. -
Prefer
{event.data.filepath}: it starts with the upload directory and can’t be mistaken for an option of the program, unlike a file name such as-rf.
You can get this image from docker hub
The default listen port must be defined with this variable
APPPORT=:2011
podman run --rm --network host --name caddy-test \
--env APPPORT=:8888 -it \
docker.io/me2digital/caddyv2-upload:latest
# or
docker run --name caddy-test --rm \
docker.io/me2digital/caddyv2-upload:latestYou can use the examples in the directory openshift to deploy the caddy uploader
into your OpenShift or kubernetes Cluster.
oc new-project caddyupload
oc -n caddyupload apply -f openshift/To adopt the used Caddyfile then can you easily do this with a configmap. The commands below show the steps.
# add configure change trigger
oc -n caddyupload \
set triggers \
deployment.apps/caddy-upload \
--from-config
# create configmap
oc -n caddyupload \
create configmap \
caddyfile-json \
--from-file=Caddyfile-upload.json=<PATH_TO_LOCAL>/Caddyfile-upload.json
# add configmap to caddy deployment
oc -n caddyupload \
set volumes \
deployment.apps/caddy-upload \
--add --configmap-name=caddyfile-json \
--mount-path=/opt/webroot/config/Caddyfile-upload.json \
--name=caddyfile-json \
--sub-path=Caddyfile-upload.json \
--type=configmapIn case the first shot of the configuration file does not work can you use the following commands to recreate the configuration file.
oc -n caddyupload-nis delete configmap caddyfile-json \
&& oc -n caddyupload-nis create configmap caddyfile-json --from-file=Caddyfile-upload.json=<PATH_TO_LOCAL>/Caddyfile-upload.json
# output of the commands
configmap "caddyfile-json" deleted
configmap/caddyfile-json createdThere is a builtin health handler with the path /health which just returns
200 and Okay.
The log output can be controled via the environment variable SKIP_LOG.
When you run the Image with port 8888 can you use curl or any other tool to post (upload) files
It’s not necessary to use -X POST as written in this Blog post
UNNECESSARY USE OF CURL -X
Here a example call with curl
curl -v --form myFile=@README.adoc http://localhost:8888/templates/upload-template.html
* Trying 127.0.0.1:8888...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8888 (#0)
> POST /templates/upload-template.html HTTP/1.1
> Host: localhost:8888
> User-Agent: curl/7.68.0
> Accept: */*
> Content-Length: 2492
> Content-Type: multipart/form-data; boundary=------------------------58b770bc61c0e691
> Expect: 100-continue
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 100 Continue
* We are completely uploaded and fine
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Accept-Ranges: bytes
< Content-Length: 299
< Etag: "rbb1gx8b"
< Last-Modified: Tue, 03 May 2022 11:34:09 GMT
< Server: Caddy
< Date: Thu, 19 May 2022 21:45:07 GMT
<
http.request.uri.path: {{placeholder "http.request.uri.path"}}
http.request.uuid {{placeholder "http.request.uuid" }}
http.request.host {{placeholder "http.request.host" }}
http.upload.filename: {{placeholder "http.upload.filename"}}
http.upload.filesize: {{placeholder "http.upload.filesize"}}The max_form_buffer paramater will be directly passed to readForm function and is used to check if the uploaded file should be saved temporarly on disk or keep it in the memory. This have dicret impact into the performance and disk usage of that module. Keep in mind when this paramter is low and the upload is a big file then will be there a lot of disk io.
INFO: The observation from https://github.com/etherwvlf in issue Memory issues on large uploads was that the initial memory usage is 7-8 times higher then the configured max_form_buffer size.