diff --git a/contrib/map/LICENSE b/contrib/map/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..235572d45630865f1ab1e82518f99509ad4cac60 --- /dev/null +++ b/contrib/map/LICENSE @@ -0,0 +1,27 @@ +BSD License + +Copyright © 2016 Cesnet z.s.p.o +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of the Cesnet z.s.p.o nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE Cesnet z.s.p.o BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/contrib/map/README b/contrib/map/README new file mode 100644 index 0000000000000000000000000000000000000000..b69f63eb1c420c1f5c7804e5057253657c4f7369 --- /dev/null +++ b/contrib/map/README @@ -0,0 +1,71 @@ ++---------------------------------+ +| Warden Map Client 1.0 | ++---------------------------------+ + +Content + + A. Introduction + B. Configuration + C. Usage & Help + +------------------------------------------------------------------------------ +A. Introduction + + Warden Map Client is very simple client for drawing a map with events from + database of the Warden server. It consists of a Python 2.7 backend and + a javascript/jquery frontend. + + Backend uses Warden API for downloading of events. Events are processed and + enhanced with a geodata via freegeoip.net API. Finally warden-map.json file + with information for the frontend is created. + + Frontend uses datamaps project (http://datamaps.github.io/) for visualisation + of events on a map. It is possible to check details of the event by moving + cursor on a arc. It is also possible to zoom map via scrolling and/or clicking + on the plus, minus and, home buttons. + +------------------------------------------------------------------------------ +B. Configuration + + 1. Copy frontend folder into desired location. + + 2. Copy html snippet into your web page, or use it as an iframe. + NOTE: If necessary, change css/js paths in a html snippet. + + 3. Copy backend folder into desired location. + + 4. Setup backend call (warden-map.py) in a crontab. + NOTE: Please make sure you will have stored warden-map.json file + in the fronted folder. + EXAMPLE: ./warden-map.py --events 100 \ + --client cz.cesnet.warden.map \ + --key certs/key.pem \ + --cert certs/cert.pem \ + --cacert certs/cacert.pem \ + --secret SeCreT \ + --output ../fronted/ + + 5. Enjoy your map. + +------------------------------------------------------------------------------ +C. Usage & Help + +usage: warden-map.py [-h] [--output /path/] --events <number> --client + <org.ex.cl> --key /path/key.pem --cert /path/cert.pem + --cacert /path/cacert.pem --secret <SeCreT> + +optional arguments: + -h, --help show this help message and exit + --output path/ path where warden-map.json should be saved + +required arguments: + --events <number> count of events for a map + --client <org.ex.cl> client name + --key path/key.pem SSL key for a client + --cert path/cert.pem SSL cert for a client + --cacert path/cacert.pem SSL cacert for a client + --secret <SeCreT> secret key for a client + + +------------------------------------------------------------------------------ +Copyright (C) 2016 Cesnet z.s.p.o diff --git a/contrib/map/backend/warden-map.py b/contrib/map/backend/warden-map.py new file mode 100755 index 0000000000000000000000000000000000000000..096448c562f5a9e35ff26cb4e525b79975731df0 --- /dev/null +++ b/contrib/map/backend/warden-map.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# warden-map.py +# +# Copyright (C) 2016 Cesnet z.s.p.o +# Use of this source is governed by a 3-clause BSD-style license, see LICENSE file. + + +def getLastEvents(events, client, key, cert, cacert, secret): + + try: + ses = Session() + req = Request('POST', 'https://warden-hub.cesnet.cz/warden3/getEvents?client='+ client +'&secret='+ secret +'&count=' + events) + pre = req.prepare() + res = ses.send(pre, cert = (cert, key), verify=cacert) + except requests.exceptions.RequestException as error: + print error + sys.exit(1) + + data = res.json() + i = 0 + eventsList = [] + for p in data['events']: + event = {} + if i < events: + for key, value in { 'event': 'Category', 'time': 'DetectTime', 'origin': 'Source', 'destination': 'Target'}.iteritems(): + if value in p: + if (key == 'origin') or (key == 'destination'): + event[key] = {} + if 'IP4' in p[value][0]: + event[key]['ip'] = p[value][0]['IP4'] + else: + event[key] = {} + elif (key == 'event'): + event[key] = ', '.join(p[value]) + else: + event[key] = p[value] + else: + if (key == 'origin') or (key == 'destination'): + event[key] = {} + else: + event[key] = {} + if 'ip' in event['origin']: + eventsList.append(event) + i += 1 + else: + break + + return eventsList + +def getGeolocation(ip): + + try: + response = requests.get('http://freegeoip.net/json/' + str(ip[0])) + except requests.exceptions.RequestException as error: + print error + sys.exit(1) + + try: + json_data = json.loads(response.text) + except ValueError as error: + print error + sys.exit(1) + + return {'latitude': json_data['latitude'], 'longitude': json_data['longitude'], 'country_name': json_data['country_name'], 'city': json_data['city']} + +def main(args): + + events = args.events[0] + client = args.client[0] + key = args.key[0] + cert = args.cert[0] + cacert = args.cacert[0] + secret = args.secret[0] + + if args.output is not None: + path = args.output[0] + 'warden-map.json' + else: + path = 'warden-map.json' + + wardenEvents = getLastEvents(events, client, key, cert, cacert, secret) + + for p in wardenEvents: + for target in {'origin', 'destination'}: + geoData = {} + if 'ip' in p[target]: + geoData = getGeolocation(p[target]['ip']) + for value in {'latitude', 'longitude', 'country_name', 'city'}: + if value in geoData: + if not geoData[value]: + p[target][value] = "???" + else: + p[target][value] = geoData[value] + else: + p[target][value] = "???" + + else: + p[target]['ip'] = "???" + p[target]['country_name'] = "Czech Republic" + p[target]['city'] = "???" + p[target]['latitude'] = 49.743 + p[target]['longitude'] = 15.338 + + try: + with open(path, 'w') as outfile: + json.dump(wardenEvents, outfile) + except IOError: + print "Error: File does not appear to exist." + sys.exit(1) + + return 0 + +if __name__ == '__main__': + import sys + import json + from requests import Request, Session + import requests + import argparse + + parser = argparse.ArgumentParser(description='Creates warden-map.json for warden-map.html frontend.', + formatter_class=lambda prog: argparse.HelpFormatter(prog,max_help_position=30)) + + parser.add_argument('--output', metavar='path/', type=str, + nargs=1, help='path where warden-map.json should be saved') + + requiredNamed = parser.add_argument_group('required arguments') + + requiredNamed.add_argument('--events', metavar='<number>', type=str, required=True, + nargs=1, help='count of events for a map') + requiredNamed.add_argument('--client', metavar='<org.ex.cl>', type=str, required=True, + nargs=1, help='client name') + requiredNamed.add_argument('--key', metavar='path/key.pem', type=str, required=True, + nargs=1, help='SSL key for a client') + requiredNamed.add_argument('--cert', metavar='path/cert.pem', type=str, required=True, + nargs=1, help='SSL cert for a client') + requiredNamed.add_argument('--cacert', metavar='path/cacert.pem', type=str, required=True, + nargs=1, help='SSL cacert for a client') + requiredNamed.add_argument('--secret', metavar='<SeCreT>', type=str, required=True, + nargs=1, help='secret key for a client') + + args = parser.parse_args() + main(args) diff --git a/contrib/map/frontend/css/warden-map.css b/contrib/map/frontend/css/warden-map.css new file mode 100644 index 0000000000000000000000000000000000000000..a1b53d9e3c8267d80c207214d6291a49094594f9 --- /dev/null +++ b/contrib/map/frontend/css/warden-map.css @@ -0,0 +1,79 @@ +/* + * + * -*- coding: utf-8 -*- + * + * warden-map.css + * + * Copyright (C) 2016 Cesnet z.s.p.o + * Use of this source is governed by a 3-clause BSD-style license, see LICENSE file. + * +*/ + +body { + font-family: 'Oswald', sans-serif; +} + +h2 { + color: #0062a2; +} + +.hoverinfo { + font-family: 'Oswald', sans-serif; +} + +#country { + color: #0062a2; + font-weight: bold; +} + + +table { + text-align: left; + margin: 0; + padding: 0; + font-size: 12px; +} + +table th { + color: #0062a2; + padding: 0; +} + +table td { + color: #4b4d4a; + padding: 0; +} + +#container { + overflow: hidden; + border: 2px solid #0062a2; + border-radius: 5px; + background: white; + position: relative; + width: 1280px; + height: 720px; + max-width: 100%; + max-height: 100% +} + +.zoom-button { + width: 40px; + height: 40px; + border-radius: 5px; + border: none; + background: #dcdcda; + font-size: 23px; + font-weight: bold; + color: white; + cursor: pointer; +} + +.zoom-button:hover { + background-color: #0062a2; +} + +#zoom-info { + display: inline-block; + padding: 10px; + color: #0062a2; +} diff --git a/contrib/map/frontend/js/warden-map.js b/contrib/map/frontend/js/warden-map.js new file mode 100644 index 0000000000000000000000000000000000000000..e4610f58490631b4805f292e9f5ba396cb00b9b3 --- /dev/null +++ b/contrib/map/frontend/js/warden-map.js @@ -0,0 +1,264 @@ +/* + * + * -*- coding: utf-8 -*- + * + * warden-map.js + * + * Copyright (C) 2016 Cesnet z.s.p.o + * Use of this source is governed by a 3-clause BSD-style license, see LICENSE file. + * +*/ + +// NOTE: Change path in a function d3.json() if you separate backend and frontend! + +// Zooming functionality is based on WunderBart's implementation +// Please see following links: +// https://github.com/wunderbart +// https://jsfiddle.net/wunderbart/Lom3b0gb/ + + function Zoom(args) { + $.extend(this, { + $buttons: $(".zoom-button"), + $info: $("#zoom-info"), + scale: { max: 50, currentShift: 0 }, + $container: args.$container, + datamap: args.datamap + }); + + this.init(); +} + +Zoom.prototype.init = function() { + var paths = this.datamap.svg.selectAll("path"), + subunits = this.datamap.svg.selectAll(".datamaps-subunit"); + + // preserve stroke thickness + paths.style("vector-effect", "non-scaling-stroke"); + + // disable click on drag end + subunits.call( + d3.behavior.drag().on("dragend", function() { + d3.event.sourceEvent.stopPropagation(); + }) + ); + + this.scale.set = this._getScalesArray(); + this.d3Zoom = d3.behavior.zoom().scaleExtent([ 1, this.scale.max ]); + + this._displayPercentage(1); + this.listen(); +}; + +Zoom.prototype.listen = function() { + this.$buttons.off("click").on("click", this._handleClick.bind(this)); + + this.datamap.svg + .call(this.d3Zoom.on("zoom", this._handleScroll.bind(this))) + .on("dblclick.zoom", null); // disable zoom on double-click +}; + +Zoom.prototype.reset = function() { + this._shift("reset"); +}; + +Zoom.prototype._handleScroll = function() { + var translate = d3.event.translate, + scale = d3.event.scale, + limited = this._bound(translate, scale); + + this.scrolled = true; + + this._update(limited.translate, limited.scale); +}; + +Zoom.prototype._handleClick = function(event) { + var direction = $(event.target).data("zoom"); + + this._shift(direction); +}; + +Zoom.prototype._shift = function(direction) { + var center = [ this.$container.width() / 2, this.$container.height() / 2 ], + translate = this.d3Zoom.translate(), translate0 = [], l = [], + view = { + x: translate[0], + y: translate[1], + k: this.d3Zoom.scale() + }, bounded; + + translate0 = [ + (center[0] - view.x) / view.k, + (center[1] - view.y) / view.k + ]; + + if (direction == "reset") { + view.k = 1; + this.scrolled = true; + } else { + view.k = this._getNextScale(direction); + } + +l = [ translate0[0] * view.k + view.x, translate0[1] * view.k + view.y ]; + + view.x += center[0] - l[0]; + view.y += center[1] - l[1]; + + bounded = this._bound([ view.x, view.y ], view.k); + + this._animate(bounded.translate, bounded.scale); +}; + +Zoom.prototype._bound = function(translate, scale) { + var width = this.$container.width(), + height = this.$container.height(); + + translate[0] = Math.min( + (width / height) * (scale - 1), + Math.max( width * (1 - scale), translate[0] ) + ); + + translate[1] = Math.min(0, Math.max(height * (1 - scale), translate[1])); + + return { translate: translate, scale: scale }; +}; + +Zoom.prototype._update = function(translate, scale) { + this.d3Zoom + .translate(translate) + .scale(scale); + + this.datamap.svg.selectAll("g") + .attr("transform", "translate(" + translate + ")scale(" + scale + ")"); + + this._displayPercentage(scale); +}; + +Zoom.prototype._animate = function(translate, scale) { + var _this = this, + d3Zoom = this.d3Zoom; + + d3.transition().duration(350).tween("zoom", function() { + var iTranslate = d3.interpolate(d3Zoom.translate(), translate), + iScale = d3.interpolate(d3Zoom.scale(), scale); + + return function(t) { + _this._update(iTranslate(t), iScale(t)); + }; + }); +}; + +Zoom.prototype._displayPercentage = function(scale) { + var value; + + value = Math.round(Math.log(scale) / Math.log(this.scale.max) * 100); + this.$info.text(value + "%"); +}; + +Zoom.prototype._getScalesArray = function() { + var array = [], + scaleMaxLog = Math.log(this.scale.max); + + for (var i = 0; i <= 10; i++) { + array.push(Math.pow(Math.E, 0.1 * i * scaleMaxLog)); + } + + return array; +}; + +Zoom.prototype._getNextScale = function(direction) { + var scaleSet = this.scale.set, + currentScale = this.d3Zoom.scale(), + lastShift = scaleSet.length - 1, + shift, temp = []; + + if (this.scrolled) { + + for (shift = 0; shift <= lastShift; shift++) { + temp.push(Math.abs(scaleSet[shift] - currentScale)); + } + + shift = temp.indexOf(Math.min.apply(null, temp)); + + if (currentScale >= scaleSet[shift] && shift < lastShift) { + shift++; + } + + if (direction == "out" && shift > 0) { + shift--; + } + + this.scrolled = false; + + } else { + + shift = this.scale.currentShift; + + if (direction == "out") { + shift > 0 && shift--; + } else { + shift < lastShift && shift++; + } + } + + this.scale.currentShift = shift; + + return scaleSet[shift]; +}; + +// Configuration of datamap canvas +// Futher reading can be found at https://datamaps.github.io/ +function Datamap() { + this.$container = $("#container"); + this.instance = new Datamaps({ + scope: 'world', + element: this.$container.get(0), + done: this._handleMapReady.bind(this), + projection: 'mercator', + fills: { + defaultFill: '#dcdcda' + }, + geographyConfig: { + borderColor: '#fdfdfd', + highlightFillColor: '#4b4d4a', + highlightBorderColor: '#fdfdfd', + popupOnHover: true, + popupTemplate: function(geography, data) { + return '<div class="hoverinfo" id="country">' + geography.properties.name + '</div>'; + }, + }, + arcConfig: { + strokeColor: '#0062a2', + strokeWidth: 1, + arcSharpness: 5, + animationSpeed: 4000, // Milliseconds + popupOnHover: true, + // Case with latitude and longitude + popupTemplate: function(geography, data) { + if ( ( data.origin && data.destination ) && data.origin.latitude && data.origin.longitude && data.destination.latitude && data.destination.longitude ) { + // Content of info table + str = '<div class="hoverinfo"><table id="event"><tr><th>Warden Event</th></tr><tr><td>Type</td><td>'+ JSON.stringify(data.event) +'</td></tr><tr><td>Detect Time</td><td>'+ JSON.stringify(data.time) +'</td></tr><tr><th>Event origin</th></tr><tr><td>IP</td><td>' + JSON.stringify(data.origin.ip) + '</td></tr><tr><td>City & Country</td><td>' + JSON.stringify(data.origin.city) + ', ' + JSON.stringify(data.origin.country_name) + '</td></tr><tr><td>GPS</td><td>' + JSON.stringify(data.origin.latitude) + ', ' + JSON.stringify(data.origin.longitude) + '</td></tr><tr><th>Event Destination</th></tr><tr><td>IP</td><td>' + JSON.stringify(data.destination.ip) + '</td></tr><tr><td>City & Country</td><td>' + JSON.stringify(data.destination.city) + ', ' + JSON.stringify(data.destination.country_name) + '</td></tr><tr><td>GPS</td><td>' + JSON.stringify(data.destination.latitude) + ', ' + JSON.stringify(data.destination.longitude) + '</td></tr></table></div>'; + return str.replace(/"/g,""); + } + // Missing information + else { + return ''; + } + } + } + }); + var instance = this.instance; + // Link to a json file with information for drawing. + // NOTE: Change path if you separate backend and fronend! + d3.json("./warden-map.json", function(error, data) { + instance.arc(data); + }); +}; + + + +Datamap.prototype._handleMapReady = function(datamap) { + this.zoom = new Zoom({ + $container: this.$container, + datamap: datamap + }); +} diff --git a/contrib/map/frontend/warden-map.html b/contrib/map/frontend/warden-map.html new file mode 100644 index 0000000000000000000000000000000000000000..09b9593e63eab318e98086e716b12285e0d5d8a0 --- /dev/null +++ b/contrib/map/frontend/warden-map.html @@ -0,0 +1,39 @@ +<!-- --> +<!-- --> +<!-- -*- coding: utf-8 -*- --> +<!-- --> +<!-- warden-map.html --> +<!-- --> +<!-- Copyright (C) 2016 Cesnet z.s.p.o --> +<!-- Use of this source is governed by a 3-clause BSD-style license, see LICENSE file. --> +<!-- --> +<!-- --> + + +<!DOCTYPE html> +<meta name="robots" content="noindex"> +<meta charset="utf-8"> +<link href='https://fonts.googleapis.com/css?family=Oswald&subset=latin,latin-ext' rel='stylesheet' type='text/css'> +<link rel="stylesheet" type="text/css" href="./css/warden-map.css"/> +<body> + +<script src="https://d3js.org/d3.v3.min.js"></script> +<script src="https://d3js.org/topojson.v1.min.js"></script> +<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script> +<script src="./js/datamaps.world.hires.min.js"></script> +<script src="./js/warden-map.js"></script> + +<h2>Warden Map</h2> +<div id="tools"> + <button class="zoom-button" data-zoom="reset">⌂</button> + <button class="zoom-button" data-zoom="out">-</button> + <button class="zoom-button" data-zoom="in">+</button> + <div id="zoom-info"></div> +</div> +<div id="container"></div> + +<!-- Draw datamap into id="container" --> +<script>new Datamap();</script> + +</body> +</html>