httphere 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (9) hide show
  1. data/.document +5 -0
  2. data/.gitignore +21 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +21 -0
  5. data/Rakefile +55 -0
  6. data/VERSION +1 -0
  7. data/bin/httphere +624 -0
  8. data/httphere.gemspec +62 -0
  9. metadata +86 -0
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 BehindLogic
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,21 @@
1
+ = httphere
2
+
3
+ httphere is a very small and simple ruby command-line HTTP file server.
4
+
5
+ = To work on
6
+
7
+ * Improve content-negotiation behind the scenes. For example, a file found to be "application/x-ruby" could just as well be transferred as "text/plain".
8
+
9
+ == Note on Patches/Pull Requests
10
+
11
+ * Fork the project.
12
+ * Make your feature addition or bug fix.
13
+ * Add tests for it. This is important so I don't break it in a
14
+ future version unintentionally.
15
+ * Commit, do not mess with rakefile, version, or history.
16
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
17
+ * Send me a pull request. Bonus points for topic branches.
18
+
19
+ == Copyright
20
+
21
+ Copyright (c) 2010 BehindLogic. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,55 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "httphere"
8
+ gem.summary = %Q{A simple Ruby HTTP file server}
9
+ gem.description = %Q{httphere is a very small and simple ruby command-line HTTP file server.}
10
+ gem.email = "gems@behindlogic.com"
11
+ gem.homepage = "http://dcparker.github.com/httphere"
12
+ gem.authors = ["Daniel Parker"]
13
+ gem.add_dependency 'shared-mime-info'
14
+ gem.add_dependency 'chardet'
15
+ gem.post_install_message = "\n\033[34mhttphere wants to detect MIME-types! For this\nyou'll need to install the open-source shared-mime-info.\nAssuming you're on a Mac, you can simply run:\n\033[0m \033[31msudo port install shared-mime-info\033[0m\n\n * This gem has not been tested on Windows.\n\n"
16
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
17
+ end
18
+ Jeweler::GemcutterTasks.new
19
+ rescue LoadError
20
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
21
+ end
22
+
23
+ require 'rake/testtask'
24
+ Rake::TestTask.new(:test) do |test|
25
+ test.libs << 'lib' << 'test'
26
+ test.pattern = 'test/**/test_*.rb'
27
+ test.verbose = true
28
+ end
29
+
30
+ begin
31
+ require 'rcov/rcovtask'
32
+ Rcov::RcovTask.new do |test|
33
+ test.libs << 'test'
34
+ test.pattern = 'test/**/test_*.rb'
35
+ test.verbose = true
36
+ end
37
+ rescue LoadError
38
+ task :rcov do
39
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
40
+ end
41
+ end
42
+
43
+ task :test => :check_dependencies
44
+
45
+ task :default => :test
46
+
47
+ require 'rake/rdoctask'
48
+ Rake::RDocTask.new do |rdoc|
49
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
50
+
51
+ rdoc.rdoc_dir = 'rdoc'
52
+ rdoc.title = "httphere #{version}"
53
+ rdoc.rdoc_files.include('README*')
54
+ rdoc.rdoc_files.include('lib/**/*.rb')
55
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
data/bin/httphere ADDED
@@ -0,0 +1,624 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $VERSION = '0.0.1'
4
+ $DEBUG = false
5
+
6
+ require 'socket'
7
+ $options = {}
8
+
9
+ ENV['XDG_DATA_DIRS'] = (ENV['XDG_DATA_DIRS'].to_s.split(/:/) << '/opt/local/share').join(':')
10
+
11
+ require 'optparse'
12
+ optparse = OptionParser.new do |opts|
13
+ opts.banner = "Usage: #{$0} [options]"
14
+
15
+ $options[:port] = 80
16
+ opts.on( '-p PORT', '--port PORT', "Run on PORT. Defaults to #{$options[:port]}" ) do |port|
17
+ $options[:port] = port.to_i
18
+ end
19
+
20
+ # Gets the IP address for the default
21
+ # $options[:address] = Socket.getaddrinfo(Socket.gethostname, nil, 'AF_INET')[0][2]
22
+ $options[:address] = '0.0.0.0'
23
+ opts.on( '-a ADDRESS', '--address ADDRESS', "Listen on ADDRESS ip. Defaults to [#{$options[:address]}]") do |address|
24
+ $options[:address] = address
25
+ end
26
+
27
+ $options[:https_domain] = $options[:address]
28
+ end
29
+
30
+ optparse.parse!
31
+
32
+ require 'socket'
33
+ require 'openssl'
34
+
35
+ class ::OpenSSL::SSL::SSLSocket
36
+ alias :read_nonblock :readpartial
37
+ end
38
+
39
+ class EventMachineMini
40
+ DEFAULT_SSL_OPTIONS = Hash.new do |h,k|
41
+ case k
42
+ when :SSLCertificate
43
+ h[k] = OpenSSL::X509::Certificate.new(File.read(h[:SSLCertificateFile]))
44
+ when :SSLPrivateKey
45
+ h[k] = OpenSSL::PKey::RSA.new(File.read(h[:SSLPrivateKeyFile]))
46
+ else
47
+ nil
48
+ end
49
+ end
50
+
51
+ DEFAULT_SSL_OPTIONS.merge!(
52
+ :GenerateSSLCert => false,
53
+ :ServerSoftware => "Ruby TCP Router OpenSSL/#{::OpenSSL::OPENSSL_VERSION.split[1]}",
54
+ :SSLCertName => [['CN', $options[:https_domain]]],
55
+ :SSLCertComment => "Generated by Ruby/OpenSSL",
56
+ :SSLCertificateFile => 'cert.pem',
57
+ :SSLPrivateKeyFile => 'key.pem',
58
+ :SSLClientCA => nil,
59
+ :SSLExtraChainCert => nil,
60
+ :SSLCACertificateFile => 'cacert.pem',
61
+ :SSLCACertificatePath => nil,
62
+ :SSLCertificateStore => nil,
63
+ :SSLVerifyClient => ::OpenSSL::SSL::VERIFY_PEER,
64
+ :SSLVerifyDepth => 1,
65
+ :SSLVerifyCallback => nil, # custom verification
66
+ :SSLTimeout => nil,
67
+ :SSLOptions => nil,
68
+ :SSLStartImmediately => true
69
+ )
70
+
71
+ class << self
72
+ def ssl_config(config=DEFAULT_SSL_OPTIONS)
73
+ @ssl_config ||= config
74
+ end
75
+ end
76
+
77
+ def running?
78
+ Thread.current[:status] == :running
79
+ end
80
+ def stop!
81
+ Thread.current[:status] = :stop
82
+ end
83
+
84
+ attr_reader :router
85
+ def initialize(routes={})
86
+ @router = {}
87
+ # Set up the listening sockets
88
+ (routes[:listen] || {}).each do |ip_port,instantiate_klass|
89
+ ip, port = ip_port.split(/:/)
90
+ socket = TCPServer.new(ip, port)
91
+ socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1)
92
+ @router[socket] = instantiate_klass
93
+ # puts "Listening on #{ip_port} for #{instantiate_klass.name} messages..."
94
+ end
95
+ # Set up the listening SSL sockets
96
+ (routes[:ssl_listen] || {}).each do |ip_port,instantiate_klass|
97
+ ip, port = ip_port.split(/:/)
98
+ socket = TCPServer.new(ip, port)
99
+ socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1)
100
+
101
+ ssl_socket = ::OpenSSL::SSL::SSLServer.new(socket, ssl_context)
102
+ ssl_socket.start_immediately = self.class.ssl_config[:SSLStartImmediately]
103
+
104
+ @router[ssl_socket] = instantiate_klass
105
+ # puts "Listening on #{ip_port} (SSL) for #{instantiate_klass.name} messages..."
106
+ end
107
+ # Set up the connect sockets
108
+ (routes[:connect] || {}).each do |ip_port,args|
109
+ args = [args] unless args.is_a?(Array); instantiate_klass = args.shift
110
+ ip, port = ip_port.split(/:/)
111
+ socket = TCPSocket.new(ip, port)
112
+ clients[socket] = instantiate_klass.new(self,socket,*args)
113
+ # puts "Connecting to #{ip_port} for #{instantiate_klass.name} messages..."
114
+ end
115
+ end
116
+
117
+ def run
118
+ Thread.current[:status] = :running
119
+ # trap("INT") { stop! } # This will end the event loop within 0.5 seconds when you hit Ctrl+C
120
+ loop do
121
+ # log "tick #{Thread.current[:status]}\n" if $DEBUG
122
+
123
+ # Clean up any closed clients
124
+ clients.each_key do |sock|
125
+ if sock.closed?
126
+ conn = clients.delete(sock)
127
+ conn.upon_unbind if conn.respond_to?(:upon_unbind)
128
+ if conn.respond_to?(:key)
129
+ connections_by_key.delete(conn.key) rescue nil
130
+ end
131
+ end
132
+ end
133
+
134
+ if !running?
135
+ unless listen_sockets.empty?
136
+ # puts "Closing all listening ports."
137
+ shutdown_listeners!
138
+ end
139
+ if client_sockets.empty?
140
+ # It's the next time around after we closed all the client connections.
141
+ break
142
+ else
143
+ # puts "Closing all client connections."
144
+ close_all_clients!
145
+ end
146
+ end
147
+
148
+ # puts "Listening to #{listen_sockets.length} sockets: #{listen_sockets.inspect}"
149
+ begin
150
+ event = select(listen_sockets + client_sockets,nil,nil,0.5)
151
+ rescue IOError
152
+ next
153
+ end
154
+ if event.nil? # nil would be a timeout, we'd do nothing and start loop over. Of course here we really have no timeout...
155
+ @router.values.each { |klass|
156
+ klass.tick if klass.respond_to?(:tick)
157
+ }
158
+ else
159
+ event[0].each do |sock| # Iterate through all sockets that have pending activity
160
+ # puts "Event on socket #{sock.inspect}"
161
+ if listen_sockets.include?(sock) # Received a new connection to a listening socket
162
+ sock = accept_client(sock)
163
+ clients[sock].upon_new_connection if clients[sock].respond_to?(:upon_new_connection)
164
+ else # Activity on a client-connected socket
165
+ if sock.eof? # Socket's been closed by the client
166
+ log "Connection #{clients[sock].to_s} was closed by the client.\n"
167
+ sock.close
168
+ clients[sock].upon_unbind if clients[sock].respond_to?(:upon_unbind)
169
+ client = clients[sock]
170
+ clients.delete(sock)
171
+ else # Data in from the client
172
+ catch :stop_reading do
173
+ # puts "Reading data from socket #{sock.inspect} / #{clients[sock].inspect}"
174
+ begin
175
+ if sock.respond_to?(:read_nonblock)
176
+ # puts "read_nonblock"
177
+ 10.times {
178
+ data = sock.read_nonblock(4096)
179
+ clients[sock].receive_data(data)
180
+ }
181
+ else
182
+ # puts "sysread"
183
+ data = sock.sysread(4096)
184
+ clients[sock].receive_data(data)
185
+ end
186
+ rescue Errno::EAGAIN, Errno::EWOULDBLOCK, EOFError => e
187
+ # no-op. This will likely happen after every request, but that's expected. It ensures that we're done with the request's data.
188
+ rescue Errno::ECONNRESET, Errno::ECONNABORTED, Errno::ECONNREFUSED, IOError => e
189
+ log "Closed Err: #{e.inspect}\n"
190
+ sock.close
191
+ clients[sock].upon_unbind if clients[sock].respond_to?(:upon_unbind)
192
+ end
193
+ end
194
+ end
195
+ end
196
+ end
197
+ end
198
+ end
199
+ end
200
+
201
+ def clients
202
+ @clients ||= {}
203
+ end
204
+
205
+ def connections_by_key
206
+ @connections_by_key ||= {}
207
+ end
208
+
209
+ def close_all_clients!
210
+ # puts "Closing #{client_sockets.length} client connections..."
211
+ client_sockets.each { |socket| socket.close rescue nil }
212
+ end
213
+ def shutdown_listeners!
214
+ # puts "Shutting down #{listen_sockets.length} listeners..."
215
+ listen_sockets.each { |socket| socket.close rescue nil }
216
+ end
217
+
218
+ private
219
+ def listen_sockets
220
+ @router.keys.select {|socket| !socket.closed? }
221
+ end
222
+
223
+ def accept_client(source_socket)
224
+ client_socket = source_socket.accept
225
+ connection = @router[source_socket].new(self,client_socket)
226
+ clients[client_socket] = connection
227
+ end
228
+
229
+ def client_sockets
230
+ @clients.keys
231
+ end
232
+
233
+ def ssl_context
234
+ unless @ssl_context
235
+ @ssl_context = OpenSSL::SSL::SSLContext.new
236
+ @ssl_context.client_ca = self.class.ssl_config[:SSLClientCA]
237
+ @ssl_context.ca_file = self.class.ssl_config[:SSLCACertificateFile]
238
+ @ssl_context.ca_path = self.class.ssl_config[:SSLCACertificatePath]
239
+ @ssl_context.extra_chain_cert = self.class.ssl_config[:SSLExtraChainCert]
240
+ @ssl_context.cert_store = self.class.ssl_config[:SSLCertificateStore]
241
+ @ssl_context.verify_mode = self.class.ssl_config[:SSLVerifyClient]
242
+ @ssl_context.verify_depth = self.class.ssl_config[:SSLVerifyDepth]
243
+ @ssl_context.verify_callback = self.class.ssl_config[:SSLVerifyCallback]
244
+ @ssl_context.timeout = self.class.ssl_config[:SSLTimeout]
245
+ @ssl_context.options = self.class.ssl_config[:SSLOptions]
246
+ if self.class.ssl_config[:GenerateSSLCert]
247
+ puts "Generating SSL Certificate..."
248
+ @ssl_context.key = OpenSSL::PKey::RSA.generate(4096)
249
+ ca = OpenSSL::X509::Name.parse("/C=US/ST=Michigan/O=BehindLogic/CN=behindlogic.com/emailAddress=cert@desktopconnect.com")
250
+ cert = OpenSSL::X509::Certificate.new
251
+ cert.version = 2
252
+ cert.serial = 1
253
+ cert.subject = ca
254
+ cert.issuer = ca
255
+ cert.public_key = @ssl_context.key.public_key
256
+ cert.not_before = Time.now
257
+ cert.not_after = Time.now + 3600 # this http session should last no longer than 1 hour
258
+ @ssl_context.cert = cert
259
+ else
260
+ @ssl_context.cert = self.class.ssl_config[:SSLCertificate]
261
+ @ssl_context.key = self.class.ssl_config[:SSLPrivateKey]
262
+ end
263
+ end
264
+ @ssl_context
265
+ end
266
+ end
267
+
268
+ require 'base64'
269
+
270
+ module EventParsers
271
+
272
+ # Implement this by including it in a class and call receive_data on every read event.
273
+ # Callbacks available:
274
+ # upon_new_request(request) # after first HTTP line
275
+ # receive_header(request, header) # after each header is received
276
+ # upon_headers_finished(request) # after all headers are received
277
+ # process_request(request) # after the full request is received
278
+ module Http11Parser
279
+ module BasicAuth
280
+ ::UnparsableBasicAuth = Class.new(RuntimeError)
281
+
282
+ class << self
283
+ def parse(basic_auth_string)
284
+ # Do the special decoding here
285
+ if basic_auth_string =~ /^Basic (.*)$/
286
+ auth_string = $1
287
+ auth_plain = Base64.decode64(auth_string)
288
+ return auth_plain.split(/:/,2)
289
+ else
290
+ warn "Bad Auth string!"
291
+ raise UnparsableBasicAuth
292
+ end
293
+ end
294
+ end
295
+
296
+ def initialize(username, password)
297
+ @username = username
298
+ @password = password
299
+ end
300
+
301
+ def to_s
302
+ # Do the special encoding here
303
+ end
304
+ end
305
+
306
+ class HeaderAndEntityStateStore
307
+ attr_accessor :state, :delimiter, :linebuffer, :textbuffer, :entity_size, :entity_pos, :bogus_lines
308
+
309
+ def initialize(state, delimiter)
310
+ @state = state
311
+ @delimiter = delimiter
312
+ reset!
313
+ end
314
+
315
+ def reset!
316
+ @linebuffer = []
317
+ @textbuffer = []
318
+ @entity_size = nil
319
+ @entity_pos = 0
320
+ @bogus_lines = 0
321
+ end
322
+
323
+ def entity?
324
+ !entity_size.nil? && entity_size > 0
325
+ end
326
+
327
+ def bogus_line!(ln=nil)
328
+ log "bogus line:\n#{ln}\n" if ln && $DEBUG
329
+ @bogus_lines += 1
330
+ end
331
+ end
332
+
333
+ class Request
334
+ attr_accessor :connection, :method, :http_version, :resource_uri, :query_params, :headers, :entity, :response
335
+ def ready!
336
+ @ready = true
337
+ end
338
+ def ready?
339
+ !!@ready
340
+ end
341
+
342
+ def initialize(request_params={})
343
+ @http_version = request_params[:http_version] if request_params.has_key? :http_version
344
+ @resource_uri = request_params[:resource_uri] if request_params.has_key? :resource_uri
345
+ @headers = request_params[:headers] if request_params.has_key? :headers
346
+ @entity = request_params[:entity] if request_params.has_key? :entity
347
+
348
+ @headers ||= {}
349
+ @entity ||= ''
350
+ end
351
+
352
+ def inspect
353
+ "HTTP Request:\n\t#{@method} #{@resource_uri} HTTP/#{@http_version}\n\t#{@headers.map {|k,v| "#{k}: #{v}"}.join("\n\t")}\n\n\t#{@entity.to_s.gsub(/\n/,"\n\t")}"
354
+ end
355
+
356
+ def has_entity?
357
+ @entity != ''
358
+ end
359
+
360
+ def basic_auth
361
+ BasicAuth.parse(@headers['authorization']) if @headers['authorization']
362
+ end
363
+
364
+ def params
365
+ query_params
366
+ end
367
+ end
368
+
369
+ def self.included(base)
370
+ base.extend ClassMethods
371
+ end
372
+ module ClassMethods
373
+ def request_klass(klass=nil)
374
+ @request_klass ||= (klass || Request)
375
+ end
376
+ end
377
+
378
+ attr_reader :socket
379
+
380
+ def request_backlog
381
+ @request_backlog ||= []
382
+ end
383
+
384
+ def current_request
385
+ request_backlog.first
386
+ end
387
+ def parsing_request
388
+ request_backlog.last
389
+ end
390
+
391
+ HttpResponseRE = /\AHTTP\/(1.[01]) ([\d]{3})/i
392
+ HttpRequestRE = /^(GET|POST|PUT|DELETE) (\/.*) HTTP\/([\d\.]+)[\r\n]?$/i
393
+ BlankLineRE = /^[\n\r]+$/
394
+
395
+ def receive_data(data)
396
+ return unless (data and data.length > 0)
397
+
398
+ @last_activity = Time.now
399
+
400
+ case parser.state
401
+ when :init
402
+ if ix = data.index("\n")
403
+ parser.linebuffer << data[0...ix+1]
404
+ ln = parser.linebuffer.join
405
+ parser.linebuffer.clear
406
+ log "[#{parser.state}]: #{ln}" if $DEBUG
407
+ if ln =~ HttpRequestRE
408
+ request_backlog << self.class.request_klass.new
409
+ parsing_request.connection = self
410
+ method, resource_uri, http_version = parse_init_line(ln)
411
+ parsing_request.method = method
412
+ parsing_request.resource_uri = resource_uri
413
+ parsing_request.query_params = parse_query_string(resource_uri.index('?') ? resource_uri[(resource_uri.index('?')+1)..-1] : '')
414
+ parsing_request.http_version = http_version
415
+ upon_new_request(parsing_request) if respond_to?(:upon_new_request)
416
+ parser.state = :headers
417
+ else
418
+ parser.bogus_line!(ln)
419
+ end
420
+ receive_data(data[(ix+1)..-1])
421
+ else
422
+ parser.linebuffer << data
423
+ end
424
+ when :headers
425
+ if ix = data.index("\n")
426
+ parser.linebuffer << data[0...ix+1]
427
+ ln = parser.linebuffer.join
428
+ parser.linebuffer.clear
429
+ # If it's a blank line, move to content state
430
+ if ln =~ BlankLineRE
431
+ upon_headers_finished(parsing_request) if respond_to?(:upon_headers_finished)
432
+ if parser.entity?
433
+ # evaluate_headers(parsing_request.headers)
434
+ parser.state = :entity
435
+ else
436
+ receive_full_request
437
+ end
438
+ else
439
+ header = parse_header_line(ln)
440
+ log "\t[#{parser.state}]: #{header.inspect}\n" if $DEBUG
441
+ receive_header(parsing_request, header.to_a[0]) if respond_to?(:receive_header)
442
+ parsing_request.headers.merge!(header)
443
+ end
444
+ receive_data(data[(ix+1)..-1])
445
+ else
446
+ parser.linebuffer << data
447
+ end
448
+ when :entity
449
+ if parser.entity_size
450
+ chars_yet_needed = parser.entity_size - parser.entity_pos
451
+ taking_this_many = [chars_yet_needed, data.length].sort.first
452
+ parser.textbuffer << data[0...taking_this_many]
453
+ leftover_data = data[taking_this_many..-1]
454
+ parser.entity_pos += taking_this_many
455
+ if parser.entity_pos >= parser.entity_size
456
+ entity_data = parser.textbuffer.join
457
+ parser.textbuffer.clear
458
+ parsing_request.entity << entity_data
459
+ log "[#{parser.state}]: #{entity_data}\n" if $DEBUG
460
+ receive_full_request
461
+ end
462
+ receive_data(leftover_data)
463
+ else
464
+ raise "TODO!"
465
+ # receive_binary_data data
466
+ end
467
+
468
+ else
469
+ # Probably shouldn't ever be here?
470
+ raise "Shouldn't be here!"
471
+ end
472
+
473
+ # TODO: Exception if number of parser.bogus_lines is higher than threshold
474
+ end
475
+
476
+ def process_request(request)
477
+ warn "STUB - overwrite process_request in a subclass of Http11Parser to process this #{request.inspect}"
478
+ end
479
+
480
+ def send_response!(http_response)
481
+ log "Sending Response: #{http_response.inspect}\n" if $DEBUG
482
+ socket.write(http_response)
483
+ request_backlog.shift
484
+ # Process the next request IF it is already waiting.
485
+ process_request(current_request) if current_request && current_request.ready?
486
+ end
487
+
488
+ private
489
+ def parse_init_line(ln)
490
+ method, resource_uri, http_version = ln.match(HttpRequestRE).to_a[1..-1]
491
+ # TODO: Exception if the request is improper!
492
+ [method, resource_uri, http_version]
493
+ end
494
+
495
+ def parse_query_string(qstring)
496
+ params = (CGI.parse(qstring) || {}) rescue {}
497
+ params.inject({}) do |h,(k,v)|
498
+ h[k.to_sym] = (v.is_a?(Array) && v.length == 1) ? v[0] : v
499
+ h
500
+ end
501
+ end
502
+
503
+ def parse_header_line(ln)
504
+ ln.chomp!
505
+ if ln =~ /:/
506
+ name,value = ln.split(/:\s*/,2)
507
+ if name.downcase == 'content-length'
508
+ parser.entity_size = Integer(value.gsub(/\D/,''))
509
+ # TODO: Exception if content-length specified is too big
510
+ end
511
+ {name.downcase => value}
512
+ else
513
+ parser.bogus_line!(ln)
514
+ {}
515
+ end
516
+ end
517
+
518
+ def receive_full_request
519
+ parsing_request.ready!
520
+ parser.state = :init
521
+ # Process this request now that we're ready -- IF any previous requests are already responded to.
522
+ # Otherwise, this request will be waiting, and when the previous one(s) are responded to, this one
523
+ # will be triggered next.
524
+ process_request(parsing_request) if parsing_request == current_request
525
+ log "Processed request. Prepared for next request.\n\n" if $DEBUG
526
+ parser.reset!
527
+ end
528
+
529
+ def parser
530
+ @parser ||= HeaderAndEntityStateStore.new(:init, "\n")
531
+ end
532
+ end
533
+ end
534
+
535
+ require 'iconv'
536
+ require 'rubygems'
537
+ require 'UniversalDetector'
538
+ require 'shared-mime-info'
539
+ class FileServer < EventParsers::Http11Parser::Request
540
+ # This is where the routing is processed.
541
+ def process
542
+ # Get the filename desired
543
+ filename = resource_uri.sub(/^\//,'')
544
+ if File.exists?(filename) && !File.directory?(filename)
545
+ content_type = MIME.check(filename).type
546
+ file_body = File.read(filename)
547
+ respond!('200 Ok', content_type, file_body)
548
+ else
549
+ respond!('400 Not Found', 'text/plain', "Could not find file: '#{resource_uri}'")
550
+ end
551
+ end
552
+
553
+ def respond!(status, content_type, body)
554
+ respond(status, content_type, body)
555
+ connection.halt!
556
+ end
557
+ def respond(status, content_type, body)
558
+ # Convert to UTF-8 if possible
559
+ chardet = UniversalDetector::chardet(body)
560
+ if chardet['confidence'] > 0.7
561
+ charset = chardet['encoding']
562
+ body = Iconv.conv('utf-8', charset, body)
563
+ # else # no conversion
564
+ end
565
+ body_length = body.length
566
+
567
+ # Send the response!
568
+ connection.send_response! "HTTP/1.1 #{status}\r\nServer: HTTP-Here, version #{$VERSION}\r\nContent-Type: #{content_type}\r\nContent-Length: #{body_length+2}\r\n\r\n#{body}\r\n"
569
+ span = (Time.now - connection.time).to_f
570
+ content_type
571
+ puts (status =~ /200/ ?
572
+ "Served #{resource_uri} (#{content_type})" :
573
+ "404 #{resource_uri}"
574
+ ) + " at #{1 / span} requests/second"
575
+ end
576
+ end
577
+
578
+ # Requires that a module includes this and defines an initialize method that defines @options
579
+ class Http11Server
580
+ include EventParsers::Http11Parser
581
+ request_klass FileServer
582
+
583
+ attr_reader :time
584
+ def initialize(server,socket)
585
+ @socket = socket
586
+ @time = Time.now
587
+ end
588
+
589
+ # These just pass the call to the request to handle itself
590
+ def upon_new_request(request)
591
+ request.upon_new_request if request.respond_to?(:upon_new_request)
592
+ end
593
+ def upon_headers_finished(request)
594
+ request.upon_headers_finished if request.respond_to?(:upon_headers_finished)
595
+ end
596
+ def process_request(request)
597
+ request.process
598
+ end
599
+
600
+ def halt!
601
+ socket.close
602
+ throw :stop_reading
603
+ end
604
+
605
+ def upon_unbind
606
+ # puts "Client #{@socket} disconnected."
607
+ end
608
+ end
609
+
610
+ # EventMachineMini.ssl_config[:GenerateSSLCert] = true
611
+
612
+ puts "HTTP Here v#{$VERSION} : listening on #{$options[:address]}:#{$options[:port]}..."
613
+ begin
614
+ $server = EventMachineMini.new( :listen => {"#{$options[:address]}:#{$options[:port]}" => Http11Server} )
615
+ rescue Errno::EACCES
616
+ puts "\tLooks like you don't have permission to use that port!"
617
+ exit
618
+ end
619
+
620
+ Kernel.trap(:INT) { $server.stop! }
621
+ $server.run
622
+
623
+ puts " HTTP Here Finished\n"
624
+ exit
data/httphere.gemspec ADDED
@@ -0,0 +1,62 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{httphere}
8
+ s.version = "1.0.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Daniel Parker"]
12
+ s.date = %q{2010-01-06}
13
+ s.default_executable = %q{httphere}
14
+ s.description = %q{httphere is a very small and simple ruby command-line HTTP file server.}
15
+ s.email = %q{gems@behindlogic.com}
16
+ s.executables = ["httphere"]
17
+ s.extra_rdoc_files = [
18
+ "LICENSE",
19
+ "README.rdoc"
20
+ ]
21
+ s.files = [
22
+ ".document",
23
+ ".gitignore",
24
+ "LICENSE",
25
+ "README.rdoc",
26
+ "Rakefile",
27
+ "VERSION",
28
+ "bin/httphere",
29
+ "httphere.gemspec"
30
+ ]
31
+ s.homepage = %q{http://dcparker.github.com/httphere}
32
+ s.post_install_message = %q{
33
+ httphere wants to detect MIME-types! For this
34
+ you'll need to install the open-source shared-mime-info.
35
+ Assuming you're on a Mac, you can simply run:
36
+  sudo port install shared-mime-info
37
+
38
+ * This gem has not been tested on Windows.
39
+
40
+ }
41
+ s.rdoc_options = ["--charset=UTF-8"]
42
+ s.require_paths = ["lib"]
43
+ s.rubygems_version = %q{1.3.5}
44
+ s.summary = %q{A simple Ruby HTTP file server}
45
+
46
+ if s.respond_to? :specification_version then
47
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
48
+ s.specification_version = 3
49
+
50
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
51
+ s.add_runtime_dependency(%q<shared-mime-info>, [">= 0"])
52
+ s.add_runtime_dependency(%q<chardet>, [">= 0"])
53
+ else
54
+ s.add_dependency(%q<shared-mime-info>, [">= 0"])
55
+ s.add_dependency(%q<chardet>, [">= 0"])
56
+ end
57
+ else
58
+ s.add_dependency(%q<shared-mime-info>, [">= 0"])
59
+ s.add_dependency(%q<chardet>, [">= 0"])
60
+ end
61
+ end
62
+
metadata ADDED
@@ -0,0 +1,86 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: httphere
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Daniel Parker
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-01-06 00:00:00 -05:00
13
+ default_executable: httphere
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: shared-mime-info
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: chardet
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: "0"
34
+ version:
35
+ description: httphere is a very small and simple ruby command-line HTTP file server.
36
+ email: gems@behindlogic.com
37
+ executables:
38
+ - httphere
39
+ extensions: []
40
+
41
+ extra_rdoc_files:
42
+ - LICENSE
43
+ - README.rdoc
44
+ files:
45
+ - .document
46
+ - .gitignore
47
+ - LICENSE
48
+ - README.rdoc
49
+ - Rakefile
50
+ - VERSION
51
+ - bin/httphere
52
+ - httphere.gemspec
53
+ has_rdoc: true
54
+ homepage: http://dcparker.github.com/httphere
55
+ licenses: []
56
+
57
+ post_install_message: "\n\
58
+ \e[34mhttphere wants to detect MIME-types! For this\n\
59
+ you'll need to install the open-source shared-mime-info.\n\
60
+ Assuming you're on a Mac, you can simply run:\n\
61
+ \e[0m \e[31msudo port install shared-mime-info\e[0m\n\n * This gem has not been tested on Windows.\n\n"
62
+ rdoc_options:
63
+ - --charset=UTF-8
64
+ require_paths:
65
+ - lib
66
+ required_ruby_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: "0"
71
+ version:
72
+ required_rubygems_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: "0"
77
+ version:
78
+ requirements: []
79
+
80
+ rubyforge_project:
81
+ rubygems_version: 1.3.5
82
+ signing_key:
83
+ specification_version: 3
84
+ summary: A simple Ruby HTTP file server
85
+ test_files: []
86
+