Class: Iodine::Http

Inherits:
Protocol show all
Defined in:
lib/iodine/http.rb,
lib/iodine/http/hpack.rb,
lib/iodine/http/http1.rb,
lib/iodine/http/http2.rb,
lib/iodine/http/request.rb,
lib/iodine/http/session.rb,
lib/iodine/http/response.rb,
lib/iodine/http/websockets.rb,
lib/iodine/http/rack_support.rb,
lib/iodine/http/websocket_client.rb,
lib/iodine/http/websocket_handler.rb

Overview

The Http class allows the creation of Http and Websocket servers using Iodine.

To start an Http server, simply require ‘iodine/http` (which isn’t required by default) and set up your Http callback. i.e.:

require 'iodine/http'
Iodine::Http.on_http { |request, response| 'Hello World!' }
exit # only if running from irb

To start a Websocket server, require ‘iodine/http` (which isn’t required by default), create a Websocket handling Class and set up your Websocket callback. i.e.:

require 'iodine/http'
class WSChatServer
   def initialize nickname, response
       @nickname = nickname || "unknown"
       @response = response
       # @response.io # => Http Protocol
   end
   def on_open
       # only now is the response.io pointing at the Websocket Protocol
       @io = @response.io
       @io.broadcast "#{@nickname} has joined the chat!"
       @io << "Welcome #{@nickname}, you have joined the chat!"
   end
   def on_message data
       @io.broadcast "#{@nickname} >> #{data}"
       @io << ">> #{data}"
   end
   def on_broadcast data
       # the http response can also be used to send websocket data.
       @response << data
   end
   def on_close
       @io.broadcast "#{@nickname} has left the chat!"
   end
end

Iodine::Http.on_websocket { |request, response| WSChatServer.new request.params[:name], response}

See WebsocketHandler for a good starting point or inherit WebsocketHandler in your handler.

Defined Under Namespace

Modules: Rack, SessionManager Classes: Http2, Request, Response, WebsocketClient, WebsocketHandler, Websockets

Instance Attribute Summary

Attributes inherited from Protocol

#io

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Protocol

#call, #close, #closed?, each, #id, #initialize, #on_close, #on_shutdown, #ping, #read, #set_timeout, #ssl?, #timeout?, #write

Constructor Details

This class inherits a constructor from Iodine::Protocol

Class Method Details

.http2Object

Returns true if Iodine will require that new connection be encrypted.



106
107
108
# File 'lib/iodine/http.rb', line 106

def self.http2
	@http2
end

.http2=(allow) ⇒ Object

Sets whether Iodine will allow connections to the experiemntal Http2 protocol. Defaults to false unless the ‘http2` command line flag is present.



102
103
104
# File 'lib/iodine/http.rb', line 102

def self.http2= allow
	@http2 = allow && true
end

.on_http(handler = nil, &block) ⇒ Object

Sets or gets the Http callback.

An Http callback is a Proc like object that answers to ‘call(request, response)` and returns either:

‘true`

the response has been set by the callback and can be managed (including any streaming) by the server.

‘false`

the request shouldn’t be answered or resource not found (error 404 will be sent as a response).

String

the String will be appended to the response and the response sent.



78
79
80
81
# File 'lib/iodine/http.rb', line 78

def self.on_http handler = nil, &block
	@http_app = handler || block if handler || block
	@http_app
end

.on_websocket(handler = nil, &block) ⇒ Object

Sets or gets the Websockets callback.

A Websockets callback is a Proc like object that answers to ‘call(request)` and returns either:

‘false`

the request shouldn’t be answered or resource not found (error 404 will be sent as a response).

Websocket Handler

a Websocket handler is an object that is expected to answer ‘on_message(data)` and `on_close`. See {} for more data.



87
88
89
90
# File 'lib/iodine/http.rb', line 87

def self.on_websocket handler = nil, &block
	@websocket_app = handler || block if handler || block
	@websocket_app
end

.session_tokenObject

Sets the session token for the Http server (String). Defaults to the name of the script.



97
98
99
# File 'lib/iodine/http.rb', line 97

def self.session_token
	@session_token
end

.session_token=(token) ⇒ Object

Sets the session token for the Http server (String). Defaults to the name of the script + ‘_id’.



93
94
95
# File 'lib/iodine/http.rb', line 93

def self.session_token= token
	@session_token = token
end

.ws_connect(url, options = {}, &block) ⇒ Object

Creates a websocket client within a new task (non-blocking).

Make sure to setup all the callbacks (as needed) prior to starting the connection. See Iodine::Http::WebsocketClient.connect

i.e.:

require 'iodine/http'
# don't start the server
Iodine.protocol = :timer
options = {}
options[:on_open] = Proc.new { write "Hello there!"}
options[:on_message] = Proc.new do |data|
    puts ">> #{data}";
    write "Bye!";
    # It's possible to update the callback midstream.
    on_message {|data| puts "-- Goodbye message: #{data}"; close;}
end
# After closing we will call `Iodine.signal_exit` to signal Iodine to finish up.
options[:on_close] = Proc.new { puts "disconnected"; Iodine.signal_exit }

Iodine::Http.ws_connect "ws://echo.websocket.org", options

#if running from irb:
exit


135
136
137
# File 'lib/iodine/http.rb', line 135

def self.ws_connect url, options={}, &block
	::Iodine.run { ::Iodine::Http::WebsocketClient.connect url, options, &block }
end

Instance Method Details

#on_message(data) ⇒ Object



9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/iodine/http/http1.rb', line 9

def on_message data
		return if @refuse_requests
		@http2_pri_review ||= ( ::Iodine::Http.http2 && ::Iodine::Http::Http2.pre_handshake(self, data) && (return true) ) || true

		data = ::StringIO.new data
		until data.eof?
			request = (@request ||= ::Iodine::Http::Request.new(self))
			unless request[:method]
				l = data.gets.strip
				next if l.empty?
				request[:method], request[:query], request[:version] = l.split(/[\s]+/, 3)
				return (Iodine.warn('Protocol Error, closing connection.') && close) unless request[:method] =~ HTTP_METHODS_REGEXP
				request[:version] = (request[:version] || '1.1'.freeze).match(/[\d\.]+/)[0]
				request[:time_recieved] = Time.now
			end
			until request[:headers_complete] || (l = data.gets).nil?
				if l.include? ':'
					# n = l.slice!(0, l.index(':')); l.slice! 0
					# n.strip! ; n.downcase!; n.freeze
					# request[n] ? (request[n].is_a?(Array) ? (request[n] << l) : request[n] = [request[n], l ]) : (request[n] = l)
					l = l.strip.split(/:[\s]?/, 2)
					l[0].strip! ; l[0].downcase!;
					request[l[0]] ? (request[l[0]].is_a?(Array) ? (request[l[0]] << l[1]) : request[l[0]] = [request[l[0]], l[1] ]) : (request[l[0]] = l[1])
				elsif l =~ /^[\r]?\n/
					request[:headers_complete] = true
				else
					#protocol error
					Iodine.warn 'Protocol Error, closing connection.'
					return close
				end
			end
			until request[:body_complete] && request[:headers_complete]
				if request['transfer-coding'.freeze] == 'chunked'.freeze
					# ad mid chunk logic here
					if @parser[:length].to_i == 0
						chunk = data.gets
						return false unless chunk
						@parser[:length] = chunk.to_i(16)
						return (Iodine.warn('Protocol Error, closing connection.') && close) unless @parser[:length]
						request[:body_complete] = true && break if @parser[:length] == 0
						@parser[:act_length] = 0
						request[:body] ||= ''
					end
					chunk = data.read(@parser[:length] - @parser[:act_length])
					return false unless chunk
					request[:body] << chunk
					@parser[:act_length] += chunk.bytesize
					(@parser[:act_length] = @parser[:length] = 0) && (data.gets) if @parser[:act_length] >= @parser[:length]
				elsif request['content-length'.freeze] && request['content-length'.freeze].to_i != 0
					request[:body] ||= ''
					packet = data.read(request['content-length'.freeze].to_i - request[:body].bytesize)
					return false unless packet
					request[:body] << packet
					request[:body_complete] = true if request['content-length'.freeze].to_i - request[:body].bytesize <= 0
				elsif request['content-type'.freeze]
					Iodine.warn 'Body type protocol error.' unless request[:body]
					line = data.gets
					return false unless line
					(request[:body] ||= '') << line
					request[:body_complete] = true if line =~ EOHEADERS
				else
					request[:body_complete] = true
				end
			end
			(@request = ::Iodine::Http::Request.new(self)) && ( (::Iodine::Http.http2 && ::Iodine::Http::Http2.handshake(request, self, data)) || dispatch(request, data) ) if request.delete :body_complete
		end
end

#on_openObject



3
4
5
6
7
8
# File 'lib/iodine/http/http1.rb', line 3

def on_open
	set_timeout 1
	@refuse_requests = false
	@bytes_sent = 0
	@parser = {}
end

#send_response(response) ⇒ Object



77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/iodine/http/http1.rb', line 77

def send_response response
	return false if response.headers.frozen?

	request = response.request
	headers = response.headers
	body = response.extract_body

	headers['content-length'.freeze] ||= body.to_s.bytesize

	keep_alive = response.keep_alive
	if (request[:version].to_f > 1 && request['connection'.freeze].nil?) || request['connection'.freeze].to_s =~ /ke/i || (headers['connection'.freeze] && headers['connection'.freeze] =~ /^ke/i)
		keep_alive = true
		headers['connection'.freeze] ||= 'Keep-Alive'.freeze
		headers['keep-alive'.freeze] ||= "timeout=#{(@timeout ||= 3).to_s}"
	else
		headers['connection'.freeze] ||= 'close'.freeze
	end

	send_headers response
	return log_finished(response) if request.head?
	(response.bytes_written += (write(body) || 0)) && (body.frozen? || body.clear) if body
	close unless keep_alive
	log_finished response
end

#stream_response(response, finish = false) ⇒ Object



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/iodine/http/http1.rb', line 101

def stream_response response, finish = false
	unless response.headers.frozen?
		response['transfer-encoding'.freeze] = 'chunked'
		response.headers['connection'.freeze] = 'close'.freeze
		send_headers response
		@refuse_requests = true
	end
	return if response.request.head?
	body = response.extract_body
	response.bytes_written += stream_data(body) if body || finish
	if finish
		response.bytes_written += stream_data('') unless body.nil?
		log_finished response
	end
	(body.frozen? || body.clear) if body
	true
end