Min před 6 roky
rodič
revize
f6b4b46e64

+ 2 - 0
.gitignore

@@ -1 +1,3 @@
 build/
+config.json
+*.html

+ 27 - 5
server/config.go

@@ -2,19 +2,26 @@ package main
 
 import (
 	"encoding/json"
+	"fmt"
+	"golang.org/x/crypto/bcrypt"
 	"io/ioutil"
 	"os"
+	"time"
 )
 
 var configFile = "config.json"
 var config = &configMain{
-	Admins: map[string]configAdmin{},
-	Boards: map[string]configBoard{},
+	Admins: map[string]configAdmin{
+		"admin": {"admin", []byte("$2a$14$iAOnPBQp6rlPIMGmTzH8qef1pK6tgl9yIfSLlZ2Z9IzIYn6hQPawi")},
+	},
+	Boards: map[uint16]configBoard{},
+	Secret: RandString(24),
 }
 
 type configMain struct{
 	Admins map[string]configAdmin `json:"admins"`
-	Boards map[string]configBoard `json:"boards"`
+	Boards map[uint16]configBoard `json:"boards"`
+	Secret string `json:"secret"`
 }
 
 type configAdmin struct{
@@ -22,13 +29,28 @@ type configAdmin struct{
 	Hash []byte		`json:"hash"`
 }
 
+func (a *configAdmin) newPassword(pass string) error {
+	var err error
+	a.Hash, err = bcrypt.GenerateFromPassword([]byte(pass), 14)
+	return err
+}
+
+func (a *configAdmin) checkPassword(pass string) bool {
+	err := bcrypt.CompareHashAndPassword(a.Hash, []byte(pass))
+	return err == nil
+}
+
 type configBoard struct{
-	ID uint16	`json:"ID"`
-	KEY []byte	`json:"KEY"`
+	ID uint16				`json:"ID"`
+	KEY []byte				`json:"KEY"`
+	KEY16 string			`json:"KEY16"`
+	LastSeen time.Time 		`json:"LastSeen"`
 }
 
 func loadConfig() {
 	if _, err := os.Stat(configFile); os.IsNotExist(err) {
+		fmt.Println("No config file. Creating a new one.")
+		saveConfig()
 		return
 	}
 	jsonFile, err := os.Open(configFile)

+ 190 - 24
server/inteface.go

@@ -1,50 +1,216 @@
 package main
 
 import (
+	"crypto/rand"
+	"encoding/hex"
 	"fmt"
+	"github.com/gorilla/sessions"
 	"github.com/labstack/echo"
+	"github.com/labstack/echo-contrib/session"
 	"github.com/labstack/echo/middleware"
-	"golang.org/x/crypto/bcrypt"
 	"net/http"
+	"regexp"
+	"strconv"
+	"strings"
+	"time"
 )
 
+type userConfig struct {
+	Admins []string			`json:"admins"`
+	Boards []configBoard	`json:"boards"`
+}
+
+func genConfig() *userConfig {
+	u := &userConfig{
+		make([]string, 0, len(config.Admins)),
+		make([]configBoard, 0, len(config.Boards)),
+	}
+	for k := range config.Admins { u.Admins = append(u.Admins, k) }
+	for b := range config.Boards { u.Boards = append(u.Boards, config.Boards[b]) }
+	return u
+}
+
+func serveIndex(c echo.Context) error {
+	return c.File("server/templates/head.html")
+}
+
+func serveLogin(c echo.Context) error {
+	return c.File("server/templates/login.html")
+}
+
 func serveInterface(address string) {
 	e := echo.New()
+
+	e.Use(middleware.GzipWithConfig(middleware.GzipConfig{Level: 5}))
+	e.Use(session.Middleware(sessions.NewCookieStore([]byte(config.Secret))))
+
 	e.GET("/", func(c echo.Context) error {
-		return c.String(http.StatusOK, "ELEC0017 Project")
-		//return c.JSON(http.StatusOK, &config)
+		sess, _ := session.Get("session", c)
+		if auth, ok := sess.Values["authorised"]; ok && auth.(bool) {
+			return c.Redirect(http.StatusTemporaryRedirect, "/admin")
+		}
+		return c.Redirect(http.StatusTemporaryRedirect, "/login")
 	})
-	e.GET("/new/admin", func(c echo.Context) error {
-		name := c.QueryParam("username")
-		pass := c.QueryParam("password")
+
+	e.GET("/login", serveLogin)
+	g := e.Group("/admin")
+
+	g.Use(func(next echo.HandlerFunc) echo.HandlerFunc  {
+		return func(c echo.Context) error {
+			sess, _ := session.Get("session", c)
+			auth, ok := sess.Values["authorised"]
+			if !ok || !auth.(bool) {
+				fmt.Println("Not authorised")
+				return c.Redirect(http.StatusTemporaryRedirect, "/login")
+			}
+			fmt.Println("Authorised")
+			return next(c)
+		}
+	})
+
+	g.GET("", serveIndex)
+	g.POST("", serveIndex)
+
+	g.POST("/user", func(c echo.Context) error {
+		sess, _ := session.Get("session", c)
+		name := strings.ToLower(c.FormValue("username"))
+		matched, _ := regexp.MatchString(`\w`, name)
+		if !matched {
+			return c.String(http.StatusNotAcceptable, "Invalid username. Allowed symbols: a-z 0-9 _")
+		}
+		pass := c.FormValue("password")
 		if len(name) == 0 {
-			return c.String(400, "No username")
+			return c.String(http.StatusNotAcceptable, "No username")
+		}
+		admin, exist := config.Admins[name]
+		if exist {
+			if len(pass) > 0 {
+				if sess.Values["username"] != name {
+					return c.String(http.StatusNotAcceptable, "Can't change other user passwords")
+				}
+				err := admin.newPassword(pass)
+				if err != nil {
+					fmt.Printf("Failed hash password %s\n", err)
+					return c.String(http.StatusNotAcceptable, "Failed to hash")
+				}
+			}
+		} else {
+			if len(pass) == 0 {
+				return c.String(http.StatusNotAcceptable, "No password")
+			}
+			admin := configAdmin{name, nil}
+			err := admin.newPassword(pass)
+			if err != nil {
+				fmt.Printf("Failed hash password %s\n", err)
+				return c.String(http.StatusNotAcceptable, "Failed to hash")
+			}
+			config.Admins[name] = admin
+ 		}
+
+		saveConfig()
+		return c.JSON(http.StatusOK, genConfig())
+	})
+	g.POST("/board", func(c echo.Context) error {
+		id64, err := strconv.ParseUint(c.FormValue("id"), 10, 16)
+		if err != nil {
+			return c.String(http.StatusNotAcceptable, "Invalid ID")
 		}
-		if len(pass) == 0 {
-			return c.String(400, "No password")
+		id := uint16(id64)
+		keysize, err := strconv.ParseUint(c.FormValue("keysize"), 10, 16)
+		if err != nil || (keysize != 128 && keysize != 192 && keysize != 256) {
+			return c.String(http.StatusNotAcceptable, "Invalid key size")
 		}
-		hash, err := bcrypt.GenerateFromPassword([]byte(pass), 14)
+		key, err := hex.DecodeString(c.FormValue("key"))
 		if err != nil {
-			fmt.Printf("Failed hash password %s\n", err)
-			return c.String(400, "Error")
+			return c.String(http.StatusNotAcceptable, "Key is not base 16")
+		}
+		if len(key) != int(keysize/8) && len(key) != 0 {
+			return c.String(http.StatusNotAcceptable, "Key does not match key size")
+		}
+
+		if board, exist := config.Boards[id]; exist {
+			// Update settings
+			if len(key) > 0 {
+				board.KEY = key
+				board.KEY16 = hex.EncodeToString(key)
+			}
+		} else {
+			// New board
+			if len(key) == 0 {
+				key = make([]byte, keysize/8)
+				_, err = rand.Read(key)
+				errCheck(err, "Failed generate random key")
+			}
+			config.Boards[id] = configBoard{
+				id,
+				key,
+				hex.EncodeToString(key),
+				time.Unix(0, 0),
+			}
 		}
-		cx := *config
-		cx.Admins[name] = configAdmin{name, hash}
 		saveConfig()
-		return c.String(200, "OK")
+		return c.JSON(http.StatusOK, genConfig())
 	})
+	g.DELETE("/user/:name", func(c echo.Context) error {
+		sess, _ := session.Get("session", c)
+		if sess.Values["username"] == c.Param("name") {
+			return c.String(http.StatusNotAcceptable, "Can't delete yourself")
+		}
+		_, ok := config.Admins[c.Param("name")]
+		if !ok {
+			return c.String(http.StatusNotFound, "Not found")
+		}
 
-	//g := e.Group("/admin")
-	e.Use(middleware.BasicAuth(func(username, password string, c echo.Context) (bool, error) {
-		user, ok := config.Admins[username]
+		delete(config.Admins, c.Param("name"))
+		saveConfig()
+		return c.JSON(http.StatusOK, genConfig())
+	})
+	g.DELETE("/board/:id", func(c echo.Context) error {
+		id64, err := strconv.ParseUint(c.Param("id"), 10, 16)
+		if err != nil {
+			return c.String(http.StatusNotAcceptable, "Invalid ID")
+		}
+		id := uint16(id64)
+		_, ok := config.Boards[id]
 		if !ok {
-			return false, nil
+			return c.String(http.StatusNotFound, "Not found")
+		}
+		delete(config.Boards, id)
+		saveConfig()
+		return c.JSON(http.StatusOK, genConfig())
+	})
+
+	e.POST("/login", func(c echo.Context) error {
+		username := c.FormValue("username")
+		password := c.FormValue("password")
+		user, ok := config.Admins[username]
+		if !ok || !user.checkPassword(password) {
+			return c.File("server/templates/login.html")
 		}
-		err := bcrypt.CompareHashAndPassword(user.Hash, []byte(password))
-		return err == nil, nil
-	}))
-	e.GET("/admin/config", func(c echo.Context) error {
-		return c.JSON(http.StatusOK, &config)
+		sess, _ := session.Get("session", c)
+		sess.Options = &sessions.Options{
+			Path:     "/",
+			MaxAge:   86400 * 7,
+			HttpOnly: true,
+		}
+		sess.Values["username"] = user.Username
+		sess.Values["authorised"] = true
+		err := sess.Save(c.Request(), c.Response())
+		errCheck(err, "Failed to save session")
+		return c.Redirect(http.StatusTemporaryRedirect, "/admin")
+	})
+
+	g.GET("/logout", func(c echo.Context) error {
+		sess, _ := session.Get("session", c)
+		sess.Values["authorised"] = false
+		err := sess.Save(c.Request(), c.Response())
+		errCheck(err, "Failed to save session")
+		return c.Redirect(http.StatusTemporaryRedirect, "/login")
+	})
+
+	//g.Use(middleware.BasicAuth()
+	g.GET("/config", func(c echo.Context) error {
+		return c.JSON(http.StatusOK, genConfig())
 	})
 
 	e.Logger.Fatal(e.Start(address))

+ 11 - 4
server/server.go

@@ -4,24 +4,30 @@ import (
 	"crypto/aes"
 	"crypto/cipher"
 	"encoding/binary"
-	"encoding/hex"
 	"fmt"
 	"net"
+	"time"
 )
 
-var aes_key = "03020100070605040b0a09080f0e0d0c"
 
 
 func serveConnection(conn net.Conn) {
 	defer logPanic()
 	defer conn.Close()
 	fmt.Printf("Connection from %s\n", conn.RemoteAddr().String())
-	decoded, _ := hex.DecodeString(aes_key)
 	buf := make([]byte, 4)
 	for {
 		_, err := conn.Read(buf)
 		errCheckPanic(err, "Connection failed with %s", conn.RemoteAddr().String())
 		id := binary.LittleEndian.Uint16(buf[:2])
+
+		board, ok := config.Boards[id]
+		if !ok {
+			fmt.Printf("Connection with ID %d is not registerd", id)
+			break
+		}
+		board.LastSeen = time.Now()
+
 		msglen := binary.LittleEndian.Uint16(buf[2:])
 		fmt.Printf("Connection from ID %d message length %d\n %x\n", id, msglen, buf)
 		payload := make([]byte, msglen)
@@ -29,7 +35,8 @@ func serveConnection(conn net.Conn) {
 		errCheckPanic(err, "Connection failed %s device #%d", conn.RemoteAddr().String(), id)
 		fmt.Printf("Payload: %x\n", payload)
 		message := make([]byte, msglen-16)
-		block, err := aes.NewCipher(decoded)
+
+		block, err := aes.NewCipher(board.KEY)
 		errCheckPanic(err, "Decryption failed %s device #%d", conn.RemoteAddr().String(), id)
 		mode := cipher.NewCBCDecrypter(block, payload[:16])
 		mode.CryptBlocks(message, payload[16:])

+ 30 - 0
server/templates/admins.pug

@@ -0,0 +1,30 @@
+h1.title Admins
+.columns
+    .column
+        .card
+            .card-header
+                p.card-header-title Create or modify user
+            .card-content
+                .field
+                    label.label Username
+                    .control
+                        input.input(type="text" v-model="admin.new_user")
+                .field
+                    label.label Password
+                    .control
+                        input.input(type="password" v-model="admin.new_pass")
+
+                .field
+                    .control
+                        button.button.is-primary(@click="admin_create") Submit
+    .column
+        table.table.is-striped.is-hoverable.is-fullwidth
+            thead
+                tr
+                    th Username
+                    th Actions
+            tbody
+                tr(v-for="admin in config.admins")
+                    td {{ admin }}
+                    td
+                        a.button.is-danger(@click="admin_delete(admin)") Delete

+ 42 - 0
server/templates/boards.pug

@@ -0,0 +1,42 @@
+h1.title Boards
+.columns
+    .column
+        .card
+            .card-header
+                p.card-header-title Create or modify board
+            .card-content
+                .field
+                    label.label ID
+                    .control
+                        input.input(type="number" v-model="board.new_id" min="0", max="65535", step="1")
+                .field
+                    label.label Key size
+                    .control
+                        .select
+                            select(v-model="board.new_keysize")
+                                option(:value="128") 128 bit
+                                option(:value="192") 192 bit
+                                option(:value="256") 256 bit
+                .field
+                    label.label Key (in base16)
+                    .control
+                        input.input(type="text" v-model="admin.new_key")
+                p.help Leave empty for random
+                .field
+                    .control
+                        button.button.is-primary(@click="board_create") Submit
+    .column
+        table.table.is-striped.is-hoverable.is-fullwidth
+            thead
+                tr
+                    th ID
+                    th Key
+                    th Last Seen
+                    th Actions
+            tbody
+                tr(v-for="board in config.boards")
+                    td {{ board.ID }}
+                    td {{ board.KEY16 }}
+                    td {{ board.LastSeen | timeSince }}
+                    td
+                        a.button.is-danger(@click="board_delete(board.ID)") Delete

+ 123 - 0
server/templates/head.pug

@@ -0,0 +1,123 @@
+doctype html
+html(lang='en')
+    head
+        meta(charset="utf-8")
+        meta(name="viewport" content="width=device-width, initial-scale=1")
+        link(rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.4/css/bulma.min.css")
+        title ELEC0017 Project
+        script(src="https://cdn.jsdelivr.net/npm/vue")
+        script(src="https://unpkg.com/axios/dist/axios.min.js")
+    body
+        #app
+            .tabs
+                ul
+                    li(v-for="menu in menus" :class="{'is-active': menu_selected === menu}")
+                        a(@click="menu_selected = menu") {{ menu }}
+
+            section.section
+                article.message.is-warning(v-show="error")
+                    .message-header
+                        p Error
+                        button.delete(@click="error = null")
+                    .message-body {{ error }}
+                template(v-if="menu_selected === 'admins'")
+                    include admins
+                template(v-else-if="menu_selected === 'boards'")
+                    include boards
+                template(v-else)
+                    include index
+
+        script.
+          axios.defaults.validateStatus = function (status) {
+            return status >= 200 && status < 410;
+          }
+          axios.interceptors.response.use(function (response) {
+            if (response.status >= 400) {
+              app.error = response.data;
+              return Promise.reject(response);
+            } else if (response.status == 307 || response.status == 306) {
+              console.log(response);
+            }
+            return response;
+          }, function (error) {
+            return Promise.reject(error);
+          });
+
+          var app = new Vue({
+            el: '#app',
+            data: {
+              menus: ['index', 'boards', 'admins'],
+              menu_selected: 'index',
+              config: {},
+              admin: {new_user: "", new_pass: ""},
+              board: {new_id: "", new_key: "", new_keysize: 128},
+              error: ""
+            },
+            methods: {
+              admin_delete: function(admin) {
+                let self = this;
+                axios.delete('/admin/user/' + admin).then(function (res) {
+                  self.config = res.data;
+                })
+              },
+              admin_create: function () {
+                let self = this;
+                const params = new URLSearchParams();
+                params.append('username', self.admin.new_user);
+                params.append('password', self.admin.new_pass);
+                axios.post('/admin/user', params).then(function (res) {
+                  self.admin.new_user = ""
+                  self.admin.new_pass = ""
+                  self.config = res.data;
+                })
+              },
+              board_delete: function (id) {
+                let self = this;
+                axios.delete('/admin/board/' + id).then(function (res) {
+                  self.config = res.data;
+                })
+              },
+              board_create: function () {
+                let self = this;
+                const params = new URLSearchParams();
+                params.append('id', self.board.new_id);
+                params.append('key', self.board.new_key);
+                params.append('keysize', self.board.new_keysize);
+                axios.post('/admin/board', params).then(function (res) {
+                    self.board = {new_id: "", new_key: "", new_keysize: 128}
+                    self.config = res.data;
+                })
+              }
+            },
+            mounted: function () {
+              let self = this;
+              axios.get('/admin/config').then(function (res) {
+                self.config = res.data;
+              })
+            },
+            filters: {
+              timeSince: function (datestr) {
+                if (datestr === "0001-01-01T00:00:00Z") return "never";
+                let date = new Date(datestr);
+                let delta = new Date() - date;
+                if (delta > 1000*60*60*24) {
+                  let year = d.getFullYear();
+                  let month = '' + (d.getMonth() + 1);
+                  let day = '' + d.getDate();
+                  if (month.length < 2) month = '0' + month;
+                  if (day.length < 2) day = '0' + day;
+                  return [year, month, day].join('/');
+                }
+                let seconds = Math.floor(delta / 1000);
+                let interval = Math.floor(seconds / 3600);
+                if (interval > 1) {
+                  return interval + " hours";
+                }
+                interval = Math.floor(seconds / 60);
+                if (interval > 1) {
+                  return interval + " minutes";
+                }
+                return Math.floor(seconds) + " seconds";
+              }
+            }
+          })

+ 1 - 0
server/templates/index.pug

@@ -0,0 +1 @@
+h1.title ELEC0017 Project

+ 38 - 0
server/templates/login.pug

@@ -0,0 +1,38 @@
+doctype html
+html(lang='en')
+    head
+        meta(charset="utf-8")
+        meta(name="viewport" content="width=device-width, initial-scale=1")
+        link(rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.4/css/bulma.min.css")
+        title ELEC0017 Project
+        style.
+            body {
+                background: url(https://source.unsplash.com/JuFcQxgCXwA);
+                background-size: cover;
+                height: 100vh;
+            }
+            .container {
+                width: 450px;
+                margin-top: 20vh;
+            }
+            .title {
+                text-align: center;
+            }
+
+body
+    section.section.is-medium
+        .container
+            .box
+                h1.title ELEC0017 Package tracker
+                form(method="POST")
+                    .field
+                        .control
+                            input.input(type="text" name="username" placeholder="Username")
+
+                    .field
+                        .control
+                            input.input(type="password" name="password" placeholder="********")
+
+                    .field
+                        .control
+                            button.button.is-link.is-fullwidth(type="submit") Login

+ 11 - 0
server/utils.go

@@ -2,9 +2,20 @@ package main
 
 import (
 	"fmt"
+	"math/rand"
 	"os"
 )
 
+var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
+
+func RandString(n int) string {
+	b := make([]rune, n)
+	for i := range b {
+		b[i] = letters[rand.Intn(len(letters))]
+	}
+	return string(b)
+}
+
 func errCheck(err error, msg string, opts ...interface{}) {
 	if err != nil {
 		fmt.Errorf(msg + "\n", opts...)