-
Notifications
You must be signed in to change notification settings - Fork 101
several fixes and features (rfc3659) #19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
690b188
eddfb12
a121710
86961ae
89378c2
adaab2c
0812101
59f0223
b1ca06d
dec280e
5e0f0e8
abfd949
1ac9e7f
abe3739
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -112,6 +112,27 @@ func (c *Client) Getwd() (string, error) { | |
| return dir, nil | ||
| } | ||
|
|
||
| // Setwd changes the current working directory. | ||
| func (c *Client) Setwd(path string) (error) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I didn't include this because it isn't compatible with the connection pool (would end up with connections in different directories). I considered making it a client-wide setting that gets applied whenever you get a connection (i.e. switches to the desired directory), but that could get complicated, too.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know(and there is a comment on the commit with this :) ) but it seems it is used as a workaround by ftp clients when LIST or MLSD fails with path argument. I ran into the issue while testing and using CWD first before LIST (without path) worked. I was considering the following:
some locking might be needed if the user starts several go routines using the same handle as for me I really need this but I could move it to a separate branch if you do not
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if each connection remembers what dir it is in, and when a new connection is gotten it automatically does the cwd if it isn't in the correct dir (user's desired dir is stored on client object when they set via setwd())? |
||
| pconn, err := c.getIdleConn() | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| defer c.returnConn(pconn) | ||
|
|
||
| code, msg, err := pconn.sendCommand("CWD %s", path) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| if code != replyFileActionOkay { | ||
| return ftpError{code: code, msg: msg} | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| func commandNotSupporterdError(err error) bool { | ||
| respCode := err.(ftpError).Code() | ||
| return respCode == replyCommandSyntaxError || respCode == replyCommandNotImplemented | ||
|
|
@@ -125,16 +146,27 @@ func commandNotSupporterdError(err error) bool { | |
| // be used. You may have to set ServerLocation in your config to get (more) | ||
| // accurate ModTimes in this case. | ||
| func (c *Client) ReadDir(path string) ([]os.FileInfo, error) { | ||
| entries, err := c.dataStringList("MLSD %s", path) | ||
| pconn, err := c.getIdleConn() | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| defer c.returnConn(pconn) | ||
|
|
||
| var entries []string | ||
|
|
||
| if !pconn.mlsdNotSupported { | ||
| entries, err = c.dataStringList(pconn, "MLSD %s", path) | ||
| } | ||
| parser := parseMLST | ||
|
|
||
| if err != nil { | ||
| if !commandNotSupporterdError(err) { | ||
| if pconn.mlsdNotSupported || err != nil { | ||
| if !pconn.mlsdNotSupported && !commandNotSupporterdError(err) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can this commandNotSupported check be moved up directly after the dataStringList() call? It's a little weird having the nested checks if mlsdNotSupported.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. right... missed that, I'll commit a fix |
||
| return nil, err | ||
| } | ||
| pconn.mlsdNotSupported = true | ||
|
|
||
| entries, err = c.dataStringList("LIST %s", path) | ||
| entries, err = c.dataStringList(pconn, "LIST %s", path) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
@@ -148,7 +180,9 @@ func (c *Client) ReadDir(path string) ([]os.FileInfo, error) { | |
| info, err := parser(entry, true) | ||
| if err != nil { | ||
| c.debug("error in ReadDir: %s", err) | ||
| return nil, err | ||
| if !strings.Contains(err.Error(), "incomplete") { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you have examples of incomplete MLSD/LIST output? Is it safe to give the user back incomplete results (i.e. maybe we just failed parsing)?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "............" is text utf8, this is a wireshark dump 220 Serv-U FTP Server v10.4 ready... "Type=dir;Perm=el; .........." is a directory that is no longer accesible and the server does not send any other information about it but we can still use the received list |
||
| return nil, err | ||
| } | ||
| } | ||
|
|
||
| if info == nil { | ||
|
|
@@ -167,10 +201,17 @@ func (c *Client) ReadDir(path string) ([]os.FileInfo, error) { | |
| // is a directory. You may have to set ServerLocation in your config to get | ||
| // (more) accurate ModTimes when using "LIST". | ||
| func (c *Client) Stat(path string) (os.FileInfo, error) { | ||
| lines, err := c.controlStringList("MLST %s", path) | ||
| pconn, err := c.getIdleConn() | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| defer c.returnConn(pconn) | ||
|
|
||
| lines, err := c.controlStringList(pconn, "MLST %s", path) | ||
| if err != nil { | ||
| if commandNotSupporterdError(err) { | ||
| lines, err = c.dataStringList("LIST %s", path) | ||
| lines, err = c.dataStringList(pconn, "LIST %s", path) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
@@ -202,17 +243,10 @@ func extractDirName(msg string) (string, error) { | |
| return strings.Replace(msg[openQuote+1:closeQuote], `""`, `"`, -1), nil | ||
| } | ||
|
|
||
| func (c *Client) controlStringList(f string, args ...interface{}) ([]string, error) { | ||
| pconn, err := c.getIdleConn() | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| defer c.returnConn(pconn) | ||
|
|
||
| func (c *Client) controlStringList(pconn *persistentConn, f string, args ...interface{}) ([]string, error) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why did you change these methods to take the pconn? Was it to avoid calling getIdleConn() multiple times?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mostly because I needed to access pconn.mlsdNotSupported in ReadDir for the connection. |
||
| cmd := fmt.Sprintf(f, args...) | ||
|
|
||
| code, msg, err := pconn.sendCommand(cmd) | ||
| code, msg, _ := pconn.sendCommand(cmd) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This error should probably be checked (oversight in existing code). |
||
|
|
||
| if !positiveCompletionReply(code) { | ||
| pconn.debug("unexpected response to %s: %d-%s", cmd, code, msg) | ||
|
|
@@ -222,14 +256,7 @@ func (c *Client) controlStringList(f string, args ...interface{}) ([]string, err | |
| return strings.Split(msg, "\n"), nil | ||
| } | ||
|
|
||
| func (c *Client) dataStringList(f string, args ...interface{}) ([]string, error) { | ||
| pconn, err := c.getIdleConn() | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| defer c.returnConn(pconn) | ||
|
|
||
| func (c *Client) dataStringList(pconn *persistentConn, f string, args ...interface{}) ([]string, error) { | ||
| dcGetter, err := pconn.prepareDataConn() | ||
| if err != nil { | ||
| return nil, err | ||
|
|
@@ -321,6 +348,66 @@ func (f *ftpFile) Sys() interface{} { | |
| return f.raw | ||
| } | ||
|
|
||
| var dirRegex = regexp.MustCompile(`^\s*(\S+\s+\S+\s{0,1}\S{2})\s+(\S+)\s+(.*)`) | ||
| var dirTimeFormats = []string{ | ||
| "01-02-06 03:04", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be 15:04 for parsing of 24hr clock value without am/pm?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes... |
||
| "01-02-06 03:04PM", | ||
| "2006-01-02 15:04", | ||
| } | ||
|
|
||
| // 08/03/2016 17:13 <DIR> directory-name | ||
| // 08/07/2016 17:20 1,135 file-name | ||
| // or | ||
| // 02-10-16 12:10PM 1067784 file.exe | ||
| func parseDirLIST(entry string, loc *time.Location, skipSelfParent bool) (os.FileInfo, error) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What server returns this format?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 220 Microsoft FTP Service LIST output looks like this: 06-20-16 03:43PM 164559 print.pdf the main issue is the date format so I tried to support the most common formats I have found |
||
| matches := dirRegex.FindStringSubmatch(entry) | ||
|
|
||
| if len(matches) == 0 { | ||
| return nil, ftpError{err: fmt.Errorf(`failed parsing LIST entry: %s`, entry)} | ||
| } | ||
|
|
||
| if skipSelfParent && (matches[3] == "." || matches[3] == "..") { | ||
| return nil, nil | ||
| } | ||
|
|
||
| var err error | ||
|
|
||
| var size uint64 | ||
| var mode os.FileMode = 0400 | ||
| if strings.Contains(matches[2], "DIR") { | ||
| mode |= os.ModeDir | ||
| } else { | ||
| size, err = strconv.ParseUint(matches[2], 10, 64) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is ParseUint going to work on sizes with a comma like in your example above?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, it will fail with err (size will be 0) but I have not found a single server that returns commas yet. Only running "dir" in local "command prompt" does that. |
||
| if err != nil { | ||
| return nil, ftpError{err: fmt.Errorf(`failed parsing LIST entry's size: %s (%s)`, err, entry)} | ||
| } | ||
| } | ||
|
|
||
| var mtime time.Time | ||
| for _, format := range dirTimeFormats { | ||
| if len(entry) >= len(format) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this check needed? Are you certain time strings can never be shorter than the formats (eg. "2006-01-02 2:04" is shorter than "2006-01-02 15:04")?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I needed a way to speed up processing large lists (thousands of files) and avoid false matches. |
||
| mtime, err = time.ParseInLocation(format, matches[1], loc) | ||
| if err == nil { | ||
| break | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if err != nil { | ||
| return nil, ftpError{err: fmt.Errorf(`failed parsing LIST entry's mtime: %s (%s)`, err, entry)} | ||
| } | ||
|
|
||
| info := &ftpFile{ | ||
| name: filepath.Base(matches[3]), | ||
| mode: mode, | ||
| mtime: mtime, | ||
| raw: entry, | ||
| size: int64(size), | ||
| } | ||
|
|
||
| return info, nil | ||
| } | ||
|
|
||
| var lsRegex = regexp.MustCompile(`^\s*(\S)(\S{3})(\S{3})(\S{3})(?:\s+\S+){3}\s+(\d+)\s+(\w+\s+\d+)\s+([\d:]+)\s+(.+)$`) | ||
|
|
||
| // total 404456 | ||
|
|
@@ -332,7 +419,8 @@ func parseLIST(entry string, loc *time.Location, skipSelfParent bool) (os.FileIn | |
|
|
||
| matches := lsRegex.FindStringSubmatch(entry) | ||
| if len(matches) == 0 { | ||
| return nil, ftpError{err: fmt.Errorf(`failed parsing LIST entry: %s`, entry)} | ||
| info, err := parseDirLIST(entry, loc, skipSelfParent) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you add tests for this additional parsing? Maybe some unit tests similar to TestParseMLST() ?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, I will look into it. |
||
| return info, err | ||
| } | ||
|
|
||
| if skipSelfParent && (matches[8] == "." || matches[8] == "..") { | ||
|
|
@@ -400,7 +488,7 @@ func parseMLST(entry string, skipSelfParent bool) (os.FileInfo, error) { | |
| parseError := ftpError{err: fmt.Errorf(`failed parsing MLST entry: %s`, entry)} | ||
| incompleteError := ftpError{err: fmt.Errorf(`MLST entry incomplete: %s`, entry)} | ||
|
|
||
| parts := strings.Split(entry, "; ") | ||
| parts := strings.SplitN(entry, "; ", 2) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you add a test case?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes |
||
| if len(parts) != 2 { | ||
| return nil, parseError | ||
| } | ||
|
|
@@ -420,7 +508,7 @@ func parseMLST(entry string, skipSelfParent bool) (os.FileInfo, error) { | |
| return nil, incompleteError | ||
| } | ||
|
|
||
| if skipSelfParent && (typ == "cdir" || typ == "pdir" || typ == "." || typ == "..") { | ||
| if skipSelfParent && (typ == "cdir" || typ == "pdir" || typ == "." || typ == ".."|| parts[1] == "." || parts[1] == "..") { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you add a test case?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No sane server should return "." or ".." as type "dir"... How would you test for this?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unit test for parseList is fine. |
||
| return nil, nil | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -63,6 +63,9 @@ type persistentConn struct { | |
| // remember EPSV support | ||
| epsvNotSupported bool | ||
|
|
||
| // remember MLSD support | ||
| mlsdNotSupported bool | ||
|
|
||
| // tracks the current type (e.g. ASCII/Image) of connection | ||
| currentType string | ||
|
|
||
|
|
@@ -177,6 +180,17 @@ func (pconn *persistentConn) readResponse() (int, string, error) { | |
| err: fmt.Errorf("error reading response: %s", err), | ||
| temporary: true, | ||
| } | ||
| } else if code == replyConnectionClosed { | ||
| pconn.controlConn.SetReadDeadline(time.Now().Add(pconn.config.Timeout)) | ||
| code, msg, err = pconn.reader.ReadResponse(0) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What case is this handling? If this is for reading the final control response after a failed data transfer, it seems like the transferFromOffset() function should be checking for this error and reading the response (i.e. if
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was hard to reproduce as it happened randomly on a high latency server (probably because of some packet loss or timeout...). Looks like a bug in the server as it should not send a 426 code as a preliminary reply. In my tests it happened while walking with ReadDir() . It could be handled in dataStringList & transferFromOffset if I had more data but this seems like a more generic fix.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it is the user of the data connection's responsibility to read the extra control connection response (even in the case of data connection being closed early). I think a sign this doesn't belong here is that it is basically duplicates all of the readResponse() method itself. |
||
| if err != nil { | ||
| pconn.broken = true | ||
| pconn.debug("error reading second response: %s", err) | ||
| err = ftpError{ | ||
| err: fmt.Errorf("error reading response: %s", err), | ||
| temporary: true, | ||
| } | ||
| } | ||
| } | ||
| return code, msg, err | ||
| } | ||
|
|
@@ -205,7 +219,7 @@ func (pconn *persistentConn) fetchFeatures() error { | |
| } | ||
|
|
||
| for _, line := range strings.Split(msg, "\n") { | ||
| if line[0] == ' ' { | ||
| if len(line) > 0 && line[0] == ' ' { | ||
| parts := strings.SplitN(strings.TrimSpace(line), " ", 2) | ||
| if len(parts) == 1 { | ||
| pconn.features[strings.ToUpper(parts[0])] = "" | ||
|
|
@@ -228,6 +242,48 @@ func (pconn *persistentConn) hasFeatureWithArg(name, arg string) bool { | |
| return found && strings.ToUpper(arg) == val | ||
| } | ||
|
|
||
| func (pconn *persistentConn) setClient() error { | ||
| code, msg, err := pconn.sendCommand("CLNT GoFTP") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the purpose of the CLNT command?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. According to the RFCs - it has no real purpose, just telling the server what client you are using. For example on some version of CGFTP: |
||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| if !positiveCompletionReply(code) { | ||
| pconn.debug("server doesn't support CLNT: %d-%s", code, msg) | ||
| return nil | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| func (pconn *persistentConn) setUnicode() error { | ||
| code, msg, err := pconn.sendCommand("OPTS UTF8 ON") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Specifying UTF8 when available makes sense, but for my own edification, when is this required? Are there servers that won't send UTF-8 path names without this command?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I had an issue with several servers that treat UTF8 in client commands (like path in LIST / MLSD) as ASCII, thus replying with "not found", although they sent proper UTF-8 paths in replies Again this is a feature not required in RFCs as UTF8 should be enabled by default if listed in FEAT but needed to work around broken servers (and there are quite a few of them). |
||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| if !positiveCompletionReply(code) { | ||
| pconn.debug("server doesn't support UTF8: %d-%s", code, msg) | ||
| return nil | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| func (pconn *persistentConn) setMLST() error { | ||
| code, msg, err := pconn.sendCommand("OPTS MLST type;size;modify;perm;unix.mode;") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. from RFC 3659: I've made some protocol dumps from a few ftp clients and they seem to use lower case so I kept it as such, though the RFC continues with: |
||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| if !positiveCompletionReply(code) { | ||
| pconn.debug("server doesn't support UTF8: %d-%s", code, msg) | ||
| return nil | ||
| } | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we check the response message to see if server supports requested options?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought of it but it seems like it would just add complexity. If the commands fail the client would still behave as before. OPTS UTF8 ON is optional and may fail but the server could still use UTF8 according to FEAT reply. |
||
| return nil | ||
| } | ||
|
|
||
| func (pconn *persistentConn) logIn() error { | ||
| if pconn.config.User == "" { | ||
| return nil | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still not sure why why need -k. What are you developing on that doesn't have a CA bundle?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do have the CA bundle... but from experience I've also ended up working with servers that do not have it or it is outdated (not my choice). If you think it is unsafe I can remove it as it is not really need, I just added it for compatibility.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should remove it.