pitchfork 0.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.

Potentially problematic release.


This version of pitchfork might be problematic. Click here for more details.

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