Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion emain/emain-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,10 @@ export function shFrameNavHandler(event: Electron.Event<Electron.WebContentsWill
}
if (
event.frame.name == "pdfview" &&
(url.startsWith("blob:file:///") || url.startsWith(getWebServerEndpoint() + "/wave/stream-file?"))
(url.startsWith("blob:file:///") ||
url.startsWith(getWebServerEndpoint() + "/wave/stream-file?") ||
url.startsWith(getWebServerEndpoint() + "/wave/stream-file/") ||
url.startsWith(getWebServerEndpoint() + "/wave/stream-local-file?"))
) {
// allowed
return;
Expand Down
4 changes: 3 additions & 1 deletion emain/emain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,9 @@ electron.ipcMain.on("webview-image-contextmenu", (event: electron.IpcMainEvent,
});

electron.ipcMain.on("download", (event, payload) => {
const streamingUrl = getWebServerEndpoint() + "/wave/stream-file?path=" + encodeURIComponent(payload.filePath);
const baseName = encodeURIComponent(path.basename(payload.filePath));
const streamingUrl =
getWebServerEndpoint() + "/wave/stream-file/" + baseName + "?path=" + encodeURIComponent(payload.filePath);
event.sender.downloadURL(streamingUrl);
});

Expand Down
5 changes: 2 additions & 3 deletions frontend/app/block/blockframe.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ const BlockFrame_Header = ({
icon: "link-slash",
title: "wsh is not installed for this connection",
};
const showNoWshButton = manageConnection && wshProblem && !util.isBlank(connName) && !connName.startsWith("aws:");

return (
<div
Expand All @@ -263,9 +264,7 @@ const BlockFrame_Header = ({
changeConnModalAtom={changeConnModalAtom}
/>
)}
{manageConnection && wshProblem && (
<IconButton decl={wshInstallButton} className="block-frame-header-iconbutton" />
)}
{showNoWshButton && <IconButton decl={wshInstallButton} className="block-frame-header-iconbutton" />}
<div className="block-frame-textelems-wrapper">{headerTextElems}</div>
<div className="block-frame-end-icons">{endIconsElem}</div>
</div>
Expand Down
18 changes: 6 additions & 12 deletions frontend/app/element/markdown-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { RpcApi } from "@/app/store/wshclientapi";
import { TabRpcClient } from "@/app/store/wshrpcutil";
import { getWebServerEndpoint } from "@/util/endpoints";
import { isBlank, makeConnRoute } from "@/util/util";
import { formatRemoteUri } from "@/util/waveutil";
import parseSrcSet from "parse-srcset";

export type MarkdownContentBlockType = {
Expand Down Expand Up @@ -158,19 +158,13 @@ export const resolveRemoteFile = async (filepath: string, resolveOpts: MarkdownR
if (!filepath || filepath.startsWith("http://") || filepath.startsWith("https://")) {
return filepath;
}

try {
const route = makeConnRoute(resolveOpts.connName);
const fileInfo = await RpcApi.RemoteFileJoinCommand(TabRpcClient, [resolveOpts.baseDir, filepath], {
route: route,
});

const baseDirUri = formatRemoteUri(resolveOpts.baseDir, resolveOpts.connName);
const fileInfo = await RpcApi.FileJoinCommand(TabRpcClient, [baseDirUri, filepath]);
const remoteUri = formatRemoteUri(fileInfo.path, resolveOpts.connName);
console.log("markdown resolve", resolveOpts, filepath, "=>", baseDirUri, remoteUri);
const usp = new URLSearchParams();
usp.set("path", fileInfo.path);
if (!isBlank(resolveOpts.connName)) {
usp.set("connection", resolveOpts.connName);
}

usp.set("path", remoteUri);
return getWebServerEndpoint() + "/wave/stream-file?" + usp.toString();
} catch (err) {
console.warn("Failed to resolve remote file:", filepath, err);
Expand Down
20 changes: 12 additions & 8 deletions frontend/app/modals/conntypeahead.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ const ChangeConnectionBlockModal = React.memo(
const connStatusMap = new Map<string, ConnStatus>();
const fullConfig = jotai.useAtomValue(atoms.fullConfigAtom);
let filterOutNowsh = util.useAtomValueSafe(viewModel.filterOutNowsh) ?? true;
const showS3 = util.useAtomValueSafe(viewModel.showS3) ?? false;

let maxActiveConnNum = 1;
for (const conn of allConnStatus) {
Expand Down Expand Up @@ -436,14 +437,17 @@ const ChangeConnectionBlockModal = React.memo(
fullConfig,
filterOutNowsh
);
const s3Suggestions = getS3Suggestions(
s3List,
connection,
connSelected,
connStatusMap,
fullConfig,
filterOutNowsh
);
let s3Suggestions: SuggestionConnectionScope = null;
if (showS3) {
s3Suggestions = getS3Suggestions(
s3List,
connection,
connSelected,
connStatusMap,
fullConfig,
filterOutNowsh
);
}
const connectionsEditItem = getConnectionsEditItem(changeConnModalAtom, connSelected);
const disconnectItem = getDisconnectItem(connection, connStatusMap);
const newConnectionSuggestionItem = getNewConnectionSuggestionItem(
Expand Down
6 changes: 4 additions & 2 deletions frontend/app/view/preview/directorypreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import { ContextMenuModel } from "@/app/store/contextmenu";
import { PLATFORM, atoms, createBlock, getApi, globalStore } from "@/app/store/global";
import { RpcApi } from "@/app/store/wshclientapi";
import { TabRpcClient } from "@/app/store/wshrpcutil";
import { formatRemoteUri, type PreviewModel } from "@/app/view/preview/preview";
import { type PreviewModel } from "@/app/view/preview/preview";
import { checkKeyPressed, isCharacterKeyEvent } from "@/util/keyutil";
import { fireAndForget, isBlank, makeNativeLabel } from "@/util/util";
import { formatRemoteUri } from "@/util/waveutil";
import { offset, useDismiss, useFloating, useInteractions } from "@floating-ui/react";
import {
Column,
Expand Down Expand Up @@ -575,7 +576,8 @@ function TableBody({
{
label: "Download File",
click: () => {
getApi().downloadFile(normPath);
const remoteUri = formatRemoteUri(finfo.path, conn);
getApi().downloadFile(remoteUri);
},
},
{
Expand Down
24 changes: 8 additions & 16 deletions frontend/app/view/preview/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
makeNativeLabel,
stringToBase64,
} from "@/util/util";
import { formatRemoteUri } from "@/util/waveutil";
import { Monaco } from "@monaco-editor/react";
import clsx from "clsx";
import { Atom, atom, Getter, PrimitiveAtom, useAtom, useAtomValue, useSetAtom, WritableAtom } from "jotai";
Expand Down Expand Up @@ -180,6 +181,8 @@ export class PreviewModel implements ViewModel {
directoryKeyDownHandler: (waveEvent: WaveKeyboardEvent) => boolean;
codeEditKeyDownHandler: (waveEvent: WaveKeyboardEvent) => boolean;

showS3 = atom(true);

constructor(blockId: string, nodeModel: BlockNodeModel) {
this.viewType = "preview";
this.blockId = blockId;
Expand Down Expand Up @@ -936,13 +939,14 @@ function StreamingPreview({ model }: SpecializedViewProps) {
const conn = useAtomValue(model.connection);
const fileInfo = useAtomValue(model.statFile);
const filePath = fileInfo.path;
const remotePath = formatRemoteUri(filePath, conn);
const usp = new URLSearchParams();
usp.set("path", filePath);
usp.set("path", remotePath);
if (conn != null) {
usp.set("connection", conn);
}
const streamingUrl = getWebServerEndpoint() + "/wave/stream-file?" + usp.toString();
if (fileInfo.mimetype == "application/pdf") {
const streamingUrl = `${getWebServerEndpoint()}/wave/stream-file?${usp.toString()}`;
if (fileInfo.mimetype === "application/pdf") {
return (
<div className="view-preview view-preview-pdf">
<iframe src={streamingUrl} width="100%" height="100%" name="pdfview" />
Expand Down Expand Up @@ -1304,16 +1308,4 @@ const ErrorOverlay = memo(({ errorMsg, resetOverlay }: { errorMsg: ErrorMsg; res
);
});

function formatRemoteUri(path: string, connection: string): string {
connection = connection ?? "local";
// TODO: We need a better way to handle s3 paths
let retVal: string;
if (connection.startsWith("aws:")) {
retVal = `${connection}:s3://${path ?? ""}`;
} else {
retVal = `wsh://${connection}/${path}`;
}
return retVal;
}

export { formatRemoteUri, PreviewView };
export { PreviewView };
3 changes: 2 additions & 1 deletion frontend/app/view/term/termsticker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,8 @@ function TermSticker({ sticker, config }: { sticker: StickerType; config: Sticke
if (sticker.imgsrc == null) {
return null;
}
const streamingUrl = getWebServerEndpoint() + "/wave/stream-file?path=" + encodeURIComponent(sticker.imgsrc);
const streamingUrl =
getWebServerEndpoint() + "/wave/stream-local-file?path=" + encodeURIComponent(sticker.imgsrc);
return (
<div className="term-sticker term-sticker-image" style={style} onClick={clickHandler}>
<img src={streamingUrl} />
Expand Down
3 changes: 3 additions & 0 deletions frontend/types/custom.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,9 @@ declare global {
// If true, filters out 'nowsh' connections (when managing connections)
filterOutNowsh?: jotai.Atom<boolean>;

// if true, show s3 connections in picker
showS3?: jotai.Atom<boolean>;

// If true, removes padding inside the block content area.
noPadding?: jotai.Atom<boolean>;

Expand Down
16 changes: 15 additions & 1 deletion frontend/util/waveutil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import { generate as generateCSS, parse as parseCSS, walk as walkCSS } from "css

function encodeFileURL(file: string) {
const webEndpoint = getWebServerEndpoint();
return webEndpoint + `/wave/stream-file?path=${encodeURIComponent(file)}&no404=1`;
const fileUri = formatRemoteUri(file, "local");
const rtn = webEndpoint + `/wave/stream-file?path=${encodeURIComponent(fileUri)}&no404=1`;
return rtn;
}

export function processBackgroundUrls(cssText: string): string {
Expand Down Expand Up @@ -86,3 +88,15 @@ export function computeBgStyleFromMeta(meta: MetaType, defaultOpacity: number =
return null;
}
}

export function formatRemoteUri(path: string, connection: string): string {
connection = connection ?? "local";
// TODO: We need a better way to handle s3 paths
let retVal: string;
if (connection.startsWith("aws:")) {
retVal = `${connection}:s3://${path ?? ""}`;
} else {
retVal = `wsh://${connection}/${path}`;
}
return retVal;
}
Comment on lines +92 to +102
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add path sanitization for remote URI construction
The formatRemoteUri function delegates to custom schemes (aws: vs wsh://), but does not currently sanitize or validate path. Unchecked input might cause incorrect or unintended URIs. Consider adding path checks (removing illegal characters, validating emptiness, etc.) to reinforce correctness and security.

13 changes: 3 additions & 10 deletions pkg/remote/fileshare/s3fs/s3fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -774,20 +774,13 @@ func (c S3Client) Delete(ctx context.Context, conn *connparse.Connection, recurs

func (c S3Client) Join(ctx context.Context, conn *connparse.Connection, parts ...string) (*wshrpc.FileInfo, error) {
var joinParts []string
if conn.Host == "" || conn.Host == fspath.Separator {
if conn.Path == "" || conn.Path == fspath.Separator {
joinParts = parts
} else {
joinParts = append([]string{conn.Path}, parts...)
}
} else if conn.Path == "" || conn.Path == "/" {
joinParts = append([]string{conn.Host}, parts...)
if conn.Path == "" || conn.Path == fspath.Separator {
joinParts = parts
} else {
joinParts = append([]string{conn.Host, conn.Path}, parts...)
joinParts = append([]string{conn.Path}, parts...)
}

conn.Path = fspath.Join(joinParts...)

return c.Stat(ctx, conn)
}

Expand Down
39 changes: 30 additions & 9 deletions pkg/web/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/wavetermdev/waveterm/pkg/docsite"
"github.com/wavetermdev/waveterm/pkg/filestore"
"github.com/wavetermdev/waveterm/pkg/panichandler"
"github.com/wavetermdev/waveterm/pkg/remote/fileshare"
"github.com/wavetermdev/waveterm/pkg/schema"
"github.com/wavetermdev/waveterm/pkg/service"
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
Expand Down Expand Up @@ -251,6 +252,10 @@ func handleRemoteStreamFile(w http.ResponseWriter, req *http.Request, conn strin
route := wshutil.MakeConnectionRouteId(conn)
rpcOpts := &wshrpc.RpcOpts{Route: route, Timeout: 60 * 1000}
rtnCh := wshclient.RemoteStreamFileCommand(client, streamFileData, rpcOpts)
return handleRemoteStreamFileFromCh(w, req, path, rtnCh, rpcOpts.StreamCancelFn, no404)
}

func handleRemoteStreamFileFromCh(w http.ResponseWriter, req *http.Request, path string, rtnCh <-chan wshrpc.RespOrErrorUnion[wshrpc.FileData], streamCancelFn func(), no404 bool) error {
firstPk := true
var fileInfo *wshrpc.FileInfo
loopDone := false
Expand All @@ -265,7 +270,9 @@ func handleRemoteStreamFile(w http.ResponseWriter, req *http.Request, conn strin
for {
select {
case <-ctx.Done():
rpcOpts.StreamCancelFn()
if streamCancelFn != nil {
streamCancelFn()
}
return ctx.Err()
case respUnion, ok := <-rtnCh:
if !ok {
Expand Down Expand Up @@ -311,6 +318,16 @@ func handleRemoteStreamFile(w http.ResponseWriter, req *http.Request, conn strin
}
}

func handleStreamLocalFile(w http.ResponseWriter, r *http.Request) {
path := r.URL.Query().Get("path")
if path == "" {
http.Error(w, "path is required", http.StatusBadRequest)
return
}
no404 := r.URL.Query().Get("no404")
handleLocalStreamFile(w, r, path, no404 != "")
}

func handleStreamFile(w http.ResponseWriter, r *http.Request) {
conn := r.URL.Query().Get("connection")
if conn == "" {
Expand All @@ -322,14 +339,16 @@ func handleStreamFile(w http.ResponseWriter, r *http.Request) {
return
}
no404 := r.URL.Query().Get("no404")
if conn == wshrpc.LocalConnName {
handleLocalStreamFile(w, r, path, no404 != "")
} else {
err := handleRemoteStreamFile(w, r, conn, path, no404 != "")
if err != nil {
log.Printf("error streaming remote file %q %q: %v\n", conn, path, err)
http.Error(w, fmt.Sprintf("error streaming file: %v", err), http.StatusInternalServerError)
}
data := wshrpc.FileData{
Info: &wshrpc.FileInfo{
Path: path,
},
}
rtnCh := fileshare.ReadStream(r.Context(), data)
err := handleRemoteStreamFileFromCh(w, r, path, rtnCh, nil, no404 != "")
if err != nil {
log.Printf("error streaming file %q %q: %v\n", conn, path, err)
http.Error(w, fmt.Sprintf("error streaming file: %v", err), http.StatusInternalServerError)
}
}

Expand Down Expand Up @@ -423,7 +442,9 @@ const schemaPrefix = "/schema/"
// blocking
func RunWebServer(listener net.Listener) {
gr := mux.NewRouter()
gr.HandleFunc("/wave/stream-local-file", WebFnWrap(WebFnOpts{AllowCaching: true}, handleStreamLocalFile))
gr.HandleFunc("/wave/stream-file", WebFnWrap(WebFnOpts{AllowCaching: true}, handleStreamFile))
gr.PathPrefix("/wave/stream-file/").HandlerFunc(WebFnWrap(WebFnOpts{AllowCaching: true}, handleStreamFile))
gr.HandleFunc("/wave/file", WebFnWrap(WebFnOpts{AllowCaching: false}, handleWaveFile))
gr.HandleFunc("/wave/service", WebFnWrap(WebFnOpts{JsonErrors: true}, handleService))
gr.HandleFunc("/vdom/{uuid}/{path:.*}", WebFnWrap(WebFnOpts{AllowCaching: true}, handleVDom))
Expand Down
Loading