Skip to content

Commit 30e95eb

Browse files
API v4.2.1- Added NGINX javascript support (#38)
* 20240214-01 Commit - Initial njs support * 20240215-01 Commit - Initial njs support * 20240216-01 Commit - njs support
1 parent 2d53452 commit 30e95eb

File tree

6 files changed

+287
-6
lines changed

6 files changed

+287
-6
lines changed

USAGE-v4.2.md

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,57 @@ Locations `.declaration.http.servers[].locations[].uri` match modifiers in `.dec
5252
- *iregex* - case insensitive regex matching
5353
- *best* - case sensitive regex matching that halts any other location matching once a match is made
5454

55+
### Javascript profiles ###
56+
57+
NGINX Javascript profiles are defined in `.declaration.http.njs[]`:
58+
59+
- `name` - the NJS profile name
60+
- `file.content` - the base64-encoded njs source code or the `http(s)://` URL of the file
61+
- `file.authentication.server[0].profile` - authentication profile name if `file.content` is a URL and the request must be authenticated
62+
63+
### Javascript hooks ###
64+
65+
NGINX Javascript hooks can be used in:
66+
67+
- `.declaration.http.njs`
68+
- Supported hooks:
69+
- `js_preload_object'
70+
- 'js_set`
71+
- `.declaration.http.server[].njs`
72+
- Supported hooks:
73+
- `js_preload_object'
74+
- 'js_set`
75+
- `.declaration.http.server[].location[].njs`
76+
- Supported hooks:
77+
- `js_body_filter'
78+
- 'js_content'
79+
- 'js_header_filter'
80+
- 'js_periodic'
81+
- 'js_preload_object'
82+
- 'js_set`
83+
84+
Hooks invocation is:
85+
86+
```
87+
"njs": [
88+
{
89+
"hook": {
90+
"name": "<HOOK_NAME>",
91+
"parameters": [
92+
{
93+
"name": "<HOOK_PARAMETER_NAME>",
94+
"value": "<HOOK_PARAMETER_VALUE>"
95+
}
96+
]
97+
},
98+
"profile": "<NGINX_JAVASCRIPT_PROFILE>",
99+
"function": "<JAVASCRIPT_FUNCTION_NAME>"
100+
}
101+
]
102+
```
103+
104+
For detailed examples see the [Postman collection](/contrib/postman)
105+
55106
### API Gateway ###
56107

57108
Swagger files and OpenAPI schemas can be used to automatically configure NGINX as an API Gateway. Developer portal creation is supported through [Redocly](https://redocly.com/)
@@ -88,11 +139,7 @@ is:
88139
"username": "{{nim_username}}",
89140
"password": "{{nim_password}}",
90141
"instancegroup": "{{nim_instancegroup}}",
91-
"synctime": 0,
92-
"modules": [
93-
"ngx_http_js_module",
94-
"ngx_stream_js_module"
95-
]
142+
"synctime": 0
96143
}
97144
},
98145
"declaration": {

contrib/postman/NGINX Declarative API.postman_collection.json

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5832,6 +5832,55 @@
58325832
"response": []
58335833
}
58345834
]
5835+
},
5836+
{
5837+
"name": "NGINX Javascript",
5838+
"item": [
5839+
{
5840+
"name": "NGINX Javascript test",
5841+
"event": [
5842+
{
5843+
"listen": "test",
5844+
"script": {
5845+
"exec": [
5846+
"var respData = JSON.parse(responseBody);",
5847+
"",
5848+
"tests[\"configUid is: \" +respData.configUid] = respData.configUid;",
5849+
"",
5850+
"pm.collectionVariables.set('configUid',respData.configUid);"
5851+
],
5852+
"type": "text/javascript"
5853+
}
5854+
}
5855+
],
5856+
"request": {
5857+
"method": "POST",
5858+
"header": [],
5859+
"body": {
5860+
"mode": "raw",
5861+
"raw": "{\n \"output\": {\n \"type\": \"nms\",\n \"nms\": {\n \"url\": \"{{nim_host}}\",\n \"username\": \"{{nim_username}}\",\n \"password\": \"{{nim_password}}\",\n \"instancegroup\": \"{{nim_instancegroup}}\",\n \"synctime\": 0,\n \"modules\": [\n \"ngx_http_js_module\",\n \"ngx_stream_js_module\"\n ]\n }\n },\n \"declaration\": {\n \"http\": {\n \"servers\": [\n {\n \"name\": \"Example HTTP server with Javascript\",\n \"resolver\": \"8.8.8.8\",\n \"names\": [\n \"njs-test.vm-test.ie.ff.lan\"\n ],\n \"listen\": {\n \"address\": \"0.0.0.0:80\"\n },\n \"log\": {\n \"access\": \"/var/log/nginx/njs-test.nginx.lab_access_log\",\n \"error\": \"/var/log/nginx/njs-test.nginx.lab_error_log\"\n },\n \"njs\": [\n {\n \"hook\": {\n \"type\": \"js_set\",\n \"js_set\": {\n \"variable\": \"$serverVarSetByNjs\"\n }\n },\n \"profile\": \"njs_set_variable\",\n \"function\": \"njsSetVariable\"\n }\n ],\n \"headers\": {\n \"to_server\": {\n \"set\": [\n {\n \"name\": \"X-Injected-Client-IP\",\n \"value\": \"$remote_addr\"\n },\n {\n \"name\": \"Host\",\n \"value\": \"echo.free.beeceptor.com\"\n }\n ]\n }\n },\n \"locations\": [\n {\n \"uri\": \"/echo\",\n \"urimatch\": \"prefix\",\n \"upstream\": \"http://test_upstream\",\n \"headers\": {\n \"to_server\": {\n \"set\": [\n {\n \"name\": \"X-Injected-Client-IP\",\n \"value\": \"$remote_addr\"\n },\n {\n \"name\": \"X-HTTP-Var-Set-By-Njs\",\n \"value\": \"$httpVarSetByNjs\"\n },\n {\n \"name\": \"X-Server-Var-Set-By-Njs\",\n \"value\": \"$serverVarSetByNjs\"\n },\n {\n \"name\": \"Host\",\n \"value\": \"echo.free.beeceptor.com\"\n }\n ]\n }\n }\n },\n {\n \"uri\": \"/generatecontent\",\n \"urimatch\": \"prefix\",\n \"upstream\": \"http://test_upstream\",\n \"njs\": [\n {\n \"hook\": {\n \"type\": \"js_content\"\n },\n \"profile\": \"njs_set_content\",\n \"function\": \"njsSetContent\"\n }\n ],\n \"headers\": {\n \"to_server\": {\n \"set\": [\n {\n \"name\": \"X-Injected-Client-IP\",\n \"value\": \"$remote_addr\"\n },\n {\n \"name\": \"X-HTTP-Var-Set-By-Njs\",\n \"value\": \"$httpVarSetByNjs\"\n },\n {\n \"name\": \"X-Server-Var-Set-By-Njs\",\n \"value\": \"$serverVarSetByNjs\"\n },\n {\n \"name\": \"Host\",\n \"value\": \"echo.free.beeceptor.com\"\n }\n ]\n }\n }\n }\n ]\n }\n ],\n \"upstreams\": [\n {\n \"name\": \"test_upstream\",\n \"origin\": [\n {\n \"server\": \"echo.free.beeceptor.com\"\n }\n ]\n }\n ],\n \"njs\": [\n {\n \"hook\": {\n \"type\": \"js_set\",\n \"js_set\": {\n \"variable\": \"$httpVarSetByNjs\"\n }\n },\n \"profile\": \"njs_set_variable\",\n \"function\": \"njsSetVariable\"\n }\n ],\n \"njs_profiles\": [\n {\n \"name\": \"njs_set_variable\",\n \"file\": {\n \"content\": \"ZnVuY3Rpb24gbmpzU2V0VmFyaWFibGUocikgewogICAgcmV0dXJuICJWYXJpYWJsZV9zZXRfYnlfamF2YXNjcmlwdCAtIFVSSSAiK3IudXJpOwp9CgpleHBvcnQgZGVmYXVsdCB7bmpzU2V0VmFyaWFibGV9Cgo=\"\n }\n },\n {\n \"name\": \"njs_set_content\",\n \"file\": {\n \"content\": \"ZnVuY3Rpb24gbmpzU2V0Q29udGVudChyKSB7CiAgci5yZXR1cm4oMjAwLCAiSGVsbG8gd29ybGQhXG4iKTsKfQoKZXhwb3J0IGRlZmF1bHQge25qc1NldENvbnRlbnR9Cg==\"\n }\n }\n ]\n }\n }\n}",
5862+
"options": {
5863+
"raw": {
5864+
"language": "json"
5865+
}
5866+
}
5867+
},
5868+
"url": {
5869+
"raw": "http://{{ncg_host}}:{{ncg_port}}/{{ngc_api_version}}/config",
5870+
"protocol": "http",
5871+
"host": [
5872+
"{{ncg_host}}"
5873+
],
5874+
"port": "{{ncg_port}}",
5875+
"path": [
5876+
"{{ngc_api_version}}",
5877+
"config"
5878+
]
5879+
}
5880+
},
5881+
"response": []
5882+
}
5883+
]
58355884
}
58365885
]
58375886
}

etc/config.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ certs_dir = '/etc/nginx/ssl'
4343
devportal_dir = '/etc/nginx/devportal'
4444
auth_client_dir = '/etc/nginx/auth/client'
4545
auth_server_dir = '/etc/nginx/auth/server'
46+
njs_dir = '/etc/nginx/njs'
4647

4748

4849
# Time to wait to get status after committing a staged config

src/V4_2_CreateConfig.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,36 @@ def createconfig(declaration: ConfigDeclaration, apiversion: str, runfromautosyn
198198
all_auth_server_profiles.append(auth_profile['name'])
199199
auxFiles['files'].append(authProfileConfigFile)
200200

201+
# NGINX Javascript profiles
202+
all_njs_profiles = []
203+
d_njs_files = v4_2.MiscUtils.getDictKey(d, 'declaration.http.njs_profiles')
204+
if d_njs_files is not None:
205+
for i in range(len(d_njs_files)):
206+
njs_file = d_njs_files[i]
207+
njs_filename = njs_file['name'].replace(' ','_')
208+
209+
status, content = v4_2.GitOps.getObjectFromRepo(object=njs_file['file'],
210+
authProfiles=d['declaration']['http'][
211+
'authentication'])
212+
213+
if status != 200:
214+
return {"status_code": 422, "message": {"status_code": status, "message": content}}
215+
216+
njsAuxFile = {'contents': content['content'],
217+
'name': NcgConfig.config['nms']['njs_dir'] + '/' + njs_filename + '.js'}
218+
auxFiles['files'].append(njsAuxFile)
219+
all_njs_profiles.append(njs_filename)
220+
221+
# HTTP level Javascript hooks
222+
d_http_njs_hooks = v4_2.MiscUtils.getDictKey(d, 'declaration.http.njs')
223+
if d_http_njs_hooks is not None:
224+
for i in range(len(d_http_njs_hooks)):
225+
if d_http_njs_hooks[i]['profile'] not in all_njs_profiles:
226+
return {"status_code": 422,
227+
"message": {"status_code": status, "message":
228+
{"code": status,
229+
"content": f"invalid njs profile [{d_http_njs_hooks[i]['profile']}] in HTTP declaration, must be one of {all_njs_profiles}"}}}
230+
201231
# Parse HTTP servers
202232
d_servers = v4_2.MiscUtils.getDictKey(d, 'declaration.http.servers')
203233
if d_servers is not None:
@@ -206,6 +236,15 @@ def createconfig(declaration: ConfigDeclaration, apiversion: str, runfromautosyn
206236
for server in d_servers:
207237
serverSnippet = ''
208238

239+
# Server level Javascript hooks
240+
if server['njs']:
241+
for i in range(len(server['njs'])):
242+
if server['njs'][i]['profile'] not in all_njs_profiles:
243+
return {"status_code": 422,
244+
"message": {"status_code": status, "message":
245+
{"code": status,
246+
"content": f"invalid njs profile [{server['njs'][i]['profile']}] in server [{server['name']}], must be one of {all_njs_profiles}"}}}
247+
209248
if server['snippet']:
210249
status, serverSnippet = v4_2.GitOps.getObjectFromRepo(object = server['snippet'], authProfiles = d['declaration']['http']['authentication'], base64Encode = False)
211250

@@ -215,6 +254,16 @@ def createconfig(declaration: ConfigDeclaration, apiversion: str, runfromautosyn
215254
serverSnippet = serverSnippet['content']
216255

217256
for loc in server['locations']:
257+
258+
# Location level Javascript hooks
259+
if loc['njs']:
260+
for i in range(len(loc['njs'])):
261+
if loc['njs'][i]['profile'] not in all_njs_profiles:
262+
return {"status_code": 422,
263+
"message": {"status_code": status, "message":
264+
{"code": status,
265+
"content": f"invalid njs profile [{loc['njs'][i]['profile']}] in location [{loc['uri']}], must be one of {all_njs_profiles}"}}}
266+
218267
if loc['snippet']:
219268
status, snippet = v4_2.GitOps.getObjectFromRepo(object = loc['snippet'], authProfiles = d['declaration']['http']['authentication'])
220269

src/V4_2_NginxConfigDeclaration.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,7 @@ class Location(BaseModel, extra="forbid"):
347347
snippet: Optional[ObjectFromSourceOfTruth] = {}
348348
authentication: Optional[LocationAuth] = {}
349349
headers: Optional[LocationHeaders]= {}
350+
njs: Optional[List[NjsHookLocation]] = []
350351

351352
@model_validator(mode='after')
352353
def check_type(self) -> 'Location':
@@ -369,6 +370,70 @@ class ObjectFromSourceOfTruth(BaseModel, extra="forbid"):
369370
authentication: Optional[List[LocationAuthServer]] = []
370371

371372

373+
class NjsHook_js_body_filter(BaseModel, extra="forbid"):
374+
buffer_type: Optional[str] = ""
375+
376+
377+
class NjsHook_js_periodic(BaseModel, extra="forbid"):
378+
interval: Optional[str] = ""
379+
jitter: Optional[int] = 0
380+
worker_affinity: Optional[str] = ""
381+
382+
383+
class NjsHook_js_preload_object(BaseModel, extra="forbid"):
384+
file: str
385+
386+
387+
class NjsHook_js_set(BaseModel, extra="forbid"):
388+
variable: str
389+
390+
391+
class NjsHookHttpServerDetails(BaseModel, extra="forbid"):
392+
type: str
393+
js_preload_object: Optional[NjsHook_js_preload_object] = {}
394+
js_set: Optional[NjsHook_js_set] = {}
395+
396+
@model_validator(mode='after')
397+
def check_type(self) -> 'NjsHookHttpServerDetails':
398+
_type = self.type
399+
400+
valid = ['js_preload_object', 'js_set']
401+
if _type not in valid:
402+
raise ValueError(f"Invalid hook [{_type}] must be one of {str(valid)}")
403+
404+
return self
405+
406+
407+
class NjsHookLocationDetails(BaseModel, extra="forbid"):
408+
type: str
409+
js_preload_object: Optional[NjsHook_js_preload_object] = {}
410+
js_set: Optional[NjsHook_js_set] = {}
411+
js_body_filter: Optional[NjsHook_js_body_filter] = {}
412+
js_periodic: Optional[NjsHook_js_periodic] = {}
413+
414+
@model_validator(mode='after')
415+
def check_type(self) -> 'NjsHookLocationDetails':
416+
_type = self.type
417+
418+
valid = ['js_body_filter', 'js_content', 'js_header_filter', 'js_periodic', 'js_preload_object', 'js_set']
419+
if _type not in valid:
420+
raise ValueError(f"Invalid hook [{_type}] must be one of {str(valid)}")
421+
422+
return self
423+
424+
class NjsHookHttpServer(BaseModel, extra="forbid"):
425+
hook: NjsHookHttpServerDetails
426+
profile: str
427+
function: str
428+
429+
430+
431+
class NjsHookLocation(BaseModel, extra="forbid"):
432+
hook: NjsHookLocationDetails
433+
profile: str
434+
function: str
435+
436+
372437
class Server(BaseModel, extra="forbid"):
373438
name: str
374439
names: Optional[List[str]] = []
@@ -379,6 +444,7 @@ class Server(BaseModel, extra="forbid"):
379444
app_protect: Optional[AppProtect] = {}
380445
snippet: Optional[ObjectFromSourceOfTruth] = {}
381446
headers: Optional[LocationHeaders] = {}
447+
njs: Optional[List[NjsHookHttpServer]] = []
382448

383449

384450
class L4Server(BaseModel, extra="forbid"):
@@ -520,6 +586,11 @@ class Authentication(BaseModel, extra="forbid"):
520586
server: Optional[List[Authentication_Server]] = []
521587

522588

589+
class NjsFile(BaseModel, extra="forbid"):
590+
name: str
591+
file: ObjectFromSourceOfTruth
592+
593+
523594
class Http(BaseModel, extra="forbid"):
524595
servers: Optional[List[Server]] = []
525596
upstreams: Optional[List[Upstream]] = []
@@ -529,6 +600,8 @@ class Http(BaseModel, extra="forbid"):
529600
maps: Optional[List[Map]] = []
530601
snippet: Optional[ObjectFromSourceOfTruth] = {}
531602
authentication: Optional[Authentication] = {}
603+
njs: Optional[List[NjsHookHttpServer]] = []
604+
njs_profiles: Optional[List[NjsFile]] = []
532605

533606

534607
class Declaration(BaseModel, extra="forbid"):

0 commit comments

Comments
 (0)