matlab-proxy 0.5.3__py3-none-any.whl → 0.30.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- matlab_proxy/app.py +578 -205
- matlab_proxy/app_state.py +1061 -431
- matlab_proxy/constants.py +37 -0
- matlab_proxy/default_configuration.py +39 -4
- matlab_proxy/devel.py +18 -22
- matlab_proxy/gui/index.html +20 -1
- matlab_proxy/gui/static/css/index.BedVwcEg.css +10 -0
- matlab_proxy/gui/static/js/index.pQwV1obF.js +64 -0
- matlab_proxy/gui/static/media/MATLAB-env-blur.NupTbPv_.png +0 -0
- matlab_proxy/matlab/evaluateUserMatlabCode.m +51 -0
- matlab_proxy/matlab/startup.m +3 -28
- matlab_proxy/settings.py +543 -112
- matlab_proxy/util/__init__.py +187 -59
- matlab_proxy/util/cookie_jar.py +72 -0
- matlab_proxy/util/event_loop.py +28 -10
- matlab_proxy/util/list_servers.py +71 -26
- matlab_proxy/util/mw.py +16 -15
- matlab_proxy/util/mwi/download.py +136 -0
- matlab_proxy/util/mwi/embedded_connector/__init__.py +1 -1
- matlab_proxy/util/mwi/embedded_connector/helpers.py +12 -4
- matlab_proxy/util/mwi/embedded_connector/request.py +78 -12
- matlab_proxy/util/mwi/environment_variables.py +120 -27
- matlab_proxy/util/mwi/exceptions.py +63 -9
- matlab_proxy/util/mwi/logger.py +141 -27
- matlab_proxy/util/mwi/session_name.py +28 -0
- matlab_proxy/util/mwi/token_auth.py +264 -121
- matlab_proxy/util/mwi/validators.py +231 -88
- matlab_proxy/util/system.py +9 -0
- matlab_proxy/util/windows.py +32 -6
- {matlab_proxy-0.5.3.dist-info → matlab_proxy-0.30.1.dist-info}/METADATA +94 -49
- matlab_proxy-0.30.1.dist-info/RECORD +88 -0
- {matlab_proxy-0.5.3.dist-info → matlab_proxy-0.30.1.dist-info}/WHEEL +1 -2
- {matlab_proxy-0.5.3.dist-info → matlab_proxy-0.30.1.dist-info}/entry_points.txt +1 -1
- matlab_proxy_manager/README.md +85 -0
- matlab_proxy_manager/__init__.py +6 -0
- matlab_proxy_manager/lib/README.md +53 -0
- matlab_proxy_manager/lib/__init__.py +1 -0
- matlab_proxy_manager/lib/api.py +419 -0
- matlab_proxy_manager/storage/README.md +54 -0
- matlab_proxy_manager/storage/__init__.py +1 -0
- matlab_proxy_manager/storage/file_repository.py +144 -0
- matlab_proxy_manager/storage/interface.py +62 -0
- matlab_proxy_manager/storage/server.py +172 -0
- matlab_proxy_manager/utils/__init__.py +1 -0
- matlab_proxy_manager/utils/auth.py +77 -0
- matlab_proxy_manager/utils/constants.py +8 -0
- matlab_proxy_manager/utils/decorators.py +37 -0
- matlab_proxy_manager/utils/environment_variables.py +51 -0
- matlab_proxy_manager/utils/exceptions.py +45 -0
- matlab_proxy_manager/utils/helpers.py +314 -0
- matlab_proxy_manager/utils/logger.py +76 -0
- matlab_proxy_manager/web/README.md +37 -0
- matlab_proxy_manager/web/__init__.py +1 -0
- matlab_proxy_manager/web/app.py +536 -0
- matlab_proxy_manager/web/monitor.py +45 -0
- matlab_proxy_manager/web/watcher.py +65 -0
- matlab_proxy/gui/asset-manifest.json +0 -23
- matlab_proxy/gui/authorization.html +0 -115
- matlab_proxy/gui/bootstrap.3.4.1.min.css +0 -6
- matlab_proxy/gui/navbar.css +0 -8
- matlab_proxy/gui/signin.css +0 -42
- matlab_proxy/gui/static/css/main.d890078a.chunk.css +0 -13
- matlab_proxy/gui/static/css/main.d890078a.chunk.css.map +0 -1
- matlab_proxy/gui/static/js/2.13be6544.chunk.js +0 -3
- matlab_proxy/gui/static/js/2.13be6544.chunk.js.LICENSE.txt +0 -59
- matlab_proxy/gui/static/js/2.13be6544.chunk.js.map +0 -1
- matlab_proxy/gui/static/js/main.c311d854.chunk.js +0 -2
- matlab_proxy/gui/static/js/main.c311d854.chunk.js.map +0 -1
- matlab_proxy/gui/static/js/runtime-main.f70e4d5f.js +0 -2
- matlab_proxy/gui/static/js/runtime-main.f70e4d5f.js.map +0 -1
- matlab_proxy/gui/static/media/arrow.0c2968b9.svg +0 -4
- matlab_proxy/gui/static/media/feedback.6e8d50eb.svg +0 -1
- matlab_proxy/gui/static/media/gripper.9defbc5e.svg +0 -1
- matlab_proxy/gui/static/media/help.15e5bfab.svg +0 -1
- matlab_proxy/gui/static/media/ico-header-contact-hover.0958c442.svg +0 -17
- matlab_proxy/gui/static/media/ico-header-contact.ae9169c8.svg +0 -17
- matlab_proxy/gui/static/media/restart.7987508a.svg +0 -1
- matlab_proxy/gui/static/media/sign-out.08356b67.svg +0 -1
- matlab_proxy/gui/static/media/start.50c4596f.svg +0 -1
- matlab_proxy/gui/static/media/stop.30c9a9ab.svg +0 -1
- matlab_proxy/gui/static/media/terminate.7ea1363e.svg +0 -1
- matlab_proxy/gui/token.html +0 -123
- matlab_proxy-0.5.3.dist-info/RECORD +0 -84
- matlab_proxy-0.5.3.dist-info/top_level.txt +0 -1
- /matlab_proxy/gui/static/media/{glyphicons-halflings-regular.82b1212e.woff → glyphicons-halflings-regular.BKjkU69z.woff} +0 -0
- /matlab_proxy/gui/static/media/{glyphicons-halflings-regular.5be1347c.eot → glyphicons-halflings-regular.BUJKDMgK.eot} +0 -0
- /matlab_proxy/gui/static/media/{glyphicons-halflings-regular.060b2710.svg → glyphicons-halflings-regular.CSehLiBc.svg} +0 -0
- /matlab_proxy/gui/static/media/{glyphicons-halflings-regular.4692b9ec.ttf → glyphicons-halflings-regular.DrwTMapi.ttf} +0 -0
- /matlab_proxy/gui/static/media/{glyphicons-halflings-regular.be810be3.woff2 → glyphicons-halflings-regular.DzqM6ju8.woff2} +0 -0
- /matlab_proxy/gui/static/media/{ico-header-account-hover.89438e91.svg → ico-header-account-hover.-jQHo6Wx.svg} +0 -0
- /matlab_proxy/gui/static/media/{ico-header-account.86b10d7b.svg → ico-header-account.CJCFoo5a.svg} +0 -0
- /matlab_proxy/gui/static/media/{ico-sprite.cbdb66c0.png → ico-sprite.DXGLgzq9.png} +0 -0
- /matlab_proxy/gui/static/media/{mathworks-eps.4d20e0ee.ttf → mathworks-eps.CGNQALa9.ttf} +0 -0
- /matlab_proxy/gui/static/media/{mathworks-eps.df1428df.svg → mathworks-eps.DrkCtQtG.svg} +0 -0
- /matlab_proxy/gui/static/media/{mathworks-eps.e5c41e84.woff → mathworks-eps.Ds7lQbql.woff} +0 -0
- /matlab_proxy/gui/static/media/{mathworks-pictograms.3fc6513a.woff → mathworks-pictograms.BdqxEfBR.woff} +0 -0
- /matlab_proxy/gui/static/media/{mathworks-pictograms.f6f087b0.svg → mathworks-pictograms.CCLweoD4.svg} +0 -0
- /matlab_proxy/gui/static/media/{mathworks-pictograms.6e128c0e.ttf → mathworks-pictograms.DZhFdRSm.ttf} +0 -0
- /matlab_proxy/gui/static/media/{mathworks.80a3218e.svg → mathworks.C-qsbhDy.svg} +0 -0
- /matlab_proxy/gui/static/media/{mathworks.c422935b.ttf → mathworks.Ceplx86V.ttf} +0 -0
- /matlab_proxy/gui/static/media/{mathworks.37a563ef.woff → mathworks.D08X1Vp8.woff} +0 -0
- /matlab_proxy/gui/static/media/{trigger-error.3f1c4ef2.svg → trigger-error.QEdsGL-m.svg} +0 -0
- /matlab_proxy/gui/static/media/{trigger-ok.7b9c238b.svg → trigger-ok.Dzg8OIrk.svg} +0 -0
- {matlab_proxy-0.5.3.dist-info → matlab_proxy-0.30.1.dist-info/licenses}/LICENSE.md +0 -0
matlab_proxy/app.py
CHANGED
|
@@ -1,25 +1,26 @@
|
|
|
1
|
-
# Copyright
|
|
1
|
+
# Copyright 2020-2026 The MathWorks, Inc.
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import json
|
|
5
5
|
import mimetypes
|
|
6
6
|
import pkgutil
|
|
7
|
+
import secrets
|
|
7
8
|
import sys
|
|
8
9
|
|
|
9
10
|
import aiohttp
|
|
10
|
-
from aiohttp import web
|
|
11
|
+
from aiohttp import client_exceptions, web
|
|
11
12
|
from aiohttp_session import setup as aiohttp_session_setup
|
|
12
13
|
from aiohttp_session.cookie_storage import EncryptedCookieStorage
|
|
13
14
|
from cryptography import fernet
|
|
14
15
|
|
|
15
16
|
import matlab_proxy
|
|
16
|
-
from matlab_proxy import settings, util
|
|
17
|
+
from matlab_proxy import constants, settings, util
|
|
17
18
|
from matlab_proxy.app_state import AppState
|
|
18
|
-
from matlab_proxy.
|
|
19
|
-
from matlab_proxy.util import
|
|
19
|
+
from matlab_proxy.constants import IS_CONCURRENCY_CHECK_ENABLED
|
|
20
|
+
from matlab_proxy.util import mwi
|
|
21
|
+
from matlab_proxy.util.mwi import download, token_auth
|
|
20
22
|
from matlab_proxy.util.mwi import environment_variables as mwi_env
|
|
21
|
-
from matlab_proxy.util.mwi import
|
|
22
|
-
from matlab_proxy.util.mwi.exceptions import LicensingError
|
|
23
|
+
from matlab_proxy.util.mwi.exceptions import AppError, InvalidTokenError, LicensingError
|
|
23
24
|
|
|
24
25
|
mimetypes.add_type("font/woff", ".woff")
|
|
25
26
|
mimetypes.add_type("font/woff2", ".woff2")
|
|
@@ -28,6 +29,11 @@ mimetypes.add_type("font/ttf", ".ttf")
|
|
|
28
29
|
mimetypes.add_type("application/json", ".map")
|
|
29
30
|
mimetypes.add_type("image/png", ".ico")
|
|
30
31
|
|
|
32
|
+
# Explicitly add JS mime types to override any incorrect types
|
|
33
|
+
# Refer https://github.com/mathworks/matlab-proxy/issues/78 for details
|
|
34
|
+
mimetypes.add_type("text/javascript", ".js")
|
|
35
|
+
mimetypes.add_type("text/javascript", ".mjs")
|
|
36
|
+
|
|
31
37
|
# TODO It is bad practice to have global state in aiohttp applications, instead this
|
|
32
38
|
# mount point should be read in the application start up function, then if it is not
|
|
33
39
|
# an empty string, registering a subapp with the given prefix. In addition, if it is
|
|
@@ -51,18 +57,25 @@ def marshal_licensing_info(licensing_info):
|
|
|
51
57
|
if licensing_info is None:
|
|
52
58
|
return None
|
|
53
59
|
|
|
54
|
-
|
|
60
|
+
licensing_type = licensing_info.get("type")
|
|
61
|
+
|
|
62
|
+
if licensing_type is None:
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
if licensing_type == "mhlm":
|
|
55
66
|
return {
|
|
56
|
-
"type": "
|
|
67
|
+
"type": "mhlm",
|
|
57
68
|
"emailAddress": licensing_info["email_addr"],
|
|
58
69
|
"entitlements": licensing_info.get("entitlements", []),
|
|
59
70
|
"entitlementId": licensing_info.get("entitlement_id", None),
|
|
60
71
|
}
|
|
61
|
-
elif
|
|
72
|
+
elif licensing_type == "nlm":
|
|
62
73
|
return {
|
|
63
|
-
"type": "
|
|
74
|
+
"type": "nlm",
|
|
64
75
|
"connectionString": licensing_info["conn_str"],
|
|
65
76
|
}
|
|
77
|
+
elif licensing_type == "existing_license":
|
|
78
|
+
return {"type": "existing_license"}
|
|
66
79
|
|
|
67
80
|
|
|
68
81
|
def marshal_error(error):
|
|
@@ -76,39 +89,107 @@ def marshal_error(error):
|
|
|
76
89
|
"""
|
|
77
90
|
if error is None:
|
|
78
91
|
return None
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
92
|
+
if isinstance(error, AppError):
|
|
93
|
+
return {
|
|
94
|
+
"message": error.message,
|
|
95
|
+
"logs": error.logs,
|
|
96
|
+
"type": error.__class__.__name__,
|
|
97
|
+
}
|
|
98
|
+
else:
|
|
99
|
+
return {"message": error.__str__, "logs": "", "type": error.__class__.__name__}
|
|
84
100
|
|
|
85
101
|
|
|
86
|
-
|
|
87
|
-
"""Send a generic status response about the state of server,MATLAB and
|
|
102
|
+
def create_status_response(app, loadUrl=None, client_id=None, is_active_client=None):
|
|
103
|
+
"""Send a generic status response about the state of server, MATLAB, MATLAB Licensing and the client session status.
|
|
88
104
|
|
|
89
105
|
Args:
|
|
90
106
|
app (aiohttp.web.Application): Web Server
|
|
91
107
|
loadUrl (String, optional): Represents the root URL. Defaults to None.
|
|
108
|
+
client_id (String, optional): Represents the generated client_id when concurrency check is enabled and client does not have a client_id. Defaults to None.
|
|
109
|
+
is_active_client (Boolean, optional): Represents whether the current client is the active_client when concurrency check is enabled. Defaults to None.
|
|
92
110
|
|
|
93
111
|
Returns:
|
|
94
|
-
JSONResponse: A JSONResponse object containing the generic state of the server, MATLAB and
|
|
112
|
+
JSONResponse: A JSONResponse object containing the generic state of the server, MATLAB, MATLAB Licensing and the client session status.
|
|
95
113
|
"""
|
|
96
114
|
state = app["state"]
|
|
115
|
+
status = {
|
|
116
|
+
"matlab": {
|
|
117
|
+
"status": state.get_matlab_state(),
|
|
118
|
+
"busyStatus": state.matlab_busy_state,
|
|
119
|
+
"version": state.settings["matlab_version"],
|
|
120
|
+
},
|
|
121
|
+
"licensing": marshal_licensing_info(state.licensing),
|
|
122
|
+
"loadUrl": loadUrl,
|
|
123
|
+
"error": marshal_error(state.error),
|
|
124
|
+
"warnings": state.warnings,
|
|
125
|
+
"wsEnv": state.settings.get("ws_env", ""),
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if client_id:
|
|
129
|
+
status["clientId"] = client_id
|
|
130
|
+
if is_active_client is not None:
|
|
131
|
+
status["isActiveClient"] = is_active_client
|
|
132
|
+
|
|
133
|
+
return web.json_response(status)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@token_auth.authenticate_access_decorator
|
|
137
|
+
async def clear_client_id(req):
|
|
138
|
+
"""API endpoint to reset the active client
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
req (HTTPRequest): HTTPRequest Object
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Response: an empty response in JSON format
|
|
145
|
+
"""
|
|
146
|
+
state = req.app["state"]
|
|
147
|
+
state.active_client = None
|
|
148
|
+
logger.debug("Client Id was cleaned!!!")
|
|
149
|
+
# This response is of no relevance to the front-end as the client has already exited
|
|
150
|
+
return web.json_response({})
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def reset_timer_decorator(endpoint):
|
|
154
|
+
"""This decorator resets the IDLE timer if MWI_IDLE_TIMEOUT environment variable is supplied.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
endpoint (callable): An asynchronous function which is a request handler.
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
async def reset_timer(req):
|
|
161
|
+
state = req.app["state"]
|
|
162
|
+
|
|
163
|
+
if state.is_idle_timeout_enabled:
|
|
164
|
+
await state.reset_timer()
|
|
165
|
+
|
|
166
|
+
return await endpoint(req)
|
|
167
|
+
|
|
168
|
+
return reset_timer
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@token_auth.authenticate_access_decorator
|
|
172
|
+
async def get_auth_token(req):
|
|
173
|
+
"""API endpoint to return the auth token
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
req (HTTPRequest): HTTPRequest Object
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Response: auth token in JSON format
|
|
180
|
+
"""
|
|
181
|
+
auth_token = await token_auth._get_token(req)
|
|
97
182
|
|
|
98
183
|
return web.json_response(
|
|
99
184
|
{
|
|
100
|
-
"
|
|
101
|
-
"status": await state.get_matlab_state(),
|
|
102
|
-
"version": state.settings["matlab_version"],
|
|
103
|
-
},
|
|
104
|
-
"licensing": marshal_licensing_info(state.licensing),
|
|
105
|
-
"loadUrl": loadUrl,
|
|
106
|
-
"error": marshal_error(state.error),
|
|
107
|
-
"wsEnv": state.settings["ws_env"],
|
|
185
|
+
"token": auth_token,
|
|
108
186
|
}
|
|
109
187
|
)
|
|
110
188
|
|
|
111
189
|
|
|
190
|
+
# @token_auth.authenticate_access_decorator
|
|
191
|
+
# Explicitly disabling authentication for this end-point,
|
|
192
|
+
# because the front end requires this endpoint to be available at all times.
|
|
112
193
|
async def get_env_config(req):
|
|
113
194
|
"""API Endpoint to get Matlab Web Desktop environment specific configuration.
|
|
114
195
|
|
|
@@ -118,10 +199,34 @@ async def get_env_config(req):
|
|
|
118
199
|
Returns:
|
|
119
200
|
JSONResponse: contains a Dict representing environment specific configuration serialized to JSON
|
|
120
201
|
"""
|
|
121
|
-
|
|
202
|
+
state = req.app["state"]
|
|
203
|
+
config = state.settings["env_config"]
|
|
204
|
+
|
|
205
|
+
config["isConcurrencyEnabled"] = IS_CONCURRENCY_CHECK_ENABLED
|
|
206
|
+
# In a previously authenticated session, if the url is accessed without the token(using session cookie), send the token as well.
|
|
207
|
+
config["authentication"] = {
|
|
208
|
+
"enabled": state.settings["mwi_is_token_auth_enabled"],
|
|
209
|
+
"status": True if await token_auth.authenticate_request(req) else False,
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
config["matlab"] = {
|
|
213
|
+
"version": state.settings["matlab_version"],
|
|
214
|
+
"supportedVersions": constants.SUPPORTED_MATLAB_VERSIONS,
|
|
215
|
+
"rootPath": str(state.settings.get("matlab_path", "")),
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
config["browserTitle"] = state.settings["browser_title"]
|
|
219
|
+
|
|
220
|
+
# Send timeout duration for the idle timer as part of the response
|
|
221
|
+
config["idleTimeoutDuration"] = state.settings["mwi_idle_timeout"]
|
|
222
|
+
|
|
122
223
|
return web.json_response(config)
|
|
123
224
|
|
|
124
225
|
|
|
226
|
+
# @token_auth.authenticate_access_decorator
|
|
227
|
+
# Explicitly disabling authentication for this end-point,
|
|
228
|
+
# because the front end requires this endpoint to be available at all times.
|
|
229
|
+
@reset_timer_decorator
|
|
125
230
|
async def get_status(req):
|
|
126
231
|
"""API Endpoint to get the generic status of the server, MATLAB and MATLAB Licensing.
|
|
127
232
|
|
|
@@ -131,24 +236,51 @@ async def get_status(req):
|
|
|
131
236
|
Returns:
|
|
132
237
|
JSONResponse: JSONResponse object containing information about the server, MATLAB and MATLAB Licensing.
|
|
133
238
|
"""
|
|
134
|
-
|
|
239
|
+
# The client sends the CLIENT_ID as a query parameter if the concurrency check has been set to true.
|
|
240
|
+
state = req.app["state"]
|
|
241
|
+
client_id = req.query.get("MWI_CLIENT_ID", None)
|
|
242
|
+
transfer_session = json.loads(req.query.get("TRANSFER_SESSION", "false"))
|
|
243
|
+
is_desktop = req.query.get("IS_DESKTOP", False)
|
|
244
|
+
|
|
245
|
+
generated_client_id, is_active_client = state.get_session_status(
|
|
246
|
+
is_desktop, client_id, transfer_session
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
return create_status_response(
|
|
250
|
+
req.app, client_id=generated_client_id, is_active_client=is_active_client
|
|
251
|
+
)
|
|
135
252
|
|
|
136
253
|
|
|
137
|
-
|
|
254
|
+
# @token_auth.authenticate_access_decorator
|
|
255
|
+
# Explicitly disabling authentication for this end-point, as it checks for authenticity internally.
|
|
256
|
+
async def authenticate(req):
|
|
138
257
|
"""API Endpoint to authenticate request to access server
|
|
139
258
|
|
|
140
259
|
Returns:
|
|
141
|
-
|
|
260
|
+
JSONResponse: JSONResponse object containing information about authentication status and error if any.
|
|
142
261
|
"""
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
262
|
+
is_authenticated = await token_auth.authenticate_request(req)
|
|
263
|
+
error = (
|
|
264
|
+
None
|
|
265
|
+
if is_authenticated
|
|
266
|
+
else marshal_error(
|
|
267
|
+
InvalidTokenError(
|
|
268
|
+
"Token invalid. Please enter a valid token to authenticate"
|
|
269
|
+
)
|
|
270
|
+
)
|
|
271
|
+
)
|
|
272
|
+
# If there is an error, state.error is not updated because the client may have set the
|
|
273
|
+
# token incorrectly which is not an error raised on the backend.
|
|
274
|
+
|
|
275
|
+
return web.json_response(
|
|
276
|
+
{
|
|
277
|
+
"status": is_authenticated,
|
|
278
|
+
"error": error,
|
|
279
|
+
}
|
|
280
|
+
)
|
|
150
281
|
|
|
151
282
|
|
|
283
|
+
@token_auth.authenticate_access_decorator
|
|
152
284
|
async def start_matlab(req):
|
|
153
285
|
"""API Endpoint to start MATLAB
|
|
154
286
|
|
|
@@ -159,13 +291,18 @@ async def start_matlab(req):
|
|
|
159
291
|
JSONResponse: JSONResponse object containing updated information on the state of MATLAB among other information.
|
|
160
292
|
"""
|
|
161
293
|
state = req.app["state"]
|
|
294
|
+
cookie_jar = req.app["settings"]["cookie_jar"]
|
|
295
|
+
|
|
296
|
+
if cookie_jar:
|
|
297
|
+
cookie_jar.clear()
|
|
162
298
|
|
|
163
299
|
# Start MATLAB
|
|
164
300
|
await state.start_matlab(restart_matlab=True)
|
|
165
301
|
|
|
166
|
-
return
|
|
302
|
+
return create_status_response(req.app)
|
|
167
303
|
|
|
168
304
|
|
|
305
|
+
@token_auth.authenticate_access_decorator
|
|
169
306
|
async def stop_matlab(req):
|
|
170
307
|
"""API Endpoint to stop MATLAB
|
|
171
308
|
|
|
@@ -179,9 +316,10 @@ async def stop_matlab(req):
|
|
|
179
316
|
|
|
180
317
|
await state.stop_matlab()
|
|
181
318
|
|
|
182
|
-
return
|
|
319
|
+
return create_status_response(req.app)
|
|
183
320
|
|
|
184
321
|
|
|
322
|
+
@token_auth.authenticate_access_decorator
|
|
185
323
|
async def set_licensing_info(req):
|
|
186
324
|
"""API Endpoint to set licensing information on the server side.
|
|
187
325
|
|
|
@@ -201,27 +339,68 @@ async def set_licensing_info(req):
|
|
|
201
339
|
lic_type = data.get("type")
|
|
202
340
|
|
|
203
341
|
try:
|
|
204
|
-
if lic_type == "
|
|
342
|
+
if lic_type == "nlm":
|
|
205
343
|
await state.set_licensing_nlm(data.get("connectionString"))
|
|
206
344
|
|
|
207
|
-
elif lic_type == "
|
|
345
|
+
elif lic_type == "mhlm":
|
|
346
|
+
# If matlab version could not be determined on startup update
|
|
347
|
+
# the value received from the front-end.
|
|
348
|
+
if not state.settings.get(
|
|
349
|
+
"matlab_version_determined_on_startup"
|
|
350
|
+
) and data.get("matlabVersion"):
|
|
351
|
+
state.settings["matlab_version"] = data.get("matlabVersion")
|
|
352
|
+
|
|
208
353
|
await state.set_licensing_mhlm(
|
|
209
354
|
data.get("token"), data.get("emailAddress"), data.get("sourceId")
|
|
210
355
|
)
|
|
356
|
+
|
|
357
|
+
elif lic_type == "existing_license":
|
|
358
|
+
state.set_licensing_existing_license()
|
|
359
|
+
|
|
211
360
|
else:
|
|
212
|
-
raise Exception(
|
|
213
|
-
|
|
361
|
+
raise Exception(
|
|
362
|
+
'License type must be "NLM" or "MHLM" or "ExistingLicense"!'
|
|
363
|
+
)
|
|
364
|
+
except Exception:
|
|
214
365
|
raise web.HTTPBadRequest(text="Error with licensing!")
|
|
215
366
|
|
|
216
|
-
#
|
|
217
|
-
|
|
367
|
+
# This is true for a user who has only one license associated with their account
|
|
368
|
+
await __start_matlab_if_licensed(state)
|
|
218
369
|
|
|
219
|
-
|
|
220
|
-
await state.start_matlab(restart_matlab=True)
|
|
370
|
+
return create_status_response(req.app)
|
|
221
371
|
|
|
222
|
-
return await create_status_response(req.app)
|
|
223
372
|
|
|
373
|
+
@token_auth.authenticate_access_decorator
|
|
374
|
+
async def update_entitlement(req):
|
|
375
|
+
"""API endpoint to update selected entitlement to start MATLAB with.
|
|
224
376
|
|
|
377
|
+
Args:
|
|
378
|
+
req (HTTPRequest): HTTPRequest Object
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
JSONResponse: JSONResponse object containing updated information on the state of MATLAB among other information.
|
|
382
|
+
"""
|
|
383
|
+
state = req.app["state"]
|
|
384
|
+
data = await req.json()
|
|
385
|
+
lic_type = data.get("type")
|
|
386
|
+
|
|
387
|
+
# Set the entitlement id only if we are not already licensed
|
|
388
|
+
if lic_type == "mhlm" and not state.is_licensed():
|
|
389
|
+
entitlement_id = data.get("entitlement_id")
|
|
390
|
+
logger.debug(f"Received type: {lic_type}, entitlement_id: {entitlement_id}")
|
|
391
|
+
await state.update_user_selected_entitlement_info(entitlement_id)
|
|
392
|
+
await __start_matlab_if_licensed(state)
|
|
393
|
+
|
|
394
|
+
return create_status_response(req.app)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
async def __start_matlab_if_licensed(state):
|
|
398
|
+
# Start MATLAB if licensing is complete
|
|
399
|
+
if state.is_licensed() and not isinstance(state.error, LicensingError):
|
|
400
|
+
await state.start_matlab(restart_matlab=True)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
@token_auth.authenticate_access_decorator
|
|
225
404
|
async def licensing_info_delete(req):
|
|
226
405
|
"""API Endpoint to stop MATLAB and remove licensing details.
|
|
227
406
|
|
|
@@ -235,43 +414,70 @@ async def licensing_info_delete(req):
|
|
|
235
414
|
# Removing license information implies terminating MATLAB
|
|
236
415
|
await state.stop_matlab()
|
|
237
416
|
|
|
417
|
+
# When removing licensing data, if matlab version was fetched from the user, remove it
|
|
418
|
+
# on the server side to have a complete 'reset'.
|
|
419
|
+
if state.licensing["type"] == "mhlm" and not state.settings.get(
|
|
420
|
+
"matlab_version_determined_on_startup"
|
|
421
|
+
):
|
|
422
|
+
state.settings["matlab_version"] = None
|
|
423
|
+
|
|
238
424
|
# Unset licensing information
|
|
239
425
|
state.unset_licensing()
|
|
240
426
|
|
|
241
|
-
# Persist
|
|
242
|
-
state.
|
|
427
|
+
# Persist config information
|
|
428
|
+
state.persist_config_data()
|
|
243
429
|
|
|
244
|
-
return
|
|
430
|
+
return create_status_response(req.app)
|
|
245
431
|
|
|
246
432
|
|
|
247
|
-
|
|
248
|
-
|
|
433
|
+
@token_auth.authenticate_access_decorator
|
|
434
|
+
async def shutdown_integration_delete(req):
|
|
435
|
+
"""API Endpoint to shutdown the Integration
|
|
249
436
|
|
|
250
437
|
Args:
|
|
251
438
|
req (HTTPRequest): HTTPRequest Object
|
|
252
439
|
"""
|
|
253
|
-
logger.debug("Terminating the integration...")
|
|
254
440
|
state = req.app["state"]
|
|
441
|
+
if state.is_shutting_down:
|
|
442
|
+
logger.debug("Shutdown already in progress")
|
|
443
|
+
return create_status_response(req.app, "../")
|
|
255
444
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
await req.app.cleanup()
|
|
265
|
-
"""When testing with pytest, its not possible to catch sys.exit(0) using the construct
|
|
266
|
-
'with pytest.raises()', there by causing the test : test_termination_integration_delete()
|
|
267
|
-
to fail. Inorder to avoid this, adding the below if condition to check to skip sys.exit(0) when testing
|
|
268
|
-
"""
|
|
269
|
-
logger.debug("Exiting with return code 0")
|
|
270
|
-
if not mwi_env.is_testing_mode_enabled():
|
|
271
|
-
sys.exit(0)
|
|
445
|
+
logger.info(f"Shutting down {state.settings['integration_name']}...")
|
|
446
|
+
state.is_shutting_down = True
|
|
447
|
+
res = create_status_response(req.app, "../")
|
|
448
|
+
|
|
449
|
+
# Schedule the shutdown to happen after the response is sent
|
|
450
|
+
asyncio.create_task(_shutdown_after_response(req.app))
|
|
451
|
+
|
|
452
|
+
return res
|
|
272
453
|
|
|
273
454
|
|
|
274
|
-
|
|
455
|
+
async def _shutdown_after_response(app):
|
|
456
|
+
# Shutdown the application after a short delay to allow the response to be fully sent
|
|
457
|
+
# back to the client before the server is stopped
|
|
458
|
+
await asyncio.sleep(0.1)
|
|
459
|
+
|
|
460
|
+
# aiohttp shutdown to be invoked before cleanup -
|
|
461
|
+
# https://docs.aiohttp.org/en/stable/web_reference.html#aiohttp.web.Application.shutdown
|
|
462
|
+
await app.shutdown()
|
|
463
|
+
await app.cleanup()
|
|
464
|
+
|
|
465
|
+
loop = util.get_event_loop()
|
|
466
|
+
|
|
467
|
+
# Cancel remaining tasks (except this one: _shutdown_after_response)
|
|
468
|
+
running_tasks = asyncio.all_tasks(loop)
|
|
469
|
+
current_task = asyncio.current_task()
|
|
470
|
+
if current_task:
|
|
471
|
+
running_tasks.discard(current_task)
|
|
472
|
+
|
|
473
|
+
await util.cancel_tasks(running_tasks)
|
|
474
|
+
|
|
475
|
+
# Stop the event loop from this task
|
|
476
|
+
loop.call_soon_threadsafe(loop.stop)
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
# @token_auth.authenticate_access_decorator
|
|
480
|
+
# Explicitly disabling authentication for this end-point, as authenticity is checked by the redirected endpoint.
|
|
275
481
|
async def root_redirect(request):
|
|
276
482
|
"""API Endpoint to return the root index.html file.
|
|
277
483
|
|
|
@@ -282,7 +488,10 @@ async def root_redirect(request):
|
|
|
282
488
|
HTTPResponse: HTTPResponse Object containing the index.html file.
|
|
283
489
|
"""
|
|
284
490
|
base_url = request.app["settings"]["base_url"]
|
|
285
|
-
|
|
491
|
+
query_params = f"?{request.query_string}" if request.query_string else ""
|
|
492
|
+
response_url = f"{base_url}/index.html{query_params}"
|
|
493
|
+
|
|
494
|
+
return aiohttp.web.HTTPFound(response_url)
|
|
286
495
|
|
|
287
496
|
|
|
288
497
|
async def static_get(req):
|
|
@@ -313,7 +522,7 @@ def make_static_route_table(app):
|
|
|
313
522
|
Returns:
|
|
314
523
|
Dict: Containing information about the static files and header information.
|
|
315
524
|
"""
|
|
316
|
-
|
|
525
|
+
import importlib.resources as resources
|
|
317
526
|
|
|
318
527
|
from matlab_proxy import gui
|
|
319
528
|
from matlab_proxy.gui import static
|
|
@@ -323,17 +532,17 @@ def make_static_route_table(app):
|
|
|
323
532
|
|
|
324
533
|
table = {}
|
|
325
534
|
|
|
326
|
-
for
|
|
535
|
+
for mod, parent in [
|
|
327
536
|
(gui.__name__, ""),
|
|
328
|
-
(
|
|
329
|
-
(
|
|
330
|
-
(
|
|
331
|
-
(
|
|
537
|
+
(static.__name__, "/static"),
|
|
538
|
+
(css.__name__, "/static/css"),
|
|
539
|
+
(js.__name__, "/static/js"),
|
|
540
|
+
(media.__name__, "/static/media"),
|
|
332
541
|
]:
|
|
333
|
-
for
|
|
334
|
-
|
|
542
|
+
for entry in resources.files(mod).iterdir():
|
|
543
|
+
name = entry.name
|
|
544
|
+
if not resources.files(mod).joinpath(name).is_dir():
|
|
335
545
|
if name != "__init__.py":
|
|
336
|
-
|
|
337
546
|
# Special case for manifest.json
|
|
338
547
|
if "manifest.json" in name:
|
|
339
548
|
content_type = "application/manifest+json"
|
|
@@ -352,6 +561,7 @@ def make_static_route_table(app):
|
|
|
352
561
|
return table
|
|
353
562
|
|
|
354
563
|
|
|
564
|
+
@token_auth.authenticate_access_decorator
|
|
355
565
|
async def matlab_view(req):
|
|
356
566
|
"""API Endpoint which proxies requests to the MATLAB Embedded Connector
|
|
357
567
|
|
|
@@ -371,93 +581,232 @@ async def matlab_view(req):
|
|
|
371
581
|
matlab_port = state.matlab_port
|
|
372
582
|
matlab_protocol = req.app["settings"]["matlab_protocol"]
|
|
373
583
|
mwapikey = req.app["settings"]["mwapikey"]
|
|
374
|
-
matlab_base_url = f"{matlab_protocol}://
|
|
584
|
+
matlab_base_url = f"{matlab_protocol}://127.0.0.1:{matlab_port}"
|
|
585
|
+
cookie_jar = req.app["settings"]["cookie_jar"]
|
|
586
|
+
|
|
587
|
+
cookies_from_jar = cookie_jar.get_dict() if cookie_jar else None
|
|
588
|
+
|
|
589
|
+
# If we are trying to send request to matlab while the matlab_port is still not assigned
|
|
590
|
+
# by embedded connector, return service not available and log a message
|
|
591
|
+
if not matlab_port:
|
|
592
|
+
logger.debug(
|
|
593
|
+
"MATLAB hasn't fully started, please retry after embedded connector has started"
|
|
594
|
+
)
|
|
595
|
+
raise web.HTTPServiceUnavailable()
|
|
375
596
|
|
|
376
597
|
# WebSocket
|
|
377
|
-
if (
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
and reqH.get("upgrade").lower() == "websocket"
|
|
382
|
-
and req.method == "GET"
|
|
383
|
-
):
|
|
384
|
-
ws_server = web.WebSocketResponse()
|
|
598
|
+
if _is_websocket_upgrade_request(req.method, reqH):
|
|
599
|
+
ws_server = web.WebSocketResponse(
|
|
600
|
+
max_msg_size=constants.MAX_WEBSOCKET_MESSAGE_SIZE_IN_MB, compress=True
|
|
601
|
+
)
|
|
385
602
|
await ws_server.prepare(req)
|
|
386
603
|
|
|
387
604
|
async with aiohttp.ClientSession(
|
|
388
|
-
cookies=
|
|
605
|
+
cookies=(
|
|
606
|
+
cookies_from_jar if cookie_jar else req.cookies
|
|
607
|
+
), # If cookie jar is not provided, use the cookies from the incoming request
|
|
608
|
+
trust_env=True,
|
|
609
|
+
connector=aiohttp.TCPConnector(ssl=False),
|
|
389
610
|
) as client_session:
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
611
|
+
try:
|
|
612
|
+
async with client_session.ws_connect(
|
|
613
|
+
matlab_base_url + req.path_qs,
|
|
614
|
+
max_msg_size=constants.MAX_WEBSOCKET_MESSAGE_SIZE_IN_MB, # max websocket message size from MATLAB to browser
|
|
615
|
+
compress=12, # enable websocket messages compression
|
|
616
|
+
) as ws_client:
|
|
617
|
+
|
|
618
|
+
async def wsforward(ws_from, ws_to):
|
|
619
|
+
async for msg in ws_from:
|
|
620
|
+
mt = msg.type
|
|
621
|
+
md = msg.data
|
|
622
|
+
|
|
623
|
+
# When a websocket is closed by the MATLAB JSD, it sends out a few http requests to the Embedded Connector about the events
|
|
624
|
+
# that had occured (figureWindowClosed etc.)
|
|
625
|
+
# The Embedded Connector responds by sending a message of type 'Error' with close code as Abnormal closure.
|
|
626
|
+
# When this happens, matlab-proxy can safely exit out of the loop
|
|
627
|
+
# and close the websocket connection it has with the Embedded Connector (ws_client)
|
|
628
|
+
if (
|
|
629
|
+
mt == aiohttp.WSMsgType.ERROR
|
|
630
|
+
and ws_from.close_code
|
|
631
|
+
== aiohttp.WSCloseCode.ABNORMAL_CLOSURE
|
|
632
|
+
):
|
|
633
|
+
break
|
|
634
|
+
|
|
635
|
+
if mt == aiohttp.WSMsgType.TEXT:
|
|
636
|
+
await ws_to.send_str(md)
|
|
637
|
+
elif mt == aiohttp.WSMsgType.BINARY:
|
|
638
|
+
await ws_to.send_bytes(md)
|
|
639
|
+
elif mt == aiohttp.WSMsgType.PING:
|
|
640
|
+
await ws_to.ping()
|
|
641
|
+
elif mt == aiohttp.WSMsgType.PONG:
|
|
642
|
+
await ws_to.pong()
|
|
643
|
+
elif ws_to.closed:
|
|
644
|
+
await ws_to.close(
|
|
645
|
+
code=ws_to.close_code, message=msg.extra
|
|
646
|
+
)
|
|
647
|
+
elif mt == aiohttp.WSMsgType.ERROR:
|
|
648
|
+
logger.error(f"WebSocket error received: {msg}")
|
|
649
|
+
if "exceeds limit" in str(msg.data):
|
|
650
|
+
logger.error(
|
|
651
|
+
f"Message too large: {msg.data}. Please refresh browser tab to reconnect."
|
|
652
|
+
)
|
|
653
|
+
break
|
|
654
|
+
else:
|
|
655
|
+
raise ValueError(f"Unexpected message type: {msg}")
|
|
656
|
+
|
|
657
|
+
await asyncio.wait(
|
|
658
|
+
[
|
|
659
|
+
asyncio.create_task(
|
|
660
|
+
wsforward(ws_server, ws_client)
|
|
661
|
+
), # browser to MATLAB
|
|
662
|
+
asyncio.create_task(
|
|
663
|
+
wsforward(ws_client, ws_server)
|
|
664
|
+
), # MATLAB to browser
|
|
665
|
+
],
|
|
666
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
return ws_server
|
|
670
|
+
|
|
671
|
+
except Exception as err:
|
|
672
|
+
logger.error(
|
|
673
|
+
f"Failed to create web socket connection with error: {err}"
|
|
428
674
|
)
|
|
429
675
|
|
|
430
|
-
|
|
676
|
+
code, message = (
|
|
677
|
+
aiohttp.WSCloseCode.INTERNAL_ERROR,
|
|
678
|
+
"Failed to establish websocket connection with MATLAB",
|
|
679
|
+
)
|
|
680
|
+
await ws_server.close(code=code, message=message.encode("utf-8"))
|
|
681
|
+
raise aiohttp.WebSocketError(code=code, message=message)
|
|
431
682
|
|
|
432
683
|
# Standard HTTP Request
|
|
433
684
|
else:
|
|
434
|
-
# Proxy, injecting request header
|
|
685
|
+
# Proxy, injecting request header, disabling request timeouts
|
|
686
|
+
timeout = aiohttp.ClientTimeout(total=None)
|
|
435
687
|
async with aiohttp.ClientSession(
|
|
436
|
-
|
|
688
|
+
trust_env=True,
|
|
689
|
+
connector=aiohttp.TCPConnector(ssl=False),
|
|
690
|
+
timeout=timeout,
|
|
437
691
|
) as client_session:
|
|
438
692
|
try:
|
|
439
693
|
req_body = await transform_body(req)
|
|
694
|
+
req_url = await transform_request_url(
|
|
695
|
+
req, matlab_base_url=matlab_base_url
|
|
696
|
+
)
|
|
440
697
|
# Set content length in case of modification
|
|
441
698
|
reqH["Content-Length"] = str(len(req_body))
|
|
442
699
|
reqH["x-forwarded-proto"] = "http"
|
|
443
700
|
|
|
444
701
|
async with client_session.request(
|
|
445
702
|
req.method,
|
|
446
|
-
|
|
703
|
+
req_url,
|
|
447
704
|
headers={**reqH, **{"mwapikey": mwapikey}},
|
|
448
705
|
allow_redirects=False,
|
|
449
706
|
data=req_body,
|
|
707
|
+
params=None,
|
|
708
|
+
cookies=cookies_from_jar, # Pass cookies from cookie_jar for HTTP requests to MATLAB. This value will
|
|
709
|
+
# be none if cookie jar is not enabled
|
|
450
710
|
) as res:
|
|
451
|
-
|
|
452
711
|
headers = res.headers.copy()
|
|
453
712
|
body = await res.read()
|
|
454
|
-
headers.update(req.app["settings"]["mwi_custom_http_headers"])
|
|
455
713
|
|
|
456
|
-
|
|
457
|
-
|
|
714
|
+
response = web.Response(
|
|
715
|
+
status=res.status, headers=headers, body=body
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
# Purpose of the cookie-jar in matlab-proxy is to:
|
|
719
|
+
# 1) Update the cookies within it when the Embedded Connector sends back Set-Cookie headers in the response.
|
|
720
|
+
# 2) Read these cookies from the cookie jar and insert them into subsequent requests to the Embedded Connector.
|
|
721
|
+
|
|
722
|
+
# Due to matlab-proxy's PING requests to EC, the number cookies present in the cookie-jar and their
|
|
723
|
+
# value will be more than the ones present on the browser side.
|
|
724
|
+
# Example: The JSESSIONID cookie will be present in the cookie-jar but not on the browser side.
|
|
725
|
+
# This inconsistency of cookies between the browser and matlab-proxy's cookie-jar is expected and okay
|
|
726
|
+
# as these cookies are HttpOnly cookies.
|
|
727
|
+
|
|
728
|
+
# Incase the Embedded Connector sends cookies which are not HttpOnly, then additional logic needs to be written
|
|
729
|
+
# to update the response with cookies from the cookie jar before it is forwarded to the browser.
|
|
730
|
+
if cookie_jar:
|
|
731
|
+
# Update the cookies in the cookie jar with the Set-Cookie headers in the response.
|
|
732
|
+
cookie_jar.update_from_response_headers(headers)
|
|
733
|
+
|
|
734
|
+
response.headers.update(
|
|
735
|
+
req.app["settings"]["mwi_custom_http_headers"]
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
return response
|
|
739
|
+
|
|
740
|
+
# Handles any pending HTTP requests from the browser when the MATLAB process is terminated before responding to them.
|
|
741
|
+
except (
|
|
742
|
+
client_exceptions.ServerDisconnectedError,
|
|
743
|
+
client_exceptions.ClientConnectionError,
|
|
744
|
+
):
|
|
745
|
+
logger.debug(
|
|
746
|
+
"Failed to forward HTTP request as MATLAB process may not be running."
|
|
747
|
+
)
|
|
748
|
+
raise web.HTTPServiceUnavailable()
|
|
749
|
+
|
|
750
|
+
# Some other exception has been raised (by MATLAB Embedded Connector), log the error and return 404
|
|
751
|
+
except Exception as err:
|
|
752
|
+
logger.error(
|
|
753
|
+
f"Failed to forward HTTP request to MATLAB with error: {err}"
|
|
754
|
+
)
|
|
458
755
|
raise web.HTTPNotFound()
|
|
459
756
|
|
|
460
757
|
|
|
758
|
+
def _is_websocket_upgrade_request(request_method, request_headers):
|
|
759
|
+
"""Check if the request is a WebSocket upgrade request.
|
|
760
|
+
|
|
761
|
+
Args:
|
|
762
|
+
method (str): The HTTP method
|
|
763
|
+
headers (dict): The request headers
|
|
764
|
+
|
|
765
|
+
Returns:
|
|
766
|
+
bool: True if the request is a WebSocket upgrade request, False otherwise
|
|
767
|
+
"""
|
|
768
|
+
# According to RFC6455 (https://www.rfc-editor.org/rfc/rfc6455.html)
|
|
769
|
+
# the values of 'connection' and 'upgrade' keys of request header
|
|
770
|
+
# should be ASCII case-insensitive matches.
|
|
771
|
+
#
|
|
772
|
+
# Different browsers may send different values for connection header, so we check if literal 'upgrade'
|
|
773
|
+
# is present in the header value. E.g. Firefox sets connection header to "keep-alive, Upgrade"
|
|
774
|
+
# while Chrome sets it to "Upgrade".
|
|
775
|
+
return (
|
|
776
|
+
"upgrade" in request_headers.get("connection", "").lower()
|
|
777
|
+
and request_headers.get("upgrade", "").lower() == "websocket"
|
|
778
|
+
and request_method == "GET"
|
|
779
|
+
)
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
async def transform_request_url(req, matlab_base_url):
|
|
783
|
+
"""
|
|
784
|
+
Performs any transformations that may be required on the URL.
|
|
785
|
+
|
|
786
|
+
If the request is identified as a download request it transforms the request URL to
|
|
787
|
+
support downloading the file.
|
|
788
|
+
|
|
789
|
+
The original constructed URL is returned, when there are no transformations to be applied.
|
|
790
|
+
|
|
791
|
+
Args:
|
|
792
|
+
req: The request object that contains the relative URL and other request information.
|
|
793
|
+
matlab_base_url: The base URL of the MATLAB service to which the relative URL should be appended.
|
|
794
|
+
|
|
795
|
+
Returns:
|
|
796
|
+
A string representing the transformed URL, or the original URL.
|
|
797
|
+
"""
|
|
798
|
+
original_request_url = f"{matlab_base_url}{req.rel_url}"
|
|
799
|
+
|
|
800
|
+
if download.is_download_request(req):
|
|
801
|
+
download_url = await download.get_download_url(req)
|
|
802
|
+
if download_url:
|
|
803
|
+
transformed_request_url = f"{matlab_base_url}{download_url}"
|
|
804
|
+
logger.debug(f"Transformed Request url: {transformed_request_url}")
|
|
805
|
+
return transformed_request_url
|
|
806
|
+
|
|
807
|
+
return original_request_url
|
|
808
|
+
|
|
809
|
+
|
|
461
810
|
async def transform_body(req):
|
|
462
811
|
"""Transform HTTP POST requests as required by the MATLAB JavaScript Desktop.
|
|
463
812
|
|
|
@@ -513,7 +862,7 @@ async def matlab_starter(app):
|
|
|
513
862
|
state = app["state"]
|
|
514
863
|
|
|
515
864
|
try:
|
|
516
|
-
if state.is_licensed() and
|
|
865
|
+
if state.is_licensed() and state.get_matlab_state() == "down":
|
|
517
866
|
await state.start_matlab()
|
|
518
867
|
except asyncio.CancelledError:
|
|
519
868
|
# Ensure MATLAB is terminated
|
|
@@ -544,38 +893,9 @@ async def cleanup_background_tasks(app):
|
|
|
544
893
|
|
|
545
894
|
await state.stop_matlab(force_quit=True)
|
|
546
895
|
|
|
547
|
-
#
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
for task_name, task in tasks.items():
|
|
551
|
-
if not task.cancelled():
|
|
552
|
-
logger.debug(f"Cancelling MWI task: {task_name} : {task} ")
|
|
553
|
-
task.cancel()
|
|
554
|
-
try:
|
|
555
|
-
await task
|
|
556
|
-
except asyncio.CancelledError:
|
|
557
|
-
pass
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
@token_auth.decorator_authenticate_access
|
|
561
|
-
async def get_mwi_auth_token(request):
|
|
562
|
-
"""Endpoint to print the MWI token."""
|
|
563
|
-
logger.info("!!!!!! Inside get_mwi_auth_token !!!!!")
|
|
564
|
-
app_settings = request.app["settings"]
|
|
565
|
-
is_mwi_token_auth_enabled = app_settings["mwi_is_mwi_token_auth_enabled"]
|
|
566
|
-
base_url = app_settings["base_url"]
|
|
567
|
-
mwi_auth_token = app_settings["mwi_auth_token"]
|
|
568
|
-
if is_mwi_token_auth_enabled:
|
|
569
|
-
logger.info("get_mwi_auth_token: Responding with token information!!")
|
|
570
|
-
return aiohttp.web.HTTPFound(
|
|
571
|
-
f"{base_url}/token.html?mwi_auth_token={mwi_auth_token}"
|
|
572
|
-
)
|
|
573
|
-
else:
|
|
574
|
-
logger.info("get_mwi_auth_token: Token Auth mode not enabled")
|
|
575
|
-
return aiohttp.web.Response(
|
|
576
|
-
content_type="text/html",
|
|
577
|
-
body=f"Token-Based Authentication is not enabled!",
|
|
578
|
-
)
|
|
896
|
+
# Cleanup server tasks
|
|
897
|
+
server_tasks = state.server_tasks
|
|
898
|
+
await util.cancel_tasks(server_tasks)
|
|
579
899
|
|
|
580
900
|
|
|
581
901
|
def configure_and_start(app):
|
|
@@ -589,10 +909,22 @@ def configure_and_start(app):
|
|
|
589
909
|
"""
|
|
590
910
|
loop = util.get_event_loop()
|
|
591
911
|
|
|
592
|
-
|
|
912
|
+
# Setup the session storage,
|
|
913
|
+
# Uniqified per session to prevent multiple proxy servers on the same FQDN from interfering with each other.
|
|
914
|
+
uniqify_session_cookie = secrets.token_hex()
|
|
915
|
+
fernet_key = fernet.Fernet.generate_key()
|
|
916
|
+
f = fernet.Fernet(fernet_key)
|
|
917
|
+
aiohttp_session_setup(
|
|
918
|
+
app,
|
|
919
|
+
EncryptedCookieStorage(
|
|
920
|
+
f, cookie_name="matlab-proxy-session-" + uniqify_session_cookie
|
|
921
|
+
),
|
|
922
|
+
)
|
|
593
923
|
|
|
594
924
|
# Setup runner
|
|
595
|
-
runner = web.AppRunner(
|
|
925
|
+
runner = web.AppRunner(
|
|
926
|
+
app, access_log=logger if mwi_env.is_web_logging_enabled() else None
|
|
927
|
+
)
|
|
596
928
|
loop.run_until_complete(runner.setup())
|
|
597
929
|
|
|
598
930
|
# Prepare site to start, then set port of the app.
|
|
@@ -609,7 +941,7 @@ def configure_and_start(app):
|
|
|
609
941
|
|
|
610
942
|
logger.debug("Starting MATLAB proxy app")
|
|
611
943
|
logger.debug(
|
|
612
|
-
f
|
|
944
|
+
f" with base_url: {app['settings']['base_url']} and app_port:{app['settings']['app_port']}."
|
|
613
945
|
)
|
|
614
946
|
|
|
615
947
|
app["state"].create_server_info_file()
|
|
@@ -628,7 +960,7 @@ def create_app(config_name=matlab_proxy.get_default_config_name()):
|
|
|
628
960
|
Returns:
|
|
629
961
|
aiohttp server: An aiohttp server with routes, settings and env_config.
|
|
630
962
|
"""
|
|
631
|
-
app = web.Application()
|
|
963
|
+
app = web.Application(client_max_size=constants.MAX_HTTP_REQUEST_SIZE)
|
|
632
964
|
|
|
633
965
|
# Get application settings
|
|
634
966
|
app["settings"] = settings.get(
|
|
@@ -647,46 +979,59 @@ def create_app(config_name=matlab_proxy.get_default_config_name()):
|
|
|
647
979
|
|
|
648
980
|
base_url = app["settings"]["base_url"]
|
|
649
981
|
app.router.add_route("GET", f"{base_url}/get_status", get_status)
|
|
650
|
-
app.router.add_route(
|
|
651
|
-
|
|
652
|
-
)
|
|
982
|
+
app.router.add_route("POST", f"{base_url}/authenticate", authenticate)
|
|
983
|
+
app.router.add_route("GET", f"{base_url}/get_auth_token", get_auth_token)
|
|
653
984
|
app.router.add_route("GET", f"{base_url}/get_env_config", get_env_config)
|
|
654
985
|
app.router.add_route("PUT", f"{base_url}/start_matlab", start_matlab)
|
|
986
|
+
app.router.add_route("POST", f"{base_url}/clear_client_id", clear_client_id)
|
|
655
987
|
app.router.add_route("DELETE", f"{base_url}/stop_matlab", stop_matlab)
|
|
656
988
|
app.router.add_route("PUT", f"{base_url}/set_licensing_info", set_licensing_info)
|
|
989
|
+
app.router.add_route("PUT", f"{base_url}/update_entitlement", update_entitlement)
|
|
657
990
|
app.router.add_route(
|
|
658
991
|
"DELETE", f"{base_url}/set_licensing_info", licensing_info_delete
|
|
659
992
|
)
|
|
660
993
|
app.router.add_route(
|
|
661
|
-
"DELETE", f"{base_url}/
|
|
994
|
+
"DELETE", f"{base_url}/shutdown_integration", shutdown_integration_delete
|
|
662
995
|
)
|
|
663
996
|
app.router.add_route("*", f"{base_url}/", root_redirect)
|
|
664
997
|
app.router.add_route("*", f"{base_url}", root_redirect)
|
|
665
|
-
mwi_auth_token_name = app["settings"]["mwi_auth_token_name"]
|
|
666
|
-
app.router.add_route("GET", f"{base_url}/get_mwi_auth_token", get_mwi_auth_token)
|
|
667
998
|
|
|
668
999
|
app.router.add_route("*", f"{base_url}/{{proxyPath:.*}}", matlab_view)
|
|
669
|
-
app.
|
|
670
|
-
|
|
671
|
-
# Setup the session storage
|
|
672
|
-
fernet_key = fernet.Fernet.generate_key()
|
|
673
|
-
f = fernet.Fernet(fernet_key)
|
|
674
|
-
aiohttp_session_setup(
|
|
675
|
-
app, EncryptedCookieStorage(f, cookie_name="matlab-proxy-session")
|
|
676
|
-
)
|
|
1000
|
+
app.on_shutdown.append(cleanup_background_tasks)
|
|
677
1001
|
|
|
678
1002
|
return app
|
|
679
1003
|
|
|
680
1004
|
|
|
681
|
-
def
|
|
682
|
-
"""
|
|
1005
|
+
def configure_no_proxy_in_env():
|
|
1006
|
+
"""Update the environment variable no_proxy to allow communication between processes on the local machine."""
|
|
1007
|
+
import os
|
|
683
1008
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
1009
|
+
no_proxy_whitelist = ["0.0.0.0", "localhost", "127.0.0.1"]
|
|
1010
|
+
|
|
1011
|
+
no_proxy_env = os.environ.get("no_proxy")
|
|
1012
|
+
if no_proxy_env is None:
|
|
1013
|
+
os.environ["no_proxy"] = ",".join(no_proxy_whitelist)
|
|
1014
|
+
else:
|
|
1015
|
+
# Create set with leading and trailing whitespaces stripped
|
|
1016
|
+
existing_no_proxy_env = [
|
|
1017
|
+
val.lstrip().rstrip() for val in no_proxy_env.split(",")
|
|
1018
|
+
]
|
|
1019
|
+
os.environ["no_proxy"] = ",".join(
|
|
1020
|
+
set(existing_no_proxy_env + no_proxy_whitelist)
|
|
1021
|
+
)
|
|
1022
|
+
logger.debug(f"Setting no_proxy to: {os.environ.get('no_proxy')}")
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
def create_and_start_app(config_name):
|
|
1026
|
+
"""Creates and start the web server. Will block until the server is interrupted or is shut down
|
|
1027
|
+
|
|
1028
|
+
Args:
|
|
1029
|
+
config_name (str): Name of the configuration to use with matlab-proxy.
|
|
1030
|
+
"""
|
|
1031
|
+
configure_no_proxy_in_env()
|
|
687
1032
|
|
|
688
1033
|
# Create, configure and start the app.
|
|
689
|
-
app = create_app(config_name
|
|
1034
|
+
app = create_app(config_name)
|
|
690
1035
|
app = configure_and_start(app)
|
|
691
1036
|
|
|
692
1037
|
loop = util.get_event_loop()
|
|
@@ -694,25 +1039,53 @@ def main():
|
|
|
694
1039
|
# Add signal handlers for the current python process
|
|
695
1040
|
loop = util.add_signal_handlers(loop)
|
|
696
1041
|
try:
|
|
1042
|
+
# Further execution is stopped here until an interrupt is raised
|
|
697
1043
|
loop.run_forever()
|
|
1044
|
+
|
|
698
1045
|
except SystemExit:
|
|
699
1046
|
pass
|
|
700
1047
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
1048
|
+
# After handling the interrupt, proceed with shutting down the server gracefully.
|
|
1049
|
+
try:
|
|
1050
|
+
# aiohttp shutdown to be invoked before cleanup -
|
|
1051
|
+
# https://docs.aiohttp.org/en/stable/web_reference.html#aiohttp.web.Application.shutdown
|
|
1052
|
+
loop.run_until_complete(app.shutdown())
|
|
1053
|
+
loop.run_until_complete(app.cleanup())
|
|
704
1054
|
|
|
705
|
-
|
|
706
|
-
await app.cleanup()
|
|
1055
|
+
running_tasks = asyncio.all_tasks(loop)
|
|
707
1056
|
|
|
708
|
-
#
|
|
709
|
-
|
|
1057
|
+
# Gracefully cancel all running background tasks
|
|
1058
|
+
loop.run_until_complete(util.cancel_tasks(running_tasks))
|
|
710
1059
|
|
|
711
|
-
|
|
712
|
-
loop.run_until_complete(shutdown())
|
|
713
|
-
except:
|
|
1060
|
+
except Exception:
|
|
714
1061
|
pass
|
|
715
1062
|
|
|
716
1063
|
logger.info("Finished shutting down. Thank you for using the MATLAB proxy.")
|
|
717
1064
|
loop.close()
|
|
718
1065
|
sys.exit(0)
|
|
1066
|
+
|
|
1067
|
+
|
|
1068
|
+
def print_version_and_exit():
|
|
1069
|
+
"""prints the version of the package and exits"""
|
|
1070
|
+
from importlib.metadata import version
|
|
1071
|
+
|
|
1072
|
+
matlab_proxy_version = version(__name__.split(".")[0])
|
|
1073
|
+
print(f"{matlab_proxy_version}")
|
|
1074
|
+
sys.exit(0)
|
|
1075
|
+
|
|
1076
|
+
|
|
1077
|
+
def main():
|
|
1078
|
+
"""Starting point of the integration. Creates the web app and runs indefinitely."""
|
|
1079
|
+
if util.parse_main_cli_args()["version"]:
|
|
1080
|
+
print_version_and_exit()
|
|
1081
|
+
|
|
1082
|
+
# The integration needs to be called with --config flag.
|
|
1083
|
+
# Parse the passed cli arguments.
|
|
1084
|
+
desired_configuration_name = util.parse_main_cli_args()["config"]
|
|
1085
|
+
|
|
1086
|
+
create_and_start_app(config_name=desired_configuration_name)
|
|
1087
|
+
|
|
1088
|
+
|
|
1089
|
+
# In support of enabling debugging in a Python Debugger (VSCode)
|
|
1090
|
+
if __name__ == "__main__":
|
|
1091
|
+
main()
|