htttee 0.5.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -2
- data/.travis.yml +6 -0
- data/README.md +13 -1
- data/bin/htttee +2 -2
- data/bin/htttee-exec +8 -7
- data/config.ru +1 -1
- data/htttee.gemspec +4 -3
- data/lib/htttee/client.rb +4 -6
- data/lib/htttee/client/consumer.rb +38 -34
- data/lib/htttee/server.rb +45 -39
- data/lib/htttee/server/api.rb +155 -139
- data/lib/htttee/server/chunked_body.rb +13 -15
- data/lib/htttee/server/middleware/async_fixer.rb +12 -15
- data/lib/htttee/server/middleware/dechunker.rb +44 -45
- data/lib/htttee/server/middleware/rechunker.rb +15 -0
- data/lib/htttee/server/mock.rb +44 -46
- data/lib/htttee/server/pubsub_redis.rb +4 -0
- data/lib/htttee/server/sse/body.rb +71 -0
- data/lib/htttee/server/sse/middleware.rb +96 -0
- data/lib/htttee/version.rb +2 -4
- data/spec/client_spec.rb +102 -2
- data/spec/helpers/rackup.rb +1 -1
- data/spec/spec_helper.rb +3 -3
- metadata +72 -46
- data/deploy/after_restart.rb +0 -1
- data/deploy/before_restart.rb +0 -4
- data/deploy/before_symlink.rb +0 -2
- data/deploy/cookbooks/cloudkick-plugins/recipes/default.rb +0 -8
- data/deploy/cookbooks/cloudkick-plugins/recipes/resque.rb +0 -18
- data/deploy/cookbooks/cloudkick-plugins/templates/default/resque.sh.erb +0 -4
- data/deploy/cookbooks/god/recipes/default.rb +0 -50
- data/deploy/cookbooks/god/templates/default/config.erb +0 -3
- data/deploy/cookbooks/god/templates/default/god-inittab.erb +0 -3
- data/deploy/cookbooks/main/attributes/owner_name.rb +0 -3
- data/deploy/cookbooks/main/attributes/recipes.rb +0 -1
- data/deploy/cookbooks/main/libraries/dnapi.rb +0 -7
- data/deploy/cookbooks/main/recipes/default.rb +0 -2
- data/deploy/cookbooks/nginx/files/default/chunkin-nginx-module-v0.22rc1.zip +0 -0
- data/deploy/cookbooks/nginx/files/default/nginx-1.0.0.tar.gz +0 -0
- data/deploy/cookbooks/nginx/recipes/default.rb +0 -2
- data/deploy/cookbooks/nginx/recipes/install.rb +0 -0
- data/deploy/cookbooks/nginx/templates/default/nginx.conf.erb +0 -41
- data/deploy/cookbooks/resque/recipes/default.rb +0 -47
- data/deploy/cookbooks/resque/templates/default/resque.rb.erb +0 -73
- data/deploy/cookbooks/resque/templates/default/resque.yml.erb +0 -3
- data/deploy/cookbooks/resque/templates/default/resque_scheduler.rb.erb +0 -73
- data/deploy/solo.rb +0 -7
@@ -1,21 +1,19 @@
|
|
1
|
-
module
|
2
|
-
module
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
@body_callback.call(chunk(fragment))
|
8
|
-
end
|
1
|
+
module HTTTee
|
2
|
+
module Server
|
3
|
+
class ChunkedBody < Thin::DeferrableBody
|
4
|
+
def call(body)
|
5
|
+
body.each do |fragment|
|
6
|
+
@body_callback.call(chunk(fragment))
|
9
7
|
end
|
8
|
+
end
|
10
9
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
10
|
+
def succeed(*a)
|
11
|
+
@body_callback.call("0\r\n\r\n")
|
12
|
+
super
|
13
|
+
end
|
15
14
|
|
16
|
-
|
17
|
-
|
18
|
-
end
|
15
|
+
def chunk(fragment)
|
16
|
+
"#{fragment.size.to_s(16)}\r\n#{fragment}\r\n"
|
19
17
|
end
|
20
18
|
end
|
21
19
|
end
|
@@ -1,20 +1,17 @@
|
|
1
|
+
module HTTTee
|
2
|
+
module Server
|
3
|
+
class AsyncFixer
|
4
|
+
def initialize(app)
|
5
|
+
@app = app
|
6
|
+
end
|
1
7
|
|
2
|
-
|
3
|
-
|
4
|
-
module Server
|
5
|
-
class AsyncFixer
|
6
|
-
def initialize(app)
|
7
|
-
@app = app
|
8
|
-
end
|
9
|
-
|
10
|
-
def call(env)
|
11
|
-
tuple = @app.call(env)
|
8
|
+
def call(env)
|
9
|
+
tuple = @app.call(env)
|
12
10
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
end
|
11
|
+
if tuple.first == -1
|
12
|
+
Thin::Connection::AsyncResponse
|
13
|
+
else
|
14
|
+
tuple
|
18
15
|
end
|
19
16
|
end
|
20
17
|
end
|
@@ -1,59 +1,58 @@
|
|
1
|
-
module
|
2
|
-
module
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
end
|
1
|
+
module HTTTee
|
2
|
+
module Server
|
3
|
+
class Dechunker
|
4
|
+
def initialize(app)
|
5
|
+
@app = app
|
6
|
+
end
|
8
7
|
|
9
|
-
|
10
|
-
|
8
|
+
def call(env)
|
9
|
+
env['rack.input'] = ChunkedBody.new(env['rack.input']) if chunked?(env)
|
11
10
|
|
12
|
-
|
13
|
-
|
11
|
+
@app.call(env)
|
12
|
+
end
|
14
13
|
|
15
|
-
|
16
|
-
|
17
|
-
|
14
|
+
def chunked?(env)
|
15
|
+
env['HTTP_TRANSFER_ENCODING'] == 'chunked'
|
16
|
+
end
|
18
17
|
|
19
|
-
|
20
|
-
|
18
|
+
class ChunkedBody
|
19
|
+
extend Forwardable
|
21
20
|
|
22
|
-
|
21
|
+
CRLF = "\r\n"
|
23
22
|
|
24
|
-
|
23
|
+
attr_reader :input
|
25
24
|
|
26
|
-
|
25
|
+
def_delegators :input, :callback, :errback
|
27
26
|
|
28
|
-
|
29
|
-
|
30
|
-
|
27
|
+
def initialize(input)
|
28
|
+
@input, @buffer = input, ''
|
29
|
+
end
|
31
30
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
end
|
31
|
+
def each(&blk)
|
32
|
+
@input.each do |chunk|
|
33
|
+
dechunk(chunk, &blk)
|
36
34
|
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def dechunk(chunk, &blk)
|
38
|
+
@buffer << chunk
|
39
|
+
|
40
|
+
loop do
|
41
|
+
return unless @buffer[CRLF]
|
42
|
+
|
43
|
+
string_length, remainder = @buffer.split(CRLF, 2)
|
44
|
+
length = string_length.to_i(16)
|
45
|
+
|
46
|
+
if length == 0
|
47
|
+
@buffer = ''
|
48
|
+
@input.succeed
|
49
|
+
return
|
50
|
+
elsif remainder.size >= length + 2 # length + CRLF
|
51
|
+
data, @buffer = remainder[0...length], remainder[(length+2)..-1]
|
37
52
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
loop do
|
42
|
-
return unless @buffer[CRLF]
|
43
|
-
|
44
|
-
string_length, remainder = @buffer.split(CRLF, 2)
|
45
|
-
length = string_length.to_i(16)
|
46
|
-
|
47
|
-
if length == 0
|
48
|
-
@buffer = ''
|
49
|
-
@input.succeed
|
50
|
-
return
|
51
|
-
elsif remainder.size >= length + 2 # length + CRLF
|
52
|
-
data, @buffer = remainder.split(CRLF, 2)
|
53
|
-
blk.call(data)
|
54
|
-
else
|
55
|
-
return
|
56
|
-
end
|
53
|
+
blk.call(data)
|
54
|
+
else
|
55
|
+
return
|
57
56
|
end
|
58
57
|
end
|
59
58
|
end
|
data/lib/htttee/server/mock.rb
CHANGED
@@ -1,66 +1,64 @@
|
|
1
1
|
require 'rack/mux'
|
2
2
|
|
3
|
-
module
|
4
|
-
module
|
5
|
-
module
|
6
|
-
module Mock
|
3
|
+
module HTTTee
|
4
|
+
module Server
|
5
|
+
module Mock
|
7
6
|
|
8
|
-
|
9
|
-
|
7
|
+
def self.boot_forking_server
|
8
|
+
o,i = IO.pipe
|
10
9
|
|
11
|
-
|
12
|
-
|
10
|
+
if pid = fork
|
11
|
+
at_exit { Process.kill(:SIGTERM, pid) }
|
13
12
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
end
|
13
|
+
i.close
|
14
|
+
URI.parse(o.read)
|
15
|
+
else
|
16
|
+
o.close
|
17
|
+
process_child(i)
|
20
18
|
end
|
19
|
+
end
|
21
20
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
uri = client.get("/mux-uri").body
|
27
|
-
i << uri
|
28
|
-
i.close
|
29
|
-
end
|
21
|
+
def self.process_child(i)
|
22
|
+
EM.run do
|
23
|
+
client = Rack::Client.new { run HTTTee::Server.mock_app }
|
30
24
|
|
31
|
-
|
25
|
+
uri = client.get("/mux-uri").body
|
26
|
+
i << uri
|
27
|
+
i.close
|
32
28
|
end
|
33
29
|
|
34
|
-
|
35
|
-
|
36
|
-
@app = Rack::Mux.new(async_safe(app), thin_options)
|
37
|
-
end
|
30
|
+
exit
|
31
|
+
end
|
38
32
|
|
39
|
-
|
40
|
-
|
41
|
-
|
33
|
+
class ThinMuxer
|
34
|
+
def initialize(app)
|
35
|
+
@app = Rack::Mux.new(async_safe(app), thin_options)
|
36
|
+
end
|
42
37
|
|
43
|
-
|
44
|
-
|
45
|
-
|
38
|
+
def call(env)
|
39
|
+
@app.call(env)
|
40
|
+
end
|
46
41
|
|
47
|
-
|
48
|
-
|
49
|
-
|
42
|
+
def async_safe(app)
|
43
|
+
AsyncFixer.new(app)
|
44
|
+
end
|
45
|
+
|
46
|
+
def thin_options
|
47
|
+
{ :server => Thin, :environment => 'none' }
|
50
48
|
end
|
49
|
+
end
|
51
50
|
|
52
|
-
|
51
|
+
class EchoUri
|
53
52
|
|
54
|
-
|
55
|
-
|
56
|
-
|
53
|
+
def initialize(app)
|
54
|
+
@app = app
|
55
|
+
end
|
57
56
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
end
|
57
|
+
def call(env)
|
58
|
+
if env['PATH_INFO'] == '/mux-uri'
|
59
|
+
[200, {'Content-Type' => 'text/plain'}, [env['X-Mux-Uri']]]
|
60
|
+
else
|
61
|
+
@app.call(env)
|
64
62
|
end
|
65
63
|
end
|
66
64
|
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module HTTTee
|
2
|
+
module Server
|
3
|
+
module SSE
|
4
|
+
class Body
|
5
|
+
include EventMachine::Deferrable
|
6
|
+
|
7
|
+
MESSAGE = 'message'
|
8
|
+
CTRL = 'ctrl'
|
9
|
+
EOF = 'eof'
|
10
|
+
|
11
|
+
CTRL_TABLE = {
|
12
|
+
"\n" => 'newline',
|
13
|
+
"\r" => 'carriage-return',
|
14
|
+
"\r \n" => 'crlf',
|
15
|
+
}
|
16
|
+
|
17
|
+
def initialize(body)
|
18
|
+
@body = body
|
19
|
+
end
|
20
|
+
|
21
|
+
def call(chunks)
|
22
|
+
chunks.each do |chunk|
|
23
|
+
send_chunk(chunk)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def send_chunk(chunk)
|
28
|
+
data, ctrl, remaining = chunk.partition(%r{\r \n|\n|\r})
|
29
|
+
|
30
|
+
send_data(data) unless data.empty?
|
31
|
+
send_ctrl(CTRL_TABLE[ctrl]) unless ctrl.empty?
|
32
|
+
send_chunk(remaining) unless remaining.empty?
|
33
|
+
end
|
34
|
+
|
35
|
+
def send_data(data)
|
36
|
+
send_event(MESSAGE)
|
37
|
+
|
38
|
+
send_message(data)
|
39
|
+
end
|
40
|
+
|
41
|
+
def send_ctrl(type)
|
42
|
+
send_event(CTRL)
|
43
|
+
|
44
|
+
send_message(type)
|
45
|
+
end
|
46
|
+
|
47
|
+
def send_message(data)
|
48
|
+
send "data", data, "\n\n"
|
49
|
+
end
|
50
|
+
|
51
|
+
def send_event(event)
|
52
|
+
send "event", event, "\n"
|
53
|
+
end
|
54
|
+
|
55
|
+
def send(type, data, term)
|
56
|
+
@body.call(["#{type}: #{data}#{term}"])
|
57
|
+
end
|
58
|
+
|
59
|
+
def succeed
|
60
|
+
send_ctrl(EOF)
|
61
|
+
|
62
|
+
@body.succeed
|
63
|
+
end
|
64
|
+
|
65
|
+
def each(&blk)
|
66
|
+
@body.each(&blk)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module HTTTee
|
2
|
+
module Server
|
3
|
+
module SSE
|
4
|
+
class Middleware
|
5
|
+
def initialize(app)
|
6
|
+
@app = app
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(env)
|
10
|
+
if sse_request?(env)
|
11
|
+
wrap(env)
|
12
|
+
elsif sse_supported?(env)
|
13
|
+
return sse_shell
|
14
|
+
end
|
15
|
+
|
16
|
+
@app.call(env)
|
17
|
+
end
|
18
|
+
|
19
|
+
def wrap(env)
|
20
|
+
env['rack.response_body'] = SSE::Body.new(env['rack.response_body'])
|
21
|
+
|
22
|
+
cb = env['async.callback']
|
23
|
+
|
24
|
+
env['async.callback'] = lambda do |(status, headers, body)|
|
25
|
+
cb.call([status, headers.merge('Content-Type' => 'text/event-stream', 'Connection' => 'Keep-Alive'), body])
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def sse_request?(env)
|
30
|
+
env['HTTP_ACCEPT'] == 'text/event-stream'
|
31
|
+
end
|
32
|
+
|
33
|
+
def sse_supported?(env)
|
34
|
+
env['HTTP_USER_AGENT'].to_s =~ /WebKit/ &&
|
35
|
+
env['HTTP_ACCEPT'] =~ %r{text/html}
|
36
|
+
end
|
37
|
+
|
38
|
+
def sse_shell
|
39
|
+
[200, {'Content-Type' => 'text/html'}, [<<-HTML]]
|
40
|
+
<!DOCTYPE html>
|
41
|
+
<html>
|
42
|
+
<head>
|
43
|
+
<meta charset="utf-8" />
|
44
|
+
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.10.1/jquery.min.js"></script>
|
45
|
+
</head>
|
46
|
+
<body><pre><code></code></pre>
|
47
|
+
<script>
|
48
|
+
var source = new EventSource(window.location.pathname);
|
49
|
+
var body = $("html,body");
|
50
|
+
var cursor = $("pre code")[0];
|
51
|
+
var resetLine = false;
|
52
|
+
|
53
|
+
function append(data) {
|
54
|
+
if(resetLine == true) {
|
55
|
+
cursor.innerHTML = data;
|
56
|
+
} else {
|
57
|
+
cursor.innerHTML += data;
|
58
|
+
}
|
59
|
+
|
60
|
+
resetLine = false;
|
61
|
+
}
|
62
|
+
|
63
|
+
source.onmessage = function(e) {
|
64
|
+
append(e.data);
|
65
|
+
};
|
66
|
+
|
67
|
+
source.onerror = function(e) {
|
68
|
+
console.log(e);
|
69
|
+
};
|
70
|
+
|
71
|
+
source.addEventListener('ctrl', function(e) {
|
72
|
+
if(e.data == 'newline') {
|
73
|
+
append("\\n");
|
74
|
+
cursor = $("<code></code>").insertAfter(cursor)[0];
|
75
|
+
} else if(e.data == 'crlf' || e.data == 'carriage-return') {
|
76
|
+
resetLine = true;
|
77
|
+
} else if(e.data == 'eof') {
|
78
|
+
source.close();
|
79
|
+
} else {
|
80
|
+
console.log(e);
|
81
|
+
}
|
82
|
+
}, false);
|
83
|
+
|
84
|
+
window.setInterval(function(){
|
85
|
+
window.scrollTo(0, document.body.scrollHeight);
|
86
|
+
}, 100);
|
87
|
+
|
88
|
+
</script>
|
89
|
+
</body>
|
90
|
+
</html>
|
91
|
+
HTML
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|