Tcl Library Source Code

Changes On Branch aku-the-big-httpd-testsuite-cleanup
Login

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Changes In Branch aku-the-big-httpd-testsuite-cleanup Excluding Merge-Ins

This is equivalent to a diff from 6ae00eec07 to 367088a3b3

2019-06-05
05:02
Integrated testsuite fixups Merged latest work from trunk Regenerated documentation. - Fixed outdated references to feedback.inc Use `modules/common-text/feedback.inc` now. check-in: e3ae2af3f4 user: aku tags: hypnotoad
2019-06-04
20:08
Added demonstrations of passing configuration parameters and seeing the data reflected in behavior in httpd Closed-Leaf check-in: 367088a3b3 user: hypnotoad tags: aku-the-big-httpd-testsuite-cleanup
19:50
Removing hardcoded path, and registering the paths in the appropriate manner in the httpd tests check-in: 357caec02f user: hypnotoad tags: aku-the-big-httpd-testsuite-cleanup
07:34
Starting to investigate the issue with `scgi-client-0006` I was unable to help myself and started a cleanup of the tests first. - Fixed up indentation all over the place. - Moved all the various expected replies (404, 200, 500) into constructor procedures. This not only made the individual tests clearer, but also easier to properly indent. - Removed the custom `httpd::test::compare` (HTC). Switched to regular tcltest glob matching, plus string map for basic templating of some expected results (200... variants). - The thing with the `norm-eol` I had to add (*) makes me suspect that HTC hid a possible bug in the proxy coding around the handling of line endings. (Of course, my norm-eol now works around the same thing, if in a different manner). (Ad *) See marker `AKU` in the code. - Moved the cgi helper application into an `assets` directory. No need for dynamic creation and deletion. Just tweak the `path` it is looked for. With all this I can now easy see that `scgi-client-0006` returns a 500 server error to us, instead of the expected file. check-in: 91d4900a3d user: aku tags: aku-the-big-httpd-testsuite-cleanup
2019-06-03
23:44
Update to clay, practcl, and httpd brought over from the clay project. clay - fixes a typo in the ensemble generator - Removed a call to cron::object_destroy (we don't always run with cron) - Added a tool-like event manager - Removed where the amalgamater was adding dictargs twice - Added a "short" uuid for local projects httpd - updates to clay - Test fixes - Remove CSS templating from the vanilla httpd - Some behind the scenes tweaks to accomodate the cuneiform module when httpd is used in external packages practcl - updates to clay - Static packages are now advertised in the tclpreinitscript, eliminating the need for the master process to throw a bootstrap to new threads check-in: 6ae00eec07 user: hypnotoad tags: hypnotoad
2019-05-30
18:22
Updated clay to mirror the latest version on http://fossil.etoyoc.com/fossil/clay (0.8) * Adds support for short UUIDs * Adds support for the embedded md5 generator in odielibc * Integrates dictargs into the clay syntax Update practcl * Embedded clay version is now 0.8 * Fixes a bug in installModule for an empty directory * Fixes a longstanding issue with bootstrapping child interps and threads from kits check-in: 0aa60d1fa8 user: hypnotoad tags: hypnotoad

Changes to modules/clay/build/event.tcl.



1

2
3
4
5
6

7
8
9

10
11
12
13
14

15
16
17
18
19
20
21
22
23
24
25
26
27





























28
29
30
31
32
33
34
1
2

3





4



5





6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
+
+
-
+
-
-
-
-
-
+
-
-
-
+
-
-
-
-
-
+













+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+







if {[info commands ::cron::object_destroy] eq {}} {
  # Provide a noop if we aren't running with the cron scheduler
::namespace eval ::clay::event {}
  namespace eval ::cron {}

###
# Mark an object for destruction on the next cleanup
###
proc ::clay::destroy args {
  proc ::cron::object_destroy args {}
  if {![info exists ::clay::idle_destroy]} {
    set ::clay::idle_destroy {}
  }
}
  foreach object $args {
    if {$object in $::clay::idle_destroy} continue
    lappend ::clay::idle_destroy  $object
  }
}
::namespace eval ::clay::event {}

###
# Process the queue of objects to be destroyed
###
proc ::clay::cleanup {} {
  if {![info exists ::clay::idle_destroy]} return
  foreach obj $::clay::idle_destroy {
    if {[info commands $obj] ne {}} {
      catch {$obj destroy}
    }
  }
  set ::clay::idle_destroy {}
}

proc ::clay::object_create {objname {class {}}} {
  #if {$::clay::trace>0} {
  #  puts [list $objname CREATE]
  #}
}

proc ::clay::object_rename {object newname} {
  if {$::clay::trace>0} {
    puts [list $object RENAME -> $newname]
  }
}

###
# Mark an objects for destruction on the next cleanup
###
proc ::clay::object_destroy args {
  if {![info exists ::clay::idle_destroy]} {
    set ::clay::idle_destroy {}
  }
  foreach objname $args {
    if {$::clay::trace>0} {
      puts [list $objname DESTROY]
    }
    ::cron::object_destroy $objname
    if {$objname in $::clay::idle_destroy} continue
    lappend ::clay::idle_destroy $objname
  }
}

###
# description: Cancel a scheduled event
###
proc ::clay::event::cancel {self {task *}} {
  variable timer_event
  variable timer_script

Changes to modules/clay/build/metaclass.tcl.

105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
105
106
107
108
109
110
111

112
113
114
115
116
117
118







-







proc ::clay::define::destructor rawbody {
  set body {
# Run the destructor once and only once
set self [self]
my variable DestroyEvent
if {$DestroyEvent} return
set DestroyEvent 1
::clay::object_destroy $self
}
  append body $rawbody
  ::oo::define [current_class] destructor $body
}

proc ::clay::define::Dict {name {values {}}} {
  set class [current_class]
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
190
191
192
193
194
195
196



























-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
#    (if missing) when the object changes class via the [emph morph] method.
###
proc ::clay::define::Variable {name {default {}}} {
  set class [current_class]
  set name [string trimright $name :/]
  $class clay set variable/ $name $default
}

proc ::clay::object_create {objname {class {}}} {
  #if {$::clay::trace>0} {
  #  puts [list $objname CREATE]
  #}
}

proc ::clay::object_rename {object newname} {
  if {$::clay::trace>0} {
    puts [list $object RENAME -> $newname]
  }
}

proc ::clay::object_destroy objname {
  if {$::clay::trace>0} {
    puts [list $objname DESTROY]
  }
  #::cron::object_destroy $objname
}

Changes to modules/clay/build/object.tcl.

457
458
459
460
461
462
463


















464
465
466
467
468
469
470
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488







+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+







        }
        foreach class $clayorder {
          if {[$class clay exists {*}$args]} {
            return $class
          }
        }
        return {}
      }
      refcount {
        my variable refcount
        if {![info exists refcount]} {
          return 0
        }
        return $refcount
      }
      refcount_incr {
        my variable refcount
        incr refcount
      }
      refcount_decr {
        my variable refcount
        incr refcount -1
        if {$refcount <= 0} {
          ::clay::object_destroy [self]
        }
      }
      replace {
        set clay [lindex $args 0]
      }
      source {
        source [lindex $args 0]
      }

Changes to modules/clay/clay.man.

443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476













477
478
479
480
481
482
483
443
444
445
446
447
448
449









450
451
452
453
454
455







456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480







-
-
-
-
-
-
-
-
-






-
-
-
-
-
-
-





+
+
+
+
+
+
+
+
+
+
+
+
+







    [example {property variable NAME {default DEFAULT}}]
    [para]
    Variables registered in the variable property are also initialized
    (if missing) when the object changes class via the [emph morph] method.




[call proc [cmd clay::object_create] [arg objname] [opt "[arg class] [const ""]"]]


[call proc [cmd clay::object_rename] [arg object] [arg newname]]


[call proc [cmd clay::object_destroy] [arg objname]]


[call proc [cmd clay::ensemble_methodbody] [arg ensemble] [arg einfo]]


[call proc [cmd clay::define::Ensemble] [arg rawmethod] [opt "[arg args]"]]


[call proc [cmd clay::destroy] [opt "[arg args]"]]

 Mark an object for destruction on the next cleanup




[call proc [cmd clay::cleanup]]

 Process the queue of objects to be destroyed




[call proc [cmd clay::object_create] [arg objname] [opt "[arg class] [const ""]"]]


[call proc [cmd clay::object_rename] [arg object] [arg newname]]


[call proc [cmd clay::object_destroy] [opt "[arg args]"]]

 Mark an objects for destruction on the next cleanup




[call proc [cmd clay::event::cancel] [arg self] [opt "[arg task] [const "*"]"]]

Cancel a scheduled event

Changes to modules/clay/clay.tcl.

1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1025
1026
1027
1028
1029
1030
1031

1032
1033
1034
1035
1036
1037
1038







-







proc ::clay::define::destructor rawbody {
  set body {
# Run the destructor once and only once
set self [self]
my variable DestroyEvent
if {$DestroyEvent} return
set DestroyEvent 1
::clay::object_destroy $self
}
  append body $rawbody
  ::oo::define [current_class] destructor $body
}
proc ::clay::define::Dict {name {values {}}} {
  set class [current_class]
  set name [string trim $name :/]
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1087
1088
1089
1090
1091
1092
1093
















1094
1095
1096
1097
1098
1099
1100







-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-







  }
}
proc ::clay::define::Variable {name {default {}}} {
  set class [current_class]
  set name [string trimright $name :/]
  $class clay set variable/ $name $default
}
proc ::clay::object_create {objname {class {}}} {
  #if {$::clay::trace>0} {
  #  puts [list $objname CREATE]
  #}
}
proc ::clay::object_rename {object newname} {
  if {$::clay::trace>0} {
    puts [list $object RENAME -> $newname]
  }
}
proc ::clay::object_destroy objname {
  if {$::clay::trace>0} {
    puts [list $objname DESTROY]
  }
  #::cron::object_destroy $objname
}

###
# END: metaclass.tcl
###
###
# START: ensemble.tcl
###
1769
1770
1771
1772
1773
1774
1775


















1776
1777
1778
1779
1780
1781
1782
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783







+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+







        }
        foreach class $clayorder {
          if {[$class clay exists {*}$args]} {
            return $class
          }
        }
        return {}
      }
      refcount {
        my variable refcount
        if {![info exists refcount]} {
          return 0
        }
        return $refcount
      }
      refcount_incr {
        my variable refcount
        incr refcount
      }
      refcount_decr {
        my variable refcount
        incr refcount -1
        if {$refcount <= 0} {
          ::clay::object_destroy [self]
        }
      }
      replace {
        set clay [lindex $args 0]
      }
      source {
        source [lindex $args 0]
      }
1905
1906
1907
1908
1909
1910
1911


1912

1913
1914

1915
1916
1917

1918
1919
1920
1921

1922
1923
1924
1925
1926
1927
1928
1929
1930























1931
1932
1933
1934
1935
1936
1937
1906
1907
1908
1909
1910
1911
1912
1913
1914

1915


1916



1917




1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957







+
+
-
+
-
-
+
-
-
-
+
-
-
-
-
+









+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+








###
# END: object.tcl
###
###
# START: event.tcl
###
if {[info commands ::cron::object_destroy] eq {}} {
  # Provide a noop if we aren't running with the cron scheduler
::namespace eval ::clay::event {
  namespace eval ::cron {}
}
proc ::clay::destroy args {
  proc ::cron::object_destroy args {}
  if {![info exists ::clay::idle_destroy]} {
    set ::clay::idle_destroy {}
  }
}
  foreach object $args {
    if {$object in $::clay::idle_destroy} continue
    lappend ::clay::idle_destroy  $object
  }
::namespace eval ::clay::event {
}
proc ::clay::cleanup {} {
  if {![info exists ::clay::idle_destroy]} return
  foreach obj $::clay::idle_destroy {
    if {[info commands $obj] ne {}} {
      catch {$obj destroy}
    }
  }
  set ::clay::idle_destroy {}
}
proc ::clay::object_create {objname {class {}}} {
  #if {$::clay::trace>0} {
  #  puts [list $objname CREATE]
  #}
}
proc ::clay::object_rename {object newname} {
  if {$::clay::trace>0} {
    puts [list $object RENAME -> $newname]
  }
}
proc ::clay::object_destroy args {
  if {![info exists ::clay::idle_destroy]} {
    set ::clay::idle_destroy {}
  }
  foreach objname $args {
    if {$::clay::trace>0} {
      puts [list $objname DESTROY]
    }
    ::cron::object_destroy $objname
    if {$objname in $::clay::idle_destroy} continue
    lappend ::clay::idle_destroy $objname
  }
}
proc ::clay::event::cancel {self {task *}} {
  variable timer_event
  variable timer_script

  foreach {id event} [array get timer_event $self:$task] {
    ::after cancel $event

Added modules/httpd/assets/test_cgi.tcl.















1
2
3
4
5
6
7
8
9
10
11
12
13
14
+
+
+
+
+
+
+
+
+
+
+
+
+
+
#!/usr/bin/tclsh

puts stdout "Status: 200 OK"
if {$::env(CONTENT_LENGTH) > 0} {
  puts stdout "Content-Type: $::env(CONTENT_TYPE)"
  set dat [read stdin $::env(CONTENT_LENGTH)]
} else {
  puts stdout "Content-Type: text/plain"
  set dat "Hi!"
}
puts stdout "Content-Length: [string length $dat]"
puts stdout {}
puts stdout $dat
exit 0

Changes to modules/httpd/build/cgi.tcl.

90
91
92
93
94
95
96

97
98
99
100
101
102
103
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104







+







      ###
      # Send any POST/PUT/etc content
      ###
      my ChannelCopy $chana $chanb -size $length
    } else {
      chan flush $chanb
    }
    my clay refcount_incr
    chan event $chanb readable [info coroutine]
    yield
  }


  method ProxyReply {chana chanb args} {
    my log ProxyReply [list args $args]
120
121
122
123
124
125
126

127
128
129
130
131
132
133
134
135
136
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138







+










    chan puts $chanb $replybuffer
    ###
    # Output the body. With no -size flag, channel will copy until EOF
    ###
    chan configure $chana -translation binary -blocking 0 -buffering full -buffersize 4096
    chan configure $chanb -translation binary -blocking 0 -buffering full -buffersize 4096
    my ChannelCopy $chana $chanb -chunk 4096
    my clay refcount_decr
  }

  ###
  # For most CGI applications a directory list is vorboten
  ###
  method DirectoryListing {local_file} {
    my error 403 {Not Allowed}
    tailcall my DoOutput
  }
}

Changes to modules/httpd/build/core.tcl.

30
31
32
33
34
35
36


37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72







































73
74
75
76
77
78
79
30
31
32
33
34
35
36
37
38




































39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84







+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+







###
# A metaclass for MIME handling behavior across a live socket
###
clay::define ::httpd::mime {


  method ChannelCopy {in out args} {
    try {
      my clay refcount_incr
    set chunk 4096
    set size -1
    foreach {f v} $args {
      set [string trim $f -] $v
    }
    dict set info coroutine [info coroutine]
    if {$size>0 && $chunk>$size} {
        set chunk $size
    }
    set bytes 0
    set sofar 0
    set method [self method]
    while 1 {
      set command {}
      set error {}
      if {$size>=0} {
        incr sofar $bytes
        set remaining [expr {$size-$sofar}]
        if {$remaining <= 0} {
          break
        } elseif {$chunk > $remaining} {
          set chunk $remaining
        }
      }
      lassign [yieldto chan copy $in $out -size $chunk \
        -command [list [info coroutine] $method]] \
        command bytes error
      if {$command ne $method} {
        error "Subroutine $method interrupted"
      }
      if {[string length $error]} {
        error $error
      }
      if {[chan eof $in]} {
        break
      }
      set chunk 4096
      set size -1
      foreach {f v} $args {
        set [string trim $f -] $v
      }
      dict set info coroutine [info coroutine]
      if {$size>0 && $chunk>$size} {
          set chunk $size
      }
      set bytes 0
      set sofar 0
      set method [self method]
      while 1 {
        set command {}
        set error {}
        if {$size>=0} {
          incr sofar $bytes
          set remaining [expr {$size-$sofar}]
          if {$remaining <= 0} {
            break
          } elseif {$chunk > $remaining} {
            set chunk $remaining
          }
        }
        lassign [yieldto chan copy $in $out -size $chunk \
          -command [list [info coroutine] $method]] \
          command bytes error
        if {$command ne $method} {
          error "Subroutine $method interrupted"
        }
        if {[string length $error]} {
          error $error
        }
        if {[chan eof $in]} {
          break
        }
      }
    } finally {
      my clay refcount_decr
    }
  }

  ###
  # Returns a block of HTML
  method html_header {{title {}} args} {
    set result {}
297
298
299
300
301
302
303

304
305
306
307
308
309
310
311

312
313
314
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321







+








+



      }
    }
    return $pathlist
  }


  method wait {mode sock} {
    my clay refcount_incr
    if {[info coroutine] eq {}} {
      chan event $sock $mode [list set ::httpd::lock_$sock $mode]
      vwait ::httpd::lock_$sock
    } else {
      chan event $sock $mode [info coroutine]
      yield
    }
    chan event $sock $mode {}
    my clay refcount_decr
  }

}

Changes to modules/httpd/build/dispatch.tcl.

25
26
27
28
29
30
31
32
33
34
35



36
37
38
39
40
41
42
43
44
45
46
47
25
26
27
28
29
30
31




32
33
34





35
36
37
38
39
40
41







-
-
-
-
+
+
+
-
-
-
-
-







  }
}

::clay::define ::httpd::content.cache {

  method Dispatch {} {
    my variable chan
    try {
      my wait writable $chan
      chan configure $chan  -translation {binary binary}
      chan puts -nonewline $chan [my clay get cache/ data]
    my wait writable $chan
    chan configure $chan  -translation {binary binary}
    chan puts -nonewline $chan [my clay get cache/ data]
    } on error {err info} {
      my <server> debug [dict get $info -errorinfo]
    } finally {
      my TransferComplete $chan
    }
  }
}

::clay::define ::httpd::content.template {

  method content {} {
    if {[my request get HTTP_STATUS] ne {}} {

Changes to modules/httpd/build/file.tcl.

127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150

















151
152
153
154
155
127
128
129
130
131
132
133

















134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150



151
152







-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-


      tailcall my DoOutput
    }
    if {$chan eq {}} return
    my wait writable $chan
    if {![info exists reply_file]} {
      tailcall my DoOutput
    }
    try {
      chan configure $chan  -translation {binary binary}
      my log HttpAccess {}
      ###
      # Return a stream of data from a file
      ###
      set size [file size $reply_file]
      my reply set Content-Length $size
      append result [my reply output] \n
      chan puts -nonewline $chan $result
      set reply_chan [open $reply_file r]
      my log SendReply [list length $size]
      ###
      # Output the file contents. With no -size flag, channel will copy until EOF
      ###
      chan configure $reply_chan -translation {binary binary} -buffersize 4096 -buffering full -blocking 0
      my ChannelCopy $reply_chan $chan -chunk 4096
    chan configure $chan  -translation {binary binary}
    my log HttpAccess {}
    ###
    # Return a stream of data from a file
    ###
    set size [file size $reply_file]
    my reply set Content-Length $size
    append result [my reply output] \n
    chan puts -nonewline $chan $result
    set reply_chan [open $reply_file r]
    my ChannelRegister $reply_chan
    my log SendReply [list length $size]
    ###
    # Output the file contents. With no -size flag, channel will copy until EOF
    ###
    chan configure $reply_chan -translation {binary binary} -buffersize 4096 -buffering full -blocking 0
    my ChannelCopy $reply_chan $chan -chunk 4096
    } finally {
      my TransferComplete $reply_chan $chan
    }
  }
}

Changes to modules/httpd/build/plugin.tcl.

199
200
201
202
203
204
205
206

207
208
209
210
199
200
201
202
203
204
205

206
207
208
209
210







-
+




    }
    $pageobj clay mixinmap {*}$mixinmap
    if {[dict exists $reply delegate]} {
      $pageobj clay delegate {*}[dict get $reply delegate]
    }
    $pageobj dispatch $sock $reply
    set output [$pageobj output]
    catch {$pageobj destroy}
    $pageobj clay refcount_decr
    return $output
  }
}

Changes to modules/httpd/build/proxy.tcl.

155
156
157
158
159
160
161
162
163
164



165
166
167
168
169
155
156
157
158
159
160
161



162
163
164



165
166







-
-
-
+
+
+
-
-
-


    if {$sock eq {}} {
      my error 404 {Not Found}
      tailcall my DoOutput
    }
    my log HttpAccess {}
    chan event $sock writable [info coroutine]
    yield
    try {
      my ProxyRequest $chan $sock
      my ProxyReply   $sock $chan
    my ChannelRegister $sock
    my ProxyRequest $chan $sock
    my ProxyReply   $sock $chan
    } finally {
      my TransferComplete $chan $sock
    }
  }
}

Changes to modules/httpd/build/reply.man.

129
130
131
132
133
134
135





136
137
138
139
140
141
142
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147







+
+
+
+
+








Terminate the transaction, and close the socket.

[call method [cmd HttpHeaders] [arg sock] [arg ?debug?]]

Stream MIME headers from the socket [arg sock], stopping at an empty line. Returns
the stream as a block of text.

[call method [cmd ChannelRegister] [arg chan]]

Registers a channel that will need to be flushed and closed when the object's destructor
invokes the close method.

[call method [cmd dispatch] [arg newsock] [arg datastate]]

Take over control of the socket [arg newsock], and store that as the [arg chan] variable
for the object. This method runs through all of the steps of reading HTTP headers, generating
content, and closing the connection. (See class writetup).

264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
269
270
271
272
273
274
275






















276
277
278
279
280







-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-





a timeout error to the request if it has, as well as destroy the object and close the
[arg chan] socket.

[call method [cmd timestamp]]

Return the current system time in the format: [example {%a, %d %b %Y %T %Z}]

[call method [cmd TransferComplete] [arg args]]

Intended to be invoked from [cmd {chan copy}] as a callback. This closes every channel
fed to it on the command line, and then destroys the object.

[example {
    ###
    # Output the body
    ###
    chan configure $sock -translation binary -blocking 0 -buffering full -buffersize 4096
    chan configure $chan -translation binary -blocking 0 -buffering full -buffersize 4096
    if {$length} {
      ###
      # Send any POST/PUT/etc content
      ###
      chan copy $sock $chan -size $SIZE -command [info coroutine]
      yield
    }
    catch {close $sock}
    chan flush $chan
}]

[call method [cmd Url_Decode] [arg string]]

De-httpizes a string.

[list_end]

Changes to modules/httpd/build/reply.tcl.

95
96
97
98
99
100
101

102
103
104
105
106
107
108
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109







+







# 	}
# }
#
# }]
###
::clay::define ::httpd::reply {
  superclass ::httpd::mime
  Variable ChannelRegister {}

  Delegate <server> {
    description {The server object which spawned this reply}
  }

  ###
  # A dictionary which will converted into the MIME headers of the reply
121
122
123
124
125
126
127
128

129
130
131
132
133
134
135
136
137
138
139
140
141
142













143
144
145
146
147
148
149
150
151
152
153









154
155

156
157
158
159
160
161
162
122
123
124
125
126
127
128

129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161






162
163
164
165
166
167
168
169
170

171
172
173
174
175
176
177
178
179







-
+














+
+
+
+
+
+
+
+
+
+
+
+
+





-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-

+







    REMOTE_ADDR {}
    REMOTE_HOST {}
    USER_AGENT {}
    SESSION {}
  }

  constructor {ServerObj args} {
    my variable chan dispatched_time uuid
    my variable dispatched_time uuid
    set uuid [namespace tail [self]]
    set dispatched_time [clock milliseconds]
    my clay delegate <server> $ServerObj
    foreach {field value} [::clay::args_to_options {*}$args] {
      my clay set config $field: $value
    }
  }

  ###
  # clean up on exit
  ###
  destructor {
    my close
  }

  # Registers a channel to be closed by the close method
  method ChannelRegister args {
    my variable ChannelRegister
    if {![info exists ChannelRegister]} {
      set ChannelRegister {}
    }
    foreach c $args {
      if {$c ni $ChannelRegister} {
        lappend ChannelRegister $c
      }
    }
  }

  ###
  # Close channels opened by this object
  ###
  method close {} {
    my variable chan
    if {[info exists chan] && $chan ne {}} {
      catch {chan event $chan readable {}}
      catch {chan event $chan writable {}}
      catch {chan flush $chan}
      catch {chan close $chan}
    my variable ChannelRegister
    if {![info exists ChannelRegister]} {
      return
    }
    foreach c $ChannelRegister {
      catch {chan event $c readable {}}
      catch {chan event $c writable {}}
      catch {chan flush $c}
      catch {chan close $c}
      set chan {}
    }
    set ChannelRegister {}
  }

  ###
  # Record a dispatch event
  ###
  method Log_Dispatched {} {
    my log Dispatched [dict create \
183
184
185
186
187
188
189

190

191
192
193
194
195
196
197
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216







+

+







  # ensemble
  # [para]
  # All other fields are passed along to the [method clay] structure of the object.
  ###
  method dispatch {newsock datastate} {
    my variable chan request
    try {
      my clay refcount_incr
      set chan $newsock
      my ChannelRegister $chan
      chan event $chan readable {}
      chan configure $chan -translation {auto crlf} -buffering line
      if {[dict exists $datastate mixin]} {
        set mixinmap [dict get $datastate mixin]
      } else {
        set mixinmap {}
      }
219
220
221
222
223
224
225



226
227
228
229
230
231
232
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254







+
+
+







      }
      my Session_Load
      my Log_Dispatched
      my Dispatch
    } on error {err errdat} {
      my error 500 $err [dict get $errdat -errorinfo]
      my DoOutput
    } finally {
      my close
      my clay refcount_decr
    }
  }

  method Dispatch {} {
    # Invoke the URL implementation.
    my content
    my DoOutput
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
356
357
358
359
360
361
362

363
364
365
366
367
368
369







-







        append result $reply_body
      } else {
        append result [my reply output]
      }
      chan puts -nonewline $chan $result
      my log HttpAccess {}
    }
    my destroy
  }

  ###
  # For GET requests, converts the QUERY_DATA header into a key/value list.
  #
  # For POST requests, reads the Post data and converts that information to
  # a key/value list for application/x-www-form-urlencoded posts. For multipart
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
440
441
442
443
444
445
446

































447
448
449
450
451
452
453







-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-







    }
    return $postdata
  }

  # Manage session data
  method Session_Load {} {}



  # Intended to be invoked from [cmd {chan copy}] as a callback. This closes every channel
  # fed to it on the command line, and then destroys the object.
  #
  # [example {
  #     ###
  #     # Output the body
  #     ###
  #     chan configure $sock -translation binary -blocking 0 -buffering full -buffersize 4096
  #     chan configure $chan -translation binary -blocking 0 -buffering full -buffersize 4096
  #     if {$length} {
  #       ###
  #       # Send any POST/PUT/etc content
  #       ###
  #       chan copy $sock $chan -size $SIZE -command [info coroutine]
  #       yield
  #     }
  #     catch {close $sock}
  #     chan flush $chan
  # }]
  method TransferComplete args {
    my log TransferComplete
    set chan {}
    foreach c $args {
      catch {chan event $c readable {}}
      catch {chan event $c writable {}}
      catch {chan flush $c}
      catch {chan close $c}
    }
    my destroy
  }

  # Appends the value of [arg string] to the end of [arg reply_body], as well as a trailing newline
  # character.
  method puts line {
    my variable reply_body
    append reply_body $line \n
  }

Changes to modules/httpd/build/scgi.tcl.

117
118
119
120
121
122
123

124
125
126
127
128
129
130
131
132
133
134
135
136

137
138
139
140
141
142
143
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136

137
138
139
140
141
142
143
144







+












-
+







    my counter url_hit
    try {
      # Read the SCGI request on byte at a time until we reach a ":"
      dict set query http HTTP_HOST {}
      dict set query http CONTENT_LENGTH 0
      dict set query http REQUEST_URI /
      dict set query http REMOTE_ADDR $ip
      dict set query http DOCUMENT_ROOT [my clay get server/ doc_root]
      set size {}
      while 1 {
        set char [::coroutine::util::read $sock 1]
        if {[chan eof $sock]} {
          catch {close $sock}
          return
        }
        if {$char eq ":"} break
        append size $char
      }
      # With length in hand, read the netstring encoded headers
      set inbuffer [::coroutine::util::read $sock [expr {$size+1}]]
      chan configure $sock -blocking 0 -buffersize 4096 -buffering full
      chan configure $sock -translation {auto crlf} -blocking 0 -buffersize 4096 -buffering full
      foreach {f v} [lrange [split [string range $inbuffer 0 end-1] \0] 0 end-1] {
        dict set query http $f $v
      }
      if {![dict exists $query http REQUEST_PATH]} {
        set uri [dict get $query http REQUEST_URI]
        set uriinfo [::uri::split $uri]
        dict set query http REQUEST_PATH    [dict get $uriinfo path]
161
162
163
164
165
166
167
168

169
170
171
172
173
174
175
162
163
164
165
166
167
168

169
170
171
172
173
174
175
176







-
+







    try {
      set pageobj [::httpd::reply create ::httpd::object::$uuid [self]]
      dict set reply mixin protocol ::httpd::protocol.scgi
      $pageobj dispatch $sock $reply
    } on error {err errdat} {
      my debug [list ip: $ip error: $err errorinfo: [dict get $errdat -errorinfo]]
      my log BadRequest $uuid [list ip: $ip error: $err errorinfo: [dict get $errdat -errorinfo]]
      catch {$pageobj destroy}
      $pageobj clay refcount_decr
      catch {chan event readable $sock {}}
      catch {chan event writeable $sock {}}
      catch {chan close $sock}
      return
    }
  }
}

Changes to modules/httpd/build/server.tcl.

110
111
112
113
114
115
116

117
118
119
120
121
122
123
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124







+







  # normal cases, an object of class [cmd ::http::reply] is created, and that class's
  # [cmd dispatch] method.
  # This action passes control of the socket to
  # the reply object. The reply object manages the rest of the transaction, including
  # closing the socket.
  ###
  method Connect {uuid sock ip} {
    ::clay::cleanup
    yield [info coroutine]
    chan event $sock readable {}
    chan configure $sock \
      -blocking 0 \
      -translation {auto crlf} \
      -buffering line
    my counter url_hit
158
159
160
161
162
163
164
165

166
167

168
169
170
171
172
173
174
159
160
161
162
163
164
165

166
167
168
169
170
171
172
173
174
175
176







-
+


+







  # Check open connections for a time out event.
  ###
  method CheckTimeout {} {
    foreach obj [info commands ::httpd::object::*] {
      try {
        $obj timeOutCheck
      } on error {} {
        catch {$obj destroy}
        $obj clay refcount_decr
      }
    }
    ::clay::cleanup
  }

  method debug args {}

  ###
  # Given a key/value list of information, return a data structure describing how
  # the server should reply.

Changes to modules/httpd/httpd.man.

226
227
228
229
230
231
232
233

234
235

236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252






253
254
255
256
257
258
259
260
261
262
263
264





265
266
267
268
269
270
271
226
227
228
229
230
231
232

233
234

235
236




237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278







-
+

-
+

-
-
-
-












+
+
+
+
+
+












+
+
+
+
+







 }

 }]



[para]
[class {Delegate}]
[class {Variable}]
[list_begin definitions]
[call delegate [cmd <server>]]The server object which spawned this reply
[call variable [cmd ChannelRegister]]

[list_end]
[para]
[class {Variable}]
[list_begin definitions]
[call variable [cmd reply]]
 A dictionary which will converted into the MIME headers of the reply




[call variable [cmd request]]
 A dictionary containing the SCGI transformed HTTP headers for the request




[list_end]
[para]
[class {Delegate}]
[list_begin definitions]
[call delegate [cmd <server>]]The server object which spawned this reply

[list_end]
[para]
[class {Methods}]
[list_begin definitions]
[call method [cmd "constructor"] [arg ServerObj] [opt "[arg args]"]]


[call method [cmd "destructor"] [opt "[arg dictargs]"]]

 clean up on exit




[call method [cmd "ChannelRegister"] [opt "[arg args]"]]
 Registers a channel to be closed by the close method



[call method [cmd "close"]]

 Close channels opened by this object


356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
363
364
365
366
367
368
369























370
371
372
373
374
375
376







-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-







 POST or PUSH. Returns an empty string otherwise.



[call method [cmd "Session_Load"]]
 Manage session data



[call method [cmd "TransferComplete"] [opt "[arg args]"]]
 Intended to be invoked from [cmd {chan copy}] as a callback. This closes every channel
 fed to it on the command line, and then destroys the object.

 [example {
     ###
     # Output the body
     ###
     chan configure $sock -translation binary -blocking 0 -buffering full -buffersize 4096
     chan configure $chan -translation binary -blocking 0 -buffering full -buffersize 4096
     if {$length} {
       ###
       # Send any POST/PUT/etc content
       ###
       chan copy $sock $chan -size $SIZE -command [info coroutine]
       yield
     }
     catch {close $sock}
     chan flush $chan
 }]



[call method [cmd "puts"] [arg line]]
 Appends the value of [arg string] to the end of [arg reply_body], as well as a trailing newline
 character.


Changes to modules/httpd/httpd.tcl.

26
27
28
29
30
31
32


33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68







































69
70
71
72
73
74
75
26
27
28
29
30
31
32
33
34




































35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80







+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+







}
namespace eval ::httpd {
}
namespace eval ::scgi {
}
clay::define ::httpd::mime {
  method ChannelCopy {in out args} {
    try {
      my clay refcount_incr
    set chunk 4096
    set size -1
    foreach {f v} $args {
      set [string trim $f -] $v
    }
    dict set info coroutine [info coroutine]
    if {$size>0 && $chunk>$size} {
        set chunk $size
    }
    set bytes 0
    set sofar 0
    set method [self method]
    while 1 {
      set command {}
      set error {}
      if {$size>=0} {
        incr sofar $bytes
        set remaining [expr {$size-$sofar}]
        if {$remaining <= 0} {
          break
        } elseif {$chunk > $remaining} {
          set chunk $remaining
        }
      }
      lassign [yieldto chan copy $in $out -size $chunk \
        -command [list [info coroutine] $method]] \
        command bytes error
      if {$command ne $method} {
        error "Subroutine $method interrupted"
      }
      if {[string length $error]} {
        error $error
      }
      if {[chan eof $in]} {
        break
      }
      set chunk 4096
      set size -1
      foreach {f v} $args {
        set [string trim $f -] $v
      }
      dict set info coroutine [info coroutine]
      if {$size>0 && $chunk>$size} {
          set chunk $size
      }
      set bytes 0
      set sofar 0
      set method [self method]
      while 1 {
        set command {}
        set error {}
        if {$size>=0} {
          incr sofar $bytes
          set remaining [expr {$size-$sofar}]
          if {$remaining <= 0} {
            break
          } elseif {$chunk > $remaining} {
            set chunk $remaining
          }
        }
        lassign [yieldto chan copy $in $out -size $chunk \
          -command [list [info coroutine] $method]] \
          command bytes error
        if {$command ne $method} {
          error "Subroutine $method interrupted"
        }
        if {[string length $error]} {
          error $error
        }
        if {[chan eof $in]} {
          break
        }
      }
    } finally {
      my clay refcount_decr
    }
  }
  method html_header {{title {}} args} {
    set result {}
    append result "<!DOCTYPE html>\n<HTML><HEAD>"
    if {$title ne {}} {
      append result "<TITLE>$title</TITLE>"
274
275
276
277
278
279
280

281
282
283
284
285
286
287
288

289
290
291
292
293
294
295
296
297
298
299

300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316

317
318
319
320
321
322
323
324
325
326











327
328
329
330
331
332
333









334
335

336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351

352

353
354
355
356
357
358
359
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323

324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346






347
348
349
350
351
352
353
354
355

356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383







+








+











+
















-
+










+
+
+
+
+
+
+
+
+
+
+

-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-

+
















+

+







          lappend pathlist $part
        }
      }
    }
    return $pathlist
  }
  method wait {mode sock} {
    my clay refcount_incr
    if {[info coroutine] eq {}} {
      chan event $sock $mode [list set ::httpd::lock_$sock $mode]
      vwait ::httpd::lock_$sock
    } else {
      chan event $sock $mode [info coroutine]
      yield
    }
    chan event $sock $mode {}
    my clay refcount_decr
  }
}

###
# END: core.tcl
###
###
# START: reply.tcl
###
::clay::define ::httpd::reply {
  superclass ::httpd::mime
  Variable ChannelRegister {}
  Delegate <server> {
    description {The server object which spawned this reply}
  }
  Dict reply {}
  Dict request {
    CONTENT_LENGTH 0
    COOKIE {}
    HTTP_HOST {}
    REFERER {}
    REQUEST_URI {}
    REMOTE_ADDR {}
    REMOTE_HOST {}
    USER_AGENT {}
    SESSION {}
  }
  constructor {ServerObj args} {
    my variable chan dispatched_time uuid
    my variable dispatched_time uuid
    set uuid [namespace tail [self]]
    set dispatched_time [clock milliseconds]
    my clay delegate <server> $ServerObj
    foreach {field value} [::clay::args_to_options {*}$args] {
      my clay set config $field: $value
    }
  }
  destructor {
    my close
  }
  method ChannelRegister args {
    my variable ChannelRegister
    if {![info exists ChannelRegister]} {
      set ChannelRegister {}
    }
    foreach c $args {
      if {$c ni $ChannelRegister} {
        lappend ChannelRegister $c
      }
    }
  }
  method close {} {
    my variable chan
    if {[info exists chan] && $chan ne {}} {
      catch {chan event $chan readable {}}
      catch {chan event $chan writable {}}
      catch {chan flush $chan}
      catch {chan close $chan}
    my variable ChannelRegister
    if {![info exists ChannelRegister]} {
      return
    }
    foreach c $ChannelRegister {
      catch {chan event $c readable {}}
      catch {chan event $c writable {}}
      catch {chan flush $c}
      catch {chan close $c}
      set chan {}
    }
    set ChannelRegister {}
  }
  method Log_Dispatched {} {
    my log Dispatched [dict create \
     REMOTE_ADDR [my request get REMOTE_ADDR] \
     REMOTE_HOST [my request get REMOTE_HOST] \
     COOKIE [my request get HTTP_COOKIE] \
     REFERER [my request get HTTP_REFERER] \
     USER_AGENT [my request get HTTP_USER_AGENT] \
     REQUEST_URI [my request get REQUEST_URI] \
     HTTP_HOST [my request get HTTP_HOST] \
     SESSION [my request get SESSION] \
    ]
  }
  method dispatch {newsock datastate} {
    my variable chan request
    try {
      my clay refcount_incr
      set chan $newsock
      my ChannelRegister $chan
      chan event $chan readable {}
      chan configure $chan -translation {auto crlf} -buffering line
      if {[dict exists $datastate mixin]} {
        set mixinmap [dict get $datastate mixin]
      } else {
        set mixinmap {}
      }
381
382
383
384
385
386
387



388
389
390
391
392
393
394
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421







+
+
+







      }
      my Session_Load
      my Log_Dispatched
      my Dispatch
    } on error {err errdat} {
      my error 500 $err [dict get $errdat -errorinfo]
      my DoOutput
    } finally {
      my close
      my clay refcount_decr
    }
  }
  method Dispatch {} {
    # Invoke the URL implementation.
    my content
    my DoOutput
  }
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
500
501
502
503
504
505
506

507
508
509
510
511
512
513







-







        append result $reply_body
      } else {
        append result [my reply output]
      }
      chan puts -nonewline $chan $result
      my log HttpAccess {}
    }
    my destroy
  }
  method FormData {} {
    my variable chan formdata
    # Run this only once
    if {[info exists formdata]} {
      return $formdata
    }
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
568
569
570
571
572
573
574











575
576
577
578
579
580
581







-
-
-
-
-
-
-
-
-
-
-







      my variable chan
      chan configure $chan -translation binary -blocking 0 -buffering full -buffersize 4096
      set postdata [::coroutine::util::read $chan $length]
    }
    return $postdata
  }
  method Session_Load {} {}
  method TransferComplete args {
    my log TransferComplete
    set chan {}
    foreach c $args {
      catch {chan event $c readable {}}
      catch {chan event $c writable {}}
      catch {chan flush $c}
      catch {chan close $c}
    }
    my destroy
  }
  method puts line {
    my variable reply_body
    append reply_body $line \n
  }
  method RequestFind {field} {
    my variable request
    if {[dict exists $request $field]} {
782
783
784
785
786
787
788

789
790
791
792
793
794
795
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811







+







    dict set result SERVER_SOFTWARE [my clay get server/ string]
    if {[string match 127.* $ip]} {
      dict set result LOCALHOST [expr {[lindex [split [dict getnull $result HTTP_HOST] :] 0] eq "localhost"}]
    }
    return $result
  }
  method Connect {uuid sock ip} {
    ::clay::cleanup
    yield [info coroutine]
    chan event $sock readable {}
    chan configure $sock \
      -blocking 0 \
      -translation {auto crlf} \
      -buffering line
    my counter url_hit
824
825
826
827
828
829
830
831

832
833

834
835
836
837
838
839
840
840
841
842
843
844
845
846

847
848
849
850
851
852
853
854
855
856
857







-
+


+







    incr counters($which)
  }
  method CheckTimeout {} {
    foreach obj [info commands ::httpd::object::*] {
      try {
        $obj timeOutCheck
      } on error {} {
        catch {$obj destroy}
        $obj clay refcount_decr
      }
    }
    ::clay::cleanup
  }
  method debug args {}
  method dispatch {data} {
    set reply [my Dispatch_Local $data]
    if {[dict size $reply]} {
      return $reply
    }
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098



1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1105
1106
1107
1108
1109
1110
1111




1112
1113
1114





1115
1116
1117
1118
1119
1120
1121







-
-
-
-
+
+
+
-
-
-
-
-







    set HTTP_STATUS [my reply get Status]
    my puts [subst $msg]
  }
}
::clay::define ::httpd::content.cache {
  method Dispatch {} {
    my variable chan
    try {
      my wait writable $chan
      chan configure $chan  -translation {binary binary}
      chan puts -nonewline $chan [my clay get cache/ data]
    my wait writable $chan
    chan configure $chan  -translation {binary binary}
    chan puts -nonewline $chan [my clay get cache/ data]
    } on error {err info} {
      my <server> debug [dict get $info -errorinfo]
    } finally {
      my TransferComplete $chan
    }
  }
}
::clay::define ::httpd::content.template {
  method content {} {
    if {[my request get HTTP_STATUS] ne {}} {
      my reply set Status [my request get HTTP_STATUS]
    }
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261

















1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1249
1250
1251
1252
1253
1254
1255

















1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272



1273
1274
1275
1276
1277
1278
1279







-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-







      tailcall my DoOutput
    }
    if {$chan eq {}} return
    my wait writable $chan
    if {![info exists reply_file]} {
      tailcall my DoOutput
    }
    try {
      chan configure $chan  -translation {binary binary}
      my log HttpAccess {}
      ###
      # Return a stream of data from a file
      ###
      set size [file size $reply_file]
      my reply set Content-Length $size
      append result [my reply output] \n
      chan puts -nonewline $chan $result
      set reply_chan [open $reply_file r]
      my log SendReply [list length $size]
      ###
      # Output the file contents. With no -size flag, channel will copy until EOF
      ###
      chan configure $reply_chan -translation {binary binary} -buffersize 4096 -buffering full -blocking 0
      my ChannelCopy $reply_chan $chan -chunk 4096
    chan configure $chan  -translation {binary binary}
    my log HttpAccess {}
    ###
    # Return a stream of data from a file
    ###
    set size [file size $reply_file]
    my reply set Content-Length $size
    append result [my reply output] \n
    chan puts -nonewline $chan $result
    set reply_chan [open $reply_file r]
    my ChannelRegister $reply_chan
    my log SendReply [list length $size]
    ###
    # Output the file contents. With no -size flag, channel will copy until EOF
    ###
    chan configure $reply_chan -translation {binary binary} -buffersize 4096 -buffering full -blocking 0
    my ChannelCopy $reply_chan $chan -chunk 4096
    } finally {
      my TransferComplete $reply_chan $chan
    }
  }
}

###
# END: file.tcl
###
###
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426



1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1425
1426
1427
1428
1429
1430
1431



1432
1433
1434



1435
1436
1437
1438
1439
1440
1441







-
-
-
+
+
+
-
-
-







    if {$sock eq {}} {
      my error 404 {Not Found}
      tailcall my DoOutput
    }
    my log HttpAccess {}
    chan event $sock writable [info coroutine]
    yield
    try {
      my ProxyRequest $chan $sock
      my ProxyReply   $sock $chan
    my ChannelRegister $sock
    my ProxyRequest $chan $sock
    my ProxyReply   $sock $chan
    } finally {
      my TransferComplete $chan $sock
    }
  }
}

###
# END: proxy.tcl
###
###
1525
1526
1527
1528
1529
1530
1531

1532
1533
1534
1535
1536
1537
1538
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544







+







      ###
      # Send any POST/PUT/etc content
      ###
      my ChannelCopy $chana $chanb -size $length
    } else {
      chan flush $chanb
    }
    my clay refcount_incr
    chan event $chanb readable [info coroutine]
    yield
  }
  method ProxyReply {chana chanb args} {
    my log ProxyReply [list args $args]
    chan event $chana readable {}
    set replyhead [my HttpHeaders $chana]
1553
1554
1555
1556
1557
1558
1559

1560
1561
1562
1563
1564
1565
1566
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573







+







    chan puts $chanb $replybuffer
    ###
    # Output the body. With no -size flag, channel will copy until EOF
    ###
    chan configure $chana -translation binary -blocking 0 -buffering full -buffersize 4096
    chan configure $chanb -translation binary -blocking 0 -buffering full -buffersize 4096
    my ChannelCopy $chana $chanb -chunk 4096
    my clay refcount_decr
  }
  method DirectoryListing {local_file} {
    my error 403 {Not Allowed}
    tailcall my DoOutput
  }
}

1684
1685
1686
1687
1688
1689
1690
1691

1692
1693
1694
1695
1696
1697
1698
1691
1692
1693
1694
1695
1696
1697

1698
1699
1700
1701
1702
1703
1704
1705







-
+







          return
        }
        if {$char eq ":"} break
        append size $char
      }
      # With length in hand, read the netstring encoded headers
      set inbuffer [::coroutine::util::read $sock [expr {$size+1}]]
      chan configure $sock -blocking 0 -buffersize 4096 -buffering full
      chan configure $sock -translation {auto crlf} -blocking 0 -buffersize 4096 -buffering full
      foreach {f v} [lrange [split [string range $inbuffer 0 end-1] \0] 0 end-1] {
        dict set query http $f $v
      }
      if {![dict exists $query http REQUEST_PATH]} {
        set uri [dict get $query http REQUEST_URI]
        set uriinfo [::uri::split $uri]
        dict set query http REQUEST_PATH    [dict get $uriinfo path]
1716
1717
1718
1719
1720
1721
1722
1723

1724
1725
1726
1727
1728
1729
1730
1723
1724
1725
1726
1727
1728
1729

1730
1731
1732
1733
1734
1735
1736
1737







-
+







    try {
      set pageobj [::httpd::reply create ::httpd::object::$uuid [self]]
      dict set reply mixin protocol ::httpd::protocol.scgi
      $pageobj dispatch $sock $reply
    } on error {err errdat} {
      my debug [list ip: $ip error: $err errorinfo: [dict get $errdat -errorinfo]]
      my log BadRequest $uuid [list ip: $ip error: $err errorinfo: [dict get $errdat -errorinfo]]
      catch {$pageobj destroy}
      $pageobj clay refcount_decr
      catch {chan event readable $sock {}}
      catch {chan event writeable $sock {}}
      catch {chan close $sock}
      return
    }
  }
}
1887
1888
1889
1890
1891
1892
1893
1894

1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1894
1895
1896
1897
1898
1899
1900

1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913







-
+












    }
    $pageobj clay mixinmap {*}$mixinmap
    if {[dict exists $reply delegate]} {
      $pageobj clay delegate {*}[dict get $reply delegate]
    }
    $pageobj dispatch $sock $reply
    set output [$pageobj output]
    catch {$pageobj destroy}
    $pageobj clay refcount_decr
    return $output
  }
}

###
# END: plugin.tcl
###

    namespace eval ::httpd {
	namespace export *
    }

Changes to modules/httpd/httpd.test.

9
10
11
12
13
14
15
16

17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46



























47
48
49
50

51
52
53
54
55
56
57
58

59
60
61









62






























































































63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97





98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122


























123
124
125
126
127
128
129
130
131
132
133








134
135
136
137
138

139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159




















160
161
162
163
164



165
166
167
168



169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187













188
189
190
191
192
193
194
195
196
197
198
199











200
201
202
203
204
205
206
207





208
209
210
211
212
213
214
215
216







217
218
219
220
221
222




223
224
225
226
227



228
229
230
231
232
233








234
235

236
237
238
239
240

241
242
243
244
245
246
247






248
249
250
251
252
253

254
255

256
257
258
259
260
261

262
263
264
265
266

267
268
269
270
271
272

273
274
275
276
277


278
279
280
281
282
283

284
285
286
287

288
289
290


291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308


309
310
311

312
313
314
315
316
317
318
319
320
321
322
323
324


325
326

327
328
329

330
331


332
333
334
335
336
337


338
339
340


341
342
343
344
345

346
347
348
349



350
351
352
353
354

355
356
357
358
359
360






361











362
363

364
365

366
367
368
369
370
371

372
373
374
375
376


377
378
379
380
381
382

383
384
385
386
387


388
389
390
391
392
393

394
395
396
397

398
399
400


401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418


419
420
421

422
423
424
425
426
427
428
429
430
431
432
433
434


435
436

437




438
439
440
441

442

443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463

464
465

466
467
468
469
470
471
472


473
474
475

476
477

478
479
480
481
482
483


484
485
486
487
488
489
490
491
492
493


494
495
496
497
498
499
500
501
502
503
504
505
506

507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529























530
531
532
533
534
535
536
537
538
539
540









541
542
543
544
545
546
547
548
549
550
551
552
553


















554
555
556
557
558
559
560
561
562
563
564








565
566
567
568
569
570
571
572
573

574
575

576
577
578
579
580
581





582
583
584
585
586

587
588

589
590

591
592
593


594

595
596
597
598
599




600

601
602
603

604
605

606
607
608
609
610
611

612
613
614
615
616
617
618


619
620
621
622
623
624

625
626
627
628
629
630


631
632
633
634
635
636

637
638
639
640
641

642
643
644


645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664



665


666
667


668
669
670
671
672
673
674
675
676
677
678
679
680
681
682




683
684
685

686
687
688
689
690






691
692
693
694
695
696
697
698
699
700
9
10
11
12
13
14
15

16
17
18
19



























20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

50
51
52
53
54
55
56
57

58
59


60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
























169





170
171
172
173
174
175
























176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204








205
206
207
208
209
210
211
212
213

214
215

216
217




















218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239



240
241
242
243



244
245
246
247
248
249
250
251
252
253
254











255
256
257
258
259
260
261
262
263
264
265
266
267
268











269
270
271
272
273
274
275
276
277
278
279
280
281
282





283
284
285
286
287
288
289







290
291
292
293
294
295
296
297
298




299
300
301
302
303
304



305
306
307
308
309




310
311
312
313
314
315
316
317
318

319
320
321
322
323
324
325
326
327





328
329
330
331
332
333
334
335
336
337
338

339


340






341

342
343
344

345






346

347
348


349
350






351

352
353

354



355
356




357










358


359
360



361









362
363


364
365


366
367
368

369


370
371
372





373
374



375
376
377
378
379
380

381
382



383
384
385
386
387

388

389
390





391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409

410


411






412

413
414


415
416






417

418
419


420
421






422

423
424

425



426
427




428










429


430
431



432









433
434


435
436


437
438
439
440
441
442
443
444

445
446

447
448


















449

450


451







452
453
454
455

456


457






458
459

460
461
462
463


464


465
466
467
468
469
470
471
472
473
474
475

476
477

478
479






















480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505








506
507
508
509
510
511
512
513
514
515












516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536








537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555

556
557





558
559
560
561
562
563
564
565
566
567
568
569

570
571

572
573
574

575
576
577
578
579




580
581
582
583
584
585
586
587

588


589






590


591

592


593
594






595


596
597


598
599






600


601
602

603



604
605
















606
607


608
609
610
611
612
613


614
615
616



617










618
619
620
621



622





623
624
625
626
627
628
629
630

631
632
633
634
635
636
637







-
+



-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+



-
+







-
+

-
-
+
+
+
+
+
+
+
+
+

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+





-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-

-
-
-
-
-
+
+
+
+
+

-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+



-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+

-


-
+

-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+


-
-
-
+
+
+

-
-
-
+
+
+








-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+

-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+



-
-
-
-
-
+
+
+
+
+


-
-
-
-
-
-
-
+
+
+
+
+
+
+


-
-
-
-
+
+
+
+


-
-
-
+
+
+


-
-
-
-
+
+
+
+
+
+
+
+

-
+





+


-
-
-
-
-
+
+
+
+
+
+





-
+
-
-
+
-
-
-
-
-
-
+
-



-
+
-
-
-
-
-
-
+
-


-
-
+
+
-
-
-
-
-
-
+
-


-
+
-
-
-
+
+
-
-
-
-

-
-
-
-
-
-
-
-
-
-

-
-
+
+
-
-
-
+
-
-
-
-
-
-
-
-
-


-
-
+
+
-
-
+


-
+
-
-
+
+

-
-
-
-
-
+
+
-
-
-
+
+




-
+

-
-
-
+
+
+


-

-
+

-
-
-
-
-
+
+
+
+
+
+

+
+
+
+
+
+
+
+
+
+
+

-
+
-
-
+
-
-
-
-
-
-
+
-


-
-
+
+
-
-
-
-
-
-
+
-


-
-
+
+
-
-
-
-
-
-
+
-


-
+
-
-
-
+
+
-
-
-
-

-
-
-
-
-
-
-
-
-
-

-
-
+
+
-
-
-
+
-
-
-
-
-
-
-
-
-


-
-
+
+
-
-
+

+
+
+
+


-

+
-
+

-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-

-
+
-
-
+
-
-
-
-
-
-
-
+
+


-
+
-
-
+
-
-
-
-
-
-
+
+
-




-
-

-
-
+
+









-


-
+

-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+



-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+

-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+



-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+









+

-
+

-
-
-
-
-
+
+
+
+
+





+

-
+

-
+


-
+
+

+

-
-
-
-
+
+
+
+

+


-
+
-
-
+
-
-
-
-
-
-
+
-
-

-

-
-
+
+
-
-
-
-
-
-
+
-
-


-
-
+
+
-
-
-
-
-
-
+
-
-


-
+
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-


-
-
+
+
+

+
+
-
-
+
+

-
-
-

-
-
-
-
-
-
-
-
-
-
+
+
+
+
-
-
-
+
-
-
-
-
-
+
+
+
+
+
+


-







} else {
  set TCLLIBMOD [file join $MODDIR .. .. tcllib modules]
}
source [file join $TCLLIBMOD devtools testutilities.tcl]

testsNeedTcl     8.6 ;# tool requires 8.6
testsNeedTcltest 2

set NOW [clock seconds]
testsNeed TclOO 1

support {
  use [file join ${TCLLIBMOD} fumagic rtcore.tcl]       fileutil::magic::rt
  use [file join ${TCLLIBMOD} fumagic filetypes.tcl]    fileutil::magic::filetype
  use [file join ${TCLLIBMOD} textutil string.tcl]      textutil::string
  use [file join ${TCLLIBMOD} textutil repeat.tcl]      textutil::repeat
  use [file join ${TCLLIBMOD} textutil tabify.tcl]      textutil::tabify
  use [file join ${TCLLIBMOD} markdown markdown.tcl]    Markdown
  use [file join ${TCLLIBMOD} ncgi ncgi.tcl]            ncgi
  use [file join ${TCLLIBMOD} log logger.tcl]           logger
  use [file join ${TCLLIBMOD} base64 base64.tcl]        base64
  use [file join ${TCLLIBMOD} md5 md5x.tcl]             md5
  use [file join ${TCLLIBMOD} mime mime.tcl]            mime
  use [file join ${TCLLIBMOD} uuid uuid.tcl]            uuid
  use [file join ${TCLLIBMOD} cmdline cmdline.tcl]      cmdline
  use [file join ${TCLLIBMOD} fileutil fileutil.tcl]    fileutil
  use [file join ${TCLLIBMOD} sha1 sha1.tcl]            sha1
  use [file join ${TCLLIBMOD} uri uri.tcl]              uri
  use [file join ${TCLLIBMOD} ncgi ncgi.tcl]            ncgi
  use [file join ${TCLLIBMOD} dns ip.tcl]               ip
  use [file join ${TCLLIBMOD} nettool nettool.tcl]      nettool
  use [file join ${TCLLIBMOD} coroutine coroutine.tcl]  coroutine
  use [file join ${TCLLIBMOD} dicttool dicttool.tcl]    dicttool
  use [file join ${TCLLIBMOD} cron cron.tcl]            cron
  use [file join ${TCLLIBMOD} virtchannel_core core.tcl] tcl::chan::core
  use [file join ${TCLLIBMOD} virtchannel_core events.tcl] tcl::chan::events
  use [file join ${TCLLIBMOD} virtchannel_base memchan.tcl] tcl::chan::memchan
  use [file join ${TCLLIBMOD} websocket websocket.tcl]  websocket
  use [file join ${MODDIR} clay clay.tcl]               clay
    use [file join ${TCLLIBMOD} fumagic rtcore.tcl]       fileutil::magic::rt
    use [file join ${TCLLIBMOD} fumagic filetypes.tcl]    fileutil::magic::filetype
    use [file join ${TCLLIBMOD} textutil string.tcl]      textutil::string
    use [file join ${TCLLIBMOD} textutil repeat.tcl]      textutil::repeat
    use [file join ${TCLLIBMOD} textutil tabify.tcl]      textutil::tabify
    use [file join ${TCLLIBMOD} markdown markdown.tcl]    Markdown
    use [file join ${TCLLIBMOD} ncgi ncgi.tcl]            ncgi
    use [file join ${TCLLIBMOD} log logger.tcl]           logger
    use [file join ${TCLLIBMOD} base64 base64.tcl]        base64
    use [file join ${TCLLIBMOD} md5 md5x.tcl]             md5
    use [file join ${TCLLIBMOD} mime mime.tcl]            mime
    use [file join ${TCLLIBMOD} uuid uuid.tcl]            uuid
    use [file join ${TCLLIBMOD} cmdline cmdline.tcl]      cmdline
    use [file join ${TCLLIBMOD} fileutil fileutil.tcl]    fileutil
    use [file join ${TCLLIBMOD} sha1 sha1.tcl]            sha1
    use [file join ${TCLLIBMOD} uri uri.tcl]              uri
    use [file join ${TCLLIBMOD} ncgi ncgi.tcl]            ncgi
    use [file join ${TCLLIBMOD} dns ip.tcl]               ip
    use [file join ${TCLLIBMOD} nettool nettool.tcl]      nettool
    use [file join ${TCLLIBMOD} coroutine coroutine.tcl]  coroutine
    use [file join ${TCLLIBMOD} dicttool dicttool.tcl]    dicttool
    use [file join ${TCLLIBMOD} cron cron.tcl]            cron
    use [file join ${TCLLIBMOD} virtchannel_core core.tcl] tcl::chan::core
    use [file join ${TCLLIBMOD} virtchannel_core events.tcl] tcl::chan::events
    use [file join ${TCLLIBMOD} virtchannel_base memchan.tcl] tcl::chan::memchan
    use [file join ${TCLLIBMOD} websocket websocket.tcl]  websocket
    use [file join ${MODDIR} clay clay.tcl]               clay
}

testing {
  useLocal httpd.tcl httpd
    useLocal httpd.tcl httpd
}

# Set to true for debugging and traces
set ::DEBUG 0
set ::clay::debug $::DEBUG

proc DEBUG args {
  if {$::DEBUG} {
    if {!$::DEBUG} return
    uplevel 1 $args
  }
}
}

# -------------------------------------------------------------------------
# Constructors for various expected replies.
proc IndexReply {{head {HTTP/1.0}}} {
   global TESTDIR
   set fin       [open [file join $TESTDIR pkgIndex.tcl] r]
   set replyfile [read $fin]
   close $fin

   append checkreply "$head 200 OK" \n
   append checkreply "Content-Type: text/plain" \n
   append checkreply "Connection: close" \n
   append checkreply "Content-Length: [string length $replyfile]" \n
   append checkreply \n
   append checkreply $replyfile

   return $checkreply
}

proc 404 {} {
    lappend map "        " ""
    lappend map "    "     ""
    # The map removes the indentation of the value
    return [string map $map {HTTP/1.0 404 Not Found
        Content-Type: text/plain
        Connection: close
        Content-Length: *

        404 Not Found
    }]
}

proc 200 {text {len *}} {
    lappend map "        " ""
    # The map removes the indentation of the value
    # and inserts the dynamic parts
    lappend map @C $text
    lappend map @L $len
    return [string map $map {HTTP/1.0 200 OK
        Content-Type: text/plain
        Connection: close
        Content-Length: @L

        @C}]
}

proc 200+status-head {text {len *}} {
    lappend map "        " ""
    # The map removes the indentation of the value
    # and inserts the dynamic parts
    lappend map @C $text
    lappend map @L $len
    return [string map $map {Status: 200 OK
        Content-Type: text/plain
        Connection: close
        Content-Length: @L

        @C}]
}

proc 200+status-conn {text {len *}} {
    lappend map "        " ""
    # The map removes the indentation of the value
    # and inserts the dynamic parts
    lappend map @C $text
    lappend map @L $len
    return [string map $map {HTTP/1.0 200 OK
        Status: 200 OK
        Content-Type: text/plain
        Content-Length: @L

        @C}]
}

proc 500 {} {
    lappend map "        " ""
    lappend map "    "     ""
    # The map removes the indentation of the value
    return [string map $map {HTTP/1.0 500 Server Internal Error
        Content-Type: text/plain
        Connection: close
        Content-Length: *

        500 Server Internal Error
    }]
}

proc 500+status-head {} {
    lappend map "        " ""
    lappend map "    "     ""
    # The map removes the indentation of the value
    return [string map $map {Status: 500 Server Internal Error
        Content-Type: text/plain
        Connection: close
        Content-Length: *

        500 Server Internal Error
    }]
}

# Likely a band aid, see AKU
proc norm-eol {x} { string map [list "\r\n" "\n"] $x }

# -------------------------------------------------------------------------

namespace eval ::httpd {}
namespace eval ::httpd::test {}

###
# Minimal test harness for the .tests
# Not intended for public consumption
# (But if you find it handy, please steal!)
proc ::httpd::test::compare {actual correct} {
	set result {}
	set cbuf [split $correct \n]
	set abuf [split $actual \n]
	for {set i 0} {$i < [llength $cbuf]} {incr i} {
		set cline [string trim [lindex $cbuf $i]]
		set aline [string trim [lindex $abuf $i]]
		if {![string match $cline $aline]} {
			if {$cline ne $aline} {
				append result "Line $i differs [list $aline] != [list $cline]" \n
			}
		}
	}
  if {[llength $result]} {
  	puts [list ACTUAL $actual]
	  puts [list CORRECT $correct]
  }
	return $result
}

proc ::httpd::test::send {port http headers body} {
  set sock [socket localhost $port]
  variable reply
  set reply($sock) {}
  chan configure $sock -translation {crlf crlf} -blocking 0 -buffering full -buffersize 4096
  chan event $sock readable [list ::httpd::test::get_reply $sock]
    set sock [socket localhost $port]
    variable reply
    set reply($sock) {}
    chan configure $sock -translation {crlf crlf} -blocking 0 -buffering full -buffersize 4096
    chan event $sock readable [list ::httpd::test::get_reply $sock]

	puts $sock $http
	if {![dict exists $headers Host]} {
	  dict set headers Host localhost
	}
	if {[string length $body]} {
		if {![dict exists $headers Content-Type]} {
			dict set headers Content_Type text/plain
		}
		dict set headers Content-Length [string length $body]
	}
	foreach {f v} $headers {
		puts $sock "${f}: $v"
	}
  puts $sock {}
	if {[string length $body]} {
    chan configure $sock -translation binary -blocking 0 -buffering full -buffersize 4096
		puts -nonewline $sock $body
	}
  flush $sock
  while {$reply($sock) eq {}} {
    update
  }
  #vwait [namespace current]::reply($sock)
  return $reply($sock)
    puts $sock $http
    if {![dict exists $headers Host]} {
        dict set headers Host localhost
    }
    if {[string length $body]} {
        if {![dict exists $headers Content-Type]} {
            dict set headers Content_Type text/plain
        }
        dict set headers Content-Length [string length $body]
    }
    foreach {f v} $headers {
        puts $sock "${f}: $v"
    }
    puts $sock {}
    if {[string length $body]} {
        chan configure $sock -translation binary -blocking 0 -buffering full -buffersize 4096
        puts -nonewline $sock $body
    }
    flush $sock
    while {$reply($sock) eq {}} {
        update
    }
    #vwait [namespace current]::reply($sock)

    #puts ZZ\t[join [split $reply($sock) \n] "|\nZZ\t"]|
    return $reply($sock)
}

proc ::httpd::test::get_reply {sock} {
  variable buffer
  set data [read $sock]
  append buffer($sock) $data
  if {[eof $sock]} {
    chan event $sock readable {}
    set [namespace current]::reply($sock) $buffer($sock)
    unset buffer($sock)
  }
    variable buffer
    set data [read $sock]
    append buffer($sock) $data
    if {[eof $sock]} {
        chan event $sock readable {}
        set [namespace current]::reply($sock) $buffer($sock)
        unset buffer($sock)
    }
}


clay::define ::httpd::server {
  method log args {}
    method log args {}

  method TemplateSearch page {
    set doc_root [my clay get server/ doc_root]
    if {$doc_root ne {} && [file exists [file join $doc_root $page.tml]]} {
      return [::fileutil::cat [file join $doc_root $page.tml]]
    }
    if {$doc_root ne {} && [file exists [file join $doc_root $page.html]]} {
      return [::fileutil::cat [file join $doc_root $page.html]]
    }
    switch $page {
      redirect {
        return {300 Redirect}
      }
      notfound {
        return {404 Not Found}
      }
      internal_error {
        return {500 Server Internal Error}
      }
    }
  }
    method TemplateSearch page {
        set doc_root [my clay get server/ doc_root]
        if {$doc_root ne {} && [file exists [file join $doc_root $page.tml]]} {
            return [::fileutil::cat [file join $doc_root $page.tml]]
        }
        if {$doc_root ne {} && [file exists [file join $doc_root $page.html]]} {
            return [::fileutil::cat [file join $doc_root $page.html]]
        }
        switch $page {
            redirect {
                return {300 Redirect}
            }
            notfound {
                return {404 Not Found}
            }
            internal_error {
                return {500 Server Internal Error}
            }
        }
    }


  ::DEBUG method debug args {
    puts stderr $args
  }
    ::DEBUG method debug args {
        puts stderr $args
    }

  ::DEBUG method log args {
    puts stdout $args
  }
    ::DEBUG method log args {
        puts stdout $args
    }
}


###
# Modify the reply class to return plain text
###
clay::define ::httpd::reply {

  method HttpHeaders_Default {} {
    return {Status {200 OK}
    Content-Type {text/plain}
		Connection close}
  }

  method reset {} {
    my variable reply_body
    my reply replace [my HttpHeaders_Default]
    set reply_body {}
  }
    method HttpHeaders_Default {} {
        return {
            Status       {200 OK}
            Content-Type {text/plain}
            Connection   close
        }
    }

    method reset {} {
        my variable reply_body
        my reply replace [my HttpHeaders_Default]
        set reply_body {}
    }

  method error {code {msg {}} {errorInfo {}}} {
    my clay set HTTP_ERROR $code
    my reset
    set errorstring [my http_code_string $code]
    set qheaders [my clay dump]
    dict with qheaders {}
    my reply replace {}
    my reply set Status "$code $errorstring"
    my reply set Content-Type text/plain
    my puts "$code $errorstring"
  }
    method error {code {msg {}} {errorInfo {}}} {
        my clay set HTTP_ERROR $code
        my reset
        set errorstring [my http_code_string $code]
        set qheaders [my clay dump]
        dict with qheaders {}
        my reply replace {}
        my reply set Status "$code $errorstring"
        my reply set Content-Type text/plain
        my puts "$code $errorstring"
    }
}

clay::define ::test::content.echo {
	method content {} {
		my variable reply_body
		set reply_body [my PostData [my request get CONTENT_LENGTH]]
		#puts [list REPLY BODY WAS $reply_body]
	}
    method content {} {
        my variable reply_body
        set reply_body [my PostData [my request get CONTENT_LENGTH]]
        #puts [list REPLY BODY WAS $reply_body]
    }
}
clay::define ::test::content.file {
	superclass ::httpd::content.file
	method content {} {
	  my reset
    set doc_root [my request get DOCUMENT_ROOT]
    my variable reply_file
    set reply_file [file join $doc_root pkgIndex.tcl]
	}
    superclass ::httpd::content.file
    method content {} {
        my reset
        set doc_root  [my clay get path]
        my variable reply_file
        set reply_file [file join $doc_root pkgIndex.tcl]
    }
}
clay::define ::test::content.time {
	method content {} {
		my variable reply_body
		set reply_body [clock seconds]
	}
    method content {} {
        my variable reply_body
        set reply_body $::NOW
    }
}
clay::define ::test::content.error {
	method content {} {
		error {The programmer asked me to die this way}
	}
    method content {} {
        error {The programmer asked me to die this way}
    }
}
clay::define ::test::content.cgi {
	superclass ::httpd::content.cgi

}

    superclass ::httpd::content.cgi
}
clay::define ::test::content.string {
    method content {} {
        my variable reply_body
        set reply_body [my clay get hardcoded_string]
    }
}
clay::define ::httpd::test::reply {
	superclass ::httpd::reply ::test::content.echo
    superclass ::httpd::reply ::test::content.echo
}

###
# Build the server
###

::httpd::server create TESTAPP port 10001 doc_root $::TESTDIR
TESTAPP plugin dict_dispatch
TESTAPP uri add * /     [list mixin {reply ::test::content.echo}]
TESTAPP uri add * /echo [list mixin {reply ::test::content.echo}]
TESTAPP uri add * /file [list mixin {reply ::test::content.file}]
TESTAPP uri add * /time [list mixin {reply ::test::content.time}]
TESTAPP uri add * /error [list mixin {replyy ::test::content.error}]
TESTAPP uri add * /      [list mixin {reply ::test::content.echo}]
TESTAPP uri add * /echo  [list mixin {reply ::test::content.echo}]
TESTAPP uri add * /file  [list mixin {reply ::test::content.file} path $::TESTDIR]
TESTAPP uri add * /time  [list mixin {reply ::test::content.time}]
TESTAPP uri add * /error [list mixin {reply ::test::content.error}]
TESTAPP uri add * /string  [list mixin {reply ::test::content.string} hardcoded_string apple]

# Catch all
#TESTAPP uri add * * [list mixin {reply httpd::content.echo}]

::DEBUG puts httpd-client-0001
test httpd-client-0001 {Do an echo request} {
test httpd-client-0001 {Do an echo request} -body {

set reply [::httpd::test::send 10001 {POST /echo HTTP/1.0} {} {THIS IS MY CODE}]
    ::httpd::test::send 10001 {POST /echo HTTP/1.0} {} {THIS IS MY CODE}
::httpd::test::compare $reply {HTTP/1.0 200 OK
Content-Type: text/plain
Connection: close
Content-Length: *

THIS IS MY CODE}
} -match glob -result [200 {THIS IS MY CODE}]
} {}

::DEBUG puts httpd-client-0002
test httpd-client-0002 {Do another echo request} {
set reply [::httpd::test::send 10001 {POST /echo HTTP/1.0} {} {THOUGH THERE ARE MANY LIKE IT}]
    ::httpd::test::send 10001 {POST /echo HTTP/1.0} {} {THOUGH THERE ARE MANY LIKE IT}
::httpd::test::compare $reply {HTTP/1.0 200 OK
Content-Type: text/plain
Connection: close
Content-Length: 29

THOUGH THERE ARE MANY LIKE IT}
} [200 {THOUGH THERE ARE MANY LIKE IT} 29]
} {}

::DEBUG puts httpd-client-0003
test httpd-client-0003 {Do another echo request} {
set reply [::httpd::test::send 10001 {POST /echo HTTP/1.0} {} {THIS ONE ALONE IS MINE}]
test httpd-client-0003 {Do another echo request} -body {
    ::httpd::test::send 10001 {POST /echo HTTP/1.0} {} {THIS ONE ALONE IS MINE}
::httpd::test::compare $reply {HTTP/1.0 200 OK
Content-Type: text/plain
Connection: close
Content-Length: *

THIS ONE ALONE IS MINE}
} -match glob -result [200 {THIS ONE ALONE IS MINE}]
}  {}

::DEBUG puts httpd-client-0004
test httpd-client-0004 {URL Generates Error} {
test httpd-client-0004 {URL Generates Error} -body {

set reply [::httpd::test::send 10001 {POST /error HTTP/1.0} {} {THIS ONE ALONE IS MINE}]

    ::httpd::test::send 10001 {POST /error HTTP/1.0} {} {THIS ONE ALONE IS MINE}
} -match glob -result [500]
::httpd::test::compare $reply {HTTP/1.0 500 Server Internal Error
Content-Type: text/plain
Connection: close
Content-Length: *

500 Server Internal Error}
} {}

set checkreply [subst {HTTP/1.0 200 OK
Content-Type: text/plain
Connection: close
Content-Length: *

[clock seconds]}]

::DEBUG puts httpd-client-0005
test httpd-client-0005 {URL Different output with a different request} {
set reply [::httpd::test::send 10001 {POST /time HTTP/1.0} {} {THIS ONE ALONE IS MINE}]
test httpd-client-0005 {URL Different output with a different request} -body {
    ::httpd::test::send 10001 {POST /time HTTP/1.0} {} {THIS ONE ALONE IS MINE}
::httpd::test::compare $reply $checkreply
} {}

} -match glob -result [200 $::NOW]
set fin [open [file join $TESTDIR pkgIndex.tcl] r]
set replyfile [read $fin]
close $fin
set checkreply "HTTP/1.0 200 OK
Content-Type: text/plain
Connection: close
Content-Length: [string length $replyfile]

$replyfile"

::DEBUG puts httpd-client-0006
test httpd-client-0006 {Return a file} {
set reply [::httpd::test::send 10001 {GET /file HTTP/1.0} {} {}]
test httpd-client-0006 {Return a file} -body {
    ::httpd::test::send 10001 {GET /file HTTP/1.0} {} {}
::httpd::test::compare $reply $checkreply
} {}
} -result [IndexReply]

::DEBUG puts httpd-client-0007
test httpd-client-0007 {URL Generates Not Found} {
test httpd-client-0007 {URL Generates Not Found} -body {

set reply [::httpd::test::send 10001 {POST /doesnotexist HTTP/1.0} {} {THIS ONE ALONE IS MINE}]
    ::httpd::test::send 10001 {POST /doesnotexist HTTP/1.0} {} {THIS ONE ALONE IS MINE}
} -match glob -result [404]

::httpd::test::compare $reply {HTTP/1.0 404 Not Found
Content-Type: text/plain
Connection: close
Content-Length: *

::DEBUG puts httpd-client-0008
test httpd-client-0008 {Pull a constant string} -body {
404 Not Found}
} {}

    ::httpd::test::send 10001 {GET /string HTTP/1.0} {} {}
} -match glob -result [200 apple]
# -------------------------------------------------------------------------
# Test proxies

clay::define ::test::content.proxy {
	superclass ::httpd::content.proxy
    superclass ::httpd::content.proxy

  method proxy_channel {} {
    return [::socket localhost [my clay get proxy_port]]
  }
    method proxy_channel {} {
        return [::socket localhost [my clay get proxy_port]]
    }
}


::httpd::server create TESTPROXY port 10002 doc_root $::TESTDIR
TESTAPP   uri add * /proxy*     [list mixin {reply ::test::content.proxy} proxy_port [TESTPROXY port_listening]]
TESTAPP   uri add * /proxy* [list mixin {reply ::test::content.proxy} proxy_port [TESTPROXY port_listening]]
TESTPROXY plugin dict_dispatch
TESTPROXY uri add * /     [list mixin {reply ::test::content.echo}]
TESTPROXY uri add * /echo [list mixin {reply ::test::content.echo}]
TESTPROXY uri add * /file [list mixin {reply ::test::content.file}]
TESTPROXY uri add * /time [list mixin {reply ::test::content.time}]
TESTPROXY uri add * /error [list mixin {reply ::test::content.error}]
TESTPROXY uri add * /       [list mixin {reply ::test::content.echo}]
TESTPROXY uri add * /echo   [list mixin {reply ::test::content.echo}]
TESTPROXY uri add * /file   [list mixin {reply ::test::content.file} path $::TESTDIR]
TESTPROXY uri add * /time   [list mixin {reply ::test::content.time}]
TESTPROXY uri add * /error  [list mixin {reply ::test::content.error}]
TESTPROXY uri add * /string   [list mixin {reply ::test::content.string} hardcoded_string banana]

## AKU ##
#
# Note: Proxy replies are not normalized to \n. They contain \r\n
# endings.  The old test::compare was ok with that due to running a
# trim on the lines it was comparing. Here we properly normalize
# before feeding into the comparison.
#
# Note 2: I suspect that this leakage / non-normalization of of \r\n
#         in the server is a bug which should be fixed. If so, norm-eol
#         becomes superfluous. Right now it feels like a band-aid

::DEBUG puts httpd-proxy-0001
test httpd-proxy-0001 {Do an echo request} {
test httpd-proxy-0001 {Do an echo request} -body {

set reply [::httpd::test::send 10001 {POST /proxy/echo HTTP/1.0} {} {THIS IS MY CODE}]
    norm-eol [::httpd::test::send 10001 {POST /proxy/echo HTTP/1.0} {} {THIS IS MY CODE}]
::httpd::test::compare $reply {HTTP/1.0 200 OK
Content-Type: text/plain
Connection: close
Content-Length: *

THIS IS MY CODE}
} -match glob -result [200 {THIS IS MY CODE}]
} {}

::DEBUG puts httpd-proxy-0002
test httpd-proxy-0002 {Do another echo request} {
set reply [::httpd::test::send 10001 {POST /proxy/echo HTTP/1.0} {} {THOUGH THERE ARE MANY LIKE IT}]
test httpd-proxy-0002 {Do another echo request} -body {
    norm-eol [::httpd::test::send 10001 {POST /proxy/echo HTTP/1.0} {} {THOUGH THERE ARE MANY LIKE IT}]
::httpd::test::compare $reply {HTTP/1.0 200 OK
Content-Type: text/plain
Connection: close
Content-Length: 29

THOUGH THERE ARE MANY LIKE IT}
} -result [200 {THOUGH THERE ARE MANY LIKE IT} 29]
} {}

::DEBUG puts httpd-proxy-0003
test httpd-proxy-0003 {Do another echo request} {
set reply [::httpd::test::send 10001 {POST /proxy/echo HTTP/1.0} {} {THIS ONE ALONE IS MINE}]
test httpd-proxy-0003 {Do another echo request} -body {
    norm-eol [::httpd::test::send 10001 {POST /proxy/echo HTTP/1.0} {} {THIS ONE ALONE IS MINE}]
::httpd::test::compare $reply {HTTP/1.0 200 OK
Content-Type: text/plain
Connection: close
Content-Length: *

THIS ONE ALONE IS MINE}
} -match glob -result [200 {THIS ONE ALONE IS MINE}]
}  {}

::DEBUG puts httpd-proxy-0004
test httpd-proxy-0004 {URL Generates Error} {
test httpd-proxy-0004 {URL Generates Error} -body {

set reply [::httpd::test::send 10001 {POST /proxy/error HTTP/1.0} {} {THIS ONE ALONE IS MINE}]

    norm-eol [::httpd::test::send 10001 {POST /proxy/error HTTP/1.0} {} {THIS ONE ALONE IS MINE}]
} -match glob -result [500]
::httpd::test::compare $reply {HTTP/1.0 500 Server Internal Error
Content-Type: text/plain
Connection: close
Content-Length: *

500 Server Internal Error}
} {}

set checkreply [subst {HTTP/1.0 200 OK
Content-Type: text/plain
Connection: close
Content-Length: *

[clock seconds]}]

::DEBUG puts httpd-proxy-0005
test httpd-proxy-0005 {URL Different output with a different request} {
set reply [::httpd::test::send 10001 {POST /proxy/time HTTP/1.0} {} {THIS ONE ALONE IS MINE}]
test httpd-proxy-0005 {URL Different output with a different request} -body {
    norm-eol [::httpd::test::send 10001 {POST /proxy/time HTTP/1.0} {} {THIS ONE ALONE IS MINE}]
::httpd::test::compare $reply $checkreply
} {}

} -match glob -result [200 $::NOW]
set fin [open [file join $TESTDIR pkgIndex.tcl] r]
set replyfile [read $fin]
close $fin
set checkreply "HTTP/1.0 200 OK
Content-Type: text/plain
Connection: close
Content-Length: [string length $replyfile]

$replyfile"

::DEBUG puts httpd-proxy-0006
test httpd-proxy-0006 {Return a file} {
set reply [::httpd::test::send 10001 {GET /proxy/file HTTP/1.0} {} {}]
test httpd-proxy-0006 {Return a file} -body {
    norm-eol [::httpd::test::send 10001 {GET /proxy/file HTTP/1.0} {} {}]
::httpd::test::compare $reply $checkreply
} {}
} -result [IndexReply]

::DEBUG puts httpd-proxy-0008
test httpd-proxy-0008 {Pull a constant string} -body {
    norm-eol [::httpd::test::send 10001 {GET /proxy/string HTTP/1.0} {} {}]
} -result [200 banana 6]
# -------------------------------------------------------------------------
# cgi
TESTAPP plugin local_memchan

TESTAPP plugin local_memchan
TESTAPP uri add * /cgi-bin* [list mixin {reply ::test::content.cgi} path $::TESTDIR]
TESTAPP uri add * /cgi-bin* [list mixin {reply ::test::content.cgi} path $::TESTDIR/assets]

set fout [open [file join $TESTDIR test.tcl] w]
puts $fout {#!/usr/bin/tclsh

puts stdout "Status: 200 OK"
if {$::env(CONTENT_LENGTH) > 0} {
  puts stdout "Content-Type: $::env(CONTENT_TYPE)"
  set dat [read stdin $::env(CONTENT_LENGTH)]
} else {
  puts stdout "Content-Type: text/plain"
  set dat "Hi!"
}
puts stdout "Content-Length: [string length $dat]"
puts stdout {}
puts stdout $dat
exit 0
}
close $fout

::DEBUG puts httpd-cgi-0001
test httpd-cgi-0001 {CGI Post} {
test httpd-cgi-0001 {CGI Post} -body {

set reply [::httpd::test::send 10001 {POST /cgi-bin/test.tcl HTTP/1.0} {} {THIS IS MY CODE}]
    norm-eol [::httpd::test::send 10001 {POST /cgi-bin/test_cgi.tcl HTTP/1.0} {} {THIS IS MY CODE}]
::httpd::test::compare $reply {HTTP/1.0 200 OK
Status: 200 OK
Content-Type: text/plain
Content-Length: *

THIS IS MY CODE}
} {}
} -match glob -result [200+status-conn {THIS IS MY CODE
}]

::DEBUG puts httpd-cgi-0002
test httpd-cgi-0002 {CGI Get} {
test httpd-cgi-0002 {CGI Get} -body {

set reply [::httpd::test::send 10001 {GET /cgi-bin/test.tcl HTTP/1.0} {} {}]
    ::httpd::test::send 10001 {GET /cgi-bin/test_cgi.tcl HTTP/1.0} {} {}
::httpd::test::compare $reply {HTTP/1.0 200 OK
Status: 200 OK
Content-Type: text/plain
Content-Length: *

Hi!}
} -match glob -result [200+status-conn {Hi!
}]
} {}

###
# Test the local geturl method
###
set now [clock seconds]
set dat [TESTAPP local_memchan geturl /time]
test httpd-memchan-0001 {Memchan GET} {
  TESTAPP local_memchan geturl /time
} $now
    TESTAPP local_memchan geturl /time
} $NOW

# -------------------------------------------------------------------------
namespace eval ::scgi {}
namespace eval ::scgi::test {}

###
# Minimal test harness for the .tests
# Not intended for public consumption
# (But if you find it handy, please steal!)
namespace eval ::scgi::test {}

proc ::scgi::encode_request {headers body info} {
  variable server_block
    variable server_block

  dict set outdict CONTENT_LENGTH [string length $body]
  set outdict [dict merge $outdict $server_block $info]
  dict set outdict PWD [pwd]
  foreach {key value} $headers {
    if {$key in {
      DOCUMENT_ROOT
      HTTPS
      PATH
      REQUEST_METHOD REQUEST_URI
      REMOTE_HOST REMOTE_ADDR REMOTE_PORT
      SCRIPT_NAME
    } || [string range $key 0 5] eq "HTTP_"} {
      dict set outdict $key $value
    } else {
      dict set outdict HTTP_[string map {"-" "_"} [string toupper $key]] $value
    }
  }
  set result {}
  foreach {name value} $outdict {
    append result $name \x00 $value \x00
  }
  return "[string length $result]:$result,"
    dict set outdict CONTENT_LENGTH [string length $body]
    set outdict [dict merge $outdict $server_block $info]
    dict set outdict PWD [pwd]

    foreach {key value} $headers {
        if {$key in {
            DOCUMENT_ROOT
            HTTPS
            PATH
            REQUEST_METHOD REQUEST_URI
            REMOTE_HOST REMOTE_ADDR REMOTE_PORT
            SCRIPT_NAME
        } || [string range $key 0 5] eq "HTTP_"} {
            dict set outdict $key $value
        } else {
            dict set outdict HTTP_[string map {"-" "_"} [string toupper $key]] $value
        }
    }
    set result {}
    foreach {name value} $outdict {
        append result $name \x00 $value \x00
    }
    return "[string length $result]:$result,"
}

proc ::scgi::test::send {port headers body} {
  set sock [socket localhost $port]
  variable reply
  set reply($sock) {}
  if {![dict exists $headers HOST]} {
    dict set headers HOST localhost
  }
  dict set headers REMOTE_IP 127.0.0.1
  dict set headers REMOTE_HOST localhost
    set sock [socket localhost $port]
    variable reply
    set reply($sock) {}

    if {![dict exists $headers HOST]} {
        dict set headers HOST localhost
    }
    dict set headers REMOTE_IP   127.0.0.1
    dict set headers REMOTE_HOST localhost

  chan configure $sock -translation binary -blocking 0 -buffering full -buffersize 4096
  chan event $sock readable [list ::scgi::test::get_reply $sock]
  set block [::scgi::encode_request $headers $body {}]
  puts -nonewline $sock $block
  flush $sock
  puts -nonewline $sock $body
  flush $sock
  while {$reply($sock) eq {}} {
    update
  }
  #vwait [namespace current]::reply($sock)
  return $reply($sock)
    chan configure $sock -translation binary -blocking 0 -buffering full -buffersize 4096
    chan event     $sock readable [list ::scgi::test::get_reply $sock]

    set block [::scgi::encode_request $headers $body {}]

    puts -nonewline $sock $block
    flush $sock
    puts -nonewline $sock $body
    flush $sock

    while {$reply($sock) eq {}} {
        update
    }

    #vwait [namespace current]::reply($sock)

    #puts ZZ\t[join [split $reply($sock) \n] "|\nZZ\t"]|
    return $reply($sock)
}

proc ::scgi::test::get_reply {sock} {
  variable buffer
  set data [read $sock]
  append buffer($sock) $data
  if {[eof $sock]} {
    chan event $sock readable {}
    set [namespace current]::reply($sock) $buffer($sock)
    unset buffer($sock)
  }
    variable buffer
    set data [read $sock]
    append buffer($sock) $data
    if {[eof $sock]} {
        chan event $sock readable {}
        set [namespace current]::reply($sock) $buffer($sock)
        unset buffer($sock)
    }
}

namespace eval ::scgi {
  variable server_block {SCGI 1.0 SERVER_SOFTWARE {TclScgiServer/0.1}}
}

###
# Build the reply class
###

::clay::define ::scgi::test::reply {
  superclass ::httpd::reply
    superclass ::httpd::reply

  method reset {} {
    my variable reply_body
    my reply replace [my HttpHeaders_Default]
    set reply_body {}
  }
    method reset {} {
        my variable reply_body
        my reply replace [my HttpHeaders_Default]
        set reply_body {}
    }
}

###
# Build the server
###

::clay::define scgi::test::app {
  superclass ::httpd::server.scgi
    superclass ::httpd::server.scgi

  clay set reply_class ::scgi::test::reply
    clay set reply_class ::scgi::test::reply
}

puts [list ::test::content.file [info commands ::test::content.file]]
::DEBUG puts [list ::test::content.file [info commands ::test::content.file]]

scgi::test::app create TESTSCGI port 10003 doc_root $::TESTDIR

TESTSCGI plugin dict_dispatch
TESTSCGI uri add * /     [list mixin {reply ::test::content.echo}]
TESTSCGI uri add * /echo [list mixin {reply ::test::content.echo}]
TESTSCGI uri add * /file [list mixin {reply ::test::content.file}]
TESTSCGI uri add * /time [list mixin {reply ::test::content.time}]
TESTSCGI uri add * /      [list mixin {reply ::test::content.echo}]
TESTSCGI uri add * /echo  [list mixin {reply ::test::content.echo}]
TESTSCGI uri add * /file  [list mixin {reply ::test::content.file} path $::TESTDIR]
TESTSCGI uri add * /time  [list mixin {reply ::test::content.time}]
TESTSCGI uri add * /error [list mixin {reply ::test::content.error}]
TESTSCGI uri add * /string  [list mixin {reply ::test::content.string} hardcoded_string cherry]

::DEBUG puts scgi-client-0001
test scgi-client-0001 {Do an echo request} {
test scgi-client-0001 {Do an echo request} -body {

set reply [::scgi::test::send 10003 {REQUEST_METHOD POST REQUEST_URI /echo} {THIS IS MY CODE}]
    ::scgi::test::send 10003 {REQUEST_METHOD POST REQUEST_URI /echo} {THIS IS MY CODE}
set checkreply {Status: 200 OK
Content-Type: text/plain
Connection: close
Content-Length: *

THIS IS MY CODE}
} -match glob -result [200+status-head {THIS IS MY CODE}]
::httpd::test::compare $reply $checkreply
} {}


::DEBUG puts scgi-client-0002
test scgi-client-0002 {Do another echo request} {
set reply [::scgi::test::send 10003 {REQUEST_METHOD POST REQUEST_URI /echo} {THOUGH THERE ARE MANY LIKE IT}]
test scgi-client-0002 {Do another echo request} -body {
    ::scgi::test::send 10003 {REQUEST_METHOD POST REQUEST_URI /echo} {THOUGH THERE ARE MANY LIKE IT}
set checkreply {Status: 200 OK
Content-Type: text/plain
Connection: close
Content-Length: *

THOUGH THERE ARE MANY LIKE IT}
} -match glob -result [200+status-head {THOUGH THERE ARE MANY LIKE IT}]
::httpd::test::compare $reply $checkreply
} {}

::DEBUG puts scgi-client-0003
test scgi-client-0003 {Do another echo request} {
set reply [::scgi::test::send 10003 {REQUEST_METHOD POST REQUEST_URI /echo} {THIS ONE ALONE IS MINE}]
test scgi-client-0003 {Do another echo request} -body {
    ::scgi::test::send 10003 {REQUEST_METHOD POST REQUEST_URI /echo} {THIS ONE ALONE IS MINE}
set checkreply {Status: 200 OK
Content-Type: text/plain
Connection: close
Content-Length: *

THIS ONE ALONE IS MINE}
} -match glob -result [200+status-head {THIS ONE ALONE IS MINE}]
::httpd::test::compare $reply $checkreply
} {}

::DEBUG puts scgi-client-0004
test scgi-client-0004 {URL Generates Error} {
test scgi-client-0004 {URL Generates Error} -body {

set reply [::scgi::test::send 10003 {REQUEST_METHOD POST REQUEST_URI /error} {THIS ONE ALONE IS MINE}]

    ::scgi::test::send 10003 {REQUEST_METHOD POST REQUEST_URI /error} {THIS ONE ALONE IS MINE}
} -match glob -result [500+status-head]
set checkreply {Status: 500 Server Internal Error
Content-Type: text/plain
Connection: close
Content-Length: *

500 Server Internal Error
}
::httpd::test::compare $reply $checkreply
} {}

set checkreply [subst {Status: 200 OK
Content-Type: text/plain
Connection: close
Content-Length: *

[clock seconds]}]

::DEBUG puts scgi-client-0005
test scgi-client-0005 {URL Different output with a different request} {
set reply [::scgi::test::send 10003 {REQUEST_METHOD POST REQUEST_URI /time} {THIS ONE ALONE IS MINE}]
test scgi-client-0005 {URL Different output with a different request} -body {
    ::scgi::test::send 10003 {REQUEST_METHOD POST REQUEST_URI /time} {THIS ONE ALONE IS MINE}
} -match glob -result [200+status-head $::NOW]

::DEBUG puts scgi-client-0006
test scgi-client-0006 {Return a file} -body {
::httpd::test::compare $reply $checkreply
} {}
    ::scgi::test::send 10003 {REQUEST_METHOD GET REQUEST_URI /file} {}
} -result [IndexReply Status:]

set fin [open [file join $TESTDIR pkgIndex.tcl] r]
set checkfile [read $fin]
close $fin

###
# Nerfed: There is something screwy that is preventing this test from working
# properly in Sak. But only this test, and not the other two (normal client and proxy)
# who are doing essentially the same operation
# Investigate at some point - Sean
###
#::DEBUG puts scgi-client-0006
#test scgi-client-0006 {Return a file} {
#set reply [::scgi::test::send 10003 {REQUEST_METHOD GET REQUEST_URI /file} {}]

::DEBUG puts scgi-client-0008
test scgi-client-0008 {Pull a constant string} -body {
    ::scgi::test::send 10003 {REQUEST_METHOD GET REQUEST_URI /string} {}
} -match glob -result [200+status-head cherry]
#set checkreply "Status: 200 OK
#Content-Type: text/plain
#Connection: close
###
#Content-Length: [string length $checkfile]

#$checkfile"
#::httpd::test::compare $reply $checkreply
#} {}
# Test the all object have been destroyed after ::clay::cleanup
###
test httpd-garbage-collection {Test that garbage collection leaves nothing behind} -body {
  ::clay::cleanup
  info commands ::httpd::object::*
} -result {}

::DEBUG puts all-tests-finished
file delete [file join $TESTDIR test.tcl]
# -------------------------------------------------------------------------

testsuiteCleanup

# Local variables:
# mode: tcl
# indent-tabs-mode: nil

Changes to modules/practcl/practcl.tcl.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
1
2
3
4
5
6
7
8
9
10
11
12








































13
14
15
16
17
18
19












-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-







###
# Amalgamated package for practcl
# Do not edit directly, tweak the source in src/ and rerun
# build.tcl
###
package require Tcl 8.6
package provide practcl 0.16.3
namespace eval ::practcl {}

###
# START: httpwget/wget.tcl
###
package provide http::wget 0.1
package require http
::namespace eval ::http {
}
proc ::http::_followRedirects {url args} {
    while 1 {
        set token [geturl $url -validate 1]
        set ncode [ncode $token]
        if { $ncode eq "404" } {
          error "URL Not found"
        }
        switch -glob $ncode {
            30[1237] {### redirect - see below ###}
            default  {cleanup $token ; return $url}
        }
        upvar #0 $token state
        array set meta [set ${token}(meta)]
        cleanup $token
        if {![info exists meta(Location)]} {
           return $url
        }
        set url $meta(Location)
        unset meta
    }
    return $url
}
proc ::http::wget {url destfile {verbose 1}} {
    set tmpchan [open $destfile w]
    fconfigure $tmpchan -translation binary
    if { $verbose } {
        puts [list  GETTING [file tail $destfile] from $url]
    }
    set real_url [_followRedirects $url]
    set token [geturl $real_url -channel $tmpchan -binary yes]
    if {[ncode $token] != "200"} {
      error "DOWNLOAD FAILED"
    }
    cleanup $token
    close $tmpchan
}

###
# END: httpwget/wget.tcl
###
###
# START: clay/clay.tcl
###
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
966
967
968
969
970
971
972

973
974
975
976
977
978
979







-







proc ::clay::define::destructor rawbody {
  set body {
# Run the destructor once and only once
set self [self]
my variable DestroyEvent
if {$DestroyEvent} return
set DestroyEvent 1
::clay::object_destroy $self
}
  append body $rawbody
  ::oo::define [current_class] destructor $body
}
proc ::clay::define::Dict {name {values {}}} {
  set class [current_class]
  set name [string trim $name :/]
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1028
1029
1030
1031
1032
1033
1034
















1035
1036
1037
1038
1039
1040
1041







-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-







  }
}
proc ::clay::define::Variable {name {default {}}} {
  set class [current_class]
  set name [string trimright $name :/]
  $class clay set variable/ $name $default
}
proc ::clay::object_create {objname {class {}}} {
  #if {$::clay::trace>0} {
  #  puts [list $objname CREATE]
  #}
}
proc ::clay::object_rename {object newname} {
  if {$::clay::trace>0} {
    puts [list $object RENAME -> $newname]
  }
}
proc ::clay::object_destroy objname {
  if {$::clay::trace>0} {
    puts [list $objname DESTROY]
  }
  #::cron::object_destroy $objname
}
::namespace eval ::clay::define {
}
proc ::clay::ensemble_methodbody {ensemble einfo} {
  set default standard
  set preamble {}
  set eswitch {}
  if {[dict exists $einfo default]} {
1729
1730
1731
1732
1733
1734
1735


















1736
1737
1738
1739
1740
1741
1742
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703







+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+







        }
        foreach class $clayorder {
          if {[$class clay exists {*}$args]} {
            return $class
          }
        }
        return {}
      }
      refcount {
        my variable refcount
        if {![info exists refcount]} {
          return 0
        }
        return $refcount
      }
      refcount_incr {
        my variable refcount
        incr refcount
      }
      refcount_decr {
        my variable refcount
        incr refcount -1
        if {$refcount <= 0} {
          ::clay::object_destroy [self]
        }
      }
      replace {
        set clay [lindex $args 0]
      }
      source {
        source [lindex $args 0]
      }
1858
1859
1860
1861
1862
1863
1864


1865

1866
1867

1868
1869
1870

1871
1872
1873
1874


1875
1876
1877
1878
1879
1880
1881
1882
1883























1884
1885
1886
1887
1888
1889
1890
1819
1820
1821
1822
1823
1824
1825
1826
1827

1828


1829



1830




1831
1832

1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870







+
+
-
+
-
-
+
-
-
-
+
-
-
-
-
+
+
-








+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+







  }
}
::clay::object clay branch array
::clay::object clay branch mixin
::clay::object clay branch option
::clay::object clay branch dict clay
::clay::object clay set variable DestroyEvent 0
if {[info commands ::cron::object_destroy] eq {}} {
  # Provide a noop if we aren't running with the cron scheduler
::namespace eval ::clay::event {
  namespace eval ::cron {}
}
proc ::clay::destroy args {
  proc ::cron::object_destroy args {}
  if {![info exists ::clay::idle_destroy]} {
    set ::clay::idle_destroy {}
  }
}
  foreach object $args {
    if {$object in $::clay::idle_destroy} continue
    lappend ::clay::idle_destroy  $object
  }
::namespace eval ::clay::event {
}
}
proc ::clay::cleanup {} {
  if {![info exists ::clay::idle_destroy]} return
  foreach obj $::clay::idle_destroy {
    if {[info commands $obj] ne {}} {
      catch {$obj destroy}
    }
  }
  set ::clay::idle_destroy {}
}
proc ::clay::object_create {objname {class {}}} {
  #if {$::clay::trace>0} {
  #  puts [list $objname CREATE]
  #}
}
proc ::clay::object_rename {object newname} {
  if {$::clay::trace>0} {
    puts [list $object RENAME -> $newname]
  }
}
proc ::clay::object_destroy args {
  if {![info exists ::clay::idle_destroy]} {
    set ::clay::idle_destroy {}
  }
  foreach objname $args {
    if {$::clay::trace>0} {
      puts [list $objname DESTROY]
    }
    ::cron::object_destroy $objname
    if {$objname in $::clay::idle_destroy} continue
    lappend ::clay::idle_destroy $objname
  }
}
proc ::clay::event::cancel {self {task *}} {
  variable timer_event
  variable timer_script

  foreach {id event} [array get timer_event $self:$task] {
    ::after cancel $event