├── nclib ├── py.typed ├── errors.py ├── __init__.py ├── select.py ├── server.py ├── process.py ├── logger.py ├── simplesock.py └── netcat.py ├── MANIFEST.in ├── .gitignore ├── docs ├── server.rst ├── process.rst ├── logger.rst ├── netcat.rst ├── Makefile ├── simplesock.rst ├── serve-stdio.rst ├── index.rst └── conf.py ├── .readthedocs.yaml ├── LICENSE ├── README.rst ├── setup.py └── serve-stdio /nclib/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include py.typed 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | dist 3 | build 4 | *.egg-info 5 | _build 6 | -------------------------------------------------------------------------------- /nclib/errors.py: -------------------------------------------------------------------------------- 1 | import socket 2 | 3 | class NetcatError(Exception): 4 | pass 5 | 6 | class NetcatTimeout(NetcatError, socket.timeout): 7 | pass 8 | 9 | class NetcatEOF(NetcatError): 10 | pass 11 | -------------------------------------------------------------------------------- /docs/server.rst: -------------------------------------------------------------------------------- 1 | Servers 2 | ======= 3 | 4 | .. automodule:: nclib.server 5 | 6 | .. autoclass:: nclib.server.TCPServer 7 | :members: __init__, close 8 | 9 | .. autoclass:: nclib.server.UDPServer 10 | :members: __init__, respond, close 11 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | # Set the version of Python and other tools you might need 4 | build: 5 | os: ubuntu-22.04 6 | tools: 7 | python: "3.8" 8 | 9 | # Build documentation in the docs/ directory with Sphinx 10 | sphinx: 11 | configuration: docs/conf.py 12 | -------------------------------------------------------------------------------- /docs/process.rst: -------------------------------------------------------------------------------- 1 | Launching and Controlling Processes 2 | =================================== 3 | 4 | .. automodule:: nclib.process 5 | 6 | .. autoclass:: nclib.process.Process 7 | :members: __init__, poll, wait, send_signal, kill, launch 8 | 9 | .. autoclass:: nclib.process.GDBProcess 10 | :members: __init__ 11 | -------------------------------------------------------------------------------- /nclib/__init__.py: -------------------------------------------------------------------------------- 1 | if bytes is str: 2 | raise Exception("nclib is python 3 only now :(") 3 | 4 | from .netcat import Netcat, ferry, merge 5 | from .select import select 6 | from .server import TCPServer, UDPServer 7 | from .process import Process, GDBProcess 8 | from .errors import NetcatError, NetcatTimeout, NetcatEOF 9 | from . import simplesock 10 | 11 | __all__ = ('Netcat', 'ferry', 'merge', 'select', 'TCPServer', 'UDPServer', 'Process', 'GDBProcess', 'simplesock', 'NetcatError', 'NetcatTimeout', 'NetcatEOF') 12 | -------------------------------------------------------------------------------- /docs/logger.rst: -------------------------------------------------------------------------------- 1 | Logging Facilities 2 | ================== 3 | 4 | .. automodule:: nclib.logger 5 | 6 | Netcat objects can be instrumented by providing a Logger object to its constructor. 7 | The job of a Logger is to receive events provided by the Netcat (for example, "we are sending data" or "we got an EOF") and do something with them. 8 | 9 | .. autoclass:: nclib.logger.Logger 10 | :members: 11 | 12 | .. autoclass:: nclib.logger.StandardLogger 13 | .. autoclass:: nclib.logger.TeeLogger 14 | .. autoclass:: nclib.logger.ManyLogger 15 | -------------------------------------------------------------------------------- /docs/netcat.rst: -------------------------------------------------------------------------------- 1 | Basic socket interfaces 2 | ======================= 3 | 4 | .. automodule:: nclib.netcat 5 | 6 | .. autoclass:: nclib.netcat.Netcat 7 | :members: __init__, send, send_line, recv, recv_until, recv_all, recv_exactly, interact, close, closed, shutdown, shutdown_rd, shutdown_wr, fileno, settimeout, gettimeout 8 | 9 | .. autofunction:: nclib.select.select 10 | .. autofunction:: nclib.netcat.merge 11 | .. autofunction:: nclib.netcat.ferry 12 | 13 | 14 | .. automodule:: nclib.errors 15 | 16 | .. autoclass:: nclib.errors.NetcatError 17 | .. autoclass:: nclib.errors.NetcatEOF 18 | .. autoclass:: nclib.errors.NetcatTimeout 19 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = nclib 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/simplesock.rst: -------------------------------------------------------------------------------- 1 | Low-level socket abstraction layer 2 | ================================== 3 | 4 | .. automodule:: nclib.simplesock 5 | 6 | Different types of stream-like classes (sockets, files, pipes, ...) have very different interfaces and behaviors under exceptional conditions. 7 | The goal of this "simple socket" module is to provide a unified interface for a variety of stream types. 8 | The Netcat class then uses this interface to provide all the convenient functionality you love. 9 | 10 | All Netcat methods should automatically wrap any stream objects you provide with the appropriate wrapper. 11 | 12 | .. autoclass:: nclib.simplesock.Simple 13 | :members: 14 | 15 | .. autofunction:: nclib.simplesock.wrap 16 | 17 | .. autoclass:: nclib.simplesock.SimpleSocket 18 | .. autoclass:: nclib.simplesock.SimpleFile 19 | .. autoclass:: nclib.simplesock.SimpleDuplex 20 | .. autoclass:: nclib.simplesock.SimpleMerge 21 | .. autoclass:: nclib.simplesock.SimpleNetcat 22 | .. autoclass:: nclib.simplesock.SimpleLogger 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Audrey Dutcher 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | nclib 2 | ===== 3 | 4 | nclib is a python socket library that wants to be your friend. 5 | 6 | nclib provides: 7 | 8 | - Easy-to-use interfaces for connecting to and listening on TCP and UDP sockets 9 | - The ability to handle any python stream-like object with a single interface 10 | - A better socket class, the Netcat object 11 | 12 | - Convenient receive methods for common socket usage patterns 13 | - Highly customizable logging 14 | - Interactive mode, connecting the socket to your stdin/stdout 15 | - Intelligent detection of socket closes and connection drops 16 | - Long-running functions cleanly abortable with ctrl-c 17 | - Lots of aliases in case you forget the right method name 18 | 19 | - Mechanisms to launch processes with their in/out streams connected to sockets 20 | 21 | - Launch a process with gdb attached 22 | 23 | - TCP and UDP server classes for writing simple python daemons 24 | - A script to easily daemonize command-line programs 25 | 26 | If you are familiar with pwntools, nclib provides much of the functionaly that 27 | pwntools' socket wrappers do, but with the bonus feature of not being pwntools. 28 | 29 | Installation 30 | ------------ 31 | 32 | .. code-block:: bash 33 | 34 | pip install nclib 35 | 36 | Documentation 37 | ------------- 38 | 39 | https://nclib.readthedocs.io/ 40 | -------------------------------------------------------------------------------- /docs/serve-stdio.rst: -------------------------------------------------------------------------------- 1 | Daemonizing Command Line Programs 2 | ================================= 3 | 4 | nclib ships with a utility script called ``serve-stdio`` that can turn any program operating over stdin and stdout into a network service. 5 | This is a task usually accomplished with ``xinetd`` or ``socat``, but for simple one-off applications, this is much easier, and a good demonstration of nclib's capabilities. 6 | 7 | Once you've installed nclib, the program ``serve-stdio`` should be installed to your path, and you should be able to run it!:: 8 | 9 | $ serve-stdio 10 | Usage: serve-stdio [options] port command ... 11 | Options: 12 | -d Daemonize, run in background 13 | -e Redirect program's stderr to socket 14 | -E Hide program's stderr 15 | -b addr Bind to a specific address 16 | 17 | Example usage:: 18 | 19 | $ serve-stdio -d 1234 echo hey 20 | 13282 21 | $ nc localhost 1234 22 | hey 23 | 24 | By default, the process' stderr stream will be untouched and will probably end 25 | up printed to your terminal. If you want the socket to see the process' 26 | stderr, you can use the -e flag. If you want the process' stderr to go away 27 | entirely, you can use the -E flag. 28 | 29 | How does it work? 30 | It's a very short python script using nclib! The heart of it is just these three lines:: 31 | 32 | for client in nclib.TCPServer((bind_addr, port)): 33 | print('Accepted client %s:%d' % client.peer) 34 | nclib.Process.launch(command, client, stderr=show_stderr) 35 | 36 | Pretty cool! 37 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | nclib 2 | ===== 3 | 4 | nclib is a python socket library that wants to be your friend. 5 | 6 | nclib provides: 7 | 8 | - Easy-to-use interfaces for connecting to and listening on TCP and UDP sockets 9 | - The ability to handle any python stream-like object with a single interface 10 | - A better socket class, the Netcat object 11 | 12 | - Convenient receive methods for common socket usage patterns 13 | - Highly customizable logging 14 | - Interactive mode, connecting the socket to your stdin/stdout 15 | - Intelligent detection of socket closes and connection drops 16 | - Long-running functions cleanly abortable with ctrl-c 17 | - Lots of aliases in case you forget the right method name 18 | 19 | - Mechanisms to launch processes with their in/out streams connected to sockets 20 | 21 | - Launch a process with gdb attached 22 | 23 | - TCP and UDP server classes for writing simple python daemons 24 | - A script to easily daemonize command-line programs 25 | 26 | Source code is available at https://github.com/rhelmot/nclib. 27 | 28 | If you are familiar with pwntools, nclib provides much of the functionaly that 29 | pwntools' socket wrappers do, but with the bonus feature of not being pwntools. 30 | 31 | To install nclib, run ``pip install nclib``. 32 | 33 | .. toctree:: 34 | :maxdepth: 2 35 | :caption: Contents: 36 | 37 | netcat 38 | server 39 | process 40 | logger 41 | simplesock 42 | serve-stdio 43 | 44 | 45 | Indices and tables 46 | ================== 47 | 48 | * :ref:`genindex` 49 | * :ref:`modindex` 50 | * :ref:`search` 51 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | long_description = ''' 2 | nclib is a python socket library that wants to be your friend. 3 | 4 | nclib provides: 5 | 6 | - Easy-to-use interfaces for connecting to and listening on TCP and UDP sockets 7 | - The ability to handle any python stream-like object with a single interface 8 | - A better socket class, the Netcat object 9 | 10 | - Convenient receive methods for common socket usage patterns 11 | - Highly customizable logging 12 | - Interactive mode, connecting the socket to your stdin/stdout 13 | - Intelligent detection of socket closes and connection drops 14 | - Long-running functions cleanly abortable with ctrl-c 15 | - Lots of aliases in case you forget the right method name 16 | 17 | - Mechanisms to launch processes with their in/out streams connected to sockets 18 | 19 | - Launch a process with gdb attached 20 | 21 | - TCP and UDP server classes for writing simple python daemons 22 | - A script to easily daemonize command-line programs 23 | 24 | Documentation is available at https://nclib.readthedocs.io/ and source code is 25 | available at https://github.com/rhelmot/nclib 26 | 27 | If you are familiar with pwntools, nclib provides much of the functionaly that 28 | pwntools' socket wrappers do, but with the bonus feature of not being pwntools. 29 | ''' 30 | 31 | if bytes is str: 32 | raise Exception("nclib is python 3 only now :(") 33 | 34 | from setuptools import setup 35 | setup(name='nclib', 36 | version='1.0.6', 37 | python_requires='>=3.5', 38 | packages=['nclib'], 39 | scripts=['serve-stdio'], 40 | description='Netcat as a library: convienent socket interfaces', 41 | long_description=long_description, 42 | url='https://github.com/rhelmot/nclib', 43 | author='rhelmot', 44 | author_email='audrey@rhelmot.io', 45 | license='MIT', 46 | keywords='netcat nc socket sock file pipe tcp udp recv until logging interact handle listen connect server serve stdio process gdb', 47 | package_data={'nclib': ['py.typed']}, 48 | ) 49 | -------------------------------------------------------------------------------- /serve-stdio: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import print_function 4 | 5 | import os, sys 6 | import signal 7 | import nclib 8 | 9 | argiter = iter(sys.argv) 10 | progname = next(argiter) 11 | daemonize = False 12 | show_stderr = None 13 | bind_addr = '0.0.0.0' 14 | 15 | def usage(): 16 | print('Usage: %s [options] port command ...' % sys.argv[0]) 17 | print('Options:') 18 | print(' -d Daemonize, run in background') 19 | print(' -e Redirect program\'s stderr to socket') 20 | print(' -E Hide program\'s stderr') 21 | print(' -b addr Bind to a specific address') 22 | sys.exit(1) 23 | 24 | for arg in argiter: 25 | try: 26 | if arg == '-d': 27 | daemonize = True 28 | elif arg == '-e': 29 | show_stderr = True 30 | elif arg == '-E': 31 | show_stderr = False 32 | elif arg == '-b': 33 | bind_addr = next(argiter) 34 | else: 35 | break 36 | except StopIteration: 37 | usage() 38 | else: 39 | usage() 40 | 41 | try: 42 | port = int(arg) # pylint: disable=undefined-loop-variable 43 | except (ValueError, NameError): 44 | usage() 45 | 46 | def quote(s): 47 | return "'%s'" % s.replace("'", "'\"'\"'") 48 | 49 | command = ' '.join(map(quote, argiter)) 50 | if command == '': 51 | usage() 52 | 53 | if daemonize: 54 | p = os.fork() 55 | if p: 56 | print(p) 57 | sys.stdout.flush() 58 | os._exit(0) 59 | 60 | children = [] 61 | def reap(): 62 | i = 0 63 | while i < len(children): 64 | if children[i].poll() is not None: 65 | children.pop(i) 66 | else: 67 | i += 1 68 | 69 | def cleanup(*args): # pylint: disable=unused-argument 70 | for child in children: 71 | child.kill() # not sure how effective this is, will probably only kill the shell... 72 | print() 73 | sys.exit(0) 74 | 75 | signal.signal(signal.SIGTERM, cleanup) 76 | signal.signal(signal.SIGINT, cleanup) 77 | 78 | for client in nclib.TCPServer((bind_addr, port)): 79 | print('Accepted client %s:%d' % client.peer) 80 | children.append(nclib.Process.launch(command, client, stderr=show_stderr)) 81 | client.close() 82 | reap() 83 | -------------------------------------------------------------------------------- /nclib/select.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple, List, TYPE_CHECKING, Iterable, Union, Optional 2 | import select as _select 3 | 4 | if TYPE_CHECKING: 5 | from .netcat import Netcat 6 | 7 | # hey!!! did you know that you can have nested loops in list/dict/generator comprehensions???? 8 | # GUESS WHAT WE'RE DOING HERE 9 | def select( 10 | select_read: Iterable["Netcat"], 11 | select_write: Optional[Iterable["Netcat"]]=None, 12 | select_exc: Optional[Iterable["Netcat"]]=None, 13 | timeout: Union[None, int, float]=None 14 | ) -> Tuple[List["Netcat"], List["Netcat"], List["Netcat"]]: 15 | """ 16 | A select function which works for any netcat or simplesock object. 17 | This function is a drop-in replacement for python's ``select.select``. 18 | 19 | The main advantage is that sockets with multiple backing file descriptors 20 | are handled cleanly. 21 | """ 22 | if select_write is None: 23 | select_write = [] 24 | if select_exc is None: 25 | select_exc = [] 26 | allsocks = set(sock for sockset in (select_read, select_write, select_exc) for sock in sockset) 27 | sock_mapping = {sock: sock._prep_select() for sock in allsocks} 28 | 29 | reverse_read = {base: sock for sock, (baselist, _, _) in sock_mapping.items() for base in baselist} 30 | reverse_write = {base: sock for sock, (_, baselist, _) in sock_mapping.items() for base in baselist} 31 | reverse_exc = {base: sock for sock, (_, _, baselist) in sock_mapping.items() for base in baselist} 32 | 33 | base_read = list(set(base for sock in select_read for base in sock_mapping[sock][0])) 34 | base_write = list(set(base for sock in select_write for base in sock_mapping[sock][1])) 35 | base_exc = list(set(base for sock in select_exc for base in sock_mapping[sock][2])) 36 | 37 | # if any socks in the *original* read have anything buffered, we should treat it as if select 38 | # returns immediately. however we need to check if any other socks have data buffered in the 39 | # *kernel*. 40 | preselected = set(sock for sock in select_read if getattr(sock, 'buf', ())) 41 | if preselected: 42 | timeout = 0 43 | 44 | sel_base_read, sel_base_write, sel_base_exc = _select.select(base_read, base_write, base_exc, timeout) 45 | 46 | sel_read = tuple(set(reverse_read[base] for base in sel_base_read) | preselected) 47 | sel_write = tuple(set(reverse_write[base] for base in sel_base_write)) 48 | sel_exc = tuple(set(reverse_exc[base] for base in sel_base_exc)) 49 | return sel_read, sel_write, sel_exc 50 | -------------------------------------------------------------------------------- /nclib/server.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Tuple 2 | import socket 3 | import select 4 | 5 | from . import Netcat 6 | from .netcat import _is_ipv6_addr 7 | 8 | class TCPServer: 9 | """ 10 | A simple TCP server model. Iterating over it will yield client sockets as 11 | Netcat objects. 12 | 13 | :param bindto: The address to bind to, a tuple (host, port) 14 | :param kernel_backlog: The argument to listen() 15 | :param family: The address family to use. set to `socket.AF_INET6` for IPv6. 16 | 17 | Any additional keyword arguments will be passed to the constructor of the 18 | Netcat object that is constructed for each client. 19 | 20 | Here is a simple echo server example: 21 | 22 | >>> from nclib import TCPServer 23 | >>> server = TCPServer(('0.0.0.0', 1337)) 24 | >>> for client in server: 25 | ... client.send(client.recv()) # or submit to a thread pool for async handling... 26 | 27 | """ 28 | def __init__(self, bindto: Tuple[str, int], kernel_backlog: int=5, family: Optional[int]=None, **kwargs): 29 | self.addr = bindto 30 | self.kwargs = kwargs 31 | 32 | if family is None: 33 | family = socket.AF_INET6 if _is_ipv6_addr(bindto[0]) else socket.AF_INET 34 | self.sock = socket.socket(type=socket.SOCK_STREAM, family=family) 35 | self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 36 | self.sock.bind(bindto) 37 | self.sock.listen(kernel_backlog) 38 | 39 | self._oob_rsock, self._oob_wsock = socket.socketpair() 40 | self.closed = False 41 | 42 | def __iter__(self): 43 | while True: 44 | rl, _, _ = select.select([self.sock, self._oob_rsock], [], []) 45 | if self._oob_rsock in rl: 46 | self._oob_rsock.close() 47 | self._oob_wsock.close() 48 | break 49 | client, addr = self.sock.accept() 50 | yield Netcat(sock=client, server=addr, **self.kwargs) 51 | 52 | def close(self): 53 | """ 54 | Tear down this server and release its resources 55 | """ 56 | if not self.closed: 57 | self.closed = True 58 | self._oob_wsock.send(b'.') 59 | self.sock.close() 60 | 61 | 62 | class UDPServer: 63 | """ 64 | A simple UDP server model. Iterating over it will yield of tuples of 65 | datagrams and peer addresses. To respond, use the respond method, which 66 | takes the response and the peer address. 67 | 68 | :param bindto: The address to bind to, a tuple (host, port) 69 | :param dgram_size: The size of the datagram to receive. This is 70 | important! If you send a message longer than the 71 | receiver's receiving size, the rest of the message 72 | will be silently lost! Default is 4096. 73 | 74 | Here is a simple echo server example: 75 | 76 | >>> from nclib import UDPServer 77 | >>> server = UDPServer(('0.0.0.0', 1337)) 78 | >>> for message, peer in server: 79 | ... server.respond(message, peer) # or submit to a thread pool for async handling... 80 | 81 | """ 82 | def __init__(self, bindto, dgram_size=4096): 83 | self.addr = bindto 84 | self.dgram_size = dgram_size 85 | self.sock = socket.socket(type=socket.SOCK_DGRAM) 86 | self.sock.bind(bindto) 87 | 88 | def __iter__(self): 89 | while True: 90 | packet, peer = self.sock.recvfrom(self.dgram_size) 91 | yield packet, peer 92 | 93 | def respond(self, packet, peer, flags=0): 94 | """ 95 | Send a message back to a peer. 96 | 97 | :param packet: The data to send 98 | :param peer: The address to send to, as a tuple (host, port) 99 | :param flags: Any sending flags you want to use for some reason 100 | """ 101 | self.sock.sendto(packet, flags, peer) 102 | 103 | def close(self): 104 | """ 105 | Tear down this server and release its resources 106 | """ 107 | return self.sock.close() 108 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # nclib documentation build configuration file, created by 4 | # sphinx-quickstart on Fri Jun 9 16:05:26 2017. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | # If extensions (or modules to document with autodoc) are in another directory, 16 | # add these directories to sys.path here. If the directory is relative to the 17 | # documentation root, use os.path.abspath to make it absolute, like shown here. 18 | 19 | import os 20 | import sys 21 | sys.path.insert(0, os.path.abspath('..')) 22 | 23 | 24 | # -- General configuration ------------------------------------------------ 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | # 28 | # needs_sphinx = '1.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = ['sphinx.ext.autodoc', 34 | 'sphinx.ext.viewcode'] 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ['_templates'] 38 | 39 | # The suffix(es) of source filenames. 40 | # You can specify multiple suffix as a list of string: 41 | # 42 | # source_suffix = ['.rst', '.md'] 43 | source_suffix = '.rst' 44 | 45 | # The master toctree document. 46 | master_doc = 'index' 47 | 48 | # General information about the project. 49 | project = u'nclib' 50 | copyright = u'2019, rhelmot' 51 | author = u'rhelmot' 52 | 53 | # The version info for the project you're documenting, acts as replacement for 54 | # |version| and |release|, also used in various other places throughout the 55 | # built documents. 56 | # 57 | # The short X.Y version. 58 | version = u'1.0.5' 59 | # The full version, including alpha/beta/rc tags. 60 | release = u'1.0.5' 61 | 62 | # The language for content autogenerated by Sphinx. Refer to documentation 63 | # for a list of supported languages. 64 | # 65 | # This is also used if you do content translation via gettext catalogs. 66 | # Usually you set "language" from the command line for these cases. 67 | language = None 68 | 69 | # List of patterns, relative to source directory, that match files and 70 | # directories to ignore when looking for source files. 71 | # This patterns also effect to html_static_path and html_extra_path 72 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 73 | 74 | # The name of the Pygments (syntax highlighting) style to use. 75 | pygments_style = 'sphinx' 76 | 77 | # If true, `todo` and `todoList` produce output, else they produce nothing. 78 | todo_include_todos = False 79 | 80 | #autodoc_default_flags = ['members'] 81 | 82 | 83 | # -- Options for HTML output ---------------------------------------------- 84 | 85 | # The theme to use for HTML and HTML Help pages. See the documentation for 86 | # a list of builtin themes. 87 | # 88 | html_theme = 'classic' 89 | 90 | # Theme options are theme-specific and customize the look and feel of a theme 91 | # further. For a list of options available for each theme, see the 92 | # documentation. 93 | # 94 | # html_theme_options = {} 95 | 96 | # Add any paths that contain custom static files (such as style sheets) here, 97 | # relative to this directory. They are copied after the builtin static files, 98 | # so a file named "default.css" will overwrite the builtin "default.css". 99 | html_static_path = ['_static'] 100 | 101 | 102 | # -- Options for HTMLHelp output ------------------------------------------ 103 | 104 | # Output file base name for HTML help builder. 105 | htmlhelp_basename = 'nclibdoc' 106 | 107 | 108 | # -- Options for LaTeX output --------------------------------------------- 109 | 110 | latex_elements = { 111 | # The paper size ('letterpaper' or 'a4paper'). 112 | # 113 | # 'papersize': 'letterpaper', 114 | 115 | # The font size ('10pt', '11pt' or '12pt'). 116 | # 117 | # 'pointsize': '10pt', 118 | 119 | # Additional stuff for the LaTeX preamble. 120 | # 121 | # 'preamble': '', 122 | 123 | # Latex figure (float) alignment 124 | # 125 | # 'figure_align': 'htbp', 126 | } 127 | 128 | # Grouping the document tree into LaTeX files. List of tuples 129 | # (source start file, target name, title, 130 | # author, documentclass [howto, manual, or own class]). 131 | latex_documents = [ 132 | (master_doc, 'nclib.tex', u'nclib Documentation', 133 | u'rhelmot', 'manual'), 134 | ] 135 | 136 | 137 | # -- Options for manual page output --------------------------------------- 138 | 139 | # One entry per manual page. List of tuples 140 | # (source start file, name, description, authors, manual section). 141 | man_pages = [ 142 | (master_doc, 'nclib', u'nclib Documentation', 143 | [author], 1) 144 | ] 145 | 146 | 147 | # -- Options for Texinfo output ------------------------------------------- 148 | 149 | # Grouping the document tree into Texinfo files. List of tuples 150 | # (source start file, target name, title, author, 151 | # dir menu entry, description, category) 152 | texinfo_documents = [ 153 | (master_doc, 'nclib', u'nclib Documentation', 154 | author, 'nclib', 'One line description of project.', 155 | 'Miscellaneous'), 156 | ] 157 | 158 | 159 | 160 | -------------------------------------------------------------------------------- /nclib/process.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import os 3 | import random 4 | 5 | from .netcat import Netcat 6 | 7 | class Process(Netcat): 8 | """ 9 | A mechanism for launching a local process and interacting with it 10 | programatically. This class is a subclass of the basic `Netcat` object so 11 | you may use any method from that class to interact with the process you've 12 | launched! 13 | 14 | :param program: The program to launch. Can be either a list of strings, 15 | in which case those strings will become the program 16 | argv, or a single string, in which case the shell will 17 | be used to launch the program. 18 | :param stderr: How the program's stderr stream should behave. True 19 | (default) will redirect stderr to the output socket, 20 | unifying it with stdout. False will redirect it to 21 | /dev/null. None will not touch it, causing it to appear 22 | on your terminal. 23 | :param cwd: The working directory to execute the program in 24 | :param env: The environment to execute the program in, as a 25 | dictionary 26 | 27 | Any additional keyword arguments will be passed to the constructor of 28 | Netcat. 29 | 30 | WARNING: If you provide a string and not a list as the description for the 31 | program to launch, then the pid we know about will be associated with the 32 | shell that launches the program, not the program itself. 33 | 34 | *Example:* Launch the `cat` process and send it a greeting. Print out its 35 | response. Close the socket and the process exits with status 0. 36 | 37 | >>> from nclib import Process 38 | >>> cat = Process('cat') 39 | >>> cat.send('Hello world!') 40 | >>> print(cat.recv()) 41 | b'Hello world!' 42 | >>> cat.close() 43 | >>> print(cat.poll()) 44 | 0 45 | """ 46 | def __init__(self, program, 47 | stderr=True, 48 | cwd=None, 49 | env=None, 50 | **kwargs): 51 | self._subprocess = self.launch(program, stderr=stderr, cwd=cwd, env=env) 52 | self.pid = self._subprocess.pid 53 | 54 | super().__init__( 55 | sock=self._subprocess.stdout, 56 | sock_send=self._subprocess.stdin, 57 | server='local program %s' % program, **kwargs) 58 | 59 | def poll(self): 60 | """ 61 | Return the exit code of the proces, or None if it has not exited. 62 | """ 63 | return self._subprocess.poll() 64 | 65 | def wait(self): 66 | """ 67 | Wait for the process to exit and return its exit code. 68 | """ 69 | return self._subprocess.wait() 70 | 71 | def send_signal(self, sig): 72 | """ 73 | Send the signal `sig` to the process. 74 | """ 75 | return self._subprocess.send_signal(sig) 76 | 77 | def kill(self): 78 | """ 79 | Terminate the process. 80 | """ 81 | return self._subprocess.kill() 82 | 83 | @staticmethod 84 | def launch(program, sock=None, stderr=True, cwd=None, env=None): 85 | """ 86 | A static method for launching a process. 87 | Same rules from the Process constructor apply. 88 | 89 | :param sock: The socket to use for stdin/stdout. If None, will be a new set of pipes. 90 | """ 91 | 92 | if stderr is True: 93 | stderr = subprocess.STDOUT 94 | elif stderr is False: 95 | stderr = subprocess.DEVNULL 96 | 97 | if sock is None: 98 | sock = subprocess.PIPE 99 | 100 | return subprocess.Popen(program, 101 | shell=type(program) not in (list, tuple), 102 | stdin=sock, stdout=sock, stderr=stderr, 103 | cwd=cwd, env=env, 104 | close_fds=True) 105 | 106 | 107 | class GDBProcess(Process): 108 | """ 109 | Like nclib.Process, but also launches gdb (in a new $TERMINAL, by default gnome-terminal) 110 | to debug the process. 111 | """ 112 | def __init__(self, program, gdbscript=None, **kwargs): 113 | """ 114 | :param program: The program to launch. Can be either a list of strings, in which case 115 | those strings will become the program argv, or a single string, in which 116 | case the shell will be used to launch the program. 117 | :param stderr: How the program's stderr stream should behave. True (default) will 118 | redirect stderr to the output socket, unifying it with stdout. False will 119 | redirect it to /dev/null. None will not touch it, causing it to appear 120 | on your terminal. 121 | :param cwd: The working directory to execute the program in 122 | :param env: The environment to execute the program in, as a dictionary 123 | :param protocol: The socket protocol to use. 'tcp' by default, can also be 'udp' 124 | :param gdbscript: The filename of a script for gdb to execute automatically on startup 125 | 126 | Any additional keyword arguments will be passed to the constructor of Netcat. 127 | """ 128 | super(GDBProcess, self).__init__(program, **kwargs) 129 | 130 | progbase = (program.split() if type(program) not in (list, tuple) else program)[0] 131 | # TODO: this should be refactored so it's the other way around 132 | # we should be fromatting stuff into bytestrings since that's what goes into the syscalls 133 | if bytes is not str and type(progbase) is bytes: 134 | progbase = progbase.decode() 135 | gdbcmd = 'gdb %s -ex "set sysroot" -ex "target remote tcp::%d"' % (progbase, self._subprocess._gdbport) # pylint: disable=no-member 136 | if gdbscript is not None: 137 | gdbcmd += " -x '%s'" % (gdbscript.replace("'", "'\"'\"'")) 138 | 139 | nul = open(os.devnull, 'r+b') 140 | t = os.environ.get("TERMINAL", "gnome-terminal").split() 141 | self.term = subprocess.Popen(t + ['-e', gdbcmd], 142 | close_fds=True, 143 | stdin=nul, stdout=nul, stderr=nul) 144 | 145 | self.recv_until(b'pid = ') 146 | self.pid = int(self.recvline()) 147 | self.recvline() 148 | self.recvline() 149 | 150 | @classmethod 151 | def launch(cls, program, *args, **kwargs): 152 | gdbport = random.randint(32768, 60999) # default /proc/sys/net/ipv4/ip_local_port_range on my machine 153 | gdbcmd = ['gdbserver', 'localhost:%d' % gdbport] 154 | if type(program) not in (list, tuple): 155 | if bytes is not str and type(program) is bytes: 156 | program = program.decode() 157 | program = '%s %s' % (' '.join(gdbcmd), program) 158 | else: 159 | program = gdbcmd + program 160 | 161 | p = super(GDBProcess, cls).launch(program, *args, **kwargs) 162 | p._gdbport = gdbport 163 | return p 164 | -------------------------------------------------------------------------------- /nclib/logger.py: -------------------------------------------------------------------------------- 1 | class Logger: 2 | """ 3 | The base class for loggers for use with Netcat objects. Each of these 4 | methods will be called to indicate a given event. 5 | """ 6 | def connected(self, peer): 7 | """ 8 | Called with a tuple of the peer to indicate "connection established", 9 | either as a client or a server 10 | """ 11 | 12 | def sending(self, data): 13 | """ 14 | Called to indicate that some data has been sent over the wire 15 | """ 16 | 17 | def buffering(self, data): 18 | """ 19 | Called to indicate that some data has been received and inserted into 20 | the buffer 21 | """ 22 | 23 | def unbuffering(self, data): 24 | """ 25 | Called to indicate that some data is being extracted from the buffer 26 | and returned to the user 27 | """ 28 | 29 | def interrupted(self): 30 | """ 31 | Called to indicate that a socket operation was interrupted via ctrl-c 32 | """ 33 | 34 | def eofed(self): 35 | """ 36 | Called to indicate that reading from the socket resulted in an EOF 37 | condition 38 | """ 39 | 40 | def requesting_send(self, data): 41 | """ 42 | Called to indicate that the user has asked to send all of some data 43 | """ 44 | 45 | def requesting_recv(self, n, timeout): 46 | """ 47 | Called to indicate that the user has asked for a receive of at most n 48 | bytes 49 | """ 50 | 51 | def requesting_recv_until(self, s, max_size, timeout): 52 | """ 53 | Called to indicate that the user has asked to receive until a given 54 | string appears 55 | """ 56 | 57 | def requesting_recv_all(self, timeout): 58 | """ 59 | Called to indicate that the user has asked to receive all data until 60 | close 61 | """ 62 | 63 | def requesting_recv_exactly(self, n, timeout): 64 | """ 65 | Called to indicate that the user has asked to receive exactly n bytes 66 | """ 67 | 68 | def interact_starting(self): 69 | """ 70 | Called to indicate that an interactive session is beginning 71 | """ 72 | 73 | def interact_ending(self): 74 | """ 75 | Called to indicate that an interactive session is ending 76 | """ 77 | 78 | class ManyLogger(Logger): 79 | """ 80 | A logger which dispatches all events to all its children, which are other 81 | loggers. You shouldn't have to deal with this much; it's used automatically 82 | by the Netcat. 83 | 84 | :param children: A list of loggers to which to dispatch events 85 | """ 86 | def __init__(self, children): 87 | self.children = children 88 | 89 | def connected(self, peer): 90 | for child in self.children: 91 | child.connected(peer) 92 | 93 | def sending(self, data): 94 | for child in self.children: 95 | child.sending(data) 96 | 97 | def buffering(self, data): 98 | for child in self.children: 99 | child.buffering(data) 100 | 101 | def unbuffering(self, data): 102 | for child in self.children: 103 | child.unbuffering(data) 104 | 105 | def interrupted(self): 106 | for child in self.children: 107 | child.interrupted() 108 | 109 | def eofed(self): 110 | for child in self.children: 111 | child.eofed() 112 | 113 | def requesting_send(self, data): 114 | for child in self.children: 115 | child.requesting_send(data) 116 | 117 | def requesting_recv(self, n, timeout): 118 | for child in self.children: 119 | child.requesting_recv(n, timeout) 120 | 121 | def requesting_recv_until(self, s, max_size, timeout): 122 | for child in self.children: 123 | child.requesting_recv_until(s, max_size, timeout) 124 | 125 | def requesting_recv_all(self, timeout): 126 | for child in self.children: 127 | child.requesting_recv_all(timeout) 128 | 129 | def requesting_recv_exactly(self, n, timeout): 130 | for child in self.children: 131 | child.requesting_recv_exactly(n, timeout) 132 | 133 | def interact_starting(self): 134 | for child in self.children: 135 | child.interact_starting() 136 | 137 | def interact_ending(self): 138 | for child in self.children: 139 | child.interact_ending() 140 | 141 | class TeeLogger(Logger): 142 | """ 143 | A logger which feeds a copy of the input and output streams to a given stream 144 | 145 | :param log_send: A simplesock object to log all sends to, or None 146 | :param log_recv: A simplesock object to log all recvs to, or None 147 | :param log_yield: Whether recv logging should happen when data is 148 | buffered or returned to the user 149 | """ 150 | def __init__(self, log_send=None, log_recv=None, log_yield=False): 151 | self.log_send = log_send 152 | self.log_recv = log_recv 153 | self.log_yield = log_yield 154 | 155 | def sending(self, data): 156 | if self.log_send is not None: 157 | self.log_send.send(data) 158 | 159 | def buffering(self, data): 160 | if not self.log_yield and self.log_recv is not None: 161 | self.log_recv.send(data) 162 | 163 | def unbuffering(self, data): 164 | if self.log_yield and self.log_recv is not None: 165 | self.log_recv.send(data) 166 | 167 | class StandardLogger(Logger): 168 | """ 169 | A logger which produces a human-readable log of what's happening 170 | 171 | :param log: A simplesock object to which the logs should 172 | be sent 173 | :param log_yield: Whether to log receives when they are recieved 174 | and written to the buffer or when they are 175 | unbuffered and returned to the user 176 | :param show_headers: Whether to show metadata notices about user 177 | actions and exceptional conditions 178 | :param hex_dump: Whether to encode logged data as a canonical 179 | hexdump 180 | :param split_newlines: When in non-hex mode, whether logging should 181 | treat newlines as record separators 182 | """ 183 | def __init__(self, log, log_yield=False, show_headers=True, 184 | hex_dump=False, split_newlines=True, 185 | send_prefix='>> ', recv_prefix='<< ', no_eol_indicator='\x1b[3m%\x1b[0m'): 186 | self.log = log 187 | self.log_yield = log_yield 188 | self.show_headers = show_headers 189 | self.hex_dump = hex_dump 190 | self.split_newlines = split_newlines 191 | self.send_prefix = send_prefix 192 | self.recv_prefix = recv_prefix 193 | self.no_eol_indicator = no_eol_indicator 194 | 195 | self.suppressed = False 196 | self.counter_send = 0 197 | self.counter_recv = 0 198 | 199 | def _log(self, s): 200 | self.log.send(s.encode()) 201 | 202 | def _header(self, s): 203 | if self.show_headers: 204 | self._log('======= %s =======\n' % s) 205 | 206 | def sending(self, data): 207 | if self.suppressed: 208 | return 209 | self._log_data(data, self.counter_send, self.send_prefix) 210 | self.counter_send += len(data) 211 | 212 | def _recving(self, data): 213 | if self.suppressed: 214 | return 215 | self._log_data(data, self.counter_recv, self.recv_prefix) 216 | self.counter_recv += len(data) 217 | 218 | def _log_data(self, data, counter, prefix): 219 | if not data: 220 | return 221 | 222 | if self.hex_dump: 223 | line_progress = counter % 16 224 | first_line_size = 16 - line_progress 225 | first_line = data[:first_line_size] 226 | self._log(prefix + self._hex_line(counter, first_line)) 227 | 228 | for i in range(first_line_size, len(data), 16): 229 | line = data[i:i+16] 230 | self._log(prefix + self._hex_line(counter + i, line)) 231 | 232 | else: 233 | noeol = False 234 | if self.split_newlines: 235 | records = data.split(b'\n') 236 | if len(records) > 1 and records[-1] == b'': 237 | records.pop() 238 | else: 239 | noeol = True 240 | else: 241 | records = [data] 242 | 243 | for i, record in enumerate(records): 244 | sep = (self.no_eol_indicator if noeol and i == len(records) - 1 else '') + '\n' 245 | self._log(prefix + self._escape(record) + sep) 246 | 247 | @staticmethod 248 | def _hex_line(counter, line): 249 | advance = counter % 16 250 | tail = 16 - advance - len(line) 251 | lhex = line.hex().upper() 252 | lhex = ' '*advance + lhex + ' '*tail 253 | lascii = ' '*advance + ''.join(chr(c) if 0x20 <= c <= 0x7e else '.' for c in line) + ' '*tail 254 | 255 | fargs = (counter,) + tuple(lhex[i:i+2] for i in range(0, 0x20, 2)) + (lascii,) 256 | return '%06X %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s |%s|\n' % fargs 257 | 258 | @staticmethod 259 | def _escape(bs): 260 | return ''.join(StandardLogger._escchr(c) for c in bs) 261 | 262 | @staticmethod 263 | def _escchr(c): 264 | if c == ord('\\'): 265 | return '\\\\' 266 | if c == ord('\n'): 267 | return '\\n' 268 | if c == ord('\t'): 269 | return '\\t' 270 | if c < 0x20 or c > 0x7e: 271 | return '\\x%02x' % c 272 | return chr(c) 273 | 274 | def buffering(self, data): 275 | if not self.log_yield: 276 | self._recving(data) 277 | 278 | def unbuffering(self, data): 279 | if self.log_yield: 280 | self._recving(data) 281 | 282 | def connected(self, peer): 283 | self._header("Connected to %s" % str(peer)) 284 | 285 | def interrupted(self): 286 | self._header("Connection interrupted") 287 | 288 | def eofed(self): 289 | self._header("Received EOF") 290 | 291 | def requesting_send(self, data): 292 | if self.suppressed: 293 | return 294 | self._header("Sending %d byte%s" % (len(data), '' if len(data) == 1 else 's')) 295 | 296 | @staticmethod 297 | def _timeout_text(timeout): 298 | if timeout is None: 299 | return '' 300 | if timeout == 0: 301 | return ' (nonblocking)' 302 | return ' (until %s second%s)' % (timeout, '' if timeout == 1 else 's') 303 | 304 | def requesting_recv(self, n, timeout): 305 | if self.suppressed: 306 | return 307 | self._header("Receiving at most %d byte%s%s" % (n, '' if n == 1 else 's', self._timeout_text(timeout))) 308 | 309 | def requesting_recv_until(self, s, max_size, timeout): 310 | if self.suppressed: 311 | return 312 | if max_size is not None: 313 | max_size_text = ', max of %d byte%s' % (max_size, '' if max_size == 1 else 's') 314 | else: 315 | max_size_text = '' 316 | 317 | self._header("Receiving until %s%s%s" % (repr(s).strip('b'), max_size_text, self._timeout_text(timeout))) 318 | 319 | def requesting_recv_all(self, timeout): 320 | if self.suppressed: 321 | return 322 | self._header("Receiving until close%s" % self._timeout_text(timeout)) 323 | 324 | def requesting_recv_exactly(self, n, timeout): 325 | self._header("Receiving exactly %d byte%s%s" % (n, '' if n == 1 else 's', self._timeout_text(timeout))) 326 | 327 | def interact_starting(self): 328 | self._header("Beginning interactive session") 329 | self.suppressed = True 330 | 331 | def interact_ending(self): 332 | self.suppressed = False 333 | -------------------------------------------------------------------------------- /nclib/simplesock.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import io 3 | import logging 4 | 5 | from .select import select 6 | from .errors import NetcatError 7 | 8 | class Simple: 9 | """ 10 | The base class for implementing a simple, unified interface for socket-like 11 | objects. Instances of this type should act like a simple unbuffered 12 | blocking socket. 13 | 14 | :ivar can_send: Whether this stream supports send operations 15 | :ivar can_recv: Whether this stream supports recv operations 16 | """ 17 | def __init__(self): 18 | self.can_send = False 19 | self.can_recv = False 20 | 21 | def recv(self, size): 22 | """ 23 | Receive at most `size` bytes, blocking until some data is available. 24 | Returns an empty bytestring as an EOF condition. 25 | """ 26 | raise NotImplementedError 27 | 28 | def send(self, data): 29 | """ 30 | Send as much of the given data as possible, returning the number of 31 | bytes sent. 32 | """ 33 | raise NotImplementedError 34 | 35 | def close(self): 36 | """ 37 | Close the stream. 38 | """ 39 | raise NotImplementedError 40 | 41 | @property 42 | def closed(self): 43 | """ 44 | Whether the stream has been closed. 45 | """ 46 | raise NotImplementedError 47 | 48 | def fileno(self): 49 | """ 50 | The file descriptor associated with the stream. Should raise 51 | `NetcatError` if there is not a single file descriptor to return. 52 | """ 53 | raise NotImplementedError 54 | 55 | def shutdown(self, how): 56 | """ 57 | Indicate somehow that there will be no more reading/writing/both to 58 | this stream. Takes the ``socket.SHUT_*`` constants. 59 | """ 60 | raise NotImplementedError 61 | 62 | def _prep_select(self): 63 | """ 64 | Return three tuples of all the raw python file objects that should be 65 | selected over in order to determine if this stream is 66 | readable/writable/in an exceptional condition. 67 | """ 68 | raise NotImplementedError 69 | 70 | def wrap(sock): 71 | """ 72 | A helper function to automatically pick the correct wrapper class for a 73 | sock-like object. 74 | """ 75 | if isinstance(sock, Simple): 76 | return sock 77 | if isinstance(sock, socket.socket): 78 | return SimpleSocket(sock) 79 | if isinstance(sock, io.IOBase): 80 | return SimpleFile(sock) 81 | raise ValueError("idk how to work with a %r. check your work or report a bug?" % type(sock)) 82 | 83 | class SimpleSocket(Simple): 84 | """ 85 | A wrapper for sockets. 86 | 87 | :param sock: A python ``socket.socket`` object. 88 | """ 89 | def __init__(self, sock): 90 | super().__init__() 91 | self.sock = sock # a socket.socket object 92 | self.can_recv = True 93 | self.can_send = True 94 | 95 | # disable timeout and enable blocking. this is a simple socket after all. 96 | sock.settimeout(None) 97 | sock.setblocking(True) 98 | 99 | def recv(self, size): 100 | return self.sock.recv(size) 101 | 102 | def send(self, data): 103 | return self.sock.send(data) 104 | 105 | def close(self): 106 | return self.sock.close() 107 | 108 | @property 109 | def closed(self): 110 | return self.sock._closed 111 | 112 | def fileno(self): 113 | return self.sock.fileno() 114 | 115 | def shutdown(self, how): 116 | try: 117 | return self.sock.shutdown(how) 118 | except OSError: 119 | # e.g. udp sockets may do this 120 | pass 121 | 122 | def _prep_select(self): 123 | return ((self.sock,) if self.can_recv else ()), ((self.sock,) if self.can_send else ()), (self.sock,) 124 | 125 | class SimpleFile(Simple): 126 | """ 127 | A wrapper for files from the python ``io`` module, including ``sys.stdin``, 128 | subprocess pipes, etc. If the file has an encoding, it will be discarded. 129 | 130 | :param fp: An ``io.IOBase`` object 131 | """ 132 | def __init__(self, fp): 133 | super().__init__() 134 | self.please_decode = None 135 | # a common case is to pass Nclib(log_send=open(file)). 136 | # this will fail because when we unwrap the object its outer layers are garbage collected 137 | # which closes the file. 138 | self._no_garbage_collection_thx = fp 139 | 140 | try: # check if we have a TextIOWrapper 141 | buf = fp.buffer 142 | self.please_decode = fp.encoding 143 | fp = buf 144 | except AttributeError: 145 | pass 146 | 147 | try: # check if we have a BufferedReader 148 | fp = fp.raw 149 | except AttributeError: 150 | pass 151 | 152 | self.file = fp # an io.IOBase object 153 | self.can_recv = fp.mode.startswith('r') 154 | self.can_send = fp.mode.startswith('w') or fp.mode.startswith('a') or '+' in fp.mode 155 | 156 | def recv(self, size): 157 | if not self.can_recv: 158 | raise Exception("Unsupported operation") 159 | return self.file.read(size) 160 | 161 | def send(self, data): 162 | if not self.can_send: 163 | raise Exception("Unsupported operation") 164 | return self.file.write(data) 165 | 166 | def close(self): 167 | return self.file.close() 168 | 169 | @property 170 | def closed(self): 171 | return self.file.closed 172 | 173 | def fileno(self): 174 | return self.file.fileno() 175 | 176 | def shutdown(self, how): 177 | if how == socket.SHUT_RDWR: 178 | self.close() 179 | elif how == socket.SHUT_RD: 180 | if not self.can_send: 181 | self.close() 182 | elif how == socket.SHUT_WR: 183 | if not self.can_recv: 184 | self.close() 185 | else: 186 | raise ValueError("invalid shutdown `how`", how) 187 | 188 | def _prep_select(self): 189 | return ((self.file,) if self.can_recv else ()), ((self.file,) if self.can_send else ()), (self.file,) 190 | 191 | 192 | class SimpleDuplex(Simple): 193 | """ 194 | A wrapper which splits recv and send operations across two different 195 | streams. 196 | 197 | :param Simple child_recv: The stream to use for recving 198 | :param Simple child_send: The stream to use for sending 199 | 200 | If either of these parameters are None, that operation will be disabled 201 | and generate exceptions. 202 | """ 203 | 204 | def __init__(self, child_recv=None, child_send=None): 205 | super().__init__() 206 | self.child_recv = child_recv 207 | self.child_send = child_send 208 | 209 | self.can_recv = self.child_recv is not None 210 | self.can_send = self.child_send is not None 211 | 212 | if self.can_recv and not self.child_recv.can_recv: 213 | raise NetcatError("Reading sock cannot be used for recving") 214 | if self.can_send and not self.child_send.can_send: 215 | raise NetcatError("Sending sock cannot be used for sending") 216 | 217 | def recv(self, size): 218 | if not self.can_recv: 219 | raise NetcatError("Unsupported operation") 220 | return self.child_recv.recv(size) 221 | 222 | def send(self, data): 223 | if not self.can_send: 224 | raise NetcatError("Unsupported operation") 225 | return self.child_send.send(data) 226 | 227 | def close(self): 228 | if self.can_recv: 229 | self.child_recv.close() 230 | if self.can_send: 231 | self.child_send.close() 232 | 233 | @property 234 | def closed(self): 235 | if self.can_recv: 236 | return self.child_recv.closed 237 | if self.can_send: 238 | return self.child_send.closed 239 | return True 240 | 241 | def fileno(self): 242 | if self.can_recv: 243 | if self.can_send: 244 | raise NetcatError("Socket has multiple filenos") 245 | return self.child_recv.fileno() 246 | if self.can_send: 247 | return self.child_send.fileno() 248 | raise NetcatError("Socket has no filenos") 249 | 250 | def shutdown(self, how): 251 | # should these filter the how value so recv never sees a send shutdown etc? 252 | if self.can_recv: 253 | self.child_recv.shutdown(how) 254 | if self.can_send: 255 | self.child_send.shutdown(how) 256 | 257 | def _prep_select(self): 258 | rfd = ((), (), ()) if self.child_recv is None else self.child_recv._prep_select() 259 | wfd = ((), (), ()) if self.child_send is None else self.child_send._prep_select() 260 | 261 | return (rfd[0] if self.can_recv else ()), (wfd[1] if self.can_send else ()), rfd[2] + wfd[2] 262 | 263 | class SimpleMerge(Simple): 264 | """ 265 | A stream that merges the output of many readable streams into one. 266 | 267 | :param children: A list of streams from which to read 268 | :type children: list of Simple 269 | """ 270 | def __init__(self, children): 271 | super().__init__() 272 | self.can_send = False 273 | self.can_recv = True 274 | self.children = children 275 | 276 | if any(not child.can_recv for child in children): 277 | raise Exception("Not all children are applicable for receiving") 278 | 279 | def recv(self, size): 280 | goodsocks, _, _ = select(self.children) 281 | if not goodsocks: 282 | raise Exception("What happened???") 283 | 284 | goodsock = goodsocks[0] 285 | return goodsock.recv(size) 286 | 287 | def send(self, data): 288 | raise Exception("Cannot send to a merged socket") 289 | 290 | def close(self): 291 | for child in self.children: 292 | child.close() 293 | 294 | @property 295 | def closed(self): 296 | # TODO: consistency check? 297 | return any(child.closed for child in self.children) 298 | 299 | def fileno(self): 300 | raise Exception("Socket has multiple filenos") 301 | 302 | def shutdown(self, how): 303 | for child in self.children: 304 | child.shutdown(how) 305 | 306 | def _prep_select(self): 307 | stuff = sum((child._prep_select()[0] for child in self.children), ()) 308 | return stuff, (), stuff 309 | 310 | class SimpleNetcat(Simple): 311 | """ 312 | A wrapper for a Netcat object! Why? Just in case you want to do 313 | Netcat-level instrumentation [logging] at a finer granularity than the 314 | top-level. 315 | 316 | :param sock: A Netcat object. 317 | """ 318 | def __init__(self, nc): 319 | super().__init__() 320 | self.nc = nc 321 | self.can_recv = nc.sock.can_recv 322 | self.can_send = nc.sock.can_send 323 | 324 | nc.settimeout(None) 325 | 326 | def recv(self, size): 327 | return self.nc.recv(size) 328 | 329 | def send(self, data): 330 | return self.nc.send(data) 331 | 332 | def close(self): 333 | return self.nc.close() 334 | 335 | @property 336 | def closed(self): 337 | return self.nc.closed 338 | 339 | def fileno(self): 340 | return self.nc.fileno() 341 | 342 | def shutdown(self, how): 343 | return self.nc.shutdown(how) 344 | 345 | def _prep_select(self): 346 | return self.nc._prep_select() 347 | 348 | class SimpleLogger(Simple): 349 | """ 350 | A socket-like interface for dumping data to a python logging endpoint. 351 | 352 | :param logger: The dotted name for the endpoint for the logs to go to 353 | :param level: The string or numeric severity level for the logging 354 | messages 355 | :param encoding: simplesock objects are fed bytestrings. Loggers consume 356 | unicode strings. How should we translate? 357 | """ 358 | def __init__(self, logger='nclib.logs', level='INFO', encoding=None): 359 | super().__init__() 360 | self.logger = logging.getLogger(logger) 361 | self.level = logging._checkLevel(level) 362 | self.encoding = encoding 363 | self._closed = False 364 | 365 | self.can_recv = False 366 | self.can_send = True 367 | 368 | def recv(self, size): 369 | raise NetcatError("Can't recv from a logger object") 370 | 371 | def send(self, data): 372 | if self._closed: 373 | raise NetcatError("I'm closed dumbass!") 374 | 375 | if self.encoding is None: 376 | data = data.decode() 377 | else: 378 | data = data.decode(self.encoding) 379 | self.logger.log(self.level, data) 380 | 381 | def close(self): 382 | self._closed = True 383 | 384 | @property 385 | def closed(self): 386 | return self._closed 387 | 388 | def fileno(self): 389 | raise NetcatError("Can't make a fileno for you") 390 | 391 | def shutdown(self, how): 392 | return None 393 | 394 | def _prep_select(self): 395 | raise NetcatError("Can't be selected") 396 | -------------------------------------------------------------------------------- /nclib/netcat.py: -------------------------------------------------------------------------------- 1 | import getopt 2 | import re 3 | import socket 4 | import sys 5 | import time 6 | from urllib.parse import urlparse 7 | from typing import Optional, Union, Literal 8 | 9 | from . import simplesock, select, errors, logger 10 | 11 | PROTOCAL_RE = re.compile('^[a-z0-9]+://') 12 | KNOWN_SCHEMES = { 13 | # schema: (udp, ipv6, port); None = unchanged 14 | 'tcp': (False, None, None), 15 | 'tcp4': (False, False, None), 16 | 'tcp6': (False, True, None), 17 | 'udp': (True, None, None), 18 | 'udp4': (True, False, None), 19 | 'udp6': (True, True, None), 20 | 'http': (False, None, 80), 21 | 'https': (False, None, 443), 22 | 'dns': (True, None, 53), 23 | 'ftp': (False, None, 20), 24 | 'ssh': (False, None, 22), 25 | 'smtp': (False, None, 25), 26 | } 27 | BYTESISH = Union[bytes, str] 28 | DFLOAT = Union[float, int, Literal['default'], None] 29 | 30 | def encode(b: BYTESISH) -> bytes: 31 | if type(b) is str: 32 | return b.encode() 33 | elif type(b) is bytes: 34 | return b 35 | else: 36 | raise ValueError("Value must be str or bytes (preferably bytes)") 37 | 38 | def _is_ipv6_addr(addr) -> bool: 39 | try: 40 | socket.inet_pton(socket.AF_INET6, addr) 41 | except socket.error: 42 | return False 43 | else: 44 | return True 45 | 46 | class Netcat: 47 | """ 48 | This is the main class you will use to interact with a peer over the 49 | network! You may instantiate this class to either connect to a server, 50 | listen for a one-off client, or wrap an existing sock/pipe/whatever. 51 | 52 | One of the following must be passed in order to initialize a Netcat 53 | object: 54 | 55 | :param connect: the address/port to connect to 56 | :param listen: the address/port to bind to for listening 57 | :param sock: a python socket, pipe, file, etc to wrap 58 | 59 | For ``connect`` and ``listen``, they accept basically any argument format 60 | known to mankind. If you find an input format you think would be useful but 61 | isn't accepted, let me know :P 62 | 63 | Additionally, the following options modify the behavior of the object: 64 | 65 | :param sock_send: If this is specified, this Netcat object will act 66 | as a multiplexer/demultiplexer, using the "normal" 67 | channel for receiving and this channel for sending. 68 | This should be specified as a python socket or pipe 69 | object. 70 | 71 | .. warning:: Using ``sock_send`` will cause issues if 72 | you pass this object into a context which 73 | expects to be able to use its 74 | ``.fileno()``. 75 | 76 | :param udp: Set to True to use udp connections when using the 77 | connect or listen parameters 78 | :param ipv6: Force using ipv6 when using the connect or listen 79 | parameters 80 | :param retry: The number of times to retry establishing a connection 81 | after a short (200ms) sleep if it fails. 82 | :param raise_timeout: 83 | Whether to raise a `NetcatTimeout` exception when a 84 | timeout is received. The default is to return any 85 | buffered data and set ``self.timed_out`` = True 86 | :param raise_eof: Whether to raise a `NetcatEOF` exception when EOF 87 | is encountered. The default is to return any buffered 88 | data and set ``self.eof = True`` 89 | :param loggers: A list of `Logger` objects to consume socket events 90 | for logging. 91 | 92 | The following options can be used to configure default loggers: 93 | 94 | :param log_send: Pass a file-like object open for writing and all 95 | data sent over the socket will be written to it. 96 | :param log_recv: Pass a file-like object open for writing and all 97 | data recieved from the socket will be written to it. 98 | :param verbose: Set to True to cause a log of socket activity to be 99 | written to stderr. 100 | :param echo_headers: 101 | Controls whether stderr logging should print headers 102 | describing network operations and exceptional 103 | conditions. 104 | :param echo_perline: 105 | Controls whether stderr logging should treat newlines 106 | as record separators. 107 | :param echo_hex: Controls whether stderr logging should produce a 108 | hexdump. 109 | :param echo_send_prefix: 110 | A prefix to print to stderr before each logged line of 111 | sent data. 112 | :param echo_recv_prefix: 113 | A prefix to print to stderr before each logged line of 114 | received data. 115 | :param log_yield: Control when logging messages are generated on 116 | recv. By default, logging is done when data is 117 | received from the socket, and may be buffered. 118 | By setting this to True, logging is done when data 119 | is yielded to the user, either directly from the 120 | socket or from a buffer. This affects both stderr 121 | and tee logging. 122 | 123 | Any data that is extracted from the target address will override the 124 | options specified here. For example, a url with the ``http://`` scheme 125 | will go over tcp and port 80. 126 | 127 | You may use this constructor as a context manager, i.e. 128 | ``with nclib.Netcat(...) as nc:``, and the socket will be automatically 129 | closed when control exits the with-block. 130 | 131 | *Example 1:* Send a greeting to a UDP server listening at 192.168.3.6:8888 132 | and wait for a response. Log the conversation to stderr as hex. 133 | 134 | >>> nc = nclib.Netcat(('192.168.3.6', 8888), 135 | ... udp=True, verbose=True, echo_hex=True) 136 | ======= Connected to ('localhost', 8888) ======= 137 | >>> nc.send(b'\\x00\\x0dHello, world!') 138 | ======= Sending 15 bytes ======= 139 | >> 000000 00 0D 48 65 6C 6C 6F 2C 20 77 6F 72 6C 64 21 |..Hello, world! | 140 | >>> response = nc.recv() 141 | ======= Receiving at most 4096 bytes ======= 142 | << 000000 00 57 68 65 6C 6C 6F 20 66 72 69 65 6E 64 2E 20 |.Whello friend. | 143 | << 000010 74 69 6D 65 20 69 73 20 73 68 6F 72 74 2E 20 70 |time is short. p| 144 | << 000020 6C 65 61 73 65 20 74 6F 20 6E 6F 74 20 77 6F 72 |lease to not wor| 145 | << 000030 72 79 2C 20 79 6F 75 20 77 69 6C 6C 20 66 69 6E |ry, you will fin| 146 | << 000040 64 20 79 6F 75 72 20 77 61 79 2E 20 62 75 74 20 |d your way. but | 147 | << 000050 64 6F 20 68 75 72 72 79 2E |do hurry. | 148 | >>> nc.send(b'\\x00\\x08oh no D:') 149 | ======= Sending 10 bytes ======= 150 | >> 00000F 00 | .| 151 | >> 000010 08 6F 68 20 6E 6F 20 44 3A |.oh no D: | 152 | 153 | *Example 2:* Listen for a local TCP connection on port 1234, allow the user 154 | to interact with the client. Log the entire interaction to log.txt. 155 | 156 | >>> logfile = open('log.txt', 'wb') 157 | >>> nc = nclib.Netcat(listen=('localhost', 1234), log_send=logfile, log_recv=logfile) 158 | >>> nc.interact() 159 | """ 160 | 161 | # 162 | # Initializer functions 163 | # 164 | 165 | def __init__(self, connect=None, sock=None, listen=None, 166 | sock_send=None, server=None, 167 | udp=False, ipv6=False, 168 | raise_timeout=False, raise_eof=False, 169 | retry=0, 170 | loggers=None, 171 | 172 | # canned options 173 | verbose=0, 174 | log_send=None, log_recv=None, log_yield=False, 175 | echo_headers=True, echo_perline=True, echo_hex=False, 176 | echo_send_prefix='>> ', echo_recv_prefix='<< ', 177 | ) -> None: 178 | 179 | # handle canned logger options 180 | if loggers is None: 181 | loggers = [] 182 | if verbose: 183 | l = logger.StandardLogger( 184 | _xwrap(sys.stderr), 185 | log_yield=log_yield, 186 | show_headers=echo_headers, 187 | hex_dump=echo_hex, 188 | split_newlines=echo_perline, 189 | send_prefix=echo_send_prefix, 190 | recv_prefix=echo_recv_prefix) 191 | loggers.append(l) 192 | if log_send is not None or log_recv is not None: 193 | l = logger.TeeLogger( 194 | log_send=_xwrap(log_send) if log_send is not None else None, 195 | log_recv=_xwrap(log_recv) if log_recv is not None else None, 196 | log_yield=log_yield) 197 | loggers.append(l) 198 | 199 | # set properties 200 | self.logger = logger.ManyLogger(loggers) 201 | self.buf = b'' 202 | self.sock = None 203 | self.peer = None 204 | 205 | self.timed_out = False # set when an operation times out 206 | self.eof = False 207 | self._raise_timeout = raise_timeout 208 | self._raise_eof = raise_eof 209 | 210 | # handle several "convenient" args-passing cases 211 | # case: Netcat(host, port) 212 | if isinstance(connect, str) and isinstance(sock, int): 213 | connect = (connect, sock) 214 | sock = None 215 | 216 | # case: Netcat(sock) 217 | if hasattr(connect, 'read') or hasattr(connect, 'recv'): 218 | sock = connect 219 | connect = None 220 | 221 | # server= as alias for connect= 222 | if server is not None: 223 | connect = server 224 | 225 | # sanity checks 226 | if sock is None and listen is None and connect is None: 227 | raise ValueError('Not enough arguments, need at least an ' 228 | 'address or a socket or a listening address!') 229 | 230 | if listen is not None and connect is not None: 231 | raise ValueError("connect and listen arguments cannot be provided at the same time") 232 | 233 | # three cases: 1) already have a sock 2) need to do a connect 3) need to do a listen 234 | if sock is None: 235 | if listen is not None: 236 | target = listen 237 | listen = True 238 | else: 239 | target = connect 240 | listen = False 241 | 242 | target, listen, udp, ipv6 = self._parse_target(target, listen, udp, ipv6) 243 | self._connect(target, listen, udp, ipv6, int(retry)) 244 | else: 245 | self.sock = sock 246 | self.peer = connect 247 | 248 | # extract the timeout from the sock before we wrap it in the simplesock 249 | try: 250 | self._timeout = self.sock.gettimeout() 251 | except AttributeError: 252 | self._timeout = None 253 | 254 | # do simplesock wrapping and take sock_send into account 255 | self.sock = _xwrap(self.sock) 256 | if sock_send is not None: 257 | self.sock = simplesock.SimpleDuplex(self.sock, _xwrap(sock_send)) 258 | 259 | @staticmethod 260 | def _parse_target(target, listen, udp, ipv6): 261 | """ 262 | Takes the basic version of the user args and extract as much data as 263 | possible from target. Returns a tuple that is its arguments but 264 | sanitized. 265 | """ 266 | if isinstance(target, str): 267 | if target.startswith('nc '): 268 | out_host = None 269 | out_port = None 270 | 271 | try: 272 | opts, pieces = getopt.getopt(target.split()[1:], 'u46lp:', 273 | []) 274 | except getopt.GetoptError as exc: 275 | raise ValueError(exc) from exc 276 | 277 | for opt, arg in opts: 278 | if opt == '-u': 279 | udp = True 280 | elif opt == '-4': 281 | ipv6 = False 282 | elif opt == '-6': 283 | ipv6 = True 284 | elif opt == '-l': 285 | listen = True 286 | elif opt == '-p': 287 | out_port = int(arg) 288 | else: 289 | assert False, "unhandled option" 290 | 291 | if not pieces: 292 | pass 293 | elif len(pieces) == 1: 294 | if listen and pieces[0].isdigit(): 295 | out_port = int(pieces[0]) 296 | else: 297 | out_host = pieces[0] 298 | elif len(pieces) == 2 and pieces[1].isdigit(): 299 | out_host = pieces[0] 300 | out_port = int(pieces[1]) 301 | else: 302 | raise ValueError("Bad cmdline: %s" % target) 303 | 304 | if out_host is None: 305 | if listen: 306 | out_host = '::' if ipv6 else '0.0.0.0' 307 | else: 308 | raise ValueError("Missing address: %s" % target) 309 | if out_port is None: 310 | raise ValueError("Missing port: %s" % target) 311 | 312 | if _is_ipv6_addr(out_host): 313 | ipv6 = True 314 | 315 | return (out_host, out_port), listen, udp, ipv6 316 | 317 | elif PROTOCAL_RE.match(target) is not None: 318 | parsed = urlparse(target) 319 | port = None 320 | 321 | try: 322 | scheme_udp, scheme_ipv6, scheme_port = KNOWN_SCHEMES[parsed.scheme] 323 | except KeyError: 324 | raise ValueError("Unknown scheme: %s" % parsed.scheme) from None 325 | 326 | if scheme_udp is not None: 327 | udp = scheme_udp 328 | if scheme_ipv6 is not None: 329 | ipv6 = scheme_ipv6 330 | if scheme_port is not None: 331 | port = scheme_port 332 | 333 | if parsed.netloc.startswith('['): 334 | addr, extra = parsed.netloc[1:].split(']', 1) 335 | if extra.startswith(':'): 336 | port = int(extra[1:]) 337 | else: 338 | if ':' in parsed.netloc: 339 | addr, port = parsed.netloc.split(':', 1) 340 | port = int(port) 341 | else: 342 | addr = parsed.netloc 343 | 344 | if addr is None or port is None: 345 | raise ValueError("Can't parse addr/port from %s" % target) 346 | 347 | if _is_ipv6_addr(addr): 348 | ipv6 = True 349 | 350 | return (addr, port), listen, udp, ipv6 351 | 352 | else: 353 | if target.startswith('['): 354 | addr, extra = target[1:].split(']', 1) 355 | if extra.startswith(':'): 356 | port = int(extra[1:]) 357 | else: 358 | port = None 359 | else: 360 | if ':' in target: 361 | addr, port = target.split(':', 1) 362 | port = int(port) 363 | else: 364 | addr = target 365 | port = None 366 | 367 | if port is None: 368 | raise ValueError("No port given: %s" % target) 369 | 370 | if _is_ipv6_addr(addr): 371 | ipv6 = True 372 | 373 | return (addr, port), listen, udp, ipv6 374 | 375 | elif isinstance(target, int): 376 | if listen: 377 | out_port = target 378 | else: 379 | raise ValueError("Can't deal with number as connection address") 380 | 381 | return ('::' if ipv6 else '0.0.0.0', out_port), listen, udp, ipv6 382 | 383 | elif isinstance(target, tuple): 384 | if len(target) >= 1 and isinstance(target[0], str) and _is_ipv6_addr(target[0]): 385 | ipv6 = True 386 | return target, listen, udp, ipv6 387 | 388 | else: 389 | raise ValueError("Can't parse target: %r" % target) 390 | 391 | def _connect(self, target, listen, udp, ipv6, retry): 392 | """ 393 | Takes target/listen/udp/ipv6 and sets self.sock and self.peer 394 | """ 395 | ty = socket.SOCK_DGRAM if udp else socket.SOCK_STREAM 396 | fam = socket.AF_INET6 if ipv6 else socket.AF_INET 397 | self.sock = socket.socket(fam, ty) 398 | if listen: 399 | self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 400 | self.sock.bind(target) 401 | if not udp: 402 | self.sock.listen(1) 403 | conn, addr = self.sock.accept() 404 | self.sock.close() 405 | self.sock = conn 406 | self.peer = addr 407 | else: 408 | self.buf, self.peer = self.sock.recvfrom(4096) 409 | self.sock.connect(self.peer) 410 | self.logger.buffering(self.buf) 411 | else: 412 | while retry >= 0: 413 | try: 414 | self.sock.connect(target) 415 | except (socket.gaierror, socket.herror) as exc: 416 | raise errors.NetcatError('Could not connect to %r' \ 417 | % (target,)) from exc 418 | except socket.error as exc: 419 | if retry: 420 | time.sleep(0.2) 421 | retry -= 1 422 | else: 423 | raise errors.NetcatError('Could not connect to %r:' \ 424 | % (target,)) from exc 425 | else: 426 | break 427 | self.peer = target 428 | self.logger.connected(self.peer) 429 | 430 | def __enter__(self) -> "Netcat": 431 | return self 432 | 433 | def __exit__(self, ty, val, tb) -> None: 434 | self.close() 435 | 436 | def add_logger(self, l) -> None: 437 | """ 438 | Add the given logger to the list of current loggers 439 | """ 440 | self.logger.children.append(l) 441 | 442 | def remove_logger(self, l) -> None: 443 | """ 444 | Remove the given logger from the list of current loggers 445 | """ 446 | self.logger.children.remove(l) 447 | 448 | # 449 | # Socket metadata functionality 450 | # 451 | 452 | def close(self) -> None: 453 | """ 454 | Close the socket. 455 | """ 456 | return self.sock.close() 457 | 458 | # inconsistent between sockets and files. support both 459 | @property 460 | def closed(self) -> bool: 461 | """ 462 | Whether the socket has been closed by the user (not the peer). 463 | """ 464 | return self.sock.closed 465 | 466 | @property 467 | def _closed(self) -> bool: 468 | return self.closed 469 | 470 | def shutdown(self, how=socket.SHUT_RDWR) -> None: 471 | """ 472 | Send a shutdown signal for one or both of reading and writing. Valid 473 | arguments are ``socket.SHUT_RDWR``, ``socket.SHUT_RD``, and 474 | ``socket.SHUT_WR``. 475 | 476 | Shutdown differs from closing in that it explicitly changes the state of 477 | the socket resource to closed, whereas closing will only decrement the 478 | number of peers on this end of the socket, since sockets can be a 479 | resource shared by multiple peers on a single OS. When the number of 480 | peers reaches zero, the socket is closed, but not deallocated, so you 481 | still need to call close. (except that this is python and close is 482 | automatically called on the deletion of the socket) 483 | 484 | http://stackoverflow.com/questions/409783/socket-shutdown-vs-socket-close 485 | """ 486 | return self.sock.shutdown(how) 487 | 488 | def shutdown_rd(self) -> None: 489 | """ 490 | Send a shutdown signal for reading - you may no longer read from this 491 | socket. 492 | """ 493 | return self.shutdown(socket.SHUT_RD) 494 | 495 | def shutdown_wr(self) -> None: 496 | """ 497 | Send a shutdown signal for writing - you may no longer write to this 498 | socket. 499 | """ 500 | return self.shutdown(socket.SHUT_WR) 501 | 502 | def fileno(self) -> int: 503 | """ 504 | Return the file descriptor associated with this socket 505 | """ 506 | return self.sock.fileno() 507 | 508 | def settimeout(self, timeout) -> None: 509 | """ 510 | Set the default timeout in seconds to use for subsequent socket 511 | operations. Set to None to wait forever, or 0 to be effectively 512 | nonblocking. 513 | """ 514 | self._timeout = timeout 515 | 516 | def gettimeout(self) -> Optional[float]: 517 | """ 518 | Retrieve the timeout currently associated with the socket 519 | """ 520 | return self._timeout 521 | 522 | def flush(self) -> None: 523 | # no output buffering 524 | pass 525 | 526 | def _prep_select(self): 527 | return self.sock._prep_select() 528 | 529 | # 530 | # Core socket data functionality 531 | # 532 | 533 | def _send(self, data: bytes) -> int: 534 | ret = self.sock.send(data) 535 | self.logger.sending(data[:ret]) 536 | return ret 537 | 538 | def _recv(self, size: int, timeout: Optional[float]=None) -> bytes: 539 | """ 540 | one-shot recv with timeout. 541 | all timeouts are expressed via raising errors.NetcatTimeout 542 | we wait until data is ready and then recv. 543 | TODO: this is not thread safe... 544 | """ 545 | if timeout is not None: 546 | r, _, _ = select.select([self.sock], timeout=timeout) # pylint: disable=no-member 547 | if not r: 548 | raise errors.NetcatTimeout() 549 | try: 550 | data = self.sock.recv(size) 551 | except ConnectionResetError: 552 | data = b'' 553 | self.logger.buffering(data) 554 | return data 555 | 556 | def _recv_predicate(self, predicate, timeout: Optional[float], raise_eof: Optional[bool]=None) -> bytes: 557 | """ 558 | this is the core function which ties together all the nclib features 559 | it will buffer data and call the predicate function on the buffer 560 | until it returns a positive integer: the amount to unbuffer. 561 | """ 562 | if timeout is None: 563 | deadline = None 564 | else: 565 | deadline = time.time() + timeout 566 | self.timed_out = False 567 | 568 | if raise_eof is None: 569 | raise_eof = self._raise_eof 570 | 571 | try: 572 | first_shot = True 573 | while True: 574 | # step 1: check if the needed data is buffered. 575 | # if so set cut_at and break out 576 | cut_at = predicate(self.buf) 577 | if cut_at > 0: 578 | break 579 | 580 | # step 2: calculate timeout for this read. 581 | # if it's elapsed, raise error 582 | if deadline is not None: 583 | timeout = deadline - time.time() 584 | if timeout < 0: 585 | if first_shot: 586 | timeout = 0 587 | else: 588 | raise errors.NetcatTimeout() 589 | first_shot = False 590 | 591 | # step 3: receive a chunk with timeout and buffer it 592 | data = self._recv(4096, timeout) 593 | self.buf += data 594 | 595 | # step 4: handle EOF. raise_eof=False should mean return the 596 | # rest of the buffer regardless of predicate 597 | if not data: 598 | self.eof = True 599 | self.logger.eofed() 600 | if raise_eof: 601 | raise errors.NetcatEOF("Connection dropped!") 602 | cut_at = len(self.buf) 603 | break 604 | self.eof = False 605 | 606 | # handle interrupt 607 | except KeyboardInterrupt: 608 | self.logger.interrupted() 609 | raise 610 | 611 | # handle timeout. needs to be done this way since recv may raise 612 | # timeout too 613 | except errors.NetcatTimeout: 614 | self.timed_out = True 615 | if self._raise_timeout: 616 | raise 617 | cut_at = len(self.buf) 618 | 619 | # handle arbitrary socket errors. should this be moved inward? 620 | except socket.error as e: 621 | raise errors.NetcatError('Socket error') from e 622 | 623 | # unbuffer whatever we need to return 624 | ret = self.buf[:cut_at] 625 | self.buf = self.buf[cut_at:] 626 | self.logger.unbuffering(ret) 627 | return ret 628 | 629 | # 630 | # Public socket data functions 631 | # 632 | 633 | def _fixup_timeout(self, timeout: DFLOAT = 'default') -> Optional[float]: 634 | if timeout == 'default': 635 | return self._timeout 636 | return timeout 637 | 638 | def recv(self, n: int=4096, timeout: DFLOAT = 'default') -> bytes: 639 | """ 640 | Receive at most n bytes (default 4096) from the socket 641 | 642 | Aliases: read, get 643 | """ 644 | 645 | timeout = self._fixup_timeout(timeout) 646 | self.logger.requesting_recv(n, timeout) 647 | return self._recv_predicate(lambda s: min(n, len(s)), timeout) 648 | 649 | def recv_until(self, s: BYTESISH, max_size: Optional[int]=None, timeout: DFLOAT = 'default') -> bytes: 650 | """ 651 | Recieve data from the socket until the given substring is observed. 652 | Data in the same datagram as the substring, following the substring, 653 | will not be returned and will be cached for future receives. 654 | 655 | Aliases: read_until, readuntil, recvuntil 656 | """ 657 | s = encode(s) 658 | timeout = self._fixup_timeout(timeout) 659 | self.logger.requesting_recv_until(s, max_size, timeout) 660 | 661 | if max_size is None: 662 | max_size = 2 ** 62 663 | 664 | def _predicate(buf): 665 | try: 666 | return min(buf.index(s) + len(s), max_size) 667 | except ValueError: 668 | return 0 if len(buf) < max_size else max_size 669 | return self._recv_predicate(_predicate, timeout) 670 | 671 | def recv_all(self, timeout: DFLOAT = 'default') -> bytes: 672 | """ 673 | Return all data recieved until connection closes or the timeout 674 | elapses. 675 | 676 | Aliases: read_all, readall, recvall 677 | """ 678 | 679 | timeout = self._fixup_timeout(timeout) 680 | self.logger.requesting_recv_all(timeout) 681 | return self._recv_predicate(lambda s: 0, timeout, raise_eof=False) 682 | 683 | def recv_exactly(self, n: int, timeout: DFLOAT = 'default') -> bytes: 684 | """ 685 | Recieve exactly n bytes 686 | 687 | Aliases: read_exactly, readexactly, recvexactly, recv_exact, 688 | read_exact, readexact, recvexact 689 | """ 690 | 691 | timeout = self._fixup_timeout(timeout) 692 | self.logger.requesting_recv_exactly(n, timeout) 693 | return self._recv_predicate(lambda s: n if len(s) >= n else 0, timeout) 694 | 695 | def send(self, s: BYTESISH) -> int: 696 | """ 697 | Sends all the given data to the socket. 698 | 699 | Aliases: write, put, sendall, send_all 700 | """ 701 | s = encode(s) 702 | self.logger.requesting_send(s) 703 | 704 | out = len(s) 705 | while s: 706 | s = s[self._send(s):] 707 | return out 708 | 709 | def interact(self, insock=sys.stdin, outsock=sys.stdout) -> None: 710 | """ 711 | Connects the socket to the terminal for user interaction. 712 | Alternate input and output files may be specified. 713 | 714 | This method cannot be used with a timeout. 715 | 716 | Aliases: interactive, interaction 717 | """ 718 | self.logger.interact_starting() 719 | other = Netcat(simplesock.SimpleDuplex(_xwrap(insock), _xwrap(outsock))) 720 | ferry(self, other, suppress_timeout=True, suppress_raise_eof=True) 721 | self.logger.interact_ending() 722 | 723 | # 724 | # Public socket data functionality 725 | # (implemented with other public socket data functions) 726 | # 727 | 728 | LINE_ENDING = b'\n' 729 | 730 | def recv_line(self, max_size: Optional[int]=None, timeout: DFLOAT = 'default', ending: Optional[BYTESISH]=None) -> bytes: 731 | """ 732 | Recieve until the next newline , default "\\n". The newline string can 733 | be changed by changing ``nc.LINE_ENDING``. The newline will be returned 734 | as part of the string. 735 | 736 | Aliases: recvline, readline, read_line, readln, recvln 737 | """ 738 | if ending is None: 739 | ending = self.LINE_ENDING 740 | return self.recv_until(ending, max_size, timeout) 741 | 742 | def send_line(self, line: BYTESISH, ending: Optional[BYTESISH]=None) -> int: 743 | """ 744 | Write the string to the wire, followed by a newline. The newline string 745 | can be changed by specifying the ``ending`` param or changing 746 | ``nc.LINE_ENDING``. 747 | 748 | Aliases: sendline, writeline, write_line, writeln, sendln 749 | """ 750 | if ending is None: 751 | ending = self.LINE_ENDING 752 | ending = encode(ending) 753 | line = encode(line) 754 | return self.send(line + ending) 755 | 756 | # 757 | # Aliases :D 758 | # 759 | 760 | read = recv 761 | get = recv 762 | 763 | write = send 764 | put = send 765 | sendall = send 766 | send_all = send 767 | 768 | read_until = recv_until 769 | readuntil = recv_until 770 | recvuntil = recv_until 771 | 772 | read_all = recv_all 773 | readall = recv_all 774 | recvall = recv_all 775 | 776 | read_exactly = recv_exactly 777 | readexactly = recv_exactly 778 | recvexactly = recv_exactly 779 | recv_exact = recv_exactly 780 | read_exact = recv_exactly 781 | readexact = recv_exactly 782 | recvexact = recv_exactly 783 | 784 | interactive = interact 785 | ineraction = interact 786 | 787 | recvline = recv_line 788 | readline = recv_line 789 | read_line = recv_line 790 | readln = recv_line 791 | recvln = recv_line 792 | 793 | sendline = send_line 794 | writeline = send_line 795 | write_line = send_line 796 | writeln = send_line 797 | sendln = send_line 798 | 799 | def merge(children, **kwargs) -> Netcat: 800 | """ 801 | Return a Netcat object whose receives will be the merged stream of all the 802 | given children sockets. 803 | 804 | :param children: A list of socks of any kind to receive from 805 | :param kwargs: Any additional keyword arguments will be passed on to 806 | the Netcat constructor. Notably, you might want to 807 | specify `sock_send`, since by default you will not 808 | be able to send data to a merged socket. 809 | """ 810 | nice_children = [_xwrap(child) for child in children] 811 | return Netcat(simplesock.SimpleMerge(nice_children), **kwargs) 812 | 813 | def _xwrap(sock): 814 | """ 815 | like simplesock.wrap but will also *unwrap* Netcat objects into their 816 | constituent sockets. Be warned that this will discard buffers. 817 | """ 818 | return sock.sock if isinstance(sock, Netcat) else simplesock.wrap(sock) 819 | 820 | 821 | def ferry(left, right, ferry_left=True, ferry_right=True, 822 | suppress_timeout=True, suppress_raise_eof=False) -> None: 823 | """ 824 | Establish a linkage between two socks, automatically copying any data 825 | that becomes available between the two. 826 | 827 | :param left: A netcat sock 828 | :param right: Another netcat sock 829 | :param ferry_left: Whether to copy data leftward, i.e. from the 830 | right sock to the left sock 831 | :param ferry_right: Whether to copy data rightward, i.e. from the 832 | left sock to the right sock 833 | :param suppress_timeout: Whether to automatically set the socks' 834 | timeout property to None and then reset it at 835 | the end 836 | :param suppress_raise_eof: Whether to automatically set the socks' 837 | raise_eof property to None and then reset it at 838 | the end 839 | """ 840 | 841 | left_timeout = left._timeout 842 | left_raise_eof = left._raise_eof 843 | right_timeout = right._timeout 844 | right_raise_eof = right._raise_eof 845 | 846 | selectable = [] 847 | if ferry_left: 848 | selectable.append(right) 849 | if ferry_right: 850 | selectable.append(left) 851 | if not selectable: 852 | return 853 | 854 | try: 855 | if suppress_timeout: 856 | left._timeout = None 857 | right._timeout = None 858 | if suppress_raise_eof: 859 | left._raise_eof = False 860 | right._raise_eof = False 861 | 862 | while True: 863 | r, _, _ = select.select(selectable) # pylint: disable=no-member 864 | for readable in r: 865 | data = readable.recv() 866 | if not data: 867 | raise errors.NetcatEOF() 868 | 869 | if readable is left: 870 | right.send(data) 871 | else: 872 | left.send(data) 873 | except (KeyboardInterrupt, errors.NetcatEOF): 874 | pass 875 | finally: 876 | if suppress_timeout: 877 | left._timeout = left_timeout 878 | right._timeout = right_timeout 879 | if suppress_raise_eof: 880 | left._raise_eof = left_raise_eof 881 | right._raise_eof = right_raise_eof 882 | 883 | 884 | # congrats, you've found the secret in-progress command-line python netcat! it barely works. 885 | #def add_arg(arg, options, args): 886 | # if arg in ('v',): 887 | # options['verbose'] += 1 888 | # elif arg in ('l',): 889 | # options['listen'] = True 890 | # elif arg in ('k',): 891 | # options['listenmore'] = True 892 | # else: 893 | # raise NetcatError('Bad argument: %s' % arg) 894 | # 895 | #def usage(verbose=False): 896 | # print """Usage: %s [-vlk] hostname port""" % sys.argv[0] 897 | # if verbose: 898 | # print """More help coming soon :)""" 899 | # 900 | #def main(*args_list): 901 | # args = iter(args_list) 902 | # args.next() 903 | # hostname = None 904 | # port = None 905 | # options = {'verbose': False, 'listen': False, 'listenmore': False} 906 | # for arg in args: 907 | # if arg.startswith('--'): 908 | # add_arg(arg, options, args) 909 | # elif arg.startswith('-'): 910 | # for argchar in arg[1:]: 911 | # add_arg(argchar, options, args) 912 | # else: 913 | # if arg.isdigit(): 914 | # if port is not None: 915 | # if hostname is not None: 916 | # usage() 917 | # raise NetcatError('Already specified hostname and port: %s' % arg) 918 | # hostname = port # on the off chance the host is totally numeric :P 919 | # port = int(arg) 920 | # else: 921 | # if hostname is not None: 922 | # usage() 923 | # raise NetcatError('Already specified hostname: %s' % arg) 924 | # hostname = arg 925 | # if port is None: 926 | # usage() 927 | # raise NetcatError('No port specified!') 928 | # if options['listen']: 929 | # hostname = '0.0.0.0' if hostname is None else hostname 930 | # while True: 931 | # Netcat(listen=(hostname, port), verbose=options['verbose']).interact() 932 | # if not options['listenmore']: 933 | # break 934 | # else: 935 | # if hostname is None: 936 | # usage() 937 | # raise NetcatError('No hostname specified!') 938 | # Netcat(server=(hostname, port), verbose=options['verbose']).interact() 939 | # 940 | # 941 | #if __name__ == '__main__': 942 | # main(*sys.argv) 943 | --------------------------------------------------------------------------------