knjappserver 0.0.16 → 0.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. data/VERSION +1 -1
  2. data/bin/knjappserver_start.rb +17 -25
  3. data/knjappserver.gemspec +17 -6
  4. data/lib/conf/conf_example.rb +3 -3
  5. data/lib/files/database_schema.rb +1 -2
  6. data/lib/include/class_customio.rb +8 -25
  7. data/lib/include/class_erbhandler.rb +7 -1
  8. data/lib/include/class_httpserver.rb +50 -34
  9. data/lib/include/class_httpsession.rb +138 -101
  10. data/lib/include/class_httpsession_contentgroup.rb +52 -24
  11. data/lib/include/class_httpsession_http_request.rb +203 -0
  12. data/lib/include/{class_httpresp.rb → class_httpsession_http_response.rb} +27 -28
  13. data/lib/include/class_httpsession_post_multipart.rb +117 -0
  14. data/lib/include/class_knjappserver.rb +146 -96
  15. data/lib/include/class_knjappserver_cleaner.rb +74 -68
  16. data/lib/include/class_knjappserver_cmdline.rb +23 -17
  17. data/lib/include/class_knjappserver_errors.rb +121 -104
  18. data/lib/include/class_knjappserver_leakproxy_client.rb +6 -0
  19. data/lib/include/class_knjappserver_leakproxy_server.rb +56 -0
  20. data/lib/include/class_knjappserver_logging.rb +25 -25
  21. data/lib/include/class_knjappserver_mailing.rb +84 -56
  22. data/lib/include/class_knjappserver_sessions.rb +15 -22
  23. data/lib/include/class_knjappserver_threadding.rb +70 -43
  24. data/lib/include/class_knjappserver_threadding_timeout.rb +20 -4
  25. data/lib/include/class_knjappserver_translations.rb +6 -4
  26. data/lib/include/class_knjappserver_web.rb +87 -35
  27. data/lib/include/class_log.rb +9 -9
  28. data/lib/include/class_log_link.rb +4 -4
  29. data/lib/include/class_session.rb +8 -4
  30. data/lib/include/gettext_funcs.rb +8 -6
  31. data/lib/include/magic_methods.rb +4 -0
  32. data/lib/pages/debug_database_connections.rhtml +46 -0
  33. data/lib/pages/debug_http_sessions.rhtml +40 -0
  34. data/lib/pages/error_notfound.rhtml +12 -0
  35. data/lib/pages/spec.rhtml +1 -1
  36. data/lib/pages/spec_post.rhtml +3 -0
  37. data/lib/pages/spec_thread_joins.rhtml +21 -0
  38. data/lib/pages/spec_threadded_content.rhtml +2 -0
  39. data/lib/pages/tests.rhtml +14 -0
  40. data/lib/scripts/benchmark.rb +25 -8
  41. data/lib/scripts/knjappserver_cgi.rb +60 -0
  42. data/lib/scripts/knjappserver_fcgi.rb +135 -0
  43. data/lib/scripts/leakproxy.rb +27 -0
  44. data/spec/knjappserver_spec.rb +16 -5
  45. data/spec/leakproxy_spec.rb +56 -0
  46. metadata +38 -27
  47. data/lib/include/class_httpsession_knjengine.rb +0 -154
  48. data/lib/include/class_httpsession_mongrel.rb +0 -75
  49. data/lib/include/class_httpsession_webrick.rb +0 -75
@@ -1,3 +1,5 @@
1
+ #encoding: utf-8
2
+
1
3
  #This class handels the adding of content and writing to socket. Since this can be done with multiple threads and multiple IO's it can get complicated.
2
4
  class Knjappserver::Httpsession::Contentgroup
3
5
  attr_reader :done, :cur_data
@@ -5,9 +7,10 @@ class Knjappserver::Httpsession::Contentgroup
5
7
  NL = "\r\n"
6
8
 
7
9
  def initialize(args = {})
8
- @block = args[:restart_proc]
9
10
  @socket = args[:socket]
10
11
  @chunked = args[:chunked]
12
+ @resp = args[:resp]
13
+ @httpsession = args[:httpsession]
11
14
  @mutex = Mutex.new
12
15
  @debug = false
13
16
  end
@@ -26,6 +29,7 @@ class Knjappserver::Httpsession::Contentgroup
26
29
  @ios = []
27
30
  @done = false
28
31
  @thread = nil
32
+ @forced = false
29
33
 
30
34
  @mutex.synchronize do
31
35
  self.new_io
@@ -40,66 +44,89 @@ class Knjappserver::Httpsession::Contentgroup
40
44
  @ios << @cur_data
41
45
  end
42
46
 
47
+ #Forces the content to be the input - nothing else can be added after calling this.
48
+ def force_content(newcont)
49
+ @ios = [{:str => newcont, :done => true}]
50
+ end
51
+
43
52
  def register_thread
44
53
  Thread.current[:knjappserver] = {} if !Thread.current[:knjappserver]
45
54
  Thread.current[:knjappserver][:contentgroup] = self
46
55
  end
47
56
 
48
57
  def new_thread
58
+ cgroup = Knjappserver::Httpsession::Contentgroup.new(:socket => @socket, :chunked => @chunked)
59
+ cgroup.init
60
+
49
61
  @mutex.synchronize do
50
- cgroup = Knjappserver::Httpsession::Contentgroup.new(:socket => @socket, :chunked => @chunked)
51
- cgroup.init
52
-
53
- data = {:cgroup => cgroup}
54
- @ios << data
62
+ @ios << cgroup
55
63
  self.new_io
56
- self.register_thread
57
-
58
- return data
64
+ end
65
+
66
+ self.register_thread
67
+ return cgroup
68
+ end
69
+
70
+ def write_begin
71
+ begin
72
+ @resp.write if @httpsession.meta["METHOD"] != "HEAD"
73
+ rescue Errno::ECONNRESET, Errno::ENOTCONN, Errno::EPIPE, Timeout::Error
74
+ #Ignore - the user probaly left.
59
75
  end
60
76
  end
61
77
 
62
78
  def write(cont)
63
79
  @mutex.synchronize do
64
- @cur_data[:str] << cont.to_s
80
+ @cur_data[:str] << cont
65
81
  end
66
82
  end
67
83
 
68
84
  def write_output
69
- if @block and !@thread
70
- @mutex.synchronize do
71
- @thread = Knj::Thread.new do
72
- @block.call
73
- end
85
+ return nil if @thread
86
+
87
+ @mutex.synchronize do
88
+ @thread = Thread.new do
89
+ self.write_begin
74
90
  end
75
91
  end
76
92
  end
77
93
 
94
+ def write_force
95
+ @mutex.synchronize do
96
+ @forced = true if !@thread
97
+ end
98
+
99
+ if @thread
100
+ @thread.join
101
+ else
102
+ self.write_begin
103
+ end
104
+ end
105
+
78
106
  def mark_done
79
107
  @cur_data[:done] = true
80
108
  @done = true
81
109
  end
82
110
 
83
111
  def join
84
- if @block
85
- sleep 0.1 while !@thread
86
- @thread.join
87
- end
112
+ return nil if @forced
113
+ sleep 0.1 while !@thread
114
+ @thread.join
88
115
  end
89
116
 
90
117
  def write_to_socket
91
118
  count = 0
92
119
 
93
120
  @ios.each do |data|
94
- if data.key?(:cgroup)
95
- data[:cgroup].write_to_socket
121
+ if data.is_a?(Knjappserver::Httpsession::Contentgroup)
122
+ data.write_to_socket
96
123
  elsif data.key?(:str)
97
124
  if data[:str].is_a?(File)
98
125
  file = data[:str]
99
126
 
100
127
  loop do
101
128
  begin
102
- buf = file.sysread(4096)
129
+ buf = file.sysread(16384)
103
130
  rescue EOFError
104
131
  break
105
132
  end
@@ -115,7 +142,7 @@ class Knjappserver::Httpsession::Contentgroup
115
142
  else
116
143
  loop do
117
144
  break if data[:done] and data[:str].size <= 0
118
- sleep 0.1 while data[:str].size <= 512 and !data[:done]
145
+ sleep 0.1 while data[:str].size < 512 and !data[:done]
119
146
 
120
147
  str = nil
121
148
  @mutex.synchronize do
@@ -123,7 +150,8 @@ class Knjappserver::Httpsession::Contentgroup
123
150
  data[:str] = ""
124
151
  end
125
152
 
126
- str.each_slice(512) do |slice|
153
+ #512 could take a long time for big pages. 16384 seems to be an optimal number.
154
+ str.each_slice(16384) do |slice|
127
155
  buf = slice.pack("C*")
128
156
 
129
157
  if @chunked
@@ -0,0 +1,203 @@
1
+ require "uri"
2
+
3
+ #If we are running on JRuby or Rubinius this will seriously speed things up if we are behind a proxy.
4
+ if RUBY_PLATFORM == "java" or RUBY_ENGINE == "rbx"
5
+ BasicSocket.do_not_reverse_lookup = true
6
+ end
7
+
8
+ #This class parses the various HTTP requests into easy programmable objects. Get, post, cookie, meta and so on...
9
+ class Knjappserver::Httpsession::Http_request
10
+ attr_reader :get, :post, :cookie, :meta, :page_path, :headers, :http_version, :read, :clength, :speed, :percent, :secs_left
11
+
12
+ #Sets the various required data on the object. Knjappserver, crlf and arguments.
13
+ def initialize(args)
14
+ @args = args
15
+ @kas = @args[:kas]
16
+ @crlf = "\r\n"
17
+ end
18
+
19
+ #Reads content from the socket until the end of headers. Also does various error-checks.
20
+ def read_socket
21
+ loop do
22
+ raise Errno::ECONNRESET, "Socket closed." if @socket.closed?
23
+ read = @socket.gets
24
+ raise Errno::ECONNRESET, "Socket returned non-string: '#{read.class.name}'." if !read.is_a?(String)
25
+ @cont << read
26
+ break if @cont[-4..-1] == "\r\n\r\n" or @cont[-2..-1] == "\n\n"
27
+ end
28
+ end
29
+
30
+ #Generates data on object from the given socket.
31
+ def socket_parse(socket)
32
+ @modified_since = nil
33
+ @cont = ""
34
+ @socket = socket
35
+ self.read_socket
36
+
37
+ #Parse URI (page_path and get).
38
+ match = @cont.match(/^(GET|POST|HEAD)\s+(.+)\s+HTTP\/1\.(\d+)\s*/)
39
+ raise "Could not parse request: '#{@cont.split("\n").first}'." if !match
40
+
41
+ @http_version = "1.#{match[3]}"
42
+
43
+ method = match[1]
44
+ @cont = @cont.gsub(match[0], "")
45
+ uri = URI.parse(match[2])
46
+
47
+ page_filepath = Knj::Web.urldec(uri.path)
48
+ if page_filepath.length <= 0 or page_filepath == "/" or File.directory?("#{@kas.config[:doc_root]}/#{page_filepath}")
49
+ page_filepath = "#{page_filepath}/#{@kas.config[:default_page]}"
50
+ end
51
+
52
+ @page_path = "#{@kas.config[:doc_root]}/#{page_filepath}"
53
+ @get = Knj::Web.parse_urlquery(uri.query.to_s, {:urldecode => true, :force_utf8 => true})
54
+
55
+ if @get["_kas_httpsession_id"]
56
+ @kas.httpsessions_ids[@get["_kas_httpsession_id"]] = @args[:httpsession]
57
+ end
58
+
59
+ begin
60
+ #Parse headers, cookies and meta.
61
+ @headers = {}
62
+ @cookie = {}
63
+ @meta = {
64
+ "REQUEST_METHOD" => method,
65
+ "QUERY_STRING" => uri.query,
66
+ "REQUEST_URI" => match[2],
67
+ "SCRIPT_NAME" => uri.path
68
+ }
69
+
70
+ @cont.scan(/^(\S+):\s*(.+)\r\n/) do |header_match|
71
+ key = header_match[0].downcase
72
+ val = header_match[1]
73
+
74
+ @headers[key] = [] if !@headers.has_key?(key)
75
+ @headers[key] << val
76
+
77
+ case key
78
+ when "cookie"
79
+ Knj::Web.parse_cookies(val).each do |key, val|
80
+ @cookie[key] = val
81
+ end
82
+ when "content-length"
83
+ @clength = val.to_i
84
+ else
85
+ key = key.upcase.gsub("-", "_")
86
+ @meta["HTTP_#{key}"] = val
87
+ end
88
+ end
89
+
90
+
91
+ #Parse post
92
+ @post = {}
93
+
94
+ if method == "POST"
95
+ post_treated = {}
96
+
97
+ @speed = nil
98
+ @read = 0
99
+ post_data = ""
100
+
101
+ Thread.new do
102
+ begin
103
+ time_cur = Time.now
104
+ read_last = 0
105
+ sleep 0.1
106
+
107
+ while @clength and @read != nil and @read < @clength
108
+ break if !@clength or !@read
109
+
110
+ time_now = Time.now
111
+ time_betw = time_now.to_f - time_cur.to_f
112
+ read_betw = @read - read_last
113
+
114
+ time_cur = time_now
115
+ read_last = @read
116
+
117
+ @percent = @read.to_f / @clength.to_f
118
+ @speed = read_betw.to_f / time_betw.to_f
119
+
120
+ bytes_left = @clength - read
121
+
122
+ if @speed > 0 and bytes_left > 0
123
+ @secs_left = bytes_left.to_f / @speed
124
+ else
125
+ @secs_left = false
126
+ end
127
+
128
+ sleep 2
129
+ end
130
+ rescue => e
131
+ if @kas
132
+ @kas.handle_error(e)
133
+ else
134
+ STDOUT.print Knj::Errors.error_str(e)
135
+ end
136
+ end
137
+ end
138
+
139
+ while @read < @clength
140
+ read_size = @clength - @read
141
+ read_size = 4096 if read_size > 4096
142
+
143
+ raise Errno::ECONNRESET, "Socket closed." if @socket.closed?
144
+ read = @socket.read(read_size)
145
+ raise Errno::ECONNRESET, "Socket returned non-string: '#{read.class.name}'." if !read.is_a?(String)
146
+ post_data << read
147
+ @read += read.length
148
+ end
149
+
150
+ if @headers["content-type"] and match = @headers["content-type"].first.match(/^multipart\/form-data; boundary=(.+)\Z/)
151
+ post_treated = Knjappserver::Httpsession::Post_multipart.new(
152
+ "io" => StringIO.new("#{post_data}"),
153
+ "boundary" => match[1],
154
+ "crlf" => @crlf
155
+ ).return
156
+
157
+ self.convert_post(@post, post_treated, {:urldecode => false})
158
+ else
159
+ post_data.split("&").each do |splitted|
160
+ splitted = splitted.split("=")
161
+ key = Knj::Web.urldec(splitted[0]).to_s.encode("utf-8")
162
+ val = splitted[1].to_s.encode("utf-8")
163
+ post_treated[key] = val
164
+ end
165
+
166
+ self.convert_post(@post, post_treated, {:urldecode => true})
167
+ end
168
+ end
169
+ ensure
170
+ @read = nil
171
+ @speed = nil
172
+ @clength = nil
173
+ @percent = nil
174
+ @secs_left = nil
175
+
176
+ #If it doesnt get unset we could have a serious memory reference GC problem.
177
+ if @get["_kas_httpsession_id"]
178
+ @kas.httpsessions_ids.delete(@get["_kas_httpsession_id"])
179
+ end
180
+ end
181
+ end
182
+
183
+ #Parses the if-modified-since header and returns it as a Time-object. Returns false is no if-modified-since-header is given or raises an RuntimeError if it cant be parsed.
184
+ def modified_since
185
+ return @modified_since if @modified_since
186
+ return false if !@meta["HTTP_IF_MODIFIED_SINCE"]
187
+
188
+ mod_match = @meta["HTTP_IF_MODIFIED_SINCE"].match(/^([A-z]+),\s+(\d+)\s+([A-z]+)\s+(\d+)\s+(\d+):(\d+):(\d+)\s+(.+)$/)
189
+ raise "Could not parse 'HTTP_IF_MODIFIED_SINCE'." if !mod_match
190
+
191
+ month_no = Knj::Datet.month_str_to_no(mod_match[3])
192
+ @modified_since = Time.utc(mod_match[4].to_i, month_no, mod_match[2].to_i, mod_match[5].to_i, mod_match[6].to_i, mod_match[7].to_i)
193
+
194
+ return @modified_since
195
+ end
196
+
197
+ #Converts post-result to the right type of hash.
198
+ def convert_post(seton, post_val, args = {})
199
+ post_val.each do |varname, value|
200
+ Knj::Web.parse_name(seton, varname, value, args)
201
+ end
202
+ end
203
+ end
@@ -1,7 +1,8 @@
1
1
  require "time"
2
2
 
3
- class Knjappserver::Httpresp
4
- attr_accessor :nl, :status, :http_version, :headers, :headers_trailing, :headers_sent
3
+ #This object writes headers, trailing headers, status headers and more for HTTP-sessions.
4
+ class Knjappserver::Httpsession::Http_response
5
+ attr_accessor :chunked, :cgroup, :nl, :status, :http_version, :headers, :headers_trailing, :headers_sent, :socket
5
6
 
6
7
  STATUS_CODES = {
7
8
  100 => "Continue",
@@ -25,7 +26,8 @@ class Knjappserver::Httpresp
25
26
  NL = "\r\n"
26
27
 
27
28
  def initialize(args)
28
- @cgroup = args[:cgroup]
29
+ @chunked = false
30
+ @socket = args[:socket]
29
31
  end
30
32
 
31
33
  def reset(args)
@@ -73,57 +75,54 @@ class Knjappserver::Httpresp
73
75
  end
74
76
 
75
77
  code = STATUS_CODES[@status]
76
- res += " #{code}" if code
77
- res += NL
78
+ res << " #{code}" if code
79
+ res << NL
78
80
 
79
81
  @headers.each do |key, val|
80
- res += "#{val[0]}: #{val[1]}#{NL}"
82
+ res << "#{val[0]}: #{val[1]}#{NL}"
81
83
  end
82
84
 
83
85
  if @http_version == "1.1"
84
86
  @headers_11.each do |key, val|
85
- res += "#{key}: #{val}#{NL}"
87
+ res << "#{key}: #{val}#{NL}"
86
88
  end
87
89
 
88
90
  @trailers.each do |trailer|
89
- res += "Trailer: #{trailer}#{NL}"
91
+ res << "Trailer: #{trailer}#{NL}"
90
92
  end
91
93
  end
92
94
 
93
95
  @cookies.each do |cookie|
94
- res += "Set-Cookie: #{Knj::Web.cookie_str(cookie)}#{NL}"
96
+ res << "Set-Cookie: #{Knj::Web.cookie_str(cookie)}#{NL}"
95
97
  end
96
98
 
97
- res += NL
99
+ res << NL
98
100
 
99
101
  return res
100
102
  end
101
103
 
102
- def write(socket)
104
+ def write
103
105
  @headers_sent = true
104
- socket.write(self.header_str)
106
+ @socket.write(self.header_str)
105
107
 
106
108
  if @status == 304
107
109
  #do nothing.
108
110
  else
109
- case @http_version
110
- when "1.0"
111
- @cgroup.write_to_socket
112
- socket.write("#{NL}#{NL}")
113
- when "1.1"
114
- @cgroup.write_to_socket
115
- socket.write("0#{NL}")
116
-
117
- @headers_trailing.each do |header_id_str, header|
118
- socket.write("#{header[0]}: #{header[1]}#{NL}")
119
- end
120
-
121
- socket.write(NL)
122
- else
123
- raise "Could not figure out of HTTP version: '#{@http_version}'."
111
+ if @chunked
112
+ @cgroup.write_to_socket
113
+ @socket.write("0#{NL}")
114
+
115
+ @headers_trailing.each do |header_id_str, header|
116
+ @socket.write("#{header[0]}: #{header[1]}#{NL}")
117
+ end
118
+
119
+ @socket.write(NL)
120
+ else
121
+ @cgroup.write_to_socket
122
+ @socket.write("#{NL}#{NL}")
124
123
  end
125
124
  end
126
125
 
127
- socket.close if @close
126
+ @socket.close if @close
128
127
  end
129
128
  end