control_tower 1.0
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.
- data/bin/control_tower +54 -0
- data/ext/CTParser/CTParser.h +28 -0
- data/ext/CTParser/CTParser.m +217 -0
- data/ext/CTParser/extconf.rb +3 -0
- data/ext/CTParser/http11_parser.c +1086 -0
- data/ext/CTParser/http11_parser.h +49 -0
- data/lib/control_tower.rb +9 -0
- data/lib/control_tower/rack_socket.rb +181 -0
- data/lib/control_tower/server.rb +32 -0
- data/lib/rack/handler/control_tower.rb +17 -0
- metadata +83 -0
@@ -0,0 +1,49 @@
|
|
1
|
+
/**
|
2
|
+
* Copyright (c) 2005 Zed A. Shaw
|
3
|
+
* You can redistribute it and/or modify it under the same terms as Ruby.
|
4
|
+
*/
|
5
|
+
|
6
|
+
#ifndef http11_parser_h
|
7
|
+
#define http11_parser_h
|
8
|
+
|
9
|
+
#include <sys/types.h>
|
10
|
+
|
11
|
+
#if defined(_WIN32)
|
12
|
+
#include <stddef.h>
|
13
|
+
#endif
|
14
|
+
|
15
|
+
typedef void (*element_cb)(void *data, const char *at, size_t length);
|
16
|
+
typedef void (*field_cb)(void *data, const char *field, size_t flen, const char *value, size_t vlen);
|
17
|
+
|
18
|
+
typedef struct http_parser {
|
19
|
+
int cs;
|
20
|
+
size_t body_start;
|
21
|
+
int content_len;
|
22
|
+
size_t nread;
|
23
|
+
size_t mark;
|
24
|
+
size_t field_start;
|
25
|
+
size_t field_len;
|
26
|
+
size_t query_start;
|
27
|
+
|
28
|
+
void *data;
|
29
|
+
|
30
|
+
field_cb http_field;
|
31
|
+
element_cb request_method;
|
32
|
+
element_cb request_uri;
|
33
|
+
element_cb fragment;
|
34
|
+
element_cb request_path;
|
35
|
+
element_cb query_string;
|
36
|
+
element_cb http_version;
|
37
|
+
element_cb header_done;
|
38
|
+
|
39
|
+
} http_parser;
|
40
|
+
|
41
|
+
int http_parser_init(http_parser *parser);
|
42
|
+
int http_parser_finish(http_parser *parser);
|
43
|
+
size_t http_parser_execute(http_parser *parser, const char *data, size_t len, size_t off);
|
44
|
+
int http_parser_has_error(http_parser *parser);
|
45
|
+
int http_parser_is_finished(http_parser *parser);
|
46
|
+
|
47
|
+
#define http_parser_nread(parser) (parser)->nread
|
48
|
+
|
49
|
+
#endif
|
@@ -0,0 +1,9 @@
|
|
1
|
+
# This file is covered by the Ruby license. See COPYING for more details.
|
2
|
+
# Copyright (C) 2009-2010, Apple Inc. All rights reserved.
|
3
|
+
|
4
|
+
require 'socket'
|
5
|
+
require 'tempfile'
|
6
|
+
$: << File.join(File.dirname(__FILE__), 'control_tower', 'vendor')
|
7
|
+
require 'rack'
|
8
|
+
require File.join(File.dirname(__FILE__), 'control_tower', 'rack_socket')
|
9
|
+
require File.join(File.dirname(__FILE__), 'control_tower', 'server')
|
@@ -0,0 +1,181 @@
|
|
1
|
+
# This file is covered by the Ruby license. See COPYING for more details.
|
2
|
+
# Copyright (C) 2009-2010, Apple Inc. All rights reserved.
|
3
|
+
|
4
|
+
framework 'Foundation'
|
5
|
+
require 'CTParser'
|
6
|
+
require 'stringio'
|
7
|
+
|
8
|
+
CTParser # Making sure the Objective-C class is pre-loaded
|
9
|
+
|
10
|
+
module ControlTower
|
11
|
+
class RackSocket
|
12
|
+
VERSION = [1,0].freeze
|
13
|
+
|
14
|
+
def initialize(host, port, server, concurrent)
|
15
|
+
@app = server.app
|
16
|
+
@socket = TCPServer.new(host, port)
|
17
|
+
@socket.listen(50)
|
18
|
+
@status = :closed # Start closed and give the server time to start
|
19
|
+
|
20
|
+
if concurrent
|
21
|
+
@multithread = true
|
22
|
+
@request_queue = Dispatch::Queue.concurrent
|
23
|
+
puts "Caution! Wake turbulance from heavy aircraft landing on parallel runway.\n(Parallel Request Action ENABLED!)"
|
24
|
+
else
|
25
|
+
@multithread = false
|
26
|
+
@request_queue = Dispatch::Queue.new('com.apple.ControlTower.rack_socket_queue')
|
27
|
+
end
|
28
|
+
@request_group = Dispatch::Group.new
|
29
|
+
end
|
30
|
+
|
31
|
+
def open
|
32
|
+
@status = :open
|
33
|
+
while (@status == :open)
|
34
|
+
connection = @socket.accept
|
35
|
+
|
36
|
+
@request_queue.async(@request_group) do
|
37
|
+
env = { 'rack.errors' => $stderr,
|
38
|
+
'rack.multiprocess' => false,
|
39
|
+
'rack.multithread' => @multithread,
|
40
|
+
'rack.run_once' => false,
|
41
|
+
'rack.version' => VERSION }
|
42
|
+
resp = nil
|
43
|
+
x_sendfile_header = 'X-Sendfile'
|
44
|
+
x_sendfile = nil
|
45
|
+
begin
|
46
|
+
request_data = parse!(connection, env)
|
47
|
+
if request_data
|
48
|
+
request_data['REMOTE_ADDR'] = connection.addr[3]
|
49
|
+
status, headers, body = @app.call(request_data)
|
50
|
+
|
51
|
+
# If there's an X-Sendfile header, we'll use sendfile(2)
|
52
|
+
if headers.has_key?(x_sendfile_header)
|
53
|
+
x_sendfile = headers[x_sendfile_header]
|
54
|
+
x_sendfile = ::File.open(x_sendfile, 'r') unless x_sendfile.kind_of? IO
|
55
|
+
x_sendfile_size = x_sendfile.stat.size
|
56
|
+
headers.delete(x_sendfile_header)
|
57
|
+
headers['Content-Length'] = x_sendfile_size
|
58
|
+
end
|
59
|
+
|
60
|
+
# Unless somebody's already set it for us (or we don't need it), set the Content-Length
|
61
|
+
unless (status == -1 ||
|
62
|
+
(status >= 100 and status <= 199) ||
|
63
|
+
status == 204 ||
|
64
|
+
status == 304 ||
|
65
|
+
headers.has_key?('Content-Length'))
|
66
|
+
headers['Content-Length'] = if body.respond_to?(:each)
|
67
|
+
size = 0
|
68
|
+
body.each { |x| size += x.bytesize }
|
69
|
+
size
|
70
|
+
else
|
71
|
+
body.bytesize
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# TODO -- We don't handle keep-alive connections yet
|
76
|
+
headers['Connection'] = 'close'
|
77
|
+
|
78
|
+
resp = "HTTP/1.1 #{status}\r\n"
|
79
|
+
headers.each do |header, value|
|
80
|
+
resp << "#{header}: #{value}\r\n"
|
81
|
+
end
|
82
|
+
resp << "\r\n"
|
83
|
+
|
84
|
+
# Start writing the response
|
85
|
+
connection.write resp
|
86
|
+
|
87
|
+
# Write the body
|
88
|
+
if x_sendfile
|
89
|
+
connection.sendfile(x_sendfile, 0, x_sendfile_size)
|
90
|
+
elsif body.respond_to?(:each)
|
91
|
+
body.each do |chunk|
|
92
|
+
connection.write chunk
|
93
|
+
end
|
94
|
+
else
|
95
|
+
connection.write body
|
96
|
+
end
|
97
|
+
|
98
|
+
else
|
99
|
+
$stderr.puts "Error: No request data received!"
|
100
|
+
end
|
101
|
+
rescue EOFError, Errno::ECONNRESET, Errno::EPIPE, Errno::EINVAL
|
102
|
+
$stderr.puts "Error: Connection terminated!"
|
103
|
+
rescue Object => e
|
104
|
+
if resp.nil? && !connection.closed?
|
105
|
+
connection.write "HTTP/1.1 400\r\n\r\n"
|
106
|
+
else
|
107
|
+
# We have a response, but there was trouble sending it:
|
108
|
+
$stderr.puts "Error: Problem transmitting data -- #{e.inspect}"
|
109
|
+
$stderr.puts e.backtrace.join("\n")
|
110
|
+
end
|
111
|
+
ensure
|
112
|
+
# We should clean up after our tempfile, if we used one.
|
113
|
+
input = env['rack.input']
|
114
|
+
input.unlink if input.class == Tempfile
|
115
|
+
connection.close rescue nil
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def close
|
122
|
+
@status = :close
|
123
|
+
|
124
|
+
# You get 30 seconds to empty the request queue and get outa here!
|
125
|
+
Dispatch::Source.timer(30, 0, 1, Dispatch::Queue.concurrent) do
|
126
|
+
$stderr.puts "Timed out waiting for connections to close"
|
127
|
+
exit 1
|
128
|
+
end
|
129
|
+
@request_group.wait
|
130
|
+
@socket.close
|
131
|
+
end
|
132
|
+
|
133
|
+
|
134
|
+
private
|
135
|
+
|
136
|
+
def parse!(connection, env)
|
137
|
+
parser = Thread.current[:http_parser] ||= CTParser.new
|
138
|
+
parser.reset
|
139
|
+
data = NSMutableData.alloc.init
|
140
|
+
data.increaseLengthBy(1) # add sentinel
|
141
|
+
parsing_headers = true # Parse headers first
|
142
|
+
nread = 0
|
143
|
+
content_length = 0
|
144
|
+
content_uploaded = 0
|
145
|
+
connection_handle = NSFileHandle.alloc.initWithFileDescriptor(connection.fileno)
|
146
|
+
|
147
|
+
while (parsing_headers || content_uploaded < content_length) do
|
148
|
+
# Read the availableData on the socket and give up if there's nothing
|
149
|
+
incoming_bytes = connection_handle.availableData
|
150
|
+
return nil if incoming_bytes.length == 0
|
151
|
+
|
152
|
+
# Until the headers are done being parsed, we'll parse them
|
153
|
+
if parsing_headers
|
154
|
+
data.setLength(data.length - 1) # Remove sentinel
|
155
|
+
data.appendData(incoming_bytes)
|
156
|
+
data.increaseLengthBy(1) # Add sentinel
|
157
|
+
nread = parser.parseData(data, forEnvironment: env, startingAt: nread)
|
158
|
+
if parser.finished == 1
|
159
|
+
parsing_headers = false # We're done, now on to receiving the body
|
160
|
+
content_length = env['CONTENT_LENGTH'].to_i
|
161
|
+
content_uploaded = env['rack.input'].length
|
162
|
+
end
|
163
|
+
else # Done parsing headers, now just collect request body:
|
164
|
+
content_uploaded += incoming_bytes.length
|
165
|
+
env['rack.input'].appendData(incoming_bytes)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
if content_length > 1024 * 1024
|
170
|
+
body_file = Tempfile.new('control-tower-request-body-')
|
171
|
+
NSFileHandle.alloc.initWithFileDescriptor(body_file.fileno).writeData(env['rack.input'])
|
172
|
+
body_file.rewind
|
173
|
+
env['rack.input'] = body_file
|
174
|
+
else
|
175
|
+
env['rack.input'] = StringIO.new(NSString.alloc.initWithData(env['rack.input'], encoding: NSASCIIStringEncoding))
|
176
|
+
end
|
177
|
+
# Returning what we've got...
|
178
|
+
return env
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# This file is covered by the Ruby license. See COPYING for more details.
|
2
|
+
# Copyright (C) 2009-2010, Apple Inc. All rights reserved.
|
3
|
+
|
4
|
+
module ControlTower
|
5
|
+
class Server
|
6
|
+
attr_reader :app
|
7
|
+
|
8
|
+
def initialize(app, options)
|
9
|
+
@app = app
|
10
|
+
parse_options(options)
|
11
|
+
@socket = RackSocket.new(@host, @port, self, @concurrent)
|
12
|
+
end
|
13
|
+
|
14
|
+
def start
|
15
|
+
trap 'INT' do
|
16
|
+
@socket.close
|
17
|
+
exit
|
18
|
+
end
|
19
|
+
|
20
|
+
# Ok, let the server do it's thing
|
21
|
+
@socket.open
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def parse_options(opt)
|
27
|
+
@port = (opt[:port] || 8080).to_i
|
28
|
+
@host = opt[:host] || `hostname`.chomp
|
29
|
+
@concurrent = opt[:concurrent]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# This file is covered by the Ruby license. See COPYING for more details.
|
2
|
+
# Copyright (C) 2009-2010, Apple Inc. All rights reserved.
|
3
|
+
|
4
|
+
require "control_tower"
|
5
|
+
|
6
|
+
module Rack
|
7
|
+
module Handler
|
8
|
+
class ControlTower
|
9
|
+
def self.run(app, options={})
|
10
|
+
app = Rack::Chunked.new(Rack::ContentLength.new(app))
|
11
|
+
server = ::ControlTower::Server.new(app, options)
|
12
|
+
yield server if block_given?
|
13
|
+
server.start
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
metadata
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: control_tower
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 1
|
7
|
+
- 0
|
8
|
+
version: "1.0"
|
9
|
+
platform: ruby
|
10
|
+
authors:
|
11
|
+
- MacRuby Team
|
12
|
+
autorequire:
|
13
|
+
bindir: bin
|
14
|
+
cert_chain: []
|
15
|
+
|
16
|
+
date: 2010-09-20 00:00:00 -07:00
|
17
|
+
default_executable:
|
18
|
+
dependencies:
|
19
|
+
- !ruby/object:Gem::Dependency
|
20
|
+
name: rack
|
21
|
+
prerelease: false
|
22
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
segments:
|
27
|
+
- 1
|
28
|
+
- 2
|
29
|
+
- 1
|
30
|
+
version: 1.2.1
|
31
|
+
type: :runtime
|
32
|
+
version_requirements: *id001
|
33
|
+
description: " Control Tower is a Rack-based HTTP server designed to work with MacRuby. It can\n be used by calling to its Rack::Handler class, or by running the control_tower\n executable with a Rackup configuration file (see the control tower help for more\n details).\n"
|
34
|
+
email: macruby-devel@lists.macosforge.org
|
35
|
+
executables:
|
36
|
+
- control_tower
|
37
|
+
extensions:
|
38
|
+
- ext/CTParser/extconf.rb
|
39
|
+
extra_rdoc_files: []
|
40
|
+
|
41
|
+
files:
|
42
|
+
- lib/control_tower.rb
|
43
|
+
- lib/control_tower/rack_socket.rb
|
44
|
+
- lib/control_tower/server.rb
|
45
|
+
- lib/rack/handler/control_tower.rb
|
46
|
+
- bin/control_tower
|
47
|
+
- ext/CTParser/http11_parser.h
|
48
|
+
- ext/CTParser/http11_parser.c
|
49
|
+
- ext/CTParser/CTParser.h
|
50
|
+
- ext/CTParser/CTParser.m
|
51
|
+
- ext/CTParser/extconf.rb
|
52
|
+
has_rdoc: true
|
53
|
+
homepage: http://www.macruby.org
|
54
|
+
licenses: []
|
55
|
+
|
56
|
+
post_install_message:
|
57
|
+
rdoc_options: []
|
58
|
+
|
59
|
+
require_paths:
|
60
|
+
- lib
|
61
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
62
|
+
requirements:
|
63
|
+
- - ">="
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
segments:
|
66
|
+
- 0
|
67
|
+
version: "0"
|
68
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
69
|
+
requirements:
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
segments:
|
73
|
+
- 0
|
74
|
+
version: "0"
|
75
|
+
requirements: []
|
76
|
+
|
77
|
+
rubyforge_project:
|
78
|
+
rubygems_version: 1.3.6
|
79
|
+
signing_key:
|
80
|
+
specification_version: 3
|
81
|
+
summary: A Rack-based HTTP server for MacRuby
|
82
|
+
test_files: []
|
83
|
+
|