new: pwngrid web client

This commit is contained in:
Simone Margaritelli 2019-11-12 20:18:02 +01:00
parent e2be21004d
commit df01a03a4b
8 changed files with 624 additions and 1 deletions

@ -23,8 +23,10 @@ def call(path, obj=None):
url = '%s%s' % (API_ADDRESS, path)
if obj is None:
r = requests.get(url, headers=None)
else:
elif isinstance(obj, dict):
r = requests.post(url, headers=None, json=obj)
else:
r = requests.post(url, headers=None, data=obj)
if r.status_code != 200:
raise Exception("(status %d) %s" % (r.status_code, r.text))
@ -39,6 +41,10 @@ def set_advertisement_data(data):
return call("/mesh/data", obj=data)
def get_advertisement_data():
return call("/mesh/data")
def peers():
return call("/mesh/peers")
@ -95,3 +101,15 @@ def report_ap(essid, bssid):
def inbox(page=1, with_pager=False):
obj = call("/inbox?p=%d" % page)
return obj["messages"] if not with_pager else obj
def inbox_message(id):
return call("/inbox/%d" % int(id))
def mark_message(id, mark):
return call("/inbox/%d/%s" % (int(id), str(mark)))
def send_message(to, message):
return call("/unit/%s/inbox" % to, message)

@ -1,5 +1,6 @@
import logging
import os
import base64
import _thread
# https://stackoverflow.com/questions/14888799/disable-console-messages-in-flask-server
@ -7,12 +8,15 @@ logging.getLogger('werkzeug').setLevel(logging.ERROR)
os.environ['WERKZEUG_RUN_MAIN'] = 'true'
import pwnagotchi
import pwnagotchi.grid as grid
import pwnagotchi.ui.web as web
from pwnagotchi import plugins
from flask import send_file
from flask import request
from flask import jsonify
from flask import abort
from flask import redirect
from flask import render_template, render_template_string
@ -24,6 +28,15 @@ class Handler:
self._app.add_url_rule('/ui', 'ui', self.ui)
self._app.add_url_rule('/shutdown', 'shutdown', self.shutdown, methods=['POST'])
self._app.add_url_rule('/restart', 'restart', self.restart, methods=['POST'])
# inbox
self._app.add_url_rule('/inbox', 'inbox', self.inbox)
self._app.add_url_rule('/inbox/profile', 'inbox_profile', self.inbox_profile)
self._app.add_url_rule('/inbox/<id>', 'show_message', self.show_message)
self._app.add_url_rule('/inbox/<id>/<mark>', 'mark_message', self.mark_message)
self._app.add_url_rule('/inbox/new', 'new_message', self.new_message)
self._app.add_url_rule('/inbox/send', 'send_message', self.send_message, methods=['POST'])
# plugins
self._app.add_url_rule('/plugins', 'plugins', self.plugins, strict_slashes=False,
defaults={'name': None, 'subpath': None})
@ -37,6 +50,81 @@ class Handler:
other_mode='AUTO' if self._agent.mode == 'manual' else 'MANU',
fingerprint=self._agent.fingerprint())
def inbox(self):
page = request.args.get("p", default=1, type=int)
inbox = {
"pages": 1,
"records": 0,
"messages": []
}
error = None
try:
inbox = grid.inbox(page, with_pager=True)
except Exception as e:
logging.exception('error while reading pwnmail inbox')
error = str(e)
return render_template('inbox.html',
name=pwnagotchi.name(),
page=page,
error=error,
inbox=inbox)
def inbox_profile(self):
data = {}
error = None
try:
data = grid.get_advertisement_data()
except Exception as e:
logging.exception('error while reading pwngrid data')
error = str(e)
return render_template('profile.html',
name=pwnagotchi.name(),
fingerprint=self._agent.fingerprint(),
data=data,
error=error)
def show_message(self, id):
message = {}
error = None
try:
message = grid.inbox_message(id)
if message['data']:
message['data'] = base64.b64decode(message['data']).decode("utf-8")
except Exception as e:
logging.exception('error while reading pwnmail message %d' % int(id))
error = str(e)
return render_template('message.html',
name=pwnagotchi.name(),
error=error,
message=message)
def new_message(self):
to = request.args.get("to", default="")
return render_template('new_message.html', to=to)
def send_message(self):
to = request.form["to"]
message = request.form["message"]
error = None
try:
grid.send_message(to, message)
except Exception as e:
error = str(e)
return jsonify({"error": error})
def mark_message(self, id, mark):
logging.info("marking message %d as %s" % (int(id), mark))
grid.mark_message(id, mark)
return redirect("/inbox")
def plugins(self, name, subpath):
if name is None:
# show plugins overview

@ -66,3 +66,11 @@ div.status {
left: 0;
width: 100%;
}
a.read {
color: #777 !important;
}
p.messagebody {
padding: 1em;
}

@ -0,0 +1,232 @@
/**
* Timeago is a jQuery plugin that makes it easy to support automatically
* updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago").
*
* @name timeago
* @version 1.6.7
* @requires jQuery >=1.5.0 <4.0
* @author Ryan McGeary
* @license MIT License - http://www.opensource.org/licenses/mit-license.php
*
* For usage and examples, visit:
* http://timeago.yarp.com/
*
* Copyright (c) 2008-2019, Ryan McGeary (ryan -[at]- mcgeary [*dot*] org)
*/
(function (factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['jquery'], factory);
} else if (typeof module === 'object' && typeof module.exports === 'object') {
factory(require('jquery'));
} else {
// Browser globals
factory(jQuery);
}
}(function ($) {
$.timeago = function(timestamp) {
if (timestamp instanceof Date) {
return inWords(timestamp);
} else if (typeof timestamp === "string") {
return inWords($.timeago.parse(timestamp));
} else if (typeof timestamp === "number") {
return inWords(new Date(timestamp));
} else {
return inWords($.timeago.datetime(timestamp));
}
};
var $t = $.timeago;
$.extend($.timeago, {
settings: {
refreshMillis: 60000,
allowPast: true,
allowFuture: false,
localeTitle: false,
cutoff: 0,
autoDispose: true,
strings: {
prefixAgo: null,
prefixFromNow: null,
suffixAgo: "ago",
suffixFromNow: "from now",
inPast: 'any moment now',
seconds: "less than a minute",
minute: "about a minute",
minutes: "%d minutes",
hour: "about an hour",
hours: "about %d hours",
day: "a day",
days: "%d days",
month: "about a month",
months: "%d months",
year: "about a year",
years: "%d years",
wordSeparator: " ",
numbers: []
}
},
inWords: function(distanceMillis) {
if (!this.settings.allowPast && ! this.settings.allowFuture) {
throw 'timeago allowPast and allowFuture settings can not both be set to false.';
}
var $l = this.settings.strings;
var prefix = $l.prefixAgo;
var suffix = $l.suffixAgo;
if (this.settings.allowFuture) {
if (distanceMillis < 0) {
prefix = $l.prefixFromNow;
suffix = $l.suffixFromNow;
}
}
if (!this.settings.allowPast && distanceMillis >= 0) {
return this.settings.strings.inPast;
}
var seconds = Math.abs(distanceMillis) / 1000;
var minutes = seconds / 60;
var hours = minutes / 60;
var days = hours / 24;
var years = days / 365;
function substitute(stringOrFunction, number) {
var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, distanceMillis) : stringOrFunction;
var value = ($l.numbers && $l.numbers[number]) || number;
return string.replace(/%d/i, value);
}
var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) ||
seconds < 90 && substitute($l.minute, 1) ||
minutes < 45 && substitute($l.minutes, Math.round(minutes)) ||
minutes < 90 && substitute($l.hour, 1) ||
hours < 24 && substitute($l.hours, Math.round(hours)) ||
hours < 42 && substitute($l.day, 1) ||
days < 30 && substitute($l.days, Math.round(days)) ||
days < 45 && substitute($l.month, 1) ||
days < 365 && substitute($l.months, Math.round(days / 30)) ||
years < 1.5 && substitute($l.year, 1) ||
substitute($l.years, Math.round(years));
var separator = $l.wordSeparator || "";
if ($l.wordSeparator === undefined) { separator = " "; }
return $.trim([prefix, words, suffix].join(separator));
},
parse: function(iso8601) {
var s = $.trim(iso8601);
s = s.replace(/\.\d+/,""); // remove milliseconds
s = s.replace(/-/,"/").replace(/-/,"/");
s = s.replace(/T/," ").replace(/Z/," UTC");
s = s.replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2"); // -04:00 -> -0400
s = s.replace(/([\+\-]\d\d)$/," $100"); // +09 -> +0900
return new Date(s);
},
datetime: function(elem) {
var iso8601 = $t.isTime(elem) ? $(elem).attr("datetime") : $(elem).attr("title");
return $t.parse(iso8601);
},
isTime: function(elem) {
// jQuery's `is()` doesn't play well with HTML5 in IE
return $(elem).get(0).tagName.toLowerCase() === "time"; // $(elem).is("time");
}
});
// functions that can be called via $(el).timeago('action')
// init is default when no action is given
// functions are called with context of a single element
var functions = {
init: function() {
functions.dispose.call(this);
var refresh_el = $.proxy(refresh, this);
refresh_el();
var $s = $t.settings;
if ($s.refreshMillis > 0) {
this._timeagoInterval = setInterval(refresh_el, $s.refreshMillis);
}
},
update: function(timestamp) {
var date = (timestamp instanceof Date) ? timestamp : $t.parse(timestamp);
$(this).data('timeago', { datetime: date });
if ($t.settings.localeTitle) {
$(this).attr("title", date.toLocaleString());
}
refresh.apply(this);
},
updateFromDOM: function() {
$(this).data('timeago', { datetime: $t.parse( $t.isTime(this) ? $(this).attr("datetime") : $(this).attr("title") ) });
refresh.apply(this);
},
dispose: function () {
if (this._timeagoInterval) {
window.clearInterval(this._timeagoInterval);
this._timeagoInterval = null;
}
}
};
$.fn.timeago = function(action, options) {
var fn = action ? functions[action] : functions.init;
if (!fn) {
throw new Error("Unknown function name '"+ action +"' for timeago");
}
// each over objects here and call the requested function
this.each(function() {
fn.call(this, options);
});
return this;
};
function refresh() {
var $s = $t.settings;
//check if it's still visible
if ($s.autoDispose && !$.contains(document.documentElement,this)) {
//stop if it has been removed
$(this).timeago("dispose");
return this;
}
var data = prepareData(this);
if (!isNaN(data.datetime)) {
if ( $s.cutoff === 0 || Math.abs(distance(data.datetime)) < $s.cutoff) {
$(this).text(inWords(data.datetime));
} else {
if ($(this).attr('title').length > 0) {
$(this).text($(this).attr('title'));
}
}
}
return this;
}
function prepareData(element) {
element = $(element);
if (!element.data("timeago")) {
element.data("timeago", { datetime: $t.datetime(element) });
var text = $.trim(element.text());
if ($t.settings.localeTitle) {
element.attr("title", element.data('timeago').datetime.toLocaleString());
} else if (text.length > 0 && !($t.isTime(element) && element.attr("title"))) {
element.attr("title", text);
}
}
return element.data("timeago");
}
function inWords(date) {
return $t.inWords(distance(date));
}
function distance(date) {
return (new Date().getTime() - date.getTime());
}
// fix for IE6 suckage
document.createElement("abbr");
document.createElement("time");
}));

@ -0,0 +1,80 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ name }} Inbox</title>
<link rel="stylesheet" href="http://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.css"/>
<script src="http://code.jquery.com/jquery-1.11.1.min.js"></script>
<script src="http://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.js"></script>
<script type="text/javascript" src="/js/jquery.timeago.js"></script>
<link rel="stylesheet" type="text/css" href="/css/style.css"/>
<script type="text/javascript">
$.mobile.ajaxEnabled = false;
jQuery(document).ready(function() {
jQuery("time.timeago").timeago();
});
</script>
</head>
<body>
<div data-role="page">
{% if error %}
<div class="error">{{ error }}</div>
{% else %}
{% if inbox.records == 0 %}
<small>Inbox is empty.</small>
{% else %}
<div data-role="footer">
<div data-role="navbar">
<ul>
<li><a href="/inbox" id="email" class="ui-btn-active" data-icon="bars">Inbox</a></li>
<li><a href="/inbox/new" id="new" data-icon="mail">New</a></li>
<li><a href="/inbox/profile" id="profile" data-icon="user">Profile</a></li>
</ul>
</div>
</div>
<ul class="inbox" data-role="listview" data-inset="true">
{% for message in inbox.messages %}
<li class="message">
<a href="/inbox/{{ message.id }}" class="{{ 'unread' if not message.seen_at else 'read' }}">
<h2>{{ message.sender_name }}@{{ message.sender }}</h2>
<p>
Received
<time class="timeago" datetime="{{ message.created_at }}">{{ message.created_at }}</time>
{% if message.seen_at %}, seen
<time class="timeago" datetime="{{ message.seen_at }}">{{ message.seen_at }}</time>
{% endif %}.
</p>
</a>
</li>
{% endfor %}
</ul>
{% endif %}
{% if inbox.pages > 1 %}
<div data-role="navbar">
<ul>
{% if page > 1 %}
<li><a href="/inbox?p={{ page - 1 }}" class="ui-btn">Prev</a></li>
{% endif %}
{% if page < inbox.pages %}
<li><a href="/inbox?p={{ page + 1 }}" class="ui-btn">Next</a></li>
{% endif %}
</ul>
</div>
{% endif %}
{% endif %}
</div>
</body>
</html>

@ -0,0 +1,70 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Message from {{ message.sender_name }}</title>
<link rel="stylesheet" href="http://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.css"/>
<script src="http://code.jquery.com/jquery-1.11.1.min.js"></script>
<script src="http://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.js"></script>
<script type="text/javascript" src="/js/jquery.timeago.js"></script>
<link rel="stylesheet" type="text/css" href="/css/style.css"/>
<script type="text/javascript">
$.mobile.ajaxEnabled = false;
jQuery(document).ready(function() {
jQuery("time.timeago").timeago();
// mark the message as read
jQuery.get("/inbox/{{ message.id }}/seen", function(res){
console.log(res);
});
});
</script>
</head>
<body>
<div data-role="page">
{% if error %}
<div class="error">{{ error }}</div>
{% else %}
<div data-role="footer">
<div data-role="navbar">
<ul>
<li><a href="/inbox" id="email" data-icon="back">Back</a></li>
<li><a href="/inbox/new?to={{ message.sender }}" id="reply" data-icon="edit">Reply</a></li>
<li><a href="/inbox/{{ message.id }}/deleted" id="delete" data-icon="delete"
onclick="return confirm('Are you sure?')">Delete</a></li>
</ul>
</div>
</div>
<p class="messagebody">
<span style="color: #888">
Message from {{ message.sender_name }}@{{ message.sender }}, received
<time class="timeago" datetime="{{ message.created_at }}">{{ message.created_at }}</time>
{% if message.seen_at %}, seen
<time class="timeago" datetime="{{ message.seen_at }}">{{ message.seen_at }}</time>{% endif %}.</span>
<br/>
<br/>
{% if message.data %}
<span style="white-space: pre-line">{{ message.data }}</span>
{% else %}
<small>This message is empty.</small>
{% endif %}
</p>
{% endif %}
</div>
</body>
</html>

@ -0,0 +1,81 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
{% if to %}
<title>Reply to {{ to }}</title>
{% else %}
<title>New Message</title>
{% endif %}
<link rel="stylesheet" href="http://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.css"/>
<script src="http://code.jquery.com/jquery-1.11.1.min.js"></script>
<script src="http://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.js"></script>
<script type="text/javascript" src="/js/jquery.timeago.js"></script>
<link rel="stylesheet" type="text/css" href="/css/style.css"/>
<script type="text/javascript">
$.mobile.ajaxEnabled = false;
$(function(){
$("#message_form").submit(function(e) {
e.preventDefault();
var form = $(this);
var url = form.attr('action');
$.ajax({
type: "POST",
url: url,
data: form.serialize(),
success: function(data) {
if( data.error ) {
if( data.error.indexOf('404') != -1 )
alert('Fingerprint not found.');
else if( data.error.indexOf('aborted') != -1 )
alert('Empty or invalid message.');
else
alert(data.error);
return;
}
alert("Message sent!");
document.location.href = "/inbox";
}
});
});
});
</script>
</head>
<body>
<div data-role="page">
<div data-role="footer">
<div data-role="navbar">
<ul>
<li><a href="/inbox" id="email" data-icon="back">Back</a></li>
</ul>
</div>
</div>
<div style="padding: 1em">
<form id="message_form" method="POST" action="/inbox/send">
<label for="to">To:</label>
<input type="text" name="to" id="to" value="{{ to }}">
<label for="message">Message:</label>
<textarea cols="40" rows="8" name="message" id="message"></textarea>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="submit" class="button" value="Send"/>
</form>
</div>
</div>
</body>
</html>

@ -0,0 +1,46 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Profile</title>
<link rel="stylesheet" href="http://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.css"/>
<script src="http://code.jquery.com/jquery-1.11.1.min.js"></script>
<script src="http://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.js"></script>
<script type="text/javascript" src="/js/jquery.timeago.js"></script>
<link rel="stylesheet" type="text/css" href="/css/style.css"/>
<script type="text/javascript">
$.mobile.ajaxEnabled = false;
</script>
</head>
<body>
<div data-role="page">
<div data-role="footer">
<div data-role="navbar">
<ul>
<li><a href="/inbox" id="email" data-icon="back">Back</a></li>
</ul>
</div>
</div>
<div style="padding: 1em">
<label for="name">Name</label>
<h4 id="name">{{ name }}</h4>
<label for="fingerprint">Fingerprint</label>
<h4 id="fingerprint">{{ fingerprint }}</h4>
<label for="data">Data</label>
<pre><code id="data">{{ data }}</code></pre>
</div>
</div>
</body>
</html>