yahns 0.0.1 → 0.0.2

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.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/Documentation/.gitignore +5 -0
  4. data/Documentation/GNUmakefile +50 -0
  5. data/Documentation/yahns-rackup.txt +152 -0
  6. data/Documentation/yahns.txt +68 -0
  7. data/Documentation/yahns_config.txt +563 -0
  8. data/GIT-VERSION-GEN +1 -1
  9. data/GNUmakefile +14 -7
  10. data/HACKING +56 -0
  11. data/INSTALL +8 -0
  12. data/README +15 -2
  13. data/Rakefile +2 -2
  14. data/bin/yahns +1 -2
  15. data/bin/yahns-rackup +9 -0
  16. data/examples/yahns_multi.conf.rb +14 -4
  17. data/examples/yahns_rack_basic.conf.rb +17 -1
  18. data/extras/README +16 -0
  19. data/extras/autoindex.rb +151 -0
  20. data/extras/exec_cgi.rb +108 -0
  21. data/extras/proxy_pass.rb +210 -0
  22. data/extras/try_gzip_static.rb +208 -0
  23. data/lib/yahns.rb +5 -2
  24. data/lib/yahns/acceptor.rb +64 -22
  25. data/lib/yahns/cap_input.rb +2 -2
  26. data/lib/yahns/{client_expire_portable.rb → client_expire_generic.rb} +12 -11
  27. data/lib/yahns/{client_expire.rb → client_expire_tcpi.rb} +7 -6
  28. data/lib/yahns/config.rb +107 -22
  29. data/lib/yahns/daemon.rb +2 -0
  30. data/lib/yahns/fdmap.rb +28 -9
  31. data/lib/yahns/http_client.rb +123 -37
  32. data/lib/yahns/http_context.rb +21 -3
  33. data/lib/yahns/http_response.rb +80 -19
  34. data/lib/yahns/log.rb +23 -4
  35. data/lib/yahns/queue_epoll.rb +20 -9
  36. data/lib/yahns/queue_quitter.rb +16 -0
  37. data/lib/yahns/queue_quitter_pipe.rb +24 -0
  38. data/lib/yahns/rack.rb +0 -1
  39. data/lib/yahns/rackup_handler.rb +57 -0
  40. data/lib/yahns/server.rb +189 -59
  41. data/lib/yahns/server_mp.rb +43 -35
  42. data/lib/yahns/sigevent_pipe.rb +1 -0
  43. data/lib/yahns/socket_helper.rb +37 -11
  44. data/lib/yahns/stream_file.rb +14 -4
  45. data/lib/yahns/stream_input.rb +13 -7
  46. data/lib/yahns/tcp_server.rb +7 -0
  47. data/lib/yahns/tmpio.rb +10 -3
  48. data/lib/yahns/unix_server.rb +7 -0
  49. data/lib/yahns/wbuf.rb +19 -2
  50. data/lib/yahns/wbuf_common.rb +10 -3
  51. data/lib/yahns/wbuf_str.rb +24 -0
  52. data/lib/yahns/worker.rb +5 -26
  53. data/test/helper.rb +15 -5
  54. data/test/server_helper.rb +37 -1
  55. data/test/test_bin.rb +17 -8
  56. data/test/test_buffer_tmpdir.rb +103 -0
  57. data/test/test_client_expire.rb +71 -35
  58. data/test/test_client_max_body_size.rb +5 -13
  59. data/test/test_config.rb +1 -1
  60. data/test/test_expect_100.rb +176 -0
  61. data/test/test_extras_autoindex.rb +53 -0
  62. data/test/test_extras_exec_cgi.rb +81 -0
  63. data/test/test_extras_exec_cgi.sh +35 -0
  64. data/test/test_extras_try_gzip_static.rb +177 -0
  65. data/test/test_input.rb +128 -0
  66. data/test/test_mt_accept.rb +48 -0
  67. data/test/test_output_buffering.rb +90 -63
  68. data/test/test_rack.rb +1 -1
  69. data/test/test_rack_hijack.rb +2 -6
  70. data/test/test_reopen_logs.rb +2 -8
  71. data/test/test_serve_static.rb +104 -8
  72. data/test/test_server.rb +448 -73
  73. data/test/test_stream_file.rb +1 -1
  74. data/test/test_unix_socket.rb +72 -0
  75. data/test/test_wbuf.rb +20 -17
  76. data/yahns.gemspec +3 -0
  77. metadata +57 -5
@@ -0,0 +1,108 @@
1
+ # -*- encoding: binary -*-
2
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
3
+ # License: GPLv2 or later (https://www.gnu.org/licenses/gpl-2.0.txt)
4
+ class ExecCgi
5
+ class MyIO < Kgio::Pipe
6
+ attr_writer :my_pid
7
+ attr_writer :body_tip
8
+ attr_writer :chunked
9
+
10
+ def each
11
+ buf = @body_tip || ""
12
+ if buf.size > 0
13
+ buf = "#{buf.size.to_s(16)}\r\n#{buf}\r\n" if @chunked
14
+ yield buf
15
+ end
16
+ while tmp = kgio_read(8192, buf)
17
+ tmp = "#{tmp.size.to_s(16)}\r\n#{tmp}\r\n" if @chunked
18
+ yield tmp
19
+ end
20
+ yield("0\r\n\r\n") if @chunked
21
+ self
22
+ end
23
+
24
+ def close
25
+ super
26
+ if defined?(@my_pid) && @my_pid
27
+ begin
28
+ Process.waitpid(@my_pid)
29
+ rescue Errno::ECHILD
30
+ end
31
+ end
32
+ nil
33
+ end
34
+ end
35
+
36
+ PASS_VARS = %w(
37
+ CONTENT_LENGTH
38
+ CONTENT_TYPE
39
+ GATEWAY_INTERFACE
40
+ AUTH_TYPE
41
+ PATH_INFO
42
+ PATH_TRANSLATED
43
+ QUERY_STRING
44
+ REMOTE_ADDR
45
+ REMOTE_HOST
46
+ REMOTE_IDENT
47
+ REMOTE_USER
48
+ REQUEST_METHOD
49
+ SERVER_NAME
50
+ SERVER_PORT
51
+ SERVER_PROTOCOL
52
+ SERVER_SOFTWARE
53
+ ).map(&:freeze) # frozen strings are faster for Hash assignments
54
+
55
+ def initialize(*args)
56
+ @args = args
57
+ first = args[0] or
58
+ raise ArgumentError, "need path to executable"
59
+ first[0] == ?/ or args[0] = ::File.expand_path(first)
60
+ File.executable?(args[0]) or
61
+ raise ArgumentError, "#{args[0]} is not executable"
62
+ end
63
+
64
+ # Calls the app
65
+ def call(env)
66
+ cgi_env = { "SCRIPT_NAME" => @args[0], "GATEWAY_INTERFACE" => "CGI/1.1" }
67
+ PASS_VARS.each { |key| val = env[key] and cgi_env[key] = val }
68
+ env.each { |key,val| cgi_env[key] = val if key =~ /\AHTTP_/ }
69
+ pipe = MyIO.pipe
70
+ pipe[0].my_pid = Process.spawn(cgi_env, *@args,
71
+ out: pipe[1], close_others: true)
72
+ pipe[1].close
73
+ pipe = pipe[0]
74
+
75
+ if head = pipe.kgio_read(8192)
76
+ until head =~ /\r?\n\r?\n/
77
+ tmp = pipe.kgio_read(8192) or break
78
+ head << tmp
79
+ end
80
+ head, body = head.split(/\r?\n\r?\n/)
81
+ pipe.body_tip = body
82
+ pipe.chunked = false
83
+
84
+ headers = Rack::Utils::HeaderHash.new
85
+ prev = nil
86
+ head.split(/\r?\n/).each do |line|
87
+ case line
88
+ when /^([A-Za-z0-9-]+):\s*(.*)$/ then headers[prev = $1] = $2
89
+ when /^[ \t]/ then headers[prev] << "\n#{line}" if prev
90
+ end
91
+ end
92
+ status = headers.delete("Status") || 200
93
+ unless headers.include?("Content-Length") ||
94
+ headers.include?("Transfer-Encoding")
95
+ case env['HTTP_VERSION']
96
+ when 'HTTP/1.0', nil
97
+ # server will drop connection anyways
98
+ else
99
+ headers["Transfer-Encoding"] = "chunked"
100
+ pipe.chunked = true
101
+ end
102
+ end
103
+ [ status, headers, pipe ]
104
+ else
105
+ [ 500, { "Content-Length" => "0", "Content-Type" => "text/plain" }, [] ]
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,210 @@
1
+ # -*- encoding: binary -*-
2
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
3
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
4
+ require 'time'
5
+ require 'socket'
6
+ require 'kgio'
7
+ require 'kcar' # gem install kcar
8
+ require 'rack/request'
9
+ require 'thread'
10
+ require 'timeout'
11
+
12
+ # Totally synchronous and Rack 1.1-compatible, this will probably be rewritten.
13
+ # to take advantage of rack.hijack and use the non-blocking I/O facilities
14
+ # in yahns. yahns may have to grow a supported API for that...
15
+ # For now, we this blocks a worker thread; fortunately threads are reasonably
16
+ # cheap on GNU/Linux...
17
+ # This is totally untested but currently doesn't serve anything important.
18
+ class ProxyPass # :nodoc:
19
+ CHUNK_SIZE = 16384
20
+ ERROR_502 = [ 502, {'Content-Length'=>'0','Content-Type'=>'text/plain'}, [] ]
21
+
22
+ class ConnPool
23
+ def initialize
24
+ @mtx = Mutex.new
25
+ @objs = []
26
+ end
27
+
28
+ def get
29
+ @mtx.synchronize { @objs.pop }
30
+ end
31
+
32
+ def put(obj)
33
+ @mtx.synchronize { @objs << obj }
34
+ end
35
+ end
36
+
37
+ class UpstreamSocket < Kgio::Socket # :nodoc:
38
+ attr_writer :expiry
39
+
40
+ # called automatically by kgio_read!
41
+ def kgio_wait_readable(timeout = nil)
42
+ super(timeout || wait_time)
43
+ end
44
+
45
+ def wait_time
46
+ tout = @expiry ? @expiry - Time.now : @timeout
47
+ raise Timeout::Error, "request timed out", [] if tout < 0
48
+ tout
49
+ end
50
+
51
+ def readpartial(bytes, buf = Thread.current[:proxy_pass_buf] ||= "")
52
+ case rv = kgio_read!(bytes, buf)
53
+ when String
54
+ @expiry += @timeout # bump expiry when we succeed
55
+ end
56
+ rv
57
+ end
58
+
59
+ def req_write(buf, timeout)
60
+ @timeout = timeout
61
+ @expiry = Time.now + timeout
62
+ case rv = kgio_trywrite(buf)
63
+ when :wait_writable
64
+ kgio_wait_writable(wait_time)
65
+ when nil
66
+ return
67
+ when String
68
+ buf = rv
69
+ end while true
70
+ end
71
+ end # class UpstreamSocket
72
+
73
+ class UpstreamResponse < Kcar::Response # :nodoc:
74
+ # Called by the Rack server at the end of a successful response
75
+ def close
76
+ reusable = @parser.keepalive? && @parser.body_eof?
77
+ super
78
+ @pool.put(self) if reusable
79
+ nil
80
+ end
81
+
82
+ # req is just a string buffer of HTTP headers
83
+ def req_write(req, timeout)
84
+ @sock.req_write(req, timeout)
85
+ end
86
+
87
+ # returns true if the socket is still alive, nil if dead
88
+ def sock_alive?
89
+ @reused = (:wait_readable == (@sock.kgio_tryread(1) rescue nil)) ?
90
+ true : @sock.close
91
+ end
92
+
93
+ # returns true if the socket was reused and thus retryable
94
+ def fail_retryable?
95
+ @sock.close
96
+ @reused
97
+ end
98
+
99
+ def initialize(sock, pool)
100
+ super(sock)
101
+ @reused = false
102
+ @pool = pool
103
+ end
104
+ end # class UpstreamResponse
105
+
106
+ # take a responder from the pool, we'll add the object back to the
107
+ # pool in UpstreamResponse#close
108
+ def responder_get
109
+ while obj = @pool.get
110
+ return obj if obj.sock_alive?
111
+ end
112
+
113
+ UpstreamResponse.new(UpstreamSocket.start(@sockaddr), @pool)
114
+ end
115
+
116
+ def initialize(dest, timeout = 5)
117
+ case dest
118
+ when %r{\Ahttp://([^/]+)(/.*)\z}
119
+ path = $2
120
+ host, port = $1.split(/:/)
121
+ @sockaddr = Socket.sockaddr_in(port || 80, host)
122
+
123
+ # methods from Rack::Request we want:
124
+ allow = %w(fullpath host_with_port host port url path)
125
+ @path = path
126
+ want = path.scan(/\$(\w+)/).flatten! || []
127
+ diff = want - allow
128
+ diff.empty? or
129
+ raise ArgumentError, "vars not allowed: #{diff.uniq.join(' ')}"
130
+ else
131
+ raise ArgumentError, "destination must be an HTTP URL"
132
+ end
133
+ @pool = ConnPool.new
134
+ @timeout = timeout
135
+ end
136
+
137
+ def call(env)
138
+ case request_method = env["REQUEST_METHOD"]
139
+ when "GET", "HEAD" # OK
140
+ else
141
+ return [ 405, [%w(Content-Length 0), %w(Content-Length 0)], [] ]
142
+ end
143
+
144
+ req = Rack::Request.new(env)
145
+ path = @path.gsub(/\$(\w+)/) { req.__send__($1.to_sym) }
146
+ req = "#{request_method} #{path} HTTP/1.1\r\n" \
147
+ "X-Forwarded-For: #{env["REMOTE_ADDR"]}\r\n"
148
+
149
+ # pass most HTTP_* headers through as-is
150
+ chunked = false
151
+ env.each do |key, val|
152
+ %r{\AHTTP_(\w+)\z} =~ key or next
153
+ key = $1
154
+ next if %r{\A(?:VERSION|CONNECTION|KEEP_ALIVE|X_FORWARDED_FOR)} =~ key
155
+ chunked = true if %r{\ATRANSFER_ENCODING} =~ key && val =~ /\bchunked\b/i
156
+ key.tr!("_", "-")
157
+ req << "#{key}: #{val}\r\n"
158
+ end
159
+
160
+ # special cases which Rack does not prefix:
161
+ ctype = env["CONTENT_TYPE"] and req << "Content-Type: #{ctype}\r\n"
162
+ clen = env["CONTENT_LENGTH"] and req << "Content-Length: #{clen}\r\n"
163
+ req << "\r\n"
164
+
165
+ # get an open socket and send the headers
166
+ ures = responder_get
167
+ ures.req_write(req, @timeout)
168
+
169
+ # send the request body if there was one
170
+ send_body(env["rack.input"], ures, chunked) if chunked || clen
171
+
172
+ # wait for the response here
173
+ status, header, body = res = ures.rack
174
+
175
+ # don't let the upstream Connection and Keep-Alive headers leak through
176
+ header.delete_if do |k,_|
177
+ k =~ /\A(?:Connection|Keep-Alive)\z/i
178
+ end
179
+
180
+ case request_method
181
+ when "HEAD"
182
+ # kcar doesn't know if it's a HEAD or GET response, and HEAD
183
+ # responses have Content-Length in it which fools kcar...
184
+ body.parser.body_bytes_left = 0
185
+ res[1] = header.dup
186
+ body.close # clobbers original header
187
+ res[2] = body = []
188
+ end
189
+ res
190
+ rescue => e
191
+ retry if ures && ures.fail_retryable? && request_method != "POST"
192
+ ERROR_502
193
+ end
194
+
195
+ def send_body(input, ures, chunked)
196
+ buf = Thread.current[:proxy_pass_buf] ||= ""
197
+
198
+ if chunked # unlikely
199
+ while input.read(16384, buf)
200
+ buf.replace("#{buf.size.to_s(16)}\r\n#{buf}\r\n")
201
+ ures.req_write(buf, @timeout)
202
+ end
203
+ ures.req_write("0\r\n\r\n")
204
+ else # common if we hit uploads
205
+ while input.read(16384, buf)
206
+ ures.req_write(buf, @timeout)
207
+ end
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,208 @@
1
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
2
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
3
+ require 'time'
4
+ require 'rack/utils'
5
+ require 'rack/mime'
6
+ require 'kgio'
7
+
8
+ class TryGzipStatic
9
+ attr_accessor :root
10
+ class KF < Kgio::File
11
+ # attr_writer :sf_range
12
+
13
+ # only used if the server does not handle #to_path,
14
+ # yahns should never hit this
15
+ def each
16
+ raise "we should never get here in yahns"
17
+ buf = ""
18
+ rsize = 8192
19
+ if @sf_range
20
+ file.seek(@sf_range.begin)
21
+ sf_count = @sf_range.end - @sf_range.begin + 1
22
+ while sf_count > 0
23
+ read(sf_count > rsize ? rsize : sf_count, buf) or break
24
+ sf_count -= buf.size
25
+ yield buf
26
+ end
27
+ raise "file truncated" if sf_count != 0
28
+ else
29
+ yield(buf) while read(rsize, buf)
30
+ end
31
+ end
32
+ end
33
+
34
+ def initialize(root, default_type = 'text/plain')
35
+ @root = root
36
+ @default_type = default_type
37
+ end
38
+
39
+ def fspath(env)
40
+ path_info = Rack::Utils.unescape(env["PATH_INFO"])
41
+ path_info =~ /\.\./ ? nil : "#@root#{path_info}"
42
+ end
43
+
44
+ def get_range(env, path, st)
45
+ if ims = env["HTTP_IF_MODIFIED_SINCE"]
46
+ return [ 304, {}, [] ] if st.mtime.httpdate == ims
47
+ end
48
+
49
+ size = st.size
50
+ ranges = Rack::Utils.byte_ranges(env, size)
51
+ if ranges.nil? || ranges.length > 1
52
+ [ 200 ] # serve the whole thing, possibly with static gzip \o/
53
+ elsif ranges.empty?
54
+ res = r(416)
55
+ res[1]["Content-Range"] = "bytes */#{size}"
56
+ res
57
+ else # partial response, no using static gzip file
58
+ range = ranges[0]
59
+ len = range.end - range.begin + 1
60
+ h = fheader(env, path, st, nil, len)
61
+ h["Content-Range"] = "bytes #{range.begin}-#{range.end}/#{size}"
62
+ [ 206, h, range ]
63
+ end
64
+ end
65
+
66
+ def fheader(env, path, st, gz_st = nil, len = nil)
67
+ if path =~ /(.[^.]+)\z/
68
+ mime = Rack::Mime.mime_type($1, @default_type)
69
+ else
70
+ mime = @default_type
71
+ end
72
+ len ||= (gz_st ? gz_st : st).size
73
+ h = {
74
+ "Content-Type" => mime,
75
+ "Content-Length" => len.to_s,
76
+ "Last-Modified" => st.mtime.httpdate,
77
+ "Accept-Ranges" => "bytes",
78
+ }
79
+ h["Content-Encoding"] = "gzip" if gz_st
80
+ h
81
+ end
82
+
83
+ def head_no_gz(res, env, path, st)
84
+ res[1] = fheader(env, path, st)
85
+ res[2] = [] # empty body
86
+ res
87
+ end
88
+
89
+ def stat_path(env)
90
+ path = fspath(env) or return r(403)
91
+ begin
92
+ st = File.stat(path)
93
+ st.file? ? [ path, st ] : r(404)
94
+ rescue Errno::ENOENT
95
+ r(404)
96
+ rescue Errno::EACCES
97
+ r(403)
98
+ rescue => e
99
+ r(500, e.message, env)
100
+ end
101
+ end
102
+
103
+ def head(env)
104
+ path, st = res = stat_path(env)
105
+ return res if Integer === path # integer status code on failure
106
+
107
+ # see if it's a range request, no gzipped version if so
108
+ status, _ = res = get_range(env, path, st)
109
+ case status
110
+ when 206
111
+ res[2] = [] # empty body, headers are all set
112
+ res
113
+ when 200 # fall through to trying gzipped version
114
+ # client requested gzipped path explicitly or did not want gzip
115
+ if path =~ /\.gz\z/i || !want_gzip?(env)
116
+ head_no_gz(res, env, path, st)
117
+ else # try the gzipped version
118
+ begin
119
+ gz_st = File.stat("#{path}.gz")
120
+ if gz_st.mtime == st.mtime
121
+ res[1] = fheader(env, path, st, gz_st)
122
+ res[2] = []
123
+ res
124
+ else
125
+ head_no_gz(res, env, path, st)
126
+ end
127
+ rescue Errno::ENOENT, Errno::EACCES
128
+ head_no_gz(res, env, path, st)
129
+ rescue => e
130
+ r(500, e.message, env)
131
+ end
132
+ end
133
+ else # 416, 304
134
+ res
135
+ end
136
+ end
137
+
138
+ def call(env)
139
+ case env["REQUEST_METHOD"]
140
+ when "GET" then get(env)
141
+ when "HEAD" then head(env)
142
+ else r(405)
143
+ end
144
+ end
145
+
146
+ def want_gzip?(env)
147
+ env["HTTP_ACCEPT_ENCODING"] =~ /\bgzip\b/i
148
+ end
149
+
150
+ def get(env)
151
+ path, st, _ = res = stat_path(env)
152
+ return res if Integer === path # integer status code on failure
153
+
154
+ # see if it's a range request, no gzipped version if so
155
+ status, _, _ = res = get_range(env, path, st)
156
+ case status
157
+ when 206
158
+ res[2] = KF.open(path) # stat succeeded
159
+ when 200
160
+ # client requested gzipped path explicitly or did not want gzip
161
+ if path =~ /\.gz\z/i || !want_gzip?(env)
162
+ res[1] = fheader(env, path, st)
163
+ res[2] = KF.open(path)
164
+ else
165
+ case gzbody = KF.tryopen("#{path}.gz")
166
+ when KF
167
+ gz_st = gzbody.stat
168
+ if gz_st.file? && gz_st.mtime == st.mtime
169
+ # yay! serve the gzipped version as the regular one
170
+ # this should be the most likely code path
171
+ res[1] = fheader(env, path, st, gz_st)
172
+ res[2] = gzbody
173
+ else
174
+ gzbody.close
175
+ res[1] = fheader(env, path, st)
176
+ res[2] = KF.open(path)
177
+ end
178
+ when :ENOENT, :EACCES
179
+ res[1] = fheader(env, path, st)
180
+ res[2] = KF.open(path)
181
+ else
182
+ res = r(500, gzbody.to_s, env)
183
+ end
184
+ end
185
+ end
186
+ res
187
+ rescue Errno::ENOENT # could get here from a race
188
+ r(404)
189
+ rescue Errno::EACCES # could get here from a race
190
+ r(403)
191
+ rescue => e
192
+ r(500, e.message, env)
193
+ end
194
+
195
+ def r(code, msg = nil, env = nil)
196
+ if env && logger = env["rack.logger"]
197
+ logger.warn("#{env['REQUEST_METHOD']} #{env['PATH_INFO']} " \
198
+ "#{code} #{msg.inspect}")
199
+ end
200
+
201
+ if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(code)
202
+ [ code, {}, [] ]
203
+ else
204
+ h = { 'Content-Type' => 'text/plain', 'Content-Length' => '0' }
205
+ [ code, h, [] ]
206
+ end
207
+ end
208
+ end