mirror of
https://github.com/syssecfsu/witty.git
synced 2024-11-01 01:29:26 +01:00
refactor files, add authentication
This commit is contained in:
parent
0acf1a95af
commit
481e5473aa
1
assets/img/sign-in.svg
Normal file
1
assets/img/sign-in.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="sign-in-alt" class="svg-inline--fa fa-sign-in-alt fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M416 448h-84c-6.6 0-12-5.4-12-12v-40c0-6.6 5.4-12 12-12h84c17.7 0 32-14.3 32-32V160c0-17.7-14.3-32-32-32h-84c-6.6 0-12-5.4-12-12V76c0-6.6 5.4-12 12-12h84c53 0 96 43 96 96v192c0 53-43 96-96 96zm-47-201L201 79c-15-15-41-4.5-41 17v96H24c-13.3 0-24 10.7-24 24v96c0 13.3 10.7 24 24 24h136v96c0 21.5 26 32 41 17l168-168c9.3-9.4 9.3-24.6 0-34z"></path></svg>
|
After Width: | Height: | Size: 578 B |
1
assets/img/sign-out.svg
Normal file
1
assets/img/sign-out.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="sign-out-alt" class="svg-inline--fa fa-sign-out-alt fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M497 273L329 441c-15 15-41 4.5-41-17v-96H152c-13.3 0-24-10.7-24-24v-96c0-13.3 10.7-24 24-24h136V88c0-21.4 25.9-32 41-17l168 168c9.3 9.4 9.3 24.6 0 34zM192 436v-40c0-6.6-5.4-12-12-12H96c-17.7 0-32-14.3-32-32V160c0-17.7 14.3-32 32-32h84c6.6 0 12-5.4 12-12V76c0-6.6-5.4-12-12-12H96c-53 0-96 43-96 96v192c0 53 43 96 96 96h84c6.6 0 12-5.4 12-12z"></path></svg>
|
After Width: | Height: | Size: 584 B |
@ -27,10 +27,15 @@
|
||||
class="d-inline-block align-text-top">
|
||||
WiTTY: Web-based interactive TTY
|
||||
</a>
|
||||
<a class="btn btn-primary btn-sm float-end" href="/new" onClick="setTimeout(function(){refresh(true)}, 1000)"
|
||||
target="_blank" role="button">
|
||||
New Session
|
||||
</a>
|
||||
<div class="btn-toolbar float-end" role="toolbar" aria-label="top buttons">
|
||||
<a class="btn btn-primary btn-sm m-1" href="/new" onClick="setTimeout(function(){refresh(true)}, 1000)"
|
||||
target="_blank" role="button">
|
||||
New Session
|
||||
</a>
|
||||
<a class="btn btn-primary btn-sm m-1" href="/logout" role="button">
|
||||
Logout
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
@ -15,8 +15,8 @@
|
||||
|
||||
<body class="text-center">
|
||||
<main class="form-signin">
|
||||
<form action="/sign-in" method="post">
|
||||
<img class="mb-4" src="/assets/img/logo.svg" alt="" width="64">
|
||||
<form action="/login" method="post">
|
||||
<img class="mb-4" src="/assets/img/keyboard.svg" alt="" width="64">
|
||||
|
||||
<div class="form-floating">
|
||||
<input type="text" class="form-control" id="username" name="username" placeholder="User Name">
|
||||
@ -27,10 +27,7 @@
|
||||
<label for="passwd">Password</label>
|
||||
</div>
|
||||
|
||||
<div class="checkbox mb-3">
|
||||
<label><input type="checkbox" name="remember" value="true"> Remember me</label>
|
||||
</div>
|
||||
<button class="w-100 btn btn-lg btn-primary" type="submit">Sign in</button>
|
||||
<button class="w-100 btn btn-lg btn-primary mt-5" type="submit">Sign in</button>
|
||||
<p class="mt-5 mb-3 text-muted">WiTTY: Web-based Interactive TTY</p>
|
||||
</form>
|
||||
</main>
|
9
go.mod
9
go.mod
@ -9,8 +9,17 @@ require (
|
||||
github.com/gorilla/websocket v1.4.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff // indirect
|
||||
github.com/gomodule/redigo v2.0.0+incompatible // indirect
|
||||
github.com/gorilla/context v1.1.1 // indirect
|
||||
github.com/gorilla/securecookie v1.1.1 // indirect
|
||||
github.com/gorilla/sessions v1.2.1 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/gin-gonic/contrib v0.0.0-20201101042839-6a891bf89f19
|
||||
github.com/go-playground/locales v0.13.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.17.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.4.1 // indirect
|
||||
|
13
go.sum
13
go.sum
@ -1,3 +1,5 @@
|
||||
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff h1:RmdPFa+slIr4SCBg4st/l/vZWVe9QJKMXGO60Bxbe04=
|
||||
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=
|
||||
github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
|
||||
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@ -7,6 +9,8 @@ github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 h1:RAV05c0xOkJ3dZGS0
|
||||
github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/contrib v0.0.0-20201101042839-6a891bf89f19 h1:J2LPEOcQmWaooBnBtUDV9KHFEnP5LYTZY03GiQ0oQBw=
|
||||
github.com/gin-gonic/contrib v0.0.0-20201101042839-6a891bf89f19/go.mod h1:iqneQ2Df3omzIVTkIfn7c1acsVnMGiSLn4XF5Blh3Yg=
|
||||
github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
|
||||
github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
|
||||
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||
@ -19,7 +23,16 @@ github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7a
|
||||
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
|
||||
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
|
||||
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
|
||||
|
233
main.go
233
main.go
@ -1,137 +1,25 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/dchest/uniuri"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/syssecfsu/witty/term_conn"
|
||||
"github.com/syssecfsu/witty/web"
|
||||
)
|
||||
|
||||
// command line options
|
||||
var cmdToExec = []string{"bash"}
|
||||
|
||||
var host *string = nil
|
||||
|
||||
// simple function to check origin
|
||||
func checkOrigin(r *http.Request) bool {
|
||||
org := r.Header.Get("Origin")
|
||||
h, err := url.Parse(org)
|
||||
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if (host == nil) || (*host != h.Host) {
|
||||
log.Println("Failed origin check of ", org)
|
||||
}
|
||||
|
||||
return (host != nil) && (*host == h.Host)
|
||||
}
|
||||
|
||||
type InteractiveSession struct {
|
||||
Ip string
|
||||
Cmd string
|
||||
Id string
|
||||
}
|
||||
|
||||
type RecordedSession struct {
|
||||
Fname string
|
||||
Fsize string
|
||||
Duration string
|
||||
Time string
|
||||
}
|
||||
|
||||
// how many seconds of the session
|
||||
func getDuration(fname string) int64 {
|
||||
fp, err := os.Open("./records/" + fname)
|
||||
|
||||
if err != nil {
|
||||
log.Println("Failed to open record file", err)
|
||||
return 0
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(fp)
|
||||
|
||||
if decoder == nil {
|
||||
log.Println("Failed to create JSON decoder")
|
||||
return 0
|
||||
}
|
||||
|
||||
// To work with javascript decoder, we organize the file as
|
||||
// an array of writeRecord. golang decode instead decode
|
||||
// as individual record. Call decoder.Token to skip opening [
|
||||
decoder.Token()
|
||||
|
||||
var dur int64 = 0
|
||||
|
||||
for decoder.More() {
|
||||
var record term_conn.WriteRecord
|
||||
|
||||
if err := decoder.Decode(&record); err != nil {
|
||||
log.Println("Failed to decode record", err)
|
||||
continue
|
||||
}
|
||||
|
||||
dur += record.Dur.Milliseconds()
|
||||
}
|
||||
|
||||
return dur/1000 + 1
|
||||
}
|
||||
|
||||
func collectTabData(c *gin.Context) (players []InteractiveSession, records []RecordedSession) {
|
||||
term_conn.ForEachSession(func(tc *term_conn.TermConn) {
|
||||
players = append(players, InteractiveSession{
|
||||
Id: tc.Name,
|
||||
Ip: tc.Ip,
|
||||
Cmd: cmdToExec[0],
|
||||
})
|
||||
})
|
||||
|
||||
files, err := ioutil.ReadDir("./records/")
|
||||
|
||||
if err == nil {
|
||||
for _, finfo := range files {
|
||||
fname := finfo.Name()
|
||||
if !strings.HasSuffix(fname, ".rec") {
|
||||
continue
|
||||
}
|
||||
fsize := finfo.Size() / 1024
|
||||
duration := getDuration(fname)
|
||||
|
||||
records = append(records,
|
||||
RecordedSession{
|
||||
Fname: fname,
|
||||
Fsize: strconv.FormatInt(fsize, 10),
|
||||
Duration: strconv.FormatInt(duration, 10),
|
||||
Time: finfo.ModTime().Format("Jan/2/2006, 15:04:05"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func main() {
|
||||
fp, err := os.OpenFile("witty.log", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
|
||||
if err == nil {
|
||||
defer fp.Close()
|
||||
log.SetOutput(fp)
|
||||
gin.DefaultWriter = 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 {
|
||||
@ -139,120 +27,5 @@ func main() {
|
||||
log.Println(cmdToExec)
|
||||
}
|
||||
|
||||
rt := gin.Default()
|
||||
|
||||
rt.SetTrustedProxies(nil)
|
||||
rt.LoadHTMLGlob("./assets/template/*")
|
||||
|
||||
// Fill in the index page
|
||||
rt.GET("/", func(c *gin.Context) {
|
||||
host = &c.Request.Host
|
||||
|
||||
c.HTML(http.StatusOK, "index.html", gin.H{})
|
||||
})
|
||||
|
||||
rt.GET("/sign-in", func(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "signin.html", gin.H{})
|
||||
})
|
||||
|
||||
rt.GET("/favicon.ico", func(c *gin.Context) {
|
||||
c.File("./assets/img/favicon.ico")
|
||||
})
|
||||
|
||||
// to update the tabs of current interactive and saved sessions
|
||||
rt.GET("/update/:active", func(c *gin.Context) {
|
||||
var active0, active1 string
|
||||
|
||||
// setup which tab is active, it is hard to do in javascript at
|
||||
// client side due to timing issues.
|
||||
which := c.Param("active")
|
||||
if which == "0" {
|
||||
active0 = "active"
|
||||
active1 = ""
|
||||
} else {
|
||||
active0 = ""
|
||||
active1 = "active"
|
||||
}
|
||||
|
||||
players, records := collectTabData(c)
|
||||
|
||||
c.HTML(http.StatusOK, "tab.html", gin.H{
|
||||
"players": players,
|
||||
"records": records,
|
||||
"active0": active0,
|
||||
"active1": active1,
|
||||
})
|
||||
})
|
||||
|
||||
// create a new interactive session
|
||||
rt.GET("/new", func(c *gin.Context) {
|
||||
if host == nil {
|
||||
host = &c.Request.Host
|
||||
}
|
||||
|
||||
id := uniuri.New()
|
||||
|
||||
c.HTML(http.StatusOK, "term.html", gin.H{
|
||||
"title": "interactive terminal",
|
||||
"path": "/ws_new/" + id,
|
||||
"id": id,
|
||||
"logo": "keyboard",
|
||||
})
|
||||
})
|
||||
|
||||
rt.GET("/ws_new/:id", func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
term_conn.ConnectTerm(c.Writer, c.Request, false, id, cmdToExec)
|
||||
})
|
||||
|
||||
// create a viewer of an interactive session
|
||||
rt.GET("/view/:id", func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
c.HTML(http.StatusOK, "term.html", gin.H{
|
||||
"title": "viewer terminal",
|
||||
"path": "/ws_view/" + id,
|
||||
"id": id,
|
||||
"logo": "view",
|
||||
})
|
||||
})
|
||||
|
||||
rt.GET("/ws_view/:id", func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
term_conn.ConnectTerm(c.Writer, c.Request, true, id, nil)
|
||||
})
|
||||
|
||||
// start/stop recording the session
|
||||
rt.GET("/record/:id", func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
term_conn.StartRecord(id)
|
||||
})
|
||||
|
||||
rt.GET("/stop/:id", func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
term_conn.StopRecord(id)
|
||||
})
|
||||
|
||||
// create a viewer of an interactive session
|
||||
rt.GET("/replay/:id", func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
log.Println("replay/ called with", id)
|
||||
c.HTML(http.StatusOK, "replay.html", gin.H{
|
||||
"fname": id,
|
||||
})
|
||||
})
|
||||
|
||||
rt.GET("/delete/:fname", func(c *gin.Context) {
|
||||
fname := c.Param("fname")
|
||||
if err := os.Remove("./records/" + fname); err != nil {
|
||||
log.Println("Failed to delete file,", err)
|
||||
}
|
||||
})
|
||||
|
||||
// handle static files
|
||||
rt.Static("/assets", "./assets")
|
||||
rt.Static("/records", "./records")
|
||||
|
||||
term_conn.Init(checkOrigin)
|
||||
|
||||
rt.RunTLS(":8080", "./tls/cert.pem", "./tls/private-key.pem")
|
||||
web.StartWeb(fp, cmdToExec)
|
||||
}
|
||||
|
83
web/auth.go
Normal file
83
web/auth.go
Normal file
@ -0,0 +1,83 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
userkey = "user"
|
||||
)
|
||||
|
||||
func login(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
|
||||
username := c.PostForm("username")
|
||||
passwd := c.PostForm("passwd")
|
||||
|
||||
// Validate form input
|
||||
if strings.Trim(username, " ") == "" || strings.Trim(passwd, " ") == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Username/password can't be empty"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check for username and password match, usually from a database
|
||||
if username != "hello" || passwd != "world" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication failed"})
|
||||
return
|
||||
}
|
||||
|
||||
// Save the username in the session
|
||||
session.Set(userkey, username) // In real world usage you'd set this to the users ID
|
||||
|
||||
if err := session.Save(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save session"})
|
||||
return
|
||||
}
|
||||
|
||||
host = &c.Request.Host
|
||||
|
||||
c.Redirect(http.StatusSeeOther, "/")
|
||||
}
|
||||
|
||||
func logout(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
user := session.Get(userkey)
|
||||
if user == nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid session token"})
|
||||
return
|
||||
}
|
||||
session.Delete(userkey)
|
||||
if err := session.Save(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save session"})
|
||||
return
|
||||
}
|
||||
c.Redirect(http.StatusFound, "/login")
|
||||
}
|
||||
|
||||
// AuthRequired is a simple middleware to check the session
|
||||
func AuthRequired(c *gin.Context) {
|
||||
if (c.Request.URL.String() == "/login") ||
|
||||
strings.HasPrefix(c.Request.URL.String(), "/assets") {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
session := sessions.Default(c)
|
||||
user := session.Get(userkey)
|
||||
if user == nil {
|
||||
// Abort the request with the appropriate error code
|
||||
c.Redirect(http.StatusTemporaryRedirect, "/login")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
// Continue down the chain to handler etc
|
||||
c.Next()
|
||||
}
|
||||
|
||||
func loginPage(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "login.html", gin.H{})
|
||||
}
|
97
web/interactive.go
Normal file
97
web/interactive.go
Normal file
@ -0,0 +1,97 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/dchest/uniuri"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/syssecfsu/witty/term_conn"
|
||||
)
|
||||
|
||||
type InteractiveSession struct {
|
||||
Ip string
|
||||
Cmd string
|
||||
Id string
|
||||
}
|
||||
|
||||
func collectSessions(c *gin.Context, cmd string) (players []InteractiveSession) {
|
||||
term_conn.ForEachSession(func(tc *term_conn.TermConn) {
|
||||
players = append(players, InteractiveSession{
|
||||
Id: tc.Name,
|
||||
Ip: tc.Ip,
|
||||
Cmd: cmd,
|
||||
})
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func indexPage(c *gin.Context) {
|
||||
host = &c.Request.Host
|
||||
|
||||
c.HTML(http.StatusOK, "index.html", gin.H{})
|
||||
}
|
||||
|
||||
func updateIndex(c *gin.Context) {
|
||||
var active0, active1 string
|
||||
|
||||
// setup which tab is active, it is hard to do in javascript at
|
||||
// client side due to timing issues.
|
||||
which := c.Param("active")
|
||||
if which == "0" {
|
||||
active0 = "active"
|
||||
active1 = ""
|
||||
} else {
|
||||
active0 = ""
|
||||
active1 = "active"
|
||||
}
|
||||
|
||||
players := collectSessions(c, cmdToExec[0])
|
||||
records := collectRecords(c, cmdToExec[0])
|
||||
|
||||
c.HTML(http.StatusOK, "tab.html", gin.H{
|
||||
"players": players,
|
||||
"records": records,
|
||||
"active0": active0,
|
||||
"active1": active1,
|
||||
})
|
||||
}
|
||||
|
||||
func newInteractive(c *gin.Context) {
|
||||
if host == nil {
|
||||
host = &c.Request.Host
|
||||
}
|
||||
|
||||
id := uniuri.New()
|
||||
|
||||
c.HTML(http.StatusOK, "term.html", gin.H{
|
||||
"title": "interactive terminal",
|
||||
"path": "/ws_new/" + id,
|
||||
"id": id,
|
||||
"logo": "keyboard",
|
||||
})
|
||||
}
|
||||
|
||||
func newTermConn(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
term_conn.ConnectTerm(c.Writer, c.Request, false, id, cmdToExec)
|
||||
}
|
||||
|
||||
func viewPage(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
c.HTML(http.StatusOK, "term.html", gin.H{
|
||||
"title": "viewer terminal",
|
||||
"path": "/ws_view/" + id,
|
||||
"id": id,
|
||||
"logo": "view",
|
||||
})
|
||||
}
|
||||
|
||||
func newViewWS(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
term_conn.ConnectTerm(c.Writer, c.Request, true, id, nil)
|
||||
}
|
||||
|
||||
func favIcon(c *gin.Context) {
|
||||
c.File("./assets/img/favicon.ico")
|
||||
}
|
108
web/record.go
Normal file
108
web/record.go
Normal file
@ -0,0 +1,108 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/syssecfsu/witty/term_conn"
|
||||
)
|
||||
|
||||
type RecordedSession struct {
|
||||
Fname string
|
||||
Fsize string
|
||||
Duration string
|
||||
Time string
|
||||
}
|
||||
|
||||
// how many seconds of the session
|
||||
func getDuration(fname string) int64 {
|
||||
fp, err := os.Open("./records/" + fname)
|
||||
|
||||
if err != nil {
|
||||
log.Println("Failed to open record file", err)
|
||||
return 0
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(fp)
|
||||
|
||||
if decoder == nil {
|
||||
log.Println("Failed to create JSON decoder")
|
||||
return 0
|
||||
}
|
||||
|
||||
// To work with javascript decoder, we organize the file as
|
||||
// an array of writeRecord. golang decode instead decode
|
||||
// as individual record. Call decoder.Token to skip opening [
|
||||
decoder.Token()
|
||||
|
||||
var dur int64 = 0
|
||||
|
||||
for decoder.More() {
|
||||
var record term_conn.WriteRecord
|
||||
|
||||
if err := decoder.Decode(&record); err != nil {
|
||||
log.Println("Failed to decode record", err)
|
||||
continue
|
||||
}
|
||||
|
||||
dur += record.Dur.Milliseconds()
|
||||
}
|
||||
|
||||
return dur/1000 + 1
|
||||
}
|
||||
|
||||
func collectRecords(c *gin.Context, cmd string) (records []RecordedSession) {
|
||||
files, err := ioutil.ReadDir("./records/")
|
||||
|
||||
if err == nil {
|
||||
for _, finfo := range files {
|
||||
fname := finfo.Name()
|
||||
if !strings.HasSuffix(fname, ".rec") {
|
||||
continue
|
||||
}
|
||||
fsize := finfo.Size() / 1024
|
||||
duration := getDuration(fname)
|
||||
|
||||
records = append(records,
|
||||
RecordedSession{
|
||||
Fname: fname,
|
||||
Fsize: strconv.FormatInt(fsize, 10),
|
||||
Duration: strconv.FormatInt(duration, 10),
|
||||
Time: finfo.ModTime().Format("Jan/2/2006, 15:04:05"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func startRecord(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
term_conn.StartRecord(id)
|
||||
}
|
||||
|
||||
func stopRecord(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
term_conn.StopRecord(id)
|
||||
}
|
||||
|
||||
func replayPage(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
log.Println("replay/ called with", id)
|
||||
c.HTML(http.StatusOK, "replay.html", gin.H{
|
||||
"fname": id,
|
||||
})
|
||||
}
|
||||
|
||||
func delRec(c *gin.Context) {
|
||||
fname := c.Param("fname")
|
||||
if err := os.Remove("./records/" + fname); err != nil {
|
||||
log.Println("Failed to delete file,", err)
|
||||
}
|
||||
}
|
87
web/routing.go
Normal file
87
web/routing.go
Normal file
@ -0,0 +1,87 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
"github.com/dchest/uniuri"
|
||||
"github.com/gin-gonic/contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/syssecfsu/witty/term_conn"
|
||||
)
|
||||
|
||||
var host *string = nil
|
||||
var cmdToExec []string
|
||||
|
||||
// simple function to check origin
|
||||
func checkOrigin(r *http.Request) bool {
|
||||
org := r.Header.Get("Origin")
|
||||
h, err := url.Parse(org)
|
||||
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if (host == nil) || (*host != h.Host) {
|
||||
log.Println("Failed origin check of ", org)
|
||||
}
|
||||
|
||||
return (host != nil) && (*host == h.Host)
|
||||
}
|
||||
|
||||
func StartWeb(fp *os.File, cmd []string) {
|
||||
cmdToExec = cmd
|
||||
|
||||
if fp != nil {
|
||||
gin.DefaultWriter = fp
|
||||
}
|
||||
|
||||
rt := gin.Default()
|
||||
|
||||
// We randomly generate a key for now, should use a fixed key
|
||||
// so login can survive server reboot
|
||||
store := sessions.NewCookieStore([]byte(uniuri.NewLen(32)))
|
||||
rt.Use(sessions.Sessions("witty-session", store))
|
||||
rt.Use(AuthRequired)
|
||||
|
||||
rt.SetTrustedProxies(nil)
|
||||
rt.LoadHTMLGlob("./assets/template/*")
|
||||
|
||||
// Fill in the index page
|
||||
rt.GET("/", indexPage)
|
||||
rt.GET("/login", loginPage)
|
||||
|
||||
rt.POST("/login", login)
|
||||
rt.GET("/logout", logout)
|
||||
|
||||
// to update the tabs of current interactive and saved sessions
|
||||
rt.GET("/update/:active", updateIndex)
|
||||
|
||||
// create a new interactive session
|
||||
rt.GET("/new", newInteractive)
|
||||
rt.GET("/ws_new/:id", newTermConn)
|
||||
|
||||
// create a viewer of an interactive session
|
||||
rt.GET("/view/:id", viewPage)
|
||||
rt.GET("/ws_view/:id", newViewWS)
|
||||
|
||||
// start/stop recording the session
|
||||
rt.GET("/record/:id", startRecord)
|
||||
rt.GET("/stop/:id", stopRecord)
|
||||
|
||||
// create a viewer of an interactive session
|
||||
rt.GET("/replay/:id", replayPage)
|
||||
|
||||
// delete a recording
|
||||
rt.GET("/delete/:fname", delRec)
|
||||
|
||||
// handle static files
|
||||
rt.Static("/assets", "./assets")
|
||||
rt.Static("/records", "./records")
|
||||
rt.GET("/favicon.ico", favIcon)
|
||||
|
||||
term_conn.Init(checkOrigin)
|
||||
rt.RunTLS(":8080", "./tls/cert.pem", "./tls/private-key.pem")
|
||||
}
|
Loading…
Reference in New Issue
Block a user