├── .gitignore
├── .codecov.yml
├── config
├── upload.html
├── t
├── methods.t
├── error_handling.t
├── misc_directives.t
├── lib
│ └── Test
│ │ └── Nginx
│ │ ├── UploadModule.pm
│ │ └── UploadModule
│ │ └── TestServer.pm
├── http2.t
├── upload.t
└── aggregate_fields.t
├── example.php
├── LICENCE
├── nginx.conf
├── LICENCE.ru
├── .travis.yml
├── Changelog
├── upload-protocol.md
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | /t/servroot
2 | *.gcov
3 | /nginx/
--------------------------------------------------------------------------------
/.codecov.yml:
--------------------------------------------------------------------------------
1 | comment:
2 | require_changes: yes
3 | layout: "header, diff, flags, files, footer"
4 |
5 | coverage:
6 | status:
7 | project: no
8 | patch: no
9 | changes: no
10 | range: 60..100
11 | round: down
12 | precision: 2
13 |
--------------------------------------------------------------------------------
/config:
--------------------------------------------------------------------------------
1 | USE_MD5=YES
2 | USE_SHA1=YES
3 | USE_OPENSSL=YES
4 | ngx_addon_name=ngx_http_upload_module
5 |
6 | if test -n "$ngx_module_link"; then
7 | ngx_module_type=HTTP
8 | ngx_module_name=$ngx_addon_name
9 | ngx_module_srcs="$ngx_addon_dir/ngx_http_upload_module.c"
10 |
11 | . auto/module
12 | else
13 | HTTP_MODULES="$HTTP_MODULES ngx_http_upload_module"
14 | NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_upload_module.c"
15 | fi
16 |
--------------------------------------------------------------------------------
/upload.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Test upload
4 |
5 |
6 | Select files to upload
7 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/t/methods.t:
--------------------------------------------------------------------------------
1 | use strict;
2 | use warnings;
3 |
4 | use File::Basename qw(dirname);
5 |
6 | use lib dirname(__FILE__) . "/lib";
7 |
8 | use Test::Nginx::Socket tests => 2;
9 | use Test::More;
10 | use Test::Nginx::UploadModule;
11 |
12 | no_long_string();
13 | no_shuffle();
14 | run_tests();
15 |
16 | __DATA__
17 | === TEST 1: OPTIONS request
18 | --- config
19 | location /upload/ {
20 | upload_pass @upstream;
21 | upload_resumable on;
22 | upload_set_form_field "upload_tmp_path" "$upload_tmp_path";
23 | upload_cleanup 400 404 499 500-505;
24 | }
25 | --- request
26 | OPTIONS /upload/
27 | --- error_code: 200
28 |
29 | === TEST 2: http2 OPTIONS request
30 | --- http2
31 | --- config
32 | location /upload/ {
33 | upload_pass @upstream;
34 | upload_resumable on;
35 | upload_set_form_field "upload_tmp_path" "$upload_tmp_path";
36 | upload_cleanup 400 404 499 500-505;
37 | }
38 | --- request
39 | OPTIONS /upload/
40 | --- error_code: 200
41 |
--------------------------------------------------------------------------------
/example.php:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 | Test upload
8 |
9 |
10 |
11 | if ($_POST){
12 | echo "Uploaded files:
";
13 | echo "";
14 |
15 | echo "| Name | Location | Content type | MD5 | Size |
";
16 |
17 | for ($i=1;$i<=$slots;$i++){
18 | $key = $header_prefix.$i;
19 | if (array_key_exists($key."_name", $_POST) && array_key_exists($key."_path",$_POST)) {
20 | $tmp_name = $_POST[$key."_path"];
21 | $name = $_POST[$key."_name"];
22 | $content_type = $_POST[$key."_content_type"];
23 | $md5 = $_POST[$key."_md5"];
24 | $size = $_POST[$key."_size"];
25 |
26 | echo "| $name | $tmp_name | $content_type | $md5 | $size | ";
27 | }
28 | }
29 |
30 | echo "
";
31 |
32 | }else{?>
33 | Select files to upload
34 |
44 | }
45 | ?>
46 |
47 |
48 |
--------------------------------------------------------------------------------
/LICENCE:
--------------------------------------------------------------------------------
1 | * Copyright (c) 2006, 2008, Valery Kholodkov
2 | * All rights reserved.
3 | *
4 | * Redistribution and use in source and binary forms, with or without
5 | * modification, are permitted provided that the following conditions are met:
6 | * * Redistributions of source code must retain the above copyright
7 | * notice, this list of conditions and the following disclaimer.
8 | * * Redistributions in binary form must reproduce the above copyright
9 | * notice, this list of conditions and the following disclaimer in the
10 | * documentation and/or other materials provided with the distribution.
11 | * * Neither the name of the Valery Kholodkov nor the
12 | * names of its contributors may be used to endorse or promote products
13 | * derived from this software without specific prior written permission.
14 | *
15 | * THIS SOFTWARE IS PROVIDED BY VALERY KHOLODKOV ''AS IS'' AND ANY
16 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
17 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18 | * DISCLAIMED. IN NO EVENT SHALL VALERY KHOLODKOV BE LIABLE FOR ANY
19 | * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
20 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
21 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
22 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
24 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25 |
--------------------------------------------------------------------------------
/nginx.conf:
--------------------------------------------------------------------------------
1 |
2 | worker_processes 20;
3 |
4 | error_log logs/error.log notice;
5 |
6 | working_directory /usr/local/nginx;
7 |
8 | events {
9 | worker_connections 1024;
10 | }
11 |
12 | http {
13 | include mime.types;
14 | default_type application/octet-stream;
15 |
16 | server {
17 | listen 80;
18 | client_max_body_size 100m;
19 |
20 | # Upload form should be submitted to this location
21 | location /upload {
22 | # Pass altered request body to this location
23 | upload_pass @test;
24 |
25 | # Store files to this directory
26 | # The directory is hashed, subdirectories 0 1 2 3 4 5 6 7 8 9 should exist
27 | upload_store /tmp 1;
28 |
29 | # Allow uploaded files to be read only by user
30 | upload_store_access user:r;
31 |
32 | # Set specified fields in request body
33 | upload_set_form_field "${upload_field_name}_name" $upload_file_name;
34 | upload_set_form_field "${upload_field_name}_content_type" $upload_content_type;
35 | upload_set_form_field "${upload_field_name}_path" $upload_tmp_path;
36 |
37 | # Inform backend about hash and size of a file
38 | upload_aggregate_form_field "${upload_field_name}_md5" $upload_file_md5;
39 | upload_aggregate_form_field "${upload_field_name}_size" $upload_file_size;
40 |
41 | upload_pass_form_field "^submit$|^description$";
42 | }
43 |
44 | # Pass altered request body to a backend
45 | location @test {
46 | proxy_pass http://localhost:8080;
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/t/error_handling.t:
--------------------------------------------------------------------------------
1 | use strict;
2 | use warnings;
3 |
4 | use File::Basename qw(dirname);
5 |
6 | use lib dirname(__FILE__) . "/lib";
7 |
8 | use Test::Nginx::Socket tests => 5;
9 | use Test::More;
10 | use Test::Nginx::UploadModule;
11 |
12 | no_long_string();
13 | no_shuffle();
14 | run_tests();
15 |
16 | __DATA__
17 | === TEST 1: invalid content-range
18 | --- config
19 | location /upload/ {
20 | upload_pass @upstream;
21 | upload_resumable on;
22 | upload_set_form_field "upload_tmp_path" "$upload_tmp_path";
23 | upload_cleanup 400 404 499 500-505;
24 | }
25 | --- more_headers
26 | X-Content-Range: bytes 0-3/4
27 | X-Progress-ID: 0000000001
28 | Session-ID: 0000000001
29 | Content-Type: text/plain
30 | Content-Disposition: form-data; name="file"; filename="test.txt"\r
31 | --- request eval
32 | qq{POST /upload/
33 | testing}
34 | --- error_code: 416
35 | --- extra_tests eval
36 | use Test::File qw(file_not_exists_ok);
37 | sub {
38 | my $block = shift;
39 | file_not_exists_ok(
40 | "${ENV{TEST_NGINX_UPLOAD_PATH}}/store/1/0000000001", $block->name . '- tmp file deleted');
41 | }
42 |
43 | === TEST 2: invalid method
44 | --- config
45 | location /upload/ {
46 | upload_pass @upstream;
47 | upload_resumable on;
48 | upload_set_form_field "upload_tmp_path" "$upload_tmp_path";
49 | upload_cleanup 400 404 499 500-505;
50 | }
51 | --- more_headers
52 | X-Content-Range: bytes 0-3/4
53 | Session-ID: 2
54 | Content-Type: text/plain
55 | Content-Disposition: form-data; name="file"; filename="test.txt"
56 | --- request
57 | PUT /upload/
58 | --- error_code: 405
59 | --- extra_tests eval
60 | use Test::File qw(file_not_exists_ok);
61 | sub {
62 | my $block = shift;
63 | file_not_exists_ok(
64 | "${ENV{TEST_NGINX_UPLOAD_PATH}}/store/2/2", $block->name . '- tmp file deleted');
65 | }
66 |
--------------------------------------------------------------------------------
/LICENCE.ru:
--------------------------------------------------------------------------------
1 | * Copyright (c) 2006, 2008, Валерий Холодков
2 | *
3 | * Разрешается повторное распространение и использование как в виде исходного
4 | * кода, так и в двоичной форме, с изменениями или без, при соблюдении
5 | * следующих условий:
6 | *
7 | * * При повторном распространении исходного кода должно оставаться
8 | * указанное выше уведомление об авторском праве, этот список условий и
9 | * последующий отказ от гарантий.
10 | * * При повторном распространении двоичного кода должна сохраняться
11 | * указанная выше информация об авторском праве, этот список условий и
12 | * последующий отказ от гарантий в документации и/или в других
13 | * материалах, поставляемых при распространении.
14 | * * Ни имя Валерия Холодкова, ни имена вкладчиков не могут быть
15 | * использованы в качестве поддержки или продвижения продуктов,
16 | * основанных на этом ПО без предварительного письменного разрешения.
17 | *
18 | * ЭТА ПРОГРАММА ПРЕДОСТАВЛЕНА ВЛАДЕЛЬЦАМИ АВТОРСКИХ ПРАВ И/ИЛИ ДРУГИМИ
19 | * СТОРОНАМИ "КАК ОНА ЕСТЬ" БЕЗ КАКОГО-ЛИБО ВИДА ГАРАНТИЙ, ВЫРАЖЕННЫХ ЯВНО
20 | * ИЛИ ПОДРАЗУМЕВАЕМЫХ, ВКЛЮЧАЯ, НО НЕ ОГРАНИЧИВАЯСЬ ИМИ, ПОДРАЗУМЕВАЕМЫЕ
21 | * ГАРАНТИИ КОММЕРЧЕСКОЙ ЦЕННОСТИ И ПРИГОДНОСТИ ДЛЯ КОНКРЕТНОЙ ЦЕЛИ. НИ В
22 | * КОЕМ СЛУЧАЕ, ЕСЛИ НЕ ТРЕБУЕТСЯ СООТВЕТСТВУЮЩИМ ЗАКОНОМ, ИЛИ НЕ УСТАНОВЛЕНО
23 | * В УСТНОЙ ФОРМЕ, НИ ОДИН ВЛАДЕЛЕЦ АВТОРСКИХ ПРАВ И НИ ОДНО ДРУГОЕ ЛИЦО,
24 | * КОТОРОЕ МОЖЕТ ИЗМЕНЯТЬ И/ИЛИ ПОВТОРНО РАСПРОСТРАНЯТЬ ПРОГРАММУ, КАК БЫЛО
25 | * СКАЗАНО ВЫШЕ, НЕ НЕСЁТ ОТВЕТСТВЕННОСТИ, ВКЛЮЧАЯ ЛЮБЫЕ ОБЩИЕ, СЛУЧАЙНЫЕ,
26 | * СПЕЦИАЛЬНЫЕ ИЛИ ПОСЛЕДОВАВШИЕ УБЫТКИ, ВСЛЕДСТВИЕ ИСПОЛЬЗОВАНИЯ ИЛИ
27 | * НЕВОЗМОЖНОСТИ ИСПОЛЬЗОВАНИЯ ПРОГРАММЫ (ВКЛЮЧАЯ, НО НЕ ОГРАНИЧИВАЯСЬ
28 | * ПОТЕРЕЙ ДАННЫХ, ИЛИ ДАННЫМИ, СТАВШИМИ НЕПРАВИЛЬНЫМИ, ИЛИ ПОТЕРЯМИ
29 | * ПРИНЕСЕННЫМИ ИЗ-ЗА ВАС ИЛИ ТРЕТЬИХ ЛИЦ, ИЛИ ОТКАЗОМ ПРОГРАММЫ РАБОТАТЬ
30 | * СОВМЕСТНО С ДРУГИМИ ПРОГРАММАМИ), ДАЖЕ ЕСЛИ ТАКОЙ ВЛАДЕЛЕЦ ИЛИ ДРУГОЕ
31 | * ЛИЦО БЫЛИ ИЗВЕЩЕНЫ О ВОЗМОЖНОСТИ ТАКИХ УБЫТКОВ.
32 |
33 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: required
2 | language: c
3 | compiler: gcc
4 | dist: trusty
5 |
6 | cache:
7 | directories:
8 | - perl5
9 | - dl
10 |
11 | addons:
12 | apt:
13 | packages:
14 | - libssl-dev
15 |
16 | env:
17 | global:
18 | - TESTNGINX_VER=12152a5
19 | - PATH=/usr/local/bin:$TRAVIS_BUILD_DIR/nginx/objs:$PATH
20 | - CURL=7.58.0
21 | - NGHTTP2=1.24.0
22 | matrix:
23 | - NGINX_VERSION=1.9.15
24 | - NGINX_VERSION=1.11.13
25 | - NGINX_VERSION=1.12.2
26 | - NGINX_VERSION=1.13.8
27 |
28 | before_install:
29 | - mkdir -p dl
30 | - |
31 | if [ ! -f dl/nghttp2-${NGHTTP2}.tar.gz ]; then
32 | (cd dl && curl -O -L https://github.com/nghttp2/nghttp2/releases/download/v${NGHTTP2}/nghttp2-${NGHTTP2}.tar.gz)
33 | fi
34 | - |
35 | if [ ! -f dl/curl-${CURL}.tar.gz ]; then
36 | (cd dl && curl -O -L https://curl.haxx.se/download/curl-${CURL}.tar.gz)
37 | fi
38 | - |
39 | if [ ! -f dl/nginx-${NGINX_VERSION}.tar.gz ]; then
40 | curl -o dl/nginx-${NGINX_VERSION}.tar.gz -L http://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz;
41 | fi
42 | - |
43 | if [ ! -f dl/test-nginx-${TESTNGINX_VER}.tar.gz ]; then
44 | curl -o dl/test-nginx-${TESTNGINX_VER}.tar.gz -L "https://github.com/openresty/test-nginx/archive/${TESTNGINX_VER}.tar.gz";
45 | fi
46 | - if [ ! -f dl/cpanm ]; then curl -o dl/cpanm https://cpanmin.us/; chmod +x dl/cpanm; fi
47 | - |
48 | if [ ! -e dl/nghttp2-${NGHTTP2} ]; then
49 | (cd dl && tar -zxf nghttp2-${NGHTTP2}.tar.gz)
50 | fi
51 | (cd dl/nghttp2-${NGHTTP2} &&
52 | [ -f Makefile ] || ./configure --prefix=/usr --disable-threads &&
53 | make && sudo make install)
54 | - |
55 | if [ ! -e dl/curl-${CURL} ]; then
56 | (cd dl && tar -zxf curl-${CURL}.tar.gz)
57 | fi
58 | (cd dl/curl-${CURL} &&
59 | [ -f Makefile ] || ./configure --with-nghttp2 --prefix=/usr/local &&
60 | make && sudo make install)
61 | - sudo ldconfig
62 | - sudo cp dl/cpanm /usr/local/bin/cpanm
63 | - tar -zxf dl/nginx-${NGINX_VERSION}.tar.gz && mv nginx-${NGINX_VERSION} nginx
64 | - cpanm --notest --local-lib=perl5 local::lib && eval $(perl -I perl5/lib/perl5/ -Mlocal::lib=./perl5)
65 | - |
66 | if [ ! -f perl5/lib/perl5/Test/Nginx.pm ]; then
67 | cpanm --notest --local-lib=perl5 dl/test-nginx-${TESTNGINX_VER}.tar.gz
68 | fi
69 | - |
70 | if [ ! -f perl5/lib/perl5/Test/File.pm ]; then
71 | cpanm --notest --local-lib=perl5 Test::File
72 | fi
73 |
74 | install:
75 | - cd nginx
76 | - ./configure --with-http_v2_module --with-http_ssl_module --add-module=$TRAVIS_BUILD_DIR --with-cc-opt='-O0 -coverage' --with-ld-opt='-fprofile-arcs'
77 | - make
78 | - cd ..
79 |
80 | script:
81 | - prove --directives --verbose -r t
82 |
83 | after_success:
84 | - bash <(curl -s https://codecov.io/bash) -G '*ngx_http_upload_module*' -a '--object-directory nginx/objs/addon/nginx-upload-module *.c -s '"$TRAVIS_BUILD_DIR"
85 |
--------------------------------------------------------------------------------
/t/misc_directives.t:
--------------------------------------------------------------------------------
1 | use strict;
2 | use warnings;
3 |
4 | use File::Basename qw(dirname);
5 |
6 | use lib dirname(__FILE__) . "/lib";
7 |
8 | use Test::Nginx::Socket tests => 13;
9 | use Test::More;
10 | use Test::Nginx::UploadModule;
11 |
12 | no_long_string();
13 | no_shuffle();
14 | run_tests();
15 |
16 | __DATA__
17 | === TEST 1: upload_pass_args on should pass GET params
18 | --- config
19 | location /test/ {
20 | upload_pass /upload/;
21 | upload_resumable on;
22 | upload_set_form_field upload_file_name $upload_file_name;
23 | upload_pass_args on;
24 | }
25 |
26 | location /upload/ {
27 | proxy_pass http://upload_upstream_server;
28 | }
29 | --- more_headers
30 | X-Content-Range: bytes 0-3/4
31 | Session-ID: 1
32 | Content-Type: text/plain
33 | Content-Disposition: form-data; name="file"; filename="test.txt"
34 | --- request
35 | POST /test/?foo=bar
36 | test
37 | --- error_code: 200
38 | --- response_body
39 | foo = bar
40 | upload_file_name = test.txt
41 |
42 | === TEST 2: upload_pass_args off should strip GET params
43 | --- config
44 | location /test/ {
45 | upload_pass /upload/;
46 | upload_resumable on;
47 | upload_pass_args off;
48 | upload_set_form_field upload_file_name $upload_file_name;
49 | }
50 |
51 | location /upload/ {
52 | proxy_pass http://upload_upstream_server;
53 | }
54 | --- more_headers
55 | X-Content-Range: bytes 0-3/4
56 | Session-ID: 2
57 | Content-Type: text/plain
58 | Content-Disposition: form-data; name="file"; filename="test.txt"
59 | --- request
60 | POST /test/?foo=bar
61 | test
62 | --- error_code: 200
63 | --- response_body
64 | upload_file_name = test.txt
65 |
66 | === TEST 3: upload_tame_arrays on
67 | --- config
68 | location /upload/ {
69 | upload_pass @upstream;
70 | upload_resumable on;
71 | upload_tame_arrays on;
72 | upload_set_form_field upload_file_name $upload_file_name;
73 | }
74 | --- more_headers
75 | X-Content-Range: bytes 0-3/4
76 | Session-ID: 3
77 | Content-Type: text/plain
78 | Content-Disposition: form-data; name="file[]"; filename="test.txt"
79 | --- request
80 | POST /upload/
81 | test
82 | --- error_code: 200
83 | --- response_body
84 | upload_file_name = test.txt
85 |
86 | === TEST 4: upload_set_form_field multiple fields
87 | --- config
88 | location /upload/ {
89 | upload_pass @upstream;
90 | upload_resumable on;
91 | upload_set_form_field upload_field_name_and_file_name "$upload_field_name $upload_file_name";
92 | }
93 | --- more_headers
94 | X-Content-Range: bytes 0-3/4
95 | Session-ID: 4
96 | Content-Type: text/plain
97 | Content-Disposition: form-data; name="file"; filename="test.txt"
98 | --- request
99 | POST /upload/
100 | test
101 | --- error_code: 200
102 | --- response_body
103 | upload_field_name_and_file_name = file test.txt
104 |
105 | === TEST 5: upload_set_form_field variable key
106 | --- config
107 | location /upload/ {
108 | upload_pass @upstream;
109 | upload_resumable on;
110 | set $form_field_name "upload_file_name";
111 | upload_set_form_field "$form_field_name" "$upload_file_name";
112 | }
113 | --- more_headers
114 | X-Content-Range: bytes 0-3/4
115 | Session-ID: 5
116 | Content-Type: text/plain
117 | Content-Disposition: form-data; name="file"; filename="test.txt"
118 | --- request
119 | POST /upload/
120 | test
121 | --- error_code: 200
122 | --- response_body
123 | upload_file_name = test.txt
124 |
125 |
126 | === TEST 6: upload_add_header
127 | --- config
128 | location /upload/ {
129 | upload_pass @upstream;
130 | upload_resumable on;
131 | upload_add_header X-Upload-Filename $upload_file_name;
132 | upload_set_form_field upload_file_name $upload_file_name;
133 | }
134 | --- more_headers
135 | X-Content-Range: bytes 0-3/4
136 | Session-ID: 3
137 | Content-Type: text/plain
138 | Content-Disposition: form-data; name="file"; filename="test.txt"
139 | --- request
140 | POST /upload/
141 | test
142 | --- error_code: 200
143 | --- raw_response_headers_like: X-Upload-Filename: test\.txt
144 | --- response_body
145 | upload_file_name = test.txt
146 |
--------------------------------------------------------------------------------
/Changelog:
--------------------------------------------------------------------------------
1 | master (unreleased)
2 | * Added feature: http2 support. Thanks to Frankie Dintino.
3 |
4 | Version 2.2.1
5 | * Added feature: SHA-256 and SHA-512
6 |
7 | Version 2.2.0
8 | * Added feature: resumable uploads
9 | * Added feature: allow to use of $variables in "upload_pass" directive (Piotr Sikora)
10 | * Added feature: allow module's directives inside if statements (David Backeus)
11 | * Added feature: directive upload_tame_arrays and ability to do some magic with php arrays
12 |
13 | Version 2.0.12
14 | * Fixed bug: keepalive connection was hanging after upload has been completed.
15 | * Change: if request method is not POST return HTTP 405 in order to simplify configuration.
16 |
17 | Version 2.0.11
18 | * Fixed bug: rb->buf was uninitiazlied at some execution paths. Found by Andrew Filonov.
19 | * Fixed bug: dummy field would not appear whenever form contains only non-file fields.
20 | * Added feature: variable $upload_file_number which indicates ordinal number
21 | of file in request
22 | * Change: compatibility with nginx API 0.8.25 and greater
23 |
24 | Version 2.0.10
25 | * Change: compatibility with nginx API 0.8.11
26 | * Fixed bug: Prevent module from registering store path if no upload location
27 | was configured
28 | * Fixed bug: upload corrupted in case of short body + keepalive. Thanks to Dmitry
29 | Dedukhin.
30 | * Change: Return error 415 instead of 400 if request content type is not
31 | multipart/form-data
32 |
33 | Version 2.0.9
34 | * Change: compatibility with nginx's API 0.7.52 and greater
35 | * Fixed bug: module directives couldn't have appeared in limit_except block
36 | * Added feature: directive upload_limit_rate and ability to limit upload rate
37 | * Change: Malformed body issues are now logged to error log instead of debug log
38 |
39 | Version 2.0.8
40 | * Change: support for named locations
41 | * Fixed bug: crash on missing Content-Type request header
42 | * Fixed bug: compilation problem on amd 64
43 |
44 | Version 2.0.7
45 | * Change: file size and output body size restrictions
46 | * Added feature: directive upload_pass_args enables forwarding
47 | of request arguments to a backend. Thanks to Todd Fisher.
48 |
49 | Version 2.0.6
50 | * Fixed bug: zero variables in aggregate field name caused allocation
51 | of random amount of memory. Thanks to Dmitry Dedukhin.
52 | * Fixed bug: Prevent generation of a field in case of empty field name
53 |
54 | Version 2.0.5
55 | * Fixed bug: prevent leaking of file descriptors on a timeout (unconfirmed problem).
56 | * Fixed bug: variables in field values in upload_set_form_field and
57 | upload_aggregate_form_field directives were not working if field name
58 | contained 0 variables.
59 | * Added feature: directive upload_cleanup now specifies statuses,
60 | which initiate removal of uploaded files. Used for cleanup after
61 | failure of a backend.
62 | * Added feature: aggregate variable upload_file_crc32 allows to calculate
63 | CRC32 if file on the fly.
64 | * Fixed bug: Indicator of necessity to calculate SHA1 sum was not inheritable
65 | from server configuration.
66 |
67 | Version 2.0.4
68 | * Fixed bug: location configuration of upload_set_form_field and upload_pass_form_field
69 | was not inheritable from server configuration.
70 | * Added feature: directive upload_aggregate_form_field to pass aggragate properties
71 | of a file like file size, MD5 and SHA1 sums to backend.
72 | * Fixed bug: missing CRLF at the end of resulting body.
73 | * Change: optimized out some unnecessary memory allocations and zeroing.
74 |
75 | Version 2.0.3
76 | * upload_store directive was not able to receive more than one argument.
77 | As a result no hashed dirs for file uploads were possible.
78 | * upload_store_access directive did not work at all. Permissions were
79 | defaulted to user:rw. Thanks to Brian Moran.
80 | * In case of any errors at the last chunk of request body only 500 Internal Server Error
81 | was generated intead of 400 Bad Request and 503 Service Unavailable.
82 | * Fixed copyrights for temporary file name generation code
83 | * Fixed compilation issue on 0.6.32. Thanks to Tomas Pollak.
84 | * Added directive upload_pass_form_field to specify fields
85 | to pass to backend. Fixes security hole found by Brian Moran.
86 |
87 | Version 2.0.2
88 | * Fixed crash in logging filename while aborting upload
89 | * Added feasible debug logging
90 | * Added support for variables to generate form fields
91 | in resulting request body
92 | * Added missing logging of errno after write failures
93 | * Simplified upload abortion logic; simply discarding
94 | already added fields
95 | * Now returning explicit error code after script failures
96 | to be able to generate Internal server error
97 |
--------------------------------------------------------------------------------
/t/lib/Test/Nginx/UploadModule.pm:
--------------------------------------------------------------------------------
1 | package Test::Nginx::UploadModule;
2 | use v5.10.1;
3 | use strict;
4 | use warnings;
5 |
6 | my $PORT = $ENV{TEST_NGINX_UPSTREAM_PORT} ||= 12345;
7 | $ENV{TEST_NGINX_UPLOAD_PATH} ||= '/tmp/upload';
8 | $ENV{TEST_NGINX_UPLOAD_FILE} = $ENV{TEST_NGINX_UPLOAD_PATH} . "/test_data.txt";
9 |
10 |
11 | use base 'Exporter';
12 |
13 | use Test::Nginx::Socket;
14 | use Test::Nginx::Util qw($RunTestHelper);
15 | use Test::File qw(file_contains_like);
16 | use Test::More;
17 |
18 | use File::Path qw(rmtree mkpath);
19 | use Test::Nginx::UploadModule::TestServer;
20 |
21 |
22 | my ($server_pid, $server);
23 |
24 | sub kill_tcp_server() {
25 | $server->shutdown if defined $server;
26 | undef $server;
27 | kill INT => $server_pid if defined $server_pid;
28 | undef $server_pid;
29 | }
30 |
31 | sub make_upload_paths {
32 | mkpath("${ENV{TEST_NGINX_UPLOAD_PATH}}/stats");
33 | for (my $i = 0; $i < 10; $i++) {
34 | mkpath("${ENV{TEST_NGINX_UPLOAD_PATH}}/store/$i");
35 | }
36 | open(my $fh, ">", $ENV{TEST_NGINX_UPLOAD_FILE});
37 | print $fh ('x' x 131072);
38 | close($fh);
39 | }
40 |
41 | add_cleanup_handler(sub {
42 | kill_tcp_server();
43 | rmtree($ENV{TEST_NGINX_UPLOAD_PATH});
44 | });
45 |
46 | my $OldRunTestHelper = $RunTestHelper;
47 |
48 | my @ResponseChecks = ();
49 |
50 | my $old_check_response_headers = \&Test::Nginx::Socket::check_response_headers;
51 |
52 | sub new_check_response_headers ($$$$$) {
53 | my ($block, $res, $raw_headers, $dry_run, $req_idx, $need_array) = @_;
54 | $old_check_response_headers->(@_);
55 | if (!$dry_run) {
56 | for my $check (@ResponseChecks) {
57 | $check->(@_);
58 | }
59 | }
60 | }
61 |
62 | $RunTestHelper = sub ($$) {
63 | if (defined $server) {
64 | $OldRunTestHelper->(@_);
65 | } else {
66 | defined (my $pid = fork()) or bail_out "Can't fork: $!";
67 | if ($pid == 0) {
68 | $Test::Nginx::Util::InSubprocess = 1;
69 | if (!defined $server) {
70 | $server = Test::Nginx::UploadModule::TestServer->new({port=>$PORT});
71 | $server->run();
72 | exit 0;
73 | }
74 | } else {
75 | $server_pid = $pid;
76 | no warnings qw(redefine);
77 | Test::Nginx::UploadModule::TestServer::wait_for_port($PORT, \&bail_out);
78 | *Test::Nginx::Socket::check_response_headers = \&new_check_response_headers;
79 |
80 | $OldRunTestHelper->(@_);
81 |
82 | *Test::Nginx::Socket::check_response_headers = &$old_check_response_headers;
83 |
84 | kill_tcp_server();
85 | }
86 | }
87 | };
88 |
89 | my $default_http_config = <<'_EOC_';
90 | upstream upload_upstream_server {
91 | server 127.0.0.1:$TEST_NGINX_UPSTREAM_PORT;
92 | }
93 |
94 | log_format custom '$remote_addr - $remote_user [$time_local] '
95 | '"$request" $status $body_bytes_sent '
96 | '"$http_referer" "$http_user_agent" $request_time';
97 | _EOC_
98 |
99 |
100 | my $default_config = <<'_EOC_';
101 | location @upstream {
102 | internal;
103 | proxy_pass http://upload_upstream_server;
104 | }
105 | upload_store $TEST_NGINX_UPLOAD_PATH/store 1;
106 | upload_state_store $TEST_NGINX_UPLOAD_PATH/stats;
107 |
108 | access_log $TEST_NGINX_SERVER_ROOT/logs/access.log custom;
109 | _EOC_
110 |
111 | # Set default configs, create upload directories, and add extra_tests blocks to @ResponseChecks
112 | add_block_preprocessor(sub {
113 | my $block = shift;
114 |
115 | make_upload_paths();
116 |
117 | if (!defined $block->http_config) {
118 | $block->set_value('http_config', $default_http_config);
119 | } else {
120 | $block->set_value('http_config', $default_http_config . $block->http_config);
121 | }
122 | if (defined $block->config) {
123 | $block->set_value('config', $default_config . $block->config);
124 | }
125 | if (defined $block->extra_tests) {
126 | if (ref $block->extra_tests ne 'CODE') {
127 | bail_out('extra_tests should be a subroutine, instead found ' . $block->extra_tests);
128 | }
129 |
130 | push(@ResponseChecks, $block->extra_tests);
131 | }
132 | });
133 |
134 | # Add 'upload_file_like' block check
135 | add_response_body_check(sub {
136 | my ($block, $body, $req_idx, $repeated_req_idx, $dry_run) = @_;
137 |
138 | if ($dry_run) {
139 | return;
140 | }
141 |
142 | my $num_requests = (ref $block->request eq 'ARRAY') ? scalar @{$block->request} : 1;
143 | my $final_request = ($req_idx == ($num_requests - 1));
144 | if ($final_request && defined $block->upload_file_like) {
145 | my $ref_type = (ref $block->upload_file_like);
146 | if (ref $block->upload_file_like ne 'Regexp') {
147 | bail_out("upload_file_like block must be a regex pattern");
148 | }
149 | my $test_name = $block->name . " - upload file check";
150 | if ($body =~ /upload_tmp_path = ([^\n]+)$/) {
151 | file_contains_like($1, $block->upload_file_like, $test_name);
152 | } else {
153 | bail_out("upload_tmp_path information not found in response");
154 | }
155 | }
156 | return $block;
157 | });
158 |
159 | 1;
160 |
--------------------------------------------------------------------------------
/t/http2.t:
--------------------------------------------------------------------------------
1 | use strict;
2 | use warnings;
3 |
4 | use File::Basename qw(dirname);
5 | use lib dirname(__FILE__) . "/lib";
6 | use Cwd qw(abs_path);
7 |
8 | use Test::Nginx::Socket tests => 20;
9 | use Test::Nginx::UploadModule;
10 |
11 | $ENV{TEST_DIR} = abs_path(dirname(__FILE__));
12 |
13 |
14 | our $config = <<'_EOC_';
15 | location = /upload/ {
16 | upload_pass @upstream;
17 | upload_resumable on;
18 |
19 | upload_set_form_field upload_file_name $upload_file_name;
20 | upload_set_form_field upload_file_number $upload_file_number;
21 | upload_set_form_field "upload_field_name" "$upload_field_name";
22 | upload_set_form_field "upload_content_type" "$upload_content_type";
23 | upload_set_form_field "upload_tmp_path" "$upload_tmp_path";
24 | upload_set_form_field "upload_content_range" "$upload_content_range";
25 | upload_aggregate_form_field "upload_file_size" "$upload_file_size";
26 | upload_max_file_size 0;
27 | upload_pass_args on;
28 | upload_cleanup 400 404 499 500-505;
29 | }
30 | _EOC_
31 |
32 | no_long_string();
33 | no_shuffle();
34 | run_tests();
35 |
36 | __DATA__
37 | === TEST 1: http2 simple upload
38 | --- config eval: $::config
39 | --- http2
40 | --- skip_nginx
41 | 3: < 1.10.0
42 | --- more_headers
43 | X-Content-Range: bytes 0-3/4
44 | Session-ID: 0000000001
45 | Content-Type: text/plain
46 | Content-Disposition: form-data; name="file"; filename="test.txt"
47 | --- request eval
48 | qq{POST /upload/
49 | test}
50 | --- error_code: 200
51 | --- response_body eval
52 | qq{upload_content_range = bytes 0-3/4
53 | upload_content_type = text/plain
54 | upload_field_name = file
55 | upload_file_name = test.txt
56 | upload_file_number = 1
57 | upload_file_size = 4
58 | upload_tmp_path = $ENV{TEST_NGINX_UPLOAD_PATH}/store/1/0000000001
59 | }
60 | --- upload_file eval
61 | "test"
62 |
63 | === TEST 2: http2 multiple chunk uploads
64 | --- http_config eval: $::http_config
65 | --- config eval: $::config
66 | --- http2
67 | --- skip_nginx
68 | 3: < 1.10.0
69 | --- more_headers eval
70 | [qq{X-Content-Range: bytes 0-1/4
71 | Session-ID: 0000000002
72 | Content-Type: text/plain
73 | Content-Disposition: form-data; name="file"; filename="test.txt"},
74 | qq{X-Content-Range: bytes 2-3/4
75 | Session-ID: 0000000002
76 | Content-Type: text/plain
77 | Content-Disposition: form-data; name="file"; filename="test.txt"}]
78 | --- request eval
79 | [["POST /upload/\r\n",
80 | "te"],
81 | ["POST /upload/\r\n",
82 | "st"]]
83 | --- error_code eval
84 | [201, 200]
85 | --- response_body eval
86 | ["0-1/4", qq{upload_content_range = bytes 2-3/4
87 | upload_content_type = text/plain
88 | upload_field_name = file
89 | upload_file_name = test.txt
90 | upload_file_number = 1
91 | upload_file_size = 4
92 | upload_tmp_path = $ENV{TEST_NGINX_UPLOAD_PATH}/store/2/0000000002
93 | }]
94 | --- upload_file eval
95 | "test"
96 |
97 | === Test 3: http2 large multiple chunk uploads
98 | --- http_config eval: $::http_config
99 | --- skip_nginx
100 | 5: < 1.10.0
101 | --- http2
102 | --- config eval: $::config
103 | --- more_headers eval
104 | [qq{X-Content-Range: bytes 0-131071/262144
105 | Session-ID: 0000000003
106 | Content-Type: text/plain
107 | Content-Disposition: form-data; name="file"; filename="test.txt"},
108 | qq{X-Content-Range: bytes 131072-262143/262144
109 | Session-ID: 0000000003
110 | Content-Type: text/plain
111 | Content-Disposition: form-data; name="file"; filename="test.txt"}]
112 | --- request eval
113 | [["POST /upload/\r\n",
114 | "@" . $ENV{TEST_NGINX_UPLOAD_FILE}],
115 | ["POST /upload/\r\n",
116 | "@" . $ENV{TEST_NGINX_UPLOAD_FILE}]]
117 | --- error_code eval
118 | [201, 200]
119 | --- response_body eval
120 | ["0-131071/262144", qq{upload_content_range = bytes 131072-262143/262144
121 | upload_content_type = text/plain
122 | upload_field_name = file
123 | upload_file_name = test.txt
124 | upload_file_number = 1
125 | upload_file_size = 262144
126 | upload_tmp_path = ${ENV{TEST_NGINX_UPLOAD_PATH}}/store/3/0000000003
127 | }]
128 | --- upload_file_like eval
129 | qr/^(??{'x' x 262144})$/
130 |
131 | === Test 4: http2 upload_limit_rate
132 | --- skip_nginx
133 | 9: < 1.10.0
134 | --- http2
135 | --- config
136 | location = /upload/ {
137 | upload_pass @upstream;
138 | upload_resumable on;
139 | upload_set_form_field "upload_tmp_path" "$upload_tmp_path";
140 | upload_limit_rate 32768;
141 | }
142 | --- timeout: 5
143 | --- more_headers eval
144 | [qq{X-Content-Range: bytes 0-131071/262144
145 | Session-ID: 0000000004
146 | Content-Type: text/plain
147 | Content-Disposition: form-data; name="file"; filename="test.txt"},
148 | qq{X-Content-Range: bytes 131072-262143/262144
149 | Session-ID: 0000000004
150 | Content-Type: text/plain
151 | Content-Disposition: form-data; name="file"; filename="test.txt"}]
152 | --- request eval
153 | [["POST /upload/\r\n",
154 | "@" . $ENV{TEST_NGINX_UPLOAD_FILE}],
155 | ["POST /upload/\r\n",
156 | "@" . $ENV{TEST_NGINX_UPLOAD_FILE}]]
157 | --- error_code eval
158 | [201, 200]
159 | --- response_body eval
160 | ["0-131071/262144", qq{upload_tmp_path = ${ENV{TEST_NGINX_UPLOAD_PATH}}/store/4/0000000004
161 | }]
162 | --- upload_file_like eval
163 | qr/^(??{'x' x 262144})$/
164 | --- access_log eval
165 | # should have taken 4 seconds, with 1 second possible error
166 | # (Test::Nginx::UploadModule::http_config adds request time to the end of
167 | # the access log)
168 | [qr/[34]\.\d\d\d$/, qr/[34]\.\d\d\d$/]
169 |
--------------------------------------------------------------------------------
/t/upload.t:
--------------------------------------------------------------------------------
1 | use strict;
2 | use warnings;
3 |
4 | use File::Basename qw(dirname);
5 |
6 | use lib dirname(__FILE__) . "/lib";
7 |
8 | use Test::Nginx::Socket tests => 27;
9 | use Test::Nginx::UploadModule;
10 |
11 |
12 | our $config = <<'_EOC_';
13 | location = /upload/ {
14 | upload_pass @upstream;
15 | upload_resumable on;
16 |
17 | upload_set_form_field upload_file_name $upload_file_name;
18 | upload_set_form_field upload_file_number $upload_file_number;
19 | upload_set_form_field "upload_field_name" "$upload_field_name";
20 | upload_set_form_field "upload_content_type" "$upload_content_type";
21 | upload_set_form_field "upload_tmp_path" "$upload_tmp_path";
22 | upload_set_form_field "upload_content_range" "$upload_content_range";
23 | upload_max_file_size 0;
24 | upload_pass_args on;
25 | upload_cleanup 400 404 499 500-505;
26 | }
27 | _EOC_
28 |
29 | no_long_string();
30 | no_shuffle();
31 | run_tests();
32 |
33 | __DATA__
34 | === TEST 1: single chunk upload
35 | --- config eval: $::config
36 | --- more_headers
37 | X-Content-Range: bytes 0-3/4
38 | Session-ID: 0000000001
39 | Content-Type: text/plain
40 | Content-Disposition: form-data; name="file"; filename="test.txt"
41 | --- request eval
42 | qq{POST /upload/
43 | test}
44 | --- error_code: 200
45 | --- response_body eval
46 | qq{upload_content_range = bytes 0-3/4
47 | upload_content_type = text/plain
48 | upload_field_name = file
49 | upload_file_name = test.txt
50 | upload_file_number = 1
51 | upload_tmp_path = ${ENV{TEST_NGINX_UPLOAD_PATH}}/store/1/0000000001
52 | }
53 | --- upload_file_like eval
54 | qr/^test$/
55 |
56 | === TEST 2: multiple chunk uploads
57 | --- http_config eval: $::http_config
58 | --- config eval: $::config
59 | --- more_headers eval
60 | [qq{X-Content-Range: bytes 0-1/4
61 | Session-ID: 0000000002
62 | Content-Type: text/plain
63 | Content-Disposition: form-data; name="file"; filename="test.txt"},
64 | qq{X-Content-Range: bytes 2-3/4
65 | Session-ID: 0000000002
66 | Content-Type: text/plain
67 | Content-Disposition: form-data; name="file"; filename="test.txt"}]
68 | --- request eval
69 | [["POST /upload/\r\n",
70 | "te"],
71 | ["POST /upload/\r\n",
72 | "st"]]
73 | --- error_code eval
74 | [201, 200]
75 | --- response_body eval
76 | ["0-1/4", qq{upload_content_range = bytes 2-3/4
77 | upload_content_type = text/plain
78 | upload_field_name = file
79 | upload_file_name = test.txt
80 | upload_file_number = 1
81 | upload_tmp_path = ${ENV{TEST_NGINX_UPLOAD_PATH}}/store/2/0000000002
82 | }]
83 | --- upload_file_like eval
84 | qr/^test$/
85 |
86 | === Test 3: large multiple chunk uploads
87 | --- config eval: $::config
88 | --- more_headers eval
89 | [qq{X-Content-Range: bytes 0-131071/262144
90 | Session-ID: 0000000003
91 | Content-Type: text/plain
92 | Content-Disposition: form-data; name="file"; filename="test.txt"},
93 | qq{X-Content-Range: bytes 131072-262143/262144
94 | Session-ID: 0000000003
95 | Content-Type: text/plain
96 | Content-Disposition: form-data; name="file"; filename="test.txt"}]
97 | --- request eval
98 | [["POST /upload/\r\n",
99 | "x" x 131072],
100 | ["POST /upload/\r\n",
101 | "x" x 131072]]
102 | --- error_code eval
103 | [201, 200]
104 | --- response_body eval
105 | ["0-131071/262144", qq{upload_content_range = bytes 131072-262143/262144
106 | upload_content_type = text/plain
107 | upload_field_name = file
108 | upload_file_name = test.txt
109 | upload_file_number = 1
110 | upload_tmp_path = ${ENV{TEST_NGINX_UPLOAD_PATH}}/store/3/0000000003
111 | }]
112 | --- upload_file_like eval
113 | qr/^(??{'x' x 262144})$/
114 |
115 | === Test 4: upload_limit_rate
116 | --- config
117 | location = /upload/ {
118 | upload_pass @upstream;
119 | upload_resumable on;
120 | upload_set_form_field "upload_tmp_path" "$upload_tmp_path";
121 | upload_max_file_size 0;
122 | upload_pass_args on;
123 | upload_cleanup 400 404 499 500-505;
124 | upload_limit_rate 32768;
125 | }
126 | --- timeout: 5
127 | --- more_headers eval
128 | [qq{X-Content-Range: bytes 0-131071/262144
129 | Session-ID: 0000000004
130 | Content-Type: text/plain
131 | Content-Disposition: form-data; name="file"; filename="test.txt"},
132 | qq{X-Content-Range: bytes 131072-262143/262144
133 | Session-ID: 0000000004
134 | Content-Type: text/plain
135 | Content-Disposition: form-data; name="file"; filename="test.txt"}]
136 | --- request eval
137 | [["POST /upload/\r\n",
138 | "x" x 131072],
139 | ["POST /upload/\r\n",
140 | "x" x 131072]]
141 | --- error_code eval
142 | [201, 200]
143 | --- response_body eval
144 | ["0-131071/262144", qq{upload_tmp_path = ${ENV{TEST_NGINX_UPLOAD_PATH}}/store/4/0000000004
145 | }]
146 | --- upload_file_like eval
147 | qr/^(??{'x' x 262144})$/
148 | --- access_log eval
149 | # should have taken 4 seconds, with 1 second possible error
150 | # (Test::Nginx::UploadModule::http_config adds request time to the end of
151 | # the access log)
152 | [qr/[34]\.\d\d\d$/, qr/[34]\.\d\d\d$/]
153 |
154 | === TEST 5: multiple chunk uploads out-of-order
155 | --- config eval: $::config
156 | --- more_headers eval
157 | [qq{X-Content-Range: bytes 131072-262143/262144
158 | Session-ID: 0000000005
159 | Content-Type: text/plain
160 | Content-Disposition: form-data; name="file"; filename="test.txt"},
161 | qq{X-Content-Range: bytes 0-131071/262144
162 | Session-ID: 0000000005
163 | Content-Type: text/plain
164 | Content-Disposition: form-data; name="file"; filename="test.txt"}]
165 | --- request eval
166 | [["POST /upload/\r\n",
167 | "b" x 131072],
168 | ["POST /upload/\r\n",
169 | "a" x 131072]]
170 | --- error_code eval
171 | [201, 200]
172 | --- response_body eval
173 | ["131072-262143/262144", qq{upload_content_range = bytes 0-131071/262144
174 | upload_content_type = text/plain
175 | upload_field_name = file
176 | upload_file_name = test.txt
177 | upload_file_number = 1
178 | upload_tmp_path = ${ENV{TEST_NGINX_UPLOAD_PATH}}/store/5/0000000005
179 | }]
180 | --- upload_file_like eval
181 | qr/^(??{'a' x 131072 . 'b' x 131072})$/
182 |
--------------------------------------------------------------------------------
/upload-protocol.md:
--------------------------------------------------------------------------------
1 | # Resumable uploads over HTTP. Protocol specification
2 |
3 | Valery Kholodkov [\](mailto:valery@grid.net.ru),
4 | 2010
5 |
6 | ## 1. Introduction
7 |
8 | This document describes application protocol that is used by [nginx
9 | upload module](upload.ru.html) to implement resumable file uploads. The
10 | first version of the module that supports this protocol is 2.2.0.
11 |
12 |
13 |
14 | ## 2. Purpose
15 |
16 | The HTTP implements file uploads according to
17 | [RFC 1867](http://www.ietf.org/rfc/rfc1867.txt). When the request length
18 | is excessively large, the probability that connection will be
19 | interrupted is high. HTTP does not foresee a resumption mechanism. The
20 | goal of the protocol being described is to implement a mechanism of
21 | resumption of interrupted file transfer or suspension of upload upon
22 | user request.
23 |
24 |
25 |
26 | ## 2.1. Splitting file into segments
27 |
28 | When TCP-connection interrupts abnormaly there is no way to determine
29 | what part of data stream has been succesfully delivered and what hasn't
30 | been delivered. Therefore a client cannot determine what position to
31 | resume from without communicating to server. In order to eliminate
32 | additional communication file is represented as an array of segments of
33 | reasonable length. When TCP-connection interrupts while transmitting
34 | certain segment, client retransmits the whole segment until a positive
35 | reponse will be received from server or maximal number of tries will be
36 | reached. In the protocol being described the client is responsible for
37 | choosing optimal length of a segment.
38 |
39 | For tracking the progress of file upload client and server use identical
40 | numbering scheme for each byte of a file. The first byte of a file has
41 | number 0, the last byte has number n-1, where n is the length of file in
42 | bytes.
43 |
44 | The order of transmission of a segment is not defined. Client may choose
45 | arbitrary order. However it is recommended to send segments in order
46 | ascention of byte numbers. Moreover, a user agent might decide to send
47 | multiple segments simultaneously using multiple independent connections.
48 | If a client exceeds maximal number of simultaneous connections allowed,
49 | server might return 503 "Service Unavailable" response.
50 |
51 | In case of simultaneous transmission it is prohibited to send 2 or more
52 | requests with overlapping ranges within one session. Whenever server
53 | detects simultaneous requests with overlapping ranges it must return an
54 | errorneous response.
55 |
56 |
57 |
58 | ## 2.2. Encapsulation
59 |
60 | Each segment of a file is encapsulated into a separate HTTP-request. The
61 | method of the request is POST. Each request contains following specific
62 | headers:
63 |
64 | | Header | Function |
65 | | ------------------- | ---------------------------------------------------------------------- |
66 | | Content-Disposition | `attachment, filename="name of the file being uploaded"` |
67 | | Content-Type | mime type of a file being uploaded (must not be `multipart/form-data`) |
68 | | X-Content-Range | byte range of a segment being uploaded |
69 | | X-Session-ID | identifier of a session of a file being uploaded (see [2.3](#2.3)) |
70 |
71 | `X-Content-Range` and `X-Session-Id` can also be `Content-Range` and `Session-ID`, respectively.
72 |
73 | The body of the request must contain a segment of the file,
74 | corresponding to the range that was specified in `X-Content-Range` or
75 | `Content-Range` headers.
76 |
77 | Whenever a user agent is not able to determine mime type of a file, it
78 | may use `application/octet-stream`.
79 |
80 |
81 |
82 | ## 2.3. Session management
83 |
84 | In order to identify requests containing segments of a file, a user
85 | agent sends a unique session identified in headers `X-Session-ID` or
86 | `Session-ID`. User agent is responsible for making session identifiers
87 | unique. Server must be ready to process requests from different
88 | IP-addresses corresponding to a single session.
89 |
90 |
91 |
92 | ## 2.4. Acknowledgment
93 |
94 | Server acknowledges reception of each segment with a positive response.
95 | Positive responses are: 201 "Created" whenever at the moment of the
96 | response generation not all segments of the file were received or other
97 | 2xx and 3xx responses whenever at the moment of the response generation
98 | all segments of the file were received. Server must return positive
99 | response only when all bytes of a segment were successfully saved and
100 | information about which of the byte ranges were received was
101 | successfully updated.
102 |
103 | Upon reception of 201 "Created" response client must proceed with
104 | transmission of a next segment. Upon reception of other positive
105 | response codes client must proceed according to their standart
106 | interpretation (see. [RFC 2616](http://www.ietf.org/rfc/rfc2616.txt)).
107 |
108 | In each 201 "Created" response server returns a Range header containing
109 | enumeration of all byte ranges of a file that were received at the
110 | moment of the response generation. Server returns identical list of
111 | ranges in response body.
112 |
113 |
114 |
115 | ## Appendix A: Session examples
116 |
117 | ### Example 1: Request from client containing the first segment of the file
118 |
119 | ```http
120 | POST /upload HTTP/1.1
121 | Host: example.com
122 | Content-Length: 51201
123 | Content-Type: application/octet-stream
124 | Content-Disposition: attachment; filename="big.TXT"
125 | X-Content-Range: bytes 0-51200/511920
126 | Session-ID: 1111215056
127 |
128 |
129 | ```
130 |
131 | ### Example 2: Response to a request containing first segment of a file
132 |
133 | ```http
134 | HTTP/1.1 201 Created
135 | Date: Thu, 02 Sep 2010 12:54:40 GMT
136 | Content-Length: 14
137 | Connection: close
138 | Range: 0-51200/511920
139 |
140 | 0-51200/511920
141 | ```
142 |
143 | ### Example 3: Request from client containing the last segment of the file
144 |
145 | ```http
146 | POST /upload HTTP/1.1
147 | Host: example.com
148 | Content-Length: 51111
149 | Content-Type: application/octet-stream
150 | Content-Disposition: attachment; filename="big.TXT"
151 | X-Content-Range: bytes 460809-511919/511920
152 | Session-ID: 1111215056
153 |
154 |
155 | ```
156 |
157 | ### Example 4: Response to a request containing last segment of a file
158 |
159 | ```http
160 | HTTP/1.1 200 OK
161 | Date: Thu, 02 Sep 2010 12:54:43 GMT
162 | Content-Type: text/html
163 | Connection: close
164 | Content-Length: 2270
165 |
166 |
167 | ```
168 |
--------------------------------------------------------------------------------
/t/lib/Test/Nginx/UploadModule/TestServer.pm:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env perl
2 | use strict;
3 | use warnings;
4 |
5 | package File;
6 | {
7 | sub new {
8 | my ($class, $opts) = @_;
9 | return bless {%$opts}, $class;
10 | }
11 | }
12 |
13 | package Test::Nginx::UploadModule::TestServer;
14 | {
15 | use HTTP::Daemon ();
16 | use POSIX 'WNOHANG';
17 | use IO::Socket;
18 | use IO::Select;
19 |
20 | use base 'Exporter';
21 |
22 | our @EXPORT = qw(wait_for_port);
23 | our @EXPORT_OK = qw(wait_for_port);
24 |
25 | local $| = 1;
26 |
27 | sub new {
28 | my ($class, $opts) = @_;
29 | my $port = $opts->{port} || 12345;
30 | my $self = {
31 | opts => {
32 | port => $port,
33 | },
34 | };
35 | $self->{sock} = HTTP::Daemon->new(
36 | LocalAddr => 'localhost',
37 | LocalPort => $self->{opts}->{port},
38 | ReuseAddr => 1
39 | ) || die("Could not open socket on port $port: $!");
40 |
41 | return bless $self, $class;
42 | }
43 |
44 | sub AUTOLOAD {
45 | my $self = shift;
46 | (my $attr = (our $AUTOLOAD)) =~ s/^.*::([^:]+)$/$1/;
47 | if (@_) {
48 | $self->{$attr} = shift;
49 | } else {
50 | return $self->{$attr};
51 | }
52 | }
53 |
54 |
55 | sub trim {
56 | my $val = shift;
57 | if (defined $val) {
58 | $val =~ s|^\s+||s;
59 | $val =~ s|\s+$||s;
60 | }
61 | return $val;
62 | }
63 |
64 | sub strip_quotes {
65 | my $s = shift;
66 | return ($s =~ /^"(.*?)"$/) ? $1 : $s;
67 | }
68 |
69 | sub process_multipart_chunk {
70 | my ($self, $value, $content_disposition) = @_;
71 | my %kv;
72 | if ($content_disposition) {
73 | (my $disposition = $content_disposition) =~ s/^\s*?form-data\s*?;\s*?(?=\S)//g;
74 | my @keyvals = map { /^(.*?)=(.*?)$/ && [lc($1), strip_quotes($2)] }
75 | map { trim($_) }
76 | split(';', $disposition);
77 | %kv = map {@$_} @keyvals;
78 | }
79 | my $kv = \%kv;
80 | if ($kv->{filename}) {
81 | $value = File->new({filename => $kv->{filename}, contents => $value});
82 | }
83 | return [$kv->{name}, $value];
84 | }
85 |
86 | sub process_multipart {
87 | my ($self, $content, $boundary) = @_;
88 | my $data = {};
89 | my $chunk_split = qr/^(.+?)\r\n\r\n(.+)$/s;
90 | my $strip_dashes = qr/(\r\n)?\-+$/;
91 | my $chunks = [];
92 | my @chunks = grep { $_ } map { s/(\r\n)?\-+$//s; s/^\-+\s+$//s; $_ }
93 | split(/$boundary/, $content);
94 | my %data = ();
95 | for my $chunk (@chunks) {
96 | my $chunk_data = {headers => {}, body => ''};
97 | while ($chunk =~ /([\w\-]+)\:\s*(.+?)(?=\r\n)/pgs) {
98 | $chunk_data->{headers}->{lc $1} = $2;
99 | ($chunk_data->{value} = $') =~ s/^\r\n\r\n//g;
100 | }
101 | my $content_disposition = $chunk_data->{headers}->{'content-disposition'};
102 | my ($k, $v) = @{$self->process_multipart_chunk($chunk_data->{value}, $content_disposition)};
103 | $data->{$k} = $v;
104 | }
105 | return $data;
106 | }
107 |
108 | sub process_body {
109 | my ($self, $client, $req) = @_;
110 | my $content = $req->content;
111 | my $content_type = $req->header('Content-Type');
112 | my $data = {};
113 | if ($content_type) {
114 | if ($content_type =~ /multipart\/form\-data; boundary=(.+?)$/i) {
115 | my $boundary = quotemeta($1);
116 | return $self->process_multipart($content, $boundary);
117 | }
118 | }
119 | my $ct_disp = $req->header('Content-Disposition');
120 | if ($ct_disp) {
121 | my ($k, $v) = @{$self->process_multipart_chunk($content, $ct_disp)};
122 | $data->{$k} = $v;
123 | }
124 | return $data;
125 | }
126 |
127 | sub shutdown {
128 | my $self = shift;
129 | if (!defined $self->{sock}) {
130 | exit 0;
131 | }
132 | $self->sock->close;
133 | kill INT => $self->{forkpid} if defined $self->{forkpid};
134 | undef $self->{sock};
135 | exit 0;
136 | }
137 |
138 | sub handle_requests {
139 | my ($self, $client) = @_;
140 | while (my $req = $client->get_request()) {
141 | my $response = HTTP::Response->new(200, 'OK');
142 | $response->header('Content-Type' => 'text/html');
143 |
144 | if ($req->uri->path eq '/shutdown/') {
145 | $response->content("");
146 | $client->send_response($response);
147 | $client->close;
148 | $self->sock->close;
149 | undef $client;
150 | $_[2] = 1;
151 | exit 0;
152 | }
153 | my $data = $self->process_body($client, $req);
154 | my %query_params = $req->uri->query_form;
155 | for my $k (keys %query_params) {
156 | if ($k ne 'headers') {
157 | $data->{$k} = $query_params{$k};
158 | }
159 | }
160 | my %headers = $req->headers->flatten;
161 | my @headers = ();
162 | for my $k (sort keys %headers) {
163 | my $v = $headers{$k};
164 | push(@headers, "$k: $v");
165 | }
166 | my @fields = ();
167 | for my $k (sort keys %$data) {
168 | my $v = $data->{$k};
169 | if ($v && $v->isa('File')) {
170 | my $filename = $v->{filename};
171 | $k .= "(${filename})";
172 | $v = $v->{contents};
173 | }
174 | push(@fields, "$k = $v");
175 | }
176 | my $response_str = join("\n", @fields) . "\n";
177 | $response->content($response_str);
178 | $client->send_response($response);
179 | }
180 | }
181 |
182 | sub run {
183 | my $self = shift;
184 | while ((defined $self->{sock}) && (my $client = $self->sock->accept)) {
185 | defined (my $pid = fork()) or die("Can't fork: $!");
186 | if ($pid == 0) {
187 | $client->close;
188 | $self->sock->close;
189 | next;
190 | }
191 | my $retval = 0;
192 | $self->handle_requests($client, $retval);
193 | $client->close;
194 | if ($retval == 1) {
195 | $client->close;
196 | $self->sock->close;
197 | undef $client;
198 | exit 0;
199 | }
200 | }
201 | if (defined $self->sock) {
202 | $self->sock->close;
203 | }
204 | }
205 |
206 | sub wait_for_port {
207 | my ($port, $errhandler) = @_;
208 | if (!defined $errhandler) {
209 | $errhandler = \¨
210 | }
211 | my $sock;
212 | eval {
213 | local $SIG{ALRM} = sub { die('timeout'); };
214 | alarm(1);
215 | while (1) {
216 | $sock = IO::Socket::INET->new(PeerHost=>'127.0.0.1', PeerPort=>$port, Timeout=>1);
217 | last if $sock;
218 | select(undef, undef, undef, 0.1);
219 | }
220 | alarm(0);
221 | };
222 | if ($@ eq 'timeout' || !$sock) {
223 | $errhandler->("Connecting to test server timed out");
224 | } elsif ($@) {
225 | alarm(0);
226 | $errhandler->($@);
227 | } elsif ($sock) {
228 | $sock->close;
229 | }
230 | }
231 |
232 |
233 | local $SIG{CHLD} = sub {
234 | while ((my $child = waitpid(-1, WNOHANG )) > 0) {}
235 | };
236 |
237 | }
238 |
239 | if (!caller) {
240 | my $server = __PACKAGE__->new();
241 | $server->run();
242 | }
243 |
244 | 1;
--------------------------------------------------------------------------------
/t/aggregate_fields.t:
--------------------------------------------------------------------------------
1 | use strict;
2 | use warnings;
3 |
4 | use File::Basename qw(dirname);
5 |
6 | use lib dirname(__FILE__) . "/lib";
7 |
8 | use Test::Nginx::Socket tests => 24;
9 | use Test::Nginx::UploadModule;
10 |
11 | our $configs = {
12 | hash_funcs => q[
13 | location = /upload/ {
14 | upload_pass @upstream;
15 | upload_resumable on;
16 | upload_aggregate_form_field "upload_file_crc32" "$upload_file_crc32";
17 | upload_aggregate_form_field "upload_file_md5" "$upload_file_md5";
18 | upload_aggregate_form_field "upload_file_md5_uc" "$upload_file_md5_uc";
19 | upload_aggregate_form_field "upload_file_sha1" "$upload_file_sha1";
20 | upload_aggregate_form_field "upload_file_sha1_uc" "$upload_file_sha1_uc";
21 | upload_aggregate_form_field "upload_file_sha256" "$upload_file_sha256";
22 | upload_aggregate_form_field "upload_file_sha256_uc" "$upload_file_sha256_uc";
23 | upload_aggregate_form_field "upload_file_sha512" "$upload_file_sha512";
24 | upload_aggregate_form_field "upload_file_sha512_uc" "$upload_file_sha512_uc";
25 | upload_set_form_field "upload_tmp_path" "$upload_tmp_path";
26 | }
27 | ],
28 | simple => q[
29 | location = /upload/ {
30 | upload_pass @upstream;
31 | upload_resumable on;
32 | upload_aggregate_form_field "upload_file_number" "$upload_file_number";
33 | upload_aggregate_form_field "upload_file_size" "$upload_file_size";
34 | upload_set_form_field "upload_tmp_path" "$upload_tmp_path";
35 | }
36 | ],
37 | };
38 |
39 | our $session_id = 0;
40 |
41 | our $requests = {
42 | single_chunk => {
43 | headers => sub { join("\n",
44 | 'X-Content-Range: bytes 0-3/4',
45 | 'Session-ID: ' . ++$session_id,
46 | 'Content-Type: text/plain',
47 | 'Content-Disposition: form-data; name="file"; filename="test.txt"');
48 | },
49 | body => "POST /upload/\ntest",
50 | },
51 | multi_chunk => {
52 | headers => sub { [
53 | join("\n",
54 | 'X-Content-Range: bytes 0-131071/262144',
55 | 'Session-ID: ' . ++$session_id,
56 | 'Content-Type: text/plain',
57 | 'Content-Disposition: form-data; name="file"; filename="test.txt"'),
58 | join("\n",
59 | 'X-Content-Range: bytes 131072-262143/262144',
60 | 'Session-ID: ' . $session_id,
61 | 'Content-Type: text/plain',
62 | 'Content-Disposition: form-data; name="file"; filename="test.txt"'),
63 | ] },
64 | body => [
65 | ["POST /upload/\r\n", "a" x 131072],
66 | ["POST /upload/\r\n", "b" x 131072],
67 | ],
68 | },
69 | standard => {
70 | raw_request => sub { join("\r\n",
71 | "POST /upload/ HTTP/1.1",
72 | "Host: 127.0.0.1",
73 | "Connection: Close",
74 | "Content-Type: multipart/form-data; boundary=------123456789",
75 | "Content-Length: 262252",
76 | "",
77 | "--------123456789",
78 | "Content-Disposition: form-data; name=\"file\"; filename=\"test.txt\"",
79 | "",
80 | ("a" x 131072) . ("b" x 131072),
81 | "--------123456789",
82 | "");
83 | },
84 | },
85 | };
86 |
87 | no_long_string();
88 | no_shuffle();
89 | run_tests();
90 |
91 | __DATA__
92 | === TEST 1: single chunk upload
93 | --- config eval: $::configs->{simple}
94 | --- more_headers eval: $::requests->{single_chunk}->{headers}->()
95 | --- request eval: $::requests->{single_chunk}->{body}
96 | --- error_code: 200
97 | --- response_body eval
98 | qq{upload_file_number = 1
99 | upload_file_size = 4
100 | upload_tmp_path = ${ENV{TEST_NGINX_UPLOAD_PATH}}/store/$::session_id/$::session_id
101 | }
102 | --- upload_file_like eval
103 | qr/^test$/
104 |
105 | === TEST 2: single chunk upload (http2)
106 | --- config eval: $::configs->{simple}
107 | --- http2
108 | --- skip_nginx
109 | 3: < 1.10.0
110 | --- more_headers eval: $::requests->{single_chunk}->{headers}->()
111 | --- request eval: $::requests->{single_chunk}->{body}
112 | --- error_code: 200
113 | --- response_body eval
114 | qq{upload_file_number = 1
115 | upload_file_size = 4
116 | upload_tmp_path = ${ENV{TEST_NGINX_UPLOAD_PATH}}/store/$::session_id/$::session_id
117 | }
118 | --- upload_file_like eval
119 | qr/^test$/
120 |
121 | === TEST 3: multi-chunk uploads
122 | --- config eval: $::configs->{simple}
123 | --- more_headers eval: $::requests->{multi_chunk}->{headers}->()
124 | --- request eval: $::requests->{multi_chunk}->{body}
125 | --- error_code eval
126 | [201, 200]
127 | --- response_body eval
128 | ["0-131071/262144", qq{upload_file_number = 1
129 | upload_file_size = 262144
130 | upload_tmp_path = ${ENV{TEST_NGINX_UPLOAD_PATH}}/store/$::session_id/$::session_id
131 | }]
132 | --- upload_file_like eval
133 | qr/^(??{'a' x 131072 . 'b' x 131072})$/
134 |
135 | === TEST 4: multi-chunk uploads (hash funcs)
136 | --- config eval: $::configs->{hash_funcs}
137 | --- more_headers eval: $::requests->{multi_chunk}->{headers}->()
138 | --- request eval: $::requests->{multi_chunk}->{body}
139 | --- error_code eval
140 | [201, 200]
141 | --- response_body eval
142 | ["0-131071/262144", qq{upload_file_crc32 =
143 | upload_file_md5 =
144 | upload_file_md5_uc =
145 | upload_file_sha1 =
146 | upload_file_sha1_uc =
147 | upload_file_sha256 =
148 | upload_file_sha256_uc =
149 | upload_file_sha512 =
150 | upload_file_sha512_uc =
151 | upload_tmp_path = ${ENV{TEST_NGINX_UPLOAD_PATH}}/store/$::session_id/$::session_id
152 | }]
153 | --- upload_file_like eval
154 | qr/^(??{'a' x 131072 . 'b' x 131072})$/
155 |
156 | === TEST 5: multi-chunk uploads out of order
157 | --- todo
158 | 2: BUG https://github.com/fdintino/nginx-upload-module/issues/106
159 | --- config eval: $::configs->{simple}
160 | --- more_headers eval: [ CORE::reverse @{$::requests->{multi_chunk}->{headers}->()} ]
161 | --- request eval: [ CORE::reverse @{$::requests->{multi_chunk}->{body}}]
162 | --- error_code eval
163 | [201, 200]
164 | --- response_body eval
165 | ["131072-262143/262144", qq{upload_file_number = 1
166 | upload_file_size = 262144
167 | upload_tmp_path = ${ENV{TEST_NGINX_UPLOAD_PATH}}/store/$::session_id/$::session_id
168 | }]
169 |
170 | === TEST 6: multipart/form-data
171 | --- config eval: $::configs->{simple}
172 | --- raw_request eval: $::requests->{standard}->{raw_request}->()
173 | --- error_code: 200
174 | --- response_body eval
175 | qq{upload_file_number = 1
176 | upload_file_size = 262144
177 | upload_tmp_path = ${ENV{TEST_NGINX_UPLOAD_PATH}}/store/1/0000000001
178 | }
179 | --- upload_file_like eval
180 | qr/^(??{'a' x 131072 . 'b' x 131072})$/
181 |
182 | === TEST 7: multipart/form-data (hash fields)
183 | --- config eval: $::configs->{hash_funcs}
184 | --- raw_request eval: $::requests->{standard}->{raw_request}->()
185 | --- error_code: 200
186 | --- response_body eval
187 | qq{upload_file_crc32 = db99345e
188 | upload_file_md5 = 01f2c9f3ccdf9c44f733ff443228e66d
189 | upload_file_md5_uc = 01F2C9F3CCDF9C44F733FF443228E66D
190 | upload_file_sha1 = a2eb84a7bee5e2263e9a3cffae44a4a11044bb2e
191 | upload_file_sha1_uc = A2EB84A7BEE5E2263E9A3CFFAE44A4A11044BB2E
192 | upload_file_sha256 = 58a200a96c5ef282be0d02ab6906655513584bf281bef027b842c2e66b1c56c7
193 | upload_file_sha256_uc = 58A200A96C5EF282BE0D02AB6906655513584BF281BEF027B842C2E66B1C56C7
194 | upload_file_sha512 = fa5af601c85900b80f40865a74a71a74ba382b51336543ba72b31d2e0af80867c1862051763ea9309f637b2ad6133b6e170e4f088a2951a3d05d6fe3a5bcd0e9
195 | upload_file_sha512_uc = FA5AF601C85900B80F40865A74A71A74BA382B51336543BA72B31D2E0AF80867C1862051763EA9309F637B2AD6133B6E170E4F088A2951A3D05D6FE3A5BCD0E9
196 | upload_tmp_path = ${ENV{TEST_NGINX_UPLOAD_PATH}}/store/8/0000123458
197 | }
198 | --- upload_file_like eval
199 | qr/^(??{'a' x 131072 . 'b' x 131072})$/
200 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # nginx-upload-module
2 |
3 | [](https://travis-ci.org/fdintino/nginx-upload-module)
4 | [](https://codecov.io/gh/fdintino/nginx-upload-module)
5 |
6 | A module for [nginx](https://www.nginx.com/) for handling file uploads using
7 | multipart/form-data encoding ([RFC 1867](http://www.ietf.org/rfc/rfc1867.txt))
8 | and resumable uploads according to
9 | [this](https://github.com/fdintino/nginx-upload-module/blob/master/upload-protocol.md)
10 | protocol.
11 |
12 | * [Description](#description)
13 | * [Directives](#directives)
14 | * [upload_pass](#upload_pass)
15 | * [upload_resumable](#upload_resumable)
16 | * [upload_store](#upload_store)
17 | * [upload_state_store](#upload_state_store)
18 | * [upload_store_access](#upload_store_access)
19 | * [upload_set_form_field](#upload_set_form_field)
20 | * [upload_aggregate_form_field](#upload_aggregate_form_field)
21 | * [upload_pass_form_field](#upload_pass_form_field)
22 | * [upload_cleanup](#upload_cleanup)
23 | * [upload_buffer_size](#upload_buffer_size)
24 | * [upload_max_part_header_len](#upload_max_part_header_len)
25 | * [upload_max_file_size](#upload_max_file_size)
26 | * [upload_limit_rate](#upload_limit_rate)
27 | * [upload_max_output_body_len](#upload_max_output_body_len)
28 | * [upload_tame_arrays](#upload_tame_arrays)
29 | * [upload_pass_args](#upload_pass_args)
30 | * [Example configuration](#example-configuration)
31 | * [License](#license)
32 |
33 | ## Description
34 |
35 | The module parses request body storing all files being uploaded to a
36 | directory specified by [`upload_store`](#upload_store) directive. The
37 | files are then being stripped from body and altered request is then
38 | passed to a location specified by [`upload_pass`](#upload_pass)
39 | directive, thus allowing arbitrary handling of uploaded files. Each of
40 | file fields are being replaced by a set of fields specified by
41 | [`upload_set_form_field`](#upload_set_form_field) directive. The
42 | content of each uploaded file then could be read from a file specified
43 | by $upload_tmp_path variable or the file could be simply moved to
44 | ultimate destination. Removal of output files is controlled by directive
45 | [`upload_cleanup`](#upload_cleanup). If a request has a method other than
46 | POST, the module returns error 405 (Method not allowed). Requests with
47 | such methods could be processed in alternative location via
48 | [`error_page`](http://nginx.org/en/docs/http/ngx_http_core_module.html#error_page)
49 | directive.
50 |
51 | ## Directives
52 |
53 | ### upload_pass
54 |
55 | **Syntax:** upload_pass location
56 | **Default:** —
57 | **Context:** `server,location`
58 |
59 | Specifies location to pass request body to. File fields will be stripped
60 | and replaced by fields, containing necessary information to handle
61 | uploaded files.
62 |
63 | ### upload_resumable
64 |
65 | **Syntax:** upload_resumable on | off
66 | **Default:** `upload_resumable off`
67 | **Context:** `main,server,location`
68 |
69 | Enables resumable uploads.
70 |
71 | ### upload_store
72 |
73 | **Syntax:** upload_store directory [level1 [level2]] ...
74 | **Default:** —
75 | **Context:** `server,location`
76 |
77 | Specifies a directory to which output files will be saved to. The
78 | directory could be hashed. In this case all subdirectories should exist
79 | before starting nginx.
80 |
81 | ### upload_state_store
82 |
83 | **Syntax:** upload_state_store directory [level1 [level2]] ...
84 | **Default:** —
85 | **Context:** `server,location`
86 |
87 | Specifies a directory that will contain state files for resumable
88 | uploads. The directory could be hashed. In this case all subdirectories
89 | should exist before starting nginx.
90 |
91 | ### upload_store_access
92 |
93 | **Syntax:** upload_store_access mode
94 | **Default:** `upload_store_access user:rw`
95 | **Context:** `server,location`
96 |
97 | Specifies access mode which will be used to create output files.
98 |
99 | ### upload_set_form_field
100 |
101 | **Syntax:** upload_set_form_field name value
102 | **Default:** —
103 | **Context:** `server,location`
104 |
105 | Specifies a form field(s) to generate for each uploaded file in request
106 | body passed to backend. Both `name` and `value` could contain following
107 | special variables:
108 |
109 | - `$upload_field_name`: the name of original file field
110 | - `$upload_content_type`: the content type of file uploaded
111 | - `$upload_file_name`: the original name of the file being uploaded
112 | with leading path elements in DOS and UNIX notation stripped. I.e.
113 | "D:\\Documents And Settings\\My Dcouments\\My Pictures\\Picture.jpg"
114 | will be converted to "Picture.jpg" and "/etc/passwd" will be
115 | converted to "passwd".
116 | - `$upload_tmp_path`: the path where the content of original file is
117 | being stored to. The output file name consists 10 digits and
118 | generated with the same algorithm as in `proxy_temp_path`
119 | directive.
120 |
121 | These variables are valid only during processing of one part of original
122 | request body.
123 |
124 | Usage example:
125 |
126 | ```nginx
127 | upload_set_form_field $upload_field_name.name "$upload_file_name";
128 | upload_set_form_field $upload_field_name.content_type "$upload_content_type";
129 | upload_set_form_field $upload_field_name.path "$upload_tmp_path";
130 | ```
131 |
132 | ### upload_aggregate_form_field
133 |
134 | **Syntax:** upload_aggregate_form_field name value
135 | **Default:** —
136 | **Context:** `server,location`
137 |
138 | Specifies a form field(s) containing aggregate attributes to generate
139 | for each uploaded file in request body passed to backend. Both name and
140 | value could contain standard nginx variables, variables from
141 | [upload_set_form_field](#upload_set_form_field) directive and
142 | following additional special variables:
143 |
144 | - `$upload_file_md5`: MD5 checksum of the file
145 | - `$upload_file_md5_uc`: MD5 checksum of the file in uppercase letters
146 | - `$upload_file_sha1`: SHA1 checksum of the file
147 | - `$upload_file_sha1_uc`: SHA1 checksum of the file in uppercase letters
148 | - `$upload_file_sha256`: SHA256 checksum of the file
149 | - `$upload_file_sha256_uc`: SHA256 checksum of the file in uppercase letters
150 | - `$upload_file_sha512`: SHA512 checksum of the file
151 | - `$upload_file_sha512_uc`: SHA512 checksum of the file in uppercase letters
152 | - `$upload_file_crc32`: hexdecimal value of CRC32 of the file
153 | - `$upload_file_size`: size of the file in bytes
154 | - `$upload_file_number`: ordinal number of file in request body
155 |
156 | The value of a field specified by this directive is evaluated after
157 | successful upload of the file, thus these variables are valid only at
158 | the end of processing of one part of original request body.
159 |
160 | **Warning:**: variables `$upload_file_md5`, `$upload_file_md5_uc`,
161 | `$upload_file_sha1`, and `$upload_file_sha1_uc` use additional
162 | resources to calculate MD5 and SHA1 checksums.
163 |
164 | Usage example:
165 |
166 | ```nginx
167 | upload_aggregate_form_field $upload_field_name.md5 "$upload_file_md5";
168 | upload_aggregate_form_field $upload_field_name.size "$upload_file_size";
169 |
170 | ```
171 |
172 | ### upload_pass_form_field
173 |
174 | **Syntax:** upload_pass_form_field regex
175 | **Default:** —
176 | **Context:** `server,location`
177 |
178 | Specifies a regex pattern for names of fields which will be passed to
179 | backend from original request body. This directive could be specified
180 | multiple times per location. Field will be passed to backend as soon as
181 | first pattern matches. For PCRE-unaware enviroments this directive
182 | specifies exact name of a field to pass to backend. If directive is
183 | omitted, no fields will be passed to backend from client.
184 |
185 | Usage example:
186 |
187 | ```nginx
188 | upload_pass_form_field "^submit$|^description$";
189 | ```
190 |
191 | For PCRE-unaware environments:
192 |
193 | ```nginx
194 | upload_pass_form_field "submit";
195 | upload_pass_form_field "description";
196 |
197 | ```
198 |
199 | ### upload_cleanup
200 |
201 | **Syntax:** upload_cleanup status/range ...
202 | **Default:** —
203 | **Context:** `server,location`
204 |
205 | Specifies HTTP statuses after generation of which all file successfuly
206 | uploaded in current request will be removed. Used for cleanup after
207 | backend or server failure. Backend may also explicitly signal errornous
208 | status if it doesn't need uploaded files for some reason. HTTP status
209 | must be a numerical value in range 400-599, no leading zeroes are
210 | allowed. Ranges of statuses could be specified with a dash.
211 |
212 | Usage example:
213 |
214 | ```nginx
215 | upload_cleanup 400 404 499 500-505;
216 | ```
217 |
218 | ### upload_buffer_size
219 |
220 | **Syntax:** upload_buffer_size size
221 | **Default:** size of memory page in bytes
222 | **Context:** `server,location`
223 |
224 | Size in bytes of write buffer which will be used to accumulate file data
225 | and write it to disk. This directive is intended to be used to
226 | compromise memory usage vs. syscall rate.
227 |
228 | ### upload_max_part_header_len
229 |
230 | **Syntax:** upload_max_part_header_len size
231 | **Default:** `512`
232 | **Context:** `server,location`
233 |
234 | Specifies maximal length of part header in bytes. Determines the size of
235 | the buffer which will be used to accumulate part headers.
236 |
237 | ### upload_max_file_size
238 |
239 | **Syntax:** upload_max_file_size size
240 | **Default:** `0`
241 | **Context:** `main,server,location`
242 |
243 | Specifies maximal size of the file. Files longer than the value of this
244 | directive will be omitted. This directive specifies "soft" limit, in the
245 | sense, that after encountering file longer than specified limit, nginx
246 | will continue to process request body, trying to receive remaining
247 | files. For "hard" limit `client_max_body_size` directive must be
248 | used. The value of zero for this directive specifies that no
249 | restrictions on file size should be applied.
250 |
251 | ### upload_limit_rate
252 |
253 | **Syntax:** upload_limit_rate rate
254 | **Default:** `0`
255 | **Context:** `main,server,location`
256 |
257 | Specifies upload rate limit in bytes per second. Zero means rate is
258 | unlimited.
259 |
260 | ### upload_max_output_body_len
261 |
262 | **Syntax:** upload_max_output_body_len size
263 | **Default:** `100k`
264 | **Context:** `main,server,location`
265 |
266 | Specifies maximal length of the output body. This prevents piling up of
267 | non-file form fields in memory. Whenever output body overcomes specified
268 | limit error 413 (Request entity too large) will be generated. The value
269 | of zero for this directive specifies that no restrictions on output body
270 | length should be applied.
271 |
272 | ### upload_tame_arrays
273 |
274 | **Syntax:** upload_tame_arrays on | off
275 | **Default:** `off`
276 | **Context:** `main,server,location`
277 |
278 | Specifies whether square brackets in file field names must be dropped
279 | (required for PHP arrays).
280 |
281 | ### upload_pass_args
282 |
283 | **Syntax:** upload_pass_args on | off
284 | **Default:** `off`
285 | **Context:** `main,server,location`
286 |
287 | Enables forwarding of query arguments to location, specified by
288 | [upload_pass](#upload_pass). Ineffective with named locations. Example:
289 |
290 | ```html
291 |
359 | ```
360 |
361 | ## License
362 |
363 | The above-described module is an addition to
364 | [nginx](https://www.nginx.com/) web-server, nevertheless they are
365 | independent products. The license of above-described module is
366 | [BSD](http://en.wikipedia.org/wiki/BSD_license) You should have received
367 | a copy of license along with the source code. By using the materials
368 | from this site you automatically agree to the terms and conditions of
369 | this license. If you don't agree to the terms and conditions of this
370 | license, you must immediately remove from your computer all materials
371 | downloaded from this site.
372 |
--------------------------------------------------------------------------------