File: protocol.go

package info (click to toggle)
golang-github-smira-go-ftp-protocol 0.0~git20140829.066b75c-2
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, bullseye, bullseye-backports, buster, buster-backports, forky, sid, trixie
  • size: 72 kB
  • sloc: makefile: 4
file content (152 lines) | stat: -rw-r--r-- 3,430 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
// Package protocol implements  ftp:// scheme plugin for http.Transport
//
// github.com/jlaffaye/ftp library is used internally as FTP client implementation.
//
// Limitations: only anonymous FTP servers, only file retrieval operations.
//
// Internally connections to FTP servers are cached and re-used when possible.
//
// Example:
//
//    transport := &http.Transport{}
//    transport.RegisterProtocol("ftp", &FTPRoundTripper{})
//    client := &http.Client{Transport: transport}
//    resp, err := client.Get("ftp://ftp.ru.debian.org/debian/README")
package protocol

import (
	"fmt"
	"github.com/jlaffaye/ftp"
	"io"
	"net/http"
	"net/textproto"
	"strings"
	"sync"
)

// FTPRoundTripper is an implementation of net/http.RoundTripper on top of FTP client
type FTPRoundTripper struct {
	lock            sync.Mutex
	idleConnections map[string][]*ftp.ServerConn
}

// verify interface
var (
	_ http.RoundTripper = &FTPRoundTripper{}
)

type readCloserWrapper struct {
	body     io.ReadCloser
	rt       *FTPRoundTripper
	hostport string
	conn     *ftp.ServerConn
}

func (w *readCloserWrapper) Read(p []byte) (n int, err error) {
	return w.body.Read(p)
}

func (w *readCloserWrapper) Close() error {
	err := w.body.Close()
	if err == nil {
		w.rt.putConnection(w.hostport, w.conn)
	}

	return err
}

func (rt *FTPRoundTripper) getConnection(hostport string) (conn *ftp.ServerConn, err error) {
	rt.lock.Lock()
	conns, ok := rt.idleConnections[hostport]
	if ok && len(conns) > 0 {
		conn = conns[0]
		rt.idleConnections[hostport] = conns[1:]
		rt.lock.Unlock()
		return
	}
	rt.lock.Unlock()

	conn, err = ftp.Connect(hostport)
	if err != nil {
		return nil, err
	}

	err = conn.Login("anonymous", "anonymous")
	if err != nil {
		conn.Quit()
		return nil, err
	}

	return conn, nil
}

func (rt *FTPRoundTripper) putConnection(hostport string, conn *ftp.ServerConn) {
	rt.lock.Lock()
	defer rt.lock.Unlock()

	if rt.idleConnections == nil {
		rt.idleConnections = make(map[string][]*ftp.ServerConn)
	}

	rt.idleConnections[hostport] = append(rt.idleConnections[hostport], conn)
}

// RoundTrip parses incoming GET "HTTP" request and transforms it into
// commands to ftp client
func (rt *FTPRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
	if request.URL.Scheme != "ftp" {
		return nil, fmt.Errorf("only ftp protocol is supported, got %s", request.Proto)
	}

	if request.Method != "GET" {
		return nil, fmt.Errorf("only GET method is supported, got %s", request.Method)
	}

	hostport := request.URL.Host
	if strings.Index(hostport, ":") == -1 {
		hostport = hostport + ":21"
	}

	connection, err := rt.getConnection(hostport)
	if err != nil {
		return nil, err
	}

	var body io.ReadCloser
	body, err = connection.Retr(request.URL.Path)

	if err != nil {
		if te, ok := err.(*textproto.Error); ok {
			rt.putConnection(hostport, connection)

			if te.Code == ftp.StatusFileUnavailable {
				return &http.Response{
					Status:     "404 Not Found",
					StatusCode: 404,
					Proto:      "FTP/1.0",
					ProtoMajor: 1,
					ProtoMinor: 0,
					Request:    request,
				}, nil
			}
		}

		return nil, err
	}

	return &http.Response{
		Status:     "200 OK",
		StatusCode: 200,
		Proto:      "FTP/1.0",
		ProtoMajor: 1,
		ProtoMinor: 0,
		Body: &readCloserWrapper{
			body:     body,
			rt:       rt,
			hostport: hostport,
			conn:     connection,
		},
		ContentLength: -1,
		Request:       request,
	}, nil
}