mirror of
https://github.com/syssecfsu/witty.git
synced 2024-12-24 11:42: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.
|
||||
|
||||
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```
|
||||
|
||||
|
@ -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
55
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 <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
|
||||
if username != "hello" || passwd != "world" {
|
||||
if !ValidateUser([]byte(username), []byte(passwd)) {
|
||||
leftLoginMsg(c, "Username/password does not match")
|
||||
c.Redirect(http.StatusSeeOther, "/login")
|
||||
return
|
||||
|
@ -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) {
|
||||
|
@ -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
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