├── .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 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | 15 | 16 |
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 | Uploaded files:"; 13 | echo ""; 14 | 15 | echo ""; 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 ""; 27 | } 28 | } 29 | 30 | echo "
NameLocationContent typeMD5Size
$name$tmp_name$content_type$md5$size
"; 31 | 32 | }else{?> 33 |

Select files to upload

34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | 42 | 43 |
44 | 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 | [![Build Status](https://travis-ci.org/fdintino/nginx-upload-module.svg?branch=master)](https://travis-ci.org/fdintino/nginx-upload-module) 4 | [![codecov](https://codecov.io/gh/fdintino/nginx-upload-module/branch/master/graph/badge.svg)](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 |
292 | 293 | ``` 294 | 295 | ```nginx 296 | location /upload/ { 297 | upload_pass /internal_upload/; 298 | upload_pass_args on; 299 | } 300 | 301 | # ... 302 | 303 | location /internal_upload/ { 304 | # ... 305 | proxy_pass http://backend; 306 | } 307 | ``` 308 | 309 | In this example backend gets request URI "/upload?id=5". In case of 310 | `upload_pass_args off` backend gets "/upload". 311 | 312 | ## Example configuration 313 | 314 | ```nginx 315 | server { 316 | client_max_body_size 100m; 317 | listen 80; 318 | 319 | # Upload form should be submitted to this location 320 | location /upload/ { 321 | # Pass altered request body to this location 322 | upload_pass @test; 323 | 324 | # Store files to this directory 325 | # The directory is hashed, subdirectories 0 1 2 3 4 5 6 7 8 9 should exist 326 | upload_store /tmp 1; 327 | 328 | # Allow uploaded files to be read only by user 329 | upload_store_access user:r; 330 | 331 | # Set specified fields in request body 332 | upload_set_form_field $upload_field_name.name "$upload_file_name"; 333 | upload_set_form_field $upload_field_name.content_type "$upload_content_type"; 334 | upload_set_form_field $upload_field_name.path "$upload_tmp_path"; 335 | 336 | # Inform backend about hash and size of a file 337 | upload_aggregate_form_field "$upload_field_name.md5" "$upload_file_md5"; 338 | upload_aggregate_form_field "$upload_field_name.size" "$upload_file_size"; 339 | 340 | upload_pass_form_field "^submit$|^description$"; 341 | 342 | upload_cleanup 400 404 499 500-505; 343 | } 344 | 345 | # Pass altered request body to a backend 346 | location @test { 347 | proxy_pass http://localhost:8080; 348 | } 349 | } 350 | ``` 351 | 352 | ```html 353 | 354 | 355 | 356 | 357 | 358 |
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 | --------------------------------------------------------------------------------