1
2 type ftp
3 {
4 str path // Дополнительный путь при открытии
5 socket sock // Главный сокет
6 socket sockdata // Сокет передачи данных
7 socket sockserv // Сокет передачи данных PORT
8 sockaddr local // Локальный адрес
9 uint notify // Функция для уведомлений
10 inetnotify ni
11 uint anonymous // 1 если anonymous
12 uint passive // 1 если passive mode
13 }
14
15 define
16 {
17 FTP_OPENING = 150 /* 150 Opening data connection*/
18 FTP_ENDTRAN = 226 /* 226 Transfer Complete */
19 FTP_OK = 200
20 FTP_FILESTAT = 213 // File status.
21 FTP_HELLO = 220
22 FTP_GOODBYE = 221
23 FTP_PASSIVEOK = 227
24 FTP_LOGINOK = 230
25 FTP_CWDOK = 250 /* 250 CWD command successful. */
26 FTP_MKDIROK = 257
27 FTP_PASSWD = 331
28 FTP_FILEOK = 350 /* RNFR command successful */
29 FTP_LOGINBAD = 530
30 FTP_NOTFOUND = 550 /* 550 No such file or directory */
31 }
32
33 define <export>
34 {
35 FTP_ANONYM = 0x0001 // anonymous
36 FTP_PASV = 0x0002 // passive mode
37 FTP_BINARY = 0x0004 // TYPE как IMAGE
38 FTP_TEXT = 0x0008 // TYPE как текстовый файл
39 FTP_CONTINUE = 0x0010 // докачка
40 FTP_SETTIME = 0x0020 // для get - установить время
41 FTP_STR = 0x0100 // для get - добавлять 0 байт в конце
42 FTP_FILE = 0x1000 // для метода get - закачка в файл
43
44 S_IRWXU = 0x0700
45 S_IRUSR = 0x0400
46 S_IWUSR = 0x0200
47 S_IXUSR = 0x0100
48 S_IRWXG = 0x0070
49 S_IRGRP = 0x0040
50 S_IWGRP = 0x0020
51 S_IXGRP = 0x0010
52 S_IRWXO = 0x0007
53 S_IROTH = 0x0004
54 S_IWOTH = 0x0002
55 S_IXOTH = 0x0001
56 }
57
58 method uint ftp.notify( uint code )
59 {
60 if !this.notify : return 1
61
62 if !this.notify->func( code, this.ni )
63 {
64 ineterror = $ERRINET_USERBREAK
65 // ret = 0
66 return 0
67 }
68 if code == $NFYINET_ERROR : return 0
69 return 1
70 }
71
72 method uint ftp.cmdresponse
73 {
74 uint ret i
75 buf data
76 arrstr lines
77
78 subfunc uint nodigit( ubyte ch )
79 {
80 return ch < '0' || ch > '9'
81 }
82 label again
83 this.sock.recv( data )
84 data += byte( 0 )
85 this.ni.head = data->str
86
87 if *data == 1 : return 0
88
89 this.ni.head.split( lines, 0xA, 0 )
90 foreach cur, lines
91 {
92 if nodigit( cur[0] ) || nodigit( cur[1] ) || nodigit( cur[2] ) ||
93 ( cur[3] != ' ' && cur[3] != '-' )
94 {
95 ineterror = $ERRFTP_RESPONSE
96 return 0
97 }
98 ret = uint( cur )
99 }
100 if (lines[ *lines - 1 ])[ 3 ] != ' '
101 {
102 data.use--
103 goto again
104 }
105
106 this.notify( $NFYFTP_RESPONSE )
107 return ret
108 }
109
110 method str ftp.lastresponse( str out )
111 {
112 return out = this.ni.head
113 }
114
115 method uint ftp.sendcmd( str cmd )
116 {
117 this.ni.head = cmd
118 if !this.ni.head.islast( 0xA ) : this.ni.head += "\l"
119
120 if !this.sock.send( this.ni.head ) : return this.notify( $NFYINET_ERROR )
121 this.notify( $NFYFTP_SENDCMD )
122
123 return 1
124 }
125
126 method uint ftp.command( str cmd )
127 {
128 if !this.sendcmd( cmd ) : return 0
129 return this.cmdresponse()
130 }
131
132 method uint ftp.close
133 {
134 uint cmd ret = 1
135
136 if this.sock.socket
137 {
138 cmd = this.command( "QUIT" )
139 if cmd && cmd != $FTP_GOODBYE
140 {
141 ineterror = $ERRFTP_QUIT
142 this.notify( $NFYINET_ERROR )
143 ret = 0
144 }
145 this.sock.close( )
146 }
147 return ret
148 }
149
150 method uint ftp.open( str url user password, uint flag notify )
151 {
152 uint len
153 str host
154
155 this.notify = notify
156 ineterror = 0
157 this.ni.url = url
158
159 if flag & $FTP_ANONYM : this.anonymous = 1
160 if flag & $FTP_PASV : this.passive = 1
161
162 this.notify( $NFYINET_CONNECT )
163
164 this.sock.flag |= $SOCKF_FTP
165
166 if !this.sock.urlconnect( url, host, this.path ) : goto error
167 if this.cmdresponse() != $FTP_HELLO : goto error
168 len = sizeof( sockaddr )
169
170 if getsockname( this.sock.socket, &this.local, &len )
171 {
172 inet_seterror()
173 goto error
174 }
175
176 if !*user || this.anonymous : user = "anonymous"
177
178 if !this.sendcmd( "USER \( user )" ) : return 0
179
180 switch this.cmdresponse( )
181 {
182 case $FTP_LOGINOK {}
183 case $FTP_LOGINBAD
184 {
185 ineterror = $ERRFTP_BADUSER
186 goto error
187 }
188 case $FTP_PASSWD
189 {
190 if !this.sendcmd( "PASS \( password )" ) : return 0
191
192 switch this.cmdresponse()
193 {
194 case $FTP_LOGINOK {}
195 case $FTP_LOGINBAD
196 {
197 ineterror = $ERRFTP_BADPSW
198 goto error
199 }
200 default
201 {
202 ineterror = $ERRFTP_RESPONSE
203 goto error
204 }
205 }
206 }
207 default
208 {
209 ineterror = $ERRFTP_RESPONSE
210 goto error
211 }
212 }
213 return 1
214
215 label error
216 this.notify( $NFYINET_ERROR )
217 this.close( )
218 return 0
219 }
220
221 method uint ftp.listen
222 {
223 uint cmd sin addr port
224 sockaddr in
225
226 if this.passive
227 {
228 cmd = this.command( "PASV" )
229 if cmd == $FTP_PASSIVEOK
230 {
231 str ports response
232 uint off till
233 arrstr portval
234
235 off = this.lastresponse( response ).findch( '(', 0 ) + 1
236 till = response.findchfrom( ')', off )
237 ports.substr( response, off, till - off )
238 ports.split( portval, ',', 0 )
239 if *portval != 6
240 {
241 ineterror = $ERRFTP_RESPONSE
242 goto error
243 }
244 this.sockdata.host = "\(portval[0]).\(portval[1]).\(portval[2]).\(portval[3])"
245 this.sockdata.port = (uint( portval[4] ) << 8) + uint( portval[5] )
246 if this.sockdata.connect() : return 1
247 }
248 this.passive = 0
249 this.notify( $NFYFTP_NOTPASV )
250 }
251
252 this.sockserv.socket = createsocket( $AF_INET, $SOCK_STREAM, $IPPROTO_TCP )
253
254 if this.sockserv.socket == $INVALID_SOCKET : return inet_seterror()
255
256 sin = sizeof( sockaddr )
257 mcopy( &in, &this.local, sin )
258 in->sockaddr_in.sin_port = 0
259
260 if bind( this.sockserv.socket, &in, sin ) ||
261 listen( this.sockserv.socket, 1 ) ||
262 getsockname( this.sockserv.socket, &in, &sin )
263 {
264 inet_seterror()
265 goto error
266 }
267
268 addr = ntohl( this.local->sockaddr_in.sin_addr )
269 port = ntohs( in->sockaddr_in.sin_port )
270
271 if this.command( "PORT \((addr >> 24) & 0xFF ),\((addr >> 16) & 0xFF ),\((addr >> 8) & 0xFF ),\(addr & 0xFF ),\((port >> 8) & 0xFF ),\(port & 0xFF )") != $FTP_OK
272 {
273 ineterror = $ERRFTP_PORT
274 goto error
275 }
276 return 1
277
278 label error
279 this.notify( $NFYINET_ERROR )
280 return 0
281 }
282
283 method uint ftp.accept
284 {
285 if !this.passive && ( this.sockdata.socket =
286 accept( this.sockserv.socket, 0, 0 )) == $INVALID_SOCKET
287 {
288 inet_seterror()
289 this.notify( $NFYINET_ERROR )
290 return 0
291 }
292 return this.sockdata.socket
293 }
294
295 method uint ftp.list( str data, str mode )
296 {
297 uint i dif
298
299 if !this.listen() : return 0
300
301 // if this.command( "MLSD" ) != $FTP_OPENING : return 0
302 if this.command( mode ) != $FTP_OPENING : return 0
303
304 if !this.accept() : return 0
305
306 data->buf.use = 0
307 do
308 {
309 i = *data
310 this.sockdata.recv( data->buf )
311 dif = *data - i
312 } while dif
313
314 data->buf += byte( 0 )
315
316 this.sockdata.close()
317
318 // ? надо или нет закрывать
319 if !this.passive : this.sockserv.close()
320
321 if this.cmdresponse() != $FTP_ENDTRAN
322 {
323 ineterror = $ERRFTP_RESPONSE
324 this.notify( $NFYINET_ERROR )
325 return 0
326 }
327 return 1
328 }
329
330 method uint ftp.createdir( str dirname )
331 {
332 return this.command("MKD \(dirname)") == $FTP_MKDIROK
333 }
334
335 method uint ftp.deldir( str dirname )
336 {
337 return this.command("RMD \(dirname)") == $FTP_CWDOK
338 }
339
340 method uint ftp.delfile( str filename )
341 {
342 return this.command("DELE \(filename)") == $FTP_CWDOK
343 }
344
345 method uint ftp.setcurdir( str dirname )
346 {
347 return this.command("CWD \(dirname)") == $FTP_CWDOK
348 }
349
350 method uint ftp.getcurdir( str dirname )
351 {
352 str response
353 uint off till
354
355 dirname.clear()
356 if this.command("PWD") != $FTP_MKDIROK : return 0
357
358 off = this.lastresponse( response ).findch( '"', 0 ) + 1
359 till = response.findchfrom( '"', off )
360 dirname.substr( response, off, till - off )
361 if !*dirname : return 0
362
363 return 1
364 }
365
366 method uint ftp.rename( str from to )
367 {
368 if this.command("RNFR \(from)") != $FTP_FILEOK : return 0
369 return this.command("RNTO \(to)") == $FTP_CWDOK
370 }
371
372 method uint ftp.getsize( str name, uint psize )
373 {
374 str size
375
376 psize->uint = 0
377 if this.command("SIZE \(name)") != $FTP_FILESTAT : return 0
378
379 this.lastresponse( size ).del( 0, 4 )
380 psize->uint = uint( size )
381 return 1
382 }
383
384 method uint ftp.gettime( str name, datetime dt )
385 {
386 str time year month day
387
388 mzero( &dt, sizeof( datetime ))
389 if this.command("MDTM \(name)") != $FTP_FILESTAT : return 0
390
391 this.lastresponse( time ).del( 0, 4 )
392 time.trimspace()
393 year.substr( time, 0, 4 )
394 month.substr( time, 4, 2 )
395 day.substr( time, 6, 2 )
396 dt.setdate( uint( day ), uint( month ), uint( year ))
397 dt.hour = uint( year.substr( time, 8, 2 ))
398 dt.minute = uint( month.substr( time, 10, 2 ))
399 dt.second = uint( day.substr( time, 12, 2 ))
400
401 return 1
402 }
403
404 method uint ftp.setattrib( str name, uint mode )
405 {
406 // str chmode
407
408 mode &= $S_IRWXU | $S_IRWXG | $S_IRWXO
409
410 if this.command("SITE CHMOD \(hex2strl( mode)) \(name)") != $FTP_OK
411 {
412 return 0
413 }
414 return 1
415 }
416
417 method uint ftp.getfile( str filename, buf databuf, uint flag )
418 {
419 uint data dif i
420 buf fbuf
421 uint ret range
422 file fdwn
423 uint isfile = flag & $FTP_FILE
424 finfo fi
425
426 if this.command( "TYPE \(?( flag & $FTP_BINARY, "I", "A" ))") != $FTP_OK
427 {
428 return 0
429 }
430 if !this.listen() : return 0
431
432 this.ni.param = 0
433
434 if isfile && flag & $FTP_CONTINUE
435 {
436 getfileinfo( databuf->str, fi )
437 if fi.sizelo
438 {
439 if this.command( "REST \(fi.sizelo)" ) != $FTP_FILEOK
440 {
441 flag &= ~$FTP_CONTINUE
442 }
443 else : this.ni.param = fi.sizelo
444 }
445 }
446 data = ?( isfile, &fbuf, &databuf )
447 data as buf
448
449 if isfile
450 {
451 data.expand( 0x20000 )
452 if !( fdwn.open( databuf->str, ?( flag & $FTP_CONTINUE,
453 $OP_ALWAYS, $OP_CREATE )))
454 {
455 this.ni.sparam = databuf->str
456 ineterror = $ERRINET_OPENFILE
457 this.notify( $NFYINET_ERROR )
458 goto end
459 }
460 if this.ni.param : fdwn.setpos( 0, $FILE_END )
461 }
462 else
463 {
464 uint fullsize
465
466 data.clear()
467 this.getsize( filename, &fullsize )
468 if uint( fullsize ) : data.expand( uint( fullsize ) + 0x7FFF )
469 }
470
471 if this.command( "RETR \(filename)" ) != $FTP_OPENING : return 0
472
473 if !this.accept() : return 0
474
475 do
476 {
477 i = *data
478 if !this.notify( $NFYINET_GET )
479 {
480 this.sockdata.close()
481 // this.command("ABOR")
482 goto end
483 }
484 this.sockdata.recv( data )
485
486 dif = *data - i
487 this.ni.param += dif
488
489 if isfile && ( *data >= 0x7FFF || !dif )
490 {
491 if !( fdwn.write( data ))
492 {
493 this.ni.sparam = databuf->str
494 ineterror = $ERRINET_WRITEFILE
495 this.notify( $NFYINET_ERROR )
496 goto end
497 }
498 data.use = 0
499 }
500 } while dif
501
502 if flag & $FTP_STR : data += byte( 0 )
503 this.notify( $NFYINET_END )
504 this.sockdata.close()
505
506 if this.cmdresponse() != $FTP_ENDTRAN
507 {
508 ineterror = $ERRFTP_RESPONSE
509 goto end
510 }
511
512 ret = 1
513 label end
514 if fdwn.fopen
515 {
516 if ret && flag & $FTP_SETTIME
517 {
518 filetime ft
519 datetime dt
520
521 if this.gettime( filename, dt )
522 {
523 datetimetoftime( dt, ft )
524 fdwn.settime( ft )
525 }
526 }
527 fdwn.close( )
528 }
529 if !ret && ineterror : this.notify( $NFYINET_ERROR )
530
531 return ret
532 }
533
534 method uint ftp.getfile( str srcname destname, uint flag )
535 {
536 flag &= 0xF0FF
537 return this.getfile( srcname, destname->buf, flag | $FTP_FILE )
538 }
539
540 method uint ftp.putfile( str srcname destname, uint flag )
541 {
542 uint offset size cur ret i
543 file fdwn
544 buf data
545 str tmode
546
547 subfunc uint nextread
548 {
549 cur = min( size, 0x20000 )
550 data.use = 0
551 if fdwn.read( data, cur ) != cur
552 {
553 this.ni.sparam = srcname
554 ineterror = $ERRINET_READFILE
555 return 0
556 }
557 size -= cur
558 return 1
559 }
560
561 if !( fdwn.open( srcname, $OP_READONLY ))
562 {
563 this.ni.sparam = srcname
564 ineterror = $ERRINET_OPENFILE
565 this.notify( $NFYINET_ERROR )
566 return 0
567 }
568 size = fdwn.getsize( )
569 if flag & $FTP_CONTINUE
570 {
571 if !this.getsize( destname, &offset ) || offset >= size
572 {
573 flag &= ~$FTP_CONTINUE
574 }
575 else
576 {
577 this.ni.param = offset
578 size -= offset
579 fdwn.setpos( offset, $FILE_BEGIN )
580 }
581 }
582 data.expand( 0x20000 )
583 if !nextread() : goto end
584
585 tmode = "TYPE A"
586 if flag & $FTP_BINARY : tmode = "TYPE I"
587 elif !( flag & $FTP_TEXT )
588 {
589 fornum i = 0, *data - 1
590 {
591 if !data[i] || ( data[i] == 0xD && data[i + 1] != 0xA )
592 {
593 tmode = "TYPE I"
594 break
595 }
596 }
597 }
598 if this.command( tmode ) != $FTP_OK : goto close
599 if !this.listen() : goto close
600
601 if this.command( "\(?( flag & $FTP_CONTINUE, "APPE",
602 "STOR")) \( destname )") != $FTP_OPENING : goto close
603
604 if !this.accept() : goto close
605
606 while 1
607 {
608 uint off last
609 uint sent
610
611 last = *data
612
613 while last
614 {
615 sent = send( this.sockdata.socket, data.ptr() + off, min( 0x7FFF, last ),
616 0 )
617 if sent == $SOCKET_ERROR
618 {
619 inet_seterror()
620 this.sockdata.close()
621 goto close
622 }
623 else
624 {
625 this.ni.param += sent
626 if !this.notify( $NFYINET_PUT )
627 {
628 this.sockdata.close()
629 goto close
630 }
631 }
632 last -= sent
633 off += sent
634 }
635 // this.sockdata.send( data )
636 if !size : break
637 nextread()
638 }
639 this.notify( $NFYINET_END )
640 this.sockdata.close()
641 if this.cmdresponse() != $FTP_ENDTRAN
642 {
643 ineterror = $ERRFTP_RESPONSE
644 goto end
645 }
646 ret = 1
647 label end
648 if !ret && ineterror : this.notify( $NFYINET_ERROR )
649 label close
650 fdwn.close( )
651
652 return ret
653 }