Class: StunClient::RFC3489Client

Inherits:
GenericClient show all
Defined in:
lib/stun-client/rfc3489/client.rb

Overview

A STUN client which can send and receive RFC3489 messages. It does not support shared secret messages.

Personal note: I would have liked to implement this, but I have not found a STUN server that supports it.

Instance Method Summary collapse

Methods inherited from GenericClient

#initialize, #parse_mapped_address, #receive_message, #send_binary, #try_sending, #validate_and_get_change

Constructor Details

This class inherits a constructor from StunClient::GenericClient

Instance Method Details

#query(parameters = {}) ⇒ Hash

Make a STUN Binding Request.

Parameters:

  • parameters (Hash) (defaults to: {})

    If the change IP or change port flag is to be set, ‘:change_ip` or `:change_port` can be set to true.

    If the default value (RFC taken) for retransmission should be changed, ‘:attempts` can be set for the number of attempts to transmit the request. Default value is 9. To change the initial interval (default value 100ms = 0.1) `:interval` can be set. This doubles according to RFC for each new attempt until to 1.6 seconds is reached.

Returns:

  • (Hash)


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
# File 'lib/stun-client/rfc3489/client.rb', line 31

def query parameters = {}
  txid = StunClient::Utilities.generate_txid(128)

  change_port, change_ip = validate_and_get_change parameters

  request = RFC3489Message::StunMessage.new

  request[:message_type] = 0x0001  # binding request
  request[:transaction_id] = txid
  request[:message_length] = request.to_binary_s.length - 20

  if change_port == 1 || change_ip == 1
    cr = RFC3489Message::ChangeRequest.new
    cr[:change_ip] = change_ip
    cr[:change_port] = change_port

    attr = RFC3489Message::Attribute.new
    attr[:attribute_type] = 0x0003
    attr[:attribute_length] = cr.to_binary_s.length
    attr[:attribute_value] = cr

    request[:message_length] += attr.to_binary_s.length
    request[:attributes] << attr
  end

  if parameters.has_key?(:response_port) && parameters[:response_port] &&
     parameters.has_key?(:response_ip) && parameters[:response_ip]
    raise 'Response Port must be a Integer' if ! parameters[:response_port].is_a? Integer
    raise 'Response IP must be a String' if ! parameters[:response_ip].is_a? String

    family = parameters[:response_ip].include?(':') ? 2 : 1

    begin
      ip = IPAddr.new(parameters[:response_ip]).hton
    rescue IPAddr::InvalidAddressError
      raise ArgumentError, 'Invalid IP'
    end

    ra = RFC3489Message::MappedAddress.new
    ra[:family] = family
    ra[:address] = ip
    ra[:port] = parameters[:response_port]

    attr = RFC3489Message::Attribute.new
    attr[:attribute_type] = 0x0002
    attr[:attribute_length] = ra.to_binary_s.length
    attr[:attribute_value] = ra

    request[:message_length] += attr.to_binary_s.length
    request[:attributes] << attr
  end

  new_interval = ->(old_interval) { (old_interval < 1.6 ? old_interval * 2 : old_interval ) }
  response = send_binary parameters, request.to_binary_s, new_interval, 9, 0.1

  msg = RFC3489Message::StunMessage.read response[0]
  res = {}
  res[:response] = msg

  if msg[:transaction_id] != txid
    expected = StunClient::Utilities.sanitize_string txid
    actual = StunClient::Utilities.sanitize_string msg[:transaction_id]
    # rubocop:disable Layout/TrailingWhitespace
    raise InvalidTransactionId, 
          "Server responsed with invalid transaction id. Expected: #{expected}, Actual: #{actual}"
    # rubocop:enable Layout/TrailingWhitespace
  end

  case RFC3489Message::StunMessage::MESSAGE_TYPES[msg[:message_type]]
  when :binding_response
    res.merge! parse_attributes(msg[:attributes])
  when :bindung_error_response
    res.merge! parse_attributes(msg[:attributes])
    msg = 'Server returns an unknown error'
    if res[:error]
      if res[:error][:unknown_attributes]
        msg += ": Unknown attributes: #{res[:error][:unknown_attributes]}"
      end
      if res[:error][:class]
        error_message = StunClient::Utilities.sanitize_string res[:error][:message]
        msg += ": #{res[:error][:class]}#{res[:error][:number]}#{error_message}"
      end
    else
      msg += '.'
    end
    raise BindungErrorResponse, msg
  else
    raise UnableToInterpretResponse, 'Server responsed with unknown message type'
  end

  return res
end