knjappserver 0.0.16 → 0.0.17

Sign up to get free protection for your applications and to get access to all the features.
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