diff --git a/README.md b/README.md
index b996369..4948f8e 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@ This program allows you to use terminal in the browser. Simply run the program a
This repository contains a recorded session in the ```assets/extra``` directory ([M1NXZvHdvA8vSCKp_61e5d60f.rec](extra/M1NXZvHdvA8vSCKp_61e5d60f.rec)) that shows me upgrading pihole. Just put the file under the ```records``` directory, run the server, you should find the recording in the ```Recorded Session``` tab.
-3. More features are planned, including user authentication. Suggestions are welcome.
+3. More features are planned, suggestions are welcome.
You can use WiTTY to run any command line programs, such as ```bash```, ```htop```, ```vi```, ```ssh```. This following screenshot shows that three interactive session running ```zsh``` on macOS Monterey.
@@ -32,11 +32,7 @@ gifsicle -O3 .\output.gif -o replay.gif
-->
-To use the program, you need to provide a TLS cert. You can request a free [Let's Encrypt](https://letsencrypt.org/) cert or use a self-signed cert. The program currently does not support user authentication. Therefore, do not run it in untrusted networks or leave it running. A probably safe use of the program is to run ```ssh```. Please ensure that you do not automatically login to the ssh server (e.g., via key authentication).
-
-__AGAIN, Do NOT run this in an untrusted network. You will expose your
-shell to anyone that can access your network and Do NOT leave
-the server running.__
+To use the program, you need to provide a TLS cert. You can request a free [Let's Encrypt](https://letsencrypt.org/) cert or use a self-signed cert. By default, WiTTY authenticate users with username and password. You can add a new user using ```witty adduser ```, and delete an existing user with ```witty deluser ```. It is also possible to disable user authentication with ```-n/-naked``` to the run command. For example, ```witty run -n zsh``` will run zsh without user authentication.
This program is written in the [go programming language](https://go.dev/), using the
[Gin web framework](https://github.com/gin-gonic/gin), [gorilla/websocket](https://github.com/gorilla/websocket), [pty](https://github.com/creack/pty), and the wonderful [xterm.js](https://xtermjs.org/)!
@@ -66,13 +62,21 @@ Most icons were provided by [fontawesome](https://fontawesome.com/) under this [
```go build .```
-5. Start the server and give it the command to run. The server listens on 8080, for example:
+5. Add a user to the user accounts, follow the instructions on screen to provide the password
- ```./witty htop``` or
+ ```./witty adduser ```
- ```./witty ssh -l ```
+6. Start the server and give it the command to run. The server listens on 8080, for example:
+
+ ```./witty run htop``` or
-6. Connect to the server, for example
+ ```./witty run ssh -l ```
+
+ If so desired, you can disable user authenticate with ```-n/-naked```, (not recommended) for example:
+
+ ```./witty run -naked htop```
+
+7. Connect to the server, for example
```https://:8080```
diff --git a/assets/template/index.html b/assets/template/index.html
index fa999a2..da753a6 100644
--- a/assets/template/index.html
+++ b/assets/template/index.html
@@ -32,7 +32,7 @@
target="_blank" role="button">
New Session
-
+
Logout
diff --git a/main.go b/main.go
index f196052..18b5c41 100644
--- a/main.go
+++ b/main.go
@@ -1,6 +1,8 @@
package main
import (
+ "flag"
+ "fmt"
"log"
"os"
@@ -15,17 +17,48 @@ func main() {
log.SetOutput(fp)
}
- // parse the arguments. User can pass the command to execute
- // by default, we use bash, but macos users might want to use zsh
- // you can also run single program, such as pstree, htop...
- // but program might misbehave (htop seems to be fine)
- var cmdToExec = []string{"bash"}
- args := os.Args
-
- if len(args) > 1 {
- cmdToExec = args[1:]
- log.Println(cmdToExec)
+ if len(os.Args) < 2 {
+ fmt.Println("witty (adduser|deluser|run)")
+ return
+ }
+
+ var naked bool
+ runCmd := flag.NewFlagSet("run", flag.ExitOnError)
+ runCmd.BoolVar(&naked, "n", false, "Run WiTTY without user authentication")
+ runCmd.BoolVar(&naked, "naked", false, "Run WiTTY without user authentication")
+
+ switch os.Args[1] {
+ case "adduser":
+ if len(os.Args) != 3 {
+ fmt.Println("witty adduser ")
+ return
+ }
+ web.AddUser(os.Args[2])
+
+ case "deluser":
+ if len(os.Args) != 3 {
+ fmt.Println("witty deluser ")
+ return
+ }
+ web.DelUser(os.Args[2])
+
+ case "run":
+ runCmd.Parse(os.Args[2:])
+
+ var cmdToExec []string
+
+ args := runCmd.Args()
+ if len(args) > 0 {
+ cmdToExec = args
+ } else {
+ cmdToExec = []string{"bash"}
+ }
+
+ web.StartWeb(fp, cmdToExec, naked)
+
+ default:
+ fmt.Println("witty (adduser|deluser|run)")
+ return
}
- web.StartWeb(fp, cmdToExec)
}
diff --git a/web/auth.go b/web/auth.go
index d3fad53..1affae8 100644
--- a/web/auth.go
+++ b/web/auth.go
@@ -33,7 +33,7 @@ func login(c *gin.Context) {
}
// Check for username and password match, usually from a database
- if username != "hello" || passwd != "world" {
+ if !ValidateUser([]byte(username), []byte(passwd)) {
leftLoginMsg(c, "Username/password does not match")
c.Redirect(http.StatusSeeOther, "/login")
return
diff --git a/web/interactive.go b/web/interactive.go
index 2b8962b..f8c23e4 100644
--- a/web/interactive.go
+++ b/web/interactive.go
@@ -28,8 +28,13 @@ func collectSessions(c *gin.Context, cmd string) (players []InteractiveSession)
func indexPage(c *gin.Context) {
host = &c.Request.Host
+ var disabled = ""
- c.HTML(http.StatusOK, "index.html", gin.H{})
+ if noAuth {
+ disabled = "disabled"
+ }
+
+ c.HTML(http.StatusOK, "index.html", gin.H{"disabled": disabled})
}
func updateIndex(c *gin.Context) {
diff --git a/web/routing.go b/web/routing.go
index 831f044..ae9874b 100644
--- a/web/routing.go
+++ b/web/routing.go
@@ -14,6 +14,7 @@ import (
var host *string = nil
var cmdToExec []string
+var noAuth bool
// simple function to check origin
func checkOrigin(r *http.Request) bool {
@@ -31,8 +32,9 @@ func checkOrigin(r *http.Request) bool {
return (host != nil) && (*host == h.Host)
}
-func StartWeb(fp *os.File, cmd []string) {
+func StartWeb(fp *os.File, cmd []string, naked bool) {
cmdToExec = cmd
+ noAuth = naked
if fp != nil {
gin.DefaultWriter = fp
@@ -55,7 +57,11 @@ func StartWeb(fp *os.File, cmd []string) {
rt.GET("/login", loginPage)
rt.POST("/login", login)
- g1 := rt.Group("/", AuthRequired)
+ g1 := rt.Group("/")
+
+ if !naked {
+ g1.Use(AuthRequired)
+ }
// Fill in the index page
g1.GET("/", indexPage)
diff --git a/web/user.go b/web/user.go
new file mode 100644
index 0000000..8a1ab41
--- /dev/null
+++ b/web/user.go
@@ -0,0 +1,152 @@
+package web
+
+import (
+ "bytes"
+ "crypto/sha256"
+ "encoding/json"
+ "fmt"
+ "os"
+
+ "github.com/dchest/uniuri"
+ "golang.org/x/term"
+)
+
+const (
+ userFileName = "user.db"
+)
+
+type UserRecord struct {
+ User []byte `json:"Username"`
+ Seed []byte `json:"Seed"`
+ Passwd [32]byte `json:"Password"`
+}
+
+func hashPassword(seed []byte, passwd []byte) [32]byte {
+ input := append(seed, passwd...)
+ return sha256.Sum256(input)
+}
+
+func addUser(username []byte, passwd []byte) {
+ var users []UserRecord
+ var err error
+
+ seed := []byte(uniuri.NewLen(64))
+ hashed := hashPassword(seed, passwd)
+
+ exist := false
+ file, err := os.ReadFile(userFileName)
+
+ if (err == nil) && (json.Unmarshal(file, users) == nil) {
+ // update the existing user if it exists
+ for _, u := range users {
+ if bytes.Equal(u.User, username) {
+ u.Seed = seed
+ u.Passwd = hashed
+ exist = true
+ break
+ }
+ }
+ }
+
+ if !exist {
+ users = append(users, UserRecord{username, seed, hashed})
+ }
+
+ output, err := json.Marshal(users)
+ if err != nil {
+ fmt.Println("Failed to marshal passwords", err)
+ return
+ }
+
+ os.WriteFile(userFileName, output, 0660)
+}
+
+func AddUser(username string) {
+ fmt.Println("Please type your password (it will not be echoed back):")
+ passwd, err := term.ReadPassword(int(os.Stdin.Fd()))
+
+ if err != nil {
+ fmt.Println("Failed to read password", err)
+ return
+ }
+
+ fmt.Println("Please type your password again:")
+ passwd2, err := term.ReadPassword(int(os.Stdin.Fd()))
+
+ if err != nil {
+ fmt.Println("Failed to read password", err)
+ return
+ }
+
+ if !bytes.Equal(passwd, passwd2) {
+ fmt.Println("Password mismatch, try again")
+ return
+ }
+
+ addUser([]byte(username), passwd)
+}
+
+func DelUser(username string) {
+ var users []UserRecord
+ var err error
+
+ exist := false
+ file, err := os.ReadFile(userFileName)
+ if err != nil {
+ fmt.Println("Failed to read users file", err)
+ return
+ }
+
+ err = json.Unmarshal(file, &users)
+
+ if err != nil {
+ fmt.Println("Failed to parse json format", err)
+ return
+ }
+ // update the existing user if it exists
+ for i, u := range users {
+ if bytes.Equal(u.User, []byte(username)) {
+ users = append(users[:i], users[i+1:]...)
+ exist = true
+ break
+ }
+ }
+
+ if exist {
+ output, err := json.Marshal(users)
+ if err != nil {
+ fmt.Println("Failed to marshal passwords", err)
+ return
+ }
+
+ os.WriteFile(userFileName, output, 0660)
+ }
+}
+
+func ValidateUser(username []byte, passwd []byte) bool {
+ var users []UserRecord
+ var err error
+
+ file, err := os.ReadFile(userFileName)
+ if err != nil {
+ fmt.Println("Failed to read users file", err)
+ return false
+ }
+
+ err = json.Unmarshal(file, &users)
+
+ if err != nil {
+ fmt.Println("Failed to parse json format", err)
+ return false
+ }
+
+ // update the existing user if it exists
+ for _, u := range users {
+ if bytes.Equal(u.User, []byte(username)) {
+ hashed := hashPassword(u.Seed, passwd)
+ return bytes.Equal(hashed[:], u.Passwd[:])
+ }
+ }
+
+ return false
+}