From 2c2c1d008a3a30f421e14698d76f388f376598a0 Mon Sep 17 00:00:00 2001 From: Vangelis Banos Date: Sat, 21 Jul 2018 11:20:49 +0000 Subject: [PATCH 01/19] New --blackout-period option to skip writing redundant revisits to WARC Add option `--blackout-period` (default=0) When set and if the record is a duplicate (revisit record), check the datetime of `dedup_info` and its inside the `blackout_period`, skip writing the record to WARC. Add some unit tests. This is an improved implementation based on @nlevitt comments here: https://github.com/internetarchive/warcprox/pull/92 --- tests/test_writer.py | 40 ++++++++++++++++++++++++++++++++++++++-- warcprox/main.py | 3 +++ warcprox/writerthread.py | 27 ++++++++++++++++++++++++++- 3 files changed, 67 insertions(+), 3 deletions(-) diff --git a/tests/test_writer.py b/tests/test_writer.py index 126932a..2675393 100644 --- a/tests/test_writer.py +++ b/tests/test_writer.py @@ -22,7 +22,7 @@ USA. import os import fcntl from multiprocessing import Process, Queue -from datetime import datetime +from datetime import datetime, timedelta import pytest import re from warcprox.mitmproxy import ProxyingRecorder @@ -129,7 +129,7 @@ def test_special_dont_write_prefix(): wwt.join() wwt = warcprox.writerthread.WarcWriterProcessor( - Options(writer_threads=1)) + Options(writer_threads=1, blackout_period=60, prefix='foo')) wwt.inq = warcprox.TimestampedQueue(maxsize=1) wwt.outq = warcprox.TimestampedQueue(maxsize=1) try: @@ -158,6 +158,42 @@ def test_special_dont_write_prefix(): recorded_url = wwt.outq.get(timeout=10) assert not recorded_url.warc_records assert wwt.outq.empty() + + # test blackout_period option. Write first revisit record because + # its outside the blackout_period (60). Do not write the second + # because its inside the blackout_period. + recorder = ProxyingRecorder(io.BytesIO(b'test1'), None) + recorder.read() + old = datetime.utcnow() - timedelta(0, 3600) + ru = RecordedUrl( + url='http://example.com/yes', + # content_type=hanzo.httptools.ResponseMessage.CONTENT_TYPE, + content_type='text/plain', + status=200, client_ip='127.0.0.2', request_data=b'abc', + response_recorder=recorder, remote_ip='127.0.0.3', + timestamp=datetime.utcnow(), + payload_digest=recorder.block_digest) + ru.dedup_info = dict(id=b'1', url=b'http://example.com/dup', + date=old.strftime('%Y-%m-%dT%H:%M:%SZ').encode('utf-8')) + wwt.inq.put(ru) + recorded_url = wwt.outq.get(timeout=10) + recorder = ProxyingRecorder(io.BytesIO(b'test2'), None) + recorder.read() + recent = datetime.utcnow() - timedelta(0, 5) + ru = RecordedUrl( + url='http://example.com/yes', content_type='text/plain', + status=200, client_ip='127.0.0.2', request_data=b'abc', + response_recorder=recorder, remote_ip='127.0.0.3', + timestamp=datetime.utcnow(), + payload_digest=recorder.block_digest) + ru.dedup_info = dict(id=b'2', url=b'http://example.com/dup', + date=recent.strftime('%Y-%m-%dT%H:%M:%SZ').encode('utf-8')) + wwt.inq.put(ru) + assert recorded_url.warc_records + recorded_url = wwt.outq.get(timeout=10) + assert not recorded_url.warc_records + assert wwt.outq.empty() + finally: wwt.stop.set() wwt.join() diff --git a/warcprox/main.py b/warcprox/main.py index 5f45a13..2ba996c 100644 --- a/warcprox/main.py +++ b/warcprox/main.py @@ -158,6 +158,9 @@ def _build_arg_parser(prog='warcprox'): # Warcprox-Meta HTTP header. By default, we dedup all requests. arg_parser.add_argument('--dedup-only-with-bucket', dest='dedup_only_with_bucket', action='store_true', default=False, help=argparse.SUPPRESS) + arg_parser.add_argument('--blackout-period', dest='blackout_period', + type=int, default=0, + help='skip writing a revisit record if its too close to the original capture') arg_parser.add_argument('--queue-size', dest='queue_size', type=int, default=500, help=argparse.SUPPRESS) arg_parser.add_argument('--max-threads', dest='max_threads', type=int, diff --git a/warcprox/writerthread.py b/warcprox/writerthread.py index ef0bd2d..83f4485 100644 --- a/warcprox/writerthread.py +++ b/warcprox/writerthread.py @@ -31,6 +31,7 @@ import logging import time import warcprox from concurrent import futures +from datetime import datetime import threading class WarcWriterProcessor(warcprox.BaseStandardPostfetchProcessor): @@ -52,6 +53,7 @@ class WarcWriterProcessor(warcprox.BaseStandardPostfetchProcessor): max_workers=options.writer_threads or 1, max_queued=10 * (options.writer_threads or 1)) self.batch = set() + self.blackout_period = options.blackout_period or 0 def _startup(self): self.logger.info('%s warc writer threads', self.pool._max_workers) @@ -112,9 +114,32 @@ class WarcWriterProcessor(warcprox.BaseStandardPostfetchProcessor): if recorded_url.warcprox_meta and 'warc-prefix' in recorded_url.warcprox_meta else self.options.prefix) + res = (prefix != '-' and not recorded_url.do_not_archive + and self._filter_accepts(recorded_url) + and not self._in_blackout(recorded_url)) + # special warc name prefix '-' means "don't archive" return (prefix != '-' and not recorded_url.do_not_archive - and self._filter_accepts(recorded_url)) + and self._filter_accepts(recorded_url) + and not self._in_blackout(recorded_url)) + + def _in_blackout(self, recorded_url): + """If --blackout-period=N (sec) is set, check if duplicate record + datetime is close to the original. If yes, we don't write it to WARC. + The aim is to avoid having unnecessary `revisit` records. + Return Boolean + """ + if self.blackout_period and hasattr(recorded_url, "dedup_info") and \ + recorded_url.dedup_info: + dedup_date = recorded_url.dedup_info.get('date') + if dedup_date: + try: + dt = datetime.strptime(dedup_date.decode('utf-8'), + '%Y-%m-%dT%H:%M:%SZ') + return (datetime.utcnow() - dt).total_seconds() <= self.blackout_period + except ValueError: + return False + return False def _log(self, recorded_url, records): try: From 6b1d60c3901277916aeec3027cf8723ba9e5c363 Mon Sep 17 00:00:00 2001 From: Vangelis Banos Date: Tue, 24 Jul 2018 07:16:21 +0000 Subject: [PATCH 02/19] Apply blackout on when dedup URL equals request URL --- tests/test_writer.py | 5 ++--- warcprox/writerthread.py | 6 +----- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/tests/test_writer.py b/tests/test_writer.py index 2675393..ab6d9aa 100644 --- a/tests/test_writer.py +++ b/tests/test_writer.py @@ -166,8 +166,7 @@ def test_special_dont_write_prefix(): recorder.read() old = datetime.utcnow() - timedelta(0, 3600) ru = RecordedUrl( - url='http://example.com/yes', - # content_type=hanzo.httptools.ResponseMessage.CONTENT_TYPE, + url='http://example.com/dup', content_type='text/plain', status=200, client_ip='127.0.0.2', request_data=b'abc', response_recorder=recorder, remote_ip='127.0.0.3', @@ -181,7 +180,7 @@ def test_special_dont_write_prefix(): recorder.read() recent = datetime.utcnow() - timedelta(0, 5) ru = RecordedUrl( - url='http://example.com/yes', content_type='text/plain', + url='http://example.com/dup', content_type='text/plain', status=200, client_ip='127.0.0.2', request_data=b'abc', response_recorder=recorder, remote_ip='127.0.0.3', timestamp=datetime.utcnow(), diff --git a/warcprox/writerthread.py b/warcprox/writerthread.py index 83f4485..5eef44f 100644 --- a/warcprox/writerthread.py +++ b/warcprox/writerthread.py @@ -114,10 +114,6 @@ class WarcWriterProcessor(warcprox.BaseStandardPostfetchProcessor): if recorded_url.warcprox_meta and 'warc-prefix' in recorded_url.warcprox_meta else self.options.prefix) - res = (prefix != '-' and not recorded_url.do_not_archive - and self._filter_accepts(recorded_url) - and not self._in_blackout(recorded_url)) - # special warc name prefix '-' means "don't archive" return (prefix != '-' and not recorded_url.do_not_archive and self._filter_accepts(recorded_url) @@ -132,7 +128,7 @@ class WarcWriterProcessor(warcprox.BaseStandardPostfetchProcessor): if self.blackout_period and hasattr(recorded_url, "dedup_info") and \ recorded_url.dedup_info: dedup_date = recorded_url.dedup_info.get('date') - if dedup_date: + if dedup_date and recorded_url.dedup_info.get('url') == recorded_url.url: try: dt = datetime.strptime(dedup_date.decode('utf-8'), '%Y-%m-%dT%H:%M:%SZ') From 17a5fabb75a28b15f19f63695dce545e54034c37 Mon Sep 17 00:00:00 2001 From: Noah Levitt Date: Thu, 16 Aug 2018 11:06:58 -0700 Subject: [PATCH 03/19] use SpooledTemporaryFile for WARCPROX_WRITE_RECORD payloads. because as of https://github.com/internetarchive/brozzler/pull/115 brozzler will be sending big videos via WARCPROX_WRITE_RECORD --- setup.py | 2 +- tests/test_writer.py | 5 +++-- warcprox/crawl_log.py | 2 +- warcprox/mitmproxy.py | 9 ++++---- warcprox/warc.py | 51 +++++++++++++++++++++++++++++++------------ warcprox/warcproxy.py | 21 ++++++++++++++---- 6 files changed, 63 insertions(+), 27 deletions(-) diff --git a/setup.py b/setup.py index 1707f1f..3add81d 100755 --- a/setup.py +++ b/setup.py @@ -40,7 +40,7 @@ except: setuptools.setup( name='warcprox', - version='2.4b3.dev180', + version='2.4b3.dev181', description='WARC writing MITM HTTP/S proxy', url='https://github.com/internetarchive/warcprox', author='Noah Levitt', diff --git a/tests/test_writer.py b/tests/test_writer.py index ab6d9aa..ed5c699 100644 --- a/tests/test_writer.py +++ b/tests/test_writer.py @@ -34,6 +34,7 @@ import warcprox import io import tempfile import logging +import hashlib def lock_file(queue, filename): """Try to lock file and return 1 if successful, else return 0. @@ -58,7 +59,7 @@ def test_warc_writer_locking(tmpdir): url='http://example.com', content_type='text/plain', status=200, client_ip='127.0.0.2', request_data=b'abc', response_recorder=recorder, remote_ip='127.0.0.3', - timestamp=datetime.utcnow()) + timestamp=datetime.utcnow(), payload_digest=hashlib.sha1()) dirname = os.path.dirname(str(tmpdir.mkdir('test-warc-writer'))) wwriter = WarcWriter(Options( @@ -247,7 +248,7 @@ def test_warc_writer_filename(tmpdir): url='http://example.com', content_type='text/plain', status=200, client_ip='127.0.0.2', request_data=b'abc', response_recorder=recorder, remote_ip='127.0.0.3', - timestamp=datetime.utcnow()) + timestamp=datetime.utcnow(), payload_digest=hashlib.sha1()) dirname = os.path.dirname(str(tmpdir.mkdir('test-warc-writer'))) wwriter = WarcWriter(Options(directory=dirname, prefix='foo', diff --git a/warcprox/crawl_log.py b/warcprox/crawl_log.py index a953402..19dde96 100644 --- a/warcprox/crawl_log.py +++ b/warcprox/crawl_log.py @@ -49,7 +49,7 @@ class CrawlLogger(object): self.options.base32) else: # WARCPROX_WRITE_RECORD request - content_length = len(recorded_url.request_data) + content_length = int(records[0].get_header(b'Content-Length')) payload_digest = records[0].get_header(b'WARC-Payload-Digest') fields = [ '{:%Y-%m-%dT%H:%M:%S}.{:03d}Z'.format(now, now.microsecond//1000), diff --git a/warcprox/mitmproxy.py b/warcprox/mitmproxy.py index e01f15e..21d5c3f 100644 --- a/warcprox/mitmproxy.py +++ b/warcprox/mitmproxy.py @@ -283,9 +283,9 @@ class MitmProxyHandler(http_server.BaseHTTPRequestHandler): self._remote_server_conn.sock) except ssl.SSLError: self.logger.warn( - "failed to establish ssl connection to %s; python " - "ssl library does not support SNI, considering " - "upgrading to python >= 2.7.9 or python 3.4", + "failed to establish ssl connection to %s; " + "python ssl library does not support SNI, " + "consider upgrading to python 2.7.9+ or 3.4+", self.hostname) raise return self._remote_server_conn.sock @@ -424,8 +424,7 @@ class MitmProxyHandler(http_server.BaseHTTPRequestHandler): self.command, self.path, self.request_version) # Swallow headers that don't make sense to forward on, i.e. most - # hop-by-hop headers, see - # http://tools.ietf.org/html/rfc2616#section-13.5. + # hop-by-hop headers. http://tools.ietf.org/html/rfc2616#section-13.5. # self.headers is an email.message.Message, which is case-insensitive # and doesn't throw KeyError in __delitem__ for key in ( diff --git a/warcprox/warc.py b/warcprox/warc.py index 708366b..21d0f5d 100644 --- a/warcprox/warc.py +++ b/warcprox/warc.py @@ -19,8 +19,6 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. ''' -from __future__ import absolute_import - import logging import warcprox import hashlib @@ -83,16 +81,21 @@ class WarcRecordBuilder: concurrent_to=principal_record.id) return principal_record, request_record else: - principal_record = self.build_warc_record(url=recorded_url.url, + principal_record = self.build_warc_record( + url=recorded_url.url, warc_date=warc_date, data=recorded_url.request_data, warc_type=recorded_url.custom_type, - content_type=recorded_url.content_type.encode("latin1")) + content_type=recorded_url.content_type.encode("latin1"), + payload_digest=warcprox.digest_str( + recorded_url.payload_digest, self.base32), + content_length=recorded_url.size) return (principal_record,) def build_warc_record(self, url, warc_date=None, recorder=None, data=None, concurrent_to=None, warc_type=None, content_type=None, remote_ip=None, profile=None, refers_to=None, refers_to_target_uri=None, - refers_to_date=None, payload_digest=None, truncated=None): + refers_to_date=None, payload_digest=None, truncated=None, + content_length=None): if warc_date is None: warc_date = warctools.warc.warc_datetime_str(datetime.datetime.utcnow()) @@ -126,21 +129,41 @@ class WarcRecordBuilder: headers.append((b'WARC-Truncated', truncated)) if recorder is not None: - headers.append((warctools.WarcRecord.CONTENT_LENGTH, str(len(recorder)).encode('latin1'))) + if content_length is not None: + headers.append(( + warctools.WarcRecord.CONTENT_LENGTH, + str(content_length).encode('latin1'))) + else: + headers.append(( + warctools.WarcRecord.CONTENT_LENGTH, + str(len(recorder)).encode('latin1'))) headers.append((warctools.WarcRecord.BLOCK_DIGEST, warcprox.digest_str(recorder.block_digest, self.base32))) recorder.tempfile.seek(0) record = warctools.WarcRecord(headers=headers, content_file=recorder.tempfile) else: - headers.append((warctools.WarcRecord.CONTENT_LENGTH, str(len(data)).encode('latin1'))) - digest = hashlib.new(self.digest_algorithm, data) - headers.append((warctools.WarcRecord.BLOCK_DIGEST, - warcprox.digest_str(digest, self.base32))) + if content_length is not None: + headers.append(( + warctools.WarcRecord.CONTENT_LENGTH, + str(content_length).encode('latin1'))) + else: + headers.append(( + warctools.WarcRecord.CONTENT_LENGTH, + str(len(data)).encode('latin1'))) + # no http headers so block digest == payload digest if not payload_digest: - headers.append((warctools.WarcRecord.PAYLOAD_DIGEST, - warcprox.digest_str(digest, self.base32))) - content_tuple = content_type, data - record = warctools.WarcRecord(headers=headers, content=content_tuple) + payload_digest = warcprox.digest_str( + hashlib.new(self.digest_algorithm, data), self.base32) + headers.append(( + warctools.WarcRecord.PAYLOAD_DIGEST, payload_digest)) + headers.append((warctools.WarcRecord.BLOCK_DIGEST, payload_digest)) + if hasattr(data, 'read'): + record = warctools.WarcRecord( + headers=headers, content_file=data) + else: + content_tuple = content_type, data + record = warctools.WarcRecord( + headers=headers, content=content_tuple) return record diff --git a/warcprox/warcproxy.py b/warcprox/warcproxy.py index 417f450..2ccfa13 100644 --- a/warcprox/warcproxy.py +++ b/warcprox/warcproxy.py @@ -44,6 +44,8 @@ import datetime import urlcanon import os from urllib3 import PoolManager +import tempfile +import hashlib class WarcProxyHandler(warcprox.mitmproxy.MitmProxyHandler): ''' @@ -285,8 +287,16 @@ class WarcProxyHandler(warcprox.mitmproxy.MitmProxyHandler): and (warc_type or 'WARC-Type' in self.headers)): timestamp = datetime.datetime.utcnow() - # stream this? - request_data = self.rfile.read(int(self.headers['Content-Length'])) + request_data = tempfile.SpooledTemporaryFile(max_size=524288) + payload_digest = hashlib.new(self.server.digest_algorithm) + + length = int(self.headers['Content-Length']) + buf = self.rfile.read(min(65536, length - request_data.tell())) + while buf != b'': + request_data.write(buf) + payload_digest.update(buf) + buf = self.rfile.read( + min(65536, length - request_data.tell())) warcprox_meta = None raw_warcprox_meta = self.headers.get('Warcprox-Meta') @@ -301,11 +311,14 @@ class WarcProxyHandler(warcprox.mitmproxy.MitmProxyHandler): warcprox_meta=warcprox_meta, content_type=self.headers['Content-Type'], custom_type=warc_type or self.headers['WARC-Type'].encode('utf-8'), - status=204, size=len(request_data), + status=204, + size=request_data.tell(), client_ip=self.client_address[0], method=self.command, timestamp=timestamp, - duration=datetime.datetime.utcnow()-timestamp) + duration=datetime.datetime.utcnow()-timestamp, + payload_digest=payload_digest) + request_data.seek(0) self.server.recorded_url_q.put(rec_custom) self.send_response(204, 'OK') From f8b86a0122f1179816474f4bb27746ebd062f2a1 Mon Sep 17 00:00:00 2001 From: Noah Levitt Date: Thu, 16 Aug 2018 12:54:30 -0700 Subject: [PATCH 04/19] update cryptography dep version github tells me there's a vulnerability <2.3 --- setup.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index 3add81d..c5da0ee 100755 --- a/setup.py +++ b/setup.py @@ -25,13 +25,13 @@ import setuptools deps = [ 'certauth==1.1.6', - 'warctools', + 'warctools>=4.10.0', 'urlcanon>=0.1.dev16', 'doublethink>=0.2.0.dev87', - 'urllib3', + 'urllib3>=1.23', 'requests>=2.0.1', - 'PySocks', - 'cryptography!=2.1.1', # 2.1.1 installation is failing on ubuntu + 'PySocks>=1.6.8', + 'cryptography>=2.3', ] try: import concurrent.futures @@ -40,7 +40,7 @@ except: setuptools.setup( name='warcprox', - version='2.4b3.dev181', + version='2.4b3.dev182', description='WARC writing MITM HTTP/S proxy', url='https://github.com/internetarchive/warcprox', author='Noah Levitt', From b72192d3d01eaabfdd5452d89634bacd482e2a93 Mon Sep 17 00:00:00 2001 From: Karl-Rainer Blumenthal Date: Thu, 28 Jun 2018 16:06:04 -0400 Subject: [PATCH 05/19] Copy edits --- README.rst | 112 ++++++++++++++--------------------------------------- 1 file changed, 29 insertions(+), 83 deletions(-) diff --git a/README.rst b/README.rst index dbb1440..51ecb94 100644 --- a/README.rst +++ b/README.rst @@ -3,22 +3,9 @@ Warcprox - WARC writing MITM HTTP/S proxy .. image:: https://travis-ci.org/internetarchive/warcprox.svg?branch=master :target: https://travis-ci.org/internetarchive/warcprox -Warcprox is a tool for archiving the web. It is an http proxy that stores its -traffic to disk in `WARC -`_ -format. Warcprox captures encrypted https traffic by using the -`"man-in-the-middle" `_ -technique (see the `Man-in-the-middle`_ section for more info). +Warcprox is an HTTP proxy designed for web archiving applications. When used in parallel with `brozzler `_ it supports a comprehensive, modern, and distributed archival web capture system. Warcprox stores its traffic to disk in the `Web ARChive (WARC) file format `_, which may then be accessed with web archival replay software like `OpenWayback `_ and `pywb `_. It captures encrypted HTTPS traffic by using the "man-in-the-middle" technique (see the `Man-in-the-middle`_ section for more info). -The web pages that warcprox stores in WARC files can be played back using -software like `OpenWayback `_ or `pywb -`_. Warcprox has been developed in -parallel with `brozzler `_ and -together they make a comprehensive modern distributed archival web crawling -system. - -Warcprox was originally based on the excellent and simple pymiproxy by Nadeem -Douba. https://github.com/allfro/pymiproxy +Warcprox was originally based on `pymiproxy `_ by Nadeem Douba. .. contents:: @@ -43,91 +30,57 @@ Try ``warcprox --help`` for documentation on command line options. Man-in-the-middle ================= -Normally, http proxies can't read https traffic, because it's encrypted. The -browser uses the http ``CONNECT`` method to establish a tunnel through the -proxy, and the proxy merely routes raw bytes between the client and server. -Since the bytes are encrypted, the proxy can't make sense of the information -it's proxying. This nonsensical encrypted data would not be very useful to -archive. +Normally, HTTP proxies can't read encrypted HTTPS traffic. The browser uses the HTTP ``CONNECT`` method to establish a tunnel through the proxy, and the proxy merely routes raw bytes between the client and server. Since the bytes are encrypted, the proxy can't make sense of the information that it proxies. This nonsensical encrypted data is not typically useful for web archiving purposes. -In order to capture https traffic, warcprox acts as a "man-in-the-middle" -(MITM). When it receives a ``CONNECT`` directive from a client, it generates a -public key certificate for the requested site, presents to the client, and -proceeds to establish an encrypted connection with the client. Then it makes a -separate, normal https connection to the remote site. It decrypts, archives, -and re-encrypts traffic in both directions. +In order to capture HTTPS traffic, warcprox acts as a "man-in-the-middle" (MITM). When it receives a ``CONNECT`` directive from a client, it generates a public key certificate for the requested site, presents to the client, and proceeds to establish an encrypted connection with the client. It then makes a separate, normal HTTPS connection to the remote site. It decrypts, archives, and re-encrypts traffic in both directions. -Although "man-in-the-middle" is often paired with "attack", there is nothing -malicious about what warcprox is doing. If you configure an instance of -warcprox as your browser's http proxy, you will see lots of certificate -warnings, since none of the certificates will be signed by trusted authorities. -To use warcprox effectively the client needs to disable certificate -verification, or add the CA cert generated by warcprox as a trusted authority. -(If you do this in your browser, make sure you undo it when you're done using -warcprox!) +Configuring a warcprox instance as a browser’s HTTP proxy will result in security certificate warnings because none of the certificates will be signed by trusted authorities. However, there is nothing malicious about warcprox functions. To use warcprox effectively, the client needs to disable certificate verification or add the CA certification generated by warcprox as a trusted authority. When using the latter, remember to undo this change when finished using warcprox. API === -For interacting with a running instance of warcprox. +The warcprox API may be used to retrieve information from and interact with a running warcprox instance, including: -* ``/status`` url -* ``WARCPROX_WRITE_RECORD`` http method -* ``Warcprox-Meta`` http request header and response header +* Retrieving status information via ``/status`` URL +* Writing WARC records via ``WARCPROX_WRITE_RECORD`` HTTP method +* Querying and editing ``Warcprox-Meta`` HTTP request header and response header -See ``_. +For warcprox API documentation, see: ``_. Deduplication ============= -Warcprox avoids archiving redundant content by "deduplicating" it. The process -for deduplication works similarly to heritrix and other web archiving tools. +Warcprox avoids archiving redundant content by "deduplicating" it. The process for deduplication works similarly to deduplication by `Heritrix `_ and other web archiving tools: -1. while fetching url, calculate payload content digest (typically sha1) -2. look up digest in deduplication database (warcprox supports a few different - ones) -3. if found, write warc ``revisit`` record referencing the url and capture time - of the previous capture -4. else (if not found), +1. While fetching URL, calculate payload content digest (typically SHA1 checksum value) +2. Look up digest in deduplication database (warcprox currently supports `sqlite `_ by default, `rethinkdb `_ with two different schemas, and `trough `_) +3. If found, write warc ``revisit`` record referencing the url and capture time of the previous capture +4. If not found, - a. write warc ``response`` record with full payload - b. store entry in deduplication database + a. Write warc ``response`` record with full payload + b. Store new entry in deduplication database -The dedup database is partitioned into different "buckets". Urls are -deduplicated only against other captures in the same bucket. If specified, the -``dedup-bucket`` field of the ``Warcprox-Meta`` http request header determines -the bucket, otherwise the default bucket is used. +The deduplication database is partitioned into different "buckets". URLs are deduplicated only against other captures in the same bucket. If specified, the ``dedup-bucket`` field of the `Warcprox-Meta HTTP request header `_ determines the bucket. Otherwise, the default bucket is used. Deduplication can be disabled entirely by starting warcprox with the argument ``--dedup-db-file=/dev/null``. Statistics ========== -Warcprox keeps some crawl statistics and stores them in sqlite or rethinkdb. -These are consulted for enforcing ``limits`` and ``soft-limits`` (see -``_), and can also be consulted by other -processes outside of warcprox, for reporting etc. +Warcprox stores some crawl statistics to sqlite or rethinkdb. These are consulted for enforcing ``limits`` and ``soft-limits`` (see `Warcprox-Meta fields `_), and can also be consulted by other processes outside of warcprox, such as for crawl job reporting. -Statistics are grouped by "bucket". Every capture is counted as part of the -``__all__`` bucket. Other buckets can be specified in the ``Warcprox-Meta`` -request header. The fallback bucket in case none is specified is called -``__unspecified__``. +Statistics are grouped by "bucket". Every capture is counted as part of the ``__all__`` bucket. Other buckets can be specified in the ``Warcprox-Meta`` request header. The fallback bucket in case none is specified is called ``__unspecified__``. Within each bucket are three sub-buckets: -* ``new`` - tallies captures for which a complete record (usually a ``response`` - record) was written to warc -* ``revisit`` - tallies captures for which a ``revisit`` record was written to - warc -* ``total`` - includes all urls processed, even those not written to warc (so the - numbers may be greater than new + revisit) +* ``new`` - tallies captures for which a complete record (usually a ``response`` record) was written to a WARC file +* ``revisit`` - tallies captures for which a ``revisit`` record was written to a WARC file +* ``total`` - includes all URLs processed, even those not written to a WARC file, and so may be greater than the sum of new and revisit records -Within each of these sub-buckets we keep two statistics: +Within each of these sub-buckets, warcprox generates two kinds of statistics: -* ``urls`` - simple count of urls -* ``wire_bytes`` - sum of bytes received over the wire, including http headers, - from the remote server for each url +* ``urls`` - simple count of URLs +* ``wire_bytes`` - sum of bytes received over the wire from the remote server for each URL, including HTTP headers -For historical reasons, in sqlite, the default store, statistics are kept as -json blobs:: +For historical reasons, the default sqlite store keeps statistics as JSON blobs:: sqlite> select * from buckets_of_stats; bucket stats @@ -137,16 +90,9 @@ json blobs:: Plugins ======= -Warcprox supports a limited notion of plugins by way of the ``--plugin`` -command line argument. Plugin classes are loaded from the regular python module -search path. They will be instantiated with one argument, a -``warcprox.Options``, which holds the values of all the command line arguments. -Legacy plugins with constructors that take no arguments are also supported. -Plugins should either have a method ``notify(self, recorded_url, records)`` or -should subclass ``warcprox.BasePostfetchProcessor``. More than one plugin can -be configured by specifying ``--plugin`` multiples times. +Warcprox supports a limited notion of plugins by way of the ``--plugin`` command line argument. Plugin classes are loaded from the regular python module search path. They are instantiated with one argument that contains the values of all command line arguments, ``warcprox.Options``. Legacy plugins with constructors that take no arguments are also supported. Plugins should either have a method ``notify(self, recorded_url, records)`` or should subclass ``warcprox.BasePostfetchProcessor``. More than one plugin can be configured by specifying ``--plugin`` multiples times. -`A minimal example `__ +See a minimal example `here `__. License ======= From fa6b98cf4e951a6629b387b16d3d8e00171dc90a Mon Sep 17 00:00:00 2001 From: Karl-Rainer Blumenthal Date: Fri, 6 Jul 2018 11:33:34 -0400 Subject: [PATCH 06/19] Copy edits updated Edits for readability updated as per https://github.com/internetarchive/warcprox/pull/95#discussion_r200491731 @nlevitt please go ahead and apply your < 80 lines retroactively and I'll refrain from that in future PRs. --- README.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 51ecb94..79bcc07 100644 --- a/README.rst +++ b/README.rst @@ -34,7 +34,7 @@ Normally, HTTP proxies can't read encrypted HTTPS traffic. The browser uses the In order to capture HTTPS traffic, warcprox acts as a "man-in-the-middle" (MITM). When it receives a ``CONNECT`` directive from a client, it generates a public key certificate for the requested site, presents to the client, and proceeds to establish an encrypted connection with the client. It then makes a separate, normal HTTPS connection to the remote site. It decrypts, archives, and re-encrypts traffic in both directions. -Configuring a warcprox instance as a browser’s HTTP proxy will result in security certificate warnings because none of the certificates will be signed by trusted authorities. However, there is nothing malicious about warcprox functions. To use warcprox effectively, the client needs to disable certificate verification or add the CA certification generated by warcprox as a trusted authority. When using the latter, remember to undo this change when finished using warcprox. +Configuring a warcprox instance as a browser’s HTTP proxy will result in security certificate warnings because none of the certificates will be signed by trusted authorities. However, there is nothing malicious about warcprox functions. To use warcprox effectively, the client needs to disable certificate verification or add the CA certificate generated by warcprox as a trusted authority. When using the latter, remember to undo this change when finished using warcprox. API === @@ -42,7 +42,7 @@ The warcprox API may be used to retrieve information from and interact with a ru * Retrieving status information via ``/status`` URL * Writing WARC records via ``WARCPROX_WRITE_RECORD`` HTTP method -* Querying and editing ``Warcprox-Meta`` HTTP request header and response header +* Controlling warcprox settings via the ``Warcprox-Meta`` HTTP header For warcprox API documentation, see: ``_. @@ -55,7 +55,7 @@ Warcprox avoids archiving redundant content by "deduplicating" it. The process f 3. If found, write warc ``revisit`` record referencing the url and capture time of the previous capture 4. If not found, - a. Write warc ``response`` record with full payload + a. Write ``response`` record with full payload b. Store new entry in deduplication database The deduplication database is partitioned into different "buckets". URLs are deduplicated only against other captures in the same bucket. If specified, the ``dedup-bucket`` field of the `Warcprox-Meta HTTP request header `_ determines the bucket. Otherwise, the default bucket is used. From 9da5e86b673c3c3c205cb408311f6f3c8ca598c5 Mon Sep 17 00:00:00 2001 From: Noah Levitt Date: Thu, 16 Aug 2018 16:32:55 -0700 Subject: [PATCH 07/19] restore 80 column lines --- README.rst | 93 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 74 insertions(+), 19 deletions(-) diff --git a/README.rst b/README.rst index 79bcc07..d76e2191 100644 --- a/README.rst +++ b/README.rst @@ -3,9 +3,19 @@ Warcprox - WARC writing MITM HTTP/S proxy .. image:: https://travis-ci.org/internetarchive/warcprox.svg?branch=master :target: https://travis-ci.org/internetarchive/warcprox -Warcprox is an HTTP proxy designed for web archiving applications. When used in parallel with `brozzler `_ it supports a comprehensive, modern, and distributed archival web capture system. Warcprox stores its traffic to disk in the `Web ARChive (WARC) file format `_, which may then be accessed with web archival replay software like `OpenWayback `_ and `pywb `_. It captures encrypted HTTPS traffic by using the "man-in-the-middle" technique (see the `Man-in-the-middle`_ section for more info). +Warcprox is an HTTP proxy designed for web archiving applications. When used in +parallel with `brozzler `_ it +supports a comprehensive, modern, and distributed archival web capture system. +Warcprox stores its traffic to disk in the `Web ARChive (WARC) file format +`_, +which may then be accessed with web archival replay software like `OpenWayback +`_ and `pywb +`_. It captures encrypted HTTPS traffic by +using the "man-in-the-middle" technique (see the `Man-in-the-middle`_ section +for more info). -Warcprox was originally based on `pymiproxy `_ by Nadeem Douba. +Warcprox was originally based on `pymiproxy +`_ by Nadeem Douba. .. contents:: @@ -30,15 +40,31 @@ Try ``warcprox --help`` for documentation on command line options. Man-in-the-middle ================= -Normally, HTTP proxies can't read encrypted HTTPS traffic. The browser uses the HTTP ``CONNECT`` method to establish a tunnel through the proxy, and the proxy merely routes raw bytes between the client and server. Since the bytes are encrypted, the proxy can't make sense of the information that it proxies. This nonsensical encrypted data is not typically useful for web archiving purposes. +Normally, HTTP proxies can't read encrypted HTTPS traffic. The browser uses the +HTTP ``CONNECT`` method to establish a tunnel through the proxy, and the proxy +merely routes raw bytes between the client and server. Since the bytes are +encrypted, the proxy can't make sense of the information that it proxies. This +nonsensical encrypted data is not typically useful for web archiving purposes. -In order to capture HTTPS traffic, warcprox acts as a "man-in-the-middle" (MITM). When it receives a ``CONNECT`` directive from a client, it generates a public key certificate for the requested site, presents to the client, and proceeds to establish an encrypted connection with the client. It then makes a separate, normal HTTPS connection to the remote site. It decrypts, archives, and re-encrypts traffic in both directions. +In order to capture HTTPS traffic, warcprox acts as a "man-in-the-middle" +(MITM). When it receives a ``CONNECT`` directive from a client, it generates a +public key certificate for the requested site, presents to the client, and +proceeds to establish an encrypted connection with the client. It then makes a +separate, normal HTTPS connection to the remote site. It decrypts, archives, +and re-encrypts traffic in both directions. -Configuring a warcprox instance as a browser’s HTTP proxy will result in security certificate warnings because none of the certificates will be signed by trusted authorities. However, there is nothing malicious about warcprox functions. To use warcprox effectively, the client needs to disable certificate verification or add the CA certificate generated by warcprox as a trusted authority. When using the latter, remember to undo this change when finished using warcprox. +Configuring a warcprox instance as a browser’s HTTP proxy will result in +security certificate warnings because none of the certificates will be signed +by trusted authorities. However, there is nothing malicious about warcprox +functions. To use warcprox effectively, the client needs to disable certificate +verification or add the CA certificate generated by warcprox as a trusted +authority. When using the latter, remember to undo this change when finished +using warcprox. API === -The warcprox API may be used to retrieve information from and interact with a running warcprox instance, including: +The warcprox API may be used to retrieve information from and interact with a +running warcprox instance, including: * Retrieving status information via ``/status`` URL * Writing WARC records via ``WARCPROX_WRITE_RECORD`` HTTP method @@ -48,37 +74,58 @@ For warcprox API documentation, see: ``_. Deduplication ============= -Warcprox avoids archiving redundant content by "deduplicating" it. The process for deduplication works similarly to deduplication by `Heritrix `_ and other web archiving tools: +Warcprox avoids archiving redundant content by "deduplicating" it. The process +for deduplication works similarly to deduplication by `Heritrix +`_ and other web archiving tools: -1. While fetching URL, calculate payload content digest (typically SHA1 checksum value) -2. Look up digest in deduplication database (warcprox currently supports `sqlite `_ by default, `rethinkdb `_ with two different schemas, and `trough `_) -3. If found, write warc ``revisit`` record referencing the url and capture time of the previous capture +1. While fetching URL, calculate payload content digest (typically SHA1 + checksum value) +2. Look up digest in deduplication database (warcprox currently supports + `sqlite `_ by default, `rethinkdb + `_ with two different schemas, and + `trough `_) +3. If found, write warc ``revisit`` record referencing the url and capture time + of the previous capture 4. If not found, a. Write ``response`` record with full payload b. Store new entry in deduplication database -The deduplication database is partitioned into different "buckets". URLs are deduplicated only against other captures in the same bucket. If specified, the ``dedup-bucket`` field of the `Warcprox-Meta HTTP request header `_ determines the bucket. Otherwise, the default bucket is used. +The deduplication database is partitioned into different "buckets". URLs are +deduplicated only against other captures in the same bucket. If specified, the +``dedup-bucket`` field of the `Warcprox-Meta HTTP request header +`_ determines the bucket. Otherwise, +the default bucket is used. Deduplication can be disabled entirely by starting warcprox with the argument ``--dedup-db-file=/dev/null``. Statistics ========== -Warcprox stores some crawl statistics to sqlite or rethinkdb. These are consulted for enforcing ``limits`` and ``soft-limits`` (see `Warcprox-Meta fields `_), and can also be consulted by other processes outside of warcprox, such as for crawl job reporting. +Warcprox stores some crawl statistics to sqlite or rethinkdb. These are +consulted for enforcing ``limits`` and ``soft-limits`` (see `Warcprox-Meta +fields `_), and can also be consulted by other +processes outside of warcprox, such as for crawl job reporting. -Statistics are grouped by "bucket". Every capture is counted as part of the ``__all__`` bucket. Other buckets can be specified in the ``Warcprox-Meta`` request header. The fallback bucket in case none is specified is called ``__unspecified__``. +Statistics are grouped by "bucket". Every capture is counted as part of the +``__all__`` bucket. Other buckets can be specified in the ``Warcprox-Meta`` +request header. The fallback bucket in case none is specified is called +``__unspecified__``. Within each bucket are three sub-buckets: -* ``new`` - tallies captures for which a complete record (usually a ``response`` record) was written to a WARC file -* ``revisit`` - tallies captures for which a ``revisit`` record was written to a WARC file -* ``total`` - includes all URLs processed, even those not written to a WARC file, and so may be greater than the sum of new and revisit records +* ``new`` - tallies captures for which a complete record (usually a + ``response`` record) was written to a WARC file +* ``revisit`` - tallies captures for which a ``revisit`` record was written to + a WARC file +* ``total`` - includes all URLs processed, even those not written to a WARC + file, and so may be greater than the sum of new and revisit records Within each of these sub-buckets, warcprox generates two kinds of statistics: * ``urls`` - simple count of URLs -* ``wire_bytes`` - sum of bytes received over the wire from the remote server for each URL, including HTTP headers +* ``wire_bytes`` - sum of bytes received over the wire from the remote server + for each URL, including HTTP headers For historical reasons, the default sqlite store keeps statistics as JSON blobs:: @@ -90,9 +137,17 @@ For historical reasons, the default sqlite store keeps statistics as JSON blobs: Plugins ======= -Warcprox supports a limited notion of plugins by way of the ``--plugin`` command line argument. Plugin classes are loaded from the regular python module search path. They are instantiated with one argument that contains the values of all command line arguments, ``warcprox.Options``. Legacy plugins with constructors that take no arguments are also supported. Plugins should either have a method ``notify(self, recorded_url, records)`` or should subclass ``warcprox.BasePostfetchProcessor``. More than one plugin can be configured by specifying ``--plugin`` multiples times. +Warcprox supports a limited notion of plugins by way of the ``--plugin`` +command line argument. Plugin classes are loaded from the regular python module +search path. They are instantiated with one argument that contains the values +of all command line arguments, ``warcprox.Options``. Legacy plugins with +constructors that take no arguments are also supported. Plugins should either +have a method ``notify(self, recorded_url, records)`` or should subclass +``warcprox.BasePostfetchProcessor``. More than one plugin can be configured by +specifying ``--plugin`` multiples times. -See a minimal example `here `__. +See a minimal example `here +`__. License ======= From 8f51ba4ab96ab76ef136c94f4d685a71f258b136 Mon Sep 17 00:00:00 2001 From: Noah Levitt Date: Thu, 16 Aug 2018 17:09:35 -0700 Subject: [PATCH 08/19] bump dev version number after merge --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c5da0ee..0e2e00f 100755 --- a/setup.py +++ b/setup.py @@ -40,7 +40,7 @@ except: setuptools.setup( name='warcprox', - version='2.4b3.dev182', + version='2.4b3.dev183', description='WARC writing MITM HTTP/S proxy', url='https://github.com/internetarchive/warcprox', author='Noah Levitt', From 1d1a73536a92060eb23a7e28f352ecd4c870a75c Mon Sep 17 00:00:00 2001 From: Noah Levitt Date: Mon, 20 Aug 2018 11:05:58 -0700 Subject: [PATCH 09/19] half-baked readme section on warcprox architecture --- README.rst | 22 ++++++++++++++++++++++ arch.jpg | Bin 0 -> 52677 bytes 2 files changed, 22 insertions(+) create mode 100644 arch.jpg diff --git a/README.rst b/README.rst index d76e2191..a026937 100644 --- a/README.rst +++ b/README.rst @@ -149,6 +149,28 @@ specifying ``--plugin`` multiples times. See a minimal example `here `__. +Architecture +============ +.. image:: arch.jpg + +Warcprox is multithreaded. It has pool of http proxy threads (100 by default). +When handling a request, a proxy thread records data from the remote server to +an in-memory buffer that spills over to disk if necessary (after 512k by +default), while it streams the data to the proxy client. Once the HTTP +transaction is complete, it puts the recorded URL in a thread-safe queue, to be +picked up by the first processor in the postfetch chain. + +The postfetch chain normally includes processors for loading deduplication +information, writing records to the WARC, saving deduplication information, and +updating statistics. The exact set of processors in the chain depends on +command line arguments; for example, plugins specified with ``--plugin`` are +processors in the postfetch chain. Each postfetch processor has its own thread +or threads. Thus the processors are able to run in parallel, independent of one +another. This design also enables them to process URLs in batch. For example, +the statistics processor gathers statistics for up to 10 seconds or 500 URLs, +whichever comes first, then updates the statistics database with just a few +queries. + License ======= diff --git a/arch.jpg b/arch.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f3c855b8ca133819c3b80bc93c138099a86a41a2 GIT binary patch literal 52677 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 Date: Mon, 20 Aug 2018 11:08:32 -0700 Subject: [PATCH 10/19] --help-hidden for help on hidden args --- warcprox/main.py | 103 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 74 insertions(+), 29 deletions(-) diff --git a/warcprox/main.py b/warcprox/main.py index 84f5ab3..0e2eddd 100644 --- a/warcprox/main.py +++ b/warcprox/main.py @@ -60,10 +60,23 @@ class BetterArgumentDefaultsHelpFormatter( else: return argparse.ArgumentDefaultsHelpFormatter._get_help_string(self, action) -def _build_arg_parser(prog='warcprox'): +def _build_arg_parser(prog='warcprox', show_hidden=False): + if show_hidden: + def suppress(msg): + return msg + else: + def suppress(msg): + return argparse.SUPPRESS + arg_parser = argparse.ArgumentParser(prog=prog, description='warcprox - WARC writing MITM HTTP/S proxy', formatter_class=BetterArgumentDefaultsHelpFormatter) + + hidden = arg_parser.add_argument_group('hidden options') + arg_parser.add_argument( + '--help-hidden', action='help', default=argparse.SUPPRESS, + help='show help message, including help on hidden options, and exit') + arg_parser.add_argument('-p', '--port', dest='port', default='8000', type=int, help='port to listen on') arg_parser.add_argument('-b', '--address', dest='address', @@ -81,8 +94,12 @@ def _build_arg_parser(prog='warcprox'): help='define custom WARC filename with variables {prefix}, {timestamp14}, {timestamp17}, {serialno}, {randomtoken}, {hostname}, {shorthostname}') arg_parser.add_argument('-z', '--gzip', dest='gzip', action='store_true', help='write gzip-compressed warc records') - arg_parser.add_argument('--no-warc-open-suffix', dest='no_warc_open_suffix', - default=False, action='store_true', help=argparse.SUPPRESS) + hidden.add_argument( + '--no-warc-open-suffix', dest='no_warc_open_suffix', + default=False, action='store_true', + help=suppress( + 'do not name warc files with suffix ".open" while writing to ' + 'them, but lock them with lockf(3) intead')) # not mentioned in --help: special value for '-' for --prefix means don't # archive the capture, unless prefix set in warcprox-meta header arg_parser.add_argument( @@ -146,43 +163,60 @@ def _build_arg_parser(prog='warcprox'): 'rethinkdb service registry table url; if provided, warcprox ' 'will create and heartbeat entry for itself')) # optional cookie values to pass to CDX Server; e.g. "cookie1=val1;cookie2=val2" - arg_parser.add_argument('--cdxserver-dedup-cookies', dest='cdxserver_dedup_cookies', - help=argparse.SUPPRESS) + hidden.add_argument( + '--cdxserver-dedup-cookies', dest='cdxserver_dedup_cookies', + help=suppress( + 'value of Cookie header to include in requests to the cdx ' + 'server, when using --cdxserver-dedup')) arg_parser.add_argument('--dedup-min-text-size', dest='dedup_min_text_size', type=int, default=0, help=('try to dedup text resources with payload size over this limit in bytes')) arg_parser.add_argument('--dedup-min-binary-size', dest='dedup_min_binary_size', type=int, default=0, help=( 'try to dedup binary resources with payload size over this limit in bytes')) - # optionally, dedup request only when `dedup-bucket` is available in - # Warcprox-Meta HTTP header. By default, we dedup all requests. - arg_parser.add_argument('--dedup-only-with-bucket', dest='dedup_only_with_bucket', - action='store_true', default=False, help=argparse.SUPPRESS) + hidden.add_argument( + '--dedup-only-with-bucket', dest='dedup_only_with_bucket', + action='store_true', default=False, help=suppress( + 'only deduplicate captures if "dedup-bucket" is set in ' + 'the Warcprox-Meta request header')) arg_parser.add_argument('--blackout-period', dest='blackout_period', type=int, default=0, help='skip writing a revisit record if its too close to the original capture') - arg_parser.add_argument('--queue-size', dest='queue_size', type=int, - default=500, help=argparse.SUPPRESS) - arg_parser.add_argument('--max-threads', dest='max_threads', type=int, - help=argparse.SUPPRESS) - arg_parser.add_argument('--profile', action='store_true', default=False, - help=argparse.SUPPRESS) - arg_parser.add_argument( - '--writer-threads', dest='writer_threads', type=int, default=None, - help=argparse.SUPPRESS) + hidden.add_argument( + '--queue-size', dest='queue_size', type=int, default=500, + help=suppress( + 'maximum number of urls that can be queued at each ' + 'step of the processing chain (see the section on warcprox ' + 'architecture in README.rst)')) + hidden.add_argument( + '--max-threads', dest='max_threads', type=int, default=100, + help=suppress('maximum number of http worker threads')) + hidden.add_argument( + '--profile', action='store_true', default=False, + help=suppress( + 'turn on performance profiling; summary statistics are dumped ' + 'every 10 minutes and at shutdown')) + hidden.add_argument( + '--writer-threads', dest='writer_threads', type=int, default=1, + help=suppress( + 'number of warc writer threads; caution, see ' + 'https://github.com/internetarchive/warcprox/issues/101')) arg_parser.add_argument( '--onion-tor-socks-proxy', dest='onion_tor_socks_proxy', default=None, help=( 'host:port of tor socks proxy, used only to connect to ' '.onion sites')) - # Configurable connection socket timeout, default is 60 sec. - arg_parser.add_argument( - '--socket-timeout', dest='socket_timeout', type=float, - default=None, help=argparse.SUPPRESS) + hidden.add_argument( + '--socket-timeout', dest='socket_timeout', type=float, default=60, + help=suppress( + 'socket timeout, used for proxy client connection and for ' + 'connection to remote server')) # Increasing this value increases memory usage but reduces /tmp disk I/O. - arg_parser.add_argument( + hidden.add_argument( '--tmp-file-max-memory-size', dest='tmp_file_max_memory_size', - type=int, default=512*1024, help=argparse.SUPPRESS) + type=int, default=512*1024, help=suppress( + 'size of in-memory buffer for each url being processed ' + '(spills over to temp space on disk if exceeded)')) arg_parser.add_argument( '--max-resource-size', dest='max_resource_size', type=int, default=None, help='maximum resource size limit in bytes') @@ -197,11 +231,18 @@ def _build_arg_parser(prog='warcprox'): 'Qualified name of plugin class, e.g. "mypkg.mymod.MyClass". ' 'May be used multiple times to register multiple plugins. ' 'See README.rst for more information.')) - arg_parser.add_argument('--version', action='version', + arg_parser.add_argument( + '-q', '--quiet', dest='quiet', action='store_true', + help='less verbose logging') + arg_parser.add_argument( + '-v', '--verbose', dest='verbose', action='store_true', + help='verbose logging') + arg_parser.add_argument( + '--trace', dest='trace', action='store_true', + help='very verbose logging') + arg_parser.add_argument( + '--version', action='version', version="warcprox {}".format(warcprox.__version__)) - arg_parser.add_argument('-v', '--verbose', dest='verbose', action='store_true') - arg_parser.add_argument('--trace', dest='trace', action='store_true') - arg_parser.add_argument('-q', '--quiet', dest='quiet', action='store_true') return arg_parser @@ -227,7 +268,11 @@ def parse_args(argv): ''' Parses command line arguments with argparse. ''' - arg_parser = _build_arg_parser(prog=os.path.basename(argv[0])) + show_hidden = False + if '--help-hidden' in argv: + show_hidden = True + argv = [argv[0], '--help-hidden'] + arg_parser = _build_arg_parser(os.path.basename(argv[0]), show_hidden) args = arg_parser.parse_args(args=argv[1:]) try: From 2e71d86072c0d75eabb19105a57a7a7822aa5cf4 Mon Sep 17 00:00:00 2001 From: Noah Levitt Date: Mon, 20 Aug 2018 11:09:53 -0700 Subject: [PATCH 11/19] WARCPROX_WRITE_RECORD respect buffer size setting --- warcprox/warcproxy.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/warcprox/warcproxy.py b/warcprox/warcproxy.py index 2ccfa13..17e0ebc 100644 --- a/warcprox/warcproxy.py +++ b/warcprox/warcproxy.py @@ -287,9 +287,11 @@ class WarcProxyHandler(warcprox.mitmproxy.MitmProxyHandler): and (warc_type or 'WARC-Type' in self.headers)): timestamp = datetime.datetime.utcnow() - request_data = tempfile.SpooledTemporaryFile(max_size=524288) + request_data = tempfile.SpooledTemporaryFile( + max_size=self.options.tmp_file_max_memory_size) payload_digest = hashlib.new(self.server.digest_algorithm) + # XXX we don't support chunked uploads for now length = int(self.headers['Content-Length']) buf = self.rfile.read(min(65536, length - request_data.tell())) while buf != b'': From bfe3f18126d63697341dbc72f84328058636fe40 Mon Sep 17 00:00:00 2001 From: Noah Levitt Date: Mon, 20 Aug 2018 11:11:13 -0700 Subject: [PATCH 12/19] set socket timeout for tor .onion fetching --- warcprox/mitmproxy.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/warcprox/mitmproxy.py b/warcprox/mitmproxy.py index 21d5c3f..ec5fc89 100644 --- a/warcprox/mitmproxy.py +++ b/warcprox/mitmproxy.py @@ -250,7 +250,7 @@ class MitmProxyHandler(http_server.BaseHTTPRequestHandler): ''' self._conn_pool = self.server.remote_connection_pool.connection_from_host( host=self.hostname, port=int(self.port), scheme='http', - pool_kwargs={'maxsize': 6}) + pool_kwargs={'maxsize': 6, 'timeout': self._socket_timeout}) self._remote_server_conn = self._conn_pool._get_conn() if is_connection_dropped(self._remote_server_conn): @@ -263,10 +263,9 @@ class MitmProxyHandler(http_server.BaseHTTPRequestHandler): self._remote_server_conn.sock.set_proxy( socks.SOCKS5, addr=self.onion_tor_socks_proxy_host, port=self.onion_tor_socks_proxy_port, rdns=True) - self._remote_server_conn.timeout = self._socket_timeout + self._remote_server_conn.sock.settimeout(self._socket_timeout) self._remote_server_conn.sock.connect((self.hostname, int(self.port))) else: - self._remote_server_conn.timeout = self._socket_timeout self._remote_server_conn.connect() # Wrap socket if SSL is required @@ -276,7 +275,8 @@ class MitmProxyHandler(http_server.BaseHTTPRequestHandler): context.check_hostname = False context.verify_mode = ssl.CERT_NONE self._remote_server_conn.sock = context.wrap_socket( - self._remote_server_conn.sock, server_hostname=self.hostname) + self._remote_server_conn.sock, + server_hostname=self.hostname) except AttributeError: try: self._remote_server_conn.sock = ssl.wrap_socket( From de01700c5445a90dd793c120eafc9e2dd7ba1704 Mon Sep 17 00:00:00 2001 From: Noah Levitt Date: Mon, 20 Aug 2018 11:13:14 -0700 Subject: [PATCH 13/19] tweak max threads option handling --- warcprox/mitmproxy.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/warcprox/mitmproxy.py b/warcprox/mitmproxy.py index ec5fc89..4153e54 100644 --- a/warcprox/mitmproxy.py +++ b/warcprox/mitmproxy.py @@ -502,10 +502,7 @@ class PooledMixIn(socketserver.ThreadingMixIn): def __init__(self, max_threads=None): self.active_requests = set() self.unaccepted_requests = 0 - if max_threads: - self.max_threads = max_threads - else: - self.max_threads = 100 + self.max_threads = max_threads or 100 self.pool = concurrent.futures.ThreadPoolExecutor(self.max_threads) self.logger.info("%s proxy threads", self.max_threads) @@ -595,11 +592,6 @@ class PooledMitmProxy(PooledMixIn, MitmProxy): request_queue_size = 4096 def __init__(self, options=warcprox.Options()): - if options.max_threads: - self.logger.info( - 'max_threads=%s set by command line option', - options.max_threads) - PooledMixIn.__init__(self, options.max_threads) self.profilers = collections.defaultdict(cProfile.Profile) From 5654bcbeb86c147d5b8d8df33f71d72cdf26b5b6 Mon Sep 17 00:00:00 2001 From: Noah Levitt Date: Mon, 20 Aug 2018 11:14:38 -0700 Subject: [PATCH 14/19] --quiet means NOTICE level logging and clean special log level code --- benchmarks/run-benchmarks.py | 2 +- tests/test_ensure_rethinkdb_tables.py | 2 +- tests/test_warcprox.py | 3 +-- warcprox/__init__.py | 16 ++++++++-------- warcprox/controller.py | 4 +--- warcprox/main.py | 4 ++-- 6 files changed, 14 insertions(+), 17 deletions(-) diff --git a/benchmarks/run-benchmarks.py b/benchmarks/run-benchmarks.py index 4491a8b..f273e96 100755 --- a/benchmarks/run-benchmarks.py +++ b/benchmarks/run-benchmarks.py @@ -194,7 +194,7 @@ if __name__ == '__main__': args = arg_parser.parse_args(args=sys.argv[1:]) if args.trace: - loglevel = warcprox.TRACE + loglevel = logging.TRACE elif args.verbose: loglevel = logging.DEBUG else: diff --git a/tests/test_ensure_rethinkdb_tables.py b/tests/test_ensure_rethinkdb_tables.py index 030cddb..f0649f4 100644 --- a/tests/test_ensure_rethinkdb_tables.py +++ b/tests/test_ensure_rethinkdb_tables.py @@ -30,7 +30,7 @@ import logging import sys logging.basicConfig( - stream=sys.stdout, level=warcprox.TRACE, + stream=sys.stdout, level=logging.TRACE, format='%(asctime)s %(process)d %(levelname)s %(threadName)s ' '%(name)s.%(funcName)s(%(filename)s:%(lineno)d) %(message)s') diff --git a/tests/test_warcprox.py b/tests/test_warcprox.py index fad7130..c41f457 100755 --- a/tests/test_warcprox.py +++ b/tests/test_warcprox.py @@ -90,8 +90,7 @@ def _send(self, data): # http_client.HTTPConnection.send = _send logging.basicConfig( - # stream=sys.stdout, level=logging.DEBUG, # level=warcprox.TRACE, - stream=sys.stdout, level=warcprox.TRACE, + stream=sys.stdout, level=logging.TRACE, format='%(asctime)s %(process)d %(levelname)s %(threadName)s ' '%(name)s.%(funcName)s(%(filename)s:%(lineno)d) %(message)s') logging.getLogger("requests.packages.urllib3").setLevel(logging.WARN) diff --git a/warcprox/__init__.py b/warcprox/__init__.py index 2dcc838..67cf654 100644 --- a/warcprox/__init__.py +++ b/warcprox/__init__.py @@ -266,21 +266,21 @@ def timestamp14(): return '{:%Y%m%d%H%M%S}'.format(now) # monkey-patch log levels TRACE and NOTICE -TRACE = 5 +logging.TRACE = (logging.NOTSET + logging.DEBUG) // 2 def _logger_trace(self, msg, *args, **kwargs): - if self.isEnabledFor(TRACE): - self._log(TRACE, msg, args, **kwargs) + if self.isEnabledFor(logging.TRACE): + self._log(logging.TRACE, msg, args, **kwargs) logging.Logger.trace = _logger_trace logging.trace = logging.root.trace -logging.addLevelName(TRACE, 'TRACE') +logging.addLevelName(logging.TRACE, 'TRACE') -NOTICE = (logging.INFO + logging.WARN) // 2 +logging.NOTICE = (logging.INFO + logging.WARN) // 2 def _logger_notice(self, msg, *args, **kwargs): - if self.isEnabledFor(NOTICE): - self._log(NOTICE, msg, args, **kwargs) + if self.isEnabledFor(logging.NOTICE): + self._log(logging.NOTICE, msg, args, **kwargs) logging.Logger.notice = _logger_notice logging.notice = logging.root.notice -logging.addLevelName(NOTICE, 'NOTICE') +logging.addLevelName(logging.NOTICE, 'NOTICE') import warcprox.controller as controller import warcprox.playback as playback diff --git a/warcprox/controller.py b/warcprox/controller.py index e89ecbb..9d20e71 100644 --- a/warcprox/controller.py +++ b/warcprox/controller.py @@ -299,9 +299,7 @@ class WarcproxController(object): status_info.update(self.proxy.status()) self.status_info = self.service_registry.heartbeat(status_info) - self.logger.log( - warcprox.TRACE, "status in service registry: %s", - self.status_info) + self.logger.trace('status in service registry: %s', self.status_info) def start(self): with self._start_stop_lock: diff --git a/warcprox/main.py b/warcprox/main.py index 0e2eddd..bf8d11e 100644 --- a/warcprox/main.py +++ b/warcprox/main.py @@ -290,11 +290,11 @@ def main(argv=None): args = parse_args(argv or sys.argv) if args.trace: - loglevel = warcprox.TRACE + loglevel = logging.TRACE elif args.verbose: loglevel = logging.DEBUG elif args.quiet: - loglevel = logging.WARNING + loglevel = logging.NOTICE else: loglevel = logging.INFO From 8dfb63f70d53a30849b0ac35200631eb613d1583 Mon Sep 17 00:00:00 2001 From: Noah Levitt Date: Mon, 20 Aug 2018 12:07:23 -0700 Subject: [PATCH 15/19] readable stack traces, thanks py.test --- .travis.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index c427b37..0ad15d4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -50,10 +50,10 @@ before_script: - docker ps script: -- py.test -v tests -- py.test -v --rethinkdb-dedup-url=rethinkdb://localhost/test1/dedup tests -- py.test -v --rethinkdb-big-table-url=rethinkdb://localhost/test2/captures tests -- py.test -v --rethinkdb-trough-db-url=rethinkdb://localhost/trough_configuration tests +- py.test -v --tb=native tests +- py.test -v --tb=native --rethinkdb-dedup-url=rethinkdb://localhost/test1/dedup tests +- py.test -v --tb=native --rethinkdb-big-table-url=rethinkdb://localhost/test2/captures tests +- py.test -v --tb=native --rethinkdb-trough-db-url=rethinkdb://localhost/trough_configuration tests after_script: - ps ww -fHe From 4f041723747ed3e3d64f05cb1ba0db764bf6df76 Mon Sep 17 00:00:00 2001 From: Noah Levitt Date: Mon, 20 Aug 2018 12:07:51 -0700 Subject: [PATCH 16/19] fix bug --- warcprox/warcproxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/warcprox/warcproxy.py b/warcprox/warcproxy.py index 17e0ebc..f50691a 100644 --- a/warcprox/warcproxy.py +++ b/warcprox/warcproxy.py @@ -288,7 +288,7 @@ class WarcProxyHandler(warcprox.mitmproxy.MitmProxyHandler): timestamp = datetime.datetime.utcnow() request_data = tempfile.SpooledTemporaryFile( - max_size=self.options.tmp_file_max_memory_size) + max_size=self._tmp_file_max_memory_size) payload_digest = hashlib.new(self.server.digest_algorithm) # XXX we don't support chunked uploads for now From 741436ddcbd0a31eb51c59962aba6ed00f816085 Mon Sep 17 00:00:00 2001 From: Noah Levitt Date: Mon, 17 Sep 2018 17:11:51 -0700 Subject: [PATCH 17/19] replace pencil drawing with nice diagram by James Kafader --- README.rst | 2 +- arch.jpg | Bin 52677 -> 0 bytes arch.svg | 433 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 434 insertions(+), 1 deletion(-) delete mode 100644 arch.jpg create mode 100644 arch.svg diff --git a/README.rst b/README.rst index a026937..b7b5c17 100644 --- a/README.rst +++ b/README.rst @@ -151,7 +151,7 @@ See a minimal example `here Architecture ============ -.. image:: arch.jpg +.. image:: arch.svg Warcprox is multithreaded. It has pool of http proxy threads (100 by default). When handling a request, a proxy thread records data from the remote server to diff --git a/arch.jpg b/arch.jpg deleted file mode 100644 index f3c855b8ca133819c3b80bc93c138099a86a41a2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 52677 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 269e9604c1608610e63f5e56f868c58b490f3f9b Mon Sep 17 00:00:00 2001 From: Noah Levitt Date: Wed, 19 Sep 2018 12:10:29 -0700 Subject: [PATCH 18/19] include warcprox host and port in filenames when using --crawl-log-dir, to avoid collisions (outside of warcprox itself, in most cases) with crawl logs written by other warcprox instances --- tests/test_warcprox.py | 33 ++++++++++++++++++++++----------- warcprox/crawl_log.py | 13 ++++++++----- warcprox/warcproxy.py | 7 +++++-- 3 files changed, 35 insertions(+), 18 deletions(-) diff --git a/tests/test_warcprox.py b/tests/test_warcprox.py index c41f457..91cf7c0 100755 --- a/tests/test_warcprox.py +++ b/tests/test_warcprox.py @@ -1716,8 +1716,14 @@ def test_slash_in_warc_prefix(warcprox_, http_daemon, archiving_proxies): def test_crawl_log(warcprox_, http_daemon, archiving_proxies): urls_before = warcprox_.proxy.running_stats.urls + hostname = socket.gethostname().split('.', 1)[0] + port = warcprox_.proxy.server_port + default_crawl_log_path = os.path.join( + warcprox_.options.crawl_log_dir, + 'crawl-%s-%s.log' % (hostname, port)) + try: - os.unlink(os.path.join(warcprox_.options.crawl_log_dir, 'crawl.log')) + os.unlink(default_crawl_log_path) except: pass @@ -1738,14 +1744,14 @@ def test_crawl_log(warcprox_, http_daemon, archiving_proxies): # wait for postfetch chain wait(lambda: warcprox_.proxy.running_stats.urls - urls_before == 2) - file = os.path.join(warcprox_.options.crawl_log_dir, 'test_crawl_log_1.log') + file = os.path.join( + warcprox_.options.crawl_log_dir, + 'test_crawl_log_1-%s-%s.log' % (hostname, port)) assert os.path.exists(file) assert os.stat(file).st_size > 0 - assert os.path.exists(os.path.join( - warcprox_.options.crawl_log_dir, 'crawl.log')) + assert os.path.exists(default_crawl_log_path) - crawl_log = open(os.path.join( - warcprox_.options.crawl_log_dir, 'crawl.log'), 'rb').read() + crawl_log = open(default_crawl_log_path, 'rb').read() # tests will fail in year 3000 :) assert re.match(b'\A2[^\n]+\n\Z', crawl_log) assert crawl_log[24:31] == b' 200 ' @@ -1766,8 +1772,7 @@ def test_crawl_log(warcprox_, http_daemon, archiving_proxies): 'contentSize', 'warcFilename', 'warcFileOffset'} assert extra_info['contentSize'] == 145 - crawl_log_1 = open(os.path.join( - warcprox_.options.crawl_log_dir, 'test_crawl_log_1.log'), 'rb').read() + crawl_log_1 = open(file, 'rb').read() assert re.match(b'\A2[^\n]+\n\Z', crawl_log_1) assert crawl_log_1[24:31] == b' 200 ' assert crawl_log_1[31:42] == b' 54 ' @@ -1798,7 +1803,9 @@ def test_crawl_log(warcprox_, http_daemon, archiving_proxies): # wait for postfetch chain wait(lambda: warcprox_.proxy.running_stats.urls - urls_before == 3) - file = os.path.join(warcprox_.options.crawl_log_dir, 'test_crawl_log_2.log') + file = os.path.join( + warcprox_.options.crawl_log_dir, + 'test_crawl_log_2-%s-%s.log' % (hostname, port)) assert os.path.exists(file) assert os.stat(file).st_size > 0 @@ -1831,7 +1838,9 @@ def test_crawl_log(warcprox_, http_daemon, archiving_proxies): # wait for postfetch chain wait(lambda: warcprox_.proxy.running_stats.urls - urls_before == 4) - file = os.path.join(warcprox_.options.crawl_log_dir, 'test_crawl_log_3.log') + file = os.path.join( + warcprox_.options.crawl_log_dir, + 'test_crawl_log_3-%s-%s.log' % (hostname, port)) assert os.path.exists(file) crawl_log_3 = open(file, 'rb').read() @@ -1869,7 +1878,9 @@ def test_crawl_log(warcprox_, http_daemon, archiving_proxies): # wait for postfetch chain wait(lambda: warcprox_.proxy.running_stats.urls - urls_before == 5) - file = os.path.join(warcprox_.options.crawl_log_dir, 'test_crawl_log_4.log') + file = os.path.join( + warcprox_.options.crawl_log_dir, + 'test_crawl_log_4-%s-%s.log' % (hostname, port)) assert os.path.exists(file) crawl_log_4 = open(file, 'rb').read() diff --git a/warcprox/crawl_log.py b/warcprox/crawl_log.py index 19dde96..2f7ea5e 100644 --- a/warcprox/crawl_log.py +++ b/warcprox/crawl_log.py @@ -24,11 +24,15 @@ import datetime import json import os import warcprox +import socket class CrawlLogger(object): def __init__(self, dir_, options=warcprox.Options()): self.dir = dir_ self.options = options + self.hostname = socket.gethostname().split('.', 1)[0] + + def start(self): if not os.path.exists(self.dir): logging.info('creating directory %r', self.dir) os.mkdir(self.dir) @@ -77,12 +81,11 @@ class CrawlLogger(object): pass line = b' '.join(fields) + b'\n' - if 'warc-prefix' in recorded_url.warcprox_meta: - filename = '%s.log' % recorded_url.warcprox_meta['warc-prefix'] - else: - filename = 'crawl.log' - + prefix = recorded_url.warcprox_meta.get('warc-prefix', 'crawl') + filename = '%s-%s-%s.log' % ( + prefix, self.hostname, self.options.server_port) crawl_log_path = os.path.join(self.dir, filename) + with open(crawl_log_path, 'ab') as f: f.write(line) diff --git a/warcprox/warcproxy.py b/warcprox/warcproxy.py index f50691a..cfe2314 100644 --- a/warcprox/warcproxy.py +++ b/warcprox/warcproxy.py @@ -507,12 +507,15 @@ class WarcProxy(SingleThreadedWarcProxy, warcprox.mitmproxy.PooledMitmProxy): def server_activate(self): http_server.HTTPServer.server_activate(self) - self.logger.info( + self.logger.notice( 'listening on %s:%s', self.server_address[0], self.server_address[1]) + # take note of actual port in case running with --port=0 so that other + # parts of warcprox have easy access to it + self.options.server_port = self.server_address[1] def server_close(self): - self.logger.info('shutting down') + self.logger.notice('shutting down') http_server.HTTPServer.server_close(self) self.remote_connection_pool.clear() From 57e1b82e3df8fb9ccc23f8b6d9150b32473b2819 Mon Sep 17 00:00:00 2001 From: Noah Levitt Date: Wed, 19 Sep 2018 13:03:59 -0700 Subject: [PATCH 19/19] bump version after merge --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 0e2e00f..cd5da0c 100755 --- a/setup.py +++ b/setup.py @@ -40,7 +40,7 @@ except: setuptools.setup( name='warcprox', - version='2.4b3.dev183', + version='2.4b3.dev184', description='WARC writing MITM HTTP/S proxy', url='https://github.com/internetarchive/warcprox', author='Noah Levitt',