qBittorrent
filterparserthread.cpp
Go to the documentation of this file.
1 /*
2  * Bittorrent Client using Qt and libtorrent.
3  * Copyright (C) 2006 Christophe Dumez <[email protected]>
4  *
5  * This program is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU General Public License
7  * as published by the Free Software Foundation; either version 2
8  * of the License, or (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18  *
19  * In addition, as a special exception, the copyright holders give permission to
20  * link this program with the OpenSSL project's "OpenSSL" library (or with
21  * modified versions of it that use the same license as the "OpenSSL" library),
22  * and distribute the linked executables. You must obey the GNU General Public
23  * License in all respects for all of the code used other than "OpenSSL". If you
24  * modify file(s), you may extend this exception to your version of the file(s),
25  * but you are not obligated to do so. If you do not wish to do so, delete this
26  * exception statement from your version.
27  */
28 
29 #include "filterparserthread.h"
30 
31 #include <cctype>
32 
33 #include <libtorrent/error_code.hpp>
34 
35 #include <QDataStream>
36 #include <QFile>
37 
38 #include "base/logger.h"
39 
40 namespace
41 {
42  class IPv4Parser
43  {
44  public:
45  bool tryParse(const char *str)
46  {
47  unsigned char octetIndex = 0;
48 
49  const char *octetStart = str;
50  char *endptr;
51  for (; *str; ++str)
52  {
53  if (*str == '.')
54  {
55  const long int extractedNum = strtol(octetStart, &endptr, 10);
56  if ((extractedNum >= 0L) && (extractedNum <= 255L))
57  m_buf[octetIndex++] = static_cast<unsigned char>(extractedNum);
58  else
59  return false;
60 
61  if (endptr != str)
62  return false;
63  if (octetIndex == 4)
64  return true;
65 
66  octetStart = str + 1;
67  }
68  }
69 
70  if (str != octetStart)
71  {
72  const long int extractedNum = strtol(octetStart, &endptr, 10);
73  if ((extractedNum >= 0L) && (extractedNum <= 255L))
74  m_buf[octetIndex] = static_cast<unsigned char>(strtol(octetStart, &endptr, 10));
75  else
76  return false;
77 
78  if ((endptr == str) && (octetIndex == 3))
79  return true;
80  }
81 
82  return false;
83  }
84 
85  lt::address_v4::bytes_type parsed() const
86  {
87  return m_buf;
88  }
89 
90  private:
91  lt::address_v4::bytes_type m_buf;
92  };
93 
94  bool parseIPAddress(const char *data, lt::address &address)
95  {
96  IPv4Parser parser;
97  lt::error_code ec;
98 
99  if (parser.tryParse(data))
100  address = lt::address_v4(parser.parsed());
101  else
102  address = lt::make_address(data, ec);
103 
104  return !ec;
105  }
106 
107  const int BUFFER_SIZE = 2 * 1024 * 1024; // 2 MiB
108  const int MAX_LOGGED_ERRORS = 5;
109 }
110 
112  : QThread(parent)
113  , m_abort(false)
114 {
115 }
116 
118 {
119  m_abort = true;
120  wait();
121 }
122 
123 // Parser for eMule ip filter in DAT format
125 {
126  int ruleCount = 0;
127  QFile file(m_filePath);
128  if (!file.exists()) return ruleCount;
129 
130  if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
131  {
132  LogMsg(tr("I/O Error: Could not open IP filter file in read mode."), Log::CRITICAL);
133  return ruleCount;
134  }
135 
136  std::vector<char> buffer(BUFFER_SIZE, 0); // seems a bit faster than QVector
137  qint64 bytesRead = 0;
138  int offset = 0;
139  int start = 0;
140  int endOfLine = -1;
141  int nbLine = 0;
142  int parseErrorCount = 0;
143  const auto addLog = [&parseErrorCount](const QString &msg)
144  {
145  if (parseErrorCount <= MAX_LOGGED_ERRORS)
146  LogMsg(msg, Log::CRITICAL);
147  };
148 
149  while (true)
150  {
151  bytesRead = file.read(buffer.data() + offset, BUFFER_SIZE - offset - 1);
152  if (bytesRead < 0)
153  break;
154  const int dataSize = bytesRead + offset;
155  if ((bytesRead == 0) && (dataSize == 0))
156  break;
157 
158  for (start = 0; start < dataSize; ++start)
159  {
160  endOfLine = -1;
161  // The file might have ended without the last line having a newline
162  if (!((bytesRead == 0) && (dataSize > 0)))
163  {
164  for (int i = start; i < dataSize; ++i)
165  {
166  if (buffer[i] == '\n')
167  {
168  endOfLine = i;
169  // We need to NULL the newline in case the line has only an IP range.
170  // In that case the parser won't work for the end IP, because it ends
171  // with the newline and not with a number.
172  buffer[i] = '\0';
173  break;
174  }
175  }
176  }
177  else
178  {
179  endOfLine = dataSize;
180  buffer[dataSize] = '\0';
181  }
182 
183  if (endOfLine == -1)
184  {
185  // read the next chunk from file
186  // but first move(copy) the leftover data to the front of the buffer
187  offset = dataSize - start;
188  memmove(buffer.data(), buffer.data() + start, offset);
189  break;
190  }
191 
192  ++nbLine;
193 
194  if ((buffer[start] == '#')
195  || ((buffer[start] == '/') && ((start + 1 < dataSize) && (buffer[start + 1] == '/'))))
196  {
197  start = endOfLine;
198  continue;
199  }
200 
201  // Each line should follow this format:
202  // 001.009.096.105 - 001.009.096.105 , 000 , Some organization
203  // The 3rd entry is access level and if above 127 the IP range isn't blocked.
204  const int firstComma = findAndNullDelimiter(buffer.data(), ',', start, endOfLine);
205  if (firstComma != -1)
206  findAndNullDelimiter(buffer.data(), ',', firstComma + 1, endOfLine);
207 
208  // Check if there is an access value (apparently not mandatory)
209  if (firstComma != -1)
210  {
211  // There is possibly one
212  const long int nbAccess = strtol(buffer.data() + firstComma + 1, nullptr, 10);
213  // Ignoring this rule because access value is too high
214  if (nbAccess > 127L)
215  {
216  start = endOfLine;
217  continue;
218  }
219  }
220 
221  // IP Range should be split by a dash
222  const int endOfIPRange = ((firstComma == -1) ? (endOfLine - 1) : (firstComma - 1));
223  const int delimIP = findAndNullDelimiter(buffer.data(), '-', start, endOfIPRange);
224  if (delimIP == -1)
225  {
226  ++parseErrorCount;
227  addLog(tr("IP filter line %1 is malformed.").arg(nbLine));
228  start = endOfLine;
229  continue;
230  }
231 
232  lt::address startAddr;
233  int newStart = trim(buffer.data(), start, delimIP - 1);
234  if (!parseIPAddress(buffer.data() + newStart, startAddr))
235  {
236  ++parseErrorCount;
237  addLog(tr("IP filter line %1 is malformed. Start IP of the range is malformed.").arg(nbLine));
238  start = endOfLine;
239  continue;
240  }
241 
242  lt::address endAddr;
243  newStart = trim(buffer.data(), delimIP + 1, endOfIPRange);
244  if (!parseIPAddress(buffer.data() + newStart, endAddr))
245  {
246  ++parseErrorCount;
247  addLog(tr("IP filter line %1 is malformed. End IP of the range is malformed.").arg(nbLine));
248  start = endOfLine;
249  continue;
250  }
251 
252  if ((startAddr.is_v4() != endAddr.is_v4())
253  || (startAddr.is_v6() != endAddr.is_v6()))
254  {
255  ++parseErrorCount;
256  addLog(tr("IP filter line %1 is malformed. One IP is IPv4 and the other is IPv6!").arg(nbLine));
257  start = endOfLine;
258  continue;
259  }
260 
261  start = endOfLine;
262 
263  // Now Add to the filter
264  try
265  {
266  m_filter.add_rule(startAddr, endAddr, lt::ip_filter::blocked);
267  ++ruleCount;
268  }
269  catch (const std::exception &e)
270  {
271  ++parseErrorCount;
272  addLog(tr("IP filter exception thrown for line %1. Exception is: %2")
273  .arg(nbLine).arg(QString::fromLocal8Bit(e.what())));
274  }
275  }
276 
277  if (start >= dataSize)
278  offset = 0;
279  }
280 
281  if (parseErrorCount > MAX_LOGGED_ERRORS)
282  LogMsg(tr("%1 extra IP filter parsing errors occurred.", "513 extra IP filter parsing errors occurred.")
283  .arg(parseErrorCount - MAX_LOGGED_ERRORS), Log::CRITICAL);
284  return ruleCount;
285 }
286 
287 // Parser for PeerGuardian ip filter in p2p format
289 {
290  int ruleCount = 0;
291  QFile file(m_filePath);
292  if (!file.exists()) return ruleCount;
293 
294  if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
295  {
296  LogMsg(tr("I/O Error: Could not open IP filter file in read mode."), Log::CRITICAL);
297  return ruleCount;
298  }
299 
300  std::vector<char> buffer(BUFFER_SIZE, 0); // seems a bit faster than QVector
301  qint64 bytesRead = 0;
302  int offset = 0;
303  int start = 0;
304  int endOfLine = -1;
305  int nbLine = 0;
306  int parseErrorCount = 0;
307  const auto addLog = [&parseErrorCount](const QString &msg)
308  {
309  if (parseErrorCount <= MAX_LOGGED_ERRORS)
310  LogMsg(msg, Log::CRITICAL);
311  };
312 
313  while (true)
314  {
315  bytesRead = file.read(buffer.data() + offset, BUFFER_SIZE - offset - 1);
316  if (bytesRead < 0)
317  break;
318  const int dataSize = bytesRead + offset;
319  if ((bytesRead == 0) && (dataSize == 0))
320  break;
321 
322  for (start = 0; start < dataSize; ++start)
323  {
324  endOfLine = -1;
325  // The file might have ended without the last line having a newline
326  if (!((bytesRead == 0) && (dataSize > 0)))
327  {
328  for (int i = start; i < dataSize; ++i)
329  {
330  if (buffer[i] == '\n')
331  {
332  endOfLine = i;
333  // We need to NULL the newline in case the line has only an IP range.
334  // In that case the parser won't work for the end IP, because it ends
335  // with the newline and not with a number.
336  buffer[i] = '\0';
337  break;
338  }
339  }
340  }
341  else
342  {
343  endOfLine = dataSize;
344  buffer[dataSize] = '\0';
345  }
346 
347  if (endOfLine == -1)
348  {
349  // read the next chunk from file
350  // but first move(copy) the leftover data to the front of the buffer
351  offset = dataSize - start;
352  memmove(buffer.data(), buffer.data() + start, offset);
353  break;
354  }
355 
356  ++nbLine;
357 
358  if ((buffer[start] == '#')
359  || ((buffer[start] == '/') && ((start + 1 < dataSize) && (buffer[start + 1] == '/'))))
360  {
361  start = endOfLine;
362  continue;
363  }
364 
365  // Each line should follow this format:
366  // Some organization:1.0.0.0-1.255.255.255
367  // The "Some organization" part might contain a ':' char itself so we find the last occurrence
368  const int partsDelimiter = findAndNullDelimiter(buffer.data(), ':', start, endOfLine, true);
369  if (partsDelimiter == -1)
370  {
371  ++parseErrorCount;
372  addLog(tr("IP filter line %1 is malformed.").arg(nbLine));
373  start = endOfLine;
374  continue;
375  }
376 
377  // IP Range should be split by a dash
378  const int delimIP = findAndNullDelimiter(buffer.data(), '-', partsDelimiter + 1, endOfLine);
379  if (delimIP == -1)
380  {
381  ++parseErrorCount;
382  addLog(tr("IP filter line %1 is malformed.").arg(nbLine));
383  start = endOfLine;
384  continue;
385  }
386 
387  lt::address startAddr;
388  int newStart = trim(buffer.data(), partsDelimiter + 1, delimIP - 1);
389  if (!parseIPAddress(buffer.data() + newStart, startAddr))
390  {
391  ++parseErrorCount;
392  addLog(tr("IP filter line %1 is malformed. Start IP of the range is malformed.").arg(nbLine));
393  start = endOfLine;
394  continue;
395  }
396 
397  lt::address endAddr;
398  newStart = trim(buffer.data(), delimIP + 1, endOfLine);
399  if (!parseIPAddress(buffer.data() + newStart, endAddr))
400  {
401  ++parseErrorCount;
402  addLog(tr("IP filter line %1 is malformed. End IP of the range is malformed.").arg(nbLine));
403  start = endOfLine;
404  continue;
405  }
406 
407  if ((startAddr.is_v4() != endAddr.is_v4())
408  || (startAddr.is_v6() != endAddr.is_v6()))
409  {
410  ++parseErrorCount;
411  addLog(tr("IP filter line %1 is malformed. One IP is IPv4 and the other is IPv6!").arg(nbLine));
412  start = endOfLine;
413  continue;
414  }
415 
416  start = endOfLine;
417 
418  try
419  {
420  m_filter.add_rule(startAddr, endAddr, lt::ip_filter::blocked);
421  ++ruleCount;
422  }
423  catch (const std::exception &e)
424  {
425  ++parseErrorCount;
426  addLog(tr("IP filter exception thrown for line %1. Exception is: %2")
427  .arg(nbLine).arg(QString::fromLocal8Bit(e.what())));
428  }
429  }
430 
431  if (start >= dataSize)
432  offset = 0;
433  }
434 
435  if (parseErrorCount > MAX_LOGGED_ERRORS)
436  LogMsg(tr("%1 extra IP filter parsing errors occurred.", "513 extra IP filter parsing errors occurred.")
437  .arg(parseErrorCount - MAX_LOGGED_ERRORS), Log::CRITICAL);
438  return ruleCount;
439 }
440 
441 int FilterParserThread::getlineInStream(QDataStream &stream, std::string &name, const char delim)
442 {
443  char c;
444  int totalRead = 0;
445  int read;
446  do
447  {
448  read = stream.readRawData(&c, 1);
449  totalRead += read;
450  if (read > 0)
451  {
452  if (c != delim)
453  {
454  name += c;
455  }
456  else
457  {
458  // Delim found
459  return totalRead;
460  }
461  }
462  }
463  while (read > 0);
464 
465  return totalRead;
466 }
467 
468 // Parser for PeerGuardian ip filter in p2p format
470 {
471  int ruleCount = 0;
472  QFile file(m_filePath);
473  if (!file.exists()) return ruleCount;
474 
475  if (!file.open(QIODevice::ReadOnly))
476  {
477  LogMsg(tr("I/O Error: Could not open IP filter file in read mode."), Log::CRITICAL);
478  return ruleCount;
479  }
480 
481  QDataStream stream(&file);
482  // Read header
483  char buf[7];
484  unsigned char version;
485  if (!stream.readRawData(buf, sizeof(buf))
486  || memcmp(buf, "\xFF\xFF\xFF\xFFP2B", 7)
487  || !stream.readRawData(reinterpret_cast<char*>(&version), sizeof(version)))
488  {
489  LogMsg(tr("Parsing Error: The filter file is not a valid PeerGuardian P2B file."), Log::CRITICAL);
490  return ruleCount;
491  }
492 
493  if ((version == 1) || (version == 2))
494  {
495  qDebug ("p2b version 1 or 2");
496  unsigned int start, end;
497 
498  std::string name;
499  while (getlineInStream(stream, name, '\0') && !m_abort)
500  {
501  if (!stream.readRawData(reinterpret_cast<char*>(&start), sizeof(start))
502  || !stream.readRawData(reinterpret_cast<char*>(&end), sizeof(end)))
503  {
504  LogMsg(tr("Parsing Error: The filter file is not a valid PeerGuardian P2B file."), Log::CRITICAL);
505  return ruleCount;
506  }
507 
508  // Network byte order to Host byte order
509  // asio address_v4 constructor expects it
510  // that way
511  const lt::address_v4 first(ntohl(start));
512  const lt::address_v4 last(ntohl(end));
513  // Apply to bittorrent session
514  try
515  {
516  m_filter.add_rule(first, last, lt::ip_filter::blocked);
517  ++ruleCount;
518  }
519  catch (const std::exception &) {}
520  }
521  }
522  else if (version == 3)
523  {
524  qDebug ("p2b version 3");
525  unsigned int namecount;
526  if (!stream.readRawData(reinterpret_cast<char*>(&namecount), sizeof(namecount)))
527  {
528  LogMsg(tr("Parsing Error: The filter file is not a valid PeerGuardian P2B file."), Log::CRITICAL);
529  return ruleCount;
530  }
531 
532  namecount = ntohl(namecount);
533  // Reading names although, we don't really care about them
534  for (unsigned int i = 0; i < namecount; ++i)
535  {
536  std::string name;
537  if (!getlineInStream(stream, name, '\0'))
538  {
539  LogMsg(tr("Parsing Error: The filter file is not a valid PeerGuardian P2B file."), Log::CRITICAL);
540  return ruleCount;
541  }
542 
543  if (m_abort) return ruleCount;
544  }
545 
546  // Reading the ranges
547  unsigned int rangecount;
548  if (!stream.readRawData(reinterpret_cast<char*>(&rangecount), sizeof(rangecount)))
549  {
550  LogMsg(tr("Parsing Error: The filter file is not a valid PeerGuardian P2B file."), Log::CRITICAL);
551  return ruleCount;
552  }
553 
554  rangecount = ntohl(rangecount);
555  unsigned int name, start, end;
556  for (unsigned int i = 0; i < rangecount; ++i)
557  {
558  if (!stream.readRawData(reinterpret_cast<char*>(&name), sizeof(name))
559  || !stream.readRawData(reinterpret_cast<char*>(&start), sizeof(start))
560  || !stream.readRawData(reinterpret_cast<char*>(&end), sizeof(end)))
561  {
562  LogMsg(tr("Parsing Error: The filter file is not a valid PeerGuardian P2B file."), Log::CRITICAL);
563  return ruleCount;
564  }
565 
566  // Network byte order to Host byte order
567  // asio address_v4 constructor expects it
568  // that way
569  const lt::address_v4 first(ntohl(start));
570  const lt::address_v4 last(ntohl(end));
571  // Apply to bittorrent session
572  try
573  {
574  m_filter.add_rule(first, last, lt::ip_filter::blocked);
575  ++ruleCount;
576  }
577  catch (const std::exception &) {}
578 
579  if (m_abort) return ruleCount;
580  }
581  }
582  else
583  {
584  LogMsg(tr("Parsing Error: The filter file is not a valid PeerGuardian P2B file."), Log::CRITICAL);
585  }
586 
587  return ruleCount;
588 }
589 
590 // Process ip filter file
591 // Supported formats:
592 // * eMule IP list (DAT): http://wiki.phoenixlabs.org/wiki/DAT_Format
593 // * PeerGuardian Text (P2P): http://wiki.phoenixlabs.org/wiki/P2P_Format
594 // * PeerGuardian Binary (P2B): http://wiki.phoenixlabs.org/wiki/P2B_Format
595 void FilterParserThread::processFilterFile(const QString &filePath)
596 {
597  if (isRunning())
598  {
599  // Already parsing a filter, m_abort first
600  m_abort = true;
601  wait();
602  }
603 
604  m_abort = false;
605  m_filePath = filePath;
606  m_filter = lt::ip_filter();
607  // Run it
608  start();
609 }
610 
612 {
613  return m_filter;
614 }
615 
617 {
618  qDebug("Processing filter file");
619  int ruleCount = 0;
620  if (m_filePath.endsWith(".p2p", Qt::CaseInsensitive))
621  {
622  // PeerGuardian p2p file
623  ruleCount = parseP2PFilterFile();
624  }
625  else if (m_filePath.endsWith(".p2b", Qt::CaseInsensitive))
626  {
627  // PeerGuardian p2b file
628  ruleCount = parseP2BFilterFile();
629  }
630  else if (m_filePath.endsWith(".dat", Qt::CaseInsensitive))
631  {
632  // eMule DAT format
633  ruleCount = parseDATFilterFile();
634  }
635 
636  if (m_abort) return;
637 
638  try
639  {
640  emit IPFilterParsed(ruleCount);
641  }
642  catch (const std::exception &)
643  {
644  emit IPFilterError();
645  }
646 
647  qDebug("IP Filter thread: finished parsing, filter applied");
648 }
649 
650 int FilterParserThread::findAndNullDelimiter(char *const data, const char delimiter, const int start, const int end, const bool reverse)
651 {
652  if (!reverse)
653  {
654  for (int i = start; i <= end; ++i)
655  {
656  if (data[i] == delimiter)
657  {
658  data[i] = '\0';
659  return i;
660  }
661  }
662  }
663  else
664  {
665  for (int i = end; i >= start; --i)
666  {
667  if (data[i] == delimiter)
668  {
669  data[i] = '\0';
670  return i;
671  }
672  }
673  }
674 
675  return -1;
676 }
677 
678 int FilterParserThread::trim(char *const data, const int start, const int end)
679 {
680  if (start >= end) return start;
681  int newStart = start;
682 
683  for (int i = start; i <= end; ++i)
684  {
685  if (isspace(data[i]) != 0)
686  {
687  data[i] = '\0';
688  }
689  else
690  {
691  newStart = i;
692  break;
693  }
694  }
695 
696  for (int i = end; i >= start; --i)
697  {
698  if (isspace(data[i]) != 0)
699  data[i] = '\0';
700  else
701  break;
702  }
703 
704  return newStart;
705 }
void processFilterFile(const QString &filePath)
lt::ip_filter IPfilter()
FilterParserThread(QObject *parent=nullptr)
lt::ip_filter m_filter
int findAndNullDelimiter(char *const data, char delimiter, int start, int end, bool reverse=false)
void IPFilterParsed(int ruleCount)
int trim(char *const data, int start, int end)
int getlineInStream(QDataStream &stream, std::string &name, char delim)
void LogMsg(const QString &message, const Log::MsgType &type)
Definition: logger.cpp:125
@ CRITICAL
Definition: logger.h:48
bool parseIPAddress(const char *data, lt::address &address)
file(GLOB QBT_TS_FILES "${qBittorrent_SOURCE_DIR}/src/lang/*.ts") set_source_files_properties($
Definition: CMakeLists.txt:5