00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017 package require Tcl 8.2
00018 namespace ::ftpd {
00019
00020
00021
00022 variable port 21
00023
00024 variable contact
00025 if {![info exists contact]} {
00026 global tcl_platform
00027 contact = "$tcl_platform(user)@[info hostname]"
00028 }
00029
00030 variable cwd
00031 if {![info exists cwd]} {
00032 cwd = ""
00033 }
00034
00035 variable welcome
00036 if {![info exists welcome]} {
00037 welcome = "[info hostname] FTP server ready."
00038 }
00039
00040
00041
00042 variable cfg
00043 if {![info exists cfg]} {
00044 array cfg = [list \
00045 closeCmd {} \
00046 authIpCmd {} \
00047 authUsrCmd {::ftpd::anonAuth} \
00048 authFileCmd {::ftpd::fileAuth} \
00049 logCmd {::ftpd::logStderr} \
00050 fsCmd {::ftpd::fsFile::fs} \
00051 xferDoneCmd {}]
00052 }
00053
00054 variable commands
00055 if {![info exists commands]} {
00056 array commands = [list \
00057 ABOR {ABOR (abort operation)} \
00058 ACCT {(specify account); unimplemented.} \
00059 ALLO {(allocate storage - vacuously); unimplemented.} \
00060 APPE {APPE <sp> file-name} \
00061 CDUP {CDUP (change to parent directory)} \
00062 CWD {CWD [ <sp> directory-name ]} \
00063 DELE {DELE <sp> file-name} \
00064 HELP {HELP [ <sp> <string> ]} \
00065 LIST {LIST [ <sp> path-name ]} \
00066 NLST {NLST [ <sp> path-name ]} \
00067 MAIL {(mail to user); unimplemented.} \
00068 MDTM {MDTM <sp> path-name} \
00069 MKD {MKD <sp> path-name} \
00070 MLFL {(mail file); unimplemented.} \
00071 MODE {(specify transfer mode); unimplemented.} \
00072 MRCP {(mail recipient); unimplemented.} \
00073 MRSQ {(mail recipient scheme question); unimplemented.} \
00074 MSAM {(mail send to terminal and mailbox); unimplemented.} \
00075 MSND {(mail send to terminal); unimplemented.} \
00076 MSOM {(mail send to terminal or mailbox); unimplemented.} \
00077 NOOP {NOOP} \
00078 PASS {PASS <sp> password} \
00079 PASV {( server = in passive mode); unimplemented.} \
00080 PORT {PORT <sp> b0, b1, b2, b3, b4, b5} \
00081 PWD {PWD (return current directory)} \
00082 QUIT {QUIT (terminate service)} \
00083 REIN {REIN (reinitialize server state)} \
00084 REST {(restart command); unimplemented.} \
00085 RETR {RETR <sp> file-name} \
00086 RMD {RMD <sp> path-name} \
00087 RNFR {RNFR <sp> file-name} \
00088 RNTO {RNTO <sp> file-name} \
00089 SIZE {SIZE <sp> path-name} \
00090 SMNT {(structure mount); unimplemented.} \
00091 STOR {STOR <sp> file-name} \
00092 STOU {STOU <sp> file-name} \
00093 STRU {(specify file structure); unimplemented.} \
00094 SYST {SYST (get type of operating system)} \
00095 TYPE {TYPE <sp> [ A | E | I | L ]} \
00096 USER {USER <sp> username} \
00097 XCUP {XCUP (change to parent directory)} \
00098 XCWD {XCWD [ <sp> directory-name ]} \
00099 XMKD {XMKD <sp> path-name} \
00100 XPWD {XPWD (return current directory)} \
00101 XRMD {XRMD <sp> path-name}]
00102 }
00103
00104 variable passwords [list ]
00105
00106
00107
00108 namespace export config hasCallback logStderr
00109 namespace export fileAuth anonAuth unixAuth server accept read
00110 }
00111
00112
00113
00114
00115
00116
00117
00118
00119
00120
00121
00122
00123
00124
00125
00126
00127
00128
00129
00130
00131
00132
00133
00134
00135
00136
00137
00138
00139
00140
00141 ret ::ftpd::config (type args) {
00142
00143 # Processing of global configuration changes.
00144
00145 package require cmdline
00146
00147 variable cfg
00148
00149 array set cfg [cmdline::getoptions args [list \
00150 {closeCmd.arg {} {Callback when a connection is closed.}} \
00151 {authIpCmd.arg {} {Callback to authenticate new connections based on the ip-address of the peer. Optional}} \
00152 {authUsrCmd.arg {::ftpd::anonAuth} {Callback to authenticate new connections based on the user logging in.}} \
00153 {authFileCmd.arg {::ftpd::fileAuth} {Callback to accept or deny a users access to read and write to a specific path or file.}} \
00154 {logCmd.arg {::ftpd::logStderr} {Callback for log information generated by the FTP engine.}} \
00155 {xferDoneCmd.arg {} {Callback for transfer completion notification. Optional}} \
00156 {fsCmd.arg {::ftpd::fsFile::fs} {Callback to connect the engine to the filesystem it operates on.}}]]
00157 return
00158 }
00159
00160
00161
00162
00163
00164
00165
00166
00167
00168
00169
00170
00171
00172
00173
00174
00175
00176 ret ::ftpd::hasCallback (type callbackType) {
00177 variable cfg
00178
00179 return [expr {[info exists cfg($callbackType)] && [string length $cfg($callbackType)]}]
00180 }
00181
00182
00183
00184
00185
00186
00187
00188
00189
00190
00191
00192
00193
00194
00195
00196
00197
00198
00199 ret ::ftpd::logStderr (type severity , type text) {
00200
00201 # Standard log handler. Prints to stderr.
00202
00203 puts stderr "\[$severity\] $text"
00204 return
00205 }
00206
00207
00208
00209
00210
00211
00212
00213
00214
00215
00216
00217
00218
00219
00220
00221
00222
00223
00224 ret ::ftpd::Log (type severity , type text) {
00225
00226 # Central call out to log handlers.
00227
00228 variable cfg
00229
00230 if {[hasCallback logCmd]} {
00231 set cmd $cfg(logCmd)
00232 lappend cmd $severity $text
00233 eval $cmd
00234 }
00235 return
00236 }
00237
00238
00239
00240
00241
00242
00243
00244
00245
00246
00247
00248
00249
00250
00251
00252
00253
00254
00255
00256
00257 ret ::ftpd::fileAuth (type user , type path , type operation) {
00258 # Standard authentication handler
00259
00260 if {(![Fs exists $path]) && ([string equal $operation "write"])} {
00261 if {[Fs exists [file dirname $path]]} {
00262 set path [file dirname $path]
00263 }
00264 } elseif {(![Fs exists $path]) && ([string equal $operation "read"])} {
00265 return 0
00266 }
00267
00268 if {[Fs exists $path]} {
00269 set mode [Fs permissions $path]
00270 if {([string equal $operation "read"] && (($mode & 00004) > 0)) || \
00271 ([string equal $operation "write"] && (($mode & 00002) > 0))} {
00272 return 1
00273 }
00274 }
00275 return 0
00276 }
00277
00278
00279
00280
00281
00282
00283
00284
00285
00286
00287
00288
00289
00290
00291
00292
00293
00294
00295
00296
00297
00298 ret ::ftpd::anonAuth (type user , type pass) {
00299 # Standard authentication handler
00300 #
00301 # Accept user 'anonymous' if a password was
00302 # provided which is at least similar to an
00303 # fully qualified email address.
00304
00305 if {(![string equal $user anonymous]) && (![string equal $user ftp])} {
00306 return 0
00307 }
00308
00309 set pass [split $pass @]
00310 if {[llength $pass] != 2} {
00311 return 0
00312 }
00313
00314 set domain [split [lindex $pass 1] .]
00315 if {[llength $domain] < 2} {
00316 return 0
00317 }
00318
00319 return 1
00320 }
00321
00322
00323
00324
00325
00326
00327
00328
00329
00330
00331
00332
00333
00334
00335
00336
00337
00338
00339
00340
00341
00342 ret ::ftpd::unixAuth (type user , type pass) {
00343
00344 variable passwords
00345 array set password $passwords
00346
00347 # Standard authentication handler
00348 #
00349 # Accept user 'anonymous' if a password was
00350 # provided which is at least similar to an
00351 # fully qualified email address.
00352
00353 if {([llength $passwords] == 0) && (![catch {package require crypt}])} {
00354 foreach file [list /etc/passwd /etc/shadow] {
00355 if {([file exists $file]) && ([file readable $file])} {
00356 set fh [open $file r]
00357 set data [read $fh [file size $file]]
00358 foreach line [split $data \n] {
00359 foreach {username passwd uid gid dir sh} [split $line :] {
00360 if {[string length $passwd] > 2} {
00361 set password($username) $passwd
00362 } elseif {$passwd == ""} {
00363 set password($username) ""
00364 }
00365 break
00366 }
00367 }
00368 }
00369 }
00370 set passwords [array get password]
00371 }
00372
00373 ::ftpd::Log debug $passwords
00374
00375 if {[string equal $user anonymous] || [string equal $user ftp]} {
00376
00377 set pass [split $pass @]
00378 if {[llength $pass] != 2} {
00379 return 0
00380 }
00381
00382 set domain [split [lindex $pass 1] .]
00383 if {[llength $domain] < 2} {
00384 return 0
00385 }
00386
00387 return 1
00388 }
00389
00390 if {[info exists password($user)]} {
00391 if {$password($user) == ""} {
00392 return 1
00393 }
00394 if {[string equal $password($user) [::crypt $pass $password($user)]]} {
00395 return 1
00396 }
00397 }
00398
00399 return 0
00400 }
00401
00402
00403
00404
00405
00406
00407
00408
00409
00410
00411
00412
00413
00414
00415
00416
00417
00418
00419 ret ::ftpd::server (optional myaddr ={)} {
00420 variable port
00421 if {[string length $myaddr]} {
00422 f = [socket -server ::ftpd::accept -myaddr $myaddr $port]
00423 } else {
00424 f = [socket -server ::ftpd::accept $port]
00425 }
00426 port = [lindex [fconfigure $f -sockname] 2]
00427 return
00428 }
00429
00430
00431
00432
00433
00434
00435
00436
00437
00438
00439
00440
00441
00442
00443
00444
00445
00446
00447
00448
00449 ret ::ftpd::accept (type sock , type ipaddr , type client_, type port) {
00450 upvar #0 ::ftpd::$sock data
00451 variable welcome
00452 variable cfg
00453 variable cwd
00454 variable CurrentSocket
00455
00456 set CurrentSocket $sock
00457 if {[info exists data]} {
00458 unset data
00459 }
00460
00461 if {[hasCallback authIpCmd]} {
00462 # Call out to authenticate the peer. A return value of 0 or an
00463 # error causes the system to reject the connection. Everything
00464 # else (with 1 prefered) leads to acceptance.
00465
00466 set cmd $cfg(authIpCmd)
00467 lappend cmd $ipaddr
00468
00469 set fail [catch {eval $cmd} res]
00470
00471 if {$fail} {
00472 Log error "AuthIp error: $res"
00473 }
00474 if {$fail || ($res == 0)} {
00475 Log note "AuthIp: Access denied to $ipaddr"
00476
00477 # Now: Close the connection. (Is there a standard response
00478 # before closing down to signal the peer that we don't want
00479 # to talk to it ? -> read RFC).
00480
00481 close $sock
00482 return
00483 }
00484
00485 # Accept the connection (for now, 'authUsrCmd' may revoke this
00486 # decision).
00487 }
00488
00489 array set data [list \
00490 access 0 \
00491 ip $ipaddr \
00492 state command \
00493 buffering line \
00494 cwd "$cwd" \
00495 mode binary \
00496 sock2a "" \
00497 sock2 ""]
00498
00499 fconfigure $sock -buffering line
00500 fileevent $sock readable [list ::ftpd::read $sock]
00501 puts $sock "220 $welcome"
00502
00503 Log debug "Accept $ipaddr"
00504 return
00505 }
00506
00507
00508
00509
00510
00511
00512
00513
00514
00515
00516
00517
00518
00519
00520
00521
00522
00523
00524
00525 ret ::ftpd::read (type sock) {
00526 upvar #0 ::ftpd::$sock data
00527 variable CurrentSocket
00528
00529 set CurrentSocket $sock
00530 if {[eof $sock]} {
00531 Finish $sock
00532 return
00533 }
00534 switch -exact -- $data(state) {
00535 command {
00536 gets $sock command
00537 set argument ""
00538 if {![regexp {^([^ ]+) (.*)$} $command -> cmd argument]} {
00539 if {![regexp {^([^ ]+)$} $command -> cmd]} {
00540 # Very bad command syntax.
00541 puts $sock "500 Command not understood."
00542 return
00543 }
00544 }
00545 set cmd [string toupper $cmd]
00546 auto_load ::ftpd::command::$cmd
00547 if {($data(access) == 0) && ((![info exists data(user)]) || \
00548 ($data(user) == "")) && (![string equal $cmd "USER"])} {
00549 if {[string equal $cmd "PASS"]} {
00550 puts $sock "503 Login with USER first."
00551 } else {
00552 puts $sock "530 Please login with USER and PASS."
00553 }
00554 } elseif {($data(access) == 0) && (![string equal $cmd "PASS"]) \
00555 && (![string equal $cmd "USER"]) \
00556 && (![string equal $cmd "QUIT"])} {
00557 puts $sock "530 Please login with USER and PASS."
00558 } elseif {[info command ::ftpd::command::$cmd] != ""} {
00559 Log debug $command
00560 ::ftpd::command::$cmd $sock $argument
00561 catch {flush $sock}
00562 } else {
00563 Log error "Unknown command: $cmd"
00564 puts $sock "500 Unknown command $cmd"
00565 }
00566 }
00567 default {
00568 error "Unknown state \"$data(state)\""
00569 }
00570 }
00571 return
00572 }
00573
00574
00575
00576
00577
00578
00579
00580
00581
00582
00583
00584
00585
00586
00587 ret ::ftpd::Finish (type sock) {
00588 upvar #0 ::ftpd::$sock data
00589 variable cfg
00590
00591 if {[hasCallback closeCmd]} then {
00592 ##
00593 ## User specified a close command so invoke it
00594 ##
00595 uplevel #0 $cfg(closeCmd)
00596 }
00597 close $sock
00598 if {[info exists data]} {
00599 unset data
00600 }
00601 return
00602 }
00603
00604
00605
00606
00607
00608
00609
00610
00611
00612
00613
00614
00615
00616
00617
00618 ret ::ftpd::FinishData (type sock) {
00619 upvar #0 ::ftpd::$sock data
00620 catch {close $data(sock2)}
00621 set data(sock2) {}
00622 return
00623 }
00624
00625
00626
00627
00628
00629
00630
00631
00632
00633
00634
00635
00636
00637
00638
00639
00640
00641
00642
00643
00644
00645
00646
00647
00648
00649
00650
00651
00652
00653
00654
00655
00656
00657
00658
00659
00660
00661
00662
00663
00664
00665
00666
00667
00668
00669
00670
00671
00672
00673
00674
00675 ret ::ftpd::Fs (type command , type path , type args) {
00676 variable cfg
00677
00678 if {![hasCallback fsCmd]} {
00679 error "-fsCmd must not be empty, need a way to access files."
00680 }
00681
00682 return [eval [list $cfg(fsCmd) $command $path] $args]
00683 }
00684
00685
00686
00687
00688
00689
00690
00691 namespace ::ftpd::command {
00692
00693 }
00694
00695
00696
00697
00698
00699
00700
00701
00702
00703
00704
00705
00706
00707
00708
00709
00710
00711 ret ::ftpd::command::ABOR (type sock , type list) {
00712
00713 ::ftpd::FinishData $sock
00714 puts $sock "225 ABOR command successful."
00715
00716 return
00717 }
00718
00719
00720
00721
00722
00723
00724
00725
00726
00727
00728
00729
00730
00731
00732
00733
00734
00735
00736
00737 ret ::ftpd::command::APPE (type sock , type filename) {
00738 upvar #0 ::ftpd::$sock data
00739
00740 set path [file join $data(cwd) [string trimleft $filename /]]
00741 if {[::ftpd::hasCallback authFileCmd]} {
00742 set cmd $::ftpd::cfg(authFileCmd)
00743 lappend cmd $data(user) $path write
00744 if {[eval $cmd] == 0} {
00745 puts $sock "550 $filename: Permission denied"
00746 return
00747 }
00748 }
00749
00750 #
00751 # Patched Mark O'Connor
00752 #
00753 if {![catch {::ftpd::Fs append $path $data(mode)} f]} {
00754 puts $sock "150 Copy Started ($data(mode))"
00755 fcopy $data(sock2) $f -command [list ::ftpd::GetDone $sock $data(sock2) $f ""]
00756 } else {
00757 puts $sock "500 Copy Failed: $path $f"
00758 ::ftpd::FinishData $sock
00759 }
00760 return
00761 }
00762
00763
00764
00765
00766
00767
00768
00769
00770
00771
00772
00773
00774
00775
00776
00777
00778 ret ::ftpd::command::CDUP (type sock , type list) {
00779 upvar #0 ::ftpd::$sock data
00780
00781 set data(cwd) [file dirname $data(cwd)]
00782 puts $sock "200 CDUP command successful."
00783 return
00784 }
00785
00786
00787
00788
00789
00790
00791
00792
00793
00794
00795
00796
00797
00798
00799
00800 ret ::ftpd::command::CWD (type sock , type relativepath) {
00801 upvar #0 ::ftpd::$sock data
00802
00803 if {[string equal $relativepath .]} {
00804 puts $sock "250 CWD command successful."
00805 return
00806 }
00807
00808 if {[string equal $relativepath ..]} {
00809 set data(cwd) [file dirname $data(cwd)]
00810 puts $sock "250 CWD command successful."
00811 return
00812 }
00813
00814 set data(cwd) [file join $data(cwd) $relativepath]
00815 puts $sock "250 CWD command successful."
00816 return
00817 }
00818
00819
00820
00821
00822
00823
00824
00825
00826
00827
00828
00829
00830
00831
00832
00833 ret ::ftpd::command::DELE (type sock , type filename) {
00834 upvar #0 ::ftpd::$sock data
00835
00836 set path [file join $data(cwd) [string trimleft $filename /]]
00837 if {[::ftpd::hasCallback authFileCmd]} {
00838 set cmd $::ftpd::cfg(authFileCmd)
00839 lappend cmd $data(user) $path write
00840 if {[eval $cmd] == 0} {
00841 puts $sock "550 $filename: Permission denied"
00842 return
00843 }
00844 }
00845
00846 if {[catch {::ftpd::Fs delete $path $sock} msg]} {
00847 puts $sock "500 DELE Failed: $path $msg"
00848 }
00849 return
00850 }
00851
00852
00853
00854
00855
00856
00857
00858
00859
00860
00861
00862
00863
00864
00865
00866
00867 ret ::ftpd::command::HELP (type sock , type command) {
00868 upvar #0 ::ftpd::$sock data
00869
00870 if {$command != ""} {
00871 set command [string toupper $command]
00872 if {![info exists ::ftpd::commands($command)]} {
00873 puts $sock "502 Unknown command '$command'."
00874 } elseif {[info commands ::ftpd::command::$command] == ""} {
00875 puts $sock "214 $command\t$::ftpd::commands($command)"
00876 } else {
00877 puts $sock "214 Syntax: $::ftpd::commands($command)"
00878 }
00879 } else {
00880 set commandList [lsort [array names ::ftpd::commands]]
00881 puts $sock "214-The following commands are recognized (* =>'s unimplemented)."
00882 set i 1
00883 foreach commandName $commandList {
00884 if {[info commands ::ftpd::command::$commandName] == ""} {
00885 puts -nonewline $sock [format " %-7s" "${commandName}*"]
00886 } else {
00887 puts -nonewline $sock [format " %-7s" $commandName]
00888 }
00889 if {($i % 8) == 0} {
00890 puts $sock ""
00891 }
00892 incr i
00893 }
00894 incr i -1
00895 if {($i % 8) != 0} {
00896 puts $sock ""
00897 }
00898 puts $sock "214 Direct comments to $::ftpd::contact."
00899 }
00900
00901 return
00902 }
00903
00904
00905
00906
00907
00908
00909
00910
00911
00912
00913
00914
00915
00916
00917
00918
00919 ret ::ftpd::command::LIST (type sock , type filename) {
00920 ::ftpd::List $sock $filename list
00921 return
00922 }
00923
00924
00925
00926
00927
00928
00929
00930
00931
00932
00933
00934
00935
00936
00937
00938
00939 ret ::ftpd::command::MDTM (type sock , type filename) {
00940 upvar #0 ::ftpd::$sock data
00941
00942 set path [file join $data(cwd) [string trimleft $filename /]]
00943 if {[catch {::ftpd::Fs mtime $path $sock} msg]} {
00944 puts $sock "500 MDTM Failed: $path $msg"
00945 ::ftpd::FinishData $sock
00946 }
00947 return
00948 }
00949
00950
00951
00952
00953
00954
00955
00956
00957
00958
00959
00960
00961
00962
00963
00964 ret ::ftpd::command::MKD (type sock , type filename) {
00965 upvar #0 ::ftpd::$sock data
00966
00967 set path [file join $data(cwd) [string trimleft $filename /]]
00968
00969 if {[::ftpd::hasCallback authFileCmd]} {
00970 set cmd $::ftpd::cfg(authFileCmd)
00971 lappend cmd $data(user) $path write
00972 if {[eval $cmd] == 0} {
00973 puts $sock "550 $filename: Permission denied"
00974 return
00975 }
00976 }
00977
00978 if {[catch {::ftpd::Fs mkdir $path $sock} f]} {
00979 puts $sock "500 MKD Failed: $path $f"
00980 }
00981 return
00982 }
00983
00984
00985
00986
00987
00988
00989
00990
00991
00992
00993
00994
00995
00996
00997
00998 ret ::ftpd::command::NOOP (type sock , type list) {
00999
01000 puts $sock "200 NOOP command successful."
01001 return
01002 }
01003
01004
01005
01006
01007
01008
01009
01010
01011
01012
01013
01014
01015
01016
01017
01018
01019 ret ::ftpd::command::NLST (type sock , type filename) {
01020 ::ftpd::List $sock $filename nlst
01021 return
01022 }
01023
01024
01025
01026
01027
01028
01029
01030
01031
01032
01033
01034
01035
01036
01037
01038
01039
01040
01041
01042
01043 ret ::ftpd::command::PASS (type sock , type password) {
01044 upvar #0 ::ftpd::$sock data
01045
01046 if {$password == ""} {
01047 puts $sock "530 Please login with USER and PASS."
01048 return
01049 }
01050 set data(pass) $password
01051
01052 ::ftpd::Log debug "pass <$data(pass)>"
01053
01054 if {![::ftpd::hasCallback authUsrCmd]} {
01055 error "-authUsrCmd must not be empty, need a way to authenticate the user."
01056 }
01057
01058 # Call out to authenticate the user. A return value of 0 or an
01059 # error causes the system to reject the connection. Everything
01060 # else (with 1 prefered) leads to acceptance.
01061
01062 set cmd $::ftpd::cfg(authUsrCmd)
01063 lappend cmd $data(user) $data(pass)
01064
01065 set fail [catch {eval $cmd} res]
01066
01067 if {$fail} {
01068 ::ftpd::Log error "AuthUsr error: $res"
01069 }
01070 if {$fail || ($res == 0)} {
01071 ::ftpd::Log note "AuthUsr: Access denied to <$data(user)> <$data(pass)>."
01072 unset data(user)
01073 unset data(pass)
01074 puts $sock "551 Access Denied"
01075 } else {
01076 puts $sock "230 OK"
01077 set data(access) 1
01078 }
01079 return
01080 }
01081
01082
01083
01084
01085
01086
01087
01088
01089
01090
01091
01092
01093
01094
01095
01096
01097 ret ::ftpd::command::PORT (type sock , type numbers) {
01098 upvar #0 ::ftpd::$sock data
01099 set x [split $numbers ,]
01100
01101 ::ftpd::FinishData $sock
01102
01103 set data(sock2) [socket [join [lrange $x 0 3] .] \
01104 [expr {([lindex $x 4] << 8) | [lindex $x 5]}]]
01105 fconfigure $data(sock2) -translation $data(mode)
01106 puts $sock "200 PORT OK"
01107 return
01108 }
01109
01110
01111
01112
01113
01114
01115
01116
01117
01118
01119
01120
01121
01122
01123
01124
01125 ret ::ftpd::command::PWD (type sock , type list) {
01126 upvar #0 ::ftpd::$sock data
01127 ::ftpd::Log debug $data(cwd)
01128 puts $sock "257 \"$data(cwd)\" is current directory."
01129 return
01130 }
01131
01132
01133
01134
01135
01136
01137
01138
01139
01140
01141
01142
01143
01144
01145
01146 ret ::ftpd::command::QUIT (type sock , type list) {
01147 ::ftpd::Log note "Closed $sock"
01148 puts $sock "221 Goodbye."
01149 ::ftpd::Finish $sock
01150 # FRINK: nocheck
01151 #unset ::ftpd::$sock
01152 return
01153 }
01154
01155
01156
01157
01158
01159
01160
01161
01162
01163
01164
01165
01166
01167
01168
01169
01170
01171
01172
01173 ret ::ftpd::command::REIN (type sock , type list) {
01174 upvar #0 ::ftpd::$sock data
01175
01176 ::ftpd::FinishData $sock
01177 catch {close $data(sock2a)}
01178
01179 # Reinitialize the user and connection data.
01180
01181 array set data [list \
01182 access 0 \
01183 state command \
01184 buffering line \
01185 cwd "$::ftpd::cwd" \
01186 mode binary \
01187 sock2a "" \
01188 sock2 ""]
01189
01190 return
01191 }
01192
01193
01194
01195
01196
01197
01198
01199
01200
01201
01202
01203
01204
01205
01206
01207
01208
01209
01210 ret ::ftpd::command::RETR (type sock , type filename) {
01211 upvar #0 ::ftpd::$sock data
01212
01213 set path [file join $data(cwd) [string trimleft $filename /]]
01214
01215 if {[::ftpd::hasCallback authFileCmd]} {
01216 set cmd $::ftpd::cfg(authFileCmd)
01217 lappend cmd $data(user) $path read
01218 if {[eval $cmd] == 0} {
01219 puts $sock "550 $filename: Permission denied"
01220 return
01221 }
01222 }
01223
01224 #
01225 # Patched Mark O'Connor
01226 #
01227 if {![catch {::ftpd::Fs retr $path $data(mode)} f]} {
01228 puts $sock "150 Copy Started ($data(mode))"
01229 fcopy $f $data(sock2) -command [list ::ftpd::GetDone $sock $data(sock2) $f ""]
01230 } else {
01231 puts $sock "500 Copy Failed: $path $f"
01232 ::ftpd::FinishData $sock
01233 }
01234 return
01235 }
01236
01237
01238
01239
01240
01241
01242
01243
01244
01245
01246
01247
01248
01249
01250
01251 ret ::ftpd::command::RMD (type sock , type filename) {
01252 upvar #0 ::ftpd::$sock data
01253
01254 set path [file join $data(cwd) [string trimleft $filename /]]
01255
01256 if {[::ftpd::hasCallback authFileCmd]} {
01257 set cmd $::ftpd::cfg(authFileCmd)
01258 lappend cmd $data(user) $path write
01259 if {[eval $cmd] == 0} {
01260 puts $sock "550 $filename: Permission denied"
01261 return
01262 }
01263 }
01264 if {[catch {::ftpd::Fs rmdir $path $sock} f]} {
01265 puts $sock "500 RMD Failed: $path $f"
01266 }
01267 return
01268 }
01269
01270
01271
01272
01273
01274
01275
01276
01277
01278
01279
01280
01281
01282
01283
01284
01285
01286 ret ::ftpd::command::RNFR (type sock , type filename) {
01287 upvar #0 ::ftpd::$sock data
01288
01289 set path [file join $data(cwd) [string trimleft $filename /]]
01290
01291 if {[file exists $path]} {
01292 if {[::ftpd::hasCallback authFileCmd]} {
01293 set cmd $::ftpd::cfg(authFileCmd)
01294 lappend cmd $data(user) $path write
01295 if {[eval $cmd] == 0} {
01296 puts $sock "550 $filename: Permission denied"
01297 return
01298 }
01299 }
01300
01301 puts $sock "350 File exists, ready for destination name"
01302 set data(renameFrom) $path
01303 } else {
01304 puts $sock "550 $path: No such file or directory."
01305 }
01306 return
01307 }
01308
01309
01310
01311
01312
01313
01314
01315
01316
01317
01318
01319
01320
01321
01322
01323
01324 ret ::ftpd::command::RNTO (type sock , type filename) {
01325
01326 if {$filename == ""} {
01327 puts $sock "500 'RNTO': command not understood."
01328 return
01329 }
01330
01331 set path [file join $data(cwd) [string trimleft $filename /]]
01332
01333 if {![info exists data(renameFrom)]} {
01334 puts $sock "503 Bad sequence of commands."
01335 return
01336 }
01337 if {[::ftpd::hasCallback authFileCmd]} {
01338 set cmd $::ftpd::cfg(authFileCmd)
01339 lappend cmd $data(user) $path write
01340 if {[eval $cmd] == 0} {
01341 puts $sock "550 $filename: Permission denied"
01342 return
01343 }
01344 }
01345
01346
01347 if {![catch {::ftpd::Fs rename $data(renameFrom) $path} msg]} {
01348 unset data(renameFrom)
01349 } else {
01350 unset data(renameFrom)
01351 puts $sock "500 'RNTO': command not understood."
01352 }
01353 return
01354 }
01355
01356
01357
01358
01359
01360
01361
01362
01363
01364
01365
01366
01367
01368
01369
01370
01371 ret ::ftpd::command::SIZE (type sock , type filename) {
01372 upvar #0 ::ftpd::$sock data
01373
01374 set path [file join $data(cwd) [string trimleft $filename /]]
01375 if {[catch {::ftpd::Fs size $path $sock} msg]} {
01376 puts $sock "500 SIZE Failed: $path $msg"
01377 ::ftpd::FinishData $sock
01378 }
01379 return
01380 }
01381
01382
01383
01384
01385
01386
01387
01388
01389
01390
01391
01392
01393
01394
01395
01396
01397
01398
01399 ret ::ftpd::command::STOR (type sock , type filename) {
01400 upvar #0 ::ftpd::$sock data
01401
01402 set path [file join $data(cwd) [string trimleft $filename /]]
01403 if {[::ftpd::hasCallback authFileCmd]} {
01404 set cmd $::ftpd::cfg(authFileCmd)
01405 lappend cmd $data(user) $path write
01406 if {[eval $cmd] == 0} {
01407 puts $sock "550 $filename: Permission denied"
01408 return
01409 }
01410 }
01411
01412 #
01413 # Patched Mark O'Connor
01414 #
01415 if {![catch {::ftpd::Fs store $path $data(mode)} f]} {
01416 puts $sock "150 Copy Started ($data(mode))"
01417 fcopy $data(sock2) $f -command [list ::ftpd::GetDone $sock $data(sock2) $f ""]
01418 } else {
01419 puts $sock "500 Copy Failed: $path $f"
01420 ::ftpd::FinishData $sock
01421 }
01422 return
01423 }
01424
01425
01426
01427
01428
01429
01430
01431
01432
01433
01434
01435
01436
01437
01438
01439
01440
01441
01442 ret ::ftpd::command::STOU (type sock , type filename) {
01443 upvar #0 ::ftpd::$sock data
01444
01445 set path [file join $data(cwd) [string trimleft $filename /]]
01446 if {[::ftpd::hasCallback authFileCmd]} {
01447 set cmd $::ftpd::cfg(authFileCmd)
01448 lappend cmd $data(user) $path write
01449 if {[eval $cmd] == 0} {
01450 puts $sock "550 $filename: Permission denied"
01451 return
01452 }
01453 }
01454
01455 set file $path
01456 set i 0
01457 while {[::ftpd::Fs exists $file]} {
01458 set file "$path.$i"
01459 incr i
01460 }
01461
01462 #
01463 # Patched Mark O'Connor
01464 #
01465 if {![catch {::ftpd::Fs store $file $data(mode)} f]} {
01466 puts $sock "150 Copy Started ($data(mode))"
01467 fcopy $data(sock2) $f -command [list ::ftpd::GetDone $sock $data(sock2) $f $file]
01468 } else {
01469 puts $sock "500 Copy Failed: $path $f"
01470 ::ftpd::FinishData $sock
01471 }
01472 return
01473 }
01474
01475
01476
01477
01478
01479
01480
01481
01482
01483
01484
01485
01486
01487
01488
01489 ret ::ftpd::command::SYST (type sock , type list) {
01490 upvar #0 ::ftpd::$sock data
01491
01492 global tcl_platform
01493
01494 if {[string equal $tcl_platform(platform) "unix"]} {
01495 set platform UNIX
01496 } elseif {[string equal $tcl_platform(platform) "windows"]} {
01497 set platform WIN32
01498 } elseif {[string equal $tcl_platform(platform) "macintosh"]} {
01499 set platform MACOS
01500 } else {
01501 set platform UNKNOWN
01502 }
01503 set version [string toupper $tcl_platform(os)]
01504 puts $sock "215 $platform Type: L8 Version: $version"
01505
01506 return
01507 }
01508
01509
01510
01511
01512
01513
01514
01515
01516
01517
01518
01519
01520
01521
01522
01523
01524
01525 ret ::ftpd::command::TYPE (type sock , type type) {
01526 upvar #0 ::ftpd::$sock data
01527
01528 if {[string compare i [string tolower $type]] == 0} {
01529 set data(mode) binary
01530 } else {
01531 set data(mode) auto
01532 }
01533
01534 if {$data(sock2) != {}} {
01535 fconfigure $data(sock2) -translation $data(mode)
01536 }
01537 puts $sock "200 Type set to $type."
01538 return
01539 }
01540
01541
01542
01543
01544
01545
01546
01547
01548
01549
01550
01551
01552
01553
01554
01555
01556 ret ::ftpd::command::USER (type sock , type username) {
01557 upvar #0 ::ftpd::$sock data
01558
01559 if {$username == ""} {
01560 puts $sock "530 Please login with USER and PASS."
01561 return
01562 }
01563 set data(user) $username
01564 puts $sock "331 Password Required"
01565
01566 ::ftpd::Log debug "user <$data(user)>"
01567 return
01568 }
01569
01570
01571
01572
01573
01574
01575
01576
01577
01578
01579
01580
01581
01582
01583
01584
01585
01586
01587
01588
01589
01590
01591
01592 ret ::ftpd::GetDone (type sock , type sock2 , type f , type filename , type bytes , optional err ={)} {
01593 upvar #0 ::ftpd::$sock data
01594 variable cfg
01595
01596 close $f
01597 FinishData $sock
01598
01599 if {[string length $err]} {
01600 puts $sock "226- $err"
01601 } elseif {$filename == ""} {
01602 puts $sock "226 Transfer complete ($bytes bytes)"
01603 } else {
01604 puts $sock "226 Transfer complete (unique file name: $filename)."
01605 }
01606 if {[hasCallback xferDoneCmd]} then {
01607 catch {$cfg(xferDoneCmd) $sock $sock2 $f $bytes $filename $err}
01608 }
01609 Log debug "GetDone $f $sock2 $bytes bytes filename: $filename"
01610 return
01611 }
01612
01613
01614
01615
01616
01617
01618
01619
01620
01621
01622
01623
01624
01625
01626
01627
01628
01629 ret ::ftpd::List (type sock , type filename , type style) {
01630 upvar #0 ::ftpd::$sock data
01631 puts $sock "150 Opening data channel"
01632
01633 set path [file join $data(cwd) $filename]
01634
01635 Fs dlist $path $style $data(sock2)
01636
01637 FinishData $sock
01638 puts $sock "226 Listing complete"
01639 return
01640 }
01641
01642
01643
01644
01645
01646 namespace ::ftpd::fsFile {
01647
01648
01649 variable docRoot
01650 if {![info exists docRoot]} {
01651 docRoot = /
01652 }
01653
01654 namespace export docRoot fs
01655 }
01656
01657
01658
01659
01660
01661
01662
01663
01664
01665
01666
01667
01668
01669
01670
01671
01672
01673
01674
01675
01676 ret ::ftpd::fsFile::docRoot (optional dir ={)} {
01677 variable docRoot
01678 if {[string length $dir] == 0} {
01679 return $docRoot
01680 } else {
01681 docRoot = $dir
01682 }
01683 return ""
01684 }
01685
01686
01687
01688
01689
01690
01691
01692
01693
01694
01695
01696
01697
01698
01699
01700
01701
01702
01703
01704
01705
01706
01707
01708
01709
01710
01711
01712
01713
01714
01715
01716
01717
01718 ret ::ftpd::fsFile::fs (type command , type path , type args) {
01719 # append <path>
01720 # delete <path> <channel-to-write-to>
01721 # dlist <path> <style> <channel-to-write-dir-list-to>
01722 # exists <path>
01723 # mkdir <path> <channel-to-write-to>
01724 # mtime <path> <channel-to-write-mtime-to>
01725 # permissions <path>
01726 # rename <path> <newpath> <channel-to-write-to>
01727 # retr <path>
01728 # rmdir <path> <channel-to-write-to>
01729 # size <path> <channel-to-write-size-to>
01730 # store <path>
01731
01732 global tcl_platform
01733
01734 variable docRoot
01735
01736 set path [file join $docRoot $path]
01737
01738 switch -exact -- $command {
01739 append {
01740 #
01741 # Patched Mark O'Connor
01742 #
01743 set fhandle [open $path a]
01744 if {[lindex $args 0] == "binary"} {
01745 fconfigure $fhandle -translation binary -encoding binary
01746 }
01747 return $fhandle
01748 }
01749 retr {
01750 #
01751 # Patched Mark O'Connor
01752 #
01753 set fhandle [open $path r]
01754 if {[lindex $args 0] == "binary"} {
01755 fconfigure $fhandle -translation binary -encoding binary
01756 }
01757 return $fhandle
01758 }
01759 store {
01760 #
01761 # Patched Mark O'Connor
01762 #
01763 set fhandle [open $path w]
01764 if {[lindex $args 0] == "binary"} {
01765 fconfigure $fhandle -translation binary -encoding binary
01766 }
01767 return $fhandle
01768 }
01769 dlist {
01770 foreach {style outchan} $args break
01771 ::ftpd::Log debug "at dlist {$style} {$outchan} {$path}"
01772 #set path [glob -nocomplain $path]
01773 #::ftpd::Log debug "at dlist2 {$style} {$outchan} {$path}"
01774
01775 # Attempt to get a list of all files (even ones that start with .)
01776
01777 if {[file isdirectory $path]} {
01778 set path1 [file join $path *]
01779 set path2 [file join $path .*]
01780 } else {
01781 set path1 $path
01782 set path2 $path
01783 }
01784
01785 # Get a list of all files that match the glob pattern
01786
01787 set fileList [lsort -unique [concat [glob -nocomplain $path1] \
01788 [glob -nocomplain $path2]]]
01789
01790 ::ftpd::Log debug "File list is {$fileList}"
01791
01792 switch -- $style {
01793 nlst {
01794 ::ftpd::Log debug "In nlist"
01795 foreach f [lsort $fileList] {
01796 if {[string equal [file tail $f] "."] || \
01797 [string equal [file tail $f] ".."]} {
01798 continue
01799 }
01800 if {[string equal {} $f]} then continue
01801 ::ftpd::Log debug [file tail $f]
01802 puts $outchan [file tail $f]
01803 }
01804 }
01805 list {
01806 # [ 766112 ] report . and .. directories (linux)
01807 # Copied the code from 'nlst' above to handle this.
01808
01809 foreach f [lsort $fileList] {
01810 if {[string equal [file tail $f] "."] || \
01811 [string equal [file tail $f] ".."]} {
01812 continue
01813 }
01814 file stat $f stat
01815 if {[string equal $tcl_platform(platform) "unix"]} {
01816 set user [file attributes $f -owner]
01817 set group [file attributes $f -group]
01818 } else {
01819 set user owner
01820 set group group
01821 }
01822 puts $outchan [format "%s %3d %s %8s %11s %s %s" \
01823 [PermBits $f $stat(mode)] $stat(nlink) \
01824 $user $group $stat(size) \
01825 [FormDate $stat(mtime)] [file tail $f]]
01826 }
01827 }
01828 default {
01829 error "Unknown list style <$style>"
01830 }
01831 }
01832 }
01833 delete {
01834 foreach {outchan} $args break
01835
01836 if {![file exists $path]} {
01837 puts $outchan "550 $path: No such file or directory."
01838 } elseif {![file isfile $path]} {
01839 puts $outchan "550 $path: File exists."
01840 } else {
01841 file delete $path
01842 puts $outchan "250 DELE command successful."
01843 }
01844 }
01845 exists {
01846 if {[file isdirectory $path]} {
01847 return 0
01848 } else {
01849 return [file exists $path]
01850 }
01851 }
01852 mkdir {
01853 foreach {outchan} $args break
01854
01855 set path [string trimright $path /]
01856 if {[file exists $path]} {
01857 if {[file isdirectory $path]} {
01858 puts $outchan "521 \"$path\" directory exists"
01859 } else {
01860 puts $outchan "521 \"$path\" already exists"
01861 }
01862 } elseif {[file exists [file dirname $path]]} {
01863 file mkdir $path
01864 puts $outchan "257 \"$path\" new directory created."
01865 } else {
01866 puts $outchan "550 $path: No such file or directory."
01867 }
01868 }
01869 mtime {
01870 foreach {outchan} $args break
01871
01872 if {![file exists $path]} {
01873 puts $outchan "550 $path: No such file or directory"
01874 } elseif {![file isfile $path]} {
01875 puts $outchan "550 $path: not a plain file."
01876 } else {
01877 set time [file mtime $path]
01878 puts $outchan [clock format $time -format "213 %Y%m%d%H%M%S"]
01879 }
01880 }
01881 permissions {
01882 file stat $path stat
01883 return $stat(mode)
01884 }
01885 rename {
01886 foreach {newname outchan} $args break
01887
01888 if {![file isdirectory [file dirname $newname]]} {
01889 puts $outchan "550 rename: No such file or directory."
01890 }
01891 file rename $path $newname
01892 puts $sock "250 RNTO command successful."
01893 }
01894 rmdir {
01895 foreach {outchan} $args break
01896
01897 if {![file isdirectory $path]} {
01898 puts $outchan "550 $path: Not a directory."
01899 } elseif {[llength [glob -nocomplain [file join $path *]]] != 0} {
01900 puts $outchan "550 $path: Directory not empty."
01901 } else {
01902 file delete $path
01903 puts $outchan "250 RMD command successful."
01904 }
01905 }
01906 size {
01907 foreach {outchan} $args break
01908
01909 if {![file exists $path]} {
01910 puts $outchan "550 $path: No such file or directory"
01911 } elseif {![file isfile $path]} {
01912 puts $outchan "550 $path: not a plain file."
01913 } else {
01914 puts $outchan "213 [file size $path]"
01915 }
01916 }
01917 default {
01918 error "Unknown command \"$command\""
01919 }
01920 }
01921 return ""
01922 }
01923
01924
01925
01926
01927
01928
01929
01930
01931
01932
01933
01934
01935
01936
01937 ret ::ftpd::fsFile::PermBits (type file , type mode) {
01938
01939 array set s {
01940 0 --- 1 --x 2 -w- 3 -wx 4 r-- 5 r-x 6 rw- 7 rwx
01941 }
01942
01943 set type [file type $file]
01944 if {[string equal $type "file"]} {
01945 set permissions "-"
01946 } else {
01947 set permissions [string index $type 0]
01948 }
01949 foreach j [split [format %03o [expr {$mode&0777}]] {}] {
01950 append permissions $s($j)
01951 }
01952
01953 return $permissions
01954 }
01955
01956
01957
01958
01959
01960
01961
01962
01963
01964
01965
01966
01967
01968
01969 ret ::ftpd::fsFile::FormDate (type seconds) {
01970
01971 set currentTime [clock seconds]
01972 set oldTime [clock scan "6 months ago" -base $currentTime]
01973 if {$seconds <= $oldTime} {
01974 set time [clock format $seconds -format "%Y"]
01975 } else {
01976 set time [clock format $seconds -format "%H:%M"]
01977 }
01978 set day [string trimleft [clock format $seconds -format "%d"] 0]
01979 set month [clock format $seconds -format "%b"]
01980 return [format "%3s %2s %5s" $month $day $time]
01981 }
01982
01983
01984
01985
01986
01987
01988
01989 package provide ftpd 1.2.3
01990
01991
01992
01993
01994
01995
01996 ret ::ftpd::command::PASV (type sock , type argument) {
01997 upvar #0 ::ftpd::$sock data
01998
01999 set data(sock2a) [socket -server [list ::ftpd::PasvAccept $sock] 0]
02000 set list1 [fconfigure $sock -sockname]
02001 set ip [lindex $list1 0]
02002 set list2 [fconfigure $data(sock2a) -sockname]
02003 set port [lindex $list2 2]
02004 ::ftpd::Log debug "PASV on {$list1} {$list2} $ip $port"
02005 set ans [split $ip {.}]
02006 lappend ans [expr {($port >> 8) & 0xff}] [expr {$port & 0xff}]
02007 set ans [join $ans {,}]
02008 puts $sock "227 Entering Passive Mode ($ans)."
02009 return
02010 }
02011
02012
02013 ret ::ftpd::PasvAccept (type sock , type sock2 , type ip , type port) {
02014 upvar #0 ::ftpd::$sock data
02015
02016 ::ftpd::Log debug "In Pasv Accept with {$sock} {$sock2} {$ip} {$port}"
02017 ##
02018 ## Verify this is from who it should be
02019 ##
02020 if {![string equal $ip $data(ip)]} then {
02021 ##
02022 ## Nope, so close it and wait some more
02023 ##
02024 close $sock2
02025 return
02026 }
02027 ::ftpd::FinishData $sock
02028
02029 set data(sock2) $sock2
02030 fconfigure $data(sock2) -translation $data(mode)
02031 close $data(sock2a)
02032 set data(sock2a) ""
02033 return
02034 }
02035
02036
02037