summaryrefslogtreecommitdiff
path: root/tests/integration/test_accesslist.py
blob: 1c84ccf656b57d9f75daf82f46aaaf9fa1902817 (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
"""The blacklist, loaded from a file at startup.

A blacklisted info hash must be refused at both the announce and the scrape
entry, and the file parser has to cope with the shapes an operator actually
produces: an empty file, and a last line with no newline after it.
"""

import os
import struct
import unittest

from harness import Tracker, TrackerTestCase

BLOCKED = b"\xb1" * 20
ALLOWED = b"\xa1" * 20


def listing(*hashes):
    return b"".join(h.hex().upper().encode() + b"\n" for h in hashes)


class Blacklist(TrackerTestCase):
    config = "access.blacklist {dir}/blacklist\n"
    files = {"blacklist": listing(BLOCKED)}

    def test_a_blacklisted_hash_is_refused(self):
        answer = self.tracker.announce(BLOCKED, port=6881, left=0)
        self.assertIn(b"failure reason", answer)
        self.assertIn(b"not authorized", answer[b"failure reason"])

    def test_a_blacklisted_hash_never_becomes_a_torrent(self):
        before = self.tracker.torrent_count()
        self.tracker.announce(BLOCKED, port=6881, left=0)
        self.assertEqual(self.tracker.torrent_count(), before, "the refused hash was stored anyway")

    def test_any_other_hash_is_served(self):
        answer = self.tracker.announce(ALLOWED, port=6881, left=0)
        self.assertNotIn(b"failure reason", answer)
        self.assertEqual(answer[b"complete"], 1)

    def test_the_block_is_an_error_packet_over_udp(self):
        """BEP 15 has an error action, which is what a refusal should use.
        It used to answer the announce header with no body behind it, a
        packet too short for any client to read."""
        sock, connection_id = self.tracker.udp_connect()
        sock.close()
        request = struct.pack(
            ">QII20s20sQQQIIIiH", connection_id, 1, 7, BLOCKED, b"p" * 20, 0, 0, 0, 0, 0, 0, -1, 6881
        )
        answer = self.tracker.udp_raw(request)
        self.assertIsNotNone(answer)
        action, transaction = struct.unpack(">II", answer[:8])
        self.assertEqual(action, 3, "action 3 is the error packet")
        self.assertEqual(transaction, 7)
        self.assertIn(b"not authorized", answer[8:])

    def test_a_blocked_hash_is_not_stored_when_it_arrives_over_udp(self):
        sock, connection_id = self.tracker.udp_connect()
        sock.close()
        self.tracker.udp_raw(
            struct.pack(">QII20s20sQQQIIIiH", connection_id, 1, 8, BLOCKED, b"p" * 20, 0, 0, 0, 0, 0, 0, -1, 6881)
        )
        self.assertEqual(self.tracker.torrent_count(), 0)


class BlacklistWithoutTrailingNewline(TrackerTestCase):
    """A single hash with no newline after it used to size the list at zero
    and then write into it, which took the tracker down before it listened."""

    config = "access.blacklist {dir}/blacklist\n"
    files = {"blacklist": BLOCKED.hex().upper().encode()}

    def test_the_last_line_counts_even_without_its_newline(self):
        answer = self.tracker.announce(BLOCKED, port=6881, left=0)
        self.assertIn(b"failure reason", answer)

    def test_and_the_tracker_still_serves_everything_else(self):
        self.assertEqual(self.tracker.announce(ALLOWED, port=6881, left=0)[b"complete"], 1)


class EmptyBlacklist(TrackerTestCase):
    config = "access.blacklist {dir}/blacklist\n"
    files = {"blacklist": b""}

    def test_an_empty_file_blocks_nothing_and_starts_cleanly(self):
        self.assertEqual(self.tracker.announce(BLOCKED, port=6881, left=0)[b"complete"], 1)
        self.assertEqual(self.tracker.announce(ALLOWED, port=6882, left=0)[b"complete"], 1)


class MissingBlacklist(TrackerTestCase):
    """The file is named but absent: the tracker warns and serves everything,
    because it may be created later."""

    config = "access.blacklist {dir}/absent\n"

    def test_a_missing_file_blocks_nothing(self):
        self.assertEqual(self.tracker.announce(BLOCKED, port=6881, left=0)[b"complete"], 1)


class ListIsInPlaceBeforeTheStateFile(TrackerTestCase):
    """The loader used to run in a thread of its own, so a state file read at
    startup was checked against a list that was still empty."""

    config = "access.blacklist {dir}/blacklist\n"
    files = {"blacklist": listing(BLOCKED)}

    def test_a_blacklisted_torrent_in_a_state_file_is_not_restored(self):
        path = os.path.join(self.tracker.dir, "state")
        with open(path, "w") as handle:
            handle.write("%s:5:7\n%s:5:7\n" % (BLOCKED.hex(), ALLOWED.hex()))

        restarted = Tracker(self.config, self.files, extra_args=["-l", path])
        try:
            self.assertNotIn(BLOCKED, restarted.scrape(BLOCKED)[b"files"], "the blacklist was not in place when the state file was read")
            self.assertIn(ALLOWED, restarted.scrape(ALLOWED)[b"files"], "the state file was not read at all")
            restarted.stop()
        finally:
            restarted._cleanup()


if __name__ == "__main__":
    unittest.main()