class UPnPServer

Attributes

action_handler[RW]
devices[RW]

Public Class Methods

new(host='0.0.0.0', port=0) click to toggle source
# File lib/upnp_server.rb, line 58
def initialize(host='0.0.0.0', port=0)
  @finishing = false
  @devices = {}
  @ssdp_listener = SSDP::SsdpListener.new
  @ssdp_listener.handler = self
  @http_server = WEBrick::HTTPServer.new :BindAddress => host, :Port => port
  @http_server.mount '/', UPnPServerServlet, self
  @action_handler = nil
  @subscriptions = {}
  @interval_timer = 10
end

Public Instance Methods

get_device_description(path) click to toggle source
# File lib/upnp_server.rb, line 93
def get_device_description(path)
  hash = path.split('/').reject(&:empty?).first
  @devices.each do |udn, device|
    if get_hash(udn) == hash
      return UPnPDevice.to_xml_doc device
    end
  end
end
get_device_location(device) click to toggle source
# File lib/upnp_server.rb, line 256
def get_device_location(device)
  host = get_ip_address
  port = @http_server.config[:Port]
  return "http://#{host}:#{port}/#{get_hash(device.udn)}/device.xml"
end
get_hash(udn) click to toggle source
# File lib/upnp_server.rb, line 279
def get_hash(udn)
  Digest::MD5.hexdigest udn
end
get_ip_address() click to toggle source
# File lib/upnp_server.rb, line 283
def get_ip_address
  Socket::getaddrinfo(Socket.gethostname, 'echo', Socket::AF_INET)[0][3]
end
get_scpd(path) click to toggle source
# File lib/upnp_server.rb, line 103
def get_scpd(path)
  hash = path.split('/').reject(&:empty?).first
  @devices.values.each do |device|
    device.all_services.each do |service|
      if get_hash(service.service_type) == hash
        return UPnPScpd.to_xml_doc service.scpd
      end
    end
  end
end
msearch_response(st, usn, location, ext_header = nil) click to toggle source
# File lib/upnp_server.rb, line 262
def msearch_response(st, usn, location, ext_header = nil)
  header = HttpHeader.new FirstLine.new ['HTTP/1.1', '200', 'OK']
  fields = {
    'Cache-Control' => 'max-age=1800',
    'Location' => location,
    'ST' => st,
    'USN' => usn,
    'Ext' => '',
    'Date' => Time.now.httpdate,
  }
  header.update! fields
  if ext_header != nil
    header.update! ext_header
  end
  return header
end
notify_alive_all() click to toggle source
# File lib/upnp_server.rb, line 89
def notify_alive_all
end
on_action_request(path, soap_req) click to toggle source
# File lib/upnp_server.rb, line 115
def on_action_request(path, soap_req)
  hash = path.split('/').reject(&:empty?).first
  @devices.values.each do |device|
    device.all_services.each do |service|
      if get_hash(service.service_type) == hash
        if @action_handler
          return @action_handler.call service, soap_req
        end
      end
    end
  end
  
end
on_msearch(st) click to toggle source
# File lib/upnp_server.rb, line 182
def on_msearch(st)

  # ST can be one of
  #  - ssdp:all
  #  - upnp:rootdevice
  #  - udn
  #  - device type
  #  - service type

  # HTTP/1.1 200 OK
  # Cache-Control: max-age=1800
  # HOST: 239.255.255.250:1900
  # Location: http://172.17.0.2:9001/device.xml?udn=e399855c-7ecb-1fff-8000-000000000000
  # ST: e399855c-7ecb-1fff-8000-000000000000
  # Server: Cross-Platform/0.0 UPnP/1.0 App/0.0
  # USN: e399855c-7ecb-1fff-8000-000000000000
  # Ext:
  # Date: Sat, 08 Sep 2018 13:47:14 GMT

  response_list = []

  if st == 'ssdp:all'
    devices = @devices.values
    devices.each do |root_device|

      location = get_device_location root_device

      response_list << msearch_response('upnp:rootdevice',
                                        root_device.udn + '::upnp:rootdevice',
                                        location)

      root_device.all_devices.each do |device|
        response_list << msearch_response(device.device_type,
                                          device.usn,
                                          location)
      end

      root_device.all_services.each do |service|
        response_list << msearch_response(service.service_type,
                                          root_device.udn + '::' + service.service_type,
                                          location)
      end
    end
  elsif st == 'upnp:rootdevice'
    devices = @devices.values
    devices.each do |root_device|

      location = get_device_location root_device

      response_list << msearch_response('upnp:rootdevice',
                                        root_device.udn + '::upnp:rootdevice',
                                        location)
    end
  else
    @devices.values.each do |root_device|
      location = get_device_location root_device
      root_device.all_devices.each do |device|
        if device.device_type == st
          response_list << msearch_response(st, device.usn, location)
        end
      end
      root_device.all_services.each do |service|
        if service.service_type == st
          response_list << msearch_response(st,
                                            root_device.udn + '::' + service.service_type,
                                            location)
        end
      end
    end
  end

  return response_list
end
on_renew_subscription(sid) click to toggle source
# File lib/upnp_server.rb, line 164
def on_renew_subscription(sid)
  subscription = @subscriptions[sid]
  subscription.renew_timeout
end
on_ssdp_header(ssdp_header) click to toggle source
# File lib/upnp_server.rb, line 175
def on_ssdp_header(ssdp_header)
  if ssdp_header.msearch?
    ret = on_msearch ssdp_header['st']
  end
end
on_subscribe(path, timeout, callback_urls) click to toggle source
# File lib/upnp_server.rb, line 148
def on_subscribe(path, timeout, callback_urls)
  hash = path.split('/').reject(&:empty?).first
  @devices.values.each do |device|
    device.all_services.each do |service|
      if get_hash(service.service_type) == hash
        sid = 'uuid:' + SecureRandom.uuid
        subscription = UPnPEventSubscription.new device, service, sid, timeout, callback_urls
        @subscriptions[sid] = subscription
        return sid
      end
    end
  end
  nil
end
on_timer() click to toggle source
# File lib/upnp_server.rb, line 287
def on_timer
  @subscriptions.reject! {|key, value| value.expired?}
  notify_alive_all
end
on_unsubscribe(sid) click to toggle source
# File lib/upnp_server.rb, line 170
def on_unsubscribe(sid)
  @subscriptions.remove sid
end
register_device(device, scpd_table) click to toggle source
# File lib/upnp_server.rb, line 72
def register_device(device, scpd_table)
  device.all_services.each do |service|
    hash = get_hash service.service_type
    service.scpdurl = "/#{hash}/scpd.xml"
    service.control_url = "/#{hash}/control.xml"
    service.event_sub_url = "/#{hash}/event.xml"
    service.scpd = scpd_table[service.service_type]
  end
  @devices[device.udn] = device
end
set_property(device, service, props) click to toggle source
# File lib/upnp_server.rb, line 129
def set_property(device, service, props)
  @subscriptions.values.each do |subscription|
    if subscription.device.udn == device.udn and
      subscription.service.service_type == service.service_type
      subscription.callback_urls.each do |url|
        url = URI.parse(url)
        header = {
          'SID' => subscription.sid
        }
        Net::HTTP.start(url.host, url.port) do |http|
          req = NotifyRequest.new url, initheader = header
          req.body = UPnPEventProperty.to_xml_doc props
          res = http.request req
        end
      end
    end
  end
end
start() click to toggle source
# File lib/upnp_server.rb, line 303
def start
  @finishing = false
  @ssdp_listener_thread = Thread.new { @ssdp_listener.run }
  @http_server_thread = Thread.new { @http_server.start }
  @timer_thread = Thread.new { timer_loop }
end
stop() click to toggle source
# File lib/upnp_server.rb, line 310
def stop
  @finishing = true
  @http_server.shutdown
  @http_server_thread.join
  @timer_thread.join
end
timer_loop() click to toggle source
# File lib/upnp_server.rb, line 292
def timer_loop
  start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :second)
  while not @finishing
    dur = Process.clock_gettime(Process::CLOCK_MONOTONIC, :second) - start
    if dur >= @interval_timer
      on_timer
      start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :second)
    end
  end
end
unregister_device(device) click to toggle source
# File lib/upnp_server.rb, line 84
def unregister_device(device)
  @devices.remove device.udn
end