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

rename.sh (14992B)


      1 #!/bin/sh
      2 
      3 #
      4 # Regression tests for rename.cgi
      5 #
      6 
      7 set -eu
      8 
      9 . "$(dirname "$0")/testlib.sh"
     10 
     11 CGI=${1:-build/rename.cgi}
     12 
     13 ##############################################################################
     14 # HTTP helpers
     15 ##############################################################################
     16 
     17 rename() {
     18 	env \
     19 		SCRIPT_NAME=/default/rename.cgi \
     20 		STORAGE_ROOT="$TESTROOT/storage" \
     21 		SIMPLEFILES_ROOT=./html \
     22 		REQUEST_METHOD=GET \
     23 		QUERY_STRING="${1:-}" \
     24 		REMOTE_USER=bob \
     25 		"$CGI"
     26 }
     27 
     28 rename_user() {
     29 	user=$1
     30 	query=${2:-}
     31 
     32 	env \
     33 		SCRIPT_NAME=/default/rename.cgi \
     34 		STORAGE_ROOT="$TESTROOT/storage" \
     35 		SIMPLEFILES_ROOT=./html \
     36 		REQUEST_METHOD=GET \
     37 		QUERY_STRING="$query" \
     38 		REMOTE_USER="$user" \
     39 		"$CGI"
     40 }
     41 
     42 rename_no_user() {
     43 	query=${1:-}
     44 
     45 	(
     46 		unset REMOTE_USER
     47 
     48 		export SCRIPT_NAME=/default/rename.cgi
     49 		export STORAGE_ROOT="$TESTROOT/storage"
     50 		export SIMPLEFILES_ROOT=./html
     51 		export SIMPLEFILES_CONFIG="$TEST_CONFIG"
     52 		export REQUEST_METHOD=GET
     53 		export QUERY_STRING="$query"
     54 
     55 		"$CGI"
     56 	)
     57 }
     58 
     59 rename_post() {
     60 	body=${1:-}
     61 
     62 	printf '%s' "$body" |
     63 		env \
     64 			SCRIPT_NAME=/default/rename.cgi \
     65 			STORAGE_ROOT="$TESTROOT/storage" \
     66 			REPOSITORY_LOCK_DIR="$TEST_LOCK_DIR" \
     67 			SIMPLEFILES_CONFIG="$TEST_CONFIG" \
     68 			SIMPLEFILES_ROOT=./html \
     69 			HTTP_ORIGIN="$TEST_CSRF_ORIGIN" \
     70 			REMOTE_USER=bob \
     71 			REQUEST_METHOD=POST \
     72 			CONTENT_LENGTH="$(printf '%s' "$body" | wc -c | tr -d ' ')" \
     73 			"$CGI"
     74 }
     75 
     76 rename_post_user() {
     77 	user=$1
     78 	body=${2:-}
     79 
     80 	printf '%s' "$body" |
     81 		env \
     82 			SCRIPT_NAME=/default/rename.cgi \
     83 			STORAGE_ROOT="$TESTROOT/storage" \
     84 			REPOSITORY_LOCK_DIR="$TEST_LOCK_DIR" \
     85 			SIMPLEFILES_CONFIG="$TEST_CONFIG" \
     86 			SIMPLEFILES_ROOT=./html \
     87 			HTTP_ORIGIN="$TEST_CSRF_ORIGIN" \
     88 			REMOTE_USER="$user" \
     89 			REQUEST_METHOD=POST \
     90 			CONTENT_LENGTH="$(printf '%s' "$body" | wc -c | tr -d ' ')" \
     91 			"$CGI"
     92 }
     93 
     94 rename_post_no_user() {
     95 	body=${1:-}
     96 
     97 	printf '%s' "$body" |
     98 		env \
     99 			SCRIPT_NAME=/default/rename.cgi \
    100 			STORAGE_ROOT="$TESTROOT/storage" \
    101 			REPOSITORY_LOCK_DIR="$TEST_LOCK_DIR" \
    102 			SIMPLEFILES_CONFIG="$TEST_CONFIG" \
    103 			SIMPLEFILES_ROOT=./html \
    104 			HTTP_ORIGIN="$TEST_CSRF_ORIGIN" \
    105 			REQUEST_METHOD=POST \
    106 			CONTENT_LENGTH="$(printf '%s' "$body" | wc -c | tr -d ' ')" \
    107 			"$CGI"
    108 }
    109 
    110 rename_post_origin() {
    111 	origin=$1
    112 	body=${2:-}
    113 
    114 	printf '%s' "$body" |
    115 		env \
    116 			SCRIPT_NAME=/default/rename.cgi \
    117 			STORAGE_ROOT="$TESTROOT/storage" \
    118 			REPOSITORY_LOCK_DIR="$TEST_LOCK_DIR" \
    119 			SIMPLEFILES_CONFIG="$TEST_CONFIG" \
    120 			SIMPLEFILES_ROOT=./html \
    121 			HTTP_ORIGIN="$origin" \
    122 			REMOTE_USER=bob \
    123 			REQUEST_METHOD=POST \
    124 			CONTENT_LENGTH="$(printf '%s' "$body" | wc -c | tr -d ' ')" \
    125 			"$CGI"
    126 }
    127 
    128 rename_post_no_origin() {
    129 	body=${1:-}
    130 
    131 	printf '%s' "$body" |
    132 		env \
    133 			SCRIPT_NAME=/default/rename.cgi \
    134 			STORAGE_ROOT="$TESTROOT/storage" \
    135 			REPOSITORY_LOCK_DIR="$TEST_LOCK_DIR" \
    136 			SIMPLEFILES_CONFIG="$TEST_CONFIG" \
    137 			SIMPLEFILES_ROOT=./html \
    138 			REMOTE_USER=bob \
    139 			REQUEST_METHOD=POST \
    140 			CONTENT_LENGTH="$(printf '%s' "$body" | wc -c | tr -d ' ')" \
    141 			"$CGI"
    142 }
    143 
    144 rename_template_failure() {
    145 	env \
    146 		SCRIPT_NAME=/default/rename.cgi \
    147 		STORAGE_ROOT="$TESTROOT/storage" \
    148 		SIMPLEFILES_ROOT="$TESTROOT/html" \
    149 		SIMPLEFILES_CONFIG="$TEST_CONFIG" \
    150 		REQUEST_METHOD=GET \
    151 		QUERY_STRING="file=file1.txt" \
    152 		REMOTE_USER=bob \
    153 		"$CGI"
    154 }
    155 
    156 ##############################################################################
    157 # Basic functionality
    158 ##############################################################################
    159 
    160 reset_testroot
    161 
    162 write_default_repo_config "ADMIN_USER=bob"
    163 
    164 printf 'hello' > "$TESTROOT/storage/default/file1.txt"
    165 printf 'second' > "$TESTROOT/storage/default/file2.txt"
    166 
    167 check_status \
    168 	"rename confirmation page" \
    169 	"200 OK" \
    170 	rename \
    171 	"file=file1.txt"
    172 
    173 check_body \
    174 	"old filename shown" \
    175 	"file1.txt" \
    176 	rename \
    177 	"file=file1.txt"
    178 
    179 check_body \
    180 	"rename form" \
    181 	'<form method="post"' \
    182 	rename \
    183 	"file=file1.txt"
    184 
    185 check_body \
    186 	"new name field" \
    187 	"new_filename" \
    188 	rename \
    189 	"file=file1.txt"
    190 
    191 check_redirect \
    192 	"rename redirects" \
    193 	"303 See Other" \
    194 	"./" \
    195 	rename_post \
    196 	"file=file1.txt&new_filename=renamed.txt"
    197 
    198 check_file_exists \
    199 	"new filename exists" \
    200 	"$TESTROOT/storage/default/renamed.txt"
    201 
    202 check_file_missing \
    203 	"old filename removed" \
    204 	"$TESTROOT/storage/default/file1.txt"
    205 
    206 check_file \
    207 	"contents preserved" \
    208 	"$TESTROOT/storage/default/renamed.txt" \
    209 	"hello"
    210 
    211 ##############################################################################
    212 # Parameter validation
    213 ##############################################################################
    214 
    215 check_body \
    216 	"missing file parameter" \
    217 	"Missing filename" \
    218 	rename \
    219 	"new_filename=file2.txt"
    220 
    221 check_body \
    222 	"empty file parameter" \
    223 	"Invalid filename" \
    224 	rename \
    225 	"file=&new_filename=file2.txt"
    226 
    227 check_body \
    228 	"missing new name" \
    229 	"Missing new filename" \
    230 	rename_post \
    231 	"file=file1.txt"
    232 
    233 check_body \
    234 	"empty new name" \
    235 	"Invalid filename" \
    236 	rename_post \
    237 	"file=file1.txt&new_filename="
    238 
    239 check_status \
    240 	"reject duplicate file parameter" \
    241 	"400 Bad Request" \
    242 	rename \
    243 	"file=file1.txt&file=file2.txt"
    244 
    245 check_status \
    246 	"reject duplicate new_filename parameter" \
    247 	"400 Bad Request" \
    248 	rename_post \
    249 	"file=file1.txt&new_filename=a.txt&new_filename=b.txt"
    250 
    251 ##############################################################################
    252 # Filename validation
    253 ##############################################################################
    254 
    255 check_body \
    256 	"reject source traversal" \
    257 	"400 Bad Request" \
    258 	rename \
    259 	"file=../passwd"
    260 
    261 check_body \
    262 	"reject destination traversal" \
    263 	"400 Bad Request" \
    264 	rename_post \
    265 	"file=file2.txt&new_filename=../passwd"
    266 
    267 check_body \
    268 	"reject encoded traversal" \
    269 	"400 Bad Request" \
    270 	rename_post \
    271 	"file=file2.txt&new_filename=%2e%2e/passwd"
    272 
    273 check_body \
    274 	"reject encoded NUL destination" \
    275 	"400 Bad Request" \
    276 	rename_post \
    277 	"file=file2.txt&new_filename=test%00.txt"
    278 
    279 check_body \
    280 	"reject malformed percent encoding" \
    281 	"400 Bad Request" \
    282 	rename_post \
    283 	"file=file2.txt&new_filename=%ZZ"
    284 
    285 ##############################################################################
    286 # Rename failures
    287 ##############################################################################
    288 
    289 printf 'original' > "$TESTROOT/storage/default/source.txt"
    290 printf 'destination' > "$TESTROOT/storage/default/destination.txt"
    291 
    292 check_body \
    293 	"reject overwrite" \
    294 	"Destination already exists" \
    295 	rename_post \
    296 	"file=source.txt&new_filename=destination.txt"
    297 
    298 check_file \
    299 	"destination unchanged" \
    300 	"$TESTROOT/storage/default/destination.txt" \
    301 	"destination"
    302 
    303 check_file \
    304 	"source survives failed overwrite" \
    305 	"$TESTROOT/storage/default/source.txt" \
    306 	"original"
    307 
    308 check_body \
    309 	"rename missing file" \
    310 	"File not found" \
    311 	rename_post \
    312 	"file=missing.txt&new_filename=new.txt"
    313 
    314 ##############################################################################
    315 # Special filenames
    316 ##############################################################################
    317 
    318 special="$TESTROOT/storage/default/<test>&file.txt"
    319 
    320 printf 'special\n' > "$special"
    321 
    322 check_redirect \
    323 	"rename encoded filename" \
    324 	"303 See Other" \
    325 	"./" \
    326 	rename_post \
    327 	"file=%3Ctest%3E%26file.txt&new_filename=special.txt"
    328 
    329 check_file_exists \
    330 	"encoded source renamed" \
    331 	"$TESTROOT/storage/default/special.txt"
    332 
    333 printf 'space\n' > "$TESTROOT/storage/default/hello world.txt"
    334 
    335 check_redirect \
    336 	"rename plus decoded source" \
    337 	"303 See Other" \
    338 	"./" \
    339 	rename_post \
    340 	"file=hello+world.txt&new_filename=space.txt"
    341 
    342 check_file_exists \
    343 	"space source renamed" \
    344 	"$TESTROOT/storage/default/space.txt"
    345 
    346 ##############################################################################
    347 # .config protection
    348 ##############################################################################
    349 
    350 printf 'QUOTA=1024\n' >> "$TESTROOT/storage/default/.config"
    351 
    352 check_status \
    353 	"reject .config source rename" \
    354 	"400 Bad Request" \
    355 	rename \
    356 	"file=.config"
    357 
    358 check_status \
    359 	"reject .config destination" \
    360 	"400 Bad Request" \
    361 	rename_post \
    362 	"file=file2.txt&new_filename=.config"
    363 
    364 check_file_exists \
    365 	".config survives" \
    366 	"$TESTROOT/storage/default/.config"
    367 
    368 ##############################################################################
    369 # CSRF protection
    370 ##############################################################################
    371 
    372 reset_testroot
    373 
    374 write_default_repo_config "ADMIN_USER=bob"
    375 
    376 printf 'csrf\n' > "$TESTROOT/storage/default/csrf.txt"
    377 
    378 check_status \
    379 	"rename accepts configured Origin" \
    380 	"303 See Other" \
    381 	rename_post_origin \
    382 	"http://localhost:8000" \
    383 	"file=csrf.txt&new_filename=renamed.txt"
    384 
    385 check_file_exists \
    386 	"csrf rename succeeded" \
    387 	"$TESTROOT/storage/default/renamed.txt"
    388 
    389 reset_testroot
    390 
    391 write_default_repo_config "ADMIN_USER=bob"
    392 
    393 printf 'csrf\n' > "$TESTROOT/storage/default/csrf.txt"
    394 
    395 check_status \
    396 	"rename rejects missing Origin" \
    397 	"403 Forbidden" \
    398 	rename_post_no_origin \
    399 	"file=csrf.txt&new_filename=renamed.txt"
    400 
    401 check_file_exists \
    402 	"missing Origin preserves source" \
    403 	"$TESTROOT/storage/default/csrf.txt"
    404 
    405 reset_testroot
    406 
    407 write_default_repo_config "ADMIN_USER=bob"
    408 
    409 printf 'csrf\n' > "$TESTROOT/storage/default/csrf.txt"
    410 
    411 check_status \
    412 	"rename rejects wrong Origin" \
    413 	"403 Forbidden" \
    414 	rename_post_origin \
    415 	"https://evil.example" \
    416 	"file=csrf.txt&new_filename=renamed.txt"
    417 
    418 check_file_exists \
    419 	"wrong Origin preserves source" \
    420 	"$TESTROOT/storage/default/csrf.txt"
    421 
    422 ##############################################################################
    423 # Other valid renames
    424 ##############################################################################
    425 
    426 reset_testroot
    427 
    428 write_default_repo_config "ADMIN_USER=bob"
    429 
    430 printf 'hello' > "$TESTROOT/storage/default/same.txt"
    431 
    432 check_redirect \
    433 	"rename to same filename" \
    434 	"303 See Other" \
    435 	"./" \
    436 	rename_post \
    437 	"file=same.txt&new_filename=same.txt"
    438 
    439 check_file \
    440 	"same filename preserved" \
    441 	"$TESTROOT/storage/default/same.txt" \
    442 	"hello"
    443 
    444 : > "$TESTROOT/storage/default/empty.txt"
    445 
    446 check_redirect \
    447 	"rename empty file" \
    448 	"303 See Other" \
    449 	"./" \
    450 	rename_post \
    451 	"file=empty.txt&new_filename=empty-renamed.txt"
    452 
    453 check_file_exists \
    454 	"empty file renamed" \
    455 	"$TESTROOT/storage/default/empty-renamed.txt"
    456 
    457 reset_testroot
    458 
    459 write_default_repo_config "ADMIN_USER=bob"
    460 
    461 printf 'extension' > "$TESTROOT/storage/default/file.txt"
    462 
    463 check_redirect \
    464 	"rename extension" \
    465 	"303 See Other" \
    466 	"./" \
    467 	rename_post \
    468 	"file=file.txt&new_filename=file.md"
    469 
    470 check_file_exists \
    471 	"renamed extension exists" \
    472 	"$TESTROOT/storage/default/file.md"
    473 
    474 check_file_missing \
    475 	"old extension removed" \
    476 	"$TESTROOT/storage/default/file.txt"
    477 
    478 check_file \
    479 	"extension rename preserves contents" \
    480 	"$TESTROOT/storage/default/file.md" \
    481 	"extension"
    482 
    483 ##############################################################################
    484 # Read-only repository
    485 ##############################################################################
    486 
    487 reset_testroot
    488 
    489 printf 'readonly' > "$TESTROOT/storage/default/readonly.txt"
    490 
    491 write_default_repo_config "READ_ONLY=1"
    492 
    493 check_status \
    494 	"reject rename in readonly repository" \
    495 	"403 Forbidden" \
    496 	rename_post \
    497 	"file=readonly.txt&new_filename=renamed.txt"
    498 
    499 check_file_exists \
    500 	"readonly source preserved" \
    501 	"$TESTROOT/storage/default/readonly.txt"
    502 
    503 check_file_missing \
    504 	"readonly destination not created" \
    505 	"$TESTROOT/storage/default/renamed.txt"
    506 
    507 check_body \
    508 	"readonly GET rename rejected" \
    509 	"read-only" \
    510 	rename \
    511 	"file=readonly.txt"
    512 
    513 check_file \
    514 	"readonly source contents preserved" \
    515 	"$TESTROOT/storage/default/readonly.txt" \
    516 	"readonly"
    517 
    518 check_status \
    519 	"readonly rename rejects missing Origin" \
    520 	"403 Forbidden" \
    521 	rename_post_no_origin \
    522 	"file=readonly.txt&new_filename=new.txt"
    523 
    524 ##############################################################################
    525 # Authorization
    526 ##############################################################################
    527 
    528 reset_testroot
    529 
    530 printf 'readonly\n' > "$TESTROOT/storage/default/readonly.txt"
    531 
    532 write_default_repo_config "ADMIN_USER=bob"
    533 
    534 check_status \
    535 	"rename rejected for non-admin user" \
    536 	"403 Forbidden" \
    537 	rename_post_user \
    538 	"alice" \
    539 	"file=readonly.txt&new_filename=renamed.txt"
    540 
    541 check_file_exists \
    542 	"non-admin source preserved" \
    543 	"$TESTROOT/storage/default/readonly.txt"
    544 
    545 check_file_missing \
    546 	"non-admin destination not created" \
    547 	"$TESTROOT/storage/default/renamed.txt"
    548 
    549 check_status \
    550 	"non-admin rename GET rejected" \
    551 	"403 Forbidden" \
    552 	rename_user \
    553 	"alice" \
    554 	"file=readonly.txt"
    555 
    556 reset_testroot
    557 
    558 printf 'anonymous\n' > "$TESTROOT/storage/default/anonymous.txt"
    559 
    560 write_default_repo_config "ADMIN_USER=bob"
    561 
    562 check_status \
    563 	"anonymous rename POST rejected" \
    564 	"403 Forbidden" \
    565 	rename_post_no_user \
    566 	"file=anonymous.txt&new_filename=renamed.txt"
    567 
    568 check_file_exists \
    569 	"anonymous source preserved" \
    570 	"$TESTROOT/storage/default/anonymous.txt"
    571 
    572 check_file_missing \
    573 	"anonymous destination not created" \
    574 	"$TESTROOT/storage/default/renamed.txt"
    575 
    576 check_status \
    577 	"anonymous rename GET rejected" \
    578 	"403 Forbidden" \
    579 	rename_no_user \
    580 	"file=anonymous.txt"
    581 
    582 ##############################################################################
    583 # Anonymous read does not grant write access
    584 ##############################################################################
    585 
    586 reset_testroot
    587 
    588 write_default_repo_config "ADMIN_USER=bob"
    589 
    590 printf 'hello' > "$TESTROOT/storage/default/file.txt"
    591 
    592 write_global_config "ANONYMOUS_READ=1"
    593 
    594 check_status \
    595 	"anonymous rename GET rejected despite anonymous read" \
    596 	"403 Forbidden" \
    597 	rename_no_user \
    598 	"file=file.txt"
    599 
    600 check_status \
    601 	"anonymous rename POST rejected despite anonymous read" \
    602 	"403 Forbidden" \
    603 	rename_post_no_user \
    604 	"file=file.txt&new_filename=renamed.txt"
    605 
    606 check_file_exists \
    607 	"anonymous rename source preserved" \
    608 	"$TESTROOT/storage/default/file.txt"
    609 
    610 check_file_missing \
    611 	"anonymous rename destination not created" \
    612 	"$TESTROOT/storage/default/renamed.txt"
    613 
    614 ##############################################################################
    615 # Authentication alone does not grant write access
    616 ##############################################################################
    617 
    618 reset_testroot
    619 
    620 write_default_repo_config ""
    621 
    622 printf 'hello' > "$TESTROOT/storage/default/file.txt"
    623 
    624 check_status \
    625 	"authenticated user cannot rename without ADMIN_USER" \
    626 	"403 Forbidden" \
    627 	rename_post_user \
    628 	"alice" \
    629 	"file=file.txt&new_filename=renamed.txt"
    630 
    631 check_file_exists \
    632 	"source preserved without ADMIN_USER" \
    633 	"$TESTROOT/storage/default/file.txt"
    634 
    635 check_file_missing \
    636 	"destination not created without ADMIN_USER" \
    637 	"$TESTROOT/storage/default/renamed.txt"
    638 
    639 #
    640 # Response generation
    641 #
    642 
    643 reset_testroot
    644 
    645 write_default_repo_config "ADMIN_USER=bob"
    646 
    647 printf 'hello\n' > "$TESTROOT/storage/default/file1.txt"
    648 
    649 mkdir -p "$TESTROOT/html"
    650 
    651 printf '{{UNKNOWN_VARIABLE}}' > "$TESTROOT/html/rename.html"
    652 
    653 check_status \
    654 	"template failure returns 500" \
    655 	"500 Internal Server Error" \
    656 	rename_template_failure
    657 
    658 check_not_body \
    659 	"template failure does not return 200" \
    660 	"Status: 200 OK" \
    661 	rename_template_failure
    662 
    663 test_result