diff --git a/README.md b/README.md index bcceeb7..82bc044 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,8 @@ import i2plib loop = asyncio.get_event_loop() # making your local web server available in the I2P network -asyncio.ensure_future(i2plib.server_tunnel(("127.0.0.1", 80))) +tunnel = i2plib.ServerTunnel(("127.0.0.1", 80)) +asyncio.ensure_future(tunnel.run()) try: loop.run_forever() @@ -110,7 +111,8 @@ import i2plib loop = asyncio.get_event_loop() # bind irc.echelon.i2p to 127.0.0.1:6669 -asyncio.ensure_future(i2plib.client_tunnel(("127.0.0.1", 6669), "irc.echelon.i2p")) +tunnel = i2plib.ClientTunnel("irc.echelon.i2p", ("127.0.0.1", 6669)) +asyncio.ensure_future(tunnel.run()) try: loop.run_forever() diff --git a/docs/api.rst b/docs/api.rst index 9192ab7..b52fa8c 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -33,14 +33,16 @@ Utilities Tunnel API ---------- -Tunnel API is the quickest way to use regular programms inside I2P. -Client tunnel binds a remote I2P destination to a port on your local machine. -Server tunnel exposes a port on your local machine to the I2P network. +Tunnel API is the quickest way to use regular software inside I2P. +Client tunnel binds a remote I2P destination to a local address. +Server tunnel exposes a local address to the I2P network. -.. autofunction:: client_tunnel -.. autofunction:: server_tunnel .. autoclass:: i2plib.tunnel.I2PTunnel :members: +.. autoclass:: i2plib.ClientTunnel + :inherited-members: +.. autoclass:: i2plib.ServerTunnel + :inherited-members: Data structures --------------- diff --git a/docs/examples/http_server.py b/docs/examples/http_server.py index ecbd8a8..000a6ee 100644 --- a/docs/examples/http_server.py +++ b/docs/examples/http_server.py @@ -39,8 +39,10 @@ def main(args): http_server_thread.start() loop = asyncio.get_event_loop() - asyncio.ensure_future(i2plib.server_tunnel(server_address, - loop=loop, private_key=priv.base64, sam_address=sam_address), loop=loop) + + tunnel = i2plib.ServerTunnel(server_address, + loop=loop, private_key=priv, sam_address=sam_address) + asyncio.ensure_future(tunnel.run(), loop=loop) try: loop.run_forever() diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 45b9c62..46ca581 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -92,7 +92,8 @@ Expose a local service to I2P like that: loop = asyncio.get_event_loop() # making your local web server available in the I2P network - asyncio.ensure_future(i2plib.server_tunnel(("127.0.0.1", 80))) + tunnel = i2plib.ServerTunnel(("127.0.0.1", 80)) + asyncio.ensure_future(tunnel.run()) try: loop.run_forever() @@ -113,7 +114,8 @@ Bind a remote I2P destination to a port on your local host: loop = asyncio.get_event_loop() # bind irc.echelon.i2p to 127.0.0.1:6669 - asyncio.ensure_future(i2plib.client_tunnel(("127.0.0.1", 6669), "irc.echelon.i2p")) + tunnel = i2plib.ClientTunnel("irc.echelon.i2p", ("127.0.0.1", 6669)) + asyncio.ensure_future(tunnel.run()) try: loop.run_forever() diff --git a/i2plib/__init__.py b/i2plib/__init__.py index 14706f4..bb4c36c 100644 --- a/i2plib/__init__.py +++ b/i2plib/__init__.py @@ -14,7 +14,7 @@ from .aiosam import ( create_session, stream_connect, stream_accept ) -from .tunnel import client_tunnel, server_tunnel +from .tunnel import ClientTunnel, ServerTunnel from .utils import get_sam_address diff --git a/i2plib/__version__.py b/i2plib/__version__.py index 72d425a..b638dcf 100644 --- a/i2plib/__version__.py +++ b/i2plib/__version__.py @@ -1,7 +1,7 @@ __title__ = 'i2plib' __description__ = 'A modern asynchronous library for building I2P applications.' __url__ = 'https://github.com/l-n-s/i2plib' -__version__ = '0.0.5' +__version__ = '0.0.6' __author__ = 'Viktor Villainov' __author_email__ = 'supervillain@riseup.net' __license__ = 'MIT' diff --git a/i2plib/tunnel.py b/i2plib/tunnel.py index cccf79d..c49fbdc 100644 --- a/i2plib/tunnel.py +++ b/i2plib/tunnel.py @@ -8,26 +8,6 @@ import i2plib.utils BUFFER_SIZE = 65536 -class I2PTunnel(object): - """I2P Tunnel object - - This object is returned by tunnel creation coroutines - - :param name: Tunnel session name - :param session_writer: Session socket writer instance - :param future: Tunnel future - """ - - def __init__(self, name, session_writer, future): - self.name = name - self.session_writer = session_writer - self.future = future - - def stop(self): - """Stop the tunnel""" - self.session_writer.close() - self.future.cancel() - async def proxy_data(reader, writer): """Proxy data from reader to writer""" try: @@ -45,104 +25,128 @@ async def proxy_data(reader, writer): pass logging.debug('close connection') -async def client_tunnel(local_address, remote_destination, loop=None, - private_key=None, session_name=None, - sam_address=i2plib.sam.DEFAULT_ADDRESS): - """Run a client tunnel in the event loop. +class I2PTunnel(object): + """Base I2P Tunnel object, not to be used directly + + :param local_address: A local address to use for a tunnel. + E.g. ("127.0.0.1", 6668) + :param private_key: (optional) Private key to use in this session. Can be + a base64 encoded string, i2plib.sam.PrivateKey instance + or None. A new key is created when it is None. + :param session_name: (optional) Session nick name. A new session nickname is + generated if not specified. + :param options: (optional) A dict object with i2cp options + :param loop: (optional) Event loop instance + :param sam_address: (optional) SAM API address + """ + + def __init__(self, local_address, private_key=None, session_name=None, + options={}, loop=None, sam_address=i2plib.sam.DEFAULT_ADDRESS): + self.local_address = local_address + self.private_key = private_key + self.session_name = session_name or i2plib.sam.generate_session_id() + self.options = options + self.loop = loop + self.sam_address = sam_address + + async def _pre_run(self): + if not self.private_key: + self.private_key = await i2plib.new_private_key( + sam_address=self.sam_address, loop=self.loop) + _, self.session_writer = await i2plib.aiosam.create_session( + self.session_name, style=self.style, options=self.options, + sam_address=self.sam_address, + loop=self.loop, private_key=self.private_key) + + def stop(self): + """Stop the tunnel""" + self.session_writer.close() + self.future.cancel() + +class ClientTunnel(I2PTunnel): + """Client tunnel, a subclass of i2plib.tunnel.I2PTunnel If you run a client tunnel with a local address ("127.0.0.1", 6668) and a remote destination "irc.echelon.i2p", all connections to 127.0.0.1:6668 will be proxied to irc.echelon.i2p. - :param local_address: A local address to bind a remote destination to. E.g. - ("127.0.0.1", 6668) :param remote_destination: Remote I2P destination, can be either .i2p domain, .b32.i2p address, base64 destination or i2plib.Destination instance - :param session_name: (optional) Session nick name. A new session nickname is - generated if not specified. - :param private_key: (optional) Private key to use in this session. Can be - a base64 encoded string, i2plib.sam.PrivateKey instance - or None. TRANSIENT destination is used when it is None. - :param sam_address: (optional) SAM API address - :param loop: (optional) Event loop instance - :return: an instance of i2plib.tunnel.I2PTunnel """ - session_name = session_name or i2plib.sam.generate_session_id() - reader, writer = await i2plib.aiosam.create_session(session_name, - style="STREAM", sam_address=sam_address, loop=loop, - private_key=private_key) - async def handle_client(client_reader, client_writer): - """Handle local client connection""" - remote_reader, remote_writer = await i2plib.aiosam.stream_connect( - session_name, remote_destination, sam_address=sam_address, - loop=loop) - asyncio.ensure_future(proxy_data(remote_reader, client_writer), - loop=loop) - asyncio.ensure_future(proxy_data(client_reader, remote_writer), - loop=loop) + def __init__(self, remote_destination, *args, **kwargs): + super().__init__(*args, **kwargs) + self.style = "STREAM" + self.remote_destination = remote_destination - tunnel_future = asyncio.ensure_future( - asyncio.start_server(handle_client, *local_address, loop=loop), - loop=loop) - return I2PTunnel(session_name, writer, tunnel_future) + async def run(self): + """A coroutine used to run the tunnel""" + await self._pre_run() -async def server_tunnel(local_address, loop=None, private_key=None, - session_name=None, sam_address=i2plib.sam.DEFAULT_ADDRESS): - """Run a server tunnel in the event loop. + async def handle_client(client_reader, client_writer): + """Handle local client connection""" + remote_reader, remote_writer = await i2plib.aiosam.stream_connect( + self.session_name, self.remote_destination, + sam_address=self.sam_address, loop=self.loop) + asyncio.ensure_future(proxy_data(remote_reader, client_writer), + loop=self.loop) + asyncio.ensure_future(proxy_data(client_reader, remote_writer), + loop=self.loop) + + self.future = asyncio.ensure_future( + asyncio.start_server(handle_client, *self.local_address, + loop=self.loop), + loop=self.loop) + +class ServerTunnel(I2PTunnel): + """Server tunnel, a subclass of i2plib.tunnel.I2PTunnel If you want to expose a local service 127.0.0.1:80 to the I2P network, run a server tunnel with a local address ("127.0.0.1", 80). If you don't provide a private key or a session name, it will use a TRANSIENT destination. - - :param local_address: A local address to bind a remote destination to. E.g. - ("127.0.0.1", 6668) - :param session_name: (optional) Session nick name. A new session nickname is - generated if not specified. - :param private_key: (optional) Private key to use in this session. Can be - a base64 encoded string, i2plib.sam.PrivateKey instance - or None. TRANSIENT destination is used when it is None. - :param sam_address: (optional) SAM API address - :param loop: (optional) Event loop instance - :return: an instance of i2plib.tunnel.I2PTunnel """ - session_name = session_name or i2plib.sam.generate_session_id() - reader, writer = await i2plib.aiosam.create_session(session_name, - style="STREAM", sam_address=sam_address, loop=loop, - private_key=private_key) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.style = "STREAM" - async def handle_client(incoming, client_reader, client_writer): - # data and dest may come in one chunk - dest, data = incoming.split(b"\n", 1) - remote_destination = i2plib.sam.Destination(dest.decode()) - logging.debug("{} client connected: {}.b32.i2p".format(session_name, - remote_destination.base32)) + async def run(self): + """A coroutine used to run the tunnel""" + await self._pre_run() - try: - remote_reader, remote_writer = await asyncio.wait_for( - asyncio.open_connection( - host=local_address[0], port=local_address[1], loop=loop), - timeout=5, loop=loop) - if data: remote_writer.write(data) - asyncio.ensure_future(proxy_data(remote_reader, client_writer), - loop=loop) - asyncio.ensure_future(proxy_data(client_reader, remote_writer), - loop=loop) - except ConnectionRefusedError: - client_writer.close() + async def handle_client(incoming, client_reader, client_writer): + # data and dest may come in one chunk + dest, data = incoming.split(b"\n", 1) + remote_destination = i2plib.sam.Destination(dest.decode()) + logging.debug("{} client connected: {}.b32.i2p".format( + self.session_name, remote_destination.base32)) - async def server_loop(): - while True: - client_reader, client_writer = await i2plib.aiosam.stream_accept( - session_name, sam_address=sam_address, loop=loop) - incoming = await client_reader.read(BUFFER_SIZE) - asyncio.ensure_future(handle_client( - incoming, client_reader, client_writer), loop=loop) + try: + remote_reader, remote_writer = await asyncio.wait_for( + asyncio.open_connection( + host=self.local_address[0], + port=self.local_address[1], loop=self.loop), + timeout=5, loop=self.loop) + if data: remote_writer.write(data) + asyncio.ensure_future(proxy_data(remote_reader, client_writer), + loop=self.loop) + asyncio.ensure_future(proxy_data(client_reader, remote_writer), + loop=self.loop) + except ConnectionRefusedError: + client_writer.close() + + async def server_loop(): + while True: + client_reader, client_writer = await i2plib.aiosam.stream_accept( + self.session_name, sam_address=self.sam_address, + loop=self.loop) + incoming = await client_reader.read(BUFFER_SIZE) + asyncio.ensure_future(handle_client( + incoming, client_reader, client_writer), loop=self.loop) + + self.future = asyncio.ensure_future(server_loop(), loop=self.loop) - tunnel_future = asyncio.ensure_future(server_loop(), loop=loop) - return I2PTunnel(session_name, writer, tunnel_future) if __name__ == '__main__': parser = argparse.ArgumentParser() @@ -171,18 +175,19 @@ if __name__ == '__main__': local_address = i2plib.utils.address_from_string(args.address) - if args.type == 'client': - asyncio.ensure_future(client_tunnel(local_address, - args.destination, loop=loop, private_key=private_key, - sam_address=SAM_ADDRESS), loop=loop) - elif args.type == 'server': - asyncio.ensure_future(server_tunnel(local_address, loop=loop, - private_key=private_key, sam_address=SAM_ADDRESS), loop=loop) + if args.type == "client": + tunnel = ClientTunnel(args.destination, local_address, loop=loop, + private_key=private_key, sam_address=SAM_ADDRESS) + elif args.type == "server": + tunnel = ServerTunnel(local_address, loop=loop, private_key=private_key, + sam_address=SAM_ADDRESS) + + asyncio.ensure_future(tunnel.run(), loop=loop) try: loop.run_forever() except KeyboardInterrupt: - pass + tunnel.stop() finally: loop.stop() loop.close()