File: base.rb

package info (click to toggle)
ruby-websocket 1.2.9-3
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, forky, sid, trixie
  • size: 464 kB
  • sloc: ruby: 2,669; makefile: 4
file content (142 lines) | stat: -rw-r--r-- 4,126 bytes parent folder | download
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
135
136
137
138
139
140
141
142
# frozen_string_literal: true

module WebSocket
  module Handshake
    # @abstract Subclass and override to implement custom handshakes
    class Base
      include ExceptionHandler
      include NiceInspect

      attr_reader :host, :path, :query,
                  :state, :version, :secure,
                  :headers, :protocols

      # Initialize new WebSocket Handshake and set it's state to :new
      def initialize(args = {})
        args.each do |k, v|
          value = begin
            v.dup
          rescue TypeError
            v
          end
          instance_variable_set("@#{k}", value)
        end

        @state = :new
        @handler = nil

        @data = String.new('')
        @headers ||= {}
        @protocols ||= []
      end

      # @abstract Add data to handshake
      def <<(data)
        @data << data
      end

      # Return textual representation of handshake request or response
      # @return [String] text of response
      def to_s
        @handler ? @handler.to_s : ''
      end
      rescue_method :to_s, return: ''

      # Is parsing of data finished?
      # @return [Boolena] True if request was completely parsed or error occured. False otherwise
      def finished?
        @state == :finished || @state == :error
      end

      # Is parsed data valid?
      # @return [Boolean] False if some errors occured. Reason for error could be found in error method
      def valid?
        finished? && @error.nil? && @handler && @handler.valid?
      end
      rescue_method :valid?, return: false

      # @abstract Should send data after parsing is finished?
      def should_respond?
        raise NotImplementedError
      end

      # Data left from parsing. Sometimes data that doesn't belong to handshake are added - use this method to retrieve them.
      # @return [String] String if some data are available. Nil otherwise
      def leftovers
        (@leftovers.to_s.split("\n", reserved_leftover_lines + 1)[reserved_leftover_lines] || '').strip
      end

      # Return default port for protocol (80 for ws, 443 for wss)
      def default_port
        secure ? 443 : 80
      end

      # Check if provided port is a default one
      def default_port?
        port == default_port
      end

      def port
        @port || default_port
      end

      # URI of request.
      # @return [String] Full URI with protocol
      # @example
      #   @handshake.uri #=> "ws://example.com/path?query=true"
      def uri
        uri =  String.new(secure ? 'wss://' : 'ws://')
        uri << host
        uri << ":#{port}" unless default_port?
        uri << path
        uri << "?#{query}" if query
        uri
      end

      private

      # Number of lines after header that should be handled as belonging to handshake. Any data after those lines will be handled as leftovers.
      # @return [Integer] Number of lines
      def reserved_leftover_lines
        0
      end

      # Changes state to error and sets error message
      # @param [String] message Error message to set
      def error=(message)
        @state = :error
        super
      end

      HEADER = /^([^:]+):\s*(.+)$/

      # Parse data imported to handshake and sets state to finished if necessary.
      # @return [Boolean] True if finished parsing. False if not all data received yet.
      def parse_data
        header, @leftovers = @data.split("\r\n\r\n", 2)
        return false unless @leftovers # The whole header has not been received yet.

        lines = header.split("\r\n")

        first_line = lines.shift
        parse_first_line(first_line)

        lines.each do |line|
          h = HEADER.match(line)
          next unless h # Skip any invalid headers
          key = h[1].strip.downcase
          val = h[2].strip
          # If the header is already set and refers to the websocket protocol, append the new value
          if @headers.key?(key) && key =~ /^(sec-)?websocket-protocol$/
            @headers[key] << ", #{val}"
          else
            @headers[key] = val
          end
        end

        @state = :finished
        true
      end
    end
  end
end