File: server.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 (179 lines) | stat: -rw-r--r-- 5,692 bytes parent folder | download | duplicates (2)
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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# frozen_string_literal: true

module WebSocket
  module Handshake
    # Construct or parse a server WebSocket handshake.
    #
    # @example
    #   handshake = WebSocket::Handshake::Server.new
    #
    #   # Parse client request
    #   @handshake << <<EOF
    #   GET /demo HTTP/1.1\r
    #   Upgrade: websocket\r
    #   Connection: Upgrade\r
    #   Host: example.com\r
    #   Origin: http://example.com\r
    #   Sec-WebSocket-Version: 13\r
    #   Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r
    #   \r
    #   EOF
    #
    #   # All data received?
    #   @handshake.finished?
    #
    #   # No parsing errors?
    #   @handshake.valid?
    #
    #   # Create response
    #   @handshake.to_s # HTTP/1.1 101 Switching Protocols
    #                   # Upgrade: websocket
    #                   # Connection: Upgrade
    #                   # Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
    #
    class Server < Base
      # Initialize new WebSocket Server
      #
      # @param [Hash] args Arguments for server
      #
      # @option args [Boolean] :secure If true then server will use wss:// protocol
      # @option args [Array<String>] :protocols an array of supported sub-protocols
      #
      # @example
      #   Websocket::Handshake::Server.new(secure: true)
      def initialize(args = {})
        super
        @secure ||= false
      end

      # Add text of request from Client. This method will parse content immediately and update version, state and error(if neccessary)
      #
      # @param [String] data Data to add
      #
      # @example
      #   @handshake << <<EOF
      #   GET /demo HTTP/1.1
      #   Upgrade: websocket
      #   Connection: Upgrade
      #   Host: example.com
      #   Origin: http://example.com
      #   Sec-WebSocket-Version: 13
      #   Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
      #
      #   EOF
      def <<(data)
        super
        set_version if parse_data
      end
      rescue_method :<<

      # Parse the request from a rack environment
      # @param env Rack Environment
      #
      # @example
      #   @handshake.from_rack(env)
      def from_rack(env)
        @headers = env.select { |key, _value| key.to_s.start_with? 'HTTP_' }.each_with_object({}) do |tuple, memo|
          key, value = tuple
          memo[key.gsub(/\AHTTP_/, '').tr('_', '-').downcase] = value
        end

        @path      = env['REQUEST_PATH']
        @query     = env['QUERY_STRING']

        set_version

        # Passenger is blocking on read
        # Unicorn doesn't support readpartial
        # Maybe someone is providing even plain string?
        # Better safe than sorry...
        if @version == 76
          input = env['rack.input']
          @leftovers = if input.respond_to?(:readpartial)
                         input.readpartial
                       elsif input.respond_to?(:read)
                         input.read
                       else
                         input.to_s
                       end
        end

        @state = :finished
      end

      # Parse the request from hash
      # @param hash Hash to import data
      # @option hash [Hash] :headers HTTP headers of request, downcased
      # @option hash [String] :path Path for request(without host and query string)
      # @option hash [String] :query Query string for request
      # @option hash [String] :body Body of request(if exists)
      #
      # @example
      #   @handshake.from_hash(hash)
      def from_hash(hash)
        @headers = hash[:headers] || {}
        @path = hash[:path] || '/'
        @query = hash[:query] || ''
        @leftovers = hash[:body]

        set_version
        @state = :finished
      end

      # Should send content to client after finished parsing?
      # @return [Boolean] true
      def should_respond?
        true
      end

      # Host of server according to client header
      # @return [String] host
      def host
        @headers['host'].to_s.split(':')[0].to_s
      end

      # Port of server according to client header
      # @return [String] port
      def port
        @headers['host'].to_s.split(':')[1]
      end

      private

      # Set version of protocol basing on client requets. AFter cotting method calls include_version.
      def set_version
        @version = @headers['sec-websocket-version'].to_i if @headers['sec-websocket-version']
        @version ||= @headers['sec-websocket-draft'].to_i if @headers['sec-websocket-draft']
        @version ||= 76 if @headers['sec-websocket-key1']
        @version ||= 75
        include_version
      end

      # Include set of methods for selected protocol version
      # @return [Boolean] false if protocol number is unknown, otherwise true
      def include_version
        @handler = case @version
                   when 75 then Handler::Server75.new(self)
                   when 76, 0..3 then Handler::Server76.new(self)
                   when 4..17 then Handler::Server04.new(self)
                   else raise WebSocket::Error::Handshake::UnknownVersion
                   end
      end

      PATH = %r{^(\w+) (\/[^\s]*) HTTP\/1\.1$}

      # Parse first line of Client response.
      # @param [String] line Line to parse
      # @return [Boolean] True if parsed correctly. False otherwise
      def parse_first_line(line)
        line_parts = line.match(PATH)
        raise WebSocket::Error::Handshake::InvalidHeader unless line_parts
        method = line_parts[1].strip
        raise WebSocket::Error::Handshake::GetRequestRequired unless method == 'GET'

        resource_name = line_parts[2].strip
        @path, @query = resource_name.split('?', 2)
      end
    end
  end
end