add easy webinterface
parent
d20e749038
commit
16fdeb85d4
@ -1,6 +1,13 @@
|
||||
state_path = "/tmp/wifictld.json"
|
||||
answer = false
|
||||
|
||||
|
||||
[webserver]
|
||||
enable = true
|
||||
bind = ":8080"
|
||||
webroot = "./webroot/"
|
||||
|
||||
[[interfaces]]
|
||||
ifname = "wlp4s0"
|
||||
#port = 1000
|
||||
#ip_address = "ff02::31f1"
|
||||
ifname = "wlp4s0"
|
||||
#port = 1000
|
||||
#ip_address = "ff02::31f1"
|
||||
|
@ -0,0 +1,16 @@
|
||||
package data
|
||||
|
||||
import "net"
|
||||
|
||||
type HardwareAddr struct{ net.HardwareAddr }
|
||||
|
||||
//MarshalJSON to bytearray
|
||||
func (a HardwareAddr) MarshalText() ([]byte, error) {
|
||||
return []byte(a.String()), nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON from bytearray
|
||||
func (a HardwareAddr) UnmarshalText(data []byte) (err error) {
|
||||
a.HardwareAddr, err = net.ParseMAC(string(data))
|
||||
return
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package web
|
||||
|
||||
type Config struct {
|
||||
Enable bool `toml:"enable"`
|
||||
Bind string `toml:"bind"`
|
||||
Webroot string `toml:"webroot"`
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/NYTimes/gziphandler"
|
||||
"github.com/bdlm/log"
|
||||
|
||||
"dev.sum7.eu/genofire/golang-lib/websocket"
|
||||
|
||||
"dev.sum7.eu/genofire/wifictld-analyzer/data"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
web *http.Server
|
||||
ws *websocket.WebsocketHandlerService
|
||||
}
|
||||
|
||||
// New creates a new webserver and starts it
|
||||
func New(config *Config) *Server {
|
||||
ws := websocket.NewWebsocketHandlerService()
|
||||
ws.Listen("/ws")
|
||||
|
||||
http.Handle("/", gziphandler.GzipHandler(http.FileServer(http.Dir(config.Webroot))))
|
||||
return &Server{
|
||||
web: &http.Server{
|
||||
Addr: config.Bind,
|
||||
},
|
||||
ws: ws,
|
||||
}
|
||||
}
|
||||
|
||||
func (srv *Server) Handler(addr *net.UDPAddr, msg *data.SocketMSG) (*data.SocketMSG, error) {
|
||||
srv.ws.SendAll(&websocket.Message{
|
||||
Subject: "wifictld_pkg",
|
||||
Body: map[string]interface{}{
|
||||
"ip": addr.IP,
|
||||
"msg": msg,
|
||||
},
|
||||
})
|
||||
return msg, nil
|
||||
}
|
||||
func (srv *Server) Start() {
|
||||
// service connections
|
||||
if err := srv.web.ListenAndServe(); err != http.ErrServerClosed {
|
||||
log.Panicf("webserver crashed: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (srv *Server) Close() {
|
||||
srv.web.Close()
|
||||
srv.ws.Close()
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestWebserver(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
srv := New(&Config{
|
||||
Bind: ":12345",
|
||||
Webroot: "/tmp",
|
||||
})
|
||||
assert.NotNil(srv)
|
||||
|
||||
go srv.Start()
|
||||
|
||||
time.Sleep(time.Millisecond * 200)
|
||||
|
||||
assert.Panics(func() {
|
||||
srv.Start()
|
||||
}, "not allowed to listen twice")
|
||||
|
||||
srv.Close()
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
wget -O vanilla-framework.css https://assets.ubuntu.com/v1/vanilla-framework-version-1.8.1.min.css
|
||||
wget -O vue.js https://cdn.jsdelivr.net/npm/vue@2.6.7/dist/vue.js
|
||||
wget -O vue-route.js https://unpkg.com/vue-router/dist/vue-router.js
|
||||
wget -O vuex.js https://unpkg.com/vuex@2.0.0/dist/vuex.min.js
|
||||
wget -O vue-websocket.js https://raw.githubusercontent.com/nathantsoi/vue-native-websocket/master/dist/build.js
|
||||
wget -O vue-websocket.js.map https://raw.githubusercontent.com/nathantsoi/vue-native-websocket/master/dist/build.js.map
|
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@ -0,0 +1,10 @@
|
||||
.p-navigation__logo {
|
||||
font-size: 1.5rem;
|
||||
line-height: 3rem;
|
||||
}
|
||||
.p-navigation .p-navigation__logo .p-navigation__link {
|
||||
color: red;
|
||||
}
|
||||
.p-navigation .p-navigation__logo.online .p-navigation__link {
|
||||
color: black;
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>Wifictld</title>
|
||||
<link rel="stylesheet" href="assets/vanilla-framework.css" />
|
||||
<link rel="stylesheet" href="css/main.css" />
|
||||
<script src="assets/vue.js"></script>
|
||||
<script src="assets/vue-route.js"></script>
|
||||
<script src="assets/vuex.js"></script>
|
||||
<script src="assets/vue-websocket.js"></script>
|
||||
</head>
|
||||
<body role="document">
|
||||
<div id="app">
|
||||
<header id="navigation" class="p-navigation">
|
||||
<div class="p-navigation__banner">
|
||||
<navbar-logo></navbar-logo>
|
||||
<a href="#navigation" class="p-navigation__toggle--open" title="menu">Menu</a>
|
||||
<a href="#navigation-closed" class="p-navigation__toggle--close" title="close menu">Close menu</a>
|
||||
</div>
|
||||
<nav class="p-navigation__nav" role="menubar">
|
||||
<span class="u-off-screen">
|
||||
<a href="#main-content">Jump to main content</a>
|
||||
</span>
|
||||
<ul class="p-navigation__links" role="menu">
|
||||
<router-link :to="{name: 'aps' }" tag="li" class="p-navigation__link" active-class="is-selected">
|
||||
<a>Access Points</a>
|
||||
</router-link>
|
||||
<router-link :to="{name: 'clients' }" tag="li" class="p-navigation__link" active-class="is-selected">
|
||||
<a>Clients</a>
|
||||
</router-link>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
<div class="content">
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
</div>
|
||||
<script src="js/view/accesspoint.js"></script>
|
||||
<script src="js/view/clients.js"></script>
|
||||
<script src="js/store.js"></script>
|
||||
<script src="js/data.js"></script>
|
||||
<script src="js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,20 @@
|
||||
store.commit('setEvent', {
|
||||
subject:"wifictld_pkg",
|
||||
callback: (state, msg) => {
|
||||
|
||||
// add to ap list
|
||||
const ip = msg.body.ip;
|
||||
if(state.controller._ap[ip] === undefined){
|
||||
state.controller.ap.push(ip)
|
||||
state.controller._ap[ip] = null;
|
||||
}
|
||||
|
||||
// add clients
|
||||
const client = msg.body.msg.client;
|
||||
if (state.controller.clients[client.addr] === undefined) {
|
||||
state.controller._clients.push(client.addr)
|
||||
}
|
||||
client.ap = ip;
|
||||
state.controller.clients[client.addr] = client;
|
||||
}
|
||||
})
|
@ -0,0 +1,35 @@
|
||||
const router = new VueRouter({
|
||||
store,
|
||||
routes: [
|
||||
{ path: '/ap', component: ViewAccessPoints, name: "aps" },
|
||||
{ path: '/ap/clients/:ip', component: ViewClients, name: "ap.clients"},
|
||||
{ path: '/clients', component: ViewClients, name: "clients" },
|
||||
{ path: '/', redirect: '/ap' }
|
||||
]
|
||||
})
|
||||
|
||||
VueNativeSock.default.install(Vue, `//${location.host}${location.pathname}ws`, {
|
||||
store: store,
|
||||
reconnection: true,
|
||||
reconnectionDelay: 5000,
|
||||
format: 'json',
|
||||
})
|
||||
|
||||
const NavbarLogo = {
|
||||
template: `<div class="p-navigation__logo" v-bind:class="{ online: isOnline }">
|
||||
<a class="p-navigation__link" href="/">Wifictld</a>
|
||||
</div>`,
|
||||
computed: {
|
||||
isOnline () {
|
||||
return this.$store.state.socket.isConnected
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const app = new Vue({
|
||||
el: '#app',
|
||||
store,
|
||||
router,
|
||||
components: { NavbarLogo },
|
||||
})
|
@ -0,0 +1,104 @@
|
||||
|
||||
function newUUID () {
|
||||
/* eslint-disable */
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||
const r = Math.random() * 16 | 0,
|
||||
v = c === 'x' ? r : r & 0x3 | 0x8;
|
||||
return v.toString(16);
|
||||
});
|
||||
/* eslint-enable */
|
||||
}
|
||||
|
||||
const store = new Vuex.Store({
|
||||
state: {
|
||||
socket: {
|
||||
_session: localStorage.getItem('session'),
|
||||
isConnected: false,
|
||||
reconnectError: false,
|
||||
eventMSGID: {},
|
||||
eventTo: {},
|
||||
},
|
||||
controller: {
|
||||
_ap : {},
|
||||
ap: [],
|
||||
_clients: [],
|
||||
clients: {},
|
||||
},
|
||||
},
|
||||
mutations:{
|
||||
SOCKET_ONOPEN (state, event) {
|
||||
state.socket.isConnected = true
|
||||
},
|
||||
SOCKET_ONCLOSE (state, event) {
|
||||
state.socket.isConnected = false
|
||||
},
|
||||
SOCKET_ONERROR (state, event) {
|
||||
console.error(state, event)
|
||||
},
|
||||
// default handler called for all methods
|
||||
SOCKET_ONMESSAGE (state, msg) {
|
||||
if (msg.subject === 'session_init') {
|
||||
if (state.socket._session === null) {
|
||||
state.socket._session = newUUID();
|
||||
localStorage.setItem('session', state.socket._session);
|
||||
}
|
||||
msg.id = state.socket._session;
|
||||
Vue.prototype.$socket.sendObj(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
const msgFunc = state.socket.eventMSGID[msg.id];
|
||||
if (msgFunc) {
|
||||
msgFunc(state, msg);
|
||||
delete state.socket.eventMSGID[msg.id];
|
||||
return;
|
||||
}
|
||||
|
||||
const eventFuncs = state.socket.eventTo[msg.subject];
|
||||
if (typeof eventFuncs === 'object' && eventFuncs.length > 0) {
|
||||
for (const key in eventFuncs) {
|
||||
const func = eventFuncs[key];
|
||||
if (func) {
|
||||
func(state, msg);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
console.log(`unable to identify message: ${msg.subject}`);
|
||||
|
||||
},
|
||||
// mutations for reconnect methods
|
||||
SOCKET_RECONNECT(state, count) {
|
||||
console.info(state, "reconnect:", count)
|
||||
},
|
||||
SOCKET_RECONNECT_ERROR(state) {
|
||||
state.socket.reconnectError = true;
|
||||
},
|
||||
setEvent (state, data) {
|
||||
state.socket.eventTo[data.subject] = [data.callback];
|
||||
},
|
||||
addEvent (state, data) {
|
||||
if (typeof state.socket.eventTo[data.subject] !== 'object') {
|
||||
state.socket.eventTo[data.subject] = [];
|
||||
}
|
||||
state.socket.eventTo[data.subject].push(data.callback);
|
||||
},
|
||||
delEvent (state, data) {
|
||||
if (typeof state.socket.eventTo[data.subject] === 'object' && state.socket.eventTo[data.subject].length > 1) {
|
||||
state.socket.eventTo[data.subject].pop(data.callback);
|
||||
} else {
|
||||
state.socket.eventTo[data.subject] = [];
|
||||
}
|
||||
},
|
||||
call (state, data) {
|
||||
if (!data.msg.id) {
|
||||
data.msg.id = newUUID();
|
||||
}
|
||||
const ret = Vue.prototype.$socket.sendObj(data.msg);
|
||||
if (typeof data.callback === 'function') {
|
||||
state.socket.eventMSGID[data.msg.id] = data.callback;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
})
|
@ -0,0 +1,37 @@
|
||||
const ViewAccessPoints = {
|
||||
template: `<div class="row">
|
||||
<p><span class="p-heading--one">AccessPoints</span></p>
|
||||
<table class="p-table--mobile-card" role="grid">
|
||||
<thead>
|
||||
<tr role="row">
|
||||
<th scope="col" role="columnheader" aria-sort="none">IP</th>
|
||||
<th scope="col" role="columnheader" aria-sort="none" class="u-align--right">Users</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr role="row" v-for="item in getAPs">
|
||||
<td role="rowheader" aria-label="IP">{{ item.ip.substring(21) }}</td>
|
||||
<td role="gridcell" aria-label="Users" class="u-align--right">
|
||||
<router-link :to="{ name: 'ap.clients', params: { ip: item.ip }}">
|
||||
{{ item.clients }}
|
||||
</router-link>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>`,
|
||||
computed: {
|
||||
getAPs () {
|
||||
const state = this.$store.state;
|
||||
return state.controller.ap.map(function (ip) {
|
||||
return {
|
||||
ip: ip,
|
||||
clients: state.controller._clients.filter(function (hwaddr) {
|
||||
const client = state.controller.clients[hwaddr];
|
||||
return client.ap === ip;
|
||||
}).length,
|
||||
};
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
const ViewClients = {
|
||||
template: `<div class="row">
|
||||
<p>
|
||||
<span class="p-heading--one">Clients</span>
|
||||
<sub v-if="$route.params.ip">of Router {{ $route.params.ip.substring(21) }}</sub>
|
||||
</p>
|
||||
<table class="p-table--mobile-card" role="grid">
|
||||
<thead>
|
||||
<tr role="row">
|
||||
<th scope="col" role="columnheader" aria-sort="none">Addr</th>
|
||||
<th scope="col" role="columnheader" aria-sort="none">AP</th>
|
||||
<th scope="col" role="columnheader" aria-sort="none" class="u-align--right">FreqHighs</th>
|
||||
<th scope="col" role="columnheader" aria-sort="none" class="u-align--right">SignalLow</th>
|
||||
<th scope="col" role="columnheader" aria-sort="none" class="u-align--right">SignalHigh</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr role="row" v-for="item in getClients">
|
||||
<td role="rowheader" aria-label="Addr">{{ item.addr.substring(12) }}</td>
|
||||
<td role="gridcell" aria-label="AP">
|
||||
<router-link :to="{ name: 'ap.clients', params: { ip: item.ap }}">
|
||||
{{ item.ap.substring(21) }}
|
||||
</router-link>
|
||||
</td>
|
||||
<td role="gridcell" aria-label="Freq Highs" class="u-align--right">{{ item.freq_highest }}</td>
|
||||
<td role="gridcell" aria-label="Signal LowF" class="u-align--right">{{ item.signal_low_freq }}</td>
|
||||
<td role="gridcell" aria-label="Signal HighF" class="u-align--right">{{ item.signal_high_freq }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>`,
|
||||
computed: {
|
||||
getClients () {
|
||||
const state = this.$store.state,
|
||||
apIP = this.$route.params.ip;
|
||||
|
||||
return state.controller._clients.map(function(addr) {
|
||||
return state.controller.clients[addr];
|
||||
}).filter((client)=> (apIP === undefined || client.ap === apIP));
|
||||
},
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue