Skip to content
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
370 lines (326 sloc) 12.4 KB
"""Register with PNP server and wait for remote peers to connect."""
# import argparse
import asyncio
import logging
import sys
import json
import aiohttp
from typing import Any
import coloredlogs
from pathlib import Path
# from aiortc import RTCIceCandidate, RTCSessionDescription
from peerjs.peer import Peer, PeerOptions
from peerjs.peerroom import PeerRoom
from peerjs.util import util
from peerjs.enums import ConnectionEventType, PeerEventType
log = logging.getLogger(__name__)
LOG_LEVEL = logging.INFO
peer = None
savedPeerId = None
# persisted config dict
config = {}
CONFIG_FILE = '.peerjsrc'
AMBIANIC_PNP_HOST = '' # 'localhost'
AMBIANIC_PNP_PORT = 443 # 9779
time_start = None
peerConnectionStatus = None
discoveryLoop = None
# aiohttp session reusable throghout the http proxy lifecycle
http_session = None
# flags when user requests shutdown
# via CTRL+C or another system signal
_is_shutting_down: bool = False
# async def _consume_signaling(pc, signaling):
# while True:
# obj = await signaling.receive()
# if isinstance(obj, RTCSessionDescription):
# await pc.setRemoteDescription(obj)
# if obj.type == "offer":
# # send answer
# await pc.setLocalDescription(await pc.createAnswer())
# await signaling.send(pc.localDescription)
# elif isinstance(obj, RTCIceCandidate):
# pc.addIceCandidate(obj)
# elif obj is None:
# print("Exiting")
# break
async def join_peer_room(peer=None):
"""Join a peer room with other local peers."""
# first try to find the remote peer ID in the same room
myRoom = PeerRoom(peer)
log.debug('Fetching room members...')
peerIds = await myRoom.getRoomMembers()'myRoom members %r', peerIds)
def _savePeerId(peerId=None):
assert peerId
global savedPeerId
savedPeerId = peerId
config['peerId'] = peerId
with open(CONFIG_FILE, 'w') as outfile:
json.dump(config, outfile)
def _loadConfig():
global config
global savedPeerId
conf_file = Path(CONFIG_FILE)
if conf_file.exists():
with as infile:
config = json.load(infile)
savedPeerId = config.get('peerId', None)
def _setPnPServiceConnectionHandlers(peer=None):
assert peer
global savedPeerId
async def peer_open(id):'Peer signaling connection open.')
global savedPeerId
# Workaround for peer.reconnect deleting previous id
if is None:'pnpService: Received null id from peer open') = savedPeerId
if savedPeerId !=
'PNP Service returned new peerId. Old %s, New %s',
_savePeerId('savedPeerId: %s',
async def peer_disconnected(peerId):
global savedPeerId'Peer %s disconnected from server.', peerId)
# Workaround for peer.reconnect deleting previous id
if not
log.debug('BUG WORKAROUND: Peer lost ID. '
'Resetting to last known ID.')
peer._id = savedPeerId
peer._lastServerId = savedPeerId
global _is_shutting_down
if not _is_shutting_down:
await peer.reconnect()
def peer_close():
# peerConnection = null'Peer connection closed')
def peer_error(err):
log.exception('Peer error %s', err)
log.warning('peerConnectionStatus %s', peerConnectionStatus)
# retry peer connection in a few seconds
# loop = asyncio.get_event_loop()
# loop.call_later(3, pnp_service_connect)
# remote peer tries to initiate connection
async def peer_connection(peerConnection):'Remote peer trying to establish connection')
async def _fetch(url: str = None, method: str = 'GET') -> Any:
global http_session
if method == 'GET':
async with http_session.get(url) as response:
content = await
# response_content = {'name': 'Ambianic-Edge', 'version': '1.24.2020'}
# rjson = json.dumps(response_content)
return response, content
raise NotImplementedError(
f'HTTP method ${method} not implemented.'
' Contributions welcome!')
async def _pong(peer_connection=None):
response_header = {
'status': 200,
header_as_json = json.dumps(response_header)
log.debug('sending keepalive pong back to remote peer')
await peer_connection.send(header_as_json)
await peer_connection.send('pong')
async def _ping(peer_connection=None, stop_flag=None):
while not stop_flag.is_set():
# send HTTP 202 Accepted status code to inform
# client that we are still waiting on the http
# server to complete its response
ping_as_json = json.dumps({'status': 202})
await peer_connection.send(ping_as_json)'webrtc peer: http proxy response ping. '
'Keeping datachannel alive.')
await asyncio.sleep(1)
def _setPeerConnectionHandlers(peerConnection):
async def pc_open():'Connected to: %s', peerConnection.peer)
# Handle incoming data (messages only since this is the signal sender)
async def pc_data(data):
log.debug('data received from remote peer \n%r', data)
request = json.loads(data)
# check if the request is just a keepalive ping
if (request['url'].startswith('ping')):
log.debug('received keepalive ping from remote peer')
await _pong(peer_connection=peerConnection)
return'webrtc peer: http proxy request: \n%r', request)
# schedule frequent pings while waiting on response_header
# to keep the peer data channel open
waiting_on_fetch = asyncio.Event()
response = None
response, content = await _fetch(**request)
except Exception as e:
log.exception('Error %s while fetching response'
' with request: \n %r',
e, request)
# fetch completed, cancel pings
if not response:
response_header = {
# internal server error code
'status': 500
response_content = None
response_content = content
response_header = {
'status': response.status,
'content-type': response.headers['content-type'],
'content-length': len(response_content)
}'Proxy fetched response with headers: \n%r', response.headers)'Answering request: \n%r '
'response header: \n %r',
request, response_header)
header_as_json = json.dumps(response_header)
await peerConnection.send(header_as_json)
await peerConnection.send(response_content)
async def pc_close():'Connection to remote peer closed')
async def pnp_service_connect() -> Peer:
"""Create a Peer instance and register with PnP signaling server."""
# Create own peer object with connection to shared PeerJS server'creating peer')
# If we already have an assigned peerId, we will reuse it forever.
# We expect that peerId is crypto secure. No need to replace.
# Unless the user explicitly requests a refresh.
global savedPeerId'last saved savedPeerId %s', savedPeerId)
new_token = util.randomToken()'Peer session token %s', new_token)
options = PeerOptions(
peer = Peer(id=savedPeerId, peer_options=options)'pnpService: peer created with id %s , options: %r',,
await peer.start()'peer activated')
return peer
async def make_discoverable(peer=None):
"""Enable remote peers to find and connect to this peer."""
log.debug('Enter peer discoverable.')
log.debug('Before _is_shutting_down')
global _is_shutting_down
log.debug('Making peer discoverable.')
while not _is_shutting_down:
log.debug('Discovery loop.')
log.debug('peer status: %r', peer)
if not peer or peer.destroyed:'Peer destroyed. Will create a new peer.')
peer = await pnp_service_connect()
await join_peer_room(peer=peer)
elif peer.disconnected:'Peer disconnected. Will try to reconnect.')
await peer.reconnect()
else:'Peer still establishing connection. %r', peer)
except Exception as e:
log.exception('Error while trying to join local peer room. '
'Will retry in a few moments. '
'Error: \n%r', e)
if peer and not peer.destroyed:
# something is not right with the connection to the server
# lets start a fresh peer connection'Peer connection was corrupted. Detroying peer.')
await peer.destroy()
peer = None
log.debug('peer status after destroy: %r', peer)
await asyncio.sleep(3)
def _config_logger():
root_logger = logging.getLogger()
format_cfg = '%(asctime)s %(levelname)-4s ' \
'%(pathname)s.%(funcName)s(%(lineno)d): %(message)s'
datefmt_cfg = '%Y-%m-%d %H:%M:%S'
fmt = logging.Formatter(fmt=format_cfg,
datefmt=datefmt_cfg, style='%')
ch = logging.StreamHandler(sys.stdout)
root_logger.handlers = []
coloredlogs.install(level=LOG_LEVEL, fmt=format_cfg)
async def _start():
global http_session
http_session = aiohttp.ClientSession()
global peer'Calling make_discoverable')
await make_discoverable(peer=peer)'Exited make_discoverable')
async def _shutdown():
global _is_shutting_down
_is_shutting_down = True
global peer
log.debug('Shutting down. Peer %r', peer)
if peer:'Destroying peer %r', peer)
await peer.destroy()
else:'Peer is None')
# loop.run_until_complete(pc.close())
# loop.run_until_complete(signaling.close())
global http_session
await http_session.close()
if __name__ == "__main__":
# args = None
# parser = argparse.ArgumentParser(description="Data channels ping/pong")
# parser.add_argument("role", choices=["offer", "answer"])
# parser.add_argument("--verbose", "-v", action="count")
# add_signaling_arguments(parser)
# args = parser.parse_args()
# if args.verbose:
# add formatter to ch
log.debug('Log level set to debug')
# signaling = create_signaling(args)
# signaling = AmbianicPnpSignaling(args)
# pc = RTCPeerConnection()
# if args.role == "offer":
# coro = _run_offer(pc, signaling)
# else:
# coro = _run_answer(pc, signaling)
# run event loop
loop = asyncio.get_event_loop()
try:'\n>>>>> Starting http-proxy over webrtc. <<<<')
except KeyboardInterrupt:'KeyboardInterrupt detected.')
finally:'Shutting down...')
loop.close()'All done.')
You can’t perform that action at this time.