racknga 0.9.1 → 0.9.2
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +30 -0
- data/README.textile +52 -0
- data/Rakefile +237 -117
- data/doc/text/news.textile +32 -0
- data/lib/racknga/cache_database.rb +18 -3
- data/lib/racknga/exception_mail_notifier.rb +76 -15
- data/lib/racknga/log_database.rb +21 -4
- data/lib/racknga/middleware/cache.rb +75 -14
- data/lib/racknga/middleware/deflater.rb +37 -0
- data/lib/racknga/middleware/exception_notifier.rb +17 -0
- data/lib/racknga/middleware/instance_name.rb +134 -0
- data/lib/racknga/middleware/jsonp.rb +82 -10
- data/lib/racknga/middleware/log.rb +18 -3
- data/lib/racknga/middleware/range.rb +10 -0
- data/lib/racknga/nginx_access_log_parser.rb +139 -0
- data/lib/racknga/reverse_line_reader.rb +73 -0
- data/lib/racknga/version.rb +2 -2
- data/lib/racknga/will_paginate.rb +1 -0
- data/lib/racknga.rb +3 -1
- data/license/lgpl-2.1.txt +502 -0
- data/munin/plugins/{passenger_domain_ → passenger_application_} +63 -47
- data/test/racknga-test-utils.rb +9 -12
- data/test/run-test.rb +5 -10
- data/test/test-middleware-cache.rb +77 -0
- data/test/test-middleware-instance-name.rb +98 -0
- data/test/test-middleware-jsonp.rb +32 -9
- data/test/test-middleware-range.rb +5 -4
- data/test/test-nginx-log-parser.rb +214 -0
- metadata +106 -56
- data/NEWS.ja.rdoc +0 -16
- data/NEWS.rdoc +0 -9
- data/README.ja.rdoc +0 -80
- data/README.rdoc +0 -51
- data/html/footer.html.erb +0 -33
- data/html/head.html.erb +0 -4
- data/html/header.html.erb +0 -17
- data/test/fixtures/rabbit-theme-change.ogv +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
# -*- coding: utf-8 -*-
|
2
2
|
#
|
3
|
-
# Copyright (C) 2010 Kouhei Sutou <kou@clear-code.com>
|
3
|
+
# Copyright (C) 2010-2011 Kouhei Sutou <kou@clear-code.com>
|
4
4
|
#
|
5
5
|
# This library is free software; you can redistribute it and/or
|
6
6
|
# modify it under the terms of the GNU Lesser General Public
|
@@ -29,21 +29,50 @@ module Racknga
|
|
29
29
|
class ExceptionMailNotifier
|
30
30
|
def initialize(options)
|
31
31
|
@options = Utils.normalize_options(options || {})
|
32
|
+
reset_limitation
|
32
33
|
end
|
33
34
|
|
34
35
|
def notify(exception, environment)
|
35
|
-
host = @options[:host] || "localhost"
|
36
36
|
return if to.empty?
|
37
|
-
|
37
|
+
|
38
|
+
if limitation_expired?
|
39
|
+
send_summaries unless @summaries.empty?
|
40
|
+
reset_limitation
|
41
|
+
end
|
42
|
+
|
43
|
+
if @mail_count < max_mail_count_in_limit_duration
|
44
|
+
send_notification(exception, environment)
|
45
|
+
else
|
46
|
+
@summaries << summarize(exception, environment)
|
47
|
+
end
|
48
|
+
|
49
|
+
@mail_count += 1
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
def reset_limitation
|
54
|
+
@mail_count = 0
|
55
|
+
@count_start_time = Time.now
|
56
|
+
@summaries = []
|
57
|
+
end
|
58
|
+
|
59
|
+
def limitation_expired?
|
60
|
+
(Time.now - @count_start_time) > limit_duration
|
61
|
+
end
|
62
|
+
|
63
|
+
def send(mail)
|
64
|
+
host = @options[:host] || "localhost"
|
38
65
|
Net::SMTP.start(host, @options[:port]) do |smtp|
|
39
66
|
smtp.send_message(mail, from, *to)
|
40
67
|
end
|
41
68
|
end
|
42
69
|
|
43
|
-
|
44
|
-
|
45
|
-
header =
|
46
|
-
|
70
|
+
def create_mail(options)
|
71
|
+
subject = [@options[:subject_label], options[:subject]].compact.join(' ')
|
72
|
+
header = header(:subject => subject)
|
73
|
+
|
74
|
+
body = options[:body]
|
75
|
+
|
47
76
|
mail = "#{header}\r\n#{body}"
|
48
77
|
mail.force_encoding("utf-8")
|
49
78
|
begin
|
@@ -53,24 +82,28 @@ module Racknga
|
|
53
82
|
mail.force_encoding("ASCII-8BIT")
|
54
83
|
end
|
55
84
|
|
56
|
-
def
|
85
|
+
def header(options)
|
57
86
|
<<-EOH
|
58
87
|
MIME-Version: 1.0
|
59
88
|
Content-Type: Text/Plain; charset=#{charset}
|
60
89
|
Content-Transfer-Encoding: #{transfer_encoding}
|
61
90
|
From: #{from}
|
62
91
|
To: #{to.join(', ')}
|
63
|
-
Subject: #{encode_subject(subject
|
92
|
+
Subject: #{encode_subject(options[:subject])}
|
64
93
|
Date: #{Time.now.rfc2822}
|
65
94
|
EOH
|
66
95
|
end
|
67
96
|
|
68
|
-
def
|
97
|
+
def send_notification(exception, environment)
|
98
|
+
mail = create_mail(:subject => exception.to_s,
|
99
|
+
:body => notification_body(exception, environment))
|
100
|
+
send(mail)
|
101
|
+
end
|
102
|
+
|
103
|
+
def notification_body(exception, environment)
|
69
104
|
request = Rack::Request.new(environment)
|
70
105
|
body = <<-EOB
|
71
|
-
|
72
|
-
--
|
73
|
-
#{exception.class}: #{exception}
|
106
|
+
#{summarize(exception, environment)}
|
74
107
|
--
|
75
108
|
#{exception.backtrace.join("\n")}
|
76
109
|
EOB
|
@@ -98,8 +131,26 @@ EOE
|
|
98
131
|
body
|
99
132
|
end
|
100
133
|
|
101
|
-
def
|
102
|
-
|
134
|
+
def send_summaries
|
135
|
+
subject = "summaries of #{@summaries.size} notifications"
|
136
|
+
mail = create_mail(:subject => subject,
|
137
|
+
:body => report_body)
|
138
|
+
send(mail)
|
139
|
+
end
|
140
|
+
|
141
|
+
def report_body
|
142
|
+
@summaries[0..10].join("\n\n")
|
143
|
+
end
|
144
|
+
|
145
|
+
def summarize(exception, environment)
|
146
|
+
request = Rack::Request.new(environment)
|
147
|
+
<<-EOB
|
148
|
+
Timestamp: #{Time.now.rfc2822}
|
149
|
+
--
|
150
|
+
URL: #{request.url}
|
151
|
+
--
|
152
|
+
#{exception.class}: #{exception}
|
153
|
+
EOB
|
103
154
|
end
|
104
155
|
|
105
156
|
def to
|
@@ -125,6 +176,16 @@ EOE
|
|
125
176
|
@options[:charset] || 'utf-8'
|
126
177
|
end
|
127
178
|
|
179
|
+
DEFAULT_MAX_MAIL_COUNT_IN_LIMIT_DURATION = 2
|
180
|
+
def max_mail_count_in_limit_duration
|
181
|
+
@options[:max_mail_count_in_limit_duration] || DEFAULT_MAX_MAIL_COUNT_IN_LIMIT_DURATION
|
182
|
+
end
|
183
|
+
|
184
|
+
DEFAULT_LIMIT_DURATION = 60 # one minute
|
185
|
+
def limit_duration
|
186
|
+
@options[:limit_duration] || DEFAULT_LIMIT_DURATION
|
187
|
+
end
|
188
|
+
|
128
189
|
def transfer_encoding
|
129
190
|
case charset
|
130
191
|
when /\Autf-8\z/i
|
data/lib/racknga/log_database.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# -*- coding: utf-8 -*-
|
2
2
|
#
|
3
|
-
# Copyright (C) 2010 Kouhei Sutou <kou@clear-code.com>
|
3
|
+
# Copyright (C) 2010-2011 Kouhei Sutou <kou@clear-code.com>
|
4
4
|
#
|
5
5
|
# This library is free software; you can redistribute it and/or
|
6
6
|
# modify it under the terms of the GNU Lesser General Public
|
@@ -21,7 +21,13 @@ require 'fileutils'
|
|
21
21
|
require 'groonga'
|
22
22
|
|
23
23
|
module Racknga
|
24
|
+
# This is a log database based on groonga. It is used by
|
25
|
+
# Racknga::Middleware::Log.
|
26
|
+
#
|
27
|
+
# Normally, #purge_old_responses is only used for log
|
28
|
+
# maintenance.
|
24
29
|
class LogDatabase
|
30
|
+
# @param [String] database_path the path for log database.
|
25
31
|
def initialize(database_path)
|
26
32
|
@database_path = database_path
|
27
33
|
@context = Groonga::Context.new(:encoding => :none)
|
@@ -45,12 +51,23 @@ module Racknga
|
|
45
51
|
@database.close
|
46
52
|
end
|
47
53
|
|
54
|
+
# Purges old responses. To clear old logs, you should
|
55
|
+
# call this method. All records created before
|
56
|
+
# +base_time+ are removed.
|
57
|
+
#
|
58
|
+
# You can call this method by the different
|
59
|
+
# process from your Rack application
|
60
|
+
# process. (e.g. cron.) It's multi process safe.
|
61
|
+
#
|
62
|
+
# @param [Time] base_time the oldest record time to be
|
63
|
+
# removed. The default value is 1 day ago.
|
48
64
|
def purge_old_entries(base_time=nil)
|
49
65
|
base_time ||= Time.now - 60 * 60 * 24
|
50
|
-
entries.select do |record|
|
66
|
+
target_entries = entries.select do |record|
|
51
67
|
record.time_stamp < base_time
|
52
|
-
end
|
53
|
-
|
68
|
+
end
|
69
|
+
target_entries.each do |entry|
|
70
|
+
entry.key.delete
|
54
71
|
end
|
55
72
|
end
|
56
73
|
|
@@ -16,17 +16,36 @@
|
|
16
16
|
# License along with this library; if not, write to the Free Software
|
17
17
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
18
18
|
|
19
|
-
require 'digest
|
19
|
+
require 'digest'
|
20
20
|
require 'yaml'
|
21
21
|
require 'racknga/cache_database'
|
22
22
|
|
23
23
|
module Racknga
|
24
24
|
module Middleware
|
25
|
+
# This is a helper middleware for
|
26
|
+
# Racknga::Middleware::Cache.
|
27
|
+
#
|
28
|
+
# If your Rack application provides different views to
|
29
|
+
# mobile user agent and PC user agent in the same URL,
|
30
|
+
# this middleware is useful. Your Rack application can
|
31
|
+
# has different caches for mobile user agent and PC user
|
32
|
+
# agent.
|
33
|
+
#
|
34
|
+
# This middleware requires jpmobile.
|
35
|
+
#
|
36
|
+
# Usage:
|
37
|
+
# use Racnkga::Middleware::PerUserAgentCache
|
38
|
+
# use Racnkga::Middleware::Cache, :database_path => "var/cache/db"
|
39
|
+
# run YourApplication
|
40
|
+
#
|
41
|
+
# @see http://jpmobile-rails.org/ jpmobile
|
42
|
+
# @see Racknga::Middleware::Cache
|
25
43
|
class PerUserAgentCache
|
26
44
|
def initialize(application)
|
27
45
|
@application = application
|
28
46
|
end
|
29
47
|
|
48
|
+
# For Rack.
|
30
49
|
def call(environment)
|
31
50
|
mobile = environment["rack.jpmobile"]
|
32
51
|
if mobile
|
@@ -35,16 +54,47 @@ module Racknga
|
|
35
54
|
else
|
36
55
|
user_agent_key = "pc"
|
37
56
|
end
|
38
|
-
key = environment[Cache::
|
39
|
-
environment[Cache::
|
57
|
+
key = environment[Cache::KEY]
|
58
|
+
environment[Cache::KEY] = [key, user_agent_key].join(":")
|
40
59
|
@application.call(environment)
|
41
60
|
end
|
42
61
|
end
|
43
62
|
|
63
|
+
# This is a middleware that provides page cache.
|
64
|
+
#
|
65
|
+
# This stores page contents into a groonga
|
66
|
+
# database. A groonga database can access by multi
|
67
|
+
# process. It means that your Rack application processes
|
68
|
+
# can share the same cache. For example, Passenger runs
|
69
|
+
# your Rack application with multi processes.
|
70
|
+
#
|
71
|
+
# Cache key is the request URL by default. It can be
|
72
|
+
# customized by env[Racknga::Cache::KEY]. For example,
|
73
|
+
# Racknga::Middleware::PerUserAgentCache and
|
74
|
+
# Racknga::Middleware::JSONP use it.
|
75
|
+
#
|
76
|
+
# This only caches the following responses:
|
77
|
+
# * 200 status response.
|
78
|
+
# * text/*, */json, */xml or */*+xml content type response.
|
79
|
+
#
|
80
|
+
# Usage:
|
81
|
+
# use Racnkga::Middleware::Cache, :database_path => "var/cache/db"
|
82
|
+
# run YourApplication
|
83
|
+
#
|
84
|
+
# @see Racknga::Middleware::PerUserAgentCache
|
85
|
+
# @see Racknga::Middleware::JSONP
|
86
|
+
# @see Racknga::Middleware::Deflater
|
87
|
+
# @see Racknga::CacheDatabase
|
44
88
|
class Cache
|
45
|
-
|
46
|
-
|
89
|
+
KEY = "racknga.cache.key"
|
90
|
+
START_TIME = "racknga.cache.start_time"
|
47
91
|
|
92
|
+
# @return [Racknga::CacheDatabase] the database used
|
93
|
+
# by this middleware.
|
94
|
+
attr_reader :database
|
95
|
+
|
96
|
+
# @option options [String] :database_path the database
|
97
|
+
# path to be stored caches.
|
48
98
|
def initialize(application, options={})
|
49
99
|
@application = application
|
50
100
|
@options = Utils.normalize_options(options || {})
|
@@ -53,12 +103,13 @@ module Racknga
|
|
53
103
|
@database = CacheDatabase.new(database_path)
|
54
104
|
end
|
55
105
|
|
106
|
+
# For Rack.
|
56
107
|
def call(environment)
|
57
108
|
request = Rack::Request.new(environment)
|
58
109
|
return @application.call(environment) unless use_cache?(request)
|
59
110
|
age = @database.configuration.age
|
60
|
-
key = environment[
|
61
|
-
environment[
|
111
|
+
key = normalize_key(environment[KEY] || request.fullpath)
|
112
|
+
environment[START_TIME] = Time.now
|
62
113
|
cache = @database.responses
|
63
114
|
record = cache[key]
|
64
115
|
if record and record.age == age
|
@@ -68,10 +119,12 @@ module Racknga
|
|
68
119
|
end
|
69
120
|
end
|
70
121
|
|
122
|
+
# ensures creating cache database.
|
71
123
|
def ensure_database
|
72
124
|
@database.ensure_database
|
73
125
|
end
|
74
126
|
|
127
|
+
# close the cache database.
|
75
128
|
def close_database
|
76
129
|
@database.close_database
|
77
130
|
end
|
@@ -97,6 +150,14 @@ module Racknga
|
|
97
150
|
true
|
98
151
|
end
|
99
152
|
|
153
|
+
def normalize_key(key)
|
154
|
+
if key.size > 4096
|
155
|
+
Digest::SHA1.hexdigest(key).force_encoding("ASCII-8BIT")
|
156
|
+
else
|
157
|
+
key
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
100
161
|
def handle_request(cache, key, age, request)
|
101
162
|
status, headers, body = @application.call(request.env)
|
102
163
|
if skip_caching_response?(status, headers, body)
|
@@ -142,13 +203,13 @@ module Racknga
|
|
142
203
|
end
|
143
204
|
|
144
205
|
def compute_checksum(status, encoded_headers, encoded_body)
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
206
|
+
checksum = Digest::SHA1.new
|
207
|
+
checksum << status.to_s
|
208
|
+
checksum << ":"
|
209
|
+
checksum << encoded_headers
|
210
|
+
checksum << ":"
|
211
|
+
checksum << encoded_body
|
212
|
+
checksum.hexdigest.force_encoding("ASCII-8BIT")
|
152
213
|
end
|
153
214
|
|
154
215
|
def valid_cache?(status, encoded_headers, encoded_body, checksum)
|
@@ -18,6 +18,42 @@
|
|
18
18
|
|
19
19
|
module Racknga
|
20
20
|
module Middleware
|
21
|
+
# This is a middleware that deflates response except for
|
22
|
+
# IE6. If your Rack application need support IE6, use
|
23
|
+
# this middleware instead of Rack::Deflater.
|
24
|
+
#
|
25
|
+
# Usage:
|
26
|
+
# require "racknga"
|
27
|
+
#
|
28
|
+
# use Racknga::Middleware::Deflater
|
29
|
+
# run YourApplication
|
30
|
+
#
|
31
|
+
# You can use this middleware with
|
32
|
+
# Racknga::Middleware::Cache. You *should* use this
|
33
|
+
# middleware before the cache middleware:
|
34
|
+
# use Racknga::Middleawre::Deflater
|
35
|
+
# use Racknga::Middleawre::Cache, :database_path => "var/cache/db"
|
36
|
+
# run YourApplication
|
37
|
+
#
|
38
|
+
# If you use this middleware after the cache middleware,
|
39
|
+
# you get two problems. It's the first problem pattern
|
40
|
+
# that the cache middleware may return deflated response
|
41
|
+
# to IE6. It's the second problem pattern that the cache
|
42
|
+
# middleware may return not deflated response to no IE6
|
43
|
+
# user agent. Here are examples:
|
44
|
+
#
|
45
|
+
# Problem case:
|
46
|
+
# use Racknga::Middleawre::Cache, :database_path => "var/cache/db"
|
47
|
+
# use Racknga::Middleawre::Deflater
|
48
|
+
# run YourApplication
|
49
|
+
#
|
50
|
+
# Problem pattern1:
|
51
|
+
# http://localhost:9292/ by Firefox -> no cache. cache deflated response.
|
52
|
+
# http://localhost:9292/ by IE6 -> use deflated response cache.
|
53
|
+
#
|
54
|
+
# Problem pattern2:
|
55
|
+
# http://localhost:9292/ by IE6 -> no cache. cache not deflated response.
|
56
|
+
# http://localhost:9292/ by Firefox -> use not deflated response cache.
|
21
57
|
class Deflater
|
22
58
|
def initialize(application, options={})
|
23
59
|
@application = application
|
@@ -25,6 +61,7 @@ module Racknga
|
|
25
61
|
@options = Utils.normalize_options(options || {})
|
26
62
|
end
|
27
63
|
|
64
|
+
# For Rack.
|
28
65
|
def call(environment)
|
29
66
|
if ie6?(environment)
|
30
67
|
@application.call(environment)
|
@@ -20,6 +20,22 @@ require 'racknga/exception_mail_notifier'
|
|
20
20
|
|
21
21
|
module Racknga
|
22
22
|
module Middleware
|
23
|
+
# This is a middleware that mails exception details on
|
24
|
+
# error. It's useful for finding your Rack application
|
25
|
+
# troubles.
|
26
|
+
#
|
27
|
+
# Usage:
|
28
|
+
# require "racknga"
|
29
|
+
# require "racknga/middleware/exception_notifier"
|
30
|
+
#
|
31
|
+
# notifier_options = {
|
32
|
+
# :subject_label => "[YourApplication]",
|
33
|
+
# :from => "reporter@example.com",
|
34
|
+
# :to => "maintainers@example.com",
|
35
|
+
# }
|
36
|
+
# notifiers = [Racknga::ExceptionMailNotifier.new(notifier_options)]
|
37
|
+
# use Racknga::Middleware::ExceptionNotifier, :notifiers => notifiers
|
38
|
+
# run YourApplication
|
23
39
|
class ExceptionNotifier
|
24
40
|
def initialize(application, options={})
|
25
41
|
@application = application
|
@@ -27,6 +43,7 @@ module Racknga
|
|
27
43
|
@notifiers = @options[:notifiers] || []
|
28
44
|
end
|
29
45
|
|
46
|
+
# For Rack.
|
30
47
|
def call(environment)
|
31
48
|
@application.call(environment)
|
32
49
|
rescue Exception => exception
|
@@ -0,0 +1,134 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
#
|
3
|
+
# Copyright (C) 2011 Ryo Onodera <onodera@clear-code.com>
|
4
|
+
#
|
5
|
+
# This library is free software; you can redistribute it and/or
|
6
|
+
# modify it under the terms of the GNU Lesser General Public
|
7
|
+
# License as published by the Free Software Foundation; either
|
8
|
+
# version 2.1 of the License, or (at your option) any later version.
|
9
|
+
#
|
10
|
+
# This library is distributed in the hope that it will be useful,
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
13
|
+
# Lesser General Public License for more details.
|
14
|
+
#
|
15
|
+
# You should have received a copy of the GNU Lesser General Public
|
16
|
+
# License along with this library; if not, write to the Free Software
|
17
|
+
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
18
|
+
|
19
|
+
module Racknga
|
20
|
+
module Middleware
|
21
|
+
# This is a middleware that adds "X-Responsed-By" header
|
22
|
+
# to responses. It's useful to determine responded
|
23
|
+
# server when your Rack applications are deployed behind
|
24
|
+
# load balancers.
|
25
|
+
#
|
26
|
+
# Usage:
|
27
|
+
# require "racknga"
|
28
|
+
# use Racknga::Middleware::InstanceName
|
29
|
+
# run YourApplication
|
30
|
+
class InstanceName
|
31
|
+
attr_reader :header
|
32
|
+
def initialize(application, options={})
|
33
|
+
@application = application
|
34
|
+
@options = options
|
35
|
+
|
36
|
+
@header = construct_header.freeze
|
37
|
+
@headers = construct_headers.freeze
|
38
|
+
end
|
39
|
+
|
40
|
+
# For Rack.
|
41
|
+
def call(environment)
|
42
|
+
response = @application.call(environment).to_a
|
43
|
+
|
44
|
+
[
|
45
|
+
response[0],
|
46
|
+
response[1].merge(@headers),
|
47
|
+
response[2],
|
48
|
+
]
|
49
|
+
end
|
50
|
+
|
51
|
+
def application_name
|
52
|
+
@options[:application_name] || @application.class.name
|
53
|
+
end
|
54
|
+
|
55
|
+
def version
|
56
|
+
@options[:version]
|
57
|
+
end
|
58
|
+
|
59
|
+
def revision
|
60
|
+
`git describe --abbrev=7 HEAD`.strip # XXX be SCM-agonostic
|
61
|
+
end
|
62
|
+
|
63
|
+
def server
|
64
|
+
`hostname`.strip
|
65
|
+
end
|
66
|
+
|
67
|
+
def user
|
68
|
+
`id --user --name`.strip
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
DEFAULT_HEADER_NAME = "X-Responsed-By"
|
73
|
+
def header_name
|
74
|
+
@options[:header_name] || DEFAULT_HEADER_NAME
|
75
|
+
end
|
76
|
+
|
77
|
+
def construct_headers
|
78
|
+
{
|
79
|
+
header_name => header,
|
80
|
+
}
|
81
|
+
end
|
82
|
+
|
83
|
+
def construct_header
|
84
|
+
format_header(format_application_name(application_name),
|
85
|
+
format_version(version),
|
86
|
+
format_revision(revision),
|
87
|
+
format_server(server),
|
88
|
+
format_user(user))
|
89
|
+
end
|
90
|
+
|
91
|
+
def format_header(*arguments)
|
92
|
+
arguments.compact.join(" ")
|
93
|
+
end
|
94
|
+
|
95
|
+
def format_application_name(name)
|
96
|
+
format_if_possible(name) do
|
97
|
+
"#{name}"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def format_version(version)
|
102
|
+
format_if_possible(version) do
|
103
|
+
"v#{version}"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def format_revision(revision)
|
108
|
+
format_if_possible(revision) do
|
109
|
+
"(at #{revision})"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def format_server(server)
|
114
|
+
format_if_possible(server) do
|
115
|
+
"on #{server}"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def format_user(user)
|
120
|
+
format_if_possible(user) do
|
121
|
+
"by #{user}"
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def format_if_possible(data)
|
126
|
+
if data and (data.respond_to?(:to_s) and not data.to_s.empty?)
|
127
|
+
result = yield
|
128
|
+
end
|
129
|
+
|
130
|
+
result
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -18,26 +18,87 @@
|
|
18
18
|
|
19
19
|
module Racknga
|
20
20
|
module Middleware
|
21
|
+
# This is a middleware that provides JSONP support.
|
22
|
+
#
|
23
|
+
# If you use this middleware, your Rack application just
|
24
|
+
# returns JSON response.
|
25
|
+
#
|
26
|
+
# Usage:
|
27
|
+
# require "racknga"
|
28
|
+
#
|
29
|
+
# use Rack::ContentLength
|
30
|
+
# use Racknga::Middleware::JSONP
|
31
|
+
# json_application = Proc.new do |env|
|
32
|
+
# [200,
|
33
|
+
# {"Content-Type" => "application/json"},
|
34
|
+
# ['{"Hello": "World"}']]
|
35
|
+
# end
|
36
|
+
# run json_application
|
37
|
+
#
|
38
|
+
# Results:
|
39
|
+
# % curl 'http://localhost:9292/'
|
40
|
+
# {"Hello": "World"}
|
41
|
+
# % curl 'http://localhost:9292/?callback=function'
|
42
|
+
# function({"Hello": "World"})
|
43
|
+
#
|
44
|
+
# You can use this middleware with
|
45
|
+
# Racknga::Middleware::Cache. You *should* use this
|
46
|
+
# middleware before the cache middleware:
|
47
|
+
# use Racknga::Middleawre::JSONP
|
48
|
+
# use Racknga::Middleawre::Cache, :database_path => "var/cache/db"
|
49
|
+
# run YourApplication
|
50
|
+
#
|
51
|
+
# If you use this middleware after the cache middleware,
|
52
|
+
# the cache middleware will cache many responses that
|
53
|
+
# just only differ callback parameter value. Here are
|
54
|
+
# examples:
|
55
|
+
#
|
56
|
+
# Recommended case:
|
57
|
+
# use Racknga::Middleawre::JSONP
|
58
|
+
# use Racknga::Middleawre::Cache, :database_path => "var/cache/db"
|
59
|
+
# run YourApplication
|
60
|
+
#
|
61
|
+
# Requests:
|
62
|
+
# http://localhost:9292/ -> no cache. cached.
|
63
|
+
# http://localhost:9292/?callback=function1 -> use cache.
|
64
|
+
# http://localhost:9292/?callback=function2 -> use cache.
|
65
|
+
# http://localhost:9292/?callback=function3 -> use cache.
|
66
|
+
# http://localhost:9292/?callback=function1 -> use cache.
|
67
|
+
#
|
68
|
+
# Not recommended case:
|
69
|
+
# use Racknga::Middleawre::Cache, :database_path => "var/cache/db"
|
70
|
+
# use Racknga::Middleawre::JSONP
|
71
|
+
# run YourApplication
|
72
|
+
#
|
73
|
+
# Requests:
|
74
|
+
# http://localhost:9292/ -> no cache. cached.
|
75
|
+
# http://localhost:9292/?callback=function1 -> no cache. cached.
|
76
|
+
# http://localhost:9292/?callback=function2 -> no cache. cached.
|
77
|
+
# http://localhost:9292/?callback=function3 -> no cache. cached.
|
78
|
+
# http://localhost:9292/?callback=function1 -> use cache.
|
21
79
|
class JSONP
|
22
80
|
def initialize(application)
|
23
81
|
@application = application
|
24
82
|
end
|
25
83
|
|
84
|
+
# For Rack.
|
26
85
|
def call(environment)
|
27
86
|
request = Rack::Request.new(environment)
|
28
87
|
callback = request["callback"]
|
29
88
|
update_cache_key(request) if callback
|
30
89
|
status, headers, body = @application.call(environment)
|
31
90
|
return [status, headers, body] unless callback
|
32
|
-
|
91
|
+
header_hash = Rack::Utils::HeaderHash.new(headers)
|
92
|
+
return [status, headers, body] unless json_response?(header_hash)
|
33
93
|
body = Writer.new(callback, body)
|
34
|
-
|
94
|
+
update_content_type(header_hash)
|
95
|
+
[status, header_hash, body]
|
35
96
|
end
|
36
97
|
|
37
98
|
private
|
38
99
|
def update_cache_key(request)
|
39
100
|
return unless Middleware.const_defined?(:Cache)
|
40
|
-
|
101
|
+
cache_key = Cache::KEY
|
41
102
|
|
42
103
|
path = request.fullpath
|
43
104
|
path, parameters = path.split(/\?/, 2)
|
@@ -49,17 +110,28 @@ module Racknga
|
|
49
110
|
path << "?" << parameters unless parameters.empty?
|
50
111
|
end
|
51
112
|
|
52
|
-
key = request.env[
|
53
|
-
request.env[
|
113
|
+
key = request.env[cache_key]
|
114
|
+
request.env[cache_key] = [key, path].compact.join(":")
|
54
115
|
end
|
55
116
|
|
56
|
-
def json_response?(
|
57
|
-
content_type =
|
58
|
-
|
59
|
-
|
60
|
-
|
117
|
+
def json_response?(header_hash)
|
118
|
+
content_type = header_hash["Content-Type"]
|
119
|
+
media_type = content_type.split(/\s*;\s*/, 2).first.downcase
|
120
|
+
media_type == "application/json" or
|
121
|
+
media_type == "application/javascript" or
|
122
|
+
media_type == "text/javascript"
|
61
123
|
end
|
62
124
|
|
125
|
+
def update_content_type(header_hash)
|
126
|
+
content_type = header_hash["Content-Type"]
|
127
|
+
media_type, parameters = content_type.split(/\s*;\s*/, 2)
|
128
|
+
# We should use application/javascript not
|
129
|
+
# text/javascript when all IE <= 8 are deprecated. :<
|
130
|
+
updated_content_type = ["text/javascript", parameters].compact.join("; ")
|
131
|
+
header_hash["Content-Type"] = updated_content_type
|
132
|
+
end
|
133
|
+
|
134
|
+
# @private
|
63
135
|
class Writer
|
64
136
|
def initialize(callback, body)
|
65
137
|
@callback = callback
|