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.
Files changed (58) hide show
  1. data/.autotest +5 -0
  2. data/.gitignore +9 -0
  3. data/.rspec +2 -0
  4. data/Gemfile +10 -0
  5. data/README.md +3 -0
  6. data/Rakefile +4 -0
  7. data/bin/yarn +24 -0
  8. data/cucumber.yml +3 -0
  9. data/features/concurrency.feature +15 -0
  10. data/features/dynamic_request.feature +13 -0
  11. data/features/logger.feature +16 -0
  12. data/features/rack.feature +18 -0
  13. data/features/server.feature +18 -0
  14. data/features/static_request.feature +25 -0
  15. data/features/step_definitions/concurrency_steps.rb +34 -0
  16. data/features/step_definitions/rack_steps.rb +3 -0
  17. data/features/step_definitions/server_steps.rb +42 -0
  18. data/features/step_definitions/web_steps.rb +7 -0
  19. data/features/support/env.rb +20 -0
  20. data/features/support/hooks.rb +5 -0
  21. data/lib/rack/handler/yarn.rb +21 -0
  22. data/lib/yarn.rb +22 -0
  23. data/lib/yarn/directory_lister.rb +62 -0
  24. data/lib/yarn/error_page.rb +16 -0
  25. data/lib/yarn/logging.rb +30 -0
  26. data/lib/yarn/parslet_parser.rb +114 -0
  27. data/lib/yarn/rack_handler.rb +42 -0
  28. data/lib/yarn/request_handler.rb +202 -0
  29. data/lib/yarn/response.rb +40 -0
  30. data/lib/yarn/server.rb +59 -0
  31. data/lib/yarn/statuses.rb +54 -0
  32. data/lib/yarn/version.rb +3 -0
  33. data/lib/yarn/worker.rb +19 -0
  34. data/lib/yarn/worker_pool.rb +36 -0
  35. data/spec/helpers.rb +84 -0
  36. data/spec/spec_helper.rb +23 -0
  37. data/spec/yarn/directory_lister_spec.rb +46 -0
  38. data/spec/yarn/error_page_spec.rb +33 -0
  39. data/spec/yarn/logging_spec.rb +52 -0
  40. data/spec/yarn/parslet_parser_spec.rb +99 -0
  41. data/spec/yarn/rack_handler_spec.rb +43 -0
  42. data/spec/yarn/request_handler_spec.rb +240 -0
  43. data/spec/yarn/response_spec.rb +36 -0
  44. data/spec/yarn/server_spec.rb +34 -0
  45. data/spec/yarn/worker_pool_spec.rb +23 -0
  46. data/spec/yarn/worker_spec.rb +26 -0
  47. data/test_objects/.gitignore +9 -0
  48. data/test_objects/1.rb +1 -0
  49. data/test_objects/3.rb +1 -0
  50. data/test_objects/5.rb +1 -0
  51. data/test_objects/app.rb +6 -0
  52. data/test_objects/app2.rb +6 -0
  53. data/test_objects/config.ru +54 -0
  54. data/test_objects/index.html +13 -0
  55. data/test_objects/jquery.js +8865 -0
  56. data/test_objects/simple_rack.rb +12 -0
  57. data/yarn.gemspec +34 -0
  58. 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
@@ -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