diff --git a/lookyloo/__init__.py b/lookyloo/__init__.py index a7587867..fb335654 100644 --- a/lookyloo/__init__.py +++ b/lookyloo/__init__.py @@ -6,15 +6,23 @@ import json from har2tree import CrawledTree from scrapysplashwrapper import crawl -from flask import Flask, render_template, request +from flask import Flask, render_template, request, session from flask_bootstrap import Bootstrap from glob import glob import os from datetime import datetime +import pickle +import tempfile + app = Flask(__name__) +app.secret_key = 'changeme' + +if app.secret_key == 'changeme': + raise Exception('FFS, please set a proper secret key...') + Bootstrap(app) app.config['BOOTSTRAP_SERVE_LOCAL'] = True app.debug = True @@ -23,11 +31,25 @@ HAR_DIR = 'scraped' SPLASH = 'http://127.0.0.1:8050' +@app.before_request +def session_management(): + # make the session last indefinitely until it is cleared + session.permanent = True + + def load_tree(report_dir): + if session.get('tree'): + # TODO delete file + pass + session.clear() har_files = sorted(glob(os.path.join(HAR_DIR, report_dir, '*.har'))) ct = CrawledTree(har_files) ct.find_parents() ct.join_trees() + temp = tempfile.NamedTemporaryFile(delete=False) + pickle.dump(ct, temp) + temp.close() + session["tree"] = temp.name return ct.jsonify(), ct.start_time.isoformat(), ct.user_agent, ct.root_url @@ -63,6 +85,25 @@ def get_report_dirs(): return sorted(os.listdir(HAR_DIR), reverse=True) +@app.route('/tree/hostname/', methods=['GET']) +def hostnode_details(node_uuid): + with open(session["tree"], 'rb') as f: + ct = pickle.load(f) + hostnode = ct.root_hartree.get_host_node_by_uuid(node_uuid) + urls = [] + for url in hostnode.urls: + urls.append(url.jsonify()) + return json.dumps(urls) + + +@app.route('/tree/url/', methods=['GET']) +def urlnode_details(node_uuid): + with open(session["tree"], 'rb') as f: + ct = pickle.load(f) + urlnode = ct.root_hartree.get_url_node_by_uuid(node_uuid) + return urlnode.jsonify() + + @app.route('/tree/', methods=['GET']) def tree(tree_id): report_dir = get_report_dirs()[tree_id] diff --git a/lookyloo/static/tree.css b/lookyloo/static/tree.css index 90ca1062..1c2c7d23 100644 --- a/lookyloo/static/tree.css +++ b/lookyloo/static/tree.css @@ -49,3 +49,24 @@ padding-bottom: 5px; padding-left: 5px; } + +.tooltip { + position: absolute; + text-align: left; + padding: 2px; + font: 12px sans-serif; + background: lightsteelblue; + border: 0px; + border-radius: 8px; + z-index: 1; + border: 2px solid; + padding-top: 5px; + padding-right: 5px; + padding-bottom: 5px; + padding-left: 5px; +} + +.tooltip text { + font: 15px sans-serif; + z-index: 2; +} diff --git a/lookyloo/static/tree.js b/lookyloo/static/tree.js index 28c14b14..fa54e90f 100644 --- a/lookyloo/static/tree.js +++ b/lookyloo/static/tree.js @@ -6,7 +6,11 @@ var margin = {top: 20, right: 200, bottom: 30, left: 90}, height = 10000 - margin.top - margin.bottom; var node_width = 0; -var node_height = 35; +var node_height = 45; + +var hostnode_tooltip = d3.select("body").append("div") + .attr("class", "tooltip") + .style("opacity", 0); var init = d3.select("body").append("svg") .attr("width", width + margin.right + margin.left) @@ -29,7 +33,12 @@ var background = init.append('rect') .attr('y', 0) .attr('width', width) .attr('height', height) - .style('fill', "url(#backstripes)"); + .style('fill', "url(#backstripes)") + .on('click', function(d) { + hostnode_tooltip.transition() + .duration(500) + .style("opacity", 0); + }); // append the svg object to the body of the page // appends a 'group' element to 'svg' @@ -71,6 +80,41 @@ function getBB(selection) { }) } +function urlnode_click(uuid) { + var url = "url/" + uuid; + d3.json(url, function(error, u) { + if (error) throw error; + console.log(u) + }) +} + + +function hostnode_click(d) { + // Modal display + var url = "hostname/" + d.data.uuid; + var pageX=d3.event.pageX; + var pageY=d3.event.pageY; + hostnode_tooltip.selectAll("ul").remove(); + d3.json(url, function(error, urls) { + if (error) throw error; + hostnode_tooltip.transition() + .duration(200) + .style("opacity", .9) + .style("left", (pageX) + "px") + .style("top", (pageY - 28) + "px"); + var list = hostnode_tooltip.append('ul') + .attr("class", "list-group"); + urls.forEach(function(url){ + jdata = JSON.parse(url) + var entry = list.append('li') + .attr("class", "list-group-item") + .attr("url_uuid", jdata['uuid']) + .text(jdata['name']) + .on('click', urlnode_click(jdata['uuid'])); + }) + }); +} + function update(source) { // reinitialize max_depth @@ -101,8 +145,7 @@ function update(source) { .attr('class', 'node') .attr("transform", function(d) { return "translate(" + source.y0 + "," + source.x0 + ")"; - }) - .on('click', click); + }); // Add Circle for the nodes nodeEnter.append('circle') @@ -110,11 +153,13 @@ function update(source) { .attr('r', 1e-6) .style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; - }); + }) + .on('click', click); // Avoid hiding the content after the circle var nodeContent = nodeEnter .append('svg') + .attr('height',node_height) .attr('x', 10) .attr('y', -20); @@ -128,7 +173,8 @@ function update(source) { .text(function(d) { d.data.total_width = 0; // reset total_width return d.data.name; - }); + }) + .on('click', hostnode_click); // This value has to be set once for all for the whole tree and cannot be updated // on click as clicking only updates a part of the tree @@ -361,4 +407,5 @@ function update(source) { } update(d); } + }