~aleteoryx/tclfeed-bsky

tclfeed-bsky/src/httpd.tcl -rw-r--r-- 2.7 KiB
45b4726bAleteoryx more heuristic tweaks 25 days ago
                                                                                
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
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
# TODO: this is only viable behind a reverse proxy.

namespace eval ::httpd {
  variable log [logger::init httpd]
  variable routes

  # https://wiki.tcl-lang.org/page/url-encoding
  proc ::httpd::uri-decode str {
    # rewrite "+" back to space
    # protect \ from quoting another '\'
    set str [string map [list + { } "\\" "\\\\"] $str]

    # prepare to process all %-escapes
    regsub -all -- {%([A-Fa-f0-9][A-Fa-f0-9])} $str {\\u00\1} str

    # process \u unicode mapped chars
    return [subst -novar -nocommand $str]
  }

  # routes: {path proc}
  proc ::httpd::router {name iroutes} {
    variable routes
    variable log
    if [info exists ::httpd::routes($name)] {
      return -code error "Can't create duplicate router!"
    }
    if {[llength $iroutes] % 2 != 0} {
      return -code error "Incorrect number of routes!"
    }

    set routes($name) $iroutes
  }

  proc ::httpd::listen {parent port} {
    socket -server [list ::httpd::accept $parent] $port
  }

  proc ::httpd::accept {parent sock addr port} {
    variable routes

    fconfigure $sock -translation crlf
    gets $sock line
    lassign $line method path version

    if {$method != "GET"} {
      puts $sock "HTTP/1.0 405 Method Not Allowed"
      puts $sock ""
      flush $sock
      close $sock
      return
    }

    set query {}
    if {[string first ? $path] != -1} {
      lassign [split $path ?] path params
      foreach param [split $params &] {
        set value [join [lassign [split $param =] key] =]
        dict lappend query [uri-decode $key] [uri-decode $value]
      }
    }

    set headers {}
    while {[gets $sock header] != -1} {
      if {$header == ""} break
      lassign [split header :] key value
      set key [string tolower [string trim $key]]
      set value [string trim $value]
      dict lappend headers $key $value
    }

    foreach {pathpat proc} [set routes($parent)] {
      if [string match $pathpat $path] {
        if [catch {{*}$proc $parent $sock $path $query $headers} result] {
          puts $sock "HTTP/1.0 500 Internal Server Error"
          puts $sock "Content-Type: text/plain"
          puts $sock ""
          puts $sock "Route Handler: $result"
          puts $sock ""
          flush $sock
          close $sock
          return
        }
        return
      }
    }

    puts $sock "HTTP/1.0 404 Not Found"
    puts $sock "Content-Type: text/plain"
    puts $sock ""
    puts $sock "No route handler available."
    puts $sock ""
    flush $sock
    close $sock
  }

  proc ::httpd::serve {mime payload _ sock _ _ _} {
    puts $sock "HTTP/1.0 200 OK"
    puts $sock "Content-Type: $mime"
    puts $sock "Content-Length: [string length $payload]"
    puts $sock ""
    fconfigure $sock -translation binary
    puts -nonewline $sock $payload
    close $sock
  }
}