Skip to content

Commit 14686e1

Browse files
diningPhilosopher64Prabhakar Kumar
authored andcommitted
Inject custom headers from the JSON string or JSON file pointed to by environment variable CUSTOM_HTTP_HEADERS.
1 parent 9a710c2 commit 14686e1

File tree

7 files changed

+517
-9
lines changed

7 files changed

+517
-9
lines changed

README.md

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -95,16 +95,48 @@ To control the behavior of the MATLAB integration for Jupyter, you can optionall
9595
```bash
9696
env APP_PORT=8888 jupyter notebook
9797
```
98+
**MARKDOWN TABLE**
9899

99100
These values are preset for you when you access the integration from the Jupyter console.
100101
| Name | Type | Example Value | Description |
101-
|------|------|-------|-------------|
102-
| **MLM_LICENSE_FILE** | string | <br/>`"[email protected]"`| When you want to use either a license file or a network license manager to license MATLAB, specify this variable. For example, specify the location of the network license manager to be `123@hostname`.|
103-
| **LOG_LEVEL** | string | <br/> `"CRITICAL"` | Specify the Python log level to be one of the following `NOTSET`, `DEBUG`, `INFO`, `WARN`, `ERROR`, or `CRITICAL`. For more information on Python log levels, see [Logging Levels](https://docs.python.org/3/library/logging.html#logging-levels) .<br />The default value is `INFO`.|
104-
| **LOG_FILE** | string | <br/> `"/tmp/logs.txt"` | Specify the full path to the file where you want the logs to be written. |
105-
| **BASE_URL** | string | <br/>`"/matlab"` | Set to control the base URL of the app. BASE_URL should start with `/` or be `empty`.|
106-
| **APP_PORT** | integer | <br/>`8080` | Specify the port for the HTTP server to listen on. |
102+
| ---- | ---- | ------------- | ----------- |
103+
| **MLM_LICENSE_FILE** | string | `"[email protected]"` | When you want to use either a license file or a network license manager to license MATLAB, specify this variable.</br> For example, specify the location of the network license manager to be `123@hostname`|
104+
| **LOG_LEVEL** | string | `"CRITICAL"` | Specify the Python log level to be one of the following `NOTSET`, `DEBUG`, `INFO`, `WARN`, `ERROR`, or `CRITICAL`. For more information on Python log levels, see [Logging Levels](https://docs.python.org/3/library/logging.html#logging-levels) .<br />The default value is `INFO`. |
105+
| **LOG_FILE** | string | `"/tmp/logs.txt"` | Specify the full path to the file where you want the logs to be written. |
106+
| **BASE_URL** | string | `"/matlab"` | Set to control the base URL of the app. BASE_URL should start with `/` or be `empty`. |
107+
| **APP_PORT** | integer | `8080` | Specify the port for the HTTP server to listen on. |
108+
| **CUSTOM_HTTP_HEADERS** | string |`'{"Content-Security-Policy": "frame-ancestors *.example.com:*"}'`<br /> OR <br />`"/path/to/your/custom/http-headers.json"` |Specify valid HTTP headers as JSON data in a string format<br />OR <br /> Specify the full path to the JSON file containing (valid) HTTP headers. These headers would be injected into the HTTP response sent to the browser. </br> For more information, see the CUSTOM_HTTP_HEADERS sub-section in the Advanced Usage section. |
109+
107110

108111
## Feedback
109112

110113
We encourage you to try this repository with your environment and provide feedback – the technical team is monitoring this repository. If you encounter a technical issue or have an enhancement request, send an email to `[email protected]`.
114+
115+
116+
## Advanced Usage
117+
118+
#### CUSTOM_HTTP_HEADERS:
119+
If the browser renders the MATLAB Integration for Jupyter with some other content, then web browsers could block the integration because of mismatch of `Content-Security-Policy` header in the response headers from the integration.
120+
121+
To avoid this, providing custom HTTP headers allow browsers to load the content.
122+
123+
For example:
124+
If this integration is rendered along with some other content on the domain `www.example.com`, sample `http-headers.json` file could be something like:
125+
126+
```json
127+
{
128+
"Content-Security-Policy": "frame-ancestors *.example.com:* https://www.example.com:*;"
129+
}
130+
```
131+
or if you are passing the custom http headers as a string in the environment variable. In bash shell, it could look like :
132+
133+
```bash
134+
export CUSTOM_HTTP_HEADERS='{"Content-Security-Policy": "frame-ancestors *.example.com:* https://www.example.com:*;"}'
135+
```
136+
137+
If you add the `frame-ancestors` directive, the browser does not block the content of this integration hosted on the domain `www.example.com`
138+
139+
140+
For more information about `Content-Security-Policy` header, check Mozilla developer docs for [Content-Security-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy)
141+
142+
**NOTE**: Setting custom HTTP headers is an advanced manoeuver, only use this functionality if you are familiar with HTTP headers.

jupyter_matlab_proxy/app.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
from .app_state import AppState
1212
from .util import mw_logger
1313
from .util.exceptions import LicensingError
14-
1514
import pkgutil
1615
import mimetypes
1716

@@ -202,6 +201,7 @@ def make_static_route_table(app):
202201
content_type = mimetypes.guess_type(name)[0]
203202

204203
headers = {"content-type": content_type}
204+
headers.update(app["settings"]["custom_http_headers"])
205205

206206
table[f"{base_url}{parent}/{name}"] = {
207207
"mod": mod,
@@ -282,8 +282,11 @@ async def wsforward(ws_from, ws_to):
282282
allow_redirects=False,
283283
data=req_body,
284284
) as res:
285+
285286
headers = res.headers.copy()
286287
body = await res.read()
288+
headers.update(req.app["settings"]["custom_http_headers"])
289+
287290
return web.Response(headers=headers, status=res.status, body=body)
288291
except Exception:
289292
raise web.HTTPNotFound()

jupyter_matlab_proxy/settings.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import xml.etree.ElementTree as ET
77
import uuid, socket
88
import shutil
9+
from .util import custom_http_headers
910

1011

1112
def get_matlab_path():
@@ -42,7 +43,6 @@ def get_dev_settings():
4243
tempfile.mkstemp(prefix="mrf_", dir=str(matlab_temp_dir))[1]
4344
)
4445
ws_env, ws_env_suffix = get_ws_env_settings()
45-
4646
return {
4747
"matlab_path": Path(),
4848
"matlab_version": "R2020b",
@@ -68,6 +68,7 @@ def get_dev_settings():
6868
"mwa_api_endpoint": f"https://login{ws_env_suffix}.mathworks.com/authenticationws/service/v4",
6969
"mhlm_api_endpoint": f"https://licensing{ws_env_suffix}.mathworks.com/mls/service/v1/entitlement/list",
7070
"mwa_login": f"https://login{ws_env_suffix}.mathworks.com",
71+
"custom_http_headers": custom_http_headers.get(),
7172
}
7273

7374

@@ -112,7 +113,6 @@ def get(dev=False):
112113
)
113114
matlab_path = get_matlab_path()
114115
ws_env, ws_env_suffix = get_ws_env_settings()
115-
116116
return {
117117
"matlab_path": matlab_path,
118118
"matlab_version": get_matlab_version(matlab_path),
@@ -137,6 +137,7 @@ def get(dev=False):
137137
"mwa_api_endpoint": f"https://login{ws_env_suffix}.mathworks.com/authenticationws/service/v4",
138138
"mhlm_api_endpoint": f"https://licensing{ws_env_suffix}.mathworks.com/mls/service/v1/entitlement/list",
139139
"mwa_login": f"https://login{ws_env_suffix}.mathworks.com",
140+
"custom_http_headers": custom_http_headers.get(),
140141
}
141142

142143

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
# Copyright 2020-2021 The MathWorks, Inc.
2+
3+
import os, json, sys
4+
from json.decoder import JSONDecodeError
5+
from . import mw_logger
6+
7+
logger = mw_logger.get()
8+
9+
10+
def get():
11+
"""Returns a dict containing custom http headers to inject into responses from the
12+
MATLAB Embedded Connector and to the static files in 'gui' folder.
13+
14+
Parses valid JSON data if provided as a string or in a JSON file.
15+
16+
Returns:
17+
Dict: Representing custom headers.
18+
"""
19+
env_var = __get_custom_header_env_var()
20+
# custom_headers_path can contain valid JSON data as a string or a path to a file containing valid JSON
21+
custom_headers_path = os.getenv(env_var)
22+
23+
# If the environment variable is not present
24+
if custom_headers_path is None:
25+
logger.info(
26+
f"Environment variable {env_var} is not set, hence no custom HTTP headers are applied."
27+
)
28+
return dict()
29+
30+
logger.info(
31+
f"Envinronment variable {env_var} is set and has the value: {custom_headers_path}"
32+
)
33+
is_file = os.path.isfile(custom_headers_path)
34+
35+
if is_file and __check_file_validity(custom_headers_path):
36+
return __get_file_contents(custom_headers_path)
37+
38+
else:
39+
exception_statement = __get_exception_statement()
40+
try:
41+
# Valid JSON data as a string
42+
data = json.loads(custom_headers_path)
43+
logger.info(
44+
f"Successfully parsed HTTP headers from the environment variable {env_var}\nThe parsed HTTP headers are:\n{data}\n"
45+
)
46+
return data
47+
48+
except JSONDecodeError as json_decode_error:
49+
# Invalid JSON data as a string.
50+
print(
51+
exception_statement.format(
52+
env_var=__get_custom_header_env_var(),
53+
env_var_value=custom_headers_path,
54+
)
55+
)
56+
logger.error(
57+
f"Failed with {type(json_decode_error).__name__} exception. Check the value in {env_var} environment variable for syntax errors."
58+
)
59+
sys.exit(1)
60+
61+
except Exception as e:
62+
print(
63+
exception_statement.format(
64+
env_var=__get_custom_header_env_var(),
65+
env_var_value=custom_headers_path,
66+
)
67+
)
68+
logger.error(
69+
f"Raised {type(e).__name__} exception when parsing JSON data in the environment variable {env_var}"
70+
)
71+
sys.exit(1)
72+
73+
74+
def __check_file_validity(custom_headers_path):
75+
"""Checks the file at custom_headers_path for the read access to the file for the current python process.
76+
77+
Args:
78+
custom_headers_path (String): File path of custom headers.
79+
80+
Raises:
81+
OSError: When the current python process doesn't have read access to the custom_headers_path
82+
"""
83+
exception_statement = __get_exception_statement()
84+
logger.debug(
85+
f"The value in environment variable {__get_custom_header_env_var()} contains a path to a file."
86+
)
87+
try:
88+
if not os.access(custom_headers_path, os.R_OK):
89+
raise OSError
90+
91+
logger.debug(
92+
f"Current python process has read access rights to the file at: {custom_headers_path}"
93+
)
94+
return True
95+
96+
except OSError as os_error:
97+
print(
98+
f"\n{type(os_error).__name__}: Permission denied. Cannot read file: {custom_headers_path}\n"
99+
+ exception_statement.format(
100+
env_var=__get_custom_header_env_var(),
101+
env_var_value=custom_headers_path,
102+
)
103+
)
104+
logger.error(
105+
f"{type(os_error).__name__}: Permission denied. For the current python process, check read access rights for the file: {custom_headers_path}."
106+
)
107+
sys.exit(1)
108+
109+
except Exception as e:
110+
print(
111+
f"\n{type(e).__name__} exception raised with error message: {e.args}\n"
112+
+ exception_statement.format(
113+
env_var=__get_custom_header_env_var(),
114+
env_var_value=custom_headers_path,
115+
)
116+
)
117+
logger.error(
118+
f"{type(e).__name__} exception raised when checking read access rights to the file at: {custom_headers_path}"
119+
)
120+
sys.exit(1)
121+
122+
123+
def __get_file_contents(custom_headers_path):
124+
"""Reads the file containing custom headers and returns a dict.
125+
Raises JSONDecodeError if the JSON data in the file is not valid.
126+
127+
Args:
128+
custom_headers_path (String): File path of custom headers.
129+
130+
Raises:
131+
JSONDecodeError: when the contents of the file containing custom headers
132+
does not have valid JSON data.
133+
134+
Returns:
135+
Dict: Containing custom headers.
136+
"""
137+
138+
custom_headers = None
139+
exception_statement = __get_exception_statement()
140+
try:
141+
with open(custom_headers_path, "r") as json_file:
142+
custom_headers = json.load(json_file)
143+
logger.info(
144+
f"Successfully parsed HTTP headers present in the file: {custom_headers_path}"
145+
)
146+
logger.debug(f"The parsed HTTP headers are :\n{custom_headers}\n")
147+
return custom_headers
148+
149+
except JSONDecodeError as json_decode_error:
150+
print(
151+
f"\n{type(json_decode_error).__name__} exception raised with error message: {json_decode_error.args[0]}\n"
152+
+ exception_statement.format(
153+
env_var=__get_custom_header_env_var(),
154+
env_var_value=custom_headers_path,
155+
)
156+
)
157+
logger.error(
158+
f"{type(json_decode_error).__name__}: Failed to parse JSON data in the file: {custom_headers_path}.\nCheck contents of the file for any syntax errors."
159+
)
160+
sys.exit(1)
161+
162+
except Exception as e:
163+
print(
164+
f"\nFailed with {type(e).__name__} exception with error message:\n{e.args}"
165+
+ exception_statement.format(
166+
env_var=__get_custom_header_env_var(),
167+
env_var_value=custom_headers_path,
168+
)
169+
)
170+
logger.error(
171+
f"Failed with {type(e).__name__} when reading JSON data from the file: {custom_headers_path}"
172+
)
173+
sys.exit(1)
174+
175+
176+
def __get_custom_header_env_var():
177+
"""Returns the name of the environment variable which contains path
178+
to the file containing custom headers.
179+
180+
Returns:
181+
String: Environment variable containing path to the file containing custom headers.
182+
"""
183+
return "CUSTOM_HTTP_HEADERS"
184+
185+
186+
def __get_exception_statement():
187+
"""Returns a generic exception statement pertaining to HTTP headers.
188+
189+
Returns:
190+
String: Containing a generic exception statement to print to stdout.
191+
"""
192+
exception_statement = """HTTP headers defined in the environment variable {env_var} are not valid.\n
193+
The value in {env_var} is: {env_var_value}\n
194+
Please provide either valid JSON data as a string or a path to a file with read access containing valid JSON data."""
195+
196+
return exception_statement

0 commit comments

Comments
 (0)