69 obj = getattr(obj, names.pop(0)) |
93 obj = getattr(obj, names.pop(0)) |
70 return obj |
94 return obj |
71 |
95 |
72 |
96 |
73 class WampSession(wamp.ApplicationSession): |
97 class WampSession(wamp.ApplicationSession): |
|
98 |
74 def onConnect(self): |
99 def onConnect(self): |
75 if "secret" in self.config.extra: |
100 if "secret" in self.config.extra: |
76 user = self.config.extra["ID"].encode('utf8') |
101 user = self.config.extra["ID"] |
77 self.join(u"Automation", [u"wampcra"], user) |
102 self.join(u"Automation", [u"wampcra"], user) |
78 else: |
103 else: |
79 self.join(u"Automation") |
104 self.join(u"Automation") |
80 |
105 |
81 def onChallenge(self, challenge): |
106 def onChallenge(self, challenge): |
82 if challenge.method == u"wampcra": |
107 if challenge.method == u"wampcra": |
83 secret = self.config.extra["secret"].encode('utf8') |
108 if "secret" in self.config.extra: |
84 signature = auth.compute_wcs(secret, challenge.extra['challenge'].encode('utf8')) |
109 secret = self.config.extra["secret"].encode('utf8') |
85 return signature.decode("ascii") |
110 signature = auth.compute_wcs( |
86 else: |
111 secret, challenge.extra['challenge'].encode('utf8')) |
87 raise Exception("don't know how to handle authmethod {}".format(challenge.method)) |
112 return signature.decode("ascii") |
|
113 else: |
|
114 raise Exception("no secret given for authentication") |
|
115 else: |
|
116 raise Exception( |
|
117 "don't know how to handle authmethod {}".format(challenge.method)) |
88 |
118 |
89 @inlineCallbacks |
119 @inlineCallbacks |
90 def onJoin(self, details): |
120 def onJoin(self, details): |
91 global _WampSession |
121 global _WampSession |
92 _WampSession = self |
122 _WampSession = self |
93 ID = self.config.extra["ID"] |
123 ID = self.config.extra["ID"] |
94 print('WAMP session joined by :', ID) |
124 |
95 for name in ExposedCalls: |
125 for name, kwargs in ExposedCalls: |
96 regoption = types.RegisterOptions(u'exact', u'last') |
126 try: |
97 yield self.register(GetCallee(name), u'.'.join((ID, name)), regoption) |
127 registerOptions = types.RegisterOptions(**kwargs) |
|
128 except TypeError as e: |
|
129 registerOptions = None |
|
130 print(_("TypeError register option: {}".format(e))) |
|
131 |
|
132 yield self.register(GetCallee(name), u'.'.join((ID, name)), registerOptions) |
98 |
133 |
99 for name in SubscribedEvents: |
134 for name in SubscribedEvents: |
100 yield self.subscribe(GetCallee(name), unicode(name)) |
135 yield self.subscribe(GetCallee(name), unicode(name)) |
101 |
136 |
102 for func in DoOnJoin: |
137 for func in DoOnJoin: |
103 yield func(self) |
138 yield func(self) |
104 |
139 |
|
140 print(_('WAMP session joined (%s) by:' % time.ctime()), ID) |
|
141 |
105 def onLeave(self, details): |
142 def onLeave(self, details): |
106 global _WampSession |
143 global _WampSession, _transportFactory |
|
144 super(WampSession, self).onLeave(details) |
107 _WampSession = None |
145 _WampSession = None |
|
146 _transportFactory = None |
108 print(_('WAMP session left')) |
147 print(_('WAMP session left')) |
109 |
148 |
110 |
149 |
111 class ReconnectingWampWebSocketClientFactory(WampWebSocketClientFactory, ReconnectingClientFactory): |
150 class ReconnectingWampWebSocketClientFactory(WampWebSocketClientFactory, ReconnectingClientFactory): |
|
151 |
|
152 def __init__(self, config, *args, **kwargs): |
|
153 global _transportFactory |
|
154 WampWebSocketClientFactory.__init__(self, *args, **kwargs) |
|
155 |
|
156 try: |
|
157 protocolOptions = config.extra.get('protocolOptions', None) |
|
158 if protocolOptions: |
|
159 self.setProtocolOptions(**protocolOptions) |
|
160 _transportFactory = self |
|
161 except Exception, e: |
|
162 print(_("Custom protocol options failed :"), e) |
|
163 _transportFactory = None |
|
164 |
|
165 def buildProtocol(self, addr): |
|
166 self.resetDelay() |
|
167 return ReconnectingClientFactory.buildProtocol(self, addr) |
|
168 |
112 def clientConnectionFailed(self, connector, reason): |
169 def clientConnectionFailed(self, connector, reason): |
113 print(_("WAMP Client connection failed (%s) .. retrying .." % time.ctime())) |
170 if self.continueTrying: |
114 ReconnectingClientFactory.clientConnectionFailed(self, connector, reason) |
171 print(_("WAMP Client connection failed (%s) .. retrying ..") % |
|
172 time.ctime()) |
|
173 super(ReconnectingWampWebSocketClientFactory, |
|
174 self).clientConnectionFailed(connector, reason) |
|
175 else: |
|
176 del connector |
115 |
177 |
116 def clientConnectionLost(self, connector, reason): |
178 def clientConnectionLost(self, connector, reason): |
117 print(_("WAMP Client connection lost (%s) .. retrying .." % time.ctime())) |
179 if self.continueTrying: |
118 ReconnectingClientFactory.clientConnectionFailed(self, connector, reason) |
180 print(_("WAMP Client connection lost (%s) .. retrying ..") % |
119 |
181 time.ctime()) |
120 |
182 super(ReconnectingWampWebSocketClientFactory, |
121 def LoadWampClientConf(wampconf): |
183 self).clientConnectionFailed(connector, reason) |
122 try: |
184 else: |
123 WSClientConf = json.load(open(wampconf)) |
185 del connector |
124 return WSClientConf |
186 |
125 except ValueError, ve: |
187 |
126 print(_("WAMP load error: "), ve) |
188 def CheckConfiguration(WampClientConf): |
127 return None |
189 url = WampClientConf["url"] |
128 except Exception: |
190 if not IsCorrectUri(url): |
129 return None |
191 raise annotate.ValidateError( |
|
192 {"url": "Invalid URL: {}".format(url)}, |
|
193 _("WAMP configuration error:")) |
|
194 |
|
195 |
|
196 def GetConfiguration(): |
|
197 global lastKnownConfig |
|
198 |
|
199 if os.path.exists(_WampConf): |
|
200 WampClientConf = json.load(open(_WampConf)) |
|
201 else: |
|
202 WampClientConf = defaultWampConfig.copy() |
|
203 |
|
204 for itemName in mandatoryConfigItems: |
|
205 if WampClientConf.get(itemName, None) is None: |
|
206 raise Exception( |
|
207 _("WAMP configuration error : missing '{}' parameter.").format(itemName)) |
|
208 |
|
209 CheckConfiguration(WampClientConf) |
|
210 |
|
211 lastKnownConfig = WampClientConf.copy() |
|
212 return WampClientConf |
|
213 |
|
214 |
|
215 def SetWampSecret(wampSecret): |
|
216 with open(os.path.realpath(_WampSecret), 'w') as f: |
|
217 f.write(wampSecret) |
|
218 |
|
219 |
|
220 def SetConfiguration(WampClientConf): |
|
221 global lastKnownConfig |
|
222 |
|
223 CheckConfiguration(WampClientConf) |
|
224 |
|
225 lastKnownConfig = WampClientConf.copy() |
|
226 |
|
227 with open(os.path.realpath(_WampConf), 'w') as f: |
|
228 json.dump(WampClientConf, f, sort_keys=True, indent=4) |
|
229 if 'active' in WampClientConf and WampClientConf['active']: |
|
230 if _transportFactory and _WampSession: |
|
231 StopReconnectWampClient() |
|
232 StartReconnectWampClient() |
|
233 else: |
|
234 StopReconnectWampClient() |
|
235 |
|
236 return WampClientConf |
130 |
237 |
131 |
238 |
132 def LoadWampSecret(secretfname): |
239 def LoadWampSecret(secretfname): |
133 try: |
240 WSClientWampSecret = open(secretfname, 'rb').read() |
134 WSClientWampSecret = open(secretfname, 'rb').read() |
241 if len(WSClientWampSecret) == 0: |
135 return WSClientWampSecret |
242 raise Exception(_("WAMP secret empty")) |
136 except ValueError, ve: |
243 return WSClientWampSecret |
137 print(_("Wamp secret load error:"), ve) |
244 |
138 return None |
245 |
139 except Exception: |
246 def IsCorrectUri(uri): |
140 return None |
247 return re.match(r'wss?://[^\s?:#-]+(:[0-9]+)?(/[^\s]*)?$', uri) is not None |
141 |
248 |
142 |
249 |
143 def RegisterWampClient(wampconf, secretfname): |
250 def RegisterWampClient(wampconf=None, wampsecret=None): |
144 |
251 global _WampConf, _WampSecret |
145 WSClientConf = LoadWampClientConf(wampconf) |
252 _WampConfDefault = os.path.join(WorkingDir, "wampconf.json") |
146 |
253 _WampSecretDefault = os.path.join(WorkingDir, "wamp.secret") |
147 if not WSClientConf: |
254 |
148 print(_("WAMP client connection not established!")) |
255 # set config file path only if not already set |
|
256 if _WampConf is None: |
|
257 # default project's wampconf has precedance over commandline given |
|
258 if os.path.exists(_WampConfDefault) or wampconf is None: |
|
259 _WampConf = _WampConfDefault |
|
260 else: |
|
261 _WampConf = wampconf |
|
262 |
|
263 WampClientConf = GetConfiguration() |
|
264 |
|
265 # set secret file path only if not already set |
|
266 if _WampSecret is None: |
|
267 # default project's wamp secret also |
|
268 # has precedance over commandline given |
|
269 if os.path.exists(_WampSecretDefault): |
|
270 _WampSecret = _WampSecretDefault |
|
271 else: |
|
272 _WampSecret = wampsecret |
|
273 |
|
274 if _WampSecret is not None: |
|
275 WampClientConf["secret"] = LoadWampSecret(_WampSecret) |
|
276 else: |
|
277 print(_("WAMP authentication has no secret configured")) |
|
278 _WampSecret = _WampSecretDefault |
|
279 |
|
280 if not WampClientConf["active"]: |
|
281 print(_("WAMP deactivated in configuration")) |
149 return |
282 return |
150 |
|
151 WampSecret = LoadWampSecret(secretfname) |
|
152 |
|
153 if WampSecret is not None: |
|
154 WSClientConf["secret"] = WampSecret |
|
155 |
283 |
156 # create a WAMP application session factory |
284 # create a WAMP application session factory |
157 component_config = types.ComponentConfig( |
285 component_config = types.ComponentConfig( |
158 realm=WSClientConf["realm"], |
286 realm=WampClientConf["realm"], |
159 extra=WSClientConf) |
287 extra=WampClientConf) |
160 session_factory = wamp.ApplicationSessionFactory( |
288 session_factory = wamp.ApplicationSessionFactory( |
161 config=component_config) |
289 config=component_config) |
162 session_factory.session = WampSession |
290 session_factory.session = WampSession |
163 |
291 |
164 # create a WAMP-over-WebSocket transport client factory |
292 # create a WAMP-over-WebSocket transport client factory |
165 transport_factory = ReconnectingWampWebSocketClientFactory( |
293 ReconnectingWampWebSocketClientFactory( |
|
294 component_config, |
166 session_factory, |
295 session_factory, |
167 url=WSClientConf["url"], |
296 url=WampClientConf["url"], |
168 serializers=[MsgPackSerializer()]) |
297 serializers=[MsgPackSerializer()]) |
169 |
298 |
170 # start the client from a Twisted endpoint |
299 # start the client from a Twisted endpoint |
171 conn = connectWS(transport_factory) |
300 if _transportFactory: |
172 print(_("WAMP client connecting to :"), WSClientConf["url"]) |
301 connectWS(_transportFactory) |
173 return conn |
302 print(_("WAMP client connecting to :"), WampClientConf["url"]) |
|
303 return True |
|
304 else: |
|
305 print(_("WAMP client can not connect to :"), WampClientConf["url"]) |
|
306 return False |
|
307 |
|
308 |
|
309 def StopReconnectWampClient(): |
|
310 if _transportFactory is not None: |
|
311 _transportFactory.stopTrying() |
|
312 if _WampSession is not None: |
|
313 _WampSession.leave() |
|
314 |
|
315 |
|
316 def StartReconnectWampClient(): |
|
317 if _WampSession: |
|
318 # do reconnect |
|
319 _WampSession.disconnect() |
|
320 return True |
|
321 else: |
|
322 # do connect |
|
323 RegisterWampClient() |
|
324 return True |
174 |
325 |
175 |
326 |
176 def GetSession(): |
327 def GetSession(): |
177 return _WampSession |
328 return _WampSession |
178 |
329 |
179 |
330 |
|
331 def getWampStatus(): |
|
332 if _transportFactory is not None: |
|
333 if _WampSession is not None: |
|
334 if _WampSession.is_attached(): |
|
335 return "Attached" |
|
336 return "Established" |
|
337 return "Connecting" |
|
338 return "Disconnected" |
|
339 |
|
340 |
180 def SetServer(pysrv): |
341 def SetServer(pysrv): |
181 global _PySrv |
|
182 _PySrv = pysrv |
342 _PySrv = pysrv |
|
343 |
|
344 |
|
345 # WEB CONFIGURATION INTERFACE |
|
346 WAMP_SECRET_URL = "secret" |
|
347 webExposedConfigItems = ['active', 'url', 'ID'] |
|
348 |
|
349 |
|
350 def wampConfigDefault(ctx, argument): |
|
351 if lastKnownConfig is not None: |
|
352 return lastKnownConfig.get(argument.name, None) |
|
353 |
|
354 |
|
355 def wampConfig(**kwargs): |
|
356 secretfile_field = kwargs["secretfile"] |
|
357 if secretfile_field is not None: |
|
358 secretfile = getattr(secretfile_field, "file", None) |
|
359 if secretfile is not None: |
|
360 secret = secretfile_field.file.read() |
|
361 SetWampSecret(secret) |
|
362 |
|
363 newConfig = lastKnownConfig.copy() |
|
364 for argname in webExposedConfigItems: |
|
365 arg = kwargs.get(argname, None) |
|
366 if arg is not None: |
|
367 newConfig[argname] = arg |
|
368 |
|
369 SetConfiguration(newConfig) |
|
370 |
|
371 |
|
372 class FileUploadDownload(annotate.FileUpload): |
|
373 pass |
|
374 |
|
375 |
|
376 class FileUploadDownloadRenderer(webform.FileUploadRenderer): |
|
377 |
|
378 def input(self, context, slot, data, name, value): |
|
379 # pylint: disable=expression-not-assigned |
|
380 slot[_("Upload:")] |
|
381 slot = webform.FileUploadRenderer.input( |
|
382 self, context, slot, data, name, value) |
|
383 download_url = data.typedValue.getAttribute('download_url') |
|
384 return slot[tags.a(href=download_url)[_("Download")]] |
|
385 |
|
386 |
|
387 registerAdapter(FileUploadDownloadRenderer, FileUploadDownload, |
|
388 formless.iformless.ITypedRenderer) |
|
389 |
|
390 |
|
391 def getDownloadUrl(ctx, argument): |
|
392 if lastKnownConfig is not None: |
|
393 return url.URL.fromContext(ctx).\ |
|
394 child(WAMP_SECRET_URL).\ |
|
395 child(lastKnownConfig["ID"] + ".secret") |
|
396 |
|
397 |
|
398 webFormInterface = [ |
|
399 ("status", |
|
400 annotate.String(label=_("Current status"), |
|
401 immutable=True, |
|
402 default=lambda *k:getWampStatus())), |
|
403 ("ID", |
|
404 annotate.String(label=_("ID"), |
|
405 default=wampConfigDefault)), |
|
406 ("secretfile", |
|
407 FileUploadDownload(label=_("File containing secret for that ID"), |
|
408 download_url=getDownloadUrl)), |
|
409 ("active", |
|
410 annotate.Boolean(label=_("Enable WAMP connection"), |
|
411 default=wampConfigDefault)), |
|
412 ("url", |
|
413 annotate.String(label=_("WAMP Server URL"), |
|
414 default=wampConfigDefault))] |
|
415 |
|
416 |
|
417 def deliverWampSecret(ctx, segments): |
|
418 # filename = segments[1].decode('utf-8') |
|
419 |
|
420 # FIXME: compare filename to ID+".secret" |
|
421 # for now all url under /secret returns the secret |
|
422 |
|
423 # TODO: make beautifull message in case of exception |
|
424 # while loading secret (if empty or dont exist) |
|
425 secret = LoadWampSecret(_WampSecret) |
|
426 return static.Data(secret, 'application/octet-stream'), () |
|
427 |
|
428 |
|
429 def RegisterWebSettings(NS): |
|
430 NS.ConfigurableSettings.addExtension( |
|
431 "wamp", |
|
432 _("Wamp Settings"), |
|
433 webFormInterface, |
|
434 _("Set"), |
|
435 wampConfig) |
|
436 |
|
437 NS.customSettingsURLs[WAMP_SECRET_URL] = deliverWampSecret |