httphere 1.0.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.
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
+