diff --git a/src/types/custom.d.ts b/src/types/custom.d.ts index 9d3383af9..466a930bd 100644 --- a/src/types/custom.d.ts +++ b/src/types/custom.d.ts @@ -797,12 +797,16 @@ declare global { }; type FileInfoType = { + type: string; name: string; size: number; modts: number; isdir: boolean; perm: number; notfound: boolean; + modestr?: string; + path?: string; + outputpos?: number; }; type ExtBlob = Blob & { diff --git a/waveshell/pkg/packet/packet.go b/waveshell/pkg/packet/packet.go index ff89de4d4..6419a1d1e 100644 --- a/waveshell/pkg/packet/packet.go +++ b/waveshell/pkg/packet/packet.go @@ -11,9 +11,11 @@ import ( "errors" "fmt" "io" + "io/fs" "os" "reflect" "sync" + "time" "github.com/wavetermdev/waveterm/waveshell/pkg/base" "github.com/wavetermdev/waveterm/waveshell/pkg/wlog" @@ -58,6 +60,7 @@ const ( WriteFileReadyPacketStr = "writefileready" // rpc-response WriteFileDonePacketStr = "writefiledone" // rpc-response FileDataPacketStr = "filedata" + FileStatPacketStr = "filestat" LogPacketStr = "log" // logging packet (sent from waveshell back to server) ShellStatePacketStr = "shellstate" @@ -112,6 +115,7 @@ func init() { TypeStrToFactory[WriteFileDonePacketStr] = reflect.TypeOf(WriteFileDonePacketType{}) TypeStrToFactory[LogPacketStr] = reflect.TypeOf(LogPacketType{}) TypeStrToFactory[ShellStatePacketStr] = reflect.TypeOf(ShellStatePacketType{}) + TypeStrToFactory[FileStatPacketStr] = reflect.TypeOf(FileStatPacketType{}) var _ RpcPacketType = (*RunPacketType)(nil) var _ RpcPacketType = (*GetCmdPacketType)(nil) @@ -379,6 +383,51 @@ func MakeReInitPacket() *ReInitPacketType { return &ReInitPacketType{Type: ReInitPacketStr} } +type FileStatPacketType struct { + Type string `json:"type"` + Name string `json:"name"` + Size int64 `json:"size"` + ModTs time.Time `json:"modts"` + IsDir bool `json:"isdir"` + Perm int `json:"perm"` + ModeStr string `json:"modestr"` + Error string `json:"error"` + Done bool `json:"done"` + RespId string `json:"respid"` + Path string `json:"path"` +} + +func (*FileStatPacketType) GetType() string { + return FileStatPacketStr +} + +func (p *FileStatPacketType) GetResponseDone() bool { + return p.Done +} + +func (p *FileStatPacketType) GetResponseId() string { + return p.RespId +} + +func MakeFileStatPacketType() *FileStatPacketType { + return &FileStatPacketType{Type: FileStatPacketStr} +} + +func MakeFileStatPacketFromFileInfo(finfo fs.FileInfo, err string, done bool) *FileStatPacketType { + resp := MakeFileStatPacketType() + resp.Error = err + resp.Done = done + + resp.IsDir = finfo.IsDir() + resp.Name = finfo.Name() + + resp.Size = finfo.Size() + resp.ModTs = finfo.ModTime() + resp.Perm = int(finfo.Mode().Perm()) + resp.ModeStr = finfo.Mode().String() + return resp +} + type StreamFilePacketType struct { Type string `json:"type"` ReqId string `json:"reqid"` diff --git a/wavesrv/cmd/main-server.go b/wavesrv/cmd/main-server.go index f106ac4d4..3e8b7f196 100644 --- a/wavesrv/cmd/main-server.go +++ b/wavesrv/cmd/main-server.go @@ -677,6 +677,40 @@ func HandleRunCommand(w http.ResponseWriter, r *http.Request) { WriteJsonSuccess(w, update) } +func CheckIsDir(dirHandler http.Handler, fileHandler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + configPath := r.URL.Path + configAbsPath, err := filepath.Abs(configPath) + if err != nil { + w.WriteHeader(500) + w.Write([]byte(fmt.Sprintf("error getting absolute path", err))) + return + } + configBaseDir := path.Join(scbase.GetWaveHomeDir(), "config") + configFullPath := path.Join(scbase.GetWaveHomeDir(), configAbsPath) + if !strings.HasPrefix(configFullPath, configBaseDir) { + w.WriteHeader(500) + w.Write([]byte(fmt.Sprintf("error: path is not in config folder"))) + return + } + fstat, err := os.Stat(configFullPath) + if errors.Is(err, fs.ErrNotExist) { + w.WriteHeader(404) + w.Write([]byte(fmt.Sprintf("file not found: ", configAbsPath))) + return + } else if err != nil { + w.WriteHeader(500) + w.Write([]byte(fmt.Sprintf("file stat err", err))) + return + } + if fstat.IsDir() { + AuthKeyMiddleWare(dirHandler).ServeHTTP(w, r) + } else { + AuthKeyMiddleWare(fileHandler).ServeHTTP(w, r) + } + }) +} + func AuthKeyMiddleWare(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { reqAuthKey := r.Header.Get("X-AuthKey") @@ -857,6 +891,39 @@ func doShutdown(reason string) { }) } +func configDirHandler(w http.ResponseWriter, r *http.Request) { + log.Printf("running?") + configPath := r.URL.Path + configFullPath := path.Join(scbase.GetWaveHomeDir(), configPath) + dirFile, err := os.Open(configFullPath) + if err != nil { + w.WriteHeader(500) + w.Write([]byte(fmt.Sprintf("error opening specified dir: ", err))) + return + } + entries, err := dirFile.Readdir(0) + if err != nil { + w.WriteHeader(500) + w.Write([]byte(fmt.Sprintf("error getting files: ", err))) + return + } + var files []*packet.FileStatPacketType + for index := 0; index < len(entries); index++ { + curEntry := entries[index] + curFile := packet.MakeFileStatPacketFromFileInfo(curEntry, "", false) + files = append(files, curFile) + } + dirListJson, err := json.Marshal(files) + if err != nil { + w.WriteHeader(500) + w.Write([]byte(fmt.Sprintf("json err: ", err))) + return + } + w.WriteHeader(200) + w.Header().Set("Content-Type", "application/json") + w.Write(dirListJson) +} + func main() { scbase.BuildTime = BuildTime scbase.WaveVersion = WaveVersion @@ -953,7 +1020,9 @@ func main() { gr.HandleFunc("/api/write-file", AuthKeyWrap(HandleWriteFile)).Methods("POST") configPath := path.Join(scbase.GetWaveHomeDir(), "config") + "/" log.Printf("[wave] config path: %q\n", configPath) - gr.PathPrefix("/config/").Handler(AuthKeyMiddleWare(http.StripPrefix("/config/", http.FileServer(http.Dir(configPath))))) + isFileHandler := http.StripPrefix("/config/", http.FileServer(http.Dir(configPath))) + isDirHandler := http.HandlerFunc(configDirHandler) + gr.PathPrefix("/config/").Handler(CheckIsDir(isDirHandler, isFileHandler)) serverAddr := MainServerAddr if scbase.IsDevMode() {