add easy webinterface

This commit is contained in:
Martin/Geno 2019-03-01 10:54:19 +01:00
parent d20e749038
commit 16fdeb85d4
No known key found for this signature in database
GPG Key ID: 9D7D3C6BFF600C6A
28 changed files with 15037 additions and 51 deletions

View File

@ -24,7 +24,7 @@ test-my-project:
stage: test
script:
- go get github.com/client9/misspell/cmd/misspell
- misspell -error .
- find . -type f | grep -v webroot/assets | xargs misspell -error
- ./.ci/check-gofmt
- ./.ci/check-testfiles
- go test $(go list ./... | grep -v /vendor/) -v -coverprofile .testCoverage.txt
@ -44,7 +44,10 @@ deploy:
script:
- go install "dev.sum7.eu/$CI_PROJECT_PATH"
- 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
- 'which rsync || ( apt-get update -y && apt-get install rsync -y )'
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null
- rsync -e "ssh -6 -o StrictHostKeyChecking=no -p $SSH_PORT" -a --delete "/builds/$CI_PROJECT_PATH/webroot/" "$CI_PROJECT_NAME@$SSH_HOST":/opt/$CI_PROJECT_NAME/webroot/
- ssh -6 -o StrictHostKeyChecking=no -p $SSH_PORT "$CI_PROJECT_NAME@$SSH_HOST" sudo /usr/bin/systemctl stop $CI_PROJECT_NAME
- scp -6 -o StrictHostKeyChecking=no -P $SSH_PORT "/go/bin/$CI_PROJECT_NAME" "$CI_PROJECT_NAME@$SSH_HOST":/opt/$CI_PROJECT_NAME/bin
- ssh -6 -o StrictHostKeyChecking=no -p $SSH_PORT "$CI_PROJECT_NAME@$SSH_HOST" sudo /usr/bin/systemctl restart $CI_PROJECT_NAME
- ssh -6 -o StrictHostKeyChecking=no -p $SSH_PORT "$CI_PROJECT_NAME@$SSH_HOST" sudo /usr/bin/systemctl start $CI_PROJECT_NAME

View File

@ -1,6 +1,7 @@
package cmd
import (
"net"
"os"
"os/signal"
"syscall"
@ -12,7 +13,9 @@ import (
"dev.sum7.eu/genofire/wifictld-analyzer/capture"
"dev.sum7.eu/genofire/wifictld-analyzer/config"
"dev.sum7.eu/genofire/wifictld-analyzer/controller"
"dev.sum7.eu/genofire/wifictld-analyzer/data"
"dev.sum7.eu/genofire/wifictld-analyzer/database"
"dev.sum7.eu/genofire/wifictld-analyzer/web"
)
// queryCmd represents the query command
@ -31,7 +34,26 @@ var controllerCmd = &cobra.Command{
ctr := controller.NewController(db)
defer ctr.Close()
coll := capture.NewCollector(ctr.Handler, configObj.Interfaces)
var handlers []data.Handler
if configObj.Webserver.Enable {
log.Infof("starting webserver on %s", configObj.Webserver.Bind)
srv := web.New(configObj.Webserver)
go srv.Start()
handlers = append(handlers, srv.Handler)
defer srv.Close()
}
coll := capture.NewCollector(func(addr *net.UDPAddr, msg *data.SocketMSG) (*data.SocketMSG, error) {
for _, a := range handlers {
a(addr, msg)
}
if !configObj.Answer {
ctr.Handler(addr, msg)
return nil, nil
}
return ctr.Handler(addr, msg)
}, configObj.Interfaces)
defer coll.Close()
ctr.Send = coll.Send
@ -47,5 +69,5 @@ var controllerCmd = &cobra.Command{
}
func init() {
RootCmd.AddCommand(controllerCmd)
RootCMD.AddCommand(controllerCmd)
}

View File

@ -58,7 +58,7 @@ var dumpCmd = &cobra.Command{
}
func init() {
RootCmd.AddCommand(dumpCmd)
RootCMD.AddCommand(dumpCmd)
dumpCmd.Flags().IntVar(&port, "port", capture.Port, "define a port to listen (if not set or set to 0 the kernel will use a random free port at its own)")
dumpCmd.Flags().StringVar(&ipAddress, "listen", capture.MulticastAddressDefault, "")
}

View File

@ -6,19 +6,22 @@ import (
"github.com/spf13/cobra"
)
var debug bool
var (
debug bool
timestamps bool
)
// RootCmd represents the base command when called without any subcommands
var RootCmd = &cobra.Command{
// RootCMD represents the base command when called without any subcommands
var RootCMD = &cobra.Command{
Use: "analyzer",
Short: "wifictld analyzer",
Long: `capture wifictld traffic and display thus`,
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
// This is called by main.main(). It only needs to happen once to the RootCMD.
func Execute() {
if err := RootCmd.Execute(); err != nil {
if err := RootCMD.Execute(); err != nil {
log.Error(err)
}
}
@ -27,7 +30,11 @@ func init() {
if debug {
log.SetLevel(log.DebugLevel)
}
log.SetFormatter(&log.TextFormatter{
DisableTimestamp: timestamps,
})
log.Debug("show debug")
})
RootCmd.PersistentFlags().BoolVar(&debug, "v", false, "show debug log")
RootCMD.PersistentFlags().BoolVar(&debug, "v", false, "show debug log")
RootCMD.PersistentFlags().BoolVar(&timestamps, "timestamps", false, "Enables timestamps for log output")
}

View File

@ -2,9 +2,12 @@ package config
import (
"dev.sum7.eu/genofire/wifictld-analyzer/capture"
"dev.sum7.eu/genofire/wifictld-analyzer/web"
)
type Config struct {
StatePath string `toml:"state_path"`
Answer bool `toml:"answer"`
Webserver *web.Config `toml:"webserver"`
Interfaces []*capture.IFaceConfig `toml:"interfaces"`
}

View File

@ -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"

16
data/hwaddr.go Normal file
View File

@ -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
}

View File

@ -3,7 +3,6 @@ package data
import (
"encoding/binary"
"fmt"
"net"
"time"
"github.com/bdlm/log"
@ -31,8 +30,8 @@ func (a SocketMSGType) Is(b SocketMSGType) bool {
// SocketMSG package of wifictld format
type SocketMSG struct {
Types SocketMSGType
Client *WifiClient
Types SocketMSGType `json:"types"`
Client *WifiClient `json:"client,omitempty"`
}
func NewSocketMSG(obj []byte) (*SocketMSG, error) {
@ -51,17 +50,17 @@ func (msg *SocketMSG) Marshal() ([]byte, error) {
pos += 4
if msg.Types.Is(SocketMSGTypeClient) {
obj[pos] = msg.Client.Addr[0]
obj[pos] = msg.Client.Addr.HardwareAddr[0]
pos++
obj[pos] = msg.Client.Addr[1]
obj[pos] = msg.Client.Addr.HardwareAddr[1]
pos++
obj[pos] = msg.Client.Addr[2]
obj[pos] = msg.Client.Addr.HardwareAddr[2]
pos++
obj[pos] = msg.Client.Addr[3]
obj[pos] = msg.Client.Addr.HardwareAddr[3]
pos++
obj[pos] = msg.Client.Addr[4]
obj[pos] = msg.Client.Addr.HardwareAddr[4]
pos++
obj[pos] = msg.Client.Addr[5]
obj[pos] = msg.Client.Addr.HardwareAddr[5]
pos++
binary.BigEndian.PutUint32(obj[pos:(pos+4)], uint32(msg.Client.Time.Unix()))
pos += 4
@ -100,7 +99,7 @@ func (msg *SocketMSG) Unmarshal(obj []byte) error {
if msg.Types.Is(SocketMSGTypeClient) {
msg.Client = &WifiClient{
Addr: net.HardwareAddr(obj[pos:(pos + 6)]),
Addr: HardwareAddr{HardwareAddr: obj[pos:(pos + 6)]},
Time: time.Unix(int64(binary.BigEndian.Uint32(obj[(pos+6):(pos+10)])), 0),
TryProbe: binary.BigEndian.Uint16(obj[(pos + 10):(pos + 12)]),
TryAuth: binary.BigEndian.Uint16(obj[(pos + 12):(pos + 14)]),

View File

@ -1,9 +1,6 @@
package data
import (
"net"
"time"
)
import "time"
const (
// default multicast group used by announced
@ -18,13 +15,13 @@ const (
// WifiClient datatype of wifictld
type WifiClient struct {
Addr net.HardwareAddr
Time time.Time
TryProbe uint16
TryAuth uint16
Connected bool
Authed bool
FreqHighest uint16
SignalLowFreq int16
SignalHighFreq int16
Addr HardwareAddr `json:"addr"`
Time time.Time `json:"time"`
TryProbe uint16 `json:"try_probe"`
TryAuth uint16 `json:"try_auth"`
Connected bool `json:"connected"`
Authed bool `json:"authed"`
FreqHighest uint16 `json:"freq_highest"`
SignalLowFreq int16 `json:"signal_low_freq"`
SignalHighFreq int16 `json:"signal_high_freq"`
}

View File

@ -11,17 +11,17 @@ import (
)
type Client struct {
AP *AP `json:"-"`
APAddr string `json:"ap"`
Addr net.HardwareAddr `json:"-"`
TryProbe uint16 `json:"try_probe"`
TryAuth uint16 `json:"try_auth"`
Connected bool `json:"connected"`
Authed bool `json:"authed"`
FreqHighest uint16 `json:"freq_highest"`
SignalLowFreq int16 `json:"signal_low_freq"`
SignalHighFreq int16 `json:"signal_high_freq"`
Lastseen jsontime.Time `json:"lastseen"`
AP *AP `json:"-"`
APAddr string `json:"ap"`
Addr data.HardwareAddr `json:"-"`
TryProbe uint16 `json:"try_probe"`
TryAuth uint16 `json:"try_auth"`
Connected bool `json:"connected"`
Authed bool `json:"authed"`
FreqHighest uint16 `json:"freq_highest"`
SignalLowFreq int16 `json:"signal_low_freq"`
SignalHighFreq int16 `json:"signal_high_freq"`
Lastseen jsontime.Time `json:"lastseen"`
}
func (db *DB) LearnClient(apIP net.IP, clientWifictl *data.WifiClient) bool {
@ -70,7 +70,7 @@ func (db *DB) LearnClient(apIP net.IP, clientWifictl *data.WifiClient) bool {
return ret
}
func (db *DB) GetClient(addr net.HardwareAddr) *data.WifiClient {
func (db *DB) GetClient(addr data.HardwareAddr) *data.WifiClient {
client, ok := db.Clients[addr.String()]
wClient := &data.WifiClient{
Addr: addr,

View File

@ -1,7 +1,6 @@
package database
import (
"net"
"time"
"github.com/bdlm/log"
@ -25,7 +24,7 @@ func NewDB(path string) *DB {
file.ReadJSON(path, db)
for addr, client := range db.Clients {
client.Addr, _ = net.ParseMAC(addr)
client.Addr.UnmarshalText([]byte(addr))
if ap, ok := db.APs[client.APAddr]; ok {
client.AP = ap
}

7
web/config.go Normal file
View File

@ -0,0 +1,7 @@
package web
type Config struct {
Enable bool `toml:"enable"`
Bind string `toml:"bind"`
Webroot string `toml:"webroot"`
}

54
web/webserver.go Normal file
View File

@ -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()
}

28
web/webserver_test.go Normal file
View File

@ -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()
}

6
webroot/assets/get_assets.sh Executable file
View File

@ -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

2626
webroot/assets/vue-route.js Normal file

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

11907
webroot/assets/vue.js Normal file

File diff suppressed because it is too large Load Diff

6
webroot/assets/vuex.js Normal file

File diff suppressed because one or more lines are too long

10
webroot/css/main.css Normal file
View File

@ -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;
}

46
webroot/index.html Normal file
View File

@ -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>

20
webroot/js/data.js Normal file
View File

@ -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;
}
})

35
webroot/js/main.js Normal file
View File

@ -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 },
})

104
webroot/js/store.js Normal file
View File

@ -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;
}
}
})

View File

@ -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,
};
})
},
}
}

View File

@ -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));
},
}
}