Multiple http connections. Bug fixes.
[tclrobot.git] / robot.tcl
1 #!/usr/bin/tclsh 
2 # $Id: robot.tcl,v 1.10 2001/01/23 09:20:32 adam Exp $
3 #
4 proc RobotFileNext1 {area lead} {
5     puts "RobotFileNext1 area=$area lead=$lead"
6     if {[catch {set ns [glob ${area}/*]}]} {
7         return {}
8     }
9     foreach n $ns {
10         if {[file isfile $n]} {
11             set off [string last / $n]
12             incr off 2
13             return $lead/[string range $n $off end]
14         }
15     }
16     foreach n $ns {
17         if {[file isdirectory $n]} {
18             set off [string last / $n]
19             incr off 2
20             set sb [RobotFileNext1 $n $lead/[string range $n $off end]]
21             if {[string length $sb]} {
22                 return $sb
23             }
24         }
25     }
26     return {}
27 }
28
29 proc RobotWriteRecord {outf fromurl distance} {
30     puts $outf "<zmbot>"
31     puts $outf "<distance>"
32     puts $outf $distance
33     puts $outf "</distance>"
34     puts $outf "<fromurl>"
35     puts $outf $fromurl
36     puts $outf "</fromurl>"
37     puts $outf "</zmbot>"
38 }
39
40 proc RobotReadRecord {inf fromurlx distancex} {
41     upvar $fromurlx fromurl
42     upvar $distancex distance
43     gets $inf
44     gets $inf
45     set distance [string trim [gets $inf]]
46     puts "got distance = $distance"
47     gets $inf
48     gets $inf
49     set fromurl [string trim [gets $inf]]
50 }
51
52 proc RobotFileNext {area} {
53     global robotSeq global idleTime ns
54
55     puts "RobotFileNext robotSeq=$robotSeq"
56     if {$robotSeq < 0} {
57         return {}
58     }
59     if {$robotSeq == 0} {
60         if {[catch {set ns [glob ${area}/*]}]} {
61             return {}
62         }
63     }
64     set off [string length $area]
65     incr off
66     set n [lindex $ns $robotSeq]
67     if {![string length $n]} {
68         set robotSeq -1
69         flush stdout
70         puts "------------ N E X T  R O U N D --------"
71         return wait
72     }
73     incr robotSeq
74     if {[file isfile $n/frobots.txt]} {
75         puts "ok returning http://[string range $n $off end]/robots.txt"
76         return http://[string range $n $off end]/robots.txt
77     } elseif {[file isdirectory $n]} {
78         set sb [RobotFileNext1 $n http://[string range $n $off end]]
79         if {[string length $sb]} {
80             return $sb
81         }
82     }
83     puts "no more work at end of RobotFileNext n=$n"
84     puts "ns=$ns"
85     return {}
86 }
87
88
89 proc RobotFileExist {area host path} {
90     puts "RobotFileExist begin area=$area host=$host path=$path"
91     set lpath [split $path /]
92     set l [llength $lpath]
93     incr l -1
94     set t [lindex $lpath $l]
95     incr l -1
96     set npath $area/$host[join [lrange $lpath 0 $l] /d]/f$t
97     puts "RobotFileExist end npath=$npath"
98     return [file exists $npath]
99 }
100
101 proc RobotFileUnlink {area host path} {
102     puts "RobotFileUnlink begin"
103     puts "area=$area host=$host path=$path"
104     set lpath [split $path /]
105     set l [llength $lpath]
106     incr l -1
107     set t [lindex $lpath $l]
108     incr l -1
109     set npath $area/$host[join [lrange $lpath 0 $l] /d]/f$t
110     puts "npath=$npath"
111     set comp [split $npath /]
112     set l [llength $comp]
113     incr l -1
114     if {[catch {exec rm [join $comp /]}]} return
115     incr l -1
116     for {set i $l} {$i > 0} {incr i -1} {
117         set path [join [lrange $comp 0 $i] /]
118         if {![catch {glob $path/*}]} return
119         exec rmdir ./$path
120     }
121     puts "RobotFileUnlink end"
122 }
123
124 proc RobotFileClose {out} {
125     if [string compare $out stdout] {
126         close $out
127     }
128 }
129
130 proc RobotFileOpen {area host path {mode w}} {
131     set orgPwd [pwd]
132     global workdir
133
134     if {![info exists workdir]} {
135         return stdout
136     }
137     puts "RobotFileOpen orgPwd=$orgPwd area=$area host=$host path=$path mode=$mode"
138     if {[string compare $orgPwd $workdir]} {
139         puts "ooops. RobotFileOpen failed"
140         puts "workdir = $workdir"
141         puts "pwd = $orgPwd"
142         exit 1
143     }
144     set comp [split $area/$host$path /]
145     set len [llength $comp]
146     incr len -1
147     for {set i 0} {$i < $len} {incr i} {
148         if {$i > 1} {
149             set d "d[lindex $comp $i]" 
150         } else {
151             set d [lindex $comp $i]
152         }
153         if {[catch {cd ./$d}]} {
154             exec mkdir $d
155             cd ./$d
156             if {![string compare $area unvisited] && $i == 1 && $mode == "w"} {
157                 set out [open frobots.txt w]
158                 puts "creating robots.txt in $d"
159                 close $out
160             }
161         }
162     }
163     set d [lindex $comp $len]
164     if {[string length $d]} {
165         if {[file isdirectory $d]} {
166             set out [open $d/f $mode]
167         } else {
168             set out [open f$d $mode]
169         }
170     } else {
171         set out [open f $mode]
172     }
173     cd $orgPwd
174     return $out
175 }
176
177 proc RobotRR {} {
178     global robotSeq robotsRunning
179
180     incr robotsRunning -1
181     while {$robotsRunning} {
182         vwait robotsRunning
183     }
184     set robotSeq 0
185     RobotStart
186 }
187
188 proc RobotRestart {url sock} {
189     global URL robotsRunning
190
191     close $sock
192     after cancel $URL($sock,cancel) 
193
194     foreach v [array names URL $url,*] {
195         unset URL($v)
196     }
197
198     incr robotsRunning -1
199     RobotStart
200 }
201
202 proc RobotStart {} {
203     global URL
204     global robotsRunning robotsMax idleTime
205   
206     puts "RobotStart"
207     while {1} {
208         set url [RobotFileNext unvisited]
209         if {![string length $url]} {
210             return
211         }
212         incr robotsRunning
213         if {[string compare $url wait] == 0} {
214             after $idleTime RobotRR
215             return
216         }
217         set r [RobotGetUrl $url {}]
218         if {!$r} {
219             if {$robotsRunning >= $robotsMax} return
220         } else {
221             incr robotsRunning -1
222             if {![RobotFileExist bad $URL($url,hostport) $URL($url,path)]} {
223                 set outf [RobotFileOpen bad $URL($url,hostport) $URL($url,path)]
224                 RobotFileClose $outf
225             }
226             RobotFileUnlink unvisited $URL($url,hostport) $URL($url,path)
227         }
228     }
229 }
230
231 proc headSave {url out} {
232     global URL
233     
234     if {[info exists URL($url,head,last-modified)]} {
235         puts $out "<lastmodified>$URL($url,head,last-modified)</lastmodified>"
236     }
237     puts $out {<si>}
238     if {[info exists URL($url,head,date)]} {
239         puts $out " <date>$URL($url,head,date)</date>"
240     }
241     if {[info exists URL($url,head,content-length)]} {
242         puts $out " <by>$URL($url,head,content-length)</by>"
243     }
244     if {[info exists URL($url,head,server)]} {
245         puts $out " <format>$URL($url,head,server)</format>"
246     }
247     puts $out {</si>}
248     puts $out {<publisher>}
249     puts $out " <identifier>$url</identifier>"
250     if {[info exists URL($url,head,content-type)]} {
251         puts $out " <type>$URL($url,head,content-type)</type>"
252     }
253     puts $out {</publisher>}
254 }
255
256 proc RobotHref {url hrefx hostx pathx} {
257     global URL domains
258     upvar $hrefx href
259     upvar $hostx host
260     upvar $pathx path
261
262     puts "Ref url = $url href=$href"
263     # get method (if any)
264     if {![regexp {^([^/:]+):(.*)} $href x method hpath]} {
265         set hpath $href
266         set method http
267     } else {
268         if {[string compare $method http]} {
269             return 0
270         }
271     }
272     # get host (if any)
273     if {[regexp {^//([^/]+)([^\#]*)} $hpath x host surl]} {
274         if {![string length $surl]} {
275             set surl /
276         }
277         set ok 0
278         foreach domain $domains {
279             if {[string match $domain $host]} {
280                 set ok 1
281                 break
282             }
283         }
284         if {!$ok} {
285             return 0
286         }
287     } else {
288         regexp {^([^\#]*)} $hpath x surl
289         set host $URL($url,hostport)
290     }
291     if {![string length $surl]} {
292         return 0
293     }
294     if {[string first / $surl]} {
295         # relative path
296         regexp {^([^\#?]*)} $URL($url,path) x dpart
297         set l [string last / $dpart]
298         if {[expr $l >= 0]} {
299             set surl [string range $dpart 0 $l]$surl
300         } else {
301             set surl $dpart/$surl
302         }
303     }
304     set c [split $surl /]
305     set i [llength $c]
306     incr i -1
307     set path [lindex $c $i]
308     incr i -1
309     while {$i >= 0} {
310         switch -- [lindex $c $i] {
311             .. {
312                 incr i -2
313                 if {$i < 0} {
314                     set i 0
315                 }
316             }
317             . {
318                 incr i -1
319             }
320             default {
321                 set path [lindex $c $i]/$path
322                 incr i -1
323             }
324         }
325     }
326     regsub -all {~} $path {%7E} path
327     set href "$method://$host$path"
328     puts "Ref href = $href"
329     return 1
330 }
331
332 proc RobotError {url code} {
333     global URL
334
335     puts "Bad URL $url, $code"
336     set fromurl {}
337     set distance -1
338     if {[RobotFileExist unvisited $URL($url,hostport) $URL($url,path)]} {
339         set inf [RobotFileOpen unvisited $URL($url,hostport) $URL($url,path) r]
340         RobotReadRecord $inf fromurl distance
341         RobotFileClose $inf
342     }
343     RobotFileUnlink unvisited $URL($url,hostport) $URL($url,path)
344     if {![RobotFileExist bad $URL($url,hostport) $URL($url,path)]} {
345         set outf [RobotFileOpen bad $URL($url,hostport) $URL($url,path)]
346         RobotWriteRecord $outf $fromurl $distance
347         RobotFileClose $outf
348     }
349 }
350
351 proc RobotRedirect {url tourl code} {
352     global URL
353
354     puts "Redirecting from $url to $tourl"
355
356     set distance {}
357     set fromurl {}
358     if {[RobotFileExist unvisited $URL($url,hostport) $URL($url,path)]} {
359         set inf [RobotFileOpen unvisited $URL($url,hostport) $URL($url,path) r]
360         RobotReadRecord $inf fromurl distance
361         RobotFileClose $inf
362     }
363     if {![RobotFileExist bad $URL($url,hostport) $URL($url,path)]} {
364         set outf [RobotFileOpen bad $URL($url,hostport) $URL($url,path)]
365         RobotWriteRecord $outf $fromurl $distance
366         RobotFileClose $outf
367     }
368     if {[RobotHref $url tourl host path]} {
369         if {![RobotFileExist visited $host $path]} {
370             if {![RobotFileExist unvisited $host $path]} {
371                 set outf [RobotFileOpen unvisited $host $path]
372                 RobotWriteRecord $outf $fromurl $distance
373                 RobotFileClose $outf
374             }
375         } else {
376             set olddistance {}
377             set inf [RobotFileOpen visited $host $path r]
378             RobotReadRecord $inf oldurl olddistance
379             RobotFileClose $inf
380             if {[string length $olddistance] == 0} {
381                 set olddistance 1000
382             }
383             if {[string length $distance] == 0} {
384                 set distance 1000
385             }
386             puts "distance=$distance olddistance=$olddistance"
387             if {[expr $distance < $olddistance]} {
388                 set outf [RobotFileOpen unvisited $host $path]
389                 RobotWriteRecord $outf $tourl $distance
390                 RobotFileClose $outf
391             }
392         }
393     }
394     if {[catch {RobotFileUnlink unvisited $URL($url,hostport) $URL($url,path)}]} {
395         puts "unlink failed"
396         exit 1
397     }
398 }
399
400 proc RobotTextHtml {url out} {
401     global URL maxDistance
402
403     set distance 0
404     if {$maxDistance < 1000 && [info exists URL($url,dist)]} {
405         set distance [expr $URL($url,dist) + 1]
406     }
407     htmlSwitch $URL($url,buf) \
408         title {
409             puts $out "<title>$body</title>"
410         } -nonest meta {
411             puts -nonewline $out "<meta"
412             foreach a [array names parm] {
413                 puts -nonewline $out " $a"
414                 puts -nonewline $out {="}
415                 puts -nonewline $out $parm($a)
416                 puts -nonewline $out {"}
417             }
418             puts $out {></meta>}
419         } body {
420             regsub -all -nocase {<script.*</script>} $body {} abody
421             regsub -all {<[^\>]+>} $abody {} nbody
422             puts $out "<documentcontent>"
423             puts $out $nbody
424             puts $out "</documentcontent>"
425         } a {
426             if {![info exists parm(href)]} {
427                 puts "no href"
428                 continue
429             }
430             if {[expr $distance <= $maxDistance]} {
431                 set href [string trim $parm(href)]
432                 if {![RobotHref $url href host path]} continue
433                 
434                 puts $out "<cr>"
435                 puts $out "<identifier>$href</identifier>"
436                 puts $out "<description>$body</description>"
437                 puts $out "</cr>"
438
439                 if {![RobotFileExist visited $host $path]} {
440                     set olddistance 1000
441                     if {![RobotFileExist bad $host $path]} {
442                         if {[RobotFileExist unvisited $host $path]} {
443                             set inf [RobotFileOpen unvisited $host $path r]
444                             RobotReadRecord $inf oldurl olddistance
445                             RobotFileClose $inf
446                         }
447                     } else {
448                         set olddistance 0
449                     }
450                     if {[string length $olddistance] == 0} {
451                         set olddistance 1000
452                     }
453                     if {[expr $distance < $olddistance]} {
454                         set outf [RobotFileOpen unvisited $host $path]
455                         RobotWriteRecord $outf $url $distance
456                         RobotFileClose $outf
457                     }
458                 } elseif {[string compare $href $url]} {
459                     set inf [RobotFileOpen visited $host $path r]
460                     RobotReadRecord $inf xurl olddistance
461                     close $inf
462                     if {[string length $olddistance] == 0} {
463                         set olddistance 1000
464                     }
465                     if {[expr $distance < $olddistance]} {
466                         puts "OK remarking url=$url href=$href"
467                         puts "olddistance = $olddistance"
468                         puts "newdistance = $distance"
469                         set outf [RobotFileOpen unvisited $host $path]
470                         RobotWriteRecord $outf $url $distance
471                         RobotFileClose $outf
472                     }
473                 }
474             }
475         }
476 }
477
478 proc RobotsTxt {url} {
479     global agent URL
480
481     RobotsTxt0 URL(URL($url,hostport),robots) $URL($url,buf)
482 }
483
484 proc RobotsTxt0 {v buf} {
485     global URL agent
486     set section 0
487     foreach l [split $buf \n] {
488         if {[regexp {([-A-Za-z]+):[ \t]*([^\#]+)} $l match cmd arg]} {
489             puts "cmd=$cmd arg=$arg"
490             switch $cmd {
491                 User-Agent {
492                     if {$section} break
493                     set pat [string tolower $arg]*
494                     set section [string match $pat $agent]
495                 }
496                 Disallow {
497                     if {$section} {
498                         puts "rule [list 0 $arg]"
499                         lappend $v [list 0 $arg]
500                     }
501                 }
502                 Allow {
503                     if {$section} {
504                         puts "rule [list 1 $arg]"
505                         lappend $v [list 1 $arg]
506                     }
507                 }
508             }
509         }
510     }
511 }
512
513 proc RobotTextPlain {url out} {
514     global URL
515
516     puts $out "<documentcontent>"
517     puts $out $URL($url,buf)
518     puts $out "</documentcontent>"
519
520     if {![string compare $URL($url,path) /robots.txt]} {
521         RobotsTxt $url
522     }
523 }
524
525 proc Robot200 {url} {
526     global URL domains
527     
528     set out [RobotFileOpen visited $URL($url,hostport) $URL($url,path)]
529     puts $out "<zmbot>"
530
531     set distance 1000
532     if {[RobotFileExist unvisited $URL($url,hostport) $URL($url,path)]} {
533         set inf [RobotFileOpen unvisited $URL($url,hostport) $URL($url,path) r]
534         RobotReadRecord $inf fromurl distance
535         RobotFileClose $inf
536     }
537     set URL($url,dist) $distance
538     puts $out "<distance>"
539     puts $out "  $distance"
540     puts $out "</distance>"
541     headSave $url $out
542     puts "Parsing $url distance=$distance"
543     switch $URL($url,head,content-type) {
544         text/html {
545             if {[string length $distance]} {
546                 RobotTextHtml $url $out
547             }
548         }
549         text/plain {
550             RobotTextPlain $url $out
551         }
552         application/pdf {
553             set pdff [open test.pdf w]
554             puts -nonewline $pdff $URL($url,buf)
555             close $pdff
556         }
557     }
558     puts $out "</zmbot>"
559     RobotFileClose $out
560     # puts "Parsing done"
561     RobotFileUnlink unvisited $URL($url,hostport) $URL($url,path)
562 }
563
564 proc RobotReadContent {url sock binary} {
565     global URL
566
567     puts "RobotReadContent $url"
568     set buffer [read $sock 16384]
569     set readCount [string length $buffer]
570
571     if {$readCount <= 0} {
572         Robot200 $url
573         RobotRestart $url $sock
574     } elseif {!$binary && [string first \0 $buffer] >= 0} {
575         Robot200 $url
576         RobotRestart $url $sock
577     } else {
578         # puts "Got $readCount bytes"
579         set URL($url,buf) $URL($url,buf)$buffer
580     }
581 }
582
583 proc RobotReadHeader {url sock} {
584     global URL
585
586     puts "RobotReadHeader $url"
587     if {[catch {set buffer [read $sock 2148]}]} {
588         RobotError $url 404
589         RobotRestart $url $sock
590     }
591     set readCount [string length $buffer]
592     
593     if {$readCount <= 0} {
594         RobotError $url 404
595         RobotRestart $url $sock
596     } else {
597         # puts "Got $readCount bytes"
598         set URL($url,buf) $URL($url,buf)$buffer
599         
600         set n [string first \r\n\r\n $URL($url,buf)]
601         if {$n > 1} {
602             set code 0
603             set version {}
604             set headbuf [string range $URL($url,buf) 0 $n]
605             incr n 4
606             set URL($url,buf) [string range $URL($url,buf) $n end]
607             
608             regexp {^HTTP/([0-9.]+)[ ]+([0-9]+)} $headbuf x version code
609             set lines [split $headbuf \n]
610             foreach line $lines {
611                 if {[regexp {^([^:]+):[ ]+(.*)} $line x name value]} {
612                     set URL($url,head,[string tolower $name]) [string trim $value]
613                 }
614             }
615             puts "code = $code"
616             set URL($url,state) skip
617             switch $code {
618                 301 {
619                     RobotRedirect $url $URL($url,head,location) 301
620                     RobotRestart $url $sock
621                 }
622                 302 {
623                     RobotRedirect $url $URL($url,head,location) 302
624                     RobotRestart $url $sock
625                 }
626                 200 {
627                     if {![info exists URL($url,head,content-type)]} {
628                         set URL($url,head,content-type) {}
629                     }
630                     set binary 0
631                     switch $URL($url,head,content-type) {
632                         application/pdf {
633                             set binary 1
634                         }
635                     }
636                     fileevent $sock readable [list RobotReadContent $url $sock $binary]
637                 }
638                 default {
639                     RobotError $url $code
640                     RobotRestart $url $sock
641                 }
642             }
643         }
644     }
645 }
646
647 proc RobotSockCancel {url sock} {
648
649     puts "RobotSockCancel sock=$sock url=$url"
650     RobotError $url 401
651     RobotRestart $url $sock
652 }
653
654 proc RobotConnect {url sock} {
655     global URL agent
656
657     fconfigure $sock -translation {lf crlf} -blocking 0
658     fileevent $sock readable [list RobotReadHeader $url $sock]
659     puts $sock "GET $URL($url,path) HTTP/1.0"
660     puts $sock "Host: $URL($url,host)"
661     puts $sock "User-Agent: $agent"
662     puts $sock ""
663     flush $sock
664     set URL($sock,cancel) [after 30000 [list RobotSockCancel $url $sock]]
665 }
666
667 proc RobotNop {} {
668
669 }
670
671 proc RobotGetUrl {url phost} {
672     global URL robotsRunning
673     flush stdout
674     puts "RobotGetUrl --------- robotsRunning=$robotsRunning url=$url"
675     if {![regexp {([^:]+)://([^/]+)(.*)} $url x method hostport path]} {
676         return -1
677     }
678     if {![regexp {([^:]+):([0-9]+)} $hostport x host port]} {
679         set port 80
680         set host $hostport
681     }
682     set URL($url,method) $method
683     set URL($url,host) $host
684     set URL($url,hostport) $hostport
685     set URL($url,path) $path
686     set URL($url,state) head
687     set URL($url,buf) {}
688
689     if {[string compare $path /robots.txt]} {
690         set ok 1
691         if {![info exists URL($hostport,robots)]} {
692             puts "READING robots.txt for host $hostport"
693             if {[RobotFileExist visited $hostport /robots.txt]} {
694                 set inf [RobotFileOpen visited $hostport /robots.txt r]
695                 set buf [read $inf 32768]
696                 close $inf
697             } else {
698                 set buf "User-Agent: *\nAllow: /\n"
699             }
700             RobotsTxt0 URL($hostport,robots) $buf
701         }
702         if {[info exists URL($hostport,robots)]} {
703             foreach l $URL($hostport,robots) {
704                 if {[string first [lindex $l 1] $path] == 0} {
705                     set ok [lindex $l 0]
706                     break
707                 }
708             }
709         }
710         if {!$ok} {
711             return -1
712         }
713     }
714     if [catch {set sock [socket -async $host $port]}] {
715         return -1
716     }
717     RobotConnect $url $sock
718
719     return 0
720 }
721
722 if {![llength [info commands htmlSwitch]]} {
723     set e [info sharedlibextension]
724     if {[catch {load ./tclrobot$e}]} {
725         load tclrobot$e
726     }
727 }
728
729 set agent "zmbot/0.0"
730 if {![catch {set os [exec uname -s -r]}]} {
731     set agent "$agent ($os)"
732         puts "agent: $agent"
733 }
734
735 proc bgerror {m} {
736     global errorInfo
737     puts "BGERROR $m"
738     puts $errorInfo
739 }
740
741 set robotsRunning 0
742 set robotsMax 5
743 set robotSeq 0
744 set workdir [pwd]
745 set idleTime 60000
746
747 if {[llength $argv] < 2} {
748     puts "Tclrobot: usage <range> <domain> <start>"
749     puts " Example: 3 '*.indexdata.dk' http://www.indexdata.dk/"
750     exit 1
751 }
752
753 set maxDistance [lindex $argv 0]
754 set domains [lindex $argv 1]
755 foreach href [lindex $argv 2] {
756     if {[RobotHref http://www.indexdata.dk/ href host path]} {
757         if {![RobotFileExist visited $host $path]} {
758             set outf [RobotFileOpen unvisited $host $path]
759             RobotWriteRecord $outf $href 0
760             RobotFileClose $outf
761         }
762     }
763 }
764
765 RobotStart
766
767 while {$robotsRunning} {
768     vwait robotsRunning
769 }