package rdpdr import ( "encoding/binary" "log" "path" "strings" "time" "unicode/utf16" ) const ( irpCreate uint32 = 0x00000000 irpClose uint32 = 0x00000002 irpRead uint32 = 0x00000003 irpWrite uint32 = 0x00000004 irpQueryInfo uint32 = 0x00000005 irpSetInfo uint32 = 0x00000006 irpDeviceCtrl uint32 = 0x0000000e irpQueryVolume uint32 = 0x0000000a irpDirControl uint32 = 0x0000000c irpLockControl uint32 = 0x00000011 irpMnQueryDir uint32 = 0x00000001 irpMnNotifyChangeDir uint32 = 0x00000002 statusSuccess uint32 = 0x00000000 statusNoMoreFiles uint32 = 0x80000006 statusAccessDenied uint32 = 0xc0000022 statusObjectNameNotFound uint32 = 0xc0000034 statusNoSuchFile uint32 = 0xc000000f statusFileIsDirectory uint32 = 0xc00000ba statusNotSupported uint32 = 0xc00000bb statusInvalidDeviceReq uint32 = 0xc0000010 statusInvalidParameter uint32 = 0xc000000d statusEndOfFile uint32 = 0xc0000011 fileOpen uint32 = 0x00000001 fileCreate uint32 = 0x00000002 fileOpenIf uint32 = 0x00000003 fileOverwrite uint32 = 0x00000004 fileOverwriteIf uint32 = 0x00000005 fileDirectoryFile uint32 = 0x00000001 fileNonDirectoryFile uint32 = 0x00000040 fileDirectoryInformation uint32 = 1 fileFullDirectoryInformation uint32 = 2 fileBothDirectoryInformation uint32 = 3 fileBasicInformation uint32 = 4 fileStandardInformation uint32 = 5 fileInternalInformation uint32 = 6 fileEaInformation uint32 = 7 fileAccessInformation uint32 = 8 fileNameInformation uint32 = 9 fileNamesInformation uint32 = 12 filePositionInformation uint32 = 14 fileModeInformation uint32 = 16 fileAlignmentInformation uint32 = 17 fileEndOfFileInformation uint32 = 20 fileNetworkOpenInformation uint32 = 34 fileAttributeTagInformation uint32 = 35 fileFsVolumeInformation uint32 = 1 fileFsSizeInformation uint32 = 3 fileFsDeviceInformation uint32 = 4 fileFsAttributeInformation uint32 = 5 fileFsFullSizeInformation uint32 = 7 fileAttrDirectory uint32 = 0x00000010 fileAttrNormal uint32 = 0x00000080 fileAttrArchive uint32 = 0x00000020 fileOpened byte = 0x01 fileCreated byte = 0x02 fileOverwritten byte = 0x03 downloadDirName = "Download" uploadDirName = "Upload" ) const windowsFiletimeOffset = 116444736000000000 type ioRequest struct { deviceID uint32 fileID uint32 completionID uint32 majorFn uint32 minorFn uint32 payload []byte } type openFile struct { path string name string isDir bool data []byte written bool dirEntries []dirEntry dirIndex int } type dirEntry struct { name string isDir bool size uint64 } func (h *Handler) handleIORequest(body []byte) { if len(body) < 20 { return } req := ioRequest{ deviceID: binary.LittleEndian.Uint32(body[0:4]), fileID: binary.LittleEndian.Uint32(body[4:8]), completionID: binary.LittleEndian.Uint32(body[8:12]), majorFn: binary.LittleEndian.Uint32(body[12:16]), minorFn: binary.LittleEndian.Uint32(body[16:20]), payload: body[20:], } log.Printf("[rdpdr] io request device=%d file=%d completion=%d major=0x%x minor=0x%x payload=%d", req.deviceID, req.fileID, req.completionID, req.majorFn, req.minorFn, len(req.payload)) switch req.majorFn { case irpCreate: h.handleCreate(req) case irpClose: h.handleClose(req) case irpRead: h.handleRead(req) case irpWrite: h.handleWrite(req) case irpQueryInfo: h.handleQueryInfo(req) case irpSetInfo: h.handleSetInfo(req) case irpDeviceCtrl: h.handleDeviceControl(req) case irpQueryVolume: h.handleQueryVolume(req) case irpDirControl: if req.minorFn == irpMnQueryDir { h.handleQueryDirectory(req) return } if req.minorFn == irpMnNotifyChangeDir { h.handleNotifyChangeDirectory(req) return } h.sendIOCompletion(req.deviceID, req.completionID, statusNotSupported, nil) case irpLockControl: h.handleLockControl(req) default: h.sendIOCompletion(req.deviceID, req.completionID, statusNotSupported, nil) } } func (h *Handler) handleCreate(req ioRequest) { var out [5]byte if len(req.payload) < 32 { h.sendIOCompletion(req.deviceID, req.completionID, statusInvalidParameter, out[:]) return } disposition := binary.LittleEndian.Uint32(req.payload[20:24]) options := binary.LittleEndian.Uint32(req.payload[24:28]) pathLen := binary.LittleEndian.Uint32(req.payload[28:32]) if uint32(len(req.payload)) < 32+pathLen { h.sendIOCompletion(req.deviceID, req.completionID, statusInvalidParameter, out[:]) return } vpath := normalizeVirtualPath(decodeUTF16(req.payload[32 : 32+pathLen])) isDir := options&fileDirectoryFile != 0 kind, name := classifyVirtualPath(vpath) if kind == "missing" { h.sendIOCompletion(req.deviceID, req.completionID, statusObjectNameNotFound, out[:]) return } switch kind { case "root", "download", "upload": isDir = true case "file", "upload_file": // Explorer 拖拽复制时可能带上目录相关 CreateOptions,虚拟盘文件必须按真实文件打开。// Logan.Li isDir = false } var initialData []byte if kind == "upload_file" { data, ok := h.getUpload(name) if !ok { h.sendIOCompletion(req.deviceID, req.completionID, statusObjectNameNotFound, out[:]) return } initialData = data } if isDir && options&fileNonDirectoryFile != 0 { h.sendIOCompletion(req.deviceID, req.completionID, statusFileIsDirectory, out[:]) return } if kind == "file" && disposition == fileOpen { // 虚拟 Download 目录只负责接收远端写入,读取历史文件留给上传目录实现。// Logan.Li h.sendIOCompletion(req.deviceID, req.completionID, statusObjectNameNotFound, out[:]) return } info := fileOpened if disposition == fileCreate { info = fileCreated } if disposition == fileOverwrite || disposition == fileOverwriteIf { info = fileOverwritten } fileID := h.storeOpenFile(&openFile{path: vpath, name: name, isDir: isDir, data: initialData}) binary.LittleEndian.PutUint32(out[0:4], fileID) out[4] = info log.Printf("[rdpdr] create path=%q fileID=%d dir=%t", vpath, fileID, isDir) h.sendIOCompletion(req.deviceID, req.completionID, statusSuccess, out[:]) } func (h *Handler) handleClose(req ioRequest) { file, ok := h.removeOpenFile(req.fileID) if ok && !file.isDir && file.written && len(file.data) > 0 && h.onDownload != nil { data := append([]byte(nil), file.data...) h.onDownload(Download{Name: file.name, Data: data}) log.Printf("[rdpdr] download captured name=%s size=%d", file.name, len(data)) } var out [5]byte h.sendIOCompletion(req.deviceID, req.completionID, statusSuccess, out[:]) } func (h *Handler) handleRead(req ioRequest) { if len(req.payload) < 20 { h.sendIOCompletion(req.deviceID, req.completionID, statusInvalidParameter, nil) return } length := int(binary.LittleEndian.Uint32(req.payload[0:4])) offset := int(binary.LittleEndian.Uint64(req.payload[4:12])) file, ok := h.getOpenFile(req.fileID) if !ok || file.isDir { log.Printf("[rdpdr] read missing file=%d offset=%d length=%d", req.fileID, offset, length) h.sendIOCompletion(req.deviceID, req.completionID, statusNoSuchFile, nil) return } if length <= 0 { var out [4]byte log.Printf("[rdpdr] read zero file=%d path=%q offset=%d", req.fileID, file.path, offset) h.sendIOCompletion(req.deviceID, req.completionID, statusSuccess, out[:]) return } if offset >= len(file.data) { var out [4]byte // Explorer 复制虚拟盘文件时更稳定地接受 0 字节成功读作为 EOF。// Logan.Li log.Printf("[rdpdr] read eof file=%d path=%q offset=%d size=%d", req.fileID, file.path, offset, len(file.data)) h.sendIOCompletion(req.deviceID, req.completionID, statusSuccess, out[:]) return } end := offset + length if end > len(file.data) { end = len(file.data) } out := make([]byte, 4+end-offset) binary.LittleEndian.PutUint32(out[0:4], uint32(end-offset)) copy(out[4:], file.data[offset:end]) log.Printf("[rdpdr] read file=%d path=%q offset=%d requested=%d returned=%d size=%d", req.fileID, file.path, offset, length, end-offset, len(file.data)) h.sendIOCompletion(req.deviceID, req.completionID, statusSuccess, out) } func (h *Handler) handleWrite(req ioRequest) { if len(req.payload) < 32 { h.sendIOCompletion(req.deviceID, req.completionID, statusInvalidParameter, nil) return } length := int(binary.LittleEndian.Uint32(req.payload[0:4])) offset := int(binary.LittleEndian.Uint64(req.payload[4:12])) chunk := req.payload[32:] if length > len(chunk) { length = len(chunk) } file, ok := h.getOpenFile(req.fileID) if !ok || file.isDir { h.sendIOCompletion(req.deviceID, req.completionID, statusNoSuchFile, nil) return } need := offset + length if need > len(file.data) { next := make([]byte, need) copy(next, file.data) file.data = next } copy(file.data[offset:need], chunk[:length]) file.written = true var out [5]byte binary.LittleEndian.PutUint32(out[0:4], uint32(length)) h.sendIOCompletion(req.deviceID, req.completionID, statusSuccess, out[:]) } func (h *Handler) handleQueryInfo(req ioRequest) { if len(req.payload) < 4 { h.sendIOCompletion(req.deviceID, req.completionID, statusInvalidParameter, nil) return } infoClass := binary.LittleEndian.Uint32(req.payload[0:4]) file, ok := h.getOpenFile(req.fileID) if !ok { h.sendIOCompletion(req.deviceID, req.completionID, statusNoSuchFile, nil) return } attrs := fileAttrArchive if file.isDir { attrs = fileAttrDirectory } size := uint64(len(file.data)) log.Printf("[rdpdr] query info file=%d class=%d path=%q dir=%t size=%d", req.fileID, infoClass, file.path, file.isDir, size) switch infoClass { case fileBasicInformation: out := make([]byte, 40) binary.LittleEndian.PutUint32(out[0:4], 36) writeFileTimes(out[4:36]) binary.LittleEndian.PutUint32(out[36:40], attrs) h.sendIOCompletion(req.deviceID, req.completionID, statusSuccess, out) case fileStandardInformation: out := make([]byte, 26) binary.LittleEndian.PutUint32(out[0:4], 22) binary.LittleEndian.PutUint64(out[4:12], align4096(size)) binary.LittleEndian.PutUint64(out[12:20], size) binary.LittleEndian.PutUint32(out[20:24], 1) if file.isDir { out[25] = 1 } h.sendIOCompletion(req.deviceID, req.completionID, statusSuccess, out) case fileAttributeTagInformation: out := make([]byte, 12) binary.LittleEndian.PutUint32(out[0:4], 8) binary.LittleEndian.PutUint32(out[4:8], attrs) h.sendIOCompletion(req.deviceID, req.completionID, statusSuccess, out) case fileInternalInformation: out := make([]byte, 12) binary.LittleEndian.PutUint32(out[0:4], 8) binary.LittleEndian.PutUint64(out[4:12], uint64(req.fileID)) h.sendIOCompletion(req.deviceID, req.completionID, statusSuccess, out) case fileEaInformation: out := make([]byte, 8) binary.LittleEndian.PutUint32(out[0:4], 4) h.sendIOCompletion(req.deviceID, req.completionID, statusSuccess, out) case fileAccessInformation, fileModeInformation, fileAlignmentInformation: out := make([]byte, 8) binary.LittleEndian.PutUint32(out[0:4], 4) h.sendIOCompletion(req.deviceID, req.completionID, statusSuccess, out) case fileNameInformation: name := encodeUTF16(fileDisplayName(file)) out := make([]byte, 8+len(name)) binary.LittleEndian.PutUint32(out[0:4], uint32(4+len(name))) binary.LittleEndian.PutUint32(out[4:8], uint32(len(name))) copy(out[8:], name) h.sendIOCompletion(req.deviceID, req.completionID, statusSuccess, out) case filePositionInformation: out := make([]byte, 12) binary.LittleEndian.PutUint32(out[0:4], 8) h.sendIOCompletion(req.deviceID, req.completionID, statusSuccess, out) case fileNetworkOpenInformation: out := make([]byte, 60) binary.LittleEndian.PutUint32(out[0:4], 56) writeFileTimes(out[4:36]) binary.LittleEndian.PutUint64(out[36:44], align4096(size)) binary.LittleEndian.PutUint64(out[44:52], size) binary.LittleEndian.PutUint32(out[52:56], attrs) h.sendIOCompletion(req.deviceID, req.completionID, statusSuccess, out) default: log.Printf("[rdpdr] query info unsupported class=%d file=%d path=%q", infoClass, req.fileID, file.path) h.sendIOCompletion(req.deviceID, req.completionID, statusNotSupported, nil) } } func (h *Handler) handleSetInfo(req ioRequest) { var out [4]byte h.sendIOCompletion(req.deviceID, req.completionID, statusSuccess, out[:]) } func (h *Handler) handleQueryVolume(req ioRequest) { if len(req.payload) < 4 { h.sendIOCompletion(req.deviceID, req.completionID, statusInvalidParameter, nil) return } infoClass := binary.LittleEndian.Uint32(req.payload[0:4]) switch infoClass { case fileFsVolumeInformation: label := encodeUTF16(h.driveName) out := make([]byte, 21+len(label)) binary.LittleEndian.PutUint32(out[0:4], uint32(17+len(label))) binary.LittleEndian.PutUint64(out[4:12], filetimeNow()) binary.LittleEndian.PutUint32(out[12:16], 0x41494f50) binary.LittleEndian.PutUint32(out[16:20], uint32(len(label))) copy(out[21:], label) h.sendIOCompletion(req.deviceID, req.completionID, statusSuccess, out) case fileFsSizeInformation: out := make([]byte, 28) binary.LittleEndian.PutUint32(out[0:4], 24) binary.LittleEndian.PutUint64(out[4:12], 1024*1024) binary.LittleEndian.PutUint64(out[12:20], 1024*1024) binary.LittleEndian.PutUint32(out[20:24], 8) binary.LittleEndian.PutUint32(out[24:28], 512) h.sendIOCompletion(req.deviceID, req.completionID, statusSuccess, out) case fileFsDeviceInformation: out := make([]byte, 12) binary.LittleEndian.PutUint32(out[0:4], 8) binary.LittleEndian.PutUint32(out[4:8], 0x00000007) h.sendIOCompletion(req.deviceID, req.completionID, statusSuccess, out) case fileFsAttributeInformation: fsName := encodeUTF16("AIOPS") out := make([]byte, 16+len(fsName)) binary.LittleEndian.PutUint32(out[0:4], uint32(12+len(fsName))) binary.LittleEndian.PutUint32(out[4:8], 0x00000007) binary.LittleEndian.PutUint32(out[8:12], 255) binary.LittleEndian.PutUint32(out[12:16], uint32(len(fsName))) copy(out[16:], fsName) h.sendIOCompletion(req.deviceID, req.completionID, statusSuccess, out) case fileFsFullSizeInformation: out := make([]byte, 36) binary.LittleEndian.PutUint32(out[0:4], 32) binary.LittleEndian.PutUint64(out[4:12], 1024*1024) binary.LittleEndian.PutUint64(out[12:20], 1024*1024) binary.LittleEndian.PutUint64(out[20:28], 1024*1024) binary.LittleEndian.PutUint32(out[28:32], 8) binary.LittleEndian.PutUint32(out[32:36], 512) h.sendIOCompletion(req.deviceID, req.completionID, statusSuccess, out) default: h.sendIOCompletion(req.deviceID, req.completionID, statusNotSupported, nil) } } func (h *Handler) handleDeviceControl(req ioRequest) { var out [4]byte ioctl := uint32(0) if len(req.payload) >= 12 { ioctl = binary.LittleEndian.Uint32(req.payload[8:12]) } // 资源管理器复制时会探测若干 FSCTL;明确返回“不支持”比 invalid parameter 更接近本地盘行为。// Logan.Li log.Printf("[rdpdr] device control file=%d ioctl=0x%x payload=%d", req.fileID, ioctl, len(req.payload)) h.sendIOCompletion(req.deviceID, req.completionID, statusInvalidDeviceReq, out[:]) } func (h *Handler) handleNotifyChangeDirectory(req ioRequest) { // Windows 资源管理器会长期挂起目录变更通知;立即成功返回会触发重复刷新和“地址无效”。// Logan.Li log.Printf("[rdpdr] notify change pending file=%d", req.fileID) } func (h *Handler) handleLockControl(req ioRequest) { var out [4]byte // 上传暂存文件是只读内存文件,锁请求直接成功,避免 Explorer 复制流程中断。// Logan.Li log.Printf("[rdpdr] lock control file=%d payload=%d", req.fileID, len(req.payload)) h.sendIOCompletion(req.deviceID, req.completionID, statusSuccess, out[:]) } func (h *Handler) handleQueryDirectory(req ioRequest) { if len(req.payload) < 32 { h.sendIOCompletion(req.deviceID, req.completionID, statusInvalidParameter, nil) return } infoClass := binary.LittleEndian.Uint32(req.payload[0:4]) initial := req.payload[4] != 0 file, ok := h.getOpenFile(req.fileID) if !ok || !file.isDir { h.sendIOCompletion(req.deviceID, req.completionID, statusNoSuchFile, nil) return } if initial { file.dirIndex = 0 if file.path == "" { file.dirEntries = []dirEntry{{name: "."}, {name: ".."}, {name: downloadDirName, isDir: true}, {name: uploadDirName, isDir: true}} } else if strings.EqualFold(file.path, uploadDirName) { file.dirEntries = append([]dirEntry{{name: "."}, {name: ".."}}, h.listUploads()...) } else { file.dirEntries = []dirEntry{{name: "."}, {name: ".."}} } } log.Printf("[rdpdr] query dir file=%d class=%d path=%q initial=%t index=%d entries=%d", req.fileID, infoClass, file.path, initial, file.dirIndex, len(file.dirEntries)) if file.dirIndex >= len(file.dirEntries) { var out [5]byte h.sendIOCompletion(req.deviceID, req.completionID, statusNoMoreFiles, out[:]) return } entry := file.dirEntries[file.dirIndex] file.dirIndex++ h.sendIOCompletion(req.deviceID, req.completionID, statusSuccess, buildDirInfo(infoClass, entry)) } func buildDirInfo(infoClass uint32, entry dirEntry) []byte { name := encodeUTF16(entry.name) nameWithNull := append(append([]byte(nil), name...), 0, 0) attrs := fileAttrNormal if entry.isDir || entry.name == "." || entry.name == ".." { attrs = fileAttrDirectory } if infoClass == fileNamesInformation { out := make([]byte, 16+len(nameWithNull)) binary.LittleEndian.PutUint32(out[0:4], uint32(12+len(nameWithNull))) binary.LittleEndian.PutUint32(out[12:16], uint32(len(nameWithNull))) copy(out[16:], nameWithNull) return out } fixed := 64 if infoClass == fileFullDirectoryInformation { fixed = 68 } if infoClass == fileBothDirectoryInformation { fixed = 93 } out := make([]byte, 4+fixed+len(nameWithNull)) binary.LittleEndian.PutUint32(out[0:4], uint32(fixed+len(nameWithNull))) writeFileTimes(out[12:44]) binary.LittleEndian.PutUint64(out[44:52], entry.size) binary.LittleEndian.PutUint64(out[52:60], align4096(entry.size)) binary.LittleEndian.PutUint32(out[60:64], attrs) binary.LittleEndian.PutUint32(out[64:68], uint32(len(nameWithNull))) copy(out[4+fixed:], nameWithNull) return out } func fileDisplayName(file *openFile) string { if file.name != "" { return file.name } if file.path != "" { parts := strings.Split(file.path, "\\") return parts[len(parts)-1] } return "\\" } func classifyVirtualPath(vpath string) (string, string) { if vpath == "" { return "root", "" } parts := strings.Split(vpath, "\\") if len(parts) == 1 && strings.EqualFold(parts[0], downloadDirName) { return "download", downloadDirName } if len(parts) == 1 && strings.EqualFold(parts[0], uploadDirName) { return "upload", uploadDirName } if len(parts) >= 2 && strings.EqualFold(parts[0], downloadDirName) { name := parts[len(parts)-1] if name == "" || name == "." || name == ".." { return "missing", "" } return "file", name } if len(parts) >= 2 && strings.EqualFold(parts[0], uploadDirName) { name := parts[len(parts)-1] if name == "" || name == "." || name == ".." { return "missing", "" } return "upload_file", name } return "missing", "" } func normalizeVirtualPath(raw string) string { clean := strings.Trim(strings.ReplaceAll(raw, "/", "\\"), "\\\x00 ") if clean == "" || clean == "." { return "" } parts := strings.FieldsFunc(clean, func(r rune) bool { return r == '\\' }) safe := make([]string, 0, len(parts)) for _, part := range parts { if part == "" || part == "." || part == ".." { continue } safe = append(safe, part) } return strings.ReplaceAll(path.Clean(strings.Join(safe, "/")), "/", "\\") } func encodeUTF16(s string) []byte { u16 := utf16.Encode([]rune(s)) out := make([]byte, len(u16)*2) for i, v := range u16 { binary.LittleEndian.PutUint16(out[i*2:], v) } return out } func decodeUTF16(data []byte) string { u16 := make([]uint16, len(data)/2) for i := range u16 { u16[i] = binary.LittleEndian.Uint16(data[i*2:]) } for len(u16) > 0 && u16[len(u16)-1] == 0 { u16 = u16[:len(u16)-1] } return string(utf16.Decode(u16)) } func writeFileTimes(out []byte) { ft := filetimeNow() for i := 0; i+8 <= len(out); i += 8 { binary.LittleEndian.PutUint64(out[i:i+8], ft) } } func filetimeNow() uint64 { return uint64(time.Now().UnixNano()/100) + windowsFiletimeOffset } func align4096(v uint64) uint64 { return (v + 4095) &^ 4095 }