pitchfork 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/.git-blame-ignore-revs +3 -0
  3. data/.gitattributes +5 -0
  4. data/.github/workflows/ci.yml +30 -0
  5. data/.gitignore +23 -0
  6. data/COPYING +674 -0
  7. data/Dockerfile +4 -0
  8. data/Gemfile +9 -0
  9. data/Gemfile.lock +30 -0
  10. data/LICENSE +67 -0
  11. data/README.md +123 -0
  12. data/Rakefile +72 -0
  13. data/docs/Application_Timeouts.md +74 -0
  14. data/docs/CONFIGURATION.md +388 -0
  15. data/docs/DESIGN.md +86 -0
  16. data/docs/FORK_SAFETY.md +80 -0
  17. data/docs/PHILOSOPHY.md +90 -0
  18. data/docs/REFORKING.md +113 -0
  19. data/docs/SIGNALS.md +38 -0
  20. data/docs/TUNING.md +106 -0
  21. data/examples/constant_caches.ru +43 -0
  22. data/examples/echo.ru +25 -0
  23. data/examples/hello.ru +5 -0
  24. data/examples/nginx.conf +156 -0
  25. data/examples/pitchfork.conf.minimal.rb +5 -0
  26. data/examples/pitchfork.conf.rb +77 -0
  27. data/examples/unicorn.socket +11 -0
  28. data/exe/pitchfork +116 -0
  29. data/ext/pitchfork_http/CFLAGS +13 -0
  30. data/ext/pitchfork_http/c_util.h +116 -0
  31. data/ext/pitchfork_http/child_subreaper.h +25 -0
  32. data/ext/pitchfork_http/common_field_optimization.h +130 -0
  33. data/ext/pitchfork_http/epollexclusive.h +124 -0
  34. data/ext/pitchfork_http/ext_help.h +38 -0
  35. data/ext/pitchfork_http/extconf.rb +14 -0
  36. data/ext/pitchfork_http/global_variables.h +97 -0
  37. data/ext/pitchfork_http/httpdate.c +79 -0
  38. data/ext/pitchfork_http/pitchfork_http.c +4318 -0
  39. data/ext/pitchfork_http/pitchfork_http.rl +1024 -0
  40. data/ext/pitchfork_http/pitchfork_http_common.rl +76 -0
  41. data/lib/pitchfork/app/old_rails/static.rb +59 -0
  42. data/lib/pitchfork/children.rb +124 -0
  43. data/lib/pitchfork/configurator.rb +314 -0
  44. data/lib/pitchfork/const.rb +23 -0
  45. data/lib/pitchfork/http_parser.rb +206 -0
  46. data/lib/pitchfork/http_response.rb +63 -0
  47. data/lib/pitchfork/http_server.rb +822 -0
  48. data/lib/pitchfork/launcher.rb +9 -0
  49. data/lib/pitchfork/mem_info.rb +36 -0
  50. data/lib/pitchfork/message.rb +130 -0
  51. data/lib/pitchfork/mold_selector.rb +29 -0
  52. data/lib/pitchfork/preread_input.rb +33 -0
  53. data/lib/pitchfork/refork_condition.rb +21 -0
  54. data/lib/pitchfork/select_waiter.rb +9 -0
  55. data/lib/pitchfork/socket_helper.rb +199 -0
  56. data/lib/pitchfork/stream_input.rb +152 -0
  57. data/lib/pitchfork/tee_input.rb +133 -0
  58. data/lib/pitchfork/tmpio.rb +35 -0
  59. data/lib/pitchfork/version.rb +8 -0
  60. data/lib/pitchfork/worker.rb +244 -0
  61. data/lib/pitchfork.rb +158 -0
  62. data/pitchfork.gemspec +30 -0
  63. metadata +137 -0
@@ -0,0 +1,206 @@
1
+ # -*- encoding: binary -*-
2
+ # :enddoc:
3
+ # no stable API here
4
+ require 'pitchfork/pitchfork_http'
5
+
6
+ module Pitchfork
7
+ class HttpParser
8
+
9
+ # default parameters we merge into the request env for Rack handlers
10
+ DEFAULTS = {
11
+ "rack.errors" => $stderr,
12
+ "rack.multiprocess" => true,
13
+ "rack.multithread" => false,
14
+ "rack.run_once" => false,
15
+ "rack.version" => [1, 2],
16
+ "rack.hijack?" => true,
17
+ "SCRIPT_NAME" => "",
18
+
19
+ # this is not in the Rack spec, but some apps may rely on it
20
+ "SERVER_SOFTWARE" => "Pitchfork #{Pitchfork::Const::UNICORN_VERSION}"
21
+ }
22
+
23
+ NULL_IO = StringIO.new("")
24
+
25
+ # :stopdoc:
26
+ HTTP_RESPONSE_START = [ 'HTTP'.freeze, '/1.1 '.freeze ]
27
+ EMPTY_ARRAY = [].freeze
28
+ @@input_class = Pitchfork::TeeInput
29
+ @@check_client_connection = false
30
+ @@tcpi_inspect_ok = Socket.const_defined?(:TCP_INFO)
31
+
32
+ def self.input_class
33
+ @@input_class
34
+ end
35
+
36
+ def self.input_class=(klass)
37
+ @@input_class = klass
38
+ end
39
+
40
+ def self.check_client_connection
41
+ @@check_client_connection
42
+ end
43
+
44
+ def self.check_client_connection=(bool)
45
+ @@check_client_connection = bool
46
+ end
47
+
48
+ # :startdoc:
49
+
50
+ # Does the majority of the IO processing. It has been written in
51
+ # Ruby using about 8 different IO processing strategies.
52
+ #
53
+ # It is currently carefully constructed to make sure that it gets
54
+ # the best possible performance for the common case: GET requests
55
+ # that are fully complete after a single read(2)
56
+ #
57
+ # Anyone who thinks they can make it faster is more than welcome to
58
+ # take a crack at it.
59
+ #
60
+ # returns an environment hash suitable for Rack if successful
61
+ # This does minimal exception trapping and it is up to the caller
62
+ # to handle any socket errors (e.g. user aborted upload).
63
+ def read(socket)
64
+ e = env
65
+
66
+ # From https://www.ietf.org/rfc/rfc3875:
67
+ # "Script authors should be aware that the REMOTE_ADDR and
68
+ # REMOTE_HOST meta-variables (see sections 4.1.8 and 4.1.9)
69
+ # may not identify the ultimate source of the request. They
70
+ # identify the client for the immediate request to the server;
71
+ # that client may be a proxy, gateway, or other intermediary
72
+ # acting on behalf of the actual source client."
73
+ address = socket.remote_address
74
+ e['REMOTE_ADDR'] = if address.unix?
75
+ "127.0.0.1"
76
+ else
77
+ address.ip_address
78
+ end
79
+
80
+ # short circuit the common case with small GET requests first
81
+ socket.readpartial(16384, buf)
82
+ if parse.nil?
83
+ # Parser is not done, queue up more data to read and continue parsing
84
+ # an Exception thrown from the parser will throw us out of the loop
85
+ false until add_parse(socket.readpartial(16384))
86
+ end
87
+
88
+ check_client_connection(socket) if @@check_client_connection
89
+
90
+ e['rack.input'] = 0 == content_length ?
91
+ NULL_IO : @@input_class.new(socket, self)
92
+
93
+ # for Rack hijacking in Rack 1.5 and later
94
+ e['pitchfork.socket'] = socket
95
+ e['rack.hijack'] = self
96
+
97
+ e.merge!(DEFAULTS)
98
+ end
99
+
100
+ # for rack.hijack, we respond to this method so no extra allocation
101
+ # of a proc object
102
+ def call
103
+ hijacked!
104
+ env['rack.hijack_io'] = env['pitchfork.socket']
105
+ end
106
+
107
+ def hijacked?
108
+ env.include?('rack.hijack_io'.freeze)
109
+ end
110
+
111
+ if Raindrops.const_defined?(:TCP_Info)
112
+ TCPI = Raindrops::TCP_Info.allocate
113
+
114
+ def check_client_connection(socket) # :nodoc:
115
+ if TCPSocket === socket
116
+ # Raindrops::TCP_Info#get!, #state (reads struct tcp_info#tcpi_state)
117
+ raise Errno::EPIPE, "client closed connection".freeze,
118
+ EMPTY_ARRAY if closed_state?(TCPI.get!(socket).state)
119
+ else
120
+ write_http_header(socket)
121
+ end
122
+ end
123
+
124
+ if Raindrops.const_defined?(:TCP)
125
+ # raindrops 0.18.0+ supports FreeBSD + Linux using the same names
126
+ # Evaluate these hash lookups at load time so we can
127
+ # generate an opt_case_dispatch instruction
128
+ eval <<-EOS
129
+ def closed_state?(state) # :nodoc:
130
+ case state
131
+ when #{Raindrops::TCP[:ESTABLISHED]}
132
+ false
133
+ when #{Raindrops::TCP.values_at(
134
+ :CLOSE_WAIT, :TIME_WAIT, :CLOSE, :LAST_ACK, :CLOSING).join(',')}
135
+ true
136
+ else
137
+ false
138
+ end
139
+ end
140
+ EOS
141
+ else
142
+ # raindrops before 0.18 only supported TCP_INFO under Linux
143
+ def closed_state?(state) # :nodoc:
144
+ case state
145
+ when 1 # ESTABLISHED
146
+ false
147
+ when 8, 6, 7, 9, 11 # CLOSE_WAIT, TIME_WAIT, CLOSE, LAST_ACK, CLOSING
148
+ true
149
+ else
150
+ false
151
+ end
152
+ end
153
+ end
154
+ else
155
+
156
+ # Ruby 2.2+ can show struct tcp_info as a string Socket::Option#inspect.
157
+ # Not that efficient, but probably still better than doing unnecessary
158
+ # work after a client gives up.
159
+ def check_client_connection(socket) # :nodoc:
160
+ if TCPSocket === socket && @@tcpi_inspect_ok
161
+ opt = socket.getsockopt(Socket::IPPROTO_TCP, Socket::TCP_INFO).inspect
162
+ if opt =~ /\bstate=(\S+)/
163
+ raise Errno::EPIPE, "client closed connection".freeze,
164
+ EMPTY_ARRAY if closed_state_str?($1)
165
+ else
166
+ @@tcpi_inspect_ok = false
167
+ write_http_header(socket)
168
+ end
169
+ opt.clear
170
+ else
171
+ write_http_header(socket)
172
+ end
173
+ end
174
+
175
+ def closed_state_str?(state)
176
+ case state
177
+ when 'ESTABLISHED'
178
+ false
179
+ # not a typo, ruby maps TCP_CLOSE (no 'D') to state=CLOSED (w/ 'D')
180
+ when 'CLOSE_WAIT', 'TIME_WAIT', 'CLOSED', 'LAST_ACK', 'CLOSING'
181
+ true
182
+ else
183
+ false
184
+ end
185
+ end
186
+ end
187
+
188
+ def write_http_header(socket) # :nodoc:
189
+ if headers?
190
+ self.response_start_sent = true
191
+ HTTP_RESPONSE_START.each { |c| socket.write(c) }
192
+ end
193
+ end
194
+
195
+ # called by ext/pitchfork_http/pitchfork_http.rl via rb_funcall
196
+ def self.is_chunked?(v) # :nodoc:
197
+ vals = v.split(/[ \t]*,[ \t]*/).map!(&:downcase)
198
+ if vals.pop == 'chunked'.freeze
199
+ return true unless vals.include?('chunked'.freeze)
200
+ raise Pitchfork::HttpParserError, 'double chunked', []
201
+ end
202
+ return false unless vals.include?('chunked'.freeze)
203
+ raise Pitchfork::HttpParserError, 'chunked not last', []
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,63 @@
1
+ # -*- encoding: binary -*-
2
+ # :enddoc:
3
+
4
+ module Pitchfork
5
+ # Writes a Rack response to your client using the HTTP/1.1 specification.
6
+ # You use it by simply doing:
7
+ #
8
+ # status, headers, body = rack_app.call(env)
9
+ # http_response_write(socket, status, headers, body)
10
+ #
11
+ # Most header correctness (including Content-Length and Content-Type)
12
+ # is the job of Rack, with the exception of the "Date" and "Status" header.
13
+ module HttpResponse
14
+ STATUS_CODES = defined?(Rack::Utils::HTTP_STATUS_CODES) ?
15
+ Rack::Utils::HTTP_STATUS_CODES : {}
16
+
17
+ # internal API, code will always be common-enough-for-even-old-Rack
18
+ def err_response(code, response_start_sent)
19
+ "#{response_start_sent ? '' : 'HTTP/1.1 '}" \
20
+ "#{code} #{STATUS_CODES[code]}\r\n\r\n"
21
+ end
22
+
23
+ # writes the rack_response to socket as an HTTP response
24
+ def http_response_write(socket, status, headers, body,
25
+ req = Pitchfork::HttpParser.new)
26
+ hijack = nil
27
+
28
+ if headers
29
+ code = status.to_i
30
+ msg = STATUS_CODES[code]
31
+ start = req.response_start_sent ? ''.freeze : 'HTTP/1.1 '.freeze
32
+ buf = "#{start}#{msg ? %Q(#{code} #{msg}) : status}\r\n" \
33
+ "Date: #{httpdate}\r\n" \
34
+ "Connection: close\r\n"
35
+ headers.each do |key, value|
36
+ case key
37
+ when %r{\A(?:Date|Connection)\z}i
38
+ next
39
+ when "rack.hijack"
40
+ # This should only be hit under Rack >= 1.5, as this was an illegal
41
+ # key in Rack < 1.5
42
+ hijack = value
43
+ else
44
+ if value =~ /\n/
45
+ # avoiding blank, key-only cookies with /\n+/
46
+ value.split(/\n+/).each { |v| buf << "#{key}: #{v}\r\n" }
47
+ else
48
+ buf << "#{key}: #{value}\r\n"
49
+ end
50
+ end
51
+ end
52
+ socket.write(buf << "\r\n".freeze)
53
+ end
54
+
55
+ if hijack
56
+ req.hijacked!
57
+ hijack.call(socket)
58
+ else
59
+ body.each { |chunk| socket.write(chunk) }
60
+ end
61
+ end
62
+ end
63
+ end