mirror of
https://github.com/sascha-hemi/spaceDevices.git
synced 2026-03-21 00:04:23 +01:00
First basic version. Features: webUI, mqtt support (gets devices updates + parsing), reads userDb.json
This commit is contained in:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
.idea/
|
||||
.vscode/
|
||||
config.toml
|
||||
vendor/
|
||||
userDb.json
|
||||
masterDb.json
|
||||
117
Gopkg.lock
generated
Normal file
117
Gopkg.lock
generated
Normal file
@@ -0,0 +1,117 @@
|
||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/BurntSushi/toml"
|
||||
packages = ["."]
|
||||
revision = "b26d9c308763d68093482582cea63d69be07a0f0"
|
||||
version = "v0.3.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/davecgh/go-spew"
|
||||
packages = ["spew"]
|
||||
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/dchest/uniuri"
|
||||
packages = ["."]
|
||||
revision = "8902c56451e9b58ff940bbe5fec35d5f9c04584a"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/eclipse/paho.mqtt.golang"
|
||||
packages = [".","packets"]
|
||||
revision = "aff15770515e3c57fc6109da73d42b0d46f7f483"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/gin-contrib/gzip"
|
||||
packages = ["."]
|
||||
revision = "9b22cb967bcc8481c9fe3ab99f70d715f88980f0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/gin-contrib/sse"
|
||||
packages = ["."]
|
||||
revision = "22d885f9ecc78bf4ee5d72b937e4bbcdc58e8cae"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/gin-gonic/gin"
|
||||
packages = [".","binding","render"]
|
||||
revision = "d459835d2b077e44f7c9b453505ee29881d5d12d"
|
||||
version = "v1.2"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/golang/protobuf"
|
||||
packages = ["proto"]
|
||||
revision = "1643683e1b54a9e88ad26d98f81400c8c9d9f4f9"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/mattn/go-isatty"
|
||||
packages = ["."]
|
||||
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
|
||||
version = "v0.0.3"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/pmezard/go-difflib"
|
||||
packages = ["difflib"]
|
||||
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/sirupsen/logrus"
|
||||
packages = ["."]
|
||||
revision = "f006c2ac4710855cf0f916dd6b77acf6b048dc6e"
|
||||
version = "v1.0.3"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/stretchr/testify"
|
||||
packages = ["assert"]
|
||||
revision = "69483b4bd14f5845b5a1e55bca19e954e827f1d0"
|
||||
version = "v1.1.4"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/ugorji/go"
|
||||
packages = ["codec"]
|
||||
revision = "459bba837a9de7a4d9c58fa5a1171a2a3c96d0d0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/crypto"
|
||||
packages = ["ssh/terminal"]
|
||||
revision = "2509b142fb2b797aa7587dad548f113b2c0f20ce"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/net"
|
||||
packages = ["proxy","websocket"]
|
||||
revision = "c73622c77280266305273cb545f54516ced95b93"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/sys"
|
||||
packages = ["unix","windows"]
|
||||
revision = "661970f62f5897bc0cd5fdca7e087ba8a98a8fa1"
|
||||
|
||||
[[projects]]
|
||||
name = "gopkg.in/go-playground/validator.v8"
|
||||
packages = ["."]
|
||||
revision = "5f1438d3fca68893a817e4a66806cea46a9e4ebf"
|
||||
version = "v8.18.2"
|
||||
|
||||
[[projects]]
|
||||
branch = "v2"
|
||||
name = "gopkg.in/yaml.v2"
|
||||
packages = ["."]
|
||||
revision = "eb3733d160e74a9c7e442f435eb3bea458e1d19f"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "13743fcc4cbd00e607d2f15ef32adb097b9e3fec858d56d2ffb0d753b998f7ee"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
50
Gopkg.toml
Normal file
50
Gopkg.toml
Normal file
@@ -0,0 +1,50 @@
|
||||
|
||||
# Gopkg.toml example
|
||||
#
|
||||
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
|
||||
# for detailed Gopkg.toml documentation.
|
||||
#
|
||||
# required = ["github.com/user/thing/cmd/thing"]
|
||||
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project"
|
||||
# version = "1.0.0"
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project2"
|
||||
# branch = "dev"
|
||||
# source = "github.com/myfork/project2"
|
||||
#
|
||||
# [[override]]
|
||||
# name = "github.com/x/y"
|
||||
# version = "2.4.0"
|
||||
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/sirupsen/logrus"
|
||||
version = "1.0.3"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/gin-gonic/gin"
|
||||
version = "1.2.0"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/gin-contrib/gzip"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/BurntSushi/toml"
|
||||
version = "0.3.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/eclipse/paho.mqtt.golang"
|
||||
version = "1.1.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/stretchr/testify"
|
||||
version = "1.1.4"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/dchest/uniuri"
|
||||
23
cmd/spaceDevices.go
Normal file
23
cmd/spaceDevices.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/ktt-ol/spaceDevices/conf"
|
||||
"github.com/ktt-ol/spaceDevices/db"
|
||||
"github.com/ktt-ol/spaceDevices/mqtt"
|
||||
"github.com/ktt-ol/spaceDevices/webService"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const CONFIG_FILE = "config.toml"
|
||||
|
||||
func main() {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
log.SetFormatter(&log.TextFormatter{DisableColors: true})
|
||||
|
||||
config := conf.LoadConfig(CONFIG_FILE)
|
||||
|
||||
//spaceDevices.EnableMqttDebugLogging()
|
||||
mqttHandler := mqtt.NewMqttHandler(config.Mqtt)
|
||||
macDb := db.NewUserMacSettings(config.MacDb)
|
||||
webService.StartWebService(config.Server, mqttHandler, macDb)
|
||||
}
|
||||
43
conf/config.go
Normal file
43
conf/config.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"github.com/BurntSushi/toml"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func LoadConfig(configFile string) TomlConfig {
|
||||
log.WithField("configFile", configFile).Info("Loading config.")
|
||||
config := &TomlConfig{}
|
||||
if _, err := toml.DecodeFile(configFile, config); err != nil {
|
||||
log.WithError(err).Fatal("Could not read config file.")
|
||||
}
|
||||
|
||||
return *config
|
||||
}
|
||||
|
||||
type TomlConfig struct {
|
||||
Server ServerConf
|
||||
MacDb MacDbConf
|
||||
Mqtt MqttConf
|
||||
}
|
||||
|
||||
type ServerConf struct {
|
||||
Host string
|
||||
Port int
|
||||
Https bool
|
||||
KeyFile string
|
||||
CertFile string
|
||||
}
|
||||
|
||||
type MacDbConf struct {
|
||||
MasterFile string
|
||||
UserFile string
|
||||
}
|
||||
|
||||
type MqttConf struct {
|
||||
Url string
|
||||
Username string
|
||||
Password string
|
||||
// if empty, the system certificates are used
|
||||
CertFile string
|
||||
}
|
||||
53
config.toml.example
Normal file
53
config.toml.example
Normal file
@@ -0,0 +1,53 @@
|
||||
[server]
|
||||
host = "0.0.0.0"
|
||||
port = 9000
|
||||
https = false
|
||||
# optional if https is false
|
||||
keyFile = "...your.key"
|
||||
# optional if https is false
|
||||
certFile = "...your.cer"
|
||||
|
||||
[mqtt]
|
||||
url = "tls://server:8883"
|
||||
# optional
|
||||
certFile = "server.cert.pem"
|
||||
username = "user"
|
||||
password = "pass"
|
||||
|
||||
[macDb]
|
||||
# JSON file, NOT modified by the app
|
||||
# Format:
|
||||
#{
|
||||
# "00:01:02:03:04:05": {
|
||||
# "name": "a name",
|
||||
# "device-type": "a type",
|
||||
# "visibility": "ignore"
|
||||
# },
|
||||
# "aa:bb:cc:dd:ee:ff": {
|
||||
# "name": "another name",
|
||||
# "device-type": "server",
|
||||
# "visibility": "ignore"
|
||||
# },
|
||||
masterFile = "masterDb.json"
|
||||
# JSON file, modified by this app
|
||||
# Format:
|
||||
#{
|
||||
# "00:01:02:03:04:05": {
|
||||
# "name": "a name",
|
||||
# "visibility": "show",
|
||||
# "ts": 1427737817755
|
||||
# },
|
||||
# "aa:bb:cc:dd:ee:ff": {
|
||||
# "name": "another name",
|
||||
# "visibility": "show",
|
||||
# "ts": 1427737817755
|
||||
# },
|
||||
userFile = "userDb.json"
|
||||
|
||||
# mqtt: {
|
||||
# server: 'tls://spacegate.mainframe.lan',
|
||||
# ca: extConfFolder + '/spacegate.crt',
|
||||
# topic: '/net/devices',
|
||||
# username: 'devices',
|
||||
# password: '6AroZF3A34E6bocd'
|
||||
# },
|
||||
13
db/userMacSettings_test.go
Normal file
13
db/userMacSettings_test.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package db
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_IsMacLocallyAdministered(t *testing.T) {
|
||||
assert.True(t, IsMacLocallyAdministered("06:00:00:00:00:00"))
|
||||
assert.True(t, IsMacLocallyAdministered("62:01:0f:b5:f2:d9"))
|
||||
assert.False(t, IsMacLocallyAdministered("20:c9:d0:7a:fa:31"))
|
||||
}
|
||||
|
||||
113
db/usersMacSettings.go
Normal file
113
db/usersMacSettings.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/ktt-ol/spaceDevices/conf"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type UserMacSettings struct {
|
||||
userMap map[string]UserMacInfo
|
||||
lock sync.RWMutex
|
||||
config conf.MacDbConf
|
||||
}
|
||||
|
||||
type Visibility uint8
|
||||
|
||||
const (
|
||||
// Not shown at all
|
||||
VisibilityIgnore Visibility = iota
|
||||
// don't show the name, but increments the anonymous user count
|
||||
VisibilityAnon
|
||||
// show the user, but not the device name(s)
|
||||
VisibilityUser
|
||||
// show user and the device names
|
||||
VisibilityAll
|
||||
)
|
||||
|
||||
func ParseVisibility(visibility uint8) (Visibility, bool) {
|
||||
if visibility > 3 {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return Visibility(visibility), true
|
||||
}
|
||||
|
||||
func NewUserMacSettings(config conf.MacDbConf) *UserMacSettings {
|
||||
instance := &UserMacSettings{config: config}
|
||||
instance.loadDb()
|
||||
return instance
|
||||
}
|
||||
|
||||
func (db *UserMacSettings) Get(mac string) (UserMacInfo, bool) {
|
||||
db.lock.RLock()
|
||||
value, ok := db.userMap[mac]
|
||||
db.lock.RUnlock()
|
||||
return value, ok
|
||||
}
|
||||
|
||||
func (db *UserMacSettings) Set(mac string, info UserMacInfo) {
|
||||
db.lock.Lock()
|
||||
defer db.lock.Unlock()
|
||||
db.userMap[mac] = info
|
||||
db.saveDb()
|
||||
}
|
||||
|
||||
func (db *UserMacSettings) Delete(mac string) {
|
||||
db.lock.Lock()
|
||||
defer db.lock.Unlock()
|
||||
delete(db.userMap, mac)
|
||||
}
|
||||
|
||||
func (db *UserMacSettings) loadDb() {
|
||||
db.lock.Lock()
|
||||
defer db.lock.Unlock()
|
||||
|
||||
file, err := ioutil.ReadFile(db.config.UserFile)
|
||||
if err != nil {
|
||||
log.Fatal("UserFile error: ", err)
|
||||
}
|
||||
|
||||
var parsed entryMap
|
||||
if err = json.Unmarshal(file, &parsed); err != nil {
|
||||
log.Fatal("Unmarshal err: ", err)
|
||||
}
|
||||
|
||||
db.userMap = parsed
|
||||
}
|
||||
|
||||
func (db *UserMacSettings) saveDb() {
|
||||
bytes, err := json.MarshalIndent(db.userMap, "", " ")
|
||||
if err != nil {
|
||||
log.Fatal("Can't marshal the userDb: ", err)
|
||||
}
|
||||
|
||||
if err = ioutil.WriteFile(db.config.UserFile, bytes, 0644); err != nil {
|
||||
log.Fatal("Can't save the userDb: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
type entryMap map[string]UserMacInfo
|
||||
|
||||
type UserMacInfo struct {
|
||||
Name string
|
||||
DeviceName string
|
||||
Visibility Visibility
|
||||
// last change in ms
|
||||
Ts int64
|
||||
}
|
||||
|
||||
// IsMacLocallyAdministered expects the mac in the format e.g. "20:c9:d0:7a:fa:31"
|
||||
// https://en.wikipedia.org/wiki/MAC_address
|
||||
func IsMacLocallyAdministered(mac string) bool {
|
||||
// 00000010
|
||||
const mask = 1 << 1
|
||||
|
||||
first2chars := mac[:2]
|
||||
decimal, _ := strconv.ParseInt(first2chars, 16, 8)
|
||||
return (decimal & mask) == mask
|
||||
}
|
||||
227
mqtt/mqtt.go
Normal file
227
mqtt/mqtt.go
Normal file
@@ -0,0 +1,227 @@
|
||||
package mqtt
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"time"
|
||||
|
||||
"github.com/eclipse/paho.mqtt.golang"
|
||||
"github.com/ktt-ol/spaceDevices/conf"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const CLIENT_ID = "spaceDevices2"
|
||||
|
||||
var logger = log.WithField("where", "mqtt")
|
||||
|
||||
type MqttHandler struct {
|
||||
WifiSessionList []WifiSession
|
||||
// more to come, e.g. LanSessions
|
||||
}
|
||||
|
||||
type WifiSession struct {
|
||||
Ip string
|
||||
Mac string
|
||||
Vlan string
|
||||
AP int
|
||||
}
|
||||
|
||||
//func init() {
|
||||
// mqtt.ERROR.SetOutput(copyOfStdLogger(log.ErrorLevel).Writer())
|
||||
// mqtt.CRITICAL.SetOutput(copyOfStdLogger(log.ErrorLevel).Writer())
|
||||
// mqtt.WARN.SetOutput(copyOfStdLogger(log.WarnLevel).Writer())
|
||||
// mqtt.DEBUG.SetOutput(copyOfStdLogger(log.DebugLevel).Writer())
|
||||
//}
|
||||
//func copyOfStdLogger(level log.Level) *log.Logger {
|
||||
// logger := log.New()
|
||||
// logger.Formatter = log.StandardLogger().Formatter
|
||||
// logger.Out = log.StandardLogger().Out
|
||||
// logger.SetLevel(level)
|
||||
// return logger
|
||||
//}
|
||||
|
||||
func EnableMqttDebugLogging() {
|
||||
stdLogWriter := log.StandardLogger().Writer()
|
||||
mqtt.ERROR.SetOutput(stdLogWriter)
|
||||
mqtt.CRITICAL.SetOutput(stdLogWriter)
|
||||
mqtt.WARN.SetOutput(stdLogWriter)
|
||||
mqtt.DEBUG.SetOutput(stdLogWriter)
|
||||
}
|
||||
|
||||
func NewMqttHandler(conf conf.MqttConf) *MqttHandler {
|
||||
opts := mqtt.NewClientOptions()
|
||||
|
||||
opts.AddBroker(conf.Url)
|
||||
|
||||
if conf.Username != "" {
|
||||
opts.SetUsername(conf.Username)
|
||||
}
|
||||
if conf.Password != "" {
|
||||
opts.SetPassword(conf.Password)
|
||||
}
|
||||
|
||||
certs := defaultCertPool(conf.CertFile)
|
||||
tlsConf := &tls.Config{
|
||||
RootCAs: certs,
|
||||
}
|
||||
opts.SetTLSConfig(tlsConf)
|
||||
|
||||
opts.SetClientID(CLIENT_ID)
|
||||
opts.SetAutoReconnect(true)
|
||||
opts.SetKeepAlive(10 * time.Second)
|
||||
opts.SetMaxReconnectInterval(5 * time.Minute)
|
||||
|
||||
handler := MqttHandler{}
|
||||
opts.SetOnConnectHandler(handler.onConnect)
|
||||
opts.SetConnectionLostHandler(handler.onConnectionLost)
|
||||
|
||||
client := mqtt.NewClient(opts)
|
||||
if tok := client.Connect(); tok.WaitTimeout(5*time.Second) && tok.Error() != nil {
|
||||
logger.WithError(tok.Error()).Fatal("Could not connect to mqtt server.")
|
||||
}
|
||||
|
||||
return &handler
|
||||
}
|
||||
|
||||
func (h *MqttHandler) onConnect(client mqtt.Client) {
|
||||
logger.Info("connected")
|
||||
|
||||
err := subscribe(client, "/net/wlan-sessions",
|
||||
func(client mqtt.Client, message mqtt.Message) {
|
||||
//logger.Debug("new wifi sessions")
|
||||
mock := `{ "38134": {
|
||||
"last-auth": 1509211121,
|
||||
"vlan": "default",
|
||||
"stats": {
|
||||
"rx-multicast-pkts": 0,
|
||||
"rx-unicast-pkts": 292,
|
||||
"tx-unicast-pkts": 654,
|
||||
"rx-unicast-bytes": 20510,
|
||||
"tx-unicast-bytes": 278565,
|
||||
"rx-multicast-bytes": 0
|
||||
},
|
||||
"ssid": "mainframe",
|
||||
"ip": "::1",
|
||||
"hostname": "-",
|
||||
"last-snr": 47,
|
||||
"last-rate-mbits": "6",
|
||||
"ap": 1,
|
||||
"mac": "10:68:3f:bb:bb:bb",
|
||||
"radio": 2,
|
||||
"userinfo": {
|
||||
"name": "Holger",
|
||||
"visibility": "show",
|
||||
"ts": 1427737817755
|
||||
},
|
||||
"session-start": 1509211121,
|
||||
"last-rssi-dbm": -48,
|
||||
"last-activity": 1509211584
|
||||
}}`
|
||||
// message.Payload()
|
||||
h.WifiSessionList = parseWifiSessions([]byte(mock))
|
||||
})
|
||||
if err != nil {
|
||||
logger.WithError(err).Fatal("Could not subscribe")
|
||||
}
|
||||
}
|
||||
|
||||
func (h *MqttHandler) onConnectionLost(client mqtt.Client, err error) {
|
||||
logger.WithError(err).Error("Connection lost.")
|
||||
}
|
||||
|
||||
func (h *MqttHandler) GetByIp(ip string) (WifiSession, bool) {
|
||||
for _, v := range h.WifiSessionList {
|
||||
if v.Ip == ip {
|
||||
return v, true
|
||||
}
|
||||
}
|
||||
|
||||
return WifiSession{}, false
|
||||
}
|
||||
|
||||
func subscribe(client mqtt.Client, topic string, cb mqtt.MessageHandler) error {
|
||||
qos := 0
|
||||
tok := client.Subscribe(topic, byte(qos), cb)
|
||||
tok.WaitTimeout(5 * time.Second)
|
||||
return tok.Error()
|
||||
}
|
||||
|
||||
func defaultCertPool(certFile string) *x509.CertPool {
|
||||
if certFile == "" {
|
||||
log.Debug("No certFile given, using system pool")
|
||||
pool, err := x509.SystemCertPool()
|
||||
if err != nil {
|
||||
log.WithError(err).Fatal("Could not create system cert pool.")
|
||||
}
|
||||
return pool
|
||||
}
|
||||
|
||||
fileData, err := ioutil.ReadFile(certFile)
|
||||
if err != nil {
|
||||
log.WithError(err).Fatal("Could not read given cert file.")
|
||||
}
|
||||
|
||||
certs := x509.NewCertPool()
|
||||
if !certs.AppendCertsFromPEM(fileData) {
|
||||
log.Fatal("unable to add given certificate to CertPool")
|
||||
}
|
||||
|
||||
return certs
|
||||
}
|
||||
|
||||
func parseWifiSessions(rawData []byte) []WifiSession {
|
||||
sessionsList := []WifiSession{}
|
||||
|
||||
// we don't use a struct here, because we are interested in a small subset, only.
|
||||
var sessionData map[string]interface{}
|
||||
if err := json.Unmarshal(rawData, &sessionData); err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"rawData": string(rawData),
|
||||
"error": err,
|
||||
}).Error("Unable to unmarshal wifi session json.")
|
||||
return sessionsList
|
||||
}
|
||||
|
||||
for _, v := range sessionData {
|
||||
entry, ok := v.(map[string]interface{})
|
||||
if !ok {
|
||||
log.WithFields(log.Fields{
|
||||
"data": v,
|
||||
}).Error("Unable to unmarshal wifi session json. Unexpected structure")
|
||||
return sessionsList
|
||||
}
|
||||
|
||||
vlan, ok := entry["vlan"].(string)
|
||||
if !ok {
|
||||
logParseError("vlan", v)
|
||||
return []WifiSession{}
|
||||
}
|
||||
ip, ok := entry["ip"].(string)
|
||||
if !ok {
|
||||
logParseError("ip", v)
|
||||
return []WifiSession{}
|
||||
}
|
||||
ap, ok := entry["ap"].(float64)
|
||||
if !ok {
|
||||
logParseError("ap", v)
|
||||
return []WifiSession{}
|
||||
}
|
||||
mac, ok := entry["mac"].(string)
|
||||
if !ok {
|
||||
logParseError("mac", v)
|
||||
return []WifiSession{}
|
||||
}
|
||||
sessionsList = append(sessionsList, WifiSession{ip, mac, vlan, int(ap)})
|
||||
}
|
||||
|
||||
return sessionsList
|
||||
}
|
||||
|
||||
func logParseError(field string, data interface{}) {
|
||||
logger.WithFields(log.Fields{
|
||||
"field": field,
|
||||
"data": data,
|
||||
}).Error("Parse error for field.")
|
||||
}
|
||||
136
mqtt/mqtt_test.go
Normal file
136
mqtt/mqtt_test.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package mqtt
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_parseWifiSessions(t *testing.T) {
|
||||
const testData = `
|
||||
{
|
||||
"38126": {
|
||||
"last-auth": 1509210709,
|
||||
"vlan": "default",
|
||||
"stats": {
|
||||
"rx-multicast-pkts": 499,
|
||||
"rx-unicast-pkts": 1817,
|
||||
"tx-unicast-pkts": 734,
|
||||
"rx-unicast-bytes": 156208,
|
||||
"tx-unicast-bytes": 272461,
|
||||
"rx-multicast-bytes": 76808
|
||||
},
|
||||
"ssid": "mainframe",
|
||||
"ip": "192.168.2.127",
|
||||
"hostname": "-",
|
||||
"last-snr": 40,
|
||||
"last-rate-mbits": "24",
|
||||
"ap": 2,
|
||||
"mac": "2c:0e:3d:aa:aa:aa",
|
||||
"radio": 2,
|
||||
"userinfo": null,
|
||||
"session-start": 1509210709,
|
||||
"last-rssi-dbm": -55,
|
||||
"last-activity": 1509211581
|
||||
},
|
||||
"38134": {
|
||||
"last-auth": 1509211121,
|
||||
"vlan": "default",
|
||||
"stats": {
|
||||
"rx-multicast-pkts": 0,
|
||||
"rx-unicast-pkts": 292,
|
||||
"tx-unicast-pkts": 654,
|
||||
"rx-unicast-bytes": 20510,
|
||||
"tx-unicast-bytes": 278565,
|
||||
"rx-multicast-bytes": 0
|
||||
},
|
||||
"ssid": "mainframe",
|
||||
"ip": "192.168.2.179",
|
||||
"hostname": "-",
|
||||
"last-snr": 47,
|
||||
"last-rate-mbits": "6",
|
||||
"ap": 1,
|
||||
"mac": "10:68:3f:bb:bb:bb",
|
||||
"radio": 2,
|
||||
"userinfo": {
|
||||
"name": "Holger",
|
||||
"visibility": "show",
|
||||
"ts": 1427737817755
|
||||
},
|
||||
"session-start": 1509211121,
|
||||
"last-rssi-dbm": -48,
|
||||
"last-activity": 1509211584
|
||||
},
|
||||
"38135": {
|
||||
"last-auth": 1509211163,
|
||||
"vlan": "default",
|
||||
"stats": {
|
||||
"rx-multicast-pkts": 114,
|
||||
"rx-unicast-pkts": 8119,
|
||||
"tx-unicast-pkts": 12440,
|
||||
"rx-unicast-bytes": 1093407,
|
||||
"tx-unicast-bytes": 15083985,
|
||||
"rx-multicast-bytes": 20379
|
||||
},
|
||||
"ssid": "mainframe",
|
||||
"ip": "192.168.2.35",
|
||||
"hostname": "happle",
|
||||
"last-snr": 39,
|
||||
"last-rate-mbits": "24",
|
||||
"ap": 1,
|
||||
"mac": "20:c9:d0:cc:cc:cc",
|
||||
"radio": 2,
|
||||
"userinfo": {
|
||||
"name": "Holger",
|
||||
"visibility": "show",
|
||||
"ts": 1438474581580
|
||||
},
|
||||
"session-start": 1509211163,
|
||||
"last-rssi-dbm": -56,
|
||||
"last-activity": 1509211584
|
||||
},
|
||||
"38137": {
|
||||
"last-auth": 1509211199,
|
||||
"vlan": "FreiFunk",
|
||||
"stats": {
|
||||
"rx-multicast-pkts": 14,
|
||||
"rx-unicast-pkts": 931,
|
||||
"tx-unicast-pkts": 615,
|
||||
"rx-unicast-bytes": 70172,
|
||||
"tx-unicast-bytes": 265390,
|
||||
"rx-multicast-bytes": 1574
|
||||
},
|
||||
"ssid": "nordwest.freifunk.net",
|
||||
"ip": "10.18.159.6",
|
||||
"hostname": "iPhonevineSager",
|
||||
"last-snr": 13,
|
||||
"last-rate-mbits": "2",
|
||||
"ap": 1,
|
||||
"mac": "b8:53:ac:dd:dd:dd",
|
||||
"radio": 1,
|
||||
"userinfo": null,
|
||||
"session-start": 1509211199,
|
||||
"last-rssi-dbm": -82,
|
||||
"last-activity": 1509211584
|
||||
}
|
||||
}
|
||||
`
|
||||
assert := assert.New(t)
|
||||
|
||||
sessions := parseWifiSessions([]byte(testData))
|
||||
assert.Equal(len(sessions), 4)
|
||||
|
||||
mustContain := [4]bool{false, false, false, false}
|
||||
for _, v := range sessions {
|
||||
mustContain[0] = mustContain[0] || (v.Ip == "192.168.2.127" && v.Mac == "2c:0e:3d:aa:aa:aa" && v.Vlan == "default" && v.AP == 2)
|
||||
mustContain[1] = mustContain[1] || (v.Ip == "192.168.2.179" && v.Mac == "10:68:3f:bb:bb:bb" && v.Vlan == "default" && v.AP == 1)
|
||||
mustContain[2] = mustContain[2] || (v.Ip == "192.168.2.35" && v.Mac == "20:c9:d0:cc:cc:cc" && v.Vlan == "default" && v.AP == 1)
|
||||
mustContain[3] = mustContain[3] || (v.Ip == "10.18.159.6" && v.Mac == "b8:53:ac:dd:dd:dd" && v.Vlan == "FreiFunk" && v.AP == 1)
|
||||
}
|
||||
for _, v := range mustContain {
|
||||
assert.True(v)
|
||||
}
|
||||
|
||||
// don't fail for garbage
|
||||
sessions = parseWifiSessions([]byte("{ totally invalid json }"))
|
||||
assert.Equal(len(sessions), 0)
|
||||
}
|
||||
92
webService/simpleXsrfCheck.go
Normal file
92
webService/simpleXsrfCheck.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package webService
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/dchest/uniuri"
|
||||
)
|
||||
|
||||
const cleanupAfter = 20 * time.Minute
|
||||
|
||||
type tokenEntry struct {
|
||||
value string
|
||||
ts int64 // unix nano
|
||||
}
|
||||
|
||||
type simpleXSRFheckInternal struct {
|
||||
tokens map[string]tokenEntry
|
||||
lock sync.RWMutex
|
||||
stopCleaner chan bool
|
||||
}
|
||||
|
||||
// SimpleXSRFCheck creates/compares and stores the XSRF tokens for the associated ip. After cleanupAfter amount of time
|
||||
// a cleanup go routine removes old entries.
|
||||
type SimpleXSRFCheck struct {
|
||||
// the real struct. The outer hull is a wrapper that allows the 'SetFinalizer' function to work probably.
|
||||
*simpleXSRFheckInternal
|
||||
}
|
||||
|
||||
// NewSimpleXSRFCheck creates a new instance
|
||||
func NewSimpleXSRFCheck() *SimpleXSRFCheck {
|
||||
obj := simpleXSRFheckInternal{tokens: make(map[string]tokenEntry), stopCleaner: make(chan bool)}
|
||||
go obj.cleanup()
|
||||
|
||||
// we need this wrapper or the SetFinalizer would never call
|
||||
wraper := &SimpleXSRFCheck{&obj}
|
||||
runtime.SetFinalizer(wraper, stopCleaner)
|
||||
return wraper
|
||||
}
|
||||
|
||||
func stopCleaner(sx *SimpleXSRFCheck) {
|
||||
select {
|
||||
case sx.stopCleaner <- true:
|
||||
break
|
||||
// don't block!
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func (sx *simpleXSRFheckInternal) NewToken(ip string) string {
|
||||
token := tokenEntry{value: uniuri.NewLen(10), ts: time.Now().UnixNano()}
|
||||
sx.lock.Lock()
|
||||
sx.tokens[ip] = token
|
||||
sx.lock.Unlock()
|
||||
return token.value
|
||||
}
|
||||
|
||||
// CheckAndClearToken returns true if the given token matches for the given ip. If no entry was found for the ip, false is returned.
|
||||
// At the end the ip entry is removed from the internal map.
|
||||
func (sx *simpleXSRFheckInternal) CheckAndClearToken(ip string, token string) bool {
|
||||
sx.lock.RLock()
|
||||
defer sx.lock.RUnlock()
|
||||
savedToken, ok := sx.tokens[ip]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
delete(sx.tokens, ip)
|
||||
return savedToken.value == token
|
||||
}
|
||||
|
||||
func (sx *simpleXSRFheckInternal) cleanup() {
|
||||
ticker := time.NewTicker(cleanupAfter)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-sx.stopCleaner:
|
||||
ticker.Stop()
|
||||
return
|
||||
case <-ticker.C:
|
||||
cleanupValue := time.Now().UnixNano() - int64(cleanupAfter)
|
||||
sx.lock.Lock()
|
||||
for k, v := range sx.tokens {
|
||||
if v.ts < cleanupValue {
|
||||
delete(sx.tokens, k)
|
||||
}
|
||||
}
|
||||
sx.lock.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
138
webService/web.go
Normal file
138
webService/web.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package webService
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-contrib/gzip"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/ktt-ol/spaceDevices/conf"
|
||||
"github.com/ktt-ol/spaceDevices/db"
|
||||
"github.com/ktt-ol/spaceDevices/mqtt"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var logger = logrus.WithField("where", "webSrv")
|
||||
|
||||
var mqttHandler *mqtt.MqttHandler
|
||||
var macDb *db.UserMacSettings
|
||||
var xsrfCheck *SimpleXSRFCheck
|
||||
|
||||
func StartWebService(conf conf.ServerConf, _mqttHandler *mqtt.MqttHandler, _macDb *db.UserMacSettings) {
|
||||
mqttHandler = _mqttHandler
|
||||
macDb = _macDb
|
||||
xsrfCheck = NewSimpleXSRFCheck()
|
||||
|
||||
router := gin.Default()
|
||||
router.Use(gzip.Gzip(gzip.DefaultCompression))
|
||||
|
||||
router.Static("/assets", "webUI/assets")
|
||||
router.LoadHTMLGlob("webUI/templates/*.html")
|
||||
router.GET("/", overviewPageHandler)
|
||||
router.POST("/", changeInfoHandler)
|
||||
router.GET("/help.html", func(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "help.html", gin.H{})
|
||||
})
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", conf.Host, conf.Port)
|
||||
if conf.Https {
|
||||
router.RunTLS(addr, conf.CertFile, conf.KeyFile)
|
||||
} else {
|
||||
router.Run(addr)
|
||||
}
|
||||
}
|
||||
|
||||
func sendError(c *gin.Context, msg string) {
|
||||
c.String(http.StatusBadRequest, "Error: "+msg)
|
||||
c.Abort()
|
||||
}
|
||||
|
||||
func overviewPageHandler(c *gin.Context) {
|
||||
ip, _, _ := net.SplitHostPort(c.Request.RemoteAddr)
|
||||
logger.WithField("ip", ip).Debug("Request ip.")
|
||||
|
||||
name := "???"
|
||||
mac := "???"
|
||||
deviceName := ""
|
||||
visibility := db.Visibility(99)
|
||||
isLocallyAdministered := false
|
||||
macNotFound := false
|
||||
if info, ok := mqttHandler.GetByIp(ip); ok {
|
||||
if userInfo, ok := macDb.Get(info.Mac); ok {
|
||||
mac = info.Mac
|
||||
name = userInfo.Name
|
||||
deviceName = userInfo.DeviceName
|
||||
visibility = userInfo.Visibility
|
||||
isLocallyAdministered = db.IsMacLocallyAdministered(mac)
|
||||
}
|
||||
} else {
|
||||
macNotFound = true
|
||||
}
|
||||
|
||||
c.HTML(http.StatusOK, "index.html", gin.H{
|
||||
"secToken": xsrfCheck.NewToken(ip),
|
||||
"name": name,
|
||||
"mac": mac,
|
||||
"deviceName": deviceName,
|
||||
"visibility": visibility,
|
||||
"isLocallyAdministered": isLocallyAdministered,
|
||||
"macNotFound": macNotFound,
|
||||
})
|
||||
}
|
||||
|
||||
type changeData struct {
|
||||
Action string `form:"action" binding:"required"`
|
||||
SecToken string `form:"secToken" binding:"required"`
|
||||
Name string `form:"name" binding:"required"`
|
||||
DeviceName string `form:"deviceName"`
|
||||
VisibilityNum uint8 `form:"visibility" binding:"required"`
|
||||
}
|
||||
|
||||
func changeInfoHandler(c *gin.Context) {
|
||||
ip, _, _ := net.SplitHostPort(c.Request.RemoteAddr)
|
||||
info, ok := mqttHandler.GetByIp(ip)
|
||||
if !ok {
|
||||
logger.WithField("ip", ip).Error("No data for ip found.")
|
||||
sendError(c, "No data for your ip found.")
|
||||
return
|
||||
}
|
||||
|
||||
xsrfCheck.stopCleaner <- true
|
||||
|
||||
logger = logger.WithField("mac", info.Mac)
|
||||
|
||||
var form changeData
|
||||
if err := c.Bind(&form); err != nil {
|
||||
logger.WithError(err).Error("Invalid binding.")
|
||||
sendError(c, "Invalid binding.")
|
||||
return
|
||||
}
|
||||
|
||||
if !xsrfCheck.CheckAndClearToken(ip, form.SecToken) {
|
||||
logger.WithFields(logrus.Fields{"ip": ip, "secToken": form.SecToken}).Error("Invalid secToken")
|
||||
sendError(c, "Invalid secToken")
|
||||
return
|
||||
}
|
||||
|
||||
if form.Action == "delete" {
|
||||
logger.WithField("user", form.Name).Info("Delete user info.")
|
||||
|
||||
macDb.Delete(info.Mac)
|
||||
} else if form.Action == "update" {
|
||||
logger.WithField("data", fmt.Sprintf("%#v", form)).Info("Change user info.")
|
||||
|
||||
visibility, ok := db.ParseVisibility(form.VisibilityNum)
|
||||
if !ok {
|
||||
logger.WithField("VisibilityNum", form.VisibilityNum).Error("Invalid visibility.")
|
||||
sendError(c, "Invalid 'visibility' value")
|
||||
return
|
||||
}
|
||||
|
||||
entry := db.UserMacInfo{Name: form.Name, DeviceName: form.DeviceName, Visibility: visibility, Ts: time.Now().Unix() * 1000}
|
||||
macDb.Set(info.Mac, entry)
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusSeeOther, "/")
|
||||
}
|
||||
6757
webUI/assets/css/bootstrap.css
vendored
Normal file
6757
webUI/assets/css/bootstrap.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
webUI/assets/css/bootstrap.css.map
Normal file
1
webUI/assets/css/bootstrap.css.map
Normal file
File diff suppressed because one or more lines are too long
6
webUI/assets/css/bootstrap.min.css
vendored
Normal file
6
webUI/assets/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
webUI/assets/css/bootstrap.min.css.map
Normal file
1
webUI/assets/css/bootstrap.min.css.map
Normal file
File diff suppressed because one or more lines are too long
108
webUI/assets/css/custom.css
Normal file
108
webUI/assets/css/custom.css
Normal file
@@ -0,0 +1,108 @@
|
||||
.container {
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
padding: 30px 0;
|
||||
margin-top: 70px;
|
||||
border-top: 1px solid #E5E5E5;
|
||||
}
|
||||
|
||||
#banner {
|
||||
border-bottom: none;
|
||||
margin-top: -20px;
|
||||
}
|
||||
|
||||
#banner h1 {
|
||||
font-size: 60px;
|
||||
line-height: 1;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
|
||||
.hero-unit {
|
||||
position: relative;
|
||||
padding: 30px 15px;
|
||||
color: #F5F5F5;
|
||||
text-align: center;
|
||||
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.1);
|
||||
background: #4393B9;
|
||||
}
|
||||
|
||||
.hero-unit h1 small {
|
||||
color: #F5F5F5;
|
||||
}
|
||||
|
||||
.mac-form {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.info-problem .alert {
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
/* http://tobiasahlin.com/spinkit/ */
|
||||
.spinner {
|
||||
margin: 10px auto 0;
|
||||
width: 70px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.spinner > div {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background-color: #333;
|
||||
|
||||
border-radius: 100%;
|
||||
display: inline-block;
|
||||
-webkit-animation: bouncedelay 1.4s infinite ease-in-out;
|
||||
animation: bouncedelay 1.4s infinite ease-in-out;
|
||||
/* Prevent first frame from flickering when animation starts */
|
||||
-webkit-animation-fill-mode: both;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
.spinner .bounce1 {
|
||||
-webkit-animation-delay: -0.32s;
|
||||
animation-delay: -0.32s;
|
||||
}
|
||||
|
||||
.spinner .bounce2 {
|
||||
-webkit-animation-delay: -0.16s;
|
||||
animation-delay: -0.16s;
|
||||
}
|
||||
|
||||
@-webkit-keyframes bouncedelay {
|
||||
0%, 80%, 100% { -webkit-transform: scale(0.0) }
|
||||
40% { -webkit-transform: scale(1.0) }
|
||||
}
|
||||
|
||||
@keyframes bouncedelay {
|
||||
0%, 80%, 100% {
|
||||
transform: scale(0.0);
|
||||
-webkit-transform: scale(0.0);
|
||||
} 40% {
|
||||
transform: scale(1.0);
|
||||
-webkit-transform: scale(1.0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* help page styling */
|
||||
.help .screenshot {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.help img {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.help .legend {
|
||||
color: #c60000;
|
||||
font-weight: bold;
|
||||
font-size: 120%;
|
||||
}
|
||||
|
||||
.help .back-button {
|
||||
margin-top: 20px;
|
||||
}
|
||||
BIN
webUI/assets/images/screenshot.png
Normal file
BIN
webUI/assets/images/screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
49
webUI/templates/help.html
Normal file
49
webUI/templates/help.html
Normal file
@@ -0,0 +1,49 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<base href="/">
|
||||
<title>Space Devices</title>
|
||||
<meta name="description" content="">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
|
||||
<link rel="stylesheet" href="assets/css/bootstrap.css">
|
||||
<link rel="stylesheet" href="assets/css/custom.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<header class="hero-unit" id="banner">
|
||||
<div class="container">
|
||||
<h1>Hilfe</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="container help">
|
||||
<h2>Ein Beispiel</h2>
|
||||
von der <a href="https://status.kreativitaet-trifft-technik.de">Status</a> Seite.
|
||||
|
||||
<div class="img-responsive screenshot">
|
||||
<img src="assets/images/screenshot.png" class="img-thumbnail" />
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
<li>Mit "Mit Name/Alias anzeigen." wirst du in <span class="legend">A</span> angezeigt.</li>
|
||||
<li>Mit "Als anonyme Person anzeigen." wirst du in <span class="legend">B</span> angezeigt.</li>
|
||||
<li>Wenn du noch gar nichts eingetragen hast oder "Eintrag löschen" gewählt hast, dann wirst du in <span class="legend">C</span> angezeigt.
|
||||
</li>
|
||||
<li>Mit "Gar nicht anzeigen" wirst du nirgends angezeigt.</li>
|
||||
</ul>
|
||||
|
||||
<a href="/" class="btn btn-primary back-button">Zurück</a>
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<p><a href="https://github.com/ktt-ol/spaceDevices">https://github.com/ktt-ol/spaceDevices</a></p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
134
webUI/templates/index.html
Normal file
134
webUI/templates/index.html
Normal file
@@ -0,0 +1,134 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<base href="/">
|
||||
<title>Space Devices</title>
|
||||
<meta name="description" content="">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
|
||||
<link rel="stylesheet" href="assets/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="assets/css/custom.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<header class="hero-unit" id="banner">
|
||||
<div class="container">
|
||||
<h1>
|
||||
{{.name}} <small>[{{.deviceName}}]</small>
|
||||
</h1>
|
||||
|
||||
<p class="lead">
|
||||
Deine Mac Adresse lautet: {{.mac}}
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{{if .noMacFound }}
|
||||
<div class="container info-problem">
|
||||
|
||||
<div class="alert alert-warning" role="alert">
|
||||
Deine Mac Adresse wurde nicht gefunden. Evt. funktioniert es in ein paar Minuten.
|
||||
<button onclick="window.location.reload()" class="btn btn-primary">Neu laden</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
|
||||
{{if .isLocallyAdministered }}
|
||||
<div class="container info-problem">
|
||||
|
||||
<div class="alert alert-warning" role="alert">
|
||||
Deine Mac Adresse wird zufällig generiert, daher kannst du dafür keinen Namen eingeben. Bei Windows 10 kann man
|
||||
das angeblich ändern.
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if (not .isLocallyAdministered) and (not .noMacFound)}}
|
||||
<div class="container mac-update">
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<h1 class="page-header">Ändern</h1>
|
||||
Ändere deinen Namen auf <a href="https://status.kreativitaet-trifft-technik.de">Status</a>.
|
||||
Der wird immer dann angezeigt, wenn du mit diesem Gerät im Mainframe WLAN bist.
|
||||
</div>
|
||||
</div>
|
||||
<form class="mac-form" action="/" method="post" onsubmit="onSubmit()" id="form">
|
||||
<input type="hidden" name="action" value="update" id="action" />
|
||||
<input type="hidden" name="secToken" value="{{.secToken}}" />
|
||||
<div class="form-group">
|
||||
<label for="name">Name/Alias</label>
|
||||
<input type="text" class="form-control" id="name" name="name" placeholder="Dein Name" value="{{.name}}" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="deviceName">Gerätename</label>
|
||||
<input type="text" class="form-control" id="deviceName" name="deviceName" placeholder="Notebook" value="{{.deviceName}}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Sichtbarkeit</label>
|
||||
<a class="help-link pull-right" href="help.html">Hilfe! Was heißt das?</a>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="visibility" value="3" {{if eq .visibility 3}}checked{{end}}>
|
||||
Alles anzeigen, d.h. Name/Alias und Gerätename.
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="visibility" value="2" {{if eq .visibility 2}}checked{{end}}>
|
||||
Mit Name/Alias anzeigen.
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="visibility" value="1" {{if eq .visibility 1}}checked{{end}}>
|
||||
Als anonyme Person anzeigen.
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="visibility" value="0" {{if eq .visibility 0}}checked{{end}}>
|
||||
Gar nicht anzeigen. Die <u>wirklich</u> paranoide Option. Meistens ist der obere Punkt besser.
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<a class="btn btn-danger" onclick="deleteName()">Eintrag löschen</a>
|
||||
<button class="btn btn-primary pull-right" type="submit" id="submitButton">Speichern</button>
|
||||
</div>
|
||||
<div class="form-group" id="waiting" style="display: none">
|
||||
<div class="spinner">
|
||||
<div class="bounce1"></div>
|
||||
<div class="bounce2"></div>
|
||||
<div class="bounce3"></div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<p><a href="https://github.com/ktt-ol/spaceDevices">https://github.com/ktt-ol/spaceDevices</a></p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
function onSubmit() {
|
||||
document.getElementById("waiting").style.display = "block";
|
||||
}
|
||||
function deleteName() {
|
||||
document.getElementById("action").value = "delete";
|
||||
document.getElementById("form").submit();
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user