forked from platypush/platypush
Merge branch 'plugin/ngrok' into 'master'
[#196] Added ngrok integration Closes #196 See merge request platypush/platypush!4
This commit is contained in:
commit
f856ae69e1
7 changed files with 248 additions and 0 deletions
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
41
platypush/message/event/ngrok.py
Normal file
41
platypush/message/event/ngrok.py
Normal 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)
|
158
platypush/plugins/ngrok/__init__.py
Normal file
158
platypush/plugins/ngrok/__init__.py
Normal 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:
|
7
platypush/plugins/ngrok/manifest.yaml
Normal file
7
platypush/plugins/ngrok/manifest.yaml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
manifest:
|
||||||
|
events: {}
|
||||||
|
install:
|
||||||
|
pip:
|
||||||
|
- pyngrok
|
||||||
|
package: platypush.plugins.ngrok
|
||||||
|
type: plugin
|
31
platypush/schemas/ngrok.py
Normal file
31
platypush/schemas/ngrok.py
Normal 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',
|
||||||
|
)
|
||||||
|
)
|
2
setup.py
2
setup.py
|
@ -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'],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue