Skip to content

Commit 0c6de29

Browse files
committed
chg: [forum] save post and user images + extract images from HAR + remove crawl item from forum crawl queue
1 parent 9167336 commit 0c6de29

8 files changed

Lines changed: 140 additions & 6 deletions

File tree

bin/importer/feeders/Forum_Extractor.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from lib.objects.Posts import Post
2323
from lib.objects.UsersAccount import UserAccount
2424
from lib.objects.Usernames import Username
25+
from lib.objects import Images
2526

2627
# TODO IMAGES + USER ACCOUNTS + FILENAMES
2728
class Forum_ExtractorFeeder(DefaultFeeder):
@@ -35,6 +36,7 @@ def __init__(self, json_data):
3536
self.seen_subforums = set()
3637
self.root_subforums = set()
3738
self.objs_to_process = set()
39+
self.har_images = {}
3840

3941
def process_meta(self):
4042
"""Import one forum-extractor result dictionary from self.json_meta."""
@@ -60,6 +62,7 @@ def _import_success(self, result):
6062
forum_type = result.get('forum_type')
6163
forum_id = result.get('forum_id')
6264
extracted = result.get('extracted') or {}
65+
self.har_images = result.get('har_images')
6366

6467
if not forum_type or not forum_id:
6568
# TODO Exception + logs
@@ -307,16 +310,48 @@ def _upsert_post(self, post_data, parent_thread):
307310
post.create(post_id, post_timestamp, content, parent_thread, self.forum, state=post_data.get('post_state'), quote_ids=quote_ids)
308311
for reaction in post_data.get('reactions', []):
309312
if isinstance(reaction, dict):
310-
reaction_name = reaction.get('reaction')
313+
reaction_name = reaction.get('reaction_type')
311314
reaction_count = reaction.get('count')
312315
else:
313316
reaction_name = None
314317
reaction_count = None
315318
if reaction_name and reaction_count:
319+
reaction_name = reaction_name.strip().lower().replace("-", "_").strip("_")
316320
post.add_reaction(reaction_name, int(reaction_count))
321+
322+
# images
323+
# print('--------------------------------------------------')
324+
# print(post_data.get('content', {}).get('images', []))
325+
for url in post_data.get('content', {}).get('images', []):
326+
print('URL:', url)
327+
if url in self.har_images:
328+
image = self._create_image_from_har_url(url, post.get_date(), post)
329+
self.objs_to_process.add(image)
330+
317331
user_account = self._create_post_user_account(post_data.get('author'), post, post_timestamp)
332+
if user_account:
333+
author_image_url = (post_data.get('author', {})).get('author_profile_image_url')
334+
if author_image_url in self.har_images:
335+
image = self._create_image_from_har_url(author_image_url, post.get_date(), user_account)
336+
if image:
337+
user_account.set_icon(image.get_global_id())
338+
self.objs_to_process.add(image)
318339
return post
319340

341+
def _create_image_from_har_url(self, url, date, obj):
342+
print(url)
343+
image_payload = self.har_images[url]
344+
content = image_payload['content']
345+
if not image_payload['b64'] and isinstance(content, str):
346+
content = content.encode()
347+
image = Images.create(content, b64=image_payload['b64'])
348+
if image:
349+
# create correlation image - obj # TODO correlation forum ?
350+
image.add(date, obj)
351+
self.objs_to_process.add(image)
352+
print(image.id)
353+
return image
354+
320355
# TODO A POST MUST HAVE AN USER ACCOUNT
321356
def _create_post_user_account(self, author, post, timestamp):
322357
"""Create/update the post author user-account and username metadata."""

bin/lib/correlations_engine.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,11 @@
7070
"subforum": ["forum", "subforum", "forum-thread"],
7171
"forum-thread": ["subforum", "post"],
7272
# TODO Extend to detection of text -> same as message
73-
"post": ["forum", "forum-thread", "user-account", *COMMON_TEXT_OBJECT_CORRELATIONS],
73+
"post": ["forum", "forum-thread", "image", "user-account", *COMMON_TEXT_OBJECT_CORRELATIONS],
7474
"file-name": ["chat", "item", "message", "pdf"],
7575
"gtracker": ["domain", "item"],
7676
"hhhash": ["domain"],
77-
"image": ["barcode", "chat", "chat-subchannel", "chat-thread", "message", "ocr", "qrcode", "user-account"], # TODO subchannel + threads ????
77+
"image": ["barcode", "chat", "chat-subchannel", "chat-thread", "message", "ocr", "post", "qrcode", "user-account"], # TODO subchannel + threads ????
7878
"ip": ["ssh-key"],
7979
"item": ["cve", "cryptocurrency", "decoded", "domain", "dom-hash", "favicon", "file-name", "gtracker", "mail", "message", "pdf", "pgp", "screenshot", "title", "username"], # chat ???
8080
"mail": ["domain", "item", "message"], # chat ??

bin/lib/crawlers.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
88
"""
99
import base64
10+
import binascii
1011
import gzip
1112
import hashlib
1213
import json
14+
import magic
1315
import os
1416
import pickle
1517
import re
@@ -72,6 +74,17 @@
7274
D_SCREENSHOT = config_loader.get_config_boolean('Crawler', 'default_screenshot')
7375
config_loader = None
7476

77+
78+
# IMAGE
79+
MAX_IMAGE_SIZE = 5000000
80+
ACCEPTED_IMAGE_MIME_TYPES = {
81+
# 'image/gif',
82+
'image/jpeg',
83+
'image/png',
84+
'image/webp',
85+
}
86+
87+
7588
# logger_crawler = logging.getLogger('crawlers.log')
7689

7790
# # # # # # # #
@@ -750,6 +763,55 @@ def _gzip_all_hars():
750763
for har_id in get_all_har_ids():
751764
_gzip_har(har_id)
752765

766+
767+
def extract_images_from_har(har, size_limit=MAX_IMAGE_SIZE):
768+
images = {}
769+
if not har:
770+
return images
771+
for entry in har.get('log', {}).get('entries', []):
772+
request = entry.get('request', {})
773+
response = entry.get('response', {})
774+
url = request.get('url')
775+
content = response.get('content', {})
776+
mime_type = content.get('mimeType', '')
777+
body = content.get('text')
778+
if not url or not body:
779+
continue
780+
is_b64 = content.get('encoding') == 'base64'
781+
if response.get('status') != 200 or mime_type not in ACCEPTED_IMAGE_MIME_TYPES:
782+
continue
783+
image_content = _get_image_content_bytes(body, is_b64)
784+
if not image_content:
785+
continue
786+
if 0 < size_limit < len(image_content):
787+
print('HAR IMAGE SIZE LIMIT', url)
788+
continue
789+
detected_mime_type = magic.from_buffer(image_content, mime=True)
790+
if detected_mime_type not in ACCEPTED_IMAGE_MIME_TYPES:
791+
print('HAR IMAGE INVALID MIME TYPE', detected_mime_type, url)
792+
continue
793+
images[url] = {
794+
'content': body,
795+
'b64': is_b64,
796+
'mime_type': detected_mime_type,
797+
}
798+
return images
799+
800+
def _get_image_content_bytes(content, b64=False):
801+
if b64:
802+
try:
803+
if isinstance(content, str):
804+
content = ''.join(content.split())
805+
return base64.b64decode(content, validate=True)
806+
except (binascii.Error, ValueError):
807+
return None
808+
if isinstance(content, bytes):
809+
return content
810+
if isinstance(content, str):
811+
return content.encode()
812+
return None
813+
814+
753815
# # # - - # # #
754816

755817
################################################################################

bin/lib/forums_viewer.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,15 @@ def purge_forum_crawl_queue(forum_id):
456456
'deleted': forum.purge_crawl_queue(),
457457
}, 200
458458

459+
def remove_forum_pending_crawl_item(forum_id, crawl_key):
460+
forum = Forums.Forum(forum_id)
461+
if not forum.exists():
462+
return {"status": "error", "reason": "Unknown forum"}, 404
463+
removed = forum.remove_pending_crawl_item(crawl_key)
464+
if not removed:
465+
return {'status': 'error', 'reason': 'not_pending'}, 404
466+
return {'forum_id': forum_id, 'crawl_key': crawl_key}, 200
467+
459468
def get_breadcrumb_for_object(obj):
460469
"""Return parent breadcrumb entries from Forum to the given object."""
461470
breadcrumb = []
@@ -548,7 +557,6 @@ def api_get_forum_thread(subtype, thread_id, page=1, nb=50, translation_target=N
548557
}, 200
549558

550559

551-
552560
def api_get_post(post_id, translation_target=None):
553561
post = Posts.Post(post_id)
554562
if not post.exists():

bin/lib/objects/Forums.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -732,6 +732,12 @@ def _cleanup_crawl_item(self, crawl_key):
732732
def complete_crawl_item(self, crawl_key):
733733
return self._cleanup_crawl_item(crawl_key)
734734

735+
def remove_pending_crawl_item(self, crawl_key):
736+
if not r_object.zrem(f'forum:crawl:queue:{self.id}', crawl_key):
737+
return False
738+
self._cleanup_crawl_item(crawl_key)
739+
return True
740+
735741
def fail_crawl_item(self, crawl_key, error=None):
736742
item = self.get_crawl_item(crawl_key)
737743
if item and item.get('type') == 'forum-thread':

bin/lib/objects/Images.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,8 @@ def create(content, size_limit=5000000, b64=False, force=False):
189189
if not image.exists():
190190
image.create(content)
191191
return image
192+
else:
193+
print('LIMIT SIZE CREATE IMAGE')
192194

193195

194196
class Images(AbstractDaterangeObjects):

var/www/blueprints/forums_explorer.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,21 @@ def forum_explorer_crawler_status():
9292
return render_template('forums_explorer_crawler_index.html', forums=forums, bootstrap_label=bootstrap_label)
9393

9494

95-
@forums_explorer.route("/forums/explorer/crawler/queue", methods=['GET'])
95+
@forums_explorer.route("/forums/explorer/crawler/queue", methods=['GET', 'POST'])
9696
@login_required
9797
@login_admin
9898
def forum_explorer_crawler_queue():
99+
if request.method == 'POST':
100+
forum_id = request.form.get('forum_id')
101+
crawl_key = request.form.get('crawl_key')
102+
sample_size = request.form.get('sample_size') or 50
103+
res = forums_viewer.remove_forum_pending_crawl_item(forum_id, crawl_key)
104+
if res[1] != 200:
105+
error = res[0].get('reason')
106+
return redirect(url_for('forums_explorer.forum_explorer_crawler_queue', id=forum_id, sample_size=sample_size, error=error))
107+
success = f"Removed pending crawl item: {crawl_key}"
108+
return redirect(url_for('forums_explorer.forum_explorer_crawler_queue', id=forum_id, sample_size=sample_size, success=success))
109+
99110
forum_id = request.args.get('id')
100111
sample_size = request.args.get('sample_size') or 50
101112
meta = forums_viewer.api_get_forum_crawl_queue(forum_id, sample_size=sample_size)

var/www/templates/forums_explorer/forums_explorer_crawler_queue.html

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ <h1 class="mb-2"><i class="fas fa-list"></i> Forum crawl queue</h1>
5959
<div class="forum-board-header">Pending crawl items</div>
6060
{% if queue.get('pending_sample') %}
6161
<div class="table-responsive"><table class="table table-sm table-striped mb-0">
62-
<thead><tr><th>crawl_key</th><th>score</th><th>type</th><th>id</th><th>page</th><th>url</th><th>referer</th><th>crawl_mode</th><th>parent</th></tr></thead>
62+
<thead><tr><th>crawl_key</th><th>score</th><th>type</th><th>id</th><th>page</th><th>url</th><th>referer</th><th>crawl_mode</th><th>parent</th><th>actions</th></tr></thead>
6363
<tbody>{% for pending in queue.get('pending_sample') %}{% set item = pending.get('item') or {} %}
6464
<tr>
6565
<td class="text-break">{{ pending.get('crawl_key') }}</td>
@@ -71,6 +71,16 @@ <h1 class="mb-2"><i class="fas fa-list"></i> Forum crawl queue</h1>
7171
<td class="text-break">{{ item.get('referer') }}</td>
7272
<td>{{ item.get('crawl_mode') }}</td>
7373
<td class="text-break">{{ item.get('parent') }}</td>
74+
<td>
75+
<form method="post" action="{{ url_for('forums_explorer.forum_explorer_crawler_queue') }}" class="mb-0">
76+
<input type="hidden" name="forum_id" value="{{ forum['id'] }}">
77+
<input type="hidden" name="crawl_key" value="{{ pending.get('crawl_key') }}">
78+
<input type="hidden" name="sample_size" value="{{ meta.get('sample_size') }}">
79+
<button class="btn btn-sm btn-outline-danger" onclick="return confirm('Remove pending crawl item {{ pending.get('crawl_key') }} from the queue?')">
80+
<i class="fas fa-times"></i> Remove
81+
</button>
82+
</form>
83+
</td>
7484
</tr>
7585
{% endfor %}</tbody>
7686
</table></div>

0 commit comments

Comments
 (0)