mirror of
https://github.com/syssecfsu/witty.git
synced 2024-12-26 04:32:34 +01:00
add user authentication
This commit is contained in:
parent
033d9b3f14
commit
8dfac2d026
24
README.md
24
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.
|
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">
|
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).
|
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.
|
||||||
|
|
||||||
__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.__
|
|
||||||
|
|
||||||
This program is written in the [go programming language](https://go.dev/), using the
|
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/)!
|
[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 .```
|
```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:
|
||||||
|
|
||||||
6. Connect to the server, for example
|
```./witty run htop``` or
|
||||||
|
|
||||||
|
```./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```
|
```https://<witty_server_ip>:8080```
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@
|
|||||||
target="_blank" role="button">
|
target="_blank" role="button">
|
||||||
New Session
|
New Session
|
||||||
</a>
|
</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
|
Logout
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
55
main.go
55
main.go
@ -1,6 +1,8 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
@ -15,17 +17,48 @@ func main() {
|
|||||||
log.SetOutput(fp)
|
log.SetOutput(fp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse the arguments. User can pass the command to execute
|
if len(os.Args) < 2 {
|
||||||
// by default, we use bash, but macos users might want to use zsh
|
fmt.Println("witty (adduser|deluser|run)")
|
||||||
// you can also run single program, such as pstree, htop...
|
return
|
||||||
// but program might misbehave (htop seems to be fine)
|
}
|
||||||
var cmdToExec = []string{"bash"}
|
|
||||||
args := os.Args
|
var naked bool
|
||||||
|
runCmd := flag.NewFlagSet("run", flag.ExitOnError)
|
||||||
if len(args) > 1 {
|
runCmd.BoolVar(&naked, "n", false, "Run WiTTY without user authentication")
|
||||||
cmdToExec = args[1:]
|
runCmd.BoolVar(&naked, "naked", false, "Run WiTTY without user authentication")
|
||||||
log.Println(cmdToExec)
|
|
||||||
|
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)
|
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,7 @@ func login(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check for username and password match, usually from a database
|
// 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")
|
leftLoginMsg(c, "Username/password does not match")
|
||||||
c.Redirect(http.StatusSeeOther, "/login")
|
c.Redirect(http.StatusSeeOther, "/login")
|
||||||
return
|
return
|
||||||
|
@ -28,8 +28,13 @@ func collectSessions(c *gin.Context, cmd string) (players []InteractiveSession)
|
|||||||
|
|
||||||
func indexPage(c *gin.Context) {
|
func indexPage(c *gin.Context) {
|
||||||
host = &c.Request.Host
|
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) {
|
func updateIndex(c *gin.Context) {
|
||||||
|
@ -14,6 +14,7 @@ import (
|
|||||||
|
|
||||||
var host *string = nil
|
var host *string = nil
|
||||||
var cmdToExec []string
|
var cmdToExec []string
|
||||||
|
var noAuth bool
|
||||||
|
|
||||||
// simple function to check origin
|
// simple function to check origin
|
||||||
func checkOrigin(r *http.Request) bool {
|
func checkOrigin(r *http.Request) bool {
|
||||||
@ -31,8 +32,9 @@ func checkOrigin(r *http.Request) bool {
|
|||||||
return (host != nil) && (*host == h.Host)
|
return (host != nil) && (*host == h.Host)
|
||||||
}
|
}
|
||||||
|
|
||||||
func StartWeb(fp *os.File, cmd []string) {
|
func StartWeb(fp *os.File, cmd []string, naked bool) {
|
||||||
cmdToExec = cmd
|
cmdToExec = cmd
|
||||||
|
noAuth = naked
|
||||||
|
|
||||||
if fp != nil {
|
if fp != nil {
|
||||||
gin.DefaultWriter = fp
|
gin.DefaultWriter = fp
|
||||||
@ -55,7 +57,11 @@ func StartWeb(fp *os.File, cmd []string) {
|
|||||||
rt.GET("/login", loginPage)
|
rt.GET("/login", loginPage)
|
||||||
rt.POST("/login", login)
|
rt.POST("/login", login)
|
||||||
|
|
||||||
g1 := rt.Group("/", AuthRequired)
|
g1 := rt.Group("/")
|
||||||
|
|
||||||
|
if !naked {
|
||||||
|
g1.Use(AuthRequired)
|
||||||
|
}
|
||||||
|
|
||||||
// Fill in the index page
|
// Fill in the index page
|
||||||
g1.GET("/", indexPage)
|
g1.GET("/", indexPage)
|
||||||
|
152
web/user.go
Normal file
152
web/user.go
Normal 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
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user