hayabusa 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (85) hide show
  1. data/.document +5 -0
  2. data/.rspec +1 -0
  3. data/Gemfile +20 -0
  4. data/Gemfile.lock +59 -0
  5. data/LICENSE.txt +20 -0
  6. data/README.rdoc +19 -0
  7. data/Rakefile +49 -0
  8. data/VERSION +1 -0
  9. data/bin/check_running.rb +69 -0
  10. data/bin/hayabusa_benchmark.rb +82 -0
  11. data/bin/hayabusa_cgi.rb +84 -0
  12. data/bin/hayabusa_fcgi.fcgi +159 -0
  13. data/bin/hayabusa_fcgi.rb +159 -0
  14. data/bin/knjappserver_start.rb +42 -0
  15. data/conf/apache2_cgi_rhtml_conf.conf +10 -0
  16. data/conf/apache2_fcgi_rhtml_conf.conf +22 -0
  17. data/hayabusa.gemspec +151 -0
  18. data/lib/hayabusa.rb +518 -0
  19. data/lib/hayabusa_cgi_session.rb +128 -0
  20. data/lib/hayabusa_cgi_tools.rb +102 -0
  21. data/lib/hayabusa_custom_io.rb +22 -0
  22. data/lib/hayabusa_database.rb +125 -0
  23. data/lib/hayabusa_erb_handler.rb +27 -0
  24. data/lib/hayabusa_ext/cleaner.rb +140 -0
  25. data/lib/hayabusa_ext/cmdline.rb +52 -0
  26. data/lib/hayabusa_ext/errors.rb +135 -0
  27. data/lib/hayabusa_ext/logging.rb +404 -0
  28. data/lib/hayabusa_ext/mailing.rb +158 -0
  29. data/lib/hayabusa_ext/sessions.rb +71 -0
  30. data/lib/hayabusa_ext/threadding.rb +96 -0
  31. data/lib/hayabusa_ext/threadding_timeout.rb +101 -0
  32. data/lib/hayabusa_ext/translations.rb +43 -0
  33. data/lib/hayabusa_ext/web.rb +190 -0
  34. data/lib/hayabusa_http_server.rb +102 -0
  35. data/lib/hayabusa_http_session.rb +361 -0
  36. data/lib/hayabusa_http_session_contentgroup.rb +176 -0
  37. data/lib/hayabusa_http_session_page_environment.rb +66 -0
  38. data/lib/hayabusa_http_session_post_multipart.rb +135 -0
  39. data/lib/hayabusa_http_session_request.rb +219 -0
  40. data/lib/hayabusa_http_session_response.rb +144 -0
  41. data/lib/hayabusa_models.rb +8 -0
  42. data/lib/kernel_ext/gettext_methods.rb +22 -0
  43. data/lib/kernel_ext/magic_methods.rb +61 -0
  44. data/lib/models/log.rb +130 -0
  45. data/lib/models/log_access.rb +88 -0
  46. data/lib/models/log_data.rb +27 -0
  47. data/lib/models/log_data_link.rb +3 -0
  48. data/lib/models/log_data_value.rb +21 -0
  49. data/lib/models/log_link.rb +65 -0
  50. data/lib/models/session.rb +35 -0
  51. data/pages/benchmark.rhtml +0 -0
  52. data/pages/benchmark_print.rhtml +14 -0
  53. data/pages/benchmark_simple.rhtml +3 -0
  54. data/pages/benchmark_threadded_content.rhtml +21 -0
  55. data/pages/debug_database_connections.rhtml +46 -0
  56. data/pages/debug_http_sessions.rhtml +40 -0
  57. data/pages/debug_memory_usage.rhtml +16 -0
  58. data/pages/error_notfound.rhtml +12 -0
  59. data/pages/logs_latest.rhtml +57 -0
  60. data/pages/logs_show.rhtml +32 -0
  61. data/pages/spec.rhtml +41 -0
  62. data/pages/spec_post.rhtml +3 -0
  63. data/pages/spec_test_multiple_clients.rhtml +3 -0
  64. data/pages/spec_thread_joins.rhtml +21 -0
  65. data/pages/spec_threadded_content.rhtml +40 -0
  66. data/pages/tests.rhtml +14 -0
  67. data/spec/cgi_spec.rb +47 -0
  68. data/spec/custom_urls_spec.rb +35 -0
  69. data/spec/fcgi_multiple_processes_spec.rb +32 -0
  70. data/spec/fcgi_spec.rb +69 -0
  71. data/spec/hayabusa_spec.rb +194 -0
  72. data/spec/spec_helper.rb +12 -0
  73. data/tests/cgi_test/config_cgi.rb +6 -0
  74. data/tests/cgi_test/threadded_content_test.rhtml +23 -0
  75. data/tests/cgi_test/vars_get_test.rhtml +4 -0
  76. data/tests/cgi_test/vars_header_test.rhtml +3 -0
  77. data/tests/cgi_test/vars_post_test.rhtml +4 -0
  78. data/tests/fcgi_test/config_fcgi.rb +6 -0
  79. data/tests/fcgi_test/index.rhtml +3 -0
  80. data/tests/fcgi_test/sleeper.rhtml +4 -0
  81. data/tests/fcgi_test/threadded_content_test.rhtml +23 -0
  82. data/tests/fcgi_test/vars_get_test.rhtml +4 -0
  83. data/tests/fcgi_test/vars_header_test.rhtml +3 -0
  84. data/tests/fcgi_test/vars_post_test.rhtml +4 -0
  85. metadata +257 -0
@@ -0,0 +1,176 @@
1
+ #encoding: utf-8
2
+
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.
4
+ class Hayabusa::Http_session::Contentgroup
5
+ attr_reader :done, :cur_data
6
+ attr_accessor :chunked, :socket
7
+ NL = "\r\n"
8
+
9
+ def initialize(args = {})
10
+ @socket = args[:socket]
11
+ @chunked = args[:chunked]
12
+ @resp = args[:resp]
13
+ @httpsession = args[:httpsession]
14
+ @mutex = Mutex.new
15
+ @debug = false
16
+ end
17
+
18
+ def init
19
+ @done = false
20
+ @thread = nil
21
+ @cur_data = {
22
+ :str => "",
23
+ :done => false
24
+ }
25
+ @ios = [@cur_data]
26
+ end
27
+
28
+ def reset
29
+ @ios = []
30
+ @done = false
31
+ @thread = nil
32
+ @forced = false
33
+
34
+ @mutex.synchronize do
35
+ self.new_io
36
+ end
37
+
38
+ self.register_thread
39
+ end
40
+
41
+ def new_io(obj = "")
42
+ @cur_data[:done] = true if @cur_data
43
+ @cur_data = {:str => obj, :done => false}
44
+ @ios << @cur_data
45
+ end
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
+
52
+ def register_thread
53
+ Thread.current[:hayabusa] = {} if !Thread.current[:hayabusa]
54
+ Thread.current[:hayabusa][:contentgroup] = self
55
+ end
56
+
57
+ def new_thread
58
+ cgroup = Hayabusa::Http_session::Contentgroup.new(:socket => @socket, :chunked => @chunked)
59
+ cgroup.init
60
+
61
+ @mutex.synchronize do
62
+ @ios << cgroup
63
+ self.new_io
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
74
+ #Ignore - the user probaly left.
75
+ end
76
+ end
77
+
78
+ def write(cont)
79
+ @mutex.synchronize do
80
+ @cur_data[:str] << cont
81
+ end
82
+ end
83
+
84
+ def write_output
85
+ return nil if @thread
86
+
87
+ @mutex.synchronize do
88
+ @thread = Thread.new do
89
+ begin
90
+ self.write_begin
91
+ rescue => e
92
+ STDERR.puts "Error while writing."
93
+ STDERR.puts e.inspect
94
+ STDERR.puts e.backtrace
95
+ end
96
+ end
97
+ end
98
+ end
99
+
100
+ def write_force
101
+ @mutex.synchronize do
102
+ @forced = true if !@thread
103
+ end
104
+
105
+ if @thread
106
+ @thread.join
107
+ else
108
+ self.write_begin
109
+ end
110
+ end
111
+
112
+ def mark_done
113
+ @cur_data[:done] = true
114
+ @done = true
115
+ end
116
+
117
+ def join
118
+ return nil if @forced
119
+ sleep 0.1 while !@thread
120
+ @thread.join
121
+ end
122
+
123
+ def write_to_socket
124
+ count = 0
125
+
126
+ @ios.each do |data|
127
+ if data.is_a?(Hayabusa::Http_session::Contentgroup)
128
+ data.write_to_socket
129
+ elsif data.key?(:str)
130
+ if data[:str].is_a?(Hash) and data[:str][:type] == :file
131
+ File.open(data[:str][:path], "r") do |file|
132
+ loop do
133
+ begin
134
+ buf = file.sysread(16384)
135
+ rescue EOFError
136
+ break
137
+ end
138
+
139
+ if @chunked
140
+ @socket.write("#{buf.length.to_s(16)}#{NL}#{buf}#{NL}")
141
+ else
142
+ @socket.write(buf)
143
+ end
144
+ end
145
+ end
146
+ else
147
+ loop do
148
+ break if data[:done] and data[:str].size <= 0
149
+ sleep 0.1 while data[:str].size < 512 and !data[:done]
150
+
151
+ str = nil
152
+ @mutex.synchronize do
153
+ str = data[:str].bytes
154
+ data[:str] = ""
155
+ end
156
+
157
+ #512 could take a long time for big pages. 16384 seems to be an optimal number.
158
+ str.each_slice(16384) do |slice|
159
+ buf = slice.pack("C*")
160
+
161
+ if @chunked
162
+ @socket.write("#{buf.length.to_s(16)}#{NL}#{buf}#{NL}")
163
+ else
164
+ @socket.write(buf)
165
+ end
166
+ end
167
+ end
168
+ end
169
+ else
170
+ raise "Unknown object: '#{data.class.name}'."
171
+ end
172
+ end
173
+
174
+ count += 1
175
+ end
176
+ end
@@ -0,0 +1,66 @@
1
+ #This class handels all the magic-methods in a different way - by defining them as methods on the binding for the .rhtml-pages.
2
+ class Hayabusa::Http_session::Page_environment
3
+ def initialize(args = {})
4
+ @args = args
5
+ end
6
+
7
+ def get_binding
8
+ return binding
9
+ end
10
+
11
+ def _buf
12
+ return $stdout
13
+ end
14
+
15
+ def _cookie
16
+ return @args[:httpsession].cookie
17
+ end
18
+
19
+ def _db
20
+ return @args[:hb].db_handler
21
+ end
22
+
23
+ def _get
24
+ return @args[:httpsession].get
25
+ end
26
+
27
+ def _hb
28
+ return @args[:hb]
29
+ end
30
+
31
+ alias _requestdata _hb
32
+
33
+ def _hb_vars
34
+ return @args[:hb].vars
35
+ end
36
+
37
+ def _httpsession
38
+ return @args[:httpsession]
39
+ end
40
+
41
+ def _httpsession_var
42
+ return @args[:httpsession].httpsession_var
43
+ end
44
+
45
+ def _post
46
+ return @args[:httpsession].post
47
+ end
48
+
49
+ def _meta
50
+ return @args[:httpsession].meta
51
+ end
52
+
53
+ alias _server _meta
54
+
55
+ def _session
56
+ return @args[:httpsession].session.sess_data
57
+ end
58
+
59
+ def _session_hash
60
+ return @args[:httpsession].session_hash
61
+ end
62
+
63
+ def _session_obj
64
+ return @args[:httpsession].session
65
+ end
66
+ end
@@ -0,0 +1,135 @@
1
+ #This class parses and handels post-multipart requests.
2
+ class Hayabusa::Http_session::Post_multipart
3
+ attr_reader :return
4
+
5
+ def initialize(args)
6
+ @args = args
7
+ boundary_regexp = /\A--#{@args["boundary"]}(--)?#{@args["crlf"]}\z/
8
+ @return = {}
9
+ @data = nil
10
+ @mode = nil
11
+ @headers = {}
12
+ @counts = {}
13
+
14
+ @args["io"].each do |line|
15
+ if boundary_regexp =~ line
16
+ #Finish the data we were writing.
17
+ self.finish_data if @data
18
+
19
+ @data = ""
20
+ @mode = "headers"
21
+ elsif @mode == "headers"
22
+ if match = line.match(/^(.+?):\s+(.+)#{@args["crlf"]}$/)
23
+ @headers[match[1].to_s.downcase] = match[2]
24
+ elsif line == @args["crlf"]
25
+ @mode = "body"
26
+ else
27
+ raise "Could not match header from: '#{line}'."
28
+ end
29
+ elsif @mode == "body"
30
+ @data << line
31
+ else
32
+ raise "Invalid mode: '#{@mode}'."
33
+ end
34
+ end
35
+
36
+ self.finish_data if @data and @data.to_s.length > 0
37
+
38
+ @data = nil
39
+ @headers = nil
40
+ @mode = nil
41
+ @args = nil
42
+ end
43
+
44
+ #Add the current treated data to the return-hash.
45
+ def finish_data
46
+ @data.chop!
47
+ name = nil
48
+
49
+ disp = @headers["content-disposition"]
50
+ raise "No 'content-disposition' was given." if !disp
51
+
52
+
53
+ #Figure out value-name in post-hash.
54
+ match_name = disp.match(/name=\"(.+?)\"/)
55
+ raise "Could not match name." if !match_name
56
+ name = match_name[1]
57
+
58
+
59
+ #Fix count with name if given as increamental [].
60
+ if match = name.match(/^(.+)\[\]$/)
61
+ if !@counts.key?(match[1])
62
+ @counts[match[1]] = 0
63
+ else
64
+ @counts[match[1]] += 1
65
+ end
66
+
67
+ name = "#{match[1]}[#{@counts[match[1]]}]"
68
+ end
69
+
70
+
71
+ #Figure out actual filename.
72
+ match_fname = disp.match(/filename=\"(.+?)\"/)
73
+
74
+ if match_fname
75
+ obj = Hayabusa::Http_session::Post_multipart::File_upload.new(
76
+ "fname" => match_fname[1],
77
+ "headers" => @headers,
78
+ "data" => @data
79
+ )
80
+ @return[name] = obj
81
+ @data = nil
82
+ @headers = {}
83
+ @mode = nil
84
+ else
85
+ @return[name] = @data
86
+ @data = nil
87
+ @headers = {}
88
+ @mode = nil
89
+ end
90
+ end
91
+ end
92
+
93
+ #This is the actual returned object for fileuploads. It is able to do various user-friendly things like save the content to a given path, return the filename, returns the content to a string and more.
94
+ class Hayabusa::Http_session::Post_multipart::File_upload
95
+ def initialize(args)
96
+ @args = args
97
+ end
98
+
99
+ #Returns the size of the upload.
100
+ def size
101
+ return @args["data"].length
102
+ end
103
+
104
+ #Returns the size of the fileupload.
105
+ def length
106
+ return @args["data"].length
107
+ end
108
+
109
+ #Returns the filename given for the fileupload.
110
+ def filename
111
+ return @args["fname"]
112
+ end
113
+
114
+ #Returns the headers given for the fileupload. Type and more should be here.
115
+ def headers
116
+ return @args["headers"]
117
+ end
118
+
119
+ #Returns the content of the file-upload as a string.
120
+ def to_s
121
+ return @args["data"]
122
+ end
123
+
124
+ #Saves the content of the fileupload to a given path.
125
+ def save_to(filepath)
126
+ File.open(filepath, "w") do |fp|
127
+ fp.write(self.to_s)
128
+ end
129
+ end
130
+
131
+ #This methods prevents the object from being converted to JSON. This can make some serious bugs.
132
+ def to_json(*args)
133
+ raise "File_upload-objects should not be converted to json."
134
+ end
135
+ end
@@ -0,0 +1,219 @@
1
+ require "knj/web"
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 Hayabusa::Http_session::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. Hayabusa, crlf and arguments.
13
+ def initialize(args)
14
+ @args = args
15
+ @hb = @args[:hb]
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(socket, cont)
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
+ def reset
31
+ @modified_since = nil
32
+ @get = nil
33
+ @post = nil
34
+ @cookie = nil
35
+ @meta = nil
36
+ @page_path = nil
37
+ @headers = nil
38
+ @http_version = nil
39
+ @read = nil
40
+ @clength = nil
41
+ @speec = nil
42
+ @percent = nil
43
+ @secs_left = nil
44
+ end
45
+
46
+ #Generates data on object from the given socket.
47
+ def socket_parse(socket)
48
+ self.reset
49
+ cont = ""
50
+ self.read_socket(socket, cont)
51
+
52
+ #Parse URI (page_path and get).
53
+ match = cont.match(/^(GET|POST|HEAD)\s+(.+)\s+HTTP\/1\.(\d+)\s*/)
54
+ raise "Could not parse request: '#{cont.split("\n").first}'." if !match
55
+
56
+ @http_version = "1.#{match[3]}"
57
+
58
+ method = match[1]
59
+ cont = cont.gsub(match[0], "")
60
+
61
+ uri = Knj::Web.parse_uri(match[2])
62
+
63
+ page_filepath = Knj::Web.urldec(uri[:path])
64
+ if page_filepath.length <= 0 or page_filepath == "/" or File.directory?("#{@hb.config[:doc_root]}/#{page_filepath}")
65
+ page_filepath = "#{page_filepath}/#{@hb.config[:default_page]}"
66
+ end
67
+
68
+ @page_path = "#{@hb.config[:doc_root]}/#{page_filepath}"
69
+ @get = Knj::Web.parse_urlquery(uri[:query], {:urldecode => true, :force_utf8 => true})
70
+
71
+ if @get["_hb_httpsession_id"]
72
+ @hb.httpsessions_ids[@get["_hb_httpsession_id"]] = @args[:httpsession]
73
+ end
74
+
75
+ begin
76
+ #Parse headers, cookies and meta.
77
+ @headers = {}
78
+ @cookie = {}
79
+ @meta = {
80
+ "REQUEST_METHOD" => method,
81
+ "QUERY_STRING" => uri[:query],
82
+ "REQUEST_URI" => match[2],
83
+ "SCRIPT_NAME" => uri[:path]
84
+ }
85
+
86
+ cont.scan(/^(\S+):\s*(.+)\r\n/) do |header_match|
87
+ key = header_match[0].downcase
88
+ val = header_match[1]
89
+
90
+ @headers[key] = [] if !@headers.has_key?(key)
91
+ @headers[key] << val
92
+
93
+ case key
94
+ when "cookie"
95
+ Knj::Web.parse_cookies(val).each do |key, val|
96
+ @cookie[key] = val
97
+ end
98
+ when "content-length"
99
+ @clength = val.to_i
100
+ else
101
+ key = key.upcase.gsub("-", "_")
102
+ @meta["HTTP_#{key}"] = val
103
+ end
104
+ end
105
+
106
+
107
+ #Parse post
108
+ @post = {}
109
+
110
+ if method == "POST"
111
+ post_treated = {}
112
+
113
+ @speed = nil
114
+ @read = 0
115
+ post_data = ""
116
+
117
+ Thread.new do
118
+ begin
119
+ time_cur = Time.now
120
+ read_last = 0
121
+ sleep 0.1
122
+
123
+ while @clength and @read != nil and @read < @clength
124
+ break if !@clength or !@read
125
+
126
+ time_now = Time.now
127
+ time_betw = time_now.to_f - time_cur.to_f
128
+ read_betw = @read - read_last
129
+
130
+ time_cur = time_now
131
+ read_last = @read
132
+
133
+ @percent = @read.to_f / @clength.to_f
134
+ @speed = read_betw.to_f / time_betw.to_f
135
+
136
+ bytes_left = @clength - read
137
+
138
+ if @speed > 0 and bytes_left > 0
139
+ @secs_left = bytes_left.to_f / @speed
140
+ else
141
+ @secs_left = false
142
+ end
143
+
144
+ sleep 2
145
+ end
146
+ rescue => e
147
+ if @hb
148
+ @hb.handle_error(e)
149
+ else
150
+ STDOUT.print Knj::Errors.error_str(e)
151
+ end
152
+ end
153
+ end
154
+
155
+ while @read < @clength
156
+ read_size = @clength - @read
157
+ read_size = 4096 if read_size > 4096
158
+
159
+ raise Errno::ECONNRESET, "Socket closed." if socket.closed?
160
+ read = socket.read(read_size)
161
+ raise Errno::ECONNRESET, "Socket returned non-string: '#{read.class.name}'." if !read.is_a?(String)
162
+ post_data << read
163
+ @read += read.length
164
+ end
165
+
166
+ if @headers["content-type"] and match = @headers["content-type"].first.match(/^multipart\/form-data; boundary=(.+)\Z/)
167
+ post_treated = Hayabusa::Http_session::Post_multipart.new(
168
+ "io" => StringIO.new("#{post_data}"),
169
+ "boundary" => match[1],
170
+ "crlf" => @crlf
171
+ ).return
172
+
173
+ self.convert_post(@post, post_treated, {:urldecode => false})
174
+ else
175
+ post_data.split("&").each do |splitted|
176
+ splitted = splitted.split("=")
177
+ key = Knj::Web.urldec(splitted[0]).to_s.encode("utf-8")
178
+ val = splitted[1].to_s.encode("utf-8")
179
+ post_treated[key] = val
180
+ end
181
+
182
+ self.convert_post(@post, post_treated, {:urldecode => true})
183
+ end
184
+ end
185
+ ensure
186
+ @read = nil
187
+ @speed = nil
188
+ @clength = nil
189
+ @percent = nil
190
+ @secs_left = nil
191
+
192
+ #If it doesnt get unset we could have a serious memory reference GC problem.
193
+ if @get["_hb_httpsession_id"]
194
+ @hb.httpsessions_ids.delete(@get["_hb_httpsession_id"])
195
+ end
196
+ end
197
+ end
198
+
199
+ #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.
200
+ def modified_since
201
+ return @modified_since if @modified_since
202
+ return false if !@meta["HTTP_IF_MODIFIED_SINCE"]
203
+
204
+ mod_match = @meta["HTTP_IF_MODIFIED_SINCE"].match(/^([A-z]+),\s+(\d+)\s+([A-z]+)\s+(\d+)\s+(\d+):(\d+):(\d+)\s+(.+)$/)
205
+ raise "Could not parse 'HTTP_IF_MODIFIED_SINCE'." if !mod_match
206
+
207
+ month_no = Datet.month_str_to_no(mod_match[3])
208
+ @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)
209
+
210
+ return @modified_since
211
+ end
212
+
213
+ #Converts post-result to the right type of hash.
214
+ def convert_post(seton, post_val, args = {})
215
+ post_val.each do |varname, value|
216
+ Knj::Web.parse_name(seton, varname, value, args)
217
+ end
218
+ end
219
+ end