Pārlūkot izejas kodu

Moved WsTTY out of ArozOS Core and tidy the folder structure

TC pushbot 5 4 gadi atpakaļ
vecāks
revīzija
ca01fdd259

+ 0 - 117
Quick Notes for Setting Up Raspberry Pi 4 as WiFi Router.md

@@ -1,117 +0,0 @@
-# Quick Notes for Setting Up Raspberry Pi 4 as WiFi Router
-
-This is just a quick notes for myself on how to setup a Raspberry Pi 4 with Mercury AC650M USB WiFi Adapter
-
-### Problem
-
-The current setup of the system make use of a ARGON ONE metal case which, will make the build in WiFi adapter really hard to use as an AP. Hence, we need to setup an external WiFi adapter for this purpose.
-
-### Required Parts
-
-- Mercury USB WiFi Adapter AC650M (Dual baud 5G no driver version)
-- ARGON ONE metal case
-- Raspberry Pi 4B
-- 64GB Micro SD card
-
-
-
-### Installation
-
-1. Install Raspberry Pi OS and run apt-updates
-
-2. Download the driver for RTL8821CU
-
-   ```
-   mkdir -p ~/build
-   cd ~/build
-   git clone https://github.com/brektrou/rtl8821CU.git
-   ```
-
-   
-
-3. Install DKMS
-
-   ```
-   sudo apt-get install dkms
-   ```
-
-   
-
-4. Upgrade apt
-
-   ```
-   sudo apt update -y
-   sudo apt upgrade -y
-   ```
-
-5. Install bc and reboot
-
-   ```
-   sudo apt-get install bc
-   sudo reboot
-   ```
-
-6. Edit the Make file of the downloaded repo and change these two lines as follows
-
-   ```
-   CONFIG_PLATFORM_I386_PC = y
-   CONFIG_PLATFORM_ARM_RPI = n
-   ```
-
-   to
-
-   ```
-   CONFIG_PLATFORM_I386_PC = n
-   CONFIG_PLATFORM_ARM_RPI = y
-   ```
-
-7. Fix the compile flag on ARM processor
-
-   ```
-   sudo cp /lib/modules/$(uname -r)/build/arch/arm/Makefile /lib/modules/$(uname -r)/build/arch/arm/Makefile.$(date +%Y%m%d%H%M)
-   sudo sed -i 's/-msoft-float//' /lib/modules/$(uname -r)/build/arch/arm/Makefile
-   sudo ln -s /lib/modules/$(uname -r)/build/arch/arm /lib/modules/$(uname -r)/build/arch/armv7l
-   ```
-
-8. Build via DKMS
-
-   ```
-   sudo ./dkms-install.sh
-   ```
-
-   
-
-9. Plug your USB-wifi-adapter into your PC
-
-10. If wifi can be detected, congratulations. If not, maybe you need to switch your device usb mode by the following steps in terminal:
-
-    1. Find your usb-wifi-adapter device ID, like "0bda:1a2b", by type: ```lsusb```
-
-    2. Need install `usb_modeswitch` 
-
-       ```
-       sudo usb_modeswitch -KW -v 0bda -p 1a2b
-       systemctl start bluetooth.service
-       ```
-
-11.  Edit `usb_modeswitch` rules:
-
-    ```
-    udo nano /lib/udev/rules.d/40-usb_modeswitch.rules
-    ```
-
-12. Append before the end line `LABEL="modeswitch_rules_end"` the following:
-
-    ```
-    # Realtek 8211CU Wifi AC USB
-    ATTR{idVendor}=="0bda", ATTR{idProduct}=="1a2b", RUN+="/usr/sbin/usb_modeswitch -K -v 0bda -p 1a2b"
-    ```
-
-    
-
-
-
-
-
-
-

+ 1 - 1
go.mod

@@ -38,7 +38,7 @@ require (
 	github.com/valyala/fasttemplate v1.1.0
 	gitlab.com/NebulousLabs/fastrand v0.0.0-20181126182046-603482d69e40 // indirect
 	gitlab.com/NebulousLabs/go-upnp v0.0.0-20181011194642-3a71999ed0d3
-	golang.org/x/net v0.0.0-20200625001655-4c5254603344
+	golang.org/x/net v0.0.0-20200301022130-244492dfa37a
 	golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e
 	golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae // indirect
 	golang.org/x/text v0.3.3 // indirect

+ 2 - 5
go.sum

@@ -408,9 +408,8 @@ golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM=
 golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
-golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
@@ -441,9 +440,8 @@ golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa h1:F+8P+gmewFQYRk6JoLQLwjBCTu3mcIURZfNkVweuRKA=
 golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
 golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4=
-golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -472,7 +470,6 @@ golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae h1:Ih9Yo4hSPImZOpfGuA4bR/ORKTAbhZo2AbWNRCnevdo=
 golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

+ 0 - 55
remote.go

@@ -1,55 +0,0 @@
-package main
-
-import (
-	"net/http"
-	"runtime"
-
-	module "imuslab.com/arozos/mod/modules"
-	prout "imuslab.com/arozos/mod/prouter"
-	"imuslab.com/arozos/mod/wsshell"
-)
-
-/*
-	Remote.go
-	author: tobychui
-
-	This module handles the remote maintaince of the arozos system
-	Any modules that handles remote access / deployment should be placed here
-
-*/
-
-func WebsocketShellInit() {
-	//This module only avaible for administrator that has permission to this module and is admin
-	if runtime.GOOS == "windows" {
-		ttyRouter := prout.NewModuleRouter(prout.RouterOption{
-			ModuleName:  "WsTTY",
-			AdminOnly:   true,
-			UserHandler: userHandler,
-			RequireLAN:  true,
-			DeniedHandler: func(w http.ResponseWriter, r *http.Request) {
-				w.WriteHeader(http.StatusForbidden)
-				w.Write([]byte("403 - Permission Denied"))
-			},
-		})
-
-		//Create new terminal object
-		terminal := wsshell.NewWebSocketShellTerminal()
-		ttyRouter.HandleFunc("/system/tty/", terminal.HandleOpen)
-
-		//Register the module
-		moduleHandler.RegisterModule(module.ModuleInfo{
-			Name:        "WsTTY",
-			Group:       "System Tools",
-			IconPath:    "SystemAO/wstty/img/small_icon.png",
-			Version:     "1.0",
-			StartDir:    "SystemAO/wstty/console.html",
-			SupportFW:   true,
-			InitFWSize:  []int{900, 480},
-			LaunchFWDir: "SystemAO/wstty/console.html",
-			SupportEmb:  false,
-		})
-	} else {
-		//Linux. Use the WsTTY subservice instead.
-	}
-
-}

+ 3 - 4
startup.go

@@ -49,10 +49,9 @@ func RunStartup() {
 	PackagManagerInit() //Start APT service agent
 
 	//7. Kickstart the File System and Desktop
-	FileSystemInit()     //Start FileSystem
-	DesktopInit()        //Start Desktop
-	HardwarePowerInit()  //Start host power manager
-	WebsocketShellInit() //Start WebSocket tty server
+	FileSystemInit()    //Start FileSystem
+	DesktopInit()       //Start Desktop
+	HardwarePowerInit() //Start host power manager
 
 	//8 Start AGI and Subservice modules (Must start after module)
 	AGIInit()        //ArOZ Javascript Gateway Interface, must start after fs

+ 1 - 0
subservice/WsTTY/.gitignore

@@ -0,0 +1 @@
+src/*

BIN
subservice/WsTTY/WsTTY.exe


BIN
subservice/WsTTY/WsTTY_linux_amd64


BIN
subservice/WsTTY/WsTTY_linux_arm


BIN
subservice/WsTTY/WsTTY_linux_arm64


+ 1 - 1
subservice/WsTTY/build.sh

@@ -12,7 +12,7 @@ GOOS=linux GOARCH=arm64 go build
 mv "${PWD##*/}" "${PWD##*/}_linux_arm64"
 
 echo "Building windows"
-#GOOS=windows GOARCH=amd64 go build
+GOOS=windows GOARCH=amd64 go build
 
 echo "Building freebsd"
 # GOOS=freebsd GOARCH=amd64 go build

+ 2 - 0
subservice/WsTTY/go.mod

@@ -1,3 +1,5 @@
 module imuslab.com/WsTTY
 
 go 1.14
+
+require github.com/gorilla/websocket v1.4.2

+ 2 - 0
subservice/WsTTY/go.sum

@@ -0,0 +1,2 @@
+github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
+github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=

+ 78 - 45
subservice/WsTTY/main.go

@@ -3,6 +3,7 @@ package main
 import (
 	"io/ioutil"
 	"log"
+	"net/http"
 	"os"
 	"os/exec"
 	"os/signal"
@@ -12,6 +13,7 @@ import (
 	"syscall"
 
 	"imuslab.com/WsTTY/mod/aroz"
+	"imuslab.com/WsTTY/mod/wsshell"
 )
 
 var (
@@ -30,60 +32,91 @@ func SetupCloseHandler() {
 }
 
 func main() {
-	//Start the aoModule pipeline (which will parse the flags as well). Pass in the module launch information
-	handler = aroz.HandleFlagParse(aroz.ServiceInfo{
-		Name:        "WsTTY",
-		Desc:        "arozos Websocket Terminal",
-		Group:       "System Tools",
-		IconPath:    "SystemAO/wstty/img/small_icon.png",
-		Version:     "1.0",
-		StartDir:    "wstty/",
-		SupportFW:   true,
-		LaunchFWDir: "wstty/",
-		InitFWSize:  []int{740, 500},
-	})
+	//Create a platform dependent aroz service register
+	if runtime.GOOS == "windows" {
+		//Start the aoModule pipeline (which will parse the flags as well). Pass in the module launch information
+		handler = aroz.HandleFlagParse(aroz.ServiceInfo{
+			Name:        "WsTTY",
+			Desc:        "arozos Websocket Terminal",
+			Group:       "System Tools",
+			IconPath:    "wstty/img/small_icon.png",
+			Version:     "1.1",
+			StartDir:    "wstty/console.html",
+			SupportFW:   true,
+			LaunchFWDir: "wstty/console.html",
+			InitFWSize:  []int{740, 500},
+		})
+
+	} else {
+		//Start the aoModule pipeline (which will parse the flags as well). Pass in the module launch information
+		handler = aroz.HandleFlagParse(aroz.ServiceInfo{
+			Name:        "WsTTY",
+			Desc:        "arozos Websocket Terminal",
+			Group:       "System Tools",
+			IconPath:    "img/icons/wstty/small_icon.png",
+			Version:     "1.1",
+			StartDir:    "wstty/",
+			SupportFW:   true,
+			LaunchFWDir: "wstty/",
+			InitFWSize:  []int{740, 500},
+		})
+
+	}
 
 	SetupCloseHandler()
 
 	//Start the gotty and rproxy it to the main system
 
 	if runtime.GOOS == "windows" {
-		//Not supported, and this should not be compiled to windows binary
-		panic("Not supported platform")
+		//Switch to using wsshell module
+		terminal := wsshell.NewWebSocketShellTerminal()
+		http.HandleFunc("/tty/", terminal.HandleOpen)
 
-	}
+		//Register the standard web services urls
+		fs := http.FileServer(http.Dir("./web"))
+		http.Handle("/", fs)
 
-	absolutePath := ""
-	if runtime.GOARCH == "amd64" {
-		abs, _ := filepath.Abs("./gotty/gotty_linux_amd64")
-		absolutePath = abs
-	} else if runtime.GOARCH == "arm" {
-		abs, _ := filepath.Abs("./gotty/gotty_linux_arm")
-		absolutePath = abs
-	} else if runtime.GOARCH == "arm64" {
-		abs, _ := filepath.Abs("./gotty/ggotty_linux_arm64")
-		absolutePath = abs
-	} else {
-		//Unsupported platform. Default use amd64
-		abs, _ := filepath.Abs("./gotty/gotty_linux_amd64")
-		absolutePath = abs
-	}
+		//Any log println will be shown in the core system via STDOUT redirection. But not STDIN.
+		log.Println("WsTTY (Windows Compatible Mode) started. Listening on " + handler.Port)
+		err := http.ListenAndServe(handler.Port, nil)
+		if err != nil {
+			log.Fatal(err)
+		}
+	} else if runtime.GOOS == "linux" {
+		//Use wstty directly
+		absolutePath := ""
+		if runtime.GOARCH == "amd64" {
+			abs, _ := filepath.Abs("./gotty/gotty_linux_amd64")
+			absolutePath = abs
+		} else if runtime.GOARCH == "arm" {
+			abs, _ := filepath.Abs("./gotty/gotty_linux_arm")
+			absolutePath = abs
+		} else if runtime.GOARCH == "arm64" {
+			abs, _ := filepath.Abs("./gotty/ggotty_linux_arm64")
+			absolutePath = abs
+		} else {
+			//Unsupported platform. Default use amd64
+			abs, _ := filepath.Abs("./gotty/gotty_linux_amd64")
+			absolutePath = abs
+		}
+		//Extract port number from listening addr
+		tmp := strings.Split(handler.Port, ":")
+		tmp = tmp[len(tmp)-1:]
+		portOnly := strings.Join(tmp, "")
 
-	//Extract port number from listening addr
-	tmp := strings.Split(handler.Port, ":")
-	tmp = tmp[len(tmp)-1:]
-	portOnly := strings.Join(tmp, "")
-
-	log.Println("Starting WsTTY Adapter on: ", portOnly)
-
-	//Start the gotty. This shoud be blocking by itself
-	cmd := exec.Command(absolutePath, "-w", "-p", portOnly, "-a", "localhost", "--ws-origin", `\w*`, "bash", "--init-file", "bashstart")
-	cmd.Stdout = os.Stdout
-	cmd.Stderr = os.Stderr
-	if err := cmd.Start(); err != nil {
-		//Fail to start gotty. Disable this module
-		ioutil.WriteFile(".disabled", []byte(""), 0755)
-		return
+		log.Println("WsTTY Started. Listening on ", portOnly)
+
+		//Start the gotty. This shoud be blocking by itself
+		cmd := exec.Command(absolutePath, "-w", "-p", portOnly, "-a", "localhost", "--ws-origin", `\w*`, "bash", "--init-file", "bashstart")
+		cmd.Stdout = os.Stdout
+		cmd.Stderr = os.Stderr
+		if err := cmd.Start(); err != nil {
+			//Fail to start gotty. Disable this module
+			ioutil.WriteFile(".disabled", []byte(""), 0755)
+			return
+		}
+	} else {
+		panic("Not supported platform: " + runtime.GOOS)
 	}
 
 }

+ 171 - 0
subservice/WsTTY/mod/wsshell/common.go

@@ -0,0 +1,171 @@
+package wsshell
+
+import (
+	"bufio"
+	"encoding/base64"
+	"errors"
+	"io/ioutil"
+	"log"
+	"net/http"
+	"os"
+	"time"
+)
+
+/*
+	SYSTEM COMMON FUNCTIONS
+
+	This is a system function that put those we usually use function but not belongs to
+	any module / system.
+
+	E.g. fileExists / IsDir etc
+
+*/
+
+/*
+	Basic Response Functions
+
+	Send response with ease
+*/
+//Send text response with given w and message as string
+func sendTextResponse(w http.ResponseWriter, msg string) {
+	w.Write([]byte(msg))
+}
+
+//Send JSON response, with an extra json header
+func sendJSONResponse(w http.ResponseWriter, json string) {
+	w.Header().Set("Content-Type", "application/json")
+	w.Write([]byte(json))
+}
+
+func sendErrorResponse(w http.ResponseWriter, errMsg string) {
+	w.Header().Set("Content-Type", "application/json")
+	w.Write([]byte("{\"error\":\"" + errMsg + "\"}"))
+}
+
+func sendOK(w http.ResponseWriter) {
+	w.Header().Set("Content-Type", "application/json")
+	w.Write([]byte("\"OK\""))
+}
+
+/*
+	The paramter move function (mv)
+
+	You can find similar things in the PHP version of ArOZ Online Beta. You need to pass in
+	r (HTTP Request Object)
+	getParamter (string, aka $_GET['This string])
+
+	Will return
+	Paramter string (if any)
+	Error (if error)
+
+*/
+func mv(r *http.Request, getParamter string, postMode bool) (string, error) {
+	if postMode == false {
+		//Access the paramter via GET
+		keys, ok := r.URL.Query()[getParamter]
+
+		if !ok || len(keys[0]) < 1 {
+			//log.Println("Url Param " + getParamter +" is missing")
+			return "", errors.New("GET paramter " + getParamter + " not found or it is empty")
+		}
+
+		// Query()["key"] will return an array of items,
+		// we only want the single item.
+		key := keys[0]
+		return string(key), nil
+	} else {
+		//Access the parameter via POST
+		r.ParseForm()
+		x := r.Form.Get(getParamter)
+		if len(x) == 0 || x == "" {
+			return "", errors.New("POST paramter " + getParamter + " not found or it is empty")
+		}
+		return string(x), nil
+	}
+
+}
+
+func stringInSlice(a string, list []string) bool {
+	for _, b := range list {
+		if b == a {
+			return true
+		}
+	}
+	return false
+}
+
+func fileExists(filename string) bool {
+	_, err := os.Stat(filename)
+	if os.IsNotExist(err) {
+		return false
+	}
+	return true
+}
+
+func isDir(path string) bool {
+	if fileExists(path) == false {
+		return false
+	}
+	fi, err := os.Stat(path)
+	if err != nil {
+		log.Fatal(err)
+		return false
+	}
+	switch mode := fi.Mode(); {
+	case mode.IsDir():
+		return true
+	case mode.IsRegular():
+		return false
+	}
+	return false
+}
+
+func inArray(arr []string, str string) bool {
+	for _, a := range arr {
+		if a == str {
+			return true
+		}
+	}
+	return false
+}
+
+func timeToString(targetTime time.Time) string {
+	return targetTime.Format("2006-01-02 15:04:05")
+}
+
+func loadImageAsBase64(filepath string) (string, error) {
+	if !fileExists(filepath) {
+		return "", errors.New("File not exists")
+	}
+	f, _ := os.Open(filepath)
+	reader := bufio.NewReader(f)
+	content, _ := ioutil.ReadAll(reader)
+	encoded := base64.StdEncoding.EncodeToString(content)
+	return string(encoded), nil
+}
+
+func pushToSliceIfNotExist(slice []string, newItem string) []string {
+	itemExists := false
+	for _, item := range slice {
+		if item == newItem {
+			itemExists = true
+		}
+	}
+
+	if !itemExists {
+		slice = append(slice, newItem)
+	}
+
+	return slice
+}
+
+func removeFromSliceIfExists(slice []string, target string) []string {
+	newSlice := []string{}
+	for _, item := range slice {
+		if item != target {
+			newSlice = append(newSlice, item)
+		}
+	}
+
+	return newSlice
+}

+ 25 - 0
subservice/WsTTY/mod/wsshell/splitter.go

@@ -0,0 +1,25 @@
+package wsshell
+
+import "strings"
+
+func customSplitter(data []byte, atEOF bool) (advance int, token []byte, err error) {
+	// Return nothing if at end of file and no data passed
+	if atEOF && len(data) == 0 {
+		return 0, nil, nil
+	}
+
+	i := strings.Index(string(data), "\n")
+	j := strings.Index(string(data), "\r")
+	if i >= 0 {
+		return i + 1, data[0:i], nil
+	} else if j >= 0 {
+		return j + 1, data[0:j], nil
+	}
+
+	// If at end of file with data return the data
+	if atEOF {
+		return len(data), data, nil
+	}
+
+	return
+}

+ 229 - 0
subservice/WsTTY/mod/wsshell/wsshell.go

@@ -0,0 +1,229 @@
+package wsshell
+
+import (
+	"bufio"
+	"io"
+	"io/ioutil"
+	"log"
+	"net/http"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"regexp"
+	"runtime"
+	"strings"
+	"time"
+
+	"github.com/gorilla/websocket"
+)
+
+/*
+	Bash Module
+	author: tobychui
+
+	This module handles the connection of bash terminal to websocket interface
+
+*/
+var upgrader = websocket.Upgrader{
+	ReadBufferSize:  1024,
+	WriteBufferSize: 1024,
+}
+
+type Terminal struct {
+	cwd string //Current Working Directory
+}
+
+func NewWebSocketShellTerminal() *Terminal {
+	baseCWD, _ := filepath.Abs("./")
+	return &Terminal{
+		cwd: baseCWD,
+	}
+}
+
+func (t *Terminal) HandleOpen(w http.ResponseWriter, r *http.Request) {
+	//Upgrade the connection to WebSocket connection
+	c, err := upgrader.Upgrade(w, r, nil)
+	if err != nil {
+		log.Println(err)
+		w.WriteHeader(http.StatusInternalServerError)
+		w.Write([]byte("500 - Websocket Upgrade Failed"))
+		return
+	}
+
+	//Check if the system is running on windows or linux. Use cmd and bash
+	var cmd *exec.Cmd
+	if runtime.GOOS == "windows" {
+		cmd = exec.Command("cmd")
+	} else if runtime.GOOS == "linux" {
+		cmd = exec.Command("/bin/bash")
+	} else {
+		//Currently not supported.
+		c.WriteMessage(1, []byte("[ERROR] Host Platform not supported: " + runtime.GOOS))
+		w.WriteHeader(http.StatusInternalServerError)
+		w.Write([]byte("500 - Host OS Not supported"))
+		return
+	}
+
+	if cmd == nil {
+		w.WriteHeader(http.StatusInternalServerError)
+		w.Write([]byte("500 -Internal Server Error"))
+		return
+	}
+
+	//Create pipe to all interfaces: STDIN, OUT AND ERR
+
+	stdout, err := cmd.StdoutPipe()
+	if err != nil {
+		log.Println(err)
+		return
+	}
+
+	//Pipe stderr to stdout
+	cmd.Stderr = cmd.Stdout
+
+	/*
+		stderr, err := cmd.StderrPipe()
+		if err != nil {
+			log.Println(err)
+			return
+		}
+	*/
+
+	stdin, err := cmd.StdinPipe()
+	if err != nil {
+		log.Println(err)
+		return
+	}
+
+	//Start the shell
+	if err := cmd.Start(); err != nil {
+		log.Println(err)
+		return
+	}
+
+	//Start listening
+	go func() {
+		//s := bufio.NewScanner(io.MultiReader(stdout, stderr))
+		s := bufio.NewScanner(io.MultiReader(stdout))
+		s.Split(customSplitter)
+		for s.Scan() {
+			resp := s.Bytes()
+
+			respstring := string(resp)
+			if runtime.GOOS == "windows" {
+				//Strip out all non ASCII characters
+				re := regexp.MustCompile("[[:^ascii:]]")
+				respstring = re.ReplaceAllLiteralString(string(resp), "?")
+
+			} else if runtime.GOOS == "linux" {
+				//Linux. Check if this is an internal test command.
+				if len(respstring) > 12 && respstring[:12] == "<arozos_pwd>" {
+					//This is an internal pwd update command
+					t.cwd = strings.TrimSpace(respstring[12:])
+					log.Println("Updating cwd: ", t.cwd)
+					continue
+				}
+			}
+
+			err := c.WriteMessage(1, []byte(respstring))
+			//log.Println(string(resp))
+			if err != nil {
+				//Fail to write websocket. (Already disconencted?) Terminate the bash
+				//log.Println(err.Error())
+				cmd.Process.Kill()
+			}
+		}
+	}()
+
+	//Do platform depending stuffs
+	if runtime.GOOS == "windows" {
+		//Force codepage to be english
+		io.WriteString(stdin, "chcp 65001\n")
+
+	} else if runtime.GOOS == "linux" {
+		//Send message of the day
+		content, err := ioutil.ReadFile("/etc/motd")
+		if err != nil {
+			//Unable to read the motd, use the arozos default one
+			c.WriteMessage(1, []byte("Terminal Connected. Start type something!"))
+		} else {
+			c.WriteMessage(1, content)
+		}
+	}
+
+	//Start looping for inputs
+	for {
+		_, message, err := c.ReadMessage()
+		if err != nil {
+			//Something went wrong. Close the socket and kill the process
+			cmd.Process.Kill()
+			c.Close()
+			return
+		}
+
+		//Check if the message is exit. If yes, terminate the section
+		if strings.TrimSpace(string(message)) == "exit" {
+			//Terminate the execution
+			cmd.Process.Kill()
+
+			//Exit listening loop
+			break
+		} else if strings.TrimSpace(string(message)) == "\x003" {
+			log.Println("WSSHELL SIGKILL RECEIVED")
+			if runtime.GOOS == "windows" {
+				//Send kill signal, see if it kill itself
+				_ = cmd.Process.Signal(os.Kill)
+
+				//Nope, just forcefully kill it
+				err := cmd.Process.Kill()
+				if err != nil {
+					c.WriteMessage(1, []byte("[Error] "+err.Error()))
+				}
+			} else if runtime.GOOS == "linux" {
+				//Do it nicely
+				go func() {
+					time.Sleep(2 * time.Second)
+					_ = cmd.Process.Signal(os.Kill)
+				}()
+				cmd.Process.Signal(os.Interrupt)
+			}
+
+		} else {
+			//Push the input valie into the shell with a newline at the last position of the line
+			if len(string(message)) > 0 && string(message)[len(message)-1:] != "\n" {
+				message = []byte(string(message) + "\n")
+			} else if len(string(message)) == 0 {
+
+				continue
+			}
+			//Write to STDIN
+			io.WriteString(stdin, string(message)+"\n")
+
+			if runtime.GOOS == "linux" {
+				//Reply what user has typed in on linux
+				hostname, err := os.Hostname()
+				if err != nil {
+					hostname = "arozos"
+				}
+
+				if len(string(message)) > 2 && string(message)[:2] == "cd" {
+					//Request an update to the pwd
+					time.Sleep(300 * time.Millisecond)
+					io.WriteString(stdin, `echo "<arozos_pwd>$PWD"`+"\n")
+					time.Sleep(300 * time.Millisecond)
+				}
+
+				c.WriteMessage(1, []byte(hostname+":"+t.cwd+" & "+strings.TrimSpace(string(message))))
+			}
+		}
+
+	}
+
+	c.WriteMessage(1, []byte("Exiting session"))
+	c.Close()
+
+}
+
+func (t *Terminal) Close() {
+	//Nothing needed to be done
+}

+ 1 - 1
web/SystemAO/wstty/console.html → subservice/WsTTY/web/console.html

@@ -77,7 +77,7 @@
 
                 writeToScreen("[info] Conencting to arozos WsTTY service...")
                 //Create WebSocket object
-                ShellWs = new WebSocket(protocol + window.location.hostname + ":" + window.location.port + "/system/tty/");
+                ShellWs = new WebSocket(protocol + window.location.hostname + ":" + window.location.port + "/wstty/tty/");
             
                 //Hook events
                 ShellWs.onopen = function(e) {

+ 0 - 0
web/SystemAO/wstty/img/desktop_icon.png → subservice/WsTTY/web/img/desktop_icon.png


+ 0 - 0
web/SystemAO/wstty/img/desktop_icon.psd → subservice/WsTTY/web/img/desktop_icon.psd


+ 0 - 0
web/SystemAO/wstty/img/small_icon.png → subservice/WsTTY/web/img/small_icon.png


+ 0 - 34
system.time.go.disabled

@@ -1,34 +0,0 @@
-package main
-
-import (
-	//"log"
-
-	"net/http"
-
-	prout "imuslab.com/arozos/mod/prouter"
-	"imuslab.com/arozos/mod/time/timezone"
-)
-
-func system_time_init() {
-	//Create a user handler
-	router := prout.NewModuleRouter(prout.RouterOption{
-		ModuleName:  "System Setting",
-		AdminOnly:   false,
-		UserHandler: userHandler,
-		DeniedHandler: func(w http.ResponseWriter, r *http.Request) {
-			sendErrorResponse(w, "Permission Denied")
-		},
-	})
-
-	//Register the (usless?) System time module
-	registerSetting(settingModule{
-		Name:     "System Time",
-		Desc:     "Current Time in the System Host",
-		IconPath: "SystemAO/disk/smart/img/small_3icon.png",
-		Group:    "Time",
-		StartDir: "SystemAO/time/currenttime.html",
-	})
-
-	router.HandleFunc("/system/time/getTime", timezone.ShowTime)
-
-}

+ 8 - 1
web/Music/embedded.html

@@ -473,12 +473,19 @@
 			updatePlayingSongSelection();
 		}
 		
-		
+		function blobToDataURL(blob, callback) {
+			var a = new FileReader();
+			a.onload = function(e) {callback(e.target.result);}
+			a.readAsDataURL(blob);
+		}
+
 		function loadSongFromSongInfo(playSongInfo){
 			var songName = ao_module_codec.decodeUmFilename(playSongInfo[0]);
 			var songPath = filterExternalStoragePath(playSongInfo[1]);
 			var fileSize = playSongInfo[3];
+
 			$(player).attr('src',"/media?file=" + encodeURIComponent(songPath));
+			
 			ao_module_setWindowTitle(songName);
 			songInfo = playSongInfo;
 			updateDisplayInformation(ao_module_codec.decodeUmFilename(songInfo[0]),songInfo[3]);

+ 28 - 364
web/SystemAO/iot/hub/index.html

@@ -66,15 +66,9 @@
         	<a class="selectable item" onClick="scanDevices();hideSideMenu();">
                 <i class="search icon"></i> Scan Devices
             </a>
-        	<a class="selectable item" onClick="manualDriverConfig();">
-                <i class="edit icon"></i> Manual Device Config
-            </a>
-
-        	<a class="selectable item">
-                <i class="object group icon"></i> Create Action Group
-            </a>
+			
         	<div class="bottom item">
-                CopyRight ArOZ Online Project 2021
+                CopyRight ArozOS Project 2021
             </div>
     </div>
 
@@ -183,41 +177,18 @@ var currentlyViewingDevices = "";
 var uselocal = false; //Use Local as command sender or use Host as command sender
 var username = $("#data_session_username").text().trim();
 //ao_module Float Window functions
-ao_module_setWindowIcon("home");
-ao_module_setWindowTitle("Home Dynamic Panel");
-ao_module_setGlassEffectMode();
+ao_module_setWindowTitle("IoT Hub");
 ao_module_setWindowSize(465,730,true);
 if (!ao_module_virtualDesktop){
     $("body").css("background-color","white");
 }
 
-var localSetting = ao_module_getStorage("hds","local");
-if ( localSetting !== undefined && localSetting !== null && localSetting != ""){
-    uselocal = (localSetting == "true");
-}
-
-if (uselocal){
-    $('#outdoor').prop('checked', false);
-}else{
-    $('#outdoor').prop('checked', true);
-}
-
 //Initiate the page content
 loadDevList();
 
-$('#outdoor').change(function() {
-    if(this.checked){
-        ao_module_saveStorage("hds","local","false");
-        uselocal = false;
-    }else{
-        ao_module_saveStorage("hds","local","true");
-        uselocal = true;
-    }
-});
     
 function inputbox(message, placeholder = ""){
     var input = prompt(message, placeholder);
-
     if (input != null) {
       return input;
     }else{
@@ -225,71 +196,11 @@ function inputbox(message, placeholder = ""){
     }
 }
 
-function addDevViaIP(){
-    var ipaddr = inputbox("Please enter the IP Address of your device.");
-    if (ipaddr != false){
-         var classType = inputbox("Select the custom driver for this device. Leave empty for default.");
-         if (classType == false){
-             alert("Driver Type cannot be empty!");
-             return;
-         }
-        $.get("manualDriverConfig.php?ipaddr=" + ipaddr + "&classType=" + classType,function(data){
-            //Finished the adding process. Realod the list of custom devices.
-            loadCustomDeviceList();
-        });
-    }else{
-        //User cancelled the opr
-    }
-   
-}
-
-function openFolderForDev(){
-    if (ao_module_virtualDesktop){
-        ao_module_openPath("SystemAOB/system/iotpipe/devices/fixed");
-    }else{
-        window.open('../SystemAOB/functions/file_system/index.php?controlLv=2&subdir=SystemAOB/system/iotpipe/devices/fixed');
-    }
-}
-
-function loadCustomDeviceList(){
-    $("#customDevList").html("");
-    $.ajax("manualDriverConfig.php").done(function(data){
-        if (data.length == 0){
-            $("#customDevList").append('<div class="item">N/A</div>');
-        }else{
-            for (var i =0; i < data.length; i++){
-                $("#customDevList").append('<div class="item">' + data[i][1] + " <br>( Config UID: " + data[i][0] + " / Driver Loader: " + data[i][2] +  ")" + '</div>');
-            }
-        }
-    });
-}
-
-function manualDriverConfig(){
-    //Open manual driver configuration interface
-    loadCustomDeviceList();
-    $("#manualDevConfig").show();
-    hideSideMenu();
-}
 
 function scanDevices(){
-    $("#loadingMask").show();
-    $.ajax("../SystemAOB/system/iotpipe/scandev.php").done(function(data){
-        if (data.includes("ERROR")){
-            alert("Scan Error! See console.log for more information.");
-            console.log(data);
-        }else{
-            loadDevList();
-        }
-        $("#loadingMask").hide();
-    });
     
 }
 
-function setSelectNickname(object){
-	currentlyViewingDevices = $(object).attr("uuid");
-	setNickname();
-}
-
 
 function hideSideMenu(){
 	ts('.right.sidebar').sidebar('hide');
@@ -321,282 +232,35 @@ function showMore(object){
 }
 
 
-function action(object){
-	var classType = $(object).parent().parent().attr("classtype");
-	var driverFound = ($(object).parent().parent().attr("driverfound") == "true");
-	var ip = $(object).parent().parent().attr("devip");
-	if (driverFound){
-		$("#actionInterface").fadeIn('fast');
-		updateIframeSize();
-		if ($(object).parent().parent().attr("location") == "remote"){
-		    	$("#controlUI").attr("src","../SystemAOB/system/iotpipe/drivers/" + classType + "/" + classType + ".php?ip=" + ip + "&location=remote");
-		}else{
-		    	$("#controlUI").attr("src","../SystemAOB/system/iotpipe/drivers/" + classType + "/" + classType + ".php?ip=" + ip);
-		}
-	
-	}else{
-		alert("Driver not found!");
-	}
-}
 
 function loadDevList(){
 	$("#devList").html("");
-	var template = '<div class="ts segment HDSDev" devIp="{deviceIP}" location="local">\
-							<div class="ts grid">\
-								<div class="four wide column"><img class="ts tiny devIcon image" src="img/system/loading.gif"></div>\
-								<div class="twelve wide column">\
-									<div class="ts container">\
-										<div class="ts header">\
-											<span class="devHeader">{deviceIP}</span>\
-											<div class="sub devProperty header"><i class="spinner loading icon"></i> Loading</div>\
-										</div>\
-									</div>\
-								</div>\
-							</div>\
-							<div class="controlBtn infoMount">\
-								<button class="ts icon button" onClick="showMore(this);"><i class="notice icon"></i></button>\
-								<button class="ts primary icon button" onClick="action(this);"><i class="external icon"></i></button>\
-							</div>\
-						</div>';	
-	$.ajax("loadDevList.php").done(function(data){
-		if (data.length == 0){
-			var nodevFound = '<div class="ts segment">\
-			<h5 class="ts center aligned icon header">\
-				<i class="remove icon"></i>No Device Found\
-					<div class="sub header">No HDS based device is found in your network.<br>\
-					Click <a href="readmore.html">here</a> to know more on how to build one yourself.</div>\
-				</h5>\
-			</div>';
-			$("#devList").append(nodevFound);
-		}else{
-			for (var i =0; i < data.length; i++){
-				var ip = data[i];
-				var box = template;
-				box = box.split("{deviceIP}").join(ip);
-				$("#devList").append(box);
-			}
-		}
-		
-		//All devices loaded. Get information about the devices.
-		$(".HDSDev").each(function(){
-			let ip = $(this).attr("devIp");
-			requestInfo(ip,"info",this,uselocal);
-			requestUUID(ip,"uuid",this,uselocal);
-		});
-	});
-	$.ajax("manualDriverConfig.php").done(function(data){
-	    for(var i =0; i < data.length; i++){
-	        var uuid = data[i][0];
-	        var ipaddr = data[i][1];
-	        var classType = data[i][2];
-	        var box = template;
-	        box = box.split("{deviceIP}").join(ipaddr);
-	        box = $(box).attr("uuid",uuid);
-	        box = $(box).attr("classtype",classType);
-	        box = $(box).attr("classname",classType.split(".").join(" "));
-	        box = $(box).removeClass("HDSDev").addClass("CustomDev");
-	        box = $(box).attr("location","fixed");
-	        $("#devList").append(box);
-	    }
-	    initCustomDevUI();
-	});
-}
-
-function initCustomDevUI(){
-    $(".CustomDev").each(function(){
-        var classType = $(this).attr("classtype");
-        loadDevImage(classType,this);
-        loadDevDefaultDescription(this);
-        getNickName(this);
-    });
-}
-
-function loadDevDefaultDescription(object){
-    var classType = $(object).attr("classtype");
-    $.ajax("loadDriverProperties.php?classType=" + classType).done(function(data){
-        $(object).find(".devProperty").text(data);
-    });
-}
-
-
-function requestUUID(ip,subpath, object,local){
-    if (local){
-        //use local device as controller
-        $.ajax({
-        url: "http://" + ip + "/" + subpath,
-        error: function(){
-            //Declare offline
-    		
-        },
-        success: function(data){
-            //UUID found.
-    		var uuid = data;
-    		$(object).attr("uuid",uuid);
-    		$(object).find(".devHeader").text(uuid);
-    		$(object).attr('location',"local");
-    		getNickName(object);
-        },
-        timeout: 5000 // sets timeout to 3 seconds
-    	});
-    }else{
-        //use host server as controller
-         $.ajax({
-	        url:"../SystemAOB/system/iotpipe/extreq.php?ipa=" + ip + "&subpath=" + subpath,
-	        error: function(){
-	            //This devices might be not on the server side. Try again with client side request
-	            requestUUID(ip,subpath, object,false);
-	        },
-	         success: function(data){
-	             let filename = data;
-	             let thisObject = object;
-	             setTimeout(function(){
-	                 tryGetUUID(filename,ip,subpath,thisObject,local);
-	             },1300);
-	             
-	         }
-         }
-	    );
-    }
-	
-}
-
-function tryGetUUID(filename,ip,subpath, object,local,retryCount=0){
-    if (retryCount > 10){
-        //Assume offline
-        return;
-    }
-    $.get("../SystemAOB/system/iotpipe/extreq.php?getreq=" + filename,function(data){
-        if (data.includes("ERROR")){
-            retryCount++;
-            setTimeout(function(){
-                tryGetUUID(filename,ip,subpath, object,local,retryCount);
-            },1300);
-        }else{
-            var uuid = data;
-    		$(object).attr("uuid",uuid);
-    		$(object).attr('location',"remote");
-    		$(object).find(".devHeader").text(uuid);
-    		getNickName(object);
-        }
-     
-     });
-}
-
-function getNickName(object){
-	$.ajax({
-    url: "nicknameman.php?uuid=" + $(object).attr("uuid"),
-    success: function(data){
-        //UUID found.
-		if (data != false){
-			//Replace the uuid with nickname
-			$(object).find(".devHeader").text(data);
-			$(object).attr("nickname",data);
-		}
-    },
-    timeout: 5000 // sets timeout to 3 seconds
-	});
-}
-
-function requestInfo(ip,subpath,object,local=true){
-	//This function should work if both devices are in the same subnet. If not, something else will be done.
-	if (local){
-	    $.ajax({
-        url: "http://" + ip + "/" + subpath,
-        error: function(){
-            //Declare offline
-    		$(object).attr("className","offline");
-    		$(object).attr("classType","offline");
-    		$(object).find(".devHeader").html("<i class='remove icon'></i> Unable to Connect");
-    		$(object).find(".devProperty").html("This device is offline or its address has been changed.");
-    		$(object).find(".devIcon").attr('src',"img/system/unable2connect.png");
-        },
-        success: function(data){
-            //Device in the same subnet. Try to load driver.
-    		if (data.includes("_")){
-    			var className = data.split("_")[0];
-    			var classType = data.split("_")[1];
-    			$(object).attr("className",className);
-    			$(object).attr("classType",classType);
-    			$(object).find(".devProperty").html(className);
-    			loadDevImage(classType,object);
-    		}else{
-    			console.log("[Homdynm] Error. Unknown devices class for ip address: " + $(object).attr("devIP"));
-    		}
-    		
-        },
-        timeout: 5000 // sets timeout to 3 seconds
-    	});
-	}else{
-	    //use server to get the required information.
-	    $.ajax({
-	        url:"../SystemAOB/system/iotpipe/extreq.php?ipa=" + ip + "&subpath=" + subpath,
-	        error: function(){
-	            //This devices might be not on the server side. Try again with client side request
-	            requestInfo(ip,subpath,object,true);
-	        },
-	        success: function(data){
-	            //The data should be a filename for getreq. Get it after 2 sec
-	            let filename = data;
-	            let thisip = ip;
-	            let thisObject = object;
-	            setTimeout(function(){
-    	                tryGetReqInfo(filename,ip,subpath,thisObject,false);
-	            },1000);
-	            
-	        },
-	        timeout: 5000 // sets timeout to 3 seconds
-	    })
-	}
-	
-}
-
-function tryGetReqInfo(filename,ip,subpath,thisObject,local,retryCount = 0){
-    if (retryCount > 10){
-        //Assume offline
-    	$(thisObject).attr("className","offline");
-		$(thisObject).attr("classType","offline");
-		$(thisObject).find(".devHeader").html("<i class='remove icon'></i> Unable to Connect");
-		$(thisObject).find(".devProperty").html("This device is offline or its address has been changed.");
-		$(thisObject).find(".devIcon").attr('src',"img/system/unable2connect.png");
-        return;
-    }
-    
-    $.get("../SystemAOB/system/iotpipe/extreq.php?getreq=" + filename,function(data){
-        console.log("[Home Dynamic] Remote returned value: " + ip + " " + data);
-        if (data.includes("ERROR")){
-            retryCount++;
-            setTimeout(function(){
-                tryGetReqInfo(filename,ip,subpath,thisObject,local,retryCount)
-            },1000);
-            
-        }else{
-            //Device found on server side. Get the information.
-            if (data.includes(".") && data.includes("_")){
-                //Found! Do something here
-                	var className = data.split("_")[0];
-        			var classType = data.split("_")[1];
-        			$(thisObject).attr("className",className);
-        			$(thisObject).attr("classType",classType);
-        			$(thisObject).find(".devProperty").html(className);
-        			loadDevImage(classType,thisObject);
-            }else{
-                //Might be trash from a random web server. Just ignore them.
-            }
-           
-            
-        }
-    });
-}
-
-function loadDevImage(classType,object){
-	$.ajax("loadDevImage.php?driverClass=" + classType).done(function(data){
-		$(object).find(".devIcon").attr('src',data[0]);
-		$(object).attr("driverFound",data[1]);
-		if (data[1] == false){
-			//Driver not found. Update the icon
-			$(object).find(".devIcon").attr('src',"img/system/driverNotFound.png");
+	$.get("../../../system/iot/list", function(data){
+		 if (data.error !== undefined){
+			 alert(data.error);
+		 }else{
+			data.forEach(device => {
+				deviceData = encodeURIComponent(JSON.stringify(device));
+				$("#devList").append(`<div class="ts segment HDSDev" devicedata="${deviceData}" uuid="${device.DeviceUUID}" devIp="${device.IPAddr}" port="${device.Port}" location="local">
+					<div class="ts grid">
+						<div class="four wide column"><img class="ts tiny devIcon image" src="img/system/loading.gif"></div>
+						<div class="twelve wide column">
+							<div class="ts container">
+								<div class="ts header">
+									<span class="devHeader">${device.Name}</span>
+									<div class="sub devProperty header">${device.Model}</div>
+								</div>
+							</div>
+						</div>
+					</div>
+					<div class="controlBtn infoMount">
+						<button class="ts icon button" onClick="showMore(this);"><i class="notice icon"></i></button>
+						<button class="ts primary icon button" onClick="action(this);"><i class="external icon"></i></button>
+					</div>
+				</div>`);
+			});
 			
-		}
+		 }
 	});
 }
 

BIN
web/img/icons/wstty/desktop_icon.png


BIN
web/img/icons/wstty/desktop_icon.psd


BIN
web/img/icons/wstty/small_icon.png