Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
af889dc1ad |
17
.github/dependabot.yml
vendored
17
.github/dependabot.yml
vendored
@ -1,17 +0,0 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gomod" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "daily"
|
||||
assignees:
|
||||
- "jlaffaye"
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
69
.github/workflows/codeql-analysis.yml
vendored
69
.github/workflows/codeql-analysis.yml
vendored
@ -1,69 +0,0 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ "master" ]
|
||||
schedule:
|
||||
- cron: '20 19 * * 2'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'go' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
|
||||
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
|
||||
# If the Autobuild fails above, remove it and uncomment the following three lines.
|
||||
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
|
||||
|
||||
# - run: |
|
||||
# echo "Run, Build Application using script"
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@64e61baeac852f409b48440cebec029a2d978f90
|
7
.github/workflows/golangci-lint.yaml
vendored
7
.github/workflows/golangci-lint.yaml
vendored
@ -5,12 +5,9 @@ jobs:
|
||||
golangci-lint:
|
||||
name: lint
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read # for actions/checkout to fetch code
|
||||
pull-requests: read # for golangci/golangci-lint-action to fetch pull requests
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.7
|
||||
- uses: actions/checkout@v2
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@aaa42aa0628b4ae2578232a66b541047968fac86
|
||||
uses: golangci/golangci-lint-action@v2
|
||||
with:
|
||||
only-new-issues: true
|
||||
|
12
.github/workflows/unit_tests.yaml
vendored
12
.github/workflows/unit_tests.yaml
vendored
@ -5,12 +5,12 @@ jobs:
|
||||
name: test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.7
|
||||
- uses: actions/checkout@master
|
||||
- name: Setup go
|
||||
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.19
|
||||
- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9
|
||||
go-version: 1.17
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
@ -21,9 +21,9 @@ jobs:
|
||||
- name: Run tests
|
||||
run: go test -v -covermode=count -coverprofile=coverage.out
|
||||
- name: Convert coverage to lcov
|
||||
uses: jandelgado/gcov2lcov-action@c680c0f7c7442485f1749eb2a13e54a686e76eb5
|
||||
uses: jandelgado/gcov2lcov-action@v1.0.8
|
||||
- name: Coveralls
|
||||
uses: coverallsapp/github-action@643bc377ffa44ace6394b2b5d0d3950076de9f63
|
||||
uses: coverallsapp/github-action@v1.1.2
|
||||
with:
|
||||
github-token: ${{ secrets.github_token }}
|
||||
path-to-lcov: coverage.lcov
|
||||
|
@ -2,8 +2,6 @@
|
||||
|
||||
[![Units tests](https://github.com/jlaffaye/ftp/actions/workflows/unit_tests.yaml/badge.svg)](https://github.com/jlaffaye/ftp/actions/workflows/unit_tests.yaml)
|
||||
[![Coverage Status](https://coveralls.io/repos/jlaffaye/ftp/badge.svg?branch=master&service=github)](https://coveralls.io/github/jlaffaye/ftp?branch=master)
|
||||
[![golangci-lint](https://github.com/jlaffaye/ftp/actions/workflows/golangci-lint.yaml/badge.svg)](https://github.com/jlaffaye/ftp/actions/workflows/golangci-lint.yaml)
|
||||
[![CodeQL](https://github.com/jlaffaye/ftp/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/jlaffaye/ftp/actions/workflows/codeql-analysis.yml)
|
||||
[![Go ReportCard](https://goreportcard.com/badge/jlaffaye/ftp)](http://goreportcard.com/report/jlaffaye/ftp)
|
||||
[![Go Reference](https://pkg.go.dev/badge/github.com/jlaffaye/ftp.svg)](https://pkg.go.dev/github.com/jlaffaye/ftp)
|
||||
|
||||
|
199
client_test.go
199
client_test.go
@ -3,8 +3,10 @@ package ftp
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/textproto"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
@ -26,145 +28,174 @@ func TestConnEPSV(t *testing.T) {
|
||||
}
|
||||
|
||||
func testConn(t *testing.T, disableEPSV bool) {
|
||||
assert := assert.New(t)
|
||||
mock, c := openConn(t, "127.0.0.1", DialWithTimeout(5*time.Second), DialWithDisabledEPSV(disableEPSV))
|
||||
|
||||
err := c.Login("anonymous", "anonymous")
|
||||
assert.NoError(err)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = c.NoOp()
|
||||
assert.NoError(err)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
err = c.ChangeDir("incoming")
|
||||
assert.NoError(err)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
dir, err := c.CurrentDir()
|
||||
if assert.NoError(err) {
|
||||
assert.Equal("/incoming", dir)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
} else {
|
||||
if dir != "/incoming" {
|
||||
t.Error("Wrong dir: " + dir)
|
||||
}
|
||||
}
|
||||
|
||||
data := bytes.NewBufferString(testData)
|
||||
err = c.Stor("test", data)
|
||||
assert.NoError(err)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
_, err = c.List(".")
|
||||
assert.NoError(err)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
err = c.Rename("test", "tset")
|
||||
assert.NoError(err)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// Read without deadline
|
||||
r, err := c.Retr("tset")
|
||||
if assert.NoError(err) {
|
||||
buf, err := io.ReadAll(r)
|
||||
if assert.NoError(err) {
|
||||
assert.Equal(testData, string(buf))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
} else {
|
||||
buf, errRead := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
t.Error(errRead)
|
||||
}
|
||||
if string(buf) != testData {
|
||||
t.Errorf("'%s'", buf)
|
||||
}
|
||||
|
||||
r.Close()
|
||||
r.Close() // test we can close two times
|
||||
}
|
||||
|
||||
// Read with deadline
|
||||
r, err = c.Retr("tset")
|
||||
if assert.NoError(err) {
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
} else {
|
||||
if err := r.SetDeadline(time.Now()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = io.ReadAll(r)
|
||||
assert.ErrorContains(err, "i/o timeout")
|
||||
_, err = ioutil.ReadAll(r)
|
||||
if err == nil {
|
||||
t.Error("deadline should have caused error")
|
||||
} else if !strings.HasSuffix(err.Error(), "i/o timeout") {
|
||||
t.Error(err)
|
||||
}
|
||||
r.Close()
|
||||
}
|
||||
|
||||
// Read with offset
|
||||
r, err = c.RetrFrom("tset", 5)
|
||||
if assert.NoError(err) {
|
||||
buf, err := io.ReadAll(r)
|
||||
if assert.NoError(err) {
|
||||
expected := testData[5:]
|
||||
assert.Equal(expected, string(buf))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
} else {
|
||||
buf, errRead := ioutil.ReadAll(r)
|
||||
if errRead != nil {
|
||||
t.Error(errRead)
|
||||
}
|
||||
expected := testData[5:]
|
||||
if string(buf) != expected {
|
||||
t.Errorf("read %q, expected %q", buf, expected)
|
||||
}
|
||||
|
||||
r.Close()
|
||||
}
|
||||
|
||||
data2 := bytes.NewBufferString(testData)
|
||||
err = c.Append("tset", data2)
|
||||
assert.NoError(err)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// Read without deadline, after append
|
||||
r, err = c.Retr("tset")
|
||||
if assert.NoError(err) {
|
||||
buf, err := io.ReadAll(r)
|
||||
if assert.NoError(err) {
|
||||
assert.Equal(testData+testData, string(buf))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
} else {
|
||||
buf, errRead := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
t.Error(errRead)
|
||||
}
|
||||
if string(buf) != testData+testData {
|
||||
t.Errorf("'%s'", buf)
|
||||
}
|
||||
|
||||
r.Close()
|
||||
}
|
||||
|
||||
fileSize, err := c.FileSize("magic-file")
|
||||
assert.NoError(err)
|
||||
assert.Equal(int64(42), fileSize)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if fileSize != 42 {
|
||||
t.Errorf("file size %q, expected %q", fileSize, 42)
|
||||
}
|
||||
|
||||
_, err = c.FileSize("not-found")
|
||||
assert.Error(err)
|
||||
|
||||
entry, err := c.GetEntry("magic-file")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if entry == nil {
|
||||
t.Fatal("expected entry, got nil")
|
||||
}
|
||||
if entry.Size != 42 {
|
||||
t.Errorf("entry size %q, expected %q", entry.Size, 42)
|
||||
}
|
||||
if entry.Type != EntryTypeFile {
|
||||
t.Errorf("entry type %q, expected %q", entry.Type, EntryTypeFile)
|
||||
}
|
||||
if entry.Name != "magic-file" {
|
||||
t.Errorf("entry name %q, expected %q", entry.Name, "magic-file")
|
||||
}
|
||||
|
||||
entry, err = c.GetEntry("multiline-dir")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if entry == nil {
|
||||
t.Fatal("expected entry, got nil")
|
||||
}
|
||||
if entry.Size != 0 {
|
||||
t.Errorf("entry size %q, expected %q", entry.Size, 0)
|
||||
}
|
||||
if entry.Type != EntryTypeFolder {
|
||||
t.Errorf("entry type %q, expected %q", entry.Type, EntryTypeFolder)
|
||||
}
|
||||
if entry.Name != "multiline-dir" {
|
||||
t.Errorf("entry name %q, expected %q", entry.Name, "multiline-dir")
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
|
||||
err = c.Delete("tset")
|
||||
assert.NoError(err)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
err = c.MakeDir(testDir)
|
||||
assert.NoError(err)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
err = c.ChangeDir(testDir)
|
||||
assert.NoError(err)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
err = c.ChangeDirToParent()
|
||||
assert.NoError(err)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
entries, err := c.NameList("/")
|
||||
assert.NoError(err)
|
||||
assert.Equal([]string{"/incoming"}, entries)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if len(entries) != 1 || entries[0] != "/incoming" {
|
||||
t.Errorf("Unexpected entries: %v", entries)
|
||||
}
|
||||
|
||||
err = c.RemoveDir(testDir)
|
||||
assert.NoError(err)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
err = c.Logout()
|
||||
assert.NoError(err)
|
||||
if err != nil {
|
||||
if protoErr := err.(*textproto.Error); protoErr != nil {
|
||||
if protoErr.Code != StatusNotImplemented {
|
||||
t.Error(err)
|
||||
}
|
||||
} else {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err = c.Quit(); err != nil {
|
||||
t.Fatal(err)
|
||||
@ -174,7 +205,9 @@ func testConn(t *testing.T, disableEPSV bool) {
|
||||
mock.Wait()
|
||||
|
||||
err = c.NoOp()
|
||||
assert.Error(err, "should error on closed conn")
|
||||
if err == nil {
|
||||
t.Error("Expected error")
|
||||
}
|
||||
}
|
||||
|
||||
// TestConnect tests the legacy Connect function
|
||||
@ -279,7 +312,7 @@ func TestMissingFolderDeleteDirRecur(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestListCurrentDir(t *testing.T) {
|
||||
mock, c := openConnExt(t, "127.0.0.1", "no-time", DialWithDisabledMLSD(true))
|
||||
mock, c := openConn(t, "127.0.0.1")
|
||||
|
||||
_, err := c.List("")
|
||||
assert.NoError(t, err)
|
||||
@ -295,20 +328,6 @@ func TestListCurrentDir(t *testing.T) {
|
||||
mock.Wait()
|
||||
}
|
||||
|
||||
func TestListCurrentDirWithForceListHidden(t *testing.T) {
|
||||
mock, c := openConnExt(t, "127.0.0.1", "no-time", DialWithDisabledMLSD(true), DialWithForceListHidden(true))
|
||||
|
||||
assert.True(t, c.options.forceListHidden)
|
||||
_, err := c.List("")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "LIST -a", mock.lastFull, "LIST -a must not have a trailing whitespace")
|
||||
|
||||
err = c.Quit()
|
||||
assert.NoError(t, err)
|
||||
|
||||
mock.Wait()
|
||||
}
|
||||
|
||||
func TestTimeUnsupported(t *testing.T) {
|
||||
mock, c := openConnExt(t, "127.0.0.1", "no-time")
|
||||
|
||||
|
173
conn_test.go
173
conn_test.go
@ -2,58 +2,86 @@ package ftp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"net"
|
||||
"net/textproto"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
const (
|
||||
certPem = `-----BEGIN CERTIFICATE-----
|
||||
MIIBhTCCASugAwIBAgIQIRi6zePL6mKjOipn+dNuaTAKBggqhkjOPQQDAjASMRAw
|
||||
DgYDVQQKEwdBY21lIENvMB4XDTE3MTAyMDE5NDMwNloXDTE4MTAyMDE5NDMwNlow
|
||||
EjEQMA4GA1UEChMHQWNtZSBDbzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABD0d
|
||||
7VNhbWvZLWPuj/RtHFjvtJBEwOkhbN/BnnE8rnZR8+sbwnc/KhCk3FhnpHZnQz7B
|
||||
5aETbbIgmuvewdjvSBSjYzBhMA4GA1UdDwEB/wQEAwICpDATBgNVHSUEDDAKBggr
|
||||
BgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MCkGA1UdEQQiMCCCDmxvY2FsaG9zdDo1
|
||||
NDUzgg4xMjcuMC4wLjE6NTQ1MzAKBggqhkjOPQQDAgNIADBFAiEA2zpJEPQyz6/l
|
||||
Wf86aX6PepsntZv2GYlA5UpabfT2EZICICpJ5h/iI+i341gBmLiAFQOyTDT+/wQc
|
||||
6MF9+Yw1Yy0t
|
||||
-----END CERTIFICATE-----`
|
||||
keyPem = `-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIIrYSSNQFaA2Hwf1duRSxKtLYX5CB04fSeQ6tF1aY/PuoAoGCCqGSM49
|
||||
AwEHoUQDQgAEPR3tU2Fta9ktY+6P9G0cWO+0kETA6SFs38GecTyudlHz6xvCdz8q
|
||||
EKTcWGekdmdDPsHloRNtsiCa697B2O9IFA==
|
||||
-----END EC PRIVATE KEY-----`
|
||||
)
|
||||
|
||||
type ftpMock struct {
|
||||
t *testing.T
|
||||
address string
|
||||
modtime string // no-time, std-time, vsftpd
|
||||
listener *net.TCPListener
|
||||
proto *textproto.Conn
|
||||
commands []string // list of received commands
|
||||
lastFull string // full last command
|
||||
rest int
|
||||
fileCont *bytes.Buffer
|
||||
dataConn *mockDataConn
|
||||
t *testing.T
|
||||
address string
|
||||
modtime string // no-time, std-time, vsftpd
|
||||
listener net.Listener
|
||||
proto *textproto.Conn
|
||||
commands []string // list of received commands
|
||||
lastFull string // full last command
|
||||
rest int
|
||||
fileCont *bytes.Buffer
|
||||
dataConn *mockDataConn
|
||||
tlsConfig *tls.Config
|
||||
sync.WaitGroup
|
||||
}
|
||||
|
||||
// newFtpMock returns a mock implementation of a FTP server
|
||||
// For simplication, a mock instance only accepts a signle connection and terminates afer
|
||||
func newFtpMock(t *testing.T, address string) (*ftpMock, error) {
|
||||
return newFtpMockExt(t, address, "no-time")
|
||||
return newFtpMockExt(t, address, false, "no-time")
|
||||
}
|
||||
|
||||
func newFtpMockExt(t *testing.T, address, modtime string) (*ftpMock, error) {
|
||||
var err error
|
||||
func newFtpMockExt(t *testing.T, address string, ssl bool, modtime string) (*ftpMock, error) {
|
||||
mock := &ftpMock{
|
||||
t: t,
|
||||
address: address,
|
||||
modtime: modtime,
|
||||
}
|
||||
|
||||
l, err := net.Listen("tcp", address+":0")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ssl {
|
||||
cert, err := tls.X509KeyPair([]byte(certPem), []byte(keyPem))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mock.tlsConfig = &tls.Config{Certificates: []tls.Certificate{cert}}
|
||||
l, err := tls.Listen("tcp", address+":0", mock.tlsConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mock.listener = l
|
||||
} else {
|
||||
l, err := net.Listen("tcp", address+":0")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mock.listener = l
|
||||
|
||||
tcpListener, ok := l.(*net.TCPListener)
|
||||
if !ok {
|
||||
return nil, errors.New("listener is not a net.TCPListener")
|
||||
}
|
||||
mock.listener = tcpListener
|
||||
|
||||
go mock.listen()
|
||||
|
||||
@ -90,7 +118,7 @@ func (mock *ftpMock) listen() {
|
||||
// At least one command must have a multiline response
|
||||
switch cmdParts[0] {
|
||||
case "FEAT":
|
||||
features := "211-Features:\r\n FEAT\r\n PASV\r\n EPSV\r\n UTF8\r\n SIZE\r\n MLST\r\n"
|
||||
features := "211-Features:\r\n FEAT\r\n PASV\r\n EPSV\r\n UTF8\r\n SIZE\r\n"
|
||||
switch mock.modtime {
|
||||
case "std-time":
|
||||
features += " MDTM\r\n MFMT\r\n"
|
||||
@ -175,26 +203,9 @@ func (mock *ftpMock) listen() {
|
||||
|
||||
mock.dataConn.Wait()
|
||||
mock.printfLine("150 Opening ASCII mode data connection for file list")
|
||||
mock.dataConn.write([]byte("-rw-r--r-- 1 ftp wheel 0 Jan 29 10:29 lo\r\ntotal 1"))
|
||||
mock.dataConn.write([]byte("-rw-r--r-- 1 ftp wheel 0 Jan 29 10:29 lo"))
|
||||
mock.printfLine("226 Transfer complete")
|
||||
mock.closeDataConn()
|
||||
case "MLSD":
|
||||
if mock.dataConn == nil {
|
||||
mock.printfLine("425 Unable to build data connection: Connection refused")
|
||||
break
|
||||
}
|
||||
|
||||
mock.dataConn.Wait()
|
||||
mock.printfLine("150 Opening data connection for file list")
|
||||
mock.dataConn.write([]byte("Type=file;Size=0;Modify=20201213202400; lo\r\n"))
|
||||
mock.printfLine("226 Transfer complete")
|
||||
mock.closeDataConn()
|
||||
case "MLST":
|
||||
if cmdParts[1] == "multiline-dir" {
|
||||
mock.printfLine("250-File data\r\n Type=dir;Size=0; multiline-dir\r\n Modify=20201213202400; multiline-dir\r\n250 End")
|
||||
} else {
|
||||
mock.printfLine("250-File data\r\n Type=file;Size=42;Modify=20201213202400; magic-file\r\n \r\n250 End")
|
||||
}
|
||||
case "NLST":
|
||||
if mock.dataConn == nil {
|
||||
mock.printfLine("425 Unable to build data connection: Connection refused")
|
||||
@ -279,6 +290,10 @@ func (mock *ftpMock) listen() {
|
||||
case "QUIT":
|
||||
mock.printfLine("221 Goodbye.")
|
||||
return
|
||||
case "PBSZ":
|
||||
mock.printfLine("200 PBSZ ok.")
|
||||
case "PROT":
|
||||
mock.printfLine("200 PROT ok.")
|
||||
default:
|
||||
mock.printfLine("500 Unknown command %s.", cmdParts[0])
|
||||
}
|
||||
@ -302,7 +317,7 @@ func (mock *ftpMock) closeDataConn() {
|
||||
|
||||
type mockDataConn struct {
|
||||
t *testing.T
|
||||
listener *net.TCPListener
|
||||
listener net.Listener
|
||||
conn net.Conn
|
||||
// WaitGroup is done when conn is accepted and stored
|
||||
sync.WaitGroup
|
||||
@ -332,16 +347,7 @@ func (mock *ftpMock) listenDataConn() (int64, error) {
|
||||
mock.closeDataConn()
|
||||
|
||||
l, err := net.Listen("tcp", mock.address+":0")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
tcpListener, ok := l.(*net.TCPListener)
|
||||
if !ok {
|
||||
return 0, errors.New("listener is not a net.TCPListener")
|
||||
}
|
||||
|
||||
addr := tcpListener.Addr().String()
|
||||
addr := l.Addr().String()
|
||||
|
||||
_, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
@ -355,7 +361,7 @@ func (mock *ftpMock) listenDataConn() (int64, error) {
|
||||
|
||||
dataConn := &mockDataConn{
|
||||
t: mock.t,
|
||||
listener: tcpListener,
|
||||
listener: l,
|
||||
}
|
||||
dataConn.Add(1)
|
||||
|
||||
@ -364,9 +370,20 @@ func (mock *ftpMock) listenDataConn() (int64, error) {
|
||||
conn, err := dataConn.listener.Accept()
|
||||
if err != nil {
|
||||
// mock.t.Fatalf("can not accept data conn: %s", err)
|
||||
dataConn.Done()
|
||||
return
|
||||
}
|
||||
|
||||
if mock.tlsConfig != nil {
|
||||
tlsConn := tls.Server(conn, mock.tlsConfig)
|
||||
if err := tlsConn.Handshake(); err != nil {
|
||||
dataConn.Done()
|
||||
return
|
||||
}
|
||||
|
||||
conn = tlsConn
|
||||
}
|
||||
|
||||
dataConn.conn = conn
|
||||
dataConn.Done()
|
||||
}()
|
||||
@ -404,15 +421,21 @@ func openConn(t *testing.T, addr string, options ...DialOption) (*ftpMock, *Serv
|
||||
}
|
||||
|
||||
func openConnExt(t *testing.T, addr, modtime string, options ...DialOption) (*ftpMock, *ServerConn) {
|
||||
mock, err := newFtpMockExt(t, addr, modtime)
|
||||
require.NoError(t, err)
|
||||
mock, err := newFtpMockExt(t, addr, false, modtime)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer mock.Close()
|
||||
|
||||
c, err := Dial(mock.Addr(), options...)
|
||||
require.NoError(t, err)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = c.Login("anonymous", "anonymous")
|
||||
require.NoError(t, err)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return mock, c
|
||||
}
|
||||
@ -430,7 +453,9 @@ func closeConn(t *testing.T, mock *ftpMock, c *ServerConn, commands []string) {
|
||||
// Wait for the connection to close
|
||||
mock.Wait()
|
||||
|
||||
assert.Equal(t, expected, mock.commands, "unexpected sequence of commands")
|
||||
if !reflect.DeepEqual(mock.commands, expected) {
|
||||
t.Fatal("unexpected sequence of commands:", mock.commands, "expected:", expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConn4(t *testing.T) {
|
||||
@ -442,3 +467,31 @@ func TestConn6(t *testing.T) {
|
||||
mock, c := openConn(t, "[::1]")
|
||||
closeConn(t, mock, c, nil)
|
||||
}
|
||||
|
||||
func TestConnTLS(t *testing.T) {
|
||||
mock, err := newFtpMockExt(t, "127.0.0.1", true, "std-time")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tlsConf := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
|
||||
c, err := Dial(mock.Addr(), DialWithTLS(tlsConf), DialWithTimeout(1*time.Second))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = c.Login("anonymous", "anonymous")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = c.List(".")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
closeConn(t, mock, c, []string{"PBSZ", "PROT", "EPSV", "LIST"})
|
||||
}
|
||||
|
271
ftp.go
271
ftp.go
@ -11,7 +11,6 @@ import (
|
||||
"io"
|
||||
"net"
|
||||
"net/textproto"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@ -19,12 +18,6 @@ import (
|
||||
"github.com/hashicorp/go-multierror"
|
||||
)
|
||||
|
||||
const (
|
||||
// 30 seconds was chosen as it's the
|
||||
// same duration as http.DefaultTransport's timeout.
|
||||
DefaultDialTimeout = 30 * time.Second
|
||||
)
|
||||
|
||||
// EntryType describes the different types of an Entry.
|
||||
type EntryType int
|
||||
|
||||
@ -35,15 +28,6 @@ const (
|
||||
EntryTypeLink
|
||||
)
|
||||
|
||||
// TransferType denotes the formats for transferring Entries.
|
||||
type TransferType string
|
||||
|
||||
// The different transfer types
|
||||
const (
|
||||
TransferTypeBinary = TransferType("I")
|
||||
TransferTypeASCII = TransferType("A")
|
||||
)
|
||||
|
||||
// Time format used by the MDTM and MFMT commands
|
||||
const timeFormat = "20060102150405"
|
||||
|
||||
@ -73,19 +57,19 @@ type DialOption struct {
|
||||
|
||||
// dialOptions contains all the options set by DialOption.setup
|
||||
type dialOptions struct {
|
||||
context context.Context
|
||||
dialer net.Dialer
|
||||
tlsConfig *tls.Config
|
||||
explicitTLS bool
|
||||
disableEPSV bool
|
||||
disableUTF8 bool
|
||||
disableMLSD bool
|
||||
writingMDTM bool
|
||||
forceListHidden bool
|
||||
location *time.Location
|
||||
debugOutput io.Writer
|
||||
dialFunc func(network, address string) (net.Conn, error)
|
||||
shutTimeout time.Duration // time to wait for data connection closing status
|
||||
context context.Context
|
||||
dialer net.Dialer
|
||||
tlsConfig *tls.Config
|
||||
explicitTLS bool
|
||||
conn net.Conn
|
||||
disableEPSV bool
|
||||
disableUTF8 bool
|
||||
disableMLSD bool
|
||||
writingMDTM bool
|
||||
location *time.Location
|
||||
debugOutput io.Writer
|
||||
dialFunc func(network, address string) (net.Conn, error)
|
||||
shutTimeout time.Duration // time to wait for data connection closing status
|
||||
}
|
||||
|
||||
// Entry describes a file and is returned by List().
|
||||
@ -115,39 +99,27 @@ func Dial(addr string, options ...DialOption) (*ServerConn, error) {
|
||||
do.location = time.UTC
|
||||
}
|
||||
|
||||
dialFunc := do.dialFunc
|
||||
tconn := do.conn
|
||||
if tconn == nil {
|
||||
var err error
|
||||
|
||||
if dialFunc == nil {
|
||||
ctx := do.context
|
||||
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if _, ok := ctx.Deadline(); !ok {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithTimeout(ctx, DefaultDialTimeout)
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
if do.tlsConfig != nil && !do.explicitTLS {
|
||||
dialFunc = func(network, address string) (net.Conn, error) {
|
||||
tlsDialer := &tls.Dialer{
|
||||
NetDialer: &do.dialer,
|
||||
Config: do.tlsConfig,
|
||||
}
|
||||
return tlsDialer.DialContext(ctx, network, addr)
|
||||
}
|
||||
if do.dialFunc != nil {
|
||||
tconn, err = do.dialFunc("tcp", addr)
|
||||
} else if do.tlsConfig != nil && !do.explicitTLS {
|
||||
tconn, err = tls.DialWithDialer(&do.dialer, "tcp", addr, do.tlsConfig)
|
||||
} else {
|
||||
ctx := do.context
|
||||
|
||||
dialFunc = func(network, address string) (net.Conn, error) {
|
||||
return do.dialer.DialContext(ctx, network, addr)
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tconn, err := dialFunc("tcp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
tconn, err = do.dialer.DialContext(ctx, "tcp", addr)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Use the resolved IP address in case addr contains a domain name
|
||||
@ -162,7 +134,7 @@ func Dial(addr string, options ...DialOption) (*ServerConn, error) {
|
||||
host: remoteAddr.IP.String(),
|
||||
}
|
||||
|
||||
_, _, err = c.conn.ReadResponse(StatusReady)
|
||||
_, _, err := c.conn.ReadResponse(StatusReady)
|
||||
if err != nil {
|
||||
_ = c.Quit()
|
||||
return nil, err
|
||||
@ -204,12 +176,10 @@ func DialWithDialer(dialer net.Dialer) DialOption {
|
||||
}
|
||||
|
||||
// DialWithNetConn returns a DialOption that configures the ServerConn with the underlying net.Conn
|
||||
//
|
||||
// Deprecated: Use [DialWithDialFunc] instead
|
||||
func DialWithNetConn(conn net.Conn) DialOption {
|
||||
return DialWithDialFunc(func(network, address string) (net.Conn, error) {
|
||||
return conn, nil
|
||||
})
|
||||
return DialOption{func(do *dialOptions) {
|
||||
do.conn = conn
|
||||
}}
|
||||
}
|
||||
|
||||
// DialWithDisabledEPSV returns a DialOption that configures the ServerConn with EPSV disabled
|
||||
@ -249,16 +219,6 @@ func DialWithWritingMDTM(enabled bool) DialOption {
|
||||
}}
|
||||
}
|
||||
|
||||
// DialWithForceListHidden returns a DialOption making ServerConn use LIST -a to include hidden files and folders in directory listings
|
||||
//
|
||||
// This is useful for servers that do not do this by default, but it forces the use of the LIST command
|
||||
// even if the server supports MLST.
|
||||
func DialWithForceListHidden(enabled bool) DialOption {
|
||||
return DialOption{func(do *dialOptions) {
|
||||
do.forceListHidden = enabled
|
||||
}}
|
||||
}
|
||||
|
||||
// DialWithLocation returns a DialOption that configures the ServerConn with specified time.Location
|
||||
// The location is used to parse the dates sent by the server which are in server's timezone
|
||||
func DialWithLocation(location *time.Location) DialOption {
|
||||
@ -332,15 +292,14 @@ func (o *dialOptions) wrapStream(rd io.ReadCloser) io.ReadCloser {
|
||||
}
|
||||
|
||||
// Connect is an alias to Dial, for backward compatibility
|
||||
//
|
||||
// Deprecated: Use [Dial] instead
|
||||
func Connect(addr string) (*ServerConn, error) {
|
||||
return Dial(addr)
|
||||
}
|
||||
|
||||
// DialTimeout initializes the connection to the specified ftp server address.
|
||||
//
|
||||
// Deprecated: Use [Dial] with [DialWithTimeout] option instead
|
||||
// It is generally followed by a call to Login() as most FTP commands require
|
||||
// an authenticated user.
|
||||
func DialTimeout(addr string, timeout time.Duration) (*ServerConn, error) {
|
||||
return Dial(addr, DialWithTimeout(timeout))
|
||||
}
|
||||
@ -381,7 +340,7 @@ func (c *ServerConn) Login(user, password string) error {
|
||||
c.mdtmCanWrite = c.mdtmSupported && c.options.writingMDTM
|
||||
|
||||
// Switch to binary mode
|
||||
if err = c.Type(TransferTypeBinary); err != nil {
|
||||
if _, _, err = c.cmd(StatusCommandOK, "TYPE I"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -529,26 +488,9 @@ func (c *ServerConn) pasv() (host string, port int, err error) {
|
||||
|
||||
// Make the IP address to connect to
|
||||
host = strings.Join(pasvData[0:4], ".")
|
||||
|
||||
if c.host != host {
|
||||
if cmdIP := net.ParseIP(c.host); cmdIP != nil {
|
||||
if dataIP := net.ParseIP(host); dataIP != nil {
|
||||
if isBogusDataIP(cmdIP, dataIP) {
|
||||
return c.host, port, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return host, port, nil
|
||||
}
|
||||
|
||||
func isBogusDataIP(cmdIP, dataIP net.IP) bool {
|
||||
// Logic stolen from lftp (https://github.com/lavv17/lftp/blob/d67fc14d085849a6b0418bb3e912fea2e94c18d1/src/ftpclass.cc#L769)
|
||||
return dataIP.IsMulticast() ||
|
||||
cmdIP.IsPrivate() != dataIP.IsPrivate() ||
|
||||
cmdIP.IsLoopback() != dataIP.IsLoopback()
|
||||
}
|
||||
|
||||
// getDataConnPort returns a host, port for a new data connection
|
||||
// it uses the best available method to do so
|
||||
func (c *ServerConn) getDataConnPort() (string, int, error) {
|
||||
@ -577,24 +519,7 @@ func (c *ServerConn) openDataConn() (net.Conn, error) {
|
||||
}
|
||||
|
||||
if c.options.tlsConfig != nil {
|
||||
// We don't use tls.DialWithDialer here (which does Dial, create
|
||||
// the Client and then do the Handshake) because it seems to
|
||||
// hang with some FTP servers, namely proftpd and pureftpd.
|
||||
//
|
||||
// Instead we do Dial, create the Client and wait for the first
|
||||
// Read or Write to trigger the Handshake.
|
||||
//
|
||||
// This means that if we are uploading a zero sized file, we
|
||||
// need to make sure we do the Handshake explicitly as Write
|
||||
// won't have been called. This is done in StorFrom().
|
||||
//
|
||||
// See: https://github.com/jlaffaye/ftp/issues/282
|
||||
conn, err := c.options.dialer.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsConn := tls.Client(conn, c.options.tlsConfig)
|
||||
return tlsConn, nil
|
||||
return tls.DialWithDialer(&c.options.dialer, "tcp", addr, c.options.tlsConfig)
|
||||
}
|
||||
|
||||
return c.options.dialer.Dial("tcp", addr)
|
||||
@ -655,12 +580,6 @@ func (c *ServerConn) cmdDataConnFrom(offset uint64, format string, args ...inter
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// Type switches the transfer mode for the connection.
|
||||
func (c *ServerConn) Type(transferType TransferType) (err error) {
|
||||
_, _, err = c.cmd(StatusCommandOK, "TYPE "+string(transferType))
|
||||
return err
|
||||
}
|
||||
|
||||
// NameList issues an NLST FTP command.
|
||||
func (c *ServerConn) NameList(path string) (entries []string, err error) {
|
||||
space := " "
|
||||
@ -696,14 +615,11 @@ func (c *ServerConn) List(path string) (entries []*Entry, err error) {
|
||||
var cmd string
|
||||
var parser parseFunc
|
||||
|
||||
if c.mlstSupported && !c.options.forceListHidden {
|
||||
if c.mlstSupported {
|
||||
cmd = "MLSD"
|
||||
parser = parseRFC3659ListLine
|
||||
} else {
|
||||
cmd = "LIST"
|
||||
if c.options.forceListHidden {
|
||||
cmd += " -a"
|
||||
}
|
||||
parser = parseListLine
|
||||
}
|
||||
|
||||
@ -724,7 +640,9 @@ func (c *ServerConn) List(path string) (entries []*Entry, err error) {
|
||||
now := time.Now()
|
||||
for scanner.Scan() {
|
||||
entry, errParse := parser(scanner.Text(), now, c.options.location)
|
||||
if errParse == nil {
|
||||
if errParse != nil {
|
||||
errs = multierror.Append(errs, errParse)
|
||||
} else {
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
}
|
||||
@ -739,62 +657,6 @@ func (c *ServerConn) List(path string) (entries []*Entry, err error) {
|
||||
return entries, errs.ErrorOrNil()
|
||||
}
|
||||
|
||||
// GetEntry issues a MLST FTP command which retrieves one single Entry using the
|
||||
// control connection. The returnedEntry will describe the current directory
|
||||
// when no path is given.
|
||||
func (c *ServerConn) GetEntry(path string) (entry *Entry, err error) {
|
||||
if !c.mlstSupported {
|
||||
return nil, &textproto.Error{Code: StatusNotImplemented, Msg: StatusText(StatusNotImplemented)}
|
||||
}
|
||||
space := " "
|
||||
if path == "" {
|
||||
space = ""
|
||||
}
|
||||
_, msg, err := c.cmd(StatusRequestedFileActionOK, "%s%s%s", "MLST", space, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// The expected reply will look something like:
|
||||
//
|
||||
// 250-File details
|
||||
// Type=file;Size=1024;Modify=20220813133357; path
|
||||
// 250 End
|
||||
//
|
||||
// Multiple lines are allowed though, so it can also be in the form:
|
||||
//
|
||||
// 250-File details
|
||||
// Type=file;Size=1024; path
|
||||
// Modify=20220813133357; path
|
||||
// 250 End
|
||||
lines := strings.Split(msg, "\n")
|
||||
lc := len(lines)
|
||||
|
||||
// lines must be a multi-line message with a length of 3 or more, and we
|
||||
// don't care about the first and last line
|
||||
if lc < 3 {
|
||||
return nil, errors.New("invalid response")
|
||||
}
|
||||
|
||||
e := &Entry{}
|
||||
for _, l := range lines[1 : lc-1] {
|
||||
// According to RFC 3659, the entry lines must start with a space when passed over the
|
||||
// control connection. Some servers don't seem to add that space though. Both forms are
|
||||
// accepted here.
|
||||
if len(l) > 0 && l[0] == ' ' {
|
||||
l = l[1:]
|
||||
}
|
||||
// Some severs seem to send a blank line at the end which we ignore
|
||||
if l == "" {
|
||||
continue
|
||||
}
|
||||
if e, err = parseNextRFC3659ListLine(l, c.options.location, e); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// IsTimePreciseInList returns true if client and server support the MLSD
|
||||
// command so List can return time with 1-second precision for all files.
|
||||
func (c *ServerConn) IsTimePreciseInList() bool {
|
||||
@ -828,7 +690,7 @@ func (c *ServerConn) CurrentDir() (string, error) {
|
||||
end := strings.LastIndex(msg, "\"")
|
||||
|
||||
if start == -1 || end == -1 {
|
||||
return "", errors.New("unsupported PWD response format")
|
||||
return "", errors.New("unsuported PWD response format")
|
||||
}
|
||||
|
||||
return msg[start+1 : end], nil
|
||||
@ -951,21 +813,8 @@ func (c *ServerConn) StorFrom(path string, r io.Reader, offset uint64) error {
|
||||
// response otherwise if the failure is not due to a connection problem,
|
||||
// for example the server denied the upload for quota limits, we miss
|
||||
// the response and we cannot use the connection to send other commands.
|
||||
if n, err := io.Copy(conn, r); err != nil {
|
||||
if _, err := io.Copy(conn, r); err != nil {
|
||||
errs = multierror.Append(errs, err)
|
||||
} else if n == 0 {
|
||||
// If we wrote no bytes and got no error, make sure we call
|
||||
// tls.Handshake on the connection as it won't get called
|
||||
// unless Write() is called. (See comment in openDataConn()).
|
||||
//
|
||||
// ProFTP doesn't like this and returns "Unable to build data
|
||||
// connection: Operation not permitted" when trying to upload
|
||||
// an empty file without this.
|
||||
if do, ok := conn.(interface{ Handshake() error }); ok {
|
||||
if err := do.Handshake(); err != nil {
|
||||
errs = multierror.Append(errs, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := conn.Close(); err != nil {
|
||||
@ -1079,7 +928,7 @@ func (c *ServerConn) RemoveDir(path string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Walk prepares the internal walk function so that the caller can begin traversing the directory
|
||||
//Walk prepares the internal walk function so that the caller can begin traversing the directory
|
||||
func (c *ServerConn) Walk(root string) *Walker {
|
||||
w := new(Walker)
|
||||
w.serverConn = c
|
||||
@ -1094,38 +943,6 @@ func (c *ServerConn) Walk(root string) *Walker {
|
||||
return w
|
||||
}
|
||||
|
||||
// Search returns all the directories matching the search pattern
|
||||
func (c *ServerConn) Search(pattern string) ([]string, error) {
|
||||
_, message, err := c.cmd(StatusCommandOK, "SITE SEARCH %s", pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
msgs := make([]string, 0)
|
||||
re := regexp.MustCompile(`^[^\/]*(?P<Path>\/.*) \(.*\).*$`)
|
||||
for _, msg := range strings.Split(message, "\n") {
|
||||
if re.MatchString(msg) {
|
||||
msgs = append(msgs, re.ReplaceAllString(msg, "${Path}"))
|
||||
}
|
||||
}
|
||||
|
||||
return msgs, nil
|
||||
}
|
||||
|
||||
// Search returns all the directories matching the search pattern
|
||||
func (c *ServerConn) Searches(patterns []string) ([]string, error) {
|
||||
msgs := make([]string, 0)
|
||||
for _, pattern := range patterns {
|
||||
msg, err := c.Search(pattern)
|
||||
if err != nil {
|
||||
return msgs, err
|
||||
}
|
||||
msgs = append(msgs, msg...)
|
||||
}
|
||||
|
||||
return msgs, nil
|
||||
}
|
||||
|
||||
// NoOp issues a NOOP FTP command.
|
||||
// NOOP has no effects and is usually used to prevent the remote FTP server to
|
||||
// close the otherwise idle connection.
|
||||
|
22
ftp_test.go
22
ftp_test.go
@ -1,22 +0,0 @@
|
||||
package ftp
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBogusDataIP(t *testing.T) {
|
||||
for _, tC := range []struct {
|
||||
cmd, data net.IP
|
||||
bogus bool
|
||||
}{
|
||||
{net.IPv4(192, 168, 1, 1), net.IPv4(192, 168, 1, 1), false},
|
||||
{net.IPv4(192, 168, 1, 1), net.IPv4(1, 1, 1, 1), true},
|
||||
{net.IPv4(10, 65, 1, 1), net.IPv4(1, 1, 1, 1), true},
|
||||
{net.IPv4(10, 65, 25, 1), net.IPv4(10, 65, 8, 1), false},
|
||||
} {
|
||||
if got, want := isBogusDataIP(tC.cmd, tC.data), tC.bogus; got != want {
|
||||
t.Errorf("%s,%s got %t, wanted %t", tC.cmd, tC.data, got, want)
|
||||
}
|
||||
}
|
||||
}
|
10
go.mod
10
go.mod
@ -1,15 +1,15 @@
|
||||
module git.siteop.biz/brouzouf/ftp
|
||||
module github.com/jlaffaye/ftp
|
||||
|
||||
go 1.23.2
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/stretchr/testify v1.6.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.0.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
|
||||
)
|
||||
|
13
go.sum
13
go.sum
@ -1,14 +1,15 @@
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
16
parse.go
16
parse.go
@ -24,16 +24,10 @@ var listLineParsers = []parseFunc{
|
||||
var dirTimeFormats = []string{
|
||||
"01-02-06 03:04PM",
|
||||
"2006-01-02 15:04",
|
||||
"01-02-2006 03:04PM",
|
||||
"01-02-2006 15:04",
|
||||
}
|
||||
|
||||
// parseRFC3659ListLine parses the style of directory line defined in RFC 3659.
|
||||
func parseRFC3659ListLine(line string, _ time.Time, loc *time.Location) (*Entry, error) {
|
||||
return parseNextRFC3659ListLine(line, loc, &Entry{})
|
||||
}
|
||||
|
||||
func parseNextRFC3659ListLine(line string, loc *time.Location, e *Entry) (*Entry, error) {
|
||||
func parseRFC3659ListLine(line string, now time.Time, loc *time.Location) (*Entry, error) {
|
||||
iSemicolon := strings.Index(line, ";")
|
||||
iWhitespace := strings.Index(line, " ")
|
||||
|
||||
@ -41,12 +35,8 @@ func parseNextRFC3659ListLine(line string, loc *time.Location, e *Entry) (*Entry
|
||||
return nil, errUnsupportedListLine
|
||||
}
|
||||
|
||||
name := line[iWhitespace+1:]
|
||||
if e.Name == "" {
|
||||
e.Name = name
|
||||
} else if e.Name != name {
|
||||
// All lines must have the same name
|
||||
return nil, errUnsupportedListLine
|
||||
e := &Entry{
|
||||
Name: line[iWhitespace+1:],
|
||||
}
|
||||
|
||||
for _, field := range strings.Split(line[:iWhitespace-1], ";") {
|
||||
|
@ -68,9 +68,7 @@ var listTests = []line{
|
||||
// DOS DIR command output
|
||||
{"08-07-15 07:50PM 718 Post_PRR_20150901_1166_265118_13049.dat", "Post_PRR_20150901_1166_265118_13049.dat", 718, EntryTypeFile, newTime(2015, time.August, 7, 19, 50)},
|
||||
{"08-10-15 02:04PM <DIR> Billing", "Billing", 0, EntryTypeFolder, newTime(2015, time.August, 10, 14, 4)},
|
||||
{"08-07-2015 07:50PM 718 Post_PRR_20150901_1166_265118_13049.dat", "Post_PRR_20150901_1166_265118_13049.dat", 718, EntryTypeFile, newTime(2015, time.August, 7, 19, 50)},
|
||||
{"08-10-2015 02:04PM <DIR> Billing", "Billing", 0, EntryTypeFolder, newTime(2015, time.August, 10, 14, 4)},
|
||||
|
||||
|
||||
// dir and file names that contain multiple spaces
|
||||
{"drwxr-xr-x 3 110 1002 3 Dec 02 2009 spaces dir name", "spaces dir name", 0, EntryTypeFolder, newTime(2009, time.December, 2)},
|
||||
{"-rwxr-xr-x 3 110 1002 1234567 Dec 02 2009 file name", "file name", 1234567, EntryTypeFile, newTime(2009, time.December, 2)},
|
||||
|
Loading…
Reference in New Issue
Block a user