From b50acd364c730ab33ed8e0504d2a26f860025ca3 Mon Sep 17 00:00:00 2001 From: dadav <33197631+dadav@users.noreply.github.com> Date: Thu, 7 Nov 2019 07:28:32 +0100 Subject: [PATCH] Add webcfg --- pwnagotchi/defaults.yml | 3 +- pwnagotchi/plugins/default/webcfg.py | 511 +++++++++++++++++++++++++++ 2 files changed, 513 insertions(+), 1 deletion(-) create mode 100644 pwnagotchi/plugins/default/webcfg.py diff --git a/pwnagotchi/defaults.yml b/pwnagotchi/defaults.yml index 0acce4d..f083270 100644 --- a/pwnagotchi/defaults.yml +++ b/pwnagotchi/defaults.yml @@ -109,7 +109,8 @@ main: epoch: 'oo oo oo oo oo oo oo' peer_detected: 'oo oo oo oo oo oo oo' peer_lost: 'oo oo oo oo oo oo oo' - + webcfg: + enabled: false # monitor interface to use iface: mon0 # command to run to bring the mon interface up in case it's not up already diff --git a/pwnagotchi/plugins/default/webcfg.py b/pwnagotchi/plugins/default/webcfg.py new file mode 100644 index 0000000..52532f2 --- /dev/null +++ b/pwnagotchi/plugins/default/webcfg.py @@ -0,0 +1,511 @@ +import logging +import json +import yaml +import _thread +import pwnagotchi.plugins as plugins +from pwnagotchi import restart +from flask import abort +from flask import render_template_string + + +INDEX = """ +<html> + <head> + <meta name="viewport" content="width=device-width, user-scalable=0" /> + <title> + webcfg + </title> + <style> + #divTop { + position: -webkit-sticky; + position: sticky; + top: 0px; + width: 100%; + font-size: 16px; + padding: 5px; + border: 1px solid #ddd; + margin-bottom: 5px; + } + + #searchText { + width: 100%; + } + + table { + table-layout: auto; + width: 100%; + } + + table, th, td { + border: 1px solid black; + border-collapse: collapse; + } + + th, td { + padding: 15px; + text-align: left; + } + + table tr:nth-child(even) { + background-color: #eee; + } + + table tr:nth-child(odd) { + background-color: #fff; + } + + table th { + background-color: black; + color: white; + } + + .remove { + background-color: #f44336; + color: white; + border: 2px solid #f44336; + padding: 4px 8px; + text-align: center; + text-decoration: none; + display: inline-block; + font-size: 12px; + margin: 4px 2px; + -webkit-transition-duration: 0.4s; /* Safari */ + transition-duration: 0.4s; + cursor: pointer; + } + + .remove:hover { + background-color: white; + color: black; + } + + #btnSave { + position: -webkit-sticky; + position: sticky; + bottom: 0px; + width: 100%; + background-color: #0061b0; + border: none; + color: white; + padding: 15px 32px; + text-align: center; + text-decoration: none; + display: inline-block; + font-size: 16px; + cursor: pointer; + float: right; + } + + #divTop { + display: table; + width: 100%; + } + #divTop > * { + display: table-cell; + } + #divTop > span { + width: 1%; + } + #divTop > input { + width: 100%; + } + + @media screen and (max-width:700px) { + table, tr, td { + padding:0; + border:1px solid black; + } + + table { + border:none; + } + + tr:first-child, thead, th { + display:none; + border:none; + } + + tr { + float: left; + width: 100%; + margin-bottom: 2em; + } + + table tr:nth-child(odd) { + background-color: #eee; + } + + td { + float: left; + width: 100%; + padding:1em; + } + + td::before { + content:attr(data-label); + word-wrap: break-word; + background: #eee; + border-right:2px solid black; + width: 20%; + float:left; + padding:1em; + font-weight: bold; + margin:-1em 1em -1em -1em; + } + + .del_btn_wrapper { + content:attr(data-label); + word-wrap: break-word; + background: #eee; + border-right:2px solid black; + width: 20%; + float:left; + padding:1em; + font-weight: bold; + margin:-1em 1em -1em -1em; + } + } + </style> + </head> + <body> + <div id="divTop"> + <input type="text" id="searchText" onkeyup="filterTable()" placeholder="Search for options ..." title="Type an option name"> + <span><select id="selAddType"><option value="text">Text</option><option value="number">Number</option></select></span> + <span><button id="btnAdd" type="button" onclick="addOption()">+</button></span> + </div> + <div id="content"></div> + <button id="btnSave" type="button" onclick="saveConfig()">Save</button> + </body> + <script type="text/javascript"> + function addOption() { + var input, table, tr, td, divDelBtn, btnDel, selType, selTypeVal; + input = document.getElementById("searchText"); + inputVal = input.value; + selType = document.getElementById("selAddType"); + selTypeVal = selType.options[selType.selectedIndex].value; + table = document.getElementById("tableOptions"); + if (table) { + tr = table.insertRow(); + // del button + divDelBtn = document.createElement("div"); + divDelBtn.className = "del_btn_wrapper"; + td = document.createElement("td"); + td.setAttribute("data-label", ""); + btnDel = document.createElement("Button"); + btnDel.innerHTML = "X"; + btnDel.onclick = function(){ delRow(this);}; + btnDel.className = "remove"; + divDelBtn.appendChild(btnDel); + td.appendChild(divDelBtn); + tr.appendChild(td); + // option + td = document.createElement("td"); + td.setAttribute("data-label", "Option"); + td.innerHTML = inputVal; + tr.appendChild(td); + // value + td = document.createElement("td"); + td.setAttribute("data-label", "Value"); + input = document.createElement("input"); + input.type = selTypeVal; + input.value = ""; + td.appendChild(input); + tr.appendChild(td); + + input.value = ""; + } + } + + function saveConfig(){ + // get table + var table = document.getElementById("tableOptions"); + if (table) { + var json = tableToJson(table); + sendJSON("webcfg/save-config", json, function(response) { + if (response) { + if (response.status == "200") { + alert("Config got updated"); + } else { + alert("Error while updating the config (err-code: " + response.status + ")"); + } + } + }); + } + } + + function filterTable(){ + var input, filter, table, tr, td, i, txtValue; + input = document.getElementById("searchText"); + filter = input.value.toUpperCase(); + table = document.getElementById("tableOptions"); + if (table) { + tr = table.getElementsByTagName("tr"); + + for (i = 0; i < tr.length; i++) { + td = tr[i].getElementsByTagName("td")[1]; + if (td) { + txtValue = td.textContent || td.innerText; + if (txtValue.toUpperCase().indexOf(filter) > -1) { + tr[i].style.display = ""; + }else{ + tr[i].style.display = "none"; + } + } + } + } + } + + function sendJSON(url, data, callback) { + var xobj = new XMLHttpRequest(); + var csrf = "{{ csrf_token() }}"; + xobj.open('POST', url); + xobj.setRequestHeader("Content-Type", "application/json"); + xobj.setRequestHeader('x-csrf-token', csrf); + xobj.onreadystatechange = function () { + if (xobj.readyState == 4) { + callback(xobj); + } + }; + xobj.send(JSON.stringify(data)); + } + + function loadJSON(url, callback) { + var xobj = new XMLHttpRequest(); + xobj.overrideMimeType("application/json"); + xobj.open('GET', url, true); + xobj.onreadystatechange = function () { + if (xobj.readyState == 4 && xobj.status == "200") { + callback(JSON.parse(xobj.responseText)); + } + }; + xobj.send(null); + } + + // https://stackoverflow.com/questions/19098797/fastest-way-to-flatten-un-flatten-nested-json-objects + function unFlattenJson(data) { + "use strict"; + if (Object(data) !== data || Array.isArray(data)) + return data; + var result = {}, cur, prop, idx, last, temp, inarray; + for(var p in data) { + cur = result, prop = "", last = 0, inarray = false; + do { + idx = p.indexOf(".", last); + temp = p.substring(last, idx !== -1 ? idx : undefined); + inarray = temp.startsWith('#') && !isNaN(parseInt(temp.substring(1))) + cur = cur[prop] || (cur[prop] = (inarray ? [] : {})); + if (inarray){ + prop = temp.substring(1); + }else{ + prop = temp; + } + last = idx + 1; + } while(idx >= 0); + cur[prop] = data[p]; + } + return result[""]; + } + + function flattenJson(data) { + var result = {}; + function recurse (cur, prop) { + if (Object(cur) !== cur) { + result[prop] = cur; + } else if (Array.isArray(cur)) { + for(var i=0, l=cur.length; i<l; i++) + recurse(cur[i], prop ? prop+".#"+i : ""+i); + if (l == 0) + result[prop] = []; + } else { + var isEmpty = true; + for (var p in cur) { + isEmpty = false; + recurse(cur[p], prop ? prop+"."+p : p); + } + if (isEmpty) + result[prop] = {}; + } + } + recurse(data, ""); + return result; + } + + function delRow(btn) { + var tr = btn.parentNode.parentNode.parentNode; + tr.parentNode.removeChild(tr); + } + + function jsonToTable(json) { + var table = document.createElement("table"); + table.id = "tableOptions"; + + // create header + var tr = table.insertRow(); + var thDel = document.createElement("th"); + thDel.innerHTML = ""; + var thOpt = document.createElement("th"); + thOpt.innerHTML = "Option"; + var thVal = document.createElement("th"); + thVal.innerHTML = "Value"; + tr.appendChild(thDel); + tr.appendChild(thOpt); + tr.appendChild(thVal); + + var td, divDelBtn, btnDel; + // iterate over keys + Object.keys(json).forEach(function(key) { + tr = table.insertRow(); + // del button + divDelBtn = document.createElement("div"); + divDelBtn.className = "del_btn_wrapper"; + td = document.createElement("td"); + td.setAttribute("data-label", ""); + btnDel = document.createElement("Button"); + btnDel.innerHTML = "X"; + btnDel.onclick = function(){ delRow(this);}; + btnDel.className = "remove"; + divDelBtn.appendChild(btnDel); + td.appendChild(divDelBtn); + tr.appendChild(td); + // option + td = document.createElement("td"); + td.setAttribute("data-label", "Option"); + td.innerHTML = key; + tr.appendChild(td); + // value + td = document.createElement("td"); + td.setAttribute("data-label", "Value"); + if(typeof(json[key])==='boolean'){ + input = document.createElement("select"); + input.setAttribute("id", "boolSelect"); + tvalue = document.createElement("option"); + tvalue.setAttribute("value", "true"); + ttext = document.createTextNode("True") + tvalue.appendChild(ttext); + fvalue = document.createElement("option"); + fvalue.setAttribute("value", "false"); + ftext = document.createTextNode("False"); + fvalue.appendChild(ftext); + input.appendChild(tvalue); + input.appendChild(fvalue); + input.value = json[key]; + document.body.appendChild(input); + td.appendChild(input); + tr.appendChild(td); + } else { + input = document.createElement("input"); + if(Array.isArray(json[key])) { + input.type = 'text'; + input.value = '[]'; + }else{ + input.type = typeof(json[key]); + input.value = json[key]; + } + td.appendChild(input); + tr.appendChild(td); + } + }); + + return table; + } + + function tableToJson(table) { + var rows = table.getElementsByTagName("tr"); + var i, td, key, value; + var json = {}; + + for (i = 0; i < rows.length; i++) { + td = rows[i].getElementsByTagName("td"); + if (td.length == 3) { + // td[0] = del button + key = td[1].textContent || td[1].innerText; + var input = td[2].getElementsByTagName("input"); + var select = td[2].getElementsByTagName("select"); + if (input && input != undefined && input.length > 0 ) { + if (input[0].type == "text") { + if (input[0].value.startsWith("[") && input[0].value.endsWith("]")) { + json[key] = JSON.parse(input[0].value); + }else{ + json[key] = input[0].value; + } + }else if (input[0].type == "number") { + json[key] = Number(input[0].value); + } + } else if(select && select != undefined && select.length > 0) { + var myValue = select[0].options[select[0].selectedIndex].value; + json[key] = myValue === 'true'; + } + } + } + return unFlattenJson(json); + } + + loadJSON("webcfg/get-config", function(response) { + var flat_json = flattenJson(response); + var table = jsonToTable(flat_json); + var divContent = document.getElementById("content"); + divContent.innerHTML = ""; + divContent.appendChild(table); + }); + </script> +</html> +""" + +def serializer(obj): + if isinstance(obj, set): + return list(obj) + raise TypeError + +class WebConfig(plugins.Plugin): + __author__ = '33197631+dadav@users.noreply.github.com' + __version__ = '1.0.0' + __license__ = 'GPL3' + __description__ = 'This plugin allows the user to make runtime changes.' + + def __init__(self): + self.ready = False + + def on_ready(self, agent): + self.config = agent.config() + self.mode = "MANU" if agent.mode == "manual" else "AUTO" + self.ready = True + + def on_internet_available(self, agent): + self.config = agent.config() + self.mode = "MANU" if agent.mode == "manual" else "AUTO" + self.ready = True + + def on_loaded(self): + """ + Gets called when the plugin gets loaded + """ + logging.info("webcfg: Plugin loaded.") + + + def on_webhook(self, path, request): + """ + Serves the current configuration + """ + if not self.ready: + return "Plugin not ready" + + if request.method == "GET": + if path == "/" or not path: + return render_template_string(INDEX) + elif path == "get-config": + # send configuration + return json.dumps(self.config, default=serializer) + else: + abort(404) + elif request.method == "POST": + if path == "save-config": + try: + with open('/etc/pwnagotchi/config.yml', 'w') as config_file: + yaml.safe_dump(request.get_json(), config_file, encoding='utf-8', + allow_unicode=True, default_flow_style=False) + + _thread.start_new_thread(restart, (self.mode,)) + return "success" + except yaml.YAMLError as yaml_ex: + return "config error" + abort(404)