MrDraw/src/octoprint/plugins/discovery/__init__.py
2014-09-07 01:16:39 +02:00

286 lines
8.9 KiB
Python

# coding=utf-8
from __future__ import absolute_import
__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"
import logging
import os
import flask
import octoprint.plugin
default_settings = {
"publicPort": None,
"pathPrefix": None,
"httpUsername": None,
"httpPassword": None
}
s = octoprint.plugin.plugin_settings("discovery", defaults=default_settings)
def get_uuid():
import uuid
return str(uuid.uuid4())
UUID = get_uuid()
del get_uuid
def get_instance_name():
import socket
return "OctoPrint instance on {}".format(socket.gethostname())
INSTANCENAME = get_instance_name()
del get_instance_name
blueprint = flask.Blueprint("plugin.discovery", __name__, template_folder=os.path.join(os.path.dirname(os.path.realpath(__file__)), "templates"))
@blueprint.route("/discovery.xml")
def discovery():
logging.getLogger("octoprint.plugins." + __name__).info("Rendering discovery.xml")
response = flask.make_response(flask.render_template("discovery.jinja2",
friendlyName=INSTANCENAME,
manufacturer="OctoPrint",
manufacturerUrl="http://www.octoprint.org",
modelDescription="Some funny description",
modelName="Some funny name",
uuid=UUID,
presentationUrl=flask.url_for("index", _external=True)))
response.headers['Content-Type'] = 'application/xml'
return response
class DiscoveryPlugin(octoprint.plugin.types.StartupPlugin, octoprint.plugin.types.BlueprintPlugin, octoprint.plugin.SettingsPlugin):
def __init__(self):
self.logger = logging.getLogger("octoprint.plugins." + __name__)
self.octoprint_sd_ref = None
self.http_sd_ref = None
##~~ BlueprintPlugin API
def get_blueprint(self):
return blueprint
##~~ TemplatePlugin API (part of SettingsPlugin)
def get_template_vars(self):
return dict(
_settings_menu_entry="Network discovery"
)
def get_template_folder(self):
return os.path.join(os.path.dirname(os.path.realpath(__file__)), "templates")
#~~ StartupPlugin API
def on_startup(self, host, port):
self._bonjour_register(host, port)
self._ssdp_register(port)
#~~ SettingsPlugin API
def on_settings_load(self):
return {
"publicPort": s.getInt(["publicPort"]),
"pathPrefix": s.get(["pathPrefix"]),
"httpUsername": s.get(["httpUsername"]),
"httpPassword": s.get(["httpPassword"])
}
def on_settings_save(self, data):
if "publicPort" in data and data["publicPort"]:
s.setInt(["publicPort"], data["publicPort"])
if "pathPrefix" in data and data["pathPrefix"]:
s.set(["pathPrefix"], data["pathPrefix"])
if "httpUsername" in data and data["httpUsername"]:
s.set(["httpUsername"], data["httpUsername"])
if "httpPassword" in data and data["httpPassword"]:
s.set(["httpPassword"], data["httpPassword"])
#~~ internals
def _bonjour_register(self, host, port):
import pybonjour
def register_callback(sd_ref, flags, error_code, name, reg_type, domain):
if error_code == pybonjour.kDNSServiceErr_NoError:
self.logger.info("Registered {name} for {reg_type} with domain {domain}".format(**locals()))
if s.getInt(["publicPort"]):
port = s.getInt(["publicPort"])
self.octoprint_sd_ref = pybonjour.DNSServiceRegister(
name=INSTANCENAME,
regtype='_octoprint._tcp',
port=port,
txtRecord=pybonjour.TXTRecord(self._create_octoprint_txt_record_dict()),
callBack=register_callback
)
pybonjour.DNSServiceProcessResult(self.octoprint_sd_ref)
self.http_sd_ref = pybonjour.DNSServiceRegister(
name=INSTANCENAME,
regtype='_http._tcp',
port=port,
txtRecord=pybonjour.TXTRecord(self._create_base_txt_record_dict()),
callBack=register_callback
)
pybonjour.DNSServiceProcessResult(self.http_sd_ref)
def _create_octoprint_txt_record_dict(self):
entries = self._create_base_txt_record_dict()
import octoprint.server
import octoprint.server.api
entries.update(dict(
version=octoprint.server.VERSION,
api=octoprint.server.api.VERSION,
))
return entries
def _create_base_txt_record_dict(self):
# determine path entry
path = "/"
if s.get(["pathPrefix"]):
path = s.get(["pathPrefix"])
else:
prefix = s.globalGet(["server", "reverseProxy", "prefixFallback"])
if prefix:
path = prefix
# fetch username and password (if set)
username = s.get(["httpUsername"])
password = s.get(["httpPassword"])
entries = dict(
path=path
)
if username and password:
entries.update(dict(u=username, p=password))
return entries
def _ssdp_register(self, port):
import threading
self._ssdp_monitor_thread = threading.Thread(target=self._ssdp_monitor, args=[port])
self._ssdp_monitor_thread.daemon = True
self._ssdp_monitor_thread.start()
def _ssdp_notify(self, port, alive=True):
import socket
import netifaces
def interface_addresses(family=netifaces.AF_INET):
for interface in netifaces.interfaces():
ifaddresses = netifaces.ifaddresses(interface)
if family in ifaddresses:
for ifaddress in ifaddresses[family]:
yield ifaddress["addr"]
for addr in 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}/plugins/discovery/discovery.xml".format(addr=addr, port=port)
self.logger.info("Sending NOTIFY alive")
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: 239.255.255.250:1900\r\n\r\n"])
message = notify_message.format(uuid=UUID, location=location, nts="ssdp:alive" if alive else "ssdp:byebye")
for _ in xrange(2):
sock.sendto(message, ("239.255.255.250", 1900))
try:
sock.recv(1024)
except socket.timeout:
pass
except Exception as e:
pass
def _ssdp_monitor(self, port):
from BaseHTTPServer import BaseHTTPRequestHandler
from httplib import HTTPResponse
from StringIO import StringIO
import socket
import struct
socket.setdefaulttimeout(5)
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
class Response(HTTPResponse):
def __init__(self, response_text):
self.fp = StringIO(response_text)
self.debuglevel = 0
self.strict = 0
self.msg = None
self._method = None
self.begin()
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(('', 1900))
sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, socket.inet_aton('239.255.255.250') + socket.inet_aton('0.0.0.0'))
self.logger.info("Registered {} for SSDP".format(INSTANCENAME))
self._ssdp_notify(port, alive=True)
try:
while (True):
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"':
message = location_message.format(uuid=UUID, location="http://192.168.1.3:5000/plugin/discovery/discovery.xml")
sock.sendto(message, address)
self.logger.info("Sent M-SEARCH reply for {path} and {st} to {address!r}".format(path=request.path, st=request.headers["ST"], address=address))
except socket.timeout:
self._ssdp_notify(port, alive=True)
finally:
try:
sock.close()
except:
pass
__plugin_name__ = "Discovery"
__plugin_version__ = "0.1"
__plugin_description__ = "Makes the OctoPrint instance discoverable via Bonjour/Avahi/Zeroconf"
__plugin_implementations__ = []
def __plugin_check__():
try:
import pybonjour
except:
# no pybonjour available, we can't continue
logging.getLogger("octoprint.plugins." + __name__).info("pybonjour is not installed, Discovery Plugin won't be available. Please manually install pybonjour and restart OctoPrint")
return False
global __plugin_implementations__
__plugin_implementations__ = [DiscoveryPlugin(),]
return True