diff --git a/src/octoprint/plugin/types.py b/src/octoprint/plugin/types.py index 42de77de..878a6481 100644 --- a/src/octoprint/plugin/types.py +++ b/src/octoprint/plugin/types.py @@ -176,6 +176,13 @@ class BlueprintPlugin(Plugin): return None + def is_blueprint_protected(self): + """ + Whether the blueprint is supposed to be protected by API key (the default) or not. + """ + + return True + class SettingsPlugin(Plugin): def on_settings_load(self): diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py index b1e75cee..c452caa5 100644 --- a/src/octoprint/server/__init__.py +++ b/src/octoprint/server/__init__.py @@ -306,9 +306,20 @@ class Server(): app.register_blueprint(apps, url_prefix="/apps") # also register any blueprints defined in BlueprintPlugins - octoprint.plugin.call_plugin(octoprint.plugin.types.BlueprintPlugin, - "get_blueprint", - callback=lambda name, _, blueprint: app.register_blueprint(blueprint, url_prefix="/plugin/{name}".format(name=name))) + blueprint_plugins = octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.BlueprintPlugin) + for name, plugin in blueprint_plugins.items(): + blueprint = plugin.get_blueprint() + if blueprint is None: + continue + + if plugin.is_blueprint_protected(): + from octoprint.server.util import apiKeyRequestHandler, corsResponseHandler + blueprint.before_request(apiKeyRequestHandler) + blueprint.after_request(corsResponseHandler) + + url_prefix = "/plugin/{name}".format(name=name) + app.register_blueprint(blueprint, url_prefix=url_prefix) + logger.debug("Registered API of plugin {name} under URL prefix {url_prefix}".format(name=name, url_prefix=url_prefix)) self._router = SockJSRouter(self._createSocketConnection, "/sockjs") diff --git a/src/octoprint/server/api/__init__.py b/src/octoprint/server/api/__init__.py index fd9fd5e3..1ee23962 100644 --- a/src/octoprint/server/api/__init__.py +++ b/src/octoprint/server/api/__init__.py @@ -17,9 +17,9 @@ import octoprint.util as util import octoprint.users import octoprint.server import octoprint.plugin -from octoprint.server import admin_permission, NO_CONTENT, UI_API_KEY, appSessionManager +from octoprint.server import admin_permission, NO_CONTENT from octoprint.settings import settings as s, valid_boolean_trues -from octoprint.server.util import get_api_key, get_user_for_apikey +from octoprint.server.util import apiKeyRequestHandler, corsResponseHandler from octoprint.server.util.flask import restricted_access @@ -40,78 +40,8 @@ from . import slicing as api_slicing VERSION = "0.1" - -def optionsAllowOrigin(request): - """ Always reply 200 on OPTIONS request """ - - resp = current_app.make_default_options_response() - - # Allow the origin which made the XHR - resp.headers['Access-Control-Allow-Origin'] = request.headers['Origin'] - # Allow the actual method - resp.headers['Access-Control-Allow-Methods'] = request.headers['Access-Control-Request-Method'] - # Allow for 10 seconds - resp.headers['Access-Control-Max-Age'] = "10" - - # 'preflight' request contains the non-standard headers the real request will have (like X-Api-Key) - customRequestHeaders = request.headers.get('Access-Control-Request-Headers', None) - if customRequestHeaders is not None: - # If present => allow them all - resp.headers['Access-Control-Allow-Headers'] = customRequestHeaders - - return resp - -@api.before_request -def beforeApiRequests(): - """ - All requests in this blueprint need to be made supplying an API key. This may be the UI_API_KEY, in which case - the underlying request processing will directly take place, or it may be the global or a user specific case. In any - case it has to be present and must be valid, so anything other than the above three types will result in denying - the request. - """ - - if request.method == 'OPTIONS' and s().getBoolean(["api", "allowCrossOrigin"]): - return optionsAllowOrigin(request) - - apikey = get_api_key(request) - if apikey is None: - # no api key => 401 - return make_response("No API key provided", 401) - - if apikey == UI_API_KEY: - # ui api key => continue regular request processing - return - - if not s().get(["api", "enabled"]): - # api disabled => 401 - return make_response("API disabled", 401) - - if apikey == s().get(["api", "key"]): - # global api key => continue regular request processing - return - - if appSessionManager.validate(apikey): - # app session key => continue regular request processing - return - - user = get_user_for_apikey(apikey) - if user is not None: - # user specific api key => continue regular request processing - return - - # invalid api key => 401 - return make_response("Invalid API key", 401) - -@api.after_request -def afterApiRequests(resp): - - # Allow crossdomain - allowCrossOrigin = s().getBoolean(["api", "allowCrossOrigin"]) - if request.method != 'OPTIONS' and 'Origin' in request.headers and allowCrossOrigin: - resp.headers['Access-Control-Allow-Origin'] = request.headers['Origin'] - - return resp - +api.before_request(apiKeyRequestHandler) +api.after_request(corsResponseHandler) #~~ data from plugins diff --git a/src/octoprint/server/util/__init__.py b/src/octoprint/server/util/__init__.py index e840fca2..83c246cc 100644 --- a/src/octoprint/server/util/__init__.py +++ b/src/octoprint/server/util/__init__.py @@ -10,12 +10,88 @@ import octoprint.timelapse import octoprint.server from octoprint.users import ApiUser +import flask as _flask + from . import flask from . import sockjs from . import tornado from . import watchdog +def apiKeyRequestHandler(): + """ + All requests in this blueprint need to be made supplying an API key. This may be the UI_API_KEY, in which case + the underlying request processing will directly take place, or it may be the global or a user specific case. In any + case it has to be present and must be valid, so anything other than the above three types will result in denying + the request. + """ + + import octoprint.server + + if _flask.request.method == 'OPTIONS' and settings().getBoolean(["api", "allowCrossOrigin"]): + return optionsAllowOrigin(_flask.request) + + apikey = get_api_key(_flask.request) + if apikey is None: + # no api key => 401 + return _flask.make_response("No API key provided", 401) + + if apikey == octoprint.server.UI_API_KEY: + # ui api key => continue regular request processing + return + + if not settings().get(["api", "enabled"]): + # api disabled => 401 + return _flask.make_response("API disabled", 401) + + if apikey == settings().get(["api", "key"]): + # global api key => continue regular request processing + return + + if octoprint.server.appSessionManager.validate(apikey): + # app session key => continue regular request processing + return + + user = get_user_for_apikey(apikey) + if user is not None: + # user specific api key => continue regular request processing + return + + # invalid api key => 401 + return _flask.make_response("Invalid API key", 401) + + +def corsResponseHandler(resp): + + # Allow crossdomain + allowCrossOrigin = settings().getBoolean(["api", "allowCrossOrigin"]) + if _flask.request.method != 'OPTIONS' and 'Origin' in _flask.request.headers and allowCrossOrigin: + resp.headers['Access-Control-Allow-Origin'] = _flask.request.headers['Origin'] + + return resp + + +def optionsAllowOrigin(request): + """ Always reply 200 on OPTIONS request """ + + resp = _flask.current_app.make_default_options_response() + + # Allow the origin which made the XHR + resp.headers['Access-Control-Allow-Origin'] = request.headers['Origin'] + # Allow the actual method + resp.headers['Access-Control-Allow-Methods'] = request.headers['Access-Control-Request-Method'] + # Allow for 10 seconds + resp.headers['Access-Control-Max-Age'] = "10" + + # 'preflight' request contains the non-standard headers the real request will have (like X-Api-Key) + customRequestHeaders = request.headers.get('Access-Control-Request-Headers', None) + if customRequestHeaders is not None: + # If present => allow them all + resp.headers['Access-Control-Allow-Headers'] = customRequestHeaders + + return resp + + def get_user_for_apikey(apikey): if settings().get(["api", "enabled"]) and apikey is not None: if apikey == settings().get(["api", "key"]) or octoprint.server.appSessionManager.validate(apikey):