Merge branch 'plugin/ngrok' into 'master'

[#196] Added ngrok integration

Closes #196

See merge request platypush/platypush!4
This commit is contained in:
Fabio Manganiello 2021-09-25 01:34:45 +02:00
commit f856ae69e1
7 changed files with 248 additions and 0 deletions

View file

@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
Given the high speed of development in the first phase, changes are being reported only starting from v0.20.2. Given the high speed of development in the first phase, changes are being reported only starting from v0.20.2.
## [Unreleased]
## Added
- `ngrok` integration (see #196).
## [0.22.1] - 2021-09-22 ## [0.22.1] - 2021-09-22
### Fixed ### Fixed

View file

@ -280,15 +280,18 @@ autodoc_mock_imports = ['googlesamples.assistant.grpc.audio_helpers',
'pysmartthings', 'pysmartthings',
'aiohttp', 'aiohttp',
'watchdog', 'watchdog',
'pyngrok',
] ]
sys.path.insert(0, os.path.abspath('../..')) sys.path.insert(0, os.path.abspath('../..'))
def skip(app, what, name, obj, skip, options): def skip(app, what, name, obj, skip, options):
if name == "__init__": if name == "__init__":
return False return False
return skip return skip
def setup(app): def setup(app):
app.connect("autodoc-skip-member", skip) app.connect("autodoc-skip-member", skip)

View file

@ -0,0 +1,41 @@
from platypush.message.event import Event
class NgrokEvent(Event):
"""
``ngrok`` base event.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class NgrokProcessStartedEvent(Event):
"""
Event triggered when the ``ngrok`` process is started.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class NgrokTunnelStartedEvent(Event):
"""
Event triggered when a tunnel is started.
"""
def __init__(self, *args, name: str, url: str, protocol: str, **kwargs):
super().__init__(*args, name=name, url=url, protocol=protocol, **kwargs)
class NgrokTunnelStoppedEvent(Event):
"""
Event triggered when a tunnel is stopped.
"""
def __init__(self, *args, name: str, url: str, protocol: str, **kwargs):
super().__init__(*args, name=name, url=url, protocol=protocol, **kwargs)
class NgrokProcessStoppedEvent(Event):
"""
Event triggered when the ngrok process is stopped.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

View file

@ -0,0 +1,158 @@
import os
from typing import Optional, Union, Callable
from platypush.context import get_bus
from platypush.message.event.ngrok import NgrokProcessStartedEvent, NgrokTunnelStartedEvent, NgrokTunnelStoppedEvent, \
NgrokProcessStoppedEvent
from platypush.plugins import Plugin, action
from platypush.schemas.ngrok import NgrokTunnelSchema
class NgrokPlugin(Plugin):
"""
Plugin to dynamically create and manage network tunnels using `ngrok <https://ngrok.com/>`_.
Requires:
* **pyngrok** (``pip install pyngrok``)
Triggers:
* :class:`platypush.message.event.ngrok.NgrokProcessStartedEvent` when the ``ngrok`` process is started.
* :class:`platypush.message.event.ngrok.NgrokProcessStoppedEvent` when the ``ngrok`` process is stopped.
* :class:`platypush.message.event.ngrok.NgrokTunnelStartedEvent` when a tunnel is started.
* :class:`platypush.message.event.ngrok.NgrokTunnelStoppedEvent` when a tunnel is stopped.
"""
def __init__(self, auth_token: Optional[str] = None, ngrok_bin: Optional[str] = None, region: Optional[str] = None,
**kwargs):
"""
:param auth_token: Specify the ``ngrok`` auth token, enabling authenticated features (e.g. more concurrent
tunnels, custom subdomains, etc.).
:param ngrok_bin: By default ``pyngrok`` manages its own version of the ``ngrok`` binary, but you can specify
this option if you want to use a different binary installed on the system.
:param region: ISO code of the region/country that should host the ``ngrok`` tunnel (default: ``us``).
"""
from pyngrok import conf, ngrok
super().__init__(**kwargs)
conf.get_default().log_event_callback = self._get_event_callback()
self._active_tunnels_by_url = {}
if auth_token:
ngrok.set_auth_token(auth_token)
if ngrok_bin:
conf.get_default().ngrok_path = os.path.expanduser(ngrok_bin)
if region:
conf.get_default().region = region
@property
def _active_tunnels_by_name(self) -> dict:
return {
tunnel['name']: tunnel
for tunnel in self._active_tunnels_by_url.values()
}
def _get_event_callback(self) -> Callable:
from pyngrok.process import NgrokLog
def callback(log: NgrokLog):
if log.msg == 'client session established':
get_bus().post(NgrokProcessStartedEvent())
elif log.msg == 'started tunnel':
# noinspection PyUnresolvedReferences
tunnel = dict(
name=log.name,
url=log.url,
protocol=log.url.split(':')[0]
)
self._active_tunnels_by_url[tunnel['url']] = tunnel
get_bus().post(NgrokTunnelStartedEvent(**tunnel))
elif (
log.msg == 'end' and
int(getattr(log, 'status', 0)) == 204 and
getattr(log, 'pg', '').startswith('/api/tunnels')
):
# noinspection PyUnresolvedReferences
tunnel = log.pg.split('/')[-1]
tunnel = self._active_tunnels_by_name.pop(tunnel, self._active_tunnels_by_url.pop(tunnel, None))
if tunnel:
get_bus().post(NgrokTunnelStoppedEvent(**tunnel))
elif log.msg == 'received stop request':
get_bus().post(NgrokProcessStoppedEvent())
return callback
@action
def create_tunnel(self, resource: Union[int, str] = 80, protocol: str = 'tcp',
name: Optional[str] = None, auth: Optional[str] = None, **kwargs) -> dict:
"""
Create an ``ngrok`` tunnel to the specified localhost port/protocol.
:param resource: This can be any of the following:
- A TCP or UDP port exposed on localhost.
- A local network address (or ``address:port``) to expose.
- The absolute path (starting with ``file://``) to a local folder - in such case, the specified directory
will be served over HTTP through an ``ngrok`` endpoint (see https://ngrok.com/docs#http-file-urls).
Default: localhost port 80.
:param protocol: Network protocol (default: ``tcp``).
:param name: Optional tunnel name.
:param auth: HTTP basic authentication credentials associated with the tunnel, in the format of
``username:password``.
:param kwargs: Extra arguments supported by the ``ngrok`` tunnel, such as ``hostname``, ``subdomain`` or
``remote_addr`` - see the `ngrok documentation <https://ngrok.com/docs#tunnel-definitions>`_ for a full
list.
:return: .. schema:: ngrok.NgrokTunnelSchema
"""
from pyngrok import ngrok
if isinstance(resource, str) and resource.startswith('file://'):
protocol = None
tunnel = ngrok.connect(resource, proto=protocol, name=name, auth=auth, **kwargs)
return NgrokTunnelSchema().dump(tunnel)
@action
def close_tunnel(self, tunnel: str):
"""
Close an ``ngrok`` tunnel.
:param tunnel: Name or public URL of the tunnel to be closed.
"""
from pyngrok import ngrok
if tunnel in self._active_tunnels_by_name:
tunnel = self._active_tunnels_by_name[tunnel]['url']
assert tunnel in self._active_tunnels_by_url, f'No such tunnel URL or name: {tunnel}'
ngrok.disconnect(tunnel)
@action
def get_tunnels(self):
"""
Get the list of active ``ngrok`` tunnels.
:return: .. schema:: ngrok.NgrokTunnelSchema(many=True)
"""
from pyngrok import ngrok
tunnels = ngrok.get_tunnels()
return NgrokTunnelSchema().dump(tunnels, many=True)
@action
def kill_process(self):
"""
The first created tunnel instance also starts the ``ngrok`` process.
The process will stay alive until the Python interpreter is stopped or this action is invoked.
"""
from pyngrok import ngrok
proc = ngrok.get_ngrok_process()
assert proc and proc.proc, 'The ngrok process is not running'
proc.proc.kill()
get_bus().post(NgrokProcessStoppedEvent())
# vim:sw=4:ts=4:et:

View file

@ -0,0 +1,7 @@
manifest:
events: {}
install:
pip:
- pyngrok
package: platypush.plugins.ngrok
type: plugin

View file

@ -0,0 +1,31 @@
from marshmallow import fields
from marshmallow.schema import Schema
from marshmallow.validate import OneOf
class NgrokTunnelSchema(Schema):
name = fields.String(
metadata=dict(
description='Tunnel friendly name or auto-generated name',
example='tcp-8080-my-tunnel',
)
)
protocol = fields.String(
allow_none=False,
attribute='proto',
validate=OneOf(['tcp', 'udp', 'http']),
metadata=dict(
description='Tunnel protocol',
example='tcp',
),
)
url = fields.String(
attribute='public_url',
required=True,
metadata=dict(
description='Public URL to the ngrok tunnel',
example='tcp://8.tcp.ngrok.io:12345',
)
)

View file

@ -250,5 +250,7 @@ setup(
'filemonitor': ['watchdog'], 'filemonitor': ['watchdog'],
# Support for Adafruit PCA9685 PWM controller # Support for Adafruit PCA9685 PWM controller
'pca9685': ['adafruit-python-shell', 'adafruit-circuitpython-pca9685'], 'pca9685': ['adafruit-python-shell', 'adafruit-circuitpython-pca9685'],
# Support for ngrok integration
'ngrok': ['pyngrok'],
}, },
) )