687 lines
22 KiB
Python
687 lines
22 KiB
Python
# coding=utf-8
|
|
from __future__ import absolute_import, division, print_function
|
|
|
|
__author__ = "Gina Häußge <osd@foosel.net>"
|
|
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
|
__copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms of the AGPLv3 License"
|
|
|
|
"""
|
|
The SSDP/UPNP implementations has been largely inspired by https://gist.github.com/schlamar/2428250
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
import flask
|
|
from flask.ext.babel import gettext
|
|
from builtins import range
|
|
|
|
import octoprint.plugin
|
|
import octoprint.util
|
|
|
|
try:
|
|
import pybonjour
|
|
except:
|
|
pybonjour = False
|
|
|
|
|
|
__plugin_name__ = "Discovery"
|
|
__plugin_author__ = "Gina Häußge"
|
|
__plugin_url__ = "http://docs.octoprint.org/en/master/bundledplugins/discovery.html"
|
|
__plugin_description__ = "Makes the OctoPrint instance discoverable via Bonjour/Avahi/Zeroconf and uPnP"
|
|
__plugin_disabling_discouraged__ = gettext("Without this plugin your OctoPrint instance will no longer be "
|
|
"discoverable on the network via Bonjour and uPnP.")
|
|
__plugin_license__ = "AGPLv3"
|
|
|
|
def __plugin_load__():
|
|
if not pybonjour:
|
|
# no pybonjour available, we can't use that
|
|
logging.getLogger("octoprint.plugins." + __name__).info("pybonjour is not installed, Zeroconf Discovery won't be available")
|
|
|
|
plugin = DiscoveryPlugin()
|
|
|
|
global __plugin_implementation__
|
|
__plugin_implementation__ = plugin
|
|
|
|
global __plugin_helpers__
|
|
__plugin_helpers__ = dict(
|
|
ssdp_browse=plugin.ssdp_browse
|
|
)
|
|
if pybonjour:
|
|
__plugin_helpers__.update(dict(
|
|
zeroconf_browse=plugin.zeroconf_browse,
|
|
zeroconf_register=plugin.zeroconf_register,
|
|
zeroconf_unregister=plugin.zeroconf_unregister
|
|
))
|
|
|
|
class DiscoveryPlugin(octoprint.plugin.StartupPlugin,
|
|
octoprint.plugin.ShutdownPlugin,
|
|
octoprint.plugin.BlueprintPlugin,
|
|
octoprint.plugin.SettingsPlugin):
|
|
|
|
ssdp_multicast_addr = "239.255.255.250"
|
|
|
|
ssdp_multicast_port = 1900
|
|
|
|
def __init__(self):
|
|
self.host = None
|
|
self.port = None
|
|
|
|
# zeroconf
|
|
self._sd_refs = dict()
|
|
self._cnames = dict()
|
|
|
|
# upnp/ssdp
|
|
self._ssdp_monitor_active = False
|
|
self._ssdp_monitor_thread = None
|
|
self._ssdp_notify_timeout = 10
|
|
self._ssdp_last_notify = 0
|
|
|
|
##~~ SettingsPlugin API
|
|
|
|
def get_settings_defaults(self):
|
|
return {
|
|
"publicHost": None,
|
|
"publicPort": None,
|
|
"pathPrefix": None,
|
|
"httpUsername": None,
|
|
"httpPassword": None,
|
|
"upnpUuid": None,
|
|
"zeroConf": [],
|
|
"model": {
|
|
"name": None,
|
|
"description": None,
|
|
"number": None,
|
|
"url": None,
|
|
"serial": None,
|
|
"vendor": None,
|
|
"vendorUrl": None
|
|
}
|
|
}
|
|
|
|
##~~ BlueprintPlugin API -- used for providing the SSDP device descriptor XML
|
|
|
|
@octoprint.plugin.BlueprintPlugin.route("/discovery.xml", methods=["GET"])
|
|
def discovery(self):
|
|
self._logger.debug("Rendering discovery.xml")
|
|
|
|
modelName = self._settings.get(["model", "name"])
|
|
if not modelName:
|
|
import octoprint.server
|
|
modelName = octoprint.server.DISPLAY_VERSION
|
|
|
|
vendor = self._settings.get(["model", "vendor"])
|
|
vendorUrl = self._settings.get(["model", "vendorUrl"])
|
|
if not vendor:
|
|
vendor = "The OctoPrint Project"
|
|
vendorUrl = "http://www.octoprint.org/"
|
|
|
|
response = flask.make_response(flask.render_template("discovery.xml.jinja2",
|
|
friendlyName=self.get_instance_name(),
|
|
manufacturer=vendor,
|
|
manufacturerUrl=vendorUrl,
|
|
modelName=modelName,
|
|
modelDescription=self._settings.get(["model", "description"]),
|
|
modelNumber=self._settings.get(["model", "number"]),
|
|
modelUrl=self._settings.get(["model", "url"]),
|
|
serialNumber=self._settings.get(["model", "serial"]),
|
|
uuid=self.get_uuid(),
|
|
presentationUrl=flask.url_for("index", _external=True)))
|
|
response.headers['Content-Type'] = 'application/xml'
|
|
return response
|
|
|
|
def is_blueprint_protected(self):
|
|
return False
|
|
|
|
##~~ StartupPlugin API -- used for registering OctoPrint's Zeroconf and SSDP services upon application startup
|
|
|
|
def on_startup(self, host, port):
|
|
public_host = self._settings.get(["publicHost"])
|
|
if public_host:
|
|
host = public_host
|
|
public_port = self._settings.get(["publicPort"])
|
|
if public_port:
|
|
port = public_port
|
|
|
|
self.host = host
|
|
self.port = port
|
|
|
|
# Zeroconf
|
|
self.zeroconf_register("_http._tcp", self.get_instance_name(), txt_record=self._create_http_txt_record_dict())
|
|
self.zeroconf_register("_octoprint._tcp", self.get_instance_name(), txt_record=self._create_octoprint_txt_record_dict())
|
|
for zeroconf in self._settings.get(["zeroConf"]):
|
|
if "service" in zeroconf:
|
|
self.zeroconf_register(
|
|
zeroconf["service"],
|
|
zeroconf["name"] if "name" in zeroconf else self.get_instance_name(),
|
|
port=zeroconf["port"] if "port" in zeroconf else None,
|
|
txt_record=zeroconf["txtRecord"] if "txtRecord" in zeroconf else None
|
|
)
|
|
|
|
# SSDP
|
|
self._ssdp_register()
|
|
|
|
##~~ ShutdownPlugin API -- used for unregistering OctoPrint's Zeroconf and SSDP service upon application shutdown
|
|
|
|
def on_shutdown(self):
|
|
for key in self._sd_refs:
|
|
reg_type, port = key
|
|
self.zeroconf_unregister(reg_type, port)
|
|
|
|
self._ssdp_unregister()
|
|
|
|
##~~ helpers
|
|
|
|
# ZeroConf
|
|
|
|
def zeroconf_register(self, reg_type, name=None, port=None, txt_record=None):
|
|
"""
|
|
Registers a new service with Zeroconf/Bonjour/Avahi.
|
|
|
|
:param reg_type: type of service to register, e.g. "_gntp._tcp"
|
|
:param name: displayable name of the service, if not given defaults to the OctoPrint instance name
|
|
:param port: port to register for the service, if not given defaults to OctoPrint's (public) port
|
|
:param txt_record: optional txt record to attach to the service, dictionary of key-value-pairs
|
|
"""
|
|
|
|
if not pybonjour:
|
|
return
|
|
|
|
if not name:
|
|
name = self.get_instance_name()
|
|
if not port:
|
|
port = self.port
|
|
|
|
params = dict(
|
|
name=name,
|
|
regtype=reg_type,
|
|
port=port
|
|
)
|
|
if txt_record:
|
|
params["txtRecord"] = pybonjour.TXTRecord(txt_record)
|
|
|
|
key = (reg_type, port)
|
|
self._sd_refs[key] = pybonjour.DNSServiceRegister(**params)
|
|
self._logger.info(u"Registered {name} for {reg_type}".format(**locals()))
|
|
|
|
def zeroconf_unregister(self, reg_type, port=None):
|
|
"""
|
|
Unregisteres a previously registered Zeroconf/Bonjour/Avahi service identified by service and port.
|
|
|
|
:param reg_type: the type of the service to be unregistered
|
|
:param port: the port of the service to be unregistered, defaults to OctoPrint's (public) port if not given
|
|
:return:
|
|
"""
|
|
|
|
if not pybonjour:
|
|
return
|
|
|
|
if not port:
|
|
port = self.port
|
|
|
|
key = (reg_type, port)
|
|
if not key in self._sd_refs:
|
|
return
|
|
|
|
sd_ref = self._sd_refs[key]
|
|
try:
|
|
sd_ref.close()
|
|
self._logger.debug("Unregistered {reg_type} on port {port}".format(reg_type=reg_type, port=port))
|
|
except:
|
|
self._logger.exception("Could not unregister {reg_type} on port {port}".format(reg_type=reg_type, port=port))
|
|
|
|
def zeroconf_browse(self, service_type, block=True, callback=None, browse_timeout=5, resolve_timeout=5):
|
|
"""
|
|
Browses for services on the local network providing the specified service type. Can be used either blocking or
|
|
non-blocking.
|
|
|
|
The non-blocking version (default behaviour) will not return until the lookup has completed and
|
|
return all results that were found.
|
|
|
|
For non-blocking version, set `block` to `False` and provide a `callback` to be called once the lookup completes.
|
|
If no callback is provided in non-blocking mode, a ValueError will be raised.
|
|
|
|
The results are provided as a list of discovered services, with each service being described by a dictionary
|
|
with the following keys:
|
|
|
|
* `name`: display name of the service
|
|
* `host`: host name of the service
|
|
* `post`: port the service is listening on
|
|
* `txt_record`: TXT record of the service as a dictionary, exact contents depend on the service
|
|
|
|
Callbacks will be called with that list as the single parameter supplied to them. Thus, the following is an
|
|
example for a valid callback:
|
|
|
|
def browse_callback(results):
|
|
for result in results:
|
|
print "Name: {name}, Host: {host}, Port: {port}, TXT: {txt_record!r}".format(**result)
|
|
|
|
:param service_type: the service type to browse for
|
|
:param block: whether to block, defaults to True
|
|
:param callback: callback to call once lookup has completed, must be set when `block` is set to `False`
|
|
:param browse_timeout: timeout for browsing operation
|
|
:param resolve_timeout: timeout for resolving operations for discovered records
|
|
:return: if `block` is `True` a list of the discovered services, an empty list otherwise (results will then be
|
|
supplied to the callback instead)
|
|
"""
|
|
|
|
if not pybonjour:
|
|
return None
|
|
|
|
import threading
|
|
import select
|
|
|
|
if not block and not callback:
|
|
raise ValueError("Non-blocking mode but no callback given")
|
|
|
|
result = []
|
|
result_available = threading.Event()
|
|
result_available.clear()
|
|
|
|
resolved = []
|
|
|
|
def resolve_callback(sd_ref, flags, interface_index, error_code, fullname, hosttarget, port, txt_record):
|
|
if error_code == pybonjour.kDNSServiceErr_NoError:
|
|
txt_record_dict = None
|
|
if txt_record:
|
|
record = pybonjour.TXTRecord.parse(txt_record)
|
|
txt_record_dict = dict()
|
|
for key, value in record:
|
|
txt_record_dict[key] = value
|
|
|
|
name = fullname[:fullname.find(service_type) - 1].replace("\\032", " ")
|
|
host = hosttarget[:-1]
|
|
|
|
self._logger.debug("Resolved a result for Zeroconf resolution of {service_type}: {name} @ {host}".format(service_type=service_type, name=name, host=host))
|
|
result.append(dict(
|
|
name=name,
|
|
host=host,
|
|
port=port,
|
|
txt_record=txt_record_dict
|
|
))
|
|
resolved.append(True)
|
|
|
|
def browse_callback(sd_ref, flags, interface_index, error_code, service_name, regtype, reply_domain):
|
|
if error_code != pybonjour.kDNSServiceErr_NoError:
|
|
return
|
|
|
|
if not (flags & pybonjour.kDNSServiceFlagsAdd):
|
|
return
|
|
|
|
self._logger.debug("Got a browsing result for Zeroconf resolution of {service_type}, resolving...".format(service_type=service_type))
|
|
resolve_ref = pybonjour.DNSServiceResolve(0, interface_index, service_name, regtype, reply_domain, resolve_callback)
|
|
|
|
try:
|
|
while not resolved:
|
|
ready = select.select([resolve_ref], [], [], resolve_timeout)
|
|
if resolve_ref not in ready[0]:
|
|
break
|
|
|
|
pybonjour.DNSServiceProcessResult(resolve_ref)
|
|
else:
|
|
resolved.pop()
|
|
finally:
|
|
resolve_ref.close()
|
|
|
|
self._logger.debug("Browsing Zeroconf for {service_type}".format(service_type=service_type))
|
|
|
|
def browse():
|
|
sd_ref = pybonjour.DNSServiceBrowse(regtype=service_type, callBack=browse_callback)
|
|
try:
|
|
while True:
|
|
ready = select.select([sd_ref], [], [], browse_timeout)
|
|
|
|
if not ready[0]:
|
|
break
|
|
|
|
if sd_ref in ready[0]:
|
|
pybonjour.DNSServiceProcessResult(sd_ref)
|
|
finally:
|
|
sd_ref.close()
|
|
|
|
if callback:
|
|
callback(result)
|
|
result_available.set()
|
|
|
|
browse_thread = threading.Thread(target=browse)
|
|
browse_thread.daemon = True
|
|
browse_thread.start()
|
|
|
|
if block:
|
|
result_available.wait()
|
|
return result
|
|
else:
|
|
return []
|
|
|
|
# SSDP/UPNP
|
|
|
|
def ssdp_browse(self, query, block=True, callback=None, timeout=1, retries=5):
|
|
"""
|
|
Browses for UPNP services matching the supplied query. Can be used either blocking or
|
|
non-blocking.
|
|
|
|
The non-blocking version (default behaviour) will not return until the lookup has completed and
|
|
return all results that were found.
|
|
|
|
For non-blocking version, set `block` to `False` and provide a `callback` to be called once the lookup completes.
|
|
If no callback is provided in non-blocking mode, a ValueError will be raised.
|
|
|
|
The results are provided as a list of discovered locations of device descriptor files.
|
|
|
|
Callbacks will be called with that list as the single parameter supplied to them. Thus, the following is an
|
|
example for a valid callback:
|
|
|
|
def browse_callback(results):
|
|
for result in results:
|
|
print "Location: {}".format(result)
|
|
|
|
:param query: the SSDP query to send, e.g. "upnp:rootdevice" to search for all devices
|
|
:param block: whether to block, defaults to True
|
|
:param callback: callback to call in non-blocking mode when lookup has finished, must be set if block is False
|
|
:param timeout: timeout in seconds to wait for replies to the M-SEARCH query per interface, defaults to 1
|
|
:param retries: number of retries to perform the lookup on all interfaces, defaults to 5
|
|
:return: if `block` is `True` a list of the discovered devices, an empty list otherwise (results will then be
|
|
supplied to the callback instead)
|
|
"""
|
|
|
|
import threading
|
|
|
|
import httplib
|
|
import io
|
|
class Response(httplib.HTTPResponse):
|
|
def __init__(self, response_text):
|
|
self.fp = io.BytesIO(response_text)
|
|
self.debuglevel = 0
|
|
self.strict = 0
|
|
self.msg = None
|
|
self._method = None
|
|
self.begin()
|
|
|
|
result = []
|
|
result_available = threading.Event()
|
|
result_available.clear()
|
|
|
|
def browse():
|
|
import socket
|
|
|
|
socket.setdefaulttimeout(timeout)
|
|
|
|
search_message = "".join([
|
|
"M-SEARCH * HTTP/1.1\r\n",
|
|
"ST: {query}\r\n",
|
|
"MX: 3\r\n",
|
|
"MAN: \"ssdp:discovery\"\r\n",
|
|
"HOST: {mcast_addr}:{mcast_port}\r\n\r\n"
|
|
])
|
|
|
|
for _ in range(retries):
|
|
for addr in octoprint.util.interface_addresses():
|
|
try:
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
|
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2)
|
|
sock.bind((addr, 0))
|
|
|
|
message = search_message.format(query=query,
|
|
mcast_addr=self.__class__.ssdp_multicast_addr,
|
|
mcast_port=self.__class__.ssdp_multicast_port)
|
|
for _ in range(2):
|
|
sock.sendto(message, (self.__class__.ssdp_multicast_addr, self.__class__.ssdp_multicast_port))
|
|
|
|
try:
|
|
data = sock.recv(1024)
|
|
except socket.timeout:
|
|
pass
|
|
else:
|
|
response = Response(data)
|
|
|
|
result.append(response.getheader("Location"))
|
|
except:
|
|
pass
|
|
|
|
if callback:
|
|
callback(result)
|
|
result_available.set()
|
|
|
|
browse_thread = threading.Thread(target=browse)
|
|
browse_thread.daemon = True
|
|
browse_thread.start()
|
|
|
|
if block:
|
|
result_available.wait()
|
|
return result
|
|
else:
|
|
return []
|
|
|
|
##~~ internals
|
|
|
|
# Zeroconf
|
|
|
|
def _create_http_txt_record_dict(self):
|
|
"""
|
|
Creates a TXT record for the _http._tcp Zeroconf service supplied by this OctoPrint instance.
|
|
|
|
Defines the keys for _http._tcp as defined in http://www.dns-sd.org/txtrecords.html
|
|
|
|
:return: a dictionary containing the defined key-value-pairs, ready to be turned into a TXT record
|
|
"""
|
|
|
|
# determine path entry
|
|
path = "/"
|
|
if self._settings.get(["pathPrefix"]):
|
|
path = self._settings.get(["pathPrefix"])
|
|
else:
|
|
prefix = self._settings.global_get(["server", "reverseProxy", "prefixFallback"])
|
|
if prefix:
|
|
path = prefix
|
|
|
|
# fetch username and password (if set)
|
|
username = self._settings.get(["httpUsername"])
|
|
password = self._settings.get(["httpPassword"])
|
|
|
|
entries = dict(
|
|
path=path
|
|
)
|
|
|
|
if username and password:
|
|
entries.update(dict(u=username, p=password))
|
|
|
|
return entries
|
|
|
|
def _create_octoprint_txt_record_dict(self):
|
|
"""
|
|
Creates a TXT record for the _octoprint._tcp Zeroconf service supplied by this OctoPrint instance.
|
|
|
|
The following keys are defined:
|
|
|
|
* `path`: path prefix to actual OctoPrint instance, inherited from _http._tcp
|
|
* `u`: username if HTTP Basic Auth is used, optional, inherited from _http._tcp
|
|
* `p`: password if HTTP Basic Auth is used, optional, inherited from _http._tcp
|
|
* `version`: OctoPrint software version
|
|
* `api`: OctoPrint API version
|
|
* `model`: Model of the device that is running OctoPrint
|
|
* `vendor`: Vendor of the device that is running OctoPrint
|
|
|
|
:return: a dictionary containing the defined key-value-pairs, ready to be turned into a TXT record
|
|
"""
|
|
|
|
entries = self._create_http_txt_record_dict()
|
|
|
|
import octoprint.server
|
|
import octoprint.server.api
|
|
|
|
entries.update(dict(
|
|
version=octoprint.server.VERSION,
|
|
api=octoprint.server.api.VERSION,
|
|
))
|
|
|
|
modelName = self._settings.get(["model", "name"])
|
|
if modelName:
|
|
entries.update(dict(model=modelName))
|
|
vendor = self._settings.get(["model", "vendor"])
|
|
if vendor:
|
|
entries.update(dict(vendor=vendor))
|
|
|
|
return entries
|
|
|
|
# SSDP/UPNP
|
|
|
|
def _ssdp_register(self):
|
|
"""
|
|
Registers the OctoPrint instance as basic service with a presentation URL pointing to the web interface
|
|
"""
|
|
|
|
import threading
|
|
|
|
self._ssdp_monitor_active = True
|
|
|
|
self._ssdp_monitor_thread = threading.Thread(target=self._ssdp_monitor, kwargs=dict(timeout=self._ssdp_notify_timeout))
|
|
self._ssdp_monitor_thread.daemon = True
|
|
self._ssdp_monitor_thread.start()
|
|
|
|
def _ssdp_unregister(self):
|
|
"""
|
|
Unregisters the OctoPrint instance again
|
|
"""
|
|
|
|
self._ssdp_monitor_active = False
|
|
if self.host and self.port:
|
|
for _ in range(2):
|
|
self._ssdp_notify(alive=False)
|
|
|
|
def _ssdp_notify(self, alive=True):
|
|
"""
|
|
Sends an SSDP notify message across the connected networks.
|
|
|
|
:param alive: True to send an "ssdp:alive" message, False to send an "ssdp:byebye" message
|
|
"""
|
|
|
|
import socket
|
|
import time
|
|
|
|
if alive and self._ssdp_last_notify + self._ssdp_notify_timeout > time.time():
|
|
# we just sent an alive, no need to send another one now
|
|
return
|
|
|
|
if alive and not self._ssdp_monitor_active:
|
|
# the monitor already shut down, alive messages don't make sense anymore as byebye will shortly follow
|
|
return
|
|
|
|
for addr in octoprint.util.interface_addresses():
|
|
try:
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
|
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2)
|
|
sock.bind((addr, 0))
|
|
|
|
location = "http://{addr}:{port}/plugin/discovery/discovery.xml".format(addr=addr, port=self.port)
|
|
|
|
self._logger.debug("Sending NOTIFY {} via {}".format("alive" if alive else "byebye", addr))
|
|
notify_message = "".join([
|
|
"NOTIFY * HTTP/1.1\r\n",
|
|
"Server: Python/2.7\r\n",
|
|
"Cache-Control: max-age=900\r\n",
|
|
"Location: {location}\r\n",
|
|
"NTS: {nts}\r\n",
|
|
"NT: upnp:rootdevice\r\n",
|
|
"USN: uuid:{uuid}::upnp:rootdevice\r\n",
|
|
"HOST: {mcast_addr}:{mcast_port}\r\n\r\n"
|
|
])
|
|
message = notify_message.format(uuid=self.get_uuid(),
|
|
location=location,
|
|
nts="ssdp:alive" if alive else "ssdp:byebye",
|
|
mcast_addr=self.__class__.ssdp_multicast_addr,
|
|
mcast_port=self.__class__.ssdp_multicast_port)
|
|
for _ in range(2):
|
|
# send twice, stuff might get lost, it's only UDP
|
|
sock.sendto(message, (self.__class__.ssdp_multicast_addr, self.__class__.ssdp_multicast_port))
|
|
except:
|
|
pass
|
|
|
|
self._ssdp_last_notify = time.time()
|
|
|
|
def _ssdp_monitor(self, timeout=5):
|
|
"""
|
|
Monitor thread that listens on the multicast address for M-SEARCH requests and answers them if they are relevant
|
|
|
|
:param timeout: timeout after which to stop waiting for M-SEARCHs for a short while in order to put out an
|
|
alive message
|
|
"""
|
|
|
|
from BaseHTTPServer import BaseHTTPRequestHandler
|
|
from StringIO import StringIO
|
|
import socket
|
|
|
|
socket.setdefaulttimeout(timeout)
|
|
|
|
location_message = "".join([
|
|
"HTTP/1.1 200 OK\r\n",
|
|
"ST: upnp:rootdevice\r\n",
|
|
"USN: uuid:{uuid}::upnp:rootdevice\r\n",
|
|
"Location: {location}\r\n",
|
|
"Cache-Control: max-age=60\r\n\r\n"
|
|
])
|
|
|
|
class Request(BaseHTTPRequestHandler):
|
|
|
|
def __init__(self, request_text):
|
|
self.rfile = StringIO(request_text)
|
|
self.raw_requestline = self.rfile.readline()
|
|
self.error_code = self.error_message = None
|
|
self.parse_request()
|
|
|
|
def send_error(self, code, message=None):
|
|
self.error_code = code
|
|
self.error_message = message
|
|
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
|
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2)
|
|
sock.bind(('', self.__class__.ssdp_multicast_port))
|
|
|
|
sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, socket.inet_aton(self.__class__.ssdp_multicast_addr) + socket.inet_aton('0.0.0.0'))
|
|
|
|
self._logger.info(u"Registered {} for SSDP".format(self.get_instance_name()))
|
|
|
|
self._ssdp_notify(alive=True)
|
|
|
|
try:
|
|
while (self._ssdp_monitor_active):
|
|
try:
|
|
data, address = sock.recvfrom(4096)
|
|
request = Request(data)
|
|
if not request.error_code and request.command == "M-SEARCH" and request.path == "*" and (request.headers["ST"] == "upnp:rootdevice" or request.headers["ST"] == "ssdp:all") and request.headers["MAN"] == '"ssdp:discover"':
|
|
interface_address = octoprint.util.address_for_client(*address)
|
|
if not interface_address:
|
|
self._logger.warn("Can't determine address to user for client {}, not sending a M-SEARCH reply".format(address))
|
|
continue
|
|
message = location_message.format(uuid=self.get_uuid(), location="http://{host}:{port}/plugin/discovery/discovery.xml".format(host=interface_address, port=self.port))
|
|
sock.sendto(message, address)
|
|
self._logger.debug("Sent M-SEARCH reply for {path} and {st} to {address!r}".format(path=request.path, st=request.headers["ST"], address=address))
|
|
except socket.timeout:
|
|
pass
|
|
finally:
|
|
self._ssdp_notify(alive=True)
|
|
finally:
|
|
try:
|
|
sock.close()
|
|
except:
|
|
pass
|
|
|
|
##~~ helpers
|
|
|
|
def get_uuid(self):
|
|
upnpUuid = self._settings.get(["upnpUuid"])
|
|
if upnpUuid is None:
|
|
import uuid
|
|
upnpUuid = str(uuid.uuid4())
|
|
self._settings.set(["upnpUuid"], upnpUuid)
|
|
self._settings.save()
|
|
return upnpUuid
|
|
|
|
def get_instance_name(self):
|
|
name = self._settings.global_get(["appearance", "name"])
|
|
if name:
|
|
return u"OctoPrint instance \"{}\"".format(name)
|
|
else:
|
|
import socket
|
|
return u"OctoPrint instance on {}".format(socket.gethostname())
|