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.

Files changed (87) hide show
  1. synapse/axon.py +19 -16
  2. synapse/cortex.py +203 -15
  3. synapse/exc.py +0 -2
  4. synapse/lib/ast.py +42 -23
  5. synapse/lib/autodoc.py +2 -2
  6. synapse/lib/cache.py +16 -1
  7. synapse/lib/cell.py +5 -5
  8. synapse/lib/httpapi.py +198 -2
  9. synapse/lib/layer.py +5 -2
  10. synapse/lib/modelrev.py +36 -3
  11. synapse/lib/node.py +2 -5
  12. synapse/lib/parser.py +1 -1
  13. synapse/lib/schemas.py +51 -0
  14. synapse/lib/snap.py +10 -0
  15. synapse/lib/storm.lark +24 -4
  16. synapse/lib/storm.py +98 -19
  17. synapse/lib/storm_format.py +1 -1
  18. synapse/lib/stormhttp.py +11 -4
  19. synapse/lib/stormlib/auth.py +16 -2
  20. synapse/lib/stormlib/backup.py +1 -0
  21. synapse/lib/stormlib/basex.py +2 -0
  22. synapse/lib/stormlib/cell.py +7 -0
  23. synapse/lib/stormlib/compression.py +3 -0
  24. synapse/lib/stormlib/cortex.py +1168 -0
  25. synapse/lib/stormlib/ethereum.py +1 -0
  26. synapse/lib/stormlib/graph.py +2 -0
  27. synapse/lib/stormlib/hashes.py +5 -0
  28. synapse/lib/stormlib/hex.py +6 -0
  29. synapse/lib/stormlib/infosec.py +6 -1
  30. synapse/lib/stormlib/ipv6.py +1 -0
  31. synapse/lib/stormlib/iters.py +58 -1
  32. synapse/lib/stormlib/json.py +5 -0
  33. synapse/lib/stormlib/mime.py +1 -0
  34. synapse/lib/stormlib/model.py +19 -3
  35. synapse/lib/stormlib/modelext.py +1 -0
  36. synapse/lib/stormlib/notifications.py +2 -0
  37. synapse/lib/stormlib/pack.py +2 -0
  38. synapse/lib/stormlib/random.py +1 -0
  39. synapse/lib/stormlib/smtp.py +0 -7
  40. synapse/lib/stormlib/stats.py +223 -0
  41. synapse/lib/stormlib/stix.py +8 -0
  42. synapse/lib/stormlib/storm.py +1 -0
  43. synapse/lib/stormlib/version.py +3 -0
  44. synapse/lib/stormlib/xml.py +3 -0
  45. synapse/lib/stormlib/yaml.py +2 -0
  46. synapse/lib/stormtypes.py +250 -170
  47. synapse/lib/trigger.py +180 -4
  48. synapse/lib/types.py +1 -1
  49. synapse/lib/version.py +2 -2
  50. synapse/lib/view.py +55 -6
  51. synapse/models/inet.py +21 -6
  52. synapse/models/orgs.py +48 -2
  53. synapse/models/risk.py +126 -2
  54. synapse/models/syn.py +6 -0
  55. synapse/tests/files/stormpkg/badapidef.yaml +13 -0
  56. synapse/tests/files/stormpkg/storm/modules/apimod +10 -0
  57. synapse/tests/files/stormpkg/testpkg.yaml +23 -0
  58. synapse/tests/test_axon.py +7 -2
  59. synapse/tests/test_cortex.py +231 -35
  60. synapse/tests/test_lib_ast.py +138 -43
  61. synapse/tests/test_lib_autodoc.py +1 -1
  62. synapse/tests/test_lib_modelrev.py +9 -0
  63. synapse/tests/test_lib_node.py +55 -0
  64. synapse/tests/test_lib_storm.py +14 -1
  65. synapse/tests/test_lib_stormhttp.py +65 -6
  66. synapse/tests/test_lib_stormlib_auth.py +12 -3
  67. synapse/tests/test_lib_stormlib_cortex.py +1327 -0
  68. synapse/tests/test_lib_stormlib_iters.py +116 -0
  69. synapse/tests/test_lib_stormlib_stats.py +187 -0
  70. synapse/tests/test_lib_stormlib_storm.py +8 -0
  71. synapse/tests/test_lib_stormsvc.py +24 -1
  72. synapse/tests/test_lib_stormtypes.py +124 -69
  73. synapse/tests/test_lib_trigger.py +315 -0
  74. synapse/tests/test_lib_view.py +1 -2
  75. synapse/tests/test_model_base.py +26 -0
  76. synapse/tests/test_model_inet.py +22 -0
  77. synapse/tests/test_model_orgs.py +28 -0
  78. synapse/tests/test_model_risk.py +73 -0
  79. synapse/tests/test_tools_autodoc.py +25 -0
  80. synapse/tests/test_tools_genpkg.py +9 -3
  81. synapse/tests/utils.py +39 -0
  82. synapse/tools/autodoc.py +42 -2
  83. {synapse-2.152.0.dist-info → synapse-2.154.0.dist-info}/METADATA +2 -2
  84. {synapse-2.152.0.dist-info → synapse-2.154.0.dist-info}/RECORD +87 -79
  85. {synapse-2.152.0.dist-info → synapse-2.154.0.dist-info}/WHEEL +1 -1
  86. {synapse-2.152.0.dist-info → synapse-2.154.0.dist-info}/LICENSE +0 -0
  87. {synapse-2.152.0.dist-info → synapse-2.154.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1168 @@
1
+ import copy
2
+ import json
3
+ import logging
4
+
5
+ import synapse.exc as s_exc
6
+
7
+ import synapse.lib.stormtypes as s_stormtypes
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ stormcmds = [
13
+ {
14
+ 'name': 'cortex.httpapi.list',
15
+ 'descr': 'List Extended HTTP API endpoints',
16
+ 'cmdargs': (),
17
+ 'storm': '''
18
+ $apis = $lib.cortex.httpapi.list()
19
+ if $apis {
20
+ $header = 'order iden owner auth runas path'
21
+ $lib.print($header)
22
+ for ($n, $api) in $lib.iters.enum($apis) {
23
+ try {
24
+ $user = $api.owner.name
25
+ } catch NoSuchUser as err {
26
+ $user = `No user found ({$err.info.user})`
27
+ }
28
+ $auth = `{$api.authenticated}`
29
+ $order = `{$n}`
30
+ $mesg=`{$order.ljust(5)} {$api.iden} {$user.ljust(20)} {$auth.ljust(5)} {$api.runas.ljust(6)} {$api.path}`
31
+ $lib.print($mesg)
32
+ }
33
+ } else {
34
+ $lib.print('No Extended HTTP API endpoints are registered.')
35
+ }
36
+ '''
37
+ },
38
+ {
39
+ 'name': 'cortex.httpapi.stat',
40
+ 'descr': 'Get details for an Extended HTTP API endpoint.',
41
+ 'cmdargs': (
42
+ ('iden', {'help': 'The iden of the endpoint to inspect. This will also match iden prefixes or name prefixes.',
43
+ 'type': 'str'}),
44
+ ),
45
+ 'storm': '''
46
+ $iden = $lib.null
47
+ for $api in $lib.cortex.httpapi.list() {
48
+ if $api.iden.startswith($cmdopts.iden) {
49
+ if $iden {
50
+ $lib.raise(StormRuntimeError, 'Already matched one Extended HTTP API!')
51
+ }
52
+ $iden = $api.iden
53
+ }
54
+ if $api.name.startswith($cmdopts.iden) {
55
+ if $iden {
56
+ $lib.raise(StormRuntimeError, 'Already matched one Extended HTTP API!')
57
+ }
58
+ $iden = $api.iden
59
+ }
60
+ }
61
+ if (not $iden) {
62
+ $lib.raise(StormRuntimeError, 'Failed to match Extended HTTP API by iden or name!')
63
+ }
64
+ $time = $lib.model.type(time)
65
+ $api = $lib.cortex.httpapi.get($iden)
66
+ $lib.print(`Iden: {$api.iden}`)
67
+ try {
68
+ $lib.print(`Creator: {$api.creator.name} ({$api.creator.iden})`)
69
+ } catch NoSuchUser as err {
70
+ $lib.print(`!Creator: No user found ({$err.info.user})`)
71
+ }
72
+ $lib.print(`Created: {$time.repr($api.created)}`)
73
+ $lib.print(`Updated: {$time.repr($api.updated)}`)
74
+ $lib.print(`Path: {$api.path}`)
75
+ try {
76
+ $lib.print(`Owner: {$api.owner.name} ({$api.owner.iden})`)
77
+ } catch NoSuchUser as err {
78
+ $lib.print(`!Owner: No user found ({$err.info.user})`)
79
+ }
80
+ $lib.print(`Runas: {$api.runas}`)
81
+ try {
82
+ $lib.print(`View: {$api.view.get(name)} ({$api.view.iden})`)
83
+ } catch NoSuchView as err {
84
+ $lib.print(`!View: No view found ({$err.info.iden})`)
85
+ }
86
+ $lib.print(`Readonly: {$api.readonly}`)
87
+ $lib.print(`Authenticated: {$api.authenticated}`)
88
+ $lib.print(`Name: {$api.name}`)
89
+ $lib.print(`Description: {$api.desc}`)
90
+ $lib.print('')
91
+ $perms = $api.perms
92
+ if $perms {
93
+ $lib.print('The following user permissions are required to run this HTTP API endpoint:')
94
+ for $pdef in $perms {
95
+ $perm = $lib.str.join(".", $pdef.perm)
96
+ $lib.print($perm)
97
+ $lib.print(` default: {$pdef.default}`)
98
+ }
99
+ $lib.print('')
100
+ } else {
101
+ $lib.print('No user permissions are required to run this HTTP API endpoint.')
102
+ }
103
+ $methods = $api.methods
104
+ if $methods {
105
+ $lib.print('The handler defines the following HTTP methods:')
106
+ for ($meth, $storm) in $methods {
107
+ $lib.print(`Method: {$meth.upper()}`)
108
+ $lib.print($storm)
109
+ $lib.print('')
110
+ }
111
+ } else {
112
+ $lib.print('No HTTP Methods are set for the handler.')
113
+ }
114
+ $vars = $api.vars
115
+ if $vars {
116
+ $lib.print('The handler has the following runtime variables set:')
117
+ for ($key, $valu) in $vars {
118
+ $lib.print(`{$key.ljust(16)} => {$valu}`)
119
+ }
120
+ $lib.print('')
121
+ } else {
122
+ $lib.print('No vars are set for the handler.')
123
+ }
124
+ '''
125
+ },
126
+ {
127
+ 'name': 'cortex.httpapi.index',
128
+ 'descr': '''Set the index of an Extended HTTP API endpoint.
129
+
130
+ Examples:
131
+
132
+ // Move an endpoint to the first index.
133
+ cortex.httpapi.index 60e5ba38e90958fd8e2ddd9e4730f16b 0
134
+
135
+ // Move an endpoint to the third index.
136
+ cortex.httpapi.index dd9e4730f16b60e5ba58fd8e2d38e909 2
137
+ ''',
138
+ 'cmdargs': (
139
+ ('iden', {'help': 'The iden of the endpoint to move. This will also match iden prefixes or name prefixes.',
140
+ 'type': 'str'}),
141
+ ('index', {'help': 'Specify the endpoint location as a 0 based index.', 'type': 'int'}),
142
+ ),
143
+ 'storm': '''
144
+ $iden = $lib.null
145
+ for $api in $lib.cortex.httpapi.list() {
146
+ if $api.iden.startswith($cmdopts.iden) {
147
+ if $iden {
148
+ $lib.raise(StormRuntimeError, 'Already matched one Extended HTTP API!')
149
+ }
150
+ $iden = $api.iden
151
+ }
152
+ if $api.name.startswith($cmdopts.iden) {
153
+ if $iden {
154
+ $lib.raise(StormRuntimeError, 'Already matched one Extended HTTP API!')
155
+ }
156
+ $iden = $api.iden
157
+ }
158
+ }
159
+ if (not $iden) {
160
+ $lib.raise(StormRuntimeError, 'Failed to match Extended HTTP API by iden or name!')
161
+ }
162
+ $api = $lib.cortex.httpapi.get($iden)
163
+ $index = $lib.cortex.httpapi.index($api.iden, $cmdopts.index)
164
+ $lib.print(`Set HTTP API {$api.iden} to index {$index}`)
165
+ '''
166
+ }
167
+ ]
168
+
169
+ def _normPermString(perm):
170
+ if perm.startswith('!'):
171
+ raise s_exc.BadArg(mesg=f'Permission assignment must not start with a !, got {perm}')
172
+ parts = perm.split('.')
173
+ pdef = {'perm': parts, 'default': False}
174
+ return pdef
175
+
176
+ @s_stormtypes.registry.registerType
177
+ class HttpApi(s_stormtypes.StormType):
178
+ '''
179
+ Extended HTTP API object.
180
+
181
+ This object represents an extended HTTP API that has been configured on the Cortex.
182
+ '''
183
+ _storm_typename = 'http:api'
184
+ _storm_locals = (
185
+ {'name': 'iden', 'desc': 'The iden of the Extended HTTP API.', 'type': 'str'},
186
+ {'name': 'created', 'desc': 'The time the Extended HTTP API was created.', 'type': 'int'},
187
+ {'name': 'updated', 'desc': 'The time the Extended HTTP API was last modified.',
188
+ 'type': {'type': 'gtor', '_gtorfunc': '_gtorUpdated', 'returns': {'type': 'int'}}},
189
+ {'name': 'creator', 'desc': 'The user that created the Extended HTTP API.',
190
+ 'type': {'type': 'gtor', '_gtorfunc': '_gtorCreator', 'returns': {'type': 'auth:user'}}},
191
+ {'name': 'owner', 'desc': 'The user that runs the endpoint query logic when runas="owner".',
192
+ 'type': {'type': ['gtor', 'stor'], '_gtorfunc': '_gtorOwner', '_storfunc': '_storOwner',
193
+ 'returns': {'type': 'auth:user'}}},
194
+ {'name': 'pack', 'desc': 'Get a packed copy of the HTTP API object.',
195
+ 'type': {'type': 'function', '_funcname': '_methPack', 'args': (),
196
+ 'returns': {'type': 'dict'}}},
197
+ {'name': 'name', 'desc': 'The name of the API instance.',
198
+ 'type': {'type': ['stor', 'gtor'], '_storfunc': '_storName', '_gtorfunc': '_gtorName',
199
+ 'returns': {'type': 'str'}}},
200
+ {'name': 'desc', 'desc': 'The description of the API instance.',
201
+ 'type': {'type': ['stor', 'gtor'], '_storfunc': '_storDesc', '_gtorfunc': '_gtorDesc',
202
+ 'returns': {'type': 'str'}}},
203
+ {'name': 'path', 'desc': '''
204
+ The path of the API instance.
205
+
206
+ This path may contain regular expression capture groups, which are used to populate
207
+ request arguments.
208
+
209
+ Note:
210
+ The Cortex does not inspect paths in order to identify duplicates or overlapping paths.
211
+ It is the responsibility of the Cortex administrator to configure their Extended HTTP API
212
+ paths so that they are correct for their use cases.
213
+
214
+ Example:
215
+ Update an API path to contain a single wildcard argument::
216
+
217
+ $api.path = 'foo/bar/(.*)/baz'
218
+
219
+ Update an API path to contain a two wildcard arguments with restricted character sets::
220
+
221
+ $api.path = 'hehe/([a-z]*)/([0-9]{1-4})'
222
+ ''',
223
+ 'type': {'type': ['stor', 'gtor'], '_storfunc': '_storPath', '_gtorfunc': '_gtorPath',
224
+ 'returns': {'type': 'str'}}},
225
+ {'name': 'vars', 'desc': 'The Storm runtime variables specific for the API instance.',
226
+ 'type': {'type': ['stor', 'ctor'], '_storfunc': '_storVars', '_ctorfunc': '_ctorVars',
227
+ 'returns': {'type': 'http:api:vars'}}},
228
+ {'name': 'view', 'desc': 'The View of the API instance. This is the view that Storm methods are executed in.',
229
+ 'type': {'type': ['stor', 'gtor'], '_storfunc': '_storView', '_gtorfunc': '_gtorView',
230
+ 'returns': {'type': 'view'}}},
231
+ {'name': 'runas', 'desc': 'String indicating whether the requests run as the owner or the authenticated user.',
232
+ 'type': {'type': ['stor', 'gtor'], '_storfunc': '_storRunas', '_gtorfunc': '_gtorRunas',
233
+ 'returns': {'type': 'str'}}},
234
+ {'name': 'perms', 'desc': 'The permissions an authenticated user must have in order to access the HTTP API.',
235
+ 'type': {'type': ['stor', 'ctor'], '_storfunc': '_storPerms', '_ctorfunc': '_ctorPerms',
236
+ 'returns': {'type': 'http:api:perms'}}},
237
+ {'name': 'readonly', 'desc': 'Boolean value indicating if the Storm methods are executed in a readonly Storm runtime.',
238
+ 'type': {'type': ['stor', 'gtor'], '_storfunc': '_storReadonly', '_gtorfunc': '_gtorReadonly',
239
+ 'returns': {'type': 'boolean'}}},
240
+ {'name': 'authenticated', 'desc': 'Boolean value indicating if the Extended HTTP API requires an authenticated user or session.',
241
+ 'type': {'type': ['stor', 'gtor'], '_storfunc': '_storAuthenticated', '_gtorfunc': '_gtorAuthenticated',
242
+ 'returns': {'type': 'boolean'}}},
243
+ )
244
+
245
+ def __init__(self, runt, info):
246
+ s_stormtypes.StormType.__init__(self)
247
+ self.runt = runt
248
+ self.info = info
249
+ self.iden = self.info.get('iden')
250
+ # Perms comes in as a tuple - convert it to a list to we can have a mutable object
251
+ self.info['perms'] = list(self.info.get('perms'))
252
+
253
+ self.stors.update({
254
+ # General helpers
255
+ 'name': self._storName,
256
+ 'desc': self._storDesc,
257
+ 'path': self._storPath,
258
+ 'vars': self._storVars,
259
+ 'view': self._storView,
260
+ 'runas': self._storRunas,
261
+ 'owner': self._storOwner,
262
+ 'perms': self._storPerms,
263
+ 'readonly': self._storReadonly,
264
+ 'authenticated': self._storAuthenticated,
265
+ })
266
+
267
+ self.gtors.update({
268
+ 'name': self._gtorName,
269
+ 'desc': self._gtorDesc,
270
+ 'path': self._gtorPath,
271
+ 'view': self._gtorView,
272
+ 'runas': self._gtorRunas,
273
+ 'owner': self._gtorOwner,
274
+ 'creator': self._gtorCreator,
275
+ 'updated': self._gtorUpdated,
276
+ 'readonly': self._gtorReadonly,
277
+ 'authenticated': self._gtorAuthenticated,
278
+ })
279
+
280
+ self.ctors.update({
281
+ 'vars': self._ctorVars,
282
+ 'perms': self._ctorPerms,
283
+ 'methods': self._ctorMethods
284
+ })
285
+
286
+ # constants
287
+ self.locls.update(self.getObjLocals())
288
+ self.locls.update({
289
+ 'iden': self.iden,
290
+ 'created': self.info.get('created'),
291
+ })
292
+
293
+ def getObjLocals(self):
294
+ return {
295
+ 'pack': self._methPack,
296
+ }
297
+
298
+ @s_stormtypes.stormfunc(readonly=True)
299
+ async def _methPack(self):
300
+ return copy.deepcopy(self.info)
301
+
302
+ @s_stormtypes.stormfunc(readonly=True)
303
+ def _ctorMethods(self, path=None):
304
+ return HttpApiMethods(self)
305
+
306
+ async def _storPath(self, path):
307
+ s_stormtypes.confirm(('storm', 'lib', 'cortex', 'httpapi', 'set'))
308
+ path = await s_stormtypes.tostr(path)
309
+ adef = await self.runt.snap.core.modHttpExtApi(self.iden, 'path', path)
310
+ self.info['path'] = path
311
+ self.info['updated'] = adef.get('updated')
312
+
313
+ @s_stormtypes.stormfunc(readonly=True)
314
+ async def _gtorPath(self):
315
+ return self.info.get('path')
316
+
317
+ async def _storName(self, name):
318
+ s_stormtypes.confirm(('storm', 'lib', 'cortex', 'httpapi', 'set'))
319
+ name = await s_stormtypes.tostr(name)
320
+ adef = await self.runt.snap.core.modHttpExtApi(self.iden, 'name', name)
321
+ self.info['name'] = name
322
+ self.info['updated'] = adef.get('updated')
323
+
324
+ @s_stormtypes.stormfunc(readonly=True)
325
+ async def _gtorView(self):
326
+ iden = self.info.get('view')
327
+ vdef = await self.runt.snap.core.getViewDef(iden)
328
+ if vdef is None:
329
+ raise s_exc.NoSuchView(mesg=f'No view with {iden=}', iden=iden)
330
+ return s_stormtypes.View(self.runt, vdef, path=self.path)
331
+
332
+ async def _storVars(self, varz):
333
+ s_stormtypes.confirm(('storm', 'lib', 'cortex', 'httpapi', 'set'))
334
+ varz = await s_stormtypes.toprim(varz)
335
+ adef = await self.runt.snap.core.modHttpExtApi(self.iden, 'vars', varz)
336
+ _varz = self.info.get('vars')
337
+ _varz.clear()
338
+ _varz.update(**adef.get('vars'))
339
+ self.info['updated'] = adef.get('updated')
340
+
341
+ async def _storView(self, iden):
342
+ s_stormtypes.confirm(('storm', 'lib', 'cortex', 'httpapi', 'set'))
343
+ if isinstance(iden, s_stormtypes.View):
344
+ view = iden.value().get('iden')
345
+ else:
346
+ view = await s_stormtypes.tostr(iden)
347
+ adef = await self.runt.snap.core.modHttpExtApi(self.iden, 'view', view)
348
+ self.info['view'] = view
349
+ self.info['updated'] = adef.get('updated')
350
+
351
+ @s_stormtypes.stormfunc(readonly=True)
352
+ async def _gtorName(self):
353
+ return self.info.get('name')
354
+
355
+ async def _storDesc(self, desc):
356
+ s_stormtypes.confirm(('storm', 'lib', 'cortex', 'httpapi', 'set'))
357
+ desc = await s_stormtypes.tostr(desc)
358
+ adef = await self.runt.snap.core.modHttpExtApi(self.iden, 'desc', desc)
359
+ self.info['desc'] = desc
360
+ self.info['updated'] = adef.get('updated')
361
+
362
+ @s_stormtypes.stormfunc(readonly=True)
363
+ async def _gtorDesc(self):
364
+ return self.info.get('desc')
365
+
366
+ async def _storRunas(self, runas):
367
+ s_stormtypes.confirm(('storm', 'lib', 'cortex', 'httpapi', 'set'))
368
+ runas = await s_stormtypes.tostr(runas)
369
+ adef = await self.runt.snap.core.modHttpExtApi(self.iden, 'runas', runas)
370
+ self.info['runas'] = runas
371
+ self.info['updated'] = adef.get('updated')
372
+
373
+ @s_stormtypes.stormfunc(readonly=True)
374
+ async def _gtorRunas(self):
375
+ return self.info.get('runas')
376
+
377
+ async def _storReadonly(self, readonly):
378
+ s_stormtypes.confirm(('storm', 'lib', 'cortex', 'httpapi', 'set'))
379
+ readonly = await s_stormtypes.tobool(readonly)
380
+ adef = await self.runt.snap.core.modHttpExtApi(self.iden, 'readonly', readonly)
381
+ self.info['readonly'] = readonly
382
+ self.info['updated'] = adef.get('updated')
383
+
384
+ @s_stormtypes.stormfunc(readonly=True)
385
+ def _ctorVars(self, path=None):
386
+ return HttpApiVars(self, path=path)
387
+
388
+ @s_stormtypes.stormfunc(readonly=True)
389
+ async def _gtorReadonly(self):
390
+ return self.info.get('readonly')
391
+
392
+ async def _storOwner(self, owner):
393
+ s_stormtypes.confirm(('storm', 'lib', 'cortex', 'httpapi', 'set'))
394
+ if isinstance(owner, s_stormtypes.User):
395
+ info = await owner.value()
396
+ owner = info.get('iden')
397
+ else:
398
+ owner = await s_stormtypes.tostr(owner)
399
+ adef = await self.runt.snap.core.modHttpExtApi(self.iden, 'owner', owner)
400
+ self.info['owner'] = owner
401
+ self.info['updated'] = adef.get('updated')
402
+
403
+ @s_stormtypes.stormfunc(readonly=True)
404
+ async def _gtorOwner(self):
405
+ iden = self.info.get('owner')
406
+ udef = await self.runt.snap.core.getUserDef(iden)
407
+ if udef is None:
408
+ raise s_exc.NoSuchUser(mesg=f'HTTP API owner does not exist {iden}', user=iden)
409
+ return s_stormtypes.User(self.runt, udef['iden'])
410
+
411
+ @s_stormtypes.stormfunc(readonly=True)
412
+ async def _gtorCreator(self):
413
+ iden = self.info.get('creator')
414
+ udef = await self.runt.snap.core.getUserDef(iden)
415
+ if udef is None:
416
+ raise s_exc.NoSuchUser(mesg=f'HTTP API creator does not exist {iden}', user=iden)
417
+ return s_stormtypes.User(self.runt, udef['iden'])
418
+
419
+ @s_stormtypes.stormfunc(readonly=True)
420
+ async def _gtorUpdated(self):
421
+ return self.info.get('updated')
422
+
423
+ async def _storPerms(self, perms):
424
+ s_stormtypes.confirm(('storm', 'lib', 'cortex', 'httpapi', 'set'))
425
+ perms = await s_stormtypes.toprim(perms)
426
+ pdefs = []
427
+ for pdef in perms:
428
+ if isinstance(pdef, str):
429
+ pdef = _normPermString(pdef)
430
+ pdefs.append(pdef)
431
+ adef = await self.runt.snap.core.modHttpExtApi(self.iden, 'perms', pdefs)
432
+ self.info['perms'].clear()
433
+ self.info['perms'].extend(pdefs)
434
+ self.info['updated'] = adef.get('updated')
435
+
436
+ @s_stormtypes.stormfunc(readonly=True)
437
+ def _ctorPerms(self, path):
438
+ return HttpPermsList(self, path)
439
+
440
+ async def _storAuthenticated(self, authenticated):
441
+ s_stormtypes.confirm(('storm', 'lib', 'cortex', 'httpapi', 'set'))
442
+ authenticated = await s_stormtypes.tobool(authenticated)
443
+ adef = await self.runt.snap.core.modHttpExtApi(self.iden, 'authenticated', authenticated)
444
+ self.info['authenticated'] = authenticated
445
+ self.info['updated'] = adef.get('updated')
446
+
447
+ @s_stormtypes.stormfunc(readonly=True)
448
+ async def _gtorAuthenticated(self):
449
+ return self.info.get('authenticated')
450
+
451
+ @s_stormtypes.registry.registerType
452
+ class HttpApiMethods(s_stormtypes.Prim):
453
+ '''
454
+ Accessor dictionary for getting and setting Extened HTTP API methods.
455
+
456
+ Notes:
457
+ The Storm code used to run these methods will have a $request object
458
+ injected into them. This allows the method to send data back to the
459
+ caller when it is run.
460
+
461
+ Examples:
462
+ Setting a simple GET method::
463
+
464
+ $api.methods.get = ${
465
+ $data = ({"someKey": "someValue})
466
+ $headers = ({"someHeader": "someOtherValue"})
467
+ $request.reply(200, headers=$headers, body=$data)
468
+ }
469
+
470
+ Removing a PUT method::
471
+
472
+ $api.methods.put = $lib.undef
473
+
474
+ Crafting a custom text response::
475
+
476
+ $api.methods.get = ${
477
+ // Create the body
478
+ $data = 'some value'
479
+ // Encode the response as bytes
480
+ $data = $data.encode()
481
+ // Set the headers
482
+ $headers = ({"Content-Type": "text/plain", "Content-Length": $lib.len($data})
483
+ $request.reply(200, headers=$headers, body=$data)
484
+ }
485
+
486
+ Streaming multiple chunks of data as JSON lines. This sends the code, headers and body separately::
487
+
488
+ $api.methods.get = ${
489
+ $request.sendcode(200)
490
+ $request.sendheaders(({"Content-Type": "text/plain; charset=utf8"}))
491
+ $values = ((1), (2), (3))
492
+ for $i in $values {
493
+ $body=`{$lib.json.save(({"value": $i}))}\n`
494
+ $request.sendbody($body.encode())
495
+ }
496
+ }
497
+
498
+ '''
499
+ _storm_typename = 'http:api:methods'
500
+ _storm_locals = (
501
+ {'name': 'get', 'desc': 'The GET request Storm code.',
502
+ 'type': {'type': ['gtor', 'stor'], '_storfunc': '_storMethGet', '_gtorfunc': '_gtorMethGet',
503
+ 'returns': {'type': ['str', 'null']}}},
504
+ {'name': 'put', 'desc': 'The PUT request Storm code.',
505
+ 'type': {'type': ['gtor', 'stor'], '_storfunc': '_storMethPut', '_gtorfunc': '_gtorMethPut',
506
+ 'returns': {'type': ['str', 'null']}}},
507
+ {'name': 'head', 'desc': 'The HEAD request Storm code',
508
+ 'type': {'type': ['gtor', 'stor'], '_storfunc': '_storMethHead', '_gtorfunc': '_gtorMethHead',
509
+ 'returns': {'type': ['str', 'null']}}},
510
+ {'name': 'post', 'desc': 'The POST request Storm code.',
511
+ 'type': {'type': ['gtor', 'stor'], '_storfunc': '_storMethPost', '_gtorfunc': '_gtorMethPost',
512
+ 'returns': {'type': ['str', 'null']}}},
513
+ {'name': 'delete', 'desc': 'The DELETE request Storm code.',
514
+ 'type': {'type': ['gtor', 'stor'], '_storfunc': '_storMethDelete', '_gtorfunc': '_gtorMethDelete',
515
+ 'returns': {'type': ['str', 'null']}}},
516
+ {'name': 'patch', 'desc': 'The PATCH request Storm code.',
517
+ 'type': {'type': ['gtor', 'stor'], '_storfunc': '_storMethPatch', '_gtorfunc': '_gtorMethPatch',
518
+ 'returns': {'type': ['str', 'null']}}},
519
+ {'name': 'options', 'desc': 'The OPTIONS request Storm code.',
520
+ 'type': {'type': ['gtor', 'stor'], '_storfunc': '_storMethOptions', '_gtorfunc': '_gtorMethOptions',
521
+ 'returns': {'type': ['str', 'null']}}},
522
+ )
523
+ _ismutable = True
524
+
525
+ def __init__(self, httpapi: HttpApi):
526
+ s_stormtypes.Prim.__init__(self, httpapi.info.get('methods'))
527
+ self.httpapi = httpapi
528
+
529
+ self.gtors.update({
530
+ 'get': self._gtorMethGet,
531
+ 'head': self._gtorMethHead,
532
+ 'post': self._gtorMethPost,
533
+ 'put': self._gtorMethPut,
534
+ 'delete': self._gtorMethDelete,
535
+ 'patch': self._gtorMethPatch,
536
+ 'options': self._gtorMethOptions,
537
+ })
538
+
539
+ self.stors.update({
540
+ 'get': self._storMethGet,
541
+ 'head': self._storMethHead,
542
+ 'post': self._storMethPost,
543
+ 'put': self._storMethPut,
544
+ 'delete': self._storMethDelete,
545
+ 'patch': self._storMethPatch,
546
+ 'options': self._storMethOptions,
547
+ })
548
+
549
+ async def iter(self):
550
+ for k, v in list(self.valu.items()):
551
+ yield (k, v)
552
+
553
+ async def _storMethFunc(self, meth, query):
554
+ s_stormtypes.confirm(('storm', 'lib', 'cortex', 'httpapi', 'set'))
555
+ meth = await s_stormtypes.tostr(meth)
556
+ methods = self.valu.copy()
557
+
558
+ if query is s_stormtypes.undef:
559
+ methods.pop(meth, None)
560
+ adef = await self.httpapi.runt.snap.core.modHttpExtApi(self.httpapi.iden, 'methods', methods)
561
+ self.valu.pop(meth, None)
562
+ self.httpapi.info['updated'] = adef.get('updated')
563
+ else:
564
+ query = await s_stormtypes.tostr(query)
565
+ query = query.strip()
566
+
567
+ # Ensure our query can be parsed.
568
+ await self.httpapi.runt.snap.core.getStormQuery(query)
569
+
570
+ methods[meth] = query
571
+ adef = await self.httpapi.runt.snap.core.modHttpExtApi(self.httpapi.iden, 'methods', methods)
572
+ self.valu[meth] = query
573
+ self.httpapi.info['updated'] = adef.get('updated')
574
+
575
+ async def _storMethGet(self, query):
576
+ return await self._storMethFunc('get', query)
577
+
578
+ async def _storMethHead(self, query):
579
+ return await self._storMethFunc('head', query)
580
+
581
+ async def _storMethPost(self, query):
582
+ return await self._storMethFunc('post', query)
583
+
584
+ async def _storMethPut(self, query):
585
+ return await self._storMethFunc('put', query)
586
+
587
+ async def _storMethPatch(self, query):
588
+ return await self._storMethFunc('patch', query)
589
+
590
+ async def _storMethOptions(self, query):
591
+ return await self._storMethFunc('options', query)
592
+
593
+ async def _storMethDelete(self, query):
594
+ return await self._storMethFunc('delete', query)
595
+
596
+ @s_stormtypes.stormfunc(readonly=True)
597
+ async def _gtorMethGet(self):
598
+ return self.valu.get('get')
599
+
600
+ @s_stormtypes.stormfunc(readonly=True)
601
+ async def _gtorMethHead(self):
602
+ return self.valu.get('head')
603
+
604
+ @s_stormtypes.stormfunc(readonly=True)
605
+ async def _gtorMethPost(self):
606
+ return self.valu.get('post')
607
+
608
+ @s_stormtypes.stormfunc(readonly=True)
609
+ async def _gtorMethPut(self):
610
+ return self.valu.get('put')
611
+
612
+ @s_stormtypes.stormfunc(readonly=True)
613
+ async def _gtorMethDelete(self):
614
+ return self.valu.get('delete')
615
+
616
+ @s_stormtypes.stormfunc(readonly=True)
617
+ async def _gtorMethPatch(self):
618
+ return self.valu.get('patch')
619
+
620
+ @s_stormtypes.stormfunc(readonly=True)
621
+ async def _gtorMethOptions(self):
622
+ return self.valu.get('options')
623
+
624
+ @s_stormtypes.registry.registerType
625
+ class HttpHeaderDict(s_stormtypes.Dict):
626
+ '''
627
+ Immutable lowercase key access dictionary for HTTP request headers.
628
+
629
+ Example:
630
+ Request headers can be accessed in a case insensitive manner::
631
+
632
+ $valu = $request.headers.Cookie
633
+ // or the lower case value
634
+ $valu = $request.headers.cookie
635
+ '''
636
+ _storm_typename = 'http:api:request:headers'
637
+ _storm_locals = ()
638
+ _ismutable = True
639
+
640
+ async def setitem(self, name, valu):
641
+ mesg = f'{self._storm_typename} may not be modified by the runtime.'
642
+ raise s_exc.StormRuntimeError(mesg=mesg, name=name)
643
+
644
+ async def deref(self, name):
645
+ name = await s_stormtypes.tostr(name)
646
+ name = name.lower()
647
+ return self.valu.get(name)
648
+
649
+ @s_stormtypes.registry.registerType
650
+ class HttpPermsList(s_stormtypes.List):
651
+ '''
652
+ Accessor list for getting and setting http:api permissions.
653
+ '''
654
+ _storm_typename = 'http:api:perms'
655
+ _storm_locals = (
656
+ {'name': 'has', 'desc': 'Check it a permission is in the list.',
657
+ 'type': {'type': 'function', '_funcname': '_methListHas',
658
+ 'args': (
659
+ {'name': 'valu', 'type': 'any', 'desc': 'The permission to check.', },
660
+ ),
661
+ 'returns': {'type': 'boolean', 'desc': 'True if the permission is in the list, false otherwise.', }}},
662
+ {'name': 'pop', 'desc': 'Pop and return the last permission in the list.',
663
+ 'type': {'type': 'function', '_funcname': '_methListPop',
664
+ 'returns': {'type': 'any', 'desc': 'The last permission from the list.', }}},
665
+ {'name': 'size', 'desc': 'Return the length of the list.',
666
+ 'type': {'type': 'function', '_funcname': '_methListSize',
667
+ 'returns': {'type': 'int', 'desc': 'The size of the list.', }}},
668
+ {'name': 'index', 'desc': 'Return a single permission from the list by index.',
669
+ 'type': {'type': 'function', '_funcname': '_methListIndex',
670
+ 'args': (
671
+ {'name': 'valu', 'type': 'int', 'desc': 'The list index value.', },
672
+ ),
673
+ 'returns': {'type': 'any', 'desc': 'The permission present in the list at the index position.', }}},
674
+ {'name': 'length', 'desc': 'Get the length of the list. This is deprecated; please use ``.size()`` instead.',
675
+ 'type': {'type': 'function', '_funcname': '_methListLength',
676
+ 'returns': {'type': 'int', 'desc': 'The size of the list.', }}},
677
+ {'name': 'append', 'desc': 'Append a permission to the list.',
678
+ 'type': {'type': 'function', '_funcname': '_methListAppend',
679
+ 'args': (
680
+ {'name': 'valu', 'type': 'any', 'desc': 'The permission to append to the list.', },
681
+ ),
682
+ 'returns': {'type': 'null', }}},
683
+ {'name': 'reverse', 'desc': 'Reverse the order of the list in place',
684
+ 'type': {'type': 'function', '_funcname': '_methListReverse',
685
+ 'returns': {'type': 'null', }}},
686
+ {'name': 'slice', 'desc': 'Get a slice of the list.',
687
+ 'type': {'type': 'function', '_funcname': '_methListSlice',
688
+ 'args': (
689
+ {'name': 'start', 'type': 'int', 'desc': 'The starting index.'},
690
+ {'name': 'end', 'type': 'int', 'default': None,
691
+ 'desc': 'The ending index. If not specified, slice to the end of the list.'},
692
+ ),
693
+ 'returns': {'type': 'list', 'desc': 'The slice of the list.'}}},
694
+
695
+ {'name': 'extend', 'desc': 'Extend a list using another iterable.',
696
+ 'type': {'type': 'function', '_funcname': '_methListExtend',
697
+ 'args': (
698
+ {'name': 'valu', 'type': 'list', 'desc': 'A list or other iterable.'},
699
+ ),
700
+ 'returns': {'type': 'null'}}},
701
+ )
702
+ _ismutable = True
703
+
704
+ def __init__(self, httpapi, path=None):
705
+ s_stormtypes.Prim.__init__(self, httpapi.info.get('perms'))
706
+ self.httpapi = httpapi
707
+ self.locls.update(self.getObjLocals())
708
+
709
+ async def setitem(self, name, valu):
710
+ indx = await s_stormtypes.toint(name)
711
+ pdefs = self.valu.copy()
712
+ if valu is s_stormtypes.undef:
713
+ try:
714
+ pdefs.pop(indx)
715
+ except IndexError:
716
+ pass
717
+ else:
718
+ await self.httpapi._storPerms(pdefs)
719
+ else:
720
+ pdef = await s_stormtypes.toprim(valu)
721
+ if isinstance(pdef, str):
722
+ pdef = _normPermString(pdef)
723
+ pdefs[indx] = pdef
724
+ await self.httpapi._storPerms(pdefs)
725
+
726
+ async def _methListAppend(self, valu):
727
+ pdef = await s_stormtypes.toprim(valu)
728
+ if isinstance(pdef, str):
729
+ pdef = _normPermString(pdef)
730
+ pdefs = self.valu.copy()
731
+ pdefs.append(pdef)
732
+ await self.httpapi._storPerms(pdefs)
733
+
734
+ async def _methListHas(self, valu):
735
+ pdef = await s_stormtypes.toprim(valu)
736
+ if isinstance(pdef, str):
737
+ pdef = _normPermString(pdef)
738
+ return await s_stormtypes.List._methListHas(self, pdef)
739
+
740
+ async def _methListReverse(self):
741
+ pdefs = self.valu.copy()
742
+ pdefs.reverse()
743
+ await self.httpapi._storPerms(pdefs)
744
+
745
+ async def _methListPop(self):
746
+ pdefs = self.valu.copy()
747
+ try:
748
+ valu = pdefs.pop()
749
+ except IndexError:
750
+ mesg = 'The permissions list is empty. Nothing to pop.'
751
+ raise s_exc.StormRuntimeError(mesg=mesg)
752
+ else:
753
+ await self.httpapi._storPerms(pdefs)
754
+ return valu
755
+
756
+ async def _methListSort(self, reverse=False):
757
+ # This is not a documented method. Attempting to sort a list of permissions has no meaning.
758
+ raise s_exc.StormRuntimeError(mesg=f'{self._storm_typename} does not support sorting.')
759
+
760
+ async def _methListExtend(self, valu):
761
+ pdefs = self.valu.copy()
762
+ async for pdef in s_stormtypes.toiter(valu):
763
+ pdef = await s_stormtypes.toprim(pdef)
764
+ if isinstance(pdef, str):
765
+ pdef = _normPermString(pdef)
766
+ pdefs.append(pdef)
767
+ await self.httpapi._storPerms(pdefs)
768
+
769
+
770
+ @s_stormtypes.registry.registerType
771
+ class HttpApiVars(s_stormtypes.Dict):
772
+ '''
773
+ Accessor dictionary for getting and setting Extended HTTP API variables.
774
+
775
+ This can be used to set, unset or iterate over the runtime variables that are
776
+ set for an Extended HTTP API endpoint. These variables are set in the Storm
777
+ runtime for all of the HTTP methods configured to be executed by the endpoint.
778
+
779
+ Example:
780
+ Set a few variables on a given API::
781
+
782
+ $api.vars.foo = 'the foo string'
783
+ $api.vars.bar = (1234)
784
+
785
+ Remove a variable::
786
+
787
+ $api.vars.foo = $lib.undef
788
+
789
+ Iterate over the variables set for the endpoint::
790
+
791
+ for ($key, $valu) in $api.vars {
792
+ $lib.print(`{$key) -> {$valu}`)
793
+ }
794
+
795
+ Overwrite all of the variables for a given API with a new dictionary::
796
+
797
+ $api.vars = ({"foo": "a new string", "bar": (137)})
798
+ '''
799
+ _storm_typename = 'http:api:vars'
800
+ _storm_locals = ()
801
+ _ismutable = True
802
+
803
+ def __init__(self, httpapi, path=None):
804
+ s_stormtypes.Dict.__init__(self, httpapi.info.get('vars'), path=path)
805
+ self.httpapi = httpapi
806
+
807
+ async def setitem(self, name, valu):
808
+ s_stormtypes.confirm(('storm', 'lib', 'cortex', 'httpapi', 'set'))
809
+ name = await s_stormtypes.tostr(name)
810
+
811
+ varz = self.valu.copy()
812
+ if valu is s_stormtypes.undef:
813
+ varz.pop(name, None)
814
+ adef = await self.httpapi.runt.snap.core.modHttpExtApi(self.httpapi.iden, 'vars', varz)
815
+ self.valu.pop(name, None)
816
+ self.httpapi.info['updated'] = adef.get('updated')
817
+ else:
818
+ valu = await s_stormtypes.toprim(valu)
819
+ varz[name] = valu
820
+ adef = await self.httpapi.runt.snap.core.modHttpExtApi(self.httpapi.iden, 'vars', varz)
821
+ self.valu[name] = valu
822
+ self.httpapi.info['updated'] = adef.get('updated')
823
+
824
+ @s_stormtypes.registry.registerType
825
+ class HttpReq(s_stormtypes.StormType):
826
+ '''
827
+ Extended HTTP API Request object.
828
+ '''
829
+ _storm_typename = 'http:api:request'
830
+ _storm_locals = (
831
+ {'name': 'args', 'desc': '''
832
+ A list of path arguments made as part of the HTTP API request.
833
+ These are the results of any capture groups defined in the Extended HTTP API path regular expression.''',
834
+ 'type': 'list'},
835
+ {'name': 'body', 'desc': 'The raw request body.', 'type': 'bytes'},
836
+ {'name': 'method', 'desc': 'The request method', 'type': 'str'},
837
+ {'name': 'params', 'desc': 'Request parameters.', 'type': 'dict'},
838
+ {'name': 'client', 'desc': 'The remote IP of the requester.', 'type': 'str'},
839
+ {'name': 'uri', 'desc': 'The full request URI.', 'type': 'str'},
840
+ {'name': 'path', 'desc': 'The path which was matched against the Extended HTTPAPI endpoint.', 'type': 'str'},
841
+ {'name': 'user',
842
+ 'desc': 'The user iden who made the HTTP API request.', 'type': 'str'},
843
+ {'name': 'api', 'desc': 'The http:api object for the request.',
844
+ 'type': {'type': 'gtor', '_gtorfunc': '_gtorApi',
845
+ 'returns': {'type': 'http:api'}}},
846
+ {'name': 'headers', 'desc': 'The request headers.',
847
+ 'type': {'type': 'ctor', '_ctorfunc': '_ctorJson',
848
+ 'returns': {'type': 'http:api:request:headers'}}},
849
+ {'name': 'json', 'desc': 'The request body as json.',
850
+ 'type': {'type': 'ctor', '_ctorfunc': '_ctorJson',
851
+ 'returns': {'type': 'dict'}}},
852
+ {'name': 'sendcode', 'desc': 'Send the HTTP response code.',
853
+ 'type': {'type': 'function', '_funcname': '_methSendCode',
854
+ 'args': (
855
+ {'name': 'code', 'type': 'int',
856
+ 'desc': 'The response code.'},
857
+ ),
858
+ 'returns': {'type': 'null'}}},
859
+ {'name': 'sendheaders', 'desc': 'Send the HTTP response headers.',
860
+ 'type': {'type': 'function', '_funcname': '_methSendHeaders',
861
+ 'args': (
862
+ {'name': 'headers', 'type': 'dict',
863
+ 'desc': 'The response headers.'},
864
+ ),
865
+ 'returns': {'type': 'null'}}},
866
+ {'name': 'sendbody', 'desc': 'Send the HTTP response body.',
867
+ 'type': {'type': 'function', '_funcname': '_methSendBody',
868
+ 'args': (
869
+ {'name': 'body', 'type': 'bytes',
870
+ 'desc': 'The response body.'},
871
+ ),
872
+ 'returns': {'type': 'null'}}},
873
+ {'name': 'reply', 'desc': '''
874
+ Convenience method to send the response code, headers and body together.
875
+
876
+ Notes:
877
+ This can only be called once.
878
+
879
+ If the response body is not bytes, this method will serialize the body as JSON
880
+ and set the ``Content-Type`` and ``Content-Length`` response headers.
881
+ ''',
882
+ 'type': {'type': 'function', '_funcname': '_methReply',
883
+ 'args': (
884
+ {'name': 'code', 'type': 'int',
885
+ 'desc': 'The response code.'},
886
+ {'name': 'headers', 'type': 'dict',
887
+ 'desc': 'The response headers.', 'default': None},
888
+ {'name': 'body', 'type': 'any',
889
+ 'desc': 'The response body.', 'default': '$lib.undef'},
890
+ ),
891
+ 'returns': {'type': 'null'}}},
892
+ )
893
+
894
+ def __init__(self, runt, rnfo):
895
+ s_stormtypes.StormType.__init__(self)
896
+
897
+ self.replied = False
898
+
899
+ self.runt = runt
900
+ self.rnfo = rnfo
901
+ self.rcode = None
902
+ self.rbody = None
903
+ self.rheaders = None
904
+ self.locls.update(self.getObjLocals())
905
+
906
+ # Constants for a given instance
907
+ self.locls.update({
908
+ 'args': self.rnfo.get('args'),
909
+ 'body': self.rnfo.get('body'),
910
+ 'method': self.rnfo.get('method'),
911
+ 'params': self.rnfo.get('params'),
912
+ 'client': self.rnfo.get('client'),
913
+ 'uri': self.rnfo.get('uri'),
914
+ 'path': self.rnfo.get('path'),
915
+ 'user': self.rnfo.get('user'),
916
+ })
917
+
918
+ self.gtors.update({
919
+ 'api': self._gtorApi, # Not a ctor since the adef retrieval is an async process
920
+ })
921
+
922
+ self.ctors.update({
923
+ 'json': self._ctorJson,
924
+ 'headers': self._ctorHeaders,
925
+ })
926
+
927
+ def getObjLocals(self):
928
+ return {
929
+ 'sendcode': self._methSendCode,
930
+ 'sendheaders': self._methSendHeaders,
931
+ 'sendbody': self._methSendBody,
932
+ 'reply': self._methReply,
933
+ }
934
+
935
+ @s_stormtypes.stormfunc(readonly=True)
936
+ def _ctorHeaders(self, path=None):
937
+ headers = self.rnfo.get('headers')
938
+ return HttpHeaderDict(valu=headers, path=path)
939
+
940
+ @s_stormtypes.stormfunc(readonly=True)
941
+ async def _gtorApi(self):
942
+ s_stormtypes.confirm(('storm', 'lib', 'cortex', 'httpapi', 'get'))
943
+ adef = await self.runt.snap.core.getHttpExtApi(self.rnfo.get('iden'))
944
+ return HttpApi(self.runt, adef)
945
+
946
+ @s_stormtypes.stormfunc(readonly=True)
947
+ def _ctorJson(self, path=None):
948
+ try:
949
+ return json.loads(self.rnfo.get('body'))
950
+ except (UnicodeDecodeError, json.JSONDecodeError) as e:
951
+ raise s_exc.StormRuntimeError(mesg='Failed to decode request body as JSON: {e}') from None
952
+
953
+ @s_stormtypes.stormfunc(readonly=True)
954
+ async def _methSendCode(self, code):
955
+ code = await s_stormtypes.toint(code)
956
+ await self.runt.snap.fire('http:resp:code', code=code)
957
+
958
+ @s_stormtypes.stormfunc(readonly=True)
959
+ async def _methSendHeaders(self, headers):
960
+ headers = await s_stormtypes.toprim(headers)
961
+ if not isinstance(headers, dict):
962
+ typ = await s_stormtypes.totype(headers)
963
+ raise s_exc.BadArg(mesg=f'HTTP Response headers must be a dictionary, got {typ}.')
964
+ await self.runt.snap.fire('http:resp:headers', headers=headers)
965
+
966
+ @s_stormtypes.stormfunc(readonly=True)
967
+ async def _methSendBody(self, body):
968
+ body = await s_stormtypes.toprim(body)
969
+ if not isinstance(body, bytes):
970
+ typ = await s_stormtypes.totype(body)
971
+ raise s_exc.BadArg(mesg=f'HTTP Response body must be bytes, got {typ}.')
972
+ await self.runt.snap.fire('http:resp:body', body=body)
973
+
974
+ # Convenience method
975
+ @s_stormtypes.stormfunc(readonly=True)
976
+ async def _methReply(self, code, headers=None, body=s_stormtypes.undef):
977
+ if self.replied:
978
+ raise s_exc.BadArg(mesg='Response.reply() has already been called.')
979
+
980
+ headers = await s_stormtypes.toprim(headers)
981
+ if headers is None:
982
+ headers = {}
983
+
984
+ if body is not s_stormtypes.undef:
985
+ if not isinstance(body, bytes):
986
+ body = await s_stormtypes.toprim(body)
987
+ body = json.dumps(body).encode('utf-8', 'surrogatepass')
988
+ headers['Content-Type'] = 'application/json; charset=utf8"'
989
+ headers['Content-Length'] = len(body)
990
+
991
+ await self._methSendCode(code)
992
+
993
+ if headers:
994
+ await self._methSendHeaders(headers)
995
+
996
+ if body is not s_stormtypes.undef:
997
+ await self._methSendBody(body)
998
+
999
+ self.replied = True
1000
+
1001
+ @s_stormtypes.registry.registerLib
1002
+ class CortexHttpApi(s_stormtypes.Lib):
1003
+ '''
1004
+ Library for interacting with the Extended HTTP API.
1005
+ '''
1006
+ _storm_locals = (
1007
+ {'name': 'add', 'desc': '''
1008
+ Add an Extended HTTP API endpoint to the Cortex.
1009
+
1010
+ This can be used to add an API endpoint which will be resolved under
1011
+ the API path "/api/ext/". New API endpoint objects are appended to
1012
+ a list of APIs to resolve in order.
1013
+
1014
+ Notes:
1015
+ The Cortex does not make any attempt to do any inspection of path values which may conflict between one
1016
+ another. This is because the paths for a given endpoint may be changed, they can contain regular
1017
+ expressions, and they may have their resolution order changed. Cortex administrators are responsible for
1018
+ configuring their Extended HTTP API endpoints with correct paths and order to meet their use cases.
1019
+
1020
+ Example:
1021
+ Add a simple API handler::
1022
+
1023
+ // Create an endpoint for /api/ext/foo/bar
1024
+ $api = $lib.cortex.httpapi.add('foo/bar')
1025
+
1026
+ // Define a GET response handler via storm that makes a simple reply.
1027
+ $api.methods.get = ${ $request.reply(200, body=({"some": "data}) }
1028
+
1029
+ Add a wildcard handler::
1030
+
1031
+ // Create a wildcard endpoint for /api/ext/some/thing([a-zA-Z0-9]*)/([a-zA-Z0-9]*)
1032
+ $api = $lib.cortex.httpapi.add('some/thing([a-zA-Z0-9]*)/([a-zA-Z0-9]*)')
1033
+
1034
+ // The capture groups are exposed as request arguments.
1035
+ // Echo them back to the caller.
1036
+ $api.methods.get = ${
1037
+ $request.reply(200, body=({"args": $request.args})
1038
+ }
1039
+ ''',
1040
+ 'type': {'type': 'function', '_funcname': 'addHttpApi',
1041
+ 'args': (
1042
+ {'name': 'path', 'type': 'string',
1043
+ 'desc': 'The extended HTTP API path.'},
1044
+ {'name': 'name', 'type': 'string',
1045
+ 'desc': 'Friendly name for the Extended HTTP API', 'default': ''},
1046
+ {'name': 'desc', 'type': 'string',
1047
+ 'desc': 'Description for the Extended HTTP API.', 'default': ''},
1048
+ {'name': 'runas', 'type': 'string',
1049
+ 'desc': 'Run the storm query as the API "owner" or as the authenticated "user".',
1050
+ 'default': 'owner'},
1051
+ {'name': 'authenticated', 'type': 'boolean',
1052
+ 'desc': 'Require the API endpoint to be authenticated.', 'default': True},
1053
+ {'name': 'readonly', 'type': 'boolean',
1054
+ 'desc': 'Run the Extended HTTP Storm methods in readonly mode.', 'default': False},
1055
+ ),
1056
+ 'returns': {'type': 'http:api', 'desc': 'A new http:api object.'}}},
1057
+ {'name': 'del', 'desc': 'Delete an Extended HTTP API endpoint.',
1058
+ 'type': {'type': 'function', '_funcname': 'delHttpApi',
1059
+ 'args': (
1060
+ {'name': 'iden', 'type': 'string',
1061
+ 'desc': 'The iden of the API to delete.'},
1062
+ ),
1063
+ 'returns': {'type': 'null'}}},
1064
+ {'name': 'get', 'desc': 'Get an Extended HTTP API object.',
1065
+ 'type': {'type': 'function', '_funcname': 'getHttpApi',
1066
+ 'args': (
1067
+ {'name': 'iden', 'type': 'string',
1068
+ 'desc': 'The iden of the API to retreive.'},
1069
+ ),
1070
+ 'returns': {'type': 'http:api', 'desc': 'The http:api object.'}}},
1071
+ {'name': 'list', 'desc': 'Get all the Extneded HTTP APIs on the Cortex',
1072
+ 'type': {'type': 'function', '_funcname': 'listHttpApis', 'args': (),
1073
+ 'returns': {'type': 'list', 'desc': 'A list of http:api objects'}}},
1074
+ {'name': 'index', 'desc': 'Set the index for a given Extended HTTP API.',
1075
+ 'type': {'type': 'function', '_funcname': 'setHttpApiIndx',
1076
+ 'args': (
1077
+ {'name': 'iden', 'type': 'string',
1078
+ 'desc': 'The iden of the API to modify.'},
1079
+ {'name': 'index', 'type': 'int', 'default': 0,
1080
+ 'desc': 'The new index of the API. Uses zero based indexing.'},
1081
+ ),
1082
+ 'returns': {'type': 'int', 'desc': 'The new index location of the API.'}}},
1083
+ {'name': 'response', 'desc': 'Make a response object. Used by API handlers automatically.',
1084
+ 'type': {'type': 'function', '_funcname': 'makeHttpResponse',
1085
+ 'args': (
1086
+ {'name': 'requestinfo', 'type': 'dict',
1087
+ 'desc': 'Request info dictionary. This is an opaque data structure which may change.'},
1088
+ ),
1089
+ 'returns': {'type': 'http:api:request'}}},
1090
+ )
1091
+ _storm_lib_path = ('cortex', 'httpapi')
1092
+
1093
+ _storm_lib_perms = (
1094
+ {'perm': ('storm', 'lib', 'cortex', 'httpapi', 'add'), 'gate': 'cortex',
1095
+ 'desc': 'Controls the ability to add a new Extended HTTP API on the Cortex.'},
1096
+ {'perm': ('storm', 'lib', 'cortex', 'httpapi', 'get'), 'gate': 'cortex',
1097
+ 'desc': 'Controls the ability to get or list Extended HTTP APIs on the Cortex.'},
1098
+ {'perm': ('storm', 'lib', 'cortex', 'httpapi', 'del'), 'gate': 'cortex',
1099
+ 'desc': 'Controls the ability to delete an Extended HTTP API on the Cortex.'},
1100
+ {'perm': ('storm', 'lib', 'cortex', 'httpapi', 'set'), 'gate': 'cortex',
1101
+ 'desc': 'Controls the ability to modify an Extended HTTP API on the Cortex.'},
1102
+ )
1103
+
1104
+ def getObjLocals(self):
1105
+ return {
1106
+ 'add': self.addHttpApi,
1107
+ 'del': self.delHttpApi,
1108
+ 'get': self.getHttpApi,
1109
+ 'list': self.listHttpApis,
1110
+ 'index': self.setHttpApiIndx,
1111
+ 'response': self.makeHttpResponse,
1112
+ }
1113
+
1114
+ @s_stormtypes.stormfunc(readonly=True)
1115
+ async def makeHttpResponse(self, requestinfo):
1116
+ requestinfo = await s_stormtypes.toprim(requestinfo)
1117
+ return HttpReq(self.runt, requestinfo)
1118
+
1119
+ @s_stormtypes.stormfunc(readonly=True)
1120
+ async def getHttpApi(self, iden):
1121
+ s_stormtypes.confirm(('storm', 'lib', 'cortex', 'httpapi', 'get'))
1122
+ iden = await s_stormtypes.tostr(iden)
1123
+ adef = await self.runt.snap.core.getHttpExtApi(iden)
1124
+ return HttpApi(self.runt, adef)
1125
+
1126
+ @s_stormtypes.stormfunc(readonly=True)
1127
+ async def listHttpApis(self):
1128
+ s_stormtypes.confirm(('storm', 'lib', 'cortex', 'httpapi', 'get'))
1129
+ adefs = await self.runt.snap.core.getHttpExtApis()
1130
+ apis = [HttpApi(self.runt, adef) for adef in adefs]
1131
+ return apis
1132
+
1133
+ async def addHttpApi(self, path, name='', desc='', runas='owner', authenticated=True, readonly=False):
1134
+ s_stormtypes.confirm(('storm', 'lib', 'cortex', 'httpapi', 'add'))
1135
+
1136
+ path = await s_stormtypes.tostr(path)
1137
+ name = await s_stormtypes.tostr(name)
1138
+ desc = await s_stormtypes.tostr(desc)
1139
+ runas = await s_stormtypes.tostr(runas)
1140
+ readonly = await s_stormtypes.tobool(readonly)
1141
+ authenticated = await s_stormtypes.tobool(authenticated)
1142
+
1143
+ adef = {
1144
+ 'path': path,
1145
+ 'view': self.runt.snap.view.iden,
1146
+ 'runas': runas,
1147
+ 'creator': self.runt.user.iden,
1148
+ 'owner': self.runt.user.iden,
1149
+ 'methods': {},
1150
+ 'authenticated': authenticated,
1151
+ 'name': name,
1152
+ 'desc': desc,
1153
+ 'readonly': readonly,
1154
+ }
1155
+
1156
+ adef = await self.runt.snap.core.addHttpExtApi(adef)
1157
+ return HttpApi(self.runt, adef)
1158
+
1159
+ async def delHttpApi(self, iden):
1160
+ s_stormtypes.confirm(('storm', 'lib', 'cortex', 'httpapi', 'del'))
1161
+ iden = await s_stormtypes.tostr(iden)
1162
+ return await self.runt.snap.view.core.delHttpExtApi(iden)
1163
+
1164
+ async def setHttpApiIndx(self, iden, index=0):
1165
+ s_stormtypes.confirm(('storm', 'lib', 'cortex', 'httpapi', 'set'))
1166
+ iden = await s_stormtypes.tostr(iden)
1167
+ index = await s_stormtypes.toint(index)
1168
+ return await self.runt.snap.view.core.setHttpApiIndx(iden, index)