Skip to content

Commit 088878b

Browse files
authored
Merge pull request #571 from powellnorma/win-root
server.go: "/" for windows
2 parents a3da03b + 6be1dd2 commit 088878b

File tree

5 files changed

+214
-8
lines changed

5 files changed

+214
-8
lines changed

examples/go-sftp-server/main.go

+8
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,13 @@ func main() {
2020
var (
2121
readOnly bool
2222
debugStderr bool
23+
winRoot bool
2324
)
2425

2526
flag.BoolVar(&readOnly, "R", false, "read-only server")
2627
flag.BoolVar(&debugStderr, "e", false, "debug to stderr")
28+
flag.BoolVar(&winRoot, "wr", false, "windows root")
29+
2730
flag.Parse()
2831

2932
debugStream := io.Discard
@@ -128,6 +131,11 @@ func main() {
128131
fmt.Fprintf(debugStream, "Read write server\n")
129132
}
130133

134+
if winRoot {
135+
serverOptions = append(serverOptions, sftp.WindowsRootEnumeratesDrives())
136+
fmt.Fprintf(debugStream, "Windows root enabled\n")
137+
}
138+
131139
server, err := sftp.NewServer(
132140
channel,
133141
serverOptions...,

go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ require (
66
github.com/kr/fs v0.1.0
77
github.com/stretchr/testify v1.8.0
88
golang.org/x/crypto v0.31.0
9+
golang.org/x/sys v0.28.0 // indirect
910
)

server.go

+29-7
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"errors"
88
"fmt"
99
"io"
10+
"io/fs"
1011
"io/ioutil"
1112
"os"
1213
"path/filepath"
@@ -21,6 +22,18 @@ const (
2122
SftpServerWorkerCount = 8
2223
)
2324

25+
type file interface {
26+
Stat() (os.FileInfo, error)
27+
ReadAt(b []byte, off int64) (int, error)
28+
WriteAt(b []byte, off int64) (int, error)
29+
Readdir(int) ([]os.FileInfo, error)
30+
Name() string
31+
Truncate(int64) error
32+
Chmod(mode fs.FileMode) error
33+
Chown(uid, gid int) error
34+
Close() error
35+
}
36+
2437
// Server is an SSH File Transfer Protocol (sftp) server.
2538
// This is intended to provide the sftp subsystem to an ssh server daemon.
2639
// This implementation currently supports most of sftp server protocol version 3,
@@ -30,14 +43,15 @@ type Server struct {
3043
debugStream io.Writer
3144
readOnly bool
3245
pktMgr *packetManager
33-
openFiles map[string]*os.File
46+
openFiles map[string]file
3447
openFilesLock sync.RWMutex
3548
handleCount int
3649
workDir string
50+
winRoot bool
3751
maxTxPacket uint32
3852
}
3953

40-
func (svr *Server) nextHandle(f *os.File) string {
54+
func (svr *Server) nextHandle(f file) string {
4155
svr.openFilesLock.Lock()
4256
defer svr.openFilesLock.Unlock()
4357
svr.handleCount++
@@ -57,7 +71,7 @@ func (svr *Server) closeHandle(handle string) error {
5771
return EBADF
5872
}
5973

60-
func (svr *Server) getHandle(handle string) (*os.File, bool) {
74+
func (svr *Server) getHandle(handle string) (file, bool) {
6175
svr.openFilesLock.RLock()
6276
defer svr.openFilesLock.RUnlock()
6377
f, ok := svr.openFiles[handle]
@@ -86,7 +100,7 @@ func NewServer(rwc io.ReadWriteCloser, options ...ServerOption) (*Server, error)
86100
serverConn: svrConn,
87101
debugStream: ioutil.Discard,
88102
pktMgr: newPktMgr(svrConn),
89-
openFiles: make(map[string]*os.File),
103+
openFiles: make(map[string]file),
90104
maxTxPacket: defaultMaxTxPacket,
91105
}
92106

@@ -118,6 +132,14 @@ func ReadOnly() ServerOption {
118132
}
119133
}
120134

135+
// WindowsRootEnumeratesDrives configures a Server to serve a virtual '/' for windows that lists all drives
136+
func WindowsRootEnumeratesDrives() ServerOption {
137+
return func(s *Server) error {
138+
s.winRoot = true
139+
return nil
140+
}
141+
}
142+
121143
// WithAllocator enable the allocator.
122144
// After processing a packet we keep in memory the allocated slices
123145
// and we reuse them for new packets.
@@ -215,7 +237,7 @@ func handlePacket(s *Server, p orderedRequest) error {
215237
}
216238
case *sshFxpLstatPacket:
217239
// stat the requested file
218-
info, err := os.Lstat(s.toLocalPath(p.Path))
240+
info, err := s.lstat(s.toLocalPath(p.Path))
219241
rpkt = &sshFxpStatResponse{
220242
ID: p.ID,
221243
info: info,
@@ -289,7 +311,7 @@ func handlePacket(s *Server, p orderedRequest) error {
289311
case *sshFxpOpendirPacket:
290312
lp := s.toLocalPath(p.Path)
291313

292-
if stat, err := os.Stat(lp); err != nil {
314+
if stat, err := s.stat(lp); err != nil {
293315
rpkt = statusFromError(p.ID, err)
294316
} else if !stat.IsDir() {
295317
rpkt = statusFromError(p.ID, &os.PathError{
@@ -493,7 +515,7 @@ func (p *sshFxpOpenPacket) respond(svr *Server) responsePacket {
493515
mode = fs.FileMode() & os.ModePerm
494516
}
495517

496-
f, err := os.OpenFile(svr.toLocalPath(p.Path), osFlags, mode)
518+
f, err := svr.openfile(svr.toLocalPath(p.Path), osFlags, mode)
497519
if err != nil {
498520
return statusFromError(p.ID, err)
499521
}

server_posix.go

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
//go:build !windows
2+
// +build !windows
3+
4+
package sftp
5+
6+
import (
7+
"io/fs"
8+
"os"
9+
)
10+
11+
func (s *Server) openfile(path string, flag int, mode fs.FileMode) (file, error) {
12+
return os.OpenFile(path, flag, mode)
13+
}
14+
15+
func (s *Server) lstat(name string) (os.FileInfo, error) {
16+
return os.Lstat(name)
17+
}
18+
19+
func (s *Server) stat(name string) (os.FileInfo, error) {
20+
return os.Stat(name)
21+
}

server_windows.go

+155-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
package sftp
22

33
import (
4+
"fmt"
5+
"io"
6+
"io/fs"
7+
"os"
48
"path"
59
"path/filepath"
10+
"time"
11+
12+
"golang.org/x/sys/windows"
613
)
714

815
func (s *Server) toLocalPath(p string) string {
@@ -12,7 +19,11 @@ func (s *Server) toLocalPath(p string) string {
1219

1320
lp := filepath.FromSlash(p)
1421

15-
if path.IsAbs(p) {
22+
if path.IsAbs(p) { // starts with '/'
23+
if len(p) == 1 && s.winRoot {
24+
return `\\.\` // for openfile
25+
}
26+
1627
tmp := lp
1728
for len(tmp) > 0 && tmp[0] == '\\' {
1829
tmp = tmp[1:]
@@ -33,7 +44,150 @@ func (s *Server) toLocalPath(p string) string {
3344
// e.g. "/C:" to "C:\\"
3445
return tmp
3546
}
47+
48+
if s.winRoot {
49+
// Make it so that "/Windows" is not found, and "/c:/Windows" has to be used
50+
return `\\.\` + tmp
51+
}
3652
}
3753

3854
return lp
3955
}
56+
57+
func bitsToDrives(bitmap uint32) []string {
58+
var drive rune = 'a'
59+
var drives []string
60+
61+
for bitmap != 0 && drive <= 'z' {
62+
if bitmap&1 == 1 {
63+
drives = append(drives, string(drive)+":")
64+
}
65+
drive++
66+
bitmap >>= 1
67+
}
68+
69+
return drives
70+
}
71+
72+
func getDrives() ([]string, error) {
73+
mask, err := windows.GetLogicalDrives()
74+
if err != nil {
75+
return nil, fmt.Errorf("GetLogicalDrives: %w", err)
76+
}
77+
return bitsToDrives(mask), nil
78+
}
79+
80+
type driveInfo struct {
81+
fs.FileInfo
82+
name string
83+
}
84+
85+
func (i *driveInfo) Name() string {
86+
return i.name // since the Name() returned from a os.Stat("C:\\") is "\\"
87+
}
88+
89+
type winRoot struct {
90+
drives []string
91+
}
92+
93+
func newWinRoot() (*winRoot, error) {
94+
drives, err := getDrives()
95+
if err != nil {
96+
return nil, err
97+
}
98+
return &winRoot{
99+
drives: drives,
100+
}, nil
101+
}
102+
103+
func (f *winRoot) Readdir(n int) ([]os.FileInfo, error) {
104+
drives := f.drives
105+
if n > 0 && len(drives) > n {
106+
drives = drives[:n]
107+
}
108+
f.drives = f.drives[len(drives):]
109+
if len(drives) == 0 {
110+
return nil, io.EOF
111+
}
112+
113+
var infos []os.FileInfo
114+
for _, drive := range drives {
115+
fi, err := os.Stat(drive + `\`)
116+
if err != nil {
117+
return nil, err
118+
}
119+
120+
di := &driveInfo{
121+
FileInfo: fi,
122+
name: drive,
123+
}
124+
infos = append(infos, di)
125+
}
126+
127+
return infos, nil
128+
}
129+
130+
func (f *winRoot) Stat() (os.FileInfo, error) {
131+
return rootFileInfo, nil
132+
}
133+
func (f *winRoot) ReadAt(b []byte, off int64) (int, error) {
134+
return 0, os.ErrPermission
135+
}
136+
func (f *winRoot) WriteAt(b []byte, off int64) (int, error) {
137+
return 0, os.ErrPermission
138+
}
139+
func (f *winRoot) Name() string {
140+
return "/"
141+
}
142+
func (f *winRoot) Truncate(int64) error {
143+
return os.ErrPermission
144+
}
145+
func (f *winRoot) Chmod(mode fs.FileMode) error {
146+
return os.ErrPermission
147+
}
148+
func (f *winRoot) Chown(uid, gid int) error {
149+
return os.ErrPermission
150+
}
151+
func (f *winRoot) Close() error {
152+
f.drives = nil
153+
return nil
154+
}
155+
156+
func (s *Server) openfile(path string, flag int, mode fs.FileMode) (file, error) {
157+
if path == `\\.\` && s.winRoot {
158+
return newWinRoot()
159+
}
160+
return os.OpenFile(path, flag, mode)
161+
}
162+
163+
type winRootFileInfo struct {
164+
name string
165+
modTime time.Time
166+
}
167+
168+
func (w *winRootFileInfo) Name() string { return w.name }
169+
func (w *winRootFileInfo) Size() int64 { return 0 }
170+
func (w *winRootFileInfo) Mode() fs.FileMode { return fs.ModeDir | 0555 } // read+execute for all
171+
func (w *winRootFileInfo) ModTime() time.Time { return w.modTime }
172+
func (w *winRootFileInfo) IsDir() bool { return true }
173+
func (w *winRootFileInfo) Sys() interface{} { return nil }
174+
175+
// Create a new root FileInfo
176+
var rootFileInfo = &winRootFileInfo{
177+
name: "/",
178+
modTime: time.Now(),
179+
}
180+
181+
func (s *Server) lstat(name string) (os.FileInfo, error) {
182+
if name == `\\.\` && s.winRoot {
183+
return rootFileInfo, nil
184+
}
185+
return os.Lstat(name)
186+
}
187+
188+
func (s *Server) stat(name string) (os.FileInfo, error) {
189+
if name == `\\.\` && s.winRoot {
190+
return rootFileInfo, nil
191+
}
192+
return os.Stat(name)
193+
}

0 commit comments

Comments
 (0)