conn_test.go 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. package ftp
  2. import (
  3. "bytes"
  4. "errors"
  5. "io"
  6. "net"
  7. "net/textproto"
  8. "reflect"
  9. "strconv"
  10. "strings"
  11. "sync"
  12. "testing"
  13. )
  14. type ftpMock struct {
  15. address string
  16. listener *net.TCPListener
  17. proto *textproto.Conn
  18. commands []string // list of received commands
  19. rest int
  20. fileCont *bytes.Buffer
  21. dataConn *mockDataConn
  22. sync.WaitGroup
  23. }
  24. // newFtpMock returns a mock implementation of a FTP server
  25. // For simplication, a mock instance only accepts a signle connection and terminates afer
  26. func newFtpMock(t *testing.T, address string) (*ftpMock, error) {
  27. var err error
  28. mock := &ftpMock{address: address}
  29. l, err := net.Listen("tcp", address+":0")
  30. if err != nil {
  31. return nil, err
  32. }
  33. tcpListener, ok := l.(*net.TCPListener)
  34. if !ok {
  35. return nil, errors.New("listener is not a net.TCPListener")
  36. }
  37. mock.listener = tcpListener
  38. go mock.listen(t)
  39. return mock, nil
  40. }
  41. func (mock *ftpMock) listen(t *testing.T) {
  42. // Listen for an incoming connection.
  43. conn, err := mock.listener.Accept()
  44. if err != nil {
  45. t.Errorf("can not accept: %s", err)
  46. return
  47. }
  48. // Do not accept incoming connections anymore
  49. mock.listener.Close()
  50. mock.Add(1)
  51. defer mock.Done()
  52. defer conn.Close()
  53. mock.proto = textproto.NewConn(conn)
  54. mock.proto.Writer.PrintfLine("220 FTP Server ready.")
  55. for {
  56. fullCommand, _ := mock.proto.ReadLine()
  57. cmdParts := strings.Split(fullCommand, " ")
  58. // Append to list of received commands
  59. mock.commands = append(mock.commands, cmdParts[0])
  60. // At least one command must have a multiline response
  61. switch cmdParts[0] {
  62. case "FEAT":
  63. mock.proto.Writer.PrintfLine("211-Features:\r\n FEAT\r\n PASV\r\n EPSV\r\n SIZE\r\n211 End")
  64. case "USER":
  65. if cmdParts[1] == "anonymous" {
  66. mock.proto.Writer.PrintfLine("331 Please send your password")
  67. } else {
  68. mock.proto.Writer.PrintfLine("530 This FTP server is anonymous only")
  69. }
  70. case "PASS":
  71. mock.proto.Writer.PrintfLine("230-Hey,\r\nWelcome to my FTP\r\n230 Access granted")
  72. case "TYPE":
  73. mock.proto.Writer.PrintfLine("200 Type set ok")
  74. case "CWD":
  75. if cmdParts[1] == "missing-dir" {
  76. mock.proto.Writer.PrintfLine("550 %s: No such file or directory", cmdParts[1])
  77. } else {
  78. mock.proto.Writer.PrintfLine("250 Directory successfully changed.")
  79. }
  80. case "DELE":
  81. mock.proto.Writer.PrintfLine("250 File successfully removed.")
  82. case "MKD":
  83. mock.proto.Writer.PrintfLine("257 Directory successfully created.")
  84. case "RMD":
  85. if cmdParts[1] == "missing-dir" {
  86. mock.proto.Writer.PrintfLine("550 No such file or directory")
  87. } else {
  88. mock.proto.Writer.PrintfLine("250 Directory successfully removed.")
  89. }
  90. case "PWD":
  91. mock.proto.Writer.PrintfLine("257 \"/incoming\"")
  92. case "CDUP":
  93. mock.proto.Writer.PrintfLine("250 CDUP command successful")
  94. case "SIZE":
  95. if cmdParts[1] == "magic-file" {
  96. mock.proto.Writer.PrintfLine("213 42")
  97. } else {
  98. mock.proto.Writer.PrintfLine("550 Could not get file size.")
  99. }
  100. case "PASV":
  101. p, err := mock.listenDataConn()
  102. if err != nil {
  103. mock.proto.Writer.PrintfLine("451 %s.", err)
  104. break
  105. }
  106. p1 := int(p / 256)
  107. p2 := p % 256
  108. mock.proto.Writer.PrintfLine("227 Entering Passive Mode (127,0,0,1,%d,%d).", p1, p2)
  109. case "EPSV":
  110. p, err := mock.listenDataConn()
  111. if err != nil {
  112. mock.proto.Writer.PrintfLine("451 %s.", err)
  113. break
  114. }
  115. mock.proto.Writer.PrintfLine("229 Entering Extended Passive Mode (|||%d|)", p)
  116. case "STOR":
  117. if mock.dataConn == nil {
  118. mock.proto.Writer.PrintfLine("425 Unable to build data connection: Connection refused")
  119. break
  120. }
  121. mock.proto.Writer.PrintfLine("150 please send")
  122. mock.recvDataConn(false)
  123. case "APPE":
  124. if mock.dataConn == nil {
  125. mock.proto.Writer.PrintfLine("425 Unable to build data connection: Connection refused")
  126. break
  127. }
  128. mock.proto.Writer.PrintfLine("150 please send")
  129. mock.recvDataConn(true)
  130. case "LIST":
  131. if mock.dataConn == nil {
  132. mock.proto.Writer.PrintfLine("425 Unable to build data connection: Connection refused")
  133. break
  134. }
  135. mock.dataConn.Wait()
  136. mock.proto.Writer.PrintfLine("150 Opening ASCII mode data connection for file list")
  137. mock.dataConn.conn.Write([]byte("-rw-r--r-- 1 ftp wheel 0 Jan 29 10:29 lo"))
  138. mock.proto.Writer.PrintfLine("226 Transfer complete")
  139. mock.closeDataConn()
  140. case "NLST":
  141. if mock.dataConn == nil {
  142. mock.proto.Writer.PrintfLine("425 Unable to build data connection: Connection refused")
  143. break
  144. }
  145. mock.dataConn.Wait()
  146. mock.proto.Writer.PrintfLine("150 Opening ASCII mode data connection for file list")
  147. mock.dataConn.conn.Write([]byte("/incoming"))
  148. mock.proto.Writer.PrintfLine("226 Transfer complete")
  149. mock.closeDataConn()
  150. case "RETR":
  151. if mock.dataConn == nil {
  152. mock.proto.Writer.PrintfLine("425 Unable to build data connection: Connection refused")
  153. break
  154. }
  155. mock.dataConn.Wait()
  156. mock.proto.Writer.PrintfLine("150 Opening ASCII mode data connection for file list")
  157. mock.dataConn.conn.Write(mock.fileCont.Bytes()[mock.rest:])
  158. mock.rest = 0
  159. mock.proto.Writer.PrintfLine("226 Transfer complete")
  160. mock.closeDataConn()
  161. case "RNFR":
  162. mock.proto.Writer.PrintfLine("350 File or directory exists, ready for destination name")
  163. case "RNTO":
  164. mock.proto.Writer.PrintfLine("250 Rename successful")
  165. case "REST":
  166. if len(cmdParts) != 2 {
  167. mock.proto.Writer.PrintfLine("500 wrong number of arguments")
  168. break
  169. }
  170. rest, err := strconv.Atoi(cmdParts[1])
  171. if err != nil {
  172. mock.proto.Writer.PrintfLine("500 REST: %s", err)
  173. break
  174. }
  175. mock.rest = rest
  176. mock.proto.Writer.PrintfLine("350 Restarting at %s. Send STORE or RETRIEVE to initiate transfer", cmdParts[1])
  177. case "NOOP":
  178. mock.proto.Writer.PrintfLine("200 NOOP ok.")
  179. case "REIN":
  180. mock.proto.Writer.PrintfLine("220 Logged out")
  181. case "QUIT":
  182. mock.proto.Writer.PrintfLine("221 Goodbye.")
  183. return
  184. default:
  185. mock.proto.Writer.PrintfLine("500 Unknown command %s.", cmdParts[0])
  186. }
  187. }
  188. }
  189. func (mock *ftpMock) closeDataConn() (err error) {
  190. if mock.dataConn != nil {
  191. err = mock.dataConn.Close()
  192. mock.dataConn = nil
  193. }
  194. return
  195. }
  196. type mockDataConn struct {
  197. listener *net.TCPListener
  198. conn net.Conn
  199. // WaitGroup is done when conn is accepted and stored
  200. sync.WaitGroup
  201. }
  202. func (d *mockDataConn) Close() (err error) {
  203. if d.listener != nil {
  204. err = d.listener.Close()
  205. }
  206. if d.conn != nil {
  207. err = d.conn.Close()
  208. }
  209. return
  210. }
  211. func (mock *ftpMock) listenDataConn() (int64, error) {
  212. mock.closeDataConn()
  213. l, err := net.Listen("tcp", mock.address+":0")
  214. if err != nil {
  215. return 0, err
  216. }
  217. tcpListener, ok := l.(*net.TCPListener)
  218. if !ok {
  219. return 0, errors.New("listener is not a net.TCPListener")
  220. }
  221. addr := tcpListener.Addr().String()
  222. _, port, err := net.SplitHostPort(addr)
  223. if err != nil {
  224. return 0, err
  225. }
  226. p, err := strconv.ParseInt(port, 10, 32)
  227. if err != nil {
  228. return 0, err
  229. }
  230. dataConn := &mockDataConn{listener: tcpListener}
  231. dataConn.Add(1)
  232. go func() {
  233. // Listen for an incoming connection.
  234. conn, err := dataConn.listener.Accept()
  235. if err != nil {
  236. // t.Errorf("can not accept: %s", err)
  237. return
  238. }
  239. dataConn.conn = conn
  240. dataConn.Done()
  241. }()
  242. mock.dataConn = dataConn
  243. return p, nil
  244. }
  245. func (mock *ftpMock) recvDataConn(append bool) {
  246. mock.dataConn.Wait()
  247. if !append {
  248. mock.fileCont = new(bytes.Buffer)
  249. }
  250. io.Copy(mock.fileCont, mock.dataConn.conn)
  251. mock.proto.Writer.PrintfLine("226 Transfer Complete")
  252. mock.closeDataConn()
  253. }
  254. func (mock *ftpMock) Addr() string {
  255. return mock.listener.Addr().String()
  256. }
  257. // Closes the listening socket
  258. func (mock *ftpMock) Close() {
  259. mock.listener.Close()
  260. }
  261. // Helper to return a client connected to a mock server
  262. func openConn(t *testing.T, addr string, options ...DialOption) (*ftpMock, *ServerConn) {
  263. mock, err := newFtpMock(t, addr)
  264. if err != nil {
  265. t.Fatal(err)
  266. }
  267. defer mock.Close()
  268. c, err := Dial(mock.Addr(), options...)
  269. if err != nil {
  270. t.Fatal(err)
  271. }
  272. err = c.Login("anonymous", "anonymous")
  273. if err != nil {
  274. t.Fatal(err)
  275. }
  276. return mock, c
  277. }
  278. // Helper to close a client connected to a mock server
  279. func closeConn(t *testing.T, mock *ftpMock, c *ServerConn, commands []string) {
  280. expected := []string{"FEAT", "USER", "PASS", "TYPE"}
  281. expected = append(expected, commands...)
  282. expected = append(expected, "QUIT")
  283. if err := c.Quit(); err != nil {
  284. t.Fatal(err)
  285. }
  286. // Wait for the connection to close
  287. mock.Wait()
  288. if !reflect.DeepEqual(mock.commands, expected) {
  289. t.Fatal("unexpected sequence of commands:", mock.commands, "expected:", expected)
  290. }
  291. }
  292. func TestConn4(t *testing.T) {
  293. mock, c := openConn(t, "127.0.0.1")
  294. closeConn(t, mock, c, nil)
  295. }
  296. func TestConn6(t *testing.T) {
  297. mock, c := openConn(t, "[::1]")
  298. closeConn(t, mock, c, nil)
  299. }