diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/integration/harness.py | 34 | ||||
| -rw-r--r-- | tests/integration/test_livesync.py | 208 | ||||
| -rw-r--r-- | tests/sync_daemon.pl | 294 | ||||
| -rw-r--r-- | tests/t_mutex.c | 2 | ||||
| -rw-r--r-- | tests/t_trackerlogic.c | 2 |
5 files changed, 537 insertions, 3 deletions
diff --git a/tests/integration/harness.py b/tests/integration/harness.py index b98ce83..10aa6de 100644 --- a/tests/integration/harness.py +++ b/tests/integration/harness.py | |||
| @@ -20,6 +20,9 @@ import urllib.error | |||
| 20 | import urllib.parse | 20 | import urllib.parse |
| 21 | import urllib.request | 21 | import urllib.request |
| 22 | 22 | ||
| 23 | # groupip_1 in src/ot_livesync.c | ||
| 24 | LIVESYNC_GROUP = "224.0.23.5" | ||
| 25 | |||
| 23 | TRACKER = os.environ.get("OPENTRACKER", os.path.join(os.path.dirname(__file__), "..", "..", "opentracker.debug")) | 26 | TRACKER = os.environ.get("OPENTRACKER", os.path.join(os.path.dirname(__file__), "..", "..", "opentracker.debug")) |
| 24 | 27 | ||
| 25 | 28 | ||
| @@ -76,6 +79,9 @@ class Tracker: | |||
| 76 | 79 | ||
| 77 | def __init__(self, extra_config="", files=None): | 80 | def __init__(self, extra_config="", files=None): |
| 78 | self.port = free_port() | 81 | self.port = free_port() |
| 82 | # Only used by a config that asks for live sync, but allocated either | ||
| 83 | # way so {livesync_port} always resolves. | ||
| 84 | self.livesync_port = free_port() | ||
| 79 | self.dir = tempfile.mkdtemp(prefix="ot-integration-") | 85 | self.dir = tempfile.mkdtemp(prefix="ot-integration-") |
| 80 | self.log = None | 86 | self.log = None |
| 81 | self.process = None | 87 | self.process = None |
| @@ -87,7 +93,7 @@ class Tracker: | |||
| 87 | with open(self.config, "w") as handle: | 93 | with open(self.config, "w") as handle: |
| 88 | handle.write("listen.tcp 127.0.0.1:%d\n" % self.port) | 94 | handle.write("listen.tcp 127.0.0.1:%d\n" % self.port) |
| 89 | handle.write("listen.udp 127.0.0.1:%d\n" % self.port) | 95 | handle.write("listen.udp 127.0.0.1:%d\n" % self.port) |
| 90 | handle.write(extra_config.replace("{dir}", self.dir)) | 96 | handle.write(extra_config.replace("{dir}", self.dir).replace("{livesync_port}", str(self.livesync_port))) |
| 91 | self.log = open(os.path.join(self.dir, "tracker.log"), "w+b") | 97 | self.log = open(os.path.join(self.dir, "tracker.log"), "w+b") |
| 92 | # Leak detection is left at the sanitizer's own default, which is on | 98 | # Leak detection is left at the sanitizer's own default, which is on |
| 93 | # where LeakSanitizer exists and off where it does not. Asking for it | 99 | # where LeakSanitizer exists and off where it does not. Asking for it |
| @@ -186,6 +192,32 @@ class Tracker: | |||
| 186 | def torrent_count(self): | 192 | def torrent_count(self): |
| 187 | return int(self.stats("torr").split()[0]) | 193 | return int(self.stats("torr").split()[0]) |
| 188 | 194 | ||
| 195 | def livesync_send(self, payload, group=LIVESYNC_GROUP): | ||
| 196 | """Put a packet on the live sync group. | ||
| 197 | |||
| 198 | It has to go to the group rather than straight at the port: the | ||
| 199 | tracker binds two sockets there, the reader on the wildcard address | ||
| 200 | and the writer on the configured one, and the kernel gives a unicast | ||
| 201 | datagram to the more specific of the two, which nobody reads.""" | ||
| 202 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) | ||
| 203 | try: | ||
| 204 | sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF, socket.inet_aton("127.0.0.1")) | ||
| 205 | sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, 1) | ||
| 206 | sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 1) | ||
| 207 | sock.sendto(payload, (group, self.livesync_port)) | ||
| 208 | finally: | ||
| 209 | sock.close() | ||
| 210 | |||
| 211 | def wait_until(self, predicate, timeout=5): | ||
| 212 | """A live sync packet is handled by another thread, so an effect of | ||
| 213 | one arrives some time after the send.""" | ||
| 214 | deadline = time.time() + timeout | ||
| 215 | while time.time() < deadline: | ||
| 216 | if predicate(): | ||
| 217 | return True | ||
| 218 | time.sleep(0.02) | ||
| 219 | return predicate() | ||
| 220 | |||
| 189 | # ------------------------------------------------------------ over udp | 221 | # ------------------------------------------------------------ over udp |
| 190 | 222 | ||
| 191 | def udp_connect(self): | 223 | def udp_connect(self): |
diff --git a/tests/integration/test_livesync.py b/tests/integration/test_livesync.py new file mode 100644 index 0000000..52e871f --- /dev/null +++ b/tests/integration/test_livesync.py | |||
| @@ -0,0 +1,208 @@ | |||
| 1 | """Live sync, driven over the multicast group a cluster would use. | ||
| 2 | |||
| 3 | Live sync sits behind WANT_SYNC_LIVE, so these only run against a binary | ||
| 4 | built with it: make check-livesync. Against any other build they skip, which | ||
| 5 | is what the feature list in -v is for. | ||
| 6 | """ | ||
| 7 | |||
| 8 | import socket | ||
| 9 | import struct | ||
| 10 | import subprocess | ||
| 11 | import unittest | ||
| 12 | |||
| 13 | from harness import TRACKER, TrackerTestCase | ||
| 14 | |||
| 15 | # enum { OT_SYNC_PEER4, OT_SYNC_PEER6 } in src/ot_livesync.c | ||
| 16 | SYNC_PEER4 = 0 | ||
| 17 | SYNC_PEER6 = 1 | ||
| 18 | |||
| 19 | # src/trackerlogic.h | ||
| 20 | FLAG_SEEDING = 0x80 | ||
| 21 | FLAG_COMPLETED = 0x40 | ||
| 22 | FLAG_STOPPED = 0x20 | ||
| 23 | |||
| 24 | |||
| 25 | def has_livesync(): | ||
| 26 | done = subprocess.run([TRACKER, "-v"], capture_output=True, timeout=30) | ||
| 27 | return b"WANT_SYNC_LIVE" in done.stderr | ||
| 28 | |||
| 29 | |||
| 30 | def peer4(address, port, flags): | ||
| 31 | """Four bytes of address, two of port, one of flags, one the tracker | ||
| 32 | overwrites with its own idea of the time.""" | ||
| 33 | return socket.inet_aton(address) + struct.pack("!H", port) + bytes([flags, 0]) | ||
| 34 | |||
| 35 | |||
| 36 | def peer6(address, port, flags): | ||
| 37 | return socket.inet_pton(socket.AF_INET6, address) + struct.pack("!H", port) + bytes([flags, 0]) | ||
| 38 | |||
| 39 | |||
| 40 | def packet(kind, entries, tracker_id=0xDEADBEEF): | ||
| 41 | body = b"".join(info_hash + peer for info_hash, peer in entries) | ||
| 42 | return struct.pack("!I", tracker_id) + struct.pack("!I", kind) + body | ||
| 43 | |||
| 44 | |||
| 45 | @unittest.skipUnless(has_livesync(), "this build has no WANT_SYNC_LIVE") | ||
| 46 | class LiveSyncIn(TrackerTestCase): | ||
| 47 | config = "livesync.cluster.listen 127.0.0.1:{livesync_port}\nlivesync.cluster.node_ip 127.0.0.1\n" | ||
| 48 | |||
| 49 | def counts(self, info_hash): | ||
| 50 | """Seeders and leechers a scrape reports for one torrent.""" | ||
| 51 | files = self.tracker.scrape(info_hash)[b"files"] | ||
| 52 | if info_hash not in files: | ||
| 53 | return None | ||
| 54 | entry = files[info_hash] | ||
| 55 | return entry[b"complete"], entry[b"incomplete"] | ||
| 56 | |||
| 57 | def test_a_synced_seeder_arrives(self): | ||
| 58 | info_hash = self.hash_for("sync-seed") | ||
| 59 | self.tracker.livesync_send(packet(SYNC_PEER4, [(info_hash, peer4("10.1.2.3", 6881, FLAG_SEEDING))])) | ||
| 60 | self.assertTrue(self.tracker.wait_until(lambda: self.counts(info_hash) == (1, 0)), | ||
| 61 | "scrape says %r" % (self.counts(info_hash),)) | ||
| 62 | |||
| 63 | def test_a_synced_leecher_arrives(self): | ||
| 64 | info_hash = self.hash_for("sync-leech") | ||
| 65 | self.tracker.livesync_send(packet(SYNC_PEER4, [(info_hash, peer4("10.1.2.4", 6881, 0))])) | ||
| 66 | self.assertTrue(self.tracker.wait_until(lambda: self.counts(info_hash) == (0, 1)), | ||
| 67 | "scrape says %r" % (self.counts(info_hash),)) | ||
| 68 | |||
| 69 | def test_a_v6_peer_arrives(self): | ||
| 70 | info_hash = self.hash_for("sync-v6") | ||
| 71 | self.tracker.livesync_send(packet(SYNC_PEER6, [(info_hash, peer6("2001:db8::1", 6881, FLAG_SEEDING))])) | ||
| 72 | self.assertTrue(self.tracker.wait_until(lambda: self.counts(info_hash) == (1, 0)), | ||
| 73 | "scrape says %r" % (self.counts(info_hash),)) | ||
| 74 | |||
| 75 | def test_one_packet_carries_several_torrents(self): | ||
| 76 | hashes = [self.hash_for("sync-many-%d" % i) for i in range(4)] | ||
| 77 | self.tracker.livesync_send(packet(SYNC_PEER4, [(h, peer4("10.1.2.%d" % (5 + i), 6881, FLAG_SEEDING)) | ||
| 78 | for i, h in enumerate(hashes)])) | ||
| 79 | self.assertTrue(self.tracker.wait_until(lambda: all(self.counts(h) == (1, 0) for h in hashes))) | ||
| 80 | |||
| 81 | def test_a_stopped_peer_is_taken_away_again(self): | ||
| 82 | info_hash = self.hash_for("sync-stop") | ||
| 83 | peer = peer4("10.1.2.9", 6881, FLAG_SEEDING) | ||
| 84 | self.tracker.livesync_send(packet(SYNC_PEER4, [(info_hash, peer)])) | ||
| 85 | self.assertTrue(self.tracker.wait_until(lambda: self.counts(info_hash) == (1, 0))) | ||
| 86 | |||
| 87 | self.tracker.livesync_send(packet(SYNC_PEER4, [(info_hash, peer4("10.1.2.9", 6881, FLAG_STOPPED))])) | ||
| 88 | self.assertTrue(self.tracker.wait_until(lambda: self.counts(info_hash) in (None, (0, 0))), | ||
| 89 | "scrape still says %r" % (self.counts(info_hash),)) | ||
| 90 | |||
| 91 | def test_a_truncated_entry_is_dropped(self): | ||
| 92 | """The loop takes whole hash and peer pairs and leaves a short tail | ||
| 93 | where it is, so the complete entry before it still lands.""" | ||
| 94 | good, short = self.hash_for("sync-good"), self.hash_for("sync-short") | ||
| 95 | payload = packet(SYNC_PEER4, [(good, peer4("10.1.2.10", 6881, FLAG_SEEDING))]) + short + b"\x0a\x01" | ||
| 96 | self.tracker.livesync_send(payload) | ||
| 97 | self.assertTrue(self.tracker.wait_until(lambda: self.counts(good) == (1, 0))) | ||
| 98 | self.assertIsNone(self.counts(short)) | ||
| 99 | |||
| 100 | def test_an_unknown_packet_type_is_ignored(self): | ||
| 101 | info_hash = self.hash_for("sync-unknown") | ||
| 102 | self.tracker.livesync_send(packet(99, [(info_hash, peer4("10.1.2.11", 6881, FLAG_SEEDING))])) | ||
| 103 | self.assertFalse(self.tracker.wait_until(lambda: self.counts(info_hash) is not None, timeout=1)) | ||
| 104 | |||
| 105 | def test_synced_peers_are_counted(self): | ||
| 106 | """The live sync worker keeps its own tally like every other worker, | ||
| 107 | and ?mode=syncs is the sum of them.""" | ||
| 108 | before = int(self.tracker.stats("syncs").split()[0]) | ||
| 109 | hashes = [self.hash_for("sync-count-%d" % i) for i in range(3)] | ||
| 110 | self.tracker.livesync_send(packet(SYNC_PEER4, [(h, peer4("10.1.2.%d" % (20 + i), 6881, FLAG_SEEDING)) | ||
| 111 | for i, h in enumerate(hashes)])) | ||
| 112 | self.assertTrue(self.tracker.wait_until( | ||
| 113 | lambda: int(self.tracker.stats("syncs").split()[0]) == before + 3), | ||
| 114 | "syncs says %s" % self.tracker.stats("syncs").split()[0]) | ||
| 115 | |||
| 116 | def test_a_packet_with_nothing_but_a_header_is_ignored(self): | ||
| 117 | before = self.tracker.torrent_count() | ||
| 118 | self.tracker.livesync_send(struct.pack("!I", 0xDEADBEEF) + struct.pack("!I", SYNC_PEER4)) | ||
| 119 | self.assertFalse(self.tracker.wait_until(lambda: self.tracker.torrent_count() != before, timeout=1)) | ||
| 120 | |||
| 121 | |||
| 122 | @unittest.skipUnless(has_livesync(), "this build has no WANT_SYNC_LIVE") | ||
| 123 | class LiveSyncAccess(TrackerTestCase): | ||
| 124 | """Nothing blesses 127.0.0.1 here, so the tracker should take nothing | ||
| 125 | from it. Source address checking is all live sync has.""" | ||
| 126 | |||
| 127 | config = "livesync.cluster.listen 127.0.0.1:{livesync_port}\nlivesync.cluster.node_ip 10.0.0.1\n" | ||
| 128 | |||
| 129 | def test_an_unblessed_sender_is_ignored(self): | ||
| 130 | info_hash = self.hash_for("sync-unblessed") | ||
| 131 | self.tracker.livesync_send(packet(SYNC_PEER4, [(info_hash, peer4("10.1.2.12", 6881, FLAG_SEEDING))])) | ||
| 132 | self.assertFalse(self.tracker.wait_until(lambda: self.tracker.torrent_count() != 0, timeout=1)) | ||
| 133 | |||
| 134 | |||
| 135 | @unittest.skipUnless(has_livesync(), "this build has no WANT_SYNC_LIVE") | ||
| 136 | class LiveSyncUnconfigured(TrackerTestCase): | ||
| 137 | """A binary that has the feature but no cluster to join. Somebody whose | ||
| 138 | packager turned WANT_SYNC_LIVE on did not ask for live sync and should | ||
| 139 | not be able to tell that it is there.""" | ||
| 140 | |||
| 141 | config = "" | ||
| 142 | |||
| 143 | def test_the_tracker_runs_anyway(self): | ||
| 144 | info_hash = self.hash_for("no-cluster") | ||
| 145 | self.tracker.announce(info_hash, port=6881, left=0) | ||
| 146 | self.assertEqual(self.tracker.torrent_count(), 1) | ||
| 147 | |||
| 148 | def test_nothing_is_said_about_the_missing_cluster(self): | ||
| 149 | self.assertNotIn("sync", self.tracker.output().lower()) | ||
| 150 | |||
| 151 | def test_no_socket_is_bound_for_it(self): | ||
| 152 | """A configured tracker holds the wildcard address on that port, so | ||
| 153 | taking it here proves nothing was started.""" | ||
| 154 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) | ||
| 155 | try: | ||
| 156 | sock.bind(("0.0.0.0", self.tracker.livesync_port)) | ||
| 157 | finally: | ||
| 158 | sock.close() | ||
| 159 | |||
| 160 | def test_announcing_stays_clean(self): | ||
| 161 | """Enough announces to pass the watermark that would flush an | ||
| 162 | outgoing buffer, if there were one to flush. The sanitizers in this | ||
| 163 | build turn anything the attempt touched into a failure.""" | ||
| 164 | for i in range(200): | ||
| 165 | self.tracker.announce(self.hash_for("no-cluster-%d" % i), port=6881 + i, left=0) | ||
| 166 | self.assertEqual(self.tracker.torrent_count(), 200) | ||
| 167 | |||
| 168 | |||
| 169 | # RFC 2365 sets 239.192.0.0/14 aside for exactly this kind of use | ||
| 170 | OWN_GROUP = "239.192.0.7" | ||
| 171 | |||
| 172 | |||
| 173 | @unittest.skipUnless(has_livesync(), "this build has no WANT_SYNC_LIVE") | ||
| 174 | class LiveSyncGroup(TrackerTestCase): | ||
| 175 | config = ("livesync.cluster.group %s\n" | ||
| 176 | "livesync.cluster.listen 127.0.0.1:{livesync_port}\n" | ||
| 177 | "livesync.cluster.node_ip 127.0.0.1\n" % OWN_GROUP) | ||
| 178 | |||
| 179 | def test_the_configured_group_is_joined(self): | ||
| 180 | info_hash = self.hash_for("own-group") | ||
| 181 | self.tracker.livesync_send(packet(SYNC_PEER4, [(info_hash, peer4("10.1.3.1", 6881, FLAG_SEEDING))]), | ||
| 182 | group=OWN_GROUP) | ||
| 183 | self.assertTrue(self.tracker.wait_until(lambda: self.tracker.torrent_count() == 1)) | ||
| 184 | |||
| 185 | def test_the_default_group_is_left_alone(self): | ||
| 186 | """A cluster that picked its own group should not hear the one every | ||
| 187 | other opentracker is on.""" | ||
| 188 | info_hash = self.hash_for("not-our-group") | ||
| 189 | self.tracker.livesync_send(packet(SYNC_PEER4, [(info_hash, peer4("10.1.3.2", 6881, FLAG_SEEDING))])) | ||
| 190 | self.assertFalse(self.tracker.wait_until(lambda: self.tracker.torrent_count() != 0, timeout=1)) | ||
| 191 | |||
| 192 | |||
| 193 | @unittest.skipUnless(has_livesync(), "this build has no WANT_SYNC_LIVE") | ||
| 194 | class LiveSyncGroupRefused(TrackerTestCase): | ||
| 195 | """A unicast address is not a group. The line is refused like any other | ||
| 196 | bad config line, which leaves the default standing.""" | ||
| 197 | |||
| 198 | config = ("livesync.cluster.group 10.0.0.1\n" | ||
| 199 | "livesync.cluster.listen 127.0.0.1:{livesync_port}\n" | ||
| 200 | "livesync.cluster.node_ip 127.0.0.1\n") | ||
| 201 | |||
| 202 | def test_the_line_is_refused(self): | ||
| 203 | self.assertIn("Parse error", self.tracker.output()) | ||
| 204 | |||
| 205 | def test_and_the_default_group_still_works(self): | ||
| 206 | info_hash = self.hash_for("fallback-group") | ||
| 207 | self.tracker.livesync_send(packet(SYNC_PEER4, [(info_hash, peer4("10.1.3.3", 6881, FLAG_SEEDING))])) | ||
| 208 | self.assertTrue(self.tracker.wait_until(lambda: self.tracker.torrent_count() == 1)) | ||
diff --git a/tests/sync_daemon.pl b/tests/sync_daemon.pl new file mode 100644 index 0000000..9e4bdb9 --- /dev/null +++ b/tests/sync_daemon.pl | |||
| @@ -0,0 +1,294 @@ | |||
| 1 | #!/usr/bin/perl | ||
| 2 | |||
| 3 | # This software was written by Philipp Wuensche <cryx-otsync@h3q.com> | ||
| 4 | # It is considered beerware. | ||
| 5 | |||
| 6 | use strict; | ||
| 7 | |||
| 8 | #use Convert::Bencode_XS qw(:all); | ||
| 9 | use Convert::Bencode qw(:all); | ||
| 10 | use Data::Dumper; | ||
| 11 | use LWP::UserAgent; | ||
| 12 | use URI::Escape; | ||
| 13 | |||
| 14 | # enable verbose output | ||
| 15 | my $debug = 0; | ||
| 16 | |||
| 17 | # tracker from where we get our sync data | ||
| 18 | my @trackers = ('127.0.0.1:8989'); | ||
| 19 | # tracker to upload merged data | ||
| 20 | my @client_tracker = ('127.0.0.1:8989'); | ||
| 21 | |||
| 22 | # time to wait between syncs | ||
| 23 | my $sleeptime = '300'; | ||
| 24 | |||
| 25 | # SSL cert and key | ||
| 26 | my $ssl_cert = 'cert.pem'; | ||
| 27 | my $ssl_key = 'key.pem'; | ||
| 28 | |||
| 29 | foreach(@trackers) { | ||
| 30 | print "Syncing from: $_\n"; | ||
| 31 | } | ||
| 32 | foreach(@client_tracker) { | ||
| 33 | print "Syncing to: $_\n"; | ||
| 34 | } | ||
| 35 | |||
| 36 | my $file = shift; | ||
| 37 | |||
| 38 | |||
| 39 | # global hash for storing the merged syncs | ||
| 40 | my %merged_syncs; | ||
| 41 | |||
| 42 | while(1) { | ||
| 43 | %merged_syncs; | ||
| 44 | my @bencoded_sync_data; | ||
| 45 | |||
| 46 | # fetch the sync from every tracker and put it into an array in bencoded form | ||
| 47 | foreach my $tracker (@trackers) { | ||
| 48 | my $bencoded_sync = fetch_sync($tracker); | ||
| 49 | # my $bencoded_sync = fetch_sync_from_file($file); | ||
| 50 | if($bencoded_sync ne 0 && $bencoded_sync =~ /^d4\:sync/) { | ||
| 51 | push(@bencoded_sync_data,$bencoded_sync); | ||
| 52 | } | ||
| 53 | } | ||
| 54 | |||
| 55 | # bdecode every sync and throw it into the merged-sync | ||
| 56 | foreach my $bencoded_sync (@bencoded_sync_data) { | ||
| 57 | print "Doing merge...\n"; | ||
| 58 | merge_sync(bdecode($bencoded_sync)); | ||
| 59 | my $num_torrents = keys(%merged_syncs); | ||
| 60 | |||
| 61 | print "number of torrents: $num_torrents\n"; | ||
| 62 | } | ||
| 63 | |||
| 64 | # number of max. peers in one changeset | ||
| 65 | my $peer_limit = 500; | ||
| 66 | # max number of changesets per commit | ||
| 67 | my $max_changesets = 10; | ||
| 68 | my $hash_count = 0; | ||
| 69 | my $peer_count = 0; | ||
| 70 | my $changeset; | ||
| 71 | my @escaped_changesets; | ||
| 72 | |||
| 73 | # run until all hashes are put into changesets and commited to the trackers | ||
| 74 | while(keys(%merged_syncs) != 0) { | ||
| 75 | |||
| 76 | foreach my $hash (keys(%merged_syncs)) { | ||
| 77 | print "Starting new changeset\n" if($peer_count == 0 && $debug); | ||
| 78 | my $num_peers = keys(%{$merged_syncs{$hash}}); | ||
| 79 | |||
| 80 | print "\t$peer_count peers for $hash_count hashes in changeset\n" if($debug); | ||
| 81 | |||
| 82 | my $pack_hash = pack('H*',$hash); | ||
| 83 | |||
| 84 | # as long as the peer_limit is not reached, add new hashes with peers to the changeset hash-table | ||
| 85 | if($peer_count < $peer_limit) { | ||
| 86 | print "\t\tAdd $num_peers peers for $hash changeset\n" if($debug); | ||
| 87 | $peer_count = $peer_count + $num_peers; | ||
| 88 | foreach my $peer_socket (keys(%{$merged_syncs{$hash}})) { | ||
| 89 | |||
| 90 | my $flags = $merged_syncs{$hash}{$peer_socket}; | ||
| 91 | |||
| 92 | print "\t\t\tAdd $peer_socket $flags\n" if($debug); | ||
| 93 | |||
| 94 | my $pack_peer = packme($peer_socket,$flags); | ||
| 95 | |||
| 96 | $changeset->{'sync'}->{$pack_hash} = $changeset->{'sync'}->{$pack_hash}.$pack_peer; | ||
| 97 | } | ||
| 98 | $hash_count++; | ||
| 99 | # hash is stored in the changeset, delete it from the hash-table | ||
| 100 | delete $merged_syncs{$hash}; | ||
| 101 | } | ||
| 102 | |||
| 103 | # the peer_limit is reached or we are out of torrents, so start preparing a changeset | ||
| 104 | if($peer_count >= $peer_limit || keys(%merged_syncs) == 0) { | ||
| 105 | |||
| 106 | print "Commit changeset for $hash_count hashes with $peer_count peers total\n" if($debug); | ||
| 107 | |||
| 108 | # bencode the changeset | ||
| 109 | my $enc_changeset = bencode($changeset); | ||
| 110 | |||
| 111 | # URL-escape the changeset and put into an array of changesets | ||
| 112 | my $foobar = uri_escape($enc_changeset); | ||
| 113 | push(@escaped_changesets,$foobar); | ||
| 114 | |||
| 115 | # the changeset is ready and stored, so delete it from the changeset hash-table | ||
| 116 | delete $changeset->{'sync'}; | ||
| 117 | |||
| 118 | $hash_count = 0; | ||
| 119 | $peer_count = 0; | ||
| 120 | print "\n\n\n" if($debug); | ||
| 121 | } | ||
| 122 | |||
| 123 | # if enought changesets are prepared or we are out of torrents for more changesets, | ||
| 124 | # sync the changesets to the trackers | ||
| 125 | if($#escaped_changesets == $max_changesets || keys(%merged_syncs) == 0) { | ||
| 126 | print "\tSync...\n"; | ||
| 127 | sync_to_tracker(\@escaped_changesets); | ||
| 128 | undef @escaped_changesets; | ||
| 129 | } | ||
| 130 | |||
| 131 | } | ||
| 132 | } | ||
| 133 | |||
| 134 | print "Sleeping for $sleeptime seconds\n"; | ||
| 135 | sleep $sleeptime; | ||
| 136 | } | ||
| 137 | |||
| 138 | sub connect_tracker { | ||
| 139 | # connect a tracker via HTTPS, returns the body of the response | ||
| 140 | my $url = shift; | ||
| 141 | |||
| 142 | $ENV{HTTPS_DEBUG} = 0; | ||
| 143 | $ENV{HTTPS_CERT_FILE} = $ssl_cert; | ||
| 144 | $ENV{HTTPS_KEY_FILE} = $ssl_key; | ||
| 145 | |||
| 146 | my $ua = new LWP::UserAgent; | ||
| 147 | my $req = new HTTP::Request('GET', $url); | ||
| 148 | my $res = $ua->request($req); | ||
| 149 | |||
| 150 | my $content = $res->content; | ||
| 151 | |||
| 152 | if($res->is_success()) { | ||
| 153 | return $content; | ||
| 154 | } else { | ||
| 155 | print $res->code."|".$res->status_line."\n"; | ||
| 156 | return 0; | ||
| 157 | } | ||
| 158 | } | ||
| 159 | |||
| 160 | sub sync_to_tracker { | ||
| 161 | # commit changesets to a tracker | ||
| 162 | my @changesets = @{(shift)}; | ||
| 163 | |||
| 164 | # prepare the URI with URL-encoded changesets concatenated by a & | ||
| 165 | my $uri = 'sync?'; | ||
| 166 | foreach my $set (@changesets) { | ||
| 167 | $uri .= 'changeset='.$set.'&'; | ||
| 168 | } | ||
| 169 | my $uri_length = length($uri); | ||
| 170 | |||
| 171 | # commit the collection of changesets to the tracker via HTTPS | ||
| 172 | foreach my $tracker (@client_tracker) { | ||
| 173 | print "\t\tTracker: $tracker (URI: $uri_length)\n"; | ||
| 174 | my $url = "https://$tracker/".$uri; | ||
| 175 | connect_tracker($url); | ||
| 176 | } | ||
| 177 | } | ||
| 178 | |||
| 179 | sub packme { | ||
| 180 | # pack data | ||
| 181 | # returns ipaddr, port and flags in packed format | ||
| 182 | my $peer_socket = shift; | ||
| 183 | my $flags = shift; | ||
| 184 | |||
| 185 | my($a,$b,$c,$d,$port) = split(/[\.,\:]/,$peer_socket); | ||
| 186 | my $pack_peer = pack('C4 n1 b16',$a,$b,$c,$d,$port,$flags); | ||
| 187 | |||
| 188 | return $pack_peer; | ||
| 189 | } | ||
| 190 | |||
| 191 | sub unpackme { | ||
| 192 | # unpack packed data | ||
| 193 | # returns ipaddr. in quad-form with port (a.b.c.d:port) and flags as bitfield | ||
| 194 | # data is packed as: | ||
| 195 | # - 4 byte ipaddr. (unsigned char value) | ||
| 196 | # - 2 byte port (unsigned short in "network" (big-endian) order) | ||
| 197 | # - 2 byte flags (bit string (ascending bit order inside each byte)) | ||
| 198 | my $data = shift; | ||
| 199 | |||
| 200 | my($a, $b, $c, $d, $port, $flags) = unpack('C4 n1 b16',$data); | ||
| 201 | my $peer_socket = "$a\.$b\.$c\.$d\:$port"; | ||
| 202 | |||
| 203 | return($peer_socket,$flags); | ||
| 204 | } | ||
| 205 | |||
| 206 | sub fetch_sync { | ||
| 207 | # fetch sync from a tracker | ||
| 208 | my $tracker = shift; | ||
| 209 | my $url = "https://$tracker/sync"; | ||
| 210 | |||
| 211 | print "Fetching from $url\n"; | ||
| 212 | |||
| 213 | my $body = connect_tracker($url); | ||
| 214 | |||
| 215 | if($body && $body =~ /^d4\:sync/) { | ||
| 216 | return $body; | ||
| 217 | } else { | ||
| 218 | return 0; | ||
| 219 | } | ||
| 220 | } | ||
| 221 | |||
| 222 | sub fetch_sync_from_file { | ||
| 223 | # fetch sync from a file | ||
| 224 | my $file = shift; | ||
| 225 | my $body; | ||
| 226 | print "Fetching from file $file\n"; | ||
| 227 | open(FILE,"<$file"); | ||
| 228 | while(<FILE>) { | ||
| 229 | $body .= $_; | ||
| 230 | } | ||
| 231 | close(FILE); | ||
| 232 | return $body; | ||
| 233 | } | ||
| 234 | |||
| 235 | sub merge_sync { | ||
| 236 | # This builds a hash table with the torrenthash as keys. The value is a hash table again with the peer-socket as keys | ||
| 237 | # and flags in the value | ||
| 238 | # Example: | ||
| 239 | # 60dd2beb4197f71677c0f5ba92b956f7d04651e5 => | ||
| 240 | # 192.168.23.23:2323 => 0000000000000000 | ||
| 241 | # 23.23.23.23:2342 => 0000000100000000 | ||
| 242 | # b220b4d7136e84a88abc090db88bec8604a808f3 => | ||
| 243 | # 42.23.42.23:55555 => 0000000000000000 | ||
| 244 | |||
| 245 | my $hashref = shift; | ||
| 246 | |||
| 247 | my $nonuniq_hash_counter = 0; | ||
| 248 | my $nonuniq_peer_counter = 0; | ||
| 249 | my $hash_counter = 0; | ||
| 250 | my $peer_counter = 0; | ||
| 251 | |||
| 252 | foreach my $key (keys(%{$hashref->{'sync'}})) { | ||
| 253 | # start merge for every sha1-hash in the sync | ||
| 254 | my $hash = unpack('H*',$key); | ||
| 255 | |||
| 256 | $hash_counter++; | ||
| 257 | $nonuniq_hash_counter++ if exists $merged_syncs{$hash}; | ||
| 258 | |||
| 259 | while(${$hashref->{'sync'}}{$key} ne "") | ||
| 260 | { | ||
| 261 | # split the value into 8-byte and unpack it for getting peer-socket and flags | ||
| 262 | my($peer_socket,$flags) = unpackme(substr(${$hashref->{'sync'}}{$key},0,8,'')); | ||
| 263 | |||
| 264 | $peer_counter++; | ||
| 265 | $nonuniq_peer_counter++ if exists $merged_syncs{$hash}{$peer_socket}; | ||
| 266 | |||
| 267 | # Create a hash table with sha1-hash as key and a hash table as value. | ||
| 268 | # The hash table in the value has the peer-socket as key and flags as value | ||
| 269 | # If the entry already exists, the flags are ORed together, if not it is ORed with 0 | ||
| 270 | $merged_syncs{$hash}{$peer_socket} = $flags | $merged_syncs{$hash}{$peer_socket}; | ||
| 271 | } | ||
| 272 | } | ||
| 273 | print "$hash_counter hashes $nonuniq_hash_counter non-uniq, $peer_counter peers $nonuniq_peer_counter non-uniq.\n"; | ||
| 274 | |||
| 275 | } | ||
| 276 | |||
| 277 | sub test_decode { | ||
| 278 | my $hashref = shift; | ||
| 279 | |||
| 280 | print "CHANGESET DEBUG OUTPUT\n"; | ||
| 281 | |||
| 282 | print Dumper $hashref; | ||
| 283 | foreach my $key (keys(%{$hashref->{'sync'}})) { | ||
| 284 | my $hash = unpack('H*',$key); | ||
| 285 | |||
| 286 | print "Changeset for $hash\n"; | ||
| 287 | while(${$hashref->{'sync'}}{$key} ne "") | ||
| 288 | { | ||
| 289 | |||
| 290 | my($peer_socket,$flags) = unpackme(substr(${$hashref->{'sync'}}{$key},0,8,'')); | ||
| 291 | print "\tSocket: $peer_socket Flags: $flags\n"; | ||
| 292 | } | ||
| 293 | } | ||
| 294 | } | ||
diff --git a/tests/t_mutex.c b/tests/t_mutex.c index e54e2fa..c0c41cc 100644 --- a/tests/t_mutex.c +++ b/tests/t_mutex.c | |||
| @@ -24,7 +24,7 @@ static void hash_for_bucket(ot_hash hash, int bucket) { | |||
| 24 | } | 24 | } |
| 25 | 25 | ||
| 26 | static void case_a_hash_names_its_bucket(void) { | 26 | static void case_a_hash_names_its_bucket(void) { |
| 27 | static const int buckets[] = {0, 1, 2, 17, 511, 512, OT_BUCKET_COUNT - 1}; | 27 | static const int buckets[] = {0, 1, 2, 17, OT_BUCKET_COUNT / 2 - 1, OT_BUCKET_COUNT / 2, OT_BUCKET_COUNT - 1}; |
| 28 | size_t i; | 28 | size_t i; |
| 29 | 29 | ||
| 30 | test_case("a hash is served by the bucket its leading bits name"); | 30 | test_case("a hash is served by the bucket its leading bits name"); |
diff --git a/tests/t_trackerlogic.c b/tests/t_trackerlogic.c index f64977f..1cf9141 100644 --- a/tests/t_trackerlogic.c +++ b/tests/t_trackerlogic.c | |||
| @@ -224,7 +224,7 @@ void test_trackerlogic(void) { | |||
| 224 | pthread_sigmask(SIG_BLOCK, &signal_mask, NULL); | 224 | pthread_sigmask(SIG_BLOCK, &signal_mask, NULL); |
| 225 | 225 | ||
| 226 | trackerlogic_init(); | 226 | trackerlogic_init(); |
| 227 | CHECK_INT(workstruct_init(&ws), 0); | 227 | CHECK_INT(workstruct_init(&ws, G_INBUF_SIZE), 0); |
| 228 | 228 | ||
| 229 | case_an_announce_makes_a_torrent(); | 229 | case_an_announce_makes_a_torrent(); |
| 230 | case_the_same_peer_twice_is_one_peer(); | 230 | case_the_same_peer_twice_is_one_peer(); |
