Module: Jimmu::WebSocketProtocol

Defined in:
lib/jimmu.rb

Overview

RFC 6455 handshake + frame codec. Implements unfragmented and fragmented text/binary messages, ping/pong, and close frames. Extensions (permessage-deflate etc.) are not negotiated or supported, which is the right amount of protocol for a small-to-medium app framework talking to browser clients.

Constant Summary collapse

MAGIC_GUID =
'258EAFA5-E914-47DA-95CA-C5AB0DC85B11'

Class Method Summary collapse

Class Method Details

.accept_key(client_key) ⇒ Object



1017
1018
1019
# File 'lib/jimmu.rb', line 1017

def accept_key(client_key)
  Base64.strict_encode64(Digest::SHA1.digest(client_key.to_s + MAGIC_GUID))
end

.read_message(socket) ⇒ Object

Reads one complete (possibly fragmented) message from socket. Returns payload: or nil on EOF/disconnect. Control frames (close/ping/pong) are returned immediately and are never fragmented, per spec.



1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
# File 'lib/jimmu.rb', line 1025

def read_message(socket)
  first_opcode = nil
  buffer = +''.b
  loop do
    frame = read_single_frame(socket)
    return nil if frame.nil?
    return frame if [0x8, 0x9, 0xA].include?(frame[:opcode])

    first_opcode ||= frame[:opcode]
    buffer << frame[:payload]
    if frame[:fin]
      payload = first_opcode == 0x1 ? buffer.force_encoding(Encoding::UTF_8) : buffer
      return { opcode: first_opcode, payload: payload }
    end
  end
end

.read_single_frame(socket) ⇒ Object



1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
# File 'lib/jimmu.rb', line 1042

def read_single_frame(socket)
  hdr = read_exactly(socket, 2)
  return nil if hdr.nil?
  b0, b1 = hdr.unpack('C2')
  fin = (b0 & 0x80) != 0
  opcode = b0 & 0x0F
  masked = (b1 & 0x80) != 0
  len = b1 & 0x7F

  if len == 126
    ext = read_exactly(socket, 2)
    return nil if ext.nil?
    len = ext.unpack1('n')
  elsif len == 127
    ext = read_exactly(socket, 8)
    return nil if ext.nil?
    len = ext.unpack1('Q>')
  end

  mask_key = nil
  if masked
    mask_key = read_exactly(socket, 4)
    return nil if mask_key.nil?
  end

  payload = len.positive? ? read_exactly(socket, len) : ''.b
  return nil if payload.nil?

  if masked
    unmasked = +''.b
    payload.each_byte.with_index { |byte, i| unmasked << (byte ^ mask_key.getbyte(i % 4)) }
    payload = unmasked
  end

  { fin: fin, opcode: opcode, payload: payload }
end

.write_frame(socket, opcode, payload, fin: true) ⇒ Object



1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
# File 'lib/jimmu.rb', line 1079

def write_frame(socket, opcode, payload, fin: true)
  payload = payload.to_s.dup.force_encoding(Encoding::BINARY)
  b0 = (fin ? 0x80 : 0x00) | (opcode & 0x0F)
  out = +''.b
  out << [b0].pack('C')
  len = payload.bytesize
  if len <= 125
    out << [len].pack('C')
  elsif len <= 0xFFFF
    out << [126, len].pack('Cn')
  else
    out << [127, len].pack('CQ>')
  end
  out << payload
  socket.write(out)
end