Compare commits

...

13 Commits

Author SHA1 Message Date
Martin/Geno f07298c40c
migrate from genofire to wifictld 2019-08-08 16:10:19 +02:00
Martin/Geno fca7f5353c
fix get assets 2019-08-08 16:07:42 +02:00
Martin/Geno 35e6446036
fetch current state 2019-03-08 17:09:01 +01:00
Martin/Geno 3b98fb36cb
database 2019-03-08 15:53:45 +01:00
Martin/Geno 39ff9a43b0
move wifictld structs from data to capture 2019-03-08 06:01:57 +01:00
Martin/Geno 16fdeb85d4
add easy webinterface 2019-03-08 01:15:15 +01:00
Martin/Geno d20e749038
fix tests 2019-02-28 20:44:00 +01:00
Martin/Geno 948fa0ddec
ci 2019-02-28 17:21:37 +01:00
Martin/Geno bfb3da9338
change logging 2019-02-28 16:24:29 +01:00
Martin/Geno 40ea97bc0c
move to genofire 2019-02-28 16:22:55 +01:00
Martin/Geno 4643af5259
learn all values 2018-07-25 14:50:47 +02:00
Martin/Geno 17ac739753
add lastseen + cleanup 2018-07-16 13:00:37 +02:00
Martin/Geno e7499dbae5
save db + config + remove central 2018-07-10 21:43:35 +02:00
48 changed files with 15435 additions and 226 deletions

8
.ci/check-gofmt Executable file
View File

@ -0,0 +1,8 @@
#!/bin/bash
result="$(gofmt -s -l . | grep -v '^vendor/' )"
if [ -n "$result" ]; then
echo "Go code is not formatted, run 'gofmt -s -w .'" >&2
echo "$result"
exit 1
fi

25
.ci/check-testfiles Executable file
View File

@ -0,0 +1,25 @@
#!/usr/bin/env python
# checks if every desired package has test files
import os
import re
import sys
source_re = re.compile(".*\.go")
test_re = re.compile(".*_test\.go")
missing = False
for root, dirs, files in os.walk("."):
# ignore some paths
if root == "." or root.startswith("./vendor") or root.startswith("./."):
continue
# source files but not test files?
if len(filter(source_re.match, files)) > 0 and len(filter(test_re.match, files)) == 0:
print("no test files for {}".format(root))
missing = True
if missing:
sys.exit(1)
else:
print("every package has test files")

View File

@ -1,34 +0,0 @@
workspace:
base: /go
path: src/dev.sum7.eu/wifictld/analyzer
pipeline:
build:
image: golang:latest
commands:
- go get ./...
- go build
codestyle:
image: golang:latest
commands:
- go get github.com/client9/misspell/cmd/misspell
- misspell -error .
- if [ -n "$(gofmt -s -l .)" ]; then echo "Go code is not formatted, run 'gofmt -s -w .'" >&2; exit 1; fi
test:
image: golang:latest
commands:
- go get github.com/stretchr/testify/assert
- go test ./... -v -cover
test-race:
image: golang:latest
commands:
- go get github.com/stretchr/testify/assert
- go test ./... -v -race
release:
image: plugins/gitea-release
base_url: https://dev.sum7.eu
secrets: [ gitea_token ]
files: /go/bin/analyzer
draft: true
when:
event: tag

53
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,53 @@
image: golang:latest
stages:
- build
- test
- deploy
before_script:
- mkdir -p "/go/src/dev.sum7.eu/$CI_PROJECT_NAMESPACE/"
- cp -R "/builds/$CI_PROJECT_PATH" "/go/src/dev.sum7.eu/$CI_PROJECT_NAMESPACE/"
- cd "/go/src/dev.sum7.eu/$CI_PROJECT_PATH"
- go get -d -t ./...
build-my-project:
stage: build
script:
- go install "dev.sum7.eu/$CI_PROJECT_PATH"
- mv "/go/bin/$CI_PROJECT_NAME" "/builds/$CI_PROJECT_PATH"
artifacts:
paths:
- config_example.conf
- "$CI_PROJECT_NAME"
test-my-project:
stage: test
script:
- go get github.com/client9/misspell/cmd/misspell
- 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
artifacts:
paths:
- .testCoverage.txt
test-race-my-project:
stage: test
script:
- go test -race ./...
deploy:
stage: deploy
only:
- master
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 start $CI_PROJECT_NAME

47
README.md Normal file
View File

@ -0,0 +1,47 @@
# wifictld-analyzer
[![Build Status](https://dev.sum7.eu/wifictld/wifictld-analyzer/badges/master/build.svg)](https://dev.sum7.eu/genofire/wifictld-analyzer/pipelines)
[![Go Report Card](https://goreportcard.com/badge/dev.sum7.eu/wifictld/wifictld-analyzer)](https://goreportcard.com/report/dev.sum7.eu/genofire/wifictld-analyzer)
[![GoDoc](https://godoc.org/dev.sum7.eu/wifictld/wifictld-analyzer?status.svg)](https://godoc.org/dev.sum7.eu/genofire/wifictld-analyzer)
## Get wifictld-analyzer
#### Download
Latest Build binary from ci here:
[Download All](https://dev.sum7.eu/wifictld/wifictld-analyzer/-/jobs/artifacts/master/download/?job=build-my-project) (with config example)
[Download Binary](https://dev.sum7.eu/wifictld/wifictld-analyzer/-/jobs/artifacts/master/raw/wifictld-analyzer?inline=false&job=build-my-project)
#### Build
```bash
go get -u dev.sum7.eu/wifictld/wifictld-analyzer
```
## Configure
see `config_example.conf`
## Start / Boot
_/lib/systemd/system/wifictld-analyzer.service_ :
```
[Unit]
Description=wifictld-analyzer
After=network.target
[Service]
Type=simple
# User=notRoot
ExecStart=/opt/go/bin/wifictld-analyzer controller /etc/wifictld-analyzer.conf
Restart=always
RestartSec=5sec
[Install]
WantedBy=multi-user.target
```
Start: `systemctl start wifictld-analyzer`
Autostart: `systemctl enable wifictld-analyzer`

View File

@ -3,20 +3,19 @@ package capture
import (
"net"
"dev.sum7.eu/wifictld/analyzer/data"
log "github.com/sirupsen/logrus"
"github.com/bdlm/log"
)
//Collector for capture
type Collector struct {
connections map[string]*net.UDPConn
handler data.Handler
handler Handler
queue chan *Packet
stop chan interface{}
}
// NewCollector creates a Collector struct
func NewCollector(handler data.Handler, ifaces []IFaceConfig) *Collector {
func NewCollector(handler Handler, ifaces []*IFaceConfig) *Collector {
coll := &Collector{
handler: handler,
@ -26,6 +25,12 @@ func NewCollector(handler data.Handler, ifaces []IFaceConfig) *Collector {
}
for _, iface := range ifaces {
if iface.Port == 0 {
iface.Port = Port
}
if iface.IPAddress == "" {
iface.IPAddress = MulticastAddressDefault
}
coll.listenUDP(iface)
}
@ -43,7 +48,7 @@ func (coll *Collector) Close() {
close(coll.queue)
}
func (coll *Collector) listenUDP(iface IFaceConfig) {
func (coll *Collector) listenUDP(iface *IFaceConfig) {
ip := net.ParseIP(iface.IPAddress)
var conn *net.UDPConn
var err error
@ -90,7 +95,7 @@ type Packet struct {
func (coll *Collector) parser() {
for obj := range coll.queue {
msg, err := data.NewSocketMSG(obj.Raw)
msg, err := NewSocketMSG(obj.Raw)
if err != nil {
log.Warnf("unable to unmarshal request from %s: %s", obj.Address.String(), err)
continue
@ -108,7 +113,7 @@ func (coll *Collector) parser() {
}
// SendTo a specifical address
func (coll *Collector) SendTo(addr *net.UDPAddr, msg *data.SocketMSG) {
func (coll *Collector) SendTo(addr *net.UDPAddr, msg *SocketMSG) {
log.Debugf("send[%s]: %s", addr, msg.String())
data, err := msg.Marshal()
if err != nil {
@ -126,7 +131,7 @@ func (coll *Collector) SendTo(addr *net.UDPAddr, msg *data.SocketMSG) {
}
// Send to every connection to default address
func (coll *Collector) Send(msg *data.SocketMSG) {
func (coll *Collector) Send(msg *SocketMSG) {
log.Debugf("send: %s", msg.String())
data, err := msg.Marshal()
if err != nil {

View File

@ -0,0 +1 @@
package capture

View File

@ -1,4 +1,4 @@
package data
package capture
import "net"

View File

@ -1,12 +1,13 @@
package data
package capture
import (
"encoding/binary"
"fmt"
"net"
"time"
log "github.com/sirupsen/logrus"
"github.com/bdlm/log"
"dev.sum7.eu/wifictld/wifictld-analyzer/data"
)
var DEBUG = false
@ -23,16 +24,15 @@ const (
func (a SocketMSGType) Is(b SocketMSGType) bool {
if DEBUG {
log.Debugf("SocketType: %x & %x = %x -> %b", a, b, (a & b), (a&b) > 0)
log.Debugf("SocketType: %x & %x = %x -> %t", a, b, (a & b), (a&b) > 0)
}
return (a & b) > 0
}
// 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) {
@ -100,7 +100,7 @@ func (msg *SocketMSG) Unmarshal(obj []byte) error {
if msg.Types.Is(SocketMSGTypeClient) {
msg.Client = &WifiClient{
Addr: net.HardwareAddr(obj[pos:(pos + 6)]),
Addr: data.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,14 +1,21 @@
package data
package capture
import (
"encoding/hex"
"log"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/bdlm/log"
"github.com/bdlm/std/logger"
)
func init() {
DEBUG = true
log.SetLevel(logger.Debug)
}
func TestMsgIsTypes(t *testing.T) {
assert := assert.New(t)
types := SocketMSGType(5)

20
capture/wifi_client.go Normal file
View File

@ -0,0 +1,20 @@
package capture
import (
"time"
"dev.sum7.eu/wifictld/wifictld-analyzer/data"
)
// WifiClient datatype of wifictld
type WifiClient struct {
Addr data.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

@ -1,50 +1,67 @@
package cmd
import (
"net"
"os"
"os/signal"
"strings"
"syscall"
log "github.com/sirupsen/logrus"
"dev.sum7.eu/genofire/golang-lib/database"
"dev.sum7.eu/genofire/golang-lib/file"
"github.com/bdlm/log"
"github.com/spf13/cobra"
"dev.sum7.eu/wifictld/analyzer/capture"
"dev.sum7.eu/wifictld/analyzer/controller"
"dev.sum7.eu/wifictld/analyzer/database"
"dev.sum7.eu/wifictld/wifictld-analyzer/capture"
"dev.sum7.eu/wifictld/wifictld-analyzer/controller"
"dev.sum7.eu/wifictld/wifictld-analyzer/web"
)
var (
central bool
)
type ControllerConfig struct {
Database database.Config `toml:"database"`
Answer bool `toml:"answer"`
Webserver *web.Config `toml:"webserver"`
Interfaces []*capture.IFaceConfig `toml:"interfaces"`
}
// queryCmd represents the query command
var controllerCmd = &cobra.Command{
Use: "controller <interfaces>",
Short: "simulate a wifictld controller",
Example: `analyzer controller "eth0,wlan0"`,
Example: `analyzer controller "/etc/wifictld.conf"`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
ifaces := strings.Split(args[0], ",")
config := &ControllerConfig{}
log.Infof("listen on: %s", ifaces)
file.ReadTOML(args[0], config)
var ifacesConfigs []capture.IFaceConfig
for _, iface := range ifaces {
ifaceConfig := capture.IFaceConfig{
InterfaceName: iface,
Port: port,
IPAddress: ipAddress,
}
ifacesConfigs = append(ifacesConfigs, ifaceConfig)
if err := database.Open(config.Database); err != nil {
log.Panicf("no database connection: %s", err)
}
defer database.Close()
db := database.NewDB()
ctr := controller.NewController(db, central)
ctr := controller.NewController()
defer ctr.Close()
coll := capture.NewCollector(ctr.Handler, ifacesConfigs)
var handlers []capture.Handler
if config.Webserver.Enable {
log.Infof("starting webserver on %s", config.Webserver.Bind)
srv := web.New(config.Webserver)
go srv.Start()
handlers = append(handlers, srv.Handler)
defer srv.Close()
}
coll := capture.NewCollector(func(addr *net.UDPAddr, msg *capture.SocketMSG) (*capture.SocketMSG, error) {
for _, a := range handlers {
a(addr, msg)
}
if !config.Answer {
ctr.Handler(addr, msg)
return nil, nil
}
return ctr.Handler(addr, msg)
}, config.Interfaces)
defer coll.Close()
ctr.Send = coll.Send
@ -60,8 +77,5 @@ var controllerCmd = &cobra.Command{
}
func init() {
RootCmd.AddCommand(controllerCmd)
controllerCmd.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)")
controllerCmd.Flags().StringVar(&ipAddress, "listen", capture.MulticastAddressDefault, "")
controllerCmd.Flags().BoolVar(&central, "central", false, "")
RootCMD.AddCommand(controllerCmd)
}

View File

@ -7,11 +7,10 @@ import (
"strings"
"syscall"
log "github.com/sirupsen/logrus"
"github.com/bdlm/log"
"github.com/spf13/cobra"
"dev.sum7.eu/wifictld/analyzer/capture"
"dev.sum7.eu/wifictld/analyzer/data"
"dev.sum7.eu/wifictld/wifictld-analyzer/capture"
)
var (
@ -30,9 +29,9 @@ var dumpCmd = &cobra.Command{
log.Infof("listen on: %s", ifaces)
var ifacesConfigs []capture.IFaceConfig
var ifacesConfigs []*capture.IFaceConfig
for _, iface := range ifaces {
ifaceConfig := capture.IFaceConfig{
ifaceConfig := &capture.IFaceConfig{
InterfaceName: iface,
Port: port,
IPAddress: ipAddress,
@ -40,9 +39,9 @@ var dumpCmd = &cobra.Command{
ifacesConfigs = append(ifacesConfigs, ifaceConfig)
}
data.DEBUG = debug
capture.DEBUG = debug
coll := capture.NewCollector(func(addr *net.UDPAddr, msg *data.SocketMSG) (*data.SocketMSG, error) {
coll := capture.NewCollector(func(addr *net.UDPAddr, msg *capture.SocketMSG) (*capture.SocketMSG, error) {
log.Infof("recv[%s]: %s", addr, msg.String())
return nil, nil
}, ifacesConfigs)
@ -58,7 +57,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

@ -1,24 +1,27 @@
package cmd
import (
log "github.com/sirupsen/logrus"
"github.com/bdlm/log"
"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")
}

1
cmd/root_test.go Normal file
View File

@ -0,0 +1 @@
package cmd

18
config_example.conf Normal file
View File

@ -0,0 +1,18 @@
answer = false
[database]
type = "sqlite3"
logging = true
connection = "/tmp/wifictld.db"
# For Master-Slave cluster
# read_connection = ""
[webserver]
enable = true
bind = ":8080"
webroot = "./webroot/"
[[interfaces]]
ifname = "wlp4s0"
#port = 1000
#ip_address = "ff02::31f1"

View File

@ -2,27 +2,41 @@ package controller
import (
"net"
"time"
// log "github.com/sirupsen/logrus"
// "github.com/bdlm/log"
"dev.sum7.eu/genofire/golang-lib/database"
"dev.sum7.eu/wifictld/analyzer/data"
"dev.sum7.eu/wifictld/wifictld-analyzer/capture"
"dev.sum7.eu/wifictld/wifictld-analyzer/data"
)
func (c *Controller) Handler(addr *net.UDPAddr, msg *data.SocketMSG) (*data.SocketMSG, error) {
func (c *Controller) Handler(addr *net.UDPAddr, msg *capture.SocketMSG) (*capture.SocketMSG, error) {
ignore := false
if msg.Types.Is(data.SocketMSGTypeClient) && msg.Client != nil {
ignore = c.db.LearnClient(addr.IP, msg.Client)
if msg.Types.Is(capture.SocketMSGTypeClient) && msg.Client != nil {
ignore = c.LearnClient(addr.IP, msg.Client)
}
if !msg.Types.Is(data.SocketMSGTypeRequest) {
if !msg.Types.Is(capture.SocketMSGTypeRequest) {
return nil, nil
}
msg = &data.SocketMSG{
Types: (data.SocketMSGTypeResponse | data.SocketMSGTypeClient),
Client: c.db.GetClient(msg.Client.Addr),
client := &data.Client{Addr: msg.Client.Addr}
if result := database.Read.Select([]string{"try_probe", "try_auth"}).First(client); result.Error != nil {
return nil, result.Error
}
if c.central || !ignore {
msg = &capture.SocketMSG{
Types: (capture.SocketMSGTypeResponse | capture.SocketMSGTypeClient),
Client: &capture.WifiClient{
Addr: msg.Client.Addr,
Time: time.Now(),
TryProbe: client.TryProbe,
TryAuth: client.TryAuth,
},
}
if !ignore {
return msg, nil
}
return nil, nil

57
controller/learn.go Normal file
View File

@ -0,0 +1,57 @@
package controller
import (
"net"
"time"
// "github.com/bdlm/log"
"dev.sum7.eu/genofire/golang-lib/database"
"dev.sum7.eu/wifictld/wifictld-analyzer/capture"
"dev.sum7.eu/wifictld/wifictld-analyzer/data"
)
func (c *Controller) LearnClient(apIP net.IP, clientWifictl *capture.WifiClient) bool {
ret := false
// learn ap
ap := &data.AP{
IP: apIP,
Lastseen: time.Now(),
}
result := database.Read.First(ap)
if result.RowsAffected > 0 {
database.Write.Save(ap)
} else {
database.Write.Create(ap)
}
// learn client
client := &data.Client{
Addr: clientWifictl.Addr,
Lastseen: time.Now(),
APAddr: apIP,
Connected: clientWifictl.Connected,
SignalLowFreq: clientWifictl.SignalLowFreq,
SignalHighFreq: clientWifictl.SignalHighFreq,
}
database.Write.FirstOrCreate(client)
if clientWifictl.TryAuth > client.TryAuth {
client.TryAuth = clientWifictl.TryAuth
}
if clientWifictl.TryProbe > client.TryProbe {
client.TryProbe = clientWifictl.TryProbe
}
if client.FreqHighest < clientWifictl.FreqHighest {
ret = (client.FreqHighest != 0)
client.FreqHighest = clientWifictl.FreqHighest
}
if clientWifictl.Authed {
client.Authed = clientWifictl.Authed
}
database.Write.Save(client)
return ret
}

View File

@ -4,25 +4,22 @@ import (
"net"
"time"
log "github.com/sirupsen/logrus"
"dev.sum7.eu/genofire/golang-lib/database"
"github.com/bdlm/log"
"dev.sum7.eu/wifictld/analyzer/data"
"dev.sum7.eu/wifictld/analyzer/database"
"dev.sum7.eu/wifictld/wifictld-analyzer/capture"
"dev.sum7.eu/wifictld/wifictld-analyzer/data"
)
type Controller struct {
SendTo func(addr *net.UDPAddr, msg *data.SocketMSG)
Send func(msg *data.SocketMSG)
db *database.DB
SendTo func(addr *net.UDPAddr, msg *capture.SocketMSG)
Send func(msg *capture.SocketMSG)
ticker *time.Ticker
central bool
}
func NewController(db *database.DB, central bool) *Controller {
func NewController() *Controller {
ctl := &Controller{
ticker: time.NewTicker(60 * time.Second),
db: db,
central: central,
ticker: time.NewTicker(time.Minute),
}
go ctl.Repeated()
return ctl
@ -33,7 +30,12 @@ func (c *Controller) Close() {
}
func (c *Controller) Repeated() {
aps := 0
clients := 0
for range c.ticker.C {
log.Infof("lerned: %d APs, %d Clients", len(c.db.APs), len(c.db.Clients))
database.Read.Model(&data.AP{}).Count(&aps)
database.Read.Model(&data.Client{}).Count(&clients)
log.Debugf("learned: %d APs, %d Clients", aps, clients)
}
}

1
controller/main_test.go Normal file
View File

@ -0,0 +1 @@
package controller

20
data/ap.go Normal file
View File

@ -0,0 +1,20 @@
package data
import (
"net"
"time"
"dev.sum7.eu/genofire/golang-lib/database"
)
type AP struct {
IP net.IP `json:"ip" gorm:"PRIMARY_KEY"`
Lastseen time.Time `json:"lastseen"`
Clients []Client `gorm:"foreignkey:APAddr" json:"-"`
}
// Function to initialize the database
func init() {
database.AddModel(&AP{})
}

26
data/client.go Normal file
View File

@ -0,0 +1,26 @@
package data
import (
"net"
"time"
"dev.sum7.eu/genofire/golang-lib/database"
)
type Client struct {
Addr HardwareAddr `gorm:"PRIMARY_KEY" json:"addr"`
APAddr net.IP `gorm:"column:ap" json:"ap"`
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 time.Time `json:"lastseen"`
}
// Function to initialize the database
func init() {
database.AddModel(&Client{})
}

25
data/hwaddr.go Normal file
View File

@ -0,0 +1,25 @@
package data
import "net"
type HardwareAddr net.HardwareAddr
//MarshalJSON to bytearray
func (a HardwareAddr) String() string {
return net.HardwareAddr(a).String()
}
//MarshalJSON to bytearray
func (a HardwareAddr) MarshalText() ([]byte, error) {
return []byte(a.String()), nil
}
// UnmarshalJSON from bytearray
func (a HardwareAddr) UnmarshalText(data []byte) error {
b, err := net.ParseMAC(string(data))
if err != nil {
return err
}
a = HardwareAddr(b)
return nil
}

1
data/hwaddr_test.go Normal file
View File

@ -0,0 +1 @@
package data

View File

@ -1,30 +0,0 @@
package data
import (
"net"
"time"
)
const (
// default multicast group used by announced
MulticastAddressDefault = "ff02::31f1"
// default udp port used by announced
Port = 1000
// maximum receivable size
MaxDataGramSize = 256
)
// 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
}

View File

@ -1,4 +0,0 @@
package database
type AP struct {
}

View File

@ -1,60 +0,0 @@
package database
import (
"net"
"time"
// log "github.com/sirupsen/logrus"
"dev.sum7.eu/wifictld/analyzer/data"
)
type Client struct {
Addr net.HardwareAddr
Time time.Time
TryProbe uint16
TryAuth uint16
Connected bool
Authed bool
FreqHighest uint16
SignalLowFreq int16
SignalHighFreq int16
}
func (db *DB) LearnClient(apIP net.IP, clientWifictl *data.WifiClient) bool {
ret := false
apAddr := apIP.String()
ap, ok := db.APs[apAddr]
if !ok {
ap = &AP{}
db.APs[apAddr] = ap
}
clientAddr := clientWifictl.Addr.String()
client, ok := db.Clients[clientAddr]
if !ok {
client = &Client{
Addr: clientWifictl.Addr,
}
db.Clients[clientAddr] = client
}
client.Time = time.Now()
if client.FreqHighest < clientWifictl.FreqHighest {
ret = (client.FreqHighest != 0)
client.FreqHighest = clientWifictl.FreqHighest
}
return ret
}
func (db *DB) GetClient(addr net.HardwareAddr) *data.WifiClient {
client, ok := db.Clients[addr.String()]
wClient := &data.WifiClient{
Addr: addr,
Time: time.Now(),
}
if ok {
wClient.TryProbe = client.TryProbe
}
return wClient
}

View File

@ -1,13 +0,0 @@
package database
type DB struct {
Clients map[string]*Client `json:"client"`
APs map[string]*AP `json:"ap"`
}
func NewDB() *DB {
return &DB{
Clients: make(map[string]*Client),
APs: make(map[string]*AP),
}
}

View File

@ -1,6 +1,6 @@
package main
import "dev.sum7.eu/wifictld/analyzer/cmd"
import "dev.sum7.eu/wifictld/wifictld-analyzer/cmd"
func main() {
cmd.Execute()

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"`
}

77
web/webserver.go Normal file
View File

@ -0,0 +1,77 @@
package web
import (
"net"
"net/http"
"github.com/NYTimes/gziphandler"
"github.com/bdlm/log"
"dev.sum7.eu/genofire/golang-lib/database"
lib "dev.sum7.eu/genofire/golang-lib/http"
"dev.sum7.eu/genofire/golang-lib/websocket"
"dev.sum7.eu/wifictld/wifictld-analyzer/capture"
"dev.sum7.eu/wifictld/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.HandleFunc("/data.json", func(w http.ResponseWriter, r *http.Request) {
type dataResponse struct {
APs []data.AP `json:"aps"`
Clients []data.Client `json:"clients"`
}
data := &dataResponse{}
if result := database.Read.Find(&data.APs); result.Error != nil {
log.WithField("error", result.Error.Error()).Warn("not possible to read APs")
http.Error(w, "not possible to read APs", http.StatusNotFound)
return
}
if result := database.Read.Find(&data.Clients); result.Error != nil {
log.WithField("error", result.Error.Error()).Warn("not possible to read Clients")
http.Error(w, "not possible to read Clients", http.StatusNotFound)
return
}
lib.Write(w, data)
log.Info("fetch data")
})
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 *capture.SocketMSG) (*capture.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://vuejs.org/js/vue.min.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;
}

48
webroot/index.html Normal file
View File

@ -0,0 +1,48 @@
<!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>
<script src="js/utils.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/view.js"></script>
<script src="js/data.js"></script>
<script src="js/main.js"></script>
</body>
</html>

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

@ -0,0 +1,32 @@
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;
}
})
VueNativeSock.default.install(Vue, `//${location.host}${location.pathname}ws`, {
store: store,
reconnection: true,
reconnectionDelay: 5000,
format: 'json',
})
getJSON(`//${location.host}${location.pathname}data.json`).then(function(data){
store.commit('initData',data);
})

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

@ -0,0 +1,6 @@
const app = new Vue({
el: '#app',
store,
router,
components: { NavbarLogo },
})

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

@ -0,0 +1,118 @@
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;
},
initData (state, data) {
data.aps.forEach((ap) => {
if(state.controller._ap[ap.ip] === undefined){
state.controller.ap.push(ap.ip)
state.controller._ap[ap.ip] = null;
}
})
data.clients.forEach((client) => {
if (state.controller.clients[client.addr] === undefined) {
state.controller._clients.push(client.addr)
}
state.controller.clients[client.addr] = client;
})
}
}
})

23
webroot/js/utils.js Normal file
View File

@ -0,0 +1,23 @@
function get(url) {
return new Promise(function (resolve, reject) {
var req = new XMLHttpRequest();
req.open('GET', url);
req.onload = function onload() {
if (req.status === 200) {
resolve(req.response);
} else {
reject(Error(req.statusText));
}
};
req.onerror = function onerror() {
reject(Error('Network Error'));
};
req.send();
});
}
function getJSON(url) {
return get(url).then(JSON.parse);
}

21
webroot/js/view.js Normal file
View File

@ -0,0 +1,21 @@
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 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' }
]
})

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