synapse 2.152.0__py311-none-any.whl → 2.154.0__py311-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.
Potentially problematic release.
This version of synapse might be problematic. Click here for more details.
- synapse/axon.py +19 -16
- synapse/cortex.py +203 -15
- synapse/exc.py +0 -2
- synapse/lib/ast.py +42 -23
- synapse/lib/autodoc.py +2 -2
- synapse/lib/cache.py +16 -1
- synapse/lib/cell.py +5 -5
- synapse/lib/httpapi.py +198 -2
- synapse/lib/layer.py +5 -2
- synapse/lib/modelrev.py +36 -3
- synapse/lib/node.py +2 -5
- synapse/lib/parser.py +1 -1
- synapse/lib/schemas.py +51 -0
- synapse/lib/snap.py +10 -0
- synapse/lib/storm.lark +24 -4
- synapse/lib/storm.py +98 -19
- synapse/lib/storm_format.py +1 -1
- synapse/lib/stormhttp.py +11 -4
- synapse/lib/stormlib/auth.py +16 -2
- synapse/lib/stormlib/backup.py +1 -0
- synapse/lib/stormlib/basex.py +2 -0
- synapse/lib/stormlib/cell.py +7 -0
- synapse/lib/stormlib/compression.py +3 -0
- synapse/lib/stormlib/cortex.py +1168 -0
- synapse/lib/stormlib/ethereum.py +1 -0
- synapse/lib/stormlib/graph.py +2 -0
- synapse/lib/stormlib/hashes.py +5 -0
- synapse/lib/stormlib/hex.py +6 -0
- synapse/lib/stormlib/infosec.py +6 -1
- synapse/lib/stormlib/ipv6.py +1 -0
- synapse/lib/stormlib/iters.py +58 -1
- synapse/lib/stormlib/json.py +5 -0
- synapse/lib/stormlib/mime.py +1 -0
- synapse/lib/stormlib/model.py +19 -3
- synapse/lib/stormlib/modelext.py +1 -0
- synapse/lib/stormlib/notifications.py +2 -0
- synapse/lib/stormlib/pack.py +2 -0
- synapse/lib/stormlib/random.py +1 -0
- synapse/lib/stormlib/smtp.py +0 -7
- synapse/lib/stormlib/stats.py +223 -0
- synapse/lib/stormlib/stix.py +8 -0
- synapse/lib/stormlib/storm.py +1 -0
- synapse/lib/stormlib/version.py +3 -0
- synapse/lib/stormlib/xml.py +3 -0
- synapse/lib/stormlib/yaml.py +2 -0
- synapse/lib/stormtypes.py +250 -170
- synapse/lib/trigger.py +180 -4
- synapse/lib/types.py +1 -1
- synapse/lib/version.py +2 -2
- synapse/lib/view.py +55 -6
- synapse/models/inet.py +21 -6
- synapse/models/orgs.py +48 -2
- synapse/models/risk.py +126 -2
- synapse/models/syn.py +6 -0
- synapse/tests/files/stormpkg/badapidef.yaml +13 -0
- synapse/tests/files/stormpkg/storm/modules/apimod +10 -0
- synapse/tests/files/stormpkg/testpkg.yaml +23 -0
- synapse/tests/test_axon.py +7 -2
- synapse/tests/test_cortex.py +231 -35
- synapse/tests/test_lib_ast.py +138 -43
- synapse/tests/test_lib_autodoc.py +1 -1
- synapse/tests/test_lib_modelrev.py +9 -0
- synapse/tests/test_lib_node.py +55 -0
- synapse/tests/test_lib_storm.py +14 -1
- synapse/tests/test_lib_stormhttp.py +65 -6
- synapse/tests/test_lib_stormlib_auth.py +12 -3
- synapse/tests/test_lib_stormlib_cortex.py +1327 -0
- synapse/tests/test_lib_stormlib_iters.py +116 -0
- synapse/tests/test_lib_stormlib_stats.py +187 -0
- synapse/tests/test_lib_stormlib_storm.py +8 -0
- synapse/tests/test_lib_stormsvc.py +24 -1
- synapse/tests/test_lib_stormtypes.py +124 -69
- synapse/tests/test_lib_trigger.py +315 -0
- synapse/tests/test_lib_view.py +1 -2
- synapse/tests/test_model_base.py +26 -0
- synapse/tests/test_model_inet.py +22 -0
- synapse/tests/test_model_orgs.py +28 -0
- synapse/tests/test_model_risk.py +73 -0
- synapse/tests/test_tools_autodoc.py +25 -0
- synapse/tests/test_tools_genpkg.py +9 -3
- synapse/tests/utils.py +39 -0
- synapse/tools/autodoc.py +42 -2
- {synapse-2.152.0.dist-info → synapse-2.154.0.dist-info}/METADATA +2 -2
- {synapse-2.152.0.dist-info → synapse-2.154.0.dist-info}/RECORD +87 -79
- {synapse-2.152.0.dist-info → synapse-2.154.0.dist-info}/WHEEL +1 -1
- {synapse-2.152.0.dist-info → synapse-2.154.0.dist-info}/LICENSE +0 -0
- {synapse-2.152.0.dist-info → synapse-2.154.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1327 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import aiohttp
|
|
4
|
+
|
|
5
|
+
import synapse.common as s_common
|
|
6
|
+
import synapse.exc as s_exc
|
|
7
|
+
|
|
8
|
+
import synapse.tests.utils as s_test
|
|
9
|
+
|
|
10
|
+
from pprint import pprint
|
|
11
|
+
|
|
12
|
+
class CortexLibTest(s_test.SynTest):
|
|
13
|
+
|
|
14
|
+
async def test_libcortex_httpapi_methods(self):
|
|
15
|
+
|
|
16
|
+
async with self.getTestCore() as core:
|
|
17
|
+
udef = await core.addUser('lowuser')
|
|
18
|
+
lowuser = udef.get('iden')
|
|
19
|
+
await core.setUserPasswd(core.auth.rootuser.iden, 'root')
|
|
20
|
+
await core.setUserPasswd(lowuser, 'secret')
|
|
21
|
+
addr, hport = await core.addHttpsPort(0)
|
|
22
|
+
|
|
23
|
+
# Define a handler. Add storm for each METHOD we support
|
|
24
|
+
q = '''
|
|
25
|
+
$api = $lib.cortex.httpapi.add(testpath00)
|
|
26
|
+
$api.methods.get = ${
|
|
27
|
+
$data = ({'method': 'get'})
|
|
28
|
+
$headers = ({'Secret-Header': 'OhBoy!'})
|
|
29
|
+
$request.reply(200, headers=$headers, body=$data)
|
|
30
|
+
}
|
|
31
|
+
$api.methods.post = ${
|
|
32
|
+
$data = ({'method': 'post'})
|
|
33
|
+
$headers = ({'Secret-Header': 'OhBoy!'})
|
|
34
|
+
$request.reply(201, headers=$headers, body=$data)
|
|
35
|
+
}
|
|
36
|
+
$api.methods.put = ${
|
|
37
|
+
$data = ({'method': 'put'})
|
|
38
|
+
$headers = ({'Secret-Header': 'OhBoy!'})
|
|
39
|
+
$request.reply(202, headers=$headers, body=$data)
|
|
40
|
+
}
|
|
41
|
+
$api.methods.patch = ${
|
|
42
|
+
$data = ({'method': 'patch'})
|
|
43
|
+
$headers = ({'Secret-Header': 'OhBoy!'})
|
|
44
|
+
$request.reply(203, headers=$headers, body=$data)
|
|
45
|
+
}
|
|
46
|
+
$api.methods.options = ${
|
|
47
|
+
$headers = ({'Secret-Header': 'Options', 'Content-Type': 'application/json'})
|
|
48
|
+
$request.reply(204, headers=$headers)
|
|
49
|
+
}
|
|
50
|
+
$api.methods.delete = ${
|
|
51
|
+
$data = ({'method': 'delete'})
|
|
52
|
+
$headers = ({'Secret-Header': 'OhBoy!'})
|
|
53
|
+
$request.reply(205, headers=$headers, body=$data)
|
|
54
|
+
}
|
|
55
|
+
$api.methods.head = ${
|
|
56
|
+
$headers = ({'Secret-Header': 'Head'})
|
|
57
|
+
$request.reply(206, headers=$headers, body=({"no":"body"}))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return ( $api.iden )
|
|
61
|
+
'''
|
|
62
|
+
testpath00 = await core.callStorm(q)
|
|
63
|
+
|
|
64
|
+
q = '''
|
|
65
|
+
$api = $lib.cortex.httpapi.add(nomeths)
|
|
66
|
+
return ( $api.iden )
|
|
67
|
+
'''
|
|
68
|
+
nomeths = await core.callStorm(q)
|
|
69
|
+
|
|
70
|
+
info = await core.callStorm('return( $lib.cortex.httpapi.get($iden).pack() )',
|
|
71
|
+
opts={'vars': {'iden': testpath00}})
|
|
72
|
+
self.eq(info.get('iden'), testpath00)
|
|
73
|
+
|
|
74
|
+
with self.raises(s_exc.BadSyntax):
|
|
75
|
+
q = '$api = $lib.cortex.httpapi.get($iden) $api.methods.get = "| | | "'
|
|
76
|
+
await core.callStorm(q, opts={'vars': {'iden': testpath00}})
|
|
77
|
+
|
|
78
|
+
with self.raises(s_exc.NoSuchName):
|
|
79
|
+
q = '$api = $lib.cortex.httpapi.get($iden) $api.methods.trace = ${}'
|
|
80
|
+
await core.callStorm(q, opts={'vars': {'iden': testpath00}})
|
|
81
|
+
|
|
82
|
+
with self.raises(s_exc.NoSuchName):
|
|
83
|
+
q = '$api = $lib.cortex.httpapi.get($iden) $api.methods.connect = ${}'
|
|
84
|
+
await core.callStorm(q, opts={'vars': {'iden': testpath00}})
|
|
85
|
+
|
|
86
|
+
with self.raises(s_exc.NoSuchName):
|
|
87
|
+
q = '$api = $lib.cortex.httpapi.get($iden) $api.methods.wildycustomverb = ${}'
|
|
88
|
+
await core.callStorm(q, opts={'vars': {'iden': testpath00}})
|
|
89
|
+
|
|
90
|
+
async with self.getHttpSess(auth=('root', 'root'), port=hport) as sess: # type: aiohttp.ClientSession
|
|
91
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/testpath00')
|
|
92
|
+
self.eq(resp.status, 200)
|
|
93
|
+
data = await resp.json()
|
|
94
|
+
self.eq(data.get('method'), 'get')
|
|
95
|
+
|
|
96
|
+
resp = await sess.post(f'https://localhost:{hport}/api/ext/testpath00')
|
|
97
|
+
self.eq(resp.status, 201)
|
|
98
|
+
data = await resp.json()
|
|
99
|
+
self.eq(data.get('method'), 'post')
|
|
100
|
+
|
|
101
|
+
resp = await sess.put(f'https://localhost:{hport}/api/ext/testpath00')
|
|
102
|
+
self.eq(resp.status, 202)
|
|
103
|
+
data = await resp.json()
|
|
104
|
+
self.eq(data.get('method'), 'put')
|
|
105
|
+
|
|
106
|
+
resp = await sess.patch(f'https://localhost:{hport}/api/ext/testpath00')
|
|
107
|
+
self.eq(resp.status, 203)
|
|
108
|
+
data = await resp.json()
|
|
109
|
+
self.eq(data.get('method'), 'patch')
|
|
110
|
+
|
|
111
|
+
resp = await sess.options(f'https://localhost:{hport}/api/ext/testpath00')
|
|
112
|
+
self.eq(resp.status, 204)
|
|
113
|
+
self.eq(resp.headers.get('Secret-Header'), 'Options')
|
|
114
|
+
# HTTP 204 code has no response content per rfc9110
|
|
115
|
+
self.eq(await resp.read(), b'')
|
|
116
|
+
|
|
117
|
+
resp = await sess.delete(f'https://localhost:{hport}/api/ext/testpath00')
|
|
118
|
+
self.eq(resp.status, 205)
|
|
119
|
+
data = await resp.json()
|
|
120
|
+
self.eq(data.get('method'), 'delete')
|
|
121
|
+
|
|
122
|
+
resp = await sess.head(f'https://localhost:{hport}/api/ext/testpath00')
|
|
123
|
+
self.eq(resp.status, 206)
|
|
124
|
+
self.eq(resp.headers.get('Secret-Header'), 'Head')
|
|
125
|
+
self.eq(resp.headers.get('Content-Length'), '14')
|
|
126
|
+
# HEAD had no body in its response
|
|
127
|
+
self.eq(await resp.read(), b'')
|
|
128
|
+
|
|
129
|
+
# exercise _gtors
|
|
130
|
+
q = '''$api = $lib.cortex.httpapi.get($iden)
|
|
131
|
+
$ret = ({
|
|
132
|
+
"get": $api.methods.get,
|
|
133
|
+
"put": $api.methods.put,
|
|
134
|
+
"head": $api.methods.head,
|
|
135
|
+
"post": $api.methods.post,
|
|
136
|
+
"patch": $api.methods.patch,
|
|
137
|
+
"delete": $api.methods.delete,
|
|
138
|
+
"options": $api.methods.options,
|
|
139
|
+
})
|
|
140
|
+
return ($ret)
|
|
141
|
+
'''
|
|
142
|
+
queries = await core.callStorm(q, opts={'vars': {'iden': testpath00}})
|
|
143
|
+
self.len(7, queries)
|
|
144
|
+
|
|
145
|
+
# Stat the api to enumerate all its method _gtors
|
|
146
|
+
msgs = await core.stormlist('cortex.httpapi.stat $iden', opts={'vars': {'iden': testpath00}})
|
|
147
|
+
self.stormIsInPrint(f'Iden: {testpath00}', msgs)
|
|
148
|
+
self.stormIsInPrint('Method: GET', msgs)
|
|
149
|
+
self.stormIsInPrint('Method: PUT', msgs)
|
|
150
|
+
self.stormIsInPrint('Method: POST', msgs)
|
|
151
|
+
self.stormIsInPrint('Method: PATCH', msgs)
|
|
152
|
+
self.stormIsInPrint('Method: OPTIONS', msgs)
|
|
153
|
+
self.stormIsInPrint('Method: DELETE', msgs)
|
|
154
|
+
self.stormIsInPrint('Method: HEAD', msgs)
|
|
155
|
+
|
|
156
|
+
# Unset a method and try to use it
|
|
157
|
+
q = '$api = $lib.cortex.httpapi.get($iden) $api.methods.post = $lib.undef'
|
|
158
|
+
await core.callStorm(q, opts={'vars': {'iden': testpath00}})
|
|
159
|
+
resp = await sess.post(f'https://localhost:{hport}/api/ext/testpath00')
|
|
160
|
+
self.eq(resp.status, 405)
|
|
161
|
+
self.eq(resp.headers.get('Allowed'), 'GET, PUT, PATCH, OPTIONS, DELETE, HEAD')
|
|
162
|
+
data = await resp.json()
|
|
163
|
+
self.eq(data.get('mesg'), f'Extended HTTP API {testpath00} has no method for POST. Supports GET, PUT, PATCH, OPTIONS, DELETE, HEAD.')
|
|
164
|
+
|
|
165
|
+
# Unsetting a HEAD method and calling it yields a 500
|
|
166
|
+
# but still has no body in the response.
|
|
167
|
+
q = '$api = $lib.cortex.httpapi.get($iden) $api.methods.head = $lib.undef'
|
|
168
|
+
await core.callStorm(q, opts={'vars': {'iden': testpath00}})
|
|
169
|
+
resp = await sess.head(f'https://localhost:{hport}/api/ext/testpath00')
|
|
170
|
+
self.eq(resp.status, 405)
|
|
171
|
+
self.eq(resp.headers.get('Allowed'), 'GET, PUT, PATCH, OPTIONS, DELETE')
|
|
172
|
+
self.eq(await resp.read(), b'')
|
|
173
|
+
|
|
174
|
+
# No methods returns a 405 and nothing allowed
|
|
175
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/nomeths')
|
|
176
|
+
self.eq(resp.status, 405)
|
|
177
|
+
self.eq(resp.headers.get('Allowed'), '')
|
|
178
|
+
data = await resp.json()
|
|
179
|
+
self.eq(data.get('mesg'), f'Extended HTTP API {nomeths} has no method for GET.')
|
|
180
|
+
|
|
181
|
+
msgs = await core.stormlist('cortex.httpapi.stat $iden', opts={'vars': {'iden': testpath00}})
|
|
182
|
+
self.stormNotInPrint('Method: POST', msgs)
|
|
183
|
+
self.stormNotInPrint('Method: HEAD', msgs)
|
|
184
|
+
self.stormIsInPrint('Method: GET', msgs)
|
|
185
|
+
self.stormIsInPrint('Method: PUT', msgs)
|
|
186
|
+
self.stormIsInPrint('Method: PATCH', msgs)
|
|
187
|
+
self.stormIsInPrint('Method: OPTIONS', msgs)
|
|
188
|
+
self.stormIsInPrint('Method: DELETE', msgs)
|
|
189
|
+
|
|
190
|
+
q = '''$api = $lib.cortex.httpapi.add(testpath01)
|
|
191
|
+
$api.methods.get = ${ $request.reply(200, body=({"hehe": "haha"})) }
|
|
192
|
+
return ( $api.iden )'''
|
|
193
|
+
testpath01 = await core.callStorm(q)
|
|
194
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/testpath01')
|
|
195
|
+
self.eq(resp.status, 200)
|
|
196
|
+
data = await resp.json()
|
|
197
|
+
self.eq(data.get('hehe'), 'haha')
|
|
198
|
+
|
|
199
|
+
q = '$lib.cortex.httpapi.del($iden)'
|
|
200
|
+
msgs = await core.stormlist(q, opts={'vars': {'iden': testpath01}})
|
|
201
|
+
self.stormHasNoWarnErr(msgs)
|
|
202
|
+
|
|
203
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/testpath01')
|
|
204
|
+
self.eq(resp.status, 404)
|
|
205
|
+
data = await resp.json()
|
|
206
|
+
self.eq(data.get('code'), 'NoSuchPath')
|
|
207
|
+
self.eq(data.get('mesg'), 'No Extended HTTP API endpoint matches testpath01')
|
|
208
|
+
|
|
209
|
+
# Test method reply types
|
|
210
|
+
q = '''$api = $lib.cortex.httpapi.add(testreply)
|
|
211
|
+
$api.methods.post = ${ $request.reply(200, body=$request.json.data) }
|
|
212
|
+
return ( $api.iden )'''
|
|
213
|
+
testreply = await core.callStorm(q)
|
|
214
|
+
edata = ('hello',
|
|
215
|
+
31337,
|
|
216
|
+
['a', 2],
|
|
217
|
+
{'key': 'valu', 'hehe': [1, '2']},
|
|
218
|
+
None,
|
|
219
|
+
)
|
|
220
|
+
for valu in edata:
|
|
221
|
+
resp = await sess.post(f'https://localhost:{hport}/api/ext/testreply',
|
|
222
|
+
json={'data': valu}
|
|
223
|
+
)
|
|
224
|
+
self.eq(resp.status, 200)
|
|
225
|
+
data = await resp.json()
|
|
226
|
+
self.eq(data, valu)
|
|
227
|
+
|
|
228
|
+
# Echo handler returns the request data.
|
|
229
|
+
# This also shows some flattening of request data we do.
|
|
230
|
+
|
|
231
|
+
q = '''$api = $lib.cortex.httpapi.add('echo/(.*)/([a-zA-Z0-9]*)?')
|
|
232
|
+
$api.methods.get = ${
|
|
233
|
+
$data = ({
|
|
234
|
+
"echo": $lib.true,
|
|
235
|
+
"method": $request.method,
|
|
236
|
+
"headers": $request.headers,
|
|
237
|
+
"params": $request.params,
|
|
238
|
+
"uri": $request.uri,
|
|
239
|
+
"path": $request.path,
|
|
240
|
+
"client": $request.client,
|
|
241
|
+
"args": $request.args,
|
|
242
|
+
})
|
|
243
|
+
try {
|
|
244
|
+
$data.json = $request.json
|
|
245
|
+
} catch StormRuntimeError as err {
|
|
246
|
+
$data.json = "err"
|
|
247
|
+
}
|
|
248
|
+
$headers = ({'Echo': 'hehe!'})
|
|
249
|
+
$request.reply(200, headers=$headers, body=$data)
|
|
250
|
+
}
|
|
251
|
+
return ( $api.iden )
|
|
252
|
+
'''
|
|
253
|
+
echoiden = await core.callStorm(q)
|
|
254
|
+
|
|
255
|
+
url = f'https://lowuser:secret@localhost:{hport}/api/ext/echo/sup/?echo=test&giggle=haha&echo=eggs'
|
|
256
|
+
resp = await sess.get(url, headers=(('hehe', 'haha'), ('apikey', 'secret'), ('hehe', 'badjoke')),
|
|
257
|
+
json={'look': 'at this!'},
|
|
258
|
+
)
|
|
259
|
+
self.eq(resp.status, 200)
|
|
260
|
+
data = await resp.json()
|
|
261
|
+
self.eq(data.get('args'), ['sup', ''])
|
|
262
|
+
self.eq(data.get('echo'), True)
|
|
263
|
+
self.eq(data.get('headers').get('hehe'), 'haha')
|
|
264
|
+
self.eq(data.get('headers').get('apikey'), 'secret')
|
|
265
|
+
self.eq(data.get('json'), {'look': 'at this!'})
|
|
266
|
+
self.eq(data.get('method'), 'GET')
|
|
267
|
+
self.eq(data.get('params'), {'echo': 'test', 'giggle': 'haha'})
|
|
268
|
+
self.eq(data.get('path'), 'echo/sup/')
|
|
269
|
+
self.isin('client', data)
|
|
270
|
+
self.eq(data.get('uri'), '/api/ext/echo/sup/?echo=test&giggle=haha&echo=eggs')
|
|
271
|
+
|
|
272
|
+
url = f'https://lowuser:secret@localhost:{hport}/api/ext/echo/words/wOw'
|
|
273
|
+
resp = await sess.get(url, headers=(('hehe', 'haha'), ('apikey', 'secret'), ('hehe', 'badjoke')),
|
|
274
|
+
data=b'hehe',
|
|
275
|
+
)
|
|
276
|
+
self.eq(resp.status, 200)
|
|
277
|
+
data = await resp.json()
|
|
278
|
+
self.eq(data.get('args'), ['words', 'wOw'])
|
|
279
|
+
self.eq(data.get('json'), 'err')
|
|
280
|
+
self.eq(data.get('path'), 'echo/words/wOw')
|
|
281
|
+
|
|
282
|
+
# Sad paths on the $request methods
|
|
283
|
+
q = '''$api = $lib.cortex.httpapi.add(testpath02)
|
|
284
|
+
$api.methods.get = ${ $request.sendcode(200) $request.sendheaders('beep beep') }
|
|
285
|
+
return ( $api.iden )'''
|
|
286
|
+
testpath02 = await core.callStorm(q)
|
|
287
|
+
|
|
288
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/testpath02')
|
|
289
|
+
self.eq(resp.status, 500)
|
|
290
|
+
data = await resp.json()
|
|
291
|
+
self.eq(data.get('code'), 'BadArg')
|
|
292
|
+
self.eq(data.get('mesg'), 'HTTP Response headers must be a dictionary, got str.')
|
|
293
|
+
|
|
294
|
+
q = '''$api = $lib.cortex.httpapi.add(testpath03)
|
|
295
|
+
$api.methods.get = ${ $request.sendcode(200) $request.sendbody('beep beep') }
|
|
296
|
+
return ( $api.iden )'''
|
|
297
|
+
testpath03 = await core.callStorm(q)
|
|
298
|
+
|
|
299
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/testpath03')
|
|
300
|
+
self.eq(resp.status, 500)
|
|
301
|
+
data = await resp.json()
|
|
302
|
+
self.eq(data.get('code'), 'BadArg')
|
|
303
|
+
self.eq(data.get('mesg'), 'HTTP Response body must be bytes, got str.')
|
|
304
|
+
|
|
305
|
+
q = '''$api = $lib.cortex.httpapi.add(testpath04)
|
|
306
|
+
$api.methods.get = ${ $request.reply(200, body=({"hehe": "yes!"})) $request.reply(201, body=({"hehe":" newp"})) }
|
|
307
|
+
return ( $api.iden )'''
|
|
308
|
+
test04 = await core.callStorm(q)
|
|
309
|
+
|
|
310
|
+
emsg = f'Error executing custom HTTP API {test04}: BadArg Response.reply() has already been called.'
|
|
311
|
+
with self.getAsyncLoggerStream('synapse.lib.httpapi', emsg) as stream:
|
|
312
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/testpath04')
|
|
313
|
+
self.eq(resp.status, 200)
|
|
314
|
+
self.eq(await resp.json(), {'hehe': 'yes!'})
|
|
315
|
+
|
|
316
|
+
async def test_libcortex_httpapi_runas_owner(self):
|
|
317
|
+
async with self.getTestCore() as core:
|
|
318
|
+
udef = await core.addUser('lowuser')
|
|
319
|
+
lowuser = udef.get('iden')
|
|
320
|
+
await core.setUserPasswd(core.auth.rootuser.iden, 'root')
|
|
321
|
+
await core.setUserPasswd(lowuser, 'secret')
|
|
322
|
+
addr, hport = await core.addHttpsPort(0)
|
|
323
|
+
|
|
324
|
+
q = '''$api = $lib.cortex.httpapi.add(testpath00)
|
|
325
|
+
$api.methods.get = ${
|
|
326
|
+
$view = $lib.view.get()
|
|
327
|
+
$request.reply(200, body=({'view': $view.iden, "username": $lib.user.name()}) )
|
|
328
|
+
}
|
|
329
|
+
return ( ($api.iden, $api.owner.name) )
|
|
330
|
+
'''
|
|
331
|
+
iden, uname = await core.callStorm(q)
|
|
332
|
+
self.eq(uname, 'root')
|
|
333
|
+
|
|
334
|
+
async with self.getHttpSess(auth=('root', 'root'), port=hport) as sess: # type: aiohttp.ClientSession
|
|
335
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/testpath00')
|
|
336
|
+
self.eq(resp.status, 200)
|
|
337
|
+
data = await resp.json()
|
|
338
|
+
self.eq(data.get('username'), 'root')
|
|
339
|
+
|
|
340
|
+
q = '''$api=$lib.cortex.httpapi.get($http_iden)
|
|
341
|
+
$user=$lib.auth.users.byname(lowuser) $api.owner = $user
|
|
342
|
+
return ($api.owner.name)'''
|
|
343
|
+
name = await core.callStorm(q, opts={'vars': {'http_iden': iden}})
|
|
344
|
+
self.eq(name, 'lowuser')
|
|
345
|
+
|
|
346
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/testpath00')
|
|
347
|
+
self.eq(resp.status, 200)
|
|
348
|
+
data = await resp.json()
|
|
349
|
+
self.eq(data.get('username'), 'lowuser')
|
|
350
|
+
|
|
351
|
+
q = '''
|
|
352
|
+
$api=$lib.cortex.httpapi.get($http_iden)
|
|
353
|
+
$api.runas = user
|
|
354
|
+
return ($api.runas)'''
|
|
355
|
+
name = await core.callStorm(q, opts={'vars': {'http_iden': iden}})
|
|
356
|
+
self.eq(name, 'user')
|
|
357
|
+
|
|
358
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/testpath00')
|
|
359
|
+
self.eq(resp.status, 200)
|
|
360
|
+
data = await resp.json()
|
|
361
|
+
self.eq(data.get('username'), 'root')
|
|
362
|
+
|
|
363
|
+
async with self.getHttpSess(auth=('lowuser', 'secret'), port=hport) as sess: # type: aiohttp.ClientSession
|
|
364
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/testpath00')
|
|
365
|
+
self.eq(resp.status, 200)
|
|
366
|
+
data = await resp.json()
|
|
367
|
+
self.eq(data.get('username'), 'lowuser')
|
|
368
|
+
|
|
369
|
+
# Set the user by iden
|
|
370
|
+
q = '''$api=$lib.cortex.httpapi.get($http_iden)
|
|
371
|
+
$user=$lib.auth.users.byname(root) $api.owner = $user.iden
|
|
372
|
+
return ($api.owner.name)'''
|
|
373
|
+
name = await core.callStorm(q, opts={'vars': {'http_iden': iden}})
|
|
374
|
+
self.eq(name, 'root')
|
|
375
|
+
|
|
376
|
+
async def test_libcortex_httpapi_order_stat(self):
|
|
377
|
+
|
|
378
|
+
async with self.getTestCore() as core:
|
|
379
|
+
udef = await core.addUser('lowuser')
|
|
380
|
+
lowuser = udef.get('iden')
|
|
381
|
+
await core.setUserPasswd(core.auth.rootuser.iden, 'root')
|
|
382
|
+
await core.setUserPasswd(lowuser, 'secret')
|
|
383
|
+
addr, hport = await core.addHttpsPort(0)
|
|
384
|
+
|
|
385
|
+
# Define a few handlers
|
|
386
|
+
q = '''
|
|
387
|
+
$api = $lib.cortex.httpapi.add('hehe/([a-z0-9]*)')
|
|
388
|
+
$api.methods.get = ${ $request.reply(200, headers=({"yup": "wildcard"}), body=({"path": $request.path})) }
|
|
389
|
+
$api.methods.head = ${ $request.reply(200, headers=({"yup": "wildcard"})) }
|
|
390
|
+
$api.name = 'the hehe wildcard handler'
|
|
391
|
+
$api.desc = 'wildcard words'
|
|
392
|
+
$api.runas = owner
|
|
393
|
+
return ( $api.iden )
|
|
394
|
+
'''
|
|
395
|
+
iden0 = await core.callStorm(q)
|
|
396
|
+
|
|
397
|
+
q = '''
|
|
398
|
+
$api = $lib.cortex.httpapi.add('hehe/haha')
|
|
399
|
+
$api.methods.get = ${ $request.reply(200, headers=({"yup": "hehe/haha"}), body=({"path": $request.path})) }
|
|
400
|
+
$api.methods.head = ${ $request.reply(200, headers=({"yup": "hehe/haha"}) ) }
|
|
401
|
+
$api.name = 'the hehe/haha handler'
|
|
402
|
+
$api.desc = 'beep boop zoop robot captain'
|
|
403
|
+
$api.runas = user
|
|
404
|
+
$api.perms = (
|
|
405
|
+
({"perm": ["hehe", "haha"]}),
|
|
406
|
+
({"perm": ["some", "thing"], "default": $lib.true}),
|
|
407
|
+
)
|
|
408
|
+
return ( $api.iden )
|
|
409
|
+
'''
|
|
410
|
+
iden1 = await core.callStorm(q)
|
|
411
|
+
|
|
412
|
+
q = '''
|
|
413
|
+
$api = $lib.cortex.httpapi.add('hehe')
|
|
414
|
+
$api.methods.get = ${ $request.reply(200, headers=({"yup": "hehe"}), body=({"path": $request.path})) }
|
|
415
|
+
$api.methods.head = ${ $request.reply(200, headers=({"yup": "hehe"})) }
|
|
416
|
+
$api.authenticated = $lib.false
|
|
417
|
+
$api.owner = $lowuser
|
|
418
|
+
return ( $api.iden )
|
|
419
|
+
'''
|
|
420
|
+
iden2 = await core.callStorm(q, opts={'vars': {'lowuser': lowuser}})
|
|
421
|
+
|
|
422
|
+
q = '''
|
|
423
|
+
$api = $lib.cortex.httpapi.add('wow')
|
|
424
|
+
$api.methods.get = ${ $request.reply(200, body=({"path": $request.path})) }
|
|
425
|
+
$api.authenticated = $lib.false
|
|
426
|
+
$api.vars.hehe = wow
|
|
427
|
+
$api.vars.items = (1, 2, (3) )
|
|
428
|
+
return ( $api.iden )
|
|
429
|
+
'''
|
|
430
|
+
iden3 = await core.callStorm(q)
|
|
431
|
+
|
|
432
|
+
msgs = await core.stormlist('cortex.httpapi.list')
|
|
433
|
+
self.stormIsInPrint(f'0 {iden0}', msgs)
|
|
434
|
+
self.stormIsInPrint(f'1 {iden1}', msgs)
|
|
435
|
+
self.stormIsInPrint(f'2 {iden2}', msgs)
|
|
436
|
+
self.stormIsInPrint(f'3 {iden3}', msgs)
|
|
437
|
+
|
|
438
|
+
# Order matters. The hehe/haha path occurs after the wildcard.
|
|
439
|
+
async with self.getHttpSess(auth=('root', 'root'), port=hport) as sess:
|
|
440
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/hehe/haha')
|
|
441
|
+
data = await resp.json()
|
|
442
|
+
self.eq(data.get('path'), 'hehe/haha')
|
|
443
|
+
self.eq(resp.headers.get('yup'), 'wildcard')
|
|
444
|
+
|
|
445
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/hehe/ohmy1234')
|
|
446
|
+
data = await resp.json()
|
|
447
|
+
self.eq(data.get('path'), 'hehe/ohmy1234')
|
|
448
|
+
self.eq(resp.headers.get('yup'), 'wildcard')
|
|
449
|
+
|
|
450
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/hehe')
|
|
451
|
+
data = await resp.json()
|
|
452
|
+
self.eq(data.get('path'), 'hehe')
|
|
453
|
+
self.eq(resp.headers.get('yup'), 'hehe')
|
|
454
|
+
|
|
455
|
+
# Move the wildcard handler after the more specific handler
|
|
456
|
+
msgs = await core.stormlist('cortex.httpapi.index $iden 1', opts={'vars': {'iden': iden0}})
|
|
457
|
+
self.stormIsInPrint(f'Set HTTP API {iden0} to index 1', msgs)
|
|
458
|
+
|
|
459
|
+
msgs = await core.stormlist('cortex.httpapi.list')
|
|
460
|
+
self.stormIsInPrint(f'0 {iden1}', msgs)
|
|
461
|
+
self.stormIsInPrint(f'1 {iden0}', msgs)
|
|
462
|
+
self.stormIsInPrint(f'2 {iden2}', msgs)
|
|
463
|
+
self.stormIsInPrint(f'3 {iden3}', msgs)
|
|
464
|
+
|
|
465
|
+
# The wildcard handler does not match the more specific request as a result of the new order
|
|
466
|
+
async with self.getHttpSess(auth=('root', 'root'), port=hport) as sess:
|
|
467
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/hehe/haha')
|
|
468
|
+
data = await resp.json()
|
|
469
|
+
self.eq(data.get('path'), 'hehe/haha')
|
|
470
|
+
self.eq(resp.headers.get('yup'), 'hehe/haha')
|
|
471
|
+
|
|
472
|
+
resp = await sess.head(f'https://localhost:{hport}/api/ext/hehe/haha')
|
|
473
|
+
data = await resp.read()
|
|
474
|
+
self.eq(data, b'')
|
|
475
|
+
self.eq(resp.headers.get('yup'), 'hehe/haha')
|
|
476
|
+
self.none(resp.headers.get('Etag')) # No etag is computed for ext api
|
|
477
|
+
|
|
478
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/hehe/ohmy1234')
|
|
479
|
+
data = await resp.json()
|
|
480
|
+
self.eq(data.get('path'), 'hehe/ohmy1234')
|
|
481
|
+
self.eq(resp.headers.get('yup'), 'wildcard')
|
|
482
|
+
|
|
483
|
+
# The paths are matched in case sensitive manner. The current regex fails to match.
|
|
484
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/hehe/OhMy1234')
|
|
485
|
+
self.eq(resp.status, 404)
|
|
486
|
+
|
|
487
|
+
q = "$api=$lib.cortex.httpapi.get($iden) $api.path='hehe/([A-Za-z0-9]*)' "
|
|
488
|
+
await core.callStorm(q, opts={'vars': {'iden': iden0}})
|
|
489
|
+
|
|
490
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/hehe/OhMy1234')
|
|
491
|
+
data = await resp.json()
|
|
492
|
+
self.eq(data.get('path'), 'hehe/OhMy1234')
|
|
493
|
+
self.eq(resp.headers.get('yup'), 'wildcard')
|
|
494
|
+
|
|
495
|
+
# We can move the endpoint to the end of the list too. An arbitrary high
|
|
496
|
+
# index will place it at the end.
|
|
497
|
+
msgs = await core.stormlist('cortex.httpapi.index $iden 100', opts={'vars': {'iden': iden0}})
|
|
498
|
+
self.stormIsInPrint(f'Set HTTP API {iden0} to index 3', msgs)
|
|
499
|
+
|
|
500
|
+
msgs = await core.stormlist('cortex.httpapi.list')
|
|
501
|
+
self.stormIsInPrint(f'0 {iden1}', msgs)
|
|
502
|
+
self.stormIsInPrint(f'1 {iden2}', msgs)
|
|
503
|
+
self.stormIsInPrint(f'2 {iden3}', msgs)
|
|
504
|
+
self.stormIsInPrint(f'3 {iden0}', msgs)
|
|
505
|
+
|
|
506
|
+
# iden Prefix + name matching
|
|
507
|
+
msgs = await core.stormlist('cortex.httpapi.index $iden 0', opts={'vars': {'iden': iden1[:6]}})
|
|
508
|
+
self.stormIsInPrint(f'Set HTTP API {iden1} to index 0', msgs)
|
|
509
|
+
|
|
510
|
+
msgs = await core.stormlist('cortex.httpapi.index $iden 0', opts={'vars': {'iden': 'the hehe/haha'}})
|
|
511
|
+
self.stormIsInPrint(f'Set HTTP API {iden1} to index 0', msgs)
|
|
512
|
+
|
|
513
|
+
# No match
|
|
514
|
+
msgs = await core.stormlist('cortex.httpapi.index $iden 0', opts={'vars': {'iden': 'newp'}})
|
|
515
|
+
self.stormIsInErr('Failed to match Extended HTTP API by iden or name!', msgs)
|
|
516
|
+
|
|
517
|
+
# too many matches
|
|
518
|
+
msgs = await core.stormlist('cortex.httpapi.index $iden 0', opts={'vars': {'iden': 'the'}})
|
|
519
|
+
self.stormIsInErr('Already matched one Extended HTTP API!', msgs)
|
|
520
|
+
|
|
521
|
+
# Show detailed information for a given api
|
|
522
|
+
msgs = await core.stormlist('cortex.httpapi.stat $iden', opts={'vars': {'iden': iden0}})
|
|
523
|
+
self.stormIsInPrint(f'Iden: {iden0}', msgs)
|
|
524
|
+
self.stormIsInPrint('Owner: root', msgs)
|
|
525
|
+
self.stormIsInPrint('Runas: owner', msgs)
|
|
526
|
+
self.stormIsInPrint('Readonly: false', msgs)
|
|
527
|
+
self.stormIsInPrint('Authenticated: true', msgs)
|
|
528
|
+
self.stormIsInPrint('Name: the hehe wildcard handler', msgs)
|
|
529
|
+
self.stormIsInPrint('Description: wildcard words', msgs)
|
|
530
|
+
self.stormIsInPrint('No user permissions are required to run this HTTP API endpoint.', msgs)
|
|
531
|
+
self.stormIsInPrint('Method: GET', msgs)
|
|
532
|
+
self.stormIsInPrint('$request.reply(200, headers=({"yup": "wildcard"}), body=({"path": $request.path}))',
|
|
533
|
+
msgs)
|
|
534
|
+
self.stormIsInPrint(f'Method: HEAD', msgs)
|
|
535
|
+
self.stormIsInPrint('$request.reply(200, headers=({"yup": "wildcard"}))', msgs)
|
|
536
|
+
self.stormIsInPrint('No vars are set for the handler.', msgs)
|
|
537
|
+
|
|
538
|
+
# iden Prefix + name matching
|
|
539
|
+
msgs = await core.stormlist('cortex.httpapi.stat $iden', opts={'vars': {'iden': iden0[:6]}})
|
|
540
|
+
self.stormIsInPrint(f'Iden: {iden0}', msgs)
|
|
541
|
+
|
|
542
|
+
msgs = await core.stormlist('cortex.httpapi.stat $iden', opts={'vars': {'iden': 'the hehe wildcard handler'}})
|
|
543
|
+
self.stormIsInPrint(f'Iden: {iden0}', msgs)
|
|
544
|
+
|
|
545
|
+
# No match
|
|
546
|
+
msgs = await core.stormlist('cortex.httpapi.stat $iden', opts={'vars': {'iden': 'newp'}})
|
|
547
|
+
self.stormIsInErr('Failed to match Extended HTTP API by iden or name!', msgs)
|
|
548
|
+
|
|
549
|
+
# too many matches
|
|
550
|
+
msgs = await core.stormlist('cortex.httpapi.stat $iden', opts={'vars': {'iden': 'the'}})
|
|
551
|
+
self.stormIsInErr('Already matched one Extended HTTP API!', msgs)
|
|
552
|
+
|
|
553
|
+
msgs = await core.stormlist('cortex.httpapi.stat $iden', opts={'vars': {'iden': iden1}})
|
|
554
|
+
self.stormIsInPrint(f'Iden: {iden1}', msgs)
|
|
555
|
+
self.stormIsInPrint('Creator: root', msgs)
|
|
556
|
+
self.stormIsInPrint('Created: ', msgs)
|
|
557
|
+
self.stormIsInPrint('Updated: ', msgs)
|
|
558
|
+
self.stormIsInPrint('Owner: root', msgs)
|
|
559
|
+
self.stormIsInPrint('Runas: user', msgs)
|
|
560
|
+
self.stormIsInPrint('Readonly: false', msgs)
|
|
561
|
+
self.stormIsInPrint('Authenticated: true', msgs)
|
|
562
|
+
self.stormIsInPrint('Name: the hehe/haha handler', msgs)
|
|
563
|
+
self.stormIsInPrint('Description: beep boop zoop robot captain', msgs)
|
|
564
|
+
self.stormIsInPrint('The following user permissions are required to run this HTTP API endpoint:',
|
|
565
|
+
msgs)
|
|
566
|
+
self.stormIsInPrint('hehe.haha\n default: false', msgs)
|
|
567
|
+
self.stormIsInPrint('some.thing\n default: true', msgs)
|
|
568
|
+
|
|
569
|
+
msgs = await core.stormlist('cortex.httpapi.stat $iden', opts={'vars': {'iden': iden3}})
|
|
570
|
+
self.stormIsInPrint(f'Iden: {iden3}', msgs)
|
|
571
|
+
self.stormIsInPrint('The handler has the following runtime variables set:', msgs)
|
|
572
|
+
self.stormIsInPrint('hehe => wow', msgs)
|
|
573
|
+
self.stormIsInPrint("items => ('1', '2', 3)", msgs)
|
|
574
|
+
|
|
575
|
+
# Remove a user + view and stat the handler
|
|
576
|
+
badview = await core.callStorm('''$view=$lib.view.get().fork() $lib.auth.users.del($user)
|
|
577
|
+
$api=$lib.cortex.httpapi.get($iden)
|
|
578
|
+
$api.view=$view.iden
|
|
579
|
+
$lib.view.del($view.iden)
|
|
580
|
+
return ( $view.iden )
|
|
581
|
+
''',
|
|
582
|
+
opts={'vars': {'user': lowuser, 'iden': iden2}})
|
|
583
|
+
|
|
584
|
+
msgs = await core.stormlist('cortex.httpapi.stat $iden', opts={'vars': {'iden': iden2}})
|
|
585
|
+
self.stormIsInPrint(f'Iden: {iden2}', msgs)
|
|
586
|
+
self.stormIsInPrint(f'!Owner: No user found ({lowuser})', msgs)
|
|
587
|
+
self.stormIsInPrint(f'!View: No view found ({badview})', msgs)
|
|
588
|
+
|
|
589
|
+
# Paths must be valid regular expressions when created or modified
|
|
590
|
+
q = '''$api=$lib.cortex.httpapi.add("hehehe/hahaha)") return ($api.iden)'''
|
|
591
|
+
with self.raises(s_exc.BadArg) as cm:
|
|
592
|
+
await core.callStorm(q)
|
|
593
|
+
|
|
594
|
+
q = '''$api=$lib.cortex.httpapi.get($iden) $api.path="newp/(stuff" '''
|
|
595
|
+
with self.raises(s_exc.BadArg) as cm:
|
|
596
|
+
ret = await core.callStorm(q, opts={'vars': {'iden': iden0}})
|
|
597
|
+
|
|
598
|
+
# Creator / Created / Updated may not be set
|
|
599
|
+
q = '''$api=$lib.cortex.httpapi.get($iden) $api.creator=$valu'''
|
|
600
|
+
with self.raises(s_exc.NoSuchName) as cm:
|
|
601
|
+
await core.callStorm(q, opts={'vars': {'iden': iden0, 'valu': lowuser}})
|
|
602
|
+
|
|
603
|
+
valu = 0x01020304
|
|
604
|
+
q = '''$api=$lib.cortex.httpapi.get($iden) $api.created=$valu'''
|
|
605
|
+
with self.raises(s_exc.NoSuchName) as cm:
|
|
606
|
+
await core.callStorm(q, opts={'vars': {'iden': iden0, 'valu': valu}})
|
|
607
|
+
|
|
608
|
+
q = '''$api=$lib.cortex.httpapi.get($iden) $api.updated=$valu'''
|
|
609
|
+
with self.raises(s_exc.NoSuchName) as cm:
|
|
610
|
+
await core.callStorm(q, opts={'vars': {'iden': iden0, 'valu': valu}})
|
|
611
|
+
|
|
612
|
+
async def test_libcortex_httpapi_auth(self):
|
|
613
|
+
async with self.getTestCore() as core:
|
|
614
|
+
udef = await core.addUser('lowuser')
|
|
615
|
+
lowuser = udef.get('iden')
|
|
616
|
+
root = core.auth.rootuser.iden
|
|
617
|
+
await core.setUserPasswd(root, 'root')
|
|
618
|
+
await core.setUserPasswd(lowuser, 'secret')
|
|
619
|
+
addr, hport = await core.addHttpsPort(0)
|
|
620
|
+
|
|
621
|
+
q = '''$api = $lib.cortex.httpapi.add('auth')
|
|
622
|
+
$api.methods.get = ${
|
|
623
|
+
$request.reply(200, body=({"username": $lib.user.name(), "user": $request.user}) )
|
|
624
|
+
}
|
|
625
|
+
return ( $api.iden )
|
|
626
|
+
'''
|
|
627
|
+
iden = await core.callStorm(q)
|
|
628
|
+
|
|
629
|
+
async with self.getHttpSess(auth=('root', 'root'), port=hport) as sess:
|
|
630
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/auth')
|
|
631
|
+
self.eq(resp.status, 200)
|
|
632
|
+
data = await resp.json()
|
|
633
|
+
self.eq(data.get('username'), 'root')
|
|
634
|
+
self.eq(data.get('user'), root)
|
|
635
|
+
|
|
636
|
+
async with self.getHttpSess() as sess:
|
|
637
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/auth')
|
|
638
|
+
self.eq(resp.status, 401)
|
|
639
|
+
data = await resp.json()
|
|
640
|
+
self.eq(data.get('code'), 'NotAuthenticated')
|
|
641
|
+
|
|
642
|
+
q = '$api = $lib.cortex.httpapi.get($iden) $api.authenticated=$lib.false'
|
|
643
|
+
msgs = await core.stormlist(q, opts={'vars': {'iden': iden}})
|
|
644
|
+
self.stormHasNoWarnErr(msgs)
|
|
645
|
+
|
|
646
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/auth')
|
|
647
|
+
self.eq(resp.status, 200)
|
|
648
|
+
data = await resp.json()
|
|
649
|
+
self.eq(data.get('username'), 'root')
|
|
650
|
+
self.eq(data.get('user'), '')
|
|
651
|
+
|
|
652
|
+
# authenticated = false + runas = user -> runs as owner
|
|
653
|
+
q = '$api = $lib.cortex.httpapi.get($iden) $api.runas=user'
|
|
654
|
+
msgs = await core.stormlist(q, opts={'vars': {'iden': iden}})
|
|
655
|
+
self.stormHasNoWarnErr(msgs)
|
|
656
|
+
|
|
657
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/auth')
|
|
658
|
+
self.eq(resp.status, 200)
|
|
659
|
+
data = await resp.json()
|
|
660
|
+
self.eq(data.get('username'), 'root')
|
|
661
|
+
self.eq(data.get('user'), '')
|
|
662
|
+
|
|
663
|
+
# The user value is populated for authenticated requests which
|
|
664
|
+
# indicates who the requester's user iden is.
|
|
665
|
+
async with self.getHttpSess(auth=('lowuser', 'secret'), port=hport) as sess:
|
|
666
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/auth')
|
|
667
|
+
self.eq(resp.status, 200)
|
|
668
|
+
data = await resp.json()
|
|
669
|
+
self.eq(data.get('username'), 'root')
|
|
670
|
+
self.eq(data.get('user'), '')
|
|
671
|
+
|
|
672
|
+
q = '$api = $lib.cortex.httpapi.get($iden) $api.authenticated=$lib.true'
|
|
673
|
+
msgs = await core.stormlist(q, opts={'vars': {'iden': iden}})
|
|
674
|
+
self.stormHasNoWarnErr(msgs)
|
|
675
|
+
|
|
676
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/auth')
|
|
677
|
+
self.eq(resp.status, 200)
|
|
678
|
+
data = await resp.json()
|
|
679
|
+
self.eq(data.get('username'), 'lowuser')
|
|
680
|
+
self.eq(data.get('user'), lowuser)
|
|
681
|
+
|
|
682
|
+
q = '$api = $lib.cortex.httpapi.get($iden) $api.runas=owner'
|
|
683
|
+
msgs = await core.stormlist(q, opts={'vars': {'iden': iden}})
|
|
684
|
+
self.stormHasNoWarnErr(msgs)
|
|
685
|
+
|
|
686
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/auth')
|
|
687
|
+
self.eq(resp.status, 200)
|
|
688
|
+
data = await resp.json()
|
|
689
|
+
self.eq(data.get('username'), 'root')
|
|
690
|
+
self.eq(data.get('user'), lowuser)
|
|
691
|
+
|
|
692
|
+
async def test_libcortex_httpapi_raw(self):
|
|
693
|
+
async with self.getTestCore() as core:
|
|
694
|
+
udef = await core.addUser('lowuser')
|
|
695
|
+
lowuser = udef.get('iden')
|
|
696
|
+
await core.setUserPasswd(core.auth.rootuser.iden, 'root')
|
|
697
|
+
await core.setUserPasswd(lowuser, 'secret')
|
|
698
|
+
addr, hport = await core.addHttpsPort(0)
|
|
699
|
+
|
|
700
|
+
# Define a handler that makes its own response headers, bytes, body.
|
|
701
|
+
q = '''
|
|
702
|
+
$api = $lib.cortex.httpapi.add('raw')
|
|
703
|
+
$api.methods.get = ${
|
|
704
|
+
$data = ({'oh': 'my'})
|
|
705
|
+
$body = $lib.json.save($data).encode()
|
|
706
|
+
$headers = ({'Secret-Header': 'OhBoy!'})
|
|
707
|
+
$headers."Content-Type" = "application/json"
|
|
708
|
+
$headers."Content-Length" = $lib.len($body)
|
|
709
|
+
$request.reply(200, headers=$headers, body=$body)
|
|
710
|
+
}
|
|
711
|
+
return ( $api.iden )
|
|
712
|
+
'''
|
|
713
|
+
await core.callStorm(q)
|
|
714
|
+
|
|
715
|
+
async with self.getHttpSess(auth=('root', 'root'), port=hport) as sess:
|
|
716
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/raw')
|
|
717
|
+
self.eq(resp.status, 200)
|
|
718
|
+
self.eq(resp.headers.get('Content-Type'), 'application/json')
|
|
719
|
+
self.eq(resp.headers.get('Content-Length'), '12')
|
|
720
|
+
self.eq(resp.headers.get('Secret-Header'), 'OhBoy!')
|
|
721
|
+
|
|
722
|
+
buf = await resp.read()
|
|
723
|
+
self.len(12, buf)
|
|
724
|
+
self.eq(json.loads(buf), {'oh': 'my'})
|
|
725
|
+
|
|
726
|
+
async def test_libcortex_httpapi_jsonlines(self):
|
|
727
|
+
async with self.getTestCore() as core:
|
|
728
|
+
await core.setUserPasswd(core.auth.rootuser.iden, 'root')
|
|
729
|
+
addr, hport = await core.addHttpsPort(0)
|
|
730
|
+
|
|
731
|
+
# Example which uses the sendcode / sendheaders / sendbody
|
|
732
|
+
# methods in order to implement a streaming API endpoint
|
|
733
|
+
|
|
734
|
+
q = '''$api = $lib.cortex.httpapi.add('jsonlines')
|
|
735
|
+
$api.methods.get = ${
|
|
736
|
+
$request.sendcode(200)
|
|
737
|
+
$request.sendheaders(({"Secret-Header": "OhBoy!", "Content-Type": "text/plain; charset=utf8"}))
|
|
738
|
+
$values = ((1), (2), (3))
|
|
739
|
+
for $i in $values {
|
|
740
|
+
$body=`{$lib.json.save(({'oh': $i}))}\\n`
|
|
741
|
+
$request.sendbody($body.encode())
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return ( $api.iden )
|
|
745
|
+
'''
|
|
746
|
+
iden00 = await core.callStorm(q)
|
|
747
|
+
|
|
748
|
+
async with self.getHttpSess(auth=('root', 'root'), port=hport) as sess:
|
|
749
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/jsonlines')
|
|
750
|
+
self.eq(resp.status, 200)
|
|
751
|
+
self.eq(resp.headers.get('Content-Type'), 'text/plain; charset=utf8')
|
|
752
|
+
|
|
753
|
+
msgs = []
|
|
754
|
+
bufr = b''
|
|
755
|
+
async for byts, x in resp.content.iter_chunks():
|
|
756
|
+
if not byts:
|
|
757
|
+
break
|
|
758
|
+
|
|
759
|
+
bufr += byts
|
|
760
|
+
for jstr in bufr.split(b'\n'):
|
|
761
|
+
if not jstr:
|
|
762
|
+
bufr = b''
|
|
763
|
+
break
|
|
764
|
+
|
|
765
|
+
try:
|
|
766
|
+
mesg = json.loads(byts)
|
|
767
|
+
except json.JSONDecodeError:
|
|
768
|
+
bufr = jstr
|
|
769
|
+
break
|
|
770
|
+
else:
|
|
771
|
+
msgs.append(mesg)
|
|
772
|
+
|
|
773
|
+
self.eq(msgs, ({'oh': 1}, {'oh': 2}, {'oh': 3}))
|
|
774
|
+
|
|
775
|
+
async def test_libcortex_httpapi_perms(self):
|
|
776
|
+
|
|
777
|
+
async with self.getTestCore() as core:
|
|
778
|
+
udef = await core.addUser('lowuser')
|
|
779
|
+
lowuser = udef.get('iden')
|
|
780
|
+
await core.setUserPasswd(core.auth.rootuser.iden, 'root')
|
|
781
|
+
await core.setUserPasswd(lowuser, 'secret')
|
|
782
|
+
addr, hport = await core.addHttpsPort(0)
|
|
783
|
+
|
|
784
|
+
# Set a handler which requires a single permission
|
|
785
|
+
|
|
786
|
+
q = '''
|
|
787
|
+
$api = $lib.cortex.httpapi.add('hehe/haha')
|
|
788
|
+
$api.methods.get = ${
|
|
789
|
+
$data = ({'path': $request.path})
|
|
790
|
+
$request.reply(200, body=$data)
|
|
791
|
+
}
|
|
792
|
+
$api.methods.head = ${
|
|
793
|
+
$request.reply(200, ({"yup": "it exists"}) )
|
|
794
|
+
}
|
|
795
|
+
$api.name = 'the hehe/haha handler'
|
|
796
|
+
$api.desc = 'beep boop zoop robot captain'
|
|
797
|
+
$api.runas = user
|
|
798
|
+
$api.perms = (foocorp.http.user, )
|
|
799
|
+
return ( $api.iden )
|
|
800
|
+
'''
|
|
801
|
+
iden0 = await core.callStorm(q)
|
|
802
|
+
|
|
803
|
+
# Set a handler which has a few permissions, using perm
|
|
804
|
+
# defs to require there is a default=true permission
|
|
805
|
+
q = '''$api = $lib.cortex.httpapi.add('weee')
|
|
806
|
+
$api.methods.get = ${
|
|
807
|
+
$data = ({'path': $request.path})
|
|
808
|
+
$request.reply(200, body=$data)
|
|
809
|
+
}
|
|
810
|
+
$api.perms = ( ({'perm': ['foocorp', 'http', 'user']}), ({'perm': ['apiuser'], 'default': $lib.true}) )
|
|
811
|
+
$api.runas = user
|
|
812
|
+
return ( $api.iden )
|
|
813
|
+
'''
|
|
814
|
+
iden1 = await core.callStorm(q)
|
|
815
|
+
|
|
816
|
+
async with self.getHttpSess(auth=('lowuser', 'secret'), port=hport) as sess:
|
|
817
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/hehe/haha')
|
|
818
|
+
self.eq(resp.status, 403)
|
|
819
|
+
data = await resp.json()
|
|
820
|
+
self.eq(data.get('mesg'), 'User (lowuser) must have permission foocorp.http.user')
|
|
821
|
+
|
|
822
|
+
await core.stormlist('auth.user.addrule lowuser foocorp.http.user')
|
|
823
|
+
|
|
824
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/hehe/haha')
|
|
825
|
+
self.eq(resp.status, 200)
|
|
826
|
+
data = await resp.json()
|
|
827
|
+
self.eq(data.get('path'), 'hehe/haha')
|
|
828
|
+
|
|
829
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/weee')
|
|
830
|
+
self.eq(resp.status, 200)
|
|
831
|
+
data = await resp.json()
|
|
832
|
+
self.eq(data.get('path'), 'weee')
|
|
833
|
+
|
|
834
|
+
# Add a deny rule for this user
|
|
835
|
+
await core.stormlist('auth.user.addrule lowuser "!apiuser"')
|
|
836
|
+
|
|
837
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/weee')
|
|
838
|
+
self.eq(resp.status, 403)
|
|
839
|
+
data = await resp.json()
|
|
840
|
+
self.eq(data.get('mesg'), 'User (lowuser) is denied the permission apiuser')
|
|
841
|
+
|
|
842
|
+
# Defining perms as !foo.bar.baz does not work
|
|
843
|
+
with self.raises(s_exc.BadArg):
|
|
844
|
+
q = "$api=$lib.cortex.httpapi.get($iden) $api.perms=('!foo.bar.baz',)"
|
|
845
|
+
await core.callStorm(q, opts={'vars': {'iden': iden0}})
|
|
846
|
+
|
|
847
|
+
# Perms are list-like objects that we can mutate
|
|
848
|
+
q = '''$api = $lib.cortex.httpapi.add('perm/mutatorr')
|
|
849
|
+
$api.perms = (hehe.haha, wow.it.works)
|
|
850
|
+
return (($api.iden, $api.perms))
|
|
851
|
+
'''
|
|
852
|
+
iden2, perms = await core.callStorm(q)
|
|
853
|
+
|
|
854
|
+
# Perform a series of modifications of permissions that should have a
|
|
855
|
+
# known end state. These exercise the HttpPermsList object
|
|
856
|
+
q = '''$api=$lib.cortex.httpapi.get($iden)
|
|
857
|
+
$perms = $api.perms
|
|
858
|
+
$perms.append(another.permission)
|
|
859
|
+
|
|
860
|
+
// We can reverse the permissions...
|
|
861
|
+
$perms.reverse()
|
|
862
|
+
|
|
863
|
+
// No sort()
|
|
864
|
+
try { $perms.sort() } catch StormRuntimeError as err { }
|
|
865
|
+
|
|
866
|
+
// Clear the perms down for extend()
|
|
867
|
+
$api.perms = (hehe.haha,)
|
|
868
|
+
$api.perms.extend( (["woah.dude", {"perm": ["giggle", "clown"], "default": $lib.true}, "a.b.c" ]) )
|
|
869
|
+
|
|
870
|
+
// Pop some values
|
|
871
|
+
$pdef = $perms.pop()
|
|
872
|
+
$pdef2 = $perms.pop()
|
|
873
|
+
|
|
874
|
+
// has
|
|
875
|
+
if $perms.has(hehe.haha) {} else { $lib.exit(mesg="test fail") }
|
|
876
|
+
if $perms.has(haha.newp) { $lib.exit(mesg="test fail") } else { }
|
|
877
|
+
if $perms.has($pdef) { $lib.exit(mesg="test fail") } else { }
|
|
878
|
+
|
|
879
|
+
// Add pdef back
|
|
880
|
+
$perms.append($pdef)
|
|
881
|
+
|
|
882
|
+
// setitem
|
|
883
|
+
$perms.0=$lib.undef
|
|
884
|
+
$old_perm = $perms.0
|
|
885
|
+
$perms.0 = $pdef2
|
|
886
|
+
|
|
887
|
+
$perms.append(c.d.e) // Add an additional perm
|
|
888
|
+
$perms.2 = something.else // Replace that perm with a perm string
|
|
889
|
+
|
|
890
|
+
// No-op
|
|
891
|
+
$perms.1000 = $lib.undef
|
|
892
|
+
|
|
893
|
+
$lib.fire(perms, ref=$api.perms, obj=$perms)
|
|
894
|
+
'''
|
|
895
|
+
msgs = await core.stormlist(q, opts={'vars': {'iden': iden2}})
|
|
896
|
+
self.stormHasNoWarnErr(msgs)
|
|
897
|
+
|
|
898
|
+
mesg = [m[1] for m in msgs if m[0] == 'storm:fire'][0]
|
|
899
|
+
adef = await core.getHttpExtApi(iden2)
|
|
900
|
+
aprm = adef.get('perms')
|
|
901
|
+
self.eq(aprm, ({'perm': ('giggle', 'clown'), 'default': True},
|
|
902
|
+
{'perm': ('a', 'b', 'c'), 'default': False},
|
|
903
|
+
{'perm': ('something', 'else'), 'default': False}),
|
|
904
|
+
|
|
905
|
+
)
|
|
906
|
+
self.eq(aprm, mesg.get('data').get('obj'))
|
|
907
|
+
self.eq(aprm, mesg.get('data').get('ref'))
|
|
908
|
+
|
|
909
|
+
with self.raises(s_exc.StormRuntimeError) as cm:
|
|
910
|
+
q = '$api=$lib.cortex.httpapi.get($iden) while $lib.true { $api.perms.pop() } '
|
|
911
|
+
await core.callStorm(q, opts={'vars': {'iden': iden2}})
|
|
912
|
+
self.eq(cm.exception.get('mesg'), 'The permissions list is empty. Nothing to pop.')
|
|
913
|
+
|
|
914
|
+
async def test_libcortex_httpapi_view(self):
|
|
915
|
+
async with self.getTestCore() as core:
|
|
916
|
+
udef = await core.addUser('lowuser')
|
|
917
|
+
lowuser = udef.get('iden')
|
|
918
|
+
await core.setUserPasswd(core.auth.rootuser.iden, 'root')
|
|
919
|
+
await core.setUserPasswd(lowuser, 'secret')
|
|
920
|
+
addr, hport = await core.addHttpsPort(0)
|
|
921
|
+
|
|
922
|
+
def_view = await core.callStorm('return ( $lib.view.get().iden )')
|
|
923
|
+
|
|
924
|
+
q = '$lyr=$lib.layer.add() $view=$lib.view.add(($lyr.iden,), name="iso view") return ( $view.iden )'
|
|
925
|
+
view = await core.callStorm(q)
|
|
926
|
+
|
|
927
|
+
q = '''$api = $lib.cortex.httpapi.add(testpath)
|
|
928
|
+
$api.methods.get = ${
|
|
929
|
+
$view = $lib.view.get()
|
|
930
|
+
$request.reply(200, body=({'view': $view.iden}) )
|
|
931
|
+
}
|
|
932
|
+
return ( $api.iden )
|
|
933
|
+
'''
|
|
934
|
+
iden = await core.callStorm(q)
|
|
935
|
+
|
|
936
|
+
async with self.getHttpSess(auth=('lowuser', 'secret'), port=hport) as sess:
|
|
937
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/testpath')
|
|
938
|
+
data = await resp.json()
|
|
939
|
+
self.eq(data.get('view'), def_view)
|
|
940
|
+
|
|
941
|
+
# Change the view the endpoint uses
|
|
942
|
+
q = '$api=$lib.cortex.httpapi.get($http_iden) $api.view = $lib.view.get($view_iden)'
|
|
943
|
+
msgs = await core.stormlist(q, opts={'vars': {'http_iden': iden, 'view_iden': view}})
|
|
944
|
+
self.stormHasNoWarnErr(msgs)
|
|
945
|
+
|
|
946
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/testpath')
|
|
947
|
+
data = await resp.json()
|
|
948
|
+
self.eq(data.get('view'), view)
|
|
949
|
+
|
|
950
|
+
# Our gtor gives a heavy view object
|
|
951
|
+
name = await core.callStorm('$api=$lib.cortex.httpapi.get($http_iden) return ($api.view.get(name))',
|
|
952
|
+
opts={'vars': {'http_iden': iden}})
|
|
953
|
+
self.eq(name, 'iso view')
|
|
954
|
+
|
|
955
|
+
# If the view is deleted out from under the object, it has no knowledge of that
|
|
956
|
+
msgs = await core.stormlist('$lib.view.del($iden)', opts={'vars': {'iden': view}})
|
|
957
|
+
self.stormHasNoWarnErr(msgs)
|
|
958
|
+
|
|
959
|
+
# The gtor throws
|
|
960
|
+
with self.raises(s_exc.NoSuchView):
|
|
961
|
+
await core.callStorm('$api=$lib.cortex.httpapi.get($http_iden) return ($api.view.get(name))',
|
|
962
|
+
opts={'vars': {'http_iden': iden}})
|
|
963
|
+
|
|
964
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/testpath')
|
|
965
|
+
self.eq(resp.status, 500)
|
|
966
|
+
data = await resp.json()
|
|
967
|
+
self.eq(data.get('code'), 'NoSuchView')
|
|
968
|
+
|
|
969
|
+
# Change the view back to the original view by iden
|
|
970
|
+
q = '$api=$lib.cortex.httpapi.get($http_iden) $api.view = $view_iden'
|
|
971
|
+
msgs = await core.stormlist(q, opts={'vars': {'http_iden': iden, 'view_iden': def_view}})
|
|
972
|
+
self.stormHasNoWarnErr(msgs)
|
|
973
|
+
|
|
974
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/testpath')
|
|
975
|
+
data = await resp.json()
|
|
976
|
+
self.eq(data.get('view'), def_view)
|
|
977
|
+
|
|
978
|
+
async def test_libcortex_httpapi_headers_params(self):
|
|
979
|
+
|
|
980
|
+
# Test around case sensitivity of request headers and parameters
|
|
981
|
+
|
|
982
|
+
async with self.getTestCore() as core:
|
|
983
|
+
udef = await core.addUser('lowuser')
|
|
984
|
+
lowuser = udef.get('iden')
|
|
985
|
+
await core.setUserPasswd(core.auth.rootuser.iden, 'root')
|
|
986
|
+
await core.setUserPasswd(lowuser, 'secret')
|
|
987
|
+
addr, hport = await core.addHttpsPort(0)
|
|
988
|
+
|
|
989
|
+
q = '''$api = $lib.cortex.httpapi.add(testpath)
|
|
990
|
+
$api.methods.get = ${
|
|
991
|
+
$data = ({
|
|
992
|
+
"Secret-Key": $request.headers."Secret-Key",
|
|
993
|
+
"secret-key": $request.headers."secret-key",
|
|
994
|
+
"aaa": $request.headers.AAA,
|
|
995
|
+
"hehe": $request.params.hehe,
|
|
996
|
+
"HeHe": $request.params.HeHe,
|
|
997
|
+
})
|
|
998
|
+
$request.reply(200, body=$data )
|
|
999
|
+
}
|
|
1000
|
+
// Cannot modify request headers
|
|
1001
|
+
$api.methods.post = ${
|
|
1002
|
+
$request.headers.newp = haha
|
|
1003
|
+
}
|
|
1004
|
+
return ( ($api.iden, $api.owner.name) )
|
|
1005
|
+
'''
|
|
1006
|
+
iden, uname = await core.callStorm(q)
|
|
1007
|
+
self.eq(uname, 'root')
|
|
1008
|
+
|
|
1009
|
+
async with self.getHttpSess(auth=('root', 'root'), port=hport) as sess:
|
|
1010
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/testpath',
|
|
1011
|
+
headers=(('secret-KEY', 'myluggagecombination'), ('aaa', 'zzz'), ('aaa', 'wtaf')),
|
|
1012
|
+
params=(('hehe', 'haha'), ('wow', 'words'), ('hehe', 'badjoke'), ('HeHe', ':)'))
|
|
1013
|
+
)
|
|
1014
|
+
self.eq(resp.status, 200)
|
|
1015
|
+
data = await resp.json()
|
|
1016
|
+
|
|
1017
|
+
# Params are flattened and case-sensitive upon access
|
|
1018
|
+
self.eq(data.get('hehe'), 'haha')
|
|
1019
|
+
self.eq(data.get('HeHe'), ':)')
|
|
1020
|
+
|
|
1021
|
+
# Headers are flattened and NOT case-sensitive upon access
|
|
1022
|
+
self.eq(data.get('aaa'), 'zzz')
|
|
1023
|
+
self.eq(data.get('Secret-Key'), 'myluggagecombination')
|
|
1024
|
+
self.eq(data.get('secret-key'), 'myluggagecombination')
|
|
1025
|
+
|
|
1026
|
+
resp = await sess.post(f'https://localhost:{hport}/api/ext/testpath')
|
|
1027
|
+
self.eq(resp.status, 500)
|
|
1028
|
+
data = await resp.json()
|
|
1029
|
+
self.eq(data.get('code'), 'StormRuntimeError')
|
|
1030
|
+
self.eq(data.get('mesg'), 'http:api:request:headers may not be modified by the runtime.')
|
|
1031
|
+
|
|
1032
|
+
async def test_libcortex_httpapi_vars(self):
|
|
1033
|
+
|
|
1034
|
+
async with self.getTestCore() as core:
|
|
1035
|
+
udef = await core.addUser('lowuser')
|
|
1036
|
+
lowuser = udef.get('iden')
|
|
1037
|
+
await core.setUserPasswd(core.auth.rootuser.iden, 'root')
|
|
1038
|
+
await core.setUserPasswd(lowuser, 'secret')
|
|
1039
|
+
addr, hport = await core.addHttpsPort(0)
|
|
1040
|
+
|
|
1041
|
+
q = '''$api = $lib.cortex.httpapi.add(testpath)
|
|
1042
|
+
$api.methods.get = ${ $data = ({"hehe": $hehe }) $request.reply(200, body=$data) }
|
|
1043
|
+
$api.methods.post = ${ $data = ({"sup": $sup }) $request.reply(200, body=$data) }
|
|
1044
|
+
$api.vars.hehe = haha
|
|
1045
|
+
$api.vars.sup = dawg
|
|
1046
|
+
return ( ($api.iden) )
|
|
1047
|
+
'''
|
|
1048
|
+
iden = await core.callStorm(q)
|
|
1049
|
+
|
|
1050
|
+
async with self.getHttpSess(auth=('root', 'root'), port=hport) as sess:
|
|
1051
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/testpath',)
|
|
1052
|
+
data = await resp.json()
|
|
1053
|
+
self.eq(data.get('hehe'), 'haha')
|
|
1054
|
+
|
|
1055
|
+
resp = await sess.post(f'https://localhost:{hport}/api/ext/testpath', )
|
|
1056
|
+
data = await resp.json()
|
|
1057
|
+
self.eq(data.get('sup'), 'dawg')
|
|
1058
|
+
|
|
1059
|
+
q = '''$api=$lib.cortex.httpapi.get($http_iden)
|
|
1060
|
+
$api.vars = ({ "hehe": "yup", "sup": "word"})
|
|
1061
|
+
'''
|
|
1062
|
+
msgs = await core.stormlist(q, opts={'vars': {'http_iden': iden}})
|
|
1063
|
+
self.stormHasNoWarnErr(msgs)
|
|
1064
|
+
|
|
1065
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/testpath',)
|
|
1066
|
+
data = await resp.json()
|
|
1067
|
+
self.eq(data.get('hehe'), 'yup')
|
|
1068
|
+
|
|
1069
|
+
resp = await sess.post(f'https://localhost:{hport}/api/ext/testpath', )
|
|
1070
|
+
data = await resp.json()
|
|
1071
|
+
self.eq(data.get('sup'), 'word')
|
|
1072
|
+
|
|
1073
|
+
# Cause a NoSuchVar error due to a missing var in the handler
|
|
1074
|
+
q = '$api=$lib.cortex.httpapi.get($iden) $api.vars.sup = $lib.undef'
|
|
1075
|
+
msgs = await core.stormlist(q, opts={'vars': {'iden': iden}})
|
|
1076
|
+
self.stormHasNoWarnErr(msgs)
|
|
1077
|
+
resp = await sess.post(f'https://localhost:{hport}/api/ext/testpath', )
|
|
1078
|
+
self.eq(resp.status, 500)
|
|
1079
|
+
data = await resp.json()
|
|
1080
|
+
self.eq(data.get('code'), 'NoSuchVar')
|
|
1081
|
+
self.eq(data.get('mesg'), 'Missing variable: sup')
|
|
1082
|
+
|
|
1083
|
+
q = '''$api = $lib.cortex.httpapi.get($iden)
|
|
1084
|
+
|
|
1085
|
+
$api = $lib.cortex.httpapi.get($iden)
|
|
1086
|
+
|
|
1087
|
+
$vars = $api.vars // _ctor to make a thin object
|
|
1088
|
+
$vars.hehe = haha // set and persist hehe=haha
|
|
1089
|
+
|
|
1090
|
+
$lib.print('pre _stor')
|
|
1091
|
+
for ($k, $v) in $vars {
|
|
1092
|
+
$lib.print(`{$k} -> {$v}`)
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// Use a _stor to set new vars in place
|
|
1096
|
+
$api.vars = ({"hehe": "i am silly", "why": "why not"})
|
|
1097
|
+
|
|
1098
|
+
$lib.print('post _stor')
|
|
1099
|
+
for ($k, $v) in $vars {
|
|
1100
|
+
$lib.print(`{$k} -> {$v}`)
|
|
1101
|
+
}
|
|
1102
|
+
'''
|
|
1103
|
+
msgs = await core.stormlist(q, opts={'vars': {'iden': iden}})
|
|
1104
|
+
self.stormHasNoWarnErr(msgs)
|
|
1105
|
+
self.stormIsInPrint('hehe -> haha', msgs)
|
|
1106
|
+
self.stormIsInPrint('hehe -> i am silly', msgs)
|
|
1107
|
+
self.stormIsInPrint('why -> why not', msgs)
|
|
1108
|
+
|
|
1109
|
+
async def test_libcortex_httpapi_readonly(self):
|
|
1110
|
+
async with self.getTestCore() as core:
|
|
1111
|
+
udef = await core.addUser('lowuser')
|
|
1112
|
+
lowuser = udef.get('iden')
|
|
1113
|
+
await core.setUserPasswd(core.auth.rootuser.iden, 'root')
|
|
1114
|
+
await core.setUserPasswd(lowuser, 'secret')
|
|
1115
|
+
addr, hport = await core.addHttpsPort(0)
|
|
1116
|
+
|
|
1117
|
+
# nothing
|
|
1118
|
+
q = '''$api = $lib.cortex.httpapi.add(testpath)
|
|
1119
|
+
$api.methods.get = ${ [inet:asn=$request.params.asn ] $request.reply(200, body=$node.value())}
|
|
1120
|
+
return ( ($api.iden) )'''
|
|
1121
|
+
iden = await core.callStorm(q)
|
|
1122
|
+
|
|
1123
|
+
async with self.getHttpSess(auth=('root', 'root'), port=hport) as sess:
|
|
1124
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/testpath?asn=0')
|
|
1125
|
+
self.eq(resp.status, 200)
|
|
1126
|
+
data = await resp.json()
|
|
1127
|
+
self.eq(data, 0)
|
|
1128
|
+
|
|
1129
|
+
msgs = await core.stormlist('$api=$lib.cortex.httpapi.get($iden) $api.readonly = $lib.true',
|
|
1130
|
+
opts={'vars': {'iden': iden}})
|
|
1131
|
+
self.stormHasNoWarnErr(msgs)
|
|
1132
|
+
|
|
1133
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/testpath?asn=1')
|
|
1134
|
+
self.eq(resp.status, 500)
|
|
1135
|
+
data = await resp.json()
|
|
1136
|
+
self.eq(data.get('code'), 'IsReadOnly')
|
|
1137
|
+
|
|
1138
|
+
q = '''$api=$lib.cortex.httpapi.get($iden)
|
|
1139
|
+
$api.methods.get = ${ inet:asn=$request.params.asn $request.reply(200, body=$node.value()) }'''
|
|
1140
|
+
msgs = await core.stormlist(q, opts={'vars': {'iden': iden}})
|
|
1141
|
+
self.stormHasNoWarnErr(msgs)
|
|
1142
|
+
|
|
1143
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/testpath?asn=0')
|
|
1144
|
+
self.eq(resp.status, 200)
|
|
1145
|
+
data = await resp.json()
|
|
1146
|
+
self.eq(data, 0)
|
|
1147
|
+
|
|
1148
|
+
async def test_libcortex_httpapi_fsm_sadpath(self):
|
|
1149
|
+
|
|
1150
|
+
# Test to exercise sad paths of the state machine for the ExtHttpApi handler
|
|
1151
|
+
async with self.getTestCore() as core:
|
|
1152
|
+
udef = await core.addUser('lowuser')
|
|
1153
|
+
lowuser = udef.get('iden')
|
|
1154
|
+
await core.setUserPasswd(core.auth.rootuser.iden, 'root')
|
|
1155
|
+
await core.setUserPasswd(lowuser, 'secret')
|
|
1156
|
+
addr, hport = await core.addHttpsPort(0)
|
|
1157
|
+
|
|
1158
|
+
# nothing
|
|
1159
|
+
q = '''$api = $lib.cortex.httpapi.add(bad00)
|
|
1160
|
+
$api.methods.get = ${ }
|
|
1161
|
+
return ( ($api.iden) )'''
|
|
1162
|
+
iden00 = await core.callStorm(q)
|
|
1163
|
+
|
|
1164
|
+
# no status code
|
|
1165
|
+
q = '''$api = $lib.cortex.httpapi.add(bad01)
|
|
1166
|
+
$api.methods.get = ${ $request.sendheaders( ({"oh":"my"}) ) }
|
|
1167
|
+
return ( ($api.iden) )'''
|
|
1168
|
+
iden01 = await core.callStorm(q)
|
|
1169
|
+
|
|
1170
|
+
# no status code and a body being sent
|
|
1171
|
+
q = '''$api = $lib.cortex.httpapi.add(bad02)
|
|
1172
|
+
$api.methods.get = ${ $request.sendheaders(({"oh":"my"})) $data='text' $request.sendbody($data.encode()) }
|
|
1173
|
+
return ( ($api.iden) )'''
|
|
1174
|
+
iden02 = await core.callStorm(q)
|
|
1175
|
+
|
|
1176
|
+
# code after body has been sent
|
|
1177
|
+
q = '''$api = $lib.cortex.httpapi.add(bad03)
|
|
1178
|
+
$api.methods.get = ${ $data='text' $request.reply(201, body=$data.encode()) $request.sendcode(403) }
|
|
1179
|
+
return ( ($api.iden) )'''
|
|
1180
|
+
iden03 = await core.callStorm(q)
|
|
1181
|
+
|
|
1182
|
+
# headers after body has been sent
|
|
1183
|
+
q = '''$api = $lib.cortex.httpapi.add(bad04)
|
|
1184
|
+
$api.methods.get = ${$d='text' $request.reply(200, body=$d.encode()) $request.sendheaders(({"oh": "hi"}))}
|
|
1185
|
+
return ( ($api.iden) )'''
|
|
1186
|
+
iden04 = await core.callStorm(q)
|
|
1187
|
+
|
|
1188
|
+
# storm error
|
|
1189
|
+
q = '''$api = $lib.cortex.httpapi.add(bad05)
|
|
1190
|
+
$api.methods.get = ${ [test:int = notAnInt ] }
|
|
1191
|
+
return ( ($api.iden) )'''
|
|
1192
|
+
iden05 = await core.callStorm(q)
|
|
1193
|
+
|
|
1194
|
+
# storm error AFTER body has been sent
|
|
1195
|
+
q = '''$api = $lib.cortex.httpapi.add(bad06)
|
|
1196
|
+
$api.methods.get = ${ $request.reply(201, body=({})) [test:int = notAnInt ] }
|
|
1197
|
+
return ( ($api.iden) )'''
|
|
1198
|
+
iden06 = await core.callStorm(q)
|
|
1199
|
+
|
|
1200
|
+
async with self.getHttpSess(auth=('root', 'root'), port=hport) as sess:
|
|
1201
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/bad00')
|
|
1202
|
+
self.eq(resp.status, 500)
|
|
1203
|
+
data = await resp.json()
|
|
1204
|
+
self.eq(data.get('code'), 'StormRuntimeError')
|
|
1205
|
+
self.eq(data.get('mesg'), f'Extended HTTP API {iden00} never set status code.')
|
|
1206
|
+
|
|
1207
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/bad01')
|
|
1208
|
+
self.eq(resp.status, 500)
|
|
1209
|
+
data = await resp.json()
|
|
1210
|
+
self.notin('oh', resp.headers)
|
|
1211
|
+
self.eq(data.get('code'), 'StormRuntimeError')
|
|
1212
|
+
self.eq(data.get('mesg'), f'Extended HTTP API {iden01} never set status code.')
|
|
1213
|
+
|
|
1214
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/bad02')
|
|
1215
|
+
self.eq(resp.status, 500)
|
|
1216
|
+
data = await resp.json()
|
|
1217
|
+
self.notin('oh', resp.headers)
|
|
1218
|
+
self.eq(data.get('code'), 'StormRuntimeError')
|
|
1219
|
+
self.eq(data.get('mesg'), f'Extended HTTP API {iden02} must set status code before sending body.')
|
|
1220
|
+
|
|
1221
|
+
with self.getAsyncLoggerStream('synapse.lib.httpapi',
|
|
1222
|
+
f'Extended HTTP API {iden03} tried to set code after sending body.') as stream:
|
|
1223
|
+
|
|
1224
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/bad03')
|
|
1225
|
+
self.true(await stream.wait(timeout=6))
|
|
1226
|
+
self.eq(resp.status, 201)
|
|
1227
|
+
self.eq(await resp.read(), b'text')
|
|
1228
|
+
|
|
1229
|
+
with self.getAsyncLoggerStream('synapse.lib.httpapi',
|
|
1230
|
+
f'Extended HTTP API {iden04} tried to set headers after sending body.') as stream:
|
|
1231
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/bad04')
|
|
1232
|
+
self.true(await stream.wait(timeout=6))
|
|
1233
|
+
self.eq(resp.status, 200)
|
|
1234
|
+
self.eq(await resp.read(), b'text')
|
|
1235
|
+
|
|
1236
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/bad05')
|
|
1237
|
+
self.eq(resp.status, 500)
|
|
1238
|
+
data = await resp.json()
|
|
1239
|
+
self.eq(data.get('code'), 'BadTypeValu')
|
|
1240
|
+
self.eq(data.get('mesg'), "invalid literal for int() with base 0: 'notAnInt'")
|
|
1241
|
+
|
|
1242
|
+
with self.getAsyncLoggerStream('synapse.lib.httpapi',
|
|
1243
|
+
f'Error executing Extended HTTP API {iden06}: BadTypeValu') as stream:
|
|
1244
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/bad06')
|
|
1245
|
+
self.true(await stream.wait(timeout=6))
|
|
1246
|
+
self.eq(resp.status, 201)
|
|
1247
|
+
self.eq(await resp.json(), {})
|
|
1248
|
+
|
|
1249
|
+
async def test_cortex_httpapi_dynamic(self):
|
|
1250
|
+
|
|
1251
|
+
# API endpoints can be dynamic. Use at your own risk.
|
|
1252
|
+
|
|
1253
|
+
async with self.getTestCore() as core:
|
|
1254
|
+
udef = await core.addUser('lowuser')
|
|
1255
|
+
lowuser = udef.get('iden')
|
|
1256
|
+
await core.setUserPasswd(core.auth.rootuser.iden, 'root')
|
|
1257
|
+
await core.setUserPasswd(lowuser, 'secret')
|
|
1258
|
+
addr, hport = await core.addHttpsPort(0)
|
|
1259
|
+
|
|
1260
|
+
q = '''$api = $lib.cortex.httpapi.add(dyn00)
|
|
1261
|
+
$api.methods.get = ${
|
|
1262
|
+
$api = $request.api
|
|
1263
|
+
if $n {
|
|
1264
|
+
$part = `redir0{$n}`
|
|
1265
|
+
$redir = `/api/ext/{$part}`
|
|
1266
|
+
$headers = ({"Location": $redir})
|
|
1267
|
+
$api.vars.n = ($n - (1) )
|
|
1268
|
+
$api.path = $part
|
|
1269
|
+
$request.reply(301, headers=$headers)
|
|
1270
|
+
} else {
|
|
1271
|
+
$api.vars.n = (3)
|
|
1272
|
+
$api.vars.melt = $lib.true
|
|
1273
|
+
$api.path = dyn00
|
|
1274
|
+
$request.reply(200, body=({"end": "youMadeIt", "melt": $melt}) )
|
|
1275
|
+
if $melt {
|
|
1276
|
+
$lib.cortex.httpapi.del($api.iden)
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
$api.vars.n = (3)
|
|
1281
|
+
$api.vars.melt = $lib.false
|
|
1282
|
+
return ( ($api.iden) )'''
|
|
1283
|
+
iden00 = await core.callStorm(q)
|
|
1284
|
+
|
|
1285
|
+
async with self.getHttpSess(auth=('root', 'root'), port=hport) as sess:
|
|
1286
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/dyn00') # type: aiohttp.ClientResponse
|
|
1287
|
+
self.eq(resp.status, 200)
|
|
1288
|
+
self.eq(resp.url.path, '/api/ext/redir01')
|
|
1289
|
+
self.len(3, resp.history)
|
|
1290
|
+
data = await resp.json()
|
|
1291
|
+
self.eq(data, {'end': 'youMadeIt', 'melt': False})
|
|
1292
|
+
|
|
1293
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/dyn00') # type: aiohttp.ClientResponse
|
|
1294
|
+
self.eq(resp.status, 200)
|
|
1295
|
+
self.len(3, resp.history)
|
|
1296
|
+
data = await resp.json()
|
|
1297
|
+
self.eq(data, {'end': 'youMadeIt', 'melt': True})
|
|
1298
|
+
|
|
1299
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/dyn00') # type: aiohttp.ClientResponse
|
|
1300
|
+
self.eq(resp.status, 404)
|
|
1301
|
+
data = await resp.json()
|
|
1302
|
+
self.eq(data.get('code'), 'NoSuchPath')
|
|
1303
|
+
|
|
1304
|
+
async def test_cortex_httpapi_cell_headers(self):
|
|
1305
|
+
async with self.getTestCore(conf={'https:headers': {'Key1': 'Valu1'}}) as core:
|
|
1306
|
+
udef = await core.addUser('lowuser')
|
|
1307
|
+
lowuser = udef.get('iden')
|
|
1308
|
+
await core.setUserPasswd(core.auth.rootuser.iden, 'root')
|
|
1309
|
+
await core.setUserPasswd(lowuser, 'secret')
|
|
1310
|
+
addr, hport = await core.addHttpsPort(0)
|
|
1311
|
+
|
|
1312
|
+
q = '''$api = $lib.cortex.httpapi.add(stuff)
|
|
1313
|
+
$api.methods.get = ${
|
|
1314
|
+
$request.reply(200, headers=({"Weee": "valu"}) )
|
|
1315
|
+
}
|
|
1316
|
+
return ( ($api.iden) )'''
|
|
1317
|
+
iden00 = await core.callStorm(q)
|
|
1318
|
+
|
|
1319
|
+
async with self.getHttpSess(auth=('root', 'root'), port=hport) as sess:
|
|
1320
|
+
resp = await sess.get(f'https://localhost:{hport}/api/ext/stuff') # type: aiohttp.ClientResponse
|
|
1321
|
+
self.eq(resp.status, 200)
|
|
1322
|
+
self.eq(resp.headers.get('Weee'), 'valu')
|
|
1323
|
+
self.eq(resp.headers.get('Key1'), 'Valu1')
|
|
1324
|
+
# general default synapse headers are not present
|
|
1325
|
+
self.none(resp.headers.get('X-Content-Type-Options'))
|
|
1326
|
+
# Server is still omitted though
|
|
1327
|
+
self.none(resp.headers.get('Server'))
|