Module: Hyperion::WebSocket::RubyFrame
- Defined in:
- lib/hyperion/websocket/frame.rb
Overview
Pure-Ruby fallback used when the C ext is missing. Same public surface as ‘CFrame` so the Parser / Builder façades above don’t need to branch on ‘NATIVE_AVAILABLE` per call. Performance is ~5–10× worse on XOR — fine for a safety net, fine for JRuby interop, NOT recommended for the production hot path.
Class Method Summary collapse
- .build(opcode, payload, fin: true, mask: false, mask_key: nil, rsv1: false) ⇒ Object
- .parse(buf, offset = 0) ⇒ Object
- .unmask(payload, key) ⇒ Object
Class Method Details
.build(opcode, payload, fin: true, mask: false, mask_key: nil, rsv1: false) ⇒ Object
305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 |
# File 'lib/hyperion/websocket/frame.rb', line 305 def build(opcode, payload, fin: true, mask: false, mask_key: nil, rsv1: false) raise ArgumentError, "unknown opcode 0x#{opcode.to_s(16)}" unless [0x0, 0x1, 0x2, 0x8, 0x9, 0xA].include?(opcode) payload_len = payload.bytesize if opcode >= 0x8 raise ArgumentError, 'control frame must have fin=true' unless fin raise ArgumentError, 'control frame payload exceeds 125 bytes' if payload_len > 125 raise ArgumentError, 'control frame must not have rsv1=true' if rsv1 end if mask raise ArgumentError, 'mask: true requires a 4-byte mask_key' if mask_key.nil? raise ArgumentError, 'mask_key must be 4 bytes' if mask_key.bytesize != 4 end out = String.new(encoding: Encoding::BINARY) out << ((fin ? 0x80 : 0x00) | (rsv1 ? 0x40 : 0x00) | (opcode & 0x0F)).chr mask_bit = mask ? 0x80 : 0x00 if payload_len < 126 out << (mask_bit | payload_len).chr elsif payload_len <= 0xFFFF out << (mask_bit | 126).chr out << ((payload_len >> 8) & 0xFF).chr out << (payload_len & 0xFF).chr else out << (mask_bit | 127).chr 8.times { |i| out << ((payload_len >> ((7 - i) * 8)) & 0xFF).chr } end if mask out << mask_key.b out << unmask(payload.b, mask_key) # XOR is symmetric else out << payload.b end out end |
.parse(buf, offset = 0) ⇒ Object
236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 |
# File 'lib/hyperion/websocket/frame.rb', line 236 def parse(buf, offset = 0) return :incomplete if offset > buf.bytesize avail = buf.bytesize - offset return :incomplete if avail < 2 b0 = buf.getbyte(offset) b1 = buf.getbyte(offset + 1) fin = (b0 & 0x80) != 0 rsv1 = (b0 & 0x40) != 0 rsv2 = (b0 & 0x20) != 0 rsv3 = (b0 & 0x10) != 0 opcode = b0 & 0x0F masked = (b1 & 0x80) != 0 len7 = b1 & 0x7F # RFC 7692 §6: RSV1 is the permessage-deflate marker. Allow it # through in the parse tuple; the Connection wrapper rejects # RSV1 when no extension was negotiated. RSV2/RSV3 are reserved # with no defined semantics → reject. return :error if rsv2 || rsv3 return :error unless [0x0, 0x1, 0x2, 0x8, 0x9, 0xA].include?(opcode) if opcode >= 0x8 return :error unless fin return :error if len7 > 125 # RFC 7692 §6.1 — control frames MUST NOT be compressed. return :error if rsv1 end header_len = 2 payload_len = case len7 when 0..125 len7 when 126 return :incomplete if avail < header_len + 2 v = (buf.getbyte(offset + 2) << 8) | buf.getbyte(offset + 3) header_len += 2 v else return :incomplete if avail < header_len + 8 return :error if (buf.getbyte(offset + 2) & 0x80) != 0 v = 0 8.times { |i| v = (v << 8) | buf.getbyte(offset + 2 + i) } header_len += 8 v end mask_key = nil if masked return :incomplete if avail < header_len + 4 mask_key = buf.byteslice(offset + header_len, 4) header_len += 4 end payload_offset = offset + header_len frame_total_len = header_len + payload_len return :incomplete if avail < frame_total_len [fin, opcode, payload_len, masked, mask_key, payload_offset, frame_total_len, rsv1] end |
.unmask(payload, key) ⇒ Object
224 225 226 227 228 229 230 231 232 233 234 |
# File 'lib/hyperion/websocket/frame.rb', line 224 def unmask(payload, key) raise ArgumentError, 'mask_key must be 4 bytes' if key.bytesize != 4 out = String.new(capacity: payload.bytesize, encoding: Encoding::BINARY) bytes = payload.bytes kbytes = key.bytes bytes.each_with_index do |b, i| out << (b ^ kbytes[i & 0x3]).chr end out end |