[Pkg-privacy-commits] [flashproxy] 01/65: Imported Upstream version 1.5
Ximin Luo
infinity0 at moszumanska.debian.org
Fri Aug 21 13:49:38 UTC 2015
This is an automated email from the git hooks/post-receive script.
infinity0 pushed a commit to branch _volatile-rc
in repository flashproxy.
commit 79f7c891ed97534f843a81f9ea777508fb5e65b9
Author: Ximin Luo <infinity0 at gmx.com>
Date: Thu Dec 12 12:33:32 2013 +0000
Imported Upstream version 1.5
---
.gitattributes | 1 +
.gitignore | 7 +
ChangeLog | 267 +++++
LICENSE | 22 +
Makefile | 100 ++
Makefile.client | 92 ++
README | 102 ++
doc/design.txt | 241 +++++
doc/flashproxy-client.1 | 156 +++
doc/flashproxy-client.1.txt | 106 ++
doc/flashproxy-reg-appspot.1 | 87 ++
doc/flashproxy-reg-appspot.1.txt | 64 ++
doc/flashproxy-reg-email.1 | 106 ++
doc/flashproxy-reg-email.1.txt | 75 ++
doc/flashproxy-reg-http.1 | 78 ++
doc/flashproxy-reg-http.1.txt | 50 +
doc/flashproxy-reg-url.1 | 87 ++
doc/flashproxy-reg-url.1.txt | 60 ++
experiments/README | 48 +
experiments/client-extract.py | 50 +
experiments/client-graph.py | 96 ++
experiments/client-graph.r | 5 +
experiments/common.sh | 59 ++
experiments/exercise/exercise.sh | 34 +
experiments/exercise/flashproxy-exercise.sh | 11 +
experiments/facilitator-graph.py | 92 ++
experiments/proxy-extract.py | 82 ++
experiments/proxy-graph.r | 5 +
experiments/switching/local-http-alternating.sh | 66 ++
experiments/switching/local-http-constant.sh | 64 ++
experiments/switching/proxy-loop.sh | 40 +
experiments/switching/remote-tor-alternating.sh | 69 ++
experiments/switching/remote-tor-constant.sh | 58 ++
experiments/switching/remote-tor-direct.sh | 37 +
experiments/switching/switching-all.sh | 38 +
experiments/switching/torrc.bridge | 5 +
experiments/throughput/httpget.py | 35 +
experiments/throughput/throughput-all.sh | 5 +
experiments/throughput/throughput.sh | 114 +++
facilitator/.gitignore | 29 +
facilitator/HACKING | 26 +
facilitator/INSTALL | 40 +
facilitator/Makefile.am | 166 +++
facilitator/README | 34 +
facilitator/appengine/app.yaml | 10 +
facilitator/appengine/config.go | 16 +
facilitator/appengine/fp-reg.go | 55 +
facilitator/autogen.sh | 2 +
facilitator/configure.ac | 49 +
facilitator/default/facilitator | 11 +
facilitator/default/facilitator-email-poller | 7 +
facilitator/default/facilitator-reg-daemon | 11 +
facilitator/doc/appspot-howto.txt | 72 ++
facilitator/doc/email-howto.txt | 75 ++
facilitator/doc/facilitator-design.txt | 41 +
facilitator/doc/http-howto.txt | 49 +
facilitator/doc/server-howto.txt | 55 +
facilitator/examples/facilitator-relays | 7 +
facilitator/examples/fp-facilitator.conf.in | 30 +
facilitator/examples/reg-email.pass | 10 +
facilitator/facilitator | 531 ++++++++++
facilitator/facilitator-email-poller | 405 ++++++++
facilitator/facilitator-reg | 70 ++
facilitator/facilitator-reg-daemon | 217 ++++
facilitator/facilitator-test.py | 316 ++++++
facilitator/facilitator.cgi | 122 +++
facilitator/init.d/facilitator-email-poller.in | 131 +++
facilitator/init.d/facilitator-reg-daemon.in | 132 +++
facilitator/init.d/facilitator.in | 133 +++
facilitator/mkman.inc | 1 +
facilitator/mkman.sh | 1 +
flashproxy-client | 1245 +++++++++++++++++++++++
flashproxy-client-test.py | 401 ++++++++
flashproxy-reg-appspot | 252 +++++
flashproxy-reg-email | 245 +++++
flashproxy-reg-http | 114 +++
flashproxy-reg-url | 107 ++
flashproxy/__init__.py | 0
flashproxy/fac.py | 218 ++++
flashproxy/keys.py | 86 ++
flashproxy/proc.py | 47 +
flashproxy/reg.py | 31 +
flashproxy/test/__init__.py | 0
flashproxy/test/test_fac.py | 93 ++
flashproxy/test/test_keys.py | 25 +
flashproxy/test/test_reg.py | 23 +
flashproxy/test/test_util.py | 43 +
flashproxy/util.py | 120 +++
mkman.inc | 9 +
mkman.sh | 55 +
proxy/Makefile | 11 +
proxy/README | 13 +
proxy/badge-de.png | Bin 0 -> 282 bytes
proxy/badge-en.png | Bin 0 -> 254 bytes
proxy/badge-export-lang.scm | 21 +
proxy/badge-pt.png | Bin 0 -> 356 bytes
proxy/badge-ru.png | Bin 0 -> 387 bytes
proxy/badge.png | 1 +
proxy/badge.xcf | Bin 0 -> 3690 bytes
proxy/embed.html | 81 ++
proxy/flashproxy-test.js | 359 +++++++
proxy/flashproxy.js | 1129 ++++++++++++++++++++
proxy/modules/drupal/flashproxy-start.js | 1 +
proxy/modules/drupal/flashproxy.info | 5 +
proxy/modules/drupal/flashproxy.js | 1 +
proxy/modules/drupal/flashproxy.module | 2 +
proxy/modules/facebook/facebook.html | 17 +
proxy/modules/mediawiki/custom.js | 9 +
proxy/modules/nodejs/.gitignore | 4 +
proxy/modules/nodejs/.npmignore | 0
proxy/modules/nodejs/INSTALL | 35 +
proxy/modules/nodejs/Makefile | 19 +
proxy/modules/nodejs/README | 5 +
proxy/modules/nodejs/main.js | 59 ++
proxy/modules/nodejs/package.json | 31 +
proxy/options.html | 166 +++
setup-client-exe.py | 20 +
setup-common.py | 47 +
torrc | 13 +
version.sh | 3 +
120 files changed, 10734 insertions(+)
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..fa1385d
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+* -text
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..5672537
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+*.pyc
+
+# built by setup*.py
+/build
+/dist
+/*.egg-info
+/py2exe-tmp
diff --git a/ChangeLog b/ChangeLog
new file mode 100644
index 0000000..2fcc7b5
--- /dev/null
+++ b/ChangeLog
@@ -0,0 +1,267 @@
+Changes in version 1.5
+ o Add manpages for the facilitator and nodejs proxy, automatically
+ generated by help2man.
+
+ o Have nodejs flashproxy take GNU-style long command-line options.
+
+ o Automate much of the configuration tasks involved in installing the
+ facilitator using GNU autotools. See facilitator/INSTALL for details
+ on the new process. Also move some common code here into the common
+ python module. Patch by Ximin Luo. Fixes bug 9974.
+
+ o Move common code to a separate flashproxy-common python module. Also
+ split out some build scripts so distro packagers have an easier
+ time. Patch by Ximin Luo. Fixes bug 6810.
+
+ o Enabled binary frames (avoiding the overhead of base64 encoding) for
+ Firefox 11 and later. Patch by Arlo Breault. Fixes bug 9069.
+
+ o Removed a Python 2.7–dependent reference in flashproxy-reg-appspot.
+
+Changes in version 1.4
+ o Allowed websocket-server to gracefully handle SIGTERM.
+
+ o Makefiles that install now obey DESTDIR to install relative to a
+ different root.
+
+ o Added a new observed Google public key pin for flashproxy-reg-email.
+
+ o New --transport options in the client programs allow you to inform
+ the facilitator that you want to receive connections of a certain
+ kind. Transports other than the default "websocket" are
+ experimental. Patch by George Kadianakis and David Fifield. Part of
+ bug 9349.
+
+ o Proxies now send a list of transport protocols they support
+ (currently only "websocket"). This will allow the facilitator to
+ assign proxies to clients that use matching transports. Patch by
+ George Kadianakis. Part of bug 9349.
+
+ o Allowed the facilitator to handle layered transports. For example, a
+ client that register with the transport "obfs3|websocket" will
+ receive a connection from a proxy using websocket, and will be
+ connected to a relay that has an obfs3 server behind a websocket
+ front end. Patch by Ximin Luo and George Kadianakis. Fixes bug 9349.
+
+ o Changed to use the pluggable transport method name "flashproxy"
+ rather than "websocket". Both names are equivalent and "websocket"
+ continues to work. The reason for this change is to reduce confusion
+ with a transport that simply makes a WebSocket connection to a
+ "websocket" bridge, without receiving an inbound connection from a
+ flash proxy. The default argument to the --transport option
+ continues to be "websocket", because that option controls which
+ particular protocol flash proxies should use to connect to you, and
+ is distinct from the transport method name used by Tor.
+
+ o Rearranged some files in the source tree. Facilitator documentation
+ is now under facilitator/doc. The App Engine source code is under
+ facilitator/appengine. The directory containing other ways to use
+ the proxy moved from modules to proxy/modules. Patch by Ximin Luo.
+ Fixes bug 9668.
+
+Changes in version 1.3
+ o Added a new observed Google public key pin.
+
+Changes in version 1.2
+ o The facilitator daemons have a --privdrop-user option that causes
+ them to change to another user ID after reading keys and opening log
+ files. facilitator-howto.txt shows how to configure them to use an
+ unprivileged facilitator-nobody user. Patch by Alexandre Allaire and
+ David Fifield. Fixes bug 8424.
+
+ o Proxies now send the list of clients they are currently serving in
+ their facilitator polling requests. This is meant to enable the
+ facilitator to estimate the level of service each client is getting.
+ Proxies send a protocol revision number "r=1" to signify the change.
+
+ o The managed transport method name "flashproxy" is now recognized as
+ a synonym for "websocket".
+
+ o The badge localization now understands language subtags such as
+ "ru-RU". Fixes bug 8828.
+
+ o Language tags for badge localization are now case-insensitive.
+ Patch by Eduardo Stalinho. Fixes bug 8829.
+
+ o The badge localization is taken from the JavaScript property
+ window.navigator.language when possible. Patch by Arlo Breault.
+ Fixes bug 8827.
+
+ o Proxies now attempt to connect to the client first, and only connect
+ to the relay after the client connection is successful. This is
+ meant to reduce the number of connections to the relay when clients
+ haven't set up port forwarding. Introduced bug 9009, later fixed.
+
+ o A proxy no longer contacts the facilitator when it is given the
+ "client" and "relay" parameters. It serves the one given client and
+ then stops. Patch by Arlo Breault. Fixes bug 9006.
+
+ o facilitator-email-poller ignores messages received a long time ago.
+ This is to fix the situation where facilitator-email-poller stops
+ running for some reason, comes back after some hours, and then
+ flushes a lot of no-longer-relevant registrations out to proxies.
+ Patch by Sukhbir Singh and David Fifield. Fixes bug 8285.
+
+ o New --port-forwarding and friends options enable flashproxy-client
+ to invoke tor-fw-helper to forward ports automatically. Patch by
+ Arlo Breault and David Fifield. Fixes bug 9033.
+
+ o The flash proxy, in debug mode, now hides potentially sensistive
+ information like IP addresses. Patch by Arlo Breault. Fixes bug
+ 9170.
+
+ o The new modules/nodejs allows running a standalone flash proxy
+ (outside a browser) under Node.js. Patch by Arlo Breault. Fixes bug
+ 7944.
+
+ o Registration helpers have a new --unsafe-logging option and helpers
+ don't log IP addresses by default. Patch by Arlo Breault. Fixes bug
+ 9185.
+
+ o Certificate pins now match against the public keys of intermediate
+ certificates, not only those of leaves. This will help with
+ flashproxy-reg-appspot, whose leaf key was often changing. It also
+ allows us to copy pin digests directly from the Chromium source
+ code. Patch by David Fifield. Fixes bug 9167.
+
+Changes in version 1.1
+ o Programs that use certificate pins now take a --disable-pin option
+ that causes pins to be ignored.
+
+Changes in version 1.0
+ o The facilitator runs on a new domain name fp-facilitator.org. Fixes
+ bug 7160.
+
+ o Fixed badge rendering for a certain combination of Chrome and
+ AdBlock Plus. Patch by Arlo Breault. Fixes bug 8300.
+
+ o websocket-server sends the new TRANSPORT command of the extended OR
+ port protocol to identify incoming connections as websocket.
+
+ o There is now a 10-second HTTP request timeout in websocket-server.
+ Fixes bug 8626.
+
+ o The new --facilitator-pubkey option of flashproxy-client lets you
+ configure a different facilitator public key, if you're using one
+ other than the one at fp-facilitator.org. Patch by Arlo Breault.
+ Fixes bug 8800.
+
+ o The badge now has a "lang" parameter for localization. Translations
+ exist for en, de, and ru. Patch by Peter Bourgelais.
+
+ o Made facilitator-email-poller reconnect after some SSL and socket
+ errors. Patch by Alexandre Allaire and David Fifield. Fixes bug
+ 8284.
+
+ o Added flashproxy-reg-url to the py2exe instructions in setup.py;
+ this lack meant that flashproxy-reg-url was missing from Windows
+ bundles. Patch by Arlo Breault. Fixes bug 8840.
+
+ o Enabled HTTP Strict Transport Security (HSTS) on the facilitator.
+ Patch by Eduardo Stalinho. Fixes bug 8772.
+
+ o Added a new "appspot" registration method, which is now the first
+ registration method tried, ahead of "email". "appspot" sends
+ registrations through Google App Engine. Patch by Arlo Breault and
+ David Fifield. Fixes bug 8860.
+
+Changes in version 0.12
+ o The new flashproxy-reg-url program prints a URL which, when
+ requested, causes an address to be registered with the facilitator.
+ You can use this program if the other registration methods are
+ blocked: pass the URL to a third party and ask them to request it.
+ Patch by Alexandre Allaire. Fixes bug 7559.
+
+ o The new websocket-server program is the server transport plugin that
+ flash proxies talk to. It replaces the third-party websockify
+ program that was used formerly. It works as a managed proxy and
+ supports the extended ORPort protocol. Fixes bug 7620.
+
+ o Added a line of JavaScript that you can use to put a proxy badge on
+ MediaWiki sites that allow custom JavaScript. Follow the
+ instructions in modules/mediawiki/custom.js. Contributed by
+ Sathyanarayanan Gunasekaran.
+
+ o Make flashproxy-client ignore errors in opening listeners, as long
+ as at least one local and one remote listener can be opened. A user
+ reported a problem with listening on IPv6, while being able to
+ listen on IPv4. Fixes bug 8319.
+
+ o The facilitator now returns a check-back-in parameter in its
+ response, telling proxies how often to poll. Fixes bug 8171. Patch
+ by Alexandre Allaire.
+
+ o Updated the Tor Browser check to match the behavior of new Tor
+ Browsers. Patch by Alexandre Allaire and Arlo Breault. Fixes bug
+ 8434.
+
+Changes in version 0.11
+ o Added -4 and -6 options to flashproxy-client and
+ flashproxy-reg-http. (The options already existed in
+ flashproxy-reg-email.) These options cause registrations helpers to
+ use IPv4 or IPv6 only. Fixes bug 7622. Patch by Jorge Couchet.
+
+ o The facilitator now gives only IPv4 clients to proxies requesting
+ over IPv4, and IPv6 clients to proxies requesting over IPv6. This is
+ to avoid the situation where an IPv4-only proxy is given an IPv6
+ address it cannot connect to. Fixes bug 6124. Patch by Jorge Couchet
+ and David Fifield.
+
+ o The proxy now accepts a cookierequired parameter that controls
+ whether users have to explicitly state their desire to be a proxy.
+ The page at http://crypto.stanford.edu/flashproxy/options.html
+ allows changing user preference.
+
+ o Proxies now poll for clients every 60 seconds rather than 10
+ seconds, and do not begin to poll immediately upon beginning to run.
+
+ o There are new alpha Tor Browser Bundles for download at
+ https://people.torproject.org/~dcf/flashproxy/.
+
+Changes in version 0.10
+ o Fixed a bug in flashproxy-client that made it susceptible to a
+ denial of service (program crash) when receiving large WebSocket
+ messages made up of many small fragmented frames.
+
+ o Made the facilitator hand out more proxies by default, reducing a
+ client's need to re-register.
+
+Changes in version 0.9
+ o There are executable Windows packages of the client programs, so
+ that the programs can be run without Python being installed. Fixes
+ bug 7283. Alexandre Allaire and David Fifield.
+
+ o There are now man pages for the client programs (flashproxy-client,
+ flashproxy-reg-email, and flashproxy-reg-http). Fixes bug 6453.
+ Alexandre Allaire.
+
+ o The proxy now tries to determine whether it is running in Tor
+ Browser, and disables itself if so. Fixes bug 6293. Patch by Jorge
+ Couchet.
+
+Changes in version 0.8
+ o flashproxy-client now operates as a managed proxy by default. This
+ means that there is no longer a need to start flashproxy-client
+ separately from Tor. Use a "ClientTransportPlugin websocket exec"
+ line as in the included torrc. To use flashproxy-client as an
+ external proxy (the way it worked before), use the --external
+ option. Fixes bug 7016.
+
+ o The proxy badge does more intelligent parsing of the boolean "debug"
+ parameter. "0", "false", and other values are now interpreted as
+ false and do not activate debug mode. Formerly any non-empty value
+ was interpreted as true. Fixes bug 7110. Patch by Alexandre Allaire.
+
+ o Fixed a runtime error in flashproxy-client on Windows:
+ AttributeError: 'module' object has no attribute 'IPPROTO_IPV6'
+ Fixes bug 7147. Patch by Alexandre Allaire.
+
+ o Fixed an exception that happened in Windows in flashproxy-reg-email
+ in reading the trusted CA list. The exception message was:
+ Failed to register: [Errno 185090050] _ssl.c:340: error:0B084002:x509 certificate routines:X509_load_cert_crl_file:system lib
+ Fixes bug 7271. Patch by Alexandre Allaire.
+
+ o Fixed an exception that happened on Windows in flashproxy-client,
+ relating to the use of nonblocking sockets:
+ Socket error writing to local: '[Errno 10035] A non-blocking socket operation could not be completed immediately'
+ Fixes bug 7272. Patch by Alexandre Allaire.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..865b15d
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,22 @@
+This is the license of the flash proxy software.
+
+Copyright 2012 David Fifield
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..bfe36be
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,100 @@
+# Makefile for a self-contained binary distribution of flashproxy-client.
+#
+# This builds two zipball targets, dist and dist-exe, for POSIX and Windows
+# respectively. Both can be extracted and run in-place by the end user.
+# (PGP-signed forms also exist, sign and sign-exe.)
+#
+# If you are a distro packager, instead see the separate build scripts for each
+# source component, all of which have an `install` target:
+# - client: Makefile.client
+# - common: setup-common.py
+# - facilitator: facilitator/{configure.ac,Makefile.am}
+#
+# Not for the faint-hearted: it is possible to build dist-exe on GNU/Linux by
+# using wine to install the windows versions of Python, py2exe, and m2crypto,
+# then running `make PYTHON_W32="wine python" dist-exe`.
+
+PACKAGE = flashproxy-client
+VERSION = $(shell sh version.sh)
+DISTNAME = $(PACKAGE)-$(VERSION)
+
+THISFILE = $(lastword $(MAKEFILE_LIST))
+PYTHON = python
+PYTHON_W32 = $(PYTHON)
+
+MAKE_CLIENT = $(MAKE) -f Makefile.client PYTHON="$(PYTHON)"
+# don't rebuild man pages due to VCS giving spurious timestamps, see #9940
+REBUILD_MAN = 0
+
+# all is N/A for a binary package, but include for completeness
+all:
+
+install:
+ $(MAKE_CLIENT) DESTDIR=$(DESTDIR) REBUILD_MAN=$(REBUILD_MAN) install
+ $(PYTHON) setup-common.py install $(if $(DESTDIR),--root=$(DESTDIR))
+
+DISTDIR = dist/$(DISTNAME)
+$(DISTDIR): Makefile.client setup-common.py $(THISFILE)
+ mkdir -p $(DISTDIR)
+ $(MAKE_CLIENT) DESTDIR=$(DISTDIR) bindir=/ docdir=/ man1dir=/doc/ \
+ REBUILD_MAN="$(REBUILD_MAN)" install
+ $(PYTHON) setup-common.py build_py -d $(DISTDIR)
+
+dist/%.zip: dist/%
+ cd dist && zip -q -r -9 "$(@:dist/%=%)" "$(<:dist/%=%)"
+
+dist/%.zip.asc: dist/%.zip
+ rm -f "$@"
+ gpg --sign --detach-sign --armor "$<"
+ gpg --verify "$@" "$<"
+
+dist: force-dist $(DISTDIR).zip
+
+sign: force-dist $(DISTDIR).zip.asc
+
+PY2EXE_TMPDIR = py2exe-tmp
+export PY2EXE_TMPDIR
+$(PY2EXE_TMPDIR): setup-client-exe.py
+ $(PYTHON_W32) setup-client-exe.py py2exe -q
+
+DISTDIR_W32 = $(DISTDIR)-win32
+# below, we override DST_SCRIPT and DST_MAN1 for windows
+$(DISTDIR_W32): $(PY2EXE_TMPDIR) $(THISFILE)
+ mkdir -p $(DISTDIR_W32)
+ $(MAKE_CLIENT) DESTDIR=$(DISTDIR_W32) bindir=/ docdir=/ man1dir=/doc/ \
+ DST_SCRIPT= DST_MAN1='$$(SRC_MAN1)' \
+ REBUILD_MAN="$(REBUILD_MAN)" install
+ cp -t $(DISTDIR_W32) $(PY2EXE_TMPDIR)/dist/*
+
+dist-exe: force-dist-exe $(DISTDIR_W32).zip
+
+sign-exe: force-dist-exe $(DISTDIR_W32).zip.asc
+
+# clean is N/A for a binary package, but include for completeness
+clean: distclean
+
+distclean:
+ $(MAKE_CLIENT) clean
+ $(PYTHON) setup-common.py clean --all
+ rm -rf dist $(PY2EXE_TMPDIR)
+
+test: check
+check:
+ $(MAKE_CLIENT) check
+ $(PYTHON) setup-common.py test
+
+
+test-full: test
+ cd facilitator && \
+ { test -x ./config.status && ./config.status || \
+ { test -x ./configure || ./autogen.sh; } && ./configure; } \
+ && make && PYTHONPATH=.. make check
+ cd proxy && make test
+
+force-dist:
+ rm -rf $(DISTDIR) $(DISTDIR).zip
+
+force-dist-exe:
+ rm -rf $(DISTDIR_W32) $(DISTDIR_W32).zip $(PY2EXE_TMPDIR)
+
+.PHONY: all dist sign dist-exe sign-exe clean distclean test check test-full force-dist force-dist-exe
diff --git a/Makefile.client b/Makefile.client
new file mode 100644
index 0000000..efdef7c
--- /dev/null
+++ b/Makefile.client
@@ -0,0 +1,92 @@
+# Makefile for a source distribution of flashproxy-client.
+#
+# This package is not self-contained and the build products may require other
+# dependencies to function; it is given as a reference for distro packagers.
+
+PACKAGE = flashproxy-client
+VERSION = $(shell sh version.sh)
+DESTDIR =
+
+THISFILE = $(lastword $(MAKEFILE_LIST))
+PYTHON = python
+
+# GNU command variables
+# see http://www.gnu.org/prep/standards/html_node/Command-Variables.html
+
+INSTALL = install
+INSTALL_DATA = $(INSTALL) -m 644
+INSTALL_PROGRAM = $(INSTALL)
+INSTALL_SCRIPT = $(INSTALL)
+
+# GNU directory variables
+# see http://www.gnu.org/prep/standards/html_node/Directory-Variables.html
+
+prefix = /usr/local
+exec_prefix = $(prefix)
+bindir = $(exec_prefix)/bin
+
+datarootdir = $(prefix)/share
+datadir = $(datarootdir)
+sysconfdir = $(prefix)/etc
+
+docdir = $(datarootdir)/doc/$(PACKAGE)
+mandir = $(datarootdir)/man
+man1dir = $(mandir)/man1
+
+srcdir = .
+
+SRC_MAN1 = doc/flashproxy-client.1.txt doc/flashproxy-reg-appspot.1.txt doc/flashproxy-reg-email.1.txt doc/flashproxy-reg-http.1.txt doc/flashproxy-reg-url.1.txt
+SRC_SCRIPT = flashproxy-client flashproxy-reg-appspot flashproxy-reg-email flashproxy-reg-http flashproxy-reg-url
+SRC_DOC = README LICENSE ChangeLog torrc
+SRC_ALL = $(SRC_SCRIPT) $(SRC_DOC) $(SRC_MAN1)
+
+DST_MAN1 = $(SRC_MAN1:%.1.txt=%.1)
+DST_SCRIPT = $(SRC_SCRIPT)
+DST_DOC = $(SRC_DOC)
+DST_ALL = $(DST_SCRIPT) $(DST_DOC) $(DST_MAN1)
+
+TEST_PY = flashproxy-client-test.py
+TEST_ALL = $(TEST_PY)
+
+REBUILD_MAN = 1
+
+all: $(DST_ALL) $(THISFILE)
+
+%.1: %.1.txt
+ifeq ($(REBUILD_MAN),0)
+ @echo "warning: $@ *may* be out-of-date; if so then rm and re-checkout from VCS or force a re-build with REBUILD_MAN=1"
+else
+ rm -f $@
+ a2x --no-xmllint --xsltproc-opts "--stringparam man.th.title.max.length 24" -d manpage -f manpage $<
+endif
+
+install: all
+ mkdir -p $(DESTDIR)$(bindir)
+ for i in $(DST_SCRIPT); do $(INSTALL_SCRIPT) "$$i" $(DESTDIR)$(bindir); done
+ mkdir -p $(DESTDIR)$(docdir)
+ for i in $(DST_DOC); do $(INSTALL_DATA) "$$i" $(DESTDIR)$(docdir); done
+ mkdir -p $(DESTDIR)$(man1dir)
+ for i in $(DST_MAN1); do $(INSTALL_DATA) "$$i" $(DESTDIR)$(man1dir); done
+
+uninstall:
+ for i in $(notdir $(DST_SCRIPT)); do rm $(DESTDIR)$(bindir)/"$$i"; done
+ for i in $(notdir $(DST_DOC)); do rm $(DESTDIR)$(docdir)/"$$i"; done
+ for i in $(notdir $(DST_MAN1)); do rm $(DESTDIR)$(man1dir)/"$$i"; done
+
+clean:
+ rm -f *.pyc
+
+distclean: clean
+
+maintainer-clean: distclean
+ rm -f $(DST_MAN1)
+
+# TODO(infinity0): eventually do this as part of 'check' once we have a decent
+# overrides file in place that filters out false-positives
+pylint: $(SRC_SCRIPT)
+ pylint -E $^
+
+check: $(THISFILE)
+ for i in $(TEST_PY); do $(PYTHON) "$$i"; done
+
+.PHONY: all install uninstall clean distclean maintainer-clean check pylint
diff --git a/README b/README
new file mode 100644
index 0000000..7888b22
--- /dev/null
+++ b/README
@@ -0,0 +1,102 @@
+== Quick start for users
+
+You must have a version of Tor that supports pluggable transports. This
+means version 0.2.3.2-alpha or later.
+
+All the flashproxy programs and source code can be downloaded this way:
+ git clone https://git.torproject.org/flashproxy.git
+But as a user you only need these files:
+ https://gitweb.torproject.org/flashproxy.git/blob_plain/HEAD:/flashproxy-client
+ https://gitweb.torproject.org/flashproxy.git/blob_plain/HEAD:/torrc
+
+You must be able to receive TCP connections; unfortunately means that
+you cannot be behind NAT. See the section "Using a public client
+transport plugin" below to try out the system even behind NAT.
+
+Run Tor using the included torrc file:
+ $ tor -f torrc
+By default the transport plugin listens on Internet-facing TCP port
+9000. If you have to use a different port (to get through a firewall,
+for example), edit the ClientTransportPlugin line of the torrc to give a
+different port number:
+ ClientTransportPlugin flashproxy exec ./flashproxy-client --register :0 :8888
+If the flashproxy-client program is in a different directoy (after being
+installed, for example), use the full path in the ClientTransportPlugin
+line:
+ ClientTransportPlugin flashproxy exec /usr/local/bin/flashproxy-client --register
+
+You should receive a flash proxy connection within about 60 seconds. See
+"Troubleshooting" below if it doesn't work.
+
+
+== Overview
+
+This is a set of tools that make it possible to connect Tor through an
+browser-based proxy running on another computer. The flash proxy can be
+run just by opening a web page in a browser. Flash proxies are one of
+several pluggable transports for Tor.
+
+There are five main parts.
+1. The Tor client, running on someone's localhost.
+2. A client transport plugin, which is a program that waits for
+ connections from a flash proxy and connects them to the Tor client.
+3. A flash proxy, which is a JavaScript program running in someone's web
+ browser.
+4. A facilitator, which is a server that keeps a list of clients that
+ want a connection and assigns those addresses to proxies.
+5. A Tor relay running a server transport plugin capable of receiving
+ WebSocket connections.
+
+The purpose of this project is to create many ephemeral bridge IP
+addresses, with the goal of outpacing a censor's ability to block them.
+Rather than increasing the number of bridges at static addresses, we aim
+to make existing bridges reachable by a larger and changing pool of
+addresses.
+
+
+== Demonstration page
+
+This page has a description of the project; viewing it also turns your
+computer into a flash proxy as long as the page is open.
+
+http://crypto.stanford.edu/flashproxy/
+
+
+== Troubleshooting
+
+Make sure someone is viewing http://crypto.stanford.edu/flashproxy/, or
+another web page with a flash proxy badge on it.
+
+You can add the --log option to the ClientTransportPlugin command line
+in order to save debugging log messages.
+
+If tor hangs at 10% with these messages:
+ [notice] Bootstrapped 10%: Finishing handshake with directory server.
+ [notice] no known bridge descriptors running yet; stalling
+as a last resort you can try deleting the files in ~/.tor and
+/var/lib/tor, and then restarting tor.
+
+If tor apparently hangs here:
+ [notice] Bootstrapped 50%: Loading relay descriptors.
+ [notice] new bridge descriptor '...' (fresh)
+wait a few minutes. It can take a while to download relay descriptors.
+
+If you suspect that the facilitator has lost your client registration, you can
+re-register:
+ $ flashproxy-reg-email
+ $ flashproxy-reg-http
+
+
+== How to run a relay
+
+Proxies talk to a relay running the websocket pluggable transport.
+Source code and documentation for the server transport plugin are in the
+Git repository at
+https://git.torproject.org/pluggable-transports/websocket.git.
+
+
+== How to put a flash proxy badge on a web page
+
+Paste in this HTML where you want the badge to appear:
+
+<iframe src="//crypto.stanford.edu/flashproxy/embed.html" width="80" height="15" frameborder="0" scrolling="no"></iframe>
diff --git a/doc/design.txt b/doc/design.txt
new file mode 100644
index 0000000..319dd9c
--- /dev/null
+++ b/doc/design.txt
@@ -0,0 +1,241 @@
+Design of flash proxies
+
+0. Problem statement
+
+ Provide access to the Tor network for users behind a restrictive
+ firewall that blocks direct access to all Tor relays and bridges.
+
+1. Overview and background
+
+ We assume the existence of an adversary powerful enough to enumerate
+ and block all public and non-public (bridge) relays. For users facing
+ such an adversary, we assume there exists a subset of reachable hosts
+ that themselves can reach the Tor network. We call this subset the
+ unrestricted Internet.
+
+ A browser-based proxy (flash proxy), running in a web page in the
+ unrestricted Internet, proxies connections between the restricted
+ Internet and the Tor network. These proxies are expected to be
+ temporary and short-lived, but their number will be great enough that
+ they can't all be blocked effectively.
+
+ The implementation of a browser-based proxy using WebSocket is
+ complicated by restrictions that prevent it being a straightforward
+ proxy. Chief among these is the lack of listening sockets. WebSocket
+ can only initiate outgoing connections, not receive incoming ones. The
+ flash proxy can only connect to external hosts by connecting directly
+ to them. Another, but less important, restriction is that
+ browser-based networking does not provide low-level socket access such
+ as control of source address.
+
+2. Components
+
+ Conceptually, each flash proxy is nothing more than a simple proxy,
+ which accepts connections from a client and forwards data to a server.
+ But because of the limited networking facilities available to an
+ in-browser application, several other pieces are needed.
+
+ 1. Tor client: with a ClientTransportPlugin config option to allow it to
+ use the flashproxy transport client.
+ 2. Client transport plugin: Runs on the same computer as the Tor client.
+ On startup, it registers with the facilitator to inform that it is
+ waiting for a connection from a flash proxy. When this is received,
+ it starts proxying data between it and the local Tor client.
+ 3. Flash proxy: Runs in someone's browser, in an uncensored region of
+ the Internet. The flash proxy first connects to the facilitator to
+ get a client registration. It then makes two outgoing connections,
+ one to a Tor relay and one to a waiting Tor client, and starts
+ proxying data between them.
+ 4. Facilitator: Keeps track of client registrations and hands them out
+ to clients. It is capable of receiving client registrations in a
+ variety of ways. It sends registrations to flash proxies over HTTP.
+ The facilitator is responsible for matching clients to proxies in a
+ reasonable manner.
+ 5. Tor relay: with a ServerTransportPlugin config option to allow it to
+ use the flashproxy transport server.
+ 6. Server transport plugin: Waits for a connection from a flash proxy and
+ proxies data between it and the local Tor relay.
+
+3. Protocols
+
+ The numbers refer to the same components as in sect 2 above. Arrows
+ indicate the direction of the initial TCP connection.
+
+ 1>2. Pluggable transport, client-side. See core tor docs for details.
+ 2>4. Secure rendezvous using a variety of custom methods; see
+ facilitator-howto.txt for details. This must be very hard to censor,
+ e.g. using a popular web service over HTTPS.
+ 3>4. Custom protocol specific to flashproxy, where each flashproxy polls
+ a facilitator for client registrations.
+ 2<3. WebSocket. This must be very hard to censor, which may require
+ additional transformations to the underlying data stream. Note
+ that this stream is controlled by the source client, not the flash
+ proxy; in a plain flashproxy-only channel, it is as described in
+ websocket-transport.txt.
+ 5<3. WebSocket.
+ 5>6. Pluggable transport, server-side. See core tor docs for details.
+
+4. Sample session
+
+ 1. The restricted Tor user starts the client transport plugin.
+ 2. The client transport plugin notifies the facilitator that it needs
+ a connection.
+ 3. The restricted user starts Tor, which connects to the client
+ transport plugin.
+ 4. An unrestricted user opens the web page containing the flash proxy.
+ 5. The flash proxy connects to the facilitator and asks for a client.
+ 6. The facilitator sends one of its client registrations to the proxy.
+ 7. The flash proxy connects to a Tor relay and to the waiting client
+ transport plugin.
+ 8. The client transport plugin receives the flash proxy's connection
+ and begins relaying data between it and the Tor relay.
+
+ Later, the flash proxy may go offline. Assuming that another flash
+ proxy is available, it will receive the same client's address from the
+ facilitator, and the local Tor client will reconnect to the client
+ through it.
+
+5. Behavior of the Tor client
+
+ The Tor client must be configured to make its connections through a
+ local proxy (the client transport plugin). This configuration is
+ sufficient:
+ ClientTransportPlugin flashproxy socks4 127.0.0.1:9001
+ UseBridges 1
+ Bridge flashproxy 0.0.1.0:1
+ LearnCircuitBuildTimeout 0
+ The address given for the "Bridge" option is actually irrelevant. The
+ client transport plugin will ignore it and connect (through the flash
+ proxy) to a Tor relay. The Tor client does not have control of its
+ first hop.
+
+6. Behavior of the client transport plugin
+
+ The client transport plugin serves two purposes: It sends a
+ registration message to the facilitator and it carries data between a
+ flash proxy and the local Tor client.
+
+ On startup, the client transport plugin sends a registration message
+ to the facilitator, informing the facilitator that it is waiting for
+ a connection. If the client transport plugin obfuscates its
+ connections using pluggable transports, then it also appends the
+ listening address of its transports to the registration message.
+
+ The facilitator will later hand this registration to a flash
+ proxy. The registration message is an HTTP POST request of the form:
+
+ POST / HTTP/1.0
+
+ client=[<address>]:<port>[&client-transport=<transport>][
+ client=[<address>]:<port>[&client-transport=<transport>] ...]
+
+ Where 'transport' is the name of the pluggable transport that is
+ listening on <address>:<port>. The default flashproxy transport is
+ named 'websocket'.
+
+ For example a registration message might look like this:
+ client=1.2.3.4:9000
+ client=1.2.3.4:10000&client-transport=obfs3|websocket
+
+ The facilitator sends a 200 reply if the registration was successful
+ and an error status otherwise. If the transport plugin omits the
+ [<address>] part, the facilitator will automatically fill it in based
+ on the HTTP client address, which means the transport plugin doesn't
+ have to know its external address.
+
+ The client transport plugin solves the impedance mismatch between the
+ Tor client and the flash proxy, both of which want to make outgoing
+ connections to the other. The transport plugin sits in between,
+ listens for connections from both ends, and matches them together. The
+ remote socket listens on port 9000 and the local on port 9001.
+
+ On the local side, it acts as a SOCKS proxy (albeit one that always
+ goes to the same destination).
+
+7. Behavior of the flash proxy
+
+ The flash proxy polls the facilitator for client registrations. When
+ it receives a registration, it opens one connection to the given Tor
+ relay, one to the given client, and begin proxying data between them.
+
+ The proxy asks the facilitator for a registration with an HTTP GET
+ request:
+
+ GET /?r=<version>&client=<addr>:<port>&transport=<transport name> HTTP/1.0
+
+ The 'r' parameter is the protocol revision number (should be '1' for now).
+ The 'client' parameter carries the IP address of a flashproxy
+ client. The client parameter can repeat to report multiple
+ connected clients.
+ The 'transport' parameter may be repeated zero or many times and
+ signals the outer-transports that this flashproxy supports. (See
+ section 10 for a discussion of inner and outer transports.)
+
+ For example:
+ GET /?r=1&client=7.1.43.21:9999&client=1.2.3.4:9000&transport=webrtc&transport=websocket HTTP/1.0
+
+ The response code is 200 and the body looks like this:
+
+ client=<address>:<port>&client-transport=<transport>&relay=<address>:<port>&relay-transport=<transport>
+
+ For example:
+ client=1.2.3.4:2000&client-transport=websocket&relay=10.10.10:9902&relay-transport=websocket
+
+ As with the request, the response transports are actually outer
+ transports; inner transports are not the proxy's concern and therefore
+ not given.
+
+ If the value for the client parameter is empty, it means that there are no
+ client registrations for this proxy.
+
+ The flash proxy may serve more than one relay–client pair at once.
+
+8. Behavior of the facilitator
+
+ The faciliator is a HTTP server that handles client POST registrations
+ and proxy GET requests according to the formats given above. The
+ facilitator listens on port 9002.
+
+ In the current implementation, the facilitator forgets a client
+ registration after giving it to a flash proxy. The client must
+ re-register if it wants another connection later.
+
+9. Behavior of the Tor relay.
+
+ The Tor relay requires no special configuration.
+
+10. Inner and outer transports
+
+ The client can talk to the relay using not only the Tor protocol, but
+ any transport protocol implemented by e.g. another pluggable transport
+ that sits between tor and the flashproxy PT. For the facilitator to
+ match a client with a relay that understands it, flashproxy-client
+ must be given the name of the transport protocol, via the --transport
+ option. This is divided into two parts, the inner and outer transport,
+ written like "inner|outer" or just "outer" if the inner transport is
+ the plain Tor protocol.
+
+ The inner transport is the protocol that the non-flashproxy parts of
+ the client and relay talk to each other with, and must be the same for
+ each connected pair. Beyond that, the semantics of the transport are
+ opaque to flashproxy; it does not know or care.
+
+ The outer transports are the protocols that the browser proxy uses to
+ talk to the client and relay, and may be different for each. The proxy
+ un-applies the outer transport of the client so that only the inner
+ traffic remains, then re-applies the outer transport of the relay to
+ this and sends it to the relay; and vice-versa for traffic going in
+ the opposite direction.
+
+ Diagram:
+
+ client <======outer-C=======> proxy <======outer-S=======> relay
+ <=======inner=========-------========inner========>
+
+ Currently the only supported outer transport is "websocket", but we
+ will also add support for newer technologies such as webRTC.
+
+ (We have also seen third-party proxies running outside the browser
+ on NodeJS that can open plain TCP connections, so that the outer
+ transport is effectively just "tcp", although this is not currently
+ recognised by the facilitator.)
diff --git a/doc/flashproxy-client.1 b/doc/flashproxy-client.1
new file mode 100644
index 0000000..74b0d30
--- /dev/null
+++ b/doc/flashproxy-client.1
@@ -0,0 +1,156 @@
+'\" t
+.\" Title: flashproxy-client
+.\" Author: [FIXME: author] [see http://docbook.sf.net/el/author]
+.\" Generator: DocBook XSL Stylesheets v1.78.1 <http://docbook.sf.net/>
+.\" Date: 10/09/2013
+.\" Manual: \ \&
+.\" Source: \ \&
+.\" Language: English
+.\"
+.TH "FLASHPROXY\-CLIENT" "1" "10/09/2013" "\ \&" "\ \&"
+.\" -----------------------------------------------------------------
+.\" * Define some portability stuff
+.\" -----------------------------------------------------------------
+.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+.\" http://bugs.debian.org/507673
+.\" http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html
+.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+.ie \n(.g .ds Aq \(aq
+.el .ds Aq '
+.\" -----------------------------------------------------------------
+.\" * set default formatting
+.\" -----------------------------------------------------------------
+.\" disable hyphenation
+.nh
+.\" disable justification (adjust text to left margin only)
+.ad l
+.\" -----------------------------------------------------------------
+.\" * MAIN CONTENT STARTS HERE *
+.\" -----------------------------------------------------------------
+.SH "NAME"
+flashproxy-client \- The flash proxy client transport plugin
+.SH "SYNOPSIS"
+.sp
+\fBflashproxy\-client\fR \fB\-\-register\fR [\fIOPTIONS\fR] [\fILOCAL\fR][:\fIPORT\fR] [\fIREMOTE\fR][:\fIPORT\fR]
+.SH "DESCRIPTION"
+.sp
+Wait for connections on a local and a remote port\&. When any pair of connections exists, data is ferried between them until one side is closed\&. By default \fILOCAL\fR is localhost addresses on port 9001 and \fIREMOTE\fR is all addresses on port 9000\&.
+.sp
+The local connection acts as a SOCKS4a proxy, but the host and port in the SOCKS request are ignored and the local connection is always linked to a remote connection\&.
+.sp
+By default, runs as a managed proxy: informs a parent Tor process of support for the "flashproxy" or "websocket" pluggable transport\&. In managed mode, the \fILOCAL\fR port is chosen arbitrarily instead of defaulting to 9001; however this can be overridden by including a \fILOCAL\fR port in the command\&. This is the way the program should be invoked in a torrc ClientTransportPlugin "exec" line\&. Use the \fB\-\-external\fR option to run as an external proxy that does not interact with Tor\&.
+.sp
+If any of the \fB\-\-register\fR, \fB\-\-register\-addr\fR, or \fB\-\-register\-methods\fR options are used, then your IP address will be sent to the facilitator so that proxies can connect to you\&. You need to register in some way in order to get any service\&. The \fB\-\-facilitator\fR option allows controlling which facilitator is used; if omitted, it uses a public default\&.
+.SH "OPTIONS"
+.PP
+\fB\-4\fR
+.RS 4
+Registration helpers use IPv4\&.
+.RE
+.PP
+\fB\-6\fR
+.RS 4
+Registration helpers use IPv6\&.
+.RE
+.PP
+\fB\-\-daemon\fR
+.RS 4
+Daemonize (Unix only)\&.
+.RE
+.PP
+\fB\-\-external\fR
+.RS 4
+Be an external proxy (don\(cqt interact with Tor using environment variables and stdout)\&.
+.RE
+.PP
+\fB\-f\fR, \fB\-\-facilitator\fR=\fIURL\fR
+.RS 4
+Advertise willingness to receive connections to URL\&.
+.RE
+.PP
+\fB\-\-facilitator\-pubkey\fR=\fIFILENAME\fR
+.RS 4
+Encrypt registrations to the given PEM\-formatted public key (default built\-in)\&.
+.RE
+.PP
+\fB\-h\fR, \fB\-\-help\fR
+.RS 4
+Display a help message and exit\&.
+.RE
+.PP
+\fB\-l\fR, \fB\-\-log\fR=\fIFILENAME\fR
+.RS 4
+Write log to
+\fIFILENAME\fR
+(default is stdout)\&.
+.RE
+.PP
+\fB\-\-pidfile\fR=\fIFILENAME\fR
+.RS 4
+Write PID to
+\fIFILENAME\fR
+after daemonizing\&.
+.RE
+.PP
+\fB\-\-port\-forwarding\fR
+.RS 4
+Attempt to forward
+\fIREMOTE\fR
+port\&.
+.RE
+.PP
+\fB\-\-port\-forwarding\-helper\fR=\fIPROGRAM\fR
+.RS 4
+Use the given
+\fIPROGRAM\fR
+to forward ports (default "tor\-fw\-helper")\&. Implies
+\fB\-\-port\-forwarding\fR\&.
+.RE
+.PP
+\fB\-\-port\-forwarding\-external\fR=\fIPORT\fR
+.RS 4
+Forward the external
+\fIPORT\fR
+to
+\fIREMOTE\fR
+on the local host (default same as REMOTE)\&. Implies
+\fB\-\-port\-forwarding\fR\&.
+.RE
+.PP
+\fB\-r\fR, \fB\-\-register\fR
+.RS 4
+Register with the facilitator\&.
+.RE
+.PP
+\fB\-\-register\-addr\fR=\fIADDR\fR
+.RS 4
+Register the given address (in case it differs from
+\fIREMOTE\fR)\&. Implies
+\fB\-\-register\fR\&.
+.RE
+.PP
+\fB\-\-register\-methods\fR=\fIMETHOD\fR[,\fIMETHOD\fR]
+.RS 4
+Register using the given comma\-separated list of methods\&. Implies
+\fB\-\-register\fR\&. Possible methods are: appspot, email, http\&. Default is "appspot,email,http"\&.
+.RE
+.PP
+\fB\-\-transport\fR=\fITRANSPORT\fR
+.RS 4
+Registrations include the fact that you intend to use the given
+\fITRANSPORT\fR
+(default "websocket")\&.
+.RE
+.PP
+\fB\-\-unsafe\-logging\fR
+.RS 4
+Don\(cqt scrub IP addresses from logs\&.
+.RE
+.SH "SEE ALSO"
+.sp
+\fBhttp://crypto\&.stanford\&.edu/flashproxy/\fR
+.sp
+\fBhttps://www\&.torproject\&.org/docs/pluggable\-transports\&.html\&.en\fR
+.SH "BUGS"
+.sp
+Please report using \fBhttps://trac\&.torproject\&.org/projects/tor\fR\&.
diff --git a/doc/flashproxy-client.1.txt b/doc/flashproxy-client.1.txt
new file mode 100644
index 0000000..b4779a2
--- /dev/null
+++ b/doc/flashproxy-client.1.txt
@@ -0,0 +1,106 @@
+// This file is asciidoc source code.
+// To generate manpages, use the a2x command i.e.
+// a2x --no-xmllint -d manpage -f manpage flashproxy-client.1.txt
+// see http://www.methods.co.nz/asciidoc/userguide.html#X1
+FLASHPROXY-CLIENT(1)
+====================
+
+NAME
+----
+flashproxy-client - The flash proxy client transport plugin
+
+SYNOPSIS
+--------
+**flashproxy-client** **--register** [__OPTIONS__] [__LOCAL__][:__PORT__] [__REMOTE__][:__PORT__]
+
+DESCRIPTION
+-----------
+Wait for connections on a local and a remote port. When any pair of connections
+exists, data is ferried between them until one side is closed. By default
+__LOCAL__ is localhost addresses on port 9001 and __REMOTE__ is all addresses
+on port 9000.
+
+The local connection acts as a SOCKS4a proxy, but the host and port in the SOCKS
+request are ignored and the local connection is always linked to a remote
+connection.
+
+By default, runs as a managed proxy: informs a parent Tor process of support for
+the "flashproxy" or "websocket" pluggable transport. In managed mode, the __LOCAL__ port is chosen
+arbitrarily instead of defaulting to 9001; however this can be
+overridden by including a __LOCAL__ port in the command. This is the way the
+program should be invoked in a torrc ClientTransportPlugin "exec" line.
+Use the **--external** option to run as an external proxy that does not
+interact with Tor.
+
+If any of the **--register**, **--register-addr**, or **--register-methods** options are
+used, then your IP address will be sent to the facilitator so that proxies can
+connect to you. You need to register in some way in order to get any service.
+The **--facilitator** option allows controlling which facilitator is used; if
+omitted, it uses a public default.
+
+OPTIONS
+-------
+**-4**::
+ Registration helpers use IPv4.
+
+**-6**::
+ Registration helpers use IPv6.
+
+**--daemon**::
+ Daemonize (Unix only).
+
+**--external**::
+ Be an external proxy (don't interact with Tor using environment variables
+ and stdout).
+
+**-f**, **--facilitator**=__URL__::
+ Advertise willingness to receive connections to URL.
+
+**--facilitator-pubkey**=__FILENAME__::
+ Encrypt registrations to the given PEM-formatted public key (default built-in).
+
+**-h**, **--help**::
+ Display a help message and exit.
+
+**-l**, **--log**=__FILENAME__::
+ Write log to __FILENAME__ (default is stdout).
+
+**--pidfile**=__FILENAME__::
+ Write PID to __FILENAME__ after daemonizing.
+
+**--port-forwarding**::
+ Attempt to forward __REMOTE__ port.
+
+**--port-forwarding-helper**=__PROGRAM__::
+ Use the given __PROGRAM__ to forward ports (default "tor-fw-helper"). Implies
+ **--port-forwarding**.
+
+**--port-forwarding-external**=__PORT__::
+ Forward the external __PORT__ to __REMOTE__ on the local host (default same
+ as REMOTE). Implies **--port-forwarding**.
+
+**-r**, **--register**::
+ Register with the facilitator.
+
+**--register-addr**=__ADDR__::
+ Register the given address (in case it differs from __REMOTE__). Implies **--register**.
+
+**--register-methods**=__METHOD__[,__METHOD__]::
+ Register using the given comma-separated list of methods. Implies **--register**.
+ Possible methods are: appspot, email, http. Default is "appspot,email,http".
+
+**--transport**=__TRANSPORT__::
+ Registrations include the fact that you intend to use the given __TRANSPORT__ (default "websocket").
+
+**--unsafe-logging**::
+ Don't scrub IP addresses from logs.
+
+SEE ALSO
+--------
+**http://crypto.stanford.edu/flashproxy/**
+
+**https://www.torproject.org/docs/pluggable-transports.html.en**
+
+BUGS
+----
+Please report using **https://trac.torproject.org/projects/tor**.
diff --git a/doc/flashproxy-reg-appspot.1 b/doc/flashproxy-reg-appspot.1
new file mode 100644
index 0000000..9af7346
--- /dev/null
+++ b/doc/flashproxy-reg-appspot.1
@@ -0,0 +1,87 @@
+'\" t
+.\" Title: flashproxy-reg-appspot
+.\" Author: [FIXME: author] [see http://docbook.sf.net/el/author]
+.\" Generator: DocBook XSL Stylesheets v1.78.1 <http://docbook.sf.net/>
+.\" Date: 10/10/2013
+.\" Manual: \ \&
+.\" Source: \ \&
+.\" Language: English
+.\"
+.TH "FLASHPROXY\-REG\-APPSPOT" "1" "10/10/2013" "\ \&" "\ \&"
+.\" -----------------------------------------------------------------
+.\" * Define some portability stuff
+.\" -----------------------------------------------------------------
+.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+.\" http://bugs.debian.org/507673
+.\" http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html
+.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+.ie \n(.g .ds Aq \(aq
+.el .ds Aq '
+.\" -----------------------------------------------------------------
+.\" * set default formatting
+.\" -----------------------------------------------------------------
+.\" disable hyphenation
+.nh
+.\" disable justification (adjust text to left margin only)
+.ad l
+.\" -----------------------------------------------------------------
+.\" * MAIN CONTENT STARTS HERE *
+.\" -----------------------------------------------------------------
+.SH "NAME"
+flashproxy-reg-appspot \- Register with a facilitator through Google App Engine\&.
+.SH "SYNOPSIS"
+.sp
+\fBflashproxy\-reg\-appspot\fR [\fIOPTIONS\fR] [\fIREMOTE\fR][:\fIPORT\fR]
+.SH "DESCRIPTION"
+.sp
+Register with a flash proxy facilitator through a Google App Engine app\&. By default the remote address registered is ":9000" (the external IP address is guessed)\&. It requires https://www\&.google\&.com/ not to be blocked\&.
+.sp
+This program uses a trick to talk to App Engine, even though appspot\&.com may be blocked\&. The IP address and Server Name Indication of the request are for www\&.google\&.com, but the Host header inside the request is for an appspot\&.com subdomain\&.
+.sp
+Requires the \fBflashproxy\-reg\-url\fR program\&.
+.SH "OPTIONS"
+.PP
+\fB\-4\fR
+.RS 4
+Name lookups use only IPv4\&.
+.RE
+.PP
+\fB\-6\fR
+.RS 4
+Name lookups use only IPv6\&.
+.RE
+.PP
+\fB\-\-disable\-pin\fR
+.RS 4
+Don\(cqt check the server\(cqs public key against a list of known pins\&. You can use this if the server\(cqs public key has changed and this program hasn\(cqt been updated yet\&.
+.RE
+.PP
+\fB\-\-facilitator\-pubkey\fR=\fIFILENAME\fR
+.RS 4
+Encrypt registrations to the given PEM\-formatted public key (default built\-in)\&.
+.RE
+.PP
+\fB\-h\fR, \fB\-\-help\fR
+.RS 4
+Display help message and exit\&.
+.RE
+.PP
+\fB\-\-transport\fR=\fITRANSPORT\fR
+.RS 4
+Registrations include the fact that you intend to use the given
+\fITRANSPORT\fR
+(default "websocket")\&.
+.RE
+.PP
+\fB\-\-unsafe\-logging\fR
+.RS 4
+Don\(cqt scrub IP addresses from logs\&.
+.RE
+.SH "SEE ALSO"
+.sp
+\fBhttp://crypto\&.stanford\&.edu/flashproxy/\fR
+.sp
+\fBhttps://www\&.torproject\&.org/docs/pluggable\-transports\&.html\&.en\fR
+.SH "BUGS"
+.sp
+Please report using \fBhttps://trac\&.torproject\&.org/projects/tor\fR\&.
diff --git a/doc/flashproxy-reg-appspot.1.txt b/doc/flashproxy-reg-appspot.1.txt
new file mode 100644
index 0000000..9b5a124
--- /dev/null
+++ b/doc/flashproxy-reg-appspot.1.txt
@@ -0,0 +1,64 @@
+// This file is asciidoc source code.
+// To generate manpages, use the a2x command.
+// This one has a long name, if you don't change the
+// default length parameter it will be truncated, use:
+// a2x --no-xmllint --xsltproc-opts "--stringparam man.th.title.max.length 24" -d manpage -f manpage flashproxy-reg-appspot.1.txt
+FLASHPROXY-REG-APPSPOT(1)
+=========================
+
+NAME
+----
+flashproxy-reg-appspot - Register with a facilitator through Google App Engine.
+
+SYNOPSIS
+--------
+**flashproxy-reg-appspot** [__OPTIONS__] [__REMOTE__][:__PORT__]
+
+DESCRIPTION
+-----------
+Register with a flash proxy facilitator through a Google App Engine app.
+By default the remote address registered is ":9000" (the
+external IP address is guessed). It requires https://www.google.com/ not
+to be blocked.
+
+This program uses a trick to talk to App Engine, even though appspot.com
+may be blocked. The IP address and Server Name Indication of the request
+are for www.google.com, but the Host header inside the request is for an
+appspot.com subdomain.
+
+Requires the **flashproxy-reg-url** program.
+
+OPTIONS
+-------
+**-4**::
+ Name lookups use only IPv4.
+
+**-6**::
+ Name lookups use only IPv6.
+
+**--disable-pin**::
+ Don't check the server's public key against a list of known pins.
+ You can use this if the server's public key has changed and this
+ program hasn't been updated yet.
+
+**--facilitator-pubkey**=__FILENAME__::
+ Encrypt registrations to the given PEM-formatted public key (default built-in).
+
+**-h**, **--help**::
+ Display help message and exit.
+
+**--transport**=__TRANSPORT__::
+ Registrations include the fact that you intend to use the given __TRANSPORT__ (default "websocket").
+
+**--unsafe-logging**::
+ Don't scrub IP addresses from logs.
+
+SEE ALSO
+--------
+**http://crypto.stanford.edu/flashproxy/**
+
+**https://www.torproject.org/docs/pluggable-transports.html.en**
+
+BUGS
+----
+Please report using **https://trac.torproject.org/projects/tor**.
diff --git a/doc/flashproxy-reg-email.1 b/doc/flashproxy-reg-email.1
new file mode 100644
index 0000000..eac32cc
--- /dev/null
+++ b/doc/flashproxy-reg-email.1
@@ -0,0 +1,106 @@
+'\" t
+.\" Title: flashproxy-reg-email
+.\" Author: [FIXME: author] [see http://docbook.sf.net/el/author]
+.\" Generator: DocBook XSL Stylesheets v1.78.1 <http://docbook.sf.net/>
+.\" Date: 10/10/2013
+.\" Manual: \ \&
+.\" Source: \ \&
+.\" Language: English
+.\"
+.TH "FLASHPROXY\-REG\-EMAIL" "1" "10/10/2013" "\ \&" "\ \&"
+.\" -----------------------------------------------------------------
+.\" * Define some portability stuff
+.\" -----------------------------------------------------------------
+.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+.\" http://bugs.debian.org/507673
+.\" http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html
+.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+.ie \n(.g .ds Aq \(aq
+.el .ds Aq '
+.\" -----------------------------------------------------------------
+.\" * set default formatting
+.\" -----------------------------------------------------------------
+.\" disable hyphenation
+.nh
+.\" disable justification (adjust text to left margin only)
+.ad l
+.\" -----------------------------------------------------------------
+.\" * MAIN CONTENT STARTS HERE *
+.\" -----------------------------------------------------------------
+.SH "NAME"
+flashproxy-reg-email \- Register with a facilitator using the email method
+.SH "SYNOPSIS"
+.sp
+\fBflashproxy\-reg\-email\fR [\fIOPTIONS\fR] [\fIREMOTE\fR][:\fIPORT\fR]
+.SH "DESCRIPTION"
+.sp
+Register with a flash proxy facilitator through email\&. Makes a STARTTLS connection to an SMTP server and sends mail with a client IP address to a designated address\&. By default the remote address registered is ":9000" (the external IP address is guessed based on the SMTP server\(cqs response)\&.
+.sp
+Using an SMTP server or email address other than the defaults will not work unless you have made special arrangements to connect them to a facilitator\&.
+.sp
+The email address is not polled continually\&. After running the program, it may take up to a minute for the registration to be recognized\&.
+.sp
+This program requires the M2Crypto library for Python\&.
+.SH "OPTIONS"
+.PP
+\fB\-4\fR
+.RS 4
+Name lookups use only IPv4\&.
+.RE
+.PP
+\fB\-6\fR
+.RS 4
+Name lookups use only IPv6\&.
+.RE
+.PP
+\fB\-d\fR, \fB\-\-debug\fR
+.RS 4
+Enable debugging output (Python smtplib messages)\&.
+.RE
+.PP
+\fB\-\-disable\-pin\fR
+.RS 4
+Don\(cqt check the server\(cqs public key against a list of known pins\&. You can use this if the server\(cqs public key has changed and this program hasn\(cqt been updated yet\&.
+.RE
+.PP
+\fB\-e\fR, \fB\-\-email\fR=\fIADDRESS\fR
+.RS 4
+Send mail to
+\fIADDRESS\fR
+(default is "flashproxyreg\&.a at gmail\&.com")\&.
+.RE
+.PP
+\fB\-\-facilitator\-pubkey\fR=\fIFILENAME\fR
+.RS 4
+Encrypt registrations to the given PEM\-formatted public key (default built\-in)\&.
+.RE
+.PP
+\fB\-h\fR, \fB\-\-help\fR
+.RS 4
+Display help message and exit\&.
+.RE
+.PP
+\fB\-s\fR, \fB\-\-smtp\fR=\fIHOST\fR[:\fIPORT\fR]
+.RS 4
+Use the given SMTP server (default is "gmail\-smtp\-in\&.l\&.google\&.com:25")\&.
+.RE
+.PP
+\fB\-\-transport\fR=\fITRANSPORT\fR
+.RS 4
+Registrations include the fact that you intend to use the given
+\fITRANSPORT\fR
+(default "websocket")\&.
+.RE
+.PP
+\fB\-\-unsafe\-logging\fR
+.RS 4
+Don\(cqt scrub IP addresses from logs\&.
+.RE
+.SH "SEE ALSO"
+.sp
+\fBhttp://crypto\&.stanford\&.edu/flashproxy/\fR
+.sp
+\fBhttps://www\&.torproject\&.org/docs/pluggable\-transports\&.html\&.en\fR
+.SH "BUGS"
+.sp
+Please report using \fBhttps://trac\&.torproject\&.org/projects/tor\fR\&.
diff --git a/doc/flashproxy-reg-email.1.txt b/doc/flashproxy-reg-email.1.txt
new file mode 100644
index 0000000..1adc242
--- /dev/null
+++ b/doc/flashproxy-reg-email.1.txt
@@ -0,0 +1,75 @@
+// This file is asciidoc source code.
+// To generate manpages, use the a2x command.
+// This one has a long name, if you don't change the
+// default length parameter it will be truncated, use:
+// a2x --no-xmllint --xsltproc-opts "--stringparam man.th.title.max.length 23" -d manpage -f manpage flashproxy-reg-email.1.txt
+FLASHPROXY-REG-EMAIL(1)
+=======================
+
+NAME
+----
+flashproxy-reg-email - Register with a facilitator using the email method
+
+SYNOPSIS
+--------
+**flashproxy-reg-email** [__OPTIONS__] [__REMOTE__][:__PORT__]
+
+DESCRIPTION
+-----------
+Register with a flash proxy facilitator through email. Makes a STARTTLS
+connection to an SMTP server and sends mail with a client IP address to a
+designated address. By default the remote address registered is
+":9000" (the external IP address is guessed based on the SMTP server's
+response).
+
+Using an SMTP server or email address other than the defaults will not work
+unless you have made special arrangements to connect them to a facilitator.
+
+The email address is not polled continually. After running the program,
+it may take up to a minute for the registration to be recognized.
+
+This program requires the M2Crypto library for Python.
+
+OPTIONS
+-------
+**-4**::
+ Name lookups use only IPv4.
+
+**-6**::
+ Name lookups use only IPv6.
+
+**-d**, **--debug**::
+ Enable debugging output (Python smtplib messages).
+
+**--disable-pin**::
+ Don't check the server's public key against a list of known pins.
+ You can use this if the server's public key has changed and this
+ program hasn't been updated yet.
+
+**-e**, **--email**=__ADDRESS__::
+ Send mail to __ADDRESS__ (default is "flashproxyreg.a at gmail.com").
+
+**--facilitator-pubkey**=__FILENAME__::
+ Encrypt registrations to the given PEM-formatted public key (default built-in).
+
+**-h**, **--help**::
+ Display help message and exit.
+
+**-s**, **--smtp**=__HOST__[:__PORT__]::
+ Use the given SMTP server (default is "gmail-smtp-in.l.google.com:25").
+
+**--transport**=__TRANSPORT__::
+ Registrations include the fact that you intend to use the given __TRANSPORT__ (default "websocket").
+
+**--unsafe-logging**::
+ Don't scrub IP addresses from logs.
+
+SEE ALSO
+--------
+**http://crypto.stanford.edu/flashproxy/**
+
+**https://www.torproject.org/docs/pluggable-transports.html.en**
+
+BUGS
+----
+Please report using **https://trac.torproject.org/projects/tor**.
diff --git a/doc/flashproxy-reg-http.1 b/doc/flashproxy-reg-http.1
new file mode 100644
index 0000000..ea88705
--- /dev/null
+++ b/doc/flashproxy-reg-http.1
@@ -0,0 +1,78 @@
+'\" t
+.\" Title: flashproxy-reg-http
+.\" Author: [FIXME: author] [see http://docbook.sf.net/el/author]
+.\" Generator: DocBook XSL Stylesheets v1.78.1 <http://docbook.sf.net/>
+.\" Date: 10/10/2013
+.\" Manual: \ \&
+.\" Source: \ \&
+.\" Language: English
+.\"
+.TH "FLASHPROXY\-REG\-HTTP" "1" "10/10/2013" "\ \&" "\ \&"
+.\" -----------------------------------------------------------------
+.\" * Define some portability stuff
+.\" -----------------------------------------------------------------
+.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+.\" http://bugs.debian.org/507673
+.\" http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html
+.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+.ie \n(.g .ds Aq \(aq
+.el .ds Aq '
+.\" -----------------------------------------------------------------
+.\" * set default formatting
+.\" -----------------------------------------------------------------
+.\" disable hyphenation
+.nh
+.\" disable justification (adjust text to left margin only)
+.ad l
+.\" -----------------------------------------------------------------
+.\" * MAIN CONTENT STARTS HERE *
+.\" -----------------------------------------------------------------
+.SH "NAME"
+flashproxy-reg-http \- Register with a facilitator using the HTTP method
+.SH "SYNOPSIS"
+.sp
+\fBflashproxy\-reg\-http\fR [\fIOPTIONS\fR] [\fIREMOTE\fR][:\fIPORT\fR]
+.SH "DESCRIPTION"
+.sp
+Register with a flash proxy facilitator using an HTTP POST\&. By default the remote address registered is ":9000"\&.
+.SH "OPTIONS"
+.PP
+\fB\-4\fR
+.RS 4
+Name lookups use only IPv4\&.
+.RE
+.PP
+\fB\-6\fR
+.RS 4
+Name lookups use only IPv6\&.
+.RE
+.PP
+\fB\-f\fR, \fB\-\-facilitator\fR=\fIURL\fR
+.RS 4
+Register with the given facilitator (default "https://fp\-facilitator\&.org/")\&.
+.RE
+.PP
+\fB\-h\fR, \fB\-\-help\fR
+.RS 4
+Display help message and exit\&.
+.RE
+.PP
+\fB\-\-transport\fR=\fITRANSPORT\fR
+.RS 4
+Registrations include the fact that you intend to use the given
+\fITRANSPORT\fR
+(default "websocket")\&.
+.RE
+.PP
+\fB\-\-unsafe\-logging\fR
+.RS 4
+Don\(cqt scrub IP addresses from logs\&.
+.RE
+.SH "SEE ALSO"
+.sp
+\fBhttp://crypto\&.stanford\&.edu/flashproxy/\fR
+.sp
+\fBhttps://www\&.torproject\&.org/docs/pluggable\-transports\&.html\&.en\fR
+.SH "BUGS"
+.sp
+Please report using \fBhttps://trac\&.torproject\&.org/projects/tor\fR\&.
diff --git a/doc/flashproxy-reg-http.1.txt b/doc/flashproxy-reg-http.1.txt
new file mode 100644
index 0000000..cae719c
--- /dev/null
+++ b/doc/flashproxy-reg-http.1.txt
@@ -0,0 +1,50 @@
+// This file is asciidoc source code.
+// To generate manpages, use the a2x command.
+// This one has a long name, if you don't change the
+// default length parameter it will be truncated, use:
+// a2x --no-xmllint --xsltproc-opts "--stringparam man.th.title.max.length 22" -d manpage -f manpage flashproxy-reg-http.1.txt
+FLASHPROXY-REG-HTTP(1)
+======================
+
+NAME
+----
+flashproxy-reg-http - Register with a facilitator using the HTTP method
+
+SYNOPSIS
+--------
+**flashproxy-reg-http** [__OPTIONS__] [__REMOTE__][:__PORT__]
+
+DESCRIPTION
+-----------
+Register with a flash proxy facilitator using an HTTP POST. By default the
+remote address registered is ":9000".
+
+OPTIONS
+-------
+**-4**::
+ Name lookups use only IPv4.
+
+**-6**::
+ Name lookups use only IPv6.
+
+**-f**, **--facilitator**=__URL__::
+ Register with the given facilitator (default "https://fp-facilitator.org/").
+
+**-h**, **--help**::
+ Display help message and exit.
+
+**--transport**=__TRANSPORT__::
+ Registrations include the fact that you intend to use the given __TRANSPORT__ (default "websocket").
+
+**--unsafe-logging**::
+ Don't scrub IP addresses from logs.
+
+SEE ALSO
+--------
+**http://crypto.stanford.edu/flashproxy/**
+
+**https://www.torproject.org/docs/pluggable-transports.html.en**
+
+BUGS
+----
+Please report using **https://trac.torproject.org/projects/tor**.
diff --git a/doc/flashproxy-reg-url.1 b/doc/flashproxy-reg-url.1
new file mode 100644
index 0000000..ec3cf1d
--- /dev/null
+++ b/doc/flashproxy-reg-url.1
@@ -0,0 +1,87 @@
+'\" t
+.\" Title: flashproxy-reg-url
+.\" Author: [FIXME: author] [see http://docbook.sf.net/el/author]
+.\" Generator: DocBook XSL Stylesheets v1.78.1 <http://docbook.sf.net/>
+.\" Date: 10/09/2013
+.\" Manual: \ \&
+.\" Source: \ \&
+.\" Language: English
+.\"
+.TH "FLASHPROXY\-REG\-URL" "1" "10/09/2013" "\ \&" "\ \&"
+.\" -----------------------------------------------------------------
+.\" * Define some portability stuff
+.\" -----------------------------------------------------------------
+.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+.\" http://bugs.debian.org/507673
+.\" http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html
+.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+.ie \n(.g .ds Aq \(aq
+.el .ds Aq '
+.\" -----------------------------------------------------------------
+.\" * set default formatting
+.\" -----------------------------------------------------------------
+.\" disable hyphenation
+.nh
+.\" disable justification (adjust text to left margin only)
+.ad l
+.\" -----------------------------------------------------------------
+.\" * MAIN CONTENT STARTS HERE *
+.\" -----------------------------------------------------------------
+.SH "NAME"
+flashproxy-reg-url \- Register with a facilitator using an indirect URL
+.SH "SYNOPSIS"
+.sp
+\fBflashproxy\-reg\-url\fR [\fIOPTIONS\fR] \fIREMOTE\fR[:\fIPORT\fR]
+.SH "DESCRIPTION"
+.sp
+Print a URL, which, when retrieved, will cause the client address \fIREMOTE\fR[:\fIPORT\fR] to be registered with the flash proxy facilitator\&. The default \fIPORT\fR is 9000\&.
+.SH "OPTIONS"
+.PP
+\fB\-f\fR, \fB\-\-facilitator\fR=\fIURL\fR
+.RS 4
+Register with the given facilitator (by default "https://fp\-facilitator\&.org/")\&.
+.RE
+.PP
+\fB\-\-facilitator\-pubkey\fR=\fIFILENAME\fR
+.RS 4
+Encrypt registrations to the given PEM\-formatted public key (default built\-in)\&.
+.RE
+.PP
+\fB\-h\fR, \fB\-\-help\fR
+.RS 4
+Display help message and exit\&.
+.RE
+.PP
+\fB\-\-transport\fR=\fITRANSPORT\fR
+.RS 4
+Registrations include the fact that you intend to use the given
+\fITRANSPORT\fR
+(default "websocket")\&.
+.RE
+.SH "EXAMPLE"
+.sp
+Say you wish to register 192\&.0\&.2\&.1:9000\&. Run
+.sp
+.if n \{\
+.RS 4
+.\}
+.nf
+\&./flashproxy\-reg\-url 192\&.0\&.2\&.1:9000
+.fi
+.if n \{\
+.RE
+.\}
+.sp
+The program should output a long string looking something like
+.sp
+https://fp\-facilitator\&.org/reg/0labtDob545HeKpLZ8LqGeOi\-OK7HXoQvfQzj0P2pjh1NrCKNDaPe91zo\&.\&.\&.
+.sp
+Copy this string and paste it into any URL fetching website or program\&. Once the URL is retrieved your address will be registered with the facilitator\&.
+.SH "SEE ALSO"
+.sp
+\fBhttp://crypto\&.stanford\&.edu/flashproxy/\fR
+.sp
+\fBhttps://www\&.torproject\&.org/docs/pluggable\-transports\&.html\&.en\fR
+.SH "BUGS"
+.sp
+Please report using \fBhttps://trac\&.torproject\&.org/projects/tor\fR\&.
diff --git a/doc/flashproxy-reg-url.1.txt b/doc/flashproxy-reg-url.1.txt
new file mode 100644
index 0000000..42757c9
--- /dev/null
+++ b/doc/flashproxy-reg-url.1.txt
@@ -0,0 +1,60 @@
+// This file is asciidoc source code.
+// To generate manpages, use the a2x command.
+// This one has a long name, if you don't change the
+// default length parameter it will be truncated, use:
+// a2x --no-xmllint --xsltproc-opts "--stringparam man.th.title.max.length 23" -d manpage -f manpage flashproxy-reg-url.1.txt
+FLASHPROXY-REG-URL(1)
+=====================
+
+NAME
+----
+flashproxy-reg-url - Register with a facilitator using an indirect URL
+
+SYNOPSIS
+--------
+**flashproxy-reg-url** [__OPTIONS__] __REMOTE__[:__PORT__]
+
+DESCRIPTION
+-----------
+Print a URL, which, when retrieved, will cause the client address
+__REMOTE__[:__PORT__] to be registered with the flash proxy facilitator. The
+default __PORT__ is 9000.
+
+OPTIONS
+-------
+**-f**, **--facilitator**=__URL__::
+ Register with the given facilitator (default "https://fp-facilitator.org/").
+
+**--facilitator-pubkey**=__FILENAME__::
+ Encrypt registrations to the given PEM-formatted public key (default built-in).
+
+**-h**, **--help**::
+ Display help message and exit.
+
+**--transport**=__TRANSPORT__::
+ Registrations include the fact that you intend to use the given __TRANSPORT__ (default "websocket").
+
+EXAMPLE
+-------
+Say you wish to register 192.0.2.1:9000. Run
+
+...................................
+./flashproxy-reg-url 192.0.2.1:9000
+...................................
+
+The program should output a long string looking something like
+
+https://fp-facilitator.org/reg/0labtDob545HeKpLZ8LqGeOi-OK7HXoQvfQzj0P2pjh1NrCKNDaPe91zo\...
+
+Copy this string and paste it into any URL fetching website or program.
+Once the URL is retrieved your address will be registered with the facilitator.
+
+SEE ALSO
+--------
+**http://crypto.stanford.edu/flashproxy/**
+
+**https://www.torproject.org/docs/pluggable-transports.html.en**
+
+BUGS
+----
+Please report using **https://trac.torproject.org/projects/tor**.
diff --git a/experiments/README b/experiments/README
new file mode 100644
index 0000000..57f8af7
--- /dev/null
+++ b/experiments/README
@@ -0,0 +1,48 @@
+This directory contains scripts for testing and benchmarking the flash
+proxy.
+
+== Preparation
+
+You need to have installed certain software before running the tests.
+ Firefox 8.0.1
+ socat
+ Wget
+ Python
+ thttpd
+ websockify
+socat, Wget, and Python are easily installed on most GNU/Linux
+distributions. thttpd can be compiled from the packages at
+http://acme.com/software/thttpd/. websockify is from
+https://github.com/kanaka/websockify/. The old Firefox is from
+http://download.mozilla.org/?product=firefox-8.0.1&os=linux&lang=en-US.
+
+Before compiling thttpd, increade IDLE_READ_TIMEOUT in config.h to a
+high value (several thousand). This is because some tests wait a long
+time between making a connection and sending an HTTP request.
+
+Firefox versions 9 and 10 will not work; these versions have a change to
+the -no-remote option that prevents the tests from running. This is
+supposed to be fixed with a -new-instance option in version 12.
+
+You need to create some dedicated Firefox profiles. Create profiles
+named flashexp1 and flashexp2 by running
+ firefox -ProfileManager -no-remote
+Start the browsers with
+ firefox -P flashexp1 -no-remote &
+ firefox -P flashexp2 -no-remote &
+and in each one, set this about:config variable:
+ browser.link.open_newwindow=1 (default is 3)
+This allows the scripts to clear the contents of a tab and replace them
+with another page.
+
+I personally run these tests in an Arch Linux VM.
+ useradd -m user
+ passwd user
+ pacman -Sy
+ pacman -Su
+ pacman -S firefox socat python2 xorg xorg-xinit xterm flashplugin gcc make
+Download thttpd, compile it (you have to rename the getline function to
+avoid a naming conflict), and install it in /usr/local/bin. Symlink
+/usr/bin/python to /usr/bin/python2. Also you have to install the
+ttf-ms-fonts package from the AUR for text to show up in Flash Player.
+Add a window manager, run "startx", and you should be set.
diff --git a/experiments/client-extract.py b/experiments/client-extract.py
new file mode 100755
index 0000000..4df242c
--- /dev/null
+++ b/experiments/client-extract.py
@@ -0,0 +1,50 @@
+#!/usr/bin/env python
+
+import datetime
+import getopt
+import re
+import sys
+
+def usage(f = sys.stdout):
+ print >> f, """\
+Usage: %s [INPUTFILE]
+Extract client connections from a facilitator log. Each output line is
+ date\tcount\n
+where count is the number of client requests in that hour.
+
+ -h, --help show this help.
+""" % sys.argv[0]
+
+opts, args = getopt.gnu_getopt(sys.argv[1:], "h", ["help"])
+for o, a in opts:
+ if o == "-h" or o == "--help":
+ usage()
+ sys.exit()
+
+if len(args) == 0:
+ input_file = sys.stdin
+elif len(args) == 1:
+ input_file = open(args[0])
+else:
+ usage()
+ sys.exit()
+
+prev_output = None
+count = 0.0
+
+for line in input_file:
+ m = re.match(r'^(\d+-\d+-\d+ \d+:\d+:\d+) client', line)
+ if not m:
+ continue
+ date_str, = m.groups()
+ date = datetime.datetime.strptime(date_str, "%Y-%m-%d %H:%M:%S")
+
+ count += 1
+
+ rounded_date = date.replace(minute=0, second=0, microsecond=0)
+ prev_output = prev_output or rounded_date
+ if prev_output is None or rounded_date != prev_output:
+ avg = float(count)
+ print date.strftime("%Y-%m-%d %H:%M:%S") + "\t" + "%.2f" % avg
+ prev_output = rounded_date
+ count = 0.0
diff --git a/experiments/client-graph.py b/experiments/client-graph.py
new file mode 100755
index 0000000..22b1afb
--- /dev/null
+++ b/experiments/client-graph.py
@@ -0,0 +1,96 @@
+#!/usr/bin/env python
+
+# Makes a graph of flash proxy client counts from a facilitator log.
+
+import datetime
+import getopt
+import re
+import sys
+
+import matplotlib
+import matplotlib.pyplot as plt
+import numpy as np
+
+START_DATE = datetime.datetime(2012, 12, 15)
+
+def usage(f = sys.stdout):
+ print >> f, """\
+Usage: %s -o OUTPUT [INPUTFILE]
+Makes a graph of flash proxy counts from a facilitator log.
+
+ -h, --help show this help.
+ -o, --output=OUTPUT output file name (required).\
+""" % sys.argv[0]
+
+output_file_name = None
+
+opts, args = getopt.gnu_getopt(sys.argv[1:], "ho:", ["help", "output="])
+for o, a in opts:
+ if o == "-h" or o == "--help":
+ usage()
+ sys.exit()
+ elif o == "-o" or o == "--output":
+ output_file_name = a
+
+if not output_file_name:
+ usage()
+ sys.exit()
+
+if len(args) == 0:
+ input_file = sys.stdin
+elif len(args) == 1:
+ input_file = open(args[0])
+else:
+ usage()
+ sys.exit()
+
+def format_date(d, pos=None):
+ d = matplotlib.dates.num2date(d)
+ return d.strftime("%B %d")
+
+def timedelta_to_seconds(delta):
+ return delta.days * (24 * 60 * 60) + delta.seconds + delta.microseconds / 1000000.0
+
+prev_output = None
+count = 0
+
+data = []
+
+for line in input_file:
+ m = re.match(r'^(\d+-\d+-\d+ \d+:\d+:\d+) client', line)
+ if not m:
+ continue
+ date_str, = m.groups()
+ date = datetime.datetime.strptime(date_str, "%Y-%m-%d %H:%M:%S")
+
+ if date < START_DATE:
+ continue
+
+ count += 1
+
+ rounded_date = date.replace(minute=0, second=0, microsecond=0)
+ prev_output = prev_output or rounded_date
+ if prev_output is None or rounded_date != prev_output:
+ delta = timedelta_to_seconds(date - prev_output)
+ # avg = float(count) / delta
+ avg = float(count)
+ data.append((date, avg))
+ print date, avg
+ prev_output = rounded_date
+ count = 0
+
+data = np.array(data)
+
+fig = plt.figure()
+ax = fig.add_axes([0.10, 0.30, 0.88, 0.60])
+ax.set_ylabel(u"Number of clients", fontsize=8)
+fig.set_size_inches((8, 3))
+
+ax.tick_params(direction="out", top="off", right="off")
+ax.set_frame_on(False)
+ax.xaxis.set_major_formatter(matplotlib.ticker.FuncFormatter(format_date))
+fig.autofmt_xdate()
+
+plt.fill_between(data[:,0], data[:,1], linewidth=0, color="black")
+
+fig.savefig(output_file_name)
diff --git a/experiments/client-graph.r b/experiments/client-graph.r
new file mode 100644
index 0000000..a17647f
--- /dev/null
+++ b/experiments/client-graph.r
@@ -0,0 +1,5 @@
+library(ggplot2)
+x <- read.delim("client.dat", header=FALSE, col.names=c("date", "count"), colClasses=c("POSIXct", "numeric"))
+
+png("client-count.png", width=720, height=480)
+qplot(date, data=x, geom="bar", weight=count, binwidth=86400, ylab="client requests per day")
diff --git a/experiments/common.sh b/experiments/common.sh
new file mode 100644
index 0000000..0d108dd
--- /dev/null
+++ b/experiments/common.sh
@@ -0,0 +1,59 @@
+# This file contains common variables and subroutines used by the experiment
+# scripts.
+
+FLASHPROXY_DIR="$(dirname $BASH_SOURCE)/.."
+
+FIREFOX=firefox
+SOCAT=socat
+WEBSOCKIFY=websockify
+THTTPD=thttpd
+TOR=tor
+
+visible_sleep() {
+ N="$1"
+ echo -n "sleep $N"
+ while [ "$N" -gt 0 ]; do
+ sleep 1
+ N=$((N-1))
+ echo -ne "\rsleep $N "
+ done
+ echo -ne "\n"
+}
+
+ensure_browser_started() {
+ local PROFILE="$1"
+ ("$FIREFOX" -P "$PROFILE" -remote "ping()" || ("$FIREFOX" -P "$PROFILE" -no-remote & visible_sleep 5)) 2>/dev/null
+}
+
+browser_clear() {
+ local PROFILE="$1"
+ ("$FIREFOX" -P "$PROFILE" -remote "ping()" && "$FIREFOX" -P "$PROFILE" -remote "openurl(about:blank)" &) 2>/dev/null
+}
+
+browser_goto() {
+ local PROFILE="$1"
+ local URL="$2"
+ ensure_browser_started "$PROFILE"
+ "$FIREFOX" -P "$PROFILE" -remote "openurl($URL)" 2>/dev/null
+}
+
+# Run a command and get the "real" part of time(1) output as a number of
+# seconds.
+real_time() {
+ # Make a spare copy of stderr (fd 2).
+ exec 3>&2
+ # Point the subcommand's stderr to our copy (fd 3), and extract the
+ # original stderr (fd 2) output of time.
+ (time -p eval "$@" 2>&3) |& tail -n 3 | head -n 1 | awk '{print $2}'
+}
+
+# Repeat a subcommand N times.
+repeat() {
+ local N
+ N="$1"
+ shift
+ while [ $N -gt 0 ]; do
+ eval "$@"
+ N=$((N-1))
+ done
+}
diff --git a/experiments/exercise/exercise.sh b/experiments/exercise/exercise.sh
new file mode 100755
index 0000000..efefde7
--- /dev/null
+++ b/experiments/exercise/exercise.sh
@@ -0,0 +1,34 @@
+#!/bin/bash
+
+# This script registers with the flash proxy facilitator, tries to download
+# check.torproject.org, and saves a timestamped log file.
+
+FLASHPROXY_DIR="$HOME/flashproxy"
+TOR="$HOME/tor/src/or/tor"
+LOCAL_PORT=1080
+REMOTE_PORT=7070
+
+declare -a PIDS_TO_KILL
+stop() {
+ if [ -n "${PIDS_TO_KILL[*]}" ]; then
+ echo "Kill pids ${PIDS_TO_KILL[@]}."
+ kill "${PIDS_TO_KILL[@]}"
+ fi
+ exit
+}
+trap stop EXIT
+
+date
+
+cd "$FLASHPROXY_DIR"
+./flashproxy-client --external --register ":$LOCAL_PORT" ":$REMOTE_PORT" &
+PIDS_TO_KILL+=($!)
+
+sleep 20
+
+"$TOR" ClientTransportPlugin "flashproxy socks4 127.0.0.1:$LOCAL_PORT" UseBridges 1 Bridge "flashproxy 0.0.1.0:1" LearnCircuitBuildTimeout 0 CircuitBuildTimeout 60 &
+PIDS_TO_KILL+=($!)
+
+sleep 60
+
+curl --retry 5 --socks4a 127.0.0.1:9050 http://check.torproject.org/
diff --git a/experiments/exercise/flashproxy-exercise.sh b/experiments/exercise/flashproxy-exercise.sh
new file mode 100755
index 0000000..3e81306
--- /dev/null
+++ b/experiments/exercise/flashproxy-exercise.sh
@@ -0,0 +1,11 @@
+#!/bin/sh
+
+# Usage (for example in crontab for hourly tests):
+# 0 * * * * cd /path/flashproxy-exercise && ./flashproxy-exercise.sh
+
+LOGDIR=log
+DATE=$(date +"%Y-%m-%d-%H:%M")
+LOG="$LOGDIR/log-$DATE"
+
+mkdir -p "$LOGDIR"
+(./exercise.sh &> "$LOG") || cat "$LOG"
diff --git a/experiments/facilitator-graph.py b/experiments/facilitator-graph.py
new file mode 100755
index 0000000..3c7ef3e
--- /dev/null
+++ b/experiments/facilitator-graph.py
@@ -0,0 +1,92 @@
+#!/usr/bin/env python
+
+# Makes a graph of flash proxy counts from a facilitator log.
+
+import datetime
+import getopt
+import re
+import sys
+
+import matplotlib
+import matplotlib.pyplot as plt
+import numpy as np
+
+POLL_INTERVAL = 10.0
+
+def usage(f = sys.stdout):
+ print >> f, """\
+Usage: %s -o OUTPUT [INPUTFILE]
+Makes a graph of flash proxy counts from a facilitator log.
+
+ -h, --help show this help.
+ -o, --output=OUTPUT output file name (required).\
+""" % sys.argv[0]
+
+output_file_name = None
+
+opts, args = getopt.gnu_getopt(sys.argv[1:], "ho:", ["help", "output="])
+for o, a in opts:
+ if o == "-h" or o == "--help":
+ usage()
+ sys.exit()
+ elif o == "-o" or o == "--output":
+ output_file_name = a
+
+if not output_file_name:
+ usage()
+ sys.exit()
+
+if len(args) == 0:
+ input_file = sys.stdin
+elif len(args) == 1:
+ input_file = open(args[0])
+else:
+ usage()
+ sys.exit()
+
+def format_date(d, pos=None):
+ d = matplotlib.dates.num2date(d)
+ return d.strftime("%B %d")
+
+def timedelta_to_seconds(delta):
+ return delta.days * (24 * 60 * 60) + delta.seconds + delta.microseconds / 1000000.0
+
+prev_output = None
+count = 0
+
+data = []
+
+for line in input_file:
+ m = re.match(r'^(\d+-\d+-\d+ \d+:\d+:\d+) proxy gets', line)
+ if not m:
+ continue
+ date_str, = m.groups()
+ date = datetime.datetime.strptime(date_str, "%Y-%m-%d %H:%M:%S")
+
+ count += 1
+
+ rounded_date = date.replace(minute=0, second=0, microsecond=0)
+ prev_output = prev_output or rounded_date
+ if prev_output is None or rounded_date != prev_output:
+ delta = timedelta_to_seconds(date - prev_output)
+ avg = float(count) / delta * POLL_INTERVAL
+ data.append((date, avg))
+ print date, avg
+ prev_output = rounded_date
+ count = 0
+
+data = np.array(data)
+
+fig = plt.figure()
+ax = fig.add_axes([0.10, 0.30, 0.88, 0.60])
+ax.set_ylabel(u"Number of proxies", fontsize=8)
+fig.set_size_inches((8, 3))
+
+ax.tick_params(direction="out", top="off", right="off")
+ax.set_frame_on(False)
+ax.xaxis.set_major_formatter(matplotlib.ticker.FuncFormatter(format_date))
+fig.autofmt_xdate()
+
+plt.fill_between(data[:,0], data[:,1], linewidth=0, color="black")
+
+fig.savefig(output_file_name)
diff --git a/experiments/proxy-extract.py b/experiments/proxy-extract.py
new file mode 100755
index 0000000..e8b02ce
--- /dev/null
+++ b/experiments/proxy-extract.py
@@ -0,0 +1,82 @@
+#!/usr/bin/env python
+
+import datetime
+import getopt
+import re
+import sys
+
+def usage(f = sys.stdout):
+ print >> f, """\
+Usage: %s [INPUTFILE]
+Extract proxy connections from a facilitator log. Each output line is
+ date\tcount\n
+where count is the approximate poll interval in effect at date.
+
+ -h, --help show this help.
+""" % sys.argv[0]
+
+opts, args = getopt.gnu_getopt(sys.argv[1:], "h", ["help"])
+for o, a in opts:
+ if o == "-h" or o == "--help":
+ usage()
+ sys.exit()
+
+if len(args) == 0:
+ input_file = sys.stdin
+elif len(args) == 1:
+ input_file = open(args[0])
+else:
+ usage()
+ sys.exit()
+
+def timedelta_to_seconds(delta):
+ return delta.days * (24 * 60 * 60) + delta.seconds + delta.microseconds / 1000000.0
+
+# commit 49de7bf689ee989997a1edbf2414a7bdbc2164f9
+# Author: David Fifield <david at bamsoftware.com>
+# Date: Thu Jan 3 21:01:39 2013 -0800
+#
+# Bump poll interval from 10 s to 60 s.
+#
+# commit 69d429db12cedc90dac9ccefcace80c86af7eb51
+# Author: David Fifield <david at bamsoftware.com>
+# Date: Tue Jan 15 14:02:02 2013 -0800
+#
+# Increase facilitator_poll_interval from 1 m to 10 m.
+
+BEGIN_60S = datetime.datetime(2013, 1, 3, 21, 0, 0)
+BEGIN_600S = datetime.datetime(2013, 1, 15, 14, 0, 0)
+
+# Proxies refresh themselves once a day, so interpolate across a day when the
+# polling interval historically changed.
+def get_poll_interval(date):
+ if date < BEGIN_60S:
+ return 10
+ elif BEGIN_60S <= date < BEGIN_60S + datetime.timedelta(1):
+ return timedelta_to_seconds(date-BEGIN_60S) / timedelta_to_seconds(datetime.timedelta(1)) * (60-10) + 10
+ elif date < BEGIN_600S:
+ return 60
+ elif BEGIN_600S <= date < BEGIN_600S + datetime.timedelta(1):
+ return timedelta_to_seconds(date-BEGIN_600S) / timedelta_to_seconds(datetime.timedelta(1)) * (600-60) + 60
+ else:
+ return 600
+
+prev_output = None
+count = 0.0
+
+for line in input_file:
+ m = re.match(r'^(\d+-\d+-\d+ \d+:\d+:\d+) proxy gets', line)
+ if not m:
+ continue
+ date_str, = m.groups()
+ date = datetime.datetime.strptime(date_str, "%Y-%m-%d %H:%M:%S")
+
+ count += get_poll_interval(date)
+
+ rounded_date = date.replace(minute=0, second=0, microsecond=0)
+ prev_output = prev_output or rounded_date
+ if prev_output is None or rounded_date != prev_output:
+ avg = float(count) / 10.0
+ print date.strftime("%Y-%m-%d %H:%M:%S") + "\t" + "%.2f" % avg
+ prev_output = rounded_date
+ count = 0.0
diff --git a/experiments/proxy-graph.r b/experiments/proxy-graph.r
new file mode 100644
index 0000000..6b3e1cc
--- /dev/null
+++ b/experiments/proxy-graph.r
@@ -0,0 +1,5 @@
+library(ggplot2)
+x <- read.delim("proxy.dat", header=FALSE, col.names=c("date", "interval"), colClasses=c("POSIXct", "numeric"))
+
+png("proxy-count.png", width=720, height=480)
+qplot(date, data=x, geom="bar", weight=interval/10, binwidth=86400, ylab="proxy requests per day")
diff --git a/experiments/switching/local-http-alternating.sh b/experiments/switching/local-http-alternating.sh
new file mode 100755
index 0000000..83e33eb
--- /dev/null
+++ b/experiments/switching/local-http-alternating.sh
@@ -0,0 +1,66 @@
+#!/bin/bash
+
+# Usage: ./local-http-alternating.sh [OUTPUT_FILENAME]
+#
+# Tests a download over alternating flash proxies. If OUTPUT_FILENAME is
+# supplied, appends the time measurement to that file.
+
+. ../common.sh
+
+PROFILE_1=flashexp1
+PROFILE_2=flashexp2
+PROXY_URL="http://127.0.0.1:8000/embed.html?facilitator=127.0.0.1:9002&ratelimit=off"
+DATA_FILE_NAME="$FLASHPROXY_DIR/dump"
+OUTPUT_FILENAME="$1"
+
+# Declare an array.
+declare -a PIDS_TO_KILL
+stop() {
+ browser_clear "$PROFILE_1"
+ browser_clear "$PROFILE_2"
+ if [ -n "${PIDS_TO_KILL[*]}" ]; then
+ echo "Kill pids ${PIDS_TO_KILL[@]}."
+ kill "${PIDS_TO_KILL[@]}"
+ fi
+ echo "Delete data file."
+ rm -f "$DATA_FILE_NAME"
+ exit
+}
+trap stop EXIT
+
+echo "Create data file."
+dd if=/dev/null of="$DATA_FILE_NAME" bs=1M seek=500 2>/dev/null || exit
+
+echo "Start web server."
+"$THTTPD" -D -d "$FLASHPROXY_DIR" -p 8000 &
+PIDS_TO_KILL+=($!)
+
+echo "Start facilitator."
+"$FLASHPROXY_DIR"/facilitator -d --relay 127.0.0.1:8000 >/dev/null &
+PIDS_TO_KILL+=($!)
+visible_sleep 5
+
+echo "Start client transport plugin."
+"$FLASHPROXY_DIR"/flashproxy-client --register --facilitator 127.0.0.1:9002 >/dev/null &
+PIDS_TO_KILL+=($!)
+visible_sleep 1
+
+echo "Start browsers."
+ensure_browser_started "$PROFILE_1"
+ensure_browser_started "$PROFILE_2"
+
+./proxy-loop.sh "$PROXY_URL" "$PROFILE_1" "$PROFILE_2" >/dev/null 2>&1 &
+PIDS_TO_KILL+=($!)
+visible_sleep 2
+
+echo "Start socat."
+"$SOCAT" TCP-LISTEN:2000,reuseaddr,fork SOCKS4A:127.0.0.1:dummy:0,socksport=9001 &
+PIDS_TO_KILL+=($!)
+visible_sleep 2
+
+
+if [ -n "$OUTPUT_FILENAME" ]; then
+ real_time wget http://127.0.0.1:2000/dump --wait=0 --waitretry=0 -t 1000 -O /dev/null >> "$OUTPUT_FILENAME"
+else
+ real_time wget http://127.0.0.1:2000/dump --wait=0 --waitretry=0 -t 1000 -O /dev/null
+fi
diff --git a/experiments/switching/local-http-constant.sh b/experiments/switching/local-http-constant.sh
new file mode 100755
index 0000000..41c1a6e
--- /dev/null
+++ b/experiments/switching/local-http-constant.sh
@@ -0,0 +1,64 @@
+#!/bin/bash
+
+# Usage: ./local-http-constant.sh [OUTPUT_FILENAME]
+#
+# Tests a download over an uninterrupted flash proxy. If OUTPUT_FILENAME
+# is supplied, appends the time measurement to that file.
+
+. ../common.sh
+
+PROFILE_1=flashexp1
+PROFILE_2=flashexp2
+PROXY_URL="http://127.0.0.1:8000/embed.html?facilitator=127.0.0.1:9002&ratelimit=off"
+DATA_FILE_NAME="$FLASHPROXY_DIR/dump"
+OUTPUT_FILENAME="$1"
+
+# Declare an array.
+declare -a PIDS_TO_KILL
+stop() {
+ browser_clear "$PROFILE_1"
+ browser_clear "$PROFILE_2"
+ if [ -n "${PIDS_TO_KILL[*]}" ]; then
+ echo "Kill pids ${PIDS_TO_KILL[@]}."
+ kill "${PIDS_TO_KILL[@]}"
+ fi
+ echo "Delete data file."
+ rm -f "$DATA_FILE_NAME"
+ exit
+}
+trap stop EXIT
+
+echo "Create data file."
+dd if=/dev/null of="$DATA_FILE_NAME" bs=1M seek=500 2>/dev/null || exit
+
+echo "Start web server."
+"$THTTPD" -D -d "$FLASHPROXY_DIR" -p 8000 &
+PIDS_TO_KILL+=($!)
+
+echo "Start websockify."
+"$WEBSOCKIFY" -v 8001 127.0.0.1:8000 >/dev/null &
+PIDS_TO_KILL+=($!)
+
+echo "Start facilitator."
+"$FLASHPROXY_DIR"/facilitator -d --relay 127.0.0.1:8001 >/dev/null &
+PIDS_TO_KILL+=($!)
+visible_sleep 5
+
+echo "Start client transport plugin."
+"$FLASHPROXY_DIR"/flashproxy-client --register --facilitator 127.0.0.1:9002 >/dev/null &
+PIDS_TO_KILL+=($!)
+visible_sleep 1
+
+echo "Start browser."
+browser_goto "$PROFILE_1" "$PROXY_URL"
+
+echo "Start socat."
+"$SOCAT" TCP-LISTEN:2000,reuseaddr,fork SOCKS4A:127.0.0.1:dummy:0,socksport=9001 &
+PIDS_TO_KILL+=($!)
+visible_sleep 2
+
+if [ -n "$OUTPUT_FILENAME" ]; then
+ real_time wget http://127.0.0.1:2000/dump --wait=0 --waitretry=0 -t 1000 -O /dev/null >> "$OUTPUT_FILENAME"
+else
+ real_time wget http://127.0.0.1:2000/dump --wait=0 --waitretry=0 -t 1000 -O /dev/null
+fi
diff --git a/experiments/switching/proxy-loop.sh b/experiments/switching/proxy-loop.sh
new file mode 100755
index 0000000..5fa76a7
--- /dev/null
+++ b/experiments/switching/proxy-loop.sh
@@ -0,0 +1,40 @@
+#!/bin/bash
+
+# Runs overlapping flash proxy instances in a loop.
+# Usage: /proxy-loop.sh <URL> PROFILE1 PROFILE2
+
+# The profiles need to have the open_newwindow configuration option set
+# properly. See ../README.
+# browser.link.open_newwindow=1 (default is 3)
+
+. ../common.sh
+
+URL=$1
+PROFILE_1=$2
+PROFILE_2=$3
+
+# OVERLAP must be at most half of PERIOD.
+PERIOD=10
+OVERLAP=2
+
+ensure_browser_started "$PROFILE_1"
+browser_clear "$PROFILE_1"
+ensure_browser_started "$PROFILE_2"
+browser_clear "$PROFILE_2"
+
+sleep 1
+
+while true; do
+ echo "1 on"
+ firefox -P "$PROFILE_1" -remote "openurl($URL)"
+ sleep $OVERLAP
+ echo "2 off"
+ firefox -P "$PROFILE_2" -remote "openurl(about:blank)"
+ sleep $(($PERIOD - (2 * $OVERLAP)))
+ echo "2 on"
+ firefox -P "$PROFILE_2" -remote "openurl($URL)"
+ sleep $OVERLAP
+ echo "1 off"
+ firefox -P "$PROFILE_1" -remote "openurl(about:blank)"
+ sleep $(($PERIOD - (2 * $OVERLAP)))
+done
diff --git a/experiments/switching/remote-tor-alternating.sh b/experiments/switching/remote-tor-alternating.sh
new file mode 100755
index 0000000..08d6dc0
--- /dev/null
+++ b/experiments/switching/remote-tor-alternating.sh
@@ -0,0 +1,69 @@
+#!/bin/bash
+
+# Usage: ./remote-tor-alternating.sh [OUTPUT_FILENAME]
+#
+# Tests a Tor download over alternating flash proxies. If OUTPUT_FILENAME is
+# supplied, appends the time measurement to that file.
+
+. ../common.sh
+
+PROFILE_1=flashexp1
+PROFILE_2=flashexp2
+PROXY_URL="http://127.0.0.1:8000/embed.html?facilitator=127.0.0.1:9002&ratelimit=off"
+DATA_FILE_NAME="$FLASHPROXY_DIR/dump"
+OUTPUT_FILENAME="$1"
+
+# Declare an array.
+declare -a PIDS_TO_KILL
+stop() {
+ browser_clear "$PROFILE_1"
+ browser_clear "$PROFILE_2"
+ if [ -n "${PIDS_TO_KILL[*]}" ]; then
+ echo "Kill pids ${PIDS_TO_KILL[@]}."
+ kill "${PIDS_TO_KILL[@]}"
+ fi
+ echo "Delete data file."
+ rm -f "$DATA_FILE_NAME"
+ exit
+}
+trap stop EXIT
+
+echo "Start web server."
+"$THTTPD" -D -d "$FLASHPROXY_DIR" -p 8000 &
+PIDS_TO_KILL+=($!)
+
+echo "Start facilitator."
+"$FLASHPROXY_DIR"/facilitator -d --relay tor1.bamsoftware.com:9901 >/dev/null &
+PIDS_TO_KILL+=($!)
+visible_sleep 15
+
+echo "Start client transport plugin."
+"$FLASHPROXY_DIR"/flashproxy-client --register --facilitator 127.0.0.1:9002 >/dev/null &
+PIDS_TO_KILL+=($!)
+visible_sleep 1
+
+echo "Start Tor."
+"$TOR" -f "$FLASHPROXY_DIR"/torrc &
+PIDS_TO_KILL+=($!)
+
+echo "Start browsers."
+ensure_browser_started "$PROFILE_1"
+ensure_browser_started "$PROFILE_2"
+
+./proxy-loop.sh "$PROXY_URL" "$PROFILE_1" "$PROFILE_2" >/dev/null 2>&1 &
+PIDS_TO_KILL+=($!)
+
+# Let Tor bootstrap.
+visible_sleep 15
+
+repeat_download() {
+ until torify wget http://torperf.torproject.org/.5mbfile --wait=0 --waitretry=0 -c -t 1000 -O "$DATA_FILE_NAME"; do
+ echo "retrying"
+ done
+}
+
+if [ -n "$OUTPUT_FILENAME" ]; then
+ real_time repeat_download >> "$OUTPUT_FILENAME"
+else
+ real_time repeat_download
+fi
diff --git a/experiments/switching/remote-tor-constant.sh b/experiments/switching/remote-tor-constant.sh
new file mode 100755
index 0000000..33c54f4
--- /dev/null
+++ b/experiments/switching/remote-tor-constant.sh
@@ -0,0 +1,58 @@
+#!/bin/bash
+
+# Usage: ./remote-tor-constant.sh [OUTPUT_FILENAME]
+#
+# Tests a Tor download over an uninterrupted flash proxy. If OUTPUT_FILENAME is
+# supplied, appends the time measurement to that file.
+
+. ../common.sh
+
+PROFILE_1=flashexp1
+PROFILE_2=flashexp2
+PROXY_URL="http://127.0.0.1:8000/embed.html?facilitator=127.0.0.1:9002&ratelimit=off"
+DATA_FILE_NAME="$FLASHPROXY_DIR/dump"
+OUTPUT_FILENAME="$1"
+
+# Declare an array.
+declare -a PIDS_TO_KILL
+stop() {
+ browser_clear "$PROFILE_1"
+ if [ -n "${PIDS_TO_KILL[*]}" ]; then
+ echo "Kill pids ${PIDS_TO_KILL[@]}."
+ kill "${PIDS_TO_KILL[@]}"
+ fi
+ echo "Delete data file."
+ rm -f "$DATA_FILE_NAME"
+ exit
+}
+trap stop EXIT
+
+echo "Start web server."
+"$THTTPD" -D -d "$FLASHPROXY_DIR" -p 8000 &
+PIDS_TO_KILL+=($!)
+
+echo "Start facilitator."
+"$FLASHPROXY_DIR"/facilitator -d --relay tor1.bamsoftware.com:9901 >/dev/null &
+PIDS_TO_KILL+=($!)
+visible_sleep 15
+
+echo "Start client transport plugin."
+"$FLASHPROXY_DIR"/flashproxy-client --register --facilitator 127.0.0.1:9002 >/dev/null &
+PIDS_TO_KILL+=($!)
+visible_sleep 1
+
+echo "Start Tor."
+"$TOR" -f "$FLASHPROXY_DIR"/torrc &
+PIDS_TO_KILL+=($!)
+
+echo "Start browsers."
+browser_goto "$PROFILE_1" "$PROXY_URL"
+
+# Let Tor bootstrap.
+visible_sleep 15
+
+if [ -n "$OUTPUT_FILENAME" ]; then
+ real_time torify wget http://torperf.torproject.org/.5mbfile --wait=0 --waitretry=0 -c -t 1000 -O "$DATA_FILE_NAME" >> "$OUTPUT_FILENAME"
+else
+ real_time torify wget http://torperf.torproject.org/.5mbfile --wait=0 --waitretry=0 -c -t 1000 -O "$DATA_FILE_NAME"
+fi
diff --git a/experiments/switching/remote-tor-direct.sh b/experiments/switching/remote-tor-direct.sh
new file mode 100755
index 0000000..9b0740c
--- /dev/null
+++ b/experiments/switching/remote-tor-direct.sh
@@ -0,0 +1,37 @@
+#!/bin/bash
+
+# Usage: ./remote-tor-direct.sh [OUTPUT_FILENAME]
+#
+# Tests a Tor download without using a flash proxy. If OUTPUT_FILENAME is
+# supplied, appends the time measurement to that file.
+
+. ../common.sh
+
+DATA_FILE_NAME="$FLASHPROXY_DIR/dump"
+OUTPUT_FILENAME="$1"
+
+# Declare an array.
+declare -a PIDS_TO_KILL
+stop() {
+ if [ -n "${PIDS_TO_KILL[*]}" ]; then
+ echo "Kill pids ${PIDS_TO_KILL[@]}."
+ kill "${PIDS_TO_KILL[@]}"
+ fi
+ echo "Delete data file."
+ rm -f "$DATA_FILE_NAME"
+ exit
+}
+trap stop EXIT
+
+echo "Start Tor."
+"$TOR" -f torrc.bridge &
+PIDS_TO_KILL+=($!)
+
+# Let Tor bootstrap.
+visible_sleep 15
+
+if [ -n "$OUTPUT_FILENAME" ]; then
+ real_time torify wget http://torperf.torproject.org/.5mbfile --wait=0 --waitretry=0 -c -t 1000 -O "$DATA_FILE_NAME" >> "$OUTPUT_FILENAME"
+else
+ real_time torify wget http://torperf.torproject.org/.5mbfile --wait=0 --waitretry=0 -c -t 1000 -O "$DATA_FILE_NAME"
+fi
diff --git a/experiments/switching/switching-all.sh b/experiments/switching/switching-all.sh
new file mode 100755
index 0000000..f306152
--- /dev/null
+++ b/experiments/switching/switching-all.sh
@@ -0,0 +1,38 @@
+#!/bin/bash
+
+# Usage: ./switching-all.sh [-n NUM_ITERATIONS]
+#
+# Runs the switching experiment scripts several times and stores the results in
+# log files
+# local-http-constant-DATE.log
+# local-http-alternating-DATE.log
+# remote-tor-constant-DATE.log
+# remote-tor-alternating-DATE.log
+# where DATE is the current date.
+
+. ../common.sh
+
+NUM_ITERATIONS=1
+
+while getopts "n:" OPTNAME; do
+ if [ "$OPTNAME" == n ]; then
+ NUM_ITERATIONS="$OPTARG"
+ fi
+done
+
+DATE="$(date --iso)"
+
+> "local-http-constant-$DATE.log"
+repeat $NUM_ITERATIONS ./local-http-constant.sh "local-http-constant-$DATE.log"
+
+> "local-http-alternating-$DATE.log"
+repeat $NUM_ITERATIONS ./local-http-alternating.sh "local-http-alternating-$DATE.log"
+
+> "remote-tor-direct-$DATE.log"
+repeat $NUM_ITERATIONS ./remote-tor-direct.sh "remote-tor-direct-$DATE.log"
+
+> "remote-tor-constant-$DATE.log"
+repeat $NUM_ITERATIONS ./remote-tor-constant.sh "remote-tor-constant-$DATE.log"
+
+> "remote-tor-alternating-$DATE.log"
+repeat $NUM_ITERATIONS ./remote-tor-alternating.sh "remote-tor-alternating-$DATE.log"
diff --git a/experiments/switching/torrc.bridge b/experiments/switching/torrc.bridge
new file mode 100644
index 0000000..d70c8b1
--- /dev/null
+++ b/experiments/switching/torrc.bridge
@@ -0,0 +1,5 @@
+# This configuration file causes a direct Tor connection to use the same bridge
+# used by a flash proxy.
+
+UseBridges 1
+Bridge tor1.bamsoftware.com:9001
diff --git a/experiments/throughput/httpget.py b/experiments/throughput/httpget.py
new file mode 100755
index 0000000..acaa565
--- /dev/null
+++ b/experiments/throughput/httpget.py
@@ -0,0 +1,35 @@
+#!/usr/bin/env python
+
+# A simple HTTP downloader that discards what it downloads and prints the time
+# taken to download. We use this rather than "time wget" because the latter
+# includes time taken to establish (and possibly retry) the connection.
+
+import getopt
+import sys
+import time
+import urllib2
+
+BLOCK_SIZE = 65536
+
+label = None
+
+opts, args = getopt.gnu_getopt(sys.argv[1:], "l:")
+for o, a in opts:
+ if o == "-l":
+ label = a
+
+try:
+ stream = urllib2.urlopen(args[0], timeout=100)
+ start_time = time.time()
+ while stream.read(BLOCK_SIZE):
+ pass
+ end_time = time.time()
+ if label:
+ print "%s %.3f" % (label, end_time - start_time)
+ else:
+ print "%.3f" % (end_time - start_time)
+except:
+ if label:
+ print "%s error" % label
+ else:
+ print "error"
diff --git a/experiments/throughput/throughput-all.sh b/experiments/throughput/throughput-all.sh
new file mode 100755
index 0000000..7d881a9
--- /dev/null
+++ b/experiments/throughput/throughput-all.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+
+for n in $(seq 1 50); do
+ ./throughput.sh -n $n
+done
diff --git a/experiments/throughput/throughput.sh b/experiments/throughput/throughput.sh
new file mode 100755
index 0000000..99ca7fc
--- /dev/null
+++ b/experiments/throughput/throughput.sh
@@ -0,0 +1,114 @@
+#!/bin/bash
+
+# Usage: ./throughput.sh [-n NUM_CLIENTS]
+#
+# Tests the raw throughput of a single proxy. This script starts a web
+# server serving swfcat.swf and a large data file, starts a facilitator,
+# client transport plugin, and socat shim, and then starts multiple
+# downloads through the proxy at once. Results are saved in a file
+# called results-NUM_CLIENTS-DATE, where DATE is the current date.
+
+# plain socks ws ws plain
+# httpget <---> socat <---> flashproxy-client <---> flashproxy <---> websockify <---> thttpd
+# 2000 9001 9000 8001 8000
+
+. ../common.sh
+
+NUM_CLIENTS=1
+
+while getopts "n:" OPTNAME; do
+ if [ "$OPTNAME" == n ]; then
+ NUM_CLIENTS="$OPTARG"
+ fi
+done
+
+PROFILE=flashexp1
+PROXY_URL="http://127.0.0.1:8000/embed.html?facilitator=127.0.0.1:9002&max_clients=$NUM_CLIENTS&ratelimit=off&facilitator_poll_interval=1.0"
+DATA_FILE_NAME="$FLASHPROXY_DIR/dump"
+RESULTS_FILE_NAME="results-$NUM_CLIENTS-$(date --iso)"
+
+# Declare an array.
+declare -a PIDS_TO_KILL
+stop() {
+ browser_clear "$PROFILE"
+ if [ -n "${PIDS_TO_KILL[*]}" ]; then
+ echo "Kill pids ${PIDS_TO_KILL[@]}."
+ kill "${PIDS_TO_KILL[@]}"
+ fi
+ echo "Delete data file."
+ rm -f "$DATA_FILE_NAME"
+ exit
+}
+trap stop EXIT
+
+echo "Create data file."
+dd if=/dev/null of="$DATA_FILE_NAME" bs=1M seek=10 2>/dev/null || exit
+
+echo "Start web server."
+"$THTTPD" -D -d "$FLASHPROXY_DIR" -p 8000 &
+PIDS_TO_KILL+=($!)
+
+echo "Start websockify."
+"$WEBSOCKIFY" -v 8001 127.0.0.1:8000 >/dev/null &
+PIDS_TO_KILL+=($!)
+
+echo "Start facilitator."
+"$FLASHPROXY_DIR"/facilitator -d --relay 127.0.0.1:8001 127.0.0.1 9002 >/dev/null &
+PIDS_TO_KILL+=($!)
+visible_sleep 1
+
+echo "Start client transport plugin."
+"$FLASHPROXY_DIR"/flashproxy-client >/dev/null &
+PIDS_TO_KILL+=($!)
+visible_sleep 1
+
+echo "Start browser."
+browser_goto "$PROFILE" "$PROXY_URL"
+visible_sleep 2
+
+# Create sufficiently many client registrations.
+i=0
+while [ $i -lt $NUM_CLIENTS ]; do
+ echo -ne "\rRegister client $((i + 1))."
+ echo $'POST / HTTP/1.0\r\n\r\nclient=127.0.0.1:9000' | socat STDIN TCP-CONNECT:127.0.0.1:9002
+ sleep 1
+ i=$((i + 1))
+done
+echo
+visible_sleep 2
+
+echo "Start socat."
+"$SOCAT" TCP-LISTEN:2000,fork,reuseaddr SOCKS4A:127.0.0.1:dummy:0,socksport=9001 &
+PIDS_TO_KILL+=($!)
+visible_sleep 1
+
+
+> "$RESULTS_FILE_NAME"
+
+# Proxied downloads.
+declare -a WAIT_PIDS
+i=0
+while [ $i -lt $NUM_CLIENTS ]; do
+ echo "Start downloader $((i + 1))."
+ ./httpget.py -l proxy http://127.0.0.1:2000/dump >> "$RESULTS_FILE_NAME" &
+ WAIT_PIDS+=($!)
+ i=$((i + 1))
+done
+for pid in "${WAIT_PIDS[@]}"; do
+ wait "$pid"
+done
+unset WAIT_PIDS
+
+# Direct downloads.
+declare -a WAIT_PIDS
+i=0
+while [ $i -lt $NUM_CLIENTS ]; do
+ echo "Start downloader $((i + 1))."
+ ./httpget.py -l direct http://127.0.0.1:8000/dump >> "$RESULTS_FILE_NAME" &
+ WAIT_PIDS+=($!)
+ i=$((i + 1))
+done
+for pid in "${WAIT_PIDS[@]}"; do
+ wait "$pid"
+done
+unset WAIT_PIDS
diff --git a/facilitator/.gitignore b/facilitator/.gitignore
new file mode 100644
index 0000000..3c413c1
--- /dev/null
+++ b/facilitator/.gitignore
@@ -0,0 +1,29 @@
+# files build by autogen.sh
+/aclocal.m4
+/autom4te.cache
+/configure
+/depcomp
+/install-sh
+/missing
+/test-driver
+/Makefile.in
+
+# files built by ./configure
+/init.d/facilitator
+/init.d/facilitator-email-poller
+/init.d/facilitator-reg-daemon
+/Makefile
+/config.status
+/config.log
+
+# files built by make
+/examples/fp-facilitator.conf
+/doc/*.1
+
+# files for binary-distribution
+/flashproxy-facilitator-*.tar.*
+
+# files output by test-driver
+test*.log
+*test.log
+*test.trs
diff --git a/facilitator/HACKING b/facilitator/HACKING
new file mode 100644
index 0000000..1bb9f77
--- /dev/null
+++ b/facilitator/HACKING
@@ -0,0 +1,26 @@
+== Running from source checkout
+
+In order to run the code directly from a source checkout, you must make sure it
+can find the flashproxy module, located in the top-level directory of the
+source checkout, which is probably the parent directory. You have two options:
+
+1. Install it in "development mode", see [1]
+
+ flashproxy# python setup-common.py develop
+
+This process is reversible too:
+
+ flashproxy# python setup-common.py develop --uninstall
+
+The disadvantage is that other programs (such as a system-installed flashproxy,
+or other checkouts in another directory) will see this development copy, rather
+than a more appropriate copy.
+
+2. Export PYTHONPATH when you need to run
+
+ $ export PYTHONPATH=..
+ $ make && make check
+
+The disadvantage is that you need to do this every shell session.
+
+[1] http://pythonhosted.org/distribute/setuptools.html#development-mode
diff --git a/facilitator/INSTALL b/facilitator/INSTALL
new file mode 100644
index 0000000..a68bffa
--- /dev/null
+++ b/facilitator/INSTALL
@@ -0,0 +1,40 @@
+Install the dependencies.
+
+ # apt-get install help2man make openssl python-m2crypto
+ # apt-get install automake autoconf # if running from git
+
+ # apt-get install apache2
+
+You may use a different webserver, but currently we only provide an apache2 site
+config example, so you will need to adapt this to the correct syntax.
+
+ # apt-get install flashproxy-common
+
+If your distro does not have flashproxy-common, you can install it
+directly from the top-level source directory:
+
+ flashproxy# python setup-common.py install --record install.log \
+ --single-version-externally-managed
+
+Configure and install.
+
+ $ ./autogen.sh # if running from git or ./configure doesn't otherwise exist
+ $ ./configure --localstatedir=/var/local --enable-initscripts && make
+ # make pre-install install post-install
+
+This installs facilitator.cgi, facilitator, facilitator-email-poller,
+facilitator-reg-daemon, facilitator-reg, and fac.py to /usr/local/bin.
+It also installs System V init files to /etc/init.d/.
+
+The pre/post-install scripts create a user for the daemon to as, and
+sets up the initscripts in the default system runlevels. They also
+generate a RSA key in /usr/local/etc/flashproxy/reg-daemon.{key,pub}.
+
+Uninstall.
+
+ # make pre-remove uninstall post-remove
+
+This will leave behind some config files (e.g. secret keys and passwords). To
+get rid of those too, run this instead:
+
+ # make pre-purge uninstall post-purge
diff --git a/facilitator/Makefile.am b/facilitator/Makefile.am
new file mode 100644
index 0000000..935a197
--- /dev/null
+++ b/facilitator/Makefile.am
@@ -0,0 +1,166 @@
+# our own variables
+
+fpfacilitatoruser = @fpfacilitatoruser@
+initconfdir = @initconfdir@
+cgibindir = @cgibindir@
+
+# unfortunately sysvinit does not support having initscripts in /usr/local/etc
+# yet, so we have to hard code a path here. :(
+initscriptdir = /etc/init.d
+exampledir = $(docdir)/examples
+appenginedir = $(pkgdatadir)/appengine
+pkgconfdir = $(sysconfdir)/flashproxy
+appengineconfdir = $(pkgconfdir)/reg-appspot
+
+PYENV = PYTHONPATH='$(srcdir):$(PYTHONPATH)'; export PYTHONPATH;
+
+# automake PLVs
+
+dist_bin_SCRIPTS = facilitator facilitator-email-poller facilitator-reg-daemon facilitator-reg
+man1_MANS = doc/facilitator.1 doc/facilitator-email-poller.1 doc/facilitator-reg-daemon.1 doc/facilitator-reg.1
+dist_cgibin_SCRIPTS = facilitator.cgi
+if DO_INITSCRIPTS
+initscript_SCRIPTS = init.d/facilitator init.d/facilitator-email-poller init.d/facilitator-reg-daemon
+dist_initconf_DATA = default/facilitator default/facilitator-email-poller default/facilitator-reg-daemon
+endif
+
+dist_doc_DATA = doc/appspot-howto.txt doc/facilitator-design.txt doc/email-howto.txt doc/http-howto.txt doc/server-howto.txt README
+dist_example_DATA = examples/fp-facilitator.conf examples/reg-email.pass examples/facilitator-relays
+pkgconf_DATA = examples/facilitator-relays
+dist_appengine_DATA = appengine/app.yaml appengine/config.go appengine/fp-reg.go
+appengineconf_DATA = appengine/config.go
+CLEANFILES = examples/fp-facilitator.conf $(man1_MANS)
+EXTRA_DIST = examples/fp-facilitator.conf.in mkman.sh mkman.inc HACKING $(TESTS)
+
+TESTS = facilitator-test.py
+# see http://www.gnu.org/software/automake/manual/html_node/Parallel-Test-Harness.html#index-TEST_005fEXTENSIONS
+TEST_EXTENSIONS = .py
+PY_LOG_COMPILER = $(PYTHON)
+AM_TESTS_ENVIRONMENT = $(PYENV)
+AM_PY_LOG_FLAGS =
+
+# AC_CONFIG_FILES doesn't fully-expand directory variables
+# see http://www.gnu.org/software/automake/manual/automake.html#Scripts
+subst_vars = sed -e 's,[@]cgibindir[@],$(cgibindir),g'
+
+# our own targets
+
+doc/%.1: % mkman.sh mkman.inc Makefile
+ { $(PYENV) $(PYTHON) "$<" --help; } \
+ | { $(PYENV) ./mkman.sh "$<" $(VERSION) > "$@"; }
+
+examples/fp-facilitator.conf: examples/fp-facilitator.conf.in Makefile
+# mkdir needed for out-of-source build
+ mkdir -p $$(dirname "$@")
+ $(subst_vars) "$<" > "$@"
+
+pylint: $(dist_bin_SCRIPTS)
+ pylint -E $^
+
+# The {pre,post}-{install,remove} targets are just given as reference, and
+# ought to be separate scripts as part of your distro's installation process.
+# They are intentionally not linked to the install target since they require
+# root access and *must not be run* for fake/staged installs, e.g. when giving
+# non-standard directories to ./configure or DESTDIR to make.
+
+pre-install: meta-install-sanity install-user
+post-install: meta-install-sanity install-secrets install-symlinks install-daemon
+pre-remove: meta-install-sanity remove-daemon remove-symlinks
+post-remove: meta-install-sanity
+pre-purge: pre-remove remove-secrets remove-daemon-data
+post-purge: post-remove remove-user
+
+meta-install-sanity:
+ test "x$(DESTDIR)" = "x" || { echo >&2 \
+ "don't run {pre,post}-{install,remove} when DESTDIR is set"; false; }
+
+install-user:
+ id -u ${fpfacilitatoruser} >/dev/null 2>&1 || { \
+ which adduser >/dev/null 2>&1 && \
+ adduser --quiet \
+ --system \
+ --group \
+ --disabled-password \
+ --home ${pkgconfdir} \
+ --no-create-home \
+ --shell /bin/false \
+ ${fpfacilitatoruser} || \
+ useradd \
+ --system \
+ --home ${pkgconfdir} \
+ -M \
+ --shell /bin/false \
+ ${fpfacilitatoruser} ; }
+
+remove-user:
+ : # deluser does actually remove the group as well
+ id -u ${fpfacilitatoruser} >/dev/null 2>&1 && { \
+ which deluser >/dev/null 2>&1 && \
+ deluser --quiet \
+ --system \
+ ${fpfacilitatoruser} || \
+ userdel \
+ ${fpfacilitatoruser} ; } || true
+
+install-secrets:
+ test -f ${pkgconfdir}/reg-daemon.key || { \
+ install -m 600 /dev/null ${pkgconfdir}/reg-daemon.key && \
+ openssl genrsa 2048 | tee ${pkgconfdir}/reg-daemon.key | \
+ openssl rsa -pubout > ${pkgconfdir}/reg-daemon.pub; }
+ test -f ${pkgconfdir}/reg-email.pass || { \
+ install -m 600 /dev/null ${pkgconfdir}/reg-email.pass && \
+ cat ${exampledir}/reg-email.pass > ${pkgconfdir}/reg-email.pass; }
+
+remove-secrets:
+ for i in reg-daemon.key reg-daemon.pub reg-email.pass; do \
+ rm -f ${pkgconfdir}/$$i; \
+ done
+
+install-symlinks:
+ for i in fp-reg.go app.yaml; do \
+ $(LN_S) -f ${appenginedir}/$$i ${appengineconfdir}/$$i; \
+ done
+
+remove-symlinks:
+ for i in fp-reg.go app.yaml; do \
+ rm -f ${appengineconfdir}/$$i; \
+ done
+
+# initscripts: assume that if the user wanted to install them, then they also
+# wanted to configure them, and that the system supports them. if this isn't the
+# case then either (a) they are doing a staged install for another system and
+# shouldn't be running {pre,post}-{install,remove} or (b) they shouldn't have
+# told us to install initscripts for their system that doesn't support it.
+
+install-daemon:
+if DO_INITSCRIPTS
+# initscripts use these directories for logs and runtime data
+ mkdir -p ${localstatedir}/log
+ mkdir -p ${localstatedir}/run
+ for i in facilitator facilitator-email-poller facilitator-reg-daemon; do \
+ update-rc.d $$i defaults; \
+ invoke-rc.d $$i start; \
+ done
+endif
+
+remove-daemon:
+if DO_INITSCRIPTS
+# we don't rm created directories since they might be system-managed
+ for i in facilitator facilitator-email-poller facilitator-reg-daemon; do \
+ invoke-rc.d $$i stop; \
+ update-rc.d $$i remove; \
+ done
+endif
+
+remove-daemon-data:
+if DO_INITSCRIPTS
+ for i in facilitator facilitator-email-poller facilitator-reg-daemon; do \
+ rm -f ${localstatedir}/log/$$i.log* \
+ rm -f ${localstatedir}/run/$$i.pid \
+ done
+endif
+
+.PHONY: pre-install post-install pre-remove post-remove pre-purge post-purge
+.PHONY: install-user install-secrets install-symlinks install-daemon
+.PHONY: remove-user remove-secrets remove-symlinks remove-daemon
+.PHONY: pylint
diff --git a/facilitator/README b/facilitator/README
new file mode 100644
index 0000000..e2fc857
--- /dev/null
+++ b/facilitator/README
@@ -0,0 +1,34 @@
+This package contains files needed to run a flashproxy facilitator.
+Normal users who just want to bypass censorship, should use the
+flashproxy-client package instead.
+
+For instructions on building/installing this package from source, see
+INSTALL. (This should only be necessary if your distro does not already
+integrate this package into its repositories.)
+
+The flashproxy config directory is installation-dependant, usually at
+/etc/flashproxy or /usr/local/etc/flashproxy. You are strongly
+recommended to keep this on encrypted storage.
+
+The main backends, facilitator and facilitator-reg-daemon, are installed
+as system services, and you should be able to configure them in the
+appropriate place for your system (e.g. /etc/default/facilitator for a
+Debian-based system using initscripts). You probably need to at least
+set RUN_DAEMON=yes to enable the services.
+
+Each installation has its own public-private keypair, stored in the
+flashproxy config directory. You will need to securely distribute the
+public key (reg-daemon.pub) to your users - e.g. by publishing it
+somewhere, signed by your own PGP key.
+
+There are three supported helper rendezvous methods: HTTP, email, and
+appspot. Each helper method may require additional manual configuration
+and might also depend on other helper methods; see the corresponding
+doc/x-howto.txt for more details. At a very minimum, you must configure
+and enable the HTTP method, since that also serves the browser proxies.
+
+For suggestions on configuring a dedicated facilitator machine, see
+doc/server-howto.txt.
+
+For documentation on the design of the facilitator components, see
+doc/facilitator-design.txt.
diff --git a/facilitator/appengine/app.yaml b/facilitator/appengine/app.yaml
new file mode 100644
index 0000000..7269664
--- /dev/null
+++ b/facilitator/appengine/app.yaml
@@ -0,0 +1,10 @@
+# override this with appcfg.py -A $YOUR_APP_ID
+application: facilitator-registration-example
+version: 1
+runtime: go
+api_version: go1
+
+handlers:
+- url: /.*
+ script: _go_app
+ secure: always
diff --git a/facilitator/appengine/config.go b/facilitator/appengine/config.go
new file mode 100644
index 0000000..20898e0
--- /dev/null
+++ b/facilitator/appengine/config.go
@@ -0,0 +1,16 @@
+/*
+This is the server-side code that runs on Google App Engine for the
+"appspot" registration method.
+
+See doc/appspot-howto.txt for more details about setting up an
+application, and advice on running one.
+
+To upload a new version:
+$ torify ~/go_appengine/appcfg.py --no_cookies -A $YOUR_APP_ID update .
+*/
+package fp_reg
+
+// host:port/basepath of the facilitator you want to register with
+// for example, fp-facilitator.org or example.com:12345/facilitator
+// https:// and /reg/ will be prepended and appended respectively.
+const FP_FACILITATOR = ""
diff --git a/facilitator/appengine/fp-reg.go b/facilitator/appengine/fp-reg.go
new file mode 100644
index 0000000..44e9528
--- /dev/null
+++ b/facilitator/appengine/fp-reg.go
@@ -0,0 +1,55 @@
+package fp_reg
+
+import (
+ "io"
+ "net"
+ "net/http"
+ "path"
+
+ "appengine"
+ "appengine/urlfetch"
+)
+
+func robotsTxtHandler(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/plain; charset=utf-8")
+ w.Write([]byte("User-agent: *\nDisallow:\n"))
+}
+
+func ipHandler(w http.ResponseWriter, r *http.Request) {
+ remoteAddr := r.RemoteAddr
+ if net.ParseIP(remoteAddr).To4() == nil {
+ remoteAddr = "[" + remoteAddr + "]"
+ }
+ w.Header().Set("Content-Type", "text/plain; charset=utf-8")
+ w.Write([]byte(remoteAddr))
+}
+
+func regHandler(w http.ResponseWriter, r *http.Request) {
+ dir, blob := path.Split(path.Clean(r.URL.Path))
+ if dir != "/reg/" {
+ http.NotFound(w, r)
+ return
+ }
+ client := urlfetch.Client(appengine.NewContext(r))
+ resp, err := client.Get("https://" + FP_FACILITATOR + "/reg/" + blob)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ for key, values := range resp.Header {
+ for _, value := range values {
+ w.Header().Add(key, value)
+ }
+ }
+ w.WriteHeader(resp.StatusCode)
+ io.Copy(w, resp.Body)
+}
+
+func init() {
+ http.HandleFunc("/robots.txt", robotsTxtHandler)
+ http.HandleFunc("/ip", ipHandler)
+ http.HandleFunc("/reg/", regHandler)
+ if FP_FACILITATOR == "" {
+ panic("FP_FACILITATOR empty; did you forget to edit config.go?")
+ }
+}
diff --git a/facilitator/autogen.sh b/facilitator/autogen.sh
new file mode 100755
index 0000000..4e7cc05
--- /dev/null
+++ b/facilitator/autogen.sh
@@ -0,0 +1,2 @@
+#!/bin/sh
+autoreconf -if
diff --git a/facilitator/configure.ac b/facilitator/configure.ac
new file mode 100644
index 0000000..cc41b8b
--- /dev/null
+++ b/facilitator/configure.ac
@@ -0,0 +1,49 @@
+AC_PREREQ([2.68])
+AC_INIT([flashproxy-facilitator], [1.4])
+AM_INIT_AUTOMAKE([-Wall foreign])
+
+AC_ARG_VAR(fpfacilitatoruser, [the user/group for the facilitator to run as])
+fpfacilitatoruser="${fpfacilitatoruser:-fp-facilitator}"
+
+# check that we want to install initscripts. don't bother checking that they
+# are supported, since we might be doing a staged install on a different system.
+# disabled by default since it ignores ${prefix} so `make distcheck` would fail
+AC_ARG_ENABLE([initscripts],
+ [AS_HELP_STRING([--enable-initscripts],
+ [install and configure sysvinit-style initscripts (default no)])],
+ [do_initscripts=yes], [do_initscripts=])
+AM_CONDITIONAL([DO_INITSCRIPTS], [test "x$do_initscripts" = xyes])
+
+AC_ARG_VAR(initconfdir, [directory for initscripts configuration, if enabled])
+# Try to detect the appropriate conf dir. Several systems have both /etc/default
+# and /etc/sysconfig but latter is always primary.
+if test "x$do_initscripts" = xyes; then
+if test "x$initconfdir" = x; then
+AC_CHECK_FILE(/etc/conf.d, [initconfdir='$(sysconfdir)/conf.d}'], [# Gentoo/Arch
+AC_CHECK_FILE(/etc/sysconfig, [initconfdir='$(sysconfdir)/sysconfig'], [# RedHat/Fedora/Slax/Mandriva/SuSE
+AC_CHECK_FILE(/etc/default, [initconfdir='$(sysconfdir)/default'], [# Debian/Ubuntu
+AC_MSG_ERROR([could not determine system initscripts config dir; please set initconfdir manually.])])])])
+fi
+fi
+
+# Try to detect cgi-bin directory, falling back to $(libexec) if not found
+# from http://wiki.apache.org/httpd/DistrosDefaultLayout
+AC_ARG_VAR(cgibindir, [directory for CGI executables])
+if test "x$cgibindir" = x; then
+AC_CHECK_FILE(/usr/lib/cgi-bin, [cgibindir='$(libdir)/cgi-bin'], [
+AC_CHECK_FILE(/var/www/cgi-bin, [cgibindir='/var/www/cgi-bin'], [
+AC_CHECK_FILE(/srv/httpd/cgi-bin, [cgibindir='/srv/httpd/cgi-bin'], [
+AC_MSG_WARN([could not determine system CGI executables dir, using \$(libexecdir); set cgibindir to override.])
+cgibindir='$(libexecdir)'
+])])])
+fi
+
+AC_PROG_LN_S
+AM_PATH_PYTHON
+
+AC_CONFIG_FILES([Makefile
+ init.d/facilitator
+ init.d/facilitator-email-poller
+ init.d/facilitator-reg-daemon])
+
+AC_OUTPUT
diff --git a/facilitator/default/facilitator b/facilitator/default/facilitator
new file mode 100644
index 0000000..b45c042
--- /dev/null
+++ b/facilitator/default/facilitator
@@ -0,0 +1,11 @@
+# Change to "yes" to run the service.
+RUN_DAEMON="no"
+
+# Uncomment this to log potentially sensitive information from your users.
+# This may be useful for debugging or diagnosing functional problems, but
+# should be avoided in most other cases.
+#UNSAFE_LOGGING="yes"
+
+# Set the port for this service to listen on.
+# If not set, uses the default (9002).
+#PORT=9002
diff --git a/facilitator/default/facilitator-email-poller b/facilitator/default/facilitator-email-poller
new file mode 100644
index 0000000..9369e18
--- /dev/null
+++ b/facilitator/default/facilitator-email-poller
@@ -0,0 +1,7 @@
+# Change to "yes" to run the service.
+RUN_DAEMON="no"
+
+# Uncomment this to log potentially sensitive information from your users.
+# This may be useful for debugging or diagnosing functional problems, but
+# should be avoided in most other cases.
+#UNSAFE_LOGGING="yes"
diff --git a/facilitator/default/facilitator-reg-daemon b/facilitator/default/facilitator-reg-daemon
new file mode 100644
index 0000000..ba0773e
--- /dev/null
+++ b/facilitator/default/facilitator-reg-daemon
@@ -0,0 +1,11 @@
+# Change to "yes" to run the service.
+RUN_DAEMON="no"
+
+# Uncomment this to log potentially sensitive information from your users.
+# This may be useful for debugging or diagnosing functional problems, but
+# should be avoided in most other cases.
+#UNSAFE_LOGGING="yes"
+
+# Set the port for this service to listen on.
+# If not set, uses the default (9003).
+#PORT=9003
diff --git a/facilitator/doc/appspot-howto.txt b/facilitator/doc/appspot-howto.txt
new file mode 100644
index 0000000..8ed1284
--- /dev/null
+++ b/facilitator/doc/appspot-howto.txt
@@ -0,0 +1,72 @@
+These are instructions for how to set up a Google App Engine application
+for the appspot rendezvous method (flashproxy-reg-appspot). It requires
+the HTTP rendezvous to be available, so you should set that up first and
+ensure it is working correctly, or find someone else's to use. If you
+choose the latter, note that it is *their* reg-daemon.pub that your users
+must give to flashproxy-reg-appspot.
+
+For more information about Google App Engine, see the links at the bottom
+of this document.
+
+You are strongly recommended to create a Google account dedicated for
+this purpose, rather than a personal or organisation account. See
+email-howto.txt for how to do that.
+
+Download the SDK:
+https://developers.google.com/appengine/docs/go/gettingstarted/devenvironment
+
+Find your facilitator appengine installation, probably in reg-appspot/
+in your flashproxy config dir. Edit config.go to point to the address of
+the HTTP facilitator.
+
+Follow the directions to register a new application:
+https://developers.google.com/appengine/docs/go/gettingstarted/uploading
+Enter an application ID and create the application.
+
+To run locally using the development server:
+$ ~/go_appengine/dev_appserver.py reg-appspot/
+You are advised to do this on a non-production machine, away from the main
+facilitator.
+
+Use the appcfg.py program to upload the program. It should look
+something like this:
+
+$ torify ./google_appengine/appcfg.py --no_cookies -A <YOUR_APP_ID> update reg-appspot/
+07:25 PM Host: appengine.google.com
+07:25 PM Application: application-id; version: 1
+07:25 PM
+Starting update of app: application-id, version: 1
+07:25 PM Getting current resource limits.
+Email: xxx at gmail.com
+Password for xxx at gmail.com:
+07:26 PM Scanning files on local disk.
+07:26 PM Cloning 2 application files.
+07:26 PM Uploading 1 files and blobs.
+07:26 PM Uploaded 1 files and blobs
+07:26 PM Compilation starting.
+07:26 PM Compilation: 1 files left.
+07:26 PM Compilation completed.
+07:26 PM Starting deployment.
+07:26 PM Checking if deployment succeeded.
+07:26 PM Deployment successful.
+07:26 PM Checking if updated app version is serving.
+07:26 PM Completed update of app: application-id, version: 1
+
+The --no_cookies flag stops authentication cookies from being written
+to disk, in ~/.appcfg_cookies. We recommend this for security, since no
+long-running services need this password, only the update process above
+which is run once. However, if this reasoning doesn't apply to you
+(e.g. if your facilitator-email-poller uses the same account, so that
+the password is already on the disk) *and* you find yourself running
+update a lot for some reason, then you may at your own risk omit it for
+convenience.
+
+Once logged in, you can disable logging for the application. Click
+"Logs" on the left panel. Under "Total Logs Storage", click "Change
+Settings". Enter "0" in the "days of logs" box and click "Save
+Settings".
+
+General links:
+https://developers.google.com/appengine/
+https://developers.google.com/appengine/docs/whatisgoogleappengine
+https://developers.google.com/appengine/docs/go/gettingstarted/
diff --git a/facilitator/doc/email-howto.txt b/facilitator/doc/email-howto.txt
new file mode 100644
index 0000000..9cbfdb6
--- /dev/null
+++ b/facilitator/doc/email-howto.txt
@@ -0,0 +1,75 @@
+These are instructions for setting up an email account for use with the
+email-based rendezvous (facilitator-email-poller / flashproxy-reg-email).
+
+You are strongly advised to use an email account dedicated for this
+purpose. If your email provider supports it, we advise you to use an
+app-specific password rather than your account password.
+
+Once you have an email address and the password for it, you should add
+this information to reg-email.pass in your flashproxy config directory.
+For your security, this file should be on encrypted storage.
+
+The following section provides some instructions on how to set up a new
+Google account whilst revealing as little information to Google as is
+feasible.
+
+== Creating a Google account securely
+
+These instructions were current as of May 2013.
+
+You may have trouble if you are using Tor to create the account, for two
+reasons. The first is that exit nodes are a source of abuse and Google
+is more suspicious of them. The second is that Gmail is suspicious and
+can lock you out of the account when your IP address is changing. While
+setting up the account, use a single node in your torrc ExitNodes
+configuration. Choose a U.S. exit node, one with low bandwidth.
+
+Go to https://mail.google.com/. Allow JavaScript to run (even from
+youtube.com; it seems to be necessary). Click the "CREATE AN ACCOUNT"
+button.
+
+Enter the account details. You don't need to fill in "Your current email
+address". Enter a mobile phone number for later activation of two-factor
+authentication. Solve the captcha. Click "Next Step". You may have to do
+a phone SMS verification here.
+
+At this point the Gmail account is created. If you are pushed into
+joining Google+, close everything out and go back to
+https://mail.google.com/.
+
+Log out of the account and then back in again. There will be new text in
+the lower right reading "Last account activity". Click "Details" and
+turn off the unusual activity alerts. This will keep you from getting
+locked out when you come from different IP addresses. At this point you
+should remove the temporary ExitNodes configuration from torrc.
+
+Add a filter to prevent registrations from being marked as spam. Click
+on the gear icon and select "Settings". Select "Filters" then "Create a
+new filter". For "Has the words" type "in:spam", then "Create filter
+with this search". There will be a warning that filters using "in:" will
+never match incoming mail; this appears to be false and you can just
+click OK. Check "Never send it to Spam" and click "Create filter".
+
+Enable IMAP. Click the gear icon, then "Settings", then "Forwarding and
+POP/IMAP".
+ * Disable POP
+ * Enable IMAP
+ * Auto-Expunge on
+Click "Save Changes".
+
+Enable two-factor authentication. We do this not so much for the
+two-factor, but because it allows creating an independent password that
+is used only for IMAP and does not have access to the web interface of
+Gmail. Click the email address in the upper right, then "Account". Click
+"Security". By "2-step verification" click "Edit". Click through until
+it lets you set up. The phone number you provided when the account was
+created will be automatically filled in. Choose "Text message (SMS)"
+then click "Send code". Get your text message, type it in, and hit
+"Verify". Uncheck "Trust this computer" on the next screen. Finally
+"Confirm". On the following summary page, click "Show backup codes" and
+save the codes to encrypted storage. Future codes can be generated at
+https://www.google.com/accounts/SmsAuthConfig.
+
+Still on the 2-step summary page, click "Manage application-specific
+passwords". Enter "IMAP" for the name and click "Generate password".
+Now store this in reg-email.pass, as mentioned in the introduction.
diff --git a/facilitator/doc/facilitator-design.txt b/facilitator/doc/facilitator-design.txt
new file mode 100644
index 0000000..19d5d47
--- /dev/null
+++ b/facilitator/doc/facilitator-design.txt
@@ -0,0 +1,41 @@
+The main facilitator program is a backend server that is essentially a
+dynamic database of client addresses, as well as helper programs that
+receive client registrations from the Internet over various means and
+pass them to the backend. There are three supported helper rendezvous
+methods: HTTP, email, and appspot.
+
+facilitator-reg is a simple program that forwards its standard input to
+a locally running facilitator-reg-daemon process. It is used by other
+components as a utility, but is also useful for debugging and testing.
+
+facilitator-reg-daemon accepts connections containing encrypted client
+registrations and forwards them to the facilitator. It exists as a
+process of its own so that only one program requires access to the
+facilitator's private key.
+
+The HTTP rendezvous uses an HTTP server and a CGI program. The HTTP
+server is responsible for speaking TLS and invoking the CGI program. The
+CGI program receives client registrations and proxy requests for
+clients, parses them, and forwards them to the backend. We use Apache 2
+as the HTTP server. The CGI script is facilitator.cgi. Currently this
+is also the only method for accepting browser proxy registrations, so
+you must enable this method, otherwise your clients will not be served.
+
+For the HTTP rendezvous, there are two formats you may use for a client
+registration - plain vs. (end-to-end) encrypted. Direct registrations
+(e.g. flashproxy-reg-http) can use the plain format over HTTPS, which
+provides transport encryption; but if you proxy registrations through
+another service (e.g. reg-appspot), you must use the end-to-end format.
+On the client side, you may use flashproxy-reg-url to generate
+registration URLs for the end-to-end encrypted format.
+
+The email rendezvous uses the helper program facilitator-email-poller.
+Clients use the flashproxy-reg-email program to send an encrypted
+message to a Gmail address. The poller constantly checks for new
+messages and forwards them to facilitator-reg.
+
+The appspot rendezvous uses Google's appengine platform as a proxy for
+the HTTP method, either yours or that of another facilitator. It takes
+advantage of the fact that a censor cannot distinguish between a TLS
+connection to appspot.com or google.com, since the IPs are the same,
+and it is highly unlikely that anyone will try to block the latter.
diff --git a/facilitator/doc/http-howto.txt b/facilitator/doc/http-howto.txt
new file mode 100644
index 0000000..478eb3e
--- /dev/null
+++ b/facilitator/doc/http-howto.txt
@@ -0,0 +1,49 @@
+These are instructions for how to set up an Apache Web Server for
+handling the HTTP client registration method (facilitator.cgi /
+flashproxy-reg-http / flashproxy-reg-url), as well as for browser
+proxies to poll and receive a client to serve.
+
+Unfortunately we only had time to give commands specific to the Debian
+distribution of Apache; other distributions may need to tweak some
+things, e.g. a2enmod, a2ensite only exist on Debian.
+
+== HTTP server setup
+
+Apache is the web server that runs the CGI program.
+
+ # apt-get install apache2 libapache2-mod-evasive
+ # a2enmod ssl headers
+
+Edit /etc/apache2/ports.conf and comment out the port 80 configuration.
+
+ # NameVirtualHost *:80
+ # Listen 80
+
+Copy examples/fp-facilitator.conf to /etc/apache2/sites-available/ or
+wherever is appropriate for your Apache2 installation, then edit it as
+per the instructions given in that file itself.
+
+Link the configured site into sites-enabled.
+ # a2ensite fp-facilitator.conf
+
+=== HTTPS setup
+
+The HTTP server should serve only over HTTPS and not unencrypted HTTP.
+You will need a certificate and private key from a certificate
+authority. An article on making a certificate signing request and
+getting it signed is here:
+ http://www.debian-administration.org/articles/284
+This is the basic command to generate a CSR.
+ $ openssl req -new -nodes -out fp-facilitator.csr.pem
+The instructions below assume you have an offline private key
+in fp-facilitator.key.pem and a certificate in fp-facilitator.crt.pem.
+
+Make a file containing both the private key and a certificate.
+
+ $ cat fp-facilitator.key.pem fp-facilitator.crt.pem > fp-facilitator.pem
+ $ chmod 400 fp-facilitator.pem
+
+Copy the new fp-facilitator.pem to the facilitator server as
+/etc/apache2/fp-facilitator.pem.
+
+ # /etc/init.d/apache2 restart
diff --git a/facilitator/doc/server-howto.txt b/facilitator/doc/server-howto.txt
new file mode 100644
index 0000000..bf1bb0b
--- /dev/null
+++ b/facilitator/doc/server-howto.txt
@@ -0,0 +1,55 @@
+This document describes how to configure a server running the facilitator on
+Debian 7. It is not necessary to make things work, but gives you some added
+security, and is a good reference if you want to create a dedicated VM for a
+facilitator from scratch.
+
+We will use the domain name fp-facilitator.example.com.
+
+== Basic and security setup
+
+Install some essential packages and configure a firewall.
+
+ # cat >/etc/apt/apt.conf.d/90suggests<<EOF
+APT::Install-Recommends "0";
+APT::Install-Suggests "0";
+EOF
+ # apt-get remove rpcbind
+ # apt-get update
+ # apt-get upgrade
+ # apt-get install shorewall shorewall6
+
+Away from the facilitator, generate an SSH key for authentication:
+
+ $ ssh-keygen -f ~/.ssh/fp-facilitator
+ $ ssh-copy-id -i ~/.ssh/fp-facilitator.pub root at fp-facilitator.example.com
+
+Then log in and edit /etc/ssh/sshd_config to disable password
+authentication:
+
+ PasswordAuthentication no
+
+Configure the firewall to allow only SSH and HTTPS.
+
+ # cd /etc/shorewall
+ # cp /usr/share/doc/shorewall/examples/Universal/{interfaces,policy,rules,zones} .
+ Edit /etc/shorewall/rules:
+SECTION NEW
+SSH(ACCEPT) net $FW
+HTTPS(ACCEPT) net $FW
+
+ # cd /etc/shorewall6
+ # cp /usr/share/doc/shorewall6/examples/Universal/{interfaces,policy,rules,zones} .
+ Edit /etc/shorewall6/rules:
+SECTION NEW
+SSH(ACCEPT) all $FW
+HTTPS(ACCEPT) all $FW
+
+Edit /etc/default/shorewall and /etc/default/shorewall6 and set
+
+ startup=1
+
+Restart servers.
+
+ # /etc/init.d/ssh restart
+ # /etc/init.d/shorewall start
+ # /etc/init.d/shorewall6 start
diff --git a/facilitator/examples/facilitator-relays b/facilitator/examples/facilitator-relays
new file mode 100644
index 0000000..0753387
--- /dev/null
+++ b/facilitator/examples/facilitator-relays
@@ -0,0 +1,7 @@
+# List some relays that the facilitator will match proxies with.
+# Each line contains a transport and an address, separated by space.
+# See doc/design.txt for details of the transport syntax.
+#obfs3|websocket 173.255.221.44:9500
+#websocket 173.255.221.44:9901
+#obfs3|websocket [2600:3c01::f03c:91ff:fe93:6cd2]:9500
+#websocket [2600:3c01::f03c:91ff:fe93:6cd2]:9901
diff --git a/facilitator/examples/fp-facilitator.conf.in b/facilitator/examples/fp-facilitator.conf.in
new file mode 100644
index 0000000..4eb98eb
--- /dev/null
+++ b/facilitator/examples/fp-facilitator.conf.in
@@ -0,0 +1,30 @@
+# This is an example apache2 config for serving the facilitator.
+#
+# For instructions on how to install it, see doc/http-howto.txt. You
+# should also edit the contents according to the instructions below.
+#
+MaxClients 256
+<VirtualHost *:443>
+ # Update this with your hostname!
+ ServerName fp-facilitator.example.com
+ DocumentRoot /dev/null
+ ScriptAliasMatch ^(.*) @cgibindir@/facilitator.cgi$1
+
+ # Non-Debian distros will need to tweak the log dir too
+ # Only log errors by default, to protect sensitive information.
+ CustomLog /dev/null common
+ #CustomLog ${APACHE_LOG_DIR}/fp-access.log common
+ ErrorLog ${APACHE_LOG_DIR}/fp-error.log
+ LogLevel warn
+
+ # requires mod_ssl
+ SSLEngine on
+ # Manually install your certificate to the following location.
+ SSLCertificateFile /etc/apache2/fp-facilitator.pem
+ # If you got an intermediate certificate, uncomment the following line
+ # and install the certificate to that location too.
+ #SSLCertificateChainFile /etc/apache2/fp-intermediate.pem
+
+ # requires mod_headers
+ Header add Strict-Transport-Security "max-age=15768000"
+</VirtualHost>
diff --git a/facilitator/examples/reg-email.pass b/facilitator/examples/reg-email.pass
new file mode 100644
index 0000000..75b6aa4
--- /dev/null
+++ b/facilitator/examples/reg-email.pass
@@ -0,0 +1,10 @@
+# This file should contain "[<imap_host>] <email> <password>" on a single line,
+# separated by whitespace. If <imap_host> is omitted, it defaults to
+# imap.(<email> domain):993.
+#
+# If your email provider supports it, we advise you to use an app-specific
+# password rather than your account password; see email-howto.txt in this
+# package's documentation for details on how to do this.
+#
+#imap.gmail.com:993 flashproxyreg.a at gmail.com topsecret11!one
+#flashproxyreg.a at gmail.com passwords with spaces are ok too
diff --git a/facilitator/facilitator b/facilitator/facilitator
new file mode 100755
index 0000000..35d74be
--- /dev/null
+++ b/facilitator/facilitator
@@ -0,0 +1,531 @@
+#!/usr/bin/env python
+"""
+The flashproxy facilitator.
+"""
+
+import SocketServer
+import getopt
+import os
+import socket
+import sys
+import threading
+import time
+from collections import defaultdict
+
+from flashproxy import fac
+from flashproxy import proc
+from flashproxy.reg import Transport, Endpoint
+from flashproxy.util import parse_addr_spec, format_addr, canonical_ip
+
+LISTEN_ADDRESS = "127.0.0.1"
+DEFAULT_LISTEN_PORT = 9002
+DEFAULT_RELAY_PORT = 9001
+DEFAULT_LOG_FILENAME = "facilitator.log"
+
+# Tell proxies to poll for clients every POLL_INTERVAL seconds.
+POLL_INTERVAL = 600
+
+# Don't indulge clients for more than this many seconds.
+CLIENT_TIMEOUT = 1.0
+# Buffer no more than this many bytes when trying to read a line.
+READLINE_MAX_LENGTH = 10240
+
+MAX_PROXIES_PER_CLIENT = 5
+DEFAULT_OUTER_TRANSPORTS = ["websocket"]
+
+LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
+
+class UnknownTransport(Exception): pass
+
+class options(object):
+ listen_port = DEFAULT_LISTEN_PORT
+ log_filename = DEFAULT_LOG_FILENAME
+ log_file = sys.stdout
+ relay_filename = None
+ daemonize = True
+ pid_filename = None
+ privdrop_username = None
+ safe_logging = True
+ outer_transports = DEFAULT_OUTER_TRANSPORTS
+
+def usage(f = sys.stdout):
+ print >> f, """\
+Usage: %(progname)s -r RELAY <OPTIONS>
+Flash proxy facilitator: Register client addresses and serve them out
+again. Listen on 127.0.0.1 and port PORT (by default %(port)d).
+
+ -d, --debug don't daemonize, log to stdout.
+ -h, --help show this help.
+ -l, --log FILENAME write log to FILENAME (default \"%(log)s\").
+ -p, --port PORT listen on PORT (default %(port)d).
+ --pidfile FILENAME write PID to FILENAME after daemonizing.
+ --privdrop-user USER switch UID and GID to those of USER.
+ -r, --relay-file RELAY learn relays from FILE.
+ --outer-transports TRANSPORTS
+ comma-sep list of outer transports to accept proxies
+ for (by default %(outer-transports)s)
+ --unsafe-logging don't scrub IP addresses from logs.\
+""" % {
+ "progname": sys.argv[0],
+ "port": DEFAULT_LISTEN_PORT,
+ "log": DEFAULT_LOG_FILENAME,
+ "outer-transports": ",".join(DEFAULT_OUTER_TRANSPORTS)
+}
+
+def safe_str(s):
+ """Return "[scrubbed]" if options.safe_logging is true, and s otherwise."""
+ if options.safe_logging:
+ return "[scrubbed]"
+ else:
+ return s
+
+log_lock = threading.Lock()
+def log(msg):
+ with log_lock:
+ print >> options.log_file, (u"%s %s" % (time.strftime(LOG_DATE_FORMAT), msg)).encode("UTF-8")
+ options.log_file.flush()
+
+
+class Endpoints(object):
+ """
+ Tracks endpoints (either client/server) and the transports they support.
+ """
+
+ matchingLock = threading.Condition()
+
+ def __init__(self, af, maxserve=float("inf")):
+ self.af = af
+ self._maxserve = maxserve
+ self._endpoints = {} # address -> transport
+ self._indexes = defaultdict(lambda: defaultdict(set)) # outer -> inner -> [ addresses ]
+ self._served = {} # address -> num_times_served
+ self._cv = threading.Condition()
+
+ def getNumEndpoints(self):
+ """:returns: the number of endpoints known to us."""
+ with self._cv:
+ return len(self._endpoints)
+
+ def getNumUnservedEndpoints(self):
+ """:returns: the number of unserved endpoints known to us."""
+ with self._cv:
+ return len(filter(lambda t: t == 0, self._served.itervalues()))
+
+ def addEndpoint(self, addr, transport):
+ """Add an endpoint.
+
+ :param addr: Address of endpoint, usage-dependent.
+ :param list transports: List of transports.
+ :returns: False if the address is already known, in which case no
+ update is made to its supported transports, else True.
+ """
+ transport = Transport.parse(transport)
+ with self._cv:
+ if addr in self._endpoints: return False
+ inner, outer = transport
+ self._endpoints[addr] = transport
+ self._served[addr] = 0
+ self._indexes[outer][inner].add(addr)
+ self._cv.notify()
+ return True
+
+ def delEndpoint(self, addr):
+ """Forget an endpoint.
+
+ :param addr: Address of endpoint, usage-dependent.
+ :returns: False if the address was already forgotten, else True.
+ """
+ with self._cv:
+ if addr not in self._endpoints: return False
+ inner, outer = self._endpoints[addr]
+ self._indexes[outer][inner].remove(addr) # TODO(infinity0): maybe delete empty bins
+ del self._served[addr]
+ del self._endpoints[addr]
+ self._cv.notify()
+ return True
+
+ def _findInnerForOuter(self, *supported_outer):
+ """Find all endpoint addresses that support any of the given outer
+ transports. Results are grouped by the inner transport.
+
+ :returns: { inner: [addr] }, where each address supports some outer
+ transport from supported_outer.
+ """
+ inners = defaultdict(set)
+ for outer in set(supported_outer) & set(self._indexes.iterkeys()):
+ for inner, addrs in self._indexes[outer].iteritems():
+ if addrs:
+ # don't add empty bins, to avoid false-positive key checks
+ inners[inner].update(addrs)
+ return inners
+
+ def _serveReg(self, addrpool):
+ """
+ :param list addrpool: List of candidate addresses.
+ :returns: An Endpoint whose address is from the given pool. The serve
+ counter for that address is also incremented, and if it hits
+ self._maxserve the endpoint is removed from this collection.
+ :raises: KeyError if any address is not registered with this collection
+ """
+ if not addrpool: raise ValueError("gave empty address pool")
+ prio_addr = min(addrpool, key=lambda a: self._served[a])
+ assert self._served[prio_addr] < self._maxserve
+ self._served[prio_addr] += 1
+ transport = self._endpoints[prio_addr]
+ if self._served[prio_addr] == self._maxserve:
+ self.delEndpoint(prio_addr)
+ return Endpoint(prio_addr, transport)
+
+ EMPTY_MATCH = (None, None)
+ @staticmethod
+ def match(ptsClient, ptsServer, supported_outer):
+ """
+ :returns: A tuple (client Reg, server Reg) arbitrarily selected from
+ the available endpoints that can satisfy supported_outer.
+ """
+ if ptsClient.af != ptsServer.af:
+ raise ValueError("address family not equal")
+ if ptsServer._maxserve < float("inf"):
+ raise ValueError("servers mustn't run out")
+ # need to operate on both structures
+ # so hold both locks plus a pair-wise lock
+ with Endpoints.matchingLock, ptsClient._cv, ptsServer._cv:
+ server_inner = ptsServer._findInnerForOuter(*supported_outer)
+ client_inner = ptsClient._findInnerForOuter(*supported_outer)
+ both = set(server_inner.keys()) & set(client_inner.keys())
+ if not both: return Endpoints.EMPTY_MATCH
+ # find a client to serve
+ client_pool = [addr for inner in both for addr in client_inner[inner]]
+ assert len(client_pool)
+ client_reg = ptsClient._serveReg(client_pool)
+ # find a server to serve that has the same inner transport
+ inner = client_reg.transport.inner
+ assert inner in server_inner and len(server_inner[inner])
+ server_reg = ptsServer._serveReg(server_inner[inner])
+ # assume servers never run out
+ return (client_reg, server_reg)
+
+
+class Handler(SocketServer.StreamRequestHandler):
+ def __init__(self, *args, **kwargs):
+ self.deadline = time.time() + CLIENT_TIMEOUT
+ # Buffer for readline.
+ self.buffer = ""
+ SocketServer.StreamRequestHandler.__init__(self, *args, **kwargs)
+
+ def recv(self):
+ timeout = self.deadline - time.time()
+ self.connection.settimeout(timeout)
+ return self.connection.recv(1024)
+
+ def readline(self):
+ # A line already buffered?
+ i = self.buffer.find("\n")
+ if i >= 0:
+ line = self.buffer[:i+1]
+ self.buffer = self.buffer[i+1:]
+ return line
+
+ auxbuf = []
+ buflen = len(self.buffer)
+ while True:
+ data = self.recv()
+ if not data:
+ if self.buffer or auxbuf:
+ raise socket.error("readline: stream does not end with a newline")
+ else:
+ return ""
+ i = data.find("\n")
+ if i >= 0:
+ line = self.buffer + "".join(auxbuf) + data[:i+1]
+ self.buffer = data[i+1:]
+ return line
+ else:
+ auxbuf.append(data)
+ buflen += len(data)
+ if buflen >= READLINE_MAX_LENGTH:
+ raise socket.error("readline: refusing to buffer %d bytes (last read was %d bytes)" % (buflen, len(data)))
+
+ @proc.catch_epipe
+ def handle(self):
+ num_lines = 0
+ while True:
+ try:
+ line = self.readline()
+ if not line:
+ break
+ num_lines += 1
+ except socket.error, e:
+ log("socket error after reading %d lines: %s" % (num_lines, str(e)))
+ break
+ if not self.handle_line(line):
+ break
+
+ def handle_line(self, line):
+ if not (len(line) > 0 and line[-1] == '\n'):
+ raise ValueError("No newline at end of string returned by readline")
+ try:
+ command, params = fac.parse_transaction(line[:-1])
+ except ValueError, e:
+ return self.error("fac.parse_transaction: %s" % e)
+
+ if command == "GET":
+ return self.do_GET(params)
+ elif command == "PUT":
+ return self.do_PUT(params)
+ else:
+ self.send_error()
+ return False
+
+ def send_ok(self):
+ print >> self.wfile, "OK"
+
+ def send_error(self):
+ print >> self.wfile, "ERROR"
+
+ def error(self, log_msg):
+ log(log_msg)
+ self.send_error()
+ return False
+
+ # Handle a GET request (got flashproxy poll; need to return a proper client registration)
+ # Example: GET FROM="3.3.3.3:3333" PROXY-TRANSPORT="websocket" PROXY-TRANSPORT="webrtc"
+ def do_GET(self, params):
+ proxy_spec = fac.param_first("FROM", params)
+ if proxy_spec is None:
+ return self.error(u"GET missing FROM param")
+ try:
+ proxy_addr = canonical_ip(*parse_addr_spec(proxy_spec, defport=0))
+ except ValueError, e:
+ return self.error(u"syntax error in proxy address %s: %s" % (safe_str(repr(proxy_spec)), safe_str(repr(str(e)))))
+
+ transport_list = fac.param_getlist("PROXY-TRANSPORT", params)
+ if not transport_list:
+ return self.error(u"GET missing PROXY-TRANSPORT param")
+
+ try:
+ client_reg, relay_reg = get_match_for_proxy(proxy_addr, transport_list)
+ except Exception, e:
+ return self.error(u"error getting reg for proxy address %s: %s" % (safe_str(repr(proxy_spec)), safe_str(repr(str(e)))))
+
+ check_back_in = get_check_back_in_for_proxy(proxy_addr)
+
+ if client_reg:
+ log(u"proxy (%s) gets client '%s' (supported transports: %s) (num relays: %s) (remaining regs: %d/%d)" %
+ (safe_str(repr(proxy_spec)), safe_str(repr(client_reg.addr)), transport_list, num_relays(), num_unhandled_regs(), num_regs()))
+ print >> self.wfile, fac.render_transaction("OK",
+ ("CLIENT", format_addr(client_reg.addr)),
+ ("CLIENT-TRANSPORT", client_reg.transport.outer),
+ ("RELAY", format_addr(relay_reg.addr)),
+ ("RELAY-TRANSPORT", relay_reg.transport.outer),
+ ("CHECK-BACK-IN", str(check_back_in)))
+ else:
+ log(u"proxy (%s) gets none" % safe_str(repr(proxy_spec)))
+ print >> self.wfile, fac.render_transaction("NONE", ("CHECK-BACK-IN", str(check_back_in)))
+
+ return True
+
+ # Handle a PUT request (client made a registration request; register it.)
+ # Example: PUT CLIENT="1.1.1.1:5555" TRANSPORT="obfs3|websocket"
+ def do_PUT(self, params):
+ # Check out if we recognize the transport in this registration request
+ transport_spec = fac.param_first("TRANSPORT", params)
+ if transport_spec is None:
+ return self.error(u"PUT missing TRANSPORT param")
+
+ transport = Transport.parse(transport_spec)
+ # See if we have relays that support this transport
+ if transport.outer not in options.outer_transports:
+ return self.error(u"Unrecognized transport: %s" % transport.outer)
+
+ client_spec = fac.param_first("CLIENT", params)
+ if client_spec is None:
+ return self.error(u"PUT missing CLIENT param")
+
+ try:
+ reg = Endpoint.parse(client_spec, transport)
+ except (UnknownTransport, ValueError) as e:
+ # XXX should we throw a better error message to the client? Is it possible?
+ return self.error(u"syntax error in %s: %s" % (safe_str(repr(client_spec)), safe_str(repr(str(e)))))
+
+ try:
+ ok = put_reg(reg)
+ except Exception, e:
+ return self.error(u"error putting reg %s: %s" % (safe_str(repr(client_spec)), safe_str(repr(str(e)))))
+
+ if ok:
+ log(u"client %s (transports: %s) (remaining regs: %d/%d)" % (safe_str(unicode(reg)), reg.transport, num_unhandled_regs(), num_regs()))
+ else:
+ log(u"client %s (already present) (transports: %s) (remaining regs: %d/%d)" % (safe_str(unicode(reg)), reg.transport, num_unhandled_regs(), num_regs()))
+
+ self.send_ok()
+ return True
+
+ finish = proc.catch_epipe(SocketServer.StreamRequestHandler.finish)
+
+class Server(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
+ allow_reuse_address = True
+
+# Addresses are plain tuples (str(host), int(port))
+
+CLIENTS = {
+ socket.AF_INET: Endpoints(af=socket.AF_INET, maxserve=MAX_PROXIES_PER_CLIENT),
+ socket.AF_INET6: Endpoints(af=socket.AF_INET6, maxserve=MAX_PROXIES_PER_CLIENT)
+}
+
+RELAYS = {
+ socket.AF_INET: Endpoints(af=socket.AF_INET),
+ socket.AF_INET6: Endpoints(af=socket.AF_INET6)
+}
+
+def num_relays():
+ """Return the total number of relays."""
+ return sum(pts.getNumEndpoints() for pts in RELAYS.itervalues())
+
+def num_regs():
+ """Return the total number of registrations."""
+ return sum(pts.getNumEndpoints() for pts in CLIENTS.itervalues())
+
+def num_unhandled_regs():
+ """Return the total number of unhandled registrations."""
+ return sum(pts.getNumUnservedEndpoints() for pts in CLIENTS.itervalues())
+
+def addr_af(addr_str):
+ """Return the address family for an address string. This is a plain string,
+ not a tuple, and IPv6 addresses are not bracketed."""
+ addrs = socket.getaddrinfo(addr_str, 0, 0, socket.SOCK_STREAM, socket.IPPROTO_TCP, socket.AI_NUMERICHOST)
+ return addrs[0][0]
+
+def get_match_for_proxy(proxy_addr, transport_list):
+ af = addr_af(proxy_addr[0])
+ try:
+ return Endpoints.match(CLIENTS[af], RELAYS[af], transport_list)
+ except ValueError as e:
+ raise UnknownTransport("Could not find registration for transport list: %s: %s" % (transport_list, e))
+
+def get_check_back_in_for_proxy(proxy_addr):
+ """Get a CHECK-BACK-IN interval suitable for this proxy."""
+ return POLL_INTERVAL
+
+def put_reg(reg):
+ """Add a registration."""
+ af = addr_af(reg.addr[0])
+ return CLIENTS[af].addEndpoint(reg.addr, reg.transport)
+
+def parse_relay_file(servers, fp):
+ """Parse a file containing Tor relays that we can point proxies to.
+ Throws ValueError on a parsing error. Each line contains a transport chain
+ and an address, for example
+ obfs2|websocket 1.4.6.1:4123
+ :returns: number of relays added
+ """
+ n = 0
+ for line in fp.readlines():
+ line = line.strip("\n")
+ if not line or line.startswith('#'): continue
+ try:
+ transport_spec, addr_spec = line.strip().split()
+ except ValueError, e:
+ raise ValueError("Wrong line format: %s." % repr(line))
+ addr = parse_addr_spec(addr_spec, defport=DEFAULT_RELAY_PORT)
+ transport = Transport.parse(transport_spec)
+ if transport.outer not in options.outer_transports:
+ raise ValueError(u"Unrecognized transport: %s" % transport)
+ af = addr_af(addr[0])
+ servers[af].addEndpoint(addr, transport)
+ n += 1
+ return n
+
+def main():
+ opts, args = getopt.gnu_getopt(sys.argv[1:], "dhl:p:r:", [
+ "debug",
+ "help",
+ "log=",
+ "port=",
+ "pidfile=",
+ "privdrop-user=",
+ "relay-file=",
+ "unsafe-logging",
+ ])
+ for o, a in opts:
+ if o == "-d" or o == "--debug":
+ options.daemonize = False
+ options.log_filename = None
+ elif o == "-h" or o == "--help":
+ usage()
+ sys.exit()
+ elif o == "-l" or o == "--log":
+ options.log_filename = a
+ elif o == "-p" or o == "--port":
+ options.listen_port = int(a)
+ elif o == "--pidfile":
+ options.pid_filename = a
+ elif o == "--privdrop-user":
+ options.privdrop_username = a
+ elif o == "-r" or o == "--relay-file":
+ options.relay_filename = a
+ elif o == "--outer-transports":
+ options.outer_transports = a.split(",")
+ elif o == "--unsafe-logging":
+ options.safe_logging = False
+
+ if not options.relay_filename:
+ print >> sys.stderr, """\
+The -r option is required. Give it the name of a file
+containing relay transports and addresses.
+ -r HOST[:PORT]
+Example file contents:
+obfs2|websocket 1.4.6.1:4123\
+"""
+ sys.exit(1)
+
+ try:
+ with open(options.relay_filename) as fp:
+ n = parse_relay_file(RELAYS, fp)
+ if not n:
+ raise ValueError("file contained no relays")
+ except ValueError as e:
+ print >> sys.stderr, u"Could not parse file %s: %s" % (repr(options.relay_filename), str(e))
+ sys.exit(1)
+
+ # Setup log file
+ if options.log_filename:
+ options.log_file = open(options.log_filename, "a")
+ # Send error tracebacks to the log.
+ sys.stderr = options.log_file
+ else:
+ options.log_file = sys.stdout
+
+ addrinfo = socket.getaddrinfo(LISTEN_ADDRESS, options.listen_port, 0, socket.SOCK_STREAM, socket.IPPROTO_TCP)[0]
+
+ server = Server(addrinfo[4], Handler)
+
+ log(u"start on %s" % format_addr(addrinfo[4]))
+ log(u"using IPv4 relays %s" % str(RELAYS[socket.AF_INET]._endpoints))
+ log(u"using IPv6 relays %s" % str(RELAYS[socket.AF_INET6]._endpoints))
+
+ if options.daemonize:
+ log(u"daemonizing")
+ pid = os.fork()
+ if pid != 0:
+ if options.pid_filename:
+ f = open(options.pid_filename, "w")
+ print >> f, pid
+ f.close()
+ sys.exit(0)
+
+ if options.privdrop_username is not None:
+ log(u"dropping privileges to those of user %s" % options.privdrop_username)
+ try:
+ proc.drop_privs(options.privdrop_username)
+ except BaseException, e:
+ print >> sys.stderr, "Can't drop privileges:", str(e)
+ sys.exit(1)
+
+ try:
+ server.serve_forever()
+ except KeyboardInterrupt:
+ sys.exit(0)
+
+if __name__ == "__main__":
+ main()
diff --git a/facilitator/facilitator-email-poller b/facilitator/facilitator-email-poller
new file mode 100755
index 0000000..b3b9b14
--- /dev/null
+++ b/facilitator/facilitator-email-poller
@@ -0,0 +1,405 @@
+#!/usr/bin/env python
+"""
+Polls a mailbox for new registrations and forwards them using facilitator-reg.
+"""
+
+import calendar
+import datetime
+import email
+import email.utils
+import getopt
+import imaplib
+import math
+import os
+import re
+import socket
+import ssl
+import stat
+import sys
+import tempfile
+import time
+
+from flashproxy import fac
+from flashproxy import keys
+from flashproxy import proc
+from flashproxy.util import parse_addr_spec
+
+from hashlib import sha1
+from M2Crypto import SSL
+
+# TODO(infinity0): we only support gmail so this is OK for now. in the future,
+# could maybe do an MX lookup and try to guess the imap server from that.
+DEFAULT_IMAP_HOST = "imap.gmail.com"
+DEFAULT_IMAP_PORT = 993
+DEFAULT_LOG_FILENAME = "facilitator-email-poller.log"
+
+POLL_INTERVAL = 60
+# Ignore message older than this many seconds old, or newer than this many
+# seconds in the future.
+REGISTRATION_AGE_LIMIT = 30 * 60
+
+FACILITATOR_ADDR = ("127.0.0.1", 9002)
+
+LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
+
+class options(object):
+ password_filename = None
+ log_filename = DEFAULT_LOG_FILENAME
+ log_file = sys.stdout
+ daemonize = True
+ pid_filename = None
+ privdrop_username = None
+ safe_logging = True
+ imaplib_debug = False
+ use_certificate_pin = True
+
+# Like socket.create_connection in that it tries resolving different address
+# families, but doesn't connect the socket.
+def create_socket(address, timeout = None, source_address = None):
+ host, port = address
+ addrs = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM)
+ if not addrs:
+ raise socket.error("getaddrinfo returns an empty list")
+ err = None
+ for addr in addrs:
+ try:
+ s = socket.socket(addr[0], addr[1], addr[2])
+ if timeout is not None and type(timeout) == float:
+ s.settimeout(timeout)
+ if source_address is not None:
+ s.bind(source_address)
+ return s
+ except Exception, e:
+ err = e
+ raise err
+
+class IMAP4_SSL_REQUIRED(imaplib.IMAP4_SSL):
+ """A subclass of of IMAP4_SSL that uses ssl_version=ssl.PROTOCOL_TLSv1 and
+ cert_reqs=ssl.CERT_REQUIRED."""
+ def open(self, host = "", port = imaplib.IMAP4_SSL_PORT):
+ ctx = SSL.Context("tlsv1")
+ ctx.set_verify(SSL.verify_peer, 3)
+ ret = ctx.load_verify_locations(self.certfile)
+ assert ret == 1
+
+ self.host = host
+ self.port = port
+ self.sock = create_socket((self.host, self.port))
+
+ self.sslobj = SSL.Connection(ctx, self.sock)
+ self.sslobj.connect((self.host, self.port))
+ self.file = self.sslobj.makefile('rb')
+
+def usage(f = sys.stdout):
+ print >> f, """\
+Usage: %(progname)s --pass=PASSFILE
+Facilitator-side helper for the facilitator-reg-email rendezvous. Polls
+an IMAP server for email messages with client registrations, deletes
+them, and forwards the registrations to the facilitator.
+
+ -d, --debug don't daemonize, log to stdout.
+ --disable-pin don't check server public key against a known pin.
+ -h, --help show this help.
+ --imaplib-debug show raw IMAP messages (will include email password).
+ -l, --log FILENAME write log to FILENAME (default \"%(log)s\").
+ -p, --pass=PASSFILE use the email/password contained in PASSFILE. This file
+ should contain "[<imap_host>] <email> <password>" on a
+ single line, separated by whitespace. If <imap_host> is
+ omitted, it defaults to imap.(<email> domain):993.
+ --pidfile FILENAME write PID to FILENAME after daemonizing.
+ --privdrop-user USER switch UID and GID to those of USER.
+ --unsafe-logging don't scrub email password and IP addresses from logs.\
+""" % {
+ "progname": sys.argv[0],
+ "log": DEFAULT_LOG_FILENAME,
+}
+
+def safe_str(s):
+ """Return "[scrubbed]" if options.safe_logging is true, and s otherwise."""
+ if options.safe_logging:
+ return "[scrubbed]"
+ else:
+ return s
+
+def log(msg):
+ print >> options.log_file, (u"%s %s" % (time.strftime(LOG_DATE_FORMAT), msg)).encode("UTF-8")
+ options.log_file.flush()
+
+def main():
+ opts, args = getopt.gnu_getopt(sys.argv[1:], "de:hi:l:p:", [
+ "debug",
+ "disable-pin",
+ "email=",
+ "help",
+ "imap=",
+ "imaplib-debug",
+ "log=",
+ "pass=",
+ "pidfile=",
+ "privdrop-user=",
+ "unsafe-logging",
+ ])
+ for o, a in opts:
+ if o == "-d" or o == "--debug":
+ options.daemonize = False
+ options.log_filename = None
+ elif o == "--disable-pin":
+ options.use_certificate_pin = False
+ elif o == "-h" or o == "--help":
+ usage()
+ sys.exit()
+ if o == "--imaplib-debug":
+ options.imaplib_debug = True
+ elif o == "-l" or o == "--log":
+ options.log_filename = a
+ elif o == "-p" or o == "--pass":
+ options.password_filename = a
+ elif o == "--pidfile":
+ options.pid_filename = a
+ elif o == "--privdrop-user":
+ options.privdrop_username = a
+ elif o == "--unsafe-logging":
+ options.safe_logging = False
+
+ if len(args) != 0:
+ usage(sys.stderr)
+ sys.exit(1)
+
+ # Load the email password.
+ if options.password_filename is None:
+ print >> sys.stderr, "The --pass option is required."
+ sys.exit(1)
+ try:
+ password_file = open(options.password_filename)
+ except Exception, e:
+ print >> sys.stderr, """\
+ Failed to open password file "%s": %s.\
+ """ % (options.password_filename, str(e))
+ sys.exit(1)
+ try:
+ if not proc.check_perms(password_file.fileno()):
+ print >> sys.stderr, "Refusing to run with group- or world-readable password file. Try"
+ print >> sys.stderr, "\tchmod 600 %s" % options.password_filename
+ sys.exit(1)
+ for (lineno0, line) in enumerate(password_file.readlines()):
+ line = line.strip("\n")
+ if not line or line.startswith('#'): continue
+ # we do this stricter regex match because passwords might have spaces in
+ res = re.match(r"(?:(\S+)\s)?(\S+@\S+)\s(.+)", line)
+ if not res:
+ raise ValueError("could not find email or password on line %s" % (lineno0+1))
+ (imap_addr_spec, email_addr, email_password) = res.groups()
+ imap_addr = parse_addr_spec(
+ imap_addr_spec or "", DEFAULT_IMAP_HOST, DEFAULT_IMAP_PORT)
+ break
+ else:
+ raise ValueError("no email line found")
+ except Exception, e:
+ print >> sys.stderr, """\
+ Failed to parse password file "%s": %s.
+ Syntax is [<imap_host>] <email> <password>.
+ """ % (options.password_filename, str(e))
+ sys.exit(1)
+ finally:
+ password_file.close()
+
+ if options.log_filename:
+ options.log_file = open(options.log_filename, "a")
+ # Send error tracebacks to the log.
+ sys.stderr = options.log_file
+ else:
+ options.log_file = sys.stdout
+
+ if options.daemonize:
+ log(u"daemonizing")
+ pid = os.fork()
+ if pid != 0:
+ if options.pid_filename:
+ f = open(options.pid_filename, "w")
+ print >> f, pid
+ f.close()
+ sys.exit(0)
+
+ if options.privdrop_username is not None:
+ log(u"dropping privileges to those of user %s" % options.privdrop_username)
+ try:
+ proc.drop_privs(options.privdrop_username)
+ except BaseException, e:
+ print >> sys.stderr, "Can't drop privileges:", str(e)
+ sys.exit(1)
+
+ if options.imaplib_debug:
+ imaplib.Debug = 4
+
+ login_limit = RateLimit()
+ while True:
+ try:
+ imap = imap_login(imap_addr, email_addr, email_password)
+ try:
+ imap_loop(imap)
+ except imaplib.IMAP4.error:
+ imap.close()
+ imap.logout()
+ except (imaplib.IMAP4.error, ssl.SSLError, SSL.SSLError, socket.error), e:
+ # Try again after a disconnection.
+ log(u"lost server connection: %s" % str(e))
+ except KeyboardInterrupt:
+ break
+
+ # Don't reconnect too fast.
+ t = login_limit.time_to_wait()
+ if t > 0:
+ log(u"waiting %.2f seconds before logging in again" % t)
+ time.sleep(t)
+
+ log(u"closing")
+ imap.close()
+ imap.logout()
+
+def message_get_date(msg):
+ """Get the datetime when the message was received by reading the X-Received
+ header, relative to UTC. Returns None on error."""
+ x_received = msg["X-Received"]
+ if x_received is None:
+ log(u"X-Received is missing")
+ return None
+ try:
+ _, date_str = x_received.rsplit(";", 1)
+ date_str = date_str.strip()
+ except ValueError:
+ log(u"can't parse X-Received %s" % repr(x_received))
+ return None
+ date_tuple = email.utils.parsedate_tz(date_str)
+ if date_tuple is None:
+ log(u"can't parse X-Received date string %s" % repr(date_str))
+ return None
+ timestamp_utc = calendar.timegm(date_tuple[:8] + (0,)) - date_tuple[9]
+ return datetime.datetime.utcfromtimestamp(timestamp_utc)
+
+def message_ok(msg):
+ date = message_get_date(msg)
+ if date is not None:
+ now = datetime.datetime.utcnow()
+ age = time.mktime(now.utctimetuple()) - time.mktime(date.utctimetuple())
+ if age > REGISTRATION_AGE_LIMIT:
+ log(u"message dated %s UTC is too old: %d seconds" % (date, age))
+ return False
+ if -age > REGISTRATION_AGE_LIMIT:
+ log(u"message dated %s UTC is from the future: %d seconds" % (date, -age))
+ return False
+ return True
+
+def handle_message(msg):
+ try:
+ if fac.put_reg_proc(["facilitator-reg"], msg.get_payload()):
+ log(u"registered client")
+ else:
+ log(u"failed to register client")
+ except Exception, e:
+ log(u"error registering client")
+ raise
+
+def truncate_repr(s, n):
+ if not isinstance(s, basestring):
+ s = repr(s)
+ if len(s) > n:
+ return repr(s[:n]) + "[...]"
+ else:
+ return repr(s)
+def check_imap_return(typ, data):
+ if typ != "OK":
+ raise imaplib.IMAP4.abort("Got type \"%s\": %s" % (typ, truncate_repr(data, 100)))
+
+def imap_get_uid(imap, index):
+ typ, data = imap.fetch(str(index), "(UID)")
+ if data[0] is None:
+ return None
+ check_imap_return(typ, data)
+ # This grepping for the UID is bogus, but imaplib doesn't properly break up
+ # the list of name-value pairs for us.
+ m = re.match(r'^\d+\s+\(.*\bUID\s+(\d+)\b.*\)\s*$', data[0])
+ if m is None:
+ raise imaplib.IMAP4.abort("Can't find UID in %s" % repr(data[0]))
+ return m.group(1)
+
+# Gmail's IMAP folders are funny: they are not real folders, but actually views
+# of messages having a particular label. INBOX consists of messages having the
+# INBOX label, for example. Deleting a message from a folder just removes its
+# label, but the message itself continues to exist in "[Gmail]/All Mail".
+# https://support.google.com/mail/bin/answer.py?answer=78755
+# http://gmailblog.blogspot.com/2008/10/new-in-labs-advanced-imap-controls.html
+# To really delete a message, you must copy it to "[Gmail]/Trash" and then
+# delete it from there. Messages in Trash are deleted automatically after 30
+# days, but we do it immediately.
+def imap_loop(imap):
+ while True:
+ # Copy all messages to Trash, and work on them from there. This is a
+ # failsafe so that messages will eventually be deleted if we are not
+ # able to retrieve them. This act of copying also deletes from All Mail.
+ typ, data = imap.select("[Gmail]/All Mail")
+ check_imap_return(typ, data)
+ imap.copy("1:*", "[Gmail]/Trash")
+
+ typ, data = imap.select("[Gmail]/Trash")
+ check_imap_return(typ, data)
+ exists = int(data[0])
+ if exists > 0:
+ while True:
+ # Grab message 1 on each iteration; remaining messages shift down so
+ # the next message we process is also message 1.
+ uid = imap_get_uid(imap, "1")
+ if uid is None:
+ break
+
+ typ, data = imap.uid("FETCH", uid, "(BODY[])")
+ check_imap_return(typ, data)
+ msg_text = data[0][1]
+ typ, data = imap.uid("STORE", uid, "+FLAGS", "\\Deleted")
+ check_imap_return(typ, data)
+ typ, data = imap.expunge()
+ check_imap_return(typ, data)
+
+ try:
+ msg = email.message_from_string(msg_text)
+ if message_ok(msg):
+ handle_message(msg)
+ except Exception, e:
+ log("Error processing message, deleting anyway: %s" % str(e))
+
+ time.sleep(POLL_INTERVAL)
+
+def imap_login(imap_addr, email_addr, email_password):
+ """Make an IMAP connection, check the certificate and public key, and log in."""
+ with keys.temp_cert(keys.PIN_GOOGLE_CA_CERT) as ca_certs_file:
+ imap = IMAP4_SSL_REQUIRED(
+ imap_addr[0], imap_addr[1], None, ca_certs_file.name)
+
+ if options.use_certificate_pin:
+ keys.check_certificate_pin(imap.ssl(), keys.PIN_GOOGLE_PUBKEY_SHA1)
+
+ log(u"logging in as %s" % email_addr)
+ imap.login(email_addr, email_password)
+
+ return imap
+
+class RateLimit(object):
+ INITIAL_INTERVAL = 1.0
+ # These constants are chosen to reach a steady state of one attempt every
+ # ten minutes, assuming a new failing attempt after each penalty interval.
+ MAX_INTERVAL = 10 * 60
+ MULTIPLIER = 2.0
+ DECAY = math.log(MULTIPLIER) / MAX_INTERVAL
+ def __init__(self):
+ self.time_last = time.time()
+ self.interval = self.INITIAL_INTERVAL
+ def time_to_wait(self):
+ now = time.time()
+ delta = now - self.time_last
+ # Discount time already served.
+ wait = max(self.interval - delta, 0)
+ self.time_last = now
+ self.interval = self.interval * math.exp(-self.DECAY * delta) * self.MULTIPLIER
+ return wait
+
+if __name__ == "__main__":
+ main()
diff --git a/facilitator/facilitator-reg b/facilitator/facilitator-reg
new file mode 100755
index 0000000..3a3d196
--- /dev/null
+++ b/facilitator/facilitator-reg
@@ -0,0 +1,70 @@
+#!/usr/bin/env python
+"""
+Forwards encrypted client registrations to a running facilitator-reg-daemon.
+"""
+
+import getopt
+import socket
+import sys
+
+CONNECT_ADDRESS = "127.0.0.1"
+DEFAULT_CONNECT_PORT = 9003
+
+class options(object):
+ connect_port = DEFAULT_CONNECT_PORT
+
+def usage(f = sys.stdout):
+ print >> f, """\
+Usage: %(progname)s
+Reads a base64-encoded encrypted client registration from stdin and
+feeds it to a local facilitator-reg-daemon process. Returns 0 if the
+registration was successful, 1 otherwise.
+
+ -h, --help show this help.
+ -p, --port PORT connect to PORT (default %(port)d).\
+""" % {
+ "progname": sys.argv[0],
+ "port": DEFAULT_CONNECT_PORT,
+}
+
+def main():
+ opts, args = getopt.gnu_getopt(sys.argv[1:], "hp:", [
+ "help",
+ "port=",
+ ])
+ for o, a in opts:
+ if o == "-h" or o == "--help":
+ usage()
+ sys.exit()
+ elif o == "-p" or o == "--port":
+ options.connect_port = int(a)
+
+ if len(args) != 0:
+ usage(sys.stderr)
+ sys.exit(1)
+
+ addrinfo = socket.getaddrinfo(CONNECT_ADDRESS, options.connect_port, 0, socket.SOCK_STREAM, socket.IPPROTO_TCP)[0]
+
+ s = socket.socket(addrinfo[0], addrinfo[1], addrinfo[2])
+ s.connect(addrinfo[4])
+
+ sent = 0
+ while True:
+ data = sys.stdin.read(1024)
+ if data == "":
+ mod = sent % 4
+ if mod != 0:
+ s.sendall((4 - mod) * "=")
+ break
+ s.sendall(data)
+ sent += len(data)
+ s.shutdown(socket.SHUT_WR)
+ data = s.recv(1024)
+
+ if data.strip() == "OK":
+ sys.exit(0)
+ else:
+ sys.exit(1)
+
+if __name__ == "__main__":
+ main()
diff --git a/facilitator/facilitator-reg-daemon b/facilitator/facilitator-reg-daemon
new file mode 100755
index 0000000..bba5aab
--- /dev/null
+++ b/facilitator/facilitator-reg-daemon
@@ -0,0 +1,217 @@
+#!/usr/bin/env python
+"""
+Accepts encrypted client registrations and forwards them to the facilitator.
+"""
+
+import SocketServer
+import getopt
+import os
+import socket
+import sys
+import threading
+import time
+
+from flashproxy import fac
+from flashproxy import proc
+from flashproxy.util import format_addr
+
+from M2Crypto import RSA
+
+# Generating an RSA keypair for use by this program:
+# openssl genrsa -out /etc/flashproxy/reg-daemon.key 2048
+# chmod 600 /etc/flashproxy/reg-daemon.key
+
+LISTEN_ADDRESS = "127.0.0.1"
+DEFAULT_LISTEN_PORT = 9003
+FACILITATOR_ADDR = ("127.0.0.1", 9002)
+DEFAULT_LOG_FILENAME = "facilitator-reg-daemon.log"
+
+# Don't indulge clients for more than this many seconds.
+CLIENT_TIMEOUT = 1.0
+# Buffer no more than this many bytes per connection.
+MAX_LENGTH = 40 * 1024
+
+LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
+
+class options(object):
+ key_filename = None
+ listen_port = DEFAULT_LISTEN_PORT
+ log_filename = DEFAULT_LOG_FILENAME
+ log_file = sys.stdout
+ daemonize = True
+ pid_filename = None
+ privdrop_username = None
+ safe_logging = True
+
+def usage(f = sys.stdout):
+ print >> f, """\
+Usage: %(progname)s --key=KEYFILE
+Facilitator-side daemon that reads base64-encoded encrypted client
+registrations and registers them with a local facilitator. This program
+exists on its own in order to isolate the reading of key material in a
+single process.
+
+ -d, --debug don't daemonize, log to stdout.
+ -h, --help show this help.
+ -k, --key=KEYFILE read the private key from KEYFILE (required).
+ -l, --log FILENAME write log to FILENAME (default \"%(log)s\").
+ -p, --port PORT listen on PORT (default %(port)d).
+ --pidfile FILENAME write PID to FILENAME after daemonizing.
+ --privdrop-user USER switch UID and GID to those of USER.
+ --unsafe-logging don't scrub IP addresses from logs.\
+""" % {
+ "progname": sys.argv[0],
+ "log": DEFAULT_LOG_FILENAME,
+ "port": DEFAULT_LISTEN_PORT,
+}
+
+def safe_str(s):
+ """Return "[scrubbed]" if options.safe_logging is true, and s otherwise."""
+ if options.safe_logging:
+ return "[scrubbed]"
+ else:
+ return s
+
+log_lock = threading.Lock()
+def log(msg):
+ log_lock.acquire()
+ try:
+ print >> options.log_file, (u"%s %s" % (time.strftime(LOG_DATE_FORMAT), msg)).encode("UTF-8")
+ options.log_file.flush()
+ finally:
+ log_lock.release()
+
+class Handler(SocketServer.StreamRequestHandler):
+ def __init__(self, *args, **kwargs):
+ self.deadline = time.time() + CLIENT_TIMEOUT
+ self.buffer = ""
+ SocketServer.StreamRequestHandler.__init__(self, *args, **kwargs)
+
+ def recv(self):
+ timeout = self.deadline - time.time()
+ self.connection.settimeout(timeout)
+ return self.connection.recv(1024)
+
+ def read_input(self):
+ while True:
+ data = self.recv()
+ if not data:
+ break
+ self.buffer += data
+ buflen = len(self.buffer)
+ if buflen > MAX_LENGTH:
+ raise socket.error("refusing to buffer %d bytes (last read was %d bytes)" % (buflen, len(data)))
+ return self.buffer
+
+ @proc.catch_epipe
+ def handle(self):
+ try:
+ b64_ciphertext = self.read_input()
+ except socket.error, e:
+ log("socket error reading input: %s" % str(e))
+ return
+ try:
+ ciphertext = b64_ciphertext.decode("base64")
+ plaintext = rsa.private_decrypt(ciphertext, RSA.pkcs1_oaep_padding)
+ for client_reg in fac.read_client_registrations(plaintext):
+ log(u"registering %s" % safe_str(format_addr(client_reg.addr)))
+ if not fac.put_reg(FACILITATOR_ADDR, client_reg.addr, client_reg.transport):
+ print >> self.wfile, "FAIL"
+ break
+ else:
+ print >> self.wfile, "OK"
+ except Exception, e:
+ log("error registering: %s" % str(e))
+ print >> self.wfile, "FAIL"
+ raise
+
+ finish = proc.catch_epipe(SocketServer.StreamRequestHandler.finish)
+
+class Server(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
+ allow_reuse_address = True
+
+def main():
+ global rsa
+
+ opts, args = getopt.gnu_getopt(sys.argv[1:], "dhk:l:p:",
+ ["debug", "help", "key=", "log=", "port=", "pidfile=", "privdrop-user=", "unsafe-logging"])
+ for o, a in opts:
+ if o == "-d" or o == "--debug":
+ options.daemonize = False
+ options.log_filename = None
+ elif o == "-h" or o == "--help":
+ usage()
+ sys.exit()
+ elif o == "-k" or o == "--key":
+ options.key_filename = a
+ elif o == "-l" or o == "--log":
+ options.log_filename = a
+ elif o == "-p" or o == "--pass":
+ options.listen_port = int(a)
+ elif o == "--pidfile":
+ options.pid_filename = a
+ elif o == "--privdrop-user":
+ options.privdrop_username = a
+ elif o == "--unsafe-logging":
+ options.safe_logging = False
+
+ if len(args) != 0:
+ usage(sys.stderr)
+ sys.exit(1)
+
+ # Load the private key.
+ if options.key_filename is None:
+ print >> sys.stderr, "The --key option is required."
+ sys.exit(1)
+ try:
+ key_file = open(options.key_filename)
+ except Exception, e:
+ print >> sys.stderr, "Failed to open private key file \"%s\": %s." % (options.key_filename, str(e))
+ sys.exit(1)
+ try:
+ if not proc.check_perms(key_file.fileno()):
+ print >> sys.stderr, "Refusing to run with group- or world-readable private key file. Try"
+ print >> sys.stderr, "\tchmod 600 %s" % options.key_filename
+ sys.exit(1)
+ rsa = RSA.load_key_string(key_file.read())
+ finally:
+ key_file.close()
+
+ if options.log_filename:
+ options.log_file = open(options.log_filename, "a")
+ # Send error tracebacks to the log.
+ sys.stderr = options.log_file
+ else:
+ options.log_file = sys.stdout
+
+ addrinfo = socket.getaddrinfo(LISTEN_ADDRESS, options.listen_port, 0, socket.SOCK_STREAM, socket.IPPROTO_TCP)[0]
+
+ server = Server(addrinfo[4], Handler)
+
+ log(u"start on %s" % format_addr(addrinfo[4]))
+
+ if options.daemonize:
+ log(u"daemonizing")
+ pid = os.fork()
+ if pid != 0:
+ if options.pid_filename:
+ f = open(options.pid_filename, "w")
+ print >> f, pid
+ f.close()
+ sys.exit(0)
+
+ if options.privdrop_username is not None:
+ log(u"dropping privileges to those of user %s" % options.privdrop_username)
+ try:
+ proc.drop_privs(options.privdrop_username)
+ except BaseException, e:
+ print >> sys.stderr, "Can't drop privileges:", str(e)
+ sys.exit(1)
+
+ try:
+ server.serve_forever()
+ except KeyboardInterrupt:
+ sys.exit(0)
+
+if __name__ == "__main__":
+ main()
diff --git a/facilitator/facilitator-test.py b/facilitator/facilitator-test.py
new file mode 100755
index 0000000..fd4ac88
--- /dev/null
+++ b/facilitator/facilitator-test.py
@@ -0,0 +1,316 @@
+#!/usr/bin/env python
+
+from cStringIO import StringIO
+import os
+import socket
+import subprocess
+import tempfile
+import sys
+import time
+import unittest
+
+from flashproxy import fac
+from flashproxy.reg import Transport, Endpoint
+from flashproxy.util import format_addr
+
+# Import the facilitator program as a module.
+import imp
+dont_write_bytecode = sys.dont_write_bytecode
+sys.dont_write_bytecode = True
+facilitator = imp.load_source("facilitator", os.path.join(os.path.dirname(__file__), "facilitator"))
+Endpoints = facilitator.Endpoints
+parse_relay_file = facilitator.parse_relay_file
+sys.dont_write_bytecode = dont_write_bytecode
+del dont_write_bytecode
+del facilitator
+
+FACILITATOR_HOST = "127.0.0.1"
+FACILITATOR_PORT = 39002 # diff port to not conflict with production service
+FACILITATOR_ADDR = (FACILITATOR_HOST, FACILITATOR_PORT)
+CLIENT_TP = "websocket"
+RELAY_TP = "websocket"
+PROXY_TPS = ["websocket", "webrtc"]
+
+def gimme_socket(host, port):
+ addrinfo = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM, socket.IPPROTO_TCP)[0]
+ s = socket.socket(addrinfo[0], addrinfo[1], addrinfo[2])
+ s.settimeout(10.0)
+ s.connect(addrinfo[4])
+ return s
+
+class EndpointsTest(unittest.TestCase):
+
+ def setUp(self):
+ self.pts = Endpoints(af=socket.AF_INET)
+
+ def test_addEndpoints_twice(self):
+ self.pts.addEndpoint("A", "a|b|p")
+ self.assertFalse(self.pts.addEndpoint("A", "zzz"))
+ self.assertEquals(self.pts._endpoints["A"], Transport("a|b", "p"))
+
+ def test_delEndpoints_twice(self):
+ self.pts.addEndpoint("A", "a|b|p")
+ self.assertTrue(self.pts.delEndpoint("A"))
+ self.assertFalse(self.pts.delEndpoint("A"))
+ self.assertEquals(self.pts._endpoints.get("A"), None)
+
+ def test_Endpoints_indexing(self):
+ self.assertEquals(self.pts._indexes.get("p"), None)
+ # test defaultdict works as expected
+ self.assertEquals(self.pts._indexes["p"]["a|b"], set(""))
+ self.pts.addEndpoint("A", "a|b|p")
+ self.assertEquals(self.pts._indexes["p"]["a|b"], set("A"))
+ self.pts.addEndpoint("B", "a|b|p")
+ self.assertEquals(self.pts._indexes["p"]["a|b"], set("AB"))
+ self.pts.delEndpoint("A")
+ self.assertEquals(self.pts._indexes["p"]["a|b"], set("B"))
+ self.pts.delEndpoint("B")
+ self.assertEquals(self.pts._indexes["p"]["a|b"], set(""))
+
+ def test_serveReg_maxserve_infinite_roundrobin(self):
+ # case for servers, they never exhaust
+ self.pts.addEndpoint("A", "a|p")
+ self.pts.addEndpoint("B", "a|p")
+ self.pts.addEndpoint("C", "a|p")
+ for i in xrange(64): # 64 is infinite ;)
+ served = set()
+ served.add(self.pts._serveReg("ABC").addr)
+ served.add(self.pts._serveReg("ABC").addr)
+ served.add(self.pts._serveReg("ABC").addr)
+ self.assertEquals(served, set("ABC"))
+
+ def test_serveReg_maxserve_finite_exhaustion(self):
+ # case for clients, we don't want to keep serving them
+ self.pts = Endpoints(af=socket.AF_INET, maxserve=5)
+ self.pts.addEndpoint("A", "a|p")
+ self.pts.addEndpoint("B", "a|p")
+ self.pts.addEndpoint("C", "a|p")
+ # test getNumUnservedEndpoints whilst we're at it
+ self.assertEquals(self.pts.getNumUnservedEndpoints(), 3)
+ served = set()
+ served.add(self.pts._serveReg("ABC").addr)
+ self.assertEquals(self.pts.getNumUnservedEndpoints(), 2)
+ served.add(self.pts._serveReg("ABC").addr)
+ self.assertEquals(self.pts.getNumUnservedEndpoints(), 1)
+ served.add(self.pts._serveReg("ABC").addr)
+ self.assertEquals(self.pts.getNumUnservedEndpoints(), 0)
+ self.assertEquals(served, set("ABC"))
+ for i in xrange(5-2):
+ served = set()
+ served.add(self.pts._serveReg("ABC").addr)
+ served.add(self.pts._serveReg("ABC").addr)
+ served.add(self.pts._serveReg("ABC").addr)
+ self.assertEquals(served, set("ABC"))
+ remaining = set("ABC")
+ remaining.remove(self.pts._serveReg(remaining).addr)
+ self.assertRaises(KeyError, self.pts._serveReg, "ABC")
+ remaining.remove(self.pts._serveReg(remaining).addr)
+ self.assertRaises(KeyError, self.pts._serveReg, "ABC")
+ remaining.remove(self.pts._serveReg(remaining).addr)
+ self.assertRaises(KeyError, self.pts._serveReg, "ABC")
+ self.assertEquals(remaining, set())
+ self.assertEquals(self.pts.getNumUnservedEndpoints(), 0)
+
+ def test_match_normal(self):
+ self.pts.addEndpoint("A", "a|p")
+ self.pts2 = Endpoints(af=socket.AF_INET)
+ self.pts2.addEndpoint("B", "a|p")
+ self.pts2.addEndpoint("C", "b|p")
+ self.pts2.addEndpoint("D", "a|q")
+ expected = (Endpoint("A", Transport("a","p")), Endpoint("B", Transport("a","p")))
+ empty = Endpoints.EMPTY_MATCH
+ self.assertEquals(expected, Endpoints.match(self.pts, self.pts2, ["p"]))
+ self.assertEquals(empty, Endpoints.match(self.pts, self.pts2, ["x"]))
+
+ def test_match_unequal_client_server(self):
+ self.pts.addEndpoint("A", "a|p")
+ self.pts2 = Endpoints(af=socket.AF_INET)
+ self.pts2.addEndpoint("B", "a|q")
+ expected = (Endpoint("A", Transport("a","p")), Endpoint("B", Transport("a","q")))
+ empty = Endpoints.EMPTY_MATCH
+ self.assertEquals(expected, Endpoints.match(self.pts, self.pts2, ["p", "q"]))
+ self.assertEquals(empty, Endpoints.match(self.pts, self.pts2, ["p"]))
+ self.assertEquals(empty, Endpoints.match(self.pts, self.pts2, ["q"]))
+ self.assertEquals(empty, Endpoints.match(self.pts, self.pts2, ["x"]))
+
+ def test_match_raw_server(self):
+ self.pts.addEndpoint("A", "p")
+ self.pts2 = Endpoints(af=socket.AF_INET)
+ self.pts2.addEndpoint("B", "p")
+ expected = (Endpoint("A", Transport("","p")), Endpoint("B", Transport("","p")))
+ empty = Endpoints.EMPTY_MATCH
+ self.assertEquals(expected, Endpoints.match(self.pts, self.pts2, ["p"]))
+ self.assertEquals(empty, Endpoints.match(self.pts, self.pts2, ["x"]))
+
+ def test_match_many_inners(self):
+ self.pts.addEndpoint("A", "a|p")
+ self.pts.addEndpoint("B", "b|p")
+ self.pts.addEndpoint("C", "p")
+ self.pts2 = Endpoints(af=socket.AF_INET)
+ self.pts2.addEndpoint("D", "a|p")
+ self.pts2.addEndpoint("E", "b|p")
+ self.pts2.addEndpoint("F", "p")
+ # this test ensures we have a sane policy for selecting between inners pools
+ expected = set()
+ expected.add((Endpoint("A", Transport("a","p")), Endpoint("D", Transport("a","p"))))
+ expected.add((Endpoint("B", Transport("b","p")), Endpoint("E", Transport("b","p"))))
+ expected.add((Endpoint("C", Transport("","p")), Endpoint("F", Transport("","p"))))
+ result = set()
+ result.add(Endpoints.match(self.pts, self.pts2, ["p"]))
+ result.add(Endpoints.match(self.pts, self.pts2, ["p"]))
+ result.add(Endpoints.match(self.pts, self.pts2, ["p"]))
+ empty = Endpoints.EMPTY_MATCH
+ self.assertEquals(expected, result)
+ self.assertEquals(empty, Endpoints.match(self.pts, self.pts2, ["x"]))
+ self.assertEquals(empty, Endpoints.match(self.pts, self.pts2, ["x"]))
+ self.assertEquals(empty, Endpoints.match(self.pts, self.pts2, ["x"]))
+
+ def test_match_exhaustion(self):
+ self.pts.addEndpoint("A", "p")
+ self.pts2 = Endpoints(af=socket.AF_INET, maxserve=2)
+ self.pts2.addEndpoint("B", "p")
+ Endpoints.match(self.pts2, self.pts, ["p"])
+ Endpoints.match(self.pts2, self.pts, ["p"])
+ empty = Endpoints.EMPTY_MATCH
+ self.assertTrue("B" not in self.pts2._endpoints)
+ self.assertTrue("B" not in self.pts2._indexes["p"][""])
+ self.assertEquals(empty, Endpoints.match(self.pts2, self.pts, ["p"]))
+
+
+class FacilitatorTest(unittest.TestCase):
+
+ def test_parse_relay_file(self):
+ fp = StringIO()
+ fp.write("websocket 0.0.1.0:1\n")
+ fp.flush()
+ fp.seek(0)
+ af = socket.AF_INET
+ servers = { af: Endpoints(af=af) }
+ parse_relay_file(servers, fp)
+ self.assertEquals(servers[af]._endpoints, {('0.0.1.0', 1): Transport('', 'websocket')})
+
+
+class FacilitatorProcTest(unittest.TestCase):
+ IPV4_CLIENT_ADDR = ("1.1.1.1", 9000)
+ IPV6_CLIENT_ADDR = ("[11::11]", 9000)
+ IPV4_PROXY_ADDR = ("2.2.2.2", 13000)
+ IPV6_PROXY_ADDR = ("[22::22]", 13000)
+ IPV4_RELAY_ADDR = ("0.0.1.0", 1)
+ IPV6_RELAY_ADDR = ("[0:0::1:0]", 1)
+
+ def gimme_socket(self):
+ return gimme_socket(FACILITATOR_HOST, FACILITATOR_PORT)
+
+ def setUp(self):
+ self.relay_file = tempfile.NamedTemporaryFile()
+ self.relay_file.write("%s %s\n" % (RELAY_TP, format_addr(self.IPV4_RELAY_ADDR)))
+ self.relay_file.write("%s %s\n" % (RELAY_TP, format_addr(self.IPV6_RELAY_ADDR)))
+ self.relay_file.flush()
+ self.relay_file.seek(0)
+ fn = os.path.join(os.path.dirname(__file__), "./facilitator")
+ self.process = subprocess.Popen(["python", fn, "-d", "-p", str(FACILITATOR_PORT), "-r", self.relay_file.name, "-l", "/dev/null"])
+ time.sleep(0.1)
+
+ def tearDown(self):
+ ret = self.process.poll()
+ if ret is not None:
+ raise Exception("facilitator subprocess exited unexpectedly with status %d" % ret)
+ self.process.terminate()
+
+ def test_timeout(self):
+ """Test that the socket will not accept slow writes indefinitely.
+ Successive sends should not reset the timeout counter."""
+ s = self.gimme_socket()
+ time.sleep(0.3)
+ s.send("w")
+ time.sleep(0.3)
+ s.send("w")
+ time.sleep(0.3)
+ s.send("w")
+ time.sleep(0.3)
+ s.send("w")
+ time.sleep(0.3)
+ self.assertRaises(socket.error, s.send, "w")
+
+ def test_readline_limit(self):
+ """Test that reads won't buffer indefinitely."""
+ s = self.gimme_socket()
+ buflen = 0
+ try:
+ while buflen + 1024 < 200000:
+ s.send("X" * 1024)
+ buflen += 1024
+ # TODO(dcf1): sometimes no error is raised, and this test fails
+ self.fail("should have raised a socket error")
+ except socket.error:
+ pass
+
+ def test_af_v4_v4(self):
+ """Test that IPv4 proxies can get IPv4 clients."""
+ fac.put_reg(FACILITATOR_ADDR, self.IPV4_CLIENT_ADDR, CLIENT_TP)
+ fac.put_reg(FACILITATOR_ADDR, self.IPV6_CLIENT_ADDR, CLIENT_TP)
+ reg = fac.get_reg(FACILITATOR_ADDR, self.IPV4_PROXY_ADDR, PROXY_TPS)
+ self.assertEqual(reg["client"], format_addr(self.IPV4_CLIENT_ADDR))
+
+ def test_af_v4_v6(self):
+ """Test that IPv4 proxies do not get IPv6 clients."""
+ fac.put_reg(FACILITATOR_ADDR, self.IPV6_CLIENT_ADDR, CLIENT_TP)
+ reg = fac.get_reg(FACILITATOR_ADDR, self.IPV4_PROXY_ADDR, PROXY_TPS)
+ self.assertEqual(reg["client"], "")
+
+ def test_af_v6_v4(self):
+ """Test that IPv6 proxies do not get IPv4 clients."""
+ fac.put_reg(FACILITATOR_ADDR, self.IPV4_CLIENT_ADDR, CLIENT_TP)
+ reg = fac.get_reg(FACILITATOR_ADDR, self.IPV6_PROXY_ADDR, PROXY_TPS)
+ self.assertEqual(reg["client"], "")
+
+ def test_af_v6_v6(self):
+ """Test that IPv6 proxies can get IPv6 clients."""
+ fac.put_reg(FACILITATOR_ADDR, self.IPV4_CLIENT_ADDR, CLIENT_TP)
+ fac.put_reg(FACILITATOR_ADDR, self.IPV6_CLIENT_ADDR, CLIENT_TP)
+ reg = fac.get_reg(FACILITATOR_ADDR, self.IPV6_PROXY_ADDR, PROXY_TPS)
+ self.assertEqual(reg["client"], format_addr(self.IPV6_CLIENT_ADDR))
+
+ def test_fields(self):
+ """Test that facilitator responses contain all the required fields."""
+ fac.put_reg(FACILITATOR_ADDR, self.IPV4_CLIENT_ADDR, CLIENT_TP)
+ reg = fac.get_reg(FACILITATOR_ADDR, self.IPV4_PROXY_ADDR, PROXY_TPS)
+ self.assertEqual(reg["client"], format_addr(self.IPV4_CLIENT_ADDR))
+ self.assertEqual(reg["client-transport"], CLIENT_TP)
+ self.assertEqual(reg["relay"], format_addr(self.IPV4_RELAY_ADDR))
+ self.assertEqual(reg["relay-transport"], RELAY_TP)
+ self.assertGreater(int(reg["check-back-in"]), 0)
+
+# def test_same_proxy(self):
+# """Test that the same proxy doesn't get the same client when asking
+# twice."""
+# self.fail()
+#
+# def test_num_clients(self):
+# """Test that the same proxy can pick up up to five different clients but
+# no more. Test that a proxy ceasing to handle a client allows the proxy
+# to handle another, different client."""
+# self.fail()
+#
+# def test_num_proxies(self):
+# """Test that a single client is handed out to five different proxies but
+# no more. Test that a proxy ceasing to handle a client reduces its count
+# so another proxy can handle it."""
+# self.fail()
+#
+# def test_proxy_timeout(self):
+# """Test that a proxy ceasing to connect for some time period causes that
+# proxy's clients to be unhandled by that proxy."""
+# self.fail()
+#
+# def test_localhost_only(self):
+# """Test that the facilitator doesn't listen on any external
+# addresses."""
+# self.fail()
+#
+# def test_hostname(self):
+# """Test that the facilitator rejects hostnames."""
+# self.fail()
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/facilitator/facilitator.cgi b/facilitator/facilitator.cgi
new file mode 100755
index 0000000..10f4b19
--- /dev/null
+++ b/facilitator/facilitator.cgi
@@ -0,0 +1,122 @@
+#!/usr/bin/env python
+
+import cgi
+import os
+import socket
+import sys
+import urllib
+
+from flashproxy import fac
+
+FACILITATOR_ADDR = ("127.0.0.1", 9002)
+
+def output_status(status):
+ print """\
+Status: %d\r
+\r""" % status
+
+def exit_error(status):
+ output_status(status)
+ sys.exit()
+
+# Send a base64-encoded client address to the registration daemon.
+def send_url_reg(reg):
+ # Translate from url-safe base64 alphabet to the standard alphabet.
+ reg = reg.replace('-', '+').replace('_', '/')
+ return fac.put_reg_proc(["facilitator-reg"], reg)
+
+method = os.environ.get("REQUEST_METHOD")
+remote_addr = (os.environ.get("REMOTE_ADDR"), None)
+path_info = os.environ.get("PATH_INFO") or "/"
+
+if not method or not remote_addr[0]:
+ exit_error(400)
+
+# Print the HEAD part of a URL-based registration response, or exit with an
+# error if appropriate.
+def url_reg(reg):
+ try:
+ if send_url_reg(reg):
+ output_status(204)
+ else:
+ exit_error(400)
+ except Exception:
+ exit_error(500)
+
+def do_head():
+ path_parts = [x for x in path_info.split("/") if x]
+ if len(path_parts) == 2 and path_parts[0] == "reg":
+ url_reg(path_parts[1])
+ else:
+ exit_error(400)
+
+def do_get():
+ """Parses flashproxy polls.
+ Example: GET /?r=1&client=7.1.43.21&client=1.2.3.4&transport=webrtc&transport=websocket
+ """
+ fs = cgi.FieldStorage()
+
+ path_parts = [x for x in path_info.split("/") if x]
+ if len(path_parts) == 2 and path_parts[0] == "reg":
+ url_reg(path_parts[1])
+ elif len(path_parts) == 0:
+ # Check for recent enough flash proxy protocol.
+ r = fs.getlist("r")
+ if len(r) != 1 or r[0] != "1":
+ exit_error(400)
+
+ # 'transports' (optional) can be repeated and carries
+ # transport names.
+ transport_list = fs.getlist("transport")
+ if not transport_list:
+ transport_list = ["websocket"]
+
+ try:
+ reg = fac.get_reg(FACILITATOR_ADDR, remote_addr, transport_list) or ""
+ except Exception:
+ exit_error(500)
+ # Allow XMLHttpRequest from any domain. http://www.w3.org/TR/cors/.
+ print """\
+Status: 200\r
+Content-Type: application/x-www-form-urlencoded\r
+Cache-Control: no-cache\r
+Access-Control-Allow-Origin: *\r
+\r"""
+ sys.stdout.write(urllib.urlencode(reg))
+ else:
+ exit_error(400)
+
+def do_post():
+ """Parse client registration."""
+
+ if path_info != "/":
+ exit_error(400)
+
+ # We treat sys.stdin as being a bunch of newline-separated query strings. I
+ # think that this is technically a violation of the
+ # application/x-www-form-urlencoded content-type the client likely used, but
+ # it at least matches the standard multiline registration format used by
+ # facilitator-reg-daemon.
+ try:
+ regs = list(fac.read_client_registrations(sys.stdin.read(), defhost=remote_addr[0]))
+ except ValueError:
+ exit_error(400)
+
+ for reg in regs:
+ # XXX need to link these registrations together, so that
+ # when one is answerered (or errors) the rest are invalidated.
+ if not fac.put_reg(FACILITATOR_ADDR, reg.addr, reg.transport):
+ exit_error(500)
+
+ print """\
+Status: 200\r
+\r"""
+
+if method == "HEAD":
+ do_head()
+elif method == "GET":
+ do_get()
+elif method == "POST":
+ do_post()
+else:
+ exit_error(405)
diff --git a/facilitator/init.d/facilitator-email-poller.in b/facilitator/init.d/facilitator-email-poller.in
new file mode 100755
index 0000000..02edd08
--- /dev/null
+++ b/facilitator/init.d/facilitator-email-poller.in
@@ -0,0 +1,131 @@
+#! /bin/sh
+### BEGIN INIT INFO
+# Provides: facilitator-email-poller
+# Required-Start: $remote_fs $syslog
+# Required-Stop: $remote_fs $syslog
+# Default-Start: 2 3 4 5
+# Default-Stop: 0 1 6
+# Short-Description: Flash proxy email rendezvous poller
+# Description: Debian init script for the flash proxy email rendezvous poller.
+### END INIT INFO
+#
+# Author: David Fifield <david at bamsoftware.com>
+#
+
+# Based on /etc/init.d/skeleton from Debian 6.
+
+PATH=/sbin:/usr/sbin:/bin:/usr/bin:/usr/local/bin
+DESC="Flash proxy email rendezvous poller"
+NAME=facilitator-email-poller
+
+prefix=@prefix@
+exec_prefix=@exec_prefix@
+PIDFILE=@localstatedir@/run/$NAME.pid
+LOGFILE=@localstatedir@/log/$NAME.log
+CONFDIR=@sysconfdir@/flashproxy
+PRIVDROP_USER=@fpfacilitatoruser@
+DAEMON=@bindir@/$NAME
+DAEMON_ARGS="--pass $CONFDIR/reg-email.pass --log $LOGFILE --pidfile $PIDFILE --privdrop-user $PRIVDROP_USER"
+DEFAULTSFILE=@sysconfdir@/default/$NAME
+
+# Exit if the package is not installed
+[ -x "$DAEMON" ] || exit 0
+
+# Read configuration variable file if it is present
+[ -r "$DEFAULTSFILE" ] && . "$DEFAULTSFILE"
+
+. /lib/init/vars.sh
+. /lib/lsb/init-functions
+
+[ "$UNSAFE_LOGGING" = "yes" ] && DAEMON_ARGS="$DAEMON_ARGS --unsafe-logging"
+
+#
+# Function that starts the daemon/service
+#
+do_start()
+{
+ # Return
+ # 0 if daemon has been started
+ # 1 if daemon was already running
+ # 2 if daemon could not be started
+ start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \
+ || return 1
+ start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON -- \
+ $DAEMON_ARGS \
+ || return 2
+}
+
+#
+# Function that stops the daemon/service
+#
+do_stop()
+{
+ # Return
+ # 0 if daemon has been stopped
+ # 1 if daemon was already stopped
+ # 2 if daemon could not be stopped
+ # other if a failure occurred
+ start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE
+ RETVAL="$?"
+ [ "$RETVAL" = 2 ] && return 2
+ # Wait for children to finish too if this is a daemon that forks
+ # and if the daemon is only ever run from this initscript.
+ # If the above conditions are not satisfied then add some other code
+ # that waits for the process to drop all resources that could be
+ # needed by services started subsequently. A last resort is to
+ # sleep for some time.
+ start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON
+ [ "$?" = 2 ] && return 2
+ rm -f $PIDFILE
+ return "$RETVAL"
+}
+
+case "$1" in
+ start)
+ if [ "$RUN_DAEMON" != "yes" ]; then
+ log_action_msg "Not starting $DESC (Disabled in $DEFAULTSFILE)."
+ exit 0
+ fi
+ [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME"
+ do_start
+ case "$?" in
+ 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
+ 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
+ esac
+ ;;
+ stop)
+ [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME"
+ do_stop
+ case "$?" in
+ 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
+ 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
+ esac
+ ;;
+ status)
+ status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $?
+ ;;
+ restart|force-reload)
+ log_daemon_msg "Restarting $DESC" "$NAME"
+ do_stop
+ case "$?" in
+ 0|1)
+ do_start
+ case "$?" in
+ 0) log_end_msg 0 ;;
+ 1) log_end_msg 1 ;; # Old process is still running
+ *) log_end_msg 1 ;; # Failed to start
+ esac
+ ;;
+ *)
+ # Failed to stop
+ log_end_msg 1
+ ;;
+ esac
+ ;;
+ *)
+ echo "Usage: $0 {start|stop|status|restart|force-reload}" >&2
+ exit 3
+ ;;
+esac
+
+:
diff --git a/facilitator/init.d/facilitator-reg-daemon.in b/facilitator/init.d/facilitator-reg-daemon.in
new file mode 100755
index 0000000..aa4afde
--- /dev/null
+++ b/facilitator/init.d/facilitator-reg-daemon.in
@@ -0,0 +1,132 @@
+#! /bin/sh
+### BEGIN INIT INFO
+# Provides: facilitator-reg-daemon
+# Required-Start: $remote_fs $syslog
+# Required-Stop: $remote_fs $syslog
+# Default-Start: 2 3 4 5
+# Default-Stop: 0 1 6
+# Short-Description: Flash proxy local registration daemon.
+# Description: Debian init script for the flash proxy local registration daemon.
+### END INIT INFO
+#
+# Author: David Fifield <david at bamsoftware.com>
+#
+
+# Based on /etc/init.d/skeleton from Debian 6.
+
+PATH=/sbin:/usr/sbin:/bin:/usr/bin:/usr/local/bin
+DESC="Flash proxy local registration daemon"
+NAME=facilitator-reg-daemon
+
+prefix=@prefix@
+exec_prefix=@exec_prefix@
+PIDFILE=@localstatedir@/run/$NAME.pid
+LOGFILE=@localstatedir@/log/$NAME.log
+CONFDIR=@sysconfdir@/flashproxy
+PRIVDROP_USER=@fpfacilitatoruser@
+DAEMON=@bindir@/$NAME
+DAEMON_ARGS="--key $CONFDIR/reg-daemon.key --log $LOGFILE --pidfile $PIDFILE --privdrop-user $PRIVDROP_USER"
+DEFAULTSFILE=@sysconfdir@/default/$NAME
+
+# Exit if the package is not installed
+[ -x "$DAEMON" ] || exit 0
+
+# Read configuration variable file if it is present
+[ -r "$DEFAULTSFILE" ] && . "$DEFAULTSFILE"
+
+[ "$UNSAFE_LOGGING" = "yes" ] && DAEMON_ARGS="$DAEMON_ARGS --unsafe-logging"
+[ -n "$PORT" ] && DAEMON_ARGS="$DAEMON_ARGS --port $PORT"
+
+. /lib/init/vars.sh
+. /lib/lsb/init-functions
+
+#
+# Function that starts the daemon/service
+#
+do_start()
+{
+ # Return
+ # 0 if daemon has been started
+ # 1 if daemon was already running
+ # 2 if daemon could not be started
+ start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \
+ || return 1
+ start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON -- \
+ $DAEMON_ARGS \
+ || return 2
+}
+
+#
+# Function that stops the daemon/service
+#
+do_stop()
+{
+ # Return
+ # 0 if daemon has been stopped
+ # 1 if daemon was already stopped
+ # 2 if daemon could not be stopped
+ # other if a failure occurred
+ start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE
+ RETVAL="$?"
+ [ "$RETVAL" = 2 ] && return 2
+ # Wait for children to finish too if this is a daemon that forks
+ # and if the daemon is only ever run from this initscript.
+ # If the above conditions are not satisfied then add some other code
+ # that waits for the process to drop all resources that could be
+ # needed by services started subsequently. A last resort is to
+ # sleep for some time.
+ start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON
+ [ "$?" = 2 ] && return 2
+ rm -f $PIDFILE
+ return "$RETVAL"
+}
+
+case "$1" in
+ start)
+ if [ "$RUN_DAEMON" != "yes" ]; then
+ log_action_msg "Not starting $DESC (Disabled in $DEFAULTSFILE)."
+ exit 0
+ fi
+ [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME"
+ do_start
+ case "$?" in
+ 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
+ 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
+ esac
+ ;;
+ stop)
+ [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME"
+ do_stop
+ case "$?" in
+ 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
+ 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
+ esac
+ ;;
+ status)
+ status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $?
+ ;;
+ restart|force-reload)
+ log_daemon_msg "Restarting $DESC" "$NAME"
+ do_stop
+ case "$?" in
+ 0|1)
+ do_start
+ case "$?" in
+ 0) log_end_msg 0 ;;
+ 1) log_end_msg 1 ;; # Old process is still running
+ *) log_end_msg 1 ;; # Failed to start
+ esac
+ ;;
+ *)
+ # Failed to stop
+ log_end_msg 1
+ ;;
+ esac
+ ;;
+ *)
+ echo "Usage: $0 {start|stop|status|restart|force-reload}" >&2
+ exit 3
+ ;;
+esac
+
+:
diff --git a/facilitator/init.d/facilitator.in b/facilitator/init.d/facilitator.in
new file mode 100755
index 0000000..8ba923f
--- /dev/null
+++ b/facilitator/init.d/facilitator.in
@@ -0,0 +1,133 @@
+#! /bin/sh
+### BEGIN INIT INFO
+# Provides: facilitator
+# Required-Start: $remote_fs $syslog
+# Required-Stop: $remote_fs $syslog
+# Default-Start: 2 3 4 5
+# Default-Stop: 0 1 6
+# Short-Description: Flash proxy facilitator
+# Description: Debian init script for the flash proxy facilitator.
+### END INIT INFO
+#
+# Author: David Fifield <david at bamsoftware.com>
+#
+
+# Based on /etc/init.d/skeleton from Debian 6.
+
+PATH=/sbin:/usr/sbin:/bin:/usr/bin:/usr/local/bin
+DESC="Flash proxy facilitator"
+NAME=facilitator
+
+prefix=@prefix@
+exec_prefix=@exec_prefix@
+PIDFILE=@localstatedir@/run/$NAME.pid
+LOGFILE=@localstatedir@/log/$NAME.log
+CONFDIR=@sysconfdir@/flashproxy
+RELAYFILE=$CONFDIR/facilitator-relays
+PRIVDROP_USER=@fpfacilitatoruser@
+DAEMON=@bindir@/$NAME
+DAEMON_ARGS="--relay-file $RELAYFILE --log $LOGFILE --pidfile $PIDFILE --privdrop-user $PRIVDROP_USER"
+DEFAULTSFILE=@sysconfdir@/default/$NAME
+
+# Exit if the package is not installed
+[ -x "$DAEMON" ] || exit 0
+
+# Read configuration variable file if it is present
+[ -r "$DEFAULTSFILE" ] && . "$DEFAULTSFILE"
+
+. /lib/init/vars.sh
+. /lib/lsb/init-functions
+
+[ "$UNSAFE_LOGGING" = "yes" ] && DAEMON_ARGS="$DAEMON_ARGS --unsafe-logging"
+[ -n "$PORT" ] && DAEMON_ARGS="$DAEMON_ARGS --port $PORT"
+
+#
+# Function that starts the daemon/service
+#
+do_start()
+{
+ # Return
+ # 0 if daemon has been started
+ # 1 if daemon was already running
+ # 2 if daemon could not be started
+ start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \
+ || return 1
+ start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON -- \
+ $DAEMON_ARGS \
+ || return 2
+}
+
+#
+# Function that stops the daemon/service
+#
+do_stop()
+{
+ # Return
+ # 0 if daemon has been stopped
+ # 1 if daemon was already stopped
+ # 2 if daemon could not be stopped
+ # other if a failure occurred
+ start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE
+ RETVAL="$?"
+ [ "$RETVAL" = 2 ] && return 2
+ # Wait for children to finish too if this is a daemon that forks
+ # and if the daemon is only ever run from this initscript.
+ # If the above conditions are not satisfied then add some other code
+ # that waits for the process to drop all resources that could be
+ # needed by services started subsequently. A last resort is to
+ # sleep for some time.
+ start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON
+ [ "$?" = 2 ] && return 2
+ rm -f $PIDFILE
+ return "$RETVAL"
+}
+
+case "$1" in
+ start)
+ if [ "$RUN_DAEMON" != "yes" ]; then
+ log_action_msg "Not starting $DESC (Disabled in $DEFAULTSFILE)."
+ exit 0
+ fi
+ [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME"
+ do_start
+ case "$?" in
+ 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
+ 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
+ esac
+ ;;
+ stop)
+ [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME"
+ do_stop
+ case "$?" in
+ 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
+ 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
+ esac
+ ;;
+ status)
+ status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $?
+ ;;
+ restart|force-reload)
+ log_daemon_msg "Restarting $DESC" "$NAME"
+ do_stop
+ case "$?" in
+ 0|1)
+ do_start
+ case "$?" in
+ 0) log_end_msg 0 ;;
+ 1) log_end_msg 1 ;; # Old process is still running
+ *) log_end_msg 1 ;; # Failed to start
+ esac
+ ;;
+ *)
+ # Failed to stop
+ log_end_msg 1
+ ;;
+ esac
+ ;;
+ *)
+ echo "Usage: $0 {start|stop|status|restart|force-reload}" >&2
+ exit 3
+ ;;
+esac
+
+:
diff --git a/facilitator/mkman.inc b/facilitator/mkman.inc
new file mode 120000
index 0000000..f10056c
--- /dev/null
+++ b/facilitator/mkman.inc
@@ -0,0 +1 @@
+../mkman.inc
\ No newline at end of file
diff --git a/facilitator/mkman.sh b/facilitator/mkman.sh
new file mode 120000
index 0000000..dcd1d5f
--- /dev/null
+++ b/facilitator/mkman.sh
@@ -0,0 +1 @@
+../mkman.sh
\ No newline at end of file
diff --git a/flashproxy-client b/flashproxy-client
new file mode 100755
index 0000000..ee0b068
--- /dev/null
+++ b/flashproxy-client
@@ -0,0 +1,1245 @@
+#!/usr/bin/env python
+"""
+The flashproxy client transport plugin.
+"""
+
+import BaseHTTPServer
+import array
+import base64
+import cStringIO
+import getopt
+import os
+import os.path
+import select
+import socket
+import struct
+import subprocess
+import sys
+import threading
+import time
+import traceback
+
+from flashproxy.util import parse_addr_spec, format_addr
+
+from hashlib import sha1
+
+try:
+ import numpy
+except ImportError:
+ numpy = None
+
+# Default local port in managed mode (choose one arbitrarily).
+DEFAULT_LOCAL_PORT_MANAGED = 0
+# Default local port in external mode.
+DEFAULT_LOCAL_PORT_EXTERNAL = 9001
+DEFAULT_REMOTE_PORT = 9000
+DEFAULT_REGISTER_METHODS = ["appspot", "email", "http"]
+DEFAULT_PORT_FORWARDING_HELPER = "tor-fw-helper"
+DEFAULT_TRANSPORT = "websocket"
+
+# We will re-register if we have fewer than this many waiting proxies. The
+# facilitator may choose to ignore our requests.
+DESIRED_NUMBER_OF_PROXIES = 3
+
+# We accept up to this many bytes from a socket not yet matched with a partner
+# before disconnecting it.
+UNCONNECTED_BUFFER_LIMIT = 10240
+
+LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
+
+class options(object):
+ local_addrs = []
+ remote_addrs = []
+ register_addr = None
+
+ managed = True
+
+ address_family = socket.AF_UNSPEC
+ daemonize = False
+ facilitator_url = None
+ facilitator_pubkey_filename = None
+ log_filename = None
+ log_file = sys.stdout
+ pid_filename = None
+ port_forwarding = False
+ port_forwarding_helper = DEFAULT_PORT_FORWARDING_HELPER
+ port_forwarding_external = None
+ register = False
+ register_commands = []
+ transport = DEFAULT_TRANSPORT
+ safe_logging = True
+
+def usage(f = sys.stdout):
+ print >> f, """\
+Usage: %(progname)s --register [LOCAL][:PORT] [REMOTE][:PORT]
+Wait for connections on a local and a remote port. When any pair of connections
+exists, data is ferried between them until one side is closed. By default
+LOCAL is localhost addresses on port %(local_port)d and REMOTE is all addresses
+on port %(remote_port)d.
+
+The local connection acts as a SOCKS4a proxy, but the host and port in the SOCKS
+request are ignored and the local connection is always linked to a remote
+connection.
+
+By default, runs as a managed proxy: informs a parent Tor process of support for
+the "flashproxy" or "websocket" pluggable transport. In managed mode,
+the LOCAL port is chosen arbitrarily instead of defaulting to
+%(local_port)d; however this can be overridden by including a LOCAL port
+in the command. This is the way the program should be invoked in a torrc
+ClientTransportPlugin "exec" line. Use the --external option to run as
+an external proxy that does not interact with Tor.
+
+If any of the --register, --register-addr, or --register-methods options are
+used, then your IP address will be sent to the facilitator so that proxies can
+connect to you. You need to register in some way in order to get any service.
+The --facilitator option allows controlling which facilitator is used; if
+omitted, it uses a public default.
+
+ -4 registration helpers use IPv4.
+ -6 registration helpers use IPv6.
+ --daemon daemonize (Unix only).
+ --external be an external proxy (don't interact with Tor using
+ environment variables and stdout).
+ -f, --facilitator=URL advertise willingness to receive connections to URL.
+ --facilitator-pubkey=FILENAME
+ encrypt registrations to the given PEM-formatted
+ public key (default built-in).
+ -h, --help show this help.
+ -l, --log FILENAME write log to FILENAME (default stdout).
+ --pidfile FILENAME write PID to FILENAME after daemonizing.
+ --port-forwarding attempt to forward REMOTE port.
+ --port-forwarding-helper=PROGRAM use the given PROGRAM to forward ports
+ (default "%(port_forwarding_helper)s"). Implies --port-forwarding.
+ --port-forwarding-external=PORT forward the external PORT to REMOTE on
+ the local host (default same as REMOTE). Implies
+ --port-forwarding.
+ -r, --register register with the facilitator.
+ --register-addr=ADDR register the given address (in case it differs from
+ REMOTE). Implies --register.
+ --register-methods=METHOD[,METHOD...]
+ register using the given comma-separated list of
+ methods. Implies --register. Possible methods are
+ appspot email http
+ Default is "%(reg_methods)s".
+ --transport=TRANSPORT register using the given transport
+ (default "%(transport)s").
+ --unsafe-logging don't scrub IP addresses from logs.\
+""" % {
+ "progname": sys.argv[0],
+ "local_port": DEFAULT_LOCAL_PORT_EXTERNAL,
+ "remote_port": DEFAULT_REMOTE_PORT,
+ "reg_methods": ",".join(DEFAULT_REGISTER_METHODS),
+ "port_forwarding_helper": DEFAULT_PORT_FORWARDING_HELPER,
+ "transport": DEFAULT_TRANSPORT,
+}
+
+def safe_str(s):
+ """Return "[scrubbed]" if options.safe_logging is true, and s otherwise."""
+ if options.safe_logging:
+ return "[scrubbed]"
+ else:
+ return s
+
+log_lock = threading.Lock()
+def log(msg):
+ with log_lock:
+ print >> options.log_file, (u"%s %s" % (time.strftime(LOG_DATE_FORMAT), msg)).encode("UTF-8")
+ options.log_file.flush()
+
+def safe_format_addr(addr):
+ return safe_str(format_addr(addr))
+
+def format_sockaddr(sockaddr):
+ host, port = socket.getnameinfo(sockaddr, socket.NI_NUMERICHOST | socket.NI_NUMERICSERV)
+ port = int(port)
+ return format_addr((host, port))
+
+def safe_format_sockaddr(sockaddr):
+ return safe_str(format_sockaddr(sockaddr))
+
+def safe_format_peername(s):
+ try:
+ return safe_format_sockaddr(s.getpeername())
+ except socket.error, e:
+ return "<unconnected>"
+
+
+def apply_mask_numpy(payload, mask_key):
+ if len(payload) == 0:
+ return ""
+ payload_a = numpy.frombuffer(payload, dtype="|u4", count=len(payload)//4)
+ m, = numpy.frombuffer(mask_key, dtype="|u4", count=1)
+ result = numpy.bitwise_xor(payload_a, m).tostring()
+ i = len(payload) // 4 * 4
+ if i < len(payload):
+ remainder = []
+ while i < len(payload):
+ remainder.append(chr(ord(payload[i]) ^ ord(mask_key[i % 4])))
+ i += 1
+ result = result + "".join(remainder)
+ return result
+
+def apply_mask_py(payload, mask_key):
+ result = array.array("B", payload)
+ m = array.array("B", mask_key)
+ i = 0
+ while i < len(result) - 7:
+ result[i] ^= m[0]
+ result[i+1] ^= m[1]
+ result[i+2] ^= m[2]
+ result[i+3] ^= m[3]
+ result[i+4] ^= m[0]
+ result[i+5] ^= m[1]
+ result[i+6] ^= m[2]
+ result[i+7] ^= m[3]
+ i += 8
+ while i < len(result):
+ result[i] ^= m[i%4]
+ i += 1
+ return result.tostring()
+
+if numpy is not None:
+ apply_mask = apply_mask_numpy
+else:
+ apply_mask = apply_mask_py
+
+class WebSocketFrame(object):
+ def __init__(self):
+ self.fin = False
+ self.opcode = None
+ self.payload = None
+
+ def is_control(self):
+ return (self.opcode & 0x08) != 0
+
+class WebSocketMessage(object):
+ def __init__(self):
+ self.opcode = None
+ self.payload = None
+
+ def is_control(self):
+ return (self.opcode & 0x08) != 0
+
+class WebSocketDecoder(object):
+ """RFC 6455 section 5 is about the WebSocket framing format."""
+ # Raise an exception rather than buffer anything larger than this.
+ MAX_MESSAGE_LENGTH = 1024 * 1024
+
+ class MaskingError(ValueError):
+ pass
+
+ def __init__(self, use_mask = False):
+ """use_mask should be True for server-to-client sockets, and False for
+ client-to-server sockets."""
+ self.use_mask = use_mask
+
+ # Per-frame state.
+ self.buf = ""
+
+ # Per-message state.
+ self.message_buf = ""
+ self.message_opcode = None
+
+ def feed(self, data):
+ self.buf += data
+
+ def read_frame(self):
+ """Read a frame from the internal buffer, if one is available. Returns a
+ WebSocketFrame object, or None if there are no complete frames to
+ read."""
+ # RFC 6255 section 5.2.
+ if len(self.buf) < 2:
+ return None
+ offset = 0
+ b0, b1 = struct.unpack_from(">BB", self.buf, offset)
+ offset += 2
+ fin = (b0 & 0x80) != 0
+ opcode = b0 & 0x0f
+ frame_masked = (b1 & 0x80) != 0
+ payload_len = b1 & 0x7f
+
+ if payload_len == 126:
+ if len(self.buf) < offset + 2:
+ return None
+ payload_len, = struct.unpack_from(">H", self.buf, offset)
+ offset += 2
+ elif payload_len == 127:
+ if len(self.buf) < offset + 8:
+ return None
+ payload_len, = struct.unpack_from(">Q", self.buf, offset)
+ offset += 8
+
+ if frame_masked:
+ if not self.use_mask:
+ # "A client MUST close a connection if it detects a masked
+ # frame."
+ raise self.MaskingError("Got masked payload from server")
+ if len(self.buf) < offset + 4:
+ return None
+ mask_key = self.buf[offset:offset+4]
+ offset += 4
+ else:
+ if self.use_mask:
+ # "The server MUST close the connection upon receiving a frame
+ # that is not masked."
+ raise self.MaskingError("Got unmasked payload from client")
+ mask_key = None
+
+ if payload_len > self.MAX_MESSAGE_LENGTH:
+ raise ValueError("Refusing to buffer payload of %d bytes" % payload_len)
+
+ if len(self.buf) < offset + payload_len:
+ return None
+ if mask_key:
+ payload = apply_mask(self.buf[offset:offset+payload_len], mask_key)
+ else:
+ payload = self.buf[offset:offset+payload_len]
+ self.buf = self.buf[offset+payload_len:]
+
+ frame = WebSocketFrame()
+ frame.fin = fin
+ frame.opcode = opcode
+ frame.payload = payload
+
+ return frame
+
+ def read_message(self):
+ """Read a complete message. If the opcode is 1, the payload is decoded
+ from a UTF-8 binary string to a unicode string. If a control frame is
+ read while another fragmented message is in progress, the control frame
+ is returned as a new message immediately. Returns None if there is no
+ complete frame to be read."""
+ # RFC 6455 section 5.4 is about fragmentation.
+ while True:
+ frame = self.read_frame()
+ if frame is None:
+ return None
+ # "Control frames (see Section 5.5) MAY be injected in the middle of
+ # a fragmented message. Control frames themselves MUST NOT be
+ # fragmented."
+ if frame.is_control():
+ if not frame.fin:
+ raise ValueError("Control frame (opcode %d) has FIN bit clear" % frame.opcode)
+ message = WebSocketMessage()
+ message.opcode = frame.opcode
+ message.payload = frame.payload
+ return message
+
+ if self.message_opcode is None:
+ if frame.opcode == 0:
+ raise ValueError("First frame has opcode 0")
+ self.message_opcode = frame.opcode
+ else:
+ if frame.opcode != 0:
+ raise ValueError("Non-first frame has nonzero opcode %d" % frame.opcode)
+ if len(self.message_buf) + len(frame.payload) > self.MAX_MESSAGE_LENGTH:
+ raise ValueError("Refusing to buffer payload of %d bytes" % (len(self.message_buf) + len(frame.payload)))
+ self.message_buf += frame.payload
+
+ if frame.fin:
+ break
+ message = WebSocketMessage()
+ message.opcode = self.message_opcode
+ message.payload = self.message_buf
+ self.postprocess_message(message)
+ self.message_opcode = None
+ self.message_buf = ""
+
+ return message
+
+ def postprocess_message(self, message):
+ if message.opcode == 1:
+ message.payload = message.payload.decode("utf-8")
+ return message
+
+class WebSocketEncoder(object):
+ def __init__(self, use_mask = False):
+ self.use_mask = use_mask
+
+ def encode_frame(self, opcode, payload):
+ if opcode >= 16:
+ raise ValueError("Opcode of %d is >= 16" % opcode)
+ length = len(payload)
+
+ if self.use_mask:
+ mask_key = os.urandom(4)
+ payload = apply_mask(payload, mask_key)
+ mask_bit = 0x80
+ else:
+ mask_key = ""
+ mask_bit = 0x00
+
+ if length < 126:
+ len_b, len_ext = length, ""
+ elif length < 0x10000:
+ len_b, len_ext = 126, struct.pack(">H", length)
+ elif length < 0x10000000000000000:
+ len_b, len_ext = 127, struct.pack(">Q", length)
+ else:
+ raise ValueError("payload length of %d is too long" % length)
+
+ return chr(0x80 | opcode) + chr(mask_bit | len_b) + len_ext + mask_key + payload
+
+ def encode_message(self, opcode, payload):
+ if opcode == 1:
+ payload = payload.encode("utf-8")
+ return self.encode_frame(opcode, payload)
+
+# WebSocket implementations generally support text (opcode 1) messages, which
+# are UTF-8-encoded text. Not all support binary (opcode 2) messages. During the
+# WebSocket handshake, we use the "base64" value of the Sec-WebSocket-Protocol
+# header field to indicate that text frames should encoded UTF-8-encoded
+# base64-encoded binary data. Binary messages are always interpreted verbatim,
+# but text messages are rejected if "base64" was not negotiated.
+#
+# The idea here is that browsers that know they don't support binary messages
+# can negotiate "base64" with both endpoints and still reliably transport binary
+# data. Those that know they can support binary messages can just use binary
+# messages in the straightforward way.
+
+class WebSocketBinaryDecoder(object):
+ def __init__(self, protocols, use_mask = False):
+ self.dec = WebSocketDecoder(use_mask)
+ self.base64 = "base64" in protocols
+
+ def feed(self, data):
+ self.dec.feed(data)
+
+ def read(self):
+ """Returns None when there are currently no data to be read. Returns ""
+ when a close message is received."""
+ while True:
+ message = self.dec.read_message()
+ if message is None:
+ return None
+ elif message.opcode == 1:
+ if not self.base64:
+ raise ValueError("Received text message on decoder incapable of base64")
+ payload = base64.b64decode(message.payload)
+ if payload:
+ return payload
+ elif message.opcode == 2:
+ if message.payload:
+ return message.payload
+ elif message.opcode == 8:
+ return ""
+ # Ignore all other opcodes.
+ return None
+
+class WebSocketBinaryEncoder(object):
+ def __init__(self, protocols, use_mask = False):
+ self.enc = WebSocketEncoder(use_mask)
+ self.base64 = "base64" in protocols
+
+ def encode(self, data):
+ if self.base64:
+ return self.enc.encode_message(1, base64.b64encode(data))
+ else:
+ return self.enc.encode_message(2, data)
+
+
+def listen_socket(addr):
+ """Return a socket listening on the given address."""
+ addrinfo = socket.getaddrinfo(addr[0], addr[1], 0, socket.SOCK_STREAM, socket.IPPROTO_TCP)[0]
+ s = socket.socket(addrinfo[0], addrinfo[1], addrinfo[2])
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ if addrinfo[0] == socket.AF_INET6 and socket.has_ipv6:
+ # Set the IPV6_V6ONLY socket option, otherwise some operating systems
+ # will listen on an IPv4 address as well as IPv6 by default. For
+ # example, "::" will listen on both "::" and "0.0.0.0", and "::1" will
+ # listen on both "::1" and "127.0.0.1". See
+ # https://trac.torproject.org/projects/tor/ticket/4760.
+ try:
+ s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
+ except AttributeError:
+ # Python 2.7.3 on Windows does not define IPPROTO_IPV6; see
+ # http://bugs.python.org/issue6926. IPV6_V6ONLY is the default
+ # behavior on Windows anyway, so we can skip the setsockopt.
+ pass
+ except socket.error:
+ # Seen on Windows XP:
+ # socket.error: [Errno 109] Protocol not available
+ pass
+ s.bind(addr)
+ s.listen(10)
+ return s
+
+# How long to wait for a WebSocket request on the remote socket. It is limited
+# to avoid Slowloris-like attacks.
+WEBSOCKET_REQUEST_TIMEOUT = 2.0
+
+# This subclass of BaseHTTPRequestHandler is essentially a means of parsing an
+# HTTP request.
+class WebSocketRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
+ def __init__(self, request_text, fd):
+ self.rfile = cStringIO.StringIO(request_text)
+ self.wfile = fd.makefile()
+ self.error = False
+ self.raw_requestline = self.rfile.readline()
+ self.parse_request()
+
+ def log_message(self, *args):
+ pass
+
+ def send_error(self, code, message = None):
+ BaseHTTPServer.BaseHTTPRequestHandler.send_error(self, code, message)
+ self.error = True
+
+MAGIC_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
+
+def handle_websocket_request(fd):
+ try:
+ request_text = fd.recv(10 * 1024)
+ except socket.error, e:
+ log(u"Socket error while receiving WebSocket request: %s" % repr(str(e)))
+ return None
+ handler = WebSocketRequestHandler(request_text, fd)
+ if handler.error or not hasattr(handler, "path"):
+ return None
+ method = handler.command
+ path = handler.path
+ headers = handler.headers
+
+ # See RFC 6455 section 4.2.1 for this sequence of checks.
+ #
+ # 1. An HTTP/1.1 or higher GET request, including a "Request-URI"...
+ if method != "GET":
+ handler.send_error(405)
+ return None
+ if path != "/":
+ handler.send_error(404)
+ return None
+
+ # 2. A |Host| header field containing the server's authority.
+ # We deliberately skip this test.
+
+ # 3. An |Upgrade| header field containing the value "websocket", treated as
+ # an ASCII case-insensitive value.
+ upgrade = headers.get("upgrade")
+ if upgrade is None:
+ handler.send_error(400)
+ return None
+ if "websocket" not in [x.strip().lower() for x in upgrade.split(",")]:
+ handler.send_error(400)
+ return None
+
+ # 4. A |Connection| header field that includes the token "Upgrade", treated
+ # as an ASCII case-insensitive value.
+ connection = headers.get("connection")
+ if connection is None:
+ handler.send_error(400)
+ return None
+ if "upgrade" not in [x.strip().lower() for x in connection.split(",")]:
+ handler.send_error(400)
+ return None
+
+ # 5. A |Sec-WebSocket-Key| header field with a base64-encoded value that,
+ # when decoded, is 16 bytes in length.
+ try:
+ key = headers.get("sec-websocket-key")
+ if len(base64.b64decode(key)) != 16:
+ raise TypeError("Sec-WebSocket-Key must be 16 bytes")
+ except TypeError:
+ handler.send_error(400)
+ return None
+
+ # 6. A |Sec-WebSocket-Version| header field, with a value of 13. We also
+ # allow 8 from draft-ietf-hybi-thewebsocketprotocol-10.
+ version = headers.get("sec-websocket-version")
+ KNOWN_VERSIONS = ["8", "13"]
+ if version not in KNOWN_VERSIONS:
+ # "If this version does not match a version understood by the server,
+ # the server MUST abort the WebSocket handshake described in this
+ # section and instead send an appropriate HTTP error code (such as 426
+ # Upgrade Required) and a |Sec-WebSocket-Version| header field
+ # indicating the version(s) the server is capable of understanding."
+ handler.send_response(426)
+ handler.send_header("Sec-WebSocket-Version", ", ".join(KNOWN_VERSIONS))
+ handler.end_headers()
+ return None
+
+ # 7. Optionally, an |Origin| header field.
+
+ # 8. Optionally, a |Sec-WebSocket-Protocol| header field, with a list of
+ # values indicating which protocols the client would like to speak, ordered
+ # by preference.
+ protocols_str = headers.get("sec-websocket-protocol")
+ if protocols_str is None:
+ protocols = []
+ else:
+ protocols = [x.strip().lower() for x in protocols_str.split(",")]
+
+ # 9. Optionally, a |Sec-WebSocket-Extensions| header field...
+
+ # 10. Optionally, other header fields...
+
+ # See RFC 6455 section 4.2.2, item 5 for these steps.
+
+ # 1. A Status-Line with a 101 response code as per RFC 2616.
+ handler.send_response(101)
+ # 2. An |Upgrade| header field with value "websocket" as per RFC 2616.
+ handler.send_header("Upgrade", "websocket")
+ # 3. A |Connection| header field with value "Upgrade".
+ handler.send_header("Connection", "Upgrade")
+ # 4. A |Sec-WebSocket-Accept| header field. The value of this header field
+ # is constructed by concatenating /key/, defined above in step 4 in Section
+ # 4.2.2, with the string "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", taking the
+ # SHA-1 hash of this concatenated value to obtain a 20-byte value and
+ # base64-encoding (see Section 4 of [RFC4648]) this 20-byte hash.
+ accept_key = base64.b64encode(sha1(key + MAGIC_GUID).digest())
+ handler.send_header("Sec-WebSocket-Accept", accept_key)
+ # 5. Optionally, a |Sec-WebSocket-Protocol| header field, with a value
+ # /subprotocol/ as defined in step 4 in Section 4.2.2.
+ if "base64" in protocols:
+ handler.send_header("Sec-WebSocket-Protocol", "base64")
+ # 6. Optionally, a |Sec-WebSocket-Extensions| header field...
+
+ handler.end_headers()
+
+ return protocols
+
+def grab_string(s, pos):
+ """Grab a NUL-terminated string from the given string, starting at the given
+ offset. Return (pos, str) tuple, or (pos, None) on error."""
+ i = pos
+ while i < len(s):
+ if s[i] == '\0':
+ return (i + 1, s[pos:i])
+ i += 1
+ return pos, None
+
+# http://ftp.icm.edu.pl/packages/socks/socks4/SOCKS4.protocol
+# https://en.wikipedia.org/wiki/SOCKS#SOCKS4a
+def parse_socks_request(data):
+ """Parse the 8-byte SOCKS header at the beginning of data. Returns a
+ (dest, port) tuple. Raises ValueError on error."""
+ try:
+ ver, cmd, dport, o1, o2, o3, o4 = struct.unpack(">BBHBBBB", data[:8])
+ except struct.error:
+ raise ValueError("Couldn't unpack SOCKS4 header")
+ if ver != 4:
+ raise ValueError("Wrong SOCKS version (%d)" % ver)
+ if cmd != 1:
+ raise ValueError("Wrong SOCKS command (%d)" % cmd)
+ pos, userid = grab_string(data, 8)
+ if userid is None:
+ raise ValueError("Couldn't read userid")
+ if o1 == 0 and o2 == 0 and o3 == 0 and o4 != 0:
+ pos, dest = grab_string(data, pos)
+ if dest is None:
+ raise ValueError("Couldn't read destination")
+ else:
+ dest = "%d.%d.%d.%d" % (o1, o2, o3, o4)
+ return dest, dport
+
+def handle_socks_request(fd):
+ try:
+ addr = fd.getpeername()
+ data = fd.recv(100)
+ except socket.error, e:
+ log(u"Socket error from SOCKS-pending: %s" % repr(str(e)))
+ return False
+ try:
+ dest_addr = parse_socks_request(data)
+ except ValueError, e:
+ log(u"Error parsing SOCKS request: %s." % str(e))
+ # Error reply.
+ fd.sendall(struct.pack(">BBHBBBB", 0, 91, 0, 0, 0, 0, 0))
+ return False
+ log(u"Got SOCKS request for %s." % safe_format_addr(dest_addr))
+ fd.sendall(struct.pack(">BBHBBBB", 0, 90, dest_addr[1], 127, 0, 0, 1))
+ # Note we throw away the requested address and port.
+ return True
+
+def report_pending():
+ log(u"locals (%d): %s" % (len(locals), [safe_format_peername(x) for x in locals]))
+ log(u"remotes (%d): %s" % (len(remotes), [safe_format_peername(x) for x in remotes]))
+
+def forward_ports(pairs):
+ """Attempt to forward all given pairs (external, internal) pairs of ports
+ using port_forwarding_helper."""
+ command = [options.port_forwarding_helper]
+ basename = os.path.basename(command[0])
+ for external, internal in pairs:
+ command += ["-p", "%d:%d" % (external, internal)]
+ try:
+ log(u"Running port forwarding command: %s" % " ".join(command))
+ p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ stdout, stderr = p.communicate()
+ except OSError, e:
+ log(u"Error running %s: %s" % (basename, str(e)))
+ return False
+ for line in stdout.splitlines():
+ log(u"%s: %s" % (basename, line))
+ for line in stderr.splitlines():
+ log(u"%s: %s" % (basename, line))
+ if p.returncode != 0:
+ log("%s exited with status %d." % (basename, p.returncode))
+ return False
+ return True
+
+register_condvar = threading.Condition()
+# register_flag true means registration_thread_func should register at its next
+# opportunity.
+register_flag = False
+def register():
+ global register_flag
+ if not options.register:
+ return
+ with register_condvar:
+ register_flag = True
+ register_condvar.notify()
+
+def register_using_command(command):
+ basename = os.path.basename(command[0])
+ try:
+ log(u"Running command: %s" % " ".join(command))
+ p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ stdout, stderr = p.communicate()
+ except OSError, e:
+ log(u"Error running %s: %s" % (basename, str(e)))
+ return False
+ for line in stdout.splitlines():
+ log(u"%s: %s" % (basename, line))
+ for line in stderr.splitlines():
+ log(u"%s: %s" % (basename, line))
+ if p.returncode != 0:
+ log("%s exited with status %d." % (basename, p.returncode))
+ return False
+ return True
+
+def register_one():
+ spec = format_addr(options.register_addr)
+ log(u"Trying to register \"%s\"." % spec)
+ for command in options.register_commands:
+ if register_using_command(command + [spec]):
+ break
+ else:
+ log(u"All registration commands failed.")
+
+def registration_thread_func():
+ global register_flag
+ while True:
+ with register_condvar:
+ while not register_flag:
+ register_condvar.wait()
+ register_flag = False
+ if len(unlinked_remotes) < DESIRED_NUMBER_OF_PROXIES:
+ register_one()
+
+def proxy_chunk_local_to_remote(local, remote, data = None):
+ if data is None:
+ try:
+ data = local.recv(65536)
+ except socket.error, e: # Can be "Connection reset by peer".
+ log(u"Socket error from local: %s" % repr(str(e)))
+ remote.close()
+ return False
+ if not data:
+ log(u"EOF from local %s." % safe_format_peername(local))
+ local.close()
+ remote.close()
+ return False
+ else:
+ try:
+ remote.send_chunk(data)
+ except socket.error, e:
+ log(u"Socket error writing to remote: %s" % repr(str(e)))
+ local.close()
+ return False
+ return True
+
+def proxy_chunk_remote_to_local(remote, local, data = None):
+ if data is None:
+ try:
+ data = remote.recv(65536)
+ except socket.error, e: # Can be "Connection reset by peer".
+ log(u"Socket error from remote: %s" % repr(str(e)))
+ local.close()
+ return False
+ if not data:
+ log(u"EOF from remote %s." % safe_format_peername(remote))
+ remote.close()
+ local.close()
+ return False
+ else:
+ remote.dec.feed(data)
+ while True:
+ try:
+ data = remote.dec.read()
+ except (WebSocketDecoder.MaskingError, ValueError), e:
+ log(u"WebSocket decode error from remote: %s" % repr(str(e)))
+ remote.close()
+ local.close()
+ return False
+ if data is None:
+ break
+ elif not data:
+ log(u"WebSocket close from remote %s." % safe_format_peername(remote))
+ remote.close()
+ local.close()
+ return False
+ try:
+ local.send_chunk(data)
+ except socket.error, e:
+ log(u"Socket error writing to local: %s" % repr(str(e)))
+ remote.close()
+ return False
+ return True
+
+def receive_unlinked(fd, label):
+ """Receive and buffer data on a socket that has not been linked yet. Returns
+ True iff there was no error and the socket may still be used; otherwise, the
+ socket will be closed before returning."""
+
+ try:
+ data = fd.recv(1024)
+ except socket.error, e:
+ log(u"Socket error from %s: %s" % (label, repr(str(e))))
+ fd.close()
+ return False
+ if not data:
+ log(u"EOF from unlinked %s %s with %d bytes buffered." % (label, safe_format_peername(fd), len(fd.buf)))
+ fd.close()
+ return False
+ else:
+ log(u"Data from unlinked %s %s (%d bytes)." % (label, safe_format_peername(fd), len(data)))
+ fd.buf += data
+ if len(fd.buf) >= UNCONNECTED_BUFFER_LIMIT:
+ log(u"Refusing to buffer more than %d bytes from %s %s." % (UNCONNECTED_BUFFER_LIMIT, label, safe_format_peername(fd)))
+ fd.close()
+ return False
+ return True
+
+def match_proxies():
+ while unlinked_remotes and unlinked_locals:
+ remote = unlinked_remotes.pop(0)
+ local = unlinked_locals.pop(0)
+ log(u"Linking %s and %s." % (safe_format_peername(local), safe_format_peername(remote)))
+ remote.partner = local
+ local.partner = remote
+ if remote.buf:
+ if not proxy_chunk_remote_to_local(remote, local, remote.buf):
+ remotes.remove(remote)
+ locals.remove(local)
+ register()
+ return
+ if local.buf:
+ if not proxy_chunk_local_to_remote(local, remote, local.buf):
+ remotes.remove(remote)
+ locals.remove(local)
+ return
+
+class TimeoutSocket(object):
+ def __init__(self, fd):
+ self.fd = fd
+ self.birthday = time.time()
+
+ def age(self):
+ return time.time() - self.birthday
+
+ def __getattr__(self, name):
+ return getattr(self.fd, name)
+
+class RemoteSocket(object):
+ def __init__(self, fd, protocols):
+ self.fd = fd
+ self.buf = ""
+ self.partner = None
+ self.dec = WebSocketBinaryDecoder(protocols, use_mask = True)
+ self.enc = WebSocketBinaryEncoder(protocols, use_mask = False)
+
+ def send_chunk(self, data):
+ self.sendall(self.enc.encode(data))
+
+ def __getattr__(self, name):
+ return getattr(self.fd, name)
+
+class LocalSocket(object):
+ def __init__(self, fd):
+ self.fd = fd
+ self.buf = ""
+ self.partner = None
+
+ def send_chunk(self, data):
+ self.sendall(data)
+
+ def __getattr__(self, name):
+ return getattr(self.fd, name)
+
+def proxy_loop():
+ while True:
+ rset = remote_listen + local_listen + websocket_pending + socks_pending + locals + remotes
+ rset, _, _ = select.select(rset, [], [], WEBSOCKET_REQUEST_TIMEOUT)
+ for fd in rset:
+ if fd in remote_listen:
+ remote_c, addr = fd.accept()
+ log(u"Remote connection from %s." % safe_format_sockaddr(addr))
+ websocket_pending.append(TimeoutSocket(remote_c))
+ elif fd in local_listen:
+ local_c, addr = fd.accept()
+ log(u"Local connection from %s." % safe_format_sockaddr(addr))
+ socks_pending.append(local_c)
+ register()
+ elif fd in websocket_pending:
+ log(u"Data from WebSocket-pending %s." % safe_format_peername(fd))
+ protocols = handle_websocket_request(fd)
+ if protocols is not None:
+ wrapped = RemoteSocket(fd, protocols)
+ remotes.append(wrapped)
+ unlinked_remotes.append(wrapped)
+ else:
+ fd.close()
+ register()
+ websocket_pending.remove(fd)
+ report_pending()
+ elif fd in socks_pending:
+ log(u"SOCKS request from %s." % safe_format_peername(fd))
+ if handle_socks_request(fd):
+ wrapped = LocalSocket(fd)
+ locals.append(wrapped)
+ unlinked_locals.append(wrapped)
+ else:
+ fd.close()
+ socks_pending.remove(fd)
+ report_pending()
+ elif fd in remotes:
+ local = fd.partner
+ if local:
+ if not proxy_chunk_remote_to_local(fd, local):
+ remotes.remove(fd)
+ locals.remove(local)
+ register()
+ else:
+ if not receive_unlinked(fd, "remote"):
+ remotes.remove(fd)
+ unlinked_remotes.remove(fd)
+ register()
+ report_pending()
+ elif fd in locals:
+ remote = fd.partner
+ if remote:
+ if not proxy_chunk_local_to_remote(fd, remote):
+ remotes.remove(remote)
+ locals.remove(fd)
+ else:
+ if not receive_unlinked(fd, "local"):
+ locals.remove(fd)
+ unlinked_locals.remove(fd)
+ report_pending()
+ match_proxies()
+ while websocket_pending:
+ pending = websocket_pending[0]
+ if pending.age() < WEBSOCKET_REQUEST_TIMEOUT:
+ break
+ log(u"Expired remote connection from %s." % safe_format_peername(pending))
+ pending.close()
+ websocket_pending.pop(0)
+ report_pending()
+
+def build_register_command(method):
+ # sys.path[0] usually contains the directory the script is located in.
+ # py2exe overwrites this for bundled executables.
+ if getattr(sys, "frozen", False):
+ script_dir = os.path.dirname(sys.executable)
+ else:
+ script_dir = sys.path[0]
+ if not script_dir:
+ # Maybe the script was read from stdin; in any case don't guess at the directory.
+ raise ValueError("Can't find executable directory for registration helpers")
+
+ # "common" is options shared by every registration helper.
+ common = []
+ if options.address_family == socket.AF_INET:
+ common += ["-4"]
+ elif options.address_family == socket.AF_INET6:
+ common += ["-6"]
+ if options.transport is not None:
+ common += ["--transport", options.transport]
+ if not options.safe_logging:
+ common += ["--unsafe-logging"]
+
+ if method == "appspot":
+ command = [os.path.join(script_dir, "flashproxy-reg-appspot")] + common
+ if options.facilitator_pubkey_filename is not None:
+ command += ["--facilitator-pubkey", options.facilitator_pubkey_filename]
+ return command
+ elif method == "email":
+ command = [os.path.join(script_dir, "flashproxy-reg-email")] + common
+ if options.facilitator_pubkey_filename is not None:
+ command += ["--facilitator-pubkey", options.facilitator_pubkey_filename]
+ return command
+ elif method == "http":
+ command = [os.path.join(script_dir, "flashproxy-reg-http")] + common
+ if options.facilitator_url is not None:
+ command += ["-f", options.facilitator_url]
+ return command
+ else:
+ raise ValueError("Unknown registration method \"%s\"" % method)
+
+def pt_escape(s):
+ result = []
+ for c in s:
+ if c == "\n":
+ result.append("\\n")
+ elif c == "\\":
+ result.append("\\\\")
+ elif 0 < ord(c) < 128:
+ result.append(c)
+ else:
+ result.append("\\x%02x" % ord(c))
+ return "".join(result)
+
+def pt_line(keyword, *args):
+ log(keyword + " " + " ".join(pt_escape(x) for x in args))
+ print keyword, " ".join(pt_escape(x) for x in args)
+ sys.stdout.flush()
+
+def pt_cmethoderror(msg):
+ pt_line("CMETHOD-ERROR", msg)
+ sys.exit(1)
+
+def pt_get_client_transports(known, wildcard = None):
+ result = []
+ if os.environ.get("TOR_PT_CLIENT_TRANSPORTS") == "*":
+ if wildcard is None:
+ wildcard = known
+ return wildcard
+ for method in os.environ.get("TOR_PT_CLIENT_TRANSPORTS", "").split(","):
+ if method in known:
+ result.append(method)
+ return result
+
+def pt_setup_managed():
+ TOR_PT_MANAGED_TRANSPORT_VER = os.environ.get("TOR_PT_MANAGED_TRANSPORT_VER")
+ if TOR_PT_MANAGED_TRANSPORT_VER is None:
+ pt_line("VERSION-ERROR", "no-version")
+ print >> sys.stderr, """\
+
+No TOR_PT_MANAGED_TRANSPORT_VER found in environment.
+If you are running flashproxy-client from the command line and not from
+a ClientTransportPlugin configuration line, you must use the --external
+option.\
+"""
+ sys.exit(1)
+
+ for ver in TOR_PT_MANAGED_TRANSPORT_VER.split(","):
+ if ver == "1":
+ pt_line("VERSION", ver)
+ break
+ else:
+ pt_line("VERSION-ERROR", "no-version")
+ sys.exit(1)
+
+ client_transports = pt_get_client_transports(["flashproxy", "websocket"], ["flashproxy"])
+ if not client_transports:
+ pt_cmethods_done()
+ sys.exit(1)
+ return client_transports
+
+def pt_cmethod(method_name, addr):
+ pt_line("CMETHOD", method_name, "socks4", format_sockaddr(addr))
+
+def pt_cmethods_done():
+ pt_line("CMETHODS", "DONE")
+
+def main():
+ global remote_listen, local_listen
+ global locals, remotes
+ global websocket_pending, socks_pending
+ global unlinked_locals, unlinked_remotes
+
+ register_addr_spec = None
+ register_methods = []
+
+ opts, args = getopt.gnu_getopt(sys.argv[1:], "46f:hl:r", [
+ "daemon",
+ "external",
+ "facilitator=",
+ "facilitator-pubkey=",
+ "help",
+ "log=",
+ "pidfile=",
+ "register",
+ "register-addr=",
+ "register-methods=",
+ "port-forwarding",
+ "port-forwarding-helper=",
+ "port-forwarding-external=",
+ "transport=",
+ "unsafe-logging",
+ ])
+ for o, a in opts:
+ if o == "-4":
+ options.address_family = socket.AF_INET
+ elif o == "-6":
+ options.address_family = socket.AF_INET6
+ elif o == "--daemon":
+ options.daemonize = True
+ elif o == "--external":
+ options.managed = False
+ elif o == "-f" or o == "--facilitator":
+ options.facilitator_url = a
+ elif o == "--facilitator-pubkey":
+ options.facilitator_pubkey_filename = a
+ elif o == "-h" or o == "--help":
+ usage()
+ sys.exit()
+ elif o == "-l" or o == "--log":
+ options.log_filename = a
+ elif o == "--pidfile":
+ options.pid_filename = a
+ elif o == "-r" or o == "--register":
+ options.register = True
+ elif o == "--register-addr":
+ if options.register_addr is not None:
+ print >> sys.stderr, "%s: only one --register-addr is allowed." % sys.argv[0]
+ sys.exit(1)
+ options.register = True
+ register_addr_spec = a
+ elif o == "--register-methods":
+ options.register = True
+ register_methods.extend(a.split(","))
+ elif o == "--port-forwarding":
+ options.port_forwarding = True
+ elif o == "--port-forwarding-helper":
+ options.port_forwarding = True
+ options.port_forwarding_helper = a
+ elif o == "--port-forwarding-external":
+ options.port_forwarding = True
+ options.port_forwarding_external = int(a)
+ elif o == "--transport":
+ options.transport = a
+ elif o == "--unsafe-logging":
+ options.safe_logging = False
+
+ if options.log_filename:
+ options.log_file = open(options.log_filename, "a")
+ # Send error tracebacks to the log.
+ sys.stderr = options.log_file
+ else:
+ options.log_file = sys.stderr
+
+ if options.managed:
+ method_names = pt_setup_managed()
+ else:
+ method_names = ["flashproxy"]
+
+ if options.managed:
+ default_local_port = DEFAULT_LOCAL_PORT_MANAGED
+ else:
+ default_local_port = DEFAULT_LOCAL_PORT_EXTERNAL
+ default_remote_port = DEFAULT_REMOTE_PORT
+
+ if len(args) == 0:
+ local_addr = ("", default_local_port)
+ remote_addr = ("", default_remote_port)
+ elif len(args) == 1:
+ local_addr = parse_addr_spec(args[0], defhost="", defport=default_local_port)
+ remote_addr = ("", default_remote_port)
+ elif len(args) == 2:
+ local_addr = parse_addr_spec(args[0], defhost="", defport=default_local_port)
+ remote_addr = parse_addr_spec(args[1], defhost="", defport=default_remote_port)
+ else:
+ usage(sys.stderr)
+ sys.exit(1)
+
+ if local_addr[0]:
+ options.local_addrs.append(local_addr)
+ else:
+ options.local_addrs.append(("127.0.0.1", local_addr[1]))
+ # Listen on both IPv4 and IPv6 if no host is given, unless we are in
+ # managed mode.
+ if not options.managed and socket.has_ipv6:
+ options.local_addrs.append(("::1", local_addr[1]))
+ if remote_addr[0]:
+ options.remote_addrs.append(remote_addr)
+ else:
+ options.remote_addrs.append(("0.0.0.0", remote_addr[1]))
+ if socket.has_ipv6:
+ options.remote_addrs.append(("::", remote_addr[1]))
+ options.register_addr = parse_addr_spec(register_addr_spec or ":", *remote_addr)
+
+ if not register_methods:
+ register_methods = DEFAULT_REGISTER_METHODS
+ for method in register_methods:
+ options.register_commands.append(build_register_command(method))
+
+ # Attempt to forward ports if requested.
+ if options.port_forwarding:
+ internal = remote_addr[1]
+ if options.port_forwarding_external is not None:
+ external = options.port_forwarding_external
+ else:
+ external = internal
+ forward_ports(((external, internal),))
+
+ # Remote sockets, accepting remote WebSocket connections from proxies.
+ remote_listen = []
+ for addr in options.remote_addrs:
+ try:
+ listen = listen_socket(addr)
+ except socket.error, e:
+ log(u"Failed to listen remote on %s: %s." % (addr, str(e)))
+ continue
+ remote_listen.append(listen)
+ log(u"Listening remote on %s." % format_sockaddr(listen.getsockname()))
+ if not remote_listen:
+ log(u"Failed to open any remote listeners, quitting.")
+ pt_cmethoderror("Failed to open any remote listeners.")
+ # Local sockets, accepting SOCKS requests from localhost
+ local_listen = []
+ for addr in options.local_addrs:
+ for method_name in method_names:
+ try:
+ listen = listen_socket(addr)
+ except socket.error, e:
+ log(u"Failed to listen local on %s: %s." % (addr, str(e)))
+ continue
+ local_listen.append(listen)
+ log(u"Listening local on %s." % format_sockaddr(listen.getsockname()))
+ if options.managed:
+ pt_cmethod(method_name, listen.getsockname())
+ if not local_listen:
+ log(u"Failed to open any local listeners, quitting.")
+ pt_cmethoderror("Failed to open any local listeners.")
+ if options.managed:
+ pt_cmethods_done()
+
+ # New remote sockets waiting to finish their WebSocket negotiation.
+ websocket_pending = []
+ # Remote connection sockets.
+ remotes = []
+ # Remotes not yet linked with a local. This is a subset of remotes.
+ unlinked_remotes = []
+ # New local sockets waiting to finish their SOCKS negotiation.
+ socks_pending = []
+ # Local Tor sockets, after SOCKS negotiation.
+ locals = []
+ # Locals not yet linked with a remote. This is a subset of remotes.
+ unlinked_locals = []
+
+ if options.daemonize:
+ log(u"Daemonizing.")
+ pid = os.fork()
+ if pid != 0:
+ if options.pid_filename:
+ f = open(options.pid_filename, "w")
+ print >> f, pid
+ f.close()
+ sys.exit(0)
+
+ if options.register:
+ registration_thread = threading.Thread(target=registration_thread_func, name="register")
+ registration_thread.daemon = True
+ registration_thread.start()
+ register()
+
+ try:
+ proxy_loop()
+ except Exception:
+ exc = traceback.format_exc()
+ log("".join(exc))
+
+if __name__ == "__main__":
+ main()
diff --git a/flashproxy-client-test.py b/flashproxy-client-test.py
new file mode 100755
index 0000000..0281f42
--- /dev/null
+++ b/flashproxy-client-test.py
@@ -0,0 +1,401 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import base64
+import cStringIO
+import httplib
+import socket
+import subprocess
+import sys
+import unittest
+try:
+ from hashlib import sha1
+except ImportError:
+ # Python 2.4 uses this name.
+ from sha import sha as sha1
+
+# Special tricks to load a module whose filename contains a dash and doesn't end
+# in ".py".
+import imp
+dont_write_bytecode = sys.dont_write_bytecode
+sys.dont_write_bytecode = True
+fp_client = imp.load_source("fp_client", "flashproxy-client")
+parse_socks_request = fp_client.parse_socks_request
+handle_websocket_request = fp_client.handle_websocket_request
+WebSocketDecoder = fp_client.WebSocketDecoder
+WebSocketEncoder = fp_client.WebSocketEncoder
+sys.dont_write_bytecode = dont_write_bytecode
+del dont_write_bytecode
+del fp_client
+
+LOCAL_ADDRESS = ("127.0.0.1", 40000)
+REMOTE_ADDRESS = ("127.0.0.1", 40001)
+
+class TestSocks(unittest.TestCase):
+ def test_parse_socks_request_empty(self):
+ self.assertRaises(ValueError, parse_socks_request, "")
+ def test_parse_socks_request_short(self):
+ self.assertRaises(ValueError, parse_socks_request, "\x04\x01\x99\x99\x01\x02\x03\x04")
+ def test_parse_socks_request_ip_userid_missing(self):
+ dest, port = parse_socks_request("\x04\x01\x99\x99\x01\x02\x03\x04\x00")
+ dest, port = parse_socks_request("\x04\x01\x99\x99\x01\x02\x03\x04\x00userid")
+ self.assertEqual((dest, port), ("1.2.3.4", 0x9999))
+ def test_parse_socks_request_ip(self):
+ dest, port = parse_socks_request("\x04\x01\x99\x99\x01\x02\x03\x04userid\x00")
+ self.assertEqual((dest, port), ("1.2.3.4", 0x9999))
+ def test_parse_socks_request_hostname_missing(self):
+ self.assertRaises(ValueError, parse_socks_request, "\x04\x01\x99\x99\x00\x00\x00\x01userid\x00")
+ self.assertRaises(ValueError, parse_socks_request, "\x04\x01\x99\x99\x00\x00\x00\x01userid\x00abc")
+ def test_parse_socks_request_hostname(self):
+ dest, port = parse_socks_request("\x04\x01\x99\x99\x00\x00\x00\x01userid\x00abc\x00")
+
+class DummySocket(object):
+ def __init__(self, read_fd, write_fd):
+ self.read_fd = read_fd
+ self.write_fd = write_fd
+ self.readp = 0
+
+ def read(self, *args, **kwargs):
+ self.read_fd.seek(self.readp, 0)
+ data = self.read_fd.read(*args, **kwargs)
+ self.readp = self.read_fd.tell()
+ return data
+
+ def readline(self, *args, **kwargs):
+ self.read_fd.seek(self.readp, 0)
+ data = self.read_fd.readline(*args, **kwargs)
+ self.readp = self.read_fd.tell()
+ return data
+
+ def recv(self, size, *args, **kwargs):
+ return self.read(size)
+
+ def write(self, data):
+ self.write_fd.seek(0, 2)
+ self.write_fd.write(data)
+
+ def send(self, data, *args, **kwargs):
+ return self.write(data)
+
+ def sendall(self, data, *args, **kwargs):
+ return self.write(data)
+
+ def makefile(self, *args, **kwargs):
+ return self
+
+def dummy_socketpair():
+ f1 = cStringIO.StringIO()
+ f2 = cStringIO.StringIO()
+ return (DummySocket(f1, f2), DummySocket(f2, f1))
+
+class HTTPRequest(object):
+ def __init__(self):
+ self.method = "GET"
+ self.path = "/"
+ self.headers = {}
+
+def transact_http(req):
+ l, r = dummy_socketpair()
+ r.send("%s %s HTTP/1.0\r\n" % (req.method, req.path))
+ for k, v in req.headers.items():
+ r.send("%s: %s\r\n" % (k, v))
+ r.send("\r\n")
+ protocols = handle_websocket_request(l)
+
+ resp = httplib.HTTPResponse(r)
+ resp.begin()
+ return resp, protocols
+
+class TestHandleWebSocketRequest(unittest.TestCase):
+ DEFAULT_KEY = "0123456789ABCDEF"
+ DEFAULT_KEY_BASE64 = base64.b64encode(DEFAULT_KEY)
+ MAGIC_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
+
+ @staticmethod
+ def default_req():
+ req = HTTPRequest()
+ req.method = "GET"
+ req.path = "/"
+ req.headers["Upgrade"] = "websocket"
+ req.headers["Connection"] = "Upgrade"
+ req.headers["Sec-WebSocket-Key"] = TestHandleWebSocketRequest.DEFAULT_KEY_BASE64
+ req.headers["Sec-WebSocket-Version"] = "13"
+
+ return req
+
+ def assert_ok(self, req):
+ resp, protocols = transact_http(req)
+ self.assertEqual(resp.status, 101)
+ self.assertEqual(resp.getheader("Upgrade").lower(), "websocket")
+ self.assertEqual(resp.getheader("Connection").lower(), "upgrade")
+ self.assertEqual(resp.getheader("Sec-WebSocket-Accept"), base64.b64encode(sha1(self.DEFAULT_KEY_BASE64 + self.MAGIC_GUID).digest()))
+ self.assertEqual(protocols, [])
+
+ def assert_not_ok(self, req):
+ resp, protocols = transact_http(req)
+ self.assertEqual(resp.status // 100, 4)
+ self.assertEqual(protocols, None)
+
+ def test_default(self):
+ req = self.default_req()
+ self.assert_ok(req)
+
+ def test_missing_upgrade(self):
+ req = self.default_req()
+ del req.headers["Upgrade"]
+ self.assert_not_ok(req)
+
+ def test_missing_connection(self):
+ req = self.default_req()
+ del req.headers["Connection"]
+ self.assert_not_ok(req)
+
+ def test_case_insensitivity(self):
+ """Test that the values of the Upgrade and Connection headers are
+ case-insensitive."""
+ req = self.default_req()
+ req.headers["Upgrade"] = req.headers["Upgrade"].lower()
+ self.assert_ok(req)
+ req.headers["Upgrade"] = req.headers["Upgrade"].upper()
+ self.assert_ok(req)
+ req.headers["Connection"] = req.headers["Connection"].lower()
+ self.assert_ok(req)
+ req.headers["Connection"] = req.headers["Connection"].upper()
+ self.assert_ok(req)
+
+ def test_bogus_key(self):
+ req = self.default_req()
+ req.headers["Sec-WebSocket-Key"] = base64.b64encode(self.DEFAULT_KEY[:-1])
+ self.assert_not_ok(req)
+
+ req.headers["Sec-WebSocket-Key"] = "///"
+ self.assert_not_ok(req)
+
+ def test_versions(self):
+ req = self.default_req()
+ req.headers["Sec-WebSocket-Version"] = "13"
+ self.assert_ok(req)
+ req.headers["Sec-WebSocket-Version"] = "8"
+ self.assert_ok(req)
+
+ req.headers["Sec-WebSocket-Version"] = "7"
+ self.assert_not_ok(req)
+ req.headers["Sec-WebSocket-Version"] = "9"
+ self.assert_not_ok(req)
+
+ del req.headers["Sec-WebSocket-Version"]
+ self.assert_not_ok(req)
+
+ def test_protocols(self):
+ req = self.default_req()
+ req.headers["Sec-WebSocket-Protocol"] = "base64"
+ resp, protocols = transact_http(req)
+ self.assertEqual(resp.status, 101)
+ self.assertEqual(protocols, ["base64"])
+ self.assertEqual(resp.getheader("Sec-WebSocket-Protocol"), "base64")
+
+ req = self.default_req()
+ req.headers["Sec-WebSocket-Protocol"] = "cat"
+ resp, protocols = transact_http(req)
+ self.assertEqual(resp.status, 101)
+ self.assertEqual(protocols, ["cat"])
+ self.assertEqual(resp.getheader("Sec-WebSocket-Protocol"), None)
+
+ req = self.default_req()
+ req.headers["Sec-WebSocket-Protocol"] = "cat, base64"
+ resp, protocols = transact_http(req)
+ self.assertEqual(resp.status, 101)
+ self.assertEqual(protocols, ["cat", "base64"])
+ self.assertEqual(resp.getheader("Sec-WebSocket-Protocol"), "base64")
+
+def read_frames(dec):
+ frames = []
+ while True:
+ frame = dec.read_frame()
+ if frame is None:
+ break
+ frames.append((frame.fin, frame.opcode, frame.payload))
+ return frames
+
+def read_messages(dec):
+ messages = []
+ while True:
+ message = dec.read_message()
+ if message is None:
+ break
+ messages.append((message.opcode, message.payload))
+ return messages
+
+class TestWebSocketDecoder(unittest.TestCase):
+ def test_rfc(self):
+ """Test samples from RFC 6455 section 5.7."""
+ TESTS = [
+ ("\x81\x05\x48\x65\x6c\x6c\x6f", False,
+ [(True, 1, "Hello")],
+ [(1, u"Hello")]),
+ ("\x81\x85\x37\xfa\x21\x3d\x7f\x9f\x4d\x51\x58", True,
+ [(True, 1, "Hello")],
+ [(1, u"Hello")]),
+ ("\x01\x03\x48\x65\x6c\x80\x02\x6c\x6f", False,
+ [(False, 1, "Hel"), (True, 0, "lo")],
+ [(1, u"Hello")]),
+ ("\x89\x05\x48\x65\x6c\x6c\x6f", False,
+ [(True, 9, "Hello")],
+ [(9, u"Hello")]),
+ ("\x8a\x85\x37\xfa\x21\x3d\x7f\x9f\x4d\x51\x58", True,
+ [(True, 10, "Hello")],
+ [(10, u"Hello")]),
+ ("\x82\x7e\x01\x00" + "\x00" * 256, False,
+ [(True, 2, "\x00" * 256)],
+ [(2, "\x00" * 256)]),
+ ("\x82\x7f\x00\x00\x00\x00\x00\x01\x00\x00" + "\x00" * 65536, False,
+ [(True, 2, "\x00" * 65536)],
+ [(2, "\x00" * 65536)]),
+ ("\x82\x7f\x00\x00\x00\x00\x00\x01\x00\x03" + "ABCD" * 16384 + "XYZ", False,
+ [(True, 2, "ABCD" * 16384 + "XYZ")],
+ [(2, "ABCD" * 16384 + "XYZ")]),
+ ]
+ for data, use_mask, expected_frames, expected_messages in TESTS:
+ dec = WebSocketDecoder(use_mask = use_mask)
+ dec.feed(data)
+ actual_frames = read_frames(dec)
+ self.assertEqual(actual_frames, expected_frames)
+
+ dec = WebSocketDecoder(use_mask = use_mask)
+ dec.feed(data)
+ actual_messages = read_messages(dec)
+ self.assertEqual(actual_messages, expected_messages)
+
+ dec = WebSocketDecoder(use_mask = not use_mask)
+ dec.feed(data)
+ self.assertRaises(WebSocketDecoder.MaskingError, dec.read_frame)
+
+ def test_empty_feed(self):
+ """Test that the decoder can handle a zero-byte feed."""
+ dec = WebSocketDecoder()
+ self.assertEqual(dec.read_frame(), None)
+ dec.feed("")
+ self.assertEqual(dec.read_frame(), None)
+ dec.feed("\x81\x05H")
+ self.assertEqual(dec.read_frame(), None)
+ dec.feed("ello")
+ self.assertEqual(read_frames(dec), [(True, 1, u"Hello")])
+
+ def test_empty_frame(self):
+ """Test that a frame may contain a zero-byte payload."""
+ dec = WebSocketDecoder()
+ dec.feed("\x81\x00")
+ self.assertEqual(read_frames(dec), [(True, 1, u"")])
+ dec.feed("\x82\x00")
+ self.assertEqual(read_frames(dec), [(True, 2, "")])
+
+ def test_empty_message(self):
+ """Test that a message may have a zero-byte payload."""
+ dec = WebSocketDecoder()
+ dec.feed("\x01\x00\x00\x00\x80\x00")
+ self.assertEqual(read_messages(dec), [(1, u"")])
+ dec.feed("\x02\x00\x00\x00\x80\x00")
+ self.assertEqual(read_messages(dec), [(2, "")])
+
+ def test_interleaved_control(self):
+ """Test that control messages interleaved with fragmented messages are
+ returned."""
+ dec = WebSocketDecoder()
+ dec.feed("\x89\x04PING\x01\x03Hel\x8a\x04PONG\x80\x02lo\x89\x04PING")
+ self.assertEqual(read_messages(dec), [(9, "PING"), (10, "PONG"), (1, u"Hello"), (9, "PING")])
+
+ def test_fragmented_control(self):
+ """Test that illegal fragmented control messages cause an error."""
+ dec = WebSocketDecoder()
+ dec.feed("\x09\x04PING")
+ self.assertRaises(ValueError, dec.read_message)
+
+ def test_zero_opcode(self):
+ """Test that it is an error for the first frame in a message to have an
+ opcode of 0."""
+ dec = WebSocketDecoder()
+ dec.feed("\x80\x05Hello")
+ self.assertRaises(ValueError, dec.read_message)
+ dec = WebSocketDecoder()
+ dec.feed("\x00\x05Hello")
+ self.assertRaises(ValueError, dec.read_message)
+
+ def test_nonzero_opcode(self):
+ """Test that every frame after the first must have a zero opcode."""
+ dec = WebSocketDecoder()
+ dec.feed("\x01\x01H\x01\x02el\x80\x02lo")
+ self.assertRaises(ValueError, dec.read_message)
+ dec = WebSocketDecoder()
+ dec.feed("\x01\x01H\x00\x02el\x01\x02lo")
+ self.assertRaises(ValueError, dec.read_message)
+
+ def test_utf8(self):
+ """Test that text frames (opcode 1) are decoded from UTF-8."""
+ text = u"Hello World or Καλημέρα κόσμε or こんにちは 世界 or \U0001f639"
+ utf8_text = text.encode("utf-8")
+ dec = WebSocketDecoder()
+ dec.feed("\x81" + chr(len(utf8_text)) + utf8_text)
+ self.assertEqual(read_messages(dec), [(1, text)])
+
+ def test_wrong_utf8(self):
+ """Test that failed UTF-8 decoding causes an error."""
+ TESTS = [
+ "\xc0\x41", # Non-shortest form.
+ "\xc2", # Unfinished sequence.
+ ]
+ for test in TESTS:
+ dec = WebSocketDecoder()
+ dec.feed("\x81" + chr(len(test)) + test)
+ self.assertRaises(ValueError, dec.read_message)
+
+ def test_overly_large_payload(self):
+ """Test that large payloads are rejected."""
+ dec = WebSocketDecoder()
+ dec.feed("\x82\x7f\x00\x00\x00\x00\x01\x00\x00\x00")
+ self.assertRaises(ValueError, dec.read_frame)
+
+class TestWebSocketEncoder(unittest.TestCase):
+ def test_length(self):
+ """Test that payload lengths are encoded using the smallest number of
+ bytes."""
+ TESTS = [(0, 0), (125, 0), (126, 2), (65535, 2), (65536, 8)]
+ for length, encoded_length in TESTS:
+ enc = WebSocketEncoder(use_mask = False)
+ eframe = enc.encode_frame(2, "\x00" * length)
+ self.assertEqual(len(eframe), 1 + 1 + encoded_length + length)
+ enc = WebSocketEncoder(use_mask = True)
+ eframe = enc.encode_frame(2, "\x00" * length)
+ self.assertEqual(len(eframe), 1 + 1 + encoded_length + 4 + length)
+
+ def test_roundtrip(self):
+ TESTS = [
+ (1, u"Hello world"),
+ (1, u"Hello \N{WHITE SMILING FACE}"),
+ ]
+ for opcode, payload in TESTS:
+ for use_mask in (False, True):
+ enc = WebSocketEncoder(use_mask = use_mask)
+ enc_message = enc.encode_message(opcode, payload)
+ dec = WebSocketDecoder(use_mask = use_mask)
+ dec.feed(enc_message)
+ self.assertEqual(read_messages(dec), [(opcode, payload)])
+
+def format_address(addr):
+ return "%s:%d" % addr
+
+class TestConnectionLimit(unittest.TestCase):
+ def setUp(self):
+ self.p = subprocess.Popen(["./flashproxy-client", format_address(LOCAL_ADDRESS), format_address(REMOTE_ADDRESS)])
+
+ def tearDown(self):
+ self.p.terminate()
+
+# def test_remote_limit(self):
+# """Test that the client transport plugin limits the number of remote
+# connections that it will accept."""
+# for i in range(5):
+# s = socket.create_connection(REMOTE_ADDRESS, 2)
+# self.assertRaises(socket.error, socket.create_connection, REMOTE_ADDRESS)
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/flashproxy-reg-appspot b/flashproxy-reg-appspot
new file mode 100755
index 0000000..db2fdbd
--- /dev/null
+++ b/flashproxy-reg-appspot
@@ -0,0 +1,252 @@
+#!/usr/bin/env python
+"""Register with a facilitator through Google App Engine."""
+
+import errno
+import getopt
+import httplib
+import os
+import socket
+import subprocess
+import sys
+import urlparse
+import urllib2
+
+from flashproxy.keys import PIN_GOOGLE_CA_CERT, PIN_GOOGLE_PUBKEY_SHA1, check_certificate_pin, temp_cert
+from flashproxy.util import parse_addr_spec, format_addr
+
+try:
+ from M2Crypto import SSL
+except ImportError:
+ # Defer the error reporting so that --help works even without M2Crypto.
+ SSL = None
+
+DEFAULT_REMOTE_ADDRESS = ""
+DEFAULT_REMOTE_PORT = 9000
+DEFAULT_TRANSPORT = "websocket"
+
+# The domain to which requests appear to go.
+FRONT_DOMAIN = "www.google.com"
+# The value of the Host header within requests.
+TARGET_DOMAIN = "fp-reg-a.appspot.com"
+
+FLASHPROXY_REG_URL = "flashproxy-reg-url"
+
+class options(object):
+ address_family = socket.AF_UNSPEC
+ use_certificate_pin = True
+ facilitator_pubkey_filename = None
+ transport = DEFAULT_TRANSPORT
+ safe_logging = True
+
+def usage(f = sys.stdout):
+ print >> f, """\
+Usage: %(progname)s [REMOTE][:PORT]
+Register with a flash proxy facilitator through a Google App Engine app.
+By default the remote address registered is "%(remote_addr)s" (the
+external IP address is guessed).
+
+ -4 name lookups use only IPv4.
+ -6 name lookups use only IPv6.
+ --disable-pin don't check server public key against a known pin.
+ --facilitator-pubkey=FILENAME
+ encrypt registrations to the given PEM-formatted
+ public key (default built-in).
+ -h, --help show this help.
+ --transport=TRANSPORT register using the given transport
+ (default "%(transport)s").
+ --unsafe-logging don't scrub IP addresses from logs.\
+""" % {
+ "progname": sys.argv[0],
+ "remote_addr": format_addr((DEFAULT_REMOTE_ADDRESS, DEFAULT_REMOTE_PORT)),
+ "transport": DEFAULT_TRANSPORT,
+}
+
+def safe_str(s):
+ """Return "[scrubbed]" if options.safe_logging is true, and s otherwise."""
+ if options.safe_logging:
+ return "[scrubbed]"
+ else:
+ return s
+
+def safe_format_addr(addr):
+ return safe_str(format_addr(addr))
+
+def get_state_dir():
+ """Get a directory where we can put temporary files. Returns None if any
+ suitable temporary directory will do."""
+ pt_dir = os.environ.get("TOR_PT_STATE_LOCATION")
+ if pt_dir is None:
+ return None
+ try:
+ os.makedirs(pt_dir)
+ except OSError, e:
+ if e.errno != errno.EEXIST:
+ raise
+ return pt_dir
+
+def generate_url(addr):
+ if getattr(sys, "frozen", False):
+ script_dir = os.path.dirname(sys.executable)
+ else:
+ script_dir = sys.path[0]
+ if not script_dir:
+ # Maybe the script was read from stdin; in any case don't guess at the directory.
+ raise ValueError("Can't find executable directory for registration helpers")
+ command = [os.path.join(script_dir, FLASHPROXY_REG_URL)]
+ command += ["-f", urlparse.urlunparse(("https", FRONT_DOMAIN, "/", "", "", ""))]
+ if options.transport is not None:
+ command += ["--transport", options.transport]
+ if options.facilitator_pubkey_filename is not None:
+ command += ["--facilitator-pubkey", options.facilitator_pubkey_filename]
+ command.append(format_addr(addr))
+ p = subprocess.Popen(command, stdout=subprocess.PIPE)
+ stdout, stderr = p.communicate()
+ if p.returncode != 0:
+ raise ValueError("%s exited with status %d" % (FLASHPROXY_REG_URL, p.returncode))
+ return stdout.strip()
+
+# Like socket.create_connection in that it tries resolving different address
+# families, but doesn't connect the socket.
+def create_socket(address, timeout = None):
+ host, port = address
+ addrs = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM)
+ if not addrs:
+ raise socket.error("getaddrinfo returns an empty list")
+ err = None
+ for addr in addrs:
+ try:
+ s = socket.socket(addr[0], addr[1], addr[2])
+ if timeout is not None and type(timeout) == float:
+ s.settimeout(timeout)
+ return s
+ except Exception, e:
+ err = e
+ raise err
+
+# Certificate validation and pinning for urllib2. Inspired by
+# http://web.archive.org/web/20110125104752/http://www.muchtooscrawled.com/2010/03/https-certificate-verification-in-python-with-urllib2/.
+
+class PinHTTPSConnection(httplib.HTTPSConnection):
+ def connect(self):
+ sock = create_socket((self.host, self.port), self.timeout)
+ if self._tunnel_host:
+ self.sock = sock
+ self._tunnel()
+
+ ctx = SSL.Context("tlsv1")
+ ctx.set_verify(SSL.verify_peer, 3)
+
+ with temp_cert(PIN_GOOGLE_CA_CERT) as ca_file:
+ ret = ctx.load_verify_locations(ca_file.name)
+ assert ret == 1
+
+ self.sock = SSL.Connection(ctx, sock)
+ self.sock.connect((self.host, self.port))
+
+ if options.use_certificate_pin:
+ check_certificate_pin(self.sock, PIN_GOOGLE_PUBKEY_SHA1)
+
+class PinHTTPSHandler(urllib2.HTTPSHandler):
+ def https_open(self, req):
+ return self.do_open(PinHTTPSConnection, req)
+
+def urlopen(url):
+ req = urllib2.Request(url)
+ req.add_header("Host", TARGET_DOMAIN)
+ opener = urllib2.build_opener(PinHTTPSHandler())
+ return opener.open(req)
+
+def get_external_ip():
+ f = urlopen(urlparse.urlunparse(("https", FRONT_DOMAIN, "/ip", "", "", "")))
+ try:
+ return f.read().strip()
+ finally:
+ f.close()
+
+opt, args = getopt.gnu_getopt(sys.argv[1:], "46h", [
+ "disable-pin",
+ "facilitator-pubkey=",
+ "help",
+ "transport=",
+ "unsafe-logging",
+])
+for o, a in opt:
+ if o == "-4":
+ options.address_family = socket.AF_INET
+ elif o == "-6":
+ options.address_family = socket.AF_INET6
+ elif o == "--disable-pin":
+ options.use_certificate_pin = False
+ elif o == "--facilitator-pubkey":
+ options.facilitator_pubkey_filename = a
+ elif o == "-h" or o == "--help":
+ usage()
+ sys.exit()
+ elif o == "--transport":
+ options.transport = a
+ elif o == "--unsafe-logging":
+ options.safe_logging = False
+
+if len(args) == 0:
+ remote_addr = (DEFAULT_REMOTE_ADDRESS, DEFAULT_REMOTE_PORT)
+elif len(args) == 1:
+ remote_addr = parse_addr_spec(args[0], DEFAULT_REMOTE_ADDRESS, DEFAULT_REMOTE_PORT)
+else:
+ usage(sys.stderr)
+ sys.exit(1)
+
+if SSL is None:
+ print >> sys.stderr, """\
+This program requires the M2Crypto library, which is not installed.
+
+You can install it using one of the packages at
+http://chandlerproject.org/Projects/MeTooCrypto#Downloads.
+
+On Debian-like systems, use the command "apt-get install python-m2crypto".\
+"""
+ sys.exit(1)
+
+if options.address_family != socket.AF_UNSPEC:
+ getaddrinfo = socket.getaddrinfo
+ def getaddrinfo_replacement(host, port, family, *args, **kwargs):
+ return getaddrinfo(host, port, options.address_family, *args, **kwargs)
+ socket.getaddrinfo = getaddrinfo_replacement
+
+if not remote_addr[0]:
+ try:
+ ip = get_external_ip()
+ except urllib2.HTTPError, e:
+ print >> sys.stderr, "Status code was %d, not 200" % e.code
+ sys.exit(1)
+ except urllib2.URLError, e:
+ print >> sys.stderr, "Failed to get external IP address: %s" % str(e.reason)
+ sys.exit(1)
+ except Exception, e:
+ print >> sys.stderr, "Failed to get external IP address: %s" % str(e)
+ sys.exit(1)
+ try:
+ remote_addr = parse_addr_spec(ip, *remote_addr)
+ except ValueError, e:
+ print >> sys.stderr, "Error parsing external IP address %s: %s" % (safe_str(repr(ip)), str(e))
+ sys.exit(1)
+
+try:
+ url = generate_url(remote_addr)
+except Exception, e:
+ print >> sys.stderr, "Error running %s: %s" % (FLASHPROXY_REG_URL, str(e))
+ sys.exit(1)
+
+try:
+ http = urlopen(url)
+except urllib2.HTTPError, e:
+ print >> sys.stderr, "Status code was %d, not 200" % e.code
+ sys.exit(1)
+except urllib2.URLError, e:
+ print >> sys.stderr, "Failed to register: %s" % str(e.reason)
+ sys.exit(1)
+except Exception, e:
+ print >> sys.stderr, "Failed to register: %s" % str(e)
+ sys.exit(1)
+http.close()
+
+print "Registered \"%s\" with %s." % (safe_format_addr(remote_addr), TARGET_DOMAIN)
diff --git a/flashproxy-reg-email b/flashproxy-reg-email
new file mode 100755
index 0000000..5d38fa5
--- /dev/null
+++ b/flashproxy-reg-email
@@ -0,0 +1,245 @@
+#!/usr/bin/env python
+"""Register with a facilitator using the email method."""
+
+import errno
+import getopt
+import os
+import re
+import smtplib
+import socket
+import sys
+import urllib
+
+from flashproxy.keys import PIN_GOOGLE_CA_CERT, PIN_GOOGLE_PUBKEY_SHA1, DEFAULT_FACILITATOR_PUBKEY_PEM, check_certificate_pin, temp_cert
+from flashproxy.util import parse_addr_spec, format_addr
+
+try:
+ from M2Crypto import BIO, RSA, SSL
+except ImportError:
+ # Defer the error reporting so that --help works even without M2Crypto.
+ BIO = None
+ RSA = None
+ SSL = None
+
+DEFAULT_REMOTE_ADDRESS = ""
+DEFAULT_REMOTE_PORT = 9000
+DEFAULT_EMAIL_ADDRESS = "flashproxyreg.a at gmail.com"
+# dig MX gmail.com
+DEFAULT_SMTP_HOST = "gmail-smtp-in.l.google.com"
+DEFAULT_SMTP_PORT = 25
+DEFAULT_TRANSPORT = "websocket"
+
+# Use this to prevent Python smtplib from guessing and leaking our hostname.
+EHLO_FQDN = "[127.0.0.1]"
+FROM_EMAIL_ADDRESS = "nobody at localhost"
+
+class options(object):
+ remote_addr = None
+
+ address_family = socket.AF_UNSPEC
+ debug = False
+ use_certificate_pin = True
+ email_addr = None
+ facilitator_pubkey_filename = None
+ smtp_addr = None
+ transport = DEFAULT_TRANSPORT
+ safe_logging = True
+
+def usage(f = sys.stdout):
+ print >> f, """\
+Usage: %(progname)s [REMOTE][:PORT]
+Register with a flash proxy facilitator through email. Makes a STARTTLS
+connection to an SMTP server and sends mail with a client IP address to a
+designated address. By default the remote address registered is
+"%(remote_addr)s" (the external IP address is guessed).
+
+Using an SMTP server or email address other than the defaults will not work
+unless you have made special arrangements to connect them to a facilitator.
+
+This program requires the M2Crypto library for Python.
+
+ -4 name lookups use only IPv4.
+ -6 name lookups use only IPv6.
+ -d, --debug enable debugging output (Python smtplib messages).
+ --disable-pin don't check server public key against a known pin.
+ -e, --email=ADDRESS send mail to ADDRESS (default "%(email_addr)s").
+ --facilitator-pubkey=FILENAME
+ encrypt registrations to the given PEM-formatted
+ public key (default built-in).
+ -h, --help show this help.
+ -s, --smtp=HOST[:PORT] use the given SMTP server
+ (default "%(smtp_addr)s").
+ --transport=TRANSPORT register using the given transport
+ (default "%(transport)s").
+ --unsafe-logging don't scrub IP addresses from logs.\
+""" % {
+ "progname": sys.argv[0],
+ "remote_addr": format_addr((DEFAULT_REMOTE_ADDRESS, DEFAULT_REMOTE_PORT)),
+ "email_addr": DEFAULT_EMAIL_ADDRESS,
+ "smtp_addr": format_addr((DEFAULT_SMTP_HOST, DEFAULT_SMTP_PORT)),
+ "transport": DEFAULT_TRANSPORT,
+}
+
+def safe_str(s):
+ """Return "[scrubbed]" if options.safe_logging is true, and s otherwise."""
+ if options.safe_logging:
+ return "[scrubbed]"
+ else:
+ return s
+
+def safe_format_addr(addr):
+ return safe_str(format_addr(addr))
+
+def build_reg(addr, transport):
+ return urllib.urlencode((
+ ("client", format_addr(addr)),
+ ("client-transport", transport),
+ ))
+
+def get_state_dir():
+ """Get a directory where we can put temporary files. Returns None if any
+ suitable temporary directory will do."""
+ pt_dir = os.environ.get("TOR_PT_STATE_LOCATION")
+ if pt_dir is None:
+ return None
+ try:
+ os.makedirs(pt_dir)
+ except OSError, e:
+ if e.errno != errno.EEXIST:
+ raise
+ return pt_dir
+
+def get_facilitator_pubkey():
+ if options.facilitator_pubkey_filename is not None:
+ return RSA.load_pub_key(options.facilitator_pubkey_filename)
+ else:
+ return RSA.load_pub_key_bio(BIO.MemoryBuffer(DEFAULT_FACILITATOR_PUBKEY_PEM))
+
+options.email_addr = DEFAULT_EMAIL_ADDRESS
+options.smtp_addr = (DEFAULT_SMTP_HOST, DEFAULT_SMTP_PORT)
+
+opts, args = getopt.gnu_getopt(sys.argv[1:], "46de:hs:", [
+ "debug",
+ "disable-pin",
+ "email=",
+ "facilitator-pubkey=",
+ "help",
+ "smtp=",
+ "transport=",
+ "unsafe-logging",
+])
+for o, a in opts:
+ if o == "-4":
+ options.address_family = socket.AF_INET
+ elif o == "-6":
+ options.address_family = socket.AF_INET6
+ elif o == "-d" or o == "--debug":
+ options.debug = True
+ elif o == "--disable-pin":
+ options.use_certificate_pin = False
+ elif o == "-e" or o == "--email":
+ options.email_addr = a
+ elif o == "--facilitator-pubkey":
+ options.facilitator_pubkey_filename = a
+ elif o == "-h" or o == "--help":
+ usage()
+ sys.exit()
+ elif o == "-s" or o == "--smtp":
+ options.smtp_addr = parse_addr_spec(a, DEFAULT_SMTP_HOST, DEFAULT_SMTP_PORT)
+ elif o == "--transport":
+ options.transport = a
+ elif o == "--unsafe-logging":
+ options.safe_logging = False
+
+if len(args) == 0:
+ options.remote_addr = (DEFAULT_REMOTE_ADDRESS, DEFAULT_REMOTE_PORT)
+elif len(args) == 1:
+ options.remote_addr = parse_addr_spec(args[0], DEFAULT_REMOTE_ADDRESS, DEFAULT_REMOTE_PORT)
+else:
+ usage(sys.stderr)
+ sys.exit(1)
+
+if SSL is None:
+ print >> sys.stderr, """\
+This program requires the M2Crypto library, which is not installed.
+
+You can install it using one of the packages at
+http://chandlerproject.org/Projects/MeTooCrypto#Downloads.
+
+On Debian-like systems, use the command "apt-get install python-m2crypto".\
+"""
+ sys.exit(1)
+
+if options.address_family != socket.AF_UNSPEC:
+ getaddrinfo = socket.getaddrinfo
+ def getaddrinfo_replacement(host, port, family, *args, **kwargs):
+ return getaddrinfo(host, port, options.address_family, *args, **kwargs)
+ socket.getaddrinfo = getaddrinfo_replacement
+
+smtp = smtplib.SMTP(options.smtp_addr[0], options.smtp_addr[1], EHLO_FQDN)
+
+if options.debug:
+ smtp.set_debuglevel(1)
+
+try:
+ ctx = SSL.Context("tlsv1")
+ ctx.set_verify(SSL.verify_peer, 3)
+
+ with temp_cert(PIN_GOOGLE_CA_CERT) as ca_file:
+ # We roll our own initial EHLO/STARTTLS because smtplib.SMTP.starttls
+ # doesn't allow enough certificate validation.
+ code, msg = smtp.docmd("EHLO", EHLO_FQDN)
+ if code != 250:
+ raise ValueError("Got code %d after EHLO" % code)
+ code, msg = smtp.docmd("STARTTLS")
+ if code != 220:
+ raise ValueError("Got code %d after STARTTLS" % code)
+ ret = ctx.load_verify_locations(ca_file.name)
+ assert ret == 1
+
+ smtp.sock = SSL.Connection(ctx, smtp.sock)
+ smtp.sock.setup_ssl()
+ smtp.sock.set_connect_state()
+ smtp.sock.connect_ssl()
+ smtp.file = smtp.sock.makefile()
+
+ if options.use_certificate_pin:
+ check_certificate_pin(smtp.sock, PIN_GOOGLE_PUBKEY_SHA1)
+ smtp.ehlo(EHLO_FQDN)
+
+ if not options.remote_addr[0]:
+ # Grep the EHLO response for our public IP address.
+ m = re.search(r'at your service, \[([0-9a-fA-F.:]+)\]', smtp.ehlo_resp)
+ if not m:
+ raise ValueError("Could not guess external IP address from EHLO response")
+ spec = m.group(1)
+ if ":" in spec:
+ # Guess IPv6.
+ spec = "[" + spec + "]"
+ options.remote_addr = parse_addr_spec(spec, *options.remote_addr)
+
+ body_plain = build_reg(options.remote_addr, options.transport)
+ rsa = get_facilitator_pubkey()
+ body_crypt = rsa.public_encrypt(body_plain, RSA.pkcs1_oaep_padding)
+ body = body_crypt.encode("base64")
+
+ # Add a random subject to keep Gmail from threading everything.
+ rand_string = os.urandom(5).encode("hex")
+ smtp.sendmail(options.email_addr, options.email_addr, """\
+To: %(to_addr)s\r
+From: %(from_addr)s\r
+Subject: client reg %(rand_string)s\r
+\r
+%(body)s
+""" % {
+ "to_addr": options.email_addr,
+ "from_addr": FROM_EMAIL_ADDRESS,
+ "rand_string": rand_string,
+ "body": body,
+ })
+ smtp.quit()
+except Exception, e:
+ print >> sys.stderr, "Failed to register: %s" % str(e)
+ sys.exit(1)
+
+print "Registered \"%s\" with %s." % (safe_format_addr(options.remote_addr), options.email_addr)
diff --git a/flashproxy-reg-http b/flashproxy-reg-http
new file mode 100755
index 0000000..9f85570
--- /dev/null
+++ b/flashproxy-reg-http
@@ -0,0 +1,114 @@
+#!/usr/bin/env python
+"""Register with a facilitator using the HTTP method."""
+
+import getopt
+import socket
+import sys
+import urllib
+import urllib2
+
+from flashproxy.util import parse_addr_spec, format_addr
+
+DEFAULT_REMOTE_ADDRESS = ""
+DEFAULT_REMOTE_PORT = 9000
+DEFAULT_FACILITATOR_URL = "https://fp-facilitator.org/"
+DEFAULT_TRANSPORT = "websocket"
+
+class options(object):
+ remote_addr = None
+
+ address_family = socket.AF_UNSPEC
+ facilitator_url = None
+ transport = DEFAULT_TRANSPORT
+ safe_logging = True
+
+def usage(f = sys.stdout):
+ print >> f, """\
+Usage: %(progname)s [REMOTE][:PORT]
+Register with a flash proxy facilitator using an HTTP POST. By default the
+remote address registered is "%(remote_addr)s".
+
+ -4 name lookups use only IPv4.
+ -6 name lookups use only IPv6.
+ -f, --facilitator=URL register with the given facilitator
+ (default "%(fac_url)s").
+ -h, --help show this help.
+ --transport=TRANSPORT register using the given transport
+ (default "%(transport)s").
+ --unsafe-logging don't scrub IP addresses from logs.\
+""" % {
+ "progname": sys.argv[0],
+ "fac_url": DEFAULT_FACILITATOR_URL,
+ "remote_addr": format_addr((DEFAULT_REMOTE_ADDRESS, DEFAULT_REMOTE_PORT)),
+ "transport": DEFAULT_TRANSPORT,
+}
+
+def safe_str(s):
+ """Return "[scrubbed]" if options.safe_logging is true, and s otherwise."""
+ if options.safe_logging:
+ return "[scrubbed]"
+ else:
+ return s
+
+def safe_format_addr(addr):
+ return safe_str(format_addr(addr))
+
+def build_reg(addr, transport):
+ return urllib.urlencode((
+ ("client", format_addr(addr)),
+ ("client-transport", transport),
+ ))
+
+options.facilitator_url = DEFAULT_FACILITATOR_URL
+options.remote_addr = (DEFAULT_REMOTE_ADDRESS, DEFAULT_REMOTE_PORT)
+
+opts, args = getopt.gnu_getopt(sys.argv[1:], "46f:h", [
+ "facilitator=",
+ "help",
+ "transport=",
+ "unsafe-logging",
+])
+for o, a in opts:
+ if o == "-4":
+ options.address_family = socket.AF_INET
+ elif o == "-6":
+ options.address_family = socket.AF_INET6
+ elif o == "-f" or o == "--facilitator":
+ options.facilitator_url = a
+ elif o == "-h" or o == "--help":
+ usage()
+ sys.exit()
+ elif o == "--transport":
+ options.transport = a
+ elif o == "--unsafe-logging":
+ options.safe_logging = False
+
+if len(args) == 0:
+ pass
+elif len(args) == 1:
+ options.remote_addr = parse_addr_spec(args[0], DEFAULT_REMOTE_ADDRESS, DEFAULT_REMOTE_PORT)
+else:
+ usage(sys.stderr)
+ sys.exit(1)
+
+if options.address_family != socket.AF_UNSPEC:
+ getaddrinfo = socket.getaddrinfo
+ def getaddrinfo_replacement(host, port, family, *args, **kwargs):
+ return getaddrinfo(host, port, options.address_family, *args, **kwargs)
+ socket.getaddrinfo = getaddrinfo_replacement
+
+body = build_reg(options.remote_addr, options.transport)
+try:
+ http = urllib2.urlopen(options.facilitator_url, body, 10)
+except urllib2.HTTPError, e:
+ print >> sys.stderr, "Status code was %d, not 200" % e.code
+ sys.exit(1)
+except urllib2.URLError, e:
+ print >> sys.stderr, "Failed to register: %s" % str(e.reason)
+ sys.exit(1)
+except Exception, e:
+ print >> sys.stderr, "Failed to register: %s" % str(e)
+ sys.exit(1)
+http.close()
+
+print "Registered \"%s\" with %s." % (safe_format_addr(options.remote_addr), options.facilitator_url)
diff --git a/flashproxy-reg-url b/flashproxy-reg-url
new file mode 100755
index 0000000..4685f28
--- /dev/null
+++ b/flashproxy-reg-url
@@ -0,0 +1,107 @@
+#!/usr/bin/env python
+"""Register with a facilitator using an indirect URL."""
+
+import base64
+import getopt
+import sys
+import urllib
+import urlparse
+
+from flashproxy.keys import DEFAULT_FACILITATOR_PUBKEY_PEM
+from flashproxy.util import parse_addr_spec, format_addr
+
+try:
+ from M2Crypto import BIO, RSA
+except ImportError:
+ # Defer the error reporting so that --help works even without M2Crypto.
+ RSA = None
+
+DEFAULT_REMOTE_ADDRESS = ""
+DEFAULT_REMOTE_PORT = 9000
+DEFAULT_FACILITATOR_URL = "https://fp-facilitator.org/"
+DEFAULT_TRANSPORT = "websocket"
+
+class options(object):
+ facilitator_url = None
+ facilitator_pubkey_filename = None
+ transport = DEFAULT_TRANSPORT
+
+def usage(f = sys.stdout):
+ print >> f, """\
+Usage: %(progname)s REMOTE[:PORT]
+Print a URL, which, when retrieved, will cause the client address
+REMOTE[:PORT] to be registered with the flash proxy facilitator. The
+default PORT is %(port)d.
+
+ -f, --facilitator=URL register with the given facilitator
+ (default "%(fac_url)s").
+ --facilitator-pubkey=FILENAME
+ encrypt registrations to the given PEM-formatted
+ public key (default built-in).
+ -h, --help show this help.
+ --transport=TRANSPORT register using the given transport
+ (default "%(transport)s").\
+""" % {
+ "progname": sys.argv[0],
+ "fac_url": DEFAULT_FACILITATOR_URL,
+ "port": DEFAULT_REMOTE_PORT,
+ "transport": DEFAULT_TRANSPORT,
+}
+
+def build_reg(addr, transport):
+ return urllib.urlencode((
+ ("client", format_addr(addr)),
+ ("client-transport", transport),
+ ))
+
+def get_facilitator_pubkey():
+ if options.facilitator_pubkey_filename is not None:
+ return RSA.load_pub_key(options.facilitator_pubkey_filename)
+ else:
+ return RSA.load_pub_key_bio(BIO.MemoryBuffer(DEFAULT_FACILITATOR_PUBKEY_PEM))
+
+options.facilitator_url = DEFAULT_FACILITATOR_URL
+
+opt, args = getopt.gnu_getopt(sys.argv[1:], "f:h", [
+ "facilitator=",
+ "facilitator-pubkey=",
+ "help",
+ "transport=",
+])
+for o, a in opt:
+ if o == "-f" or o == "--facilitator":
+ options.facilitator_url = a
+ elif o == "--facilitator-pubkey":
+ options.facilitator_pubkey_filename = a
+ elif o == "-h" or o == "--help":
+ usage()
+ sys.exit()
+ elif o == "--transport":
+ options.transport = a
+
+if len(args) != 1:
+ usage(sys.stderr)
+ sys.exit(1)
+
+remote_addr = parse_addr_spec(args[0], DEFAULT_REMOTE_ADDRESS, DEFAULT_REMOTE_PORT)
+if not remote_addr[0]:
+ print >> sys.stderr, "An IP address (not just a port) is required."
+ sys.exit(1)
+
+if RSA is None:
+ print >> sys.stderr, """\
+This program requires the M2Crypto library, which is not installed.
+
+You can install it using one of the packages at
+http://chandlerproject.org/Projects/MeTooCrypto#Downloads.
+
+On Debian-like systems, use the command "apt-get install python-m2crypto".\
+"""
+ sys.exit(1)
+
+reg_plain = build_reg(remote_addr, options.transport)
+rsa = get_facilitator_pubkey()
+reg_crypt = rsa.public_encrypt(reg_plain, RSA.pkcs1_oaep_padding)
+reg = base64.urlsafe_b64encode(reg_crypt)
+
+print urlparse.urljoin(options.facilitator_url, "reg/" + reg)
diff --git a/flashproxy/__init__.py b/flashproxy/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/flashproxy/fac.py b/flashproxy/fac.py
new file mode 100644
index 0000000..25c1f84
--- /dev/null
+++ b/flashproxy/fac.py
@@ -0,0 +1,218 @@
+import socket
+import subprocess
+import urlparse
+
+from flashproxy import reg
+from flashproxy.util import parse_addr_spec, format_addr
+
+DEFAULT_CLIENT_TRANSPORT = "websocket"
+
+def read_client_registrations(body, defhost=None, defport=None):
+ """Yield client registrations (as Endpoints) from an encoded registration
+ message body. The message format is one registration per line, with each
+ line being encoded as application/x-www-form-urlencoded. The key "client" is
+ required and contains the client address and port (perhaps filled in by
+ defhost and defport). The key "client-transport" is optional and defaults to
+ "websocket".
+ Example:
+ client=1.2.3.4:9000&client-transport=websocket
+ client=1.2.3.4:9090&client-transport=obfs3|websocket
+ """
+ for line in body.splitlines():
+ qs = urlparse.parse_qs(line, keep_blank_values=True, strict_parsing=True)
+ # Get the unique value associated with the given key in qs. If the key
+ # is absent or appears more than once, raise ValueError.
+ def get_unique(key, default=None):
+ try:
+ vals = qs[key]
+ except KeyError:
+ if default is None:
+ raise ValueError("missing %r key" % key)
+ vals = (default,)
+ if len(vals) != 1:
+ raise ValueError("more than one %r key" % key)
+ return vals[0]
+ addr = parse_addr_spec(get_unique("client"), defhost, defport)
+ transport = get_unique("client-transport", DEFAULT_CLIENT_TRANSPORT)
+ yield reg.Endpoint(addr, transport)
+
+def skip_space(pos, line):
+ """Skip a (possibly empty) sequence of space characters (the ASCII character
+ '\x20' exactly). Returns a pair (pos, num_skipped)."""
+ begin = pos
+ while pos < len(line) and line[pos] == "\x20":
+ pos += 1
+ return pos, pos - begin
+
+TOKEN_CHARS = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-")
+def get_token(pos, line):
+ begin = pos
+ while pos < len(line) and line[pos] in TOKEN_CHARS:
+ pos += 1
+ if begin == pos:
+ raise ValueError("No token found at position %d" % pos)
+ return pos, line[begin:pos]
+
+def get_quoted_string(pos, line):
+ chars = []
+ if not (pos < len(line) and line[pos] == '"'):
+ raise ValueError("Expected '\"' at beginning of quoted string.")
+ pos += 1
+ while pos < len(line) and line[pos] != '"':
+ if line[pos] == '\\':
+ pos += 1
+ if not (pos < len(line)):
+ raise ValueError("End of line after backslash in quoted string")
+ chars.append(line[pos])
+ pos += 1
+ if not (pos < len(line) and line[pos] == '"'):
+ raise ValueError("Expected '\"' at end of quoted string.")
+ pos += 1
+ return pos, "".join(chars)
+
+def parse_transaction(line):
+ """A transaction is a command followed by zero or more key-value pairs. Like so:
+ COMMAND KEY="VALUE" KEY="\"ESCAPED\" VALUE"
+ Values must be quoted. Any byte value may be escaped with a backslash.
+ Returns a pair: (COMMAND, ((KEY1, VALUE1), (KEY2, VALUE2), ...)).
+ """
+ pos = 0
+ pos, skipped = skip_space(pos, line)
+ pos, command = get_token(pos, line)
+
+ pairs = []
+ while True:
+ pos, skipped = skip_space(pos, line)
+ if not (pos < len(line)):
+ break
+ if skipped == 0:
+ raise ValueError("Expected space before key-value pair")
+ pos, key = get_token(pos, line)
+ if not (pos < len(line) and line[pos] == '='):
+ raise ValueError("No '=' found after key")
+ pos += 1
+ pos, value = get_quoted_string(pos, line)
+ pairs.append((key, value))
+ return command, tuple(pairs)
+
+def param_first(key, params):
+ """Search 'params' for 'key' and return the first value that
+ occurs. If 'key' was not found, return None."""
+ for k, v in params:
+ if key == k:
+ return v
+ return None
+
+def param_getlist(key, params):
+ """Search 'params' for 'key' and return a list with its values. If
+ 'key' did not appear in 'params', return the empty list."""
+ result = []
+ for k, v in params:
+ if key == k:
+ result.append(v)
+ return result
+
+def quote_string(s):
+ chars = []
+ for c in s:
+ if c == "\\":
+ c = "\\\\"
+ elif c == "\"":
+ c = "\\\""
+ chars.append(c)
+ return "\"" + "".join(chars) + "\""
+
+def render_transaction(command, *params):
+ parts = [command]
+ for key, value in params:
+ parts.append("%s=%s" % (key, quote_string(value)))
+ return " ".join(parts)
+
+def fac_socket(facilitator_addr):
+ return socket.create_connection(facilitator_addr, 1.0).makefile()
+
+def transact(f, command, *params):
+ transaction = render_transaction(command, *params)
+ print >> f, transaction
+ f.flush()
+ line = f.readline()
+ if not (len(line) > 0 and line[-1] == '\n'):
+ raise ValueError("No newline at end of string returned by facilitator")
+ return parse_transaction(line[:-1])
+
+def put_reg(facilitator_addr, client_addr, transport):
+ """Send a registration to the facilitator using a one-time socket. Returns
+ true iff the command was successful. transport is a transport string such as
+ "websocket" or "obfs3|websocket"."""
+ f = fac_socket(facilitator_addr)
+ params = [("CLIENT", format_addr(client_addr))]
+ params.append(("TRANSPORT", transport))
+ try:
+ command, params = transact(f, "PUT", *params)
+ finally:
+ f.close()
+ return command == "OK"
+
+def get_reg(facilitator_addr, proxy_addr, proxy_transport_list):
+ """
+ Get a client registration for proxy proxy_addr from the
+ facilitator at facilitator_addr using a one-time
+ socket. proxy_transport_list is a list containing the transport names that
+ the flashproxy supports.
+
+ Returns a dict with keys "client", "client-transport", "relay",
+ and "relay-transport" if successful, or a dict with the key "client"
+ mapped to the value "" if there are no registrations available for
+ proxy_addr. Raises an exception otherwise."""
+ f = fac_socket(facilitator_addr)
+
+ # Form a list (in transact() format) with the transports that we
+ # should send to the facilitator. Then pass that list to the
+ # transact() function.
+ # For example, PROXY-TRANSPORT=obfs2 PROXY-TRANSPORT=obfs3.
+ transports = [("PROXY-TRANSPORT", tp) for tp in proxy_transport_list]
+
+ try:
+ command, params = transact(f, "GET", ("FROM", format_addr(proxy_addr)), *transports)
+ finally:
+ f.close()
+ response = {}
+ check_back_in = param_first("CHECK-BACK-IN", params)
+ if check_back_in is not None:
+ try:
+ float(check_back_in)
+ except ValueError:
+ raise ValueError("Facilitator returned non-numeric polling interval.")
+ response["check-back-in"] = check_back_in
+ if command == "NONE":
+ response["client"] = ""
+ return response
+ elif command == "OK":
+ client_spec = param_first("CLIENT", params)
+ client_transport = param_first("CLIENT-TRANSPORT", params)
+ relay_spec = param_first("RELAY", params)
+ relay_transport = param_first("RELAY-TRANSPORT", params)
+ if not client_spec:
+ raise ValueError("Facilitator did not return CLIENT")
+ if not client_transport:
+ raise ValueError("Facilitator did not return CLIENT-TRANSPORT")
+ if not relay_spec:
+ raise ValueError("Facilitator did not return RELAY")
+ if not relay_transport:
+ raise ValueError("Facilitator did not return RELAY-TRANSPORT")
+ # Check the syntax returned by the facilitator.
+ client = parse_addr_spec(client_spec)
+ relay = parse_addr_spec(relay_spec)
+ response["client"] = format_addr(client)
+ response["client-transport"] = client_transport
+ response["relay"] = format_addr(relay)
+ response["relay-transport"] = relay_transport
+ return response
+ else:
+ raise ValueError("Facilitator response was not \"OK\"")
+
+def put_reg_proc(args, data):
+ """Attempt to add a registration by running a program."""
+ p = subprocess.Popen(args, stdin=subprocess.PIPE)
+ stdout, stderr = p.communicate(data)
+ return p.returncode == 0
diff --git a/flashproxy/keys.py b/flashproxy/keys.py
new file mode 100644
index 0000000..ff27448
--- /dev/null
+++ b/flashproxy/keys.py
@@ -0,0 +1,86 @@
+import tempfile
+
+from hashlib import sha1
+
+# We trust no other CA certificate than this.
+#
+# To find the certificate to copy here,
+# $ strace openssl s_client -connect FRONT_DOMAIN:443 -verify 10 -CApath /etc/ssl/certs 2>&1 | grep /etc/ssl/certs
+# stat("/etc/ssl/certs/XXXXXXXX.0", {st_mode=S_IFREG|0644, st_size=YYYY, ...}) = 0
+PIN_GOOGLE_CA_CERT = """\
+subject=/C=US/O=Equifax/OU=Equifax Secure Certificate Authority
+issuer=/C=US/O=Equifax/OU=Equifax Secure Certificate Authority
+-----BEGIN CERTIFICATE-----
+MIIDIDCCAomgAwIBAgIENd70zzANBgkqhkiG9w0BAQUFADBOMQswCQYDVQQGEwJV
+UzEQMA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2Vy
+dGlmaWNhdGUgQXV0aG9yaXR5MB4XDTk4MDgyMjE2NDE1MVoXDTE4MDgyMjE2NDE1
+MVowTjELMAkGA1UEBhMCVVMxEDAOBgNVBAoTB0VxdWlmYXgxLTArBgNVBAsTJEVx
+dWlmYXggU2VjdXJlIENlcnRpZmljYXRlIEF1dGhvcml0eTCBnzANBgkqhkiG9w0B
+AQEFAAOBjQAwgYkCgYEAwV2xWGcIYu6gmi0fCG2RFGiYCh7+2gRvE4RiIcPRfM6f
+BeC4AfBONOziipUEZKzxa1NfBbPLZ4C/QgKO/t0BCezhABRP/PvwDN1Dulsr4R+A
+cJkVV5MW8Q+XarfCaCMczE1ZMKxRHjuvK9buY0V7xdlfUNLjUA86iOe/FP3gx7kC
+AwEAAaOCAQkwggEFMHAGA1UdHwRpMGcwZaBjoGGkXzBdMQswCQYDVQQGEwJVUzEQ
+MA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2VydGlm
+aWNhdGUgQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMBoGA1UdEAQTMBGBDzIwMTgw
+ODIyMTY0MTUxWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUSOZo+SvSspXXR9gj
+IBBPM5iQn9QwHQYDVR0OBBYEFEjmaPkr0rKV10fYIyAQTzOYkJ/UMAwGA1UdEwQF
+MAMBAf8wGgYJKoZIhvZ9B0EABA0wCxsFVjMuMGMDAgbAMA0GCSqGSIb3DQEBBQUA
+A4GBAFjOKer89961zgK5F7WF0bnj4JXMJTENAKaSbn+2kmOeUJXRmm/kEd5jhW6Y
+7qj/WsjTVbJmcVfewCHrPSqnI0kBBIZCe/zuf6IWUrVnZ9NA2zsmWLIodz2uFHdh
+1voqZiegDfqnc1zqcPGUIWVEX/r87yloqaKHee9570+sB3c4
+-----END CERTIFICATE-----
+"""
+# SHA-1 digest of expected public keys. Any of these is valid. See
+# http://www.imperialviolet.org/2011/05/04/pinning.html for the reason behind
+# hashing the public key, not the entire certificate.
+PIN_GOOGLE_PUBKEY_SHA1 = (
+ # https://src.chromium.org/viewvc/chrome/trunk/src/net/http/transport_security_state_static.h?revision=209003&view=markup
+ # kSPKIHash_Google1024
+ "\x40\xc5\x40\x1d\x6f\x8c\xba\xf0\x8b\x00\xed\xef\xb1\xee\x87\xd0\x05\xb3\xb9\xcd",
+ # kSPKIHash_GoogleG2
+ "\x43\xda\xd6\x30\xee\x53\xf8\xa9\x80\xca\x6e\xfd\x85\xf4\x6a\xa3\x79\x90\xe0\xea",
+)
+
+# Registrations are encrypted with this public key before being emailed. Only
+# the facilitator operators should have the corresponding private key. Given a
+# private key in reg-email, get the public key like this:
+# openssl rsa -pubout < reg-email > reg-email.pub
+DEFAULT_FACILITATOR_PUBKEY_PEM = """\
+-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA44Mt8c599/4N2fgu6ppN
+oatPW1GOgZxxObljFtEy0OWM1eHB35OOn+Kn9MxNHTRxVWwCEi0HYxWNVs2qrXxV
+84LmWBz6A65d2qBlgltgLXusiXLrpwxVmJeO+GfmbF8ur0U9JSYxA20cGW/kujNg
+XYDGQxO1Gvxq2lHK2LQmBpkfKEE1DMFASmIvlHDQgDj3XBb5lYeOsHZmg16UrGAq
+1UH238hgJITPGLXBtwLtJkYbrATJvrEcmvI7QSm57SgYGpaB5ZdCbJL5bag5Pgt6
+M5SDDYYY4xxEPzokjFJfCQv+kcyAnzERNMQ9kR41ePTXG62bpngK5iWGeJ5XdkxG
+gwIDAQAB
+-----END PUBLIC KEY-----
+"""
+
+def check_certificate_pin(sock, cert_pubkey):
+ found = []
+ for cert in sock.get_peer_cert_chain():
+ pubkey_der = cert.get_pubkey().as_der()
+ pubkey_digest = sha1(pubkey_der).digest()
+ if pubkey_digest in cert_pubkey:
+ break
+ found.append(pubkey_digest)
+ else:
+ found = "(" + ", ".join(x.encode("hex") for x in found) + ")"
+ expected = "(" + ", ".join(x.encode("hex") for x in cert_pubkey) + ")"
+ raise ValueError("Public key does not match pin: got %s but expected any of %s" % (found, expected))
+
+class temp_cert(object):
+ """Implements a with-statement over raw certificate data."""
+
+ def __init__(self, certdata):
+ self.fd = tempfile.NamedTemporaryFile(prefix="fp-cert-temp-", suffix=".crt", delete=True)
+ self.fd.write(certdata)
+ self.fd.flush()
+ self.fd.seek(0)
+
+ def __enter__(self):
+ return self.fd
+
+ def __exit__(self, type, value, traceback):
+ self.fd.close()
diff --git a/flashproxy/proc.py b/flashproxy/proc.py
new file mode 100644
index 0000000..4a008b2
--- /dev/null
+++ b/flashproxy/proc.py
@@ -0,0 +1,47 @@
+import errno
+import os
+import socket
+import stat
+import pwd
+
+DEFAULT_CLIENT_TRANSPORT = "websocket"
+
+# Return true iff the given fd is readable, writable, and executable only by its
+# owner.
+def check_perms(fd):
+ mode = os.fstat(fd)[0]
+ return (mode & (stat.S_IRWXG | stat.S_IRWXO)) == 0
+
+# Drop privileges by switching ID to that of the given user.
+# http://stackoverflow.com/questions/2699907/dropping-root-permissions-in-python/2699996#2699996
+# https://www.securecoding.cert.org/confluence/display/seccode/POS36-C.+Observe+correct+revocation+order+while+relinquishing+privileges
+# https://www.securecoding.cert.org/confluence/display/seccode/POS37-C.+Ensure+that+privilege+relinquishment+is+successful
+def drop_privs(username):
+ uid = pwd.getpwnam(username).pw_uid
+ gid = pwd.getpwnam(username).pw_gid
+ os.setgroups([])
+ os.setgid(gid)
+ os.setuid(uid)
+ try:
+ os.setuid(0)
+ except OSError:
+ pass
+ else:
+ raise AssertionError("setuid(0) succeeded after attempting to drop privileges")
+
+# A decorator to ignore "broken pipe" errors.
+def catch_epipe(fn):
+ def ret(self, *args):
+ try:
+ return fn(self, *args)
+ except socket.error, e:
+ try:
+ err_num = e.errno
+ except AttributeError:
+ # Before Python 2.6, exception can be a pair.
+ err_num, errstr = e
+ except:
+ raise
+ if err_num != errno.EPIPE:
+ raise
+ return ret
diff --git a/flashproxy/reg.py b/flashproxy/reg.py
new file mode 100644
index 0000000..0551f06
--- /dev/null
+++ b/flashproxy/reg.py
@@ -0,0 +1,31 @@
+from collections import namedtuple
+
+from flashproxy.util import parse_addr_spec
+
+class Transport(namedtuple("Transport", "inner outer")):
+ @classmethod
+ def parse(cls, transport):
+ if isinstance(transport, cls):
+ return transport
+ elif type(transport) == str:
+ if "|" in transport:
+ inner, outer = transport.rsplit("|", 1)
+ else:
+ inner, outer = "", transport
+ return cls(inner, outer)
+ else:
+ raise ValueError("could not parse transport: %s" % transport)
+
+ def __init__(self, inner, outer):
+ if not outer:
+ raise ValueError("outer (proxy) part of transport must be non-empty: %s" % str(self))
+
+ def __str__(self):
+ return "%s|%s" % (self.inner, self.outer) if self.inner else self.outer
+
+
+class Endpoint(namedtuple("Endpoint", "addr transport")):
+ @classmethod
+ def parse(cls, spec, transport, defhost = None, defport = None):
+ host, port = parse_addr_spec(spec, defhost, defport)
+ return cls((host, port), Transport.parse(transport))
diff --git a/flashproxy/test/__init__.py b/flashproxy/test/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/flashproxy/test/test_fac.py b/flashproxy/test/test_fac.py
new file mode 100644
index 0000000..e7dfa00
--- /dev/null
+++ b/flashproxy/test/test_fac.py
@@ -0,0 +1,93 @@
+#!/usr/bin/env python
+
+import unittest
+
+from flashproxy.fac import parse_transaction, read_client_registrations
+
+class ParseTransactionTest(unittest.TestCase):
+ def test_empty_string(self):
+ self.assertRaises(ValueError, parse_transaction, "")
+
+ def test_correct(self):
+ self.assertEqual(parse_transaction("COMMAND"), ("COMMAND", ()))
+ self.assertEqual(parse_transaction("COMMAND X=\"\""), ("COMMAND", (("X", ""),)))
+ self.assertEqual(parse_transaction("COMMAND X=\"ABC\""), ("COMMAND", (("X", "ABC"),)))
+ self.assertEqual(parse_transaction("COMMAND X=\"\\A\\B\\C\""), ("COMMAND", (("X", "ABC"),)))
+ self.assertEqual(parse_transaction("COMMAND X=\"\\\\\\\"\""), ("COMMAND", (("X", "\\\""),)))
+ self.assertEqual(parse_transaction("COMMAND X=\"ABC\" Y=\"DEF\""), ("COMMAND", (("X", "ABC"), ("Y", "DEF"))))
+ self.assertEqual(parse_transaction("COMMAND KEY-NAME=\"ABC\""), ("COMMAND", (("KEY-NAME", "ABC"),)))
+ self.assertEqual(parse_transaction("COMMAND KEY_NAME=\"ABC\""), ("COMMAND", (("KEY_NAME", "ABC"),)))
+
+ def test_missing_command(self):
+ self.assertRaises(ValueError, parse_transaction, "X=\"ABC\"")
+ self.assertRaises(ValueError, parse_transaction, " X=\"ABC\"")
+
+ def test_missing_space(self):
+ self.assertRaises(ValueError, parse_transaction, "COMMAND/X=\"ABC\"")
+ self.assertRaises(ValueError, parse_transaction, "COMMAND X=\"ABC\"Y=\"DEF\"")
+
+ def test_bad_quotes(self):
+ self.assertRaises(ValueError, parse_transaction, "COMMAND X=\"")
+ self.assertRaises(ValueError, parse_transaction, "COMMAND X=\"ABC")
+ self.assertRaises(ValueError, parse_transaction, "COMMAND X=\"ABC\" Y=\"ABC")
+ self.assertRaises(ValueError, parse_transaction, "COMMAND X=\"ABC\\")
+
+ def test_truncated(self):
+ self.assertRaises(ValueError, parse_transaction, "COMMAND X=")
+
+ def test_newline(self):
+ self.assertRaises(ValueError, parse_transaction, "COMMAND X=\"ABC\" \nY=\"DEF\"")
+
+class ReadClientRegistrationsTest(unittest.TestCase):
+ def testSingle(self):
+ l = list(read_client_registrations(""))
+ self.assertEqual(len(l), 0)
+ l = list(read_client_registrations("client=1.2.3.4:1111"))
+ self.assertEqual(len(l), 1)
+ self.assertEqual(l[0].addr, ("1.2.3.4", 1111))
+ l = list(read_client_registrations("client=1.2.3.4:1111\n"))
+ self.assertEqual(len(l), 1)
+ self.assertEqual(l[0].addr, ("1.2.3.4", 1111))
+ l = list(read_client_registrations("foo=bar&client=1.2.3.4:1111&baz=quux"))
+ self.assertEqual(len(l), 1)
+ self.assertEqual(l[0].addr, ("1.2.3.4", 1111))
+ l = list(read_client_registrations("foo=b%3dar&client=1.2.3.4%3a1111"))
+ self.assertEqual(len(l), 1)
+ self.assertEqual(l[0].addr, ("1.2.3.4", 1111))
+ l = list(read_client_registrations("client=%5b1::2%5d:3333"))
+ self.assertEqual(len(l), 1)
+ self.assertEqual(l[0].addr, ("1::2", 3333))
+
+ def testDefaultAddress(self):
+ l = list(read_client_registrations("client=:1111&transport=websocket", defhost="1.2.3.4"))
+ self.assertEqual(l[0].addr, ("1.2.3.4", 1111))
+ l = list(read_client_registrations("client=1.2.3.4:&transport=websocket", defport=1111))
+ self.assertEqual(l[0].addr, ("1.2.3.4", 1111))
+
+ def testDefaultTransport(self):
+ l = list(read_client_registrations("client=1.2.3.4:1111"))
+ self.assertEqual(l[0].transport, "websocket")
+
+ def testMultiple(self):
+ l = list(read_client_registrations("client=1.2.3.4:1111&foo=bar\nfoo=bar&client=5.6.7.8:2222"))
+ self.assertEqual(len(l), 2)
+ self.assertEqual(l[0].addr, ("1.2.3.4", 1111))
+ self.assertEqual(l[1].addr, ("5.6.7.8", 2222))
+ l = list(read_client_registrations("client=1.2.3.4:1111&foo=bar\nfoo=bar&client=%5b1::2%5d:3333"))
+ self.assertEqual(len(l), 2)
+ self.assertEqual(l[0].addr, ("1.2.3.4", 1111))
+ self.assertEqual(l[1].addr, ("1::2", 3333))
+
+ def testInvalid(self):
+ # Missing "client".
+ with self.assertRaises(ValueError):
+ list(read_client_registrations("foo=bar"))
+ # More than one "client".
+ with self.assertRaises(ValueError):
+ list(read_client_registrations("client=1.2.3.4:1111&foo=bar&client=5.6.7.8:2222"))
+ # Single client with bad syntax.
+ with self.assertRaises(ValueError):
+ list(read_client_registrations("client=1.2.3.4,1111"))
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/flashproxy/test/test_keys.py b/flashproxy/test/test_keys.py
new file mode 100644
index 0000000..15c4449
--- /dev/null
+++ b/flashproxy/test/test_keys.py
@@ -0,0 +1,25 @@
+import os.path
+import unittest
+
+from flashproxy.keys import PIN_GOOGLE_CA_CERT, PIN_GOOGLE_PUBKEY_SHA1, check_certificate_pin, temp_cert
+
+class TempCertTest(unittest.TestCase):
+
+ def test_temp_cert_success(self):
+ fn = None
+ with temp_cert(PIN_GOOGLE_CA_CERT) as ca_file:
+ fn = ca_file.name
+ self.assertTrue(os.path.exists(fn))
+ lines = ca_file.readlines()
+ self.assertIn("-----BEGIN CERTIFICATE-----\n", lines)
+ self.assertFalse(os.path.exists(fn))
+
+ def test_temp_cert_raise(self):
+ fn = None
+ try:
+ with temp_cert(PIN_GOOGLE_CA_CERT) as ca_file:
+ fn = ca_file.name
+ raise ValueError()
+ self.fail()
+ except ValueError:
+ self.assertFalse(os.path.exists(fn))
diff --git a/flashproxy/test/test_reg.py b/flashproxy/test/test_reg.py
new file mode 100644
index 0000000..6b0e196
--- /dev/null
+++ b/flashproxy/test/test_reg.py
@@ -0,0 +1,23 @@
+#!/usr/bin/env python
+
+import unittest
+
+from flashproxy.reg import Transport
+
+class TransportTest(unittest.TestCase):
+
+ def test_transport_parse(self):
+ self.assertEquals(Transport.parse("a"), Transport("", "a"))
+ self.assertEquals(Transport.parse("|a"), Transport("", "a"))
+ self.assertEquals(Transport.parse("a|b|c"), Transport("a|b","c"))
+ self.assertEquals(Transport.parse(Transport("a|b","c")), Transport("a|b","c"))
+ self.assertRaises(ValueError, Transport, "", "")
+ self.assertRaises(ValueError, Transport, "a", "")
+ self.assertRaises(ValueError, Transport.parse, "")
+ self.assertRaises(ValueError, Transport.parse, "|")
+ self.assertRaises(ValueError, Transport.parse, "a|")
+ self.assertRaises(ValueError, Transport.parse, ["a"])
+ self.assertRaises(ValueError, Transport.parse, [Transport("a", "b")])
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/flashproxy/test/test_util.py b/flashproxy/test/test_util.py
new file mode 100644
index 0000000..935dd1f
--- /dev/null
+++ b/flashproxy/test/test_util.py
@@ -0,0 +1,43 @@
+#!/usr/bin/env python
+
+import unittest
+
+from flashproxy.util import parse_addr_spec, canonical_ip
+
+class ParseAddrSpecTest(unittest.TestCase):
+ def test_ipv4(self):
+ self.assertEqual(parse_addr_spec("192.168.0.1:9999"), ("192.168.0.1", 9999))
+
+ def test_ipv6(self):
+ self.assertEqual(parse_addr_spec("[12::34]:9999"), ("12::34", 9999))
+
+ def test_defhost_defport_ipv4(self):
+ self.assertEqual(parse_addr_spec("192.168.0.2:8888", defhost="192.168.0.1", defport=9999), ("192.168.0.2", 8888))
+ self.assertEqual(parse_addr_spec("192.168.0.2:", defhost="192.168.0.1", defport=9999), ("192.168.0.2", 9999))
+ self.assertEqual(parse_addr_spec("192.168.0.2", defhost="192.168.0.1", defport=9999), ("192.168.0.2", 9999))
+ self.assertEqual(parse_addr_spec(":8888", defhost="192.168.0.1", defport=9999), ("192.168.0.1", 8888))
+ self.assertEqual(parse_addr_spec(":", defhost="192.168.0.1", defport=9999), ("192.168.0.1", 9999))
+ self.assertEqual(parse_addr_spec("", defhost="192.168.0.1", defport=9999), ("192.168.0.1", 9999))
+
+ def test_defhost_defport_ipv6(self):
+ self.assertEqual(parse_addr_spec("[1234::2]:8888", defhost="1234::1", defport=9999), ("1234::2", 8888))
+ self.assertEqual(parse_addr_spec("[1234::2]:", defhost="1234::1", defport=9999), ("1234::2", 9999))
+ self.assertEqual(parse_addr_spec("[1234::2]", defhost="1234::1", defport=9999), ("1234::2", 9999))
+ self.assertEqual(parse_addr_spec(":8888", defhost="1234::1", defport=9999), ("1234::1", 8888))
+ self.assertEqual(parse_addr_spec(":", defhost="1234::1", defport=9999), ("1234::1", 9999))
+ self.assertEqual(parse_addr_spec("", defhost="1234::1", defport=9999), ("1234::1", 9999))
+
+ def test_empty_defaults(self):
+ self.assertEqual(parse_addr_spec("192.168.0.2:8888"), ("192.168.0.2", 8888))
+ self.assertEqual(parse_addr_spec("", defhost="", defport=0), ("", 0))
+ self.assertEqual(parse_addr_spec(":8888", defhost=""), ("", 8888))
+ self.assertRaises(ValueError, parse_addr_spec, ":8888")
+ self.assertEqual(parse_addr_spec("192.168.0.2", defport=0), ("192.168.0.2", 0))
+ self.assertRaises(ValueError, parse_addr_spec, "192.168.0.2")
+
+ def test_canonical_ip_noresolve(self):
+ """Test that canonical_ip does not do DNS resolution by default."""
+ self.assertRaises(ValueError, canonical_ip, *parse_addr_spec("example.com:80"))
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/flashproxy/util.py b/flashproxy/util.py
new file mode 100644
index 0000000..a53bdad
--- /dev/null
+++ b/flashproxy/util.py
@@ -0,0 +1,120 @@
+import re
+import socket
+
+def parse_addr_spec(spec, defhost = None, defport = None):
+ """Parse a host:port specification and return a 2-tuple ("host", port) as
+ understood by the Python socket functions.
+
+ >>> parse_addr_spec("192.168.0.1:9999")
+ ('192.168.0.1', 9999)
+
+ If defhost or defport are given and not None, the respective parts of the
+ specification may be omitted, and will be filled in with the defaults.
+ If defhost or defport are omitted or None, the respective parts of the
+ specification must be given, or else a ValueError will be raised.
+
+ >>> parse_addr_spec("192.168.0.2:8888", defhost="192.168.0.1", defport=9999)
+ ('192.168.0.2', 8888)
+ >>> parse_addr_spec(":8888", defhost="192.168.0.1", defport=9999)
+ ('192.168.0.1', 8888)
+ >>> parse_addr_spec("192.168.0.2", defhost="192.168.0.1", defport=9999)
+ ('192.168.0.2', 9999)
+ >>> parse_addr_spec("192.168.0.2:", defhost="192.168.0.1", defport=9999)
+ ('192.168.0.2', 9999)
+ >>> parse_addr_spec(":", defhost="192.168.0.1", defport=9999)
+ ('192.168.0.1', 9999)
+ >>> parse_addr_spec("", defhost="192.168.0.1", defport=9999)
+ ('192.168.0.1', 9999)
+ >>> parse_addr_spec(":")
+ Traceback (most recent call last):
+ [..]
+ ValueError: Bad address specification ":"
+ >>> parse_addr_spec(":", "", 0)
+ ('', 0)
+
+ IPv6 addresses must be enclosed in square brackets."""
+ host = None
+ port = None
+ af = 0
+ m = None
+ # IPv6 syntax.
+ if not m:
+ m = re.match(ur'^\[(.+)\]:(\d*)$', spec)
+ if m:
+ host, port = m.groups()
+ af = socket.AF_INET6
+ if not m:
+ m = re.match(ur'^\[(.+)\]$', spec)
+ if m:
+ host, = m.groups()
+ af = socket.AF_INET6
+ # IPv4/hostname/port-only syntax.
+ if not m:
+ try:
+ host, port = spec.split(":", 1)
+ except ValueError:
+ host = spec
+ if re.match(ur'^[\d.]+$', host):
+ af = socket.AF_INET
+ else:
+ af = 0
+ host = host or defhost
+ port = port or defport
+ if host is None or port is None:
+ raise ValueError("Bad address specification \"%s\"" % spec)
+ return host, int(port)
+
+def resolve_to_ip(host, port, af=0, gai_flags=0):
+ """Resolves a host string to an IP address in canonical format.
+
+ Note: in many cases this is not necessary since the consumer of the address
+ can probably accept host names directly.
+
+ :param: host string to resolve; may be a DNS name or an IP address.
+ :param: port of the host
+ :param: af address family, default unspecified. set to socket.AF_INET or
+ socket.AF_INET6 to force IPv4 or IPv6 name resolution.
+ :returns: (IP address in canonical format, port)
+ """
+ # Forward-resolve the name into an addrinfo struct. Real DNS resolution is
+ # done only if resolve is true; otherwise the address must be numeric.
+ try:
+ addrs = socket.getaddrinfo(host, port, af, 0, 0, gai_flags)
+ except socket.gaierror, e:
+ raise ValueError("Bad host or port: \"%s\" \"%s\": %s" % (host, port, str(e)))
+ if not addrs:
+ raise ValueError("Bad host or port: \"%s\" \"%s\"" % (host, port))
+
+ # Convert the result of socket.getaddrinfo (which is a 2-tuple for IPv4 and
+ # a 4-tuple for IPv6) into a (host, port) 2-tuple.
+ host, port = socket.getnameinfo(addrs[0][4], socket.NI_NUMERICHOST | socket.NI_NUMERICSERV)
+ return host, int(port)
+
+def canonical_ip(host, port, af=0):
+ """Convert an IP address to a canonical format. Identical to resolve_to_ip,
+ except that the host param must already be an IP address."""
+ return resolve_to_ip(host, port, af, gai_flags=socket.AI_NUMERICHOST)
+
+def format_addr(addr):
+ host, port = addr
+ host_str = u""
+ port_str = u""
+ if host is not None:
+ # Numeric IPv6 address?
+ try:
+ addrs = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM, socket.IPPROTO_TCP, socket.AI_NUMERICHOST)
+ af = addrs[0][0]
+ except socket.gaierror, e:
+ af = 0
+ if af == socket.AF_INET6:
+ host_str = u"[%s]" % host
+ else:
+ host_str = u"%s" % host
+ if port is not None:
+ if not (0 < port <= 65535):
+ raise ValueError("port must be between 1 and 65535 (is %d)" % port)
+ port_str = u":%d" % port
+
+ if not host_str and not port_str:
+ raise ValueError("host and port may not both be None")
+ return u"%s%s" % (host_str, port_str)
diff --git a/mkman.inc b/mkman.inc
new file mode 100644
index 0000000..6207f0b
--- /dev/null
+++ b/mkman.inc
@@ -0,0 +1,9 @@
+[REPORTING BUGS]
+.sp
+Please report using \fBhttps://trac\&.torproject\&.org/projects/tor\fR\&.
+
+[SEE ALSO]
+.sp
+\fBhttp://crypto\&.stanford\&.edu/flashproxy/\fR
+.sp
+\fBhttps://www\&.torproject\&.org/docs/pluggable\-transports\&.html\&.en\fR
diff --git a/mkman.sh b/mkman.sh
new file mode 100755
index 0000000..1087e41
--- /dev/null
+++ b/mkman.sh
@@ -0,0 +1,55 @@
+#!/bin/bash
+# Wrapper around help2man that takes input from stdin.
+
+set -o errexit
+
+# Read a python program's description from the first paragraph of its docstring.
+get_description() {
+ PYTHONPATH=".:$PYTHONPATH" python - "./$1" <<EOF
+import imp, sys
+sys.dont_write_bytecode = True
+mod = imp.load_source("mod", sys.argv[1])
+doclines = mod.__doc__.splitlines()
+# skip to start of first paragraph
+while not doclines[0]:
+ doclines.pop(0)
+# find where the paragraph ends
+try:
+ r = doclines.index("")
+except ValueError:
+ r = len(doclines)
+print " ".join(doclines[:r]).strip()
+EOF
+}
+
+# Fixes some help2man quirks, see `man man`
+help2man_fixup() {
+ sed -re '
+# restricted to usage synopsis section
+/^\.SH SYNOPSIS$/,/^\.SH \w+$/{
+ # change hypenated parameters to bold, "type exactly as shown"
+ s/\\fI\-/\\fB\-/g;
+ # change ALL-CAPS parameters to italic, "replace with appropriate argument"
+ s/\b([A-Z]+)\b/\\fI\1\\fR/g;
+}'
+}
+
+prog="$1"
+ver="$2"
+name="${3:-$(get_description "$1")}"
+
+# Prepare a temporary executable file that just dumps its own contents.
+trap 'rm -rf .tmp.$$' EXIT INT TERM
+shebang="#!/usr/bin/tail -n+2"
+mkdir -p ".tmp.$$"
+{
+echo "$shebang"
+cat
+} > ".tmp.$$/$prog"
+test $(stat -c "%s" ".tmp.$$/$prog") -gt $((${#shebang} + 1)) || { echo >&2 "no input received; abort"; exit 1; }
+chmod +x ".tmp.$$/$prog"
+
+help2man ".tmp.$$/$prog" --help-option="-q" \
+ --name="$name" --version-string="$ver" \
+ --no-info --include "$(dirname "$0")/mkman.inc" \
+ | help2man_fixup
diff --git a/proxy/Makefile b/proxy/Makefile
new file mode 100644
index 0000000..8f3c9c7
--- /dev/null
+++ b/proxy/Makefile
@@ -0,0 +1,11 @@
+LANGS = de en pt ru
+
+all: $(addprefix badge-, $(addsuffix .png, $(LANGS)))
+
+test:
+ ./flashproxy-test.js
+
+badge-%.png: badge.xcf
+ (cat badge-export-lang.scm; echo '(export "$*") (gimp-quit 0)') | gimp -i -b -
+
+.PHONY: all test
diff --git a/proxy/README b/proxy/README
new file mode 100644
index 0000000..18de1c8
--- /dev/null
+++ b/proxy/README
@@ -0,0 +1,13 @@
+The proxy directory contains the flash proxy JavaScript proxy program and
+associated HTML and media files. End users don't have to do anything
+with these files. They are meant to be installed on a centralized web
+server and then accessed through a browser.
+
+The modules subdirectory contains modules and plugins for making flash
+proxies work with other systems such as web publishing platforms.
+
+See a collection of modules for other platforms at
+https://github.com/glamrock/cupcake.
+
+For a plugin for Mozilla Firefox, see
+https://addons.mozilla.org/en-us/firefox/addon/tor-flashproxy-badge/.
diff --git a/proxy/badge-de.png b/proxy/badge-de.png
new file mode 100644
index 0000000..f00fec6
Binary files /dev/null and b/proxy/badge-de.png differ
diff --git a/proxy/badge-en.png b/proxy/badge-en.png
new file mode 100644
index 0000000..068cf78
Binary files /dev/null and b/proxy/badge-en.png differ
diff --git a/proxy/badge-export-lang.scm b/proxy/badge-export-lang.scm
new file mode 100644
index 0000000..51f78f1
--- /dev/null
+++ b/proxy/badge-export-lang.scm
@@ -0,0 +1,21 @@
+; This is a Gimp script-fu script that selects and exports the appropriate
+; language layers from an input XCF containing multiple layers.
+
+(define xcf-filename "badge.xcf")
+
+(define (export lang)
+ (let* ((image (car (gimp-file-load RUN-NONINTERACTIVE xcf-filename xcf-filename)))
+ (shine-layer (car (gimp-image-get-layer-by-name image "shine")))
+ (text-layer (car (gimp-image-get-layer-by-name image (string-append "text-" lang))))
+ (output-filename (string-append "badge-" lang ".png")))
+ ; Turn off all layers.
+ (for-each (lambda (x) (gimp-item-set-visible x FALSE))
+ (vector->list (cadr (gimp-image-get-layers image))))
+ ; Except the shine and the wanted text.
+ (gimp-item-set-visible shine-layer TRUE)
+ (gimp-item-set-visible text-layer TRUE)
+ (gimp-image-merge-visible-layers image CLIP-TO-IMAGE)
+ (file-png-save RUN-NONINTERACTIVE image
+ (car (gimp-image-get-active-layer image))
+ output-filename output-filename FALSE 9 FALSE FALSE FALSE FALSE FALSE)
+ ))
diff --git a/proxy/badge-pt.png b/proxy/badge-pt.png
new file mode 100644
index 0000000..84dc454
Binary files /dev/null and b/proxy/badge-pt.png differ
diff --git a/proxy/badge-ru.png b/proxy/badge-ru.png
new file mode 100644
index 0000000..9bfc626
Binary files /dev/null and b/proxy/badge-ru.png differ
diff --git a/proxy/badge.png b/proxy/badge.png
new file mode 120000
index 0000000..fdd103c
--- /dev/null
+++ b/proxy/badge.png
@@ -0,0 +1 @@
+badge-en.png
\ No newline at end of file
diff --git a/proxy/badge.xcf b/proxy/badge.xcf
new file mode 100644
index 0000000..cc3af06
Binary files /dev/null and b/proxy/badge.xcf differ
diff --git a/proxy/embed.html b/proxy/embed.html
new file mode 100644
index 0000000..178b80a
--- /dev/null
+++ b/proxy/embed.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<meta http-equiv="refresh" content="86400">
+<script type="text/javascript" >
+/*
+ You can tweak the behaviour of the proxy by giving URL query parameters to
+ this embedding document. If you wish to override the default values even in
+ the absence of those parameters, you may set them below.
+
+ See the main code of the proxy for details on which URL query parameters
+ are available, as well as which defaults you can override.
+*/
+//var DEFAULT_FACILITATOR_URL = "https://fp-facilitator.org/";
+</script>
+<style type="text/css">
+html {
+ width: 100%;
+ height: 100%;
+}
+body {
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ height: 100%;
+}
+#flashproxy-badge {
+ width: 100%;
+ height: 100%;
+ margin: 0;
+ padding: 0;
+ border: 0;
+ border-collapse: collapse;
+ line-height: 0;
+}
+#flashproxy-badge.idle {
+ background-color: #227;
+}
+#flashproxy-badge.active {
+ background-color: #28f;
+}
+#flashproxy-badge.disabled {
+ background-color: #777;
+}
+#flashproxy-badge.dead {
+ background-color: #111;
+}
+#flashproxy-badge td {
+ margin: 0;
+ padding: 0;
+ vertical-align: middle;
+ text-align: center;
+}
+#flashproxy-badge a img {
+ border: 0;
+}
+#flashproxy-badge.debug {
+ position: absolute;
+ margin: 0;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ overflow: auto;
+ color: #4c4;
+ background-color: #021;
+ line-height: inherit;
+}
+</style>
+</head>
+<body>
+<script type="text/javascript" src="flashproxy.js"></script>
+<script type="text/javascript">
+flashproxy_badge_insert().start();
+</script>
+<noscript>
+<table id="flashproxy-badge" class="disabled"><tr><td><a href="options.html" target="_blank"><img src="badge.png" alt="Internet freedom"></a></td></tr></table>
+</noscript>
+</body>
+</html>
diff --git a/proxy/flashproxy-test.js b/proxy/flashproxy-test.js
new file mode 100755
index 0000000..dabbb8b
--- /dev/null
+++ b/proxy/flashproxy-test.js
@@ -0,0 +1,359 @@
+#!/usr/bin/env rhino
+
+/* To run this test program, install the Rhino JavaScript interpreter
+ (apt-get install rhino). */
+
+var VERBOSE = false;
+if ("-v" in arguments)
+ VERBOSE = true;
+
+var num_tests = 0;
+var num_failed = 0;
+
+var window = {location: {search: "?"}};
+var document = {cookie: ""};
+
+load("flashproxy.js");
+
+function objects_equal(a, b) {
+ if ((a === null) != (b === null))
+ return false;
+ if (typeof a != typeof b)
+ return false;
+ if (typeof a != "object")
+ return a == b;
+
+ for (var k in a) {
+ if (!objects_equal(a[k], b[k]))
+ return false;
+ }
+ for (var k in b) {
+ if (!objects_equal(a[k], b[k]))
+ return false;
+ }
+
+ return true;
+}
+
+var top = true;
+function announce(test_name) {
+ if (VERBOSE) {
+ if (!top)
+ print();
+ print(test_name);
+ }
+ top = false;
+}
+
+function pass(test) {
+ num_tests++;
+ if (VERBOSE)
+ print("PASS " + repr(test));
+}
+
+function fail(test, expected, actual) {
+ num_tests++;
+ num_failed++;
+ print("FAIL " + repr(test) + " expected: " + repr(expected) + " actual: " + repr(actual));
+}
+
+function test_build_url() {
+ var TESTS = [
+ { args: ["http", "example.com"],
+ expected: "http://example.com" },
+ { args: ["http", "example.com", 80],
+ expected: "http://example.com" },
+ { args: ["http", "example.com", 81],
+ expected: "http://example.com:81" },
+ { args: ["https", "example.com", 443],
+ expected: "https://example.com" },
+ { args: ["https", "example.com", 444],
+ expected: "https://example.com:444" },
+ { args: ["http", "example.com", 80, "/"],
+ expected: "http://example.com/" },
+ { args: ["http", "example.com", 80, "/test?k=%#v"],
+ expected: "http://example.com/test%3Fk%3D%25%23v" },
+ { args: ["http", "example.com", 80, "/test", []],
+ expected: "http://example.com/test?" },
+ { args: ["http", "example.com", 80, "/test", [["k", "%#v"]]],
+ expected: "http://example.com/test?k=%25%23v" },
+ { args: ["http", "example.com", 80, "/test", [["a", "b"], ["c", "d"]]],
+ expected: "http://example.com/test?a=b&c=d" },
+ { args: ["http", "1.2.3.4"],
+ expected: "http://1.2.3.4" },
+ { args: ["http", "1:2::3:4"],
+ expected: "http://[1:2::3:4]" },
+ { args: ["http", "bog][us"],
+ expected: "http://bog%5D%5Bus" },
+ { args: ["http", "bog:u]s"],
+ expected: "http://bog%3Au%5Ds" },
+ ];
+
+ announce("test_build_url");
+ for (var i = 0; i < TESTS.length; i++) {
+ var test = TESTS[i];
+ var actual;
+
+ actual = build_url.apply(undefined, test.args);
+ if (objects_equal(actual, test.expected))
+ pass(test.args);
+ else
+ fail(test.args, test.expected, actual);
+ }
+}
+
+/* This test only checks that things work for strings
+ formatted like document.cookie. Browsers maintain
+ several properties about this string, for example
+ cookie names are unique with no trailing whitespace.
+ See http://www.ietf.org/rfc/rfc2965.txt for the grammar. */
+function test_parse_cookie_string() {
+ var TESTS = [
+ { cs: "",
+ expected: { } },
+ { cs: "a=b",
+ expected: { a: "b"} },
+ { cs: "a=b=c",
+ expected: { a: "b=c"} },
+ { cs: "a=b; c=d",
+ expected: { a: "b", c: "d" } },
+ { cs: "a=b ; c=d",
+ expected: { a: "b", c: "d" } },
+ { cs: "a= b",
+ expected: {a: "b" } },
+ { cs: "a=",
+ expected: { a: "" } },
+ { cs: "key",
+ expected: null },
+ { cs: "key=%26%20",
+ expected: { key: "& " } },
+ { cs: "a=\"\"",
+ expected: { a: "\"\"" } },
+ ];
+
+ announce("test_parse_cookie_string");
+ for (var i = 0; i < TESTS.length; i++) {
+ var test = TESTS[i];
+ var actual;
+
+ actual = parse_cookie_string(test.cs);
+ if (objects_equal(actual, test.expected))
+ pass(test.cs);
+ else
+ fail(test.cs, test.expected, actual);
+ }
+}
+
+function test_parse_query_string() {
+ var TESTS = [
+ { qs: "",
+ expected: { } },
+ { qs: "a=b",
+ expected: { a: "b" } },
+ { qs: "a=b=c",
+ expected: { a: "b=c" } },
+ { qs: "a=b&c=d",
+ expected: { a: "b", c: "d" } },
+ { qs: "client=&relay=1.2.3.4%3A9001",
+ expected: { client: "", relay: "1.2.3.4:9001" } },
+ { qs: "a=b%26c=d",
+ expected: { a: "b&c=d" } },
+ { qs: "a%3db=d",
+ expected: { "a=b": "d" } },
+ { qs: "a=b+c%20d",
+ expected: { "a": "b c d" } },
+ { qs: "a=b+c%2bd",
+ expected: { "a": "b c+d" } },
+ { qs: "a+b=c",
+ expected: { "a b": "c" } },
+ { qs: "a=b+c+d",
+ expected: { a: "b c d" } },
+ /* First appearance wins. */
+ { qs: "a=b&c=d&a=e",
+ expected: { a: "b", c: "d" } },
+ { qs: "a",
+ expected: { a: "" } },
+ { qs: "=b",
+ expected: { "": "b" } },
+ { qs: "&a=b",
+ expected: { "": "", a: "b" } },
+ { qs: "a=b&",
+ expected: { "": "", a: "b" } },
+ { qs: "a=b&&c=d",
+ expected: { "": "", a: "b", c: "d" } },
+ ];
+
+ announce("test_parse_query_string");
+ for (var i = 0; i < TESTS.length; i++) {
+ var test = TESTS[i];
+ var actual;
+
+ actual = parse_query_string(test.qs);
+ if (objects_equal(actual, test.expected))
+ pass(test.qs);
+ else
+ fail(test.qs, test.expected, actual);
+ }
+}
+
+function test_get_param_boolean() {
+ var TESTS = [
+ { qs: "param=true",
+ expected: true },
+ { qs: "param",
+ expected: true },
+ { qs: "param=",
+ expected: true },
+ { qs: "param=1",
+ expected: true },
+ { qs: "param=0",
+ expected: false },
+ { qs: "param=false",
+ expected: false },
+ { qs: "param=unexpected",
+ expected: null },
+ { qs: "pram=true",
+ expected: false },
+ ];
+
+ announce("test_get_param_boolean");
+ for (var i = 0; i < TESTS.length; i++) {
+ var test = TESTS[i];
+ var actual;
+ var query;
+
+ query = parse_query_string(test.qs);
+ actual = get_param_boolean(query, "param", false);
+ if (objects_equal(actual, test.expected))
+ pass(test.qs);
+ else
+ fail(test.qs, test.expected, actual);
+ }
+}
+
+function test_parse_addr_spec() {
+ var TESTS = [
+ { spec: "",
+ expected: null },
+ { spec: "3.3.3.3:4444",
+ expected: { host: "3.3.3.3", port: 4444 } },
+ { spec: "3.3.3.3",
+ expected: null },
+ { spec: "3.3.3.3:0x1111",
+ expected: null },
+ { spec: "3.3.3.3:-4444",
+ expected: null },
+ { spec: "3.3.3.3:65536",
+ expected: null },
+ { spec: "[1:2::a:f]:4444",
+ expected: { host: "1:2::a:f", port: 4444 } },
+ { spec: "[1:2::a:f]",
+ expected: null },
+ { spec: "[1:2::a:f]:0x1111",
+ expected: null },
+ { spec: "[1:2::a:f]:-4444",
+ expected: null },
+ { spec: "[1:2::a:f]:65536",
+ expected: null },
+ { spec: "[1:2::ffff:1.2.3.4]:4444",
+ expected: { host: "1:2::ffff:1.2.3.4", port: 4444 } },
+ ];
+
+ announce("test_parse_addr_spec");
+ for (var i = 0; i < TESTS.length; i++) {
+ var test = TESTS[i];
+ var actual;
+
+ actual = parse_addr_spec(test.spec);
+ if (objects_equal(actual, test.expected))
+ pass(test.spec);
+ else
+ fail(test.spec, test.expected, actual);
+ }
+}
+
+function test_get_param_addr() {
+ var DEFAULT = { host: "1.1.1.1", port: 2222 };
+ var TESTS = [
+ { query: { },
+ expected: DEFAULT },
+ { query: { addr: "3.3.3.3:4444" },
+ expected: { host: "3.3.3.3", port: 4444 } },
+ { query: { x: "3.3.3.3:4444" },
+ expected: DEFAULT },
+ { query: { addr: "---" },
+ expected: null },
+ ];
+
+ announce("test_get_param_addr");
+ for (var i = 0; i < TESTS.length; i++) {
+ var test = TESTS[i];
+ var actual;
+
+ actual = get_param_addr(test.query, "addr", DEFAULT);
+ if (objects_equal(actual, test.expected))
+ pass(test.query);
+ else
+ fail(test.query, test.expected, actual);
+ }
+}
+
+function test_lang_keys() {
+ var TESTS = [
+ { code: "de", expected: ["de"] },
+ { code: "DE", expected: ["de"] },
+ { code: "de-at", expected: ["de-at", "de"] },
+ { code: "de-AT", expected: ["de-at", "de"] },
+ ];
+ for (var i = 0; i < TESTS.length; i++) {
+ var test = TESTS[i];
+ var actual = lang_keys(test.code);
+
+ var j, k;
+ k = 0;
+ for (j = 0; j < test.expected.length; j++) {
+ for (; k < actual.length; k++) {
+ if (test.expected[j] === actual[k])
+ break;
+ }
+ if (k === actual.length)
+ fail(test.code, test.expected, actual)
+ }
+ }
+}
+
+function test_have_websocket_binary_frames() {
+ var TESTS = [
+ { ua: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:10.0.2) Gecko/20100101 Firefox/10.0.2", expected: false },
+ { ua: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:11.0) Gecko/20100101 Firefox/11.0", expected: true },
+ { ua: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.110 Safari/537.36", expected: true },
+ { ua: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_4) AppleWebKit/536.30.1 (KHTML, like Gecko) Version/6.0.5 Safari/536.30.1", expected: true },
+ { expected: false }, // no userAgent
+ ];
+ var _navigator = window.navigator;
+ for (var i = 0; i < TESTS.length; i++) {
+ var test = TESTS[i];
+ window.navigator = { userAgent: test.ua };
+ var actual = have_websocket_binary_frames();
+
+ if (objects_equal(actual, test.expected))
+ pass(test.ua);
+ else
+ fail(test.ua, test.expected, actual);
+ }
+ window.navigator = _navigator;
+}
+
+test_build_url();
+test_parse_cookie_string();
+test_parse_query_string();
+test_get_param_boolean();
+test_parse_addr_spec();
+test_get_param_addr();
+test_lang_keys();
+test_have_websocket_binary_frames();
+
+if (num_failed == 0)
+ quit(0);
+else
+ quit(1);
diff --git a/proxy/flashproxy.js b/proxy/flashproxy.js
new file mode 100644
index 0000000..133266a
--- /dev/null
+++ b/proxy/flashproxy.js
@@ -0,0 +1,1129 @@
+/* Query string parameters. These change how the program runs from the outside.
+ * For example:
+ * http://www.example.com/embed.html?facilitator=http://127.0.0.1:9002&debug=1
+ *
+ * cookierequired=0|1
+ * If true, the proxy will disable itself if the user has not explicitly opted
+ * in by setting a cookie through the options page. If absent or false, the proxy
+ * will run unless the user has explicitly opted out.
+ *
+ * lang=<CODE>
+ * Display language of the badge, as an IETF language tag.
+ *
+ * facilitator_poll_interval=<FLOAT>
+ * How often to poll the facilitator, in seconds. The default is
+ * DEFAULT_FACILITATOR_POLL_INTERVAL. There is a sanity-check minimum of 1.0 s.
+ *
+ * initial_facilitator_poll_interval=<FLOAT>
+ * How long to wait before polling the facilitator the first time, in seconds.
+ * DEFAULT_INITIAL_FACILITATOR_POLL_INTERVAL.
+ *
+ * max_clients=<NUM>
+ * How many clients to serve concurrently. The default is
+ * DEFAULT_MAX_NUM_PROXY_PAIRS.
+ *
+ * ratelimit=<FLOAT>(<UNIT>)?|off
+ * What rate to limit all proxy traffic combined to. The special value "off"
+ * disables the limit. The default is DEFAULT_RATE_LIMIT. There is a
+ * sanity-check minimum of "10K".
+ *
+ * facilitator=https://host:port/
+ * The URL of the facilitator CGI script. By default it is
+ * DEFAULT_FACILITATOR_URL.
+ *
+ * debug=0|1
+ * If true, show verbose terminal-like output instead of the badge. The values
+ * "1", "true", and the empty string "" all enable debug mode. Any other value
+ * uses the normal badge display.
+ *
+ * client=<HOST>:<PORT>
+ * The address of the client to connect to. The proxy normally receives this
+ * information from the facilitator. When this option is used, the facilitator
+ * query is not done. The "relay" parameter must be given as well.
+ *
+ * relay=<HOST>:<PORT>
+ * The address of the relay to connect to. The proxy normally receives this
+ * information from the facilitator. When this option is used, the facilitator
+ * query is not done. The "client" parameter must be given as well.
+ */
+
+/* WebSocket links.
+ *
+ * The WebSocket Protocol
+ * https://tools.ietf.org/html/rfc6455
+ *
+ * The WebSocket API
+ * http://dev.w3.org/html5/websockets/
+ *
+ * MDN page with browser compatibility
+ * https://developer.mozilla.org/en/WebSockets
+ *
+ * Implementation tests (including tests of binary messages)
+ * http://autobahn.ws/testsuite/reports/clients/index.html
+ */
+
+var DEFAULT_FACILITATOR_URL = DEFAULT_FACILITATOR_URL || "https://fp-facilitator.org/";
+
+var DEFAULT_MAX_NUM_PROXY_PAIRS = DEFAULT_MAX_NUM_PROXY_PAIRS || 10;
+
+var DEFAULT_INITIAL_FACILITATOR_POLL_INTERVAL = DEFAULT_INITIAL_FACILITATOR_POLL_INTERVAL || 60.0;
+var DEFAULT_FACILITATOR_POLL_INTERVAL = DEFAULT_FACILITATOR_POLL_INTERVAL || 3600.0;
+var MIN_FACILITATOR_POLL_INTERVAL = 10.0;
+
+/* Bytes per second. Set to undefined to disable limit. */
+var DEFAULT_RATE_LIMIT = DEFAULT_RATE_LIMIT || undefined;
+var MIN_RATE_LIMIT = 10 * 1024;
+var RATE_LIMIT_HISTORY = 5.0;
+
+/* Name of cookie that controls opt-in/opt-out. */
+var OPT_IN_COOKIE = "flashproxy-allow";
+
+/* Firefox before version 11.0 uses the name MozWebSocket. Whether the global
+ variable WebSocket is defined indicates whether WebSocket is supported at
+ all. */
+var WebSocket = window.WebSocket || window.MozWebSocket;
+
+var query = parse_query_string(window.location.search.substr(1));
+var DEBUG = get_param_boolean(query, "debug", false);
+var SAFE_LOGGING = !get_param_boolean(query, "unsafe_logging", false);
+var debug_div;
+/* HEADLESS is true if we are running not in a browser with a DOM. */
+var HEADLESS = typeof(document) === "undefined";
+
+var cookies;
+if (HEADLESS) {
+ cookies = {};
+} else {
+ cookies = parse_cookie_string(document.cookie);
+ if (DEBUG) {
+ debug_div = document.createElement("pre");
+ debug_div.className = "debug";
+ }
+}
+
+function puts(s) {
+ if (DEBUG) {
+ /* This shows up in the Web Console in Firefox and F12 developer tools
+ in Internet Explorer. */
+ (console.debug || console.log).call(console, s);
+
+ if (debug_div) {
+ var at_bottom;
+
+ /* http://www.w3.org/TR/cssom-view/#element-scrolling-members */
+ at_bottom = (debug_div.scrollTop + debug_div.clientHeight === debug_div.scrollHeight);
+ debug_div.appendChild(document.createTextNode(s + "\n"));
+ if (at_bottom)
+ debug_div.scrollTop = debug_div.scrollHeight;
+ }
+ }
+}
+
+/* Parse a cookie data string (usually document.cookie). The return type
+ is an object mapping cookies names to values. Returns null on error.
+
+ http://www.w3.org/TR/DOM-Level-2-HTML/html.html#ID-8747038 */
+function parse_cookie_string(cookies) {
+ var strings;
+ var result;
+
+ result = {};
+ if (cookies)
+ strings = cookies.split(";");
+ else
+ strings = [];
+ for (var i = 0; i < strings.length; i++) {
+ var string = strings[i];
+ var j, name, value;
+
+ j = string.indexOf("=");
+ if (j === -1) {
+ return null;
+ }
+ name = decodeURIComponent(string.substr(0, j).trim());
+ value = decodeURIComponent(string.substr(j + 1).trim());
+
+ if (!(name in result))
+ result[name] = value;
+ }
+
+ return result;
+}
+
+/* Parse a URL query string or application/x-www-form-urlencoded body. The
+ return type is an object mapping string keys to string values. By design,
+ this function doesn't support multiple values for the same named parameter,
+ for example "a=1&a=2&a=3"; the first definition always wins. Returns null on
+ error.
+
+ Always decodes from UTF-8, not any other encoding.
+
+ http://dev.w3.org/html5/spec/Overview.html#url-encoded-form-data */
+function parse_query_string(qs) {
+ var strings;
+ var result;
+
+ result = {};
+ if (qs)
+ strings = qs.split("&");
+ else
+ strings = [];
+ for (var i = 0; i < strings.length; i++) {
+ var string = strings[i];
+ var j, name, value;
+
+ j = string.indexOf("=");
+ if (j === -1) {
+ name = string;
+ value = "";
+ } else {
+ name = string.substr(0, j);
+ value = string.substr(j + 1);
+ }
+ name = decodeURIComponent(name.replace(/\+/g, " "));
+ value = decodeURIComponent(value.replace(/\+/g, " "));
+ if (!(name in result))
+ result[name] = value;
+ }
+
+ return result;
+}
+
+/* params is a list of (key, value) 2-tuples. */
+function build_query_string(params) {
+ var parts = [];
+ for (var i = 0; i < params.length; i++) {
+ parts.push(encodeURIComponent(params[i][0]) + "=" + encodeURIComponent(params[i][1]));
+ }
+ return parts.join("&");
+}
+
+var DEFAULT_PORTS = {
+ http: 80,
+ https: 443
+}
+/* Build an escaped URL string from unescaped components. Only scheme and host
+ are required. See RFC 3986, section 3. */
+function build_url(scheme, host, port, path, params) {
+ var parts = []
+
+ parts.push(encodeURIComponent(scheme));
+ parts.push("://");
+
+ /* If it contains a colon but no square brackets, treat it like an IPv6
+ address. */
+ if (host.match(/:/) && !host.match(/[[\]]/)) {
+ parts.push("[");
+ parts.push(host);
+ parts.push("]");
+ } else {
+ parts.push(encodeURIComponent(host));
+ }
+ if (port !== undefined && port !== DEFAULT_PORTS[scheme]) {
+ parts.push(":");
+ parts.push(encodeURIComponent(port.toString()));
+ }
+
+ if (path !== undefined && path !== "") {
+ if (!path.match(/^\//))
+ path = "/" + path;
+ /* Slash is significant so we must protect it from encodeURIComponent,
+ while still encoding question mark and number sign. RFC 3986, section
+ 3.3: "The path is terminated by the first question mark ('?') or
+ number sign ('#') character, or by the end of the URI. ... A path
+ consists of a sequence of path segments separated by a slash ('/')
+ character." */
+ path = path.replace(/[^\/]+/, function(m) {
+ return encodeURIComponent(m);
+ });
+ parts.push(path);
+ }
+
+ if (params !== undefined) {
+ parts.push("?");
+ parts.push(build_query_string(params));
+ }
+
+ return parts.join("");
+}
+
+/* Get an object value and return it as a boolean. True values are "1", "true",
+ and "". False values are "0" and "false". Any other value causes the function
+ to return null (effectively false). Returns default_val if param is not a
+ key.
+
+ The empty string is true so that URLs like http://example.com/?debug will
+ enable debug mode. */
+function get_param_boolean(query, param, default_val) {
+ var val;
+
+ val = query[param];
+ if (val === undefined)
+ return default_val;
+ else if (val === "true" || val === "1" || val === "")
+ return true;
+ else if (val === "false" || val === "0")
+ return false;
+ else
+ return null;
+}
+
+/* Get an object value and return it as a string. Returns default_val if param
+ is not a key. */
+function get_param_string(query, param, default_val) {
+ var val;
+
+ val = query[param];
+ if (val === undefined)
+ return default_val;
+ else
+ return val;
+}
+
+/* Get an object value and parse it as an address spec. Returns default_val if
+ param is not a key. Returns null on a parsing error. */
+function get_param_addr(query, param, default_val) {
+ var val;
+
+ val = query[param];
+ if (val === undefined)
+ return default_val;
+ else
+ return parse_addr_spec(val);
+}
+
+/* Get an object value and parse it as an integer. Returns default_val if param
+ is not a key. Return null on a parsing error. */
+function get_param_integer(query, param, default_val) {
+ var spec;
+ var val;
+
+ spec = query[param];
+ if (spec === undefined) {
+ return default_val;
+ } else if (!spec.match(/^-?[0-9]+/)) {
+ return null;
+ } else {
+ val = parseInt(spec, 10);
+ if (isNaN(val))
+ return null;
+ else
+ return val;
+ }
+}
+
+/* Get an object value and parse it as a real number. Returns default_val if
+ param is not a key. Return null on a parsing error. */
+function get_param_number(query, param, default_val) {
+ var spec;
+ var val;
+
+ spec = query[param];
+ if (spec === undefined) {
+ return default_val;
+ } else {
+ val = Number(spec);
+ if (isNaN(val))
+ return null;
+ else
+ return val;
+ }
+}
+
+/* Get a floating-point number of seconds from a time specification. The only
+ time specification format is a decimal number of seconds. Returns null on
+ error. */
+function get_param_timespec(query, param, default_val) {
+ return get_param_number(query, param, default_val);
+}
+
+/* Parse a count of bytes. A suffix of "k", "m", or "g" (or uppercase)
+ does what you would think. Returns null on error. */
+function parse_byte_count(spec) {
+ var UNITS = {
+ k: 1024, m: 1024 * 1024, g: 1024 * 1024 * 1024,
+ K: 1024, M: 1024 * 1024, G: 1024 * 1024 * 1024
+ };
+ var count, units;
+ var matches;
+
+ matches = spec.match(/^(\d+(?:\.\d*)?)(\w*)$/);
+ if (matches === null)
+ return null;
+
+ count = Number(matches[1]);
+ if (isNaN(count))
+ return null;
+
+ if (matches[2] === "") {
+ units = 1;
+ } else {
+ units = UNITS[matches[2]];
+ if (units === null)
+ return null;
+ }
+
+ return count * Number(units);
+}
+
+/* Get an object value and parse it as a byte count. Example byte counts are
+ "100" and "1.3m". Returns default_val if param is not a key. Return null on a
+ parsing error. */
+function get_param_byte_count(query, param, default_val) {
+ var spec;
+
+ spec = query[param];
+ if (spec === undefined)
+ return default_val;
+ else
+ return parse_byte_count(spec);
+}
+
+/* Return an array of the user's preferred IETF language tags, in descending order
+ of priority. Return an empty array in case of no preference. */
+function get_langs() {
+ var param, result;
+
+ result = [];
+ param = get_param_string(query, "lang");
+ if (param !== undefined)
+ result.push(param);
+
+ /* https://developer.mozilla.org/en/docs/DOM/window.navigator.language */
+ if (window.navigator.language)
+ result.push(window.navigator.language);
+
+ return result;
+}
+
+/* Parse an address in the form "host:port". Returns an Object with
+ keys "host" (String) and "port" (int). Returns null on error. */
+function parse_addr_spec(spec) {
+ var m, host, port;
+
+ m = null;
+ /* IPv6 syntax. */
+ if (!m)
+ m = spec.match(/^\[([\0-9a-fA-F:.]+)\]:([0-9]+)$/);
+ /* IPv4 syntax. */
+ if (!m)
+ m = spec.match(/^([0-9.]+):([0-9]+)$/);
+ if (!m)
+ return null;
+ host = m[1];
+ port = parseInt(m[2], 10);
+ if (isNaN(port) || port < 0 || port > 65535)
+ return null;
+
+ return { host: host, port: port }
+}
+
+function format_addr(addr) {
+ return addr.host + ":" + addr.port;
+}
+
+/* Does the WebSocket implementation in this browser support binary frames? (RFC
+ 6455 section 5.6.) If not, we have to use base64-encoded text frames. It is
+ assumed that the client and relay endpoints always support binary frames. */
+function have_websocket_binary_frames() {
+ var BROWSERS = [
+ { idString: "Chrome", verString: "Chrome", version: 16 },
+ { idString: "Safari", verString: "Version", version: 6 },
+ { idString: "Firefox", verString: "Firefox", version: 11 }
+ ];
+ var ua;
+
+ ua = window.navigator.userAgent;
+ if (!ua)
+ return false;
+
+ for (var i = 0; i < BROWSERS.length; i++) {
+ var matches, reg;
+
+ reg = "\\b" + BROWSERS[i].idString + "\\b";
+ if (!ua.match(new RegExp(reg, "i")))
+ continue;
+ reg = "\\b" + BROWSERS[i].verString + "\\/(\\d+)";
+ matches = ua.match(new RegExp(reg, "i"));
+ return matches !== null && Number(matches[1]) >= BROWSERS[i].version;
+ }
+
+ return false;
+}
+
+function make_websocket(addr) {
+ var url;
+ var ws;
+
+ url = build_url("ws", addr.host, addr.port, "/");
+
+ if (have_websocket_binary_frames())
+ ws = new WebSocket(url);
+ else
+ ws = new WebSocket(url, "base64");
+ /* "User agents can use this as a hint for how to handle incoming binary
+ data: if the attribute is set to 'blob', it is safe to spool it to disk,
+ and if it is set to 'arraybuffer', it is likely more efficient to keep
+ the data in memory." */
+ ws.binaryType = "arraybuffer";
+
+ return ws;
+}
+
+function FlashProxy() {
+ if (HEADLESS) {
+ /* No badge. */
+ } else if (DEBUG) {
+ this.badge_elem = debug_div;
+ } else {
+ this.badge = new Badge();
+ this.badge_elem = this.badge.elem;
+ }
+ if (this.badge_elem)
+ this.badge_elem.setAttribute("id", "flashproxy-badge");
+
+ this.proxy_pairs = [];
+
+ this.start = function() {
+ var client_addr;
+ var relay_addr;
+ var rate_limit_bytes;
+
+ this.fac_url = get_param_string(query, "facilitator", DEFAULT_FACILITATOR_URL);
+
+ this.max_num_proxy_pairs = get_param_integer(query, "max_clients", DEFAULT_MAX_NUM_PROXY_PAIRS);
+ if (this.max_num_proxy_pairs === null || this.max_num_proxy_pairs < 0) {
+ puts("Error: max_clients must be a nonnegative integer.");
+ this.die();
+ return;
+ }
+
+ this.initial_facilitator_poll_interval = get_param_timespec(query, "initial_facilitator_poll_interval", DEFAULT_INITIAL_FACILITATOR_POLL_INTERVAL);
+ if (this.initial_facilitator_poll_interval === null || this.initial_facilitator_poll_interval < 0) {
+ puts("Error: initial_facilitator_poll_interval must be a nonnegative number.");
+ this.die();
+ return;
+ }
+
+ this.facilitator_poll_interval = get_param_timespec(query, "facilitator_poll_interval");
+ if (this.facilitator_poll_interval !== undefined && (this.facilitator_poll_interval === null || this.facilitator_poll_interval < MIN_FACILITATOR_POLL_INTERVAL)) {
+ puts("Error: facilitator_poll_interval must be a nonnegative number at least " + MIN_FACILITATOR_POLL_INTERVAL + ".");
+ this.die();
+ return;
+ }
+
+ if (query["ratelimit"] === "off")
+ rate_limit_bytes = undefined;
+ else
+ rate_limit_bytes = get_param_byte_count(query, "ratelimit", DEFAULT_RATE_LIMIT);
+ if (rate_limit_bytes === undefined) {
+ this.rate_limit = new DummyRateLimit();
+ } else if (rate_limit_bytes === null || rate_limit_bytes < MIN_FACILITATOR_POLL_INTERVAL) {
+ puts("Error: ratelimit must be a nonnegative number at least " + MIN_RATE_LIMIT + ".");
+ this.die();
+ return;
+ } else {
+ this.rate_limit = new BucketRateLimit(rate_limit_bytes * RATE_LIMIT_HISTORY, RATE_LIMIT_HISTORY);
+ }
+
+ client_addr = get_param_addr(query, "client");
+ if (client_addr === null) {
+ puts("Error: can't parse \"client\" parameter.");
+ this.die();
+ return;
+ }
+ relay_addr = get_param_addr(query, "relay");
+ if (relay_addr === null) {
+ puts("Error: can't parse \"relay\" parameter.");
+ this.die();
+ return;
+ }
+ if (client_addr !== undefined && relay_addr !== undefined) {
+ this.begin_proxy(client_addr, relay_addr);
+ return;
+ } else if (client_addr !== undefined) {
+ puts("Error: the \"client\" parameter requires \"relay\" also.")
+ this.die();
+ return;
+ } else if (relay_addr !== undefined) {
+ puts("Error: the \"relay\" parameter requires \"client\" also.")
+ this.die();
+ return;
+ }
+
+ puts("Starting; will contact facilitator in " + this.initial_facilitator_poll_interval + " seconds.");
+ setTimeout(this.proxy_main.bind(this), this.initial_facilitator_poll_interval * 1000);
+ };
+
+ this.proxy_main = function() {
+ var params;
+ var base_url, url;
+ var xhr;
+
+ if (this.proxy_pairs.length >= this.max_num_proxy_pairs) {
+ setTimeout(this.proxy_main.bind(this), this.facilitator_poll_interval * 1000);
+ return;
+ }
+
+ /* Flash proxy protocol revision. */
+ params = [["r", "1"]];
+ params.push(["transport", "websocket"]);
+ /* Clients we're currently handling. */
+ for (var i = 0; i < this.proxy_pairs.length; i++)
+ params.push(["client", format_addr(this.proxy_pairs[i].client_addr)]);
+ base_url = this.fac_url.replace(/\?.*/, "");
+ url = base_url + "?" + build_query_string(params);
+ xhr = new XMLHttpRequest();
+ try {
+ xhr.open("GET", url);
+ } catch (err) {
+ /* An exception happens here when, for example, NoScript allows the
+ domain on which the proxy badge runs, but not the domain to which
+ it's trying to make the HTTP request. The exception message is
+ like "Component returned failure code: 0x805e0006
+ [nsIXMLHttpRequest.open]" on Firefox. */
+ puts("Facilitator: exception while connecting: " + repr(err.message) + ".");
+ this.die();
+ return;
+ }
+ xhr.responseType = "text";
+ xhr.onreadystatechange = function() {
+ if (xhr.readyState === xhr.DONE) {
+ if (xhr.status === 200) {
+ this.fac_complete(xhr.responseText);
+ } else {
+ puts("Facilitator: can't connect: got status " + repr(xhr.status) + " and status text " + repr(xhr.statusText) + ".");
+ this.die();
+ }
+ }
+ }.bind(this);
+
+ /* Remove query string if scrubbing. */
+ if (SAFE_LOGGING)
+ puts("Facilitator: connecting to " + base_url + ".");
+ else
+ puts("Facilitator: connecting to " + url + ".");
+
+ xhr.send(null);
+ };
+
+ this.fac_complete = function(text) {
+ var response;
+ var client_addr;
+ var relay_addr;
+ var poll_interval;
+
+ response = parse_query_string(text);
+
+ if (this.facilitator_poll_interval) {
+ poll_interval = this.facilitator_poll_interval;
+ } else {
+ poll_interval = get_param_integer(response, "check-back-in", DEFAULT_FACILITATOR_POLL_INTERVAL);
+ if (poll_interval === null) {
+ puts("Error: can't parse polling interval from facilitator, " + repr(poll_interval) + ".");
+ poll_interval = DEFAULT_FACILITATOR_POLL_INTERVAL;
+ }
+ if (poll_interval < MIN_FACILITATOR_POLL_INTERVAL)
+ poll_interval = MIN_FACILITATOR_POLL_INTERVAL;
+ }
+
+ puts("Next check in " + repr(poll_interval) + " seconds.");
+ setTimeout(this.proxy_main.bind(this), poll_interval * 1000);
+
+ if (!response.client) {
+ puts("No clients.");
+ return;
+ }
+ client_addr = parse_addr_spec(response.client);
+ if (client_addr === null) {
+ puts("Error: can't parse client spec " + safe_repr(response.client) + ".");
+ return;
+ }
+ if (!response.relay) {
+ puts("Error: missing relay in response.");
+ return;
+ }
+ relay_addr = parse_addr_spec(response.relay);
+ if (relay_addr === null) {
+ puts("Error: can't parse relay spec " + safe_repr(response.relay) + ".");
+ return;
+ }
+ puts("Facilitator: got client:" + safe_repr(client_addr) + " "
+ + "relay:" + safe_repr(relay_addr) + ".");
+
+ this.begin_proxy(client_addr, relay_addr);
+ };
+
+ this.begin_proxy = function(client_addr, relay_addr) {
+ /* Start two proxy connections because of some versions of Tor making
+ two pt connections:
+ https://lists.torproject.org/pipermail/tor-dev/2012-December/004221.html
+ https://trac.torproject.org/projects/tor/ticket/7733 */
+ this.make_proxy_pair(client_addr, relay_addr);
+ this.make_proxy_pair(client_addr, relay_addr);
+ };
+
+ this.make_proxy_pair = function(client_addr, relay_addr) {
+ var proxy_pair;
+
+ proxy_pair = new ProxyPair(client_addr, relay_addr, this.rate_limit);
+ this.proxy_pairs.push(proxy_pair);
+ proxy_pair.complete_callback = function(event) {
+ puts("Complete.");
+ /* Delete from the list of active proxy pairs. */
+ this.proxy_pairs.splice(this.proxy_pairs.indexOf(proxy_pair), 1);
+ if (this.badge)
+ this.badge.proxy_end();
+ }.bind(this);
+ try {
+ proxy_pair.connect();
+ } catch (err) {
+ puts("ProxyPair: exception while connecting: " + safe_repr(err.message) + ".");
+ this.die();
+ return;
+ }
+
+ if (this.badge)
+ this.badge.proxy_begin();
+ };
+
+ /* Cease all network operations and prevent any future ones. */
+ this.disable = function() {
+ puts("Disabling.");
+ this.start = function() { };
+ this.proxy_main = function() { };
+ this.make_proxy_pair = function(client_addr, relay_addr) { };
+ while (this.proxy_pairs.length > 0)
+ this.proxy_pairs.pop().close();
+ if (this.badge)
+ this.badge.disable();
+ };
+
+ this.die = function() {
+ puts("Dying.");
+ if (this.badge)
+ this.badge.die();
+ };
+}
+
+/* An instance of a client-relay connection. */
+function ProxyPair(client_addr, relay_addr, rate_limit) {
+ var MAX_BUFFER = 10 * 1024 * 1024;
+
+ function log(s) {
+ puts(s)
+ }
+
+ this.client_addr = client_addr;
+ this.relay_addr = relay_addr;
+ this.rate_limit = rate_limit;
+
+ this.c2r_schedule = [];
+ this.r2c_schedule = [];
+
+ this.running = true;
+ this.flush_timeout_id = null;
+
+ /* This callback function can be overridden by external callers. */
+ this.complete_callback = function() {
+ };
+
+ this.connect = function() {
+ log("Client: connecting.");
+ this.client_s = make_websocket(this.client_addr);
+
+ /* Try to connect to the client first (since that is more likely to
+ fail) and only after that try to connect to the relay. */
+ this.client_s.label = "Client";
+ this.client_s.onopen = this.client_onopen_callback;
+ this.client_s.onclose = this.onclose_callback;
+ this.client_s.onerror = this.onerror_callback;
+ this.client_s.onmessage = this.onmessage_client_to_relay;
+ };
+
+ this.client_onopen_callback = function(event) {
+ var ws = event.target;
+
+ log(ws.label + ": connected.");
+ log("Relay: connecting.");
+ this.relay_s = make_websocket(this.relay_addr);
+
+ this.relay_s.label = "Relay";
+ this.relay_s.onopen = this.relay_onopen_callback;
+ this.relay_s.onclose = this.onclose_callback;
+ this.relay_s.onerror = this.onerror_callback;
+ this.relay_s.onmessage = this.onmessage_relay_to_client;
+ }.bind(this);
+
+ this.relay_onopen_callback = function(event) {
+ var ws = event.target;
+
+ log(ws.label + ": connected.");
+ }.bind(this);
+
+ this.onclose_callback = function(event) {
+ var ws = event.target;
+
+ log(ws.label + ": closed.");
+ this.flush();
+
+ if (this.running && is_closed(this.client_s) && is_closed(this.relay_s)) {
+ this.running = false;
+ this.complete_callback();
+ }
+ }.bind(this);
+
+ this.onerror_callback = function(event) {
+ var ws = event.target;
+
+ log(ws.label + ": error.");
+ this.close();
+ }.bind(this);
+
+ this.onmessage_client_to_relay = function(event) {
+ this.c2r_schedule.push(event.data);
+ this.flush();
+ }.bind(this);
+
+ this.onmessage_relay_to_client = function(event) {
+ this.r2c_schedule.push(event.data);
+ this.flush();
+ }.bind(this);
+
+ function is_open(ws) {
+ return ws !== undefined && ws.readyState === WebSocket.OPEN;
+ }
+
+ function is_closed(ws) {
+ return ws === undefined || ws.readyState === WebSocket.CLOSED;
+ }
+
+ this.close = function() {
+ if (!is_closed(this.client_s))
+ this.client_s.close();
+ if (!is_closed(this.relay_s))
+ this.relay_s.close();
+ };
+
+ /* Send as much data as the rate limit currently allows. */
+ this.flush = function() {
+ var busy;
+
+ if (this.flush_timeout_id)
+ clearTimeout(this.flush_timeout_id);
+ this.flush_timeout_id = null;
+
+ busy = true;
+ while (busy && !this.rate_limit.is_limited()) {
+ var chunk;
+
+ busy = false;
+ if (is_open(this.client_s) && this.client_s.bufferedAmount < MAX_BUFFER && this.r2c_schedule.length > 0) {
+ chunk = this.r2c_schedule.shift();
+ this.rate_limit.update(chunk.length);
+ this.client_s.send(chunk);
+ busy = true;
+ }
+ if (is_open(this.relay_s) && this.relay_s.bufferedAmount < MAX_BUFFER && this.c2r_schedule.length > 0) {
+ chunk = this.c2r_schedule.shift();
+ this.rate_limit.update(chunk.length);
+ this.relay_s.send(chunk);
+ busy = true;
+ }
+ }
+
+ if (is_closed(this.relay_s) && !is_closed(this.client_s) && this.client_s.bufferedAmount === 0 && this.r2c_schedule.length === 0) {
+ log("Client: closing.");
+ this.client_s.close();
+ }
+ if (is_closed(this.client_s) && !is_closed(this.relay_s) && this.relay_s.bufferedAmount === 0 && this.c2r_schedule.length === 0) {
+ log("Relay: closing.");
+ this.relay_s.close();
+ }
+
+ if (this.r2c_schedule.length > 0 || (is_open(this.client_s) && this.client_s.bufferedAmount > 0)
+ || this.c2r_schedule.length > 0 || (is_open(this.relay_s) && this.relay_s.bufferedAmount > 0))
+ this.flush_timeout_id = setTimeout(this.flush.bind(this), this.rate_limit.when() * 1000);
+ };
+}
+
+function BucketRateLimit(capacity, time) {
+ this.amount = 0.0;
+ /* capacity / time is the rate we are aiming for. */
+ this.capacity = capacity;
+ this.time = time;
+ this.last_update = new Date();
+
+ this.age = function() {
+ var now;
+ var delta;
+
+ now = new Date();
+ delta = (now - this.last_update) / 1000.0;
+ this.last_update = now;
+
+ this.amount -= delta * this.capacity / this.time;
+ if (this.amount < 0.0)
+ this.amount = 0.0;
+ };
+
+ this.update = function(n) {
+ this.age();
+ this.amount += n;
+
+ return this.amount <= this.capacity;
+ };
+
+ /* How many seconds in the future will the limit expire? */
+ this.when = function() {
+ this.age();
+
+ return (this.amount - this.capacity) / (this.capacity / this.time);
+ }
+
+ this.is_limited = function() {
+ this.age();
+
+ return this.amount > this.capacity;
+ }
+}
+
+/* A rate limiter that never limits. */
+function DummyRateLimit(capacity, time) {
+ this.update = function(n) {
+ return true;
+ };
+
+ this.when = function() {
+ return 0.0;
+ }
+
+ this.is_limited = function() {
+ return false;
+ }
+}
+
+var HTML_ESCAPES = {
+ "&": "amp",
+ "<": "lt",
+ ">": "gt",
+ "'": "apos",
+ "\"": "quot"
+};
+function escape_html(s) {
+ return s.replace(/&<>'"/, function(x) { return HTML_ESCAPES[x] });
+}
+
+var LOCALIZATIONS = {
+ "en": { filename: "badge-en.png", text: "Internet Freedom" },
+ "de": { filename: "badge-de.png", text: "Internetfreiheit" },
+ "pt": { filename: "badge-pt.png", text: "Internet Livre" },
+ "ru": { filename: "badge-ru.png", text: "Свобода Интернета" }
+};
+var DEFAULT_LOCALIZATION = { filename: "badge.png", text: "Internet Freedom" };
+/* Return an array of progressively less specific language tags, canonicalized
+ for lookup in LOCALIZATIONS. */
+function lang_keys(code) {
+ code = code.toLowerCase();
+ var result = [code];
+ var m = code.match(/^(\w+)/);
+ if (m !== null) {
+ result.push(m[0]);
+ }
+ return result;
+}
+/* Return an object with "filename" and "text" keys appropriate for the given
+ array of language codes. Returns a default value if there is no localization
+ for any of the codes. */
+function get_badge_localization(langs) {
+ for (var i = 0; i < langs.length; i++) {
+ var tags = lang_keys(langs[i]);
+ for (var j = 0; j < tags.length; j++) {
+ var localization = LOCALIZATIONS[tags[j]];
+ if (localization !== undefined)
+ return localization;
+ }
+ }
+ return DEFAULT_LOCALIZATION;
+}
+
+/* The usual embedded HTML badge. The "elem" member is a DOM element that can be
+ included elsewhere. */
+function Badge() {
+ /* Number of proxy pairs currently connected. */
+ this.num_proxy_pairs = 0;
+
+ var table, tr, td, a, img;
+
+ table = document.createElement("table");
+ tr = document.createElement("tr");
+ table.appendChild(tr);
+ td = document.createElement("td");
+ tr.appendChild(td);
+ a = document.createElement("a");
+ a.setAttribute("href", "options.html");
+ a.setAttribute("target", "_blank");
+ td.appendChild(a);
+ img = document.createElement("img");
+ var localization = get_badge_localization(get_langs());
+ img.setAttribute("src", localization.filename);
+ img.setAttribute("alt", localization.text);
+ a.appendChild(img);
+
+ this.elem = table;
+ this.elem.className = "idle";
+
+ this.proxy_begin = function() {
+ this.num_proxy_pairs++;
+ this.elem.className = "active";
+ };
+
+ this.proxy_end = function() {
+ this.num_proxy_pairs--;
+ if (this.num_proxy_pairs <= 0) {
+ this.elem.className = "idle";
+ }
+ }
+
+ this.disable = function() {
+ this.elem.className = "disabled";
+ }
+
+ this.die = function() {
+ this.elem.className = "dead";
+ }
+}
+
+function quote(s) {
+ return "\"" + s.replace(/([\\\"])/g, "\\$1") + "\"";
+}
+
+function maybe_quote(s) {
+ if (!/^[a-zA-Z_]\w*$/.test(s))
+ return quote(s);
+ else
+ return s;
+}
+
+function repr(x) {
+ if (x === null) {
+ return "null";
+ } else if (typeof x === "undefined") {
+ return "undefined";
+ } else if (typeof x === "object") {
+ var elems = [];
+ for (var k in x)
+ elems.push(maybe_quote(k) + ": " + repr(x[k]));
+ return "{ " + elems.join(", ") + " }";
+ } else if (typeof x === "string") {
+ return quote(x);
+ } else {
+ return x.toString();
+ }
+}
+
+function safe_repr(s) {
+ return SAFE_LOGGING ? "[scrubbed]" : repr(s);
+}
+
+/* Do we seem to be running in Tor Browser? Check the user-agent string and for
+ no listing of supported MIME types. */
+var TBB_UAS = [
+ "Mozilla/5.0 (Windows NT 6.1; rv:10.0) Gecko/20100101 Firefox/10.0",
+ "Mozilla/5.0 (Windows NT 6.1; rv:17.0) Gecko/20100101 Firefox/17.0",
+];
+function is_likely_tor_browser() {
+ return TBB_UAS.indexOf(window.navigator.userAgent) > -1
+ && (window.navigator.mimeTypes && window.navigator.mimeTypes.length === 0);
+}
+
+/* Are circumstances such that we should self-disable and not be a
+ proxy? We take a best-effort guess as to whether this device runs on
+ a battery or the data transfer might be expensive.
+
+ http://www.zytrax.com/tech/web/mobile_ids.html
+ http://googlewebmastercentral.blogspot.com/2011/03/mo-better-to-also-detect-mobile-user.html
+ http://search.cpan.org/~cmanley/Mobile-UserAgent-1.05/lib/Mobile/UserAgent.pm
+*/
+function flashproxy_should_disable() {
+ var ua;
+
+ /* https://trac.torproject.org/projects/tor/ticket/6293 */
+ if (is_likely_tor_browser()) {
+ puts("Disable because running in Tor Browser.");
+ return true;
+ }
+
+ ua = window.navigator.userAgent;
+ if (ua) {
+ var UA_LIST = [
+ /\bmobile\b/i,
+ /\bandroid\b/i,
+ /\bopera mobi\b/i,
+ ];
+
+ for (var i = 0; i < UA_LIST.length; i++) {
+ var re = UA_LIST[i];
+
+ if (ua.match(re)) {
+ puts("Disable because User-Agent matches mobile pattern " + re + ".");
+ return true;
+ }
+ }
+
+ if (ua.match(/\bsafari\b/i) && !ua.match(/\bchrome\b/i)
+ && !ua.match(/\bversion\/[6789]\./i)) {
+ /* Disable before Safari 6.0 because it doesn't have the hybi/RFC type
+ of WebSockets. */
+ puts("Disable because User-Agent is Safari before 6.0.");
+ return true;
+ }
+ }
+
+ if (!WebSocket) {
+ /* No WebSocket support. */
+ puts("Disable because of no WebSocket support.");
+ return true;
+ }
+
+ var flashproxy_allow = get_param_boolean(cookies, OPT_IN_COOKIE);
+ var cookierequired = get_param_boolean(query, "cookierequired", false);
+ /* flashproxy_allow may be true, false, or undefined. If undefined, only
+ disable if the cookierequired param is also set. */
+ if (flashproxy_allow === false) {
+ puts("Disable because of cookie opt-out.");
+ return true;
+ } else if (cookierequired && !flashproxy_allow) {
+ puts("Disable because of cookie required and no opt-in.");
+ return true;
+ }
+
+ return false;
+}
+
+function flashproxy_badge_new() {
+ var fp;
+
+ fp = new FlashProxy();
+ if (flashproxy_should_disable())
+ fp.disable();
+
+ return fp;
+}
+
+function flashproxy_badge_insert() {
+ var fp;
+ var e;
+
+ fp = flashproxy_badge_new();
+
+ /* http://intertwingly.net/blog/2006/11/10/Thats-Not-Write for this trick to
+ insert right after the <script> element in the DOM. */
+ e = document.body;
+ while (e.lastChild && e.lastChild.nodeType === 1) {
+ e = e.lastChild;
+ }
+ e.parentNode.appendChild(fp.badge_elem);
+
+ return fp;
+}
diff --git a/proxy/modules/drupal/flashproxy-start.js b/proxy/modules/drupal/flashproxy-start.js
new file mode 100644
index 0000000..fe7d10b
--- /dev/null
+++ b/proxy/modules/drupal/flashproxy-start.js
@@ -0,0 +1 @@
+flashproxy_badge_insert().start();
diff --git a/proxy/modules/drupal/flashproxy.info b/proxy/modules/drupal/flashproxy.info
new file mode 100644
index 0000000..2f9b174
--- /dev/null
+++ b/proxy/modules/drupal/flashproxy.info
@@ -0,0 +1,5 @@
+name = Flashproxy Module
+description = Insert Flashproxy js into some or all pages on your site.
+core = 7.x
+scripts[] = flashproxy.js
+scripts[] = flashproxy-start.js
diff --git a/proxy/modules/drupal/flashproxy.js b/proxy/modules/drupal/flashproxy.js
new file mode 120000
index 0000000..db0eda6
--- /dev/null
+++ b/proxy/modules/drupal/flashproxy.js
@@ -0,0 +1 @@
+../../flashproxy.js
\ No newline at end of file
diff --git a/proxy/modules/drupal/flashproxy.module b/proxy/modules/drupal/flashproxy.module
new file mode 100644
index 0000000..a4abe2d
--- /dev/null
+++ b/proxy/modules/drupal/flashproxy.module
@@ -0,0 +1,2 @@
+<?php
+
diff --git a/proxy/modules/facebook/facebook.html b/proxy/modules/facebook/facebook.html
new file mode 100644
index 0000000..8321dbc
--- /dev/null
+++ b/proxy/modules/facebook/facebook.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>flash proxy facebook app demo</title>
+<style type="text/css">
+iframe {
+ display: block;
+ padding: 5%;
+ border: none;
+}
+</style>
+</head>
+<body>
+<iframe src="//crypto.stanford.edu/flashproxy/embed.html" width="80" height="15" frameborder="0" scrolling="no"></iframe>
+</body>
+</html>
diff --git a/proxy/modules/mediawiki/custom.js b/proxy/modules/mediawiki/custom.js
new file mode 100644
index 0000000..4b882c6
--- /dev/null
+++ b/proxy/modules/mediawiki/custom.js
@@ -0,0 +1,9 @@
+/* Flash proxy badge for MediaWiki. By Sathyanarayanan Gunasekaran.
+ Requires $wgAllowUserJs to be true in the MediaWiki configuration.
+ https://www.mediawiki.org/wiki/Manual:Interface/JavaScript
+ This affects only your own user. Works on Wikipedia.
+
+ Go to Preferences → Appearance → Custom JavaScript.
+ You will end up editing common.js; paste in this code and save it. */
+
+$('#p-personal ul').append('<li><iframe src="//crypto.stanford.edu/flashproxy/embed.html" width="80" height="15" frameborder="0" scrolling="no"></iframe></li>');
diff --git a/proxy/modules/nodejs/.gitignore b/proxy/modules/nodejs/.gitignore
new file mode 100644
index 0000000..ca29a34
--- /dev/null
+++ b/proxy/modules/nodejs/.gitignore
@@ -0,0 +1,4 @@
+/flashproxy.js
+/flashproxy.1
+/node_modules
+/npm-debug.log
diff --git a/proxy/modules/nodejs/.npmignore b/proxy/modules/nodejs/.npmignore
new file mode 100644
index 0000000..e69de29
diff --git a/proxy/modules/nodejs/INSTALL b/proxy/modules/nodejs/INSTALL
new file mode 100644
index 0000000..adef21a
--- /dev/null
+++ b/proxy/modules/nodejs/INSTALL
@@ -0,0 +1,35 @@
+First, install nodejs:
+
+ # apt-get install nodejs
+
+Make sure flashproxy.js exists in the current directory, either a symlink or
+a plain copy.
+
+== Install from source
+
+If your distribution contains both the following packages (newer suites of
+Debian and Ubuntu), you can run main.js directly:
+
+ # apt-get install node-optimist node-xmlhttprequest node-ws
+ $ nodejs main.js
+
+Otherwise, you can use npm to install it as a package, along with all its
+dependencies:
+
+ # apt-get install npm
+ # npm install -g
+ $ flashproxy
+
+== Install from npm registry
+
+This package is also being published on the npm registry. To install from there
+rather than your local copy, run:
+
+ # npm install -g flashproxy
+ $ flashproxy
+
+Rather than install globally, you can also,
+
+ $ cd ~/ # or, to the dir of your choice
+ $ npm install flashproxy
+ $ npm start flashproxy # or, node node_modules/flashproxy
diff --git a/proxy/modules/nodejs/Makefile b/proxy/modules/nodejs/Makefile
new file mode 100644
index 0000000..4c93948
--- /dev/null
+++ b/proxy/modules/nodejs/Makefile
@@ -0,0 +1,19 @@
+.PHONY: all prepublish
+topsrcdir = ../../..
+
+ALL_DST = flashproxy.js flashproxy.1
+VERSION = $(shell sed -nre 's/^\s*"version"\s*:\s*"([^"]+)".*/\1/gp' package.json)
+
+all: $(ALL_DST)
+
+prepublish: flashproxy.js
+flashproxy.js: $(topsrcdir)/proxy/flashproxy.js
+ cp -f $< $@
+
+flashproxy.1: main.js flashproxy.js $(topsrcdir)/mkman.sh $(topsrcdir)/mkman.inc Makefile
+ ./main.js --help \
+ | sed -re 's,/\S+/nodejs ./main.js,flashproxy,g' \
+ | $(topsrcdir)/mkman.sh flashproxy "$(VERSION)" "The flashproxy standalone nodejs proxy" "$(VERSION)" > "$@"
+
+clean:
+ rm -f $(ALL_DST)
diff --git a/proxy/modules/nodejs/README b/proxy/modules/nodejs/README
new file mode 100644
index 0000000..2f14bcd
--- /dev/null
+++ b/proxy/modules/nodejs/README
@@ -0,0 +1,5 @@
+== Headless flash proxies with node.js
+
+Traditionally, flash proxies have run in the browser. However, it may be
+desirable to run them as background daemons. This directory contains a main.js
+file which wraps the regular flashproxy.js and runs headlessly from node.js.
diff --git a/proxy/modules/nodejs/main.js b/proxy/modules/nodejs/main.js
new file mode 100755
index 0000000..cfadff5
--- /dev/null
+++ b/proxy/modules/nodejs/main.js
@@ -0,0 +1,59 @@
+#!/usr/bin/env node
+
+var fs = require("fs");
+var path = require("path");
+var querystring = require("querystring")
+
+var meta = require("./package.json")
+
+// Get a querystring from the command line
+var argv = require("optimist")
+ .default("debug", 1)
+ .default("initial_facilitator_poll_interval", 10)
+ .argv
+
+if ("v" in argv || "version" in argv) {
+ console.log(meta.version)
+ process.exit()
+}
+if ("h" in argv || "help" in argv) {
+ console.log("Usage: %s [-h|-v] [--param[=val]] ... [extra querystring]\n\
+\n\
+Run flashproxy on the node.js server. You can give querystring parameters as \n\
+command line options; see the main flashproxy.js program for documentation on \n\
+which parameters are accepted. For example: \n\
+\n\
+%s --debug --initial_facilitator_poll_interval=10\n\
+", argv.$0, argv.$0)
+ process.exit()
+}
+
+var extra = argv._.join("&")
+delete argv._
+delete argv.$0
+var location_search = querystring.stringify(argv)
+if (extra) {
+ location_search += "&" + extra
+}
+
+// Setup global variables that flashproxy.js expects
+var XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest;
+var window = {
+ location: { search: "?" + location_search },
+ navigator: { userAgent: "Chrome/16" },
+ WebSocket: require("ws")
+};
+
+// Include flashproxy.js using eval to run it in the scope of this script
+// so we don't need to make non-browser adjustments to flashproxy.js
+var file = path.join(__dirname, "flashproxy.js");
+try {
+ var data = fs.readFileSync(file, "utf8");
+} catch (e) {
+ console.log("Can't locate the flashproxy.js file. You probably need to run \"npm install\".");
+ process.exit(1);
+}
+eval(data);
+
+// Start 'er up
+flashproxy_badge_new().start();
diff --git a/proxy/modules/nodejs/package.json b/proxy/modules/nodejs/package.json
new file mode 100644
index 0000000..9d1844a
--- /dev/null
+++ b/proxy/modules/nodejs/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "flashproxy",
+ "version": "0.1.1",
+ "description": "Standalone flash proxy. A flash proxy is a lightweight proxy providing access to the Tor network.",
+ "main": "main.js",
+ "scripts": {
+ "prepublish": "make prepublish",
+ "start": "node main.js"
+ },
+ "bin": {
+ "flashproxy": "main.js"
+ },
+ "dependencies": {
+ "optimist": "~0.3.5",
+ "xmlhttprequest": "~1.5.0",
+ "ws": "~0.4.27"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://git.torproject.org/flashproxy.git"
+ },
+ "keywords": [
+ "flashproxy",
+ "tor",
+ "bridge",
+ "relay",
+ "websocket",
+ "pluggable transport"
+ ],
+ "license": "MIT"
+}
diff --git a/proxy/options.html b/proxy/options.html
new file mode 100644
index 0000000..45bae54
--- /dev/null
+++ b/proxy/options.html
@@ -0,0 +1,166 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<style type="text/css">
+body {
+ font-family: Calibri, Arial, sans-serif;
+ font-size: small;
+ background-color: #E7D19A;
+ text-align: center;
+}
+.content {
+ width: 760px;
+ margin: 0 auto;
+ background-color: white;
+ text-align: left;
+ border-style: solid;
+ border-width: 1px;
+ border-color: #D0A760;
+}
+h1 {
+ padding: 18px 7px;
+ margin: 0 auto;
+ background-color: #900;
+ color: white;
+ font-size: 200%;
+ font-weight: bold;
+}
+p {
+ margin: 7px;
+}
+button {
+ margin: 0px 7px 7px;
+}
+#badge-state {
+ font-size: 36px;
+ text-align: center;
+ padding: 10px 0;
+ margin: 0;
+ color: white;
+}
+#badge-state { display: none; }
+</style>
+<title>Flash proxy options</title>
+</head>
+<body>
+<div class="content">
+<div>
+<h1>Flash proxy options</h1>
+<p>
+This page enables you to use your web browser as a proxy to help
+censored Internet users. When you click yes, your browser will act as a
+censorship circumvention proxy as long as you are viewing a page with
+the flash proxy badge.
+</p>
+<p>
+<a href="http://crypto.stanford.edu/flashproxy/">For more information on this system click here</a>.
+</p>
+<p>Do you want your browser to act as a proxy?</p>
+</div>
+<noscript>
+<p>
+While JavaScript is disabled, your computer will not be a proxy. Enable
+JavaScript to change your options.
+</p>
+</noscript>
+<div id="buttons" style="display: none;">
+ <button onclick="set_allowed()">Yes</button>
+ <button onclick="set_disallowed()">No</button>
+</div>
+<div id="badge-state">
+ <p id="inactive">Proxy disabled</p>
+ <p id="active">Proxy enabled</p>
+</div>
+<div id="cookies_disabled" style="display: none;">
+<p>
+It seems your browser has cookies disabled. You will need to enable them
+in order to set flash proxy settings.
+</p>
+</div>
+</div>
+<script type="text/javascript">
+
+var COOKIE_NAME = "flashproxy-allow";
+/* max-age is not supported in IE. */
+var COOKIE_LIFETIME = "Thu, 01 Jan 2038 00:00:00 GMT";
+
+function set_cookie_allowed() {
+ document.cookie = COOKIE_NAME + "=1 ;path=/ ;expires=" + COOKIE_LIFETIME;
+}
+
+function set_cookie_disallowed() {
+ document.cookie = COOKIE_NAME + "=0 ;path=/ ;expires=" + COOKIE_LIFETIME;
+}
+
+function set_allowed() {
+ set_cookie_allowed();
+ refresh();
+}
+
+function set_disallowed() {
+ set_cookie_disallowed();
+ refresh();
+}
+
+function parse_cookie_string(cookies) {
+ var strings;
+ var result;
+
+ result = {};
+ if (cookies)
+ strings = cookies.split(";");
+ else
+ strings = [];
+ for (var i = 0; i < strings.length; i++) {
+ var string = strings[i];
+ var j, name, value;
+
+ j = string.indexOf("=");
+ if (j === -1) {
+ return null;
+ }
+ name = decodeURIComponent(string.substr(0, j).trim());
+ value = decodeURIComponent(string.substr(j + 1).trim());
+
+ if (!(name in result))
+ result[name] = value;
+ }
+
+ return result;
+}
+
+/* Returns the value of the cookie, or undefined
+ if the cookie is not present. */
+function read_cookie() {
+ var cookies = parse_cookie_string(document.cookie);
+ return cookies[COOKIE_NAME];
+}
+
+/* Updates the text telling the user what his current setting is.*/
+function refresh() {
+ var value = read_cookie();
+
+ if (value === undefined || value === "1") {
+ document.getElementById("active").style.display = "block";
+ document.getElementById("inactive").style.display = "none";
+ document.getElementById("badge-state").style.backgroundColor = "darkgreen";
+ } else {
+ document.getElementById("inactive").style.display = "block";
+ document.getElementById("active").style.display = "none";
+ document.getElementById("badge-state").style.backgroundColor = "red";
+ }
+}
+
+window.onload = function() {
+ if (navigator.cookieEnabled) {
+ document.getElementById("buttons").style.display = "block";
+ document.getElementById("badge-state").style.display = "block";
+ refresh();
+ } else {
+ document.getElementById("cookies_disabled").style.display = "block";
+ }
+}
+</script>
+</body>
+</html>
diff --git a/setup-client-exe.py b/setup-client-exe.py
new file mode 100755
index 0000000..62b9c87
--- /dev/null
+++ b/setup-client-exe.py
@@ -0,0 +1,20 @@
+#!/usr/bin/python
+"""Setup file for the flashproxy-common python module."""
+from distutils.core import setup
+import os
+import py2exe
+
+build_path = os.path.join(os.environ["PY2EXE_TMPDIR"], "build")
+dist_path = os.path.join(os.environ["PY2EXE_TMPDIR"], "dist")
+
+setup(
+ console=["flashproxy-client", "flashproxy-reg-appspot", "flashproxy-reg-email", "flashproxy-reg-http", "flashproxy-reg-url"],
+ zipfile="py2exe-flashproxy.zip",
+ options={
+ "build": { "build_base": build_path },
+ "py2exe": {
+ "includes": ["M2Crypto"],
+ "dist_dir": dist_path
+ }
+ }
+)
diff --git a/setup-common.py b/setup-common.py
new file mode 100755
index 0000000..bcfdb71
--- /dev/null
+++ b/setup-common.py
@@ -0,0 +1,47 @@
+#!/usr/bin/env python
+"""Setup file for the flashproxy-common python module.
+
+To build/install a self-contained binary distribution of flashproxy-client
+(which integrates this module within it), see Makefile.
+"""
+# Note to future developers:
+#
+# We place flashproxy-common in the same directory as flashproxy-client for
+# convenience, so that it's possible to run the client programs directly from
+# a source checkout without needing to set PYTHONPATH. This works OK currently
+# because flashproxy-client does not contain python modules, only programs, and
+# therefore doesn't conflict with the flashproxy-common module.
+#
+# If we ever need to have a python module specific to flashproxy-client, the
+# natural thing would be to add a setup.py for it. That is the reason why this
+# file is called setup-common.py instead. However, there are still issues that
+# arise from having two setup*.py files in the same directory, which is an
+# unfortunate limitation of python's setuptools.
+#
+# See discussion on #6810 for more details.
+
+import subprocess
+import sys
+
+from setuptools import setup, find_packages
+
+version = subprocess.check_output(["sh", "version.sh"]).strip()
+
+setup(
+ name = "flashproxy-common",
+ author = "dcf",
+ author_email = "dcf at torproject.org",
+ description = ("Common code for flashproxy"),
+ license = "BSD",
+ keywords = ['tor', 'flashproxy'],
+
+ packages = find_packages(exclude=['*.test']),
+ test_suite='flashproxy.test',
+
+ version = version,
+
+ install_requires = [
+ 'setuptools',
+ 'M2Crypto',
+ ],
+)
diff --git a/torrc b/torrc
new file mode 100644
index 0000000..4a66162
--- /dev/null
+++ b/torrc
@@ -0,0 +1,13 @@
+## Configuration file for Tor over flash proxies.
+## Usage:
+## tor -f torrc
+
+UseBridges 1
+# The address and port are ignored by the client transport plugin.
+Bridge flashproxy 0.0.1.0:1
+LearnCircuitBuildTimeout 0
+CircuitBuildTimeout 60
+# Change the second number here (9000) to the number of a port that can
+# receive connections from the Internet (the port for which you
+# configured port forwarding).
+ClientTransportPlugin flashproxy exec ./flashproxy-client --register :0 :9000
diff --git a/version.sh b/version.sh
new file mode 100755
index 0000000..87cf555
--- /dev/null
+++ b/version.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+# Read version from the ChangeLog to avoid repeating in multiple build scripts
+sed -nre 's/^Changes .* version (.+)$/\1/g;tx;b;:x p;q' ChangeLog
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-privacy/packages/flashproxy.git
More information about the Pkg-privacy-commits
mailing list