From 6be4125be5c6883504836a7fbc151730bdc38280 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anton=20Luka=20=C5=A0ijanec?= Date: Tue, 10 Jan 2023 23:49:18 +0100 Subject: attempts to fix ping flood and sybil attacks --- README.md | 11 ++- misc/sybil.txt | 75 +++++++++++++++++ src/dht.c | 252 ++++++++++++++++++++++++++++++++++++++++++++----------- utils/midpoint.c | 9 +- www/index.php | 1 + 5 files changed, 296 insertions(+), 52 deletions(-) create mode 100644 misc/sybil.txt diff --git a/README.md b/README.md index 0e04d85..39eba7c 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,20 @@ # external libraries * https://github.com/kokke/tiny-AES-c +* libmd-dev for sha1.h and sha2.h # todo * use ppoll * implement BEP-0042 DHT security extension, requires crc32c library * metadata receiving from uTorrent does not work for some unknown reason -* uses A LOT of bandwidth, implement packet deduplication +* uses A LOT of bandwidth, implement packet deduplication (fixed?) + +# anti sybil measures: +* detecting sybil with buckets > 32 (done) +* disallowing IP addresses to be added into our own bucket that already exist in the routing table (done) + - currently only if IP address is the one that causes the split, this may be suboptimal +* TODO: accept every replied node into the routing table only after pinging it with a random ID, storing in t encrypted first MAXT-1 bytes of the id that it sent with packet that generated a replied(). upon receiving the reply, if the r/id and decrypted t don't match in MAXT-1 bytes, ignore the node (it's a sybil attacker). + - MAXT-1 instead of MAXT because MAXT sized ts are sent in get_peers queries + - only consider possible nodes when reading "nodes" and "nodes6" in responses when responses are verified with this strategy, I think this prevents massive data sendings because of poorly programmed sybil nodes and poorly programed travnik. # known non-posix * `MSG_DONTWAIT`, `SOCK_NONBLOCK`: replace with `fcntl` `O_NONBLOCK` diff --git a/misc/sybil.txt b/misc/sybil.txt new file mode 100644 index 0000000..5b2d6ac --- /dev/null +++ b/misc/sybil.txt @@ -0,0 +1,75 @@ + BUCKET id=449a918e2f5d0eafffffffffffffffffffffffff + BUCKET id=449a918e2f5d0eb7ffffffffffffffffffffffff + BUCKET id=449a918e2f5d0eb8ffffffffffffffffffffffff + BUCKET id=449a918e2f5d0eb97fffffffffffffffffffffff + BUCKET id=449a918e2f5d0eb9bfffffffffffffffffffffff + BUCKET id=449a918e2f5d0eb9c1ffffffffffffffffffffff + BUCKET id=449a918e2f5d0eb9c2ffffffffffffffffffffff + BUCKET id=449a918e2f5d0eb9c31fffffffffffffffffffff + BUCKET id=449a918e2f5d0eb9c32fffffffffffffffffffff + BUCKET id=449a918e2f5d0eb9c337ffffffffffffffffffff + BUCKET id=449a918e2f5d0eb9c33bffffffffffffffffffff + BUCKET id=449a918e2f5d0eb9c33cffffffffffffffffffff + 449a918e2f5d0eb9c33d269d398f2b2b181f35ed ::ffff:82.156.184.234/6881 unans=0 good + BUCKET id=449a918e2f5d0eb9c33d3fffffffffffffffffff + BUCKET id=449a918e2f5d0eb9c33d4fffffffffffffffffff + BUCKET id=449a918e2f5d0eb9c33d53ffffffffffffffffff + BUCKET id=449a918e2f5d0eb9c33d54ffffffffffffffffff + BUCKET id=449a918e2f5d0eb9c33d553fffffffffffffffff + BUCKET id=449a918e2f5d0eb9c33d555fffffffffffffffff + BUCKET id=449a918e2f5d0eb9c33d556fffffffffffffffff + BUCKET id=449a918e2f5d0eb9c33d5573ffffffffffffffff + BUCKET id=449a918e2f5d0eb9c33d5575ffffffffffffffff + BUCKET id=449a918e2f5d0eb9c33d55767fffffffffffffff + BUCKET id=449a918e2f5d0eb9c33d5576bfffffffffffffff + BUCKET id=449a918e2f5d0eb9c33d5576cfffffffffffffff + BUCKET id=449a918e2f5d0eb9c33d5576d1ffffffffffffff + BUCKET id=449a918e2f5d0eb9c33d5576d2ffffffffffffff + BUCKET id=449a918e2f5d0eb9c33d5576d31fffffffffffff + BUCKET id=449a918e2f5d0eb9c33d5576d321ffffffffffff + BUCKET id=449a918e2f5d0eb9c33d5576d3227fffffffffff + BUCKET id=449a918e2f5d0eb9c33d5576d322bfffffffffff + BUCKET id=449a918e2f5d0eb9c33d5576d322cfffffffffff + BUCKET id=449a918e2f5d0eb9c33d5576d322d3ffffffffff + BUCKET id=449a918e2f5d0eb9c33d5576d322d5ffffffffff + 449a918e2f5d0eb9c33d5576d322d6050db58f35 ::ffff:184.72.202.243/6881 unans=0 good + 449a918e2f5d0eb9c33d5576d322d63a050f460b ::ffff:192.241.151.29/6882 unans=0 questionable + 449a918e2f5d0eb9c33d5576d322d63e82e28652 ::ffff:34.89.135.129/6881 unans=0 good + 449a918e2f5d0eb9c33d5576d322d652d63f0de1 ::ffff:85.10.202.14/6881 unans=0 questionable + 449a918e2f5d0eb9c33d5576d322d67444e8eb27 ::ffff:64.235.252.215/6881 unans=0 good + BUCKET id=449a918e2f5d0eb9c33d5576d322d67fffffffff + 449a918e2f5d0eb9c33d5576d322d688a2f0eaab ::ffff:192.241.151.29/6883 unans=0 questionable + 449a918e2f5d0eb9c33d5576d322d6c69af7ebe7 ::ffff:220.191.18.238/6881 unans=0 good + 449a918e2f5d0eb9c33d5576d322d6cdfede2698 ::ffff:65.108.201.176/56881 unans=0 good + 449a918e2f5d0eb9c33d5576d322d6cdfede5aa4 ::ffff:106.255.239.227/6688 unans=0 questionable + 449a918e2f5d0eb9c33d5576d322d6cdfede7776 ::ffff:123.173.71.216/6688 unans=0 good + 449a918e2f5d0eb9c33d5576d322d6cdfedeabdc ::ffff:52.214.147.108/6971 unans=0 good + 449a918e2f5d0eb9c33d5576d322d6cdfeded216 ::ffff:136.243.96.42/1688 unans=0 good + 449a918e2f5d0eb9c33d5576d322d6cfc5eba6c7 ::ffff:35.232.31.198/6881 unans=0 good + BUCKET id=449a918e2f5d0eb9c33d5576d322d6ffffffffff + BUCKET id=449a918e2f5d0eb9c33d5576d322d7ffffffffff + BUCKET id=449a918e2f5d0eb9c33d5576d322dfffffffffff + BUCKET id=449a918e2f5d0eb9c33d5576d322ffffffffffff + BUCKET id=449a918e2f5d0eb9c33d5576d323ffffffffffff + BUCKET id=449a918e2f5d0eb9c33d5576d327ffffffffffff + BUCKET id=449a918e2f5d0eb9c33d5576d32fffffffffffff + BUCKET id=449a918e2f5d0eb9c33d5576d33fffffffffffff + BUCKET id=449a918e2f5d0eb9c33d5576d37fffffffffffff + BUCKET id=449a918e2f5d0eb9c33d5576d3ffffffffffffff + BUCKET id=449a918e2f5d0eb9c33d5576d7ffffffffffffff + BUCKET id=449a918e2f5d0eb9c33d5576dfffffffffffffff + BUCKET id=449a918e2f5d0eb9c33d5576ffffffffffffffff + BUCKET id=449a918e2f5d0eb9c33d5577ffffffffffffffff + BUCKET id=449a918e2f5d0eb9c33d557fffffffffffffffff + BUCKET id=449a918e2f5d0eb9c33d55ffffffffffffffffff + BUCKET id=449a918e2f5d0eb9c33d57ffffffffffffffffff + BUCKET id=449a918e2f5d0eb9c33d5fffffffffffffffffff + BUCKET id=449a918e2f5d0eb9c33d7fffffffffffffffffff + 449a918e2f5d0eb9c33d9b1f68e7c9c199c75672 ::ffff:167.172.226.132/6060 unans=0 questionable + BUCKET id=449a918e2f5d0eb9c33dffffffffffffffffffff + BUCKET id=449a918e2f5d0eb9c33fffffffffffffffffffff + BUCKET id=449a918e2f5d0eb9c37fffffffffffffffffffff + BUCKET id=449a918e2f5d0eb9c3ffffffffffffffffffffff + BUCKET id=449a918e2f5d0eb9c7ffffffffffffffffffffff + BUCKET id=449a918e2f5d0eb9cfffffffffffffffffffffff + BUCKET id=449a918e2f5d0eb9dfffffffffffffffffffffff diff --git a/src/dht.c b/src/dht.c index 467b036..137c5d8 100644 --- a/src/dht.c +++ b/src/dht.c @@ -82,7 +82,7 @@ struct node { struct sockaddr_in6 addr; int unanswered; /**< number of packets I've sent since last_received */ time_t last_received; /**< time when I received the last packet from it */ - time_t last_sent; /**< time when I sent the last query to it */ + time_t last_sent; /**< time when I sent the last query to it. not incremented if it has unanswered queries. */ struct node * next; }; @@ -115,6 +115,46 @@ void node_free (struct node * n) { free(n); } +enum node_grade { + good, + bad, + questionable +}; + +/** + * @return a textual form of node_grade enum + */ + +char * node_grade_str (enum node_grade g) { + switch (g) { + case good: + return "good"; + case bad: + return "bad"; + case questionable: + return "questionable"; + } + return "unknown node_grade"; +} + +/** + * determines if node is considered good, bad or questionable + * + * @param n [in] node + * @return enum node_grade + */ + +#define QUESTIONABLE_AFTER (15*60) + +enum node_grade node_grade (const struct node * n) { + if (n->last_received + QUESTIONABLE_AFTER < seconds()) { + if (n->last_sent + 60 < seconds() && n->unanswered > 1) + return bad; + return questionable; + } + return good; +} + /** * print node to stream, for debugging * @@ -129,7 +169,7 @@ void node_print (FILE * s, const struct node * n) { char remote[INET6_ADDRSTRLEN + 64]; if (!inet_ntop(n->addr.sin6_family, n->addr.sin6_addr.s6_addr, remote, INET6_ADDRSTRLEN+7)) snprintf(remote, sizeof remote, "(inet_ntop: %s)", strerror(errno)); - fprintf(s, "%s %s/%d unans=%d", buf, remote, ntohs(n->addr.sin6_port), n->unanswered); + fprintf(s, "%s %s/%d unans=%d %s", buf, remote, ntohs(n->addr.sin6_port), n->unanswered, node_grade_str(node_grade(n))); } /** @@ -478,6 +518,10 @@ struct dht { unsigned char sample[60000]; /**< for sample infohashes */ int samples; /**< for sample infohashes, max 3000 */ #endif + unsigned p; /**< number of sent pings */ +#define PINGS_CAP 256 /**< capacity of circular buffer, one element is ~28 bytes, so this is 7168 B */ + struct sockaddr_in6 pings[PINGS_CAP]; /**< circular buffer of recent pings */ + unsigned periods; /**< number of times periodic() was called */ }; /** @@ -494,7 +538,7 @@ void dht_print (FILE * s, const struct dht * d) { char secret[17*2]; secret[17*2+1] = '\0'; bin2hex(secret, d->secret, 16); - fprintf(s, "id=%s socket=%d t=%u p=%u tmax=%u pmax=%u p/t-max=%u runsec=%ld rxp=%u txp=%u rxb=%u txb=%u secret=%s tt=%u tr=%u\n", buf, d->socket, d->torrents_num, d->peers_num, d->torrents_max, d->peers_max, d->peers_per_torrent_max, seconds()-d->time, d->rxp, d->txp, d->rxb, d->txb, secret, d->tt, d->tr); + fprintf(s, "id=%s socket=%d t=%u p=%u tmax=%u pmax=%u p/t-max=%u runsec=%ld rxp=%u txp=%u rxb=%u txb=%u secret=%s tt=%u tr=%u p=%u\n", buf, d->socket, d->torrents_num, d->peers_num, d->torrents_max, d->peers_max, d->peers_per_torrent_max, seconds()-d->time, d->rxp, d->txp, d->rxb, d->txb, secret, d->tt, d->tr, d->p); fprintf(s, "**** NODES ****\n"); int nodes = 0; for (int i = 0; i <= 1; i++) { @@ -516,7 +560,7 @@ void dht_print (FILE * s, const struct dht * d) { } b = b->next; } - fprintf(s, "\t**** COUNT OF %s BUCKETS: %d\n", i ? "IPv4" : "IPV6", buckets); + fprintf(s, "\t**** COUNT OF %s BUCKETS: %d\n", i ? "IPv6" : "IPv4", buckets); } fprintf(s, "**** COUNT OF NODES: %d\n", nodes); printf("**** TORRENTS ****\n"); @@ -644,6 +688,8 @@ void find_node (struct dht * d, const struct sockaddr_in6 * addr, const unsigned * * instead of sending a ping query, we send a find_node query. this gets us useful information of peers around our ID instead of just a blank ping reply. infolgedessen we don't have to actively search for our neighbour nodes, since we'll get them through pings anyways * + * does not ping if the same node was pinged in the last PINGS_CAP pings + * * DEV THOUGHT: instead of sending a find_node for an ID close to ours, we could send a find_node for a random ID far from us. though those buckets will probably quickly be filled by torrent searches. * * @param d [in] library handle @@ -651,6 +697,11 @@ void find_node (struct dht * d, const struct sockaddr_in6 * addr, const unsigned */ void ping_node (struct dht * d, const struct sockaddr_in6 * a) { + for (int i = 0; i < PINGS_CAP; i++) + if (!memcmp(a, &d->pings[i], sizeof *a)) { + L(debug, d, "already pinged in last " STR(PINGS_CAP) " pings, ignoring request"); + return; + } unsigned char target[20]; memcpy(target, d->id, 20); if (target[19] & 1) // flip the last bit, so the other node doesn't just return @@ -1077,28 +1128,6 @@ unsigned int distance (const unsigned char * a, const unsigned char * b) { return r; } -enum node_grade { - good, - bad, - questionable -}; - -/** - * determines if node is considered good, bad or questionable - * - * @param n [in] node - * @return enum node_grade - */ - -enum node_grade node_grade (const struct node * n) { - if (n->last_received + 15*60 < seconds()) { - if (n->last_sent + 14*60 < seconds() && n->unanswered > 1) - return bad; - return questionable; - } - return good; -} - /** * returns 1 if bucket is perfect, meaning it is fresh, has K nodes, and all nodes are good. bucket that contains id is almost never perfect, as it can usually be split into smaller buckets, that's why param d is required to get own id * @@ -1167,21 +1196,44 @@ void replied (const struct dht * d, const unsigned char * id, const struct socka struct node * node = node_init(); memcpy(&node->addr, addr, sizeof *addr); memcpy(node->id, id, 20); - if (!n) { - node->next = b->nodes; - b->nodes = node; - return; - } if (node_count(b->nodes) < K) { - struct node * index = b->nodes; - while (index->next && memcmp(node->id, index->next->id, 20) > 1) - index = index->next; - node->next = index->next; - index->next = node; + if (!n) { + node->next = b->nodes; + b->nodes = node; + } else { + node->next = n->next; + n->next = node; + } return; } node_free(node); if (in_bucket(d->id, b)) { + struct bucket * bucket = d->buckets; + if (family(addr->sin6_addr.s6_addr) == AF_INET6) + bucket = d->buckets6; + while (bucket) { + struct node * n = bucket->nodes; + while (n) { + char remote[INET_ADDRSTRLEN + INET6_ADDRSTRLEN + 64]; + if (!inet_ntop(AF_INET6, addr->sin6_addr.s6_addr, remote, INET6_ADDRSTRLEN+7+INET_ADDRSTRLEN)) + snprintf(remote, sizeof remote, "(inet_ntop: %s)", strerror(errno)); + switch(family(addr->sin6_addr.s6_addr)) { + case AF_INET: + if (!memcmp(addr->sin6_addr.s6_addr+12, n->addr.sin6_addr.s6_addr+12, 4)) { + L(disagreement, d, "sybil: %s/%d is already present", remote, ntohs(addr->sin6_port)); + return; + } + break; + case AF_INET6: + if (!memcmp(addr->sin6_addr.s6_addr+1, n->addr.sin6_addr.s6_addr+1, 5)) { + L(disagreement, d, "sybil: %s/%d is already present", remote, ntohs(addr->sin6_port)); + } + break; + } + n = n->next; + } + bucket = bucket->next; + } split(b); replied(d, id, addr); // find bucket again } @@ -1936,10 +1988,10 @@ void handle (struct dht * d, char * pkt, int len, struct sockaddr_in6 addr) { struct bencoding * nodes = bpath(b, "r/nodes"); struct bencoding * nodes6 = bpath(b, "r/nodes6"); if (nodes && nodes->type & string && !(nodes->valuelen % 26)) - for (unsigned i = 0; i < nodes->valuelen; i += 26) + for (unsigned i = 0; i < MIN(nodes->valuelen, K); i += 26) compact(d, nodes->value+i, 26, torrent); if (nodes6 && nodes6->type & string && !(nodes6->valuelen % 38)) - for (unsigned i = 0; i < nodes6->valuelen; i += 38) + for (unsigned i = 0; i < MIN(nodes6->valuelen, K); i += 38) compact(d, nodes6->value+i, 38, torrent); break; case 'E': @@ -2068,16 +2120,24 @@ d: } // do not log, it may have been a bencoded reply } +#define PERIODIC 10 + /** - * do periodic housekeeping on the routing table LL, making sure no nodes are bad. removes bad nodes and does not ping questionable nodes. see NOTE03 + * do periodic housekeeping on the routing table LL, making sure no nodes are bad. removes bad nodes. detects sybil. see NOTE03 * - * @param b [in] first bucket in LL - basically either d->buckets or d->buckets6 + * @param d [in] library handle + * @param fam [in] AF_INET for buckets and AF_INET6 for buckets6 * @return number of good nodes */ -int refresh (struct bucket * b) { +int refresh (struct dht * d, int fam) { int nrgood = 0; + int buckets = 0; + struct bucket * b = d->buckets; + if (fam == AF_INET6) + b = d->buckets6; while (b) { + buckets++; struct node ** n = &b->nodes; while (*n) { switch (node_grade(*n)) { @@ -2088,8 +2148,9 @@ int refresh (struct bucket * b) { node_free(old); continue; case questionable: - // ping_node(d, *n); // NOTE03 about not pinging questionable nodes: this ensures a constant regeneration of the routing table. this is just an idea, if the client frequently gets in a situation without any nodes in the routing table, remove the comment before ping_node call. - break; + if (!(rand() % (QUESTIONABLE_AFTER/PERIODIC))) + ping_node(d, &(*n)->addr); // NOTE03 about not pinging questionable nodes: this ensures a constant regeneration of the routing table. this is just an idea, if the client frequently gets in a situation without any nodes in the routing table, remove the comment before ping_node call. + break; // update on why I uncommented: to mitigate sybil attack, it's baje important to prefer old nodes case good: nrgood++; break; @@ -2098,11 +2159,40 @@ int refresh (struct bucket * b) { } b = b->next; } + if (buckets > 32) { // sybil attack - node is broken - clear whole routing table, keeping one bucket + L(disagreement, d, "@@@@@@ SYBIL ATTACK - CLEARING ROUTING TABLE @@@@@@"); + int keep_first = rand() % 2; // should we even keep one bucket? the sybil node has a 1/2 + if (keep_first) { // chance of having stared in the bucket farthest away, so it's stored there ... + memset(d->buckets->id, '\0', 20); + b = d->buckets->next; + d->buckets->next = NULL; + while (b) { + bucket_free(b); + b = b->next; + } + } else { + b = d->buckets; + while (b->next) { + struct bucket * old = b; + b = b->next; + bucket_free(old); + } + d->buckets = b; + memset(d->buckets->id, '\0', 20); + } + if (getrandom(d->id, 20, GRND_NONBLOCK) == -1) // changing our ID. note that this might make + L(std_fail, d, "getrandom: %s", strerror(errno)); // existing nodes hate us + switch (fam) { + case AF_INET: + return node_count(d->buckets6->nodes); + case AF_INET6: + return node_count(d->buckets6->nodes); + } + return 0; + } return nrgood; } -#define PERIODIC 10 - /** * does periodic work for the library * @@ -2124,11 +2214,12 @@ int refresh (struct bucket * b) { */ void periodic (struct dht * d) { + d->periods++; L(debug, d, "called"); int dns = 0; - if (!refresh(d->buckets)) + if (!refresh(d, AF_INET)) dns++; - if (!refresh(d->buckets6)) + if (!refresh(d, AF_INET6)) dns++; if (dns) { char packet[512]; @@ -2187,12 +2278,71 @@ void periodic (struct dht * d) { struct torrent * t = d->torrents; while (t) { if (t->type & (peers | announce)) { + /* + struct node * n = t->nodes; + int c = node_count(n); + if (!c) { +#define RTGP(buckets) {struct bucket * b = d->buckets; \ + find(t->hash, &b, NULL); \ + struct node * n = b->nodes; \ + c = node_count(n); \ + if (c) { \ + c = rand() % c; \ + while (c--) \ + n = n->next; \ + if (!n->unanswered) \ + n->last_sent = seconds(); \ + n->unanswered++; \ + get_peers(d, &n->addr, t->hash); \ + } else { \ + struct bucket * b = d->buckets; \ + c = 0; \ + while (b) { \ + c += node_count(b->nodes); \ + b = b->next; \ + } \ + if (c) { \ + c = rand() % c; \ + b = d->buckets; \ + int i = 0; \ + while (b) { \ + struct node * n = b->nodes; \ + while (n) { \ + if (i++ == c) { \ + i = -1; \ + if (!n->unanswered) \ + n->last_sent = seconds(); \ + n->unanswered++; \ + get_peers(d, &n->addr, t->hash); \ + break; \ + } \ + n = n->nodes; \ + } \ + if (i == -1) \ + break; \ + b = b->next; \ + } \ + } \ + } + RTGP(buckets); + RTGP(buckets6); + } else { + c = rand() % c; + while (c--) + n = n->next; + if (!n->unanswered) + n->last_sent = seconds(); + n->unanswered++; + get_peers(d, &n->addr, t->hash); + } + */ struct node * n = t->nodes; int sent = 0; while (n) { sent++; + if (!n->unanswered) + n->last_sent = seconds(); n->unanswered++; - n->last_sent = seconds(); get_peers(d, &n->addr, t->hash); n = n->next; } @@ -2202,8 +2352,9 @@ void periodic (struct dht * d) { struct node * n = b->nodes; \ while (sent < K && n) { \ sent++; \ + if (!n->unanswered) \ + n->last_sent = seconds(); \ n->unanswered++; \ - n->last_sent = seconds(); \ get_peers(d, &n->addr, t->hash); \ n = n->next; \ }} @@ -2216,8 +2367,9 @@ void periodic (struct dht * d) { n = b->nodes; while (sent < K && n) { sent++; + if (!n->unanswered) + n->last_sent = seconds(); n->unanswered++; - n->last_sent = seconds(); get_peers(d, &n->addr, t->hash); n = n->next; } diff --git a/utils/midpoint.c b/utils/midpoint.c index c736143..60ad65c 100644 --- a/utils/midpoint.c +++ b/utils/midpoint.c @@ -4,7 +4,7 @@ int main (int argc, char ** argv) { if (argc < 3) - error_at_line(1, 0, __FILE__, __LINE__, "%s [b]", S0(argv[0])); + error_at_line(1, 0, __FILE__, __LINE__, "%s [b] [t]", S0(argv[0])); if (argv[1][0] == 'b' || argv[1][0] == 'B') { unsigned char a[strlen(argv[2])/2+1]; a[strlen(argv[2])/2] = '\0'; @@ -38,6 +38,13 @@ int main (int argc, char ** argv) { char out[41]; out[40] = '\0'; unsigned char r[20]; + if (argv[1][0] == 'c' || argv[1][0] == 'C') { + if (!argv[4] || strlen(argv[4]) != 40) + error_at_line(4, 0, __FILE__, __LINE__, "(!t || strlen(t) != 40) && closer"); + unsigned char t[20]; + hex2bin(t, argv[4], 20); + return closer(a, b, t); + } if (argv[1][0] == 'a' || argv[1][0] == 'A') { memcpy(r, a, 20); add(r, b); diff --git a/www/index.php b/www/index.php index 2fd5a92..bc55638 100644 --- a/www/index.php +++ b/www/index.php @@ -21,6 +21,7 @@ if (!preg_match("/^[a-f0-9A-F]{40}$/", $_REQUEST["h"])) die('!preg_match("/^[a-f0-9A-F]{40}$/", $_REQUEST["h"])'); $t = TorrentFile::load("../".$_REQUEST["h"].".torrent"); ?> +