mirror of
https://github.com/syssecfsu/witty.git
synced 2025-01-25 19:44:24 +01:00
WIP
This commit is contained in:
parent
4cc5f72d13
commit
38151a2f42
@ -8,11 +8,12 @@
|
|||||||
<meta name="author" content="">
|
<meta name="author" content="">
|
||||||
|
|
||||||
<!-- automatically refresh the page every 30 seconds -->
|
<!-- automatically refresh the page every 30 seconds -->
|
||||||
<meta http-equiv="refresh" content="30">
|
<!-- <meta http-equiv="refresh" content="30"> -->
|
||||||
|
|
||||||
<title>Web Terminal</title>
|
<title>Web Terminal</title>
|
||||||
|
|
||||||
<!-- Bootstrap core CSS -->
|
<!-- Bootstrap core CSS -->
|
||||||
|
<script src="/assets/external/bootstrap.min.js"></script>
|
||||||
<link href="/assets/external/bootstrap.min.css" rel="stylesheet">
|
<link href="/assets/external/bootstrap.min.css" rel="stylesheet">
|
||||||
<link href="/assets/main.css" rel="stylesheet">
|
<link href="/assets/main.css" rel="stylesheet">
|
||||||
|
|
||||||
@ -21,12 +22,11 @@
|
|||||||
<nav class="navbar navbar-dark bg-dark shadow-sm navbar-xs">
|
<nav class="navbar navbar-dark bg-dark shadow-sm navbar-xs">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<a class="navbar-brand mx-auto" href="https://github.com/syssecfsu/witty" target="_blank">
|
<a class="navbar-brand mx-auto" href="https://github.com/syssecfsu/witty" target="_blank">
|
||||||
<img src="/assets/logo.svg" style="margin-right: 0.5rem;" height="32"
|
<img src="/assets/logo.svg" style="margin-right: 0.5rem;" height="32" class="d-inline-block align-text-top">
|
||||||
class="d-inline-block align-text-top">
|
|
||||||
WiTTY: Web-based interactive TTY
|
WiTTY: Web-based interactive TTY
|
||||||
</a>
|
</a>
|
||||||
<a class="btn btn-primary btn-sm float-end" href="/new"
|
<a class="btn btn-primary btn-sm float-end" href="/new"
|
||||||
onClick="setTimeout(function(){window.location.reload()}, 3000);" target="_blank" role="button">
|
onClick="setTimeout(function(){window.location.reload()}, 2000);" target="_blank" role="button">
|
||||||
New Session
|
New Session
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@ -34,25 +34,81 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<div class="container" style="margin-top:1em;">
|
<div class="container-fluid" style="margin-top:1em;">
|
||||||
<div class="card-deck row justify-content-center">
|
<nav>
|
||||||
|
<div class="nav nav-tabs" id="nav-tab" role="tablist">
|
||||||
|
<button class="nav-link border bg-light" id="nav-home-tab" data-bs-toggle="tab" data-bs-target="#nav-home"
|
||||||
|
type="button" role="tab" aria-controls="nav-home" aria-selected="true">Live</button>
|
||||||
|
<button class="nav-link border active bg-light" id="nav-profile-tab" data-bs-toggle="tab"
|
||||||
|
data-bs-target="#nav-profile" type="button" role="tab" aria-controls="nav-profile"
|
||||||
|
aria-selected="false">Saved</button>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="tab-content bg-light" id="nav-tabContent">
|
||||||
|
<div class="tab-pane fade" id="nav-home" role="tabpanel" aria-labelledby="nav-home-tab">
|
||||||
|
<div class="card-deck row justify-content-center">
|
||||||
|
|
||||||
|
<!-- repeat this for each interactive session -->
|
||||||
|
{{range .players}}
|
||||||
|
<div class="card shadow-sm border-danger bg-white mb-3" style="width: 16rem; margin:1em;">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Interactive session</h5>
|
||||||
|
<p class="card-text">From <em>{{.Ip}}</em>, running <strong>{{.Cmd}}</strong>, session ID:
|
||||||
|
<u>{{.Id}}</u>
|
||||||
|
</p>
|
||||||
|
<a class="btn btn-outline-success btn-sm float-end" href="/view/{{.Id}}" target="_blank"
|
||||||
|
role="button">View
|
||||||
|
Session</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
<!-- repeat this for each interactive session -->
|
|
||||||
{{range .players}}
|
|
||||||
<div class="card shadow-sm border-info bg-light mb-3" style="width: 16rem; margin:1em;">
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title">Interactive session</h5>
|
|
||||||
<p class="card-text">From <em>{{.Ip}}</em>, running <strong>{{.Cmd}}</strong>, session ID: <u>{{.Id}}</u>
|
|
||||||
</p>
|
|
||||||
<a class="btn btn-secondary btn-sm float-end" href="/view/{{.Id}}" target="_blank" role="button">View
|
|
||||||
Session</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
|
||||||
|
|
||||||
|
<div class="tab-pane fade show active" id="nav-profile" role="tabpanel" aria-labelledby="nav-profile-tab">
|
||||||
|
<div class="card-deck row justify-content-center">
|
||||||
|
|
||||||
|
<!-- repeat this for each recorded session -->
|
||||||
|
{{range .records}}
|
||||||
|
<div class="card shadow-sm border-info bg-light mb-3" style="width: 16rem; margin:1em;">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Recorded session</h5>
|
||||||
|
<p class="card-text">File name: <u>{{.Fname}}</u>, file size: <em>{{.Fsize}}KB</em>,
|
||||||
|
recorded at <strong>{{.Time}}</strong>, duration: <mark>{{.Duration}}s</mark>,
|
||||||
|
</p>
|
||||||
|
<div class="btn-toolbar float-end" role="toolbar" aria-label="records buttons">
|
||||||
|
<a class="btn btn-outline-success btn-sm m-1" href="/replay/{{.Fname}}" target="_blank" role="button">
|
||||||
|
<img src="/assets/play.svg" height="18px">
|
||||||
|
</a>
|
||||||
|
<a class="btn btn-outline-success btn-sm m-1" href="/records/{{.Fname}}" role="button" download>
|
||||||
|
<img src="/assets/download.svg" height="18px">
|
||||||
|
</a>
|
||||||
|
<button type="button" class="btn btn-outline-success btn-sm m-1" onclick="del_btn({{.Fname}})">
|
||||||
|
<img src="/assets/delete.svg" height="18px">
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function del_btn(path) {
|
||||||
|
fetch("/delete/" + path)
|
||||||
|
setTimeout(function () {
|
||||||
|
window.location.reload()
|
||||||
|
}, 800);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
@ -36,3 +36,8 @@
|
|||||||
padding-top: 0px;
|
padding-top: 0px;
|
||||||
padding-bottom: 0px;
|
padding-bottom: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn:focus {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
@ -52,7 +52,7 @@
|
|||||||
rc.value = 0
|
rc.value = 0
|
||||||
|
|
||||||
var icon = document.getElementById("play-btn")
|
var icon = document.getElementById("play-btn")
|
||||||
var path = "/records/5FGD56YdzAYDGF9s_61e33645.rec"
|
var path = "/records/{{.fname}}"
|
||||||
var pause = false
|
var pause = false
|
||||||
|
|
||||||
function playbtn() {
|
function playbtn() {
|
||||||
|
@ -112,13 +112,14 @@ async function replay_session(term, path, start, paused, prog, end) {
|
|||||||
|
|
||||||
// we will blast through the beginning of the session
|
// we will blast through the beginning of the session
|
||||||
if (cur >= start) {
|
if (cur >= start) {
|
||||||
if (await sleep(item.Duration, paused) == true) {
|
// we are cheating a little bit here, we do not want to wait for too long
|
||||||
|
if (await sleep(Math.min(item.Duration, 1000), paused) == true) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.Duration >= total_dur / 100) {
|
if (item.Duration >= total_dur / 100) {
|
||||||
prog(cur * 100 / total_dur)
|
prog(parseInt(cur * 100 / total_dur))
|
||||||
}
|
}
|
||||||
|
|
||||||
term.write(base64ToUint8array(item.Data))
|
term.write(base64ToUint8array(item.Data))
|
||||||
|
83
main.go
83
main.go
@ -1,10 +1,14 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/dchest/uniuri"
|
"github.com/dchest/uniuri"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@ -38,8 +42,53 @@ type InteractiveSession struct {
|
|||||||
Id 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 fillIndex(c *gin.Context) {
|
func fillIndex(c *gin.Context) {
|
||||||
var players []InteractiveSession
|
var players []InteractiveSession
|
||||||
|
var records []RecordedSession
|
||||||
|
|
||||||
term_conn.ForEachSession(func(tc *term_conn.TermConn) {
|
term_conn.ForEachSession(func(tc *term_conn.TermConn) {
|
||||||
players = append(players, InteractiveSession{
|
players = append(players, InteractiveSession{
|
||||||
@ -49,9 +98,30 @@ func fillIndex(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
files, err := ioutil.ReadDir("./records/")
|
||||||
|
if err == nil {
|
||||||
|
for _, finfo := range files {
|
||||||
|
fname := finfo.Name()
|
||||||
|
if !strings.HasSuffix(fname, ".rec") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fsize := finfo.Size()
|
||||||
|
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"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
c.HTML(http.StatusOK, "index.html", gin.H{
|
c.HTML(http.StatusOK, "index.html", gin.H{
|
||||||
"title": "interactive terminal",
|
"title": "interactive terminal",
|
||||||
"players": players,
|
"players": players,
|
||||||
|
"records": records,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,10 +202,19 @@ func main() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// create a viewer of an interactive session
|
// create a viewer of an interactive session
|
||||||
rt.GET("/replay/*id", func(c *gin.Context) {
|
rt.GET("/replay/:id", func(c *gin.Context) {
|
||||||
id := c.Param("id")
|
id := c.Param("id")
|
||||||
log.Println("replay/ called with", id)
|
log.Println("replay/ called with", id)
|
||||||
c.HTML(http.StatusOK, "replay.html", nil)
|
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
|
// handle static files
|
||||||
|
@ -61,7 +61,7 @@ type TermConn struct {
|
|||||||
pty_done chan struct{} // pty is closed, close this chan in pty reader
|
pty_done chan struct{} // pty is closed, close this chan in pty reader
|
||||||
}
|
}
|
||||||
|
|
||||||
type writeRecord struct {
|
type WriteRecord struct {
|
||||||
Dur time.Duration `json:"Duration"`
|
Dur time.Duration `json:"Duration"`
|
||||||
Data []byte `json:"Data"`
|
Data []byte `json:"Data"`
|
||||||
}
|
}
|
||||||
@ -244,7 +244,7 @@ out:
|
|||||||
|
|
||||||
// Do we need to record the session?
|
// Do we need to record the session?
|
||||||
if tc.record != nil {
|
if tc.record != nil {
|
||||||
jbuf, err := json.Marshal(writeRecord{Dur: time.Since(tc.lastRecTime), Data: buf})
|
jbuf, err := json.Marshal(WriteRecord{Dur: time.Since(tc.lastRecTime), Data: buf})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Failed to marshal record", err)
|
log.Println("Failed to marshal record", err)
|
||||||
} else {
|
} else {
|
||||||
@ -341,6 +341,13 @@ func (tc *TermConn) release() {
|
|||||||
|
|
||||||
close(tc.viewChan)
|
close(tc.viewChan)
|
||||||
close(tc.recordChan)
|
close(tc.recordChan)
|
||||||
|
|
||||||
|
if tc.record != nil {
|
||||||
|
// write a ] and close the file
|
||||||
|
tc.record.Write([]byte("]"))
|
||||||
|
tc.record.Close()
|
||||||
|
tc.record = nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tc.ws.Close()
|
tc.ws.Close()
|
||||||
|
Loading…
Reference in New Issue
Block a user