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.
Files changed (104) hide show
  1. matlab_proxy/app.py +578 -205
  2. matlab_proxy/app_state.py +1061 -431
  3. matlab_proxy/constants.py +37 -0
  4. matlab_proxy/default_configuration.py +39 -4
  5. matlab_proxy/devel.py +18 -22
  6. matlab_proxy/gui/index.html +20 -1
  7. matlab_proxy/gui/static/css/index.BedVwcEg.css +10 -0
  8. matlab_proxy/gui/static/js/index.pQwV1obF.js +64 -0
  9. matlab_proxy/gui/static/media/MATLAB-env-blur.NupTbPv_.png +0 -0
  10. matlab_proxy/matlab/evaluateUserMatlabCode.m +51 -0
  11. matlab_proxy/matlab/startup.m +3 -28
  12. matlab_proxy/settings.py +543 -112
  13. matlab_proxy/util/__init__.py +187 -59
  14. matlab_proxy/util/cookie_jar.py +72 -0
  15. matlab_proxy/util/event_loop.py +28 -10
  16. matlab_proxy/util/list_servers.py +71 -26
  17. matlab_proxy/util/mw.py +16 -15
  18. matlab_proxy/util/mwi/download.py +136 -0
  19. matlab_proxy/util/mwi/embedded_connector/__init__.py +1 -1
  20. matlab_proxy/util/mwi/embedded_connector/helpers.py +12 -4
  21. matlab_proxy/util/mwi/embedded_connector/request.py +78 -12
  22. matlab_proxy/util/mwi/environment_variables.py +120 -27
  23. matlab_proxy/util/mwi/exceptions.py +63 -9
  24. matlab_proxy/util/mwi/logger.py +141 -27
  25. matlab_proxy/util/mwi/session_name.py +28 -0
  26. matlab_proxy/util/mwi/token_auth.py +264 -121
  27. matlab_proxy/util/mwi/validators.py +231 -88
  28. matlab_proxy/util/system.py +9 -0
  29. matlab_proxy/util/windows.py +32 -6
  30. {matlab_proxy-0.5.3.dist-info → matlab_proxy-0.30.1.dist-info}/METADATA +94 -49
  31. matlab_proxy-0.30.1.dist-info/RECORD +88 -0
  32. {matlab_proxy-0.5.3.dist-info → matlab_proxy-0.30.1.dist-info}/WHEEL +1 -2
  33. {matlab_proxy-0.5.3.dist-info → matlab_proxy-0.30.1.dist-info}/entry_points.txt +1 -1
  34. matlab_proxy_manager/README.md +85 -0
  35. matlab_proxy_manager/__init__.py +6 -0
  36. matlab_proxy_manager/lib/README.md +53 -0
  37. matlab_proxy_manager/lib/__init__.py +1 -0
  38. matlab_proxy_manager/lib/api.py +419 -0
  39. matlab_proxy_manager/storage/README.md +54 -0
  40. matlab_proxy_manager/storage/__init__.py +1 -0
  41. matlab_proxy_manager/storage/file_repository.py +144 -0
  42. matlab_proxy_manager/storage/interface.py +62 -0
  43. matlab_proxy_manager/storage/server.py +172 -0
  44. matlab_proxy_manager/utils/__init__.py +1 -0
  45. matlab_proxy_manager/utils/auth.py +77 -0
  46. matlab_proxy_manager/utils/constants.py +8 -0
  47. matlab_proxy_manager/utils/decorators.py +37 -0
  48. matlab_proxy_manager/utils/environment_variables.py +51 -0
  49. matlab_proxy_manager/utils/exceptions.py +45 -0
  50. matlab_proxy_manager/utils/helpers.py +314 -0
  51. matlab_proxy_manager/utils/logger.py +76 -0
  52. matlab_proxy_manager/web/README.md +37 -0
  53. matlab_proxy_manager/web/__init__.py +1 -0
  54. matlab_proxy_manager/web/app.py +536 -0
  55. matlab_proxy_manager/web/monitor.py +45 -0
  56. matlab_proxy_manager/web/watcher.py +65 -0
  57. matlab_proxy/gui/asset-manifest.json +0 -23
  58. matlab_proxy/gui/authorization.html +0 -115
  59. matlab_proxy/gui/bootstrap.3.4.1.min.css +0 -6
  60. matlab_proxy/gui/navbar.css +0 -8
  61. matlab_proxy/gui/signin.css +0 -42
  62. matlab_proxy/gui/static/css/main.d890078a.chunk.css +0 -13
  63. matlab_proxy/gui/static/css/main.d890078a.chunk.css.map +0 -1
  64. matlab_proxy/gui/static/js/2.13be6544.chunk.js +0 -3
  65. matlab_proxy/gui/static/js/2.13be6544.chunk.js.LICENSE.txt +0 -59
  66. matlab_proxy/gui/static/js/2.13be6544.chunk.js.map +0 -1
  67. matlab_proxy/gui/static/js/main.c311d854.chunk.js +0 -2
  68. matlab_proxy/gui/static/js/main.c311d854.chunk.js.map +0 -1
  69. matlab_proxy/gui/static/js/runtime-main.f70e4d5f.js +0 -2
  70. matlab_proxy/gui/static/js/runtime-main.f70e4d5f.js.map +0 -1
  71. matlab_proxy/gui/static/media/arrow.0c2968b9.svg +0 -4
  72. matlab_proxy/gui/static/media/feedback.6e8d50eb.svg +0 -1
  73. matlab_proxy/gui/static/media/gripper.9defbc5e.svg +0 -1
  74. matlab_proxy/gui/static/media/help.15e5bfab.svg +0 -1
  75. matlab_proxy/gui/static/media/ico-header-contact-hover.0958c442.svg +0 -17
  76. matlab_proxy/gui/static/media/ico-header-contact.ae9169c8.svg +0 -17
  77. matlab_proxy/gui/static/media/restart.7987508a.svg +0 -1
  78. matlab_proxy/gui/static/media/sign-out.08356b67.svg +0 -1
  79. matlab_proxy/gui/static/media/start.50c4596f.svg +0 -1
  80. matlab_proxy/gui/static/media/stop.30c9a9ab.svg +0 -1
  81. matlab_proxy/gui/static/media/terminate.7ea1363e.svg +0 -1
  82. matlab_proxy/gui/token.html +0 -123
  83. matlab_proxy-0.5.3.dist-info/RECORD +0 -84
  84. matlab_proxy-0.5.3.dist-info/top_level.txt +0 -1
  85. /matlab_proxy/gui/static/media/{glyphicons-halflings-regular.82b1212e.woff → glyphicons-halflings-regular.BKjkU69z.woff} +0 -0
  86. /matlab_proxy/gui/static/media/{glyphicons-halflings-regular.5be1347c.eot → glyphicons-halflings-regular.BUJKDMgK.eot} +0 -0
  87. /matlab_proxy/gui/static/media/{glyphicons-halflings-regular.060b2710.svg → glyphicons-halflings-regular.CSehLiBc.svg} +0 -0
  88. /matlab_proxy/gui/static/media/{glyphicons-halflings-regular.4692b9ec.ttf → glyphicons-halflings-regular.DrwTMapi.ttf} +0 -0
  89. /matlab_proxy/gui/static/media/{glyphicons-halflings-regular.be810be3.woff2 → glyphicons-halflings-regular.DzqM6ju8.woff2} +0 -0
  90. /matlab_proxy/gui/static/media/{ico-header-account-hover.89438e91.svg → ico-header-account-hover.-jQHo6Wx.svg} +0 -0
  91. /matlab_proxy/gui/static/media/{ico-header-account.86b10d7b.svg → ico-header-account.CJCFoo5a.svg} +0 -0
  92. /matlab_proxy/gui/static/media/{ico-sprite.cbdb66c0.png → ico-sprite.DXGLgzq9.png} +0 -0
  93. /matlab_proxy/gui/static/media/{mathworks-eps.4d20e0ee.ttf → mathworks-eps.CGNQALa9.ttf} +0 -0
  94. /matlab_proxy/gui/static/media/{mathworks-eps.df1428df.svg → mathworks-eps.DrkCtQtG.svg} +0 -0
  95. /matlab_proxy/gui/static/media/{mathworks-eps.e5c41e84.woff → mathworks-eps.Ds7lQbql.woff} +0 -0
  96. /matlab_proxy/gui/static/media/{mathworks-pictograms.3fc6513a.woff → mathworks-pictograms.BdqxEfBR.woff} +0 -0
  97. /matlab_proxy/gui/static/media/{mathworks-pictograms.f6f087b0.svg → mathworks-pictograms.CCLweoD4.svg} +0 -0
  98. /matlab_proxy/gui/static/media/{mathworks-pictograms.6e128c0e.ttf → mathworks-pictograms.DZhFdRSm.ttf} +0 -0
  99. /matlab_proxy/gui/static/media/{mathworks.80a3218e.svg → mathworks.C-qsbhDy.svg} +0 -0
  100. /matlab_proxy/gui/static/media/{mathworks.c422935b.ttf → mathworks.Ceplx86V.ttf} +0 -0
  101. /matlab_proxy/gui/static/media/{mathworks.37a563ef.woff → mathworks.D08X1Vp8.woff} +0 -0
  102. /matlab_proxy/gui/static/media/{trigger-error.3f1c4ef2.svg → trigger-error.QEdsGL-m.svg} +0 -0
  103. /matlab_proxy/gui/static/media/{trigger-ok.7b9c238b.svg → trigger-ok.Dzg8OIrk.svg} +0 -0
  104. {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 (c) 2020-2022 The MathWorks, Inc.
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.default_configuration import config
19
- from matlab_proxy.util import list_servers, mwi
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 token_auth
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
- if licensing_info["type"] == "mhlm":
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": "MHLM",
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 licensing_info["type"] == "nlm":
72
+ elif licensing_type == "nlm":
62
73
  return {
63
- "type": "NLM",
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
- return {
80
- "message": error.message,
81
- "logs": error.logs,
82
- "type": error.__class__.__name__,
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
- async def create_status_response(app, loadUrl=None):
87
- """Send a generic status response about the state of server,MATLAB and MATLAB Licensing
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 MATLAB Licensing.
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
- "matlab": {
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
- config = req.app["state"].settings["env_config"]
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
- return await create_status_response(req.app)
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
- async def authenticate_request(req):
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
- Response with status = 200 if request is authentic else return status = 401
260
+ JSONResponse: JSONResponse object containing information about authentication status and error if any.
142
261
  """
143
- if await token_auth.authenticate_request(req):
144
- logger.debug("!!!!!! REQUEST IS AUTHORIZED !!!!")
145
- return web.Response(status=200)
146
- else:
147
- # Return HTTPUnauthorized
148
- logger.debug("!!!!!! REQUEST IS NOT AUTHORIZED !!!!")
149
- return web.Response(status=401)
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 await create_status_response(req.app)
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 await create_status_response(req.app)
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 == "NLM":
342
+ if lic_type == "nlm":
205
343
  await state.set_licensing_nlm(data.get("connectionString"))
206
344
 
207
- elif lic_type == "MHLM":
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('License type must be "NLM" or "MHLM"!')
213
- except Exception as e:
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
- # Start MATLAB if licensing is complete
217
- if state.is_licensed() is True and not isinstance(state.error, LicensingError):
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
- # Start MATLAB
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 licensing information
242
- state.persist_licensing()
427
+ # Persist config information
428
+ state.persist_config_data()
243
429
 
244
- return await create_status_response(req.app)
430
+ return create_status_response(req.app)
245
431
 
246
432
 
247
- async def termination_integration_delete(req):
248
- """API Endpoint to terminate the Integration and shutdown the server.
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
- # Send response manually because this has to happen before the application exits
257
- res = await create_status_response(req.app, "../")
258
- await res.prepare(req)
259
- await res.write_eof()
260
-
261
- logger.debug("Shutting down the server...")
262
- # End termination with 0 exit code to indicate intentional termination
263
- await req.app.shutdown()
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
- @token_auth.decorator_authenticate_access
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
- return aiohttp.web.HTTPFound(f"{base_url}/index.html")
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
- from pkg_resources import resource_isdir, resource_listdir
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 (mod, parent) in [
535
+ for mod, parent in [
327
536
  (gui.__name__, ""),
328
- (gui.static.__name__, "/static"),
329
- (gui.static.css.__name__, "/static/css"),
330
- (gui.static.js.__name__, "/static/js"),
331
- (gui.static.media.__name__, "/static/media"),
537
+ (static.__name__, "/static"),
538
+ (css.__name__, "/static/css"),
539
+ (js.__name__, "/static/js"),
540
+ (media.__name__, "/static/media"),
332
541
  ]:
333
- for name in resource_listdir(mod, ""):
334
- if not resource_isdir(mod, name):
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}://localhost:{matlab_port}"
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
- reqH.get("connection")
379
- and reqH.get("connection").lower() == "upgrade"
380
- and reqH.get("upgrade")
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=req.cookies, connector=aiohttp.TCPConnector(verify_ssl=False)
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
- async with client_session.ws_connect(
392
- matlab_base_url + req.path_qs,
393
- ) as ws_client:
394
-
395
- async def wsforward(ws_from, ws_to):
396
- async for msg in ws_from:
397
- mt = msg.type
398
- md = msg.data
399
-
400
- # When a websocket is closed by the MATLAB JSD, it sends out a few http requests to the Embedded Connector about the events
401
- # that had occured (figureWindowClosed etc.)
402
- # The Embedded Connector responds by sending a message of type 'Error' with close code as Abnormal closure.
403
- # When this happens, matlab-proxy can safely exit out of the loop
404
- # and close the websocket connection it has with the Embedded Connector (ws_client)
405
- if (
406
- mt == aiohttp.WSMsgType.ERROR
407
- and ws_from.close_code
408
- == aiohttp.WSCloseCode.ABNORMAL_CLOSURE
409
- ):
410
- break
411
-
412
- if mt == aiohttp.WSMsgType.TEXT:
413
- await ws_to.send_str(md)
414
- elif mt == aiohttp.WSMsgType.BINARY:
415
- await ws_to.send_bytes(md)
416
- elif mt == aiohttp.WSMsgType.PING:
417
- await ws_to.ping()
418
- elif mt == aiohttp.WSMsgType.PONG:
419
- await ws_to.pong()
420
- elif ws_to.closed:
421
- await ws_to.close(code=ws_to.close_code, message=msg.extra)
422
- else:
423
- raise ValueError(f"Unexpected message type: {msg}")
424
-
425
- await asyncio.wait(
426
- [wsforward(ws_server, ws_client), wsforward(ws_client, ws_server)],
427
- return_when=asyncio.FIRST_COMPLETED,
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
- return ws_server
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
- connector=aiohttp.TCPConnector(verify_ssl=False),
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
- f"{matlab_base_url}{req.rel_url}",
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
- return web.Response(headers=headers, status=res.status, body=body)
457
- except Exception:
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 await state.get_matlab_state() == "down":
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
- # Stop any running async tasks
548
- logger = mwi.logger.get()
549
- tasks = state.tasks
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
- web_logger = None if not mwi_env.is_web_logging_enabled() else logger
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(app, logger=web_logger, access_log=web_logger)
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' with base_url: {app["settings"]["base_url"]} and app_port:{app["settings"]["app_port"]}.'
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
- "GET", f"{base_url}/authenticate_request", authenticate_request
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}/terminate_integration", termination_integration_delete
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.on_cleanup.append(cleanup_background_tasks)
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 main():
682
- """Starting point of the integration. Creates the web app and runs indefinitely."""
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
- # The integration needs to be called with --config flag.
685
- # Parse the passed cli arguments.
686
- desired_configuration_name = util.parse_cli_args()["config"]
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=desired_configuration_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
- async def shutdown():
702
- """Shuts down the app in the event of a signal interrupt."""
703
- logger.info("Shutting down MATLAB proxy-app")
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
- await app.shutdown()
706
- await app.cleanup()
1055
+ running_tasks = asyncio.all_tasks(loop)
707
1056
 
708
- # Shutdown any running tasks.
709
- await util.cancel_tasks(asyncio.all_tasks())
1057
+ # Gracefully cancel all running background tasks
1058
+ loop.run_until_complete(util.cancel_tasks(running_tasks))
710
1059
 
711
- try:
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()