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
}
|