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.
@@ -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