aboutsummaryrefslogtreecommitdiff
path: root/src/test/test_parseconf.sh
blob: 4fe27d9f5d26a52289a5fc7b7f664129b2381848 (plain)
1
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
#!/bin/sh
# Copyright 2019, The Tor Project, Inc.
# See LICENSE for licensing information

# Integration test script for verifying that Tor configurations are parsed as
# we expect.
#
# Valid configurations are tested with --dump-config, which parses and
# validates the configuration before writing it out.  We then make sure that
# the result is what we expect, before parsing and dumping it again to make
# sure that there is no change. Optionally, we can also test the log messages
# with --verify-config.
#
# Invalid configurations are tested with --verify-config, which parses
# and validates the configuration. We capture its output and make sure that
# it contains the error message we expect.
#
# When tor is compiled with different libraries or modules, some
# configurations may have different results. We can specify these result
# variants using additional result files.

# This script looks for its test cases as individual directories in
# src/test/conf_examples/.  Each test may have these files:
#
# Configuration Files
#
# torrc -- Usually needed. This file is passed to Tor on the command line
#      with the "-f" flag. (If you omit it, you'll test Tor's behavior when
#      it receives a nonexistent configuration file.)
#
# torrc.defaults -- Optional. If present, it is passed to Tor on the command
#      line with the --defaults-torrc option. If this file is absent, an empty
#      file is passed instead to prevent Tor from reading the system defaults.
#
# cmdline -- Optional. If present, it contains command-line arguments that
#      will be passed to Tor.
#
# (included torrc files or directories) -- Optional. Additional files can be
#      included in configuration, using the "%include" directive. Files or
#      directories can be included in any of the config files listed above.
#      Include paths should be specified relative to the test case directory.
#
# Result Files
#
# expected -- If this file is present, then it should be the expected result
#      of "--dump-config short" for this test case.  Exactly one of
#      "expected" or "error" must be present, or the test will fail.
#
# expected_log -- Optional. If this file is present, then it contains a regex
#      that must be matched by some line in the output of "--verify-config",
#      which must succeed. Only used if "expected" is also present.
#
# error -- If this file is present, then it contains a regex that must be
#      matched by some line in the output of "--verify-config", which must
#      fail. Exactly one of "expected" or "error" must be present, or the
#      test will fail.
#
# {expected,expected_log,error}_${TOR_LIBS_ENABLED}* -- If this file is
#      present, then the outcome is different when some optional libraries are
#      enabled. If there is no result file matching the exact list of enabled
#      libraries, the script searches for result files with one or more of
#      those libraries disabled. The search terminates at the standard result
#      file. If expected* is present, the script also searches for
#      expected_log*.
#
#      For example:
#      A test that succeeds, regardless of any enabled libraries:
#       - expected
#      A test that has a different result if the nss library is enabled
#      (but the same result if any other library is enabled). We also check
#      the log output in this test:
#       - expected
#       - expected_log
#       - expected_nss
#       - expected_log_nss
#      A test that fails if the lzma and zstd modules are *not* enabled:
#       - error
#       - expected_lzma_zstd
#
# {expected,expected_log,error}*_no_${TOR_MODULES_DISABLED} -- If this file is
#      present, then the outcome is different when some modules are disabled.
#      If there is no result file matching the exact list of disabled modules,
#      the standard result file is used. If expected* is present, the script
#      also searches for expected_log*.
#
#      For example:
#      A test that succeeds, regardless of any disabled modules:
#       - expected
#      A test that has a different result if the relay module is disabled
#      (but the same result if just the dirauth module is disabled):
#       - expected
#       - expected_no_relay_dirauth
#      A test that fails if the dirauth module is disabled:
#       - expected
#       - error_no_dirauth
#       - error_no_relay_dirauth
#      (Disabling the relay module also disables dirauth module. But we don't
#      want to encode that knowledge in this test script, so we supply a
#      separate result file for every combination of disabled modules that
#      has a different result.)

umask 077
set -e

MYNAME="$0"

# emulate realpath(), in case coreutils or equivalent is not installed.
abspath() {
    f="$*"
    if test -d "$f"; then
        dir="$f"
        base=""
    else
        dir="$(dirname "$f")"
        base="/$(basename "$f")"
    fi
    dir="$(cd "$dir" && pwd)"
    echo "$dir$base"
}

# find the tor binary
if test $# -ge 1; then
  TOR_BINARY="$1"
  shift
else
  TOR_BINARY="${TESTING_TOR_BINARY:-./src/app/tor}"
fi

TOR_BINARY="$(abspath "$TOR_BINARY")"

echo "Using Tor binary '$TOR_BINARY'."

# make a safe space for temporary files
DATA_DIR=$(mktemp -d -t tor_parseconf_tests.XXXXXX)
trap 'rm -rf "$DATA_DIR"' 0

# This is where we look for examples
EXAMPLEDIR="$(dirname "$0")"/conf_examples

case "$(uname -s)" in
    CYGWIN*) WINDOWS=1;;
    MINGW*) WINDOWS=1;;
    MSYS*) WINDOWS=1;;
    *) WINDOWS=0;;
esac

####
# BUG WORKAROUND FOR 31757:
#  On Appveyor, it seems that Tor sometimes randomly fails to produce
#  output with --dump-config.  Whil we are figuring this out, do not treat
#  windows errors as hard failures.
####
if test "$WINDOWS" = 1; then
    EXITCODE=0
else
    EXITCODE=1
fi

FINAL_EXIT=0
NEXT_TEST=

# Log a failure message to stderr, using $@ as a printf string and arguments
# Set NEXT_TEST to "yes" and FINAL_EXIT to $EXITCODE.
fail_printf()
{
    printf "FAIL: " >&2
    # The first argument is a printf string, so this warning is spurious
    # shellcheck disable=SC2059
    printf "$@" >&2
    NEXT_TEST="yes"
    FINAL_EXIT=$EXITCODE
}

# Log a failure message to stderr, using $@ as a printf string and arguments
# Exit with status $EXITCODE.
die_printf()
{
    printf "FAIL: CRITICAL error in '%s':" "$MYNAME" >&2
    # The first argument is a printf string, so this warning is spurious
    # shellcheck disable=SC2059
    printf "$@" >&2
    exit $EXITCODE
}

if test "$WINDOWS" = 1; then
    FILTER="dos2unix"
else
    FILTER="cat"
fi

EMPTY="${DATA_DIR}/EMPTY"
touch "$EMPTY" || die_printf "Couldn't create empty file '%s'.\\n" \
                             "$EMPTY"
NON_EMPTY="${DATA_DIR}/NON_EMPTY"
echo "This pattern should not match any log messages" \
     > "$NON_EMPTY" || die_printf "Couldn't create non-empty file '%s'.\\n" \
                                  "$NON_EMPTY"

STANDARD_LIBS="libevent\\|openssl\\|zlib"
# Lib names are restricted to [a-z0-9]* at the moment
# We don't actually want to support foreign accents here
# shellcheck disable=SC2018,SC2019
TOR_LIBS_ENABLED="$("$TOR_BINARY" --verify-config \
                      -f "$EMPTY" --defaults-torrc "$EMPTY" \
                    | sed -n 's/.* Tor .* running on .* with\(.*\)\./\1/p' \
                    | tr 'A-Z' 'a-z' | tr ',' '\n' \
                    | grep -v "$STANDARD_LIBS" | grep -v "n/a" \
                    | sed 's/\( and\)* \(lib\)*\([a-z0-9]*\) .*/\3/' \
                    | sort | tr '\n' '_')"
# Remove the last underscore, if there is one
TOR_LIBS_ENABLED=${TOR_LIBS_ENABLED%_}

# If we ever have more than 3 optional libraries, we'll need more code here
TOR_LIBS_ENABLED_COUNT="$(echo "$TOR_LIBS_ENABLED_SEARCH" \
                          | tr ' ' '\n' | wc -l)"
if test "$TOR_LIBS_ENABLED_COUNT" -gt 3; then
    die_printf "Can not handle more than 3 optional libraries.\\n"
fi
# Brute-force the combinations of libraries
TOR_LIBS_ENABLED_SEARCH_3="$(echo "$TOR_LIBS_ENABLED" \
    | sed -n \
      's/^\([^_]*\)_\([^_]*\)_\([^_]*\)$/_\1_\2 _\1_\3 _\2_\3 _\1 _\2 _\3/p')"
TOR_LIBS_ENABLED_SEARCH_2="$(echo "$TOR_LIBS_ENABLED" \
    | sed -n 's/^\([^_]*\)_\([^_]*\)$/_\1 _\2/p')"
TOR_LIBS_ENABLED_SEARCH="_$TOR_LIBS_ENABLED \
                           $TOR_LIBS_ENABLED_SEARCH_3 \
                           $TOR_LIBS_ENABLED_SEARCH_2"
TOR_LIBS_ENABLED_SEARCH="$(echo "$TOR_LIBS_ENABLED_SEARCH" | tr ' ' '\n' \
                           | grep -v '^_*$' | tr '\n' ' ')"

TOR_MODULES_DISABLED="$("$TOR_BINARY" --list-modules | grep ': no' \
                        | cut -d ':' -f1 | sort | tr '\n' '_')"
# Remove the last underscore, if there is one
TOR_MODULES_DISABLED=${TOR_MODULES_DISABLED%_}

echo "Tor is configured with:"
echo "Optional Libraries: ${TOR_LIBS_ENABLED:-(None)}"
if test "$TOR_LIBS_ENABLED"; then
    echo "Optional Library Search List: $TOR_LIBS_ENABLED_SEARCH"
fi
echo "Disabled Modules: ${TOR_MODULES_DISABLED:-(None)}"

# Yes, unix uses "0" for a successful command
TRUE=0
FALSE=1

# Run tor --verify-config on the torrc $1, and defaults torrc $2, which may
# be $EMPTY. Pass tor the extra command line arguments $3, which will be
# passed unquoted.
# Send tor's standard output to stderr.
log_verify_config()
{
    # show the command we're about to execute
    # log_verify_config() is only called when we've failed
    printf "Tor --verify-config said:\\n" >&2
    printf "$ %s %s %s %s %s %s %s\\n" \
    "$TOR_BINARY" --verify-config \
                  -f "$1" \
                  --defaults-torrc "$2" \
                  "$3" \
                  >&2
    # We need cmdline unquoted
    # shellcheck disable=SC2086
    "$TOR_BINARY" --verify-config \
                  -f "$1" \
                  --defaults-torrc "$2" \
                  $3 \
                  >&2 \
        || true
}

# Run "tor --dump-config short" on the torrc $1, and defaults torrc $2, which
# may be $EMPTY. Pass tor the extra command line arguments $3, which will be
# passed unquoted. Send tor's standard output to $4.
#
# Set $FULL_TOR_CMD to the tor command line that was executed.
#
# If tor fails, fail_printf() using the file name $5, and context $6,
# which may be an empty string. Then run log_verify_config().
dump_config()
{
    if test "$6"; then
        CONTEXT=" $6"
    else
        CONTEXT=""
    fi

    # keep the command we're about to execute, and show if it we fail
    FULL_TOR_CMD=$(printf "$ %s %s %s %s %s %s %s %s" \
         "$TOR_BINARY" --dump-config short \
                       -f "$1" \
                       --defaults-torrc "$2" \
                       "$3"
             )
    # We need cmdline unquoted
    # shellcheck disable=SC2086
    if ! "$TOR_BINARY" --dump-config short \
                       -f "$1" \
                       --defaults-torrc "$2" \
                       $3 \
                       > "$4"; then
        fail_printf "'%s': Tor --dump-config reported an error%s:\\n%s\\n" \
                    "$5" \
                    "$CONTEXT" \
                    "$FULL_TOR_CMD"
        log_verify_config "$1" \
                          "$2" \
                          "$3"
    fi
}

# Run "$FILTER" on the input $1.
# Send the standard output to $2.
# If tor fails, log a failure message using the file name $3, and context $4,
# which may be an empty string.
filter()
{
    if test "$4"; then
        CONTEXT=" $4"
    else
        CONTEXT=""
    fi

    "$FILTER" "$1" \
              > "$2" \
        || fail_printf "'%s': Filter '%s' reported an error%s.\\n" \
                       "$3" \
                       "$FILTER" \
                       "$CONTEXT"
}

# Compare the expected file $1, and output file $2.
#
# If they are different, fail. Log the differences between the files.
# Run log_verify_config() with torrc $3, defaults torrc $4, and command
# line $5, to log Tor's error messages.
#
# If the file contents are identical, returns true. Otherwise, return false.
#
# Log failure messages using fail_printf(), with the expected file name,
# context $6, which may be an empty string, and the tor command line $7.
check_diff()
{
    if test "$6"; then
        CONTEXT=" $6"
    else
        CONTEXT=""
    fi

    if cmp "$1" "$2" > /dev/null; then
        return "$TRUE"
    else
        fail_printf "'%s': Tor --dump-config said%s:\\n%s\\n" \
                    "$1" \
                    "$CONTEXT" \
                    "$7"
        diff -u "$1" "$2" >&2 \
            || true
        log_verify_config "$3" \
                          "$4" \
                          "$5"
        return "$FALSE"
    fi
}

# Run "tor --dump-config short" on the torrc $1, and defaults torrc $2, which
# may be $EMPTY. Pass tor the extra command line arguments $3, which will be
# passed unquoted. Send tor's standard output to $4, after running $FILTER
# on it.
#
# If tor fails, run log_verify_config().
#
# Compare the expected file $5, and output file. If they are different, fail.
# If this is the first step that failed in this test, run log_verify_config().
#
# If the file contents are identical, returns true. Otherwise, return false,
# and log the differences between the files.
#
# Log failure messages using fail_printf(), with the expected file name, and
# context $6, which may be an empty string.
check_dump_config()
{
    OUTPUT="$4"
    OUTPUT_RAW="${OUTPUT}_raw"

    FULL_TOR_CMD=
    dump_config "$1" \
                "$2" \
                "$3" \
                "$OUTPUT_RAW" \
                "$5" \
                "$6"

    filter "$OUTPUT_RAW" \
           "$OUTPUT" \
           "$5" \
           "$6"

    if check_diff "$5" \
                  "$OUTPUT" \
                  "$1" \
                  "$2" \
                  "$3" \
                  "$6" \
                  "$FULL_TOR_CMD"; then
        return "$TRUE"
    else
        return "$FALSE"
    fi
}

# Check if $1 is an empty file.
# If it is, fail_printf() using $2 as the type of file.
# Returns true if the file is empty, false otherwise.
check_empty_pattern()
{
    if ! test -s "$1"; then
        fail_printf "%s file '%s' is empty, and will match any output.\\n" \
                    "$2" \
                    "$1"
        return "$TRUE"
    else
        return "$FALSE"
    fi
}

# Run tor --verify-config on the torrc $1, and defaults torrc $2, which may
# be $EMPTY. Pass tor the extra command line arguments $3, which will be
# passed unquoted. Send tor's standard output to $4.
#
# Set $FULL_TOR_CMD to the tor command line that was executed.
#
# If tor's exit status does not match the boolean $5, fail_printf()
# using the file name $6, and context $7, which is required.
verify_config()
{
    RESULT=$TRUE

    # keep the command we're about to execute, and show if it we fail
    FULL_TOR_CMD=$(printf "$ %s %s %s %s %s %s %s" \
    "$TOR_BINARY" --verify-config \
                  -f "$1" \
                  --defaults-torrc "$2" \
                  "$3"
             )
    # We need cmdline unquoted
    # shellcheck disable=SC2086
    "$TOR_BINARY" --verify-config \
                  -f "$1" \
                  --defaults-torrc "$2" \
                  $3 \
                  > "$4" || RESULT=$FALSE

    # Convert the actual and expected results to boolean, and compare
    if test $((! (! RESULT))) -ne $((! (! $5))); then
        fail_printf "'%s': Tor --verify-config did not %s:\\n%s\\n" \
                    "$6" \
                    "$7" \
                    "$FULL_TOR_CMD"
        cat "$4" >&2
    fi
}

# Check for the patterns in the match file $1, in the output file $2.
# Uses grep with the entire contents of the match file as the pattern.
# (Not "grep -f".)
#
# If the pattern does not match any lines in the output file, fail.
# Log the pattern, and the entire contents of the output file.
#
# Log failure messages using fail_printf(), with the match file name,
# context $3, and tor command line $4, which are required.
check_pattern()
{
    expect_log="$(cat "$1")"
    if ! grep "$expect_log" "$2" > /dev/null; then
        fail_printf "Expected %s '%s':\\n%s\\n" \
                    "$3" \
                    "$1" \
                    "$expect_log"
        printf "Tor --verify-config said:\\n%s\\n" \
               "$4" >&2
        cat "$2" >&2
    fi
}

# Run tor --verify-config on the torrc $1, and defaults torrc $2, which may
# be $EMPTY. Pass tor the extra command line arguments $3, which will be
# passed unquoted. Send tor's standard output to $4.
#
# If tor's exit status does not match the boolean $5, fail.
#
# Check for the patterns in the match file $6, in the output file.
# Uses grep with the entire contents of the match file as the pattern.
# (Not "grep -f".) The match file must not be empty.
#
# If the pattern does not match any lines in the output file, fail.
# Log the pattern, and the entire contents of the output file.
#
# Log failure messages using fail_printf(), with the match file name,
# and context $7, which is required.
check_verify_config()
{
    if check_empty_pattern "$6" "$7"; then
        return
    fi

    FULL_TOR_CMD=
    verify_config "$1" \
                  "$2" \
                  "$3" \
                  "$4" \
                  "$5" \
                  "$6" \
                  "$7"

    check_pattern "$6" \
                  "$4" \
                  "$7" \
                  "$FULL_TOR_CMD"
}

for dir in "${EXAMPLEDIR}"/*; do
    NEXT_TEST=

    if ! test -d "$dir"; then
       # Only count directories.
       continue
    fi

    testname="$(basename "${dir}")"
    # We use printf since "echo -n" is not standard
    printf "%s: " \
           "$testname"

    PREV_DIR="$(pwd)"
    cd "$dir"

    if test -f "./torrc.defaults"; then
        DEFAULTS="./torrc.defaults"
    else
        DEFAULTS="${DATA_DIR}/EMPTY"
    fi

    if test -f "./cmdline"; then
        CMDLINE="$(cat ./cmdline)"
    else
        CMDLINE=""
    fi

    EXPECTED=
    EXPECTED_LOG=
    ERROR=
    # Search for a custom result file for any combination of enabled optional
    # libraries
    # The libs in the list are [A-Za-z0-9_]* and space-separated.
    # shellcheck disable=SC2086
    for lib_suffix in $TOR_LIBS_ENABLED_SEARCH ""; do
        # Search for a custom result file for any disabled modules
        for mod_suffix in "_no_${TOR_MODULES_DISABLED}" ""; do
            suffix="${lib_suffix}${mod_suffix}"

            if test -f "./expected${suffix}"; then

                # Check for broken configs
                if test -f "./error${suffix}"; then
                    fail_printf "Found both '%s' and '%s'.%s\\n" \
                                "${dir}/expected${suffix}" \
                                "${dir}/error${suffix}" \
                                "(Only one of these files should exist.)"
                    break
                fi

                EXPECTED="./expected${suffix}"
                if test -f "./expected_log${suffix}"; then
                    EXPECTED_LOG="./expected_log${suffix}"
                fi
                break

            elif test -f "./error${suffix}"; then
                ERROR="./error${suffix}"
                break
            fi
        done

        # Exit as soon as the inner loop finds a file, or fails
        if test -f "$EXPECTED" || test -f "$ERROR" || test "$NEXT_TEST"; then
            break
        fi
    done

    if test "$NEXT_TEST"; then
        # The test failed inside the file search loop: go to the next test
        continue
    elif test -f "$EXPECTED"; then
        # This case should succeed: run dump-config and see if it does.

        if check_dump_config "./torrc" \
                             "$DEFAULTS" \
                             "$CMDLINE" \
                             "${DATA_DIR}/output.${testname}" \
                             "$EXPECTED" \
                             ""; then
            # Check round-trip.
            check_dump_config "${DATA_DIR}/output.${testname}" \
                              "$EMPTY" \
                              "" \
                              "${DATA_DIR}/output_2.${testname}" \
                              "$EXPECTED" \
                              "on round-trip" || true
        fi

        if test -f "$EXPECTED_LOG"; then
            # This case should succeed: run verify-config and see if it does.

            check_verify_config "./torrc" \
                                "$DEFAULTS" \
                                "$CMDLINE" \
                                "${DATA_DIR}/output_log.${testname}" \
                                "$TRUE" \
                                "$EXPECTED_LOG" \
                                "log success"
        else
            printf "\\nNOTICE: Missing '%s_log' file:\\n" \
                   "$EXPECTED" >&2
            log_verify_config "./torrc" \
                              "$DEFAULTS" \
                              "$CMDLINE"
        fi

   elif test -f "$ERROR"; then
        # This case should fail: run verify-config and see if it does.

        check_verify_config "./torrc" \
                            "$DEFAULTS" \
                            "$CMDLINE" \
                            "${DATA_DIR}/output.${testname}" \
                            "$FALSE" \
                            "$ERROR" \
                            "log error"
    else
        # This case is not actually configured with a success or a failure.
        # call that an error.
        fail_printf "Did not find ${dir}/*expected or ${dir}/*error.\\n"
    fi

    if test -z "$NEXT_TEST"; then
        echo "OK"
    fi

    cd "$PREV_DIR"

done

exit "$FINAL_EXIT"