pubnub 0.1.4 → 0.1.5
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of pubnub might be problematic. Click here for more details.
- data/lib/pubnub.rb +297 -1
- data/lib/{pubnub/pubnub_crypto.rb → pubnub_crypto.rb} +0 -0
- metadata +3 -4
- data/lib/pubnub/pubnub.rb +0 -297
data/lib/pubnub.rb
CHANGED
@@ -1 +1,297 @@
|
|
1
|
-
|
1
|
+
## www.pubnub.com - PubNub realtime push service in the cloud.
|
2
|
+
## http://www.pubnub.com/blog/ruby-push-api - Ruby Push API Blog
|
3
|
+
|
4
|
+
## PubNub Real Time Push APIs and Notifications Framework
|
5
|
+
## Copyright (c) 2010 Stephen Blum
|
6
|
+
## http://www.pubnub.com/
|
7
|
+
|
8
|
+
## -----------------------------------
|
9
|
+
## PubNub 3.1 Real-time Push Cloud API
|
10
|
+
## -----------------------------------
|
11
|
+
|
12
|
+
## including required libraries
|
13
|
+
require 'openssl'
|
14
|
+
require 'base64'
|
15
|
+
require 'open-uri'
|
16
|
+
require 'uri'
|
17
|
+
require 'net/http'
|
18
|
+
require 'json'
|
19
|
+
require 'pp'
|
20
|
+
require 'rubygems'
|
21
|
+
require 'securerandom'
|
22
|
+
require 'digest'
|
23
|
+
require 'pubnub_ruby/pubnub_crypto'
|
24
|
+
require 'eventmachine'
|
25
|
+
require 'em-http'
|
26
|
+
require 'fiber'
|
27
|
+
|
28
|
+
class Pubnub
|
29
|
+
MAX_RETRIES = 3
|
30
|
+
retries=0
|
31
|
+
#**
|
32
|
+
#* Pubnub
|
33
|
+
#*
|
34
|
+
#* Init the Pubnub Client API
|
35
|
+
#*
|
36
|
+
#* @param string publish_key required key to send messages.
|
37
|
+
#* @param string subscribe_key required key to receive messages.
|
38
|
+
#* @param string secret_key required key to sign messages.
|
39
|
+
#* @param string cipher_key required to encrypt messages.
|
40
|
+
#* @param boolean ssl required for 2048 bit encrypted messages.
|
41
|
+
#*
|
42
|
+
def initialize( publish_key, subscribe_key, secret_key, cipher_key, ssl_on )
|
43
|
+
@publish_key = publish_key
|
44
|
+
@subscribe_key = subscribe_key
|
45
|
+
@secret_key = secret_key
|
46
|
+
@cipher_key = cipher_key
|
47
|
+
@ssl = ssl_on
|
48
|
+
@origin = 'pubsub.pubnub.com'
|
49
|
+
|
50
|
+
if @ssl
|
51
|
+
@origin = 'https://' + @origin
|
52
|
+
else
|
53
|
+
@origin = 'http://' + @origin
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
#**
|
58
|
+
#* Publish
|
59
|
+
#*
|
60
|
+
#* Send a message to a channel.
|
61
|
+
#*
|
62
|
+
#* @param array args with channel and message.
|
63
|
+
#* @return array success information.
|
64
|
+
#*
|
65
|
+
def publish(args)
|
66
|
+
## Fail if bad input.
|
67
|
+
if !(args['channel'] && args['message'])
|
68
|
+
puts('Missing Channel or Message')
|
69
|
+
return false
|
70
|
+
end
|
71
|
+
|
72
|
+
## Capture User Input
|
73
|
+
channel = args['channel']
|
74
|
+
message = args['message']
|
75
|
+
|
76
|
+
#encryption of message
|
77
|
+
if @cipher_key.length > 0
|
78
|
+
pc=PubnubCrypto.new(@cipher_key)
|
79
|
+
if message.is_a? Array
|
80
|
+
message=pc.encryptArray(message)
|
81
|
+
else
|
82
|
+
message=pc.encryptObject(message)
|
83
|
+
end
|
84
|
+
else
|
85
|
+
message = args['message'].to_json();
|
86
|
+
end
|
87
|
+
|
88
|
+
## Sign message using HMAC
|
89
|
+
String signature = '0'
|
90
|
+
if @secret_key.length > 0
|
91
|
+
signature = "{@publish_key,@subscribe_key,@secret_key,channel,message}"
|
92
|
+
digest = OpenSSL::Digest.new("sha256")
|
93
|
+
key = [ @secret_key ]
|
94
|
+
hmac = OpenSSL::HMAC.hexdigest(digest, key.pack("H*"), signature)
|
95
|
+
signature = hmac
|
96
|
+
end
|
97
|
+
|
98
|
+
## Send Message
|
99
|
+
return _request([
|
100
|
+
'publish',
|
101
|
+
@publish_key,
|
102
|
+
@subscribe_key,
|
103
|
+
signature,
|
104
|
+
channel,
|
105
|
+
'0',
|
106
|
+
message
|
107
|
+
])
|
108
|
+
end
|
109
|
+
|
110
|
+
#**
|
111
|
+
#* Subscribe
|
112
|
+
#*
|
113
|
+
#* This is NON-BLOCKING.
|
114
|
+
#* Listen for a message on a channel.
|
115
|
+
#*
|
116
|
+
#* @param array args with channel and message.
|
117
|
+
#* @return false on fail, array on success.
|
118
|
+
#*
|
119
|
+
def subscribe(args)
|
120
|
+
## Capture User Input
|
121
|
+
channel = args['channel']
|
122
|
+
callback = args['callback']
|
123
|
+
|
124
|
+
## Fail if missing channel
|
125
|
+
if !channel
|
126
|
+
puts "Missing Channel."
|
127
|
+
return false
|
128
|
+
end
|
129
|
+
|
130
|
+
## Fail if missing callback
|
131
|
+
if !callback
|
132
|
+
puts "Missing Callback."
|
133
|
+
return false
|
134
|
+
end
|
135
|
+
|
136
|
+
## Begin Subscribe
|
137
|
+
loop do
|
138
|
+
begin
|
139
|
+
timetoken = args['timetoken'] ? args['timetoken'] : 0
|
140
|
+
|
141
|
+
## Wait for Message
|
142
|
+
response = _request([
|
143
|
+
'subscribe',
|
144
|
+
@subscribe_key,
|
145
|
+
channel,
|
146
|
+
'0',
|
147
|
+
timetoken.to_s
|
148
|
+
])
|
149
|
+
|
150
|
+
messages = response[0]
|
151
|
+
args['timetoken'] = response[1]
|
152
|
+
|
153
|
+
## If it was a timeout
|
154
|
+
next if !messages.length
|
155
|
+
|
156
|
+
## Run user Callback and Reconnect if user permits.
|
157
|
+
## Capture the message and encrypt it
|
158
|
+
if @cipher_key.length > 0
|
159
|
+
pc = PubnubCrypto.new(@cipher_key)
|
160
|
+
messages.each do |message|
|
161
|
+
if message.is_a? Array
|
162
|
+
message=pc.decryptArray(message)
|
163
|
+
else
|
164
|
+
message=pc.decryptObject(message)
|
165
|
+
end
|
166
|
+
if !callback.call(message)
|
167
|
+
return
|
168
|
+
end
|
169
|
+
end
|
170
|
+
else
|
171
|
+
messages.each do |message|
|
172
|
+
if !callback.call(message)
|
173
|
+
return
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
rescue Timeout::Error
|
178
|
+
rescue
|
179
|
+
sleep(1)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
#**
|
185
|
+
#* History
|
186
|
+
#*
|
187
|
+
#* Load history from a channel.
|
188
|
+
#*
|
189
|
+
#* @param array args with 'channel' and 'limit'.
|
190
|
+
#* @return mixed false on fail, array on success.
|
191
|
+
#*
|
192
|
+
def history(args)
|
193
|
+
## Capture User Input
|
194
|
+
limit = +args['limit'] ? +args['limit'] : 5
|
195
|
+
channel = args['channel']
|
196
|
+
|
197
|
+
## Fail if bad input.
|
198
|
+
if (!channel)
|
199
|
+
puts 'Missing Channel.'
|
200
|
+
return false
|
201
|
+
end
|
202
|
+
|
203
|
+
## Get History
|
204
|
+
response = _request([ 'history', @subscribe_key, channel, '0', limit.to_s])
|
205
|
+
if @cipher_key.length > 0
|
206
|
+
myarr=Array.new()
|
207
|
+
response.each do |message|
|
208
|
+
pc=PubnubCrypto.new(@cipher_key)
|
209
|
+
if message.is_a? Array
|
210
|
+
message=pc.decryptArray(message)
|
211
|
+
else
|
212
|
+
message=pc.decryptObject(message)
|
213
|
+
end
|
214
|
+
myarr.push(message)
|
215
|
+
end
|
216
|
+
return myarr
|
217
|
+
else
|
218
|
+
return response
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
#**
|
223
|
+
#* Time
|
224
|
+
#*
|
225
|
+
#* Timestamp from PubNub Cloud.
|
226
|
+
#*
|
227
|
+
#* @return int timestamp.
|
228
|
+
#*
|
229
|
+
def time()
|
230
|
+
return _request([
|
231
|
+
'time',
|
232
|
+
'0'
|
233
|
+
])[0]
|
234
|
+
end
|
235
|
+
|
236
|
+
#**
|
237
|
+
#* UUID
|
238
|
+
#*
|
239
|
+
#* Unique identifier generation
|
240
|
+
#*
|
241
|
+
#* @return Unique Identifier
|
242
|
+
#*
|
243
|
+
def UUID()
|
244
|
+
uuid=SecureRandom.base64(32).gsub("/","_").gsub(/=+$/,"")
|
245
|
+
end
|
246
|
+
|
247
|
+
private
|
248
|
+
|
249
|
+
#**
|
250
|
+
#* Request URL
|
251
|
+
#*
|
252
|
+
#* @param array request of url directories.
|
253
|
+
#* @return array from JSON response.
|
254
|
+
#*
|
255
|
+
def _request(request)
|
256
|
+
## Construct Request URL
|
257
|
+
url = '/' + request.map{ |bit| bit.split('').map{ |ch|
|
258
|
+
' ~`!@#$%^&*()+=[]\\{}|;\':",./<>?'.index(ch) ?
|
259
|
+
'%' + ch.unpack('H2')[0].to_s.upcase : URI.encode(ch)
|
260
|
+
}.join('') }.join('/')
|
261
|
+
|
262
|
+
url = @origin + url
|
263
|
+
http_response = ''
|
264
|
+
|
265
|
+
EventMachine.run do
|
266
|
+
Fiber.new{
|
267
|
+
http = async_fetch(url)
|
268
|
+
http_response = http.response
|
269
|
+
EventMachine.stop
|
270
|
+
}.resume
|
271
|
+
end
|
272
|
+
JSON.parse(http_response)
|
273
|
+
end
|
274
|
+
|
275
|
+
## Non-blocking IO using EventMachine
|
276
|
+
def async_fetch(url)
|
277
|
+
f = Fiber.current
|
278
|
+
|
279
|
+
request_options = {
|
280
|
+
:timeout => 310, # set request timeout
|
281
|
+
:query => {'V' => '3.1', 'User-Agent' => 'Ruby', 'Accept-Encoding' => 'gzip'}, # set request headers
|
282
|
+
}
|
283
|
+
|
284
|
+
http = EventMachine::HttpRequest.new(url).get request_options
|
285
|
+
http.callback { f.resume(http) }
|
286
|
+
http.errback { f.resume(http) }
|
287
|
+
|
288
|
+
Fiber.yield
|
289
|
+
|
290
|
+
if http.error
|
291
|
+
p [:HTTP_ERROR, http.error]
|
292
|
+
end
|
293
|
+
|
294
|
+
http
|
295
|
+
end
|
296
|
+
|
297
|
+
end
|
File without changes
|
metadata
CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
|
|
5
5
|
segments:
|
6
6
|
- 0
|
7
7
|
- 1
|
8
|
-
-
|
9
|
-
version: 0.1.
|
8
|
+
- 5
|
9
|
+
version: 0.1.5
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Luke Carpenter / PubNub.com
|
@@ -71,8 +71,7 @@ files:
|
|
71
71
|
- examples/subscribe_example.rb
|
72
72
|
- examples/uuid_example.rb
|
73
73
|
- lib/pubnub.rb
|
74
|
-
- lib/
|
75
|
-
- lib/pubnub/pubnub_crypto.rb
|
74
|
+
- lib/pubnub_crypto.rb
|
76
75
|
- tests/unit_test.rb
|
77
76
|
- README
|
78
77
|
has_rdoc: true
|
data/lib/pubnub/pubnub.rb
DELETED
@@ -1,297 +0,0 @@
|
|
1
|
-
## www.pubnub.com - PubNub realtime push service in the cloud.
|
2
|
-
## http://www.pubnub.com/blog/ruby-push-api - Ruby Push API Blog
|
3
|
-
|
4
|
-
## PubNub Real Time Push APIs and Notifications Framework
|
5
|
-
## Copyright (c) 2010 Stephen Blum
|
6
|
-
## http://www.pubnub.com/
|
7
|
-
|
8
|
-
## -----------------------------------
|
9
|
-
## PubNub 3.1 Real-time Push Cloud API
|
10
|
-
## -----------------------------------
|
11
|
-
|
12
|
-
## including required libraries
|
13
|
-
require 'openssl'
|
14
|
-
require 'base64'
|
15
|
-
require 'open-uri'
|
16
|
-
require 'uri'
|
17
|
-
require 'net/http'
|
18
|
-
require 'json'
|
19
|
-
require 'pp'
|
20
|
-
require 'rubygems'
|
21
|
-
require 'securerandom'
|
22
|
-
require 'digest'
|
23
|
-
require 'pubnub_ruby/pubnub_crypto'
|
24
|
-
require 'eventmachine'
|
25
|
-
require 'em-http'
|
26
|
-
require 'fiber'
|
27
|
-
|
28
|
-
class Pubnub
|
29
|
-
MAX_RETRIES = 3
|
30
|
-
retries=0
|
31
|
-
#**
|
32
|
-
#* Pubnub
|
33
|
-
#*
|
34
|
-
#* Init the Pubnub Client API
|
35
|
-
#*
|
36
|
-
#* @param string publish_key required key to send messages.
|
37
|
-
#* @param string subscribe_key required key to receive messages.
|
38
|
-
#* @param string secret_key required key to sign messages.
|
39
|
-
#* @param string cipher_key required to encrypt messages.
|
40
|
-
#* @param boolean ssl required for 2048 bit encrypted messages.
|
41
|
-
#*
|
42
|
-
def initialize( publish_key, subscribe_key, secret_key, cipher_key, ssl_on )
|
43
|
-
@publish_key = publish_key
|
44
|
-
@subscribe_key = subscribe_key
|
45
|
-
@secret_key = secret_key
|
46
|
-
@cipher_key = cipher_key
|
47
|
-
@ssl = ssl_on
|
48
|
-
@origin = 'pubsub.pubnub.com'
|
49
|
-
|
50
|
-
if @ssl
|
51
|
-
@origin = 'https://' + @origin
|
52
|
-
else
|
53
|
-
@origin = 'http://' + @origin
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
#**
|
58
|
-
#* Publish
|
59
|
-
#*
|
60
|
-
#* Send a message to a channel.
|
61
|
-
#*
|
62
|
-
#* @param array args with channel and message.
|
63
|
-
#* @return array success information.
|
64
|
-
#*
|
65
|
-
def publish(args)
|
66
|
-
## Fail if bad input.
|
67
|
-
if !(args['channel'] && args['message'])
|
68
|
-
puts('Missing Channel or Message')
|
69
|
-
return false
|
70
|
-
end
|
71
|
-
|
72
|
-
## Capture User Input
|
73
|
-
channel = args['channel']
|
74
|
-
message = args['message']
|
75
|
-
|
76
|
-
#encryption of message
|
77
|
-
if @cipher_key.length > 0
|
78
|
-
pc=PubnubCrypto.new(@cipher_key)
|
79
|
-
if message.is_a? Array
|
80
|
-
message=pc.encryptArray(message)
|
81
|
-
else
|
82
|
-
message=pc.encryptObject(message)
|
83
|
-
end
|
84
|
-
else
|
85
|
-
message = args['message'].to_json();
|
86
|
-
end
|
87
|
-
|
88
|
-
## Sign message using HMAC
|
89
|
-
String signature = '0'
|
90
|
-
if @secret_key.length > 0
|
91
|
-
signature = "{@publish_key,@subscribe_key,@secret_key,channel,message}"
|
92
|
-
digest = OpenSSL::Digest.new("sha256")
|
93
|
-
key = [ @secret_key ]
|
94
|
-
hmac = OpenSSL::HMAC.hexdigest(digest, key.pack("H*"), signature)
|
95
|
-
signature = hmac
|
96
|
-
end
|
97
|
-
|
98
|
-
## Send Message
|
99
|
-
return _request([
|
100
|
-
'publish',
|
101
|
-
@publish_key,
|
102
|
-
@subscribe_key,
|
103
|
-
signature,
|
104
|
-
channel,
|
105
|
-
'0',
|
106
|
-
message
|
107
|
-
])
|
108
|
-
end
|
109
|
-
|
110
|
-
#**
|
111
|
-
#* Subscribe
|
112
|
-
#*
|
113
|
-
#* This is NON-BLOCKING.
|
114
|
-
#* Listen for a message on a channel.
|
115
|
-
#*
|
116
|
-
#* @param array args with channel and message.
|
117
|
-
#* @return false on fail, array on success.
|
118
|
-
#*
|
119
|
-
def subscribe(args)
|
120
|
-
## Capture User Input
|
121
|
-
channel = args['channel']
|
122
|
-
callback = args['callback']
|
123
|
-
|
124
|
-
## Fail if missing channel
|
125
|
-
if !channel
|
126
|
-
puts "Missing Channel."
|
127
|
-
return false
|
128
|
-
end
|
129
|
-
|
130
|
-
## Fail if missing callback
|
131
|
-
if !callback
|
132
|
-
puts "Missing Callback."
|
133
|
-
return false
|
134
|
-
end
|
135
|
-
|
136
|
-
## Begin Subscribe
|
137
|
-
loop do
|
138
|
-
begin
|
139
|
-
timetoken = args['timetoken'] ? args['timetoken'] : 0
|
140
|
-
|
141
|
-
## Wait for Message
|
142
|
-
response = _request([
|
143
|
-
'subscribe',
|
144
|
-
@subscribe_key,
|
145
|
-
channel,
|
146
|
-
'0',
|
147
|
-
timetoken.to_s
|
148
|
-
])
|
149
|
-
|
150
|
-
messages = response[0]
|
151
|
-
args['timetoken'] = response[1]
|
152
|
-
|
153
|
-
## If it was a timeout
|
154
|
-
next if !messages.length
|
155
|
-
|
156
|
-
## Run user Callback and Reconnect if user permits.
|
157
|
-
## Capture the message and encrypt it
|
158
|
-
if @cipher_key.length > 0
|
159
|
-
pc = PubnubCrypto.new(@cipher_key)
|
160
|
-
messages.each do |message|
|
161
|
-
if message.is_a? Array
|
162
|
-
message=pc.decryptArray(message)
|
163
|
-
else
|
164
|
-
message=pc.decryptObject(message)
|
165
|
-
end
|
166
|
-
if !callback.call(message)
|
167
|
-
return
|
168
|
-
end
|
169
|
-
end
|
170
|
-
else
|
171
|
-
messages.each do |message|
|
172
|
-
if !callback.call(message)
|
173
|
-
return
|
174
|
-
end
|
175
|
-
end
|
176
|
-
end
|
177
|
-
rescue Timeout::Error
|
178
|
-
rescue
|
179
|
-
sleep(1)
|
180
|
-
end
|
181
|
-
end
|
182
|
-
end
|
183
|
-
|
184
|
-
#**
|
185
|
-
#* History
|
186
|
-
#*
|
187
|
-
#* Load history from a channel.
|
188
|
-
#*
|
189
|
-
#* @param array args with 'channel' and 'limit'.
|
190
|
-
#* @return mixed false on fail, array on success.
|
191
|
-
#*
|
192
|
-
def history(args)
|
193
|
-
## Capture User Input
|
194
|
-
limit = +args['limit'] ? +args['limit'] : 5
|
195
|
-
channel = args['channel']
|
196
|
-
|
197
|
-
## Fail if bad input.
|
198
|
-
if (!channel)
|
199
|
-
puts 'Missing Channel.'
|
200
|
-
return false
|
201
|
-
end
|
202
|
-
|
203
|
-
## Get History
|
204
|
-
response = _request([ 'history', @subscribe_key, channel, '0', limit.to_s])
|
205
|
-
if @cipher_key.length > 0
|
206
|
-
myarr=Array.new()
|
207
|
-
response.each do |message|
|
208
|
-
pc=PubnubCrypto.new(@cipher_key)
|
209
|
-
if message.is_a? Array
|
210
|
-
message=pc.decryptArray(message)
|
211
|
-
else
|
212
|
-
message=pc.decryptObject(message)
|
213
|
-
end
|
214
|
-
myarr.push(message)
|
215
|
-
end
|
216
|
-
return myarr
|
217
|
-
else
|
218
|
-
return response
|
219
|
-
end
|
220
|
-
end
|
221
|
-
|
222
|
-
#**
|
223
|
-
#* Time
|
224
|
-
#*
|
225
|
-
#* Timestamp from PubNub Cloud.
|
226
|
-
#*
|
227
|
-
#* @return int timestamp.
|
228
|
-
#*
|
229
|
-
def time()
|
230
|
-
return _request([
|
231
|
-
'time',
|
232
|
-
'0'
|
233
|
-
])[0]
|
234
|
-
end
|
235
|
-
|
236
|
-
#**
|
237
|
-
#* UUID
|
238
|
-
#*
|
239
|
-
#* Unique identifier generation
|
240
|
-
#*
|
241
|
-
#* @return Unique Identifier
|
242
|
-
#*
|
243
|
-
def UUID()
|
244
|
-
uuid=SecureRandom.base64(32).gsub("/","_").gsub(/=+$/,"")
|
245
|
-
end
|
246
|
-
|
247
|
-
private
|
248
|
-
|
249
|
-
#**
|
250
|
-
#* Request URL
|
251
|
-
#*
|
252
|
-
#* @param array request of url directories.
|
253
|
-
#* @return array from JSON response.
|
254
|
-
#*
|
255
|
-
def _request(request)
|
256
|
-
## Construct Request URL
|
257
|
-
url = '/' + request.map{ |bit| bit.split('').map{ |ch|
|
258
|
-
' ~`!@#$%^&*()+=[]\\{}|;\':",./<>?'.index(ch) ?
|
259
|
-
'%' + ch.unpack('H2')[0].to_s.upcase : URI.encode(ch)
|
260
|
-
}.join('') }.join('/')
|
261
|
-
|
262
|
-
url = @origin + url
|
263
|
-
http_response = ''
|
264
|
-
|
265
|
-
EventMachine.run do
|
266
|
-
Fiber.new{
|
267
|
-
http = async_fetch(url)
|
268
|
-
http_response = http.response
|
269
|
-
EventMachine.stop
|
270
|
-
}.resume
|
271
|
-
end
|
272
|
-
JSON.parse(http_response)
|
273
|
-
end
|
274
|
-
|
275
|
-
## Non-blocking IO using EventMachine
|
276
|
-
def async_fetch(url)
|
277
|
-
f = Fiber.current
|
278
|
-
|
279
|
-
request_options = {
|
280
|
-
:timeout => 310, # set request timeout
|
281
|
-
:query => {'V' => '3.1', 'User-Agent' => 'Ruby', 'Accept-Encoding' => 'gzip'}, # set request headers
|
282
|
-
}
|
283
|
-
|
284
|
-
http = EventMachine::HttpRequest.new(url).get request_options
|
285
|
-
http.callback { f.resume(http) }
|
286
|
-
http.errback { f.resume(http) }
|
287
|
-
|
288
|
-
Fiber.yield
|
289
|
-
|
290
|
-
if http.error
|
291
|
-
p [:HTTP_ERROR, http.error]
|
292
|
-
end
|
293
|
-
|
294
|
-
http
|
295
|
-
end
|
296
|
-
|
297
|
-
end
|