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/__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()