Przeglądaj źródła

start on admin dashboard

Jordan Schalm 8 lat temu
rodzic
commit
902d6c753f

+ 164 - 0
dashboard/dashboard.go

@@ -0,0 +1,164 @@
+package dashboard
+
+import (
+	"html/template"
+	"math/rand"
+	"net/http"
+	"time"
+
+	"github.com/gorilla/mux"
+	"github.com/gorilla/websocket"
+)
+
+const (
+	dashboard      = "index.html"
+	login          = "login.html"
+	dashboardPath  = "html/index.html"
+	loginPath      = "html/login.html"
+	sessionTimeout = time.Hour * 24 // TODO replace with config
+)
+
+var (
+	// Cache of HTML templates
+	templates = template.Must(template.ParseFiles(dashboardPath, loginPath))
+	config    *Config
+	sessions  sessionStore
+	store     *dataStore
+)
+
+var upgrader = websocket.Upgrader{
+	ReadBufferSize:  1024,
+	WriteBufferSize: 1024,
+}
+
+type Config struct {
+	Username        string
+	Password        string
+	ListenInterface string
+}
+
+func Run(c *Config) {
+	config = c
+	r := mux.NewRouter()
+	r.HandleFunc("/", indexHandler)
+	r.HandleFunc("/login", loginHandler)
+	r.HandleFunc("/ws", webSocketHandler)
+
+	rand.Seed(time.Now().UnixNano())
+
+	sessions = make(sessionStore)
+	go sessions.cleaner(sessionTimeout)
+	store = newDataStore()
+	go ramListener(tickInterval, store)
+
+	http.ListenAndServe(c.ListenInterface, r)
+}
+
+func indexHandler(w http.ResponseWriter, r *http.Request) {
+	if isLoggedIn(r) {
+		w.WriteHeader(http.StatusOK)
+		templates.ExecuteTemplate(w, dashboard, nil)
+	} else {
+		http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
+	}
+}
+
+func loginHandler(w http.ResponseWriter, r *http.Request) {
+	switch r.Method {
+	case "GET":
+		if isLoggedIn(r) {
+			http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
+		} else {
+			templates.ExecuteTemplate(w, login, nil)
+		}
+
+	case "POST":
+		user := r.FormValue("username")
+		pass := r.FormValue("password")
+
+		if user == config.Username && pass == config.Password {
+			err := startSession(w, r)
+			if err != nil {
+				w.WriteHeader(http.StatusInternalServerError)
+				// TODO Internal error
+				return
+			}
+			http.Redirect(w, r, "/", http.StatusSeeOther)
+		} else {
+			templates.ExecuteTemplate(w, login, nil) // TODO info about failed login
+		}
+
+	default:
+		w.WriteHeader(http.StatusMethodNotAllowed)
+	}
+}
+
+func webSocketHandler(w http.ResponseWriter, r *http.Request) {
+	if !isLoggedIn(r) {
+		w.WriteHeader(http.StatusForbidden)
+		return
+	}
+
+	sess := getSession(r)
+	conn, err := upgrader.Upgrade(w, r, nil)
+	if err != nil {
+		w.WriteHeader(http.StatusInternalServerError)
+		// TODO Internal error
+		return
+	}
+	sess.ws = conn
+	c := make(chan *point)
+	sess.send = c
+	store.subscribe(c)
+	go sess.receive()
+	go sess.transmit()
+}
+
+func startSession(w http.ResponseWriter, r *http.Request) error {
+	sessionID := newSessionID()
+
+	cookie := &http.Cookie{
+		Name:  "SID",
+		Value: sessionID,
+		Path:  "/",
+		// Secure: true,
+	}
+
+	sess := &session{
+		start:   time.Now(),
+		expires: time.Now().Add(sessionTimeout), // TODO config for this
+		id:      sessionID,
+	}
+
+	http.SetCookie(w, cookie)
+	sessions[sessionID] = sess
+	return nil
+}
+
+func getSession(r *http.Request) *session {
+	c, err := r.Cookie("SID")
+	if err != nil {
+		return nil
+	}
+
+	sid := c.Value
+	sess, ok := sessions[sid]
+	if !ok {
+		return nil
+	}
+
+	return sess
+}
+
+func isLoggedIn(r *http.Request) bool {
+	sess := getSession(r)
+	if sess == nil {
+		return false
+	}
+
+	if !sess.valid() {
+		return false
+	}
+
+	return true
+}

+ 79 - 0
dashboard/datastore.go

@@ -0,0 +1,79 @@
+package dashboard
+
+import (
+	"runtime"
+	"time"
+)
+
+const (
+	tickInterval = time.Second
+	maxWindow    = time.Hour * 24
+	maxTicks     = int(maxWindow / tickInterval)
+)
+
+type dataStore struct {
+	ram  []*point
+	subs []chan<- *point
+}
+
+func newDataStore() *dataStore {
+	return &dataStore{
+		ram: make([]*point, 0, maxTicks),
+	}
+}
+
+func (ds *dataStore) addPoint(p *point) {
+	if len(ds.ram) == int(maxTicks) {
+		ds.ram = append(ds.ram[1:], p)
+	} else {
+		ds.ram = append(ds.ram, p)
+	}
+	ds.notify(p)
+}
+
+func (ds *dataStore) subscribe(c chan<- *point) {
+	ds.subs = append(ds.subs, c)
+}
+
+func (ds *dataStore) notify(p *point) {
+	var toUnsubscribe []int
+	for i, c := range ds.subs {
+		select {
+		case c <- p:
+		default:
+			close(c)
+			toUnsubscribe = append(toUnsubscribe, i)
+		}
+	}
+
+	if len(toUnsubscribe) > 0 {
+		newSubs := ds.subs[:0]
+		for i, c := range ds.subs {
+			if i != toUnsubscribe[0] {
+				newSubs = append(newSubs, c)
+			} else {
+				toUnsubscribe = toUnsubscribe[1:]
+				if len(toUnsubscribe) == 0 {
+					break
+				}
+			}
+		}
+		ds.subs = newSubs
+	}
+}
+
+type point struct {
+	T time.Time `json:"t"`
+	Y uint64    `json:"y"`
+}
+
+func ramListener(interval time.Duration, store *dataStore) {
+	ticker := time.Tick(interval)
+	memStats := &runtime.MemStats{}
+
+	for {
+		t := <-ticker
+		runtime.ReadMemStats(memStats)
+		store.addPoint(&point{t, memStats.Alloc})
+	}
+}

+ 13 - 0
dashboard/exe/main.go

@@ -0,0 +1,13 @@
+package main
+
+import (
+	"github.com/jordanschalm/guerrilla/dashboard"
+)
+
+func main() {
+	dashboard.Run(&dashboard.Config{
+		Password:        "password",
+		Username:        "admin",
+		ListenInterface: ":8080",
+	})
+}

+ 28 - 0
dashboard/html/index.html

@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html>
+	<head>
+		<title>Guerrilla | Dashboard</title>
+	</head>
+	<body>
+		<canvas id="ram-graph" width="500" height="200"></canvas>
+	</body>
+	<script src="https://cdnjs.cloudflare.com/ajax/libs/smoothie/1.27.0/smoothie.min.js"></script>
+	<script>
+	var conn = new WebSocket('ws://localhost:8080/ws');
+	var smoothie = new SmoothieChart();
+	var ram = new TimeSeries();
+
+	smoothie.addTimeSeries(ram);
+	smoothie.streamTo(document.getElementById('ram-graph'), 1000);
+
+	conn.onclose = function(event) {
+		console.log(event);
+	}
+
+	conn.onmessage = function(event) {
+		console.log(JSON.parse(event.data));
+		var point = JSON.parse(event.data);
+		ram.append(new Date(point.t).getTime(), point.y);
+	}
+	</script>
+</html>

+ 13 - 0
dashboard/html/login.html

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+	<head>
+		<title>Guerrilla | Login</title>
+	</head>
+	<body>
+		<form action="/login" method="POST">
+			<input type="text" name="username" required>Username</input>
+			<input type="password" name="password" required>Password</input>
+			<input type="submit" value="Submit" />
+		</form>
+	</body>
+</html>

BIN
dashboard/main


+ 118 - 0
dashboard/session.go

@@ -0,0 +1,118 @@
+package dashboard
+
+import (
+	"math/rand"
+	"time"
+
+	log "github.com/Sirupsen/logrus"
+	"github.com/gorilla/websocket"
+)
+
+const (
+	maxMessageSize = 1024
+	writeWait      = 5 * time.Second
+	pongWait       = 60 * time.Second
+	pingPeriod     = 50 * time.Second
+)
+
+var idCharset = []byte("qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890")
+
+type session struct {
+	start, expires time.Time
+	id             string
+	ws             *websocket.Conn
+	// Messages to send over the websocket are received on this channel
+	send <-chan *point
+}
+
+func (s *session) valid() bool {
+	return s.expires.After(time.Now())
+}
+
+// Receives messages from the websocket connection associated with a session
+func (s *session) receive() {
+	defer s.ws.Close()
+	s.ws.SetReadLimit(maxMessageSize)
+	s.ws.SetReadDeadline(time.Now().Add(pongWait))
+	s.ws.SetPongHandler(func(string) error {
+		s.ws.SetReadDeadline(time.Now().Add(pongWait))
+		return nil
+	})
+
+	for {
+		_, message, err := s.ws.ReadMessage()
+		if err != nil {
+			if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) {
+				log.WithError(err).Error("Websocket closed unexpectedly")
+				break
+			}
+		}
+		log.Infof("Message: %s", string(message))
+	}
+}
+
+// Transmits messages to the websocket connection associated with a session
+func (s *session) transmit() {
+	ticker := time.NewTicker(pingPeriod)
+	defer s.ws.Close()
+	defer ticker.Stop()
+
+	for {
+		select {
+		case p, ok := <-s.send:
+			s.ws.SetWriteDeadline(time.Now().Add(writeWait))
+			if !ok || !s.valid() {
+				s.ws.WriteMessage(websocket.CloseMessage, []byte{})
+				break
+			}
+
+			err := s.ws.WriteJSON(p)
+			if err != nil {
+				log.WithError(err).Debug("Failed to write next websocket message. Closing connection")
+				break
+			}
+		case <-ticker.C:
+			s.ws.SetWriteDeadline(time.Now().Add(writeWait))
+			if err := s.ws.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
+				log.WithError(err).Debug("Failed to write next websocket message. Closing connection")
+				break
+			}
+		}
+	}
+}
+
+type sessionStore map[string]*session
+
+// Remove expired sessions
+func (ss sessionStore) clean() {
+	now := time.Now()
+	for id, sess := range ss {
+		if sess.expires.Before(now) {
+			delete(ss, id)
+		}
+	}
+}
+
+// Cleans the store on each tick
+func (ss sessionStore) cleaner(interval time.Duration) {
+	ticker := time.NewTicker(interval)
+	defer ticker.Stop()
+	for {
+		<-ticker.C
+		ss.clean()
+	}
+}
+
+// Returns a random alphanumeric 10-character ID
+func newSessionID() string {
+	mask := int64(63)
+	gen := rand.Int63()
+	out := []byte{}
+
+	for i := 0; i < 10; i++ {
+		out = append(out, idCharset[int(gen&mask)%58])
+		gen = gen >> 6
+	}
+
+	return string(out)
+}