<commit_before>require 'uri'
require 'savon'
require_relative 'base'

module RUPNP
  module CP

    # Service class for device's services.
    #
    # ==Actions
    # This class defines ruby methods from actions defined in
    # service description, as provided by the device.
    #
    # By example, from this description:
    #  <action>
    #    <name>actionName</name>
    #    <argumentList>
    #    <argument>
    #      <name>argumentNameIn</name>
    #      <direction>in</direction>
    #      <relatedStateVariable>stateVariableName</relatedStateVariable>
    #    </argument>
    #    <argument>
    #      <name>argumentNameOut</name>
    #      <direction>out</direction>
    #      <relatedStateVariable>stateVariableName</relatedStateVariable>
    #    </argument>
    #  </action>
    # a +#action_name+ method is created. This method requires a hash with
    # an element named +argument_name_in+.
    # If no <i>in</i> argument is required, an empty hash (<code>{}</code>)
    # must be passed to the method. Hash keys may not be symbols.
    #
    # A Hash is returned, with a key for each <i>out</i> argument.
    #
    # @author <NAME>
    class RemoteService < Base

      # @private
      @@event_sub_count = 0

      # Get event subscription count for all services
      # (unique ID for subscription)
      # @return [Integer]
      def self.event_sub_count
        @@event_sub_count += 1
      end


      # @private SOAP integer types
      INTEGER_TYPES = %w(ui1 ui2 ui4 i1 i2 i4 int).freeze
      # @private SOAP float types
      FLOAT_TYPES = %w(r4 r8 number float).freeze
      # @private SOAP string types
      STRING_TYPES = %w(char string uuid).freeze
      # @private SOAP true values
      TRUE_TYPES = %w(1 true yes).freeze
      # @private SOAP false values
      FALSE_TYPES = %w(0 false no).freeze

      # Get device to which this service belongs to
      # @return [Device]
      attr_reader :device

      # Get service type
      # @return [String]
      attr_reader :type
      # Get service id
      # @return [String]
      attr_reader :id
      # URL for service description
      # @return [String]
      attr_reader :scpd_url
      # URL for control
      # @return [String]
      attr_reader :control_url
      # URL for eventing
      # @return [String]
      attr_reader :event_sub_url

      # XML namespace for device description
      # @return [String]
      attr_reader :xmlns
      # Define architecture on which the service is implemented
      # @return [String]
      attr_reader :spec_version
      # Available actions on this service
      # @return [Array<Hash>]
      attr_reader :actions
      # State table for the service
      # @return [Array<Hash>]
      attr_reader :state_table

      # @param [Device] device
      # @param [String] url_base
      # @param [Hash] service
      def initialize(device, url_base, service)
        super()
        @device = device
        @description = service

        @type = service[:service_type].to_s
        @id = service[:service_id].to_s
        @scpd_url = build_url(url_base,  service[:scpdurl].to_s)
        @control_url =  build_url(url_base, service[:control_url].to_s)
        @event_sub_url =  build_url(url_base, service[:event_sub_url].to_s)
        @actions = []

        initialize_savon
      end

      # Get service from its description
      # @return [void]
      def fetch
        scpd_getter = EM::DefaultDeferrable.new

        scpd_getter.errback do
          fail "cannot get SCPD from #@scpd_url"
          next
        end

        scpd_getter.callback do |scpd|
          if bad_description?(scpd)
            fail 'not a UPNP 1.0/1.1 SCPD'
            next
          end

          extract_service_state_table scpd
          extract_actions scpd

          succeed self
        end

        get_description @scpd_url, scpd_getter
      end

      # Subscribe to event
      # @param [Hash] options
      # @option options [Integer] timeout
      # @yieldparam [Event] event event received
      # @yieldparam [Object] msg message received
      # @return [Integer] subscribe id. May be used to unsubscribe on event
      def subscribe_to_event(options={}, &blk)
        cp = device.control_point

        cp.start_event_server

        port = cp.event_port
        num = self.class.event_sub_count
        @callback_url = "http://#{HOST_IP}:#{port}/event#{num}}"

        uri = URI(@event_sub_url)
        options[:timeout] ||= EVENT_SUB_DEFAULT_TIMEOUT

        log :info, "send SUBSCRIBE request to #{uri}"
        con = EM::HttpRequest.new(@event_sub_url)
        http = con.setup_request(:subscribe, :head => {
                                   'HOST' => "#{uri.host}:#{uri.port}",
                                   'USER-AGENT' => RUPNP::USER_AGENT,
                                   'CALLBACK' => @callback_url,
                                   'NT' => 'upnp:event',
                                   'TIMEOUT' => "Second-#{options[:timeout]}"})

        http.errback do |client|
          log :warn, "Cannot subscribe to event: #{client.error}"
        end

        http.callback do
          log :debug, 'Close connection to subscribe event URL'
          con.close
          if http.response_header.status != 200
            log :warn, "Cannot subscribe to event #@event_sub_url:" +
              " #{http.response_header.http_reason}"
          else
            timeout = http.response_header['TIMEOUT'].match(/(\d+)/)[1] || 1800
            event = Event.new(@event_sub_url, @callback_url,
                              http.response_header['SID'], timeout.to_i)
            EventServer.add_event_url ["/event#{num}", event]
            event.subscribe &blk
          end
        end
      end


      private

      def bad_description?(scpd)
        if scpd[:scpd]
          bd = false
          @xmlns = scpd[:scpd][:@xmlns]
          bd |= @xmlns != "urn:schemas-upnp-org:service-1-0"
          bd |= scpd[:scpd][:spec_version][:major].to_i != 1
          @spec_version = scpd[:scpd][:spec_version][:major] + '.'
          @spec_version += scpd[:scpd][:spec_version][:minor]<commit_msg>CP::RemoteService#subscribe_to_event: * Replace EventServer.add_event_url by EventServer.add_event ; * use URI#path on callback URL on creating event to keep only path ; * remove trailing '}' in callback URL creation.<commit_after>require 'uri'
require 'savon'
require_relative 'base'

module RUPNP
  module CP

    # Service class for device's services.
    #
    # ==Actions
    # This class defines ruby methods from actions defined in
    # service description, as provided by the device.
    #
    # By example, from this description:
    #  <action>
    #    <name>actionName</name>
    #    <argumentList>
    #    <argument>
    #      <name>argumentNameIn</name>
    #      <direction>in</direction>
    #      <relatedStateVariable>stateVariableName</relatedStateVariable>
    #    </argument>
    #    <argument>
    #      <name>argumentNameOut</name>
    #      <direction>out</direction>
    #      <relatedStateVariable>stateVariableName</relatedStateVariable>
    #    </argument>
    #  </action>
    # a +#action_name+ method is created. This method requires a hash with
    # an element named +argument_name_in+.
    # If no <i>in</i> argument is required, an empty hash (<code>{}</code>)
    # must be passed to the method. Hash keys may not be symbols.
    #
    # A Hash is returned, with a key for each <i>out</i> argument.
    #
    # @author <NAME>
    class RemoteService < Base

      # @private
      @@event_sub_count = 0

      # Get event subscription count for all services
      # (unique ID for subscription)
      # @return [Integer]
      def self.event_sub_count
        @@event_sub_count += 1
      end


      # @private SOAP integer types
      INTEGER_TYPES = %w(ui1 ui2 ui4 i1 i2 i4 int).freeze
      # @private SOAP float types
      FLOAT_TYPES = %w(r4 r8 number float).freeze
      # @private SOAP string types
      STRING_TYPES = %w(char string uuid).freeze
      # @private SOAP true values
      TRUE_TYPES = %w(1 true yes).freeze
      # @private SOAP false values
      FALSE_TYPES = %w(0 false no).freeze

      # Get device to which this service belongs to
      # @return [Device]
      attr_reader :device

      # Get service type
      # @return [String]
      attr_reader :type
      # Get service id
      # @return [String]
      attr_reader :id
      # URL for service description
      # @return [String]
      attr_reader :scpd_url
      # URL for control
      # @return [String]
      attr_reader :control_url
      # URL for eventing
      # @return [String]
      attr_reader :event_sub_url

      # XML namespace for device description
      # @return [String]
      attr_reader :xmlns
      # Define architecture on which the service is implemented
      # @return [String]
      attr_reader :spec_version
      # Available actions on this service
      # @return [Array<Hash>]
      attr_reader :actions
      # State table for the service
      # @return [Array<Hash>]
      attr_reader :state_table

      # @param [Device] device
      # @param [String] url_base
      # @param [Hash] service
      def initialize(device, url_base, service)
        super()
        @device = device
        @description = service

        @type = service[:service_type].to_s
        @id = service[:service_id].to_s
        @scpd_url = build_url(url_base,  service[:scpdurl].to_s)
        @control_url =  build_url(url_base, service[:control_url].to_s)
        @event_sub_url =  build_url(url_base, service[:event_sub_url].to_s)
        @actions = []

        initialize_savon
      end

      # Get service from its description
      # @return [void]
      def fetch
        scpd_getter = EM::DefaultDeferrable.new

        scpd_getter.errback do
          fail "cannot get SCPD from #@scpd_url"
          next
        end

        scpd_getter.callback do |scpd|
          if bad_description?(scpd)
            fail 'not a UPNP 1.0/1.1 SCPD'
            next
          end

          extract_service_state_table scpd
          extract_actions scpd

          succeed self
        end

        get_description @scpd_url, scpd_getter
      end

      # Subscribe to event
      # @param [Hash] options
      # @option options [Integer] timeout
      # @yieldparam [Event] event event received
      # @yieldparam [Object] msg message received
      # @return [Integer] subscribe id. May be used to unsubscribe on event
      def subscribe_to_event(options={}, &blk)
        cp = device.control_point

        cp.start_event_server

        port = cp.event_port
        num = self.class.event_sub_count
        @callback_url = "http://#{HOST_IP}:#{port}/event#{num}"

        uri = URI(@event_sub_url)
        options[:timeout] ||= EVENT_SUB_DEFAULT_TIMEOUT

        log :info, "send SUBSCRIBE request to #{uri}"
        con = EM::HttpRequest.new(@event_sub_url)
        http = con.setup_request(:subscribe, :head => {
                                   'HOST' => "#{uri.host}:#{uri.port}",
                                   'USER-AGENT' => RUPNP::USER_AGENT,
                                   'CALLBACK' => @callback_url,
                                   'NT' => 'upnp:event',
                                   'TIMEOUT' => "Second-#{options[:timeout]}"})

        http.errback do |client|
          log :warn, "Cannot subscribe to event: #{client.error}"
        end

        http.callback do
          log :debug, 'Close connection to subscribe event URL'
          con.close
          if http.response_header.status != 200
            log :warn, "Cannot subscribe to event #@event_sub_url:" +
              " #{http.response_header.http_reason}"
          else
            timeout = http.response_header['TIMEOUT'].match(/(\d+)/)[1] || 1800
            event = Event.new(@event_sub_url, URI(@callback_url).path,
                              http.response_header['SID'], timeout.to_i)
            EventServer.add_event event
            event.subscribe &blk
          end
        end
      end


      private

      def bad_description?(scpd)
        if scpd[:scpd]
          bd = false
          @xmlns = scpd[:scpd][:@xmlns]
          bd |= @xmlns != "urn:schemas-upnp-org:service-1-0"
          bd |= scpd[:scpd][:spec_version][:major].to_i != 1
          @spec_version = scpd[:scpd][:spec_version][:major] + '.'
          @spec_version += scpd[:scpd][:spec_version][:minor]
{'fraction_non_alphanumeric': 0.1027304027544239, 'fraction_numerical': 0.004323804948354552, 'mean_word_length': 2.07408318976126, 'pattern_counts': {'":': 0, '<': 65, '<?xml version=': 0, '>': 75, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 8}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 10, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5405794', 'n_tokens_mistral': 3769, 'n_tokens_neox': 3654, 'n_words': 974}