webgrep-tool 1.20__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- webgrep/VERSION.txt +1 -0
- webgrep/__info__.py +20 -0
- webgrep/__init__.py +4 -0
- webgrep/__main__.py +752 -0
- webgrep_tool-1.20.dist-info/METADATA +844 -0
- webgrep_tool-1.20.dist-info/RECORD +10 -0
- webgrep_tool-1.20.dist-info/WHEEL +5 -0
- webgrep_tool-1.20.dist-info/entry_points.txt +2 -0
- webgrep_tool-1.20.dist-info/licenses/LICENSE +674 -0
- webgrep_tool-1.20.dist-info/top_level.txt +1 -0
webgrep/__main__.py
ADDED
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
# -*- coding: UTF-8 -*-
|
|
2
|
+
import argparse
|
|
3
|
+
import base64
|
|
4
|
+
import bs4
|
|
5
|
+
import coloredlogs
|
|
6
|
+
import gc
|
|
7
|
+
import jsbeautifier
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
import requests
|
|
13
|
+
import shutil
|
|
14
|
+
import signal
|
|
15
|
+
import sys
|
|
16
|
+
import urllib3
|
|
17
|
+
from os.path import basename, dirname, exists, join, splitext
|
|
18
|
+
from subprocess import call, Popen, PIPE, STDOUT
|
|
19
|
+
from urllib.parse import unquote, urlparse, urljoin
|
|
20
|
+
from urllib.request import getproxies, url2pathname
|
|
21
|
+
|
|
22
|
+
from .__info__ import __author__, __copyright__, __repository__, __version__
|
|
23
|
+
|
|
24
|
+
# disable annoying requests warnings
|
|
25
|
+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
26
|
+
logging.getLogger("requests").setLevel(logging.CRITICAL)
|
|
27
|
+
logger = logging.getLogger()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
__doc__ = """Search for PATTERN in each input URL and its related resources (images, scripts and style sheets).
|
|
31
|
+
By default,
|
|
32
|
+
- resources are NOT downloaded
|
|
33
|
+
- response HTTP headers are NOT included in grepping ; use '--include-headers'
|
|
34
|
+
- PATTERN is a basic regular expression (BRE) ; use '-E' for extended (ERE)
|
|
35
|
+
Important note: webgrep does not handle recursion (in other words, it does not spider additional web pages).
|
|
36
|
+
Examples:
|
|
37
|
+
webgrep example http://www.example.com # will only grep on HTML code
|
|
38
|
+
webgrep -r example http://www.example.com # will only grep on LOCAL images, ...
|
|
39
|
+
webgrep -R example http://www.example.com # will only grep on ALL images, ...
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# -------------------- CONSTANTS SECTION --------------------
|
|
44
|
+
CSS_URL_REGEX = re.compile(rb'url\(([^)]+)\)')
|
|
45
|
+
DATE_FORMAT = '%H:%M:%S'
|
|
46
|
+
DEVNULL = open(os.devnull, 'w')
|
|
47
|
+
DATA_NAME = '{}-{:>03}.{}'
|
|
48
|
+
LOG_FORMAT = '%(asctime)s [%(levelname)s] %(message)s'
|
|
49
|
+
PROXIES = getproxies() or {k.split('_')[0].lower(): os.environ[k] for k in ['HTTP_PROXY', 'HTTPS_PROXY', 'FTP_PROXY'] \
|
|
50
|
+
if k in os.environ.keys()}
|
|
51
|
+
TEMP_DIR = '/tmp/webgrep'
|
|
52
|
+
CACHE_FILE = "cache.json"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# -------------------- HANDLERS SECTION ---------------------
|
|
56
|
+
# Categories:
|
|
57
|
+
# - preprocessors: applied on downloaded resources ; these REPLACE the resource
|
|
58
|
+
# - tools: applied after resource download and processing ; these DERIVE new resources
|
|
59
|
+
# Naming convention:
|
|
60
|
+
# [resource type]_[preprocessors|tools]
|
|
61
|
+
# e.g. STYLE_TOOLS, IMAGE_PREPROCESSORS
|
|
62
|
+
# Structure: dictionary with
|
|
63
|
+
# - key: the name of the binary or module
|
|
64
|
+
# - value: tuple of the form (type, handler, message if not exists)
|
|
65
|
+
# NB: the handler always takes a Resource object in argument
|
|
66
|
+
# Handlers are automatically set as attributes in 'args'
|
|
67
|
+
b = lambda c: c if isinstance(c, bytes) else bytes(c, 'utf-8')
|
|
68
|
+
s = lambda c: c.decode("utf-8") if isinstance(c, bytes) else c
|
|
69
|
+
get_cmd = lambda t: lambda r: Popen([t, r.rel_fn], stdout=PIPE, stderr=DEVNULL, cwd=args.tmp).communicate()[0]
|
|
70
|
+
|
|
71
|
+
def css_unminifier(res, indent=2, maxlen=256):
|
|
72
|
+
""" Minimalistic CSS unminifying function. """
|
|
73
|
+
if res.type != "style":
|
|
74
|
+
return
|
|
75
|
+
# CSS is considered minified if any line is longer than a given length
|
|
76
|
+
res.content, i = b(res.content), indent
|
|
77
|
+
if any([len(l) > maxlen for l in res.content.split(b('\n'))]):
|
|
78
|
+
res.content = re.sub(rb"\*/", b"*/\r\n", res.content)
|
|
79
|
+
res.content = re.sub(rb"\{", b" {\r\n" + b" " * i, res.content)
|
|
80
|
+
res.content = re.sub(rb";", b";\r\n" + b" " * i, res.content)
|
|
81
|
+
res.content = re.sub(rb"\}", b";\r\n}\r\n", res.content)
|
|
82
|
+
return res.content
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def inline_items(tag, rtype=None, attrs=None):
|
|
86
|
+
""" Decorator for extracting inline items from a Web page. """
|
|
87
|
+
if not rtype:
|
|
88
|
+
rtype = tag
|
|
89
|
+
if not attrs:
|
|
90
|
+
attrs = {}
|
|
91
|
+
def _inline_items(page):
|
|
92
|
+
""" Inline item extraction function. """
|
|
93
|
+
for item in page.soup.find_all(tag, **attrs):
|
|
94
|
+
inline = "data:inline/{};none,...".format(rtype)
|
|
95
|
+
with Resource(inline, page) as res:
|
|
96
|
+
res.type = rtype
|
|
97
|
+
if not hasattr(res, "content"): # not in cache
|
|
98
|
+
logger.debug("> Extracting {}".format(res.abs_fn))
|
|
99
|
+
res.content = item.text or item.html or item.string
|
|
100
|
+
res.preprocess()
|
|
101
|
+
with open(res.abs_fn, 'wb') as f:
|
|
102
|
+
f.write(b(res.content))
|
|
103
|
+
item.decompose()
|
|
104
|
+
res.grep().handle()
|
|
105
|
+
return b(str(page.soup))
|
|
106
|
+
return _inline_items
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def steghide(res):
|
|
110
|
+
""" Steghide handler for applying steganography on images (only trying an empty passphrase). """
|
|
111
|
+
if res.type != "image":
|
|
112
|
+
return
|
|
113
|
+
tmp_file = join(args.tmp, "steghide-extracted-text")
|
|
114
|
+
cmd = ['steghide', 'extract', '-sf', res.abs_fn, '-p', "", '-f', '-xf', tmp_file]
|
|
115
|
+
p = Popen(cmd, stdout=PIPE, stderr=PIPE)
|
|
116
|
+
_, output = p.communicate()
|
|
117
|
+
if b(tmp_file) in output:
|
|
118
|
+
with open(tmp_file, 'rb') as f:
|
|
119
|
+
content = f.read()
|
|
120
|
+
os.remove(tmp_file)
|
|
121
|
+
return content
|
|
122
|
+
else:
|
|
123
|
+
return b("")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def tesseract(res):
|
|
127
|
+
""" Tesseract handler for applying OCR on images. """
|
|
128
|
+
if res.type != "image":
|
|
129
|
+
return
|
|
130
|
+
output = []
|
|
131
|
+
for i in range(3,11):
|
|
132
|
+
cmd = ['tesseract', res.rel_fn, 'stdout', '--psm', str(i)]
|
|
133
|
+
result = Popen(cmd, stdout=PIPE, stderr=DEVNULL, cwd=args.tmp).communicate()[0].strip()
|
|
134
|
+
if len(result) > 0:
|
|
135
|
+
for line in result.split(b('\n')):
|
|
136
|
+
if line not in output:
|
|
137
|
+
output.append(line)
|
|
138
|
+
return b('\n').join(output)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
RESOURCE_TYPES = ['page', 'image', 'script', 'style']
|
|
142
|
+
IMAGE_TOOLS = {
|
|
143
|
+
"exiftool": ('binary', get_cmd("exiftool"), "Binary required for getting image EXIF info ;\n consider running "
|
|
144
|
+
"'sudo apt-get install libimage-exiftool-perl'"),
|
|
145
|
+
"strings": ('binary', get_cmd("strings"), "Binary required for getting strings from downloaded files ;\n consider "
|
|
146
|
+
"running 'sudo apt-get install binutils'"),
|
|
147
|
+
"steghide": ('binary', steghide, "Binary required for applying steganography on images ;\n consider running "
|
|
148
|
+
"'sudo apt-get install steghide'"),
|
|
149
|
+
"tesseract": ('binary', tesseract, "Binary required for trying OCR on images ;\n consider running "
|
|
150
|
+
"'sudo apt-get install tesseract-ocr'"),
|
|
151
|
+
}
|
|
152
|
+
PAGE_PREPROCESSORS = {
|
|
153
|
+
"inline-script": ('function', inline_items("script", attrs={'src': False}), None),
|
|
154
|
+
"inline-style": ('function', inline_items("style"), None),
|
|
155
|
+
}
|
|
156
|
+
SCRIPT_PREPROCESSORS = {
|
|
157
|
+
"jsbeautifier": ('module', lambda r: b(jsbeautifier.beautify(s(r.content))), "Python library required for "
|
|
158
|
+
"deobfuscating Javascript ;\n consider running 'pip install jsbeautifier'"),
|
|
159
|
+
}
|
|
160
|
+
STYLE_PREPROCESSORS = {
|
|
161
|
+
"unminifier": ('function', css_unminifier, None),
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# -------------------- FUNCTIONS SECTION --------------------
|
|
166
|
+
def __exit_handler(signal=None, frame=None, code=0):
|
|
167
|
+
""" Exit handler.
|
|
168
|
+
|
|
169
|
+
:param signal: signal number
|
|
170
|
+
:param stack: stack frame
|
|
171
|
+
:param code: exit code
|
|
172
|
+
"""
|
|
173
|
+
if 'args' in globals():
|
|
174
|
+
if args.keep:
|
|
175
|
+
if args.cache:
|
|
176
|
+
logger.debug("Saving cache at {}".format(args.cache_file))
|
|
177
|
+
with open(args.cache_file, 'w') as f:
|
|
178
|
+
json.dump(args.cache, f, indent=2)
|
|
179
|
+
logger.info("Temporary files are available at {}".format(args.tmp))
|
|
180
|
+
else:
|
|
181
|
+
logger.debug("Removing temporary folder {}".format(args.tmp))
|
|
182
|
+
shutil.rmtree(args.tmp, ignore_errors=True)
|
|
183
|
+
logging.shutdown()
|
|
184
|
+
sys.exit(code)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def __installed(item, itype, message=None):
|
|
188
|
+
""" Item existence check ; display message if the item is not installed and return boolean so that it can be
|
|
189
|
+
determined if the item can be used.
|
|
190
|
+
|
|
191
|
+
:param item: item to be checked
|
|
192
|
+
:param itype: item type (binary | module | function)
|
|
193
|
+
:param message: message to be displayed if the item is not installed
|
|
194
|
+
:return: True if the item is installed, False otherwise.
|
|
195
|
+
"""
|
|
196
|
+
if itype == 'binary':
|
|
197
|
+
try:
|
|
198
|
+
call([item, '--help'], stdout=DEVNULL, stderr=STDOUT)
|
|
199
|
+
return True
|
|
200
|
+
except OSError:
|
|
201
|
+
if message is not None:
|
|
202
|
+
logger.warning(message)
|
|
203
|
+
return False
|
|
204
|
+
elif itype == 'module':
|
|
205
|
+
try:
|
|
206
|
+
globals()[item] = __import__(item)
|
|
207
|
+
return True
|
|
208
|
+
except ImportError:
|
|
209
|
+
if message is not None:
|
|
210
|
+
logger.warning(message)
|
|
211
|
+
return False
|
|
212
|
+
elif itype == 'function':
|
|
213
|
+
# no check required ; this relates to a locally declared function
|
|
214
|
+
return True
|
|
215
|
+
else:
|
|
216
|
+
logger.warning("Unknown item type ({})".format(itype))
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# --------------------- CLASSES SECTION ---------------------
|
|
220
|
+
class ArgCollectOption(argparse.Action):
|
|
221
|
+
""" Argparse action for handling keyword arguments collection in grep_opts so that these can be passed to grep. """
|
|
222
|
+
def __call__(self, parser, args, values, option_string=None):
|
|
223
|
+
if not hasattr(args, "grep_opts") or args.grep_opts is None:
|
|
224
|
+
args.grep_opts = []
|
|
225
|
+
if values is not None:
|
|
226
|
+
if len(self.dest) == 1:
|
|
227
|
+
args.grep_opts.append("-" + self.dest)
|
|
228
|
+
args.grep_opts.append("{}".format(values))
|
|
229
|
+
elif len(self.dest) > 1:
|
|
230
|
+
dest = self.dest.replace('_', '-')
|
|
231
|
+
args.grep_opts.extend(["--" + dest, values])
|
|
232
|
+
delattr(args, self.dest)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class ArgVersion(argparse.Action):
|
|
236
|
+
""" Display version by calling -V/--version. """
|
|
237
|
+
def __call__(self, parser, args, values, option_string=None):
|
|
238
|
+
print("webgrep {}".format(__version__))
|
|
239
|
+
print(__copyright__)
|
|
240
|
+
print("\nWritten by {}, see <{}>.".format(__author__, __repository__))
|
|
241
|
+
globals()['__exit_handler']()
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class GetHeader(argparse.Action):
|
|
245
|
+
""" Collect HTTP headers. """
|
|
246
|
+
def __call__(self, parser, args, values, option_string=None):
|
|
247
|
+
if not hasattr(args, "headers") or args.headers is None:
|
|
248
|
+
args.headers = {}
|
|
249
|
+
if values is not None:
|
|
250
|
+
args.headers[self.dest.capitalize()] = values
|
|
251
|
+
delattr(args, self.dest)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
#based on: https://stackoverflow.com/questions/10123929/fetch-a-file-from-a-local-url-with-python-requests
|
|
255
|
+
class LocalFileAdapter(requests.adapters.BaseAdapter):
|
|
256
|
+
""" Protocol Adapter to allow Requests to GET file:// URL's. """
|
|
257
|
+
@staticmethod
|
|
258
|
+
def _check_path(method, path):
|
|
259
|
+
"""Return an HTTP status for the given filesystem path."""
|
|
260
|
+
if method.lower() in ["delete", "option", "put", "trace"]:
|
|
261
|
+
return 501, "Not Implemented"
|
|
262
|
+
elif method.lower() not in ["get", "head"]:
|
|
263
|
+
return 405, "Method Not Allowed"
|
|
264
|
+
elif os.path.isdir(path):
|
|
265
|
+
return 400, "Path Not A File"
|
|
266
|
+
elif not os.path.isfile(path):
|
|
267
|
+
return 404, "File Not Found"
|
|
268
|
+
elif not os.access(path, os.R_OK):
|
|
269
|
+
return 403, "Access Denied"
|
|
270
|
+
return 200, "OK"
|
|
271
|
+
|
|
272
|
+
def send(self, request, **kwargs):
|
|
273
|
+
""" Return the file specified by the given request. """
|
|
274
|
+
path, resp = os.path.normcase(os.path.normpath(url2pathname(request.path_url))), requests.Response()
|
|
275
|
+
resp.status_code, resp.reason = self._check_path(request.method, path)
|
|
276
|
+
if resp.status_code == 200 and request.method.lower() != "head":
|
|
277
|
+
try:
|
|
278
|
+
resp.raw = open(path, 'rb')
|
|
279
|
+
except (OSError, IOError) as err:
|
|
280
|
+
resp.status_code = 500
|
|
281
|
+
resp.reason = str(err)
|
|
282
|
+
resp.url = request.url.decode('utf-8') if isinstance(request.url, bytes) else request.url
|
|
283
|
+
resp.request = request
|
|
284
|
+
resp.connection = self
|
|
285
|
+
return resp
|
|
286
|
+
|
|
287
|
+
def close(self):
|
|
288
|
+
pass
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class ProxySetting(argparse.Action):
|
|
292
|
+
""" Manually set proxy setting. """
|
|
293
|
+
def __call__(self, parser, args, values, option_string=None):
|
|
294
|
+
if values is not None:
|
|
295
|
+
PROXIES[self.dest] = values
|
|
296
|
+
delattr(args, self.dest)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class Resource(object):
|
|
300
|
+
""" Class for downloading web page and its related resources and for grepping each downloaded data. """
|
|
301
|
+
grep_exclude = ["image"]
|
|
302
|
+
headers = {'User-Agent': "Mozilla/5.0 (X11; Ubuntu; Linux x86_64;"
|
|
303
|
+
" rv:50.0) Gecko/20100101 Firefox/50.0",
|
|
304
|
+
'Accept': "text/html,application/xhtml+xml,application/"
|
|
305
|
+
"xml;q=0.9,*/*;q=0.8",
|
|
306
|
+
'Accept-Language': "en-US,en;q=0.5",
|
|
307
|
+
'Accept-Encoding': "gzip, deflate",
|
|
308
|
+
'Connection': "keep-alive",
|
|
309
|
+
'DNT': "1",
|
|
310
|
+
'Upgrade-Insecure-Requests': "1"}
|
|
311
|
+
|
|
312
|
+
def __init__(self, rurl, parent=None):
|
|
313
|
+
self.url = rurl
|
|
314
|
+
self.parent = parent
|
|
315
|
+
self.path = dirname(rurl) + "/"
|
|
316
|
+
parsed = urlparse(rurl)
|
|
317
|
+
self.base = "{}://{}".format(parsed.scheme, parsed.netloc)
|
|
318
|
+
self.netloc = parsed.netloc
|
|
319
|
+
self._download = True
|
|
320
|
+
self._error = False
|
|
321
|
+
self._raw_data = 0
|
|
322
|
+
self._main = parent is None or parsed.scheme == "data"
|
|
323
|
+
self._same_origin = parent is None or self.base == parent.base or parsed.scheme == 'data' # i.e. embedded image
|
|
324
|
+
# check if the resource is an embedded data
|
|
325
|
+
if parsed.scheme == "data":
|
|
326
|
+
self._download = False
|
|
327
|
+
# first, decompose the path (data:[parsed.path])
|
|
328
|
+
# form: [category]/[type];[encoding],[data]
|
|
329
|
+
# e.g.: image/png;base64,...
|
|
330
|
+
try:
|
|
331
|
+
rtype, data = parsed.path.split(";", 1)
|
|
332
|
+
self.type, ext = rtype.split('/', 1)
|
|
333
|
+
enc, data = data.split(",", 1)
|
|
334
|
+
except ValueError: # occurs e.g. with "image/svg+xml,%3Csvg..."
|
|
335
|
+
rtype, data = parsed.path.split(",", 1)
|
|
336
|
+
self.type, ext = rtype.split('/', 1)
|
|
337
|
+
ext = ext.split("+", 1)[0]
|
|
338
|
+
data = unquote(data)
|
|
339
|
+
enc = "none"
|
|
340
|
+
try:
|
|
341
|
+
if enc.strip() not in ["none", "base64"]:
|
|
342
|
+
raise Exception("Bad image encoding")
|
|
343
|
+
except Exception as e:
|
|
344
|
+
logger.warning("{} (from {})".format(str(e), self.parent.rel_fn))
|
|
345
|
+
self._error = True
|
|
346
|
+
return
|
|
347
|
+
fn = DATA_NAME.format(self.type, self.parent._raw_data, ext)
|
|
348
|
+
self.rel_fn = "{}_{}".format(self.parent.rel_fn, fn)
|
|
349
|
+
self.abs_fn = "{}_{}".format(self.parent.abs_fn, fn)
|
|
350
|
+
if len(data.strip('.')) > 0:
|
|
351
|
+
with open(self.abs_fn, 'wb') as f:
|
|
352
|
+
content = data if enc == "none" else base64.b64decode(data)
|
|
353
|
+
f.write(b(content))
|
|
354
|
+
self.parent._raw_data += 1
|
|
355
|
+
# otherwise, compute resource paths
|
|
356
|
+
else:
|
|
357
|
+
url_path = parsed.path
|
|
358
|
+
fn = basename(url_path) or "index.html"
|
|
359
|
+
fp = dirname(url_path).strip('/')
|
|
360
|
+
self.rel_fn = join(self.netloc, fp, fn)
|
|
361
|
+
self.abs_fn = join(args.tmp, self.rel_fn)
|
|
362
|
+
abs_fp = dirname(self.abs_fn)
|
|
363
|
+
if not exists(abs_fp):
|
|
364
|
+
os.makedirs(abs_fp)
|
|
365
|
+
|
|
366
|
+
def __enter__(self):
|
|
367
|
+
return self._load()
|
|
368
|
+
|
|
369
|
+
def __exit__(self, *unused):
|
|
370
|
+
self._cache()
|
|
371
|
+
del self
|
|
372
|
+
gc.collect()
|
|
373
|
+
|
|
374
|
+
def _allowed(self):
|
|
375
|
+
""" Handle only same-origin or also external resources. """
|
|
376
|
+
return self._main or args.inc_all or (args.inc_local and self._same_origin)
|
|
377
|
+
|
|
378
|
+
def _cache(self):
|
|
379
|
+
""" Resource caching for use with '--keep-files'. """
|
|
380
|
+
if args.cache is not None and self.rel_fn not in args.cache.get(url, {}):
|
|
381
|
+
logger.debug("> Registering '{}' to cache".format(self.rel_fn))
|
|
382
|
+
if not hasattr(self, "type"):
|
|
383
|
+
self._update_type()
|
|
384
|
+
if self.type != "undefined":
|
|
385
|
+
args.cache.setdefault(url, {})
|
|
386
|
+
args.cache[url][self.rel_fn] = self.type
|
|
387
|
+
|
|
388
|
+
def _load(self):
|
|
389
|
+
""" Resource loading for use with '--keep-files', allowing to re-grep on different keywords without
|
|
390
|
+
re-downloading everything. """
|
|
391
|
+
if exists(self.abs_fn) and args.cache is not None and self.rel_fn in args.cache.get(url, {}):
|
|
392
|
+
with open(self.abs_fn, 'rb') as f:
|
|
393
|
+
self.content = f.read()
|
|
394
|
+
self.type = args.cache[url][self.rel_fn]
|
|
395
|
+
logger.debug("> Loading '{}' from cache".format(self.rel_fn))
|
|
396
|
+
return self
|
|
397
|
+
else:
|
|
398
|
+
return self.download()
|
|
399
|
+
|
|
400
|
+
def _update_type(self):
|
|
401
|
+
""" Update resource type based on HTTP response content type. """
|
|
402
|
+
if self._error:
|
|
403
|
+
self.type = "failure"
|
|
404
|
+
return
|
|
405
|
+
try:
|
|
406
|
+
ct = self.response.headers['Content-Type'].split(';')[0].split(',')
|
|
407
|
+
except:
|
|
408
|
+
self.type = "undefined"
|
|
409
|
+
return
|
|
410
|
+
_, ext = splitext(self.url)
|
|
411
|
+
# only consider the following MIME types
|
|
412
|
+
if any(c.startswith("image") for c in ct):
|
|
413
|
+
self.type = "image"
|
|
414
|
+
elif "text/css" in ct:
|
|
415
|
+
self.type = "style"
|
|
416
|
+
elif "application/javascript" in ct or "text/javascript" in ct or \
|
|
417
|
+
(any(c in ["application/octet-stream", "text/plain", "text/html"] for c in ct) and ext == ".js"):
|
|
418
|
+
self.type = "script"
|
|
419
|
+
elif "text/html" in ct:
|
|
420
|
+
self.type = "page"
|
|
421
|
+
else:
|
|
422
|
+
self.type = "other"
|
|
423
|
+
logger.debug("> Resource type set to '{}'".format(self.type))
|
|
424
|
+
|
|
425
|
+
def download(self):
|
|
426
|
+
""" Download the resource then save it to a temporary folder. Afterwards, trigger grep and particular handling.
|
|
427
|
+
"""
|
|
428
|
+
if not self._download or not self._allowed():
|
|
429
|
+
return self
|
|
430
|
+
logger.debug("> Downloading {}".format(self.url))
|
|
431
|
+
self.headers.update(args.headers)
|
|
432
|
+
req = requests.Request('GET', self.url, headers=self.headers).prepare()
|
|
433
|
+
try:
|
|
434
|
+
resp = session.send(req, proxies=PROXIES if args.proxy else {}, verify=False, stream=True)
|
|
435
|
+
except requests.exceptions.ProxyError as e:
|
|
436
|
+
if bool(PROXIES):
|
|
437
|
+
# try next requests disabling the proxy
|
|
438
|
+
logger.debug("Disabling proxy settings")
|
|
439
|
+
globals()['PROXIES'] = {}
|
|
440
|
+
resp = session.send(req, proxies=PROXIES if args.proxy else {}, verify=False, stream=True)
|
|
441
|
+
else: # proxy already disabled, just throw the exception
|
|
442
|
+
raise e
|
|
443
|
+
# attach HTTP headers if they are to be grepped
|
|
444
|
+
if args.inc_headers:
|
|
445
|
+
self.resp_headers = resp.headers
|
|
446
|
+
# print debug information if necessary
|
|
447
|
+
Resource.pprint_req(req)
|
|
448
|
+
Resource.pprint_resp(resp)
|
|
449
|
+
# adjust resource type if necessary
|
|
450
|
+
self.response = resp
|
|
451
|
+
self._error = resp.status_code != 200
|
|
452
|
+
self._update_type()
|
|
453
|
+
if resp.status_code == 200:
|
|
454
|
+
# images handled separately in chunks, no preprocessor applied
|
|
455
|
+
if self.type == "image":
|
|
456
|
+
with open(self.abs_fn, 'wb') as f:
|
|
457
|
+
for chunk in resp:
|
|
458
|
+
f.write(b(chunk))
|
|
459
|
+
self.content = ''
|
|
460
|
+
# other types of resources are handled with preprocessors
|
|
461
|
+
else:
|
|
462
|
+
self.content = resp.text.encode(resp.encoding) if resp.encoding is not None else resp.text
|
|
463
|
+
if self.type == "page":
|
|
464
|
+
self.soup = bs4.BeautifulSoup(self.content, 'html.parser')
|
|
465
|
+
self.preprocess()
|
|
466
|
+
with open(self.abs_fn, 'wb') as f:
|
|
467
|
+
f.write(b(self.content))
|
|
468
|
+
self._cache()
|
|
469
|
+
elif resp.status_code == 204: # No Content
|
|
470
|
+
pass
|
|
471
|
+
else:
|
|
472
|
+
reason = "({} - {})".format(resp.status_code, resp.reason)
|
|
473
|
+
logger.error("Failed to get {} {}".format(self.url, reason))
|
|
474
|
+
return self
|
|
475
|
+
|
|
476
|
+
def grep(self):
|
|
477
|
+
""" Call built-in grep command on the downloaded resource then remove it. """
|
|
478
|
+
if not self._allowed() or self._error or self.type in self.grep_exclude: # or not hasattr(self, "type")
|
|
479
|
+
return self
|
|
480
|
+
logger.debug("> Grepping {}".format(self.abs_fn))
|
|
481
|
+
# make the command from input arguments
|
|
482
|
+
tokens = ['grep', '--color=always']
|
|
483
|
+
if hasattr(args, "grep_opts"):
|
|
484
|
+
tokens.extend(args.grep_opts or [])
|
|
485
|
+
if hasattr(args, "pattern") and args.pattern is not None:
|
|
486
|
+
tokens.append(args.pattern)
|
|
487
|
+
tokens.append(self.rel_fn)
|
|
488
|
+
# then execute it ; no shell, as the file name is derived from a URL that a Web page controls
|
|
489
|
+
logger.debug(">> Command: {}".format(tokens))
|
|
490
|
+
out = Popen(tokens, stdout=PIPE, stderr=STDOUT, cwd=args.tmp).communicate()[0]
|
|
491
|
+
if len(out) > 0:
|
|
492
|
+
print(out.decode('utf-8'))
|
|
493
|
+
return self
|
|
494
|
+
|
|
495
|
+
def handle(self):
|
|
496
|
+
""" Handle the downloaded resource according to its type.
|
|
497
|
+
- web page: search for images (also handle raw images), scripts and style sheets
|
|
498
|
+
- images: apply multiple tools then only grep on these results
|
|
499
|
+
- scripts: try to deobfuscate then also grep on these results
|
|
500
|
+
- style sheets: search for image references then download them for normal image handling
|
|
501
|
+
"""
|
|
502
|
+
if not self._allowed() or self._error:
|
|
503
|
+
return self
|
|
504
|
+
# check for headers if they are to be included
|
|
505
|
+
if args.inc_headers:
|
|
506
|
+
with Resource("data:other/headers;none,...", self) as res:
|
|
507
|
+
if not hasattr(res, "content"): # not in cache
|
|
508
|
+
# 'resp_headers' does not exist when --keep-files was used as the resource is not downloaded again
|
|
509
|
+
if hasattr(self, "resp_headers"): # download performed
|
|
510
|
+
with open(res.abs_fn, 'wb') as f:
|
|
511
|
+
f.write(b('\n'.join(": ".join(item) for item in self.resp_headers.items())))
|
|
512
|
+
res.grep()
|
|
513
|
+
# then, derive new resources by applying available tools as defined in the global constants (see HANDLERS
|
|
514
|
+
# SECTION above)
|
|
515
|
+
attr = "{}_tools".format(self.type)
|
|
516
|
+
if hasattr(args, attr):
|
|
517
|
+
# create a new resource for each time a tool is executed
|
|
518
|
+
for tool, handler in getattr(args, attr):
|
|
519
|
+
raw = "data:{}/txt;none,...".format(tool)
|
|
520
|
+
with Resource(raw, self) as res:
|
|
521
|
+
res.type = "derived"
|
|
522
|
+
if not hasattr(res, "content"): # not in cache
|
|
523
|
+
logger.debug("> Deriving {}".format(res.abs_fn))
|
|
524
|
+
with open(res.abs_fn, 'wb') as f:
|
|
525
|
+
f.write(b(handler(self)))
|
|
526
|
+
res.grep()
|
|
527
|
+
# now, handle resource for some particular types
|
|
528
|
+
if self.type == "page":
|
|
529
|
+
if not args.inc_local and not args.inc_all:
|
|
530
|
+
return
|
|
531
|
+
if not hasattr(self, "soup"):
|
|
532
|
+
self.soup = bs4.BeautifulSoup(self.content, 'html.parser')
|
|
533
|
+
# download images
|
|
534
|
+
for img in self.soup.find_all("img", src=True):
|
|
535
|
+
with Resource(urljoin(self.path, img['src']), self) as res:
|
|
536
|
+
res.grep().handle()
|
|
537
|
+
# download scripts
|
|
538
|
+
for script in self.soup.find_all("script", src=True):
|
|
539
|
+
with Resource(urljoin(self.path, script.attrs['src']), self) as res:
|
|
540
|
+
res.grep().handle()
|
|
541
|
+
# download styles
|
|
542
|
+
for link in self.soup.find_all("link", href=True, rel="stylesheet"):
|
|
543
|
+
with Resource(urljoin(self.path, link.attrs['href']), self) as res:
|
|
544
|
+
res.grep().handle()
|
|
545
|
+
elif self.type == "style":
|
|
546
|
+
# search for image URLs in the style sheet
|
|
547
|
+
for u in CSS_URL_REGEX.findall(self.content):
|
|
548
|
+
u = u.strip(b("\'\"")).decode()
|
|
549
|
+
if u.startswith("#"): # exclude anchor tags
|
|
550
|
+
continue
|
|
551
|
+
with Resource(urljoin(self.path, u), self) as res:
|
|
552
|
+
res.grep().handle()
|
|
553
|
+
|
|
554
|
+
def preprocess(self):
|
|
555
|
+
""" Preprocess the resource according to its type. """
|
|
556
|
+
attr = '{}_preprocessors'.format(self.type)
|
|
557
|
+
if hasattr(args, attr):
|
|
558
|
+
for tool, handler in getattr(args, attr):
|
|
559
|
+
logger.debug("> Preprocessing with {}".format(tool))
|
|
560
|
+
self.content = handler(self)
|
|
561
|
+
self.content = b(self.content)
|
|
562
|
+
|
|
563
|
+
@staticmethod
|
|
564
|
+
def pprint_req(request):
|
|
565
|
+
""" Pretty print method for debugging HTTP communication. """
|
|
566
|
+
data = "\n\n {}\n".format(request.body) if request.method == "POST" else "\n"
|
|
567
|
+
logger.debug("Sent request:\n {} {}\n{}".format(request.method, request.url,
|
|
568
|
+
'\n'.join(' {}: {}'.format(k, v) for k, v in sorted(request.headers.items()))) + data)
|
|
569
|
+
|
|
570
|
+
@staticmethod
|
|
571
|
+
def pprint_resp(response):
|
|
572
|
+
""" Pretty print method for debugging HTTP communication. """
|
|
573
|
+
logger.debug("Received response:\n {} {}\n{}\n".format(response.status_code, response.reason,
|
|
574
|
+
'\n'.join(' {}: {}'.format(k, v) for k, v in sorted(response.headers.items()))))
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
# ----------------------- MAIN SECTION ----------------------
|
|
578
|
+
def main():
|
|
579
|
+
global logger, session, args, url
|
|
580
|
+
# bind termination signal (Ctrl+C) to exit handler
|
|
581
|
+
signal.signal(signal.SIGINT, __exit_handler)
|
|
582
|
+
parser = argparse.ArgumentParser(
|
|
583
|
+
prog="webgrep", description=__doc__, add_help=False,
|
|
584
|
+
usage="%(prog)s [OPTION]... PATTERN [URL]...",
|
|
585
|
+
epilog="Please report bugs on GitHub: {}/issues/new".format(__repository__),
|
|
586
|
+
formatter_class=argparse.RawTextHelpFormatter,
|
|
587
|
+
)
|
|
588
|
+
# argument groups are willingly the same as in grep
|
|
589
|
+
regex = parser.add_argument_group("Regexp selection and interpretation")
|
|
590
|
+
regex_pat = regex.add_mutually_exclusive_group()
|
|
591
|
+
regex_pat.add_argument("-e", "--regexp", action=ArgCollectOption, help="use PATTERN for matching")
|
|
592
|
+
regex_pat.add_argument("-f", "--file", action=ArgCollectOption, help="obtain PATTERN from FILE")
|
|
593
|
+
regex_type = regex.add_mutually_exclusive_group()
|
|
594
|
+
regex_type.add_argument("-E", "--extended-regexp", dest="grep_opts", const="-E", action="append_const",
|
|
595
|
+
help="PATTERN is an extended regular expression (ERE)")
|
|
596
|
+
regex_type.add_argument("-F", "--fixed-strings", dest="grep_opts", const="-F", action="append_const",
|
|
597
|
+
help="PATTERN is a set of newline-separated fixed strings")
|
|
598
|
+
regex_type.add_argument("-G", "--basic-regexp", dest="grep_opts", const="-G", action="append_const",
|
|
599
|
+
help="PATTERN is a basic regular expression (BRE)")
|
|
600
|
+
regex_type.add_argument("-P", "--perl-regexp", dest="grep_opts", const="-P", action="append_const",
|
|
601
|
+
help="PATTERN is a Perl regular expression")
|
|
602
|
+
regex.add_argument("-i", "--ignore-case", dest="grep_opts", const="-i", action="append_const",
|
|
603
|
+
help="ignore case distinctions")
|
|
604
|
+
regex.add_argument("-w", "--word-regexp", dest="grep_opts", const="-w", action="append_const",
|
|
605
|
+
help="force PATTERN to match only whole words")
|
|
606
|
+
regex.add_argument("-x", "--line-regexp", dest="grep_opts", const="-x", action="append_const",
|
|
607
|
+
help="force PATTERN to match only whole lines")
|
|
608
|
+
regex.add_argument("-z", "--null-data", dest="grep_opts", const="-z", action="append_const",
|
|
609
|
+
help="a data line ends in 0 byte, not newline")
|
|
610
|
+
misc = parser.add_argument_group("Miscellaneous")
|
|
611
|
+
misc.add_argument("-s", "--no-messages", dest="grep_opts", const="-s", action="append_const",
|
|
612
|
+
help="suppress error messages")
|
|
613
|
+
misc.add_argument("-v", "--invert-match", dest="grep_opts", const="-v", action="append_const",
|
|
614
|
+
help="select non-matching lines")
|
|
615
|
+
misc.add_argument("-V", "--version", nargs=0, action=ArgVersion, help="print version information and exit")
|
|
616
|
+
misc.add_argument("--help", action="help", help="display this help and exit")
|
|
617
|
+
# ----- START new arguments -----
|
|
618
|
+
misc.add_argument("--verbose", action="store_true", help="verbose mode")
|
|
619
|
+
misc.add_argument("--keep-files", dest="keep", action="store_true",
|
|
620
|
+
help="keep temporary files in the temporary directory")
|
|
621
|
+
misc.add_argument("--temp-dir", dest="tmp", default=TEMP_DIR,
|
|
622
|
+
help="define the temporary directory (default: {})".format(TEMP_DIR))
|
|
623
|
+
# ----- END new arguments -----
|
|
624
|
+
output = parser.add_argument_group("Output control")
|
|
625
|
+
output.add_argument("-m", "--max-count", dest="m", metavar="NUM", type=int, action=ArgCollectOption,
|
|
626
|
+
help="stop after NUM matches")
|
|
627
|
+
output.add_argument("-b", "--byte-offset", dest="grep_opts", const="-b", action="append_const",
|
|
628
|
+
help="print the byte offset with output lines")
|
|
629
|
+
output.add_argument("-n", "--line-number", dest="grep_opts", const="-n", action="append_const",
|
|
630
|
+
help="print line number with output lines")
|
|
631
|
+
output.add_argument("--line-buffered", dest="grep_opts", const="--line-buffered", action="append_const",
|
|
632
|
+
help="flush output on every line")
|
|
633
|
+
output.add_argument("-H", "--with-filename", dest="grep_opts", const="-H", action="append_const",
|
|
634
|
+
help="print the file name for each match")
|
|
635
|
+
output.add_argument("-h", "--no-filename", dest="grep_opts", const="-h", action="append_const",
|
|
636
|
+
help="suppress the file name prefix on output")
|
|
637
|
+
output.add_argument("--label", dest="label", metavar="LABEL", type=str, action=ArgCollectOption,
|
|
638
|
+
help="use LABEL as the standard input filename prefix")
|
|
639
|
+
output.add_argument("-o", "--only-matching", dest="grep_opts", const="-o", action="append_const",
|
|
640
|
+
help="show only the part of a line matching PATTERN")
|
|
641
|
+
output.add_argument("-q", "--quiet", "--silent", dest="grep_opts", const="-q", action="append_const",
|
|
642
|
+
help="suppress all normal output")
|
|
643
|
+
output_type = output.add_mutually_exclusive_group()
|
|
644
|
+
output_type.add_argument("--binary-files", dest="binary_files", metavar="TYPE", choices=['binary', 'text',
|
|
645
|
+
'without-match'], default=None, action=ArgCollectOption,
|
|
646
|
+
help="assume that binary files are TYPE;\nTYPE is 'binary', 'text', or 'without-match'")
|
|
647
|
+
output_type.add_argument("-a", "--text", dest="grep_opts", const="-a", action="append_const",
|
|
648
|
+
help="equivalent to --binary-files=text")
|
|
649
|
+
output_type.add_argument("-I", dest="grep_opts", const="-I", action="append_const",
|
|
650
|
+
help="equivalent to --binary-files=without-match")
|
|
651
|
+
output_match = output.add_mutually_exclusive_group()
|
|
652
|
+
output_match.add_argument("-L", "--files-without-match", dest="grep_opts", const="-L", action="append_const",
|
|
653
|
+
help="print only names of FILEs containing no match")
|
|
654
|
+
output_match.add_argument("-l", "--files-with-match", dest="grep_opts", const="-l", action="append_const",
|
|
655
|
+
help="print only names of FILEs containing matches")
|
|
656
|
+
output.add_argument("-c", "--count", dest="grep_opts", const="-c", action="append_const",
|
|
657
|
+
help="print only a count of matching lines per FILE")
|
|
658
|
+
output.add_argument("-T", "--initial-tab", dest="grep_opts", const="-T", action="append_const",
|
|
659
|
+
help="make tabs line up (if needed)")
|
|
660
|
+
output.add_argument("-Z", "--null", dest="grep_opts", const="-Z", action="append_const",
|
|
661
|
+
help="print 0 byte after FILE name")
|
|
662
|
+
context = parser.add_argument_group("Context control")
|
|
663
|
+
context.add_argument("-B", "--before-context", dest="B", metavar="NUM", type=int, action=ArgCollectOption,
|
|
664
|
+
help="print NUM lines of leading context")
|
|
665
|
+
context.add_argument("-A", "--after-context", dest="A", metavar="NUM", type=int, action=ArgCollectOption,
|
|
666
|
+
help="print NUM lines of trailing context")
|
|
667
|
+
context.add_argument("-C", "--context", dest="C", metavar="NUM", type=int, action=ArgCollectOption,
|
|
668
|
+
help="print NUM lines of output context")
|
|
669
|
+
regex_pat.add_argument("pattern", metavar="PATTERN", nargs='?', default=None, help=argparse.SUPPRESS)
|
|
670
|
+
parser.add_argument("url", metavar="URL", nargs='+', help=argparse.SUPPRESS)
|
|
671
|
+
# ----- START new arguments -----
|
|
672
|
+
web = parser.add_argument_group("Web options")
|
|
673
|
+
web.add_argument("-r", "--local-resources", dest="inc_local", action="store_true",
|
|
674
|
+
help="also grep local resources (same-origin)")
|
|
675
|
+
web.add_argument("-R", "--all-resources", dest="inc_all", action="store_true",
|
|
676
|
+
help="also grep all resources (even non-same-origin)")
|
|
677
|
+
web.add_argument("--include-headers", dest="inc_headers", action="store_true", help="also grep HTTP headers")
|
|
678
|
+
web.add_argument("--cookie", action=GetHeader, help="use a session cookie in the HTTP headers")
|
|
679
|
+
web.add_argument("--referer", action=GetHeader, help="provide the referer in the HTTP headers")
|
|
680
|
+
web = parser.add_argument_group("Proxy settings (by default, system proxy settings are used)")
|
|
681
|
+
web.add_argument("-d", "--disable-proxy", dest="proxy", action="store_false", help="manually disable proxy")
|
|
682
|
+
web.add_argument("--http-proxy", dest="http", action=ProxySetting, help="manually set the HTTP proxy")
|
|
683
|
+
web.add_argument("--https-proxy", dest="https", action=ProxySetting, help="manually set the HTTPS proxy")
|
|
684
|
+
# ----- END new arguments -----
|
|
685
|
+
args = parser.parse_args()
|
|
686
|
+
args.cache = None
|
|
687
|
+
if not args.keep:
|
|
688
|
+
try:
|
|
689
|
+
shutil.rmtree(args.tmp, ignore_errors=True)
|
|
690
|
+
except OSError:
|
|
691
|
+
pass
|
|
692
|
+
else:
|
|
693
|
+
try:
|
|
694
|
+
args.cache_file = join(args.tmp, CACHE_FILE)
|
|
695
|
+
with open(args.cache_file) as f:
|
|
696
|
+
args.cache = json.load(f)
|
|
697
|
+
except Exception:
|
|
698
|
+
args.cache = {}
|
|
699
|
+
if args.pattern is None and (args.grep_opts is None or "--file" not in args.grep_opts and \
|
|
700
|
+
"--regexp" not in args.grep_opts):
|
|
701
|
+
parser.error("no pattern provided")
|
|
702
|
+
if not hasattr(args, "headers"):
|
|
703
|
+
args.headers = {}
|
|
704
|
+
# configure logging and get the root logger
|
|
705
|
+
args.verbose = [logging.INFO, logging.DEBUG][args.verbose]
|
|
706
|
+
logging.basicConfig(format=LOG_FORMAT, datefmt=DATE_FORMAT, level=args.verbose)
|
|
707
|
+
logger = logging.getLogger()
|
|
708
|
+
coloredlogs.DEFAULT_LOG_FORMAT = LOG_FORMAT
|
|
709
|
+
coloredlogs.DEFAULT_DATE_FORMAT = DATE_FORMAT
|
|
710
|
+
coloredlogs.install(args.verbose)
|
|
711
|
+
# attach tools and preprocessors to args
|
|
712
|
+
for item in ['tools', 'preprocessors']:
|
|
713
|
+
for res in RESOURCE_TYPES:
|
|
714
|
+
a = '{}_{}'.format(res, item).upper()
|
|
715
|
+
if a in globals():
|
|
716
|
+
setattr(args, a.lower(), [(k, v[1]) for k, v in globals()[a].items() \
|
|
717
|
+
if __installed(k, v[0], None if "-s" in (args.grep_opts or []) else v[2])])
|
|
718
|
+
# running the main stuff ;
|
|
719
|
+
done = []
|
|
720
|
+
# if a cache exists and --keep-files is used, use it
|
|
721
|
+
if args.cache:
|
|
722
|
+
for url in args.url:
|
|
723
|
+
if url in done:
|
|
724
|
+
continue
|
|
725
|
+
for rel_fn in args.cache.get(url).keys():
|
|
726
|
+
with Resource(rel_fn) as res:
|
|
727
|
+
res.grep()
|
|
728
|
+
__exit_handler()
|
|
729
|
+
# otherwise, download the page and its resources
|
|
730
|
+
try:
|
|
731
|
+
session = requests.Session()
|
|
732
|
+
session.mount("file://", LocalFileAdapter())
|
|
733
|
+
logger.debug("Creating temporary folder {}".format(args.tmp))
|
|
734
|
+
logger.debug("Cache {}".format(["enabled", "disabled"][args.cache is None]))
|
|
735
|
+
if not exists(args.tmp):
|
|
736
|
+
os.makedirs(args.tmp)
|
|
737
|
+
for url in args.url:
|
|
738
|
+
if url in done:
|
|
739
|
+
continue
|
|
740
|
+
logger.debug("Downloading page {} and its associated resources".format(url))
|
|
741
|
+
with Resource(url) as page:
|
|
742
|
+
page.grep().handle()
|
|
743
|
+
done.append(url)
|
|
744
|
+
except Exception as e:
|
|
745
|
+
logger.exception("Unexpected error: {}".format(str(e)))
|
|
746
|
+
__exit_handler(code=1)
|
|
747
|
+
# gracefully close after running the main stuff
|
|
748
|
+
__exit_handler()
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
if __name__ == '__main__':
|
|
752
|
+
main()
|