yarn 0.0.1
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.
- data/.autotest +5 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/Gemfile +10 -0
- data/README.md +3 -0
- data/Rakefile +4 -0
- data/bin/yarn +24 -0
- data/cucumber.yml +3 -0
- data/features/concurrency.feature +15 -0
- data/features/dynamic_request.feature +13 -0
- data/features/logger.feature +16 -0
- data/features/rack.feature +18 -0
- data/features/server.feature +18 -0
- data/features/static_request.feature +25 -0
- data/features/step_definitions/concurrency_steps.rb +34 -0
- data/features/step_definitions/rack_steps.rb +3 -0
- data/features/step_definitions/server_steps.rb +42 -0
- data/features/step_definitions/web_steps.rb +7 -0
- data/features/support/env.rb +20 -0
- data/features/support/hooks.rb +5 -0
- data/lib/rack/handler/yarn.rb +21 -0
- data/lib/yarn.rb +22 -0
- data/lib/yarn/directory_lister.rb +62 -0
- data/lib/yarn/error_page.rb +16 -0
- data/lib/yarn/logging.rb +30 -0
- data/lib/yarn/parslet_parser.rb +114 -0
- data/lib/yarn/rack_handler.rb +42 -0
- data/lib/yarn/request_handler.rb +202 -0
- data/lib/yarn/response.rb +40 -0
- data/lib/yarn/server.rb +59 -0
- data/lib/yarn/statuses.rb +54 -0
- data/lib/yarn/version.rb +3 -0
- data/lib/yarn/worker.rb +19 -0
- data/lib/yarn/worker_pool.rb +36 -0
- data/spec/helpers.rb +84 -0
- data/spec/spec_helper.rb +23 -0
- data/spec/yarn/directory_lister_spec.rb +46 -0
- data/spec/yarn/error_page_spec.rb +33 -0
- data/spec/yarn/logging_spec.rb +52 -0
- data/spec/yarn/parslet_parser_spec.rb +99 -0
- data/spec/yarn/rack_handler_spec.rb +43 -0
- data/spec/yarn/request_handler_spec.rb +240 -0
- data/spec/yarn/response_spec.rb +36 -0
- data/spec/yarn/server_spec.rb +34 -0
- data/spec/yarn/worker_pool_spec.rb +23 -0
- data/spec/yarn/worker_spec.rb +26 -0
- data/test_objects/.gitignore +9 -0
- data/test_objects/1.rb +1 -0
- data/test_objects/3.rb +1 -0
- data/test_objects/5.rb +1 -0
- data/test_objects/app.rb +6 -0
- data/test_objects/app2.rb +6 -0
- data/test_objects/config.ru +54 -0
- data/test_objects/index.html +13 -0
- data/test_objects/jquery.js +8865 -0
- data/test_objects/simple_rack.rb +12 -0
- data/yarn.gemspec +34 -0
- metadata +227 -0
@@ -0,0 +1,114 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'parslet'
|
3
|
+
|
4
|
+
module Parslet::Atoms::DSL; def once; repeat(1); end; end
|
5
|
+
|
6
|
+
module Yarn
|
7
|
+
class ParsletParser < Parslet::Parser
|
8
|
+
|
9
|
+
# general rules
|
10
|
+
|
11
|
+
rule(:crlf) { str("\r\n") | str("\n") }
|
12
|
+
|
13
|
+
rule(:space) { match('\s') }
|
14
|
+
|
15
|
+
rule(:spaces) { match('\s+') }
|
16
|
+
|
17
|
+
# header rules
|
18
|
+
|
19
|
+
rule(:header_value) { match['^\r\n'].once }
|
20
|
+
|
21
|
+
rule(:header_name) { match['a-zA-Z\-'].once }
|
22
|
+
|
23
|
+
rule(:header) do
|
24
|
+
header_name.as(:name) >>
|
25
|
+
str(":") >>
|
26
|
+
space.maybe >>
|
27
|
+
header_value.as(:value).maybe >>
|
28
|
+
crlf.maybe
|
29
|
+
end
|
30
|
+
|
31
|
+
# request-line rules
|
32
|
+
|
33
|
+
rule(:http_version) { match['HTTP\/\d\.\d'].once }
|
34
|
+
|
35
|
+
rule(:param_value) { match['^&\s+'].once }
|
36
|
+
|
37
|
+
rule(:param_name) { match['^=+'].once }
|
38
|
+
|
39
|
+
rule(:param) do
|
40
|
+
param_name.as(:name) >>
|
41
|
+
str("=") >>
|
42
|
+
param_value.as(:value) >>
|
43
|
+
str("&").maybe
|
44
|
+
end
|
45
|
+
|
46
|
+
rule(:query) do
|
47
|
+
match['\S+'].repeat(1)
|
48
|
+
end
|
49
|
+
|
50
|
+
rule(:path) do
|
51
|
+
match['^\?'].repeat(1).as(:path) >> str("?") >> query.as(:query) | match['^\s'].once.as(:path)
|
52
|
+
end
|
53
|
+
|
54
|
+
rule(:port) { match['\d+'].repeat(1) }
|
55
|
+
|
56
|
+
rule(:host) { match['^\/:'].once }
|
57
|
+
|
58
|
+
rule(:absolute_uri) do
|
59
|
+
str("http://") >>
|
60
|
+
host.as(:host) >>
|
61
|
+
str(":").maybe >>
|
62
|
+
port.as(:port).maybe >>
|
63
|
+
path
|
64
|
+
end
|
65
|
+
|
66
|
+
rule(:request_uri) { str('*') | absolute_uri | path }
|
67
|
+
|
68
|
+
rule(:spaces) { match('\s+') }
|
69
|
+
|
70
|
+
rule(:method) { match['OPTIONS|GET|HEAD|POST|PUT|DELETE|TRACE|CONNECT'].once }
|
71
|
+
|
72
|
+
# RFC2616: Method SP Request-URI SP HTTP-Version CRLF
|
73
|
+
rule(:request_line) do
|
74
|
+
method.as(:method) >>
|
75
|
+
space >>
|
76
|
+
request_uri.as(:uri) >>
|
77
|
+
space >>
|
78
|
+
http_version.as(:version) >>
|
79
|
+
crlf.maybe
|
80
|
+
end
|
81
|
+
|
82
|
+
# RFC2616: Request-Line *(( header ) CRLF) CRLF [ message-body ]
|
83
|
+
rule(:request) do
|
84
|
+
request_line >>
|
85
|
+
header.repeat.as(:_process_headers).as(:headers) >>
|
86
|
+
crlf.maybe
|
87
|
+
end
|
88
|
+
|
89
|
+
# starts parsing from the beginning using the :request rule
|
90
|
+
root(:request)
|
91
|
+
|
92
|
+
def run(input)
|
93
|
+
tree = parse input
|
94
|
+
tree = ParamsTransformer.new.apply tree
|
95
|
+
HeadersTransformer.new.apply tree
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
class ParamsTransformer < Parslet::Transform
|
100
|
+
rule(:_process_params => subtree(:params)) do
|
101
|
+
hash = {}
|
102
|
+
params.each { |h| hash[h[:name].to_s] = h[:value] }
|
103
|
+
hash
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
class HeadersTransformer < Parslet::Transform
|
108
|
+
rule(:_process_headers => subtree(:headers)) do
|
109
|
+
hash = {}
|
110
|
+
headers.each { |h| hash[h[:name].to_s] = h[:value].to_s }
|
111
|
+
hash
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'rack'
|
2
|
+
require 'pry'
|
3
|
+
|
4
|
+
module Yarn
|
5
|
+
class RackHandler < RequestHandler
|
6
|
+
|
7
|
+
def initialize(app, opts)
|
8
|
+
super(opts)
|
9
|
+
@host,@port = opts[:host], opts[:port]
|
10
|
+
@app = app
|
11
|
+
end
|
12
|
+
|
13
|
+
def prepare_response
|
14
|
+
begin
|
15
|
+
env = make_env
|
16
|
+
@response.content = @app.call(env)
|
17
|
+
rescue Exception => e
|
18
|
+
log e.message
|
19
|
+
log e.backtrace
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def make_env
|
24
|
+
env = {
|
25
|
+
"REQUEST_METHOD" => @request[:method].to_s,
|
26
|
+
"PATH_INFO" => @request[:uri][:path].to_s,
|
27
|
+
"QUERY_STRING" => @request[:uri][:query].to_s,
|
28
|
+
"SERVER_NAME" => @host || @request[:uri][:host].to_s,
|
29
|
+
"SERVER_PORT" => @port.to_s || @request[:uri][:port].to_s,
|
30
|
+
"SCRIPT_NAME" => "",
|
31
|
+
"rack.input" => StringIO.new("").set_encoding(Encoding::ASCII_8BIT),
|
32
|
+
"rack.version" => Rack::VERSION,
|
33
|
+
"rack.errors" => $output,
|
34
|
+
"rack.multithread" => true,
|
35
|
+
"rack.multiprocess" => false,
|
36
|
+
"rack.run_once" => false,
|
37
|
+
"rack.url_scheme" => "http"
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,202 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'rubygems'
|
3
|
+
require 'parslet'
|
4
|
+
|
5
|
+
module Yarn
|
6
|
+
|
7
|
+
class EmptyRequestError < StandardError; end
|
8
|
+
class ProcessingError < StandardError; end
|
9
|
+
|
10
|
+
class RequestHandler
|
11
|
+
|
12
|
+
include Logging
|
13
|
+
include ErrorPage
|
14
|
+
|
15
|
+
attr_accessor :session, :parser, :request, :response
|
16
|
+
|
17
|
+
def initialize(options={})
|
18
|
+
@parser = ParsletParser.new
|
19
|
+
@response = Response.new
|
20
|
+
end
|
21
|
+
|
22
|
+
def run(session)
|
23
|
+
@response = Response.new
|
24
|
+
set_common_headers
|
25
|
+
@session = session
|
26
|
+
begin
|
27
|
+
parse_request
|
28
|
+
prepare_response
|
29
|
+
return_response
|
30
|
+
log "Served (#{STATUS_CODES[@response.status]}) #{client_address} #{request_path}"
|
31
|
+
rescue EmptyRequestError
|
32
|
+
log "Empty request from #{client_address}"
|
33
|
+
ensure
|
34
|
+
close_connection
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def parse_request
|
39
|
+
raw_request = read_request
|
40
|
+
raise EmptyRequestError if raw_request.empty?
|
41
|
+
|
42
|
+
begin
|
43
|
+
@request = @parser.run raw_request
|
44
|
+
rescue Parslet::ParseFailed => e
|
45
|
+
@response.status = 400
|
46
|
+
debug "Parse failed: #{@request}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def prepare_response
|
51
|
+
path = extract_path
|
52
|
+
|
53
|
+
@response.headers["Content-Type"] = "text/html"
|
54
|
+
|
55
|
+
begin
|
56
|
+
if File.directory? path
|
57
|
+
serve_directory path
|
58
|
+
elsif File.exists?(path)
|
59
|
+
if path =~ /.*\.rb$/
|
60
|
+
@response.body << execute_script(path)
|
61
|
+
@response.status = 200
|
62
|
+
else
|
63
|
+
serve_file(path)
|
64
|
+
end
|
65
|
+
else
|
66
|
+
serve_404_page
|
67
|
+
end
|
68
|
+
rescue ProcessingError
|
69
|
+
log "An error occured processing #{path}"
|
70
|
+
serve_500_page
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def execute_script(path)
|
75
|
+
response = `ruby #{path}`
|
76
|
+
if !! ($?.to_s =~ /1$/)
|
77
|
+
raise ProcessingError
|
78
|
+
else
|
79
|
+
response
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def return_response
|
84
|
+
@session.puts "HTTP/1.1 #{@response.status} #{STATUS_CODES[@response.status]}"
|
85
|
+
@session.puts @response.headers.map { |k,v| "#{k}: #{v}" }
|
86
|
+
@session.puts ""
|
87
|
+
|
88
|
+
@response.body.each do |line|
|
89
|
+
@session.puts line
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def close_connection
|
94
|
+
if @session #&& !persistent?
|
95
|
+
@session.close
|
96
|
+
else
|
97
|
+
# TODO: start some kind of timeout
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def read_request
|
102
|
+
input = []
|
103
|
+
while (line = @session.gets) do
|
104
|
+
break if line.length <= 2
|
105
|
+
input << line
|
106
|
+
end
|
107
|
+
input.join
|
108
|
+
end
|
109
|
+
|
110
|
+
def persistent?
|
111
|
+
return @request[:headers]["Connection"] == "keep-alive"
|
112
|
+
end
|
113
|
+
|
114
|
+
def set_common_headers
|
115
|
+
@response.headers[:Server] = "Yarn webserver v#{VERSION}"
|
116
|
+
|
117
|
+
# HTTP date format: Fri, 31 Dec 1999 23:59:59 GMT
|
118
|
+
time = DateTime.now.new_offset(0)
|
119
|
+
@response.headers[:Date] = time.strftime("%a, %d %b %Y %H:%M:%S GMT")
|
120
|
+
# Close connection header ( until support for persistent connections )
|
121
|
+
@response.headers[:Connection] = "Close"
|
122
|
+
end
|
123
|
+
|
124
|
+
def serve_file(path)
|
125
|
+
@response.status = 200
|
126
|
+
@response.body << read_file(path)
|
127
|
+
@response.headers["Content-Type"] = get_mime_type path
|
128
|
+
end
|
129
|
+
|
130
|
+
def serve_directory(path)
|
131
|
+
@response.status = 200
|
132
|
+
if File.exists?("index.html") || File.exists?("/index.html")
|
133
|
+
@response.body = read_file "index.html"
|
134
|
+
@response.headers["Content-Type"] = "text/html"
|
135
|
+
else
|
136
|
+
@response.headers["Content-Type"] = "text/html"
|
137
|
+
directory_lister = DirectoryLister.new
|
138
|
+
@response.body << directory_lister.list(path)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def read_file(path)
|
143
|
+
file_contents = []
|
144
|
+
|
145
|
+
File.open(path, "r") do |file|
|
146
|
+
while (line = file.gets) do
|
147
|
+
file_contents << line
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
file_contents
|
152
|
+
end
|
153
|
+
|
154
|
+
def extract_path
|
155
|
+
path = @request[:uri][:path].to_s
|
156
|
+
if path[0] == "/" && path != "/"
|
157
|
+
path = path[1..-1]
|
158
|
+
end
|
159
|
+
path.gsub(/%20/, " ").strip
|
160
|
+
end
|
161
|
+
|
162
|
+
def serve_directory(path)
|
163
|
+
@response.status = 200
|
164
|
+
if File.exists?("index.html")# || File.exists?("/index.html")
|
165
|
+
@response.body = read_file "index.html"
|
166
|
+
@response.headers["Content-Type"] = "text/html"
|
167
|
+
else
|
168
|
+
@response.headers["Content-Type"] = "text/html"
|
169
|
+
@response.body << DirectoryLister.list(path)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def get_mime_type(path)
|
174
|
+
return false unless path.include? '.'
|
175
|
+
filetype = path.split('.').last
|
176
|
+
|
177
|
+
return case
|
178
|
+
when ["html", "htm"].include?(filetype)
|
179
|
+
"text/html"
|
180
|
+
when "txt" == filetype
|
181
|
+
"text/plain"
|
182
|
+
when "css" == filetype
|
183
|
+
"text/css"
|
184
|
+
when "js" == filetype
|
185
|
+
"text/javascript"
|
186
|
+
when ["png", "jpg", "jpeg", "gif", "tiff"].include?(filetype)
|
187
|
+
"image/#{filetype}"
|
188
|
+
when ["zip","pdf","postscript","x-tar","x-dvi"].include?(filetype)
|
189
|
+
"application/#{filetype}"
|
190
|
+
else false
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def request_path
|
195
|
+
@request[:uri][:path] if @request
|
196
|
+
end
|
197
|
+
|
198
|
+
def client_address
|
199
|
+
@session.peeraddr(:numeric)[2] if @session
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
|
2
|
+
module Yarn
|
3
|
+
class Response
|
4
|
+
|
5
|
+
attr_accessor :content
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@content = [nil, {}, []]
|
9
|
+
end
|
10
|
+
|
11
|
+
def status=(status)
|
12
|
+
@content[0] = status
|
13
|
+
end
|
14
|
+
|
15
|
+
def status
|
16
|
+
@content[0]
|
17
|
+
end
|
18
|
+
|
19
|
+
def headers=(headers)
|
20
|
+
@content[1] = headers
|
21
|
+
end
|
22
|
+
|
23
|
+
def headers
|
24
|
+
@content[1]
|
25
|
+
end
|
26
|
+
|
27
|
+
def body=(body)
|
28
|
+
@content[2] = body
|
29
|
+
end
|
30
|
+
|
31
|
+
def body
|
32
|
+
@content[2]
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_s
|
36
|
+
@content
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
data/lib/yarn/server.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'socket'
|
2
|
+
|
3
|
+
module Yarn
|
4
|
+
class Server
|
5
|
+
|
6
|
+
include Logging
|
7
|
+
|
8
|
+
attr_accessor :host, :port, :socket, :socket_listener
|
9
|
+
|
10
|
+
def initialize(app=nil,opts={})
|
11
|
+
# merge given options with default values
|
12
|
+
options = {
|
13
|
+
output: $stdout,
|
14
|
+
host: '127.0.0.1',
|
15
|
+
port: 3000
|
16
|
+
}.merge(opts)
|
17
|
+
|
18
|
+
@app = app
|
19
|
+
@host,@port,$output = options[:host], options[:port], options[:output]
|
20
|
+
|
21
|
+
@socket = TCPServer.new(@host, @port)
|
22
|
+
|
23
|
+
@handler = @app ? RackHandler.new(@app, options) : RequestHandler.new(options)
|
24
|
+
|
25
|
+
log "Yarn started #{"w/ Rack " if opts[:rackup_file]}and accepting requests on #{@host}:#{@port}"
|
26
|
+
end
|
27
|
+
|
28
|
+
def start
|
29
|
+
@socket_listener = Thread.new do
|
30
|
+
loop do
|
31
|
+
begin
|
32
|
+
session = @socket.accept
|
33
|
+
Thread.new { @handler.clone.run session }
|
34
|
+
rescue Exception => e
|
35
|
+
session.close
|
36
|
+
log e.message
|
37
|
+
log e.backtrace
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
begin
|
43
|
+
@socket_listener.join
|
44
|
+
rescue Interrupt => e
|
45
|
+
log "Caught interrupt, stopping..."
|
46
|
+
ensure
|
47
|
+
stop
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def stop
|
52
|
+
@socket.close if @socket
|
53
|
+
@socket = nil
|
54
|
+
@socket_listener.kill if @socket_listener
|
55
|
+
|
56
|
+
log "Server stopped"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|