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.


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")


Also popular now: