1
2# This is the rsync file transfer interface.
3#
4# Rsync does not support a command line mode, i.e. each
5# invocation completes a given operation and then
6# terminates. There are only three commands possible:
7# 1) File(s) transfer. This can be either read or write.
8# 2) Directory transfer, i.e. get a file list
9# 3) Delete
10#
11# The above directly inform the "get" and "put" comands
12# to move files and the "list" command to get dirs and the
13# "delete" command.
14
15# Indirectly, we manage the working directory (cd and pwd).
16# Internally we carry the "pwd" with out the VFS header
17# (i.e. we keep the pwd "path" component in variable pwd)
18#
19# Also with rsync, there is no real notion of alive, so the
20# alive functions are disabled.
21
22# package require -exact Expect 0.0.0
23package require Expect
24package require fileutil
25package provide VFSrsync 1.0
26
27namespace eval VFSrsync {
28  variable commands
29  variable debug 0
30  variable os $::tcl_platform(platform)
31  variable abortflag
32  variable tmpdir
33
34  array set commands [list\
35			  open ::VFSrsync::RSYNCopen\
36			  live ::VFSrsync::RSYNClive\
37			  cd ::VFSrsync::RSYNCcd\
38			  mkdir ::VFSrsync::RSYNCmkdir\
39			  delete ::VFSrsync::RSYNCdelete\
40			  rmdir ::VFSrsync::RSYNCdelete\
41			  rmdirEmpty 1\
42			  isdir ::VFSrsync::RSYNCisDir\
43			  pwd ::VFSrsync::RSYNCpwd\
44			  list ::VFSrsync::RSYNClist\
45			  get ::VFSrsync::RSYNCget\
46			  put ::VFSrsync::RSYNCput\
47			  RcopyOk 3\
48			  close ::VFSrsync::RSYNCclose\
49			  debug ::VFSrsync::RSYNC_debug]
50
51  # Support routines *********************************
52  proc eId {n} {return "(rsync $n): "}
53  # ******************* all with names "RSYNC_..."
54  proc RSYNClog_user {} {
55    variable debug
56    variable tmpdir
57    upvar VFStok VFStok
58    VFSglobal logging
59    if {$logging || $debug} {
60      exp_log_user $debug
61      if {$debug} {
62	catch {exp_log_file}
63	exp_log_file -a $tmpdir/rsync.log
64      }
65    }
66  }
67  proc dproc {args} {}
68
69  # On logging. We set logging true at open time and if there is
70  # an error. We turn it off on sucess of any given command.
71
72  proc RSYNClog {mess} {
73    upvar VFStok VFStok
74    VFSglobal errorExpected logging log
75    set mess [regsub {\r} $mess {}]
76    if {$logging && !$errorExpected } {
77      eval [list {*}$log $mess]
78    }
79  }
80
81  proc RSYNCfixParms {wh} {
82    upvar VFStok VFStok
83    upvar $wh fx
84    VFSglobal mode module
85    frputs mode module fx
86    if {$mode == "daemon" && [string match /${module}* $fx]} {
87      set fx [string range $fx [string length $module]+1  end]
88      frputs fx
89      if {$fx == {}} {set fx /}
90    }
91    frputs fx
92    return
93  }
94  # The open request.  We have host, user, password & port
95  # host and VFStok are usually the same and MUST not be ""
96  # possible connection modes are:
97
98  proc RSYNCopen {URL args} {
99    global   config
100    variable debug
101    variable tmpdir
102    IsVFS $URL
103    frputs args
104
105
106    # We are likely to change the URL so lets sort that out so we
107    # can keep stuff where we can find it...
108
109    frputs args "[llength $args] "
110    array set parms {*}$args
111    set Xuser $parms(user)
112    # over ride user if found in address
113    # The VFSadd (set by IsVFS) may contain a user name
114    regexp {(.*)@(.*)} $VFSadd ma Xuser VFSadd
115
116    set Xhost [regsub {:[0-9]*} $VFSadd {}]
117    # host is address sans user and port (so far)
118    # set timeout $rq_timeout
119    set searchOps [list -index 0 -all -inline]
120    set Xmodule [regsub {^/$|^/([^/]+).*} $VFSpath {\1}]
121    set daemonKey $Xhost/$Xmodule
122    # Lean on the rsync table for extra options...
123    # mostly we want the mode info.
124    # host is VFSadd cleared of user & port
125    set info [lsearch {*}$searchOps $config(rsync,table) "*$Xhost*"]
126    if {$info == {}} {
127      set info [list [lindex $config(rsync,table) 0]]
128    }
129    frputs info
130    if {[llength $info] > 1} {
131      # try to pick the most useful one...
132      # if we are dealing with modules (i.e. mode == deamon) we
133      # need to match the host + the first element of the path.
134      # defaults to last looked at...
135
136     foreach inf $info {
137	lassign $inf key rPath Mode tops
138	if {($Mode == "daemon" && [string match *$daemonKey $key]) ||\
139		[string match *sftp* $key] ||\
140		[string match *scp* $Mode]} {
141	  incr found
142	  break
143	}
144      }
145      if {![info exist found]} {
146	lassign {} key rPath Mode
147      }
148    } else {
149      lassign [lindex $info 0] key rPath Mode tops
150    }
151    frputs  key rPath Mode ops tops
152    # An alternate program can be indicated by Mode "use=alt"
153    # we don't use this module (it should be part of the host...)
154    lassign [split $Mode =] Xmode modulex
155    set Xprog [expr {$Xmode == {use} ? "$Xmodule" : "rsync -s"}]
156    # Default port depends on mode...
157    set Xport [expr {$Xmode == "daemon" ? 873 : 22}]
158    regexp {(.*):(.*)} $VFSadd ma VFSadd Xport
159    # allow override of that if port in URL
160    #
161    # Here is the URL as we will use it:
162    set URL [URL norm rsync://$Xuser@${VFSadd}:$Xport/$VFSpath]
163    IsVFS $URL
164    VFSglobal echo errorExpected user host log logging mode module password port prog\
165	passwordx spawn_id timeout ops filePrefix pwd
166
167    foreach p {port user host mode module prog} {
168      set $p [set X$p]
169    }
170    set logging 1
171    set debug 0
172    set errorExpected 0
173    set passwordx {}
174    set timeout {}
175    set abortflag ::[namespace current]::abtflag
176    set timefmt "%d%m%y "
177    set tmpdir "/tmp"
178    set log ::[namespace current]::dproc
179    # ops is a global option to use on all requests..
180    if {$::MSW} {
181      # check the table opts (tops) to see if we have plink
182      # if so, pass it the creds.
183    }
184    set ops {--no-motd}
185
186    set poss {timeout abortflag timefmt log debug tmpdir}
187    foreach opt $poss {
188      if {[info exists parms($opt)]} {
189	set $opt $parms($opt)
190      }
191    }
192    set echo [expr {1 && $debug ? "" : "-noecho"}]
193    # pelim done, lets see if we can talk...
194    # we inclued the module address as the first path comp
195    set filePrefix rsync://$VFSadd
196    frputs  VFStok host user password port rq_timeout
197    if {$mode == "daemon"} {
198      set filePrefix $filePrefix/$module
199      set pwd [regsub {(^/[^/]+)} $VFSpath {}]
200      if {$pwd == {}} {
201	set pwd /
202      }
203    } else {
204      set pwd $VFSpath
205    }
206    frputs filePrefix
207    # Here we try a list request to prove we can logon...
208    ::VFSvars::VFS_WriteCache $filePrefix [RSYNClist $URL all]
209    return $URL
210  }
211
212
213  proc RSYNCsingle {VFStok what args} {
214    variable debug
215    VFSglobal echo logging mode module user password passwordx prog\
216	spawn_id timeout ops filePrefix pwd
217
218    RSYNClog_user
219    frputs #4 #3 #2 #1 ::glob(right,pwd)
220    #lassign $password Pword none passphrase
221    #  set send [string trim [slList {*}$what {*}$args]]
222    # exp_send -- $send\r
223    # We have two protocols to deal with, shell & daemon
224    if {$mode != "daemon"} {
225      foreach fil $args {
226	if {[regsub -all {rsync://|:[0-9]+} $fil {} nfil] == 0} {
227	  # simple file, pass it
228	  lappend nargs $fil
229	} else {
230	  # rsync remote file, need a : in front of the first /
231	  lappend nargs [regsub {/} $nfil {:/}]
232	}
233      }
234    } else {
235      # For daemon transfers, we have modules
236      # The module is part of the filePrefix and should already be
237      # in the args as such...
238      # lappend nargs [regsub {(.*://.*)(/)} $args \\1/$module/]
239      set nargs $args
240    }
241    frputs what args nargs
242    set cmd [frECF [list spawn {*}$echo {*}$prog  {*}$ops {*}$what]\
243		 $nargs\
244		 [list -f 1]]
245    while {1} {
246      lassign $passwordx password none passphrase
247      set acmd $cmd
248      if {$::MSW} {
249	# we need the password here...
250	set acmd [subst -nocommands -nobackslashes $cmd]
251      }
252      frputs "spawn  " acmd
253      set r [catch {{*}$acmd} out]
254      if {$r != 0} {
255	return -code error "[eId 2]Really bad error: $out"
256      }
257      set stuff {}
258      # The first pattern takes care of the input line limit where back
259      # spaces are inserted in the echo...
260      expect_after timeout \
261	  {set expect_out(buffer) "Connection timed out $VFStok";\
262	       set re 1}\
263	  eof {set expect_out(buffer) "Connection closed $VFStok" ;\
264		   frputs "1.1 "
265	    set re 0}
266
267      expect -re "(.*assword:.*)" \
268	  {RSYNClog  $expect_out(buffer)
269	    # Log "sending password"
270	    if {$password == {}} {
271	      set passwordx [::pwLocker::getPw $filePrefix \
272				full 1 \
273				abort 1\
274				prompt "(2)[regsub {\r} $expect_out(1,string) {}]"]
275	      lassign $passwordx password none passphrase
276	    }
277	    frputs "open password  " Pword
278	    RSYNClog  "sending pasword\n"
279	    frputs "2 "
280	    if {$::MSW} {
281	      set re 5
282	    } else {
283	      exp_send $Pword\r
284	      exp_continue
285	    }
286	  } \
287	  -re "(.*.?assphrase for key .*: )" {
288	    RSYNClog  $expect_out(buffer)
289	    if {$passphrase == {}} {
290	      set passwordx [::pwLocker::getPw $filePrefix \
291				full 1 \
292				abort 1\
293				prompt "(3)[regsub {\r} $expect_out(1,string) {}]"]
294	      lassign $passwordx password none passphrase
295	    }
296	    RSYNClog  "sending passphrase\n"
297	    frputs "3 "
298	    exp_send "$passphrase\r"
299	    exp_continue} \
300	  -re "(.* host key is not .*y/n. |.* authenticity of host .*\(yes/no\)\? )"\
301	  {
302	    # clean the out string for the smart_dialog call
303	    set st [regsub -all {\r} $expect_out(1,string) {}]
304	    RSYNClog  1$expect_out(buffer)
305	    frputs "4 "
306	    incr ignorto
307	    if { [smart_dialog .apop . [_ "Accept new host?"] \
308		      [list {} "$st" [_ "\nClick your answer."]] \
309		      1 2 [list [_ "No"] [_ "Yes"]]] == 1} {
310	      set an [expr {[string match "*(yes/*" $expect_out(1,string)] ? \
311				"yes" : "y" } ]
312	      unset ignorto
313	      frputs "back from y/n " an
314	      RSYNClog  "sending $an\n"
315	      frputs "5 "
316	      exp_send "$an\r"
317	      exp_continue
318	    } else {
319	      RSYNClog  "sending 'no'\n"
320	      frputs "6 "
321	      exp_send "no\r"
322	      Log "Aborting Login"
323	      set re 10
324	    }
325	  } \
326	  -re {(^[^\n]{0,10}yes\r\n)} {
327	    RSYNClog  6$expect_out(buffer)
328	    frputs "6 "
329	    exp_continue} \
330	  -re "(.*Warning: .*)\r?\n" {
331	    RSYNClog  $expect_out(buffer)
332	    frputs "7 "
333	    exp_continue}\
334	  -re "(.*Connecting .*)" {
335	    RSYNClog  $expect_out(buffer)
336	    RSYNClog  "sending password2\n"
337	    frputs "8"
338	    exp_send $password\r
339	    exp_continue} \
340	  -re "(.*Permanently added .*)\r*\n" {
341	    RSYNClog  $expect_out(buffer)
342	    frputs "9 "
343	    exp_continue}\
344	  -re "(.*assword.*|.*unable.*|.*assphrase.*|.*onnection closed.*)" {
345	    if {[string match {*    *} $expect_out(1,string)]} {
346	      append stuff $expect_out(buffer)
347	      exp_continue
348	    }
349	    set ms "(10)$expect_out(buffer)\
350            \n\nPlease try again."
351	    set passwordx [::pwLocker::getPw $filePrefix \
352			      full 1 \
353			      startwith $password\
354			      abort 1\
355			      prompt [regsub {\r} $ms {}]]
356	    lassign $passwordx password none passphrase
357
358	    RSYNClog  "exit open: Failed with: $expect_out(1,string)\n"
359	    frputs "10 "
360	    set re 5}\
361	  -re "(.*)\r*\n" {
362	    #frputs expect_out(1,string) expect_out(0,string)
363	    append stuff $expect_out(buffer)
364	    #exp_log_user $debug
365	    exp_continue}\
366	  -re "(.*)\r" {
367	    #frputs expect_out(1,string) expect_out(0,string)
368	    append stuff $expect_out(buffer)
369	    #exp_log_user $debug
370	    exp_continue}
371
372      if {$re != 5} {break}
373    }
374    # We get here on:
375    # close            (re = 0)
376    # timeout          (re = 1)
377    # password failure (re = 5)
378    # reject new host  (re = 10)
379    #
380    # After a close/timeout we need to do a wait to get the
381    # status of the completion...m
382    frputs "10.1 "
383    catch "exp_close -i $spawn_id"
384    frputs "10.2 "
385    #ForceUpdate $inst
386    set rtn [exp_wait -i $spawn_id]
387    lassign $rtn r1 r2 r3 r4
388    if {$r3 == 0 && $r4 == 0} {
389      frputs "11 "
390      set logging 0
391      frputs "12 "
392      return [regsub -all \r $stuff {}]
393    }
394    set logging 1
395    switch $re {
396      5 -
397      10 {set err $expect_out(buffer)}
398      default {
399	# some sort of error...
400	set err "rsync [expr {$r3 == 0 ? {threw system error} :\
401               	                     {returned completion code}}]: $r4\n"
402      }
403    }
404    frputs re err
405    RSYNClog $err
406    return -code error [eId 3]$err
407  }
408
409  # To verify if a directory exists, we try to cd to it. This is
410  # rather painful (mostly due to the reconnedtion overhead) Sooo
411  # we try to verify only when it makes sense.
412  proc RSYNCcd {VFStok new_wd} {
413    VFSglobal filePrefix pwd
414
415    RSYNCfixParms new_wd
416    # We depend on this to throw an error if "new_wd" is not a dir
417    frputs #6 #5 #4 #3 #2 #1 new_wd
418    # Don't verify if we are going to subset of where we are (including
419    # not moving at all).
420    if {![string match $new_wd* $pwd]} {
421    set tmp [RSYNCsingle $VFStok {--list-only --include=. --exclude=* }\
422		 [URL norm $filePrefix/$new_wd]/]
423    }
424    frputs tmp new_wd
425    set pwd $new_wd
426    return 1
427   }
428
429  proc RSYNCisDir {VFStok dir} {
430    # what to do here???
431    VFSglobal errorExpected logging
432    set errorExpected 1
433    set r [catch {RSYNCcd $VFStok $dir} really]
434    set logging 0
435    set errorExpected 0
436    if {$r == 0 || [string match -nocase *permission* $really]} {
437      return [RSYNCpwd $VFStok]
438    }
439    return 0
440  }
441
442  # proc RSYNCrename {VFStok  old new} {
443  #   # current name old change to new
444  #   return -code error "VFS rsync does not support rename"
445  # }
446
447  # There is no way to do a delete with out excluding all the
448  # files that are not to be deleted. For now, at least, this
449  # is just too much.
450  # Ok, lets try overlaying the file with a zero length file
451  # and then bring it back with a delete-source option
452  # The --force option to rsync allows us to delete dirs
453  # by forcing a file where a directory was so this works
454  # for files and dirs.
455
456  proc RSYNCdelete {VFStok filename} {
457    VFSglobal ops filePrefix
458
459    RSYNCfixParms fileName
460
461    set tmp [makeTmp]/[file tail $filename]
462    file delete -force $tmp
463    ::fileutil::touch $tmp
464    RSYNCsingle $VFStok [list {*}$ops --force] $tmp\
465	[URL norm $filePrefix/[file dir $filename]]
466    file delete -force $tmp
467
468    set op [list -rvv  --remove-source-files]
469    RSYNCsingle $VFStok $op [URL norm $filePrefix/$filename] $tmp
470    file delete -force $tmp
471    return
472  }
473
474
475  proc RSYNCmkdir {VFStok dir} {
476    VFSglobal filePrefix
477    # No create command, so we make one and copy it over
478    set tmp [makeTmp]/[file tail $dir]
479    set r [Try {file mkdir $tmp} "" 1]
480    if {!$r} {
481      RSYNCput  $VFStok $tmp $dir
482      file delete $tmp
483    }
484    return
485   }
486
487  proc RSYNClive { VFStok } {
488    # we just require a simple transation to prove life..
489    # Not really feasable for rsync
490    return 1
491  }
492
493  proc RSYNCpwd {VFStok} {
494    VFSglobal filePrefix pwd
495    # just when you thought this was simple...
496    frputs pwd filePrefix
497    if {$pwd != "/"} {
498      set ppwd [RSYNCsingle $VFStok {--list-only -d -l --no-h} \
499		    [URL norm $filePrefix/$pwd]]
500      if {[set st [string first { -> } $ppwd]] != -1} {
501	# It is a link... Clean it up
502	regsub -all {\n} [string range $ppwd $st+3 end] {} ppwd
503	set pwd [URL norm [file dir $pwd]/[string trim $ppwd]]
504      }
505    }
506    return [URL norm $filePrefix/$pwd]
507  }
508
509  proc RSYNClist {URL all args} {
510    VFSglobal filePrefix pwd
511    # use -l (long list) -f (no sort) -a depends on all
512    # about 12.5k entries at 80 chars each (hay, just poke around on sourceforge)
513    #  set op [expr {$all ? "-l -a" : "-l"}]
514    #set dir "${pwd}[expr {$pwd == {/} ? {} : {/}}]"
515    set rtn [split [RSYNCsingle $VFStok {--list-only -d -l --no-h} \
516			[URL norm $filePrefix/$pwd/]] \n]
517	     if {[llength $rtn] < 3 && [string match -nocase *permission* $rtn]} {
518	       set errorInfo {}
519	       return -code error "[eId 4]Permission denied"
520	     }
521    frputs rtn'
522    return  $rtn
523  }
524
525
526  proc RSYNCget { VFStok remoteFileName localFileName args} {
527    RSYNCfixParms remoteFileName
528    VFSglobal ops filePrefix pwd
529    frputs localFileName remoteFileName
530    # if {[file tail $remoteFileName] != [file tail $localFileName]} {
531    #   return -code error "CopyAs not supported by VFS rsync."
532    # }
533    return [RSYNCsingle $VFStok "$ops -LkK"\
534		[URL norm $filePrefix/$remoteFileName] $localFileName]
535  }
536
537
538  proc RSYNCput { VFStok localFileName remoteFileName args} {
539    RSYNCfixParms remoteFileName
540    VFSglobal timeout ops filePrefix pwd
541    frputs localFileName remoteFileName
542    # if {[file tail $remoteFileName] != [file tail $localFileName]} {
543    #   return -code error "CopyAs not supported by VFS rsync."
544    # }
545    set localname [regsub -all " " $localFileName {\\ }]
546    #
547    return [RSYNCsingle $VFStok "$ops -LkKa " $localFileName\
548		[URL norm $filePrefix/$remoteFileName]]
549  }
550
551  proc RSYNC_debug {onoff} {
552    variable debug
553    set debug $onoff
554  }
555}
556