simplefiles

A lightweight C CGI web application for managing files.
git clone https://scm.kuandu.systems/git-raw/simplefiles.git
Log | Files | Refs | README | LICENSE

download.sh (8132B)


      1 #!/bin/sh
      2 
      3 set -eu
      4 
      5 . "$(dirname "$0")/testlib.sh"
      6 
      7 CGI=${1:-build/download.cgi}
      8 
      9 download() {
     10   env \
     11     SCRIPT_NAME=/default/download.cgi \
     12     STORAGE_ROOT="$TESTROOT/storage" \
     13     SIMPLEFILES_CONFIG="$TEST_CONFIG" \
     14     REQUEST_METHOD=GET \
     15     QUERY_STRING="${1:-}" \
     16     REMOTE_USER=bob \
     17     "$CGI"
     18 }
     19 
     20 download_post() {
     21   env \
     22     SCRIPT_NAME=/default/download.cgi \
     23     STORAGE_ROOT="$TESTROOT/storage" \
     24     SIMPLEFILES_CONFIG="$TEST_CONFIG" \
     25     REQUEST_METHOD=POST \
     26     CONTENT_LENGTH=0 \
     27     QUERY_STRING="${1:-}" \
     28     REMOTE_USER=bob \
     29     "$CGI"
     30 }
     31 
     32 download_user() {
     33   user=$1
     34 
     35   env \
     36     SCRIPT_NAME=/default/download.cgi \
     37     STORAGE_ROOT="$TESTROOT/storage" \
     38     SIMPLEFILES_CONFIG="$TEST_CONFIG" \
     39     REQUEST_METHOD=GET \
     40     QUERY_STRING="file=download.txt" \
     41     REMOTE_USER="$user" \
     42     "$CGI"
     43 }
     44 
     45 download_no_user() {
     46   query=${1:-}
     47 
     48   (
     49     unset REMOTE_USER
     50 
     51     export SCRIPT_NAME=/default/download.cgi
     52     export STORAGE_ROOT="$TESTROOT/storage"
     53     export SIMPLEFILES_CONFIG="$TEST_CONFIG"
     54     export REQUEST_METHOD=GET
     55     export QUERY_STRING="$query"
     56 
     57     "$CGI"
     58   )
     59 }
     60 
     61 ####################################################
     62 
     63 reset_testroot
     64 
     65 write_default_repo_config "ADMIN_USER=bob"
     66 
     67 echo "hello world" > "$TESTROOT/storage/default/file1.txt"
     68 echo "hello world" > "$TESTROOT/storage/default/file2.txt"
     69 echo "special filename" > "$TESTROOT/storage/default/<test>&file.txt"
     70 printf 'space filename\n' > "$TESTROOT/storage/default/hello world.txt"
     71 
     72 dd if=/dev/urandom \
     73   of="$TESTROOT/storage/default/binary.bin" \
     74   bs=1024 count=1 2>/dev/null
     75 
     76 #
     77 # Valid download
     78 #
     79 
     80 check_status \
     81   "valid download status" \
     82   "200 OK" \
     83   download \
     84   "file=file1.txt"
     85 
     86 check_header \
     87   "content-type header" \
     88   "Content-Type" \
     89   "application/octet-stream" \
     90   download \
     91   "file=file1.txt"
     92 
     93 check_header \
     94   "content-disposition header" \
     95   "Content-Disposition" \
     96   'attachment; filename="file1.txt"' \
     97   download \
     98   "file=file1.txt"
     99 
    100 check_header \
    101   "content-length header" \
    102   "Content-Length" \
    103   "12" \
    104   download \
    105   "file=file1.txt"
    106 
    107 #
    108 # Binary file
    109 #
    110 
    111 check_header \
    112   "binary content-length" \
    113   "Content-Length" \
    114   "1024" \
    115   download \
    116   "file=binary.bin"
    117 
    118 #
    119 # Missing / invalid files
    120 #
    121 
    122 check_body \
    123   "missing file" \
    124   "404 Not Found" \
    125   download \
    126   "file=nonexistent.txt"
    127 
    128 printf 'QUOTA=1024\n' > "$TESTROOT/storage/default/.config"
    129 
    130 check_body \
    131   ".config not downloadable" \
    132   "400 Bad Request" \
    133   download \
    134   "file=.config"
    135 
    136 check_body \
    137   "path traversal ../" \
    138   "400 Bad Request" \
    139   download \
    140   "file=../passwd"
    141 
    142 check_body \
    143   "path traversal .." \
    144   "400 Bad Request" \
    145   download \
    146   "file=.."
    147 
    148 check_body \
    149   "path traversal with slash" \
    150   "400 Bad Request" \
    151   download \
    152   "file=subdir/file.txt"
    153 
    154 check_body \
    155   "encoded dot traversal" \
    156   "400 Bad Request" \
    157   download \
    158   "file=%2e%2e/passwd"
    159 
    160 check_body \
    161   "mixed encoded traversal" \
    162   "400 Bad Request" \
    163   download \
    164   "file=.%2e/passwd"
    165 
    166 check_body \
    167   "mixed encoded slash traversal" \
    168   "400 Bad Request" \
    169   download \
    170   "file=..%2Fpasswd"
    171 
    172 check_body \
    173   "encoded nested traversal" \
    174   "400 Bad Request" \
    175   download \
    176   "file=%2e%2e%2f%2e%2e%2fpasswd"
    177 
    178 check_body \
    179   "encoded NUL" \
    180   "400 Bad Request" \
    181   download \
    182   "file=file%00.txt"
    183 
    184 check_body \
    185   "malformed percent encoding" \
    186   "400 Bad Request" \
    187   download \
    188   "file=file%ZZ.txt"
    189 
    190 check_body \
    191   "incomplete percent encoding" \
    192   "400 Bad Request" \
    193   download \
    194   "file=file%2.txt"
    195 
    196 check_body \
    197   "empty filename" \
    198   "400 Bad Request" \
    199   download \
    200   "file="
    201 
    202 check_body \
    203   "missing query string" \
    204   "400 Bad Request" \
    205   download
    206 
    207 check_body \
    208   "missing file parameter" \
    209   "Missing filename" \
    210   download \
    211   "foo=bar"
    212 
    213 check_body \
    214   "file parameter without equals" \
    215   "Missing filename" \
    216   download \
    217   "file"
    218 
    219 check_body \
    220   "empty file parameter" \
    221   "Invalid filename" \
    222   download \
    223   "file="
    224 
    225 check_body \
    226   "empty file among parameters" \
    227   "Invalid filename" \
    228   download \
    229   "foo=bar&file=&baz=qux"
    230 
    231 check_status \
    232   "file parameter after another parameter" \
    233   "200 OK" \
    234   download \
    235   "foo=bar&file=file1.txt"
    236 
    237 check_status \
    238   "file parameter before another parameter" \
    239   "200 OK" \
    240   download \
    241   "file=file1.txt&foo=bar"
    242 
    243 check_status \
    244   "file parameter with extra query parameters" \
    245   "200 OK" \
    246   download \
    247   "foo=bar&file=file1.txt&baz=qux"
    248 
    249 check_status \
    250   "reject duplicate file parameter" \
    251   "400 Bad Request" \
    252   download \
    253   "file=file1.txt&file=file2.txt"
    254 
    255 check_status \
    256   "reject duplicate file parameter reversed" \
    257   "400 Bad Request" \
    258   download \
    259   "file=file2.txt&file=file1.txt"
    260 
    261 #
    262 # Invalid method
    263 #
    264 
    265 check_body \
    266   "post not allowed" \
    267   "GET required" \
    268   download_post \
    269   "file=file1.txt"
    270 
    271 #
    272 # Directory
    273 #
    274 
    275 mkdir -p "$TESTROOT/storage/default/subdir"
    276 
    277 check_body \
    278   "directory not downloadable" \
    279   "404 Not Found" \
    280   download \
    281   "file=subdir"
    282 
    283 check_body \
    284   "encoded path traversal" \
    285   "400 Bad Request" \
    286   download \
    287   "file=subdir%2ffile.txt"
    288 
    289 #
    290 # Special filenames
    291 #
    292 
    293 check_body \
    294   "encoded special filename" \
    295   "200 OK" \
    296   download \
    297   "file=%3Ctest%3E%26file.txt"
    298 
    299 check_body \
    300   "plus decoded to space" \
    301   "200 OK" \
    302   download \
    303   "file=hello+world.txt"
    304 
    305 check_body \
    306   "plus decoded filename content" \
    307   "space filename" \
    308   download \
    309   "file=hello+world.txt"
    310 
    311 #
    312 # Header injection
    313 #
    314 
    315 check_body \
    316   "encoded CR rejected" \
    317   "400 Bad Request" \
    318   download \
    319   "file=foo%0d%0aX-Test%3A%20injected.txt"
    320 
    321 check_body \
    322   "encoded LF rejected" \
    323   "400 Bad Request" \
    324   download \
    325   "file=foo%0aX-Test%3A%20injected.txt"
    326 
    327 #
    328 # Symlink outside repository
    329 #
    330 
    331 reset_testroot
    332 
    333 write_default_repo_config "ADMIN_USER=bob"
    334 
    335 mkdir -p "$TESTROOT/storage/other"
    336 
    337 printf 'secret\n' > "$TESTROOT/storage/other/secret.txt"
    338 
    339 ln -s "$TESTROOT/storage/other/secret.txt" \
    340   "$TESTROOT/storage/default/linked.txt"
    341 
    342 check_body \
    343   "symlink outside repository" \
    344   "404 Not Found" \
    345   download \
    346   "file=linked.txt"
    347 
    348 check_not_body \
    349   "symlink target not exposed" \
    350   "secret" \
    351   download \
    352   "file=linked.txt"
    353 
    354 #
    355 # Authentication
    356 #
    357 
    358 reset_testroot
    359 
    360 write_default_repo_config ""
    361 
    362 printf '%s' 'download content' \
    363   > "$TESTROOT/storage/default/download.txt"
    364 
    365 check_status \
    366   "anonymous download rejected without ADMIN_USER" \
    367   "403 Forbidden" \
    368   download_no_user \
    369   "file=download.txt"
    370 
    371 check_file \
    372   "anonymous download preserves file" \
    373   "$TESTROOT/storage/default/download.txt" \
    374   "download content"
    375 
    376 #
    377 # ADMIN_USER configured: admin user allowed
    378 #
    379 
    380 reset_testroot
    381 
    382 write_default_repo_config "ADMIN_USER=bob"
    383 
    384 printf '%s' 'download content' \
    385   > "$TESTROOT/storage/default/download.txt"
    386 
    387 check_status \
    388   "admin user allowed to download" \
    389   "200 OK" \
    390   download_user \
    391   "bob"
    392 
    393 check_body \
    394   "admin download succeeds" \
    395   "download content" \
    396   download_user \
    397   "bob"
    398 
    399 #
    400 # ADMIN_USER configured: non-admin still has read access
    401 #
    402 
    403 reset_testroot
    404 
    405 write_default_repo_config "ADMIN_USER=bob"
    406 
    407 printf '%s' 'download content' \
    408   > "$TESTROOT/storage/default/download.txt"
    409 
    410 check_status \
    411   "non-admin user allowed to download" \
    412   "200 OK" \
    413   download_user \
    414   "alice"
    415 
    416 check_body \
    417   "non-admin download succeeds" \
    418   "download content" \
    419   download_user \
    420   "alice"
    421 
    422 #
    423 # Anonymous access with ADMIN_USER
    424 #
    425 
    426 reset_testroot
    427 
    428 write_default_repo_config "ADMIN_USER=bob"
    429 
    430 printf '%s' 'download content' \
    431   > "$TESTROOT/storage/default/download.txt"
    432 
    433 check_status \
    434   "anonymous download rejected" \
    435   "403 Forbidden" \
    436   download_no_user \
    437   "file=download.txt"
    438 
    439 check_file \
    440   "anonymous download preserves file" \
    441   "$TESTROOT/storage/default/download.txt" \
    442   "download content"
    443 
    444 #
    445 # Anonymous read access
    446 #
    447 
    448 reset_testroot
    449 
    450 write_default_repo_config "ADMIN_USER=bob"
    451 
    452 write_global_config "ANONYMOUS_READ=1"
    453 
    454 printf '%s' 'download content' \
    455   > "$TESTROOT/storage/default/download.txt"
    456 
    457 check_status \
    458   "anonymous download allowed with anonymous read" \
    459   "200 OK" \
    460   download_no_user \
    461   "file=download.txt"
    462 
    463 check_body \
    464   "anonymous download succeeds with anonymous read" \
    465   "download content" \
    466   download_no_user \
    467   "file=download.txt"
    468 
    469 test_result
    470 
    471 test_result