├── .gitignore ├── AUTHORS ├── CHANGELOG.textile ├── COPYING ├── LICENSE ├── README.textile ├── config ├── docs ├── directives │ ├── channels_statistics.textile │ ├── main.textile │ ├── publishers.textile │ └── subscribers.textile ├── examples │ ├── curl.textile │ ├── event_source.textile │ ├── forever_iframe.textile │ ├── long_polling.textile │ ├── m_jpeg.textile │ └── websocket.textile ├── javascript_client.textile └── server_tests.textile ├── include ├── ngx_http_push_stream_module.h ├── ngx_http_push_stream_module_ipc.h ├── ngx_http_push_stream_module_publisher.h ├── ngx_http_push_stream_module_setup.h ├── ngx_http_push_stream_module_subscriber.h ├── ngx_http_push_stream_module_utils.h ├── ngx_http_push_stream_module_version.h ├── ngx_http_push_stream_module_websocket.h └── ngx_http_push_stream_rbtree_util.h ├── misc ├── Gemfile ├── Gemfile.lock ├── Rakefile ├── examples │ ├── chat.html │ └── chat_longpolling.html ├── github_template.html.erb ├── js │ ├── jquery.min.js │ └── pushstream.js ├── mime.types ├── nginx.conf ├── package.json ├── spec │ ├── javascripts │ │ ├── PushStreamSpec.js │ │ ├── UtilsSpec.js │ │ ├── helpers │ │ │ └── SpecHelper.js │ │ └── support │ │ │ └── jasmine-browser.json │ ├── mix │ │ ├── channel_statistics_spec.rb │ │ ├── cleanup_memory_spec.rb │ │ ├── events_channel_spec.rb │ │ ├── keepalive_spec.rb │ │ ├── measure_memory_spec.rb │ │ ├── send_signals_spec.rb │ │ ├── setup_parameters_spec.rb │ │ └── wildcard_properties_spec.rb │ ├── nginx_configuration.rb │ ├── publisher │ │ ├── channel_id_collision_spec.rb │ │ ├── properties_spec.rb │ │ └── publish_messages_spec.rb │ ├── spec_helper.rb │ └── subscriber │ │ ├── comunication_properties_spec.rb │ │ ├── connection_cleanup_spec.rb │ │ ├── event_source_spec.rb │ │ ├── long_polling_spec.rb │ │ ├── padding_by_user_agent_spec.rb │ │ ├── polling_spec.rb │ │ ├── properties_spec.rb │ │ ├── receive_old_message_spec.rb │ │ └── websocket_spec.rb ├── tools │ ├── Makefile │ ├── README │ ├── publisher.c │ ├── subscriber.c │ ├── util.c │ └── util.h └── yarn.lock └── src ├── ngx_http_push_stream_module.c ├── ngx_http_push_stream_module_ipc.c ├── ngx_http_push_stream_module_publisher.c ├── ngx_http_push_stream_module_setup.c ├── ngx_http_push_stream_module_subscriber.c ├── ngx_http_push_stream_module_utils.c ├── ngx_http_push_stream_module_websocket.c └── ngx_http_push_stream_rbtree_util.c /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | .cproject 3 | .project 4 | .settings/ 5 | test/.bundle/ 6 | test/tmp/ 7 | misc/*.textile* 8 | misc/CHANGELOG.html 9 | misc/README.html 10 | misc/docs 11 | *.o 12 | misc/tools/publisher 13 | misc/tools/subscriber 14 | misc/node_modules 15 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Wandenberg Peixoto 2 | Rogério Carvalho Schneider 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2010-2022 Wandenberg Peixoto , Rogério Carvalho Schneider 2 | 3 | This program is free software: you can redistribute it and/or modify 4 | it under the terms of the GNU General Public License as published by 5 | the Free Software Foundation, either version 3 of the License, or 6 | (at your option) any later version. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU General Public License for more details. 12 | 13 | You should have received a copy of the GNU General Public License 14 | along with this program. If not, see . 15 | 16 | All files in this program are under GPL unless otherwise noted in 17 | file's header. Some files may be sublicensed. 18 | -------------------------------------------------------------------------------- /config: -------------------------------------------------------------------------------- 1 | ngx_addon_name=ngx_http_push_stream_module 2 | CORE_INCS="$CORE_INCS ${ngx_addon_dir}/src ${ngx_addon_dir}/include" 3 | 4 | if test -n "$ngx_module_link"; then 5 | ngx_module_type=HTTP 6 | ngx_module_name=${ngx_addon_name} 7 | ngx_module_srcs="${ngx_addon_dir}/src/${ngx_addon_name}.c" 8 | 9 | . auto/module 10 | else 11 | HTTP_MODULES="$HTTP_MODULES ${ngx_addon_name}" 12 | NGX_ADDON_SRCS="$NGX_ADDON_SRCS ${ngx_addon_dir}/src/${ngx_addon_name}.c" 13 | fi 14 | 15 | have=NGX_HTTP_HEADERS . auto/have 16 | . auto/feature 17 | 18 | #if not have sha1 or do not want to use WebSocket comment the lines bellow 19 | USE_SHA1=YES 20 | have=NGX_HAVE_SHA1 . auto/have 21 | -------------------------------------------------------------------------------- /docs/directives/channels_statistics.textile: -------------------------------------------------------------------------------- 1 | h1(#channels_statistic). Channel Statistics Configuration 2 | 3 | h2(#push_stream_channels_statistics). push_stream_channels_statistics   4 | 5 | *syntax:* _push_stream_channels_statistics_ 6 | 7 | *context:* _location_ 8 | 9 | *release version:* _0.2.0_ 10 | 11 | Defines a location as a source of statistics. You can use this location to get statistics about a specific, group or all channels, in a resumed or summarized way. 12 | To get statistics about: 13 | - all channels in a summarized way, you have to make a GET in this location without specify a name in the push_stream_channels_path directive. 14 | - all channels in a detailed way, you have to specify "ALL" in the push_stream_channels_path. 15 | - prefixed channels in a detailed way, you have to specify "_prefix_ *" in the push_stream_channels_path. 16 | - a channel, you have to specify the name in the push_stream_channels_path. 17 | - some channels, you have to specify their names in the push_stream_channels_path. 18 | 19 | You can get statistics in the formats plain, xml, yaml and json. The default is json, to change this behavior you can use *Accept* header parameter passing values like "text/plain", "application/xml", "application/yaml" and "application/json" respectively. 20 | 21 |
22 |   location /channels-stats {
23 |       push_stream_channels_statistics;
24 |       push_stream_channels_path               $arg_id;
25 |   }
26 | 
27 |   # /channels-stats -> get statistics about all channels in a summarized way
28 |   # /channels-stats?id=ALL -> get statistics about all channels in a detailed way
29 |   # /channels-stats?id=channel_* -> get statistics about all channels which starts with 'channel_'
30 |   # /channels-stats?id=channel_id -> get statistics about a channel
31 |   # /channels-stats?id=channel_id_1/channel_id_5 -> get statistics about some channels
32 | 
33 | -------------------------------------------------------------------------------- /docs/directives/main.textile: -------------------------------------------------------------------------------- 1 | h1(#main_configuration). Main Configuration 2 | 3 | h2(#push_stream_shared_memory_size). push_stream_shared_memory_size   4 | 5 | *syntax:* _push_stream_shared_memory_size size [name]_ 6 | 7 | *default:* _none_ 8 | 9 | *context:* _http_ 10 | 11 | The size of the memory chunk this module will use to store published messages, channels and other shared structures. 12 | When this memory is full any new request for publish a message or subscribe a channel will receive an 500 Internal Server Error response. 13 | If you have more than one http block on same Nginx instance and do not want they share the same memory, you can set different names to each one with the optional argument _name_. 14 | 15 | 16 | h2(#push_stream_channel_deleted_message_text). push_stream_channel_deleted_message_text   17 | 18 | *syntax:* _push_stream_channel_deleted_message_text string_ 19 | 20 | *default:* _Channel deleted_ 21 | 22 | *context:* _http_ 23 | 24 | *release version:* _0.2.5_ 25 | 26 | The string used on channel deleted message sent to subscribers when the channel is deleted by a publisher. 27 | 28 | 29 | h2(#push_stream_ping_message_text). push_stream_ping_message_text   30 | 31 | *syntax:* _push_stream_ping_message_text string_ 32 | 33 | *default:* _none_ 34 | 35 | *context:* _http_ 36 | 37 | *release version:* _0.2.5_ 38 | 39 | The string used on ping message sent to subscribers. 40 | 41 | 42 | h2(#push_stream_channel_inactivity_time). push_stream_channel_inactivity_time   43 | 44 | *syntax:* _push_stream_channel_inactivity_time time_ 45 | 46 | *default:* _30s_ 47 | 48 | *context:* _http_ 49 | 50 | *release version:* _0.3.5_ 51 | 52 | The length of time after what a channel will be considered inactive, counted after the last message was published on it or the last subscriber entered on it. 53 | After this time the channel will no longer be available and will be moved to the trash queue. 54 | When the "push_stream_authorized_channels_only":push_stream_authorized_channels_only is set to on, the inactivity time is only used to know when the channel should be moved to trash. 55 | 56 | 57 | h2(#push_stream_message_ttl). push_stream_message_ttl   58 | 59 | *syntax:* _push_stream_message_ttl time_ 60 | 61 | *default:* _30m_ 62 | 63 | *context:* _http_ 64 | 65 | The length of time a message may be queued before it is considered expired. 66 | 67 | 68 | h2(#push_stream_max_subscribers_per_channel). push_stream_max_subscribers_per_channel   69 | 70 | *syntax:* _push_stream_max_subscribers_per_channel number_ 71 | 72 | *default:* _none_ 73 | 74 | *context:* _http_ 75 | 76 | *release version:* _0.3.1_ 77 | 78 | The maximum number of subscribers accepted per channel. If you do not want to limit number of subscribers access to channels, just not set this directive. 79 | 80 | 81 | h2(#push_stream_max_messages_stored_per_channel). push_stream_max_messages_stored_per_channel   82 | 83 | *syntax:* _push_stream_max_messages_stored_per_channel number_ 84 | 85 | *default:* _none_ 86 | 87 | *context:* _http_ 88 | 89 | The maximum number of messages to store per channel. A channel's message buffer will retain at most this many most recent messages. If you do not want messages to be discarded by length, just not set this directive. 90 | 91 | 92 | h2(#push_stream_max_channel_id_length). push_stream_max_channel_id_length   93 | 94 | *syntax:* _push_stream_max_channel_id_length number_ 95 | 96 | *default:* _none_ 97 | 98 | *context:* _http_ 99 | 100 | Maximum permissible channel id length (number of characters). Longer ids will receive an 400 Bad Request response. If you do not want to limit channel id length, just not set this directive. 101 | 102 | 103 | h2(#push_stream_max_number_of_channels). push_stream_max_number_of_channels   104 | 105 | *syntax:* _push_stream_max_number_of_channels number_ 106 | 107 | *default:* _none_ 108 | 109 | *context:* _http_ 110 | 111 | The maximum number of concurrent channels on the server. If you do not want to limit the number of channels, just not set this directive. 112 | 113 | 114 | h2(#push_stream_max_number_of_wildcard_channels). push_stream_max_number_of_wildcard_channels   115 | 116 | *syntax:* _push_stream_max_number_of_wildcard_channels number_ 117 | 118 | *default:* _none_ 119 | 120 | *context:* _http_ 121 | 122 | The maximum number of concurrent wildcard channels on the server. If you do not want to limit the number of wildcard channels, just not set this directive. 123 | 124 | 125 | h2(#push_stream_wildcard_channel_prefix). push_stream_wildcard_channel_prefix   126 | 127 | *syntax:* _push_stream_wildcard_channel_prefix string_ 128 | 129 | *default:* _none_ 130 | 131 | *context:* _http_ 132 | 133 | The string prefix used to identify a wildcard channel, example: when you set this directive as "bd_", "bd_ch1" will be a wildcard channel. 134 | A wildcard channel is technically equals to a normal one. It is intended to be used when the "push_stream_authorized_channels_only":push_stream_authorized_channels_only is set to on. 135 | 136 | 137 | h2(#push_stream_events_channel_id). push_stream_events_channel_id   138 | 139 | *syntax:* _push_stream_events_channel_id string_ 140 | 141 | *default:* _none_ 142 | 143 | *context:* _http_ 144 | 145 | *release version:* _0.6.0_ 146 | 147 | The string identify an events channel where some control messages will be published. 148 | Examples: 149 | {"type": "channel_created", "channel": "CHANNEL_ID"} 150 | {"type": "channel_destroyed", "channel": "CHANNEL_ID"} 151 | {"type": "client_subscribed", "channel": "CHANNEL_ID"} 152 | {"type": "client_unsubscribed", "channel": "CHANNEL_ID"} 153 | 154 | By default this channel is not available to subscription. To allow subscriptions to it is necessary set "push_stream_allow_connections_to_events_channel":push_stream_allow_connections_to_events_channel to on. 155 | 156 | 157 | [push_stream_authorized_channels_only]subscribers.textile#push_stream_authorized_channels_only 158 | [push_stream_allow_connections_to_events_channel]subscribers.textile#push_stream_allow_connections_to_events_channel 159 | -------------------------------------------------------------------------------- /docs/directives/publishers.textile: -------------------------------------------------------------------------------- 1 | h1(#publishers_configuration). Publishers Configuration 2 | 3 | h2(#push_stream_publisher). push_stream_publisher   4 | 5 | *syntax:* _push_stream_publisher [normal | admin]_ 6 | 7 | *default:* _normal_ 8 | 9 | *context:* _location_ 10 | 11 | Defines a location as a message publisher. Requests to a publisher location are treated as messages to be sent to subscribers. 12 | This location supports the following http methods: 13 | GET, make possible to get statistics about the channel 14 | POST/PUT, publish a message to the channel 15 | DELETE, remove any existent stored messages, disconnect any subscriber, and delete the channel. Available only if _admin_ value is used in this directive. 16 | 17 |
18 |   # normal publisher location
19 |   location /pub {
20 |       push_stream_publisher;
21 |       push_stream_channels_path               $arg_id;
22 |   }
23 | 
24 |   # GET    /pub?id=channel_id -> get statistics about a channel
25 |   # POST   /pub?id=channel_id -> publish a message to the channel
26 | 
27 |   # admin publisher location
28 |   location /pub_admin {
29 |       push_stream_publisher                   admin;
30 |       push_stream_channels_path               $arg_id;
31 |   }
32 | 
33 |   # GET    /pub_admin?id=channel_id -> get statistics about a channel
34 |   # POST   /pub_admin?id=channel_id -> publish a message to the channel
35 |   # DELETE /pub_admin?id=channel_id -> delete the channel
36 | 
37 | 38 | 39 | h2(#push_stream_channels_path). push_stream_channels_path   40 | 41 | *values:* _channel id_ 42 | 43 | *location:* _push_stream_publisher, push_stream_channels_statistics_ 44 | 45 | A string to uniquely identify a communication channel. Must be present on location of the push_stream_publisher and push_stream_channels_statistics. 46 | 47 |
48 | push_stream_channels_path $arg_id;
49 | #channel id is now the url query string parameter "id"
50 | #(/pub?id=channel_id_string or /channels-stats?id=channel_id_string)
51 | 
52 | 53 | 54 | h2(#push_stream_store_messages). push_stream_store_messages   55 | 56 | *syntax:* _push_stream_store_messages on | off_ 57 | 58 | *default:* _off_ 59 | 60 | *context:* _location (push_stream_publisher)_ 61 | 62 | Whether or not message queuing is enabled. 63 | 64 | 65 | h2(#push_stream_channel_info_on_publish). push_stream_channel_info_on_publish   66 | 67 | *syntax:* _push_stream_channel_info_on_publish on | off_ 68 | 69 | *default:* _on_ 70 | 71 | *context:* _location (push_stream_publisher)_ 72 | 73 | *release version:* _0.3.5_ 74 | 75 | Enable send back channel information after publish a message. 76 | -------------------------------------------------------------------------------- /docs/examples/curl.textile: -------------------------------------------------------------------------------- 1 | h1(#curl). Curl examples   2 | 3 | Some commands to explain how the module protocol is implemented, and help to do your own client ;) 4 | 5 | Configure your server like suggested bellow. You should complete this configuration with other directives according to the target application. 6 | 7 | *Server:* 8 | 9 |
 10 |     location /channels-stats {
 11 |         # activate channels statistics mode for this location
 12 |         push_stream_channels_statistics;
 13 | 
 14 |         # query string based channel id
 15 |         push_stream_channels_path               $arg_id;
 16 |     }
 17 | 
 18 |     location /pub {
 19 |         # activate publisher (admin) mode for this location
 20 |         push_stream_publisher admin;
 21 | 
 22 |         # query string based channel id
 23 |         push_stream_channels_path               $arg_id;
 24 |     }
 25 | 
 26 |     location ~ /sub/(.*) {
 27 |         # activate long-polling mode for this location
 28 |         push_stream_subscriber      long-polling;
 29 | 
 30 |         # positional channel path
 31 |         push_stream_channels_path         $1;
 32 | 
 33 |         # message template
 34 |         push_stream_message_template                "{\"id\":~id~,\"channel\":\"~channel~\",\"text\":\"~text~\"}";
 35 | 
 36 |         # connection timeout
 37 |         push_stream_longpolling_connection_ttl        30s;
 38 |     }
 39 | 
40 | 41 | 42 | h2(#common_operations). Common operations 43 | 44 |
 45 | # Subscribe to a channel
 46 | curl -s -v --no-buffer 'http://localhost/sub/my_channel_1'
 47 | curl -s -v --no-buffer 'http://localhost/sub/your_channel_1'
 48 | curl -s -v --no-buffer 'http://localhost/sub/your_channel_2'
 49 | 
 50 | # Publish messages
 51 | curl -s -v -X POST 'http://localhost/pub?id=my_channel_1' -d 'Hello World!'
 52 | curl -s -v -X POST 'http://localhost/pub?id=your_channel_1' -d 'Hi everybody!'
 53 | curl -s -v -X POST 'http://localhost/pub?id=your_channel_2' -d 'Goodbye!'
 54 | 
 55 | # Channels Stats for publisher (json format)
 56 | curl -s -v 'http://localhost/pub?id=my_channel_1'
 57 | 
 58 | # All Channels Stats summarized (json format)
 59 | curl -s -v 'http://localhost/channels-stats'
 60 | 
 61 | # All Channels Stats detailed (json format)
 62 | curl -s -v 'http://localhost/channels-stats?id=ALL'
 63 | 
 64 | # Prefixed Channels Stats detailed (json format)
 65 | curl -s -v 'http://localhost/channels-stats?id=your_channel_*'
 66 | 
 67 | # Channels Stats (json format)
 68 | curl -s -v 'http://localhost/channels-stats?id=my_channel_1'
 69 | 
 70 | # Delete Channels
 71 | curl -s -v -X DELETE 'http://localhost/pub?id=my_channel_1'
 72 | 
73 | 74 | 75 | h2(#getting_old_messages). Getting old messages 76 | 77 | To get old messages you can use a backtrack, an event id or a time in the past to specify a start point. 78 | All control methods use some HTTP headers by default, except for backtrack. 79 | But they can use other methods also, like URL parameters. 80 | 81 | To deliver old messages it's necessary to properly configure the directives "push_stream_store_messages", "push_stream_max_messages_stored_per_channel" and "push_stream_message_ttl". 82 | 83 | 84 | *Using backtrack:* 85 | 86 |
 87 | # To get the last 4 messages from channelA, the last 2 messages from channelC and no old messages from channelB
 88 | curl -s -v --no-buffer 'http://localhost/sub/channelA.b4/channelB/channelC.b2'
 89 | 
90 | 91 | 92 | *Using time in the past:* 93 | 94 | When a message is published it receives a time and a tag value. 95 | The tag is used to untie messages published on the same second. 96 | These values are available to the message template using the ==~time~== and ==~tag~== patterns, and also on headers "Last-Modified" and "Etag" when on long-polling mode. 97 | 98 |
 99 | # publish a message on t1
100 | curl -s -v -X POST 'http://localhost/pub?id=channelA' -d '1'
101 | # publish another message on t2
102 | curl -s -v -X POST 'http://localhost/pub?id=channelA' -d '2'
103 | # publish another message on t2
104 | curl -s -v -X POST 'http://localhost/pub?id=channelA' -d '3'
105 | # publish another message on t3
106 | curl -s -v -X POST 'http://localhost/pub?id=channelA' -d '4'
107 | 
108 | # Get the messages published after the t2 time, tag 1
109 | curl -s -v --no-buffer 'http://localhost/sub/channelA' -H 'If-Modified-Since: t2' -H 'If-None-Match: 1'
110 | 
111 | # t2, must be on the format "%a, %d %b %Y %T %Z", for instance "Thu, 1 Jan 1970 00:00:00 GMT"
112 | # this subscriber will receive the messages "3" and "4"
113 | 
114 | 115 | *Using time in the past (not using headers):* 116 | 117 | Adding the directives "push_stream_last_received_message_time", "push_stream_last_received_message_tag" to subscriber location, 118 | is possible to set the values for getting old messages using other methods, like URL parameters or a piece of the path. 119 | 120 | modified server: 121 | 122 |
123 |     location ~ /sub/(.*) {
124 |         push_stream_subscriber      long-polling;
125 |         push_stream_channels_path         $1;
126 | 
127 |         push_stream_last_received_message_time $arg_time;
128 |         push_stream_last_received_message_tag  $arg_tag;
129 | 
130 |         push_stream_message_template           "{\"id\":~id~,\"channel\":\"~channel~\",\"text\":\"~text~\", \"time\":\"~time~\", \"tag\":\"~tag~\"}";
131 | 
132 |         push_stream_longpolling_connection_ttl        30s;
133 |     }
134 | 
135 | 136 | using the same published messages on the above example: 137 | 138 |
139 | # Get the messages published after the t2 time, tag 2
140 | curl -s -v --no-buffer 'http://localhost/sub/channelA?tag=2&time=t2'
141 | 
142 | # t2, must be on the format "%a, %d %b %Y %T %Z", for instance "Thu, 1 Jan 1970 00:00:00 GMT"
143 | # this subscriber will receive only the message "4"
144 | 
145 | 146 | 147 | *Using EventId:* 148 | 149 | When a message is published with an Event-Id header this value can be used as a start point to get old messages. 150 | It's available to the message template using the ==~event-id~== pattern. 151 | 152 |
153 | 
154 | curl -s -v -X POST 'http://localhost/pub?id=channelA' -d '1'
155 | curl -s -v -X POST 'http://localhost/pub?id=channelA' -d '2' -H 'Event-Id: some_special_event'
156 | curl -s -v -X POST 'http://localhost/pub?id=channelA' -d '3'
157 | curl -s -v -X POST 'http://localhost/pub?id=channelA' -d '4'
158 | 
159 | # Get the messages published after that event, in this example messages '3' and '4'
160 | curl -s -v --no-buffer 'http://localhost/sub/channelA' -H 'Last-Event-Id: some_special_event'
161 | 
162 | 163 | *Using EventId (not using headers):* 164 | 165 | Adding the directive "push_stream_last_event_id" to subscriber location, 166 | is possible to set the value for getting old messages using other methods, like URL parameters or a piece of the path. 167 | 168 | modified server: 169 | 170 |
171 |     location ~ /sub/(.*) {
172 |         push_stream_subscriber      long-polling;
173 |         push_stream_channels_path         $1;
174 | 
175 |         push_stream_last_event_id $arg_last_id;
176 | 
177 |         push_stream_message_template           "{\"id\":~id~,\"channel\":\"~channel~\",\"text\":\"~text~\", \"event\":\"~event-id~\"}";
178 | 
179 |         push_stream_longpolling_connection_ttl        30s;
180 |     }
181 | 
182 | 183 | using the same published messages on the above example: 184 | 185 |
186 | # Get the messages published after the event 'some_special_event'
187 | curl -s -v --no-buffer 'http://localhost/sub/channelA?last_id=some_special_event'
188 | 
189 | # this subscriber will receive the messages '3' and '4'
190 | 
191 | -------------------------------------------------------------------------------- /docs/examples/event_source.textile: -------------------------------------------------------------------------------- 1 | h1(#event_source). Event Source   2 | 3 | Using EventSource to receive the messages. 4 | *This example uses the PushStream class present in _misc/js/pushstream.js_ file, copy it to your server htdocs.* 5 | 6 | Configure your server like suggested bellow. You should complete this configuration with other directives according to the target application. 7 | Create a html page with the content on **Client** part, access it from browser and try with the command *curl http://localhost/pub?id=ch1 -d =="Some Text"==* . 8 | 9 | *Server:* 10 | 11 |
 12 |     location /pub {
 13 |         # activate publisher (admin) mode for this location
 14 |         push_stream_publisher admin;
 15 | 
 16 |         # query string based channel id
 17 |         push_stream_channels_path               $arg_id;
 18 |     }
 19 | 
 20 |     location ~ /ev/(.*) {
 21 |         # activate event source mode for this location
 22 |         push_stream_subscriber eventsource;
 23 | 
 24 |         # positional channel path
 25 |         push_stream_channels_path                   $1;
 26 |         # message template
 27 |         push_stream_message_template                "{\"id\":~id~,\"channel\":\"~channel~\",\"text\":\"~text~\"}";
 28 | 
 29 |         # ping frequency
 30 |         push_stream_ping_message_interval           10s;
 31 |     }
 32 | 
33 | 34 | *Client:* 35 | 36 |
 37 | 
 39 | 
 40 | 
 41 |     
 42 |     Event Source Example
 43 | 
 44 | 
 45 |     

Messages:

46 |
47 | 48 | 49 | 65 | 66 | 67 |
68 | 69 | h2(#using_channels_by_argument). Using Channels by argument 70 | 71 | By default pushstream.js send the desired channels to the server as part of the url. 72 | If needed you can change this behavior changing the javascript usage, like the example bellow, to not set the location as a regular expression. 73 | 74 | *Server:* 75 | 76 |
 77 |     location /pub {
 78 |         # activate publisher (admin) mode for this location
 79 |         push_stream_publisher admin;
 80 | 
 81 |         # query string based channel id
 82 |         push_stream_channels_path               $arg_id;
 83 |     }
 84 | 
 85 |     location /ev {
 86 |         # activate event source mode for this location
 87 |         push_stream_subscriber eventsource;
 88 | 
 89 |         # positional channel path
 90 |         push_stream_channels_path                   $arg_channels;
 91 |         # message template
 92 |         push_stream_message_template                "{\"id\":~id~,\"channel\":\"~channel~\",\"text\":\"~text~\"}";
 93 | 
 94 |         # ping frequency
 95 |         push_stream_ping_message_interval           10s;
 96 |     }
 97 | 
98 | 99 | *Client:* 100 | 101 |
102 | 
104 | 
105 | 
106 |     
107 |     Event Source Example
108 | 
109 | 
110 |     

Messages:

111 |
112 | 113 | 114 | 132 | 133 | 134 |
135 | 136 | h2(#getting_old_messages). Getting old messages 137 | 138 | To get old messages you can set a backtrack, an event id or a time in the past. 139 | To proper work on reconnections you should set ==~tag~ and ~time~== on the message template, and configure the server to receive the values. 140 | 141 | *Server:* 142 | 143 |
144 |     location /pub {
145 |         # activate publisher (admin) mode for this location
146 |         push_stream_publisher admin;
147 | 
148 |         # query string based channel id
149 |         push_stream_channels_path               $arg_id;
150 | 
151 |         # store messages in memory
152 |         push_stream_store_messages              on;
153 |     }
154 | 
155 |     location ~ /ev/(.*) {
156 |         # activate event source mode for this location
157 |         push_stream_subscriber eventsource;
158 | 
159 |         # positional channel path
160 |         push_stream_channels_path                   $1;
161 | 
162 |         push_stream_last_received_message_time      "$arg_time";
163 |         push_stream_last_received_message_tag       "$arg_tag";
164 | 
165 |         # message template
166 |         push_stream_message_template                "{\"id\":~id~,\"channel\":\"~channel~\",\"text\":\"~text~\",\"tag\":\"~tag~\",\"time\":\"~time~\"}";
167 | 
168 |         # ping frequency
169 |         push_stream_ping_message_interval           10s;
170 | 
171 |     }
172 | 
173 | 174 | *Client:* 175 | 176 |
177 | 
179 | 
180 | 
181 |     
182 |     Event Source Example
183 | 
184 | 
185 |     

Messages:

186 |
187 | 188 | 189 | 207 | 208 | 209 |
210 | 211 | *Observations:* 212 | 213 | * _push_stream_message_template_ should be exactly like as the example to be used with PushStream class 214 | * WebSocket, EventSource and Forever iFrame may be combined setting _/ws_, _/sub_ and _/ev_ locations on same server and setting *modes: "websocket|eventsource|stream"* on client. With that if the browser supports Websocket or Event Source, it will use it, if not it will use iFrame, following the order on _modes_ attribute. 215 | -------------------------------------------------------------------------------- /docs/examples/forever_iframe.textile: -------------------------------------------------------------------------------- 1 | h1(#forever_iframe). Forever Iframe   2 | 3 | Using an invisible iFrame on the page to receive the messages and pass them to main page. 4 | *This example uses the PushStream class present in _misc/js/pushstream.js_ file, copy it to your server htdocs.* 5 | 6 | Configure your server like suggested bellow. You should complete this configuration with other directives according to the target application. 7 | Create a html page with the content on **Client** part, access it from browser and try with the command *curl http://localhost/pub?id=ch1 -d =="Some Text"==* . 8 | 9 | *Server:* 10 | 11 |
 12 |     location /pub {
 13 |         # activate publisher (admin) mode for this location
 14 |         push_stream_publisher admin;
 15 | 
 16 |         # query string based channel id
 17 |         push_stream_channels_path               $arg_id;
 18 |     }
 19 | 
 20 |     location ~ /sub/(.*) {
 21 |         # activate subscriber (streaming) mode for this location
 22 |         push_stream_subscriber;
 23 | 
 24 |         # positional channel path
 25 |         push_stream_channels_path                   $1;
 26 | 
 27 |         # header to be sent when receiving new subscriber connection
 28 |         push_stream_header_template                 "\r\n\r\n\r\n\r\n\r\n\r\n\r\n";
 29 |         # message template
 30 |         push_stream_message_template                "";
 31 |         # footer to be sent when finishing subscriber connection
 32 |         push_stream_footer_template                 "";
 33 |         # content-type
 34 |         default_type                                "text/html; charset=utf-8";
 35 |         # ping frequency
 36 |         push_stream_ping_message_interval           10s;
 37 |     }
 38 | 
39 | 40 | *Client:* 41 | 42 |
 43 | 
 45 | 
 46 | 
 47 |     
 48 |     Forever iFrame Example
 49 | 
 50 | 
 51 |     

Messages:

52 |
53 | 54 | 55 | 71 | 72 | 73 |
74 | 75 | h2(#using_channels_by_argument). Using Channels by argument 76 | 77 | By default pushstream.js send the desired channels to the server as part of the url. 78 | If needed you can change this behavior changing the javascript usage, like the example bellow, to not set the location as a regular expression. 79 | 80 | *Server:* 81 | 82 |
 83 |     location /pub {
 84 |         # activate publisher (admin) mode for this location
 85 |         push_stream_publisher admin;
 86 | 
 87 |         # query string based channel id
 88 |         push_stream_channels_path               $arg_id;
 89 |     }
 90 | 
 91 |     location /sub {
 92 |         # activate subscriber (streaming) mode for this location
 93 |         push_stream_subscriber;
 94 | 
 95 |         # positional channel path
 96 |         push_stream_channels_path                   $arg_channels;
 97 | 
 98 |         # header to be sent when receiving new subscriber connection
 99 |         push_stream_header_template                 "\r\n\r\n\r\n\r\n\r\n\r\n\r\n";
100 |         # message template
101 |         push_stream_message_template                "";
102 |         # footer to be sent when finishing subscriber connection
103 |         push_stream_footer_template                 "";
104 |         # content-type
105 |         default_type                                "text/html; charset=utf-8";
106 |         # ping frequency
107 |         push_stream_ping_message_interval           10s;
108 |     }
109 | 
110 | 111 | *Client:* 112 | 113 |
114 | 
116 | 
117 | 
118 |     
119 |     Forever iFrame Example
120 | 
121 | 
122 |     

Messages:

123 |
124 | 125 | 126 | 144 | 145 | 146 |
147 | 148 | h2(#getting_old_messages). Getting old messages 149 | 150 | To get old messages you can set a backtrack, an event id or a time in the past. 151 | To proper work on reconnections you should set ==~tag~ and ~time~== on the message template, and configure the server to receive the values. 152 | 153 | *Server:* 154 | 155 |
156 |     location /pub {
157 |         # activate publisher (admin) mode for this location
158 |         push_stream_publisher admin;
159 | 
160 |         # query string based channel id
161 |         push_stream_channels_path               $arg_id;
162 | 
163 |         # store messages in memory
164 |         push_stream_store_messages              on;
165 |     }
166 | 
167 |     location ~ /sub/(.*) {
168 |         # activate subscriber (streaming) mode for this location
169 |         push_stream_subscriber;
170 | 
171 |         # positional channel path
172 |         push_stream_channels_path                   $1;
173 | 
174 |         push_stream_last_received_message_time      "$arg_time";
175 |         push_stream_last_received_message_tag       "$arg_tag";
176 | 
177 |         # header to be sent when receiving new subscriber connection
178 |         push_stream_header_template                 "\r\n\r\n\r\n\r\n\r\n\r\n\r\n";
179 |         # message template
180 |         push_stream_message_template                "";
181 |         # footer to be sent when finishing subscriber connection
182 |         push_stream_footer_template                 "";
183 |         # content-type
184 |         default_type                                "text/html; charset=utf-8";
185 |         # ping frequency
186 |         push_stream_ping_message_interval           10s;
187 |     }
188 | 
189 | 190 | *Client:* 191 | 192 |
193 | 
195 | 
196 | 
197 |     
198 |     Event Source Example
199 | 
200 | 
201 |     

Messages:

202 |
203 | 204 | 205 | 223 | 224 | 225 |
226 | 227 | *Observations:* 228 | 229 | * _push_stream_message_template_ should be exactly like as the example to be used with PushStream class 230 | * WebSocket, EventSource and Forever iFrame may be combined setting _/ws_, _/sub_ and _/ev_ locations on same server and setting *modes: "websocket|eventsource|stream"* on client. With that if the browser supports Websocket or Event Source, it will use it, if not it will use iFrame, following the order on _modes_ attribute. 231 | -------------------------------------------------------------------------------- /docs/examples/m_jpeg.textile: -------------------------------------------------------------------------------- 1 | h1(#m_jpeg). M-JPEG example   2 | 3 | Using the module to stream images over HTTP "(wiki)":wiki. 4 | 5 | Configure your server like suggested bellow. You should complete this configuration with other directives according to the target application. 6 | 7 | *Server:* 8 | 9 |
10 |     location /pub {
11 |         client_max_body_size                    1m;
12 |         client_body_buffer_size                 1m;
13 | 
14 |         # activate publisher (admin) mode for this location
15 |         push_stream_publisher admin;
16 | 
17 |         # query string based channel id
18 |         push_stream_channels_path               $arg_id;
19 |     }
20 | 
21 |     location ~ /sub/(.*) {
22 |         default_type "multipart/x-mixed-replace; boundary=endofsection";
23 | 
24 |         push_stream_subscriber;
25 | 
26 |         # positional channel path
27 |         push_stream_channels_path         $1;
28 | 
29 |         # message template
30 |         push_stream_message_template "--endofsection\nX-Timestamp: ~time~\nContent-Type: image/jpg\nContent-Length: ~size~\n\n~text~";
31 |     }
32 | 
33 |     location / {
34 |         default_type "text/html";
35 |         return 200 "M-JPEG example";
36 |     }
37 | 
38 | 39 | 40 | Open any browser and point it to your server, like "http://localhost/" 41 | Post jpeg images. 42 | 43 |
44 | 
45 | curl -s -v -X POST 'http://localhost:9080/pub?id=ch1' --data-binary @image1.jpg
46 | curl -s -v -X POST 'http://localhost:9080/pub?id=ch1' --data-binary @image2.jpg
47 | curl -s -v -X POST 'http://localhost:9080/pub?id=ch1' --data-binary @image3.jpg
48 | ...
49 | 
50 | 
51 | 52 | [wiki]https://en.wikipedia.org/wiki/Motion_JPEG#M-JPEG_over_HTTP -------------------------------------------------------------------------------- /docs/examples/websocket.textile: -------------------------------------------------------------------------------- 1 | h1(#websocket). WebSocket   2 | 3 | Using WebSocket to receive the messages. 4 | *This example uses the PushStream class present in _misc/js/pushstream.js_ file, copy it to your server htdocs.* 5 | 6 | Configure your server like suggested bellow. You should complete this configuration with other directives according to the target application. 7 | Create a html page with the content on **Client** part, access it from browser and try with the command *curl http://localhost/pub?id=ch1 -d =="Some Text"==* . 8 | 9 | *Server:* 10 | 11 |
 12 |     location /pub {
 13 |         # activate publisher (admin) mode for this location
 14 |         push_stream_publisher admin;
 15 | 
 16 |         # query string based channel id
 17 |         push_stream_channels_path               $arg_id;
 18 |     }
 19 | 
 20 |     location ~ /ws/(.*) {
 21 |         # activate websocket mode for this location
 22 |         push_stream_subscriber websocket;
 23 | 
 24 |         # positional channel path
 25 |         push_stream_channels_path                   $1;
 26 |         # message template
 27 |         push_stream_message_template                "{\"id\":~id~,\"channel\":\"~channel~\",\"text\":\"~text~\"}";
 28 | 
 29 |         push_stream_websocket_allow_publish         on;
 30 | 
 31 |         # ping frequency
 32 |         push_stream_ping_message_interval           10s;
 33 |     }
 34 | 
35 | 36 | *Client:* 37 | 38 |
 39 | 
 41 | 
 42 | 
 43 |     
 44 |     WebSocket Example
 45 | 
 46 | 
 47 |     

Messages:

48 |
49 | 50 | 51 | 67 | 68 | 69 |
70 | 71 | h2(#using_channels_by_argument). Using Channels by argument 72 | 73 | By default pushstream.js send the desired channels to the server as part of the url. 74 | If needed you can change this behavior changing the javascript usage, like the example bellow, to not set the location as a regular expression. 75 | 76 | *Server:* 77 | 78 |
 79 |     location /pub {
 80 |         # activate publisher (admin) mode for this location
 81 |         push_stream_publisher admin;
 82 | 
 83 |         # query string based channel id
 84 |         push_stream_channels_path               $arg_id;
 85 |     }
 86 | 
 87 |     location /ws {
 88 |         # activate websocket mode for this location
 89 |         push_stream_subscriber websocket;
 90 | 
 91 |         # positional channel path
 92 |         push_stream_channels_path                   $arg_channels;
 93 |         # message template
 94 |         push_stream_message_template                "{\"id\":~id~,\"channel\":\"~channel~\",\"text\":\"~text~\"}";
 95 | 
 96 |         push_stream_websocket_allow_publish         on;
 97 | 
 98 |         # ping frequency
 99 |         push_stream_ping_message_interval           10s;
100 |     }
101 | 
102 | 103 | *Client:* 104 | 105 |
106 | 
108 | 
109 | 
110 |     
111 |     WebSocket Example
112 | 
113 | 
114 |     

Messages:

115 |
116 | 117 | 118 | 136 | 137 | 138 |
139 | 140 | h2(#getting_old_messages). Getting old messages 141 | 142 | To get old messages you can set a backtrack, an event id or a time in the past. 143 | To proper work on reconnections you should set ==~tag~ and ~time~== on the message template, and configure the server to receive the values. 144 | 145 | *Server:* 146 | 147 |
148 |     location /pub {
149 |         # activate publisher (admin) mode for this location
150 |         push_stream_publisher admin;
151 | 
152 |         # query string based channel id
153 |         push_stream_channels_path               $arg_id;
154 | 
155 |         # store messages in memory
156 |         push_stream_store_messages              on;
157 |     }
158 | 
159 |     location ~ /ws/(.*) {
160 |         # activate websocket mode for this location
161 |         push_stream_subscriber websocket;
162 | 
163 |         # positional channel path
164 |         push_stream_channels_path                   $1;
165 | 
166 |         push_stream_last_received_message_time      "$arg_time";
167 |         push_stream_last_received_message_tag       "$arg_tag";
168 | 
169 |         # message template
170 |         push_stream_message_template                "{\"id\":~id~,\"channel\":\"~channel~\",\"text\":\"~text~\",\"tag\":\"~tag~\",\"time\":\"~time~\"}";
171 | 
172 |         # ping frequency
173 |         push_stream_ping_message_interval           10s;
174 | 
175 |     }
176 | 
177 | 178 | *Client:* 179 | 180 |
181 | 
183 | 
184 | 
185 |     
186 |     Event Source Example
187 | 
188 | 
189 |     

Messages:

190 |
191 | 192 | 193 | 211 | 212 | 213 |
214 | 215 | *Observations:* 216 | 217 | * _push_stream_message_template_ should be exactly like as the example to be used with PushStream class 218 | * WebSocket, EventSource and Forever iFrame may be combined setting _/ws_, _/sub_ and _/ev_ locations on same server and setting *modes: "websocket|eventsource|stream"* on client. With that if the browser supports Websocket or Event Source, it will use it, if not it will use iFrame, following the order on _modes_ attribute. 219 | -------------------------------------------------------------------------------- /docs/javascript_client.textile: -------------------------------------------------------------------------------- 1 | h1(#javascript_client). Javascript Client   2 | 3 | The _PushStream_ javascript class is an abstraction for which kind of connection is in use. 4 | It supports 4 kinds of connection: Stream, EventSource, WebSocket and LongPolling. 5 | The main idea is to provide a single interface to be used on your code, be easy to change from one kind to another, or to use some of the kinds together, one as a fallback to the others. 6 | It does not depend of any framework, like jQuery or MooTools, and can be used with any of them. 7 | 8 | h2(#basic_usage). Basic Usage   9 | 10 |
11 | 
19 | 
20 | 21 | h2(#configuration). Configuration   22 | 23 | The _PushStream_ class accept some configurations on its constructor to replace the default values. 24 | Example: 25 | 26 |
27 | 
34 | 
35 | 36 | (head). | configuration | default | type | description | 37 | | useSSL | false | boolean | if should use or not SSL on the connection | 38 | | host | current host name | string | the host name to connect and get messages | 39 | | port | 80/443 (if using SSL) | number | the port number to connect and get messages | 40 | | timeout | 30000 | number | the amount of time to consider that a connection has some problem (in milliseconds) | 41 | | pingtimeout | 30000 | number | the amount of time to consider that a connection does not receive a ping message (in milliseconds) | 42 | | reconnectOnTimeoutInterval | 3000 | number | the amount of time to do a new connection after a timeout happens (in milliseconds) | 43 | | reconnectOnChannelUnavailableInterval | 60000 | number | the amount of time to do a new connection after receives a 403, indicating that a channel is unavailable (in milliseconds) | 44 | | autoReconnect | true | boolean | enable / disable the internal routine to reconnect the client after a failure | 45 | | messagesPublishedAfter | - | number/date | get messages published at less than this time, on the first connection | 46 | | lastEventId | - | string | get messages published after the message with this event id | 47 | | messagesControlByArgument | true | boolean | when to use time and tag values by headers instead of arguments on long polling connections | 48 | | tagArgument | 'tag' | string | argument name to send tag value, specially used on JSONP mode | 49 | | timeArgument | 'time' | string | argument name to send time value, specially used on JSONP mode | 50 | | eventIdArgument | 'eventid' | string | argument name to send eventid value, specially used on JSONP mode | 51 | | useJSONP | false | boolean | when to use JSONP mode on long polling connections, mandatorily true when current domain or port is different from the target server (cross domain restrictions) | 52 | | urlPrefixPublisher | '/pub' | string | the location prefix used to post messages | 53 | | urlPrefixStream | '/sub' | string | the location prefix used to do Stream mode connections | 54 | | urlPrefixEventsource | '/ev' | string | the location prefix used to do EventSource mode connections | 55 | | urlPrefixLongpolling | '/lp' | string | the location prefix used to do LongPolling mode connections | 56 | | urlPrefixWebsocket | '/ws' | string | the location prefix used to do WebSocket mode connections | 57 | | jsonIdKey | 'id' | string | the key name to extract message id from received message | 58 | | jsonChannelKey | 'channel' | string | the key name to extract channel id from received message | 59 | | jsonTextKey | 'text' | string | the key name to extract message text from received message | 60 | | jsonTagKey | 'tag' | string | the key name to extract message tag from received message | 61 | | jsonTimeKey | 'time' | string | the key name to extract message time from received message | 62 | | jsonEventIdKey | 'eventid' | string | the key name to extract message event id from received message | 63 | | modes | 'eventsource|websocket|stream|longpolling' | string | methods supported by the server which can be used by the browser, separated by '|', on the order of preference | 64 | | channelsByArgument | false | boolean | when to send target channels names by argument on subscriber connections | 65 | | channelsArgument | 'channels' | string | the argument name to send target channels names on subscriber connections | 66 | 67 | h2(#callbacks). Callbacks and Functions   68 | 69 | The _PushStream_ class has some callbacks and functions that can be overwritten. 70 | Example: 71 | 72 |
73 | 
80 | 
81 | 82 | (head). | callback/function | description | 83 | | extraParams | implement this function returning an object with extra parameters to send on subscriber connections, where the key is the parameter name and the value will be the parameter value, like {"foo":"bar", "xyz":"1"} -> "foo=bar&xyz=1" | 84 | | onerror | implement this function to be notified when an error happens, the argument received is an object with a key named 'type' indicating if was a 'load' or a 'timeout' error | 85 | | onstatuschange | implement this function to receive the new connection status as argument, which can be PushStream.CLOSED, PushStream.CONNECTING or PushStream.OPEN | 86 | | onchanneldeleted | implement this function to be notified when a channel was deleted on the server. The channel id will be the given argument| 87 | | onmessage | implement this function to receive the messages from server, the arguments are, in order: text, id, channel, eventid, isLastMessageFromBatch, time. The isLastMessageFromBatch argument indicate when is, or not, the last message received on a batch when using long polling connections | 88 | -------------------------------------------------------------------------------- /docs/server_tests.textile: -------------------------------------------------------------------------------- 1 | h1(#tests). Tests   2 | 3 | The tests for this module are written in Ruby, and are acceptance tests. 4 | To run them is needed to have an environment with: 5 | 6 | * ruby >= 1.9.3 7 | * bundler >= 1.1.4 8 | 9 | and install required gems doing: 10 | 11 |
12 | cd misc/
13 | bundle install --without docs
14 | 
15 | 16 | Then issue @rake spec@. 17 | This command run the tests using nginx *executable* located at _/usr/local/nginx/sbin/nginx_ with _1_ *worker* responding at *host* _127.0.0.1_ and *port* _9990_. 18 | To change this behavior use the commands bellow 19 | 20 |
21 | NGINX_EXEC="../build/nginx-1.2.0/objs/nginx" rake spec   # to change default path for nginx executable
22 | NGINX_HOST="my_machine" rake spec                        # to change default hostname
23 | NGINX_PORT=9889 rake spec                                # to change default port
24 | NGINX_WORKERS=2 rake spec                                # to change dafault number of workers used
25 | 
26 | and can combine any of these parameters, like:
27 | 
28 | NGINX_PORT=9889 NGINX_EXEC="../build/nginx-1.2.0/objs/nginx" rake spec
29 | 
30 | -------------------------------------------------------------------------------- /include/ngx_http_push_stream_module_ipc.h: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is distributed under the MIT License. 3 | * 4 | * Copyright (c) 2009 Leo Ponomarev 5 | * 6 | * Permission is hereby granted, free of charge, to any person 7 | * obtaining a copy of this software and associated documentation 8 | * files (the "Software"), to deal in the Software without 9 | * restriction, including without limitation the rights to use, 10 | * copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the 12 | * Software is furnished to do so, subject to the following 13 | * conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be 16 | * included in all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | * OTHER DEALINGS IN THE SOFTWARE. 26 | * 27 | * 28 | * ngx_http_push_stream_module_ipc.h 29 | * 30 | * Modified: Oct 26, 2010 31 | * Modifications by: Wandenberg Peixoto , Rogério Carvalho Schneider 32 | */ 33 | 34 | #ifndef NGX_HTTP_PUSH_STREAM_MODULE_IPC_H_ 35 | #define NGX_HTTP_PUSH_STREAM_MODULE_IPC_H_ 36 | 37 | #include 38 | #include 39 | 40 | #include 41 | 42 | // constants 43 | static ngx_channel_t NGX_CMD_HTTP_PUSH_STREAM_CHECK_MESSAGES = {49, 0, 0, -1}; 44 | static ngx_channel_t NGX_CMD_HTTP_PUSH_STREAM_CENSUS_SUBSCRIBERS = {50, 0, 0, -1}; 45 | static ngx_channel_t NGX_CMD_HTTP_PUSH_STREAM_DELETE_CHANNEL = {51, 0, 0, -1}; 46 | static ngx_channel_t NGX_CMD_HTTP_PUSH_STREAM_CLEANUP_SHUTTING_DOWN = {52, 0, 0, -1}; 47 | 48 | // worker processes of the world, unite. 49 | ngx_socket_t ngx_http_push_stream_socketpairs[NGX_MAX_PROCESSES][2]; 50 | 51 | static ngx_int_t ngx_http_push_stream_register_worker_message_handler(ngx_cycle_t *cycle); 52 | 53 | static void ngx_http_push_stream_broadcast(ngx_http_push_stream_channel_t *channel, ngx_http_push_stream_msg_t *msg, ngx_log_t *log, ngx_http_push_stream_main_conf_t *mcf); 54 | 55 | static ngx_int_t ngx_http_push_stream_alert_worker(ngx_pid_t pid, ngx_int_t slot, ngx_log_t *log, ngx_channel_t command); 56 | #define ngx_http_push_stream_alert_worker_check_messages(pid, slot, log) ngx_http_push_stream_alert_worker(pid, slot, log, NGX_CMD_HTTP_PUSH_STREAM_CHECK_MESSAGES) 57 | #define ngx_http_push_stream_alert_worker_census_subscribers(pid, slot, log) ngx_http_push_stream_alert_worker(pid, slot, log, NGX_CMD_HTTP_PUSH_STREAM_CENSUS_SUBSCRIBERS) 58 | #define ngx_http_push_stream_alert_worker_delete_channel(pid, slot, log) ngx_http_push_stream_alert_worker(pid, slot, log, NGX_CMD_HTTP_PUSH_STREAM_DELETE_CHANNEL) 59 | #define ngx_http_push_stream_alert_worker_shutting_down_cleanup(pid, slot, log) ngx_http_push_stream_alert_worker(pid, slot, log, NGX_CMD_HTTP_PUSH_STREAM_CLEANUP_SHUTTING_DOWN) 60 | 61 | static ngx_int_t ngx_http_push_stream_send_worker_message(ngx_http_push_stream_channel_t *channel, ngx_queue_t *subscriptions_sentinel, ngx_pid_t pid, ngx_int_t worker_slot, ngx_http_push_stream_msg_t *msg, ngx_flag_t *queue_was_empty, ngx_log_t *log, ngx_http_push_stream_main_conf_t *mcf); 62 | 63 | static ngx_int_t ngx_http_push_stream_init_ipc(ngx_cycle_t *cycle, ngx_int_t workers); 64 | static void ngx_http_push_stream_ipc_exit_worker(ngx_cycle_t *cycle); 65 | static ngx_int_t ngx_http_push_stream_ipc_init_worker(void); 66 | static void ngx_http_push_stream_clean_worker_data(ngx_http_push_stream_shm_data_t *data); 67 | static void ngx_http_push_stream_channel_handler(ngx_event_t *ev); 68 | static void ngx_http_push_stream_alert_shutting_down_workers(void); 69 | 70 | 71 | static ngx_inline void ngx_http_push_stream_process_worker_message(void); 72 | static ngx_inline void ngx_http_push_stream_census_worker_subscribers(void); 73 | static ngx_inline void ngx_http_push_stream_cleanup_shutting_down_worker(void); 74 | 75 | static ngx_int_t ngx_http_push_stream_respond_to_subscribers(ngx_http_push_stream_channel_t *channel, ngx_queue_t *subscriptions, ngx_http_push_stream_msg_t *msg); 76 | 77 | #endif /* NGX_HTTP_PUSH_STREAM_MODULE_IPC_H_ */ 78 | -------------------------------------------------------------------------------- /include/ngx_http_push_stream_module_publisher.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2010-2022 Wandenberg Peixoto , Rogério Carvalho Schneider 3 | * 4 | * This file is part of Nginx Push Stream Module. 5 | * 6 | * Nginx Push Stream Module is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * Nginx Push Stream Module is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with Nginx Push Stream Module. If not, see . 18 | * 19 | * 20 | * ngx_http_push_stream_module_publisher.h 21 | * 22 | * Created: Oct 26, 2010 23 | * Authors: Wandenberg Peixoto , Rogério Carvalho Schneider 24 | */ 25 | 26 | #ifndef NGX_HTTP_PUSH_STREAM_MODULE_PUBLISHER_H_ 27 | #define NGX_HTTP_PUSH_STREAM_MODULE_PUBLISHER_H_ 28 | 29 | #include 30 | 31 | static ngx_int_t ngx_http_push_stream_channels_statistics_handler(ngx_http_request_t *r); 32 | static ngx_int_t ngx_http_push_stream_publisher_handler(ngx_http_request_t *r); 33 | static void ngx_http_push_stream_publisher_body_handler(ngx_http_request_t *r); 34 | static void ngx_http_push_stream_publisher_delete_handler(ngx_http_request_t *r); 35 | 36 | #endif /* NGX_HTTP_PUSH_STREAM_MODULE_PUBLISHER_H_ */ 37 | -------------------------------------------------------------------------------- /include/ngx_http_push_stream_module_setup.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2010-2022 Wandenberg Peixoto , Rogério Carvalho Schneider 3 | * 4 | * This file is part of Nginx Push Stream Module. 5 | * 6 | * Nginx Push Stream Module is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * Nginx Push Stream Module is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with Nginx Push Stream Module. If not, see . 18 | * 19 | * 20 | * ngx_http_push_stream_module_setup.h 21 | * 22 | * Created: Oct 26, 2010 23 | * Authors: Wandenberg Peixoto , Rogério Carvalho Schneider 24 | */ 25 | 26 | #ifndef NGX_HTTP_PUSH_STREAM_MODULE_SETUP_H_ 27 | #define NGX_HTTP_PUSH_STREAM_MODULE_SETUP_H_ 28 | 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | 37 | #define NGX_HTTP_PUSH_STREAM_MESSAGE_BUFFER_CLEANUP_INTERVAL 5000 // 5 seconds 38 | static time_t NGX_HTTP_PUSH_STREAM_DEFAULT_SHM_MEMORY_CLEANUP_OBJECTS_TTL = 10; // 10 seconds 39 | static time_t NGX_HTTP_PUSH_STREAM_DEFAULT_SHM_MEMORY_CLEANUP_INTERVAL = 4000; // 4 seconds 40 | static time_t NGX_HTTP_PUSH_STREAM_DEFAULT_MESSAGE_TTL = 1800; // 30 minutes 41 | static time_t NGX_HTTP_PUSH_STREAM_DEFAULT_CHANNEL_INACTIVITY_TIME = 30; // 30 seconds 42 | 43 | #define NGX_HTTP_PUSH_STREAM_DEFAULT_HEADER_TEMPLATE "" 44 | #define NGX_HTTP_PUSH_STREAM_DEFAULT_MESSAGE_TEMPLATE "~text~" 45 | #define NGX_HTTP_PUSH_STREAM_DEFAULT_FOOTER_TEMPLATE "" 46 | 47 | #define NGX_HTTP_PUSH_STREAM_DEFAULT_ALLOWED_ORIGINS "" 48 | 49 | #define NGX_HTTP_PUSH_STREAM_DEFAULT_PADDING_BY_USER_AGENT "" 50 | 51 | #define NGX_HTTP_PUSH_STREAM_DEFAULT_WILDCARD_CHANNEL_PREFIX "" 52 | 53 | #define NGX_HTTP_PUSH_STREAM_DEFAULT_EVENTS_CHANNEL_ID "" 54 | 55 | static char * ngx_http_push_stream_channels_statistics(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); 56 | 57 | // publisher 58 | static char * ngx_http_push_stream_publisher(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); 59 | 60 | // subscriber 61 | static char * ngx_http_push_stream_subscriber(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); 62 | 63 | // setup 64 | static char * ngx_http_push_stream_setup_handler(ngx_conf_t *cf, void *conf, ngx_int_t (*handler) (ngx_http_request_t *)); 65 | static ngx_int_t ngx_http_push_stream_init_module(ngx_cycle_t *cycle); 66 | static ngx_int_t ngx_http_push_stream_init_worker(ngx_cycle_t *cycle); 67 | static void ngx_http_push_stream_exit_worker(ngx_cycle_t *cycle); 68 | static void ngx_http_push_stream_exit_master(ngx_cycle_t *cycle); 69 | static ngx_int_t ngx_http_push_stream_preconfig(ngx_conf_t *cf); 70 | static ngx_int_t ngx_http_push_stream_postconfig(ngx_conf_t *cf); 71 | static void * ngx_http_push_stream_create_main_conf(ngx_conf_t *cf); 72 | static char * ngx_http_push_stream_init_main_conf(ngx_conf_t *cf, void *parent); 73 | static void * ngx_http_push_stream_create_loc_conf(ngx_conf_t *cf); 74 | static char * ngx_http_push_stream_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child); 75 | 76 | // shared memory 77 | char * ngx_http_push_stream_set_shm_size_slot(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); 78 | ngx_int_t ngx_http_push_stream_init_shm_zone(ngx_shm_zone_t *shm_zone, void *data); 79 | ngx_int_t ngx_http_push_stream_init_global_shm_zone(ngx_shm_zone_t *shm_zone, void *data); 80 | 81 | char * ngx_http_push_stream_set_header_template_from_file(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); 82 | 83 | #endif /* NGX_HTTP_PUSH_STREAM_MODULE_SETUP_H_ */ 84 | -------------------------------------------------------------------------------- /include/ngx_http_push_stream_module_subscriber.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2010-2022 Wandenberg Peixoto , Rogério Carvalho Schneider 3 | * 4 | * This file is part of Nginx Push Stream Module. 5 | * 6 | * Nginx Push Stream Module is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * Nginx Push Stream Module is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with Nginx Push Stream Module. If not, see . 18 | * 19 | * 20 | * ngx_http_push_stream_module_subscriber.h 21 | * 22 | * Created: Oct 26, 2010 23 | * Authors: Wandenberg Peixoto , Rogério Carvalho Schneider 24 | */ 25 | 26 | #ifndef NGX_HTTP_PUSH_STREAM_MODULE_SUBSCRIBER_H_ 27 | #define NGX_HTTP_PUSH_STREAM_MODULE_SUBSCRIBER_H_ 28 | 29 | static ngx_int_t ngx_http_push_stream_subscriber_handler(ngx_http_request_t *r); 30 | static ngx_int_t ngx_http_push_stream_validate_channels(ngx_http_request_t *r, ngx_http_push_stream_requested_channel_t *channels_ids, ngx_int_t *status_code, ngx_str_t **explain_error_message); 31 | 32 | #endif /* NGX_HTTP_PUSH_STREAM_MODULE_SUBSCRIBER_H_ */ 33 | -------------------------------------------------------------------------------- /include/ngx_http_push_stream_module_version.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2010-2022 Wandenberg Peixoto , Rogério Carvalho Schneider 3 | * 4 | * This file is part of Nginx Push Stream Module. 5 | * 6 | * Nginx Push Stream Module is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * Nginx Push Stream Module is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with Nginx Push Stream Module. If not, see . 18 | * 19 | * 20 | * ngx_http_push_stream_module.h 21 | * 22 | * Created: Oct 26, 2010 23 | * Authors: Wandenberg Peixoto , Rogério Carvalho Schneider 24 | */ 25 | 26 | #ifndef NGX_HTTP_PUSH_STREAM_MODULE_VERSION_H_ 27 | #define NGX_HTTP_PUSH_STREAM_MODULE_VERSION_H_ 28 | 29 | static const ngx_str_t NGX_HTTP_PUSH_STREAM_TAG = ngx_string("0.5.5"); 30 | static const ngx_str_t NGX_HTTP_PUSH_STREAM_COMMIT = ngx_string("3bc016f811d50374251960db091557571dfd7393"); 31 | 32 | 33 | #endif /* NGX_HTTP_PUSH_STREAM_MODULE_VERSION_H_ */ 34 | -------------------------------------------------------------------------------- /include/ngx_http_push_stream_module_websocket.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2010-2022 Wandenberg Peixoto , Rogério Carvalho Schneider 3 | * 4 | * This file is part of Nginx Push Stream Module. 5 | * 6 | * Nginx Push Stream Module is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * Nginx Push Stream Module is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with Nginx Push Stream Module. If not, see . 18 | * 19 | * 20 | * ngx_http_push_stream_module_websocket.h 21 | * 22 | * Created: Oct 20, 2011 23 | * Authors: Wandenberg Peixoto , Rogério Carvalho Schneider 24 | */ 25 | 26 | #ifndef NGX_HTTP_PUSH_STREAM_MODULE_WEBSOCKET_H_ 27 | #define NGX_HTTP_PUSH_STREAM_MODULE_WEBSOCKET_H_ 28 | 29 | #if (NGX_HAVE_SHA1) 30 | #include 31 | #endif 32 | 33 | #include 34 | #include 35 | 36 | static ngx_int_t ngx_http_push_stream_websocket_handler(ngx_http_request_t *r); 37 | 38 | #define NGX_HTTP_PUSH_STREAM_WEBSOCKET_READ_START_STEP 0 39 | #define NGX_HTTP_PUSH_STREAM_WEBSOCKET_READ_GET_REAL_SIZE_STEP 1 40 | #define NGX_HTTP_PUSH_STREAM_WEBSOCKET_READ_GET_MASK_KEY_STEP 2 41 | #define NGX_HTTP_PUSH_STREAM_WEBSOCKET_READ_GET_PAYLOAD_STEP 3 42 | 43 | #endif /* NGX_HTTP_PUSH_STREAM_MODULE_WEBSOCKET_H_ */ 44 | -------------------------------------------------------------------------------- /include/ngx_http_push_stream_rbtree_util.h: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is distributed under the MIT License. 3 | * 4 | * Copyright (c) 2009 Leo Ponomarev 5 | * 6 | * Permission is hereby granted, free of charge, to any person 7 | * obtaining a copy of this software and associated documentation 8 | * files (the "Software"), to deal in the Software without 9 | * restriction, including without limitation the rights to use, 10 | * copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the 12 | * Software is furnished to do so, subject to the following 13 | * conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be 16 | * included in all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | * OTHER DEALINGS IN THE SOFTWARE. 26 | * 27 | * 28 | * ngx_http_push_stream_rbtree_util.h 29 | * 30 | * Modified: Oct 26, 2010 31 | * Modifications by: Wandenberg Peixoto , Rogério Carvalho Schneider 32 | */ 33 | 34 | #ifndef NGX_HTTP_PUSH_STREAM_RBTREE_UTIL_H_ 35 | #define NGX_HTTP_PUSH_STREAM_RBTREE_UTIL_H_ 36 | 37 | static ngx_http_push_stream_channel_t * ngx_http_push_stream_get_channel(ngx_str_t *id, ngx_log_t *log, ngx_http_push_stream_main_conf_t *mcf); 38 | static ngx_http_push_stream_channel_t * ngx_http_push_stream_find_channel(ngx_str_t *id, ngx_log_t *log, ngx_http_push_stream_main_conf_t *mcf); 39 | 40 | static void ngx_rbtree_generic_insert(ngx_rbtree_node_t *temp, ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel, int (*compare) (const ngx_rbtree_node_t *left, const ngx_rbtree_node_t *right)); 41 | static void ngx_http_push_stream_rbtree_insert(ngx_rbtree_node_t *temp, ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel); 42 | static int ngx_http_push_stream_compare_rbtree_node(const ngx_rbtree_node_t *v_left, const ngx_rbtree_node_t *v_right); 43 | 44 | #endif /* NGX_HTTP_PUSH_STREAM_RBTREE_UTIL_H_ */ 45 | -------------------------------------------------------------------------------- /misc/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | ruby '~> 3.0' 4 | 5 | gem 'rake', '~> 13.2.1' 6 | 7 | group :test do 8 | gem 'rspec' 9 | gem 'nginx_test_helper', '~> 0.4.0' 10 | gem 'listen' 11 | gem 'rb-inotify', require: RUBY_PLATFORM.include?('linux') && 'rb-inotify' 12 | gem 'rb-fsevent', require: RUBY_PLATFORM.include?('darwin') && 'rb-fsevent' 13 | gem 'json' 14 | gem 'puma' 15 | gem 'net-http-persistent', require: 'net/http/persistent' 16 | gem 'websocket-eventmachine-client' 17 | gem 'em-eventsource' 18 | 19 | gem 'byebug' 20 | end 21 | 22 | group :docs do 23 | gem 'github-markup', require: 'github/markup' 24 | gem 'RedCloth' 25 | gem 'nokogiri' 26 | gem 'filewatcher' 27 | end 28 | -------------------------------------------------------------------------------- /misc/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | Platform (0.4.2) 5 | RedCloth (4.3.4) 6 | addressable (2.8.6) 7 | public_suffix (>= 2.0.2, < 6.0) 8 | byebug (11.1.3) 9 | connection_pool (2.4.1) 10 | cookiejar (0.3.4) 11 | diff-lcs (1.5.1) 12 | em-eventsource (0.3.2) 13 | em-http-request (~> 1.0) 14 | eventmachine (~> 1.0) 15 | em-http-request (1.1.7) 16 | addressable (>= 2.3.4) 17 | cookiejar (!= 0.3.1) 18 | em-socksify (>= 0.3) 19 | eventmachine (>= 1.0.3) 20 | http_parser.rb (>= 0.6.0) 21 | em-socksify (0.3.2) 22 | eventmachine (>= 1.0.0.beta.4) 23 | eventmachine (1.2.7) 24 | ffi (1.16.3) 25 | filewatcher (2.1.0) 26 | module_methods (~> 0.1.0) 27 | github-markup (4.0.2) 28 | http_parser.rb (0.8.0) 29 | json (2.7.2) 30 | listen (3.9.0) 31 | rb-fsevent (~> 0.10, >= 0.10.3) 32 | rb-inotify (~> 0.9, >= 0.9.10) 33 | mini_portile2 (2.8.6) 34 | module_methods (0.1.0) 35 | net-http-persistent (4.0.2) 36 | connection_pool (~> 2.2) 37 | nginx_test_helper (0.4.2) 38 | popen4 39 | nio4r (2.7.3) 40 | nokogiri (1.16.4) 41 | mini_portile2 (~> 2.8.2) 42 | racc (~> 1.4) 43 | open4 (1.3.4) 44 | popen4 (0.1.2) 45 | Platform (>= 0.4.0) 46 | open4 (>= 0.4.0) 47 | public_suffix (5.0.5) 48 | puma (6.4.2) 49 | nio4r (~> 2.0) 50 | racc (1.7.3) 51 | rake (13.2.1) 52 | rb-fsevent (0.11.2) 53 | rb-inotify (0.10.1) 54 | ffi (~> 1.0) 55 | rspec (3.13.0) 56 | rspec-core (~> 3.13.0) 57 | rspec-expectations (~> 3.13.0) 58 | rspec-mocks (~> 3.13.0) 59 | rspec-core (3.13.0) 60 | rspec-support (~> 3.13.0) 61 | rspec-expectations (3.13.0) 62 | diff-lcs (>= 1.2.0, < 2.0) 63 | rspec-support (~> 3.13.0) 64 | rspec-mocks (3.13.1) 65 | diff-lcs (>= 1.2.0, < 2.0) 66 | rspec-support (~> 3.13.0) 67 | rspec-support (3.13.1) 68 | websocket (1.2.10) 69 | websocket-eventmachine-base (1.2.0) 70 | eventmachine (~> 1.0) 71 | websocket (~> 1.0) 72 | websocket-native (~> 1.0) 73 | websocket-eventmachine-client (1.3.0) 74 | websocket-eventmachine-base (~> 1.0) 75 | websocket-native (1.0.0) 76 | 77 | PLATFORMS 78 | ruby 79 | 80 | DEPENDENCIES 81 | RedCloth 82 | byebug 83 | em-eventsource 84 | filewatcher 85 | github-markup 86 | json 87 | listen 88 | net-http-persistent 89 | nginx_test_helper (~> 0.4.0) 90 | nokogiri 91 | puma 92 | rake (~> 13.2.1) 93 | rb-fsevent 94 | rb-inotify 95 | rspec 96 | websocket-eventmachine-client 97 | 98 | RUBY VERSION 99 | ruby 3.0.2p107 100 | 101 | BUNDLED WITH 102 | 2.2.32 103 | -------------------------------------------------------------------------------- /misc/Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'erb' 3 | require 'tmpdir' 4 | require 'uri' 5 | require 'open-uri' 6 | require 'fileutils' 7 | 8 | # Set up gems listed in the Gemfile. 9 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('./Gemfile', File.dirname(__FILE__)) 10 | require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) 11 | Bundler.require(:default, :test) if defined?(Bundler) 12 | 13 | project_dir = File.expand_path('..', File.dirname(__FILE__)) 14 | base_path = File.expand_path('pushstream/docs/preview', Dir.tmpdir) 15 | javascript_dir = File.expand_path(Dir["#{project_dir}/**/js"].first) 16 | 17 | begin 18 | require "rspec/core/rake_task" 19 | 20 | desc "Run all examples" 21 | RSpec::Core::RakeTask.new(:spec) do |t| 22 | t.rspec_opts = %w[--color] 23 | t.pattern = '**/*_spec.rb' 24 | end 25 | rescue LoadError 26 | task :spec do 27 | abort "RSpec is not available. In order to run rspec, you must: (sudo) gem install rspec" 28 | end 29 | end 30 | 31 | begin 32 | desc 'Start server to host jasmine specs' 33 | task 'jasmine' => ['dependencies', 'jshint', 'test_server'] do 34 | system("npx jasmine-browser-runner serve --config=#{File.expand_path('misc/spec/javascripts/support/jasmine-browser.json', project_dir)}") rescue Interrupt 35 | end 36 | 37 | desc 'Run jasmine tests in a browser, random and seed override config' 38 | task 'jasmine:ci', [:random, :seed] => ['dependencies', 'jshint', 'test_server'] do |t, args| 39 | params = [] 40 | params << "--seed=#{args[:seed]}" if args[:seed] 41 | params << '--random' if args[:random] 42 | system("npx jasmine-browser-runner runSpecs #{params.join(' ')} --config=#{File.expand_path('misc/spec/javascripts/support/jasmine-browser.json', project_dir)}") rescue Interrupt 43 | end 44 | 45 | desc 'Run jshint' 46 | task :jshint => ['dependencies'] do 47 | system("npx jshint #{javascript_dir}/pushstream.js") 48 | end 49 | 50 | task :dependencies do 51 | system('yarn install -s') 52 | end 53 | 54 | task :test_server do 55 | require File.expand_path('misc/spec/spec_helper', project_dir) 56 | include NginxTestHelper 57 | template = File.read(File.expand_path('misc/nginx.conf', project_dir)) 58 | template.gsub!(/push_stream_subscriber_connection_ttl.*;/, 'push_stream_subscriber_connection_ttl 3s;') 59 | template.gsub!(/push_stream_longpolling_connection_ttl.*;/, 'push_stream_longpolling_connection_ttl 3s;') 60 | config = NginxTestHelper::Config.new "jasmine", {:configuration_template => (RUBY_PLATFORM =~ /darwin/) ? template.gsub('epoll', 'kqueue') : template } 61 | abort "Could not start test server" if start_server(config).include?("[emerg]") 62 | end 63 | end 64 | 65 | namespace :docs do 66 | 67 | begin 68 | Bundler.require(:default, :docs) if defined?(Bundler) 69 | 70 | task :get_static_files do 71 | FileUtils.mkdir_p("#{base_path}/css") 72 | 73 | download_file("https://github.githubassets.com/assets/global-efcb6353627d.css", "#{base_path}/css/github.css") unless File.exists?("#{base_path}/css/github.css") 74 | download_file("https://github.githubassets.com/assets/github-07f750db5d7c.css", "#{base_path}/css/github2.css") unless File.exists?("#{base_path}/css/github2.css") 75 | end 76 | 77 | def generate_preview_for(file, project_dir, base_path) 78 | template = ERB.new File.read("#{project_dir}/misc/github_template.html.erb") 79 | filename = File.basename(file) 80 | content = GitHub::Markup.render(file, File.read(file)) 81 | rendered = template.result(binding) 82 | output = File.expand_path(file.gsub(project_dir, "./"), base_path) 83 | output_dir = File.dirname(output) 84 | FileUtils.mkdir_p(output_dir) 85 | File.open(output, 'w') {|f| f.write(rendered) } 86 | puts "Preview rendered to #{output}" 87 | end 88 | 89 | desc "Generates docs files to preview." 90 | task :generate => :get_static_files do 91 | Dir.glob("#{project_dir}/**/*.textile").each do |file| 92 | generate_preview_for(file, project_dir, base_path) 93 | end 94 | end 95 | 96 | desc "Watch for changes on doc to generate the preview." 97 | task :watch do 98 | puts "watching for changes on textile files" 99 | Dir.chdir(project_dir) do 100 | FileWatcher.new(["**/*.textile"]).watch do |file, event| 101 | if(event != :delete) 102 | generate_preview_for(file, project_dir, base_path) 103 | end 104 | end 105 | end 106 | end 107 | 108 | desc "Convert docs to Nginx wiki format." 109 | task :convert_to_wiki do 110 | Dir.glob("#{project_dir}/**/*.textile").each do |file| 111 | filename = File.basename(file) 112 | content = File.read(file) 113 | 114 | output = file.gsub(project_dir, File.expand_path('pushstream/docs/html', Dir.tmpdir)).gsub(".textile", ".html") 115 | output_wiki = file.gsub(project_dir, File.expand_path('pushstream/docs/wiki', Dir.tmpdir)).gsub(".textile", ".wiki") 116 | FileUtils.mkdir_p(File.dirname(output)) 117 | FileUtils.mkdir_p(File.dirname(output_wiki)) 118 | 119 | File.open(output, 'w') {|f| f.write(RedCloth.new(content).to_html) } 120 | File.open(output_wiki, 'w') {|f| f.write(convert_to_wiki_syntax(content)) } 121 | puts "Wiki converted to #{output_wiki}" 122 | end 123 | end 124 | rescue LoadError 125 | desc "Generates docs files to preview." 126 | task :generate do 127 | abort "github-markup is not available. In order to run docs:generate, you must: (sudo) gem install github-markup" 128 | end 129 | 130 | desc "Convert docs to Nginx wiki format." 131 | task :convert_to_wiki do 132 | abort "RedCloth or nokogiri is not available. In order to run docs:convert_to_wiki, you must: (sudo) gem install RedCloth nokogiri" 133 | end 134 | end 135 | 136 | def download_file(url, output_file) 137 | case io = URI.open(url) 138 | when StringIO then File.open(output_file, 'w') { |f| f.write(io) } 139 | when Tempfile then io.close; FileUtils.mv(io.path, output_file) 140 | end 141 | end 142 | 143 | def convert_to_wiki_syntax(text) 144 | doc = Nokogiri::HTML(RedCloth.new(text).to_html) 145 | convert_elements(doc.children.to_a) 146 | end 147 | 148 | def convert_elements(nodes) 149 | result = "" 150 | nodes.each do |node| 151 | if node.element? && !node.text? 152 | childrens = node.children.to_a 153 | unless childrens.empty? 154 | result += convert_element(convert_elements(childrens), node) 155 | end 156 | elsif node.text? 157 | result += node.text 158 | end 159 | end 160 | result 161 | end 162 | 163 | def convert_element(text, node) 164 | tag = node.name 165 | text ||= "" 166 | case tag 167 | when "strong" 168 | "'''#{text}'''" 169 | when "b" 170 | "'''#{text}'''" 171 | when "em" 172 | "''#{text}''" 173 | when "h1" 174 | "\n= #{text} =" 175 | when "h2" 176 | "\n== #{text} ==" 177 | when "h3" 178 | "\n=== #{text} ===" 179 | when "h4" 180 | "\n==== #{text} ====" 181 | when "h5" 182 | "\n===== #{text} =====" 183 | when "h6" 184 | "\n====== #{text} ======" 185 | when "p" 186 | "\n#{text}" 187 | when "a" 188 | if node.attributes['href'].value.start_with?("#") 189 | "[[#{node.attributes['href'].value}|#{text}]]" 190 | else 191 | "[#{node.attributes['href'].value} #{text}]" 192 | end 193 | when "html" 194 | text 195 | when "body" 196 | text 197 | when "span" 198 | text 199 | else 200 | "<#{tag}>#{text}" 201 | end 202 | end 203 | end 204 | -------------------------------------------------------------------------------- /misc/examples/chat.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | Chat - Push Stream Module Example 7 | 10 | 11 | 12 |
13 |

14 | mode: 15 | 16 |

17 |

18 | satus: 19 | 20 | offline 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 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /misc/examples/chat_longpolling.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | Chat Long Polling - Push Stream Module Example 7 | 10 | 11 | 12 |
13 |

14 | mode: 15 | 16 |

17 |

18 | satus: 19 | 20 | offline 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 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /misc/github_template.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <%= filename %> - Preview to GitHub 7 | 8 | 9 | 10 | 17 | 18 | 19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | <%= content %> 29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | 39 | 40 | -------------------------------------------------------------------------------- /misc/mime.types: -------------------------------------------------------------------------------- 1 | 2 | types { 3 | text/html html htm shtml; 4 | text/css css; 5 | text/xml xml; 6 | image/gif gif; 7 | image/jpeg jpeg jpg; 8 | application/x-javascript js; 9 | application/atom+xml atom; 10 | application/rss+xml rss; 11 | 12 | text/mathml mml; 13 | text/plain txt; 14 | text/vnd.sun.j2me.app-descriptor jad; 15 | text/vnd.wap.wml wml; 16 | text/x-component htc; 17 | 18 | image/png png; 19 | image/tiff tif tiff; 20 | image/vnd.wap.wbmp wbmp; 21 | image/x-icon ico; 22 | image/x-jng jng; 23 | image/x-ms-bmp bmp; 24 | image/svg+xml svg; 25 | 26 | application/java-archive jar war ear; 27 | application/mac-binhex40 hqx; 28 | application/msword doc; 29 | application/pdf pdf; 30 | application/postscript ps eps ai; 31 | application/rtf rtf; 32 | application/vnd.ms-excel xls; 33 | application/vnd.ms-powerpoint ppt; 34 | application/vnd.wap.wmlc wmlc; 35 | application/vnd.wap.xhtml+xml xhtml; 36 | application/vnd.google-earth.kml+xml kml; 37 | application/vnd.google-earth.kmz kmz; 38 | application/x-cocoa cco; 39 | application/x-java-archive-diff jardiff; 40 | application/x-java-jnlp-file jnlp; 41 | application/x-makeself run; 42 | application/x-perl pl pm; 43 | application/x-pilot prc pdb; 44 | application/x-rar-compressed rar; 45 | application/x-redhat-package-manager rpm; 46 | application/x-sea sea; 47 | application/x-shockwave-flash swf; 48 | application/x-stuffit sit; 49 | application/x-tcl tcl tk; 50 | application/x-x509-ca-cert der pem crt; 51 | application/x-xpinstall xpi; 52 | application/zip zip; 53 | 54 | application/octet-stream bin exe dll; 55 | application/octet-stream deb; 56 | application/octet-stream dmg; 57 | application/octet-stream eot; 58 | application/octet-stream iso img; 59 | application/octet-stream msi msp msm; 60 | 61 | audio/midi mid midi kar; 62 | audio/mpeg mp3; 63 | audio/x-realaudio ra; 64 | 65 | video/3gpp 3gpp 3gp; 66 | video/mpeg mpeg mpg; 67 | video/quicktime mov; 68 | video/x-flv flv; 69 | video/x-mng mng; 70 | video/x-ms-asf asx asf; 71 | video/x-ms-wmv wmv; 72 | video/x-msvideo avi; 73 | } 74 | -------------------------------------------------------------------------------- /misc/nginx.conf: -------------------------------------------------------------------------------- 1 | pid logs/nginx.pid; 2 | error_log logs/nginx-main_error.log debug; 3 | 4 | # Development Mode 5 | master_process off; 6 | daemon off; 7 | worker_rlimit_core 2500M; 8 | working_directory /tmp; 9 | debug_points abort; 10 | env MOCKEAGAIN_VERBOSE; 11 | env MOCKEAGAIN_WRITE_TIMEOUT_PATTERN; 12 | #env MOCKEAGAIN; 13 | env LD_PRELOAD; 14 | 15 | worker_processes 2; 16 | 17 | events { 18 | worker_connections 1024; 19 | use poll; 20 | } 21 | 22 | http { 23 | postpone_output 1; # only postpone a single byte, default 1460 bytes 24 | access_log logs/nginx-http_access.log; 25 | 26 | push_stream_shared_memory_size 100m; 27 | push_stream_max_channel_id_length 200; 28 | # max messages to store in memory 29 | push_stream_max_messages_stored_per_channel 20; 30 | # message ttl 31 | push_stream_message_ttl 5m; 32 | # ping frequency 33 | push_stream_ping_message_interval 30s; 34 | # connection ttl to enable recycle 35 | push_stream_subscriber_connection_ttl 15m; 36 | # connection ttl for long polling 37 | push_stream_longpolling_connection_ttl 30s; 38 | push_stream_timeout_with_body off; 39 | 40 | # wildcard 41 | push_stream_wildcard_channel_prefix "broad_"; 42 | push_stream_wildcard_channel_max_qtd 3; 43 | 44 | push_stream_message_template "{\"id\":~id~,\"channel\":\"~channel~\",\"text\":\"~text~\", \"tag\":\"~tag~\", \"time\":\"~time~\", \"eventid\":\"~event-id~\"}"; 45 | 46 | # subscriber may create channels on demand or only authorized (publisher) may do it? 47 | push_stream_authorized_channels_only off; 48 | 49 | push_stream_allowed_origins "*"; 50 | 51 | server { 52 | listen 9080 default_server; 53 | #listen 9443 ssl; 54 | #ssl_certificate /usr/local/nginx/ssl/server.crt; 55 | #ssl_certificate_key /usr/local/nginx/ssl/server.key; 56 | server_name localhost; 57 | 58 | location /channels-stats { 59 | # activate channels statistics mode for this location 60 | push_stream_channels_statistics; 61 | 62 | # query string based channel id 63 | push_stream_channels_path $arg_id; 64 | } 65 | 66 | location /pub { 67 | # activate publisher mode for this location, with admin support 68 | push_stream_publisher admin; 69 | 70 | # query string based channel id 71 | push_stream_channels_path $arg_id; 72 | 73 | # store messages in memory 74 | push_stream_store_messages on; 75 | 76 | # Message size limit 77 | # client_max_body_size MUST be equal to client_body_buffer_size or 78 | # you will be sorry. 79 | client_max_body_size 32k; 80 | client_body_buffer_size 32k; 81 | } 82 | 83 | location ~ /sub/(.*) { 84 | # activate subscriber mode for this location 85 | push_stream_subscriber; 86 | 87 | # positional channel path 88 | push_stream_channels_path $1; 89 | if ($arg_tests = "on") { 90 | push_stream_channels_path "test_$1"; 91 | } 92 | 93 | # header to be sent when receiving new subscriber connection 94 | push_stream_header_template "\r\n\r\n\r\n\r\n\r\n\r\n\r\n"; 95 | 96 | # message template 97 | push_stream_message_template ""; 98 | # footer to be sent when finishing subscriber connection 99 | push_stream_footer_template ""; 100 | # content-type 101 | default_type "text/html; charset=utf-8"; 102 | 103 | if ($arg_qs = "on") { 104 | push_stream_last_received_message_time "$arg_time"; 105 | push_stream_last_received_message_tag "$arg_tag"; 106 | push_stream_last_event_id "$arg_eventid"; 107 | } 108 | } 109 | 110 | location ~ /ev/(.*) { 111 | # activate event source mode for this location 112 | push_stream_subscriber eventsource; 113 | 114 | # positional channel path 115 | push_stream_channels_path $1; 116 | if ($arg_tests = "on") { 117 | push_stream_channels_path "test_$1"; 118 | } 119 | 120 | if ($arg_qs = "on") { 121 | push_stream_last_received_message_time "$arg_time"; 122 | push_stream_last_received_message_tag "$arg_tag"; 123 | push_stream_last_event_id "$arg_eventid"; 124 | } 125 | } 126 | 127 | location ~ /lp/(.*) { 128 | # activate long-polling mode for this location 129 | push_stream_subscriber long-polling; 130 | 131 | # positional channel path 132 | push_stream_channels_path $1; 133 | if ($arg_tests = "on") { 134 | push_stream_channels_path "test_$1"; 135 | } 136 | 137 | if ($arg_qs = "on") { 138 | push_stream_last_received_message_time "$arg_time"; 139 | push_stream_last_received_message_tag "$arg_tag"; 140 | push_stream_last_event_id "$arg_eventid"; 141 | } 142 | } 143 | 144 | location ~ /jsonp/(.*) { 145 | # activate long-polling mode for this location 146 | push_stream_subscriber long-polling; 147 | 148 | push_stream_last_received_message_time "$arg_time"; 149 | push_stream_last_received_message_tag "$arg_tag"; 150 | push_stream_last_event_id "$arg_eventid"; 151 | 152 | # positional channel path 153 | push_stream_channels_path $1; 154 | if ($arg_tests = "on") { 155 | push_stream_channels_path "test_$1"; 156 | } 157 | } 158 | 159 | location ~ /ws/(.*) { 160 | # activate websocket mode for this location 161 | push_stream_subscriber websocket; 162 | 163 | # positional channel path 164 | push_stream_channels_path $1; 165 | if ($arg_tests = "on") { 166 | push_stream_channels_path "test_$1"; 167 | } 168 | 169 | # store messages in memory 170 | push_stream_store_messages on; 171 | 172 | push_stream_websocket_allow_publish on; 173 | 174 | if ($arg_qs = "on") { 175 | push_stream_last_received_message_time "$arg_time"; 176 | push_stream_last_received_message_tag "$arg_tag"; 177 | push_stream_last_event_id "$arg_eventid"; 178 | } 179 | } 180 | 181 | location / { 182 | if (!-f $request_filename) { 183 | proxy_pass "http://localhost:8888"; 184 | } 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /misc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "MIT", 3 | "devDependencies": { 4 | "jasmine-browser-runner": "2.4.0", 5 | "jasmine-core": "5.1.2", 6 | "jshint": "2.13.6" 7 | }, 8 | "jshintConfig": { 9 | "browser": true, 10 | "evil": true, 11 | "plusplus": false, 12 | "regexp": false, 13 | "undef": true, 14 | "unused": true, 15 | "globals": {} 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /misc/spec/javascripts/UtilsSpec.js: -------------------------------------------------------------------------------- 1 | describe("Utils", function() { 2 | var jsonKeys = { 3 | jsonIdKey : 'id', 4 | jsonChannelKey : 'channel', 5 | jsonTextKey : 'text', 6 | jsonTagKey : 'tag', 7 | jsonTimeKey : 'time', 8 | jsonEventIdKey : 'eventid' 9 | }; 10 | 11 | beforeEach(function() { 12 | }); 13 | 14 | describe("when formatting dates to UTC string", function() { 15 | it("should return the string with two digits for day", function () { 16 | expect(Utils.dateToUTCString(Date.fromISO("2012-11-09T12:00:00-03:00"))).toBe("Fri, 09 Nov 2012 15:00:00 GMT"); 17 | expect(Utils.dateToUTCString(Date.fromISO("2012-11-10T12:00:00-03:00"))).toBe("Sat, 10 Nov 2012 15:00:00 GMT"); 18 | }); 19 | 20 | it("should return the string with two digits for hour", function () { 21 | expect(Utils.dateToUTCString(Date.fromISO("2012-11-09T06:00:00-03:00"))).toBe("Fri, 09 Nov 2012 09:00:00 GMT"); 22 | expect(Utils.dateToUTCString(Date.fromISO("2012-11-10T07:00:00-03:00"))).toBe("Sat, 10 Nov 2012 10:00:00 GMT"); 23 | }); 24 | 25 | it("should return the string with two digits for minute", function () { 26 | expect(Utils.dateToUTCString(Date.fromISO("2012-11-09T06:09:00-03:00"))).toBe("Fri, 09 Nov 2012 09:09:00 GMT"); 27 | expect(Utils.dateToUTCString(Date.fromISO("2012-11-10T07:10:00-03:00"))).toBe("Sat, 10 Nov 2012 10:10:00 GMT"); 28 | }); 29 | 30 | it("should return the string with two digits for second", function () { 31 | expect(Utils.dateToUTCString(Date.fromISO("2012-11-09T06:09:09-03:00"))).toBe("Fri, 09 Nov 2012 09:09:09 GMT"); 32 | expect(Utils.dateToUTCString(Date.fromISO("2012-11-10T07:10:10-03:00"))).toBe("Sat, 10 Nov 2012 10:10:10 GMT"); 33 | }); 34 | 35 | it("should return the right text for months", function () { 36 | expect(Utils.dateToUTCString(Date.fromISO("2012-01-09T06:09:09-03:00"))).toBe("Mon, 09 Jan 2012 09:09:09 GMT"); 37 | expect(Utils.dateToUTCString(Date.fromISO("2012-02-09T06:09:09-03:00"))).toBe("Thu, 09 Feb 2012 09:09:09 GMT"); 38 | expect(Utils.dateToUTCString(Date.fromISO("2012-03-09T06:09:09-03:00"))).toBe("Fri, 09 Mar 2012 09:09:09 GMT"); 39 | expect(Utils.dateToUTCString(Date.fromISO("2012-04-09T06:09:09-03:00"))).toBe("Mon, 09 Apr 2012 09:09:09 GMT"); 40 | expect(Utils.dateToUTCString(Date.fromISO("2012-05-09T06:09:09-03:00"))).toBe("Wed, 09 May 2012 09:09:09 GMT"); 41 | expect(Utils.dateToUTCString(Date.fromISO("2012-06-09T06:09:09-03:00"))).toBe("Sat, 09 Jun 2012 09:09:09 GMT"); 42 | expect(Utils.dateToUTCString(Date.fromISO("2012-07-09T06:09:09-03:00"))).toBe("Mon, 09 Jul 2012 09:09:09 GMT"); 43 | expect(Utils.dateToUTCString(Date.fromISO("2012-08-09T06:09:09-03:00"))).toBe("Thu, 09 Aug 2012 09:09:09 GMT"); 44 | expect(Utils.dateToUTCString(Date.fromISO("2012-09-09T06:09:09-03:00"))).toBe("Sun, 09 Sep 2012 09:09:09 GMT"); 45 | expect(Utils.dateToUTCString(Date.fromISO("2012-10-09T06:09:09-03:00"))).toBe("Tue, 09 Oct 2012 09:09:09 GMT"); 46 | expect(Utils.dateToUTCString(Date.fromISO("2012-11-09T06:09:09-03:00"))).toBe("Fri, 09 Nov 2012 09:09:09 GMT"); 47 | expect(Utils.dateToUTCString(Date.fromISO("2012-12-09T06:09:09-03:00"))).toBe("Sun, 09 Dec 2012 09:09:09 GMT"); 48 | }); 49 | 50 | it("should return the right text for days", function () { 51 | expect(Utils.dateToUTCString(Date.fromISO("2012-01-01T06:09:09-03:00"))).toBe("Sun, 01 Jan 2012 09:09:09 GMT"); 52 | expect(Utils.dateToUTCString(Date.fromISO("2012-01-02T06:09:09-03:00"))).toBe("Mon, 02 Jan 2012 09:09:09 GMT"); 53 | expect(Utils.dateToUTCString(Date.fromISO("2012-01-03T06:09:09-03:00"))).toBe("Tue, 03 Jan 2012 09:09:09 GMT"); 54 | expect(Utils.dateToUTCString(Date.fromISO("2012-01-04T06:09:09-03:00"))).toBe("Wed, 04 Jan 2012 09:09:09 GMT"); 55 | expect(Utils.dateToUTCString(Date.fromISO("2012-01-05T06:09:09-03:00"))).toBe("Thu, 05 Jan 2012 09:09:09 GMT"); 56 | expect(Utils.dateToUTCString(Date.fromISO("2012-01-06T06:09:09-03:00"))).toBe("Fri, 06 Jan 2012 09:09:09 GMT"); 57 | expect(Utils.dateToUTCString(Date.fromISO("2012-01-07T06:09:09-03:00"))).toBe("Sat, 07 Jan 2012 09:09:09 GMT"); 58 | }); 59 | }); 60 | 61 | describe("when parse JSON", function() { 62 | it("should return null when data is null", function () { 63 | expect(Utils.parseJSON(null)).toBe(null); 64 | }); 65 | 66 | it("should return null when data is undefined", function () { 67 | expect(Utils.parseJSON(undefined)).toBe(null); 68 | }); 69 | 70 | it("should return null when data is not a string", function () { 71 | expect(Utils.parseJSON({})).toBe(null); 72 | }); 73 | 74 | if (window.JSON) { 75 | describe("when have a default implementation for JSON.parse", function () { 76 | var jsonImplementation = null; 77 | beforeEach(function() { 78 | jsonImplementation = window.JSON; 79 | // window.JSON = null; 80 | }); 81 | 82 | afterEach(function() { 83 | window.JSON = jsonImplementation; 84 | }); 85 | 86 | it("should use the browser default implementation when available", function () { 87 | spyOn(window.JSON, "parse"); 88 | Utils.parseJSON('{"a":1}'); 89 | expect(window.JSON.parse).toHaveBeenCalledWith('{"a":1}'); 90 | }); 91 | 92 | it("should parse a well formed json string", function () { 93 | expect(Utils.parseJSON('{"a":1}')["a"]).toBe(1); 94 | }); 95 | 96 | it("should parse when the string has leading spaces", function () { 97 | expect(Utils.parseJSON(' {"a":1}')["a"]).toBe(1); 98 | }); 99 | 100 | it("should parse when the string has trailing spaces", function () { 101 | expect(Utils.parseJSON('{"a":1} ')["a"]).toBe(1); 102 | }); 103 | 104 | it("should raise error when string is a invalid json", function () { 105 | expect(function () { Utils.parseJSON('{"a":1[]}'); }).toThrow('Invalid JSON: {"a":1[]}'); 106 | }); 107 | }); 108 | } 109 | 110 | describe("when do not have a default implementation for JSON.parse", function () { 111 | var jsonImplementation = null; 112 | beforeEach(function() { 113 | jsonImplementation = window.JSON; 114 | window.JSON = null; 115 | }); 116 | 117 | afterEach(function() { 118 | window.JSON = jsonImplementation; 119 | }); 120 | 121 | it("should parse a well formed json string", function () { 122 | expect(Utils.parseJSON('{"a":1}')["a"]).toBe(1); 123 | }); 124 | 125 | it("should parse when the string has leading spaces", function () { 126 | expect(Utils.parseJSON(' {"a":1}')["a"]).toBe(1); 127 | }); 128 | 129 | it("should parse when the string has trailing spaces", function () { 130 | expect(Utils.parseJSON('{"a":1} ')["a"]).toBe(1); 131 | }); 132 | 133 | it("should raise error when string is a invalid json", function () { 134 | expect(function () { Utils.parseJSON('{"a":1[]}'); }).toThrow('Invalid JSON: {"a":1[]}'); 135 | }); 136 | }); 137 | }); 138 | 139 | describe("when extract xss domain", function() { 140 | it("should return the ip address when domain is only an ip", function() { 141 | expect(Utils.extract_xss_domain("201.10.32.52")).toBe("201.10.32.52"); 142 | }); 143 | 144 | it("should return the full domain when it has only two parts", function() { 145 | expect(Utils.extract_xss_domain("domain.com")).toBe("domain.com"); 146 | }); 147 | 148 | it("should return the last two parts when domain has three parts", function() { 149 | expect(Utils.extract_xss_domain("example.domain.com")).toBe("domain.com"); 150 | }); 151 | 152 | it("should return all parts minus the first one when domain has more than three parts", function() { 153 | expect(Utils.extract_xss_domain("another.example.domain.com")).toBe("example.domain.com"); 154 | }); 155 | }); 156 | 157 | describe("when parsing a message", function() { 158 | it("should accept a simple string as text", function() { 159 | var message = Utils.parseMessage('{"id":31,"channel":"54x19","text":"some simple string"}', jsonKeys); 160 | expect(message.text).toBe("some simple string"); 161 | }); 162 | 163 | it("should accept a json as text", function() { 164 | var message = Utils.parseMessage('{"id":31,"channel":"54x19","text":{"id":"500516b7639e4029b8000001","type":"Player","change":{"loc":[54.34772390000001,18.5610535],"version":7}}}', jsonKeys); 165 | expect(message.text.id).toBe("500516b7639e4029b8000001"); 166 | expect(message.text.type).toBe("Player"); 167 | expect(message.text.change.loc[0]).toBe(54.34772390000001); 168 | expect(message.text.change.loc[1]).toBe(18.5610535); 169 | expect(message.text.change.version).toBe(7); 170 | }); 171 | 172 | it("should accept an escaped json as text", function() { 173 | var message = Utils.parseMessage('{"id":31,"channel":"54x19","text":"%7B%22id%22%3A%22500516b7639e4029b8000001%22%2C%22type%22%3A%22Player%22%2C%22change%22%3A%7B%22loc%22%3A%5B54.34772390000001%2C18.5610535%5D%2C%22version%22%3A7%7D%7D"}', jsonKeys); 174 | expect(message.text).toBe('{"id":"500516b7639e4029b8000001","type":"Player","change":{"loc":[54.34772390000001,18.5610535],"version":7}}'); 175 | }); 176 | }); 177 | }); 178 | -------------------------------------------------------------------------------- /misc/spec/javascripts/helpers/SpecHelper.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var D = new Date('2011-06-02T09:34:29+02:00'); 3 | if (!D || +D !== 1307000069000) { 4 | Date.fromISO = function(s) { 5 | var day, tz, rx = /^(\d{4}\-\d\d\-\d\d([tT ][\d:\.]*)?)([zZ]|([+\-])(\d\d):(\d\d))?$/, p = rx.exec(s) || []; 6 | if (p[1]) { 7 | day = p[1].split(/\D/); 8 | for (var i = 0, L = day.length; i < L; i++) { 9 | day[i] = parseInt(day[i], 10) || 0; 10 | }; 11 | day[1] -= 1; 12 | day = new Date(Date.UTC.apply(Date, day)); 13 | if (!day.getDate()) 14 | return NaN; 15 | if (p[5]) { 16 | tz = (parseInt(p[5], 10) * 60); 17 | if (p[6]) 18 | tz += parseInt(p[6], 10); 19 | if (p[4] == '+') 20 | tz *= -1; 21 | if (tz) 22 | day.setUTCMinutes(day.getUTCMinutes() + tz); 23 | } 24 | return day; 25 | } 26 | return NaN; 27 | }; 28 | } else { 29 | Date.fromISO = function(s) { 30 | return new Date(s); 31 | }; 32 | } 33 | })(); 34 | 35 | 36 | // This is the equivalent of the old waitsFor/runs syntax 37 | // which was removed from Jasmine 2 38 | var waitsForAndRuns = function(escapeFunction, runFunction, escapeTime) { 39 | // check the escapeFunction every millisecond so as soon as it is met we can escape the function 40 | var interval = setInterval(function() { 41 | if (escapeFunction()) { 42 | clearMe(); 43 | runFunction(); 44 | } 45 | }, 1); 46 | 47 | // in case we never reach the escapeFunction, we will time out 48 | // at the escapeTime 49 | var timeOut = setTimeout(function() { 50 | clearMe(); 51 | runFunction(); 52 | }, escapeTime); 53 | 54 | // clear the interval and the timeout 55 | function clearMe(){ 56 | clearInterval(interval); 57 | clearTimeout(timeOut); 58 | } 59 | }; 60 | -------------------------------------------------------------------------------- /misc/spec/javascripts/support/jasmine-browser.json: -------------------------------------------------------------------------------- 1 | { 2 | "srcDir": "js", 3 | "srcFiles": [ 4 | "**/*.js" 5 | ], 6 | "specDir": "spec/javascripts", 7 | "specFiles": [ 8 | "**/*[sS]pec.js" 9 | ], 10 | "helpers": [ 11 | "helpers/**/*.js" 12 | ], 13 | "env": { 14 | "stopSpecOnExpectationFailure": false, 15 | "stopOnSpecFailure": false, 16 | "random": false 17 | }, 18 | "browser": { 19 | "name": "chrome" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /misc/spec/mix/keepalive_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Keepalive" do 4 | let(:config) do 5 | { 6 | :shared_memory_size => '256m', 7 | :keepalive_requests => 500, 8 | :header_template => '', 9 | :message_template => '~text~', 10 | :footer_template => '', 11 | :publisher_mode => 'admin' 12 | } 13 | end 14 | 15 | it "should create many channels on the same socket" do 16 | channel = 'ch_test_create_many_channels_' 17 | body = 'channel started' 18 | channels_to_be_created = 4000 19 | 20 | nginx_run_server(config, :timeout => 25) do |conf| 21 | http_single = Net::HTTP::Persistent.new name: "single_channel" 22 | http_double = Net::HTTP::Persistent.new name: "double_channel" 23 | uri = URI.parse nginx_address 24 | 25 | 0.step(channels_to_be_created - 1, 500) do |i| 26 | 1.upto(500) do |j| 27 | post_single = Net::HTTP::Post.new "/pub?id=#{channel}#{i + j}" 28 | post_single.body = body 29 | response_single = http_single.request(uri, post_single) 30 | expect(response_single.code).to eql("200") 31 | expect(response_single.body).to eql(%({"channel": "#{channel}#{i + j}", "published_messages": 1, "stored_messages": 1, "subscribers": 0}\r\n)) 32 | 33 | post_double = Net::HTTP::Post.new "/pub?id=#{channel}#{i + j}/#{channel}#{i}_#{j}" 34 | post_double.body = body 35 | response_double = http_double.request(uri, post_double) 36 | expect(response_double.code).to eql("200") 37 | expect(response_double.body).to match_the_pattern(/"hostname": "[^"]*", "time": "\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}", "channels": #{(i + j) * 2}, "wildcard_channels": 0, "uptime": [0-9]*, "infos": \[\r\n/) 38 | expect(response_double.body).to match_the_pattern(/"channel": "#{channel}#{i + j}", "published_messages": 2, "stored_messages": 2, "subscribers": 0},\r\n/) 39 | expect(response_double.body).to match_the_pattern(/"channel": "#{channel}#{i}_#{j}", "published_messages": 1, "stored_messages": 1, "subscribers": 0}\r\n/) 40 | end 41 | end 42 | end 43 | end 44 | 45 | it "should create many channels on the same socket without info on response" do 46 | channel = 'ch_test_create_many_channels_' 47 | body = 'channel started' 48 | channels_to_be_created = 4000 49 | 50 | nginx_run_server(config.merge({:channel_info_on_publish => "off"}), :timeout => 25) do |conf| 51 | uri = URI.parse nginx_address 52 | 0.step(channels_to_be_created - 1, 500) do |i| 53 | http = Net::HTTP::Persistent.new 54 | 1.upto(500) do |j| 55 | post = Net::HTTP::Post.new "/pub?id=#{channel}#{i + j}" 56 | post.body = body 57 | response = http.request(uri, post) 58 | expect(response.code).to eql("200") 59 | expect(response.body).to eql("") 60 | end 61 | end 62 | end 63 | end 64 | 65 | it "should execute different operations using the same socket" do 66 | channel = 'ch_test_different_operation_with_keepalive' 67 | content = 'message to be sent' 68 | 69 | nginx_run_server(config) do |conf| 70 | socket = open_socket(nginx_host, nginx_port) 71 | 72 | headers, body = get_in_socket("/pub", socket) 73 | expect(body).to eql("") 74 | expect(headers).to include("No channel id provided.") 75 | 76 | headers, body = post_in_socket("/pub?id=#{channel}", content, socket, {:wait_for => "}\r\n"}) 77 | expect(body).to eql("{\"channel\": \"#{channel}\", \"published_messages\": 1, \"stored_messages\": 1, \"subscribers\": 0}\r\n") 78 | 79 | headers, body = get_in_socket("/channels-stats", socket) 80 | 81 | expect(body).to match_the_pattern(/"channels": 1, "wildcard_channels": 0, "published_messages": 1, "stored_messages": 1, "messages_in_trash": 0, "channels_in_delete": 0, "channels_in_trash": 0, "subscribers": 0, "uptime": [0-9]*, "by_worker": \[\r\n/) 82 | expect(body).to match_the_pattern(/\{"pid": "[0-9]*", "subscribers": 0, "uptime": [0-9]*\}/) 83 | 84 | socket.print("DELETE /pub?id=#{channel}_1 HTTP/1.1\r\nHost: test\r\n\r\n") 85 | headers, body = read_response_on_socket(socket) 86 | expect(headers).to include("HTTP/1.1 404 Not Found") 87 | 88 | headers, body = get_in_socket("/channels-stats?id=ALL", socket) 89 | 90 | expect(body).to match_the_pattern(/"hostname": "[^"]*", "time": "\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}", "channels": 1, "wildcard_channels": 0, "uptime": [0-9]*, "infos": \[\r\n/) 91 | expect(body).to match_the_pattern(/"channel": "#{channel}", "published_messages": 1, "stored_messages": 1, "subscribers": 0}\r\n/) 92 | 93 | headers, body = get_in_socket("/pub?id=#{channel}", socket) 94 | expect(body).to eql("{\"channel\": \"#{channel}\", \"published_messages\": 1, \"stored_messages\": 1, \"subscribers\": 0}\r\n") 95 | 96 | headers, body = post_in_socket("/pub?id=#{channel}/broad_#{channel}", content, socket, {:wait_for => "}\r\n"}) 97 | expect(body).to match_the_pattern(/"hostname": "[^"]*", "time": "\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}", "channels": 1, "wildcard_channels": 1, "uptime": [0-9]*, "infos": \[\r\n/) 98 | expect(body).to match_the_pattern(/"channel": "#{channel}", "published_messages": 2, "stored_messages": 2, "subscribers": 0},\r\n/) 99 | expect(body).to match_the_pattern(/"channel": "broad_#{channel}", "published_messages": 1, "stored_messages": 1, "subscribers": 0}\r\n/) 100 | 101 | headers, body = get_in_socket("/channels-stats?id=#{channel}", socket) 102 | expect(body).to match_the_pattern(/{"channel": "#{channel}", "published_messages": 2, "stored_messages": 2, "subscribers": 0}\r\n/) 103 | 104 | socket.print("DELETE /pub?id=#{channel} HTTP/1.1\r\nHost: test\r\n\r\n") 105 | headers, body = read_response_on_socket(socket) 106 | expect(headers).to include("X-Nginx-PushStream-Explain: Channel deleted.") 107 | 108 | socket.close 109 | end 110 | end 111 | 112 | it "should accept subscribe many times using the same socket" do 113 | channel = 'ch_test_subscribe_with_keepalive' 114 | body_prefix = 'message to be sent' 115 | get_messages = "GET /sub/#{channel} HTTP/1.1\r\nHost: test\r\n\r\n" 116 | 117 | nginx_run_server(config.merge(:store_messages => 'off', :subscriber_mode => 'long-polling'), :timeout => 5) do |conf| 118 | socket = open_socket(nginx_host, nginx_port) 119 | socket_pub = open_socket(nginx_host, nginx_port) 120 | 121 | 1.upto(500) do |j| 122 | socket.print(get_messages) 123 | post_in_socket("/pub?id=#{channel}", "#{body_prefix} #{j.to_s.rjust(3, '0')}", socket_pub, {:wait_for => "}\r\n"}) 124 | headers, body = read_response_on_socket(socket, "\r\n0\r\n\r\n") 125 | expect(body).to eql("16\r\nmessage to be sent #{j.to_s.rjust(3, '0')}\r\n0\r\n\r\n") 126 | end 127 | 128 | socket.close 129 | socket_pub.close 130 | end 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /misc/spec/mix/measure_memory_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Measure Memory" do 4 | let(:config) do 5 | { 6 | :shared_memory_size => "2m", 7 | :message_ttl => "60m", 8 | :max_messages_stored_per_channel => nil, 9 | :keepalive_requests => 15000, 10 | :header_template => nil, 11 | :message_template => nil, 12 | :footer_template => nil, 13 | :ping_message_interval => nil 14 | } 15 | end 16 | 17 | message_estimate_size = 168 18 | channel_estimate_size = 270 19 | subscriber_estimate_size = 400 20 | subscriber_estimate_system_size = 8384 21 | 22 | it "should check message size" do 23 | channel = 'ch_test_message_size' 24 | body = '1' 25 | 26 | nginx_run_server(config, :timeout => 30) do |conf| 27 | shared_size = conf.shared_memory_size.to_i * 1024 * 1024 28 | 29 | post_channel_message = "POST /pub?id=#{channel} HTTP/1.1\r\nHost: localhost\r\nContent-Length: #{body.size}\r\n\r\n#{body}" 30 | socket = open_socket(nginx_host, nginx_port) 31 | 32 | while (true) do 33 | socket.print(post_channel_message) 34 | resp_headers, resp_body = read_response_on_socket(socket, "}\r\n") 35 | break unless resp_headers.match(/200 OK/) 36 | end 37 | socket.close 38 | 39 | EventMachine.run do 40 | pub_2 = EventMachine::HttpRequest.new(nginx_address + '/channels-stats').get 41 | pub_2.callback do 42 | expect(pub_2).to be_http_status(200).with_body 43 | 44 | resp = JSON.parse(pub_2.response) 45 | expected_message = shared_size / (message_estimate_size + body.size) 46 | expect(resp["published_messages"].to_i).to be_within(80).of(expected_message) 47 | EventMachine.stop 48 | end 49 | end 50 | end 51 | end 52 | 53 | it "should check channel size" do 54 | body = '1' 55 | 56 | nginx_run_server(config, :timeout => 150) do |conf| 57 | shared_size = conf.shared_memory_size.to_i * 1024 * 1024 58 | 59 | socket = open_socket(nginx_host, nginx_port) 60 | 61 | channel = 1000 62 | while (true) do 63 | post_channel_message = "POST /pub?id=#{channel} HTTP/1.1\r\nHost: localhost\r\nContent-Length: #{body.size}\r\n\r\n#{body}" 64 | socket.print(post_channel_message) 65 | resp_headers, resp_body = read_response_on_socket(socket, "}\r\n") 66 | break unless resp_headers.match(/200 OK/) 67 | channel += 1 68 | end 69 | socket.close 70 | 71 | EventMachine.run do 72 | pub_2 = EventMachine::HttpRequest.new(nginx_address + '/channels-stats').get 73 | pub_2.callback do 74 | expect(pub_2).to be_http_status(200).with_body 75 | 76 | resp = JSON.parse(pub_2.response) 77 | expected_channel = (shared_size - ((body.size + message_estimate_size) * resp["published_messages"].to_i)) / (channel_estimate_size + 4) # 4 channel id size 78 | expect(resp["channels"].to_i).to be_within(10).of(expected_channel) 79 | EventMachine.stop 80 | end 81 | end 82 | end 83 | end 84 | 85 | it "should check subscriber size" do 86 | nginx_run_server(config.merge({:shared_memory_size => "128k", :header_template => "H"})) do |conf| 87 | shared_size = conf.shared_memory_size.to_i * 1024 #shm size is in kbytes for this test 88 | 89 | EventMachine.run do 90 | subscriber_in_loop(1000, headers) do 91 | pub_2 = EventMachine::HttpRequest.new(nginx_address + '/channels-stats').get :head => headers 92 | pub_2.callback do 93 | expect(pub_2).to be_http_status(200).with_body 94 | 95 | resp = JSON.parse(pub_2.response) 96 | expected_subscriber = (shared_size - ((channel_estimate_size + 4) * resp["channels"].to_i)) / subscriber_estimate_size # 4 channel id size 97 | expect(resp["subscribers"].to_i).to be_within(10).of(expected_subscriber) 98 | EventMachine.stop 99 | end 100 | end 101 | end 102 | end 103 | end 104 | 105 | it "should check subscriber system size" do 106 | channel = 'ch_test_subscriber_system_size' 107 | 108 | nginx_run_server(config.merge({:header_template => "H", :master_process => 'off', :daemon => 'off'}), :timeout => 15) do |conf| 109 | #warming up 110 | EventMachine.run do 111 | sub = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_i.to_s).get :head => headers 112 | sub.stream do |chunk| 113 | EventMachine.stop 114 | end 115 | end 116 | 117 | per_subscriber = 0 118 | EventMachine.run do 119 | memory_1 = `ps -o rss= -p #{File.read conf.pid_file}`.split(' ')[0].to_i 120 | subscriber_in_loop_with_limit(channel, headers, 1000, 1099) do 121 | sleep(1) 122 | memory_2 = `ps -o rss= -p #{File.read conf.pid_file}`.split(' ')[0].to_i 123 | 124 | per_subscriber = ((memory_2 - memory_1).to_f / 100) * 1000 125 | 126 | EventMachine.stop 127 | end 128 | end 129 | 130 | expect(per_subscriber).to be_within(100).of(subscriber_estimate_system_size) 131 | end 132 | end 133 | end 134 | 135 | def subscriber_in_loop(channel, headers, &block) 136 | called = false 137 | sub = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_i.to_s).get :head => headers 138 | sub.stream do |chunk| 139 | next if called 140 | called = true 141 | subscriber_in_loop(channel.to_i + 1, headers, &block) 142 | end 143 | sub.callback do 144 | block.call 145 | end 146 | end 147 | 148 | def subscriber_in_loop_with_limit(channel, headers, start, limit, &block) 149 | called = false 150 | sub = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_i.to_s).get :head => headers 151 | sub.stream do |chunk| 152 | if start == limit 153 | block.call 154 | else 155 | next if called 156 | called = true 157 | subscriber_in_loop_with_limit(channel, headers, start + 1, limit, &block) 158 | end 159 | end 160 | sub.callback do 161 | block.call 162 | end 163 | end 164 | -------------------------------------------------------------------------------- /misc/spec/mix/send_signals_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Send Signals" do 4 | old_cld_trap = nil 5 | before do 6 | old_cld_trap = Signal.trap("CLD", "IGNORE") 7 | end 8 | 9 | after do 10 | Signal.trap("CLD", old_cld_trap) 11 | end 12 | 13 | let(:config) do 14 | { 15 | :master_process => 'on', 16 | :daemon => 'on', 17 | :workers => 1, 18 | :header_template => 'HEADER', 19 | :footer_template => 'FOOTER', 20 | :message_ttl => '60s', 21 | :subscriber_connection_ttl => '65s' 22 | } 23 | end 24 | 25 | it "should disconnect subscribers when receives TERM signal" do 26 | channel = 'ch_test_send_term_signal' 27 | body = 'body' 28 | response = '' 29 | 30 | nginx_run_server(config, :timeout => 5) do |conf| 31 | EventMachine.run do 32 | sub_1 = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s).get :head => headers.merge('X-Nginx-PushStream-Mode' => 'long-polling') 33 | sub_1.callback do 34 | expect(sub_1).to be_http_status(304).without_body 35 | expect(Time.parse(sub_1.response_header['LAST_MODIFIED'].to_s).utc.to_i).to be_in_the_interval(Time.now.utc.to_i-1, Time.now.utc.to_i) 36 | expect(sub_1.response_header['ETAG'].to_s).to eql("W/0") 37 | end 38 | 39 | sub_2 = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s).get :head => headers 40 | sub_2.stream do |chunk| 41 | # send stop signal 42 | `#{ nginx_executable } -c #{ conf.configuration_filename } -s stop > /dev/null 2>&1` 43 | response += chunk 44 | end 45 | sub_2.callback do 46 | expect(response).to include("FOOTER") 47 | EventMachine.stop 48 | end 49 | end 50 | end 51 | end 52 | 53 | 54 | it "should reload normaly when receives HUP signal" do 55 | channel = 'ch_test_send_hup_signal' 56 | body = 'body' 57 | response = response2 = '' 58 | pid = pid2 = 0 59 | open_sockets_1 = 0 60 | socket = nil 61 | 62 | nginx_run_server(config, :timeout => 60) do |conf| 63 | error_log_pre = File.readlines(conf.error_log) 64 | 65 | EventMachine.run do 66 | # create subscriber 67 | sub_1 = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s).get :head => headers 68 | sub_1.stream do |chunk| 69 | response = response + chunk 70 | if response.strip == conf.header_template 71 | # check statistics 72 | pub_1 = EventMachine::HttpRequest.new(nginx_address + '/channels-stats').get :head => headers 73 | pub_1.callback do 74 | expect(pub_1).to be_http_status(200).with_body 75 | resp_1 = JSON.parse(pub_1.response) 76 | expect(resp_1.has_key?("channels")).to be_truthy 77 | expect(resp_1["channels"].to_i).to eql(1) 78 | expect(resp_1["by_worker"].count).to eql(1) 79 | pid = resp_1["by_worker"][0]['pid'].to_i 80 | 81 | open_sockets_1 = `lsof -p #{Process.getpgid pid} | grep socket | wc -l`.strip 82 | 83 | socket = open_socket(nginx_host, nginx_port) 84 | socket.print "GET /sub/#{channel} HTTP/1.1\r\nHost: test\r\nX-Nginx-PushStream-Mode: long-polling\r\n\r\n" 85 | 86 | # send reload signal 87 | `#{ nginx_executable } -c #{ conf.configuration_filename } -s reload > /dev/null 2>&1` 88 | end 89 | end 90 | end 91 | 92 | # check if first worker die 93 | timer = EM.add_periodic_timer(0.5) do 94 | 95 | # check statistics again 96 | pub_4 = EventMachine::HttpRequest.new(nginx_address + '/channels-stats').get :head => headers 97 | pub_4.callback do 98 | resp_3 = JSON.parse(pub_4.response) 99 | expect(resp_3.has_key?("by_worker")).to be_truthy 100 | 101 | old_process_running = Process.getpgid(pid) rescue false 102 | if !old_process_running && (resp_3["by_worker"].count == 1) && (pid != resp_3["by_worker"][0]['pid'].to_i) 103 | timer.cancel 104 | 105 | # publish a message 106 | pub_2 = EventMachine::HttpRequest.new(nginx_address + '/pub?id=' + channel.to_s).post :head => headers, :body => body 107 | pub_2.callback do 108 | # add new subscriber 109 | sub_2 = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s + '.b1').get :head => headers 110 | sub_2.stream do |chunk| 111 | response2 = response2 + chunk 112 | if response2.strip == conf.header_template 113 | # check statistics again 114 | pub_3 = EventMachine::HttpRequest.new(nginx_address + '/channels-stats').get :head => headers 115 | pub_3.callback do 116 | 117 | resp_2 = JSON.parse(pub_3.response) 118 | expect(resp_2.has_key?("channels")).to be_truthy 119 | expect(resp_2["channels"].to_i).to eql(1) 120 | expect(resp_2["published_messages"].to_i).to eql(1) 121 | expect(resp_2["subscribers"].to_i).to eql(1) 122 | 123 | open_sockets_2 = `lsof -p #{Process.getpgid resp_3["by_worker"][0]['pid'].to_i} | grep socket | wc -l`.strip 124 | expect(open_sockets_2).to eql(open_sockets_1) 125 | 126 | EventMachine.stop 127 | 128 | # send stop signal 129 | `#{ nginx_executable } -c #{ conf.configuration_filename } -s stop > /dev/null 2>&1` 130 | 131 | error_log_pos = File.readlines(conf.error_log) 132 | expect((error_log_pos - error_log_pre).join).not_to include("open socket") 133 | socket.close unless socket.nil? 134 | end 135 | end 136 | end 137 | end 138 | end 139 | end 140 | end 141 | end 142 | end 143 | end 144 | 145 | shared_examples_for "reload server" do 146 | it "should reload fast" do 147 | channel = 'ch_test_send_hup_signal' 148 | pid = pid2 = 0 149 | 150 | nginx_run_server(config.merge(custom_config), :timeout => 5) do |conf| 151 | EventMachine.run do 152 | # create subscriber 153 | sub_1 = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s).get :head => headers 154 | sub_1.stream do |chunk| 155 | end 156 | 157 | EM.add_timer(1) do 158 | # check statistics 159 | pub_1 = EventMachine::HttpRequest.new(nginx_address + '/channels-stats').get :head => headers 160 | pub_1.callback do 161 | expect(pub_1).to be_http_status(200).with_body 162 | resp_1 = JSON.parse(pub_1.response) 163 | expect(resp_1["subscribers"].to_i).to eql(1) 164 | expect(resp_1["channels"].to_i).to eql(1) 165 | expect(resp_1["by_worker"].count).to eql(1) 166 | pid = resp_1["by_worker"][0]['pid'].to_i 167 | 168 | # send reload signal 169 | `#{ nginx_executable } -c #{ conf.configuration_filename } -s reload > /dev/null 2>&1` 170 | 171 | # check if first worker die 172 | EM.add_periodic_timer(1) do 173 | 174 | # check statistics 175 | pub_4 = EventMachine::HttpRequest.new(nginx_address + '/channels-stats').get :head => headers 176 | pub_4.callback do 177 | resp_3 = JSON.parse(pub_4.response) 178 | expect(resp_3.has_key?("by_worker")).to be_truthy 179 | 180 | if resp_3["by_worker"].count == 1 181 | expect(resp_3["subscribers"].to_i).to eql(0) 182 | expect(resp_3["channels"].to_i).to eql(1) 183 | pid2 = resp_3["by_worker"][0]['pid'].to_i 184 | 185 | expect(pid).not_to eql(pid2) 186 | EventMachine.stop 187 | end 188 | end 189 | end 190 | end 191 | end 192 | end 193 | end 194 | end 195 | end 196 | 197 | context "with a big ping message interval" do 198 | let(:custom_config) do 199 | { 200 | :ping_message_interval => "10m", 201 | :subscriber_connection_ttl => '10s' 202 | } 203 | end 204 | 205 | it_should_behave_like "reload server" 206 | end 207 | 208 | context "with a big subscriber connection ttl" do 209 | let(:custom_config) do 210 | { 211 | :ping_message_interval => "1s", 212 | :subscriber_connection_ttl => '10m' 213 | } 214 | end 215 | 216 | it_should_behave_like "reload server" 217 | end 218 | 219 | it "should ignore changes on shared memory size when doing a reload" do 220 | channel = 'ch_test_reload_with_different_shared_memory_size' 221 | body = 'body' 222 | response = response2 = '' 223 | pid = pid2 = 0 224 | 225 | nginx_run_server(config, :timeout => 10) do |conf| 226 | EventMachine.run do 227 | publish_message(channel, {}, body) 228 | # check statistics 229 | pub_1 = EventMachine::HttpRequest.new(nginx_address + '/channels-stats').get :head => headers 230 | pub_1.callback do 231 | expect(pub_1).to be_http_status(200).with_body 232 | resp_1 = JSON.parse(pub_1.response) 233 | expect(resp_1.has_key?("channels")).to be_truthy 234 | expect(resp_1["channels"].to_i).to eql(1) 235 | expect(resp_1["published_messages"].to_i).to eql(1) 236 | 237 | conf.configuration[:shared_memory_size] = '20m' 238 | conf.create_configuration_file 239 | 240 | # send reload signal 241 | `#{ nginx_executable } -c #{ conf.configuration_filename } -s reload > /dev/null 2>&1` 242 | 243 | sleep 5 244 | 245 | pub_2 = EventMachine::HttpRequest.new(nginx_address + '/channels-stats').get :head => headers 246 | pub_2.callback do 247 | expect(pub_2).to be_http_status(200).with_body 248 | resp_2 = JSON.parse(pub_2.response) 249 | expect(resp_2.has_key?("channels")).to be_truthy 250 | expect(resp_2["channels"].to_i).to eql(1) 251 | expect(resp_2["published_messages"].to_i).to eql(1) 252 | 253 | error_log = File.read(conf.error_log) 254 | expect(error_log).to include("Cannot change memory area size without restart, ignoring change") 255 | 256 | EventMachine.stop 257 | end 258 | end 259 | end 260 | end 261 | end 262 | end 263 | -------------------------------------------------------------------------------- /misc/spec/mix/setup_parameters_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Setup Parameters" do 4 | it "should not accept '0' as ping message interval" do 5 | expect(nginx_test_configuration({:ping_message_interval => 0})).to include("push_stream_ping_message_interval cannot be zero") 6 | end 7 | 8 | it "should not accept a blank message template" do 9 | expect(nginx_test_configuration({:message_template => ""})).to include("push_stream_message_template cannot be blank") 10 | end 11 | 12 | it "should not accept '0' as subscriber connection ttl" do 13 | expect(nginx_test_configuration({:subscriber_connection_ttl => 0})).to include("push_stream_subscriber_connection_ttl cannot be zero") 14 | end 15 | 16 | it "should not accept '0' as long polling subscriber connection ttl" do 17 | expect(nginx_test_configuration({:longpolling_connection_ttl => 0})).to include("push_stream_longpolling_connection_ttl cannot be zero") 18 | end 19 | 20 | it "should not accept '0' as max channel id length" do 21 | expect(nginx_test_configuration({:max_channel_id_length => 0})).to include("push_stream_max_channel_id_length cannot be zero") 22 | end 23 | 24 | it "should not accept '0' as message ttl" do 25 | expect(nginx_test_configuration({:message_ttl => 0})).to include("push_stream_message_ttl cannot be zero") 26 | end 27 | 28 | it "should not accept '0' as max subscribers per channel" do 29 | expect(nginx_test_configuration({:max_subscribers_per_channel => 0})).to include("push_stream_max_subscribers_per_channel cannot be zero") 30 | end 31 | 32 | it "should not accept '0' as max messages stored per channel" do 33 | expect(nginx_test_configuration({:max_messages_stored_per_channel => 0})).to include("push_stream_max_messages_stored_per_channel cannot be zero") 34 | end 35 | 36 | it "should not accept '0' as max number of channels" do 37 | expect(nginx_test_configuration({:max_number_of_channels => 0})).to include("push_stream_max_number_of_channels cannot be zero") 38 | end 39 | 40 | it "should not accept '0' as max number of wildcard channels" do 41 | expect(nginx_test_configuration({:max_number_of_wildcard_channels => 0})).to include("push_stream_max_number_of_wildcard_channels cannot be zero") 42 | end 43 | 44 | it "should not accept '0' as max wildcard channels" do 45 | expect(nginx_test_configuration({:wildcard_channel_max_qtd => 0})).to include("push_stream_wildcard_channel_max_qtd cannot be zero") 46 | end 47 | 48 | it "should not set max wildcard channels without set boadcast channel prefix" do 49 | expect(nginx_test_configuration({:wildcard_channel_max_qtd => 1, :wildcard_channel_prefix => ""})).to include("cannot set wildcard channel max qtd if push_stream_wildcard_channel_prefix is not set or blank") 50 | end 51 | 52 | it "should not accept '0' as max number of wildcard channels" do 53 | config = {:max_number_of_wildcard_channels => 3, :wildcard_channel_max_qtd => 4, :wildcard_channel_prefix => "broad_"} 54 | expect(nginx_test_configuration(config)).to include("max number of wildcard channels cannot be smaller than value in push_stream_wildcard_channel_max_qtd") 55 | end 56 | 57 | it "should accept a configuration without http block" do 58 | config = { 59 | :configuration_template => %q{ 60 | pid <%= pid_file %>; 61 | error_log <%= error_log %> debug; 62 | # Development Mode 63 | master_process off; 64 | daemon off; 65 | worker_processes <%= nginx_workers %>; 66 | 67 | events { 68 | worker_connections 1024; 69 | use <%= (RUBY_PLATFORM =~ /darwin/) ? 'kqueue' : 'epoll' %>; 70 | } 71 | } 72 | } 73 | expect(nginx_test_configuration(config)).to include("ngx_http_push_stream_module will not be used with this configuration.") 74 | end 75 | 76 | it "should not accept an invalid push mode" do 77 | expect(nginx_test_configuration({:subscriber_mode => "unknown"})).to include("invalid push_stream_subscriber mode value: unknown, accepted values (streaming, polling, long-polling, eventsource, websocket)") 78 | end 79 | 80 | it "should accept the known push modes" do 81 | expect(nginx_test_configuration({:subscriber_mode => ""})).not_to include("invalid push_stream_subscriber mode value") 82 | expect(nginx_test_configuration({:subscriber_mode => "streaming"})).not_to include("invalid push_stream_subscriber mode value") 83 | expect(nginx_test_configuration({:subscriber_mode => "polling"})).not_to include("invalid push_stream_subscriber mode value") 84 | expect(nginx_test_configuration({:subscriber_mode => "long-polling"})).not_to include("invalid push_stream_subscriber mode value") 85 | expect(nginx_test_configuration({:subscriber_mode => "eventsource"})).not_to include("invalid push_stream_subscriber mode value") 86 | expect(nginx_test_configuration({:subscriber_mode => "websocket"})).not_to include("invalid push_stream_subscriber mode value") 87 | end 88 | 89 | it "should not accept an invalid publisher mode" do 90 | expect(nginx_test_configuration({:publisher_mode => "unknown"})).to include("invalid push_stream_publisher mode value: unknown, accepted values (normal, admin)") 91 | end 92 | 93 | it "should accept the known publisher modes" do 94 | expect(nginx_test_configuration({:publisher_mode => ""})).not_to include("invalid push_stream_publisher mode value") 95 | expect(nginx_test_configuration({:publisher_mode => "normal"})).not_to include("invalid push_stream_publisher mode value") 96 | expect(nginx_test_configuration({:publisher_mode => "admin"})).not_to include("invalid push_stream_publisher mode value") 97 | end 98 | 99 | it "should not accept an invalid pattern for padding by user agent" do 100 | expect(nginx_test_configuration({:padding_by_user_agent => "user_agent,as,df"})).to include("padding pattern not match the value user_agent,as,df") 101 | expect(nginx_test_configuration({:padding_by_user_agent => "user_agent;10;0"})).to include("padding pattern not match the value user_agent;10;0") 102 | expect(nginx_test_configuration({:padding_by_user_agent => "user_agent,10,0:other_user_agent;20;0:another_user_agent,30,0"})).to include("error applying padding pattern to other_user_agent;20;0:another_user_agent,30,0") 103 | end 104 | 105 | it "should not accept an invalid shared memory size" do 106 | expect(nginx_test_configuration({:shared_memory_size => nil})).to include("push_stream_shared_memory_size must be set.") 107 | end 108 | 109 | it "should not accept a small shared memory size" do 110 | expect(nginx_test_configuration({:shared_memory_size => "100k"})).to include("The push_stream_shared_memory_size value must be at least") 111 | end 112 | 113 | it "should not accept an invalid channels path value" do 114 | expect(nginx_test_configuration({:channels_path => nil})).to include("push stream module: push_stream_channels_path must be set.") 115 | expect(nginx_test_configuration({:channels_path_for_pub => nil})).to include("push stream module: push_stream_channels_path must be set.") 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /misc/spec/mix/wildcard_properties_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Wildcard Properties" do 4 | let(:config) do 5 | { 6 | :authorized_channels_only => "on", 7 | :header_template => 'connected', 8 | :wildcard_channel_prefix => "XXX_" 9 | } 10 | end 11 | 12 | it "should identify wildcard channels by prefix" do 13 | channel = 'ch_test_wildcard_channel_prefix' 14 | channel_broad = 'XXX_123' 15 | channel_broad_fail = 'YYY_123' 16 | 17 | body = 'wildcard channel prefix' 18 | 19 | nginx_run_server(config) do |conf| 20 | EventMachine.run do 21 | pub = EventMachine::HttpRequest.new(nginx_address + '/pub?id=' + channel.to_s ).post :head => headers, :body => body 22 | pub.callback do 23 | sub_1 = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s + '/' + channel_broad_fail).get :head => headers 24 | sub_1.callback do |chunk| 25 | expect(sub_1).to be_http_status(403).without_body 26 | 27 | sub_2 = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s + '/' + channel_broad).get :head => headers 28 | sub_2.stream do |chunk2| 29 | expect(chunk2).to eql(conf.header_template) 30 | EventMachine.stop 31 | end 32 | end 33 | end 34 | end 35 | end 36 | end 37 | 38 | it "should limit the number of wildcard channels in the same request" do 39 | channel = 'ch_test_wildcard_channel_max_qtd' 40 | channel_broad1 = 'XXX_123' 41 | channel_broad2 = 'XXX_321' 42 | channel_broad3 = 'XXX_213' 43 | body = 'wildcard channel prefix' 44 | 45 | nginx_run_server(config.merge(:wildcard_channel_max_qtd => 2)) do |conf| 46 | EventMachine.run do 47 | pub = EventMachine::HttpRequest.new(nginx_address + '/pub?id=' + channel.to_s ).post :head => headers, :body => body 48 | pub.callback do 49 | sub_1 = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s + '/' + channel_broad1 + '/' + channel_broad2 + '/' + channel_broad3).get :head => headers 50 | sub_1.callback do |chunk| 51 | expect(sub_1).to be_http_status(403).without_body 52 | sub_2 = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s + '/' + channel_broad1 + '/' + channel_broad2).get :head => headers 53 | sub_2.stream do 54 | EventMachine.stop 55 | end 56 | end 57 | end 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /misc/spec/nginx_configuration.rb: -------------------------------------------------------------------------------- 1 | module NginxConfiguration 2 | def self.default_configuration 3 | { 4 | :disable_start_stop_server => false, 5 | :master_process => 'on', 6 | :daemon => 'on', 7 | :workers => 2, 8 | 9 | :gzip => 'off', 10 | 11 | :content_type => 'text/html', 12 | 13 | :keepalive_requests => nil, 14 | :ping_message_interval => '10s', 15 | :header_template_file => nil, 16 | :header_template => %{\\r\\n\\r\\n\\r\\n\\r\\n\\r\\n\\r\\n}, 17 | :message_template => "", 18 | :footer_template => "", 19 | 20 | :store_messages => 'on', 21 | 22 | :subscriber_connection_ttl => nil, 23 | :longpolling_connection_ttl => nil, 24 | :timeout_with_body => 'off', 25 | :message_ttl => '50m', 26 | 27 | :max_channel_id_length => 200, 28 | :max_subscribers_per_channel => nil, 29 | :max_messages_stored_per_channel => 20, 30 | :max_number_of_channels => nil, 31 | :max_number_of_wildcard_channels => nil, 32 | 33 | :wildcard_channel_max_qtd => 3, 34 | :wildcard_channel_prefix => 'broad_', 35 | 36 | :subscriber_mode => nil, 37 | :publisher_mode => nil, 38 | :padding_by_user_agent => nil, 39 | 40 | :shared_memory_size => '10m', 41 | 42 | :channel_deleted_message_text => nil, 43 | :ping_message_text => nil, 44 | :last_received_message_time => nil, 45 | :last_received_message_tag => nil, 46 | :last_event_id => nil, 47 | :user_agent => nil, 48 | 49 | :authorized_channels_only => 'off', 50 | :allowed_origins => nil, 51 | 52 | :client_max_body_size => '32k', 53 | :client_body_buffer_size => '32k', 54 | 55 | :channel_info_on_publish => "on", 56 | :channel_inactivity_time => nil, 57 | 58 | :channel_id => '$arg_id', 59 | :channels_path_for_pub => '$arg_id', 60 | :channels_path => '$1', 61 | 62 | :events_channel_id => nil, 63 | :allow_connections_to_events_channel => nil, 64 | 65 | :extra_location => '', 66 | :extra_configuration => '' 67 | } 68 | end 69 | 70 | 71 | def self.template_configuration 72 | %( 73 | pid <%= pid_file %>; 74 | error_log <%= error_log %> info; 75 | 76 | # Development Mode 77 | master_process <%= master_process %>; 78 | daemon <%= daemon %>; 79 | worker_processes <%= workers %>; 80 | worker_rlimit_core 2500M; 81 | working_directory <%= File.join(nginx_tests_tmp_dir, "cores", config_id) %>; 82 | debug_points abort; 83 | 84 | events { 85 | worker_connections 256; 86 | use <%= (RUBY_PLATFORM =~ /darwin/) ? 'kqueue' : 'epoll' %>; 87 | } 88 | 89 | http { 90 | default_type application/octet-stream; 91 | 92 | access_log <%= access_log %>; 93 | 94 | 95 | gzip <%= gzip %>; 96 | gzip_buffers 16 4k; 97 | gzip_proxied any; 98 | gzip_types text/plain text/css application/x-javascript text/xml application/xml application/xml+rss text/javascript application/json; 99 | gzip_comp_level 9; 100 | gzip_http_version 1.0; 101 | 102 | tcp_nopush on; 103 | tcp_nodelay on; 104 | keepalive_timeout 100; 105 | <%= write_directive("keepalive_requests", keepalive_requests) %> 106 | send_timeout 10; 107 | client_body_timeout 10; 108 | client_header_timeout 10; 109 | sendfile on; 110 | client_header_buffer_size 1k; 111 | large_client_header_buffers 2 4k; 112 | client_max_body_size 1k; 113 | client_body_buffer_size 1k; 114 | ignore_invalid_headers on; 115 | client_body_in_single_buffer on; 116 | client_body_temp_path <%= client_body_temp %>; 117 | 118 | <%= write_directive("push_stream_ping_message_interval", ping_message_interval, "ping frequency") %> 119 | 120 | <%= write_directive("push_stream_message_template", message_template, "message template") %> 121 | 122 | <%= write_directive("push_stream_subscriber_connection_ttl", subscriber_connection_ttl, "timeout for subscriber connections") %> 123 | <%= write_directive("push_stream_longpolling_connection_ttl", longpolling_connection_ttl, "timeout for long polling connections") %> 124 | <%= write_directive("push_stream_timeout_with_body", timeout_with_body) %> 125 | <%= write_directive("push_stream_header_template", header_template, "header to be sent when receiving new subscriber connection") %> 126 | <%= write_directive("push_stream_header_template_file", header_template_file, "file with the header to be sent when receiving new subscriber connection") %> 127 | <%= write_directive("push_stream_message_ttl", message_ttl, "message ttl") %> 128 | <%= write_directive("push_stream_footer_template", footer_template, "footer to be sent when finishing subscriber connection") %> 129 | 130 | <%= write_directive("push_stream_max_channel_id_length", max_channel_id_length) %> 131 | <%= write_directive("push_stream_max_subscribers_per_channel", max_subscribers_per_channel, "max subscribers per channel") %> 132 | <%= write_directive("push_stream_max_messages_stored_per_channel", max_messages_stored_per_channel, "max messages to store in memory") %> 133 | <%= write_directive("push_stream_max_number_of_channels", max_number_of_channels) %> 134 | <%= write_directive("push_stream_max_number_of_wildcard_channels", max_number_of_wildcard_channels) %> 135 | 136 | <%= write_directive("push_stream_wildcard_channel_max_qtd", wildcard_channel_max_qtd) %> 137 | <%= write_directive("push_stream_wildcard_channel_prefix", wildcard_channel_prefix) %> 138 | 139 | <%= write_directive("push_stream_padding_by_user_agent", padding_by_user_agent) %> 140 | 141 | <%= write_directive("push_stream_authorized_channels_only", authorized_channels_only, "subscriber may create channels on demand or only authorized (publisher) may do it?") %> 142 | 143 | <%= write_directive("push_stream_shared_memory_size", shared_memory_size) %> 144 | 145 | <%= write_directive("push_stream_user_agent", user_agent) %> 146 | 147 | <%= write_directive("push_stream_allowed_origins", allowed_origins) %> 148 | 149 | <%= write_directive("push_stream_last_received_message_time", last_received_message_time) %> 150 | <%= write_directive("push_stream_last_received_message_tag", last_received_message_tag) %> 151 | <%= write_directive("push_stream_last_event_id", last_event_id) %> 152 | 153 | <%= write_directive("push_stream_channel_deleted_message_text", channel_deleted_message_text) %> 154 | 155 | <%= write_directive("push_stream_ping_message_text", ping_message_text) %> 156 | <%= write_directive("push_stream_channel_inactivity_time", channel_inactivity_time) %> 157 | 158 | <%= write_directive("push_stream_events_channel_id", events_channel_id) %> 159 | <%= write_directive("push_stream_allow_connections_to_events_channel", allow_connections_to_events_channel) %> 160 | 161 | server { 162 | listen <%= nginx_port %>; 163 | server_name <%= nginx_host %>; 164 | 165 | location /channels-stats { 166 | # activate channels statistics mode for this location 167 | push_stream_channels_statistics; 168 | 169 | <%= write_directive("push_stream_channels_path", channels_path_for_pub) %> 170 | } 171 | 172 | location /pub { 173 | # activate publisher mode for this location 174 | push_stream_publisher <%= publisher_mode unless publisher_mode.nil? || publisher_mode == "normal" %>; 175 | 176 | <%= write_directive("push_stream_channels_path", channels_path_for_pub) %> 177 | <%= write_directive("push_stream_store_messages", store_messages, "store messages") %> 178 | <%= write_directive("push_stream_channel_info_on_publish", channel_info_on_publish, "channel_info_on_publish") %> 179 | 180 | # client_max_body_size MUST be equal to client_body_buffer_size or 181 | # you will be sorry. 182 | client_max_body_size <%= client_max_body_size %>; 183 | client_body_buffer_size <%= client_body_buffer_size %>; 184 | } 185 | 186 | location ~ /sub/(.*)? { 187 | # activate subscriber mode for this location 188 | push_stream_subscriber <%= subscriber_mode unless subscriber_mode.nil? || subscriber_mode == "streaming" %>; 189 | 190 | # positional channel path 191 | <%= write_directive("push_stream_channels_path", channels_path) %> 192 | <%= write_directive("default_type", content_type, "content-type") %> 193 | } 194 | 195 | <%= extra_location %> 196 | } 197 | } 198 | 199 | <%= extra_configuration %> 200 | ) 201 | end 202 | end 203 | -------------------------------------------------------------------------------- /misc/spec/publisher/channel_id_collision_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Publisher Channel id collision" do 4 | 5 | it "should create and retrieve channels with ids that collide" do 6 | channels = ["A", "plumless", "buckeroo", "B", "fc0591", "123rainerbommert", "C", "a1sellers", "advertees", "D"] 7 | 8 | nginx_run_server do |conf| 9 | channels.each do |channel| 10 | EventMachine.run do 11 | pub = EventMachine::HttpRequest.new(nginx_address + '/pub?id=' + channel).post :body => 'x' 12 | pub.callback do 13 | expect(pub).to be_http_status(200) 14 | EventMachine.stop 15 | end 16 | end 17 | end 18 | 19 | channels.each do |channel| 20 | EventMachine.run do 21 | pub = EventMachine::HttpRequest.new(nginx_address + '/channels-stats?id=' + channel).get :timeout => 30 22 | pub.callback do 23 | expect(pub).to be_http_status(200) 24 | EventMachine.stop 25 | end 26 | end 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /misc/spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | 3 | # Set up gems listed in the Gemfile. 4 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', File.dirname(__FILE__)) 5 | 6 | require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) 7 | Bundler.require(:default, :test) if defined?(Bundler) 8 | 9 | require File.expand_path('nginx_configuration', File.dirname(__FILE__)) 10 | 11 | Signal.trap("CLD", "IGNORE") 12 | 13 | RSpec.configure do |config| 14 | config.after(:each) do 15 | non_time_wait_connections = `netstat -an | grep ":#{nginx_port} " | grep -v TIME_WAIT | grep -v LISTEN | grep -v ESTABLISHED`.chomp.split("\n") 16 | fail "There are sockects on non time wait state: #{non_time_wait_connections.join("\n")}" if non_time_wait_connections.count > 0 17 | NginxTestHelper::Config.delete_config_and_log_files(config_id) if has_passed? 18 | end 19 | config.order = "random" 20 | end 21 | 22 | def publish_message_inline(channel, headers, body, delay=0.01, &block) 23 | EM.add_timer(delay) do 24 | pub = EventMachine::HttpRequest.new(nginx_address + '/pub?id=' + channel.to_s).post :head => headers, :body => body 25 | pub.callback do 26 | expect(pub).to be_http_status(200) 27 | block.call(pub) unless block.nil? 28 | end 29 | end 30 | end 31 | 32 | def publish_message(channel, headers, body) 33 | http = Net::HTTP.new(nginx_host, nginx_port) 34 | req = Net::HTTP::Post.new("/pub?id=#{channel}", headers) 35 | req.body = body 36 | res = http.request(req) 37 | content = res.body 38 | if res.get_fields("content-encoding").to_a.include?("gzip") 39 | content = Zlib::GzipReader.new(StringIO.new(content)).read 40 | end 41 | response = JSON.parse(content) 42 | expect(response["channel"].to_s).to eql(channel) 43 | end 44 | 45 | def post_to(path, headers, body) 46 | http = Net::HTTP.new(nginx_host, nginx_port) 47 | req = Net::HTTP::Post.new(path, headers) 48 | req.body = body 49 | http.request(req) 50 | end 51 | 52 | def create_channel_by_subscribe(channel, headers, timeout=60, &block) 53 | EventMachine.run do 54 | sub_1 = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s, :connect_timeout => timeout, :inactivity_timeout => timeout).get :head => headers.merge({"accept-encoding" => ""}) 55 | sub_1.stream do |chunk| 56 | block.call 57 | end 58 | 59 | sub_1.callback do 60 | EventMachine.stop 61 | end 62 | end 63 | end 64 | 65 | def publish_messages_until_fill_the_memory(channel, body, &block) 66 | i = 0 67 | resp_headers, resp_body = nil 68 | socket = open_socket(nginx_host, nginx_port) 69 | loop do 70 | socket.print("POST /pub?id=#{(channel.to_s % (i)).gsub(' ', '%20')} HTTP/1.1\r\nHost: localhost\r\nContent-Length: #{body.size}\r\n\r\n#{body}") 71 | resp_headers, resp_body = read_response_on_socket(socket, "}\r\n") 72 | break unless resp_headers.match(/200 OK/) 73 | i += 1 74 | end 75 | socket.close 76 | 77 | status = resp_headers.match(/HTTP[^ ]* ([^ ]*)/)[1] 78 | block.call(status, resp_body) unless block.nil? 79 | end 80 | -------------------------------------------------------------------------------- /misc/spec/subscriber/comunication_properties_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Comunication Properties" do 4 | let(:config) do 5 | { 6 | :authorized_channels_only => "off", 7 | :header_template => "connected", 8 | :message_ttl => "12s", 9 | :message_template => "~text~", 10 | :ping_message_interval => "1s" 11 | } 12 | end 13 | 14 | it "should not block to connected to a nonexistent channel" do 15 | channel = 'ch_test_all_authorized' 16 | 17 | nginx_run_server(config) do |conf| 18 | EventMachine.run do 19 | sub = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s).get :head => headers 20 | sub.stream do |chunk| 21 | expect(chunk).to eql(conf.header_template) 22 | EventMachine.stop 23 | end 24 | end 25 | end 26 | end 27 | 28 | it "should block to connected to a nonexistent channel when authorized only is 'on'" do 29 | channel = 'ch_test_only_authorized' 30 | body = 'message to create a channel' 31 | 32 | nginx_run_server(config.merge(:authorized_channels_only => "on")) do |conf| 33 | EventMachine.run do 34 | sub_1 = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s).get :head => headers 35 | sub_1.callback do |chunk| 36 | expect(sub_1).to be_http_status(403).without_body 37 | expect(sub_1.response_header['X_NGINX_PUSHSTREAM_EXPLAIN']).to eql("Subscriber could not create channels.") 38 | 39 | pub = EventMachine::HttpRequest.new(nginx_address + '/pub?id=' + channel.to_s ).post :head => headers, :body => body 40 | pub.callback do 41 | sub_2 = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s).get :head => headers 42 | sub_2.stream do |chunk2| 43 | expect(chunk2).to eql(conf.header_template) 44 | EventMachine.stop 45 | end 46 | end 47 | end 48 | end 49 | end 50 | end 51 | 52 | it "should discard messages published a more time than the value configured to message ttl" do 53 | channel = 'ch_test_message_ttl' 54 | body = 'message to test buffer timeout ' 55 | response_1 = response_2 = response_3 = "" 56 | sub_1 = sub_2 = sub_3 = nil 57 | 58 | nginx_run_server(config, :timeout => 20) do |conf| 59 | EventMachine.run do 60 | pub = EventMachine::HttpRequest.new(nginx_address + '/pub?id=' + channel.to_s ).post :head => headers, :body => body 61 | time_2 = EM.add_timer(2) do 62 | sub_1 = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s + '.b1').get :head => headers 63 | sub_1.stream do |chunk| 64 | response_1 += chunk unless response_1.include?(body) 65 | sub_1.close if response_1.include?(body) 66 | end 67 | end 68 | 69 | EM.add_timer(6) do 70 | sub_2 = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s + '.b1').get :head => headers 71 | sub_2.stream do |chunk| 72 | response_2 += chunk unless response_2.include?(body) 73 | sub_2.close if response_2.include?(body) 74 | end 75 | end 76 | 77 | #message will be certainly expired at 15 seconds 78 | EM.add_timer(16) do 79 | sub_3 = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s + '.b1').get :head => headers 80 | sub_3.stream do |chunk| 81 | response_3 += chunk unless response_3.include?(body) 82 | sub_3.close if response_3.include?(body) 83 | end 84 | end 85 | 86 | EM.add_timer(17) do 87 | expect(response_1).to eql("#{conf.header_template}#{body}") 88 | expect(response_2).to eql("#{conf.header_template}#{body}") 89 | expect(response_3).to eql("#{conf.header_template}") 90 | EventMachine.stop 91 | end 92 | end 93 | end 94 | end 95 | 96 | it "should apply the message template to published message with the available keyworkds" do 97 | channel = 'ch_test_message_template' 98 | body = 'message to create a channel' 99 | 100 | response = "" 101 | nginx_run_server(config.merge(:message_template => '|{\"duplicated\":\"~channel~\", \"channel\":\"~channel~\", \"message\":\"~text~\", \"message_id\":\"~id~\"}')) do |conf| 102 | publish_message(channel, headers, body) 103 | 104 | EventMachine.run do 105 | sub = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s + '.b1').get :head => headers 106 | sub.stream do |chunk| 107 | response += chunk 108 | 109 | lines = response.split("|") 110 | 111 | if lines.length >= 3 112 | expect(lines[0]).to eql("#{conf.header_template}") 113 | expect(lines[1]).to eql("{\"duplicated\":\"#{channel}\", \"channel\":\"#{channel}\", \"message\":\"#{body}\", \"message_id\":\"1\"}") 114 | expect(lines[2]).to eql("{\"duplicated\":\"\", \"channel\":\"\", \"message\":\" \", \"message_id\":\"-1\"}") 115 | EventMachine.stop 116 | end 117 | end 118 | end 119 | end 120 | end 121 | 122 | it "should not be in loop when channel or published message contains one of the keywords" do 123 | channel = 'ch_test_message_and_channel_with_same_pattern_of_the_template~channel~~channel~~channel~~text~~text~~text~' 124 | body = '~channel~~channel~~channel~~text~~text~~text~' 125 | 126 | response = "" 127 | nginx_run_server(config.merge(:message_template => '|{\"channel\":\"~channel~\", \"message\":\"~text~\", \"message_id\":\"~id~\"}')) do |conf| 128 | publish_message(channel, headers, body) 129 | 130 | EventMachine.run do 131 | sub = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s + '.b1').get :head => headers 132 | sub.stream do |chunk| 133 | response += chunk 134 | 135 | lines = response.split("|") 136 | 137 | if lines.length >= 3 138 | expect(lines[0]).to eql("#{conf.header_template}") 139 | expect(lines[1]).to eql("{\"channel\":\"ch_test_message_and_channel_with_same_pattern_of_the_template~channel~~channel~~channel~~text~~text~~text~\", \"message\":\"~channel~~channel~~channel~~text~~text~~text~\", \"message_id\":\"1\"}") 140 | expect(lines[2]).to eql("{\"channel\":\"\", \"message\":\" \", \"message_id\":\"-1\"}") 141 | EventMachine.stop 142 | end 143 | end 144 | end 145 | end 146 | end 147 | end 148 | -------------------------------------------------------------------------------- /misc/spec/subscriber/connection_cleanup_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Subscriber Connection Cleanup" do 4 | let(:config) do 5 | { 6 | :subscriber_connection_ttl => '17s', 7 | :header_template => 'HEADER_TEMPLATE', 8 | :footer_template => 'FOOTER_TEMPLATE', 9 | :ping_message_interval => '3s' 10 | } 11 | end 12 | 13 | it "should disconnect the subscriber after the configured connection ttl be reached" do 14 | channel = 'ch_test_subscriber_connection_timeout' 15 | 16 | nginx_run_server(config.merge(:ping_message_interval => nil), :timeout => 25) do |conf| 17 | start = Time.now 18 | response = '' 19 | 20 | EventMachine.run do 21 | sub = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s, :inactivity_timeout => 20).get :head => headers 22 | 23 | sub.stream do |chunk| 24 | response += chunk 25 | expect(response).to include(conf.header_template) 26 | end 27 | 28 | sub.callback do 29 | stop = Time.now 30 | expect(time_diff_sec(start, stop)).to be_in_the_interval(17, 17.5) 31 | expect(response).to include(conf.footer_template) 32 | EventMachine.stop 33 | end 34 | end 35 | end 36 | end 37 | 38 | it "should disconnect the subscriber after the configured connection ttl be reached with ping message" do 39 | channel = 'ch_test_subscriber_connection_timeout_with_ping_message' 40 | 41 | nginx_run_server(config.merge(:header_template => nil, :footer_template => nil), :timeout => 25) do |conf| 42 | start = Time.now 43 | chunks_received = 0 44 | 45 | EventMachine.run do 46 | sub = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s).get :head => headers 47 | 48 | sub.stream do |chunk| 49 | chunks_received += 1 50 | end 51 | 52 | sub.callback do 53 | stop = Time.now 54 | expect(time_diff_sec(start, stop)).to be_in_the_interval(17, 17.5) 55 | expect(chunks_received).to be_eql(5) 56 | EventMachine.stop 57 | end 58 | end 59 | end 60 | end 61 | 62 | it "should disconnect each subscriber after the configured connection ttl be reached starting when it connects" do 63 | channel = 'ch_test_multiple_subscribers_connection_timeout' 64 | 65 | nginx_run_server(config.merge(:subscriber_connection_ttl => '5s', :ping_message_interval => nil), :timeout => 25) do |conf| 66 | EventMachine.run do 67 | response_1 = '' 68 | sub_1 = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s).get :head => headers 69 | sub_1.stream do |chunk| 70 | response_1 += chunk 71 | expect(response_1).to include(conf.header_template) 72 | end 73 | sub_1.callback do 74 | expect(response_1).to include(conf.footer_template) 75 | end 76 | 77 | sleep(2) 78 | 79 | response_2 = '' 80 | sub_2 = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s).get :head => headers 81 | sub_2.stream do |chunk| 82 | response_2 += chunk 83 | expect(response_2).to include(conf.header_template) 84 | end 85 | sub_2.callback do 86 | expect(response_2).to include(conf.footer_template) 87 | 88 | response_4 = '' 89 | sub_4 = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s).get :head => headers 90 | sub_4.stream do |chunk| 91 | response_4 += chunk 92 | expect(response_4).to include(conf.header_template) 93 | end 94 | sub_4.callback do 95 | expect(response_4).to include(conf.footer_template) 96 | EventMachine.stop 97 | end 98 | end 99 | 100 | sleep(6) 101 | 102 | response_3 = '' 103 | sub_3 = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s).get :head => headers 104 | sub_3.stream do |chunk| 105 | response_3 += chunk 106 | expect(response_3).to include(conf.header_template) 107 | end 108 | sub_3.callback do 109 | expect(response_3).to include(conf.footer_template) 110 | end 111 | 112 | end 113 | end 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /misc/spec/subscriber/padding_by_user_agent_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Subscriber Padding by user agent" do 4 | let(:default_config) do 5 | { 6 | :padding_by_user_agent => "[T|t]est 1,0,508", 7 | :user_agent => nil, 8 | :subscriber_connection_ttl => '1s', 9 | :header_template => nil, 10 | :message_template => nil, 11 | :footer_template => nil 12 | } 13 | end 14 | 15 | shared_examples_for "apply padding" do 16 | it "should apply a padding to the header" do 17 | channel = 'ch_test_header_padding' 18 | 19 | nginx_run_server(config.merge(:header_template => "0123456789", :padding_by_user_agent => "[T|t]est 1,1024,508:[T|t]est 2,4097,0")) do |conf| 20 | EventMachine.run do 21 | expected_size = conf.header_template.size + header_delta 22 | 23 | sub_1 = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s).get :head => headers.merge("User-Agent" => "Test 1") 24 | sub_1.callback do 25 | expect(sub_1).to be_http_status(200) 26 | expect(sub_1.response.size).to eql(1100 + expected_size) 27 | expect(sub_1.response).to match padding_pattern 28 | 29 | sub_2 = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s).get :head => headers.merge("User-Agent" => "Test 2") 30 | sub_2.callback do 31 | expect(sub_2).to be_http_status(200) 32 | expect(sub_2.response.size).to eql(4097 + expected_size) 33 | 34 | sub_3 = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s).get :head => headers.merge("User-Agent" => "Test 3") 35 | sub_3.callback do 36 | expect(sub_3).to be_http_status(200) 37 | expect(sub_3.response.size).to eql(expected_size) 38 | 39 | EventMachine.stop 40 | end 41 | end 42 | end 43 | end 44 | end 45 | end 46 | 47 | it "should apply a padding to the message" do 48 | channel = 'ch_test_message_padding' 49 | 50 | body = "0123456789" 51 | 52 | nginx_run_server(config) do |conf| 53 | EventMachine.run do 54 | expected_size = body.size + header_delta + body_delta 55 | 56 | sub_1 = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s).get :head => headers.merge("User-Agent" => "Test 1") 57 | sub_1.callback { 58 | expect(sub_1).to be_http_status(200) 59 | expect(sub_1.response.size).to eql(500 + expected_size) 60 | expect(sub_1.response).to match padding_pattern 61 | 62 | sub_2 = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s).get :head => headers.merge("User-Agent" => "Test 2") 63 | sub_2.callback { 64 | expect(sub_2).to be_http_status(200) 65 | expect(sub_2.response.size).to eql(expected_size) 66 | 67 | sub_3 = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s).get :head => headers.merge("User-Agent" => "Test 3") 68 | sub_3.callback { 69 | expect(sub_3).to be_http_status(200) 70 | expect(sub_3.response.size).to eql(expected_size) 71 | 72 | EventMachine.stop 73 | } 74 | publish_message_inline(channel, headers, body) 75 | } 76 | publish_message_inline(channel, headers, body) 77 | } 78 | publish_message_inline(channel, headers, body) 79 | end 80 | end 81 | end 82 | 83 | it "should apply a padding to the message with different sizes" do 84 | channel = 'ch_test_message_padding_with_different_sizes' 85 | 86 | nginx_run_server(config.merge(:padding_by_user_agent => "[T|t]est 1,0,545"), :timeout => 10) do |conf| 87 | EventMachine.run do 88 | i = 1 89 | expected_padding = 545 90 | expected_size = header_delta + body_delta 91 | 92 | sub_1 = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s).get :head => headers.merge("User-Agent" => "Test 1") 93 | sub_1.callback do 94 | expect(sub_1).to be_http_status(200) 95 | expect(sub_1.response.size).to eql(expected_padding + i + expected_size) 96 | expect(sub_1.response).to match padding_pattern 97 | 98 | i = 105 99 | expected_padding = 600 - ((i/100).to_i * 100) 100 | 101 | sub_1 = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s).get :head => headers.merge("User-Agent" => "Test 1") 102 | sub_1.callback do 103 | expect(sub_1).to be_http_status(200) 104 | expect(sub_1.response.size).to eql(expected_padding + i + expected_size) 105 | 106 | i = 221 107 | expected_padding = 600 - ((i/100).to_i * 100) 108 | 109 | sub_1 = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s).get :head => headers.merge("User-Agent" => "Test 1") 110 | sub_1.callback do 111 | expect(sub_1).to be_http_status(200) 112 | expect(sub_1.response.size).to eql(expected_padding + i + expected_size) 113 | 114 | i = 331 115 | expected_padding = 600 - ((i/100).to_i * 100) 116 | 117 | sub_1 = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s).get :head => headers.merge("User-Agent" => "Test 1") 118 | sub_1.callback do 119 | expect(sub_1).to be_http_status(200) 120 | expect(sub_1.response.size).to eql(expected_padding + i + expected_size) 121 | 122 | i = 435 123 | expected_padding = 600 - ((i/100).to_i * 100) 124 | 125 | sub_1 = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s).get :head => headers.merge("User-Agent" => "Test 1") 126 | sub_1.callback do 127 | expect(sub_1).to be_http_status(200) 128 | expect(sub_1.response.size).to eql(expected_padding + i + expected_size) 129 | 130 | i = 502 131 | expected_padding = 600 - ((i/100).to_i * 100) 132 | 133 | sub_1 = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s).get :head => headers.merge("User-Agent" => "Test 1") 134 | sub_1.callback do 135 | expect(sub_1).to be_http_status(200) 136 | expect(sub_1.response.size).to eql(expected_padding + i + expected_size) 137 | 138 | i = 550 139 | 140 | sub_1 = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s).get :head => headers.merge("User-Agent" => "Test 1") 141 | sub_1.callback do 142 | expect(sub_1).to be_http_status(200) 143 | expect(sub_1.response.size).to eql(i + expected_size) 144 | 145 | EventMachine.stop 146 | end 147 | publish_message_inline(channel, headers, "_" * i) 148 | end 149 | publish_message_inline(channel, headers, "_" * i) 150 | end 151 | publish_message_inline(channel, headers, "_" * i) 152 | end 153 | publish_message_inline(channel, headers, "_" * i) 154 | end 155 | publish_message_inline(channel, headers, "_" * i) 156 | end 157 | publish_message_inline(channel, headers, "_" * i) 158 | end 159 | publish_message_inline(channel, headers, "_" * i) 160 | end 161 | end 162 | end 163 | 164 | it "should accept the user agent set by a complex value" do 165 | channel = 'ch_test_user_agent_by_complex_value' 166 | 167 | nginx_run_server(config.merge(:padding_by_user_agent => "[T|t]est 1,1024,512", :user_agent => "$arg_ua", :header_template => "0123456789"), :timeout => 10) do |conf| 168 | EventMachine.run do 169 | expected_size = conf.header_template.size + header_delta 170 | 171 | sub_1 = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s + '?ua=test%201').get :head => headers 172 | sub_1.callback do 173 | expect(sub_1).to be_http_status(200) 174 | expect(sub_1.response.size).to eql(1024 + expected_size) 175 | expect(sub_1.response).to match padding_pattern 176 | 177 | sub_2 = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s + '?ua=test%202').get :head => headers 178 | sub_2.callback do 179 | expect(sub_2).to be_http_status(200) 180 | expect(sub_2.response.size).to eql(expected_size) 181 | 182 | EventMachine.stop 183 | end 184 | end 185 | end 186 | end 187 | end 188 | end 189 | 190 | describe "for non EventSource mode" do 191 | let(:config) { default_config } 192 | let(:padding_pattern) { /(\r\n)+\r\n\r\n\r\n$/ } 193 | let(:header_delta) { 0 } 194 | let(:body_delta) { 0 } 195 | 196 | it_should_behave_like "apply padding" 197 | end 198 | 199 | describe "for EventSource mode" do 200 | let(:config) { default_config.merge(:subscriber_mode => "eventsource") } 201 | let(:padding_pattern) { /(:::)+\n$/ } 202 | let(:header_delta) { 3 } 203 | let(:body_delta) { 8 } 204 | 205 | it_should_behave_like "apply padding" 206 | end 207 | end 208 | -------------------------------------------------------------------------------- /misc/spec/subscriber/polling_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Subscriber Properties" do 4 | 5 | shared_examples_for "polling location" do 6 | 7 | describe "when has no messages" do 8 | 9 | it "should receive a 304" do 10 | channel = 'ch_test_receive_a_304_when_has_no_messages' 11 | 12 | nginx_run_server(config) do |conf| 13 | EventMachine.run do 14 | sub_1 = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s).get :head => headers 15 | sub_1.callback do 16 | expect(sub_1).to be_http_status(304).without_body 17 | expect(sub_1.response_header['LAST_MODIFIED'].to_s).to eql("") 18 | expect(sub_1.response_header['ETAG'].to_s).to eql("") 19 | EventMachine.stop 20 | end 21 | end 22 | end 23 | end 24 | 25 | it "should receive a 304 keeping sent headers" do 26 | channel = 'ch_test_receive_a_304_when_has_no_messages_keeping_headers' 27 | 28 | sent_headers = headers.merge({'If-Modified-Since' => Time.now.utc.strftime("%a, %d %b %Y %T %Z"), 'If-None-Match' => 'W/3'}) 29 | nginx_run_server(config) do |conf| 30 | EventMachine.run do 31 | sub_1 = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s).get :head => sent_headers 32 | sub_1.callback do 33 | expect(sub_1).to be_http_status(304).without_body 34 | expect(Time.parse(sub_1.response_header['LAST_MODIFIED'].to_s)).to eql(Time.parse(sent_headers['If-Modified-Since'])) 35 | expect(sub_1.response_header['ETAG'].to_s).to eql(sent_headers['If-None-Match']) 36 | EventMachine.stop 37 | end 38 | end 39 | end 40 | end 41 | 42 | end 43 | 44 | describe "when has messages" do 45 | 46 | it "should receive specific headers" do 47 | channel = 'ch_test_receive_specific_headers_when_has_messages' 48 | body = 'body' 49 | 50 | nginx_run_server(config) do |conf| 51 | EventMachine.run do 52 | publish_message(channel, {}, body) 53 | 54 | sub_1 = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s).get :head => headers.merge({'If-Modified-Since' => Time.at(0).utc.strftime("%a, %d %b %Y %T %Z")}) 55 | sub_1.callback do 56 | expect(sub_1).to be_http_status(200) 57 | expect(sub_1.response_header['LAST_MODIFIED'].to_s).not_to eql("") 58 | expect(sub_1.response_header['ETAG'].to_s).to eql("W/1") 59 | expect(sub_1.response).to eql("#{body}") 60 | EventMachine.stop 61 | end 62 | end 63 | end 64 | end 65 | 66 | it "should accept a callback parameter to works with JSONP" do 67 | channel = 'ch_test_return_message_using_function_name_specified_in_callback_parameter_when_polling' 68 | body = 'body' 69 | response = "" 70 | callback_function_name = "callback_function" 71 | 72 | nginx_run_server(config) do |conf| 73 | EventMachine.run do 74 | publish_message(channel, {}, body) 75 | sub_1 = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s + '?callback=' + callback_function_name).get :head => headers.merge({'If-Modified-Since' => Time.at(0).utc.strftime("%a, %d %b %Y %T %Z")}) 76 | sub_1.callback do 77 | expect(sub_1.response).to eql("#{callback_function_name}([#{body}]);") 78 | EventMachine.stop 79 | end 80 | end 81 | end 82 | end 83 | 84 | it "should return old messages using function name specified in callback parameter grouping in one answer" do 85 | channel = 'ch_test_return_old_messages_using_function_name_specified_in_callback_parameter_grouping_in_one_answer' 86 | body = 'body' 87 | response = "" 88 | callback_function_name = "callback_function" 89 | 90 | nginx_run_server(config) do |conf| 91 | EventMachine.run do 92 | publish_message(channel, {'Event-Id' => 'event_id'}, body) 93 | publish_message(channel, {}, body + "1") 94 | 95 | sub_1 = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s + '.b2' + '?callback=' + callback_function_name).get :head => headers 96 | sub_1.callback do 97 | expect(sub_1.response).to eql("#{callback_function_name}([#{body},#{body + "1"}]);") 98 | 99 | sub_2 = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s + '?callback=' + callback_function_name).get :head => headers.merge({'Last-Event-Id' => 'event_id'}) 100 | sub_2.callback do 101 | expect(sub_2.response).to eql("#{callback_function_name}([#{body + "1"}]);") 102 | 103 | sub_3 = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s + '?callback=' + callback_function_name).get :head => headers.merge({'If-Modified-Since' => Time.at(0).utc.strftime("%a, %d %b %Y %T %Z")}) 104 | sub_3.callback do 105 | expect(sub_3.response).to eql("#{callback_function_name}([#{body},#{body + "1"}]);") 106 | 107 | EventMachine.stop 108 | end 109 | end 110 | end 111 | end 112 | end 113 | end 114 | 115 | it "should force content_type to be application/javascript when using function name specified in callback parameter" do 116 | channel = 'test_force_content_type_to_be_application_javascript_when_using_function_name_specified_in_callback_parameter_when_polling' 117 | body = 'body' 118 | response = "" 119 | callback_function_name = "callback_function" 120 | 121 | nginx_run_server(config.merge({:content_type => "anything/value"})) do |conf| 122 | EventMachine.run do 123 | publish_message(channel, {}, body) 124 | sent_headers = headers.merge({'accept' => 'otherknown/value'}) 125 | sub_1 = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s + '?callback=' + callback_function_name).get :head => sent_headers 126 | sub_1.callback do 127 | expect(sub_1.response_header['CONTENT_TYPE']).to eql('application/javascript') 128 | EventMachine.stop 129 | end 130 | end 131 | end 132 | end 133 | 134 | it "should accept return content gzipped" do 135 | channel = 'ch_test_get_content_gzipped' 136 | body = 'body' 137 | actual_response = '' 138 | 139 | nginx_run_server(config.merge({:gzip => "on"})) do |conf| 140 | EventMachine.run do 141 | publish_message(channel, {}, body) 142 | 143 | sent_headers = headers.merge({'accept-encoding' => 'gzip, compressed', 'If-Modified-Since' => Time.at(0).utc.strftime("%a, %d %b %Y %T %Z")}) 144 | sub_1 = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s).get :head => sent_headers, :decoding => false 145 | sub_1.stream do |chunk| 146 | actual_response << chunk 147 | end 148 | sub_1.callback do 149 | expect(sub_1).to be_http_status(200) 150 | 151 | expect(sub_1.response_header["ETAG"]).to match(/W\/\d+/) 152 | expect(sub_1.response_header["CONTENT_ENCODING"]).to eql("gzip") 153 | actual_response = Zlib::GzipReader.new(StringIO.new(actual_response)).read 154 | 155 | expect(actual_response).to eql("#{body}") 156 | EventMachine.stop 157 | end 158 | end 159 | end 160 | end 161 | end 162 | 163 | it "should not cache the response" do 164 | channel = 'ch_test_not_cache_the_response' 165 | 166 | nginx_run_server(config) do |conf| 167 | EventMachine.run do 168 | sub_1 = EventMachine::HttpRequest.new(nginx_address + '/sub/' + channel.to_s).get :head => headers 169 | sub_1.callback do 170 | expect(sub_1.response_header["EXPIRES"]).to eql("Thu, 01 Jan 1970 00:00:01 GMT") 171 | expect(sub_1.response_header["CACHE_CONTROL"]).to eql("no-cache, no-store, must-revalidate") 172 | EventMachine.stop 173 | end 174 | end 175 | end 176 | end 177 | end 178 | 179 | context "when using subscriber push mode config" do 180 | let(:config) do 181 | { 182 | :ping_message_interval => nil, 183 | :header_template => nil, 184 | :footer_template => nil, 185 | :message_template => nil, 186 | :subscriber_mode => 'polling' 187 | } 188 | end 189 | 190 | let(:headers) do 191 | {'accept' => 'text/html'} 192 | end 193 | 194 | it_should_behave_like "polling location" 195 | end 196 | 197 | context "when using push mode header" do 198 | let(:config) do 199 | { 200 | :ping_message_interval => nil, 201 | :header_template => nil, 202 | :footer_template => nil, 203 | :message_template => nil, 204 | :subscriber_mode => nil 205 | } 206 | end 207 | 208 | let(:headers) do 209 | {'accept' => 'text/html', 'X-Nginx-PushStream-Mode' => 'polling'} 210 | end 211 | 212 | it_should_behave_like "polling location" 213 | end 214 | end 215 | -------------------------------------------------------------------------------- /misc/tools/Makefile: -------------------------------------------------------------------------------- 1 | all: publisher subscriber 2 | 3 | subscriber: subscriber.o util.o 4 | gcc -g -Oo subscriber.o util.o -o subscriber -largtable2 5 | 6 | publisher: publisher.o util.o 7 | gcc -g -Oo publisher.o util.o -o publisher -largtable2 8 | 9 | subscriber.o: subscriber.c 10 | gcc -g -c subscriber.c 11 | 12 | publisher.o: publisher.c 13 | gcc -g -c publisher.c 14 | 15 | util.o: util.c 16 | gcc -g -c util.c 17 | 18 | clean: 19 | rm -rf *o publisher subscriber 20 | -------------------------------------------------------------------------------- /misc/tools/README: -------------------------------------------------------------------------------- 1 | Copyright (C) 2011 Michael Costello, Wandenberg Peixoto 2 | 3 | These tools, publisher and subscriber, were developed only to do some load tests on push stream module. 4 | Their use is very restricted and is not intended to cover all possible configuration for the module. 5 | The first version was developed by Michael Costello and I made some improvements to distribute it. 6 | Feel free to help continuous improvement. 7 | 8 | Any feedbacks will be welcome. 9 | 10 | ============= 11 | Requirements: 12 | ============= 13 | 14 | lib argtable2 15 | GCC, make, the usual guys 16 | epoll event support 17 | 18 | ================ 19 | Developer Guide: 20 | ================ 21 | 22 | The basic configuration used on the load tests are listed bellow. 23 | To compile the tools only execute a make. 24 | 25 | To see all options use: 26 | ./publisher --help 27 | ./subscriber --help 28 | 29 | Pay attention on default values to run your tests. 30 | 31 | ==================== 32 | Basic Configuration: 33 | ==================== 34 | 35 | pid logs/nginx.pid; 36 | error_log logs/nginx-main_error.log debug; 37 | 38 | worker_rlimit_core 500M; 39 | working_directory /tmp/nginx; 40 | 41 | worker_processes 2; 42 | 43 | events { 44 | worker_connections 1024; 45 | use epoll; 46 | } 47 | 48 | http { 49 | include mime.types; 50 | default_type application/octet-stream; 51 | 52 | access_log logs/nginx-http_access.log; 53 | error_log logs/nginx-http_error.log debug; 54 | 55 | push_stream_shared_memory_size 500M; 56 | 57 | server { 58 | listen 9080 default_server; 59 | server_name localhost; 60 | 61 | location /channels-stats { 62 | push_stream_channels_statistics; 63 | push_stream_channels_path $arg_id; 64 | } 65 | 66 | location /pub { 67 | push_stream_publisher admin; 68 | push_stream_channels_path $arg_id; 69 | push_stream_store_messages off; 70 | } 71 | 72 | location ~ /sub/(.*) { 73 | push_stream_subscriber; 74 | push_stream_channels_path $1; 75 | push_stream_message_template "~text~:~id~:~channel~"; 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /misc/tools/subscriber.c: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2011 Michael Costello, Wandenberg Peixoto 3 | 4 | Usage './subscriber --help' to see option 5 | */ 6 | #include 7 | #include "util.h" 8 | 9 | 10 | void subscribe_channels(Connection *connection, Statistics *stats); 11 | void read_response(Connection *connection, Statistics *stats, char *buffer, int buffer_len); 12 | 13 | int 14 | main_program(int num_channels, int num_connections, const char *server_hostname, int server_port, int timeout) 15 | { 16 | struct sockaddr_in server_address; 17 | int main_sd = -1, num_events = 0, i, j, event_mask, channels_per_connection, num, start_time = 0, iters_to_next_summary = 0; 18 | Connection *connections = NULL, *connection; 19 | Statistics stats = {0,0,0,0,0}; 20 | int exitcode = EXIT_SUCCESS; 21 | struct epoll_event events[MAX_EVENTS]; 22 | char buffer[BIG_BUFFER_SIZE]; 23 | 24 | info("Subscriber starting up\n"); 25 | info("Subscriber: %d connections to %d channels on server: %s:%d\n", num_connections, num_channels, server_hostname, server_port); 26 | 27 | if ((fill_server_address(server_hostname, server_port, &server_address)) != 0) { 28 | error2("ERROR host name not found\n"); 29 | } 30 | 31 | if ((main_sd = epoll_create(200 /* this size is not used on Linux kernel 2.6.8+ */)) < 0) { 32 | error3("Failed %d creating main epoll socket\n", errno); 33 | } 34 | 35 | if ((connections = init_connections(num_connections, &server_address, main_sd)) == NULL) { 36 | error2("Failed to create to connections\n"); 37 | } 38 | 39 | stats.requested_connections = num_connections; 40 | 41 | for (i = 0; i < num_connections; i++) { 42 | connections[i].channel_start = 0; 43 | connections[i].channel_end = num_channels - 1; 44 | } 45 | 46 | // infinite loop 47 | debug("Entering Infinite Loop\n"); 48 | 49 | iters_to_next_summary = ITERATIONS_TILL_SUMMARY_PER_TIMEOUT/timeout; 50 | 51 | for(;;) { 52 | if ((num_events = epoll_wait(main_sd, events, MAX_EVENTS, timeout)) < 0) { 53 | error3("epoll_wait failed\n"); 54 | } 55 | 56 | for (i = 0; i < num_events; i++) { 57 | event_mask = events[i].events; 58 | connection = (Connection *)(events[i].data.ptr); 59 | 60 | if (event_mask & EPOLLHUP) { // SERVER HUNG UP 61 | debug("EPOLLHUP\n"); 62 | info("Server hung up on conncetion %d. Reconecting...\n", connection->index); 63 | sleep(1); 64 | stats.connections--; 65 | reopen_connection(connection); 66 | 67 | continue; 68 | } 69 | 70 | if (event_mask & EPOLLERR) { 71 | debug("EPOLLERR\n"); 72 | info("Server returned an error on connection %d. Reconecting...\n", connection->index); 73 | stats.connections--; 74 | reopen_connection(connection); 75 | 76 | continue; 77 | } 78 | 79 | if (event_mask & EPOLLIN) { // READ 80 | debug("----------READ AVAILABLE-------\n"); 81 | 82 | if (connection->state == CONNECTED) { 83 | read_response(connection, &stats, buffer, BIG_BUFFER_SIZE); 84 | } 85 | } 86 | 87 | if (event_mask & EPOLLOUT) { // WRITE 88 | debug("----------WRITE AVAILABLE-------\n"); 89 | 90 | if (start_time == 0) { 91 | start_time = time(NULL); 92 | } 93 | 94 | if (connection->state == CONNECTING) { 95 | connection->state = CONNECTED; 96 | stats.connections++; 97 | debug("Connection opened for index=%d\n", connection->index); 98 | 99 | subscribe_channels(connection, &stats); 100 | 101 | // remove write flag from event 102 | if (change_connection(connection, EPOLLIN | EPOLLHUP) < 0) { 103 | error2("Failed creating socket for connection = %d\n", connection->index); 104 | } 105 | } 106 | } 107 | } 108 | 109 | if ((iters_to_next_summary-- <= 0)) { 110 | iters_to_next_summary = ITERATIONS_TILL_SUMMARY_PER_TIMEOUT/timeout; 111 | summary("Connections=%ld, Messages=%ld BytesRead=%ld Msg/Sec=%0.2f\n", stats.connections, stats.messages, stats.bytes_read, calc_message_per_second(stats.messages, start_time)); 112 | } 113 | 114 | if (stats.connections == 0) { 115 | num = 0; 116 | for (j = 0; j < num_connections; j++) { 117 | if (connections[i].state != CLOSED) { 118 | num++; 119 | break; 120 | } 121 | } 122 | 123 | if (num == 0) { 124 | exitcode = EXIT_SUCCESS; 125 | goto exit; 126 | } 127 | } 128 | } 129 | 130 | exit: 131 | if (connections != NULL) free(connections); 132 | 133 | return exitcode; 134 | } 135 | 136 | 137 | void 138 | subscribe_channels(Connection *connection, Statistics *stats) 139 | { 140 | char buffer[BUFFER_SIZE]; 141 | int len = 0, bytes_written = 0; 142 | long i = 0; 143 | 144 | len = sprintf(buffer, "GET /sub"); 145 | for (i = connection->channel_start; i <= connection->channel_end; i++) { 146 | len += sprintf(buffer + len, "/my_channel_%ld", i); 147 | } 148 | 149 | len += sprintf(buffer + len, "?conn=%d HTTP/1.1\r\nHost: loadtest\r\n\r\n", connection->index); 150 | 151 | if (write_connection(connection, stats, buffer, len) == EXIT_FAILURE) { 152 | stats->connections--; 153 | reopen_connection(connection); 154 | return; 155 | } 156 | } 157 | 158 | 159 | void 160 | read_response(Connection *connection, Statistics *stats, char *buffer, int buffer_len) 161 | { 162 | int bytes_read = 0, bad_count = 0, msg_count = 0, close_count = 0; 163 | 164 | bytes_read = read(connection->sd, buffer, buffer_len - 1); 165 | 166 | if (bytes_read < 0) { 167 | error("Error reading from socket for connection %d\n", connection->index); 168 | stats->connections--; 169 | reopen_connection(connection); 170 | return; 171 | } 172 | 173 | if (bytes_read == 0) { // server disconnected us 174 | // reconnect 175 | info("Server disconnected as requested %d.\n", connection->index); 176 | stats->connections--; 177 | reopen_connection(connection); 178 | return; 179 | } 180 | 181 | stats->bytes_read += bytes_read; 182 | buffer[bytes_read] = '\0'; 183 | debug("Read %d bytes\n", bytes_read); 184 | trace("Read Message: %s\n", buffer); 185 | 186 | bad_count = count_strinstr(buffer, "HTTP/1.1 4"); 187 | bad_count += count_strinstr(buffer, "HTTP/1.1 5"); 188 | 189 | if (bad_count > 0) { 190 | info("Recevied error. Buffer is %s\n", buffer); 191 | stats->connections--; 192 | reopen_connection(connection); 193 | return; 194 | } 195 | 196 | msg_count = count_strinstr(buffer, "**MSG**"); 197 | stats->messages += msg_count; 198 | 199 | if ((close_count = count_strinstr(buffer, "**CLOSE**")) > 0) { 200 | connection->channel_count += close_count; 201 | info("%d Channel(s) has(have) been closed by server.\n", close_count); 202 | if (connection->channel_count >= (connection->channel_end - connection->channel_start + 1)) { 203 | info("Connection %d will be closed \n", connection->index); 204 | close_connection(connection); 205 | stats->connections--; 206 | } 207 | } 208 | } 209 | 210 | 211 | int 212 | main(int argc, char **argv) 213 | { 214 | struct arg_int *channels = arg_int0("c", "channels", "", "define number of channels (default is 1)"); 215 | struct arg_int *subscribers = arg_int0("s", "subscribers", "", "define number of subscribers (default is 1)"); 216 | 217 | struct arg_str *server_name = arg_str0("S", "server", "", "server hostname where messages will be published (default is \"127.0.0.1\")"); 218 | struct arg_int *server_port = arg_int0("P", "port", "", "server port where messages will be published (default is 9080)"); 219 | 220 | struct arg_int *timeout = arg_int0(NULL, "timeout", "", "timeout when waiting events on communication to the server (default is 1000)"); 221 | struct arg_int *verbose = arg_int0("v", "verbose", "", "increase output messages detail (0 (default) - no messages, 1 - info messages, 2 - debug messages, 3 - trace messages"); 222 | 223 | struct arg_lit *help = arg_lit0(NULL, "help", "print this help and exit"); 224 | struct arg_lit *version = arg_lit0(NULL, "version", "print version information and exit"); 225 | struct arg_end *end = arg_end(20); 226 | 227 | void* argtable[] = { channels, subscribers, server_name, server_port, timeout, verbose, help, version, end }; 228 | 229 | const char* progname = "subscriber"; 230 | int nerrors; 231 | int exitcode = EXIT_SUCCESS; 232 | 233 | /* verify the argtable[] entries were allocated sucessfully */ 234 | if (arg_nullcheck(argtable) != 0) { 235 | /* NULL entries were detected, some allocations must have failed */ 236 | printf("%s: insufficient memory\n", progname); 237 | exitcode = EXIT_FAILURE; 238 | goto exit; 239 | } 240 | 241 | /* set any command line default values prior to parsing */ 242 | subscribers->ival[0] = DEFAULT_CONCURRENT_CONN; 243 | channels->ival[0] = DEFAULT_NUM_CHANNELS; 244 | server_name->sval[0] = DEFAULT_SERVER_HOSTNAME; 245 | server_port->ival[0] = DEFAULT_SERVER_PORT; 246 | timeout->ival[0] = DEFAULT_TIMEOUT; 247 | verbose->ival[0] = 0; 248 | 249 | /* Parse the command line as defined by argtable[] */ 250 | nerrors = arg_parse(argc, argv, argtable); 251 | 252 | /* special case: '--help' takes precedence over error reporting */ 253 | if (help->count > 0) { 254 | printf(DESCRIPTION_SUBSCRIBER, progname, VERSION, COPYRIGHT); 255 | printf("Usage: %s", progname); 256 | arg_print_syntax(stdout, argtable, "\n"); 257 | arg_print_glossary(stdout, argtable, " %-25s %s\n"); 258 | exitcode = EXIT_SUCCESS; 259 | goto exit; 260 | } 261 | 262 | /* special case: '--version' takes precedence error reporting */ 263 | if (version->count > 0) { 264 | printf(DESCRIPTION_SUBSCRIBER, progname, VERSION, COPYRIGHT); 265 | exitcode = EXIT_SUCCESS; 266 | goto exit; 267 | } 268 | 269 | /* If the parser returned any errors then display them and exit */ 270 | if (nerrors > 0) { 271 | /* Display the error details contained in the arg_end struct.*/ 272 | arg_print_errors(stdout, end, progname); 273 | printf("Try '%s --help' for more information.\n", progname); 274 | exitcode = EXIT_FAILURE; 275 | goto exit; 276 | } 277 | 278 | verbose_messages = verbose->ival[0]; 279 | 280 | /* normal case: take the command line options at face value */ 281 | exitcode = main_program(channels->ival[0], subscribers->ival[0], server_name->sval[0], server_port->ival[0], timeout->ival[0]); 282 | 283 | exit: 284 | /* deallocate each non-null entry in argtable[] */ 285 | arg_freetable(argtable, sizeof(argtable) / sizeof(argtable[0])); 286 | 287 | return exitcode; 288 | } 289 | -------------------------------------------------------------------------------- /misc/tools/util.c: -------------------------------------------------------------------------------- 1 | #include "util.h" 2 | 3 | 4 | int 5 | fill_server_address(const char *server_hostname, int server_port, struct sockaddr_in *server_address) 6 | { 7 | struct hostent *server = NULL; 8 | if ((server = gethostbyname(server_hostname)) == NULL) { 9 | return EXIT_FAILURE; 10 | } 11 | 12 | bzero((char *) server_address, sizeof(struct sockaddr_in)); 13 | server_address->sin_family = AF_INET; 14 | memcpy((char *) &server_address->sin_addr.s_addr, (const char *) server->h_addr, server->h_length); 15 | server_address->sin_port = htons(server_port); 16 | 17 | return EXIT_SUCCESS; 18 | } 19 | 20 | 21 | Connection * 22 | init_connections(int num_connections, struct sockaddr_in *server_address, int main_sd) 23 | { 24 | Connection *connections; 25 | int i; 26 | 27 | if ((connections = (Connection *) malloc(sizeof(Connection) * num_connections)) == NULL) { 28 | return NULL; 29 | } 30 | 31 | for (i = 0; i < num_connections; ++i) { 32 | connections[i].index = i; 33 | connections[i].server_address = server_address; 34 | connections[i].main_sd = main_sd; 35 | if (open_connection(&connections[i]) != 0) { 36 | error("Opening connection %d\n", i); 37 | return NULL; 38 | } 39 | } 40 | 41 | info("Added %d connections.\n", num_connections); 42 | 43 | return connections; 44 | } 45 | 46 | 47 | int 48 | open_connection(Connection *connection) 49 | { 50 | struct epoll_event anEvent; 51 | int exitcode = EXIT_SUCCESS; 52 | 53 | connection->state = CONNECTING; 54 | connection->channel_id = -1; 55 | connection->content_length = 0; 56 | connection->channel_count = 0; 57 | 58 | if ((connection->sd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { 59 | error3("ERROR %d opening socket for connection %d\n", errno, connection->index); 60 | } 61 | 62 | // // set nonblocking 63 | // int flags = fcntl(connection->sd, F_GETFL, 0); 64 | // fcntl(connection->sd, F_SETFL, flags | O_NONBLOCK); 65 | // 66 | // int rc = connect(connection->sd, (struct sockaddr *) connection->server_address, sizeof(struct sockaddr_in)); 67 | // if ((rc < 0) && (errno != EINPROGRESS)) { 68 | // error3("ERROR connecting to server on connection %d\n", connection->index); 69 | // } 70 | 71 | if (connect(connection->sd, (struct sockaddr *) connection->server_address, sizeof(struct sockaddr_in)) < 0) { 72 | error3("ERROR connecting to server on connection %d\n", connection->index); 73 | } 74 | 75 | debug("Adding connection %d\n", connection->index); 76 | 77 | anEvent.events = EPOLLIN | EPOLLOUT | EPOLLHUP; 78 | anEvent.data.ptr = (void *) connection; 79 | if (epoll_ctl(connection->main_sd, EPOLL_CTL_ADD, connection->sd, &anEvent) < 0) { 80 | error3("ERROR %d Failed creating socket for connection %d\n", errno, connection->index); 81 | } 82 | 83 | debug("Connection opening for index %d\n", connection->index); 84 | 85 | exit: 86 | return exitcode; 87 | } 88 | 89 | 90 | void 91 | close_connection(Connection *connection) 92 | { 93 | connection->state = CLOSED; 94 | close(connection->sd); 95 | } 96 | 97 | 98 | 99 | int 100 | reopen_connection(Connection *connection) 101 | { 102 | close_connection(connection); 103 | return open_connection(connection); 104 | } 105 | 106 | 107 | int 108 | change_connection(Connection *connection, uint32_t events) 109 | { 110 | struct epoll_event anEvent; 111 | anEvent.events = events; 112 | anEvent.data.ptr = (void *) connection; 113 | return epoll_ctl(connection->main_sd, EPOLL_CTL_MOD, connection->sd, &anEvent); 114 | } 115 | 116 | 117 | int 118 | write_connection(Connection *connection, Statistics *stats, char *buffer, int buffer_len) 119 | { 120 | int bytes_written = 0; 121 | 122 | bytes_written = write(connection->sd, buffer, buffer_len); 123 | 124 | if (bytes_written != buffer_len) { 125 | error4("Error %d writing bytes (wrote=%d, wanted=%d) for connection %d\n", errno, bytes_written, buffer_len, connection->index); 126 | return EXIT_FAILURE; 127 | } 128 | stats->bytes_written += bytes_written; 129 | trace("Wrote %s\n", buffer); 130 | 131 | return EXIT_SUCCESS; 132 | } 133 | 134 | 135 | float 136 | calc_message_per_second(int num_messages, int start_time) 137 | { 138 | float ret_val = 0.0; 139 | int now = time(NULL); 140 | int diff = now - start_time; 141 | 142 | if (diff == 0) { 143 | diff = 1; 144 | } 145 | 146 | ret_val = (float) num_messages/diff; 147 | 148 | info("CALC TIME. Messages=%d, Time=%d Avg=%0.2f\n", num_messages, diff, ret_val); 149 | 150 | return ret_val; 151 | } 152 | 153 | 154 | int 155 | count_strinstr(const char *big, const char *little) 156 | { 157 | const char *p; 158 | int count = 0; 159 | size_t lil_len = strlen(little); 160 | 161 | /* you decide what to do here */ 162 | if (lil_len == 0) 163 | return -1; 164 | 165 | p = strstr(big, little); 166 | while (p) { 167 | count++; 168 | p = strstr(p + lil_len, little); 169 | } 170 | return count; 171 | } 172 | 173 | -------------------------------------------------------------------------------- /misc/tools/util.h: -------------------------------------------------------------------------------- 1 | #ifndef _UTIL_H_ 2 | #define _UTIL_H_ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #define error(...) {fprintf(stderr, __VA_ARGS__);} 15 | #define error2(...) {error(__VA_ARGS__); exitcode = EXIT_FAILURE; goto exit;} 16 | #define error3(...) {perror("ERROR");error(__VA_ARGS__); exitcode = EXIT_FAILURE; goto exit;} 17 | #define error4(...) {perror("ERROR");error(__VA_ARGS__);} 18 | 19 | #define info(...) {if (verbose_messages >= 1) {fprintf(stdout, __VA_ARGS__);}} 20 | #define debug(...) {if (verbose_messages >= 2) {fprintf(stdout, __VA_ARGS__);}} 21 | #define trace(...) {if (verbose_messages >= 3) {fprintf(stdout, __VA_ARGS__);}} 22 | 23 | #define summary(...) fprintf(stdout, __VA_ARGS__) 24 | 25 | #define VERSION "0.1" 26 | #define COPYRIGHT "Copyright (C) 2011 Michael Costello, Wandenberg Peixoto " 27 | #define DESCRIPTION_PUBLISHER "'%s' v%s - program to publish messages to test Push Stream Module.\n%s\n" 28 | #define DESCRIPTION_SUBSCRIBER "'%s' v%s - program to subscribe channels to test Push Stream Module.\n%s\n" 29 | 30 | #define DEFAULT_NUM_MESSAGES 1 31 | #define DEFAULT_CONCURRENT_CONN 1 32 | #define DEFAULT_NUM_CHANNELS 1 33 | #define DEFAULT_SERVER_HOSTNAME "127.0.0.1" 34 | #define DEFAULT_SERVER_PORT 9080 35 | #define DEFAULT_TIMEOUT 1000 36 | 37 | #define MAX_EVENTS (60000 * 8) 38 | #define ITERATIONS_TILL_SUMMARY_PER_TIMEOUT 10000 //timeout: 1000 -> summary each 10 seconds 39 | #define BUFFER_SIZE 1024 40 | #define BIG_BUFFER_SIZE 640000 41 | 42 | typedef struct 43 | { 44 | long requested_connections; 45 | long connections; 46 | long messages; 47 | long bytes_written; 48 | long bytes_read; 49 | } Statistics; 50 | 51 | enum State {INIT=0, CONNECTING, CONNECTED, CLOSED}; 52 | 53 | // store per connection state here 54 | typedef struct 55 | { 56 | int index; 57 | int main_sd; 58 | int sd; 59 | int message_count; 60 | int num_messages; 61 | int channel_count; 62 | long channel_id; 63 | long channel_start; 64 | long channel_end; 65 | char content_buffer[BUFFER_SIZE]; 66 | int content_length; 67 | enum State state; 68 | struct sockaddr_in *server_address; 69 | } Connection; 70 | 71 | static int verbose_messages = 0; 72 | 73 | int fill_server_address(const char *server_hostname, int server_port, struct sockaddr_in *server_address); 74 | Connection *init_connections(int count, struct sockaddr_in *server_address, int main_sd); 75 | int open_connection(Connection *connection); 76 | void close_connection(Connection *connection); 77 | int reopen_connection(Connection *connection); 78 | int write_connection(Connection *connection, Statistics *stats, char *buffer, int buffer_len); 79 | int change_connection(Connection *connection, uint32_t events); 80 | float calc_message_per_second(int num_messages, int start_time); 81 | 82 | 83 | #endif /* _UTIL_H_ */ 84 | -------------------------------------------------------------------------------- /src/ngx_http_push_stream_rbtree_util.c: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is distributed under the MIT License. 3 | * 4 | * Copyright (c) 2009 Leo Ponomarev 5 | * 6 | * Permission is hereby granted, free of charge, to any person 7 | * obtaining a copy of this software and associated documentation 8 | * files (the "Software"), to deal in the Software without 9 | * restriction, including without limitation the rights to use, 10 | * copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the 12 | * Software is furnished to do so, subject to the following 13 | * conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be 16 | * included in all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | * OTHER DEALINGS IN THE SOFTWARE. 26 | * 27 | * 28 | * ngx_http_push_stream_rbtree_util.c 29 | * 30 | * Modified: Oct 26, 2010 31 | * Modifications by: Wandenberg Peixoto , Rogério Carvalho Schneider 32 | */ 33 | 34 | #include 35 | 36 | static ngx_http_push_stream_channel_t * 37 | ngx_http_push_stream_find_channel_on_tree(ngx_str_t *id, ngx_log_t *log, ngx_rbtree_t *tree) 38 | { 39 | uint32_t hash; 40 | ngx_rbtree_node_t *node, *sentinel; 41 | ngx_int_t rc; 42 | ngx_http_push_stream_channel_t *channel = NULL; 43 | 44 | hash = ngx_crc32_short(id->data, id->len); 45 | 46 | node = tree->root; 47 | sentinel = tree->sentinel; 48 | 49 | while ((node != NULL) && (node != sentinel)) { 50 | if (hash < node->key) { 51 | node = node->left; 52 | continue; 53 | } 54 | 55 | if (hash > node->key) { 56 | node = node->right; 57 | continue; 58 | } 59 | 60 | /* hash == node->key */ 61 | 62 | channel = (ngx_http_push_stream_channel_t *) node; 63 | 64 | rc = ngx_memn2cmp(id->data, channel->id.data, id->len, channel->id.len); 65 | if (rc == 0) { 66 | return channel; 67 | } 68 | 69 | node = (rc < 0) ? node->left : node->right; 70 | } 71 | 72 | return NULL; 73 | } 74 | 75 | 76 | static ngx_http_push_stream_channel_t * 77 | ngx_http_push_stream_find_channel(ngx_str_t *id, ngx_log_t *log, ngx_http_push_stream_main_conf_t *mcf) 78 | { 79 | ngx_http_push_stream_shm_data_t *data = mcf->shm_data; 80 | ngx_http_push_stream_channel_t *channel = NULL; 81 | 82 | if (id == NULL) { 83 | ngx_log_error(NGX_LOG_ERR, log, 0, "push stream module: tried to find a channel with a null id"); 84 | return NULL; 85 | } 86 | 87 | ngx_shmtx_lock(&data->channels_queue_mutex); 88 | channel = ngx_http_push_stream_find_channel_on_tree(id, log, &data->tree); 89 | ngx_shmtx_unlock(&data->channels_queue_mutex); 90 | 91 | return channel; 92 | } 93 | 94 | 95 | // find a channel by id. if channel not found, make one, insert it, and return that. 96 | static ngx_http_push_stream_channel_t * 97 | ngx_http_push_stream_get_channel(ngx_str_t *id, ngx_log_t *log, ngx_http_push_stream_main_conf_t *mcf) 98 | { 99 | ngx_http_push_stream_shm_data_t *data = mcf->shm_data; 100 | ngx_http_push_stream_channel_t *channel; 101 | ngx_slab_pool_t *shpool = mcf->shpool; 102 | ngx_flag_t is_wildcard_channel = 0; 103 | 104 | if (id == NULL) { 105 | ngx_log_error(NGX_LOG_ERR, log, 0, "push stream module: tried to create a channel with a null id"); 106 | return NULL; 107 | } 108 | 109 | ngx_shmtx_lock(&data->channels_queue_mutex); 110 | 111 | // check again to see if any other worker didn't create the channel 112 | channel = ngx_http_push_stream_find_channel_on_tree(id, log, &data->tree); 113 | if (channel != NULL) { // we found our channel 114 | ngx_shmtx_unlock(&data->channels_queue_mutex); 115 | return channel; 116 | } 117 | 118 | if ((mcf->wildcard_channel_prefix.len > 0) && (ngx_strncmp(id->data, mcf->wildcard_channel_prefix.data, mcf->wildcard_channel_prefix.len) == 0)) { 119 | is_wildcard_channel = 1; 120 | } 121 | 122 | if (((!is_wildcard_channel) && (mcf->max_number_of_channels != NGX_CONF_UNSET_UINT) && (mcf->max_number_of_channels == data->channels)) || 123 | ((is_wildcard_channel) && (mcf->max_number_of_wildcard_channels != NGX_CONF_UNSET_UINT) && (mcf->max_number_of_wildcard_channels == data->wildcard_channels))) { 124 | ngx_shmtx_unlock(&data->channels_queue_mutex); 125 | ngx_log_error(NGX_LOG_ERR, log, 0, "push stream module: number of channels were exceeded"); 126 | return NGX_HTTP_PUSH_STREAM_NUMBER_OF_CHANNELS_EXCEEDED; 127 | } 128 | 129 | if ((channel = ngx_slab_alloc(shpool, sizeof(ngx_http_push_stream_channel_t))) == NULL) { 130 | ngx_shmtx_unlock(&data->channels_queue_mutex); 131 | ngx_log_error(NGX_LOG_ERR, log, 0, "push stream module: unable to allocate memory for new channel"); 132 | return NULL; 133 | } 134 | 135 | if ((channel->id.data = ngx_slab_alloc(shpool, id->len + 1)) == NULL) { 136 | ngx_slab_free(shpool, channel); 137 | ngx_shmtx_unlock(&data->channels_queue_mutex); 138 | ngx_log_error(NGX_LOG_ERR, log, 0, "push stream module: unable to allocate memory for new channel id"); 139 | return NULL; 140 | } 141 | 142 | channel->id.len = id->len; 143 | ngx_memcpy(channel->id.data, id->data, channel->id.len); 144 | channel->id.data[channel->id.len] = '\0'; 145 | 146 | channel->wildcard = is_wildcard_channel; 147 | channel->channel_deleted_message = NULL; 148 | channel->last_message_id = 0; 149 | channel->last_message_time = 0; 150 | channel->last_message_tag = 0; 151 | channel->stored_messages = 0; 152 | channel->subscribers = 0; 153 | channel->deleted = 0; 154 | channel->for_events = ((mcf->events_channel_id.len > 0) && (channel->id.len == mcf->events_channel_id.len) && (ngx_strncmp(channel->id.data, mcf->events_channel_id.data, mcf->events_channel_id.len) == 0)); 155 | channel->expires = ngx_time() + mcf->channel_inactivity_time; 156 | 157 | ngx_queue_init(&channel->message_queue); 158 | ngx_queue_init(&channel->workers_with_subscribers); 159 | 160 | channel->node.key = ngx_crc32_short(channel->id.data, channel->id.len); 161 | ngx_rbtree_insert(&data->tree, &channel->node); 162 | ngx_queue_insert_tail(&data->channels_queue, &channel->queue); 163 | (channel->wildcard) ? data->wildcard_channels++ : data->channels++; 164 | 165 | channel->mutex = &data->channels_mutex[data->mutex_round_robin++ % 10]; 166 | 167 | ngx_shmtx_unlock(&data->channels_queue_mutex); 168 | 169 | ngx_http_push_stream_send_event(mcf, log, channel, &NGX_HTTP_PUSH_STREAM_EVENT_TYPE_CHANNEL_CREATED, NULL); 170 | 171 | return channel; 172 | } 173 | 174 | 175 | static void 176 | ngx_rbtree_generic_insert(ngx_rbtree_node_t *temp, ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel, int (*compare) (const ngx_rbtree_node_t *left, const ngx_rbtree_node_t *right)) 177 | { 178 | ngx_rbtree_node_t **p; 179 | 180 | for (;;) { 181 | if (node->key < temp->key) { 182 | p = &temp->left; 183 | } else if (node->key > temp->key) { 184 | p = &temp->right; 185 | } else { /* node->key == temp->key */ 186 | p = (compare(node, temp) < 0) ? &temp->left : &temp->right; 187 | } 188 | 189 | if (*p == sentinel) { 190 | break; 191 | } 192 | 193 | temp = *p; 194 | } 195 | 196 | *p = node; 197 | node->parent = temp; 198 | node->left = sentinel; 199 | node->right = sentinel; 200 | ngx_rbt_red(node); 201 | } 202 | 203 | 204 | static void 205 | ngx_http_push_stream_rbtree_insert(ngx_rbtree_node_t *temp, ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel) 206 | { 207 | ngx_rbtree_generic_insert(temp, node, sentinel, ngx_http_push_stream_compare_rbtree_node); 208 | } 209 | 210 | 211 | static int 212 | ngx_http_push_stream_compare_rbtree_node(const ngx_rbtree_node_t *v_left, const ngx_rbtree_node_t *v_right) 213 | { 214 | ngx_http_push_stream_channel_t *left = (ngx_http_push_stream_channel_t *) v_left, *right = (ngx_http_push_stream_channel_t *) v_right; 215 | 216 | return ngx_memn2cmp(left->id.data, right->id.data, left->id.len, right->id.len); 217 | } 218 | --------------------------------------------------------------------------------