spider-gazelle 1.2.0 → 2.0.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.
@@ -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