
Managing Nginx Push Module Channels Using Ruby
Welcome all! Many developers are aware of the excellent Nginx Push Module for the Nginx web server . Many tried it, felt it.
The task of the module is to allow the Nginx web server to act as a Comet server .
There is enough material to use this module: the official project page , the description of the Basic HTTP Push Relay Protocol , as well as many articles, for example Nginx & Comet: Low Latency Server Push, are good. However, many manuals only consider the basic configuration of the module using a single public channel by all clients. Despite its great usefulness, the module does not provide developers with flexible channel management and their protection.
In this article, I will write a small example demonstrating a possible way to control channels.
What do we need?
As a result, a unique channel will be allocated for each user (after passing authorization, for example).
The Nginx Push Module provides us with some directives in the nginx security configuration config. Consider only those that I applied:
So, let's name our module - Channel. We will develop it in Ruby (there will also be small inserts in Rails).
To manage the channels (see Basic HTTP Push Relay Protocol ), we need an HTTP client. I like Patron .
An array of open channels will be stored in the array opened_channels. The channel id will be generated using the generate_channel_id method.
Creating a channel (the open method) is done by sending a PUT request to the publish point (for us it’s just / publish). Upon successful creation of a new channel (status 200), the generated id is added to the opened_channels array and returned.
Closing a channel (close method) is done by sending a DELETE request to publish.
Checking for the existence of a channel (exist method?) Is done by sending a GET request to publish. If the server returned 200, the channel is open, otherwise, delete the channel from the array.
Data is sent to the channel (push method) by sending a POST request to publish with data and content-type. We send data only to open channels.
All HTTP requests must contain the channel parameter (we have this channel). Naturally, publish-point should be protected.
The task of the module is to allow the Nginx web server to act as a Comet server .
There is enough material to use this module: the official project page , the description of the Basic HTTP Push Relay Protocol , as well as many articles, for example Nginx & Comet: Low Latency Server Push, are good. However, many manuals only consider the basic configuration of the module using a single public channel by all clients. Despite its great usefulness, the module does not provide developers with flexible channel management and their protection.
In this article, I will write a small example demonstrating a possible way to control channels.
Task
What do we need?
- creating a new channel
- closing an existing channel
- channel existence check
- sending data to the channel
- sending data to all channels
As a result, a unique channel will be allocated for each user (after passing authorization, for example).
Nginx Push Module - Secure
The Nginx Push Module provides us with some directives in the nginx security configuration config. Consider only those that I applied:
- push_authorized_channels_only [on | off]
on - allow the client to listen to a specific channel only after it is explicitly created (sending a POST or PUT request to the publisher point). Otherwise, when trying to listen to the closed channel, the response 403 is returned to the client.
Off - the client can start listening to the closed channel. - push_max_channel_subscribers [number] The
maximum number of concurrent channel listeners.
Implementation
So, let's name our module - Channel. We will develop it in Ruby (there will also be small inserts in Rails).
To manage the channels (see Basic HTTP Push Relay Protocol ), we need an HTTP client. I like Patron .
An array of open channels will be stored in the array opened_channels. The channel id will be generated using the generate_channel_id method.
Creating a channel (the open method) is done by sending a PUT request to the publish point (for us it’s just / publish). Upon successful creation of a new channel (status 200), the generated id is added to the opened_channels array and returned.
Closing a channel (close method) is done by sending a DELETE request to publish.
Checking for the existence of a channel (exist method?) Is done by sending a GET request to publish. If the server returned 200, the channel is open, otherwise, delete the channel from the array.
Data is sent to the channel (push method) by sending a POST request to publish with data and content-type. We send data only to open channels.
All HTTP requests must contain the channel parameter (we have this channel). Naturally, publish-point should be protected.
Module Code:
module Channel
@http_client = Patron::Session.new
@http_client.base_url = "http://localhost/publish"
@@opened_channels = []
mattr_accessor :opened_channels
class << self
def open
id = generate_channel_id
resp = @http_client.put(build_request_for_channel(id), "")
if resp.status == 200
opened_channels << id
id
else
false
end
end
def close(id)
resp = @http_client.delete(build_request_for_channel(id))
resp.status
end
def exist?(id)
resp = @http_client.get(build_request_for_channel id)
if resp.status == 200
true
else
opened_channels.delete id
false
end
end
def push(id, data, content_type)
if exist? id
puts "pushing to channel with id=#{id}..."
resp = @http_client.post(build_request_for_channel(id), data, {"Content-Type" => content_type})
resp.status
end
end
def push_to_all_channels(data, content_type="application/json")
opened_channels.each { |c| push(c, data, content_type) }
end
private
def generate_channel_id
UUIDTools::UUID.timestamp_create.to_s
end
def build_request_for_channel(id)
"/?channel=#{id}"
end
end
end
Channel opening upon request:
def subscribe
if channel_id = Channel::open
render text: channel_id
else
render nothing: true, status: 500
end
end
Example of sending data:
user = current_user
channel_id = user.channel_id
msg = user.messages.last
data = msg.to_json(only: [:created_at, :text])
status = Channel::push(channel_id, msg, "application/json")