├── .coveragerc ├── .gitignore ├── .travis.yml ├── .travis ├── run.sh └── twistedchecker-trunk-diff.sh ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── NAME.txt ├── NEWS.txt ├── README.md ├── bin ├── gvertex └── vertex ├── doc ├── notes ├── q2q-standalone.tac └── vertex.rst ├── prime └── plugins │ └── vertex_client.py ├── setup.py └── vertex ├── __init__.py ├── _version.py ├── address.py ├── amputil.py ├── bits.py ├── command.py ├── conncache.py ├── depserv.py ├── endpoint.py ├── exceptions.py ├── gtk2hack.glade ├── gtk2hack.py ├── icon-active.png ├── icon-inactive.png ├── ivertex.py ├── ptcp.py ├── q2q.py ├── q2qadmin.py ├── q2qclient.py ├── q2qstandalone.py ├── scripts └── __init__.py ├── sigma.py ├── subproducer.py ├── tcpdfa.py └── test ├── __init__.py ├── _fakes.py ├── amphelpers.py ├── helpers.py ├── mock_data.py ├── test_bits.py ├── test_client.py ├── test_conncache.py ├── test_dependencyservice.py ├── test_identity.py ├── test_ptcp.py ├── test_q2q.py ├── test_q2qclient.py ├── test_sigma.py ├── test_standalone.py └── test_subproducer.py /.coveragerc: -------------------------------------------------------------------------------- 1 | # .coveragerc to control coverage 2 | # see http://nedbatchelder.com/code/coverage/config.html 3 | 4 | [run] 5 | branch = True 6 | source = vertex 7 | omit = 8 | vertex/scripts/* 9 | vertex/test/* 10 | vertex/_version.py 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | 3 | Vertex.egg-info 4 | dist 5 | build 6 | 7 | _trial_temp* 8 | .coverage 9 | 10 | .q2q-data 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | cache: 4 | directories: 5 | - $HOME/.cache/pip 6 | branches: 7 | only: 8 | - master 9 | python: 10 | - '2.7' 11 | 12 | install: 13 | - pip install pyflakes 14 | - pip install codecov 15 | - pip install pretend 16 | - pip install git+https://github.com/twisted/twistedchecker@9cec9f5b40d1b5cb4d6dcda26ec0ba6e05de4b62#egg=twistedchecker 17 | - pip install -e . 18 | 19 | matrix: 20 | include: 21 | - python: 2.7 22 | env: TOXENV=py27 23 | - python: 2.7 24 | env: TOXENV=lint 25 | 26 | # no pep8, since vertex uses twisted pep8 27 | script: 28 | ./.travis/run.sh 29 | 30 | after_success: 31 | - codecov 32 | 33 | notifications: 34 | email: false 35 | irc: 36 | channels: "chat.freenode.net#divmod" 37 | template: 38 | - "%{repository}@%{branch} - %{author}: %{message} (%{build_url})" 39 | use_notice: true 40 | -------------------------------------------------------------------------------- /.travis/run.sh: -------------------------------------------------------------------------------- 1 | 2 | case "${TOXENV}" in 3 | lint) 4 | pyflakes vertex bin prime 5 | pip install diff_cover 6 | bash .travis/twistedchecker-trunk-diff.sh vertex 7 | ;; 8 | py27) 9 | coverage run `which trial` vertex 10 | ;; 11 | esac; 12 | -------------------------------------------------------------------------------- /.travis/twistedchecker-trunk-diff.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # 3 | # Helper for running twistedchecker and reporting only errors that are part 4 | # of the changes since master. 5 | # 6 | # Call it as: 7 | # * SCRIPT_NAME twisted 8 | # * SCRIPT_NAME twisted/words/ 9 | # * SCRIPT_NAME twisted.words 10 | 11 | target=$1 12 | 13 | # FIXME: https://github.com/twisted/twistedchecker/issues/116 14 | # Since for unknown modules twistedchecker will return the same error, the 15 | # diff will fail to detect that we are trying to check an invalid path or 16 | # module. 17 | # This is why we check that the argument is a path and if not a path, it is 18 | # an importable module. 19 | if [ ! -d "$target" ]; then 20 | python -c "import $target" 2> /dev/null 21 | if [ $? -ne 0 ]; then 22 | >&2 echo "$target does not exists as a path or as a module." 23 | exit 1 24 | fi 25 | fi 26 | 27 | # Make sure we have master on the local repo. 28 | git fetch origin +refs/heads/master:refs/remotes/origin/master 29 | 30 | # Explicitly ignore extension modules. See: https://github.com/twisted/twistedchecker/issues/118 31 | mkdir -p build/ 32 | twistedchecker --ignore='raiser.so,portmap.so,_sendmsg.so' -f parseable "$target" > build/twistedchecker-branch.report 33 | 34 | # Make sure repo is producing the diff with prefix so that the output of 35 | # `git diff` can be parsed by diff_cover. 36 | git config diff.noprefix false 37 | 38 | diff-quality \ 39 | --violations=pylint \ 40 | --fail-under=100 \ 41 | --compare-branch=origin/master build/twistedchecker-branch.report 42 | 43 | diff_exit_code=$? 44 | exit $diff_exit_code 45 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Coding Standard 2 | =============== 3 | 4 | Vertex follows the [Twisted Coding Standard](https://twistedmatrix.com/documents/current/core/development/policy/coding-standard.html). 5 | Although it has not currently incorporated as a checker, the [Twisted Coding Standard Checker](https://launchpad.net/twistedchecker) would be a useful tool. 6 | 7 | 8 | Testing Standard 9 | ================ 10 | 11 | Please make sure that all new code is tested according to the [Twisted Test Standard](https://twistedmatrix.com/documents/current/core/development/policy/test-standard.html) and has complete coverage. 12 | 13 | [travis-ci](https://travis-ci.org/twisted/vertex) is used to make sure that tests keep passing 14 | 15 | Please document test cases and methods with useful, relevant docstrings stated in a present tense and in the active voice. 16 | [This post](https://plus.google.com/115348217455779620753/posts/YA3ThKWhSAj) has a concise summary of what makes a good test docstring. 17 | 18 | 19 | Documentation Standard 20 | ====================== 21 | 22 | Please document all public interface methods. 23 | 24 | The [Twisted documentation standard](http://twistedmatrix.com/trac/browser/trunk/doc/core/development/policy/writing-standard.xhtml?format=raw) is a good guide on style. 25 | 26 | 27 | Review Process 28 | ============== 29 | 30 | Vertex mostly follows the [Twisted review process](http://twistedmatrix.com/trac/wiki/ReviewProcess) for contributions, with some minor changes for working with Github. 31 | 32 | Issues 33 | ------ 34 | Issues should generally be opened first, and pull requests linked to issues for the following reasons: 35 | 36 | - Writing the issue first will help clarify what needs to be done for both a contributor and a reviewer 37 | - If a pull request is abandoned, it may be closed without also closing the underlying issue. 38 | - If the work is taken over by someone else, both the old and new pull requests may be linked to the same issue. 39 | - Overall discussion of how to go about resolving the problem may happen in the issue, and code review can happen in a pull request. 40 | 41 | Issues should have a meaningful title and description of what needs to change and why. 42 | Although implementation details are not necessarily needed, well-defined completion conditions should be included in the description (in list form would be helpful). 43 | 44 | 45 | Pull Requests 46 | ------------- 47 | Pull requests should be small and self-contained, which makes it easier for the reviewer and may increase review turnaround time. 48 | 49 | If one pull request is insufficient to solve the issue (and not because the pull request is then abandoned and taken over by someone else), the issue it is trying to resolve to should be broken up into multiple smaller issues, and the pull request linked to one of them. 50 | 51 | All pull requests must be reviewed prior to merging. 52 | 53 | There is currently no particular standard as to whether pull requests should be made on branches vs forks. 54 | As such, those who have push access to the repo can use either branches or forks, and everyone else who does not have push access perfoce must use forks. 55 | 56 | 57 | Reviews 58 | ------- 59 | To pass review, pull requests should: 60 | 61 | 1. follow the stated coding standard 62 | 1. have 100% unit test coverage of modified and new code (even if it didn't have tests before) 63 | 1. have 100% API docstring coverage for all modified and new code (even if it didn't have docs before) 64 | 1. have prose documentation giving a high-level sense of how an API is meant to be used and what capabilities the library offers. 65 | 66 | In addition: 67 | 68 | 1. All tests must pass - this is enforced with [Travis-CI](https://travis-ci.org/twisted/vertex) 69 | 1. Code coverage must not decrease - this is enforced by (and more details available at) [Coveralls](https://coveralls.io/r/twisted/vertex). 70 | Although coverage does not say anything about the quality of the tests or the correct behavior of the tests (both of which should be evaluated during review), it provides a minimal baseline. 71 | 72 | Once a pull request is approved, the big green button should be used to merge. 73 | 74 | In the merge commit, Github determines the top line of the message, and the title of the pull request is the second line, but can be edited. 75 | Please make sure this second line is meaningful, either by making the pull request title meaningful or by editing the commit message. 76 | 77 | Following the second line, the reviewers and a "fixes" should be included, as per the Twisted merge commit messages. 78 | Finally, a longer description should be added that details the change. 79 | 80 | 81 | 82 | 83 | Reviewers 84 | Fixes # 85 | 86 | Long description (as long as you wish) 87 | 88 | ["Fixes #issue" will close one or more issues](https://help.github.com/articles/closing-issues-via-commit-messages). Note the lack of colon after the word "Fixes" - if a colon appears after "Fixes", the issue will not be automatically closed. 89 | 90 | Once the pull request is merged, if using branches, please delete the branch. 91 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2005 Divmod Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include NAME.txt 3 | include doc/q2q-standalone.tac 4 | -------------------------------------------------------------------------------- /NAME.txt: -------------------------------------------------------------------------------- 1 | 2 | See: http://mathworld.wolfram.com/Vertex.html 3 | 4 | A vertex in mathematics is a location where two or more lines or edges meet. 5 | 6 | Divmod Vertex is an implementation of and interface to the Q2Q protocol. It is 7 | named for a vertext because it provides a way for peers on the internet to 8 | establish connections with each other, e.g. to make their connection lines 9 | meet, regardless of intermediary interference such as network address 10 | translators and lack of naming information. 11 | -------------------------------------------------------------------------------- /NEWS.txt: -------------------------------------------------------------------------------- 1 | 0.3.1 (2015-03-05): 2 | - Switch to setuptools; this should resolve issues with Vertex being 3 | installed through pip, particularly as a dependency of something else. 4 | - Numerous other small fixes and cleanups. 5 | 6 | 0.3.0 (2009-11-25): 7 | - Remove use of deprecated Twisted APIs from the test suite and improve 8 | some error handling as a necessary consequence. 9 | - Use twisted.internet.ssl instead of epsilon.sslverify. 10 | - Remove an implementation of deferLater. 11 | 12 | 0.2.0 (2006-06-12): 13 | - Moved JUICE implementation into Epsilon. 14 | - Removed dependency on Nevow's formless. 15 | - Clarify licensing terms. 16 | - Fix bugs on 64-bit platforms. 17 | - removed buggy legacy non-TLS options which would break negotiation with 18 | OpenSSL 0.9.8a. 19 | - Deprecated twisted test APIs removed. 20 | - First phase of integration with twisted.cred; vertex endpoints can now be 21 | authenticated against a Twisted UsernamePassword cred authenticator. 22 | 23 | 0.1.0 (2005-10-10): 24 | 25 | - Initial release. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/twisted/vertex.png?branch=master)](https://travis-ci.org/twisted/vertex) 2 | [![codecov.io](https://codecov.io/github/twisted/vertex/coverage.svg?branch=master)](https://codecov.io/github/twisted/vertex?branch=master) 3 | # Divmod Vertex # 4 | 5 | Pull up a chair, and let me tell you a story of the once and future Internet. 6 | 7 | ## The Age of Innocence ## 8 | 9 | In the beginning of the Internet, all networking was “peer to peer”. 10 | If you were lucky enough to have an Internet-connected computer, you could talk to any other Internet-connected computer; in fact, being able to do so was practically the definition of “Internet connected”. 11 | 12 | However, many of these computers were the size of a large truck and required a full time staff to operate. 13 | In the fullness of time, the Internet would grow to include much smaller devices, computers owned by individuals. 14 | 15 | ## The Sundering ## 16 | 17 | The growing number of connected devices created a number of problems. 18 | All these new devices needed IP addresses; at first, this was simply a logistical problem that you'd want to avoid, since you might want to connect multiple computers to your home network, and later, there weren't enough IP addresses to go around any more. 19 | So some clever folks came up with Network Address Translation. 20 | As far as the Internet was concerned, your whole network was a single device, with one address; internally, of course, each computer would have its own network address, but a device on the border of the network would hide all that. 21 | This meant that you could connect as many devices to your network as you liked, and you didn't have to ask permission, and nobody outside could even necessarily tell that you'd done so. 22 | NAT was therefore a great innovation - it allowed billions more devices to get connected to the internet. 23 | 24 | But NAT also effectively means that as a “consumer” of Internet services, you can make outgoing requests but can not accept incoming ones. 25 | In other words, you can subscribe but you can't publish; you can listen but you can't speak. 26 | Slowly, this began to break things: 27 | Internet telephones that expected to be able to call each other. 28 | Games that expected to be able to make a direct connection between two players. 29 | Even things as simple as file transfer programs that allowed you to send information directly to another person. 30 | All of this can be worked around, of course, but there are few standards for how NAT devices behave, inconsistently implemented. 31 | Most require users to become experts at networking, or endure poor performance, security problems. 32 | Even basic tasks like [updating a video game](https://us.battle.net/support/en/article/firewall-proxy-router-and-port-configuration) require memorization of long lists of numbers and familiarity with network administration. 33 | 34 | Nevertheless, even as all of this is happening, as all of this functionality is disappearing, the Internet's popularity is exploding. 35 | What it *does* enable is so amazing that we all forget the even *more* amazing promise we've lost. 36 | 37 | The Internet is Broken: Long Live the Internet. 38 | 39 | ## The Desert Of The Now ## 40 | 41 | Today, almost all basic Internet-connected functionailty takes the form of a server - almost always a web site - controlled by a third party, rather than a program you can control on your own computer. 42 | There's nothing wrong with web sites, of course; the web has been a fantastic innovation in its own right. 43 | But there *is* something wrong with *needing* to put your information under someone else's control just because that's the only way to get it from Point A to Point B, where Point A is your house and Point B is your friend's house. 44 | 45 | Especially when there are other ways. 46 | 47 | Teleconferencing software, video games, and file sharing networks have all *had* to solve this problem in order for their basic functionality to work. 48 | So it's possible to do. 49 | But they've all solved these problems in vastly different, application-specific ways, and none of them share any common infrastructure. for direct communication. 50 | If you want to create a new application that makes use of direct connectivity, you have to become an expert in [about ten times as much technology](https://tools.ietf.org/html/rfc5389) as if you wanted to create a [basic web site](https://www.djangoproject.com/). 51 | 52 | ## A False Hope ## 53 | 54 | IPv6 is coming, of course, and in principle it could free us from all this. 55 | 56 | But in practice, it won't. 57 | 58 | After two decades of depending on NAT for security, home computers are not prepared for the onslaught of the public Internet. 59 | When IPv6 rolls out to the general public, it will need to be done in such a way that prevents incoming traffic by default. 60 | Without a secure way to allow incoming traffic, networked devices will stay shut off in the way. 61 | 62 | # Okay, What Is Vertex, Already?! # 63 | 64 | Vertex is a general purpose system for securely connecting to a program running on behalf of another person, with a trust model based on Trust On First Use (TOFU) and Persistence of Pseudonym (POP). 65 | 66 | Currently, when a program wants to connect somewhere over the Internet, it gives the name of the machine, and a port number. 67 | Something like: 68 | 69 | example.com 443 70 | ^ computer ^ port 71 | 72 | With Vertex, instead, a program identifies a *person* and a *purpose*. 73 | Like this: 74 | 75 | bob@b.example.com/messaging 76 | ^ person ^ purpose 77 | ^ server 78 | 79 | Let's say Alice has a chat program that she wants to use to talk to Bob. 80 | Alice puts in an identifier like the one above into her that program, and using Vertex, it can talk directly to the same program on Bob's computer; all communication is therefore secured. 81 | 82 | ## What's the point? ## 83 | 84 | If you want to have a program on your computer (or, potentially, your mobile device) communicate some information directly to another, you should be able to do it: 85 | 86 | 1. easily, 87 | 2. securely, 88 | 3. quickly, and 89 | 4. directly. 90 | 91 | Vertex attempts to enable all of this, taking care of the details of networking so that applications can just communicate. 92 | 93 | ## How's this supposed to work? ## 94 | 95 | Alice runs a local Vertex agent, which she registers with a Vertex server on a.example.com as alice@a.example.com; she gets a certificate signed by a.example.com, and then maintains a connection to that server. 96 | Bob registers with a Vertex server on b.example.com as bob@b.example.com; he gets a certificate from b.example.com and maintains a connection to that server. 97 | 98 | Alice then connects to b.example.com; since she's never talked to it before, she requests its certificate. 99 | (Alice can also ask a.example.com, or any of her existing connections to other Vertex clients or servers, to double-check on b.example.com's certificate, to make sure that they get the same result, potentially automating the usual call-somebody-up-to-ask-if-the-SSH-server's-key-really-changed workflow we all go through.) 100 | 101 | Alice secures her connection to b.example.com with the certificate that a.example.com previously signed; b.example.com verifies it by talking to a.example.com. 102 | On that connection, she asks to speak to Bob. 103 | 104 | At this point, b.example.com talks to Bob and sends along Alice's certificate. 105 | If Bob approves of Alice's connection, then (and only then!) b.example.com sends along instructions for how to connect to Bob. 106 | 107 | These instructions are a *list* of potential connection techniques; TCPv4, TCPv6, multiple different UDP hole punching techniques, local (behind NAT) addresses, addresses discovered by talking to Vertex servers, and so on. 108 | All of these are attempted, and the best connection is used. 109 | Regardless of which connection is selected, the local Vertex agents on Alice and Bob's computers should use the same TLS certificates to communicate with each other, and the traffic should be encrypted. 110 | 111 | ## Wow, this sounds great, what kind of shape is it in? ## 112 | 113 | Sadly, Vertex's current status is that of "proof of concept". 114 | Many of the things in the story above say "should" instead of "does" because it doesn't actually do those things yet. 115 | It can make some connections over the Internet and transfer some bytes, but: 116 | 117 | - It doesn't yet implement a workable trust model, or any way to revoke certificates. 118 | - There's no mechanism to ask your peers to tell you about a certificate to guard aganst DNS cache poisoning on first use. 119 | - Despite all the fancy certificate memory stuff, fundamentally trust is established by plain passwords. 120 | - It stores user passwords in plaintext. 121 | - There's no UI for the local agent, and no real persistence of the "buddy list". 122 | - There's no support for UPnP, or any other kind of automatic router configuration. 123 | - Its UDP-over-TCP implementation doesn't implement [window scaling](https://en.wikipedia.org/wiki/TCP_window_scale_option), among other things; it is *very* slow. 124 | - When using UDP tunnelling, it doesn't currently use encryption at all. This is actually due to a design flaw, long since fixed, in Twisted's implementation of TLS; Vertex is one of the reasons that [this flaw was fixed](https://twistedmatrix.com/trac/ticket/593). 125 | - There's no defined protocol for an agent to talk to other applications; each agent currently contains all of the code for the applications that want to speak to other nodes. 126 | 127 | But all these flaws and all this unfinished work are just a chance for you to be a hero and improve Vertex's functionality until it's actually useful! 128 | 129 | ### What's "Divmod"? ### 130 | 131 | Divmod is a now-defunct start-up company that open sourced many projects in the Twisted ecosystem, including this one. 132 | All the Divmod projects were therefore named “Divmod X”. 133 | As an acknowledgement of Divmod’s contributions, the current maintainers (some of whom worked for Divmod at the time) are preserving that nomenclature. 134 | 135 | ### Why "Vertex"? ### 136 | 137 | The Divmod projects are all named for various mathematical concepts. 138 | 139 | The vertex of an angle is where two rays begin or meet, and Vertex is meant to be the meeting point for your network communications. 140 | -------------------------------------------------------------------------------- /bin/gvertex: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Copyright 2005 Divmod, Inc. See LICENSE file for details 3 | 4 | from vertex.gtk2hack import main 5 | 6 | if __name__ == '__main__': 7 | main() 8 | -------------------------------------------------------------------------------- /bin/vertex: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Copyright 2005 Divmod, Inc. See LICENSE file for details 3 | 4 | from vertex.q2qclient import Q2QClientProgram 5 | 6 | if __name__ == '__main__': 7 | Q2QClientProgram().parseOptions() 8 | -------------------------------------------------------------------------------- /doc/notes: -------------------------------------------------------------------------------- 1 | 2 | Gin 3 | === 4 | 5 | TCP-alike over UDP 6 | 7 | Packet Format 8 | ============= 9 | 10 | Connection ID - 32 bits 11 | Sequence Number - 32 bits 12 | Ack number - 32 bits 13 | Window - 32 bits 14 | Flags (SYN, ACK, FIN, IGN) - 8 bits 15 | Checksum - 32 bits 16 | Data length in bytes - 16 bits 17 | Data - See previous 18 | 19 | SEMANTICS 20 | ========= 21 | Different types of packets: 22 | 23 | just SYN: 24 | SYN+ACK: 25 | just ACK: normal tcp packet meanings 26 | 27 | SYN+NAT: Request for information about the sender's public address 28 | 29 | ACK+NAT: Response with information about the recipient's public address. The 30 | data is the IP address and port number to which the packet is being sent, 31 | formatted as a dotted-quad formatted IP address followed by a colon followed by 32 | the base-10 string representation of the port number.. 33 | 34 | STB: Size Too Big - a packet with a dlen greater than the length of its data 35 | was received. 36 | 37 | Other Stuff: 38 | 39 | Connection IDs uniquely identify a stream for a protocol. All bytes 40 | associated with a particular connectionID will be delivered to the 41 | same protocol instance. 42 | 43 | Sequence Numbers indicate the senders notion of how far into its 44 | outgoing stream this packet is. Sequence numbers start from a 45 | pseudo-random value within the allowed range and are incremented by 46 | the number of bytes in each packet transmitted (re-transmits 47 | discounted). This indicates to the peer where the bytes in each 48 | packet lie in the stream, allowing ordered delivery. 49 | 50 | Ack numbers indicate the senders notion of how far into its incoming 51 | stream all data has been received. This value is always what the 52 | sender expects the receiver to use as its next sequence number. 53 | 54 | Window indicates the number of bytes in advance of the senders ack 55 | number the receiver may proceed in sending. This receiver's sequence 56 | numbers must never be greater than the sender's last ack number plus 57 | their window number. 58 | 59 | Checksum is a CRC 32 of the data (and only the data - wait maybe this 60 | should be the header less the checksum, too, in case things get 61 | corrupted there) 62 | -------------------------------------------------------------------------------- /doc/q2q-standalone.tac: -------------------------------------------------------------------------------- 1 | # Copyright 2005 Divmod, Inc. See LICENSE file for details 2 | from vertex.q2qstandalone import defaultConfig 3 | 4 | application = defaultConfig() 5 | 6 | -------------------------------------------------------------------------------- /doc/vertex.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Vertex 3 | ======== 4 | 5 | Files 6 | ----- 7 | 8 | Interesting files 9 | ================= 10 | 11 | conncache.py 12 | Connection cache for message-based protocols. 13 | endpoint.py 14 | Q2Q endpoint should be written here using IStreamClientEndpoint 15 | ivertex.py 16 | Interfaces. 17 | q2q.py 18 | The main event. Contains AMP verbs for Q2Q protocol, 19 | connection-attempt code, Q2Q protocol, Q2Q service, and certificate 20 | store. 21 | q2qclient.py 22 | Command-line clients; file sender/receiver, port forwarder. 23 | q2qstandalone.py 24 | Presence server frontend components. 25 | subproducer.py 26 | Multiplexer of multiple producers. 27 | 28 | Less interesting files 29 | ====================== 30 | 31 | bits.py 32 | bit array for Sigma 33 | depserv.py 34 | MultiService derivative. Possibly junk. 35 | gtk2hack.py 36 | What it says. 37 | ptcp.py 38 | PTCP, a TCP-alike over UDP. Here be dragons. 39 | q2qadmin.py 40 | mantissa shim. 41 | sigma.py 42 | cheap knockoff of bittorrent. 43 | statemachine.py 44 | used in tcpdfa 45 | tcpdfa.py 46 | used in ptcp 47 | 48 | Q2Q 49 | --- 50 | 51 | Protocol 52 | ======== 53 | 54 | AMP messages handled: 55 | 56 | - Identity 57 | 58 | * Identify 59 | 60 | Vertex nodes send this to presence servers authoritative for an 61 | address. The server responds with a self-signed certificate for the 62 | address. This message will be sent in the clear. 63 | 64 | + q2q address 65 | + *response*: certificate for address 66 | 67 | * Secure 68 | 69 | Q2Q clients send this to presence servers authoritative for the 70 | destination address. Upon successful mutual validation of SSL 71 | certificates, a TLS session using these certificates is 72 | established. This message will be sent in the clear. 73 | 74 | + local certificate 75 | + certificate authorities 76 | + source q2q address (optional) 77 | + destination q2q address 78 | + whether the server should verify the client's certificate. 79 | 80 | * Sign 81 | 82 | Q2Q clients send this to presence servers authoritative for their 83 | own address. The server checks the password and the address given 84 | in the certificate request, and if valid creates a new certificate 85 | by signing the certificate request. 86 | 87 | + certificate request 88 | + password 89 | + *response*: certificate 90 | 91 | - Presence 92 | 93 | * Listen 94 | 95 | Q2Q clients send this to presence servers authoritative for their 96 | own address. The server registers the client's interest in 97 | connections for the named services. 98 | 99 | + listening q2q address 100 | + list of service names 101 | + description 102 | + *response*: empty 103 | 104 | * Inbound 105 | 106 | Q2Q clients send this to presence servers authoritative for the 107 | destination address. Presence servers send this to all Q2Q clients 108 | with the destination address who have registered interest in 109 | connections for the named service. 110 | 111 | + service name 112 | + source q2q address 113 | + destination q2q address 114 | + optional udp source port 115 | + *response*: list of (q2q identity, cert, connection methods, expiration, description) 116 | 117 | * Choke 118 | 119 | Used by VirtualTransport to signal backpressure. 120 | 121 | + connection id 122 | 123 | * Unchoke 124 | 125 | Used by VirtualTransport to signal relief of backpressure. 126 | 127 | + connection id 128 | 129 | - Virtual 130 | 131 | Q2Q clients send this to presence servers after receiving a response 132 | to Inbound. Presence servers send this to destination Q2Q clients 133 | after receiving a Virtual message. Starts a VirtualTransport upon 134 | receipt. 135 | 136 | + connection id 137 | + *response*: empty 138 | 139 | - WRITE 140 | 141 | Low-level AMP command sent over a virtual channel. For passing to a 142 | Q2Q client, through a presence server. 143 | 144 | + data 145 | + *response*: empty 146 | 147 | - BindUDP 148 | 149 | Q2Q clients send this to presence servers after receiving a response 150 | to Inbound. Presence servers send this to destination Q2Q clients 151 | after receiving a BindUDP message. Used as part of PTCP connection 152 | process; the receiving Q2Q client sends a UDP packet to the 153 | requested (host, port) address. 154 | 155 | + protocol name 156 | + source q2q address 157 | + destination q2q address 158 | + udp source (host, port) 159 | + udp destination (host, port) 160 | + *response*: empty 161 | 162 | - WhoAmI 163 | 164 | Q2Q clients send this to presence servers. The response is the 165 | (host, port) the server received the message from. Used as part of 166 | address discovery. 167 | 168 | + *response*: (IP, port) pair 169 | 170 | - SourceIP 171 | 172 | All Vertex nodes send this message to their peer upon connection. The 173 | remote node responds with the IP it received the message from. 174 | 175 | + *response*: probable public IP 176 | 177 | - RetrieveConnection 178 | 179 | + connection identifier 180 | 181 | 182 | Other notes 183 | =========== 184 | 185 | Presence server 186 | ~~~~~~~~~~~~~~~ 187 | 188 | starts a Q2QService with file-based cert store and a pFF for an admin 189 | that unconditionally adds users when asked to. 190 | 191 | Cert storage 192 | ~~~~~~~~~~~~ 193 | 194 | provides IRealm for IQ2QUser, avatars can sign cert requests. 195 | manages private certs for users 196 | 197 | Q2Q.requestCertificateForAddress invokes the cert management stuff. 198 | 199 | Q2QService 200 | ~~~~~~~~~~ 201 | 202 | protocolFactoryFactory maps address/protocol-name to a handler for connections. 203 | 204 | public methods: 205 | 206 | listenQ2Q 207 | ephemeral publication of interest in connections 208 | requestCertificateForAddress 209 | initial "login" for a client to presence server 210 | startService 211 | might start ptcp dispatcher, might listen for q2q 212 | connections, might listen for inbound connections 213 | sendMessage 214 | find a cached q2q connection, send an amp message 215 | connectQ2Q 216 | creates a connection from a pair of addresses and a protocol 217 | -------------------------------------------------------------------------------- /prime/plugins/vertex_client.py: -------------------------------------------------------------------------------- 1 | 2 | from vertex.gtk2hack import PlugEntry 3 | 4 | pe = PlugEntry() 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | import re 3 | 4 | versionPattern = re.compile(r"""^__version__ = ['"](.*?)['"]$""", re.M) 5 | with open("vertex/_version.py", "rt") as f: 6 | version = versionPattern.search(f.read()).group(1) 7 | 8 | setup( 9 | name="Vertex", 10 | version=version, 11 | maintainer="Twisted Matrix Laboratories", 12 | maintainer_email="vertex-dev@twistedmatrix.com", 13 | url="https://github.com/twisted/vertex", 14 | scripts=["bin/gvertex", "bin/vertex"], 15 | install_requires=[ 16 | 'attrs', 17 | 'Twisted[tls]>=16.6.0', 18 | 'automat', 19 | 'pretend', 20 | 'txscrypt', 21 | ], 22 | license="MIT", 23 | platforms=["any"], 24 | description= 25 | """ 26 | Divmod Vertex is the first implementation of the Q2Q protocol, which 27 | is a peer-to-peer communication protocol for establishing 28 | stream-based communication between named endpoints. 29 | """, 30 | classifiers=[ 31 | "Development Status :: 2 - Pre-Alpha", 32 | "Framework :: Twisted", 33 | "Intended Audience :: Developers", 34 | "License :: OSI Approved :: MIT License", 35 | "Programming Language :: Python", 36 | "Topic :: Communications", 37 | "Topic :: Internet", 38 | "Topic :: Internet :: File Transfer Protocol (FTP)", 39 | "Topic :: Internet :: Name Service (DNS)", 40 | "Topic :: Software Development :: Libraries :: Python Modules", 41 | ], 42 | packages=find_packages() + ['prime.plugins'], 43 | include_package_data=True, 44 | ) 45 | -------------------------------------------------------------------------------- /vertex/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- test-case-name: vertex.test -*- 2 | 3 | from vertex._version import __version__ 4 | from twisted.python import versions 5 | 6 | 7 | def asTwistedVersion(packageName, versionString): 8 | return versions.Version(packageName, *map(int, versionString.split("."))) 9 | 10 | version = asTwistedVersion("vertex", __version__) 11 | __all__ = ['version'] 12 | -------------------------------------------------------------------------------- /vertex/_version.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.3.1" 2 | -------------------------------------------------------------------------------- /vertex/address.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Twisted Matrix Laboratories. 2 | # See LICENSE for details. 3 | from functools import total_ordering 4 | 5 | @total_ordering 6 | class Q2QAddress(object): 7 | def __init__(self, domain, resource=None): 8 | self.resource = resource 9 | self.domain = domain 10 | 11 | 12 | def domainAddress(self): 13 | """ 14 | Return an Address object which is the same as this one with ONLY 15 | the 'domain' attribute set, not 'resource'. 16 | 17 | May return 'self' if 'resource' is already None. 18 | 19 | @return: 20 | """ 21 | if self.resource is None: 22 | return self 23 | else: 24 | return Q2QAddress(self.domain) 25 | 26 | 27 | def claimedAsIssuerOf(self, cert): 28 | """ 29 | Check if the information in a provided certificate *CLAIMS* to be 30 | issued by this address. 31 | 32 | PLEASE NOTE THAT THIS METHOD IS IN NO WAY AUTHORITATIVE. It does not 33 | perform any cryptographic checks. 34 | 35 | Currently this check is if L{Q2QAddress.__str__}C{(self)} is equivalent 36 | to the commonName on the certificate's issuer. 37 | 38 | @param cert: 39 | 40 | @return: 41 | """ 42 | return cert.getIssuer().commonName == str(self) 43 | 44 | 45 | def claimedAsSubjectOf(self, cert): 46 | """ 47 | Check if the information in a provided certificate *CLAIMS* to be 48 | provided for use by this address. 49 | 50 | PLEASE NOTE THAT THIS METHOD IS IN NO WAY AUTHORITATIVE. It does not 51 | perform any cryptographic checks. 52 | 53 | Currently this check is if L{Q2QAddress.__str__}C{(self)} is equivalent 54 | to the commonName on the certificate's subject. 55 | 56 | @param cert: 57 | 58 | @return: 59 | """ 60 | return cert.getSubject().commonName == str(self) 61 | 62 | 63 | def _tupleme(self): 64 | """ 65 | L{Q2QAddress}es sort by domain, then by resource. 66 | """ 67 | return (self.domain, self.resource) 68 | 69 | 70 | def __lt__(self, other): 71 | """ 72 | Is this less than something? 73 | 74 | @param other: the thing that this is maybe less than 75 | @type other: maybe L{Q2QAddress}? who knows 76 | 77 | @return: L{True} or L{False} 78 | """ 79 | if not isinstance(other, Q2QAddress): 80 | return NotImplemented 81 | return (self._tupleme() < other._tupleme()) 82 | 83 | 84 | def __eq__(self, other): 85 | """ 86 | Is this equal to something? 87 | 88 | @param other: the thing that this is maybe equal to 89 | @type other: maybe L{Q2QAddress}? who knows 90 | 91 | @return: L{True} or L{False} 92 | """ 93 | if not isinstance(other, Q2QAddress): 94 | return NotImplemented 95 | return (self._tupleme() == other._tupleme()) 96 | 97 | 98 | def __iter__(self): 99 | return iter((self.resource, self.domain)) 100 | 101 | 102 | def __str__(self): 103 | """ 104 | Return a string of the normalized form of this address. e.g.:: 105 | 106 | glyph@divmod.com # for a user 107 | divmod.com # for a domain 108 | """ 109 | if self.resource: 110 | resource = self.resource + '@' 111 | else: 112 | resource = '' 113 | return (resource + self.domain).encode('utf-8') 114 | 115 | 116 | def __repr__(self): 117 | return '' % self.__str__() 118 | 119 | 120 | def __hash__(self): 121 | return hash(str(self)) 122 | 123 | 124 | def fromString(cls, string): 125 | args = string.split("@", 1) 126 | args.reverse() 127 | return cls(*args) 128 | fromString = classmethod(fromString) 129 | 130 | 131 | 132 | class VirtualTransportAddress: 133 | def __init__(self, underlying): 134 | self.underlying = underlying 135 | 136 | 137 | def __repr__(self): 138 | return 'VirtualTransportAddress(%r)' % (self.underlying,) 139 | 140 | 141 | 142 | class Q2QTransportAddress: 143 | """ 144 | The return value of getPeer() and getHost() for Q2Q-enabled transports. 145 | Passed to buildProtocol of factories passed to listenQ2Q. 146 | 147 | @ivar underlying: The return value of the underlying transport's getPeer() 148 | or getHost(); an address which indicates the path which the bytes carrying 149 | Q2Q traffic are travelling over. It is tempting to think of this as a 150 | 'physical' layer but that it not necessarily accurate; there are 151 | potentially multiple layers of wrapping on any Q2Q transport, as an SSL 152 | transport may be tunnelled over a UDP NAT-traversal layer. Implements 153 | C{IAddress} from Twisted, for all the good that will do you. 154 | 155 | @ivar logical: a L{Q2QAddress}, The logical peer; the user ostensibly 156 | listening to data on the other end of this transport. 157 | 158 | @ivar protocol: a L{str}, the name of the protocol that is connected. 159 | """ 160 | 161 | def __init__(self, underlying, logical, protocol): 162 | self.underlying = underlying 163 | self.logical = logical 164 | self.protocol = protocol 165 | 166 | 167 | def __repr__(self): 168 | return 'Q2QTransportAddress(%r, %r, %r)' % ( 169 | self.underlying, 170 | self.logical, 171 | self.protocol) 172 | -------------------------------------------------------------------------------- /vertex/amputil.py: -------------------------------------------------------------------------------- 1 | # -*- test-case-name: vertex.test.test_bits -*- 2 | # Copyright (c) Twisted Matrix Laboratories. 3 | # See LICENSE for details. 4 | 5 | """ 6 | AMP arguments for (de-)serializing various types that Vertex needs to 7 | communicate over the wire. 8 | """ 9 | 10 | import datetime 11 | 12 | from twisted.internet.ssl import CertificateRequest, Certificate 13 | 14 | from twisted.protocols.amp import Argument, String 15 | 16 | from vertex.address import Q2QAddress 17 | 18 | class AmpTime(Argument): 19 | """ 20 | AMP argument for serializing a L{datetime.datetime} object. 21 | """ 22 | 23 | def toString(self, inObject): 24 | """ 25 | Convert the given L{datetime.datetime} into some bytes to serialize to 26 | AMP. 27 | 28 | @param inObject: 29 | 30 | @return: 31 | """ 32 | return inObject.strftime("%Y-%m-%dT%H:%M:%S") 33 | 34 | 35 | def fromString(self, inString): 36 | """ 37 | Convert the given string (produced by L{toString}) to a 38 | L{datetime.datetime}. 39 | 40 | @param inString: 41 | 42 | @return: 43 | """ 44 | return datetime.datetime.strptime(inString, "%Y-%m-%dT%H:%M:%S") 45 | 46 | 47 | 48 | class Q2QAddressArgument(Argument): 49 | """ 50 | AMP argument for serializing a L{Q2QAddress} object. 51 | """ 52 | fromString = Q2QAddress.fromString 53 | toString = Q2QAddress.__str__ 54 | 55 | 56 | 57 | class HostPort(Argument): 58 | """ 59 | AMP argument for serializing a host name and port number as a 60 | colon-separated pair. 61 | """ 62 | 63 | def toString(self, inObj): 64 | """ 65 | Convert the given C{(host, port)} tuple into some bytes for 66 | serialization on the wire. 67 | 68 | @param inObj: a C{(host, port)} tuple 69 | @type inObj: 2-L{tuple} of L{bytes}, L{int} 70 | 71 | @return: bytes in the format C{host:port} 72 | @rtype: L{bytes} 73 | """ 74 | host, port = inObj 75 | return "%s:%d" % (host, port) 76 | 77 | 78 | def fromString(self, inStr): 79 | """ 80 | Convert the given bytes into a C{(host, port)} tuple. 81 | 82 | @param inStr: bytes in the format C{host:port} 83 | @type inStr: L{bytes} 84 | 85 | @return: a C{(host, port)} tuple 86 | @rtype: 2-L{tuple} of L{bytes}, L{int} 87 | """ 88 | host, sPort = inStr.split(":") 89 | return (host, int(sPort)) 90 | 91 | 92 | 93 | def _argumentForLoader(loaderClass): 94 | """ 95 | Create an AMP argument for (de-)serializing instances of C{loaderClass}. 96 | 97 | @param loaderClass: A type object with a L{load} class method that takes 98 | some bytes and returns an instance of itself, and a L{dump} instance 99 | method that returns some bytes. 100 | 101 | @return: a class decorator which decorates an AMP argument class by 102 | replacing it with the one defined for loading and saving C{loaderClass} 103 | instances. 104 | """ 105 | def decorator(argClass): 106 | class LoadableArgument(String): 107 | def toString(self, arg): 108 | assert isinstance(arg, loaderClass), \ 109 | ("%r not %r" % (arg, loaderClass)) 110 | return String.toString(self, arg.dump()) 111 | 112 | def fromString(self, arg): 113 | return loaderClass.load(String.fromString(self, arg)) 114 | 115 | LoadableArgument.__name__ = argClass.__name__ 116 | return LoadableArgument 117 | return decorator 118 | 119 | 120 | 121 | @_argumentForLoader(CertificateRequest) 122 | class CertReq(Argument): 123 | """ 124 | AMP Argument that serializes and deserializes L{CertificateRequest}s. 125 | """ 126 | 127 | 128 | 129 | @_argumentForLoader(Certificate) 130 | class Cert(Argument): 131 | """ 132 | AMP Argument that serializes and deserializes L{Certificate}s. 133 | """ 134 | -------------------------------------------------------------------------------- /vertex/bits.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Twisted Matrix Laboratories. 2 | # See LICENSE for details. 3 | # -*- test-case-name: vertex.test.test_bits -*- 4 | """ 5 | The purpose of this module is to provide the class BitArray, a compact 6 | overlay onto an array of bytes which is instead bit-addressable. It also 7 | includes several bitwise operators. 8 | 9 | It does not include all array operations yet, most notably those related to 10 | slicing, since it is written primarily for use by the swarming implementation 11 | and swarming only requires fixed-size bit masks. 12 | 13 | """ 14 | 15 | __metaclass__ = type 16 | 17 | import array 18 | import operator 19 | import math 20 | 21 | BITS_PER_BYTE = 8 22 | 23 | def operate(operation): 24 | # XXX TODO: optimize this and countbits later 25 | def __x__(self, other): 26 | if len(self) < len(other): 27 | return operation(other, self) 28 | new = BitArray(size=len(self)) 29 | for offt, (mybit, hisbit) in enumerate(zip(self, other)): 30 | new[offt] = operation(mybit, hisbit) 31 | 32 | for j in range(offt+1, len(self)): 33 | new[j] = operation(self[j], 0) 34 | return new 35 | return __x__ 36 | 37 | 38 | 39 | class BitArray: 40 | """ 41 | A large mutable array of bits. 42 | """ 43 | 44 | def __init__(self, bytes=None, size=None, default=0): 45 | if bytes is None and size is None: 46 | size = 0 47 | if bytes is None: 48 | bytes = array.array("B") 49 | bytesize = int(math.ceil(float(size) / BITS_PER_BYTE)) 50 | if default: 51 | padbyte = 255 52 | else: 53 | padbyte = 0 54 | bytes.fromlist([padbyte] * bytesize) 55 | self.bytes = bytes 56 | if size is None: 57 | size = len(self.bytes) * self.bytes.itemsize * BITS_PER_BYTE 58 | self.size = size 59 | 60 | # Initialize 'on' and 'off' lists to optimize various things 61 | self.on = [] 62 | self.off = [] 63 | blists = self.blists = self.off, self.on 64 | 65 | for index, bit in enumerate(self): 66 | blists[bit].append(index) 67 | 68 | 69 | def append(self, bit): 70 | offt = self.size 71 | self.size += 1 72 | if (len(self.bytes) * self.bytes.itemsize * BITS_PER_BYTE) < self.size: 73 | self.bytes.append(0) 74 | self[offt] = bit 75 | 76 | 77 | def any(self, req=1): 78 | return bool(self.blists[req]) 79 | 80 | 81 | def percent(self): 82 | """ 83 | debugging method; returns a string indicating percentage completion 84 | 85 | @return: 86 | """ 87 | if not len(self): 88 | return 'Inf%' 89 | return '%0.2f%%' % ((float(self.countbits()) / len(self)) * 100,) 90 | 91 | 92 | def __getitem__(self, bitcount): 93 | if bitcount < 0: 94 | bitcount += self.size 95 | if bitcount >= self.size: 96 | raise IndexError("%r >= %r" % (bitcount, self.size)) 97 | div, mod = divmod(bitcount, self.bytes.itemsize * BITS_PER_BYTE) 98 | byte = self.bytes[div] 99 | return (byte >> mod) & 1 100 | 101 | 102 | def __setitem__(self, bitcount, bit): 103 | if bitcount < 0: 104 | bitcount += self.size 105 | if bitcount >= self.size: 106 | raise IndexError("bitcount too big") 107 | div, mod = divmod(bitcount, self.bytes.itemsize * BITS_PER_BYTE) 108 | if bit: 109 | self.bytes[div] |= 1 << mod 110 | else: 111 | self.bytes[div] &= ~(1 << mod) 112 | 113 | # Change updating 114 | notbitlist = self.blists[not bit] 115 | try: 116 | notbitlist.remove(bitcount) 117 | except ValueError: 118 | pass 119 | bitlist = self.blists[bit] 120 | if bitcount not in bitlist: 121 | bitlist.append(bitcount) 122 | 123 | 124 | def __len__(self): 125 | return self.size 126 | 127 | 128 | def __repr__(self): 129 | l = [] 130 | l.append('[') 131 | for b in self: 132 | if b: 133 | c = 'X' 134 | else: 135 | c = ' ' 136 | l.append(c) 137 | l.append(']') 138 | return ''.join(l) 139 | 140 | 141 | def countbits(self, on=True): 142 | return len(self.blists[on]) 143 | 144 | 145 | def positions(self, bit): 146 | """ 147 | An iterator of all positions that a bit holds in this BitArray. 148 | 149 | @param bit: 1 or 0 150 | 151 | @return: 152 | """ 153 | return self.blists[bit][:] 154 | 155 | __xor__ = operate(operator.xor) 156 | __and__ = operate(operator.and_) 157 | __or__ = operate(operator.or_) 158 | -------------------------------------------------------------------------------- /vertex/command.py: -------------------------------------------------------------------------------- 1 | # -*- test-case-name: vertex.test.test_q2q -*- 2 | # Copyright (c) Twisted Matrix Laboratories. 3 | # See LICENSE for details. 4 | 5 | """ 6 | AMP command definitions for the Q2Q protocol spoken by Vertex. 7 | """ 8 | 9 | # Twisted 10 | from twisted.protocols.amp import ( 11 | AmpBox, String, Unicode, ListOf, Command, 12 | Integer, _objectsToStrings 13 | ) 14 | 15 | # Vertex 16 | from vertex.amputil import ( 17 | Cert, CertReq, HostPort, Q2QAddressArgument 18 | ) 19 | from vertex.exceptions import ConnectionError, BadCertificateRequest 20 | 21 | class ConnectionStartBox(AmpBox): 22 | """ 23 | An L{AmpBox} that, when sent, calls C{startProtocol} on the transport it 24 | was sent on. 25 | """ 26 | 27 | def __init__(self, transport): 28 | """ 29 | Create a L{ConnectionStartBox}. 30 | """ 31 | super(ConnectionStartBox, self).__init__() 32 | self.virtualTransport = transport 33 | 34 | 35 | def _sendTo(self, proto): 36 | """ 37 | When sent, call the C{startProtocol} method on the virtual transport 38 | object. 39 | 40 | @see: L{vertex.ptcp.PTCP.startProtocol} 41 | 42 | @see: L{vertex.q2q.VirtualTransport.startProtocol} 43 | 44 | @param proto: the AMP protocol that this is being sent on. 45 | """ 46 | # XXX This is overriding a private interface 47 | super(ConnectionStartBox, self)._sendTo(proto) 48 | self.virtualTransport.startProtocol() 49 | 50 | 51 | 52 | class Listen(Command): 53 | """ 54 | A simple command for registering interest with an active Q2Q connection 55 | to hear from a server when others come calling. An occurrence of this 56 | command might have this appearance on the wire:: 57 | 58 | C: -Command: Listen 59 | C: -Ask: 1 60 | C: From: glyph@divmod.com 61 | C: Protocols: q2q-example, q2q-example2 62 | C: Description: some simple protocols 63 | C: 64 | S: -Answer: 1 65 | S: 66 | 67 | This puts some state on the server side that will affect any Connect 68 | commands with q2q-example or q2q-example2 in the Protocol: header. 69 | """ 70 | 71 | commandName = 'listen' 72 | arguments = [ 73 | ('From', Q2QAddressArgument()), 74 | ('protocols', ListOf(String())), 75 | ('description', Unicode())] 76 | 77 | result = [] 78 | 79 | 80 | 81 | class Virtual(Command): 82 | """ 83 | Initiate a virtual multiplexed connection over this TCP connection. 84 | """ 85 | commandName = 'virtual' 86 | result = [] 87 | 88 | arguments = [('id', Integer())] 89 | 90 | def makeResponse(cls, objects, proto): 91 | """ 92 | Create a response dictionary using this L{Virtual} command's schema; do 93 | the same thing as L{Command.makeResponse}, but additionally do 94 | addition. 95 | 96 | @param objects: The dictionary of strings mapped to Python objects. 97 | 98 | @param proto: The AMP protocol that this command is serialized to. 99 | 100 | @return: A L{ConnectionStartBox} containing the serialized form of 101 | C{objects}. 102 | """ 103 | tpt = objects.pop('__transport__') 104 | # XXX Using a private API 105 | return _objectsToStrings( 106 | objects, cls.response, 107 | ConnectionStartBox(tpt), 108 | proto) 109 | 110 | makeResponse = classmethod(makeResponse) 111 | 112 | 113 | 114 | class Identify(Command): 115 | """ 116 | Respond to an IDENTIFY command with a self-signed certificate for the 117 | domain requested, assuming we are an authority for said domain. An 118 | occurrence of this command might have this appearance on the wire:: 119 | 120 | C: -Command: Identify 121 | C: -Ask: 1 122 | C: Domain: divmod.com 123 | C: 124 | S: -Answer: 1 125 | S: Certificate: <<>> 126 | S: 127 | 128 | """ 129 | 130 | commandName = 'identify' 131 | 132 | arguments = [('subject', Q2QAddressArgument())] 133 | 134 | response = [('certificate', Cert())] 135 | 136 | 137 | 138 | class BindUDP(Command): 139 | """ 140 | See L{PTCPMethod} 141 | """ 142 | 143 | commandName = 'bind-udp' 144 | 145 | arguments = [ 146 | ('protocol', String()), 147 | ('q2qsrc', Q2QAddressArgument()), 148 | ('q2qdst', Q2QAddressArgument()), 149 | ('udpsrc', HostPort()), 150 | ('udpdst', HostPort()), 151 | ] 152 | 153 | errors = {ConnectionError: 'ConnectionError'} 154 | 155 | response = [] 156 | 157 | 158 | 159 | class SourceIP(Command): 160 | """ 161 | Ask a server on the public internet what my public IP probably is. An 162 | occurrence of this command might have this appearance on the wire:: 163 | 164 | C: -Command: Source-IP 165 | C: -Ask: 1 166 | C: 167 | S: -Answer: 1 168 | S: IP: 4.3.2.1 169 | S: 170 | 171 | """ 172 | 173 | commandName = 'source-ip' 174 | 175 | arguments = [] 176 | 177 | response = [('ip', String())] 178 | 179 | 180 | 181 | class Sign(Command): 182 | """ 183 | Request a certificate signature. 184 | """ 185 | commandName = 'sign' 186 | arguments = [('certificate_request', CertReq()), 187 | ('password', String())] 188 | 189 | response = [('certificate', Cert())] 190 | 191 | errors = {KeyError: "NoSuchUser", 192 | BadCertificateRequest: "BadCertificateRequest"} 193 | 194 | 195 | 196 | class Write(Command): 197 | """ 198 | Write the given bytes to a multiplexed virtual connection. 199 | """ 200 | commandName = 'write' 201 | arguments = [('id', Integer()), 202 | ('body', String())] 203 | requiresAnswer = False 204 | 205 | 206 | 207 | class Close(Command): 208 | """ 209 | Close the given multiplexed virtual connetion. 210 | """ 211 | commandName = 'close' 212 | arguments = [('id', Integer())] 213 | requiresAnswer = True 214 | 215 | 216 | 217 | class Choke(Command): 218 | """ 219 | Flow control: ask the peer to stop sending data over this virtual channel. 220 | """ 221 | commandName = 'Choke' 222 | arguments = [('id', Integer())] 223 | requiresAnswer = False 224 | 225 | 226 | 227 | class Unchoke(Command): 228 | """ 229 | Reverse of L{Choke}; flow may resume over this virtual channel. 230 | """ 231 | commandName = 'Unchoke' 232 | arguments = [('id', Integer())] 233 | requiresAnswer = False 234 | 235 | 236 | 237 | class WhoAmI(Command): 238 | """ 239 | Send a response identifying TCP host and port of the sender. This is used 240 | for NATed machines to identify themselves from the perspective of the 241 | public Internet. 242 | """ 243 | commandName = 'Who-Am-I' 244 | 245 | response = [ 246 | ('address', HostPort()), 247 | ] 248 | -------------------------------------------------------------------------------- /vertex/conncache.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Twisted Matrix Laboratories. 2 | # See LICENSE for details. 3 | # -*- test-case-name: vertex.test.test_conncache,vertex.test.test_q2q.TCPConnection.testSendingFiles -*- 4 | 5 | """ 6 | Connect between two endpoints using a message-based protocol to exchange 7 | messages lazily in response to UI events, caching the protocol as necessary. 8 | Using connection-oriented protocols, you will most likely not want to use this 9 | class - you might end up retrieving a cached connection in the middle of a 10 | chunk of data being sent. For the purposes of this distinction, a 11 | 'message-oriented' protocol is one which has an API which either:: 12 | 13 | a) writes only whole messages to its transport so there is never an 14 | opportunity to insert data into the middle of a message, or 15 | 16 | b) provides an API on the Protocol instance for queuing whole messages such 17 | that if partial messages are sent, calling the API multiple times will 18 | queue them internally so that clients do not need to care whether the 19 | connection is made or not. 20 | 21 | It is worth noting that all Juice-derived protocols meet constraint (b). 22 | """ 23 | 24 | from zope.interface import implements 25 | 26 | from twisted.internet.defer import maybeDeferred, DeferredList, Deferred 27 | from twisted.internet.main import CONNECTION_LOST 28 | from twisted.internet import interfaces 29 | from twisted.internet.protocol import ClientFactory 30 | 31 | 32 | 33 | class ConnectionCache: 34 | def __init__(self): 35 | """ 36 | """ 37 | # Map (fromAddress, toAddress, protoName): protocol instance 38 | self.cachedConnections = {} 39 | # Map (fromAddress, toAddress, protoName): list of Deferreds 40 | self.inProgress = {} 41 | self._shuttingDown = None 42 | 43 | 44 | def connectCached(self, endpoint, protocolFactory, 45 | extraWork=lambda x: x, 46 | extraHash=None): 47 | """ 48 | See module docstring 49 | 50 | @param endpoint: 51 | @param protocolFactory: 52 | @param extraWork: 53 | @param extraHash: 54 | 55 | @return: the D 56 | """ 57 | key = endpoint, extraHash 58 | D = Deferred() 59 | if key in self.cachedConnections: 60 | D.callback(self.cachedConnections[key]) 61 | elif key in self.inProgress: 62 | self.inProgress[key].append(D) 63 | else: 64 | self.inProgress[key] = [D] 65 | endpoint.connect( 66 | _CachingClientFactory( 67 | self, key, protocolFactory, 68 | extraWork)) 69 | return D 70 | 71 | 72 | def cacheUnrequested(self, endpoint, extraHash, protocol): 73 | self.connectionMadeForKey((endpoint, extraHash), protocol) 74 | 75 | 76 | def connectionMadeForKey(self, key, protocol): 77 | deferreds = self.inProgress.pop(key, []) 78 | self.cachedConnections[key] = protocol 79 | for d in deferreds: 80 | d.callback(protocol) 81 | 82 | 83 | def connectionLostForKey(self, key): 84 | """ 85 | Remove lost connection from cache. 86 | 87 | @param key: key of connection that was lost 88 | @type key: L{tuple} of L{IAddress} and C{extraHash} 89 | """ 90 | if key in self.cachedConnections: 91 | del self.cachedConnections[key] 92 | if self._shuttingDown and self._shuttingDown.get(key): 93 | d, self._shuttingDown[key] = self._shuttingDown[key], None 94 | d.callback(None) 95 | 96 | 97 | def connectionFailedForKey(self, key, reason): 98 | deferreds = self.inProgress.pop(key) 99 | for d in deferreds: 100 | d.errback(reason) 101 | 102 | 103 | def shutdown(self): 104 | """ 105 | Disconnect all cached connections. 106 | 107 | @returns: a deferred that fires once all connection are disconnected. 108 | @rtype: L{Deferred} 109 | """ 110 | self._shuttingDown = {key: Deferred() 111 | for key in self.cachedConnections.keys()} 112 | return DeferredList( 113 | [maybeDeferred(p.transport.loseConnection) 114 | for p in self.cachedConnections.values()] 115 | + self._shuttingDown.values()) 116 | 117 | 118 | 119 | class _CachingClientFactory(ClientFactory): 120 | debug = False 121 | 122 | def __init__(self, cache, key, subFactory, extraWork): 123 | """ 124 | @param cache: a Q2QService 125 | 126 | @param key: a 2-tuple of (endpoint, extra) that represents what 127 | connections coming from this factory are for. 128 | 129 | @param subFactory: a ClientFactory which I forward methods to. 130 | 131 | @param extraWork: extraWork(proto) -> Deferred which fires when the 132 | connection has been prepared sufficiently to be used by subsequent 133 | connections and can be counted as a success. 134 | """ 135 | 136 | self.cache = cache 137 | self.key = key 138 | self.subFactory = subFactory 139 | self.finishedExtraWork = False 140 | self.extraWork = extraWork 141 | 142 | lostAsFailReason = CONNECTION_LOST 143 | 144 | 145 | def clientConnectionMade(self, protocol): 146 | def success(reason): 147 | self.cache.connectionMadeForKey(self.key, protocol) 148 | self.finishedExtraWork = True 149 | return protocol 150 | 151 | def failed(reason): 152 | self.lostAsFailReason = reason 153 | protocol.transport.loseConnection() 154 | return reason 155 | maybeDeferred(self.extraWork, protocol).addCallbacks( 156 | success, failed) 157 | 158 | 159 | def clientConnectionLost(self, connector, reason): 160 | if self.finishedExtraWork: 161 | self.cache.connectionLostForKey(self.key) 162 | else: 163 | self.cache.connectionFailedForKey(self.key, 164 | self.lostAsFailReason) 165 | self.subFactory.clientConnectionLost(connector, reason) 166 | 167 | 168 | def clientConnectionFailed(self, connector, reason): 169 | self.cache.connectionFailedForKey(self.key, reason) 170 | self.subFactory.clientConnectionFailed(connector, reason) 171 | 172 | 173 | def buildProtocol(self, addr): 174 | return _CachingTransportShim(self, self.subFactory.buildProtocol(addr)) 175 | 176 | 177 | 178 | class _CachingTransportShim: 179 | disconnecting = property(lambda self: self.transport.disconnecting) 180 | 181 | implements(interfaces.IProtocol) 182 | 183 | def __init__(self, factory, protocol): 184 | self.factory = factory 185 | self.protocol = protocol 186 | 187 | # IProtocol 188 | self.dataReceived = protocol.dataReceived 189 | self.connectionLost = protocol.connectionLost 190 | 191 | 192 | def makeConnection(self, transport): 193 | self.transport = transport 194 | self.protocol.makeConnection(transport) 195 | self.factory.clientConnectionMade(self.protocol) 196 | 197 | 198 | def __repr__(self): 199 | return 'Q2Q-Cached<%r, %r>' % (self.transport, 200 | self.protocol) 201 | -------------------------------------------------------------------------------- /vertex/depserv.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Twisted Matrix Laboratories. 2 | # See LICENSE for details. 3 | """ 4 | This module is no longer supported for use outside Vertex. 5 | """ 6 | 7 | from twisted.python import log 8 | from sets import Set 9 | from twisted.persisted import sob 10 | from twisted.application import service, internet 11 | 12 | from zope.interface import implements 13 | 14 | class Conf(dict): 15 | """ 16 | A class to help in construction the configuration for delpoy(). 17 | 18 | Typical usage:: 19 | 20 | from vertex.depserv import Conf 21 | conf = Conf() 22 | s = conf.section 23 | s('pop', 24 | port = 110, 25 | sslPort = 995) 26 | ... 27 | """ 28 | def section(self, name, **kw): 29 | self.setdefault(name, {}).update(kw) 30 | 31 | 32 | 33 | class NotPersistable: 34 | implements(sob.IPersistable) 35 | def __init__(self, original): 36 | self.original = original 37 | 38 | 39 | def setStyle(self, style): 40 | self.style = style 41 | 42 | 43 | def save(self, tag=None, filename=None, passphrase=None): 44 | pass 45 | 46 | 47 | 48 | class StartupError(Exception): 49 | pass 50 | 51 | 52 | 53 | class DependencyService(service.MultiService): 54 | """ 55 | A MultiService that can start multiple services with interdependencies. 56 | 57 | Each keyword parameter is a dict which serves as the options for that 58 | service. 59 | 60 | Each service defines a method setup_SERVICE, which is called with the 61 | matching parameters (the service name must be all caps). If there is no key 62 | for SERVICE in the class parameters, the setup method is not called. The 63 | return value is ignored, and DependencyService makes no assumptions about 64 | any side effects. 65 | 66 | Each service may also optionally define depends_SERVICE which is called 67 | before the setup method with the same parameters as the setup method. This 68 | method returns a list of names of services on which SERVICE depends. 69 | DependencyService will then initialize the service is the correct order. If 70 | circular dependencies result, or a service depends on another service which 71 | does not exist or is not configured to run, StartupError is raised. 72 | 73 | The class can define required services by setting 'requiredServices' to a 74 | list of service names. These services will be initialized first in the 75 | order they appear in the list, ignoring all dependency information. If 76 | there are no parameters for a required service (consequently, the setup 77 | method would not normally be called), StartupError is raised. 78 | """ 79 | 80 | requiredServices = [] 81 | 82 | 83 | def __init__(self, **kw): 84 | service.MultiService.__init__(self) 85 | 86 | # This makes it possible for one service to change the configuration of 87 | # another. Avoid if possible, there if you need it. Be sure to properly 88 | # set the dependencies. 89 | self.config = kw 90 | self.servers = [] 91 | 92 | services = kw.keys() 93 | initedServices = Set() 94 | uninitedServices = Set(services) 95 | 96 | # Build dependencies 97 | dependencies = {} 98 | for serv in services: 99 | try: 100 | dependMethod = self._getDependsMethod(serv) 101 | except AttributeError: 102 | continue 103 | dependencies[serv] = dependMethod(**kw[serv]) 104 | 105 | def initializeService(svc): 106 | self._getServiceMethod(svc)(**kw[svc]) 107 | initedServices.add(svc) 108 | uninitedServices.remove(svc) 109 | 110 | for svc in self.requiredServices: 111 | if dependencies.get(svc): 112 | raise StartupError( 113 | '%r is a required service but has unsatisfied ' 114 | 'dependency on %r' % (svc, dependencies[svc])) 115 | initializeService(svc) 116 | 117 | while uninitedServices: 118 | # Iterate over the uninitialized services, adding those with no 119 | # outstanding dependencies to initThisRound. 120 | initThisRound = [] 121 | for serv in uninitedServices: 122 | for dep in dependencies.get(serv, []): 123 | if dep not in initedServices: 124 | if dep not in uninitedServices: 125 | raise StartupError( 126 | 'service %r depends on service %r,' 127 | 'which is not configured or' 128 | 'does not exist.' % (serv, dep)) 129 | break 130 | else: 131 | initThisRound.append(serv) 132 | if not initThisRound: 133 | raise StartupError( 134 | 'Can not initialize all services. Circular dependencies ' 135 | 'between setup methods?') 136 | for svc in initThisRound: 137 | initializeService(svc) 138 | 139 | 140 | def _getServiceMethod(self, service): 141 | return getattr(self, 'setup_%s' % (service.upper(),)) 142 | 143 | 144 | def _getDependsMethod(self, service): 145 | return getattr(self, 'depends_%s' % (service.upper(),)) 146 | 147 | 148 | def deploy(Class, name=None, uid=None, gid=None, **kw): 149 | """ 150 | Create an application with the give name, uid, and gid. 151 | 152 | The application has one child service, an instance of Class 153 | configured based on the additional keyword arguments passed. 154 | 155 | The application is not persistable. 156 | 157 | @param Class: 158 | @param name: 159 | @param uid: 160 | @param gid: 161 | @param kw: 162 | 163 | @return: 164 | """ 165 | svc = Class(**kw) 166 | 167 | if name is None: 168 | name = Class.__name__ 169 | # Make it easier (possible) to find this service by name later on 170 | svc.setName(name) 171 | 172 | app = service.Application(name, uid=uid, gid=gid) 173 | app.addComponent(NotPersistable(app), ignoreClass=True) 174 | svc.setServiceParent(app) 175 | 176 | return app 177 | deploy = classmethod(deploy) 178 | 179 | 180 | def attach(self, subservice): 181 | subservice.setServiceParent(self) 182 | return subservice 183 | 184 | 185 | def detach(self, subservice): 186 | subservice.disownServiceParent() 187 | 188 | 189 | def addServer(self, normalPort, sslPort, f, name): 190 | """ 191 | Add a TCP and an SSL server. Name them `name` and `name`+'s'. 192 | 193 | @param normalPort: 194 | @param sslPort: 195 | @param f: 196 | @param name: 197 | """ 198 | tcp = internet.TCPServer(normalPort, f) 199 | tcp.setName(name) 200 | self.servers.append(tcp) 201 | if sslPort is not None: 202 | ssl = internet.SSLServer(sslPort, f, contextFactory=self.sslfac) 203 | ssl.setName(name+'s') 204 | self.servers.append(ssl) 205 | 206 | 207 | def discernPrivilegedServers(self): 208 | return [srv for srv in self.servers if srv.args[0] <= 1024] 209 | 210 | 211 | def discernUnprivilegedServers(self): 212 | return [srv for srv in self.servers if srv.args[0] > 1024] 213 | 214 | 215 | def privilegedStartService(self): 216 | for server in self.discernPrivilegedServers(): 217 | log.msg("privileged attach %r" % server) 218 | self.attach(server) 219 | return service.MultiService.privilegedStartService(self) 220 | 221 | 222 | def startService(self): 223 | for server in self.discernUnprivilegedServers(): 224 | log.msg("attaching %r" % server) 225 | self.attach(server) 226 | 227 | return service.MultiService.startService(self) 228 | -------------------------------------------------------------------------------- /vertex/endpoint.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Twisted Matrix Laboratories. 2 | # See LICENSE for details. 3 | 4 | def stablesort(self, other): 5 | return cmp(self.__class__, getattr(other, '__class__', type(other))) 6 | 7 | 8 | 9 | class TCPEndpoint: 10 | def __init__(self, host, port): 11 | self.host = host 12 | self.port = port 13 | 14 | 15 | def __hash__(self): 16 | return hash((self.host, self.port)) + 5 17 | 18 | 19 | def connect(self, protocolFactory): 20 | from twisted.internet import reactor 21 | return reactor.connectTCP(self.host, self.port, protocolFactory) 22 | 23 | 24 | def __repr__(self): 25 | return '' % (self.host, self.port) 26 | 27 | 28 | def __cmp__(self, other): 29 | if isinstance(other, TCPEndpoint): 30 | return cmp((self.host, self.port), 31 | (other.host, other.port)) 32 | return stablesort(self, other) 33 | 34 | 35 | 36 | class Q2QEndpoint: 37 | def __init__(self, service, fromAddress, toAddress, protocolName): 38 | self.service = service 39 | self.fromAddress = fromAddress 40 | self.toAddress = toAddress 41 | self.protocolName = protocolName 42 | 43 | 44 | def __repr__(self): 45 | return ' to <%s> on %r>' % ( 46 | self.fromAddress, self.toAddress, self.protocolName) 47 | 48 | 49 | def __cmp__(self, other): 50 | if isinstance(other, Q2QEndpoint): 51 | return cmp( 52 | (self.fromAddress, self.toAddress, self.protocolName), 53 | (other.fromAddress, other.toAddress, other.protocolName) 54 | ) 55 | return stablesort(self, other) 56 | 57 | 58 | def __hash__(self): 59 | return hash((self.fromAddress, 60 | self.toAddress, 61 | self.protocolName)) + 7 62 | 63 | 64 | def connect(self, protocolFactory): 65 | # From twisted.python.context import get 66 | # get("q2q-service") 67 | return self.service.connectQ2Q( 68 | self.fromAddress, self.toAddress, self.protocolName, 69 | protocolFactory) 70 | -------------------------------------------------------------------------------- /vertex/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- test-case-name: vertex.test.test_q2q -*- 2 | # Copyright (c) Twisted Matrix Laboratories. 3 | # See LICENSE for details. 4 | 5 | """ 6 | All exception types defined for Vertex. 7 | """ 8 | 9 | class ConnectionError(Exception): 10 | """ 11 | An error occurred trying to establish a connection. 12 | """ 13 | 14 | 15 | 16 | class AttemptsFailed(ConnectionError): 17 | """ 18 | All attempts to establish a connection have failed. 19 | """ 20 | 21 | 22 | 23 | class NoAttemptsMade(ConnectionError): 24 | """ 25 | No viable connection paths were found so no attempts to connect were made. 26 | """ 27 | 28 | 29 | 30 | class VerifyError(Exception): 31 | """ 32 | An error occurred while verifying or authenticating a certificate. 33 | """ 34 | 35 | 36 | 37 | class BadCertificateRequest(VerifyError): 38 | """ 39 | The given certificate request could not be signed because of a problem with 40 | either it or the party it was sent to. 41 | """ 42 | -------------------------------------------------------------------------------- /vertex/gtk2hack.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Twisted Matrix Laboratories. 2 | # See LICENSE for details. 3 | 4 | import os 5 | import rfc822 6 | 7 | from twisted.python.filepath import FilePath 8 | 9 | # We import gtk ### pyflakes complains about this, due to the next line 10 | import gtk.glade 11 | 12 | from vertex.q2qclient import ClientQ2QService 13 | from vertex.q2q import Q2QAddress 14 | 15 | class _NullCb: 16 | def __init__(self, name): 17 | self.name = name 18 | 19 | 20 | def __call__(self, *a, **kw): 21 | print 'No callback provided for', self.name, a, kw 22 | 23 | 24 | 25 | class _SignalAttacher: 26 | def __init__(self, original): 27 | self.original = original 28 | 29 | 30 | def __getitem__(self, callbackName): 31 | return getattr( 32 | self.original, callbackName, None) or _NullCb(callbackName) 33 | 34 | GLADE_FILE = os.path.splitext(__file__)[0] + '.glade' 35 | 36 | class IdentificationDialog: 37 | def __init__(self, clientService, plug): 38 | self.xml = gtk.glade.XML(GLADE_FILE, "ident_dialog") 39 | self.clientService = clientService 40 | self.xml.signal_autoconnect(_SignalAttacher(self)) 41 | self.addressEntry = self.xml.get_widget('addressEntry') 42 | self.passwordEntry = self.xml.get_widget('passwordEntry') 43 | self.progressBar = self.xml.get_widget('identifyProgressBar') 44 | self.progressLabel = self.xml.get_widget('identifyProgressLabel') 45 | self.identifyWindow = self.xml.get_widget("ident_dialog") 46 | self.cancelButton = self.xml.get_widget('cancelbutton1') 47 | self.okButton = self.xml.get_widget('okbutton1') 48 | self.plug = plug 49 | 50 | 51 | def identifyCancel(self, event): 52 | self.identifyWindow.destroy() 53 | 54 | 55 | def identifyOK(self, event): 56 | idstr = self.addressEntry.get_text() 57 | D = self.clientService.authorize( 58 | Q2QAddress.fromString(idstr), 59 | self.passwordEntry.get_text()) 60 | 61 | sensitiveWidgets = [self.addressEntry, 62 | self.passwordEntry, 63 | self.okButton, 64 | self.cancelButton] 65 | for widget in sensitiveWidgets: 66 | widget.set_sensitive(False) 67 | self.progressLabel.set_text("Authenticating...") 68 | def itWorked(workedNone): 69 | self.identifyWindow.destroy() 70 | self.plug.setCurrentID(idstr) 71 | def itDidntWork(error): 72 | self.progressLabel.set_text(error.getErrorMessage()) 73 | for widget in sensitiveWidgets: 74 | widget.set_sensitive(True) 75 | D.addCallbacks(itWorked, itDidntWork) 76 | 77 | 78 | 79 | class AddContactDialog: 80 | def __init__(self, plug): 81 | self.xml = gtk.glade.XML(GLADE_FILE, "add_contact_dialog") 82 | self.xml.signal_autoconnect(_SignalAttacher(self)) 83 | self.window = self.xml.get_widget("add_contact_dialog") 84 | self.window.show_all() 85 | self.plug = plug 86 | 87 | 88 | def doAddContact(self, evt): 89 | name = self.xml.get_widget("nameentry").get_text() 90 | addr = self.xml.get_widget("q2qidentry").get_text() 91 | self.plug.addBuddy(name, addr) 92 | self.popdownDialog() 93 | 94 | 95 | def popdownDialog(self, evt=None): 96 | self.window.destroy() 97 | 98 | 99 | 100 | class AcceptConnectionDialog: 101 | def __init__(self, d, From, to, protocol): 102 | self.d = d 103 | self.xml = gtk.glade.XML(GLADE_FILE, "accept_connection_dialog") 104 | self.xml.signal_autoconnect(_SignalAttacher(self)) 105 | self.label = self.xml.get_widget("accept_connection_label") 106 | self.label.set_text( 107 | "Accept connection from %s for %s?" % (From, protocol)) 108 | self.window = self.xml.get_widget("accept_connection_dialog") 109 | self.window.show_all() 110 | 111 | done = False 112 | 113 | 114 | def destroyit(self, evt): 115 | self.window.destroy() 116 | 117 | 118 | def acceptConnectionEvt(self, evt): 119 | self.done = True 120 | print "YES" 121 | self.d.callback(1) 122 | print "WHAT" 123 | self.window.destroy() 124 | 125 | 126 | def rejectConnectionEvt(self, evt): 127 | print "DSTRY" 128 | if not self.done: 129 | print "DIE!" 130 | from twisted.python import failure 131 | self.d.errback( 132 | failure.Failure(KeyError("Connection rejected by user")) 133 | ) 134 | else: 135 | print "OK" 136 | 137 | from twisted.internet.protocol import ServerFactory 138 | from twisted.internet.protocol import Protocol 139 | 140 | class VertexDemoProtocol(Protocol): 141 | 142 | def connectionMade(self): 143 | print 'CONN MADE' 144 | 145 | 146 | def dataReceived(self, data): 147 | print 'HOLY SHNIKIES', data 148 | 149 | 150 | 151 | class VertexFactory(ServerFactory): 152 | protocol = VertexDemoProtocol 153 | 154 | def __init__(self, plug): 155 | self.plug = plug 156 | 157 | 158 | def startFactory(self): 159 | # Self.plug.animator.stop(1) 160 | pass 161 | 162 | 163 | def stopFactory(self): 164 | # Self.plug.animator.stop(0) 165 | pass 166 | 167 | 168 | 169 | class BuddyItem: 170 | def __init__(self, plug, alias, q2qaddress): 171 | mi = self.menuItem = gtk.MenuItem(alias + " <"+q2qaddress+">") 172 | mi.connect("activate", self.initiateFileTransfer) 173 | mi.show_all() 174 | self.plug = plug 175 | self.alias = alias 176 | self.q2qaddress = q2qaddress 177 | self.plug.loadedBuddies[q2qaddress] = self 178 | 179 | 180 | def initiateFileTransfer(self, evt): 181 | print 'Initiate transfer with ' + self.alias + self.q2qaddress 182 | 183 | 184 | def addToMenu(self): 185 | self.plug.section.append(self.menuItem) 186 | 187 | 188 | def removeFromMenu(self): 189 | self.plug.section.remove(self.menuItem) 190 | 191 | from twisted.plugin import IPlugin 192 | from prime.iprime import IMenuApplication 193 | from zope.interface import implements 194 | 195 | class PlugEntry: 196 | implements(IMenuApplication, IPlugin) 197 | 198 | def __init__(self): 199 | self.xml = gtk.glade.XML(GLADE_FILE, "notification_popup") 200 | 201 | 202 | def register(self, section): 203 | print 'REGISTER' 204 | self.section = section 205 | 206 | workingdir = FilePath(os.path.expanduser("~/.vertex")) 207 | self.clientService = ClientQ2QService( 208 | workingdir.child("q2q-certificates").path, 209 | verifyHook=self.displayVerifyDialog, 210 | inboundTCPPortnum=8172, 211 | # Q2qPortnum=8173, 212 | udpEnabled=False) 213 | self.setCurrentID(self.clientService.getDefaultFrom()) 214 | self.buddiesfile = workingdir.child("q2q-buddies.txt") 215 | self.loadedBuddies = {} 216 | self.parseBuddies() 217 | 218 | 219 | def parseBuddies(self): 220 | try: 221 | self.buddyList = rfc822.AddressList(self.buddiesfile.open().read()) 222 | except IOError: 223 | return 224 | self.clearContactMenu() 225 | for dispn, addr in self.buddyList: 226 | if addr not in self.loadedBuddies: 227 | BuddyItem(self, dispn, addr) 228 | self.buildContactMenu() 229 | 230 | 231 | def clearContactMenu(self): 232 | for bud in self.loadedBuddies.values(): 233 | bud.removeFromMenu() 234 | 235 | 236 | def buildContactMenu(self): 237 | l = self.loadedBuddies.values() 238 | l.sort(key=lambda x: x.alias) 239 | l.reverse() 240 | for bud in l: 241 | bud.addToMenu() 242 | 243 | 244 | def addBuddy(self, alias, q2qaddr): 245 | temp = self.buddiesfile.temporarySibling() 246 | try: 247 | origdata = self.buddiesfile.open().read() 248 | except IOError: 249 | origdata = '' 250 | moredata = '\n%s <%s>' % (alias, q2qaddr) 251 | ftemp = temp.open('w') 252 | ftemp.write(origdata) 253 | ftemp.write(moredata) 254 | ftemp.close() 255 | temp.moveTo(self.buddiesfile) 256 | self.parseBuddies() 257 | 258 | 259 | def displayVerifyDialog(self, From, to, protocol): 260 | from twisted.internet import defer 261 | d = defer.Deferred() 262 | AcceptConnectionDialog(d, From, to, protocol) 263 | return d 264 | 265 | 266 | def setCurrentID(self, idName): 267 | 268 | if idName is not None: 269 | currentID = Q2QAddress.fromString(idName) 270 | # Log in? 271 | # Self.animator.start() 272 | SL = self.xml.get_widget( 273 | "identifymenuitem").get_children()[0].set_label 274 | def loggedIn(result): 275 | SL(str(currentID)) 276 | self.currentID = currentID 277 | def notLoggedIn(error): 278 | SL("Identify") 279 | # Self.animator.stop(0) 280 | # This following order is INSANE - you should definitely not have 281 | # to wait until the LISTEN succeeds to start the service; quite the 282 | # opposite, you should wait until the service has started, then 283 | # issue the LISTEN!! For some reason, the connection drops 284 | # immediately if you do that, and I have no idea why. As soon as I 285 | # can fix that issue the startService should be moved up previous 286 | # to listenQ2Q. 287 | self.clientService.listenQ2Q(currentID, 288 | {'vertex': VertexFactory(self)}, 289 | "desktop vertex UI").addCallbacks( 290 | loggedIn, notLoggedIn).addCallback( 291 | lambda ign: self.clientService.startService()) 292 | 293 | # XXX event handlers 294 | 295 | 296 | def toggleAnimate(self, event): 297 | if self.animator.animating: 298 | # SL("Animate") 299 | self.animator.stop() 300 | else: 301 | # SL("Stop Animating") 302 | self.animator.start() 303 | 304 | 305 | def identifyDialog(self, event): 306 | IdentificationDialog(self.clientService, self) 307 | 308 | 309 | def addContact(self, event): 310 | AddContactDialog(self) 311 | -------------------------------------------------------------------------------- /vertex/icon-active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twisted/vertex/feb591aa1b9a3b2b8fdcf53e4962dad2a0bc38ca/vertex/icon-active.png -------------------------------------------------------------------------------- /vertex/icon-inactive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twisted/vertex/feb591aa1b9a3b2b8fdcf53e4962dad2a0bc38ca/vertex/icon-inactive.png -------------------------------------------------------------------------------- /vertex/ivertex.py: -------------------------------------------------------------------------------- 1 | # Copyright 2005 Divmod, Inc. See LICENSE file for details 2 | 3 | from zope.interface import Interface 4 | 5 | class IQ2QTransport(Interface): 6 | """ 7 | I am a byte-stream-oriented transport which has Q2Q identifiers associated 8 | with the endpoints, and possibly some cryptographic verification of the 9 | authenticity of those endpoints. 10 | """ 11 | 12 | def getQ2QHost(): 13 | """ Returns a Q2QAddress object representing the user on this end of the 14 | connection. 15 | """ 16 | 17 | def getQ2QPeer(): 18 | """ Returns a Q2QAddress object representing the user on the other end of the 19 | connection. 20 | """ 21 | 22 | class IQ2QUser(Interface): 23 | """ 24 | A cred interface for Q2Q users. 25 | """ 26 | def signCertificateRequest(certificateRequest, domainCert, suggestedSerial): 27 | """ 28 | Return a signed certificate object if the subject fields in the 29 | certificateRequest are valid. 30 | """ 31 | 32 | 33 | 34 | class IQ2QUserStore(Interface): 35 | """ 36 | A store of L{IQ2QUser} providers. 37 | """ 38 | 39 | def store(domain, username, password): 40 | """ 41 | Store a password for a user. 42 | 43 | @param domain: The domain where this username is exists. 44 | @type domain: L{str} 45 | 46 | @param username: The user's name. 47 | @type username: L{str} 48 | 49 | @param password: The user's password. 50 | @type password: L{str} 51 | 52 | @return: A L{defer.Deferred} that fires when the key derived from 53 | C{password} as been associated with this user and domain. 54 | @rtype: L{defer.Deferred} 55 | """ 56 | 57 | def key(domain, username): 58 | """ 59 | Retrieve the key derived from a user's password. 60 | 61 | @param domain: The domain where this username is exists. 62 | @type domain: L{str} 63 | 64 | @param username: The user's name. 65 | @type username: L{str} 66 | 67 | @return: The derived key for this user. 68 | @rtype: L{str} 69 | """ 70 | 71 | 72 | class IFileTransfer(Interface): 73 | 74 | def getUploadSink(self, path): 75 | """ 76 | @param path: a PathFragment that the client wishes to upload to. 77 | 78 | @return: a DataSink where we'll save the data to. 79 | """ 80 | 81 | def getDownloadSource(self, path): 82 | """ 83 | @param path: a PathFragment that the client wishes to download. 84 | 85 | @return: a DataSource to download data from. 86 | """ 87 | 88 | def listChildren(self, path): 89 | """ 90 | @param path: a PathFragment that the client wishes to get a list of. 91 | 92 | @return: a list of dictionaries mapping:: 93 | {'name': str, 94 | 'size': int, 95 | 'type': vertex.filexfer.MIMEType, 96 | 'modified': datetime.datetime} 97 | """ 98 | 99 | class ISessionTokenStorage(Interface): 100 | def idFromCookie(self, cookie, domain): 101 | """Look up a user ID from the given cookie in the given domain. 102 | """ 103 | 104 | class ICertificateStorage(Interface): 105 | def getSelfSignedCertificate(self, domainName): 106 | """ 107 | @return: a Deferred which will fire with the certificate for the given 108 | domain name. 109 | """ 110 | 111 | def storeSelfSignedCertificate(self, domainName, mainCert): 112 | """ 113 | @type mainCert: C{str} 114 | @param mainCert: Serialized, self-signed certificate to associate 115 | with the given domain. 116 | 117 | @return: a Deferred which will fire when the certificate has been 118 | stored successfully. 119 | """ 120 | 121 | def getPrivateCertificate(self, domainName): 122 | """ 123 | @return: a PrivateCertificate instance, e.g. a certificate including a 124 | private key, for 'domainName'. 125 | """ 126 | 127 | def addPrivateCertificate(self, domainName, existingCertificate=None): 128 | """ 129 | """ 130 | 131 | class IOfferUp(Interface): 132 | """ 133 | Sharing control database storage. 134 | """ 135 | 136 | class IPlugin(Interface): 137 | """ 138 | """ 139 | 140 | class ITestPlugin(Interface): 141 | """ 142 | Dummy plug-in interface for unit testing. 143 | """ 144 | 145 | class ITestPlugin2(Interface): 146 | """ 147 | Dummy plug-in interface for unit testing. 148 | """ 149 | -------------------------------------------------------------------------------- /vertex/q2qadmin.py: -------------------------------------------------------------------------------- 1 | # Copyright 2005 Divmod, Inc. See LICENSE file for details 2 | 3 | from twisted.protocols.amp import Command, String 4 | 5 | class NotAllowed(Exception): 6 | pass 7 | 8 | class AddUser(Command): 9 | """ 10 | Add a user to a domain. 11 | """ 12 | commandName = "add_user" 13 | 14 | arguments = [ 15 | ("name", String()), 16 | ("password", String()) 17 | ] 18 | 19 | response = [] 20 | 21 | errors = {NotAllowed: "NotAllowed"} 22 | -------------------------------------------------------------------------------- /vertex/q2qclient.py: -------------------------------------------------------------------------------- 1 | # Copyright 2005 Divmod, Inc. See LICENSE file for details 2 | 3 | import os 4 | import sys 5 | import struct 6 | import getpass 7 | 8 | from twisted.protocols.amp import AMP 9 | 10 | from vertex import q2q, sigma 11 | from twisted.python.usage import Options 12 | 13 | from twisted.python import log 14 | from twisted.python.failure import Failure 15 | from twisted.internet import reactor 16 | from twisted.internet import protocol 17 | from twisted.internet.task import LoopingCall 18 | from twisted.internet import error 19 | from vertex.q2qadmin import AddUser 20 | 21 | class Q2QAuthorize(Options): 22 | def parseArgs(self, who, password=None): 23 | self.who = who 24 | self.password = password 25 | 26 | def reportNoCertificate(self, error): 27 | print "No certificate retrieved:", error.getErrorMessage(), "(see ~/.q2q-client-log for details)" 28 | log.err(error) 29 | return None 30 | 31 | def postOptions(self): 32 | def go(): 33 | self.parent.getService().authorize( 34 | q2q.Q2QAddress.fromString(self.who), 35 | self.password).addErrback(self.reportNoCertificate).addCallback(lambda x: reactor.stop()) 36 | 37 | if self.password is None: 38 | self.password = getpass.getpass() 39 | 40 | reactor.callWhenRunning(go) 41 | self.parent.start() 42 | 43 | 44 | class BandwidthEstimator: 45 | bufsize = 20 46 | totalBytes = 0 47 | def __init__(self, message, length): 48 | self.length = length 49 | self.message = message 50 | self.estim = [] 51 | self.bytes = 0 52 | self.call = LoopingCall(self.estimateBandwidth) 53 | self.call.start(1) 54 | 55 | def estimateBandwidth(self): 56 | bytes = self.bytes 57 | self.totalBytes += bytes 58 | self.estim.append(bytes) 59 | self.message("%0.2f k/s (%0.2d%%)" 60 | % ((sum(self.estim) / len(self.estim)) / 1024., 61 | (float(self.totalBytes) / self.length) * 100)) 62 | if len(self.estim) > self.bufsize: 63 | self.estim.pop(0) 64 | self.bytes = 0 65 | 66 | def stop(self): 67 | self.call.stop() 68 | self.estimateBandwidth() 69 | self.message("Finished receiving: %d bytes (%d%%)" % ( 70 | self.totalBytes, (float(self.totalBytes) / self.length) * 100)) 71 | 72 | class FileReceiver(protocol.Protocol): 73 | gotLength = False 74 | estimator = None 75 | 76 | def connectionMade(self): 77 | self.f = open(self.factory.program.filename, 'wb') 78 | self.factory.program.parent.info("Started receiving...") 79 | 80 | def dataReceived(self, data): 81 | if not self.gotLength: 82 | self.length ,= struct.unpack("!Q", data[:8]) 83 | data = data[8:] 84 | self.estimator = BandwidthEstimator(self.factory.program.parent.info, 85 | self.length) 86 | self.gotLength = True 87 | 88 | self.estimator.bytes += len(data) 89 | self.f.write(data) 90 | 91 | def connectionLost(self, reason): 92 | self.f.close() 93 | if self.estimator: 94 | self.estimator.stop() 95 | reactor.stop() 96 | 97 | from twisted.protocols.basic import FileSender as fsdr 98 | 99 | class FileSender(protocol.Protocol): 100 | def connectionMade(self): 101 | self.file = self.factory.openFile() 102 | self.file.seek(0, 2) 103 | self.length = self.file.tell() 104 | self.file.seek(0) 105 | self.estimator = BandwidthEstimator(self.factory.program.parent.info, 106 | self.length) 107 | self.transport.write(struct.pack("!Q", self.length)) 108 | fsdr().beginFileTransfer( 109 | self.file, self).addCallback( 110 | lambda x: self.done()) 111 | 112 | def done(self): 113 | self.factory.program.parent.info("Done sending data: %d bytes" % ( 114 | self.file.tell(),)) 115 | self.transport.loseConnection() 116 | 117 | def dataReceived(self, data): 118 | print "WTF THE CLIENT IS GETTING DATA", repr(data) 119 | 120 | def registerProducer(self, producer, streaming): 121 | self.transport.registerProducer(producer, streaming) 122 | 123 | def unregisterProducer(self): 124 | self.transport.unregisterProducer() 125 | 126 | def write(self, data): 127 | self.estimator.bytes += len(data) 128 | self.transport.write(data) 129 | 130 | def connectionLost(self, reason): 131 | reactor.stop() 132 | 133 | class FileSenderFactory(protocol.ClientFactory): 134 | protocol = FileSender 135 | 136 | def __init__(self, sendprogram): 137 | self.program = sendprogram 138 | 139 | def openFile(self): 140 | return file(self.program.filename, 'r') 141 | 142 | def clientConnectionFailed(self, connector, reason): 143 | self.program.parent.info( 144 | "Could not connect: %r" % (reason.getErrorMessage(),)) 145 | reactor.stop() 146 | 147 | def clientConnectionLost(self, connector, reason): 148 | reason.trap(error.ConnectionDone) 149 | 150 | class FileReceiverFactory(protocol.Factory): 151 | def __init__(self, program): 152 | self.program = program 153 | protocol = FileReceiver 154 | 155 | 156 | class ClientCertificateStore(q2q.DirectoryCertificateStore): 157 | def __init__(self, filepath): 158 | q2q.DirectoryCertificateStore.__init__(self, os.path.expanduser(filepath)) 159 | 160 | 161 | class ClientQ2QService(q2q.Q2QService): 162 | """ 163 | This variant is used by Q2Q clients. 164 | 165 | It is I{almost} exactly the same as L{q2q.Q2QService}, except for 166 | implementing a query for a default C{From} address for client connections, 167 | since a client service will generally only register for a single C{From} 168 | address. 169 | """ 170 | def __init__(self, certspath, *a, **kw): 171 | q2q.Q2QService.__init__(self, 172 | certificateStorage=ClientCertificateStore(certspath), 173 | q2qPortnum=0, 174 | *a, **kw) 175 | 176 | def getDefaultFrom(self, default=None): 177 | i = self.certificateStorage.localStore.iterkeys() 178 | try: 179 | return i.next() 180 | except StopIteration: 181 | return default 182 | 183 | 184 | class TunnelProtocol(protocol.Protocol): 185 | def __init__(self, tunnel): 186 | self.tunnel = tunnel 187 | self.buffer = [] 188 | 189 | def connectionMade(self): 190 | if self.tunnel is not None: 191 | self.tunnel.setTunnel(self) 192 | 193 | def dataReceived(self, data): 194 | if self.tunnel is not None: 195 | self.tunnel.transport.write(data) 196 | else: 197 | self.buffer.append(data) 198 | 199 | def setTunnel(self, tunnel): 200 | if self.tunnel is None: 201 | self.tunnel = tunnel 202 | self.dataReceived(''.join(self.buffer)) 203 | del self.buffer 204 | self.tunnel.setTunnel(self) 205 | 206 | class TunnelFactory(protocol.ClientFactory): 207 | def __init__(self, tunnel): 208 | self.tunnel = tunnel 209 | 210 | def buildProtocol(self, addr): 211 | return TunnelProtocol(self.tunnel) 212 | 213 | def clientConnectionFailed(self, connector, reason): 214 | self.tunnel.transport.loseConnection() 215 | reactor.stop() 216 | 217 | clientConnectionLost = clientConnectionFailed 218 | 219 | class Q2QTunnel(Options): 220 | optParameters = [ 221 | ['port', 'p', '13000', 'Port on which to start the TCP server'], 222 | ['destination', 'd', None, 'Q2Q address to which to create the tunnel'], 223 | ['protocol', 'r', None, 'Q2Q protocol which will operate over the tunnel']] 224 | 225 | def postOptions(self): 226 | self.toAddr = q2q.Q2QAddress.fromString(self['destination']) 227 | 228 | reactor.listenTCP(int(self['port']), self, interface='127.0.0.1') 229 | self.parent.start() 230 | 231 | def doStart(self): 232 | pass 233 | 234 | def doStop(self): 235 | pass 236 | 237 | def buildProtocol(self, addr): 238 | p = TunnelProtocol(None) 239 | svc = self.parent.getService() 240 | svc.connectQ2Q(self.parent.getFrom(), self.toAddr, 241 | self['protocol'], TunnelFactory(p)) 242 | return p 243 | 244 | class Q2QReceive(Options): 245 | optParameters = [["port", "p", "41235", "Port to start the listening server on."]] 246 | 247 | def parseArgs(self, filename): 248 | self.filename = filename 249 | 250 | def postOptions(self): 251 | serv = self.parent.getService() 252 | def pr(x): 253 | return x 254 | def stopit(err): 255 | print "Couldn't Register for File Transfer:", err.getErrorMessage() 256 | log.err(err) 257 | reactor.stop() 258 | serv.listenQ2Q(self.parent.getFrom(), 259 | {'file-transfer': FileReceiverFactory(self)}, 260 | "simple file transfer test").addCallback(pr).addErrback(stopit) 261 | self.parent.start() 262 | 263 | class Q2QSend(Options): 264 | 265 | def parseArgs(self, to, filename): 266 | self.to = to 267 | self.filename = filename 268 | 269 | def postOptions(self): 270 | fs = q2q.Q2QAddress.fromString 271 | toAddress = fs(self.to) 272 | fromAddress = self.parent.getFrom() 273 | 274 | svc = self.parent.getService() 275 | svc.connectQ2Q(fromAddress, toAddress, 'file-transfer', 276 | FileSenderFactory(self)) 277 | self.parent.start() 278 | 279 | 280 | class TextNexusUI(sigma.BaseNexusUI): 281 | def __init__(self): 282 | sigma.BaseNexusUI.__init__(self) 283 | self.call = LoopingCall(self.report) 284 | self.call.start(5) 285 | 286 | def report(self): 287 | print 'Transloads:', len(self.transloads) 288 | for transloadui in self.transloads: 289 | print '---', transloadui.name, '---' 290 | print transloadui.bits.percent() 291 | for peer, mask in transloadui.masks.items(): 292 | print peer, mask.percent() 293 | print 'end report' 294 | 295 | class Q2QSigma(Options): 296 | 297 | def __init__(self, *a, **k): 298 | Options.__init__(self,*a,**k) 299 | self.pushers = [] 300 | 301 | def opt_push(self, filename): 302 | self.pushers.append([file(filename), filename, []]) 303 | 304 | def opt_to(self, q2qid): 305 | fs = q2q.Q2QAddress.fromString 306 | addr = fs(q2qid) 307 | self.pushers[-1][-1].append(addr) 308 | 309 | def postOptions(self): 310 | nex = sigma.Nexus(self.parent.getService(), 311 | self.parent.getFrom(), 312 | TextNexusUI()) 313 | # XXX TODO: there has _GOT_ to be a smarter way to handle text UI for 314 | # this. 315 | for sharefile, sharename, sharepeers in self.pushers: 316 | nex.push(sharefile, sharename, sharepeers) 317 | self.parent.start() 318 | 319 | 320 | 321 | def enregister(svc, newAddress, password): 322 | """ 323 | Register a new account and return a Deferred that fires if it worked. 324 | 325 | @param svc: a Q2QService 326 | 327 | @param newAddress: a Q2QAddress object 328 | 329 | @param password: a shared secret (str) 330 | """ 331 | return svc.connectQ2Q(q2q.Q2QAddress("",""), 332 | q2q.Q2QAddress(newAddress.domain, "accounts"), 333 | 'identity-admin', 334 | protocol.ClientFactory.forProtocol(AMP) 335 | ).addCallback( 336 | AMP.callRemote, 337 | AddUser, 338 | name=newAddress.resource, 339 | password=password 340 | ).addErrback( 341 | Failure.trap, 342 | error.ConnectionDone 343 | ) 344 | 345 | class Q2QRegister(Options): 346 | synopsis = " " 347 | def parseArgs(self, newaddress, password): 348 | self.newaddress = newaddress 349 | self.password = password 350 | 351 | def postOptions(self): 352 | fs = q2q.Q2QAddress.fromString 353 | newAddress = fs(self.newaddress) 354 | svc = self.parent.getService() 355 | 356 | def showit(x): 357 | print "%s: %s" % (x.value.__class__, x.getErrorMessage()) 358 | 359 | enregister(svc, newAddress, self.password).addErrback( 360 | showit).addBoth(lambda nothing: reactor.stop()) 361 | self.parent.start() 362 | 363 | 364 | class Q2QClientProgram(Options): 365 | subCommands = [ 366 | ['authorize', 'a', Q2QAuthorize, 'Authorize a user'], 367 | ['register', 'r', Q2QRegister, 'Create a new user '], 368 | ['tunnel', 't', Q2QTunnel, 'Create an SSL tunnel to a given resource'], 369 | ['receive', 'l', Q2QReceive, 'Receive for a filetransfer connection'], 370 | ['send', 's', Q2QSend, 'Send'], 371 | ['sigma', 'g', Q2QSigma, 'Sigma swarming file-transfer'] 372 | ] 373 | 374 | optParameters = [ 375 | ['from', 'f', None, "Who to send as?"], 376 | ['tcp', 'p', None, 'TCP port number'], 377 | ['udp', 'u', 0, 'UDP port number'], 378 | ['certspath', 'c', "~/.q2qcerts", 379 | "Path to directory full of public/private certificates."], 380 | ['logfile', 'l', "~/.q2q-client-log", 381 | "Path to file where logs of client activity will be written."] 382 | ] 383 | 384 | optFlags = [] 385 | 386 | service = None 387 | 388 | def postOptions(self): 389 | if not self.subCommand: 390 | self.opt_help() 391 | 392 | def info(self, message): 393 | sys.stderr.write(">> %s\n" % (message,)) 394 | 395 | def getService(self): 396 | if self.service is None: 397 | u = self['udp'] 398 | if u is not None: 399 | u = int(u) 400 | t = self['tcp'] 401 | if t is not None: 402 | t = int(t) 403 | self.service = ClientQ2QService(self['certspath'], 404 | inboundTCPPortnum=t) 405 | return self.service 406 | 407 | def getDefaultPath(self): 408 | return os.path.expanduser(os.path.join(self['certspath'], 'default-address')) 409 | 410 | def getFrom(self): 411 | fr = self['from'] 412 | if not fr: 413 | defpath = self.getDefaultPath() 414 | if os.path.exists(defpath): 415 | fr = file(defpath).read() 416 | else: 417 | fr = self.getService().getDefaultFrom() 418 | if fr is None: 419 | self.info("No default address available, exiting.") 420 | self.info( 421 | " (Try 'q2q register yourself@divmod.net; " 422 | "q2q authorize yourself@divmod.net')") 423 | sys.exit(19) 424 | self.info("Selected default address:" +fr) 425 | f = file(defpath, 'wb') 426 | f.write(fr) 427 | f.close() 428 | 429 | return q2q.Q2QAddress.fromString(fr) 430 | 431 | def start(self, portno=None): 432 | import sys 433 | lfname = self['logfile'] 434 | if lfname == '-': 435 | lf = sys.stdout 436 | else: 437 | lf = file(os.path.expanduser(lfname), 'ab+') 438 | log.startLogging(lf, 439 | setStdout=False) 440 | srv = self.getService() 441 | from twisted.application.app import startApplication 442 | startApplication(srv, False) 443 | reactor.run() 444 | 445 | verbosity = 0 446 | 447 | def verboseLogger(self, messageDict): 448 | self.info(' '.join([str(x) for x in messageDict.get('message', [])])) 449 | 450 | def opt_verbose(self): 451 | self.verbosity += 1 452 | log.addObserver(log.FileLogObserver(sys.stderr).emit) 453 | 454 | opt_v = opt_verbose 455 | -------------------------------------------------------------------------------- /vertex/q2qstandalone.py: -------------------------------------------------------------------------------- 1 | # -*- test-case-name:vertex.test.test_standalone -*- 2 | 3 | # Copyright 2005 Divmod, Inc. See LICENSE file for details 4 | 5 | import os 6 | 7 | from twisted.cred.portal import Portal 8 | 9 | from twisted.internet import defer 10 | from twisted.protocols.amp import AMP, Box, parseString 11 | from twisted.python.filepath import FilePath 12 | 13 | from vertex import q2q 14 | from vertex.ivertex import IQ2QUserStore 15 | from vertex.depserv import DependencyService, Conf 16 | from vertex.q2qadmin import AddUser, NotAllowed 17 | 18 | import attr 19 | import txscrypt 20 | 21 | from zope.interface import implementer 22 | 23 | 24 | class IdentityAdmin(AMP): 25 | 26 | @AddUser.responder 27 | def command_ADD_USER(self, name, password): 28 | # all security is transport security 29 | theDomain = self.transport.getQ2QHost().domain 30 | userDeferred = self.factory.store.addUser(theDomain, name, password) 31 | userDeferred.addCallback(lambda _: {}) 32 | return userDeferred 33 | 34 | 35 | class IdentityAdminFactory: 36 | def __init__(self, certstore): 37 | self.store = certstore 38 | 39 | def buildProtocol(self, addr): 40 | p = IdentityAdmin() 41 | p.factory = self 42 | return p 43 | 44 | def examineRequest(self, fromAddress, toAddress, protocolName): 45 | if toAddress.resource == "accounts" and protocolName == "identity-admin": 46 | return [(self, "identity admin")] 47 | return [] 48 | 49 | 50 | 51 | @implementer(IQ2QUserStore) 52 | @attr.s 53 | class _UserStore(object): 54 | """ 55 | A L{IQ2QUserStore} implementation that stores usernames, domains, 56 | and keys derived from passwords in files. 57 | 58 | @param path: Where to write user information. 59 | @type path: L{str} 60 | 61 | @param keyDeriver: An object whose C{computeKey} method 62 | matches L{txscrypt.computeKey} 63 | @type keyDeriver: L{txscrypt} 64 | """ 65 | 66 | path = attr.ib(convert=FilePath) 67 | _keyDeriver = attr.ib(default=txscrypt) 68 | 69 | 70 | def store(self, domain, username, password): 71 | """ 72 | Store a key derived from this password, for this user, in this 73 | domain. 74 | 75 | @param domain: The domain for this user. 76 | @type domain: L{str} 77 | 78 | @param username: The name of this user. 79 | @type username: L{str} 80 | 81 | @param password: This user's password. 82 | @type password: L{str} 83 | 84 | @return: A L{defer.Deferred} that fires with the domain, 85 | username pair if this user has never been seen before, and 86 | L{NotAllowed} if it has. 87 | @rtype: L{defer.Deferred} 88 | """ 89 | domainpath = self.path.child(domain) 90 | domainpath.makedirs(ignoreExistingDirectory=True) 91 | userpath = domainpath.child(username + ".info") 92 | if userpath.exists(): 93 | return defer.fail(NotAllowed()) 94 | 95 | def _cbWriteIdentity(key): 96 | with userpath.open('w') as f: 97 | f.write(Box(username=username, 98 | key=key).serialize()) 99 | return (domain, username) 100 | 101 | keyDeferred = self._keyDeriver.computeKey(password) 102 | keyDeferred.addCallback(_cbWriteIdentity) 103 | return keyDeferred 104 | 105 | 106 | def key(self, domain, username): 107 | """ 108 | Retrieve the derived key for user with this name, in this 109 | domain. 110 | 111 | @param domain: This user's domain. 112 | @type domain: L{str} 113 | 114 | @param username: This user's name. 115 | @type username: L{str} 116 | 117 | @return: The user's key if they exist; otherwise L{None}. 118 | @rtype: L{str} or L{None} 119 | """ 120 | userpath = self.path.child(domain).child(username + ".info") 121 | if userpath.exists(): 122 | with userpath.open() as f: 123 | data = parseString(f.read())[0] 124 | return data['key'] 125 | 126 | 127 | 128 | class DirectoryCertificateAndUserStore(q2q.DirectoryCertificateStore): 129 | def __init__(self, filepath): 130 | q2q.DirectoryCertificateStore.__init__(self, filepath) 131 | self.users = _UserStore(os.path.join(filepath, "users")) 132 | 133 | def getPrivateCertificate(self, domain): 134 | try: 135 | return q2q.DirectoryCertificateStore.getPrivateCertificate(self, domain) 136 | except KeyError: 137 | if len(self.localStore.keys()) > 10: 138 | # avoid DoS; nobody is going to need autocreated certs for more 139 | # than 10 domains 140 | raise 141 | self.addPrivateCertificate(domain) 142 | return q2q.DirectoryCertificateStore.getPrivateCertificate(self, domain) 143 | 144 | class StandaloneQ2Q(DependencyService): 145 | def setup_Q2Q(self, path, 146 | q2qPortnum=q2q.port, 147 | inboundTCPPortnum=q2q.port+1, 148 | publicIP=None 149 | ): 150 | """Set up a Q2Q service. 151 | """ 152 | store = DirectoryCertificateAndUserStore(path) 153 | # store.addPrivateCertificate("kazekage") 154 | # store.addUser("kazekage", "username", "password1234") 155 | 156 | self.attach(q2q.Q2QService( 157 | protocolFactoryFactory=IdentityAdminFactory(store).examineRequest, 158 | certificateStorage=store, 159 | portal=Portal(store, checkers=[store]), 160 | q2qPortnum=q2qPortnum, 161 | inboundTCPPortnum=inboundTCPPortnum, 162 | publicIP=publicIP, 163 | )) 164 | 165 | def defaultConfig(): 166 | # Put this into a .tac file< and customize to your heart's content 167 | c = Conf() 168 | s = c.section 169 | s('q2q', 170 | path='q2q-data') 171 | application = deploy(**c) 172 | return application 173 | 174 | deploy = StandaloneQ2Q.deploy 175 | -------------------------------------------------------------------------------- /vertex/scripts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twisted/vertex/feb591aa1b9a3b2b8fdcf53e4962dad2a0bc38ca/vertex/scripts/__init__.py -------------------------------------------------------------------------------- /vertex/sigma.py: -------------------------------------------------------------------------------- 1 | # -*- test-case-name: vertex.test.test_sigma -*- 2 | # Copyright 2005 Divmod, Inc. See LICENSE file for details 3 | 4 | """ 5 | %file transfer protocol, a-la bittorrent. 6 | """ 7 | 8 | import array 9 | import random 10 | import sha 11 | import os 12 | import sets 13 | 14 | from twisted.internet import protocol 15 | 16 | from twisted.python.filepath import FilePath 17 | 18 | from twisted.protocols.amp import Integer, String, Command, AMP 19 | 20 | from vertex import q2q 21 | from vertex import bits 22 | from vertex import conncache 23 | from vertex import endpoint 24 | 25 | __metaclass__ = type 26 | 27 | # protocol below 28 | 29 | PROTOCOL_NAME = 'sigma' 30 | 31 | class VerifyError(Exception): 32 | pass 33 | 34 | class BitArrayArgument(String): 35 | def toString(self, arr): 36 | return str(arr.size) + ':' + arr.bytes.tostring() 37 | 38 | def fromString(self, st): 39 | size, bytes = st.split(":", 1) 40 | b = array.array("B") 41 | b.fromstring(bytes) 42 | return bits.BitArray(b, int(size)) 43 | 44 | class Put(Command): 45 | """ 46 | Tells the remote end it should request a file from me. 47 | """ 48 | 49 | arguments = [("name", String())] 50 | 51 | 52 | class Get(Command): 53 | """ 54 | Tells the remote it should start sending me chunks of a file. 55 | """ 56 | 57 | arguments = [("name", String()), 58 | ('mask', BitArrayArgument(optional=True))] 59 | 60 | response = [("size", Integer())] # number of octets!! 61 | 62 | 63 | 64 | class Data(Command): 65 | """ 66 | Sends some data for a transfer. 67 | """ 68 | requiresAnswer = False 69 | 70 | arguments = [('name', String()), 71 | ('chunk', Integer()), 72 | ('body', String())] 73 | 74 | 75 | 76 | class Introduce(Command): 77 | """ 78 | Tells the remote end about another node which should have information about 79 | this transfer. 80 | 81 | Peer: the address of the peer 82 | Name: the name of the file given. 83 | """ 84 | requiresAnswer = False 85 | arguments = [('peer', q2q.Q2QAddressArgument()), 86 | ('name', String())] 87 | 88 | 89 | 90 | class Verify(Command): 91 | """ 92 | Verify that the checksum of the given chunk is correct. 93 | 94 | Errors: 95 | 96 | - chunk checksum incorrect 97 | - host hasn't computed checksum for that chunk yet. 98 | """ 99 | 100 | arguments = [('name', String()), 101 | ('peer', q2q.Q2QAddressArgument()), 102 | ('chunk', Integer()), 103 | ('sha1sum', String())] 104 | 105 | 106 | 107 | # this is a fixed, protocol-level constant 108 | CHUNK_SIZE = 1024 * 16 109 | 110 | DONE = {} # perhaps Juice should map this to None? 111 | 112 | def countChunks(bytes): 113 | div, mod = divmod(bytes, CHUNK_SIZE) 114 | div += bool(mod) 115 | return div 116 | 117 | class SigmaProtocol(AMP): 118 | """I am a connection to a peer who has some resources I want in the 119 | file-swarming network. 120 | """ 121 | 122 | def __init__(self, nexus): 123 | AMP.__init__(self) 124 | self.nexus = nexus 125 | self.sentTransloads = [] 126 | 127 | 128 | def _get(self, name, mask=None): 129 | peer = self.transport.getQ2QPeer() 130 | tl = self.nexus.transloads[name] 131 | size = tl.getSize() 132 | if mask is None: 133 | # all zeroes! 134 | mask = bits.BitArray(size=countChunks(size)) 135 | # retrieve persistent scoring and such? 136 | tl.updatePeerMask(peer, mask) 137 | peerK = tl.peers[peer] 138 | if (not peerK.sentGet) and peerK.mask.any(0): 139 | # send a reciprocal GET 140 | self.get(name, tl.mask) 141 | return dict(size=size) 142 | Get.responder(_get) 143 | 144 | 145 | def _data(self, name, chunk, body): 146 | self.nexus.transloads[name].chunkReceived( 147 | self.transport.getQ2QPeer(), chunk, body) 148 | return DONE 149 | Data.responder(_data) 150 | 151 | 152 | def _put(self, name): 153 | peer = self.transport.getQ2QPeer() 154 | incompleteFilePath, fullFilePath = self.nexus.ui.allocateFile( 155 | name, peer) 156 | self.nexus.pull(incompleteFilePath, fullFilePath, name, peer) 157 | return DONE 158 | Put.responder(_put) 159 | 160 | 161 | def _verify(self, peer, name, chunk, sha1sum): 162 | if self.nexus.transloads[name].verifyLocalChunk(peer, chunk, sha1sum): 163 | return dict() 164 | raise RuntimeError("checksum incorrect") 165 | Verify.responder(_verify) 166 | 167 | 168 | def data(self, name, chunk, body): 169 | """ 170 | Issue a DATA command 171 | 172 | return None 173 | 174 | Sends a chunk of data to a peer. 175 | """ 176 | self.callRemote(Data, name=name, chunk=chunk, body=body) 177 | 178 | 179 | def introduce(self, name, peerToIntroduce): 180 | self.callRemote( 181 | Introduce, peer=peerToIntroduce, name=name) 182 | 183 | 184 | def _introduce(self, peer, name): 185 | # Like a PUT, really, but assuming the transload is already 186 | # established. 187 | 188 | self.nexus.ui.receivedIntroduction(peer, name) 189 | 190 | t = self.nexus.transloads[name] 191 | if peer in t.peers: 192 | return {} 193 | 194 | # all bits are set until he responds that he wants something. 195 | 196 | t.updatePeerMask(peer, bits.BitArray(default=1, size=len(t.mask))) 197 | 198 | self.nexus.connectPeer(peer).addCallback( 199 | lambda peerProto: peerProto.get(name, t.mask)) 200 | return {} 201 | Introduce.responder(_introduce) 202 | 203 | 204 | def get(self, name, mask=None): 205 | """ 206 | Issue a GET command 207 | 208 | Return a Deferred which fires with the size of the name being requested 209 | """ 210 | mypeer = self.transport.getQ2QPeer() 211 | tl = self.nexus.transloads[name] 212 | peerz = tl.peers 213 | if mypeer in peerz: 214 | peerk = peerz[mypeer] 215 | else: 216 | # all turned on initially; we aren't going to send them anything. 217 | peerk = PeerKnowledge(bits.BitArray(size=len(tl.mask), default=1)) 218 | peerz[mypeer] = peerk 219 | peerk.sentGet = True 220 | return self.callRemote( 221 | Get, name=name, mask=mask).addCallback(lambda r: r['size']) 222 | 223 | 224 | def verify(self, name, peer, chunkNumber, sha1sum): 225 | return self.callRemote( 226 | Verify, name=name, peer=peer, chunk=chunkNumber, sha1sum=sha1sum) 227 | 228 | 229 | def connectionMade(self): 230 | self.nexus.conns.cacheUnrequested(endpoint.Q2QEndpoint( 231 | self.nexus.svc, 232 | self.nexus.addr, 233 | self.transport.getQ2QPeer(), 234 | PROTOCOL_NAME), None, self) 235 | self.transport.registerProducer(self, 0) 236 | 237 | 238 | def connectionLost(self, reason): 239 | """ 240 | Inform the associated L{conncache.ConnectionCache} that this 241 | protocol has been disconnected. 242 | """ 243 | self.nexus.conns.connectionLostForKey((endpoint.Q2QEndpoint( 244 | self.nexus.svc, 245 | self.nexus.addr, 246 | self.transport.getQ2QPeer(), 247 | PROTOCOL_NAME), None)) 248 | AMP.connectionLost(self, reason) 249 | 250 | 251 | def stopProducing(self): 252 | "" 253 | 254 | pauses = 0 255 | 256 | def pauseProducing(self): 257 | self.pauses += 1 258 | 259 | def resumeProducing(self): 260 | """ 261 | algorithm needed here: determine the proportion of my bandwidth that 262 | should be going to _ALL_ consumers based on the proportion of the sum 263 | of all scores that are available. then determine how long I need to 264 | wait before I send data to my peer. 265 | """ 266 | self.nexus.callLater(0.0001, self.sendSomeData, 2) 267 | 268 | def sendSomeData(self, howMany): 269 | """ 270 | Send some DATA commands to my peer(s) to relay some data. 271 | 272 | @param howMany: an int, the number of chunks to send out. 273 | """ 274 | # print 'sending some data', howMany 275 | if self.transport is None: 276 | return 277 | peer = self.transport.getQ2QPeer() 278 | while howMany > 0: 279 | # sort transloads so that the least-frequently-serviced ones will 280 | # come first 281 | tloads = [ 282 | (findin(tl.name, self.sentTransloads), 283 | tl) for tl in self.nexus.transloadsForPeer(peer)] 284 | tloads.sort() 285 | tloads = [tl for (idx, tl) in tloads if tl.peerNeedsData(peer)] 286 | if not tloads: 287 | break 288 | 289 | wasHowMany = howMany 290 | 291 | for myTransload in tloads: 292 | # move this transload to the end so it will be sorted last next 293 | # time. 294 | name = myTransload.name 295 | if name in self.sentTransloads: 296 | self.sentTransloads.remove(name) 297 | self.sentTransloads.append(name) 298 | 299 | knowledge = myTransload.peers[peer] 300 | chunkNumber, chunkData = myTransload.selectOptimalChunk(peer) 301 | if chunkNumber is None: 302 | continue 303 | 304 | peerToIntroduce = knowledge.selectPeerToIntroduce( 305 | myTransload.peers.keys()) 306 | 307 | if peerToIntroduce is not None: 308 | self.introduce(myTransload.name, peerToIntroduce) 309 | 310 | self.data(name, chunkNumber, chunkData) 311 | # Don't re-send that chunk again unless they explicitly tell us 312 | # they need it for some reason 313 | knowledge.mask[chunkNumber] = 1 314 | howMany -= 1 315 | if howMany <= 0: 316 | break 317 | 318 | if wasHowMany == howMany: 319 | # couldn't find anything to send. 320 | break 321 | 322 | 323 | def findin(item, list): 324 | """ 325 | Find C{item} in C{list}. 326 | """ 327 | try: 328 | return list.index(item) 329 | except ValueError: 330 | # x not in list 331 | return -1 332 | 333 | class PeerKnowledge: 334 | """ 335 | Local representation of a peer's knowledge of a transload. 336 | """ 337 | 338 | sentGet = False 339 | 340 | def __init__(self, mask): 341 | self.mask = mask 342 | self.otherPeers = [] 343 | 344 | def selectPeerToIntroduce(self, otherPeers): 345 | """ 346 | Choose a peer to introduce. Return a q2q address or None, if there are 347 | no suitable peers to introduce at this time. 348 | """ 349 | for peer in otherPeers: 350 | if peer not in self.otherPeers: 351 | self.otherPeers.append(peer) 352 | return peer 353 | 354 | 355 | class Transload: 356 | """ 357 | An upload/download currently in progress 358 | 359 | @ivar maximumMaskUpdateDelayAfterChange: the maximum amount of time to wait 360 | after a change to the bitmask before sending out an updated mask to 361 | our peers. 362 | 363 | @ivar maximumChangeCountBeforeMaskUpdate: the maximum number of bits we 364 | will allow to change in our mask before sending an update to our 365 | peers. 366 | 367 | """ 368 | 369 | maximumMaskUpdateDelayAfterChange = 30.0 370 | maximumChangeCountBeforeMaskUpdate = 25 371 | 372 | def __init__(self, authority, nexus, name, 373 | incompletePath, fullPath, ui, 374 | seed=False): 375 | """ 376 | Create a Transload. 377 | 378 | @param authority: the q2q address of the first authority on this file. 379 | """ 380 | 381 | self.incompletePath = incompletePath 382 | self.fullPath = fullPath 383 | 384 | self.ui = ui 385 | self.authorities = [authority] # q2q address(es) that you send VERIFYs to 386 | 387 | self.seed = seed 388 | 389 | if not seed: 390 | self.file = openReadWrite(incompletePath.path) 391 | else: 392 | self.file = fullPath.open() 393 | 394 | chunkCount = countChunks(self.getSize()) 395 | mask = bits.BitArray(size=chunkCount, default=int(seed)) 396 | if seed: 397 | maskfile = None 398 | else: 399 | maskfile = openMaskFile(incompletePath.path) 400 | 401 | self.mask = mask # BitArray object representing which chunks of 402 | # the file I've got 403 | self.maskfile = maskfile # ugh - open file object that keeps a record 404 | # of the bitmask 405 | self.sha1sums = {} # map {chunk-number: sha1sum} 406 | self.nexus = nexus # Nexus instance that I belong to 407 | self.name = name # the name of the file object being 408 | # transferred. 409 | 410 | self.changes = 0 # the number of mask changes since the last update 411 | self.peers = {} # map {q2q address: [PeerKnowledge]} 412 | 413 | # We want to retransmit GET every so often 414 | self.call = self.nexus.callLater(0.002, self.maybeUpdateMask) 415 | 416 | def stop(self): 417 | if self.call is not None: 418 | self.call.cancel() 419 | self.call = None 420 | 421 | def changeSize(self, size): 422 | assert len(self.mask) == 0 423 | self.file.seek(size-1) 424 | assert self.file.read(1) == '' 425 | self.file.write("\x00") 426 | chunkCount = countChunks(size) 427 | self.mask = bits.BitArray(size=chunkCount) 428 | self.writeMaskFile() 429 | 430 | def writeMaskFile(self): 431 | self.maskfile.seek(0) 432 | self.maskfile.write(buffer(self.mask.bytes)) 433 | self.maskfile.flush() 434 | 435 | def updatePeerMask(self, peer, mask): 436 | if peer in self.peers: 437 | self.peers[peer].mask = mask 438 | else: 439 | self.peers[peer] = PeerKnowledge(mask) 440 | self.ui.updatePeerMask(peer, mask) 441 | 442 | def verifyLocalChunk(self, peer, chunkNumber, remoteSum): 443 | assert self.mask[chunkNumber] # XXX legit exception(?) 444 | localSum = self.sha1sums.get(chunkNumber) 445 | if localSum is None: 446 | self.file.seek(chunkNumber * CHUNK_SIZE) 447 | localChunk = self.file.read(CHUNK_SIZE) 448 | localSum = self.sha1sums[chunkNumber] = sha.new(localChunk).digest() 449 | return remoteSum == localSum 450 | 451 | def getSize(self): 452 | """ 453 | return the size of my file in bytes 454 | """ 455 | self.file.seek(0, 2) 456 | return self.file.tell() 457 | 458 | def chunkReceived(self, who, chunkNumber, chunkData): 459 | """ 460 | A chunk was received from the peer. 461 | """ 462 | def verifyError(error): 463 | error.trap(VerifyError) 464 | self.nexus.decreaseScore(who, self.authorities) 465 | return self.nexus.verifyChunk(self.name, 466 | who, 467 | chunkNumber, 468 | sha.new(chunkData).digest(), 469 | self.authorities).addCallbacks( 470 | lambda whatever: self.chunkVerified(who, chunkNumber, chunkData), 471 | verifyError) 472 | 473 | def chunkVerified(self, who, chunkNumber, chunkData): 474 | """A chunk (#chunkNumber) containing the data C{chunkData} was verified, sent 475 | to us by the Q2QAddress C{who}. 476 | """ 477 | if self.mask[chunkNumber]: 478 | # already received that chunk. 479 | return 480 | self.file.seek(chunkNumber * CHUNK_SIZE) 481 | self.file.write(chunkData) 482 | self.file.flush() 483 | self.sha1sums[chunkNumber] = sha.new(chunkData).digest() 484 | 485 | if not self.mask[chunkNumber]: 486 | self.nexus.increaseScore(who) 487 | self.mask[chunkNumber] = 1 488 | self.writeMaskFile() 489 | self.changes += 1 490 | 491 | if self.changes > self.maximumChangeCountBeforeMaskUpdate: 492 | self.call.cancel() 493 | self.sendMaskUpdate() 494 | self.call = self.nexus.callLater( 495 | self.maximumChangeCountBeforeMaskUpdate, 496 | self.maybeUpdateMask) 497 | 498 | if not self.seed and not self.mask.countbits(0): 499 | # we're done, let's let other people get at that file. 500 | self.file.close() 501 | os.rename(self.incompletePath.path, 502 | self.fullPath.path) 503 | self.file = self.fullPath.open() 504 | self.maskfile.close() 505 | os.unlink(self.maskfile.name) 506 | 507 | self.ui.updateHostMask(self.mask) 508 | 509 | 510 | def maybeUpdateMask(self): 511 | if self.changes: 512 | self.sendMaskUpdate() 513 | self.call = self.nexus.callLater( 514 | self.maximumMaskUpdateDelayAfterChange, 515 | self.maybeUpdateMask) 516 | 517 | 518 | def selectOptimalChunk(self, peer): 519 | """ 520 | select an optimal chunk to send to a peer. 521 | 522 | @return: int(chunkNumber), str(chunkData) if there is data to be sent, 523 | otherwise None, None 524 | """ 525 | 526 | # stuff I have 527 | have = sets.Set(self.mask.positions(1)) 528 | # stuff that this peer wants 529 | want = sets.Set(self.peers[peer].mask.positions(0)) 530 | exchangeable = have.intersection(want) 531 | finalSet = dict.fromkeys(exchangeable, 0) 532 | 533 | # taking a page from bittorrent, rarest-first 534 | for chunkNumber in exchangeable: 535 | for otherPeer in self.peers.itervalues(): 536 | finalSet[chunkNumber] += not otherPeer.mask[chunkNumber] 537 | rarityList = [(rarity, random.random(), chunkNumber) 538 | for (chunkNumber, rarity) 539 | in finalSet.iteritems()] 540 | if not rarityList: 541 | return None, None 542 | rarityList.sort() 543 | chunkNumber = rarityList[-1][-1] # sorted in ascending order of rarity 544 | 545 | # sanity check 546 | assert self.mask[chunkNumber], "I wanted to send a chunk I didn't have" 547 | 548 | self.file.seek(chunkNumber * CHUNK_SIZE) 549 | chunkData = self.file.read(CHUNK_SIZE) 550 | self.sha1sums[chunkNumber] = sha.new(chunkData).digest() 551 | return chunkNumber, chunkData 552 | 553 | 554 | def sendMaskUpdate(self): 555 | # xxx magic 556 | self.changes = 0 557 | for peer in self.peers: 558 | self.nexus.connectPeer(peer).addCallback( 559 | self._connectedPeer, peer) 560 | 561 | def _connectedPeer(self, proto, peer): 562 | proto.get(self.name, self.mask) 563 | 564 | def peerNeedsData(self, peer): 565 | mask = self.peers[peer].mask 566 | return bool(list(mask.positions(0))) 567 | 568 | def putToPeers(self, peers): 569 | def eachPeer(proto): 570 | proto.callRemote(Put, name=self.name) 571 | return proto 572 | 573 | for peer in peers: 574 | self.nexus.connectPeer(peer).addCallback(eachPeer) 575 | 576 | 577 | 578 | 579 | def openReadWrite(filename): 580 | """ 581 | Return a 2-tuple of: (whether the file existed before, open file object) 582 | """ 583 | try: 584 | os.makedirs(os.path.dirname(filename)) 585 | except OSError: 586 | pass 587 | try: 588 | return file(filename, 'rb+') 589 | except IOError: 590 | return file(filename, 'wb+') 591 | 592 | def existed(fileobj): 593 | """ 594 | Returns a boolean indicating whether a file opened by openReadWrite existed 595 | in the filesystem before it was opened. 596 | """ 597 | return 'r' in getattr(fileobj, "mode", '') 598 | 599 | def openMaskFile(filename): 600 | """ 601 | Open the bitmask file sitting next to a file in the filesystem. 602 | """ 603 | dirname, basename = os.path.split(filename) 604 | newbasename = '_%s_.sbm' % (basename,) 605 | maskfname = os.path.join(dirname, newbasename) 606 | maskfile = openReadWrite(maskfname) 607 | return maskfile 608 | 609 | 610 | class SigmaServerFactory(protocol.ServerFactory): 611 | def __init__(self, nexus): 612 | self.nexus = nexus 613 | def buildProtocol(self, addr): 614 | return SigmaProtocol(self.nexus) 615 | 616 | class SigmaClientFactory(protocol.ClientFactory): 617 | def __init__(self, nexus): 618 | self.nexus = nexus 619 | def buildProtocol(self, addr): 620 | return SigmaProtocol(self.nexus) 621 | 622 | class BaseTransloadUI: 623 | 624 | def __init__(self, nexusUI, name, sender): 625 | self.name = name 626 | self.sender = sender 627 | self.nexusUI = nexusUI 628 | self.masks = {} 629 | self.bits = bits.BitArray() 630 | 631 | def updatePeerMask(self, q2qid, bits): 632 | self.masks[q2qid] = bits 633 | 634 | def updateHostMask(self, bits): 635 | self.bits = bits 636 | 637 | class BaseNexusUI: 638 | 639 | transloadFactory = BaseTransloadUI 640 | receivedIntroductions = 0 641 | 642 | def __init__(self, basepath=os.path.expanduser("~/Sigma/Downloads")): 643 | self.basepath = FilePath(basepath) 644 | self.transloads = [] 645 | 646 | def allocateFile(self, sharename, peer): 647 | """ 648 | return a 2-tuple of incompletePath, fullPath 649 | """ 650 | peerDir = self.basepath.child(str(peer)) 651 | if not peerDir.isdir(): 652 | peerDir.makedirs() 653 | return (peerDir.child(sharename+'.incomplete'), 654 | peerDir.child(sharename)) 655 | 656 | def receivedIntroduction(self, peer, name): 657 | self.receivedIntroductions += 1 658 | 659 | def startTransload(self, *a, **kw): 660 | tl = self.transloadFactory(self, *a, **kw) 661 | self.transloads.append(tl) 662 | return tl 663 | 664 | class Nexus(object): 665 | """Orchestrator & factory 666 | """ 667 | 668 | def __init__(self, svc, addr, ui, callLater=None): 669 | """ 670 | Create a Sigma Nexus 671 | 672 | @param svc: a Q2QService 673 | 674 | @param addr: a Q2QAddress 675 | 676 | @param ui: an ISigmaNexusUI implementor. 677 | 678 | @param callLater: a callable with the signature and semantics of 679 | IReactorTime.callLater 680 | """ 681 | 682 | # callLater is for testing purposes. 683 | self.scores = {} # map q2qaddress to score 684 | self.transloads = {} # map filename to active transloads 685 | self.svc = svc 686 | self.addr = addr 687 | self.conns = conncache.ConnectionCache() 688 | if callLater is None: 689 | from twisted.internet import reactor 690 | callLater = reactor.callLater 691 | self.callLater = callLater 692 | self.ui = ui 693 | 694 | self.serverFactory = SigmaServerFactory(self) 695 | self.clientFactory = SigmaClientFactory(self) 696 | 697 | svc.listenQ2Q(addr, {PROTOCOL_NAME: self.serverFactory}, 698 | 'Nexus device description') 699 | 700 | def stopService(self): 701 | # XXX Not really a service, but maybe it should be? hmm. 702 | for transload in self.transloads.values(): 703 | transload.stop() 704 | 705 | def transloadsForPeer(self, peer): 706 | """ 707 | Returns an iterator of transloads that apply to a particular peer. 708 | """ 709 | for tl in self.transloads.itervalues(): 710 | if peer in tl.peers: 711 | yield tl 712 | 713 | def seed(self, path, name): 714 | """Create a transload from an existing file that is complete. 715 | """ 716 | t = self.transloads[name] = Transload(self.addr, self, name, 717 | None, path, 718 | self.ui.startTransload(name, 719 | self.addr), 720 | seed=True) 721 | return t 722 | 723 | def connectPeer(self, peer): 724 | """Establish a SIGMA connection to the given peer. 725 | 726 | @param peer: a Q2QAddress of a peer which has a file that I want 727 | 728 | @return: a Deferred which fires a SigmaProtocol. 729 | """ 730 | return self.conns.connectCached(endpoint.Q2QEndpoint(self.svc, 731 | self.addr, 732 | peer, 733 | PROTOCOL_NAME), 734 | self.clientFactory) 735 | 736 | 737 | def push(self, fpath, name, peers): 738 | t = self.seed(fpath, name) 739 | t.putToPeers(peers) 740 | 741 | def pull(self, incompletePath, finalPath, name, peer): 742 | t = self.transloads[name] = Transload(peer, self, name, 743 | incompletePath, finalPath, 744 | self.ui.startTransload(name, peer)) 745 | D = self.connectPeer(peer).addCallback(lambda proto: proto.get(name)) 746 | D.addCallback(t.changeSize) 747 | return D 748 | 749 | def increaseScore(self, participant): 750 | """ 751 | The participant successfully transferred a chunk to me. 752 | """ 753 | if participant not in self.scores: 754 | self.scores[participant] = 0 755 | self.scores[participant] += 1 756 | 757 | 758 | def decreaseScore(self, participant, authorities): 759 | """ 760 | Much more severe than increaseScore, this implies that the named 761 | participant has a broken client or is cheating. Report them to 762 | authorities if they do this more than once. 763 | """ 764 | self.scores[participant] -= 10 765 | 766 | 767 | def anyAuthority(self, authorities): 768 | return self.connectPeer(random.choice(authorities)) 769 | 770 | def verifyChunk(self, name, who, chunkNumber, digest, authorities): 771 | return self.anyAuthority(authorities).addCallback( 772 | lambda authority: authority.verify(name, who, chunkNumber, digest)) 773 | 774 | -------------------------------------------------------------------------------- /vertex/subproducer.py: -------------------------------------------------------------------------------- 1 | # -*- test-case-name: vertex.test.test_subproducer -*- 2 | # Copyright 2005 Divmod, Inc. See LICENSE file for details 3 | 4 | from twisted.python import log 5 | 6 | class SuperProducer: 7 | """I am a mixin which provides support for mixing in several producers to one 8 | producer. I act as a consumer for my producers and as a producer for one 9 | consumer. 10 | 11 | I must be mixed into a protocol, or something else with a 'transport' attribute. 12 | """ 13 | 14 | producersPaused = False 15 | 16 | def __init__(self): 17 | self.producingTransports = {} 18 | 19 | def pauseProducing(self): 20 | self.producersPaused = True 21 | for transport in self.producingTransports.keys(): 22 | try: 23 | transport.parentPauseProducing() 24 | except: 25 | del self.producingTransports[transport] 26 | log.err() 27 | 28 | def resumeProducing(self): 29 | producersWerePaused = self.producersPaused 30 | if producersWerePaused: 31 | self.producersPaused = False 32 | for transport in self.producingTransports.keys(): 33 | try: 34 | transport.parentResumeProducing() 35 | except: 36 | del self.producingTransports[transport] 37 | log.err() 38 | 39 | def stopProducing(self): 40 | for transport in self.producingTransports.keys(): 41 | try: 42 | transport.parentStopProducing() 43 | except: 44 | log.err() 45 | self.producingTransports = {} 46 | 47 | def registerProducerFor(self, trans): 48 | if not self.producersPaused: 49 | trans.parentResumeProducing() 50 | wasProducing = bool(self.producingTransports) 51 | assert trans not in self.producingTransports 52 | self.producingTransports[trans] = 1 53 | if not wasProducing: 54 | self.transport.registerProducer(self, False) 55 | 56 | def unregisterProducerFor(self, trans): 57 | if trans in self.producingTransports: 58 | del self.producingTransports[trans] 59 | if not self.producingTransports: 60 | self.transport.unregisterProducer() 61 | 62 | 63 | class SubProducer: 64 | """ I am a mixin that provides upwards-registration of my producer to a 65 | SuperProducer instance. 66 | """ 67 | def __init__(self, superproducer): 68 | self.superproducer = superproducer 69 | self.producer = None 70 | self.parentAcceptingData = True 71 | self.peerAcceptingData = True 72 | self.producerPaused = False 73 | self.parentStopped = False 74 | 75 | def maybeResumeProducing(self): 76 | if ((self.producer is not None) and 77 | ((not self.streamingProducer) or 78 | (self.producerPaused)) and 79 | (self.peerAcceptingData) and 80 | (self.parentAcceptingData)): 81 | self.producerPaused = False 82 | self.producer.resumeProducing() 83 | 84 | def maybePauseProducing(self): 85 | if ((self.producer is not None) and 86 | ((not self.peerAcceptingData) or 87 | (not self.parentAcceptingData)) and 88 | (not self.producerPaused)): 89 | self.producerPaused = True 90 | self.producer.pauseProducing() 91 | 92 | def parentResumeProducing(self): 93 | self.parentAcceptingData = True 94 | self.maybeResumeProducing() 95 | 96 | def parentPauseProducing(self): 97 | self.parentAcceptingData = False 98 | self.maybePauseProducing() 99 | 100 | def parentStopProducing(self): 101 | self.parentStopped = True 102 | if self.producer is not None: 103 | self.producer.stopProducing() 104 | 105 | def choke(self): 106 | self.peerAcceptingData = False 107 | self.maybePauseProducing() 108 | 109 | def unchoke(self): 110 | self.peerAcceptingData = True 111 | self.maybeResumeProducing() 112 | 113 | def registerProducer(self, producer, streaming): 114 | if self.parentStopped: 115 | producer.stopProducing() 116 | return 117 | self.producer = producer 118 | self.streamingProducer = streaming 119 | self.superproducer.registerProducerFor(self) 120 | 121 | def unregisterProducer(self): 122 | if not self.parentStopped: 123 | self.superproducer.unregisterProducerFor(self) 124 | self.producer = None 125 | 126 | -------------------------------------------------------------------------------- /vertex/tcpdfa.py: -------------------------------------------------------------------------------- 1 | # -*- test-case-name: vertex.test.test_ptcp -*- 2 | # Copyright 2005 Divmod, Inc. See LICENSE file for details 3 | 4 | from automat import MethodicalMachine 5 | 6 | 7 | class TCP(object): 8 | """ 9 | A L{TCP} represents a single connection's TCP-over-UDP state machine. 10 | """ 11 | 12 | _machine = MethodicalMachine() 13 | 14 | def __init__(self, impl): 15 | """ 16 | Initialize a L{TCP}. 17 | 18 | @param impl: The implementation of packet-sending. 19 | @type impl: L{vertex.ptcp.PTCPConnection} 20 | """ 21 | self._impl = impl 22 | self.ackPredicate = lambda packet: False 23 | 24 | @_machine.state(initial=True) 25 | def closed(self): 26 | """ 27 | The initial state where our state-machine starts, and where it ends; 28 | the connection is closed or does not exist yet. 29 | """ 30 | 31 | # This isn't detailed by the spec in the diagram, so we use a different 32 | # identifier, but in various places it does make references to going 33 | # straight to the 'closed' state. 34 | broken = closed 35 | 36 | @_machine.state() 37 | def synSent(self): 38 | """ 39 | The state in which we have sent a SYN packet to our peer, but have not 40 | received anything back yet. 41 | """ 42 | 43 | @_machine.state() 44 | def synRcvd(self): 45 | """ 46 | The state in which we have received a SYN packet from our peer, 47 | """ 48 | 49 | @_machine.state() 50 | def listen(self): 51 | """ 52 | 53 | """ 54 | 55 | 56 | @_machine.state() 57 | def established(self): 58 | """ 59 | 60 | """ 61 | 62 | 63 | @_machine.state() 64 | def closeWait(self): 65 | """ 66 | 67 | """ 68 | 69 | @_machine.state() 70 | def lastAck(self): 71 | """ 72 | 73 | """ 74 | 75 | @_machine.state() 76 | def finWait1(self): 77 | """ 78 | 79 | """ 80 | @_machine.state() 81 | def finWait2(self): 82 | """ 83 | 84 | """ 85 | 86 | @_machine.state() 87 | def closing(self): 88 | """ 89 | 90 | """ 91 | 92 | @_machine.state() 93 | def timeWait(self): 94 | """ 95 | 96 | """ 97 | 98 | @_machine.input() 99 | def appPassiveOpen(self): 100 | """ 101 | 102 | """ 103 | 104 | 105 | @_machine.input() 106 | def appActiveOpen(self): 107 | """ 108 | 109 | """ 110 | 111 | @_machine.input() 112 | def timeout(self): 113 | """ 114 | 115 | """ 116 | 117 | @_machine.input() 118 | def appClose(self): 119 | """ 120 | 121 | """ 122 | 123 | @_machine.input() 124 | def synAck(self): 125 | """ 126 | 127 | """ 128 | 129 | @_machine.input() 130 | def ack(self): 131 | """ 132 | 133 | """ 134 | 135 | @_machine.input() 136 | def rst(self): 137 | """ 138 | 139 | """ 140 | 141 | 142 | @_machine.input() 143 | def appSendData(self): 144 | """ 145 | 146 | """ 147 | 148 | 149 | @_machine.input() 150 | def syn(self): 151 | """ 152 | 153 | """ 154 | 155 | @_machine.input() 156 | def fin(self): 157 | """ 158 | 159 | """ 160 | 161 | 162 | @_machine.input() 163 | def segmentReceived(self): 164 | """ 165 | Bonus input! This is when the segment length of an incoming packet is 166 | non-zero; in other words, some data has arrived, probably (hopefully?) 167 | in ESTABLISHED, and we have to send an acknowledgement. 168 | """ 169 | 170 | 171 | @_machine.output() 172 | def expectAck(self): 173 | """ 174 | When the most recent packet produced as an output of this state machine 175 | is acknowledged by our peer, generate a single 'ack' input. 176 | """ 177 | last = self.lastTransmitted 178 | self.ackPredicate = lambda ackPacket: ( 179 | ackPacket.relativeAck() >= last.relativeSeq() 180 | ) 181 | 182 | 183 | def originate(self, **kw): 184 | """ 185 | Originate a packet. 186 | """ 187 | self.lastTransmitted = self._impl.originate(**kw) 188 | 189 | 190 | @_machine.output() 191 | def sendSyn(self): 192 | """ 193 | 194 | """ 195 | self.originate(syn=True) 196 | 197 | 198 | @_machine.output() 199 | def sendFin(self): 200 | """ 201 | 202 | """ 203 | self.originate(fin=True) 204 | 205 | 206 | @_machine.output() 207 | def sendSynAck(self): 208 | """ 209 | 210 | """ 211 | self.originate(syn=True, ack=True) 212 | 213 | 214 | @_machine.output() 215 | def sendAck(self): 216 | """ 217 | Send an ACK-only packet, immediately. 218 | """ 219 | # You never need to ACK the ACK, so don't record it as lastTransmitted. 220 | self._impl.originate(ack=True) 221 | 222 | 223 | @_machine.output() 224 | def sendAckSoon(self): 225 | """ 226 | Send an ACK-only packet, but, give it a second; some more data might be 227 | coming shortly. 228 | """ 229 | self._impl.ackSoon() 230 | 231 | 232 | @_machine.output() 233 | def sendRst(self): 234 | """ 235 | 236 | """ 237 | # note: unused / undefined in original impl, need test 238 | self.originate(rst=True) 239 | 240 | 241 | def maybeReceiveAck(self, ackPacket): 242 | """ 243 | Receive an L{ack} or L{synAck} input from the given packet. 244 | """ 245 | ackPredicate = self.ackPredicate 246 | self.ackPredicate = lambda packet: False 247 | if ackPacket.syn: 248 | # New SYN packets are always news. 249 | self.synAck() 250 | return 251 | if ackPredicate(ackPacket): 252 | self.ack() 253 | 254 | 255 | @_machine.output() 256 | def appNotifyConnected(self): 257 | """ 258 | 259 | """ 260 | # we just entered the 'established' state so clear the ack-expectation 261 | # high water mark 262 | self.ackReceiveHighWaterMark = None 263 | self._impl.connectionJustEstablished() 264 | 265 | 266 | @_machine.output() 267 | def appNotifyDisconnected(self): 268 | """ 269 | 270 | """ 271 | self._impl.connectionJustEnded() 272 | 273 | 274 | @_machine.output() 275 | def releaseResources(self): 276 | """ 277 | 278 | """ 279 | self._impl.releaseConnectionResources() 280 | 281 | 282 | @_machine.output() 283 | def startTimeWaiting(self): 284 | """ 285 | 286 | """ 287 | self._impl.scheduleTimeWaitTimeout() 288 | 289 | @_machine.output() 290 | def appNotifyListen(self): 291 | """ 292 | 293 | """ 294 | self._impl.nowListeningSocket() 295 | 296 | @_machine.output() 297 | def appNotifyHalfClose(self): 298 | """ 299 | Input ended. 300 | """ 301 | self._impl.nowHalfClosed() 302 | 303 | 304 | @_machine.output() 305 | def appNotifyAttemptFailed(self): 306 | """ 307 | 308 | """ 309 | self._impl.outgoingConnectionFailed() 310 | 311 | 312 | # invariant: if a state has .upon(ack) in it, all enter=that-state edges 313 | # here must produce the "expectAck" output. 314 | closed.upon(appPassiveOpen, enter=listen, outputs=[appNotifyListen]) 315 | closed.upon(appActiveOpen, enter=synSent, outputs=[sendSyn, 316 | expectAck]) 317 | 318 | synSent.upon(timeout, enter=closed, 319 | outputs=[appNotifyAttemptFailed, releaseResources]) 320 | synSent.upon(appClose, enter=closed, 321 | outputs=[appNotifyAttemptFailed, releaseResources]) 322 | synSent.upon(synAck, enter=established, 323 | outputs=[sendAck, appNotifyConnected]) 324 | 325 | synRcvd.upon(ack, enter=established, 326 | outputs=[appNotifyConnected]) 327 | synRcvd.upon(appClose, enter=finWait1, 328 | outputs=[sendFin, expectAck]) 329 | synRcvd.upon(timeout, enter=closed, 330 | outputs=[sendRst, releaseResources]) 331 | synRcvd.upon(rst, enter=broken, 332 | outputs=[releaseResources]) 333 | 334 | listen.upon(appSendData, enter=synSent, 335 | outputs=[sendSyn, expectAck]) 336 | listen.upon(syn, enter=synRcvd, 337 | outputs=[sendSynAck, expectAck]) 338 | 339 | established.upon(appClose, enter=finWait1, 340 | outputs=[appNotifyDisconnected, 341 | sendFin, 342 | expectAck]) 343 | established.upon(fin, enter=closeWait, 344 | outputs=[appNotifyHalfClose, 345 | sendAck]) 346 | established.upon(timeout, enter=broken, outputs=[appNotifyDisconnected, 347 | releaseResources]) 348 | 349 | established.upon(segmentReceived, enter=established, 350 | outputs=[sendAckSoon]) 351 | 352 | 353 | closeWait.upon(appClose, enter=lastAck, 354 | outputs=[sendFin, 355 | expectAck, 356 | appNotifyDisconnected]) 357 | closeWait.upon(timeout, enter=broken, 358 | outputs=[appNotifyDisconnected, 359 | releaseResources]) 360 | 361 | lastAck.upon(ack, enter=closed, outputs=[releaseResources]) 362 | lastAck.upon(timeout, enter=broken, outputs=[releaseResources]) 363 | 364 | # TODO: is this actually just "ack" or is it ack _of_ something in 365 | # particular? ack of the fin we sent upon transitioning to this state? 366 | finWait1.upon(ack, enter=finWait2, outputs=[]) 367 | finWait1.upon(fin, enter=closing, outputs=[sendAck]) 368 | finWait1.upon(timeout, enter=broken, outputs=[releaseResources]) 369 | 370 | finWait2.upon(timeout, enter=broken, outputs=[releaseResources]) 371 | finWait2.upon(fin, enter=timeWait, outputs=[sendAck, startTimeWaiting]) 372 | 373 | closing.upon(timeout, enter=broken, outputs=[releaseResources]) 374 | closing.upon(ack, enter=timeWait, outputs=[startTimeWaiting]) 375 | 376 | timeWait.upon(timeout, enter=closed, outputs=[releaseResources]) 377 | 378 | for noDataState in [finWait1, finWait2, closing]: 379 | noDataState.upon(segmentReceived, enter=noDataState, outputs=[]) 380 | 381 | if __name__ == '__main__': 382 | for line in TCP._machine.graphviz(): 383 | print(line) 384 | -------------------------------------------------------------------------------- /vertex/test/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- test-case-name: vertex.test -*- 2 | # Copyright 2005 Divmod, Inc. See LICENSE file for details 3 | 4 | -------------------------------------------------------------------------------- /vertex/test/_fakes.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Twisted Matrix Laboratories. 2 | # See LICENSE for details. 3 | """ 4 | Verified fakes used across tests. 5 | """ 6 | from pretend import stub, call, call_recorder 7 | from twisted.internet import defer 8 | from twisted.trial import unittest 9 | import txscrypt 10 | 11 | 12 | 13 | def _makeStubTxscrypt(computeKeyReturns, checkPasswordReturns): 14 | """ 15 | Construct a stub L{txscrypt} implementation. 16 | 17 | @param computeKeyReturns: What C{computeKey} should return. 18 | 19 | @param checkPasswordReturns: What C{checkPassword} should return. 20 | 21 | @return: A L{stub} implementation of L{txscrypt} 22 | @rtype: L{stub} 23 | """ 24 | 25 | def computeKey(password): 26 | return computeKeyReturns 27 | 28 | 29 | def checkPassword(key, password): 30 | return checkPasswordReturns 31 | 32 | return stub(computeKey=call_recorder(computeKey), 33 | checkPassword=call_recorder(checkPassword)) 34 | 35 | 36 | 37 | def _makeStubCredentials(username, password, checkPasswordReturns): 38 | """ 39 | Construct a stub L{IUsernamePassword} implementer. 40 | 41 | @param username: The username. 42 | @type username: L{str} 43 | 44 | @param password: The password. 45 | @type password: L{str} 46 | 47 | @param checkPasswordReturns: What C{checkPassword} should return. 48 | 49 | @return: A L{stub} implementation of 50 | L{twisted.python.cred.credentials.UsernamePassword} 51 | @rtype: L{stub} 52 | """ 53 | 54 | def checkPassword(password): 55 | return checkPasswordReturns 56 | 57 | return stub(username=username, 58 | password=password, 59 | checkPassword=call_recorder(checkPassword)) 60 | 61 | 62 | 63 | def _makeStubIQ2QUserStore(storeReturns, keyReturns): 64 | """ 65 | Construct a stub L{vertex.ivertex.IQ2QUserStore} implementer. 66 | 67 | @param storeReturns: What C{store} returns. 68 | 69 | @param keyReturns: What C{key} returns. 70 | 71 | @return: A L{stub} implementation of 72 | L{vertex.ivertex.IQ2QUserStore} 73 | @rtype: L{stub} 74 | """ 75 | 76 | def store(domain, username, password): 77 | return storeReturns 78 | 79 | 80 | def key(domain, username): 81 | return keyReturns 82 | 83 | return stub(store=call_recorder(store), key=call_recorder(key)) 84 | 85 | 86 | 87 | class VerifyStubTxscrypt(unittest.TestCase): 88 | """ 89 | Test that the stub returned by L{_makeStubTxscrypt} behaves the 90 | same as L{txscrypt}. 91 | """ 92 | 93 | def setUp(self): 94 | """ 95 | Setup the test. 96 | """ 97 | self.computeKeyReturns = defer.Deferred() 98 | self.checkPasswordReturns = defer.Deferred() 99 | 100 | self.fakeTxscrypt = _makeStubTxscrypt( 101 | computeKeyReturns=self.computeKeyReturns, 102 | checkPasswordReturns=self.checkPasswordReturns, 103 | ) 104 | 105 | 106 | @defer.inlineCallbacks 107 | def test_computeKey(self): 108 | """ 109 | The stub key computer accepts the same arguments as the real 110 | key computer, both return a L{defer.Deferred} that fires with 111 | the computed key. 112 | """ 113 | password = "password" 114 | realKey = yield txscrypt.computeKey(password) 115 | 116 | self.computeKeyReturns.callback(realKey) 117 | fakeKey = yield self.fakeTxscrypt.computeKey(password) 118 | 119 | self.assertEqual(realKey, fakeKey) 120 | self.assertEqual(self.fakeTxscrypt.computeKey.calls, [call(password)]) 121 | 122 | 123 | @defer.inlineCallbacks 124 | def compareCheckPassword(self, keyPassword, password): 125 | """ 126 | Assert that L{txscrypt.checkPassword} and the stub 127 | C{checkPassword} agree that C{password} matches or does not 128 | match the key derived from C{keyPassword}. 129 | 130 | @param keyPassword: The password from which to derive a key. 131 | @type keyPassword: L{str} 132 | 133 | @param password: The password to check against the derived 134 | key. 135 | @type password: L{str} 136 | 137 | @return: A L{defer.Deferred} that fires when the results have 138 | been compared. 139 | @rtype: L{defer.Deferred} 140 | """ 141 | key = yield txscrypt.computeKey(keyPassword) 142 | 143 | realResult = yield txscrypt.checkPassword(key, password) 144 | 145 | self.checkPasswordReturns.callback(realResult) 146 | fakeResult = yield self.fakeTxscrypt.checkPassword(key, password) 147 | 148 | self.assertEqual(realResult, fakeResult) 149 | self.assertEqual( 150 | self.fakeTxscrypt.checkPassword.calls, 151 | [call(key, password)], 152 | ) 153 | 154 | 155 | def test_checkPasswordMatches(self): 156 | """ 157 | The stub password checker accepts the same arguments as the 158 | real password checker, and both return a L{defer.Deferred} that 159 | fires with L{True} when the the password matches the key. 160 | """ 161 | return self.compareCheckPassword(keyPassword="password", 162 | password="password") 163 | 164 | 165 | def test_checkPassword_doesNotMatch(self): 166 | """ 167 | The stub password checker accepts the same arguments as the 168 | real password checker, and both return a L{defer.Deferred} that 169 | fires with L{False} when the the password does not match the 170 | key. 171 | """ 172 | return self.compareCheckPassword(keyPassword="password", 173 | password="wrong password") 174 | -------------------------------------------------------------------------------- /vertex/test/amphelpers.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Twisted Matrix Laboratories. 2 | # See LICENSE for details. 3 | 4 | """ 5 | Helpers for testing AMP protocols. 6 | """ 7 | 8 | from twisted.protocols import amp 9 | 10 | def callResponder(__locator, __command, **args): 11 | """ 12 | Call an I{AMP} responder on a given locator, 13 | 14 | @param __locator: Locator on which to find responder 15 | @type __locator: L{amp.IResponderLocator} 16 | @param __command: Command to run. 17 | @type __command: L{amp.Command} 18 | """ 19 | box = __command.makeArguments(args, None) 20 | d = __locator.locateResponder(__command.commandName)(box) 21 | d.addCallback(amp._stringsToObjects, __command.response, None) 22 | return d 23 | -------------------------------------------------------------------------------- /vertex/test/helpers.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Twisted Matrix Laboratories. 2 | # See LICENSE for details. 3 | 4 | from twisted.internet import defer 5 | from twisted.python.failure import Failure 6 | 7 | from twisted.test.iosim import connectedServerAndClient, FakeTransport 8 | 9 | 10 | 11 | class FakeDelayedCall: 12 | def __init__(self, fqs, tup): 13 | self.fqs = fqs 14 | self.tup = tup 15 | 16 | 17 | def cancel(self): 18 | self.fqs.calls.remove(self.tup) 19 | 20 | 21 | 22 | class FakeQ2QTransport(FakeTransport): 23 | 24 | def __init__(self, protocol, isServer, q2qhost, q2qpeer): 25 | FakeTransport.__init__(self, protocol, isServer) 26 | self.q2qhost = q2qhost 27 | self.q2qpeer = q2qpeer 28 | 29 | 30 | def getQ2QPeer(self): 31 | return self.q2qpeer 32 | 33 | 34 | def getQ2QHost(self): 35 | return self.q2qhost 36 | 37 | 38 | 39 | class FakeQ2QService: 40 | # XXX TODO: move this into test_q2q and make sure that all the q2q tests 41 | # run with it in order to verify that the test harness is not broken. 42 | 43 | def __init__(self): 44 | self.listeners = {} # Map listening {(q2qid, protocol name):(protocol 45 | # factory, protocol description)} 46 | self.pumps = [] # a list of IOPumps that we have to flush 47 | self.calls = [] 48 | self.time = 0 49 | 50 | 51 | def callLater(self, s, f, *a, **k): 52 | # XXX TODO: return canceller 53 | assert f is not None 54 | tup = (self.time + s, f, a, k) 55 | self.calls.append(tup) 56 | self.calls.sort() 57 | return FakeDelayedCall(self, tup) 58 | 59 | 60 | def flush(self, debug=False): 61 | result = True 62 | while result: 63 | self.time += 1 64 | result = False 65 | for x in range(2): 66 | # Run twice so that timed functions can interact with I/O 67 | for pump in self.pumps: 68 | if pump.flush(debug): 69 | result = True 70 | if debug: 71 | print 'iteration finished. continuing?', result 72 | c = self.calls 73 | self.calls = [] 74 | for s, f, a, k in c: 75 | if debug: 76 | print 'timed event', s, f, a, k 77 | f(*a, **k) 78 | return result 79 | 80 | 81 | def listenQ2Q(self, fromAddress, protocolsToFactories, serverDescription): 82 | for pname, pfact in protocolsToFactories.items(): 83 | self.listeners[fromAddress, pname] = pfact, serverDescription 84 | return defer.succeed(None) 85 | 86 | 87 | def connectQ2Q(self, fromAddress, toAddress, 88 | protocolName, protocolFactory, 89 | chooser=lambda x: x and [x[0]]): 90 | # XXX update this when q2q is updated to return a connector rather than 91 | # a Deferred. 92 | 93 | # XXX this isn't really dealing with the multiple-connectors use case 94 | # now. sigma doesn't need this functionality, but we will need to 95 | # update this class to do it properly before using it to test other Q2Q 96 | # code. 97 | 98 | listener, description = self.listeners.get((toAddress, protocolName)) 99 | if listener is None: 100 | print 'void listener', fromAddress, toAddress, self.listeners, self.listener 101 | reason = Failure(KeyError()) 102 | protocolFactory.clientConnectionFailed(None, reason) 103 | return defer.fail(reason) 104 | else: 105 | def makeFakeClient(c): 106 | ft = FakeQ2QTransport(c, False, fromAddress, toAddress) 107 | return ft 108 | 109 | def makeFakeServer(s): 110 | ft = FakeQ2QTransport(s, True, toAddress, fromAddress) 111 | return ft 112 | 113 | client, server, pump = connectedServerAndClient( 114 | lambda: listener.buildProtocol(fromAddress), 115 | lambda: protocolFactory.buildProtocol(toAddress), 116 | makeFakeClient, 117 | makeFakeServer) 118 | self.pumps.append(pump) 119 | 120 | return defer.succeed(client) 121 | -------------------------------------------------------------------------------- /vertex/test/test_bits.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Twisted Matrix Laboratories. 2 | # See LICENSE for details. 3 | 4 | import array 5 | from vertex.bits import BitArray 6 | 7 | from twisted.trial import unittest 8 | 9 | bitResult = [ 10 | 0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4, 1, 2, 2, 3, 2, 3, 3, 4, 2, 11 | 3, 3, 4, 3, 4, 4, 5, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2, 3, 12 | 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 13 | 4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 2, 3, 3, 4, 14 | 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 15 | 6, 6, 7, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4, 16 | 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 17 | 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 2, 3, 3, 4, 3, 4, 4, 5, 18 | 3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 3, 19 | 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 4, 5, 5, 6, 5, 6, 6, 7, 5, 6, 20 | 6, 7, 6, 7, 7, 8] 21 | 22 | 23 | 24 | class BitArrayTests(unittest.TestCase): 25 | 26 | def test_BasicBits(self): 27 | prev = BitArray(size=3) 28 | prev[0] = 1 29 | prev[1] = 1 30 | prev[2] = 1 31 | for size in (5, 6, 8, 12, 14, 15): 32 | ba = BitArray(size=size) 33 | ba[0] = 1 34 | ba[2] = 1 35 | ba[-1] = 1 36 | assert ba.countbits() == 3, str(ba.countbits()) 37 | xo = (prev ^ ba) 38 | cb = xo.countbits() 39 | assert cb == 2, cb 40 | prev = ba 41 | 42 | 43 | def test_Positions(self): 44 | SIZE = 25 45 | bitz = BitArray(size=SIZE) 46 | self.assertEquals(list(bitz.positions(0)), range(SIZE)) 47 | self.assertEquals(list(bitz.positions(1)), []) 48 | rs = range(SIZE) 49 | rs.remove(7) 50 | bitz[7] = 1 51 | self.assertEquals(list(bitz.positions(0)), rs) 52 | self.assertEquals(list(bitz.positions(1)), [7]) 53 | 54 | 55 | def test_DefaultBit(self): 56 | a = BitArray(size=100, default=0) 57 | b = BitArray(size=100, default=1) 58 | self.assertEquals(list(a), [0] * 100) 59 | self.assertEquals(list(b), [1] * 100) 60 | 61 | 62 | def test_CalculateOnBits(self): 63 | calc = [] 64 | for x in range(256): 65 | c = 0 66 | a = array.array('B') 67 | a.append(x) 68 | for n in BitArray(a): 69 | c += n 70 | calc.append(c) 71 | self.assertEquals(calc, bitResult) 72 | -------------------------------------------------------------------------------- /vertex/test/test_client.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Twisted Matrix Laboratories. 2 | # See LICENSE for details. 3 | from twisted.trial import unittest 4 | from vertex import q2qclient 5 | import sys 6 | from StringIO import StringIO 7 | 8 | class TimeoutTests(unittest.TestCase): 9 | def test_NoUsage(self): 10 | """ 11 | When the vertex Q2QClientProgram is run without any arguments, it 12 | should print a usage error and exit. 13 | """ 14 | cp = q2qclient.Q2QClientProgram() 15 | 16 | # Smash stdout for the duration of the test. 17 | sys.stdout, realout = StringIO(), sys.stdout 18 | try: 19 | # The act of showing the help will cause a sys.exit(0), catch that 20 | # exception. 21 | self.assertRaises(SystemExit, cp.parseOptions, []) 22 | 23 | # Check that the usage string was (roughly) output. 24 | output = sys.stdout.getvalue() 25 | self.assertIn('Usage:', output) 26 | self.assertIn('Options:', output) 27 | self.assertIn('Commands:', output) 28 | finally: 29 | # Always restore stdout. 30 | sys.stdout = realout 31 | -------------------------------------------------------------------------------- /vertex/test/test_conncache.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Twisted Matrix Laboratories. 2 | # See LICENSE for details. 3 | 4 | """ 5 | Tests for L{vertex.conncache}. 6 | """ 7 | 8 | from twisted.internet.protocol import ClientFactory, Protocol 9 | from twisted.internet.defer import Deferred 10 | from twisted.trial.unittest import TestCase 11 | from twisted.test.proto_helpers import StringTransport 12 | 13 | from vertex import conncache 14 | 15 | 16 | class FakeEndpointTests(object): 17 | """ 18 | Fake vertex endpoint for tesing. 19 | 20 | @ivar factories: factories endpoint has been connected to. 21 | @type factories: L{list} of 22 | L{ClientFactory}s. 23 | """ 24 | 25 | def __init__(self): 26 | self.factories = [] 27 | 28 | 29 | def connect(self, factory): 30 | """ 31 | Record factory used for connection. 32 | 33 | @param factory: factory to connect 34 | """ 35 | self.factories.append(factory) 36 | 37 | 38 | 39 | class DisconnectingTransport(StringTransport): 40 | def loseConnection(self): 41 | self.loseConnectionDeferred = Deferred() 42 | return self.loseConnectionDeferred 43 | 44 | 45 | 46 | class TestConnectionCacheTests(TestCase): 47 | """ 48 | Tests for L{conncache.ConnectionCache}. 49 | 50 | @ivar cache: cache to use for testing. 51 | @type cache: L{conncache.ConnectionCache} 52 | """ 53 | 54 | def setUp(self): 55 | """ 56 | Create a L{conncache.ConnectionCache}, endpoint and protocol to test 57 | against. 58 | """ 59 | self.cache = conncache.ConnectionCache() 60 | self.endpoint = FakeEndpointTests() 61 | self.protocol = Protocol() 62 | 63 | 64 | def getCachedConnection(self): 65 | """ 66 | Ask the L{conncache.ConnectionCache} for a connection to the endpoint 67 | created in L{setUp}. 68 | """ 69 | factory = ClientFactory() 70 | factory.protocol = lambda: self.protocol 71 | return self.cache.connectCached(self.endpoint, factory) 72 | 73 | 74 | def test_connectCached(self): 75 | """ 76 | When called with an endpoint it isn't connected to, 77 | L{conncache.ConnectionCache.connectCache} connects 78 | to that endpoint and returns a deferred that fires 79 | with that protocol. 80 | """ 81 | d = self.getCachedConnection() 82 | 83 | self.assertEqual(len(self.endpoint.factories), 1) 84 | connectedFactory = self.endpoint.factories.pop(0) 85 | connectedProtocol = connectedFactory.buildProtocol(None) 86 | self.assertNoResult(d) 87 | connectedProtocol.makeConnection(object()) 88 | 89 | self.assertEqual(self.successResultOf(d), self.protocol) 90 | 91 | 92 | def test_connectCached_cachedConnection(self): 93 | """ 94 | When called with an endpoint it is connected to, 95 | L{conncache.ConnectionCache.connectCache} returns 96 | a deferred that has been fired with that protocol. 97 | """ 98 | self.getCachedConnection() 99 | 100 | connectedFactory = self.endpoint.factories.pop(0) 101 | connectedProtocol = connectedFactory.buildProtocol(None) 102 | connectedProtocol.makeConnection(object()) 103 | 104 | d = self.getCachedConnection() 105 | 106 | self.assertEqual(len(self.endpoint.factories), 0) 107 | self.assertEqual(self.successResultOf(d), self.protocol) 108 | 109 | 110 | def test_connectCached_inProgressConnection(self): 111 | """ 112 | When called with an endpoint it is connecting to, 113 | L{conncache.ConnectionCache.connectCache} returns 114 | a deferred that fires with that protocol. 115 | """ 116 | self.getCachedConnection() 117 | connectedFactory = self.endpoint.factories.pop(0) 118 | 119 | d = self.getCachedConnection() 120 | self.assertEqual(len(self.endpoint.factories), 0) 121 | self.assertNoResult(d) 122 | 123 | connectedProtocol = connectedFactory.buildProtocol(None) 124 | connectedProtocol.makeConnection(object()) 125 | 126 | self.assertEqual(self.successResultOf(d), self.protocol) 127 | 128 | 129 | def test_shutdown_waitsForConnectionLost(self): 130 | """ 131 | L{conncache.ConnectionCache.shutdown} returns a 132 | deferred that fires after all protocols have been 133 | completely disconnected. 134 | 135 | @see: U{http://mumak.net/stuff/twisted-disconnect.html} 136 | """ 137 | self.getCachedConnection() 138 | 139 | connectedFactory = self.endpoint.factories.pop(0) 140 | connectedProtocol = connectedFactory.buildProtocol(None) 141 | transport = DisconnectingTransport() 142 | connectedProtocol.makeConnection(transport) 143 | 144 | d = self.cache.shutdown() 145 | self.assertNoResult(d) 146 | transport.loseConnectionDeferred.callback(None) 147 | self.assertNoResult(d) 148 | connectedFactory.clientConnectionLost(None, None) 149 | self.successResultOf(d) 150 | -------------------------------------------------------------------------------- /vertex/test/test_dependencyservice.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Twisted Matrix Laboratories. 2 | # See LICENSE for details. 3 | from twisted.trial import unittest 4 | 5 | from vertex import depserv 6 | 7 | 8 | class Serv(depserv.DependencyService): 9 | requiredServices = ['one'] 10 | 11 | def __init__(self, **kw): 12 | self.initialized = [] 13 | depserv.DependencyService.__init__(self, **kw) 14 | 15 | 16 | def setup_ONE(self): 17 | self.initialized.append('ONE') 18 | 19 | 20 | def setup_TWO(self): 21 | self.initialized.append('TWO') 22 | 23 | 24 | def setup_THREE(self): 25 | self.initialized.append('THREE') 26 | 27 | 28 | 29 | class DependencyServiceTests(unittest.TestCase): 30 | 31 | def test_depends(self): 32 | class One(Serv): 33 | def depends_TWO(self): 34 | return ['three'] 35 | 36 | class Two(Serv): 37 | def depends_THREE(self): 38 | return ['two'] 39 | 40 | args = dict(one={}, two={}, three={}) 41 | 42 | one = One(**args) 43 | self.assert_(one.initialized == ['ONE', 'THREE', 'TWO']) 44 | 45 | two = Two(**args) 46 | self.assert_(two.initialized == ['ONE', 'TWO', 'THREE']) 47 | 48 | 49 | def test_circularDepends(self): 50 | class One(Serv): 51 | def depends_THREE(self): 52 | return ['two'] 53 | def depends_TWO(self): 54 | return ['three'] 55 | try: 56 | One(one={}, two={}, three={}) 57 | except depserv.StartupError: 58 | pass 59 | else: 60 | raise unittest.FailTest, 'circular dependencies did not raise an error' 61 | 62 | 63 | def test_requiredWithDependency(self): 64 | """ 65 | A service is required but has dependencies 66 | """ 67 | 68 | class One(Serv): 69 | def depends_ONE(self): 70 | return ['three'] 71 | try: 72 | One(one={}, two={}, three={}) 73 | except depserv.StartupError: 74 | pass 75 | else: 76 | raise( 77 | unittest.FailTest, 'unsatisfied dependencies did not raise an error' 78 | ) 79 | -------------------------------------------------------------------------------- /vertex/test/test_identity.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Twisted Matrix Laboratories. 2 | # See LICENSE for details. 3 | 4 | """ 5 | Tests for I{AMP} commands related to identity. 6 | """ 7 | 8 | from twisted.trial import unittest 9 | 10 | from twisted.protocols import amp 11 | from twisted.internet.defer import succeed 12 | from twisted.internet.ssl import DN, KeyPair, CertificateRequest 13 | 14 | from vertex.ivertex import IQ2QUser 15 | from vertex.q2q import Q2Q, Q2QAddress, Identify, Sign 16 | 17 | from vertex.test.amphelpers import callResponder 18 | 19 | 20 | def makeCert(cn): 21 | """ 22 | Create a self-signed certificate with the given common name. 23 | 24 | @param cn: Common Name to use in certificate. 25 | @type cn: L{bytes} 26 | 27 | @return: Self-signed certificate. 28 | @rtype: L{Certificate} 29 | """ 30 | sharedDN = DN(CN=cn) 31 | key = KeyPair.generate() 32 | cr = key.certificateRequest(sharedDN) 33 | sscrd = key.signCertificateRequest(sharedDN, cr, lambda dn: True, 1) 34 | return key.newCertificate(sscrd) 35 | 36 | 37 | 38 | def makeCertRequest(cn): 39 | """ 40 | Create a certificate request with the given common name. 41 | 42 | @param cn: Common Name to use in certificate request. 43 | @type cn: L{bytes} 44 | 45 | @return: Certificate request. 46 | @rtype: L{CertificateRequest} 47 | """ 48 | key = KeyPair.generate() 49 | return key.certificateRequest(DN(CN=cn)) 50 | 51 | 52 | 53 | class IdentityTests(unittest.TestCase): 54 | """ 55 | Tests for L{Identify}. 56 | """ 57 | 58 | def test_identify(self): 59 | """ 60 | A presence server responds to Identify messages with the cert 61 | stored for the requested domain. 62 | """ 63 | target = "example.com" 64 | fakeCert = makeCert("fake certificate") 65 | 66 | class FakeStorage(object): 67 | def getPrivateCertificate(cs, subject): 68 | self.assertEqual(subject, target) 69 | return fakeCert 70 | class FakeService(object): 71 | certificateStorage = FakeStorage() 72 | 73 | q = Q2Q() 74 | q.service = FakeService() 75 | 76 | d = callResponder(q, Identify, subject=Q2QAddress(target)) 77 | response = self.successResultOf(d) 78 | self.assertEqual(response, {'certificate': fakeCert}) 79 | self.assertFalse(hasattr(response['certificate'], 'privateKey')) 80 | 81 | 82 | 83 | class SignTests(unittest.TestCase): 84 | """ 85 | Tests for L{Sign}. 86 | """ 87 | 88 | def test_cannotSign(self): 89 | """ 90 | Vertex nodes with no portal will not sign cert requests. 91 | """ 92 | cr = CertificateRequest.load(makeCertRequest("example.com")) 93 | class FakeService(object): 94 | portal = None 95 | 96 | q = Q2Q() 97 | q.service = FakeService() 98 | 99 | d = callResponder(q, Sign, 100 | certificate_request=cr, 101 | password='hunter2') 102 | self.failureResultOf(d, amp.RemoteAmpError) 103 | 104 | 105 | def test_sign(self): 106 | """ 107 | 'Sign' messages with a cert request result in a cred login with 108 | the given password. The avatar returned is then asked to sign 109 | the cert request with the presence server's certificate. The 110 | resulting certificate is returned as a response. 111 | """ 112 | user = 'jethro@example.com' 113 | passwd = 'hunter2' 114 | 115 | issuerName = "fake certificate" 116 | domainCert = makeCert(issuerName) 117 | 118 | class FakeAvatar(object): 119 | def signCertificateRequest(fa, certificateRequest, hostcert, 120 | suggestedSerial): 121 | self.assertEqual(hostcert, domainCert) 122 | return hostcert.signRequestObject(certificateRequest, 123 | suggestedSerial) 124 | 125 | class FakeStorage(object): 126 | def getPrivateCertificate(cs, subject): 127 | return domainCert 128 | 129 | def genSerial(cs, domain): 130 | return 1 131 | 132 | cr = CertificateRequest.load(makeCertRequest(user)) 133 | class FakePortal(object): 134 | def login(fp, creds, proto, iface): 135 | self.assertEqual(iface, IQ2QUser) 136 | self.assertEqual(creds.username, user) 137 | self.assertEqual(creds.password, passwd) 138 | return succeed([None, FakeAvatar(), None]) 139 | 140 | class FakeService(object): 141 | portal = FakePortal() 142 | certificateStorage = FakeStorage() 143 | 144 | q = Q2Q() 145 | q.service = FakeService() 146 | 147 | d = callResponder(q, Sign, 148 | certificate_request=cr, 149 | password=passwd) 150 | response = self.successResultOf(d) 151 | self.assertEqual(response['certificate'].getIssuer().commonName, 152 | issuerName) 153 | -------------------------------------------------------------------------------- /vertex/test/test_ptcp.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Twisted Matrix Laboratories. 2 | # See LICENSE for details. 3 | # -*- test-case-name: vertex.test.test_ptcp -*- 4 | from __future__ import print_function 5 | 6 | import random, os 7 | 8 | from twisted.internet import reactor, protocol, defer, error 9 | from twisted.trial import unittest 10 | 11 | from vertex import ptcp 12 | 13 | def reallyLossy(method): 14 | r = random.Random() 15 | r.seed(42) 16 | def worseMethod(*a, **kw): 17 | if r.choice([True, True, False]): 18 | method(*a, **kw) 19 | return worseMethod 20 | 21 | 22 | 23 | def insufficientTransmitter(method, mtu): 24 | def worseMethod(bytes, addr): 25 | method(bytes[:mtu], addr) 26 | return worseMethod 27 | 28 | 29 | 30 | class TestProtocol(protocol.Protocol): 31 | buffer = None 32 | def __init__(self): 33 | self.onConnect = defer.Deferred() 34 | self.onDisconn = defer.Deferred() 35 | self._waiting = None 36 | self.buffer = [] 37 | 38 | 39 | def connectionMade(self): 40 | self.onConnect.callback(None) 41 | 42 | 43 | def connectionLost(self, reason): 44 | self.onDisconn.callback(None) 45 | 46 | 47 | def gotBytes(self, bytes): 48 | assert self._waiting is None 49 | if ''.join(self.buffer) == bytes: 50 | return defer.succeed(None) 51 | self._waiting = (defer.Deferred(), bytes) 52 | return self._waiting[0] 53 | 54 | 55 | def dataReceived(self, bytes): 56 | self.buffer.append(bytes) 57 | if self._waiting is not None: 58 | bytes = ''.join(self.buffer) 59 | if not self._waiting[1].startswith(bytes): 60 | x = len(os.path.commonprefix([bytes, self._waiting[1]])) 61 | print(x) 62 | print('it goes wrong starting with', repr(bytes[x:x+100]), repr(self._waiting[1][x:x+100])) 63 | if bytes == self._waiting[1]: 64 | self._waiting[0].callback(None) 65 | self._waiting = None 66 | 67 | 68 | 69 | class Django(protocol.ClientFactory): 70 | def __init__(self): 71 | self.onConnect = defer.Deferred() 72 | 73 | 74 | def buildProtocol(self, addr): 75 | p = protocol.ClientFactory.buildProtocol(self, addr) 76 | self.onConnect.callback(p) 77 | return p 78 | 79 | 80 | def clientConnectionFailed(self, conn, err): 81 | self.onConnect.errback(err) 82 | 83 | 84 | 85 | class ConnectedPTCPMixin: 86 | serverPort = None 87 | 88 | def setUpForATest(self, 89 | ServerProtocol=TestProtocol, ClientProtocol=TestProtocol): 90 | serverProto = ServerProtocol() 91 | clientProto = ClientProtocol() 92 | 93 | self.serverProto = serverProto 94 | self.clientProto = clientProto 95 | 96 | sf = protocol.ServerFactory() 97 | sf.protocol = lambda: serverProto 98 | 99 | cf = Django() 100 | cf.protocol = lambda: clientProto 101 | 102 | serverTransport = ptcp.PTCP(sf) 103 | clientTransport = ptcp.PTCP(None) 104 | 105 | self.serverTransport = serverTransport 106 | self.clientTransport = clientTransport 107 | 108 | serverPort = reactor.listenUDP(0, serverTransport) 109 | clientPort = reactor.listenUDP(0, clientTransport) 110 | 111 | self.clientPort = clientPort 112 | self.serverPort = serverPort 113 | 114 | return ( 115 | serverProto, clientProto, 116 | sf, cf, 117 | serverTransport, clientTransport, 118 | serverPort, clientPort 119 | ) 120 | 121 | 122 | def tearDown(self): 123 | td = [] 124 | 125 | for ptcpTransport in (self.serverTransport, self.clientTransport): 126 | td.append(ptcpTransport.waitForAllConnectionsToClose()) 127 | d = defer.DeferredList(td) 128 | return d 129 | 130 | 131 | 132 | class TestProducerProtocol(protocol.Protocol): 133 | NUM_WRITES = 32 134 | WRITE_SIZE = 32 135 | 136 | def __init__(self): 137 | self.onConnect = defer.Deferred() 138 | self.onPaused = defer.Deferred() 139 | 140 | 141 | def connectionMade(self): 142 | self.onConnect.callback(None) 143 | self.count = -1 144 | self.transport.registerProducer(self, False) 145 | 146 | 147 | def pauseProducing(self): 148 | if self.onPaused is not None: 149 | self.onPaused.callback(None) 150 | self.onPaused = None 151 | 152 | 153 | def resumeProducing(self): 154 | self.count += 1 155 | if self.count < self.NUM_WRITES: 156 | bytes = chr(self.count) * self.WRITE_SIZE 157 | # print 'Issuing a write', len(bytes) 158 | self.transport.write(bytes) 159 | if self.count == self.NUM_WRITES - 1: 160 | # Last time through, intentionally drop the connection before 161 | # the buffer is empty to ensure we handle this case properly. 162 | self.transport.loseConnection() 163 | else: 164 | self.transport.unregisterProducer() 165 | 166 | 167 | 168 | class PTCPTransportTests(ConnectedPTCPMixin, unittest.TestCase): 169 | def setUp(self): 170 | """ 171 | I have no idea why one of these values is divided by 10 and the 172 | other is multiplied by 10. -exarkun 173 | """ 174 | self.patch( 175 | ptcp.PTCPConnection, '_retransmitTimeout', 176 | ptcp.PTCPConnection._retransmitTimeout / 10) 177 | self.patch( 178 | ptcp.PTCPPacket, 'retransmitCount', 179 | ptcp.PTCPPacket.retransmitCount * 10) 180 | 181 | 182 | def xtestWhoAmI(self): 183 | (serverProto, clientProto, 184 | sf, cf, 185 | serverTransport, clientTransport, 186 | serverPort, clientPort) = self.setUpForATest() 187 | 188 | def gotAddress(results): 189 | (serverSuccess, serverAddress), (clientSuccess, clientAddress) = results 190 | self.failUnless(serverSuccess) 191 | self.failUnless(clientSuccess) 192 | 193 | self.assertEquals(serverAddress[1], serverPort.getHost().port) 194 | self.assertEquals(clientAddress[1], clientPort.getHost().port) 195 | 196 | def connectionsMade(ignored): 197 | return defer.DeferredList([serverProto.transport.whoami(), clientProto.transport.whoami()]).addCallback(gotAddress) 198 | 199 | clientTransport.connect(cf, '127.0.0.1', serverPort.getHost().port) 200 | 201 | return defer.DeferredList([serverProto.onConnect, clientProto.onConnect]).addCallback(connectionsMade) 202 | 203 | 204 | def test_VerySimpleConnection(self): 205 | (serverProto, clientProto, 206 | sf, cf, 207 | serverTransport, clientTransport, 208 | serverPort, clientPort) = self.setUpForATest() 209 | 210 | clientTransport.connect(cf, '127.0.0.1', serverPort.getHost().port) 211 | 212 | def sendSomeBytes(ignored, n=10, server=False): 213 | if n: 214 | bytes = 'not a lot of bytes' * 1000 215 | if server: 216 | serverProto.transport.write(bytes) 217 | else: 218 | clientProto.transport.write(bytes) 219 | if server: 220 | clientProto.buffer = [] 221 | d = clientProto.gotBytes(bytes) 222 | else: 223 | serverProto.buffer = [] 224 | d = serverProto.gotBytes(bytes) 225 | return d.addCallback(sendSomeBytes, n - 1, not server) 226 | 227 | def loseConnections(ignored): 228 | serverProto.transport.loseConnection() 229 | clientProto.transport.loseConnection() 230 | return defer.DeferredList([ 231 | serverProto.onDisconn, 232 | clientProto.onDisconn 233 | ]) 234 | 235 | dl = defer.DeferredList([serverProto.onConnect, clientProto.onConnect]) 236 | dl.addCallback(sendSomeBytes) 237 | dl.addCallback(loseConnections) 238 | return dl 239 | 240 | 241 | def test_ProducerConsumer(self): 242 | (serverProto, clientProto, 243 | sf, cf, 244 | serverTransport, clientTransport, 245 | serverPort, clientPort) = self.setUpForATest( 246 | ServerProtocol=TestProducerProtocol) 247 | 248 | def disconnected(ignored): 249 | self.assertEquals( 250 | ''.join(clientProto.buffer), 251 | ''.join([chr(n) * serverProto.WRITE_SIZE 252 | for n in range(serverProto.NUM_WRITES)])) 253 | 254 | clientTransport.connect(cf, '127.0.0.1', serverPort.getHost().port) 255 | return clientProto.onDisconn.addCallback(disconnected) 256 | 257 | 258 | def test_TransportProducer(self): 259 | (serverProto, clientProto, 260 | sf, cf, 261 | serverTransport, clientTransport, 262 | serverPort, clientPort) = self.setUpForATest() 263 | 264 | resumed = [] 265 | def resumeProducing(): 266 | resumed.append(True) 267 | clientProto.transport.resumeProducing() 268 | 269 | def cbBytes(ignored): 270 | self.failUnless(resumed) 271 | clientProto.transport.loseConnection() 272 | 273 | def cbConnect(ignored): 274 | BYTES = 'Here are bytes' 275 | clientProto.transport.pauseProducing() 276 | serverProto.transport.write(BYTES) 277 | reactor.callLater(2, resumeProducing) 278 | return clientProto.gotBytes(BYTES).addCallback(cbBytes) 279 | 280 | clientTransport.connect(cf, '127.0.0.1', serverPort.getHost().port) 281 | connD = defer.DeferredList([clientProto.onConnect, serverProto.onConnect]) 282 | connD.addCallback(cbConnect) 283 | return connD 284 | 285 | 286 | def test_TransportProducerProtocolProducer(self): 287 | (serverProto, clientProto, 288 | sf, cf, 289 | serverTransport, clientTransport, 290 | serverPort, clientPort) = self.setUpForATest( 291 | ServerProtocol=TestProducerProtocol) 292 | 293 | paused = [] 294 | def cbPaused(ignored): 295 | # print 'Paused' 296 | paused.append(True) 297 | # print 'RESUMING', clientProto, clientTransport, clientPort 298 | clientProto.transport.resumeProducing() 299 | serverProto.onPaused.addCallback(cbPaused) 300 | 301 | def cbBytes(ignored): 302 | # print 'Disconnected' 303 | self.assertEquals( 304 | ''.join(clientProto.buffer), 305 | ''.join([chr(n) * serverProto.WRITE_SIZE 306 | for n in range(serverProto.NUM_WRITES)])) 307 | 308 | def cbConnect(ignored): 309 | # The server must write enough to completely fill the outgoing buffer, 310 | # since our peer isn't ACKing /anything/ and our server waits for 311 | # writes to be acked before proceeding. 312 | serverProto.WRITE_SIZE = serverProto.transport.sendWindow * 5 313 | 314 | # print 'Connected' 315 | # print 'PAUSING CLIENT PROTO', clientProto, clientTransport, clientPort 316 | clientProto.transport.pauseProducing() 317 | return clientProto.onDisconn.addCallback(cbBytes) 318 | 319 | clientTransport.connect(cf, '127.0.0.1', serverPort.getHost().port) 320 | connD = defer.DeferredList([clientProto.onConnect, serverProto.onConnect]) 321 | connD.addCallback(cbConnect) 322 | return connD 323 | 324 | 325 | 326 | class LossyTransportTestCase(PTCPTransportTests): 327 | def setUpForATest(self, *a, **kw): 328 | results = PTCPTransportTests.setUpForATest(self, *a, **kw) 329 | results[-2].write = reallyLossy(results[-2].write) 330 | results[-1].write = reallyLossy(results[-1].write) 331 | return results 332 | 333 | 334 | 335 | class SmallMTUTransportTestCase(PTCPTransportTests): 336 | def setUpForATest(self, *a, **kw): 337 | results = PTCPTransportTests.setUpForATest(self, *a, **kw) 338 | results[-2].write = insufficientTransmitter(results[-2].write, 128) 339 | results[-1].write = insufficientTransmitter(results[-1].write, 128) 340 | return results 341 | 342 | 343 | 344 | class TimeoutTests(ConnectedPTCPMixin, unittest.TestCase): 345 | def setUp(self): 346 | """ 347 | Shorten the retransmit timeout so that tests finish more quickly. 348 | """ 349 | self.patch( 350 | ptcp.PTCPConnection, '_retransmitTimeout', 351 | ptcp.PTCPConnection._retransmitTimeout / 10) 352 | 353 | 354 | def test_ConnectTimeout(self): 355 | (serverProto, clientProto, 356 | sf, cf, 357 | serverTransport, clientTransport, 358 | serverPort, clientPort) = self.setUpForATest() 359 | 360 | clientTransport.sendPacket = lambda *a, **kw: None 361 | clientTransport.connect(cf, '127.0.0.1', serverPort.getHost().port) 362 | return cf.onConnect.addBoth( 363 | lambda result: result.trap(error.TimeoutError) and None 364 | ) 365 | 366 | 367 | def test_DataTimeout(self): 368 | (serverProto, clientProto, 369 | sf, cf, 370 | serverTransport, clientTransport, 371 | serverPort, clientPort) = self.setUpForATest() 372 | 373 | def cbConnected(ignored): 374 | serverProto.transport.ptcp.sendPacket = lambda *a, **kw: None 375 | clientProto.transport.write('Receive this data.') 376 | serverProto.transport.write('Send this data.') 377 | # Have to send data or the server will never time out: 378 | # need a SO_KEEPALIVE option somewhere 379 | return clientProto.onDisconn 380 | 381 | clientTransport.connect(cf, '127.0.0.1', serverPort.getHost().port) 382 | 383 | d = defer.DeferredList([serverProto.onConnect, clientProto.onConnect]) 384 | d.addCallback(cbConnected) 385 | return d 386 | -------------------------------------------------------------------------------- /vertex/test/test_q2qclient.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Twisted Matrix Laboratories. 2 | # See LICENSE for details. 3 | # -*- vertex.test.test_q2q.UDPConnection -*- 4 | 5 | """ 6 | Tests for L{vertex.q2qclient}. 7 | """ 8 | 9 | from twisted.trial import unittest 10 | from twisted.internet.protocol import Factory 11 | 12 | from twisted.protocols.amp import AMP, AmpBox 13 | 14 | from vertex import q2q, q2qclient 15 | 16 | from vertex.test.helpers import FakeQ2QService 17 | 18 | 19 | # I'm naming this 'TestCaseTests' to please the linter. 20 | # But honestly this ... is not ok hah! 21 | class TestCaseTests(unittest.TestCase): 22 | def test_stuff(self): 23 | svc = FakeQ2QService() 24 | 25 | serverAddr = q2q.Q2QAddress("domain", "accounts") 26 | 27 | server = AMP() 28 | def respond(box): 29 | self.assertEqual(box['_command'], "add_user") 30 | self.assertEqual(box['name'], "user") 31 | self.assertEqual(box['password'], "password") 32 | return AmpBox() 33 | server.amp_ADD_USER = respond 34 | factory = Factory.forProtocol(lambda: server) 35 | chooser = {"identity-admin": factory} 36 | 37 | svc.listenQ2Q(serverAddr, chooser, "Admin") 38 | 39 | d = q2qclient.enregister( 40 | svc, q2q.Q2QAddress("domain", "user"), "password" 41 | ) 42 | svc.flush() 43 | 44 | self.successResultOf(d) 45 | -------------------------------------------------------------------------------- /vertex/test/test_sigma.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Twisted Matrix Laboratories. 2 | # See LICENSE for details. 3 | 4 | from twisted.python.failure import Failure 5 | from twisted.python.filepath import FilePath 6 | from twisted.internet.error import ConnectionDone 7 | 8 | from twisted.trial import unittest 9 | 10 | from vertex.q2q import Q2QAddress 11 | from vertex import sigma, conncache 12 | 13 | from vertex.test.mock_data import data as TEST_DATA 14 | from vertex.test.test_conncache import DisconnectingTransport 15 | 16 | from vertex.test.helpers import FakeQ2QService 17 | 18 | sender = Q2QAddress("sending-data.net", "sender") 19 | receiver = Q2QAddress("receiving-data.org", "receiver") 20 | 21 | class TestBase(unittest.TestCase): 22 | def setUp(self): 23 | self.realChunkSize = sigma.CHUNK_SIZE 24 | sigma.CHUNK_SIZE = 100 25 | svc = self.service = FakeQ2QService() 26 | fname = self.mktemp() 27 | 28 | sf = self.sfile = FilePath(fname) 29 | if not sf.parent().isdir(): 30 | sf.parent().makedirs() 31 | sf.open('w').write(TEST_DATA) 32 | self.senderNexus = sigma.Nexus(svc, sender, 33 | sigma.BaseNexusUI(self.mktemp()), 34 | svc.callLater) 35 | 36 | 37 | def tearDown(self): 38 | self.senderNexus.stopService() 39 | sigma.CHUNK_SIZE = self.realChunkSize 40 | 41 | 42 | 43 | class BasicTransferTests(TestBase): 44 | def setUp(self): 45 | TestBase.setUp(self) 46 | self.stoppers = [] 47 | self.receiverNexus = sigma.Nexus(self.service, receiver, 48 | sigma.BaseNexusUI(self.mktemp()), 49 | self.service.callLater) 50 | self.stoppers.append(self.receiverNexus) 51 | 52 | 53 | def tearDown(self): 54 | TestBase.tearDown(self) 55 | for stopper in self.stoppers: 56 | stopper.stopService() 57 | 58 | 59 | def test_OneSenderOneRecipient(self): 60 | self.senderNexus.push(self.sfile, 'TESTtoTEST', [receiver]) 61 | self.service.flush() 62 | peerThingyoes = childrenOf(self.receiverNexus.ui.basepath) 63 | self.assertEquals(len(peerThingyoes), 1) 64 | rfiles = childrenOf(peerThingyoes[0]) 65 | self.assertEquals(len(rfiles), 1) 66 | rfile = rfiles[0] 67 | rfdata = rfile.open().read() 68 | self.assertEquals(len(rfdata), 69 | len(TEST_DATA)) 70 | self.assertEquals(rfdata, TEST_DATA, 71 | "file values unequal") 72 | 73 | 74 | def test_OneSenderManyRecipients(self): 75 | raddresses = [Q2QAddress("receiving-data.org", "receiver%d" % (x,)) 76 | for x in range(10)] 77 | 78 | nexi = [sigma.Nexus(self.service, 79 | radr, 80 | sigma.BaseNexusUI(self.mktemp()), 81 | self.service.callLater) for radr in raddresses] 82 | 83 | self.stoppers.extend(nexi) 84 | 85 | self.senderNexus.push(self.sfile, 'TESTtoTEST', raddresses) 86 | self.service.flush() 87 | 88 | receivedIntroductions = 0 89 | 90 | for nexium in nexi: 91 | receivedIntroductions += nexium.ui.receivedIntroductions 92 | self.failUnless(receivedIntroductions > 1) 93 | 94 | for nexium in nexi: 95 | peerFiles = childrenOf(nexium.ui.basepath) 96 | self.assertEquals(len(peerFiles), 1) 97 | rfiles = childrenOf(peerFiles[0]) 98 | self.assertEquals(len(rfiles), 1, rfiles) 99 | rfile = rfiles[0] 100 | self.assertEquals(rfile.open().read(), 101 | TEST_DATA, 102 | "file value mismatch") 103 | 104 | 105 | 106 | class SigmaConnectionCacheTests(unittest.TestCase): 107 | """ 108 | Tests for the interaction of L{sigma.SigmaProtocol} and 109 | L{conncache.ConnectionCache}. 110 | """ 111 | 112 | def test_connectionLost_unregistersFromConnectionCache(self): 113 | """ 114 | L{sigma.SigmaProtocol.connectionLost} notifies the connection 115 | cache that the connection is lost. 116 | """ 117 | cache = conncache.ConnectionCache() 118 | 119 | class FakeNexus(object): 120 | conns = cache 121 | addr = object() 122 | svc = object() 123 | 124 | protocol = sigma.SigmaProtocol(FakeNexus()) 125 | transport = DisconnectingTransport() 126 | q2qPeer = object() 127 | transport.getQ2QPeer = lambda: q2qPeer 128 | 129 | protocol.makeConnection(transport) 130 | d = cache.shutdown() 131 | transport.loseConnectionDeferred.callback(None) 132 | self.assertNoResult(d) 133 | protocol.connectionLost(Failure(ConnectionDone)) 134 | self.successResultOf(d) 135 | 136 | 137 | 138 | def childrenOf(x): 139 | # This should be a part of FilePath, but hey 140 | return map(x.child, x.listdir()) 141 | -------------------------------------------------------------------------------- /vertex/test/test_standalone.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Twisted Matrix Laboratories. 2 | # See LICENSE for details. 3 | """ 4 | Tests for L{vertex.q2qstandalone} 5 | """ 6 | 7 | from pretend import call_recorder, call, stub 8 | 9 | from twisted.internet import defer 10 | from twisted.python.filepath import FilePath 11 | from twisted.protocols.amp import AMP 12 | from twisted.test.iosim import connect, makeFakeClient, makeFakeServer 13 | from twisted.trial.unittest import TestCase, SynchronousTestCase 14 | 15 | from vertex.q2q import Q2QAddress 16 | from vertex.q2qadmin import AddUser, NotAllowed 17 | from vertex.q2qstandalone import IdentityAdmin 18 | from vertex.q2qstandalone import _UserStore 19 | from vertex import ivertex 20 | 21 | from zope.interface.verify import verifyObject 22 | 23 | from ._fakes import _makeStubTxscrypt 24 | 25 | 26 | class AddUserAdminTests(TestCase): 27 | """ 28 | Tests that IdentityAdmin can successfully add a user 29 | """ 30 | def setUp(self): 31 | self.addUser = call_recorder( 32 | lambda *args, **kwargs: defer.succeed("ignored") 33 | ) 34 | store = stub(addUser=self.addUser) 35 | self.adminFactory = stub(store=store) 36 | 37 | 38 | def test_IdentityAdmin_responder_adds_user(self): 39 | """ 40 | L{IdentityAdmin} has a L{AddUser} responder. 41 | """ 42 | responder = IdentityAdmin().locateResponder(AddUser.commandName) 43 | self.assertIsNotNone(responder) 44 | 45 | 46 | def test_adds_user(self): 47 | """ 48 | When L{UserAdder} is connected to L{IdentityAdmin}, the L{AddUser} 49 | command is called and L{IdentityAdmin} adds the user to its factory's 50 | store. 51 | """ 52 | admin = IdentityAdmin() 53 | admin.factory = self.adminFactory 54 | 55 | serverTransport = makeFakeServer(admin) 56 | serverTransport.getQ2QHost = lambda: Q2QAddress('Q2Q Host') 57 | 58 | client = AMP() 59 | pump = connect(admin, serverTransport, client, makeFakeClient(client)) 60 | 61 | d = client.callRemote(AddUser, name='q2q username', 62 | password='q2q password') 63 | pump.flush() 64 | 65 | # The username and password are added, along with the domain=q2q 66 | # host, to the IdentityAdmin's factory's store 67 | self.assertEqual([call('Q2Q Host', 'q2q username', 'q2q password')], 68 | self.addUser.calls) 69 | 70 | # The server responds with {} 71 | self.assertEqual({}, self.successResultOf(d)) 72 | 73 | 74 | 75 | class UserStoreTests(SynchronousTestCase): 76 | """ 77 | Tests for L{_UserStore} 78 | """ 79 | 80 | def setUp(self): 81 | self.userPath = FilePath(self.mktemp()) 82 | self.userPath.makedirs() 83 | self.addCleanup(self.userPath.remove) 84 | self.makeUsers(self.userPath.path) 85 | 86 | 87 | def makeUsers(self, path): 88 | """ 89 | Create a L{_UserStore} instance pointed at C{path}. 90 | 91 | @param path: The path where the instance will store its 92 | per-user files. 93 | @type path: L{str} 94 | """ 95 | 96 | self.computeKeyReturns = defer.Deferred() 97 | 98 | self.fakeTxscrypt = _makeStubTxscrypt( 99 | computeKeyReturns=self.computeKeyReturns, 100 | checkPasswordReturns=defer.Deferred(), 101 | ) 102 | 103 | self.users = _UserStore( 104 | path=path, 105 | keyDeriver=self.fakeTxscrypt, 106 | ) 107 | 108 | 109 | def test_providesIQ2QUserStore(self): 110 | """ 111 | The store provides L{ivertex.IQ2QUserStore} 112 | """ 113 | verifyObject(ivertex.IQ2QUserStore, self.users) 114 | 115 | 116 | def assertStored(self, domain, username, password, key): 117 | """ 118 | Assert that C{password} is stored under C{user} and C{domain}. 119 | 120 | @param domain: The user's 'domain. 121 | @type domain: L{str} 122 | 123 | @param username: The username. 124 | @type username: L{str} 125 | 126 | @param password: The password. 127 | @type password: L{str} 128 | 129 | @param key: The key "derived" from C{password} 130 | @type key: L{str} 131 | """ 132 | storedDeferred = self.users.store(domain, username, password) 133 | 134 | self.assertNoResult(storedDeferred) 135 | self.computeKeyReturns.callback(key) 136 | self.assertEqual(self.successResultOf(storedDeferred), 137 | (domain, username)) 138 | 139 | 140 | def test_storeAndRetrieveKey(self): 141 | """ 142 | A key is derived for a password and stored under the domain 143 | and user. 144 | """ 145 | domain, username, password, key = "domain", "user", "password", "key" 146 | 147 | self.assertStored(domain, username, password, key) 148 | self.assertEqual(self.users.key(domain, username), key) 149 | 150 | 151 | def test_missingKey(self): 152 | """ 153 | The derived key for an unknown domain and user combination is 154 | L{None}. 155 | """ 156 | self.assertIsNone(self.users.key("mystery domain", "mystery user")) 157 | 158 | 159 | def test_storeExistingUser(self): 160 | """ 161 | Attempting to overwrite an existing user fails with 162 | L{NotAllowed} 163 | """ 164 | domain, username, password, key = "domain", "user", "password", "key" 165 | 166 | self.assertStored(domain, username, password, key) 167 | 168 | self.makeUsers(self.userPath.path) 169 | 170 | failure = self.failureResultOf(self.users.store(domain, 171 | username, 172 | password)) 173 | self.assertIsInstance(failure.value, NotAllowed) 174 | -------------------------------------------------------------------------------- /vertex/test/test_subproducer.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Twisted Matrix Laboratories. 2 | # See LICENSE for details. 3 | 4 | """ 5 | Tests for L{vertex.subproducer}. 6 | """ 7 | from twisted.trial import unittest 8 | 9 | from vertex.subproducer import SuperProducer, SubProducer 10 | 11 | class TestSuper(SuperProducer): 12 | transport = property(lambda self: self) 13 | 14 | def registerProducer(self, producer, streaming): 15 | self.producer = producer 16 | self.streamingProducer = streaming 17 | 18 | 19 | def unregisterProducer(self): 20 | self.producer = None 21 | 22 | 23 | 24 | class TestProducer: 25 | def __init__(self): 26 | self.calls = [] 27 | 28 | 29 | def resumeProducing(self): 30 | self.calls.append('resume') 31 | 32 | 33 | def pauseProducing(self): 34 | self.calls.append('pause') 35 | 36 | 37 | def stopProducing(self): 38 | self.calls.append('stop') 39 | 40 | 41 | def clear(self): 42 | del self.calls[:] 43 | 44 | 45 | 46 | class SuperProducerTests(unittest.TestCase): 47 | 48 | def test_BasicNotification(self): 49 | sup = TestSuper() 50 | sub = SubProducer(sup) 51 | 52 | tp1 = TestProducer() 53 | sub.registerProducer(tp1, False) 54 | self.assertEquals(tp1.calls, ['resume']) 55 | sub.unregisterProducer() 56 | 57 | tp2 = TestProducer() 58 | sub.registerProducer(tp2, True) 59 | self.assertEquals(tp2.calls, []) 60 | sub.unregisterProducer() 61 | 62 | 63 | def test_PauseSuperBeforeRegister(self): 64 | sup = TestSuper() 65 | sub1 = SubProducer(sup) 66 | sub2 = SubProducer(sup) 67 | 68 | tp1 = TestProducer() 69 | tp2 = TestProducer() 70 | 71 | sub1.registerProducer(tp1, False) 72 | sub2.registerProducer(tp2, False) 73 | 74 | self.assertEquals(sup.producer, sup) 75 | # Make sure it's registered with itself; IOW it has called 76 | # self.transport.registerProducer(self). 77 | 78 | sup.pauseProducing() 79 | sup.resumeProducing() 80 | 81 | self.assertEquals(tp1.calls, ['resume', 'pause', 'resume']) 82 | self.assertEquals(tp2.calls, ['resume', 'pause', 'resume']) 83 | 84 | sup.stopProducing() 85 | self.assertEquals(tp1.calls, ['resume', 'pause', 'resume', 'stop']) 86 | self.assertEquals(tp2.calls, ['resume', 'pause', 'resume', 'stop']) 87 | 88 | 89 | def test_NonStreamingChoke(self): 90 | sup = TestSuper() 91 | sub1 = SubProducer(sup) 92 | sub2 = SubProducer(sup) 93 | 94 | tp1 = TestProducer() 95 | tp2 = TestProducer() 96 | 97 | sub1.registerProducer(tp1, False) 98 | sub2.registerProducer(tp2, False) 99 | 100 | self.assertEquals(tp1.calls, ['resume']) 101 | self.assertEquals(tp2.calls, ['resume']) 102 | 103 | tp1.clear() 104 | tp2.clear() 105 | 106 | self.assertEquals(sup.producer, sup) 107 | 108 | sub1.choke() 109 | self.assertEquals(tp1.calls, ['pause']) 110 | self.assertEquals(tp2.calls, []) 111 | 112 | sup.pauseProducing() 113 | self.assertEquals(tp1.calls, ['pause']) 114 | self.assertEquals(tp2.calls, ['pause']) 115 | 116 | sup.resumeProducing() 117 | self.assertEquals(tp1.calls, ['pause']) 118 | self.assertEquals(tp2.calls, ['pause', 'resume']) 119 | 120 | sup.pauseProducing() 121 | sup.resumeProducing() 122 | self.assertEquals(tp1.calls, ['pause']) 123 | self.assertEquals(tp2.calls, ['pause', 'resume', 'pause', 'resume']) 124 | sub1.unchoke() 125 | 126 | self.assertEquals(tp1.calls, ['pause', 'resume']) 127 | self.assertEquals(tp2.calls, ['pause', 'resume', 'pause', 'resume']) 128 | 129 | sup.pauseProducing() 130 | sub1.choke() 131 | sub1.choke() 132 | sub1.choke() 133 | self.assertEquals(tp1.calls, ['pause', 'resume', 'pause']) 134 | self.assertEquals(tp2.calls, ['pause', 'resume', 'pause', 'resume', 135 | 'pause']) 136 | 137 | sub1.unchoke() 138 | self.assertEquals(tp1.calls, ['pause', 'resume', 'pause']) 139 | self.assertEquals(tp2.calls, ['pause', 'resume', 'pause', 'resume', 140 | 'pause']) 141 | 142 | sup.resumeProducing() 143 | self.assertEquals(tp1.calls, ['pause', 'resume', 'pause', 'resume']) 144 | self.assertEquals(tp2.calls, ['pause', 'resume', 'pause', 'resume', 145 | 'pause', 'resume']) 146 | tp1.clear() 147 | tp2.clear() 148 | sup.stopProducing() 149 | 150 | self.assertEquals(tp1.calls, ['stop']) 151 | self.assertEquals(tp2.calls, ['stop']) 152 | 153 | 154 | def test_StreamingChoke(self): 155 | sup = TestSuper() 156 | sub1 = SubProducer(sup) 157 | sub2 = SubProducer(sup) 158 | 159 | tp1 = TestProducer() 160 | tp2 = TestProducer() 161 | 162 | sub1.registerProducer(tp1, True) 163 | sub2.registerProducer(tp2, True) 164 | 165 | self.assertEquals(tp1.calls, []) 166 | self.assertEquals(tp2.calls, []) 167 | 168 | sub1.choke() 169 | self.assertEquals(tp1.calls, ['pause']) 170 | self.assertEquals(tp2.calls, []) 171 | 172 | sup.pauseProducing() 173 | self.assertEquals(tp1.calls, ['pause']) 174 | self.assertEquals(tp2.calls, ['pause']) 175 | 176 | sup.resumeProducing() 177 | self.assertEquals(tp1.calls, ['pause']) 178 | self.assertEquals(tp2.calls, ['pause', 'resume']) 179 | 180 | sub1.unchoke() 181 | self.assertEquals(tp1.calls, ['pause', 'resume']) 182 | self.assertEquals(tp2.calls, ['pause', 'resume']) 183 | 184 | tp1.clear() 185 | tp2.clear() 186 | sup.stopProducing() 187 | self.assertEquals(tp1.calls, ['stop']) 188 | self.assertEquals(tp2.calls, ['stop']) 189 | --------------------------------------------------------------------------------