Compare commits

...

5 Commits

11 changed files with 174 additions and 24 deletions
+4 -1
View File
@@ -48,12 +48,15 @@ class SimpleImageRenderer extends React.Component<
return (
<div className="image-renderer" style={{ fontSize: this.props.opts.termFontSize }}>
<div className="load-error-text">
ERROR: file {dataBlob && dataBlob.name ? JSON.stringify(dataBlob.name) : ""} not found
ERROR: file {dataBlob?.name ? JSON.stringify(dataBlob.name) : ""} not found
</div>
</div>
);
}
if (this.objUrl == null) {
if (dataBlob.name?.endsWith(".svg")) {
dataBlob = new Blob([dataBlob], { type: "image/svg+xml" }) as ExtBlob;
}
this.objUrl = URL.createObjectURL(dataBlob);
}
let opts = this.props.opts;
+14
View File
@@ -0,0 +1,14 @@
.media-renderer {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding-top: var(--termpad);
video {
object-fit: contain;
object-position: center;
height: 100%;
width: auto;
}
}
+60
View File
@@ -0,0 +1,60 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as React from "react";
import * as mobx from "mobx";
import * as mobxReact from "mobx-react";
import * as util from "@/util/util";
import { GlobalModel } from "@/models";
import "./media.less";
@mobxReact.observer
class SimpleMediaRenderer extends React.Component<
{ data: ExtBlob; context: RendererContext; opts: RendererOpts; savedHeight: number; lineState: LineStateType },
{}
> {
objUrl: string = null;
componentWillUnmount() {
if (this.objUrl != null) {
URL.revokeObjectURL(this.objUrl);
}
}
render() {
let dataBlob = this.props.data;
if (dataBlob == null || dataBlob.notFound) {
return (
<div className="media-renderer" style={{ fontSize: this.props.opts.termFontSize }}>
<div className="load-error-text">
ERROR: file {dataBlob && dataBlob.name ? JSON.stringify(dataBlob.name) : ""} not found
</div>
</div>
);
}
let fileUrl = this.props.lineState["wave:fileurl"];
if (util.isBlank(fileUrl)) {
return (
<div className="media-renderer" style={{ fontSize: this.props.opts.termFontSize }}>
<div className="load-error-text">
ERROR: no fileurl found (please use `mediaview` to view media files)
</div>
</div>
);
}
let fullVideoUrl = GlobalModel.getBaseHostPort() + fileUrl;
const opts = this.props.opts;
const height = opts.idealSize.height - 10;
const width = opts.maxSize.width - 10;
return (
<div className="media-renderer" style={{ height: height, width: width }}>
<video controls>
<source src={fullVideoUrl} />
</video>
</div>
);
}
}
export { SimpleMediaRenderer };
+1 -1
View File
@@ -32,7 +32,7 @@ class SimplePdfRenderer extends React.Component<
);
}
if (this.objUrl == null) {
const pdfBlob = new File([dataBlob], "test.pdf", { type: "application/pdf" });
const pdfBlob = new File([dataBlob], dataBlob.name ?? "file.pdf", { type: "application/pdf" });
this.objUrl = URL.createObjectURL(pdfBlob);
}
const opts = this.props.opts;
+11
View File
@@ -8,6 +8,7 @@ import { SimpleMustacheRenderer } from "./mustache/mustache";
import { CSVRenderer } from "./csv/csv";
import { OpenAIRenderer, OpenAIRendererModel } from "./openai/openai";
import { SimplePdfRenderer } from "./pdf/pdf";
import { SimpleMediaRenderer } from "./media/media";
import { isBlank } from "@/util/util";
import { sprintf } from "sprintf-js";
@@ -89,6 +90,16 @@ const PluginConfigs: RendererPluginType[] = [
mimeTypes: ["application/pdf"],
simpleComponent: SimplePdfRenderer,
},
{
name: "media",
rendererType: "simple",
heightType: "pixels",
dataType: "blob",
collapseType: "hide",
globalCss: null,
mimeTypes: ["video/*", "audio/*"],
simpleComponent: SimpleMediaRenderer,
},
];
class PluginModelClass {
+6 -8
View File
@@ -74,7 +74,6 @@ var BuildTime = "0"
var GlobalLock = &sync.Mutex{}
var WSStateMap = make(map[string]*scws.WSState) // clientid -> WsState
var GlobalAuthKey string
var shutdownOnce sync.Once
var ContentTypeHeaderValidRe = regexp.MustCompile(`^\w+/[\w.+-]+$`)
@@ -140,7 +139,7 @@ func HandleWs(w http.ResponseWriter, r *http.Request) {
}
state := getWSState(clientId)
if state == nil {
state = scws.MakeWSState(clientId, GlobalAuthKey)
state = scws.MakeWSState(clientId, scbase.WaveAuthKey)
state.ReplaceShell(shell)
setWSState(state)
} else {
@@ -687,7 +686,7 @@ func AuthKeyMiddleWare(next http.Handler) http.Handler {
w.Write([]byte("no x-authkey header"))
return
}
if reqAuthKey != GlobalAuthKey {
if reqAuthKey != scbase.WaveAuthKey {
w.WriteHeader(500)
w.Write([]byte("x-authkey header is invalid"))
return
@@ -707,14 +706,14 @@ func AuthKeyWrapAllowHmac(fn WebFnType) WebFnType {
w.Write([]byte("no x-authkey header"))
return
}
hmacOk, err := promptenc.ValidateUrlHmac([]byte(GlobalAuthKey), r.URL.Path, qvals)
hmacOk, err := promptenc.ValidateUrlHmac([]byte(scbase.WaveAuthKey), r.URL.Path, qvals)
if err != nil || !hmacOk {
w.WriteHeader(500)
w.Write([]byte(fmt.Sprintf("error validating hmac")))
return
}
// fallthrough (hmac is valid)
} else if reqAuthKey != GlobalAuthKey {
} else if reqAuthKey != scbase.WaveAuthKey {
w.WriteHeader(500)
w.Write([]byte("x-authkey header is invalid"))
return
@@ -733,7 +732,7 @@ func AuthKeyWrap(fn WebFnType) WebFnType {
w.Write([]byte("no x-authkey header"))
return
}
if reqAuthKey != GlobalAuthKey {
if reqAuthKey != scbase.WaveAuthKey {
w.WriteHeader(500)
w.Write([]byte("x-authkey header is invalid"))
return
@@ -890,12 +889,11 @@ func main() {
}
return
}
authKey, err := scbase.ReadWaveAuthKey()
err = scbase.InitializeWaveAuthKey()
if err != nil {
log.Printf("[error] %v\n", err)
return
}
GlobalAuthKey = authKey
err = sstore.TryMigrateUp()
if err != nil {
log.Printf("[error] migrate up: %v\n", err)
+57 -2
View File
@@ -37,6 +37,7 @@ import (
"github.com/wavetermdev/waveterm/wavesrv/pkg/comp"
"github.com/wavetermdev/waveterm/wavesrv/pkg/dbutil"
"github.com/wavetermdev/waveterm/wavesrv/pkg/pcloud"
"github.com/wavetermdev/waveterm/wavesrv/pkg/promptenc"
"github.com/wavetermdev/waveterm/wavesrv/pkg/releasechecker"
"github.com/wavetermdev/waveterm/wavesrv/pkg/remote"
"github.com/wavetermdev/waveterm/wavesrv/pkg/remote/openai"
@@ -280,6 +281,7 @@ func init() {
registerCmdFn("mdview", MarkdownViewCommand)
registerCmdFn("markdownview", MarkdownViewCommand)
registerCmdFn("pdfview", PdfViewCommand)
registerCmdFn("mediaview", MediaViewCommand)
registerCmdFn("csvview", CSVViewCommand)
}
@@ -4861,11 +4863,12 @@ func ImageViewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sc
if pk.Args[0] == "" {
return nil, fmt.Errorf("%s argument cannot be empty", GetCmdStr(pk))
}
filePath := pk.Args[0]
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_RemoteConnected)
if err != nil {
return nil, err
}
outputStr := fmt.Sprintf("%s %q", GetCmdStr(pk), pk.Args[0])
outputStr := fmt.Sprintf("%s %q", GetCmdStr(pk), filePath)
cmd, err := makeStaticCmd(ctx, GetCmdStr(pk), ids, pk.GetRawStr(), []byte(outputStr))
if err != nil {
// TODO tricky error since the command was a success, but we can't show the output
@@ -4874,7 +4877,7 @@ func ImageViewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sc
// set the line state
lineState := make(map[string]any)
lineState[sstore.LineState_Source] = "file"
lineState[sstore.LineState_File] = pk.Args[0]
lineState[sstore.LineState_File] = filePath
update, err := addLineForCmd(ctx, "/"+GetCmdStr(pk), false, ids, cmd, "image", lineState)
if err != nil {
// TODO tricky error since the command was a success, but we can't show the output
@@ -4915,6 +4918,58 @@ func PdfViewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbu
return update, nil
}
func MakeReadFileUrl(screenId string, lineId string, filePath string) (string, error) {
qvals := make(url.Values)
qvals.Set("screenid", screenId)
qvals.Set("lineid", lineId)
qvals.Set("path", filePath)
qvals.Set("nonce", uuid.New().String())
hmacStr, err := promptenc.ComputeUrlHmac([]byte(scbase.WaveAuthKey), "/api/read-file", qvals)
if err != nil {
return "", fmt.Errorf("error computing hmac-url: %v", err)
}
qvals.Set("hmac", hmacStr)
return "/api/read-file?" + qvals.Encode(), nil
}
func MediaViewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.UpdatePacket, error) {
if len(pk.Args) == 0 {
return nil, fmt.Errorf("%s requires an argument (file name)", GetCmdStr(pk))
}
// TODO more error checking on filename format?
if pk.Args[0] == "" {
return nil, fmt.Errorf("%s argument cannot be empty", GetCmdStr(pk))
}
fileName := pk.Args[0]
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen|R_RemoteConnected)
if err != nil {
return nil, err
}
outputStr := fmt.Sprintf("%s %q", GetCmdStr(pk), fileName)
cmd, err := makeStaticCmd(ctx, GetCmdStr(pk), ids, pk.GetRawStr(), []byte(outputStr))
if err != nil {
// TODO tricky error since the command was a success, but we can't show the output
return nil, err
}
// compute hmac read-file URL
readFileUrl, err := MakeReadFileUrl(ids.ScreenId, cmd.LineId, fileName)
if err != nil {
// TODO tricky error since the command was a success, but we can't show the output
return nil, fmt.Errorf("error making read-file url: %v", err)
}
// set the line state
lineState := make(map[string]any)
lineState[sstore.LineState_FileUrl] = readFileUrl
lineState[sstore.LineState_File] = fileName
update, err := addLineForCmd(ctx, "/"+GetCmdStr(pk), false, ids, cmd, "media", lineState)
if err != nil {
// TODO tricky error since the command was a success, but we can't show the output
return nil, err
}
update.AddUpdate(sstore.InteractiveUpdate(pk.Interactive))
return update, nil
}
func MarkdownViewCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.UpdatePacket, error) {
if len(pk.Args) == 0 {
return nil, fmt.Errorf("%s requires an argument (file name)", GetCmdStr(pk))
+1
View File
@@ -36,6 +36,7 @@ var BareMetaCmds = []BareMetaCmdDecl{
{"mdview", "markdownview"},
{"csvview", "csvview"},
{"pdfview", "pdfview"},
{"mediaview", "mediaview"},
}
const (
+2 -2
View File
@@ -12,7 +12,7 @@ import (
)
func ComputeUrlHmac(key []byte, baseUrl string, qvals url.Values) (string, error) {
if qvals.Has("nonce") {
if !qvals.Has("nonce") {
return "", fmt.Errorf("nonce is required for hmac")
}
if qvals.Has("hmac") {
@@ -37,7 +37,7 @@ func ValidateUrlHmac(key []byte, baseUrl string, qvalsOrig url.Values) (bool, er
qvals := copyUrlValues(qvalsOrig)
hmacStr := qvals.Get("hmac")
if hmacStr == "" {
return false, fmt.Errorf("no hmac key found"))
return false, fmt.Errorf("no hmac key found")
}
qvals.Del("hmac")
encStr := baseUrl + "?" + qvals.Encode()
+17 -10
View File
@@ -38,6 +38,9 @@ const WaveAppPathVarName = "WAVETERM_APP_PATH"
const WaveAuthKeyFileName = "waveterm.authkey"
const MShellVersion = "v0.5.0"
// initialized by InitialzeWaveAuthKey (called by main-server)
var WaveAuthKey string
var SessionDirCache = make(map[string]string)
var ScreenDirCache = make(map[string]string)
var BaseLock = &sync.Mutex{}
@@ -108,25 +111,28 @@ func MShellBinaryReader(version string, goos string, goarch string) (io.ReadClos
return fd, nil
}
func createWaveAuthKeyFile(fileName string) (string, error) {
// also sets WaveAuthKey
func createWaveAuthKeyFile(fileName string) error {
fd, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return "", err
return err
}
defer fd.Close()
keyStr := GenWaveUUID()
_, err = fd.Write([]byte(keyStr))
if err != nil {
return "", err
return err
}
return keyStr, nil
WaveAuthKey = keyStr
return nil
}
func ReadWaveAuthKey() (string, error) {
// sets WaveAuthKey
func InitializeWaveAuthKey() error {
homeDir := GetWaveHomeDir()
err := ensureDir(homeDir)
if err != nil {
return "", fmt.Errorf("cannot find/create WAVETERM_HOME directory %q", homeDir)
return fmt.Errorf("cannot find/create WAVETERM_HOME directory %q", homeDir)
}
fileName := path.Join(homeDir, WaveAuthKeyFileName)
fd, err := os.Open(fileName)
@@ -134,19 +140,20 @@ func ReadWaveAuthKey() (string, error) {
return createWaveAuthKeyFile(fileName)
}
if err != nil {
return "", fmt.Errorf("error opening wave authkey:%s: %v", fileName, err)
return fmt.Errorf("error opening wave authkey:%s: %v", fileName, err)
}
defer fd.Close()
buf, err := io.ReadAll(fd)
if err != nil {
return "", fmt.Errorf("error reading wave authkey:%s: %v", fileName, err)
return fmt.Errorf("error reading wave authkey:%s: %v", fileName, err)
}
keyStr := string(buf)
_, err = uuid.Parse(keyStr)
if err != nil {
return "", fmt.Errorf("invalid authkey:%s format: %v", fileName, err)
return fmt.Errorf("invalid authkey:%s format: %v", fileName, err)
}
return keyStr, nil
WaveAuthKey = keyStr
return nil
}
func AcquireWaveLock() (*os.File, error) {
+1
View File
@@ -63,6 +63,7 @@ const (
const (
LineState_Source = "prompt:source"
LineState_File = "prompt:file"
LineState_FileUrl = "wave:fileurl"
LineState_Min = "wave:min"
LineState_Template = "template"
LineState_Mode = "mode"