add user authentication

This commit is contained in:
Zhi Wang 2022-01-21 21:42:22 -05:00
parent 033d9b3f14
commit 8dfac2d026
7 changed files with 226 additions and 26 deletions

View File

@ -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. <img src="extra/main.png" width="800px">
@ -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 <username>```, and delete an existing user with ```witty deluser <username>```. 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 <username>```
```./witty ssh <ssh_server_ip> -l <user_name>```
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 <ssh_server_ip> -l <user_name>```
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://<witty_server_ip>:8080```

View File

@ -32,7 +32,7 @@
target="_blank" role="button">
New Session
</a>
<a class="btn btn-primary btn-sm m-1" href="/logout" role="button">
<a class="btn btn-primary btn-sm m-1 {{.disabled}}" href="/logout" role="button">
Logout
</a>
</div>

55
main.go
View File

@ -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 <username>")
return
}
web.AddUser(os.Args[2])
case "deluser":
if len(os.Args) != 3 {
fmt.Println("witty deluser <username>")
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)
}

View File

@ -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

View File

@ -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) {

View File

@ -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)

152
web/user.go Normal file
View File

@ -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
}