[TASK] add websocket and webroot

This commit is contained in:
Martin Geno 2017-05-08 19:13:29 +02:00
parent d954d7c851
commit 7de61d62fb
No known key found for this signature in database
GPG Key ID: F0D39A37E925E941
31 changed files with 3184 additions and 26 deletions

View File

@ -9,14 +9,14 @@ import (
"time" "time"
"github.com/NYTimes/gziphandler" "github.com/NYTimes/gziphandler"
goji "goji.io"
"goji.io/pat"
configPackage "github.com/FreifunkBremen/freifunkmanager/config" configPackage "github.com/FreifunkBremen/freifunkmanager/config"
httpLib "github.com/FreifunkBremen/freifunkmanager/lib/http"
"github.com/FreifunkBremen/freifunkmanager/lib/log" "github.com/FreifunkBremen/freifunkmanager/lib/log"
"github.com/FreifunkBremen/freifunkmanager/lib/worker" "github.com/FreifunkBremen/freifunkmanager/lib/worker"
"github.com/FreifunkBremen/freifunkmanager/runtime" "github.com/FreifunkBremen/freifunkmanager/runtime"
"github.com/FreifunkBremen/freifunkmanager/ssh" "github.com/FreifunkBremen/freifunkmanager/ssh"
"github.com/FreifunkBremen/freifunkmanager/websocket"
"github.com/FreifunkBremen/freifunkmanager/yanic" "github.com/FreifunkBremen/freifunkmanager/yanic"
) )
@ -38,10 +38,10 @@ func main() {
sshmanager := ssh.NewManager(config.SSHPrivateKey) sshmanager := ssh.NewManager(config.SSHPrivateKey)
nodes := runtime.NewNodes(config.StatePath, config.SSHInterface, sshmanager) nodes := runtime.NewNodes(config.StatePath, config.SSHInterface, sshmanager)
nodesUpdateWorker := worker.NewWorker(time.Duration(3)*time.Minute, nodes.Updater) nodesUpdateWorker := worker.NewWorker(time.Duration(3)*time.Minute, nodes.Updater)
nodesSaveWorker := worker.NewWorker(time.Duration(3)*time.Minute, nodes.Saver) nodesSaveWorker := worker.NewWorker(time.Duration(3)*time.Second, nodes.Saver)
nodesUpdateWorker.Start() go nodesUpdateWorker.Start()
nodesSaveWorker.Start() go nodesSaveWorker.Start()
if config.Yanic.Enable { if config.Yanic.Enable {
yanicDialer := yanic.Dial(config.Yanic.Type, config.Yanic.Address) yanicDialer := yanic.Dial(config.Yanic.Type, config.Yanic.Address)
@ -49,15 +49,19 @@ func main() {
yanicDialer.Start() yanicDialer.Start()
} }
// Startwebserver websocket.Start(nodes)
router := goji.NewMux()
router.Handle(pat.New("/*"), gziphandler.GzipHandler(http.FileServer(http.Dir(config.Webroot)))) // Startwebserver
http.HandleFunc("/nodes", func(w http.ResponseWriter, r *http.Request) {
httpLib.Write(w, nodes)
log.HTTP(r).Info("done")
})
http.Handle("/", gziphandler.GzipHandler(http.FileServer(http.Dir(config.Webroot))))
srv := &http.Server{ srv := &http.Server{
Addr: config.WebserverBind, Addr: config.WebserverBind,
Handler: router,
} }
go func() { go func() {
if err := srv.ListenAndServe(); err != nil { if err := srv.ListenAndServe(); err != nil {
panic(err) panic(err)
@ -72,6 +76,7 @@ func main() {
sig := <-sigs sig := <-sigs
// Stop services // Stop services
websocket.Close()
srv.Close() srv.Close()
if config.Yanic.Enable { if config.Yanic.Enable {
yanicDialer.Close() yanicDialer.Close()

View File

@ -1,5 +1,7 @@
webserver_bind = ":8080" webserver_bind = ":8080"
webroot = "webroot" webroot = "./webroot"
state_path = "/tmp/freifunkmanager.json"
ssh_key = "/etc/id_rsa" ssh_key = "/etc/id_rsa"
ssh_interface = "wlp4s0" ssh_interface = "wlp4s0"

32
lib/http/io.go Normal file
View File

@ -0,0 +1,32 @@
// Package http provides the
// logic of the webserver
package http
import (
"encoding/json"
"errors"
"net/http"
)
// Function to read data from a request via json format
// Input: pointer to http request r, interface to
func Read(r *http.Request, to interface{}) (err error) {
if r.Header.Get("Content-Type") != "application/json" {
err = errors.New("no json data recived")
return
}
err = json.NewDecoder(r.Body).Decode(to)
return
}
// Function to write data as json to a http output
// Input: http response writer w, interface data
func Write(w http.ResponseWriter, data interface{}) {
js, err := json.Marshal(data)
if err != nil {
http.Error(w, "failed to encode response: "+err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(js)
}

56
lib/http/io_test.go Normal file
View File

@ -0,0 +1,56 @@
// Package http provides the
// logic of the webserver
package http
import (
"bytes"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
// Function to test the writing into a http response
// Input: pointer to testing object
func TestWrite(t *testing.T) {
assert := assert.New(t)
w := httptest.NewRecorder()
from := map[string]string{"a": "b"}
Write(w, from)
result := w.Result()
assert.Equal([]string{"application/json"}, result.Header["Content-Type"], "no header information")
buf := new(bytes.Buffer)
buf.ReadFrom(result.Body)
to := buf.String()
assert.Equal("{\"a\":\"b\"}", to, "wrong content")
w = httptest.NewRecorder()
value := make(chan int)
Write(w, value)
result = w.Result()
assert.Equal(http.StatusInternalServerError, result.StatusCode, "wrong statuscode")
}
// Function to test the reading from a http response
// Input: pointer to testing object
func TestRead(t *testing.T) {
assert := assert.New(t)
to := make(map[string]string)
r, _ := http.NewRequest("GET", "/a", strings.NewReader("{\"a\":\"b\"}"))
r.Header["Content-Type"] = []string{"application/json"}
err := Read(r, &to)
assert.NoError(err, "no error")
assert.Equal(map[string]string{"a": "b"}, to, "wrong content")
r.Header["Content-Type"] = []string{""}
err = Read(r, &to)
assert.Error(err, "no error")
}

11
lib/http/main.go Normal file
View File

@ -0,0 +1,11 @@
package http
import "net/http"
func GetRemoteIP(r *http.Request) string {
ip := r.Header.Get("X-Forwarded-For")
if len(ip) <= 1 {
ip = r.RemoteAddr
}
return ip
}

19
lib/http/main_test.go Normal file
View File

@ -0,0 +1,19 @@
package http
import (
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
// Function to test the logging
// Input: pointer to teh testing object
func TestGetIP(t *testing.T) {
assertion := assert.New(t)
req, _ := http.NewRequest("GET", "https://google.com/lola/duda?q=wasd", nil)
ip := GetRemoteIP(req)
assertion.Equal("", ip, "no remote ip address")
}

View File

@ -7,6 +7,8 @@ import (
"net/http" "net/http"
logger "github.com/Sirupsen/logrus" logger "github.com/Sirupsen/logrus"
httpLib "github.com/FreifunkBremen/freifunkmanager/lib/http"
) )
// current logger with configuration // current logger with configuration
@ -21,12 +23,8 @@ func init() {
// Function to add the information of a http request to the log // Function to add the information of a http request to the log
// Input: pointer to the http request r // Input: pointer to the http request r
func HTTP(r *http.Request) *logger.Entry { func HTTP(r *http.Request) *logger.Entry {
ip := r.Header.Get("X-Forwarded-For")
if len(ip) <= 1 {
ip = r.RemoteAddr
}
return Log.WithFields(logger.Fields{ return Log.WithFields(logger.Fields{
"remote": ip, "remote": httpLib.GetRemoteIP(r),
"method": r.Method, "method": r.Method,
"url": r.URL.RequestURI(), "url": r.URL.RequestURI(),
}) })

View File

@ -49,6 +49,9 @@ func NewNode(node *yanicRuntime.Node) *Node {
} }
func (n *Node) SSHUpdate(ssh *ssh.Manager, iface string, oldnode *Node) { func (n *Node) SSHUpdate(ssh *ssh.Manager, iface string, oldnode *Node) {
if oldnode == nil {
return
}
addr := n.GetAddress(iface) addr := n.GetAddress(iface)
if n.Hostname != oldnode.Hostname { if n.Hostname != oldnode.Hostname {
ssh.ExecuteOn(addr, fmt.Sprintf(SSHUpdateHostname, n.Hostname)) ssh.ExecuteOn(addr, fmt.Sprintf(SSHUpdateHostname, n.Hostname))

View File

@ -13,16 +13,17 @@ import (
type Nodes struct { type Nodes struct {
List map[string]*Node `json:"nodes"` List map[string]*Node `json:"nodes"`
ToUpdate map[string]struct{} ToUpdate map[string]*Node
ssh *ssh.Manager ssh *ssh.Manager
statePath string statePath string
iface string iface string
notifyFunc []func(*Node, bool)
} }
func NewNodes(path string, iface string, mgmt *ssh.Manager) *Nodes { func NewNodes(path string, iface string, mgmt *ssh.Manager) *Nodes {
nodes := &Nodes{ nodes := &Nodes{
List: make(map[string]*Node), List: make(map[string]*Node),
ToUpdate: make(map[string]struct{}), ToUpdate: make(map[string]*Node),
ssh: mgmt, ssh: mgmt,
statePath: path, statePath: path,
iface: iface, iface: iface,
@ -43,9 +44,12 @@ func (nodes *Nodes) AddNode(n *yanic.Node) {
if _, ok := nodes.ToUpdate[node.NodeID]; ok { if _, ok := nodes.ToUpdate[node.NodeID]; ok {
if nodes.List[node.NodeID].IsEqual(node) { if nodes.List[node.NodeID].IsEqual(node) {
delete(nodes.ToUpdate, node.NodeID) delete(nodes.ToUpdate, node.NodeID)
nodes.List[node.NodeID] = node
nodes.notify(node, true)
} }
} else { } else {
nodes.List[node.NodeID] = node nodes.List[node.NodeID] = node
nodes.notify(node, true)
} }
logger.Debugf("know already these node") logger.Debugf("know already these node")
return return
@ -61,14 +65,28 @@ func (nodes *Nodes) AddNode(n *yanic.Node) {
logger.Infof("new node with uptime: %s", uptime) logger.Infof("new node with uptime: %s", uptime)
nodes.List[node.NodeID] = node nodes.List[node.NodeID] = node
nodes.notify(node, true)
}
func (nodes *Nodes) AddNotify(f func(*Node, bool)) {
nodes.notifyFunc = append(nodes.notifyFunc, f)
}
func (nodes *Nodes) notify(node *Node, real bool) {
for _, f := range nodes.notifyFunc {
f(node, real)
}
} }
func (nodes *Nodes) UpdateNode(node *Node) { func (nodes *Nodes) UpdateNode(node *Node) {
if node == nil {
log.Log.Warn("no new node to update")
return
}
if n, ok := nodes.List[node.NodeID]; ok { if n, ok := nodes.List[node.NodeID]; ok {
go node.SSHUpdate(nodes.ssh, nodes.iface, n) go node.SSHUpdate(nodes.ssh, nodes.iface, n)
} }
nodes.List[node.NodeID] = node nodes.ToUpdate[node.NodeID] = node
nodes.ToUpdate[node.NodeID] = struct{}{} nodes.notify(node, false)
} }
func (nodes *Nodes) Updater() { func (nodes *Nodes) Updater() {
@ -77,12 +95,13 @@ func (nodes *Nodes) Updater() {
go node.SSHSet(nodes.ssh, nodes.iface) go node.SSHSet(nodes.ssh, nodes.iface)
} }
} }
log.Log.Debug("updater per ssh runs")
} }
func (nodes *Nodes) load() { func (nodes *Nodes) load() {
if f, err := os.Open(nodes.statePath); err == nil { // transform data to legacy meshviewer if f, err := os.Open(nodes.statePath); err == nil { // transform data to legacy meshviewer
if err = json.NewDecoder(f).Decode(nodes); err == nil { if err = json.NewDecoder(f).Decode(nodes); err == nil {
log.Log.Info("loaded", len(nodes.List), "nodes") log.Log.Infof("loaded %d nodes", len(nodes.List))
} else { } else {
log.Log.Error("failed to unmarshal nodes:", err) log.Log.Error("failed to unmarshal nodes:", err)
} }
@ -93,4 +112,5 @@ func (nodes *Nodes) load() {
func (nodes *Nodes) Saver() { func (nodes *Nodes) Saver() {
yanic.SaveJSON(nodes, nodes.statePath) yanic.SaveJSON(nodes, nodes.statePath)
log.Log.Debug("saved state file")
} }

13
webroot/.gitignore vendored Normal file
View File

@ -0,0 +1,13 @@
!node_modules/
node_modules/*
!node_modules/semantic-ui-css/
node_modules/semantic-ui-css/*
!node_modules/semantic-ui-css/semantic.min.css
!node_modules/semantic-ui-css/themes/
node_modules/semantic-ui-css/themes/*
!node_modules/semantic-ui-css/themes/default/
node_modules/semantic-ui-css/themes/default/*
!node_modules/semantic-ui-css/themes/default/assets/
node_modules/semantic-ui-css/themes/default/assets/*
!node_modules/semantic-ui-css/themes/default/assets/fonts/

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

@ -0,0 +1,11 @@
.loader {
color: #dc0067;
font-size: 1.8em;
line-height: 2;
margin: 30vh auto;
text-align: center;
}
.loader img {
}

BIN
webroot/img/logo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

29
webroot/index.html Normal file
View File

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>FreifunkManager</title>
<link rel="stylesheet" href="/css/main.css">
<script src="//localhost:35729/livereload.js"></script>
<script src="/js/config.js"></script>
<script src="/js/store.js"></script>
<script src="/js/gui.js"></script>
<script src="/js/socket.js"></script>
<script src="/js/app.js"></script>
</head>
<body>
<div class="loader">
<p>
Lade
<br />
<img src="/img/logo.jpg" class="spinner" />
<br />
Karten &amp; Knoten...
</p>
<noscript>
<strong>JavaScript required</strong>
</noscript>
</div>
</body>
</html>

1
webroot/js/app.js Normal file
View File

@ -0,0 +1 @@
document.title = config.title

4
webroot/js/config.js Normal file
View File

@ -0,0 +1,4 @@
var config = {
title: 'FreifunkManager - Breminale',
backend: 'ws://localhost:8080/websocket'
};

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

@ -0,0 +1,20 @@
var gui = {};
(function(){
gui.enable = function enable(){
};
gui.render = function render (){
console.log(store.will())
}
gui.disable = function disable(err){
document.querySelector('.loader').innerHTML += err
+ '<br /><br /><button onclick="location.reload(true)" class="btn text">Try to reload</button>';
console.warn(err);
};
gui.connecting = function connecting(){
};
})();

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

@ -0,0 +1,35 @@
var socket = new WebSocket(config.backend);
(function(){
// When the connection is open, send some data to the server
socket.onopen = function () {
gui.enable();
};
// Log errors
socket.onerror = function (err) {
gui.disable(err);
};
// Log messages from the server
socket.onmessage = function (e) {
var msg = JSON.parse(e.data);
if(msg.state === "current") {
store.updateReal(msg.node);
gui.render();
} else if (msg.state === "to-update") {
store.update(msg.node);
gui.render();
} else {
gui.disable(e);
}
};
socket.sendnode = function(node) {
var msg = {node:node};
var string = JSON.stringify(msg);
socket.send(string);
};
})();

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

@ -0,0 +1,21 @@
var store = {
list:{},
toupdate:{}
};
(function(){
store.updateReal = function updateReal(node){
store.list[node.node_id] = node;
};
store.update = function update(node){
store.toupdate[node.node_id] = node;
};
store.will = function() {
return Object.keys(store.list).map(function(nodeid){
if (store.toupdate[nodeid]) {
return store.toupdate[nodeid];
}
return store.list[nodeid];
});
};
})();

11
webroot/node_modules/semantic-ui-css/semantic.min.css generated vendored Executable file

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

5
webroot/package.json Normal file
View File

@ -0,0 +1,5 @@
{
"dependencies": {
"semantic-ui-css": "^2.2.10"
}
}

13
webroot/yarn.lock Normal file
View File

@ -0,0 +1,13 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
jquery@x.*:
version "3.2.1"
resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.2.1.tgz#5c4d9de652af6cd0a770154a631bba12b015c787"
semantic-ui-css@^2.2.10:
version "2.2.10"
resolved "https://registry.yarnpkg.com/semantic-ui-css/-/semantic-ui-css-2.2.10.tgz#f8f4470dbeffca0f0f3ff4fb71a35c71e88ad89c"
dependencies:
jquery x.*

106
websocket/client.go Normal file
View File

@ -0,0 +1,106 @@
package websocket
import (
"io"
"golang.org/x/net/websocket"
"github.com/FreifunkBremen/freifunkmanager/lib/log"
)
const channelBufSize = 100
type Client struct {
ip string
ws *websocket.Conn
ch chan *Message
doneCh chan bool
}
func disconnect(c *Client) {
delete(clients, c.ip)
}
func NewClient(ip string, ws *websocket.Conn) *Client {
if ws == nil {
panic("ws cannot be nil")
}
return &Client{
ws: ws,
ch: make(chan *Message, channelBufSize),
doneCh: make(chan bool),
ip: ip,
}
}
func (c *Client) Write(msg *Message) {
select {
case c.ch <- msg:
default:
disconnect(c)
log.HTTP(c.ws.Request()).Error("client is disconnected.")
}
}
func (c *Client) Done() {
c.doneCh <- true
}
func (c *Client) allNodes() {
for _, node := range nodes.List {
c.Write(&Message{State: StateCurrentNode, Node: node})
}
for _, node := range nodes.ToUpdate {
c.Write(&Message{State: StateUpdateNode, Node: node})
}
}
// Listen Write and Read request via chanel
func (c *Client) Listen() {
go c.listenWrite()
c.allNodes()
c.listenRead()
}
// Listen write request via chanel
func (c *Client) listenWrite() {
for {
select {
case msg := <-c.ch:
websocket.JSON.Send(c.ws, msg)
case <-c.doneCh:
disconnect(c)
c.doneCh <- true
return
}
}
}
// Listen read request via chanel
func (c *Client) listenRead() {
for {
select {
case <-c.doneCh:
disconnect(c)
c.doneCh <- true
return
default:
var msg Message
err := websocket.JSON.Receive(c.ws, &msg)
if err == io.EOF {
log.HTTP(c.ws.Request()).Info("disconnect")
c.doneCh <- true
} else if err != nil {
log.HTTP(c.ws.Request()).Error(err)
} else {
log.HTTP(c.ws.Request()).Info("recieve nodeupdate")
nodes.UpdateNode(msg.Node)
}
}
}
}

13
websocket/msg.go Normal file
View File

@ -0,0 +1,13 @@
package websocket
import "github.com/FreifunkBremen/freifunkmanager/runtime"
type Message struct {
State string `json:"state"`
Node *runtime.Node `json:"node"`
}
const (
StateUpdateNode = "to-update"
StateCurrentNode = "current"
)

59
websocket/server.go Normal file
View File

@ -0,0 +1,59 @@
package websocket
import (
"net/http"
"golang.org/x/net/websocket"
httpLib "github.com/FreifunkBremen/freifunkmanager/lib/http"
"github.com/FreifunkBremen/freifunkmanager/lib/log"
"github.com/FreifunkBremen/freifunkmanager/runtime"
)
var nodes *runtime.Nodes
var clients map[string]*Client
var quit chan struct{}
func Start(nodeBind *runtime.Nodes) {
nodes = nodeBind
clients = make(map[string]*Client)
quit = make(chan struct{})
http.Handle("/websocket", websocket.Handler(func(ws *websocket.Conn) {
defer func() {
ws.Close()
}()
r := ws.Request()
log.HTTP(r).Infof("new client")
ip := httpLib.GetRemoteIP(r)
client := NewClient(ip, ws)
clients[ip] = client
client.Listen()
}))
nodes.AddNotify(Notify)
/*
for {
select {
case <-quit:
return
}
}*/
}
func Notify(node *runtime.Node, real bool) {
state := StateUpdateNode
if real {
state = StateCurrentNode
}
for _, c := range clients {
c.Write(&Message{State: state, Node: node})
}
}
func Close() {
for _, c := range clients {
c.Done()
}
close(quit)
}

View File

@ -22,7 +22,7 @@ type Dialer struct {
func Dial(ctype, addr string) *Dialer { func Dial(ctype, addr string) *Dialer {
conn, err := net.Dial(ctype, addr) conn, err := net.Dial(ctype, addr)
if err != nil { if err != nil {
log.Log.Panic("yanic dial failed") log.Log.Panicf("yanic dial to %s:%s failed", ctype, addr)
} }
dialer := &Dialer{ dialer := &Dialer{
conn: conn, conn: conn,