File: writer.rb

package info (click to toggle)
ruby-ftw 0.0.49-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 548 kB
  • sloc: ruby: 1,922; makefile: 5
file content (134 lines) | stat: -rw-r--r-- 4,645 bytes parent folder | download | duplicates (4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
require "ftw/namespace"
require "ftw/websocket"
require "ftw/singleton"
require "ftw/websocket/constants"

# This class implements a writer for WebSocket messages over a stream.
#
# Protocol diagram copied from RFC6455
#     0                   1                   2                   3
#     0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
#    +-+-+-+-+-------+-+-------------+-------------------------------+
#    |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
#    |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
#    |N|V|V|V|       |S|             |   (if payload len==126/127)   |
#    | |1|2|3|       |K|             |                               |
#    +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
#    |     Extended payload length continued, if payload len == 127  |
#    + - - - - - - - - - - - - - - - +-------------------------------+
#    |                               |Masking-key, if MASK set to 1  |
#    +-------------------------------+-------------------------------+
#    | Masking-key (continued)       |          Payload Data         |
#    +-------------------------------- - - - - - - - - - - - - - - - +
#    :                     Payload Data continued ...                :
#    + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
#    |                     Payload Data continued ...                |
#    +---------------------------------------------------------------+

class FTW::WebSocket::Writer
  include FTW::WebSocket::Constants
  extend FTW::Singleton

  # A list of valid modes. Used to validate input in #write_text.
  #
  # In :server mode, payloads are not masked. In :client mode, payloads
  # are masked. Masking is described in RFC6455.
  VALID_MODES = [:server, :client]

  private

  # Write the given text in a websocket frame to the connection.
  #
  # Valid 'mode' settings are :server or :client. If :client, the
  # payload will be masked according to RFC6455 section 5.3:
  # http://tools.ietf.org/html/rfc6455#section-5.3
  def write_text(connection, text, mode=:server)
    if !VALID_MODES.include?(mode)
      raise InvalidArgument.new("Invalid message mode: #{mode}, expected one of" \
                                "#{VALID_MODES.inspect}")
    end

    data = []
    pack = []

    # For now, assume single-fragment, text frames
    pack_opcode(data, pack, OPCODE_TEXT)
    pack_payload(data, pack, text, mode)
    connection.write(data.pack(pack.join("")))
  end # def write_text

  # Pack the opcode and flags
  #
  # Currently assumes 'fin' flag is set.
  def pack_opcode(data, pack, opcode)
    # Pack the first byte (fin + opcode)
    data << ((1 << 7) | opcode)
    pack << "C"
  end # def pack_opcode

  # Pack the payload.
  def pack_payload(data, pack, text, mode)
    pack_maskbit_and_length(data, pack, text.bytesize, mode)
    pack_extended_length(data, pack, text.bytesize) if text.bytesize >= 126
    if mode == :client
      mask_key = [rand(1 << 32)].pack("Q")
      pack_mask(data, pack, mask_key)
      data << mask(text, mask_key)
      pack << "A*"
    else
      data << text
      pack << "A*"
    end
  end # def pack_payload

  # Implement masking as described by http://tools.ietf.org/html/rfc6455#section-5.3
  # Basically, we take a 4-byte random string and use it, round robin, to XOR
  # every byte. Like so:
  #   message[0] ^ key[0]
  #   message[1] ^ key[1]
  #   message[2] ^ key[2]
  #   message[3] ^ key[3]
  #   message[4] ^ key[0]
  #   ...
  def mask(message, key)
    masked = []
    mask_bytes = key.unpack("C4")
    i = 0
    message.each_byte do |byte|
      masked << (byte ^ mask_bytes[i % 4])
      i += 1
    end
    return masked.pack("C*")
  end # def mask

  # Pack the first part of the length (mask and 7-bit length)
  def pack_maskbit_and_length(data, pack, length, mode)
    # Pack mask + payload length
    maskbit = (mode == :client) ? (1 << 7) : 0
    if length >= 126
      if length < (1 << 16) # if less than 2^16, use 2 bytes
        lengthbits = 126
      else
        lengthbits = 127
      end
    else
      lengthbits = length
    end
    data << (maskbit | lengthbits)
    pack << "C"
  end # def pack_maskbit_and_length

  # Pack the extended length. 16 bits or 64 bits
  def pack_extended_length(data, pack, length)
    data << length
    if length >= (1 << 16)
      # For lengths >= 16 bits, pack 8 byte length
      pack << "Q>"
    else
      # For lengths < 16 bits, pack 2 byte length
      pack << "S>"
    end
  end # def pack_extended_length

  public(:initialize, :write_text)
end # module FTW::WebSocket::Writer