spider-gazelle 1.2.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/sg +1 -64
- data/lib/rack/handler/spider-gazelle.rb +17 -26
- data/lib/rack/lock_patch.rb +27 -27
- data/lib/spider-gazelle.rb +165 -16
- data/lib/spider-gazelle/gazelle.rb +151 -134
- data/lib/spider-gazelle/gazelle/app_store.rb +86 -0
- data/lib/spider-gazelle/gazelle/http1.rb +496 -0
- data/lib/spider-gazelle/gazelle/request.rb +155 -0
- data/lib/spider-gazelle/logger.rb +122 -0
- data/lib/spider-gazelle/options.rb +213 -0
- data/lib/spider-gazelle/reactor.rb +69 -0
- data/lib/spider-gazelle/signaller.rb +214 -0
- data/lib/spider-gazelle/signaller/signal_parser.rb +66 -0
- data/lib/spider-gazelle/spider.rb +305 -343
- data/lib/spider-gazelle/spider/binding.rb +80 -0
- data/lib/spider-gazelle/upgrades/websocket.rb +92 -88
- data/spec/http1_spec.rb +173 -0
- data/spec/rack_lock_spec.rb +97 -97
- data/spider-gazelle.gemspec +6 -6
- metadata +24 -17
- data/lib/spider-gazelle/app_store.rb +0 -64
- data/lib/spider-gazelle/binding.rb +0 -53
- data/lib/spider-gazelle/connection.rb +0 -371
- data/lib/spider-gazelle/const.rb +0 -206
- data/lib/spider-gazelle/request.rb +0 -103
@@ -0,0 +1,155 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
require 'rack' # Ruby webserver abstraction
|
3
|
+
|
4
|
+
module SpiderGazelle
|
5
|
+
class Request < ::Libuv::Q::DeferredPromise
|
6
|
+
RACK_VERSION = 'rack.version'.freeze
|
7
|
+
RACK_ERRORS = 'rack.errors'.freeze
|
8
|
+
RACK_MULTITHREAD = "rack.multithread".freeze
|
9
|
+
RACK_MULTIPROCESS = "rack.multiprocess".freeze
|
10
|
+
RACK_RUN_ONCE = "rack.run_once".freeze
|
11
|
+
SCRIPT_NAME = "SCRIPT_NAME".freeze
|
12
|
+
EMPTY = ''.freeze
|
13
|
+
SERVER_PROTOCOL = "SERVER_PROTOCOL".freeze
|
14
|
+
HTTP_11 = "HTTP/1.1".freeze
|
15
|
+
SERVER_SOFTWARE = "SERVER_SOFTWARE".freeze
|
16
|
+
GATEWAY_INTERFACE = "GATEWAY_INTERFACE".freeze
|
17
|
+
CGI_VER = "CGI/1.2".freeze
|
18
|
+
SERVER = "SpiderGazelle".freeze
|
19
|
+
LOCALHOST = 'localhost'.freeze
|
20
|
+
|
21
|
+
|
22
|
+
# TODO:: Add HTTP headers to the env and capitalise them and prefix them with HTTP_
|
23
|
+
# convert - signs to underscores
|
24
|
+
PROTO_ENV = {
|
25
|
+
RACK_VERSION => ::Rack::VERSION, # Should be an array of integers
|
26
|
+
RACK_ERRORS => $stderr, # An error stream that supports: puts, write and flush
|
27
|
+
RACK_MULTITHREAD => true, # can the app be simultaneously invoked by another thread?
|
28
|
+
RACK_MULTIPROCESS => true, # will the app be simultaneously be invoked in a separate process?
|
29
|
+
RACK_RUN_ONCE => false, # this isn't CGI so will always be false
|
30
|
+
|
31
|
+
SCRIPT_NAME => ENV['SCRIPT_NAME'.freeze] || EMPTY, # The virtual path of the app base (empty if root)
|
32
|
+
SERVER_PROTOCOL => HTTP_11,
|
33
|
+
|
34
|
+
GATEWAY_INTERFACE => CGI_VER,
|
35
|
+
SERVER_SOFTWARE => SERVER
|
36
|
+
}
|
37
|
+
|
38
|
+
attr_accessor :env, :url, :header, :body, :keep_alive, :upgrade
|
39
|
+
attr_reader :hijacked, :defer, :is_async
|
40
|
+
|
41
|
+
|
42
|
+
SERVER_PORT = "SERVER_PORT".freeze
|
43
|
+
REMOTE_ADDR = "REMOTE_ADDR".freeze
|
44
|
+
RACK_URL_SCHEME = "rack.url_scheme".freeze
|
45
|
+
ASYNC = "async.callback".freeze
|
46
|
+
|
47
|
+
def initialize(thread, app, port, remote_ip, scheme, async_callback)
|
48
|
+
super(thread, thread.defer)
|
49
|
+
|
50
|
+
@app = app
|
51
|
+
@body = ''
|
52
|
+
@header = ''
|
53
|
+
@url = ''
|
54
|
+
@env = PROTO_ENV.dup
|
55
|
+
@env[SERVER_PORT] = port
|
56
|
+
@env[REMOTE_ADDR] = remote_ip
|
57
|
+
@env[RACK_URL_SCHEME] = scheme
|
58
|
+
@env[ASYNC] = async_callback
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
CONTENT_LENGTH = "CONTENT_LENGTH".freeze
|
63
|
+
HTTP_CONTENT_LENGTH = "HTTP_CONTENT_LENGTH".freeze
|
64
|
+
CONTENT_TYPE = "CONTENT_TYPE".freeze
|
65
|
+
HTTP_CONTENT_TYPE = "HTTP_CONTENT_TYPE".freeze
|
66
|
+
DEFAULT_TYPE = "text/plain".freeze
|
67
|
+
REQUEST_URI= "REQUEST_URI".freeze
|
68
|
+
ASCII_8BIT = "ASCII-8BIT".freeze
|
69
|
+
RACK_INPUT = "rack.input".freeze
|
70
|
+
PATH_INFO = "PATH_INFO".freeze
|
71
|
+
REQUEST_PATH = "REQUEST_PATH".freeze
|
72
|
+
QUERY_STRING = "QUERY_STRING".freeze
|
73
|
+
HTTP_HOST = "HTTP_HOST".freeze
|
74
|
+
COLON = ":".freeze
|
75
|
+
SERVER_NAME = "SERVER_NAME".freeze
|
76
|
+
# Hijacking IO is supported
|
77
|
+
HIJACK_P = "rack.hijack?".freeze
|
78
|
+
# Callback for indicating that this socket will be hijacked
|
79
|
+
HIJACK = "rack.hijack".freeze
|
80
|
+
# The object for performing IO on after hijack is called
|
81
|
+
HIJACK_IO = "rack.hijack_io".freeze
|
82
|
+
QUESTION_MARK = "?".freeze
|
83
|
+
|
84
|
+
HTTP_UPGRADE = 'HTTP_UPGRADE'.freeze
|
85
|
+
USE_HTTP2 = 'h2c'.freeze
|
86
|
+
|
87
|
+
|
88
|
+
def execute!
|
89
|
+
@env[CONTENT_LENGTH] = @env.delete(HTTP_CONTENT_LENGTH) || @body.length
|
90
|
+
@env[CONTENT_TYPE] = @env.delete(HTTP_CONTENT_TYPE) || DEFAULT_TYPE
|
91
|
+
@env[REQUEST_URI] = @url.freeze
|
92
|
+
|
93
|
+
# For Rack::Lint on 1.9, ensure that the encoding is always for spec
|
94
|
+
@body.force_encoding(ASCII_8BIT) if @body.respond_to?(:force_encoding)
|
95
|
+
@env[RACK_INPUT] = StringIO.new @body
|
96
|
+
|
97
|
+
# Break the request into its components
|
98
|
+
query_start = @url.index QUESTION_MARK
|
99
|
+
if query_start
|
100
|
+
path = @url[0...query_start].freeze
|
101
|
+
@env[PATH_INFO] = path
|
102
|
+
@env[REQUEST_PATH] = path
|
103
|
+
@env[QUERY_STRING] = @url[query_start + 1..-1].freeze
|
104
|
+
else
|
105
|
+
@env[PATH_INFO] = @url
|
106
|
+
@env[REQUEST_PATH] = @url
|
107
|
+
@env[QUERY_STRING] = EMPTY
|
108
|
+
end
|
109
|
+
|
110
|
+
# Grab the host name from the request
|
111
|
+
if host = @env[HTTP_HOST]
|
112
|
+
if colon = host.index(COLON)
|
113
|
+
@env[SERVER_NAME] = host[0, colon]
|
114
|
+
@env[SERVER_PORT] = host[colon+1, host.bytesize]
|
115
|
+
else
|
116
|
+
@env[SERVER_NAME] = host
|
117
|
+
end
|
118
|
+
else
|
119
|
+
@env[SERVER_NAME] = LOCALHOST
|
120
|
+
end
|
121
|
+
|
122
|
+
# Provide hijack options if this is an upgrade request
|
123
|
+
if @upgrade == true
|
124
|
+
if @env[HTTP_UPGRADE] == USE_HTTP2
|
125
|
+
# TODO:: implement the upgrade process here
|
126
|
+
else
|
127
|
+
@env[HIJACK_P] = true
|
128
|
+
@env[HIJACK] = method :hijack
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Execute the request
|
133
|
+
# NOTE:: Catch was overloaded by Promise so this does the
|
134
|
+
resp = ruby_catch(:async) { @app.call @env }
|
135
|
+
if resp.nil? || resp[0] == -1
|
136
|
+
@is_async = true
|
137
|
+
|
138
|
+
# close the body for deferred responses
|
139
|
+
unless resp.nil?
|
140
|
+
body = resp[2]
|
141
|
+
body.close if body.respond_to?(:close)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
resp
|
145
|
+
end
|
146
|
+
|
147
|
+
protected
|
148
|
+
|
149
|
+
def hijack
|
150
|
+
@hijacked = @loop.defer
|
151
|
+
@env.delete HIJACK # don't want to hold a reference to this request object
|
152
|
+
@env[HIJACK_IO] = @hijacked.promise
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'libuv'
|
2
|
+
|
3
|
+
module SpiderGazelle
|
4
|
+
class Logger
|
5
|
+
include Singleton
|
6
|
+
attr_reader :level, :thread, :pipe
|
7
|
+
attr_accessor :formatter
|
8
|
+
|
9
|
+
|
10
|
+
LEVEL = {
|
11
|
+
debug: 0,
|
12
|
+
info: 1,
|
13
|
+
warn: 2,
|
14
|
+
error: 3,
|
15
|
+
fatal: 4
|
16
|
+
}.freeze
|
17
|
+
DEFAULT_LEVEL = 1
|
18
|
+
LEVELS = LEVEL.keys.freeze
|
19
|
+
|
20
|
+
|
21
|
+
def initialize
|
22
|
+
@thread = ::Libuv::Loop.default
|
23
|
+
@level = DEFAULT_LEVEL
|
24
|
+
@write = method(:server_write)
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
def self.log(data)
|
29
|
+
Logger.instance.server_write(data)
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
def set_client(uv_io)
|
34
|
+
@pipe = uv_io
|
35
|
+
@write = method(:client_write)
|
36
|
+
end
|
37
|
+
|
38
|
+
def level=(level)
|
39
|
+
@level = LEVEL[level] || level
|
40
|
+
end
|
41
|
+
|
42
|
+
def verbose!(enabled = true)
|
43
|
+
@verbose = enabled
|
44
|
+
end
|
45
|
+
|
46
|
+
def debug(msg = nil)
|
47
|
+
if @level <= 0
|
48
|
+
msg = yield if block_given?
|
49
|
+
log(:debug, msg)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def info(msg = nil)
|
54
|
+
if @level <= 1
|
55
|
+
msg = yield if block_given?
|
56
|
+
log(:info, msg)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def warn(msg = nil)
|
61
|
+
if @level <= 2
|
62
|
+
msg = yield if block_given?
|
63
|
+
log(:warn, msg)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def error(msg = nil)
|
68
|
+
if @level <= 3
|
69
|
+
msg = yield if block_given?
|
70
|
+
log(:error, msg)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def fatal(msg = nil)
|
75
|
+
if @level <= 4
|
76
|
+
msg = yield if block_given?
|
77
|
+
log(:fatal, msg)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def verbose(msg = nil)
|
82
|
+
if @verbose
|
83
|
+
msg = yield if block_given?
|
84
|
+
@write.call ">> #{msg}\n"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def print_error(e, msg = '', trace = nil)
|
89
|
+
msg << ":\n" unless msg.empty?
|
90
|
+
msg << "#{e.message}\n"
|
91
|
+
backtrace = e.backtrace if e.respond_to?(:backtrace)
|
92
|
+
if backtrace
|
93
|
+
msg << "#{backtrace.join("\n")}\n"
|
94
|
+
elsif trace.nil?
|
95
|
+
trace = caller
|
96
|
+
end
|
97
|
+
msg << "Caller backtrace:\n#{trace.join("\n")}\n" if trace
|
98
|
+
error(msg)
|
99
|
+
end
|
100
|
+
|
101
|
+
# NOTE:: should only be called on reactor thread
|
102
|
+
def server_write(msg)
|
103
|
+
STDOUT.write msg
|
104
|
+
end
|
105
|
+
|
106
|
+
|
107
|
+
protected
|
108
|
+
|
109
|
+
|
110
|
+
def log(level, msg)
|
111
|
+
output = "[#{level}] #{msg}\n"
|
112
|
+
@thread.schedule do
|
113
|
+
@write.call output
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# NOTE:: should only be called on reactor thread
|
118
|
+
def client_write(msg)
|
119
|
+
@pipe.write "\x02Logger log #{msg}\x03"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,213 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'optparse'
|
3
|
+
require 'spider-gazelle/logger'
|
4
|
+
|
5
|
+
|
6
|
+
module SpiderGazelle
|
7
|
+
module Options
|
8
|
+
DEFAULTS = {
|
9
|
+
host: "0.0.0.0",
|
10
|
+
port: 3000,
|
11
|
+
verbose: false,
|
12
|
+
tls: false,
|
13
|
+
backlog: 5000,
|
14
|
+
rackup: "#{Dir.pwd}/config.ru",
|
15
|
+
mode: :thread,
|
16
|
+
app_mode: :thread_pool,
|
17
|
+
isolate: true
|
18
|
+
}.freeze
|
19
|
+
|
20
|
+
|
21
|
+
# Options that can't be used when more than one set of options is being applied
|
22
|
+
APP_OPTIONS = [:port, :host, :verbose, :debug, :environment, :rackup, :mode, :backlog, :count, :name, :loglevel].freeze
|
23
|
+
MUTUALLY_EXCLUSIVE = {
|
24
|
+
|
25
|
+
# Only :password is valid when this option is present
|
26
|
+
update: APP_OPTIONS
|
27
|
+
|
28
|
+
}.freeze
|
29
|
+
|
30
|
+
|
31
|
+
def self.parse(args)
|
32
|
+
options = {}
|
33
|
+
|
34
|
+
parser = OptionParser.new do |opts|
|
35
|
+
# ================
|
36
|
+
# STANDARD OPTIONS
|
37
|
+
# ================
|
38
|
+
opts.on "-p", "--port PORT", Integer, "Define what port TCP port to bind to (default: 3000)" do |arg|
|
39
|
+
options[:port] = arg
|
40
|
+
end
|
41
|
+
|
42
|
+
opts.on "-h", "--host ADDRESS", "bind to address (default: 0.0.0.0)" do |arg|
|
43
|
+
options[:host] = arg
|
44
|
+
end
|
45
|
+
|
46
|
+
opts.on "-v", "--verbose", "loud output" do
|
47
|
+
options[:verbose] = true
|
48
|
+
end
|
49
|
+
|
50
|
+
opts.on "-d", "--debug", "debugging mode with lowered security and manual processes" do
|
51
|
+
options[:debug] = true
|
52
|
+
end
|
53
|
+
|
54
|
+
opts.on "-e", "--environment ENVIRONMENT", "The environment to run the Rack app on (default: development)" do |arg|
|
55
|
+
options[:environment] = arg
|
56
|
+
end
|
57
|
+
|
58
|
+
opts.on "-r", "--rackup FILE", "Load Rack config from this file (default: config.ru)" do |arg|
|
59
|
+
options[:rackup] = arg
|
60
|
+
end
|
61
|
+
|
62
|
+
opts.on "-m", "--mode MODE", MODES, "Either process, thread or no_ipc (default: process)" do |arg|
|
63
|
+
options[:mode] = arg
|
64
|
+
end
|
65
|
+
|
66
|
+
opts.on "-a", "--app-mode MODE", APP_MODE, "How should requests be processed (default: thread_pool)" do |arg|
|
67
|
+
options[:host] = arg
|
68
|
+
end
|
69
|
+
|
70
|
+
opts.on "-b", "--backlog BACKLOG", Integer, "Number of pending connections allowed (default: 5000)" do |arg|
|
71
|
+
options[:backlog] = arg
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
# =================
|
76
|
+
# TLS Configuration
|
77
|
+
# =================
|
78
|
+
opts.on "-t", "--use-tls PRIVATE_KEY_FILE", "Enables TLS on the port specified using the provided private key in PEM format" do |arg|
|
79
|
+
options[:tls] = true
|
80
|
+
options[:private_key] = arg
|
81
|
+
end
|
82
|
+
|
83
|
+
opts.on "-tc", "--tls-chain-file CERT_CHAIN_FILE", "The certificate chain to provide clients" do |arg|
|
84
|
+
options[:cert_chain] = arg
|
85
|
+
end
|
86
|
+
|
87
|
+
opts.on "-ts", "--tls-ciphers CIPHER_LIST", "A list of Ciphers that the server will accept" do |arg|
|
88
|
+
options[:ciphers] = arg
|
89
|
+
end
|
90
|
+
|
91
|
+
opts.on "-tv", "--tls-verify-peer", "Do we want to verify the client connections? (default: false)" do
|
92
|
+
options[:verify_peer] = true
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
# ========================
|
97
|
+
# CHILD PROCESS INDICATORS
|
98
|
+
# ========================
|
99
|
+
opts.on "-g", "--gazelle PASSWORD", 'For internal use only' do |arg|
|
100
|
+
options[:gazelle] = arg
|
101
|
+
end
|
102
|
+
|
103
|
+
opts.on "-f", "--file IPC", 'For internal use only' do |arg|
|
104
|
+
options[:gazelle_ipc] = arg
|
105
|
+
end
|
106
|
+
|
107
|
+
|
108
|
+
opts.on "-s", "--spider PASSWORD", 'For internal use only' do |arg|
|
109
|
+
options[:spider] = arg
|
110
|
+
end
|
111
|
+
|
112
|
+
|
113
|
+
opts.on "-i", "--interactive-mode", 'Loads a multi-process version of spider-gazelle that can live update your app' do
|
114
|
+
options[:isolate] = false
|
115
|
+
end
|
116
|
+
|
117
|
+
opts.on "-c", "--count NUMBER", Integer, "Number of gazelle processes to launch (default: number of CPU cores)" do |arg|
|
118
|
+
options[:count] = arg
|
119
|
+
end
|
120
|
+
|
121
|
+
|
122
|
+
# ==================
|
123
|
+
# SIGNALLING OPTIONS
|
124
|
+
# ==================
|
125
|
+
opts.on "-u", "--update", "Live migrates to a new process without dropping existing connections" do |arg|
|
126
|
+
options[:update] = true
|
127
|
+
end
|
128
|
+
|
129
|
+
opts.on "-up", "--update-password PASSWORD", "Sets a password for performing updates" do |arg|
|
130
|
+
options[:password] = arg
|
131
|
+
end
|
132
|
+
|
133
|
+
opts.on "-l", "--loglevel LEVEL", Logger::LEVELS, "Sets the log level" do |arg|
|
134
|
+
options[:loglevel] = arg
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
parser.banner = "sg <options> <rackup file>"
|
139
|
+
parser.on_tail "-h", "--help", "Show help" do
|
140
|
+
puts parser
|
141
|
+
exit 1
|
142
|
+
end
|
143
|
+
parser.parse!(args)
|
144
|
+
|
145
|
+
# Check for rackup file
|
146
|
+
if args.last =~ /\.ru$/
|
147
|
+
options[:rackup] = args.last
|
148
|
+
end
|
149
|
+
|
150
|
+
# Unless this is a signal then we want to include the default options
|
151
|
+
unless options[:update]
|
152
|
+
options = DEFAULTS.merge(options)
|
153
|
+
|
154
|
+
unless File.exist? options[:rackup]
|
155
|
+
abort "No rackup found at #{options[:rackup]}"
|
156
|
+
end
|
157
|
+
|
158
|
+
options[:environment] ||= ENV['RACK_ENV'] || 'development'
|
159
|
+
ENV['RACK_ENV'] = options[:environment]
|
160
|
+
|
161
|
+
# isolation and process mode don't mix
|
162
|
+
options[:isolate] = false if options[:mode] == :process
|
163
|
+
end
|
164
|
+
|
165
|
+
options
|
166
|
+
end
|
167
|
+
|
168
|
+
def self.sanitize(args)
|
169
|
+
# Use "\0" as this character won't be used in the command
|
170
|
+
cmdline = args.join("\0")
|
171
|
+
components = cmdline.split("\0--", -1)
|
172
|
+
|
173
|
+
# Ensure there is at least one component
|
174
|
+
# (This will occur when no options are provided)
|
175
|
+
components << '' if components.empty?
|
176
|
+
|
177
|
+
# Parse the commandline options
|
178
|
+
options = []
|
179
|
+
components.each do |app_opts|
|
180
|
+
options << parse(app_opts.split(/\0+/))
|
181
|
+
end
|
182
|
+
|
183
|
+
# Check for any invalid requests
|
184
|
+
exclusive = Set.new(MUTUALLY_EXCLUSIVE.keys)
|
185
|
+
|
186
|
+
if options.length > 1
|
187
|
+
|
188
|
+
# Some options can only be used by themselves
|
189
|
+
options.each do |opt|
|
190
|
+
keys = Set.new(opt.keys)
|
191
|
+
|
192
|
+
if exclusive.intersect? keys
|
193
|
+
invalid = exclusive & keys
|
194
|
+
|
195
|
+
abort "The requested actions can only be used in isolation: #{invalid.to_a}"
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
# Ensure there are no conflicting ports
|
200
|
+
ports = [options[0][:port]]
|
201
|
+
options[1..-1].each do |opt|
|
202
|
+
# If there is a clash we'll increment the port by 1
|
203
|
+
while ports.include? opt[:port]
|
204
|
+
opt[:port] += 1
|
205
|
+
end
|
206
|
+
ports << opt[:port]
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
options
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|