pax_global_header00006660000000000000000000000064143212767240014522gustar00rootroot0000000000000052 comment=a2e23a2a38f1bb110f90eb77b60fd9a6be62e42d txi2p-0.3.7/000077500000000000000000000000001432127672400125775ustar00rootroot00000000000000txi2p-0.3.7/.circleci/000077500000000000000000000000001432127672400144325ustar00rootroot00000000000000txi2p-0.3.7/.circleci/config.yml000066400000000000000000000114371432127672400164300ustar00rootroot00000000000000version: 2.1 orbs: # Get easier access to the Windows machine executor. win: "circleci/windows@4.1.1" jobs: build-wheel: docker: - image: "cimg/python:3.10" steps: - "checkout" - run: name: "Build Wheel" command: | python -m pip install build python -m build - store_artifacts: path: "./dist/" - persist_to_workspace: root: "./dist/" paths: - "*" pypi-upload: parameters: repository: type: "string" docker: - image: "cimg/python:3.10" environment: TWINE_REPOSITORY: "<< parameters.repository >>" TWINE_USERNAME: "__token__" steps: - attach_workspace: at: "release-workspace" - run: name: "Upload Wheel" command: | set -eux -o pipefail if [ "<< parameters.repository >>" = "testpypi" ]; then if [ -v TESTPYPI_API_TOKEN ]; then export TWINE_PASSWORD="${TESTPYPI_API_TOKEN}" fi elif [ "<< parameters.repository >>" = "pypi" ]; then if [ -v PYPI_API_TOKEN ]; then export TWINE_PASSWORD="${PYPI_API_TOKEN}" fi else echo "Unknown repository: << parameters.repository >>" exit 1 fi if [ -v TWINE_PASSWORD ]; then python -m pip install twine python -m twine upload --non-interactive release-workspace/* else # If we're building a from a forked repository then we're # allowed to not have the credentials (but it's als fine of the # owner of the fork supplied their own). # # https://circleci.com/docs/built-in-environment-variables/ says # about `CIRCLE_PR_REPONAME`: # # The name of the GitHub or Bitbucket repository where the # pull request was created. Only available on forked PRs. # # So if it is not set then we should have had credentials and we # fail if we get here. if ! [ -v CIRCLE_PR_REPONAME ]; then echo "Required credentials (<>) are missing." exit 1 fi fi tests: parameters: image-base: type: "string" python-version: type: "string" twisted-version: type: "string" docker: - image: "<< parameters.image-base >>:<< parameters.python-version >>" steps: - "checkout" - run: name: "Setup Test Environment" command: | python -m pip install coverage coveralls .[test] case << parameters.twisted-version >> in current) TW="Twisted" ;; trunk) TW="git+https://github.com/twisted/twisted.git" ;; *) TW="Twisted==<< parameters.twisted-version >>" ;; esac python -m pip install --upgrade "$TW" - run: name: "Run Tests" command: | python -m coverage run --source=txi2p --omit=*/_version.py,*test* -m twisted.trial txi2p python run_coveralls.py workflows: version: 2 release: jobs: - "build-wheel": filters: tags: only: "/^\\d+\\.\\d+\\.\\d+$/" branches: ignore: "/.*/" - "pypi-upload": filters: tags: only: "/^\\d+\\.\\d+\\.\\d+$/" branches: ignore: "/.*/" repository: "pypi" requires: - "build-wheel" tests: jobs: - "build-wheel" - "pypi-upload": repository: "testpypi" requires: - "build-wheel" - "tests": matrix: parameters: image-base: - "cimg/python" python-version: - "3.7" - "3.8" - "3.9" - "3.10" twisted-version: - "trunk" - "current" - "22.2.0" - "19.2.1" - "tests": matrix: parameters: image-base: - "pypy" python-version: - "3.7" - "3.8" - "3.9" twisted-version: - "trunk" - "current" - "22.2.0" - "19.2.1" - "tests": matrix: parameters: image-base: - "pypy" - "cimg/python" python-version: - "2.7" twisted-version: - "20.3.0" txi2p-0.3.7/.gitignore000066400000000000000000000002011432127672400145600ustar00rootroot00000000000000*.pyc *.swp *~ .coverage .eggs/ coverage/ dropin.cache env/ _trial_temp/ build/ dist/ docs/_build txi2p.egg-info/ _version.py txi2p-0.3.7/COPYING000066400000000000000000000016351432127672400136370ustar00rootroot00000000000000Copyright (c) 2013, str4d Overall design and flow of client-side was inspired by https://github.com/habnabit/txsocksx (and some testing code was reused). Copyright (c) 2010-2013, Aaron Gallagher <_@habnab.it> Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. txi2p-0.3.7/MANIFEST.in000066400000000000000000000001071432127672400143330ustar00rootroot00000000000000graft examples include version.txt requirements.txt COPYING README.rst txi2p-0.3.7/README.rst000066400000000000000000000115441432127672400142730ustar00rootroot00000000000000=========== txi2p-tahoe =========== This is a hopefully temporary fork of txi2p_, to help Tahoe-LAFS_ project to get unstuck in Python 3 porting efforts. .. _txi2p: https://pypi.org/project/txi2p/ .. _Tahoe-LAFS: https://pypi.org/project/tahoe-lafs/ .. image:: https://api.travis-ci.org/str4d/txi2p.svg?branch=master :target: https://www.travis-ci.org/str4d/txi2p :alt: travis .. image:: https://coveralls.io/repos/github/str4d/txi2p/badge.svg?branch=master :target: https://coveralls.io/github/str4d/txi2p?branch=master :alt: coveralls |txi2p| is a set of I2P bindings for `Twisted `_ 10.1 or greater. It currently requires Python 2. |txi2p| will run on Python 3.3+ (requiring `Twisted`_ 15.4 or greater). |txi2p| supports both the SAM and BOB APIs for I2P. The default API is SAM. Installation ============ You can install |txi2p| from PyPI:: $ pip install txi2p-tahoe or by downloading the source and running:: $ pip install . inside the source directory. Quickstart ========== If you are not familiar with using endpoints or endpoint strings, read the `Twisted endpoints`_ documentation. .. _Twisted endpoints: https://twistedmatrix.com/documents/current/core/howto/endpoints.html Using endpoint classes ---------------------- To connect to an I2P site:: from twisted.internet import reactor from twisted.internet.endpoints import clientFromString from txi2p.sam import SAMI2PStreamClientEndpoint samEndpoint = clientFromString(reactor, 'tcp:127.0.0.1:7656') endpoint = SAMI2PStreamClientEndpoint.new(samEndpoint, 'stats.i2p') d = endpoint.connect(factory) To have a server listen on an I2P Destination:: from twisted.internet import reactor from twisted.internet.endpoints import clientFromString from txi2p.sam import SAMI2PStreamServerEndpoint samEndpoint = clientFromString(reactor, 'tcp:127.0.0.1:7656') endpoint = SAMI2PStreamServerEndpoint.new(samEndpoint, '/path/to/keyfile') d = endpoint.listen(factory) Using endpoint strings ---------------------- Requires `Twisted`_ 14.0 or greater. To connect to an I2P site:: from twisted.internet import reactor from twisted.internet.endpoints import clientFromString endpoint = clientFromString(reactor, 'i2p:stats.i2p') d = endpoint.connect(factory) To have a server listen on an I2P Destination:: from twisted.internet import reactor from twisted.internet.endpoints import serverFromString endpoint = serverFromString(reactor, 'i2p:/path/to/keyfile') d = endpoint.listen(factory) To connect using a specific API:: from twisted.internet import reactor from twisted.internet.endpoints import clientFromString endpoint = clientFromString(reactor, 'i2p:stats.i2p:api=BOB') d = endpoint.connect(factory) To connect using a non-standard API host or port:: from twisted.internet import reactor from twisted.internet.endpoints import clientFromString endpoint = clientFromString(reactor, 'i2p:stats.i2p:api=SAM:apiEndpoint=tcp\:127.0.0.1\:31337') d = endpoint.connect(factory) Endpoint strings ================ The Twisted plugin for |clientFromString| and |serverFromString| will only work for `Twisted`_ 14.0 or greater. Both client and server strings support the following keyword arguments: * ``api=`` - Either ``SAM`` or ``BOB``. * ``apiEndpoint=`` - An escaped client endpoint string pointing to the API, e.g. ``tcp\:127.0.0.1\:2827``. * ``options=keyOne\:valueOne,keyTwo\:valueTwo`` - I2CP options as a comma-separated key:value list. See the `I2CP specification` for available options. .. _I2CP specification: https://geti2p.net/en/docs/protocol/i2cp Clients ------- Client string format:: i2p:[:port][:key=value]* Supported arguments: **SAM** * ``nickname`` * ``autoClose`` * ``keyfile`` * ``localPort`` * ``sigType`` **BOB** * ``tunnelNick`` * ``inhost`` * ``inport`` Servers ------- Server string format:: i2p:[:port][:key=value]* Supported arguments: **SAM** * ``nickname`` * ``autoClose`` * ``sigType`` **BOB** * ``tunnelNick`` * ``outhost`` * ``outport`` Important changes ================= 0.3.2 ----- * The default signature type for new Destinations is Ed25519. * If the SAM server does not support that (Java I2P 0.9.16 and earlier), txi2p will fall back on ECDSA_SHA256_P256, followed by the old default DSA_SHA1. 0.3 --- * Ports are now supported on the SAM API. * Previous ``port`` options are no longer ignored. * New ``localPort`` option for setting the client's local port. * The ``SAMI2PStreamServerEndpoint`` API has changed to no longer require a reactor. Documentation ============= API documentation is available at https://txi2p.readthedocs.org .. |txi2p| replace:: ``txi2p`` .. |clientFromString| replace:: ``clientFromString()`` .. |serverFromString| replace:: ``serverFromString()`` txi2p-0.3.7/TODO000066400000000000000000000022071432127672400132700ustar00rootroot00000000000000- Use Twisted for logging/debug - Handle more errors properly - Only run BOB tunnel creation Protocol for first Protocol created by the Factory, shortcut the process for subsequent Protocols. - Does this matter? Will a Factory ever have multiple Protocols at once? - For clients, the tunnel dies with the first Protocol - If a tunnel exists and its settings match what the tunnel creator wants, don't stop and start the tunnel - Important for a server tunnel used multiple times by client endpoints, first client will stop the tunnel and add inport, later clients should not (unless the inport is explicitly specified by the client tunnel and does not match) - Examine design, decide where I2P tunnels should be created and removed - Client and server tunnels are created by the endpoint - Once the tunnel is created, the real Protocol/ListeningPort is created and returned via a Deferred callback chain - Should the tunnels be created by the Protocol/ListeningPort instead? - Client tunnels are removed by I2PClientTunnelProtocol.connectionLost() - Server tunnels are removed by I2PListeningPort.stopListening() txi2p-0.3.7/docs/000077500000000000000000000000001432127672400135275ustar00rootroot00000000000000txi2p-0.3.7/docs/Makefile000066400000000000000000000163551432127672400152010ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " applehelp to make an Apple Help Book" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage to run coverage check of the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/txi2p.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/txi2p.qhc" applehelp: $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp @echo @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." @echo "N.B. You won't be able to view it unless you put it in" \ "~/Library/Documentation/Help or install it in your application" \ "bundle." devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/txi2p" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/txi2p" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." coverage: $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ "results in $(BUILDDIR)/coverage/python.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." txi2p-0.3.7/docs/conf.py000066400000000000000000000223231432127672400150300ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # txi2p documentation build configuration file, created by # sphinx-quickstart on Fri Sep 18 01:15:49 2015. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys import os import shlex import vcversioner # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath('..')) sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.napoleon', 'sphinx.ext.todo', 'sphinx.ext.coverage', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'txi2p' copyright = u'2015, str4d' author = u'str4d' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # version = release = vcversioner.find_version(root='..').version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all # documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. #keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = True # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'alabaster' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. #html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' #html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # Now only 'ja' uses this config value #html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. #html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = 'txi2pdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', # Latex figure (float) alignment #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'txi2p.tex', u'txi2p Documentation', u'str4d', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'txi2p', u'txi2p Documentation', [author], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'txi2p', u'txi2p Documentation', author, 'txi2p', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { 'python': ('https://docs.python.org/', None), } txi2p-0.3.7/docs/index.rst000066400000000000000000000006521432127672400153730ustar00rootroot00000000000000.. include:: ../README.rst :end-line: -8 API Docs ======== .. toctree:: :maxdepth: 2 txi2p txi2p-bob txi2p-sam Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` .. |txi2p| replace:: :mod:`txi2p` .. |clientFromString| replace:: :func:`twisted.internet.endpoints.clientFromString` .. |serverFromString| replace:: :func:`twisted.internet.endpoints.serverFromString` txi2p-0.3.7/docs/requirements.txt000066400000000000000000000000141432127672400170060ustar00rootroot00000000000000vcversioner txi2p-0.3.7/docs/txi2p-bob.rst000066400000000000000000000004411432127672400160660ustar00rootroot00000000000000:mod:`txi2p.bob` Module =========================== This module contains classes and helper functions specific to the BOB API. .. autoclass:: txi2p.bob.BOBI2PClientEndpoint :members: :undoc-members: .. autoclass:: txi2p.bob.BOBI2PServerEndpoint :members: :undoc-members: txi2p-0.3.7/docs/txi2p-sam.rst000066400000000000000000000006661432127672400161150ustar00rootroot00000000000000:mod:`txi2p.sam` Module =========================== This module contains classes and helper functions specific to the SAM API. .. autofunction:: txi2p.sam.generateDestination .. autofunction:: txi2p.sam.getSession .. autoclass:: txi2p.sam.SAMSession :members: .. autoclass:: txi2p.sam.SAMI2PStreamClientEndpoint :members: :undoc-members: .. autoclass:: txi2p.sam.SAMI2PStreamServerEndpoint :members: :undoc-members: txi2p-0.3.7/docs/txi2p.rst000066400000000000000000000003111432127672400153220ustar00rootroot00000000000000:mod:`txi2p` Module ======================= This module contains helper functions that are not API-specific. .. autofunction:: txi2p.generateDestination .. autoclass:: txi2p.I2PAddress :members: txi2p-0.3.7/examples/000077500000000000000000000000001432127672400144155ustar00rootroot00000000000000txi2p-0.3.7/examples/answerserver.py000066400000000000000000000023471432127672400175230ustar00rootroot00000000000000from __future__ import print_function from twisted.internet import reactor, defer from twisted.internet.endpoints import serverFromString from twisted.internet.protocol import Factory from twisted.protocols.basic import LineReceiver class Answer(LineReceiver): answers = { 'How are you?': 'Fine', 'W007!': 'INORITE?', None : "I don't know what you mean", } def lineReceived(self, line): print('Line received from %s' % self.transport.getPeer()) if self.answers.has_key(line): self.sendLine(self.answers[line]) else: self.sendLine(self.answers[None]) class AnswerFactory(Factory): protocol = Answer def printDest(port): # Print out the I2P Destination to copy to the client print('This server is listening on:') print(port.getHost().destination) # Handle Ctl+C def shutdown(): print('Shutting down') port.stopListening() d = defer.Deferred() reactor.callLater(3, d.callback, 1) return d reactor.addSystemEventTrigger('before', 'shutdown', shutdown) endpoint = serverFromString(reactor, 'i2p:keypair.answerserver') d = endpoint.listen(AnswerFactory()) d.addCallback(printDest) reactor.run() txi2p-0.3.7/examples/chatserver.py000066400000000000000000000036661432127672400171500ustar00rootroot00000000000000from __future__ import print_function from twisted.internet import reactor, defer from twisted.internet.endpoints import serverFromString from twisted.internet.protocol import Factory from twisted.protocols.basic import LineReceiver class Chat(LineReceiver): def __init__(self, users): self.users = users self.name = None self.state = "GETNAME" def connectionMade(self): self.sendLine("What's your name?") def connectionLost(self, reason): if self.users.has_key(self.name): del self.users[self.name] def lineReceived(self, line): if self.state == "GETNAME": self.handle_GETNAME(line) else: self.handle_CHAT(line) def handle_GETNAME(self, name): if self.users.has_key(name): self.sendLine("Name taken, please choose another.") return self.sendLine("Welcome, %s!" % (name,)) self.name = name self.users[name] = self self.state = "CHAT" def handle_CHAT(self, message): message = "<%s> %s" % (self.name, message) for name, protocol in self.users.iteritems(): if protocol != self: protocol.sendLine(message) class ChatFactory(Factory): def __init__(self): self.users = {} # maps user names to Chat instances def buildProtocol(self, addr): return Chat(self.users) def printDest(port): # Print out the I2P Destination to copy to the client print('This server is listening on:') print(port.getHost().destination) # Handle Ctl+C def shutdown(): print('Shutting down') port.stopListening() d = defer.Deferred() reactor.callLater(3, d.callback, 1) return d reactor.addSystemEventTrigger('before', 'shutdown', shutdown) endpoint = serverFromString(reactor, 'i2p:keypair.chatserver') d = endpoint.listen(ChatFactory()) d.addCallback(printDest) reactor.run() txi2p-0.3.7/examples/eepsitefetcher.py000066400000000000000000000017221432127672400177700ustar00rootroot00000000000000from __future__ import print_function from sys import stdout from twisted.internet import reactor from twisted.internet.endpoints import clientFromString from twisted.internet.protocol import ClientFactory, Protocol class Eepsite(Protocol): def connectionMade(self): print('Connection made, sending eepsite request') self.transport.write(b'GET / HTTP/1.1\r\n\r\n') def dataReceived(self, data): stdout.write(data) def connectionLost(self, reason): print('Lost connection. Reason:', reason) reactor.stop() class EepsiteFactory(ClientFactory): protocol = Eepsite endpoint = clientFromString(reactor, 'i2p:stats.i2p:81') d = endpoint.connect(EepsiteFactory()) def printProto(proto): print('My address:') print(proto.transport.getHost()) print('Eepsite address:') print(proto.transport.getPeer()) def printErr(err): print(err) reactor.stop() d.addCallbacks(printProto, printErr) reactor.run() txi2p-0.3.7/pyproject.toml000066400000000000000000000002311432127672400155070ustar00rootroot00000000000000[build-system] requires = ["setuptools", "setuptools_scm[toml]"] [tool.setuptools_scm] write_to = "txi2p/_version.py" local_scheme = "no-local-version" txi2p-0.3.7/requirements.txt000066400000000000000000000000331432127672400160570ustar00rootroot00000000000000Twisted>=10.2 Parsley>=1.2 txi2p-0.3.7/run-coverage.sh000077500000000000000000000002161432127672400155320ustar00rootroot00000000000000#!/bin/bash . env/bin/activate coverage run --branch --source=txi2p --omit=*/_version.py,*test* env/bin/trial txi2p coverage html -d coverage txi2p-0.3.7/run_coveralls.py000066400000000000000000000016271432127672400160350ustar00rootroot00000000000000#!/bin/env/python import os try: from contextlib import suppress except ImportError: from contextlib import contextmanager @contextmanager def suppress(*exceptions): try: yield except exceptions: pass from distutils.sysconfig import get_python_lib from subprocess import call if __name__ == '__main__': # chdir to the site-packages directory so the report lists relative paths dot_coverage_path = os.path.join(os.getcwd(), '.coverage') os.chdir(get_python_lib()) with suppress(OSError): os.remove('.coverage') os.symlink(dot_coverage_path, '.coverage') # create a report from the coverage data if 'TRAVIS' in os.environ: rc = call('coveralls') # don't fail test if coveralls submission failed raise SystemExit(None) else: rc = call(['coverage', 'report']) raise SystemExit(rc) txi2p-0.3.7/setup.py000066400000000000000000000037341432127672400143200ustar00rootroot00000000000000# Copyright (c) str4d # See COPYING for details. from setuptools import setup from os import path import sys here = path.abspath(path.dirname(__file__)) def readme(): with open(path.join(here, 'README.rst')) as f: return f.read() install_requires = [ 'Parsley>=1.2', ] # future is only a requirement for Py2 # This will not work on Py3 if any of the 14 standard library modules listed # here get used later on: # http://python-future.org/standard_library_imports.html#list-standard-library-refactored if sys.version_info[0] < 3: install_requires.append('future>=0.14.0') install_requires.append('Twisted>=10.1') else: install_requires.append('Twisted>=15.4') setup( name='txi2p-tahoe', description='I2P bindings for Twisted', long_description=readme(), author='str4d', author_email='str4d@i2pmail.org', url='https://github.com/str4d/txi2p', classifiers=[ 'Development Status :: 4 - Beta', 'Framework :: Twisted', 'Intended Audience :: Developers', 'License :: OSI Approved :: ISC License (ISCL)', 'Operating System :: OS Independent', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Topic :: Internet', ], license='ISC', install_requires=install_requires, packages=[ 'txi2p', 'txi2p.bob', 'txi2p.sam', 'txi2p.test', 'txi2p.bob.test', 'txi2p.sam.test', 'twisted.plugins', ], extras_require={ "test": [ "mock; python_version < '3.0'", ], }, ) # Make Twisted regenerate the dropin.cache, if possible. This is necessary # because in a site-wide install, dropin.cache cannot be rewritten by # normal users. try: from twisted.plugin import IPlugin, getPlugins except ImportError: pass else: list(getPlugins(IPlugin)) txi2p-0.3.7/twisted/000077500000000000000000000000001432127672400142625ustar00rootroot00000000000000txi2p-0.3.7/twisted/plugins/000077500000000000000000000000001432127672400157435ustar00rootroot00000000000000txi2p-0.3.7/twisted/plugins/txi2p_endpoint_parsers.py000066400000000000000000000003301432127672400230160ustar00rootroot00000000000000# Copyright (c) str4d # See COPYING for details. from txi2p.plugins import I2PClientParser, I2PServerParser i2pClientEndpointParser = I2PClientParser() i2pServerEndpointParser = I2PServerParser() txi2p-0.3.7/txi2p/000077500000000000000000000000001432127672400136455ustar00rootroot00000000000000txi2p-0.3.7/txi2p/__init__.py000066400000000000000000000003351432127672400157570ustar00rootroot00000000000000try: from txi2p._version import __version__, __version_tuple__ except ImportError: __version__ = __version_tuple__ = None from txi2p.address import I2PAddress from txi2p.utils import generateDestination, testAPI txi2p-0.3.7/txi2p/address.py000066400000000000000000000075011432127672400156470ustar00rootroot00000000000000# Copyright (c) str4d # See COPYING for details. from builtins import object import base64 import hashlib from twisted.internet.interfaces import IAddress, ITransport from twisted.internet.protocol import Protocol from twisted.python.util import FancyEqMixin from zope.interface import implementer @implementer(IAddress) class I2PAddress(FancyEqMixin, object): """An :class:`IAddress` that represents the address of an I2P Destination. Args: destination (str): An I2P Destination string in I2P-style B64 format, or an :class:`I2PAddress`. In the latter case, the default host is also taken from the provided address. host (str): An I2P host string; for example, ``'example.i2p'``. port (int): An integer representing the port number. Attributes: destination (str): An I2P Destination string in I2P-style B64 format. host (str): An I2P host string; for example, ``'example.i2p'`` or ``'fiftytwocharacters.b32.i2p'``. If looked up, it is guaranteed to resolve to ``destination``. port (int): An integer representing the port number. Will be ``None`` if no port is configured. """ compareAttributes = ('destination', 'port') def __init__(self, destination, host=None, port=None): if hasattr(destination, 'destination'): self.destination = destination.destination else: self.destination = destination self.port = int(port) if port else None if host: self.host = host elif hasattr(destination, 'host'): self.host = destination.host else: raw_key = base64.b64decode(destination.encode('utf-8'), b'-~') hash = hashlib.sha256(raw_key) base32_hash = base64.b32encode(hash.digest()).decode('utf-8') self.host = base32_hash.lower().replace('=', '')+'.b32.i2p' def __repr__(self): if self.port: return '%s(%s, %d)' % ( self.__class__.__name__, self.host, self.port) return '%s(%s)' % ( self.__class__.__name__, self.host) def __hash__(self): return hash((self.host, self.port)) @implementer(ITransport) class I2PTunnelTransport(object): def __init__(self, wrappedTransport, localAddr, peerAddr=None, invertTLS=False): self.t = wrappedTransport self._localAddr = localAddr self.peerAddr = peerAddr # Workaround for https://tahoe-lafs.org/trac/tahoe-lafs/ticket/2861 if invertTLS and hasattr(self.t, 'startTLS'): import types def startTLSWrapper(self, ctx, normal=True): self.t.startTLS(ctx, not normal) self.startTLS = types.MethodType(startTLSWrapper, self) def __getattr__(self, attr): return getattr(self.t, attr) def getPeer(self): return self.peerAddr def getHost(self): return self._localAddr class I2PServerTunnelProtocol(Protocol): def __init__(self, wrappedProto, serverAddr): self.wrappedProto = wrappedProto self._serverAddr = serverAddr self.peer = None def connectionMade(self): # Substitute transport for an I2P wrapper self.transport = I2PTunnelTransport(self.transport, self._serverAddr) self.wrappedProto.makeConnection(self.transport) def dataReceived(self, data): if self.peer: # Pass all other data to the wrapped Protocol. self.wrappedProto.dataReceived(data.encode('utf-8')) else: # First line is the peer's Destination. self.setPeer(data) def setPeer(self, data): self.peer = I2PAddress(data.split('\n')[0]) self.transport.peerAddr = self.peer def connectionLost(self, reason): self.wrappedProto.connectionLost(reason) txi2p-0.3.7/txi2p/bob/000077500000000000000000000000001432127672400144075ustar00rootroot00000000000000txi2p-0.3.7/txi2p/bob/__init__.py000066400000000000000000000001171432127672400165170ustar00rootroot00000000000000from .endpoints import ( BOBI2PClientEndpoint, BOBI2PServerEndpoint, ) txi2p-0.3.7/txi2p/bob/endpoints.py000066400000000000000000000134151432127672400167700ustar00rootroot00000000000000# Copyright (c) str4d # See COPYING for details. from builtins import object from twisted.internet import interfaces from zope.interface import implementer from txi2p.bob.factory import BOBI2PClientFactory, BOBI2PServerFactory def _validateDestination(dest): # TODO: Validate I2P domain, B32 etc. pass @implementer(interfaces.IStreamClientEndpoint) class BOBI2PClientEndpoint(object): """I2P client endpoint backed by the BOB API. Args: reactor: The client endpoint will be constructed with this reactor. bobEndpoint (twisted.internet.interfaces.IStreamClientEndpoint): An endpoint that will connect to the BOB API. host (str): The I2P hostname or Destination to connect to. port (int): The port to connect to inside I2P. If unset or `None`, the default (null) port is used. Ignored because BOB doesn't support ports yet. tunnelNick (str): The tunnel nickname to use. If a tunnel with this nickname already exists, it will be used. The default is ``txi2p-#`` where ``#`` is the PID of the current process. * The implication of this is that by default, all endpoints (both client and server) created by the same process will use the same BOB tunnel. inhost (str): The host that the tunnel created by BOB will listen on. Defaults to ``localhost``. inport (int): The port that the tunnel created by BOB will listen on. Defaults to a port over 9000. options (dict): I2CP options to configure the tunnel with. """ def __init__(self, reactor, bobEndpoint, dest, port=None, tunnelNick=None, inhost='localhost', inport=None, options=None): _validateDestination(dest) self._reactor = reactor self._bobEndpoint = bobEndpoint self._dest = dest self._port = port self._tunnelNick = tunnelNick self._inhost = inhost self._inport = inport self._options = options def connect(self, fac): """Connect over I2P. The provided factory will have its ``buildProtocol`` method called once an I2P client tunnel has been successfully created. If the factory's ``buildProtocol`` returns ``None``, the connection will immediately close. """ i2pFac = BOBI2PClientFactory(self._reactor, fac, self._bobEndpoint, self._dest, self._tunnelNick, self._inhost, self._inport, self._options) d = self._bobEndpoint.connect(i2pFac) # Once the BOB IProtocol is returned, wait for the # real IProtocol to be returned after tunnel creation, # and pass it to any further registered callbacks. d.addCallback(lambda proto: i2pFac.deferred) return d @implementer(interfaces.IStreamServerEndpoint) class BOBI2PServerEndpoint(object): """I2P server endpoint backed by the BOB API. Args: reactor: The server endpoint will be constructed with this reactor. bobEndpoint (twisted.internet.interfaces.IStreamClientEndpoint): An endpoint that will connect to the BOB API. keyfile (str): Path to a local file containing the keypair to use for the server Destination. If non-existent, new keys will be generated and stored. port (int): The port to connect to inside I2P. If unset or `None`, the default (null) port is used. Ignored because BOB doesn't support ports yet. tunnelNick (str): The tunnel nickname to use. If a tunnel with this nickname already exists, it will be used. The default is ``txi2p-#`` where ``#`` is the PID of the current process. * The implication of this is that by default, all endpoints (both client and server) created by the same process will use the same BOB tunnel. outhost (str): The host that the tunnel created by BOB will forward data to. Defaults to ``localhost``. outport (int): The port that the tunnel created by BOB will forward data to. Defaults to a port over 9000. options (dict): I2CP options to configure the tunnel with. """ def __init__(self, reactor, bobEndpoint, keyfile, port=None, tunnelNick=None, outhost='localhost', outport=None, options=None): self._reactor = reactor self._bobEndpoint = bobEndpoint self._keyfile = keyfile self._port = port self._tunnelNick = tunnelNick self._outhost = outhost self._outport = outport self._options = options def listen(self, fac): """Listen over I2P. The provided factory will have its ``buildProtocol`` method called once an I2P server tunnel has been successfully created. If the factory's ``buildProtocol`` returns ``None``, the connection will immediately close. """ i2pFac = BOBI2PServerFactory(self._reactor, fac, self._bobEndpoint, self._keyfile, self._tunnelNick, self._outhost, self._outport, self._options) d = self._bobEndpoint.connect(i2pFac) # Once the BOB IProtocol is returned, wait for the # IListeningPort to be returned after tunnel creation, # and pass it to any further registered callbacks. d.addCallback(lambda proto: i2pFac.deferred) return d txi2p-0.3.7/txi2p/bob/factory.py000066400000000000000000000211301432127672400164250ustar00rootroot00000000000000# Copyright (c) str4d # See COPYING for details. from __future__ import print_function from builtins import object from twisted.internet.defer import Deferred from twisted.internet.endpoints import TCP4ClientEndpoint, TCP4ServerEndpoint from twisted.internet.protocol import ClientFactory, Factory from txi2p.address import I2PAddress from txi2p.bob.protocol import (I2PClientTunnelCreatorBOBClient, I2PServerTunnelCreatorBOBClient, I2PTunnelRemoverBOBClient, I2PClientTunnelProtocol, I2PServerTunnelProtocol, I2PListeningPort) class BOBI2PClientFactory(ClientFactory): protocol = I2PClientTunnelCreatorBOBClient bobProto = None canceled = False removeTunnelWhenFinished = True def _cancel(self, d): self.bobProto.sender.transport.abortConnection() self.canceled = True def __init__(self, reactor, clientFactory, bobEndpoint, dest, tunnelNick=None, inhost='localhost', inport=None, options={}): self._reactor = reactor self._clientFactory = clientFactory self._bobEndpoint = bobEndpoint self.dest = dest self.tunnelNick = tunnelNick self.inhost = inhost self.inport = inport self.options = options self.deferred = Deferred(self._cancel); def buildProtocol(self, addr): proto = self.protocol() proto.factory = self self.bobProto = proto return proto def bobConnectionFailed(self, reason): if not self.canceled: self.deferred.errback(reason) # This method is not called if an endpoint deferred errbacks def clientConnectionFailed(self, connector, reason): self.bobConnectionFailed(reason) def i2pTunnelCreated(self): # BOB is now listening for a tunnel. # BOB only listens on TCP4 (for now). clientEndpoint = TCP4ClientEndpoint(self._reactor, self.inhost, self.inport) # Wrap the client Factory. wrappedFactory = BOBClientFactoryWrapper(self._clientFactory, self._bobEndpoint, I2PAddress(self.localDest), self.tunnelNick, self.removeTunnelWhenFinished) wrappedFactory.setDest(self.dest) d = clientEndpoint.connect(wrappedFactory) def checkProto(proto): if proto is None: self.deferred.cancel() return proto d.addCallback(checkProto) # When the Deferred returns an IProtocol, pass it on. d.chainDeferred(self.deferred) class BOBI2PServerFactory(Factory): protocol = I2PServerTunnelCreatorBOBClient bobProto = None canceled = False removeTunnelWhenFinished = True def _cancel(self, d): self.bobProto.sender.transport.abortConnection() self.canceled = True def __init__(self, reactor, serverFactory, bobEndpoint, keyfile, tunnelNick=None, outhost='localhost', outport=None, options={}): self._reactor = reactor self._serverFactory = serverFactory self._bobEndpoint = bobEndpoint self._keyfile = keyfile self._writeKeypair = False self.tunnelNick = tunnelNick self.outhost = outhost self.outport = outport self.options = options self.deferred = Deferred(self._cancel) def startFactory(self): try: f = open(self._keyfile, 'r') self.keypair = f.read() f.close() except IOError: self.keypair = None self._writeKeypair = True def buildProtocol(self, addr): proto = self.protocol() proto.factory = self self.bobProto = proto return proto def bobConnectionFailed(self, reason): if not self.canceled: self.deferred.errback(reason) # This method is not called if an endpoint deferred errbacks def clientConnectionFailed(self, connector, reason): self.bobConnectionFailed(reason) def i2pTunnelCreated(self): if self._writeKeypair: try: f = open(self._keyfile, 'w') f.write(self.keypair) f.close() except IOError: print('Could not save keypair') # BOB will now forward data to a listener. # BOB only forwards to TCP4 (for now). serverEndpoint = TCP4ServerEndpoint(self._reactor, self.outport) # Wrap the server Factory. wrappedFactory = BOBServerFactoryWrapper(self._serverFactory, self._bobEndpoint, I2PAddress(self.localDest), self.tunnelNick, self.removeTunnelWhenFinished) d = serverEndpoint.listen(wrappedFactory) def handlePort(port): if port is None: self.deferred.cancel() serverAddr = I2PAddress(self.localDest) p = I2PListeningPort(port, wrappedFactory, serverAddr) return p d.addCallback(handlePort) # When the Deferred returns an IListeningPort, pass it on. d.chainDeferred(self.deferred) class BOBFactoryWrapperCommon(object): def __init__(self, wrappedFactory, bobEndpoint, localAddr, tunnelNick, removeTunnelWhenFinished): self.w = wrappedFactory self.bobEndpoint = bobEndpoint self.localAddr = localAddr self.tunnelNick = tunnelNick self.removeTunnelWhenFinished = removeTunnelWhenFinished def __getattr__(self, attr): return getattr(self.w, attr) class BOBClientFactoryWrapper(BOBFactoryWrapperCommon): protocol = I2PClientTunnelProtocol def setDest(self, dest): self.dest = dest def buildProtocol(self, addr): wrappedProto = self.w.buildProtocol(addr) proto = self.protocol(wrappedProto, self.localAddr, self.dest) proto.factory = self return proto def i2pConnectionLost(self, wrappedProto, reason): if self.removeTunnelWhenFinished: # Notify the underlying Protocol once the tunnel has # been removed, in case they stop the reactor. rmTunnelFac = BOBI2PClientTunnelRemoverFactory(self.tunnelNick, wrappedProto, reason) self.bobEndpoint.connect(rmTunnelFac) else: # Notify the underlying Protocol now. wrappedProto.connectionLost(reason) class BOBServerFactoryWrapper(BOBFactoryWrapperCommon): protocol = I2PServerTunnelProtocol def buildProtocol(self, addr): wrappedProto = self.w.buildProtocol(addr) proto = self.protocol(wrappedProto, self.localAddr) proto.factory = self return proto def stopListening(self, wrappedPort): if self.removeTunnelWhenFinished: # Notify the underlying ListeningPort once the tunnel # has been removed, in case they stop the reactor. rmTunnelFac = BOBI2PServerTunnelRemoverFactory(self.tunnelNick, wrappedPort) self.bobEndpoint.connect(rmTunnelFac) else: # Notify the underlying ListeningPort now. wrappedPort.stopListening() class BOBI2PClientTunnelRemoverFactory(ClientFactory): protocol = I2PTunnelRemoverBOBClient def __init__(self, tunnelNick, protoToNotify, reason): self.tunnelNick = tunnelNick self._protoToNotify = protoToNotify self._reason = reason def i2pTunnelRemoved(self): # Now that the I2P tunnel has been removed, # notify the underlying Protocol. self._protoToNotify.connectionLost(self._reason) class BOBI2PServerTunnelRemoverFactory(ClientFactory): protocol = I2PTunnelRemoverBOBClient def __init__(self, tunnelNick, portToNotify): self.tunnelNick = tunnelNick self._portToNotify = portToNotify def i2pTunnelRemoved(self): # Now that the I2P tunnel has been removed, # notify the underlying ListeningPort. self._portToNotify.stopListening() txi2p-0.3.7/txi2p/bob/protocol.py000066400000000000000000000362341432127672400166320ustar00rootroot00000000000000# Copyright (c) str4d # See COPYING for details. from __future__ import print_function from builtins import range from builtins import object import os from parsley import makeProtocol from twisted.internet.error import ConnectError, UnknownHostError from twisted.internet.interfaces import IListeningPort from twisted.internet.protocol import Protocol from twisted.python.failure import Failure from zope.interface import implementer from txi2p import grammar from txi2p.address import ( I2PAddress, I2PTunnelTransport, I2PServerTunnelProtocol, ) DEFAULT_INPORT = 9000 DEFAULT_OUTPORT = 9001 class BOBSender(object): def __init__(self, transport): self.transport = transport def sendClear(self): self.transport.write(b'clear\n') def sendGetdest(self): self.transport.write(b'getdest\n') def sendGetkeys(self): self.transport.write(b'getkeys\n') def sendGetnick(self, tunnelNick): msg = 'getnick %s\n' % tunnelNick self.transport.write(msg.encode('utf-8')) def sendInhost(self, inhost): msg = 'inhost %s\n' % inhost self.transport.write(msg.encode('utf-8')) def sendInport(self, inport): msg = 'inport %s\n' % inport self.transport.write(msg.encode('utf-8')) def sendList(self): self.transport.write(b'list\n') def sendNewkeys(self): self.transport.write(b'newkeys\n') def sendOption(self, options={}): msg = 'option' for key in sorted(options): msg += ' %s=%s' % (key, options[key]) msg += '\n' self.transport.write(msg.encode('utf-8')) def sendOuthost(self, outhost): msg = 'outhost %s\n' % outhost self.transport.write(msg.encode('utf-8')) def sendOutport(self, outport): msg = 'outport %s\n' % outport self.transport.write(msg.encode('utf-8')) def sendQuiet(self): self.transport.write(b'quiet\n') def sendQuit(self): self.transport.write(b'quit\n') def sendSetkeys(self, keys): msg = 'setkeys %s\n' % keys self.transport.write(msg.encode('utf-8')) def sendSetnick(self, tunnelNick): msg = 'setnick %s\n' % tunnelNick self.transport.write(msg.encode('utf-8')) def sendShow(self): self.transport.write(b'show\n') def sendShowprops(self): self.transport.write(b'showprops\n') def sendStart(self): self.transport.write(b'start\n') def sendStatus(self, tunnelNick): msg = 'status %s\n' % tunnelNick self.transport.write(msg.encode('utf-8')) def sendStop(self): self.transport.write(b'stop\n') def sendVerify(self, key): msg = 'verify %s\n' % key self.transport.write(msg.encode('utf-8')) def sendVisit(self): self.transport.write(b'visit\n') class BOBReceiver(object): currentRule = 'State_init' def __init__(self, sender): self.sender = sender self.tunnelExists = False def prepareParsing(self, parser): # Store the factory for later use self.factory = parser.factory def finishParsing(self, reason): if self.currentRule != 'State_quit': self.factory.bobConnectionFailed(reason) def initBOB(self, version): self.sender.sendList() self.currentRule = 'State_list' def processTunnelList(self, tunnels): if not (hasattr(self.factory, 'tunnelNick') and self.factory.tunnelNick): # All tunnels in the same process use the same tunnelNick # TODO is using the PID a security risk? self.factory.tunnelNick = 'txi2p-%d' % os.getpid() used_ports = [] for i in range(0, len(tunnels)): if tunnels[i]['nickname'] == self.factory.tunnelNick: # Tunnel already exists, use its settings. self.tunnelExists = True self.tunnelRunning = tunnels[i]['running'] self.factory.inhost = tunnels[i]['inhost'] self.factory.inport = tunnels[i]['inport'] self.factory.outhost = tunnels[i]['outhost'] self.factory.outport = tunnels[i]['outport'] # The tunnel will be removed by the Factory # that created it. self.factory.removeTunnelWhenFinished = False break else: if tunnels[i]['inport']: used_ports.append(tunnels[i]['inport']) if tunnels[i]['outport']: used_ports.append(tunnels[i]['outport']) else: # This is a new tunnel. # Default port offset is at the end of the tunnels list offset = 2*(len(tunnels)) # Find an offset that does not clash while (DEFAULT_INPORT + offset) in used_ports or (DEFAULT_OUTPORT + offset) in used_ports: offset += 2 # If the in/outport were not user-configured, set them. if not (hasattr(self.factory, 'inport') and self.factory.inport): self.factory.inport = DEFAULT_INPORT + offset if not (hasattr(self.factory, 'outport') and self.factory.outport): self.factory.outport = DEFAULT_OUTPORT + offset def getnick(self, success, info): if success: if self.tunnelRunning: self.sender.sendStop() self.currentRule = 'State_stop' else: # Update the local Destination self.sender.sendGetdest() self.currentRule = 'State_getdest' else: print('ERROR: %s' % info) def stop(self, success, info): if success: # Update the local Destination self.sender.sendGetdest() self.currentRule = 'State_getdest' else: print('stop ERROR: %s' % info) def setnick(self, success, info): if success: # Set the options self.sender.sendOption(self.factory.options) self.currentRule = 'State_option' else: print('setnick ERROR: %s' % info) def option(self, success, info): if success: if hasattr(self.factory, 'keypair') and self.factory.keypair: # If a keypair was provided, use it self.sender.sendSetkeys(self.factory.keypair) self.currentRule = 'State_setkeys' else: # Get a new keypair self.sender.sendNewkeys() self.currentRule = 'State_newkeys' else: print('option ERROR: %s' % info) def setkeys(self, success, info): if success: # Update the local Destination self.sender.sendGetdest() self.currentRule = 'State_getdest' else: print('setkeys ERROR: %s' % info) def newkeys(self, success, info): if success: # Save the new local Destination self.factory.localDest = info # Get the new keypair self.sender.sendGetkeys() self.currentRule = 'State_getkeys' else: print('newkeys ERROR: %s' % info) def quit(self, success, info): pass class I2PClientTunnelCreatorBOBReceiver(BOBReceiver): def list(self, success, info, data): if success: self.processTunnelList(data) if self.tunnelExists: self.sender.sendGetnick(self.factory.tunnelNick) self.currentRule = 'State_getnick' else: # Set tunnel nickname (and update keypair/localDest state) self.sender.sendSetnick(self.factory.tunnelNick) self.currentRule = 'State_setnick' else: print('list ERROR: %s' % info) def getdest(self, success, info): if success: # Save the local Destination self.factory.localDest = info if self.tunnelExists: # Get the keypair self.sender.sendGetkeys() self.currentRule = 'State_getkeys' else: self._setInhost() else: print('getdest ERROR: %s' % info) def getkeys(self, success, info): if success: # Save the keypair self.factory.keypair = info self._setInhost() else: print('getkeys ERROR: %s' % info) def _setInhost(self): self.sender.sendInhost(self.factory.inhost) self.currentRule = 'State_inhost' def inhost(self, success, info): if success: self.sender.sendInport(self.factory.inport) self.currentRule = 'State_inport' else: if info in ['tunnel is active', 'tunnel shutting down']: # Try again. TODO: Limit retries self.sender.sendInhost(self.factory.inhost) else: print('inhost ERROR: %s' % info) def inport(self, success, info): if success: self.sender.sendStart() self.currentRule = 'State_start' else: print('inport ERROR: %s' % info) def start(self, success, info): if success: print("Client tunnel started") self.factory.i2pTunnelCreated() self.sender.sendQuit() self.currentRule = 'State_quit' else: print('start ERROR: %s' % info) class I2PServerTunnelCreatorBOBReceiver(BOBReceiver): def list(self, success, info, data): if success: self.processTunnelList(data) if self.tunnelExists: self.sender.sendGetnick(self.factory.tunnelNick) self.currentRule = 'State_getnick' else: # Set tunnel nickname (and update keypair/localDest state) self.sender.sendSetnick(self.factory.tunnelNick) self.currentRule = 'State_setnick' else: print('list ERROR: %s' % info) def getdest(self, success, info): if success: # Save the local Destination self.factory.localDest = info self._setOuthost() else: print('getdest ERROR: %s' % info) def getkeys(self, success, info): if success: # Save the keypair self.factory.keypair = info self._setOuthost() else: print('getkeys ERROR: %s' % info) def _setOuthost(self): self.sender.sendOuthost(self.factory.outhost) self.currentRule = 'State_outhost' def outhost(self, success, info): if success: self.sender.sendOutport(self.factory.outport) self.currentRule = 'State_outport' else: if info in ['tunnel is active', 'tunnel shutting down']: # Try again. TODO: Limit retries self.sender.sendOuthost(self.factory.outhost) else: print('outhost ERROR: %s' % info) def outport(self, success, info): if success: self.sender.sendStart() self.currentRule = 'State_start' else: print('outport ERROR: %s' % info) def start(self, success, info): if success: print("Server tunnel started") self.factory.i2pTunnelCreated() self.sender.sendQuit() self.currentRule = 'State_quit' else: print('start ERROR: %s' % info) class I2PTunnelRemoverBOBReceiver(BOBReceiver): def list(self, success, info, data): if success: self.processTunnelList(data) if self.tunnelExists: # Get tunnel for nickname self.sender.sendGetnick(self.factory.tunnelNick) self.currentRule = 'State_getnick' else: # Tunnel already removed pass else: print('list ERROR: %s' % info) def getnick(self, success, info): if success: self.sender.sendStop() self.currentRule = 'State_stop' else: print('getnick ERROR: %s' % info) def stop(self, success, info): if success: self.sender.sendClear() self.currentRule = 'State_clear' else: print('stop ERROR: %s' % info) def clear(self, success, info): if success: print('Tunnel removed') self.factory.i2pTunnelRemoved() self.sender.sendQuit() self.currentRule = 'State_quit' else: if info in ['tunnel is active', 'tunnel shutting down']: # Try again. TODO: Limit retries self.sender.sendClear() else: print('clear ERROR: %s ' % info) # A Protocol for making an I2P client tunnel via BOB I2PClientTunnelCreatorBOBClient = makeProtocol( grammar.bobGrammarSource, BOBSender, I2PClientTunnelCreatorBOBReceiver) # A Protocol for making an I2P server tunnel via BOB I2PServerTunnelCreatorBOBClient = makeProtocol( grammar.bobGrammarSource, BOBSender, I2PServerTunnelCreatorBOBReceiver) # A Protocol for removing a BOB I2P tunnel I2PTunnelRemoverBOBClient = makeProtocol( grammar.bobGrammarSource, BOBSender, I2PTunnelRemoverBOBReceiver) class I2PClientTunnelProtocol(Protocol): def __init__(self, wrappedProto, clientAddr, dest): self.wrappedProto = wrappedProto self._clientAddr = clientAddr self.dest = dest self._errmsg = None def connectionMade(self): # Substitute transport for an I2P wrapper self.transport = I2PTunnelTransport(self.transport, self._clientAddr, I2PAddress(self.dest)) self.isConnected = False # First line sent must be the Destination to connect to. self.transport.write((self.dest + '\n').encode('utf-8')) self.wrappedProto.makeConnection(self.transport) def dataReceived(self, data): # Check for a successful connection if not self.isConnected: if data.startswith('ERROR'): self._errmsg = data[6:] # I2P connection failed self.transport.loseConnection() return else: self.isConnected = True # Pass all received data to the wrapped Protocol. self.wrappedProto.dataReceived(data.encode('utf-8')) def connectionLost(self, reason): if self._errmsg: if self._errmsg.startswith("Can't find destination"): reason = Failure(UnknownHostError(string=self._errmsg)) else: reason = Failure(ConnectError(string=self._errmsg)) self.factory.i2pConnectionLost(self.wrappedProto, reason) @implementer(IListeningPort) class I2PListeningPort(object): def __init__(self, wrappedPort, factoryWrapper, serverAddr): self._wrappedPort = wrappedPort self._factoryWrapper = factoryWrapper self._serverAddr = serverAddr def startListening(self): self._wrappedPort.startListening() def stopListening(self): self._factoryWrapper.stopListening(self._wrappedPort) def getHost(self): return self._serverAddr txi2p-0.3.7/txi2p/bob/test/000077500000000000000000000000001432127672400153665ustar00rootroot00000000000000txi2p-0.3.7/txi2p/bob/test/__init__.py000066400000000000000000000000001432127672400174650ustar00rootroot00000000000000txi2p-0.3.7/txi2p/bob/test/test_endpoints.py000066400000000000000000000030571432127672400210070ustar00rootroot00000000000000# Copyright (c) str4d # See COPYING for details. from twisted.internet.error import ConnectionLost, ConnectionRefusedError from twisted.python import failure from twisted.trial import unittest from txi2p.bob import endpoints from txi2p.test.util import FakeEndpoint, FakeFactory connectionLostFailure = failure.Failure(ConnectionLost()) connectionRefusedFailure = failure.Failure(ConnectionRefusedError()) class BOBI2PClientEndpointTestCase(unittest.TestCase): """ Tests for I2P client Endpoint backed by the BOB API. """ def test_bobConnectionFailed(self): reactor = object() bobEndpoint = FakeEndpoint(failure=connectionRefusedFailure) endpoint = endpoints.BOBI2PClientEndpoint(reactor, bobEndpoint, '') d = endpoint.connect(None) return self.assertFailure(d, ConnectionRefusedError) def TODO_test_destination(self): reactor = object() bobEndpoint = FakeEndpoint() endpoint = endpoints.BOBI2PClientEndpoint(reactor, bobEndpoint, 'foo.i2p') endpoint.connect(None) self.assertEqual(bobEndpoint.transport.value(), 'foo.i2p') # TODO: Fix. def TODO_test_clientDataSent(self): reactor = object() wrappedFac = FakeFactory() bobEndpoint = FakeEndpoint() endpoint = endpoints.BOBI2PClientEndpoint(reactor, bobEndpoint, '') endpoint.connect(wrappedFac) bobEndpoint.proto.transport.clear() wrappedFac.proto.transport.write('xxxxx') self.assertEqual(bobEndpoint.proto.transport.value(), 'xxxxx') txi2p-0.3.7/txi2p/bob/test/test_factory.py000066400000000000000000000104311432127672400204450ustar00rootroot00000000000000# Copyright (c) str4d # See COPYING for details. from builtins import object from twisted.internet import defer from twisted.internet.error import ConnectionLost, ConnectionRefusedError from twisted.python import failure from twisted.test import proto_helpers from twisted.trial import unittest from txi2p.bob.factory import (BOBI2PClientFactory, BOBI2PServerFactory, BOBClientFactoryWrapper, BOBServerFactoryWrapper) from txi2p.test.util import FakeFactory connectionLostFailure = failure.Failure(ConnectionLost()) connectionRefusedFailure = failure.Failure(ConnectionRefusedError()) class BOBFactoryTestMixin(object): def setUp(self): self.aborted = [] def makeProto(self, *a, **kw): fac = self.factory(*a, **kw) proto = fac.buildProtocol(None) transport = proto_helpers.StringTransport() transport.abortConnection = lambda: self.aborted.append(True) proto.makeConnection(transport) return fac, proto def test_cancellation(self): fac, proto = self.makeProto(None, None, None, '') fac.deferred.cancel() self.assert_(self.aborted) return self.assertFailure(fac.deferred, defer.CancelledError) def test_cancellationBeforeFailure(self): fac, proto = self.makeProto(None, None, None, '') fac.deferred.cancel() proto.connectionLost(connectionLostFailure) self.assert_(self.aborted) return self.assertFailure(fac.deferred, defer.CancelledError) def test_cancellationAfterFailure(self): fac, proto = self.makeProto(None, None, None, '') proto.connectionLost(connectionLostFailure) fac.deferred.cancel() self.assertFalse(self.aborted) return self.assertFailure(fac.deferred, ConnectionLost) def test_clientConnectionFailed(self): fac, proto = self.makeProto(None, None, None, '') fac.clientConnectionFailed(None, connectionRefusedFailure) return self.assertFailure(fac.deferred, ConnectionRefusedError) def test_defaultFactoryListsTunnels(self): fac, proto = self.makeProto(None, None, None, '') proto.dataReceived('BOB 00.00.10\nOK\n') self.assertEqual(proto.transport.value(), b'list\n') class TestBOBI2PClientFactory(BOBFactoryTestMixin, unittest.TestCase): factory = BOBI2PClientFactory def TODO_test_noProtocolFromWrappedFactory(self): wrappedFac = FakeFactory(returnNoProtocol=True) mreactor = proto_helpers.MemoryReactor() fac, proto = self.makeProto(mreactor, wrappedFac, None, '') fac.tunnelNick = 'spam' # Shortcut to end of BOB protocol proto.receiver.currentRule = 'State_start' proto._parser._setupInterp() proto.dataReceived('OK HTTP 418\n') self.assert_(self.aborted) # TODO: Check the Deferred chain return self.assertFailure(fac.deferred, defer.CancelledError) class TestBOBI2PServerFactory(BOBFactoryTestMixin, unittest.TestCase): factory = BOBI2PServerFactory def TODO_test_noProtocolFromWrappedFactory(self): wrappedFac = FakeFactory(returnNoProtocol=True) mreactor = proto_helpers.MemoryReactor() fac, proto = self.makeProto(mreactor, wrappedFac, None, '') fac.tunnelNick = 'spam' fac.localDest = 'spam.i2p' # Shortcut to end of BOB protocol proto.receiver.currentRule = 'State_start' proto._parser._setupInterp() proto.dataReceived('OK HTTP 418\n') self.assert_(self.aborted) # TODO: Check the Deferred chain return self.assertFailure(fac.deferred, defer.CancelledError) class TestBOBClientFactoryWrapper(unittest.TestCase): def test_buildProtocol(self): wrappedFac = FakeFactory() fac = BOBClientFactoryWrapper(wrappedFac, None, None, '', True) fac.setDest('spam.i2p') proto = fac.buildProtocol(None) self.assertEqual(proto.wrappedProto.factory, wrappedFac) class TestBOBServerFactoryWrapper(unittest.TestCase): def test_buildProtocol(self): wrappedFac = FakeFactory() fac = BOBServerFactoryWrapper(wrappedFac, None, None, '', True) proto = fac.buildProtocol(None) self.assertEqual(proto.wrappedProto.factory, wrappedFac) txi2p-0.3.7/txi2p/bob/test/test_protocol.py000066400000000000000000000632111432127672400206430ustar00rootroot00000000000000# Copyright (c) str4d # See COPYING for details. from builtins import object import os from twisted.internet.error import UnknownHostError from twisted.internet.protocol import ClientFactory from twisted.python.failure import Failure from twisted.test import proto_helpers from twisted.trial import unittest from txi2p.bob.protocol import (I2PClientTunnelCreatorBOBClient, I2PServerTunnelCreatorBOBClient, I2PTunnelRemoverBOBClient, I2PClientTunnelProtocol, I2PServerTunnelProtocol, DEFAULT_INPORT, DEFAULT_OUTPORT) TEST_B64 = "2wDRF5nDfeTNgM4X-TI5xEk3R-WiaTABvkMQ2eYpvEzayUZQJgr9E2T6Y2m9HHn3xHYGEOg-RLisjW9AubTaUTx-v66AsEEtv745qPcuWuV1SP~w1bdzYEn8MSoK7Zh4mwHBg1uHq8z17TUNvWz19q76vHNth-2PDuBToD7ySBn3cGBFDUU83wJJXPD6OueLY8yosWWtksk7WZk60~6z~nVePPSEY8JDry3myLDe11szAVER4A8eX1sFpw247cXGGJK9wQhV-TXFj~m76GPVcFKh7u79zwTwZnZ1GXXKqqyRoj1c4-U69CvvJsQRLmdLFwFEpRkxwV8z6LIFclYJk443YpTnPXC7vNdFOzqqS4FLR1ra~DNfN5foMtR2~2VxuR5m2dYiOS6GzHDxA4acJJSGqnasJjcEIFNVSQKxMnFu9PvGLNJHZ83EraHCErENcOGkPlnVgcJCtPGNGiirwCbBz38jE0lfjkrNrWabc6uWeU559CobG8F8KUDx1irpAAAA" class BOBProtoTestMixin(object): def makeProto(self, *a, **kw): protoClass = kw.pop('_protoClass', self.protocol) fac = ClientFactory(*a, **kw) fac.protocol = protoClass fac.options = {} def raise_(reason): raise reason.value fac.bobConnectionFailed = lambda reason: raise_(reason) proto = fac.buildProtocol(None) transport = proto_helpers.StringTransport() transport.abortConnection = lambda: None proto.makeConnection(transport) return fac, proto def test_initBOBListsTunnels(self): fac, proto = self.makeProto() proto.dataReceived('BOB 00.00.10\nOK\n') self.assertEqual(proto.transport.value(), b'list\n') def TODO_test_quitDoesNotErrback(self): fac, proto = self.makeProto() # Shortcut to end of BOB protocol proto.receiver.currentRule = 'State_quit' proto._parser._setupInterp() proto.dataReceived('OK Bye!\n') self.assertTrue(False, 'TODO: Test something') # TODO: Test something class BOBTunnelCreationMixin(BOBProtoTestMixin): def test_defaultInportSelected(self): fac, proto = self.makeProto() fac.tunnelNick = 'spam' fac.inhost = 'camelot' proto.dataReceived('BOB 00.00.10\nOK\n') proto.transport.clear() proto.dataReceived('OK Listing done\n') # No DATA, no tunnels self.assertTrue(hasattr(fac, 'inport')) self.assertEqual(fac.inport, DEFAULT_INPORT) def test_higherInportSelectedWhenDefaultBusy(self): fac, proto = self.makeProto() fac.tunnelNick = 'spam' fac.inhost = 'camelot' proto.dataReceived('BOB 00.00.10\nOK\n') proto.transport.clear() proto.dataReceived('DATA NICKNAME: test STARTING: false RUNNING: false STOPPING: false KEYS: false QUIET: false INPORT: 9000 INHOST: localhost OUTPORT: not_set OUTHOST: localhost\nOK Listing done\n') self.assertTrue(hasattr(fac, 'inport')) self.assertEqual(fac.inport, DEFAULT_INPORT + 2) def test_existingInhostAndInportSelectedForExistingTunnel(self): fac, proto = self.makeProto() fac.tunnelNick = 'spam' fac.inhost = 'camelot' fac.inport = 1234 proto.dataReceived('BOB 00.00.10\nOK\n') proto.transport.clear() proto.dataReceived('DATA NICKNAME: spam STARTING: false RUNNING: false STOPPING: false KEYS: false QUIET: false INPORT: 2345 INHOST: localhost OUTPORT: not_set OUTHOST: localhost\nOK Listing done\n') self.assertEqual(fac.inhost, 'localhost') self.assertEqual(fac.inport, 2345) def test_defaultOutportSelected(self): fac, proto = self.makeProto() fac.tunnelNick = 'spam' fac.inhost = 'camelot' proto.dataReceived('BOB 00.00.10\nOK\n') proto.transport.clear() proto.dataReceived('OK Listing done\n') # No DATA, no tunnels self.assertTrue(hasattr(fac, 'outport')) self.assertEqual(fac.outport, DEFAULT_OUTPORT) def test_higherOutportSelectedWhenDefaultBusy(self): fac, proto = self.makeProto() fac.tunnelNick = 'spam' fac.outhost = 'camelot' proto.dataReceived('BOB 00.00.10\nOK\n') proto.transport.clear() proto.dataReceived('DATA NICKNAME: test STARTING: false RUNNING: false STOPPING: false KEYS: false QUIET: false INPORT: not_set INHOST: localhost OUTPORT: 9001 OUTHOST: localhost\nOK Listing done\n') self.assertTrue(hasattr(fac, 'outport')) self.assertEqual(fac.outport, DEFAULT_OUTPORT + 2) def test_existingOuthostAndOutportSelectedForExistingTunnel(self): fac, proto = self.makeProto() fac.tunnelNick = 'spam' fac.outhost = 'camelot' fac.outport = 1234 proto.dataReceived('BOB 00.00.10\nOK\n') proto.transport.clear() proto.dataReceived('DATA NICKNAME: spam STARTING: false RUNNING: false STOPPING: false KEYS: false QUIET: false INPORT: not_set INHOST: localhost OUTPORT: 2345 OUTHOST: localhost\nOK Listing done\n') self.assertEqual(fac.outhost, 'localhost') self.assertEqual(fac.outport, 2345) def test_defaultNickSetsNick(self): fac, proto = self.makeProto() fac.tunnelNick = None proto.dataReceived('BOB 00.00.10\nOK\n') proto.transport.clear() proto.dataReceived('OK Listing done\n') # No DATA, no tunnels self.assertEqual(proto.transport.value().decode('utf-8'), 'setnick txi2p-%d\n' % os.getpid()) def test_newNickSetsNick(self): fac, proto = self.makeProto() fac.tunnelNick = 'spam' proto.dataReceived('BOB 00.00.10\nOK\n') proto.transport.clear() proto.dataReceived('OK Listing done\n') # No DATA, no tunnels self.assertEqual(proto.transport.value(), b'setnick spam\n') def test_nickSetSendsOptions(self): fac, proto = self.makeProto() fac.tunnelNick = 'spam' fac.options = {'foo': 'bar', 'spam': 'eggs'} proto.dataReceived('BOB 00.00.10\nOK\n') proto.transport.clear() proto.dataReceived('OK Listing done\n') # No DATA, no tunnels proto.transport.clear() proto.dataReceived('OK HTTP 418\n') self.assertEqual(proto.transport.value(), b'option foo=bar spam=eggs\n') def test_nickSetWithKeypair(self): fac, proto = self.makeProto() fac.tunnelNick = 'spam' fac.keypair = 'eggs' proto.dataReceived('BOB 00.00.10\nOK\n') proto.transport.clear() proto.dataReceived('OK Listing done\n') # No DATA, no tunnels proto.transport.clear() proto.dataReceived('OK HTTP 418\n') proto.transport.clear() proto.dataReceived('OK HTTP 418 options set\n') self.assertEqual(proto.transport.value(), b'setkeys eggs\n') def test_destFetchedAfterNickSetWithKeypair(self): fac, proto = self.makeProto() fac.tunnelNick = 'spam' fac.keypair = 'eggs' proto.dataReceived('BOB 00.00.10\nOK\n') proto.transport.clear() proto.dataReceived('OK Listing done\n') # No DATA, no tunnels proto.transport.clear() proto.dataReceived('OK HTTP 418\n') proto.transport.clear() proto.dataReceived('OK HTTP 418 options set\n') proto.transport.clear() proto.dataReceived('OK HTTP 418\n') self.assertEqual(proto.transport.value(), b'getdest\n') def test_nickSetWithNoKeypair(self): fac, proto = self.makeProto() fac.tunnelNick = 'spam' proto.dataReceived('BOB 00.00.10\nOK\n') proto.transport.clear() proto.dataReceived('OK Listing done\n') # No DATA, no tunnels proto.transport.clear() proto.dataReceived('OK HTTP 418\n') proto.transport.clear() proto.dataReceived('OK HTTP 418 options set\n') self.assertEqual(proto.transport.value(), b'newkeys\n') def test_keypairFetchedAfterNickSetWithNoKeypair(self): fac, proto = self.makeProto() fac.tunnelNick = 'spam' proto.dataReceived('BOB 00.00.10\nOK\n') proto.transport.clear() proto.dataReceived('OK Listing done\n') # No DATA, no tunnels proto.transport.clear() proto.dataReceived('OK HTTP 418\n') proto.transport.clear() proto.dataReceived('OK HTTP 418 options set\n') proto.transport.clear() proto.dataReceived('OK shrubbery\n') # The new Destination self.assertEqual(proto.transport.value(), b'getkeys\n') def test_existingNickGetsNick(self): fac, proto = self.makeProto() fac.tunnelNick = 'spam' proto.dataReceived('BOB 00.00.10\nOK\n') proto.transport.clear() proto.dataReceived('DATA NICKNAME: spam STARTING: false RUNNING: true STOPPING: false KEYS: true QUIET: false INPORT: 12345 INHOST: localhost OUTPORT: 23456 OUTHOST: localhost\nOK Listing done\n') self.assertEqual(proto.transport.value(), b'getnick spam\n') def test_stopRequestedForRunningTunnel(self): fac, proto = self.makeProto() fac.tunnelNick = 'spam' proto.dataReceived('BOB 00.00.10\nOK\n') proto.transport.clear() proto.dataReceived('DATA NICKNAME: spam STARTING: false RUNNING: true STOPPING: false KEYS: true QUIET: false INPORT: 12345 INHOST: localhost OUTPORT: 23456 OUTHOST: localhost\nOK Listing done\n') proto.transport.clear() proto.dataReceived('OK HTTP 418\n') self.assertEqual(proto.transport.value(), b'stop\n') def test_stopNotRequestedForStoppedTunnel(self): fac, proto = self.makeProto() fac.tunnelNick = 'spam' proto.dataReceived('BOB 00.00.10\nOK\n') proto.transport.clear() proto.dataReceived('DATA NICKNAME: spam STARTING: false RUNNING: false STOPPING: false KEYS: true QUIET: false INPORT: 12345 INHOST: localhost OUTPORT: 23456 OUTHOST: localhost\nOK Listing done\n') proto.transport.clear() proto.dataReceived('OK HTTP 418\n') self.assertNotEqual(proto.transport.value(), b'stop\n') # TODO: Refactor to test against what is actually expected def test_quitRequestedAfterStart(self): fac, proto = self.makeProto() called = [] fac.i2pTunnelCreated = lambda: called.append(True) # Shortcut proto.receiver.currentRule = 'State_start' proto._parser._setupInterp() proto.dataReceived('OK HTTP 418\n') self.assertEqual((called, proto.transport.value()), ([True], b'quit\n')) class TestI2PClientTunnelCreatorBOBClient(BOBTunnelCreationMixin, unittest.TestCase): protocol = I2PClientTunnelCreatorBOBClient def test_inhostRequestRepeatedIfActive(self): fac, proto = self.makeProto() fac.tunnelNick = 'spam' fac.inhost = 'camelot' # Shortcut proto.receiver.currentRule = 'State_inhost' proto._parser._setupInterp() proto.dataReceived('ERROR tunnel is active\n') self.assertEqual(proto.transport.value(), b'inhost camelot\n') def test_inhostRequestRepeatedIfShuttingDown(self): fac, proto = self.makeProto() fac.tunnelNick = 'spam' fac.inhost = 'camelot' # Shortcut proto.receiver.currentRule = 'State_inhost' proto._parser._setupInterp() proto.dataReceived('ERROR tunnel shutting down\n') self.assertEqual(proto.transport.value(), b'inhost camelot\n') def test_inhostSetAfterNickSetWithKeypair(self): fac, proto = self.makeProto() fac.tunnelNick = 'spam' fac.keypair = 'eggs' fac.inhost = 'camelot' proto.dataReceived('BOB 00.00.10\nOK\n') proto.transport.clear() proto.dataReceived('OK Listing done\n') # No DATA, no tunnels proto.transport.clear() proto.dataReceived('OK HTTP 418\n') proto.transport.clear() proto.dataReceived('OK HTTP 418 options set\n') proto.transport.clear() proto.dataReceived('OK HTTP 418\n') proto.transport.clear() proto.dataReceived('OK shrubbery\n') # The Destination self.assertEqual(proto.transport.value(), b'inhost camelot\n') def test_inhostSetAfterNickSetWithNoKeypair(self): fac, proto = self.makeProto() fac.tunnelNick = 'spam' fac.inhost = 'camelot' proto.dataReceived('BOB 00.00.10\nOK\n') proto.transport.clear() proto.dataReceived('OK Listing done\n') # No DATA, no tunnels proto.transport.clear() proto.dataReceived('OK HTTP 418\n') proto.transport.clear() proto.dataReceived('OK HTTP 418 options set\n') proto.transport.clear() proto.dataReceived('OK shrubbery\n') # The new Destination proto.transport.clear() proto.dataReceived('OK rubberyeggs\n') # The new keypair self.assertEqual(proto.transport.value(), b'inhost camelot\n') def test_defaultInportSet(self): fac, proto = self.makeProto() fac.tunnelNick = 'spam' fac.inhost = 'camelot' proto.dataReceived('BOB 00.00.10\nOK\n') proto.transport.clear() proto.dataReceived('OK Listing done\n') # No DATA, no tunnels proto.transport.clear() proto.dataReceived('OK HTTP 418\n') proto.transport.clear() proto.dataReceived('OK HTTP 418 options set\n') proto.transport.clear() proto.dataReceived('OK shrubbery\n') # The new Destination proto.transport.clear() proto.dataReceived('OK rubberyeggs\n') # The new keypair proto.transport.clear() proto.dataReceived('OK HTTP 418\n') self.assertEqual(proto.transport.value().decode('utf-8'), 'inport %d\n' % DEFAULT_INPORT) def test_inportSet(self): fac, proto = self.makeProto() fac.tunnelNick = 'spam' fac.inhost = 'camelot' fac.inport = '1234' proto.dataReceived('BOB 00.00.10\nOK\n') proto.transport.clear() proto.dataReceived('OK Listing done\n') # No DATA, no tunnels proto.transport.clear() proto.dataReceived('OK HTTP 418\n') proto.transport.clear() proto.dataReceived('OK HTTP 418 options set\n') proto.transport.clear() proto.dataReceived('OK shrubbery\n') # The new Destination proto.transport.clear() proto.dataReceived('OK rubberyeggs\n') # The new keypair proto.transport.clear() proto.dataReceived('OK HTTP 418\n') self.assertEqual(proto.transport.value(), b'inport 1234\n') def test_startRequested(self): fac, proto = self.makeProto() fac.tunnelNick = 'spam' fac.inhost = 'camelot' fac.inport = '1234' proto.dataReceived('BOB 00.00.10\nOK\n') proto.transport.clear() proto.dataReceived('OK Listing done\n') # No DATA, no tunnels proto.transport.clear() proto.dataReceived('OK HTTP 418\n') proto.transport.clear() proto.dataReceived('OK HTTP 418 options set\n') proto.transport.clear() proto.dataReceived('OK shrubbery\n') # The new Destination proto.transport.clear() proto.dataReceived('OK rubberyeggs\n') # The new keypair proto.transport.clear() proto.dataReceived('OK HTTP 418\n') proto.transport.clear() proto.dataReceived('OK HTTP 418\n') self.assertEqual(proto.transport.value(), b'start\n') class TestI2PServerTunnelCreatorBOBClient(BOBTunnelCreationMixin, unittest.TestCase): protocol = I2PServerTunnelCreatorBOBClient def test_outhostRequestRepeatedIfActive(self): fac, proto = self.makeProto() fac.tunnelNick = 'spam' fac.outhost = 'camelot' # Shortcut proto.receiver.currentRule = 'State_outhost' proto._parser._setupInterp() proto.dataReceived('ERROR tunnel is active\n') self.assertEqual(proto.transport.value(), b'outhost camelot\n') def test_outhostRequestRepeatedIfShuttingDown(self): fac, proto = self.makeProto() fac.tunnelNick = 'spam' fac.outhost = 'camelot' # Shortcut proto.receiver.currentRule = 'State_outhost' proto._parser._setupInterp() proto.dataReceived('ERROR tunnel shutting down\n') self.assertEqual(proto.transport.value(), b'outhost camelot\n') def test_outhostSetAfterNickSetWithKeypair(self): fac, proto = self.makeProto() fac.tunnelNick = 'spam' fac.keypair = 'eggs' fac.outhost = 'camelot' proto.dataReceived('BOB 00.00.10\nOK\n') proto.transport.clear() proto.dataReceived('OK Listing done\n') # No DATA, no tunnels proto.transport.clear() proto.dataReceived('OK HTTP 418\n') proto.transport.clear() proto.dataReceived('OK HTTP 418 options set\n') proto.transport.clear() proto.dataReceived('OK HTTP 418\n') proto.transport.clear() proto.dataReceived('OK shrubbery\n') # The Destination self.assertEqual(proto.transport.value(), b'outhost camelot\n') def test_outhostSetAfterNickSetWithNoKeypair(self): fac, proto = self.makeProto() fac.tunnelNick = 'spam' fac.outhost = 'camelot' proto.dataReceived('BOB 00.00.10\nOK\n') proto.transport.clear() proto.dataReceived('OK Listing done\n') # No DATA, no tunnels proto.transport.clear() proto.dataReceived('OK HTTP 418\n') proto.transport.clear() proto.dataReceived('OK HTTP 418 options set\n') proto.transport.clear() proto.dataReceived('OK shrubbery\n') # The new Destination proto.transport.clear() proto.dataReceived('OK rubberyeggs\n') # The new keypair self.assertEqual(proto.transport.value(), b'outhost camelot\n') def test_defaultOutportSet(self): fac, proto = self.makeProto() fac.tunnelNick = 'spam' fac.outhost = 'camelot' proto.dataReceived('BOB 00.00.10\nOK\n') proto.transport.clear() proto.dataReceived('OK Listing done\n') # No DATA, no tunnels proto.transport.clear() proto.dataReceived('OK HTTP 418\n') proto.transport.clear() proto.dataReceived('OK HTTP 418 options set\n') proto.transport.clear() proto.dataReceived('OK shrubbery\n') # The new Destination proto.transport.clear() proto.dataReceived('OK rubberyeggs\n') # The new keypair proto.transport.clear() proto.dataReceived('OK HTTP 418\n') self.assertEqual(proto.transport.value().decode('utf-8'), 'outport %d\n' % DEFAULT_OUTPORT) def test_outportSet(self): fac, proto = self.makeProto() fac.tunnelNick = 'spam' fac.outhost = 'camelot' fac.outport = '1234' proto.dataReceived('BOB 00.00.10\nOK\n') proto.transport.clear() proto.dataReceived('OK Listing done\n') # No DATA, no tunnels proto.transport.clear() proto.dataReceived('OK HTTP 418\n') proto.transport.clear() proto.dataReceived('OK HTTP 418 options set\n') proto.transport.clear() proto.dataReceived('OK shrubbery\n') # The new Destination proto.transport.clear() proto.dataReceived('OK rubberyeggs\n') # The new keypair proto.transport.clear() proto.dataReceived('OK HTTP 418\n') self.assertEqual(proto.transport.value(), b'outport 1234\n') def test_startRequested(self): fac, proto = self.makeProto() fac.tunnelNick = 'spam' fac.outhost = 'camelot' fac.outport = '1234' proto.dataReceived('BOB 00.00.10\nOK\n') proto.transport.clear() proto.dataReceived('OK Listing done\n') # No DATA, no tunnels proto.transport.clear() proto.dataReceived('OK HTTP 418\n') proto.transport.clear() proto.dataReceived('OK HTTP 418 options set\n') proto.transport.clear() proto.dataReceived('OK shrubbery\n') # The new Destination proto.transport.clear() proto.dataReceived('OK rubberyeggs\n') # The new keypair proto.transport.clear() proto.dataReceived('OK HTTP 418\n') proto.transport.clear() proto.dataReceived('OK HTTP 418\n') self.assertEqual(proto.transport.value(), b'start\n') class TestI2PTunnelRemoverBOBClient(BOBProtoTestMixin, unittest.TestCase): protocol = I2PTunnelRemoverBOBClient def test_noTunnelWithNick(self): fac, proto = self.makeProto() fac.tunnelNick = 'spam' proto.dataReceived('BOB 00.00.10\nOK\n') proto.transport.clear() proto.dataReceived('OK Listing done\n') # No DATA, no tunnels self.assertEqual(proto.transport.value(), b'') def test_tunnelExistsGetsNick(self): fac, proto = self.makeProto() fac.tunnelNick = 'spam' proto.dataReceived('BOB 00.00.10\nOK\n') proto.transport.clear() proto.dataReceived('DATA NICKNAME: spam STARTING: false RUNNING: true STOPPING: false KEYS: true QUIET: false INPORT: 12345 INHOST: localhost OUTPORT: 23456 OUTHOST: localhost\nOK Listing done\n') self.assertEqual(proto.transport.value(), b'getnick spam\n') def test_stopRequested(self): fac, proto = self.makeProto() fac.tunnelNick = 'spam' proto.dataReceived('BOB 00.00.10\nOK\n') proto.transport.clear() proto.dataReceived('DATA NICKNAME: spam STARTING: false RUNNING: true STOPPING: false KEYS: true QUIET: false INPORT: 12345 INHOST: localhost OUTPORT: 23456 OUTHOST: localhost\nOK Listing done\n') proto.transport.clear() proto.dataReceived('OK HTTP 418\n') self.assertEqual(proto.transport.value(), b'stop\n') def test_clearRequested(self): fac, proto = self.makeProto() fac.tunnelNick = 'spam' proto.dataReceived('BOB 00.00.10\nOK\n') proto.transport.clear() proto.dataReceived('DATA NICKNAME: spam STARTING: false RUNNING: true STOPPING: false KEYS: true QUIET: false INPORT: 12345 INHOST: localhost OUTPORT: 23456 OUTHOST: localhost\nOK Listing done\n') proto.transport.clear() proto.dataReceived('OK HTTP 418\n') proto.transport.clear() proto.dataReceived('OK HTTP 418\n') self.assertEqual(proto.transport.value(), b'clear\n') def test_clearRequestRepeatedIfActive(self): fac, proto = self.makeProto() fac.tunnelNick = 'spam' # Shortcut proto.receiver.currentRule = 'State_clear' proto._parser._setupInterp() proto.dataReceived('ERROR tunnel is active\n') self.assertEqual(proto.transport.value(), b'clear\n') def test_clearRequestRepeatedIfShuttingDown(self): fac, proto = self.makeProto() fac.tunnelNick = 'spam' # Shortcut proto.receiver.currentRule = 'State_clear' proto._parser._setupInterp() proto.dataReceived('ERROR tunnel shutting down\n') self.assertEqual(proto.transport.value(), b'clear\n') def test_quitRequestedAfterClearSuccess(self): fac, proto = self.makeProto() fac.tunnelNick = 'spam' fac.i2pTunnelRemoved = lambda: None # Shortcut proto.receiver.currentRule = 'State_clear' proto._parser._setupInterp() proto.dataReceived('OK HTTP 418\n') self.assertEqual(proto.transport.value(), b'quit\n') class FakeDisconnectingFactory(object): def i2pConnectionLost(self, wrappedProto, reason): wrappedProto.connectionLost(reason) class TestI2PClientTunnelProtocol(unittest.TestCase): def makeProto(self): wrappedProto = proto_helpers.AccumulatingProtocol() proto = I2PClientTunnelProtocol(wrappedProto, None, TEST_B64) proto.factory = FakeDisconnectingFactory() transport = proto_helpers.StringTransportWithDisconnection() transport.abortConnection = lambda: None transport.protocol = proto proto.makeConnection(transport) return proto def test_destRequested(self): proto = self.makeProto() self.assertEqual(proto.transport.value().decode('utf-8'), '%s\n' % TEST_B64) def test_wrappedProtoConnectionMade(self): proto = self.makeProto() self.assertEqual(proto.transport, proto.wrappedProto.transport) def test_connectionFailed(self): proto = self.makeProto() proto.dataReceived("ERROR Can't find destination: spam.i2p") self.assertEqual(proto.wrappedProto.closed, 1) expected = UnknownHostError(string="Can't find destination: spam.i2p") got = proto.wrappedProto.closedReason.value self.assertEqual(type(expected), type(got)) self.assertEqual(expected.args, got.args) def test_dataPassed(self): proto = self.makeProto() proto.dataReceived('shrubbery') self.assertEqual(proto.wrappedProto.data, b'shrubbery') class TestI2PServerTunnelProtocol(unittest.TestCase): def makeProto(self): wrappedProto = proto_helpers.AccumulatingProtocol() proto = I2PServerTunnelProtocol(wrappedProto, None) transport = proto_helpers.StringTransport() transport.abortConnection = lambda: None proto.makeConnection(transport) return proto def test_wrappedProtoConnectionMade(self): proto = self.makeProto() self.assertEqual(proto.transport, proto.wrappedProto.transport) def test_peerDestStored(self): proto = self.makeProto() proto.dataReceived('%s\n' % TEST_B64) self.assertEqual(proto.peer.destination, TEST_B64) def test_dataAfterPeerDestPassed(self): proto = self.makeProto() proto.dataReceived('%s\n' % TEST_B64) proto.dataReceived('shrubbery') self.assertEqual(proto.wrappedProto.data, b'shrubbery') txi2p-0.3.7/txi2p/grammar.py000066400000000000000000000135461432127672400156560ustar00rootroot00000000000000# Copyright (c) str4d # See COPYING for details. # General I2P grammar i2pGrammarSource = r""" digit = anything:x ?(x in '0123456789') number = :ds -> int(ds) b64char = :x ?(x in 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-~') -> x b64 = """ # BOB grammar bobGrammarSource = i2pGrammarSource + r""" KEYS = b64:keys KEY = b64:pubkey ERROR = 'ERROR ' <(~'\n' anything)*>:desc '\n' -> (False, desc) OK = 'OK ' <(~'\n' anything)*>:info '\n' -> (True, info) OK_KEY = 'OK ' KEY:pubkey '\n' -> (True, pubkey) OK_KEYS = 'OK ' KEYS:keys '\n' -> (True, keys) DATA = 'DATA ' <(~'\n' anything)*>:data '\n' -> data TUNNEL_STATUS = 'NICKNAME: ' <(~' ' anything)*>:nickname ' STARTING: ' <'true'|'false'>:starting ' RUNNING: ' <'true'|'false'>:running ' STOPPING: ' <'true'|'false'>:stopping ' KEYS: ' <'true'|'false'>:keys ' QUIET: ' <'true'|'false'>:quiet ' INPORT: ' <'not_set'|number>:inport ' INHOST: ' <(~' ' anything)*>:inhost ' OUTPORT: ' <'not_set'|number>:outport ' OUTHOST: ' <(~'\n' anything)*>:outhost '\n' -> { 'nickname': nickname, 'starting': starting=='true', 'running': running=='true', 'stopping': stopping=='true', 'keys': keys=='true', 'quiet': quiet=='true', 'inport': None if inport=='not_set' else int(inport), 'inhost': inhost, 'outport': None if outport=='not_set' else int(outport), 'outhost': outhost } DATA_TUNNEL_STATUS = 'DATA ' TUNNEL_STATUS:status -> status OK_TUNNEL_STATUS = 'OK ' TUNNEL_STATUS:status -> (True, status) OK_DATA_TUNNEL_STATUS = 'OK ' DATA_TUNNEL_STATUS:status -> (True, status) versionString = BOB_init = 'BOB ' versionString:version '\nOK\n' -> version BOB_clear = (ERROR | OK) BOB_getdest = (ERROR | OK_KEY) BOB_getkeys = (ERROR | OK_KEYS) BOB_getnick = (ERROR | OK) BOB_inhost = (ERROR | OK) BOB_inport = (ERROR | OK) BOB_list = ((ERROR:(result, info) -> (result, info, [])) |((DATA_TUNNEL_STATUS)*:data OK:(result, info) -> (result, info, data))) BOB_newkeys = (ERROR | OK_KEY) BOB_option = (ERROR | OK) BOB_outhost = (ERROR | OK) BOB_outport = (ERROR | OK) BOB_quiet = (ERROR | OK) BOB_quit = (OK) BOB_setkeys = (ERROR | OK) BOB_setnick = (ERROR | OK) BOB_show = ((ERROR:(result, info) -> (result, info, {})) |(OK_TUNNEL_STATUS:(result, status) -> (result, '', status))) BOB_showprops = (ERROR | OK) BOB_start = (ERROR | OK) BOB_status = ((ERROR:(result, info) -> (result, info, {})) |(OK_DATA_TUNNEL_STATUS:(result, status) -> (result, '', status))) BOB_stop = (ERROR | OK) BOB_verify = (ERROR | OK) BOB_visit = (OK) State_init = BOB_init:version -> receiver.initBOB(version) State_clear = BOB_clear:response -> receiver.clear(*response) State_getdest = BOB_getdest:response -> receiver.getdest(*response) State_getkeys = BOB_getkeys:response -> receiver.getkeys(*response) State_getnick = BOB_getnick:response -> receiver.getnick(*response) State_inhost = BOB_inhost:response -> receiver.inhost(*response) State_inport = BOB_inport:response -> receiver.inport(*response) State_list = BOB_list:response -> receiver.list(*response) State_newkeys = BOB_newkeys:response -> receiver.newkeys(*response) State_option = BOB_option:response -> receiver.option(*response) State_outhost = BOB_outhost:response -> receiver.outhost(*response) State_outport = BOB_outport:response -> receiver.outport(*response) State_quiet = BOB_quiet:response -> receiver.quiet(*response) State_quit = BOB_quit:response -> receiver.quit(*response) State_setkeys = BOB_setkeys:response -> receiver.setkeys(*response) State_setnick = BOB_setnick:response -> receiver.setnick(*response) State_show = BOB_show:response -> receiver.show(*response) State_showprops = BOB_showprops:response -> receiver.showprops(*response) State_start = BOB_start:response -> receiver.start(*response) State_status = BOB_status:response -> receiver.status(*response) State_stop = BOB_stop:response -> receiver.stop(*response) State_verify = BOB_verify:response -> receiver.verify(*response) State_visit = BOB_visit:response -> receiver.visit(*response) """ # SAM grammar samGrammarSource = i2pGrammarSource + r""" KEY = <(~'=' anything)*> VALUE = (('"' <(~'"' anything)*>:value '"' -> value) |(<(~(' '|'\n') anything)*>)) OPTION = KEY:key '=' VALUE:value -> (key.lower(), value) OPTIONS = (OPTION:first (' ' OPTION)*:rest -> dict([first] + rest)) | -> {} SAM_hello = 'HELLO REPLY ' OPTIONS:options '\n' -> options SAM_session_status = 'SESSION STATUS ' OPTIONS:options '\n' -> options SAM_stream_status = 'STREAM STATUS ' OPTIONS:options '\n' -> options SAM_naming_reply = 'NAMING REPLY ' OPTIONS:options '\n' -> options SAM_dest_reply = 'DEST REPLY ' OPTIONS:options '\n' -> options State_hello = SAM_hello:options -> receiver.hello(**options) State_create = SAM_session_status:options -> receiver.create(**options) State_connect = SAM_stream_status:options -> receiver.connect(**options) State_accept = SAM_stream_status:options -> receiver.accept(**options) State_forward = SAM_stream_status:options -> receiver.forward(**options) State_naming = SAM_naming_reply:options -> receiver.lookupReply(**options) State_dest = SAM_dest_reply:options -> receiver.destGenerated(**options) State_readData = anything:data -> receiver.dataReceived(data) KEEPALIVE_DATA = (' '+ <(~'\n' anything)+>:data -> data) | -> None SAM_ping = 'PING' KEEPALIVE_DATA:data '\n' -> data SAM_pong = 'PONG' KEEPALIVE_DATA:data '\n' -> data State_keepalive = ((SAM_ping:data -> receiver.ping(data)) |(SAM_pong:data -> receiver.pong(data))) """ txi2p-0.3.7/txi2p/plugins.py000066400000000000000000000104271432127672400157040ustar00rootroot00000000000000# Copyright (c) str4d # See COPYING for details. from twisted.internet.endpoints import clientFromString from twisted.internet.interfaces import IStreamClientEndpointStringParserWithReactor from twisted.internet.interfaces import IStreamServerEndpointStringParser from zope.interface import implementer from txi2p.bob.endpoints import BOBI2PClientEndpoint, BOBI2PServerEndpoint from txi2p.sam.endpoints import ( SAMI2PStreamClientEndpoint, SAMI2PStreamServerEndpoint, ) from txi2p.utils import getApi from twisted.plugin import IPlugin def _parseOptions(options): return dict([option.split(':') for option in options.split(',')]) if options else {} @implementer(IPlugin, IStreamClientEndpointStringParserWithReactor) class I2PClientParser: prefix = 'i2p' def _parseBOBClient(self, reactor, host, port, bobEndpoint, tunnelNick=None, inhost='localhost', inport=None, options=None): return BOBI2PClientEndpoint(reactor, clientFromString(reactor, bobEndpoint), host, port, tunnelNick, inhost, inport and int(inport) or None, _parseOptions(options)) def _parseSAMClient(self, reactor, host, port, samEndpoint, nickname=None, autoClose=False, keyfile=None, localPort=None, options=None, sigType=None): return SAMI2PStreamClientEndpoint.new( clientFromString(reactor, samEndpoint), host, port, nickname, autoClose, keyfile, localPort and int(localPort) or None, _parseOptions(options), sigType) _apiParsers = { 'BOB': _parseBOBClient, 'SAM': _parseSAMClient, } def _parseClient(self, reactor, host, port=None, api=None, apiEndpoint=None, **kwargs): api, apiEndpoint = getApi(api, apiEndpoint, self._apiParsers) return self._apiParsers[api](self, reactor, host, port and int(port) or None, apiEndpoint, **kwargs) def parseStreamClient(self, reactor, *args, **kwargs): # Delegate to another function with a sane signature. This function has # an insane signature to trick zope.interface into believing the # interface is correctly implemented. return self._parseClient(reactor, *args, **kwargs) @implementer(IPlugin, IStreamServerEndpointStringParser) class I2PServerParser: prefix = 'i2p' def _parseBOBServer(self, reactor, keyfile, port, bobEndpoint, tunnelNick=None, outhost='localhost', outport=None, options=None): return BOBI2PServerEndpoint(reactor, clientFromString(reactor, bobEndpoint), keyfile, port, tunnelNick, outhost, outport and int(outport) or None, _parseOptions(options)) def _parseSAMServer(self, reactor, keyfile, port, samEndpoint, nickname=None, autoClose=False, options=None, sigType=None): return SAMI2PStreamServerEndpoint.new( clientFromString(reactor, samEndpoint), keyfile, port, nickname, autoClose, _parseOptions(options), sigType) _apiParsers = { 'BOB': _parseBOBServer, 'SAM': _parseSAMServer, } def _parseServer(self, reactor, keyfile, port=None, api=None, apiEndpoint=None, **kwargs): api, apiEndpoint = getApi(api, apiEndpoint, self._apiParsers) return self._apiParsers[api](self, reactor, keyfile, port and int(port) or None, apiEndpoint, **kwargs) def parseStreamServer(self, reactor, *args, **kwargs): # Delegate to another function with a sane signature. This function has # an insane signature to trick zope.interface into believing the # interface is correctly implemented. return self._parseServer(reactor, *args, **kwargs) txi2p-0.3.7/txi2p/sam/000077500000000000000000000000001432127672400144255ustar00rootroot00000000000000txi2p-0.3.7/txi2p/sam/__init__.py000066400000000000000000000002721432127672400165370ustar00rootroot00000000000000from .endpoints import ( SAMI2PStreamClientEndpoint, SAMI2PStreamServerEndpoint, ) from .session import ( SAMSession, generateDestination, getSession, testAPI, ) txi2p-0.3.7/txi2p/sam/base.py000066400000000000000000000154361432127672400157220ustar00rootroot00000000000000# Copyright (c) str4d # See COPYING for details. from builtins import str from builtins import object import functools from ometa.grammar import OMeta from ometa.protocol import ParserProtocol import re import time from twisted.internet import reactor from twisted.internet.interfaces import IProtocolFactory from twisted.internet.protocol import ClientFactory from twisted.python.failure import Failure from zope.interface import implementer from txi2p import grammar from txi2p.address import ( I2PAddress, I2PServerTunnelProtocol, I2PTunnelTransport, ) from txi2p.sam import constants as c KEEPALIVE_TIMEOUT = 2 * 60 def cmpSAM(a, b): def normalize(v): return [int(x) for x in re.sub(r'(\.0+)*$','', v).split(".")] a_n = normalize(a) b_n = normalize(b) return (a_n > b_n) - (a_n < b_n) def peerSAM(data): peerInfo = data.decode('utf-8').split('\n')[0].split(' ') peerOptions = {x: y for x, y in [x.split('=', 1) for x in peerInfo[1:] if x]} fromPort = peerOptions['FROM_PORT'] if 'FROM_PORT' in peerOptions else None return I2PAddress(peerInfo[0], port=fromPort) class SAMParserProtocol(ParserProtocol): def __init__(self, *args): ParserProtocol.__init__(self, *args) def dataReceived(self, data): """ Receive and parse some data. :param data: A ``bytes`` from Twisted. """ if self._disconnecting: return if self.receiver.currentRule == 'State_readData': # Shortcut for efficiency self.receiver.dataReceived(data) else: # Duplicated from Parsley because it expects a str but Twisted # provides a bytes. try: self._parser.receive(data.decode('utf-8')) except Exception: self.connectionLost(Failure()) self.transport.abortConnection() return def makeSAMProtocol(senderFactory, receiverFactory): g = OMeta(grammar.samGrammarSource).parseGrammar('Grammar') return functools.partial( SAMParserProtocol, g, senderFactory, receiverFactory, {}) class SAMSender(object): def __init__(self, transport): self.transport = transport def sendHello(self): self.transport.write(b'HELLO VERSION MIN=3.0 MAX=3.2\n') def sendNamingLookup(self, name): msg = 'NAMING LOOKUP NAME=%s\n' % name self.transport.write(msg.encode('utf-8')) def sendPing(self, data): if data: self.transport.write(('PING %s\n' % data).encode('utf-8')) else: self.transport.write(b'PING\n') def sendPong(self, data): if data: self.transport.write(('PONG %s\n' % data).encode('utf-8')) else: self.transport.write(b'PONG\n') class SAMReceiver(object): wrappedProto = None currentRule = 'State_hello' pinger = None lastPing = '' pingTimeout = None def __init__(self, sender): self.sender = sender def prepareParsing(self, parser): # Store the factory for later use self.factory = parser.factory self.sender.sendHello() def wrapProto(self, proto, peerAddress, invertTLS=False): self.wrappedProto = proto if hasattr(self.factory, 'localPort'): localAddress = I2PAddress(self.factory.session.address, port=self.factory.localPort) else: localAddress = self.factory.session.address self.transportWrapper = I2PTunnelTransport( self.sender.transport, localAddress, peerAddress, invertTLS) proto.makeConnection(self.transportWrapper) def dataReceived(self, data): self.wrappedProto.dataReceived(data) def finishParsing(self, reason): if self.wrappedProto: self.wrappedProto.connectionLost(reason) else: self.factory.connectionFailed(reason) if hasattr(self.factory, 'session'): self.factory.session.removeStream(self) def hello(self, result, version=None, message=None): if result != c.RESULT_OK: self.factory.resultNotOK(result, message) return self.factory.samVersion = version self.command() def lookupReply(self, result, name, value=None, message=None): if result != c.RESULT_OK: self.factory.resultNotOK(result, message) return self.postLookup(value) def _sendPing(self): self.lastPing = str(time.time()) self.sender.sendPing(self.lastPing) self.pingTimeout = reactor.callLater(KEEPALIVE_TIMEOUT, self.sender.transport.loseConnection) def _resetPingTimeout(self): if self.pingTimeout: self.pingTimeout.cancel() self.pinger = reactor.callLater(KEEPALIVE_TIMEOUT, self._sendPing) def ping(self, data): self.sender.sendPong(data) self._resetPingTimeout() def pong(self, data): if (data == str(self.lastPing)): self._resetPingTimeout() def startPinging(self): self.pinger = reactor.callLater(KEEPALIVE_TIMEOUT, self._sendPing) self.currentRule = 'State_keepalive' def stopPinging(self): if self.pinger and self.pinger.active(): self.pinger.cancel() if self.pingTimeout and self.pingTimeout.active(): self.pingTimeout.cancel() class SAMFactory(ClientFactory): currentCandidate = None canceled = False def _cancel(self, d): self.currentCandidate.sender.transport.abortConnection() self.canceled = True def buildProtocol(self, addr): proto = self.protocol() proto.factory = self self.currentCandidate = proto return proto def connectionFailed(self, reason): if not self.canceled and not self.deferred.called: self.deferred.errback(reason) # This method is not called if an endpoint deferred errbacks def clientConnectionFailed(self, connector, reason): self.connectionFailed(reason) def resultNotOK(self, result, message): raise c.samErrorMap.get(result)(string=(message if message else result)) class SAMI2PServerTunnelProtocol(I2PServerTunnelProtocol): def setPeer(self, data): self.peer = peerSAM(data) self.transport.peerAddr = self.peer @implementer(IProtocolFactory) class I2PFactoryWrapper(object): protocol = SAMI2PServerTunnelProtocol def __init__(self, wrappedFactory, serverAddr): self.w = wrappedFactory self.serverAddr = serverAddr def buildProtocol(self, addr): wrappedProto = self.w.buildProtocol(addr) proto = self.protocol(wrappedProto, self.serverAddr) proto.factory = self return proto def __getattr__(self, attr): return getattr(self.w, attr) txi2p-0.3.7/txi2p/sam/constants.py000066400000000000000000000016061432127672400170160ustar00rootroot00000000000000# Copyright (c) str4d # See COPYING for details. from twisted.internet.error import ( ConnectBindError, ConnectError, NoRouteError, TCPTimedOutError, UnknownHostError, ) DEFAULT_SIGTYPE = 'EdDSA_SHA512_Ed25519' RESULT_OK = 'OK' RESULT_CANT_REACH_PEER = 'CANT_REACH_PEER' RESULT_DUPLICATED_DEST = 'DUPLICATED_DEST' RESULT_I2P_ERROR = 'I2P_ERROR' RESULT_INVALID_KEY = 'INVALID_KEY' RESULT_KEY_NOT_FOUND = 'KEY_NOT_FOUND' RESULT_PEER_NOT_FOUND = 'PEER_NOT_FOUND' RESULT_INVALID_ID = 'INVALID_ID' RESULT_TIMEOUT = 'TIMEOUT' samErrorMap = { RESULT_CANT_REACH_PEER: NoRouteError, RESULT_DUPLICATED_DEST: ConnectBindError, RESULT_I2P_ERROR: ConnectError, RESULT_INVALID_KEY: ConnectError, RESULT_KEY_NOT_FOUND: UnknownHostError, RESULT_PEER_NOT_FOUND: NoRouteError, RESULT_INVALID_ID: ConnectError, RESULT_TIMEOUT: TCPTimedOutError, } txi2p-0.3.7/txi2p/sam/endpoints.py000066400000000000000000000224241432127672400170060ustar00rootroot00000000000000# Copyright (c) str4d # See COPYING for details. from builtins import object from twisted.internet import defer, error, interfaces from twisted.internet.endpoints import serverFromString from zope.interface import implementer from txi2p.sam.base import I2PFactoryWrapper from txi2p.sam.session import SAMSession, getSession from txi2p.sam.stream import ( StreamConnectFactory, StreamAcceptPort, StreamForwardFactory, StreamForwardPort, ) def _parseHost(host): # TODO: Validate I2P domain, B32 etc. return (host, None) if host[-4:] == '.i2p' else (None, host) @implementer(interfaces.IStreamClientEndpoint) class SAMI2PStreamClientEndpoint(object): """I2P stream client endpoint backed by the SAM API. Args: session (txi2p.sam.SAMSession): The SAM session to connect with. host (str): The I2P hostname or Destination to connect to. port (int): The port to connect to inside I2P. If unset or `None`, the default (null) port is used. Ignored if the SAM server doesn't support SAM v3.2 or higher. localPort (int): The port to connect from inside I2P. This can be used to distinguish between multiple connections to the same server. If unset or `None`, the default (null) port is used. Ignored if the SAM server doesn't support SAM v3.2 or higher. """ @classmethod def new(cls, samEndpoint, host, port=None, nickname=None, autoClose=False, keyfile=None, localPort=None, options=None, sigType=None): """Create an I2P client endpoint backed by the SAM API. If a SAM session for ``nickname`` already exists, it will be used, and all options other than ``host`` and ``port`` will be ignored. Otherwise, a new SAM session will be created. The implication of this is that by default, all endpoints (both client and server) created by the same process will use the same SAM session. Args: samEndpoint (twisted.internet.interfaces.IStreamClientEndpoint): An endpoint that will connect to the SAM API. host (str): The I2P hostname or Destination to connect to. port (int): The port to connect to inside I2P. If unset or `None`, the default (null) port is used. Ignored if the SAM server doesn't support SAM v3.2 or higher. nickname (str): The SAM session nickname. autoClose (bool): `true` if the session should close automatically once no more connections are using it. keyfile (str): Path to a local file containing the keypair to use for the session Destination. If non-existent, new keys will be generated and stored. localPort (int): The port to connect from inside I2P. This can be used to distinguish between multiple connections to the same server. If unset or `None`, the default (null) port is used. Ignored if the SAM server doesn't support SAM v3.2 or higher. options (dict): I2CP options to configure the session with. sigType (str): The SigType to use if generating a new Destination. Defaults to Ed25519 if supported, falling back to ECDSA_SHA256_P256 and then DSA_SHA1. """ d = getSession(nickname, samEndpoint=samEndpoint, autoClose=autoClose, keyfile=keyfile, options=options, sigType=sigType) return cls(d, host, port, localPort) def __init__(self, session, host, port=None, localPort=None): self._host, self._dest = _parseHost(host) self._port = port self._localPort = localPort if isinstance(session, SAMSession): self._session = session else: self._session = None self._sessionDeferred = session def connect(self, fac): """Connect over I2P. The provided factory will have its ``buildProtocol`` method called once an I2P client tunnel has been successfully created. If the factory's ``buildProtocol`` returns ``None``, the connection will immediately close. """ def createStream(val): if self._session.style != 'STREAM': raise error.UnsupportedSocketType() i2pFac = StreamConnectFactory(fac, self._session, self._host, self._dest, self._port, self._localPort) d = self._session.samEndpoint.connect(i2pFac) # Once the SAM IProtocol is returned, wait for the # real IProtocol to be returned after tunnel creation, # and pass it to any further registered callbacks. d.addCallback(lambda proto: i2pFac.deferred) return d if self._session: return createStream(None) def saveSession(session): self._session = session return None self._sessionDeferred.addCallback(saveSession) self._sessionDeferred.addCallback(createStream) return self._sessionDeferred @implementer(interfaces.IStreamServerEndpoint) class SAMI2PStreamServerEndpoint(object): """I2P server endpoint backed by the SAM API. Args: session (txi2p.sam.SAMSession): The SAM session to listen on. """ @classmethod def new(cls, samEndpoint, keyfile, port=None, nickname=None, autoClose=False, options=None, sigType=None): """Create an I2P server endpoint backed by the SAM API. If a SAM session for ``nickname`` already exists, it will be used, and all options other than ``port`` will be ignored. Otherwise, a new SAM session will be created. The implication of this is that by default, all endpoints (both client and server) created by the same process will use the same SAM session. Args: samEndpoint (twisted.internet.interfaces.IStreamClientEndpoint): An endpoint that will connect to the SAM API. keyfile (str): Path to a local file containing the keypair to use for the session Destination. If non-existent, new keys will be generated and stored. port (int): The port to listen on inside I2P. If unset or `None`, the default (null) port is used. Ignored if the SAM server doesn't support SAM v3.2 or higher. nickname (str): The SAM session nickname. autoClose (bool): `true` if the session should close automatically once no more connections are using it. options (dict): I2CP options to configure the session with. sigType (str): The SigType to use if generating a new Destination. Defaults to Ed25519 if supported, falling back to ECDSA_SHA256_P256 and then DSA_SHA1. """ d = getSession(nickname, samEndpoint=samEndpoint, autoClose=autoClose, keyfile=keyfile, localPort=port, options=options, sigType=sigType) return cls(d) def __init__(self, session): if isinstance(session, SAMSession): self._session = session else: self._session = None self._sessionDeferred = session def listen(self, fac): """Listen over I2P. The provided factory will have its ``buildProtocol`` method called once an I2P server tunnel has been successfully created. If the factory's ``buildProtocol`` returns ``None``, the connection will immediately close. """ def createAcceptingStream(val): if self._session.style != 'STREAM': raise error.UnsupportedSocketType() p = StreamAcceptPort(self._session, fac) p.startListening() return p # def createForwardingStream(val): # if self._session.style != 'STREAM': # raise error.UnsupportedSocketType() # # serverEndpoint = serverFromString(self._reactor, # 'tcp:0:interface=127.0.0.1') # wrappedFactory = I2PFactoryWrapper(fac, self._session.address) # d = serverEndpoint.listen(wrappedFactory) # # def setupForward(port): # local_port = port.getHost().port # i2pFac = StreamForwardFactory(self._session, local_port) # d2 = self._session.samEndpoint.connect(i2pFac) # d2.addCallback(lambda proto: i2pFac.deferred) # d2.addCallback(lambda forwardingProto: (port, forwardingProto)) # return d2 # # def handlePort((port, forwardingProto)): # return StreamForwardPort(port, forwardingProto, self._session.address) # # d.addCallback(setupForward) # d.addCallback(handlePort) # return d if self._session: return createAcceptingStream(None) def saveSession(session): self._session = session return None self._sessionDeferred.addCallback(saveSession) self._sessionDeferred.addCallback(createAcceptingStream) return self._sessionDeferred txi2p-0.3.7/txi2p/sam/session.py000066400000000000000000000317741432127672400164760ustar00rootroot00000000000000# Copyright (c) str4d # See COPYING for details. from __future__ import print_function from builtins import object import os import sys from twisted.internet import defer, error from twisted.python import failure, log from txi2p import grammar from txi2p.address import I2PAddress from txi2p.sam import constants as c from txi2p.sam.base import ( cmpSAM, makeSAMProtocol, SAMSender, SAMReceiver, SAMFactory, ) def eprint(*args, **kwargs): print(*args, file=sys.stderr, **kwargs) class SessionCreateSender(SAMSender): def sendSessionCreate(self, samVersion, style, id, privKey, localPort, options, sigType): msg = 'SESSION CREATE' msg += ' STYLE=%s' % style msg += ' ID=%s' % id msg += ' DESTINATION=%s' % (privKey if privKey else 'TRANSIENT') if cmpSAM(samVersion, '3.1') >= 0 and not privKey: msg += ' SIGNATURE_TYPE=%s' % (sigType and sigType or c.DEFAULT_SIGTYPE) if localPort: msg += ' FROM_PORT=%d' % localPort for key in options: msg += ' %s=%s' % (key, options[key]) msg += '\n' self.transport.write(msg.encode('utf-8')) class SessionCreateReceiver(SAMReceiver): def command(self): if not (hasattr(self.factory, 'nickname') and self.factory.nickname): # All tunnels in the same process use the same nickname # TODO is using the PID a security risk? self.factory.nickname = 'txi2p-%d' % os.getpid() self.sender.sendSessionCreate( self.factory.samVersion, self.factory.style, self.factory.nickname, self.factory.privKey, self.factory.localPort, self.factory.options, self.factory.sigType) self.currentRule = 'State_create' def create(self, result, destination=None, message=None): if result != c.RESULT_OK: # If the user didn't specify a SigType, try falling back if cmpSAM(self.factory.samVersion, '3.1') >= 0 and \ message.startswith('SIGNATURE_TYPE') and \ not self.factory.sigType: fallback = 'ECDSA_SHA256_P256' in message and 'DSA_SHA1' or 'ECDSA_SHA256_P256' eprint('Warning: %s, falling back to %s' % (message, fallback)) self.sender.sendSessionCreate( self.factory.samVersion, self.factory.style, self.factory.nickname, self.factory.privKey, self.factory.localPort, self.factory.options, fallback) else: self.factory.resultNotOK(result, message) return self.factory.privKey = destination self.sender.sendNamingLookup('ME') self.currentRule = 'State_naming' def postLookup(self, dest): # Help keep the session open if cmpSAM(self.factory.samVersion, '3.2') >= 0: self.startPinging() else: try: self.sender.transport.setTcpKeepAlive(1) except AttributeError as e: eprint(e) self.factory.sessionCreated(self, dest) # A Protocol for making a SAM session SessionCreateProtocol = makeSAMProtocol( SessionCreateSender, SessionCreateReceiver) class SessionCreateFactory(SAMFactory): protocol = SessionCreateProtocol def __init__(self, nickname, style='STREAM', keyfile=None, localPort=None, options=None, sigType=None): if style != 'STREAM': raise error.UnsupportedSocketType() if options is None: options = {} self.nickname = nickname self.style = style self._keyfile = keyfile self.localPort = localPort self.options = options self.sigType = sigType self.deferred = defer.Deferred(self._cancel) self.samVersion = None self.privKey = None self._writeKeypair = False def startFactory(self): if self._keyfile: try: f = open(self._keyfile, 'r') self.privKey = f.read() f.close() except IOError: log.msg('Could not load private key from %s' % self._keyfile) self._writeKeypair = True def sessionCreated(self, proto, pubKey): if self._writeKeypair: try: f = open(self._keyfile, 'w') f.write(str(self.privKey)) f.close() except IOError: log.msg('Could not save private key to %s' % self._keyfile) # Now continue on with creation of SAMSession self.deferred.callback((self.samVersion, self.style, self.nickname, proto, pubKey, self.localPort)) # Dictionary containing all active SAM sessions _sessions = {} # Dictionary containing all pending SAM sessions _pending_sessions = {} class SAMSession(object): """A SAM session represents an active I2P Destination. Attributes: nickname (str): The user-assigned session nickname, can be `None`. samEndpoint (twisted.internet.interfaces.IStreamClientEndpoint): An endpoint that will connect to the SAM API. samVersion (str): The SAM version in use by this session. style (str): The session style. id (str): SAM Session ID, autogenerated if ``nickname`` is None, else ``nickname``. address (txi2p.I2PAddress): The Destination of this session. """ def __init__(self): self.nickname = None self.samEndpoint = None self.samVersion = '' self.style = 'STREAM' self.id = None self.address = None self._proto = None self._autoClose = False self._closed = False self._streams = [] def addStream(self, stream): """Register a stream with this session. Raises: twisted.internet.error.ConnectionDone: if the session is closed. """ if self._closed: raise error.ConnectionDone self._streams.append(stream) def removeStream(self, stream): """Remove a stream from this session. If this was the last stream, and the session was set to auto-close, the session will close and become unusable. Raises: twisted.internet.error.ConnectionDone: if the session is closed. """ if self._closed: raise error.ConnectionDone # Streams are only added once they have been established if stream in self._streams: self._streams.remove(stream) if not self._streams and self._autoClose: # No more streams, close the session self.close() def close(self): """Close the session.""" self._closed = True self._streams = [] self._proto.sender.transport.loseConnection() del _sessions[self.nickname] def getSession(nickname, samEndpoint=None, autoClose=False, **kwargs): """Get or create a SAM session. Args: nickname (str): The session nickname. samEndpoint (twisted.internet.interfaces.IStreamClientEndpoint): An endpoint that will connect to the SAM API. autoClose (bool): `true` if the session should close automatically once no more connections are using it. """ if nickname in _sessions: return defer.succeed(_sessions[nickname]) elif nickname in _pending_sessions: def cancel(d): if nickname in _pending_sessions and d in _pending_sessions[nickname]: _pending_sessions[nickname].remove(d) d = defer.Deferred(cancel) _pending_sessions[nickname].append(d) return d if not samEndpoint: raise ValueError('A new session cannot be created without an API Endpoint') def createSession(xxx_todo_changeme): (samVersion, style, id, proto, pubKey, localPort) = xxx_todo_changeme s = SAMSession() s.nickname = nickname s.samEndpoint = samEndpoint s.samVersion = samVersion s.style = style s.id = id s.address = I2PAddress(pubKey, port=localPort) s._proto = proto s._autoClose = autoClose _sessions[nickname] = s waiting = _pending_sessions.pop(nickname, []) for d in waiting: d.callback(s) return s def errbackPending(f): waiting = _pending_sessions.pop(nickname, []) for d in waiting: d.errback(f) return f _pending_sessions[nickname] = [] sessionFac = SessionCreateFactory(nickname, **kwargs) d = samEndpoint.connect(sessionFac) # Force caller to wait until the session is actually created d.addCallback(lambda proto: sessionFac.deferred) d.addCallback(createSession) d.addErrback(errbackPending) return d class DestGenerateSender(SAMSender): def sendDestGenerate(self, samVersion, sigType=None): msg = 'DEST GENERATE' if cmpSAM(samVersion, '3.1') >= 0: msg += ' SIGNATURE_TYPE=%s' % (sigType and sigType or 'EdDSA_SHA512_Ed25519') msg += '\n' self.transport.write(msg.encode('utf-8')) class DestGenerateReceiver(SAMReceiver): def command(self): self.sender.sendDestGenerate( self.factory.samVersion, self.factory.sigType) self.currentRule = 'State_dest' def destGenerated(self, result=None, pub=None, priv=None, message=None): if result: # If the user didn't specify a SigType, try falling back if cmpSAM(self.factory.samVersion, '3.1') >= 0 and \ message.startswith('SIGNATURE_TYPE') and \ not self.factory.sigType: fallback = 'ECDSA_SHA256_P256' in message and 'DSA_SHA1' or 'ECDSA_SHA256_P256' eprint('Warning: %s, falling back to %s' % (message, fallback)) self.sender.sendDestGenerate( self.factory.samVersion, fallback) else: self.factory.resultNotOK(result, message) return self.factory.destGenerated(pub, priv) self.sender.transport.loseConnection() # A Protocol for generating an I2P Destination via SAM DestGenerateProtocol = makeSAMProtocol( DestGenerateSender, DestGenerateReceiver) class DestGenerateFactory(SAMFactory): protocol = DestGenerateProtocol def __init__(self, keyfile, sigType=None): self._keyfile = keyfile self.sigType = sigType self.deferred = defer.Deferred(self._cancel) def destGenerated(self, pubKey, privKey): if os.path.exists(self._keyfile): self.deferred.errback(failure.Failure(ValueError('The keyfile already exists'))) return try: f = open(self._keyfile, 'w') f.write(str(privKey)) f.close() self.deferred.callback(I2PAddress(pubKey)) except IOError as e: self.deferred.errback(failure.Failure(e)) def generateDestination(keyfile, samEndpoint, sigType=None): """Generate a new I2P Destination. The function returns a :class:`twisted.internet.defer.Deferred`; register callbacks to receive the return value or errors. Args: keyfile (str): Path to a local file where the keypair for the new Destination should be stored. samEndpoint (twisted.internet.interfaces.IStreamClientEndpoint): An endpoint that will connect to the SAM API. sigType (str): The SigType to generate. Defaults to Ed25519 if supported, falling back to ECDSA_SHA256_P256 and then DSA_SHA1. Returns: txi2p.I2PAddress: The new Destination. Once this is received via the Deferred callback, the ``keyfile`` will have been written. Raises: ValueError: if the ``keyfile`` already exists. IOError: if the ``keyfile`` write fails. """ destFac = DestGenerateFactory(keyfile, sigType) d = samEndpoint.connect(destFac) d.addCallback(lambda proto: destFac.deferred) return d class TestAPIReceiver(SAMReceiver): def command(self): self.factory.samConnected() self.sender.transport.loseConnection() # A Protocol for testing whether a SAM API is reachable TestAPIProtocol = makeSAMProtocol( SAMSender, TestAPIReceiver) class TestAPIFactory(SAMFactory): protocol = TestAPIProtocol def __init__(self): self.deferred = defer.Deferred(self._cancel) def samConnected(self): self.deferred.callback(True) def testAPI(samEndpoint): """Test whether a SAM API is reachable. The function returns a :class:`twisted.internet.defer.Deferred`; register callbacks to receive the return value or errors. Args: samEndpoint (twisted.internet.interfaces.IStreamClientEndpoint): An endpoint that may connect to the SAM API. Returns: True if the API is reachable. """ testFac = TestAPIFactory() d = samEndpoint.connect(testFac) d.addCallback(lambda proto: testFac.deferred) return d txi2p-0.3.7/txi2p/sam/stream.py000066400000000000000000000170061432127672400162760ustar00rootroot00000000000000# Copyright (c) str4d # See COPYING for details. from builtins import range from builtins import object from twisted.internet.defer import Deferred from twisted.internet.error import ConnectError, UnknownHostError from twisted.internet.interfaces import IListeningPort from zope.interface import implementer from txi2p.address import I2PAddress from txi2p.sam import constants as c from txi2p.sam.base import ( cmpSAM, peerSAM, makeSAMProtocol, SAMSender, SAMReceiver, SAMFactory, ) class StreamConnectSender(SAMSender): def sendStreamConnect(self, id, destination, port=None, localPort=None): msg = 'STREAM CONNECT' msg += ' ID=%s' % id msg += ' DESTINATION=%s' % destination msg += ' SILENT=false' if port: msg += ' TO_PORT=%d' % port if localPort: msg += ' FROM_PORT=%d' % localPort msg += '\n' self.transport.write(msg.encode('utf-8')) class StreamConnectReceiver(SAMReceiver): def command(self): if self.factory.dest: self.doConnect() else: self.sender.sendNamingLookup(self.factory.host) self.currentRule = 'State_naming' def postLookup(self, dest): self.factory.dest = dest self.doConnect() def doConnect(self): self.sender.sendStreamConnect( self.factory.session.id, self.factory.dest, self.factory.port, self.factory.localPort) self.currentRule = 'State_connect' def connect(self, result, message=None): if result != c.RESULT_OK: self.factory.resultNotOK(result, message) return self.factory.streamConnectionEstablished(self) self.currentRule = 'State_readData' StreamConnectProtocol = makeSAMProtocol( StreamConnectSender, StreamConnectReceiver) class StreamConnectFactory(SAMFactory): protocol = StreamConnectProtocol def __init__(self, clientFactory, session, host, dest, port=None, localPort=None): self._clientFactory = clientFactory self.session = session self.host = host self.dest = dest self.port = port self.localPort = localPort self.deferred = Deferred(self._cancel); def streamConnectionEstablished(self, streamProto): self.session.addStream(streamProto) peerAddress = I2PAddress(self.dest, self.host, self.port) proto = self._clientFactory.buildProtocol(peerAddress) if proto is None: self.deferred.cancel() return streamProto.wrapProto(proto, peerAddress) self.deferred.callback(proto) class StreamAcceptSender(SAMSender): def sendStreamAccept(self, id): msg = 'STREAM ACCEPT' msg += ' ID=%s' % id msg += ' SILENT=false' msg += '\n' self.transport.write(msg.encode('utf-8')) class StreamAcceptReceiver(SAMReceiver): peer = None initialData = b'' def command(self): self.sender.sendStreamAccept( self.factory.session.id) self.currentRule = 'State_accept' def accept(self, result, message=None): if result != c.RESULT_OK: self.factory.resultNotOK(result, message) return self.factory.streamAcceptEstablished(self) self.currentRule = 'State_readData' def dataReceived(self, data): if self.peer: # Pass all other data to the wrapped Protocol. self.wrappedProto.dataReceived(data) else: self.initialData += data if b'\n' in self.initialData: # First line is the peer's Destination. data, self.initialData = self.initialData.split(b'\n', 1) self.peer = peerSAM(data) # Create the wrapped Protocol... self.factory.streamAcceptIncoming(self) # ... and pass through any initial data. if self.initialData: data, self.initialData = self.initialData, None self.wrappedProto.dataReceived(data) StreamAcceptProtocol = makeSAMProtocol( StreamAcceptSender, StreamAcceptReceiver) class StreamAcceptFactory(SAMFactory): protocol = StreamAcceptProtocol def __init__(self, clientFactory, session, listeningPort): self._clientFactory = clientFactory self.session = session self.listeningPort = listeningPort self.deferred = Deferred(self._cancel); def streamAcceptEstablished(self, streamProto): self.session.addStream(streamProto) self.listeningPort.addAccept(streamProto) def streamAcceptIncoming(self, streamProto): self.listeningPort.removeAccept(streamProto) proto = self._clientFactory.buildProtocol(streamProto.peer) if proto is None: self.deferred.cancel() return streamProto.wrapProto(proto, streamProto.peer, True) @implementer(IListeningPort) class StreamAcceptPort(object): def __init__(self, session, factory): self.session = session self.factory = StreamAcceptFactory(factory, session, self) self.accepts = [] def startListening(self): if cmpSAM(self.session.samVersion, '3.2') >= 0: active = 8 else: active = 1 for i in range(0, active): self.openAccept() def stopListening(self): for pending in self.accepts: pending.sender.transport.loseConnection() self.accepts = [] def openAccept(self): self.session.samEndpoint.connect(self.factory) def addAccept(self, proto): self.accepts.append(proto) def removeAccept(self, proto): self.accepts.remove(proto) self.openAccept() def getHost(self): return self.session.address class StreamForwardSender(SAMSender): def sendStreamForward(self, id, port, host=None): msg = 'STREAM FORWARD' msg += ' ID=%s' % id msg += ' PORT=%s' % port if host: msg += ' HOST=%s' % host msg += ' SILENT=false' msg += '\n' self.transport.write(msg.encode('utf-8')) class StreamForwardReceiver(SAMReceiver): def command(self): self.sender.sendStreamForward( self.factory.session.id, self.factory.forwardPort) self.currentRule = 'State_forward' def forward(self, result, message=None): if result != c.RESULT_OK: self.factory.resultNotOK(result, message) return self.factory.streamForwardEstablished(self) StreamForwardProtocol = makeSAMProtocol( StreamForwardSender, StreamForwardReceiver) class StreamForwardFactory(SAMFactory): protocol = StreamForwardProtocol def __init__(self, session, forwardPort): self.session = session self.forwardPort = forwardPort self.deferred = Deferred(self._cancel); def streamForwardEstablished(self, forwardingProto): self.session.addStream(forwardingProto) self.deferred.callback(forwardingProto) @implementer(IListeningPort) class StreamForwardPort(object): def __init__(self, listeningPort, forwardingProto, serverAddr): self._listeningPort = listeningPort self._forwardingProto = forwardingProto self._serverAddr = serverAddr def startListening(self): self._listeningPort.startListening() def stopListening(self): self._listeningPort.stopListening() self._forwardingProto.sender.transport.loseConnection() def getHost(self): return self._serverAddr txi2p-0.3.7/txi2p/sam/test/000077500000000000000000000000001432127672400154045ustar00rootroot00000000000000txi2p-0.3.7/txi2p/sam/test/__init__.py000066400000000000000000000000001432127672400175030ustar00rootroot00000000000000txi2p-0.3.7/txi2p/sam/test/test_endpoints.py000066400000000000000000000111051432127672400210160ustar00rootroot00000000000000# Copyright (c) str4d # See COPYING for details. from twisted.internet.error import ConnectionLost, ConnectionRefusedError from twisted.internet.protocol import Factory, Protocol from twisted.internet.interfaces import IStreamServerEndpoint from twisted.python import failure from twisted.test import proto_helpers from twisted.trial import unittest from txi2p.sam import endpoints from txi2p.sam.session import SAMSession from txi2p.test.util import FakeEndpoint, FakeFactory, fakeSession connectionLostFailure = failure.Failure(ConnectionLost()) connectionRefusedFailure = failure.Failure(ConnectionRefusedError()) def sessionOptionsFromNew(cls, **kwargs): """ Create a new session factory and return the value of its ``options`` attribute. :param cls: A class like ``endpoints.SAMI2PStreamClientEndpoint``. :return: A ``Deferred`` that fires with the ``options`` value. """ samEndpoint = FakeEndpoint(failure=connectionRefusedFailure) endpoint = cls.new(samEndpoint, '', **kwargs) if IStreamServerEndpoint.providedBy(endpoint): f = endpoint.listen else: f = endpoint.connect d = f(Factory.forProtocol(Protocol)) d.addErrback(lambda err: err.trap(ConnectionRefusedError)) d.addCallback(lambda ignored: samEndpoint.factory.options) return d class SAMI2PStreamClientEndpointTestCase(unittest.TestCase): """ Tests for I2P client Endpoint backed by the SAM API. """ endpointClass = endpoints.SAMI2PStreamClientEndpoint def test_newWithOptions(self): """ If ``SAMI2PStreamClientEndpoint.new`` is called options then ``SessionCreateFactory`` is created with those options. """ options = {'inbound.length': 5, 'outbound.length': 5} d = sessionOptionsFromNew(self.endpointClass, options=options) self.assertEqual(self.successResultOf(d), options) def test_newWithoutOptions(self): """ If ``SAMI2PStreamClientEndpoint.new`` is called without options then ``SessionCreateFactory`` is created with an empty options dictionary. """ d = sessionOptionsFromNew(self.endpointClass) self.assertEqual(self.successResultOf(d), {}) def test_samConnectionFailed(self): samEndpoint = FakeEndpoint(failure=connectionRefusedFailure) endpoint = endpoints.SAMI2PStreamClientEndpoint.new(samEndpoint, '') d = endpoint.connect(Factory.forProtocol(Protocol)) self.failureResultOf(d, ConnectionRefusedError) def test_streamConnect(self): samEndpoint = FakeEndpoint() session = SAMSession() session.nickname = 'foo' session.samEndpoint = samEndpoint session.samVersion = '3.1' session.id = 'foo' session._autoClose = True endpoint = endpoints.SAMI2PStreamClientEndpoint(session, 'foo.i2p') endpoint.connect(None) self.assertSubstring('HELLO VERSION', samEndpoint.transport.value().decode('utf-8')) class SAMI2PStreamServerEndpointTestCase(unittest.TestCase): """ Tests for I2P server Endpoint backed by the SAM API. """ endpointClass = endpoints.SAMI2PStreamServerEndpoint def test_newWithOptions(self): """ If ``SAMI2PStreamServerEndpoint.new`` is called options then ``SessionCreateFactory`` is created with those options. """ options = {'inbound.length': 5, 'outbound.length': 5} d = sessionOptionsFromNew(self.endpointClass, options=options) self.assertEqual(self.successResultOf(d), options) def test_newWithoutOptions(self): """ If ``SAMI2PStreamServerEndpoint.new`` is called without options then ``SessionCreateFactory`` is created with an empty options dictionary. """ d = sessionOptionsFromNew(self.endpointClass) self.assertEqual(self.successResultOf(d), {}) def test_samConnectionFailed(self): samEndpoint = FakeEndpoint(failure=connectionRefusedFailure) endpoint = endpoints.SAMI2PStreamServerEndpoint.new(samEndpoint, '') d = endpoint.listen(Factory.forProtocol(Protocol)) self.failureResultOf(d, ConnectionRefusedError) def test_streamListen(self): samEndpoint = FakeEndpoint() session = SAMSession() session.nickname = 'foo' session.samEndpoint = samEndpoint session.samVersion = '3.1' session.id = 'foo' session._autoClose = True endpoint = endpoints.SAMI2PStreamServerEndpoint(session) endpoint.listen(None) self.assertSubstring('HELLO VERSION', str(samEndpoint.transport.value())) txi2p-0.3.7/txi2p/sam/test/test_session.py000066400000000000000000000514721432127672400205110ustar00rootroot00000000000000# Copyright (c) str4d # See COPYING for details. from builtins import object try: # Python 3 from unittest.mock import Mock except: # Python 2 (library) from mock import Mock import os import twisted from twisted.internet import defer from twisted.python.versions import Version from twisted.test import proto_helpers from twisted.trial import unittest from txi2p.address import I2PAddress from txi2p.sam import session from txi2p.sam.constants import DEFAULT_SIGTYPE from txi2p.test.util import TEST_B64 from .util import SAMProtocolTestMixin, SAMFactoryTestMixin if twisted.version < Version('twisted', 12, 3, 0): skipSRO = 'TestCase.successResultOf() requires twisted 12.3 or newer' else: skipSRO = None class TestSessionCreateProtocol(SAMProtocolTestMixin, unittest.TestCase): protocol = session.SessionCreateProtocol def test_sessionCreateAfterHello(self): fac, proto = self.makeProto() fac.style = 'STREAM' proto.transport.clear() proto.dataReceived(b'HELLO REPLY RESULT=OK VERSION=3.1\n') self.assertEquals( 'SESSION CREATE STYLE=STREAM ID=foo DESTINATION=TRANSIENT SIGNATURE_TYPE=%s\n' % DEFAULT_SIGTYPE, proto.transport.value().decode('utf-8')) def test_sessionCreateAfterHelloWith3point0(self): fac, proto = self.makeProto() fac.style = 'STREAM' proto.transport.clear() proto.dataReceived(b'HELLO REPLY RESULT=OK VERSION=3.0\n') self.assertEquals( b'SESSION CREATE STYLE=STREAM ID=foo DESTINATION=TRANSIENT\n', proto.transport.value()) def test_sessionCreateAfterHelloWithSigType(self): fac, proto = self.makeProto() fac.style = 'STREAM' fac.sigType = 'foobar' proto.transport.clear() proto.dataReceived(b'HELLO REPLY RESULT=OK VERSION=3.1\n') self.assertEquals( b'SESSION CREATE STYLE=STREAM ID=foo DESTINATION=TRANSIENT SIGNATURE_TYPE=foobar\n', proto.transport.value()) def test_sessionCreateWithAutoNickAfterHello(self): fac, proto = self.makeProto() fac.nickname = None fac.style = 'STREAM' proto.transport.clear() proto.dataReceived(b'HELLO REPLY RESULT=OK VERSION=3.1\n') self.assertEquals( 'SESSION CREATE STYLE=STREAM ID=txi2p-%s DESTINATION=TRANSIENT SIGNATURE_TYPE=%s\n' % (os.getpid(), DEFAULT_SIGTYPE), proto.transport.value().decode('utf-8')) def test_sessionCreateWithPortAfterHello(self): fac, proto = self.makeProto() fac.style = 'STREAM' fac.localPort = 81 proto.transport.clear() proto.dataReceived(b'HELLO REPLY RESULT=OK VERSION=3.1\n') self.assertEquals( 'SESSION CREATE STYLE=STREAM ID=foo DESTINATION=TRANSIENT SIGNATURE_TYPE=%s FROM_PORT=81\n' % DEFAULT_SIGTYPE, proto.transport.value().decode('utf-8')) def test_sessionCreateWithOptionsAfterHello(self): fac, proto = self.makeProto() fac.style = 'STREAM' fac.options['bar'] = 'baz' proto.transport.clear() proto.dataReceived(b'HELLO REPLY RESULT=OK VERSION=3.1\n') self.assertEquals( 'SESSION CREATE STYLE=STREAM ID=foo DESTINATION=TRANSIENT SIGNATURE_TYPE=%s bar=baz\n' % DEFAULT_SIGTYPE, proto.transport.value().decode('utf-8')) def test_sessionCreateFallbackForUnsupportedDest(self): fac, proto = self.makeProto() fac.style = 'STREAM' proto.transport.clear() proto.dataReceived(b'HELLO REPLY RESULT=OK VERSION=3.1\n') proto.transport.clear() proto.dataReceived(b'SESSION STATUS RESULT=I2P_ERROR MESSAGE="SIGNATURE_TYPE foobar unsupported"\n') self.assertEquals( b'SESSION CREATE STYLE=STREAM ID=foo DESTINATION=TRANSIENT SIGNATURE_TYPE=ECDSA_SHA256_P256\n', proto.transport.value()) def test_sessionCreateSecondFallbackForUnsupportedDest(self): fac, proto = self.makeProto() fac.style = 'STREAM' proto.transport.clear() proto.dataReceived(b'HELLO REPLY RESULT=OK VERSION=3.1\n') proto.transport.clear() proto.dataReceived(b'SESSION STATUS RESULT=I2P_ERROR MESSAGE="SIGNATURE_TYPE ECDSA_SHA256_P256 unsupported"\n') self.assertEquals( b'SESSION CREATE STYLE=STREAM ID=foo DESTINATION=TRANSIENT SIGNATURE_TYPE=DSA_SHA1\n', proto.transport.value()) def test_sessionCreateReturnsUnsupportedDestWithSigType(self): fac, proto = self.makeProto() fac.style = 'STREAM' fac.sigType = DEFAULT_SIGTYPE proto.transport.clear() proto.dataReceived(b'HELLO REPLY RESULT=OK VERSION=3.1\n') proto.transport.clear() proto.dataReceived(('SESSION STATUS RESULT=I2P_ERROR MESSAGE="SIGNATURE_TYPE %s unsupported"\n' % DEFAULT_SIGTYPE).encode('utf-8')) fac.resultNotOK.assert_called_with('I2P_ERROR', 'SIGNATURE_TYPE %s unsupported' % DEFAULT_SIGTYPE) def test_sessionCreateReturnsUnsupportedDestAgainWithSigType(self): fac, proto = self.makeProto() fac.style = 'STREAM' fac.sigType = 'ECDSA_SHA256_P256' proto.transport.clear() proto.dataReceived(b'HELLO REPLY RESULT=OK VERSION=3.1\n') proto.transport.clear() proto.dataReceived(b'SESSION STATUS RESULT=I2P_ERROR MESSAGE="SIGNATURE_TYPE ECDSA_SHA256_P256 unsupported"\n') fac.resultNotOK.assert_called_with('I2P_ERROR', 'SIGNATURE_TYPE ECDSA_SHA256_P256 unsupported') def test_sessionCreateReturnsError(self): fac, proto = self.makeProto() fac.style = 'STREAM' proto.transport.clear() proto.dataReceived(b'HELLO REPLY RESULT=OK VERSION=3.1\n') proto.transport.clear() proto.dataReceived(b'SESSION STATUS RESULT=I2P_ERROR MESSAGE="foo bar baz"\n') fac.resultNotOK.assert_called_with('I2P_ERROR', 'foo bar baz') def test_namingLookupAfterSessionCreate(self): fac, proto = self.makeProto() fac.style = 'STREAM' proto.transport.clear() proto.dataReceived(b'HELLO REPLY RESULT=OK VERSION=3.1\n') proto.transport.clear() proto.dataReceived(('SESSION STATUS RESULT=OK DESTINATION=%s\n' % TEST_B64).encode('utf-8')) self.assertEquals( b'NAMING LOOKUP NAME=ME\n', proto.transport.value()) def test_sessionCreatedAfterNamingLookup(self): fac, proto = self.makeProto() fac.style = 'STREAM' fac.sessionCreated = Mock() proto.transport.clear() proto.dataReceived(b'HELLO REPLY RESULT=OK VERSION=3.1\n') proto.transport.clear() proto.dataReceived(('SESSION STATUS RESULT=OK DESTINATION=%s\n' % TEST_B64).encode('utf-8')) proto.transport.clear() proto.dataReceived(('NAMING REPLY RESULT=OK NAME=ME VALUE=%s\n' % TEST_B64).encode('utf-8')) fac.sessionCreated.assert_called_with(proto.receiver, TEST_B64) def test_sessionCreatedAndKeepaliveStartedAfterNamingLookupWith3_2(self): fac, proto = self.makeProto() fac.style = 'STREAM' fac.sessionCreated = Mock() proto.transport.clear() proto.dataReceived(b'HELLO REPLY RESULT=OK VERSION=3.2\n') proto.transport.clear() proto.dataReceived(('SESSION STATUS RESULT=OK DESTINATION=%s\n' % TEST_B64).encode('utf-8')) proto.transport.clear() proto.dataReceived(('NAMING REPLY RESULT=OK NAME=ME VALUE=%s\n' % TEST_B64).encode('utf-8')) self.assertEquals('State_keepalive', proto.receiver.currentRule) fac.sessionCreated.assert_called_with(proto.receiver, TEST_B64) # Cleanup self.addCleanup(proto.receiver.stopPinging) class FakeEndpoint(object): def __init__(self, failure=None): self.failure = failure self.deferred = None self.facDeferred = None self.called = 0 def connect(self, fac): self.called += 1 fac.deferred = self.facDeferred self.factory = fac return self.deferred class TestSessionCreateFactory(SAMFactoryTestMixin, unittest.TestCase): factory = session.SessionCreateFactory blankFactoryArgs = ('',) def test_startFactory(self): tmp = '/tmp/TestSessionCreateFactory.privKey' fac, proto = self.makeProto('foo', keyfile=tmp) fac.doStart() self.assertTrue(fac._writeKeypair) def test_startFactoryWithExistingKeyfile(self): tmp = '/tmp/TestSessionCreateFactory.privKey' f = open(tmp, 'w') f.write(u'foo') f.close() fac, proto = self.makeProto('foo', keyfile=tmp) fac.doStart() self.assertEqual('foo', fac.privKey) os.remove(tmp) def test_sessionCreated(self): mreactor = proto_helpers.MemoryReactor() fac, proto = self.makeProto('foo') fac.samVersion = '3.1' # Shortcut to end of SAM session create protocol proto.receiver.currentRule = 'State_naming' proto._parser._setupInterp() proto.dataReceived(('NAMING REPLY RESULT=OK NAME=ME VALUE=%s\n' % TEST_B64).encode('utf-8')) s = self.successResultOf(fac.deferred) self.assertEqual(('3.1', 'STREAM', 'foo', proto.receiver, TEST_B64, None), s) test_sessionCreated.skip = skipSRO def test_sessionCreatedWithKeyfile(self): tmp = '/tmp/TestSessionCreateFactory.privKey' mreactor = proto_helpers.MemoryReactor() fac, proto = self.makeProto('foo', keyfile=tmp) fac.samVersion = '3.1' fac.privKey = 'bar' fac._writeKeypair = True # Shortcut to end of SAM session create protocol proto.receiver.currentRule = 'State_naming' proto._parser._setupInterp() proto.dataReceived(('NAMING REPLY RESULT=OK NAME=ME VALUE=%s\n' % TEST_B64).encode('utf-8')) f = open(tmp, 'r') privKey = f.read() f.close() self.assertEqual('bar', privKey) # Cleanup os.remove(tmp) class TestSAMSession(unittest.TestCase): def setUp(self): self.tr = proto_helpers.StringTransportWithDisconnection() proto = Mock() proto.sender = Mock() proto.sender.transport = self.tr self.tr.protocol = proto self.s = session.SAMSession() self.s.nickname = 'foo' self.s.samVersion = '3.1' self.s.id = 'foo' self.s._proto = proto session._sessions['foo'] = self.s def tearDown(self): session._sessions = {} def test_addStream(self): self.assertEqual([], self.s._streams) self.s.addStream('foo') self.assertEqual(['foo'], self.s._streams) def test_removeStream_autoClose(self): self.s._autoClose = True self.s.addStream('bar') self.s.addStream('baz') self.s.removeStream('bar') self.assertEqual(['baz'], self.s._streams) self.assertEqual(True, 'foo' in session._sessions) self.assertEqual(self.s, session._sessions['foo']) self.s.removeStream('baz') self.assertEqual([], self.s._streams) self.assertEqual({}, session._sessions) def test_removeStream_noAutoClose(self): self.s.addStream('bar') self.s.addStream('baz') self.s.removeStream('bar') self.assertEqual(['baz'], self.s._streams) self.assertEqual(True, 'foo' in session._sessions) self.assertEqual(self.s, session._sessions['foo']) self.s.removeStream('baz') self.assertEqual([], self.s._streams) self.assertEqual({'foo': self.s}, session._sessions) class TestGetSession(unittest.TestCase): def tearDown(self): session._sessions = {} def test_getSession_newNickname(self): proto = proto_helpers.AccumulatingProtocol() samEndpoint = FakeEndpoint() samEndpoint.deferred = defer.succeed(None) samEndpoint.facDeferred = defer.succeed(('3.1', 'STREAM', 'nick', proto, TEST_B64, None)) d = session.getSession('nick', samEndpoint) s = self.successResultOf(d) self.assertEqual(1, samEndpoint.called) self.assertEqual('nick', s.nickname) self.assertEqual('nick', s.id) self.assertEqual(proto, s._proto) self.assertEqual(TEST_B64, s.address.destination) self.assertEqual(None, s.address.port) test_getSession_newNickname.skip = skipSRO def test_getSession_newNickname_withPort(self): proto = proto_helpers.AccumulatingProtocol() samEndpoint = FakeEndpoint() samEndpoint.deferred = defer.succeed(None) samEndpoint.facDeferred = defer.succeed(('3.2', 'STREAM', 'nick', proto, TEST_B64, 81)) d = session.getSession('nick', samEndpoint) s = self.successResultOf(d) self.assertEqual(1, samEndpoint.called) self.assertEqual('nick', s.nickname) self.assertEqual('nick', s.id) self.assertEqual(proto, s._proto) self.assertEqual(TEST_B64, s.address.destination) self.assertEqual(81, s.address.port) test_getSession_newNickname_withPort.skip = skipSRO def test_getSession_newNickname_withoutEndpoint(self): proto = proto_helpers.AccumulatingProtocol() samEndpoint = FakeEndpoint() samEndpoint.deferred = defer.succeed(None) samEndpoint.facDeferred = defer.succeed(('3.1', 'STREAM', 'nick', proto, TEST_B64, None)) self.assertRaises(ValueError, session.getSession, 'nick') test_getSession_newNickname_withoutEndpoint.skip = skipSRO def test_getSession_existingNickname(self): proto = proto_helpers.AccumulatingProtocol() samEndpoint = FakeEndpoint() samEndpoint.deferred = defer.succeed(None) samEndpoint.facDeferred = defer.succeed(('3.1', 'STREAM', 'nick', proto, TEST_B64, None)) d = session.getSession('nick', samEndpoint) s = self.successResultOf(d) d2 = session.getSession('nick', samEndpoint) s2 = self.successResultOf(d2) self.assertEqual(1, samEndpoint.called) self.assertEqual(s, s2) test_getSession_existingNickname.skip = skipSRO def test_getSession_existingNickname_withoutEndpoint(self): proto = proto_helpers.AccumulatingProtocol() samEndpoint = FakeEndpoint() samEndpoint.deferred = defer.succeed(None) samEndpoint.facDeferred = defer.succeed(('3.1', 'STREAM', 'nick', proto, TEST_B64, None)) d = session.getSession('nick', samEndpoint) s = self.successResultOf(d) d2 = session.getSession('nick') s2 = self.successResultOf(d2) self.assertEqual(1, samEndpoint.called) self.assertEqual(s, s2) test_getSession_existingNickname_withoutEndpoint.skip = skipSRO class TestDestGenerateProtocol(SAMProtocolTestMixin, unittest.TestCase): protocol = session.DestGenerateProtocol def test_destGenerateAfterHello(self): fac, proto = self.makeProto() proto.transport.clear() proto.dataReceived(b'HELLO REPLY RESULT=OK VERSION=3.1\n') self.assertEquals('DEST GENERATE SIGNATURE_TYPE=%s\n' % DEFAULT_SIGTYPE, proto.transport.value().decode('utf-8')) def test_destGenerateAfterHelloWith3point0(self): fac, proto = self.makeProto() proto.transport.clear() proto.dataReceived(b'HELLO REPLY RESULT=OK VERSION=3.0\n') self.assertEquals(b'DEST GENERATE\n', proto.transport.value()) def test_destGenerateAfterHelloWithSigType(self): fac, proto = self.makeProto() fac.sigType = 'foobar' proto.transport.clear() proto.dataReceived(b'HELLO REPLY RESULT=OK VERSION=3.1\n') self.assertEquals(b'DEST GENERATE SIGNATURE_TYPE=foobar\n', proto.transport.value()) def test_destGeneratedFallbackForUnsupportedDest(self): fac, proto = self.makeProto() proto.transport.clear() proto.dataReceived(b'HELLO REPLY RESULT=OK VERSION=3.1\n') proto.transport.clear() proto.dataReceived(('DEST REPLY RESULT=I2P_ERROR MESSAGE="SIGNATURE_TYPE %s unsupported"\n' % DEFAULT_SIGTYPE).encode('utf-8')) self.assertEquals(b'DEST GENERATE SIGNATURE_TYPE=ECDSA_SHA256_P256\n', proto.transport.value()) def test_destGeneratedSecondFallbackForUnsupportedDest(self): fac, proto = self.makeProto() proto.transport.clear() proto.dataReceived(b'HELLO REPLY RESULT=OK VERSION=3.1\n') proto.transport.clear() proto.dataReceived(b'DEST REPLY RESULT=I2P_ERROR MESSAGE="SIGNATURE_TYPE ECDSA_SHA256_P256 unsupported"\n') self.assertEquals(b'DEST GENERATE SIGNATURE_TYPE=DSA_SHA1\n', proto.transport.value()) def test_destGeneratedReturnsError(self): fac, proto = self.makeProto() proto.transport.clear() proto.dataReceived(b'HELLO REPLY RESULT=OK VERSION=3.1\n') proto.transport.clear() proto.dataReceived(b'DEST REPLY RESULT=I2P_ERROR MESSAGE="foo bar baz"\n') fac.resultNotOK.assert_called_with('I2P_ERROR', 'foo bar baz') def test_destGenerated(self): fac, proto = self.makeProto() fac.destGenerated = Mock() proto.transport.clear() proto.dataReceived(b'HELLO REPLY RESULT=OK VERSION=3.1\n') proto.transport.clear() proto.dataReceived(('DEST REPLY PUB=%s PRIV=%s\n' % (TEST_B64, 'TEST_PRIV')).encode('utf-8')) fac.destGenerated.assert_called_with(TEST_B64, 'TEST_PRIV') class TestDestGenerateFactory(SAMFactoryTestMixin, unittest.TestCase): factory = session.DestGenerateFactory blankFactoryArgs = ('', None) def test_destGenerated(self): tmp = '/tmp/TestDestGenerateFactory.privKey' mreactor = proto_helpers.MemoryReactor() fac, proto = self.makeProto(tmp) # Shortcut to end of SAM dest generate protocol proto.receiver.currentRule = 'State_dest' proto._parser._setupInterp() proto.dataReceived(('DEST REPLY PUB=%s PRIV=%s\n' % (TEST_B64, 'TEST_PRIV')).encode('utf-8')) s = self.successResultOf(fac.deferred) self.assertEqual(I2PAddress(TEST_B64), s) os.remove(tmp) test_destGenerated.skip = skipSRO def test_destGenerated_privKeySaved(self): tmp = '/tmp/TestDestGenerateFactory.privKey' mreactor = proto_helpers.MemoryReactor() fac, proto = self.makeProto(tmp) # Shortcut to end of SAM dest generate protocol proto.receiver.currentRule = 'State_dest' proto._parser._setupInterp() proto.dataReceived(('DEST REPLY PUB=%s PRIV=%s\n' % (TEST_B64, 'TEST_PRIV')).encode('utf-8')) f = open(tmp, 'r') privKey = f.read() f.close() self.assertEqual('TEST_PRIV', privKey) os.remove(tmp) def test_destGenerated_keyfileExists(self): tmp = '/tmp/TestDestGenerateFactory.privKey' f = open(tmp, 'w') f.write(u'foo') f.close() mreactor = proto_helpers.MemoryReactor() fac, proto = self.makeProto(tmp) # Shortcut to end of SAM dest generate protocol proto.receiver.currentRule = 'State_dest' proto._parser._setupInterp() proto.dataReceived(('DEST REPLY PUB=%s PRIV=%s\n' % (TEST_B64, 'TEST_PRIV')).encode('utf-8')) self.assertIsInstance(self.failureResultOf(fac.deferred).value, ValueError) os.remove(tmp) test_destGenerated_keyfileExists.skip = skipSRO class TestGenerateDestination(unittest.TestCase): def test_generateDestination(self): proto = proto_helpers.AccumulatingProtocol() samEndpoint = FakeEndpoint() samEndpoint.deferred = defer.succeed(None) samEndpoint.facDeferred = defer.succeed(I2PAddress(TEST_B64)) d = session.generateDestination('', samEndpoint) s = self.successResultOf(d) self.assertEqual(1, samEndpoint.called) self.assertEqual(I2PAddress(TEST_B64), s) test_generateDestination.skip = skipSRO class TestTestAPIProtocol(SAMProtocolTestMixin, unittest.TestCase): protocol = session.TestAPIProtocol def test_samConnected(self): fac, proto = self.makeProto() fac.samConnected = Mock() proto.transport.clear() proto.dataReceived(b'HELLO REPLY RESULT=OK VERSION=3.1\n') fac.samConnected.assert_called_with() class TestTestAPIFactory(SAMFactoryTestMixin, unittest.TestCase): factory = session.TestAPIFactory blankFactoryArgs = [] def test_samConnected(self): mreactor = proto_helpers.MemoryReactor() fac, proto = self.makeProto() proto.dataReceived(b'HELLO REPLY RESULT=OK VERSION=3.1\n') s = self.successResultOf(fac.deferred) self.assertEqual(True, s) test_samConnected.skip = skipSRO class TestTestAPI(unittest.TestCase): def test_testAPI(self): proto = proto_helpers.AccumulatingProtocol() samEndpoint = FakeEndpoint() samEndpoint.deferred = defer.succeed(None) samEndpoint.facDeferred = defer.succeed(True) d = session.testAPI(samEndpoint) s = self.successResultOf(d) self.assertEqual(1, samEndpoint.called) self.assertEqual(True, s) test_testAPI.skip = skipSRO txi2p-0.3.7/txi2p/sam/test/test_stream.py000066400000000000000000000314521432127672400203150ustar00rootroot00000000000000# Copyright (c) str4d # See COPYING for details. try: # Python 3 from unittest.mock import Mock except: # Python 2 (library) from mock import Mock import os import twisted from twisted.internet import defer from twisted.python.versions import Version from twisted.test import proto_helpers from twisted.trial import unittest from txi2p.address import I2PAddress from txi2p.sam import stream from txi2p.test.util import TEST_B64, FakeFactory from .util import SAMProtocolTestMixin, SAMFactoryTestMixin if twisted.version < Version('twisted', 12, 3, 0): skipSRO = 'TestCase.successResultOf() requires twisted 12.3 or newer' else: skipSRO = None class TestStreamConnectProtocol(SAMProtocolTestMixin, unittest.TestCase): protocol = stream.StreamConnectProtocol def test_streamConnectAfterHello(self): fac, proto = self.makeProto() fac.session = Mock() fac.session.id = 'foo' fac.dest = 'bar' proto.transport.clear() proto.dataReceived(b'HELLO REPLY RESULT=OK VERSION=3.1\n') self.assertEquals( b'STREAM CONNECT ID=foo DESTINATION=bar SILENT=false\n', proto.transport.value()) def test_streamConnectWithPortAfterHello(self): fac, proto = self.makeProto() fac.session = Mock() fac.session.id = 'foo' fac.dest = 'bar' fac.port = 80 proto.transport.clear() proto.dataReceived(b'HELLO REPLY RESULT=OK VERSION=3.1\n') self.assertEquals( b'STREAM CONNECT ID=foo DESTINATION=bar SILENT=false TO_PORT=80\n', proto.transport.value()) def test_streamConnectWithLocalPortAfterHello(self): fac, proto = self.makeProto() fac.session = Mock() fac.session.id = 'foo' fac.dest = 'bar' fac.localPort = 34444 proto.transport.clear() proto.dataReceived(b'HELLO REPLY RESULT=OK VERSION=3.1\n') self.assertEquals( b'STREAM CONNECT ID=foo DESTINATION=bar SILENT=false FROM_PORT=34444\n', proto.transport.value()) def test_namingLookupAfterHello(self): fac, proto = self.makeProto() fac.dest = None fac.host = 'spam.i2p' proto.transport.clear() proto.dataReceived(b'HELLO REPLY RESULT=OK VERSION=3.1\n') self.assertEquals( b'NAMING LOOKUP NAME=spam.i2p\n', proto.transport.value()) def test_namingLookupReturnsError(self): fac, proto = self.makeProto() fac.dest = None fac.host = 'spam.i2p' proto.transport.clear() proto.dataReceived(b'HELLO REPLY RESULT=OK VERSION=3.1\n') proto.transport.clear() proto.dataReceived(b'NAMING REPLY RESULT=KEY_NOT_FOUND NAME=spam.i2p MESSAGE="foo bar baz"\n') fac.resultNotOK.assert_called_with('KEY_NOT_FOUND', 'foo bar baz') def test_streamConnectAfterNamingLookup(self): fac, proto = self.makeProto() fac.session = Mock() fac.session.id = 'foo' fac.dest = None fac.host = 'spam.i2p' proto.transport.clear() proto.dataReceived(b'HELLO REPLY RESULT=OK VERSION=3.1\n') proto.transport.clear() proto.dataReceived(b'NAMING REPLY RESULT=OK NAME=spam.i2p VALUE=bar\n') self.assertEquals( b'STREAM CONNECT ID=foo DESTINATION=bar SILENT=false\n', proto.transport.value()) def test_streamConnectReturnsError(self): fac, proto = self.makeProto() fac.session = Mock() fac.session.id = 'foo' fac.dest = 'bar' proto.transport.clear() proto.dataReceived(b'HELLO REPLY RESULT=OK VERSION=3.1\n') proto.transport.clear() proto.dataReceived(b'STREAM STATUS RESULT=I2P_ERROR MESSAGE="foo bar baz"\n') fac.resultNotOK.assert_called_with('I2P_ERROR', 'foo bar baz') def test_streamConnectionEstablishedAfterStreamConnect(self): fac, proto = self.makeProto() fac.streamConnectionEstablished = Mock() fac.session = Mock() fac.session.id = 'foo' fac.dest = 'bar' proto.transport.clear() proto.dataReceived(b'HELLO REPLY RESULT=OK VERSION=3.1\n') proto.transport.clear() proto.dataReceived(b'STREAM STATUS RESULT=OK\n') fac.streamConnectionEstablished.assert_called_with(proto.receiver) class TestStreamConnectFactory(SAMFactoryTestMixin, unittest.TestCase): factory = stream.StreamConnectFactory blankFactoryArgs = (None, Mock(), '', '') def test_streamConnectionEstablished(self): mreactor = proto_helpers.MemoryReactor() wrappedFactory = FakeFactory() session = Mock() fac, proto = self.makeProto(wrappedFactory, session, 'spam.i2p', 'foo') # Shortcut to end of SAM stream connect protocol proto.receiver.currentRule = 'State_connect' proto._parser._setupInterp() proto.dataReceived(b'STREAM STATUS RESULT=OK\n') session.addStream.assert_called_with(proto.receiver) streamProto = self.successResultOf(fac.deferred) self.assertEqual(proto.receiver.wrappedProto, streamProto) test_streamConnectionEstablished.skip = skipSRO class TestStreamAcceptProtocol(SAMProtocolTestMixin, unittest.TestCase): protocol = stream.StreamAcceptProtocol def test_streamAcceptAfterHello(self): fac, proto = self.makeProto() fac.session = Mock() fac.session.id = 'foo' proto.transport.clear() proto.dataReceived(b'HELLO REPLY RESULT=OK VERSION=3.1\n') self.assertEquals( b'STREAM ACCEPT ID=foo SILENT=false\n', proto.transport.value()) def test_streamAcceptReturnsError(self): fac, proto = self.makeProto() fac.session = Mock() fac.session.id = 'foo' fac.forwardPort = 1337 proto.transport.clear() proto.dataReceived(b'HELLO REPLY RESULT=OK VERSION=3.1\n') proto.transport.clear() proto.dataReceived(b'STREAM STATUS RESULT=I2P_ERROR MESSAGE="foo bar baz"\n') fac.resultNotOK.assert_called_with('I2P_ERROR', 'foo bar baz') def test_streamAcceptEstablishedAfterStreamAccept(self): fac, proto = self.makeProto() fac.streamAcceptEstablished = Mock() fac.session = Mock() fac.session.id = 'foo' fac.forwardPort = 1337 proto.transport.clear() proto.dataReceived(b'HELLO REPLY RESULT=OK VERSION=3.1\n') proto.transport.clear() proto.dataReceived(b'STREAM STATUS RESULT=OK\n') fac.streamAcceptEstablished.assert_called_with(proto.receiver) def test_streamAcceptIncomingAfterPeerAddress(self): fac, proto = self.makeProto() fac.streamAcceptIncoming = Mock() fac.session = Mock() fac.session.id = 'foo' fac.forwardPort = 1337 # Shortcut to end of SAM stream accept protocol proto.receiver.currentRule = 'State_readData' proto._parser._setupInterp() proto.dataReceived(('%s FROM_PORT=34444 TO_PORT=0\n' % TEST_B64).encode('utf-8')) fac.streamAcceptIncoming.assert_called_with(proto.receiver) self.assertEquals( I2PAddress(TEST_B64, port=34444), proto.receiver.peer) def test_peerDataWrapped_allAtOnce(self): fac, proto = self.makeProto() fac.streamAcceptIncoming = Mock() fac.session = Mock() fac.session.id = 'foo' fac.forwardPort = 1337 proto.receiver.wrappedProto = proto_helpers.AccumulatingProtocol() # Shortcut to end of SAM stream accept protocol proto.receiver.currentRule = 'State_readData' proto._parser._setupInterp() proto.dataReceived(('%s FROM_PORT=34444 TO_PORT=0\nEgg and spam' % TEST_B64).encode('utf-8')) self.assertEquals(b'Egg and spam', proto.receiver.wrappedProto.data) def test_peerDataWrapped_twoParts(self): fac, proto = self.makeProto() fac.streamAcceptIncoming = Mock() fac.session = Mock() fac.session.id = 'foo' fac.forwardPort = 1337 proto.receiver.wrappedProto = proto_helpers.AccumulatingProtocol() # Shortcut to end of SAM stream accept protocol proto.receiver.currentRule = 'State_readData' proto._parser._setupInterp() proto.dataReceived(('%s FROM_PORT=34444 TO_PORT=0\nEgg a' % TEST_B64).encode('utf-8')) self.assertEquals(b'Egg a', proto.receiver.wrappedProto.data) proto.dataReceived(b'nd spam') self.assertEquals(b'Egg and spam', proto.receiver.wrappedProto.data) def test_peerDataWrapped_threeParts(self): fac, proto = self.makeProto() fac.streamAcceptIncoming = Mock() fac.session = Mock() fac.session.id = 'foo' fac.forwardPort = 1337 proto.receiver.wrappedProto = proto_helpers.AccumulatingProtocol() # Shortcut to end of SAM stream accept protocol proto.receiver.currentRule = 'State_readData' proto._parser._setupInterp() proto.dataReceived(('%s FROM_PORT=34444 T' % TEST_B64).encode('utf-8')) proto.dataReceived(b'O_PORT=0\nEgg a') self.assertEquals(b'Egg a', proto.receiver.wrappedProto.data) proto.dataReceived(b'nd spam') self.assertEquals(b'Egg and spam', proto.receiver.wrappedProto.data) class TestStreamAcceptFactory(SAMFactoryTestMixin, unittest.TestCase): factory = stream.StreamAcceptFactory blankFactoryArgs = (Mock(), Mock(), Mock()) def test_streamAcceptEstablished(self): mreactor = proto_helpers.MemoryReactor() session = Mock() listeningPort = Mock() fac, proto = self.makeProto(Mock(), session, listeningPort) # Shortcut to end of SAM stream accept protocol proto.receiver.currentRule = 'State_accept' proto._parser._setupInterp() proto.dataReceived(b'STREAM STATUS RESULT=OK\n') session.addStream.assert_called_with(proto.receiver) listeningPort.addAccept.assert_called_with(proto.receiver) test_streamAcceptEstablished.skip = skipSRO def test_streamAcceptIncoming(self): mreactor = proto_helpers.MemoryReactor() wrappedFactory = FakeFactory() session = Mock() listeningPort = Mock() fac, proto = self.makeProto(wrappedFactory, session, listeningPort) # Shortcut to end of SAM stream accept protocol proto.receiver.currentRule = 'State_readData' proto._parser._setupInterp() proto.dataReceived(('%s FROM_PORT=34444 TO_PORT=0\n' % TEST_B64).encode('utf-8')) listeningPort.removeAccept.assert_called_with(proto.receiver) self.assertEqual(wrappedFactory.proto, proto.receiver.wrappedProto) test_streamAcceptEstablished.skip = skipSRO class TestStreamForwardProtocol(SAMProtocolTestMixin, unittest.TestCase): protocol = stream.StreamForwardProtocol def test_streamForwardAfterHello(self): fac, proto = self.makeProto() fac.session = Mock() fac.session.id = 'foo' fac.forwardPort = 1337 proto.transport.clear() proto.dataReceived(b'HELLO REPLY RESULT=OK VERSION=3.1\n') self.assertEquals( b'STREAM FORWARD ID=foo PORT=1337 SILENT=false\n', proto.transport.value()) def test_streamForwardReturnsError(self): fac, proto = self.makeProto() fac.session = Mock() fac.session.id = 'foo' fac.forwardPort = 1337 proto.transport.clear() proto.dataReceived(b'HELLO REPLY RESULT=OK VERSION=3.1\n') proto.transport.clear() proto.dataReceived(b'STREAM STATUS RESULT=I2P_ERROR MESSAGE="foo bar baz"\n') fac.resultNotOK.assert_called_with('I2P_ERROR', 'foo bar baz') def test_streamForwardEstablishedAfterStreamForward(self): fac, proto = self.makeProto() fac.streamForwardEstablished = Mock() fac.session = Mock() fac.session.id = 'foo' fac.forwardPort = 1337 proto.transport.clear() proto.dataReceived(b'HELLO REPLY RESULT=OK VERSION=3.1\n') proto.transport.clear() proto.dataReceived(b'STREAM STATUS RESULT=OK\n') fac.streamForwardEstablished.assert_called_with(proto.receiver) class TestStreamForwardFactory(SAMFactoryTestMixin, unittest.TestCase): factory = stream.StreamForwardFactory blankFactoryArgs = (Mock(), '') def test_streamForwardEstablished(self): mreactor = proto_helpers.MemoryReactor() session = Mock() fac, proto = self.makeProto(session, '') # Shortcut to end of SAM stream forward protocol proto.receiver.currentRule = 'State_forward' proto._parser._setupInterp() proto.dataReceived(b'STREAM STATUS RESULT=OK\n') session.addStream.assert_called_with(proto.receiver) streamProto = self.successResultOf(fac.deferred) self.assertEqual(proto.receiver, streamProto) test_streamForwardEstablished.skip = skipSRO txi2p-0.3.7/txi2p/sam/test/util.py000066400000000000000000000143321432127672400167360ustar00rootroot00000000000000# Copyright (c) str4d # See COPYING for details. from builtins import object try: # Python 3 from unittest.mock import Mock except: # Python 2 (library) from mock import Mock from twisted.internet import defer from twisted.internet.error import ConnectionLost, ConnectionRefusedError from twisted.internet.protocol import ClientFactory from twisted.python import failure from twisted.test import proto_helpers from txi2p.sam import constants as c connectionLostFailure = failure.Failure(ConnectionLost()) connectionRefusedFailure = failure.Failure(ConnectionRefusedError()) class SAMProtocolTestMixin(object): def makeProto(self, *a, **kw): protoClass = kw.pop('_protoClass', self.protocol) fac = ClientFactory(*a, **kw) fac.nickname = 'foo' fac.privKey = None fac.port = None fac.localPort = None fac.options = {} fac.sigType = None fac.protocol = protoClass fac.resultNotOK = Mock() def raise_(reason): raise reason.value fac.connectionFailed = lambda reason: raise_(reason) proto = fac.buildProtocol(None) transport = proto_helpers.StringTransport() transport.abortConnection = lambda: None proto.makeConnection(transport) return fac, proto def test_initSendsHello(self): fac, proto = self.makeProto() self.assertSubstring('HELLO VERSION', str(proto.transport.value())) def test_helloReturnsError(self): fac, proto = self.makeProto() proto.transport.clear() proto.dataReceived(b'HELLO REPLY RESULT=I2P_ERROR MESSAGE="foo bar baz"\n') fac.resultNotOK.assert_called_with('I2P_ERROR', 'foo bar baz') def test_pingReceived(self): fac, proto = self.makeProto() self.addCleanup(proto.receiver.stopPinging) proto.transport.clear() # Enable keepalive proto.receiver.currentRule = 'State_keepalive' proto._parser._setupInterp() proto.dataReceived(b'PING\n') self.assertEquals( b'PONG\n', proto.transport.value()) def test_pingReceivedWithData(self): fac, proto = self.makeProto() self.addCleanup(proto.receiver.stopPinging) proto.transport.clear() # Enable keepalive proto.receiver.currentRule = 'State_keepalive' proto._parser._setupInterp() proto.dataReceived(b'PING some random data\n') self.assertEquals( b'PONG some random data\n', proto.transport.value()) def test_pingReceivedResetsTimeout(self): fac, proto = self.makeProto() self.addCleanup(proto.receiver.stopPinging) proto.transport.clear() # Enable keepalive proto.receiver.currentRule = 'State_keepalive' proto._parser._setupInterp() proto.receiver._sendPing() self.assertEquals( 'PING %s\n' % proto.receiver.lastPing, proto.transport.value().decode('utf-8')) self.assertTrue(proto.receiver.pingTimeout.active()) proto.transport.clear() proto.dataReceived(b'PING\n') self.assertEquals( b'PONG\n', proto.transport.value()) self.assertFalse(proto.receiver.pingTimeout.active()) def test_validPongResponseResetsTimeout(self): fac, proto = self.makeProto() self.addCleanup(proto.receiver.stopPinging) proto.transport.clear() # Enable keepalive proto.receiver.currentRule = 'State_keepalive' proto._parser._setupInterp() proto.receiver._sendPing() self.assertEquals( 'PING %s\n' % proto.receiver.lastPing, proto.transport.value().decode('utf-8')) self.assertTrue(proto.receiver.pingTimeout.active()) proto.transport.clear() proto.dataReceived(('PONG %s\n' % proto.receiver.lastPing).encode('utf-8')) self.assertFalse(proto.receiver.pingTimeout.active()) def test_invalidPongResponseDoesNotResetTimeout(self): fac, proto = self.makeProto() self.addCleanup(proto.receiver.stopPinging) proto.transport.clear() # Enable keepalive proto.receiver.currentRule = 'State_keepalive' proto._parser._setupInterp() proto.receiver._sendPing() self.assertEquals( 'PING %s\n' % proto.receiver.lastPing, proto.transport.value().decode('utf-8')) self.assertTrue(proto.receiver.pingTimeout.active()) proto.transport.clear() proto.dataReceived(b'PONG not what was expected\n') self.assertTrue(proto.receiver.pingTimeout.active()) class SAMFactoryTestMixin(object): def setUp(self): self.aborted = [] def makeProto(self, *a, **kw): fac = self.factory(*a, **kw) proto = fac.buildProtocol(None) transport = proto_helpers.StringTransport() transport.abortConnection = lambda: self.aborted.append(True) proto.makeConnection(transport) return fac, proto def test_cancellation(self): fac, proto = self.makeProto(*self.blankFactoryArgs) fac.deferred.cancel() self.assert_(self.aborted) return self.assertFailure(fac.deferred, defer.CancelledError) def test_cancellationBeforeFailure(self): fac, proto = self.makeProto(*self.blankFactoryArgs) fac.deferred.cancel() proto.connectionLost(connectionLostFailure) self.assert_(self.aborted) return self.assertFailure(fac.deferred, defer.CancelledError) def test_cancellationAfterFailure(self): fac, proto = self.makeProto(*self.blankFactoryArgs) proto.connectionLost(connectionLostFailure) fac.deferred.cancel() self.assertFalse(self.aborted) return self.assertFailure(fac.deferred, ConnectionLost) def test_clientConnectionFailed(self): fac, proto = self.makeProto(*self.blankFactoryArgs) fac.clientConnectionFailed(None, connectionRefusedFailure) return self.assertFailure(fac.deferred, ConnectionRefusedError) def test_resultNotOK(self): fac, proto = self.makeProto(*self.blankFactoryArgs) for result, error in list(c.samErrorMap.items()): self.assertRaises(error, fac.resultNotOK, result, '') txi2p-0.3.7/txi2p/test/000077500000000000000000000000001432127672400146245ustar00rootroot00000000000000txi2p-0.3.7/txi2p/test/__init__.py000066400000000000000000000000001432127672400167230ustar00rootroot00000000000000txi2p-0.3.7/txi2p/test/test_address.py000066400000000000000000000063561432127672400176740ustar00rootroot00000000000000# Copyright (c) str4d # See COPYING for details. import unittest from txi2p.address import I2PAddress from txi2p.test.util import TEST_B64, TEST_B32 class TestI2PAddress(unittest.TestCase): def test_noHost_noPort(self): addr = I2PAddress(TEST_B64) self.assertEqual((addr.destination, addr.host, addr.port), (TEST_B64, TEST_B32, None)) def test_host_noPort(self): addr = I2PAddress(TEST_B64, 'spam.i2p') self.assertEqual((addr.destination, addr.host, addr.port), (TEST_B64, 'spam.i2p', None)) def test_noHost_port(self): addr = I2PAddress(TEST_B64, port=81) self.assertEqual((addr.destination, addr.host, addr.port), (TEST_B64, TEST_B32, 81)) def test_host_port(self): addr = I2PAddress(TEST_B64, 'spam.i2p', 81) self.assertEqual((addr.destination, addr.host, addr.port), (TEST_B64, 'spam.i2p', 81)) def test_portString(self): addr = I2PAddress(TEST_B64, 'spam.i2p', '81') self.assertEqual((addr.destination, addr.host, addr.port), (TEST_B64, 'spam.i2p', 81)) def test_address_host_noPort(self): addr = I2PAddress(TEST_B64, 'spam.i2p', 81) addr2 = I2PAddress(addr, host='eggs.i2p') self.assertEqual((addr2.destination, addr2.host, addr2.port), (TEST_B64, 'eggs.i2p', None)) def test_address_noHost_port(self): addr = I2PAddress(TEST_B64, 'spam.i2p', 81) addr2 = I2PAddress(addr, port=82) self.assertEqual((addr2.destination, addr2.host, addr2.port), (TEST_B64, 'spam.i2p', 82)) def test_address_host_port(self): addr = I2PAddress(TEST_B64, 'spam.i2p', 81) addr2 = I2PAddress(addr, host='eggs.i2p', port=82) self.assertEqual((addr2.destination, addr2.host, addr2.port), (TEST_B64, 'eggs.i2p', 82)) def test_reprWithNoHostNoPort(self): addr = I2PAddress(TEST_B64) self.assertEqual(repr(addr), 'I2PAddress(%s)' % TEST_B32) def test_reprWithHostNoPort(self): addr = I2PAddress(TEST_B64, 'spam.i2p') self.assertEqual(repr(addr), 'I2PAddress(spam.i2p)') def test_reprWithNoHostPort(self): addr = I2PAddress(TEST_B64, port=81) self.assertEqual(repr(addr), 'I2PAddress(%s, 81)' % TEST_B32) def test_reprWithHostPort(self): addr = I2PAddress(TEST_B64, 'spam.i2p', 81) self.assertEqual(repr(addr), 'I2PAddress(spam.i2p, 81)') def test_reprWithPortString(self): addr = I2PAddress(TEST_B64, 'spam.i2p', '81') self.assertEqual(repr(addr), 'I2PAddress(spam.i2p, 81)') def test_hashWithNoHostNoPort(self): addr = I2PAddress(TEST_B64) self.assertEqual(hash(addr), hash((TEST_B32, None))) def test_hashWithHostNoPort(self): addr = I2PAddress(TEST_B64, 'spam.i2p') self.assertEqual(hash(addr), hash(('spam.i2p', None))) def test_hashWithNoHostPort(self): addr = I2PAddress(TEST_B64, port=81) self.assertEqual(hash(addr), hash((TEST_B32, 81))) def test_hashWithHostPort(self): addr = I2PAddress(TEST_B64, 'spam.i2p', 81) self.assertEqual(hash(addr), hash(('spam.i2p', 81))) def test_hashWithPortString(self): addr = I2PAddress(TEST_B64, 'spam.i2p', '81') self.assertEqual(hash(addr), hash(('spam.i2p', 81))) txi2p-0.3.7/txi2p/test/test_grammar.py000066400000000000000000000105331432127672400176650ustar00rootroot00000000000000# Copyright (c) str4d # See COPYING for details. import unittest from parsley import makeGrammar, ParseError from txi2p.grammar import bobGrammarSource, samGrammarSource bobGrammar = makeGrammar(bobGrammarSource, {}) samGrammar = makeGrammar(samGrammarSource, {}) def stringParserFromRule(grammar, rule): def parseString(s): return getattr(grammar(s), rule)() return parseString class TestBOBGrammar(unittest.TestCase): def _test(self, rule, data, expected): parse = stringParserFromRule(bobGrammar, rule) result = parse(data) self.assertEqual(result, expected) def test_BOB_clear(self): self._test('BOB_clear', 'OK cleared\n', (True, 'cleared')) self._test('BOB_clear', 'ERROR tunnel is active\n', (False, 'tunnel is active')) def test_BOB_getdest(self): self._test('BOB_getdest', 'OK spam\n', (True, 'spam')) def test_BOB_getkeys(self): self._test('BOB_getkeys', 'OK spameggs\n', (True, 'spameggs')) def test_BOB_list(self): spam = { 'nickname': 'spam', 'starting': False, 'running': True, 'stopping': False, 'keys': True, 'quiet': False, 'inport': 12345, 'inhost': 'localhost', 'outport': 23456, 'outhost': 'localhost' } eggs = { 'nickname': 'eggs', 'starting': False, 'running': False, 'stopping': False, 'keys': True, 'quiet': False, 'inport': None, 'inhost': 'localhost', 'outport': None, 'outhost': 'localhost' } self._test('BOB_list', 'OK Listing done\n', (True, 'Listing done', [])) self._test('BOB_list', 'DATA NICKNAME: spam STARTING: false RUNNING: true STOPPING: false KEYS: true QUIET: false INPORT: 12345 INHOST: localhost OUTPORT: 23456 OUTHOST: localhost\nDATA NICKNAME: eggs STARTING: false RUNNING: false STOPPING: false KEYS: true QUIET: false INPORT: not_set INHOST: localhost OUTPORT: not_set OUTHOST: localhost\nOK Listing done\n', (True, 'Listing done', [spam, eggs])) self._test('BOB_list', 'ERROR ni!\n', (False, 'ni!', [])) class TestSAMGrammar(unittest.TestCase): def _test(self, rule, data, expected): parse = stringParserFromRule(samGrammar, rule) result = parse(data) self.assertEqual(result, expected) def test_SAM_hello(self): self._test('SAM_hello', 'HELLO REPLY RESULT=OK VERSION=3.1\n', {'result': 'OK', 'version': '3.1'}) self._test('SAM_hello', 'HELLO REPLY RESULT=NOVERSION\n', {'result': 'NOVERSION'}) self._test('SAM_hello', 'HELLO REPLY RESULT=I2P_ERROR MESSAGE="Something failed"\n', {'result': 'I2P_ERROR', 'message': 'Something failed'}) def test_SAM_session_status(self): self._test('SAM_session_status', 'SESSION STATUS RESULT=OK DESTINATION=privkey\n', {'result': 'OK', 'destination': 'privkey'}) self._test('SAM_session_status', 'SESSION STATUS RESULT=DUPLICATED_ID\n', {'result': 'DUPLICATED_ID'}) def test_SAM_stream_status(self): self._test('SAM_stream_status', 'STREAM STATUS RESULT=OK\n', {'result': 'OK'}) self._test('SAM_stream_status', 'STREAM STATUS RESULT=CANT_REACH_PEER MESSAGE="Can\'t reach peer"\n', {'result': 'CANT_REACH_PEER', 'message': 'Can\'t reach peer'}) def test_SAM_naming_reply(self): self._test('SAM_naming_reply', 'NAMING REPLY RESULT=OK NAME=name VALUE=dest\n', {'result': 'OK', 'name': 'name', 'value': 'dest'}) self._test('SAM_naming_reply', 'NAMING REPLY RESULT=KEY_NOT_FOUND\n', {'result': 'KEY_NOT_FOUND'}) def test_SAM_dest_reply(self): self._test('SAM_dest_reply', 'DEST REPLY PUB=foo PRIV=foobar\n', {'pub': 'foo', 'priv': 'foobar'}) def test_SAM_ping(self): self._test('SAM_ping', 'PING\n', None) self._test('SAM_ping', 'PING 1234567890\n', '1234567890') def test_SAM_pong(self): self._test('SAM_pong', 'PONG\n', None) self._test('SAM_pong', 'PONG 1234567890\n', '1234567890') txi2p-0.3.7/txi2p/test/test_plugins.py000066400000000000000000000142761432127672400177300ustar00rootroot00000000000000# Copyright (c) str4d # See COPYING for details. from builtins import object try: # Python 3 from unittest import mock except: # Python 2 (library) import mock import sys from twisted.internet import interfaces from twisted.python.versions import Version from twisted.test.proto_helpers import MemoryReactor from twisted.trial import unittest import twisted from zope.interface.verify import verifyObject from txi2p.bob.endpoints import (BOBI2PClientEndpoint, BOBI2PServerEndpoint) from txi2p.sam.endpoints import (SAMI2PStreamClientEndpoint, SAMI2PStreamServerEndpoint) from txi2p.test.util import fakeSession if twisted.version < Version('twisted', 14, 0, 0): skip = 'txi2p.plugins requires twisted 14.0 or newer' else: skip = None class I2PPluginTestMixin(object): def test_pluginDiscovery(self): from twisted.internet.endpoints import getPlugins parsers = list(getPlugins(self._parserInterface)) for p in parsers: if isinstance(p, self._parserClass): break else: self.fail( "Did not find %s parser in %r" % (self._parserClass, parsers,)) def test_interface(self): parser = self._parserClass() self.assertTrue(verifyObject(self._parserInterface, parser)) class I2PClientEndpointPluginTest(I2PPluginTestMixin, unittest.TestCase): """ Unit tests for the I2P client endpoint description parser. """ skip = skip @property def _parserInterface(self): return interfaces.IStreamClientEndpointStringParserWithReactor @property def _parserClass(self): from txi2p.plugins import I2PClientParser return I2PClientParser def test_badAPI(self): from twisted.internet.endpoints import clientFromString self.failUnlessRaises(ValueError, clientFromString, MemoryReactor(), "i2p:stats.i2p:api=FOO") def test_apiEndpointWithNoAPI(self): from twisted.internet.endpoints import clientFromString self.failUnlessRaises(ValueError, clientFromString, MemoryReactor(), "i2p:stats.i2p:apiEndpoint=tcp\:127.0.0.1\:2827") def test_stringDescription_default(self): from twisted.internet.endpoints import clientFromString with mock.patch('txi2p.sam.endpoints.getSession', fakeSession): ep = clientFromString( MemoryReactor(), "i2p:stats.i2p") self.assertIsInstance(ep, SAMI2PStreamClientEndpoint) def test_stringDescription_BOB(self): from twisted.internet.endpoints import clientFromString ep = clientFromString( MemoryReactor(), "i2p:stats.i2p:api=BOB:tunnelNick=spam:inport=12345:options=inbound.length\:5,outbound.length\:5") self.assertIsInstance(ep, BOBI2PClientEndpoint) self.assertIsInstance(ep._reactor, MemoryReactor) self.assertEqual(ep._dest,"stats.i2p") self.assertEqual(ep._tunnelNick,"spam") self.assertEqual(ep._inport,12345) self.assertEqual(ep._options, {'inbound.length': '5', 'outbound.length': '5'}) def test_stringDescription_SAM(self): from twisted.internet.endpoints import clientFromString with mock.patch('txi2p.sam.endpoints.getSession', fakeSession): ep = clientFromString( MemoryReactor(), "i2p:stats.i2p:81:api=SAM:localPort=34444:options=inbound.length\:5,outbound.length\:5:sigType=foobar") self.assertIsInstance(ep, SAMI2PStreamClientEndpoint) self.assertEqual(ep._host, "stats.i2p") self.assertEqual(ep._port, 81) self.assertEqual(ep._localPort, 34444) s = ep._sessionDeferred self.assertEqual(s.kwargs['options'], {'inbound.length': '5', 'outbound.length': '5'}) self.assertEqual(s.kwargs['sigType'], 'foobar') class I2PServerEndpointPluginTest(I2PPluginTestMixin, unittest.TestCase): """ Unit tests for the I2P client endpoint description parser. """ skip = skip @property def _parserInterface(self): return interfaces.IStreamServerEndpointStringParser @property def _parserClass(self): from txi2p.plugins import I2PServerParser return I2PServerParser def test_badAPI(self): from twisted.internet.endpoints import serverFromString self.failUnlessRaises(ValueError, serverFromString, MemoryReactor(), "i2p:/tmp/testkeys.foo:api=FOO") def test_apiEndpointWithNoAPI(self): from twisted.internet.endpoints import serverFromString self.failUnlessRaises(ValueError, serverFromString, MemoryReactor(), "i2p:/tmp/testkeys.foo:apiEndpoint=tcp\:127.0.0.1\:2827") def test_stringDescription_default(self): from twisted.internet.endpoints import serverFromString with mock.patch('txi2p.sam.endpoints.getSession', fakeSession): ep = serverFromString( MemoryReactor(), "i2p:/tmp/testkeys.foo") self.assertIsInstance(ep, SAMI2PStreamServerEndpoint) def test_stringDescription_BOB(self): from twisted.internet.endpoints import serverFromString ep = serverFromString( MemoryReactor(), "i2p:/tmp/testkeys.foo:api=BOB:tunnelNick=spam:outport=23456:options=inbound.length\:5,outbound.length\:5") self.assertIsInstance(ep, BOBI2PServerEndpoint) self.assertIsInstance(ep._reactor, MemoryReactor) self.assertEqual(ep._keyfile, "/tmp/testkeys.foo") self.assertEqual(ep._tunnelNick, "spam") self.assertEqual(ep._outport, 23456) self.assertEqual(ep._options, {'inbound.length': '5', 'outbound.length': '5'}) def test_stringDescription_SAM(self): from twisted.internet.endpoints import serverFromString with mock.patch('txi2p.sam.endpoints.getSession', fakeSession): ep = serverFromString( MemoryReactor(), "i2p:/tmp/testkeys.foo:81:api=SAM:options=inbound.length\:5,outbound.length\:5:sigType=foobar") self.assertIsInstance(ep, SAMI2PStreamServerEndpoint) s = ep._sessionDeferred self.assertEqual(s.kwargs['options'], {'inbound.length': '5', 'outbound.length': '5'}) self.assertEqual(s.kwargs['sigType'], 'foobar') txi2p-0.3.7/txi2p/test/util.py000066400000000000000000000041631432127672400161570ustar00rootroot00000000000000# Copyright (c) str4d # See COPYING for details. from builtins import object try: # Python 3 from unittest import mock except: # Python 2 (library) import mock from twisted.internet import defer, protocol from twisted.test import proto_helpers TEST_B64 = "2wDRF5nDfeTNgM4X-TI5xEk3R-WiaTABvkMQ2eYpvEzayUZQJgr9E2T6Y2m9HHn3xHYGEOg-RLisjW9AubTaUTx-v66AsEEtv745qPcuWuV1SP~w1bdzYEn8MSoK7Zh4mwHBg1uHq8z17TUNvWz19q76vHNth-2PDuBToD7ySBn3cGBFDUU83wJJXPD6OueLY8yosWWtksk7WZk60~6z~nVePPSEY8JDry3myLDe11szAVER4A8eX1sFpw247cXGGJK9wQhV-TXFj~m76GPVcFKh7u79zwTwZnZ1GXXKqqyRoj1c4-U69CvvJsQRLmdLFwFEpRkxwV8z6LIFclYJk443YpTnPXC7vNdFOzqqS4FLR1ra~DNfN5foMtR2~2VxuR5m2dYiOS6GzHDxA4acJJSGqnasJjcEIFNVSQKxMnFu9PvGLNJHZ83EraHCErENcOGkPlnVgcJCtPGNGiirwCbBz38jE0lfjkrNrWabc6uWeU559CobG8F8KUDx1irpAAAA" TEST_B32 = "tv5iv4i5roywnv2rg6rjqufniqbogn4rokjkooa7n4jht3lex3ga.b32.i2p" def fakeSession(nickname, **kwargs): m = mock.Mock() m.nickname = nickname m.kwargs = kwargs return m class FakeFactory(protocol.ClientFactory): protocol = proto_helpers.AccumulatingProtocol def __init__(self, returnNoProtocol=False): self.returnNoProtocol = returnNoProtocol self.protocolConnectionMade = defer.Deferred() def buildProtocol(self, addr): if self.returnNoProtocol: return None self.proto = protocol.ClientFactory.buildProtocol(self, addr) return self.proto class FakeEndpoint(object): def __init__(self, failure=None): self.failure = failure self.deferred = None def connect(self, fac): self.factory = fac if self.deferred: return self.deferred if self.failure: return defer.fail(self.failure) self.proto = fac.buildProtocol(None) transport = proto_helpers.StringTransport() self.aborted = [] transport.abortConnection = lambda: self.aborted.append(True) self.tlsStarts = [] transport.startTLS = lambda ctx: self.tlsStarts.append(ctx) self.proto.makeConnection(transport) self.transport = transport return defer.succeed(self.proto) txi2p-0.3.7/txi2p/utils.py000066400000000000000000000055621432127672400153670ustar00rootroot00000000000000from twisted.internet.endpoints import clientFromString from txi2p import sam DEFAULT_ENDPOINT = { 'BOB': 'tcp:127.0.0.1:2827', 'SAM': 'tcp:127.0.0.1:7656', } DEFAULT_API = 'SAM' def getApi(api, apiEndpoint, apiDict): if not api: if apiEndpoint: raise ValueError('api must be specified if apiEndpoint is given') else: api = DEFAULT_API if api not in apiDict: raise ValueError('Specified I2P API is invalid or unsupported') if not apiEndpoint: apiEndpoint = DEFAULT_ENDPOINT[api] return (api, apiEndpoint) _apiTesters = { 'SAM': sam.testAPI, } def testAPI(reactor, api=None, apiEndpoint=None): """Test whether an API is reachable. The function returns a :class:`twisted.internet.defer.Deferred`; register callbacks to receive the return value or errors. Args: reactor: The API endpoint will be constructed with this reactor. api (str): The API to test. apiEndpoint (str): An endpoint string that may connect to the API. Alternatively, the caller can directly provide an :class:`twisted.internet.interfaces.IStreamClientEndpoint`, and the ``reactor`` will be ignored. Returns: True if the API is reachable. Raises: ValueError: if the API doesn't support this method. """ api, apiEndpoint = getApi(api, apiEndpoint, _apiTesters) if isinstance(apiEndpoint, str): apiEndpoint = clientFromString(reactor, apiEndpoint) return _apiTesters[api](apiEndpoint) _apiGenerators = { 'SAM': sam.generateDestination, } def generateDestination(reactor, keyfile, api=None, apiEndpoint=None): """Generate a new I2P Destination. The function returns a :class:`twisted.internet.defer.Deferred`; register callbacks to receive the return value or errors. Args: reactor: The API endpoint will be constructed with this reactor. keyfile (str): Path to a local file where the keypair for the new Destination should be stored. api (str): The API to use. apiEndpoint (str): An endpoint string that will connect to the API. Alternatively, the caller can directly provide an :class:`twisted.internet.interfaces.IStreamClientEndpoint`, and the ``reactor`` will be ignored. Returns: txi2p.I2PAddress: The new Destination. Once this is received via the Deferred callback, the ``keyfile`` will have been written. Raises: ValueError: if the API doesn't support this method. ValueError: if the ``keyfile`` already exists. IOError: if the ``keyfile`` write fails. """ api, apiEndpoint = getApi(api, apiEndpoint, _apiGenerators) if isinstance(apiEndpoint, str): apiEndpoint = clientFromString(reactor, apiEndpoint) return _apiGenerators[api](keyfile, apiEndpoint)