pagekite-0.5.8a/0000775000175000017500000000000012610153761013024 5ustar brebre00000000000000pagekite-0.5.8a/rpm/0000775000175000017500000000000012610153761013622 5ustar brebre00000000000000pagekite-0.5.8a/rpm/rpm-install.sh0000775000175000017500000000222512603542202016416 0ustar brebre00000000000000# This is a replacement for the default disttools RPM build method # which gets the file lists right, including the byte-compiled files. # # We also process our man-pages here. python setup.py install --root=$RPM_BUILD_ROOT for manpage in $(cd doc && echo *.1); do mkdir -m 755 -p $RPM_BUILD_ROOT/usr/share/man/man1/ install -v -m 644 doc/$manpage $RPM_BUILD_ROOT/usr/share/man/man1/ gzip --verbose $RPM_BUILD_ROOT/usr/share/man/man1/$manpage done mkdir -m 755 -p $RPM_BUILD_ROOT/etc/pagekite.d/default for rcfile in etc/pagekite.d/*; do install -v -m 644 $rcfile $RPM_BUILD_ROOT/etc/pagekite.d/default/ done chmod 600 $RPM_BUILD_ROOT/etc/pagekite.d/default/*account* find $RPM_BUILD_ROOT -type f \ |sed -e "s|^$RPM_BUILD_ROOT/*|/|" \ -e 's|/[^/]*$||' \ |uniq >INSTALLED_FILES mkdir -m 755 -p $RPM_BUILD_ROOT/var/log/pagekite echo /var/log/pagekite >>INSTALLED_FILES for where in init.d logrotate.d sysconfig; do if [ -e etc/$where/pagekite.fedora ]; then mkdir -m 755 -p $RPM_BUILD_ROOT/etc/$where install -v -m 755 etc/$where/pagekite.fedora $RPM_BUILD_ROOT/etc/$where/pagekite echo /etc/$where/pagekite >>INSTALLED_FILES fi done pagekite-0.5.8a/rpm/rpm-setup.sh0000775000175000017500000000027712603542202016115 0ustar brebre00000000000000#!/bin/bash cat <setup.cfg [install] prefix=/usr install_lib=$2 single_version_externally_managed=yes [bdist_rpm] release=$1 vendor=PageKite Packaging Team tac pagekite-0.5.8a/rpm/rpm-preun.sh0000775000175000017500000000064512603542202016105 0ustar brebre00000000000000 (service pagekite status >/dev/null \ && service pagekite stop \ || true) (chkconfig --del pagekite || true) # HACK: uninstall config files that have not changed. cd /etc/pagekite.d/default for conffile in *; do if [ -f "../$conffile" ]; then md5org=$(md5sum "$conffile" |awk '{print $1}') md5act=$(md5sum "../$conffile" |awk '{print $1}') [ "$md5org" = "$md5act" ] && rm -f "../$conffile" fi done pagekite-0.5.8a/rpm/rpm-post.sh0000775000175000017500000000044212603542202015734 0ustar brebre00000000000000# HACK: Enable default config files, without overwriting. cd /etc/pagekite.d/default for conffile in *; do [ -e ../$conffile ] || cp -a $conffile .. done # Make sure PageKite is restarted if necessary chkconfig --add pagekite || true service pagekite status && service pagekite restart pagekite-0.5.8a/rpm/pagekite.init0000775000175000017500000000356512603542202016306 0ustar brebre00000000000000#!/bin/bash # # pagekite This shell script enables pagekite # # Author: Edvin Dunaway # # chkconfig: - 50 01 # # description: Enable execution of pagekite # config: /etc/pagekite/pagekite.rc # # source function library . /etc/rc.d/init.d/functions # Make sure HOSTNAME is in the environment HOSTNAME=$(hostname) export HOSTNAME CONTROL=/usr/bin/pagekite CRONLOCK=/var/lock/subsys/pagekite.init LOCKFILE=/var/lock/subsys/pagekite RETVAL=0 [ -f /etc/sysconfig/pagekite ] && . /etc/sysconfig/pagekite c_start() { echo -n $"Enabling pagekite: " touch "$CRONLOCK" && success || failure RETVAL=$? echo } c_stop() { echo -n $"Disabling pagekite: " rm -f "$CRONLOCK" && success || failure RETVAL=$? echo } c_restart() { c_stop c_start } c_condrestart() { [ -f "$CRONLOCK" ] && c_restart } c_status() { if [ -f $CRONLOCK ]; then echo $"Pagekite is enabled." RETVAL=0 else echo $"Pagekite is disabled." RETVAL=3 fi } d_condrestart() { $CONTROL condrestart; RETVAL=$?; } d_restart() { $CONTROL restart; RETVAL=$?; } d_start() { $CONTROL start; RETVAL=$?; } d_status() { $CONTROL status; RETVAL=$?; } d_stop() { $CONTROL stop; RETVAL=$?; } condrestart() { if [ $DAEMON = "yes" ]; then d_condrestart; else c_restart; fi } restart() { if [ $DAEMON = "yes" ]; then d_restart; else c_restart; fi } start() { if [ $DAEMON = "yes" ]; then d_start; else c_start; fi } status() { if [ $DAEMON = "yes" ]; then d_status; else c_status; fi } stop() { if [ $DAEMON = "yes" ]; then d_stop; else c_stop; fi } case "$1" in start) start ;; stop) stop ;; restart|force-reload) restart ;; reload) ;; condrestart) condrestart ;; status) status ;; *) echo $"Usage: $0 {start|stop|status|restart|reload|force-reload|condrestart}" exit 1 esac exit $RETVAL pagekite-0.5.8a/rpm/pagekite.logrotate0000775000175000017500000000030712603542202017332 0ustar brebre00000000000000/var/log/pagekite/pagekite.log { missingok notifempty size 100k create 0644 root root postrotate /sbin/service pagekite condrestart > /dev/null 2>&1 || : endscript } pagekite-0.5.8a/rpm/pagekite.sysconfig0000775000175000017500000000006112603542202017333 0ustar brebre00000000000000# Should pagekite run in daemon mode? DAEMON=yes pagekite-0.5.8a/setup.cfg0000664000175000017500000000042212610153761014643 0ustar brebre00000000000000[install] prefix = /usr install_lib = /usr/lib/python2.7/site-packages single_version_externally_managed = yes [bdist_rpm] release = 0pagekite_fc14fc15fc16 vendor = PageKite Packaging Team [egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 pagekite-0.5.8a/PKG-INFO0000664000175000017500000000146112610153761014123 0ustar brebre00000000000000Metadata-Version: 1.0 Name: pagekite Version: 0.5.8a Summary: PageKite makes localhost servers visible to the world. Home-page: http://pagekite.org/ Author: Bjarni R. Einarsson Author-email: bre@pagekite.net License: AGPLv3+ Description: PageKite is a system for running publicly visible servers (generally web servers) on machines without a direct connection to the Internet, such as mobile devices or computers behind restrictive firewalls. PageKite works around NAT, firewalls and IP-address limitations by using a combination of tunnels and reverse proxies. Natively supported protocols: HTTP, HTTPS Any other TCP-based service, including SSH and VNC, may be exposed as well to clients supporting HTTP Proxies. Platform: UNKNOWN pagekite-0.5.8a/setup.py0000775000175000017500000000222612603542202014535 0ustar brebre00000000000000#!/usr/bin/python import time from datetime import date from setuptools import setup from pagekite.common import APPVER import os try: # This borks sdist. os.remove('.SELF') except: pass setup( name="pagekite", version=APPVER.replace('github', 'dev%d' % (120*int(time.time()/120))), license="AGPLv3+", author="Bjarni R. Einarsson", author_email="bre@pagekite.net", url="http://pagekite.org/", description="""PageKite makes localhost servers visible to the world.""", long_description="""\ PageKite is a system for running publicly visible servers (generally web servers) on machines without a direct connection to the Internet, such as mobile devices or computers behind restrictive firewalls. PageKite works around NAT, firewalls and IP-address limitations by using a combination of tunnels and reverse proxies. Natively supported protocols: HTTP, HTTPS Any other TCP-based service, including SSH and VNC, may be exposed as well to clients supporting HTTP Proxies. """, packages=['pagekite', 'pagekite.ui', 'pagekite.proto'], scripts=['scripts/pagekite', 'scripts/lapcat'], install_requires=['SocksipyChain >= 2.0.15'] ) pagekite-0.5.8a/Makefile0000664000175000017500000001160012603542200014452 0ustar brebre00000000000000# Makefile for building combined pagekite.py files. export PYTHONPATH := . BREED_PAGEKITE = pagekite/__init__.py \ pagekite/common.py \ pagekite/compat.py \ pagekite/logging.py \ pagekite/manual.py \ pagekite/proto/__init__.py \ pagekite/proto/proto.py \ pagekite/proto/parsers.py \ pagekite/proto/selectables.py \ pagekite/proto/filters.py \ pagekite/proto/conns.py \ pagekite/ui/__init__.py \ pagekite/ui/nullui.py \ pagekite/ui/basic.py \ pagekite/ui/remote.py \ pagekite/yamond.py \ pagekite/httpd.py \ pagekite/pk.py \ combined: pagekite tools doc/MANPAGE.md dev .header @./scripts/breeder.py --compress --header .header \ sockschain $(BREED_PAGEKITE) \ pagekite/__main__.py \ >pagekite-tmp.py @chmod +x pagekite-tmp.py @./scripts/blackbox-test.sh ./pagekite-tmp.py - \ && ./scripts/blackbox-test.sh ./pagekite-tmp.py - --nopyopenssl \ && ./scripts/blackbox-test.sh ./pagekite-tmp.py - --nossl \ && ./scripts/blackbox-test.sh ./pagekite-tmp.py - --tls_legacy @killall pagekite-tmp.py @mv pagekite-tmp.py dist/pagekite-`python setup.py --version`.py @ls -l dist/pagekite-*.py gtk: pagekite tools dev .header @./scripts/breeder.py --gtk-images --compress --header .header \ sockschain $(BREED_PAGEKITE) gui \ pagekite_gtk.py \ >pagekite-tmp.py @chmod +x pagekite-tmp.py @mv pagekite-tmp.py dist/pagekite-gtk-`python setup.py --version`.py @ls -l dist/pagekite-*.py android: pagekite tools .header @./scripts/breeder.py --compress --header .header \ sockschain $(BREED_PAGEKITE) \ pagekite/android.py \ >pagekite-tmp.py @chmod +x pagekite-tmp.py @mv pagekite-tmp.py dist/pk-android-`./pagekite-tmp.py --appver`.py @ls -l dist/pk-android-*.py doc/MANPAGE.md: pagekite pagekite/manual.py @./pagekite/manual.py --nopy --markdown >doc/MANPAGE.md doc/pagekite.1: pagekite pagekite/manual.py @./pagekite/manual.py --nopy --man >doc/pagekite.1 dist: combined .deb gtk allrpm android allrpm: rpm_el4 rpm_el5 rpm_el6-fc13 rpm_fc14-15-16 alldeb: .deb rpm_fc14-15-16: @./rpm/rpm-setup.sh 0pagekite_fc14fc15fc16 /usr/lib/python2.7/site-packages @make .rpm rpm_el4: @./rpm/rpm-setup.sh 0pagekite_el4 /usr/lib/python2.3/site-packages @make .rpm rpm_el5: @./rpm/rpm-setup.sh 0pagekite_el5 /usr/lib/python2.4/site-packages @make .rpm rpm_el6-fc13: @./rpm/rpm-setup.sh 0pagekite_el6fc13 /usr/lib/python2.6/site-packages @make .rpm .rpm: doc/pagekite.1 @python setup.py bdist_rpm --install=rpm/rpm-install.sh \ --post-install=rpm/rpm-post.sh \ --pre-uninstall=rpm/rpm-preun.sh \ --requires=python-SocksipyChain VERSION=`python setup.py --version` .debprep: doc/pagekite.1 @rm -f setup.cfg @sed -e "s/@VERSION@/$(VERSION)/g" \ < debian/control.in >debian/control @sed -e "s/@VERSION@/$(VERSION)/g" \ < debian/copyright.in >debian/copyright @sed -e "s/@VERSION@/$(VERSION)/g" \ -e "s/@DATE@/`date -R`/g" \ < debian/changelog.in >debian/changelog @ls -1 doc/*.? >debian/pagekite.manpages @ln -fs ../etc/logrotate.d/pagekite.debian debian/pagekite.logrotate @ln -fs ../etc/init.d/pagekite.debian debian/init.d .targz: .debprep @python setup.py sdist .deb: .targz @cp -v dist/pagekite*.tar.gz \ ../pagekite-$(VERSION)_$(VERSION).orig.tar.gz @debuild -i -us -uc -b @mv ../pagekite_*.deb dist/ @rm ../pagekite-*.orig.tar.gz .header: pagekite doc/header.txt @sed -e "s/@VERSION@/$(VERSION)/g" \ < doc/header.txt >.header test: dev @./scripts/blackbox-test.sh ./pk - @./scripts/blackbox-test.sh ./pk - --nopyopenssl @./scripts/blackbox-test.sh ./pk - --nossl @./scripts/blackbox-test.sh ./pk - --tls_legacy @(for pkb in scripts/legacy-testing/*py; do \ ./scripts/blackbox-test.sh $$pkb ./pk --nossl && \ ./scripts/blackbox-test.sh $$pkb ./pk || \ ./scripts/blackbox-test.sh $$pkb ./pk --tls_legacy \ ;done) pagekite: pagekite/__init__.py pagekite/httpd.py pagekite/__main__.py dev: sockschain @rm -f .SELF @ln -fs . .SELF @ln -fs scripts/pagekite_gtk pagekite_gtk.py @echo export PYTHONPATH=`pwd` @echo export HTTP_PROXY= @echo export http_proxy= sockschain: @ln -fs ../PySocksipyChain/sockschain . tools: scripts/breeder.py Makefile scripts/breeder.py: @ln -fs ../../PyBreeder/breeder.py scripts/breeder.py distclean: clean @rm -rvf dist/*.* clean: @rm -vf sockschain *.pyc */*.pyc */*/*.pyc scripts/breeder.py .SELF @rm -vf .appver pagekite-tmp.py MANIFEST setup.cfg pagekite_gtk.py @rm -vrf *.egg-info .header doc/pagekite.1 build/ @rm -vf debian/files debian/control debian/copyright debian/changelog @rm -vrf debian/pagekite* debian/python* debian/init.d pagekite-0.5.8a/doc/0000775000175000017500000000000012610153761013571 5ustar brebre00000000000000pagekite-0.5.8a/doc/pagekite.10000664000175000017500000003555112610153713015452 0ustar brebre00000000000000.\" This man page is autogenerated from the pagekite built-in manual. .TH PAGEKITE "1" "2015-10-16" "https://pagekite.net/" "Awesome Commands" .nh .ad l .SH NAME pagekite v0.5.8a \- Make localhost servers publicly visible .SH SYNOPSIS \fBpagekite\fR [\fI\-\-options\fR] [\fIservice\fR] \fIkite\-name\fR [\fI+flags\fR] .SH DESCRIPTION PageKite is a system for exposing \fIlocalhost\fR servers to the public Internet. It is most commonly used to make local web servers or SSH servers publicly visible, although almost any TCP\-based protocol can work if the client knows how to use an HTTP proxy. PageKite uses a combination of tunnels and reverse proxies to compensate for the fact that \fIlocalhost\fR usually does not have a public IP address and is often subject to adverse network conditions, including aggressive firewalls and multiple layers of NAT. This program implements both ends of the tunnel: the local "back\-end" and the remote "front\-end" reverse\-proxy relay. For convenience, \fBpagekite\fR also includes a basic HTTP server for quickly exposing files and directories to the World Wide Web for casual sharing and collaboration. .SH BASIC USAGE .nf Basic usage, gives \fIhttp://localhost:80/\fR a public name: $ pagekite NAME.pagekite.me To expose specific folders, files or use alternate local ports: $ pagekite /a/path/ NAME.pagekite.me +indexes # built\-in HTTPD $ pagekite *.html NAME.pagekite.me # built\-in HTTPD $ pagekite 3000 NAME.pagekite.me # HTTPD on 3000 To expose multiple local servers (SSH and HTTP): $ pagekite ssh://NAME.pagekite.me AND 3000 NAME.pagekite.me .fi .SH SERVICES AND KITES The most comman usage of \fBpagekite\fR is as a back\-end, where it is used to expose local services to the outside world. Examples of services are: a local HTTP server, a local SSH server, a folder or a file. A service is exposed by describing it on the command line, along with the desired public kite name. If a kite name is requested which does not already exist in the configuration file and program is run interactively, the user will be prompted and given the option of signing up and/or creating a new kite using the \fBpagekite.net\fR service. Multiple services and kites can be specified on a single command\-line, separated by the word 'AND' (note capital letters are required). This may cause problems if you have many files and folders by that name, but that should be relatively rare. :\-) .SH KITE CONFIGURATION The options \fB\-\-list\fR, \fB\-\-add\fR, \fB\-\-disable\fR and \fB\-\-remove\fR can be used to manipulate the kites and service definitions in your configuration file, if you prefer not to edit it by hand. Examples: .nf Adding new kites $ pagekite \-\-add /a/path/ NAME.pagekite.me +indexes $ pagekite \-\-add 80 OTHER\-NAME.pagekite.me To display the current configuration $ pagekite \-\-list Disable or delete kites (\-\-add re\-enables) $ pagekite \-\-disable OTHER\-NAME.pagekite.me $ pagekite \-\-remove NAME.pagekite.me .fi .SH FLAGS Flags are used to tune the behavior of a particular kite, for example by enabling access controls or specific features of the built\-in HTTP server. .SS Common flags .TP \fB+ip\fR/\fI1.2.3.4\fR \fR Enable connections only from this IP address. .TP \fB+ip\fR/\fI1.2.3\fR \fR Enable connections only from this /24 netblock. .SS HTTP protocol flags .TP \fB+password\fR/\fIname\fR=\fIpass\fR Require a username and password (HTTP Basic Authentication) .TP \fB+rewritehost\fR \fR Rewrite the incoming Host: header. .TP \fB+rewritehost\fR=\fIN\fR \fR Replace Host: header value with N. .TP \fB+rawheaders\fR \fR Do not rewrite (or add) any HTTP headers at all. .TP \fB+insecure\fR \fR Allow access to phpMyAdmin, /admin, etc. (per kite). .SS Built-in HTTPD flags .TP \fB+indexes \fR Enable directory indexes. .TP \fB+indexes\fR=\fIall\fR \fR Enable directory indexes including hidden (dot\-) files. .TP \fB+hide \fR Obfuscate URLs of shared files. .TP \fB+cgi\fR=\fIlist\fR A list of extensions, for which files should be treated as CGI scripts (example: \fI+cgi=cgi,pl,sh\fR). .SH OPTIONS The full power of \fBpagekite\fR lies in the numerous options which can be specified on the command line or in a configuration file (see below). Note that many options, especially the service and domain definitions, are additive and if given multiple options the program will attempt to obey them all. Options are processed in order and if they are not additive then the last option will override all preceding ones. Although \fBpagekite\fR accepts a great many options, most of the time the program defaults will Just Work. .SS Common options .TP \fB\-\-clean \fR Skip loading the default configuration file. .TP \fB\-\-signup \fR Interactively sign up for pagekite.net service. .TP \fB\-\-defaults \fR Set defaults for use with pagekite.net service. .TP \fB\-\-nocrashreport\fR Don't send anonymous crash reports to pagekite.net. .SS Back-end options .TP \fB\-\-shell \fR Run PageKite in an interactive shell. .TP \fB\-\-nullui \fR Silent UI for scripting. Assumes Yes on all questions. .TP \fB\-\-list \fR List all configured kites. .TP \fB\-\-add \fR Add (or enable) the following kites, save config. .TP \fB\-\-remove \fR Remove the following kites, save config. .TP \fB\-\-disable \fR Disable the following kites, save config. .TP \fB\-\-only \fR Disable all but the following kites, save config. .TP \fB\-\-insecure \fR Allow access to phpMyAdmin, /admin, etc. (global). .TP \fB\-\-local\fR=\fIports\fR \fR Configure for local serving only (no remote front\-end). .TP \fB\-\-watch\fR=\fIN\fR \fR Display proxied data (higher N = more verbosity). .TP \fB\-\-noproxy \fR Ignore system (or config file) proxy settings. .TP \fB\-\-proxy\fR=\fItype\fR:\fIserver\fR:\fIport\fR, \fB\-\-socksify\fR=\fIserver\fR:\fIport\fR, \fB\-\-torify\fR=\fIserver\fR:\fIport\fR Connect to the front\-ends using SSL, an HTTP proxy, a SOCKS proxy, or the Tor anonymity network. The type can be any of 'ssl', 'http' or 'socks5'. The server name can either be a plain hostname, user@hostname or user:password@hostname. For SSL connections the user part may be a path to a client cert PEM file. If multiple proxies are defined, they will be chained one after another. .TP \fB\-\-service_on\fR=\fIproto\fR:\fIkitename\fR:\fIhost\fR:\fIport\fR:\fIsecret\fR Explicit configuration for a service kite. Generally kites are created on the command\-line using the service short\-hand described above, but this syntax is used in the config file. .TP \fB\-\-service_off\fR=\fIproto\fR:\fIkitename\fR:\fIhost\fR:\fIport\fR:\fIsecret\fR Same as \-\-service_on, except disabled by default. .TP \fB\-\-service_cfg\fR=\fI...\fR, \fB\-\-webpath\fR=\fI...\fR These options are used in the configuration file to store service and flag settings (see above). These are both likely to change in the near future, so please just pretend you didn't notice them. .TP \fB\-\-frontend\fR=\fIhost\fR:\fIport\fR Connect to the named front\-end server. If this option is repeated, multiple connections will be made. .TP \fB\-\-frontends\fR=\fInum\fR:\fIdns\-name\fR:\fIport\fR Choose \fInum\fR front\-ends from the A records of a DNS domain name, using the given port number. Default behavior is to probe all addresses and use the fastest one. .TP \fB\-\-nofrontend\fR=\fIip\fR:\fIport\fR Never connect to the named front\-end server. This can be used to exclude some front\-ends from auto\-configuration. .TP \fB\-\-fe_certname\fR=\fIdomain\fR Connect using SSL, accepting valid certs for this domain. If this option is repeated, any of the named certificates will be accepted, but the first will be preferred. .TP \fB\-\-ca_certs\fR=\fI/path/to/file\fR Path to your trusted root SSL certificates file. .TP \fB\-\-dyndns\fR=\fIX\fR Register changes with DynDNS provider X. X can either be simply the name of one of the 'built\-in' providers, or a URL format string for ad\-hoc updating. .TP \fB\-\-all \fR Terminate early if any tunnels fail to register. .TP \fB\-\-new \fR Don't attempt to connect to any kites' old front\-ends. .TP \fB\-\-fingerpath\fR=\fIP\fR \fR Path recipe for the httpfinger back\-end proxy. .TP \fB\-\-noprobes \fR Reject all probes for service state. .SS Front-end options .TP \fB\-\-isfrontend \fR Enable front\-end operation. .TP \fB\-\-domain\fR=\fIproto,proto2,pN\fR:\fIdomain\fR:\fIsecret\fR Accept tunneling requests for the named protocols and specified domain, using the given secret. A * may be used as a wildcard for subdomains or protocols. .TP \fB\-\-authdomain\fR=\fIauth\-domain\fR, \fB\-\-authdomain\fR=\fItarget\-domain\fR:\fIauth\-domain\fR Use \fIauth\-domain\fR as a remote authentication server for the DNS\-based authetication protocol. If no \fItarget\-domain\fR is given, use this as the default authentication method. .TP \fB\-\-motd\fR=\fI/path/to/motd\fR Send the contents of this file to new back\-ends as a "message of the day". .TP \fB\-\-host\fR=\fIhostname\fRListen on the given hostname only. .TP \fB\-\-ports\fR=\fIlist\fR \fR Listen on a comma\-separated list of ports. .TP \fB\-\-portalias\fR=\fIA:B\fRReport port A as port B to backends (because firewalls). .TP \fB\-\-protos\fR=\fIlist\fR \fR Accept the listed protocols for tunneling. .TP \fB\-\-rawports\fR=\fIlist\fR Listen for raw connections these ports. The string '%s' allows arbitrary ports in HTTP CONNECT. .TP \fB\-\-accept_acl_file\fR=\fI/path/to/file\fR Consult an external access control file before accepting an incoming connection. Quick'n'dirty for mitigating abuse. The format is one rule per line: `rule policy comment` where a rule is an IP or regexp and policy is 'allow' or 'deny'. .TP \fB\-\-client_acl\fR=\fIpolicy\fR:\fIregexp\fR, \fB\-\-tunnel_acl\fR=\fIpolicy\fR:\fIregexp\fR Add a client connection or tunnel access control rule. Policies should be 'allow' or 'deny', the regular expression should be written to match IPv4 or IPv6 addresses. If defined, access rules are checkd in order and if none matches, incoming connections will be rejected. .TP \fB\-\-tls_default\fR=\fIname\fR Default name to use for SSL, if SNI (Server Name Indication) is missing from incoming HTTPS connections. .TP \fB\-\-tls_endpoint\fR=\fIname\fR:\fI/path/to/file\fR Terminate SSL/TLS for a name using key/cert from a file. .SS System options .TP \fB\-\-optfile\fR=\fI/path/to/file\fR Read settings from file X. Default is \fI~/.pagekite.rc\fR. .TP \fB\-\-optdir\fR=\fI/path/to/directory\fR Read settings from \fI/path/to/directory/*.rc\fR, in lexicographical order. .TP \fB\-\-savefile\fR=\fI/path/to/file\fR Saved settings will be written to this file. .TP \fB\-\-save \fR Save the current configuration to the savefile. .TP \fB\-\-settings\fR Dump the current settings to STDOUT, formatted as a configuration file would be. .TP \fB\-\-nozchunks \fR Disable zlib tunnel compression. .TP \fB\-\-sslzlib \fR Enable zlib compression in OpenSSL. .TP \fB\-\-buffers\fR=\fIN\fR \fR Buffer at most N kB of data before blocking. .TP \fB\-\-logfile\fR=\fIF\fR \fR Log to file F, \fIstdio\fR means standard output. .TP \fB\-\-daemonize \fR Run as a daemon. .TP \fB\-\-runas\fR=\fIU\fR:\fIG\fR \fR Set UID:GID after opening our listening sockets. .TP \fB\-\-pidfile\fR=\fIP\fR \fR Write PID to the named file. .TP \fB\-\-errorurl\fR=\fIU\fR \fR URL to redirect to when back\-ends are not found. .TP \fB\-\-selfsign\fR Configure the built\-in HTTP daemon for HTTPS, first generating a new self\-signed certificate using \fBopenssl\fR if necessary. .TP \fB\-\-httpd\fR=\fIX\fR:\fIP\fR, \fB\-\-httppass\fR=\fIX\fR, \fB\-\-pemfile\fR=\fIX\fR Configure the built\-in HTTP daemon. These options are likely to change in the near future, please pretend you didn't see them. .SH CONFIGURATION FILES If you are using \fBpagekite\fR as a command\-line utility, it will load its configuration from a file in your home directory. The file is named \fI.pagekite.rc\fR on Unix systems (including Mac OS X), or \fIpagekite.cfg\fR on Windows. If you are using \fBpagekite\fR as a system\-daemon which starts up when your computer boots, it is generally configured to load settings from \fI/etc/pagekite.d/*.rc\fR (in lexicographical order). In both cases, the configuration files contain one or more of the same options as are used on the command line, with the difference that at most one option may be present on each line, and the parser is more tolerant of white\-space. The leading '\-\-' may also be omitted for readability and blank lines and lines beginning with '#' are treated as comments. \fBNOTE:\fR When using \fB\-o\fR, \fB\-\-optfile\fR or \fB\-\-optdir\fR on the command line, it is advisable to use \fB\-\-clean\fR to suppress the default configuration. .SH SECURITY Please keep in mind, that whenever exposing a server to the public Internet, it is important to think about security. Hacked webservers are frequently abused as part of virus, spam or phishing campaigns and in some cases security breaches can compromise the entire operating system. Some advice: .nf * Switch PageKite off when not using it. * Use the built\-in access controls and SSL encryption. * Leave the firewall enabled unless you have good reason not to. * Make sure you use good passwords everywhere. * Static content is very hard to hack! * Always, always make frequent backups of any important work. .fi Note that as of version 0.5, \fBpagekite\fR includes a very basic request firewall, which attempts to prevent access to phpMyAdmin and other sensitive systems. If it gets in your way, the \fB+insecure\fR flag or \fB\-\-insecure\fR option can be used to turn it off. For more, please visit: .SH BUGS Using \fBpagekite\fR as a front\-end relay with the native Python SSL module may result in poor performance. Please use the pyOpenSSL wrappers instead. .SH SEE ALSO lapcat(1), , .SH CREDITS .nf \- Bjarni R. Einarsson \- The Beanstalks Project ehf. \- The Rannis Technology Development Fund \- Joar Wandborg .fi \- Luc\-Pierre Terral .SH COPYRIGHT AND LICENSE Copyright 2010\-2015, the Beanstalks Project ehf. and Bjarni R. Einarsson. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see: pagekite-0.5.8a/doc/MANPAGE.md0000664000175000017500000003556412610153542015175 0ustar brebre00000000000000## Name ## pagekite v0.5.8a - Make localhost servers publicly visible ## Synopsis ## pagekite [`--options`] [`service`] `kite-name` [`+flags`] ## Description ## PageKite is a system for exposing `localhost` servers to the public Internet. It is most commonly used to make local web servers or SSH servers publicly visible, although almost any TCP-based protocol can work if the client knows how to use an HTTP proxy. PageKite uses a combination of tunnels and reverse proxies to compensate for the fact that `localhost` usually does not have a public IP address and is often subject to adverse network conditions, including aggressive firewalls and multiple layers of NAT. This program implements both ends of the tunnel: the local "back-end" and the remote "front-end" reverse-proxy relay. For convenience, pagekite also includes a basic HTTP server for quickly exposing files and directories to the World Wide Web for casual sharing and collaboration. ## Basic usage ##
Basic usage, gives `http://localhost:80/` a public name:
$ pagekite NAME.pagekite.me

To expose specific folders, files or use alternate local ports:
$ pagekite /a/path/ NAME.pagekite.me +indexes  # built-in HTTPD
$ pagekite *.html   NAME.pagekite.me           # built-in HTTPD
$ pagekite 3000     NAME.pagekite.me           # HTTPD on 3000

To expose multiple local servers (SSH and HTTP):
$ pagekite ssh://NAME.pagekite.me AND 3000 NAME.pagekite.me
## Services and kites ## The most comman usage of pagekite is as a back-end, where it is used to expose local services to the outside world. Examples of services are: a local HTTP server, a local SSH server, a folder or a file. A service is exposed by describing it on the command line, along with the desired public kite name. If a kite name is requested which does not already exist in the configuration file and program is run interactively, the user will be prompted and given the option of signing up and/or creating a new kite using the pagekite.net service. Multiple services and kites can be specified on a single command-line, separated by the word 'AND' (note capital letters are required). This may cause problems if you have many files and folders by that name, but that should be relatively rare. :-) ## Kite configuration ## The options --list, --add, --disable and --remove can be used to manipulate the kites and service definitions in your configuration file, if you prefer not to edit it by hand. Examples:
Adding new kites
$ pagekite --add /a/path/ NAME.pagekite.me +indexes
$ pagekite --add 80 OTHER-NAME.pagekite.me

To display the current configuration
$ pagekite --list

Disable or delete kites (--add re-enables)
$ pagekite --disable OTHER-NAME.pagekite.me
$ pagekite --remove NAME.pagekite.me
## Flags ## Flags are used to tune the behavior of a particular kite, for example by enabling access controls or specific features of the built-in HTTP server. ### Common flags ### * +ip/`1.2.3.4` Enable connections only from this IP address. * +ip/`1.2.3` Enable connections only from this /24 netblock. ### HTTP protocol flags ### * +password/`name`=`pass` Require a username and password (HTTP Basic Authentication) * +rewritehost Rewrite the incoming Host: header. * +rewritehost=`N` Replace Host: header value with N. * +rawheaders Do not rewrite (or add) any HTTP headers at all. * +insecure Allow access to phpMyAdmin, /admin, etc. (per kite). ### Built-in HTTPD flags ### * +indexes Enable directory indexes. * +indexes=`all` Enable directory indexes including hidden (dot-) files. * +hide Obfuscate URLs of shared files. * +cgi=`list` A list of extensions, for which files should be treated as CGI scripts (example: `+cgi=cgi,pl,sh`). ## Options ## The full power of pagekite lies in the numerous options which can be specified on the command line or in a configuration file (see below). Note that many options, especially the service and domain definitions, are additive and if given multiple options the program will attempt to obey them all. Options are processed in order and if they are not additive then the last option will override all preceding ones. Although pagekite accepts a great many options, most of the time the program defaults will Just Work. ### Common options ### * --clean Skip loading the default configuration file. * --signup Interactively sign up for pagekite.net service. * --defaults Set defaults for use with pagekite.net service. * --nocrashreport Don't send anonymous crash reports to pagekite.net. ### Back-end options ### * --shell Run PageKite in an interactive shell. * --nullui Silent UI for scripting. Assumes Yes on all questions. * --list List all configured kites. * --add Add (or enable) the following kites, save config. * --remove Remove the following kites, save config. * --disable Disable the following kites, save config. * --only Disable all but the following kites, save config. * --insecure Allow access to phpMyAdmin, /admin, etc. (global). * --local=`ports` Configure for local serving only (no remote front-end). * --watch=`N` Display proxied data (higher N = more verbosity). * --noproxy Ignore system (or config file) proxy settings. * --proxy=`type`:`server`:`port`, --socksify=`server`:`port`, --torify=`server`:`port`
Connect to the front-ends using SSL, an HTTP proxy, a SOCKS proxy, or the Tor anonymity network. The type can be any of 'ssl', 'http' or 'socks5'. The server name can either be a plain hostname, user@hostname or user:password@hostname. For SSL connections the user part may be a path to a client cert PEM file. If multiple proxies are defined, they will be chained one after another. * --service_on=`proto`:`kitename`:`host`:`port`:`secret`
Explicit configuration for a service kite. Generally kites are created on the command-line using the service short-hand described above, but this syntax is used in the config file. * --service_off=`proto`:`kitename`:`host`:`port`:`secret`
Same as --service_on, except disabled by default. * --service_cfg=`...`, --webpath=`...`
These options are used in the configuration file to store service and flag settings (see above). These are both likely to change in the near future, so please just pretend you didn't notice them. * --frontend=`host`:`port`
Connect to the named front-end server. If this option is repeated, multiple connections will be made. * --frontends=`num`:`dns-name`:`port`
Choose `num` front-ends from the A records of a DNS domain name, using the given port number. Default behavior is to probe all addresses and use the fastest one. * --nofrontend=`ip`:`port`
Never connect to the named front-end server. This can be used to exclude some front-ends from auto-configuration. * --fe_certname=`domain`
Connect using SSL, accepting valid certs for this domain. If this option is repeated, any of the named certificates will be accepted, but the first will be preferred. * --ca_certs=`/path/to/file`
Path to your trusted root SSL certificates file. * --dyndns=`X`
Register changes with DynDNS provider X. X can either be simply the name of one of the 'built-in' providers, or a URL format string for ad-hoc updating. * --all Terminate early if any tunnels fail to register. * --new Don't attempt to connect to any kites' old front-ends. * --fingerpath=`P` Path recipe for the httpfinger back-end proxy. * --noprobes Reject all probes for service state. ### Front-end options ### * --isfrontend Enable front-end operation. * --domain=`proto,proto2,pN`:`domain`:`secret`
Accept tunneling requests for the named protocols and specified domain, using the given secret. A * may be used as a wildcard for subdomains or protocols. * --authdomain=`auth-domain`, --authdomain=`target-domain`:`auth-domain`
Use `auth-domain` as a remote authentication server for the DNS-based authetication protocol. If no target-domain is given, use this as the default authentication method. * --motd=`/path/to/motd`
Send the contents of this file to new back-ends as a "message of the day". * --host=`hostname` Listen on the given hostname only. * --ports=`list` Listen on a comma-separated list of ports. * --portalias=`A:B` Report port A as port B to backends (because firewalls). * --protos=`list` Accept the listed protocols for tunneling. * --rawports=`list`
Listen for raw connections these ports. The string '%s' allows arbitrary ports in HTTP CONNECT. * --accept_acl_file=`/path/to/file`
Consult an external access control file before accepting an incoming connection. Quick'n'dirty for mitigating abuse. The format is one rule per line: `rule policy comment` where a rule is an IP or regexp and policy is 'allow' or 'deny'. * --client_acl=`policy`:`regexp`, --tunnel_acl=`policy`:`regexp`
Add a client connection or tunnel access control rule. Policies should be 'allow' or 'deny', the regular expression should be written to match IPv4 or IPv6 addresses. If defined, access rules are checkd in order and if none matches, incoming connections will be rejected. * --tls_default=`name`
Default name to use for SSL, if SNI (Server Name Indication) is missing from incoming HTTPS connections. * --tls_endpoint=`name`:`/path/to/file`
Terminate SSL/TLS for a name using key/cert from a file. ### System options ### * --optfile=`/path/to/file`
Read settings from file X. Default is `~/.pagekite.rc`. * --optdir=`/path/to/directory`
Read settings from `/path/to/directory/*.rc`, in lexicographical order. * --savefile=`/path/to/file`
Saved settings will be written to this file. * --save Save the current configuration to the savefile. * --settings
Dump the current settings to STDOUT, formatted as a configuration file would be. * --nozchunks Disable zlib tunnel compression. * --sslzlib Enable zlib compression in OpenSSL. * --buffers=`N` Buffer at most N kB of data before blocking. * --logfile=`F` Log to file F, `stdio` means standard output. * --daemonize Run as a daemon. * --runas=`U`:`G` Set UID:GID after opening our listening sockets. * --pidfile=`P` Write PID to the named file. * --errorurl=`U` URL to redirect to when back-ends are not found. * --selfsign
Configure the built-in HTTP daemon for HTTPS, first generating a new self-signed certificate using openssl if necessary. * --httpd=`X`:`P`, --httppass=`X`, --pemfile=`X`
Configure the built-in HTTP daemon. These options are likely to change in the near future, please pretend you didn't see them. ## Configuration files ## If you are using pagekite as a command-line utility, it will load its configuration from a file in your home directory. The file is named `.pagekite.rc` on Unix systems (including Mac OS X), or `pagekite.cfg` on Windows. If you are using pagekite as a system-daemon which starts up when your computer boots, it is generally configured to load settings from `/etc/pagekite.d/*.rc` (in lexicographical order). In both cases, the configuration files contain one or more of the same options as are used on the command line, with the difference that at most one option may be present on each line, and the parser is more tolerant of white-space. The leading '--' may also be omitted for readability and blank lines and lines beginning with '#' are treated as comments. NOTE: When using -o, --optfile or --optdir on the command line, it is advisable to use --clean to suppress the default configuration. ## Security ## Please keep in mind, that whenever exposing a server to the public Internet, it is important to think about security. Hacked webservers are frequently abused as part of virus, spam or phishing campaigns and in some cases security breaches can compromise the entire operating system. Some advice:
   * Switch PageKite off when not using it.
   * Use the built-in access controls and SSL encryption.
   * Leave the firewall enabled unless you have good reason not to.
   * Make sure you use good passwords everywhere.
   * Static content is very hard to hack!
   * Always, always make frequent backups of any important work.
Note that as of version 0.5, pagekite includes a very basic request firewall, which attempts to prevent access to phpMyAdmin and other sensitive systems. If it gets in your way, the +insecure flag or --insecure option can be used to turn it off. For more, please visit: ## Bugs ## Using pagekite as a front-end relay with the native Python SSL module may result in poor performance. Please use the pyOpenSSL wrappers instead. ## See Also ## lapcat(1), , ## Credits ##
- Bjarni R. Einarsson 
- The Beanstalks Project ehf. 
- The Rannis Technology Development Fund 
- Joar Wandborg 
- Luc-Pierre Terral ## Copyright and license ## Copyright 2010-2015, the Beanstalks Project ehf. and Bjarni R. Einarsson. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see: pagekite-0.5.8a/doc/REMOTEUI.md0000664000175000017500000001135712603542201015344 0ustar brebre00000000000000# The PageKite v0.5 remote-control protocol # PageKite 0.4 introduced a basic protocol for implementing a UI wrapper around PageKite, where PageKite would send easily parsed information about what was going on to stdout, and could drive the signup process as well. PageKite 0.5 expands on this, adding the ability to change the configuration of a running PageKite on the fly, save config changes to disk and allow remote control over a socket. What follows is a basic description of the the control channel. ## Basic message format ## PageKite will send messages looking like this: status_msg: Starting up... status_tag: startup notify: Hello! This is pk v0.4.99pre5-1. status_msg: Collecting entropy for a secure secret... ... status_msg: Kites are flying and all is well. status_tag: flying ... be_status: status=1000 domain=foo.pagekite.me port= proto=http ... be_path: url=http://foo.pagekite.me/ policy=default src=/path/to/file ... notify: Some random text message These messages will continue to arrive every few seconds for the lifetime of the program, updating the UI on the current state. Each message will span exactly one line (`\n` terminated), and is split into the name and argument separated by `: ` (semicolon, space). ### Existing status tags ### Status tags are meant to be used to update an indicator icon. The following tags currently exist: startup - The program is starting up connect - Connecting to a front-end dyndns - Updating dynamic DNS traffic - A nontrivial amount of bytes are being transferred serving - An HTTP request is being handled idle - Running as front-end, waiting for back-ends. down - Running as back-end, waiting for a front-end. flying - Flying some kites! exiting - Shutting down ## Run-time control commands ## The UI can control the pagekite process by sending commands in the same format as basic messages: `command: argument\n` The following commands are recognized: exit: reason # Quit the program restart: reason # Shut down all tunnels and restart config: var=value # Parse one line of configuration addkite: kitename # Add a new kite (triggers wizard) save: reason # Save the running config to disk Reasons are currently ignored, but will be written to the log to help with debugging. Configuration commands of particular interest to UI programmers are: webpath=::: nowebpath=: These can be used to register/deregister paths with the built-in HTTPD on the fly during program runtime. ## Interaction message format ## When PageKite enters its signup or kite creation phase, it will send requests to the UI for user input. The UI must implement the following actions: ask_yesno ask_email ask_kitename ask_multiplechoice tell_message tell_error An optional additional pair of UI hints are `start_wizard` and `end_wizard` which group together a related sequence of questions and answers. Note that `start_wizard` may be sent repeatedly during the same session, to update the subject of the current conversation. A typical session might look like so: $ pagekite.py --remoteui --friendly # add --clean to simulate first use start_wizard: Create your first kite! begin_ask_yesno default: True question: Use the PageKite.net service? expect: yesno end_ask_yesno Reply: `y` begin_ask_email question: What is your e-mail address? expect: email end_ask_email Reply: `person@example.com` begin_ask_kitename domain: .pagekite.me question: Name your kite: expect: kitename end_ask_kitename Reply: `person.pagekite.me` start_wizard: Creating kite: person.pagekite.me begin_ask_multiplechoice preamble: Do you accept the license and terms of service? choice_1: Yes, I agree! choice_2: View Software License (AGPLv3). choice_3: View PageKite.net Terms of Service. choice_4: No, I do not accept these terms. default: 1 question: Your choice: expect: choice_index end_ask_multiplechoice Reply: `1` tell_message: Your kite, person.pagekite.me, is live! ... end_wizard: done begin_ask_yesno default: True question: Save settings to /home/bre/.pagekite.rc? expect: yesno end_ask_yesno Reply: `y` status_msg: Starting up... status_tag: startup notify: Hello! This is pk v0.4.99pre5-1. ... Implementations should accept unknown arguments/commands gracefully by ignoring them, or in the case of unknown commands, immediately replying with the default value if present. Note that control commands won't work while PageKite is waiting for a reply to a UI request. pagekite-0.5.8a/doc/README.md0000664000175000017500000011064012603542201015043 0ustar brebre00000000000000# pagekite.py # PageKite implements a tunneled reverse proxy which makes it easy to make a service (such as an HTTP or HTTPS server) on localhost visible to the wider Internet. It also supports other protocols (including SSH), by behaving as a specialized HTTP proxy. Try `./pagekite.py --help` for instructions (or read the source). Managed front-end relay service is available at , or you can run your own using pagekite.py. ## 0. Table of contents ## 1. [The basics ](#basics) 1. [Requirements ](#req) 2. [Getting started ](#qs) 3. [Configuring from the command line ](#cli) 4. [Terminology, how PageKite works ](#how) 2. [Advanced usage ](#advanced) 1. [Running the back-end, using pagekite.net ](#bes) 2. [Running the back-end, using a custom front-end ](#bec) 3. [Running your own front-end relay ](#fe) 4. [The built-in HTTP server ](#stp) 5. [Coexisting front-ends and other HTTP servers ](#co) 6. [Configuring DNS ](#dns) 7. [Connecting over Socks or Tor ](#tor) 8. [Raw services (HTTP CONNECT) ](#ipr) 9. [SSL/TLS back-ends, endpoints and SNI ](#tls) 10. [Unix/Linux systems integration ](#unx) 11. [Saving your configuration ](#cfg) 12. [A word about security and logs ](#sec) 3. [Limitations, caveats and known bugs ](#lim) 4. [Credits and licence ](#lic) ## 1. The basics ## ### 1.1. Requirements ### `Pagekite.py` requires Python 2.x, version 2.2 or later. `Pagekite.py` includes a basic web server for serving static content, but is more commonly used to expose other HTTP servers running on localhost (such as Apache or a Django development server) to the wider Internet. In order for pagekite.py to terminate SSL connections or encrypt the built in HTTP server, you will need openssl and either python 2.6+ or the pyOpenSSL module. These are not required if you just want to route HTTPS requests. You can download pagekite.py from . **Note:** pagekite.py uses a customized version of SocksiPy (named SocksipyChain) to handle SSL compatibility and connecting over Tor or other proxy servers. If you are downloading the "all-in-one" .py file from , this library is included and built in. However, you are working with the source code, you will need to download SocksipyChain. For further details, please see . [ [up](#toc) ] ### 1.2. Getting started ### The quickest way to get started with pagekite.py, is to download a prebuilt package (combined .py file, .deb or .rpm) from . Next, interactively sign up for service with : pagekite.py --signup This will ask you for your e-mail address, ask you to choose your initial kite name (*SOMETHING.pagekite.me*), and then make whatever server you have running on visible to the outside world as . Here are a few useful examples: # Expose a local HTTP server, but password protect it. pagekite.py 80 http://FOO.pagekite.me/ +password/guest=foo # Expose a directory to the web, enabling indexes. pagekite.py /var/www http://FOO.pagekite.me:8080/ +indexes # Expose an SSH server to the world. pagekite.py localhost:22 ssh://FOO.pagekite.me/ The above examples could all be combined into a single command, and the configuration saved as your default, like so: pagekite.py --add \ 80 http://FOO.pagekite.me +password/guest=foo AND \ /var/www http://BAR.pagekite.me:8080/ +indexes AND \ localhost:22 ssh://FOO.pagekite.me After running that (admittedly longish) command, you can simply type `pagekite.py` at any time to make all three services visible to the Internet. If you have configured multiple services, but only want to expose one of them, you can use the following short-hand: pagekite.py FOO.pagekite.me Please consult the pagekite.net quick-start and wiki for more examples: * * **Note:** If using the .deb or .rpm packages, the command is simply `pagekite`, not pagekite.py. [ [up](#toc) ] ### 1.3. Configuring from the command line ### PageKite knows how to both read and write its own configuration file, which is usually named `.pagekite.rc` on Linux and the Mac, or `pagekite.cfg` on Windows. It is possible to add and remove service definitions from the configuration file, by using commands like the following: # Add a service pagekite.py --add 8000 django-FOO.pagekite.me # Temporarily disable it pagekite.py --disable django-FOO.pagekite.me # List all configured services pagekite.py --list # Permanently remove one pagekite.py --remove django-FOO.pagekite.me You can also use `--add` to update the configuration of a particular service, for example to add a `+indexes` flag or access controls. **Hint:** Another useful flag in this context, is `--nullui`. Making that the first argument will suppress the normal interactive user interface and simply assume the answer to all questions is "yes". If you already have valid service credentials in your configuration file, this can be combined with `--add` to launch new kites automatically from within a script or other program. [ [up](#toc) ] ### 1.4. Terminology, how PageKite works ### PageKite works more or less like this: 1. Your **services**, typically one or more HTTP servers, run on localhost. 2. You run pagekite.py as a **back-end connector**, on the same machine. 3. Another instance of pagekite.py runs as a **front-end relay** on a machine with a public IP address, in "the cloud". 4. The **back-end** pagekite.py connects to the **front-end**, and creates a tunnel for the configured **services**. 5. Clients, typically web browsers, connect to the **front-end** and request **service**. The **front-end** relays the request over the appropriate tunnel, where the **back-end** forwards it to the actual server. Responses travel back the same way. In practice, the back-end is often configured to use multiple front-ends, and will choose between them based on network latency or other factors. In those cases it will also update dynamic DNS servers to direct your services' DNS names to the IP address of the front-end server. If the connection to the front-end is lost, the back-end will attempt to re-establish a connection, either with the same server or another one. #### Terminology #### The following terms are use throughout the rest of this document: * A **service** is a local server, for example a website or SSH server. * A **front-end** is an instance of pagekite.py which is configured to act as a public, client-facing visible relay server. * A **back-end** is an instance of pagekite.py which is configured to establish and maintain the tunnels and DNS records required to make a local **service** visible to the wider Internet. The **back-end** must be able to make direct connections to local servers. * A **service** which is exposed to the wider Internet using PageKite is sometimes called **a kite**. To avoid confusion, we prefer not to use the terms "client" and "server" to describe the different roles of pagekite.py. [ [up](#toc) ] ## 2. Advanced usage ## In this chapter, the more esoteric features of PageKite will be explored. Note that many of these topics are only relevant to people who are running their own PageKite front-end relay servers. In all examples below, the long-form `--argument=foo` syntax is used, for precision and for compatibility with the PageKite configuration file. Note that in release 0.4.7, the `--backend` argument was renamed to `--service_on`. This document uses the new name, although the old one is still recognized by the program and will be for the forseeable future. ### 2.1. Running the back-end, using pagekite.net ### The most common use of pagekite.py, is to make a web server visible to the outside world. Assuming you are using the pagekite.net service and your web server runs on port 80, a command like this should get you up and running: backend$ pagekite.py \ --defaults \ --service_on=http:YOURNAME:localhost:80:SECRET Replace YOURNAME with your PageKite domain name (for example *something.pagekite.me*) and SECRET with the shared secret displayed on your account page. You can add multiple service specifications, one for each name and protocol you wish to expose. Here is an example running two websites, one of which is available using three protocols: HTTP, HTTPS and WebSocket. backend$ pagekite.py \ --defaults \ --service_on=http:YOURNAME:localhost:80:SECRET \ --service_on=https:YOURNAME:localhost:443:SECRET \ --service_on=http:OTHERNAME:localhost:8080:SECRET Alternately, if you want to expose different HTTP services on different ports for the same domain name, you can include port numbers in your service specs: backend$ pagekite.py \ --defaults \ --service_on=http/80:YOURNAME:localhost:80:SECRET \ --service_on=http/8080:YOURNAME:localhost:8080:SECRET Note that this really only works for HTTP. Also, which ports are actually available depends on the front-end, and the protocol must still be one supported by PageKite (HTTP, HTTPS or WebSocket). [ [up](#toc) ] ### 2.2. Running the back-end, using a custom front-end ### If you prefer to run your own front-ends, you will need to follow the instructions in this section on your back-ends, and the instructions in the next section on your front-end. When running your own front-end, you need to tell pagekite.py where it is, using the `--frontend` argument: backend$ pagekite.py \ --frontend=HOST:PORT \ --service_on=http:YOURNAME:localhost:80:YOURSECRET Replace HOST with the DNS name or IP address of your front-end, and PORT with one of the ports it listens for connections on. If your front-end supports TLS-encrypted tunnels, add the --fe_certname=HOST argument as well. [ [up](#toc) ] ### 2.3. Running your own front-end relay ### To configure pagekite.py as a front-end relay, you will need to have a host with a publicly visible IP address, and you will need to configure DNS correctly, [as discussed below](#dns). Assuming you are not already running a web server on that machine, the optimal configuration is to run pagekite.py so it listens on a few ports (80 and 443 at least), like so: frontend$ sudo pagekite.py \ --isfrontend \ --ports=80,443 --protos=http,https \ --domain=http,https:YOURNAME:YOURSECRET In this case, YOURNAME must be a DNS name which points to the IP of the front-end relay (either an A or CNAME record), and YOURSECRET is a shared secret of your choosing - it has to match on the back-end, or the connection will be rejected. Perceptive readers will have noticed a few problems with this though. One, is that you are running pagekite.py as root, which is generally frowned upon by those concerned with security. Another, is you have only enabled a single back-end, which is a bit limited. The second problem is easily addressed, as the `--domain` parameter will accept wild-cards, and of course you can have as many `--domain` parameters as you like. So something like this might make sense: frontend$ sudo pagekite.py \ --isfrontend \ --ports=80,443,8080 --protos=http,https \ --domain=http,https:*.YOURDOMAIN.COM:YOURSECRET \ --domain=http,https:*.YOUROTHERDOMAIN.NET:YOUROTHERSECRET Unfortunately, root permissions are required in order to bind ports 80 and 443, but it is possible to instruct pagekite.py to drop all privileges as soon as possible, like so: frontend$ sudo pagekite.py \ --isfrontend \ --runas=nobody:nogroup \ --ports=80,443,8080 --protos=http,https \ --domain=http,https:YOURNAME:YOURSECRET This assumes the *nobody* user and *nogroup* group exist on your system. Replace with other values as necessary. See the section on [Unix/Linux systems integration](#unx) for more useful flags for running a production pagekite.py. [ [up](#toc) ] ### 2.4. The built-in HTTP server ### *FIXME: Write this.* #### Enabling SSL in the built-in HTTP server #### If you have the OpenSSL (pyOpenSSL or python 2.6+), you can increase the security of your HTTP console even further by creating a self-signed SSL certificate and enabling it using the `--pemfile` option: backend$ pagekite.py \ --defaults \ --pemfile=cert.pem \ ... To generate a self-signed certificate: openssl req -new -x509 \ -keyout cert.pem -out cert.pem \ -days 365 -nodes Note that your browser will complain when you first visit the console and you will have to add a security exception in order to access the page. [ [up](#toc) ] ### 2.5. Coexisting front-ends and other HTTP servers ### What to do if you already have a web server running on the machine you want to use as a PageKite front-end? Generally only one process can run on a given IP:PORT pair, which is why this poses a problem. The simplest solution, is to get another IP address for the machine, and use one for pagekite.py, and the other for your web-server. In that case you would add the `--host=IP` argument to your pagekite.py configuration. If, however, you have to share a single IP, things get slightly more complicated. Either the web-server will have to forward connections to pagekite.py, or the other way around. #### pagekite.py on port 80 (recommended) #### As of pagekite.py 0.3.6, it is possible for front-ends to have direct local back-ends, so just letting pagekite.py have port 80 (and 443) is the simplest way to get the two to coexist: 1. Move your old web-server to another port (such as 8080) 2. Configure pagekite.py [as a front-end](#fe) on port 80 3. Add `--service_on` specifications for your old web-server. As of 0.3.14, you can make pagekite.py use a local back-end as a catch-all for any unrecongized domains, by using the special hostname "unknown" in the `--service_on` line (remember to specify a protocol). For example: frontend$ sudo pagekite.py \ --isfrontend \ --runas=nobody:nogroup \ --ports=80,443 --protos=http,https \ --domain=http,https:YOURNAME:YOURSECRET \ --service_on=http:OLDNAME:localhost:8080: \ --service_on=https:OLDNAME:localhost:8443: \ --service_on=https:unknown:localhost:8090: Note that no password is required for configuring local back-ends. #### Another HTTP server on port 80 #### The other option, assuming your web-server supports proxying, is to configure it to proxy requests for your PageKite domains to pagekite.py, and run pagekite.py on an alternate port. How this is done depends on your HTTP server software, but Apache and lighttpd at least are both capable of forwarding requests to alternate ports. This is likely to work in many cases for standard HTTP traffic, but very unlikely to work for HTTPS. **Warning:** If you have more than one domain behind PageKite, it is of critical importance that the HTTP server *not* re-use the same proxy connection for multiple requests. For performance and compatibility reasons, pagekite.py does not currently continue parsing the HTTP/1.1 request stream once it has chosen a back-end: it blindly forwards packets back and forth. This means that if the web server proxy code sends a request for *a.foo.com* first, and then requests *b.foo.com* over the same connection, the second request will be routed to the wrong back-end. Unfortunately, this means putting pagekite.py behind a high-performance load-balancer may cause unpredictable (and quite undesirable) results: Varnish at least is known to cause problems in this configuration. Please send reports of success (or failure) configuring pagekite.py behind another HTTP server, proxy or load-balancer to our Google Group: . [ [up](#toc) ] ### 2.6. Configuring DNS ### In order for your PageKite websites to be visible to the wider Internet, you will have to make sure DNS records for them are properly configured. If you are using the pagekite.net service, this is handled automatically by a dynamic DNS server, but if you are running your own front-end, then you may need to take some additional steps. #### Static DNS configuration #### Generally if you have a single fixed front-end, you can simply use a static DNS entry, either an A record or a CNAME, linking your site's domain name to the IP address of **the machine running the front-end**. So, if the front-end's name is *foo.com* with the IP address *1.2.3.4*, and your website is *blah.foo.com*, then you would need to configure the DNS record for *blah.foo.com* as a CNAME to *foo.com* or an A record to *1.2.3.4*. This is the same kind of configuration as if your front-end were a normal web host. Alternately, it might be useful to set up a wildcard DNS record for the domain *foo.com*, directing all unspecified names to your front-end. That, combined with the wildcard `--domain` argument described [above](#fe), will give you the flexibility to trivially create as many PageKite websites as you like, just by changing arguments to the [back-end](#bec). #### Dynamic DNS configuration #### This all gets a bit more complicated if you are running multiple front-ends, and letting the back-end choose between them based on ping times (this is the `--default` behavior does when using the PageKite service). First of all, the back-end will need a way to receive the list of available front-ends. Secondly, the back-end will need to be able to dynamically update the DNS records for the sites it is connecting. The list of front-ends should be provided to pagekite.py as a DNS name with multiple A records. As an example, the default for the PageKite service, is the name **frontends.b5p.us**: $ host frontends.b5p.us frontends.b5p.us has address 69.164.211.158 frontends.b5p.us has address 93.95.226.149 frontends.b5p.us has address 178.79.140.143 ... When started up with a `--frontends` argument (note the trailing s), pagekite.py will measure the distance of each of these IP addresses and pick the one closest. (It will also perform DNS lookups on its own name and connect to any old back-ends as well, to guarantee reachability while the old DNS records expire.) Pagekite.py has built-in support for most of the common dynamic DNS providers, which can be accessed via. the `--dyndns` flag. Assuming you were using `dyndns.org`, running the back-end like this might work in that case: backend$ pagekite.py \ --frontends=1:YOUR.FRONTENDS.COM:443 \ --dyndns=USER:PASS@dyndns.org \ --service_on=http:YOURNAME.dyndns.org:localhost:80:YOURSECRET Instead of dyndns.org above, pagekite.py also has built-in support for no-ip.com and of course pagekite.net. Other providers can be used by providing a full HTTP or HTTPS URL, with the following python formatting tokens in the appropriate places: %(ip)s - will be replaced by your new front-end IP address %(domain)s - will be replaced by your domain name This example argument manually implements no-ip.com support (split between lines for readability): --dyndns='https://USER:PASS@dynupdate.no-ip.com/nic/update? hostname=%(domain)s&myip=%(ip)s' [ [up](#toc) ] ### 2.7. Connecting over Socks or Tor ### If you want to run pagekite.py from behind a restrictive firewall which does not even allow outgoing connections, you might be able to work around the problem by using a Socks proxy. Alternately, if you are concerned about anonymity and want to hide your IP even from the person running the front-end, you might want to connect using the [Tor](https://www.torproject.org/) network. For these situations, you can use the `--torify` or `--socksify` arguments, like so: backend$ pagekite.py \ --defaults \ --socksify=SOCKSHOST:PORT \ --service_on=http:YOURNAME:localhost:80:YOURSECRET In the case of Tor, replace `--socksify` with `--torify` and (probably) connect to localhost, on port 9050. With `--torify`, some behavior is modified slightly in order to avoid leaking information about which domains you are hosting through DNS side channels. [ [up](#toc) ] ### 2.8. Raw services (HTTP CONNECT) ### Pagekite.py version 0.3.7 added the "raw" protocol, which allows you to bind a back-end to a raw port. This may be useful for all sorts of things, but was primarily designed as a "good enough" hack for tunneling SSH connections. As the pagekite.py front-end, and all the ports it listens on, are assumed to be shared by multiple back-ends, raw ports do not work like normal ports, they must be accessed using an HTTP Proxy CONNECT request. To expose a raw service, use a command like so: backend$ pagekite.py \ --defaults \ --service_on=raw/22:YOURNAME:localhost:22:SECRET \ ... This means you can place more or less any server behind PageKite, as long as the client can be configured to use an HTTP Proxy to connect: simply configure the client to use the PageKite front-end (and a normal port, not a raw port) as an HTTP Proxy. As an example, the following lines in **.ssh/config** provide reliable direct access to an SSH server exposed via. pagekite.py and the pagekite.net service: Host HOME.pagekite.me CheckHostIP no ProxyCommand /bin/nc -X connect -x HOME.pagekite.me:443 %h %p **Note:** The old 'RAW-after-HTTP' method is deprecated and no longer supported. It has therefore been removed from this manual. [ [up](#toc) ] ### 2.9. SSL/TLS back-ends, endpoints and SNI ### Pagekite.py includes powerful support for name-based virtual hosting of multiple encrypted (HTTPS) web-sites behind a single IP address, something which until recently was practically largely impossible. This is done based on the new TLS/SNI extension, along with some work-arounds for older clients (see Limitations below). #### Encrypted back-ends (end-to-end) #### The most secure use of TLS involves encrypted back-ends, registered with a `--service_on=https:NAME:...` argument. These back-ends themselves take care of the encryption and decryption, so all pagekite.py sees is an incomprehensible stream of binary - this is as secure as it gets! How to obtain certificates and configure your back-ends is outside the scope of this document. #### Encrypted tunnels #### As of pagekite.py version 0.3.8, it is possible to connect to the front-end using a TLS-encrypted tunnel. This is much more secure and is highly recommended: not only does this prevent people from sniffing the traffic between your web server and the front-end, it also protects you against any man-in-the-middle attacks where someone impersonates your front-end of choice. This requires additional configuration both on the front-end and on the back. On the front-end, you need to define a TLS endpoint (and certificate) for the domain of the SSL certificate (it does not actually have to match the domain name of the front-end, but the front- and back-ends have to agree what the certificate name is). frontend$ sudo pagekite.py \ ... --tls_endpoint=frontend.domain.com:/path/to/key-and-cert-chain.pem \ ... On the back-end, you need to tell pagekite.py which certificate to accept, and possibly give it the path to [a list of certificate authority certificates](http://curl.haxx.se/ca/cacert.pem) (the default works on Linux). backend$ pagekite.py \ --frontend=frontend.domain.com:443 \ --fe_certname=frontend.domain.com \ --ca_certs=/path/to/ca-certificates.pem \ ... #### Creating your own key and certificate (for tunnels) #### Note that if you are running your own front-end, you do not need to purchase a commecial certificate for this to work - you can generate your own self-signed certificate and use that on both ends (this will actually be *more* secure than using a 3rd party certificate). To generate a certificate and a key, run this: openssl req -new -x509 \ -keyout site-key.pem -out site-cert.pem \ -days 365 -nodes OpenSSL will ask a few questions - you can answer them in any way you like, it does not really matter. For use on the server, the key and certificate must be combined into a single file, like so: cat site-key.pem site-cert.pem > frontend.pem This frontend.pem file you would then configure as a TLS endpoint on the front-end, and a copy of site-cert.pem would be distributed to all the back-ends and used with the `--ca_certs` parameter. #### Encrypting unencrypted back-ends #### If you want to enable encryption for back-ends which do not themselves support HTTPS, you can use the `--tls_endpoint` flag to ask pagekite.py itself to handle TLS for a given domain. In this configuration, clients will communicate securely with the pagekite.py front-end, which will in turn forward decrypted requests to its backends, encrypting any replies as they are sent to the client. As the tunnel between pagekite front- and back-ends itself is generally encapsulated in a secure TLS connection, this provides almost the same level of security as end-to-end encryption above, with the exception that the pagekite.py front-end has access to unencrypted data. So back-ends have to trust the person running their front-end! Although not perfect, for those concerned with casual snooping on shared public WiFi, school or corporate networks, this is a significant security benefit. The expected use-case for this feature, is to deploy a wild-card certificate at the front-end, allowing multiple back-ends to encrypt their communication without the administrative overhead of generating, distributing and maintaining keys and certificates on every single one. An example: frontend$ sudo pagekite.py \ --isfrontend \ --ports=80,443 --protos=http,https \ --tls_endpoint=frontend.domain.com:/tunnel/key-and-cert-chain.pem \ --tls_endpoint=*.domain.com:/path/to/key-and-cert-chain.pem \ --domain=http:*.domain.com:SECRET backend$ sudo pagekite.py \ --frontend=frontend.domain.com:443 \ --fe_certname=frontend.domain.com \ --service_on=http:foo.domain.com:localhost:80:SECRET This would enable both https://foo.domain.com/ and http://foo.domain.com/, without an explicit https back-end being defined or configured - but the tunnel between the back- and front-ends will be encrypted using TLS and the *frontend.domain.com* certificate. **Note:** Currently SSL endpoints are only available at the front-end, but will be available on the back-end as well in a future release. **Note:** This requires either pyOpenSSL or python 2.6+ and openssl support at the OS level. #### Limitations #### Windows XP (and older) ships with an implementation of the HTTPS (TLS) protocol which does not support the SNI extension. The same is true for certain older browsers under Linux (such as lynx), Android 1.6, and generally any old or poorly maintained HTTPS clients. Without SNI, pagekite.py can not reliably detect which domain is being requested. In its absence, pagekite.py employs the following fall-back strategies to facilitate access: 1. Obey the TLS/SNI extension, if present. 2. Check if any known back-end was recently visited by the client IP, if one is found, try to use that for the domain. 3. Fall back to a default domain specified by the `--tls_default` flag. This means the common pattern of a clear-text HTTP website "upgrading" to HTTPS on certain pages is quite likely to work even for older browsers. But it is *not* guaranteed if the guest IP address is shared by multiple users, or if the browser is idle for too long (so the SSL connection times out and the IP expires from the tracking map maintained by pagekite.py). When the above measures all fail and the wrong domain is chosen for routing the TLS request, browsers should detect a certificate mismatch and abort the request. So although inconvenient and not very user-friendly, this failure mode should not pose a significant security risk. The best solution is of course to upgrade all browsers accessing your site to a recent version of Chrome, which includes proper SNI support. As this may not be realistic, it might be wise want to provide an unencrypted (HTTP) version of your website for older clients, upgrading to HTTPS only when it has been verified to work; this can be done by fetching a javascript upgrade script from the HTTPS version of your site. [ [up](#toc) ] ### 2.10. Unix/Linux systems integration ### When deploying pagekite.py as a system component on Unix, there are quite a few specialized arguments which can come in handy. In addtion to `--runas` and `--host` (discussed above), pagekite.py understands these: `--pidfile`, `--logfile` and `--daemonize`, each of which does more or less what you would expect. Special cases worth noting are `--logfile=syslog`, which instead of writing to a file, logs to the system log service and `--logfile=stdio` which logs to standard output. Putting these all together, a real production invocation of pagekite.py at the front-end might look something like this: frontend$ sudo pagekite.py \ --runas=nobody:nogroup \ --pidfile=/var/run/pagekite.pid \ --logfile=syslog \ --daemonize \ --isfrontend \ --host=1.2.3.4 \ --ports=80,443 \ --protos=http,https \ --domain=http,https:*.YOURDOMAIN.COM:YOURSECRET \ --domain=http,https:*.YOUROTHERDOMAIN.NET:YOUROTHERSECRET That is quite a lot of arguments! So please read on, and learn how to generate a configuration file... [ [up](#toc) ] ### 2.11. Saving your configuration ### Once you have everything up and running properly, you may find it more convenient to save the settings to a configuration file. Pagekite.py can generate the configuration file for you: just add `--settings` to **the very end** of the command line and save the output to a file. On Linux or OS X, that might look something like this: $ pagekite.py \ --defaults \ --service_on=http:YOURNAME:localhost:80:SECRET \ --settings \ | tee ~/.pagekite.rc The default configuration file on Linux and Mac OS X is `~/.pagekite.rc`, on Windows it is usually either `C:\\Users\\USERNAME\\pagekite.cfg` or `C:\\Documents and Settings\\USERNAME\\pagekite.cfg`. If you save your settings to this location, they will be loaded by default whenever you run pagekite.py - which may not always be what you want if you are experimenting. To *skip* the configuration file, you can use the `--clean` argument, and to load an alternate configuration, you can use `--optfile`. Combining both, you might end up with something like this: $ pagekite.py --clean --optfile=/etc/pagekite.cfg The `--optfile` option can be used within configuration files as well, if you want to "include" a one configuration into another for some reason. [ [up](#toc) ] ### 2.12. A word about security and logs ### When exposing services to the wider Internet, as pagekite.py is designed to do, it is always important to keep some basic security principles in mind. Pagekite.py itself should be quite secure - it never invokes any external processes and the only modifications it makes to the file-system are the log-files it writes. The main security concern is your HTTP server, which you are exposing to the wider Internet. Covering general web server security is out of scope for this brief manual, but there is one important difference between running a web server on a public host and running one through PageKite: Just like most other reverse proxies, PageKite will make your logs "look funny" and may break certain forms of naive access control. This is because from the point of view of your web server, all connections that travel over PageKite will appear to originate from **localhost**, with the IP address 127.0.0.1. **This will break any access controls based on IP addresses.** For logging purposes, the HTTP and WebSocket protocols, the "standard" X-Forwarded-For header is added to initial requests (if HTTP 1.1 persistent connections are used, subsequent requests may be lacking the header), in all cases pagekite.py will report the actual remote IP in its own log. [ [up](#toc) ] ## 3. Limitations, caveats and known bugs ## There are certain limitations to what can be accomplished using Pagekite, due to the nature of the underlying protocls. Here is a brief discussion of the most important ones. #### HTTPS routing and Windows XP ### HTTPS support depends on recent additions to the TLS protocol, which are unsupported by older browsers and operating systems - most importantly including the still common Windows XP. The mechanisms employed by pagekite.py to work around these problems are discussed in [the TLS/SSL section](#tls). #### Raw ports ### Raw ports are unreliable for clients sharing IP addresses with others or accessing multiple resources behind the same front-end at the same time. See the discussed in [the raw port section](#ipr) for details and instructions on how to reliably configure clients to use the HTTP CONNECT method to work around this limitation. [ [up](#toc) ] ## 4. Credits and licence ## Please see individual files for details on their licensing; as a rule Python source code falls under the same license as pagekite.py, documentation (including this document) under the Creative Commons Attribution-ShareAlike (CC-BY-SA) 3.0, and sample configuration files are placed in the Public Domain. If these licensing terms to not suit you for some reason, please contact the authors as it may be possible to negotiate alternate terms. #### This document #### This document is (C) Copyright 2010-2012, Bjarni Rúnar Einarsson and The Beanstalks Project ehf. This work is licensed under the Creative Commons Attribution-ShareAlike 3.0 Unported License. To view a copy of this license, visit or send a letter to Creative Commons, 171 Second Street, Suite 300, San Francisco, California, 94105, USA. #### pagekite.py #### Pagekite.py is (C) Copyright 2010-2012, Bjarni Rúnar Einarsson and The Beanstalks Project ehf. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . [ [up](#toc) ] pagekite-0.5.8a/doc/CREDITS.txt0000664000175000017500000000034712603542201015424 0ustar brebre00000000000000## CREDITS ## FamFamFam Silk Icons by Mark James Joar Wandborg Luc-Pierre Terral ## AUTHORS ## Bjarni Rúnar Einarsson Már Örlygsson pagekite-0.5.8a/doc/header.txt0000664000175000017500000000140312603542201015551 0ustar brebre00000000000000# WARNING: This file is a combination of multiple Python files. # The source code lives here: http://pagekite.org/ # # This file is part of pagekite.py (version @VERSION@) # Copyright 2010-2012, the Beanstalks Project ehf. and Bjarni Runar Einarsson # # This program is free software: you can redistribute it and/or modify it under # the terms of the GNU Affero General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more # details. pagekite-0.5.8a/doc/HISTORY.txt0000664000175000017500000002531712610153276015504 0ustar brebre00000000000000Version history - highlights ============================ v0.5.8a 2015.10.16 ------ - Speed up startup by pinging relays in parallel - Attempt to fix infinite loop when using epoll - Misc. crashers avoided, including in log code on disk full - Fix multiple TunnelManager crashes which would prevent reconnection v0.5.7b 2015.09.15 ------ - Allow legacy SSL support with --tls_legacy - Added --auththreads=N to tune size of authentication thread pool - Improve automated regression testing to test older versions too v0.5.7a 2015.09.06 ------- - Security: Drop SSLv2 and SSLv3 support from the front-end! - Fix permissions bug in Debian logrotate script v0.5.6e (not released) ------- - HTTPS Back-end generates TLSv1 Internal Error alerts if server is down - Added --accept_acl_file=/... for mitigating frontend abuse and DDoS. v0.5.6d 2013.06.14 ------- - Fixed bug in proxy and Tor support v0.5.6b,c 2013.05.24 --------- - Fixed bug where PageKite would not recover from network errors - Fixed IPv6 frontend selection behavior - Avoid duplicate connection woes when a frontend has multiple IPs - Fixed incorrect frontend certificate priority (bogus sorting) - Fixed loopback tunnel bugs introduced by new FE selection v0.5.6a 2013.03.18 ------- - Added default privacy-friendly robots.txt to built-in HTTPD. - Fixed bugs in DNS update logic - Improved frontend selection algorithm to fail back to faster hosts - Improved frontend selection algorithm to disconnect unused tunnels - Fixed major front-end memory leak - Started measuring round-trip-times within tunnels - Fixed multiple bugs in frontend quota rechecking code. v0.5.5 2013.02.01 ------ - Fixed broken internal buffered byte counter - Log and allow monitoring of tunnel round-trip-times - Minecraft protocol support at the frontend - Fixed connection bug: native Python SSL + no SSL on front-end = fail - Dropped support for the insecure SSLv2 v0.5.4 2012.11.29 ------ - Improved --proxy argument handling to do chains properly - Added --client_acl and --tunnel_acl - Fixed bug in --pemfile - Fixed built-in HTTPS server's silly incompatibility with SNI. - Added --selfsign for easily enabling self-signed HTTPS. - Fixed behavior of --remove and --disable for nonexistant kites. v0.5.2, v0.5.3 -------------- - Forgot to document these, oops. v0.5.1 2012.07.22 ------ - Fixed lots and lots of file descriptor leaks. - Added --shell for easier use in a GUI environment. v0.5.0 2012.07.20 ------ - Prefer and use epoll() if it is available. - Added better probe diagnostics, using json returns and CORS headers. - Correctly handle and report the new pagekite.net quota dimensions. - Corrected error messages when using an invalid shared secret. - Added support for multiple auth domains at the front-end. - Brought README.md up-to-date - Renamed --backend/--disable_backend to --service_on/--service_off - Allow white-space in the config file and make it more readable - Added: --watch= for watching tunneled traffic (back-end only) - Deprecated: --reloadfile, --delete_backend - Refactored and rewrote built-in manual and man page. - Added support for Flash socket-policy responses (open policy) - Support kites over IPv6. - Improved HTTP header filtering, now always inserts X-Forwarded-For, added X-Forwarded-Proto and +rawheaders flag for disabling. - Fixed bugs in Loopback tunnels with bad backends - Added URL firewall, +insecure and --insecure to disable it. v0.4.6 2012.01.15 ------ - Improved new kite wizard a bit - Added human readable date/time to log output - Cleaned up auto-generated configuration file - Fixed bug in front-end HTTP CONNECT for wild-card TLS endpoints - Behave gracefully when X.pagekite.me is in /etc/hosts as 127.x.x.x - Added proper MOTD handling v0.4.5 2011.08.22 ------ - Finalize and document finger and IRC support. - Support wild-card backends (*.domain.com). v0.4.4 2011.08.02 ------ - Major code reorganization, split giant pagekite.py into multiple parts, and spawned two spin-off projects: - http://pagekite.net/wiki/Floss/PyBreeder/ - http://pagekite.net/wiki/Floss/PySocksipyChain/ - Made the built-in HTTPD reply to http://localhost:port/ requests. - Added back-end flags: - Added +rewritehost - Renamed +user/ to +password/ - Allow +options after the domain name - Experimental support for the finger and IRC protocols. - Experimental setuptools, .deb and .rpm packaging rules. - Experimental --remoteui to facilitate development of GUIs. v0.4.3 2011.05.26 ------ - UI is more colorful! - UI is more friendly on Windows and in OS X transient windows. - UI now gives useful feedback in front-end mode as well. - UI now reports https:// URLs when they are available. - Added --add, --only, --remove and --disable for manipulating your kite configuration from the command-line. - HTTP Basic Auth can now be required for any HTTP back-end. - Back-ported from 0.3.20: - Fixed more file-descriptor leaks. - Fixed a bug in initial handshake when front-end was using python's SSL. - Fixed infinite recursion bug in loopback tunnels. - Made auxillary threads handle exceptions more gracefully. - Made connection hand-shake more verbose (prep. better error reporting). - Added --debugio flag for low-level debugging. v0.4.2 2011.05.13 ------ - Fixed some file descriptor leaks - Added name-based virtual server and virtual file tree to built in HTTPD. - Revamped the command-line short-cuts to follow the common Unix 'action source source ... destination' pattern. v0.4.1 2011.05.05 ------ - Branched major revision 0.4.x from stable 0.3.x. - Much improved interactive console user interface and shortcut feature. To disable the interface, use --nullui. - Added built-in static HTTP daemon. v0.3.17 2011.04.20 ------- - Crypto cleanup: better random numbers, clarified code, added timestamps to front-end challenges (limits replay attack window), allowed hardcoding of front-end SSL cert hash in config file. - Fixed hanging SSL connections on front-ends with native termination. - Rapid network switching should work (session-id based disconnects). - Minor flow-control tweaks and fixes. - Fixed a bug where large file transfers could disconnect tunnels. - Fixed some logging issues on Windows. v0.3.16 2011.03.11 ------- - Worked around bug in native Python ssl module which kills busy tunnels. - Fixed lame bug in --all code. v0.3.15 2011.03.03 ------- - Revamped stream EOF handling, fixing many corner case bugs in the process. - Fixed GitHub issue #12 v0.3.14 2011.02.11 ------- - Moved fancy error messages to a frame, instead of a redirect. - Added support for catch-all backends (hostname = unknown). - Added timeouts to tunnel and backend connection code to reduce stalling. - Moved tunnel management to separate thread. - Added --rawports=virtual for virtual (HTTP CONNECT only) raw ports. v0.3.13 2011.01.25 ------- - Fixed yet another flow-control problem (bad error handling) v0.3.12 2011.01.21 ------- - Report a config error when the same backend is defined twice. - Don't submit crash reports when misconfigured. *sigh* v0.3.11 2011.01.20 ------- - Removed debugging code to improve privacy. - Reduced memory footprint slightly, especially on the front-end. - Fixed bugs in 3rd party dynamic DNS support, improved docs. v0.3.10 2011.01.15 ------- - BUGFIX: More improvements to IO error handling. v0.3.9 2011.01.05 ------ - BUGFIX: 0.3.8 broke Windows connections, this should fix them again. - Re-opens logs on SIGHUP, for compatibility with logrotate. - Tweaked internal CONNECT to work with HTTP/1.1 clients: putty can ssh! - Look for CA Certificates in the rc-file if not found in the host OS. - Added --errorurl for fancier "back-end unavailable" messages. - Better detection of dead tunnels and connection re-establishment. v0.3.8 2011.01.02 ------ - Many TLS/SSL fixes: - Works with pyOpenSSL or the default Python 2.6 ssl module. - Can now terminate/unwrap TLS/SSL at the front-end. - Routing support for the old lame SSLv2. - Built-in TLS/SSL works with pyOpenSSL or python 2.6+ ssl. - TLS tunnels: encryption and FE auth. See --ca_certs and --fe_certname. - Protocol fixes: switching from "magic" request paths to HTTP CONNECT. - Added --noprobes and probe logging at the back-end. - Misc. minor bugfixes. v0.3.7 2010.12.26 v0.3.6 ------ - Added support for the websocket protocols (Upgrade: WebSocket header) - Added support for binding to, and routing by ports as well as protocols - Added time-based routing of non-SNI SSL connections. - Added time-based routing of raw ports (for ssh-after-HTTP). - Added X-Forwarded-For header to for HTTP and WEBSOCKET - The IP address of visiters now gets reported to back-end and logged. - Built-in httpd now based on SimpleXMLRPCServer - Enbled --pemfile, for SSL encrypted admin consoles - Front-ends can now have local (non-tunneled) back-ends v0.3.5 2010.12.15 ------ - Misc. minor bugfixes. - Added support for WebDAV and other missing HTTP request methods. - Added some real Yamon variables for monitoring - Log-format normalized a bit, created pagekite_logparse.py. - Bugfix: minor memory leak when target servers are down (BE unavailable). - Bugfix: bad flow-control bug could freeze the select-loop. v0.3.4 2010.11.09 ------ - Added basic flow-control to avoid excessive memory use on large file transfers with fast backends and slow upstream pipes. v0.3.3 2010.11.03 ------ - Fixed crash report misbehavior on some Python versions. v0.3.2 2010.10.25 ------ - HTTP UI now has logs & connection details, and --httppass works. - Anonymized IP addresses in HTTP UI and all logs. - Protocol tweaks: front-end is backwards compatible, back-end is not. - Added support for probe requests, showing status in the UI. v0.3.1 2010.10.14 v0.3.0 ------ * BUG: ValueErrors in invalid configs generated crash report spam. * BUG: Fixed chunking alignment problem. * BUG: Fixed HTTP header parsing problem - Added support for tunneling through tor, or other socks5 proxies. - Added support for zlib compressed tunnels - Added basic unit-tests! - Added crash report feature and auto-restart on crash. v0.2.1 2010.10.12 ------ - Added support for --defaults and --settings - Renamed from beanstalks_net.py to pagekite.py v0.2.0 2010.09.22 ------ - First alpha-testing release. pagekite-0.5.8a/doc/DEV-HOWTO.md0000664000175000017500000000132312603542201015417 0ustar brebre00000000000000## HowTo for developers ## ### Getting started ### $ git clone https://github.com/pagekite/PyPagekite.git $ git clone https://github.com/pagekite/PyBreeder.git $ git clone https://github.com/pagekite/PySocksipyChain.git $ cd PyPagekite $ $(make dev) # sets up the environment $ ./pk # run the local code $ make # run tests, build distributable "binary" ### Exploring the code ### PageKite has is still being refactored from its original form as one big giant Python script. The code mostly lives in the `pagekite/` directory and its subdirectories. Some utilities and custom applications built on top of PageKite live in `scripts/`, and documentation is in `doc/`. pagekite-0.5.8a/doc/lapcat.10000664000175000017500000000400312603542201015105 0ustar brebre00000000000000.\" Hey, EMACS: -*- nroff -*- .\" First parameter, NAME, should be all caps .\" Second parameter, SECTION, should be 1-8, maybe w/ subsection .\" other parameters are allowed: see man(7), man(1) .TH LAPCAT 1 "2011-07-31" .\" Please adjust this date whenever revising the manpage. .\" .\" Some roff macros, for reference: .\" .nh disable hyphenation .\" .hy enable hyphenation .\" .ad l left justify .\" .ad b justify to both left and right margins .\" .nf disable filling .\" .fi enable filling .\" .br insert line break .\" .sp insert n+1 empty lines .\" for manpage-specific macros, see man(7) .SH NAME lapcat \- Location Aware Proxy Chooser And Tunneler .SH SYNOPSIS .B lapcat .RI [ options ] .SH DESCRIPTION .PP \fBlapcat\fP is a netcat-like tool which opens up a TCP/IP connection to a particular host, on a particular port. How the connection is established depends on a set of rules which may vary depending on host, active network connection and availability. .SH OPTIONS .P -v Enable debug output. .P -v -v Enable even more debug output. .SH MODES Lapcat operates in one of 4 different modes: netcat mode, HTTP Proxy mode, dedicated mode and one-off mode. Each are described below. In netcat mode, lapcat will connect to the remote host and relay data back and forth to the standard input and output. This is similar to using the telnet or netcat tools. .SH AUTHOR .P Written by Bjarni R. Einarsson for The Beanstalks Project ehf. and PageKite . .SH CONFIGURATION FIXME: Write this! .SH SEE ALSO .P pagekite(1), , .SH COPYRIGHT .P Copyright © 2011 Bjarni R. Einarsson and The Beanstalks Project ehf. .P License: AGPLv3+, GNU Affero GPL version 3 or later . This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. pagekite-0.5.8a/scripts/0000775000175000017500000000000012610153761014513 5ustar brebre00000000000000pagekite-0.5.8a/scripts/installer.sh0000775000175000017500000000565112603542202017050 0ustar brebre00000000000000#!/bin/bash #

This is the PageKite mini-installer!

# Run with: curl http://pagekite.net/pk/ |sudo bash #
# or just: curl http://pagekite.net/pk/ |bash #



###############################################################################
# Check if SSL works
if [ "$(which curl)" == "" ]; then
    cat </dev/null; then
        cat </dev/null 2>&1 || DEST=/usr/bin
if [ ! -d "$DEST" ]; then
  mkdir -p "$DEST" >/dev/null 2>&1 || true
fi
if [ ! -w "$DEST" ]; then
  [ -w "$HOME/bin" ] && DEST="$HOME/bin" || DEST="$HOME"
fi
DESTFILE="$DEST/pagekite.py"
PAGEKITE="$DESTFILE"
echo ":$PATH:" |grep -c :$DEST: >/dev/null 2>&1 && PAGEKITE=pagekite.py
export DESTFILE

DESTFILE_GTK=
echo 'import gtk' |python 2>/dev/null && DESTFILE_GTK="$DEST/pagekite-gtk.py"
PAGEKITE_GTK="$DESTFILE_GTK"
echo ":$PATH:" |grep -c :$DEST: >/dev/null 2>&1 && PAGEKITE_GTK=pagekite-gtk.py
export DESTFILE_GTK

###############################################################################
# Install!
(
  set -x
  curl https://pagekite.net/pk/pagekite.py >"$DESTFILE"  || exit 1
  chmod +x "$DESTFILE"                                   || exit 2
  if [ "$DESTFILE_GTK" != "" ]; then
    curl https://pagekite.net/pk/pagekite-gtk.py >"$DESTFILE_GTK" || exit 3
    chmod +x "$DESTFILE_GTK"                                      || exit 4
  fi
)\
 && cat <
 Welcome to PageKite!

PageKite has been installed to $DESTFILE !

Some useful commands:

  $ $PAGEKITE --signup             # Sign up for service
  $ $PAGEKITE 80 NAME.pagekite.me  # Expose port 80 as NAME.pagekite.me

For further instructions:

  $ $PAGEKITE --help |less

tac
if [ "$PAGEKITE" != "pagekite.py" ]; then
  echo 'To install system-wide, run: '
  echo
  echo "  $ sudo mv $PAGEKITE /usr/local/bin"
  echo
fi
if [ "$DESTFILE_GTK" != "" ]; then
  cat </dev/null 2>&1 \
  || HAVE_TLS=""
$PKF --clean $PKARGS "--tls_endpoint=a:$0" --settings >/dev/null 2>&1 \
  || HAVE_TLS=""
echo "$HAVE_TLS"


###############################################################################

__logwait() {
  COUNT=0
  while [ 1 ]; do
    [ -e "$1" ] && grep "$2" $1 >/dev/null && return 0
    perl -e 'use Time::HiRes qw(sleep); sleep(0.2)'
    let COUNT=$COUNT+1
    [ $COUNT -gt 30 ] && {
      echo -n ' TIMED OUT! '
      return 1
    }
  done
}
__TEST__() { echo -n " * $1 ..."; shift; rm -f "$@"; touch "$@"; }
__PART_OK__() { echo -n " ok:$1"; }
__TEST_OK__() { echo ' OK'; }
__TEST_FAIL__() { echo " FAIL:$1"; shift; kill "$@"; exit 1; }
__TEST_END__() { echo; kill "$@"; }

###############################################################################
__TEST__ "Basic FE/BE/HTTPD setup" "$LOG-1" "$LOG-2" "$LOG-3" "$LOG-4"

  FE_ARGS="$PKARGS $PKA --isfrontend --ports=$PORT --domain=*:testing:ok"
  [ "$HAVE_TLS" = "" ] || FE_ARGS="$FE_ARGS --tls_endpoint=testing:$0 \
                                            --tls_default=testing"
 ($PKF $FE_ARGS --settings
  $PKF $FE_ARGS --logfile=stdio 2>&1) >$LOG-1 2>&1 &
  KID_FE=$!
__logwait $LOG-1 listen=:$PORT || __TEST_FAIL__ 'setup:FE' $KID_FE

  BE_ARGS1="$PKA --frontend=localhost:$PORT \
                 --backend=http:testing:localhost:80:ok"
  [ "$PKF" = "$PKB" ] && BE_ARGS1="$PKARGS $BE_ARGS1"
  [ "$HAVE_TLS" = "" ] || BE_ARGS1="$BE_ARGS1 --fe_certname=testing"
  if [ $(echo $PKB |grep -c 0.3.2) = "0" ]; then
      TESTINGv3="no"
      BE_ARGS2="/etc/passwd $LOG-4 http://testing/"
  else
      TESTINGv3="yes"
      BE_ARGS2=""
  fi

 ($PKB $BE_ARGS1 --settings $BE_ARGS2
  $PKB $BE_ARGS1 --logfile=stdio $BE_ARGS2 2>&1) >$LOG-2 2>&1 &
  KID_BE=$!
__logwait $LOG-2 domain=testing || __TEST_FAIL__ 'setup:BE' $KID_FE $KID_BE

  # First, make sure we get a Sorry response for invalid requests.
  curl -v --silent -H "Host: invalid" http://localhost:$PORT/ 2>&1 \
    |tee $LOG-3 |grep -i 'sorry! (fe)' >/dev/null \
    && __PART_OK__ 'frontend' || __TEST_FAIL__ 'frontend' $KID_FE $KID_BE

  # Next, see if our test host responds at all...
  curl -v --silent -H "Host: testing" http://localhost:$PORT/ 2>&1 \
    |tee -a $LOG-3 |grep -i '/dev/null \
    && __PART_OK__ 'backend' || __TEST_FAIL__ 'backend' $KID_FE $KID_BE

  if [ "$TESTINGv3" = "no" ]; then
    # See if expected content is served.
    curl -v --silent -H "Host: testing" http://localhost:$PORT/etc/passwd 2>&1 \
      |tee -a $LOG-3 |grep -i 'root' >/dev/null \
      && __PART_OK__ 'httpd' || __TEST_FAIL__ 'httpd' $KID_FE $KID_BE

    # Check large-file download
    dd if=/dev/urandom of=$LOG-4 bs=1M count=1 2>/dev/null
    (echo; echo EOF;) >>$LOG-4
    curl -v --silent -H "Host: testing" http://localhost:$PORT$LOG-4 2>&1 \
      |tail -3|tee -a $LOG-3 |grep 'EOF' >/dev/null \
      && __PART_OK__ 'bigfile' || __TEST_FAIL__ 'bigfile' $KID_FE $KID_BE
  fi

  rm -f "$LOG-1" "$LOG-2" "$LOG-3" "$LOG-4"
__TEST_END__ $KID_FE $KID_BE


###############################################################################




exit 0
##[ Test certificates follow ]#################################################

-----BEGIN RSA PRIVATE KEY-----
MIICXwIBAAKBgQDId+cQqU0fR9sxaP96ukUdpdYMDXU7hyl/7AGTz6RkpQWzFRFr
8OwHKLLzMQMTCv31WtrjxtEWm/3mJcePCajcukfb9aXSGtMG06btwZyNDbp9H2No
Qkzspg4o86tLo6NY4ts4qTUJQJVrvcwW27n2FZhJFzU6EIzPmCzJviBYiwIDAQAB
AoGBALIHUYvJXnUuIiniHiiGrYSj1tBDT147LY6uL8RtvYenycT9K8iZX3MIIMu6
Ngm+VESFmCh6UwtqIvQ1juCnam5vGFoJoFwNKkPgXVDaXLF1UvgT9eknUMvCI757
wLsNy8rTJqzhUeBwiJvloi8vTQ4emFzt3/QWWtOrsHGi1A+JAkEA+mnZGxeA6uHM
dNatMSkOxSQP1/gbBTS0SkoYa5XiGvOht/wPBn6xobkOXvi9ZoU5Wfh4eS0wH+Gf
Ik2lelWcrQJBAMzwz1no6BzGw6RWC9y8uJzV5owcgW5MCOTcsHcOUFdTmAxIMgqP
B3JFwakiY0X0qoZCSmc/e5NGUTbTpHWX+RcCQQDEpxlbgEK6sqaI3wpWAANcaGyU
04AMv44ShUvWOXe+aLQIs8bs99PxyE1z4e2DtH4MnOenaghQETSSkN2yS8dlAkEA
l07LqDP++w/87d3hkC19l72NI7EAFnDouB//4UaeJns/bQH4gDctZj7+RmNvK/0B
0XIsAKKsGAX4fCQx7egwLQJBAKHzGacCxAqBzA7Vnr/vPtA8mJVAYXsDibbYMpVC
HT9ybtKfqL4HHWZfOmUYc9qUtS4jmRnsRVjFuNDMbO80bT4=
-----END RSA PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIIDIjCCAougAwIBAgIJAM5iMtoXM7wvMA0GCSqGSIb3DQEBBQUAMGoxCzAJBgNV
BAYTAklTMRIwEAYDVQQIEwlUZXN0c3RhdGUxEjAQBgNVBAcTCVRlc3R2aWxsZTEP
MA0GA1UEChMGVGVzdGNvMRAwDgYDVQQLEwdUZXN0ZXJzMRAwDgYDVQQDEwd0ZXN0
aW5nMB4XDTExMDcwNjE5NDM1N1oXDTIxMDcwMzE5NDM1N1owajELMAkGA1UEBhMC
SVMxEjAQBgNVBAgTCVRlc3RzdGF0ZTESMBAGA1UEBxMJVGVzdHZpbGxlMQ8wDQYD
VQQKEwZUZXN0Y28xEDAOBgNVBAsTB1Rlc3RlcnMxEDAOBgNVBAMTB3Rlc3Rpbmcw
gZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMh35xCpTR9H2zFo/3q6RR2l1gwN
dTuHKX/sAZPPpGSlBbMVEWvw7AcosvMxAxMK/fVa2uPG0Rab/eYlx48JqNy6R9v1
pdIa0wbTpu3BnI0Nun0fY2hCTOymDijzq0ujo1ji2zipNQlAlWu9zBbbufYVmEkX
NToQjM+YLMm+IFiLAgMBAAGjgc8wgcwwHQYDVR0OBBYEFLoSm4Mq/Wt5MOYyb5Dp
L246YgDWMIGcBgNVHSMEgZQwgZGAFLoSm4Mq/Wt5MOYyb5DpL246YgDWoW6kbDBq
MQswCQYDVQQGEwJJUzESMBAGA1UECBMJVGVzdHN0YXRlMRIwEAYDVQQHEwlUZXN0
dmlsbGUxDzANBgNVBAoTBlRlc3RjbzEQMA4GA1UECxMHVGVzdGVyczEQMA4GA1UE
AxMHdGVzdGluZ4IJAM5iMtoXM7wvMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEF
BQADgYEAjLF30yL6HBmbAEMcylPBRYgO4S951jOB+u4017sD2agiDd1cip2K8ND9
DaLCv7c3MWgzR9/EQmi0BMyhNxtddPF+FZ9RgK3H0bOWlrN5u+MhIHhSMUAp8tdk
pD3zEbiDGGOZi5zjAYXUZtCOZTVcGz3IS42dX9RDNZIrIE1Lb/I=
-----END CERTIFICATE-----
pagekite-0.5.8a/scripts/lapcat0000775000175000017500000003743512603542202015713 0ustar  brebre00000000000000#!/usr/bin/python
__DOC__ = """\
# Copyright 2011, Bjarni R. Einarsson 
# License: AGPLv3
#
# lapcat: Location Aware Proxy Chooser And Tunneler
#         a.k.a. Netcat for your Laptop.
#
# This is a netcat-like tool for opening up a TCP connection to some port
# on some host, where the connection strategy depends on where you are.
#
# Requirements:
#   Python 2.x or 3.x
#   PySocksipyChain, 
#
##############################################################################
#
# For example, say we want 'ssh homeserver' to behave like so:
#
#   - When at home, connect directly (fast!)
#   - At work, use the local HTTP Proxy and PageKite (fast!)
#   - From anywhere else, use a Tor hidden service (private!)
#
# With lapcat, this is possible by defining the following rules in a file
# named ~/.lapcat/homeserver (use lapcat -N to generate network IDs).
#
#   [home]
#   if network = 10.1.2.254/aa:bb:cc:dd:ee:ff
#   host = homeserver.local
#   chain = none
#   priority = 1
#
#   [work]
#   if network = 192.168.55.254/gw:ma:ca:dd:re:ss
#   host = homeserver.pagekite.me
#   chain = http:proxy.corp:8080, http:homeserver.pagekite.me:443
#   priority = 1
#
#   [default]
#   host = 12345123451234512345.onion
#   chain = socks5:localhost:9050
#   priority = 100
#
# Then add the following to ~/.ssh/config
#
#   Host homeserver homeserver.pagekite.me
#     CheckHostIP no
#     ProxyCommand /path/to/lapcat homeserver 22
#
"""
import getopt, os, select, socket, subprocess, sys
import sockschain as socks

def DebugPrint(text):
  sys.stderr.write(text+'\n')
  sys.stderr.flush()

global TRACE
global DEBUG
TRACE = DEBUG = False
TRACE = False

SYS_CONF_DIR = '/etc/lapcat'
USER_CONF_DIR = '~/.lapcat'
IMPORT_KEYWORD = 'import'
DEFAULT_RULE = 'default'
DEFAULT_CHAIN = 'default'

V_ACTIVE = 'active'
V_CHAIN = 'chain'
V_DEFAULT_CHAIN = 'default chain'
V_FINAL = 'final'
V_HOST = 'host'
V_PORT = 'port'
V_PRIORITY = 'priority'
V_TEST_COMMAND = 'test command'
V_TEST_HOST = 'if host'
V_TEST_PORT = 'if port'
V_TEST_NETWORK = 'if network'
VARIABLE_DEFAULTS = {
  V_ACTIVE: True,
  V_CHAIN: DEFAULT_CHAIN,
  V_DEFAULT_CHAIN: None,
  V_HOST: '%h',
  V_PORT: '%p',
  V_FINAL: False,
  V_TEST_COMMAND: None,
  V_TEST_HOST: None,
  V_TEST_PORT: None,
  V_TEST_NETWORK: None,
  V_PRIORITY: 100
}


def Run(argv):
  return subprocess.Popen(argv, stdout=subprocess.PIPE
                          ).communicate()[0].decode().splitlines()

def RunTest(command):
  try:
    if DEBUG: DEBUG("Running: %s" % command)
    retcode = subprocess.call(command, shell=True)
    if DEBUG:
      if retcode < 0:
        DEBUG("Child was terminated by signal: %s" % -retcode)
      else:
        DEBUG("Child returned: %s" % retcode)
    return (retcode == 0)
  except OSError:
    if DEBUG: DEBUG("Execution failed: %s" % (sys.exc_info(), ))
    return False


def GetNetworkId():
  # FIXME: This probably only works on Linux/IPv4 !

  gateway = 'unknown'
  for line in Run(['netstat', '-rn']):
    if line.startswith('0.0.0.0'):
      gateway = line.split()[1].lower()

  network = 'unknown'
  if gateway != 'unknown':
    for line in Run(['arp', '-n', gateway]):
      if line.lower().startswith(gateway):
        network = line.split()[2].lower()

  if DEBUG: DEBUG("Network is: %s/%s" % (gateway, network))
  return '%s/%s' % (gateway, network)


class LapCatConfig(object):
  def __init__(self, hostname, portnum, network):
    self.hostname = hostname
    self.portnum = str(int(portnum))
    self.network = network
    self.rules = {DEFAULT_RULE: {}}
    self.rules[DEFAULT_RULE].update(VARIABLE_DEFAULTS)

  def sysConfig(self, name=None):
    return os.path.join(SYS_CONF_DIR, name or self.hostname)

  def userConfig(self, name=None):
    return os.path.join(os.path.expanduser(USER_CONF_DIR),
                        name or self.hostname)

  def globalConfigs(self):
    """List all global configuration files, in order of preference."""
    configs = []
    for order, dirn in ( ('0', SYS_CONF_DIR),
                         ('1', os.path.expanduser(USER_CONF_DIR)) ):
      try:
        for fn in os.listdir(dirn):
          try:
            pri, rest = fn.split('-', 1)
            pri = '%3.3d-%s' % (int(pri), order)
            configs.append((pri, os.path.join(dirn, fn)))
          except ValueError:
            pass
      except:
        if DEBUG: DEBUG("%s: %s" % (dirn, sys.exc_info()))

    configs.sort(key=lambda k: k[0])
    if DEBUG: DEBUG('Configs are: %s' % configs)
    return [cfg[1] for cfg in configs]

  def load(self, filename=None, require=False, wildcards=False):
    """Load and parse a rule configuration file."""
    filename = filename or self.userConfig()
    try:
      fd = open(filename, 'r')
      if DEBUG: DEBUG("Loading: %s" % filename)
    except:
      fd = None
      if wildcards:
        filedir = os.path.dirname(filename)
        parts = os.path.basename(filename).split('.')
        while len(parts) > 0:
          parts[0] = '_ANY_'
          try:
            filename = os.path.join(filedir, '.'.join(parts))
            fd = open(filename, 'r')
            if DEBUG: DEBUG("Loading: %s" % filename)
            break
          except:
            parts.pop(0)

      if not fd:
        if not require: return self
        raise

    section = self.rules[DEFAULT_RULE]
    count = 0
    for line in fd:
      count += 1
      line = line.strip()

      if line == '' or line.startswith('#'):
        pass

      elif line.startswith('[') and line.endswith(']'):
        secname = line[1:-1]
        if secname == '':
          raise ValueError(('%s(line=%s): Null section') % (filename, count))
        elif secname not in self.rules:
          self.rules[secname] = {}
        section = self.rules[secname]

      elif line.startswith(IMPORT_KEYWORD):
        files = [self.sysConfig(name=line[len(IMPORT_KEYWORD)+1:]),
                 self.userConfig(name=line[len(IMPORT_KEYWORD)+1:])]
        loaded = False
        for fn in files:
          try:
            self.load(filename=fn, require=True)
            loaded = True
          except IOError:
            pass
        if not loaded:
          raise ValueError(('%s(line=%s): File not found, tried: %s'
                            ) % (filename, count, files))

      elif '=' in line:
        var, value = line.split('=')

        var = var.strip().lower()
        if var not in VARIABLE_DEFAULTS:
          raise ValueError(('%s(line=%s): Unknown variable: %s'
                            ) % (filename, count, var))

        value = value.strip()
        if value.lower() in ('true', 'yes'): value = True
        elif value.lower() in ('false', 'no'): value = False
        section[var] = value

      else:
        raise ValueError(('%s(line=%s): Invalid line') % (filename, count))

    return self

  def configure(self):
    """Load all the rules pertaining to this host:port."""
    for config in self.globalConfigs():
      self.load(filename=config, require=True)
    self.load(filename=self.sysConfig(),  require=False, wildcards=True)
    self.load(filename=self.userConfig(), require=False, wildcards=True)
    return self

  def ruleOrder(self):
    """Calculate the order in which to evaluate our rules."""
    keys = [r for r in self.rules]
    keys.sort(key=lambda rule: int(self.rules[rule].get(V_PRIORITY, 999)))
    if DEBUG: DEBUG('Rule order: %s' % keys)
    return keys

  def test(self, rule):
    """Test whether a particular rule matches."""
    if not (rule.get(V_ACTIVE, True) or rule.get(V_DEFAULT_CHAIN, False)):
      return False

    try:
      hosts = (rule.get(V_TEST_HOST, '') or self.hostname).lower().split(', ')
      if self.hostname.lower() not in hosts: return False

      ports = (rule.get(V_TEST_PORT, '') or self.portnum).lower().split(', ')
      if self.portnum not in ports: return False

      ntwks = (rule.get(V_TEST_NETWORK, '') or self.network).split(', ')
      if self.network not in ntwks: return False

      if rule.get(V_TEST_COMMAND, False):
        return RunTest(rule[V_TEST_COMMAND])
      else:
        return True
    except:
      return False

  def connect(self):
    """Connect to the host:port."""
    rules = self.ruleOrder()

    for ruleName in rules:
      rule = self.rules[ruleName]
      if self.test(rule):

        if rule.get(V_DEFAULT_CHAIN, False):
          if DEBUG: DEBUG("Configuring default proxy chain: %s" % rule)
          socks.setdefaultproxy()
          for proxy in rule[V_DEFAULT_CHAIN].split(', '):
            socks.adddefaultproxy(*socks.parseproxy(proxy))

        if rule.get(V_CHAIN, False) and rule.get(V_ACTIVE, True):
          try:
            host = (rule.get(V_HOST, '') or self.hostname
                    ).replace('%h', self.hostname)
            port = (rule.get(V_PORT, '') or self.portnum
                    ).replace('%p', self.portnum)

            sock = socks.socksocket(socket.AF_INET, socket.SOCK_STREAM)
            for proxy in rule.get(V_CHAIN, DEFAULT_CHAIN).split(', '):
              sock.addproxy(*socks.parseproxy(proxy.strip()
                                              .replace('%h', host)
                                              .replace('%p', port)))
            sock.connect((host, int(port)))
            if DEBUG: DEBUG('Connected! [%s]' % ruleName)
            return sock

          except:
            if DEBUG: DEBUG('connect(%s) failed: %s' % (ruleName,
                                                        sys.exc_info()))
            if rule.get(V_FINAL, False):
              raise IOError("Connect failed at: %s" % ruleName)

    raise IOError("Connect failed, tried: %s" % rules)


def NetCat(host, port, input_fd, output_fd):
  try:
    network = GetNetworkId()
    socks.netcat(LapCatConfig(host, port, network).configure().connect(),
                 input_fd, output_fd)
  except IOError:
    DebugPrint('%s' % (sys.exc_info(), ))
    sys.exit(1)

def SetProcTitle(title):
  try:
    import setproctitle
    setproctitle.setproctitle(title)
  except:
    pass

def HttpProxy(input_fd, output_fd):
  try:
    # Get the initial request
    request = ''
    loops = 1024
    while not (loops < 1 or
               request.endswith('\n\n') or
               request.endswith('\r\n\r\n')):
      request += os.read(input_fd.fileno(), 1)
      loops -= 1

    if TRACE: TRACE('<<< Got request (l:%s):\n%s<<<\n' % (1024-loops, request))

    # If it is a HTTP CONNECT, we connect directly.
    words = request.split()
    if (len(words) >= 3 and
        words[0].upper() == 'CONNECT' and
        words[2].upper().startswith('HTTP/')):
      output_fd.write('HTTP/1.1 200 Tunnel established\r\n\r\n')
      output_fd.flush()
      host, port = words[1].split(':')
      if DEBUG: DEBUG('Using native lapcat connection to %s:%s' % (host, port))
      SetProcTitle('lapcat: %s:%s' % (host, port))
      NetCat(host, port, input_fd, output_fd)

    # Otherwise, forward this to a real HTTP Proxy for processing.
    elif len(words) > 2:
      if DEBUG: DEBUG('Connecting via. lapcat-http-proxy')
      host = 'lapcat-http-proxy'
      network = GetNetworkId()
      SetProcTitle('lapcat: %s' % words[1])
      conn = LapCatConfig(host, 0, network).configure().connect()
      conn.sendall(request)
      socks.netcat(conn, input_fd, output_fd)

  except (ValueError, IOError):
    DebugPrint('%s' % (sys.exc_info(), ))
    sys.exit(1)


class FileWrapper(object):
  def __init__(self, sock):
    self.sock = sock
  def flush(self): pass
  def close(self): return self.sock.close()
  def write(self, data): return self.sock.send(data)
  def fileno(self): return self.sock.fileno()


def ForkAndListen(outfmt, baseport=0, tries=1, loop=False, relative=False):
  for t in range(0, tries):
    try:
      try:
        srv = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
      except:
        srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
      srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
      srv.bind(('', baseport+t))
      break
    except:
      srv = None

  srv.listen(3)

  if relative:
    sys.stdout.write((outfmt+'\n') % (srv.getsockname()[1]-baseport))
  else:
    sys.stdout.write((outfmt+'\n') % srv.getsockname()[1])

  sys.stdout.flush()
  os.close(sys.stdout.fileno())
  os.close(sys.stdin.fileno())
  if not loop and os.fork() != 0: os._exit(0)

  while True:
    # Wait for a connection...
    i, o, e = select.select([srv], [], [], 15)
    if srv in i:
      client, address = srv.accept()
      if DEBUG: DEBUG('Accepted: %s' % (address, ))
      if not (loop and (os.fork() != 0)):
        srv.close()
        fw = FileWrapper(client)
        return fw, fw
      client.close()
    elif not loop:
      # Or die?
      os._exit(0)


if __name__ == '__main__':
  opts, args = getopt.getopt(sys.argv[1:], 'hl:NPRtvV:',
                             ['listen=', 'tc', 'tp', 'tf=', 'vnc', 'rdp'])

  if len(args) == 1 and ':' in args[0]:
    args = args[0].split(':')

  use_sysdefaults = True
  mode, portadd, inlinefmt, inlineargs = 'netcat', 0, '', {}

  for opt, arg in opts:
    if '-V' == opt:
      opt = '-v'
      sys.stderr = open(arg, 'a')
    if '-v' == opt:
      if DEBUG and socks.DEBUG: TRACE = DebugPrint
      if DEBUG: socks.DEBUG = DebugPrint
      DEBUG = DebugPrint

    if '-N' == opt: mode = 'networkid'
    elif '-P' == opt: mode = 'httpproxy'
    elif '-R' == opt: use_sysdefaults = False
    else:
      if mode not in ('netcat', 'httpproxy'):
        mode = 'invalid'
        break
      elif '-t' == opt:   inlinefmt = '127.0.0.1 %d'
      elif '--tc' == opt: inlinefmt = '127.0.0.1:%d'
      elif '--tp' == opt: inlinefmt = '%d'
      elif '--tf' == opt: inlinefmt = arg
      elif '--rdp' == opt:
        inlinefmt = '127.0.0.1:%d'
        if len(args) == 1: args.append('3389')
      elif '--vnc' == opt:
        inlinefmt, portadd = '127.0.0.1:%d', 5900
        inlineargs = {'baseport': 5900, 'tries': 20, 'relative': True}
        if len(args) == 1: args.append('0')
      elif opt in ('-l', '--listen'):
        inlinefmt = '127.0.0.1:%d'
        inlineargs = {'baseport': int(arg), 'tries': 1, 'loop': True}

  if use_sysdefaults:
    socks.usesystemdefaults()

  # Set up the listener, if necessary...
  if inlinefmt and ((mode == 'netcat' and len(args) == 2) or
                    (mode == 'httpproxy')):
    fin, fout = ForkAndListen(inlinefmt, **inlineargs)
  else:
    fin, fout = sys.stdin, sys.stdout

  # Do proxy stuff!
  if mode == 'netcat' and len(args) == 2:
    NetCat(args[0], portadd + int(args[1].replace(':', '')), fin, fout)
  elif mode == 'httpproxy' and len(args) == 0:
    HttpProxy(fin, fout)

  # Or print information!
  elif mode == 'networkid' and len(args) == 0:
    print('%s' % GetNetworkId())
  elif len(args) == 1 and args[0] in ('-h', '--help'):
    DebugPrint(__DOC__)
  else:
    print((
      '%(p)s: Location Aware Proxy Chooser And Tunneler / NetCat for Laptops\n'
      '\n'
      'Usage: %(p)s [-v [-v]]      host port     # Connect to host:port\n'
      '       %(p)s <-t|--tc|--tp> host port     # Inline proxy mode\n'
      '       %(p)s --tf=     host port     # Inline proxy mode\n'
      '       %(p)s --rdp          host [port]   # Inline RDP proxy mode\n'
      '       %(p)s --vnc          host [screen] # Inline VNC proxy mode\n'
      '       %(p)s -l port        host port     # Local port <=> host proxy\n'
      '       %(p)s -N                           # Show current network ID\n'
      '       %(p)s -P                           # Behave like an HTTP Proxy\n'
      '       %(p)s -h                           # Print instructions\n'
      '\n'
      'To use with ssh, add to ~/.ssh/config:\n'
      '    ProxyCommand %(fp)s %%h %%p\n'
      '    CheckHostIP no\n'
      '\n'
      'Inline use examples:\n'
      '    $ vncviewer `%(p)s --vnc hostname`\n'
      '    $ rdesktop `%(p)s --rdp homebox.pagekite.me`\n'
      '    $ irssi -c localhost -p `%(p)s --tp irc.freenode.net 6667`\n'
    ) % {'fp': os.path.abspath(sys.argv[0]),
         'p': os.path.basename(sys.argv[0])})
    sys.exit(100)

pagekite-0.5.8a/scripts/legacy-testing/0000775000175000017500000000000012610153761017432 5ustar  brebre00000000000000pagekite-0.5.8a/scripts/legacy-testing/pagekite-0.3.21.py0000775000175000017500000041234512603542202022322 0ustar  brebre00000000000000#!/usr/bin/python -u
#
# pagekite.py, Copyright 2010, 2011, the Beanstalks Project ehf.
#                                    and Bjarni Runar Einarsson
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see .
#
#
##[ Maybe TODO: ]##############################################################
#
# Optimization:
#  - Implement epoll() support.
#  - Stress test this thing: when do we need a C rewrite?
#  - Make multi-process, use the FD-over-socket trick? Threads=>GIL=>bleh
#  - Add QoS and bandwidth shaping
#  - Add a scheduler for deferred/periodic processing.
#  - Replace string concatenation ops with lists of buffers.
#
# Protocols:
#  - Make tunnel creation more stubborn (try multiple ports etc.)
#  - Add XMPP and incoming SMTP support.
#  - Replace/augment current tunnel auth scheme with SSL certificates.
#
# User interface:
#  - Enable (re)configuration from within HTTP UI.
#  - More human readable console output?
#
# Bugs?
#  - Front-ends should time-out dead back-ends.
#  - Gzip-related memory issues.
#
#
##[ Hacking guide! ]###########################################################
#
# Hello! Welcome to my source code.
#
# Here's a brief intro to how the program is structured, to encourage people
# to hack and improve.
#
#  * The PageKite object contains the master configuration and some related
#    routines. It takes care of parsing configuration files and implements
#    things like the authentication protocol. It also contains the main event
#    loop, which is select() or epoll() based. In short, it's the boss.
#
#  * The Connections object keeps track of which tunnels and user connections
#    are open at any given time and which protocol/domain pairs they belong to.
#    It gets passed around as an argument quite a lot - not too elegant.
#
#  * The Selectable and it's *Parser subclasses incrementally build up basic
#    parsers for the supported protocols. Note that none of the protocols
#    are fully implemented, we only implement the bare minimum required to
#    figure out which back-end should handle a given request, and then forward
#    the bytes unmodified over that channel. As a result, the current HTTP
#    proxy code is not HTTP 1.1 compliant - but if you put it behind Varnish
#    or some other decent reverse-proxy, then *the combination* should be!
#
#  * The UserConn object represents connections on behalf of users. It can
#    be created as a FrontEnd, which will find the right tunnel and send
#    traffic to the back-end PageKite process, where a BackEnd UserConn
#    will be created to connect to the actual HTTP server.
#
#  * The Tunnel object represents one end of a PageKite tunnel and is also
#    created either as a FrontEnd or BackEnd, depending on which end it is.
#    Tunnels handle multiplexing and demultiplexing all the traffic for
#    a given back-end so multiple requests can share a single TCP/IP
#    connection.
#
# Although most of the work done by pagekite.py happens in an event-loop
# on a single thread, there are some exceptions:
#
#  * The AuthThread handles checking whether an incoming tunnel request is
#    allowed or not; authentication requests may end up blocking and waiting
#    for each other, but the main work of proxying data back and forth won't
#    be blocked.
#
#  * The HttpUiThread implements a basic HTTP (or HTTPS) server, for basic
#    monitoring and interactive configuration.
#
# WARNING: The UI threading code assumes it is running in CPython, where the
#          GIL makes snooping across the thread-boundary relatively safe, even
#          without explicit locking. Beware!
#
###############################################################################
#
PROTOVER = '0.8'
APPVER = '0.3.21'
AUTHOR = 'Bjarni Runar Einarsson, http://bre.klaki.net/'
WWWHOME = 'http://pagekite.net/'
DOC = """\
pagekite.py is Copyright 2010, 2011, the Beanstalks Project ehf.
     v%s                               http://pagekite.net/

This the reference implementation of the PageKite tunneling protocol,
both the front- and back-end. This following protocols are supported:

  HTTP      - HTTP 1.1 only, requires a valid HTTP Host: header
  HTTPS     - Recent versions of TLS only, requires the SNI extension.
  WEBSOCKET - Using the proposed Upgrade: WebSocket method.
  XMPP      - ...unfinished... (FIXME)
  SMTP      - ...unfinished... (FIXME)

Other protocols may be proxied by using "raw" back-ends and HTTP CONNECT.

This program is free software: you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License. For the full text of the
license, see: http://www.gnu.org/licenses/agpl-3.0.html

Usage:

  pagekite.py [options]

Common Options:

 --optfile=X    -o X    Read options from file X. Default is ~/.pagekite.rc.
 --savefile=X   -S X    Read/write options from file X.
 --reloadfile=X         Re-read config from X on SIGHUP.
 --httpd=X:P    -H X:P  Enable the HTTP user interface on hostname X, port P.
 --pemfile=X    -P X    Use X as a PEM key for the HTTPS UI.
 --httppass=X   -X X    Require password X to access the UI.
 --nozchunks            Disable zlib tunnel compression.
 --sslzlib              Enable zlib compression in OpenSSL.
 --buffers       N      Buffer at most N kB of back-end data before blocking.
 --logfile=F    -L F    Log to file F.
 --daemonize    -Z      Run as a daemon.
 --runas        -U U:G  Set UID:GID after opening our listening sockets.
 --pidfile=P    -I P    Write PID to the named file.
 --clean                Skip loading the default configuration file.
 --nocrashreport        Don't send anonymous crash reports to PageKite.net.
 --tls_default=N        Default name to use for SSL, if SNI and tracking fail.
 --tls_endpoint=N:F     Terminate SSL/TLS for name N, using key/cert from F.
 --defaults             Set some reasonable default setings.
 --errorurl=U  -E U    URL to redirect to when back-ends are not found.
 --settings             Dump the current settings to STDOUT, formatted as
                       an options file would be.

Front-end Options:

 --isfrontend   -f      Enable front-end mode.
 --authdomain=X -A X    Use X as a remote authentication domain.
 --host=H       -h H    Listen on H (hostname).
 --ports=A,B,C  -p A,B  Listen on ports A, B, C, ...
 --portalias=A:B        Report port A as port B to backends.
 --protos=A,B,C         Accept the listed protocols for tunneling.
 --rawports=A,B,C       Listen on ports A, B, C, ... (raw/timed connections)

 --domain=proto,proto2,pN:domain:secret
                  Accept tunneling requests for the named protocols and
                 specified domain, using the given secret.  A * may be
               used as a wildcard for subdomains. (FIXME)

Back-end Options:

 --all          -a      Terminate early if any tunnels fail to register.
 --dyndns=X     -D X    Register changes with DynDNS provider X.  X can either
                       be simply the name of one of the 'built-in' providers,
                      or a URL format string for ad-hoc updating.

 --frontends=N:X:P      Choose N front-ends from X (a DNS domain name), port P.
 --frontend=host:port   Connect to the named front-end server.
 --new          -N      Don't attempt to connect to the domain's old front-end.           
 --socksify=S:P         Connect via SOCKS server S, port P (requires socks.py)
 --torify=S:P           Same as socksify, but more paranoid.
 --noprobes             Reject all probes for back-end liveness.
 --fe_certname=N        Connect using SSL, accepting valid certs for domain N.
 --ca_certs=PATH        Path to your trusted root SSL certificates file.

 --backend=proto:domain:host:port:secret
                  Configure a back-end service on host:port, using
                 protocol proto and the given domain. As a special
                case, if host and port are left blank and the proto
               is HTTP or HTTPS, the built-in server will be used.

About the options file:

The options file contains the same options as are available to the command
line, with the restriction that there be exactly one "argument" per line.

The leading '--' may also be omitted for readability, and for the same reason
it is recommended to use the long form of the options in the configuration
file (also, as the short form may not always parse correctly).

Blank lines and lines beginning with # (comments) are stripped from the
options file before it is parsed.  It is perfectly acceptable to have multiple
options files, and options files can include other options files.


Examples:

# Create a config-file with default options, and then edit it.
pagekite.py --defaults --settings > ~/.pagekite.rc
vim ~/.pagekite.rc

# Run pagekite with the HTTP UI, for browsing state over the web.
pagekite.py --httpd=localhost:8888
firefox http://localhost:8888/

# Fly a PageKite on pagekite.net for somedomain.com, and register the new
# front-ends with the No-IP Dynamic DNS provider.
pagekite.py \\
       --frontends=1:frontends.b5p.us:443 \\
       --dyndns=user:pass@no-ip.com \\
       --backend=http:somedomain.com:localhost:80:mygreatsecret

""" % APPVER

MAGIC_PREFIX = '/~:PageKite:~/'
MAGIC_PATH = '%sv%s' % (MAGIC_PREFIX, PROTOVER)
MAGIC_PATHS = (MAGIC_PATH, '/Beanstalk~Magic~Beans/0.2')

OPT_FLAGS = 'o:S:H:P:X:L:ZI:fA:R:h:p:aD:U:NE:'
OPT_ARGS = ['noloop', 'clean', 'nopyopenssl', 'nocrashreport',
            'optfile=', 'savefile=', 'reloadfile=',
            'httpd=', 'pemfile=', 'httppass=', 'errorurl=',
            'logfile=', 'daemonize', 'nodaemonize', 'runas=', 'pidfile=',
            'isfrontend', 'noisfrontend', 'settings', 'defaults', 'domain=',
            'authdomain=', 'authhelpurl=', 'register=', 'host=',
            'noupgradeinfo', 'upgradeinfo=', 'motd=',
            'ports=', 'protos=', 'portalias=', 'rawports=',
            'tls_default=', 'tls_endpoint=', 'fe_certname=', 'ca_certs=',
            'backend=', 'frontend=', 'frontends=', 'torify=', 'socksify=',
            'new', 'all', 'noall', 'dyndns=', 'nozchunks', 'sslzlib',
            'buffers=', 'noprobes', 'debugio',]

DEBUG_IO = False

AUTH_ERRORS           = '255.255.255.'
AUTH_ERR_USER_UNKNOWN = '.0'
AUTH_ERR_INVALID      = '.1'
AUTH_QUOTA_MAX        = '255.255.254.255'

VIRTUAL_PN = 'virtual'
CATCHALL_HN = 'unknown'
LOOPBACK_HN = 'loopback'
LOOPBACK_FE = LOOPBACK_HN + ':1'
LOOPBACK_BE = LOOPBACK_HN + ':2'
LOOPBACK = {'FE': LOOPBACK_FE, 'BE': LOOPBACK_BE}

BE_PROTO = 0
BE_PORT = 1
BE_DOMAIN = 2
BE_BACKEND = 3
BE_SECRET = 4
BE_STATUS = 5

BE_STATUS_OK = 0
BE_STATUS_BE_FAIL = 2
BE_STATUS_NO_TUNNEL = 1
BE_STATUS_DISABLED = -1
BE_STATUS_UNKNOWN = -2

DYNDNS = {
  'pagekite.net': ('http://up.pagekite.net/'
                   '?hostname=%(domain)s&myip=%(ips)s&sign=%(sign)s'),
  'beanstalks.net': ('http://up.b5p.us/'
                     '?hostname=%(domain)s&myip=%(ips)s&sign=%(sign)s'),
  'dyndns.org': ('https://%(user)s:%(pass)s@members.dyndns.org'
                 '/nic/update?wildcard=NOCHG&backmx=NOCHG'
                 '&hostname=%(domain)s&myip=%(ip)s'),
  'no-ip.com': ('https://%(user)s:%(pass)s@dynupdate.no-ip.com'
                '/nic/update?hostname=%(domain)s&myip=%(ip)s'),
}


##[ Standard imports ]########################################################

import base64
from cgi import escape as escape_html
import errno
import getopt
import os
import random
import re
import select
import socket
rawsocket = socket.socket

import struct
import sys
import threading
import time
import traceback
import urllib
import zlib

from SimpleXMLRPCServer import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler


##[ Conditional imports & compatibility magic! ]###############################

# System logging on Unix
try:
  import syslog
except ImportError:
  pass


# Backwards compatibility for old Pythons.
if not 'SHUT_RD' in dir(socket):
  socket.SHUT_RD = 0
  socket.SHUT_WR = 1
  socket.SHUT_RDWR = 2

try:
  sorted([1, 2, 3])
except:
  def sorted(l):
    tmp = l[:]
    tmp.sort()
    return tmp


# SSL/TLS strategy: prefer pyOpenSSL, as it comes with built-in Context
# objects. If that fails, look for Python 2.6+ native ssl support and 
# create a compatibility wrapper. If both fail, bomb with a ConfigError
# when the user tries to enable anything SSL-related.
SEND_MAX_BYTES = 16 * 1024
SEND_ALWAYS_BUFFERS = False
try:
  if '--nopyopenssl' in sys.argv:
    raise ImportError('pyOpenSSL disabled')

  from OpenSSL import SSL
  def SSL_Connect(ctx, sock,
                  server_side=False, accepted=False, connected=False,
                  verify_names=None):
    LogInfo('TLS is provided by pyOpenSSL')
    if verify_names:
      def vcb(conn, x509, errno, depth, rc):
        # FIXME: No ALT names, no wildcards ...
        if errno != 0: return False
        if depth != 0: return True
        commonName = x509.get_subject().commonName.lower()
        cNameDigest = '%s/%s' % (commonName, x509.digest('sha1').replace(':','').lower())
        if (commonName in verify_names) or (cNameDigest in verify_names):
          LogDebug('Cert OK: %s' % (cNameDigest))
          return True
        return False
      ctx.set_verify(SSL.VERIFY_PEER | SSL.VERIFY_FAIL_IF_NO_PEER_CERT, vcb)
    else:
      def vcb(conn, x509, errno, depth, rc): return (errno == 0)
      ctx.set_verify(SSL.VERIFY_NONE, vcb)

    nsock = SSL.Connection(ctx, sock)
    if accepted: nsock.set_accept_state()
    if connected: nsock.set_connect_state()
    if verify_names: nsock.do_handshake()

    return nsock

except ImportError:
  try:
    import ssl

    # Because the native Python ssl module does not expose WantWriteError,
    # we need this to keep tunnels from shutting down when busy.
    SEND_ALWAYS_BUFFERS = True
    SEND_MAX_BYTES = 4 * 1024

    class SSL(object):
      SSLv23_METHOD = ssl.PROTOCOL_SSLv23
      TLSv1_METHOD = ssl.PROTOCOL_TLSv1
      WantReadError = ssl.SSLError
      class Error(Exception): pass
      class SysCallError(Exception): pass
      class WantWriteError(Exception): pass
      class ZeroReturnError(Exception): pass
      class Context(object):
        def __init__(self, method):
          self.method = method
          self.privatekey_file = None
          self.certchain_file = None
          self.ca_certs = None
        def use_privatekey_file(self, fn): self.privatekey_file = fn
        def use_certificate_chain_file(self, fn): self.certchain_file = fn
        def load_verify_locations(self, pemfile, capath=None): self.ca_certs = pemfile

    def SSL_CheckPeerName(fd, names):
      cert = fd.getpeercert()
      certhash = sha1hex(fd.getpeercert(binary_form=True))
      if not cert: return None
      for field in cert['subject']:
        if field[0][0].lower() == 'commonname':
          name = field[0][1].lower()
          namehash = '%s/%s' % (name, certhash)
          if name in names or namehash in names:
            LogDebug('Cert OK: %s' % (namehash))
            return name

      if 'subjectAltName' in cert:
        for field in cert['subjectAltName']:
          if field[0].lower() == 'dns':
            name = field[1].lower()
            namehash = '%s/%s' % (name, certhash)
            if name in names or namehash in names:
              LogDebug('Cert OK: %s' % (namehash))
              return name

      return None

    def SSL_Connect(ctx, sock,
                    server_side=False, accepted=False, connected=False,
                    verify_names=None):
      LogInfo('TLS is provided by native Python ssl')
      reqs = (verify_names and ssl.CERT_REQUIRED or ssl.CERT_NONE)
      fd = ssl.wrap_socket(sock, keyfile=ctx.privatekey_file, 
                                 certfile=ctx.certchain_file,
                                 cert_reqs=reqs,
                                 ca_certs=ctx.ca_certs,
                                 do_handshake_on_connect=False,
                                 ssl_version=ctx.method,
                                 server_side=server_side)
      if verify_names:
        fd.do_handshake()
        if not SSL_CheckPeerName(fd, verify_names):
          raise SSL.Error('Cert not in %s (%s)' % (verify_names, reqs)) 
      return fd

  except ImportError:
    class SSL(object):
      SSLv23_METHOD = 0
      TLSv1_METHOD = 0
      class Error(Exception): pass
      class SysCallError(Exception): pass
      class WantReadError(Exception): pass
      class WantWriteError(Exception): pass
      class ZeroReturnError(Exception): pass
      class Context(object):
        def __init__(self, method):
          raise ConfigError('Neither pyOpenSSL nor python 2.6+ ssl modules found!')


def DisableSSLCompression():
  # Hack to disable compression in OpenSSL and reduce memory usage *lots*.
  # Source:
  #   http://journal.paul.querna.org/articles/2011/04/05/openssl-memory-use/
  try:
    import ctypes
    import glob
    openssl = ctypes.CDLL(None, ctypes.RTLD_GLOBAL)
    try:
      f = openssl.SSL_COMP_get_compression_methods
    except AttributeError:
      ssllib = sorted(glob.glob("/usr/lib/libssl.so.*"))[0]
      openssl = ctypes.CDLL(ssllib, ctypes.RTLD_GLOBAL)

    openssl.SSL_COMP_get_compression_methods.restype = ctypes.c_void_p
    openssl.sk_zero.argtypes = [ctypes.c_void_p]
    openssl.sk_zero(openssl.SSL_COMP_get_compression_methods())
  except Exception, e:
    LogError('disableSSLCompression: Failed: %s' % e)
 

# Different Python 2.x versions complain about deprecation depending on
# where we pull these from.
try:
  from urlparse import parse_qs, urlparse
except ImportError, e:
  from cgi import parse_qs
  from urlparse import urlparse
try:
  import hashlib
  def sha1hex(data):
    hl = hashlib.sha1()
    hl.update(data)
    return hl.hexdigest().lower()
except ImportError:
  import sha
  def sha1hex(data):
    return sha.new(data).hexdigest().lower()


# YamonD is a part of PageKite.net's internal monitoring systems. It's not
# required, so if you don't have it, the mock makes things Just Work.
class MockYamonD(object):
  def __init__(self, sspec, server=None, handler=None): pass
  def vmax(self, var, value): pass
  def vscale(self, var, ratio, add=0): pass
  def vset(self, var, value): pass
  def vadd(self, var, value, wrap=None): pass
  def vmin(self, var, value): pass
  def vdel(self, var): pass
  def lcreate(self, listn, elems): pass
  def ladd(self, listn, value): pass
  def render_vars_text(self): return ''
  def quit(self): pass
  def run(self): pass

gYamon = MockYamonD(())

try:
  import yamond
  YamonD=yamond.YamonD
except Exception:
  YamonD=MockYamonD


##[ PageKite.py code starts here! ]############################################

gSecret = None
def globalSecret():
  global gSecret
  if not gSecret:
    # This always works...
    gSecret = '%8.8x%8.8x%8.8x' % (random.randint(0, 0x7FFFFFFE), 
                                   time.time(),
                                   random.randint(0, 0x7FFFFFFE))

    # Next, see if we can augment that with some real randomness.
    try:
      newSecret = sha1hex(open('/dev/random').read(16) + gSecret)
      gSecret = newSecret
      LogDebug('Seeded signatures using /dev/random, hooray!')
    except:
      try:
        newSecret = sha1hex(os.urandom(64) + gSecret)
        gSecret = newSecret
        LogDebug('Seeded signatures using os.urandom(), hooray!')
      except:
        LogInfo('WARNING: Seeding signatures with time.time() and random.randint()')

  return gSecret

TOKEN_LENGTH=36
def signToken(token=None, secret=None, payload='', timestamp=None,
              length=TOKEN_LENGTH):
  """
  This will generate a random token with a signature which could only have come
  from this server.  If a token is provided, it is re-signed so the original
  can be compared with what we would have generated, for verification purposes.

  If a timestamp is provided it will be embedded in the signature to a
  resolution of 10 minutes, and the signature will begin with the letter 't'

  Note: This is only as secure as random.randint() is random.
  """
  if not secret: secret = globalSecret()
  if not token: token = sha1hex('%s%8.8x' % (globalSecret(),
                                             random.randint(0, 0x7FFFFFFD)+1))
  if timestamp:
    tok = 't' + token[1:]
    ts = '%x' % int(timestamp/600)
    return tok[0:8] + sha1hex(secret + payload + ts + tok[0:8])[0:length-8]
  else:
    return token[0:8] + sha1hex(secret + payload + token[0:8])[0:length-8]

def checkSignature(sign='', secret='', payload=''):
  """
  Check a signature for validity. When using timestamped signatures, we only
  accept signatures from the current and previous windows.
  """
  if sign[0] == 't':
    ts = int(time.time())
    for window in (0, 1):
      valid = signToken(token=sign, secret=secret, payload=payload,
                        timestamp=(ts-(window*600)))
      if sign == valid: return True
    return False
  else:
    valid = signToken(token=sign, secret=secret, payload=payload)
    return sign == valid


class ConfigError(Exception):
  pass

class ConnectError(Exception):
  pass


def HTTP_PageKiteRequest(server, backends, tokens=None, nozchunks=False,
                         tls=False, testtoken=None, replace=None):
  req = ['CONNECT PageKite:1 HTTP/1.0\r\n',
         'X-PageKite-Version: %s\r\n' % APPVER]

  if not nozchunks: req.append('X-PageKite-Features: ZChunks\r\n')
  if replace: req.append('X-PageKite-Replace: %s\r\n' % replace)
  if tls: req.append('X-PageKite-Features: TLS\r\n')
         
  tokens = tokens or {}
  for d in backends.keys():
    if backends[d][BE_BACKEND]:

      # A stable (for replay on challenge) but unguessable salt.
      my_token = sha1hex(globalSecret() + server + backends[d][BE_SECRET]
                         )[:TOKEN_LENGTH]

      # This is the challenge (salt) from the front-end, if any.
      server_token = d in tokens and tokens[d] or ''

      # Our payload is the (proto, name) combined with both salts
      data = '%s:%s:%s' % (d, my_token, server_token)

      # Sign the payload with the shared secret (random salt).
      sign = signToken(secret=backends[d][BE_SECRET],
                       payload=data,
                       token=testtoken)

      req.append('X-PageKite: %s:%s\r\n' % (data, sign))

  req.append('\r\n')
  return ''.join(req)

def HTTP_ResponseHeader(code, title, mimetype='text/html'):
  return ('HTTP/1.1 %s %s\r\nContent-Type: %s\r\nPragma: no-cache\r\n'
          'Expires: 0\r\nCache-Control: no-store\r\nConnection: close'
          '\r\n') % (code, title, mimetype)

def HTTP_Header(name, value):
  return '%s: %s\r\n' % (name, value)

def HTTP_StartBody():
  return '\r\n'

def HTTP_ConnectOK():
  return 'HTTP/1.0 200 Connection Established\r\n\r\n'

def HTTP_ConnectBad():
  return 'HTTP/1.0 503 Sorry\r\n\r\n'

def HTTP_Response(code, title, body, mimetype='text/html', headers=None):
  data = [HTTP_ResponseHeader(code, title, mimetype)]
  if headers: data.extend(headers)
  data.extend([HTTP_StartBody(), ''.join(body)])
  return ''.join(data)

def HTTP_NoFeConnection():
  return HTTP_Response(200, 'OK', base64.decodestring(
    'R0lGODlhCgAKAMQCAN4hIf/+/v///+EzM+AuLvGkpORISPW+vudgYOhiYvKpqeZY'
    'WPbAwOdaWup1dfOurvW7u++Rkepycu6PjwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
    'AAAAAAAAAAAAAAAAACH5BAEAAAIALAAAAAAKAAoAAAUtoCAcyEA0jyhEQOs6AuPO'
    'QJHQrjEAQe+3O98PcMMBDAdjTTDBSVSQEmGhEIUAADs='),
      headers=[HTTP_Header('X-PageKite-Status', 'Down-FE')],
      mimetype='image/gif')

def HTTP_NoBeConnection():
  return HTTP_Response(200, 'OK', base64.decodestring(
    'R0lGODlhCgAKAPcAAI9hE6t2Fv/GAf/NH//RMf/hd7u6uv/mj/ntq8XExMbFxc7N'
    'zc/Ozv/xwfj31+jn5+vq6v///////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
    'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
    'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
    'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
    'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
    'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
    'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
    'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
    'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
    'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
    'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
    'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
    'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
    'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
    'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
    'AAAAAAAAAAAAAAAAACH5BAEAABIALAAAAAAKAAoAAAhDACUIlBAgwMCDARo4MHiQ'
    '4IEGDAcGKAAAAESEBCoiiBhgQEYABzYK7OiRQIEDBgMIEDCgokmUKlcOKFkgZcGb'
    'BSUEBAA7'),
      headers=[HTTP_Header('X-PageKite-Status', 'Down-BE')],
      mimetype='image/gif')
                            
def HTTP_GoodBeConnection():
  return HTTP_Response(200, 'OK', base64.decodestring(
    'R0lGODlhCgAKANUCAEKtP0StQf8AAG2/a97w3qbYpd/x3mu/aajZp/b79vT69Mnn'
    'yK7crXTDcqraqcfmxtLr0VG0T0ivRpbRlF24Wr7jveHy4Pv9+53UnPn8+cjnx4LI'
    'gNfu1v///37HfKfZpq/crmG6XgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
    'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
    'AAAAAAAAAAAAAAAAACH5BAEAAAIALAAAAAAKAAoAAAZIQIGAUDgMEASh4BEANAGA'
    'xRAaaHoYAAPCCZUoOIDPAdCAQhIRgJGiAG0uE+igAMB0MhYoAFmtJEJcBgILVU8B'
    'GkpEAwMOggJBADs='),
      headers=[HTTP_Header('X-PageKite-Status', 'OK')],
      mimetype='image/gif')
 
def HTTP_Unavailable(where, proto, domain, comment='', frame_url=None):
  code, status = 503, 'Unavailable'
  message = ''.join(['

Sorry! (', where, ')

', '

The ', proto.upper(),' ', 'PageKite for ', domain, ' is unavailable at the moment.

', '

Please try again later.

']) if frame_url: if '?' in frame_url: frame_url += '&where=%s&proto=%s&domain=%s' % (where.upper(), proto, domain) return HTTP_Response(code, status, ['', '', '', message, '', '']) else: return HTTP_Response(code, status, ['', message, '']) LOG = [] LOG_LENGTH = 300 LOG_THRESHOLD = 256 * 1024 def LogValues(values, testtime=None): words = [('ts', '%x' % (testtime or time.time()))] words.extend([(kv[0], ('%s' % kv[1]).replace('\t', ' ') .replace('\r', ' ') .replace('\n', ' ') .replace('; ', ', ') .strip()) for kv in values]) wdict = dict(words) LOG.append(wdict) while len(LOG) > LOG_LENGTH: LOG.pop(0) return (words, wdict) def LogSyslog(values, wdict=None, words=None): if values: words, wdict = LogValues(values) if 'err' in wdict: syslog.syslog(syslog.LOG_ERR, '; '.join(['='.join(x) for x in words])) elif 'debug' in wdict: syslog.syslog(syslog.LOG_DEBUG, '; '.join(['='.join(x) for x in words])) else: syslog.syslog(syslog.LOG_INFO, '; '.join(['='.join(x) for x in words])) LogFile = sys.stdout def LogToFile(values, wdict=None, words=None): if values: words, wdict = LogValues(values) LogFile.write('; '.join(['='.join(x) for x in words])) LogFile.write('\n') def LogToMemory(values, wdict=None, words=None): if values: LogValues(values) def FlushLogMemory(): for l in LOG: Log(None, wdict=l, words=[(w, l[w]) for w in l]) Log = LogToMemory def LogError(msg, parms=None): emsg = [('err', msg)] if parms: emsg.extend(parms) Log(emsg) global gYamon gYamon.vadd('errors', 1, wrap=1000000) def LogDebug(msg, parms=None): emsg = [('debug', msg)] if parms: emsg.extend(parms) Log(emsg) def LogInfo(msg, parms=None): emsg = [('info', msg)] if parms: emsg.extend(parms) Log(emsg) # FIXME: This could easily be a pool of threads to let us handle more # than one incoming request at a time. class AuthThread(threading.Thread): """Handle authentication work in a separate thread.""" def __init__(self, conns): threading.Thread.__init__(self) self.qc = threading.Condition() self.jobs = [] self.conns = conns def check(self, requests, conn, callback): self.qc.acquire() self.jobs.append((requests, conn, callback)) self.qc.notify() self.qc.release() def quit(self): self.qc.acquire() self.keep_running = False self.qc.notify() self.qc.release() def run(self): self.keep_running = True while self.keep_running: try: self._run() except Exception, e: LogError('AuthThread died: %s' % e) time.sleep(5) def _run(self): self.qc.acquire() while self.keep_running: now = int(time.time()) if self.jobs: (requests, conn, callback) = self.jobs.pop(0) if DEBUG_IO: print '=== AUTH REQUESTS\n%s\n===' % requests self.qc.release() quotas = [] results = [] session = '%x:%s:' % (now, globalSecret()) for request in requests: try: proto, domain, srand, token, sign, prefix = request except: LogError('Invalid request: %s' % (request, )) continue what = '%s:%s:%s' % (proto, domain, srand) session += what if not token or not sign: # Send a challenge. Our challenges are time-stamped, so we can # put stict bounds on possible replay attacks (20 minutes atm). results.append(('%s-SignThis' % prefix, '%s:%s' % (what, signToken(payload=what, timestamp=now)))) else: # This is a bit lame, but we only check the token if the quota # for this connection has never been verified. (quota, reason) = self.conns.config.GetDomainQuota(proto, domain, srand, token, sign, check_token=(conn.quota is None)) if not quota: results.append(('%s-Invalid' % prefix, what)) results.append(('%s-Invalid-Why' % prefix, '%s;%s' % (what, reason))) elif self.conns.Tunnel(proto, domain): # FIXME: Allow multiple backends? results.append(('%s-Duplicate' % prefix, what)) else: results.append(('%s-OK' % prefix, what)) quotas.append(quota) if (proto.startswith('http') and self.conns.config.GetTlsEndpointCtx(domain)): results.append(('%s-SSL-OK' % prefix, what)) results.append(('%s-SessionID' % prefix, '%x:%s' % (now, sha1hex(session)))) if self.conns.config.motd: results.append(('%s-MOTD' % prefix, self.conns.config.motd)) for upgrade in self.conns.config.upgrade_info: results.append(('%s-Upgrade' % prefix, ';'.join(upgrade))) if quotas: nz_quotas = [q for q in quotas if q and q > 0] if nz_quotas: quota = min(nz_quotas) if quota is not None: conn.quota = [quota, requests[quotas.index(quota)], time.time()] results.append(('%s-Quota' % prefix, quota)) elif requests: if not conn.quota: conn.quota = [None, requests[0], time.time()] else: conn.quota[2] = time.time() if DEBUG_IO: print '=== AUTH RESULTS\n%s\n===' % results callback(results) self.qc.acquire() else: self.qc.wait() self.buffering = 0 self.qc.release() def fmt_size(count): if count > 2*(1024*1024*1024): return '%dGB' % (count / (1024*1024*1024)) if count > 2*(1024*1024): return '%dMB' % (count / (1024*1024)) if count > 2*(1024): return '%dKB' % (count / 1024) return '%dB' % count class UiRequestHandler(SimpleXMLRPCRequestHandler): # Make all paths/endpoints legal, we interpret them below. rpc_paths = ( ) TEMPLATE_TEXT = ('%(body)s') TEMPLATE_HTML = ('\n' '\n' '%(title)s - %(prog)s v%(ver)s\n' '\n' '

%(title)s

\n' '
%(body)s
\n' '\n' '\n') def setup(self): if self.server.enable_ssl: self.connection = self.request self.rfile = socket._fileobject(self.request, "rb", self.rbufsize) self.wfile = socket._fileobject(self.request, "wb", self.wbufsize) else: SimpleXMLRPCRequestHandler.setup(self) def log_message(self, format, *args): Log([('uireq', format % args)]) def html_overview(self): conns = self.server.conns backends = self.server.pkite.backends html = [( '

Welcome to your PageKite control panel!

\n' '\n' '

Flying kites:

    \n' )] for tid in conns.tunnels: proto, domain = tid.split(':') if '-' in proto: proto, port = proto.split('-') if tid in backends: backend = backends[tid][BE_BACKEND] if proto.startswith('http'): binfo = '%s' % (proto, backend, backend) else: binfo = '%s' % backend else: binfo = 'none' if proto.startswith('http'): tinfo = '%s: %s' % (proto, proto, domain, domain) else: tinfo = '%s: %s' % (proto, domain) for tunnel in conns.tunnels[tid]: html.append(('
  • %s' ' (%s to' ' %s,' ' %s in, %s out)' '
  • \n') % (tinfo, tunnel.server_info[tunnel.S_NAME].split(':')[0], binfo, fmt_size(tunnel.all_in + tunnel.read_bytes), fmt_size(tunnel.all_out + tunnel.wrote_bytes))) if not conns.tunnels: html.append('None') html.append( '
\n' ) return { 'title': 'Control Panel', 'body': ''.join(html) } def txt_log(self): return '\n'.join(['%s' % x for x in LOG]) def html_log(self, path): debug = path.find('debug') >= 0 httpd = path.find('httpd') >= 0 alllog = path.find('all') >= 0 html = ['' ''] lines = [] for line in LOG: if not alllog and ('debug' in line) != debug: continue if not alllog and ('uireq' in line) != httpd: continue keys = line.keys() keys.sort() lhtml = ('' '' % time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(int(line['ts'], 16)))) for key in keys: if key != 'ts': lhtml += ('' '' % (key, escape_html(line[key]))) lines.insert(0, lhtml) html.extend(lines) html.append('
%s
%s =%s
') return { 'title': 'Log viewer, recent events', 'body': ''.join(html) } def html_conns(self): html = ['
    '] sids = SELECTABLES.keys() sids.sort(reverse=True) for sid in sids: sel = SELECTABLES[sid] html.append('
  • %s%s' ' ' % (sid, escape_html(str(sel)), sel.dead and ' ' or ' alive')) html.append('
') return { 'title': 'Connection log', 'body': ''.join(html) } def html_conn(self, path): sid = int(path[len('/conn/'):]) if sid in SELECTABLES: html = ['

%s

' % escape_html('%s' % SELECTABLES[sid]), SELECTABLES[sid].__html__()] else: html = ['

Connection %s not found. Expired?

' % sid] return { 'title': 'Connection details', 'body': ''.join(html) } def begin_headers(self, code, mimetype): self.send_response(code) self.send_header('Cache-Control', 'no-store') self.send_header('Pragma', 'no-cache') self.send_header('Content-Type', mimetype) def do_GET(self): (scheme, netloc, path, params, query, frag) = urlparse(self.path) data = { 'prog': (sys.argv[0] or 'pagekite.py').split('/')[-1], 'now': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), 'ver': APPVER } authenticated = False if self.server.pkite.ui_password: auth = self.headers.get('authorization') if auth: (how, ab64) = auth.split() if how.lower() == 'basic': (uid, password) = base64.b64decode(ab64).split(':') authenticated = (password == self.server.pkite.ui_password) elif query.find('auth=%s' % self.server.pkite.ui_password) != -1: authenticated = True if not authenticated: self.begin_headers(401, 'text/html') self.send_header('WWW-Authenticate', 'Basic realm=PK%d' % (time.time()/3600)) self.end_headers() data['title'] = data['body'] = 'Authentication required.' self.wfile.write(self.TEMPLATE_HTML % data) return if path.endswith('.txt'): template = self.TEMPLATE_TEXT self.begin_headers(200, 'text/plain') else: template = self.TEMPLATE_HTML self.begin_headers(200, 'text/html') self.end_headers() qs = parse_qs(query) if path == '/vars.txt': global gYamon data['body'] = gYamon.render_vars_text() elif path == '/log.txt': data['body'] = self.txt_log() elif path.endswith('log.html'): data.update(self.html_log(path)) elif path == '/conns/': data.update(self.html_conns()) elif path.startswith('/conn/'): data.update(self.html_conn(path)) else: data.update(self.html_overview()) self.wfile.write(template % data) class UiHttpServer(SimpleXMLRPCServer): def __init__(self, sspec, pkite, conns, handler=UiRequestHandler, ssl_pem_filename=None): SimpleXMLRPCServer.__init__(self, sspec, handler) self.pkite = pkite self.conns = conns # FIXME: There should be access control on these #self.register_introspection_functions() #self.register_instance(conns) if ssl_pem_filename: ctx = SSL.Context(SSL.SSLv23_METHOD) ctx.use_privatekey_file (ssl_pem_filename) ctx.use_certificate_chain_file(ssl_pem_filename) self.socket = SSL_Connect(ctx, socket.socket(self.address_family, self.socket_type), server_side=True) self.server_bind() self.server_activate() self.enable_ssl = True else: self.enable_ssl = False global gYamon gYamon = YamonD(sspec) gYamon.vset('started', int(time.time())) gYamon.vset('version', APPVER) gYamon.vset('httpd_ssl_enabled', self.enable_ssl) gYamon.vset('errors', 0) gYamon.vset("bytes_all", 0) class HttpUiThread(threading.Thread): """Handle HTTP UI in a separate thread.""" def __init__(self, pkite, conns, server=UiHttpServer, handler=UiRequestHandler, ssl_pem_filename=None): threading.Thread.__init__(self) self.ui_sspec = pkite.ui_sspec self.httpd = server(self.ui_sspec, pkite, conns, handler=handler, ssl_pem_filename=ssl_pem_filename) self.httpd.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.serve = True global SELECTABLES SELECTABLES = {} def quit(self): self.serve = False try: knock = rawsocket(socket.AF_INET, socket.SOCK_STREAM) knock.connect(self.ui_sspec) knock.close() except Exception: pass def run(self): while self.serve: try: self.httpd.handle_request() except KeyboardInterrupt: self.serve = False except Exception, e: LogInfo('HTTP UI caught exception: %s' % e) LogDebug('HttpUiThread: done') self.httpd.socket.close() HTTP_METHODS = ['OPTIONS', 'CONNECT', 'GET', 'HEAD', 'POST', 'PUT', 'TRACE', 'PROPFIND', 'PROPPATCH', 'MKCOL', 'DELETE', 'COPY', 'MOVE', 'LOCK', 'UNLOCK', 'PING'] HTTP_VERSIONS = ['HTTP/1.0', 'HTTP/1.1'] class HttpParser(object): """Parse an HTTP request, line-by-line.""" IN_REQUEST = 1 IN_HEADERS = 2 IN_BODY = 3 IN_RESPONSE = 4 PARSE_FAILED = -1 def __init__(self, lines=None, state=None, testbody=False): self.state = state or self.IN_REQUEST self.method = None self.path = None self.version = None self.code = None self.message = None self.headers = [] self.lines = [] self.body_result = testbody if lines is not None: for line in lines: if not self.Parse(line): break def ParseResponse(self, line): self.version, self.code, self.message = line.split() if not self.version.upper() in HTTP_VERSIONS: LogDebug('Invalid version: %s' % self.version) return False self.state = self.IN_HEADERS return True def ParseRequest(self, line): self.method, self.path, self.version = line.split() if not self.method in HTTP_METHODS: LogDebug('Invalid method: %s' % self.method) return False if not self.version.upper() in HTTP_VERSIONS: LogDebug('Invalid version: %s' % self.version) return False self.state = self.IN_HEADERS return True def ParseHeader(self, line): if line in ('', '\r', '\n', '\r\n'): self.state = self.IN_BODY return True header, value = line.split(':', 1) if value and value.startswith(' '): value = value[1:] self.headers.append((header.lower(), value)) return True def ParseBody(self, line): # Could be overridden by subclasses, for now we just play dumb. return self.body_result def Parse(self, line): self.lines.append(line) try: if (self.state == self.IN_RESPONSE): return self.ParseResponse(line) elif (self.state == self.IN_REQUEST): return self.ParseRequest(line) elif (self.state == self.IN_HEADERS): return self.ParseHeader(line) elif (self.state == self.IN_BODY): return self.ParseBody(line) except ValueError, err: LogInfo('Parse failed: %s, %s, %s' % (self.state, err, self.lines)) self.state = self.PARSE_FAILED return False def Header(self, header): return [h[1].strip() for h in self.headers if h[0] == header.lower()] def obfuIp(ip): quads = ('%s' % ip).replace(':', '.').split('.') return '~%s' % '.'.join([q for q in quads[-2:]]) selectable_id = 0 buffered_bytes = 0 SELECTABLES = None class Selectable(object): """A wrapper around a socket, for use with select.""" HARMLESS_ERRNOS = (errno.EINTR, errno.EAGAIN, errno.ENOMEM, errno.EBUSY, errno.EDEADLK, errno.EWOULDBLOCK, errno.ENOBUFS, errno.EALREADY) def __init__(self, fd=None, address=None, on_port=None, maxread=16000, tracked=True): self.fd = None try: self.SetFD(fd or rawsocket(socket.AF_INET6, socket.SOCK_STREAM), six=True) except Exception: self.SetFD(fd or rawsocket(socket.AF_INET, socket.SOCK_STREAM)) self.address = address self.on_port = on_port self.created = self.bytes_logged = time.time() self.dead = False # Quota-related stuff self.quota = None # Read-related variables self.maxread = maxread self.read_bytes = self.all_in = 0 self.read_eof = False self.peeking = False self.peeked = 0 # Write-related variables self.wrote_bytes = self.all_out = 0 self.write_blocked = '' self.write_speed = 102400 self.write_eof = False self.write_retry = None # Throttle reads and writes self.throttle_until = 0 # Compression stuff self.zw = None self.zlevel = 1 self.zreset = False # Logging self.logged = [] global selectable_id selectable_id += 1 self.sid = selectable_id self.alt_id = None if address: addr = address or ('x.x.x.x', 'x') self.log_id = 's%s/%s:%s' % (self.sid, obfuIp(addr[0]), addr[1]) else: self.log_id = 's%s' % self.sid # Introspection if SELECTABLES is not None: old = selectable_id-150 if old in SELECTABLES: del SELECTABLES[old] if tracked: SELECTABLES[selectable_id] = self global gYamon self.countas = 'selectables_live' gYamon.vadd(self.countas, 1) gYamon.vadd('selectables', 1) def CountAs(self, what): global gYamon gYamon.vadd(self.countas, -1) self.countas = what gYamon.vadd(self.countas, 1) def __del__(self): global gYamon gYamon.vadd(self.countas, -1) gYamon.vadd('selectables', -1) def __str__(self): return '%s: %s' % (self.log_id, self.__class__) def __html__(self): try: peer = self.fd.getpeername() sock = self.fd.getsockname() except Exception: peer = ('x.x.x.x', 'x') sock = ('x.x.x.x', 'x') return ('Outgoing ZChunks: %s
' 'Buffered bytes: %s
' 'Remote address: %s
' 'Local address: %s
' 'Bytes in / out: %s / %s
' 'Created: %s
' 'Status: %s
' '
' 'Logged:
    %s

' '\n') % (self.zw and ('level %d' % self.zlevel) or 'off', len(self.write_blocked), self.dead and '-' or (obfuIp(peer[0]), peer[1]), self.dead and '-' or (obfuIp(sock[0]), sock[1]), fmt_size(self.all_in + self.read_bytes), fmt_size(self.all_out + self.wrote_bytes), time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(self.created)), self.dead and 'dead' or 'alive', ''.join(['
  • %s' % (l, ) for l in self.logged])) def ResetZChunks(self): if self.zw: self.zreset = True self.zw = zlib.compressobj(self.zlevel) def EnableZChunks(self, level=1): self.zlevel = level self.zw = zlib.compressobj(level) def SetFD(self, fd, six=False): if self.fd: self.fd.close() self.fd = fd self.fd.setblocking(0) try: if six: self.fd.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) self.fd.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) self.fd.setsockopt(socket.SOL_TCP, socket.TCP_KEEPIDLE, 60) self.fd.setsockopt(socket.SOL_TCP, socket.TCP_KEEPCNT, 10) self.fd.setsockopt(socket.SOL_TCP, socket.TCP_KEEPINTVL, 1) except Exception: pass def SetConn(self, conn): self.SetFD(conn.fd) self.log_id = conn.log_id self.read_bytes = conn.read_bytes self.wrote_bytes = conn.wrote_bytes def Log(self, values): if self.log_id: values.append(('id', self.log_id)) Log(values) self.logged.append(('', values)) def LogError(self, error, params=None): values = params or [] if self.log_id: values.append(('id', self.log_id)) LogError(error, values) self.logged.append((error, values)) def LogDebug(self, message, params=None): values = params or [] if self.log_id: values.append(('id', self.log_id)) LogDebug(message, values) self.logged.append((message, values)) def LogInfo(self, message, params=None): values = params or [] if self.log_id: values.append(('id', self.log_id)) LogInfo(message, values) self.logged.append((message, values)) def LogTraffic(self, final=False): if self.wrote_bytes or self.read_bytes: now = time.time() self.all_out += self.wrote_bytes self.all_in += self.read_bytes global gYamon gYamon.vadd("bytes_all", self.wrote_bytes + self.read_bytes, wrap=1000000000) if final: self.Log([('wrote', '%d' % self.wrote_bytes), ('wbps', '%d' % self.write_speed), ('read', '%d' % self.read_bytes), ('eof', '1')]) else: self.Log([('wrote', '%d' % self.wrote_bytes), ('wbps', '%d' % self.write_speed), ('read', '%d' % self.read_bytes)]) self.bytes_logged = now self.wrote_bytes = self.read_bytes = 0 elif final: self.Log([('eof', '1')]) def Cleanup(self, close=True): global buffered_bytes buffered_bytes -= len(self.write_blocked) self.write_blocked = self.peeked = self.zw = '' if not self.dead: self.dead = True self.CountAs('selectables_dead') if close: if self.fd: self.fd.close() self.fd = None self.LogTraffic(final=True) def ProcessData(self, data): self.LogError('Selectable::ProcessData: Should be overridden!') return False def ProcessEof(self): if self.read_eof and self.write_eof and not self.write_blocked: self.Cleanup() return False return True def ProcessEofRead(self): self.read_eof = True self.LogError('Selectable::ProcessEofRead: Should be overridden!') return False def ProcessEofWrite(self): self.write_eof = True self.LogError('Selectable::ProcessEofWrite: Should be overridden!') return False def EatPeeked(self, eat_bytes=None, keep_peeking=False): if not self.peeking: return if eat_bytes is None: eat_bytes = self.peeked discard = '' while len(discard) < eat_bytes: try: discard += self.fd.recv(eat_bytes - len(discard)) except socket.error, (errno, msg): self.LogInfo('Error reading (%d/%d) socket: %s (errno=%s)' % ( eat_bytes, self.peeked, msg, errno)) time.sleep(0.1) self.peeked -= eat_bytes self.peeking = keep_peeking return def ReadData(self, maxread=None): if self.read_eof: return False try: maxread = maxread or self.maxread if self.peeking: data = self.fd.recv(maxread, socket.MSG_PEEK) self.peeked = len(data) if DEBUG_IO: print '<== IN (peeked)\n%s\n===' % data else: data = self.fd.recv(maxread) if DEBUG_IO: print '<== IN\n%s\n===' % data except (SSL.WantReadError, SSL.WantWriteError), err: return True except IOError, err: if err.errno not in self.HARMLESS_ERRNOS: self.LogDebug('Error reading socket: %s (%s)' % (err, err.errno)) return False else: return True except (SSL.Error, SSL.ZeroReturnError, SSL.SysCallError), err: self.LogDebug('Error reading socket (SSL): %s' % err) return False except socket.error, (errno, msg): if errno in self.HARMLESS_ERRNOS: return True else: self.LogInfo('Error reading socket: %s (errno=%s)' % (msg, errno)) return False if data is None or data == '': self.read_eof = True return self.ProcessData('') else: if not self.peeking: self.read_bytes += len(data) if self.read_bytes > LOG_THRESHOLD: self.LogTraffic() return self.ProcessData(data) def Throttle(self, max_speed=None, remote=False, delay=0.2): if max_speed: self.throttle_until = time.time() flooded = self.read_bytes + self.all_in flooded -= max_speed * (time.time() - self.created) delay = min(15, max(0.2, flooded/max_speed)) if flooded < 0: delay = 15 else: if self.throttle_until < time.time(): self.throttle_until = time.time() flooded = '?' self.throttle_until += delay self.LogInfo('Throttled until %x (flooded=%s, bps=%s, remote=%s)' % ( int(self.throttle_until), flooded, max_speed, remote)) return True def Send(self, data, try_flush=False): global buffered_bytes buffered_bytes -= len(self.write_blocked) # If we're already blocked, just buffer unless explicitly asked to flush. if (not try_flush) and (len(self.write_blocked) > 0 or SEND_ALWAYS_BUFFERS): self.write_blocked += ''.join(data) buffered_bytes += len(self.write_blocked) return True self.write_speed = int((self.wrote_bytes + self.all_out) / (0.1 + time.time() - self.created)) sending = self.write_blocked+(''.join(data)) self.write_blocked = '' sent_bytes = 0 if sending: try: sent_bytes = self.fd.send(sending[:(self.write_retry or SEND_MAX_BYTES)]) if DEBUG_IO: print '==> OUT\n%s\n===' % sending[:sent_bytes] self.wrote_bytes += sent_bytes self.write_retry = None except IOError, err: if err.errno not in self.HARMLESS_ERRNOS: self.LogInfo('Error sending: %s' % err) self.ProcessEofWrite() return False else: self.write_retry = len(sending) except (SSL.WantWriteError, SSL.WantReadError), err: self.write_retry = len(sending) except socket.error, (errno, msg): if errno not in self.HARMLESS_ERRNOS: self.LogInfo('Error sending: %s (errno=%s)' % (msg, errno)) self.ProcessEofWrite() return False else: self.write_retry = len(sending) except (SSL.Error, SSL.ZeroReturnError, SSL.SysCallError), err: self.LogInfo('Error sending (SSL): %s' % err) self.ProcessEofWrite() return False self.write_blocked = sending[sent_bytes:] buffered_bytes += len(self.write_blocked) if self.wrote_bytes >= LOG_THRESHOLD: self.LogTraffic() if self.write_eof and not self.write_blocked: self.ProcessEofWrite() return True def SendChunked(self, data, compress=True, zhistory=None): rst = '' if self.zreset: self.zreset = False rst = 'R' # Stop compressing streams that just get bigger. if zhistory and (zhistory[0] < zhistory[1]): compress = False sdata = ''.join(data) if self.zw and compress: try: zdata = self.zw.compress(sdata) + self.zw.flush(zlib.Z_SYNC_FLUSH) if zhistory: zhistory[0] = len(sdata) zhistory[1] = len(zdata) return self.Send(['%xZ%x%s\r\n%s' % (len(sdata), len(zdata), rst, zdata)]) except zlib.error: LogError('Error compressing, resetting ZChunks.') self.ResetZChunks() return self.Send(['%x%s\r\n%s' % (len(sdata), rst, sdata)]) def Flush(self, loops=50, wait=False): while loops != 0 and len(self.write_blocked) > 0 and self.Send([], try_flush=True): if wait and len(self.write_blocked) > 0: time.sleep(0.1) loops -= 1 if self.write_blocked: return False return True class Connections(object): """A container for connections (Selectables), config and tunnel info.""" def __init__(self, config): self.config = config self.ip_tracker = {} self.conns = [] self.conns_by_id = {} self.tunnels = {} self.auth = None def start(self, auth_thread=None): self.auth = auth_thread or AuthThread(self) self.auth.start() def Add(self, conn, alt_id=None): self.conns.append(conn) if alt_id: self.conns_by_id[alt_id] = conn def TrackIP(self, ip, domain): tick = '%d' % (time.time()/12) if tick not in self.ip_tracker: deadline = int(tick)-10 for ot in self.ip_tracker.keys(): if int(ot) < deadline: del self.ip_tracker[ot] self.ip_tracker[tick] = {} if ip not in self.ip_tracker[tick]: self.ip_tracker[tick][ip] = [1, domain] else: self.ip_tracker[tick][ip][0] += 1 self.ip_tracker[tick][ip][1] = domain def LastIpDomain(self, ip): domain = None for tick in sorted(self.ip_tracker.keys()): if ip in self.ip_tracker[tick]: domain = self.ip_tracker[tick][ip][1] return domain def Remove(self, conn): if conn.alt_id and conn.alt_id in self.conns_by_id: del self.conns_by_id[conn.alt_id] if conn in self.conns: self.conns.remove(conn) for tid in self.tunnels.keys(): if conn in self.tunnels[tid]: self.tunnels[tid].remove(conn) if not self.tunnels[tid]: del self.tunnels[tid] def Readable(self): # FIXME: This is O(n) now = time.time() return [s.fd for s in self.conns if (s.fd and (not s.read_eof) and (s.throttle_until <= now))] def Blocked(self): # FIXME: This is O(n) return [s.fd for s in self.conns if s.fd and len(s.write_blocked) > 0] def DeadConns(self): return [s for s in self.conns if s.read_eof and s.write_eof and not s.write_blocked] def CleanFds(self): evil = [] for s in self.conns: try: i, o, e = select.select([s.fd], [s.fd], [s.fd], 0) except Exception: evil.append(s) for s in evil: LogDebug('Removing broken Selectable: %s' % s) self.Remove(s) def Connection(self, fd): for conn in self.conns: if conn.fd == fd: return conn return None def TunnelServers(self): servers = {} for tid in self.tunnels: for tunnel in self.tunnels[tid]: server = tunnel.server_info[tunnel.S_NAME] if server is not None: servers[server] = 1 return servers.keys() def Tunnel(self, proto, domain, conn=None): tid = '%s:%s' % (proto, domain) if conn is not None: if tid not in self.tunnels: self.tunnels[tid] = [] self.tunnels[tid].append(conn) if tid in self.tunnels: return self.tunnels[tid] else: return [] class LineParser(Selectable): """A Selectable which parses the input as lines of text.""" def __init__(self, fd=None, address=None, on_port=None, tracked=True): Selectable.__init__(self, fd, address, on_port, tracked=tracked) self.leftovers = '' def __html__(self): return Selectable.__html__(self) def Cleanup(self, close=True): Selectable.Cleanup(self, close=close) self.leftovers = '' def ProcessData(self, data): lines = (self.leftovers+data).splitlines(True) self.leftovers = '' while lines: line = lines.pop(0) if line.endswith('\n'): if self.ProcessLine(line, lines) is False: return False else: if not self.peeking: self.leftovers += line if self.read_eof: return self.ProcessEofRead() return True def ProcessLine(self, line, lines): self.LogError('LineParser::ProcessLine: Should be overridden!') return False TLS_CLIENTHELLO = '%c' % 026 SSL_CLIENTHELLO = '\x80' # FIXME: XMPP support class MagicProtocolParser(LineParser): """A Selectable which recognizes HTTP, TLS or XMPP preambles.""" def __init__(self, fd=None, address=None, on_port=None): LineParser.__init__(self, fd, address, on_port, tracked=False) self.leftovers = '' self.might_be_tls = True self.is_tls = False def __html__(self): return ('Detected TLS: %s
    ' '%s') % (self.is_tls, LineParser.__html__(self)) # FIXME: DEPRECATE: Make this all go away, switch to CONNECT. def ProcessMagic(self, data): args = {} try: prefix, words, data = data.split('\r\n', 2) for arg in words.split('; '): key, val = arg.split('=', 1) args[key] = val self.EatPeeked(eat_bytes=len(prefix)+2+len(words)+2) except ValueError, e: return True try: port = 'port' in args and args['port'] or None if port: self.on_port = int(port) except ValueError, e: return False proto = 'proto' in args and args['proto'] or None if proto in ('http', 'websocket'): return LineParser.ProcessData(self, data) domain = 'domain' in args and args['domain'] or None if proto == 'https': return self.ProcessTls(data, domain) if proto == 'raw' and domain: return self.ProcessRaw(data, domain) return False def ProcessData(self, data): if data.startswith(MAGIC_PREFIX): return self.ProcessMagic(data) if self.might_be_tls: self.might_be_tls = False if not data.startswith(TLS_CLIENTHELLO) and not data.startswith(SSL_CLIENTHELLO): self.EatPeeked() return LineParser.ProcessData(self, data) self.is_tls = True if self.is_tls: return self.ProcessTls(data) else: self.EatPeeked() return LineParser.ProcessData(self, data) def GetMsg(self, data): mtype, ml24, mlen = struct.unpack('>BBH', data[0:4]) mlen += ml24 * 0x10000 return mtype, data[4:4+mlen], data[4+mlen:] def GetClientHelloExtensions(self, msg): # Ugh, so many magic numbers! These are accumulated sizes of # the different fields we are ignoring in the TLS headers. slen = struct.unpack('>B', msg[34])[0] cslen = struct.unpack('>H', msg[35+slen:37+slen])[0] cmlen = struct.unpack('>B', msg[37+slen+cslen])[0] extofs = 34+1+2+1+2+slen+cslen+cmlen if extofs < len(msg): return msg[extofs:] return None def GetSniNames(self, extensions): names = [] while extensions: etype, elen = struct.unpack('>HH', extensions[0:4]) if etype == 0: # OK, we found an SNI extension, get the list. namelist = extensions[6:4+elen] while namelist: ntype, nlen = struct.unpack('>BH', namelist[0:3]) if ntype == 0: names.append(namelist[3:3+nlen].lower()) namelist = namelist[3+nlen:] extensions = extensions[4+elen:] return names def GetSni(self, data): hello, vmajor, vminor, mlen = struct.unpack('>BBBH', data[0:5]) data = data[5:] sni = [] while data: mtype, msg, data = self.GetMsg(data) if mtype == 1: # ClientHello! sni.extend(self.GetSniNames(self.GetClientHelloExtensions(msg))) return sni def ProcessTls(self, data, domain=None): self.LogError('TlsOrLineParser::ProcessTls: Should be overridden!') return False def ProcessRaw(self, data, domain): self.LogError('TlsOrLineParser::ProcessRaw: Should be overridden!') return False class ChunkParser(Selectable): """A Selectable which parses the input as chunks.""" def __init__(self, fd=None, address=None, on_port=None): Selectable.__init__(self, fd, address, on_port) self.want_cbytes = 0 self.want_bytes = 0 self.compressed = False self.header = '' self.chunk = '' self.zr = zlib.decompressobj() def __html__(self): return Selectable.__html__(self) def Cleanup(self, close=True): Selectable.Cleanup(self, close=close) self.zr = self.chunk = self.header = None def ProcessData(self, data): if self.peeking: self.want_cbytes = 0 self.want_bytes = 0 self.header = '' self.chunk = '' if self.want_bytes == 0: self.header += data if self.header.find('\r\n') < 0: if self.read_eof: return self.ProcessEofRead() return True try: size, data = self.header.split('\r\n', 1) self.header = '' if size.endswith('R'): self.zr = zlib.decompressobj() size = size[0:-1] if 'Z' in size: csize, zsize = size.split('Z') self.compressed = True self.want_cbytes = int(csize, 16) self.want_bytes = int(zsize, 16) else: self.compressed = False self.want_bytes = int(size, 16) except ValueError, err: self.LogError('ChunkParser::ProcessData: %s' % err) self.Log([('bad_data', data)]) return False if self.want_bytes == 0: return False process = data[:self.want_bytes] leftover = data[self.want_bytes:] self.chunk += process self.want_bytes -= len(process) result = 1 if self.want_bytes == 0: if self.compressed: try: cchunk = self.zr.decompress(self.chunk) except zlib.error: cchunk = '' if len(cchunk) != self.want_cbytes: result = self.ProcessCorruptChunk(self.chunk) else: result = self.ProcessChunk(cchunk) else: result = self.ProcessChunk(self.chunk) self.chunk = '' if result and leftover: result = self.ProcessData(leftover) if self.read_eof: result = self.ProcessEofRead() and result return result def ProcessCorruptChunk(self, chunk): self.LogError('ChunkParser::ProcessData: ProcessCorruptChunk not overridden!') return False def ProcessChunk(self, chunk): self.LogError('ChunkParser::ProcessData: ProcessChunk not overridden!') return False class Tunnel(ChunkParser): """A Selectable representing a PageKite tunnel.""" S_NAME = 0 S_PORTS = 1 S_RAW_PORTS = 2 S_PROTOS = 3 def __init__(self, conns): ChunkParser.__init__(self) # We want to be sure to read the entire chunk at once, including # headers to save cycles, so we double the size we're willing to # read here. self.maxread *= 2 self.server_info = ['x.x.x.x:x', [], [], []] self.conns = conns self.users = {} self.remote_ssl = {} self.zhistory = {} self.backends = {} self.rtt = 100000 self.last_activity = time.time() self.last_ping = 0 def __html__(self): return ('Server name: %s
    ' '%s') % (self.server_info[self.S_NAME], ChunkParser.__html__(self)) def _FrontEnd(conn, body, conns): """This is what the front-end does when a back-end requests a new tunnel.""" self = Tunnel(conns) requests = [] try: for prefix in ('X-Beanstalk', 'X-PageKite'): for feature in conn.parser.Header(prefix+'-Features'): if feature == 'ZChunks': self.EnableZChunks(level=1) # Track which versions we see in the wild. version = 'old' for v in conn.parser.Header(prefix+'-Version'): version = v global gYamon gYamon.vadd('version-%s' % version, 1, wrap=10000000) for replace in conn.parser.Header(prefix+'-Replace'): if replace in self.conns.conns_by_id: repl = self.conns.conns_by_id[replace] self.LogInfo('Disconnecting old tunnel: %s' % repl) self.conns.Remove(repl) repl.Cleanup() for bs in conn.parser.Header(prefix): # X-Beanstalk: proto:my.domain.com:token:signature proto, domain, srand, token, sign = bs.split(':') requests.append((proto.lower(), domain.lower(), srand, token, sign, prefix)) except Exception, err: self.LogError('Discarding connection: %s' % err) self.Cleanup() return None except socket.error, err: self.LogInfo('Discarding connection: %s' % err) self.Cleanup() return None self.CountAs('backends_live') self.SetConn(conn) conns.auth.check(requests[:], conn, lambda r: self.AuthCallback(conn, r)) return self def RecheckQuota(self, conns, when=None): if when is None: when = time.time() if (self.quota and self.quota[0] is not None and self.quota[1] and (self.quota[2] < when-900)): self.quota[2] = when LogDebug('Rechecking: %s' % (self.quota, )) conns.auth.check([self.quota[1]], self, lambda r: self.QuotaCallback(conns, r)) def QuotaCallback(self, conns, results): # Report new values to the back-end... if self.quota and (self.quota[0] >= 0): self.SendQuota() for r in results: if r[0] in ('X-PageKite-OK', 'X-PageKite-Duplicate'): return self self.LogInfo('Ran out of quota or account deleted, closing tunnel.') conns.Remove(self) self.Cleanup() return None def AuthCallback(self, conn, results): output = [HTTP_ResponseHeader(200, 'OK'), HTTP_Header('Content-Transfer-Encoding', 'chunked'), HTTP_Header('X-PageKite-Features', 'ZChunks'), HTTP_Header('X-PageKite-Protos', ', '.join(['%s' % p for p in self.conns.config.server_protos])), HTTP_Header('X-PageKite-Ports', ', '.join( ['%s' % self.conns.config.server_portalias.get(p, p) for p in self.conns.config.server_ports]))] if self.conns.config.server_raw_ports: output.append( HTTP_Header('X-PageKite-Raw-Ports', ', '.join(['%s' % p for p in self.conns.config.server_raw_ports]))) ok = {} for r in results: if r[0] in ('X-PageKite-OK', 'X-Beanstalk-OK'): ok[r[1]] = 1 if r[0] == 'X-PageKite-SessionID': self.alt_id = r[1] output.append('%s: %s\r\n' % r) output.append(HTTP_StartBody()) if not self.Send(output, try_flush=True): conn.LogDebug('No tunnels configured, closing connection (send failed).') self.Cleanup() return None self.backends = ok.keys() if self.backends: for backend in self.backends: proto, domain, srand = backend.split(':') self.Log([('BE', 'Live'), ('proto', proto), ('domain', domain)]) self.conns.Tunnel(proto, domain, self) if conn.quota: self.quota = conn.quota self.Log([('BE', 'Live'), ('quota', self.quota[0])]) self.conns.Add(self, alt_id=self.alt_id) return self else: conn.LogDebug('No tunnels configured, closing connection.') self.Cleanup() return None def _RecvHttpHeaders(self): data = '' while not data.endswith('\r\n\r\n') and not data.endswith('\n\n'): try: buf = self.fd.recv(4096) except: # This is sloppy, but the back-end will just connect somewhere else # instead, so laziness here should be fine. buf = None if buf is None or buf == '': LogDebug('Remote end closed connection.') return None data += buf self.read_bytes += len(buf) if DEBUG_IO: print '<== IN (headers)\n%s\n===' % data return data def _Connect(self, server, conns, tokens=None): if self.fd: self.fd.close() if conns.config.socks_server: import socks sock = socks.socksocket() self.SetFD(sock) else: self.SetFD(rawsocket(socket.AF_INET, socket.SOCK_STREAM)) try: self.fd.settimeout(20.0) # Missing in Python 2.2 except Exception: self.fd.setblocking(1) sspec = server.split(':') if len(sspec) > 1: self.fd.connect((sspec[0], int(sspec[1]))) else: self.fd.connect((server, 443)) if self.conns.config.fe_certname: # We can't set the SNI directly from Python, so we use CONNECT instead commonName = self.conns.config.fe_certname[0].split('/')[0] if (not self.Send(['CONNECT %s:443 HTTP/1.0\r\n\r\n' % commonName], try_flush=True) or not self.Flush(wait=True)): return None, None data = self._RecvHttpHeaders() if data is None or not data.startswith(HTTP_ConnectOK().strip()): LogError('CONNECT failed, could not initiate TLS.') self.fd.close() return None, None try: self.fd.setblocking(1) raw_fd = self.fd ctx = SSL.Context(SSL.TLSv1_METHOD) ctx.load_verify_locations(self.conns.config.ca_certs) self.fd = SSL_Connect(ctx, self.fd, connected=True, server_side=False, verify_names=self.conns.config.fe_certname) LogDebug('TLS connection to %s OK' % server) except SSL.Error, e: self.fd = raw_fd self.fd.close() LogError('SSL handshake failed: probably a bad cert (%s)' % e) return None, None replace_sessionid = self.conns.config.servers_sessionids.get(server, None) if (not self.Send(HTTP_PageKiteRequest(server, conns.config.backends, tokens, nozchunks=conns.config.disable_zchunks, replace=replace_sessionid), try_flush=True) or not self.Flush(wait=True)): return None, None data = self._RecvHttpHeaders() if data is None: return None, None self.fd.setblocking(0) parse = HttpParser(lines=data.splitlines(), state=HttpParser.IN_RESPONSE) return data, parse def _BackEnd(server, backends, require_all, conns): """This is the back-end end of a tunnel.""" self = Tunnel(conns) self.backends = backends self.require_all = require_all self.server_info[self.S_NAME] = server try: begin = time.time() data, parse = self._Connect(server, conns) if data and parse: # Collect info about front-end capabilities, for interactive config for portlist in parse.Header('X-PageKite-Ports'): self.server_info[self.S_PORTS].extend(portlist.split(', ')) for portlist in parse.Header('X-PageKite-Raw-Ports'): self.server_info[self.S_RAW_PORTS].extend(portlist.split(', ')) for protolist in parse.Header('X-PageKite-Protos'): self.server_info[self.S_PROTOS].extend(protolist.split(', ')) for sessionid in parse.Header('X-PageKite-SessionID'): self.alt_id = sessionid conns.config.servers_sessionids[server] = sessionid tryagain = False tokens = {} for request in parse.Header('X-PageKite-SignThis'): proto, domain, srand, token = request.split(':') tokens['%s:%s' % (proto, domain)] = token tryagain = True if tryagain: begin = time.time() data, parse = self._Connect(server, conns, tokens) if data and parse: if not conns.config.disable_zchunks: for feature in parse.Header('X-PageKite-Features'): if feature == 'ZChunks': self.EnableZChunks(level=9) invalid_reasons = {} for request in parse.Header('X-PageKite-Invalid-Why'): # This is future-compatible, in that we can add more fields later. details = request.split(';') invalid_reasons[details[0]] = details[1] for request in parse.Header('X-PageKite-Invalid'): proto, domain, srand = request.split(':') reason = invalid_reasons.get(request, 'unknown') self.Log([('FE', self.server_info[self.S_NAME]), ('err', 'Rejected'), ('proto', proto), ('reason', reason), ('domain', domain)]) for request in parse.Header('X-PageKite-Duplicate'): proto, domain, srand = request.split(':') self.Log([('FE', self.server_info[self.S_NAME]), ('err', 'Duplicate'), ('proto', proto), ('domain', domain)]) for quota in parse.Header('X-PageKite-Quota'): self.quota = [int(quota), None, None] self.Log([('FE', self.server_info[self.S_NAME]), ('quota', quota)]) ssl_available = {} for request in parse.Header('X-PageKite-SSL-OK'): ssl_available[request] = True for request in parse.Header('X-PageKite-OK'): abort = False proto, domain, srand = request.split(':') conns.Tunnel(proto, domain, self) if request in ssl_available: self.remote_ssl[(proto, domain)] = True self.Log([('FE', self.server_info[self.S_NAME]), ('proto', proto), ('ssl', (request in ssl_available)), ('domain', domain)]) self.rtt = (time.time() - begin) except socket.error, e: self.Cleanup() return None except Exception, e: self.LogError('Server response parsing failed: %s' % e) self.Cleanup() return None conns.Add(self) self.CountAs('frontends_live') return self FrontEnd = staticmethod(_FrontEnd) BackEnd = staticmethod(_BackEnd) def SendData(self, conn, data, sid=None, host=None, proto=None, port=None, chunk_headers=None): sid = int(sid or conn.sid) if conn: self.users[sid] = conn if not sid in self.zhistory: self.zhistory[sid] = [0, 0] sending = ['SID: %s\r\n' % sid] if proto: sending.append('Proto: %s\r\n' % proto) if host: sending.append('Host: %s\r\n' % host) if port: porti = int(port) if porti in self.conns.config.server_portalias: sending.append('Port: %s\r\n' % self.conns.config.server_portalias[porti]) else: sending.append('Port: %s\r\n' % port) if chunk_headers: for ch in chunk_headers: sending.append('%s: %s\r\n' % ch) sending.append('\r\n') sending.append(data) return self.SendChunked(sending, zhistory=self.zhistory[sid]) def SendStreamEof(self, sid, write_eof=False, read_eof=False): return self.SendChunked('SID: %s\r\nEOF: 1%s%s\r\n\r\nBye!' % (sid, (write_eof or not read_eof) and 'W' or '', (read_eof or not write_eof) and 'R' or '')) def EofStream(self, sid, eof_type='WR'): if sid in self.users and self.users[sid] is not None: write_eof = (-1 != eof_type.find('W')) read_eof = (-1 != eof_type.find('R')) self.users[sid].ProcessTunnelEof(read_eof=(read_eof or not write_eof), write_eof=(write_eof or not read_eof)) def CloseStream(self, sid, stream_closed=False): if sid in self.users: stream = self.users[sid] del self.users[sid] if not stream_closed and stream is not None: stream.CloseTunnel(tunnel_closed=True) if sid in self.zhistory: del self.zhistory[sid] def Cleanup(self, close=True): if self.users: for sid in self.users.keys(): self.CloseStream(sid) ChunkParser.Cleanup(self, close=close) self.conns = None self.users = self.zhistory = self.backends = {} def ResetRemoteZChunks(self): return self.SendChunked('NOOP: 1\r\nZRST: 1\r\n\r\n!', compress=False) def SendPing(self): self.last_ping = int(time.time()) self.LogDebug("Ping", [('host', self.server_info[self.S_NAME])]) return self.SendChunked('NOOP: 1\r\nPING: 1\r\n\r\n!', compress=False) def SendPong(self): return self.SendChunked('NOOP: 1\r\n\r\n!', compress=False) def SendQuota(self): return self.SendChunked('NOOP: 1\r\nQuota: %s\r\n\r\n!' % self.quota[0], compress=False) def SendThrottle(self, sid, write_speed): return self.SendChunked('NOOP: 1\r\nSID: %s\r\nSPD: %d\r\n\r\n!' % ( sid, write_speed), compress=False) def ProcessCorruptChunk(self, data): self.ResetRemoteZChunks() return True def Probe(self, host): for bid in self.conns.config.backends: be = self.conns.config.backends[bid] if be[BE_DOMAIN] == host: bhost, bport = be[BE_BACKEND].split(':') if self.conns.config.Ping(bhost, int(bport)) > 2: return False return True def Throttle(self, parse): try: sid = int(parse.Header('SID')[0]) bps = int(parse.Header('SPD')[0]) if sid in self.users: self.users[sid].Throttle(bps, remote=True) except Exception, e: LogError('Tunnel::ProcessChunk: Invalid throttle request!') return True # If a tunnel goes down, we just go down hard and kill all our connections. def ProcessEofRead(self): if self.conns: self.conns.Remove(self) self.Cleanup() return True def ProcessEofWrite(self): return self.ProcessEofRead() def ProcessChunk(self, data): try: headers, data = data.split('\r\n\r\n', 1) parse = HttpParser(lines=headers.splitlines(), state=HttpParser.IN_HEADERS) except ValueError: LogError('Tunnel::ProcessChunk: Corrupt packet!') return False self.last_activity = time.time() try: if parse.Header('Quota'): if self.quota: self.quota[0] = int(parse.Header('Quota')[0]) else: self.quota = [int(parse.Header('Quota')[0]), None, None] if parse.Header('PING'): return self.SendPong() if parse.Header('ZRST') and not self.ResetZChunks(): return False if parse.Header('SPD') and not self.Throttle(parse): return False if parse.Header('NOOP'): return True except Exception, e: LogError('Tunnel::ProcessChunk: Corrupt chunk: %s' % e) return False conn = None sid = None try: sid = int(parse.Header('SID')[0]) eof = parse.Header('EOF') except IndexError, e: LogError('Tunnel::ProcessChunk: Corrupt packet!') return False if eof: self.EofStream(sid, eof[0]) else: if sid in self.users: conn = self.users[sid] else: proto = (parse.Header('Proto') or [''])[0].lower() port = (parse.Header('Port') or [''])[0].lower() host = (parse.Header('Host') or [''])[0].lower() rIp = (parse.Header('RIP') or [''])[0].lower() rPort = (parse.Header('RPort') or [''])[0].lower() if proto and host: # FIXME: # if proto == 'https': # if host in self.conns.config.tls_endpoints: # print 'Should unwrap SSL from %s' % host if proto == 'probe': if self.conns.config.no_probes: LogDebug('Responding to probe for %s: rejected' % host) if not self.SendChunked('SID: %s\r\n\r\n%s' % ( sid, HTTP_NoFeConnection() )): return False elif self.Probe(host): LogDebug('Responding to probe for %s: good' % host) if not self.SendChunked('SID: %s\r\n\r\n%s' % ( sid, HTTP_GoodBeConnection() )): return False else: LogDebug('Responding to probe for %s: back-end down' % host) if not self.SendChunked('SID: %s\r\n\r\n%s' % ( sid, HTTP_NoBeConnection() )): return False else: conn = UserConn.BackEnd(proto, host, sid, self, port, remote_ip=rIp, remote_port=rPort) if proto in ('http', 'websocket'): if not conn: if not self.SendChunked('SID: %s\r\n\r\n%s' % (sid, HTTP_Unavailable('be', proto, host, frame_url=self.conns.config.error_url) )): return False elif rIp: req, rest = re.sub(r'(?mi)^x-forwarded-for', 'X-Old-Forwarded-For', data ).split('\n', 1) data = ''.join([req, '\nX-Forwarded-For: %s\r\n' % rIp, rest]) if conn: self.users[sid] = conn if not conn: self.CloseStream(sid) if not self.SendStreamEof(sid): return False else: if not conn.Send(data): # FIXME pass if len(conn.write_blocked) > 2*max(conn.write_speed, 50000): if conn.created < time.time()-3: if not self.SendThrottle(sid, conn.write_speed): return False return True class LoopbackTunnel(Tunnel): """A Tunnel which just loops back to this process.""" def __init__(self, conns, which, backends): Tunnel.__init__(self, conns) self.backends = backends self.require_all = True self.server_info[self.S_NAME] = LOOPBACK[which] self.other_end = None if which == 'FE': for d in backends.keys(): if backends[d][BE_BACKEND]: proto, domain = d.split(':') self.conns.Tunnel(proto, domain, self) self.Log([('FE', self.server_info[self.S_NAME]), ('proto', proto), ('domain', domain)]) def Cleanup(self, close=True): Tunnel.Cleanup(self, close=close) other = self.other_end self.other_end = None if other and other.other_end: other.Cleanup() def Linkup(self, other): self.other_end = other other.other_end = self def _Loop(conns, backends): return LoopbackTunnel(conns, 'FE', backends ).Linkup(LoopbackTunnel(conns, 'BE', backends)) Loop = staticmethod(_Loop) def Send(self, data): return self.other_end.ProcessData(''.join(data)) class UserConn(Selectable): """A Selectable representing a user's connection.""" def __init__(self, address): Selectable.__init__(self, address=address) self.tunnel = None self.conns = None def __html__(self): return ('Tunnel: %s
    ' '%s') % (self.tunnel and self.tunnel.sid or '', escape_html('%s' % (self.tunnel or '')), Selectable.__html__(self)) def CloseTunnel(self, tunnel_closed=False): tunnel = self.tunnel self.tunnel = None if tunnel and not tunnel_closed: if not self.read_eof or not self.write_eof: tunnel.SendStreamEof(self.sid, write_eof=True, read_eof=True) tunnel.CloseStream(self.sid, stream_closed=True) self.ProcessTunnelEof(read_eof=True, write_eof=True) def Cleanup(self, close=True): if close: self.CloseTunnel() Selectable.Cleanup(self, close=close) if self.conns: self.conns.Remove(self) self.conns = None def _FrontEnd(conn, address, proto, host, on_port, body, conns): # This is when an external user connects to a server and requests a # web-page. We have to give it to them! self = UserConn(address) self.conns = conns self.SetConn(conn) if ':' in host: host, port = host.split(':', 1) self.proto = proto self.host = host # If the listening port is an alias for another... if int(on_port) in conns.config.server_portalias: on_port = conns.config.server_portalias[int(on_port)] # Try and find the right tunnel. We prefer proto/port specifications first, # then the just the proto. If the protocol is WebSocket and no tunnel is # found, look for a plain HTTP tunnel. if proto == 'probe': protos = ['http', 'https', 'websocket', 'raw'] ports = conns.config.server_ports[:] ports.extend(conns.config.server_aliasport.keys()) ports.extend([x for x in conns.config.server_raw_ports if x != VIRTUAL_PN]) else: protos = [proto] ports = [on_port] if proto == 'websocket': protos.append('http') tunnels = None for p in protos: for prt in ports: if not tunnels: tunnels = conns.Tunnel('%s-%s' % (p, prt), host) if not tunnels: tunnels = conns.Tunnel(p, host) if not tunnels: tunnels = conns.Tunnel(protos[0], CATCHALL_HN) if self.address: chunk_headers = [('RIP', self.address[0]), ('RPort', self.address[1])] if tunnels: self.tunnel = tunnels[0] if (self.tunnel and self.tunnel.SendData(self, ''.join(body), host=host, proto=proto, port=on_port, chunk_headers=chunk_headers) and self.conns): self.Log([('domain', self.host), ('on_port', on_port), ('proto', self.proto), ('is', 'FE')]) self.conns.Add(self) self.conns.TrackIP(address[0], host) # FIXME: Use the tracked data to detect & mitigate abuse? return self else: self.LogDebug('No back-end', [('on_port', on_port), ('proto', self.proto), ('domain', self.host), ('is', 'FE')]) self.Cleanup(close=False) return None def _BackEnd(proto, host, sid, tunnel, on_port, remote_ip=None, remote_port=None): # This is when we open a backend connection, because a user asked for it. self = UserConn(None) self.sid = sid self.proto = proto self.host = host self.conns = tunnel.conns self.tunnel = tunnel # Try and find the right back-end. We prefer proto/port specifications # first, then the just the proto. If the protocol is WebSocket and no # tunnel is found, look for a plain HTTP tunnel. backend = None protos = [proto] if proto == 'probe': protos = ['http'] if proto == 'websocket': protos.append('http') for p in protos: if not backend: backend = self.conns.config.GetBackendServer('%s-%s' % (p, on_port), host) if not backend: backend = self.conns.config.GetBackendServer(p, host) if not backend: backend = self.conns.config.GetBackendServer(p, CATCHALL_HN) logInfo = [ ('on_port', on_port), ('proto', proto), ('domain', host), ('is', 'BE') ] if remote_ip: logInfo.append(('remote_ip', remote_ip)) if not backend: logInfo.append(('err', 'No back-end')) self.Log(logInfo) self.Cleanup(close=False) return None try: self.SetFD(rawsocket(socket.AF_INET, socket.SOCK_STREAM)) try: self.fd.settimeout(2.0) # Missing in Python 2.2 except Exception: self.fd.setblocking(1) sspec = backend.split(':') if len(sspec) > 1: self.fd.connect((sspec[0], int(sspec[1]))) else: self.fd.connect((backend, 80)) self.fd.setblocking(0) except socket.error, err: logInfo.append(('socket_error', '%s' % err)) self.Log(logInfo) self.Cleanup(close=False) return None self.Log(logInfo) self.conns.Add(self) return self FrontEnd = staticmethod(_FrontEnd) BackEnd = staticmethod(_BackEnd) def Shutdown(self, direction): try: if self.fd: if 'sock_shutdown' in dir(self.fd): # This is a pyOpenSSL socket, which has incompatible shutdown. if direction == socket.SHUT_RD: self.fd.shutdown() else: self.fd.sock_shutdown(direction) else: self.fd.shutdown(direction) except Exception, e: pass def ProcessTunnelEof(self, read_eof=False, write_eof=False): if read_eof and not self.write_eof: self.ProcessEofWrite(tell_tunnel=False) if write_eof and not self.read_eof: self.ProcessEofRead(tell_tunnel=False) return True def ProcessEofRead(self, tell_tunnel=True): self.read_eof = True self.Shutdown(socket.SHUT_RD) if tell_tunnel and self.tunnel: self.tunnel.SendStreamEof(self.sid, read_eof=True) return self.ProcessEof() def ProcessEofWrite(self, tell_tunnel=True): self.write_eof = True if not self.write_blocked: self.Shutdown(socket.SHUT_WR) if tell_tunnel and self.tunnel: self.tunnel.SendStreamEof(self.sid, write_eof=True) return self.ProcessEof() def Send(self, data, try_flush=False): rv = Selectable.Send(self, data, try_flush=try_flush) if self.write_eof and not self.write_blocked: self.Shutdown(socket.SHUT_WR) return rv def ProcessData(self, data): if not self.tunnel: self.LogError('No tunnel! %s' % self) return False if not self.tunnel.SendData(self, data): self.LogDebug('Send to tunnel failed') return False # Back off if tunnel is stuffed. if self.tunnel and len(self.tunnel.write_blocked) > 1024000: self.Throttle(delay=(len(self.tunnel.write_blocked)-204800)/max(50000, self.tunnel.write_speed)) if self.read_eof: return self.ProcessEofRead() return True class UnknownConn(MagicProtocolParser): """This class is a connection which we're not sure what is yet.""" def __init__(self, fd, address, on_port, conns): MagicProtocolParser.__init__(self, fd, address, on_port) self.peeking = True self.parser = HttpParser() self.conns = conns self.conns.Add(self) self.sid = -1 self.host = None self.proto = None def Cleanup(self, close=True): if self.conns: self.conns.Remove(self) MagicProtocolParser.Cleanup(self, close=close) self.conns = self.parser = None def __str__(self): return '%s (%s/%s:%s)' % (MagicProtocolParser.__str__(self), (self.proto or '?'), (self.on_port or '?'), (self.host or '?')) def ProcessEofRead(self): self.read_eof = True return self.ProcessEof() def ProcessEofWrite(self): self.read_eof = True return self.ProcessEof() def ProcessLine(self, line, lines): if not self.parser: return True if self.parser.Parse(line) is False: return False if self.parser.state != self.parser.IN_BODY: return True done = False if self.parser.method == 'PING': self.Send('PONG %s\r\n\r\n' % self.parser.path) self.read_eof = self.write_eof = done = True self.fd.close() elif self.parser.method == 'CONNECT': if self.parser.path.lower().startswith('pagekite:'): if Tunnel.FrontEnd(self, lines, self.conns) is None: return False done = True else: try: connect_parser = self.parser chost, cport = connect_parser.path.split(':', 1) cport = int(cport) chost = chost.lower() sid1 = ':%s' % chost sid2 = '-%s:%s' % (cport, chost) tunnels = self.conns.tunnels # These allow explicit CONNECTs to direct https or raw backends. # If no match is found, we fall through to default HTTP processing. if cport == 443: if (('https'+sid1) in tunnels) or ( ('https'+sid2) in tunnels) or ( chost in self.conns.config.tls_endpoints): (self.on_port, self.host) = (cport, chost) self.parser = HttpParser() self.Send(HTTP_ConnectOK()) return self.ProcessTls(''.join(lines), chost) if (cport in self.conns.config.server_raw_ports or VIRTUAL_PN in self.conns.config.server_raw_ports): if (('raw'+sid1) in tunnels) or (('raw'+sid2) in tunnels): (self.on_port, self.host) = (cport, chost) self.parser = HttpParser() self.Send(HTTP_ConnectOK()) return self.ProcessRaw(''.join(lines), self.host) except ValueError: pass if (not done and self.parser.method == 'POST' and self.parser.path in MAGIC_PATHS): # FIXME: DEPRECATE: Make this go away! if Tunnel.FrontEnd(self, lines, self.conns) is None: return False done = True if not done: if not self.host: hosts = self.parser.Header('Host') if hosts: self.host = hosts[0].lower() else: self.Send(HTTP_Response(400, 'Bad request', ['

    400 Bad request

    ', '

    Invalid request, no Host: found.

    ', ''])) return False if self.parser.path.startswith(MAGIC_PREFIX): try: self.host = self.parser.path.split('/')[2] self.proto = 'probe' except ValueError: pass if self.proto is None: self.proto = 'http' upgrade = self.parser.Header('Upgrade') if 'websocket' in self.conns.config.server_protos: if upgrade and upgrade[0].lower() == 'websocket': self.proto = 'websocket' address = self.address if int(self.on_port) in self.conns.config.server_portalias: xfwdf = self.parser.Header('X-Forwarded-For') if xfwdf and address[0] == '127.0.0.1': address = (xfwdf[0], address[1]) done = True if UserConn.FrontEnd(self, address, self.proto, self.host, self.on_port, self.parser.lines + lines, self.conns) is None: if self.proto == 'probe': self.Send(HTTP_NoFeConnection()) else: self.Send(HTTP_Unavailable('fe', self.proto, self.host, frame_url=self.conns.config.error_url)) return False # We are done! self.Cleanup(close=False) return True def ProcessTls(self, data, domain=None): if domain: domains = [domain] else: try: domains = self.GetSni(data) if not domains: domains = [self.conns.LastIpDomain(self.address[0]) or self.conns.config.tls_default] LogDebug('No SNI - trying: %s' % domains[0]) if not domains[0]: domains = None except Exception: # Probably insufficient data, just return True and assume we'll have # better luck on the next round. return True if domains: # If we know how to terminate the TLS/SSL, do so! ctx = self.conns.config.GetTlsEndpointCtx(domains[0]) if ctx: self.fd = SSL_Connect(ctx, self.fd, accepted=True, server_side=True) self.peeking = False self.is_tls = False return True if domains and domains[0] is not None: self.EatPeeked() if UserConn.FrontEnd(self, self.address, 'https', domains[0], self.on_port, [data], self.conns) is None: return False # We are done! self.Cleanup(close=False) return True def ProcessRaw(self, data, domain): if UserConn.FrontEnd(self, self.address, 'raw', domain, self.on_port, [data], self.conns) is None: return False # We are done! self.Cleanup(close=False) return True class RawConn(Selectable): """This class is a raw/timed connection.""" def __init__(self, fd, address, on_port, conns): Selectable.__init__(self, fd, address, on_port) domain = conns.LastIpDomain(address[0]) if domain and UserConn.FrontEnd(self, address, 'raw', domain, on_port, [], conns): self.Cleanup(close=False) else: self.Cleanup() class Listener(Selectable): """This class listens for incoming connections and accepts them.""" def __init__(self, host, port, conns, backlog=100, connclass=UnknownConn): Selectable.__init__(self) self.fd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.fd.bind((host, port)) self.fd.listen(backlog) self.fd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.Log([('listen', '%s:%s' % (host, port))]) self.connclass = connclass self.port = port self.conns = conns self.conns.Add(self) def __str__(self): return '%s port=%s' % (Selectable.__str__(self), self.port) def __html__(self): return '

    Listening on port %s

    ' % self.port def ReadData(self, maxread=None): try: client, address = self.fd.accept() if client: self.Log([('accept', '%s:%s' % (obfuIp(address[0]), address[1]))]) uc = self.connclass(client, address, self.port, self.conns) return True except Exception, e: LogDebug('Listener::ReadData: %s' % e) return False class TunnelManager(threading.Thread): """Create new tunnels as necessary or kill idle ones.""" def __init__(self, pkite, conns): threading.Thread.__init__(self) self.pkite = pkite self.conns = conns def CheckTunnelQuotas(self, now): for tid in self.conns.tunnels: for tunnel in self.conns.tunnels[tid]: tunnel.RecheckQuota(self.conns, when=now) def PingTunnels(self, now): dead = {} for tid in self.conns.tunnels: for tunnel in self.conns.tunnels[tid]: grace = max(40, len(tunnel.write_blocked)/(tunnel.write_speed or 0.001)) if tunnel.last_activity < tunnel.last_ping-(5+grace): dead['%s' % tunnel] = tunnel elif tunnel.last_activity < now-30 and tunnel.last_ping < now-2: tunnel.SendPing() for tunnel in dead.values(): Log([('dead', tunnel.server_info[tunnel.S_NAME])]) self.conns.Remove(tunnel) tunnel.Cleanup() def quit(self): self.keep_running = False def run(self): self.keep_running = True while self.keep_running: try: self._run() except Exception, e: LogError('TunnelManager died: %s' % e) time.sleep(5) def _run(self): check_interval = 5 while self.keep_running: # Reconnect if necessary, randomized exponential fallback. if self.pkite.CreateTunnels(self.conns) > 0: check_interval += int(random.random()*check_interval) if check_interval > 300: check_interval = 300 else: check_interval = 5 # If all connected, make sure tunnels are really alive. if self.pkite.isfrontend: self.CheckTunnelQuotas(time.time()) # FIXME: Front-ends should close dead back-end tunnels. else: self.PingTunnels(time.time()) for i in xrange(0, check_interval): if self.keep_running: time.sleep(1) class PageKite(object): """Configuration and master select loop.""" def __init__(self): self.isfrontend = False self.motd = None self.upgrade_info = [] self.auth_domain = None self.auth_help_url = None self.server_host = '' self.server_ports = [80] self.server_raw_ports = [] self.server_portalias = {} self.server_aliasport = {} self.server_protos = ['http', 'https', 'websocket', 'raw'] self.tls_default = None self.tls_endpoints = {} self.fe_certname = [] self.daemonize = False self.pidfile = None self.logfile = None self.setuid = None self.setgid = None self.ui_request_handler = UiRequestHandler self.ui_http_server = UiHttpServer self.ui_sspec = None self.ui_httpd = None self.ui_password = None self.ui_pemfile = None self.disable_zchunks = False self.enable_sslzlib = False self.buffer_max = 1024 self.error_url = None self.tunnel_manager = None self.client_mode = 0 self.socks_server = None self.require_all = False self.no_probes = False self.servers = [] self.servers_manual = [] self.servers_auto = None self.servers_new_only = False self.servers_no_ping = False self.servers_preferred = [] self.servers_sessionids = {} self.dyndns = None self.last_updates = [] self.backends = {} # These are the backends we want tunnels for. self.conns = None self.looping = False self.main_loop = True self.crash_report_url = '%scgi-bin/crashes.pl' % WWWHOME self.rcfile_recursion = 0 self.rcfiles_loaded = [] self.savefile = None self.reloadfile = None # Searching for our configuration file! We prefer the documented # 'standard' locations, but if nothing is found there and something local # exists, use that instead. try: if os.getenv('USERPROFILE'): # Windows self.rcfile = os.path.join(os.getenv('USERPROFILE'), 'pagekite.cfg') self.devnull = 'nul' else: # Everything else self.rcfile = os.path.join(os.getenv('HOME'), '.pagekite.rc') self.devnull = '/dev/null' except Exception, e: # The above stuff may fail in some cases, e.g. on Android in SL4A. self.rcfile = 'pagekite.cfg' self.devnull = '/dev/null' if not os.path.exists(self.rcfile): for rcf in ('pagekite.rc', 'pagekite.cfg'): prog_rcf = os.path.join(os.path.dirname(sys.argv[0]), rcf) if os.path.exists(prog_rcf): self.rcfile = prog_rcf elif os.path.exists(rcf): self.rcfile = rcf # Look for CA Certificates. If we don't find them in the host OS, # we assume there might be something good in the config file. self.ca_certs_default = '/etc/ssl/certs/ca-certificates.crt' if not os.path.exists(self.ca_certs_default): self.ca_certs_default = self.rcfile self.ca_certs = self.ca_certs_default def PrintSettings(self): print '### Current settings for PageKite v%s. ###' % APPVER print print '# HTTP control-panel settings:' print (self.ui_sspec and 'httpd=%s:%d' % self.ui_sspec or '#httpd=host:port') print (self.ui_password and 'httppass=%s' % self.ui_password or '#httppass=YOURSECRET') print (self.ui_pemfile and 'pemfile=%s' % self.ui_pemfile or '#pemfile=/path/to/sslcert.pem') print print '# Back-end Options:' print (self.servers_auto and 'frontends=%d:%s:%d' % self.servers_auto or '#frontends=1:frontends.b5p.us:443') for server in self.servers_manual: print 'frontend=%s' % server for server in self.fe_certname: print 'fe_certname=%s' % server if self.dyndns: provider, args = self.dyndns for prov in DYNDNS: if DYNDNS[prov] == provider and prov != 'beanstalks.net': args['prov'] = prov if 'prov' not in args: args['prov'] = provider if args['pass']: print 'dyndns=%(user)s:%(pass)s@%(prov)s' % args elif args['user']: print 'dyndns=%(user)s@%(prov)s' % args else: print 'dyndns=%(prov)s' % args else: print '#dyndns=pagekite.net OR' print '#dyndns=user:pass@dyndns.org OR' print '#dyndns=user:pass@no-ip.com' bprinted = 0 for bid in self.backends: be = self.backends[bid] if be[BE_BACKEND]: print 'backend=%s:%s:%s' % (bid, be[BE_BACKEND], be[BE_SECRET]) bprinted += 1 if bprinted == 0: print '#backend=http:YOU.pagekite.me:localhost:80:SECRET' print '#backend=https:YOU.pagekite.me:localhost:443:SECRET' print '#backend=websocket:YOU.pagekite.me:localhost:8080:SECRET' print (self.error_url and ('errorurl=%s' % self.error_url) or '#errorurl=http://host/page/') print (self.servers_new_only and 'new' or '#new') print (self.require_all and 'all' or '#all') print (self.no_probes and 'noprobes' or '#noprobes') print eprinted = 0 print '# Domains we terminate SSL/TLS for natively, with key/cert-files' for ep in self.tls_endpoints: print 'tls_endpoint=%s:%s' % (ep, self.tls_endpoints[ep][0]) eprinted += 1 if eprinted == 0: print '#tls_endpoint=DOMAIN:PEM_FILE' print (self.tls_default and 'tls_default=%s' % self.tls_default or '#tls_default=DOMAIN') print print print '### The following stuff can usually be ignored. ###' print print '# Includes (should usually be at the top of the file)' print '#optfile=/path/to/common/settings' print print '# Front-end Options:' print (self.isfrontend and 'isfrontend' or '#isfrontend') comment = (self.isfrontend and '' or '#') print (self.server_host and '%shost=%s' % (comment, self.server_host) or '#host=machine.domain.com') print '%sports=%s' % (comment, ','.join(['%s' % x for x in self.server_ports] or [])) print '%sprotos=%s' % (comment, ','.join(['%s' % x for x in self.server_protos] or [])) for pa in self.server_portalias: print 'portalias=%s:%s' % (int(pa), int(self.server_portalias[pa])) print '%srawports=%s' % (comment, ','.join(['%s' % x for x in self.server_raw_ports] or [])) print (self.auth_domain and '%sauthdomain=%s' % (comment, self.auth_domain) or '#authdomain=foo.com') for bid in self.backends: be = self.backends[bid] if not be[BE_BACKEND]: print 'domain=%s:%s' % (bid, be[BE_SECRET]) print '#domain=http:*.pagekite.me:SECRET1' print '#domain=http,https,websocket:THEM.pagekite.me:SECRET2' print print '# Systems administration settings:' print (self.logfile and 'logfile=%s' % self.logfile or '#logfile=/path/file') print (self.daemonize and 'daemonize' % self.logfile or '#daemonize') if self.setuid and self.setgid: print 'runas=%s:%s' % (self.setuid, self.setgid) elif self.setuid: print 'runas=%s' % self.setuid else: print '#runas=uid:gid' print (self.pidfile and 'pidfile=%s' % self.pidfile or '#pidfile=/path/file') if self.ca_certs != self.ca_certs_default: print 'ca_certs=%s' % self.ca_certs else: print '#ca_certs=%s' % self.ca_certs print def FallDown(self, message, help=True, noexit=False): if self.conns and self.conns.auth: self.conns.auth.quit() if self.ui_httpd: self.ui_httpd.quit() if self.tunnel_manager: self.tunnel_manager.quit() self.conns = self.ui_httpd = self.tunnel_manager = None if help: print DOC print '*****' if message: print 'Error: %s' % message if not noexit: sys.exit(1) def GetTlsEndpointCtx(self, domain): if domain in self.tls_endpoints: return self.tls_endpoints[domain][1] parts = domain.split('.') # Check for wildcards ... while len(parts) > 2: parts[0] = '*' domain = '.'.join(parts) if domain in self.tls_endpoints: return self.tls_endpoints[domain][1] parts.pop(0) return None def GetBackendData(self, proto, domain, field, recurse=True): backend = '%s:%s' % (proto.lower(), domain.lower()) if backend in self.backends: if BE_STATUS_DISABLED != self.backends[backend][BE_STATUS]: return self.backends[backend][field] if recurse: dparts = domain.split('.') while len(dparts) > 1: dparts = dparts[1:] data = self.GetBackendData(proto, '.'.join(['*'] + dparts), field, recurse=False) if data: return data return None def GetBackendServer(self, proto, domain, recurse=True): server = self.GetBackendData(proto, domain, BE_BACKEND) if server == '-': return None return server def IsSignatureValid(self, sign, secret, proto, domain, srand, token): return checkSignature(sign=sign, secret=secret, payload='%s:%s:%s:%s' % (proto, domain, srand, token)) def LookupDomainQuota(self, lookup): if not lookup.endswith('.'): lookup += '.' if DEBUG_IO: print '=== AUTH LOOKUP\n%s\n===' % lookup (hn, al, ips) = socket.gethostbyname_ex(lookup) # Extract auth error hints from domain name, if we got a CNAME reply. if al: error = hn.split('.')[0] else: error = None # If not an authentication error, quota should be encoded as an IP. ip = ips[0] if not ip.startswith(AUTH_ERRORS): o = [int(x) for x in ip.split('.')] return ((((o[0]*256 + o[1])*256 + o[2])*256 + o[3]), None) # Errors on real errors are final. if not ip.endswith(AUTH_ERR_USER_UNKNOWN): return (None, error) # User unknown, fall through to local test. return (-1, error) def GetDomainQuota(self, protoport, domain, srand, token, sign, recurse=True, check_token=True): if '-' in protoport: try: proto, port = protoport.split('-', 1) if proto == 'raw': port_list = self.server_raw_ports else: port_list = self.server_ports porti = int(port) if porti in self.server_aliasport: porti = self.server_aliasport[porti] if porti not in port_list and VIRTUAL_PN not in port_list: LogInfo('Unsupported port request: %s (%s:%s)' % (porti, protoport, domain)) return (None, 'port') except ValueError: LogError('Invalid port request: %s:%s' % (protoport, domain)) return (None, 'port') else: proto, port = protoport, None if proto not in self.server_protos: LogInfo('Invalid proto request: %s:%s' % (protoport, domain)) return (None, 'proto') data = '%s:%s:%s' % (protoport, domain, srand) auth_error_type = None if (not token) or (not check_token) or checkSignature(sign=token, payload=data): if self.auth_domain: try: lookup = '.'.join([srand, token, sign, protoport, domain, self.auth_domain]) (rv, auth_error_type) = self.LookupDomainQuota(lookup) if rv is None or rv >= 0: return (rv, auth_error_type) except Exception, e: # Lookup failed, fail open. LogError('Quota lookup failed: %s' % e) return (-2, None) secret = self.GetBackendData(protoport, domain, BE_SECRET) if not secret: secret = self.GetBackendData(proto, domain, BE_SECRET) if secret: if self.IsSignatureValid(sign, secret, protoport, domain, srand, token): return (-1, None) else: LogError('Invalid signature for: %s (%s)' % (domain, protoport)) return (None, auth_error_type or 'signature') LogInfo('No authentication found for: %s (%s)' % (domain, protoport)) return (None, auth_error_type or 'auth') def ConfigureFromFile(self, filename=None): if not filename: filename = self.rcfile if self.rcfile_recursion > 25: raise ConfigError('Nested too deep: %s' % filename) self.rcfiles_loaded.append(filename) optfile = open(filename) args = [] for line in optfile: line = line.strip() if line and not line.startswith('#'): if line.startswith('END'): break if not line.startswith('-'): line = '--%s' % line args.append(line) self.rcfile_recursion += 1 self.Configure(args) self.rcfile_recursion -= 1 return self def HelpAndExit(self): print DOC sys.exit(0) def Configure(self, argv): opts, args = getopt.getopt(argv, OPT_FLAGS, OPT_ARGS) # Complain about crap on the command-line. if args: raise ConfigError("Unknown arguments: %s" % args) for opt, arg in opts: if opt in ('-o', '--optfile'): self.ConfigureFromFile(arg) elif opt == '--reloadfile': self.ConfigureFromFile(arg) self.reloadfile = arg elif opt in ('-S', '--savefile'): if self.savefile: raise ConfigError('Multiple save-files!') self.ConfigureFromFile(arg) self.savefile = arg elif opt in ('-I', '--pidfile'): self.pidfile = arg elif opt in ('-L', '--logfile'): self.logfile = arg elif opt in ('-Z', '--daemonize'): self.daemonize = True elif opt in ('-U', '--runas'): import pwd import grp parts = arg.split(':') if len(parts) > 1: self.setuid, self.setgid = (pwd.getpwnam(parts[0])[2], grp.getgrnam(parts[1])[2]) else: self.setuid = pwd.getpwnam(parts[0])[2] self.main_loop = False elif opt in ('-X', '--httppass'): self.ui_password = arg elif opt in ('-P', '--pemfile'): self.ui_pemfile = arg elif opt in ('-H', '--httpd'): parts = arg.split(':') host = parts[0] or 'localhost' if len(parts) > 1: self.ui_sspec = (host, int(parts[1])) else: self.ui_sspec = (host, 80) elif opt == '--tls_default': self.tls_default = arg elif opt == '--tls_endpoint': name, pemfile = arg.split(':', 1) ctx = SSL.Context(SSL.SSLv23_METHOD) ctx.use_privatekey_file(pemfile) ctx.use_certificate_chain_file(pemfile) self.tls_endpoints[name] = (pemfile, ctx) elif opt in ('-D', '--dyndns'): if arg.startswith('http'): self.dyndns = (arg, {'user': '', 'pass': ''}) elif '@' in arg: splits = arg.split('@') provider = splits.pop() usrpwd = '@'.join(splits) if provider in DYNDNS: provider = DYNDNS[provider] if ':' in usrpwd: usr, pwd = usrpwd.split(':', 1) self.dyndns = (provider, {'user': usr, 'pass': pwd}) else: self.dyndns = (provider, {'user': usrpwd, 'pass': ''}) else: if arg in DYNDNS: arg = DYNDNS[arg] self.dyndns = (arg, {'user': '', 'pass': ''}) elif opt in ('-p', '--ports'): self.server_ports = [int(x) for x in arg.split(',')] elif opt == '--portalias': port, alias = arg.split(':') self.server_portalias[int(port)] = int(alias) self.server_aliasport[int(alias)] = int(port) elif opt == '--protos': self.server_protos = [x.lower() for x in arg.split(',')] elif opt == '--rawports': self.server_raw_ports = [(x == VIRTUAL_PN and x or int(x)) for x in arg.split(',')] elif opt in ('-h', '--host'): self.server_host = arg elif opt in ('-A', '--authdomain'): self.auth_domain = arg elif opt == '--authhelpurl': self.auth_help_url = arg elif opt == '--motd': self.motd = arg elif opt == '--noupgradeinfo': self.upgrade_info = [] elif opt == '--upgradeinfo': version, tag, md5, human_url, file_url = arg.split(';') self.upgrade_info.append((version, tag, md5, human_url, file_url)) elif opt in ('-f', '--isfrontend'): self.isfrontend = True global LOG_THRESHOLD LOG_THRESHOLD *= 4 elif opt in ('-a', '--all'): self.require_all = True elif opt in ('-N', '--new'): self.servers_new_only = True elif opt in ('--socksify', '--torify'): try: import socks (host, port) = arg.split(':') socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, host, int(port)) self.socks_server = (host, port) # This increases the odds of unrelated requests getting lumped # together in the tunnel, which makes traffic analysis harder. global SEND_ALWAYS_BUFFERS SEND_ALWAYS_BUFFERS = True except Exception, e: raise ConfigError("Please instally SocksiPy: " " http://code.google.com/p/socksipy-branch/") if opt == '--torify': self.servers_new_only = True # Disable initial DNS lookups (leaks) self.servers_no_ping = True # Disable front-end pings self.crash_report_url = None # Disable crash reports socks.wrapmodule(urllib) # Make DynDNS updates go via tor elif opt == '--ca_certs': self.ca_certs = arg elif opt == '--fe_certname': self.fe_certname.append(arg.lower()) elif opt == '--frontend': self.servers_manual.append(arg) elif opt == '--frontends': count, domain, port = arg.split(':') self.servers_auto = (int(count), domain, int(port)) elif opt in ('--errorurl', '-E'): self.error_url = arg elif opt == '--backend': protos, domain, bhost, bport, secret = arg.split(':') for proto in protos.split(','): proto = proto.replace('/', '-') if '-' in proto: proto, port = proto.split('-') bid = '%s-%d:%s' % (proto.lower(), int(port), domain.lower()) else: port = '' bid = '%s:%s' % (proto.lower(), domain.lower()) backend = '%s:%s' % (bhost.lower(), bport) if bid in self.backends: raise ConfigError("Same backend/domain defined twice: %s" % bid) self.backends[bid] = (proto.lower(), port, domain.lower(), backend, secret, BE_STATUS_UNKNOWN) elif opt == '--domain': protos, domain, secret = arg.split(':') if protos in ('*', ''): protos = ','.join(self.server_protos) for proto in protos.split(','): bid = '%s:%s' % (proto, domain) if bid in self.backends: raise ConfigError("Same backend/domain defined twice: %s" % bid) self.backends[bid] = (proto, None, domain, None, secret, BE_STATUS_UNKNOWN) elif opt == '--noprobes': self.no_probes = True elif opt == '--nofrontend': self.isfrontend = False elif opt == '--nodaemonize': self.daemonize = False elif opt == '--noall': self.require_all = False elif opt == '--nozchunks': self.disable_zchunks = True elif opt == '--sslzlib': self.enable_sslzlib = True elif opt == '--debugio': global DEBUG_IO DEBUG_IO = True elif opt == '--buffers': self.buffer_max = int(arg) elif opt == '--nocrashreport': self.crash_report_url = None elif opt == '--clean': pass elif opt == '--nopyopenssl': pass elif opt == '--noloop': self.main_loop = False elif opt == '--defaults': self.dyndns = (DYNDNS['pagekite.net'], {'user': '', 'pass': ''}) self.servers_auto = (1, 'frontends.b5p.us', 443) #self.fe_certname = ['frontends.b5p.us', 'b5p.us'] elif opt == '--settings': self.PrintSettings() sys.exit(0) else: self.HelpAndExit() return self def CheckConfig(self): if not self.servers_manual and not self.servers_auto and not self.isfrontend: if not self.servers: raise ConfigError('Nothing to do! List some servers, or run me as one.') return self def CheckAllTunnels(self, conns): missing = [] for backend in self.backends: proto, domain = backend.split(':') if not conns.Tunnel(proto, domain): missing.append(domain) if missing: self.FallDown('No tunnel for %s' % missing, help=False) def Ping(self, host, port): if self.servers_no_ping: return 0 start = time.time() try: fd = rawsocket(socket.AF_INET, socket.SOCK_STREAM) try: fd.settimeout(2.0) # Missing in Python 2.2 except Exception: fd.setblocking(1) fd.connect((host, port)) fd.send('PING / HTTP/1.0\r\n\r\n') fd.recv(1024) fd.close() except Exception, e: LogDebug('Ping %s:%s failed: %s' % (host, port, e)) return 100000 elapsed = (time.time() - start) LogDebug('Pinged %s:%s: %f' % (host, port, elapsed)) return elapsed def GetHostIpAddr(self, host): return socket.gethostbyname(host) def GetHostDetails(self, host): return socket.gethostbyname_ex(host) def ChooseFrontEnds(self): self.servers = [] self.servers_preferred = [] # Enable internal loopback if self.isfrontend: need_loopback = False for be in self.backends.values(): if be[BE_BACKEND]: need_loopback = True if need_loopback: self.servers.append(LOOPBACK_FE) # Convert the hostnames into IP addresses... for server in self.servers_manual: (host, port) = server.split(':') try: ipaddr = self.GetHostIpAddr(host) server = '%s:%s' % (ipaddr, port) if server not in self.servers: self.servers.append(server) self.servers_preferred.append(ipaddr) except Exception, e: LogDebug('DNS lookup failed for %s' % host) # Lookup and choose from the auto-list (and our old domain). if self.servers_auto: (count, domain, port) = self.servers_auto # First, check for old addresses and always connect to those. if not self.servers_new_only: for bid in self.backends: (proto, bdom) = bid.split(':') try: (hn, al, ips) = self.GetHostDetails(bdom) for ip in ips: server = '%s:%s' % (ip, port) if server not in self.servers: self.servers.append(server) except Exception, e: LogDebug('DNS lookup failed for %s' % bdom) try: (hn, al, ips) = socket.gethostbyname_ex(domain) times = [self.Ping(ip, port) for ip in ips] except Exception, e: LogDebug('Unreachable: %s, %s' % (domain, e)) ips = times = [] while count > 0 and ips: count -= 1 mIdx = times.index(min(times)) server = '%s:%s' % (ips[mIdx], port) if server not in self.servers: self.servers.append(server) if ips[mIdx] not in self.servers_preferred: self.servers_preferred.append(ips[mIdx]) del times[mIdx] del ips[mIdx] def CreateTunnels(self, conns): live_servers = conns.TunnelServers() failures = 0 connections = 0 if self.backends: if not self.servers or len(self.servers) > len(live_servers): self.ChooseFrontEnds() for server in self.servers: if server not in live_servers: if server == LOOPBACK_FE: LoopbackTunnel.Loop(conns, self.backends) else: if Tunnel.BackEnd(server, self.backends, self.require_all, conns): Log([('connect', server)]) connections += 1 else: failures += 1 LogInfo('Failed to connect', [('FE', server)]) if self.dyndns: updates = {} ddns_fmt, ddns_args = self.dyndns for bid in self.backends.keys(): proto, domain = bid.split(':') if bid in conns.tunnels: ips = [] bips = [] for tunnel in conns.tunnels[bid]: ip = tunnel.server_info[tunnel.S_NAME].split(':')[0] if not ip == LOOPBACK_HN: if not self.servers_preferred or ip in self.servers_preferred: ips.append(ip) else: bips.append(ip) if not ips: ips = bips if ips: iplist = ','.join(ips) payload = '%s:%s' % (domain, iplist) args = {} args.update(ddns_args) args.update({ 'domain': domain, 'ip': ips[0], 'ips': iplist, 'sign': signToken(secret=self.backends[bid][BE_SECRET], payload=payload, length=100) }) # FIXME: This may fail if different front-ends support different # protocols. In practice, this should be rare. update = ddns_fmt % args if domain not in updates or len(update) < len(updates[domain]): updates[payload] = update last_updates = self.last_updates self.last_updates = [] for update in updates: if update not in last_updates: try: result = ''.join(urllib.urlopen(updates[update]).readlines()) self.last_updates.append(update) if result.startswith('good') or result.startswith('nochg'): Log([('dyndns', result), ('data', update)]) else: LogInfo('DynDNS update failed: %s' % result, [('data', update)]) failures += 1 except Exception, e: LogInfo('DynDNS update failed: %s' % e, [('data', update)]) failures += 1 if not self.last_updates: self.last_updates = last_updates return failures def LogTo(self, filename, close_all=True, dont_close=[]): global Log if filename == 'memory': Log = LogToMemory filename = self.devnull elif filename == 'syslog': Log = LogSyslog filename = self.devnull syslog.openlog((sys.argv[0] or 'pagekite.py').split('/')[-1], syslog.LOG_PID, syslog.LOG_DAEMON) if filename != 'stdio': global LogFile try: LogFile = fd = open(filename, "a", 0) os.dup2(fd.fileno(), sys.stdin.fileno()) os.dup2(fd.fileno(), sys.stdout.fileno()) os.dup2(fd.fileno(), sys.stderr.fileno()) except Exception, e: raise ConfigError('%s' % e) def Daemonize(self): # Fork once... if os.fork() != 0: os._exit(0) # Fork twice... os.setsid() if os.fork() != 0: os._exit(0) def SelectLoop(self): global buffered_bytes conns = self.conns last_loop = time.time() self.looping = True iready, oready, eready = None, None, None while self.looping: isocks, osocks = conns.Readable(), conns.Blocked() try: if isocks or osocks: iready, oready, eready = select.select(isocks, osocks, [], 1.1) else: # Windoes does not seem to like empty selects, so we do this instead. time.sleep(0.5) except KeyboardInterrupt, e: raise KeyboardInterrupt() except Exception, e: LogError('Error in select: %s (%s/%s)' % (e, isocks, osocks)) conns.CleanFds() last_loop -= 1 now = time.time() if not iready and not oready: if (isocks or osocks) and (now < last_loop + 1): LogError('Spinning, pausing ...') time.sleep(0.1) if oready: for socket in oready: conn = conns.Connection(socket) if conn and not conn.Send([], try_flush=True): # LogDebug("Write error in main loop, closing %s" % conn) conns.Remove(conn) conn.Cleanup() if buffered_bytes < 1024 * self.buffer_max: throttle = None else: LogDebug("FIXME: Nasty pause to let buffers clear!") time.sleep(0.1) throttle = 1024 if iready: for socket in iready: conn = conns.Connection(socket) if conn and not conn.ReadData(maxread=throttle): # LogDebug("Read error in main loop, closing %s" % conn) conns.Remove(conn) conn.Cleanup() for conn in conns.DeadConns(): conns.Remove(conn) conn.Cleanup() last_loop = now def Loop(self): self.conns.start() if self.ui_httpd: self.ui_httpd.start() if self.tunnel_manager: self.tunnel_manager.start() try: epoll = select.epoll() except Exception, msg: epoll = None if epoll: LogDebug("FIXME: Should try epoll!") self.SelectLoop() def Start(self): conns = self.conns = Connections(self) global Log # Log that we've started up config_report = [('started', sys.argv[0]), ('version', APPVER), ('argv', ' '.join(sys.argv[1:])), ('ca_certs', self.ca_certs)] for optf in self.rcfiles_loaded: config_report.append(('optfile', optf)) Log(config_report) try: # Set up our listeners if we are a server. if self.isfrontend: for port in self.server_ports: Listener(self.server_host, port, conns) for port in self.server_raw_ports: if port != VIRTUAL_PN and port > 0: Listener(self.server_host, port, conns, connclass=RawConn) # Start the UI thread if self.ui_sspec: self.ui_httpd = HttpUiThread(self, conns, handler=self.ui_request_handler, server=self.ui_http_server, ssl_pem_filename = self.ui_pemfile) # Create the Tunnel Manager self.tunnel_manager = TunnelManager(self, conns) except Exception, e: Log = LogToFile FlushLogMemory() raise ConfigError(e) # Create log-file Log = LogToFile if self.logfile: keep_open = [s.fd.fileno() for s in conns.conns] if self.ui_httpd: keep_open.append(self.ui_httpd.httpd.socket.fileno()) self.LogTo(self.logfile, dont_close=keep_open) # Flush in-memory log, if necessary FlushLogMemory() # Set up SIGHUP handler. if self.logfile or self.reloadfile: try: import signal def reopen(x,y): if self.logfile: self.LogTo(self.logfile, close_all=False) LogDebug('SIGHUP received, reopening: %s' % self.logfile) if self.reloadfile: self.ConfigureFromFile(self.reloadfile) signal.signal(signal.SIGHUP, reopen) except Exception: LogError('Warning: signal handler unavailable, logrotate will not work.') # Disable compression in OpenSSL if not self.enable_sslzlib: DisableSSLCompression() # Daemonize! if self.daemonize: self.Daemonize() # Create global secret globalSecret() # Create PID file if self.pidfile: pf = open(self.pidfile, 'w') pf.write('%s\n' % os.getpid()) pf.close() # Do this after creating the PID and log-files. if self.daemonize: os.chdir('/') # Drop privileges, if we have any. if self.setgid: os.setgid(self.setgid) if self.setuid: os.setuid(self.setuid) if self.setuid or self.setgid: Log([('uid', os.getuid()), ('gid', os.getgid())]) # Make sure we have what we need if self.require_all: self.CreateTunnels(conns) self.CheckAllTunnels(conns) # Finally, run our select/epoll loop. self.Loop() Log([('stopping', 'pagekite.py')]) if self.ui_httpd: self.ui_httpd.quit() if self.tunnel_manager: self.tunnel_manager.quit() if self.conns: if self.conns.auth: self.conns.auth.quit() for conn in self.conns.conns: conn.Cleanup() ##[ Main ]##################################################################### def Main(pagekite, configure): crashes = 1 while True: pk = pagekite() try: try: try: configure(pk) except Exception, e: raise ConfigError(e) pk.Start() except (ConfigError, getopt.GetoptError), msg: pk.FallDown(msg) except KeyboardInterrupt, msg: pk.FallDown(None, help=False, noexit=True) return except SystemExit: sys.exit(0) except Exception, msg: traceback.print_exc(file=sys.stderr) if pk.crash_report_url: try: print 'Submitting crash report to %s' % pk.crash_report_url LogDebug(''.join(urllib.urlopen(pk.crash_report_url, urllib.urlencode({ 'crash': traceback.format_exc() })).readlines())) except Exception, e: print 'FAILED: %s' % e pk.FallDown(msg, help=False, noexit=pk.main_loop) # If we get this far, then we're looping. Clean up. sockets = pk.conns and pk.conns.Sockets() or [] for fd in sockets: fd.close() # Exponential fall-back. LogDebug('Restarting in %d seconds...' % (2 ** crashes)) time.sleep(2 ** crashes) crashes += 1 if crashes > 9: crashes = 9 def Configure(pk): if '--appver' in sys.argv: print '%s' % APPVER sys.exit(0) if '--clean' not in sys.argv: if os.path.exists(pk.rcfile): pk.ConfigureFromFile() pk.Configure(sys.argv[1:]) pk.CheckConfig() if __name__ == '__main__': Main(PageKite, Configure) # vi:ts=2 expandtab pagekite-0.5.8a/scripts/legacy-testing/pagekite-0.5.6d.py0000775000175000017500000047743512603542202022426 0ustar brebre00000000000000#!/usr/bin/python # # WARNING: This file is a combination of multiple Python files. # The source code lives here: http://pagekite.org/ # # This file is part of pagekite.py (version 0.5.6d) # Copyright 2010-2012, the Beanstalks Project ehf. and Bjarni Runar Einarsson # # This program is free software: you can redistribute it and/or modify it under # the terms of the GNU Affero General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more # details. # ##[ Combined with Breeder: http://pagekite.net/wiki/Floss/PyBreeder/ ]######### import base64, imp, os, sys, StringIO, zlib __FILES = {} __os_path_exists = os.path.exists __os_path_getsize = os.path.getsize __builtin_open = open def __comb_open(filename, *args, **kwargs): if filename in __FILES: return StringIO.StringIO(__FILES[filename]) else: return __builtin_open(filename, *args, **kwargs) def __comb_exists(filename, *args, **kwargs): if filename in __FILES: return True else: return __os_path_exists(filename, *args, **kwargs) def __comb_getsize(filename, *args, **kwargs): if filename in __FILES: return len(__FILES[filename]) else: return __os_path_getsize(filename, *args, **kwargs) if 'b64decode' in dir(base64): __b64d = base64.b64decode else: __b64d = base64.decodestring open = __comb_open os.path.exists = __comb_exists os.path.getsize = __comb_getsize sys.path[0:0] = ['.SELF/'] ############################################################################### __FILES[".SELF/sockschain/__init__.py"] = zlib.decompress(__b64d("""\ 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 """)) sys.modules["sockschain"] = imp.new_module("sockschain") sys.modules["sockschain"].open = __comb_open exec __FILES[".SELF/sockschain/__init__.py"] in sys.modules["sockschain"].__dict__ ############################################################################### __FILES[".SELF/pagekite/__init__.py"] = zlib.decompress(__b64d("""\ eNqtk8Fv2jAUxu/+Kz7BZZNY6NZbu00KKLSRGEUhqELaxSQviVtjR7YDyn+/F2jFYdJ2aQ62bL/3+fc 9v4zHH/mJZTpPVpsEPzAajX6LvFEeldIEnlvpAmzFc02vKlDU9pGY27Z3qm4Cvt18vfnCw+0EoSHMSB ofpH71WDv7QkUANVUEaUrMXqQzCllnpEOiePTeGnG5rnW2dvIw3Fg5InhbhZN0dIfediikgaNS+eDUv gsMFgbJqXU42FJV/bDRmZKcGCgCuYMfoIcFHlZbIK4qchYPZMhJjXW316rAUhVkPEEywLDjGyqx7895 C8YQmzcMLCzLy6CsmYAUnzscyXle4/b9pje1CRjrkwwDuYNth6TPjNsLLcM1L/rb+dVgCWXOmo1t2U/ DauzwpLTGntB5qjo9ATgUeE7zx6dtLuLVDs9xlsWrfHfPsaGxfExHuiipQ6sVC7MdJ03oB+pfSTZ/5P h4li7TfDeAL9J8lWw2YvGUIcY6zvJ0vl3GGdbbbP20SSJgQ3RWHAr777pW5wdyJEoKUmnPnnf8nJ7Jd IlGHomftSB1ZC6JgrvqvZb/1RZSW1OfbXLCtY7Ml1YwNkzgidvnexNCezednk6nqDZdZF091RcJP/0p uOHF+GP/pj+4Zhjw """)) sys.modules["pagekite"] = imp.new_module("pagekite") sys.modules["pagekite"].open = __comb_open exec __FILES[".SELF/pagekite/__init__.py"] in sys.modules["pagekite"].__dict__ ############################################################################### __FILES[".SELF/pagekite/common.py"] = zlib.decompress(__b64d("""\ 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 """)) sys.modules["pagekite.common"] = imp.new_module("pagekite.common") sys.modules["pagekite.common"].open = __comb_open sys.modules["pagekite"].common = sys.modules["pagekite.common"] exec __FILES[".SELF/pagekite/common.py"] in sys.modules["pagekite.common"].__dict__ ############################################################################### __FILES[".SELF/pagekite/compat.py"] = zlib.decompress(__b64d("""\ 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 jV9umP/9nfqKh1fcUWCOly6rPkTlkfv31h5xXg= """)) sys.modules["pagekite.compat"] = imp.new_module("pagekite.compat") sys.modules["pagekite.compat"].open = __comb_open sys.modules["pagekite"].compat = sys.modules["pagekite.compat"] exec __FILES[".SELF/pagekite/compat.py"] in sys.modules["pagekite.compat"].__dict__ ############################################################################### __FILES[".SELF/pagekite/logging.py"] = zlib.decompress(__b64d("""\ eNq1VlFv2zgMftevIHoY4ux8btLd3UN3HZB2SRsgS4okvaHIgkKNaUerYhmSkjT/fpRkN10HrNfi5gd LpMiPHylK9sHBARuoPBdFnrADEn77Xx826J91h5MunACBf2HTpTCQCYlAY8m1BZXRmOOdsJiUu4SdqX KnRb60cNRqt/6g17sY7BLhFHlhLJd3Bi61+ooLC7jMEuBFCqdfuS4EjNcF19AV9DZGFSyEK7XKNV+5i JlGBKMyu+Uaj2Gn1rDgBWhMhbFa3K4tEbMO8lBpWKlUZDunWBcpauZYWNQr40g7Ac6HVwCdLEOt4BwL 1FzC5fpWigUMxAILg8CJgNOYJaZwu/N+PaLBJhUN6CmC51aoIgYUtK5hg9qQDO/qSBVaDEQr4tYx16B K59Qkujsmud37JT9mvk8wBVF4zKUqKZ8loVGGWyEl3CKsDWZrGQOQKcDn/vRidDVlneE1fO6Mx53h9P o92dqlomXcYEASq1IKAqZ0NC/szrH+1B2fXZB957Q/6E+vHfFefzrsTiasNxpDBy4742n/7GrQGcPl1 fhyNOkmABNEj+gK+/O6Zn6DNLIULRfSUM7XtJ2GmMkUlnyDtK0LFBvixWFBXVXX8llsxqUqcp8mOezr SPz6GRTKxmCQ2uefpbXl8eHhdrtN8mKdKJ0fygBhDj/8itNEhVZ0ZqxYYT03O8Pq+UKtSk7saFxR+2d arSodVBZvH5RksFcyQpEqp1MazJMgM8roxtjUbfaJi5QEgbGP3dOr85v+iNQ9LqlkbDA6J2E2d5ObQX /ojnwrCN3h+fSCxHetoJhejLuTi9HgI+mO/vob3kK7dfQno53MgO6if7lco4k2fqCzj8a6hE+GqsDmM QPIpbqlTSOoGOpo1awzmd5M+5+6ZFWoLeGLwkY1gutBNybuFTWbZLRVOjWOd9SwphFD4819A94432ZM yw8PLdMqLRu3bM2NVTfCqMgZPrWUcg9U02vO62AJ3lss0mgW3W1mrXlMDgGUxPa8mWgsJV9g1PjiIja g0XyM/pPnkad+tWfxWs/34DzjF7i6+6ikXfAn+W7jbqWw5XO/MalYuKZzQ+QL57QPvfX7CbSDnPCydP X0Dt5z6T4vEouIVpvwAfY9eOy5kTxrHUd79WG71ZyH5iUDjXatCwhBY6hw6+ac+IPx0Jx+1XdmHLZ33 6Uiq/IJUR/DUaynfd4MHg3UuuFK4e2CZziL1ZGMKsmx747HVHCqfPJViSKaNU6q2X0o6r0HcnHnvtlR uggp3q7z/xzDH/QXRjH4DGh/2Bu9ALOu/VT1aGt/Te3rO0XlLoZrrTBLtpr+TKIX5P/EkY7U4ww+IX2 0dq/I4UfSHrQn12ZJaxXuk/uRBEdROookPiBFVUQfXdahZ9E2BjnbzkNiW+cl53v2Xa2VjlYmj92v2+ oRVyRluEVd/8ZAor/xRBYsj71FffV5VVWqyC00WbANn6Ukv+Y0BLLfqZINT1MfQ2l3W7eJueblSbvln z3Tj67Hn2EazsFruFZB+kWmnokhyOTVIcZo0Drdd5sauit2E/c3WH+f941HkZ+qQ8/XzcfYHpmxb/4i jh8= """)) sys.modules["pagekite.logging"] = imp.new_module("pagekite.logging") sys.modules["pagekite.logging"].open = __comb_open sys.modules["pagekite"].logging = sys.modules["pagekite.logging"] exec __FILES[".SELF/pagekite/logging.py"] in sys.modules["pagekite.logging"].__dict__ ############################################################################### __FILES[".SELF/pagekite/manual.py"] = zlib.decompress(__b64d("""\ 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 """)) sys.modules["pagekite.manual"] = imp.new_module("pagekite.manual") sys.modules["pagekite.manual"].open = __comb_open sys.modules["pagekite"].manual = sys.modules["pagekite.manual"] exec __FILES[".SELF/pagekite/manual.py"] in sys.modules["pagekite.manual"].__dict__ ############################################################################### __FILES[".SELF/pagekite/proto/__init__.py"] = zlib.decompress(__b64d("""\ eNq1k8tu2zAQRff8igt30wKunDa79AEohp0IdW1DlhEY6IaWRhITmhRIyob+PkM7QRYF2k3LBQk+5vL c4XA0GomiJU+QjhBawlo29EMFQudssKXVaKWptDINSi29J5+IEUe9+6dNLLLpbLmZ4RtY/BczKY9aaQ KPnXQBtuaxoSdGS7ohEVPbDU41bcDnq09XH7m7Hp8N3JI0Pkj95LF29pHKAGrrBOwCt4/SGYW8N9Jhp rj33hpxuY4NN04e4o21I4K3dThxWm4w2B6lNHBUKR+c2vecHxWi5MQ6HGyl6iEu9KYiJyJFIHfwETpO cLfcAmldk7O4I0NOaqz7vVYlFqokE/PPAHHFt1RhP5zj5owhNi8YmFuWl0FZMwYp3nc4kvM8x/XrTS9 qYzDWexkiuYPtYtAHxh2EluEtLvnd+ZvBCsqcNVvbxdJgNXZ4UlpjT+g91b0eA3wUeMiK+9W2EOlyh4 c0z9NlsfvCZ0NreZuOdFFSh04rFmY7TpowROqfs3x6z+fT22yRFbsIPs+K5WyzEfNVjhTrNC+y6XaR5 lhv8/VqM0uADV2qNSb2z3mtzw/kSFQUpNJcvWLHz+mZTFdc20fiZy1JHZlLouSqes3lX7WF1Ja/RbTJ AW95ZL6shrFhDE9cPl/bELqbyeR0OiWN6RPrmom+SPjJ9//xm8Qzp9Yr8w== """)) sys.modules["pagekite.proto"] = imp.new_module("pagekite.proto") sys.modules["pagekite.proto"].open = __comb_open sys.modules["pagekite"].proto = sys.modules["pagekite.proto"] exec __FILES[".SELF/pagekite/proto/__init__.py"] in sys.modules["pagekite.proto"].__dict__ ############################################################################### __FILES[".SELF/pagekite/proto/proto.py"] = zlib.decompress(__b64d("""\ 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 F6X8wzgfwBVEg1y """)) sys.modules["pagekite.proto.proto"] = imp.new_module("pagekite.proto.proto") sys.modules["pagekite.proto.proto"].open = __comb_open sys.modules["pagekite.proto"].proto = sys.modules["pagekite.proto.proto"] exec __FILES[".SELF/pagekite/proto/proto.py"] in sys.modules["pagekite.proto.proto"].__dict__ ############################################################################### __FILES[".SELF/pagekite/proto/parsers.py"] = zlib.decompress(__b64d("""\ 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 """)) sys.modules["pagekite.proto.parsers"] = imp.new_module("pagekite.proto.parsers") sys.modules["pagekite.proto.parsers"].open = __comb_open sys.modules["pagekite.proto"].parsers = sys.modules["pagekite.proto.parsers"] exec __FILES[".SELF/pagekite/proto/parsers.py"] in sys.modules["pagekite.proto.parsers"].__dict__ ############################################################################### __FILES[".SELF/pagekite/proto/selectables.py"] = zlib.decompress(__b64d("""\ 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 9ZeAo0jzi0zfH4bFc7v/X9Yd3uY= """)) sys.modules["pagekite.proto.selectables"] = imp.new_module("pagekite.proto.selectables") sys.modules["pagekite.proto.selectables"].open = __comb_open sys.modules["pagekite.proto"].selectables = sys.modules["pagekite.proto.selectables"] exec __FILES[".SELF/pagekite/proto/selectables.py"] in sys.modules["pagekite.proto.selectables"].__dict__ ############################################################################### __FILES[".SELF/pagekite/proto/filters.py"] = zlib.decompress(__b64d("""\ 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 """)) sys.modules["pagekite.proto.filters"] = imp.new_module("pagekite.proto.filters") sys.modules["pagekite.proto.filters"].open = __comb_open sys.modules["pagekite.proto"].filters = sys.modules["pagekite.proto.filters"] exec __FILES[".SELF/pagekite/proto/filters.py"] in sys.modules["pagekite.proto.filters"].__dict__ ############################################################################### __FILES[".SELF/pagekite/proto/conns.py"] = zlib.decompress(__b64d("""\ eNrVfWt320aS6Hf+CsQ+viBiipJsZzbLNZ0jS3SsjSxpRXk8WUWHByRBCSOIYADQNGd3/vutRz+BBkj a3nvPeiYiCXRXv6qrq+v55MmT1vV9lEdemEVecR95x+l8Hk2KOJ17kyTM8yjveFmUhEX8OUrW3n18d+ 8lEXyXr6FWWHj34XyaRK14Pkkf4/mdl2ZeuizuUvw+j4pVmj14EwU677aeQMtPv+u/1tnp8eB8OPD6H gD/A8YV594sTiIPPhdhVnjpDD7vooe4iLqLdbd1nC7WGQyo8F4cHB7swZ+XHZqEt1E4z4sweci9yyz9 O3Tai+5nXQ8G6b39e5jNY+9qOQ8zbxDD3zxP5y1ubpGld1n4iC3Osijy8nRWrGBqe946XXqTcA5zOY3 zIovHywI6ViDIfZisx3Qaz9b4YDmfRlkLe1FE2WOOncYf3q/nHz3vaDaLstT7NZpHWZh4l8txEk+8s3 gSzXENoQP4JL+Ppt54TfXeQTdaQ9EN710K4ENcgo4XxfA+8z5HWY6r/VK2JKB1cA3bsLTQc1jMBVYKo LvrFiCDrtetjlwPcOrFc4J5ny4iRhQY4SpOEm8cecs8mi2TjudBUc/7dHr9/uLjdevo/Hfv09HV1dH5 9e//BmWLe0AkDzCOIcWPiyQGwDCcLJwXa+z1h8HV8Xsof/T29Oz0+nfs+LvT6/PBcNh6d3HlHXmXR1f Xp8cfz46uvMuPV5cXw0HX84YRYzxObPO8zmiBsqg1jYowTgB7W7/DcubQs2QKqP85gmWdRLBDpl4IaL 5Yy7ncCLsVJilsERwmVNDzCP07nXnztOh4eQTo8/q+KBa9/f3VatW9my+7aXa3nzCIfP/N/8RugolOY c/k6eQhKtSvdS6/FvdZFE5hf6sH8WOkvmfhJBqHk4dWa5alj3rbAXlYIBZwsR+rbx8BFdVb8aX8HvCc v1UKJOndHZIcKCG+ig4AHQCkzSXo9zCbw2iyzOJi/Y5ecbE8SmCzh+Mkyit9zHIDgHyapUWqn7VaRBS 96yVQuqR9fL+cP1xSxaDX8pAsHQHaySYAaRYZ0N55QV32LmEUv8EovIKqd3FRodZwdH70AanaAf24vL i6HsKvQ/p1dfRJPXnB768uri/w50v6eXRyMvrt9HqAT17Rk9Ph6MMF7BQE+RM2MI1m3mgUz+NiNGrDD Mw6RKpz6rPnGaPoloot4z6VhIWZz+K77jIOqAq+7UJ5oBFQYZZCSzf+ly79r/fF73g3t/q/d2GC1IY+ bnX1U2iozf2gh0+9TxHs+jkgV4rUI1/iiZV6iIW013Ae4dEEu+sBjqXzCYCFEylZEpoykHsojusINXP cuJP1JMEzLk+9VeRN0yUuDILL439Ae5EPIJFg4RIVqQBCbQLxjLq6v4/hF3r8Iy6EmFUaQ3VGqTw9gI mhT/14SVjW9/7rn/pZFj2mRTTK86T04h9AMYo0W5ce48aL5tMyGEDNYrTAgTAuicerCI/AaDrKigLe7 B2anYHSoyJBSLQ++pXcULCyt3K4xwmcm8uFHHGS5lH/OltGYtjxzBgiP/KItuYxHRT6ZfchWuftQJYR TR4jwGEBs/zYhipBBTtd7dPfoIRW5+kcninUvy8eE4HTos0sKpbZ3Gv7r8dvhoTH3jx8jF7vj9/0vGf 563H2xledw3/+s9wPvGdeu4z6N/SA9zAgu72ZzIZVf87Su+ssnM3iybAIi2UuhjMDViPp0yqU5zOWEw VPqJieuMcoz4GswDL5J3EuODA4ppB64Uh86HNjl+0lWMZd0Sl/mq7mPuJ1kmZ9+fLXq8HvHdloX3wGA kYEXe/Vwit4zL6ah1+jAqnhVfTnMsoLOQ1Eh9Uq8StGQolNQFNn8RdEqLb/tz3FykFf4aeksX5gIuA4 x+IEuvueCESboRgo+NTTtXtM+HuP6+40fQzjOZ5MvSKFXdfL47s5jCiLVE0q2/G4JFAa4FumwGdiafg FxaH/47ybA2NTtP2eH6iacoDdcLGADd1uEyg45VbQw0CCVL8tjLT+VdvsiHkKAhPhZYNqb7zL0nkxgK YRdTreOJ2ubVoGhxTxf/D/FfJ3SDpnWGkPOgwdjPA58G6hh1SJHqplC+FqsDJPO4kXMCHiCDUOgCJbm 2u24zJzpVlES4O1EHKXT3V7zZ/7e++4WG5Wp90FDJlnnXjA6+JRPvoHHTt5z1oC3I6iwT5swP+kzZ/7 vdI60U4YzBGOKNKmG1b/MLBKRkkJ4NF0ioOsgejY0YoZuMUZBsrcDP9DOobr09bQFWOhoBubB0ja5AF QIZ7cy6tDjicucLfykgDH7LSrqsh7CfQjTaa+tYyfNy3gX7myvX4a4ueWsUTMSnbvfg/hwyxvveh+Dq fTti9g7DHlFL863mHHW2Xhon94wP+CltVfYPQSYIg39fqKi1WwzqiuWQf+OxqvR/HUXiEsDoN0F70Rw G5blSWFg+cUlrNtnBR0lYc7Du9ReWAgiKDSZPckjtqwHyZhNh2Nl3jr4dNfzoVBrqm9MoE3pkbUib5M okXhDeiDL61ZJgcr+zzIsjTjTkPL2GMtaJA9hmpBy2IlmFMIVNeI/CFfYLXMt59uhE04G9cT9v3a1sx aCIA+wxUFZgxvV1380zaYmWO4zhdHcHhKjm+UwCXUN0oMowIlOjS3QUvhE8+4HAwjSbgs7ruT+2jy0J YFbnq3TO1rj5YkfBxPQw8mB7CDWjwCMMdhkmCXxKmBb+1zBkvKM+Yqokb/Y5kWocksd+jg6BOvphgeO kvgrMGnPf5VnRsox3zYnwgTRSw210GPbw5uERBSdARWV+rw1npjwL15ceu9pi7s/Svs+MBCDVWiTyVK WHMSjZd3bV+MHNBG4ooBvuMFQd36WP3rENgdVohm2lqiXK2RWJPLLJ0A84ZreRXly0SxXxn/AtZjCez KJITTZ5Q+9MUdDkjkiElFbjL+6YPm0MZwRZJ3BnwzEhDpiqgP7r2L33xVoa7M6fxzmMRTURBW3eoUri 52Qq6LbqsbfSmQnbKAnci6/i3POx2HFYg0UgnS6NtmmIpBzZCQi2oG554RRs6Nfmqynj5IFjCDFdcMt VHN6IuuBw+bK+Ixb3R4CIsOpOv0xC8x6oyCQE2OkuJ0KpGBQFLBJL2L9SLLm0kjkyBbwKqKwfUfmecA Lg7Xzg+CTQA1T+MGiDj5QFySAVMChfmx7gFMRsV09kwuwXhl3bHNo3caJfbLG/FFHraumwAy/1yqegO Q9KJ90/bfDnAAJ8Ay+MD5t32C5XcYJj1hsL6EH9x6z2kuLGi8kHSVFix2uVN4GVUTZOzomolKH5zzhH S1aa7c8yTkWl83W675OqPz8Ovmy5qxpsmyrgel+diCFQ/FdFkoVzkszlPBhaHwE+8ewKXDlS6eomiq2 +36Qbmu2rOn0yQSW/YvB7qYfR/ftkEUqNiMjqNpZAUFEokTX94H8HSxDyDryFfnCywEzZc4Qp4Cm0BC VrwxAtVfRiTCw2uDvFfCHHjLeQIUDB+vSaPF1KRr0RDFFFiHOdDCN33vIOjZq7k9GVMM13zKrExgcHO O87TmJMV5sg9SFvlse3q0S4eodR02DiTjjlHiyXCyz9PiHhcZDryL31Dpg+qPjncHu8lbLmjy8vtl4a EQiBZBXOJb1i5Ua9iqssxX4Rw1hKgo4QWBoYWTCTK0SEejwsQ1Ad1kbBnDalhKiwfV+FWPXjCB8olxi qD+oDQQ5lqWxWKJstKb99fXlyNY0QXcZiNxnXtxcACTDlNfEchQaVEKjqJwnsMdaW8wn6R4dcClIilC NN1Q1VhQJanoGOKArWtfIjWjuvD/7t/TeA78C3Oii3phkhTAVO+jKBERm4UIZX4bbN8X2N12Vxp7IPt Z3wGAB9xhmHfvoqK9AMIfNALcZkzYRRjSrToeFYnYRijEaCM5k21WVEqMDI6ltnNZuOIOuptrbZr/q3 Al18A5Tw4U4SlrnlWvcUZVp2FWN3DI9nCgC3hn+iP7Y04SCWtnWlM8LMKseJtO123NS6plQ2rd5kpAd MVtW95m/r6EK7gpyDDvy19zUOKBA5RzFsKZNA26NptXKz2pIdIkuYJ+6ONM/4SzoHobFbom/uFkMPfo 4PI79i05uC1hX/1hZt+BecGBKrU1m2TyHDWcgknICf8d1Pz70PEdBqRJvn9+cXHZ8w4J9W4NntiFtJv R1lkK3/9gowdi6jGfDm1fbESuFeDh9oia5LwWc5U8/yqafEblNxMApVuammKWaUjI4rPMdYY8N/xBPR 0p/lgYsrpHyxrcSFi8i1w8GjFw32l8gWJsyyXmfxiSWUOsj5YgM2qtm0E/20r+zdI4UxcjdQ55ki4W6 w6ZkJjMIBuZ4DzI/efl6WO0QqUtIaEBK57nBUwH6YCT8B/xHFlIKigsPMYRqtWibqmbJLHTFx94JgRT OFdUBCex16rhr69Iq+thZ0lJOa1hqU35oLhn4gI972MbJoqg+nk0XgMHgC+TaN6GAoGxL2gfnAzefvx 1dHrRU3fSGFgu/zV09vTcawv9eOD1b57lt/1++4/5szzo95V0qkPNW6wXPlAYJgzIRFmm84q9JwVUXp LpCbSyyAKgAE2K4qLzRTSBOc/UrU/C1gOEAVOxwHvtvVDQREV+BeSs47169TKQjO7HHC0G4C4H0z9MJ w95fLkm2wA0DoGFXGakBljOkWsmcycCiiV5JhF0eWppr+givBEMOkQIoAChOJwK018SOGs5Jr95f/TX wWg4PFMHEPaYiBFMeQgERxEiJ4cwi0bhPJ2jIn+EagqNkgRIkZ08T/aw4A/P8h+kPFLNGn87vA2CDU1 NoqxAZXldK2jJtF0Ljv5hRVg/rOl3BBls7IUChiT6PiUGj6Aa+xLmuAs3LmBZv6zbP/KUkx6Cn0CtwB apvztpYymBRQYFk7ibRwXKo4FAw3WgexAApn2Ic2IHoP3LdXEP3MCL7otWlboZIMZJStJhJIQta2uIP WZMHmzitp5AuT1J1TPKWaoXTytaIc2K5boUc8xy57KthBSr25wTMViSgxR6HFlxE1to6tmM3kiR0Nb1 maRsWXyeCp6838SwbwdMzG6/Msu1ivgygwmIM5oly/xe3vqTJF2N5KqbF3/iETLNt77DWu1VGBdNVYO eW+/wjhhQJnNAmVA6KnVCim3VJ5Bx/nQMLZXgEgh0ha2wOG0subEntN9kV/g+vX1fXHtGCLoYbp8s/s 6AzLPtTTvBU75PjAmdKPS70YQCWinCIurbgLqn56OrwfDy4nw4sNlYhC3MVTQ3G00e3qXZNaGsw6ClY PmHNM9i1NZWXKxKpnWq2KtYUvz4bo4MkmayGkxQ8FBlmFWJKncA73s9Qa0tQAHKa6mMLM/9fy6luGIq +LE8/ZWOCbsv+3wcLsJxDM3HkWti6KIJ98Qk3jB0vsHaiO8Q35HJ5K3U2UjIcgLgphsEu7Wrb88b21Y mm9u2j1O+eeAsytk8cjIP1U1L4NW2dxVvlOxranvqMq7ZwlKm1krmX02J9jYWMrtbx2xjGbOTVQzi/3 tymdCnJxM9B/LztLvObnqN9ues4VKEQz2S1rcGqUSmSMJqNPpTNr3d87SIZ2u2g1R8vV3dEN48xvmkE QGAE5roxQ+zO+w4lR79mbexeqDv7uKYuYGzAhU61P3AvHZDfWwNwejlEBWVFhDedqjIDfy5RaFG4BYP 6LsNIaUPV7SpL+ETH09Q+PmtIWepzteHi2tgE7G/nXIldf14d/q3D4OedxXB+Q232JW6cj5E0YKs6B+ 0hHwKHOWcDadg4AKCxNBwck+nZ9fzjvgq/Aj3XbSWJq+LfO4LJ4qM9CgRqkvUkjH8pjVjyVDglCrdzJ I0LNr0M+gYh/OtQ8pkrCLq4P4UEieubGrJt+gTXjXLJO/PkTSsNnu1RUe43td25SRcV3syhYdf0RGsV ulHVXtVa8piiCkthNQWNqpax+xqx5pB2S7bWIyyKMzTr2FGhJHG3qf7tZ4gLcCZLZGo7rFfSAzEvcPm gIC0sBvQYwpuZuR9483iKJnmHrkfSVmM8MipMjH/ppiY0ghuRBUYPVJk+evwtvU1w9JDqtBc4wSp0WT X8l3cVShR6jtdzBSn7sMhOEcr7E1YVWFq236UZXjUX0V/J3tw31mopDJ3lOB++R3RY2eZipo9qMFPPg a5Q2VFO8OHG0EmrjwlIHApf8s3R2FPLisKOIBE/beD0fD66PrjcDS4uhpdfzw/H5wFu626Q3/6Pde9u oBNp3TTwhod/bqV/S6r5k91N77reuV5Mgo/w8YlP6rdL0jDM9SNa3ptgrsREGwr5m1Bm2CbUMPg2p5r o5evQphtDVRymmkApyf24jfDgkCPzpoPQ1DGAP7bhHA1AD6HhJO2nFv7LN04Lo3GROxAtHbHWlcZ6BI UaNcNN6ig+ja4ylNjSwBKy49LqVddycsRNDpYSHmbEoDRgsRZNALesNbnwtJ54H/ALYZbu1SUvcXkV9 P1TPVB4KH41Wq68xB5oksGvqKi4Ritd4yVN6SmlgrIkJso8ZLWKRjahDrlkMHRC9Nw+pDG9wBHWtvCn n4MixEAaBsy5yyMc+W6gCagoWC4qE+mEuo4TdCX0+PlHaM5i3Z9mRgSjQ6Rj3gOjAvJACPPuL9pI6Fa gQjfBm2fAi3UbaR0yqAz6NXah5k2nVqK2aqR0FblxTf8iFdcPNSdhdUN72i/KHkWG6/b0jAxRkMiICu WvCG2NwS13A6g2ZKkWutADaz6ajm0KTGzLU0k6luyc6nLbZv6ZNYMs/Z3h3bR8VDqhMVog53k75u0x2 ZpcSGXG9RQS7psCkube8ct3nHOZ3nDf59t79j8jdvf86TMhQZQI80pYbVGhjbXFhrjgIIt8KPX3mFQ6 4TS+xovFtN/psZ7Rji5StE7DRa1VWyrolxZdnNkQfthHG6vroDLQsR2bCF66vRsaXKQcZmTSE9GXDA4 rOPJY1Tcp9O2cnFE2OIwrpQRz5UdBWm/tDbcVOVIV4gGYyJ+VLdBS6ocw/LI2EowtcwQj4YXx78Nrkd vz+BzyBIqC4CywjB6ULJJqGobrfUyfZYbBq6+7UIy1TTJL7tUNufU+L5T+/Zk2z+FDB49qpP/JzN/YC HYSWg5RMkZz2O21ul492leiK/EisrvsOX46+aJIBH+SBh9mHYZOWmLSasck/EP2Y4pt3thfdYznPZv4 OWtMDKzFAiGd78MWNCzf8qqNwcd7+BWCkcvMZSGiiJEdLi4z9Ll3T1aJOM0kdmGiESgbmkz1Zp4U8tm omxOFBrh7xHq6wUbFE9NQ5ctuMy2f3o3TzPsaiQPHgEbGUM2/Go61Sl8wKxjH09K8T+fcuCGG394emL akOHUyelmr3RZWtlNXPJjXYcvULIWolG10nt6qutgKVVlYVB1/B4LXMHvhvicX20yqSUrXXNjlHqPbZ kD3gjrhtq9rYs70AxeDwFx3NwepqJrcs+GJObrCmjb2G9yH7RcHWBjOdcrxr/yYWaxj6JGx5NbqV/dW BZR4fgZg3RmIvoqw1tqlM7kaUWmZOq3HRSjao5oYOTg4h3wss9yzcm+XUc/sKEPtNRIkNqqG9LWQXaD Laj8Tz6+8P0NYGQtCUWBFWCuBJgNPLLNFqtJhKmTMUj0BALwUbFeRH3/05VvWLaVA5towb1BMh2iez0 TwCnuHXo/9FUTQLAQaz75gSEtnjaWvdJlS21L01cWDSBWqJVvmMatD1iNVw1rG+jwMUZ8Fz23OT0ZsY 1kJQBKeYYVf0u1JHeuR9wqueoZb0qeP2azvG4M0rFcsjnLr46FMLLfhid8qdvqTCx3zdrE2lsaGAY2H ZVq8GrcmuoWNS6Z/3k1vBZf+aa5yzZosC2+zuK7uyi7REbG6FLVR9vb8y5Pz38d/Xp1dDwYfTg9lw9O z68HV389Oht9OPqbO2yRk/MnB2uDxJV7ME9XdQ70Jmw8v6Co7azEdkpPEOSTDmqh8RjcKJi/DbZeDRw 4EM/uy5leEKCW0JHvtCxih1+mYlKQPwTWtnKj4OhPOAkYxOLHtjFfe6zBpHoV808rfNRr76DE4ZaiS8 Hf+mhApbLt6sPnCCDYf9FyCI+3WhmnEqvAev6zKZnkF4Wz0MoqVemZ0tM2hRSxA4okFFBEUAmGjoC2K S06U+2Feev/K3pJDkyhwQIYamujaJwgVqNnR0+weKw4l1fxmkgJtR6VhtL3Q7jG0HyrKEm8x/CB7Kuh xucoF8Egf6h6uxgelYiAZLN30/vLq9uqQ8lW++2C9ltu7TYFc9PJtvWu053We67vG6wBPtAcNBEg3+w bs6IibJ1S/rNJgeMEqht6SbxIXZJNkNGC+kHGFPbMtLaQO9KilDyGmqwKvn2Gt131pqF/2/iC74omlx jxEwqbPA/f9/G2WxRJZDE9ymjoYgHUGYMUUtzQcAV7S7g/P0bhHA4s2E8cTpT8WWDfjSM4ih/hsvILQ XrMEeusabKmxLpp2i9+ewsvpsLNR/D2HgfwCZOELrzPWWKwylANSF4pwf7hwYtXIpiXGBoezPIrERB/ eHmiYOOtiYEg85gvomgaMO/efL7K+wdfPGCcei6D73usHqdZtlwU1HKVlFLPHBxbUOcZD1DH0gSQLtz a/HVssIxOTYUKShI5zfx1rAXNBaPbUnTzdjA6ufhwdHpO4UBIGqA9nfBnxxsvhNSai799fzG8hm0ufq E1621gKMYEhg7Zlu1zmK0REaFj47UKEIH3k0qgBKvHxMeJDiBfQp0IAu+N9u4xplHHp3TM69GySK8FB ojpfQy/MEYJqRkrrOW6AxMervsH3Rdq151HKCBXyCo8PrsNK0kb7zp12XkbHiNKzmbr8IaosiNTQbEQ i1yUKxe8hIK4KW72Dm/NGvnDuKbGb2/dNZruU9Wr41U0STODej2MEU/ywOHT4hCW8SWp19Pz1POu0eo L6RMcbygLyaMfGmVmgVOZU1qMpxhIWarEvTsMSIhRC8jYkvwD71IOY3AfZnzPe0DXQVR4s/eXEbTc2v pwW76CGTVvGu7IBIyZlcqfkKjVXd5KbVTIDtIROtEomkIVw+bRaiTtMw8Uz2GhQaPNolm/ipyialADu NkucwNoUbkOtm2HasI6LF1M6HmvJvKXs2mGbWwIxx1FGb021q9awaJUQXbW4SW9ja3mtlKXZpPOMhad xFlENgkuv4vK/COLWlpZ84LpKqzmswrs1ARWPs1dAE+bAaJow/A71sevOnhLjelDowJL0VUH8XQFiTE IvjmD2zWGLFhlJrTxWUlxikvIUctPRb6FstZCnGl4/zWVOk+ePCGlikrTYClVhLqkQ3H2Io5rH87Xyo xoe+WKOEhCihClXfEJiFS4wKqMoBCzjjpKjKl1d0raalQ3wg/0O2tu4NtXK25qHKR/jZhbPInyIp6TU 7EztDEHoC5vALKA48Pb9+nwdkf/LdcDzgmq7VwPlUBfU+/q9BKr7V5PdHTnetdnw5p+BlpyukjW16nJ YgsDPkIZg912cqPzdEQ8rMJ4DHfcIS9QFEL6mTSl7nCIlfP0XaSTqbQNlRt5E0maAX0xWq5AvUtTBfF X+P62FqY+qUoglHmgiNgtelcHqRKlAC1Cphz+XnDxSAjEtkDUZw4dWw2CrRVFfClXiiHurqEL4L69HZ i7WiwXXgN4vaDa6QL+XNKTDJDAun7h+sEMfASeFeF1pamlgMMQWMcghDQAptXs80tWrfGizw3zT1K4i 04Q9TUUdyq+gU0upf7cOCPokot9koFaBOKhzb9vvPYtQmnWKwW8rAErLmD6HudkduyKioUte/6WjyFJ fMXZ818KnjDeFZHTO8Zz3PG90tyzMLfHS6SfqumHV7gAlTcCGK+FfqvyGOA7xBL9ahyNRFuELSqOTmW W7Dqiofo6dCc26hQZ3DWAOhh1WF6SizQknnDpanSTt3AinnOUCl8kzzBjbflZuBJ66Uo3LbzSxSQUa1 Y11vVMlBBF/qmRXGRCwQB0GOABReX4+UJ+eYlfVtGYjdksFkr1jt2xMV4TR4qpZmnpXg3+fXB8bYXlv wRyQxFf4NoYoapuWdxjLhAO2EHEahrO7yJgcnLv49VZ3rJmVF/X5COJwZN0Gknc76Da2fvp4MC5WURJ WlBt0odD+ungJR5Krw4OW5Ug8uYgzJokv8sVCwZ3YRXi0FGDuieqlIYOb4XFtlGzHP5qC+K88eJBR8r HubKhb8MW8RW9tjdxQwC2LHyMRsss6VdPX2LM8N02gHA5+vhnY1nYMvVhvsqse8fg4F22ruK4McwvEx O7eYeg9zDuh1mMKOlr3cgV7y4v9PiVsluGSji/XRejPYuznGIXUEgspNQ6pAEGWoJFOAzsYPcNB4ukA F95gFRnxK5sEA52rkUUc4o0eQZGi7C4N8EJV2FgtuWwu5hpbIGpMMR5rNrjCakPdqpgbaooTru2/+vg 2vOfN3b2uU9LtX/YrUrTmaL+be9dmq3CbBpN8ZtTvM4lj41A8mRDUFPOtNMS+xYvJDg+6wLktAKu2AG zzTTfZCj6P45n7x0NsWIFXEtBrV1QYnUaDAaNg6Rmm4gW6wxWxeuhNJ6qhFhpbWkHyvtAmT2Ww63s5h OwTVP2elTiqcymfTHwoHEmDip46/uWCEGfbyVZEFqtf1xMMQyt4zr61DtL0wfTCY580I38LXn9zUmrj ntbZoKwrOYdCQss0Vld3oLGqPi1wfGrEQd1ZPzSPLCpfM5JyBomZXcHGIfzC1s0bBW4wfPMBGDeP824 3Sze536YYeIN3dDW2iPtWMM2jOyAssHJRru3CdeO+iiHNY4xlICEXaBb36LF29mBAw0jlNeGFEwxm2e 4brijW5alrlVd4VPvHZ5qpJdYcEny7uN8doJJ75bVR+J5R2536/iXkRgNJqAxHJJsZOuISPUxkd4Pjk 4GV0NlFPNUjp4svfZxafejYtLzHsO1CJpJUTYw/kVW7E3ibLKMC8nzZJ/lKtVIrisE47NbePjUYxrna ZMVCjmFGwmPPOKaUVKPjYRop07KdSGNNMCEyQoVJZi80TNSeIYejowiScrUHuz6V3BASYxYGc6MSJKV QWmlTsl1yCxpEutSMZnI63NrRwUjG5DaZQYX7/zd9XlqLD1PqMcZiXtfocyrKtFw9qFfwuLyF7kzofv WzjPMdIWBrjHWioD9qSfolDclwTliDpkyJ3BXzT3glGkvppQxV7jZEmYYITs3mqRCvznlrkgdGu1FX+ K8sGME5zIui+Kk3AasNXKl7aQzVZnhUw/D5wOmriIVMBXOtbBy9dxCBmec6hVJd3mjMrdHrtFkd2Dd6 1HWqJPFZZHIMj31pjGScth7yRpuqEgxPEExrMsOZ8szBAo+yS991w1P6IoMEXFFONxySAnUWa7llW5J 5fYsY41Is3yLqySjcWqaqpKKEnf+lGyQCLtpPOhZLCP7d138u+kSKSUZ9Ry3Jak5nXFgF3YqpIC8Ks3 AKiM7K4zju4r8JMGdx3tuWUSZAYRT9O5HixQLpenCm4TF5F641yN+ePrg0DaPRpBYw6SIGGc0KnrjHR gCMqAbmCTztWk0vPfSmTGha5twOYy3DIbD3nHnqbHvf/HY/p3JijAnt70tHdlPq10x/DugkPY34CuMK yeHyFkMjO0CN7uwXucPnbaYf4sEfmQ3gXOfE33gTBycvBqJv8xcXJdYuMNgNDMosITb6DpzEbeaowWb nsjurLYHB0bO24Tj7tpVdgiEoKIYbIqAcAbswNuj499uaMRGemM6SkbR3NFzmuI+z5HRO2JfR3aESNt ZNhFLaDnLUtI0ggh8+ruBb3oxiUxHPNZK0l2dzSe/md5qcY9JcKzQHsh/utLy7Bin5LtEwtkuYkhtrB uFvbATnamB/We51H8ptDXKdoy1DLbMjyzgbMhizDxIv4RFGxALlpLrIT2gb7qcCnFPj2XrVrsyM3E8f 1Ado+LargDfwYVllZYICV6PgX1C0GY4Equb9F0PznpH7tvGvHNZuTzYlsxgVyIm0Nox0XHMM4s7oyNA kCrTDJsiTMPSMSpwVS9FeyWyKNpinLSoRA1X2xWTVgPnrQmHpQxYsuKAjg8DNmcTxpUyAkzo/SPK0j0 42O7gAsJj0fHwSTORRUkcUYpjPHI9Clect4zogamZxAe1DpyQnmF2Pcy8nlGIQqFh8RbxIqIbTYfVVk 89OshzDrt/lwLPk2XxGCrQuU4uOCGzGXOA9458wFas4sDzA8cyXrf4fAceMSSu+BMLxKEenV4ijHpK/ jp07qRzaAE6T6+738c5v1HAV++br9xhTL6iZ2tMb6olMOcYSeNMNxizgE7EgNIVOM+UXI/FLZo3ZKER D5TCSSCvY1RCJgcNsA842ET1UHnTh/dWBqy6nAEqa8B/P8v/G8ig98dcfNNJLMURb4QLFU9Mbrb+ZLM 5G0UR5A2YfOPNwbmzi1igjThbznk2wLnuiYJPktYE7SExohQySnFJ+hkaNABLiNo42kt4gfOt+2I9kx ROp8hNdrxlbFpvaeBdd4W+WXEZByUb9HZgORFWDFfF9i/zI9Ju08kscTSx84vzwU3v1qrDIWtFYDZxe gtNd9+yZNtwKuL1A5/1KsywIKVBeXI2HKCuCcEFvV4vLHNcuaMr6q6SmR7Q8L4OkO2u0qmCMozmHd4s eMDsxIRYyFFlRNQAA4O3uS8eEyfctv96/Ian9/X++E3Pex1691k06z/ZR2zYf5Y/efMsf70fvnk9zt7 YMh1fKKDaJk6pa4iITCZiStS6ckf5JFxE1MG2b86sPKvJf7tOOGnNhTlINfbTHM2qSVXNWIKOn/ay1/ T8NH/LBKPBllQ8MLrhbM/yexbIzG9t12FTKSvPd3vDGr86FtNnDALv6hZcZbnJA6sGB+iWYgNwpgHlI G6Ghiok62p7cHcdDty6minRdDigc3N28xp5ZdQekWJaUT7Ltiudj1gEM06nazuGno5ASw7K4dzDiOjZ PEyIVEtCnbMUTGhfcB61QErAWUXjvUV4FzGfRCYbUIdSJcaFSFL5+IMZiE8dISa9LtmBB1UCTJ91qb3 lisPFCxk9junBsyB8d/BH10ifc2g0IZSuPHv6sam4bxkynMjDkPERsdIEHflQmEQMvsFBuefMyXeVCA kF0GI5AhmTa2MwEFFBjr023ocJXIWPuc7WtGAYA4H6nKFEQKIqrhUc0MAd8KD3qSFMoBLPhP1MzuYVH QEPeVeCQ9IP/MLyRTkl9GuSJjgZn6LxkAydxNaTG1HoEJ7CJC0xA0MitYkhcuUwLahut5JpbiPG5FyL FBzGtL3KbZOrDhmF4UecTRyE1xe69o6leb81orzkDQuRy/NfFJUJBlzladGwlBA5BK6KN19oYr7UIYt KHYhT9AWjXvz19Or649HZ6PLcca7qOaJv5WHdCPS5dRki6DkU5ou5lczIt9Jq2/YLuobKD14yi5OO4z KFoMFoqYyUDMKU3CwyDkNrJnxUtl0yYbMB05K9wIm6J/N5IMXEFHI6vM/WYBYdOyjQVnVoJOQjfHx0f fz+6Oxs9P68JN0TZFFnuzKi7HAuALQv71iF2etGWpCX3h3eBre2vWH3cY32oD0buM4aTgblSCN110pp sIUwWTyly5VeCOkdmENn2oDkfWFoADS5cHjslxlwUR3GZHvk1/AkpWBh8mqIh55Y2f6OOgi1Y/qGJqM vj9Pd4Nghxqxf7oysanjmeW2LBJXETh1TtPiig746+a3M3/qko8cx0Ufgs3XA39q0lTV0mHe/0yrlGn M3nF62NXrae8yQ5HwkqX/E6R6iqXDKSTE0Parh/o/3GBfxHWqtwzHwJr84MoI6L79WhlIp7fIpisn2E 7XVWtcuSN0c21JGywTK4es0qjfcl0xvHWpqg33Ty3ekQ+Rpdyk3Y7iKvHSBDKJyXNZXeHSAnoSwJOJ+ 74U5rh8r/LtOjo/bBHZPbN16ro/19vB3Jy6twjSa7bTqKA2fNmGcUH4bSztYw0UptfA2fJTkedi+5Vv 4KMmKSW5qKz7Kw6sZ2XLRHOWYVUIAIgXzHfKyGSzcMpeyYTqj91/uv1K9EaY3WuxhGTiWD30301G1ib 9t2XzDV3B5DdDKzEtjrxrgNXBRTXyKYAnElBmywxHKt20mRJKhsklapy6cwa86KDvnJQKoTi6m0vzuk P/n4Fo8UFnrZoCGS/EDz3WS3p1yMoGbliS+DmreataAGQSbabV6zhT77UAwtYoBUYTUsAHCjmiWSTvn dHRpbTg5REttL53NkFRSmJLTy89/IdoRfyGf00JqNOb46pW803crXbB2Sa83g3894faL119cHUrFLIr f/EvPkNmJhxTN3ihgpXSXu1yEqpNiu1JWbeWs3UaO2ttDn7j9fRIFeu13R6dnPY5jQFlq6wyfSFSme4 WCLZnWxS3MqDuSeSL7/i8+CjqSNOvLTv4+ODu7+FS9EuEmH4nDAwawL3ajFRFExvggaSX17ucDO/uEp ojjqMRGsQgYWxjV5zDrjqOR4ZCgOtXx/uufhvng0YSsB6FkkeHZgLH0hT17j23txHoC7kjMicQNXtpR xIsRXjhx7/Bp8SBFJbJ/sP4PFnbFi33fvDsIEHprPhzi7EExnj21krrACyzQFTz5w6EUwXT94KaHATj KtzcoU+4WdvVF6WE1nn8jPsKsHB0fD4ZD72Rwfjo4CTZ6NH8PpDQR84dGxKwhKrBK43g6jeZ7RFf80i xb8dtLZ3Od50dZFwGL5++EZ4g5xF+8DfN4UnK6kriG/ODO2IbGSqs0m9o4p0DpUeAjWIkVnDbItokhy 6gTVu5C2zfPEgfVHPsWZu2WP9WKdA/zUrEoRcv6NIv/QXNlmYhU8gS079NVB648f3kVABiEhl7dwuFH 7CL7AkmxjFfS8Zu4nzEukV/OFaCmj0x98ugvr7rTCFXlCH1+16ZG3ZYsruQDxPtzF1st+zGuG+KYWll GYHxlZ7Xg+8N8qmvZyNKrjlS9u5F1KIATDKs8XoEjlhcvJ92EF6X0GvU6X0Pv24d2rgb/8XEwvCbj/H mfM8QbutPtyNLH4eDqfzlhIs2uIk34axuqZN63fH97YiURyXcxLLW8mcgKZsoBgkrSMVFrx4u6GIY7F TrnSs/ClchrL1JZHL0bnZ4PrjsytQVG0h8Nr68GRx9Utyx64MqrvimtenWvNqVW9zj9PEwxqjbaYkID W9hHRYiwHPa4vJzinw+CUv52maO9WC5Q60c1g1ZrU/B/Z9aPDgYmqV1cLjmikhoR4Wcp6vNmdpV/xBw h6/uxrM1ZtVUKe5W+fgOHu2m/fiNCG7lyBUa0HX1kaUA0Eul026Js3xnUaSQQh24o42WcFHD1IiscnV qbi7TM5RLpxny8QgCG+PZbczFfW4v5LK9Zu91XTg4xsIOD2FNbKzb9H03Bcr8sEE2lpRc7A6VzZ66Uk tEwa0px34xyAYa0pgCkLYpaTNBTw9pusb6AfTccnokdKoyZKcAmxVUSeUw9Cblrn6+qo4Qsgv69/3g9 ujqxj1pFI+RAg8Y8S6q4Oaq2npbmI8Zsp1SnwX3YDChcUeTzutiZBCqpBqTdyWfTqBpNllW4eCuYl4T Wq3MMkwHziihJRiz/k868pLz/rIUJIrq9BV8129QAB+5rbsFyw6oJGtjxTBiGyZM11Iq1uUZ8C3kMRZ WGWlYV9arqplrDj5KhR0OIwoqLoxXdsHmYZqYDEwFKS1IyqKyfiE9X330iHCYo5SjZ1ZjY2uBKGt5bZ doVlA4CR1hA4J9Op8po6PDFwZbrsIMJLJvZfI/0VLRXDKOnrXJFfXUGqG9L/rRF3ifboreGVOyEl0o6 b9npNkO0zdFKvKyBuFYU6a4KahU0UiNDaWwHgFd9cuwWHSziXNq1/CCsEE2FadVvswq3rLk2OlHRYbK bmrKkYR8yv8lL9C2ZiM9mhiEcuVYhnky7dcZ+xOcbXaw4ipEN9cGB9uMSaly0gH8QaUBRlK0dOQmcCj /MoYXbze3svTh49fPBQbD/GH5p/wTNHbgRuwqBo2SXLCoq52ZTqNlay2dmFUmP+SG8iyeXQjfGMiFpB U2sEtcghknrSwWntIr8LGJUwPsvBo7DkuuoaLCJnk0NIz/FnZoWAo4udbcC0mh8B7P5wPlBTG9hoMve ckEBgkm+hQmhwnjeNerRY5L+2aIzpVE5zSaGz/zl1cX1Ra1hnKVXM+HLW6AFTNEvjtvy/VopwwvK44X h0jIYkkIBom/CU1nN6q0ba1NDOg/GAxMia873DisactuCXWrT7ad5GE9H98A4pKbAbAs7dRf6bWOJLs dvz6Jp/jAM1++xQy7jdN3dWmvkil2INUST52JXC2FzieLiFz8h0wF/Xvz04qegQvE+Regu7xd4hi/JP ShfLqjy8MP1JW5ooqgAAff3OL1b5t1ytI/2jf/ixQExm2tvQPVkDA6eUjyrvqz/mPu3Fd/iutjrzQbz bVJ19fCSjNdhN9kwLeg3cBRtA5vwRv2Lv2UVOdM7VSJUFjWEQ9jRHCaeAM0oMgLZVXBAOLJCXi56Rkh fXC/Y9Es0eMCjCskvgXmIOa4BIqQk2FIlu444MMmKlhuVFVj1Mc3Ya3yHyOjTOJ+E2bSa0qOWpd0YN/ 0bYNvAkWqJvcox1Uir4eCKeJv2LNwz99BCUFv8IFVJoL31ndGgS5C55vTit3bQqwarsCmFWY3TWdF3V 3+UZU2/rM/hg0FvcFJvoUSDDHnCxJuEOaYxj+eTZEnBYI8vzs8Hx9deVEy69UwFjwPbapsz2pjDxapK u7NtTQ32dGivUDnXVamqkG3lNcvaeI+zDkqK2MQSjqNJInhtxVBg0T77mdimukL7Zp+v1LQKTavcGQx /OQm4tdlMs2Ots+lU3d3OaNPCIpwe7zlPU8c0xayLyL1JsuqMuWIcAWbrdLegQByUTbRvDiKoOQMc6a gbci8YSFm32TEKjDr/HTuJhZJ0s6eY9L3KsUbB742wfCprmNyLYXFvCaYNeU9FMCK6Y+h1lZYBJ7sdt EqBpavdFPvVL98rje5IxWlJJX0XYYiknh/0nDFEv2qDVJCH5ozCiMpIKiH6Dkyj/quDlx0SBS/zvv9O at3qjksXdtSGRuFkznx5Km1AN+478d7Z9aCza1fMNXaGbikpyMW1auQ6CMxiLM+fGH40uhYvu+0TZNY VldC9ZmKb5wnACJH8igTqmHMTT8k0Rqg3Jlb0T3pNhjF72nVyIu50tpm06RlhIJwMsGcL1xuw8jiJo3 mxNVZWFtcKbzuLtDvNZCfb+iac3hrIVpFyaxnKesR0yVG0BiRCQ2cUlHnRF0CZSVxILoC85FhtQFZl7 Zx8zGF+dHQRC9gpLpX3SIF7tBXvKqKoOSvyxsNhdEvLO1E3k58POt7PBz8fBBW7iLYwy3+OCEiuZspF AxVujtSOqviLrYu/2BH8ix3hv9wR/ksH/LIRiHXpMA31KXJ1zd6r8pslQyBn4QpJRE6204h4DgZBZjM w6IyNDn6XtHYrO2WZWcn/EYo81+Z3K9MJEi3wDnu3gRPJ4Mx89eplHXblO65PvvX6M1l1RkUukhxDIC xgIEVd9dW21f+XYoflMwykXPo6MfOmumivaFvRjdrc8dqbsBKGX7sVbgehNLUzQQjJ/E44YArfysoiE IJBmRrkkq+a9/k3rOWOq/kt6+m+8+FdprKmMGpzEK3vzIC1bNM+R95cO44cLhLyGcSwKbWi42JwMbx2 mGGUayALhsv54ejX0+PR5dH1+2FV0nYyuLwaHB9dw9cPmDeX5DJ3KWX9/KH13Zny78iS1y2Cgwf+Jmb 8qzDB0YkKGy6mdWq0Zs60ffawf5EtlLFzKbVKJpx55T5huHPlRh6jZvsNPXIVufjVwQF6coQqZEENf3 njv8YYGW9eo7/om9f3h2+gpmfUe70Pz+qYU//14s3p/HOYxKo8RrfwOCw9MXbd1/uLhvr73O4+dULKe YFrRE6b12vT1nXdZY07rNhZVwPYTEHtfcqc+yo05hj2gWN4cWsfLpWyyO3y3bn79xyNnB3XCql7YB8v LrfZuseu5e9KuIzuskjKuYdUI8QWa4vzxV0WTqMa3P7Ib230NozNG0/OslyM7aFFe2QSzd+NzVDj2eY ahi7Van33e+LGO6Lui3GEbXXP++aL4tdfErcil+KpmB6JF+Kn4TAzLyx2JGhGhXIEEM/7MltNZzVoV8 qmYaMfV0T80T7YhDeHL/6lewD/O7TwRg+kTTXJ3NMIINByHBAb5bjbiHDdKLK98HZ70a292y1isClKc gnRS8ntNjqM12HUhrPse+6nb5OabL8lnqJaFINVI6L80NosHncnj5YXHNN+iuX6prO65EUNDYZxf/k2 KueObyWb5b6oAIf0i6wd+GvVddg6bnV5GSl8OI/blTjXzHhRUSsBqG7Nfc2FOQyXSVE6qUU1w42zLlj C8PzU28Mewz1NZr3RtZvtcHXnDH+PsicCRzcPMUAmlF3OZvEE10OsMylwER+YeuX58jESUbFRg2tAGU dFATe1ZEnBMMl1fR59KbyM+K5ul+Npc2YC4TjRbdWMeyBzn0rsw9DjFM8Ta5KuGF4evjiAijDX07zrB y5dmcNmsVbEo1eFE8CpOXZpmr6HvkwFRtJN7UBqb3B9brdQiTmIgM5MEBaXUUQR5FrbK9DKquYy6pFM dRV5aKKFXnBkKhdljxh4n8OLwHruD4dnOHQvT3WnJsWXOn91IEADITE6Lr60XVsABWbFF4c9ucfW7YA PwzN5HWtD0Y4ssK20O5zg3hGR4zrCnxpTB0eVe33JYstWsIjXcY6Bf+reclgg+3zfHr9dy+TKilampw 3KyndJmN9fpkk8WYuMOs7UMSwmGN6ny2QqwulSIgey9MDz85dWjU0lpSjJgPpMk7VKaoTYQy173LQn2 jZdUFjN+XqSpXm+x6ixt6DSb6qJynzvNakO9kLyr92jFBjiPHvy4xNob4+EZ/Rj3wXg9X5NS2U8ajAS cmqDSeZkHrNWaPL/7wftN5I8R5z1LUhdDaEzidz35nykkWlM9qWGjSElBRleH10PRpdHw+Gni6sTESW YH56d/nVAPcLwvl9rPGqYwlTq9o3q/TKYvhW5hO3rMCeSJKl2x7/B7pFZe2B5jDC7f6J3WnGPpgPosn uMiZqJIbcMLO9h50XzO+rSfXh4H31Bn7FnP3d//kJ617sEWJFkGE0yDIm7S3SvDA7t9LGLH3jPO+h4B 1/+5R39OwmeH5pOY9GXBWoHMaLR3ZwynrUpAVjf7mNz6zn10cG+H9PHdkNYhOskDac7NcyR0PuYRMKZ JXtAozO4RWPMFZIp7Rt/kckfZXnVGWXQhCubSKs009ztz0k3nFBGCsFFrO7jJNKEiLClp8pi6kJREN9 I5KRS3UW6aJvDgvJZBHs2j2xbGiwtO7Yy3HuMk8jp/LiaQdfYnsVg8fWEqGzZZSvbBqBdAur1G1qsr5 s11c3sunLbOcua2XTq42UbtMUanRzySZxLZ+HyqBlhMPH7eB35TmOn3UwYTQJlUSikopYVUwXDzAmRB uFk21iqNWe/1PJjG6WqnJI2XnJ1TxJQM/qikf9VleYtVxVyuFauV/+qa6xJ0GAQqHFAheHbCmObS2a1 JR1HCy6cQx7pX/z2AxGX7a8QRuVhmmXrrru+27LcZgfEYX4VrmpC5ZedRLJwtY/XzOl2EfI3H+j1wfJ ddY2dpW4Aepyue0NL3/VVjNOzMC9OFyf00Aj8GNhXXbrpbhIYSuMiybftFHrz5taei018WMVDwKAuMj sUxXuOsual5KjQHAI6FlnnzNx2LM+g+xwlt3xsWGIdu1pljUJboiS9w2O4ZiKwIHWlbzgrdTwgY8A2m A7H9egxjufTflu3Huh2xWfJFf6m7fO4OfaCtGYzIAS3Voxc6k3PSPJn7n3h1e97e2LOcQrTuWcDJg+A H30Jv8RW8mL09WwYZuNs0YcfX8+K6oRTAitUwqmtHDCIbd4ubQHvzc0JC1ALaU0XjfMZYyJmKli80Tk EDKsINUNGaozQ9Eh8DL8gD2bKXMsxTsjoXjqvIj+uc8bJNaY7YKesK5lNu7wX2kaYES5bzc+NaMalbT RLx7Pl6aJtRUE2tBaBIapZTkw+hobdLnXNmHbr8lcvwRO6x9OLQSVQSYlHliSk15Nz3GMrP+SZOWgFD ggeERQUyc/ToCkoSpuKwCLldxWvUUqhuqlNqt+XLQMYarnSqiv0wRaDU9nSK5fd/wuiZfjM """)) sys.modules["pagekite.proto.conns"] = imp.new_module("pagekite.proto.conns") sys.modules["pagekite.proto.conns"].open = __comb_open sys.modules["pagekite.proto"].conns = sys.modules["pagekite.proto.conns"] exec __FILES[".SELF/pagekite/proto/conns.py"] in sys.modules["pagekite.proto.conns"].__dict__ ############################################################################### __FILES[".SELF/pagekite/ui/__init__.py"] = zlib.decompress(__b64d("""\ eNoDAAAAAAE= """)) sys.modules["pagekite.ui"] = imp.new_module("pagekite.ui") sys.modules["pagekite.ui"].open = __comb_open sys.modules["pagekite"].ui = sys.modules["pagekite.ui"] exec __FILES[".SELF/pagekite/ui/__init__.py"] in sys.modules["pagekite.ui"].__dict__ ############################################################################### __FILES[".SELF/pagekite/ui/nullui.py"] = zlib.decompress(__b64d("""\ eNrFWv9z4rYS/52/YnudPEPLObmk7ZtHL+2QxLnQJpACmUwmzTACy+CLsVzJHKFv+v72tyvJ38DJXW/ ea3M3YEm7q9VHu6tdmVevXjXGi1AB/mcwZSqcwav+KopewUpxCWGcchmwGYf1IpwtwBdcQSzSRRjPga XAoshtvEIhX/5P/xqXvVOvP/LgGFD4r0bFIIw46ZkwmYII8HvOH8OUu8nGbZyKZCPD+SKFw4M3B6/x4 6gN6YLDCWexSln0qOBaivd8lgJfBC6w2IeT90zGIQxXMZPghfiplIgbZrpEirlkS5oxkJyDEkG6ZpJ3 YCNWMGMxSO6HKpXhdJWiYimJ3BcSlsIPgw11rGKfywZpgSguFSlNDXjXvwHoBgGXAt7xmEsWwfVqGiH 4l+GMx4oDQwWoRy24D9ON5jtHNRojqwacCxTP0lDEbeAhjkv4wKXCNhxlM1lpbUC1mrhdqLkEkRBTC9 XdNCKWFnzu7sqLBfpoC1rmQiS4ngVKwxWuwyiCKSdjCVZRGwBJAW5744vBzbjR7d/BbXc47PbHd98jb boQOMw/cCMpXCZRiIJxOZLF6Ya0vvKGpxdI3z3pXfbGd6T4eW/c90ajxvlgCF247g7HvdOby+4Qrm+G 14OR5wKMONcSCdiXcQ30Bkne8HnKwkjhmu9wOxVqFvmwYB84buuMhx9QLwYztKoMy4/KbrBIoFfQMpG hwBH16wXkNG1QHM3n7SJNk87+/nq9dufxyhVyvh8ZEWr/h/+HNyHQAn1GbVSjEUixLFxnJpYJ7aQh+G p3dInmlI/ah3w8EvO5DgQK7GOjMYuYUkAh5CZsiik5XKvTAPLjItDc9IwFsWjNNgoRT1cyRmvjAVtFK Xxg0QrjDG6VZCGiAlxKIZVL0KCos653NehPzoc9r392eYdBYixXHAe6l5eD29Gk17++GWPvOYsUdd+i PY0mo/GZNxyWuofeT97p2DubDL3uaNAf4dC/sRvA+W0lUuZ0wCHTIF8js0UzMP1tQxQLH3W3VBLUaqp mMtS+hXaEOj8lIUaIgvwTpc5EHKsyWRBtCORUCFiizwJBrzJ6DF0rdCsZ/o5TIVMvRvBCtN3ZDONDSh iqBUrx0fZmiLODbH8QiIg1TCZhHKaTSVPxKGjDmke44/y4L2IMGWuKt8doM65KMYzJNshyTxjrbQUIA zIsN8FQgs61vO8cPVCoaDrrMHba4Ah16FhSAJrIPb30urQRzq/xr/Geok8H9pDj2IGv4J//apWJ+4Ph FdLq59uL3tjLGu+G3l3p2etnjTuPzIDkO2VBJ5c3Oe/QO8ser7rvPAw3WfP0rtsvWDlayjOqHxwd3V/ oz5+cGn0NwcGyMpbpbwbfVAftegpO+NI8Hh18v0Vq1Lejb77fFaTBsOOH2+MFPobgaJvAImWHv9keLh CzFN9uU2QgmuHv9HAjH9WGhcP6u+iWtltWu61REr15KkkacsXTZqvoGKEVqgX2WAM3BDRkLVBThfFkH f7OpF8KBtlsun+ScjzWjoEcoRhD0ekkDWePOHJQdGNgx+N+grEtFXJDQeSPYhAzj3SlJimbF0ZVHpiJ KLM8Mpyd8aXaZsTjGAPiMaY5JR0wVcMzHL05EJnaFgGLiIEAI7dS2citgbMyZHlSzLBuNRI2MqRhGvE qkRf7FZLfViFPo82xBnRLXoIJpKzT4USKteJWwkpGWUgx58yaT6eaQBYrHePONO+dQcJjCop7imKNTm ssqeu6FExQ2IMxjEKKi5lL3KRpsvnPzIEzkOeYC+QL4cpkVfY4KuJcdj6FOgfWOHfs4ZWNaVJ9asGpi INw7tHR1XRuYkxD15g1TNHIsyk6qD8pm7Vzvbrq8VLgefqcSjZE8yWqbZ6t++V/1pAxT0/NlrQRVTy3 LeeUzR71o12cXYSGuArKLhwlJb3lC7jZqRLJ/04V77jqi09VETZcHTt3dLziDh87feH8TWr/Ygd3NCd d7x92V4CJXFqP81+m88+YmPTZMvNnX6B1xOqjyv9t+l5hO0wifroQmH5brWe68Vla/5W6n6AYHvvKak 05YYzQa0tIBapPERS/JFvbp5311KygalR/wUpuhXzEOG5XseRKofytQ0KHfEMQhTFtjS4HMpW21cA4r cezvM1WJi4GVBuLwXHfCwyuWloryzZrgnYdYTklLIlu3jcdqz4lvVucD/kkBiNdr9jl9XX+UEVA71EQ PmFCvB2AEpGskmztmD4gEGY7YrG2T1gAzGN9GXLsOAUqmrOzrTTlDKTxnqJ/Og/PlHjWxLf/ign1tQo qTXWH43yOBA1WBZqrwfjMwoPlaYyG7pdMRU9RNhfDRLFoyKn65FmGoj0jC0voGZwpMkks2tIsa8mgMm OUWth8u1opdvLd1GTH9VT3ZvihSF7sVjsZJZ3/HUxhmnuqpZGvVbG1C2NmHV84mQ00aRka/bxCoNKvy PdbW6heYr6aA0PS0LlTvlRWYIYFdeEC9fd9x6xlvdA3cdSVF0cLsUay+wfbLpFopSJMvMw0Lfhat3IX IdZWC97Cd9928oVSp8sSzNf8ppbiovU2D1rV0tDCCa8JR5tLNbPVbE2QAWVWV8VipNNni4aYvqeLmjy hLlcO1Ty71NrdYwwkMVqfqdyN4b7G1VDGalPUEvs9TuqOJv3ulVcTl43ieZHY2p5Mb6VzTbaDpJS77I q+Hg7Gg9FDGz4qhk6LehGD4bhGArqLg6eMY7zlmXk7u/tmphuytTmqaqccdm/rpq1s3i90fZKnSfiMX xO6mqFvfZlid/C3qxPctDcHh9/opi2tNHsH6LJF3/7tuYcBXJ1oUwoiwdKmFtqCfRKQL9lMUa0ESOLX KLINez7oyyHK7TVhwaY1qufTnoKsM2s6IrYSNM+OiWk25Ip4kLrOs0Giicd3toa3eg0mSH9hgjQ4Lxq cLfa3MD/XV1LnXjWy0j4WwWvKy6dyWfGmY/h1+Nvfp3Nn39k9Klo1EVHXhVr9jvO1buiD5oUQ+Z+3P+ RBMr+cyJejS12yRFSHUiovS6l2it2XiCrYnGSoTEOfUGjTbeBEqQhXkbB0oZ45FUM1ma5CzEjj7HQPc D4uE0n5VwnKMhYWezTkKb8/8SZng6tur//Qtk3ynaJBrmgCNDI9IU/TMh8bD7aGAc2L8fha04Tcb9mz PM8iKiyaQ++DvvI7PESsDw8JF6uWo9Qi57VAGKZcDl2HO1mAyPmoVxlOrNipJzcW5yXDaDodQ0E9LWs ejUwDg8Ro3B3fjB5o9rw1uen/3B/c9rOCvp7jHyWGwc+Z0i+SecPhpNu/K51t+mqHVjSW5AaF6WsrrV 5jNqr2TFx07Wu7y8loWbDxr2cEkwPUiCVH2clwLRYZGIWNZgRTU4eQADsCZD1nu6JKlDrr0btoYDu5G IwKOz3RVttq1MQNPDzxP6NPtITnYobBoZ3NqG+WtKM8PR8nsqxhO0kw99sW2ALZbFH0OslP9NGH5oap pnFx95FvVLPVKu3NTsJiEdC6+UmrEqPoBrZVGFcpEBS+Q1eEeG7S66am9ZYX5jsvxRIzc0lofUQq6aM vrlu1wptkj70r2rNuf9yBc3rBxp7C5WpJrxxWmJZu2vR+EJhpcyyxGR5wkVMfBx3KlygHkEt9sVcX3L fUsxnuM4sHmFP9qTApZWkFS5yIPyV41LrOJ89RHBxoEtn1KJvnFYmtwYrKbOuQ8J6SCCOWqTANuy5X7 SVrm14kUdlSV9JqC0wirAp4RNeDTiWtMlejVopDNZ1+gxRhFeFvTHLD4uzFkAtY/+lXuoLyDfTbFXdr duQhq7apZM0q4Io++hLyY5pkr6X4a6IG5vsS4XLhWi8GUrkBNkdYfvwsFRYI1eZjKlwsly6MxJJjOdD W7/BTLDjRFriv3++qhC1fBxjCuPzzQOjTz5xFKcOgQxOaJj3hltpG60Uld2d1RkLKTdu8LTUydLVICe QqZh8QTLpRNvWEMZw6KbWie4FGQfI5pjYaBqogjblguORywRKlSWJOGAm6OgCsdynk1Epk4If0hpyjb 1V3+sda+s/a7NmCzx4DlE0vPP8kmuMaGCkPnyHKCF20QUkprJIynp+jY8zTtZCPn6EfggdrRu/IMcjj zi6BXsKv4nDGdC2pf12wpyob/oww61tYUtHPUbQFbbIqh8Ha3LzVbIEDPfrVD66hVInocyd30y8+dee 2Xp9WIdAxEOfweVa820hIiZGZCPQPVD4hKjT+CyAs5Ko= """)) sys.modules["pagekite.ui.nullui"] = imp.new_module("pagekite.ui.nullui") sys.modules["pagekite.ui.nullui"].open = __comb_open sys.modules["pagekite.ui"].nullui = sys.modules["pagekite.ui.nullui"] exec __FILES[".SELF/pagekite/ui/nullui.py"] in sys.modules["pagekite.ui.nullui"].__dict__ ############################################################################### __FILES[".SELF/pagekite/ui/basic.py"] = zlib.decompress(__b64d("""\ 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 """)) sys.modules["pagekite.ui.basic"] = imp.new_module("pagekite.ui.basic") sys.modules["pagekite.ui.basic"].open = __comb_open sys.modules["pagekite.ui"].basic = sys.modules["pagekite.ui.basic"] exec __FILES[".SELF/pagekite/ui/basic.py"] in sys.modules["pagekite.ui.basic"].__dict__ ############################################################################### __FILES[".SELF/pagekite/ui/remote.py"] = zlib.decompress(__b64d("""\ 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 """)) sys.modules["pagekite.ui.remote"] = imp.new_module("pagekite.ui.remote") sys.modules["pagekite.ui.remote"].open = __comb_open sys.modules["pagekite.ui"].remote = sys.modules["pagekite.ui.remote"] exec __FILES[".SELF/pagekite/ui/remote.py"] in sys.modules["pagekite.ui.remote"].__dict__ ############################################################################### __FILES[".SELF/pagekite/yamond.py"] = zlib.decompress(__b64d("""\ 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 """)) sys.modules["pagekite.yamond"] = imp.new_module("pagekite.yamond") sys.modules["pagekite.yamond"].open = __comb_open sys.modules["pagekite"].yamond = sys.modules["pagekite.yamond"] exec __FILES[".SELF/pagekite/yamond.py"] in sys.modules["pagekite.yamond"].__dict__ ############################################################################### __FILES[".SELF/pagekite/httpd.py"] = zlib.decompress(__b64d("""\ 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 +VsHrf+B1AIAaw= """)) sys.modules["pagekite.httpd"] = imp.new_module("pagekite.httpd") sys.modules["pagekite.httpd"].open = __comb_open sys.modules["pagekite"].httpd = sys.modules["pagekite.httpd"] exec __FILES[".SELF/pagekite/httpd.py"] in sys.modules["pagekite.httpd"].__dict__ ############################################################################### __FILES[".SELF/pagekite/pk.py"] = zlib.decompress(__b64d("""\ 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 """)) sys.modules["pagekite.pk"] = imp.new_module("pagekite.pk") sys.modules["pagekite.pk"].open = __comb_open sys.modules["pagekite"].pk = sys.modules["pagekite.pk"] exec __FILES[".SELF/pagekite/pk.py"] in sys.modules["pagekite.pk"].__dict__ ############################################################################### #!/usr/bin/env python """ This is the pagekite.py Main() function. """ ############################################################################## LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2013, the Beanstalks Project ehf. and Bjarni Runar Einarsson This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see: """ ############################################################################## import sys from pagekite import pk from pagekite import httpd if __name__ == "__main__": if sys.stdout.isatty(): import pagekite.ui.basic uiclass = pagekite.ui.basic.BasicUi else: import pagekite.ui.nullui uiclass = pagekite.ui.nullui.NullUi pk.Main(pk.PageKite, pk.Configure, uiclass=uiclass, http_handler=httpd.UiRequestHandler, http_server=httpd.UiHttpServer) ############################################################################## CERTS="""\ StartCom Ltd. ============= -----BEGIN CERTIFICATE----- MIIFFjCCBH+gAwIBAgIBADANBgkqhkiG9w0BAQQFADCBsDELMAkGA1UEBhMCSUwxDzANBgNVBAgT BklzcmFlbDEOMAwGA1UEBxMFRWlsYXQxFjAUBgNVBAoTDVN0YXJ0Q29tIEx0ZC4xGjAYBgNVBAsT EUNBIEF1dGhvcml0eSBEZXAuMSkwJwYDVQQDEyBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1dGhv cml0eTEhMB8GCSqGSIb3DQEJARYSYWRtaW5Ac3RhcnRjb20ub3JnMB4XDTA1MDMxNzE3Mzc0OFoX DTM1MDMxMDE3Mzc0OFowgbAxCzAJBgNVBAYTAklMMQ8wDQYDVQQIEwZJc3JhZWwxDjAMBgNVBAcT BUVpbGF0MRYwFAYDVQQKEw1TdGFydENvbSBMdGQuMRowGAYDVQQLExFDQSBBdXRob3JpdHkgRGVw LjEpMCcGA1UEAxMgRnJlZSBTU0wgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxITAfBgkqhkiG9w0B CQEWEmFkbWluQHN0YXJ0Y29tLm9yZzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA7YRgACOe yEpRKSfeOqE5tWmrCbIvNP1h3D3TsM+x18LEwrHkllbEvqoUDufMOlDIOmKdw6OsWXuO7lUaHEe+ o5c5s7XvIywI6Nivcy+5yYPo7QAPyHWlLzRMGOh2iCNJitu27Wjaw7ViKUylS7eYtAkUEKD4/mJ2 IhULpNYILzUCAwEAAaOCAjwwggI4MA8GA1UdEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgHmMB0GA1Ud DgQWBBQcicOWzL3+MtUNjIExtpidjShkjTCB3QYDVR0jBIHVMIHSgBQcicOWzL3+MtUNjIExtpid jShkjaGBtqSBszCBsDELMAkGA1UEBhMCSUwxDzANBgNVBAgTBklzcmFlbDEOMAwGA1UEBxMFRWls YXQxFjAUBgNVBAoTDVN0YXJ0Q29tIEx0ZC4xGjAYBgNVBAsTEUNBIEF1dGhvcml0eSBEZXAuMSkw JwYDVQQDEyBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEhMB8GCSqGSIb3DQEJARYS YWRtaW5Ac3RhcnRjb20ub3JnggEAMB0GA1UdEQQWMBSBEmFkbWluQHN0YXJ0Y29tLm9yZzAdBgNV HRIEFjAUgRJhZG1pbkBzdGFydGNvbS5vcmcwEQYJYIZIAYb4QgEBBAQDAgAHMC8GCWCGSAGG+EIB DQQiFiBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAyBglghkgBhvhCAQQEJRYjaHR0 cDovL2NlcnQuc3RhcnRjb20ub3JnL2NhLWNybC5jcmwwKAYJYIZIAYb4QgECBBsWGWh0dHA6Ly9j ZXJ0LnN0YXJ0Y29tLm9yZy8wOQYJYIZIAYb4QgEIBCwWKmh0dHA6Ly9jZXJ0LnN0YXJ0Y29tLm9y Zy9pbmRleC5waHA/YXBwPTExMTANBgkqhkiG9w0BAQQFAAOBgQBscSXhnjSRIe/bbL0BCFaPiNhB OlP1ct8nV0t2hPdopP7rPwl+KLhX6h/BquL/lp9JmeaylXOWxkjHXo0Hclb4g4+fd68p00UOpO6w NnQt8M2YI3s3S9r+UZjEHjQ8iP2ZO1CnwYszx8JSFhKVU2Ui77qLzmLbcCOxgN8aIDjnfg== -----END CERTIFICATE----- StartCom Certification Authority ================================ -----BEGIN CERTIFICATE----- MIIHyTCCBbGgAwIBAgIBATANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJJTDEWMBQGA1UEChMN U3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmlu ZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDYwOTE3MTk0 NjM2WhcNMzYwOTE3MTk0NjM2WjB9MQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRk LjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMg U3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw ggIKAoICAQDBiNsJvGxGfHiflXu1M5DycmLWwTYgIiRezul38kMKogZkpMyONvg45iPwbm2xPN1y o4UcodM9tDMr0y+v/uqwQVlntsQGfQqedIXWeUyAN3rfOQVSWff0G0ZDpNKFhdLDcfN1YjS6LIp/ Ho/u7TTQEceWzVI9ujPW3U3eCztKS5/CJi/6tRYccjV3yjxd5srhJosaNnZcAdt0FCX+7bWgiA/d eMotHweXMAEtcnn6RtYTKqi5pquDSR3l8u/d5AGOGAqPY1MWhWKpDhk6zLVmpsJrdAfkK+F2PrRt 2PZE4XNiHzvEvqBTViVsUQn3qqvKv3b9bZvzndu/PWa8DFaqr5hIlTpL36dYUNk4dalb6kMMAv+Z 6+hsTXBbKWWc3apdzK8BMewM69KN6Oqce+Zu9ydmDBpI125C4z/eIT574Q1w+2OqqGwaVLRcJXrJ osmLFqa7LH4XXgVNWG4SHQHuEhANxjJ/GP/89PrNbpHoNkm+Gkhpi8KWTRoSsmkXwQqQ1vp5Iki/ untp+HDH+no32NgN0nZPV/+Qt+OR0t3vwmC3Zzrd/qqc8NSLf3Iizsafl7b4r4qgEKjZ+xjGtrVc UjyJthkqcwEKDwOzEmDyei+B26Nu/yYwl/WL3YlXtq09s68rxbd2AvCl1iuahhQqcvbjM4xdCUsT 37uMdBNSSwIDAQABo4ICUjCCAk4wDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAa4wHQYDVR0OBBYE FE4L7xqkQFulF2mHMMo0aEPQQa7yMGQGA1UdHwRdMFswLKAqoCiGJmh0dHA6Ly9jZXJ0LnN0YXJ0 Y29tLm9yZy9zZnNjYS1jcmwuY3JsMCugKaAnhiVodHRwOi8vY3JsLnN0YXJ0Y29tLm9yZy9zZnNj YS1jcmwuY3JsMIIBXQYDVR0gBIIBVDCCAVAwggFMBgsrBgEEAYG1NwEBATCCATswLwYIKwYBBQUH AgEWI2h0dHA6Ly9jZXJ0LnN0YXJ0Y29tLm9yZy9wb2xpY3kucGRmMDUGCCsGAQUFBwIBFilodHRw Oi8vY2VydC5zdGFydGNvbS5vcmcvaW50ZXJtZWRpYXRlLnBkZjCB0AYIKwYBBQUHAgIwgcMwJxYg U3RhcnQgQ29tbWVyY2lhbCAoU3RhcnRDb20pIEx0ZC4wAwIBARqBl0xpbWl0ZWQgTGlhYmlsaXR5 LCByZWFkIHRoZSBzZWN0aW9uICpMZWdhbCBMaW1pdGF0aW9ucyogb2YgdGhlIFN0YXJ0Q29tIENl cnRpZmljYXRpb24gQXV0aG9yaXR5IFBvbGljeSBhdmFpbGFibGUgYXQgaHR0cDovL2NlcnQuc3Rh cnRjb20ub3JnL3BvbGljeS5wZGYwEQYJYIZIAYb4QgEBBAQDAgAHMDgGCWCGSAGG+EIBDQQrFilT dGFydENvbSBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTANBgkqhkiG9w0BAQUFAAOC AgEAFmyZ9GYMNPXQhV59CuzaEE44HF7fpiUFS5Eyweg78T3dRAlbB0mKKctmArexmvclmAk8jhvh 3TaHK0u7aNM5Zj2gJsfyOZEdUauCe37Vzlrk4gNXcGmXCPleWKYK34wGmkUWFjgKXlf2Ysd6AgXm vB618p70qSmD+LIU424oh0TDkBreOKk8rENNZEXO3SipXPJzewT4F+irsfMuXGRuczE6Eri8sxHk fY+BUZo7jYn0TZNmezwD7dOaHZrzZVD1oNB1ny+v8OqCQ5j4aZyJecRDjkZy42Q2Eq/3JR44iZB3 fsNrarnDy0RLrHiQi+fHLB5LEUTINFInzQpdn4XBidUaePKVEFMy3YCEZnXZtWgo+2EuvoSoOMCZ EoalHmdkrQYuL6lwhceWD3yJZfWOQ1QOq92lgDmUYMA0yZZwLKMS9R9Ie70cfmu3nZD0Ijuu+Pwq yvqCUqDvr0tVk+vBtfAii6w0TiYiBKGHLHVKt+V9E9e4DGTANtLJL4YSjCMJwRuCO3NJo2pXh5Tl 1njFmUNj403gdy3hZZlyaQQaRwnmDwFWJPsfvw55qVguucQJAX6Vum0ABj6y6koQOdjQK/W/7HW/ lwLFCRsI3FU34oH7N4RDYiDK51ZLZer+bMEkkyShNOsF/5oirpt9P/FlUQqmMGqz9IgcgA38coro g14= -----END CERTIFICATE----- """ #EOF# pagekite-0.5.8a/scripts/legacy-testing/pagekite-0.4.6a.py0000775000175000017500000115210312603542202022401 0ustar brebre00000000000000#!/usr/bin/python # # NOTE: This is a compilation of multiple Python files. # See below for details on individual segments. # import base64, imp, os, sys, StringIO, zlib __FILES = {} __os_path_exists = os.path.exists __builtin_open = open def __comb_open(filename, *args, **kwargs): if filename in __FILES: return StringIO.StringIO(__FILES[filename]) else: return __builtin_open(filename, *args, **kwargs) def __comb_exists(filename, *args, **kwargs): if filename in __FILES: return True else: return __os_path_exists(filename, *args, **kwargs) open = __comb_open os.path.exists = __comb_exists sys.path[0:0] = ['.SELF/'] ############################################################################### __FILES[".SELF/sockschain/__init__.py"] = """\ #!/usr/bin/python \"\"\"SocksiPy - Python SOCKS module. Version 2.00 Copyright 2011 Bjarni R. Einarsson. All rights reserved. Copyright 2006 Dan-Haim. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of Dan Haim nor the names of his contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY DAN HAIM \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL DAN HAIM OR HIS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMANGE. This module provides a standard socket-like interface for Python for tunneling connections through SOCKS proxies. \"\"\" \"\"\" Refactored to allow proxy chaining and use as a command-line netcat-like tool by Bjarni R. Einarsson (http://bre.klaki.net/) for use with PageKite (http://pagekite.net/). Minor modifications made by Christopher Gilbert (http://motomastyle.com/) for use in PyLoris (http://pyloris.sourceforge.net/) Minor modifications made by Mario Vilas (http://breakingcode.wordpress.com/) mainly to merge bug fixes found in Sourceforge \"\"\" import base64, errno, os, socket, sys, select, struct, threading DEBUG = False #def DEBUG(foo): print foo ##[ SSL compatibility code ]################################################## try: import hashlib def sha1hex(data): hl = hashlib.sha1() hl.update(data) return hl.hexdigest().lower() except ImportError: import sha def sha1hex(data): return sha.new(data).hexdigest().lower() def SSL_CheckName(commonName, digest, valid_names): pairs = [(commonName, '%s/%s' % (commonName, digest))] valid = 0 if commonName.startswith('*.'): commonName = commonName[1:].lower() for name in valid_names: name = name.split('/')[0].lower() if ('.'+name).endswith(commonName): pairs.append((name, '%s/%s' % (name, digest))) for commonName, cNameDigest in pairs: if ((commonName in valid_names) or (cNameDigest in valid_names)): valid += 1 if DEBUG: DEBUG(('*** Cert score: %s (%s ?= %s)' ) % (valid, pairs, valid_names)) return valid HAVE_SSL = False HAVE_PYOPENSSL = False TLS_CA_CERTS = \"/etc/ssl/certs/ca-certificates.crt\" try: if '--nopyopenssl' in sys.argv or '--nossl' in sys.argv: raise ImportError('pyOpenSSL disabled') from OpenSSL import SSL HAVE_SSL = HAVE_PYOPENSSL = True def SSL_Connect(ctx, sock, server_side=False, accepted=False, connected=False, verify_names=None): if DEBUG: DEBUG('*** TLS is provided by pyOpenSSL') if verify_names: def vcb(conn, x509, errno, depth, rc): if errno != 0: return False if depth != 0: return True return (SSL_CheckName(x509.get_subject().commonName.lower(), x509.digest('sha1').replace(':',''), verify_names) > 0) ctx.set_verify(SSL.VERIFY_PEER | SSL.VERIFY_FAIL_IF_NO_PEER_CERT, vcb) else: def vcb(conn, x509, errno, depth, rc): return (errno == 0) ctx.set_verify(SSL.VERIFY_NONE, vcb) nsock = SSL.Connection(ctx, sock) if accepted: nsock.set_accept_state() if connected: nsock.set_connect_state() if verify_names: nsock.do_handshake() return nsock except ImportError: try: if '--nossl' in sys.argv: raise ImportError('SSL disabled') import ssl HAVE_SSL = True class SSL(object): SSLv23_METHOD = ssl.PROTOCOL_SSLv23 SSLv3_METHOD = ssl.PROTOCOL_SSLv3 TLSv1_METHOD = ssl.PROTOCOL_TLSv1 WantReadError = ssl.SSLError class Error(Exception): pass class SysCallError(Exception): pass class WantWriteError(Exception): pass class ZeroReturnError(Exception): pass class Context(object): def __init__(self, method): self.method = method self.privatekey_file = None self.certchain_file = None self.ca_certs = None self.ciphers = None def use_privatekey_file(self, fn): self.privatekey_file = fn def use_certificate_chain_file(self, fn): self.certchain_file = fn def set_cipher_list(self, ciphers): self.ciphers = ciphers def load_verify_locations(self, pemfile, capath=None): self.ca_certs = pemfile def SSL_CheckPeerName(fd, names): cert = fd.getpeercert() certhash = sha1hex(fd.getpeercert(binary_form=True)) if not cert: return None valid = 0 for field in cert['subject']: if field[0][0].lower() == 'commonname': valid += SSL_CheckName(field[0][1].lower(), certhash, names) if 'subjectAltName' in cert: for field in cert['subjectAltName']: if field[0].lower() == 'dns': name = field[1].lower() valid += SSL_CheckName(field[1].lower(), certhash, names) return (valid > 0) def SSL_Connect(ctx, sock, server_side=False, accepted=False, connected=False, verify_names=None): if DEBUG: DEBUG('*** TLS is provided by native Python ssl') reqs = (verify_names and ssl.CERT_REQUIRED or ssl.CERT_NONE) try: fd = ssl.wrap_socket(sock, keyfile=ctx.privatekey_file, certfile=ctx.certchain_file, cert_reqs=reqs, ca_certs=ctx.ca_certs, do_handshake_on_connect=False, ssl_version=ctx.method, ciphers=ctx.ciphers, server_side=server_side) except: fd = ssl.wrap_socket(sock, keyfile=ctx.privatekey_file, certfile=ctx.certchain_file, cert_reqs=reqs, ca_certs=ctx.ca_certs, do_handshake_on_connect=False, ssl_version=ctx.method, server_side=server_side) if verify_names: fd.do_handshake() if not SSL_CheckPeerName(fd, verify_names): raise SSL.Error(('Cert not in %s (%s)' ) % (verify_names, reqs)) return fd except ImportError: class SSL(object): # Mock to let our try/except clauses below not fail. class Error(Exception): pass class SysCallError(Exception): pass class WantWriteError(Exception): pass class ZeroReturnError(Exception): pass def DisableSSLCompression(): # Hack to disable compression in OpenSSL and reduce memory usage *lots*. # Source: # http://journal.paul.querna.org/articles/2011/04/05/openssl-memory-use/ try: import ctypes import glob openssl = ctypes.CDLL(None, ctypes.RTLD_GLOBAL) try: f = openssl.SSL_COMP_get_compression_methods except AttributeError: ssllib = sorted(glob.glob(\"/usr/lib/libssl.so.*\"))[0] openssl = ctypes.CDLL(ssllib, ctypes.RTLD_GLOBAL) openssl.SSL_COMP_get_compression_methods.restype = ctypes.c_void_p openssl.sk_zero.argtypes = [ctypes.c_void_p] openssl.sk_zero(openssl.SSL_COMP_get_compression_methods()) except Exception: if DEBUG: DEBUG('disableSSLCompression: Failed') ##[ SocksiPy itself ]######################################################### PROXY_TYPE_DEFAULT = -1 PROXY_TYPE_NONE = 0 PROXY_TYPE_SOCKS4 = 1 PROXY_TYPE_SOCKS5 = 2 PROXY_TYPE_HTTP = 3 PROXY_TYPE_SSL = 4 PROXY_TYPE_SSL_WEAK = 5 PROXY_TYPE_SSL_ANON = 6 PROXY_TYPE_TOR = 7 PROXY_TYPE_HTTPS = 8 PROXY_TYPE_HTTP_CONNECT = 9 PROXY_TYPE_HTTPS_CONNECT = 10 PROXY_SSL_TYPES = (PROXY_TYPE_SSL, PROXY_TYPE_SSL_WEAK, PROXY_TYPE_SSL_ANON, PROXY_TYPE_HTTPS, PROXY_TYPE_HTTPS_CONNECT) PROXY_HTTP_TYPES = (PROXY_TYPE_HTTP, PROXY_TYPE_HTTPS) PROXY_HTTPC_TYPES = (PROXY_TYPE_HTTP_CONNECT, PROXY_TYPE_HTTPS_CONNECT) PROXY_SOCKS5_TYPES = (PROXY_TYPE_SOCKS5, PROXY_TYPE_TOR) PROXY_DEFAULTS = { PROXY_TYPE_NONE: 0, PROXY_TYPE_DEFAULT: 0, PROXY_TYPE_HTTP: 8080, PROXY_TYPE_HTTP_CONNECT: 8080, PROXY_TYPE_SOCKS4: 1080, PROXY_TYPE_SOCKS5: 1080, PROXY_TYPE_TOR: 9050, } PROXY_TYPES = { 'none': PROXY_TYPE_NONE, 'default': PROXY_TYPE_DEFAULT, 'defaults': PROXY_TYPE_DEFAULT, 'http': PROXY_TYPE_HTTP, 'httpc': PROXY_TYPE_HTTP_CONNECT, 'socks': PROXY_TYPE_SOCKS5, 'socks4': PROXY_TYPE_SOCKS4, 'socks4a': PROXY_TYPE_SOCKS4, 'socks5': PROXY_TYPE_SOCKS5, 'tor': PROXY_TYPE_TOR, } if HAVE_SSL: PROXY_DEFAULTS.update({ PROXY_TYPE_HTTPS: 443, PROXY_TYPE_HTTPS_CONNECT: 443, PROXY_TYPE_SSL: 443, PROXY_TYPE_SSL_WEAK: 443, PROXY_TYPE_SSL_ANON: 443, }) PROXY_TYPES.update({ 'https': PROXY_TYPE_HTTPS, 'httpcs': PROXY_TYPE_HTTPS_CONNECT, 'ssl': PROXY_TYPE_SSL, 'ssl-anon': PROXY_TYPE_SSL_ANON, 'ssl-weak': PROXY_TYPE_SSL_WEAK, }) P_TYPE = 0 P_HOST = 1 P_PORT = 2 P_RDNS = 3 P_USER = 4 P_PASS = P_CACERTS = 5 P_CERTS = 6 DEFAULT_ROUTE = '*' _proxyroutes = { } _orgsocket = socket.socket _orgcreateconn = getattr(socket, 'create_connection', None) try: _thread_locals = threading.local() def _thread_local(): return _thread_locals except AttributeError: # Pre 2.4, we have to implement our own. _thread_local_dict = {} class Storage(object): pass def _thread_local(): tid = str(threading.currentThread()) if not tid in _thread_local_dict: _thread_local_dict[tid] = Storage() return _thread_local_dict[tid] class ProxyError(Exception): pass class GeneralProxyError(ProxyError): pass class Socks5AuthError(ProxyError): pass class Socks5Error(ProxyError): pass class Socks4Error(ProxyError): pass class HTTPError(ProxyError): pass _generalerrors = (\"success\", \"invalid data\", \"not connected\", \"not available\", \"bad proxy type\", \"bad input\") _socks5errors = (\"succeeded\", \"general SOCKS server failure\", \"connection not allowed by ruleset\", \"Network unreachable\", \"Host unreachable\", \"Connection refused\", \"TTL expired\", \"Command not supported\", \"Address type not supported\", \"Unknown error\") _socks5autherrors = (\"succeeded\", \"authentication is required\", \"all offered authentication methods were rejected\", \"unknown username or invalid password\", \"unknown error\") _socks4errors = (\"request granted\", \"request rejected or failed\", \"request rejected because SOCKS server cannot connect to identd on the client\", \"request rejected because the client program and identd report different user-ids\", \"unknown error\") def parseproxy(arg): # This silly function will do a quick-and-dirty parse of our argument # into a proxy specification array. It lets people omit stuff. args = arg.replace('/', '').split(':') args[0] = PROXY_TYPES[args[0] or 'http'] if (len(args) in (3, 4, 5)) and ('@' in args[2]): # Re-order http://user:pass@host:port/ => http:host:port:user:pass pwd, host = args[2].split('@') user = args[1] args[1:3] = [host] if len(args) == 2: args.append(PROXY_DEFAULTS[args[0]]) if len(args) == 3: args.append(False) args.extend([user, pwd]) elif (len(args) in (2, 3, 4)) and ('@' in args[1]): user, host = args[1].split('@') args[1] = host if len(args) == 2: args.append(PROXY_DEFAULTS[args[0]]) if len(args) == 3: args.append(False) args.append(user) if len(args) == 2: args.append(PROXY_DEFAULTS[args[0]]) if len(args) > 2: args[2] = int(args[2]) if args[P_TYPE] in PROXY_SSL_TYPES: names = (args[P_HOST] or '').split(',') args[P_HOST] = names[0] while len(args) <= P_CERTS: args.append((len(args) == P_RDNS) and True or None) args[P_CERTS] = (len(names) > 1) and names[1:] or names return args def addproxy(dest, proxytype=None, addr=None, port=None, rdns=True, username=None, password=None, certnames=None): global _proxyroutes route = _proxyroutes.get(dest.lower(), None) proxy = (proxytype, addr, port, rdns, username, password, certnames) if route is None: route = _proxyroutes.get(DEFAULT_ROUTE, [])[:] route.append(proxy) _proxyroutes[dest.lower()] = route if DEBUG: DEBUG('Routes are: %s' % (_proxyroutes, )) def setproxy(dest, *args, **kwargs): global _proxyroutes dest = dest.lower() if args: _proxyroutes[dest] = [] return addproxy(dest, *args, **kwargs) else: if dest in _proxyroutes: del _proxyroutes[dest.lower()] def setdefaultcertfile(path): global TLS_CA_CERTS TLS_CA_CERTS = path def setdefaultproxy(*args, **kwargs): \"\"\"setdefaultproxy(proxytype, addr[, port[, rdns[, username[, password[, certnames]]]]]) Sets a default proxy which all further socksocket objects will use, unless explicitly changed. \"\"\" if args and args[P_TYPE] == PROXY_TYPE_DEFAULT: raise ValueError(\"Circular reference to default proxy.\") return setproxy(DEFAULT_ROUTE, *args, **kwargs) def adddefaultproxy(*args, **kwargs): if args and args[P_TYPE] == PROXY_TYPE_DEFAULT: raise ValueError(\"Circular reference to default proxy.\") return addproxy(DEFAULT_ROUTE, *args, **kwargs) def usesystemdefaults(): import os no_proxy = ['localhost', 'localhost.localdomain', '127.0.0.1'] no_proxy.extend(os.environ.get('NO_PROXY', os.environ.get('NO_PROXY', '')).split(',')) for host in no_proxy: setproxy(host, PROXY_TYPE_NONE) for var in ('ALL_PROXY', 'HTTPS_PROXY', 'http_proxy'): val = os.environ.get(var.lower(), os.environ.get(var, None)) if val: setdefaultproxy(*parseproxy(val)) os.environ[var] = '' return def sockcreateconn(*args, **kwargs): _thread_local().create_conn = args[0] try: rv = _orgcreateconn(*args, **kwargs) return rv finally: del(_thread_local().create_conn) class socksocket(socket.socket): \"\"\"socksocket([family[, type[, proto]]]) -> socket object Open a SOCKS enabled socket. The parameters are the same as those of the standard socket init. In order for SOCKS to work, you must specify family=AF_INET, type=SOCK_STREAM and proto=0. \"\"\" def __init__(self, family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0): self.__sock = _orgsocket(family, type, proto) self.__proxy = None self.__proxysockname = None self.__proxypeername = None self.__makefile_refs = 0 self.__buffer = '' self.__negotiating = False self.__override = ['addproxy', 'setproxy', 'getproxysockname', 'getproxypeername', 'close', 'connect', 'getpeername', 'makefile', 'recv'] #, 'send', 'sendall'] def __getattribute__(self, name): if name.startswith('_socksocket__'): return object.__getattribute__(self, name) elif name in self.__override: return object.__getattribute__(self, name) else: return getattr(object.__getattribute__(self, \"_socksocket__sock\"), name) def __setattr__(self, name, value): if name.startswith('_socksocket__'): return object.__setattr__(self, name, value) else: return setattr(object.__getattribute__(self, \"_socksocket__sock\"), name, value) def __recvall(self, count): \"\"\"__recvall(count) -> data Receive EXACTLY the number of bytes requested from the socket. Blocks until the required number of bytes have been received or a timeout occurs. \"\"\" self.__sock.setblocking(1) try: self.__sock.settimeout(20) except: # Python 2.2 compatibility hacks. pass data = self.recv(count) while len(data) < count: d = self.recv(count-len(data)) if d == '': raise GeneralProxyError((0, \"connection closed unexpectedly\")) data = data + d return data def close(self): if self.__makefile_refs < 1: self.__sock.close() else: self.__makefile_refs -= 1 def makefile(self, mode='r', bufsize=-1): self.__makefile_refs += 1 try: return socket._fileobject(self, mode, bufsize, close=True) except TypeError: # Python 2.2 compatibility hacks. return socket._fileobject(self, mode, bufsize) def addproxy(self, proxytype=None, addr=None, port=None, rdns=True, username=None, password=None, certnames=None): \"\"\"setproxy(proxytype, addr[, port[, rdns[, username[, password[, certnames]]]]]) Sets the proxy to be used. proxytype - The type of the proxy to be used. Three types are supported: PROXY_TYPE_SOCKS4 (including socks4a), PROXY_TYPE_SOCKS5 and PROXY_TYPE_HTTP addr - The address of the server (IP or DNS). port - The port of the server. Defaults to 1080 for SOCKS servers and 8080 for HTTP proxy servers. rdns - Should DNS queries be preformed on the remote side (rather than the local side). The default is True. Note: This has no effect with SOCKS4 servers. username - Username to authenticate with to the server. The default is no authentication. password - Password to authenticate with to the server. Only relevant when username is also provided. \"\"\" proxy = (proxytype, addr, port, rdns, username, password, certnames) if not self.__proxy: self.__proxy = [] self.__proxy.append(proxy) def setproxy(self, *args, **kwargs): \"\"\"setproxy(proxytype, addr[, port[, rdns[, username[, password[, certnames]]]]]) (see addproxy) \"\"\" self.__proxy = [] self.addproxy(*args, **kwargs) def __negotiatesocks5(self, destaddr, destport, proxy): \"\"\"__negotiatesocks5(self, destaddr, destport, proxy) Negotiates a connection through a SOCKS5 server. \"\"\" # First we'll send the authentication packages we support. if (proxy[P_USER]!=None) and (proxy[P_PASS]!=None): # The username/password details were supplied to the # setproxy method so we support the USERNAME/PASSWORD # authentication (in addition to the standard none). self.sendall(struct.pack('BBBB', 0x05, 0x02, 0x00, 0x02)) else: # No username/password were entered, therefore we # only support connections with no authentication. self.sendall(struct.pack('BBB', 0x05, 0x01, 0x00)) # We'll receive the server's response to determine which # method was selected chosenauth = self.__recvall(2) if chosenauth[0:1] != chr(0x05).encode(): self.close() raise GeneralProxyError((1, _generalerrors[1])) # Check the chosen authentication method if chosenauth[1:2] == chr(0x00).encode(): # No authentication is required pass elif chosenauth[1:2] == chr(0x02).encode(): # Okay, we need to perform a basic username/password # authentication. self.sendall(chr(0x01).encode() + chr(len(proxy[P_USER])) + proxy[P_USER] + chr(len(proxy[P_PASS])) + proxy[P_PASS]) authstat = self.__recvall(2) if authstat[0:1] != chr(0x01).encode(): # Bad response self.close() raise GeneralProxyError((1, _generalerrors[1])) if authstat[1:2] != chr(0x00).encode(): # Authentication failed self.close() raise Socks5AuthError((3, _socks5autherrors[3])) # Authentication succeeded else: # Reaching here is always bad self.close() if chosenauth[1] == chr(0xFF).encode(): raise Socks5AuthError((2, _socks5autherrors[2])) else: raise GeneralProxyError((1, _generalerrors[1])) # Now we can request the actual connection req = struct.pack('BBB', 0x05, 0x01, 0x00) # If the given destination address is an IP address, we'll # use the IPv4 address request even if remote resolving was specified. try: ipaddr = socket.inet_aton(destaddr) req = req + chr(0x01).encode() + ipaddr except socket.error: # Well it's not an IP number, so it's probably a DNS name. if proxy[P_RDNS]: # Resolve remotely ipaddr = None req = req + (chr(0x03).encode() + chr(len(destaddr)).encode() + destaddr) else: # Resolve locally ipaddr = socket.inet_aton(socket.gethostbyname(destaddr)) req = req + chr(0x01).encode() + ipaddr req = req + struct.pack(\">H\", destport) self.sendall(req) # Get the response resp = self.__recvall(4) if resp[0:1] != chr(0x05).encode(): self.close() raise GeneralProxyError((1, _generalerrors[1])) elif resp[1:2] != chr(0x00).encode(): # Connection failed self.close() if ord(resp[1:2])<=8: raise Socks5Error((ord(resp[1:2]), _socks5errors[ord(resp[1:2])])) else: raise Socks5Error((9, _socks5errors[9])) # Get the bound address/port elif resp[3:4] == chr(0x01).encode(): boundaddr = self.__recvall(4) elif resp[3:4] == chr(0x03).encode(): resp = resp + self.recv(1) boundaddr = self.__recvall(ord(resp[4:5])) else: self.close() raise GeneralProxyError((1,_generalerrors[1])) boundport = struct.unpack(\">H\", self.__recvall(2))[0] self.__proxysockname = (boundaddr, boundport) if ipaddr != None: self.__proxypeername = (socket.inet_ntoa(ipaddr), destport) else: self.__proxypeername = (destaddr, destport) def getproxysockname(self): \"\"\"getsockname() -> address info Returns the bound IP address and port number at the proxy. \"\"\" return self.__proxysockname def getproxypeername(self): \"\"\"getproxypeername() -> address info Returns the IP and port number of the proxy. \"\"\" return _orgsocket.getpeername(self) def getpeername(self): \"\"\"getpeername() -> address info Returns the IP address and port number of the destination machine (note: getproxypeername returns the proxy) \"\"\" return self.__proxypeername def __negotiatesocks4(self, destaddr, destport, proxy): \"\"\"__negotiatesocks4(self, destaddr, destport, proxy) Negotiates a connection through a SOCKS4 server. \"\"\" # Check if the destination address provided is an IP address rmtrslv = False try: ipaddr = socket.inet_aton(destaddr) except socket.error: # It's a DNS name. Check where it should be resolved. if proxy[P_RDNS]: ipaddr = struct.pack(\"BBBB\", 0x00, 0x00, 0x00, 0x01) rmtrslv = True else: ipaddr = socket.inet_aton(socket.gethostbyname(destaddr)) # Construct the request packet req = struct.pack(\">BBH\", 0x04, 0x01, destport) + ipaddr # The username parameter is considered userid for SOCKS4 if proxy[P_USER] != None: req = req + proxy[P_USER] req = req + chr(0x00).encode() # DNS name if remote resolving is required # NOTE: This is actually an extension to the SOCKS4 protocol # called SOCKS4A and may not be supported in all cases. if rmtrslv: req = req + destaddr + chr(0x00).encode() self.sendall(req) # Get the response from the server resp = self.__recvall(8) if resp[0:1] != chr(0x00).encode(): # Bad data self.close() raise GeneralProxyError((1,_generalerrors[1])) if resp[1:2] != chr(0x5A).encode(): # Server returned an error self.close() if ord(resp[1:2]) in (91, 92, 93): self.close() raise Socks4Error((ord(resp[1:2]), _socks4errors[ord(resp[1:2]) - 90])) else: raise Socks4Error((94, _socks4errors[4])) # Get the bound address/port self.__proxysockname = (socket.inet_ntoa(resp[4:]), struct.unpack(\">H\", resp[2:4])[0]) if rmtrslv != None: self.__proxypeername = (socket.inet_ntoa(ipaddr), destport) else: self.__proxypeername = (destaddr, destport) def __getproxyauthheader(self, proxy): if proxy[P_USER] and proxy[P_PASS]: auth = proxy[P_USER] + \":\" + proxy[P_PASS] return \"Proxy-Authorization: Basic %s\\r\\n\" % base64.b64encode(auth) else: return \"\" def __stop_http_negotiation(self): buf = self.__buffer host, port, proxy = self.__negotiating self.__buffer = self.__negotiating = None self.__override.remove('send') self.__override.remove('sendall') return (buf, host, port, proxy) def recv(self, count): if self.__negotiating: # If the calling code tries to read before negotiating is done, # assume this is not HTTP, bail and attempt HTTP CONNECT. if DEBUG: DEBUG(\"*** Not HTTP, failing back to HTTP CONNECT.\") buf, host, port, proxy = self.__stop_http_negotiation() self.__negotiatehttpconnect(host, port, proxy) self.__sock.sendall(buf) while True: try: return self.__sock.recv(count) except SSL.SysCallError: return '' except SSL.WantReadError: pass def send(self, *args, **kwargs): if self.__negotiating: self.__buffer += args[0] self.__negotiatehttpproxy() else: return self.__sock.send(*args, **kwargs) def sendall(self, *args, **kwargs): if self.__negotiating: self.__buffer += args[0] self.__negotiatehttpproxy() else: return self.__sock.sendall(*args, **kwargs) def __negotiatehttp(self, destaddr, destport, proxy): \"\"\"__negotiatehttpproxy(self, destaddr, destport, proxy) Negotiates a connection through an HTTP proxy server. \"\"\" if destport in (21, 22, 23, 25, 109, 110, 143, 220, 443, 993, 995): # Go straight to HTTP CONNECT for anything related to e-mail, # SSH, telnet, FTP, SSL, ... self.__negotiatehttpconnect(destaddr, destport, proxy) else: if DEBUG: DEBUG('*** Transparent HTTP proxy mode...') self.__negotiating = (destaddr, destport, proxy) self.__override.extend(['send', 'sendall']) def __negotiatehttpproxy(self): \"\"\"__negotiatehttp(self, destaddr, destport, proxy) Negotiates an HTTP request through an HTTP proxy server. \"\"\" buf = self.__buffer host, port, proxy = self.__negotiating # If our buffer is tiny, wait for data. if len(buf) <= 3: return # If not HTTP, fall back to HTTP CONNECT. if buf[0:3].lower() not in ('get', 'pos', 'hea', 'put', 'del', 'opt', 'pro'): if DEBUG: DEBUG(\"*** Not HTTP, failing back to HTTP CONNECT.\") self.__stop_http_negotiation() self.__negotiatehttpconnect(host, port, proxy) self.__sock.sendall(buf) return # Have we got the end of the headers? if buf.find('\\r\\n\\r\\n'.encode()) != -1: CRLF = '\\r\\n' elif buf.find('\\n\\n'.encode()) != -1: CRLF = '\\n' else: # Nope return # Remove our send/sendall hooks. self.__stop_http_negotiation() # Format the proxy request. host += ':%d' % port headers = buf.split(CRLF) for hdr in headers: if hdr.lower().startswith('host: '): host = hdr[6:] req = headers[0].split(' ', 1) headers[0] = '%s http://%s%s' % (req[0], host, req[1]) headers[1] = self.__getproxyauthheader(proxy) + headers[1] # Send it! if DEBUG: DEBUG(\"*** Proxy request:\\n%s***\" % CRLF.join(headers)) self.__sock.sendall(CRLF.join(headers).encode()) def __negotiatehttpconnect(self, destaddr, destport, proxy): \"\"\"__negotiatehttp(self, destaddr, destport, proxy) Negotiates an HTTP CONNECT through an HTTP proxy server. \"\"\" # If we need to resolve locally, we do this now if not proxy[P_RDNS]: addr = socket.gethostbyname(destaddr) else: addr = destaddr self.__sock.sendall((\"CONNECT \" + addr + \":\" + str(destport) + \" HTTP/1.1\\r\\n\" + self.__getproxyauthheader(proxy) + \"Host: \" + destaddr + \"\\r\\n\\r\\n\" ).encode()) # We read the response until we get \"\\r\\n\\r\\n\" or \"\\n\\n\" resp = self.__recvall(1) while (resp.find(\"\\r\\n\\r\\n\".encode()) == -1 and resp.find(\"\\n\\n\".encode()) == -1): resp = resp + self.__recvall(1) # We just need the first line to check if the connection # was successful statusline = resp.splitlines()[0].split(\" \".encode(), 2) if statusline[0] not in (\"HTTP/1.0\".encode(), \"HTTP/1.1\".encode()): self.close() raise GeneralProxyError((1, _generalerrors[1])) try: statuscode = int(statusline[1]) except ValueError: self.close() raise GeneralProxyError((1, _generalerrors[1])) if statuscode != 200: self.close() raise HTTPError((statuscode, statusline[2])) self.__proxysockname = (\"0.0.0.0\", 0) self.__proxypeername = (addr, destport) def __get_ca_ciphers(self): return 'HIGH:MEDIUM:!MD5' def __get_ca_anon_ciphers(self): return 'aNULL' def __get_ca_certs(self): return TLS_CA_CERTS def __negotiatessl(self, destaddr, destport, proxy, weak=False, anonymous=False): \"\"\"__negotiatessl(self, destaddr, destport, proxy) Negotiates an SSL session. \"\"\" ssl_version = SSL.SSLv3_METHOD want_hosts = ca_certs = self_cert = None ciphers = self.__get_ca_ciphers() if anonymous: # Insecure and use anon ciphers - this is just camoflage ciphers = self.__get_ca_anon_ciphers() elif not weak: # This is normal, secure mode. self_cert = proxy[P_USER] or None ca_certs = proxy[P_CACERTS] or self.__get_ca_certs() or None want_hosts = proxy[P_CERTS] or [proxy[P_HOST]] try: ctx = SSL.Context(ssl_version) ctx.set_cipher_list(ciphers) if self_cert: ctx.use_certificate_chain_file(self_cert) ctx.use_privatekey_file(self_cert) if ca_certs and want_hosts: ctx.load_verify_locations(ca_certs) self.__sock.setblocking(1) self.__sock = SSL_Connect(ctx, self.__sock, connected=True, verify_names=want_hosts) except: if DEBUG: DEBUG('*** SSL problem: %s/%s/%s' % (sys.exc_info(), self.__sock, want_hosts)) raise self.__encrypted = True if DEBUG: DEBUG('*** Wrapped %s:%s in %s' % (destaddr, destport, self.__sock)) def __default_route(self, dest): route = _proxyroutes.get(str(dest).lower(), [])[:] if not route or route[0][P_TYPE] == PROXY_TYPE_DEFAULT: route[0:1] = _proxyroutes.get(DEFAULT_ROUTE, []) while route and route[0][P_TYPE] == PROXY_TYPE_DEFAULT: route.pop(0) return route def connect(self, destpair): \"\"\"connect(self, despair) Connects to the specified destination through a chain of proxies. destpar - A tuple of the IP/DNS address and the port number. (identical to socket's connect). To select the proxy servers use setproxy() and chainproxy(). \"\"\" if DEBUG: DEBUG('*** Connect: %s / %s' % (destpair, self.__proxy)) destpair = getattr(_thread_local(), 'create_conn', destpair) # Do a minimal input check first if ((not type(destpair) in (list, tuple)) or (len(destpair) < 2) or (type(destpair[0]) != type('')) or (type(destpair[1]) != int)): raise GeneralProxyError((5, _generalerrors[5])) if self.__proxy: proxy_chain = self.__proxy default_dest = destpair[0] else: proxy_chain = self.__default_route(destpair[0]) default_dest = DEFAULT_ROUTE for proxy in proxy_chain: if (proxy[P_TYPE] or PROXY_TYPE_NONE) not in PROXY_DEFAULTS: raise GeneralProxyError((4, _generalerrors[4])) chain = proxy_chain[:] chain.append([PROXY_TYPE_NONE, destpair[0], destpair[1]]) if DEBUG: DEBUG('*** Chain: %s' % (chain, )) first = True result = None while chain: proxy = chain.pop(0) if proxy[P_TYPE] == PROXY_TYPE_DEFAULT: chain[0:0] = self.__default_route(default_dest) if DEBUG: DEBUG('*** Chain: %s' % chain) continue if proxy[P_PORT] != None: portnum = proxy[P_PORT] else: portnum = PROXY_DEFAULTS[proxy[P_TYPE] or PROXY_TYPE_NONE] if first and proxy[P_HOST]: if DEBUG: DEBUG('*** Connect: %s:%s' % (proxy[P_HOST], portnum)) result = self.__sock.connect((proxy[P_HOST], portnum)) if chain: nexthop = (chain[0][P_HOST] or '', int(chain[0][P_PORT] or 0)) if proxy[P_TYPE] in PROXY_SSL_TYPES: if DEBUG: DEBUG('*** TLS/SSL Setup: %s' % (nexthop, )) self.__negotiatessl(nexthop[0], nexthop[1], proxy, weak=(proxy[P_TYPE] == PROXY_TYPE_SSL_WEAK), anonymous=(proxy[P_TYPE] == PROXY_TYPE_SSL_ANON)) if proxy[P_TYPE] in PROXY_HTTPC_TYPES: if DEBUG: DEBUG('*** HTTP CONNECT: %s' % (nexthop, )) self.__negotiatehttpconnect(nexthop[0], nexthop[1], proxy) elif proxy[P_TYPE] in PROXY_HTTP_TYPES: if len(chain) > 1: # Chaining requires HTTP CONNECT. if DEBUG: DEBUG('*** HTTP CONNECT: %s' % (nexthop, )) self.__negotiatehttpconnect(nexthop[0], nexthop[1], proxy) else: # If we are last in the chain, do transparent magic. if DEBUG: DEBUG('*** HTTP PROXY: %s' % (nexthop, )) self.__negotiatehttp(nexthop[0], nexthop[1], proxy) if proxy[P_TYPE] in PROXY_SOCKS5_TYPES: if DEBUG: DEBUG('*** SOCKS5: %s' % (nexthop, )) self.__negotiatesocks5(nexthop[0], nexthop[1], proxy) elif proxy[P_TYPE] == PROXY_TYPE_SOCKS4: if DEBUG: DEBUG('*** SOCKS4: %s' % (nexthop, )) self.__negotiatesocks4(nexthop[0], nexthop[1], proxy) elif proxy[P_TYPE] == PROXY_TYPE_NONE: if first and nexthop[0] and nexthop[1]: if DEBUG: DEBUG('*** Connect: %s:%s' % nexthop) result = self.__sock.connect(nexthop) else: raise GeneralProxyError((4, _generalerrors[4])) first = False if DEBUG: DEBUG('*** Connected! (%s)' % result) return result def wrapmodule(module): \"\"\"wrapmodule(module) Attempts to replace a module's socket library with a SOCKS socket. This will only work on modules that import socket directly into the namespace; most of the Python Standard Library falls into this category. \"\"\" module.socket.socket = socksocket module.socket.create_connection = sockcreateconn ## Netcat-like proxy-chaining tools follow ## def netcat(s, i, o, keep_open=''): if hasattr(o, 'buffer'): o = o.buffer try: in_fileno = i.fileno() isel = [s, i] obuf, sbuf, oselo, osels = [], [], [], [] while isel: in_r, out_r, err_r = select.select(isel, oselo+osels, isel, 1000) # print 'In:%s Out:%s Err:%s' % (in_r, out_r, err_r) if s in in_r: obuf.append(s.recv(4096)) oselo = [o] if len(obuf[-1]) == 0: if DEBUG: DEBUG('EOF(s, in)') isel.remove(s) if o in out_r: o.write(obuf[0]) if len(obuf) == 1: if len(obuf[0]) == 0: if DEBUG: DEBUG('CLOSE(o)') o.close() if i in isel and 'i' not in keep_open: isel.remove(i) i.close() else: o.flush() obuf, oselo = [], [] else: obuf.pop(0) if i in in_r: sbuf.append(os.read(in_fileno, 4096)) osels = [s] if len(sbuf[-1]) == 0: if DEBUG: DEBUG('EOF(i)') isel.remove(i) if s in out_r: s.send(sbuf[0]) if len(sbuf) == 1: if len(sbuf[0]) == 0: if s in isel and 's' not in keep_open: if DEBUG: DEBUG('CLOSE(s)') isel.remove(s) s.close() else: if DEBUG: DEBUG('SHUTDOWN(s, WR)') s.shutdown(socket.SHUT_WR) sbuf, osels = [], [] else: sbuf.pop(0) for data in sbuf: s.sendall(data) for data in obuf: o.write(data) except: if DEBUG: DEBUG(\"Disconnected: %s\" % (sys.exc_info(), )) i.close() s.close() o.close() def __proxy_connect_netcat(hostname, port, chain, keep_open): try: s = socksocket(socket.AF_INET, socket.SOCK_STREAM) for proxy in chain: s.addproxy(*proxy) s.connect((hostname, port)) except: sys.stderr.write('Error: %s\\n' % (sys.exc_info(), )) return False netcat(s, sys.stdin, sys.stdout, keep_open) return True def __make_proxy_chain(args): chain = [] for arg in args: chain.append(parseproxy(arg)) return chain def DebugPrint(text): print(text) def Main(): keep_open = 's' try: args = sys.argv[1:] if '--wait' in args: keep_open = 'si' args.remove('--wait') if '--nowait' in args: keep_open = '' args.remove('--nowait') if '--debug' in args: global DEBUG DEBUG = DebugPrint args.remove('--debug') for arg in ('--nopyopenssl', '--nossl'): while arg in args: args.remove(arg) usesystemdefaults() dest_host, dest_port = args.pop().split(':', 1) dest_port = int(dest_port) chain = __make_proxy_chain(args) except: DebugPrint('Error: %s' % (sys.exc_info(), )) sys.stderr.write(('Usage: %s ' '[ [ ...]] ' '\\n') % os.path.basename(sys.argv[0])) sys.exit(1) try: if not __proxy_connect_netcat(dest_host, dest_port, chain, keep_open): sys.exit(2) except KeyboardInterrupt: sys.exit(0) if __name__ == \"__main__\": Main() """ sys.modules["sockschain"] = imp.new_module("sockschain") sys.modules["sockschain"].open = __comb_open exec __FILES[".SELF/sockschain/__init__.py"] in sys.modules["sockschain"].__dict__ ############################################################################### __FILES[".SELF/pagekite/__init__.py"] = """\ #!/usr/bin/python -u LICENSE = \"\"\"\\ pagekite.py, Copyright 2010, 2011, the Beanstalks Project ehf. and Bjarni Runar Einarsson This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see: \"\"\" ##[ Maybe TODO: ]############################################################## # # Optimization: # - Implement epoll() support. # - Stress test this thing: when do we need a C rewrite? # - Make multi-process, use the FD-over-socket trick? Threads=>GIL=>bleh # - Add QoS and bandwidth shaping # - Add a scheduler for deferred/periodic processing. # - Replace string concatenation ops with lists of buffers. # # Protocols: # - Make tunnel creation more stubborn (try multiple ports etc.) # - Add XMPP and incoming SMTP support. # - Replace/augment current tunnel auth scheme with SSL certificates. # # User interface: # - Enable (re)configuration from within HTTP UI. # - More human readable console output? # # Bugs? # - Front-ends should time-out dead back-ends. # - Gzip-related memory issues. # # ##[ Hacking guide! ]########################################################### # # Hello! Welcome to my source code. # # Here's a brief intro to how the program is structured, to encourage people # to hack and improve. # # * The PageKite object contains the master configuration and some related # routines. It takes care of parsing configuration files and implements # things like the authentication protocol. It also contains the main event # loop, which is select() or epoll() based. In short, it's the boss. # # * The Connections object keeps track of which tunnels and user connections # are open at any given time and which protocol/domain pairs they belong to. # It gets passed around as an argument quite a lot - not too elegant. # # * The Selectable and it's *Parser subclasses incrementally build up basic # parsers for the supported protocols. Note that none of the protocols # are fully implemented, we only implement the bare minimum required to # figure out which back-end should handle a given request, and then forward # the bytes unmodified over that channel. As a result, the current HTTP # proxy code is not HTTP 1.1 compliant - but if you put it behind Varnish # or some other decent reverse-proxy, then *the combination* should be! # # * The UserConn object represents connections on behalf of users. It can # be created as a FrontEnd, which will find the right tunnel and send # traffic to the back-end PageKite process, where a BackEnd UserConn # will be created to connect to the actual HTTP server. # # * The Tunnel object represents one end of a PageKite tunnel and is also # created either as a FrontEnd or BackEnd, depending on which end it is. # Tunnels handle multiplexing and demultiplexing all the traffic for # a given back-end so multiple requests can share a single TCP/IP # connection. # # Although most of the work done by pagekite.py happens in an event-loop # on a single thread, there are some exceptions: # # * The AuthThread handles checking whether an incoming tunnel request is # allowed or not; authentication requests may end up blocking and waiting # for each other, but the main work of proxying data back and forth won't # be blocked. # # * The HttpUiThread implements a basic HTTP (or HTTPS) server, for basic # monitoring and static file serving. # # WARNING: The UI threading code assumes it is running in CPython, where the # GIL makes snooping across the thread-boundary relatively safe, even # without explicit locking. Beware! # ############################################################################### # PROTOVER = '0.8' APPVER = '0.4.6a' AUTHOR = 'Bjarni Runar Einarsson, http://bre.klaki.net/' WWWHOME = 'http://pagekite.net/' LICENSE_URL = 'http://www.gnu.org/licenses/agpl.html' EXAMPLES = (\"\"\"\\ Basic usage, gives http://localhost:80/ a public name: $ pagekite.py NAME.pagekite.me To expose specific folders, files or use alternate local ports: $ pagekite.py +indexes /a/path/ NAME.pagekite.me # built-in HTTPD $ pagekite.py *.html NAME.pagekite.me # built-in HTTPD $ pagekite.py 3000 NAME.pagekite.me # http://localhost:3000/ To expose multiple local servers (SSH and HTTP): $ pagekite.py ssh://NAME.pagekite.me AND 3000 http://NAME.pagekite.me \"\"\") MINIDOC = (\"\"\"\\ >>> Welcome to pagekite.py v%s! %s To sign up with PageKite.net or get advanced instructions: $ pagekite.py --signup $ pagekite.py --help If you request a kite which does not exist in your configuration file, the program will offer to help you sign up with http://pagekite.net/ and create it. Pick a name, any name!\"\"\") % (APPVER, EXAMPLES) DOC = (\"\"\"\\ pagekite.py is Copyright 2010, 2011, the Beanstalks Project ehf. v%s http://pagekite.net/ This the reference implementation of the PageKite tunneling protocol, both the front- and back-end. This following protocols are supported: HTTP - HTTP 1.1 only, requires a valid HTTP Host: header HTTPS - Recent versions of TLS only, requires the SNI extension. WEBSOCKET - Using the proposed Upgrade: WebSocket method. Other protocols may be proxied by using \"raw\" back-ends and HTTP CONNECT. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License. For the full text of the license, see: http://www.gnu.org/licenses/agpl-3.0.html Usage: pagekite.py [options] [shortcuts] Common Options: --clean Skip loading the default configuration file. --signup Interactively sign up for PageKite.net service. --defaults Set defaults for use with PageKite.net service. --local=ports Configure for local serving only (no remote front-end) --optfile=X -o X Read settings from file X. Default is ~/.pagekite.rc. --optdir=X -O X Read settings from *.rc in directory X. --savefile=X -S X Saved settings will be written to file X. --reloadfile=X Re-read settings from X on SIGHUP. --autosave Enable auto-saving. --noautosave Disable auto-saving. --save Save this configuration. --settings Dump the current settings to STDOUT, formatted as an options file would be. --httpd=X:P -H X:P Enable the HTTP user interface on hostname X, port P. --pemfile=X -P X Use X as a PEM key for the HTTPS UI. --httppass=X -X X Require password X to access the UI. --nozchunks Disable zlib tunnel compression. --sslzlib Enable zlib compression in OpenSSL. --buffers N Buffer at most N kB of back-end data before blocking. --logfile=F -L F Log to file F. --daemonize -Z Run as a daemon. --runas -U U:G Set UID:GID after opening our listening sockets. --pidfile=P -I P Write PID to the named file. --nocrashreport Don't send anonymous crash reports to PageKite.net. --tls_default=N Default name to use for SSL, if SNI and tracking fail. --tls_endpoint=N:F Terminate SSL/TLS for name N, using key/cert from F. --errorurl=U -E U URL to redirect to when back-ends are not found. Front-end Options: --isfrontend -f Enable front-end mode. --authdomain=X -A X Use X as a remote authentication domain. --motd=/path/to/motd Send the contents of this file to new back-ends. --host=H -h H Listen on H (hostname). --ports=A,B,C -p A,B Listen on ports A, B, C, ... --portalias=A:B Report port A as port B to backends. --protos=A,B,C Accept the listed protocols for tunneling. --rawports=A,B,C Listen on ports A, B, C, ... (raw/timed connections) --domain=proto,proto2,pN:domain:secret Accept tunneling requests for the named protocols and specified domain, using the given secret. A * may be used as a wildcard for subdomains or protocols. Back-end Options: --backend=proto:kitename:host:port:secret Configure a back-end service on host:port, using protocol proto and the given kite name as the public domain. As a special case, if host is 'localhost' and the word 'built-in' is used as a port number, pagekite.py's HTTP server will be used. --define_backend=... Same as --backend, except not enabled by default. --delete_backend=... Delete a given back-end. --frontends=N:X:P Choose N front-ends from X (a DNS domain name), port P. --frontend=host:port Connect to the named front-end server. --fe_certname=N Connect using SSL, accepting valid certs for domain N. --ca_certs=PATH Path to your trusted root SSL certificates file. --dyndns=X -D X Register changes with DynDNS provider X. X can either be simply the name of one of the 'built-in' providers, or a URL format string for ad-hoc updating. --all -a Terminate early if any tunnels fail to register. --new -N Don't attempt to connect to the domain's old front-end. --noprobes Reject all probes for back-end liveness. --fingerpath=P Path recipe for the httpfinger back-end proxy. --proxy=T:S:P Connect using a chain of proxies (requires socks.py) --socksify=S:P Connect via SOCKS server S, port P (requires socks.py) --torify=S:P Same as socksify, but more paranoid. About the configuration file: The configuration file contains the same options as are available to the command line, with the restriction that there be exactly one \"option\" per line. The leading '--' may also be omitted for readability, and for the same reason it is recommended to use the long form of the options in the configuration file (also, the short form may not always parse correctly). Blank lines and lines beginning with # (comments) are treated as comments and are ignored. It is perfectly acceptable to have multiple configuration files, and configuration files can include other configuration files. NOTE: When using -o or --optfile on the command line, it is almost always advisable to use --clean as well, to suppress the default configuration. Examples: Create a configuration file with default options, and then edit it. $ pagekite.py --defaults --settings > ~/.pagekite.rc $ vim ~/.pagekite.rc Run the built-in HTTPD. $ pagekite.py --defaults --httpd=localhost:9999 $ firefox http://localhost:9999/ Fly a PageKite on pagekite.net for somedomain.com, and register the new front-ends with the No-IP Dynamic DNS provider. $ pagekite.py \\\\ --defaults \\\\ --dyndns=user:pass@no-ip.com \\\\ --backend=http:kitename.com:localhost:80:mygreatsecret Shortcuts: A shortcut is simply the name of a kite following a list of zero or more 'things' to expose using that name. Pagekite knows how to expose either servers running on localhost ports or directories and files using the built-in HTTP server. If no list of things to expose is provided, the defaults for that kite are read from the configuration file or http://localhost:80/ used as a last-resort default. If a kite name is requested which does not already exist in the configuration file and program is run interactively, the user will be prompted and given the option of signing up and/or creating a new kite using the PageKite.net service. Multiple short-cuts can be specified on a single command-line, separated by the word 'AND' (note capital letters are required). This may cause problems if you have many files and folders by that name, but that should be relatively rare. :-) Shortcut examples: \"\"\"+EXAMPLES) % APPVER MAGIC_PREFIX = '/~:PageKite:~/' MAGIC_PATH = '%sv%s' % (MAGIC_PREFIX, PROTOVER) MAGIC_PATHS = (MAGIC_PATH, '/Beanstalk~Magic~Beans/0.2') SERVICE_PROVIDER = 'PageKite.net' SERVICE_DOMAINS = ('pagekite.me', ) SERVICE_XMLRPC = 'http://pagekite.net/xmlrpc/' SERVICE_TOS_URL = 'https://pagekite.net/support/terms/' OPT_FLAGS = 'o:O:S:H:P:X:L:ZI:fA:R:h:p:aD:U:NE:' OPT_ARGS = ['noloop', 'clean', 'nopyopenssl', 'nossl', 'nocrashreport', 'nullui', 'remoteui', 'uiport=', 'help', 'settings', 'optfile=', 'optdir=', 'savefile=', 'reloadfile=', 'autosave', 'noautosave', 'friendly', 'signup', 'list', 'add', 'only', 'disable', 'remove', 'save', 'service_xmlrpc=', 'controlpanel', 'controlpass', 'httpd=', 'pemfile=', 'httppass=', 'errorurl=', 'webpath=', 'logfile=', 'daemonize', 'nodaemonize', 'runas=', 'pidfile=', 'isfrontend', 'noisfrontend', 'settings', 'defaults', 'local=', 'domain=', 'authdomain=', 'motd=', 'register=', 'host=', 'noupgradeinfo', 'upgradeinfo=', 'ports=', 'protos=', 'portalias=', 'rawports=', 'tls_default=', 'tls_endpoint=', 'fe_certname=', 'jakenoia', 'ca_certs=', 'kitename=', 'kitesecret=', 'fingerpath=', 'backend=', 'define_backend=', 'be_config=', 'delete_backend', 'frontend=', 'frontends=', 'torify=', 'socksify=', 'proxy=', 'new', 'all', 'noall', 'dyndns=', 'nozchunks', 'sslzlib', 'buffers=', 'noprobes', 'debugio', # DEPRECATED: 'webroot=', 'webaccess=', 'webindexes='] DEBUG_IO = False DEFAULT_CHARSET = 'utf-8' DEFAULT_BUFFER_MAX = 1024 AUTH_ERRORS = '255.255.255.' AUTH_ERR_USER_UNKNOWN = '.0' AUTH_ERR_INVALID = '.1' AUTH_QUOTA_MAX = '255.255.254.255' VIRTUAL_PN = 'virtual' CATCHALL_HN = 'unknown' LOOPBACK_HN = 'loopback' LOOPBACK_FE = LOOPBACK_HN + ':1' LOOPBACK_BE = LOOPBACK_HN + ':2' LOOPBACK = {'FE': LOOPBACK_FE, 'BE': LOOPBACK_BE} WEB_POLICY_DEFAULT = 'default' WEB_POLICY_PUBLIC = 'public' WEB_POLICY_PRIVATE = 'private' WEB_POLICY_OTP = 'otp' WEB_POLICIES = (WEB_POLICY_DEFAULT, WEB_POLICY_PUBLIC, WEB_POLICY_PRIVATE, WEB_POLICY_OTP) WEB_INDEX_ALL = 'all' WEB_INDEX_ON = 'on' WEB_INDEX_OFF = 'off' WEB_INDEXTYPES = (WEB_INDEX_ALL, WEB_INDEX_ON, WEB_INDEX_OFF) BE_PROTO = 0 BE_PORT = 1 BE_DOMAIN = 2 BE_BHOST = 3 BE_BPORT = 4 BE_SECRET = 5 BE_STATUS = 6 BE_STATUS_REMOTE_SSL = 0x0010000 BE_STATUS_OK = 0x0001000 BE_STATUS_ERR_DNS = 0x0000100 BE_STATUS_ERR_BE = 0x0000010 BE_STATUS_ERR_TUNNEL = 0x0000001 BE_STATUS_ERR_ANY = 0x0000fff BE_STATUS_UNKNOWN = 0 BE_STATUS_DISABLED = 0x8000000 BE_STATUS_DISABLE_ONCE = 0x4000000 BE_INACTIVE = (BE_STATUS_DISABLED, BE_STATUS_DISABLE_ONCE) BE_NONE = ['', '', None, None, None, '', BE_STATUS_UNKNOWN] DYNDNS = { 'pagekite.net': ('http://up.pagekite.net/' '?hostname=%(domain)s&myip=%(ips)s&sign=%(sign)s'), 'beanstalks.net': ('http://up.b5p.us/' '?hostname=%(domain)s&myip=%(ips)s&sign=%(sign)s'), 'dyndns.org': ('https://%(user)s:%(pass)s@members.dyndns.org' '/nic/update?wildcard=NOCHG&backmx=NOCHG' '&hostname=%(domain)s&myip=%(ip)s'), 'no-ip.com': ('https://%(user)s:%(pass)s@dynupdate.no-ip.com' '/nic/update?hostname=%(domain)s&myip=%(ip)s'), } ##[ Standard imports ]######################################################## import base64 import cgi from cgi import escape as escape_html import errno import getopt import httplib import os import random import re import select import socket rawsocket = socket.socket import struct import sys import tempfile import threading import time import traceback import urllib import xmlrpclib import zlib import SocketServer from CGIHTTPServer import CGIHTTPRequestHandler from SimpleXMLRPCServer import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler import Cookie # This should be our socksipy import sockschain as socks ##[ Conditional imports & compatibility magic! ]############################### # Create our service-domain matching regexp SERVICE_DOMAIN_RE = re.compile('\\.(' + '|'.join(SERVICE_DOMAINS) + ')$') SERVICE_SUBDOMAIN_RE = re.compile(r'^([A-Za-z0-9_-]+\\.)*[A-Za-z0-9_-]+$') # System logging on Unix try: import syslog except ImportError: pass # Backwards compatibility for old Pythons. if not 'SHUT_RD' in dir(socket): socket.SHUT_RD = 0 socket.SHUT_WR = 1 socket.SHUT_RDWR = 2 try: import datetime ts_to_date = datetime.datetime.fromtimestamp except ImportError: ts_to_date = str try: sorted([1, 2, 3]) except: def sorted(l): tmp = l[:] tmp.sort() return tmp # SSL/TLS strategy: prefer pyOpenSSL, as it comes with built-in Context # objects. If that fails, look for Python 2.6+ native ssl support and # create a compatibility wrapper. If both fail, bomb with a ConfigError # when the user tries to enable anything SSL-related. SEND_ALWAYS_BUFFERS = False SEND_MAX_BYTES = 16 * 1024 if socks.HAVE_PYOPENSSL: SSL = socks.SSL elif socks.HAVE_SSL: SEND_ALWAYS_BUFFERS = True SEND_MAX_BYTES = 4 * 1024 SSL = socks.SSL else: class SSL(object): SSLv23_METHOD = 0 TLSv1_METHOD = 0 class Error(Exception): pass class SysCallError(Exception): pass class WantReadError(Exception): pass class WantWriteError(Exception): pass class ZeroReturnError(Exception): pass class Context(object): def __init__(self, method): raise ConfigError('Neither pyOpenSSL nor python 2.6+ ' 'ssl modules found!') # Different Python 2.x versions complain about deprecation depending on # where we pull these from. try: from urlparse import parse_qs, urlparse except ImportError, e: from cgi import parse_qs from urlparse import urlparse try: import hashlib def sha1hex(data): hl = hashlib.sha1() hl.update(data) return hl.hexdigest().lower() except ImportError: import sha def sha1hex(data): return sha.new(data).hexdigest().lower() # Enable system proxies # This will all fail if we don't have PySocksipyChain available. # FIXME: Move this code somewhere else? socks.usesystemdefaults() socks.wrapmodule(sys.modules[__name__]) if socks.HAVE_SSL: # Secure connections to pagekite.net in SSL tunnels. def_hop = socks.parseproxy('default') https_hop = socks.parseproxy('httpcs:pagekite.net:443') for dest in ('pagekite.net', 'up.pagekite.net', 'up.b5p.us'): socks.setproxy(dest, *def_hop) socks.addproxy(dest, *socks.parseproxy('http:%s:443' % dest)) socks.addproxy(dest, *https_hop) else: # FIXME: Should scream and shout about lack of security. pass # YamonD is a part of PageKite.net's internal monitoring systems. It's not # required, so if you don't have it, the mock makes things Just Work. class MockYamonD(object): def __init__(self, sspec, server=None, handler=None): pass def vmax(self, var, value): pass def vscale(self, var, ratio, add=0): pass def vset(self, var, value): pass def vadd(self, var, value, wrap=None): pass def vmin(self, var, value): pass def vdel(self, var): pass def lcreate(self, listn, elems): pass def ladd(self, listn, value): pass def render_vars_text(self): return '' def quit(self): pass def run(self): pass YamonD = MockYamonD gYamon = YamonD(()) class MockPageKiteXmlRpc: def __init__(self, config): self.config = config def getSharedSecret(self, email, p): for be in self.config.backends.values(): if be[BE_SECRET]: return be[BE_SECRET] def getAvailableDomains(self, a, b): return ['.%s' % x for x in SERVICE_DOMAINS] def signUp(self, a, b): return { 'secret': self.getSharedSecret(a, b) } def addCnameKite(self, a, s, k): return {} def addKite(self, a, s, k): return {} ##[ PageKite.py code starts here! ]############################################ gSecret = None def globalSecret(): global gSecret if not gSecret: # This always works... gSecret = '%8.8x%s%8.8x' % (random.randint(0, 0x7FFFFFFE), time.time(), random.randint(0, 0x7FFFFFFE)) # Next, see if we can augment that with some real randomness. try: newSecret = sha1hex(open('/dev/urandom').read(64) + gSecret) gSecret = newSecret LogDebug('Seeded signatures using /dev/urandom, hooray!') except: try: newSecret = sha1hex(os.urandom(64) + gSecret) gSecret = newSecret LogDebug('Seeded signatures using os.urandom(), hooray!') except: LogInfo('WARNING: Seeding signatures with time.time() and random.randint()') return gSecret TOKEN_LENGTH=36 def signToken(token=None, secret=None, payload='', timestamp=None, length=TOKEN_LENGTH): \"\"\" This will generate a random token with a signature which could only have come from this server. If a token is provided, it is re-signed so the original can be compared with what we would have generated, for verification purposes. If a timestamp is provided it will be embedded in the signature to a resolution of 10 minutes, and the signature will begin with the letter 't' Note: This is only as secure as random.randint() is random. \"\"\" if not secret: secret = globalSecret() if not token: token = sha1hex('%s%8.8x' % (globalSecret(), random.randint(0, 0x7FFFFFFD)+1)) if timestamp: tok = 't' + token[1:] ts = '%x' % int(timestamp/600) return tok[0:8] + sha1hex(secret + payload + ts + tok[0:8])[0:length-8] else: return token[0:8] + sha1hex(secret + payload + token[0:8])[0:length-8] def checkSignature(sign='', secret='', payload=''): \"\"\" Check a signature for validity. When using timestamped signatures, we only accept signatures from the current and previous windows. \"\"\" if sign[0] == 't': ts = int(time.time()) for window in (0, 1): valid = signToken(token=sign, secret=secret, payload=payload, timestamp=(ts-(window*600))) if sign == valid: return True return False else: valid = signToken(token=sign, secret=secret, payload=payload) return sign == valid class ConfigError(Exception): pass class ConnectError(Exception): pass def HTTP_PageKiteRequest(server, backends, tokens=None, nozchunks=False, tls=False, testtoken=None, replace=None): req = ['CONNECT PageKite:1 HTTP/1.0\\r\\n', 'X-PageKite-Version: %s\\r\\n' % APPVER] if not nozchunks: req.append('X-PageKite-Features: ZChunks\\r\\n') if replace: req.append('X-PageKite-Replace: %s\\r\\n' % replace) if tls: req.append('X-PageKite-Features: TLS\\r\\n') tokens = tokens or {} for d in backends.keys(): if (backends[d][BE_BHOST] and backends[d][BE_SECRET] and backends[d][BE_STATUS] not in BE_INACTIVE): # A stable (for replay on challenge) but unguessable salt. my_token = sha1hex(globalSecret() + server + backends[d][BE_SECRET] )[:TOKEN_LENGTH] # This is the challenge (salt) from the front-end, if any. server_token = d in tokens and tokens[d] or '' # Our payload is the (proto, name) combined with both salts data = '%s:%s:%s' % (d, my_token, server_token) # Sign the payload with the shared secret (random salt). sign = signToken(secret=backends[d][BE_SECRET], payload=data, token=testtoken) req.append('X-PageKite: %s:%s\\r\\n' % (data, sign)) req.append('\\r\\n') return ''.join(req) def HTTP_ResponseHeader(code, title, mimetype='text/html'): if mimetype.startswith('text/') and ';' not in mimetype: mimetype += ('; charset=%s' % DEFAULT_CHARSET) return ('HTTP/1.1 %s %s\\r\\nContent-Type: %s\\r\\nPragma: no-cache\\r\\n' 'Expires: 0\\r\\nCache-Control: no-store\\r\\nConnection: close' '\\r\\n') % (code, title, mimetype) def HTTP_Header(name, value): return '%s: %s\\r\\n' % (name, value) def HTTP_StartBody(): return '\\r\\n' def HTTP_ConnectOK(): return 'HTTP/1.0 200 Connection Established\\r\\n\\r\\n' def HTTP_ConnectBad(): return 'HTTP/1.0 503 Sorry\\r\\n\\r\\n' def HTTP_Response(code, title, body, mimetype='text/html', headers=None): data = [HTTP_ResponseHeader(code, title, mimetype)] if headers: data.extend(headers) data.extend([HTTP_StartBody(), ''.join(body)]) return ''.join(data) def HTTP_NoFeConnection(): return HTTP_Response(200, 'OK', base64.decodestring( 'R0lGODlhCgAKAMQCAN4hIf/+/v///+EzM+AuLvGkpORISPW+vudgYOhiYvKpqeZY' 'WPbAwOdaWup1dfOurvW7u++Rkepycu6PjwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAACH5BAEAAAIALAAAAAAKAAoAAAUtoCAcyEA0jyhEQOs6AuPO' 'QJHQrjEAQe+3O98PcMMBDAdjTTDBSVSQEmGhEIUAADs='), headers=[HTTP_Header('X-PageKite-Status', 'Down-FE')], mimetype='image/gif') def HTTP_NoBeConnection(): return HTTP_Response(200, 'OK', base64.decodestring( 'R0lGODlhCgAKAPcAAI9hE6t2Fv/GAf/NH//RMf/hd7u6uv/mj/ntq8XExMbFxc7N' 'zc/Ozv/xwfj31+jn5+vq6v///////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAACH5BAEAABIALAAAAAAKAAoAAAhDACUIlBAgwMCDARo4MHiQ' '4IEGDAcGKAAAAESEBCoiiBhgQEYABzYK7OiRQIEDBgMIEDCgokmUKlcOKFkgZcGb' 'BSUEBAA7'), headers=[HTTP_Header('X-PageKite-Status', 'Down-BE')], mimetype='image/gif') def HTTP_GoodBeConnection(): return HTTP_Response(200, 'OK', base64.decodestring( 'R0lGODlhCgAKANUCAEKtP0StQf8AAG2/a97w3qbYpd/x3mu/aajZp/b79vT69Mnn' 'yK7crXTDcqraqcfmxtLr0VG0T0ivRpbRlF24Wr7jveHy4Pv9+53UnPn8+cjnx4LI' 'gNfu1v///37HfKfZpq/crmG6XgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAACH5BAEAAAIALAAAAAAKAAoAAAZIQIGAUDgMEASh4BEANAGA' 'xRAaaHoYAAPCCZUoOIDPAdCAQhIRgJGiAG0uE+igAMB0MhYoAFmtJEJcBgILVU8B' 'GkpEAwMOggJBADs='), headers=[HTTP_Header('X-PageKite-Status', 'OK')], mimetype='image/gif') def HTTP_Unavailable(where, proto, domain, comment='', frame_url=None, code=503, status='Unavailable', headers=None): if code == 401: headers = headers or [] headers.append(HTTP_Header('WWW-Authenticate', 'Basic realm=PageKite')) message = ''.join(['

    Sorry! (', where, ')

    ', '

    The ', proto.upper(),' ', 'PageKite for ', domain, ' is unavailable at the moment.

    ', '

    Please try again later.

    ']) if frame_url: if '?' in frame_url: frame_url += '&where=%s&proto=%s&domain=%s' % (where.upper(), proto, domain) return HTTP_Response(code, status, ['', '', '', message, '', ''], headers=headers) else: return HTTP_Response(code, status, ['', message, ''], headers=headers) LOG = [] LOG_LINE = 0 LOG_LENGTH = 300 LOG_THRESHOLD = 256 * 1024 def LogValues(values, testtime=None): global LOG_LINE, LOG_LAST_TIME now = int(testtime or time.time()) words = [('ts', '%x' % now), ('t', '%s' % datetime.datetime.fromtimestamp(now).isoformat()), ('ll', '%x' % LOG_LINE)] words.extend([(kv[0], ('%s' % kv[1]).replace('\\t', ' ') .replace('\\r', ' ') .replace('\\n', ' ') .replace('; ', ', ') .strip()) for kv in values]) wdict = dict(words) LOG_LINE += 1 LOG.append(wdict) while len(LOG) > LOG_LENGTH: LOG.pop(0) return (words, wdict) def LogSyslog(values, wdict=None, words=None): if values: words, wdict = LogValues(values) if 'err' in wdict: syslog.syslog(syslog.LOG_ERR, '; '.join(['='.join(x) for x in words])) elif 'debug' in wdict: syslog.syslog(syslog.LOG_DEBUG, '; '.join(['='.join(x) for x in words])) else: syslog.syslog(syslog.LOG_INFO, '; '.join(['='.join(x) for x in words])) LogFile = sys.stdout def LogToFile(values, wdict=None, words=None): if values: words, wdict = LogValues(values) LogFile.write('; '.join(['='.join(x) for x in words])) LogFile.write('\\n') def LogToMemory(values, wdict=None, words=None): if values: LogValues(values) def FlushLogMemory(): for l in LOG: Log(None, wdict=l, words=[(w, l[w]) for w in l]) Log = LogToMemory def LogError(msg, parms=None): emsg = [('err', msg)] if parms: emsg.extend(parms) Log(emsg) global gYamon gYamon.vadd('errors', 1, wrap=1000000) def LogDebug(msg, parms=None): emsg = [('debug', msg)] if parms: emsg.extend(parms) Log(emsg) def LogInfo(msg, parms=None): emsg = [('info', msg)] if parms: emsg.extend(parms) Log(emsg) # FIXME: This could easily be a pool of threads to let us handle more # than one incoming request at a time. class AuthThread(threading.Thread): \"\"\"Handle authentication work in a separate thread.\"\"\" #daemon = True def __init__(self, conns): threading.Thread.__init__(self) self.qc = threading.Condition() self.jobs = [] self.conns = conns def check(self, requests, conn, callback): self.qc.acquire() self.jobs.append((requests, conn, callback)) self.qc.notify() self.qc.release() def quit(self): self.qc.acquire() self.keep_running = False self.qc.notify() self.qc.release() try: self.join() except RuntimeError: pass def run(self): self.keep_running = True while self.keep_running: try: self._run() except Exception, e: LogError('AuthThread died: %s' % e) time.sleep(5) LogDebug('AuthThread: done') def _run(self): self.qc.acquire() while self.keep_running: now = int(time.time()) if self.jobs: (requests, conn, callback) = self.jobs.pop(0) if DEBUG_IO: print '=== AUTH REQUESTS\\n%s\\n===' % requests self.qc.release() quotas = [] results = [] session = '%x:%s:' % (now, globalSecret()) for request in requests: try: proto, domain, srand, token, sign, prefix = request except: LogError('Invalid request: %s' % (request, )) continue what = '%s:%s:%s' % (proto, domain, srand) session += what if not token or not sign: # Send a challenge. Our challenges are time-stamped, so we can # put stict bounds on possible replay attacks (20 minutes atm). results.append(('%s-SignThis' % prefix, '%s:%s' % (what, signToken(payload=what, timestamp=now)))) else: # This is a bit lame, but we only check the token if the quota # for this connection has never been verified. (quota, reason) = self.conns.config.GetDomainQuota(proto, domain, srand, token, sign, check_token=(conn.quota is None)) if not quota: if not reason: reason = 'quota' results.append(('%s-Invalid' % prefix, what)) results.append(('%s-Invalid-Why' % prefix, '%s;%s' % (what, reason))) Log([('rejected', domain), ('quota', quota), ('reason', reason)]) elif self.conns.Tunnel(proto, domain): # FIXME: Allow multiple backends? results.append(('%s-Duplicate' % prefix, what)) Log([('rejected', domain), ('duplicate', 'yes')]) else: results.append(('%s-OK' % prefix, what)) quotas.append(quota) if (proto.startswith('http') and self.conns.config.GetTlsEndpointCtx(domain)): results.append(('%s-SSL-OK' % prefix, what)) results.append(('%s-SessionID' % prefix, '%x:%s' % (now, sha1hex(session)))) results.append(('%s-Misc' % prefix, urllib.urlencode({ 'motd': (self.conns.config.motd_message or ''), }))) for upgrade in self.conns.config.upgrade_info: results.append(('%s-Upgrade' % prefix, ';'.join(upgrade))) if quotas: nz_quotas = [q for q in quotas if q and q > 0] if nz_quotas: quota = min(nz_quotas) if quota is not None: conn.quota = [quota, requests[quotas.index(quota)], time.time()] results.append(('%s-Quota' % prefix, quota)) elif requests: if not conn.quota: conn.quota = [None, requests[0], time.time()] else: conn.quota[2] = time.time() if DEBUG_IO: print '=== AUTH RESULTS\\n%s\\n===' % results callback(results) self.qc.acquire() else: self.qc.wait() self.buffering = 0 self.qc.release() HTTP_METHODS = ['OPTIONS', 'CONNECT', 'GET', 'HEAD', 'POST', 'PUT', 'TRACE', 'PROPFIND', 'PROPPATCH', 'MKCOL', 'DELETE', 'COPY', 'MOVE', 'LOCK', 'UNLOCK', 'PING'] HTTP_VERSIONS = ['HTTP/1.0', 'HTTP/1.1'] ##[ Protocol parsers! ]######################################################## class BaseLineParser(object): \"\"\"Base protocol parser class.\"\"\" PROTO = 'unknown' PROTOS = ['unknown'] PARSE_UNKNOWN = -2 PARSE_FAILED = -1 PARSE_OK = 100 def __init__(self, lines=None, state=PARSE_UNKNOWN, proto=PROTO): self.state = state self.protocol = proto self.lines = [] self.domain = None self.last_parser = self if lines is not None: for line in lines: if not self.Parse(line): break def ParsedOK(self): return (self.state == self.PARSE_OK) def Parse(self, line): self.lines.append(line) return False def ErrorReply(self, port=None): return '' class MagicLineParser(BaseLineParser): \"\"\"Parse an unknown incoming connection request, line-by-line.\"\"\" PROTO = 'magic' def __init__(self, lines=None, state=BaseLineParser.PARSE_UNKNOWN, parsers=[]): self.parsers = [p() for p in parsers] BaseLineParser.__init__(self, lines, state, self.PROTO) if self.last_parser == self: self.last_parser = self.parsers[-1] def ParsedOK(self): return self.last_parser.ParsedOK() def Parse(self, line): BaseLineParser.Parse(self, line) self.last_parser = self.parsers[-1] for p in self.parsers[:]: if not p.Parse(line): self.parsers.remove(p) elif p.ParsedOK(): self.last_parser = p self.domain = p.domain self.protocol = p.protocol self.state = p.state self.parsers = [p] break if not self.parsers: LogDebug('No more parsers!') return (len(self.parsers) > 0) class HttpLineParser(BaseLineParser): \"\"\"Parse an HTTP request, line-by-line.\"\"\" PROTO = 'http' PROTOS = ['http'] IN_REQUEST = 11 IN_HEADERS = 12 IN_BODY = 13 IN_RESPONSE = 14 def __init__(self, lines=None, state=IN_REQUEST, testbody=False): self.method = None self.path = None self.version = None self.code = None self.message = None self.headers = [] self.body_result = testbody BaseLineParser.__init__(self, lines, state, self.PROTO) def ParseResponse(self, line): self.version, self.code, self.message = line.split() if not self.version.upper() in HTTP_VERSIONS: LogDebug('Invalid version: %s' % self.version) return False self.state = self.IN_HEADERS return True def ParseRequest(self, line): self.method, self.path, self.version = line.split() if not self.method in HTTP_METHODS: LogDebug('Invalid method: %s' % self.method) return False if not self.version.upper() in HTTP_VERSIONS: LogDebug('Invalid version: %s' % self.version) return False self.state = self.IN_HEADERS return True def ParseHeader(self, line): if line in ('', '\\r', '\\n', '\\r\\n'): self.state = self.IN_BODY return True header, value = line.split(':', 1) if value and value.startswith(' '): value = value[1:] self.headers.append((header.lower(), value)) return True def ParseBody(self, line): # Could be overridden by subclasses, for now we just play dumb. return self.body_result def ParsedOK(self): return (self.state == self.IN_BODY) def Parse(self, line): BaseLineParser.Parse(self, line) try: if (self.state == self.IN_RESPONSE): return self.ParseResponse(line) elif (self.state == self.IN_REQUEST): return self.ParseRequest(line) elif (self.state == self.IN_HEADERS): return self.ParseHeader(line) elif (self.state == self.IN_BODY): return self.ParseBody(line) except ValueError, err: LogDebug('Parse failed: %s, %s, %s' % (self.state, err, self.lines)) self.state = BaseLineParser.PARSE_FAILED return False def Header(self, header): return [h[1].strip() for h in self.headers if h[0] == header.lower()] class FingerLineParser(BaseLineParser): \"\"\"Parse an incoming Finger request, line-by-line.\"\"\" PROTO = 'finger' PROTOS = ['finger', 'httpfinger'] WANT_FINGER = 71 def __init__(self, lines=None, state=WANT_FINGER): BaseLineParser.__init__(self, lines, state, self.PROTO) def ErrorReply(self, port=None): if port == 79: return ('PageKite wants to know, what domain?\\n' 'Try: finger user+domain@domain\\n') else: return '' def Parse(self, line): BaseLineParser.Parse(self, line) if ' ' in line: return False if '+' in line: arg0, self.domain = line.strip().split('+', 1) elif '@' in line: arg0, self.domain = line.strip().split('@', 1) if self.domain: self.state = BaseLineParser.PARSE_OK self.lines[-1] = '%s\\n' % arg0 return True else: self.state = BaseLineParser.PARSE_FAILED return False class IrcLineParser(BaseLineParser): \"\"\"Parse an incoming IRC connection, line-by-line.\"\"\" PROTO = 'irc' PROTOS = ['irc'] WANT_USER = 61 def __init__(self, lines=None, state=WANT_USER): self.seen = [] BaseLineParser.__init__(self, lines, state, self.PROTO) def ErrorReply(self): return ':pagekite 451 :IRC Gateway requires user@HOST or nick@HOST\\n' def Parse(self, line): BaseLineParser.Parse(self, line) if line in ('\\n', '\\r\\n'): return True if self.state == IrcLineParser.WANT_USER: try: ocmd, arg = line.strip().split(' ', 1) cmd = ocmd.lower() self.seen.append(cmd) args = arg.split(' ') if cmd == 'pass': pass elif cmd in ('user', 'nick'): if '@' in args[0]: parts = args[0].split('@') self.domain = parts[-1] arg0 = '@'.join(parts[:-1]) elif 'nick' in self.seen and 'user' in self.seen and not self.domain: raise Error('No domain found') if self.domain: self.state = BaseLineParser.PARSE_OK self.lines[-1] = '%s %s %s\\n' % (ocmd, arg0, ' '.join(args[1:])) else: self.state = BaseLineParser.PARSE_FAILED except Exception, err: LogDebug('Parse failed: %s, %s, %s' % (self.state, err, self.lines)) self.state = BaseLineParser.PARSE_FAILED return (self.state != BaseLineParser.PARSE_FAILED) ##[ Selectables ]############################################################## def obfuIp(ip): quads = ('%s' % ip).replace(':', '.').split('.') return '~%s' % '.'.join([q for q in quads[-2:]]) selectable_id = 0 buffered_bytes = 0 SELECTABLES = None class Selectable(object): \"\"\"A wrapper around a socket, for use with select.\"\"\" HARMLESS_ERRNOS = (errno.EINTR, errno.EAGAIN, errno.ENOMEM, errno.EBUSY, errno.EDEADLK, errno.EWOULDBLOCK, errno.ENOBUFS, errno.EALREADY) def __init__(self, fd=None, address=None, on_port=None, maxread=16000, ui=None, tracked=True, bind=None, backlog=100): self.fd = None try: self.SetFD(fd or rawsocket(socket.AF_INET6, socket.SOCK_STREAM), six=True) if bind: self.fd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.fd.bind(bind) self.fd.listen(backlog) self.fd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) except: self.SetFD(fd or rawsocket(socket.AF_INET, socket.SOCK_STREAM)) if bind: self.fd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.fd.bind(bind) self.fd.listen(backlog) self.fd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.address = address self.on_port = on_port self.created = self.bytes_logged = time.time() self.last_activity = 0 self.dead = False self.ui = ui # Quota-related stuff self.quota = None # Read-related variables self.maxread = maxread self.read_bytes = self.all_in = 0 self.read_eof = False self.peeking = False self.peeked = 0 # Write-related variables self.wrote_bytes = self.all_out = 0 self.write_blocked = '' self.write_speed = 102400 self.write_eof = False self.write_retry = None # Throttle reads and writes self.throttle_until = 0 # Compression stuff self.zw = None self.zlevel = 1 self.zreset = False # Logging self.logged = [] global selectable_id selectable_id += 1 selectable_id %= 0x10000 self.sid = selectable_id self.alt_id = None if address: addr = address or ('x.x.x.x', 'x') self.log_id = 's%x/%s:%s' % (self.sid, obfuIp(addr[0]), addr[1]) else: self.log_id = 's%x' % self.sid # Introspection global SELECTABLES if SELECTABLES is not None: SELECTABLES.append(self) global gYamon self.countas = 'selectables_live' gYamon.vadd(self.countas, 1) gYamon.vadd('selectables', 1) def CountAs(self, what): global gYamon gYamon.vadd(self.countas, -1) self.countas = what gYamon.vadd(self.countas, 1) def __del__(self): global gYamon gYamon.vadd(self.countas, -1) gYamon.vadd('selectables', -1) def __str__(self): return '%s: %s' % (self.log_id, self.__class__) def __html__(self): try: peer = self.fd.getpeername() sock = self.fd.getsockname() except Exception: peer = ('x.x.x.x', 'x') sock = ('x.x.x.x', 'x') return ('Outgoing ZChunks: %s
    ' 'Buffered bytes: %s
    ' 'Remote address: %s
    ' 'Local address: %s
    ' 'Bytes in / out: %s / %s
    ' 'Created: %s
    ' 'Status: %s
    ' '
    ' 'Logged:
      %s

    ' '\\n') % (self.zw and ('level %d' % self.zlevel) or 'off', len(self.write_blocked), self.dead and '-' or (obfuIp(peer[0]), peer[1]), self.dead and '-' or (obfuIp(sock[0]), sock[1]), self.all_in + self.read_bytes, self.all_out + self.wrote_bytes, time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(self.created)), self.dead and 'dead' or 'alive', ''.join(['
  • %s' % (l, ) for l in self.logged])) def ResetZChunks(self): if self.zw: self.zreset = True self.zw = zlib.compressobj(self.zlevel) def EnableZChunks(self, level=1): self.zlevel = level self.zw = zlib.compressobj(level) def SetFD(self, fd, six=False): if self.fd: self.fd.close() self.fd = fd self.fd.setblocking(0) try: if six: self.fd.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) self.fd.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) self.fd.setsockopt(socket.SOL_TCP, socket.TCP_KEEPIDLE, 60) self.fd.setsockopt(socket.SOL_TCP, socket.TCP_KEEPCNT, 10) self.fd.setsockopt(socket.SOL_TCP, socket.TCP_KEEPINTVL, 1) except Exception: pass def SetConn(self, conn): self.SetFD(conn.fd) self.log_id = conn.log_id self.read_bytes = conn.read_bytes self.wrote_bytes = conn.wrote_bytes def Log(self, values): if self.log_id: values.append(('id', self.log_id)) Log(values) self.logged.append(('', values)) def LogError(self, error, params=None): values = params or [] if self.log_id: values.append(('id', self.log_id)) LogError(error, values) self.logged.append((error, values)) def LogDebug(self, message, params=None): values = params or [] if self.log_id: values.append(('id', self.log_id)) LogDebug(message, values) self.logged.append((message, values)) def LogInfo(self, message, params=None): values = params or [] if self.log_id: values.append(('id', self.log_id)) LogInfo(message, values) self.logged.append((message, values)) def LogTraffic(self, final=False): if self.wrote_bytes or self.read_bytes: now = time.time() self.all_out += self.wrote_bytes self.all_in += self.read_bytes if self.ui: self.ui.Status('traffic') global gYamon gYamon.vadd(\"bytes_all\", self.wrote_bytes + self.read_bytes, wrap=1000000000) if final: self.Log([('wrote', '%d' % self.wrote_bytes), ('wbps', '%d' % self.write_speed), ('read', '%d' % self.read_bytes), ('eof', '1')]) else: self.Log([('wrote', '%d' % self.wrote_bytes), ('wbps', '%d' % self.write_speed), ('read', '%d' % self.read_bytes)]) self.bytes_logged = now self.wrote_bytes = self.read_bytes = 0 elif final: self.Log([('eof', '1')]) def Cleanup(self, close=True): global buffered_bytes buffered_bytes -= len(self.write_blocked) self.write_blocked = self.peeked = self.zw = '' if not self.dead: self.dead = True self.CountAs('selectables_dead') if close: if self.fd: self.fd.close() self.LogTraffic(final=True) self.fd = None def SayHello(self): pass def ProcessData(self, data): self.LogError('Selectable::ProcessData: Should be overridden!') return False def ProcessEof(self): if self.read_eof and self.write_eof and not self.write_blocked: self.Cleanup() return False return True def ProcessEofRead(self): self.read_eof = True self.LogError('Selectable::ProcessEofRead: Should be overridden!') return False def ProcessEofWrite(self): self.write_eof = True self.LogError('Selectable::ProcessEofWrite: Should be overridden!') return False def EatPeeked(self, eat_bytes=None, keep_peeking=False): if not self.peeking: return if eat_bytes is None: eat_bytes = self.peeked discard = '' while len(discard) < eat_bytes: try: discard += self.fd.recv(eat_bytes - len(discard)) except socket.error, (errno, msg): self.LogInfo('Error reading (%d/%d) socket: %s (errno=%s)' % ( eat_bytes, self.peeked, msg, errno)) time.sleep(0.1) self.peeked -= eat_bytes self.peeking = keep_peeking return def ReadData(self, maxread=None): if self.read_eof: return False try: maxread = maxread or self.maxread if self.peeking: data = self.fd.recv(maxread, socket.MSG_PEEK) self.peeked = len(data) if DEBUG_IO: print '<== IN (peeked)\\n%s\\n===' % data else: data = self.fd.recv(maxread) if DEBUG_IO: print '<== IN\\n%s\\n===' % data except (SSL.WantReadError, SSL.WantWriteError), err: return True except IOError, err: if err.errno not in self.HARMLESS_ERRNOS: self.LogDebug('Error reading socket: %s (%s)' % (err, err.errno)) return False else: return True except (SSL.Error, SSL.ZeroReturnError, SSL.SysCallError), err: self.LogDebug('Error reading socket (SSL): %s' % err) return False except socket.error, (errno, msg): if errno in self.HARMLESS_ERRNOS: return True else: self.LogInfo('Error reading socket: %s (errno=%s)' % (msg, errno)) return False self.last_activity = time.time() if data is None or data == '': self.read_eof = True return self.ProcessData('') else: if not self.peeking: self.read_bytes += len(data) if self.read_bytes > LOG_THRESHOLD: self.LogTraffic() return self.ProcessData(data) def Throttle(self, max_speed=None, remote=False, delay=0.2): if max_speed: self.throttle_until = time.time() flooded = self.read_bytes + self.all_in flooded -= max_speed * (time.time() - self.created) delay = min(15, max(0.2, flooded/max_speed)) if flooded < 0: delay = 15 else: if self.throttle_until < time.time(): self.throttle_until = time.time() flooded = '?' self.throttle_until += delay self.LogInfo('Throttled until %x (flooded=%s, bps=%s, remote=%s)' % ( int(self.throttle_until), flooded, max_speed, remote)) return True def Send(self, data, try_flush=False): global buffered_bytes buffered_bytes -= len(self.write_blocked) # If we're already blocked, just buffer unless explicitly asked to flush. if (not try_flush) and (len(self.write_blocked) > 0 or SEND_ALWAYS_BUFFERS): self.write_blocked += ''.join(data) buffered_bytes += len(self.write_blocked) return True self.write_speed = int((self.wrote_bytes + self.all_out) / (0.1 + time.time() - self.created)) sending = self.write_blocked+(''.join(data)) self.write_blocked = '' sent_bytes = 0 if sending: try: sent_bytes = self.fd.send(sending[:(self.write_retry or SEND_MAX_BYTES)]) if DEBUG_IO: print '==> OUT\\n%s\\n===' % sending[:sent_bytes] self.wrote_bytes += sent_bytes self.write_retry = None except IOError, err: if err.errno not in self.HARMLESS_ERRNOS: self.LogInfo('Error sending: %s' % err) self.ProcessEofWrite() return False else: self.write_retry = len(sending) except (SSL.WantWriteError, SSL.WantReadError), err: self.write_retry = len(sending) except socket.error, (errno, msg): if errno not in self.HARMLESS_ERRNOS: self.LogInfo('Error sending: %s (errno=%s)' % (msg, errno)) self.ProcessEofWrite() return False else: self.write_retry = len(sending) except (SSL.Error, SSL.ZeroReturnError, SSL.SysCallError), err: self.LogInfo('Error sending (SSL): %s' % err) self.ProcessEofWrite() return False self.write_blocked = sending[sent_bytes:] buffered_bytes += len(self.write_blocked) if self.wrote_bytes >= LOG_THRESHOLD: self.LogTraffic() if self.write_eof and not self.write_blocked: self.ProcessEofWrite() return True def SendChunked(self, data, compress=True, zhistory=None): rst = '' if self.zreset: self.zreset = False rst = 'R' # Stop compressing streams that just get bigger. if zhistory and (zhistory[0] < zhistory[1]): compress = False sdata = ''.join(data) if self.zw and compress: try: zdata = self.zw.compress(sdata) + self.zw.flush(zlib.Z_SYNC_FLUSH) if zhistory: zhistory[0] = len(sdata) zhistory[1] = len(zdata) return self.Send(['%xZ%x%s\\r\\n%s' % (len(sdata), len(zdata), rst, zdata)]) except zlib.error: LogError('Error compressing, resetting ZChunks.') self.ResetZChunks() return self.Send(['%x%s\\r\\n%s' % (len(sdata), rst, sdata)]) def Flush(self, loops=50, wait=False): while loops != 0 and len(self.write_blocked) > 0 and self.Send([], try_flush=True): if wait and len(self.write_blocked) > 0: time.sleep(0.1) LogDebug('Flushing...') loops -= 1 if self.write_blocked: return False return True class Connections(object): \"\"\"A container for connections (Selectables), config and tunnel info.\"\"\" def __init__(self, config): self.config = config self.ip_tracker = {} self.idle = [] self.conns = [] self.conns_by_id = {} self.tunnels = {} self.auth = None def start(self, auth_thread=None): self.auth = auth_thread or AuthThread(self) self.auth.start() def Add(self, conn, alt_id=None): self.idle.append(conn) self.conns.append(conn) if alt_id: self.conns_by_id[alt_id] = conn def TrackIP(self, ip, domain): tick = '%d' % (time.time()/12) if tick not in self.ip_tracker: deadline = int(tick)-10 for ot in self.ip_tracker.keys(): if int(ot) < deadline: del self.ip_tracker[ot] self.ip_tracker[tick] = {} if ip not in self.ip_tracker[tick]: self.ip_tracker[tick][ip] = [1, domain] else: self.ip_tracker[tick][ip][0] += 1 self.ip_tracker[tick][ip][1] = domain def LastIpDomain(self, ip): domain = None for tick in sorted(self.ip_tracker.keys()): if ip in self.ip_tracker[tick]: domain = self.ip_tracker[tick][ip][1] return domain def Remove(self, conn): try: if conn.alt_id and conn.alt_id in self.conns_by_id: del self.conns_by_id[conn.alt_id] if conn in self.conns: self.conns.remove(conn) if conn in self.idle: self.idle.remove(conn) for tid in self.tunnels.keys(): if conn in self.tunnels[tid]: self.tunnels[tid].remove(conn) if not self.tunnels[tid]: del self.tunnels[tid] except ValueError: # Let's not asplode if another thread races us for this. pass def Readable(self): # FIXME: This is O(n) now = time.time() return [s.fd for s in self.conns if (s.fd and (not s.read_eof) and (s.throttle_until <= now))] def Blocked(self): # FIXME: This is O(n) return [s.fd for s in self.conns if s.fd and len(s.write_blocked) > 0] def DeadConns(self): return [s for s in self.conns if s.read_eof and s.write_eof and not s.write_blocked] def CleanFds(self): evil = [] for s in self.conns: try: i, o, e = select.select([s.fd], [s.fd], [s.fd], 0) except Exception: evil.append(s) for s in evil: LogDebug('Removing broken Selectable: %s' % s) self.Remove(s) def Connection(self, fd): for conn in self.conns: if conn.fd == fd: return conn return None def TunnelServers(self): servers = {} for tid in self.tunnels: for tunnel in self.tunnels[tid]: server = tunnel.server_info[tunnel.S_NAME] if server is not None: servers[server] = 1 return servers.keys() def Tunnel(self, proto, domain, conn=None): tid = '%s:%s' % (proto, domain) if conn is not None: if tid not in self.tunnels: self.tunnels[tid] = [] self.tunnels[tid].append(conn) if tid in self.tunnels: return self.tunnels[tid] else: try: dparts = domain.split('.')[1:] while len(dparts) > 1: wild_tid = '%s:*.%s' % (proto, '.'.join(dparts)) if wild_tid in self.tunnels: return self.tunnels[wild_tid] dparts = dparts[1:] except: pass return [] class LineParser(Selectable): \"\"\"A Selectable which parses the input as lines of text.\"\"\" def __init__(self, fd=None, address=None, on_port=None, ui=None, tracked=True): Selectable.__init__(self, fd, address, on_port, ui=ui, tracked=tracked) self.leftovers = '' def __html__(self): return Selectable.__html__(self) def Cleanup(self, close=True): Selectable.Cleanup(self, close=close) self.leftovers = '' def ProcessData(self, data): lines = (self.leftovers+data).splitlines(True) self.leftovers = '' while lines: line = lines.pop(0) if line.endswith('\\n'): if self.ProcessLine(line, lines) is False: return False else: if not self.peeking: self.leftovers += line if self.read_eof: return self.ProcessEofRead() return True def ProcessLine(self, line, lines): self.LogError('LineParser::ProcessLine: Should be overridden!') return False TLS_CLIENTHELLO = '%c' % 026 SSL_CLIENTHELLO = '\\x80' # FIXME: XMPP support class MagicProtocolParser(LineParser): \"\"\"A Selectable which recognizes HTTP, TLS or XMPP preambles.\"\"\" def __init__(self, fd=None, address=None, on_port=None, ui=None): LineParser.__init__(self, fd, address, on_port, ui=ui, tracked=False) self.leftovers = '' self.might_be_tls = True self.is_tls = False self.my_tls = False def __html__(self): return ('Detected TLS: %s
    ' '%s') % (self.is_tls, LineParser.__html__(self)) # FIXME: DEPRECATE: Make this all go away, switch to CONNECT. def ProcessMagic(self, data): args = {} try: prefix, words, data = data.split('\\r\\n', 2) for arg in words.split('; '): key, val = arg.split('=', 1) args[key] = val self.EatPeeked(eat_bytes=len(prefix)+2+len(words)+2) except ValueError, e: return True try: port = 'port' in args and args['port'] or None if port: self.on_port = int(port) except ValueError, e: return False proto = 'proto' in args and args['proto'] or None if proto in ('http', 'http2', 'http3', 'websocket'): return LineParser.ProcessData(self, data) domain = 'domain' in args and args['domain'] or None if proto == 'https': return self.ProcessTls(data, domain) if proto == 'raw' and domain: return self.ProcessRaw(data, domain) return False def ProcessData(self, data): if data.startswith(MAGIC_PREFIX): return self.ProcessMagic(data) if self.might_be_tls: self.might_be_tls = False if not data.startswith(TLS_CLIENTHELLO) and not data.startswith(SSL_CLIENTHELLO): self.EatPeeked() return LineParser.ProcessData(self, data) self.is_tls = True if self.is_tls: return self.ProcessTls(data) else: self.EatPeeked() return LineParser.ProcessData(self, data) def GetMsg(self, data): mtype, ml24, mlen = struct.unpack('>BBH', data[0:4]) mlen += ml24 * 0x10000 return mtype, data[4:4+mlen], data[4+mlen:] def GetClientHelloExtensions(self, msg): # Ugh, so many magic numbers! These are accumulated sizes of # the different fields we are ignoring in the TLS headers. slen = struct.unpack('>B', msg[34])[0] cslen = struct.unpack('>H', msg[35+slen:37+slen])[0] cmlen = struct.unpack('>B', msg[37+slen+cslen])[0] extofs = 34+1+2+1+2+slen+cslen+cmlen if extofs < len(msg): return msg[extofs:] return None def GetSniNames(self, extensions): names = [] while extensions: etype, elen = struct.unpack('>HH', extensions[0:4]) if etype == 0: # OK, we found an SNI extension, get the list. namelist = extensions[6:4+elen] while namelist: ntype, nlen = struct.unpack('>BH', namelist[0:3]) if ntype == 0: names.append(namelist[3:3+nlen].lower()) namelist = namelist[3+nlen:] extensions = extensions[4+elen:] return names def GetSni(self, data): hello, vmajor, vminor, mlen = struct.unpack('>BBBH', data[0:5]) data = data[5:] sni = [] while data: mtype, msg, data = self.GetMsg(data) if mtype == 1: # ClientHello! sni.extend(self.GetSniNames(self.GetClientHelloExtensions(msg))) return sni def ProcessTls(self, data, domain=None): self.LogError('TlsOrLineParser::ProcessTls: Should be overridden!') return False def ProcessRaw(self, data, domain): self.LogError('TlsOrLineParser::ProcessRaw: Should be overridden!') return False class ChunkParser(Selectable): \"\"\"A Selectable which parses the input as chunks.\"\"\" def __init__(self, fd=None, address=None, on_port=None, ui=None): Selectable.__init__(self, fd, address, on_port, ui=ui) self.want_cbytes = 0 self.want_bytes = 0 self.compressed = False self.header = '' self.chunk = '' self.zr = zlib.decompressobj() def __html__(self): return Selectable.__html__(self) def Cleanup(self, close=True): Selectable.Cleanup(self, close=close) self.zr = self.chunk = self.header = None def ProcessData(self, data): if self.peeking: self.want_cbytes = 0 self.want_bytes = 0 self.header = '' self.chunk = '' if self.want_bytes == 0: self.header += data if self.header.find('\\r\\n') < 0: if self.read_eof: return self.ProcessEofRead() return True try: size, data = self.header.split('\\r\\n', 1) self.header = '' if size.endswith('R'): self.zr = zlib.decompressobj() size = size[0:-1] if 'Z' in size: csize, zsize = size.split('Z') self.compressed = True self.want_cbytes = int(csize, 16) self.want_bytes = int(zsize, 16) else: self.compressed = False self.want_bytes = int(size, 16) except ValueError, err: self.LogError('ChunkParser::ProcessData: %s' % err) self.Log([('bad_data', data)]) return False if self.want_bytes == 0: return False process = data[:self.want_bytes] leftover = data[self.want_bytes:] self.chunk += process self.want_bytes -= len(process) result = 1 if self.want_bytes == 0: if self.compressed: try: cchunk = self.zr.decompress(self.chunk) except zlib.error: cchunk = '' if len(cchunk) != self.want_cbytes: result = self.ProcessCorruptChunk(self.chunk) else: result = self.ProcessChunk(cchunk) else: result = self.ProcessChunk(self.chunk) self.chunk = '' if result and leftover: # FIXME: This blows the stack from time to time. We need a loop # or better yet, to just process more in a subsequent # iteration of the main select() loop. result = self.ProcessData(leftover) if self.read_eof: result = self.ProcessEofRead() and result return result def ProcessCorruptChunk(self, chunk): self.LogError('ChunkParser::ProcessData: ProcessCorruptChunk not overridden!') return False def ProcessChunk(self, chunk): self.LogError('ChunkParser::ProcessData: ProcessChunk not overridden!') return False class Tunnel(ChunkParser): \"\"\"A Selectable representing a PageKite tunnel.\"\"\" S_NAME = 0 S_PORTS = 1 S_RAW_PORTS = 2 S_PROTOS = 3 def __init__(self, conns): ChunkParser.__init__(self, ui=conns.config.ui) # We want to be sure to read the entire chunk at once, including # headers to save cycles, so we double the size we're willing to # read here. self.maxread *= 2 self.server_info = ['x.x.x.x:x', [], [], []] self.conns = conns self.users = {} self.remote_ssl = {} self.zhistory = {} self.backends = {} self.rtt = 100000 self.last_ping = 0 self.using_tls = False def __html__(self): return ('Server name: %s
    ' '%s') % (self.server_info[self.S_NAME], ChunkParser.__html__(self)) def _FrontEnd(conn, body, conns): \"\"\"This is what the front-end does when a back-end requests a new tunnel.\"\"\" self = Tunnel(conns) requests = [] try: for prefix in ('X-Beanstalk', 'X-PageKite'): for feature in conn.parser.Header(prefix+'-Features'): if not conns.config.disable_zchunks: if feature == 'ZChunks': self.EnableZChunks(level=1) # Track which versions we see in the wild. version = 'old' for v in conn.parser.Header(prefix+'-Version'): version = v global gYamon gYamon.vadd('version-%s' % version, 1, wrap=10000000) for replace in conn.parser.Header(prefix+'-Replace'): if replace in self.conns.conns_by_id: repl = self.conns.conns_by_id[replace] self.LogInfo('Disconnecting old tunnel: %s' % repl) self.conns.Remove(repl) repl.Cleanup() for bs in conn.parser.Header(prefix): # X-Beanstalk: proto:my.domain.com:token:signature proto, domain, srand, token, sign = bs.split(':') requests.append((proto.lower(), domain.lower(), srand, token, sign, prefix)) except Exception, err: self.LogError('Discarding connection: %s' % err) self.Cleanup() return None except socket.error, err: self.LogInfo('Discarding connection: %s' % err) self.Cleanup() return None self.last_activity = time.time() self.CountAs('backends_live') self.SetConn(conn) conns.auth.check(requests[:], conn, lambda r: self.AuthCallback(conn, r)) return self def RecheckQuota(self, conns, when=None): if when is None: when = time.time() if (self.quota and self.quota[0] is not None and self.quota[1] and (self.quota[2] < when-900)): self.quota[2] = when LogDebug('Rechecking: %s' % (self.quota, )) conns.auth.check([self.quota[1]], self, lambda r: self.QuotaCallback(conns, r)) def QuotaCallback(self, conns, results): # Report new values to the back-end... if self.quota and (self.quota[0] >= 0): self.SendQuota() for r in results: if r[0] in ('X-PageKite-OK', 'X-PageKite-Duplicate'): return self self.LogInfo('Ran out of quota or account deleted, closing tunnel.') conns.Remove(self) self.Cleanup() return None def AuthCallback(self, conn, results): output = [HTTP_ResponseHeader(200, 'OK'), HTTP_Header('Transfer-Encoding', 'chunked'), HTTP_Header('X-PageKite-Protos', ', '.join(['%s' % p for p in self.conns.config.server_protos])), HTTP_Header('X-PageKite-Ports', ', '.join( ['%s' % self.conns.config.server_portalias.get(p, p) for p in self.conns.config.server_ports]))] if not self.conns.config.disable_zchunks: output.append(HTTP_Header('X-PageKite-Features', 'ZChunks')) if self.conns.config.server_raw_ports: output.append( HTTP_Header('X-PageKite-Raw-Ports', ', '.join(['%s' % p for p in self.conns.config.server_raw_ports]))) ok = {} for r in results: if r[0] in ('X-PageKite-OK', 'X-Beanstalk-OK'): ok[r[1]] = 1 if r[0] == 'X-PageKite-SessionID': self.alt_id = r[1] output.append('%s: %s\\r\\n' % r) output.append(HTTP_StartBody()) if not self.Send(output, try_flush=True): conn.LogDebug('No tunnels configured, closing connection (send failed).') self.Cleanup() return None self.backends = ok.keys() if self.backends: for backend in self.backends: proto, domain, srand = backend.split(':') self.Log([('BE', 'Live'), ('proto', proto), ('domain', domain)]) self.conns.Tunnel(proto, domain, self) if conn.quota: self.quota = conn.quota self.Log([('BE', 'Live'), ('quota', self.quota[0])]) self.conns.Add(self, alt_id=self.alt_id) return self else: conn.LogDebug('No tunnels configured, closing connection.') self.Cleanup() return None def _RecvHttpHeaders(self, fd=None): data = '' fd = fd or self.fd while not data.endswith('\\r\\n\\r\\n') and not data.endswith('\\n\\n'): try: buf = fd.recv(1) except: # This is sloppy, but the back-end will just connect somewhere else # instead, so laziness here should be fine. buf = None if buf is None or buf == '': LogDebug('Remote end closed connection.') return None data += buf self.read_bytes += len(buf) if DEBUG_IO: print '<== IN (headers)\\n%s\\n===' % data return data def _Connect(self, server, conns, tokens=None): if self.fd: self.fd.close() sspec = server.split(':') if len(sspec) < 2: sspec = (sspec[0], 443) # Use chained SocksiPy to secure our communication. socks.DEBUG = (DEBUG_IO or socks.DEBUG) and LogDebug sock = socks.socksocket() if socks.HAVE_SSL: chain = ['default'] if self.conns.config.fe_anon_tls_wrap: chain.append('ssl-anon:%s:%s' % (sspec[0], sspec[1])) if self.conns.config.fe_certname: chain.append('http:%s:%s' % (sspec[0], sspec[1])) chain.append('ssl:%s:443' % ','.join(self.conns.config.fe_certname)) for hop in chain: sock.addproxy(*socks.parseproxy(hop)) self.SetFD(sock) try: self.fd.settimeout(20.0) # Missing in Python 2.2 except Exception: self.fd.setblocking(1) self.fd.connect((sspec[0], int(sspec[1]))) replace_sessionid = self.conns.config.servers_sessionids.get(server, None) if (not self.Send(HTTP_PageKiteRequest(server, conns.config.backends, tokens, nozchunks=conns.config.disable_zchunks, replace=replace_sessionid), try_flush=True) or not self.Flush(wait=True)): return None, None data = self._RecvHttpHeaders() if not data: return None, None self.fd.setblocking(0) parse = HttpLineParser(lines=data.splitlines(), state=HttpLineParser.IN_RESPONSE) return data, parse def _BackEnd(server, backends, require_all, conns): \"\"\"This is the back-end end of a tunnel.\"\"\" self = Tunnel(conns) self.backends = backends self.require_all = require_all self.server_info[self.S_NAME] = server abort = True try: begin = time.time() data, parse = self._Connect(server, conns) if data and parse: # Collect info about front-end capabilities, for interactive config for portlist in parse.Header('X-PageKite-Ports'): self.server_info[self.S_PORTS].extend(portlist.split(', ')) for portlist in parse.Header('X-PageKite-Raw-Ports'): self.server_info[self.S_RAW_PORTS].extend(portlist.split(', ')) for protolist in parse.Header('X-PageKite-Protos'): self.server_info[self.S_PROTOS].extend(protolist.split(', ')) for sessionid in parse.Header('X-PageKite-SessionID'): self.alt_id = sessionid conns.config.servers_sessionids[server] = sessionid tryagain = False tokens = {} for request in parse.Header('X-PageKite-SignThis'): proto, domain, srand, token = request.split(':') tokens['%s:%s' % (proto, domain)] = token tryagain = True if tryagain: begin = time.time() data, parse = self._Connect(server, conns, tokens) if data and parse: sname = self.server_info[self.S_NAME] conns.config.ui.NotifyServer(self, self.server_info) for misc in parse.Header('X-PageKite-Misc'): args = parse_qs(misc) logdata = [('FE', sname)] for arg in args: logdata.append((arg, args[arg][0])) Log(logdata) if 'motd' in args and args['motd'][0]: conns.config.ui.NotifyMOTD(sname, args['motd'][0]) for quota in parse.Header('X-PageKite-Quota'): self.quota = [int(quota), None, None] self.Log([('FE', sname), ('quota', quota)]) conns.config.ui.NotifyQuota(float(quota)) invalid_reasons = {} for request in parse.Header('X-PageKite-Invalid-Why'): # This is future-compatible, in that we can add more fields later. details = request.split(';') invalid_reasons[details[0]] = details[1] for request in parse.Header('X-PageKite-Invalid'): proto, domain, srand = request.split(':') reason = invalid_reasons.get(request, 'unknown') self.Log([('FE', sname), ('err', 'Rejected'), ('proto', proto), ('reason', reason), ('domain', domain)]) conns.config.ui.NotifyKiteRejected(proto, domain, reason, crit=True) conns.config.SetBackendStatus(domain, proto, add=BE_STATUS_ERR_TUNNEL) for request in parse.Header('X-PageKite-Duplicate'): abort = True proto, domain, srand = request.split(':') self.Log([('FE', self.server_info[self.S_NAME]), ('err', 'Duplicate'), ('proto', proto), ('domain', domain)]) conns.config.ui.NotifyKiteRejected(proto, domain, 'duplicate') conns.config.SetBackendStatus(domain, proto, add=BE_STATUS_ERR_TUNNEL) if not conns.config.disable_zchunks: for feature in parse.Header('X-PageKite-Features'): if feature == 'ZChunks': self.EnableZChunks(level=9) ssl_available = {} for request in parse.Header('X-PageKite-SSL-OK'): ssl_available[request] = True for request in parse.Header('X-PageKite-OK'): abort = False proto, domain, srand = request.split(':') conns.Tunnel(proto, domain, self) status = BE_STATUS_OK if request in ssl_available: status |= BE_STATUS_REMOTE_SSL self.remote_ssl[(proto, domain)] = True self.Log([('FE', sname), ('proto', proto), ('domain', domain), ('ssl', (request in ssl_available))]) conns.config.SetBackendStatus(domain, proto, add=status) self.rtt = (time.time() - begin) except socket.error, e: self.Cleanup() return None except Exception, e: self.LogError('Server response parsing failed: %s' % e) self.Cleanup() return None if abort: return None conns.Add(self) self.CountAs('frontends_live') self.last_activity = time.time() return self FrontEnd = staticmethod(_FrontEnd) BackEnd = staticmethod(_BackEnd) def SendData(self, conn, data, sid=None, host=None, proto=None, port=None, chunk_headers=None): sid = int(sid or conn.sid) if conn: self.users[sid] = conn if not sid in self.zhistory: self.zhistory[sid] = [0, 0] sending = ['SID: %s\\r\\n' % sid] if proto: sending.append('Proto: %s\\r\\n' % proto) if host: sending.append('Host: %s\\r\\n' % host) if port: porti = int(port) if porti in self.conns.config.server_portalias: sending.append('Port: %s\\r\\n' % self.conns.config.server_portalias[porti]) else: sending.append('Port: %s\\r\\n' % port) if chunk_headers: for ch in chunk_headers: sending.append('%s: %s\\r\\n' % ch) sending.append('\\r\\n') sending.append(data) return self.SendChunked(sending, zhistory=self.zhistory[sid]) def SendStreamEof(self, sid, write_eof=False, read_eof=False): return self.SendChunked('SID: %s\\r\\nEOF: 1%s%s\\r\\n\\r\\nBye!' % (sid, (write_eof or not read_eof) and 'W' or '', (read_eof or not write_eof) and 'R' or '')) def EofStream(self, sid, eof_type='WR'): if sid in self.users and self.users[sid] is not None: write_eof = (-1 != eof_type.find('W')) read_eof = (-1 != eof_type.find('R')) self.users[sid].ProcessTunnelEof(read_eof=(read_eof or not write_eof), write_eof=(write_eof or not read_eof)) def CloseStream(self, sid, stream_closed=False): if sid in self.users: stream = self.users[sid] del self.users[sid] if not stream_closed and stream is not None: stream.CloseTunnel(tunnel_closed=True) if sid in self.zhistory: del self.zhistory[sid] def Cleanup(self, close=True): if self.users: for sid in self.users.keys(): self.CloseStream(sid) ChunkParser.Cleanup(self, close=close) self.conns = None self.users = self.zhistory = self.backends = {} def ResetRemoteZChunks(self): return self.SendChunked('NOOP: 1\\r\\nZRST: 1\\r\\n\\r\\n!', compress=False) def SendPing(self): self.last_ping = int(time.time()) self.LogDebug(\"Ping\", [('host', self.server_info[self.S_NAME])]) return self.SendChunked('NOOP: 1\\r\\nPING: 1\\r\\n\\r\\n!', compress=False) def SendPong(self): return self.SendChunked('NOOP: 1\\r\\n\\r\\n!', compress=False) def SendQuota(self): return self.SendChunked('NOOP: 1\\r\\nQuota: %s\\r\\n\\r\\n!' % self.quota[0], compress=False) def SendThrottle(self, sid, write_speed): return self.SendChunked('NOOP: 1\\r\\nSID: %s\\r\\nSPD: %d\\r\\n\\r\\n!' % ( sid, write_speed), compress=False) def ProcessCorruptChunk(self, data): self.ResetRemoteZChunks() return True def Probe(self, host): for bid in self.conns.config.backends: be = self.conns.config.backends[bid] if be[BE_DOMAIN] == host: bhost, bport = (be[BE_BHOST], be[BE_BPORT]) # FIXME: Should vary probe by backend type if self.conns.config.Ping(bhost, int(bport)) > 2: return False return True def Throttle(self, parse): try: sid = int(parse.Header('SID')[0]) bps = int(parse.Header('SPD')[0]) if sid in self.users: self.users[sid].Throttle(bps, remote=True) except Exception, e: LogError('Tunnel::ProcessChunk: Invalid throttle request!') return True # If a tunnel goes down, we just go down hard and kill all our connections. def ProcessEofRead(self): if self.conns: self.conns.Remove(self) self.Cleanup() return True def ProcessEofWrite(self): return self.ProcessEofRead() def ProcessChunk(self, data): try: headers, data = data.split('\\r\\n\\r\\n', 1) parse = HttpLineParser(lines=headers.splitlines(), state=HttpLineParser.IN_HEADERS) except ValueError: LogError('Tunnel::ProcessChunk: Corrupt packet!') return False try: if parse.Header('Quota'): if self.quota: self.quota[0] = int(parse.Header('Quota')[0]) else: self.quota = [int(parse.Header('Quota')[0]), None, None] self.conns.config.ui.Notify(('You have %.2f MB of quota left.' ) % (float(self.quota[0]) / 1024), color=self.conns.config.ui.MAGENTA) if parse.Header('PING'): return self.SendPong() if parse.Header('ZRST') and not self.ResetZChunks(): return False if parse.Header('SPD') and not self.Throttle(parse): return False if parse.Header('NOOP'): return True except Exception, e: LogError('Tunnel::ProcessChunk: Corrupt chunk: %s' % e) return False proto = conn = sid = None try: sid = int(parse.Header('SID')[0]) eof = parse.Header('EOF') except IndexError, e: LogError('Tunnel::ProcessChunk: Corrupt packet!') return False if eof: self.EofStream(sid, eof[0]) else: if sid in self.users: conn = self.users[sid] else: proto = (parse.Header('Proto') or [''])[0].lower() port = (parse.Header('Port') or [''])[0].lower() host = (parse.Header('Host') or [''])[0].lower() rIp = (parse.Header('RIP') or [''])[0].lower() rPort = (parse.Header('RPort') or [''])[0].lower() rTLS = (parse.Header('RTLS') or [''])[0].lower() if proto and host: # FIXME: # if proto == 'https': # if host in self.conns.config.tls_endpoints: # print 'Should unwrap SSL from %s' % host if proto == 'probe': if self.conns.config.no_probes: LogDebug('Responding to probe for %s: rejected' % host) if not self.SendChunked('SID: %s\\r\\n\\r\\n%s' % ( sid, HTTP_NoFeConnection() )): return False elif self.Probe(host): LogDebug('Responding to probe for %s: good' % host) if not self.SendChunked('SID: %s\\r\\n\\r\\n%s' % ( sid, HTTP_GoodBeConnection() )): return False else: LogDebug('Responding to probe for %s: back-end down' % host) if not self.SendChunked('SID: %s\\r\\n\\r\\n%s' % ( sid, HTTP_NoBeConnection() )): return False else: conn = UserConn.BackEnd(proto, host, sid, self, port, remote_ip=rIp, remote_port=rPort, data=data) if proto in ('http', 'http2', 'http3', 'websocket'): if conn is None: if not self.SendChunked('SID: %s\\r\\n\\r\\n%s' % (sid, HTTP_Unavailable('be', proto, host, frame_url=self.conns.config.error_url))): return False elif not conn: if not self.SendChunked('SID: %s\\r\\n\\r\\n%s' % (sid, HTTP_Unavailable('be', proto, host, frame_url=self.conns.config.error_url, code=401))): return False elif rIp: add_headers = ('\\nX-Forwarded-For: %s\\r\\n' 'X-PageKite-Port: %s\\r\\n' 'X-PageKite-Proto: %s\\r\\n' ) % (rIp, port, # FIXME: Checking for port == 443 is wrong! ((rTLS or (int(port) == 443)) and 'https' or 'http')) rewritehost = conn.config.get('rewritehost', False) if rewritehost: if rewritehost is True: rewritehost = conn.backend[BE_BHOST] for hdr in ('host', 'connection', 'keep-alive'): data = re.sub(r'(?mi)^'+hdr, 'X-Old-'+hdr, data) add_headers += ('Connection: close\\r\\n' 'Host: %s\\r\\n') % rewritehost req, rest = re.sub(r'(?mi)^x-forwarded-for', 'X-Old-Forwarded-For', data).split('\\n', 1) data = ''.join([req, add_headers, rest]) elif proto == 'httpfinger': # Rewrite a finger request to HTTP. try: firstline, rest = data.split('\\n', 1) if conn.config.get('rewritehost', False): rewritehost = conn.backend[BE_BHOST] else: rewritehost = host if '%s' in self.conns.config.finger_path: args = (firstline.strip(), rIp, rewritehost, rest) else: args = (rIp, rewritehost, rest) data = ('GET '+self.conns.config.finger_path+' HTTP/1.1\\r\\n' 'X-Forwarded-For: %s\\r\\n' 'Connection: close\\r\\n' 'Host: %s\\r\\n\\r\\n%s') % args except Exception, e: self.LogError('Error formatting HTTP-Finger: %s' % e) conn = None if conn: self.users[sid] = conn if proto == 'httpfinger': conn.fd.setblocking(1) conn.Send(data, try_flush=True) or conn.Flush(wait=True) self._RecvHttpHeaders(fd=conn.fd) conn.fd.setblocking(0) data = '' if not conn: self.CloseStream(sid) if not self.SendStreamEof(sid): return False else: if not conn.Send(data, try_flush=True): # FIXME pass if len(conn.write_blocked) > 2*max(conn.write_speed, 50000): if conn.created < time.time()-3: if not self.SendThrottle(sid, conn.write_speed): return False return True class LoopbackTunnel(Tunnel): \"\"\"A Tunnel which just loops back to this process.\"\"\" def __init__(self, conns, which, backends): Tunnel.__init__(self, conns) self.backends = backends self.require_all = True self.server_info[self.S_NAME] = LOOPBACK[which] self.other_end = None if which == 'FE': for d in backends.keys(): if backends[d][BE_BHOST]: proto, domain = d.split(':') self.conns.Tunnel(proto, domain, self) self.Log([('FE', self.server_info[self.S_NAME]), ('proto', proto), ('domain', domain)]) def Cleanup(self, close=True): Tunnel.Cleanup(self, close=close) other = self.other_end self.other_end = None if other and other.other_end: other.Cleanup() def Linkup(self, other): self.other_end = other other.other_end = self def _Loop(conns, backends): return LoopbackTunnel(conns, 'FE', backends ).Linkup(LoopbackTunnel(conns, 'BE', backends)) Loop = staticmethod(_Loop) def Send(self, data): return self.other_end.ProcessData(''.join(data)) class UserConn(Selectable): \"\"\"A Selectable representing a user's connection.\"\"\" def __init__(self, address, ui=None): Selectable.__init__(self, address=address, ui=ui) self.tunnel = None self.conns = None self.backend = BE_NONE[:] self.config = {} # UserConn objects are considered active immediately self.last_activity = time.time() def __html__(self): return ('Tunnel: %s
    ' '%s') % (self.tunnel and self.tunnel.sid or '', escape_html('%s' % (self.tunnel or '')), Selectable.__html__(self)) def CloseTunnel(self, tunnel_closed=False): tunnel = self.tunnel self.tunnel = None if tunnel and not tunnel_closed: if not self.read_eof or not self.write_eof: tunnel.SendStreamEof(self.sid, write_eof=True, read_eof=True) tunnel.CloseStream(self.sid, stream_closed=True) self.ProcessTunnelEof(read_eof=True, write_eof=True) def Cleanup(self, close=True): if close: self.CloseTunnel() Selectable.Cleanup(self, close=close) if self.conns: self.conns.Remove(self) self.backend = self.config = self.conns = None def _FrontEnd(conn, address, proto, host, on_port, body, conns): # This is when an external user connects to a server and requests a # web-page. We have to give it to them! self = UserConn(address, ui=conns.config.ui) self.conns = conns self.SetConn(conn) if ':' in host: host, port = host.split(':', 1) self.proto = proto self.host = host # If the listening port is an alias for another... if int(on_port) in conns.config.server_portalias: on_port = conns.config.server_portalias[int(on_port)] # Try and find the right tunnel. We prefer proto/port specifications first, # then the just the proto. If the protocol is WebSocket and no tunnel is # found, look for a plain HTTP tunnel. if proto == 'probe': protos = ['http', 'https', 'websocket', 'raw', 'irc', 'finger', 'httpfinger'] ports = conns.config.server_ports[:] ports.extend(conns.config.server_aliasport.keys()) ports.extend([x for x in conns.config.server_raw_ports if x != VIRTUAL_PN]) else: protos = [proto] ports = [on_port] if proto == 'websocket': protos.append('http') elif proto == 'http': protos.extend(['http2', 'http3']) tunnels = None for p in protos: for prt in ports: if not tunnels: tunnels = conns.Tunnel('%s-%s' % (p, prt), host) if not tunnels: tunnels = conns.Tunnel(p, host) if not tunnels: tunnels = conns.Tunnel(protos[0], CATCHALL_HN) if self.address: chunk_headers = [('RIP', self.address[0]), ('RPort', self.address[1])] if conn.my_tls: chunk_headers.append(('RTLS', 1)) if tunnels: self.tunnel = tunnels[0] if (self.tunnel and self.tunnel.SendData(self, ''.join(body), host=host, proto=proto, port=on_port, chunk_headers=chunk_headers) and self.conns): self.Log([('domain', self.host), ('on_port', on_port), ('proto', self.proto), ('is', 'FE')]) self.conns.Add(self) if proto.startswith('http'): self.conns.TrackIP(address[0], host) # FIXME: Use the tracked data to detect & mitigate abuse? return self else: self.LogDebug('No back-end', [('on_port', on_port), ('proto', self.proto), ('domain', self.host), ('is', 'FE')]) self.Cleanup(close=False) return None def _BackEnd(proto, host, sid, tunnel, on_port, remote_ip=None, remote_port=None, data=None): # This is when we open a backend connection, because a user asked for it. self = UserConn(None, ui=tunnel.conns.config.ui) self.sid = sid self.proto = proto self.host = host self.conns = tunnel.conns self.tunnel = tunnel failure = None # Try and find the right back-end. We prefer proto/port specifications # first, then the just the proto. If the protocol is WebSocket and no # tunnel is found, look for a plain HTTP tunnel. Fallback hosts can # be registered using the http2/3/4 protocols. backend = None if proto == 'http': protos = [proto, 'http2', 'http3'] elif proto == 'probe': protos = ['http', 'http2', 'http3'] elif proto == 'websocket': protos = [proto, 'http', 'http2', 'http3'] else: protos = [proto] for p in protos: if not backend: backend, be = self.conns.config.GetBackendServer('%s-%s' % (p, on_port), host) if not backend: backend, be = self.conns.config.GetBackendServer(p, host) if not backend: backend, be = self.conns.config.GetBackendServer(p, CATCHALL_HN) logInfo = [ ('on_port', on_port), ('proto', proto), ('domain', host), ('is', 'BE') ] if remote_ip: logInfo.append(('remote_ip', remote_ip)) # Strip off useless IPv6 prefix, if this is an IPv4 address. if remote_ip.startswith('::ffff:') and ':' not in remote_ip[7:]: remote_ip = remote_ip[7:] if not backend or not backend[0]: self.ui.Notify(('%s - %s://%s:%s (FAIL: no server)' ) % (remote_ip or 'unknown', proto, host, on_port), prefix='?', color=self.ui.YELLOW) else: http_host = '%s/%s' % (be[BE_DOMAIN], be[BE_PORT] or '80') self.backend = be self.config = host_config = self.conns.config.be_config.get(http_host, {}) # Access control interception: check remote IP addresses first. ip_keys = [k for k in host_config if k.startswith('ip/')] if ip_keys: k1 = 'ip/%s' % remote_ip k2 = '.'.join(k1.split('.')[:-1]) if not (k1 in host_config or k2 in host_config): self.ui.Notify(('%s - %s://%s:%s (IP ACCESS DENIED)' ) % (remote_ip or 'unknown', proto, host, on_port), prefix='!', color=self.ui.YELLOW) logInfo.append(('forbidden-ip', '%s' % remote_ip)) backend = None # Access control interception: check for HTTP Basic authentication. user_keys = [k for k in host_config if k.startswith('password/')] if user_keys: user, pwd, fail = None, None, True if proto in ('websocket', 'http', 'http2', 'http3'): parse = HttpLineParser(lines=data.splitlines()) auth = parse.Header('Authorization') try: (how, ab64) = auth[0].strip().split() if how.lower() == 'basic': user, pwd = base64.decodestring(ab64).split(':') except: user = auth user_key = 'password/%s' % user if user and user_key in host_config: if host_config[user_key] == pwd: fail = False if fail: if DEBUG_IO: print '=== REQUEST\\n%s\\n===' % data self.ui.Notify(('%s - %s://%s:%s (USER ACCESS DENIED)' ) % (remote_ip or 'unknown', proto, host, on_port), prefix='!', color=self.ui.YELLOW) logInfo.append(('forbidden-user', '%s' % user)) backend = None failure = '' if not backend: logInfo.append(('err', 'No back-end')) self.Log(logInfo) self.Cleanup(close=False) return failure try: self.SetFD(rawsocket(socket.AF_INET, socket.SOCK_STREAM)) try: self.fd.settimeout(2.0) # Missing in Python 2.2 except Exception: self.fd.setblocking(1) sspec = list(backend) if len(sspec) == 1: sspec.append(80) self.fd.connect(tuple(sspec)) self.fd.setblocking(0) except socket.error, err: logInfo.append(('socket_error', '%s' % err)) self.ui.Notify(('%s - %s://%s:%s (FAIL: %s:%s is down)' ) % (remote_ip or 'unknown', proto, host, on_port, sspec[0], sspec[1]), prefix='!', color=self.ui.YELLOW) self.Log(logInfo) self.Cleanup(close=False) return None sspec = (sspec[0], sspec[1]) be_name = (sspec == self.conns.config.ui_sspec) and 'builtin' or ('%s:%s' % sspec) self.ui.Status('serving') self.ui.Notify(('%s < %s://%s:%s (%s)' ) % (remote_ip or 'unknown', proto, host, on_port, be_name)) self.Log(logInfo) self.conns.Add(self) return self FrontEnd = staticmethod(_FrontEnd) BackEnd = staticmethod(_BackEnd) def Shutdown(self, direction): try: if self.fd: if 'sock_shutdown' in dir(self.fd): # This is a pyOpenSSL socket, which has incompatible shutdown. if direction == socket.SHUT_RD: self.fd.shutdown() else: self.fd.sock_shutdown(direction) else: self.fd.shutdown(direction) except Exception, e: pass def ProcessTunnelEof(self, read_eof=False, write_eof=False): if read_eof and not self.write_eof: self.ProcessEofWrite(tell_tunnel=False) if write_eof and not self.read_eof: self.ProcessEofRead(tell_tunnel=False) return True def ProcessEofRead(self, tell_tunnel=True): self.read_eof = True self.Shutdown(socket.SHUT_RD) if tell_tunnel and self.tunnel: self.tunnel.SendStreamEof(self.sid, read_eof=True) return self.ProcessEof() def ProcessEofWrite(self, tell_tunnel=True): self.write_eof = True if not self.write_blocked: self.Shutdown(socket.SHUT_WR) if tell_tunnel and self.tunnel: self.tunnel.SendStreamEof(self.sid, write_eof=True) return self.ProcessEof() def Send(self, data, try_flush=False): rv = Selectable.Send(self, data, try_flush=try_flush) if self.write_eof and not self.write_blocked: self.Shutdown(socket.SHUT_WR) return rv def ProcessData(self, data): if not self.tunnel: self.LogError('No tunnel! %s' % self) return False if not self.tunnel.SendData(self, data): self.LogDebug('Send to tunnel failed') return False # Back off if tunnel is stuffed. if self.tunnel and len(self.tunnel.write_blocked) > 1024000: self.Throttle(delay=(len(self.tunnel.write_blocked)-204800)/max(50000, self.tunnel.write_speed)) if self.read_eof: return self.ProcessEofRead() return True class UnknownConn(MagicProtocolParser): \"\"\"This class is a connection which we're not sure what is yet.\"\"\" def __init__(self, fd, address, on_port, conns): MagicProtocolParser.__init__(self, fd, address, on_port, ui=conns.config.ui) self.peeking = True # Set up our parser chain. self.parsers = [HttpLineParser] if IrcLineParser.PROTO in conns.config.server_protos: self.parsers.append(IrcLineParser) if FingerLineParser.PROTO in conns.config.server_protos: self.parsers.append(FingerLineParser) self.parser = MagicLineParser(parsers=self.parsers) self.conns = conns self.conns.Add(self) self.sid = -1 self.host = None self.proto = None self.said_hello = False def Cleanup(self, close=True): if self.conns: self.conns.Remove(self) MagicProtocolParser.Cleanup(self, close=close) self.conns = self.parser = None def SayHello(self): if self.said_hello: return else: self.said_hello = True if self.on_port in (25, 125, ): # FIXME: We don't actually support SMTP yet and 125 is bogus. self.Send(['220 ready ESMTP PageKite Magic Proxy\\n'], try_flush=True) def __str__(self): return '%s (%s/%s:%s)' % (MagicProtocolParser.__str__(self), (self.proto or '?'), (self.on_port or '?'), (self.host or '?')) def ProcessEofRead(self): self.read_eof = True return self.ProcessEof() def ProcessEofWrite(self): self.read_eof = True return self.ProcessEof() def ProcessLine(self, line, lines): if not self.parser: return True if self.parser.Parse(line) is False: return False if not self.parser.ParsedOK(): return True self.parser = self.parser.last_parser if self.parser.protocol == HttpLineParser.PROTO: # HTTP has special cases, including CONNECT etc. return self.ProcessParsedHttp(line, lines) else: return self.ProcessParsedMagic(self.parser.PROTOS, line, lines) def ProcessParsedMagic(self, protos, line, lines): for proto in protos: if UserConn.FrontEnd(self, self.address, proto, self.parser.domain, self.on_port, self.parser.lines + lines, self.conns) is not None: self.Cleanup(close=False) return True self.Send([self.parser.ErrorReply(port=self.on_port)], try_flush=True) self.Cleanup() return False def ProcessParsedHttp(self, line, lines): done = False if self.parser.method == 'PING': self.Send('PONG %s\\r\\n\\r\\n' % self.parser.path) self.read_eof = self.write_eof = done = True self.fd.close() elif self.parser.method == 'CONNECT': if self.parser.path.lower().startswith('pagekite:'): if Tunnel.FrontEnd(self, lines, self.conns) is None: return False done = True else: try: connect_parser = self.parser chost, cport = connect_parser.path.split(':', 1) cport = int(cport) chost = chost.lower() sid1 = ':%s' % chost sid2 = '-%s:%s' % (cport, chost) tunnels = self.conns.tunnels # These allow explicit CONNECTs to direct http(s) or raw backends. # If no match is found, we fall through to default HTTP processing. if cport in (80, 8080): if (('http'+sid1) in tunnels) or ( ('http'+sid2) in tunnels) or ( ('http2'+sid1) in tunnels) or ( ('http2'+sid2) in tunnels) or ( ('http3'+sid1) in tunnels) or ( ('http3'+sid2) in tunnels): (self.on_port, self.host) = (cport, chost) self.parser = HttpLineParser() self.Send(HTTP_ConnectOK(), try_flush=True) return True whost = chost if '.' in whost: whost = '*.' + '.'.join(whost.split('.')[1:]) if cport == 443: if (('https'+sid1) in tunnels) or ( ('https'+sid2) in tunnels) or ( chost in self.conns.config.tls_endpoints) or ( whost in self.conns.config.tls_endpoints): (self.on_port, self.host) = (cport, chost) self.parser = HttpLineParser() self.Send(HTTP_ConnectOK(), try_flush=True) return self.ProcessTls(''.join(lines), chost) if (cport in self.conns.config.server_raw_ports or VIRTUAL_PN in self.conns.config.server_raw_ports): for raw in ('raw', 'finger'): if ((raw+sid1) in tunnels) or ((raw+sid2) in tunnels): (self.on_port, self.host) = (cport, chost) self.parser = HttpLineParser() self.Send(HTTP_ConnectOK(), try_flush=True) return self.ProcessRaw(''.join(lines), self.host) except ValueError: pass if (not done and self.parser.method == 'POST' and self.parser.path in MAGIC_PATHS): # FIXME: DEPRECATE: Make this go away! if Tunnel.FrontEnd(self, lines, self.conns) is None: return False done = True if not done: if not self.host: hosts = self.parser.Header('Host') if hosts: self.host = hosts[0].lower() else: self.Send(HTTP_Response(400, 'Bad request', ['

    400 Bad request

    ', '

    Invalid request, no Host: found.

    ', ''])) return False if self.parser.path.startswith(MAGIC_PREFIX): try: self.host = self.parser.path.split('/')[2] self.proto = 'probe' except ValueError: pass if self.proto is None: self.proto = 'http' upgrade = self.parser.Header('Upgrade') if 'websocket' in self.conns.config.server_protos: if upgrade and upgrade[0].lower() == 'websocket': self.proto = 'websocket' address = self.address if int(self.on_port) in self.conns.config.server_portalias: xfwdf = self.parser.Header('X-Forwarded-For') if xfwdf and address[0] == '127.0.0.1': address = (xfwdf[0], address[1]) done = True if UserConn.FrontEnd(self, address, self.proto, self.host, self.on_port, self.parser.lines + lines, self.conns) is None: if self.proto == 'probe': self.Send(HTTP_NoFeConnection(), try_flush=True) else: self.Send(HTTP_Unavailable('fe', self.proto, self.host, frame_url=self.conns.config.error_url), try_flush=True) return False # We are done! self.Cleanup(close=False) return True def ProcessTls(self, data, domain=None): if domain: domains = [domain] else: try: domains = self.GetSni(data) if not domains: domains = [self.conns.LastIpDomain(self.address[0]) or self.conns.config.tls_default] LogDebug('No SNI - trying: %s' % domains[0]) if not domains[0]: domains = None except Exception: # Probably insufficient data, just return True and assume we'll have # better luck on the next round. return True if domains and domains[0] is not None: if UserConn.FrontEnd(self, self.address, 'https', domains[0], self.on_port, [data], self.conns) is not None: # We are done! self.EatPeeked() self.Cleanup(close=False) return True else: # If we know how to terminate the TLS/SSL, do so! ctx = self.conns.config.GetTlsEndpointCtx(domains[0]) if ctx: self.fd = socks.SSL_Connect(ctx, self.fd, accepted=True, server_side=True) self.peeking = False self.is_tls = False self.my_tls = True return True else: return False return False def ProcessRaw(self, data, domain): if UserConn.FrontEnd(self, self.address, 'raw', domain, self.on_port, [data], self.conns) is None: return False # We are done! self.Cleanup(close=False) return True class UiConn(LineParser): STATE_PASSWORD = 0 STATE_LIVE = 1 def __init__(self, fd, address, on_port, conns): LineParser.__init__(self, fd=fd, address=address, on_port=on_port) self.state = self.STATE_PASSWORD self.conns = conns self.conns.Add(self) self.lines = [] self.qc = threading.Condition() self.challenge = sha1hex('%s%8.8x' % (globalSecret(), random.randint(0, 0x7FFFFFFD)+1)) self.expect = signToken(token=self.challenge, secret=self.conns.config.ConfigSecret(), payload=self.challenge, length=1000) LogDebug('Expecting: %s' % self.expect) self.Send('PageKite? %s\\r\\n' % self.challenge) def readline(self): self.qc.acquire() while not self.lines: self.qc.wait() line = self.lines.pop(0) self.qc.release() return line def write(self, data): self.conns.config.ui_wfile.write(data) self.Send(data) def Cleanup(self): self.conns.config.ui.wfile = self.conns.config.ui_wfile self.conns.config.ui.rfile = self.conns.config.ui_rfile self.lines = self.conns.config.ui_conn = None self.conns = None LineParser.Cleanup(self) def Disconnect(self): self.Send('Goodbye') self.Cleanup() def ProcessLine(self, line, lines): if self.state == self.STATE_LIVE: self.qc.acquire() self.lines.append(line) self.qc.notify() self.qc.release() return True elif self.state == self.STATE_PASSWORD: if line.strip() == self.expect: if self.conns.config.ui_conn: self.conns.config.ui_conn.Disconnect() self.conns.config.ui_conn = self self.conns.config.ui.wfile = self self.conns.config.ui.rfile = self self.state = self.STATE_LIVE self.Send('OK!\\r\\n') return True else: self.Send('Sorry.\\r\\n') return False else: return False class RawConn(Selectable): \"\"\"This class is a raw/timed connection.\"\"\" def __init__(self, fd, address, on_port, conns): Selectable.__init__(self, fd, address, on_port) self.my_tls = False self.is_tls = False domain = conns.LastIpDomain(address[0]) if domain and UserConn.FrontEnd(self, address, 'raw', domain, on_port, [], conns): self.Cleanup(close=False) else: self.Cleanup() class Listener(Selectable): \"\"\"This class listens for incoming connections and accepts them.\"\"\" def __init__(self, host, port, conns, backlog=100, connclass=UnknownConn, quiet=False): Selectable.__init__(self, bind=(host, port), backlog=backlog) self.Log([('listen', '%s:%s' % (host, port))]) if not quiet: conns.config.ui.Notify(' - Listening on %s:%s' % (host or '*', port)) self.connclass = connclass self.port = port self.last_activity = self.created + 1 self.conns = conns self.conns.Add(self) def __str__(self): return '%s port=%s' % (Selectable.__str__(self), self.port) def __html__(self): return '

    Listening on port %s for %s

    ' % (self.port, self.connclass) def ReadData(self, maxread=None): try: client, address = self.fd.accept() if client: self.Log([('accept', '%s:%s' % (obfuIp(address[0]), address[1]))]) uc = self.connclass(client, address, self.port, self.conns) return True except IOError, err: if err.errno in self.HARMLESS_ERRNOS: return True else: self.LogDebug('Listener::ReadData: error: %s (%s)' % (err, err.errno)) except socket.error, (errno, msg): if errno in self.HARMLESS_ERRNOS: return True else: self.LogInfo('Listener::ReadData: error: %s (errno=%s)' % (msg, errno)) except Exception, e: LogDebug('Listener::ReadData: %s' % e) return False class HttpUiThread(threading.Thread): \"\"\"Handle HTTP UI in a separate thread.\"\"\" daemon = True def __init__(self, pkite, conns, server=None, handler=None, ssl_pem_filename=None): threading.Thread.__init__(self) if not (server and handler): self.serve = False self.httpd = None return self.ui_sspec = pkite.ui_sspec self.httpd = server(self.ui_sspec, pkite, conns, handler=handler, ssl_pem_filename=ssl_pem_filename) self.httpd.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.ui_sspec = pkite.ui_sspec = (self.ui_sspec[0], self.httpd.socket.getsockname()[1]) self.serve = True def quit(self): self.serve = False try: knock = rawsocket(socket.AF_INET, socket.SOCK_STREAM) knock.connect(self.ui_sspec) knock.close() except IOError: pass try: self.join() except RuntimeError: try: if self.httpd and self.httpd.socket: self.httpd.socket.close() except IOError: pass def run(self): while self.serve: try: self.httpd.handle_request() except KeyboardInterrupt: self.serve = False except Exception, e: LogInfo('HTTP UI caught exception: %s' % e) if self.httpd: self.httpd.socket.close() LogDebug('HttpUiThread: done') class UiCommunicator(threading.Thread): \"\"\"Listen for interactive commands.\"\"\" def __init__(self, config, conns): threading.Thread.__init__(self) self.looping = False self.config = config self.conns = conns LogDebug('UiComm: Created') def run(self): self.looping = True while self.looping: if not self.config or not self.config.ui.ALLOWS_INPUT: time.sleep(1) continue line = '' try: i, o, e = select.select([self.config.ui.rfile], [], [], 1) if not i: continue except: pass if self.config: line = self.config.ui.rfile.readline().strip() if line: self.Parse(line) LogDebug('UiCommunicator: done') def Reconnect(self): if self.config.tunnel_manager: self.config.ui.Status('reconfig') self.config.tunnel_manager.CloseTunnels() self.config.tunnel_manager.HurryUp() def Parse(self, line): try: command, args = line.split(': ', 1) LogDebug('UiComm: %s(%s)' % (command, args)) if args.lower() == 'none': args = None elif args.lower() == 'true': args = True elif args.lower() == 'false': args = False if command == 'exit': self.config.keep_looping = False self.config.main_loop = False elif command == 'restart': self.config.keep_looping = False self.config.main_loop = True elif command == 'config': command = 'change settings' self.config.Configure(['--%s' % args]) elif command == 'enablekite': command = 'enable kite' if args and args in self.config.backends: self.config.backends[args][BE_STATUS] = BE_STATUS_UNKNOWN self.Reconnect() else: raise Exception('No such kite: %s' % args) elif command == 'disablekite': command = 'disable kite' if args and args in self.config.backends: self.config.backends[args][BE_STATUS] = BE_STATUS_DISABLED self.Reconnect() else: raise Exception('No such kite: %s' % args) elif command == 'delkite': command = 'remove kite' if args and args in self.config.backends: del self.config.backends[args] self.Reconnect() else: raise Exception('No such kite: %s' % args) elif command == 'addkite': command = 'create new kite' args = (args or '').strip().split() or [''] if self.config.RegisterNewKite(kitename=args[0], autoconfigure=True, ask_be=True): self.Reconnect() elif command == 'save': command = 'save configuration' self.config.SaveUserConfig(quiet=(args == 'quietly')) except ValueError: LogDebug('UiComm: bogus: %s' % line) except SystemExit: self.config.keep_looping = False self.config.main_loop = False except: LogDebug('UiComm: %s' % (sys.exc_info(), )) self.config.ui.Tell(['Oops!', '', 'Failed to %s, details:' % command, '', '%s' % (sys.exc_info(), )], error=True) def quit(self): self.looping = False self.conns = None try: self.join() except RuntimeError: pass class TunnelManager(threading.Thread): \"\"\"Create new tunnels as necessary or kill idle ones.\"\"\" daemon = True def __init__(self, pkite, conns): threading.Thread.__init__(self) self.pkite = pkite self.conns = conns def CheckIdleConns(self, now): active = [] for conn in self.conns.idle: if conn.last_activity: active.append(conn) elif conn.created < now - 10: LogDebug('Removing idle connection: %s' % conn) self.conns.Remove(conn) conn.Cleanup() elif conn.created < now - 1: conn.SayHello() for conn in active: self.conns.idle.remove(conn) def CheckTunnelQuotas(self, now): for tid in self.conns.tunnels: for tunnel in self.conns.tunnels[tid]: tunnel.RecheckQuota(self.conns, when=now) def PingTunnels(self, now): dead = {} for tid in self.conns.tunnels: for tunnel in self.conns.tunnels[tid]: grace = max(40, len(tunnel.write_blocked)/(tunnel.write_speed or 0.001)) if tunnel.last_activity == 0: pass elif tunnel.last_activity < tunnel.last_ping-(5+grace): dead['%s' % tunnel] = tunnel elif tunnel.last_activity < now-30 and tunnel.last_ping < now-2: tunnel.SendPing() for tunnel in dead.values(): Log([('dead', tunnel.server_info[tunnel.S_NAME])]) self.conns.Remove(tunnel) tunnel.Cleanup() def CloseTunnels(self): close = [] for tid in self.conns.tunnels: for tunnel in self.conns.tunnels[tid]: close.append(tunnel) for tunnel in close: Log([('closing', tunnel.server_info[tunnel.S_NAME])]) self.conns.Remove(tunnel) tunnel.Cleanup() def quit(self): self.keep_running = False def run(self): self.keep_running = True self.explained = False while self.keep_running: try: self._run() except Exception, e: LogError('TunnelManager died: %s' % e) if DEBUG_IO: traceback.print_exc(file=sys.stderr) time.sleep(5) LogDebug('TunnelManager: done') def _run(self): self.check_interval = 5 while self.keep_running: # Reconnect if necessary, randomized exponential fallback. problem = False if self.pkite.CreateTunnels(self.conns) > 0: self.check_interval += int(1+random.random()*self.check_interval) if self.check_interval > 300: self.check_interval = 300 problem = True time.sleep(1) else: self.check_interval = 5 # If all connected, make sure tunnels are really alive. if self.pkite.isfrontend: self.CheckTunnelQuotas(time.time()) # FIXME: Front-ends should close dead back-end tunnels. for tid in self.conns.tunnels: proto, domain = tid.split(':') if '-' in proto: proto, port = proto.split('-') else: port = '' self.pkite.ui.NotifyFlyingFE(proto, port, domain) self.PingTunnels(time.time()) self.pkite.ui.StartListingBackEnds() for bid in self.pkite.backends: be = self.pkite.backends[bid] # Do we have auto-SSL at the front-end? protoport, domain = bid.split(':', 1) tunnels = self.conns.Tunnel(protoport, domain) if be[BE_PROTO] in ('http', 'http2', 'http3') and tunnels: has_ssl = True for t in tunnels: if (protoport, domain) not in t.remote_ssl: has_ssl = False else: has_ssl = False # Get list of webpaths... domainp = '%s/%s' % (domain, be[BE_PORT] or '80') if (self.pkite.ui_sspec and be[BE_BHOST] == self.pkite.ui_sspec[0] and be[BE_BPORT] == self.pkite.ui_sspec[1]): builtin = True dpaths = self.pkite.ui_paths.get(domainp, {}) else: builtin = False dpaths = {} self.pkite.ui.NotifyBE(bid, be, has_ssl, dpaths, is_builtin=builtin) self.pkite.ui.EndListingBackEnds() if self.pkite.isfrontend: self.pkite.LoadMOTD() tunnel_count = len(self.pkite.conns and self.pkite.conns.TunnelServers() or []) tunnel_total = len(self.pkite.servers) if tunnel_count == 0: if self.pkite.isfrontend: self.pkite.ui.Status('idle', message='Waiting for back-ends.') elif tunnel_total == 0: self.pkite.ui.Status('down', color=self.pkite.ui.GREY, message='No kites ready to fly. Boring...') else: self.pkite.ui.Status('down', color=self.pkite.ui.RED, message='Not connected to any front-ends, will retry...') elif tunnel_count < tunnel_total: self.pkite.ui.Status('flying', color=self.pkite.ui.YELLOW, message=('Only connected to %d/%d front-ends, will retry...' ) % (tunnel_count, tunnel_total)) elif problem: self.pkite.ui.Status('flying', color=self.pkite.ui.YELLOW, message='DynDNS updates may be incomplete, will retry...') else: self.pkite.ui.Status('flying', color=self.pkite.ui.GREEN, message='Kites are flying and all is well.') for i in xrange(0, self.check_interval): if self.keep_running: time.sleep(1) if i > self.check_interval: break if self.pkite.isfrontend: self.CheckIdleConns(time.time()) def HurryUp(self): self.check_interval = 0 class NullUi(object): \"\"\"This is a UI that always returns default values or raises errors.\"\"\" DAEMON_FRIENDLY = True ALLOWS_INPUT = False WANTS_STDERR = False REJECTED_REASONS = { 'quota': 'You are out of quota', 'nodays': 'Your subscription has expired', 'noquota': 'You are out of quota', 'noconns': 'You are flying too many kites', 'unauthorized': 'Invalid account or shared secret' } def __init__(self, welcome=None, wfile=sys.stderr, rfile=sys.stdin): if sys.platform in ('win32', 'os2', 'os2emx'): self.CLEAR = '\\n\\n' self.NORM = self.WHITE = self.GREY = self.GREEN = self.YELLOW = '' self.BLUE = self.RED = self.MAGENTA = self.CYAN = '' else: self.CLEAR = '\\033[H\\033[J' self.NORM = '\\033[0m' self.WHITE = '\\033[1m' self.GREY = '\\033[0m' #'\\033[30;1m' self.RED = '\\033[31;1m' self.GREEN = '\\033[32;1m' self.YELLOW = '\\033[33;1m' self.BLUE = '\\033[34;1m' self.MAGENTA = '\\033[35;1m' self.CYAN = '\\033[36;1m' self.wfile = wfile self.rfile = rfile self.in_wizard = False self.wizard_tell = None self.last_tick = 0 self.notify_history = {} self.status_tag = '' self.status_col = self.NORM self.status_msg = '' self.welcome = welcome self.tries = 200 self.server_info = None self.Splash() def Splash(self): pass def Welcome(self): pass def StartWizard(self, title): pass def EndWizard(self): pass def Spacer(self): pass def Browse(self, url): import webbrowser self.Tell(['Opening %s in your browser...' % url]) webbrowser.open(url) def DefaultOrFail(self, question, default): if default is not None: return default raise ConfigError('Unanswerable question: %s' % question) def AskLogin(self, question, default=None, email=None, wizard_hint=False, image=None, back=None): return self.DefaultOrFail(question, default) def AskEmail(self, question, default=None, pre=None, wizard_hint=False, image=None, back=None): return self.DefaultOrFail(question, default) def AskYesNo(self, question, default=None, pre=None, yes='Yes', no='No', wizard_hint=False, image=None, back=None): return self.DefaultOrFail(question, default) def AskKiteName(self, domains, question, pre=[], default=None, wizard_hint=False, image=None, back=None): return self.DefaultOrFail(question, default) def AskMultipleChoice(self, choices, question, pre=[], default=None, wizard_hint=False, image=None, back=None): return self.DefaultOrFail(question, default) def AskBackends(self, kitename, protos, ports, rawports, question, pre=[], default=None, wizard_hint=False, image=None, back=None): return self.DefaultOrFail(question, default) def Working(self, message): pass def Tell(self, lines, error=False, back=None): if error: LogError(' '.join(lines)) raise ConfigError(' '.join(lines)) else: Log(['message', ' '.join(lines)]) return True def Notify(self, message, prefix=' ', popup=False, color=None, now=None, alignright=''): if popup: Log([('info', '%s%s%s' % (message, alignright and ' ' or '', alignright))]) def NotifyMOTD(self, frontend, message): pass def NotifyKiteRejected(self, proto, domain, reason, crit=False): if reason in self.REJECTED_REASONS: reason = self.REJECTED_REASONS[reason] self.Notify('REJECTED: %s:%s (%s)' % (proto, domain, reason), prefix='!', color=(crit and self.RED or self.YELLOW)) def NotifyServer(self, obj, server_info): self.server_info = server_info self.Notify('Connecting to front-end %s ...' % server_info[obj.S_NAME], color=self.GREY) self.Notify(' - Protocols: %s' % ' '.join(server_info[obj.S_PROTOS]), color=self.GREY) self.Notify(' - Ports: %s' % ' '.join(server_info[obj.S_PORTS]), color=self.GREY) if 'raw' in server_info[obj.S_PROTOS]: self.Notify(' - Raw ports: %s' % ' '.join(server_info[obj.S_RAW_PORTS]), color=self.GREY) def NotifyQuota(self, quota): qMB = 1024 self.Notify('You have %.2f MB of quota left.' % (quota / qMB), prefix=(int(quota) < qMB) and '!' or ' ', color=self.MAGENTA) def NotifyFlyingFE(self, proto, port, domain, be=None): self.Notify(('Flying: %s://%s%s/' ) % (proto, domain, port and ':'+port or ''), prefix='~<>', color=self.CYAN) def StartListingBackEnds(self): pass def EndListingBackEnds(self): pass def NotifyBE(self, bid, be, has_ssl, dpaths, is_builtin=False): domain, port, proto = be[BE_DOMAIN], be[BE_PORT], be[BE_PROTO] prox = (proto == 'raw') and ' (HTTP proxied)' or '' if proto == 'raw' and port in ('22', 22): proto = 'ssh' url = '%s://%s%s' % (proto, domain, port and (':%s' % port) or '') if be[BE_STATUS] == BE_STATUS_UNKNOWN: return if be[BE_STATUS] & BE_STATUS_OK: if be[BE_STATUS] & BE_STATUS_ERR_ANY: status = 'Trying' color = self.YELLOW prefix = ' ' else: status = 'Flying' color = self.CYAN prefix = '~<>' else: return self.Notify(('%s %s:%s as %s/%s' ) % (status, be[BE_BHOST], be[BE_BPORT], url, prox), prefix=prefix, color=color) if status == 'Flying': for dp in sorted(dpaths.keys()): self.Notify(' - %s%s' % (url, dp), color=self.BLUE) def Status(self, tag, message=None, color=None): pass def ExplainError(self, error, title, subject=None): if error == 'pleaselogin': self.Tell([title, '', 'You already have an account. Log in to continue.' ], error=True) elif error == 'email': self.Tell([title, '', 'Invalid e-mail address. Please try again?' ], error=True) elif error == 'honey': self.Tell([title, '', 'Hmm. Somehow, you triggered the spam-filter.' ], error=True) elif error in ('domaintaken', 'domain', 'subdomain'): self.Tell([title, '', 'Sorry, that domain (%s) is unavailable.' % subject ], error=True) elif error == 'checkfailed': self.Tell([title, '', 'That domain (%s) is not correctly set up.' % subject ], error=True) elif error == 'network': self.Tell([title, '', 'There was a problem communicating with %s.' % subject, '', 'Please verify that you have a working' ' Internet connection and try again!' ], error=True) else: self.Tell([title, 'Error code: %s' % error, 'Try again later?' ], error=True) class PageKite(object): \"\"\"Configuration and master select loop.\"\"\" def __init__(self, ui=None, http_handler=None, http_server=None): self.progname = ((sys.argv[0] or 'pagekite.py').split('/')[-1] .split('\\\\')[-1]) self.ui = ui or NullUi() self.ui_request_handler = http_handler self.ui_http_server = http_server self.ResetConfiguration() def ResetConfiguration(self): self.isfrontend = False self.upgrade_info = [] self.auth_domain = None self.motd = None self.motd_message = None self.server_host = '' self.server_ports = [80] self.server_raw_ports = [] self.server_portalias = {} self.server_aliasport = {} self.server_protos = ['http', 'http2', 'http3', 'https', 'websocket', 'irc', 'finger', 'httpfinger', 'raw'] self.tls_default = None self.tls_endpoints = {} self.fe_certname = [] self.fe_anon_tls_wrap = False self.service_provider = SERVICE_PROVIDER self.service_xmlrpc = SERVICE_XMLRPC self.daemonize = False self.pidfile = None self.logfile = None self.setuid = None self.setgid = None self.ui_httpd = None self.ui_sspec_cfg = None self.ui_sspec = None self.ui_socket = None self.ui_password = None self.ui_pemfile = None self.ui_magic_file = '.pagekite.magic' self.ui_paths = {} self.be_config = {} self.disable_zchunks = False self.enable_sslzlib = False self.buffer_max = DEFAULT_BUFFER_MAX self.error_url = None self.finger_path = '/~%s/.finger' self.tunnel_manager = None self.client_mode = 0 self.proxy_server = None self.require_all = False self.no_probes = False self.servers = [] self.servers_manual = [] self.servers_auto = None self.servers_new_only = False self.servers_no_ping = False self.servers_preferred = [] self.servers_sessionids = {} self.kitename = '' self.kitesecret = '' self.dyndns = None self.last_updates = [] self.backends = {} # These are the backends we want tunnels for. self.conns = None self.last_loop = 0 self.keep_looping = True self.main_loop = True self.crash_report_url = '%scgi-bin/crashes.pl' % WWWHOME self.rcfile_recursion = 0 self.rcfiles_loaded = [] self.savefile = None self.autosave = 0 self.reloadfile = None self.added_kites = False self.ui_wfile = sys.stderr self.ui_rfile = sys.stdin self.ui_port = None self.ui_conn = None self.ui_comm = None self.save = 0 self.kite_add = False self.kite_only = False self.kite_disable = False self.kite_remove = False # Searching for our configuration file! We prefer the documented # 'standard' locations, but if nothing is found there and something local # exists, use that instead. try: if sys.platform in ('win32', 'os2', 'os2emx'): self.rcfile = os.path.join(os.path.expanduser('~'), 'pagekite.cfg') self.devnull = 'nul' else: # Everything else self.rcfile = os.path.join(os.path.expanduser('~'), '.pagekite.rc') self.devnull = '/dev/null' except Exception, e: # The above stuff may fail in some cases, e.g. on Android in SL4A. self.rcfile = 'pagekite.cfg' self.devnull = '/dev/null' # Look for CA Certificates. If we don't find them in the host OS, # we assume there might be something good in the program itself. self.ca_certs_default = '/etc/ssl/certs/ca-certificates.crt' if not os.path.exists(self.ca_certs_default): self.ca_certs_default = sys.argv[0] self.ca_certs = self.ca_certs_default def SetLocalSettings(self, ports): self.isfrontend = True self.servers_auto = None self.servers_manual = [] self.server_ports = ports self.backends = self.ArgToBackendSpecs('http:localhost:localhost:builtin:-') def SetServiceDefaults(self, clobber=True, check=False): def_dyndns = (DYNDNS['pagekite.net'], {'user': '', 'pass': ''}) def_frontends = (1, 'frontends.b5p.us', 443) def_ca_certs = sys.argv[0] def_fe_certs = ['b5p.us', 'frontends.b5p.us', 'pagekite.net'] def_error_url = 'https://pagekite.net/offline/?' if check: return (self.dyndns == def_dyndns and self.servers_auto == def_frontends and self.error_url == def_error_url and self.ca_certs == def_ca_certs and (self.fe_certname == def_fe_certs or not socks.HAVE_SSL)) else: self.dyndns = (not clobber and self.dyndns) or def_dyndns self.servers_auto = (not clobber and self.servers_auto) or def_frontends self.error_url = (not clobber and self.error_url) or def_error_url self.ca_certs = def_ca_certs if socks.HAVE_SSL: for cert in def_fe_certs: if cert not in self.fe_certname: self.fe_certname.append(cert) self.fe_certname.sort() return True def GenerateConfig(self, safe=False): config = [ '###[ Current settings for pagekite.py v%s. ]#########' % APPVER, '#', '## NOTE: This file may be rewritten/reordered by pagekite.py.', '#', '', ] if not self.kitename: for be in self.backends.values(): if not self.kitename or len(self.kitename) < len(be[BE_DOMAIN]): self.kitename = be[BE_DOMAIN] self.kitesecret = be[BE_SECRET] new = not (self.kitename or self.kitesecret or self.backends) def p(vfmt, value, dval): return '%s%s' % (value and value != dval and ('', vfmt % value) or ('# ', vfmt % dval)) if self.kitename or self.kitesecret or new: config.extend([ '##[ Default kite and account details ]##', p('kitename=%s', self.kitename, 'NAME'), p('kitesecret=%s', self.kitesecret, 'SECRET'), '' ]) if self.SetServiceDefaults(check=True): config.extend([ '##[ Front-end settings: use service defaults ]##', 'defaults', '' ]) if self.servers_manual: config.append('##[ Manual front-ends ]##') for server in sorted(self.servers_manual): config.append('frontend=%s' % server) config.append('') else: if not self.servers_auto and not self.servers_manual: new = True config.extend([ '##[ Use this to just use service defaults ]##', '# defaults', '' ]) config.append('##[ Custom front-end and dynamic DNS settings ]##') if self.servers_auto: config.append('frontends=%d:%s:%d' % self.servers_auto) if self.servers_manual: for server in sorted(self.servers_manual): config.append('frontend=%s' % server) if not self.servers_auto and not self.servers_manual: new = True config.append('# frontends=N:hostname:port') config.append('# frontend=hostname:port') for server in sorted(self.fe_certname): config.append('fe_certname=%s' % server) if self.ca_certs != self.ca_certs_default: config.append('ca_certs=%s' % self.ca_certs) if self.dyndns: provider, args = self.dyndns for prov in sorted(DYNDNS.keys()): if DYNDNS[prov] == provider and prov != 'beanstalks.net': args['prov'] = prov if 'prov' not in args: args['prov'] = provider if args['pass']: config.append('dyndns=%(user)s:%(pass)s@%(prov)s' % args) elif args['user']: config.append('dyndns=%(user)s@%(prov)s' % args) else: config.append('dyndns=%(prov)s' % args) else: new = True config.extend([ '# dyndns=pagekite.net OR', '# dyndns=user:pass@dyndns.org OR', '# dyndns=user:pass@no-ip.com' , '#', p('errorurl=%s', self.error_url, 'http://host/page/'), p('fingerpath=%s', self.finger_path, '/~%s/.finger'), '', ]) if self.ui_sspec or self.ui_password or self.ui_pemfile: config.extend([ '##[ Built-in HTTPD settings ]##', p('httpd=%s:%s', self.ui_sspec_cfg, ('host', 'port')) ]) if self.ui_password: config.append('httppass=%s' % self.ui_password) if self.ui_pemfile: config.append('pemfile=%s' % self.pemfile) for http_host in sorted(self.ui_paths.keys()): for path in sorted(self.ui_paths[http_host].keys()): up = self.ui_paths[http_host][path] config.append('webpath=%s:%s:%s:%s' % (http_host, path, up[0], up[1])) config.append('') config.append('##[ Back-ends and local services ]##') bprinted = 0 for bid in sorted(self.backends.keys()): be = self.backends[bid] proto, domain = bid.split(':') if be[BE_BHOST]: be_spec = (be[BE_BHOST], be[BE_BPORT]) config.append(('%s=%s:%s:%s:%s' ) % ((be[BE_STATUS] == BE_STATUS_DISABLED ) and 'define_backend' or 'backend', proto, ((domain == self.kitename) and '@kitename' or domain), (be_spec == self.ui_sspec) and 'localhost:builtin' or ('%s:%s' % be_spec), (be[BE_SECRET] == self.kitesecret) and '@kitesecret' or be[BE_SECRET])) bprinted += 1 if bprinted == 0: config.append('# No back-ends! How boring!') for http_host in sorted(self.be_config.keys()): for key in sorted(self.be_config[http_host].keys()): config.append('be_config=%s:%s:%s' % (http_host, key, self.be_config[http_host][key])) config.append('') if bprinted == 0: new = True config.extend([ '##[ Back-end examples ... ]##', '#', '# backend=http:YOU.pagekite.me:localhost:80:SECRET', '# backend=ssh:YOU.pagekite.me:localhost:22:SECRET', '# backend=http/8080:YOU.pagekite.me:localhost:8080:SECRET', '# backend=https:YOU.pagekite.me:localhost:443:SECRET', '# backend=websocket:YOU.pagekite.me:localhost:8080:SECRET', '#', '# define_backend=http:YOU.pagekite.me:localhost:4545:SECRET', '' ]) if self.isfrontend or new: config.extend([ '##[ Front-end Options ]##', (self.isfrontend and 'isfrontend' or '# isfrontend') ]) comment = ((not self.isfrontend) and '# ' or '') config.extend([ p('host=%s', self.isfrontend and self.server_host, 'machine.domain.com'), '%sports=%s' % (comment, ','.join(['%s' % x for x in sorted(self.server_ports)] or [])), '%sprotos=%s' % (comment, ','.join(['%s' % x for x in sorted(self.server_protos)] or [])) ]) for pa in self.server_portalias: config.append('portalias=%s:%s' % (int(pa), int(self.server_portalias[pa]))) config.extend([ '%srawports=%s' % (comment or (not self.server_raw_ports) and '# ' or '', ','.join(['%s' % x for x in sorted(self.server_raw_ports)] or [VIRTUAL_PN])), p('authdomain=%s', self.isfrontend and self.auth_domain, 'foo.com'), p('motd=%s', self.isfrontend and self.motd, '/path/to/motd.txt') ]) dprinted = 0 for bid in sorted(self.backends.keys()): be = self.backends[bid] if not be[BE_BHOST]: config.append('domain=%s:%s' % (bid, be[BE_SECRET])) dprinted += 1 if not dprinted: new = True config.extend([ '# domain=http:*.pagekite.me:SECRET1', '# domain=http,https,websocket:THEM.pagekite.me:SECRET2', '', ]) eprinted = 0 config.append('##[ Domains we terminate SSL/TLS for natively, with key/cert-files ]##') for ep in sorted(self.tls_endpoints.keys()): config.append('tls_endpoint=%s:%s' % (ep, self.tls_endpoints[ep][0])) eprinted += 1 if eprinted == 0: new = True config.append('# tls_endpoint=DOMAIN:PEM_FILE') config.extend([ p('tls_default=%s', self.tls_default, 'DOMAIN'), '', ]) config.extend([ '', '###[ Anything below this line can usually be ignored. ]#########', '', '##[ Miscellaneous settings ]##', p('logfile=%s', self.logfile, '/path/to/file'), p('buffers=%s', self.buffer_max, DEFAULT_BUFFER_MAX), (self.servers_new_only is True) and 'new' or '# new', (self.require_all and 'all' or '# all'), (self.no_probes and 'noprobes' or '# noprobes'), (self.crash_report_url and '# nocrashreport' or 'nocrashreport'), p('savefile=%s', safe and self.savefile, '/path/to/savefile'), (self.autosave and 'autosave' or '# autosave'), '', ]) if self.daemonize or self.setuid or self.setgid or self.pidfile or new: config.extend([ '##[ Systems administration settings ]##', (self.daemonize and 'daemonize' or '# daemonize') ]) if self.setuid and self.setgid: config.append('runas=%s:%s' % (self.setuid, self.setgid)) elif self.setuid: config.append('runas=%s' % self.setuid) else: new = True config.append('# runas=uid:gid') config.append(p('pidfile=%s', self.pidfile, '/path/to/file')) config.extend([ '', '###[ End of pagekite.py configuration ]#########', 'END', '' ]) if not new: config = [l for l in config if not l.startswith('# ')] clean_config = [] for i in range(0, len(config)-1): if i > 0 and (config[i].startswith('#') or config[i] == ''): if config[i+1] != '' or clean_config[-1].startswith('#'): clean_config.append(config[i]) else: clean_config.append(config[i]) clean_config.append(config[-1]) return clean_config else: return config def ConfigSecret(self, new=False): # This method returns a stable secret for the lifetime of this process. # # The secret depends on the active configuration as, reported by # GenerateConfig(). This lets external processes generate the same # secret and use the remote-control APIs as long as they can read the # *entire* config (which contains all the sensitive bits anyway). # if self.ui_httpd and self.ui_httpd.httpd and not new: return self.ui_httpd.httpd.secret else: return sha1hex('\\n'.join(self.GenerateConfig())) def LoginPath(self, goto): return '/_pagekite/login/%s/%s' % (self.ConfigSecret(), goto) def LoginUrl(self, goto=''): return 'http%s://%s%s' % (self.ui_pemfile and 's' or '', '%s:%s' % self.ui_sspec, self.LoginPath(goto)) def ListKites(self): self.ui.welcome = '>>> ' + self.ui.WHITE + 'Your kites:' + self.ui.NORM message = [] for bid in sorted(self.backends.keys()): be = self.backends[bid] be_be = (be[BE_BHOST], be[BE_BPORT]) backend = (be_be == self.ui_sspec) and 'builtin' or '%s:%s' % be_be fe_port = be[BE_PORT] or '' frontend = '%s://%s%s%s' % (be[BE_PROTO], be[BE_DOMAIN], fe_port and ':' or '', fe_port) if be[BE_STATUS] == BE_STATUS_DISABLED: color = self.ui.GREY status = '(disabled)' else: color = self.ui.NORM status = (be[BE_PROTO] == 'raw') and '(HTTP proxied)' or '' message.append(''.join([color, backend, ' ' * (19-len(backend)), frontend, ' ' * (42-len(frontend)), status])) message.append(self.ui.NORM) self.ui.Tell(message) def PrintSettings(self, safe=False): print '\\n'.join(self.GenerateConfig(safe=safe)) def SaveUserConfig(self, quiet=False): self.savefile = self.savefile or self.rcfile try: fd = open(self.savefile, 'w') fd.write('\\n'.join(self.GenerateConfig(safe=True))) fd.close() if not quiet: self.ui.Tell(['Settings saved to: %s' % self.savefile]) self.ui.Spacer() Log([('saved', 'Settings saved to: %s' % self.savefile)]) except Exception, e: self.ui.Tell(['Could not save to %s: %s' % (self.savefile, e)], error=True) self.ui.Spacer() def FallDown(self, message, help=True, longhelp=False, noexit=False): if self.conns and self.conns.auth: self.conns.auth.quit() if self.ui_httpd: self.ui_httpd.quit() if self.ui_comm: self.ui_comm.quit() if self.tunnel_manager: self.tunnel_manager.quit() self.keep_looping = False self.conns = self.ui_httpd = self.ui_comm = self.tunnel_manager = None if help or longhelp: print longhelp and DOC or MINIDOC print '***' else: self.ui.Status('exiting', message=(message or 'Good-bye!')) if message: print 'Error: %s' % message if DEBUG_IO: traceback.print_exc(file=sys.stderr) if not noexit: self.main_loop = False sys.exit(1) def GetTlsEndpointCtx(self, domain): if domain in self.tls_endpoints: return self.tls_endpoints[domain][1] parts = domain.split('.') # Check for wildcards ... while len(parts) > 2: parts[0] = '*' domain = '.'.join(parts) if domain in self.tls_endpoints: return self.tls_endpoints[domain][1] parts.pop(0) return None def SetBackendStatus(self, domain, proto='', add=None, sub=None): match = '%s:%s' % (proto, domain) for bid in self.backends: if bid == match or (proto == '' and bid.endswith(match)): status = self.backends[bid][BE_STATUS] if add: self.backends[bid][BE_STATUS] |= add if sub and (status & sub): self.backends[bid][BE_STATUS] -= sub Log([('bid', bid), ('status', '0x%x' % self.backends[bid][BE_STATUS])]) def GetBackendData(self, proto, domain, recurse=True): backend = '%s:%s' % (proto.lower(), domain.lower()) if backend in self.backends: if self.backends[backend][BE_STATUS] not in BE_INACTIVE: return self.backends[backend] if recurse: dparts = domain.split('.') while len(dparts) > 1: dparts = dparts[1:] data = self.GetBackendData(proto, '.'.join(['*'] + dparts), recurse=False) if data: return data return None def GetBackendServer(self, proto, domain, recurse=True): backend = self.GetBackendData(proto, domain) or BE_NONE bhost, bport = (backend[BE_BHOST], backend[BE_BPORT]) if bhost == '-' or not bhost: return None, None return (bhost, bport), backend def IsSignatureValid(self, sign, secret, proto, domain, srand, token): return checkSignature(sign=sign, secret=secret, payload='%s:%s:%s:%s' % (proto, domain, srand, token)) def LookupDomainQuota(self, lookup): if not lookup.endswith('.'): lookup += '.' if DEBUG_IO: print '=== AUTH LOOKUP\\n%s\\n===' % lookup (hn, al, ips) = socket.gethostbyname_ex(lookup) if DEBUG_IO: print 'hn=%s\\nal=%s\\nips=%s\\n' % (hn, al, ips) # Extract auth error hints from domain name, if we got a CNAME reply. if al: error = hn.split('.')[0] else: error = None # If not an authentication error, quota should be encoded as an IP. ip = ips[0] if not ip.startswith(AUTH_ERRORS): o = [int(x) for x in ip.split('.')] return ((((o[0]*256 + o[1])*256 + o[2])*256 + o[3]), None) # Errors on real errors are final. if not ip.endswith(AUTH_ERR_USER_UNKNOWN): return (None, error) # User unknown, fall through to local test. return (-1, error) def GetDomainQuota(self, protoport, domain, srand, token, sign, recurse=True, check_token=True): if '-' in protoport: try: proto, port = protoport.split('-', 1) if proto == 'raw': port_list = self.server_raw_ports else: port_list = self.server_ports porti = int(port) if porti in self.server_aliasport: porti = self.server_aliasport[porti] if porti not in port_list and VIRTUAL_PN not in port_list: LogInfo('Unsupported port request: %s (%s:%s)' % (porti, protoport, domain)) return (None, 'port') except ValueError: LogError('Invalid port request: %s:%s' % (protoport, domain)) return (None, 'port') else: proto, port = protoport, None if proto not in self.server_protos: LogInfo('Invalid proto request: %s:%s' % (protoport, domain)) return (None, 'proto') data = '%s:%s:%s' % (protoport, domain, srand) auth_error_type = None if ((not token) or (not check_token) or checkSignature(sign=token, payload=data)): secret = (self.GetBackendData(protoport, domain) or BE_NONE)[BE_SECRET] if not secret: secret = (self.GetBackendData(proto, domain) or BE_NONE)[BE_SECRET] if secret: if self.IsSignatureValid(sign, secret, protoport, domain, srand, token): return (-1, None) elif not self.auth_domain: LogError('Invalid signature for: %s (%s)' % (domain, protoport)) return (None, auth_error_type or 'signature') if self.auth_domain: try: lookup = '.'.join([srand, token, sign, protoport, domain, self.auth_domain]) (rv, auth_error_type) = self.LookupDomainQuota(lookup) if rv is None or rv >= 0: return (rv, auth_error_type) except Exception, e: # Lookup failed, fail open. LogError('Quota lookup failed: %s' % e) return (-2, None) LogInfo('No authentication found for: %s (%s)' % (domain, protoport)) return (None, auth_error_type or 'unauthorized') def ConfigureFromFile(self, filename=None, data=None): if not filename: filename = self.rcfile if self.rcfile_recursion > 25: raise ConfigError('Nested too deep: %s' % filename) self.rcfiles_loaded.append(filename) optfile = data or open(filename) args = [] for line in optfile: line = line.strip() if line and not line.startswith('#'): if line.startswith('END'): break if not line.startswith('-'): line = '--%s' % line args.append(line) self.rcfile_recursion += 1 self.Configure(args) self.rcfile_recursion -= 1 return self def ConfigureFromDirectory(self, dirname): for fn in sorted(os.listdir(dirname)): if not fn.startswith('.') and fn.endswith('.rc'): self.ConfigureFromFile(os.path.join(dirname, fn)) def HelpAndExit(self, longhelp=False): print longhelp and DOC or MINIDOC sys.exit(0) def ArgToBackendSpecs(self, arg, status=BE_STATUS_UNKNOWN, secret=None): protos, fe_domain, be_host, be_port = '', '', '', '' # Interpret the argument into a specification of what we want. parts = arg.split(':') if len(parts) == 5: protos, fe_domain, be_host, be_port, secret = parts elif len(parts) == 4: protos, fe_domain, be_host, be_port = parts elif len(parts) == 3: protos, fe_domain, be_port = parts elif len(parts) == 2: if (parts[1] == 'builtin') or ('.' in parts[0] and os.path.exists(parts[1])): fe_domain, be_port = parts[0], parts[1] protos = 'http' else: try: fe_domain, be_port = parts[0], '%s' % int(parts[1]) protos = 'http' except: be_port = '' protos, fe_domain = parts elif len(parts) == 1: fe_domain = parts[0] else: return {} # Allow http:// as a common typo instead of http: fe_domain = fe_domain.replace('/', '').lower() # Allow easy referencing of built-in HTTPD if be_port == 'builtin': self.BindUiSspec() be_host, be_port = self.ui_sspec # Specs define what we are searching for... specs = [] if protos: for proto in protos.replace('/', '-').lower().split(','): if proto == 'ssh': specs.append(['raw', '22', fe_domain, be_host, be_port or '22', secret]) else: if '-' in proto: proto, port = proto.split('-') else: if len(parts) == 1: port = '*' else: port = '' specs.append([proto, port, fe_domain, be_host, be_port, secret]) else: specs = [[None, '', fe_domain, be_host, be_port, secret]] backends = {} # For each spec, search through the existing backends and copy matches # or just shared secrets for partial matches. for proto, port, fdom, bhost, bport, sec in specs: matches = 0 for bid in self.backends: be = self.backends[bid] if fdom and fdom != be[BE_DOMAIN]: continue if not sec and be[BE_SECRET]: sec = be[BE_SECRET] if proto and (proto != be[BE_PROTO]): continue if bhost and (bhost.lower() != be[BE_BHOST]): continue if bport and (int(bport) != be[BE_BHOST]): continue if port and (port != '*') and (int(port) != be[BE_PORT]): continue backends[bid] = be[:] backends[bid][BE_STATUS] = status matches += 1 if matches == 0: proto = (proto or 'http') bhost = (bhost or 'localhost') bport = (bport or (proto in ('http', 'httpfinger', 'websocket') and 80) or (proto == 'irc' and 6667) or (proto == 'https' and 443) or (proto == 'finger' and 79)) if port: bid = '%s-%d:%s' % (proto, int(port), fdom) else: bid = '%s:%s' % (proto, fdom) backends[bid] = BE_NONE[:] backends[bid][BE_PROTO] = proto backends[bid][BE_PORT] = port and int(port) or '' backends[bid][BE_DOMAIN] = fdom backends[bid][BE_BHOST] = bhost.lower() backends[bid][BE_BPORT] = int(bport) backends[bid][BE_SECRET] = sec backends[bid][BE_STATUS] = status return backends def BindUiSspec(self, force=False): # Create the UI thread if self.ui_httpd and self.ui_httpd.httpd: if not force: return self.ui_sspec self.ui_httpd.httpd.socket.close() self.ui_sspec = self.ui_sspec or ('localhost', 0) self.ui_httpd = HttpUiThread(self, self.conns, handler=self.ui_request_handler, server=self.ui_http_server, ssl_pem_filename = self.ui_pemfile) return self.ui_sspec def LoadMOTD(self): if self.motd: try: f = open(self.motd, 'r') self.motd_message = ''.join(f.readlines()).strip()[:8192] f.close() except (OSError, IOError): pass def Configure(self, argv): self.conns = self.conns or Connections(self) opts, args = getopt.getopt(argv, OPT_FLAGS, OPT_ARGS) for opt, arg in opts: if opt in ('-o', '--optfile'): self.ConfigureFromFile(arg) elif opt in ('-O', '--optdir'): self.ConfigureFromDirectory(arg) elif opt == '--reloadfile': self.ConfigureFromFile(arg) self.reloadfile = arg elif opt in ('-S', '--savefile'): if self.savefile: raise ConfigError('Multiple save-files!') self.savefile = arg elif opt == '--autosave': self.autosave = True elif opt == '--noautosave': self.autosave = False elif opt == '--save': self.save = True elif opt == '--only': self.save = self.kite_only = True if self.kite_remove or self.kite_add or self.kite_disable: raise ConfigError('One change at a time please!') elif opt == '--add': self.save = self.kite_add = True if self.kite_remove or self.kite_only or self.kite_disable: raise ConfigError('One change at a time please!') elif opt == '--remove': self.save = self.kite_remove = True if self.kite_add or self.kite_only or self.kite_disable: raise ConfigError('One change at a time please!') elif opt == '--disable': self.save = self.kite_disable = True if self.kite_add or self.kite_only or self.kite_remove: raise ConfigError('One change at a time please!') elif opt == '--list': pass elif opt in ('-I', '--pidfile'): self.pidfile = arg elif opt in ('-L', '--logfile'): self.logfile = arg elif opt in ('-Z', '--daemonize'): self.daemonize = True if not self.ui.DAEMON_FRIENDLY: self.ui = NullUi() elif opt in ('-U', '--runas'): import pwd import grp parts = arg.split(':') if len(parts) > 1: self.setuid, self.setgid = (pwd.getpwnam(parts[0])[2], grp.getgrnam(parts[1])[2]) else: self.setuid = pwd.getpwnam(parts[0])[2] self.main_loop = False elif opt in ('-X', '--httppass'): self.ui_password = arg elif opt in ('-P', '--pemfile'): self.ui_pemfile = arg elif opt in ('-H', '--httpd'): parts = arg.split(':') host = parts[0] or 'localhost' if len(parts) > 1: self.ui_sspec = self.ui_sspec_cfg = (host, int(parts[1])) else: self.ui_sspec = self.ui_sspec_cfg = (host, 0) elif opt == '--nowebpath': host, path = arg.split(':', 1) if host in self.ui_paths and path in self.ui_paths[host]: del self.ui_paths[host][path] elif opt == '--webpath': host, path, policy, fpath = arg.split(':', 3) # Defaults... path = path or os.path.normpath(fpath) host = host or '*' policy = policy or WEB_POLICY_DEFAULT if policy not in WEB_POLICIES: raise ConfigError('Policy must be one of: %s' % WEB_POLICIES) elif os.path.isdir(fpath): if not path.endswith('/'): path += '/' hosti = self.ui_paths.get(host, {}) hosti[path] = (policy or 'public', os.path.abspath(fpath)) self.ui_paths[host] = hosti elif opt == '--tls_default': self.tls_default = arg elif opt == '--tls_endpoint': name, pemfile = arg.split(':', 1) ctx = SSL.Context(SSL.SSLv23_METHOD) ctx.use_privatekey_file(pemfile) ctx.use_certificate_chain_file(pemfile) self.tls_endpoints[name] = (pemfile, ctx) elif opt in ('-D', '--dyndns'): if arg.startswith('http'): self.dyndns = (arg, {'user': '', 'pass': ''}) elif '@' in arg: splits = arg.split('@') provider = splits.pop() usrpwd = '@'.join(splits) if provider in DYNDNS: provider = DYNDNS[provider] if ':' in usrpwd: usr, pwd = usrpwd.split(':', 1) self.dyndns = (provider, {'user': usr, 'pass': pwd}) else: self.dyndns = (provider, {'user': usrpwd, 'pass': ''}) elif arg: if arg in DYNDNS: arg = DYNDNS[arg] self.dyndns = (arg, {'user': '', 'pass': ''}) else: self.dyndns = None elif opt in ('-p', '--ports'): self.server_ports = [int(x) for x in arg.split(',')] elif opt == '--portalias': port, alias = arg.split(':') self.server_portalias[int(port)] = int(alias) self.server_aliasport[int(alias)] = int(port) elif opt == '--protos': self.server_protos = [x.lower() for x in arg.split(',')] elif opt == '--rawports': self.server_raw_ports = [(x == VIRTUAL_PN and x or int(x)) for x in arg.split(',')] elif opt in ('-h', '--host'): self.server_host = arg elif opt in ('-A', '--authdomain'): self.auth_domain = arg elif opt == '--motd': self.motd = arg self.LoadMOTD() elif opt == '--noupgradeinfo': self.upgrade_info = [] elif opt == '--upgradeinfo': version, tag, md5, human_url, file_url = arg.split(';') self.upgrade_info.append((version, tag, md5, human_url, file_url)) elif opt in ('-f', '--isfrontend'): self.isfrontend = True global LOG_THRESHOLD LOG_THRESHOLD *= 4 elif opt in ('-a', '--all'): self.require_all = True elif opt in ('-N', '--new'): self.servers_new_only = True elif opt in ('--proxy', '--socksify', '--torify'): if opt == '--proxy': socks.setdefaultproxy() for proxy in arg.split(','): socks.adddefaultproxy(*socks.parseproxy(proxy)) else: (host, port) = arg.split(':') socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, host, int(port)) if not self.proxy_server: # Make DynDNS updates go via the proxy. socks.wrapmodule(urllib) self.proxy_server = arg else: self.proxy_server += ',' + arg if opt == '--torify': self.servers_new_only = True # Disable initial DNS lookups (leaks) self.servers_no_ping = True # Disable front-end pings self.crash_report_url = None # Disable crash reports # This increases the odds of unrelated requests getting lumped # together in the tunnel, which makes traffic analysis harder. global SEND_ALWAYS_BUFFERS SEND_ALWAYS_BUFFERS = True elif opt == '--ca_certs': self.ca_certs = arg elif opt == '--jakenoia': self.fe_anon_tls_wrap = True elif opt == '--fe_certname': if arg == '': self.fe_certname = [] else: cert = arg.lower() if cert not in self.fe_certname: self.fe_certname.append(cert) self.fe_certname.sort() elif opt == '--service_xmlrpc': self.service_xmlrpc = arg elif opt == '--frontend': self.servers_manual.append(arg) elif opt == '--frontends': count, domain, port = arg.split(':') self.servers_auto = (int(count), domain, int(port)) elif opt in ('--errorurl', '-E'): self.error_url = arg elif opt == '--fingerpath': self.finger_path = arg elif opt == '--kitename': self.kitename = arg elif opt == '--kitesecret': self.kitesecret = arg elif opt in ('--backend', '--define_backend'): bes = self.ArgToBackendSpecs(arg.replace('@kitesecret', self.kitesecret) .replace('@kitename', self.kitename), status=((opt != '--backend') and BE_STATUS_DISABLED or BE_STATUS_UNKNOWN)) for bid in bes: if bid in self.backends: raise ConfigError(\"Same backend/domain defined twice: %s\" % bid) if not self.kitename: self.kitename = bes[bid][BE_DOMAIN] self.kitesecret = bes[bid][BE_SECRET] self.backends.update(bes) elif opt == '--be_config': host, key, val = arg.split(':', 2) if key.startswith('user/'): key = key.replace('user/', 'password/') hostc = self.be_config.get(host, {}) hostc[key] = {'True': True, 'False': False, 'None': None}.get(val, val) self.be_config[host] = hostc elif opt == '--delete_backend': bes = self.ArgToBackendSpecs(arg) for bid in bes: if bid in self.backends: del self.backends[bid] elif opt == '--domain': protos, domain, secret = arg.split(':') if protos in ('*', ''): protos = ','.join(self.server_protos) for proto in protos.split(','): bid = '%s:%s' % (proto, domain) if bid in self.backends: raise ConfigError(\"Same backend/domain defined twice: %s\" % bid) self.backends[bid] = BE_NONE[:] self.backends[bid][BE_PROTO] = proto self.backends[bid][BE_DOMAIN] = domain self.backends[bid][BE_SECRET] = secret self.backends[bid][BE_STATUS] = BE_STATUS_UNKNOWN elif opt == '--noprobes': self.no_probes = True elif opt == '--nofrontend': self.isfrontend = False elif opt == '--nodaemonize': self.daemonize = False elif opt == '--noall': self.require_all = False elif opt == '--nozchunks': self.disable_zchunks = True elif opt == '--nullui': self.ui = NullUi() elif opt == '--remoteui': import pagekite.remoteui self.ui = pagekite.remoteui.RemoteUi() elif opt == '--uiport': self.ui_port = int(arg) elif opt == '--sslzlib': self.enable_sslzlib = True elif opt == '--debugio': global DEBUG_IO DEBUG_IO = True elif opt == '--buffers': self.buffer_max = int(arg) elif opt == '--nocrashreport': self.crash_report_url = None elif opt == '--noloop': self.main_loop = False elif opt == '--local': self.SetLocalSettings([int(p) for p in arg.split(',')]) if not 'localhost' in args: args.append('localhost') elif opt == '--defaults': self.SetServiceDefaults() elif opt in ('--clean', '--nopyopenssl', '--nossl', '--settings', '--webaccess', '--webindexes', '--webroot', '--signup', '--friendly'): pass elif opt == '--help': self.HelpAndExit(longhelp=True) elif opt == '--controlpanel': import webbrowser webbrowser.open(self.LoginUrl()) sys.exit(0) elif opt == '--controlpass': print self.ConfigSecret() sys.exit(0) else: self.HelpAndExit() # Make sure these are configured before we try and do XML-RPC stuff. socks.DEBUG = (DEBUG_IO or socks.DEBUG) and LogDebug if self.ca_certs: socks.setdefaultcertfile(self.ca_certs) # Handle the user-friendly argument stuff and simple registration. return self.ParseFriendlyBackendSpecs(args) def ParseFriendlyBackendSpecs(self, args): just_these_backends = {} just_these_webpaths = {} just_these_be_configs = {} argsets = [] while 'AND' in args: argsets.append(args[0:args.index('AND')]) args[0:args.index('AND')+1] = [] if args: argsets.append(args) for args in argsets: # Extract the config options first... be_config = [p for p in args if p.startswith('+')] args = [p for p in args if not p.startswith('+')] fe_spec = (args.pop().replace('@kitesecret', self.kitesecret) .replace('@kitename', self.kitename)) if os.path.exists(fe_spec): raise ConfigError('Is a local file: %s' % fe_spec) be_paths = [] be_path_prefix = '' if len(args) == 0: be_spec = '' elif len(args) == 1: if '*' in args[0] or '?' in args[0]: if sys.platform in ('win32', 'os2', 'os2emx'): be_paths = [args[0]] be_spec = 'builtin' elif os.path.exists(args[0]): be_paths = [args[0]] be_spec = 'builtin' else: be_spec = args[0] else: be_spec = 'builtin' be_paths = args[:] be_proto = 'http' # A sane default... if be_spec == '': be = None else: be = be_spec.replace('/', '').split(':') if be[0].lower() in ('http', 'http2', 'http3', 'https', 'httpfinger', 'finger', 'ssh', 'irc'): be_proto = be.pop(0) if len(be) < 2: be.append({'http': '80', 'http2': '80', 'http3': '80', 'https': '443', 'irc': '6667', 'httpfinger': '80', 'finger': '79', 'ssh': '22'}[be_proto]) if len(be) > 2: raise ConfigError('Bad back-end definition: %s' % be_spec) if len(be) < 2: be = ['localhost', be[0]] # Extract the path prefix from the fe_spec fe_urlp = fe_spec.split('/', 3) if len(fe_urlp) == 4: fe_spec = '/'.join(fe_urlp[:3]) be_path_prefix = '/' + fe_urlp[3] fe = fe_spec.replace('/', '').split(':') if len(fe) == 3: fe = ['%s-%s' % (fe[0], fe[2]), fe[1]] elif len(fe) == 2: try: fe = ['%s-%s' % (be_proto, int(fe[1])), fe[0]] except ValueError: pass elif len(fe) == 1 and be: fe = [be_proto, fe[0]] # Do our own globbing on Windows if sys.platform in ('win32', 'os2', 'os2emx'): import glob new_paths = [] for p in be_paths: new_paths.extend(glob.glob(p)) be_paths = new_paths for f in be_paths: if not os.path.exists(f): raise ConfigError('File or directory not found: %s' % f) spec = ':'.join(fe) if be: spec += ':' + ':'.join(be) specs = self.ArgToBackendSpecs(spec) just_these_backends.update(specs) spec = specs[specs.keys()[0]] http_host = '%s/%s' % (spec[BE_DOMAIN], spec[BE_PORT] or '80') if be_config: # Map the +foo=bar values to per-site config settings. host_config = just_these_be_configs.get(http_host, {}) for cfg in be_config: if '=' in cfg: key, val = cfg[1:].split('=', 1) elif cfg.startswith('+no'): key, val = cfg[3:], False else: key, val = cfg[1:], True if ':' in key: raise ConfigError('Please do not use : in web config keys.') if key.startswith('user/'): key = key.replace('user/', 'password/') host_config[key] = val just_these_be_configs[http_host] = host_config if be_paths: host_paths = just_these_webpaths.get(http_host, {}) host_config = just_these_be_configs.get(http_host, {}) rand_seed = '%s:%x' % (specs[specs.keys()[0]][BE_SECRET], time.time()/3600) first = (len(host_paths.keys()) == 0) or be_path_prefix paranoid = host_config.get('hide', False) set_root = host_config.get('root', True) if len(be_paths) == 1: skip = 0 else: skip = len(os.path.dirname(os.path.commonprefix(be_paths)+'X')) for path in be_paths: phead, ptail = os.path.split(path) if paranoid: if path.endswith('/'): path = path[0:-1] webpath = '%s/%s' % (sha1hex(rand_seed+os.path.dirname(path))[0:9], os.path.basename(path)) elif (first and set_root and os.path.isdir(path)): webpath = '' elif (os.path.isdir(path) and not path.startswith('.') and not os.path.isabs(path)): webpath = path[skip:] + '/' elif path == '.': webpath = '' else: webpath = path[skip:] while webpath.endswith('/.'): webpath = webpath[:-2] host_paths[(be_path_prefix + '/' + webpath).replace('///', '/' ).replace('//', '/') ] = (WEB_POLICY_DEFAULT, os.path.abspath(path)) first = False just_these_webpaths[http_host] = host_paths need_registration = {} for be in just_these_backends.values(): if not be[BE_SECRET]: if self.kitesecret and be[BE_DOMAIN] == self.kitename: be[BE_SECRET] = self.kitesecret else: need_registration[be[BE_DOMAIN]] = True for domain in need_registration: result = self.RegisterNewKite(kitename=domain) if not result: raise ConfigError(\"Not sure what to do with %s, giving up.\" % domain) # Update the secrets... rdom, rsecret = result for be in just_these_backends.values(): if be[BE_DOMAIN] == domain: be[BE_SECRET] = rsecret # Update the kite names themselves, if they changed. if rdom != domain: for bid in just_these_backends.keys(): nbid = bid.replace(':'+domain, ':'+rdom) if nbid != bid: just_these_backends[nbid] = just_these_backends[bid] just_these_backends[nbid][BE_DOMAIN] = rdom del just_these_backends[bid] if just_these_backends.keys(): if self.kite_add: self.backends.update(just_these_backends) elif self.kite_remove: for bid in just_these_backends: be = self.backends[bid] if be[BE_PROTO] in ('http', 'http2', 'http3'): http_host = '%s/%s' % (be[BE_DOMAIN], be[BE_PORT] or '80') if http_host in self.ui_paths: del self.ui_paths[http_host] if http_host in self.be_config: del self.be_config[http_host] del self.backends[bid] elif self.kite_disable: for bid in just_these_backends: self.backends[bid][BE_STATUS] = BE_STATUS_DISABLED elif self.kite_only: for be in self.backends.values(): be[BE_STATUS] = BE_STATUS_DISABLED self.backends.update(just_these_backends) else: # Nothing explictly requested: 'only' behavior with a twist; # If kites are new, don't make disables persist on save. for be in self.backends.values(): be[BE_STATUS] = (need_registration and BE_STATUS_DISABLE_ONCE or BE_STATUS_DISABLED) self.backends.update(just_these_backends) self.ui_paths.update(just_these_webpaths) self.be_config.update(just_these_be_configs) return self def GetServiceXmlRpc(self): service = self.service_xmlrpc if service == 'mock': return MockPageKiteXmlRpc(self) else: return xmlrpclib.ServerProxy(self.service_xmlrpc, None, None, False) def _KiteInfo(self, kitename): is_service_domain = kitename and SERVICE_DOMAIN_RE.search(kitename) is_subdomain_of = is_cname_for = is_cname_ready = False secret = None for be in self.backends.values(): if be[BE_SECRET] and (be[BE_DOMAIN] == kitename): secret = be[BE_SECRET] if is_service_domain: parts = kitename.split('.') if '-' in parts[0]: parts[0] = '-'.join(parts[0].split('-')[1:]) is_subdomain_of = '.'.join(parts) elif len(parts) > 3: is_subdomain_of = '.'.join(parts[1:]) elif kitename: try: (hn, al, ips) = socket.gethostbyname_ex(kitename) if hn != kitename and SERVICE_DOMAIN_RE.search(hn): is_cname_for = hn except: pass return (secret, is_subdomain_of, is_service_domain, is_cname_for, is_cname_ready) def RegisterNewKite(self, kitename=None, first=False, ask_be=False, autoconfigure=False): registered = False if kitename: (secret, is_subdomain_of, is_service_domain, is_cname_for, is_cname_ready) = self._KiteInfo(kitename) if secret: self.ui.StartWizard('Updating kite: %s' % kitename) registered = True else: self.ui.StartWizard('Creating kite: %s' % kitename) else: if first: self.ui.StartWizard('Create your first kite') else: self.ui.StartWizard('Creating a new kite') is_subdomain_of = is_service_domain = False is_cname_for = is_cname_ready = False # This is the default... be_specs = ['http:%s:localhost:80'] service = self.GetServiceXmlRpc() service_accounts = {} if self.kitename and self.kitesecret: service_accounts[self.kitename] = self.kitesecret for be in self.backends.values(): if SERVICE_DOMAIN_RE.search(be[BE_DOMAIN]): if be[BE_DOMAIN] == is_cname_for: is_cname_ready = True if be[BE_SECRET] not in service_accounts.values(): service_accounts[be[BE_DOMAIN]] = be[BE_SECRET] service_account_list = service_accounts.keys() if registered: state = ['choose_backends'] if service_account_list: state = ['choose_kite_account'] else: state = ['use_service_question'] history = [] def Goto(goto, back_skips_current=False): if not back_skips_current: history.append(state[0]) state[0] = goto def Back(): if history: state[0] = history.pop(-1) else: Goto('abort') register = is_cname_for or kitename account = email = None while 'end' not in state: try: if 'use_service_question' in state: ch = self.ui.AskYesNo('Use the service?', pre=['Welcome to PageKite!', '', 'Please answer a few quick questions to', 'create your first kite.', '', 'By continuing, you agree to play nice', 'and abide by the Terms of Service at:', '- %s' % (SERVICE_TOS_URL, SERVICE_TOS_URL)], default=True, back=-1, no='Abort') if ch is True: self.SetServiceDefaults(clobber=False) if not kitename: Goto('service_signup_email') elif is_cname_for and is_cname_ready: register = kitename Goto('service_signup_email') elif is_service_domain: register = is_cname_for or kitename if is_subdomain_of: # FIXME: Shut up if parent is already in local config! Goto('service_signup_is_subdomain') else: Goto('service_signup_email') else: Goto('service_signup_bad_domain') else: Goto('manual_abort') elif 'service_login_email' in state: p = None while not email or not p: (email, p) = self.ui.AskLogin('Please log on ...', pre=[ 'By logging on to %s,' % self.service_provider, 'you will be able to use this kite', 'with your pre-existing account.' ], email=email, back=(email, False)) if email and p: try: self.ui.Working('Logging on to your account') service_accounts[email] = service.getSharedSecret(email, p) # FIXME: Should get the list of preconfigured kites via. RPC # so we don't try to create something that already # exists? Or should the RPC not just not complain? account = email Goto('create_kite') except: email = p = None self.ui.Tell(['Login failed! Try again?'], error=True) if p is False: Back() break elif ('service_signup_is_subdomain' in state): ch = self.ui.AskYesNo('Use this name?', pre=['%s is a sub-domain.' % kitename, '', 'NOTE: This process will fail if you', 'have not already registered the parent', 'domain, %s.' % is_subdomain_of], default=True, back=-1) if ch is True: if account: Goto('create_kite') elif email: Goto('service_signup') else: Goto('service_signup_email') elif ch is False: Goto('service_signup_kitename') else: Back() elif ('service_signup_bad_domain' in state or 'service_login_bad_domain' in state): if is_cname_for: alternate = is_cname_for ch = self.ui.AskYesNo('Create both?', pre=['%s is a CNAME for %s.' % (kitename, is_cname_for)], default=True, back=-1) else: alternate = kitename.split('.')[-2]+'.'+SERVICE_DOMAINS[0] ch = self.ui.AskYesNo('Try to create %s instead?' % alternate, pre=['Sorry, %s is not a valid service domain.' % kitename], default=True, back=-1) if ch is True: register = alternate Goto(state[0].replace('bad_domain', 'email')) elif ch is False: register = alternate = kitename = False Goto('service_signup_kitename', back_skips_current=True) else: Back() elif 'service_signup_email' in state: email = self.ui.AskEmail('What is your e-mail address?', pre=['We need to be able to contact you', 'now and then with news about the', 'service and your account.', '', 'Your details will be kept private.'], back=False) if email and register: Goto('service_signup') elif email: Goto('service_signup_kitename') else: Back() elif ('service_signup_kitename' in state or 'service_ask_kitename' in state): try: self.ui.Working('Fetching list of available domains') domains = service.getAvailableDomains(None, None) except: domains = ['.%s' % x for x in SERVICE_DOMAINS] ch = self.ui.AskKiteName(domains, 'Name this kite:', pre=['Your kite name becomes the public name', 'of your personal server or web-site.', '', 'Names are provided on a first-come,', 'first-serve basis. You can create more', 'kites with different names later on.'], back=False) if ch: kitename = register = ch (secret, is_subdomain_of, is_service_domain, is_cname_for, is_cname_ready) = self._KiteInfo(ch) if secret: self.ui.StartWizard('Updating kite: %s' % kitename) registered = True else: self.ui.StartWizard('Creating kite: %s' % kitename) Goto('choose_backends') else: Back() elif 'choose_backends' in state: if ask_be and autoconfigure: skip = False ch = self.ui.AskBackends(kitename, ['http'], ['80'], [], 'Enable which service?', back=False, pre=[ 'You control which of your files or servers', 'PageKite exposes to the Internet. ', ], default=','.join(be_specs)) if ch: be_specs = ch.split(',') else: skip = ch = True if ch: if registered: Goto('create_kite', back_skips_current=skip) elif is_subdomain_of: Goto('service_signup_is_subdomain', back_skips_current=skip) elif account: Goto('create_kite', back_skips_current=skip) elif email: Goto('service_signup', back_skips_current=skip) else: Goto('service_signup_email', back_skips_current=skip) else: Back() elif 'service_signup' in state: try: self.ui.Working('Signing up') details = service.signUp(email, register) if details.get('secret', False): service_accounts[email] = details['secret'] self.ui.AskYesNo('Continue?', pre=[ 'Your kite is ready to fly!', '', 'Note: To complete the signup process,', 'check your e-mail (and spam folders) for', 'activation instructions. You can give', 'PageKite a try first, but un-activated', 'accounts are disabled after %d minutes.' % details['timeout'], ], yes='Finish', no=False, default=True) self.ui.EndWizard() if autoconfigure: print 'Backends: %s (register=%s)' % (be_specs, register) for be_spec in be_specs: self.backends.update(self.ArgToBackendSpecs( be_spec % register, secret=details['secret'])) self.added_kites = True return (register, details['secret']) else: error = details.get('error', 'unknown') except IOError: error = 'network' except: error = '%s' % (sys.exc_info(), ) if error == 'pleaselogin': #self.ui.ExplainError(error, # '%s log-in required.' % self.service_provider, # subject=register) Goto('service_login_email', back_skips_current=True) elif error == 'email': self.ui.ExplainError(error, 'Signup failed!', subject=register) Goto('service_login_email', back_skips_current=True) elif error in ('domain', 'domaintaken', 'subdomain'): register, kitename = None, None self.ui.ExplainError(error, 'Invalid domain!', subject=register) Goto('service_signup_kitename', back_skips_current=True) elif error == 'network': self.ui.ExplainError(error, 'Network error!', subject=self.service_provider) Goto('service_signup', back_skips_current=True) else: self.ui.ExplainError(error, 'Unknown problem!') print 'FIXME! Error is %s' % error Goto('abort') elif 'choose_kite_account' in state: choices = service_account_list[:] choices.append('Use another service provider') justdoit = (len(service_account_list) == 1) if justdoit: ch = 1 else: ch = self.ui.AskMultipleChoice(choices, 'Register with', pre=['Choose an account for this kite:'], default=1) account = choices[ch-1] if ch == len(choices): Goto('manual_abort') elif kitename: Goto('choose_backends', back_skips_current=justdoit) else: Goto('service_ask_kitename', back_skips_current=justdoit) elif 'create_kite' in state: secret = service_accounts[account] subject = None cfgs = {} result = {} error = None try: if registered and kitename and secret: pass elif is_cname_for and is_cname_ready: self.ui.Working('Creating your kite') subject = kitename result = service.addCnameKite(account, secret, kitename) time.sleep(2) # Give the service side a moment to replicate... else: self.ui.Working('Creating your kite') subject = register result = service.addKite(account, secret, register) time.sleep(2) # Give the service side a moment to replicate... for be_spec in be_specs: cfgs.update(self.ArgToBackendSpecs(be_spec % register, secret=secret)) if is_cname_for == register and 'error' not in result: subject = kitename result.update(service.addCnameKite(account, secret, kitename)) error = result.get('error', None) if not error: for be_spec in be_specs: cfgs.update(self.ArgToBackendSpecs(be_spec % kitename, secret=secret)) except Exception, e: error = '%s' % e if error: self.ui.ExplainError(error, 'Kite creation failed!', subject=subject) Goto('abort') else: self.ui.Tell(['Success!']) self.ui.EndWizard() if autoconfigure: self.backends.update(cfgs) self.added_kites = True return (register or kitename, secret) elif 'manual_abort' in state: if self.ui.Tell(['Aborted!', '', 'Please manually add information about your', 'kites and front-ends to the configuration file:', '', ' %s' % self.rcfile], error=True, back=False) is False: Back() else: self.ui.EndWizard() if self.ui.ALLOWS_INPUT: return None sys.exit(0) elif 'abort' in state: self.ui.EndWizard() if self.ui.ALLOWS_INPUT: return None sys.exit(0) else: raise ConfigError('Unknown state: %s' % state) except KeyboardInterrupt: sys.stderr.write('\\n') if history: Back() else: raise KeyboardInterrupt() self.ui.EndWizard() return None def CheckConfig(self): if self.ui_sspec: self.BindUiSspec() if not self.servers_manual and not self.servers_auto and not self.isfrontend: if not self.servers and not self.ui.ALLOWS_INPUT: raise ConfigError('Nothing to do! List some servers, or run me as one.') return self def CheckAllTunnels(self, conns): missing = [] for backend in self.backends: proto, domain = backend.split(':') if not conns.Tunnel(proto, domain): missing.append(domain) if missing: self.FallDown('No tunnel for %s' % missing, help=False) def Ping(self, host, port): if self.servers_no_ping: return 0 start = time.time() try: fd = rawsocket(socket.AF_INET, socket.SOCK_STREAM) try: fd.settimeout(2.0) # Missing in Python 2.2 except Exception: fd.setblocking(1) fd.connect((host, port)) fd.send('HEAD / HTTP/1.0\\r\\n\\r\\n') fd.recv(1024) fd.close() except Exception, e: LogDebug('Ping %s:%s failed: %s' % (host, port, e)) return 100000 elapsed = (time.time() - start) LogDebug('Pinged %s:%s: %f' % (host, port, elapsed)) return elapsed def GetHostIpAddr(self, host): return socket.gethostbyname(host) def GetHostDetails(self, host): return socket.gethostbyname_ex(host) def GetActiveBackends(self): active = [] for bid in self.backends: (proto, bdom) = bid.split(':') if (self.backends[bid][BE_STATUS] not in BE_INACTIVE and self.backends[bid][BE_SECRET] and not bdom.startswith('*')): active.append(bid) return active def ChooseFrontEnds(self): self.servers = [] self.servers_preferred = [] # Enable internal loopback if self.isfrontend: need_loopback = False for be in self.backends.values(): if be[BE_BHOST]: need_loopback = True if need_loopback: self.servers.append(LOOPBACK_FE) # Convert the hostnames into IP addresses... for server in self.servers_manual: (host, port) = server.split(':') try: ipaddr = self.GetHostIpAddr(host) server = '%s:%s' % (ipaddr, port) if server not in self.servers: self.servers.append(server) self.servers_preferred.append(ipaddr) except Exception, e: LogDebug('DNS lookup failed for %s' % host) # Lookup and choose from the auto-list (and our old domain). if self.servers_auto: (count, domain, port) = self.servers_auto # First, check for old addresses and always connect to those. if not self.servers_new_only: for bid in self.GetActiveBackends(): (proto, bdom) = bid.split(':') try: (hn, al, ips) = self.GetHostDetails(bdom) for ip in ips: if not ip.startswith('127.'): server = '%s:%s' % (ip, port) if server not in self.servers: self.servers.append(server) except Exception, e: LogDebug('DNS lookup failed for %s' % bdom) try: (hn, al, ips) = socket.gethostbyname_ex(domain) times = [self.Ping(ip, port) for ip in ips] except Exception, e: LogDebug('Unreachable: %s, %s' % (domain, e)) ips = times = [] while count > 0 and ips: count -= 1 mIdx = times.index(min(times)) server = '%s:%s' % (ips[mIdx], port) if server not in self.servers: self.servers.append(server) if ips[mIdx] not in self.servers_preferred: self.servers_preferred.append(ips[mIdx]) del times[mIdx] del ips[mIdx] def CreateTunnels(self, conns): live_servers = conns.TunnelServers() failures = 0 connections = 0 if len(self.GetActiveBackends()) > 0: if not self.servers or len(self.servers) > len(live_servers): self.ChooseFrontEnds() else: self.servers_preferred = [] self.servers = [] for server in self.servers: if server not in live_servers: if server == LOOPBACK_FE: LoopbackTunnel.Loop(conns, self.backends) else: self.ui.Status('connect', color=self.ui.YELLOW, message='Connecting to front-end: %s' % server) if Tunnel.BackEnd(server, self.backends, self.require_all, conns): Log([('connect', server)]) connections += 1 else: failures += 1 LogInfo('Failed to connect', [('FE', server)]) self.ui.Notify('Failed to connect to %s' % server, prefix='!', color=self.ui.YELLOW) if self.dyndns: updates = {} ddns_fmt, ddns_args = self.dyndns for bid in self.backends.keys(): proto, domain = bid.split(':') if bid in conns.tunnels: ips = [] bips = [] for tunnel in conns.tunnels[bid]: ip = tunnel.server_info[tunnel.S_NAME].split(':')[0] if not ip == LOOPBACK_HN: if not self.servers_preferred or ip in self.servers_preferred: ips.append(ip) else: bips.append(ip) if not ips: ips = bips if ips: iplist = ','.join(ips) payload = '%s:%s' % (domain, iplist) args = {} args.update(ddns_args) args.update({ 'domain': domain, 'ip': ips[0], 'ips': iplist, 'sign': signToken(secret=self.backends[bid][BE_SECRET], payload=payload, length=100) }) # FIXME: This may fail if different front-ends support different # protocols. In practice, this should be rare. update = ddns_fmt % args if domain not in updates or len(update) < len(updates[domain]): updates[payload] = update last_updates = self.last_updates self.last_updates = [] for update in updates: if update not in last_updates: try: self.ui.Status('dyndns', color=self.ui.YELLOW, message='Updating DNS...') result = ''.join(urllib.urlopen(updates[update]).readlines()) self.last_updates.append(update) if result.startswith('good') or result.startswith('nochg'): Log([('dyndns', result), ('data', update)]) self.SetBackendStatus(update.split(':')[0], sub=BE_STATUS_ERR_DNS) else: LogInfo('DynDNS update failed: %s' % result, [('data', update)]) self.SetBackendStatus(update.split(':')[0], add=BE_STATUS_ERR_DNS) failures += 1 except Exception, e: LogInfo('DynDNS update failed: %s' % e, [('data', update)]) if DEBUG_IO: traceback.print_exc(file=sys.stderr) self.SetBackendStatus(update.split(':')[0], add=BE_STATUS_ERR_DNS) failures += 1 if not self.last_updates: self.last_updates = last_updates return failures def LogTo(self, filename, close_all=True, dont_close=[]): global Log if filename == 'memory': Log = LogToMemory filename = self.devnull elif filename == 'syslog': Log = LogSyslog filename = self.devnull syslog.openlog(self.progname, syslog.LOG_PID, syslog.LOG_DAEMON) else: Log = LogToFile global LogFile if filename in ('stdio', 'stdout'): try: LogFile = os.fdopen(sys.stdout.fileno(), 'w', 0) except: LogFile = sys.stdout else: try: LogFile = fd = open(filename, \"a\", 0) os.dup2(fd.fileno(), sys.stdin.fileno()) os.dup2(fd.fileno(), sys.stdout.fileno()) if not self.ui.WANTS_STDERR: os.dup2(fd.fileno(), sys.stderr.fileno()) except Exception, e: raise ConfigError('%s' % e) def Daemonize(self): # Fork once... if os.fork() != 0: os._exit(0) # Fork twice... os.setsid() if os.fork() != 0: os._exit(0) def SelectLoop(self): global buffered_bytes conns = self.conns self.last_loop = time.time() iready, oready, eready = None, None, None while self.keep_looping: isocks, osocks = conns.Readable(), conns.Blocked() try: if isocks or osocks: iready, oready, eready = select.select(isocks, osocks, [], 1.1) else: # Windoes does not seem to like empty selects, so we do this instead. time.sleep(0.5) except KeyboardInterrupt, e: raise KeyboardInterrupt() except Exception, e: LogError('Error in select: %s (%s/%s)' % (e, isocks, osocks)) conns.CleanFds() self.last_loop -= 1 now = time.time() if not iready and not oready: if (isocks or osocks) and (now < self.last_loop + 1): LogError('Spinning, pausing ...') time.sleep(0.1) if oready: for socket in oready: conn = conns.Connection(socket) if conn and not conn.Send([], try_flush=True): # LogDebug(\"Write error in main loop, closing %s\" % conn) conns.Remove(conn) conn.Cleanup() if buffered_bytes < 1024 * self.buffer_max: throttle = None else: LogDebug(\"FIXME: Nasty pause to let buffers clear!\") time.sleep(0.1) throttle = 1024 if iready: for socket in iready: conn = conns.Connection(socket) if conn and not conn.ReadData(maxread=throttle): # LogDebug(\"Read error in main loop, closing %s\" % conn) conns.Remove(conn) conn.Cleanup() for conn in conns.DeadConns(): conns.Remove(conn) conn.Cleanup() self.last_loop = now def Loop(self): self.conns.start() if self.ui_httpd: self.ui_httpd.start() if self.tunnel_manager: self.tunnel_manager.start() if self.ui_comm: self.ui_comm.start() try: epoll = select.epoll() except Exception, msg: epoll = None if epoll: LogDebug(\"FIXME: Should try epoll!\") self.SelectLoop() def Start(self, howtoquit='CTRL+C = Quit'): conns = self.conns = self.conns or Connections(self) global Log # If we are going to spam stdout with ugly crap, then there is no point # attempting the fancy stuff. This also makes us backwards compatible # for the most part. if self.logfile == 'stdio': if not self.ui.DAEMON_FRIENDLY: self.ui = NullUi() # Announce that we've started up! self.ui.Status('startup', message='Starting up...') self.ui.Notify(('Hello! This is %s v%s.' ) % (self.progname, APPVER), prefix='>', color=self.ui.GREEN, alignright='[%s]' % howtoquit) config_report = [('started', sys.argv[0]), ('version', APPVER), ('platform', sys.platform), ('argv', ' '.join(sys.argv[1:])), ('ca_certs', self.ca_certs)] for optf in self.rcfiles_loaded: config_report.append(('optfile_%s' % optf, 'ok')) Log(config_report) if not socks.HAVE_SSL: self.ui.Notify('SECURITY WARNING: No SSL support was found, tunnels are insecure!', prefix='!', color=self.ui.WHITE) self.ui.Notify('Please install either pyOpenSSL or python-ssl.', prefix='!', color=self.ui.WHITE) # Create global secret self.ui.Status('startup', message='Collecting entropy for a secure secret...') LogInfo('Collecting entropy for a secure secret.') globalSecret() self.ui.Status('startup', message='Starting up...') # Create the UI Communicator self.ui_comm = UiCommunicator(self, conns) try: # Set up our listeners if we are a server. if self.isfrontend: self.ui.Notify('This is a PageKite front-end server.') for port in self.server_ports: Listener(self.server_host, port, conns) for port in self.server_raw_ports: if port != VIRTUAL_PN and port > 0: Listener(self.server_host, port, conns, connclass=RawConn) if self.ui_port: Listener('127.0.0.1', self.ui_port, conns, connclass=UiConn) # Create the Tunnel Manager self.tunnel_manager = TunnelManager(self, conns) except Exception, e: self.LogTo('stdio') FlushLogMemory() if DEBUG_IO: traceback.print_exc(file=sys.stderr) raise ConfigError('Configuring listeners: %s ' % e) # Configure logging if self.logfile: keep_open = [s.fd.fileno() for s in conns.conns] if self.ui_httpd: keep_open.append(self.ui_httpd.httpd.socket.fileno()) self.LogTo(self.logfile, dont_close=keep_open) elif not sys.stdout.isatty(): # Preserve sane behavior when not run at the console. self.LogTo('stdio') # Flush in-memory log, if necessary FlushLogMemory() # Set up SIGHUP handler. if self.logfile or self.reloadfile: try: import signal def reopen(x,y): if self.logfile: self.LogTo(self.logfile, close_all=False) LogDebug('SIGHUP received, reopening: %s' % self.logfile) if self.reloadfile: self.ConfigureFromFile(self.reloadfile) signal.signal(signal.SIGHUP, reopen) except Exception: LogError('Warning: signal handler unavailable, logrotate will not work.') # Disable compression in OpenSSL if socks.HAVE_SSL and not self.enable_sslzlib: socks.DisableSSLCompression() # Daemonize! if self.daemonize: self.Daemonize() # Create PID file if self.pidfile: pf = open(self.pidfile, 'w') pf.write('%s\\n' % os.getpid()) pf.close() # Do this after creating the PID and log-files. if self.daemonize: os.chdir('/') # Drop privileges, if we have any. if self.setgid: os.setgid(self.setgid) if self.setuid: os.setuid(self.setuid) if self.setuid or self.setgid: Log([('uid', os.getuid()), ('gid', os.getgid())]) # Make sure we have what we need if self.require_all: self.CreateTunnels(conns) self.CheckAllTunnels(conns) # Finally, run our select/epoll loop. self.Loop() self.ui.Status('exiting', message='Stopping...') Log([('stopping', 'pagekite.py')]) if self.ui_httpd: self.ui_httpd.quit() if self.ui_comm: self.ui_comm.quit() if self.tunnel_manager: self.tunnel_manager.quit() if self.conns: if self.conns.auth: self.conns.auth.quit() for conn in self.conns.conns: conn.Cleanup() ##[ Main ]##################################################################### def Main(pagekite, configure, uiclass=NullUi, progname=None, appver=APPVER, http_handler=None, http_server=None): crashes = 1 ui = uiclass() while True: pk = pagekite(ui=ui, http_handler=http_handler, http_server=http_server) try: try: try: configure(pk) except SystemExit, status: sys.exit(status) except Exception, e: raise ConfigError(e) pk.Start() except (ConfigError, getopt.GetoptError), msg: pk.FallDown(msg) except KeyboardInterrupt, msg: pk.FallDown(None, help=False, noexit=True) return except SystemExit, status: sys.exit(status) except Exception, msg: traceback.print_exc(file=sys.stderr) if pk.crash_report_url: try: print 'Submitting crash report to %s' % pk.crash_report_url LogDebug(''.join(urllib.urlopen(pk.crash_report_url, urllib.urlencode({ 'platform': sys.platform, 'appver': APPVER, 'crash': traceback.format_exc() })).readlines())) except Exception, e: print 'FAILED: %s' % e pk.FallDown(msg, help=False, noexit=pk.main_loop) # If we get this far, then we're looping. Clean up. sockets = pk.conns and pk.conns.Sockets() or [] for fd in sockets: fd.close() # Exponential fall-back. LogDebug('Restarting in %d seconds...' % (2 ** crashes)) time.sleep(2 ** crashes) crashes += 1 if crashes > 9: crashes = 9 # No exception, do we keep looping? if not pk.main_loop: return def Configure(pk): if '--appver' in sys.argv: print '%s' % APPVER sys.exit(0) if '--clean' not in sys.argv and '--help' not in sys.argv: if os.path.exists(pk.rcfile): pk.ConfigureFromFile() pk.Configure(sys.argv[1:]) if '--settings' in sys.argv: pk.PrintSettings(safe=True) sys.exit(0) if not pk.backends.keys() and (not pk.kitesecret or not pk.kitename): friendly_mode = (('--friendly' in sys.argv) or (sys.platform in ('win32', 'os2', 'os2emx', 'darwin', 'darwin1', 'darwin2'))) if '--signup' in sys.argv or friendly_mode: pk.RegisterNewKite(autoconfigure=True, first=True) if friendly_mode: pk.save = True pk.CheckConfig() if pk.added_kites: if (pk.autosave or pk.save or pk.ui.AskYesNo('Save settings to %s?' % pk.rcfile, default=(len(pk.backends.keys()) > 0))): pk.SaveUserConfig() pk.servers_new_only = 'Once' elif pk.save: pk.SaveUserConfig(quiet=True) if ('--list' in sys.argv or pk.kite_add or pk.kite_remove or pk.kite_only or pk.kite_disable): pk.ListKites() sys.exit(0) """ sys.modules["pagekite"] = imp.new_module("pagekite") sys.modules["pagekite"].open = __comb_open exec __FILES[".SELF/pagekite/__init__.py"] in sys.modules["pagekite"].__dict__ ############################################################################### __FILES[".SELF/pagekite/basicui.py"] = """\ import re, sys, time import pagekite from pagekite import NullUi HTML_BR_RE = re.compile(r'<(br|/p|/li|/tr|/h\\d)>\\s*') HTML_LI_RE = re.compile(r'
  • \\s*') HTML_NBSP_RE = re.compile(r' ') HTML_TAGS_RE = re.compile(r'<[^>\\s][^>]*>') def clean_html(text): return HTML_LI_RE.sub(' * ', HTML_NBSP_RE.sub('_', HTML_BR_RE.sub('\\n', text))) def Q(text): return HTML_TAGS_RE.sub('', clean_html(text)) class BasicUi(NullUi): \"\"\"Stdio based user interface.\"\"\" DAEMON_FRIENDLY = False WANTS_STDERR = True EMAIL_RE = re.compile(r'^[a-z0-9!#$%&\\'\\*\\+\\/=?^_`{|}~-]+' '(?:\\.[a-z0-9!#$%&\\'*+/=?^_`{|}~-]+)*@' '(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)*' '(?:[a-zA-Z]{2,4}|museum)$') def Notify(self, message, prefix=' ', popup=False, color=None, now=None, alignright=''): now = int(now or time.time()) color = color or self.NORM # We suppress duplicates that are either new or still on the screen. keys = self.notify_history.keys() if len(keys) > 20: for key in keys: if self.notify_history[key] < now-300: del self.notify_history[key] message = '%s' % message if message not in self.notify_history: # Display the time now and then. if (not alignright and (now >= (self.last_tick + 60)) and (len(message) < 68)): try: self.last_tick = now d = datetime.datetime.fromtimestamp(now) alignright = '[%2.2d:%2.2d]' % (d.hour, d.minute) except: pass # Fails on Python 2.2 self.notify_history[message] = now msg = '\\r%s %s%s%s%s%s\\n' % ((prefix * 3)[0:3], color, message, self.NORM, ' ' * (75-len(message)-len(alignright)), alignright) self.wfile.write(msg) self.Status(self.status_tag, self.status_msg) def NotifyMOTD(self, frontend, motd_message): self.Notify('Message of the day:', prefix=' ++', color=self.WHITE) lc = 1 for line in Q(motd_message).splitlines(): self.Notify((line.strip() or ' ' * (lc+2))) lc += 1 self.Notify(' ' * (lc+2), alignright='[from %s]' % frontend) def Status(self, tag, message=None, color=None): self.status_tag = tag self.status_col = color or self.status_col or self.NORM self.status_msg = '%s' % (message or self.status_msg) if not self.in_wizard: message = self.status_msg msg = ('\\r << pagekite.py [%s]%s %s%s%s\\r%s' ) % (tag, ' ' * (8-len(tag)), self.status_col, message, ' ' * (52-len(message)), self.NORM) self.wfile.write(msg) if tag == 'exiting': self.wfile.write('\\n') def Welcome(self, pre=None): if self.in_wizard: self.wfile.write('%s%s%s' % (self.CLEAR, self.WHITE, self.in_wizard)) if self.welcome: self.wfile.write('%s\\r%s\\n' % (self.NORM, Q(self.welcome))) self.welcome = None if self.in_wizard and self.wizard_tell: self.wfile.write('\\n%s\\r' % self.NORM) for line in self.wizard_tell: self.wfile.write('*** %s\\n' % Q(line)) self.wizard_tell = None if pre: self.wfile.write('\\n%s\\r' % self.NORM) for line in pre: self.wfile.write(' %s\\n' % Q(line)) self.wfile.write('\\n%s\\r' % self.NORM) def StartWizard(self, title): self.Welcome() banner = '>>> %s' % title banner = ('%s%s[CTRL+C = Cancel]\\n') % (banner, ' ' * (62-len(banner))) self.in_wizard = banner self.tries = 200 def Retry(self): self.tries -= 1 return self.tries def EndWizard(self): if self.wizard_tell: self.Welcome() self.in_wizard = None if sys.platform in ('win32', 'os2', 'os2emx'): self.wfile.write('\\n<<< press ENTER to continue >>>\\n') self.rfile.readline() def Spacer(self): self.wfile.write('\\n') def AskEmail(self, question, default=None, pre=[], wizard_hint=False, image=None, back=None, welcome=True): if welcome: self.Welcome(pre) while self.Retry(): self.wfile.write(' => %s ' % (Q(question), )) answer = self.rfile.readline().strip() if default and answer == '': return default if self.EMAIL_RE.match(answer): return answer if back is not None and answer == 'back': return back raise Exception('Too many tries') def AskLogin(self, question, default=None, email=None, pre=None, wizard_hint=False, image=None, back=None): self.Welcome(pre) def_email, def_pass = default or (email, None) self.wfile.write(' %s\\n' % (Q(question), )) if not email: email = self.AskEmail('Your e-mail:', default=def_email, back=back, welcome=False) if email == back: return back import getpass self.wfile.write(' => ') return (email, getpass.getpass() or def_pass) def AskYesNo(self, question, default=None, pre=[], yes='yes', no='no', wizard_hint=False, image=None, back=None): self.Welcome(pre) yn = ((default is True) and '[Y/n]' ) or ((default is False) and '[y/N]' ) or ('[y/n]') while self.Retry(): self.wfile.write(' => %s %s ' % (Q(question), yn)) answer = self.rfile.readline().strip().lower() if default is not None and answer == '': answer = default and 'y' or 'n' if back is not None and answer.startswith('b'): return back if answer in ('y', 'n'): return (answer == 'y') raise Exception('Too many tries') def AskKiteName(self, domains, question, pre=[], default=None, wizard_hint=False, image=None, back=None): self.Welcome(pre) if len(domains) == 1: self.wfile.write(('\\n (Note: the ending %s will be added for you.)' ) % domains[0]) else: self.wfile.write('\\n Please use one of the following domains:\\n') for domain in domains: self.wfile.write('\\n *%s' % domain) self.wfile.write('\\n') while self.Retry(): self.wfile.write('\\n => %s ' % Q(question)) answer = self.rfile.readline().strip().lower() if back is not None and answer == 'back': return back elif len(domains) == 1: answer = answer.replace(domains[0], '') if answer and pagekite.SERVICE_SUBDOMAIN_RE.match(answer): return answer+domains[0] else: for domain in domains: if answer.endswith(domain): answer = answer.replace(domain, '') if answer and pagekite.SERVICE_SUBDOMAIN_RE.match(answer): return answer+domain self.wfile.write(' (Please only use characters A-Z, 0-9, - and _.)') raise Exception('Too many tries') def AskMultipleChoice(self, choices, question, pre=[], default=None, wizard_hint=False, image=None, back=None): self.Welcome(pre) for i in range(0, len(choices)): self.wfile.write((' %s %d) %s\\n' ) % ((default==i+1) and '*' or ' ', i+1, choices[i])) self.wfile.write('\\n') while self.Retry(): d = default and (', default=%d' % default) or '' self.wfile.write(' => %s [1-%d%s] ' % (Q(question), len(choices), d)) try: answer = self.rfile.readline().strip() if back is not None and answer.startswith('b'): return back choice = int(answer or default) if choice > 0 and choice <= len(choices): return choice except (ValueError, IndexError): pass raise Exception('Too many tries') def Tell(self, lines, error=False, back=None): if self.in_wizard: self.wizard_tell = lines else: self.Welcome() for line in lines: self.wfile.write(' %s\\n' % line) if error: self.wfile.write('\\n') return True def Working(self, message): self.Tell([message]) """ sys.modules["pagekite.basicui"] = imp.new_module("pagekite.basicui") sys.modules["pagekite.basicui"].open = __comb_open sys.modules["pagekite"].basicui = sys.modules["pagekite.basicui"] exec __FILES[".SELF/pagekite/basicui.py"] in sys.modules["pagekite.basicui"].__dict__ ############################################################################### __FILES[".SELF/pagekite/remoteui.py"] = """\ import re, sys, time import pagekite from pagekite import NullUi class RemoteUi(NullUi): \"\"\"Stdio based user interface for interacting with other processes.\"\"\" DAEMON_FRIENDLY = True ALLOWS_INPUT = True WANTS_STDERR = True EMAIL_RE = re.compile(r'^[a-z0-9!#$%&\\'\\*\\+\\/=?^_`{|}~-]+' '(?:\\.[a-z0-9!#$%&\\'*+/=?^_`{|}~-]+)*@' '(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)*' '(?:[a-zA-Z]{2,4}|museum)$') def __init__(self, welcome=None, wfile=sys.stderr, rfile=sys.stdin): NullUi.__init__(self, welcome=welcome, wfile=wfile, rfile=rfile) self.CLEAR = '' self.NORM = self.WHITE = self.GREY = self.GREEN = self.YELLOW = '' self.BLUE = self.RED = self.MAGENTA = self.CYAN = '' def StartListingBackEnds(self): self.wfile.write('begin_be_list\\n') def EndListingBackEnds(self): self.wfile.write('end_be_list\\n') def NotifyBE(self, bid, be, has_ssl, dpaths, is_builtin=False, now=None): domain = be[pagekite.BE_DOMAIN] port = be[pagekite.BE_PORT] proto = be[pagekite.BE_PROTO] prox = (proto == 'raw') and ' (HTTP proxied)' or '' if proto == 'raw' and port in ('22', 22): proto = 'ssh' url = '%s://%s%s' % (proto, domain, port and (':%s' % port) or '') message = (' be_status:' ' status=%x; bid=%s; domain=%s; port=%s; proto=%s;' ' bhost=%s; bport=%s%s%s' '\\n') % (be[pagekite.BE_STATUS], bid, domain, port, proto, be[pagekite.BE_BHOST], be[pagekite.BE_BPORT], has_ssl and '; ssl=1' or '', is_builtin and '; builtin=1' or '') self.wfile.write(message) for path in dpaths: message = (' be_path: domain=%s; port=%s; path=%s; policy=%s; src=%s\\n' ) % (domain, port or 80, path, dpaths[path][0], dpaths[path][1]) self.wfile.write(message) def Notify(self, message, prefix=' ', popup=False, color=None, now=None, alignright=''): message = '%s' % message self.wfile.write('notify: %s\\n' % message) def NotifyMOTD(self, frontend, message): self.wfile.write('motd: %s %s\\n' % (frontend, message.replace('\\n', ' '))) def Status(self, tag, message=None, color=None): self.status_tag = tag self.status_msg = '%s' % (message or self.status_msg) if message: self.wfile.write('status_msg: %s\\n' % message) if tag: self.wfile.write('status_tag: %s\\n' % tag) def Welcome(self, pre=None): self.wfile.write('welcome: %s\\n' % (pre or '').replace('\\n', ' ')) def StartWizard(self, title): self.wfile.write('start_wizard: %s\\n' % title) def Retry(self): self.tries -= 1 if self.tries < 0: raise Exception('Too many tries') return self.tries def EndWizard(self): self.wfile.write('end_wizard: done\\n') def Spacer(self): pass def AskEmail(self, question, default=None, pre=[], wizard_hint=False, image=None, back=None, welcome=True): while self.Retry(): self.wfile.write('begin_ask_email\\n') if pre: self.wfile.write(' preamble: %s\\n' % '\\n'.join(pre).replace('\\n', ' ')) if default: self.wfile.write(' default: %s\\n' % default) self.wfile.write(' question: %s\\n' % (question or '').replace('\\n', ' ')) self.wfile.write(' expect: email\\n') self.wfile.write('end_ask_email\\n') answer = self.rfile.readline().strip() if self.EMAIL_RE.match(answer): return answer if back is not None and answer == 'back': return back def AskLogin(self, question, default=None, email=None, pre=None, wizard_hint=False, image=None, back=None): while self.Retry(): self.wfile.write('begin_ask_login\\n') if pre: self.wfile.write(' preamble: %s\\n' % '\\n'.join(pre).replace('\\n', ' ')) if email: self.wfile.write(' default: %s\\n' % email) self.wfile.write(' question: %s\\n' % (question or '').replace('\\n', ' ')) self.wfile.write(' expect: email\\n') self.wfile.write(' expect: password\\n') self.wfile.write('end_ask_login\\n') answer_email = self.rfile.readline().strip() if back is not None and answer_email == 'back': return back answer_pass = self.rfile.readline().strip() if back is not None and answer_pass == 'back': return back if self.EMAIL_RE.match(answer_email) and answer_pass: return (answer_email, answer_pass) def AskYesNo(self, question, default=None, pre=[], yes='Yes', no='No', wizard_hint=False, image=None, back=None): while self.Retry(): self.wfile.write('begin_ask_yesno\\n') if yes: self.wfile.write(' yes: %s\\n' % yes) if no: self.wfile.write(' no: %s\\n' % no) if pre: self.wfile.write(' preamble: %s\\n' % '\\n'.join(pre).replace('\\n', ' ')) if default: self.wfile.write(' default: %s\\n' % default) self.wfile.write(' question: %s\\n' % (question or '').replace('\\n', ' ')) self.wfile.write(' expect: yesno\\n') self.wfile.write('end_ask_yesno\\n') answer = self.rfile.readline().strip().lower() if back is not None and answer == 'back': return back if answer in ('y', 'n'): return (answer == 'y') if answer == str(default).lower(): return default def AskKiteName(self, domains, question, pre=[], default=None, wizard_hint=False, image=None, back=None): while self.Retry(): self.wfile.write('begin_ask_kitename\\n') if pre: self.wfile.write(' preamble: %s\\n' % '\\n'.join(pre).replace('\\n', ' ')) for domain in domains: self.wfile.write(' domain: %s\\n' % domain) if default: self.wfile.write(' default: %s\\n' % default) self.wfile.write(' question: %s\\n' % (question or '').replace('\\n', ' ')) self.wfile.write(' expect: kitename\\n') self.wfile.write('end_ask_kitename\\n') answer = self.rfile.readline().strip().lower() if back is not None and answer == 'back': return back if answer: for d in domains: if answer.endswith(d) or answer.endswith(d): return answer return answer+domains[0] def AskBackends(self, kitename, protos, ports, rawports, question, pre=[], default=None, wizard_hint=False, image=None, back=None): while self.Retry(): self.wfile.write('begin_ask_backends\\n') if pre: self.wfile.write(' preamble: %s\\n' % '\\n'.join(pre).replace('\\n', ' ')) count = 0 if self.server_info: protos = self.server_info[pagekite.Tunnel.S_PROTOS] ports = self.server_info[pagekite.Tunnel.S_PORTS] rawports = self.server_info[pagekite.Tunnel.S_RAW_PORTS] self.wfile.write(' kitename: %s\\n' % kitename) self.wfile.write(' protos: %s\\n' % ', '.join(protos)) self.wfile.write(' ports: %s\\n' % ', '.join(ports)) self.wfile.write(' rawports: %s\\n' % ', '.join(rawports)) if default: self.wfile.write(' default: %s\\n' % default) self.wfile.write(' question: %s\\n' % (question or '').replace('\\n', ' ')) self.wfile.write(' expect: backends\\n') self.wfile.write('end_ask_backends\\n') answer = self.rfile.readline().strip().lower() if back is not None and answer == 'back': return back return answer def AskMultipleChoice(self, choices, question, pre=[], default=None, wizard_hint=False, image=None, back=None): while self.Retry(): self.wfile.write('begin_ask_multiplechoice\\n') if pre: self.wfile.write(' preamble: %s\\n' % '\\n'.join(pre).replace('\\n', ' ')) count = 0 for choice in choices: count += 1 self.wfile.write(' choice_%d: %s\\n' % (count, choice)) if default: self.wfile.write(' default: %s\\n' % default) self.wfile.write(' question: %s\\n' % (question or '').replace('\\n', ' ')) self.wfile.write(' expect: choice_index\\n') self.wfile.write('end_ask_multiplechoice\\n') answer = self.rfile.readline().strip().lower() try: ch = int(answer) if ch > 0 and ch <= len(choices): return ch except: pass if back is not None and answer == 'back': return back def Tell(self, lines, error=False, back=None): dialog = error and 'error' or 'message' self.wfile.write('tell_%s: %s\\n' % (dialog, ' '.join(lines))) def Working(self, message): self.wfile.write('working: %s\\n' % message) """ sys.modules["pagekite.remoteui"] = imp.new_module("pagekite.remoteui") sys.modules["pagekite.remoteui"].open = __comb_open sys.modules["pagekite"].remoteui = sys.modules["pagekite.remoteui"] exec __FILES[".SELF/pagekite/remoteui.py"] in sys.modules["pagekite.remoteui"].__dict__ ############################################################################### __FILES[".SELF/pagekite/yamond.py"] = """\ #!/usr/bin/python -u # # yamond.py, Copyright 2010, The Beanstalks Project ehf. # http://beanstalks-project.net/ # # This is a class implementing a flexible metric-store and an HTTP # thread for browsing the numbers. # ############################################################################# # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # ############################################################################# # import getopt import os import random import re import select import socket import struct import sys import threading import time import traceback import urllib import BaseHTTPServer try: from urlparse import parse_qs, urlparse except Exception, e: from cgi import parse_qs from urlparse import urlparse class YamonRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): def do_yamon_vars(self): self.send_response(200) self.send_header('Content-Type', 'text/plain') self.send_header('Cache-Control', 'no-cache') self.end_headers() self.wfile.write(self.server.yamond.render_vars_text()) def do_404(self): self.send_response(404) self.send_header('Content-Type', 'text/html') self.end_headers() self.wfile.write('

    404: What? Where? Cannot find it!

    ') def do_root(self): self.send_response(200) self.send_header('Content-Type', 'text/html') self.end_headers() self.wfile.write('

    Hello!

    ') def handle_path(self, path, query): if path == '/vars.txt': self.do_yamon_vars() elif path == '/': self.do_root() else: self.do_404() def do_GET(self): (scheme, netloc, path, params, query, frag) = urlparse(self.path) qs = parse_qs(query) return self.handle_path(path, query) class YamonHttpServer(BaseHTTPServer.HTTPServer): def __init__(self, yamond, handler): BaseHTTPServer.HTTPServer.__init__(self, yamond.sspec, handler) self.yamond = yamond class YamonD(threading.Thread): \"\"\"Handle HTTP in a separate thread.\"\"\" def __init__(self, sspec, server=YamonHttpServer, handler=YamonRequestHandler): threading.Thread.__init__(self) self.server = server self.handler = handler self.sspec = sspec self.httpd = None self.running = False self.values = {} self.lists = {} def vmax(self, var, value): if value > self.values[var]: self.values[var] = value def vscale(self, var, ratio, add=0): if var not in self.values: self.values[var] = 0 self.values[var] *= ratio self.values[var] += add def vset(self, var, value): self.values[var] = value def vadd(self, var, value, wrap=None): if var not in self.values: self.values[var] = 0 self.values[var] += value if wrap is not None and self.values[var] >= wrap: self.values[var] -= wrap def vmin(self, var, value): if value < self.values[var]: self.values[var] = value def vdel(self, var): if var in self.values: del self.values[var] def lcreate(self, listn, elems): self.lists[listn] = [elems, 0, ['' for x in xrange(0, elems)]] def ladd(self, listn, value): list = self.lists[listn] list[2][list[1]] = value list[1] += 1 list[1] %= list[0] def render_vars_text(self): data = [] for var in self.values: data.append('%s: %s\\n' % (var, self.values[var])) for lname in self.lists: (elems, offset, list) = self.lists[lname] l = list[offset:] l.extend(list[:offset]) data.append('%s: %s\\n' % (lname, ' '.join(['%s' % x for x in l]))) return ''.join(data) def quit(self): if self.httpd: self.running = False urllib.urlopen('http://%s:%s/exiting/' % self.sspec, proxies={}).readlines() def run(self): self.httpd = self.server(self, self.handler) self.sspec = self.httpd.server_address self.running = True while self.running: self.httpd.handle_request() if __name__ == '__main__': yd = YamonD(('', 0)) yd.vset('bjarni', 100) yd.lcreate('foo', 2) yd.ladd('foo', 1) yd.ladd('foo', 2) yd.ladd('foo', 3) yd.run() """ sys.modules["pagekite.yamond"] = imp.new_module("pagekite.yamond") sys.modules["pagekite.yamond"].open = __comb_open sys.modules["pagekite"].yamond = sys.modules["pagekite.yamond"] exec __FILES[".SELF/pagekite/yamond.py"] in sys.modules["pagekite.yamond"].__dict__ ############################################################################### __FILES[".SELF/pagekite/httpd.py"] = """\ #!/usr/bin/python -u # # pagekite.py, Copyright 2010, 2011, the Beanstalks Project ehf. # and Bjarni Runar Einarsson # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # # ############################################################################### import base64 import cgi from cgi import escape as escape_html import os import re import socket import sys import tempfile import threading import time import traceback import urllib import SocketServer from CGIHTTPServer import CGIHTTPRequestHandler from SimpleXMLRPCServer import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler import Cookie import pagekite import sockschain as socks ##[ Conditional imports & compatibility magic! ]############################### try: import datetime ts_to_date = datetime.datetime.fromtimestamp except ImportError: ts_to_date = str try: sorted([1, 2, 3]) except: def sorted(l): tmp = l[:] tmp.sort() return tmp # Different Python 2.x versions complain about deprecation depending on # where we pull these from. try: from urlparse import parse_qs, urlparse except ImportError, e: from cgi import parse_qs from urlparse import urlparse try: import hashlib def sha1hex(data): hl = hashlib.sha1() hl.update(data) return hl.hexdigest().lower() except ImportError: import sha def sha1hex(data): return sha.new(data).hexdigest().lower() ##[ PageKite HTTPD code starts here! ]######################################### class AuthError(Exception): pass def fmt_size(count): if count > 2*(1024*1024*1024): return '%dGB' % (count / (1024*1024*1024)) if count > 2*(1024*1024): return '%dMB' % (count / (1024*1024)) if count > 2*(1024): return '%dKB' % (count / 1024) return '%dB' % count class CGIWrapper(CGIHTTPRequestHandler): def __init__(self, request, path_cgi): self.path = path_cgi self.cgi_info = (os.path.dirname(path_cgi), os.path.basename(path_cgi)) self.request = request self.server = request.server self.command = request.command self.headers = request.headers self.client_address = ('unknown', 0) self.rfile = request.rfile self.wfile = tempfile.TemporaryFile() def translate_path(self, path): return path def send_response(self, code, message): self.wfile.write('X-Response-Code: %s\\r\\n' % code) self.wfile.write('X-Response-Message: %s\\r\\n' % message) def send_error(self, code, message): return self.send_response(code, message) def Run(self): self.run_cgi() self.wfile.seek(0) return self.wfile class UiRequestHandler(SimpleXMLRPCRequestHandler): # Make all paths/endpoints legal, we interpret them below. rpc_paths = ( ) E403 = { 'code': '403', 'msg': 'Missing', 'mimetype': 'text/html', 'title': '403 Not found', 'body': '

    File or directory not found. Sorry!

    ' } E404 = { 'code': '404', 'msg': 'Not found', 'mimetype': 'text/html', 'title': '404 Not found', 'body': '

    File or directory not found. Sorry!

    ' } MIME_TYPES = { '3gp': 'video/3gpp', 'aac': 'audio/aac', 'atom': 'application/atom+xml', 'avi': 'video/avi', 'bmp': 'image/bmp', 'bz2': 'application/x-bzip2', 'c': 'text/plain', 'cpp': 'text/plain', 'css': 'text/css', 'conf': 'text/plain', 'cfg': 'text/plain', 'dtd': 'application/xml-dtd', 'doc': 'application/msword', 'gif': 'image/gif', 'gz': 'application/x-gzip', 'h': 'text/plain', 'hpp': 'text/plain', 'htm': 'text/html', 'html': 'text/html', 'hqx': 'application/mac-binhex40', 'java': 'text/plain', 'jar': 'application/java-archive', 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'js': 'application/javascript', 'json': 'application/json', 'jsonp': 'application/javascript', 'log': 'text/plain', 'md': 'text/plain', 'midi': 'audio/x-midi', 'mov': 'video/quicktime', 'mpeg': 'video/mpeg', 'mp2': 'audio/mpeg', 'mp3': 'audio/mpeg', 'm4v': 'video/mp4', 'mp4': 'video/mp4', 'm4a': 'audio/mp4', 'ogg': 'audio/vorbis', 'pdf': 'application/pdf', 'ps': 'application/postscript', 'pl': 'text/plain', 'png': 'image/png', 'ppt': 'application/vnd.ms-powerpoint', 'py': 'text/plain', 'pyw': 'text/plain', 'pk-shtml': 'text/html', 'pk-js': 'application/javascript', 'rc': 'text/plain', 'rtf': 'application/rtf', 'rss': 'application/rss+xml', 'sgml': 'text/sgml', 'sh': 'text/plain', 'shtml': 'text/plain', 'svg': 'image/svg+xml', 'swf': 'application/x-shockwave-flash', 'tar': 'application/x-tar', 'tgz': 'application/x-tar', 'tiff': 'image/tiff', 'txt': 'text/plain', 'wav': 'audio/wav', 'xml': 'application/xml', 'xls': 'application/vnd.ms-excel', 'xrdf': 'application/xrds+xml','zip': 'application/zip', 'DEFAULT': 'application/octet-stream' } TEMPLATE_RAW = ('%(body)s') TEMPLATE_JSONP = ('window.pkData = %s;') TEMPLATE_HTML = ('\\n' '\\n' '%(title)s - %(prog)s v%(ver)s\\n' '\\n' '

    %(title)s

    \\n' '
    %(body)s
    \\n' '\\n' '\\n') def setup(self): self.suppress_body = False if self.server.enable_ssl: self.connection = self.request self.rfile = socket._fileobject(self.request, \"rb\", self.rbufsize) self.wfile = socket._fileobject(self.request, \"wb\", self.wbufsize) else: SimpleXMLRPCRequestHandler.setup(self) def log_message(self, format, *args): pagekite.Log([('uireq', format % args)]) def send_header(self, header, value): self.wfile.write('%s: %s\\r\\n' % (header, value)) def end_headers(self): self.wfile.write('\\r\\n') def sendStdHdrs(self, header_list=[], cachectrl='private', mimetype='text/html'): if mimetype.startswith('text/') and ';' not in mimetype: mimetype += ('; charset=%s' % pagekite.DEFAULT_CHARSET) self.send_header('Cache-Control', cachectrl) self.send_header('Content-Type', mimetype) for header in header_list: self.send_header(header[0], header[1]) self.end_headers() def sendChunk(self, chunk): if self.chunked: if pagekite.DEBUG_IO: print '<== SENDING CHUNK ===\\n%s\\n' % chunk self.wfile.write('%x\\r\\n' % len(chunk)) self.wfile.write(chunk) self.wfile.write('\\r\\n') else: if pagekite.DEBUG_IO: print '<== SENDING ===\\n%s\\n' % chunk self.wfile.write(chunk) def sendEof(self): if self.chunked and not self.suppress_body: self.wfile.write('0\\r\\n\\r\\n') def sendResponse(self, message, code=200, msg='OK', mimetype='text/html', header_list=[], chunked=False, length=None): self.log_request(code, message and len(message) or '-') self.wfile.write('HTTP/1.1 %s %s\\r\\n' % (code, msg)) if code == 401: self.send_header('WWW-Authenticate', 'Basic realm=PK%d' % (time.time()/3600)) self.chunked = chunked if chunked: self.send_header('Transfer-Encoding', 'chunked') else: if length: self.send_header('Content-Length', length) elif not chunked: self.send_header('Content-Length', len(message or '')) self.sendStdHdrs(header_list=header_list, mimetype=mimetype) if message and not self.suppress_body: self.sendChunk(message) def needPassword(self): if self.server.pkite.ui_password: return True userkeys = [k for k in self.host_config.keys() if k.startswith('password/')] return userkeys def checkUsernamePasswordAuth(self, username, password): userkey = 'password/%s' % username if userkey in self.host_config: if self.host_config[userkey] == password: return if (self.server.pkite.ui_password and password == self.server.pkite.ui_password): return if self.needPassword(): raise AuthError(\"Invalid password\") def checkRequestAuth(self, scheme, netloc, path, qs): if self.needPassword(): raise AuthError(\"checkRequestAuth not implemented\") def checkPostAuth(self, scheme, netloc, path, qs, posted): if self.needPassword(): raise AuthError(\"checkPostAuth not implemented\") def performAuthChecks(self, scheme, netloc, path, qs): try: auth = self.headers.get('authorization') if auth: (how, ab64) = auth.strip().split() if how.lower() == 'basic': (username, password) = base64.decodestring(ab64).split(':') self.checkUsernamePasswordAuth(username, password) return True self.checkRequestAuth(scheme, netloc, path, qs) return True except (ValueError, KeyError, AuthError), e: pagekite.LogDebug('HTTP Auth failed: %s' % e) else: pagekite.LogDebug('HTTP Auth failed: Unauthorized') self.sendResponse('

    Unauthorized

    \\n', code=401, msg='Forbidden') return False def performPostAuthChecks(self, scheme, netloc, path, qs, posted): try: self.checkPostAuth(scheme, netloc, path, qs, posted) return True except AuthError: self.sendResponse('

    Unauthorized

    \\n', code=401, msg='Forbidden') return False def do_UNSUPPORTED(self): self.sendResponse('Unsupported request method.\\n', code=503, msg='Sorry', mimetype='text/plain') # Misc methods we don't support (yet) def do_OPTIONS(self): self.do_UNSUPPORTED() def do_DELETE(self): self.do_UNSUPPORTED() def do_PUT(self): self.do_UNSUPPORTED() def getHostInfo(self): http_host = self.headers.get('HOST', self.headers.get('host', 'unknown')) if http_host == 'unknown' or (http_host.startswith('localhost:') and http_host.replace(':', '/') not in self.server.pkite.be_config): http_host = None for bid in sorted(self.server.pkite.backends.keys()): be = self.server.pkite.backends[bid] if (be[pagekite.BE_BPORT] == self.server.pkite.ui_sspec[1] and be[pagekite.BE_STATUS] not in pagekite.BE_INACTIVE): http_host = '%s:%s' % (be[pagekite.BE_DOMAIN], be[pagekite.BE_PORT] or 80) if not http_host: if self.server.pkite.be_config.keys(): http_host = sorted(self.server.pkite.be_config.keys() )[0].replace('/', ':') else: http_host = 'unknown' self.http_host = http_host self.host_config = self.server.pkite.be_config.get((':' in http_host and http_host or http_host+':80' ).replace(':', '/'), {}) def do_GET(self, command='GET'): (scheme, netloc, path, params, query, frag) = urlparse(self.path) qs = parse_qs(query) self.getHostInfo() self.post_data = None self.command = command if not self.performAuthChecks(scheme, netloc, path, qs): return try: return self.handleHttpRequest(scheme, netloc, path, params, query, frag, qs, None) except Exception, e: pagekite.Log([('err', 'GET error at %s: %s' % (path, e))]) if pagekite.DEBUG_IO: print '=== ERROR\\n%s\\n===' % traceback.format_exc() self.sendResponse('

    Internal Error

    \\n', code=500, msg='Error') def do_HEAD(self): self.suppress_body = True self.do_GET(command='HEAD') def do_POST(self, command='POST'): (scheme, netloc, path, params, query, frag) = urlparse(self.path) qs = parse_qs(query) self.getHostInfo() self.command = command if not self.performAuthChecks(scheme, netloc, path, qs): return posted = None self.post_data = tempfile.TemporaryFile() self.old_rfile = self.rfile try: # First, buffer the POST data to a file... clength = cleft = int(self.headers.get('content-length')) while cleft > 0: rbytes = min(64*1024, cleft) self.post_data.write(self.rfile.read(rbytes)) cleft -= rbytes # Juggle things so the buffering is invisble. self.post_data.seek(0) self.rfile = self.post_data ctype, pdict = cgi.parse_header(self.headers.get('content-type')) if ctype == 'multipart/form-data': self.post_data.seek(0) posted = cgi.parse_multipart(self.rfile, pdict) elif ctype == 'application/x-www-form-urlencoded': if clength >= 50*1024*1024: raise Exception((\"Refusing to parse giant posted query \" \"string (%s bytes).\") % clength) posted = cgi.parse_qs(self.rfile.read(clength), 1) elif self.host_config.get('xmlrpc', False): # We wrap the XMLRPC request handler in _BEGIN/_END in order to # expose the request environment to the RPC functions. RCI = self.server.RCI return RCI._END(SimpleXMLRPCRequestHandler.do_POST(RCI._BEGIN(self))) self.post_data.seek(0) except Exception, e: pagekite.Log([('err', 'POST error at %s: %s' % (path, e))]) self.sendResponse('

    Internal Error

    \\n', code=500, msg='Error') self.rfile = self.old_rfile self.post_data = None return if not self.performPostAuthChecks(scheme, netloc, path, qs, posted): return try: return self.handleHttpRequest(scheme, netloc, path, params, query, frag, qs, posted) except Exception, e: pagekite.Log([('err', 'POST error at %s: %s' % (path, e))]) self.sendResponse('

    Internal Error

    \\n', code=500, msg='Error') self.rfile = self.old_rfile self.post_data = None def openCGI(self, full_path, path, shtml_vars): cgi_file = CGIWrapper(self, full_path).Run() lines = cgi_file.read(32*1024).splitlines(True) if '\\r\\n' in lines: lines = lines[0:lines.index('\\r\\n')+1] elif '\\n' in lines: lines = lines[0:lines.index('\\n')+1] else: lines.append('') header_list = [] response_code = 200 response_message = 'OK' response_mimetype = 'text/html' for line in lines[:-1]: key, val = line.strip().split(': ', 1) if key == 'X-Response-Code': response_code = val elif key == 'X-Response-Message': response_message = val elif key.lower() == 'content-type': response_mimetype = val elif key.lower() == 'location': response_code = 302 header_list.append((key, val)) else: header_list.append((key, val)) self.sendResponse(None, code=response_code, msg=response_message, mimetype=response_mimetype, chunked=True, header_list=header_list) cgi_file.seek(sum([len(l) for l in lines])) return cgi_file def renderIndex(self, full_path, files=None): files = files or [(f, os.path.join(full_path, f)) for f in sorted(os.listdir(full_path))] # Remove dot-files and PageKite metadata files if self.host_config.get('indexes') != pagekite.WEB_INDEX_ALL: files = [f for f in files if not (f[0].startswith('.') or f[0].startswith('_pagekite'))] fhtml = [''] if files: for (fn, fpath) in files: fmimetype = self.getMimeType(fn) try: fsize = os.path.getsize(fpath) or '' except OSError: fsize = 0 ops = [ ] if os.path.isdir(fpath): fclass = ['dir'] if not fn.endswith('/'): fn += '/' qfn = urllib.quote(fn) else: qfn = urllib.quote(fn) fn = os.path.basename(fn) fclass = ['file'] ops.append('download') if (fmimetype.startswith('text/') or (fmimetype == 'application/octet-stream' and fsize < 512000)): ops.append('view') (unused, ext) = os.path.splitext(fn) if ext: fclass.append(ext.replace('.', 'ext_')) fclass.append('mime_%s' % fmimetype.replace('/', '_')) ophtml = ', '.join([('%s' ) % (op, qfn, op, qfn, op) for op in sorted(ops)]) try: mtime = full_path and int(os.path.getmtime(fpath) or time.time()) except OSError: mtime = int(time.time()) fhtml.append(('' '' '' '' '' '' ) % (' '.join(fclass), ophtml, fsize, str(ts_to_date(mtime)), qfn, fn.replace('<', '<'), )) else: fhtml.append('') fhtml.append('
    %s%s%s%s
    empty
    ') return ''.join(fhtml) def sendStaticPath(self, path, mimetype, shtml_vars=None): pkite = self.server.pkite is_shtml, is_cgi, is_dir = False, False, False index_list = None try: path = urllib.unquote(path) if path.find('..') >= 0: raise IOError(\"Evil\") paths = pkite.ui_paths def_paths = paths.get('*', {}) http_host = self.http_host if ':' not in http_host: http_host += ':80' host_paths = paths.get(http_host.replace(':', '/'), {}) path_parts = path.split('/') path_rest = [] full_path = '' root_path = '' while len(path_parts) > 0 and not full_path: pf = '/'.join(path_parts) pd = pf+'/' m = None if pf in host_paths: m = host_paths[pf] elif pd in host_paths: m = host_paths[pd] elif pf in def_paths: m = def_paths[pf] elif pd in def_paths: m = def_paths[pd] if m: policy = m[0] root_path = m[1] full_path = os.path.join(root_path, *path_rest) else: path_rest.insert(0, path_parts.pop()) if full_path: is_dir = os.path.isdir(full_path) else: if not self.host_config.get('indexes', False): return False if self.host_config.get('hide', False): return False # Generate pseudo-index ipath = path if not ipath.endswith('/'): ipath += '/' plen = len(ipath) index_list = [(p[plen:], host_paths[p][1]) for p in sorted(host_paths.keys()) if p.startswith(ipath)] if not index_list: return False full_path = '' mimetype = 'text/html' is_dir = True if is_dir and not path.endswith('/'): self.sendResponse('\\n', code=302, msg='Moved', header_list=[ ('Location', '%s/' % path) ]) return True indexes = ['index.html', 'index.htm', '_pagekite.html'] dynamic_suffixes = [] if self.host_config.get('pk-shtml'): indexes[0:0] = ['index.pk-shtml'] dynamic_suffixes = ['.pk-shtml', '.pk-js'] cgi_suffixes = [] cgi_config = self.host_config.get('cgi', False) if cgi_config: if cgi_config == True: cgi_config = 'cgi' for suffix in cgi_config.split(','): indexes[0:0] = ['index.%s' % suffix] cgi_suffixes.append('.%s' % suffix) for index in indexes: ipath = os.path.join(full_path, index) if os.path.exists(ipath): mimetype = 'text/html' full_path = ipath is_dir = False break self.chunked = False rf_stat = rf_size = None if full_path: if is_dir: mimetype = 'text/html' rf_size = rf = None rf_stat = os.stat(full_path) else: for s in dynamic_suffixes: if full_path.endswith(s): is_shtml = True for s in cgi_suffixes: if full_path.endswith(s): is_cgi = True if not is_shtml and not is_cgi: shtml_vars = None rf = open(full_path, \"rb\") try: rf_stat = os.fstat(rf.fileno()) rf_size = rf_stat.st_size except: self.chunked = True except (IOError, OSError), e: return False headers = [ ] if rf_stat and not (is_dir or is_shtml or is_cgi): # ETags for static content: we trust the file-system. etag = sha1hex(':'.join(['%s' % s for s in [full_path, rf_stat.st_mode, rf_stat.st_ino, rf_stat.st_dev, rf_stat.st_nlink, rf_stat.st_uid, rf_stat.st_gid, rf_stat.st_size, int(rf_stat.st_mtime), int(rf_stat.st_ctime)]]))[0:24] if etag == self.headers.get('if-none-match', None): rf.close() self.sendResponse('', code=304, msg='Not Modified', mimetype=mimetype) return True else: headers.append(('ETag', etag)) # FIXME: Support ranges for resuming aborted transfers? if is_cgi: self.chunked = True rf = self.openCGI(full_path, path, shtml_vars) else: self.sendResponse(None, mimetype=mimetype, length=rf_size, chunked=self.chunked or (shtml_vars is not None), header_list=headers) chunk_size = (is_shtml and 1024 or 16) * 1024 if rf: while not self.suppress_body: data = rf.read(chunk_size) if data == \"\": break if is_shtml and shtml_vars: self.sendChunk(data % shtml_vars) else: self.sendChunk(data) rf.close() elif shtml_vars and not self.suppress_body: shtml_vars['title'] = '//%s%s' % (shtml_vars['http_host'], path) if self.host_config.get('indexes') in (True, pagekite.WEB_INDEX_ON, pagekite.WEB_INDEX_ALL): shtml_vars['body'] = self.renderIndex(full_path, files=index_list) else: shtml_vars['body'] = ('

    Directory listings disabled and ' 'index.html not found.

    ') self.sendChunk(self.TEMPLATE_HTML % shtml_vars) self.sendEof() return True def getMimeType(self, path): try: ext = path.split('.')[-1].lower() except IndexError: ext = 'DIRECTORY' if ext in self.MIME_TYPES: return self.MIME_TYPES[ext] return self.MIME_TYPES['DEFAULT'] def add_kite(self, path, qs): if path.find(self.server.secret) == -1: return {'mimetype': 'text/plain', 'body': 'Invalid secret'} pass def handleHttpRequest(self, scheme, netloc, path, params, query, frag, qs, posted): data = { 'prog': self.server.pkite.progname, 'mimetype': self.getMimeType(path), 'hostname': socket.gethostname() or 'Your Computer', 'http_host': self.http_host, 'query_string': query, 'code': 200, 'body': '', 'msg': 'OK', 'now': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), 'ver': pagekite.APPVER } for key in self.headers.keys(): data['http_'+key.lower()] = self.headers.get(key) if 'download' in qs: data['mimetype'] = 'application/octet-stream' # Would be nice to set Content-Disposition too. elif 'view' in qs: data['mimetype'] = 'text/plain' data['method'] = data.get('http_x-pagekite-proto', 'http').lower() if 'http_cookie' in data: cookies = Cookie.SimpleCookie(data['http_cookie']) else: cookies = {} # Do we expose the built-in console? console = self.host_config.get('console', False) if path == self.host_config.get('yamon', False): data['body'] = pagekite.gYamon.render_vars_text() elif console and path.startswith('/_pagekite/logout/'): parts = path.split('/') location = parts[3] or ('%s://%s/' % (data['method'], data['http_host'])) self.sendResponse('\\n', code=302, msg='Moved', header_list=[ ('Set-Cookie', 'pkite_token=; path=/'), ('Location', location) ]) return elif console and path.startswith('/_pagekite/login/'): parts = path.split('/', 4) token = parts[3] location = parts[4] or ('%s://%s/_pagekite/' % (data['method'], data['http_host'])) if query: location += '?' + query if token == self.server.secret: self.sendResponse('\\n', code=302, msg='Moved', header_list=[ ('Set-Cookie', 'pkite_token=%s; path=/' % token), ('Location', location) ]) return else: pagekite.LogDebug(\"Invalid token, %s != %s\" % (token, self.server.secret)) data.update(self.E404) elif console and path.startswith('/_pagekite/'): if not ('pkite_token' in cookies and cookies['pkite_token'].value == self.server.secret): self.sendResponse('

    Forbidden

    \\n', code=403, msg='Forbidden') return if path == '/_pagekite/': if not self.sendStaticPath('%s/control.pk-shtml' % console, 'text/html', shtml_vars=data): self.sendResponse('

    Not found

    \\n', code=404, msg='Missing') return elif path.startswith('/_pagekite/quitquitquit/'): self.sendResponse('

    Kaboom

    \\n', code=500, msg='Asplode') self.wfile.flush() os._exit(2) elif path.startswith('/_pagekite/add_kite/'): data.update(self.add_kite(path, qs)) elif path.endswith('/pagekite.rc'): data.update({'mimetype': 'application/octet-stream', 'body': '\\n'.join(self.server.pkite.GenerateConfig())}) elif path.endswith('/pagekite.rc.txt'): data.update({'mimetype': 'text/plain', 'body': '\\n'.join(self.server.pkite.GenerateConfig())}) elif path.endswith('/pagekite.cfg'): data.update({'mimetype': 'application/octet-stream', 'body': '\\r\\n'.join(self.server.pkite.GenerateConfig())}) else: data.update(self.E403) else: if self.sendStaticPath(path, data['mimetype'], shtml_vars=data): return data.update(self.E404) if data['mimetype'] in ('application/octet-stream', 'text/plain'): response = self.TEMPLATE_RAW % data elif path.endswith('.jsonp'): response = self.TEMPLATE_JSONP % (data, ) else: response = self.TEMPLATE_HTML % data self.sendResponse(response, msg=data['msg'], code=data['code'], mimetype=data['mimetype'], chunked=False) self.sendEof() class RemoteControlInterface(object): ACL_OPEN = '' ACL_READ = 'r' ACL_WRITE = 'w' def __init__(self, httpd, pkite, conns, yamon): self.httpd = httpd self.pkite = pkite self.conns = conns self.yamon = yamon self.modified = False self.lock = threading.Lock() self.request = None # For now, nobody gets ACL_WRITE self.auth_tokens = {httpd.secret: self.ACL_READ} # Channels are in-memory logs which can be tailed over XML-RPC. # Javascript apps can create these for implementing chat etc. self.channels = {'LOG': {'access': self.ACL_READ, 'tokens': self.auth_tokens, 'data': pagekite.LOG}} def _BEGIN(self, request_object): self.lock.acquire() self.request = request_object return request_object def _END(self, rv=None): if self.request: self.request = None self.lock.release() return rv def connections(self, auth_token): if (not self.request.host_config.get('console', False) or self.ACL_READ not in self.auth_tokens.get(auth_token, self.ACL_OPEN)): raise AuthError('Unauthorized') return [{'sid': c.sid, 'dead': c.dead, 'html': c.__html__()} for c in self.conns.conns] def add_kite(self, auth_token, kite_domain, kite_proto): if (not self.request.host_config.get('console', False) or self.ACL_WRITE not in self.auth_tokens.get(auth_token, self.ACL_OPEN)): raise AuthError('Unauthorized') pass def get_kites(self, auth_token): if (not self.request.host_config.get('console', False) or self.ACL_READ not in self.auth_tokens.get(auth_token, self.ACL_OPEN)): raise AuthError('Unauthorized') kites = [] for bid in self.pkite.backends: proto, domain = bid.split(':') fe_proto = proto.split('-') kite_info = { 'id': bid, 'domain': domain, 'fe_proto': fe_proto[0], 'fe_port': (len(fe_proto) > 1) and fe_proto[1] or '', 'fe_secret': self.pkite.backends[bid][BE_SECRET], 'be_proto': self.pkite.backends[bid][BE_PROTO], 'backend': self.pkite.backends[bid][BE_BACKEND], 'fe_list': [{'name': fe.server_name, 'tls': fe.using_tls, 'sid': fe.sid} for fe in self.conns.Tunnel(proto, domain)] } kites.append(kite_info) return kites def add_kite(self, auth_token, proto, fe_port, fe_domain, be_port, be_domain, shared_secret): if (not self.request.host_config.get('console', False) or self.ACL_WRITE not in self.auth_tokens.get(auth_token, self.ACL_OPEN)): raise AuthError('Unauthorized') # FIXME def remove_kite(self, auth_token, kite_id): if (not self.request.host_config.get('console', False) or self.ACL_WRITE not in self.auth_tokens.get(auth_token, self.ACL_OPEN)): raise AuthError('Unauthorized') if kite_id in self.pkite.backends: del self.pkite.backends[kite_id] pagekite.Log([('reconfigured', '1'), ('removed', kite_id)]) self.modified = True return self.get_kites(auth_token) def mk_channel(self, auth_token, channel): if not self.request.host_config.get('channels', False): raise AuthError('Unauthorized') chid = '%s/%s' % (self.request.http_host, channel) if chid in self.channels: raise Error('Exists') else: self.channels[chid] = {'access': self.ACL_WRITE, 'tokens': {auth_token: self.ACL_WRITE}, 'data': []} return self.append_channel(auth_token, channel, {'created': channel}) def get_channel(self, auth_token, channel): if not self.request.host_config.get('channels', False): raise AuthError('Unauthorized') chan = self.channels.get('%s/%s' % (self.request.http_host, channel), self.channels.get(channel, {})) req = chan.get('access', self.ACL_WRITE) if req not in chan.get('tokens', self.auth_tokens).get(auth_token, self.ACL_OPEN): raise AuthError('Unauthorized') return chan.get('data', []) def append_channel(self, auth_token, channel, values): data = self.get_channel(auth_token, channel) global LOG_LINE values.update({'ts': '%x' % time.time(), 'll': '%x' % LOG_LINE}) LOG_LINE += 1 data.append(values) return values def get_channel_after(self, auth_token, channel, last_seen, timeout): data = self.get_channel(auth_token, channel) last_seen = int(last_seen, 16) # line at the remote end, then we've restarted and should send everything. if (last_seen == 0) or (LOG_LINE < last_seen): return data # FIXME: LOG_LINE global for all channels? Is that suck? # We are about to get sleepy, so release our environment lock. self._END() # If our internal LOG_LINE counter is less than the count of the last seen # Else, wait at least one second, AND wait for a new line to be added to # the log (or the timeout to expire). time.sleep(1) last_ll = data[-1]['ll'] while (timeout > 0) and (data[-1]['ll'] == last_ll): time.sleep(1) timeout -= 1 # Return everything the client hasn't already seen. return [ll for ll in data if int(ll['ll'], 16) > last_seen] class UiHttpServer(SocketServer.ThreadingMixIn, SimpleXMLRPCServer): def __init__(self, sspec, pkite, conns, handler=UiRequestHandler, ssl_pem_filename=None): SimpleXMLRPCServer.__init__(self, sspec, handler) self.pkite = pkite self.conns = conns self.secret = pkite.ConfigSecret() self.server_name = sspec[0] self.server_port = sspec[1] if ssl_pem_filename: ctx = pagekite.SSL.Context(pagekite.SSL.SSLv3_METHOD) ctx.use_privatekey_file (ssl_pem_filename) ctx.use_certificate_chain_file(ssl_pem_filename) self.socket = pagekite.SSL_Connect(ctx, socket.socket(self.address_family, self.socket_type), server_side=True) self.server_bind() self.server_activate() self.enable_ssl = True else: self.enable_ssl = False try: from pagekite import yamond pagekite.YamonD = yamond.YamonD except: pass gYamon = pagekite.gYamon = pagekite.YamonD(sspec) gYamon.vset('started', int(time.time())) gYamon.vset('version', pagekite.APPVER) gYamon.vset('httpd_ssl_enabled', self.enable_ssl) gYamon.vset('errors', 0) gYamon.vset(\"bytes_all\", 0) self.RCI = RemoteControlInterface(self, pkite, conns, gYamon) self.register_introspection_functions() self.register_instance(self.RCI) """ sys.modules["pagekite.httpd"] = imp.new_module("pagekite.httpd") sys.modules["pagekite.httpd"].open = __comb_open sys.modules["pagekite"].httpd = sys.modules["pagekite.httpd"] exec __FILES[".SELF/pagekite/httpd.py"] in sys.modules["pagekite.httpd"].__dict__ ############################################################################### #!/usr/bin/python import sys import pagekite as pk import pagekite.httpd as httpd if __name__ == "__main__": if sys.stdout.isatty(): import pagekite.basicui uiclass = pagekite.basicui.BasicUi else: uiclass = pk.NullUi pk.Main(pk.PageKite, pk.Configure, uiclass=uiclass, http_handler=httpd.UiRequestHandler, http_server=httpd.UiHttpServer) ############################################################################## CERTS="""\ StartCom Ltd. ============= -----BEGIN CERTIFICATE----- MIIFFjCCBH+gAwIBAgIBADANBgkqhkiG9w0BAQQFADCBsDELMAkGA1UEBhMCSUwxDzANBgNVBAgT BklzcmFlbDEOMAwGA1UEBxMFRWlsYXQxFjAUBgNVBAoTDVN0YXJ0Q29tIEx0ZC4xGjAYBgNVBAsT EUNBIEF1dGhvcml0eSBEZXAuMSkwJwYDVQQDEyBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1dGhv cml0eTEhMB8GCSqGSIb3DQEJARYSYWRtaW5Ac3RhcnRjb20ub3JnMB4XDTA1MDMxNzE3Mzc0OFoX DTM1MDMxMDE3Mzc0OFowgbAxCzAJBgNVBAYTAklMMQ8wDQYDVQQIEwZJc3JhZWwxDjAMBgNVBAcT BUVpbGF0MRYwFAYDVQQKEw1TdGFydENvbSBMdGQuMRowGAYDVQQLExFDQSBBdXRob3JpdHkgRGVw LjEpMCcGA1UEAxMgRnJlZSBTU0wgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxITAfBgkqhkiG9w0B CQEWEmFkbWluQHN0YXJ0Y29tLm9yZzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA7YRgACOe yEpRKSfeOqE5tWmrCbIvNP1h3D3TsM+x18LEwrHkllbEvqoUDufMOlDIOmKdw6OsWXuO7lUaHEe+ o5c5s7XvIywI6Nivcy+5yYPo7QAPyHWlLzRMGOh2iCNJitu27Wjaw7ViKUylS7eYtAkUEKD4/mJ2 IhULpNYILzUCAwEAAaOCAjwwggI4MA8GA1UdEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgHmMB0GA1Ud DgQWBBQcicOWzL3+MtUNjIExtpidjShkjTCB3QYDVR0jBIHVMIHSgBQcicOWzL3+MtUNjIExtpid jShkjaGBtqSBszCBsDELMAkGA1UEBhMCSUwxDzANBgNVBAgTBklzcmFlbDEOMAwGA1UEBxMFRWls YXQxFjAUBgNVBAoTDVN0YXJ0Q29tIEx0ZC4xGjAYBgNVBAsTEUNBIEF1dGhvcml0eSBEZXAuMSkw JwYDVQQDEyBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEhMB8GCSqGSIb3DQEJARYS YWRtaW5Ac3RhcnRjb20ub3JnggEAMB0GA1UdEQQWMBSBEmFkbWluQHN0YXJ0Y29tLm9yZzAdBgNV HRIEFjAUgRJhZG1pbkBzdGFydGNvbS5vcmcwEQYJYIZIAYb4QgEBBAQDAgAHMC8GCWCGSAGG+EIB DQQiFiBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAyBglghkgBhvhCAQQEJRYjaHR0 cDovL2NlcnQuc3RhcnRjb20ub3JnL2NhLWNybC5jcmwwKAYJYIZIAYb4QgECBBsWGWh0dHA6Ly9j ZXJ0LnN0YXJ0Y29tLm9yZy8wOQYJYIZIAYb4QgEIBCwWKmh0dHA6Ly9jZXJ0LnN0YXJ0Y29tLm9y Zy9pbmRleC5waHA/YXBwPTExMTANBgkqhkiG9w0BAQQFAAOBgQBscSXhnjSRIe/bbL0BCFaPiNhB OlP1ct8nV0t2hPdopP7rPwl+KLhX6h/BquL/lp9JmeaylXOWxkjHXo0Hclb4g4+fd68p00UOpO6w NnQt8M2YI3s3S9r+UZjEHjQ8iP2ZO1CnwYszx8JSFhKVU2Ui77qLzmLbcCOxgN8aIDjnfg== -----END CERTIFICATE----- StartCom Certification Authority ================================ -----BEGIN CERTIFICATE----- MIIHyTCCBbGgAwIBAgIBATANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJJTDEWMBQGA1UEChMN U3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmlu ZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDYwOTE3MTk0 NjM2WhcNMzYwOTE3MTk0NjM2WjB9MQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRk LjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMg U3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw ggIKAoICAQDBiNsJvGxGfHiflXu1M5DycmLWwTYgIiRezul38kMKogZkpMyONvg45iPwbm2xPN1y o4UcodM9tDMr0y+v/uqwQVlntsQGfQqedIXWeUyAN3rfOQVSWff0G0ZDpNKFhdLDcfN1YjS6LIp/ Ho/u7TTQEceWzVI9ujPW3U3eCztKS5/CJi/6tRYccjV3yjxd5srhJosaNnZcAdt0FCX+7bWgiA/d eMotHweXMAEtcnn6RtYTKqi5pquDSR3l8u/d5AGOGAqPY1MWhWKpDhk6zLVmpsJrdAfkK+F2PrRt 2PZE4XNiHzvEvqBTViVsUQn3qqvKv3b9bZvzndu/PWa8DFaqr5hIlTpL36dYUNk4dalb6kMMAv+Z 6+hsTXBbKWWc3apdzK8BMewM69KN6Oqce+Zu9ydmDBpI125C4z/eIT574Q1w+2OqqGwaVLRcJXrJ osmLFqa7LH4XXgVNWG4SHQHuEhANxjJ/GP/89PrNbpHoNkm+Gkhpi8KWTRoSsmkXwQqQ1vp5Iki/ untp+HDH+no32NgN0nZPV/+Qt+OR0t3vwmC3Zzrd/qqc8NSLf3Iizsafl7b4r4qgEKjZ+xjGtrVc UjyJthkqcwEKDwOzEmDyei+B26Nu/yYwl/WL3YlXtq09s68rxbd2AvCl1iuahhQqcvbjM4xdCUsT 37uMdBNSSwIDAQABo4ICUjCCAk4wDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAa4wHQYDVR0OBBYE FE4L7xqkQFulF2mHMMo0aEPQQa7yMGQGA1UdHwRdMFswLKAqoCiGJmh0dHA6Ly9jZXJ0LnN0YXJ0 Y29tLm9yZy9zZnNjYS1jcmwuY3JsMCugKaAnhiVodHRwOi8vY3JsLnN0YXJ0Y29tLm9yZy9zZnNj YS1jcmwuY3JsMIIBXQYDVR0gBIIBVDCCAVAwggFMBgsrBgEEAYG1NwEBATCCATswLwYIKwYBBQUH AgEWI2h0dHA6Ly9jZXJ0LnN0YXJ0Y29tLm9yZy9wb2xpY3kucGRmMDUGCCsGAQUFBwIBFilodHRw Oi8vY2VydC5zdGFydGNvbS5vcmcvaW50ZXJtZWRpYXRlLnBkZjCB0AYIKwYBBQUHAgIwgcMwJxYg U3RhcnQgQ29tbWVyY2lhbCAoU3RhcnRDb20pIEx0ZC4wAwIBARqBl0xpbWl0ZWQgTGlhYmlsaXR5 LCByZWFkIHRoZSBzZWN0aW9uICpMZWdhbCBMaW1pdGF0aW9ucyogb2YgdGhlIFN0YXJ0Q29tIENl cnRpZmljYXRpb24gQXV0aG9yaXR5IFBvbGljeSBhdmFpbGFibGUgYXQgaHR0cDovL2NlcnQuc3Rh cnRjb20ub3JnL3BvbGljeS5wZGYwEQYJYIZIAYb4QgEBBAQDAgAHMDgGCWCGSAGG+EIBDQQrFilT dGFydENvbSBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTANBgkqhkiG9w0BAQUFAAOC AgEAFmyZ9GYMNPXQhV59CuzaEE44HF7fpiUFS5Eyweg78T3dRAlbB0mKKctmArexmvclmAk8jhvh 3TaHK0u7aNM5Zj2gJsfyOZEdUauCe37Vzlrk4gNXcGmXCPleWKYK34wGmkUWFjgKXlf2Ysd6AgXm vB618p70qSmD+LIU424oh0TDkBreOKk8rENNZEXO3SipXPJzewT4F+irsfMuXGRuczE6Eri8sxHk fY+BUZo7jYn0TZNmezwD7dOaHZrzZVD1oNB1ny+v8OqCQ5j4aZyJecRDjkZy42Q2Eq/3JR44iZB3 fsNrarnDy0RLrHiQi+fHLB5LEUTINFInzQpdn4XBidUaePKVEFMy3YCEZnXZtWgo+2EuvoSoOMCZ EoalHmdkrQYuL6lwhceWD3yJZfWOQ1QOq92lgDmUYMA0yZZwLKMS9R9Ie70cfmu3nZD0Ijuu+Pwq yvqCUqDvr0tVk+vBtfAii6w0TiYiBKGHLHVKt+V9E9e4DGTANtLJL4YSjCMJwRuCO3NJo2pXh5Tl 1njFmUNj403gdy3hZZlyaQQaRwnmDwFWJPsfvw55qVguucQJAX6Vum0ABj6y6koQOdjQK/W/7HW/ lwLFCRsI3FU34oH7N4RDYiDK51ZLZer+bMEkkyShNOsF/5oirpt9P/FlUQqmMGqz9IgcgA38coro g14= -----END CERTIFICATE----- """ #EOF# pagekite-0.5.8a/scripts/pagekite_gtk0000775000175000017500000017102112603542202017073 0ustar brebre00000000000000#!/usr/bin/env python import datetime import getopt import gobject import gtk import os from random import randint import sys import socket import threading import traceback import time import webbrowser from pagekite.compat import * from pagekite import common, httpd, pk from pagekite.ui import basic, remote SHARE_DIR = "~/PageKite" URL_HOME = ('https://pagekite.net/home/') URL_HELP = ('https://pagekite.net/support/') # FIXME: App specific help! IMG_DIR_WINDOWS = '.SELF/gui/icons-16' IMG_DIR_DEFAULT = '.SELF/gui/icons-127' IMG_FILE_WIZARD = '.SELF/gui/dreki.png' IMG_FILE_WIZBACK = '.SELF/gui/background.jpg' ICON_FILE_ACTIVE = 'pk-active.png' ICON_FILE_TRAFFIC = 'pk-traffic.png' ICON_FILE_IDLE = 'pk-idle.png' try: # If this works, we are inside a PyBreeder archive PIXBUF_WIZBACK = gtk_open_image(IMG_FILE_WIZBACK) except NameError: def gtk_open_image(fn): return gtk.gdk.pixbuf_new_from_file(fn) PIXBUF_WIZBACK = gtk_open_image(IMG_FILE_WIZBACK) def ExposeFancyBackground(widget, ev): try: alloc = widget.get_allocation() pixbuf = PIXBUF_WIZBACK.scale_simple(alloc.width, alloc.height, gtk.gdk.INTERP_BILINEAR) widget.window.draw_pixbuf(widget.style.bg_gc[gtk.STATE_NORMAL], pixbuf, 0, 0, alloc.x, alloc.y) if hasattr(widget, 'get_child') and widget.get_child() is not None: widget.propagate_expose(widget.get_child(), ev) return True except: traceback.print_exc() return False def ShowInfoDialog(message, d_type=gtk.MESSAGE_INFO): dlg = gtk.MessageDialog(type=d_type, buttons=gtk.BUTTONS_CLOSE, message_format=message.replace(' ', '\n')) dlg.set_position(gtk.WIN_POS_CENTER) dlg.get_action_area().get_children()[0].connect('clicked', lambda w: dlg.destroy()) # dlg.connect('expose-event', ExposeFancyBackground) dlg.show() if d_type != gtk.MESSAGE_ERROR: def killit(): dlg.destroy() return False gobject.timeout_add(5000, killit) def ShowErrorDialog(message): ShowInfoDialog(message, d_type=gtk.MESSAGE_ERROR) def Button(stock_id, text, action): b = gtk.Button() l = gtk.Label() l.set_markup_with_mnemonic(text) l.set_mnemonic_widget(b) hb = gtk.HBox() hb.pack_start(gtk.image_new_from_stock(stock_id, gtk.ICON_SIZE_MENU)) hb.pack_start(l, padding=5) b.add(hb) b.connect('clicked', action) return b def DescribeKite(domain, protoport, info): proto = protoport.split('/')[0] fdesc = protoport url = None if proto.startswith('https'): fdesc = 'Secure Website (end-to-end)' url = 'https://%s%s' % (domain, info['port'] and ':%s' % info['port'] or '') elif proto.startswith('http'): secure = (('ssl' in info or info['proto'] == 'https') and 'Secure ' or '') pdesc = info['port'] and ' on port %s' % info['port'] or '' fdesc = '%sWebsite%s' % (secure, pdesc) url = '%s://%s%s' % (secure and 'https' or 'http', domain, info['port'] and ':%s' % info['port'] or '') elif proto in ('ssh', 'raw'): if info['port'] == '22': fdesc = 'SSH (HTTP proxied)' else: fdesc = 'TCP Port %s (HTTP proxied)' % info['port'] bdesc = ('builtin' in info and 'PageKite Sharing' or '%s:%s' % (info['bhost'], info['bport'])) status = ['Unknown'] code = int(info['status'], 16) if code in BE_INACTIVE: status = ['Disabled'] else: if code & BE_STATUS_OK: status = ['Flying'] elif code & BE_STATUS_ERR_ANY: status = ['Error'] if code & BE_STATUS_ERR_DNS: status.append('DNS') if code & BE_STATUS_ERR_BE: status.append('Server down') if code & BE_STATUS_ERR_TUNNEL: status.append('Rejected') return (url and 'WWW, %s/' % url or fdesc), bdesc, ', '.join(status), url def GetScreenShot(): w = gtk.gdk.get_default_root_window() sz = w.get_size() pb = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, 8, sz[0], sz[1]) pb = pb.get_from_drawable(w, w.get_colormap(), 0,0,0,0, sz[0], sz[1]) return pb def mkdirs(path, perms, touch=None, touchparents=None): if not os.path.exists(path): mkdirs(os.path.dirname(path), perms, touch=(touch or touchparents)) os.mkdir(path, perms) for fn in [os.path.join(path, c) for c in (touch or [])]: open(fn, 'a').close() class ShareBucket: S_CLIPBOARD = 1 S_PATHS = 2 S_SCREENSHOT = 3 T_TEXT = 1 T_HTML = 2 T_MARKDOWN = 3 JSON_INDEX = """\ {"title": %(title)s, "date": %(date)s, "expires": %(expires)s, "content": %(content)s, "files": [\n\t%(files)s\n ]}\ """ HTML_INDEX = """\ %(title)s

    %(title)s

    Last modified on %(date)s

    %(content)s
      \n %(files)s\n
    """ def __init__(self, kitename, kiteport, title=None, dirname=None, random=False): self.share_dir = os.path.expanduser(SHARE_DIR) self.kite_dir = os.path.join(self.share_dir, kitename) if dirname: self.fullpath = os.path.join(self.kite_dir, dirname) else: while True: randcrap = sha1hex('%s%s' % (randint(0, 0x7ffffffe), globalSecret())) dirparts = datetime.datetime.now().strftime("%Y/%m-%d").split('/') if random: dirparts[-1] = randcrap[:16] dirparts = [os.path.join(*dirparts)] if title: dirparts.append(title.replace(' ', '_')) dirparts.append(randcrap[-5:]) dirname = '.'.join(dirparts) self.fullpath = os.path.join(self.kite_dir, dirname) if not os.path.exists(self.fullpath): break self.dirname = os.path.join('.', dirname)[1:] self.kitename = kitename self.kiteport = kiteport self.webpath = None self.title = title or 'Shared with PageKite' self.content = (self.T_TEXT, '') # Create directory! mkdirs(self.fullpath, 0700, touchparents=['_pagekite.html']) def load(self): return self def fmt_title(self, ftype='html'): if ftype == 'json': # FIXME: Escape better return '"%s"' % self.title.replace('"', '\\"') else: # FIXME: Escape better return '%s' % self.title def fmt_content(self, ftype='html'): if ftype == 'json': # FIXME: Escape better return '"%s"' % self.content[1].replace('"', '\\"') else: # FIXME: Escape better return '
    %s
    ' % self.content[1] def fmt_file(self, filename, ftype='html'): # FIXME: Do something friendly with file types/extensions if ftype == 'json': # FIXME: Escape better return '"%s"' % filename.replace('"', '\\"') else: # FIXME: Escape better return ('
  • %s
  • ' ) % (filename, os.path.basename(filename)) def save(self): filelist = [] for fn in os.listdir(self.fullpath): if not (fn.startswith('.') or fn in ('_pagekite.html', '_pagekite.json')): filelist.append(fn) SEP = {'html': '\n ', 'json': ',\n '} for ft, tp in (#('html', self.HTML_INDEX), ('json', self.JSON_INDEX), ): fd = open(os.path.join(self.fullpath, '_pagekite.%s' % ft), 'w') fd.write(tp % { 'title': self.fmt_title(ft), 'date': 0, 'expires': 0, 'content': self.fmt_content(ft), 'files': SEP[ft].join([self.fmt_file(f, ft) for f in sorted(filelist)]) }) fd.close() return self def set_title(self, title): self.title = title return self def set_content(self, content, ctype=T_TEXT): self.content = (ctype, content) return self def add_paths(self, paths): for path in paths: os.symlink(path, os.path.join(self.fullpath, os.path.basename(path))) return self def add_screenshot(self, screenshot): screenshot.save(os.path.join(self.fullpath, 'screenshot.png'), 'png') return self def pk_config(self): # This is just one config line per PageKite hostname, finer granularity # just makes things more confusing. return ['webpath=%s/80:/:default:%s' % (self.kitename, self.kite_dir), 'be_config=%s/80:indexes:True' % self.kitename] class PageKiteThread(remote.PageKiteRestarter): def postpone(self, func, argument): gobject.idle_add(func, argument) def configure(self, pkobj): return pk.Configure(pkobj) def startup(self): pk.Main(pk.PageKite, self.config_wrapper, uiclass=remote.RemoteUi, http_handler=httpd.UiRequestHandler, http_server=httpd.UiHttpServer) class CommThread(remote.CommThread): def call_cb(self, which, args): gobject.idle_add(self.cb[which], args) class UiContainer: def cfg(self, title, width, height): self.window.set_size_request(width, height) self.window.set_position(gtk.WIN_POS_CENTER) if title: self.window.set_title(title) def win(self, title=None, child=None, width=500, height=400, cls=gtk.Window): self.window = cls() self.cfg(title, width, height) if child: self.window.add(child) self.window.show_all() return self class UiWizard(UiContainer): def __init__(self): pass class PageKiteWizard: def __init__(self, title=''): self.window = UiContainer().win(width=500, height=300, cls=gtk.Dialog).window # Just keep window open forever and ever self.window.connect("delete_event", lambda w, e: True) self.window.connect("destroy", lambda w: False) # Prepare our standard widgets self.title = gtk.Label("PageKite") self.title.set_justify(gtk.JUSTIFY_CENTER) self.question = gtk.Label('Welcome to PageKite!') self.question.set_justify(gtk.JUSTIFY_LEFT) self.decoration = gtk.Image() self.decoration.set_from_pixbuf(gtk_open_image(IMG_FILE_WIZARD)) self.inputprefix = gtk.Label('') self.textinput = gtk.Entry() self.textinput.set_activates_default(True) self.inputsuffix = gtk.Label('') # Set up our packing... self.right = gtk.VBox(False, spacing=15) self.left = gtk.VBox(False, spacing=5) self.hbox = gtk.HBox(False, spacing=0) self.input_hbox = gtk.HBox(False, spacing=0) self.hbox.pack_start(self.right, expand=False, fill=False, padding=10) self.hbox.pack_start(self.left, expand=True, fill=True, padding=10) self.right.pack_start(self.decoration, expand=True, fill=True) self.left.pack_start(self.question, expand=True, fill=True) self.input_hbox.pack_start(self.inputprefix, expand=False, fill=False) self.input_hbox.pack_start(self.textinput, expand=True, fill=True) self.input_hbox.pack_start(self.inputsuffix, expand=False, fill=False) self.left.pack_start(self.input_hbox, expand=True, fill=True) self.window.vbox.pack_start(self.title, expand=False, fill=False, padding=5) self.window.vbox.pack_start(self.hbox, expand=True, fill=True, padding=0) # Draw a fancy background! #self.window.vbox.connect('expose-event', ExposeFancyBackground) if title: self.set_title(title) self.buttons = [] self.window.show_all() self.show_input_area(False) def show_input_area(self, really): if really: self.input_hbox.show() self.textinput.grab_focus() self.question.set_alignment(0, 1) else: self.input_hbox.hide() self.question.set_alignment(0, 0.5) def set_title(self, title): self.title.set_markup(' %s ' % title) def click_last(self, w, e): if self.buttons: self.buttons[-1][0](e) def clear_buttons(self): for b in self.buttons: self.window.action_area.remove(b) self.buttons = [] def set_question(self, question): self.question.set_markup(question.replace(' ', '\n')) self.question.set_justify(gtk.JUSTIFY_LEFT) def set_buttons(self, buttonlist): self.clear_buttons() last = None for label, callback in buttonlist: button = gtk.Button(label) button.connect('clicked', callback) button.show() last = button self.window.action_area.pack_start(button) self.buttons.append(button) if last: last.set_flags(gtk.CAN_DEFAULT) last.grab_default() def close(self): self.clear_buttons() self.window.hide() self.window.destroy() self.window = self.buttons = None class SharingDialog(gtk.Dialog): DEFAULT_EXPIRATION = 0 EXPIRATION = { "Never expires": 0, "Expires in 2 days": 2*24*3600, "Expires in 7 days": 7*24*3600, "Expires in 14 days": 14*24*3600, "Expires in 30 days": 30*24*3600, "Expires in 90 days": 90*24*3600, "Expires in 180 days": 180*24*3600, "Expires in 365 days": 365*24*3600 } def __init__(self, kites, stype, sdata, title=''): gtk.Dialog.__init__(self, title='Sharing Details', buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OK, gtk.RESPONSE_OK)) self.set_position(gtk.WIN_POS_CENTER) horizontal = gtk.HBox() table = gtk.Table() t_row = 0 def ta_factory(tr): def ta(left, right=None, hint=None, yalign=0.5, rright=2, **rargs): left = gtk.Label('%s ' % left) table.attach(left, 0, right and 1 or rright, tr[0], tr[0]+1) if right: left.set_alignment(1, yalign) table.attach(right, 1, rright, tr[0], tr[0]+1, **rargs) if hint: hint = gtk.Label(' %s' % hint) hint.set_alignment(0, yalign) table.attach(hint, rright, rright+1, tr[0], tr[0]+1) else: left.set_alignment(0.5, yalign) tr[0] += 1 return ta table_append = ta_factory([t_row]) preview_box = gtk.Label("FIXME: Cropper") kitelist = [] for domain in kites: for bid in kites[domain]: if 'builtin' in kites[domain][bid] and bid.startswith('http/'): kitelist.append('%s:%s' % (domain, bid[5:])) if len(kitelist) > 1: combo = gtk.combo_box_new_text() kitelist.sort(key=lambda k: len(k)) for kite in kitelist: combo.append_text(kite) combo.set_active(0) table_append("Share on:", combo) self.kite_chooser = combo elif len(kitelist) == 1: table_append("Sharing on %s" % kitelist[0]) self.kite_chooser = kitelist[0] else: table_append("No kites!") self.kite_chooser = None self.title_box = gtk.Entry() self.title_box.set_text(title) table_append("Title:", self.title_box) self.description = gtk.TextView() self.description.set_wrap_mode(gtk.WRAP_WORD) self.description.set_border_width(1) dbox = gtk.ScrolledWindow() dbox.set_size_request(250, 75) dbox.set_policy('automatic', 'automatic') dbox.set_shadow_type('out') dbox.add(self.description) table_append("Description:", dbox, rright=3, yalign=0.1, xpadding=2, ypadding=2) elist = (self.EXPIRATION.keys()[:]) elist.sort(key=lambda k: self.EXPIRATION[k]) self.expiration = ecombo = gtk.combo_box_new_text() for exp in elist: ecombo.append_text(exp) ecombo.set_active(self.DEFAULT_EXPIRATION) table_append("Expiration:", ecombo) self.password_box = gtk.Entry() table_append("Password:", self.password_box, 'optional') self.open_browser = gtk.CheckButton('Open in browser') self.open_browser.set_active(True) self.action_area.pack_start(self.open_browser, expand=True, fill=True, padding=0) self.action_area.reorder_child(self.open_browser, 0) horizontal.pack_start(preview_box, expand=False, fill=False, padding=10) horizontal.pack_end(table, expand=True, fill=True, padding=0) self.vbox.pack_start(horizontal, expand=True, fill=True, padding=0) self.show_all() def get_kiteinfo(self): if str(type(self.kite_chooser)) == "": return self.kite_chooser.get_model()[self.kite_chooser.get_active()][0] else: return self.kite_chooser def get_kitename(self): return (self.get_kiteinfo() or ':').split(':')[0] def get_kiteport(self): return int((self.get_kiteinfo() or ':').split(':')[1]) def get_password(self): return self.password_box.get_text() def get_expiration(self): return self.password_box.get_text() def get_title(self): return self.title_box.get_text() def get_description(self): buf = self.description.get_buffer() return buf.get_text(buf.get_start_iter(), buf.get_end_iter()) class PageKiteManagerPage: def __init__(self, parent, status=None): self.parent = parent self.cancel = Button(gtk.STOCK_CLOSE, '_Close', self.parent.close) self.actions = gtk.HBox() self.actions.pack_end(self.cancel, expand=False, fill=False, padding=1) action_frame = gtk.Frame() action_frame.add(self.actions) self.content = gtk.VBox() self.inactive = gtk.Label('Loading ...') self.page = gtk.VBox() self.page.pack_start(self.inactive, expand=True, fill=True) self.page.pack_start(self.content, expand=True, fill=True) self.page.pack_end(action_frame, expand=False, fill=False) def set_active(self): self.inactive.hide() self.content.show_all() self.actions.show_all() def set_inactive(self, reason): self.inactive.set_text(reason) self.inactive.show() self.content.hide() self.actions.hide() def update(self, kites, visible=False): self.set_active() def update_status(self, status): pass def update_motd(self, motd): pass class PageKiteConfigEditor(PageKiteManagerPage): def __init__(self, parent, status=None): PageKiteManagerPage.__init__(self, parent) self.save = Button(gtk.STOCK_SAVE, '_Save and Restart', self.on_save) self.refresh = Button(gtk.STOCK_REFRESH, '_Reload', self.on_refresh) self.edit = gtk.TextView() ebox = gtk.ScrolledWindow() ebox.add(self.edit) self.actions.pack_start(self.save, expand=False, fill=False, padding=1) self.actions.pack_start(self.refresh, expand=False, fill=False, padding=1) self.content.pack_start(ebox, expand=True, fill=True, padding=0) self.config = '' def get_text(self): buf = self.edit.get_buffer() return buf.get_text(buf.get_start_iter(), buf.get_end_iter()) def update(self, kites=None, visible=False): if self.parent.pkt.pk: config = '\n'.join(self.parent.pkt.pk.GenerateConfig()) if not self.config or not visible or self.get_text() == self.config: self.config = config self.edit.get_buffer().set_text(self.config) self.set_active() def on_refresh(self, ev): self.config = '' self.update() def on_save(self, ev): self.parent.set_all_inactive('Saving changes and restarting ...') self.parent.pkt.stop(then=self.do_save) def do_save(self): ln = None lines = self.get_text().split('\n') try: new_pk = pk.PageKite(http_handler=httpd.UiRequestHandler, http_server=httpd.UiHttpServer) for ln in range(1, len(lines)+1): new_pk.ConfigureFromFile(data=[lines[ln-1]]) ln = None new_pk.CheckConfig() new_pk.SaveUserConfig() if new_pk.ui_httpd: new_pk.ui_httpd.quit() new_pk = None # We are definitely not clean anymore! if '--clean' in sys.argv: sys.argv.remove('--clean') self.config = '' except (IndexError, ValueError, getopt.GetoptError, common.ConfigError), e: ShowInfoDialog(('Oops! Invalid configuration, not saved.\n' '%s') % (ln and 'Bad line %s: %s\n' % (ln, lines[ln-1]) or str(e))) self.parent.pkt.restart() class PageKiteKiteList(PageKiteManagerPage): def __init__(self, parent, status=None): PageKiteManagerPage.__init__(self, parent) self.kite_count = 0 self.kite_add = Button(gtk.STOCK_NEW, '_New Kite', self.add_kite) self.hint = gtk.Label('Select a kite or service for more options.') self.kite_remove = Button(gtk.STOCK_DELETE, '_Delete Kite', self.remove_kite) self.svc_add = Button(gtk.STOCK_ADD, '_Add Service', self.add_service) self.svc_remove = Button(gtk.STOCK_REMOVE, '_Remove', self.remove_service) self.svc_disable = Button(gtk.STOCK_STOP, '_Disable', self.disable_service) self.svc_enable = Button(gtk.STOCK_YES, '_Enable', self.enable_service) self.actions.pack_start(self.kite_add, expand=False, fill=False, padding=1) self.actions.pack_start(self.hint, expand=False, fill=False, padding=5) self.actions.pack_start(self.svc_add, expand=False, fill=False, padding=1) self.actions.pack_start(self.svc_enable, expand=False, fill=False, padding=1) self.actions.pack_start(self.svc_disable, expand=False, fill=False, padding=1) self.actions.pack_end(self.svc_remove, expand=False, fill=False, padding=1) self.actions.pack_end(self.kite_remove, expand=False, fill=False, padding=1) self.kites, self.kites_sig = {}, None # FIXME: name, backend, status, actions self.store = gtk.TreeStore(str, str, str, str) self.textcell = gtk.CellRendererText() self.view = gtk.TreeView(self.store) for order, title, cell, attrs in ( ( 0, 'Kites', self.textcell, [('text', 0)]), ( 1, 'Services', self.textcell, [('text', 1)]), ( 2, 'Status', self.textcell, [('text', 2)]), # (-1, '', self.textcell, [('text', 3)]), ): kc = gtk.TreeViewColumn(title, cell) if title and order >= 0: kc.set_sort_column_id(order) for attname, attorder in attrs: kc.add_attribute(cell, attname, attorder) self.view.append_column(kc) self.view.connect('cursor-changed', self.update_buttons) sw = gtk.ScrolledWindow() sw.set_policy('automatic', 'automatic') sw.add(self.view) self.content.pack_start(sw, expand=True, fill=True) def add_kite(self, w): self.parent.pkt.send('addkite: None\n') def remove_kite(self, w): print 'FIXME: delete %s' % self.get_kite() def get_kite(self): model, row = self.view.get_selection().get_selected() if not row: return None return model.get_value(model.iter_parent(row) or row, 0) def get_service(self): model, row = self.view.get_selection().get_selected() if not row or not model.iter_parent(row): return None return (model.get_value(row, 3), model.get_value(row, 2)) def update_buttons(self, w=None, hide=False): svc = self.get_service() if svc and not hide: for w in (self.hint, self.kite_remove, self.svc_add): w.hide() self.svc_remove.show() #self.svc_remove.set_sensitive(self.kite_count > 1) if svc[1].lower() == 'disabled': self.svc_enable.show() self.svc_disable.hide() else: self.svc_enable.hide() self.svc_disable.show() else: for w in (self.svc_remove, self.svc_enable, self.svc_disable): w.hide() if self.get_kite() and not hide: self.hint.hide() self.svc_add.show() # self.kite_remove.show() else: for w in (self.svc_add, self.kite_remove): w.hide() if self.kite_count: self.hint.show() else: self.hint.hide() def update(self, kites, visible=False): self.store.clear() self.kite_count = 0 for k in kites: pid = self.store.append(None, ['%s' % k, '', '', '']) for key in sorted(kites[k]): svc = kites[k][key] fdesc, bdesc, status, url = DescribeKite(k, key, svc) # FIXME: Report if front-end HTTPS is available for WWW services self.store.append(pid, [fdesc, bdesc, status, svc['bid']]) self.kite_count += 1 self.view.expand_all() self.set_active() def add_service(self, w): kite = self.get_kite() if not kite: return ShowErrorDialog('Please choose a kite from the list above.') self.parent.pkt.send('addkite: %s\n' % kite) def set_active(self): PageKiteManagerPage.set_active(self) self.update_buttons() def set_inactive(self, reason): PageKiteManagerPage.set_inactive(self, reason) self.update_buttons(hide=True) def remove_service(self, w): self.parent.pkt.send('delkite: %s\n' % self.get_service()[0]) self.parent.pkt.send('save: quietly\n') def disable_service(self, w): self.parent.pkt.send('disablekite: %s\n' % self.get_service()[0]) self.parent.pkt.send('save: quietly\n') def enable_service(self, w): self.parent.pkt.send('enablekite: %s\n' % self.get_service()[0]) self.parent.pkt.send('save: quietly\n') class PageKiteShareList(PageKiteManagerPage): def __init__(self, parent, status=None): PageKiteManagerPage.__init__(self, parent) self.status = gtk.Label('Loading kite information ...') self.content.pack_start(self.status, expand=True, fill=True) class PageKiteLogView(PageKiteManagerPage): def __init__(self, parent, status=None): PageKiteManagerPage.__init__(self, parent) self.status = gtk.Label('Loading PageKite log ...') self.content.pack_start(self.status, expand=True, fill=True) class PageKiteHome(PageKiteManagerPage): def __init__(self, parent, status='Starting up ...'): PageKiteManagerPage.__init__(self, parent) self.status = gtk.Label('Welcome to PageKite!') self.content.pack_start(self.status, expand=True, fill=True) self.info = gtk.Label(status) self.info.set_alignment(0, 0.5) self.quit = Button(gtk.STOCK_QUIT, '_Quit', self.parent.quit) self.actions.pack_end(self.quit, expand=False, fill=False, padding=1) self.actions.pack_start(self.info, expand=True, fill=True, padding=5) self.content.connect('expose-event', ExposeFancyBackground) def update_motd(self, motd): if motd: self.status.set_markup(basic.clean_html(motd)) else: self.status.set_markup('Welcome to PageKite!') def update_status(self, status): self.info.set_text(status) def set_inactive(self, message): pass class PageKiteManager: PAGE_HOME = '_PageKite' PAGE_KITES = 'My _Kites' PAGE_SHARING = 'S_haring' PAGE_CONFIG = 'Config _File' PAGE_LOG = '_Log' def __init__(self, pkm, pkt, development=False): self.pkm = pkm self.pkt = pkt self.development = development self.window = gtk.Window(gtk.WINDOW_TOPLEVEL) self.window.set_title('PageKite Manager') self.window.set_position(gtk.WIN_POS_CENTER) self.window.set_size_request(520, 320) self.window.connect("delete_event", self.close) self.window.connect("destroy", self.close) self.notebook = gtk.Notebook() self.notebook.set_tab_pos(gtk.POS_TOP) self.pages = [ (self.PAGE_HOME, PageKiteHome(self, status=pkm.status), False), (self.PAGE_KITES, PageKiteKiteList(self, status=pkm.status), False), (self.PAGE_SHARING, PageKiteShareList(self, status=pkm.status), True), (self.PAGE_CONFIG, PageKiteConfigEditor(self, status=pkm.status),False), (self.PAGE_LOG, PageKiteLogView(self, status=pkm.status), True), ] for t, p, dev in self.pages: if not dev or development: l = gtk.Label() self.notebook.append_page(p.page, l) l.set_markup_with_mnemonic(t) vbox = gtk.VBox() vbox.pack_start(self.notebook, expand=True, fill=True) if not self.pkm.is_embedded(): for title, page, dev in self.pages: page.actions.remove(page.cancel) self.window.add(vbox) self.window.show_all() def is_visible(self, which): return (self.pages[self.notebook.get_current_page()][1] == which) def show_page(self, which): for i in range(0, len(self.pages)): title, page, dev = self.pages[i] if i == which or title == which or page == which: self.notebook.set_current_page(i) def update(self, kites): for title, page, dev in self.pages: if self.development or not dev: page.update(kites, visible=self.is_visible(page)) def update_status(self, status): for title, page, dev in self.pages: if self.development or not dev: page.update_status(status) def update_motd(self, motd): for title, page, dev in self.pages: page.update_motd(motd) def set_all_inactive(self, reason): for title, page, dev in self.pages: if self.development or not dev: page.set_inactive(reason) def close(self, a, aa=None): self.window.destroy() self.window = self.pages = self.notebook = () return True def quit(self, a, aa=None): self.set_all_inactive('Shutting down ...') self.update_status('Shutting down ...') gobject.idle_add(self.quit2, a) def quit2(self, a): self.pkm.quit(a) self.close(a) class PageKiteStatusIcon(gtk.StatusIcon): MENU_TEMPLATE = ''' %(kitelist)s ''' def __init__(self, pkComm, development=False, open_manager=False): gtk.StatusIcon.__init__(self) self.development = development self.open_manager = open_manager self.menu = None self.motd = None self.wizard = None self.kite_manager = None self.suppress_updates = False self.pkComm = pkComm self.pkComm.cb.update({ 'status_tag': self.set_status_tag, 'status_msg': self.set_status_msg, 'motd': self.set_motd, 'tell_message': ShowInfoDialog, 'tell_error': ShowErrorDialog, 'working': self.show_working, 'start_wizard': self.start_wizard, 'end_wizard': self.end_wizard, 'ask_yesno': self.ask_yesno, 'ask_email': self.ask_email, 'ask_kitename': self.ask_kitename, 'ask_backends': self.ask_backends, 'ask_multiplechoice': self.ask_multiplechoice, 'ask_login': self.ask_login, 'be_list': self.update_be_list, }) self.status = 'PageKite' self.set_tooltip(self.status) self.icon_file = ICON_FILE_IDLE if sys.platform in ('win32', 'os2', 'os2emx'): self.icon_dir = IMG_DIR_WINDOWS else: self.icon_dir = IMG_DIR_DEFAULT self.set_from_pixbuf(gtk_open_image(os.path.join(self.icon_dir, self.icon_file))) self.connect('activate', self.on_activate) self.connect('popup-menu', self.on_popup_menu) #gobject.timeout_add_seconds(1, self.on_tick) self.kites, self.kites_sig = {}, None try: GetScreenShot() self.have_screenshots = True except: self.have_screenshots = False self.have_sharing = False self.pkComm.start() self.set_visible(True) def create_menu(self): self.manager = gtk.UIManager() ag = gtk.ActionGroup('Actions') ag.add_actions([ ('Menu', None, 'Menu'), ('QuotaDisplay', None, 'XX.YY GB of Quota left'), ('GetQuota', None, 'Get _More Quota...', None, 'Get more Quota from PageKite.net', self.on_stub), ('SharePath', None, 'Share _File or Folder', None, 'Make a file or folder visible to the Web', self.share_path), ('ShareClipboard', None, '_Paste to Web', None, 'Make the contents of the clipboard visible to the Web', self.share_clipboard), ('ShareScreenshot', None, 'Share _Screenshot', None, 'Put a screenshot of your desktop on the Web', self.share_screenshot), ('CfgKites', None, '_Manage ...', None, 'Manage Kites and Sharing', self.manage_kites), ('HelpMenu', None, '_Help'), ('OpenHelp', None, '_PageKite.net Support', None, 'Access the PageKite.net support site', self.on_help), ('About', gtk.STOCK_ABOUT, '_About', None, 'About PageKite', self.on_about), ('Quit', None, '_Quit PageKite', None, 'Turn PageKite off completely', self.quit), ]) ag.add_toggle_actions([ ('EnablePageKite', None, '_Enable PageKite', None, 'Enable local PageKite', self.toggle_enable, (not self.pkComm.pkThread.stopped)), ('VerboseLog', None, 'Verbose Logging', None, 'Verbose logging facilitate troubleshooting.', self.on_stub, False), ]) self.manager.insert_action_group(ag, 0) self.manager.add_ui_from_string(self.MENU_TEMPLATE % { 'kitelist': self.kite_menu(action_group=ag), }) #self.manager.get_widget('/Menubar/Menu/QuotaDisplay').set_sensitive(False) self.menu = self.manager.get_widget('/Menubar/Menu/Quit').props.parent if not self.menu: print '%s' % dir(self.manager) def kite_menu(self, action_group=None): xml, actions, toggles = [], [], [] mc = 0 def a(elem, tit, action=None, tooltip=None, close=True, cb=None, toggle=None): if elem == 'menu': close = False if not action: action = 'PageKiteList_%s' % mc xml.append('<%s action="%s"%s>' % (elem, action, close and '/' or '')) if toggle is not None: toggles.append((action, None, tit, None, tooltip, cb, toggle)) else: actions.append((action, None, tit, None, tooltip, cb)) return 1 def sn(path): p = path[-30:] if p != path: if '/' in p: p = '/'.join(('...', p.split('/', 1)[1])) elif '\\' in p: p = '\\'.join(('...', p.split('\\', 1)[1])) return p def make_cb(func, *data): def tmp(what): return func(what, *data) return tmp domains = sorted(self.kites.keys()) if len(domains): a('menuitem', 'My Kites:', action='PageKiteList') for domain in domains: mc += a('menu', ' %s' % domain) www = [k for k in self.kites[domain].keys() if k.startswith('http')] www.sort(key=lambda x: int(self.kites[domain][x]['port'] or self.kites[domain][x]['bport'] or 0)) for protoport in www: info = self.kites[domain][protoport] fdesc, bdesc, status, url = DescribeKite(domain, protoport, info) live = (status != 'Disabled') mc += a('menuitem', '%s to %s' % (fdesc, bdesc), cb=make_cb(self.kite_toggle, info, live), toggle=live) # if BE_STATUS_OK & int(info['status'], 16): # mc += a('menuitem', 'Open in Browser', # cb=make_cb(self.open_url, url), tooltip=url) if live: if ('paths' not in info) or not development: mc += a('menuitem', 'Copy Link to Site', cb=make_cb(self.copy_url, url), tooltip=url) elif len(info['paths'].keys()): for path in sorted(info['paths'].keys()): mc += a('menu', ' ' + sn(info['paths'][path]['src'])) if BE_STATUS_OK & int(info['status'], 16): mc += a('menuitem', 'Open in Browser', cb=make_cb(self.open_url, url+path), tooltip=url+path) mc += a('menuitem', ('Copy Link to: %s' ) % (path == '/' and 'Home page' or path), cb=make_cb(self.copy_url, url+path), tooltip=url+path) mc += a('menuitem', 'Stop Sharing') xml.append('') xml.append('') others = [k for k in self.kites[domain].keys() if k not in www] others.sort(key=lambda x: int(self.kites[domain][x]['port'] or self.kites[domain][x]['bport'] or 0)) for protoport in others: info = self.kites[domain][protoport] fdesc, bdesc, status, url = DescribeKite(domain, protoport, info) live = (status != 'Disabled') mc += a('menuitem', '%s to %s' % (fdesc, bdesc), cb=make_cb(self.kite_toggle, info, live), toggle=live) xml.append('') else: a('menuitem', 'No Kites Yet', action='PageKiteList') if action_group and actions: action_group.add_actions(actions) if action_group and toggles: action_group.add_toggle_actions(toggles) return ''.join(xml) def set_status_msg(self, message): self.status = message self.set_tooltip('PageKite: %s' % self.status) if self.kite_manager: self.kite_manager.update_status(self.status) def set_status_tag(self, status): old_if = self.icon_file km = self.kite_manager if status in ('traffic', 'serving'): self.icon_file = ICON_FILE_TRAFFIC # Connecting.. elif status == 'connect': self.icon_file = ICON_FILE_ACTIVE if km: km.set_all_inactive('Connecting ...') elif status == 'dyndns': self.icon_file = ICON_FILE_IDLE elif status == 'startup': self.icon_file = ICON_FILE_IDLE if km: km.set_all_inactive('Starting up ...') elif status == 'reconfig': self.icon_file = ICON_FILE_ACTIVE if km: km.set_all_inactive('Updating configuration ...') # Inactive, boo elif status in ('idle', 'down'): self.icon_file = ICON_FILE_IDLE elif status == 'exiting': self.icon_file = ICON_FILE_IDLE if km: km.set_all_inactive('Disconnecting ...') # Ready and waiting elif status in ('active', 'flying'): self.icon_file = ICON_FILE_ACTIVE # Ignore bogus updates which would cause the screen to flicker. self.set_suppress_updates(status in ('reconnecting', 'exiting')) if self.icon_file != old_if: self.set_from_pixbuf(gtk_open_image(os.path.join(self.icon_dir, self.icon_file))) if self.open_manager: # This will open the manager on first run... self.manage_kites(None, page=0) self.open_manager = False elif not self.is_embedded(): # This will also open the manager if we fail to embed the icon and # in that case, we also quit the programmer when the manager is closed. if not self.kite_manager: self.manage_kites(None, page=0) elif not self.kite_manager.window: self.quit(None, set_status_tag=False) def set_motd(self, args): frontend, message = args.split(' ', 1) self.motd = message.strip() if self.kite_manager: self.kite_manager.update_motd(self.motd) def on_activate(self, ignored): if self.wizard: self.wizard.window.hide() self.wizard.window.show() elif self.kite_manager and self.kite_manager.window: self.kite_manager.window.destroy() self.kite_manager = None else: self.manage_kites(None, page=0) #self.emit('popup-menu', 2, 0) return False def on_popup_menu(self, status, button, when): if self.menu and self.menu.props.visible: self.menu.popdown() else: if not self.menu: self.create_menu() self.show_menu(button, when) return False def ui_full(self): return (not self.pkComm.pkThread.stopped and not self.wizard) def show_menu(self, button, when): w = self.manager.get_widget for item in ('/Menubar/Menu/PageKiteList', '/Menubar/Menu/SharedItems', '/Menubar/Menu/SharePath', '/Menubar/Menu/ShareClipboard', '/Menubar/Menu/ShareScreenshot', '/Menubar/Menu/AdvancedMenu/ViewLog', '/Menubar/Menu/AdvancedMenu/VerboseLog'): try: w(item).set_sensitive(self.ui_full()) except: pass for item in ('/Menubar/Menu/PageKiteList', ): try: w(item).set_sensitive(False) except: pass if not self.have_screenshots: w('/Menubar/Menu/ShareScreenshot').hide() if not self.have_sharing or not self.development: w('/Menubar/Menu/ShareScreenshot').hide() w('/Menubar/Menu/ShareClipboard').hide() w('/Menubar/Menu/SharePath').hide() for item in ('/Menubar/Menu/CfgKites', ): try: if self.pkComm.pkThread.stopped: w(item).hide() else: w(item).show() except: pass self.menu.popup(None, None, gtk.status_icon_position_menu, button, when, self) def set_suppress_updates(self, yesno): if self.suppress_updates != yesno: self.kites_sig = '-reload-' self.suppress_updates = yesno def update_be_list(self, args): if self.suppress_updates: return ks = self.kites_sig self.kites_sig = '\n'.join(args.get('_raw', [])) if ks == self.kites_sig: return self.menu = None self.kites = {} self.have_sharing = False for line in args.get('_raw', []): self.parse_status(line.strip().split(': ', 1)[1]) if self.kite_manager: self.kite_manager.update(self.kites) def parse_status(self, argtext): args = {} for arg in argtext.split('; '): var, val = arg.split('=', 1) args[var] = val if 'domain' in args: domain_info = self.kites.get(args['domain'], {}) proto = args.get('proto', 'http') port = args.get('port') or '80' # FIXME: this is dumb bid = '%s/%s' % (proto, port) backend_info = domain_info.get(bid, {}) if 'path' in args: path_info = backend_info.get('paths', {}) if 'delete' in args: if args['path'] in path_info: del path_info[args['path']] else: path_info[args['path']] = { 'domain': args['domain'], 'policy': args['policy'], 'port': port, 'src': args['src'] } backend_info['paths'] = path_info domain_info[bid] = backend_info else: if 'delete' in args: if bid in domain_info: del domain_info[bid] else: if 'builtin' in args: self.have_sharing = True for i in ('proto', 'port', 'status', 'bhost', 'bport', 'bid', 'ssl', 'builtin'): if i in args: backend_info[i] = args[i] domain_info[bid] = backend_info self.kites[args['domain']] = domain_info def show_working(self, message): self.wizard.set_question('Communicating with PageKite.net.\n' 'This may take a moment or two.\n\n' '%s ...' % message) self.wizard.show_input_area(False) def wizard_prepare(self, args): if 'preamble' in args: question = ''.join([args['preamble'].replace(' ', '\n'), '\n\n', args['question'], '']) else: question = '%s' % args['question'] wizard = self.wizard if not wizard: wizard = PageKiteWizard(title='A question!') wizard.set_question(question) return question, wizard def ask_yesno(self, args): question, wizard = self.wizard_prepare(args) wizard.show_input_area(False) def respond(window, what): self.pkComm.pkThread.send('%s\n' % what) self.wizard_first = False if not self.wizard: wizard.close() buttons = [] if 'no' in args: buttons.append((args['no'], lambda w: respond(w, 'n'))) if 'yes' in args: buttons.append((args['yes'], lambda w: respond(w, 'y'))) wizard.set_buttons(buttons) def ask_question(self, args, valid_re, prefix=' ', callback=None, password=False): question, wizard = self.wizard_prepare(args) wizard.textinput.set_text(args.get('default', '')) wizard.textinput.set_visibility(not password) wizard.inputprefix.set_text(prefix) wizard.inputsuffix.set_text(args.get('domain', '')+' ') wizard.show_input_area(True) def respond(window, what): wizard.inputprefix.set_text('') wizard.inputsuffix.set_text('') self.wizard_first = False if not self.wizard: wizard.close() if callback: callback(window, what) else: self.pkComm.pkThread.send('%s\n' % what) wizard.set_buttons([ ((self.wizard and not self.wizard_first) and '<<' or 'Cancel', lambda w: respond(w, 'back')), (self.wizard and 'Next' or 'OK', lambda w: respond(w, wizard.textinput.get_text())), ]) def ask_email(self, args): return self.ask_question(args, '.*@.*$') # FIXME def ask_kitename(self, args): return self.ask_question(args, '.*') # FIXME def ask_login(self, args): def askpass(window, what): if 'default' in args: del args['default'] self.pkComm.pkThread.send('%s\n' % what) self.ask_question(args, '.*', prefix='Password: ', password=True) if 'default' in args: return askpass(None, args['default']) else: return self.ask_question(args, '.*', prefix='E-mail: ', callback=askpass) def ask_backends(self, args): question, wizard = self.wizard_prepare(args) wizard.show_input_area(False) choices = gtk.VBox(False, spacing=5) protos = args.get('protos', '').split(', ') ports = args.get('ports', '').split(', ') rawports = args.get('rawports', '').split(', ') ktypes = [] if 'http' in args['protos']: if self.development: ktypes.append(('builtin', 'PageKite Sharing', None, ports)) ktypes.append(('http', 'HTTP server', '80', ports)) if 'https' in args['protos']: ktypes.append(('https', 'HTTPS server', '443', ports)) if 'raw' in args['protos']: ktypes.append(('ssh', 'SSH server', '22', rawports)) ktypes.append(('raw', 'Raw TCP server','1234', rawports)) fly, flyb = gtk.HBox(), gtk.HBox() fly_cb = gtk.combo_box_new_text() for t, o, bp, fpl in ktypes: fly_cb.append_text(o) fly_on = gtk.Label() fly_on.set_markup(' on localhost:') fly_port = gtk.Entry() fly_port.set_width_chars(5) fly_asbb, fly_asb = gtk.HBox(), gtk.HBox() fly_ast = gtk.Label() fly_as = gtk.combo_box_new_text() hint = gtk.Label() hint.set_markup('Note: New services may replace old ones.') hint.set_alignment(0, 1) flyb.pack_start(fly_cb, expand=False, fill=False, padding=0) flyb.pack_start(fly_on, expand=False, fill=False, padding=0) flyb.pack_start(fly_port, expand=False, fill=False, padding=0) fly.pack_start(flyb, expand=True, fill=False, padding=0) fly_asbb.pack_start(fly_ast, expand=False, fill=False, padding=0) fly_asbb.pack_start(fly_as, expand=False, fill=False, padding=0) fly_asb.pack_start(fly_asbb, expand=True, fill=False, padding=0) choices.pack_start(fly, expand=False, fill=False, padding=5) choices.pack_start(fly_asb, expand=True, fill=False, padding=5) choices.pack_end(hint, expand=True, fill=True, padding=5) # FIXME: update() should set the backend string for our reponse def update(w, choice): chosen = fly_cb.get_active_text() kitename = args['kitename'] for t, o, bp, fpl in ktypes: if o == chosen: if w == fly_cb: fly_port.set_sensitive(bp is not None) fly_port.set_text(bp or '--') lport = fly_port.get_text() proto = (t == 'builtin') and 'http' or t if proto in ('raw', 'ssh'): fly_as.hide() if 'virtual' in rawports or lport in rawports: fly_ast.set_markup(('Fly as %s:%s (HTTP proxied)' ) % (kitename, lport)) choice[0] = 'raw-%s:%%s:localhost:%s' % (lport, lport) elif w == fly_cb: fly_ast.set_text('Fly as: ') fly_as.get_model().clear() fly_as.show() if proto.startswith('http'): fly_as.append_text('%s://%s' % (proto, kitename)) if t == 'builtin': choice[0] = '%s:builtin' else: choice[0] = '%s:%%s:localhost:%s' % (proto, lport) for port in fpl: fly_as.append_text('%s://%s:%s' % (proto, kitename, port)) fly_as.set_active(0) else: fly_as_text = (fly_as.get_active_text() or '').split(':') if len(fly_as_text) > 2: port = fly_as_text[2] if t == 'builtin': choice[0] = 'http-%s:%%s:builtin' % port else: choice[0] = '%s-%s:%%s:localhost:%s' % (proto, port, lport) else: if t == 'builtin': choice[0] = '%s:builtin' else: choice[0] = '%s:%%s:localhost:%s' % (proto, lport) choice = [None] choices.show_all() update(None, choice) fly_cb.connect('changed', lambda w: update(w, choice)) fly_as.connect('changed', lambda w: update(w, choice)) fly_port.connect('changed', lambda w: update(w, choice)) fly_cb.set_active(0) self.wizard.left.pack_start(choices) def respond(window, ch=None): self.wizard_first = False self.wizard.left.remove(choices) self.pkComm.pkThread.send('%s\n' % (ch or choice)[0]) if not self.wizard: wizard.close() wizard.set_buttons([ ((self.wizard and not self.wizard_first) and '<<' or 'Cancel', lambda w: respond(w, ['back'])), (self.wizard and 'Next' or 'OK', lambda w: respond(w)), ]) def ask_multiplechoice(self, args): question, wizard = self.wizard_prepare(args) wizard.show_input_area(False) choices = gtk.VBox(False, spacing=5) clist = [] rb = None for ch in sorted([k for k in args if k.startswith('choice_')]): rb = gtk.RadioButton(rb, args[ch]) clist.append((rb, int(ch[7:]))) choices.pack_start(rb, expand=False, fill=False) choices.show_all() self.wizard.left.pack_start(choices) def respond(window, choice=None): if not choice: choice = args.get('default', None) for cw, cn in clist: if cw.get_active(): choice = cn self.wizard_first = False self.wizard.left.remove(choices) print 'Choice is: %s' % choice self.pkComm.pkThread.send('%s\n' % choice) if not self.wizard: wizard.close() wizard.set_buttons([ ((self.wizard and not self.wizard_first) and '<<' or 'Cancel', lambda w: respond(w, 'back')), (self.wizard and 'Next' or 'OK', lambda w: respond(w)), ]) def kite_toggle(self, ev, kite_info, live): bid = kite_info['bid'] if live: self.pkComm.pkThread.send('disablekite: %s\n' % bid) ShowInfoDialog('Disabling %s ...' % bid) else: self.pkComm.pkThread.send('enablekite: %s\n' % bid) ShowInfoDialog('Enabling %s ...' % bid) return False def copy_url(self, junk, url): gtk.clipboard_get('CLIPBOARD').set_text(url, len=-1) def open_url(self, junk, url): webbrowser.open(url) def share_clipboard_cb(self, clipboard, text, data): print 'CB: %s / %s / %s' % (clipboard, text, data) ShowErrorDialog('Unimplemented... %s [%s/%s]' % (text, clipboard, data)) def share_clipboard(self, data): cb = gtk.clipboard_get(gtk.gdk.SELECTION_CLIPBOARD) cb.request_text(self.share_clipboard_cb) def get_sharebucket(self, title, dtype, data): self.wizard = sd = SharingDialog(self.kites, dtype, data, title=title) if sd.run() == gtk.RESPONSE_OK: kitename = sd.get_kitename() kiteport = sd.get_kiteport() else: kitename = kiteport = None sd.hide() self.wizard = None if not kitename: return None, None, None, None sb = ShareBucket(kitename, kiteport, title=title) return kitename, kiteport, sb, sd def save_configuration(self, lines): for line in lines: self.pkComm.pkThread.send('config: %s\n' % line) self.pkComm.pkThread.send('save: quietly\n') if '--clean' in sys.argv: sys.argv.remove('--clean') def share_path(self, data): try: RESPONSE_SHARE = gtk.RESPONSE_CANCEL + gtk.RESPONSE_OK + 1000 self.wizard = fs = gtk.FileChooserDialog('Share Files or Folders', None, gtk.FILE_CHOOSER_ACTION_OPEN, (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, "Share!", RESPONSE_SHARE)) fs.set_default_response(RESPONSE_SHARE) fs.set_select_multiple(True) expl = gtk.Label("Hint: You can share multiple files or folders " "by holding the key.") expl.show() fs.set_extra_widget(expl) paths = (fs.run() == RESPONSE_SHARE) and fs.get_filenames() fs.destroy() expl.destroy() self.wizard = None if paths: kitename, kiteport, sb, sd = self.get_sharebucket('Shared', ShareBucket.S_PATHS, paths) if not sb: return sb.add_paths(paths).save() self.save_configuration(sb.pk_config()) url = 'http://%s:%s%s' % (kitename, kiteport, sb.dirname) self.copy_url(None, url) self.open_url(None, url) except: ShowErrorDialog('Sharing failed: %s' % (sys.exc_info(), )) def share_screenshot(self, data, title='Screenshot'): self.menu.hide() try: screenshot = GetScreenShot() kitename, kiteport, sb, sd = self.get_sharebucket('Screenshot', ShareBucket.S_SCREENSHOT, screenshot) if not sb: return sb.add_screenshot(screenshot).save() self.save_configuration(sb.pk_config()) url = 'http://%s:%s%s' % (kitename, kiteport, sb.dirname) self.copy_url(None, url) self.open_url(None, url) except: ShowErrorDialog('Screenshot failed: %s' % (sys.exc_info(), )) def new_kite(self, data): self.pkComm.pkThread.send('addkite: None\n') def manage_kites(self, data, page=PageKiteManager.PAGE_KITES): if self.kite_manager and self.kite_manager.pages: self.kite_manager.window.present() else: self.kite_manager = PageKiteManager(self, self.pkComm.pkThread, development=self.development) self.kite_manager.update(self.kites) self.kite_manager.update_motd(self.motd) self.kite_manager.show_page(page) return False def show_about(self): dialog = gtk.AboutDialog() dialog.set_position(gtk.WIN_POS_CENTER) dialog.set_name('PageKite') dialog.set_version(common.APPVER) dialog.set_comments('PageKite is a tool for running personal servers, ' 'sharing work and communicating over the WWW.') dialog.set_website(common.WWWHOME) dialog.set_license(common.LICENSE) dialog.connect('expose-event', ExposeFancyBackground) dialog.run() dialog.destroy() def toggle_verboselog(self, data): pass def toggle_enable(self, data): pkt = self.pkComm.pkThread pkt.toggle() data.set_active(not pkt.stopped) if pkt.stopped: self.kites, self.kites_sig = {}, None def start_wizard(self, title): if self.wizard: self.wizard.set_title(title) else: self.wizard = PageKiteWizard(title=title) self.wizard_first = True def end_wizard(self, message): if self.wizard: self.wizard.close() self.wizard = None self.pkComm.pkThread.send('save: quietly\n') if '--clean' in sys.argv: sys.argv.remove('--clean') def on_stub(self, data): print 'Stub' def on_help(self, data): ShowInfoDialog('Opening %s in your web browser ...' % URL_HELP) self.open_url(None, URL_HELP) def on_about(self, data): self.show_about() def quit(self, data, set_status_tag=True): if set_status_tag: self.set_status_tag('exiting') self.set_status_msg('Shutting down...') gobject.timeout_add_seconds(1, self.quitting) self.pkComm.quit() def quitting(self): if self.pkComm and self.pkComm.pkThread and self.pkComm.pkThread.pk: return gtk.main_quit() if __name__ == '__main__': pkt = pksi = ct = None try: pkt = PageKiteThread(startup_args=['--friendly']) if '--remote' in sys.argv: sys.argv.remove('--remote') pkt.stopped = True else: pkt.stopped = False if '--dev' in sys.argv: sys.argv.remove('--dev') development = True else: development = False if '--nomanager' in sys.argv: sys.argv.remove('--nomanager') open_manager = False else: open_manager = True ct = CommThread(pkt) pksi = PageKiteStatusIcon(ct, development=development, open_manager=open_manager) gobject.threads_init() gtk.main() except: traceback.print_exc() finally: if pkt: pkt.quit() if ct: ct.quit() ############################################################################## CERTS="""\ StartCom Ltd. ============= -----BEGIN CERTIFICATE----- MIIFFjCCBH+gAwIBAgIBADANBgkqhkiG9w0BAQQFADCBsDELMAkGA1UEBhMCSUwxDzANBgNVBAgT BklzcmFlbDEOMAwGA1UEBxMFRWlsYXQxFjAUBgNVBAoTDVN0YXJ0Q29tIEx0ZC4xGjAYBgNVBAsT EUNBIEF1dGhvcml0eSBEZXAuMSkwJwYDVQQDEyBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1dGhv cml0eTEhMB8GCSqGSIb3DQEJARYSYWRtaW5Ac3RhcnRjb20ub3JnMB4XDTA1MDMxNzE3Mzc0OFoX DTM1MDMxMDE3Mzc0OFowgbAxCzAJBgNVBAYTAklMMQ8wDQYDVQQIEwZJc3JhZWwxDjAMBgNVBAcT BUVpbGF0MRYwFAYDVQQKEw1TdGFydENvbSBMdGQuMRowGAYDVQQLExFDQSBBdXRob3JpdHkgRGVw LjEpMCcGA1UEAxMgRnJlZSBTU0wgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxITAfBgkqhkiG9w0B CQEWEmFkbWluQHN0YXJ0Y29tLm9yZzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA7YRgACOe yEpRKSfeOqE5tWmrCbIvNP1h3D3TsM+x18LEwrHkllbEvqoUDufMOlDIOmKdw6OsWXuO7lUaHEe+ o5c5s7XvIywI6Nivcy+5yYPo7QAPyHWlLzRMGOh2iCNJitu27Wjaw7ViKUylS7eYtAkUEKD4/mJ2 IhULpNYILzUCAwEAAaOCAjwwggI4MA8GA1UdEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgHmMB0GA1Ud DgQWBBQcicOWzL3+MtUNjIExtpidjShkjTCB3QYDVR0jBIHVMIHSgBQcicOWzL3+MtUNjIExtpid jShkjaGBtqSBszCBsDELMAkGA1UEBhMCSUwxDzANBgNVBAgTBklzcmFlbDEOMAwGA1UEBxMFRWls YXQxFjAUBgNVBAoTDVN0YXJ0Q29tIEx0ZC4xGjAYBgNVBAsTEUNBIEF1dGhvcml0eSBEZXAuMSkw JwYDVQQDEyBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEhMB8GCSqGSIb3DQEJARYS YWRtaW5Ac3RhcnRjb20ub3JnggEAMB0GA1UdEQQWMBSBEmFkbWluQHN0YXJ0Y29tLm9yZzAdBgNV HRIEFjAUgRJhZG1pbkBzdGFydGNvbS5vcmcwEQYJYIZIAYb4QgEBBAQDAgAHMC8GCWCGSAGG+EIB DQQiFiBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAyBglghkgBhvhCAQQEJRYjaHR0 cDovL2NlcnQuc3RhcnRjb20ub3JnL2NhLWNybC5jcmwwKAYJYIZIAYb4QgECBBsWGWh0dHA6Ly9j ZXJ0LnN0YXJ0Y29tLm9yZy8wOQYJYIZIAYb4QgEIBCwWKmh0dHA6Ly9jZXJ0LnN0YXJ0Y29tLm9y Zy9pbmRleC5waHA/YXBwPTExMTANBgkqhkiG9w0BAQQFAAOBgQBscSXhnjSRIe/bbL0BCFaPiNhB OlP1ct8nV0t2hPdopP7rPwl+KLhX6h/BquL/lp9JmeaylXOWxkjHXo0Hclb4g4+fd68p00UOpO6w NnQt8M2YI3s3S9r+UZjEHjQ8iP2ZO1CnwYszx8JSFhKVU2Ui77qLzmLbcCOxgN8aIDjnfg== -----END CERTIFICATE----- StartCom Certification Authority ================================ -----BEGIN CERTIFICATE----- MIIHyTCCBbGgAwIBAgIBATANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJJTDEWMBQGA1UEChMN U3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmlu ZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDYwOTE3MTk0 NjM2WhcNMzYwOTE3MTk0NjM2WjB9MQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRk LjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMg U3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw ggIKAoICAQDBiNsJvGxGfHiflXu1M5DycmLWwTYgIiRezul38kMKogZkpMyONvg45iPwbm2xPN1y o4UcodM9tDMr0y+v/uqwQVlntsQGfQqedIXWeUyAN3rfOQVSWff0G0ZDpNKFhdLDcfN1YjS6LIp/ Ho/u7TTQEceWzVI9ujPW3U3eCztKS5/CJi/6tRYccjV3yjxd5srhJosaNnZcAdt0FCX+7bWgiA/d eMotHweXMAEtcnn6RtYTKqi5pquDSR3l8u/d5AGOGAqPY1MWhWKpDhk6zLVmpsJrdAfkK+F2PrRt 2PZE4XNiHzvEvqBTViVsUQn3qqvKv3b9bZvzndu/PWa8DFaqr5hIlTpL36dYUNk4dalb6kMMAv+Z 6+hsTXBbKWWc3apdzK8BMewM69KN6Oqce+Zu9ydmDBpI125C4z/eIT574Q1w+2OqqGwaVLRcJXrJ osmLFqa7LH4XXgVNWG4SHQHuEhANxjJ/GP/89PrNbpHoNkm+Gkhpi8KWTRoSsmkXwQqQ1vp5Iki/ untp+HDH+no32NgN0nZPV/+Qt+OR0t3vwmC3Zzrd/qqc8NSLf3Iizsafl7b4r4qgEKjZ+xjGtrVc UjyJthkqcwEKDwOzEmDyei+B26Nu/yYwl/WL3YlXtq09s68rxbd2AvCl1iuahhQqcvbjM4xdCUsT 37uMdBNSSwIDAQABo4ICUjCCAk4wDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAa4wHQYDVR0OBBYE FE4L7xqkQFulF2mHMMo0aEPQQa7yMGQGA1UdHwRdMFswLKAqoCiGJmh0dHA6Ly9jZXJ0LnN0YXJ0 Y29tLm9yZy9zZnNjYS1jcmwuY3JsMCugKaAnhiVodHRwOi8vY3JsLnN0YXJ0Y29tLm9yZy9zZnNj YS1jcmwuY3JsMIIBXQYDVR0gBIIBVDCCAVAwggFMBgsrBgEEAYG1NwEBATCCATswLwYIKwYBBQUH AgEWI2h0dHA6Ly9jZXJ0LnN0YXJ0Y29tLm9yZy9wb2xpY3kucGRmMDUGCCsGAQUFBwIBFilodHRw Oi8vY2VydC5zdGFydGNvbS5vcmcvaW50ZXJtZWRpYXRlLnBkZjCB0AYIKwYBBQUHAgIwgcMwJxYg U3RhcnQgQ29tbWVyY2lhbCAoU3RhcnRDb20pIEx0ZC4wAwIBARqBl0xpbWl0ZWQgTGlhYmlsaXR5 LCByZWFkIHRoZSBzZWN0aW9uICpMZWdhbCBMaW1pdGF0aW9ucyogb2YgdGhlIFN0YXJ0Q29tIENl cnRpZmljYXRpb24gQXV0aG9yaXR5IFBvbGljeSBhdmFpbGFibGUgYXQgaHR0cDovL2NlcnQuc3Rh cnRjb20ub3JnL3BvbGljeS5wZGYwEQYJYIZIAYb4QgEBBAQDAgAHMDgGCWCGSAGG+EIBDQQrFilT dGFydENvbSBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTANBgkqhkiG9w0BAQUFAAOC AgEAFmyZ9GYMNPXQhV59CuzaEE44HF7fpiUFS5Eyweg78T3dRAlbB0mKKctmArexmvclmAk8jhvh 3TaHK0u7aNM5Zj2gJsfyOZEdUauCe37Vzlrk4gNXcGmXCPleWKYK34wGmkUWFjgKXlf2Ysd6AgXm vB618p70qSmD+LIU424oh0TDkBreOKk8rENNZEXO3SipXPJzewT4F+irsfMuXGRuczE6Eri8sxHk fY+BUZo7jYn0TZNmezwD7dOaHZrzZVD1oNB1ny+v8OqCQ5j4aZyJecRDjkZy42Q2Eq/3JR44iZB3 fsNrarnDy0RLrHiQi+fHLB5LEUTINFInzQpdn4XBidUaePKVEFMy3YCEZnXZtWgo+2EuvoSoOMCZ EoalHmdkrQYuL6lwhceWD3yJZfWOQ1QOq92lgDmUYMA0yZZwLKMS9R9Ie70cfmu3nZD0Ijuu+Pwq yvqCUqDvr0tVk+vBtfAii6w0TiYiBKGHLHVKt+V9E9e4DGTANtLJL4YSjCMJwRuCO3NJo2pXh5Tl 1njFmUNj403gdy3hZZlyaQQaRwnmDwFWJPsfvw55qVguucQJAX6Vum0ABj6y6koQOdjQK/W/7HW/ lwLFCRsI3FU34oH7N4RDYiDK51ZLZer+bMEkkyShNOsF/5oirpt9P/FlUQqmMGqz9IgcgA38coro g14= -----END CERTIFICATE----- """ pagekite-0.5.8a/scripts/pagekite_test.py.old0000775000175000017500000005161112603542202020473 0ustar brebre00000000000000#!/usr/bin/python -u # # pagekite_test.py, Copyright 2010, The PageKites Project ehf. # http://beanstalks-project.net/ # # Testing for the core pagekite code. # ############################################################################# # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # ############################################################################# # DESIGN: # # TestInternals: # Basic unittests for key parts of the code: protocol parsers, signatures, # things that need to stay strictly compatible. # # TestNetwork: # Tests network communication by creating multiple threads and making # them talk to each-other: # - FrontEnd threads (mocked DNS code) # - BackEnd threads (mocked DNS code) # - HTTPD threads # - SMTPD threads # - XMPP threads # - Client threads checking results # # TextNetworkExternal: # Similar to TestNetwork, but adds the ability to use external servers # as well, for verifying compatibility with other implementations. # import os import random import socket import sys import time import threading import unittest import urllib import pagekite class MockSocketFD(object): def __init__(self, recv_values=None, maxsend=1500, maxread=5000): self.recv_values = recv_values or [] self.sent_values = [] self.maxsend = maxsend self.maxread = maxread self.closed = False def recv(self, maxread): if self.recv_values: if maxread > self.maxread: maxread = self.maxread if len(self.recv_values[0]) <= maxread: data = self.recv_values.pop(0) else: data = self.recv_values[0][0:maxread] self.recv_values[0] = self.recv_values[0][maxread:] return data else: return None def send(self, data): if len(data) > self.maxsend: self.sent_values.append(data[0:self.maxsend]) return self.maxsend else: self.sent_values.append(data) return len(data) def setblocking(self, val): pass def setsockopt(self, a, b, c): pass def flush(self): pass def close(self): self.closed = True def closed(self): return self.closed class MockUiRequestHandler(pagekite.UiRequestHandler): def do_GET(self): self.send_response(200) self.send_header('Content-Type', 'text/plain') self.end_headers() self.wfile.write('I am %s\n' % self.server.pkite) self.wfile.write('asdf random junk junk crap! ' * random.randint(0, 200)) class MockPageKite(pagekite.PageKite): def __init__(self): pagekite.PageKite.__init__(self) self.felldown = None def FallDown(self, message, help=True): raise Exception(message) def HelpAndExit(self): raise Exception('Should print help') def LookupDomainQuota(lookup): return -1 def Ping(self, host, port): return len(host)+port def GetHostIpAddr(self, host): if host == 'localhost': return '127.0.0.1' return '10.1.2.%d' % len(host) def GetHostDetails(self, host): return (host, [host], ['10.1.2.%d' % len(host), '192.168.1.%d' % len(host)]) class TestInternals(unittest.TestCase): def setUp(self): pagekite.Log = pagekite.LogValues self.gSecret = pagekite.globalSecret() def test_signToken(self): # Basic signature self.assertEqual(pagekite.signToken(token='1234567812', secret='Bjarni', payload='Bjarni'), '1234567843b16458418175599012be884a18') # Make sure it varies based on all variables self.assertNotEqual(pagekite.signToken(token='1234567812345689', secret='BjarniTheDude', payload='Bjarni'), '1234567843b16458418175599012be884a18') self.assertNotEqual(pagekite.signToken(token='234567812345689', secret='Bjarni', payload='Bjarni'), '1234567843b16458418175599012be884a18') self.assertNotEqual(pagekite.signToken(token='1234567812345689', secret='Bjarni', payload='BjarniTheDude'), '1234567843b16458418175599012be884a18') # Test non-standard signature lengths self.assertEqual(pagekite.signToken(token='1234567812', secret='Bjarni', payload='Bjarni', length=1000), '1234567843b16458418175599012be884a18963f10be4670') def test_PageKiteRequest(self): request = ['CONNECT PageKite:1 HTTP/1.0\r\n'] zlibreq = 'X-PageKite-Features: ZChunks\r\n' reqbody = '\r\n' # Basic request, no servers. req = request[:] req.extend([zlibreq, reqbody]) self.assertEqual(pagekite.HTTP_PageKiteRequest('x', {}), ''.join(req)) # Basic request, no servers, zchunks disabled. req = request[:] req.append(reqbody) self.assertEqual(pagekite.HTTP_PageKiteRequest('x', {}, nozchunks=True), ''.join(req)) # Full request, single server. bid = 'http:a' token = '0123456789' backends = {bid: ['a', 'b', 'c', 'd']} backends[bid][pagekite.BE_SECRET] = 'Secret' data = '%s:%s:%s' % (bid, pagekite.signToken(token=self.gSecret, payload=self.gSecret, secret='x'), token) sign = pagekite.signToken(secret='Secret', payload=data, token=token) req = request[:] req.extend([zlibreq, 'X-PageKite: %s:%s\r\n' % (data, sign), reqbody]) self.assertEqual(pagekite.HTTP_PageKiteRequest('x', backends, tokens={bid: token}, testtoken=token), ''.join(req)) def test_LogValues(self): # Make sure the LogValues dumbs down our messages so they are easy # to parse and survive a trip through syslog etc. words, wdict = pagekite.LogValues([('spaces', ' bar '), ('tab', 'one\ttwo'), ('cr', 'one\rtwo'), ('lf', 'one\ntwo'), ('semi', 'one;two; three')], testtime=1000) self.assertEqual(wdict['ts'], '%x' % 1000) self.assertEqual(wdict['spaces'], 'bar') self.assertEqual(wdict['tab'], 'one two') self.assertEqual(wdict['cr'], 'one two') self.assertEqual(wdict['lf'], 'one two') self.assertEqual(wdict['semi'], 'one;two, three') for key, val in words: self.assertEqual(wdict[key], val) def test_HttpParser(self): Response11 = 'HTTP/1.1 200 OK' Request11 = 'GET / HTTP/1.1' Headers = ['Host: foo.com', 'Content-Type: text/html', 'Borked:', 'Multi: foo', 'Multi: bar'] BadHeader = 'BadHeader' Body = 'This is the Body' # Parse a valid request. pagekite.LOG = [] GoodRequest = [Request11] GoodRequest.extend(Headers) GoodRequest.extend(['', Body]) goodRParse = pagekite.HttpParser(lines=GoodRequest, testbody=True) self.assertEquals(pagekite.LOG, []) self.assertEquals(goodRParse.state, goodRParse.IN_BODY) self.assertEquals(goodRParse.lines, GoodRequest) # Make sure the headers parsed properly and that we aren't case-sensitive. self.assertEquals(goodRParse.Header('Host')[0], 'foo.com') self.assertEquals(goodRParse.Header('CONTENT-TYPE')[0], 'text/html') self.assertEquals(goodRParse.Header('multi')[0], 'foo') self.assertEquals(goodRParse.Header('Multi')[1], 'bar') self.assertEquals(goodRParse.Header('noheader'), []) # Parse a valid response. pagekite.LOG = [] GoodMessage = [Response11] GoodMessage.extend(Headers) GoodMessage.extend(['', Body]) goodParse = pagekite.HttpParser(lines=GoodMessage, state=pagekite.HttpParser.IN_RESPONSE, testbody=True) self.assertEquals(pagekite.LOG, []) self.assertEquals(goodParse.state, goodParse.IN_BODY) self.assertEquals(goodParse.lines, GoodMessage) # Fail to parse a bad request. pagekite.LOG = [] BadRequest = Headers[:] BadRequest.extend([BadHeader, '', Body]) badParse = pagekite.HttpParser(lines=BadRequest, state=pagekite.HttpParser.IN_HEADERS, testbody=True) self.assertEquals(badParse.state, badParse.PARSE_FAILED) self.assertNotEqual(pagekite.LOG, []) self.assertEquals(pagekite.LOG[0]['err'][-11:-2], "BadHeader") def test_Selectable(self): packets = ['abc', '123', 'This is a long packet', 'short'] class EchoSelectable(pagekite.Selectable): def __init__(self, data=None): pagekite.Selectable.__init__(self, fd=MockSocketFD(data, maxsend=6)) def ProcessData(self, data): return self.Send(data) # This is a basic test of the EchoSelectable, which simply reads all # the available data and echos it back... pagekite.LOG = [] ss = EchoSelectable(packets[:]) while ss.ReadData() is not False: pass ss.Flush() ss.Cleanup() self.assertEquals(pagekite.LOG[0]['read'], '%d' % len(''.join(packets))) self.assertEquals(pagekite.LOG[0]['wrote'], '%d' % len(''.join(ss.fd.sent_values))) self.assertEquals(''.join(ss.fd.sent_values), ''.join(packets)) # NOTE: This test does not cover the compression code and the SendChunked # method, those are tested in the ChunkParser test below. def test_LineParser(self): packets = ['This is a line\n', 'This ', 'is', ' a line\nThis', ' is a line\n'] class EchoLineParser(pagekite.LineParser): def __init__(self, data=None): pagekite.LineParser.__init__(self, fd=MockSocketFD(data)) def ProcessLine(self, line, lines): return self.Send(line) # This is a basic test of the EchoLineParser, which simply reads all # the available data and echos it back... pagekite.LOG = [] ss = EchoLineParser(packets[:]) while ss.ReadData() is not False: pass ss.Flush() ss.Cleanup() self.assertEquals(pagekite.LOG[0]['read'], '%d' % len(''.join(packets))) self.assertEquals(pagekite.LOG[0]['wrote'], '%d' % len(''.join(ss.fd.sent_values))) self.assertEquals(''.join(ss.fd.sent_values), ''.join(packets)) # Verify that the data was reassembled into complete lines. self.assertEquals(ss.fd.sent_values[0], 'This is a line\n') self.assertEquals(ss.fd.sent_values[1], 'This is a line\n') self.assertEquals(ss.fd.sent_values[2], 'This is a line\n') def test_ChunkParser(self): # Easily compressed raw data... unchunked = ['This would be chunk one, one, one, one, one!!1', 'This is chunk two, chunk two, chunk two, woot!', 'And finally, chunk three, three, chunk, three chunk three'] chunker = pagekite.Selectable(fd=MockSocketFD()) chunked = chunker.fd.sent_values # First, let's just test the basic chunk generation for chunk in unchunked: chunker.SendChunked(chunk) for i in [0, 1, 2]: self.assertEquals(chunked[i], '%x\r\n%s' % (len(unchunked[i]), unchunked[i])) # Second, test compressed chunk generation chunker.EnableZChunks(9) for chunk in unchunked: chunker.SendChunked(chunk) for i in [0, 1, 2]: self.assertTrue(chunked[i+3].startswith('%xZ' % len(unchunked[i]))) self.assertTrue(len(chunked[i+3]) < len(unchunked[i])) # Define our EchoChunkParser... class EchoChunkParser(pagekite.ChunkParser): def __init__(self, data=None): pagekite.ChunkParser.__init__(self, fd=MockSocketFD(data, maxread=1)) def ProcessChunk(self, chunk): return self.Send(chunk) # Finally, let's let the ChunkParser unchunk it all again. pagekite.LOG = [] ss = EchoChunkParser(chunked[:]) while ss.ReadData() is not False: pass ss.Flush() ss.Cleanup() self.assertEquals(pagekite.LOG[-1]['read'], '%d' % len(''.join(chunked))) self.assertEquals(pagekite.LOG[-1]['wrote'], '%d' % (2*len(''.join(unchunked)))) self.assertEquals(''.join(ss.fd.sent_values), 2 * ''.join(unchunked)) # FIXME: Corrupt chunks aren't tested. def test_PageKite(self): bn = MockPageKite() def C1(arg): return bn.Configure([arg]) or True def C2(a1,a2): return bn.Configure([a1,a2]) or True def EQ(val, var): return self.assertEquals(val, var) or True ##[ Common options ]###################################################### C1('--httpd=localhost:1234') and EQ(('localhost', 1234), bn.ui_sspec) C2('-H', 'localhost:4321') and EQ(('localhost', 4321), bn.ui_sspec) C1('--httppass=password') and EQ('password', bn.ui_password) C2('-X', 'passx') and EQ('passx', bn.ui_password) # C1('--pemfile=/dev/null') and EQ('/dev/null', bn.ui_pemfile) # C2('-P', '/dev/zero') and EQ('/dev/zero', bn.ui_pemfile) C1('--nozchunks') and EQ(True, bn.disable_zchunks) C1('--logfile=/dev/null') and EQ('/dev/null', bn.logfile) C2('-L', '/dev/zero') and EQ('/dev/zero', bn.logfile) C1('--daemonize') and EQ(True, bn.daemonize) bn.daemonize = False C1('-Z') and EQ(True, bn.daemonize) C1('--runas=root:root') and EQ(0, bn.setuid) and EQ(0, bn.setgid) C1('--runas=daemon') and EQ(1, bn.setuid) C2('-U', 'root:daemon') and EQ(0, bn.setuid) and EQ(1, bn.setgid) C1('--pidfile=/dev/null') and EQ('/dev/null', bn.pidfile) C2('-I', '/dev/zero') and EQ('/dev/zero', bn.pidfile) ##[ Front-end options ]################################################### C1('--isfrontend') and EQ(True, bn.isfrontend) bn.isfrontend = False C1('-f') and EQ(True, bn.isfrontend) C1('--authdomain=a.com') and EQ('a.com', bn.auth_domain) C2('-A', 'b.com') and EQ('b.com', bn.auth_domain) # C1('--register=a.com') and EQ('a.com', bn.register_with) # C2('-R', 'b.com') and EQ('b.com', bn.register_with) C1('--host=a.com') and EQ('a.com', bn.server_host) C2('-h', 'b.com') and EQ('b.com', bn.server_host) C1('--ports=1,2,3') and EQ([1,2,3], bn.server_ports) C2('-p', '4,5') and EQ([4,5], bn.server_ports) C1('--protos=HTTP,https') and EQ(['http', 'https'], bn.server_protos) # C1('--domain=http,https:a.com:secret') ##[ Back-end options ]################################################### C1('--all') and EQ(True, bn.require_all) bn.require_all = False C1('-a') and EQ(True, bn.require_all) C1('--dyndns=beanstalks.net') and EQ(bn.dyndns[0], pagekite.DYNDNS['beanstalks.net']) C2('-D', 'a@no-ip.com') and EQ(bn.dyndns, (pagekite.DYNDNS['no-ip.com'], {'user': 'a', 'pass': ''})) C1('--dyndns=a:b@c') and EQ(bn.dyndns, ('c', {'user': 'a', 'pass': 'b'})) C1('--frontends=2:a.com:80') and EQ((2, 'a.com', 80), bn.servers_auto) C1('--frontend=b.com:80') and EQ(['b.com:80'], bn.servers_manual) C1('--new') and EQ(True, bn.servers_new_only) bn.servers_new_only = False C1('-N') and EQ(True, bn.servers_new_only) C1('--backend=http:a.com:LOCALhost:80:x') EQ(bn.backends, {'http:a.com': ('http', 'a.com', 'localhost:80', 'x')}) def test_Connections(self): class MockTunnel(pagekite.Selectable): def __init__(self, sname): pagekite.Selectable.__init__(self, fd=MockSocketFD([])) self.server_name = sname class MockAuthThread(pagekite.AuthThread): def __init__(self, conns): self.conns = conns def start(self): self.started = True conns = pagekite.Connections(MockPageKite()) sel = MockTunnel('test.com') conns.Add(sel) conns.start(auth_thread=MockAuthThread(conns)) self.assertEqual(conns.auth.started, True) self.assertEqual(conns.Sockets(), [sel.fd]) self.assertEqual(conns.Blocked(), []) sel.write_blocked = ['block'] self.assertEqual(conns.Blocked(), [sel.fd]) self.assertEqual(conns.Connection(sel.fd), sel) sel.fd.close() conns.CleanFds() self.assertEqual(conns.Sockets(), []) sel.fd.closed = False conns.Tunnel('http', 'test.com', conn=sel) self.assertEqual(conns.TunnelServers(), ['test.com']) self.assertEqual(conns.Tunnel('http', 'test.com'), [sel]) conns.Remove(sel) self.assertEqual(conns.Tunnel('http', 'test.com'), None) def test_AuthThread(self): at = pagekite.AuthThread(None) # FIXME pass def test_MagicProtocolParser(self): # FIXME pass def test_Tunnel(self): # FIXME pass def test_UserConn(self): # FIXME pass def test_UnknownConn(self): # FIXME pass class KiteRunner(threading.Thread): def __init__(self, pagekite_object): threading.Thread.__init__(self) self.pagekite_object = pagekite_object def run(self): self.pagekite_object.Start() class RequestRunner(threading.Thread): def __init__(self, loops, urls, expect): threading.Thread.__init__(self) self.loops = loops self.urls = urls self.expect = expect self.errors = [] def run(self): while self.loops > 0: try: url = self.urls[random.randint(0, len(self.urls)-1)] result = ''.join(urllib.urlopen(url).readlines()) if self.expect not in result: self.errors.append('Bad result: %s' % result) except Exception, e: self.loops = 0 self.errors.append('Error: %s' % e) finally: self.loops -= 1 class ForkRequestRunner(RequestRunner): def start(self): if 0 == os.fork(): self.run() os._exit(0) class TestNetwork(unittest.TestCase): def setUp(self): pagekite.LOG = [] self.fe = [] self.be = [] self.startFrontEnds(2) def startFrontEnds(self, count): n = 0 while n < count: fe = MockPageKite().Configure([ '--isfrontend', '--host=localhost', '--ports=99%d0' % n, '--httpd=:99%d1' % n, '--domain=http,https:localhost:1234' ]) KiteRunner(fe).start() self.fe.append(fe) n += 1 for fe in self.fe: while not fe.looping: time.sleep(1) def stopPageKites(self, pks): for pk in pks: pk.looping = False fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM) fd.connect((pk.server_host, pk.server_ports[0])) def LogData(self, data=None): return ''.join(['\n%s' % l for l in (data or pagekite.LOG) if 'debug' not in l]) def tearDown(self): self.stopPageKites(self.fe) self.stopPageKites(self.be) # TESTS: # - End-to-end test of web-server behind multiple FE=BE tunnel, # multiple clients, large number of requests in parallel. # - Test reconnection logic. # def test_OneBackEnd(self): be = MockPageKite().Configure([ '--isfrontend', '--host=localhost', '--ports=9800', '--httpd=:9801', '--frontend=localhost:9900', '--frontend=localhost:9910', '--backend=http,https:localhost:localhost:9801:1234' ]) be.ui_request_handler = MockUiRequestHandler pagekite.LOG = [] KiteRunner(be).start() self.be.append(be) # Parse the log until we see connections are up and running... waiting = len(self.fe) loops = 5 parsed = [] while waiting > 0 and loops > 0: loops -= 1 while pagekite.LOG: line = pagekite.LOG.pop(0) parsed.append(line) if 'connect' in line: waiting -= 1 if waiting > 0: time.sleep(1) if not loops: raise Exception('No connection after 5 seconds\n%s' % self.LogData(data=parsed)) urls = ['http://LOCALhost:%d/' % pk.server_ports[0] for pk in self.fe] ForkRequestRunner(10, urls, 'MockPageKite').start() ForkRequestRunner(10, urls, 'MockPageKite').start() rr = RequestRunner(15, urls, 'MockPageKite') rr.start() while rr.loops > 0: time.sleep(1) if rr.errors: raise Exception('Ick: %s, %s%s' % ( rr.errors, self.LogData(data=parsed), self.LogData(data=pagekite.LOG))) class TestNetworkExternal(unittest.TestCase): def setUp(self): # FIXME pass if __name__ == '__main__': unittest.main() pagekite-0.5.8a/scripts/pkvnc0000775000175000017500000000343312603542202015557 0ustar brebre00000000000000#!/bin/bash ################################[ This file is placed in the Public Domain. ]## # # This highly self-referencial script will open up VNC via. PageKite. # # It is assumed that the VNC server has a raw PageKite service registered # on whatever port is given to the vnc viewer. # # It requires GNU netcat and (OpenBSD netcat OR socat), and a vnc viewer that # understands the -via argument and uses the VNC_VIA_CMD environment variable. If you # don't have them, this might help: # # sudo apt-get install xvnc4viewer netcat-traditional socat # # (socat is preferred, as it uses SSL to connect to the front-end) # NC_TRAD=nc.traditional NC_OPEN=nc.openbsd VNC_CMD=xvnc4viewer SC_OPEN=socat SC_SSL_AUTH="capath=/etc/ssl/certs/" # If self-signed, use: SC_SSL_AUTH="verify=0" SELF="$0" if [ "$PKVNC_LPORT" = "" ]; then if [ "$1" != "--nc" ]; then # Invoked by the user! export VNC_VIA_CMD="$SELF --nc "'"$L" "$H" "$R"' exec "$VNC_CMD" -via 127.0.0.1 "$@" else # Invoked by the vncviewer! export PKVNC_LPORT="$2" export PKVNC_RHOST="$3" export PKVNC_RPORT="$4" "$NC_TRAD" -l -p "$PKVNC_LPORT" -e "$SELF" & fi else # Invoked by netcat! if [ "$(which $SC_OPEN)" == "" ]; then # No socat available, try netcat. echo 1>&2 echo 'WARNING: Connection to $PKVNC_RHOST:443 is not encrypted.' 1>&2 echo ' Please install socat if you care.' 1>&2 echo 1>&2 exec "$NC_OPEN" -X connect -x "$PKVNC_RHOST:443" "$PKVNC_RHOST" "$PKVNC_RPORT" else # We have socat! Use the more secure SSL connection, manual HTTP CONNECT ( echo CONNECT $PKVNC_RHOST:$PKVNC_RPORT HTTP/1.0 echo exec cat ) | exec "$SC_OPEN" - "openssl:$PKVNC_RHOST:443,$SC_SSL_AUTH" | ( read REPLY read NOTHING exec cat ) fi fi pagekite-0.5.8a/scripts/pagekite0000777000175000017500000000000012603542202022312 2../pagekite/__main__.pyustar brebre00000000000000pagekite-0.5.8a/scripts/self-signed.pem0000644000175000017500000000540612052263662017423 0ustar brebre00000000000000-----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEAvm5LUkJf4TING2ZiEWNNAN6G6HDvYs8KwkGSRTja2khc2skt Oj3sDDNJqDvtAmdGNICA7g+OffNZ/k3nhH81lPmqoaCZNHeoY+fLBo+EQt+0rAFu JxZhUjX4Mq9pj9GKkeMes45l2Eb8JScvurYTHEaI9a23YZh88ZtwJXvjkMjEvww/ M+kLstsg3nDbVSRl1kt3VtEFDCxciRsUxO6+cGRPGrUKIWyeRlv8mZhDdIThRvHD c3ZFmtBMM+gZi3pA3n3gPh7oB7l+37wiZPwdbnLjIRL0dvqqkvxDPuS8qWr0K+Ob 242obbHo0IxCnJrcSwYzdP+gQ5OTMg1rzOfrWQIDAQABAoIBAEh6E+K/7lv4P7LQ +bwibhlJmFD5QrL5l9+tMy1zYSA48FY4wSTskl1mW79S53bFtZuf2PJCK3pWV0cJ gXcVL2B+0WlohUbJz+YOP2BE2RLWr53TgYgQ5YNzlP49ctDQ8ItrxLCUHsD861R8 oJbQW1+knNXcgvH7JnqSLVGm4Eqm5eufK+Vt8x9FOCuFTDjX8i2TNOBJeAHv0PjO 6d/uQs2bkzdMZhopbFpbImPpr9rhAHrSgXjkLpcLSh4qnbicqc24P0D/isIgQH6s 4nFZvLa9Ear/mJeC69tM1Zulbr1siVTz5xyqFIMrztMZ+Ut6j2YMhX5j2m2jr3Z5 d1t0Th0CgYEA9a1AGmonGubMQwe+yl+DlkLGBrLJL51IYRfzz8JBC0utA7vRt2zq 5MPt9AS8yJzwj3N0hPabLCE0g3IxrFWV5VQDaK7y6pU4KD1TdtibmZ+DWnxEjBSs 0TofdH3Ut37r0u6gZn1Bjr3a9jebTrgOYyjXOY9O9FgA/3QLlZUyD7sCgYEAxm7D Kw2J5ZOyv34Q+XxSxImqGKMk9+8/F2IBe6Tmgqu05QPbBrBIeFl0DOwi8kA/O3wI 8kxlR+9kflzl4B4BUQ7dj3hwxH/D86eo6vQGdpBM5U+zD1uwb3pVq54a4pmv/HNE IarElKErggJELK+PbifcJ7wHBPh+NqbKxn6yDfsCgYB0nhGwuDTp0yagptuM6rvm prmjUlinrmw+EoWcWCRR/VEaVZxeCmiRwOOCEoGeZMjX/0EPIJRV5UktIBauLE4j 9rZLicgrTDvE9h9ZacaqrIpIeHZ9XA8QnhtyS4yesbO1g6pGHCzzWfHHMGwbeMjz jt5jJ0CeQevvVqFtFs4VowKBgQDDMJ0GspCcpYbE2usznlmEPq51AbYRtONoyt0O lQyyKNkOFZbTo4AF8mg3deiCRzRr/PyH6yINeqxtqE/u/1ToOSk0QZMbl1pXSOre AyCAbD1Xq+hFCToqzjmLUUC8+dSlDXVNcL9iPI+hmT0af68k+kyTQV/eQdlVRQhd 7K6VFQKBgQC07kVsgTvz0wprebo0Rxa6oWBl6dLxnZ/JQhotuAQS8/qlWFmI5j2W 3v7S1OFgVRUKy/dDv0DH4c3gevLuvtT27s+mLrzBq59WOFOudc86yUBYtV/SeJMP FEUuwmfcCQf8oaPboASLYnN/8kyNrBi5xytwbeE+eemqy1VeGomF5A== -----END RSA PRIVATE KEY----- -----BEGIN CERTIFICATE----- MIIDIDCCAgigAwIBAgIJAOae70ES4m+LMA0GCSqGSIb3DQEBBQUAMDsxEjAQBgNV BAMTCUFub255bW91czEUMBIGA1UEChMLSW5kZXBlbmRlbnQxDzANBgNVBAsTBlBl cnNvbjAeFw0xMjExMTgyMjU2NTBaFw0yMjExMTYyMjU2NTBaMDsxEjAQBgNVBAMT CUFub255bW91czEUMBIGA1UEChMLSW5kZXBlbmRlbnQxDzANBgNVBAsTBlBlcnNv bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL5uS1JCX+EyDRtmYhFj TQDehuhw72LPCsJBkkU42tpIXNrJLTo97AwzSag77QJnRjSAgO4Pjn3zWf5N54R/ NZT5qqGgmTR3qGPnywaPhELftKwBbicWYVI1+DKvaY/RipHjHrOOZdhG/CUnL7q2 ExxGiPWtt2GYfPGbcCV745DIxL8MPzPpC7LbIN5w21UkZdZLd1bRBQwsXIkbFMTu vnBkTxq1CiFsnkZb/JmYQ3SE4Ubxw3N2RZrQTDPoGYt6QN594D4e6Ae5ft+8ImT8 HW5y4yES9Hb6qpL8Qz7kvKlq9Cvjm9uNqG2x6NCMQpya3EsGM3T/oEOTkzINa8zn 61kCAwEAAaMnMCUwIwYDVR0RBBwwGoINKi5wYWdla2l0ZS5tZYIJKi5rYXp6LmFt MA0GCSqGSIb3DQEBBQUAA4IBAQB4nqFLG/ydLXUV5dhoptHsElI0/AVuPMC0lW7c GqRZF2SNhi6jMQHYk3I+gGSvNuwMSSd/VTOgQnGzQunjqY2ay4/1S1CZTBO3+D5Q ZkO5j4YXjQXPrwAl+2+I/405LgMH47h7B646tkP2VsFxL4scwoKdrjFXAJIhz8P8 pvUPGkogEQCGA7RnVmlmFN9fs6MUeN1eieK5ZNqx7dchCRCs9uek6qE6Q4Rc4UFF Uq7VIu+yIYxVDtxksHtQMWXyv5eHUKflQNb//HUhvjGlRSjSdVSsfBHKneq37s9Z vQLSammlSJq/SdQmAUkoWWgcUu2Tizrt/jXM98uHkDpz6S5y -----END CERTIFICATE----- pagekite-0.5.8a/scripts/mk-dropper.sh0000775000175000017500000000151712603542202017130 0ustar brebre00000000000000#!/bin/bash # set -e KITENAME="$1" SECRET="$2" [ "$SECRET" = "" ] && { echo "Usage: $0 kitename.pagekite.me secret" exit 1 } shift shift ARGS="$*" make tools make dev ./scripts/breeder.py sockschain \ pagekite/__init__.py \ pagekite/basicui.py \ pagekite/remoteui.py \ pagekite/yamond.py \ pagekite/httpd.py \ pagekite/__main__.py \ pagekite/__dropper__.py \ |sed -e "s/@KITENAME@/$KITENAME/g" \ -e "s/@SECRET@/$SECRET/g" \ -e "s#@ARGS@#$ARGS#g" \ >pagekite-tmp.py python pagekite-tmp.py --appver >/dev/null \ || rm -f bin/pagekite-tmp.py .failplease chmod +x pagekite-tmp.py mv pagekite-tmp.py dist/pagekite-$KITENAME.py ls -l dist/pagekite-$KITENAME.py pagekite-0.5.8a/scripts/breeder.py0000777000175000017500000000000012610153542022756 2../../PyBreeder/breeder.pyustar brebre00000000000000pagekite-0.5.8a/scripts/mk-self-signed.sh0000775000175000017500000000166012603542202017654 0ustar brebre00000000000000#!/bin/bash # # This script will generate a self signed certificate which claims # validity for one or more domain names using the subjectAltName extension. # # Country, organization and other expected fields are left blank. # DOMAIN=$1 if [ "$DOMAIN" = "" ]; then echo "Usage: $0 maindomain.com [otherdomain1.net otherdomain2.org ...]" exit 1 fi cat <self-signed.cfg subjectAltName = @alt_names [alt_names] tac COUNT=1 for dom in $@; do echo "DNS.$COUNT = $dom" >>self-signed.cfg let COUNT=$COUNT+1 done openssl genrsa -out self-signed.key 2048 openssl req -new -key self-signed.key -out self-signed.csr \ -subj "/CN=Anonymous/O=Independent/OU=Person" openssl x509 -req -extfile self-signed.cfg -days 3650 \ -in self-signed.csr -signkey self-signed.key -out self-signed.crt cat self-signed.key self-signed.crt >self-signed.pem rm -f self-signed.cfg self-signed.key self-signed.csr self-signed.crt pagekite-0.5.8a/scripts/tests/0000775000175000017500000000000012610153761015655 5ustar brebre00000000000000pagekite-0.5.8a/scripts/tests/proxy.rc0000644000175000017500000000365612055164463017400 0ustar brebre00000000000000###[ Current settings for pagekite.py v0.5.3+github. ]######### # ## NOTE: This file may be rewritten/reordered by pagekite.py. # ##[ Default kite and account details ]## kitename = test.pagekite.me kitesecret = bax4aee5g4a3w0 ##[ Use this to just use pagekite.net defaults ]## # defaults ##[ Custom front-end and dynamic DNS settings ]## # frontends = N:hostname:port # frontend = hostname:port # nofrontend = hostname:port # never connect # dyndns = pagekite.net OR # dyndns = user:pass@dyndns.org OR # dyndns = user:pass@no-ip.com # # errorurl = http://host/page/ # fingerpath = /~%s/.finger ##[ Built-in HTTPD settings ]## # httpd = host:port webpath = test.pagekite.me/80:/:default:/data/Home/bre/Projects/Beanstalks/PyBeanstalks/scripts/tests ##[ Back-ends and local services ]## service_on = http:@kitename : localhost:builtin : @kitesecret ##[ Allow risky known-to-be-risky incoming HTTP requests? ]## # insecure ##[ Front-end Options ]## # isfrontend # host = machine.domain.com # ports = 80 # protos = finger,http,http2,http3,httpfinger,https,irc,raw,websocket # rawports = virtual # authdomain = foo.com # motd = /path/to/motd.txt # domain = http:*.pagekite.me:SECRET1 # domain = http,https,websocket:THEM.pagekite.me:SECRET2 ##[ Domains we terminate SSL/TLS for natively, with key/cert-files ]## # tls_endpoint = DOMAIN:PEM_FILE # tls_default = DOMAIN ##[ Proxy-chain settings ]## # noproxy proxy = http:localhost:3128 ##[ Front-end access controls (default=deny, if configured) ]## # client_acl=[allow|deny]:IP-regexp # tunnel_acl=[allow|deny]:IP-regexp ###[ Anything below this line can usually be ignored. ]######### ##[ Miscellaneous settings ]## # logfile = /path/to/file # buffers = 1024 # new # all # noprobes # nocrashreport # savefile = /path/to/savefile ##[ Systems administration settings ]## # daemonize # runas = uid:gid # pidfile = /path/to/file ###[ End of pagekite.py configuration ]######### END pagekite-0.5.8a/scripts/tests/auth.rc0000600000175000017500000000023112055164216017126 0ustar brebre00000000000000##[ Default kite and account details ]## kitename = test.pagekite.me kitesecret = bax4aee5g4a3w0 ###[ End of pagekite.py configuration ]######### END pagekite-0.5.8a/scripts/tests/crypto.rc0000644000175000017500000000070212055162145017517 0ustar brebre00000000000000##[ Default kite and account details ]## optfile = auth.rc ##[ Front-end settings: use pagekite.net defaults ]## defaults ##[ Built-in HTTPD settings ]## pemfile=/home/bre/.pagekite.pem webpath = test.pagekite.me/80:/:default:/data/Home/bre/Projects/Beanstalks/PyBeanstalks/scripts/tests ##[ Back-ends and local services ]## service_on = https:test.pagekite.me : localhost:builtin : wiggle ###[ End of pagekite.py configuration ]######### END pagekite-0.5.8a/pagekite_gtk.py0000777000175000017500000000000012610153542022074 2scripts/pagekite_gtkustar brebre00000000000000pagekite-0.5.8a/COPYING0000664000175000017500000010333012603542200014047 0ustar brebre00000000000000 GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU Affero General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Remote Network Interaction; Use with the GNU General Public License. Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see . pagekite-0.5.8a/README.md0000664000175000017500000000103612603542200014273 0ustar brebre00000000000000# pagekite.py # This is `pagekite.py`, a fast and reliable tool to make localhost servers visible to the public Internet. For stable releases and quick-start instructions, please see: The full manual is in the `docs/` folder, or visible on-line here: **Note:** This program is under active development and the contents of this repository may at times be somewhat unstable. Stable source releases are archived here: pagekite-0.5.8a/pk0000777000175000017500000000000012603542202017231 2pagekite/__main__.pyustar brebre00000000000000pagekite-0.5.8a/pagekite.egg-info/0000775000175000017500000000000012610153761016307 5ustar brebre00000000000000pagekite-0.5.8a/pagekite.egg-info/PKG-INFO0000664000175000017500000000146112610153760017405 0ustar brebre00000000000000Metadata-Version: 1.0 Name: pagekite Version: 0.5.8a Summary: PageKite makes localhost servers visible to the world. Home-page: http://pagekite.org/ Author: Bjarni R. Einarsson Author-email: bre@pagekite.net License: AGPLv3+ Description: PageKite is a system for running publicly visible servers (generally web servers) on machines without a direct connection to the Internet, such as mobile devices or computers behind restrictive firewalls. PageKite works around NAT, firewalls and IP-address limitations by using a combination of tunnels and reverse proxies. Natively supported protocols: HTTP, HTTPS Any other TCP-based service, including SSH and VNC, may be exposed as well to clients supporting HTTP Proxies. Platform: UNKNOWN pagekite-0.5.8a/pagekite.egg-info/top_level.txt0000664000175000017500000000001112610153760021030 0ustar brebre00000000000000pagekite pagekite-0.5.8a/pagekite.egg-info/requires.txt0000664000175000017500000000002712610153760020705 0ustar brebre00000000000000SocksipyChain >= 2.0.15pagekite-0.5.8a/pagekite.egg-info/dependency_links.txt0000664000175000017500000000000112610153760022354 0ustar brebre00000000000000 pagekite-0.5.8a/pagekite.egg-info/SOURCES.txt0000664000175000017500000001134612610153761020200 0ustar brebre00000000000000COPYING HTTPD-PLAN.txt MANIFEST.in Makefile README.md TODO.md UI.txt __main__.py droiddemo.py pagekite_gtk.py pk setup.cfg setup.py debian/changelog debian/changelog.in debian/compat debian/control debian/control.in debian/copyright debian/copyright.in debian/dirs debian/docs debian/files debian/init.d debian/pagekite.debhelper.log debian/pagekite.logrotate debian/pagekite.manpages debian/pagekite.postinst.debhelper debian/pagekite.postrm.debhelper debian/pagekite.prerm.debhelper debian/pagekite.substvars debian/postinst debian/postrm debian/preinst debian/prerm debian/rules debian/pagekite/DEBIAN/conffiles debian/pagekite/DEBIAN/control debian/pagekite/DEBIAN/md5sums debian/pagekite/DEBIAN/postinst debian/pagekite/DEBIAN/postrm debian/pagekite/DEBIAN/preinst debian/pagekite/DEBIAN/prerm debian/pagekite/etc/init.d/pagekite debian/pagekite/etc/logrotate.d/pagekite debian/pagekite/etc/pagekite.d/10_account.rc debian/pagekite/etc/pagekite.d/20_frontends.rc debian/pagekite/etc/pagekite.d/80_httpd.rc.sample debian/pagekite/etc/pagekite.d/80_sshd.rc.sample debian/pagekite/usr/bin/lapcat debian/pagekite/usr/bin/pagekite debian/pagekite/usr/share/doc/pagekite/COPYING.gz debian/pagekite/usr/share/doc/pagekite/CREDITS.txt debian/pagekite/usr/share/doc/pagekite/HISTORY.txt.gz debian/pagekite/usr/share/doc/pagekite/README.md.gz debian/pagekite/usr/share/doc/pagekite/changelog.Debian.gz debian/pagekite/usr/share/doc/pagekite/copyright debian/pagekite/usr/share/man/man1/lapcat.1.gz debian/pagekite/usr/share/man/man1/pagekite.1.gz debian/pagekite/usr/share/pyshared/pagekite/__init__.py debian/pagekite/usr/share/pyshared/pagekite/__main__.py debian/pagekite/usr/share/pyshared/pagekite/android.py debian/pagekite/usr/share/pyshared/pagekite/common.py debian/pagekite/usr/share/pyshared/pagekite/compat.py debian/pagekite/usr/share/pyshared/pagekite/dropper.py debian/pagekite/usr/share/pyshared/pagekite/httpd.py debian/pagekite/usr/share/pyshared/pagekite/logging.py debian/pagekite/usr/share/pyshared/pagekite/logparse.py debian/pagekite/usr/share/pyshared/pagekite/manual.py debian/pagekite/usr/share/pyshared/pagekite/pk.py debian/pagekite/usr/share/pyshared/pagekite/yamond.py debian/pagekite/usr/share/pyshared/pagekite-0.5.8a.egg-info/PKG-INFO debian/pagekite/usr/share/pyshared/pagekite-0.5.8a.egg-info/SOURCES.txt debian/pagekite/usr/share/pyshared/pagekite-0.5.8a.egg-info/dependency_links.txt debian/pagekite/usr/share/pyshared/pagekite-0.5.8a.egg-info/requires.txt debian/pagekite/usr/share/pyshared/pagekite-0.5.8a.egg-info/top_level.txt debian/pagekite/usr/share/pyshared/pagekite/proto/__init__.py debian/pagekite/usr/share/pyshared/pagekite/proto/conns.py debian/pagekite/usr/share/pyshared/pagekite/proto/filters.py debian/pagekite/usr/share/pyshared/pagekite/proto/parsers.py debian/pagekite/usr/share/pyshared/pagekite/proto/proto.py debian/pagekite/usr/share/pyshared/pagekite/proto/selectables.py debian/pagekite/usr/share/pyshared/pagekite/ui/basic.py debian/pagekite/usr/share/pyshared/pagekite/ui/nullui.py debian/pagekite/usr/share/pyshared/pagekite/ui/remote.py debian/pagekite/usr/share/python-support/pagekite.public debian/python-module-stampdir/pagekite doc/CREDITS.txt doc/DEV-HOWTO.md doc/HISTORY.txt doc/MANPAGE.md doc/README.md doc/REMOTEUI.md doc/header.txt doc/lapcat.1 doc/pagekite.1 etc/init.d/pagekite.debian etc/init.d/pagekite.fedora etc/logrotate.d/pagekite.debian etc/logrotate.d/pagekite.fedora etc/pagekite.d/10_account.rc etc/pagekite.d/20_frontends.rc etc/pagekite.d/80_httpd.rc.sample etc/pagekite.d/80_sshd.rc.sample etc/pagekite.d/accept.acl.sample etc/sysconfig/pagekite.fedora pagekite/__init__.py pagekite/__main__.py pagekite/android.py pagekite/common.py pagekite/compat.py pagekite/dropper.py pagekite/httpd.py pagekite/logging.py pagekite/logparse.py pagekite/manual.py pagekite/pk.py pagekite/yamond.py pagekite.egg-info/PKG-INFO pagekite.egg-info/SOURCES.txt pagekite.egg-info/dependency_links.txt pagekite.egg-info/requires.txt pagekite.egg-info/top_level.txt pagekite/proto/__init__.py pagekite/proto/conns.py pagekite/proto/filters.py pagekite/proto/parsers.py pagekite/proto/proto.py pagekite/proto/selectables.py pagekite/ui/__init__.py pagekite/ui/basic.py pagekite/ui/nullui.py pagekite/ui/remote.py rpm/pagekite.init rpm/pagekite.logrotate rpm/pagekite.sysconfig rpm/rpm-install.sh rpm/rpm-post.sh rpm/rpm-preun.sh rpm/rpm-setup.sh scripts/blackbox-test.sh scripts/breeder.py scripts/installer.sh scripts/lapcat scripts/mk-dropper.sh scripts/mk-self-signed.sh scripts/pagekite scripts/pagekite_gtk scripts/pagekite_test.py.old scripts/pkvnc scripts/self-signed.pem scripts/legacy-testing/pagekite-0.3.21.py scripts/legacy-testing/pagekite-0.4.6a.py scripts/legacy-testing/pagekite-0.5.6d.py scripts/tests/auth.rc scripts/tests/crypto.rc scripts/tests/proxy.rcpagekite-0.5.8a/MANIFEST.in0000664000175000017500000000043312603542200014552 0ustar brebre00000000000000include pk *.py *.md *.txt *.sample *.1 Makefile COPYING recursive-include pagekite *.py recursive-include scripts * recursive-include doc * recursive-include rpm * recursive-include debian * recursive-include etc * exclude *.pyc exclude socks.py exclude sockschain.py exclude .SELF pagekite-0.5.8a/__main__.py0000775000175000017500000000100112603542200015101 0ustar brebre00000000000000#!/usr/bin/env python import os import runpy PKG = 'pagekite' try: run_globals = runpy.run_module(PKG, run_name='__main__', alter_sys=True) executed = os.path.splitext(os.path.basename(run_globals['__file__']))[0] if executed != '__main__': # For Python 2.5 compatibility raise ImportError('Incorrectly executed %s instead of __main__' % executed) except ImportError: # For Python 2.6 compatibility runpy.run_module('%s.__main__' % PKG, run_name='__main__', alter_sys=True) pagekite-0.5.8a/TODO.md0000664000175000017500000000565112603542200014112 0ustar brebre00000000000000# TODOS # ## Known bugs ## * PageKite frontends will disconnect tunnels when kites run out of quota. This will hurt Kazz.am, should recheck all kites and only disable out of quota ones, disconnecting only when all run out. Also, UI issues. * XML-RPC CNAME creation fail * Signup message weirdness * Poor handling of reconfiguration * Poor handling of FD exhaustion * Kite creation can be confusing if a name is already taken. * WONTFIX: SSL verification fail - unfixable with pyOpenSSL :-( ## Code cleanup ## * Files are still too big, github chokes * Function naming is inconsistent * Need docstrings all over pagekite source * Unneeded ( ) in a few places * Bad form: if this: thenfoo ## General ## * 0.5.x: Add UI to report available upgrades at the back-end. * 0.5.x: Windows: Auto upgrades? ## Built-in HTTPD ## * 0.5: Allow uploading, somehow * 0.5: Allow access controls based on OpenID/Twitter/Facebook ? * 0.5: Create javascript for making directory listings prettier * Add basic photo albums * Add feature to thumbnail/preview/re-encode images/audio/video ## Packaging ## * 0.5: Create Windows distribution * 0.5: Create Windows .msi * 0.6: Package lapcat * 0.6: Create Mac OS X GUI and package: Talk to Sveinbjörn? ### Optimization ### * Add QoS and bandwidth shaping * Add a scheduler for deferred/periodic processing * Replace string concatenation ops with lists of buffers ### Protocols ### * Make tunnel creation more stubborn, try multiple ports, etc. * Add XMPP and incoming SMTP support * Replace/augment current tunnel auth with SSL certificates ### Better kite registration ### 1. Quota calculations should be done on a kite-by-kite basis: - kites out of quota get disabled - tunnels only die if they have *no* live/challenged kites left 3. Stop requiring the reconnect after a challenge, just establish a tunnel with zero kites? 2. Registration - recognize the challenge/response headers within chunk headers, so kite can be set up using NOOP chunks. - add a back-end initiated "remove this kite" message ### Dynamic DNS ### Dynamic DNS updates are the only SPoF left in the PageKite.net service, should fix by: * Modify pagekite.py to update multiple (all) update servers ### Lame-duck ### Lame-duck mode is when a front-end knows it can no longer handle traffic but still has established user connections. The goal is to shut down as quickly as possible, without dropping (too much) traffic. * Trigger on: normal shutdown, out of FDs, OOM, uncaught exceptions * Add signaling to tunnels to warn that FE is lame. * Shut down all listening sockets and daemonize so new FE can start up * Implement protocol for sending entire live tunnel to new FE process? * Give existing conns 60 seconds to finish? * Add "lame" recognition in back-end (also "rejected") pagekite-0.5.8a/debian/0000775000175000017500000000000012610153761014246 5ustar brebre00000000000000pagekite-0.5.8a/debian/rules0000775000175000017500000000116012603542200015314 0ustar brebre00000000000000#!/usr/bin/make -f DEB_PYTHON_SYSTEM=pysupport # This keeps backwards compatibility with older Debians DEB_DH_BUILDDEB_ARGS = -- -Zgzip include /usr/share/cdbs/1/rules/debhelper.mk include /usr/share/cdbs/1/class/python-distutils.mk # After the basic Python installation, add files for /etc/ common-binary-post-install-indep:: mkdir -m 755 -p $(CURDIR)/debian/pagekite/etc/pagekite.d/ install -m 644 $(CURDIR)/etc/pagekite.d/[23456789]* \ $(CURDIR)/debian/pagekite/etc/pagekite.d/ install -m 600 $(CURDIR)/etc/pagekite.d/10* \ $(CURDIR)/debian/pagekite/etc/pagekite.d/ pagekite-0.5.8a/debian/prerm0000664000175000017500000000220712603542200015307 0ustar brebre00000000000000#!/bin/sh # prerm script for pagekite # # see: dh_installdeb(1) set -e # summary of how this script can be called: # * `remove' # * `upgrade' # * `failed-upgrade' # * `remove' `in-favour' # * `deconfigure' `in-favour' # `removing' # # for details, see http://www.debian.org/doc/debian-policy/ or # the debian-policy package if dpkg-maintscript-helper supports rm_conffile 2>/dev/null; then dpkg-maintscript-helper rm_conffile /etc/pagekite.rc 0.3.9-1 -- "$@" dpkg-maintscript-helper rm_conffile /etc/pagekite/pagekite.rc -- "$@" dpkg-maintscript-helper rm_conffile /etc/pagekite/local.rc -- "$@" fi case "$1" in remove|upgrade|deconfigure) ;; failed-upgrade) ;; *) echo "prerm called with unknown argument \`$1'" >&2 exit 1 ;; esac # dh_installdeb will replace this with shell code automatically # generated by other debhelper scripts. #DEBHELPER# exit 0 pagekite-0.5.8a/debian/control.in0000664000175000017500000000224612610152541016255 0ustar brebre00000000000000Source: pagekite-@VERSION@ Section: net Priority: optional Maintainer: PageKite Packaging Team Build-Depends: cdbs (>= 0.4.52), debhelper (>= 7.0.15~), python-support (>= 0.8.4), python (>= 2.4) XS-Python-Version: >= 2.3 Standards-Version: 3.9.1 Homepage: https://pagekite.net/ Package: pagekite Section: net Architecture: all Depends: ${misc:Depends}, ${python:Depends}, daemon (>= 0.6), python (>= 2.3), python (<< 3.0), python-socksipychain (>= 2.0.15), python-openssl Description: Make localhost servers publicly visible. PageKite is a system for running publicly visible servers (generally web servers) on machines without a direct connection to the Internet, such as mobile devices or computers behind restrictive firewalls. PageKite works around NAT, firewalls and IP-address limitations by using a combination of tunnels and reverse proxies. . Natively supported protocols: HTTP, HTTPS Partially supported protocols: IRC, Finger . Any other TCP-based service, including SSH and VNC, may be exposed as well to clients supporting HTTP Proxies. pagekite-0.5.8a/debian/control0000664000175000017500000000224312610153615015650 0ustar brebre00000000000000Source: pagekite-0.5.8a Section: net Priority: optional Maintainer: PageKite Packaging Team Build-Depends: cdbs (>= 0.4.52), debhelper (>= 7.0.15~), python-support (>= 0.8.4), python (>= 2.4) XS-Python-Version: >= 2.3 Standards-Version: 3.9.1 Homepage: https://pagekite.net/ Package: pagekite Section: net Architecture: all Depends: ${misc:Depends}, ${python:Depends}, daemon (>= 0.6), python (>= 2.3), python (<< 3.0), python-socksipychain (>= 2.0.15), python-openssl Description: Make localhost servers publicly visible. PageKite is a system for running publicly visible servers (generally web servers) on machines without a direct connection to the Internet, such as mobile devices or computers behind restrictive firewalls. PageKite works around NAT, firewalls and IP-address limitations by using a combination of tunnels and reverse proxies. . Natively supported protocols: HTTP, HTTPS Partially supported protocols: IRC, Finger . Any other TCP-based service, including SSH and VNC, may be exposed as well to clients supporting HTTP Proxies. pagekite-0.5.8a/debian/dirs0000664000175000017500000000004012603542200015114 0ustar brebre00000000000000var/log/pagekite etc/pagekite.d pagekite-0.5.8a/debian/copyright0000664000175000017500000000221612610153616016201 0ustar brebre00000000000000Format: http://dep.debian.net/deps/dep5 Upstream-Name: pagekite-0.5.8a Source: http://pagekite.net/pk/src/pagekite-0.5.8a.tar.gz Files: * Copyright: 2013 Bjarni Runar Einarsson 2013 The Beanstalks Project ehf. License: AGPL-3+ See the file COPYING, included with this distribution. Files: debian/* Copyright: 2013 PageKite Packaging Team License: GPL-2+ This package is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. . This package is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. . You should have received a copy of the GNU General Public License along with this program. If not, see . On Debian systems, the complete text of the GNU General Public License version 2 can be found in "/usr/share/common-licenses/GPL-2". pagekite-0.5.8a/debian/pagekite.debhelper.log0000644000175000017500000000077712610153674020507 0ustar brebre00000000000000dh_prep dh_installdirs dh_installdirs dh_installdocs dh_installexamples dh_installman dh_installinfo dh_installmenu dh_installcron dh_installinit dh_installdebconf dh_installemacsen dh_installcatalogs dh_installpam dh_installlogrotate dh_installlogcheck dh_installchangelogs dh_installudev dh_lintian dh_bugfiles dh_install dh_link dh_installmime dh_installgsettings dh_pysupport dh_strip dh_compress dh_fixperms dh_makeshlibs dh_installdeb dh_perl dh_shlibdeps dh_gencontrol dh_md5sums dh_builddeb dh_builddeb pagekite-0.5.8a/debian/compat0000664000175000017500000000000212603542200015434 0ustar brebre000000000000007 pagekite-0.5.8a/debian/pagekite.substvars0000644000175000017500000000013512610153672020013 0ustar brebre00000000000000python:Versions=2.7 python:Depends=python (>= 2.3), python-support (>= 0.90.0) misc:Depends= pagekite-0.5.8a/debian/python-module-stampdir/0000775000175000017500000000000012610153761020673 5ustar brebre00000000000000pagekite-0.5.8a/debian/python-module-stampdir/pagekite0000644000175000017500000000000012610153655022375 0ustar brebre00000000000000pagekite-0.5.8a/debian/pagekite.manpages0000664000175000017500000000003412610153616017550 0ustar brebre00000000000000doc/lapcat.1 doc/pagekite.1 pagekite-0.5.8a/debian/pagekite.postinst.debhelper0000644000175000017500000000070312610153667021600 0ustar brebre00000000000000# Automatically added by dh_installinit if [ -x "/etc/init.d/pagekite" ] || [ -e "/etc/init/pagekite.conf" ]; then if [ ! -e "/etc/init/pagekite.conf" ]; then update-rc.d pagekite defaults >/dev/null fi invoke-rc.d pagekite start || exit $? fi # End automatically added section # Automatically added by dh_pysupport if which update-python-modules >/dev/null 2>&1; then update-python-modules pagekite.public fi # End automatically added section pagekite-0.5.8a/debian/preinst0000664000175000017500000000171412603542200015650 0ustar brebre00000000000000#!/bin/sh # preinst script for pagekite # # see: dh_installdeb(1) set -e # summary of how this script can be called: # * `install' # * `install' # * `upgrade' # * `abort-upgrade' # for details, see http://www.debian.org/doc/debian-policy/ or # the debian-policy package if dpkg-maintscript-helper supports rm_conffile 2>/dev/null; then dpkg-maintscript-helper rm_conffile /etc/pagekite.rc 0.3.9-1 -- "$@" dpkg-maintscript-helper rm_conffile /etc/pagekite/pagekite.rc -- "$@" dpkg-maintscript-helper rm_conffile /etc/pagekite/local.rc -- "$@" fi case "$1" in install|upgrade) ;; abort-upgrade) ;; *) echo "preinst called with unknown argument \`$1'" >&2 exit 1 ;; esac # dh_installdeb will replace this with shell code automatically # generated by other debhelper scripts. #DEBHELPER# exit 0 pagekite-0.5.8a/debian/copyright.in0000664000175000017500000000222412603542200016576 0ustar brebre00000000000000Format: http://dep.debian.net/deps/dep5 Upstream-Name: pagekite-@VERSION@ Source: http://pagekite.net/pk/src/pagekite-@VERSION@.tar.gz Files: * Copyright: 2013 Bjarni Runar Einarsson 2013 The Beanstalks Project ehf. License: AGPL-3+ See the file COPYING, included with this distribution. Files: debian/* Copyright: 2013 PageKite Packaging Team License: GPL-2+ This package is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. . This package is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. . You should have received a copy of the GNU General Public License along with this program. If not, see . On Debian systems, the complete text of the GNU General Public License version 2 can be found in "/usr/share/common-licenses/GPL-2". pagekite-0.5.8a/debian/pagekite.prerm.debhelper0000644000175000017500000000055112610153667021043 0ustar brebre00000000000000# Automatically added by dh_pysupport if which update-python-modules >/dev/null 2>&1; then update-python-modules -c pagekite.public fi # End automatically added section # Automatically added by dh_installinit if [ -x "/etc/init.d/pagekite" ] || [ -e "/etc/init/pagekite.conf" ]; then invoke-rc.d pagekite stop || exit $? fi # End automatically added section pagekite-0.5.8a/debian/docs0000664000175000017500000000006612603542200015113 0ustar brebre00000000000000doc/CREDITS.txt doc/HISTORY.txt doc/README.md COPYING pagekite-0.5.8a/debian/pagekite/0000775000175000017500000000000012610153761016037 5ustar brebre00000000000000pagekite-0.5.8a/debian/pagekite/usr/0000775000175000017500000000000012610153761016650 5ustar brebre00000000000000pagekite-0.5.8a/debian/pagekite/usr/share/0000775000175000017500000000000012610153761017752 5ustar brebre00000000000000pagekite-0.5.8a/debian/pagekite/usr/share/doc/0000775000175000017500000000000012610153761020517 5ustar brebre00000000000000pagekite-0.5.8a/debian/pagekite/usr/share/doc/pagekite/0000775000175000017500000000000012610153761022310 5ustar brebre00000000000000pagekite-0.5.8a/debian/pagekite/usr/share/doc/pagekite/README.md.gz0000644000175000017500000003026612603542201024204 0ustar brebre00000000000000‹Í}ëvÛF¶æÿz ´Ü=‘Ø$%_rS·}Ž¢Ø±æ8––)O’åîÕ*E PgÍš›¿ób³¯U²´ÝkÎ%‰H¢P—}ßßÞõ ©ìÂ]g­W›ä1ðçÁŸI¶ªr·rEÛ$6i×Eár—&µ»quã’ª.ï6Éí2›-“•½vM’µ‰³Í&iKú ±¦qõM6sÉ~³†_Y§H^^^^$eMÿž$ø W$e‘äåÌæË²i“›¬É¦¹ÃÚ¥Kn³ÔÕæ¬h]]¸vœ$gmbó¦LšuU•5L¯„ŸÕ8£¶œ•y“ìgÅ,_§Y±H&“—ÃdºI¦nioàƒÓHšÊÍ2›gÿVD3¢åŒ¹¬7ÉÕø0Þ“Ñhéòê*™Ã¬³¢iëõ¬ÍÊ^ÔΦ4˦\×3wCüh x:MæuY´#Wàžåv“ènd0›å×hÛä¯Ë¶­šãÃðRXæá³!ì’Ù”ëd»V¯‹þ»NÊÛ"Y7¸²hŠðRóW›üKÿSØ•{ û÷쯇ö™yð 9'—4ÅržÌ`%D€@àÇÇÉ»KXôÔ6Ù¬ùø—ü}ÿ?r`ø(y÷ÆýsÕBj7Jíþ©C<Â!~pm‹›Ò´¶naï?bˆúI<ÆNËbž-Ö5Ž'·¢C•«•…̳Âm0Ë3â qéêUV”y¹Ø “ey›xVº-ëëf×$àW´£°Šw'é-f0ûu~ôŽZy¬»§À¯¸”–Nivt8ìSÐY|2®éìéû†°ÉlÝ´°KÈyˆYgSuOº}žØ^ÍÜu÷_¾Îòv”‰ì`‘ñžýhÚJ‡ø’OÖÝeM+˯GQ”ŠÜˆFmôdKá«>m|ÿzò1‡’~3¿–! 7£I”8ýI9ŠrYÖ÷ Ñ–µñ m¦½U²‡f}zþúõóÓ˃ûf‘U~ˆoqˆÉäÕá嫉?Îf˜À?«2#!;2y}ÖŸE˜D»·Evwø*+ÖwI³iZ·ÁÂaQ[Š;g±.îü(9&$ˆ™(f²µ÷<íYm¾ðC« K¥‰–ë6egÓnhÀ}Ín²@Ú4L™ï^e«¬¥wÂÌì³²×’ét½ØÉ®y¶â1€6ßÖ.Íä±xðc(~zð¯ÊkÖ,I½¼†D2 êO¢@Òê+ð𖎸~€/ºººè 8›¾o’‹M»„C}4¾&È\ýñ©>· ÍÇý'Ym;TдŒäÖM•ßQûõ³”oá[ÑJ Þ²Æ¬ÊšEvYäT IÁ„pwU öÊ^¯E:uŒ²UÐH8©ì ö^k“ïµÅ¢LR°}ò²Â•{Ë%6Ro¤°WàÑT&›ø) Ø0O^á2H4 I¿:ª7UËb%ŸéJ¾!2¼/Ï“ÂÁËÊM“ºŒ-!Ýø¯þŒ#âHÕæ~¯ƒ]J×¹#bm±°eEÙê¡¥I6§ññžÜZX(Ì·.sb±á]Ó6°Ä_Ä8Ieòì x™¤CÉ´é[6úóæð 2¼.[w<tž†Ãk¼’!+M ¬’ÙÅ&ÙG M ÿ]mN—6+èD–°¹îîªj™f¹JˆYO£SË‘w¥´5y;p›tÞª÷lžƒV•…ÛKhÉYîÌG­{€˜gÓÚ‚Á™5Jú)ÍP}œ¼,o?sœZ8[8éÈî„E¥®OmiüÑtöÖõ=_×´æÔµ`Ž‚ {ßM4ΙÝFémv¾È˦9¼Øt†¤³|—¼[W¤¹fÉß?•QúÏ&@ ýûÆÉ ”}@Â3ðDp‰Ï`ßèhÃ"â®ÃoüYSÕŽv~6»F lˆg ¦_êw˜ŒSI°yãºZ•› ò×îä*ÊÚé"Ë`ËWA²ÍÏr×xÏŽIï$]LJXW¸xX ¼m®‰ pPR°n´‚ÃMlš‚H†3ÖÀÒgË%#ý,+²"ƒcÓ¶'ûƒÉùÏ/_ž½þaìߺrp§Fr öón— É‘DRÃx|-ËW]’—³Çß>ë{z f£Hä9HãwvçLhc_ºš…˜MæîåÆ|ƒÌ·è¼6²i’ç¬, ú®8EíQÙ¦!‹HàÌŽ·öû›£Dæóâü¼3“äÏ:Àá¥ãÓyYn½:ù:£nãu†›¢ýNîRV¤îÎ5Û¯=¼±õáííí=/‡„½Lþ,Ï÷_[  ¬G£/ÆýÝ~Q8œG’¦YîX(ó˜]p¼ºÉ {Öx\@}SÒ¿Ì2@ì%zÚ°¶Ü{RžvL×ðk€^ðÄ™S7·ë&Ï®QÂí¤} èäojßw2»&9yý½²¿¹ß¼yÏævýðvs2Ž÷ŒÐ§$û6ÛDRŽ;^,²fy¶Gþ£/@)›Ê™«*6’`[À7ÙÊ…P ì}»¬ B—µ¢ð‰h4âP=ØûlxÇéG`Æ SJ죦TAAv& œG´5/ó¼¼%ñ¼,ëv„ªxÇùmíÔë˜Q3a›%öSI¶Hš ¡*")GV_—á÷ÄT$`tHcÑP‡ÏÞ÷{RwÏb6`œ_¬ Tg4ÃNàÄ2Ÿd8Ä«!\ÝðÍçÑœ³<‹U'8>Žr2õ t‡ŠeMÁDâ€@MAB ¿ÇÔegV•4°ëf º¡I¥ÉU8øzv…ÚÝI‘ Év†±¯$=xø;ó°!Ì­k4ø FOä(,ÅV(œT¡‚(!Õ†Æu´àh²†';ÝÈÉÊn4,|:$íÕÉ ¾Lßqlúæèè(IÉqmQ;séV@¶Î`oÒ¬¡h[ÖîO¿|ÿp¯²¦%Q±µróŽAsø¹>z‰-À¯™È—ïxH¾¼o"êP|EÂmƞкJÑåÙ:”%^SƒO·Î­7††Ù[8Ûqr¥Âø*™çvAÚ VØ?X—yC^ÅKÐ?ȱ'öbÐ#àP‘ýM$f N´Xçù:»ëøG{íå5JÏyVãÖÖ‹5ù~di¡0A“ŠT”°ylàá kþ`ngŽhSDh¤õŠ7ÂÍ-ëe<7ò¬ˆTaB{×ìEHŽœ·a¹}cóÌŸ­™ÁAôÀ|C?bW\…Iœ× §«i²8Ã)™Ü®‹ÙˆÛO¼°5¸`èo á3ð"à€Y«Š”zO‹Ú®>›@A ´'àÅ~ æÚgüi Bàé —g¢:Ž“_pÿåœÁ`ˆjXÖOºO”NLRˆ>Ž$Œ5®‹<_ÆŒDIˆÁ@ãpê“–5¾­,سC+|eg°Õn¬‘+%gLD`Ô™'^Ôèè½`+0ŒlÅz‘ùüýÖ`…Î’³‹à)À ï³æå:ÝkÜ À0óžß.ëhÔÒŒ&+&×ÍZ¢.É,‘*Ef|üú/Aƒåžâ3Á(Ñ´Å@ç SØ9ƒhÇ/w$²¸ø Ú:fr}„¢(x$ r ¬êŒ„-Uú%m«`‘·¶N)W&óiÊQ†bb‚y¼qMr6¨­ásŠØªÿ–CLI›™v¢ô(>J°<‹x7[ÈÞÎ ±p>jâN]{ë‘á Cqf*@&´È@¼+f›Àö à€t)\‚ÏÀffN ²WJê€åºVRÃ@ºFâÐ'—ûø#ƒœï *¨˜ QJAv6f®¤$5}:ü£/À¤½}ãÉ‚q¾ªÈà­Ý´™$ 8ÔP[ˆÃ่›…*Iø´$ÞEyõ –U(—$h f 6ä̲1]—ëÅÜL|Hysâi9#M¤æîILÆ”`GW}ÜX“Zd˜Å!|œÃq4VÌ¥mï“7œf]Q“4‰".ÀšÄ»# \®z(!;ÚDÄ7¿güþèøà|W²ÍX¾2¯rü‰è¯DÞô¡Pï]ñfò€ñFïJT'Q¢zKT®0ª:E:é¡ïòcÁðU|ˆÍxçaûųs–ض!·Ï!øÖ+‡ž$ÚyN2×’Þ'‰{ ySf¤žæë†èþSÿnc£#B…¶“¨wÏxvug¾G‘H‡ÆÂ”›fsM)°×\Ó;ÐOn>ìJ—€bîå[‘!ÏÔ2\ÚªEÆÁÙ²‡Ù€ XƒÜšƒâZ×<é`Xdý€CÈá×騠ÇÈä ÝuV¸Ue…ùärò­úÝÄÙ+W"ƒÞ.Kú^ÂÌ8}êß· [À$rV)éƒ3SÒ…×цH‚Ù{bÌbDä*i6Àw ùj.%yáÐYF1wýFßájÑÍ?ÛíkžÌ9ÖrYØùýE øPü?ÊâŠ8+’‚œ- 3¬V|4ZzÄ'Çó” 7 z°¯awÕåÀï£X[®ZàÃT“þÝ8GØ|gÿÉðSŶQrícóö^¬0kÅY/âÅ./i˜[Y”P ÂËlÅ^a‡OÐI‘4­è$Wgêh£ AÊ;z£0- º€?<$ª±1»1L„áCŒÕã«Ö•!7ž÷AZy[ª¥ ƒiù˜N€0)a`øÑc°ŽÉ;JÒçñ“›bÆÈ‰cô™ÏôþÇš]Ï=yòøƒŸ_¾|¾ýˆŒNrÔ¶`äæÈwì·kUb§AóyÀ%á+Bµ?dYFÔb®’4dþ*Ö«)ÚÒˆ©¤ùT|t«ý}Û~H;µóÙhƒ² ÙN";Ýèå‘ï>è J(p*$ÈÈ;Æy ò§0Êâ 4uA~įöºÓ'#<Ù²Æ:bÊ!0‰³¦ðÂ`?¦n˜’'îƒÏû˜ºY¬+½_Wìh‘Æh’˜p˜Û‘…±OØÏ%‹£BŽo©Ñ´F|­RŒ@FºÏ½ç ëì»vëY?ØÎŸ0ÓXß‹$ã™ê$÷òžs@®™5ŒtX4BÇìø8.Ñçž¾<Ÿ\_œ¿¹üÍ2 ?íë-è tHH„qu}ÞîþðÞâ$ =r3Âà‚c ×˜pؤðáÅhDãaµ—¯&#A£àæ²»4$]„ãÃN¸Ì\ÝyÒô½ýæÛ-œÄç`憹‹™áq`†{¡†l0•ÁIÜÈõÙÁ}µ†BÈ(5¨âÎÖ ~OœNtýPe¼ƒY€Iœ‹ŽYî%„Ü2ÈÐÿÒ¨°²GÇÂ#yG…úÈÜ6eþžÍ{¶:‰ý(eSÆäƒŽ%ÙY=ísDËÕ‰9I´ëÛƒ~ÊVyåI³NË{*kôgчô’§`4âøð'Jè†xjHZyà9Õ)‹ 'æ+Ó'¬} B>Av=¥a9š `‰0 ²µL× T¦¦àžâ·}iÅpogKU]Aʳm`¢—z-µû>AÔ\¸zæ*ÊF ™ u@¿"*òÉÐù•3­K ÝU£ ‚¨±9GÒ¡¿më‰O ®ÇCuY¶Ã“X¸ÂÕ¤aÓn1ݰ®`ªäs•œía|BÒ äDåÎñ3z±OW£`0AóhAØÿ^›L‰pWoúI0xÉæ0.”"&¶ÁÌ›0+º½¶QåÀ4u…i) ˆ–"}yn0ÓTQP3Í0’ËçKùuÍ`2ÍXP4ìüï¯1Œv †'“2ñnD䭲Ųeç­tŸ”­†hzýfÞŒ‘š¿?ÿñäìõøôüLjÇ>â9²¡åá×Ï/ýgÊ¥o PS kÄŠF‚J* œ6 )-">VT%f§3<]R\ß‘L‚ ÝÊ«!ÒGf¦uYQü¤ª³›,w GçÔ”(üŸNÌÙæiQNËts\”‹º\WŸô¸vŠB Ÿp’)~À3pÖ7o “$<'ÂÐ{ûŒ¡ßco¼°íAbðÆækG9(LÀW¶F~ž8ŽüE†^%7; ä(ƒË¶LPz°%)›•æß«hÚ*¶>$)¶³L¬ŽÁ‹³Ÿá’Ÿ2Œ=HNà¹Â¿«*éû æ;Iü©€z“}ïEÍá:ÞbMÁ7T3¯Ê‡Þ…è›8Ä݀ɭðPÖ”­ã­n\>'Ü!o_  !8Üðlm×Ü®Ü #‚W Z eñ»=Rç)¾x ø¯ÆcY³ÒÁéÄ“'*ïVä4ˆ“d„¡ÃÑÝ—G߆W]» f_ü‹F¿üÏRÌ>þêK£L€œXöƒ85ɪC¨9zò·èÕR“°h<¶šµÂ30Ó‘\!þðÜéw4à"I(p Õ}>#|VÆl€ÉÙßRjCýùúÃÕ€Iè`{áDÅ“RX^2Øe1’Œ S~;RþIòƒš#Q@_ $cG¤ÈŽ2ä‹ éÿì‚»Êfud[Ü.7¬›1žÓ°ðA‹BÍ *›Ä(B™¯9UÈÆ585+ytåñF:Rׂ²ëÕGZò® ò…MiîøL¬~4s9æÇðLñÚ€Ñ{yzv…Ûaz4N/•JªÀ"ÀÑ•&ɘ æØÙ!MŠEÃøëMØm”á†ÈŸx0'ÏÙn(¬, Kï’0ïåÅLgOdyKùmAaQä€$l¼¦‹NöÑ@_ÁêS—ˆt=ég¢’#p-¿ö Ü÷ˆ¾Õ/”' £è¸QTN‘ n=~=èç$V˛ۘLh§\ÛmÉŽ$±›G®üˆÐ,v€áÄ£=E¡ ¤Gïò¹hRNåt§cü®ës…^´‘ŠKòêfizáé@¬Ý¹uÛq«> ꊌÞ^MˆÏkûŒ=Ín†>Ò›b¦hq¯ <ÞbA)6ŠšãG{Ë%À ry}{낪¿ö42Õ[Á#÷±ð ­Dù´Â Ë ¢£þ"äþæá§° ßü~õý®Àíûâì;Á8ûû‘#è¼åÛ£ãX§e@ëgQ¶^¢[ÞÚ%”Æ£0AW‘\À pT,Äm~‰n¨B§Ò!ø… üæa´—iýTà‡^†‰žÝú@¦'Éf) š>¦s(@›€0E ž½™gtæå¼éêØI’z7NÎ-ð8R×!¯‹à¿3[IQ¸ÙŒ{âE@ÆxNÍX¼Œ¡v9É1‚e{Ç ¢*˜ÞŽâžÎ¦­í|ްœÌwcÖEoÍLòù“­)gÈ8m¯¢ÈyÒ)H¥J"eê–è/êÞ‹ˆ7èȃe…À2ì À‰A;DR‹ø°‘" (QC§ÇeÈÑÔ¯žã ,pl1OïàÒ´(ÁnÐZ§Ä^|ði ›…¡?0kì"Œ¼(”–µo8ÓÇã‡FQkàð:»J0â¢á¥a ª¶d.9ÆoÐ|ÇâVCh»kRiŠh—cN¯œ-$6”ÍU—ë1Écm†/ÐÔ I=°ãyYŽaÕ6ƒ£Ò"ORƒiø‘"ïh«M ÇjC„GìbX½p1 d~Áã­€CU­·õµÐM–À#£èü {¦6Ç?àÄ-6Rƺ¨°PxÖߊ‰Õâà-d5~|€ð.ôsŽÍÿ@"F¸’²L‡‹’Q¤Ðˆ>Vá˜;¦Ú…ÖñÑnHŒ9H*v °oÅÜf9ˆ¦ƒŽÈÜ^¨±ÛÒr(ÇŠÚΚa‚(b~(K´@]uìK¸Hu5ã}‰§ÉŸø‚‹‘¾?_aZtÀ_u«!0øû@ @¡p·+ 5Ná&÷~ø×Ðl9oÆC+†›‰°"‹HRWç›Ê6ö^ÿ‡QC/ü¹6í¢·Ím•m¦DÉæ[ÑÝ]Ù6äNZ’¸O»ÑÒJ„¤)•[ n­uUл.Úæ”Gœ{`|³,’ÝÞ«˜gwqK•­B)6¹0œÒ* ë‡iRlžwœ+º)Ha¼âÚ/Ïö‹&Nµ›¨ÓÁ v?ë( á&aÝ“r¨2Ñ㓱 G4ðbÍgý¢÷ °Ôõñø‰ ¦Õêh½R1·K/ùT’àím¥š4©hdHüvÆ`Sš“ðd˜]ÙÝ>üNç)w¯¯3 ˆÇ‡ ƒgýL#¬‡G.ž0¨78ßR´Ÿú ÚÞj¬0@å)†â#¦Rž2r”ƒ°Oì£Q( 8s]ˆ“»åaƽ4¨9 ÜÜ-˜`~—Gá}ïQ+Ü1 «%Ùorã \„‹¤‘ƒ':$7 <0DÞ'¶¤Pœ@²‡‰¡:àü­O¦â3XïTéI£Ï™ßGakr´¶ ÜOÍ­h¯õæwI]Ps k©#ÜÁ^¿z^ÑÏ ¨º/"N]4 ^I‡'8y2‘(¾¤¥ß=‘”ä¨Qä «ò|œ˜Èj19ÐË$ìšgò bLBRL» Ù!lLì“Õ‡Ý!ò˜Îœ1òF=‘°öáhDhc ‘'T™lÝ”ËM–ò»·òÞšød?6eù†à€DŒ&ï'§[l¼2±@O@å´CÓ/«ñº Ä=þ#±}Òÿ¾ã:ë‡d·ªŒüêÛñ=;÷Ëo>üûo¿ýrüèÑWã‡O¾ýðÏ~ýÍøkxÅ“#øÿÇÆÇ˜ ¢}@  ‚”4‘$Ø/J©z‡&£Ðxs0ì8sD`s6*¨ÁòHwBóyøpÐŽ«¦Ê¤0›YŽ1Ip÷ÏâÊ 1Séœó²¼C ™Jk7éè£Î.ÚP ÆûÏ éRjmk +¥¼2HE/±†"ncvwÞñ×b…àW’âGK¾G‹´¡Klµ!f¬öá°¬µqà›…dvì3¹›,?.ìãZQWü»qY/®†¥î¸Ÿ–U/Ö‡[~¨yúB/ãoÎ__>ýý„Ò©ʉÂ64«§o'Ïß_œL&ÿ¦ù¨£qøõý¤³ 5K:;œ´V×õ$ZºïÌLQ޲ µl/#Û©ãä\ëùõä¨YÏt#'Ë™&Pôy¯]àÛ7¯†A‡ÊIx‘/Fª ·k§Ø4×)Ñ¢„¥"ÿ´ŸUM2ŠÐô=¡ýÈÀ¼P°XûÉã¬øß?DlP²2õ¥8*$@Ãs‰tÔt1l¨nó~ê¶Ui0 )™P8?aÂY™'/´´½CC¬hÆþ‡E6;äÿƒž×°èÓ°Æÿ¶ÚdÕSÞ²/ÌçrÓÚ²ŽÝ´¯ÉM»¿wZœõýŠz8&*Zõž;VRÕéÎA*Ý¢iøf\¡¨%¼c¹n%†û»Þ*UMN³F`¥„5ñqg+s×n“»Ã¶Žq1Üá ¼ðb³ÒFºÌ%âõ‘ %§lµÂ$~S»}“xò:”¬Íƒélîß÷•vnooÇp00o¡€8<Ðz@ y³~ em£=Š[C€0†1²ùæŠÊûAlQ!üÛ«CÓEVüIJ útr~ú_“OƒÑúüíÑ—Gh^tDz_í­[T?eÊ>‹O®uÒ¿T²•;ªa7YÁR‘œ0"&V™lV’鎟êf(:€†??[\&«: ÿÍ8y_§C_ЭĠÁ0ô5JgljνÚÞîùÄŒZ ÄÓt@2S–^ÃSÇxNÃéá´›Èõ¤ –ÿkTsCƒ!,[5³’–N ddï-Êḻ{ Fg¾fX­´ªBš 'Æ:‘e1ð—¿ìÀúRʽkù¤†]Á‚<òF~”§Ô…c„¤ Wû"?Júvˆ²mp“jt‰ˆØÊÅåØ4&Ë §‘êpÎC‰àtêƒþ)xÞrøèÑ..‡O{y/Æ„°Š0fõNã´%̽•L° Ûa +® ÇšÈÐz¡_“Ýݳ Ž5ºRIÑpQí厺@ 4Ûøà¸ïL ïud¾P´íç3‹  ¨ ÆM³<äia ¶ç›qÉ…YŒ"QÊ^*­U%;½cbjAÆŽà¦ÐÂKô ^žƒqwÁoN—nv_ƒ:,¸þ—–u*4uì~XÌ’ÑÏÞÓÝm EøŸ–ÉŸ*wþ¹¯æ‹7'?,¶uáÆ}‘ h³¤Tdê°d’¢!øº¢$"pu¨!áæÕK†™ÖnŽô4u”íÀ¶*©*p’:hŽ?Ÿ™•w¢áߎ“j»%~}ÏΪo %dìÈá«FŹÉj¬ÌñЦœ/„B•IûÉ'X)Úáí6/ ðz™5,Ý×E›Qu''ÆXS‡2®s03Ù刘”.õ'­&… 9Äu»;«œ &ÖfÀ2jf”’#¶ú¸h Èhœ¹ôWnrÔo–qý!úöܯ=øÚûðOÐÿøÇA”‘&·˜À_NK=ñÀ²â¦Ìo`«ÜöPh§,P'Ôº´ÝjZI¼“déL1Ž*5‘† ˜\ãèUðŸQ3»éòn-Nþ‰§dl¯*ö¤ñUýF¬Ý÷e‡&*çéÅö˜#ŸÕy*Ë·¤í0 ùc^rªrJµýЯцŸïÖ%Q› ){%L䬬ÜV…¥îC Å&É7Ãðã^’P{£n®қˆVWß!m¤ÛÁ--0)I5åÚtLBŸànäÜàÒ°²û†ê ¹Þœ„NSdó¹ÿ’z÷ÁØ~a¯6ñ‹¬‘LšòKE1,hcrÃ:hØ-­²4¥Þ÷­EoˆJ´ òi¯Øy¡ãë% 0ª°,pu/|¯á(íÔ aj¡_ùÇ!ŠPSû¼$ ×R1Çã¥"ëØˆÜ¬L*Z\~á>Nv6Ë»šZ°¥ ±â#J_pؼ?¿©tá¸q¬'m]ÔΙ[Å/ij<ÔÁø£AJh©úãºOõÙ1Ï LJ•m—‡myxí6#˜Üß=šQÿÙ-Øîy¿¾å5}ô‹Ö¶<—f°Y,¼¶I(Ó’i;Á–jŸßY*נÇUkbÂ.0xÀÀfùxiïîÆ;œYø?E`J"„ùNU»ÚŒ[Má®ìF㢻¿Ž~9³ôËæ©?ЙŋÛ>–t§ ÷öù_8äGì\/¢PµTÔìáã²Éøñ6ô¸«u TÒ8ÃÞ£ôâsÂAV䢌¼î±ßú.sœVÃæ³¥²’¬|ÛÐMWysêÌåí@.á‰TZ?ÆÆ±¶n7a0Nº`ôè;ÃÆ9ì*÷ ýÇ>”޶þw¦Ó'N×:ß.˜ ÀBï¹°›ÚSqØï­ØP2Ì'M,ǤV°Ô cBŒcp# ð¿wP“Qwòžþ±ÜÁ®ëÂsÝ}ènÁ3/ÒðOQñG4h”4§z0ÈkïÈ{^ŠéûàðLE…Ý÷ßjî ³9u6U’øì&J­0-F¹å«À½QÍXßAê[;¬¾nµˆoA@•s±¥†œÒÀwöñ¶žº/ƒßÖ‹”´eÄ…qE⨙1êÏŽ›Kñ”„÷"Å¢RÍX•½I­•«uÁÌÌL™‡ž4枘 /‘žÇÔÀº.<Ä\,V—vÐŒ™àÞÔòûŽÌ€!ÀŒk($bÉ +¼]Çó iu§V•Îq·—ÝêTQÂûmÕ¬sθZ Â;Ù…Ãez嘻'ÏÁC4r¼Ç±Žáæ tÔåèØòœò„*N|sÌ[͉TïþÇTšÚÖRÁã–µbÚz-óÜ`g‘!ø sƒŸ‚ ç°ô¡¨†¥¥3‹=hÁÊ-ËJjH8f¤óáOÙ‹ \D!ä A‰kìÍŠv‡¹›®²Ô»$W®1S8§y&ŽÆ8¨ùdD‘c¯·¤…”oAdTnÊBe¥]]Ñnµ­Ð€ÌvùÜß ‘ÕŸ`yn†¶”ÃFÔÊ&K¹Ì­,%(j‹ÐÌ^|1Ù§F»¨áß z›¾,'ëëD6*½± Öô~2\üo¾ØPe¶üÚið{lÝ ÿ¸3„¦:Æâ{¶å³XŒýÌ⦢awöNâ»H׉‚!SJ3GÝ!‰VĤî}5ôÄIHD!N™ŽÓGè§©ŽÝ±4‰ ÿð×H]"Ó´Þµ`ôrÀÇø¶m^8±I‡âÔJo°cË1¹w.19õ¨l´±¢ø™ö{ë\ý¶íÑRø]¯ö_,ÿ¹!™öUw#–±ƒì¯†Ù]'ʱ؞¢ðUòœOXcxó#Žf±¥!¹“Ÿ/Ø)¦ØˆØ¬’–éÑܶoû¬@õI²[~`|5EĶT „œé8'MT‚þ 1K.9àŠÓæ°¹®¥Ý¸¯Ê7ÅÝÁ¤SZcâì!|áá{µëÁ0¸eeY“•Ë"P®œ¨i"é;$b˜g>6Ê$Çü]5çãpDâ Ûç˜1cuì´ÁK¦º£º*¦©ŸãÝ4H1†Ä»[dŒmÄÖ›9žæm|íØùÔq'¼Oðå°Õ5Ù2R+†¡vü·FîÌò87êÝ'!Xªk mö$uqv1™Cpg>˜üšàÅ|?†wÍ]¦¶!—›½ Û&¨'.: ê{Ë&P§¼VLT$,P'Ñ!*ò`¤-ù øªa—pΓ(wo]-jº ˆº[J#(>âðˆÈV`¬¯n…“õ>T¬ô9N¾ ÀáB®.ñ@«T‘ÄtE ¦z[i½55C6˜/æç´ -˜TjÛ’SÀ eTE]s pÉ™…! ÊuÓ›ˆze«˜1`:¡(p®õˆâ±¥sº¥FÒ?\¤XB UêR®[iŠIbZød˜\@Â]¿;Ye ‡×Èq›ŠLQf#«ÔwŽÄÀtEË ®C¢Í _¤¾dD *ðz-§S Ó”S» ƤΚk±§qÉ0÷oVµÎGbã{>ü:™;èuŠƒ óÅ7f.á°œ/bö)ª6 ‰*V}I{ãÑd1b†µX1û6kœw.5ˆŽa0ö åsâ¹tì[é’aâ GU¾Ê©‘Wa¤p‰rf0&3¹pÕ_: î9‰kgKÓü :•;ÖÝPi`ïi™ßן-Nõ³ÁÖÅ]§èÑ8ùÀ‹R83y;úðXy Ú%^ì€Â Çrê‘ ’M†ã?ñ¸:à•3Lue¹îÒ|$6×¾’RUF¼HùU²ºYËtt—!uLE Ã‡Ž©QD–R£ˆ!þ‘— î!ã¦ÖŒ†YÂׂŒ5‘±ÐÉÐßjÃ"±NÉ!Ã^$RùË…‡·XÌFÝðràÚEo} ›ÿyÇ#%ñö¡Q«Á)¼yR<Þ}Å R;C·iV^ÉÀú¨/€!W­‘/Ö*ßÈ=?åÂqk$KŒ5?¡LàÌ[WšhM7Â\Œhà]‡~‹ƒö¡e>ΧtÓü6º[9‹]ºîžÇ.“ù‡œBêk ¥)÷ùŠÑÇÿ?u=º¤¹Z XåÎõʈHP5Éý|te¢©–‚­Â7ô˜8ڼ݋yüù.š/:2 ìÉû/y%ñuŽèv_æEÁ&A¬é ý}Aœ€-^ǹÎVûIx ì£mq.0aäÉ®­H’‹®e¶nû~ K5à1ÚXmwÀï¡ûm¸,Œ •¹Û{`÷×+Ñ50:Mæò CÀ ×ËàmàvýŒX+Û~³þ;:éʶòGÿ+Á €ÿ}ßçÄÖŒÚã;ö±sÝÓvFˤ xº] £Ÿ$…®÷H‰/{uzü·¿½E[þ5áÖ¼û[ïÖ¨ÚÐϾ—l¾ Yd!÷>Š.é°¤¡n *¾¾Sdí£¿ê¹cQ,›¿² M'W#n“G¢ÔÞ²ù-¦l¦.Ò_hcqB4j²:Ãå`õ¦™ÍuV î¡Ý­="@б‰ Ö¤òºLø.̸µ@/…ã”UK aÚ˜¹AÊÄPŒ2F1­u2K¿2ÃÄ¿áé!Xr‡ñ)1­Esf ¢¹«h{W¢Š–lnâ<ÉžÈ{À–TUÓm•YDÝT¨kZF\«ÿù:Žu›ÿâØ¸;‰¶£²#…Ôî.X¦w1²« VºJV‘,B¡ÚöìÚ¹J€Êts³ŸPUÃnft6|Ȩæ¾ ãÆ(Qv‘àIŒ0Á0øŸs z07sœŽöŸ…âJß9ÏtÛÅú{ ¨ /ÖÃ8¤BÃm |ûß©¯22—lS º¨æ>wöŽï^“Aeð´wD¨{¨ÖÌÞ³Ù“KZ6°8g¯}0ìäÝëÞ¦M‘3j`B§mçÉM.Nä|ú“ëòBs)ÊjñKx<¦;w[U`oجÎ|‡7½m0ðª/ºÚQÑm>úz|ÿ ÆÑ`nìå¥"Q÷®± `ĸèo<p½lÕùVëš:‘ Cƒ’N»ýГŸ±§NÆžùyô‚“Ÿ.ÅÿJ–Ô$–X<•ZQ¹8äD÷AÑKŽR–ŽçmŒ\×+ŒÒ!¸ ÓŸ,Ú0‚àés‰áœøÅtç¶qbÏl«D’{YD×fn¶¥]ÎB=#ìÌgÄy¶òWÈ<Ç‘ð!¨gŘèš$1Éñ¹•¦ó(†Ž ®!Ô2F*¬n–>!¡s˜¤kß•  ,¢š™äLç1šaÕy3×;ˆ¸<„ß ñmÀç¡eƒÜø^úÔ_ÅùIÁð/4<5’”Âò¼èÅÝVMaFû½¸½$'8#éc"£¤;ù|cXsû_­`Faê*×–¶d ŒÄAô­˜e¯¦Kœpß~çp¦ï|ü|òJ;"æM€û¾ñE´áObšÂ#è©•à0¾‹kêÿ†–«X gB40j6Ä—Ã{ µD@È€á^l[á|roiZ9­-«jnÑ*—È35jóoJõNCâ#`lt…Q-C\î£àz9ãϺI)}><¨BÏíOƈ‹K3apøŠT*ááµßFÈÒì&KQ6±5ï gò²šÆÃú ëêuîÌgãøÌ¸i¦q4kÐô°ë™kC£€e6S÷cVˆàÌÑpŒózJ,Ò˜“V2ýe1š`Rá„tûþééè»_F““yGCqb©‚u§] |!%°R{ÁVÅ÷ߣåì~#äú°¶”|_Öúkì#ûz¨!Ô‘xÇ©n6 í£­ ·([ªý N ½.ÜÌ×¹N*êzá?ƒÿÞ?=€ª65ù4Žà†Éw¿bϦäÍÿý?ØÏ´Þ4‚ƒAc¾Ã SkóëëQ°€2qËùØ§Î±Š¼ÑcMßs>Éîó1p(ÉÛB¤ç+‡ÜB2{>Mø†¾rjÎ÷gšÉ»XZR ø¡ü´9œnF=„×>ÃÈ5–²ÔƃJM¢Ãäá×¥F2ik‡îÅ„ìúÇG@@Pt/j ޵Csjó NºÈì0ùöÉã/‡ÉÛɉ?X"?Ø.@ùl'£w‚lïV—~yÇÞFy Ø>BÀé!µ/›ñnÑY2}‹®þáõÛä­òR{Ò*‹¼òÌÍÀ$²$‹ù'1‘I$/PÚÎe”š·x¬Êý•ž5Lj_û³“|@&'"Éüs;ÖC…——è…0Ä7ôXãâH.‚üéìòåùÛËääõ/ÉO'oÞœ¼¾üå/‰b;(õIwÁ ‰ƒº­1ʼnj~|þæô%üþ仳Wg—¿à´_œ]¾~>™$/Î߀Ë{qòæòìôí«“7ÉÅÛ7ç“ç¡·¸ùð®ú^â"‘å†-ñ=))m^ÒÛ|ĉ™¨,¨v‘¯X)ðZÔÊnXP½(Ö]6{¶C“ý?° õX ‘pagekite-0.5.8a/debian/pagekite/usr/share/doc/pagekite/copyright0000644000175000017500000000221612610153616024241 0ustar brebre00000000000000Format: http://dep.debian.net/deps/dep5 Upstream-Name: pagekite-0.5.8a Source: http://pagekite.net/pk/src/pagekite-0.5.8a.tar.gz Files: * Copyright: 2013 Bjarni Runar Einarsson 2013 The Beanstalks Project ehf. License: AGPL-3+ See the file COPYING, included with this distribution. Files: debian/* Copyright: 2013 PageKite Packaging Team License: GPL-2+ This package is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. . This package is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. . You should have received a copy of the GNU General Public License along with this program. If not, see . On Debian systems, the complete text of the GNU General Public License version 2 can be found in "/usr/share/common-licenses/GPL-2". pagekite-0.5.8a/debian/pagekite/usr/share/doc/pagekite/changelog.Debian.gz0000644000175000017500000000032312610153616025755 0ustar brebre00000000000000‹¥ŽÍnÂ0„ï~Š9¶¥¶Öi ü¨\JpàLºŠ,Œ%¶ª¾=ŽqãÂ^fµ;³ûµ¶æ£‹,I5±x¹¨¤öºxE }´Ï3¤®æPý/|ó'ð†UŠÍÉFW¡µÕ1'pHÎÿ"4‘U¶H‰]žþä;¹.ÔØ³=a~Mô_·O*p\ëνC±­" ÒZ—Æ”š0"M$D{GþT4g‘ôô;¸è¬GÇžmÏÏ Sl’Ð4Š¢ü˜–Fg´\â ð˜’ÂMpagekite-0.5.8a/debian/pagekite/usr/share/doc/pagekite/CREDITS.txt0000644000175000017500000000034712603542201024141 0ustar brebre00000000000000## CREDITS ## FamFamFam Silk Icons by Mark James Joar Wandborg Luc-Pierre Terral ## AUTHORS ## Bjarni Rúnar Einarsson Már Örlygsson pagekite-0.5.8a/debian/pagekite/usr/share/doc/pagekite/HISTORY.txt.gz0000644000175000017500000001065412610153276024636 0ustar brebre00000000000000‹•ZksÛÆ’ýî_1ÉVm¤˜€HʲcÕz«¬‡Õ•m•)'ÞO®!0$'Âëb¤˜_¿§{h9÷^%.Q0ÓÏÓ§{ð»j®+±ÑÆÖí^$ø´Þøgͳ7óóìÙvšž¥¿HA?óéì,MÓÙËg ÿ<׉X4Jå¢k„±²µø½Ü‹FWkü/ZUȽºleQ¨Â=óÖZU6VØZ¬ô#®¯t¥­E]7b·Q•è =¯šºð¼×&KEÖJ³:Bnk«|‚g³¢ËéfìRÔk‘Õ¹Ð6׿A¬ºðü;ìSv…ÕM¡Ä}WUªx/+¹V­_Ô`cmÄ®îŠ\4­ÚªÊBƒ¬Æ½™…Ÿy{¼Zöö˜¾Ngg#{¼-Šz' µ–Ù^,·ÂtMS·Vì´Ýˆ$±…ùê®úûsèïeg7vÓ*™›7È2¶«”0ú/h³tòèL’$ÂÝ(šºöêÝ”M[oÝW—ÒbÅV­[eØñVK¢EñQÔEµ·., ¾®£jr¤Ú4¸:øZe]«íþ\\µð´ÛÎ…¬rþt5]µu }¨l¢ªü‡Þ jKmÜÎËŽÝv¥–Z²÷ÚÚBxa²V76õR±PGUMþ(”4*? öÛýýÝB\ÈìvkU© q»ØÎÄMeU[ÉB\·mÝ Y¨Ö",W¨fÚˆ¼ÞUÉ2ÕØ¯2+¾®t¡Þœ¤i*Vx¼ÔV¯%[”¤å²3ŠMquU/Ò(zìy c¦³c±alå­>îy‰{lâmZN²¸ÐY: }»ò§Uâ¡ýÊ)ÏÎvYMÚ²{*ewuû YÄ ×¸¹Û¾ì308G¿XªÜêºõV¢yדJôi‚9™Å²_g#MŸ7w£‘Ãu él{é•[ºi±+¢N-ëug„]`üãá„K¸Ÿ²©M† ܱmwfwâÝu¯P4­ v=Mg¿Œäb!W+ ÑI-ò6Yµ2{ÑÖËÚšÔ>2”-;]Øž¤h¼JüÂ0xõa¨Ì%cÝZg£äÍŸ²º,Ö¤ü¦d°”ª±šô‡AT‹Mm¬ù×6z‰®BàæÞl#¯”òϺíSX”ª¤êü{ðh@€O7"# „÷uWå‰Eò&V—Øj?µzˆ…`š(ó?;€ëFe´*!z̨³˜ót:{öD ´õBO‡”_v«‚B€Ã´£ î[” Ê7É]Ö¨A5ëÀõqt¨Q(F°]+Wµ­³ºˆØ'm|Ðf(Ù E ô¹¨€!€ì»½Ýà+*Ï‘§ü¡®†Ã~w+î6X+B-\DûéÊ6+‡ÉÁX/œ±æél–Î_Œc%I<ð´ë®¤’·I _.òZd‰ÅIQ wqP´²™ÀɆDuc³ÑO ¶R%aéøÚ k‘B–ëFÐP60ÔR\DnÒ±ˆó•ÑëŠípÔxTUrÉzÐÅ„®âVÞdœšÖÈéIÒ"È·Ê+ƒ4ÁŠ­êJ=‚@IXè¸jbDÎ'‚Ÿö< ʺ]×ÖÙ2s†¿Œš V¿È,xjú*ÏŸë@ÃbñÈJv2¹R )/Í¡Y6ª(¢MT¤4áò¯Ÿo` ­F‘LQŽéPŽéHŽ»V!‘XZ†¹ÙÑ1UQm©‚Ê-¢”ì5b©,¡Âg qµ\WÀ+™‰§xÄz«l×VN¿ËŸbvzâWºtÕ>åØtîi'?!{ƒzG~IQØ<„äHØŠ‰ÆxHÅUÀe 3CÊ) :¶²@q3I¸Ä‚|#¥†ÙŒ(œ\rº a€’Ø?~<ퟮß^½¿NK"Ή­ªîŽOÛ’G8'Ob~õßP0QÈ·[©¯uu2øcµ²P0Z«ÓÈŒÝN„Vzí¢‡ÌXÊE¶+A¤’vèzŽ­vÒf›7ÿs{ýûõíÿ²Òü cç;À!ê5jt auUì}¾RàÓTËy9p¸Zæ$Á„’ Û¨[0ÂJfj¬ë\½̪*JYu²ðòWìûïúç]v΀åm‚˜Õ å Æ Âž?ªaþÚ »ðs– fLD‰ÒÜ$$ñqJÖDÃàÄõdçÚÃ<óKØÉ’ѧÉ3æ„’%^»£ZÂJ=oåΧ€XrÍŠ¸(À&O2‹Û1r¥\!Þ²fh ÏŸn!4ìŠÒ7Ïcõp¸ÿtT1PŒx‘¾Œ1;l~¢u(%˜~ê¿ g©íP‚MGž '(N¨ÂÒžÔÆÕm:ÿÄ%°­rM&õ7I ÷¹æ®u]Ñ·¥e™‡$ûìòã‡×—÷.Žu‘' ˆF€˜75ˆƒ7ÕU%Ö-Ò‡ɽÉ/iĈ«Ùú'Êf'ÌÄxîlþ*}¤ÿ† »*Þ¼¿Š%6XÔ“šY:ýå[ô‰¡&\KúåµÇâ›O—!ÞÓq÷Ê…G?§¡R”Õãèуý§ãýß3 䦺U¨e²‚CÉÚaÀý­Xk*‰Ñ$ Õlø0À¢ v´ÿOòväL´ø¬«˜EÆùÈlÎÝí4¢°¶9?9ÂúÉN?è“wEmÌÉÝþ¢U ròï?±Ýì/‰Ñœõ á☼Su¿¡‡_µ¨3YƒÏÉ´'¸þÏ­ô¸àF補èâ®>'ƒ`´J¼àþ9Jj{B>o¤1hÍò^3æÏëÆr× Þ©ës¾´„“ãú1FeÏÊâŽx4ºÆÏ¢"ƒVàêD¤¹ZòSiÛ”peö Ýt§+Ô“;eU§]Ÿ’w£ž'W[`Ã! bb žÆ¥˜ÀUf\¡ oÝ"øæJlÊè ý¼ã°ÏÇ…øB d€vÞ¹«i\‚0{ *nˆØ`q±Bh1˜ŽÐ£¤$@zï@ªÆ;.b8X ¢…ðÕ³ ž¸µú=JÔ*ƒT-'G@—ºé 7wØ×]ëƒAöÃø9ާòPve¼ÌÄ[â+@˜4X*iM%—v“ÕÞÝb:ú“”u]f)¦é)ˆbë¾»cŽ“'êàNPš&€ÁCØæVÂöëÍ¿“Æs´†;&ôhv׋³Ä–J˜ñÍ–Ž+dÿ$ƒ€ìÑqHt¸~ Ȧz¤9g«Õ׃ƒý=š8-øˆe Ò|Ô¤;ꂇéb9‚ðºîI€ )(‹_Z3ð4dÐìô‰ÂÔ¥zºo†$¡ È'MÐØŽÑ€Çd”á+^ƃãäCL>>©­,©W= Ek(œd5Œ5Ã[¸ Öû\éGgŠŸ¤³§A¼ƒÃú_4ˆËy¦Éÿ0‰ìYEKÌ–˜ŽÊ` ÛÄñF«¶šƒ„{t!&’Ž"ûÑëò¾Ë6Bró’lËN75î&üvVĶ¹Ò‘šÐp"Ñß(ïÞûžT¹ÎÝ?3á¶*I*„V§Ç…%'ˆ†æÉef.AçÓtö*èü¢ïܼҗí¾ñ¨®9ñSä4 ìÊ%˜æ×e«WšyUyE幇…+“Ÿ\“2ÛÐX¿¢&ê¨Ð¥†K©tJ`žµ”ll'nÈBÄ„Ëûá娕ËÑìƒF4.ÜP@ Ú•íE‚q âbÞ™ÑàÄs`?f±4wvT6:ƒPƒ›]O¿ÑÈ”¿=2n†žh*ï”ýì̧qD“2ÊM\²m]€ÝPZq¬ ° /sZ˜|­B6¡B­ˆóg,À`J7„¬Q6ƒ*³´12ƒÚ×ÇÅËø<ÇÅP‘äá)WÀÉñXʘ‚ J½?yJÒäÞ쟔ªzôó8}8¾ƒg½4ÓÓ±40 pE–âúã»È”'dFR´¤Ê”ÕmE7pHl€(—Ÿü5èWmë–Î@â¿fó(É‹ Éü»¼wST ÅþpT@B ZÒÙ“±t Cç3û\Ótá»hF=3[¤§ãÄ oÅÑU(ÄÕñèyʽÚ㤟HRH…AÀ â0CÇ]£#¤ôŠN!¿BÉç^n®èlÎ*_öØ Q¦6o"îC“ðùhÔOq¿LÙ ~¢wùi0ô,Ÿ=u²WjÕðàAÑÔ¨P% ÂÌ&Äq\~>X~vQn&`Ä-x™0«Þ–t¤À¸5ÑY€›+Ô ?.mî¤0²=^§ä$eö¥òTülôzós”m6mz([ÉN‰u<ºÐ×™pÜË©;ÉðcøU][Ü>É%ö¨L£2p(G‚Ÿš?g§h ©3Û‹|(Da¡ó ³“¾Þ¡í¸sÚë4;pçÅç_ßÝ|9G¸Ùüãg¿7\—|-ú%˵_‘nûÅÍö#»àþÚ¹éLÚ—B®©Õ öKhÖc2(7¿þöùnâróÛ!s<ôÏߢ‡âO !î¡— ~Š2â†n2nÎEÓY,I<Û˜Íáà¡~à}/ߊËþ¤+âX›%\ôŠîV ÏþA:šƒ¢ƒß]ó9 E\0“_3ž!º~p¥øOªiÊûó'gÔ‹a*ûc‰V=û1Œ+ˆ(o8‡È}JUÍÃz·§›Û–áÍxкW3J&A€Ïw|P¼rA1NS[Ïß»ûo+%í±SK7³íçâèsƒæ+Wçâµ\¸«nLzü½¥–À w–å¦O­w7½¤Â îûyú7:¬À¾ ÏÖ”•U²øpsH?Óýd¬âˆä&$<ÏIÈ#ãâ07Ãfn™ZçARéë‹ÅÇË\ß{œÚÐù:qwzýƒ¶¤Ç­ä9‡rݵðÔ»Œp‡K뵊½~ukänLÀêxÝ õåýí§»Ëw‡~T- w¨èNú¼’™| ÝÌKÇì©q ‡Â=i# ¾òÜ^ý¨’pÔpE6!ÈÎAv0‘~:8¿-««·¿³%á7Uà86vH¢R!³óƒUˆŒƒ?âÿ$µ°[tR„¶ÆÍnâÑr5R;r{,\9çúÞšŠ+ÏãÊÓžÞ†m~DHï¿0óøïa-EÑ…3üdWýNî`³ívhª~o«†QÀްhãz\×z×Ö¬î¡Gà{&ê™,a…­­Ö¸$ДߜOÃp†•«žðå¡[á’%VÖØG×OÑ‹æ®~L^Åg’{ÛÃë€J °­:\¤Z}6§\0–ûÜ´ÏaݲÅ5=® øå[yãž\Üáù%ÜäktûônÕ3‘ó†YêH·w;ÿ‹={un ßG³ k›ì@ çÙçp¸pƒ0’¦xÓðóÞ(MÌïÞßÛÙõ{ysýfñ°¸¹¾Ç‡¿¿´®Þ‘ÞŸ<$Š`ÂQ€ýG=ü7ù“× æö$¶«Àp¸ ›^ÖX uñ,:°ØíˆGÖ¦QkÙhÁ»m…¸VÈyÛÂà;°ƒIþ§Ð£Ä{’¨'.• )ù’H‚…ÞÚy›É#l+—%p=‘ƒ·ÐŒxj"/8?¡›™à¥‚­ºjBrx‰êª¬@ €´&À‰(šêŠˆö‡ÖNXsÂ" #J]Šuúm»²Ø‘?€?ì@Åèuà;Õ"È}¿!ABRŠ5ŒZQ‡OÀuV4"óQx4Æ}šÞc)“¨+!L]à‰˜LT`]¡ÀWxþ¯É²`6¡Ó§ÄT˜¬@Ýwð þn"¨p• :° {Êe'ËÓêFlùs@2Hã]ñzüÏ%‘ Ùgì©€6cý!z̤Ø{n‡ºdM¤ôÁªÒ8ÔЩàǺRÃ.g·AfŒR;01>ï˜ajÝ—•ƒ‹v_ÜjèÅDÖ7(÷ÀÜB"[‹55XèOÛÐxg·rN$°^ê¤g'&'gtØ6ªëT¶€i­Æ@°’D'±’`c5,š`倇DF<]:bO ^P©zp3ê:ÜàèÉÉùy)h—dƒkp{YÚ ñãºêi´]°ýÙ@K͉+F¡z Ù™°f×P§¢½ùÆAYXðö¿v¢ÜÙæŸœáÌ¨Ò û<9Þ€~±.@á¾wu±¡1Ûᱺ ¯êŠÌêë|Ujk¸/=˜ ¹¼‚wlP²Îa¡mÕÀO@fO©r³vE<.ôÂÎÀåE²w$·†ö>éá’Ø ,½Ø"tpPÅz³{RP*y왬ÐFzy¡Qì¡R6°ž„%c•Ö u£`VµÂnÀ·B­TVæ‹ Q•(Àkt“{…ëɉ(`[O²“‰ö]AšnzGb9êšÂÇSöÅãp1貈¡•¶Îõž÷_uð×N­¡Wöž­¾K0œ‚êŸ$¦à„½öL±u€î8È8øó6“óý`¶L™•˜¡êQ)Ý,ÿË‘Çå#o5móRì8]´Èï}_ è*íB‘_OÉüȹ¢¿+Y­ª¢6^W(Ñ´`3®@ÖlAç¡Å-x»lKà§öÐÙ y5ì xÈö«->ñ󶈆Üöü+4sxž>àr™Šm Ï%n›$«ˆ]"ÞÑýÞã=_UË®@¡6aí(R9š£A}ˆn5A·ÒSHJ`'¶µÊ?+Îyòv©Hhàb@.Èý€œ[}.YÈ¿/þ p âªmB¼MO‘JÑ$€ èq“*<ádå®§ …VÛkÑ vCéÁHsLèÊsºì oÆìJHåƒÔ/wC·Ã•û¶e#\þ€®pŒÿ`,UÃyJ»sIŒNÀ*Ð"“=õdäJ_ÎÙ’¡…S½EýR [Lí±{4Aõ'†Dp×캪ɪòí 5zÉì*·É”X© ƒ“nÌ\ô.A Ú8}±oŠ-‡SL]5ŸQnË€µ ‚7p2 ±’©QuºÄ _mÑ )‹¾ÐàŠø²äõ2)¬Á©ï©v®a$›†$àØõz•AŽá•¨<£¡`ök¶óƒ¶S6°Åз²½±Ã½³í o÷uXrV˽õÅ69ÙöuáA2݇„ÝÈø…¶”r(PD”úg5â0Úæ h|«ÁTD7e6àO-;-jË1]õH†& aàã[×kPF÷w_Ðë©Ðn-ÀjÀØ…´‡¦®¶®‘‡šU¶z}✂Óö;ߊ†ÏÐSŠ>$9¬ò3`pH r¨XVâPXCJR-¨òà?|_õà'°-ŸvÓ>ƒsüÈñóÎÈ` ¸¨8›"ÍÈOEÍúÙG”.÷¹OHü \M4“Ñã™bÄ`§6ËÇT¸¶˜Åaã:ø³i´ T_öQ!w¡Y@‚ñƒT’o eƒ1ÚG›K Çó-’ Ëa¸ØOèܲ —Û²àQÔƒ‡‹¨Ù׸H¢k@•’8(ó@2V5K^|.PQ,Kà(¡TÍö`qO¸0Ê~ìD SGÂ.(·ºŠí3ÑŒa#N!‰®psD¤w6Cˆ«g@Ž.ÍÈQívð¨îðfRLKÐ-ݦ¨×SáoúÇ *Œór(A™#ÓÙè耈Mµ¤Ð XF|Ž‘qÎЊá®ŒÊñЮ(¬Ï÷µ©v¬‚àM¢ÕË€7 vÀúLò«ª[ [ôÐÂÏRèH#h±KÐÔe4JNŽÁNkïÉ\”(kž(ÿc0¤N^}o´(5(Ç ž/º=øãÊL©á 8÷ÄNù3ì[DÏ ´ÕËKù ÍIXõJØñºÍ.U)Èõ4غePûh1­×Žc°á¦iëö• ø–e*"Ž’ °½]5hóšèü(Ü!Ï£3FØ«Wª‚>.noÁÑw®_½(Á­å¼ÑßÛ7€*‰xõ÷¿ÿ„w%–bö’Ä^4(–R­Šg‚‰ Wû—c+i.'išµ~DEö 'êŠs2^9ê‡Ê~Û'AÆ£DIèTÑ2•Äu€*8H„9Ë',n)±\÷ZþG{Q½ò ÇÜ æŒTS3?ð³‚+Zæg5•Å+¤Ø‘œÆKYDü:DŠh¼@\ar‚q&rÐÕ9Ó žQmXR üºà '‘¸$ó‡ïë¡8gˆ±DxÎrYì |Ñzï¼f‹˜#-ð :›ã星g0Mùq¤êƒ´`Ú(• ž‰è¦*=êT}ˆc&%_äNåÊ‹®¬1¶6ÆË7È:‚§¢+ÇŽ 4ŒéÜKq©Þj4èž‹=Ç"“ g®M…„ÈÚ9YTê·¨$Ì;œå¹(‡RÃ\Ö–çv‘¨ËMá¿’jL‘¼b뙓´ÊÉÄ˯ˆ‰/eÊk¼“(D¦‰EسÒNïÂ:›– S„9>d‹“­/qx[!·¡˜~óŠ9É•n‡e‡M¯ ó< ŵG`µ7œ&"Ã)«Ê ’ïù Ø’¢úš!Õ°›[̬ > Ñù):Œèìbjú©­‡­¨ø¾Å‚(ü[–ŽTS I17fR<>"AcÞ¶RH#Šèð½ÏŠªTå äFC¨lš‘’åò 3œÚƒõ_H-¥Y: ˆ‰~ż¾8½ìÈ`ê©!—íØõQ–þ§'Š1ÍU1p… ÏäPj=ÄÜg´t!¢ŸRz ÆŠ¨Ó·p9'ti(90…‘‰ЃdFäÿõ¤"Lz[`L —X€G2ïhDl´Ùؤazj\T¬ |•zöÓÉ0·´ Ùj”ÐÛì=ÙÀl°Ì:‹ñéä‰#4z>•:¤¢©Ba-q<ÔW}ak¥°åÐqüLWçYƒäj·\=@4K1Z­þ#ùÃEªQµÿ¿ž¹`¡Ö¡ÿÞ°8µ$õÙÚ= ¦ò ÖHî]Ñqè6y„5gRcrÇÚŠˆ½Ì$F&–8¨Žæ¦w0‡!N¦jqQÝZ·—`J2™T<Ë—Œé¯ÅmYç—(@ ;êdðqzœø „ð?OSÍ’å.Z|Ûr5€D€õ|ÛHÁ 'ÀuOô¥Òœ†Ø31úÌb¢*,Žõâ|úÑä>¨,Ðò$ZpBωtͽËôꤢ#¹°CzDÓ2Ìc¦6œ–““ê5¨Äâvµ*½)äJ"߉þe‡]¤Š½®à$: ¶Ø}Á HNé ZõY+×§ ObPx—¹ϱ<ÌjO%1§_Ÿ o ´ÜL›‡Æ^h^¨ªBUFÌ[{¤dÎ6ûÌ›ôÂ5î$× Ü9×½ìÛ—øo.ÿ %ŠaZ!¯Žp"ÐQQ ãîH&<Ï âB¡Y,°Ã*t–¶kRrM’­•Ùk$|#¾v"&ÊÐvi £$ø˜ˆ~&)Ò°G% º´Ø‚Æš þ~w©É+5 fŽCÊ›”sͫŶŠEcø„VAbÙ : ðZ¹U#|ÇE“Áz¨¤ì/;ìÔ”í°ì×CÍ 1ëWÓÖOŒçuñÄ= dy$Pߎ*¨ŒîÔÕj%%VèöLí$CTVWmúýŽlÅ–«è€¼Béª.X0(죰„ær*Hç›[>1HA킱àfô¨7}P(ùŠÜ â“f#rÞq&§þiBÀ°Â(˜‘GÑ>‚\/+YƒÈ‚}AUG&Ú¨ÔË­iFF‘à îÐÐÒd ào`?)W¤ YHcÔä°™“Ƥñ‰ÎB•ð ®ÛayA’Šþ[˃RK*·p¬¶ôS¤•+110Åê‡MÛIźýìöŒ^|U\[.¡X ‡(ˆÀõB‡ #þHtCëñ2Q™#ýHÔçO[t.£BÆXªèÆjF’}Õ ( ††ä¨¾Y³ -£R[[.]„ Ã4 ‹ñ¹¸4‡R›KGn~žBÊYb™ v·Ñý-ÖY­9•i(V…¾x|¸§õÒªœµô1²˜b7Ö%Öþj5 §æMHb²:,t«„¥bdFGqÉH›&»M,k‘ÊêDÇÓNê«v®°GXíRÃ4•ªœ oæzRŽðXÂÿ#ÇÎUa|î<¾­H¥PâÒ¥~¯‘&¾S<†ÝÂC¯y1¢"=Ó16<³bûnZN'v ¼ÝS—/'…ÐØÛ§¼5¢Ié,cË;Ã8î…r³4˜jˆîdAÖw7ïÏCÙR âG:úa…^aFK(—¥Ë©K¶#•£köˆzØa™k#$÷C<Ù&à¡KŽZ-…®¦BJæ=š«o-ŠŠ"8@…QŸ@ÌýÒQXäy㚃$ *W¯C!…¦3K”eŽ‹¡H[Å®ÂÄZÓ–§ª­|¸¡æ’=Ø}»ÂêÆµ(ãXUW¬ºÖût!)Ñø /°T8yÏj S@.Í{eîL¢—CLD{Î?ÌQ«·äGì¨føtÁ°ΉïJ»«çBšä!š‚`•<#À€(ÐfDCƒiJ¼c€RŠÄÓ"lý|ag1/óà4 :I~ØÖ¹´ôi\꥛Úv†4+õ8ÜQÁ½€ToØ8nú霪½˜r»0Ç®:É@I®IË&8'¦é2#ApÍ7¹aûì`6s3MZ¬ž²²Z £*•Në;èyª6ÒtÅQØ G¾µJ=­¡ y[ÿá_„É”ù$£½Õöf^›ÓUG° cÑ$áösP‚Ås¬€ôØÇOp² †ƒUÇJcð…ôøs+ ˆÏm+å2Ç·Ñ|vÑK‹Š9 ø`RŸÑf(-qv‚Jy5‹u»’/jŸ x8ð£ÐggÿãY8ªô¾8É ±˜ࣜ¡8•ܱÄEÈcÊsRyÝ¥u¾Å{Ö}Äݤn«Çk¤N-}ÓÙ!-öR©ÜM œ›n¤ëXÒ#†[¤.1œ£2Š0m¨KšŠíèêtíó¯ Š¼L‰þ“o¤ ‰¼I-¿Àü漨M¦R#"Ĥ´œY5ã"o_ý•„髟Æ0üŠ6¦&!îB»)¹-ÝSP_±…' ?sÊ-”½pj”ÑôÒ>¸±þ°ÓØâA¶•‘Œ«ædõœžCË£`g»ê#ô«sdÿPó”|¯LÃM>VMpn#Í ø±ã–ÿ~ÌÙ!ÿ.œe‰}·Ýg’§²DÀÐ3µíù$zÂ0 HfÅÄ£”çp9rÙÒÃÇ R•xñàq;Å–ÿƒÓûm7º‰à£+Àq#wŽe„5#“-Hl£ªºœ<×1ŠûAi„­Ð>ÁPLããì\Š%HøñqßuJi˜ðÞ6YY]< ÕE¤Ç($A¿Ïk2F׊ëfŸ>'š“K†Ž®šÁ@j©‚]ÂÅøÃQ„î–r£"¶qµ él O€}‡rm"¡yJAɼÁ³ 'bü@sD¡ò6ØU¹æ€%Õ.‰Ó£Ú½âR{öz ÇÊ’}²æ­`gQ ÎÂ9HØ¢gùu¡mr0¦+Àg‚6Y,‹c“ ¸µ%«‰NM¿DÿS,‘(ó“'Iù´Ÿ6*–gæ©ÌíÔè·Q »€ñž¤`çüiŒ‚Àe3·=Ì9åÐy ™èd„"¼PÀ–63M©dP@7 A…ÂÍgH0CÈëä: 9afqEe’™¡lüÙAAVÂ?혣¦jPIɺ$†cËmRð¤6W˜Q‚²¯W<:¿Ž ‰¦y 9®Ä@i¼» ¥j8‘Ö}P?Zè‰#›F7' Ý*CÆt ¤Ã2@ô‹Ñ@…³€˜JúFØÉ»Ù‡Û=µ•ø™T/—wYõr—N¼Jç©(Ç§Õ $JúdèËa+”ã ¶ øÛn“‰­Wúx—…‘ñŽõ<ŸÜï£&b/–pgÂ=λ&¡ê±h)FDñv€ÏM0B9¡,‘a ¨³Rµ#³®ª¦4ëªÉ‘˜7öÄ^`¤Ú‚ÇLcm•,ndqœ%Eì ´–œ%?Ñ∆@$6 Å•qz7 ÿô½-ɪY÷rÔHô=ø¶-a=kBúSH4 “3Iß “TÎ'g1ß>‹ŒÙªØNXWV¶T[œ‘¨ÜDÖÀÒ')FûiÙ>=~œƒ›Vƒ$㪿?¦ø5Rñàì‚ãÌ@qp/ÊüëåœÖ‹\‰ L†Á_™ÈÖùeZ¾6 ƒâi¹ùBõJx˜Ö¢ˆ\Ø»”Ò‹>½ê„¦IÛ›ýo°ŸÈ/mÄæ”Í’Ôª´lV½ŒÖ âìïýÛQëzbŒJúðw´Ú%´v2MŽ+—1ê5EÉå* (LêòË*’b÷ã¬YÜ,¸”;LK9,9¤h>™Ì’…(4Å0i§Á7à&KàÁáM$äuhØ XJ¸Ñ¼ li6z(-@FIÍ™—Ó \s>jÃt҉͞#ÜIx_„¹t비|¨ãÂŒn=d$4˜N¥ €ì QX|ÏㆤÜ-å™j>yDŒé7í3P4ÎuBÓÂz‰†SÉs¢×*ϪdÚUå”O ÜCÿ28SiÄk#Îr+<è…öô§"ÈþÊ›ó†eh ¦&´í‰û2+ÂÚrŸw8%6d:clÖØ FñÐyŠùŸ [üiF(äœxnÕäWéH.6ÁbÙ¥&ÌqÕ-¸Œú U¨±áq¸ÆÖuL9é¼/’o§Ø•§Þp³Vm5öðtRæÎI"B5(ô¬(„“+NÅWš`qnxëvE£<×~εp²}ÿ›]I]”†¡$'x ¤Kv†º4‰‹æ¦E«v ö¶¸À|®„KºÇ}'u鹞€ Í3þ;õ÷£‘°ÒòT½[¯±äêÀl%ÏÊkæMÚ CîsÔ’*ŸúÞOÒÙhq MºäXÝܵû¢–LY›”Ðq÷V„Å|s4@´2‰qÚr8–™1½š¬X˜K/¹ ’ïŸ*RégJú`K逡LŸ=ªoC]Ž»ŒY)k%+\53•4 º¨™3]ˆz¥CápŸXø$]%¯^]Ø[ÚÝÇ‘s GÛn¢…7#“y*Dt©'àˆ?RÒÉ`ºlZŒþv¢66V„Éeg?†¦©D2B"–P A)”BLQ§nSðˆb+N<üç˜XtpaˆÌH&% ç‘鮨40"-+SP’’esÞ–¡§liÌ…ãÆÁc½•&“µOpXÓR Û9AÍp 7Û,š5²@칓¸ Z²Luåž\,®Ãyá .Èb³ŽÙ¸lL**×:/ª=&Ͳ-™:Èä»aé ¾õŠóØ2ˆŽÎ¨<)X/±)/G=í…\äN×X90ª$’CÆktƒQ1Eò IÀ¤ RrFrMK´ ¥ˆ4¶;RœL?–ÁÆ’Rƒ»b¿¥:§6&d‡l*…Œ¦Ñøª Üsa¾ˆ•ÑŒ¾t¿ñÚl›Mu¤yÕ1ðÊ’DãtÜ¡×)µ%¥ä3ø4ôP*äx™H E´R¼sÆõs•“vˆ¿ÞzÐ|ÎÊ“µ8r‰gSÛ:°¨Ô¹{1=´MÛ«L¤ìì–D Âæ(FPòL!Ð(ÖLh,Íq‚Ÿ!ºÆ¨û³ï‹n ?&¥õE›0Z6 û…N &× !Ç'îtRªC2@V4›Q\Ëøy’uÓdsÅ¥0d[0‘—.¯œ a÷4Ó©•ÁV¯~¸ÀáV÷Xô(Ü÷ Ms{A_Ë*Û­Úo£y¢(eN™=SÿÆÙ 4†Ó‰ý=×*6,|(«U(Ë×-Ž¥Üö:߉ê÷ ±¡Óï^DóŒ•DÐä*Þ·2Þ@[Ë|µê¾h$âJ½ƒÉ\YH@G¤h§F*èèñ5Q/qù4ü#‚Œ/høÉ8T¤2QK¼˜×î:ʹ“­ =ŽPQ?ŽL Ð‚,ž„gá-0ÛDå›Q)¦t©È—µ8жlÉþk³ï]dX .8%Ö21WgjZÞ<–N3zõã…½sÛNx-öö"Î}ÿ«½£zú 8ÿyu à>–@š€¢ƒÎ.|Hf¸³¿>ƒdÂè‘ä{±ŽÝ´‡Ãë©î¶”Lè %ß-‡N}%ôGþ[’á95Î4¨2=zϾs´9öÛàÑŸ"Ó/ # ÐVa`šôÀaìGèU—¯OÉ›£À°@Ng Ÿîö—,mÚÊ>Šb’¢œø¶!%ß’*3+œ+D4bÿ9IÇg<ʧù,šG¹äÑÔjʾ"uÔG¿sêóLÜïÈN„‰ƒcâ0ât:Èè #Ò¼u¼FžjEÒ.’lD U’…Ïá6ZøÏZ?hPCpCö:õ&§WÊU,™þ‚’ î^ÿ#ûRÙ(‡ç=õ%J®¡–Qw¬&_\K>L$¾?óñ,ì˜?D´Á¿i¢& !]Î0¦ ó!q$×®êªÐ>.e²ñ+Sˆ]–«Vñ…[˜ðKdF¾ŸC[݂ݱ4]×$‰M¥3@â%k©c ‘èÍ€“,C©¡ Ý Rt¬îG¨YçòÂã(3#”MD `óMŒ¶³%¦X¹Ž á ~SIlñÄŸ$У…| ¨ €v:0ƒÀf_ÌêÇTd"d_‘ѵO>ŽÅÛ«‹ñM‚ŠÕ‹«M«I2]‹ÂŸLs Ì„ØÕ²L=¸N Ÿ/{þ´"ü¥Ô®leÆŸü6?àJÒ>2 Ce¾ì_x éåŇyj?"--• ª%@ÇÒ¯†™ƒ ‘( ¿^„&¬Ò¢ÀâïÝünn÷öúÆ~œÝÝÍ®>Ù·7wø{{wóûÝìýÔ>ÜÐÏó?̯ìíüîýâáaþƾþdf··W‹ËÙ뫹½š}Äïwýûr~û`?¾›_Û\þãâ~nïføÂâÚ~¼[<,®§/on?Ý-~÷`ÞÝ\½™ßÑçÒ¾ƒÝéE{;»{XÌïŽ?oæ)Lv2»°'öãâáÝ͇‡¼¹y ‹|²ÿ\\¿™Úù‚šÿûön~ÀÚ‹÷ñþ¸¸¾¼úð`™Ú×°Âõ̓½ZÀÉే›©ÁÝäY]õßÏï.ßÁ³×‹«à ¿ñövñp [îf ù凫ٹýpw{s?¿°ŒBX~·¸ÿ§…bÿõaìÂïg×—sÜ+9³kÂãÚO7PoÀ¹¯ÞdHADÍí›ùÛùåÃâùŸ„mî?¼Ÿ ¾ï`Q3»º²×óK€wv÷ÉÞÏïþX\îæ·³ÅbéòæîW¹¹f2úé‚[BÚíJkçYp\#Íÿ@úøp}…˜¸›ÿëœ©ÄæT‚ëÏ~¿›¢š0Þ^ Ë„1¥Wà‘0>‰ÝØ÷7ooñZ„p.o®ÿ˜º7)VÏ‘dg¯o1¯Á –ðÞÞÌÞÏ~Ÿß'”{ùöÔÞßÎ/øðw G €+FÕõ=œ¯~!‹ØÜ1®€ÄÉ÷h># ^+áÀÞø»س¸÷!QÚ«›{¤@ófö0³1üûõŸ¾›_¢ˆÇf——î€ßð | ¹ÿ¸¸æÛÀó‹/îÞe2¢Û·³ÅÕ‡»1ááÎ7€B\’0¹ ~âþ|jðòíâ-luùN®Íf¬üɾƒ«x=‡ÇfoþX;Ê>äBp§£L}?_°c‰f xÐ*•ê°2z¡/ ¬3BŽM aÔ ×{K€béĪ[¹Á-T<ßZªìE sÓª4Ý3{G¹äô°Í,+ÏÚ²„³aë–û‘±½ê }©ÃŒ¬.}[ãßÍÆâÕSU'°‰Üeî°–3gj±½%GDlºç<üA$nW1tãáÂGþº¤{>ñ9ÌøÏ;þºØŒPÄE…Úàð UÞ5X°€Oò˜òu)ùZè.ýt=§O‡IžNÎñHݶ4w+YÀÁ:œ§’Ÿó=OÒÂòÑ åuB1²dg«ÞäŸ"f«Èéçêù«&É'·“o‡,§O¤RqŠ¥ý…„¤£1« |ÁÐÊÔyS¾XãÑâðö6|P¶—ž*eKš=ø«A˜úÔ¡ùõÞý%1õd¶f>›V¢%ü†¢Id‰ë Br&«øEÒšý_üŠå®¥ÀG¹t†Óz†ñ4k4Q…¸þè¤÷uÒ`rþžšÚdéeW¹5æñŠ0"KÒ4¿Él,µ²Î.Ïí?pFâo°-Ñjéo¼/Å2v±x(»î_Â÷ƳK®z›}[º×ŽçµÿŒ±\øÌéî³ÓýT}›ƒøA,êá^¸³¼÷ùüÐݹ8އxÜð!µ 溴cL=Y`.¸U‘Œ¾ªZm¨HÔrû5ýr5¯¥‘ø(³¸pl€ŽOÙ_6Ú_÷Î…~Û?áªkZ=id†9²”ÊCµ}^íùÍõeäa2&/b–=Fà,Êqö›¾ßýòÝwÏÏÏÍpÑvßi%Òw¿\3¬*Å~°têηaJ©þ‚=}ŽCÐ]Ûà@3üŒM±Ã¢*8bR’̺\ñ#› *û+AQ¾è¹Øâí\Ì’ ™Ëš)ŒËáÀs¨~ÍàxܽKÉÖpKM°0 àׄ◼88®s£ ;Ñ·QxŽ;ù\Qúç6Aî?9²oe(Xç-ñÇö>‰¿Ë(U¸Gßö Î"és2\”=–®ï¥ê*öë'·~%"-?ªó»‡÷>ÐŽˆ$\9@]»Çº‰œÇoaèw]wN•}è_ÇòG÷(÷‰s°xœ ÎhcMb1Gîsv—_ކʊœF‘²…„¿¯6°}*pa¾Åÿ ?ìig؆pagekite-0.5.8a/debian/pagekite/usr/share/pyshared/0000775000175000017500000000000012610153761021571 5ustar brebre00000000000000pagekite-0.5.8a/debian/pagekite/usr/share/pyshared/pagekite/0000775000175000017500000000000012610153761023362 5ustar brebre00000000000000pagekite-0.5.8a/debian/pagekite/usr/share/pyshared/pagekite/__init__.py0000644000175000017500000000163512603542201025467 0ustar brebre00000000000000############################################################################## LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2015, the Beanstalks Project ehf. and Bjarni Runar Einarsson This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see: """ ############################################################################## pagekite-0.5.8a/debian/pagekite/usr/share/pyshared/pagekite/compat.py0000644000175000017500000000746312603542201025220 0ustar brebre00000000000000""" Compatibility hacks to work around differences between Python versions. """ ############################################################################## LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2015, the Beanstalks Project ehf. and Bjarni Runar Einarsson This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see: """ ############################################################################## import common from common import * # System logging on Unix try: import syslog except ImportError: class mockSyslog: def openlog(*args): raise ConfigError('No Syslog on this machine') def syslog(*args): raise ConfigError('No Syslog on this machine') LOG_DAEMON = 0 LOG_DEBUG = 0 LOG_ERROR = 0 LOG_PID = 0 syslog = mockSyslog() # Backwards compatibility for old Pythons. import socket rawsocket = socket.socket if not 'SHUT_RD' in dir(socket): socket.SHUT_RD = 0 socket.SHUT_WR = 1 socket.SHUT_RDWR = 2 try: import datetime ts_to_date = datetime.datetime.fromtimestamp def ts_to_iso(ts=None): return datetime.datetime.fromtimestamp(ts).isoformat() except ImportError: ts_to_date = str ts_to_iso = str try: sorted([1, 2, 3]) except: def sorted(l): tmp = l[:] tmp.sort() return tmp try: sum([1, 2, 3]) except: def sum(l): s = 0 for v in l: s += v return s try: from urlparse import parse_qs, urlparse except ImportError, e: from cgi import parse_qs from urlparse import urlparse try: import hashlib def sha1hex(data): hl = hashlib.sha1() hl.update(data) return hl.hexdigest().lower() except ImportError: import sha def sha1hex(data): return sha.new(data).hexdigest().lower() common.MAGIC_UUID = sha1hex(common.MAGIC_UUID) try: from traceback import format_exc except ImportError: import traceback import StringIO def format_exc(): sio = StringIO.StringIO() traceback.print_exc(file=sio) return sio.getvalue() # Old Pythons lack rsplit def rsplit(ch, data): parts = data.split(ch) if (len(parts) > 2): tail = parts.pop(-1) return (ch.join(parts), tail) else: return parts # SSL/TLS strategy: prefer pyOpenSSL, as it comes with built-in Context # objects. If that fails, look for Python 2.6+ native ssl support and # create a compatibility wrapper. If both fail, bomb with a ConfigError # when the user tries to enable anything SSL-related. # import sockschain socks = sockschain if socks.HAVE_PYOPENSSL: SSL = socks.SSL SEND_ALWAYS_BUFFERS = False SEND_MAX_BYTES = 16 * 1024 TUNNEL_SOCKET_BLOCKS = False elif socks.HAVE_SSL: SSL = socks.SSL SEND_ALWAYS_BUFFERS = True SEND_MAX_BYTES = 4 * 1024 TUNNEL_SOCKET_BLOCKS = True # Workaround for http://bugs.python.org/issue8240 else: SEND_ALWAYS_BUFFERS = False SEND_MAX_BYTES = 16 * 1024 TUNNEL_SOCKET_BLOCKS = False class SSL(object): TLSv1_METHOD = 0 SSLv23_METHOD = 0 class Error(Exception): pass class SysCallError(Exception): pass class WantReadError(Exception): pass class WantWriteError(Exception): pass class ZeroReturnError(Exception): pass class Context(object): def __init__(self, method): raise ConfigError('Neither pyOpenSSL nor python 2.6+ ' 'ssl modules found!') pagekite-0.5.8a/debian/pagekite/usr/share/pyshared/pagekite/yamond.py0000644000175000017500000001341712603542202025221 0ustar brebre00000000000000""" This is a class implementing a flexible metric-store and an HTTP thread for browsing the numbers. """ ############################################################################## LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2015, the Beanstalks Project ehf. and Bjarni Runar Einarsson This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see: """ ############################################################################## import getopt import os import random import re import select import socket import struct import sys import threading import time import traceback import urllib import BaseHTTPServer try: from urlparse import parse_qs, urlparse except Exception, e: from cgi import parse_qs from urlparse import urlparse class YamonRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): def do_yamon_vars(self): self.send_response(200) self.send_header('Content-Type', 'text/plain') self.send_header('Cache-Control', 'no-cache') self.end_headers() self.wfile.write(self.server.yamond.render_vars_text()) def do_heapy(self): from guppy import hpy self.send_response(200) self.send_header('Content-Type', 'text/plain') self.send_header('Cache-Control', 'no-cache') self.end_headers() self.wfile.write(hpy().heap()) def do_404(self): self.send_response(404) self.send_header('Content-Type', 'text/html') self.end_headers() self.wfile.write('

    404: What? Where? Cannot find it!

    ') def do_root(self): self.send_response(200) self.send_header('Content-Type', 'text/html') self.end_headers() self.wfile.write('

    Hello!

    ') def handle_path(self, path, query): if path == '/vars.txt': self.do_yamon_vars() elif path == '/heap.txt': self.do_heapy() elif path == '/': self.do_root() else: self.do_404() def do_GET(self): (scheme, netloc, path, params, query, frag) = urlparse(self.path) qs = parse_qs(query) return self.handle_path(path, query) class YamonHttpServer(BaseHTTPServer.HTTPServer): def __init__(self, yamond, handler): BaseHTTPServer.HTTPServer.__init__(self, yamond.sspec, handler) self.yamond = yamond class YamonD(threading.Thread): """Handle HTTP in a separate thread.""" def __init__(self, sspec, server=YamonHttpServer, handler=YamonRequestHandler): threading.Thread.__init__(self) self.lock = threading.Lock() self.server = server self.handler = handler self.sspec = sspec self.httpd = None self.running = False self.values = {} self.lists = {} self.views = {} def vmax(self, var, value): try: self.lock.acquire() if value > self.values[var]: self.values[var] = value finally: self.lock.release() def vscale(self, var, ratio, add=0): try: self.lock.acquire() if var not in self.values: self.values[var] = 0 self.values[var] *= ratio self.values[var] += add finally: self.lock.release() def vset(self, var, value): try: self.lock.acquire() self.values[var] = value finally: self.lock.release() def vadd(self, var, value, wrap=None): try: self.lock.acquire() if var not in self.values: self.values[var] = 0 self.values[var] += value if wrap is not None and self.values[var] >= wrap: self.values[var] -= wrap finally: self.lock.release() def vmin(self, var, value): try: self.lock.acquire() if value < self.values[var]: self.values[var] = value finally: self.lock.release() def vdel(self, var): try: self.lock.acquire() if var in self.values: del self.values[var] finally: self.lock.release() def lcreate(self, listn, elems): try: self.lock.acquire() self.lists[listn] = [elems, 0, ['' for x in xrange(0, elems)]] finally: self.lock.release() def ladd(self, listn, value): try: self.lock.acquire() lst = self.lists[listn] lst[2][lst[1]] = value lst[1] += 1 lst[1] %= lst[0] finally: self.lock.release() def render_vars_text(self, view=None): if view: if view == 'heapy': from guppy import hpy return hpy().heap() else: values, lists = self.views[view] else: values, lists = self.values, self.lists data = [] for var in values: data.append('%s: %s\n' % (var, values[var])) for lname in lists: (elems, offset, lst) = lists[lname] l = lst[offset:] l.extend(lst[:offset]) data.append('%s: %s\n' % (lname, ' '.join(['%s' % (x, ) for x in l]))) data.sort() return ''.join(data) def quit(self): if self.httpd: self.running = False urllib.urlopen('http://%s:%s/exiting/' % self.sspec, proxies={}).readlines() def run(self): self.httpd = self.server(self, self.handler) self.sspec = self.httpd.server_address self.running = True while self.running: self.httpd.handle_request() if __name__ == '__main__': yd = YamonD(('', 0)) yd.vset('bjarni', 100) yd.lcreate('foo', 2) yd.ladd('foo', 1) yd.ladd('foo', 2) yd.ladd('foo', 3) yd.run() pagekite-0.5.8a/debian/pagekite/usr/share/pyshared/pagekite/logparse.py0000644000175000017500000001315112603542201025540 0ustar brebre00000000000000""" A basic tool for processing and parsing the Pagekite logs. This class doesn't actually do anything much, it's meant for subclassing. """ ############################################################################## LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2015, the Beanstalks Project ehf. and Bjarni Runar Einarsson This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see: """ ############################################################################## import os import sys import time class PageKiteLogParser(object): def __init__(self): pass def ParseLine(self, line, data=None): try: if data is None: data = {} for word in line.split('; '): key, val = word.split('=', 1); data[key] = val return data except Exception: return {'raw': '%s' % line} def ProcessData(self, data): print '%s' % data def ProcessLine(self, line, data=None): self.ProcessData(self.ParseLine(line, data)) def Follow(self, fd, filename): # Record last position... pos = fd.tell() try: if os.stat(filename).st_size < pos: # Re-open log-file if it's been rotated/trucated new_fd = open(filename, 'r') fd.close() return new_fd except (OSError, IOError), e: # Failed to stat or open new file, just try again later. pass # Sleep a bit and then try to read some more time.sleep(1) fd.seek(pos) return fd def ReadLog(self, filename=None, after=None, follow=False): if filename is not None: fd = open(filename, 'r') else: fd = sys.stdin first = True while first or follow: for line in fd: if line.endswith('\n'): data = self.ParseLine(line.strip()) if after is None or ('ts' in data and int(data['ts'], 16) > after): self.ProcessData(data) else: fd.seek(fd.tell() - len(line)) break if follow: fd = self.Follow(fd, filename) first = False def ReadSyslog(self, filename, pname='pagekite.py', after=None, follow=False): fd = open(filename, 'r') tag = ' %s[' % pname first = True while first or follow: for line in fd: if line.endswith('\n'): try: parts = line.split(':', 3) if parts[2].find(tag) > -1: data = self.ParseLine(parts[3].strip()) if after is None or int(data['ts'], 16) > after: self.ProcessData(data) except ValueError, e: pass else: fd.seek(fd.tell() - len(line)) break if follow: fd = self.Follow(fd, filename) first = False class PageKiteLogTracker(PageKiteLogParser): def __init__(self): PageKiteLogParser.__init__(self) self.streams = {} def ProcessRestart(self, data): # Program just restarted, discard streams state. self.streams = {} def ProcessBandwidthRead(self, stream, data): stream['read'] += int(data['read']) def ProcessBandwidthWrote(self, stream, data): stream['wrote'] += int(data['wrote']) def ProcessError(self, stream, data): stream['err'] = data['err'] def ProcessEof(self, stream, data): del self.streams[stream['id']] def ProcessNewStream(self, stream, data): self.streams[stream['id']] = stream stream['read'] = 0 stream['wrote'] = 0 def ProcessData(self, data): if 'id' in data: # This is info about a specific stream... sid = data['id'] if 'proto' in data and 'domain' in data and sid not in self.streams: self.ProcessNewStream(data, data) if sid in self.streams: stream = self.streams[sid] if 'err' in data: self.ProcessError(stream, data) if 'read' in data: self.ProcessBandwidthRead(stream, data) if 'wrote' in data: self.ProcessBandwidthWrote(stream, data) if 'eof' in data: self.ProcessEof(stream, data) elif 'started' in data and 'version' in data: self.ProcessRestart(data) class DebugPKLT(PageKiteLogTracker): def ProcessRestart(self, data): PageKiteLogTracker.ProcessRestart(self, data) print 'RESTARTED %s' % data def ProcessNewStream(self, stream, data): PageKiteLogTracker.ProcessNewStream(self, stream, data) print '[%s] NEW %s' % (stream['id'], data) def ProcessBandwidthRead(self, stream, data): PageKiteLogTracker.ProcessBandwidthRead(self, stream, data) print '[%s] BWR %s' % (stream['id'], data) def ProcessBandwidthWrote(self, stream, data): PageKiteLogTracker.ProcessBandwidthWrote(self, stream, data) print '[%s] BWW %s' % (stream['id'], data) def ProcessError(self, stream, data): PageKiteLogTracker.ProcessError(self, stream, data) print '[%s] ERR %s' % (stream['id'], data) def ProcessEof(self, stream, data): PageKiteLogTracker.ProcessEof(self, stream, data) print '[%s] EOF %s' % (stream['id'], data) if __name__ == '__main__': sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0) if len(sys.argv) > 2: DebugPKLT().ReadSyslog(sys.argv[1], pname=sys.argv[2]) else: DebugPKLT().ReadLog(sys.argv[1]) pagekite-0.5.8a/debian/pagekite/usr/share/pyshared/pagekite/ui/0000775000175000017500000000000012610153761023777 5ustar brebre00000000000000pagekite-0.5.8a/debian/pagekite/usr/share/pyshared/pagekite/ui/remote.py0000644000175000017500000003556512603542202025652 0ustar brebre00000000000000""" This is a user interface class which communicates over a pipe or socket. """ ############################################################################## LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2015, the Beanstalks Project ehf. and Bjarni Runar Einarsson This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see: """ ############################################################################## import re import sys import time import threading from pagekite.compat import * from pagekite.common import * from pagekite.proto.conns import Tunnel from nullui import NullUi class RemoteUi(NullUi): """Stdio based user interface for interacting with other processes.""" DAEMON_FRIENDLY = True ALLOWS_INPUT = True WANTS_STDERR = True EMAIL_RE = re.compile(r'^[a-z0-9!#$%&\'\*\+\/=?^_`{|}~-]+' '(?:\.[a-z0-9!#$%&\'*+/=?^_`{|}~-]+)*@' '(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)*' '(?:[a-zA-Z]{2,4}|museum)$') def __init__(self, welcome=None, wfile=sys.stderr, rfile=sys.stdin): NullUi.__init__(self, welcome=welcome, wfile=wfile, rfile=rfile) self.CLEAR = '' self.NORM = self.WHITE = self.GREY = self.GREEN = self.YELLOW = '' self.BLUE = self.RED = self.MAGENTA = self.CYAN = '' def StartListingBackEnds(self): self.wfile.write('begin_be_list\n') def EndListingBackEnds(self): self.wfile.write('end_be_list\n') def NotifyBE(self, bid, be, has_ssl, dpaths, is_builtin=False, fingerprint=None, now=None): domain = be[BE_DOMAIN] port = be[BE_PORT] proto = be[BE_PROTO] prox = (proto == 'raw') and ' (HTTP proxied)' or '' if proto == 'raw' and port in ('22', 22): proto = 'ssh' url = '%s://%s%s' % (proto, domain, port and (':%s' % port) or '') message = (' be_status:' ' status=%x; bid=%s; domain=%s; port=%s; proto=%s;' ' bhost=%s; bport=%s%s%s%s' '\n') % (be[BE_STATUS], bid, domain, port, proto, be[BE_BHOST], be[BE_BPORT], has_ssl and '; ssl=1' or '', is_builtin and '; builtin=1' or '', fingerprint and ('; fingerprint=%s' % fingerprint) or '') self.wfile.write(message) for path in dpaths: message = (' be_path: domain=%s; port=%s; path=%s; policy=%s; src=%s\n' ) % (domain, port or 80, path, dpaths[path][0], dpaths[path][1]) self.wfile.write(message) def Notify(self, message, prefix=' ', popup=False, color=None, now=None, alignright=''): message = '%s' % message self.wfile.write('notify: %s\n' % message) def NotifyMOTD(self, frontend, message): self.wfile.write('motd: %s %s\n' % (frontend, message.replace('\n', ' '))) def Status(self, tag, message=None, color=None): self.status_tag = tag self.status_msg = '%s' % (message or self.status_msg) if message: self.wfile.write('status_msg: %s\n' % message) if tag: self.wfile.write('status_tag: %s\n' % tag) def Welcome(self, pre=None): self.wfile.write('welcome: %s\n' % (pre or '').replace('\n', ' ')) def StartWizard(self, title): self.wfile.write('start_wizard: %s\n' % title) def Retry(self): self.tries -= 1 if self.tries < 0: raise Exception('Too many tries') return self.tries def EndWizard(self, quietly=False): self.wfile.write('end_wizard: %s\n' % (quietly and 'quietly' or 'done')) def Spacer(self): pass def AskEmail(self, question, default=None, pre=[], wizard_hint=False, image=None, back=None, welcome=True): while self.Retry(): self.wfile.write('begin_ask_email\n') if pre: self.wfile.write(' preamble: %s\n' % '\n'.join(pre).replace('\n', ' ')) if default: self.wfile.write(' default: %s\n' % default) self.wfile.write(' question: %s\n' % (question or '').replace('\n', ' ')) self.wfile.write(' expect: email\n') self.wfile.write('end_ask_email\n') answer = self.rfile.readline().strip() if self.EMAIL_RE.match(answer): return answer if back is not None and answer == 'back': return back def AskLogin(self, question, default=None, email=None, pre=None, wizard_hint=False, image=None, back=None): while self.Retry(): self.wfile.write('begin_ask_login\n') if pre: self.wfile.write(' preamble: %s\n' % '\n'.join(pre).replace('\n', ' ')) if email: self.wfile.write(' default: %s\n' % email) self.wfile.write(' question: %s\n' % (question or '').replace('\n', ' ')) self.wfile.write(' expect: email\n') self.wfile.write(' expect: password\n') self.wfile.write('end_ask_login\n') answer_email = self.rfile.readline().strip() if back is not None and answer_email == 'back': return back answer_pass = self.rfile.readline().strip() if back is not None and answer_pass == 'back': return back if self.EMAIL_RE.match(answer_email) and answer_pass: return (answer_email, answer_pass) def AskYesNo(self, question, default=None, pre=[], yes='Yes', no='No', wizard_hint=False, image=None, back=None): while self.Retry(): self.wfile.write('begin_ask_yesno\n') if yes: self.wfile.write(' yes: %s\n' % yes) if no: self.wfile.write(' no: %s\n' % no) if pre: self.wfile.write(' preamble: %s\n' % '\n'.join(pre).replace('\n', ' ')) if default: self.wfile.write(' default: %s\n' % default) self.wfile.write(' question: %s\n' % (question or '').replace('\n', ' ')) self.wfile.write(' expect: yesno\n') self.wfile.write('end_ask_yesno\n') answer = self.rfile.readline().strip().lower() if back is not None and answer == 'back': return back if answer in ('y', 'n'): return (answer == 'y') if answer == str(default).lower(): return default def AskKiteName(self, domains, question, pre=[], default=None, wizard_hint=False, image=None, back=None): while self.Retry(): self.wfile.write('begin_ask_kitename\n') if pre: self.wfile.write(' preamble: %s\n' % '\n'.join(pre).replace('\n', ' ')) for domain in domains: self.wfile.write(' domain: %s\n' % domain) if default: self.wfile.write(' default: %s\n' % default) self.wfile.write(' question: %s\n' % (question or '').replace('\n', ' ')) self.wfile.write(' expect: kitename\n') self.wfile.write('end_ask_kitename\n') answer = self.rfile.readline().strip().lower() if back is not None and answer == 'back': return back if answer: for d in domains: if answer.endswith(d) or answer.endswith(d): return answer return answer+domains[0] def AskBackends(self, kitename, protos, ports, rawports, question, pre=[], default=None, wizard_hint=False, image=None, back=None): while self.Retry(): self.wfile.write('begin_ask_backends\n') if pre: self.wfile.write(' preamble: %s\n' % '\n'.join(pre).replace('\n', ' ')) count = 0 if self.server_info: protos = self.server_info[Tunnel.S_PROTOS] ports = self.server_info[Tunnel.S_PORTS] rawports = self.server_info[Tunnel.S_RAW_PORTS] self.wfile.write(' kitename: %s\n' % kitename) self.wfile.write(' protos: %s\n' % ', '.join(protos)) self.wfile.write(' ports: %s\n' % ', '.join(ports)) self.wfile.write(' rawports: %s\n' % ', '.join(rawports)) if default: self.wfile.write(' default: %s\n' % default) self.wfile.write(' question: %s\n' % (question or '').replace('\n', ' ')) self.wfile.write(' expect: backends\n') self.wfile.write('end_ask_backends\n') answer = self.rfile.readline().strip().lower() if back is not None and answer == 'back': return back return answer def AskMultipleChoice(self, choices, question, pre=[], default=None, wizard_hint=False, image=None, back=None): while self.Retry(): self.wfile.write('begin_ask_multiplechoice\n') if pre: self.wfile.write(' preamble: %s\n' % '\n'.join(pre).replace('\n', ' ')) count = 0 for choice in choices: count += 1 self.wfile.write(' choice_%d: %s\n' % (count, choice)) if default: self.wfile.write(' default: %s\n' % default) self.wfile.write(' question: %s\n' % (question or '').replace('\n', ' ')) self.wfile.write(' expect: choice_index\n') self.wfile.write('end_ask_multiplechoice\n') answer = self.rfile.readline().strip().lower() try: ch = int(answer) if ch > 0 and ch <= len(choices): return ch except: pass if back is not None and answer == 'back': return back def Tell(self, lines, error=False, back=None): dialog = error and 'error' or 'message' self.wfile.write('tell_%s: %s\n' % (dialog, ' '.join(lines))) def Working(self, message): self.wfile.write('working: %s\n' % message) class PageKiteThread(threading.Thread): def __init__(self, startup_args=None, debug=False): threading.Thread.__init__(self) self.pk = None self.pk_readlock = threading.Condition() self.gui_readlock = threading.Condition() self.debug = debug self.reset() def reset(self): self.pk_incoming = [] self.pk_eof = False self.gui_incoming = '' self.gui_eof = False # These routines are used by the PageKite UI, to communicate with us... def readline(self): try: self.pk_readlock.acquire() while (not self.pk_incoming) and (not self.pk_eof): self.pk_readlock.wait() if self.pk_incoming: line = self.pk_incoming.pop(0) else: line = '' if self.debug: print '>>PK>> %s' % line.strip() return line finally: self.pk_readlock.release() def write(self, data): if self.debug: print '>>GUI>> %s' % data.strip() try: self.gui_readlock.acquire() if data: self.gui_incoming += data else: self.gui_eof = True self.gui_readlock.notify() finally: self.gui_readlock.release() # And these are used by the GUI, to communicate with PageKite. def recv(self, bytecount): try: self.gui_readlock.acquire() while (len(self.gui_incoming) < bytecount) and (not self.gui_eof): self.gui_readlock.wait() data = self.gui_incoming[0:bytecount] self.gui_incoming = self.gui_incoming[bytecount:] return data finally: self.gui_readlock.release() def send(self, data): if not data.endswith('\n') and data != '': raise ValueError('Please always send whole lines') if self.debug: print '< """ ############################################################################# import re import sys import time from nullui import NullUi from pagekite.common import * HTML_BR_RE = re.compile(r'<(br|/p|/li|/tr|/h\d)>\s*') HTML_LI_RE = re.compile(r'
  • \s*') HTML_NBSP_RE = re.compile(r' ') HTML_TAGS_RE = re.compile(r'<[^>\s][^>]*>') def clean_html(text): return HTML_LI_RE.sub(' * ', HTML_NBSP_RE.sub('_', HTML_BR_RE.sub('\n', text))) def Q(text): return HTML_TAGS_RE.sub('', clean_html(text)) class BasicUi(NullUi): """Stdio based user interface.""" DAEMON_FRIENDLY = False WANTS_STDERR = True EMAIL_RE = re.compile(r'^[a-z0-9!#$%&\'\*\+\/=?^_`{|}~-]+' '(?:\.[a-z0-9!#$%&\'*+/=?^_`{|}~-]+)*@' '(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)*' '(?:[a-zA-Z]{2,4}|museum)$') def Notify(self, message, prefix=' ', popup=False, color=None, now=None, alignright=''): now = int(now or time.time()) color = color or self.NORM # We suppress duplicates that are either new or still on the screen. keys = self.notify_history.keys() if len(keys) > 20: for key in keys: if self.notify_history[key] < now-300: del self.notify_history[key] message = '%s' % message if message not in self.notify_history: # Display the time now and then. if (not alignright and (now >= (self.last_tick + 60)) and (len(message) < 68)): try: self.last_tick = now d = datetime.datetime.fromtimestamp(now) alignright = '[%2.2d:%2.2d]' % (d.hour, d.minute) except: pass # Fails on Python 2.2 if not now or now > 0: self.notify_history[message] = now msg = '\r%s %s%s%s%s%s\n' % ((prefix * 3)[0:3], color, message, self.NORM, ' ' * (75-len(message)-len(alignright)), alignright) self.wfile.write(msg) self.Status(self.status_tag, self.status_msg) def NotifyMOTD(self, frontend, motd_message): lc = 1 self.Notify(' ') for line in Q(motd_message).splitlines(): self.Notify((line.strip() or ' ' * (lc+2)), prefix=' ++', color=self.WHITE) lc += 1 self.Notify(' ' * (lc+2), alignright='[MOTD from %s]' % frontend) self.Notify(' ') def Status(self, tag, message=None, color=None): self.status_tag = tag self.status_col = color or self.status_col or self.NORM self.status_msg = '%s' % (message or self.status_msg) if not self.in_wizard: message = self.status_msg msg = ('\r << pagekite.py [%s]%s %s%s%s\r%s' ) % (tag, ' ' * (8-len(tag)), self.status_col, message[:52], ' ' * (52-len(message)), self.NORM) self.wfile.write(msg) if tag == 'exiting': self.wfile.write('\n') def Welcome(self, pre=None): if self.in_wizard: self.wfile.write('%s%s%s' % (self.CLEAR, self.WHITE, self.in_wizard)) if self.welcome: self.wfile.write('%s\r%s\n' % (self.NORM, Q(self.welcome))) self.welcome = None if self.in_wizard and self.wizard_tell: self.wfile.write('\n%s\r' % self.NORM) for line in self.wizard_tell: self.wfile.write('*** %s\n' % Q(line)) self.wizard_tell = None if pre: self.wfile.write('\n%s\r' % self.NORM) for line in pre: self.wfile.write(' %s\n' % Q(line)) self.wfile.write('\n%s\r' % self.NORM) def StartWizard(self, title): self.Welcome() banner = '>>> %s' % title banner = ('%s%s[CTRL+C = Cancel]\n') % (banner, ' ' * (62-len(banner))) self.in_wizard = banner self.tries = 200 def Retry(self): self.tries -= 1 return self.tries def EndWizard(self, quietly=False): if self.wizard_tell: self.Welcome() self.in_wizard = None if sys.platform in ('win32', 'os2', 'os2emx') and not quietly: self.wfile.write('\n<<< press ENTER to continue >>>\n') self.rfile.readline() def Spacer(self): self.wfile.write('\n') def Readline(self): line = self.rfile.readline() if line: return line.strip() else: raise IOError('EOF') def AskEmail(self, question, default=None, pre=[], wizard_hint=False, image=None, back=None, welcome=True): if welcome: self.Welcome(pre) while self.Retry(): self.wfile.write(' => %s ' % (Q(question), )) answer = self.Readline() if default and answer == '': return default if self.EMAIL_RE.match(answer.lower()): return answer if back is not None and answer == 'back': return back raise Exception('Too many tries') def AskLogin(self, question, default=None, email=None, pre=None, wizard_hint=False, image=None, back=None): self.Welcome(pre) def_email, def_pass = default or (email, None) self.wfile.write(' %s\n' % (Q(question), )) if not email: email = self.AskEmail('Your e-mail:', default=def_email, back=back, welcome=False) if email == back: return back import getpass self.wfile.write(' => ') return (email, getpass.getpass() or def_pass) def AskYesNo(self, question, default=None, pre=[], yes='yes', no='no', wizard_hint=False, image=None, back=None): self.Welcome(pre) yn = ((default is True) and '[Y/n]' ) or ((default is False) and '[y/N]' ) or ('[y/n]') while self.Retry(): self.wfile.write(' => %s %s ' % (Q(question), yn)) answer = self.Readline().lower() if default is not None and answer == '': answer = default and 'y' or 'n' if back is not None and answer.startswith('b'): return back if answer in ('y', 'n'): return (answer == 'y') raise Exception('Too many tries') def AskQuestion(self, question, pre=[], default=None, prompt=' =>', wizard_hint=False, image=None, back=None): self.Welcome(pre) self.wfile.write('%s %s ' % (prompt, Q(question))) return self.Readline() def AskKiteName(self, domains, question, pre=[], default=None, wizard_hint=False, image=None, back=None): self.Welcome(pre) if len(domains) == 1: self.wfile.write(('\n (Note: the ending %s will be added for you.)' ) % domains[0]) else: self.wfile.write('\n Please use one of the following domains:\n') for domain in domains: self.wfile.write('\n *%s' % domain) self.wfile.write('\n') while self.Retry(): self.wfile.write('\n => %s ' % Q(question)) answer = self.Readline().lower() if back is not None and answer == 'back': return back elif len(domains) == 1: answer = answer.replace(domains[0], '') if answer and SERVICE_SUBDOMAIN_RE.match(answer): return answer+domains[0] else: for domain in domains: if answer.endswith(domain): answer = answer.replace(domain, '') if answer and SERVICE_SUBDOMAIN_RE.match(answer): return answer+domain self.wfile.write(' (Please only use characters A-Z, 0-9, - and _.)') raise Exception('Too many tries') def AskMultipleChoice(self, choices, question, pre=[], default=None, wizard_hint=False, image=None, back=None): self.Welcome(pre) for i in range(0, len(choices)): self.wfile.write((' %s %d) %s\n' ) % ((default==i+1) and '*' or ' ', i+1, choices[i])) self.wfile.write('\n') while self.Retry(): d = default and (', default=%d' % default) or '' self.wfile.write(' => %s [1-%d%s] ' % (Q(question), len(choices), d)) try: answer = self.Readline().strip() if back is not None and answer.startswith('b'): return back choice = int(answer or default) if choice > 0 and choice <= len(choices): return choice except (ValueError, IndexError): pass raise Exception('Too many tries') def Tell(self, lines, error=False, back=None): if self.in_wizard: self.wizard_tell = lines else: self.Welcome() for line in lines: self.wfile.write(' %s\n' % line) if error: self.wfile.write('\n') return True def Working(self, message): if self.in_wizard: pending_messages = self.wizard_tell or [] self.wizard_tell = pending_messages + [message+' ...'] self.Welcome() self.wizard_tell = pending_messages + [message+' ... done.'] else: self.Tell([message]) return True pagekite-0.5.8a/debian/pagekite/usr/share/pyshared/pagekite/ui/nullui.py0000644000175000017500000002231212603542202025651 0ustar brebre00000000000000""" This is a basic "Null" user interface which does nothing at all. """ ############################################################################## LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2015, the Beanstalks Project ehf. and Bjarni Runar Einarsson This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see: """ ############################################################################## import sys from pagekite.compat import * from pagekite.common import * import pagekite.logging as logging class NullUi(object): """This is a UI that always returns default values or raises errors.""" DAEMON_FRIENDLY = True ALLOWS_INPUT = False WANTS_STDERR = False REJECTED_REASONS = { 'quota': 'You are out of quota', 'nodays': 'Your subscription has expired', 'noquota': 'You are out of quota', 'noconns': 'You are flying too many kites', 'unauthorized': 'Invalid account or shared secret' } def __init__(self, welcome=None, wfile=sys.stderr, rfile=sys.stdin): if sys.platform[:3] in ('win', 'os2'): self.CLEAR = '\n\n%s\n\n' % ('=' * 79) self.NORM = self.WHITE = self.GREY = self.GREEN = self.YELLOW = '' self.BLUE = self.RED = self.MAGENTA = self.CYAN = '' else: self.CLEAR = '\033[H\033[J' self.NORM = '\033[0m' self.WHITE = '\033[1m' self.GREY = '\033[0m' #'\033[30;1m' self.RED = '\033[31;1m' self.GREEN = '\033[32;1m' self.YELLOW = '\033[33;1m' self.BLUE = '\033[34;1m' self.MAGENTA = '\033[35;1m' self.CYAN = '\033[36;1m' self.wfile = wfile self.rfile = rfile self.welcome = welcome self.Reset() self.Splash() def Reset(self): self.in_wizard = False self.wizard_tell = None self.last_tick = 0 self.notify_history = {} self.status_tag = '' self.status_col = self.NORM self.status_msg = '' self.tries = 200 self.server_info = None def Splash(self): pass def Welcome(self): pass def StartWizard(self, title): pass def EndWizard(self, quietly=False): pass def Spacer(self): pass def Browse(self, url): import webbrowser self.Tell(['Opening %s in your browser...' % url]) webbrowser.open(url) def DefaultOrFail(self, question, default): if default is not None: return default raise ConfigError('Unanswerable question: %s' % question) def AskLogin(self, question, default=None, email=None, wizard_hint=False, image=None, back=None): return self.DefaultOrFail(question, default) def AskEmail(self, question, default=None, pre=None, wizard_hint=False, image=None, back=None): return self.DefaultOrFail(question, default) def AskYesNo(self, question, default=None, pre=None, yes='Yes', no='No', wizard_hint=False, image=None, back=None): return self.DefaultOrFail(question, default) def AskQuestion(self, question, pre=[], default=None, prompt=None, wizard_hint=False, image=None, back=None): return self.DefaultOrFail(question, default) def AskKiteName(self, domains, question, pre=[], default=None, wizard_hint=False, image=None, back=None): return self.DefaultOrFail(question, default) def AskMultipleChoice(self, choices, question, pre=[], default=None, wizard_hint=False, image=None, back=None): return self.DefaultOrFail(question, default) def AskBackends(self, kitename, protos, ports, rawports, question, pre=[], default=None, wizard_hint=False, image=None, back=None): return self.DefaultOrFail(question, default) def Working(self, message): pass def Tell(self, lines, error=False, back=None): if error: logging.LogError(' '.join(lines)) raise ConfigError(' '.join(lines)) else: logging.Log([('message', ' '.join(lines))]) return True def Notify(self, message, prefix=' ', popup=False, color=None, now=None, alignright=''): if popup: logging.Log([('info', '%s%s%s' % (message, alignright and ' ' or '', alignright))]) def NotifyMOTD(self, frontend, message): pass def NotifyKiteRejected(self, proto, domain, reason, crit=False): if reason in self.REJECTED_REASONS: reason = self.REJECTED_REASONS[reason] self.Notify('REJECTED: %s:%s (%s)' % (proto, domain, reason), prefix='!', color=(crit and self.RED or self.YELLOW)) def NotifyList(self, prefix, items, color): items = items[:] while items: show = [] while items and len(prefix) + len(' '.join(show)) < 65: show.append(items.pop(0)) self.Notify(' - %s: %s' % (prefix, ' '.join(show)), color=color) def NotifyServer(self, obj, server_info): self.server_info = server_info self.Notify('Connecting to front-end %s ...' % server_info[obj.S_NAME], color=self.GREY) self.NotifyList('Protocols', server_info[obj.S_PROTOS], self.GREY) self.NotifyList('Ports', server_info[obj.S_PORTS], self.GREY) if 'raw' in server_info[obj.S_PROTOS]: self.NotifyList('Raw ports', server_info[obj.S_RAW_PORTS], self.GREY) def NotifyQuota(self, quota, q_days, q_conns): qMB = 1024 msg = 'Quota: You have %.2f MB' % (float(quota) / qMB) if q_days is not None: msg += ', %d days' % q_days if q_conns is not None: msg += ' and %d connections' % q_conns self.Notify(msg + ' left.', prefix=(int(quota) < qMB) and '!' or ' ', color=self.MAGENTA) def NotifyFlyingFE(self, proto, port, domain, be=None): self.Notify(('Flying: %s://%s%s/' ) % (proto, domain, port and ':'+port or ''), prefix='~<>', color=self.CYAN) def StartListingBackEnds(self): pass def EndListingBackEnds(self): pass def NotifyBE(self, bid, be, has_ssl, dpaths, is_builtin=False, fingerprint=None): domain, port, proto = be[BE_DOMAIN], be[BE_PORT], be[BE_PROTO] prox = (proto == 'raw') and ' (HTTP proxied)' or '' if proto == 'raw' and port in ('22', 22): proto = 'ssh' if has_ssl and proto == 'http': proto = 'https' url = '%s://%s%s' % (proto, domain, port and (':%s' % port) or '') if be[BE_STATUS] == BE_STATUS_UNKNOWN: return if be[BE_STATUS] & BE_STATUS_OK: if be[BE_STATUS] & BE_STATUS_ERR_ANY: status = 'Trying' color = self.YELLOW prefix = ' ' else: status = 'Flying' color = self.CYAN prefix = '~<>' else: return if is_builtin: backend = 'builtin HTTPD' else: backend = '%s:%s' % (be[BE_BHOST], be[BE_BPORT]) self.Notify(('%s %s as %s/%s' ) % (status, backend, url, prox), prefix=prefix, color=color) if status == 'Flying': for dp in sorted(dpaths.keys()): self.Notify(' - %s%s' % (url, dp), color=self.BLUE) if fingerprint and proto.startswith('https'): self.Notify(' - Fingerprint=%s' % fingerprint, color=self.WHITE) self.Notify((' IMPORTANT: For maximum security, use a secure channel' ' to inform your'), color=self.YELLOW) self.Notify(' guests what fingerprint to expect.', color=self.YELLOW) def Status(self, tag, message=None, color=None): pass def ExplainError(self, error, title, subject=None): if error == 'pleaselogin': self.Tell([title, '', 'You already have an account. Log in to continue.' ], error=True) elif error == 'email': self.Tell([title, '', 'Invalid e-mail address. Please try again?' ], error=True) elif error == 'honey': self.Tell([title, '', 'Hmm. Somehow, you triggered the spam-filter.' ], error=True) elif error in ('domaintaken', 'domain', 'subdomain'): self.Tell([title, '', 'Sorry, that domain (%s) is unavailable.' % subject, '', 'If you registered it already, perhaps you need to log on with', 'a different e-mail address?', '' ], error=True) elif error == 'checkfailed': self.Tell([title, '', 'That domain (%s) is not correctly set up.' % subject ], error=True) elif error == 'network': self.Tell([title, '', 'There was a problem communicating with %s.' % subject, '', 'Please verify that you have a working' ' Internet connection and try again!' ], error=True) else: self.Tell([title, 'Error code: %s' % error, 'Try again later?' ], error=True) pagekite-0.5.8a/debian/pagekite/usr/share/pyshared/pagekite/dropper.py0000644000175000017500000000317512603542201025404 0ustar brebre00000000000000""" This is a "dropper template". A dropper is a single-purpose PageKite back-end connector which embeds its own configuration. """ ############################################################################## LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2015, the Beanstalks Project ehf. and Bjarni Runar Einarsson This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see: """ ############################################################################## import sys import pagekite.pk as pk import pagekite.httpd as httpd if __name__ == "__main__": kn = '@KITENAME@' ss = '@SECRET@' if len(sys.argv) == 1: sys.argv.extend([ '--daemonize', '--runas=nobody', '--logfile=/tmp/pagekite-%s.log' % kn, ]) sys.argv[1:1] = [ '--clean', '--noloop', '--nocrashreport', '--defaults', '--kitename=%s' % kn, '--kitesecret=%s' % ss, '--all' ] sys.argv.extend('@ARGS@'.split()) pk.Main(pk.PageKite, pk.Configure, http_handler=httpd.UiRequestHandler, http_server=httpd.UiHttpServer) pagekite-0.5.8a/debian/pagekite/usr/share/pyshared/pagekite/proto/0000775000175000017500000000000012610153761024525 5ustar brebre00000000000000pagekite-0.5.8a/debian/pagekite/usr/share/pyshared/pagekite/proto/__init__.py0000644000175000017500000000173012603542202026627 0ustar brebre00000000000000""" These are the PageKite protocol handling classes. """ ############################################################################## LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2015, the Beanstalks Project ehf. and Bjarni Runar Einarsson This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see: """ ############################################################################## pagekite-0.5.8a/debian/pagekite/usr/share/pyshared/pagekite/proto/filters.py0000644000175000017500000001603512603542202026544 0ustar brebre00000000000000""" These are filters placed at the end of a tunnel for watching or modifying the traffic. """ ############################################################################## LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2015, the Beanstalks Project ehf. and Bjarni Runar Einarsson This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see: """ ############################################################################## import re import time from pagekite.compat import * class TunnelFilter: """Base class for watchers/filters for data going in/out of Tunnels.""" IDLE_TIMEOUT = 1800 def __init__(self, ui): self.sid = {} self.ui = ui def clean_idle_sids(self, now=None): now = now or time.time() for sid in self.sid.keys(): if self.sid[sid]['_ts'] < now - self.IDLE_TIMEOUT: del self.sid[sid] def filter_set_sid(self, sid, info): now = time.time() if sid not in self.sid: self.sid[sid] = {} self.sid[sid].update(info) self.sid[sid]['_ts'] = now self.clean_idle_sids(now=now) def filter_data_in(self, tunnel, sid, data): if sid not in self.sid: self.sid[sid] = {} self.sid[sid]['_ts'] = time.time() return data def filter_data_out(self, tunnel, sid, data): if sid not in self.sid: self.sid[sid] = {} self.sid[sid]['_ts'] = time.time() return data class TunnelWatcher(TunnelFilter): """Base class for watchers/filters for data going in/out of Tunnels.""" def __init__(self, ui, watch_level=0): TunnelFilter.__init__(self, ui) self.watch_level = watch_level def format_data(self, data, level): if '\r\n\r\n' in data: head, tail = data.split('\r\n\r\n', 1) output = self.format_data(head, level) output[-1] += '\\r\\n' output.append('\\r\\n') if tail: output.extend(self.format_data(tail, level)) return output else: output = data.encode('string_escape').replace('\\n', '\\n\n') if output.count('\\') > 0.15*len(output): if level > 2: output = [['', '']] count = 0 for d in data: output[-1][0] += '%2.2x' % ord(d) output[-1][1] += '%c' % ((ord(d) > 31 and ord(d) < 127) and d or '.') count += 1 if (count % 2) == 0: output[-1][0] += ' ' if (count % 20) == 0: output.append(['', '']) return ['%-50s %s' % (l[0], l[1]) for l in output] else: return ['<< Binary bytes: %d >>' % len(data)] else: return output.strip().splitlines() def now(self): return ts_to_iso(int(10*time.time())/10.0 ).replace('T', ' ').replace('00000', '') def filter_data_in(self, tunnel, sid, data): if data and self.watch_level[0] > 0: self.ui.Notify('===[ INCOMING @ %s ]===' % self.now(), color=self.ui.WHITE, prefix=' __') for line in self.format_data(data, self.watch_level[0]): self.ui.Notify(line, prefix=' <=', now=-1, color=self.ui.GREEN) return TunnelFilter.filter_data_in(self, tunnel, sid, data) def filter_data_out(self, tunnel, sid, data): if data and self.watch_level[0] > 1: self.ui.Notify('===[ OUTGOING @ %s ]===' % self.now(), color=self.ui.WHITE, prefix=' __') for line in self.format_data(data, self.watch_level[0]): self.ui.Notify(line, prefix=' =>', now=-1, color=self.ui.BLUE) return TunnelFilter.filter_data_out(self, tunnel, sid, data) class HttpHeaderFilter(TunnelFilter): """Filter that adds X-Forwarded-For and X-Forwarded-Proto to requests.""" HTTP_HEADER = re.compile('(?ism)^(([A-Z]+) ([^\n]+) HTTP/\d+\.\d+\s*)$') DISABLE = 'rawheaders' def filter_data_in(self, tunnel, sid, data): info = self.sid.get(sid) if (info and info.get('proto') in ('http', 'http2', 'http3', 'websocket') and not info.get(self.DISABLE, False)): # FIXME: Check content-length and skip bodies entirely http_hdr = self.HTTP_HEADER.search(data) if http_hdr: data = self.filter_header_data_in(http_hdr, data, info) return TunnelFilter.filter_data_in(self, tunnel, sid, data) def filter_header_data_in(self, http_hdr, data, info): clean_headers = [ r'(?mi)^(X-(PageKite|Forwarded)-(For|Proto|Port):)' ] add_headers = [ 'X-Forwarded-For: %s' % info.get('remote_ip', 'unknown'), 'X-Forwarded-Proto: %s' % (info.get('using_tls') and 'https' or 'http'), 'X-PageKite-Port: %s' % info.get('port', 0) ] if info.get('rewritehost', False): add_headers.append('Host: %s' % info.get('rewritehost')) clean_headers.append(r'(?mi)^(Host:)') if http_hdr.group(1).upper() in ('POST', 'PUT'): # FIXME: This is a bit ugly add_headers.append('Connection: close') clean_headers.append(r'(?mi)^(Connection|Keep-Alive):') info['rawheaders'] = True for hdr_re in clean_headers: data = re.sub(hdr_re, 'X-Old-\\1', data) return re.sub(self.HTTP_HEADER, '\\1\n%s\r' % '\r\n'.join(add_headers), data) class HttpSecurityFilter(HttpHeaderFilter): """Filter that blocks known-to-be-dangerous requests.""" DISABLE = 'trusted' HTTP_DANGER = re.compile('(?ism)^((get|post|put|patch|delete) ' # xampp paths, anything starting with /adm* '((?:/+(?:xampp/|security/|licenses/|webalizer/|server-(?:status|info)|adm)' '|[^\n]*/' # WordPress admin pages '(?:wp-admin/(?!admin-ajax|css/)|wp-config\.php' # Hackzor tricks '|system32/|\.\.|\.ht(?:access|pass)' # phpMyAdmin and similar tools '|(?:php|sql)?my(?:sql)?(?:adm|manager)' # Setup pages for common PHP tools '|(?:adm[^\n]*|install[^\n]*|setup)\.php)' ')[^\n]*)' ' HTTP/\d+\.\d+\s*)$') REJECT = 'PAGEKITE_REJECT_' def filter_header_data_in(self, http_hdr, data, info): danger = self.HTTP_DANGER.search(data) if danger: self.ui.Notify('BLOCKED: %s %s' % (danger.group(2), danger.group(3)), color=self.ui.RED, prefix='***') self.ui.Notify('See https://pagekite.net/support/security/ for more' ' details.') return self.REJECT+data else: return data pagekite-0.5.8a/debian/pagekite/usr/share/pyshared/pagekite/proto/selectables.py0000644000175000017500000006571612603560755027411 0ustar brebre00000000000000""" Selectables are low level base classes which cooperate with our select-loop. """ ############################################################################## LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2015, the Beanstalks Project ehf. and Bjarni Runar Einarsson This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see: """ ############################################################################## import errno import struct import threading import time import zlib from pagekite.compat import * from pagekite.common import * import pagekite.logging as logging import pagekite.compat as compat import pagekite.common as common def obfuIp(ip): quads = ('%s' % ip).replace(':', '.').split('.') return '~%s' % '.'.join([q for q in quads[-2:]]) SELECTABLE_LOCK = threading.Lock() SELECTABLE_ID = 0 SELECTABLES = {} def getSelectableId(what): global SELECTABLES, SELECTABLE_ID, SELECTABLE_LOCK try: SELECTABLE_LOCK.acquire() count = 0 while SELECTABLE_ID in SELECTABLES: SELECTABLE_ID += 1 SELECTABLE_ID %= 0x10000 if (SELECTABLE_ID % 0x00800) == 0: logging.LogDebug('Selectable map: %s' % (SELECTABLES, )) count += 1 if count > 0x10001: raise ValueError('Too many conns!') SELECTABLES[SELECTABLE_ID] = what return SELECTABLE_ID finally: SELECTABLE_LOCK.release() class Selectable(object): """A wrapper around a socket, for use with select.""" HARMLESS_ERRNOS = (errno.EINTR, errno.EAGAIN, errno.ENOMEM, errno.EBUSY, errno.EDEADLK, errno.EWOULDBLOCK, errno.ENOBUFS, errno.EALREADY) def __init__(self, fd=None, address=None, on_port=None, maxread=16*1024, ui=None, tracked=True, bind=None, backlog=100): self.fd = None try: self.SetFD(fd or rawsocket(socket.AF_INET6, socket.SOCK_STREAM), six=True) if bind: self.fd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.fd.bind(bind) self.fd.listen(backlog) self.fd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) except: self.SetFD(fd or rawsocket(socket.AF_INET, socket.SOCK_STREAM)) if bind: self.fd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.fd.bind(bind) self.fd.listen(backlog) self.fd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.address = address self.on_port = on_port self.created = self.bytes_logged = time.time() self.last_activity = 0 self.dead = False self.ui = ui # Quota-related stuff self.quota = None self.q_conns = None self.q_days = None # Read-related variables self.maxread = maxread self.read_bytes = self.all_in = 0 self.read_eof = False self.peeking = False self.peeked = 0 # Write-related variables self.wrote_bytes = self.all_out = 0 self.write_blocked = '' self.write_speed = 102400 self.write_eof = False self.write_retry = None # Flow control v1 self.throttle_until = (time.time() - 1) self.max_read_speed = 96*1024 # Flow control v2 self.acked_kb_delta = 0 # Compression stuff self.lock = threading.Lock() self.zw = None self.zlevel = 1 self.zreset = False # Logging self.alt_id = None self.countas = 'selectables_live' self.sid = self.gsid = getSelectableId(self.countas) if address: addr = address or ('x.x.x.x', 'x') self.log_id = 's%x/%s:%s' % (self.sid, obfuIp(addr[0]), addr[1]) else: self.log_id = 's%x' % self.sid if common.gYamon: common.gYamon.vadd(self.countas, 1) common.gYamon.vadd('selectables', 1) def CountAs(self, what): if common.gYamon: common.gYamon.vadd(self.countas, -1) common.gYamon.vadd(what, 1) self.countas = what global SELECTABLES SELECTABLES[self.gsid] = '%s %s' % (self.countas, self) def Cleanup(self, close=True): self.peeked = self.zw = '' self.Die(discard_buffer=True) if close: if self.fd: if logging.DEBUG_IO: self.LogDebug('Closing FD: %s' % self) self.fd.close() self.fd = None if not self.dead: self.dead = True self.CountAs('selectables_dead') if close: self.LogTraffic(final=True) def __del__(self): try: if common.gYamon: common.gYamon.vadd(self.countas, -1) common.gYamon.vadd('selectables', -1) except AttributeError: pass try: global SELECTABLES del SELECTABLES[self.gsid] except (KeyError, TypeError): pass def __str__(self): return '%s: %s<%s%s%s>' % (self.log_id, self.__class__, self.read_eof and '-' or 'r', self.write_eof and '-' or 'w', len(self.write_blocked)) def __html__(self): try: peer = self.fd.getpeername() sock = self.fd.getsockname() except: peer = ('x.x.x.x', 'x') sock = ('x.x.x.x', 'x') return ('Outgoing ZChunks: %s
    ' 'Buffered bytes: %s
    ' 'Remote address: %s
    ' 'Local address: %s
    ' 'Bytes in / out: %s / %s
    ' 'Created: %s
    ' 'Status: %s
    ' '\n') % (self.zw and ('level %d' % self.zlevel) or 'off', len(self.write_blocked), self.dead and '-' or (obfuIp(peer[0]), peer[1]), self.dead and '-' or (obfuIp(sock[0]), sock[1]), self.all_in + self.read_bytes, self.all_out + self.wrote_bytes, time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(self.created)), self.dead and 'dead' or 'alive') def ResetZChunks(self): if self.zw: self.zreset = True self.zw = zlib.compressobj(self.zlevel) def EnableZChunks(self, level=1): self.zlevel = level self.zw = zlib.compressobj(level) def SetFD(self, fd, six=False): if self.fd: self.fd.close() self.fd = fd if fd: self.fd.setblocking(0) try: if six: self.fd.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) # This hurts mobile devices, let's try living without it #self.fd.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) #self.fd.setsockopt(socket.SOL_TCP, socket.TCP_KEEPIDLE, 60) #self.fd.setsockopt(socket.SOL_TCP, socket.TCP_KEEPCNT, 10) #self.fd.setsockopt(socket.SOL_TCP, socket.TCP_KEEPINTVL, 1) except: pass def SetConn(self, conn): self.SetFD(conn.fd) self.log_id = conn.log_id self.read_bytes = conn.read_bytes self.wrote_bytes = conn.wrote_bytes def Log(self, values): if self.log_id: values.append(('id', self.log_id)) logging.Log(values) def LogError(self, error, params=None): values = params or [] if self.log_id: values.append(('id', self.log_id)) logging.LogError(error, values) def LogDebug(self, message, params=None): values = params or [] if self.log_id: values.append(('id', self.log_id)) logging.LogDebug(message, values) def LogInfo(self, message, params=None): values = params or [] if self.log_id: values.append(('id', self.log_id)) logging.LogInfo(message, values) def LogTrafficStatus(self, final=False): if self.ui: self.ui.Status('traffic') def LogTraffic(self, final=False): if self.wrote_bytes or self.read_bytes: now = time.time() self.all_out += self.wrote_bytes self.all_in += self.read_bytes self.LogTrafficStatus(final) if common.gYamon: common.gYamon.vadd("bytes_all", self.wrote_bytes + self.read_bytes, wrap=1000000000) if final: self.Log([('wrote', '%d' % self.wrote_bytes), ('wbps', '%d' % self.write_speed), ('read', '%d' % self.read_bytes), ('eof', '1')]) else: self.Log([('wrote', '%d' % self.wrote_bytes), ('wbps', '%d' % self.write_speed), ('read', '%d' % self.read_bytes)]) self.bytes_logged = now self.wrote_bytes = self.read_bytes = 0 elif final: self.Log([('eof', '1')]) global SELECTABLES SELECTABLES[self.gsid] = '%s %s' % (self.countas, self) def SayHello(self): pass def ProcessData(self, data): self.LogError('Selectable::ProcessData: Should be overridden!') return False def ProcessEof(self): global SELECTABLES SELECTABLES[self.gsid] = '%s %s' % (self.countas, self) if self.read_eof and self.write_eof and not self.write_blocked: self.Cleanup() return False return True def ProcessEofRead(self): self.read_eof = True self.LogError('Selectable::ProcessEofRead: Should be overridden!') return False def ProcessEofWrite(self): self.write_eof = True self.LogError('Selectable::ProcessEofWrite: Should be overridden!') return False def EatPeeked(self, eat_bytes=None, keep_peeking=False): if not self.peeking: return if eat_bytes is None: eat_bytes = self.peeked discard = '' while len(discard) < eat_bytes: try: discard += self.fd.recv(eat_bytes - len(discard)) except socket.error, (errno, msg): self.LogInfo('Error reading (%d/%d) socket: %s (errno=%s)' % ( eat_bytes, self.peeked, msg, errno)) time.sleep(0.1) if logging.DEBUG_IO: print '===[ ATE %d PEEKED BYTES ]===\n' % eat_bytes self.peeked -= eat_bytes self.peeking = keep_peeking return def ReadData(self, maxread=None): if self.read_eof: return False now = time.time() maxread = maxread or self.maxread flooded = self.Flooded(now) if flooded > self.max_read_speed and not self.acked_kb_delta: # FIXME: This is v1 flow control, kill it when 0.4.7 is "everywhere" last = self.throttle_until # Disable local throttling for really slow connections; remote # throttles (trigged by blocked sockets) still work. if self.max_read_speed > 1024: self.AutoThrottle() maxread = 1024 if now > last and self.all_in > 2*self.max_read_speed: self.max_read_speed *= 1.25 self.max_read_speed += maxread try: if self.peeking: data = self.fd.recv(maxread, socket.MSG_PEEK) self.peeked = len(data) if logging.DEBUG_IO: print '<== PEEK =[%s]==(\n%s)==' % (self, data[:160]) else: data = self.fd.recv(maxread) if logging.DEBUG_IO: print ('<== IN =[%s @ %dbps]==(\n%s)==' ) % (self, self.max_read_speed, data[:160]) except (SSL.WantReadError, SSL.WantWriteError), err: return True except IOError, err: if err.errno not in self.HARMLESS_ERRNOS: self.LogDebug('Error reading socket: %s (%s)' % (err, err.errno)) return False else: return True except (SSL.Error, SSL.ZeroReturnError, SSL.SysCallError), err: self.LogDebug('Error reading socket (SSL): %s' % err) return False except socket.error, (errno, msg): if errno in self.HARMLESS_ERRNOS: return True else: self.LogInfo('Error reading socket: %s (errno=%s)' % (msg, errno)) return False self.last_activity = now if data is None or data == '': self.read_eof = True if logging.DEBUG_IO: print '<== IN =[%s]==(EOF)==' % self return self.ProcessData('') else: if not self.peeking: self.read_bytes += len(data) if self.acked_kb_delta: self.acked_kb_delta += (len(data)/1024) if self.read_bytes > logging.LOG_THRESHOLD: self.LogTraffic() return self.ProcessData(data) def Flooded(self, now=None): delta = ((now or time.time()) - self.created) if delta >= 1: flooded = self.read_bytes + self.all_in flooded -= self.max_read_speed * 0.95 * delta return flooded else: return 0 def RecordProgress(self, skb, bps): if skb >= 0: all_read = (self.all_in + self.read_bytes) / 1024 if self.acked_kb_delta: self.acked_kb_delta = max(1, all_read - skb) self.LogDebug('Delta is: %d' % self.acked_kb_delta) elif bps >= 0: self.Throttle(max_speed=bps, remote=True) def Throttle(self, max_speed=None, remote=False, delay=0.2): if max_speed: self.max_read_speed = max_speed flooded = max(-1, self.Flooded()) if self.max_read_speed: delay = min(10, max(0.1, flooded/self.max_read_speed)) if flooded < 0: delay = 0 if delay: ot = self.throttle_until self.throttle_until = time.time() + delay if ((self.throttle_until - ot) > 30 or (int(ot) != int(self.throttle_until) and delay > 8)): self.LogInfo('Throttled %.1fs until %x (flood=%d, bps=%s, %s)' % ( delay, self.throttle_until, flooded, self.max_read_speed, remote and 'remote' or 'local')) return True def AutoThrottle(self, max_speed=None, remote=False, delay=0.2): return self.Throttle(max_speed, remote, delay) def Send(self, data, try_flush=False, activity=False, just_buffer=False, allow_blocking=False): self.write_speed = int((self.wrote_bytes + self.all_out) / max(1, (time.time() - self.created))) # If we're already blocked, just buffer unless explicitly asked to flush. if ((just_buffer) or ((not try_flush) and (len(self.write_blocked) > 0 or compat.SEND_ALWAYS_BUFFERS))): self.write_blocked += str(''.join(data)) return True sending = ''.join([self.write_blocked, str(''.join(data))]) self.write_blocked = '' sent_bytes = 0 if sending: try: want_send = self.write_retry or min(len(sending), SEND_MAX_BYTES) sent_bytes = self.fd.send(sending[:want_send]) if logging.DEBUG_IO: print ('==> OUT =[%s: %d/%d bytes]==(\n%s)==' ) % (self, sent_bytes, want_send, sending[:min(160, sent_bytes)]) self.wrote_bytes += sent_bytes self.write_retry = None except (SSL.WantWriteError, SSL.WantReadError), err: if logging.DEBUG_IO: print '=== WRITE SSL RETRY: =[%s: %s bytes]==' % (self, want_send) self.write_retry = want_send except IOError, err: if err.errno not in self.HARMLESS_ERRNOS: self.LogInfo('Error sending: %s' % err) self.ProcessEofWrite() return False else: if logging.DEBUG_IO: print '=== WRITE HICCUP: =[%s: %s bytes]==' % (self, want_send) self.write_retry = want_send except socket.error, (errno, msg): if errno not in self.HARMLESS_ERRNOS: self.LogInfo('Error sending: %s (errno=%s)' % (msg, errno)) self.ProcessEofWrite() return False else: if logging.DEBUG_IO: print '=== WRITE HICCUP: =[%s: %s bytes]==' % (self, want_send) self.write_retry = want_send except (SSL.Error, SSL.ZeroReturnError, SSL.SysCallError), err: self.LogInfo('Error sending (SSL): %s' % err) self.ProcessEofWrite() return False except AttributeError: # This has been seen in the wild, is most likely some sort of # race during shutdown. :-( self.LogInfo('AttributeError, self.fd=%s' % self.fd) self.ProcessEofWrite() return False if activity: self.last_activity = time.time() self.write_blocked = sending[sent_bytes:] if self.wrote_bytes >= logging.LOG_THRESHOLD: self.LogTraffic() if self.write_eof and not self.write_blocked: self.ProcessEofWrite() return True def SendChunked(self, data, compress=True, zhistory=None, just_buffer=False): rst = '' if self.zreset: self.zreset = False rst = 'R' # Stop compressing streams that just get bigger. if zhistory and (zhistory[0] < zhistory[1]): compress = False try: try: if self.lock: self.lock.acquire() sdata = ''.join(data) if self.zw and compress and len(sdata) > 64: try: zdata = self.zw.compress(sdata) + self.zw.flush(zlib.Z_SYNC_FLUSH) if zhistory: zhistory[0] = len(sdata) zhistory[1] = len(zdata) return self.Send(['%xZ%x%s\r\n' % (len(sdata), len(zdata), rst), zdata], activity=False, try_flush=(not just_buffer), just_buffer=just_buffer) except zlib.error: logging.LogError('Error compressing, resetting ZChunks.') self.ResetZChunks() return self.Send(['%x%s\r\n' % (len(sdata), rst), sdata], activity=False, try_flush=(not just_buffer), just_buffer=just_buffer) except UnicodeDecodeError: logging.LogError('UnicodeDecodeError in SendChunked, wtf?') return False finally: if self.lock: self.lock.release() def Flush(self, loops=50, wait=False, allow_blocking=False): while (loops != 0 and len(self.write_blocked) > 0 and self.Send([], try_flush=True, activity=False, allow_blocking=allow_blocking)): if wait and len(self.write_blocked) > 0: time.sleep(0.1) logging.LogDebug('Flushing...') loops -= 1 if self.write_blocked: return False return True def IsReadable(s, now): return (s.fd and (not s.read_eof) and (s.acked_kb_delta < 64) # FIXME and (s.throttle_until <= now)) def IsBlocked(s): return (s.fd and (len(s.write_blocked) > 0)) def IsDead(s): return (s.read_eof and s.write_eof and not s.write_blocked) def Die(self, discard_buffer=False): if discard_buffer: self.write_blocked = '' self.read_eof = self.write_eof = True return True class LineParser(Selectable): """A Selectable which parses the input as lines of text.""" def __init__(self, fd=None, address=None, on_port=None, ui=None, tracked=True): Selectable.__init__(self, fd, address, on_port, ui=ui, tracked=tracked) self.leftovers = '' def __html__(self): return Selectable.__html__(self) def Cleanup(self, close=True): Selectable.Cleanup(self, close=close) self.leftovers = '' def ProcessData(self, data): lines = (self.leftovers+data).splitlines(True) self.leftovers = '' while lines: line = lines.pop(0) if line.endswith('\n'): if self.ProcessLine(line, lines) is False: return False else: if not self.peeking: self.leftovers += line if self.read_eof: return self.ProcessEofRead() return True def ProcessLine(self, line, lines): self.LogError('LineParser::ProcessLine: Should be overridden!') return False TLS_CLIENTHELLO = '%c' % 026 SSL_CLIENTHELLO = '\x80' MINECRAFT_HANDSHAKE = '%c' % (0x02, ) FLASH_POLICY_REQ = '' # FIXME: XMPP support class MagicProtocolParser(LineParser): """A Selectable which recognizes HTTP, TLS or XMPP preambles.""" def __init__(self, fd=None, address=None, on_port=None, ui=None): LineParser.__init__(self, fd, address, on_port, ui=ui, tracked=False) self.leftovers = '' self.might_be_tls = True self.is_tls = False self.my_tls = False def __html__(self): return ('Detected TLS: %s
    ' '%s') % (self.is_tls, LineParser.__html__(self)) # FIXME: DEPRECATE: Make this all go away, switch to CONNECT. def ProcessMagic(self, data): args = {} try: prefix, words, data = data.split('\r\n', 2) for arg in words.split('; '): key, val = arg.split('=', 1) args[key] = val self.EatPeeked(eat_bytes=len(prefix)+2+len(words)+2) except ValueError, e: return True try: port = 'port' in args and args['port'] or None if port: self.on_port = int(port) except ValueError, e: return False proto = 'proto' in args and args['proto'] or None if proto in ('http', 'http2', 'http3', 'websocket'): return LineParser.ProcessData(self, data) domain = 'domain' in args and args['domain'] or None if proto == 'https': return self.ProcessTls(data, domain) if proto == 'raw' and domain: return self.ProcessProto(data, 'raw', domain) return False def ProcessData(self, data): # Uncomment when adding support for new protocols: # #self.LogDebug(('DATA: >%s<' # ) % ' '.join(['%2.2x' % ord(d) for d in data])) if data.startswith(MAGIC_PREFIX): return self.ProcessMagic(data) if self.might_be_tls: self.might_be_tls = False if not (data.startswith(TLS_CLIENTHELLO) or data.startswith(SSL_CLIENTHELLO)): self.EatPeeked() # FIXME: These only work if the full policy request or minecraft # handshake are present in the first data packet. if data.startswith(FLASH_POLICY_REQ): return self.ProcessFlashPolicyRequest(data) if data.startswith(MINECRAFT_HANDSHAKE): user, server, port = self.GetMinecraftInfo(data) if user and server: return self.ProcessProto(data, 'minecraft', server) return LineParser.ProcessData(self, data) self.is_tls = True if self.is_tls: return self.ProcessTls(data) else: self.EatPeeked() return LineParser.ProcessData(self, data) def GetMsg(self, data): mtype, ml24, mlen = struct.unpack('>BBH', data[0:4]) mlen += ml24 * 0x10000 return mtype, data[4:4+mlen], data[4+mlen:] def GetClientHelloExtensions(self, msg): # Ugh, so many magic numbers! These are accumulated sizes of # the different fields we are ignoring in the TLS headers. slen = struct.unpack('>B', msg[34])[0] cslen = struct.unpack('>H', msg[35+slen:37+slen])[0] cmlen = struct.unpack('>B', msg[37+slen+cslen])[0] extofs = 34+1+2+1+2+slen+cslen+cmlen if extofs < len(msg): return msg[extofs:] return None def GetSniNames(self, extensions): names = [] while extensions: etype, elen = struct.unpack('>HH', extensions[0:4]) if etype == 0: # OK, we found an SNI extension, get the list. namelist = extensions[6:4+elen] while namelist: ntype, nlen = struct.unpack('>BH', namelist[0:3]) if ntype == 0: names.append(namelist[3:3+nlen].lower()) namelist = namelist[3+nlen:] extensions = extensions[4+elen:] return names def GetSni(self, data): hello, vmajor, vminor, mlen = struct.unpack('>BBBH', data[0:5]) data = data[5:] sni = [] while data: mtype, msg, data = self.GetMsg(data) if mtype == 1: # ClientHello! sni.extend(self.GetSniNames(self.GetClientHelloExtensions(msg))) return sni def GetMinecraftInfo(self, data): try: (packet, version, unlen) = struct.unpack('>bbh', data[0:4]) unlen *= 2 (hnlen, ) = struct.unpack('>h', data[4+unlen:6+unlen]) hnlen *= 2 (port, ) = struct.unpack('>i', data[6+unlen+hnlen:10+unlen+hnlen]) uname = data[4:4+unlen].decode('utf_16_be').encode('utf-8') sname = data[6+unlen:6+hnlen+unlen].decode('utf_16_be').encode('utf-8') return uname, sname, port except: return None, None, None def ProcessFlashPolicyRequest(self, data): self.LogError('MagicProtocolParser::ProcessFlashPolicyRequest: Should be overridden!') return False def ProcessTls(self, data, domain=None): self.LogError('MagicProtocolParser::ProcessTls: Should be overridden!') return False def ProcessProto(self, data, proto, domain): self.LogError('MagicProtocolParser::ProcessProto: Should be overridden!') return False class ChunkParser(Selectable): """A Selectable which parses the input as chunks.""" def __init__(self, fd=None, address=None, on_port=None, ui=None): Selectable.__init__(self, fd, address, on_port, ui=ui) self.want_cbytes = 0 self.want_bytes = 0 self.compressed = False self.header = '' self.chunk = '' self.zr = zlib.decompressobj() def __html__(self): return Selectable.__html__(self) def Cleanup(self, close=True): Selectable.Cleanup(self, close=close) self.zr = self.chunk = self.header = None def ProcessData(self, data): loops = 1500 result = more = True while result and more and (loops > 0): loops -= 1 if self.peeking: self.want_cbytes = 0 self.want_bytes = 0 self.header = '' self.chunk = '' if self.want_bytes == 0: self.header += (data or '') if self.header.find('\r\n') < 0: if self.read_eof: return self.ProcessEofRead() return True try: size, data = self.header.split('\r\n', 1) self.header = '' if size.endswith('R'): self.zr = zlib.decompressobj() size = size[0:-1] if 'Z' in size: csize, zsize = size.split('Z') self.compressed = True self.want_cbytes = int(csize, 16) self.want_bytes = int(zsize, 16) else: self.compressed = False self.want_bytes = int(size, 16) except ValueError, err: self.LogError('ChunkParser::ProcessData: %s' % err) self.Log([('bad_data', data)]) return False if self.want_bytes == 0: return False process = data[:self.want_bytes] data = more = data[self.want_bytes:] self.chunk += process self.want_bytes -= len(process) if self.want_bytes == 0: if self.compressed: try: cchunk = self.zr.decompress(self.chunk) except zlib.error: cchunk = '' if len(cchunk) != self.want_cbytes: result = self.ProcessCorruptChunk(self.chunk) else: result = self.ProcessChunk(cchunk) else: result = self.ProcessChunk(self.chunk) self.chunk = '' if result and more: self.LogError('Unprocessed data: %s' % data) raise BugFoundError('Too much data') elif self.read_eof: return self.ProcessEofRead() and result else: return result def ProcessCorruptChunk(self, chunk): self.LogError('ChunkParser::ProcessData: ProcessCorruptChunk not overridden!') return False def ProcessChunk(self, chunk): self.LogError('ChunkParser::ProcessData: ProcessChunk not overridden!') return False pagekite-0.5.8a/debian/pagekite/usr/share/pyshared/pagekite/proto/conns.py0000644000175000017500000017334712607425760026243 0ustar brebre00000000000000""" These are the Connection classes, relatively high level classes that handle incoming or outgoing network connections. """ ############################################################################## LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2015, the Beanstalks Project ehf. and Bjarni Runar Einarsson This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see: """ ############################################################################## import socket import sys import threading import time import traceback from pagekite.compat import * from pagekite.common import * import pagekite.common as common import pagekite.logging as logging from filters import HttpSecurityFilter from selectables import * from parsers import * from proto import * class Tunnel(ChunkParser): """A Selectable representing a PageKite tunnel.""" S_NAME = 0 S_PORTS = 1 S_RAW_PORTS = 2 S_PROTOS = 3 S_ADD_KITES = 4 S_IS_MOBILE = 5 def __init__(self, conns): ChunkParser.__init__(self, ui=conns.config.ui) self.server_info = ['x.x.x.x:x', [], [], [], False, False] self.Init(conns) # We want to be sure to read the entire chunk at once, including # headers to save cycles, so we double the size we're willing to # read here. self.maxread *= 2 def Init(self, conns): self.conns = conns self.users = {} self.remote_ssl = {} self.zhistory = {} self.backends = {} self.last_ping = 0 self.weighted_rtt = -1 self.using_tls = False self.filters = [] def Cleanup(self, close=True): if self.users: for sid in self.users.keys(): self.CloseStream(sid) ChunkParser.Cleanup(self, close=close) self.Init(None) def __html__(self): return ('Server name: %s
    ' '%s') % (self.server_info[self.S_NAME], ChunkParser.__html__(self)) def LogTrafficStatus(self, final=False): if self.ui: if final: message = 'Disconnected from: %s' % self.server_info[self.S_NAME] self.ui.Status('down', color=self.ui.GREY, message=message) else: self.ui.Status('traffic') def GetKiteRequests(self, parse): requests = [] for prefix in ('X-Beanstalk', 'X-PageKite'): for bs in parse.Header(prefix): # X-PageKite: proto:my.domain.com:token:signature proto, domain, srand, token, sign = bs.split(':') requests.append((proto.lower(), domain.lower(), srand, token, sign, prefix)) return requests def _FrontEnd(conn, body, conns): """This is what the front-end does when a back-end requests a new tunnel.""" self = Tunnel(conns) try: for prefix in ('X-Beanstalk', 'X-PageKite'): for feature in conn.parser.Header(prefix+'-Features'): if not conns.config.disable_zchunks: if feature == 'ZChunks': self.EnableZChunks(level=1) elif feature == 'AddKites': self.server_info[self.S_ADD_KITES] = True elif feature == 'Mobile': self.server_info[self.S_IS_MOBILE] = True # Track which versions we see in the wild. version = 'old' for v in conn.parser.Header(prefix+'-Version'): version = v if common.gYamon: common.gYamon.vadd('version-%s' % version, 1, wrap=10000000) for replace in conn.parser.Header(prefix+'-Replace'): if replace in self.conns.conns_by_id: repl = self.conns.conns_by_id[replace] self.LogInfo('Disconnecting old tunnel: %s' % repl) repl.Die(discard_buffer=True) requests = self.GetKiteRequests(conn.parser) except Exception, err: self.LogError('Discarding connection: %s' % err) self.Cleanup() return None except socket.error, err: self.LogInfo('Discarding connection: %s' % err) self.Cleanup() return None self.last_activity = time.time() self.CountAs('backends_live') self.SetConn(conn) if requests: conns.auth().check(requests[:], conn, lambda r, l: self.AuthCallback(conn, r, l)) return self def RecheckQuota(self, conns, when=None): if when is None: when = time.time() if (self.quota and self.quota[0] is not None and self.quota[1] and (self.quota[2] < when-900)): self.quota[2] = when self.LogDebug('Rechecking: %s' % (self.quota, )) conns.auth().check(self.quota[1], self, lambda r, l: self.QuotaCallback(conns, r, l)) def ProcessAuthResults(self, results, duplicates_ok=False, add_tunnels=True): ok = [] bad = [] if not self.conns: # This can be delayed until the connecting client gives up, which # means we may have already called Die(). In that case, just abort. return True ok_results = ['X-PageKite-OK'] bad_results = ['X-PageKite-Invalid'] if duplicates_ok is True: ok_results.extend(['X-PageKite-Duplicate']) elif duplicates_ok is False: bad_results.extend(['X-PageKite-Duplicate']) for r in results: if r[0] in ok_results: ok.append(r[1]) elif r[0] in bad_results: bad.append(r[1]) elif r[0] == 'X-PageKite-SessionID': self.conns.SetAltId(self, r[1]) logi = [] if self.server_info[self.S_IS_MOBILE]: logi.append(('mobile', 'True')) if self.server_info[self.S_ADD_KITES]: logi.append(('add_kites', 'True')) if bad: for backend in bad: if backend in self.backends: del self.backends[backend] proto, domain, srand = backend.split(':') self.Log([('BE', 'Dead'), ('proto', proto), ('domain', domain)] + logi) self.conns.CloseTunnel(proto, domain, self) if add_tunnels: for backend in ok: if backend not in self.backends: self.backends[backend] = 1 proto, domain, srand = backend.split(':') self.Log([('BE', 'Live'), ('proto', proto), ('domain', domain)] + logi) self.conns.Tunnel(proto, domain, self) if not ok: if self.server_info[self.S_ADD_KITES] and not bad: self.LogDebug('No tunnels configured, idling...') self.conns.SetIdle(self, 60) else: self.LogDebug('No tunnels configured, closing connection.') self.Die() return True def QuotaCallback(self, conns, results, log_info): # Report new values to the back-end... unless they are mobile. if self.quota and (self.quota[0] >= 0): if not self.server_info[self.S_IS_MOBILE]: self.SendQuota() self.ProcessAuthResults(results, duplicates_ok=True, add_tunnels=False) for r in results: if r[0] in ('X-PageKite-OK', 'X-PageKite-Duplicate'): return self # Nothing is OK anymore, give up and shut down the tunnel. self.Log(log_info) self.LogInfo('Ran out of quota or account deleted, closing tunnel.') self.Die() return self def AuthCallback(self, conn, results, log_info): if log_info: logging.Log(log_info) output = [HTTP_ResponseHeader(200, 'OK'), HTTP_Header('Transfer-Encoding', 'chunked'), HTTP_Header('X-PageKite-Features', 'AddKites'), HTTP_Header('X-PageKite-Protos', ', '.join(['%s' % p for p in self.conns.config.server_protos])), HTTP_Header('X-PageKite-Ports', ', '.join( ['%s' % self.conns.config.server_portalias.get(p, p) for p in self.conns.config.server_ports]))] if not self.conns.config.disable_zchunks: output.append(HTTP_Header('X-PageKite-Features', 'ZChunks')) if self.conns.config.server_raw_ports: output.append( HTTP_Header('X-PageKite-Raw-Ports', ', '.join(['%s' % p for p in self.conns.config.server_raw_ports]))) for r in results: output.append('%s: %s\r\n' % r) output.append(HTTP_StartBody()) if not self.Send(output, activity=False, just_buffer=True): conn.LogDebug('No tunnels configured, closing connection (send failed).') self.Die(discard_buffer=True) return self if conn.quota and conn.quota[0]: self.quota = conn.quota self.Log([('BE-Quota', self.quota[0])]) if self.ProcessAuthResults(results): self.conns.Add(self) else: self.Die() return self def ChunkAuthCallback(self, results, log_info): if log_info: logging.Log(log_info) if self.ProcessAuthResults(results): output = ['NOOP: 1\r\n'] for r in results: output.append('%s: %s\r\n' % r) output.append('\r\n!') self.SendChunked(''.join(output), compress=False, just_buffer=True) def _RecvHttpHeaders(self, fd=None): data = '' fd = fd or self.fd while not data.endswith('\r\n\r\n') and not data.endswith('\n\n'): try: buf = fd.recv(1) except: # This is sloppy, but the back-end will just connect somewhere else # instead, so laziness here should be fine. buf = None if buf is None or buf == '': self.LogDebug('Remote end closed connection.') return None data += buf self.read_bytes += len(buf) if logging.DEBUG_IO: print '<== IN (headers) =[%s]==(\n%s)==' % (self, data) return data def _Connect(self, server, conns, tokens=None): if self.fd: self.fd.close() sspec = rsplit(':', server) if len(sspec) < 2: sspec = (sspec[0], 443) # Use chained SocksiPy to secure our communication. socks.DEBUG = (logging.DEBUG_IO or socks.DEBUG) and logging.LogDebug sock = socks.socksocket() if socks.HAVE_SSL: chain = ['default'] if self.conns.config.fe_anon_tls_wrap: chain.append('ssl-anon!%s!%s' % (sspec[0], sspec[1])) if self.conns.config.fe_certname: chain.append('http!%s!%s' % (sspec[0], sspec[1])) chain.append('ssl!%s!443' % ','.join(self.conns.config.fe_certname)) for hop in chain: sock.addproxy(*socks.parseproxy(hop)) self.SetFD(sock) try: self.fd.settimeout(20.0) # Missing in Python 2.2 except: self.fd.setblocking(1) self.LogDebug('Connecting to %s:%s' % (sspec[0], sspec[1])) self.fd.connect((sspec[0], int(sspec[1]))) replace_sessionid = self.conns.config.servers_sessionids.get(server, None) if (not self.Send(HTTP_PageKiteRequest(server, conns.config.backends, tokens, nozchunks=conns.config.disable_zchunks, replace=replace_sessionid), activity=False, try_flush=True, allow_blocking=False) or not self.Flush(wait=True, allow_blocking=False)): self.LogDebug('Failed to send kite request, closing.') return None, None data = self._RecvHttpHeaders() if not data: self.LogDebug('Failed to parse kite response, closing.') return None, None self.fd.setblocking(0) parse = HttpLineParser(lines=data.splitlines(), state=HttpLineParser.IN_RESPONSE) return data, parse def CheckForTokens(self, parse): tcount = 0 tokens = {} for request in parse.Header('X-PageKite-SignThis'): proto, domain, srand, token = request.split(':') tokens['%s:%s' % (proto, domain)] = token tcount += 1 return tcount, tokens def ParsePageKiteCapabilities(self, parse): for portlist in parse.Header('X-PageKite-Ports'): self.server_info[self.S_PORTS].extend(portlist.split(', ')) for portlist in parse.Header('X-PageKite-Raw-Ports'): self.server_info[self.S_RAW_PORTS].extend(portlist.split(', ')) for protolist in parse.Header('X-PageKite-Protos'): self.server_info[self.S_PROTOS].extend(protolist.split(', ')) if not self.conns.config.disable_zchunks: for feature in parse.Header('X-PageKite-Features'): if feature == 'ZChunks': self.EnableZChunks(level=9) elif feature == 'AddKites': self.server_info[self.S_ADD_KITES] = True elif feature == 'Mobile': self.server_info[self.S_IS_MOBILE] = True def HandlePageKiteResponse(self, parse): config = self.conns.config have_kites = 0 have_kite_info = None sname = self.server_info[self.S_NAME] config.ui.NotifyServer(self, self.server_info) for misc in parse.Header('X-PageKite-Misc'): args = parse_qs(misc) logdata = [('FE', sname)] for arg in args: logdata.append((arg, args[arg][0])) logging.Log(logdata) if 'motd' in args and args['motd'][0]: config.ui.NotifyMOTD(sname, args['motd'][0]) # FIXME: Really, we should keep track of quota dimensions for # each kite. At the moment that isn't even reported... for quota in parse.Header('X-PageKite-Quota'): self.quota = [float(quota), None, None] self.Log([('FE', sname), ('quota', quota)]) for quota in parse.Header('X-PageKite-QConns'): self.q_conns = float(quota) self.Log([('FE', sname), ('q_conns', quota)]) for quota in parse.Header('X-PageKite-QDays'): self.q_days = float(quota) self.Log([('FE', sname), ('q_days', quota)]) if self.quota and self.quota[0] is not None: config.ui.NotifyQuota(self.quota[0], self.q_days, self.q_conns) invalid_reasons = {} for request in parse.Header('X-PageKite-Invalid-Why'): # This is future-compatible, in that we can add more fields later. details = request.split(';') invalid_reasons[details[0]] = details[1] for request in parse.Header('X-PageKite-Invalid'): have_kite_info = True proto, domain, srand = request.split(':') reason = invalid_reasons.get(request, 'unknown') self.Log([('FE', sname), ('err', 'Rejected'), ('proto', proto), ('reason', reason), ('domain', domain)]) config.ui.NotifyKiteRejected(proto, domain, reason, crit=True) config.SetBackendStatus(domain, proto, add=BE_STATUS_ERR_TUNNEL) for request in parse.Header('X-PageKite-Duplicate'): have_kite_info = True proto, domain, srand = request.split(':') self.Log([('FE', self.server_info[self.S_NAME]), ('err', 'Duplicate'), ('proto', proto), ('domain', domain)]) config.ui.NotifyKiteRejected(proto, domain, 'duplicate') config.SetBackendStatus(domain, proto, add=BE_STATUS_ERR_TUNNEL) ssl_available = {} for request in parse.Header('X-PageKite-SSL-OK'): ssl_available[request] = True for request in parse.Header('X-PageKite-OK'): have_kite_info = True have_kites += 1 proto, domain, srand = request.split(':') self.conns.Tunnel(proto, domain, self) status = BE_STATUS_OK if request in ssl_available: status |= BE_STATUS_REMOTE_SSL self.remote_ssl[(proto, domain)] = True self.Log([('FE', sname), ('proto', proto), ('domain', domain), ('ssl', (request in ssl_available))]) config.SetBackendStatus(domain, proto, add=status) return have_kite_info and have_kites def _BackEnd(server, backends, require_all, conns): """This is the back-end end of a tunnel.""" self = Tunnel(conns) self.backends = backends self.require_all = require_all self.server_info[self.S_NAME] = server abort = True try: try: data, parse = self._Connect(server, conns) except: logging.LogError('Error in connect: %s' % format_exc()) raise if data and parse: # Collect info about front-end capabilities, for interactive config self.ParsePageKiteCapabilities(parse) for sessionid in parse.Header('X-PageKite-SessionID'): conns.SetAltId(self, sessionid) conns.config.servers_sessionids[server] = sessionid tryagain, tokens = self.CheckForTokens(parse) if tryagain: if self.server_info[self.S_ADD_KITES]: request = PageKiteRequestHeaders(server, conns.config.backends, tokens) abort = not self.SendChunked(('NOOP: 1\r\n%s\r\n\r\n!' ) % ''.join(request), compress=False, just_buffer=True) data = parse = None else: try: data, parse = self._Connect(server, conns, tokens) except: logging.LogError('Error in connect: %s' % format_exc()) raise if data and parse: kites = self.HandlePageKiteResponse(parse) abort = (kites is None) or (kites < 1) except socket.error: self.Cleanup() return None except Exception, e: self.LogError('Server response parsing failed: %s' % e) self.Cleanup() return None if abort: return None conns.Add(self) self.CountAs('frontends_live') self.last_activity = time.time() return self FrontEnd = staticmethod(_FrontEnd) BackEnd = staticmethod(_BackEnd) def Send(self, data, try_flush=False, activity=False, just_buffer=False, allow_blocking=True): try: if TUNNEL_SOCKET_BLOCKS and allow_blocking and not just_buffer: if self.fd is not None: self.fd.setblocking(1) return ChunkParser.Send(self, data, try_flush=try_flush, activity=activity, just_buffer=just_buffer, allow_blocking=allow_blocking) finally: if TUNNEL_SOCKET_BLOCKS and allow_blocking and not just_buffer: if self.fd is not None: self.fd.setblocking(0) def SendData(self, conn, data, sid=None, host=None, proto=None, port=None, chunk_headers=None): sid = int(sid or conn.sid) if conn: self.users[sid] = conn if not sid in self.zhistory: self.zhistory[sid] = [0, 0] # Pass outgoing data through any defined filters for f in self.filters: try: data = f.filter_data_out(self, sid, data) except: logging.LogError(('Ignoring error in filter_out %s: %s' ) % (f, format_exc())) sending = ['SID: %s\r\n' % sid] if proto: sending.append('Proto: %s\r\n' % proto) if host: sending.append('Host: %s\r\n' % host) if port: porti = int(port) if self.conns and (porti in self.conns.config.server_portalias): sending.append('Port: %s\r\n' % self.conns.config.server_portalias[porti]) else: sending.append('Port: %s\r\n' % port) if chunk_headers: for ch in chunk_headers: sending.append('%s: %s\r\n' % ch) sending.append('\r\n') sending.append(data) return self.SendChunked(sending, zhistory=self.zhistory[sid]) def SendStreamEof(self, sid, write_eof=False, read_eof=False): return self.SendChunked('SID: %s\r\nEOF: 1%s%s\r\n\r\nBye!' % (sid, (write_eof or not read_eof) and 'W' or '', (read_eof or not write_eof) and 'R' or ''), compress=False) def EofStream(self, sid, eof_type='WR'): if sid in self.users and self.users[sid] is not None: write_eof = (-1 != eof_type.find('W')) read_eof = (-1 != eof_type.find('R')) self.users[sid].ProcessTunnelEof(read_eof=(read_eof or not write_eof), write_eof=(write_eof or not read_eof)) def CloseStream(self, sid, stream_closed=False): if sid in self.users: stream = self.users[sid] del self.users[sid] if not stream_closed and stream is not None: stream.CloseTunnel(tunnel_closed=True) if sid in self.zhistory: del self.zhistory[sid] def ResetRemoteZChunks(self): return self.SendChunked('NOOP: 1\r\nZRST: 1\r\n\r\n!', compress=False, just_buffer=True) def TriggerPing(self): when = time.time() - PING_GRACE_MIN - PING_INTERVAL_MAX self.last_ping = self.last_activity = when def SendPing(self): now = time.time() self.last_ping = int(now) self.LogDebug("Ping", [('host', self.server_info[self.S_NAME])]) return self.SendChunked('NOOP: 1\r\nPING: %.3f\r\n\r\n!' % now, compress=False, just_buffer=True) def ProcessPong(self, pong): try: rtt = int(1000*(time.time()-float(pong))) if self.weighted_rtt < 0: self.weighted_rtt = rtt else: self.weighted_rtt = (self.weighted_rtt + rtt)/2 self.Log([('host', self.server_info[self.S_NAME]), ('rtt', '%d' % rtt), ('wrtt', '%d' % self.weighted_rtt)]) if common.gYamon: common.gYamon.ladd('tunnel_rtt', rtt) common.gYamon.ladd('tunnel_wrtt', self.weighted_rtt) except ValueError: pass def SendPong(self, data): if (self.conns.config.isfrontend and self.quota and (self.quota[0] >= 0)): # May as well make ourselves useful! return self.SendQuota(pong=data[:64]) else: return self.SendChunked('NOOP: 1\r\nPONG: %s\r\n\r\n!' % data[:64], compress=False, just_buffer=True) def SendQuota(self, pong=''): if pong: pong = 'PONG: %s\r\n' % pong if self.q_days is not None: return self.SendChunked(('NOOP: 1\r\n%sQuota: %s\r\nQDays: %s\r\nQConns: %s\r\n\r\n!' ) % (pong, self.quota[0], self.q_days, self.q_conns), compress=False, just_buffer=True) else: return self.SendChunked(('NOOP: 1\r\n%sQuota: %s\r\n\r\n!' ) % (pong, self.quota[0]), compress=False, just_buffer=True) def SendProgress(self, sid, conn, throttle=False): # FIXME: Optimize this away unless meaningful progress has been made? msg = ('NOOP: 1\r\n' 'SID: %s\r\n' 'SKB: %d\r\n') % (sid, (conn.all_out + conn.wrote_bytes)/1024) throttle = throttle and ('SPD: %d\r\n' % conn.write_speed) or '' return self.SendChunked('%s%s\r\n!' % (msg, throttle), compress=False, just_buffer=True) def ProcessCorruptChunk(self, data): self.ResetRemoteZChunks() return True def Probe(self, host): for bid in self.conns.config.backends: be = self.conns.config.backends[bid] if be[BE_DOMAIN] == host: bhost, bport = (be[BE_BHOST], be[BE_BPORT]) # FIXME: Should vary probe by backend type if self.conns.config.Ping(bhost, int(bport)) > 2: return False return True def AutoThrottle(self, max_speed=None, remote=False, delay=0.2): # Never throttle tunnels. return True def ProgressTo(self, parse): try: sid = int(parse.Header('SID')[0]) bps = int((parse.Header('SPD') or [-1])[0]) skb = int((parse.Header('SKB') or [-1])[0]) if sid in self.users: self.users[sid].RecordProgress(skb, bps) except: logging.LogError(('Tunnel::ProgressTo: That made no sense! %s' ) % format_exc()) return True # If a tunnel goes down, we just go down hard and kill all our connections. def ProcessEofRead(self): self.Die() return False def ProcessEofWrite(self): return self.ProcessEofRead() def ProcessChunkQuotaInfo(self, parse): new_quota = 0 if parse.Header('QDays'): self.q_days = new_quota = int(parse.Header('QDays')) if parse.Header('QConns'): self.q_conns = new_quota = int(parse.Header('QConns')) if parse.Header('Quota'): new_quota = 1 if self.quota: self.quota[0] = int(parse.Header('Quota')[0]) else: self.quota = [int(parse.Header('Quota')[0]), None, None] if new_quota: self.conns.config.ui.NotifyQuota(self.quota[0], self.q_days, self.q_conns) def ProcessChunkDirectives(self, parse): if parse.Header('PONG'): self.ProcessPong(parse.Header('PONG')[0]) if parse.Header('PING'): return self.SendPong(parse.Header('PING')[0]) if parse.Header('ZRST') and not self.ResetZChunks(): return False if parse.Header('SPD') or parse.Header('SKB'): if not self.ProgressTo(parse): return False if parse.Header('NOOP'): return True return None def FilterIncoming(self, sid, data=None, info=None): """Pass incoming data through filters, if we have any.""" for f in self.filters: try: if sid and info: f.filter_set_sid(sid, info) if data is not None: data = f.filter_data_in(self, sid, data) except: logging.LogError(('Ignoring error in filter_in %s: %s' ) % (f, format_exc())) return data def GetChunkDestination(self, parse): return ((parse.Header('Proto') or [''])[0].lower(), (parse.Header('Port') or [''])[0].lower(), (parse.Header('Host') or [''])[0].lower(), (parse.Header('RIP') or [''])[0].lower(), (parse.Header('RPort') or [''])[0].lower(), (parse.Header('RTLS') or [''])[0].lower()) def ReplyToProbe(self, proto, sid, host): if self.conns.config.no_probes: what, reply = 'rejected', HTTP_NoFeConnection(proto) elif self.Probe(host): what, reply = 'good', HTTP_GoodBeConnection(proto) else: what, reply = 'back-end down', HTTP_NoBeConnection(proto) self.LogDebug('Responding to probe for %s: %s' % (host, what)) return self.SendChunked('SID: %s\r\n\r\n%s' % (sid, reply)) def ConnectBE(self, sid, proto, port, host, rIp, rPort, rTLS, data): conn = UserConn.BackEnd(proto, host, sid, self, port, remote_ip=rIp, remote_port=rPort, data=data) if self.filters: if conn: rewritehost = conn.config.get('rewritehost') if rewritehost is True: rewritehost = conn.backend[BE_BHOST] else: rewritehost = False data = self.FilterIncoming(sid, data, info={ 'proto': proto, 'port': port, 'host': host, 'remote_ip': rIp, 'remote_port': rPort, 'using_tls': rTLS, 'be_host': conn and conn.backend[BE_BHOST], 'be_port': conn and conn.backend[BE_BPORT], 'trusted': conn and (conn.security or conn.config.get('insecure', False)), 'rawheaders': conn and conn.config.get('rawheaders', False), 'rewritehost': rewritehost }) if proto in ('http', 'http2', 'http3', 'websocket'): if conn and data.startswith(HttpSecurityFilter.REJECT): # Pretend we need authentication for dangerous URLs conn.Die() conn, data, code = False, '', 500 else: code = (conn is None) and 503 or 401 if not conn: # conn is None means we have no back-end. # conn is False means authentication is required. if not self.SendChunked('SID: %s\r\n\r\n%s' % (sid, HTTP_Unavailable('be', proto, host, frame_url=self.conns.config.error_url, code=code )), just_buffer=True): return False, False else: conn = None elif conn and proto == 'httpfinger': # Rewrite a finger request to HTTP. try: firstline, rest = data.split('\n', 1) if conn.config.get('rewritehost', False): rewritehost = conn.backend[BE_BHOST] else: rewritehost = host if '%s' in self.conns.config.finger_path: args = (firstline.strip(), rIp, rewritehost, rest) else: args = (rIp, rewritehost, rest) data = ('GET '+self.conns.config.finger_path+' HTTP/1.1\r\n' 'X-Forwarded-For: %s\r\n' 'Connection: close\r\n' 'Host: %s\r\n\r\n%s') % args except Exception, e: self.LogError('Error formatting HTTP-Finger: %s' % e) conn.Die() conn = None elif not conn and proto == 'https': if not self.SendChunked('SID: %s\r\n\r\n%s' % (sid, TLS_Unavailable(unavailable=True)), just_buffer=True): return False, False if conn: self.users[sid] = conn if proto == 'httpfinger': conn.fd.setblocking(1) conn.Send(data, try_flush=True, allow_blocking=False) or conn.Flush(wait=True, allow_blocking=False) self._RecvHttpHeaders(fd=conn.fd) conn.fd.setblocking(0) data = '' return conn, data def ProcessKiteUpdates(self, parse): # Look for requests for new tunnels if self.conns.config.isfrontend: requests = self.GetKiteRequests(parse) if requests: self.conns.auth().check(requests[:], self, lambda r, l: self.ChunkAuthCallback(r, l)) # Look for responses to requests for new tunnels tryagain, tokens = self.CheckForTokens(parse) if tryagain: server = self.server_info[self.S_NAME] backends = { } for bid in tokens: backends[bid] = self.conns.config.backends[bid] request = '\r\n'.join(PageKiteRequestHeaders(server, backends, tokens)) self.SendChunked('NOOP: 1\r\n%s\r\n\r\n!' % request, compress=False, just_buffer=True) kites = self.HandlePageKiteResponse(parse) if (kites is not None) and (kites < 1): self.Die() def ProcessChunk(self, data): # First, we process the chunk headers. try: headers, data = data.split('\r\n\r\n', 1) parse = HttpLineParser(lines=headers.splitlines(), state=HttpLineParser.IN_HEADERS) # Process PING/NOOP/etc: may result in a short-circuit. rv = self.ProcessChunkDirectives(parse) if rv is not None: # Update quota and kite information if necessary: this data is # always sent along with a NOOP, so checking for it here is safe. self.ProcessChunkQuotaInfo(parse) self.ProcessKiteUpdates(parse) return rv sid = int(parse.Header('SID')[0]) eof = parse.Header('EOF') except: logging.LogError(('Tunnel::ProcessChunk: Corrupt chunk: %s' ) % format_exc()) return False # EOF stream? if eof: self.EofStream(sid, eof[0]) return True # Headers done, not EOF: let's get the other end of this connection. if sid in self.users: # Either from pre-existing connections... conn = self.users[sid] if self.filters: data = self.FilterIncoming(sid, data) else: # ... or we connect to a back-end. proto, port, host, rIp, rPort, rTLS = self.GetChunkDestination(parse) if proto and host: # Probe requests are handled differently (short circuit) if proto.startswith('probe'): return self.ReplyToProbe(proto, sid, host) conn, data = self.ConnectBE(sid, proto, port, host, rIp, rPort, rTLS, data) if conn is False: return False else: conn = None # Send the data or shut down. if conn: if data and not conn.Send(data, try_flush=True): # If that failed something is wrong, but we'll let the outer # select/epoll loop catch and handle it. pass if len(conn.write_blocked) > 0 and conn.created < time.time()-3: return self.SendProgress(sid, conn, throttle=True) else: # No connection? Close this stream. self.CloseStream(sid) return self.SendStreamEof(sid) and self.Flush() return True class LoopbackTunnel(Tunnel): """A Tunnel which just loops back to this process.""" def __init__(self, conns, which, backends): Tunnel.__init__(self, conns) if self.fd: self.fd = None self.weighted_rtt = -1000 self.lock = None self.backends = backends self.require_all = True self.server_info[self.S_NAME] = LOOPBACK[which] self.other_end = None self.which = which self.buffer_count = 0 self.CountAs('loopbacks_live') if which == 'FE': for d in backends.keys(): if backends[d][BE_BHOST]: proto, domain = d.split(':') self.conns.Tunnel(proto, domain, self) self.Log([('FE', self.server_info[self.S_NAME]), ('proto', proto), ('domain', domain)]) def __str__(self): return '%s %s' % (Tunnel.__str__(self), self.which) def Cleanup(self, close=True): Tunnel.Cleanup(self, close=close) other = self.other_end self.other_end = None if other and other.other_end: other.Cleanup(close=close) def Linkup(self, other): """Links two LoopbackTunnels together.""" self.other_end = other other.other_end = self return other def _Loop(conns, backends): """Creates a loop, returning the back-end tunnel object.""" return LoopbackTunnel(conns, 'FE', backends ).Linkup(LoopbackTunnel(conns, 'BE', backends)) Loop = staticmethod(_Loop) # FIXME: This is a zero-length tunnel, but the code relies in some places # on the tunnel having a length. We really need a pipe here, or # things will go horribly wrong now and then. For now we hack this by # separating Write and Flush and looping back only on Flush. def Send(self, data, try_flush=False, activity=False, just_buffer=True, allow_blocking=True): if self.write_blocked: data = [self.write_blocked] + data self.write_blocked = '' joined_data = ''.join(data) if try_flush or (len(joined_data) > 10240) or (self.buffer_count >= 100): if logging.DEBUG_IO: print '|%s| %s \n|%s| %s' % (self.which, self, self.which, data) self.buffer_count = 0 return self.other_end.ProcessData(joined_data) else: self.buffer_count += 1 self.write_blocked = joined_data return True class UserConn(Selectable): """A Selectable representing a user's connection.""" def __init__(self, address, ui=None): Selectable.__init__(self, address=address, ui=ui) self.Reset() def Reset(self): self.tunnel = None self.conns = None self.backend = BE_NONE[:] self.config = {} self.security = None def Cleanup(self, close=True): if close: self.CloseTunnel() Selectable.Cleanup(self, close=close) self.Reset() def ConnType(self): if self.backend[BE_BHOST]: return 'BE=%s:%s' % (self.backend[BE_BHOST], self.backend[BE_BPORT]) else: return 'FE' def __str__(self): return '%s %s' % (Selectable.__str__(self), self.ConnType()) def __html__(self): return ('Tunnel: %s
    ' '%s') % (self.tunnel and self.tunnel.sid or '', escape_html('%s' % (self.tunnel or '')), Selectable.__html__(self)) def IsReadable(self, now): if self.tunnel and self.tunnel.IsBlocked(): return False return Selectable.IsReadable(self, now) def CloseTunnel(self, tunnel_closed=False): tunnel, self.tunnel = self.tunnel, None if tunnel and not tunnel_closed: tunnel.SendStreamEof(self.sid, write_eof=True, read_eof=True) tunnel.CloseStream(self.sid, stream_closed=True) self.ProcessTunnelEof(read_eof=True, write_eof=True) def _FrontEnd(conn, address, proto, host, on_port, body, conns): # This is when an external user connects to a server and requests a # web-page. We have to give it to them! self = UserConn(address, ui=conns.config.ui) self.conns = conns self.SetConn(conn) if ':' in host: host, port = host.split(':', 1) self.proto = proto self.host = host # If the listening port is an alias for another... if int(on_port) in conns.config.server_portalias: on_port = conns.config.server_portalias[int(on_port)] # Try and find the right tunnel. We prefer proto/port specifications first, # then the just the proto. If the protocol is WebSocket and no tunnel is # found, look for a plain HTTP tunnel. if proto.startswith('probe'): protos = ['http', 'https', 'websocket', 'raw', 'irc', 'finger', 'httpfinger'] ports = conns.config.server_ports[:] ports.extend(conns.config.server_aliasport.keys()) ports.extend([x for x in conns.config.server_raw_ports if x != VIRTUAL_PN]) else: protos = [proto] ports = [on_port] if proto == 'websocket': protos.append('http') elif proto == 'http': protos.extend(['http2', 'http3']) tunnels = None for p in protos: for prt in ports: if not tunnels: tunnels = conns.Tunnel('%s-%s' % (p, prt), host) if not tunnels: tunnels = conns.Tunnel(p, host) if not tunnels: tunnels = conns.Tunnel(protos[0], CATCHALL_HN) if self.address: chunk_headers = [('RIP', self.address[0]), ('RPort', self.address[1])] if conn.my_tls: chunk_headers.append(('RTLS', 1)) if tunnels: if len(tunnels) > 1: tunnels.sort(key=lambda t: t.weighted_rtt) self.tunnel = tunnels[0] if (self.tunnel and self.tunnel.SendData(self, ''.join(body), host=host, proto=proto, port=on_port, chunk_headers=chunk_headers) and self.conns): self.Log([('domain', self.host), ('on_port', on_port), ('proto', self.proto), ('is', 'FE')]) self.conns.Add(self) if proto.startswith('http'): self.conns.TrackIP(address[0], host) # FIXME: Use the tracked data to detect & mitigate abuse? return self else: self.LogDebug('No back-end', [('on_port', on_port), ('proto', self.proto), ('domain', self.host), ('is', 'FE')]) self.Cleanup(close=False) return None def _BackEnd(proto, host, sid, tunnel, on_port, remote_ip=None, remote_port=None, data=None): # This is when we open a backend connection, because a user asked for it. self = UserConn(None, ui=tunnel.conns.config.ui) self.sid = sid self.proto = proto self.host = host self.conns = tunnel.conns self.tunnel = tunnel failure = None # Try and find the right back-end. We prefer proto/port specifications # first, then the just the proto. If the protocol is WebSocket and no # tunnel is found, look for a plain HTTP tunnel. Fallback hosts can # be registered using the http2/3/4 protocols. backend = None if proto == 'http': protos = [proto, 'http2', 'http3'] elif proto.startswith('probe'): protos = ['http', 'http2', 'http3'] elif proto == 'websocket': protos = [proto, 'http', 'http2', 'http3'] else: protos = [proto] for p in protos: if not backend: p_p = '%s-%s' % (p, on_port) backend, be = self.conns.config.GetBackendServer(p_p, host) if not backend: backend, be = self.conns.config.GetBackendServer(p, host) if not backend: backend, be = self.conns.config.GetBackendServer(p, CATCHALL_HN) if backend: break logInfo = [ ('on_port', on_port), ('proto', proto), ('domain', host), ('is', 'BE') ] if remote_ip: logInfo.append(('remote_ip', remote_ip)) # Strip off useless IPv6 prefix, if this is an IPv4 address. if remote_ip.startswith('::ffff:') and ':' not in remote_ip[7:]: remote_ip = remote_ip[7:] if not backend or not backend[0]: self.ui.Notify(('%s - %s://%s:%s (FAIL: no server)' ) % (remote_ip or 'unknown', proto, host, on_port), prefix='?', color=self.ui.YELLOW) else: http_host = '%s/%s' % (be[BE_DOMAIN], be[BE_PORT] or '80') self.backend = be self.config = host_config = self.conns.config.be_config.get(http_host, {}) # Access control interception: check remote IP addresses first. ip_keys = [k for k in host_config if k.startswith('ip/')] if ip_keys: k1 = 'ip/%s' % remote_ip k2 = '.'.join(k1.split('.')[:-1]) if not (k1 in host_config or k2 in host_config): self.ui.Notify(('%s - %s://%s:%s (IP ACCESS DENIED)' ) % (remote_ip or 'unknown', proto, host, on_port), prefix='!', color=self.ui.YELLOW) logInfo.append(('forbidden-ip', '%s' % remote_ip)) backend = None else: self.security = 'ip' # Access control interception: check for HTTP Basic authentication. user_keys = [k for k in host_config if k.startswith('password/')] if user_keys: user, pwd, fail = None, None, True if proto in ('websocket', 'http', 'http2', 'http3'): parse = HttpLineParser(lines=data.splitlines()) auth = parse.Header('Authorization') try: (how, ab64) = auth[0].strip().split() if how.lower() == 'basic': user, pwd = base64.decodestring(ab64).split(':') except: user = auth user_key = 'password/%s' % user if user and user_key in host_config: if host_config[user_key] == pwd: fail = False if fail: if logging.DEBUG_IO: print '=== REQUEST\n%s\n===' % data self.ui.Notify(('%s - %s://%s:%s (USER ACCESS DENIED)' ) % (remote_ip or 'unknown', proto, host, on_port), prefix='!', color=self.ui.YELLOW) logInfo.append(('forbidden-user', '%s' % user)) backend = None failure = '' else: self.security = 'password' if not backend: logInfo.append(('err', 'No back-end')) self.Log(logInfo) self.Cleanup(close=False) return failure try: self.SetFD(rawsocket(socket.AF_INET, socket.SOCK_STREAM)) try: self.fd.settimeout(2.0) # Missing in Python 2.2 except: self.fd.setblocking(1) sspec = list(backend) if len(sspec) == 1: sspec.append(80) self.fd.connect(tuple(sspec)) self.fd.setblocking(0) except socket.error, err: logInfo.append(('socket_error', '%s' % err)) self.ui.Notify(('%s - %s://%s:%s (FAIL: %s:%s is down)' ) % (remote_ip or 'unknown', proto, host, on_port, sspec[0], sspec[1]), prefix='!', color=self.ui.YELLOW) self.Log(logInfo) self.Cleanup(close=False) return None sspec = (sspec[0], sspec[1]) be_name = (sspec == self.conns.config.ui_sspec) and 'builtin' or ('%s:%s' % sspec) self.ui.Status('serving') self.ui.Notify(('%s < %s://%s:%s (%s)' ) % (remote_ip or 'unknown', proto, host, on_port, be_name)) self.Log(logInfo) self.conns.Add(self) return self FrontEnd = staticmethod(_FrontEnd) BackEnd = staticmethod(_BackEnd) def Shutdown(self, direction): try: if self.fd: if 'sock_shutdown' in dir(self.fd): # This is a pyOpenSSL socket, which has incompatible shutdown. if direction == socket.SHUT_RD: self.fd.shutdown() else: self.fd.sock_shutdown(direction) else: self.fd.shutdown(direction) except Exception, e: pass def ProcessTunnelEof(self, read_eof=False, write_eof=False): rv = True if write_eof and not self.read_eof: rv = self.ProcessEofRead(tell_tunnel=False) and rv if read_eof and not self.write_eof: rv = self.ProcessEofWrite(tell_tunnel=False) and rv return rv def ProcessEofRead(self, tell_tunnel=True): self.read_eof = True self.Shutdown(socket.SHUT_RD) if tell_tunnel and self.tunnel: self.tunnel.SendStreamEof(self.sid, read_eof=True) return self.ProcessEof() def ProcessEofWrite(self, tell_tunnel=True): self.write_eof = True if not self.write_blocked: self.Shutdown(socket.SHUT_WR) if tell_tunnel and self.tunnel: self.tunnel.SendStreamEof(self.sid, write_eof=True) if (self.conns and self.ConnType() == 'FE' and (not self.read_eof)): self.conns.SetIdle(self, 120) return self.ProcessEof() def Send(self, data, try_flush=False, activity=True, just_buffer=False, allow_blocking=True): rv = Selectable.Send(self, data, try_flush=try_flush, activity=activity, just_buffer=just_buffer, allow_blocking=allow_blocking) if self.write_eof and not self.write_blocked: self.Shutdown(socket.SHUT_WR) elif try_flush or not self.write_blocked: if self.tunnel: self.tunnel.SendProgress(self.sid, self) return rv def ProcessData(self, data): if not self.tunnel: self.LogError('No tunnel! %s' % self) return False if not self.tunnel.SendData(self, data): self.LogDebug('Send to tunnel failed') return False # Back off if tunnel is stuffed. if self.tunnel and len(self.tunnel.write_blocked) > 1024000: # FIXME: think about this... self.Throttle(delay=(len(self.tunnel.write_blocked)-204800)/max(50000, self.tunnel.write_speed)) if self.read_eof: return self.ProcessEofRead() return True class UnknownConn(MagicProtocolParser): """This class is a connection which we're not sure what is yet.""" def __init__(self, fd, address, on_port, conns): MagicProtocolParser.__init__(self, fd, address, on_port, ui=conns.config.ui) self.peeking = True # Set up our parser chain. self.parsers = [HttpLineParser] if IrcLineParser.PROTO in conns.config.server_protos: self.parsers.append(IrcLineParser) if FingerLineParser.PROTO in conns.config.server_protos: self.parsers.append(FingerLineParser) self.parser = MagicLineParser(parsers=self.parsers) self.conns = conns self.conns.Add(self) self.conns.SetIdle(self, 10) self.sid = -1 self.host = None self.proto = None self.said_hello = False self.bad_loops = 0 def Cleanup(self, close=True): MagicProtocolParser.Cleanup(self, close=close) self.conns = self.parser = None def SayHello(self): if self.said_hello: return False else: self.said_hello = True if self.on_port in (25, 125, 2525): # FIXME: We don't actually support SMTP yet and 125 is bogus. self.Send(['220 ready ESMTP PageKite Magic Proxy\n'], try_flush=True) return True def __str__(self): return '%s (%s/%s:%s)' % (MagicProtocolParser.__str__(self), (self.proto or '?'), (self.on_port or '?'), (self.host or '?')) # Any sort of EOF just means give up: if we haven't figured out what # kind of connnection this is yet, we won't without more data. def ProcessEofRead(self): self.Die(discard_buffer=True) return self.ProcessEof() def ProcessEofWrite(self): self.Die(discard_buffer=True) return self.ProcessEof() def ProcessLine(self, line, lines): if not self.parser: return True if self.parser.Parse(line) is False: return False if not self.parser.ParsedOK(): return True self.parser = self.parser.last_parser if self.parser.protocol == HttpLineParser.PROTO: # HTTP has special cases, including CONNECT etc. return self.ProcessParsedHttp(line, lines) else: return self.ProcessParsedMagic(self.parser.PROTOS, line, lines) def ProcessParsedMagic(self, protos, line, lines): if (self.conns and self.conns.config.CheckTunnelAcls(self.address, conn=self)): for proto in protos: if UserConn.FrontEnd(self, self.address, proto, self.parser.domain, self.on_port, self.parser.lines + lines, self.conns) is not None: self.Cleanup(close=False) return True self.Send([self.parser.ErrorReply(port=self.on_port)], try_flush=True) self.Cleanup() return False def ProcessParsedHttp(self, line, lines): done = False if self.parser.method == 'PING': self.Send('PONG %s\r\n\r\n' % self.parser.path) self.read_eof = self.write_eof = done = True self.fd.close() elif self.parser.method == 'CONNECT': if self.parser.path.lower().startswith('pagekite:'): if not self.conns.config.CheckTunnelAcls(self.address, conn=self): self.Send(HTTP_ConnectBad(code=403, status='Forbidden'), try_flush=True) return False if Tunnel.FrontEnd(self, lines, self.conns) is None: self.Send(HTTP_ConnectBad(), try_flush=True) return False done = True else: try: connect_parser = self.parser chost, cport = connect_parser.path.split(':', 1) cport = int(cport) chost = chost.lower() sid1 = ':%s' % chost sid2 = '-%s:%s' % (cport, chost) tunnels = self.conns.tunnels if not self.conns.config.CheckClientAcls(self.address, conn=self): self.Send(HTTP_Unavailable('fe', 'raw', chost, code=403, status='Forbidden', frame_url=self.conns.config.error_url), try_flush=True) return False # These allow explicit CONNECTs to direct http(s) or raw backends. # If no match is found, we throw an error. if cport in (80, 8080): if (('http'+sid1) in tunnels) or ( ('http'+sid2) in tunnels) or ( ('http2'+sid1) in tunnels) or ( ('http2'+sid2) in tunnels) or ( ('http3'+sid1) in tunnels) or ( ('http3'+sid2) in tunnels): (self.on_port, self.host) = (cport, chost) self.parser = HttpLineParser() self.Send(HTTP_ConnectOK(), try_flush=True) return True whost = chost if '.' in whost: whost = '*.' + '.'.join(whost.split('.')[1:]) if cport == 443: if (('https'+sid1) in tunnels) or ( ('https'+sid2) in tunnels) or ( chost in self.conns.config.tls_endpoints) or ( whost in self.conns.config.tls_endpoints): (self.on_port, self.host) = (cport, chost) self.parser = HttpLineParser() self.Send(HTTP_ConnectOK(), try_flush=True) return self.ProcessTls(''.join(lines), chost) if (cport in self.conns.config.server_raw_ports or VIRTUAL_PN in self.conns.config.server_raw_ports): for raw in ('raw', 'finger'): if ((raw+sid1) in tunnels) or ((raw+sid2) in tunnels): (self.on_port, self.host) = (cport, chost) self.parser = HttpLineParser() self.Send(HTTP_ConnectOK(), try_flush=True) return self.ProcessProto(''.join(lines), raw, self.host) self.Send(HTTP_ConnectBad(), try_flush=True) return False except ValueError: pass if (not done and self.parser.method == 'POST' and self.parser.path in MAGIC_PATHS): # FIXME: DEPRECATE: Make this go away! if not self.conns.config.CheckTunnelAcls(self.address, conn=self): self.Send(HTTP_ConnectBad(code=403, status='Forbidden'), try_flush=True) return False if Tunnel.FrontEnd(self, lines, self.conns) is None: self.Send(HTTP_ConnectBad(), try_flush=True) return False done = True if not done: if not self.host: hosts = self.parser.Header('Host') if hosts: self.host = hosts[0].lower() else: self.Send(HTTP_Response(400, 'Bad request', ['

    400 Bad request

    ', '

    Invalid request, no Host: found.

    ', '\n'], trackable=True)) return False if self.parser.path.startswith(MAGIC_PREFIX): try: self.host = self.parser.path.split('/')[2] if self.parser.path.endswith('.json'): self.proto = 'probe.json' else: self.proto = 'probe' except ValueError: pass if self.proto is None: self.proto = 'http' upgrade = self.parser.Header('Upgrade') if 'websocket' in self.conns.config.server_protos: if upgrade and upgrade[0].lower() == 'websocket': self.proto = 'websocket' if not self.conns.config.CheckClientAcls(self.address, conn=self): self.Send(HTTP_Unavailable('fe', self.proto, self.host, code=403, status='Forbidden', frame_url=self.conns.config.error_url), try_flush=True) return False address = self.address if int(self.on_port) in self.conns.config.server_portalias: xfwdf = self.parser.Header('X-Forwarded-For') if xfwdf and address[0] == '127.0.0.1': address = (xfwdf[0], address[1]) done = True if UserConn.FrontEnd(self, address, self.proto, self.host, self.on_port, self.parser.lines + lines, self.conns) is None: if self.proto.startswith('probe'): self.Send(HTTP_NoFeConnection(self.proto), try_flush=True) else: self.Send(HTTP_Unavailable('fe', self.proto, self.host, frame_url=self.conns.config.error_url), try_flush=True) return False # We are done! self.Cleanup(close=False) return True def ProcessTls(self, data, domain=None): if (not self.conns or not self.conns.config.CheckClientAcls(self.address, conn=self)): self.Send(TLS_Unavailable(forbidden=True), try_flush=True) return False if domain: domains = [domain] else: try: domains = self.GetSni(data) if not domains: domains = [self.conns.config.tls_default] if domains[0]: self.LogDebug('No SNI - trying: %s' % domains[0]) else: domains = None except: # Probably insufficient data, just True and assume we'll have # better luck on the next round... but with a timeout. self.bad_loops += 1 if self.bad_loops < 25: self.LogDebug('Error in ProcessTLS, will time out in 120 seconds.') self.conns.SetIdle(self, 120) return True else: self.LogDebug('Persistent error in ProcessTLS, aborting.') self.Send(TLS_Unavailable(unavailable=True), try_flush=True) return False if domains and domains[0] is not None: if UserConn.FrontEnd(self, self.address, 'https', domains[0], self.on_port, [data], self.conns) is not None: # We are done! self.EatPeeked() self.Cleanup(close=False) return True else: # If we know how to terminate the TLS/SSL, do so! ctx = self.conns.config.GetTlsEndpointCtx(domains[0]) if ctx: self.fd = socks.SSL_Connect(ctx, self.fd, accepted=True, server_side=True) self.peeking = False self.is_tls = False self.my_tls = True self.conns.SetIdle(self, 120) return True else: self.Send(TLS_Unavailable(unavailable=True), try_flush=True) return False self.Send(TLS_Unavailable(unavailable=True), try_flush=True) return False def ProcessFlashPolicyRequest(self, data): # FIXME: Should this be configurable? self.LogDebug('Sending friendly response to Flash Policy Request') self.Send('\n' ' \n' '\n', try_flush=True) return False def ProcessProto(self, data, proto, domain): if (not self.conns or not self.conns.config.CheckClientAcls(self.address, conn=self)): return False if UserConn.FrontEnd(self, self.address, proto, domain, self.on_port, [data], self.conns) is None: return False # We are done! self.Cleanup(close=False) return True class UiConn(LineParser): STATE_PASSWORD = 0 STATE_LIVE = 1 def __init__(self, fd, address, on_port, conns): LineParser.__init__(self, fd=fd, address=address, on_port=on_port) self.state = self.STATE_PASSWORD self.conns = conns self.conns.Add(self) self.lines = [] self.qc = threading.Condition() self.challenge = sha1hex('%s%8.8x' % (globalSecret(), random.randint(0, 0x7FFFFFFD)+1)) self.expect = signToken(token=self.challenge, secret=self.conns.config.ConfigSecret(), payload=self.challenge, length=1000) self.LogDebug('Expecting: %s' % self.expect) self.Send('PageKite? %s\r\n' % self.challenge) def readline(self): self.qc.acquire() while not self.lines: self.qc.wait() line = self.lines.pop(0) self.qc.release() return line def write(self, data): self.conns.config.ui_wfile.write(data) self.Send(data) def Cleanup(self): self.conns.config.ui.wfile = self.conns.config.ui_wfile self.conns.config.ui.rfile = self.conns.config.ui_rfile self.lines = self.conns.config.ui_conn = None self.conns = None LineParser.Cleanup(self) def Disconnect(self): self.Send('Goodbye') self.Cleanup() def ProcessLine(self, line, lines): if self.state == self.STATE_LIVE: self.qc.acquire() self.lines.append(line) self.qc.notify() self.qc.release() return True elif self.state == self.STATE_PASSWORD: if line.strip() == self.expect: if self.conns.config.ui_conn: self.conns.config.ui_conn.Disconnect() self.conns.config.ui_conn = self self.conns.config.ui.wfile = self self.conns.config.ui.rfile = self self.state = self.STATE_LIVE self.Send('OK!\r\n') return True else: self.Send('Sorry.\r\n') return False else: return False class RawConn(Selectable): """This class is a raw/timed connection.""" def __init__(self, fd, address, on_port, conns): Selectable.__init__(self, fd, address, on_port) self.my_tls = False self.is_tls = False domain = conns.LastIpDomain(address[0]) if domain and UserConn.FrontEnd(self, address, 'raw', domain, on_port, [], conns): self.Cleanup(close=False) else: self.Cleanup() class Listener(Selectable): """This class listens for incoming connections and accepts them.""" def __init__(self, host, port, conns, backlog=100, connclass=UnknownConn, quiet=False, acl=None): Selectable.__init__(self, bind=(host, port), backlog=backlog) self.Log([('listen', '%s:%s' % (host, port))]) if not quiet: conns.config.ui.Notify(' - Listening on %s:%s' % (host or '*', port)) self.acl = acl self.acl_match = None self.connclass = connclass self.port = port self.conns = conns self.conns.Add(self) self.CountAs('listeners_live') def __str__(self): return '%s port=%s' % (Selectable.__str__(self), self.port) def __html__(self): return '

    Listening on port %s for %s

    ' % (self.port, self.connclass) def check_acl(self, ipaddr, default=True): if self.acl: try: ipaddr = '%s' % ipaddr lc = 0 for line in open(self.acl, 'r'): line = line.lower().strip() lc += 1 if line.startswith('#') or not line: continue try: words = line.split() pattern, rule = words[:2] reason = ' '.join(words[2:]) if ipaddr == pattern: self.acl_match = (lc, pattern, rule, reason) return bool('allow' in rule) elif re.compile(pattern).match(ipaddr): self.acl_match = (lc, pattern, rule, reason) return bool('allow' in rule) except IndexError: self.LogDebug('Invalid line %d in ACL %s' % (lc, self.acl)) except: self.LogDebug('Failed to read/parse %s' % self.acl) self.acl_match = (0, '.*', default and 'allow' or 'reject', 'Default') return default def ReadData(self, maxread=None): try: self.last_activity = time.time() client, address = self.fd.accept() if client: if self.check_acl(address[0]): log_info = [('accept', '%s:%s' % (obfuIp(address[0]), address[1]))] uc = self.connclass(client, address, self.port, self.conns) else: log_info = [('reject', '%s:%s' % (obfuIp(address[0]), address[1]))] client.close() if self.acl: log_info.extend([('acl_line', '%s' % self.acl_match[0]), ('reason', self.acl_match[3])]) self.Log(log_info) return True except IOError, err: self.LogDebug('Listener::ReadData: error: %s (%s)' % (err, err.errno)) except socket.error, (errno, msg): self.LogInfo('Listener::ReadData: error: %s (errno=%s)' % (msg, errno)) except Exception, e: self.LogDebug('Listener::ReadData: %s' % e) return True pagekite-0.5.8a/debian/pagekite/usr/share/pyshared/pagekite/proto/parsers.py0000644000175000017500000001666312603542202026562 0ustar brebre00000000000000""" Protocol parsers for classifying incoming network connections. """ ############################################################################## LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2015, the Beanstalks Project ehf. and Bjarni Runar Einarsson This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see: """ ############################################################################## from pagekite.compat import * from pagekite.common import * import pagekite.logging as logging HTTP_METHODS = ['OPTIONS', 'CONNECT', 'GET', 'HEAD', 'POST', 'PUT', 'TRACE', 'PROPFIND', 'PROPPATCH', 'MKCOL', 'DELETE', 'COPY', 'MOVE', 'LOCK', 'UNLOCK', 'PING', 'PATCH'] HTTP_VERSIONS = ['HTTP/1.0', 'HTTP/1.1'] class BaseLineParser(object): """Base protocol parser class.""" PROTO = 'unknown' PROTOS = ['unknown'] PARSE_UNKNOWN = -2 PARSE_FAILED = -1 PARSE_OK = 100 def __init__(self, lines=None, state=PARSE_UNKNOWN, proto=PROTO): self.state = state self.protocol = proto self.lines = [] self.domain = None self.last_parser = self if lines is not None: for line in lines: if not self.Parse(line): break def ParsedOK(self): return (self.state == self.PARSE_OK) def Parse(self, line): self.lines.append(line) return False def ErrorReply(self, port=None): return '' class MagicLineParser(BaseLineParser): """Parse an unknown incoming connection request, line-by-line.""" PROTO = 'magic' def __init__(self, lines=None, state=BaseLineParser.PARSE_UNKNOWN, parsers=[]): self.parsers = [p() for p in parsers] BaseLineParser.__init__(self, lines, state, self.PROTO) if self.last_parser == self: self.last_parser = self.parsers[-1] def ParsedOK(self): return self.last_parser.ParsedOK() def Parse(self, line): BaseLineParser.Parse(self, line) self.last_parser = self.parsers[-1] for p in self.parsers[:]: if not p.Parse(line): self.parsers.remove(p) elif p.ParsedOK(): self.last_parser = p self.domain = p.domain self.protocol = p.protocol self.state = p.state self.parsers = [p] break if not self.parsers: logging.LogDebug('No more parsers!') return (len(self.parsers) > 0) class HttpLineParser(BaseLineParser): """Parse an HTTP request, line-by-line.""" PROTO = 'http' PROTOS = ['http'] IN_REQUEST = 11 IN_HEADERS = 12 IN_BODY = 13 IN_RESPONSE = 14 def __init__(self, lines=None, state=IN_REQUEST, testbody=False): self.method = None self.path = None self.version = None self.code = None self.message = None self.headers = [] self.body_result = testbody BaseLineParser.__init__(self, lines, state, self.PROTO) def ParseResponse(self, line): self.version, self.code, self.message = line.split() if not self.version.upper() in HTTP_VERSIONS: logging.LogDebug('Invalid version: %s' % self.version) return False self.state = self.IN_HEADERS return True def ParseRequest(self, line): self.method, self.path, self.version = line.split() if not self.method in HTTP_METHODS: logging.LogDebug('Invalid method: %s' % self.method) return False if not self.version.upper() in HTTP_VERSIONS: logging.LogDebug('Invalid version: %s' % self.version) return False self.state = self.IN_HEADERS return True def ParseHeader(self, line): if line in ('', '\r', '\n', '\r\n'): self.state = self.IN_BODY return True header, value = line.split(':', 1) if value and value.startswith(' '): value = value[1:] self.headers.append((header.lower(), value)) return True def ParseBody(self, line): # Could be overridden by subclasses, for now we just play dumb. return self.body_result def ParsedOK(self): return (self.state == self.IN_BODY) def Parse(self, line): BaseLineParser.Parse(self, line) try: if (self.state == self.IN_RESPONSE): return self.ParseResponse(line) elif (self.state == self.IN_REQUEST): return self.ParseRequest(line) elif (self.state == self.IN_HEADERS): return self.ParseHeader(line) elif (self.state == self.IN_BODY): return self.ParseBody(line) except ValueError, err: logging.LogDebug('Parse failed: %s, %s, %s' % (self.state, err, self.lines)) self.state = BaseLineParser.PARSE_FAILED return False def Header(self, header): return [h[1].strip() for h in self.headers if h[0] == header.lower()] class FingerLineParser(BaseLineParser): """Parse an incoming Finger request, line-by-line.""" PROTO = 'finger' PROTOS = ['finger', 'httpfinger'] WANT_FINGER = 71 def __init__(self, lines=None, state=WANT_FINGER): BaseLineParser.__init__(self, lines, state, self.PROTO) def ErrorReply(self, port=None): if port == 79: return ('PageKite wants to know, what domain?\n' 'Try: finger user+domain@domain\n') else: return '' def Parse(self, line): BaseLineParser.Parse(self, line) if ' ' in line: return False if '+' in line: arg0, self.domain = line.strip().split('+', 1) elif '@' in line: arg0, self.domain = line.strip().split('@', 1) if self.domain: self.state = BaseLineParser.PARSE_OK self.lines[-1] = '%s\n' % arg0 return True else: self.state = BaseLineParser.PARSE_FAILED return False class IrcLineParser(BaseLineParser): """Parse an incoming IRC connection, line-by-line.""" PROTO = 'irc' PROTOS = ['irc'] WANT_USER = 61 def __init__(self, lines=None, state=WANT_USER): self.seen = [] BaseLineParser.__init__(self, lines, state, self.PROTO) def ErrorReply(self): return ':pagekite 451 :IRC Gateway requires user@HOST or nick@HOST\n' def Parse(self, line): BaseLineParser.Parse(self, line) if line in ('\n', '\r\n'): return True if self.state == IrcLineParser.WANT_USER: try: ocmd, arg = line.strip().split(' ', 1) cmd = ocmd.lower() self.seen.append(cmd) args = arg.split(' ') if cmd == 'pass': pass elif cmd in ('user', 'nick'): if '@' in args[0]: parts = args[0].split('@') self.domain = parts[-1] arg0 = '@'.join(parts[:-1]) elif 'nick' in self.seen and 'user' in self.seen and not self.domain: raise Error('No domain found') if self.domain: self.state = BaseLineParser.PARSE_OK self.lines[-1] = '%s %s %s\n' % (ocmd, arg0, ' '.join(args[1:])) else: self.state = BaseLineParser.PARSE_FAILED except Exception, err: logging.LogDebug('Parse failed: %s, %s, %s' % (self.state, err, self.lines)) self.state = BaseLineParser.PARSE_FAILED return (self.state != BaseLineParser.PARSE_FAILED) pagekite-0.5.8a/debian/pagekite/usr/share/pyshared/pagekite/proto/proto.py0000644000175000017500000002527212603542202026242 0ustar brebre00000000000000""" PageKite protocol and HTTP protocol related code and constants. """ ############################################################################## LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2015, the Beanstalks Project ehf. and Bjarni Runar Einarsson This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see: """ ############################################################################## import base64 import os import random import struct import time from pagekite.compat import * from pagekite.common import * import pagekite.logging as logging gSecret = None def globalSecret(): global gSecret if not gSecret: # This always works... gSecret = '%8.8x%s%8.8x' % (random.randint(0, 0x7FFFFFFE), time.time(), random.randint(0, 0x7FFFFFFE)) # Next, see if we can augment that with some real randomness. try: newSecret = sha1hex(open('/dev/urandom').read(64) + gSecret) gSecret = newSecret logging.LogDebug('Seeded signatures using /dev/urandom, hooray!') except: try: newSecret = sha1hex(os.urandom(64) + gSecret) gSecret = newSecret logging.LogDebug('Seeded signatures using os.urandom(), hooray!') except: logging.LogInfo('WARNING: Seeding signatures with time.time() and random.randint()') return gSecret TOKEN_LENGTH=36 def signToken(token=None, secret=None, payload='', timestamp=None, length=TOKEN_LENGTH): """ This will generate a random token with a signature which could only have come from this server. If a token is provided, it is re-signed so the original can be compared with what we would have generated, for verification purposes. If a timestamp is provided it will be embedded in the signature to a resolution of 10 minutes, and the signature will begin with the letter 't' Note: This is only as secure as random.randint() is random. """ if not secret: secret = globalSecret() if not token: token = sha1hex('%s%8.8x' % (globalSecret(), random.randint(0, 0x7FFFFFFD)+1)) if timestamp: tok = 't' + token[1:] ts = '%x' % int(timestamp/600) return tok[0:8] + sha1hex(secret + payload + ts + tok[0:8])[0:length-8] else: return token[0:8] + sha1hex(secret + payload + token[0:8])[0:length-8] def checkSignature(sign='', secret='', payload=''): """ Check a signature for validity. When using timestamped signatures, we only accept signatures from the current and previous windows. """ if sign[0] == 't': ts = int(time.time()) for window in (0, 1): valid = signToken(token=sign, secret=secret, payload=payload, timestamp=(ts-(window*600))) if sign == valid: return True return False else: valid = signToken(token=sign, secret=secret, payload=payload) return sign == valid def PageKiteRequestHeaders(server, backends, tokens=None, testtoken=None): req = [] tokens = tokens or {} for d in backends.keys(): if (backends[d][BE_BHOST] and backends[d][BE_SECRET] and backends[d][BE_STATUS] not in BE_INACTIVE): # A stable (for replay on challenge) but unguessable salt. my_token = sha1hex(globalSecret() + server + backends[d][BE_SECRET] )[:TOKEN_LENGTH] # This is the challenge (salt) from the front-end, if any. server_token = d in tokens and tokens[d] or '' # Our payload is the (proto, name) combined with both salts data = '%s:%s:%s' % (d, my_token, server_token) # Sign the payload with the shared secret (random salt). sign = signToken(secret=backends[d][BE_SECRET], payload=data, token=testtoken) req.append('X-PageKite: %s:%s\r\n' % (data, sign)) return req def HTTP_PageKiteRequest(server, backends, tokens=None, nozchunks=False, tls=False, testtoken=None, replace=None): req = ['CONNECT PageKite:1 HTTP/1.0\r\n', 'X-PageKite-Features: AddKites\r\n', 'X-PageKite-Version: %s\r\n' % APPVER] if not nozchunks: req.append('X-PageKite-Features: ZChunks\r\n') if replace: req.append('X-PageKite-Replace: %s\r\n' % replace) if tls: req.append('X-PageKite-Features: TLS\r\n') req.extend(PageKiteRequestHeaders(server, backends, tokens=tokens, testtoken=testtoken)) req.append('\r\n') return ''.join(req) def HTTP_ResponseHeader(code, title, mimetype='text/html'): if mimetype.startswith('text/') and ';' not in mimetype: mimetype += ('; charset=%s' % DEFAULT_CHARSET) return ('HTTP/1.1 %s %s\r\nContent-Type: %s\r\nPragma: no-cache\r\n' 'Expires: 0\r\nCache-Control: no-store\r\nConnection: close' '\r\n') % (code, title, mimetype) def HTTP_Header(name, value): return '%s: %s\r\n' % (name, value) def HTTP_StartBody(): return '\r\n' def HTTP_ConnectOK(): return 'HTTP/1.0 200 Connection Established\r\n\r\n' def HTTP_ConnectBad(code=503, status='Unavailable'): return 'HTTP/1.0 %s %s\r\n\r\n' % (code, status) def HTTP_Response(code, title, body, mimetype='text/html', headers=None, trackable=False): data = [HTTP_ResponseHeader(code, title, mimetype)] if headers: data.extend(headers) if trackable: data.extend('X-PageKite-UUID: %s\r\n' % MAGIC_UUID) data.extend([HTTP_StartBody(), ''.join(body)]) return ''.join(data) def HTTP_NoFeConnection(proto): if proto.endswith('.json'): (mt, content) = ('application/json', '{"pagekite-status": "down-fe"}') else: (mt, content) = ('image/gif', base64.decodestring( 'R0lGODlhCgAKAMQCAN4hIf/+/v///+EzM+AuLvGkpORISPW+vudgYOhiYvKpqeZY' 'WPbAwOdaWup1dfOurvW7u++Rkepycu6PjwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAACH5BAEAAAIALAAAAAAKAAoAAAUtoCAcyEA0jyhEQOs6AuPO' 'QJHQrjEAQe+3O98PcMMBDAdjTTDBSVSQEmGhEIUAADs=')) return HTTP_Response(200, 'OK', content, mimetype=mt, headers=[HTTP_Header('X-PageKite-Status', 'Down-FE'), HTTP_Header('Access-Control-Allow-Origin', '*')]) def HTTP_NoBeConnection(proto): if proto.endswith('.json'): (mt, content) = ('application/json', '{"pagekite-status": "down-be"}') else: (mt, content) = ('image/gif', base64.decodestring( 'R0lGODlhCgAKAPcAAI9hE6t2Fv/GAf/NH//RMf/hd7u6uv/mj/ntq8XExMbFxc7N' 'zc/Ozv/xwfj31+jn5+vq6v///////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAACH5BAEAABIALAAAAAAKAAoAAAhDACUIlBAgwMCDARo4MHiQ' '4IEGDAcGKAAAAESEBCoiiBhgQEYABzYK7OiRQIEDBgMIEDCgokmUKlcOKFkgZcGb' 'BSUEBAA7')) return HTTP_Response(200, 'OK', content, mimetype=mt, headers=[HTTP_Header('X-PageKite-Status', 'Down-BE'), HTTP_Header('Access-Control-Allow-Origin', '*')]) def HTTP_GoodBeConnection(proto): if proto.endswith('.json'): (mt, content) = ('application/json', '{"pagekite-status": "ok"}') else: (mt, content) = ('image/gif', base64.decodestring( 'R0lGODlhCgAKANUCAEKtP0StQf8AAG2/a97w3qbYpd/x3mu/aajZp/b79vT69Mnn' 'yK7crXTDcqraqcfmxtLr0VG0T0ivRpbRlF24Wr7jveHy4Pv9+53UnPn8+cjnx4LI' 'gNfu1v///37HfKfZpq/crmG6XgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAACH5BAEAAAIALAAAAAAKAAoAAAZIQIGAUDgMEASh4BEANAGA' 'xRAaaHoYAAPCCZUoOIDPAdCAQhIRgJGiAG0uE+igAMB0MhYoAFmtJEJcBgILVU8B' 'GkpEAwMOggJBADs=')) return HTTP_Response(200, 'OK', content, mimetype=mt, headers=[HTTP_Header('X-PageKite-Status', 'OK'), HTTP_Header('Access-Control-Allow-Origin', '*')]) def HTTP_Unavailable(where, proto, domain, comment='', frame_url=None, code=503, status='Unavailable', headers=None): if code == 401: headers = headers or [] headers.append(HTTP_Header('WWW-Authenticate', 'Basic realm=PageKite')) message = ''.join(['

    Sorry! (', where, ')

    ', '

    The ', proto.upper(),' ', 'PageKite for ', domain, ' is unavailable at the moment.

    ', '

    Please try again later.

    ']) if frame_url: if '?' in frame_url: frame_url += '&where=%s&proto=%s&domain=%s' % (where.upper(), proto, domain) return HTTP_Response(code, status, ['', '', '', message, '', '\n'], headers=headers) else: return HTTP_Response(code, status, ['', message, '\n'], headers=headers) def TLS_Unavailable(forbidden=False, unavailable=False): """Generate a TLS alert record aborting this connectin.""" # FIXME: Should we really be ignoring forbidden and unavailable? # Unfortunately, Chrome/ium only honors code 49, any other code will # cause it to transparently retry with SSLv3. So although this is a # bit misleading, this is what we send... return struct.pack('>BBBBBBB', 0x15, 3, 3, 0, 2, 2, 49) # 49 = Access denied pagekite-0.5.8a/debian/pagekite/usr/share/pyshared/pagekite/__main__.py0000644000175000017500000001410512603560755025462 0ustar brebre00000000000000#!/usr/bin/env python """ This is the pagekite.py Main() function. """ ############################################################################## LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2015, the Beanstalks Project ehf. and Bjarni Runar Einarsson This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see: """ ############################################################################## import sys from pagekite import pk from pagekite import httpd if __name__ == "__main__": if hasattr(sys.stdout, 'isatty') and sys.stdout.isatty(): import pagekite.ui.basic uiclass = pagekite.ui.basic.BasicUi else: import pagekite.ui.nullui uiclass = pagekite.ui.nullui.NullUi pk.Main(pk.PageKite, pk.Configure, uiclass=uiclass, http_handler=httpd.UiRequestHandler, http_server=httpd.UiHttpServer) ############################################################################## CERTS="""\ StartCom Ltd. ============= -----BEGIN CERTIFICATE----- MIIFFjCCBH+gAwIBAgIBADANBgkqhkiG9w0BAQQFADCBsDELMAkGA1UEBhMCSUwxDzANBgNVBAgT BklzcmFlbDEOMAwGA1UEBxMFRWlsYXQxFjAUBgNVBAoTDVN0YXJ0Q29tIEx0ZC4xGjAYBgNVBAsT EUNBIEF1dGhvcml0eSBEZXAuMSkwJwYDVQQDEyBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1dGhv cml0eTEhMB8GCSqGSIb3DQEJARYSYWRtaW5Ac3RhcnRjb20ub3JnMB4XDTA1MDMxNzE3Mzc0OFoX DTM1MDMxMDE3Mzc0OFowgbAxCzAJBgNVBAYTAklMMQ8wDQYDVQQIEwZJc3JhZWwxDjAMBgNVBAcT BUVpbGF0MRYwFAYDVQQKEw1TdGFydENvbSBMdGQuMRowGAYDVQQLExFDQSBBdXRob3JpdHkgRGVw LjEpMCcGA1UEAxMgRnJlZSBTU0wgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxITAfBgkqhkiG9w0B CQEWEmFkbWluQHN0YXJ0Y29tLm9yZzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA7YRgACOe yEpRKSfeOqE5tWmrCbIvNP1h3D3TsM+x18LEwrHkllbEvqoUDufMOlDIOmKdw6OsWXuO7lUaHEe+ o5c5s7XvIywI6Nivcy+5yYPo7QAPyHWlLzRMGOh2iCNJitu27Wjaw7ViKUylS7eYtAkUEKD4/mJ2 IhULpNYILzUCAwEAAaOCAjwwggI4MA8GA1UdEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgHmMB0GA1Ud DgQWBBQcicOWzL3+MtUNjIExtpidjShkjTCB3QYDVR0jBIHVMIHSgBQcicOWzL3+MtUNjIExtpid jShkjaGBtqSBszCBsDELMAkGA1UEBhMCSUwxDzANBgNVBAgTBklzcmFlbDEOMAwGA1UEBxMFRWls YXQxFjAUBgNVBAoTDVN0YXJ0Q29tIEx0ZC4xGjAYBgNVBAsTEUNBIEF1dGhvcml0eSBEZXAuMSkw JwYDVQQDEyBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEhMB8GCSqGSIb3DQEJARYS YWRtaW5Ac3RhcnRjb20ub3JnggEAMB0GA1UdEQQWMBSBEmFkbWluQHN0YXJ0Y29tLm9yZzAdBgNV HRIEFjAUgRJhZG1pbkBzdGFydGNvbS5vcmcwEQYJYIZIAYb4QgEBBAQDAgAHMC8GCWCGSAGG+EIB DQQiFiBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAyBglghkgBhvhCAQQEJRYjaHR0 cDovL2NlcnQuc3RhcnRjb20ub3JnL2NhLWNybC5jcmwwKAYJYIZIAYb4QgECBBsWGWh0dHA6Ly9j ZXJ0LnN0YXJ0Y29tLm9yZy8wOQYJYIZIAYb4QgEIBCwWKmh0dHA6Ly9jZXJ0LnN0YXJ0Y29tLm9y Zy9pbmRleC5waHA/YXBwPTExMTANBgkqhkiG9w0BAQQFAAOBgQBscSXhnjSRIe/bbL0BCFaPiNhB OlP1ct8nV0t2hPdopP7rPwl+KLhX6h/BquL/lp9JmeaylXOWxkjHXo0Hclb4g4+fd68p00UOpO6w NnQt8M2YI3s3S9r+UZjEHjQ8iP2ZO1CnwYszx8JSFhKVU2Ui77qLzmLbcCOxgN8aIDjnfg== -----END CERTIFICATE----- StartCom Certification Authority ================================ -----BEGIN CERTIFICATE----- MIIHyTCCBbGgAwIBAgIBATANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJJTDEWMBQGA1UEChMN U3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmlu ZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDYwOTE3MTk0 NjM2WhcNMzYwOTE3MTk0NjM2WjB9MQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRk LjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMg U3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw ggIKAoICAQDBiNsJvGxGfHiflXu1M5DycmLWwTYgIiRezul38kMKogZkpMyONvg45iPwbm2xPN1y o4UcodM9tDMr0y+v/uqwQVlntsQGfQqedIXWeUyAN3rfOQVSWff0G0ZDpNKFhdLDcfN1YjS6LIp/ Ho/u7TTQEceWzVI9ujPW3U3eCztKS5/CJi/6tRYccjV3yjxd5srhJosaNnZcAdt0FCX+7bWgiA/d eMotHweXMAEtcnn6RtYTKqi5pquDSR3l8u/d5AGOGAqPY1MWhWKpDhk6zLVmpsJrdAfkK+F2PrRt 2PZE4XNiHzvEvqBTViVsUQn3qqvKv3b9bZvzndu/PWa8DFaqr5hIlTpL36dYUNk4dalb6kMMAv+Z 6+hsTXBbKWWc3apdzK8BMewM69KN6Oqce+Zu9ydmDBpI125C4z/eIT574Q1w+2OqqGwaVLRcJXrJ osmLFqa7LH4XXgVNWG4SHQHuEhANxjJ/GP/89PrNbpHoNkm+Gkhpi8KWTRoSsmkXwQqQ1vp5Iki/ untp+HDH+no32NgN0nZPV/+Qt+OR0t3vwmC3Zzrd/qqc8NSLf3Iizsafl7b4r4qgEKjZ+xjGtrVc UjyJthkqcwEKDwOzEmDyei+B26Nu/yYwl/WL3YlXtq09s68rxbd2AvCl1iuahhQqcvbjM4xdCUsT 37uMdBNSSwIDAQABo4ICUjCCAk4wDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAa4wHQYDVR0OBBYE FE4L7xqkQFulF2mHMMo0aEPQQa7yMGQGA1UdHwRdMFswLKAqoCiGJmh0dHA6Ly9jZXJ0LnN0YXJ0 Y29tLm9yZy9zZnNjYS1jcmwuY3JsMCugKaAnhiVodHRwOi8vY3JsLnN0YXJ0Y29tLm9yZy9zZnNj YS1jcmwuY3JsMIIBXQYDVR0gBIIBVDCCAVAwggFMBgsrBgEEAYG1NwEBATCCATswLwYIKwYBBQUH AgEWI2h0dHA6Ly9jZXJ0LnN0YXJ0Y29tLm9yZy9wb2xpY3kucGRmMDUGCCsGAQUFBwIBFilodHRw Oi8vY2VydC5zdGFydGNvbS5vcmcvaW50ZXJtZWRpYXRlLnBkZjCB0AYIKwYBBQUHAgIwgcMwJxYg U3RhcnQgQ29tbWVyY2lhbCAoU3RhcnRDb20pIEx0ZC4wAwIBARqBl0xpbWl0ZWQgTGlhYmlsaXR5 LCByZWFkIHRoZSBzZWN0aW9uICpMZWdhbCBMaW1pdGF0aW9ucyogb2YgdGhlIFN0YXJ0Q29tIENl cnRpZmljYXRpb24gQXV0aG9yaXR5IFBvbGljeSBhdmFpbGFibGUgYXQgaHR0cDovL2NlcnQuc3Rh cnRjb20ub3JnL3BvbGljeS5wZGYwEQYJYIZIAYb4QgEBBAQDAgAHMDgGCWCGSAGG+EIBDQQrFilT dGFydENvbSBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTANBgkqhkiG9w0BAQUFAAOC AgEAFmyZ9GYMNPXQhV59CuzaEE44HF7fpiUFS5Eyweg78T3dRAlbB0mKKctmArexmvclmAk8jhvh 3TaHK0u7aNM5Zj2gJsfyOZEdUauCe37Vzlrk4gNXcGmXCPleWKYK34wGmkUWFjgKXlf2Ysd6AgXm vB618p70qSmD+LIU424oh0TDkBreOKk8rENNZEXO3SipXPJzewT4F+irsfMuXGRuczE6Eri8sxHk fY+BUZo7jYn0TZNmezwD7dOaHZrzZVD1oNB1ny+v8OqCQ5j4aZyJecRDjkZy42Q2Eq/3JR44iZB3 fsNrarnDy0RLrHiQi+fHLB5LEUTINFInzQpdn4XBidUaePKVEFMy3YCEZnXZtWgo+2EuvoSoOMCZ EoalHmdkrQYuL6lwhceWD3yJZfWOQ1QOq92lgDmUYMA0yZZwLKMS9R9Ie70cfmu3nZD0Ijuu+Pwq yvqCUqDvr0tVk+vBtfAii6w0TiYiBKGHLHVKt+V9E9e4DGTANtLJL4YSjCMJwRuCO3NJo2pXh5Tl 1njFmUNj403gdy3hZZlyaQQaRwnmDwFWJPsfvw55qVguucQJAX6Vum0ABj6y6koQOdjQK/W/7HW/ lwLFCRsI3FU34oH7N4RDYiDK51ZLZer+bMEkkyShNOsF/5oirpt9P/FlUQqmMGqz9IgcgA38coro g14= -----END CERTIFICATE----- """ pagekite-0.5.8a/debian/pagekite/usr/share/pyshared/pagekite/android.py0000644000175000017500000001415012603542201025344 0ustar brebre00000000000000""" This is the main function for the Android version of PageKite. """ ############################################################################# LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2015, the Beanstalks Project ehf. and Bjarni Runar Einarsson This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see: """ ############################################################################# import sys import pagekite.pk as pk import pagekite.httpd as httpd def Configure(pkobj): pkobj.rcfile = "/sdcard/pagekite.cfg" pkobj.enable_sslzlib = True pk.Configure(pkobj) if __name__ == "__main__": if sys.stdout.isatty(): import pagekite.ui.basic uiclass = pagekite.ui.basic.BasicUi else: uiclass = pk.NullUi pk.Main(pk.PageKite, Configure, uiclass=uiclass, http_handler=httpd.UiRequestHandler, http_server=httpd.UiHttpServer) ############################################################################## CERTS="""\ StartCom Ltd. ============= -----BEGIN CERTIFICATE----- MIIFFjCCBH+gAwIBAgIBADANBgkqhkiG9w0BAQQFADCBsDELMAkGA1UEBhMCSUwxDzANBgNVBAgT BklzcmFlbDEOMAwGA1UEBxMFRWlsYXQxFjAUBgNVBAoTDVN0YXJ0Q29tIEx0ZC4xGjAYBgNVBAsT EUNBIEF1dGhvcml0eSBEZXAuMSkwJwYDVQQDEyBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1dGhv cml0eTEhMB8GCSqGSIb3DQEJARYSYWRtaW5Ac3RhcnRjb20ub3JnMB4XDTA1MDMxNzE3Mzc0OFoX DTM1MDMxMDE3Mzc0OFowgbAxCzAJBgNVBAYTAklMMQ8wDQYDVQQIEwZJc3JhZWwxDjAMBgNVBAcT BUVpbGF0MRYwFAYDVQQKEw1TdGFydENvbSBMdGQuMRowGAYDVQQLExFDQSBBdXRob3JpdHkgRGVw LjEpMCcGA1UEAxMgRnJlZSBTU0wgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxITAfBgkqhkiG9w0B CQEWEmFkbWluQHN0YXJ0Y29tLm9yZzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA7YRgACOe yEpRKSfeOqE5tWmrCbIvNP1h3D3TsM+x18LEwrHkllbEvqoUDufMOlDIOmKdw6OsWXuO7lUaHEe+ o5c5s7XvIywI6Nivcy+5yYPo7QAPyHWlLzRMGOh2iCNJitu27Wjaw7ViKUylS7eYtAkUEKD4/mJ2 IhULpNYILzUCAwEAAaOCAjwwggI4MA8GA1UdEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgHmMB0GA1Ud DgQWBBQcicOWzL3+MtUNjIExtpidjShkjTCB3QYDVR0jBIHVMIHSgBQcicOWzL3+MtUNjIExtpid jShkjaGBtqSBszCBsDELMAkGA1UEBhMCSUwxDzANBgNVBAgTBklzcmFlbDEOMAwGA1UEBxMFRWls YXQxFjAUBgNVBAoTDVN0YXJ0Q29tIEx0ZC4xGjAYBgNVBAsTEUNBIEF1dGhvcml0eSBEZXAuMSkw JwYDVQQDEyBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEhMB8GCSqGSIb3DQEJARYS YWRtaW5Ac3RhcnRjb20ub3JnggEAMB0GA1UdEQQWMBSBEmFkbWluQHN0YXJ0Y29tLm9yZzAdBgNV HRIEFjAUgRJhZG1pbkBzdGFydGNvbS5vcmcwEQYJYIZIAYb4QgEBBAQDAgAHMC8GCWCGSAGG+EIB DQQiFiBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAyBglghkgBhvhCAQQEJRYjaHR0 cDovL2NlcnQuc3RhcnRjb20ub3JnL2NhLWNybC5jcmwwKAYJYIZIAYb4QgECBBsWGWh0dHA6Ly9j ZXJ0LnN0YXJ0Y29tLm9yZy8wOQYJYIZIAYb4QgEIBCwWKmh0dHA6Ly9jZXJ0LnN0YXJ0Y29tLm9y Zy9pbmRleC5waHA/YXBwPTExMTANBgkqhkiG9w0BAQQFAAOBgQBscSXhnjSRIe/bbL0BCFaPiNhB OlP1ct8nV0t2hPdopP7rPwl+KLhX6h/BquL/lp9JmeaylXOWxkjHXo0Hclb4g4+fd68p00UOpO6w NnQt8M2YI3s3S9r+UZjEHjQ8iP2ZO1CnwYszx8JSFhKVU2Ui77qLzmLbcCOxgN8aIDjnfg== -----END CERTIFICATE----- StartCom Certification Authority ================================ -----BEGIN CERTIFICATE----- MIIHyTCCBbGgAwIBAgIBATANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJJTDEWMBQGA1UEChMN U3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmlu ZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDYwOTE3MTk0 NjM2WhcNMzYwOTE3MTk0NjM2WjB9MQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRk LjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMg U3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw ggIKAoICAQDBiNsJvGxGfHiflXu1M5DycmLWwTYgIiRezul38kMKogZkpMyONvg45iPwbm2xPN1y o4UcodM9tDMr0y+v/uqwQVlntsQGfQqedIXWeUyAN3rfOQVSWff0G0ZDpNKFhdLDcfN1YjS6LIp/ Ho/u7TTQEceWzVI9ujPW3U3eCztKS5/CJi/6tRYccjV3yjxd5srhJosaNnZcAdt0FCX+7bWgiA/d eMotHweXMAEtcnn6RtYTKqi5pquDSR3l8u/d5AGOGAqPY1MWhWKpDhk6zLVmpsJrdAfkK+F2PrRt 2PZE4XNiHzvEvqBTViVsUQn3qqvKv3b9bZvzndu/PWa8DFaqr5hIlTpL36dYUNk4dalb6kMMAv+Z 6+hsTXBbKWWc3apdzK8BMewM69KN6Oqce+Zu9ydmDBpI125C4z/eIT574Q1w+2OqqGwaVLRcJXrJ osmLFqa7LH4XXgVNWG4SHQHuEhANxjJ/GP/89PrNbpHoNkm+Gkhpi8KWTRoSsmkXwQqQ1vp5Iki/ untp+HDH+no32NgN0nZPV/+Qt+OR0t3vwmC3Zzrd/qqc8NSLf3Iizsafl7b4r4qgEKjZ+xjGtrVc UjyJthkqcwEKDwOzEmDyei+B26Nu/yYwl/WL3YlXtq09s68rxbd2AvCl1iuahhQqcvbjM4xdCUsT 37uMdBNSSwIDAQABo4ICUjCCAk4wDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAa4wHQYDVR0OBBYE FE4L7xqkQFulF2mHMMo0aEPQQa7yMGQGA1UdHwRdMFswLKAqoCiGJmh0dHA6Ly9jZXJ0LnN0YXJ0 Y29tLm9yZy9zZnNjYS1jcmwuY3JsMCugKaAnhiVodHRwOi8vY3JsLnN0YXJ0Y29tLm9yZy9zZnNj YS1jcmwuY3JsMIIBXQYDVR0gBIIBVDCCAVAwggFMBgsrBgEEAYG1NwEBATCCATswLwYIKwYBBQUH AgEWI2h0dHA6Ly9jZXJ0LnN0YXJ0Y29tLm9yZy9wb2xpY3kucGRmMDUGCCsGAQUFBwIBFilodHRw Oi8vY2VydC5zdGFydGNvbS5vcmcvaW50ZXJtZWRpYXRlLnBkZjCB0AYIKwYBBQUHAgIwgcMwJxYg U3RhcnQgQ29tbWVyY2lhbCAoU3RhcnRDb20pIEx0ZC4wAwIBARqBl0xpbWl0ZWQgTGlhYmlsaXR5 LCByZWFkIHRoZSBzZWN0aW9uICpMZWdhbCBMaW1pdGF0aW9ucyogb2YgdGhlIFN0YXJ0Q29tIENl cnRpZmljYXRpb24gQXV0aG9yaXR5IFBvbGljeSBhdmFpbGFibGUgYXQgaHR0cDovL2NlcnQuc3Rh cnRjb20ub3JnL3BvbGljeS5wZGYwEQYJYIZIAYb4QgEBBAQDAgAHMDgGCWCGSAGG+EIBDQQrFilT dGFydENvbSBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTANBgkqhkiG9w0BAQUFAAOC AgEAFmyZ9GYMNPXQhV59CuzaEE44HF7fpiUFS5Eyweg78T3dRAlbB0mKKctmArexmvclmAk8jhvh 3TaHK0u7aNM5Zj2gJsfyOZEdUauCe37Vzlrk4gNXcGmXCPleWKYK34wGmkUWFjgKXlf2Ysd6AgXm vB618p70qSmD+LIU424oh0TDkBreOKk8rENNZEXO3SipXPJzewT4F+irsfMuXGRuczE6Eri8sxHk fY+BUZo7jYn0TZNmezwD7dOaHZrzZVD1oNB1ny+v8OqCQ5j4aZyJecRDjkZy42Q2Eq/3JR44iZB3 fsNrarnDy0RLrHiQi+fHLB5LEUTINFInzQpdn4XBidUaePKVEFMy3YCEZnXZtWgo+2EuvoSoOMCZ EoalHmdkrQYuL6lwhceWD3yJZfWOQ1QOq92lgDmUYMA0yZZwLKMS9R9Ie70cfmu3nZD0Ijuu+Pwq yvqCUqDvr0tVk+vBtfAii6w0TiYiBKGHLHVKt+V9E9e4DGTANtLJL4YSjCMJwRuCO3NJo2pXh5Tl 1njFmUNj403gdy3hZZlyaQQaRwnmDwFWJPsfvw55qVguucQJAX6Vum0ABj6y6koQOdjQK/W/7HW/ lwLFCRsI3FU34oH7N4RDYiDK51ZLZer+bMEkkyShNOsF/5oirpt9P/FlUQqmMGqz9IgcgA38coro g14= -----END CERTIFICATE----- """ pagekite-0.5.8a/debian/pagekite/usr/share/pyshared/pagekite/httpd.py0000644000175000017500000010677412603542201025065 0ustar brebre00000000000000""" This is the pagekite.py built-in HTTP server. """ ############################################################################## LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2015, the Beanstalks Project ehf. and Bjarni Runar Einarsson This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see: """ ############################################################################## import base64 import cgi from cgi import escape as escape_html import os import re import socket import sys import tempfile import threading import time import traceback import urllib import SocketServer from CGIHTTPServer import CGIHTTPRequestHandler from SimpleXMLRPCServer import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler import Cookie from pagekite.common import * from pagekite.compat import * import pagekite.common as common import pagekite.logging as logging import pagekite.proto.selectables as selectables import sockschain as socks ##[ Conditional imports & compatibility magic! ]############################### try: import datetime ts_to_date = datetime.datetime.fromtimestamp except ImportError: ts_to_date = str try: sorted([1, 2, 3]) except: def sorted(l): tmp = l[:] tmp.sort() return tmp # Different Python 2.x versions complain about deprecation depending on # where we pull these from. try: from urlparse import parse_qs, urlparse except ImportError, e: from cgi import parse_qs from urlparse import urlparse try: import hashlib def sha1hex(data): hl = hashlib.sha1() hl.update(data) return hl.hexdigest().lower() except ImportError: import sha def sha1hex(data): return sha.new(data).hexdigest().lower() ##[ PageKite HTTPD code starts here! ]######################################### class AuthError(Exception): pass def fmt_size(count): if count > 2*(1024*1024*1024): return '%dGB' % (count / (1024*1024*1024)) if count > 2*(1024*1024): return '%dMB' % (count / (1024*1024)) if count > 2*(1024): return '%dKB' % (count / 1024) return '%dB' % count class CGIWrapper(CGIHTTPRequestHandler): def __init__(self, request, path_cgi): self.path = path_cgi self.cgi_info = (os.path.dirname(path_cgi), os.path.basename(path_cgi)) self.request = request self.server = request.server self.command = request.command self.headers = request.headers self.client_address = ('unknown', 0) self.rfile = request.rfile self.wfile = tempfile.TemporaryFile() def translate_path(self, path): return path def send_response(self, code, message): self.wfile.write('X-Response-Code: %s\r\n' % code) self.wfile.write('X-Response-Message: %s\r\n' % message) def send_error(self, code, message): return self.send_response(code, message) def Run(self): self.run_cgi() self.wfile.seek(0) return self.wfile class UiRequestHandler(SimpleXMLRPCRequestHandler): # Make all paths/endpoints legal, we interpret them below. rpc_paths = ( ) E403 = { 'code': '403', 'msg': 'Missing', 'mimetype': 'text/html', 'title': '403 Not found', 'body': '

    File or directory not found. Sorry!

    ' } E404 = { 'code': '404', 'msg': 'Not found', 'mimetype': 'text/html', 'title': '404 Not found', 'body': '

    File or directory not found. Sorry!

    ' } ROBOTSTXT = { 'code': '200', 'msg': 'OK', 'mimetype': 'text/plain', 'body': ('User-agent: *\n' 'Disallow: /\n' '# pagekite.py default robots.txt\n') } MIME_TYPES = { '3gp': 'video/3gpp', 'aac': 'audio/aac', 'atom': 'application/atom+xml', 'avi': 'video/avi', 'bmp': 'image/bmp', 'bz2': 'application/x-bzip2', 'c': 'text/plain', 'cpp': 'text/plain', 'css': 'text/css', 'conf': 'text/plain', 'cfg': 'text/plain', 'dtd': 'application/xml-dtd', 'doc': 'application/msword', 'gif': 'image/gif', 'gz': 'application/x-gzip', 'h': 'text/plain', 'hpp': 'text/plain', 'htm': 'text/html', 'html': 'text/html', 'hqx': 'application/mac-binhex40', 'java': 'text/plain', 'jar': 'application/java-archive', 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'js': 'application/javascript', 'json': 'application/json', 'jsonp': 'application/javascript', 'log': 'text/plain', 'md': 'text/plain', 'midi': 'audio/x-midi', 'mov': 'video/quicktime', 'mpeg': 'video/mpeg', 'mp2': 'audio/mpeg', 'mp3': 'audio/mpeg', 'm4v': 'video/mp4', 'mp4': 'video/mp4', 'm4a': 'audio/mp4', 'ogg': 'audio/vorbis', 'pdf': 'application/pdf', 'ps': 'application/postscript', 'pl': 'text/plain', 'png': 'image/png', 'ppt': 'application/vnd.ms-powerpoint', 'py': 'text/plain', 'pyw': 'text/plain', 'pk-shtml': 'text/html', 'pk-js': 'application/javascript', 'rc': 'text/plain', 'rtf': 'application/rtf', 'rss': 'application/rss+xml', 'sgml': 'text/sgml', 'sh': 'text/plain', 'shtml': 'text/plain', 'svg': 'image/svg+xml', 'swf': 'application/x-shockwave-flash', 'tar': 'application/x-tar', 'tgz': 'application/x-tar', 'tiff': 'image/tiff', 'txt': 'text/plain', 'wav': 'audio/wav', 'xml': 'application/xml', 'xls': 'application/vnd.ms-excel', 'xrdf': 'application/xrds+xml','zip': 'application/zip', 'DEFAULT': 'application/octet-stream' } TEMPLATE_RAW = ('%(body)s') TEMPLATE_JSONP = ('window.pkData = %s;') TEMPLATE_HTML = ('\n' '\n' '%(title)s - %(prog)s v%(ver)s\n' '\n' '

    %(title)s

    \n' '
    %(body)s
    \n' '\n' '\n') def setup(self): self.suppress_body = False if self.server.enable_ssl: self.connection = self.request self.rfile = socket._fileobject(self.request, "rb", self.rbufsize) self.wfile = socket._fileobject(self.request, "wb", self.wbufsize) else: SimpleXMLRPCRequestHandler.setup(self) def log_message(self, format, *args): logging.Log([('uireq', format % args)]) def send_header(self, header, value): self.wfile.write('%s: %s\r\n' % (header, value)) def end_headers(self): self.wfile.write('\r\n') def sendStdHdrs(self, header_list=[], cachectrl='private', mimetype='text/html'): if mimetype.startswith('text/') and ';' not in mimetype: mimetype += ('; charset=%s' % DEFAULT_CHARSET) self.send_header('Cache-Control', cachectrl) self.send_header('Content-Type', mimetype) for header in header_list: self.send_header(header[0], header[1]) self.end_headers() def sendChunk(self, chunk): if self.chunked: if logging.DEBUG_IO: print '<== SENDING CHUNK ===\n%s\n' % chunk self.wfile.write('%x\r\n' % len(chunk)) self.wfile.write(chunk) self.wfile.write('\r\n') else: if logging.DEBUG_IO: print '<== SENDING ===\n%s\n' % chunk self.wfile.write(chunk) def sendEof(self): if self.chunked and not self.suppress_body: self.wfile.write('0\r\n\r\n') def sendResponse(self, message, code=200, msg='OK', mimetype='text/html', header_list=[], chunked=False, length=None): self.log_request(code, message and len(message) or '-') self.wfile.write('HTTP/1.1 %s %s\r\n' % (code, msg)) if code == 401: self.send_header('WWW-Authenticate', 'Basic realm=PK%d' % (time.time()/3600)) self.chunked = chunked if chunked: self.send_header('Transfer-Encoding', 'chunked') else: if length: self.send_header('Content-Length', length) elif not chunked: self.send_header('Content-Length', len(message or '')) self.sendStdHdrs(header_list=header_list, mimetype=mimetype) if message and not self.suppress_body: self.sendChunk(message) def needPassword(self): if self.server.pkite.ui_password: return True userkeys = [k for k in self.host_config.keys() if k.startswith('password/')] return userkeys def checkUsernamePasswordAuth(self, username, password): userkey = 'password/%s' % username if userkey in self.host_config: if self.host_config[userkey] == password: return if (self.server.pkite.ui_password and password == self.server.pkite.ui_password): return if self.needPassword(): raise AuthError("Invalid password") def checkRequestAuth(self, scheme, netloc, path, qs): if self.needPassword(): raise AuthError("checkRequestAuth not implemented") def checkPostAuth(self, scheme, netloc, path, qs, posted): if self.needPassword(): raise AuthError("checkPostAuth not implemented") def performAuthChecks(self, scheme, netloc, path, qs): try: auth = self.headers.get('authorization') if auth: (how, ab64) = auth.strip().split() if how.lower() == 'basic': (username, password) = base64.decodestring(ab64).split(':') self.checkUsernamePasswordAuth(username, password) return True self.checkRequestAuth(scheme, netloc, path, qs) return True except (ValueError, KeyError, AuthError), e: logging.LogDebug('HTTP Auth failed: %s' % e) else: logging.LogDebug('HTTP Auth failed: Unauthorized') self.sendResponse('

    Unauthorized

    \n', code=401, msg='Forbidden') return False def performPostAuthChecks(self, scheme, netloc, path, qs, posted): try: self.checkPostAuth(scheme, netloc, path, qs, posted) return True except AuthError: self.sendResponse('

    Unauthorized

    \n', code=401, msg='Forbidden') return False def do_UNSUPPORTED(self): self.sendResponse('Unsupported request method.\n', code=503, msg='Sorry', mimetype='text/plain') # Misc methods we don't support (yet) def do_OPTIONS(self): self.do_UNSUPPORTED() def do_DELETE(self): self.do_UNSUPPORTED() def do_PUT(self): self.do_UNSUPPORTED() def getHostInfo(self): http_host = self.headers.get('HOST', self.headers.get('host', 'unknown')) if http_host == 'unknown' or (http_host.startswith('localhost:') and http_host.replace(':', '/') not in self.server.pkite.be_config): http_host = None for bid in sorted(self.server.pkite.backends.keys()): be = self.server.pkite.backends[bid] if (be[BE_BPORT] == self.server.pkite.ui_sspec[1] and be[BE_STATUS] not in BE_INACTIVE): http_host = '%s:%s' % (be[BE_DOMAIN], be[BE_PORT] or 80) if not http_host: if self.server.pkite.be_config.keys(): http_host = sorted(self.server.pkite.be_config.keys() )[0].replace('/', ':') else: http_host = 'unknown' self.http_host = http_host self.host_config = self.server.pkite.be_config.get((':' in http_host and http_host or http_host+':80' ).replace(':', '/'), {}) def do_GET(self, command='GET'): (scheme, netloc, path, params, query, frag) = urlparse(self.path) qs = parse_qs(query) self.getHostInfo() self.post_data = None self.command = command if not self.performAuthChecks(scheme, netloc, path, qs): return try: return self.handleHttpRequest(scheme, netloc, path, params, query, frag, qs, None) except socket.error: pass except Exception, e: logging.Log([('err', 'GET error at %s: %s' % (path, e))]) if logging.DEBUG_IO: print '=== ERROR\n%s\n===' % format_exc() self.sendResponse('

    Internal Error

    \n', code=500, msg='Error') def do_HEAD(self): self.suppress_body = True self.do_GET(command='HEAD') def do_POST(self, command='POST'): (scheme, netloc, path, params, query, frag) = urlparse(self.path) qs = parse_qs(query) self.getHostInfo() self.command = command if not self.performAuthChecks(scheme, netloc, path, qs): return posted = None self.post_data = tempfile.TemporaryFile() self.old_rfile = self.rfile try: # First, buffer the POST data to a file... clength = cleft = int(self.headers.get('content-length')) while cleft > 0: rbytes = min(64*1024, cleft) self.post_data.write(self.rfile.read(rbytes)) cleft -= rbytes # Juggle things so the buffering is invisble. self.post_data.seek(0) self.rfile = self.post_data ctype, pdict = cgi.parse_header(self.headers.get('content-type')) if ctype == 'multipart/form-data': self.post_data.seek(0) posted = cgi.parse_multipart(self.rfile, pdict) elif ctype == 'application/x-www-form-urlencoded': if clength >= 50*1024*1024: raise Exception(("Refusing to parse giant posted query " "string (%s bytes).") % clength) posted = cgi.parse_qs(self.rfile.read(clength), 1) elif self.host_config.get('xmlrpc', False): # We wrap the XMLRPC request handler in _BEGIN/_END in order to # expose the request environment to the RPC functions. RCI = self.server.RCI return RCI._END(SimpleXMLRPCRequestHandler.do_POST(RCI._BEGIN(self))) self.post_data.seek(0) except socket.error: pass except Exception, e: logging.Log([('err', 'POST error at %s: %s' % (path, e))]) self.sendResponse('

    Internal Error

    \n', code=500, msg='Error') self.rfile = self.old_rfile self.post_data = None return if not self.performPostAuthChecks(scheme, netloc, path, qs, posted): return try: return self.handleHttpRequest(scheme, netloc, path, params, query, frag, qs, posted) except socket.error: pass except Exception, e: logging.Log([('err', 'POST error at %s: %s' % (path, e))]) self.sendResponse('

    Internal Error

    \n', code=500, msg='Error') self.rfile = self.old_rfile self.post_data = None def openCGI(self, full_path, path, shtml_vars): cgi_file = CGIWrapper(self, full_path).Run() lines = cgi_file.read(32*1024).splitlines(True) if '\r\n' in lines: lines = lines[0:lines.index('\r\n')+1] elif '\n' in lines: lines = lines[0:lines.index('\n')+1] else: lines.append('') header_list = [] response_code = 200 response_message = 'OK' response_mimetype = 'text/html' for line in lines[:-1]: key, val = line.strip().split(': ', 1) if key == 'X-Response-Code': response_code = val elif key == 'X-Response-Message': response_message = val elif key.lower() == 'content-type': response_mimetype = val elif key.lower() == 'location': response_code = 302 header_list.append((key, val)) else: header_list.append((key, val)) self.sendResponse(None, code=response_code, msg=response_message, mimetype=response_mimetype, chunked=True, header_list=header_list) cgi_file.seek(sum([len(l) for l in lines])) return cgi_file def renderIndex(self, full_path, files=None): files = files or [(f, os.path.join(full_path, f)) for f in sorted(os.listdir(full_path))] # Remove dot-files and PageKite metadata files if self.host_config.get('indexes') != WEB_INDEX_ALL: files = [f for f in files if not (f[0].startswith('.') or f[0].startswith('_pagekite'))] fhtml = [''] if files: for (fn, fpath) in files: fmimetype = self.getMimeType(fn) try: fsize = os.path.getsize(fpath) or '' except OSError: fsize = 0 ops = [ ] if os.path.isdir(fpath): fclass = ['dir'] if not fn.endswith('/'): fn += '/' qfn = urllib.quote(fn) else: qfn = urllib.quote(fn) fn = os.path.basename(fn) fclass = ['file'] ops.append('download') if (fmimetype.startswith('text/') or (fmimetype == 'application/octet-stream' and fsize < 512000)): ops.append('view') (unused, ext) = os.path.splitext(fn) if ext: fclass.append(ext.replace('.', 'ext_')) fclass.append('mime_%s' % fmimetype.replace('/', '_')) ophtml = ', '.join([('%s' ) % (op, qfn, op, qfn, op) for op in sorted(ops)]) try: mtime = full_path and int(os.path.getmtime(fpath) or time.time()) except OSError: mtime = int(time.time()) fhtml.append(('' '' '' '' '' '' ) % (' '.join(fclass), ophtml, fsize, str(ts_to_date(mtime)), qfn, fn.replace('<', '<'), )) else: fhtml.append('') fhtml.append('
    %s%s%s%s
    empty
    ') return ''.join(fhtml) def sendStaticPath(self, path, mimetype, shtml_vars=None): pkite = self.server.pkite is_shtml, is_cgi, is_dir = False, False, False index_list = None try: path = urllib.unquote(path) if path.find('..') >= 0: raise IOError("Evil") paths = pkite.ui_paths def_paths = paths.get('*', {}) http_host = self.http_host if ':' not in http_host: http_host += ':80' host_paths = paths.get(http_host.replace(':', '/'), {}) path_parts = path.split('/') path_rest = [] full_path = '' root_path = '' while len(path_parts) > 0 and not full_path: pf = '/'.join(path_parts) pd = pf+'/' m = None if pf in host_paths: m = host_paths[pf] elif pd in host_paths: m = host_paths[pd] elif pf in def_paths: m = def_paths[pf] elif pd in def_paths: m = def_paths[pd] if m: policy = m[0] root_path = m[1] full_path = os.path.join(root_path, *path_rest) else: path_rest.insert(0, path_parts.pop()) if full_path: is_dir = os.path.isdir(full_path) else: if not self.host_config.get('indexes', False): return False if self.host_config.get('hide', False): return False # Generate pseudo-index ipath = path if not ipath.endswith('/'): ipath += '/' plen = len(ipath) index_list = [(p[plen:], host_paths[p][1]) for p in sorted(host_paths.keys()) if p.startswith(ipath)] if not index_list: return False full_path = '' mimetype = 'text/html' is_dir = True if is_dir and not path.endswith('/'): self.sendResponse('\n', code=302, msg='Moved', header_list=[ ('Location', '%s/' % path) ]) return True indexes = ['index.html', 'index.htm', '_pagekite.html'] dynamic_suffixes = [] if self.host_config.get('pk-shtml'): indexes[0:0] = ['index.pk-shtml'] dynamic_suffixes = ['.pk-shtml', '.pk-js'] cgi_suffixes = [] cgi_config = self.host_config.get('cgi', False) if cgi_config: if cgi_config == True: cgi_config = 'cgi' for suffix in cgi_config.split(','): indexes[0:0] = ['index.%s' % suffix] cgi_suffixes.append('.%s' % suffix) for index in indexes: ipath = os.path.join(full_path, index) if os.path.exists(ipath): mimetype = 'text/html' full_path = ipath is_dir = False break self.chunked = False rf_stat = rf_size = None if full_path: if is_dir: mimetype = 'text/html' rf_size = rf = None rf_stat = os.stat(full_path) else: for s in dynamic_suffixes: if full_path.endswith(s): is_shtml = True for s in cgi_suffixes: if full_path.endswith(s): is_cgi = True if not is_shtml and not is_cgi: shtml_vars = None rf = open(full_path, "rb") try: rf_stat = os.fstat(rf.fileno()) rf_size = rf_stat.st_size except: self.chunked = True except (IOError, OSError), e: return False headers = [ ] if rf_stat and not (is_dir or is_shtml or is_cgi): # ETags for static content: we trust the file-system. etag = sha1hex(':'.join(['%s' % s for s in [full_path, rf_stat.st_mode, rf_stat.st_ino, rf_stat.st_dev, rf_stat.st_nlink, rf_stat.st_uid, rf_stat.st_gid, rf_stat.st_size, int(rf_stat.st_mtime), int(rf_stat.st_ctime)]]))[0:24] if etag == self.headers.get('if-none-match', None): rf.close() self.sendResponse('', code=304, msg='Not Modified', mimetype=mimetype) return True else: headers.append(('ETag', etag)) # FIXME: Support ranges for resuming aborted transfers? if is_cgi: self.chunked = True rf = self.openCGI(full_path, path, shtml_vars) else: self.sendResponse(None, mimetype=mimetype, length=rf_size, chunked=self.chunked or (shtml_vars is not None), header_list=headers) chunk_size = (is_shtml and 1024 or 16) * 1024 if rf: while not self.suppress_body: data = rf.read(chunk_size) if data == "": break if is_shtml and shtml_vars: self.sendChunk(data % shtml_vars) else: self.sendChunk(data) rf.close() elif shtml_vars and not self.suppress_body: shtml_vars['title'] = '//%s%s' % (shtml_vars['http_host'], path) if self.host_config.get('indexes') in (True, WEB_INDEX_ON, WEB_INDEX_ALL): shtml_vars['body'] = self.renderIndex(full_path, files=index_list) else: shtml_vars['body'] = ('

    Directory listings disabled and ' 'index.html not found.

    ') self.sendChunk(self.TEMPLATE_HTML % shtml_vars) self.sendEof() return True def getMimeType(self, path): try: ext = path.split('.')[-1].lower() except IndexError: ext = 'DIRECTORY' if ext in self.MIME_TYPES: return self.MIME_TYPES[ext] return self.MIME_TYPES['DEFAULT'] def add_kite(self, path, qs): if path.find(self.server.secret) == -1: return {'mimetype': 'text/plain', 'body': 'Invalid secret'} pass def handleHttpRequest(self, scheme, netloc, path, params, query, frag, qs, posted): data = { 'prog': self.server.pkite.progname, 'mimetype': self.getMimeType(path), 'hostname': socket.gethostname() or 'Your Computer', 'http_host': self.http_host, 'query_string': query, 'code': 200, 'body': '', 'msg': 'OK', 'now': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), 'ver': APPVER } for key in self.headers.keys(): data['http_'+key.lower()] = self.headers.get(key) if 'download' in qs: data['mimetype'] = 'application/octet-stream' # Would be nice to set Content-Disposition too. elif 'view' in qs: data['mimetype'] = 'text/plain' data['method'] = data.get('http_x-pagekite-proto', 'http').lower() if 'http_cookie' in data: cookies = Cookie.SimpleCookie(data['http_cookie']) else: cookies = {} # Do we expose the built-in console? console = self.host_config.get('console', False) if path == self.host_config.get('yamon', False): if common.gYamon: data['body'] = common.gYamon.render_vars_text(qs.get('view', [None])[0]) else: data['body'] = '' elif console and path.startswith('/_pagekite/logout/'): parts = path.split('/') location = parts[3] or ('%s://%s/' % (data['method'], data['http_host'])) self.sendResponse('\n', code=302, msg='Moved', header_list=[ ('Set-Cookie', 'pkite_token=; path=/'), ('Location', location) ]) return elif console and path.startswith('/_pagekite/login/'): parts = path.split('/', 4) token = parts[3] location = parts[4] or ('%s://%s/_pagekite/' % (data['method'], data['http_host'])) if query: location += '?' + query if token == self.server.secret: self.sendResponse('\n', code=302, msg='Moved', header_list=[ ('Set-Cookie', 'pkite_token=%s; path=/' % token), ('Location', location) ]) return else: logging.LogDebug("Invalid token, %s != %s" % (token, self.server.secret)) data.update(self.E404) elif console and path.startswith('/_pagekite/'): if not ('pkite_token' in cookies and cookies['pkite_token'].value == self.server.secret): self.sendResponse('

    Forbidden

    \n', code=403, msg='Forbidden') return if path == '/_pagekite/': if not self.sendStaticPath('%s/control.pk-shtml' % console, 'text/html', shtml_vars=data): self.sendResponse('

    Not found

    \n', code=404, msg='Missing') return elif path.startswith('/_pagekite/quitquitquit/'): self.sendResponse('

    Kaboom

    \n', code=500, msg='Asplode') self.wfile.flush() os._exit(2) elif path.startswith('/_pagekite/add_kite/'): data.update(self.add_kite(path, qs)) elif path.endswith('/pagekite.rc'): data.update({'mimetype': 'application/octet-stream', 'body': '\n'.join(self.server.pkite.GenerateConfig())}) elif path.endswith('/pagekite.rc.txt'): data.update({'mimetype': 'text/plain', 'body': '\n'.join(self.server.pkite.GenerateConfig())}) elif path.endswith('/pagekite.cfg'): data.update({'mimetype': 'application/octet-stream', 'body': '\r\n'.join(self.server.pkite.GenerateConfig())}) else: data.update(self.E403) else: if self.sendStaticPath(path, data['mimetype'], shtml_vars=data): return if path == '/robots.txt': data.update(self.ROBOTSTXT) else: data.update(self.E404) if data['mimetype'] in ('application/octet-stream', 'text/plain'): response = self.TEMPLATE_RAW % data elif path.endswith('.jsonp'): response = self.TEMPLATE_JSONP % (data, ) else: response = self.TEMPLATE_HTML % data self.sendResponse(response, msg=data['msg'], code=data['code'], mimetype=data['mimetype'], chunked=False) self.sendEof() class RemoteControlInterface(object): ACL_OPEN = '' ACL_READ = 'r' ACL_WRITE = 'w' def __init__(self, httpd, pkite, conns): self.httpd = httpd self.pkite = pkite self.conns = conns self.modified = False self.lock = threading.Lock() self.request = None # For now, nobody gets ACL_WRITE self.auth_tokens = {httpd.secret: self.ACL_READ} # Channels are in-memory logs which can be tailed over XML-RPC. # Javascript apps can create these for implementing chat etc. self.channels = {'LOG': {'access': self.ACL_READ, 'tokens': self.auth_tokens, 'data': logging.LOG}} def _BEGIN(self, request_object): self.lock.acquire() self.request = request_object return request_object def _END(self, rv=None): if self.request: self.request = None self.lock.release() return rv def connections(self, auth_token): if (not self.request.host_config.get('console', False) or self.ACL_READ not in self.auth_tokens.get(auth_token, self.ACL_OPEN)): raise AuthError('Unauthorized') return [{'sid': c.sid, 'dead': c.dead, 'html': c.__html__()} for c in self.conns.conns] def add_kite(self, auth_token, kite_domain, kite_proto): if (not self.request.host_config.get('console', False) or self.ACL_WRITE not in self.auth_tokens.get(auth_token, self.ACL_OPEN)): raise AuthError('Unauthorized') pass def get_kites(self, auth_token): if (not self.request.host_config.get('console', False) or self.ACL_READ not in self.auth_tokens.get(auth_token, self.ACL_OPEN)): raise AuthError('Unauthorized') kites = [] for bid in self.pkite.backends: proto, domain = bid.split(':') fe_proto = proto.split('-') kite_info = { 'id': bid, 'domain': domain, 'fe_proto': fe_proto[0], 'fe_port': (len(fe_proto) > 1) and fe_proto[1] or '', 'fe_secret': self.pkite.backends[bid][BE_SECRET], 'be_proto': self.pkite.backends[bid][BE_PROTO], 'backend': self.pkite.backends[bid][BE_BACKEND], 'fe_list': [{'name': fe.server_name, 'tls': fe.using_tls, 'sid': fe.sid} for fe in self.conns.Tunnel(proto, domain)] } kites.append(kite_info) return kites def add_kite(self, auth_token, proto, fe_port, fe_domain, be_port, be_domain, shared_secret): if (not self.request.host_config.get('console', False) or self.ACL_WRITE not in self.auth_tokens.get(auth_token, self.ACL_OPEN)): raise AuthError('Unauthorized') # FIXME def remove_kite(self, auth_token, kite_id): if (not self.request.host_config.get('console', False) or self.ACL_WRITE not in self.auth_tokens.get(auth_token, self.ACL_OPEN)): raise AuthError('Unauthorized') if kite_id in self.pkite.backends: del self.pkite.backends[kite_id] logging.Log([('reconfigured', '1'), ('removed', kite_id)]) self.modified = True return self.get_kites(auth_token) def mk_channel(self, auth_token, channel): if not self.request.host_config.get('channels', False): raise AuthError('Unauthorized') chid = '%s/%s' % (self.request.http_host, channel) if chid in self.channels: raise Error('Exists') else: self.channels[chid] = {'access': self.ACL_WRITE, 'tokens': {auth_token: self.ACL_WRITE}, 'data': []} return self.append_channel(auth_token, channel, {'created': channel}) def get_channel(self, auth_token, channel): if not self.request.host_config.get('channels', False): raise AuthError('Unauthorized') chan = self.channels.get('%s/%s' % (self.request.http_host, channel), self.channels.get(channel, {})) req = chan.get('access', self.ACL_WRITE) if req not in chan.get('tokens', self.auth_tokens).get(auth_token, self.ACL_OPEN): raise AuthError('Unauthorized') return chan.get('data', []) def append_channel(self, auth_token, channel, values): data = self.get_channel(auth_token, channel) global LOG_LINE values.update({'ts': '%x' % time.time(), 'll': '%x' % LOG_LINE}) LOG_LINE += 1 data.append(values) return values def get_channel_after(self, auth_token, channel, last_seen, timeout): data = self.get_channel(auth_token, channel) last_seen = int(last_seen, 16) # line at the remote end, then we've restarted and should send everything. if (last_seen == 0) or (LOG_LINE < last_seen): return data # FIXME: LOG_LINE global for all channels? Is that suck? # We are about to get sleepy, so release our environment lock. self._END() # If our internal LOG_LINE counter is less than the count of the last seen # Else, wait at least one second, AND wait for a new line to be added to # the log (or the timeout to expire). time.sleep(1) last_ll = data[-1]['ll'] while (timeout > 0) and (data[-1]['ll'] == last_ll): time.sleep(1) timeout -= 1 # Return everything the client hasn't already seen. return [ll for ll in data if int(ll['ll'], 16) > last_seen] class UiHttpServer(SocketServer.ThreadingMixIn, SimpleXMLRPCServer): def __init__(self, sspec, pkite, conns, handler=UiRequestHandler, ssl_pem_filename=None): SimpleXMLRPCServer.__init__(self, sspec, handler) self.pkite = pkite self.conns = conns self.secret = pkite.ConfigSecret() self.server_name = sspec[0] self.server_port = sspec[1] if ssl_pem_filename: ctx = socks.SSL.Context(socks.SSL.TLSv1_METHOD) ctx.set_ciphers('HIGH:-aNULL:-eNULL:-PSK:RC4-SHA:RC4-MD5') ctx.use_privatekey_file (ssl_pem_filename) ctx.use_certificate_chain_file(ssl_pem_filename) self.socket = socks.SSL_Connect(ctx, socket.socket(self.address_family, self.socket_type), server_side=True) self.server_bind() self.server_activate() self.enable_ssl = True else: self.enable_ssl = False try: from pagekite import yamond gYamon = common.gYamon = yamond.YamonD(sspec) gYamon.vset('started', int(time.time())) gYamon.vset('version', APPVER) gYamon.vset('httpd_ssl_enabled', self.enable_ssl) gYamon.vset('errors', 0) gYamon.lcreate("tunnel_rtt", 100) gYamon.lcreate("tunnel_wrtt", 100) gYamon.lists['buffered_bytes'] = [1, 0, common.buffered_bytes] gYamon.views['selectables'] = (selectables.SELECTABLES, { 'idle': [0, 0, self.conns.idle], 'conns': [0, 0, self.conns.conns] }) except: pass self.RCI = RemoteControlInterface(self, pkite, conns) self.register_introspection_functions() self.register_instance(self.RCI) def finish_request(self, request, client_address): try: SimpleXMLRPCServer.finish_request(self, request, client_address) except (socket.error, socks.SSL.ZeroReturnError, socks.SSL.Error): pass pagekite-0.5.8a/debian/pagekite/usr/share/pyshared/pagekite/pk.py0000644000175000017500000036250012607426200024347 0ustar brebre00000000000000""" This is what is left of the original monolithic pagekite.py. This is slowly being refactored into smaller sub-modules. """ ############################################################################## LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2015, the Beanstalks Project ehf. and Bjarni Runar Einarsson This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see: """ ############################################################################## import base64 import cgi from cgi import escape as escape_html import errno import getopt import httplib import os import random import re import select import socket import struct import sys import tempfile import threading import time import traceback import urllib import xmlrpclib import zlib import SocketServer from CGIHTTPServer import CGIHTTPRequestHandler from SimpleXMLRPCServer import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler import Cookie from compat import * from common import * import compat import logging OPT_FLAGS = 'o:O:S:H:P:X:L:ZI:fA:R:h:p:aD:U:NE:' OPT_ARGS = ['noloop', 'clean', 'nopyopenssl', 'nossl', 'nocrashreport', 'nullui', 'remoteui', 'uiport=', 'help', 'settings', 'optfile=', 'optdir=', 'savefile=', 'friendly', 'shell', 'signup', 'list', 'add', 'only', 'disable', 'remove', 'save', 'service_xmlrpc=', 'controlpanel', 'controlpass', 'httpd=', 'pemfile=', 'httppass=', 'errorurl=', 'webpath=', 'logfile=', 'daemonize', 'nodaemonize', 'runas=', 'pidfile=', 'isfrontend', 'noisfrontend', 'settings', 'defaults', 'local=', 'domain=', 'auththreads=', 'authdomain=', 'motd=', 'register=', 'host=', 'noupgradeinfo', 'upgradeinfo=', 'ports=', 'protos=', 'portalias=', 'rawports=', 'tls_legacy', 'tls_default=', 'tls_endpoint=', 'selfsign', 'fe_certname=', 'jakenoia', 'ca_certs=', 'kitename=', 'kitesecret=', 'fingerpath=', 'backend=', 'define_backend=', 'be_config=', 'insecure', 'service_on=', 'service_off=', 'service_cfg=', 'tunnel_acl=', 'client_acl=', 'accept_acl_file=', 'frontend=', 'nofrontend=', 'frontends=', 'torify=', 'socksify=', 'proxy=', 'noproxy', 'new', 'all', 'noall', 'dyndns=', 'nozchunks', 'sslzlib', 'buffers=', 'noprobes', 'debugio', 'watch=', # DEPRECATED: 'reloadfile=', 'autosave', 'noautosave', 'webroot=', 'webaccess=', 'webindexes=', 'delete_backend='] # Enable system proxies # This will all fail if we don't have PySocksipyChain available. # FIXME: Move this code somewhere else? socks.usesystemdefaults() socks.wrapmodule(sys.modules[__name__]) if socks.HAVE_SSL: # Secure connections to pagekite.net in SSL tunnels. def_hop = socks.parseproxy('default') https_hop = socks.parseproxy(('httpcs!%s!443' ) % ','.join(['pagekite.net']+SERVICE_CERTS)) for dest in ('pagekite.net', 'up.pagekite.net', 'up.b5p.us'): socks.setproxy(dest, *def_hop) socks.addproxy(dest, *socks.parseproxy('http!%s!443' % dest)) socks.addproxy(dest, *https_hop) else: # FIXME: Should scream and shout about lack of security. pass ##[ PageKite.py code starts here! ]############################################ from proto.proto import * from proto.parsers import * from proto.selectables import * from proto.filters import * from proto.conns import * from ui.nullui import NullUi # FIXME: This could easily be a pool of threads to let us handle more # than one incoming request at a time. class AuthThread(threading.Thread): """Handle authentication work in a separate thread.""" #daemon = True def __init__(self, conns): threading.Thread.__init__(self) self.qc = threading.Condition() self.jobs = [] self.conns = conns def check(self, requests, conn, callback): self.qc.acquire() self.jobs.append((requests, conn, callback)) self.qc.notify() self.qc.release() def quit(self): self.qc.acquire() self.keep_running = False self.qc.notify() self.qc.release() try: self.join() except RuntimeError: pass def run(self): self.keep_running = True while self.keep_running: try: self._run() except Exception, e: logging.LogError('AuthThread died: %s' % e) time.sleep(5) logging.LogDebug('AuthThread: done') def _run(self): self.qc.acquire() while self.keep_running: now = int(time.time()) if not self.jobs: (requests, conn, callback) = None, None, None self.qc.wait() else: (requests, conn, callback) = self.jobs.pop(0) if logging.DEBUG_IO: print '=== AUTH REQUESTS\n%s\n===' % requests self.qc.release() quotas = [] q_conns = [] q_days = [] results = [] log_info = [] session = '%x:%s:' % (now, globalSecret()) for request in requests: try: proto, domain, srand, token, sign, prefix = request except: logging.LogError('Invalid request: %s' % (request, )) continue what = '%s:%s:%s' % (proto, domain, srand) session += what if not token or not sign: # Send a challenge. Our challenges are time-stamped, so we can # put stict bounds on possible replay attacks (20 minutes atm). results.append(('%s-SignThis' % prefix, '%s:%s' % (what, signToken(payload=what, timestamp=now)))) else: # This is a bit lame, but we only check the token if the quota # for this connection has never been verified. (quota, days, conns, reason ) = self.conns.config.GetDomainQuota(proto, domain, srand, token, sign, check_token=(conn.quota is None)) duplicates = self.conns.Tunnel(proto, domain) if not quota: if not reason: reason = 'quota' results.append(('%s-Invalid' % prefix, what)) results.append(('%s-Invalid-Why' % prefix, '%s;%s' % (what, reason))) log_info.extend([('rejected', domain), ('quota', quota), ('reason', reason)]) elif duplicates: # Duplicates... is the old one dead? Trigger a ping. for conn in duplicates: conn.TriggerPing() results.append(('%s-Duplicate' % prefix, what)) log_info.extend([('rejected', domain), ('duplicate', 'yes')]) else: results.append(('%s-OK' % prefix, what)) quotas.append((quota, request)) if conns: q_conns.append(conns) if days: q_days.append(days) if (proto.startswith('http') and self.conns.config.GetTlsEndpointCtx(domain)): results.append(('%s-SSL-OK' % prefix, what)) results.append(('%s-SessionID' % prefix, '%x:%s' % (now, sha1hex(session)))) results.append(('%s-Misc' % prefix, urllib.urlencode({ 'motd': (self.conns.config.motd_message or ''), }))) for upgrade in self.conns.config.upgrade_info: results.append(('%s-Upgrade' % prefix, ';'.join(upgrade))) if quotas: min_qconns = min(q_conns or [0]) if q_conns and min_qconns: results.append(('%s-QConns' % prefix, min_qconns)) min_qdays = min(q_days or [0]) if q_days and min_qdays: results.append(('%s-QDays' % prefix, min_qdays)) nz_quotas = [qp for qp in quotas if qp[0] and qp[0] > 0] if nz_quotas: quota = min(nz_quotas)[0] conn.quota = [quota, [qp[1] for qp in nz_quotas], time.time()] results.append(('%s-Quota' % prefix, quota)) elif requests: if not conn.quota: conn.quota = [None, requests, time.time()] else: conn.quota[2] = time.time() if logging.DEBUG_IO: print '=== AUTH RESULTS\n%s\n===' % results callback(results, log_info) self.qc.acquire() self.buffering = 0 self.qc.release() ##[ Selectables ]############################################################## class Connections(object): """A container for connections (Selectables), config and tunnel info.""" def __init__(self, config): self.config = config self.ip_tracker = {} self.idle = [] self.conns = [] self.conns_by_id = {} self.tunnels = {} self.auth_pool = [] def start(self, auth_threads=None, auth_thread_count=1): self.auth_pool = auth_threads or [] while len(self.auth_pool) < auth_thread_count: self.auth_pool.append(AuthThread(self)) for th in self.auth_pool: th.start() def Add(self, conn): self.conns.append(conn) def auth(self): return self.auth_pool[random.randint(0, len(self.auth_pool)-1)] def SetAltId(self, conn, new_id): if conn.alt_id and conn.alt_id in self.conns_by_id: del self.conns_by_id[conn.alt_id] if new_id: self.conns_by_id[new_id] = conn conn.alt_id = new_id def SetIdle(self, conn, seconds): self.idle.append((time.time() + seconds, conn.last_activity, conn)) def TrackIP(self, ip, domain): tick = '%d' % (time.time()/12) if tick not in self.ip_tracker: deadline = int(tick)-10 for ot in self.ip_tracker.keys(): if int(ot) < deadline: del self.ip_tracker[ot] self.ip_tracker[tick] = {} if ip not in self.ip_tracker[tick]: self.ip_tracker[tick][ip] = [1, domain] else: self.ip_tracker[tick][ip][0] += 1 self.ip_tracker[tick][ip][1] = domain def LastIpDomain(self, ip): domain = None for tick in sorted(self.ip_tracker.keys()): if ip in self.ip_tracker[tick]: domain = self.ip_tracker[tick][ip][1] return domain def Remove(self, conn, retry=True): try: if conn.alt_id and conn.alt_id in self.conns_by_id: del self.conns_by_id[conn.alt_id] if conn in self.conns: self.conns.remove(conn) rmp = [] for elc in self.idle: if elc[-1] == conn: rmp.append(elc) for elc in rmp: self.idle.remove(elc) for tid, tunnels in self.tunnels.items(): if conn in tunnels: tunnels.remove(conn) if not tunnels: del self.tunnels[tid] except (ValueError, KeyError): # Let's not asplode if another thread races us for this. logging.LogError('Failed to remove %s: %s' % (conn, format_exc())) if retry: return self.Remove(conn, retry=False) def IdleConns(self): return [p[-1] for p in self.idle] def Sockets(self): return [s.fd for s in self.conns] def Readable(self): # FIXME: This is O(n) now = time.time() return [s.fd for s in self.conns if s.IsReadable(now)] def Blocked(self): # FIXME: This is O(n) # Magic side-effect: update buffered byte counter blocked = [s for s in self.conns if s.IsBlocked()] common.buffered_bytes[0] = sum([len(s.write_blocked) for s in blocked]) return [s.fd for s in blocked] def DeadConns(self): return [s for s in self.conns if s.IsDead()] def CleanFds(self): evil = [] for s in self.conns: try: i, o, e = select.select([s.fd], [s.fd], [s.fd], 0) except: evil.append(s) for s in evil: logging.LogDebug('Removing broken Selectable: %s' % s) s.Cleanup() self.Remove(s) def Connection(self, fd): for conn in self.conns: if conn.fd == fd: return conn return None def TunnelServers(self): servers = {} for tid in self.tunnels: for tunnel in self.tunnels[tid]: server = tunnel.server_info[tunnel.S_NAME] if server is not None: servers[server] = 1 return servers.keys() def CloseTunnel(self, proto, domain, conn): tid = '%s:%s' % (proto, domain) if tid in self.tunnels: if conn in self.tunnels[tid]: self.tunnels[tid].remove(conn) if not self.tunnels[tid]: del self.tunnels[tid] def CheckIdleConns(self, now): active = [] for elc in self.idle: expire, last_activity, conn = elc if conn.last_activity > last_activity: active.append(elc) elif expire < now: logging.LogDebug('Killing idle connection: %s' % conn) conn.Die(discard_buffer=True) elif conn.created < now - 1: conn.SayHello() for pair in active: self.idle.remove(pair) def Tunnel(self, proto, domain, conn=None): tid = '%s:%s' % (proto, domain) if conn is not None: if tid not in self.tunnels: self.tunnels[tid] = [] self.tunnels[tid].append(conn) if tid in self.tunnels: return self.tunnels[tid] else: try: dparts = domain.split('.')[1:] while len(dparts) > 1: wild_tid = '%s:*.%s' % (proto, '.'.join(dparts)) if wild_tid in self.tunnels: return self.tunnels[wild_tid] dparts = dparts[1:] except: pass return [] class HttpUiThread(threading.Thread): """Handle HTTP UI in a separate thread.""" daemon = True def __init__(self, pkite, conns, server=None, handler=None, ssl_pem_filename=None): threading.Thread.__init__(self) if not (server and handler): self.serve = False self.httpd = None return self.ui_sspec = pkite.ui_sspec self.httpd = server(self.ui_sspec, pkite, conns, handler=handler, ssl_pem_filename=ssl_pem_filename) self.httpd.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.ui_sspec = pkite.ui_sspec = (self.ui_sspec[0], self.httpd.socket.getsockname()[1]) self.serve = True def quit(self): self.serve = False try: knock = rawsocket(socket.AF_INET, socket.SOCK_STREAM) knock.connect(self.ui_sspec) knock.close() except IOError: pass try: self.join() except RuntimeError: try: if self.httpd and self.httpd.socket: self.httpd.socket.close() except IOError: pass def run(self): while self.serve: try: self.httpd.handle_request() except KeyboardInterrupt: self.serve = False except Exception, e: logging.LogInfo('HTTP UI caught exception: %s' % e) if self.httpd: self.httpd.socket.close() logging.LogDebug('HttpUiThread: done') class UiCommunicator(threading.Thread): """Listen for interactive commands.""" def __init__(self, config, conns): threading.Thread.__init__(self) self.looping = False self.config = config self.conns = conns logging.LogDebug('UiComm: Created') def run(self): self.looping = True while self.looping: if not self.config or not self.config.ui.ALLOWS_INPUT: time.sleep(1) continue line = '' try: i, o, e = select.select([self.config.ui.rfile], [], [], 1) if not i: continue except: pass if self.config: line = self.config.ui.rfile.readline().strip() if line: self.Parse(line) logging.LogDebug('UiCommunicator: done') def Reconnect(self): if self.config.tunnel_manager: self.config.ui.Status('reconfig') self.config.tunnel_manager.CloseTunnels() self.config.tunnel_manager.HurryUp() def Parse(self, line): try: command, args = line.split(': ', 1) logging.LogDebug('UiComm: %s(%s)' % (command, args)) if args.lower() == 'none': args = None elif args.lower() == 'true': args = True elif args.lower() == 'false': args = False if command == 'exit': self.config.keep_looping = False self.config.main_loop = False elif command == 'restart': self.config.keep_looping = False self.config.main_loop = True elif command == 'config': command = 'change settings' self.config.Configure(['--%s' % args]) elif command == 'enablekite': command = 'enable kite' if args and args in self.config.backends: self.config.backends[args][BE_STATUS] = BE_STATUS_UNKNOWN self.Reconnect() else: raise Exception('No such kite: %s' % args) elif command == 'disablekite': command = 'disable kite' if args and args in self.config.backends: self.config.backends[args][BE_STATUS] = BE_STATUS_DISABLED self.Reconnect() else: raise Exception('No such kite: %s' % args) elif command == 'delkite': command = 'remove kite' if args and args in self.config.backends: del self.config.backends[args] self.Reconnect() else: raise Exception('No such kite: %s' % args) elif command == 'addkite': command = 'create new kite' args = (args or '').strip().split() or [''] if self.config.RegisterNewKite(kitename=args[0], autoconfigure=True, ask_be=True): self.Reconnect() elif command == 'save': command = 'save configuration' self.config.SaveUserConfig(quiet=(args == 'quietly')) except ValueError: logging.LogDebug('UiComm: bogus: %s' % line) except SystemExit: self.config.keep_looping = False self.config.main_loop = False except: logging.LogDebug('UiComm: failed %s' % (sys.exc_info(), )) self.config.ui.Tell(['Oops!', '', 'Failed to %s, details:' % command, '', '%s' % (sys.exc_info(), )], error=True) def quit(self): self.looping = False self.conns = None try: self.join() except RuntimeError: pass class TunnelManager(threading.Thread): """Create new tunnels as necessary or kill idle ones.""" daemon = True def __init__(self, pkite, conns): threading.Thread.__init__(self) self.pkite = pkite self.conns = conns def CheckTunnelQuotas(self, now): for tid in self.conns.tunnels: for tunnel in self.conns.tunnels[tid]: tunnel.RecheckQuota(self.conns, when=now) def PingTunnels(self, now): dead = {} for tid in self.conns.tunnels: for tunnel in self.conns.tunnels[tid]: pings = PING_INTERVAL if tunnel.server_info[tunnel.S_IS_MOBILE]: pings = PING_INTERVAL_MOBILE grace = max(PING_GRACE_DEFAULT, len(tunnel.write_blocked)/(tunnel.write_speed or 0.001)) if tunnel.last_activity == 0: pass elif tunnel.last_ping < now - PING_GRACE_MIN: if tunnel.last_activity < tunnel.last_ping-(PING_GRACE_MIN+grace): dead['%s' % tunnel] = tunnel elif tunnel.last_activity < now-pings: tunnel.SendPing() elif random.randint(0, 10*pings) == 0: tunnel.SendPing() for tunnel in dead.values(): logging.Log([('dead', tunnel.server_info[tunnel.S_NAME])]) tunnel.Die(discard_buffer=True) def CloseTunnels(self): close = [] for tid in self.conns.tunnels: for tunnel in self.conns.tunnels[tid]: close.append(tunnel) for tunnel in close: logging.Log([('closing', tunnel.server_info[tunnel.S_NAME])]) tunnel.Die(discard_buffer=True) def quit(self): self.keep_running = False try: self.join() except RuntimeError: pass def run(self): self.keep_running = True self.explained = False while self.keep_running: try: self._run() except Exception, e: logging.LogError('TunnelManager died: %s' % e) if logging.DEBUG_IO: traceback.print_exc(file=sys.stderr) time.sleep(5) logging.LogDebug('TunnelManager: done') def DoFrontendWork(self): self.CheckTunnelQuotas(time.time()) self.pkite.LoadMOTD() # FIXME: Front-ends should close dead back-end tunnels. for tid in self.conns.tunnels: proto, domain = tid.split(':') if '-' in proto: proto, port = proto.split('-') else: port = '' self.pkite.ui.NotifyFlyingFE(proto, port, domain) def ListBackEnds(self): self.pkite.ui.StartListingBackEnds() for bid in self.pkite.backends: be = self.pkite.backends[bid] # Do we have auto-SSL at the front-end? protoport, domain = bid.split(':', 1) tunnels = self.conns.Tunnel(protoport, domain) if be[BE_PROTO] in ('http', 'http2', 'http3') and tunnels: has_ssl = True for t in tunnels: if (protoport, domain) not in t.remote_ssl: has_ssl = False else: has_ssl = False # Get list of webpaths... domainp = '%s/%s' % (domain, be[BE_PORT] or '80') if (self.pkite.ui_sspec and be[BE_BHOST] == self.pkite.ui_sspec[0] and be[BE_BPORT] == self.pkite.ui_sspec[1]): builtin = True dpaths = self.pkite.ui_paths.get(domainp, {}) else: builtin = False dpaths = {} self.pkite.ui.NotifyBE(bid, be, has_ssl, dpaths, is_builtin=builtin, fingerprint=(builtin and self.pkite.ui_pemfingerprint)) self.pkite.ui.EndListingBackEnds() def UpdateUiStatus(self, problem, connecting): tunnel_count = len(self.pkite.conns and self.pkite.conns.TunnelServers() or []) tunnel_total = len(self.pkite.servers) if tunnel_count == 0: if self.pkite.isfrontend: self.pkite.ui.Status('idle', message='Waiting for back-ends.') elif tunnel_total == 0: self.pkite.ui.Notify('It looks like your Internet connection might be ' 'down! Will retry soon.') self.pkite.ui.Status('down', color=self.pkite.ui.GREY, message='No kites ready to fly. Waiting...') elif connecting == 0: self.pkite.ui.Status('down', color=self.pkite.ui.RED, message='Not connected to any front-ends, will retry...') elif tunnel_count < tunnel_total: self.pkite.ui.Status('flying', color=self.pkite.ui.YELLOW, message=('Only connected to %d/%d front-ends, will retry...' ) % (tunnel_count, tunnel_total)) elif problem: self.pkite.ui.Status('flying', color=self.pkite.ui.YELLOW, message='DynDNS updates may be incomplete, will retry...') else: self.pkite.ui.Status('flying', color=self.pkite.ui.GREEN, message='Kites are flying and all is well.') def _run(self): self.check_interval = 5 loop_count = 0 last_log = 0 while self.keep_running: loop_count += 1 now = time.time() if (now - last_log) >= (60 * 15): # Report liveness/state roughly once every 15 minutes logging.LogDebug('TunnelManager: loop #%d, interval=%s' % (loop_count, self.check_interval)) last_log = now # Reconnect if necessary, randomized exponential fallback. problem, connecting = self.pkite.CreateTunnels(self.conns) if problem or connecting: logging.LogDebug('TunnelManager: problem=%s, connecting=%s' % (problem, connecting)) incr = int(1+random.random()*self.check_interval) self.check_interval = min(60, self.check_interval + incr) time.sleep(1) else: self.check_interval = 5 # Make sure tunnels are really alive. if self.pkite.isfrontend: self.DoFrontendWork() self.PingTunnels(time.time()) # FIXME: This is constant noise, instead there should be a # command which requests this stuff. self.ListBackEnds() self.UpdateUiStatus(problem, connecting) for i in xrange(0, self.check_interval): if self.keep_running: time.sleep(1) if i > self.check_interval: break if self.pkite.isfrontend: self.conns.CheckIdleConns(time.time()) def HurryUp(self): self.check_interval = 0 def SecureCreate(path): fd = open(path, 'w') try: os.chmod(path, 0600) except OSError: pass return fd def CreateSelfSignedCert(pem_path, ui): ui.Notify('Creating a 2048-bit self-signed TLS certificate ...', prefix='-', color=ui.YELLOW) workdir = tempfile.mkdtemp() def w(fn): return os.path.join(workdir, fn) os.system(('openssl genrsa -out %s 2048') % w('key')) os.system(('openssl req -batch -new -key %s -out %s' ' -subj "/CN=PageKite/O=Self-Hosted/OU=Website"' ) % (w('key'), w('csr'))) os.system(('openssl x509 -req -days 3650 -in %s -signkey %s -out %s' ) % (w('csr'), w('key'), w('crt'))) pem = SecureCreate(pem_path) pem.write(open(w('key')).read()) pem.write('\n') pem.write(open(w('crt')).read()) pem.close() for fn in ['key', 'csr', 'crt']: os.remove(w(fn)) os.rmdir(workdir) ui.Notify('Saved certificate to: %s' % pem_path, prefix='-', color=ui.YELLOW) class PageKite(object): """Configuration and master select loop.""" def __init__(self, ui=None, http_handler=None, http_server=None): self.progname = ((sys.argv[0] or 'pagekite.py').split('/')[-1] .split('\\')[-1]) self.ui = ui or NullUi() self.ui_request_handler = http_handler self.ui_http_server = http_server self.ResetConfiguration() def ResetConfiguration(self): self.isfrontend = False self.upgrade_info = [] self.auth_threads = 1 self.auth_domain = None self.auth_domains = {} self.motd = None self.motd_message = None self.server_host = '' self.server_ports = [80] self.server_raw_ports = [] self.server_portalias = {} self.server_aliasport = {} self.server_protos = ['http', 'http2', 'http3', 'https', 'websocket', 'irc', 'finger', 'httpfinger', 'raw', 'minecraft'] self.accept_acl_file = None self.tunnel_acls = [] self.client_acls = [] self.tls_legacy = False self.tls_default = None self.tls_endpoints = {} self.fe_certname = [] self.fe_anon_tls_wrap = False self.service_provider = SERVICE_PROVIDER self.service_xmlrpc = SERVICE_XMLRPC self.daemonize = False self.pidfile = None self.logfile = None self.setuid = None self.setgid = None self.ui_httpd = None self.ui_sspec_cfg = None self.ui_sspec = None self.ui_socket = None self.ui_password = None self.ui_pemfile = None self.ui_pemfingerprint = None self.ui_magic_file = '.pagekite.magic' self.ui_paths = {} self.insecure = False self.be_config = {} self.disable_zchunks = False self.enable_sslzlib = False self.buffer_max = DEFAULT_BUFFER_MAX self.error_url = None self.finger_path = '/~%s/.finger' self.tunnel_manager = None self.client_mode = 0 self.proxy_servers = [] self.no_proxy = False self.require_all = False self.no_probes = False self.servers = [] self.servers_manual = [] self.servers_never = [] self.servers_auto = None self.servers_new_only = False self.servers_no_ping = False self.servers_preferred = [] self.servers_sessionids = {} self.dns_cache = {} self.ping_cache = {} self.last_frontend_choice = 0 self.kitename = '' self.kitesecret = '' self.dyndns = None self.last_updates = [] self.backends = {} # These are the backends we want tunnels for. self.conns = None self.last_loop = 0 self.keep_looping = True self.main_loop = True self.watch_level = [None] self.crash_report_url = '%scgi-bin/crashes.pl' % WWWHOME self.rcfile_recursion = 0 self.rcfiles_loaded = [] self.savefile = None self.added_kites = False self.ui_wfile = sys.stderr self.ui_rfile = sys.stdin self.ui_port = None self.ui_conn = None self.ui_comm = None self.save = 0 self.shell = False self.kite_add = False self.kite_only = False self.kite_disable = False self.kite_remove = False # Searching for our configuration file! We prefer the documented # 'standard' locations, but if nothing is found there and something local # exists, use that instead. try: if sys.platform[:3] in ('win', 'os2'): self.rcfile = os.path.join(os.path.expanduser('~'), 'pagekite.cfg') self.devnull = 'nul' else: # Everything else self.rcfile = os.path.join(os.path.expanduser('~'), '.pagekite.rc') self.devnull = '/dev/null' except Exception, e: # The above stuff may fail in some cases, e.g. on Android in SL4A. self.rcfile = 'pagekite.cfg' self.devnull = '/dev/null' # Look for CA Certificates. If we don't find them in the host OS, # we assume there might be something good in the program itself. self.ca_certs_default = '/etc/ssl/certs/ca-certificates.crt' if not os.path.exists(self.ca_certs_default): self.ca_certs_default = sys.argv[0] self.ca_certs = self.ca_certs_default ACL_SHORTHAND = { 'localhost': '((::ffff:)?127\..*|::1)', 'any': '.*' } def CheckAcls(self, acls, address, which, conn=None): if not acls: return True for policy, pattern in acls: if re.match(self.ACL_SHORTHAND.get(pattern, pattern)+'$', address[0]): if (policy.lower() == 'allow'): return True else: if conn: conn.LogError(('%s rejected by %s ACL: %s:%s' ) % (address[0], which, policy, pattern)) return False if conn: conn.LogError('%s rejected by default %s ACL' % (address[0], which)) return False def CheckClientAcls(self, address, conn=None): return self.CheckAcls(self.client_acls, address, 'client', conn) def CheckTunnelAcls(self, address, conn=None): return self.CheckAcls(self.tunnel_acls, address, 'tunnel', conn) def SetLocalSettings(self, ports): self.isfrontend = True self.servers_auto = None self.servers_manual = [] self.servers_never = [] self.server_ports = ports self.backends = self.ArgToBackendSpecs('http:localhost:localhost:builtin:-') def SetServiceDefaults(self, clobber=True, check=False): def_dyndns = (DYNDNS['pagekite.net'], {'user': '', 'pass': ''}) def_frontends = (1, 'frontends.b5p.us', 443) def_ca_certs = sys.argv[0] def_fe_certs = ['b5p.us'] + [c for c in SERVICE_CERTS if c != 'b5p.us'] def_error_url = 'https://pagekite.net/offline/?' if check: return (self.dyndns == def_dyndns and self.servers_auto == def_frontends and self.error_url == def_error_url and self.ca_certs == def_ca_certs and (sorted(self.fe_certname) == sorted(def_fe_certs) or not socks.HAVE_SSL)) else: self.dyndns = (not clobber and self.dyndns) or def_dyndns self.servers_auto = (not clobber and self.servers_auto) or def_frontends self.error_url = (not clobber and self.error_url) or def_error_url self.ca_certs = def_ca_certs if socks.HAVE_SSL: for cert in def_fe_certs: if cert not in self.fe_certname: self.fe_certname.append(cert) return True def GenerateConfig(self, safe=False): config = [ '###[ Current settings for pagekite.py v%s. ]#########' % APPVER, '#', '## NOTE: This file may be rewritten/reordered by pagekite.py.', '#', '', ] if not self.kitename: for be in self.backends.values(): if not self.kitename or len(self.kitename) < len(be[BE_DOMAIN]): self.kitename = be[BE_DOMAIN] self.kitesecret = be[BE_SECRET] new = not (self.kitename or self.kitesecret or self.backends) def p(vfmt, value, dval): return '%s%s' % (value and value != dval and ('', vfmt % value) or ('# ', vfmt % dval)) if self.kitename or self.kitesecret or new: config.extend([ '##[ Default kite and account details ]##', p('kitename = %s', self.kitename, 'NAME'), p('kitesecret = %s', self.kitesecret, 'SECRET'), '' ]) if self.SetServiceDefaults(check=True): config.extend([ '##[ Front-end settings: use pagekite.net defaults ]##', 'defaults', '' ]) if self.servers_manual or self.servers_never: config.append('##[ Manual front-ends ]##') for server in sorted(self.servers_manual): config.append('frontend=%s' % server) for server in sorted(self.servers_never): config.append('nofrontend=%s' % server) config.append('') else: if not self.servers_auto and not self.servers_manual: new = True config.extend([ '##[ Use this to just use pagekite.net defaults ]##', '# defaults', '' ]) config.append('##[ Custom front-end and dynamic DNS settings ]##') if self.servers_auto: config.append('frontends = %d:%s:%d' % self.servers_auto) if self.servers_manual: for server in sorted(self.servers_manual): config.append('frontend = %s' % server) if self.servers_never: for server in sorted(self.servers_never): config.append('nofrontend = %s' % server) if not self.servers_auto and not self.servers_manual: new = True config.append('# frontends = N:hostname:port') config.append('# frontend = hostname:port') config.append('# nofrontend = hostname:port # never connect') for server in self.fe_certname: config.append('fe_certname = %s' % server) if self.ca_certs != self.ca_certs_default: config.append('ca_certs = %s' % self.ca_certs) if self.dyndns: provider, args = self.dyndns for prov in sorted(DYNDNS.keys()): if DYNDNS[prov] == provider and prov != 'beanstalks.net': args['prov'] = prov if 'prov' not in args: args['prov'] = provider if args['pass']: config.append('dyndns = %(user)s:%(pass)s@%(prov)s' % args) elif args['user']: config.append('dyndns = %(user)s@%(prov)s' % args) else: config.append('dyndns = %(prov)s' % args) else: new = True config.extend([ '# dyndns = pagekite.net OR', '# dyndns = user:pass@dyndns.org OR', '# dyndns = user:pass@no-ip.com' , '#', p('errorurl = %s', self.error_url, 'http://host/page/'), p('fingerpath = %s', self.finger_path, '/~%s/.finger'), '', ]) config.append('') if self.ui_sspec or self.ui_password or self.ui_pemfile: config.extend([ '##[ Built-in HTTPD settings ]##', p('httpd = %s:%s', self.ui_sspec_cfg, ('host', 'port')) ]) if self.ui_password: config.append('httppass=%s' % self.ui_password) if self.ui_pemfile: config.append('pemfile=%s' % self.ui_pemfile) for http_host in sorted(self.ui_paths.keys()): for path in sorted(self.ui_paths[http_host].keys()): up = self.ui_paths[http_host][path] config.append('webpath = %s:%s:%s:%s' % (http_host, path, up[0], up[1])) config.append('') config.append('##[ Back-ends and local services ]##') bprinted = 0 for bid in sorted(self.backends.keys()): be = self.backends[bid] proto, domain = bid.split(':') if be[BE_BHOST]: be_spec = (be[BE_BHOST], be[BE_BPORT]) be_spec = ((be_spec == self.ui_sspec) and 'localhost:builtin' or ('%s:%s' % be_spec)) fe_spec = ('%s:%s' % (proto, (domain == self.kitename) and '@kitename' or domain)) secret = ((be[BE_SECRET] == self.kitesecret) and '@kitesecret' or be[BE_SECRET]) config.append(('%s = %-33s: %-18s: %s' ) % ((be[BE_STATUS] == BE_STATUS_DISABLED ) and 'service_off' or 'service_on ', fe_spec, be_spec, secret)) bprinted += 1 if bprinted == 0: config.append('# No back-ends! How boring!') config.append('') for http_host in sorted(self.be_config.keys()): for key in sorted(self.be_config[http_host].keys()): config.append(('service_cfg = %-30s: %-15s: %s' ) % (http_host, key, self.be_config[http_host][key])) config.append('') if bprinted == 0: new = True config.extend([ '##[ Back-end service examples ... ]##', '#', '# service_on = http:YOU.pagekite.me:localhost:80:SECRET', '# service_on = ssh:YOU.pagekite.me:localhost:22:SECRET', '# service_on = http/8080:YOU.pagekite.me:localhost:8080:SECRET', '# service_on = https:YOU.pagekite.me:localhost:443:SECRET', '# service_on = websocket:YOU.pagekite.me:localhost:8080:SECRET', '# service_on = minecraft:YOU.pagekite.me:localhost:8080:SECRET', '#', '# service_off = http:YOU.pagekite.me:localhost:4545:SECRET', '' ]) config.extend([ '##[ Allow risky known-to-be-risky incoming HTTP requests? ]##', (self.insecure) and 'insecure' or '# insecure', '' ]) if self.isfrontend or new: config.extend([ '##[ Front-end Options ]##', (self.isfrontend and 'isfrontend' or '# isfrontend') ]) comment = ((not self.isfrontend) and '# ' or '') config.extend([ p('host = %s', self.isfrontend and self.server_host, 'machine.domain.com'), '%sports = %s' % (comment, ','.join(['%s' % x for x in sorted(self.server_ports)] or [])), '%sprotos = %s' % (comment, ','.join(['%s' % x for x in sorted(self.server_protos)] or [])) ]) for pa in self.server_portalias: config.append('portalias = %s:%s' % (int(pa), int(self.server_portalias[pa]))) config.extend([ '%srawports = %s' % (comment or (not self.server_raw_ports) and '# ' or '', ','.join(['%s' % x for x in sorted(self.server_raw_ports)] or [VIRTUAL_PN])), p('auththreads = %s', self.isfrontend and self.auth_threads, 1), p('authdomain = %s', self.isfrontend and self.auth_domain, 'foo.com'), p('motd = %s', self.isfrontend and self.motd, '/path/to/motd.txt') ]) for d in sorted(self.auth_domains.keys()): config.append('authdomain=%s:%s' % (d, self.auth_domains[d])) dprinted = 0 for bid in sorted(self.backends.keys()): be = self.backends[bid] if not be[BE_BHOST]: config.append('domain = %s:%s' % (bid, be[BE_SECRET])) dprinted += 1 if not dprinted: new = True config.extend([ '# domain = http:*.pagekite.me:SECRET1', '# domain = http,https,websocket:THEM.pagekite.me:SECRET2', ]) eprinted = 0 config.extend([ '', '##[ Domains we terminate SSL/TLS for natively, with key/cert-files ]##' ]) for ep in sorted(self.tls_endpoints.keys()): config.append('tls_endpoint = %s:%s' % (ep, self.tls_endpoints[ep][0])) eprinted += 1 if eprinted == 0: new = True config.append('# tls_endpoint = DOMAIN:PEM_FILE') config.extend([ p('tls_default = %s', self.tls_default, 'DOMAIN'), p('tls_legacy = %s', self.tls_legacy, False), '', ]) config.extend([ '##[ Proxy-chain settings ]##', (self.no_proxy and 'noproxy' or '# noproxy'), ]) for proxy in self.proxy_servers: config.append('proxy = %s' % proxy) if not self.proxy_servers: config.extend([ '# socksify = host:port', '# torify = host:port', '# proxy = ssl:/path/to/client-cert.pem@host,CommonName:port', '# proxy = http://user:password@host:port/', '# proxy = socks://user:password@host:port/' ]) config.extend([ '', '##[ Front-end access controls (default=deny, if configured) ]##', p('accept_acl_file = %s', self.accept_acl_file, '/path/to/file'), ]) for policy, pattern in self.client_acls: config.append('client_acl=%s:%s' % (policy, pattern)) if not self.client_acls: config.append('# client_acl=[allow|deny]:IP-regexp') for policy, pattern in self.tunnel_acls: config.append('tunnel_acl=%s:%s' % (policy, pattern)) if not self.tunnel_acls: config.append('# tunnel_acl=[allow|deny]:IP-regexp') config.extend([ '', '', '###[ Anything below this line can usually be ignored. ]#########', '', '##[ Miscellaneous settings ]##', p('logfile = %s', self.logfile, '/path/to/file'), p('buffers = %s', self.buffer_max, DEFAULT_BUFFER_MAX), (self.servers_new_only is True) and 'new' or '# new', (self.require_all and 'all' or '# all'), (self.no_probes and 'noprobes' or '# noprobes'), (self.crash_report_url and '# nocrashreport' or 'nocrashreport'), p('savefile = %s', safe and self.savefile, '/path/to/savefile'), '', ]) if self.daemonize or self.setuid or self.setgid or self.pidfile or new: config.extend([ '##[ Systems administration settings ]##', (self.daemonize and 'daemonize' or '# daemonize') ]) if self.setuid and self.setgid: config.append('runas = %s:%s' % (self.setuid, self.setgid)) elif self.setuid: config.append('runas = %s' % self.setuid) else: new = True config.append('# runas = uid:gid') config.append(p('pidfile = %s', self.pidfile, '/path/to/file')) config.extend([ '', '###[ End of pagekite.py configuration ]#########', 'END', '' ]) if not new: config = [l for l in config if not l.startswith('# ')] clean_config = [] for i in range(0, len(config)-1): if i > 0 and (config[i].startswith('#') or config[i] == ''): if config[i+1] != '' or clean_config[-1].startswith('#'): clean_config.append(config[i]) else: clean_config.append(config[i]) clean_config.append(config[-1]) return clean_config else: return config def ConfigSecret(self, new=False): # This method returns a stable secret for the lifetime of this process. # # The secret depends on the active configuration as, reported by # GenerateConfig(). This lets external processes generate the same # secret and use the remote-control APIs as long as they can read the # *entire* config (which contains all the sensitive bits anyway). # if self.ui_httpd and self.ui_httpd.httpd and not new: return self.ui_httpd.httpd.secret else: return sha1hex('\n'.join(self.GenerateConfig())) def LoginPath(self, goto): return '/_pagekite/login/%s/%s' % (self.ConfigSecret(), goto) def LoginUrl(self, goto=''): return 'http%s://%s%s' % (self.ui_pemfile and 's' or '', '%s:%s' % self.ui_sspec, self.LoginPath(goto)) def ListKites(self): self.ui.welcome = '>>> ' + self.ui.WHITE + 'Your kites:' + self.ui.NORM message = [] for bid in sorted(self.backends.keys()): be = self.backends[bid] be_be = (be[BE_BHOST], be[BE_BPORT]) backend = (be_be == self.ui_sspec) and 'builtin' or '%s:%s' % be_be fe_port = be[BE_PORT] or '' frontend = '%s://%s%s%s' % (be[BE_PROTO], be[BE_DOMAIN], fe_port and ':' or '', fe_port) if be[BE_STATUS] == BE_STATUS_DISABLED: color = self.ui.GREY status = '(disabled)' else: color = self.ui.NORM status = (be[BE_PROTO] == 'raw') and '(HTTP proxied)' or '' message.append(''.join([color, backend, ' ' * (19-len(backend)), frontend, ' ' * (42-len(frontend)), status])) message.append(self.ui.NORM) self.ui.Tell(message) def PrintSettings(self, safe=False): print '\n'.join(self.GenerateConfig(safe=safe)) def SaveUserConfig(self, quiet=False): self.savefile = self.savefile or self.rcfile try: fd = SecureCreate(self.savefile) fd.write('\n'.join(self.GenerateConfig(safe=True))) fd.close() if not quiet: self.ui.Tell(['Settings saved to: %s' % self.savefile]) self.ui.Spacer() logging.Log([('saved', 'Settings saved to: %s' % self.savefile)]) except Exception, e: if logging.DEBUG_IO: traceback.print_exc(file=sys.stderr) self.ui.Tell(['Could not save to %s: %s' % (self.savefile, e)], error=True) self.ui.Spacer() def FallDown(self, message, help=True, longhelp=False, noexit=False): if self.conns and self.conns.auth_pool: for th in self.conns.auth_pool: th.quit() if self.ui_httpd: self.ui_httpd.quit() if self.ui_comm: self.ui_comm.quit() if self.tunnel_manager: self.tunnel_manager.quit() self.keep_looping = False for fd in (self.conns and self.conns.Sockets() or []): try: fd.close() except (IOError, OSError, TypeError, AttributeError): pass self.conns = self.ui_httpd = self.ui_comm = self.tunnel_manager = None try: os.dup2(sys.stderr.fileno(), sys.stdout.fileno()) except: pass print if help or longhelp: import manual print longhelp and manual.DOC() or manual.MINIDOC() print '***' elif not noexit: self.ui.Status('exiting', message=(message or 'Good-bye!')) if message: print 'Error: %s' % message if logging.DEBUG_IO: traceback.print_exc(file=sys.stderr) if not noexit: self.main_loop = False sys.exit(1) def GetTlsEndpointCtx(self, domain): if domain in self.tls_endpoints: return self.tls_endpoints[domain][1] parts = domain.split('.') # Check for wildcards ... if len(parts) > 2: parts[0] = '*' domain = '.'.join(parts) if domain in self.tls_endpoints: return self.tls_endpoints[domain][1] return None def SetBackendStatus(self, domain, proto='', add=None, sub=None): match = '%s:%s' % (proto, domain) for bid in self.backends: if bid == match or (proto == '' and bid.endswith(match)): status = self.backends[bid][BE_STATUS] if add: self.backends[bid][BE_STATUS] |= add if sub and (status & sub): self.backends[bid][BE_STATUS] -= sub logging.Log([('bid', bid), ('status', '0x%x' % self.backends[bid][BE_STATUS])]) def GetBackendData(self, proto, domain, recurse=True): backend = '%s:%s' % (proto.lower(), domain.lower()) if backend in self.backends: if self.backends[backend][BE_STATUS] not in BE_INACTIVE: return self.backends[backend] if recurse: dparts = domain.split('.') while len(dparts) > 1: dparts = dparts[1:] data = self.GetBackendData(proto, '.'.join(['*'] + dparts), recurse=False) if data: return data return None def GetBackendServer(self, proto, domain, recurse=True): backend = self.GetBackendData(proto, domain) or BE_NONE bhost, bport = (backend[BE_BHOST], backend[BE_BPORT]) if bhost == '-' or not bhost: return None, None return (bhost, bport), backend def IsSignatureValid(self, sign, secret, proto, domain, srand, token): return checkSignature(sign=sign, secret=secret, payload='%s:%s:%s:%s' % (proto, domain, srand, token)) def LookupDomainQuota(self, lookup): if not lookup.endswith('.'): lookup += '.' if logging.DEBUG_IO: print '=== AUTH LOOKUP\n%s\n===' % lookup (hn, al, ips) = socket.gethostbyname_ex(lookup) if logging.DEBUG_IO: print 'hn=%s\nal=%s\nips=%s\n' % (hn, al, ips) # Extract auth error and extended quota info from CNAME replies if al: error, hg, hd, hc, junk = hn.split('.', 4) q_days = int(hd, 16) q_conns = int(hc, 16) else: error = q_days = q_conns = None # If not an authentication error, quota should be encoded as an IP. ip = ips[0] if ip.startswith(AUTH_ERRORS): if not error and (ip.endswith(AUTH_ERR_USER_UNKNOWN) or ip.endswith(AUTH_ERR_INVALID)): error = 'unauthorized' else: o = [int(x) for x in ip.split('.')] return ((((o[0]*256 + o[1])*256 + o[2])*256 + o[3]), q_days, q_conns, None) # Errors on real errors are final. if not ip.endswith(AUTH_ERR_USER_UNKNOWN): return (None, q_days, q_conns, error) # User unknown, fall through to local test. return (-1, q_days, q_conns, error) def GetDomainQuota(self, protoport, domain, srand, token, sign, recurse=True, check_token=True): if '-' in protoport: try: proto, port = protoport.split('-', 1) if proto == 'raw': port_list = self.server_raw_ports else: port_list = self.server_ports porti = int(port) if porti in self.server_aliasport: porti = self.server_aliasport[porti] if porti not in port_list and VIRTUAL_PN not in port_list: logging.LogInfo('Unsupported port request: %s (%s:%s)' % (porti, protoport, domain)) return (None, None, None, 'port') except ValueError: logging.LogError('Invalid port request: %s:%s' % (protoport, domain)) return (None, None, None, 'port') else: proto, port = protoport, None if proto not in self.server_protos: logging.LogInfo('Invalid proto request: %s:%s' % (protoport, domain)) return (None, None, None, 'proto') data = '%s:%s:%s' % (protoport, domain, srand) auth_error_type = None if ((not token) or (not check_token) or checkSignature(sign=token, payload=data)): secret = (self.GetBackendData(protoport, domain) or BE_NONE)[BE_SECRET] if not secret: secret = (self.GetBackendData(proto, domain) or BE_NONE)[BE_SECRET] if secret: if self.IsSignatureValid(sign, secret, protoport, domain, srand, token): return (-1, None, None, None) elif not self.auth_domain: logging.LogError('Invalid signature for: %s (%s)' % (domain, protoport)) return (None, None, None, auth_error_type or 'signature') if self.auth_domain: adom = self.auth_domain for dom in self.auth_domains: if domain.endswith('.%s' % dom): adom = self.auth_domains[dom] try: lookup = '.'.join([srand, token, sign, protoport, domain.replace('*', '_any_'), adom]) (rv, qd, qc, auth_error_type) = self.LookupDomainQuota(lookup) if rv is None or rv >= 0: return (rv, qd, qc, auth_error_type) except Exception, e: # Lookup failed, fail open. logging.LogError('Quota lookup failed: %s' % e) return (-2, None, None, None) logging.LogInfo('No authentication found for: %s (%s)' % (domain, protoport)) return (None, None, None, auth_error_type or 'unauthorized') def ConfigureFromFile(self, filename=None, data=None): if not filename: filename = self.rcfile if self.rcfile_recursion > 25: raise ConfigError('Nested too deep: %s' % filename) self.rcfiles_loaded.append(filename) optfile = data or open(filename) args = [] for line in optfile: line = line.strip() if line and not line.startswith('#'): if line.startswith('END'): break if not line.startswith('-'): line = '--%s' % line args.append(re.sub(r'\s*:\s*', ':', re.sub(r'\s*=\s*', '=', line))) self.rcfile_recursion += 1 self.Configure(args) self.rcfile_recursion -= 1 return self def ConfigureFromDirectory(self, dirname): for fn in sorted(os.listdir(dirname)): if not fn.startswith('.') and fn.endswith('.rc'): self.ConfigureFromFile(os.path.join(dirname, fn)) def HelpAndExit(self, longhelp=False): import manual print longhelp and manual.DOC() or manual.MINIDOC() sys.exit(0) def AddNewKite(self, kitespec, status=BE_STATUS_UNKNOWN, secret=None): new_specs = self.ArgToBackendSpecs(kitespec, status, secret) self.backends.update(new_specs) req = {} for server in self.conns.TunnelServers(): req[server] = '\r\n'.join(PageKiteRequestHeaders(server, new_specs, {})) for tid, tunnels in self.conns.tunnels.iteritems(): for tunnel in tunnels: server_name = tunnel.server_info[tunnel.S_NAME] if server_name in req: tunnel.SendChunked('NOOP: 1\r\n%s\r\n\r\n!' % req[server_name], compress=False) del req[server_name] def ArgToBackendSpecs(self, arg, status=BE_STATUS_UNKNOWN, secret=None): protos, fe_domain, be_host, be_port = '', '', '', '' # Interpret the argument into a specification of what we want. parts = arg.split(':') if len(parts) == 5: protos, fe_domain, be_host, be_port, secret = parts elif len(parts) == 4: protos, fe_domain, be_host, be_port = parts elif len(parts) == 3: protos, fe_domain, be_port = parts elif len(parts) == 2: if (parts[1] == 'builtin') or ('.' in parts[0] and os.path.exists(parts[1])): fe_domain, be_port = parts[0], parts[1] protos = 'http' else: try: fe_domain, be_port = parts[0], '%s' % int(parts[1]) protos = 'http' except: be_port = '' protos, fe_domain = parts elif len(parts) == 1: fe_domain = parts[0] else: return {} # Allow http:// as a common typo instead of http: fe_domain = fe_domain.replace('/', '').lower() # Allow easy referencing of built-in HTTPD if be_port == 'builtin': self.BindUiSspec() be_host, be_port = self.ui_sspec # Specs define what we are searching for... specs = [] if protos: for proto in protos.replace('/', '-').lower().split(','): if proto == 'ssh': specs.append(['raw', '22', fe_domain, be_host, be_port or '22', secret]) else: if '-' in proto: proto, port = proto.split('-') else: if len(parts) == 1: port = '*' else: port = '' specs.append([proto, port, fe_domain, be_host, be_port, secret]) else: specs = [[None, '', fe_domain, be_host, be_port, secret]] backends = {} # For each spec, search through the existing backends and copy matches # or just shared secrets for partial matches. for proto, port, fdom, bhost, bport, sec in specs: matches = 0 for bid in self.backends: be = self.backends[bid] if fdom and fdom != be[BE_DOMAIN]: continue if not sec and be[BE_SECRET]: sec = be[BE_SECRET] if proto and (proto != be[BE_PROTO]): continue if bhost and (bhost.lower() != be[BE_BHOST]): continue if bport and (int(bport) != be[BE_BHOST]): continue if port and (port != '*') and (int(port) != be[BE_PORT]): continue backends[bid] = be[:] backends[bid][BE_STATUS] = status matches += 1 if matches == 0: proto = (proto or 'http') bhost = (bhost or 'localhost') bport = (bport or (proto in ('http', 'httpfinger', 'websocket') and 80) or (proto == 'irc' and 6667) or (proto == 'https' and 443) or (proto == 'minecraft' and 25565) or (proto == 'finger' and 79)) if port: bid = '%s-%d:%s' % (proto, int(port), fdom) else: bid = '%s:%s' % (proto, fdom) backends[bid] = BE_NONE[:] backends[bid][BE_PROTO] = proto backends[bid][BE_PORT] = port and int(port) or '' backends[bid][BE_DOMAIN] = fdom backends[bid][BE_BHOST] = bhost.lower() backends[bid][BE_BPORT] = int(bport) backends[bid][BE_SECRET] = sec backends[bid][BE_STATUS] = status return backends def BindUiSspec(self, force=False): # Create the UI thread if self.ui_httpd and self.ui_httpd.httpd: if not force: return self.ui_sspec self.ui_httpd.httpd.socket.close() self.ui_sspec = self.ui_sspec or ('localhost', 0) self.ui_httpd = HttpUiThread(self, self.conns, handler=self.ui_request_handler, server=self.ui_http_server, ssl_pem_filename = self.ui_pemfile) return self.ui_sspec def LoadMOTD(self): if self.motd: try: f = open(self.motd, 'r') self.motd_message = ''.join(f.readlines()).strip()[:8192] f.close() except (OSError, IOError): pass def SetPem(self, filename): self.ui_pemfile = filename try: p = os.popen('openssl x509 -noout -fingerprint -in %s' % filename, 'r') data = p.read().strip() p.close() self.ui_pemfingerprint = data.split('=')[1] except (OSError, ValueError): pass def Configure(self, argv): self.conns = self.conns or Connections(self) opts, args = getopt.getopt(argv, OPT_FLAGS, OPT_ARGS) for opt, arg in opts: if opt in ('-o', '--optfile'): self.ConfigureFromFile(arg) elif opt in ('-O', '--optdir'): self.ConfigureFromDirectory(arg) elif opt in ('-S', '--savefile'): if self.savefile: raise ConfigError('Multiple save-files!') self.savefile = arg elif opt == '--shell': self.shell = True elif opt == '--save': self.save = True elif opt == '--only': self.save = self.kite_only = True if self.kite_remove or self.kite_add or self.kite_disable: raise ConfigError('One change at a time please!') elif opt == '--add': self.save = self.kite_add = True if self.kite_remove or self.kite_only or self.kite_disable: raise ConfigError('One change at a time please!') elif opt == '--remove': self.save = self.kite_remove = True if self.kite_add or self.kite_only or self.kite_disable: raise ConfigError('One change at a time please!') elif opt == '--disable': self.save = self.kite_disable = True if self.kite_add or self.kite_only or self.kite_remove: raise ConfigError('One change at a time please!') elif opt == '--list': pass elif opt in ('-I', '--pidfile'): self.pidfile = arg elif opt in ('-L', '--logfile'): self.logfile = arg elif opt in ('-Z', '--daemonize'): self.daemonize = True if not self.ui.DAEMON_FRIENDLY: self.ui = NullUi() elif opt in ('-U', '--runas'): import pwd import grp parts = arg.split(':') if len(parts) > 1: self.setuid, self.setgid = (pwd.getpwnam(parts[0])[2], grp.getgrnam(parts[1])[2]) else: self.setuid = pwd.getpwnam(parts[0])[2] self.main_loop = False elif opt in ('-X', '--httppass'): self.ui_password = arg elif opt in ('-P', '--pemfile'): self.SetPem(arg) elif opt in ('--selfsign', ): pf = self.rcfile.replace('.rc', '.pem').replace('.cfg', '.pem') if not os.path.exists(pf): CreateSelfSignedCert(pf, self.ui) self.SetPem(pf) elif opt in ('-H', '--httpd'): parts = arg.split(':') host = parts[0] or 'localhost' if len(parts) > 1: self.ui_sspec = self.ui_sspec_cfg = (host, int(parts[1])) else: self.ui_sspec = self.ui_sspec_cfg = (host, 0) elif opt == '--nowebpath': host, path = arg.split(':', 1) if host in self.ui_paths and path in self.ui_paths[host]: del self.ui_paths[host][path] elif opt == '--webpath': host, path, policy, fpath = arg.split(':', 3) # Defaults... path = path or os.path.normpath(fpath) host = host or '*' policy = policy or WEB_POLICY_DEFAULT if policy not in WEB_POLICIES: raise ConfigError('Policy must be one of: %s' % WEB_POLICIES) elif os.path.isdir(fpath): if not path.endswith('/'): path += '/' hosti = self.ui_paths.get(host, {}) hosti[path] = (policy or 'public', os.path.abspath(fpath)) self.ui_paths[host] = hosti elif opt == '--tls_default': self.tls_default = arg elif opt == '--tls_legacy': self.tls_legacy = True elif opt == '--tls_endpoint': name, pemfile = arg.split(':', 1) ctx = socks.MakeBestEffortSSLContext(legacy=self.tls_legacy) ctx.use_privatekey_file(pemfile) ctx.use_certificate_chain_file(pemfile) self.tls_endpoints[name] = (pemfile, ctx) elif opt in ('-D', '--dyndns'): if arg.startswith('http'): self.dyndns = (arg, {'user': '', 'pass': ''}) elif '@' in arg: splits = arg.split('@') provider = splits.pop() usrpwd = '@'.join(splits) if provider in DYNDNS: provider = DYNDNS[provider] if ':' in usrpwd: usr, pwd = usrpwd.split(':', 1) self.dyndns = (provider, {'user': usr, 'pass': pwd}) else: self.dyndns = (provider, {'user': usrpwd, 'pass': ''}) elif arg: if arg in DYNDNS: arg = DYNDNS[arg] self.dyndns = (arg, {'user': '', 'pass': ''}) else: self.dyndns = None elif opt in ('-p', '--ports'): self.server_ports = [int(x) for x in arg.split(',')] elif opt == '--portalias': port, alias = arg.split(':') self.server_portalias[int(port)] = int(alias) self.server_aliasport[int(alias)] = int(port) elif opt == '--protos': self.server_protos = [x.lower() for x in arg.split(',')] elif opt == '--rawports': self.server_raw_ports = [(x == VIRTUAL_PN and x or int(x)) for x in arg.split(',')] elif opt in ('-h', '--host'): self.server_host = arg elif opt == '--auththreads': self.auth_threads = int(arg) elif opt in ('-A', '--authdomain'): if ':' in arg: d, a = arg.split(':') self.auth_domains[d.lower()] = a if not self.auth_domain: self.auth_domain = a else: self.auth_domains = {} self.auth_domain = arg elif opt == '--motd': self.motd = arg self.LoadMOTD() elif opt == '--noupgradeinfo': self.upgrade_info = [] elif opt == '--upgradeinfo': version, tag, md5, human_url, file_url = arg.split(';') self.upgrade_info.append((version, tag, md5, human_url, file_url)) elif opt in ('-f', '--isfrontend'): self.isfrontend = True logging.LOG_THRESHOLD *= 4 elif opt in ('-a', '--all'): self.require_all = True elif opt in ('-N', '--new'): self.servers_new_only = True elif opt == '--accept_acl_file': self.accept_acl_file = arg elif opt == '--client_acl': policy, pattern = arg.split(':', 1) self.client_acls.append((policy, pattern)) elif opt == '--tunnel_acl': policy, pattern = arg.split(':', 1) self.tunnel_acls.append((policy, pattern)) elif opt in ('--noproxy', ): self.no_proxy = True self.proxy_servers = [] socks.setdefaultproxy() elif opt in ('--proxy', '--socksify', '--torify'): if opt == '--proxy': socks.adddefaultproxy(*socks.parseproxy(arg)) else: (host, port) = arg.rsplit(':', 1) socks.adddefaultproxy(socks.PROXY_TYPE_SOCKS5, host, int(port)) if not self.proxy_servers: # Make DynDNS updates go via the proxy. socks.wrapmodule(urllib) self.proxy_servers = [arg] else: self.proxy_servers.append(arg) if opt == '--torify': self.servers_new_only = True # Disable initial DNS lookups (leaks) self.servers_no_ping = True # Disable front-end pings self.crash_report_url = None # Disable crash reports # This increases the odds of unrelated requests getting lumped # together in the tunnel, which makes traffic analysis harder. compat.SEND_ALWAYS_BUFFERS = True elif opt == '--ca_certs': self.ca_certs = arg elif opt == '--jakenoia': self.fe_anon_tls_wrap = True elif opt == '--fe_certname': if arg == '': self.fe_certname = [] else: cert = arg.lower() if cert not in self.fe_certname: self.fe_certname.append(cert) elif opt == '--service_xmlrpc': self.service_xmlrpc = arg elif opt == '--frontend': self.servers_manual.append(arg) elif opt == '--nofrontend': self.servers_never.append(arg) elif opt == '--frontends': count, domain, port = arg.split(':') self.servers_auto = (int(count), domain, int(port)) elif opt in ('--errorurl', '-E'): self.error_url = arg elif opt == '--fingerpath': self.finger_path = arg elif opt == '--kitename': self.kitename = arg elif opt == '--kitesecret': self.kitesecret = arg elif opt in ('--service_on', '--service_off', '--backend', '--define_backend'): if opt in ('--backend', '--service_on'): status = BE_STATUS_UNKNOWN else: status = BE_STATUS_DISABLED bes = self.ArgToBackendSpecs(arg.replace('@kitesecret', self.kitesecret) .replace('@kitename', self.kitename), status=status) for bid in bes: if bid in self.backends: raise ConfigError("Same service/domain defined twice: %s" % bid) if not self.kitename: self.kitename = bes[bid][BE_DOMAIN] self.kitesecret = bes[bid][BE_SECRET] self.backends.update(bes) elif opt in ('--be_config', '--service_cfg'): host, key, val = arg.split(':', 2) if key.startswith('user/'): key = key.replace('user/', 'password/') hostc = self.be_config.get(host, {}) hostc[key] = {'True': True, 'False': False, 'None': None}.get(val, val) self.be_config[host] = hostc elif opt == '--domain': protos, domain, secret = arg.split(':') if protos in ('*', ''): protos = ','.join(self.server_protos) for proto in protos.split(','): bid = '%s:%s' % (proto, domain) if bid in self.backends: raise ConfigError("Same service/domain defined twice: %s" % bid) self.backends[bid] = BE_NONE[:] self.backends[bid][BE_PROTO] = proto self.backends[bid][BE_DOMAIN] = domain self.backends[bid][BE_SECRET] = secret self.backends[bid][BE_STATUS] = BE_STATUS_UNKNOWN elif opt == '--insecure': self.insecure = True elif opt == '--noprobes': self.no_probes = True elif opt == '--nofrontend': self.isfrontend = False elif opt == '--nodaemonize': self.daemonize = False elif opt == '--noall': self.require_all = False elif opt == '--nozchunks': self.disable_zchunks = True elif opt == '--nullui': self.ui = NullUi() elif opt == '--remoteui': import pagekite.ui.remote self.ui = pagekite.ui.remote.RemoteUi() elif opt == '--uiport': self.ui_port = int(arg) elif opt == '--sslzlib': self.enable_sslzlib = True elif opt == '--watch': self.watch_level[0] = int(arg) elif opt == '--debugio': logging.DEBUG_IO = True elif opt == '--buffers': self.buffer_max = int(arg) elif opt == '--nocrashreport': self.crash_report_url = None elif opt == '--noloop': self.main_loop = False elif opt == '--local': self.SetLocalSettings([int(p) for p in arg.split(',')]) if not 'localhost' in args: args.append('localhost') elif opt == '--defaults': self.SetServiceDefaults() elif opt in ('--clean', '--nopyopenssl', '--nossl', '--settings', '--signup', '--friendly'): # These are handled outside the main loop, we just ignore them. pass elif opt in ('--webroot', '--webaccess', '--webindexes', '--noautosave', '--autosave', '--reloadfile', '--delete_backend'): # FIXME: These are deprecated, we should probably warn the user. pass elif opt == '--help': self.HelpAndExit(longhelp=True) elif opt == '--controlpanel': import webbrowser webbrowser.open(self.LoginUrl()) sys.exit(0) elif opt == '--controlpass': print self.ConfigSecret() sys.exit(0) else: self.HelpAndExit() # Make sure these are configured before we try and do XML-RPC stuff. socks.DEBUG = (logging.DEBUG_IO or socks.DEBUG) and logging.LogDebug if self.ca_certs: socks.setdefaultcertfile(self.ca_certs) # Handle the user-friendly argument stuff and simple registration. return self.ParseFriendlyBackendSpecs(args) def ParseFriendlyBackendSpecs(self, args): just_these_backends = {} just_these_webpaths = {} just_these_be_configs = {} argsets = [] while 'AND' in args: argsets.append(args[0:args.index('AND')]) args[0:args.index('AND')+1] = [] if args: argsets.append(args) for args in argsets: # Extract the config options first... be_config = [p for p in args if p.startswith('+')] args = [p for p in args if not p.startswith('+')] fe_spec = (args.pop().replace('@kitesecret', self.kitesecret) .replace('@kitename', self.kitename)) if os.path.exists(fe_spec): raise ConfigError('Is a local file: %s' % fe_spec) be_paths = [] be_path_prefix = '' if len(args) == 0: be_spec = '' elif len(args) == 1: if '*' in args[0] or '?' in args[0]: if sys.platform[:3] in ('win', 'os2'): be_paths = [args[0]] be_spec = 'builtin' elif os.path.exists(args[0]): be_paths = [args[0]] be_spec = 'builtin' else: be_spec = args[0] else: be_spec = 'builtin' be_paths = args[:] be_proto = 'http' # A sane default... if be_spec == '': be = None else: be = be_spec.replace('/', '').split(':') if be[0].lower() in ('http', 'http2', 'http3', 'https', 'httpfinger', 'finger', 'ssh', 'irc'): be_proto = be.pop(0) if len(be) < 2: be.append({'http': '80', 'http2': '80', 'http3': '80', 'https': '443', 'irc': '6667', 'httpfinger': '80', 'finger': '79', 'ssh': '22'}[be_proto]) if len(be) > 2: raise ConfigError('Bad back-end definition: %s' % be_spec) if len(be) < 2: try: if be[0] != 'builtin': int(be[0]) be = ['localhost', be[0]] except ValueError: raise ConfigError('`%s` should be a file, directory, port or ' 'protocol' % be_spec) # Extract the path prefix from the fe_spec fe_urlp = fe_spec.split('/', 3) if len(fe_urlp) == 4: fe_spec = '/'.join(fe_urlp[:3]) be_path_prefix = '/' + fe_urlp[3] fe = fe_spec.replace('/', '').split(':') if len(fe) == 3: fe = ['%s-%s' % (fe[0], fe[2]), fe[1]] elif len(fe) == 2: try: fe = ['%s-%s' % (be_proto, int(fe[1])), fe[0]] except ValueError: pass elif len(fe) == 1 and be: fe = [be_proto, fe[0]] # Do our own globbing on Windows if sys.platform[:3] in ('win', 'os2'): import glob new_paths = [] for p in be_paths: new_paths.extend(glob.glob(p)) be_paths = new_paths for f in be_paths: if not os.path.exists(f): raise ConfigError('File or directory not found: %s' % f) spec = ':'.join(fe) if be: spec += ':' + ':'.join(be) specs = self.ArgToBackendSpecs(spec) just_these_backends.update(specs) spec = specs[specs.keys()[0]] http_host = '%s/%s' % (spec[BE_DOMAIN], spec[BE_PORT] or '80') if be_config: # Map the +foo=bar values to per-site config settings. host_config = just_these_be_configs.get(http_host, {}) for cfg in be_config: if '=' in cfg: key, val = cfg[1:].split('=', 1) elif cfg.startswith('+no'): key, val = cfg[3:], False else: key, val = cfg[1:], True if ':' in key: raise ConfigError('Please do not use : in web config keys.') if key.startswith('user/'): key = key.replace('user/', 'password/') host_config[key] = val just_these_be_configs[http_host] = host_config if be_paths: host_paths = just_these_webpaths.get(http_host, {}) host_config = just_these_be_configs.get(http_host, {}) rand_seed = '%s:%x' % (specs[specs.keys()[0]][BE_SECRET], time.time()/3600) first = (len(host_paths.keys()) == 0) or be_path_prefix paranoid = host_config.get('hide', False) set_root = host_config.get('root', True) if len(be_paths) == 1: skip = len(os.path.dirname(be_paths[0])) else: skip = len(os.path.dirname(os.path.commonprefix(be_paths)+'X')) for path in be_paths: phead, ptail = os.path.split(path) if paranoid: if path.endswith('/'): path = path[0:-1] webpath = '%s/%s' % (sha1hex(rand_seed+os.path.dirname(path))[0:9], os.path.basename(path)) elif (first and set_root and os.path.isdir(path)): webpath = '' elif (os.path.isdir(path) and not path.startswith('.') and not os.path.isabs(path)): webpath = path[skip:] + '/' elif path == '.': webpath = '' else: webpath = path[skip:] while webpath.endswith('/.'): webpath = webpath[:-2] host_paths[(be_path_prefix + '/' + webpath).replace('///', '/' ).replace('//', '/') ] = (WEB_POLICY_DEFAULT, os.path.abspath(path)) first = False just_these_webpaths[http_host] = host_paths need_registration = {} for be in just_these_backends.values(): if not be[BE_SECRET]: if self.kitesecret and be[BE_DOMAIN] == self.kitename: be[BE_SECRET] = self.kitesecret elif not self.kite_remove and not self.kite_disable: need_registration[be[BE_DOMAIN]] = True for domain in need_registration: if '.' not in domain: raise ConfigError('Not valid domain: %s' % domain) for domain in need_registration: result = self.RegisterNewKite(kitename=domain) if not result: raise ConfigError("Not sure what to do with %s, giving up." % domain) # Update the secrets... rdom, rsecret = result for be in just_these_backends.values(): if be[BE_DOMAIN] == domain: be[BE_SECRET] = rsecret # Update the kite names themselves, if they changed. if rdom != domain: for bid in just_these_backends.keys(): nbid = bid.replace(':'+domain, ':'+rdom) if nbid != bid: just_these_backends[nbid] = just_these_backends[bid] just_these_backends[nbid][BE_DOMAIN] = rdom del just_these_backends[bid] if just_these_backends.keys(): if self.kite_add: self.backends.update(just_these_backends) elif self.kite_remove: try: for bid in just_these_backends: be = self.backends[bid] if be[BE_PROTO] in ('http', 'http2', 'http3'): http_host = '%s/%s' % (be[BE_DOMAIN], be[BE_PORT] or '80') if http_host in self.ui_paths: del self.ui_paths[http_host] if http_host in self.be_config: del self.be_config[http_host] del self.backends[bid] except KeyError: raise ConfigError('No such kite: %s' % bid) elif self.kite_disable: try: for bid in just_these_backends: self.backends[bid][BE_STATUS] = BE_STATUS_DISABLED except KeyError: raise ConfigError('No such kite: %s' % bid) elif self.kite_only: for be in self.backends.values(): be[BE_STATUS] = BE_STATUS_DISABLED self.backends.update(just_these_backends) else: # Nothing explictly requested: 'only' behavior with a twist; # If kites are new, don't make disables persist on save. for be in self.backends.values(): be[BE_STATUS] = (need_registration and BE_STATUS_DISABLE_ONCE or BE_STATUS_DISABLED) self.backends.update(just_these_backends) self.ui_paths.update(just_these_webpaths) self.be_config.update(just_these_be_configs) return self def GetServiceXmlRpc(self): service = self.service_xmlrpc return xmlrpclib.ServerProxy(self.service_xmlrpc, None, None, False) def _KiteInfo(self, kitename): is_service_domain = kitename and SERVICE_DOMAIN_RE.search(kitename) is_subdomain_of = is_cname_for = is_cname_ready = False secret = None for be in self.backends.values(): if be[BE_SECRET] and (be[BE_DOMAIN] == kitename): secret = be[BE_SECRET] if is_service_domain: parts = kitename.split('.') if '-' in parts[0]: parts[0] = '-'.join(parts[0].split('-')[1:]) is_subdomain_of = '.'.join(parts) elif len(parts) > 3: is_subdomain_of = '.'.join(parts[1:]) elif kitename: try: (hn, al, ips) = socket.gethostbyname_ex(kitename) if hn != kitename and SERVICE_DOMAIN_RE.search(hn): is_cname_for = hn except: pass return (secret, is_subdomain_of, is_service_domain, is_cname_for, is_cname_ready) def RegisterNewKite(self, kitename=None, first=False, ask_be=False, autoconfigure=False): registered = False if kitename: (secret, is_subdomain_of, is_service_domain, is_cname_for, is_cname_ready) = self._KiteInfo(kitename) if secret: self.ui.StartWizard('Updating kite: %s' % kitename) registered = True else: self.ui.StartWizard('Creating kite: %s' % kitename) else: if first: self.ui.StartWizard('Create your first kite') else: self.ui.StartWizard('Creating a new kite') is_subdomain_of = is_service_domain = False is_cname_for = is_cname_ready = False # This is the default... be_specs = ['http:%s:localhost:80'] service = self.GetServiceXmlRpc() service_accounts = {} if self.kitename and self.kitesecret: service_accounts[self.kitename] = self.kitesecret for be in self.backends.values(): if SERVICE_DOMAIN_RE.search(be[BE_DOMAIN]): if be[BE_DOMAIN] == is_cname_for: is_cname_ready = True if be[BE_SECRET] not in service_accounts.values(): service_accounts[be[BE_DOMAIN]] = be[BE_SECRET] service_account_list = service_accounts.keys() if registered: state = ['choose_backends'] if service_account_list: state = ['choose_kite_account'] else: state = ['use_service_question'] history = [] def Goto(goto, back_skips_current=False): if not back_skips_current: history.append(state[0]) state[0] = goto def Back(): if history: state[0] = history.pop(-1) else: Goto('abort') register = is_cname_for or kitename account = email = None while 'end' not in state: try: if 'use_service_question' in state: ch = self.ui.AskYesNo('Use the PageKite.net service?', pre=['Welcome to PageKite!', '', 'Please answer a few quick questions to', 'create your first kite.', '', 'By continuing, you agree to play nice', 'and abide by the Terms of Service at:', '- %s' % (SERVICE_TOS_URL, SERVICE_TOS_URL)], default=True, back=-1, no='Abort') if ch is True: self.SetServiceDefaults(clobber=False) if not kitename: Goto('service_signup_email') elif is_cname_for and is_cname_ready: register = kitename Goto('service_signup_email') elif is_service_domain: register = is_cname_for or kitename if is_subdomain_of: # FIXME: Shut up if parent is already in local config! Goto('service_signup_is_subdomain') else: Goto('service_signup_email') else: Goto('service_signup_bad_domain') else: Goto('manual_abort') elif 'service_login_email' in state: p = None while not email or not p: (email, p) = self.ui.AskLogin('Please log on ...', pre=[ 'By logging on to %s,' % self.service_provider, 'you will be able to use this kite', 'with your pre-existing account.' ], email=email, back=(email, False)) if email and p: try: self.ui.Working('Logging on to your account') service_accounts[email] = service.getSharedSecret(email, p) # FIXME: Should get the list of preconfigured kites via. RPC # so we don't try to create something that already # exists? Or should the RPC not just not complain? account = email Goto('create_kite') except: email = p = None self.ui.Tell(['Login failed! Try again?'], error=True) if p is False: Back() break elif ('service_signup_is_subdomain' in state): ch = self.ui.AskYesNo('Use this name?', pre=['%s is a sub-domain.' % kitename, '', 'NOTE: This process will fail if you', 'have not already registered the parent', 'domain, %s.' % is_subdomain_of], default=True, back=-1) if ch is True: if account: Goto('create_kite') elif email: Goto('service_signup') else: Goto('service_signup_email') elif ch is False: Goto('service_signup_kitename') else: Back() elif ('service_signup_bad_domain' in state or 'service_login_bad_domain' in state): if is_cname_for: alternate = is_cname_for ch = self.ui.AskYesNo('Create both?', pre=['%s is a CNAME for %s.' % (kitename, is_cname_for)], default=True, back=-1) else: alternate = kitename.split('.')[-2]+'.'+SERVICE_DOMAINS[0] ch = self.ui.AskYesNo('Try to create %s instead?' % alternate, pre=['Sorry, %s is not a valid service domain.' % kitename], default=True, back=-1) if ch is True: register = alternate Goto(state[0].replace('bad_domain', 'email')) elif ch is False: register = alternate = kitename = False Goto('service_signup_kitename', back_skips_current=True) else: Back() elif 'service_signup_email' in state: email = self.ui.AskEmail('What is your e-mail address?', pre=['We need to be able to contact you', 'now and then with news about the', 'service and your account.', '', 'Your details will be kept private.'], back=False) if email and register: Goto('service_signup') elif email: Goto('service_signup_kitename') else: Back() elif ('service_signup_kitename' in state or 'service_ask_kitename' in state): try: self.ui.Working('Fetching list of available domains') domains = service.getAvailableDomains('', '') except: domains = ['.%s' % x for x in SERVICE_DOMAINS_SIGNUP] ch = self.ui.AskKiteName(domains, 'Name this kite:', pre=['Your kite name becomes the public name', 'of your personal server or web-site.', '', 'Names are provided on a first-come,', 'first-serve basis. You can create more', 'kites with different names later on.'], back=False) if ch: kitename = register = ch (secret, is_subdomain_of, is_service_domain, is_cname_for, is_cname_ready) = self._KiteInfo(ch) if secret: self.ui.StartWizard('Updating kite: %s' % kitename) registered = True else: self.ui.StartWizard('Creating kite: %s' % kitename) Goto('choose_backends') else: Back() elif 'choose_backends' in state: if ask_be and autoconfigure: skip = False ch = self.ui.AskBackends(kitename, ['http'], ['80'], [], 'Enable which service?', back=False, pre=[ 'You control which of your files or servers', 'PageKite exposes to the Internet. ', ], default=','.join(be_specs)) if ch: be_specs = ch.split(',') else: skip = ch = True if ch: if registered: Goto('create_kite', back_skips_current=skip) elif is_subdomain_of: Goto('service_signup_is_subdomain', back_skips_current=skip) elif account: Goto('create_kite', back_skips_current=skip) elif email: Goto('service_signup', back_skips_current=skip) else: Goto('service_signup_email', back_skips_current=skip) else: Back() elif 'service_signup' in state: try: self.ui.Working('Signing up') details = service.signUp(email, register) if details.get('secret', False): service_accounts[email] = details['secret'] self.ui.AskYesNo('Continue?', pre=[ 'Your kite is ready to fly!', '', 'Note: To complete the signup process,', 'check your e-mail (and spam folders) for', 'activation instructions. You can give', 'PageKite a try first, but un-activated', 'accounts are disabled after %d minutes.' % details['timeout'], ], yes='Finish', no=False, default=True) self.ui.EndWizard() if autoconfigure: for be_spec in be_specs: self.backends.update(self.ArgToBackendSpecs( be_spec % register, secret=details['secret'])) self.added_kites = True return (register, details['secret']) else: error = details.get('error', 'unknown') except IOError: error = 'network' except: error = '%s' % (sys.exc_info(), ) if error == 'pleaselogin': self.ui.ExplainError(error, 'Signup failed!', subject=email) Goto('service_login_email', back_skips_current=True) elif error == 'email': self.ui.ExplainError(error, 'Signup failed!', subject=register) Goto('service_login_email', back_skips_current=True) elif error in ('domain', 'domaintaken', 'subdomain'): self.ui.ExplainError(error, 'Invalid domain!', subject=register) register, kitename = None, None Goto('service_signup_kitename', back_skips_current=True) elif error == 'network': self.ui.ExplainError(error, 'Network error!', subject=self.service_provider) Goto('service_signup', back_skips_current=True) else: self.ui.ExplainError(error, 'Unknown problem!') print 'FIXME! Error is %s' % error Goto('abort') elif 'choose_kite_account' in state: choices = service_account_list[:] choices.append('Use another service provider') justdoit = (len(service_account_list) == 1) if justdoit: ch = 1 else: ch = self.ui.AskMultipleChoice(choices, 'Register with', pre=['Choose an account for this kite:'], default=1) account = choices[ch-1] if ch == len(choices): Goto('manual_abort') elif kitename: Goto('choose_backends', back_skips_current=justdoit) else: Goto('service_ask_kitename', back_skips_current=justdoit) elif 'create_kite' in state: secret = service_accounts[account] subject = None cfgs = {} result = {} error = None try: if registered and kitename and secret: pass elif is_cname_for and is_cname_ready: self.ui.Working('Creating your kite') subject = kitename result = service.addCnameKite(account, secret, kitename) time.sleep(2) # Give the service side a moment to replicate... else: self.ui.Working('Creating your kite') subject = register result = service.addKite(account, secret, register) time.sleep(2) # Give the service side a moment to replicate... for be_spec in be_specs: cfgs.update(self.ArgToBackendSpecs(be_spec % register, secret=secret)) if is_cname_for == register and 'error' not in result: subject = kitename result.update(service.addCnameKite(account, secret, kitename)) error = result.get('error', None) if not error: for be_spec in be_specs: cfgs.update(self.ArgToBackendSpecs(be_spec % kitename, secret=secret)) except Exception, e: error = '%s' % e if error: self.ui.ExplainError(error, 'Kite creation failed!', subject=subject) Goto('abort') else: self.ui.Tell(['Success!']) self.ui.EndWizard() if autoconfigure: self.backends.update(cfgs) self.added_kites = True return (register or kitename, secret) elif 'manual_abort' in state: if self.ui.Tell(['Aborted!', '', 'Please manually add information about your', 'kites and front-ends to the configuration file:', '', ' %s' % self.rcfile], error=True, back=False) is False: Back() else: self.ui.EndWizard() if self.ui.ALLOWS_INPUT: return None sys.exit(0) elif 'abort' in state: self.ui.EndWizard() if self.ui.ALLOWS_INPUT: return None sys.exit(0) else: raise ConfigError('Unknown state: %s' % state) except KeyboardInterrupt: sys.stderr.write('\n') if history: Back() else: raise KeyboardInterrupt() self.ui.EndWizard() return None def CheckConfig(self): if self.ui_sspec: self.BindUiSspec() if not self.servers_manual and not self.servers_auto and not self.isfrontend: if not self.servers and not self.ui.ALLOWS_INPUT: raise ConfigError('Nothing to do! List some servers, or run me as one.') return self def CheckAllTunnels(self, conns): missing = [] for backend in self.backends: proto, domain = backend.split(':') if not conns.Tunnel(proto, domain): missing.append(domain) if missing: self.FallDown('No tunnel for %s' % missing, help=False) TMP_UUID_MAP = { '2400:8900::f03c:91ff:feae:ea35:443': '106.187.99.46:443', '2a01:7e00::f03c:91ff:fe96:234:443': '178.79.140.143:443', '2600:3c03::f03c:91ff:fe96:2bf:443': '50.116.52.206:443', '2600:3c01::f03c:91ff:fe96:257:443': '173.230.155.164:443', '69.164.211.158:443': '50.116.52.206:443', } def Ping(self, host, port): cid = uuid = '%s:%s' % (host, port) if self.servers_no_ping: return (0, uuid) while ((cid not in self.ping_cache) or (len(self.ping_cache[cid]) < 2) or (time.time()-self.ping_cache[cid][0][0] > 60)): start = time.time() try: try: if ':' in host: fd = socks.socksocket(socket.AF_INET6, socket.SOCK_STREAM) else: fd = socks.socksocket(socket.AF_INET, socket.SOCK_STREAM) except: fd = socks.socksocket(socket.AF_INET, socket.SOCK_STREAM) try: fd.settimeout(3.0) # Missing in Python 2.2 except: fd.setblocking(1) fd.connect((host, port)) fd.send('HEAD / HTTP/1.0\r\n\r\n') data = fd.recv(1024) fd.close() except Exception, e: logging.LogDebug('Ping %s:%s failed: %s' % (host, port, e)) return (100000, uuid) elapsed = (time.time() - start) try: uuid = data.split('X-PageKite-UUID: ')[1].split()[0] except: uuid = self.TMP_UUID_MAP.get(uuid, uuid) if cid not in self.ping_cache: self.ping_cache[cid] = [] elif len(self.ping_cache[cid]) > 10: self.ping_cache[cid][8:] = [] self.ping_cache[cid][0:0] = [(time.time(), (elapsed, uuid))] window = min(3, len(self.ping_cache[cid])) pingval = sum([e[1][0] for e in self.ping_cache[cid][:window]])/window uuid = self.ping_cache[cid][0][1][1] logging.LogDebug(('Pinged %s:%s: %f [win=%s, uuid=%s]' ) % (host, port, pingval, window, uuid)) return (pingval, uuid) def GetHostIpAddrs(self, host): rv = [] try: info = socket.getaddrinfo(host, 0, socket.AF_UNSPEC, socket.SOCK_STREAM) rv = [i[4][0] for i in info] except AttributeError: rv = socket.gethostbyname_ex(host)[2] return rv def CachedGetHostIpAddrs(self, host): now = int(time.time()) if host in self.dns_cache: # FIXME: This number (900) is 3x the pagekite.net service DNS TTL, which # should be about right. BUG: nothing keeps those two numbers in sync! # This number must be larger, or we prematurely disconnect frontends. for exp in [t for t in self.dns_cache[host] if t < now-900]: del self.dns_cache[host][exp] else: self.dns_cache[host] = {} try: self.dns_cache[host][now] = self.GetHostIpAddrs(host) except: logging.LogDebug('DNS lookup failed for %s' % host) ips = {} for ipaddrs in self.dns_cache[host].values(): for ip in ipaddrs: ips[ip] = 1 return ips.keys() def GetActiveBackends(self): active = [] for bid in self.backends: (proto, bdom) = bid.split(':') if (self.backends[bid][BE_STATUS] not in BE_INACTIVE and self.backends[bid][BE_SECRET] and not bdom.startswith('*')): active.append(bid) return active def ChooseFrontEnds(self): self.servers = [] self.servers_preferred = [] self.last_frontend_choice = time.time() servers_all = {} servers_pref = {} # Enable internal loopback if self.isfrontend: need_loopback = False for be in self.backends.values(): if be[BE_BHOST]: need_loopback = True if need_loopback: servers_all['loopback'] = servers_pref['loopback'] = LOOPBACK_FE # Convert the hostnames into IP addresses... def sping(server): (host, port) = server.split(':') ipaddrs = self.CachedGetHostIpAddrs(host) if ipaddrs: ptime, uuid = self.Ping(ipaddrs[0], int(port)) server = '%s:%s' % (ipaddrs[0], port) if server not in self.servers_never: servers_all[uuid] = servers_pref[uuid] = server threads, deadline = [], time.time() + 5 for server in self.servers_manual: threads.append(threading.Thread(target=sping, args=(server,))) threads[-1].daemon = True threads[-1].start() for t in threads: t.join(max(0.1, deadline - time.time())) # Lookup and choose from the auto-list (and our old domain). if self.servers_auto: (count, domain, port) = self.servers_auto # First, check for old addresses and always connect to those. selected = {} if not self.servers_new_only: def bping(bid): (proto, bdom) = bid.split(':') for ip in self.CachedGetHostIpAddrs(bdom): # FIXME: What about IPv6 localhost? if not ip.startswith('127.'): server = '%s:%s' % (ip, port) if server not in self.servers_never: servers_all[self.Ping(ip, int(port))[1]] = server threads, deadline = [], time.time() + 5 for bid in self.GetActiveBackends(): threads.append(threading.Thread(target=bping, args=(bid,))) threads[-1].daemon = True threads[-1].start() for t in threads: t.join(max(0.1, deadline - time.time())) try: pings = [] ips = [ip for ip in self.CachedGetHostIpAddrs(domain) if ('%s:%s' % (ip, port)) not in self.servers_never] def iping(ip): pings.append(self.Ping(ip, port)) threads, deadline = [], time.time() + 5 for ip in ips: threads.append(threading.Thread(target=iping, args=(ip,))) threads[-1].daemon = True threads[-1].start() for t in threads: t.join(max(0.1, deadline - time.time())) except Exception, e: logging.LogDebug('Unreachable: %s, %s' % (domain, e)) ips = pings = [] while count > 0 and ips and pings: mIdx = pings.index(min(pings)) if pings[mIdx][0] > 60: # This is worthless data, abort. break else: count -= 1 uuid = pings[mIdx][1] server = '%s:%s' % (ips[mIdx], port) if uuid not in servers_all: servers_all[uuid] = server if uuid not in servers_pref: servers_pref[uuid] = ips[mIdx] del pings[mIdx] del ips[mIdx] self.servers = servers_all.values() self.servers_preferred = servers_pref.values() logging.LogDebug('Preferred: %s' % ', '.join(self.servers_preferred)) def ConnectFrontend(self, conns, server): self.ui.Status('connect', color=self.ui.YELLOW, message='Front-end connect: %s' % server) tun = Tunnel.BackEnd(server, self.backends, self.require_all, conns) if tun: tun.filters.append(HttpHeaderFilter(self.ui)) if not self.insecure: tun.filters.append(HttpSecurityFilter(self.ui)) if self.watch_level[0] is not None: tun.filters.append(TunnelWatcher(self.ui, self.watch_level)) logging.Log([('connect', server)]) return True else: logging.LogInfo('Failed to connect', [('FE', server)]) self.ui.Notify('Failed to connect to %s' % server, prefix='!', color=self.ui.YELLOW) return False def DisconnectFrontend(self, conns, server): logging.Log([('disconnect', server)]) kill = [] for bid in conns.tunnels: for tunnel in conns.tunnels[bid]: if (server == tunnel.server_info[tunnel.S_NAME] and tunnel.countas.startswith('frontend')): kill.append(tunnel) for tunnel in kill: if len(tunnel.users.keys()) < 1: tunnel.Die() return kill and True or False def CreateTunnels(self, conns): live_servers = conns.TunnelServers() failures = 0 connections = 0 if len(self.GetActiveBackends()) > 0: if self.last_frontend_choice < time.time()-FE_PING_INTERVAL: self.servers = [] if not self.servers or len(self.servers) > len(live_servers): self.ChooseFrontEnds() else: self.servers_preferred = [] self.servers = [] if not self.servers: logging.LogDebug('Not sure which servers to contact, making no changes.') return 0, 0 threads, deadline = [], time.time() + 120 def connect_in_thread(conns, server, state): try: state[1] = self.ConnectFrontend(conns, server) except (IOError, OSError): state[1] = False for server in self.servers: if server not in live_servers: if server == LOOPBACK_FE: loop = LoopbackTunnel.Loop(conns, self.backends) loop.filters.append(HttpHeaderFilter(self.ui)) if not self.insecure: loop.filters.append(HttpSecurityFilter(self.ui)) else: state = [None, None] state[0] = threading.Thread(target=connect_in_thread, args=(conns, server, state)) state[0].daemon = True state[0].start() threads.append(state) for thread, result in threads: thread.join(max(0.1, deadline - time.time())) for thread, result in threads: # This will treat timeouts both as connections AND failures if result is not False: connections += 1 if result is not True: failures += 1 for server in live_servers: if server not in self.servers and server not in self.servers_preferred: if self.DisconnectFrontend(conns, server): connections += 1 if self.dyndns: ddns_fmt, ddns_args = self.dyndns domains = {} for bid in self.backends.keys(): proto, domain = bid.split(':') if domain not in domains: domains[domain] = (self.backends[bid][BE_SECRET], []) if bid in conns.tunnels: ips, bips = [], [] for tunnel in conns.tunnels[bid]: ip = rsplit(':', tunnel.server_info[tunnel.S_NAME])[0] if not ip == LOOPBACK_HN and not tunnel.read_eof: if not self.servers_preferred or ip in self.servers_preferred: ips.append(ip) else: bips.append(ip) for ip in (ips or bips): if ip not in domains[domain]: domains[domain][1].append(ip) updates = {} for domain, (secret, ips) in domains.iteritems(): if ips: iplist = ','.join(ips) payload = '%s:%s' % (domain, iplist) args = {} args.update(ddns_args) args.update({ 'domain': domain, 'ip': ips[0], 'ips': iplist, 'sign': signToken(secret=secret, payload=payload, length=100) }) # FIXME: This may fail if different front-ends support different # protocols. In practice, this should be rare. updates[payload] = ddns_fmt % args last_updates = self.last_updates self.last_updates = [] for update in updates: if update in last_updates: # Was successful last time, no point in doing it again. self.last_updates.append(update) else: domain, ips = update.split(':', 1) try: self.ui.Status('dyndns', color=self.ui.YELLOW, message='Updating DNS for %s...' % domain) # FIXME: If the network misbehaves, can this stall forever? result = ''.join(urllib.urlopen(updates[update]).readlines()) if result.startswith('good') or result.startswith('nochg'): logging.Log([('dyndns', result), ('data', update)]) self.SetBackendStatus(update.split(':')[0], sub=BE_STATUS_ERR_DNS) self.last_updates.append(update) # Success! Make sure we remember these IP were live. if domain not in self.dns_cache: self.dns_cache[domain] = {} self.dns_cache[domain][int(time.time())] = ips.split(',') else: logging.LogInfo('DynDNS update failed: %s' % result, [('data', update)]) self.SetBackendStatus(update.split(':')[0], add=BE_STATUS_ERR_DNS) failures += 1 except Exception, e: logging.LogInfo('DynDNS update failed: %s' % e, [('data', update)]) if logging.DEBUG_IO: traceback.print_exc(file=sys.stderr) self.SetBackendStatus(update.split(':')[0], add=BE_STATUS_ERR_DNS) # Hmm, the update may have succeeded - assume the "worst". self.dns_cache[domain][int(time.time())] = ips.split(',') failures += 1 return failures, connections def LogTo(self, filename, close_all=True, dont_close=[]): if filename == 'memory': logging.Log = logging.LogToMemory filename = self.devnull elif filename == 'syslog': logging.Log = logging.LogSyslog filename = self.devnull compat.syslog.openlog(self.progname, syslog.LOG_PID, syslog.LOG_DAEMON) else: logging.Log = logging.LogToFile if filename in ('stdio', 'stdout'): try: logging.LogFile = os.fdopen(sys.stdout.fileno(), 'w', 0) except: logging.LogFile = sys.stdout else: try: logging.LogFile = fd = open(filename, "a", 0) os.dup2(fd.fileno(), sys.stdout.fileno()) if not self.ui.WANTS_STDERR: os.dup2(fd.fileno(), sys.stdin.fileno()) os.dup2(fd.fileno(), sys.stderr.fileno()) except Exception, e: raise ConfigError('%s' % e) def Daemonize(self): # Fork once... if os.fork() != 0: os._exit(0) # Fork twice... os.setsid() if os.fork() != 0: os._exit(0) def ProcessWritable(self, oready): if logging.DEBUG_IO: print '\n=== Ready for Write: %s' % [o and o.fileno() or '' for o in oready] for osock in oready: if osock: conn = self.conns.Connection(osock) if conn and not conn.Send([], try_flush=True): conn.Die(discard_buffer=True) def ProcessReadable(self, iready, throttle): if logging.DEBUG_IO: print '\n=== Ready for Read: %s' % [i and i.fileno() or None for i in iready] for isock in iready: if isock is not None: conn = self.conns.Connection(isock) if conn and not (conn.fd and conn.ReadData(maxread=throttle)): conn.Die(discard_buffer=True) def ProcessDead(self, epoll=None): for conn in self.conns.DeadConns(): if epoll and conn.fd: try: epoll.unregister(conn.fd) except (IOError, TypeError): pass conn.Cleanup() self.conns.Remove(conn) def Select(self, epoll, waittime): iready = oready = eready = None isocks, osocks = self.conns.Readable(), self.conns.Blocked() try: if isocks or osocks: iready, oready, eready = select.select(isocks, osocks, [], waittime) else: # Windoes does not seem to like empty selects, so we do this instead. time.sleep(waittime/2) except KeyboardInterrupt: raise except: logging.LogError('Error in select(%s/%s): %s' % (isocks, osocks, format_exc())) self.conns.CleanFds() self.last_loop -= 1 now = time.time() if not iready and not oready: if (isocks or osocks) and (now < self.last_loop + 1): logging.LogError('Spinning, pausing ...') time.sleep(0.1) return None, iready, oready, eready def Epoll(self, epoll, waittime): fdc = {} now = time.time() evs = [] broken = False try: bbc = 0 for c in self.conns.conns: fd, mask = c.fd, 0 if not c.IsDead(): if c.IsBlocked(): bbc += len(c.write_blocked) mask |= select.EPOLLOUT if c.IsReadable(now): mask |= select.EPOLLIN if mask: try: fdc[fd.fileno()] = fd except socket.error: # If this fails, then the socket has HUPed, however we need to # bypass epoll to make sure that's reflected in iready below. bid = 'dead-%d' % len(evs) fdc[bid] = fd evs.append((bid, select.EPOLLHUP)) # Trigger removal of c.fd, if it was still in the epoll. fd, mask = None, 0 if mask: try: epoll.modify(fd, mask) except IOError: try: epoll.register(fd, mask) except (IOError, TypeError): evs.append((fd, select.EPOLLHUP)) # Error == HUP else: try: epoll.unregister(c.fd) # Important: Use c.fd, not fd! except (IOError, TypeError): # Failing to unregister is OK, ignore pass common.buffered_bytes[0] = bbc evs.extend(epoll.poll(waittime)) except (IOError, OSError): broken = 'in poll' except KeyboardInterrupt: epoll.close() raise rmask = select.EPOLLIN | select.EPOLLHUP iready = [fdc.get(e[0]) for e in evs if e[1] & rmask] oready = [fdc.get(e[0]) for e in evs if e[1] & select.EPOLLOUT] if not broken and ((None in iready) or (None in oready)): broken = 'unknown FDs' if broken: logging.LogError('Epoll appears to be broken (%s), recreating' % broken) try: epoll.close() except (IOError, OSError, TypeError, AttributeError): pass epoll = select.epoll() return epoll, iready, oready, [] def CreatePollObject(self): try: epoll = select.epoll() mypoll = self.Epoll except: epoll = None mypoll = self.Select return epoll, mypoll def Loop(self): self.conns.start(auth_thread_count=self.auth_threads) if self.ui_httpd: self.ui_httpd.start() if self.tunnel_manager: self.tunnel_manager.start() if self.ui_comm: self.ui_comm.start() epoll, mypoll = self.CreatePollObject() self.last_barf = self.last_loop = time.time() logging.LogDebug('Entering main %s loop' % (epoll and 'epoll' or 'select')) loop_count = 0 while self.keep_looping: epoll, iready, oready, eready = mypoll(epoll, 1.1) now = time.time() if oready: self.ProcessWritable(oready) if common.buffered_bytes[0] < 1024 * self.buffer_max: throttle = None else: logging.LogDebug("FIXME: Nasty pause to let buffers clear!") time.sleep(0.1) throttle = 1024 if iready: self.ProcessReadable(iready, throttle) self.ProcessDead(epoll) self.last_loop = now loop_count += 1 if now - self.last_barf > (logging.DEBUG_IO and 15 or 600): self.last_barf = now if epoll: epoll.close() epoll, mypoll = self.CreatePollObject() logging.LogDebug('Loop #%d, selectable map: %s' % (loop_count, SELECTABLES)) if epoll: epoll.close() def Start(self, howtoquit='CTRL+C = Stop'): conns = self.conns = self.conns or Connections(self) # If we are going to spam stdout with ugly crap, then there is no point # attempting the fancy stuff. This also makes us backwards compatible # for the most part. if self.logfile == 'stdio': if not self.ui.DAEMON_FRIENDLY: self.ui = NullUi() # Announce that we've started up! self.ui.Status('startup', message='Starting up...') self.ui.Notify(('Hello! This is %s v%s.' ) % (self.progname, APPVER), prefix='>', color=self.ui.GREEN, alignright='[%s]' % howtoquit) config_report = [('started', sys.argv[0]), ('version', APPVER), ('platform', sys.platform), ('argv', ' '.join(sys.argv[1:])), ('ca_certs', self.ca_certs)] for optf in self.rcfiles_loaded: config_report.append(('optfile_%s' % optf, 'ok')) logging.Log(config_report) if not socks.HAVE_SSL: self.ui.Notify('SECURITY WARNING: No SSL support was found, tunnels are insecure!', prefix='!', color=self.ui.WHITE) self.ui.Notify('Please install either pyOpenSSL or python-ssl.', prefix='!', color=self.ui.WHITE) # Create global secret self.ui.Status('startup', message='Collecting entropy for a secure secret...') logging.LogInfo('Collecting entropy for a secure secret.') globalSecret() self.ui.Status('startup', message='Starting up...') # Create the UI Communicator self.ui_comm = UiCommunicator(self, conns) try: # Set up our listeners if we are a server. if self.isfrontend: self.ui.Notify('This is a PageKite front-end server.') for port in self.server_ports: Listener(self.server_host, port, conns, acl=self.accept_acl_file) for port in self.server_raw_ports: if port != VIRTUAL_PN and port > 0: Listener(self.server_host, port, conns, connclass=RawConn, acl=self.accept_acl_file) if self.ui_port: Listener('127.0.0.1', self.ui_port, conns, connclass=UiConn, acl=self.accept_acl_file) # Create the Tunnel Manager self.tunnel_manager = TunnelManager(self, conns) except Exception, e: self.LogTo('stdio') logging.FlushLogMemory() if logging.DEBUG_IO: traceback.print_exc(file=sys.stderr) raise ConfigError('Configuring listeners: %s ' % e) # Configure logging if self.logfile: keep_open = [s.fd.fileno() for s in conns.conns] if self.ui_httpd: keep_open.append(self.ui_httpd.httpd.socket.fileno()) self.LogTo(self.logfile, dont_close=keep_open) elif not (hasattr(sys.stdout, 'isatty') and sys.stdout.isatty()): # Preserve sane behavior when not run at the console. self.LogTo('stdio') # Flush in-memory log, if necessary logging.FlushLogMemory() # Set up SIGHUP handler. if self.logfile: try: import signal def reopen(x,y): if self.logfile: self.LogTo(self.logfile, close_all=False) logging.LogDebug('SIGHUP received, reopening: %s' % self.logfile) signal.signal(signal.SIGHUP, reopen) except Exception: logging.LogError('Warning: signal handler unavailable, logrotate will not work.') # Disable compression in OpenSSL if socks.HAVE_SSL and not self.enable_sslzlib: socks.DisableSSLCompression() # Daemonize! if self.daemonize: self.Daemonize() # Create PID file if self.pidfile: pf = open(self.pidfile, 'w') pf.write('%s\n' % os.getpid()) pf.close() # Do this after creating the PID and log-files. if self.daemonize: os.chdir('/') # Drop privileges, if we have any. if self.setgid: os.setgid(self.setgid) if self.setuid: os.setuid(self.setuid) if self.setuid or self.setgid: logging.Log([('uid', os.getuid()), ('gid', os.getgid())]) # Make sure we have what we need if self.require_all: self.CreateTunnels(conns) self.CheckAllTunnels(conns) # Finally, run our select loop. self.Loop() self.ui.Status('exiting', message='Stopping...') logging.Log([('stopping', 'pagekite.py')]) if self.ui_httpd: self.ui_httpd.quit() if self.ui_comm: self.ui_comm.quit() if self.tunnel_manager: self.tunnel_manager.quit() if self.conns: if self.conns.auth_pool: for th in self.conns.auth_pool: th.quit() for conn in self.conns.conns: conn.Cleanup() ##[ Main ]##################################################################### def Main(pagekite, configure, uiclass=NullUi, progname=None, appver=APPVER, http_handler=None, http_server=None): crashes = 0 shell_mode = None while True: ui = uiclass() logging.ResetLog() pk = pagekite(ui=ui, http_handler=http_handler, http_server=http_server) try: try: try: configure(pk) except SystemExit, status: sys.exit(status) except Exception, e: if logging.DEBUG_IO: raise raise ConfigError(e) shell_mode = shell_mode or pk.shell if shell_mode is not True: pk.Start() except (ConfigError, getopt.GetoptError), msg: pk.FallDown(msg, help=(not shell_mode), noexit=shell_mode) if shell_mode: shell_mode = 'more' except KeyboardInterrupt, msg: pk.FallDown(None, help=False, noexit=True) if shell_mode: shell_mode = 'auto' else: return except SystemExit, status: if shell_mode: shell_mode = 'more' else: sys.exit(status) except Exception, msg: traceback.print_exc(file=sys.stderr) if pk.crash_report_url: try: print 'Submitting crash report to %s' % pk.crash_report_url logging.LogDebug(''.join(urllib.urlopen(pk.crash_report_url, urllib.urlencode({ 'platform': sys.platform, 'appver': APPVER, 'crash': format_exc() })).readlines())) except Exception, e: print 'FAILED: %s' % e pk.FallDown(msg, help=False, noexit=pk.main_loop) crashes = min(9, crashes+1) if shell_mode: crashes = 0 try: sys.argv[1:] = Shell(pk, ui, shell_mode) shell_mode = 'more' except (KeyboardInterrupt, IOError, OSError): ui.Status('quitting') print return elif not pk.main_loop: return # Exponential fall-back. logging.LogDebug('Restarting in %d seconds...' % (2 ** crashes)) time.sleep(2 ** crashes) def Shell(pk, ui, shell_mode): import manual try: ui.Reset() if shell_mode != 'more': ui.StartWizard('The PageKite Shell') pre = [ 'Press ENTER to fly your kites or CTRL+C to quit. Or, type some', 'arguments to and try other things. Type `help` for help.' ] else: pre = '' prompt = os.path.basename(sys.argv[0]) while True: rv = ui.AskQuestion(prompt, prompt=' $', back=False, pre=pre ).strip().split() ui.EndWizard(quietly=True) while rv and rv[0] in ('pagekite.py', prompt): rv.pop(0) if rv and rv[0] == 'help': ui.welcome = '>>> ' + ui.WHITE + ' '.join(rv) + ui.NORM ui.Tell(manual.HELP(rv[1:]).splitlines()) pre = [] elif rv and rv[0] == 'quit': raise KeyboardInterrupt() else: if rv and rv[0] in OPT_ARGS: rv[0] = '--'+rv[0] return rv finally: ui.EndWizard(quietly=True) print def Configure(pk): if '--appver' in sys.argv: print '%s' % APPVER sys.exit(0) if '--clean' not in sys.argv and '--help' not in sys.argv: if os.path.exists(pk.rcfile): pk.ConfigureFromFile() friendly_mode = (('--friendly' in sys.argv) or (sys.platform[:3] in ('win', 'os2', 'dar'))) if friendly_mode and hasattr(sys.stdout, 'isatty') and sys.stdout.isatty(): pk.shell = (len(sys.argv) < 2) and 'auto' pk.Configure(sys.argv[1:]) if '--settings' in sys.argv: pk.PrintSettings(safe=True) sys.exit(0) if not pk.backends.keys() and (not pk.kitesecret or not pk.kitename): if '--signup' in sys.argv or friendly_mode: pk.RegisterNewKite(autoconfigure=True, first=True) if friendly_mode: pk.save = True pk.CheckConfig() if pk.added_kites: if (pk.save or pk.ui.AskYesNo('Save settings to %s?' % pk.rcfile, default=(len(pk.backends.keys()) > 0))): pk.SaveUserConfig() pk.servers_new_only = 'Once' elif pk.save: pk.SaveUserConfig(quiet=True) if ('--list' in sys.argv or pk.kite_add or pk.kite_remove or pk.kite_only or pk.kite_disable): pk.ListKites() sys.exit(0) pagekite-0.5.8a/debian/pagekite/usr/share/pyshared/pagekite/manual.py0000644000175000017500000005257512603542201025216 0ustar brebre00000000000000#!/usr/bin/env python """ The program manual! """ import re import time from common import * from compat import ts_to_iso MAN_NAME = ("""\ pagekite.py v%s - Make localhost servers publicly visible """ % APPVER) MAN_SYNOPSIS = ("""\ pagekite.py [--options] [service] kite-name [+flags] """) MAN_DESCRIPTION = ("""\ PageKite is a system for exposing localhost servers to the public Internet. It is most commonly used to make local web servers or SSH servers publicly visible, although almost any TCP-based protocol can work if the client knows how to use an HTTP proxy. PageKite uses a combination of tunnels and reverse proxies to compensate for the fact that localhost usually does not have a public IP address and is often subject to adverse network conditions, including aggressive firewalls and multiple layers of NAT. This program implements both ends of the tunnel: the local "back-end" and the remote "front-end" reverse-proxy relay. For convenience, pagekite.py also includes a basic HTTP server for quickly exposing files and directories to the World Wide Web for casual sharing and collaboration. """) MAN_EXAMPLES = ("""\
    Basic usage, gives http://localhost:80/ a public name:
        $ pagekite.py NAME.pagekite.me
    
        To expose specific folders, files or use alternate local ports:
        $ pagekite.py /a/path/ NAME.pagekite.me +indexes  # built-in HTTPD
        $ pagekite.py *.html   NAME.pagekite.me           # built-in HTTPD
        $ pagekite.py 3000     NAME.pagekite.me           # HTTPD on 3000
    
        To expose multiple local servers (SSH and HTTP):
        $ pagekite.py ssh://NAME.pagekite.me AND 3000 NAME.pagekite.me
    """) MAN_KITES = ("""\ The most comman usage of pagekite.py is as a back-end, where it is used to expose local services to the outside world. Examples of services are: a local HTTP server, a local SSH server, a folder or a file. A service is exposed by describing it on the command line, along with the desired public kite name. If a kite name is requested which does not already exist in the configuration file and program is run interactively, the user will be prompted and given the option of signing up and/or creating a new kite using the pagekite.net service. Multiple services and kites can be specified on a single command-line, separated by the word 'AND' (note capital letters are required). This may cause problems if you have many files and folders by that name, but that should be relatively rare. :-) """) MAN_KITE_EXAMPLES = ("""\ The options --list, --add, --disable and \ --remove can be used to manipulate the kites and service definitions in your configuration file, if you prefer not to edit it by hand. Examples:
    Adding new kites
        $ pagekite.py --add /a/path/ NAME.pagekite.me +indexes
        $ pagekite.py --add 80 OTHER-NAME.pagekite.me
    
        To display the current configuration
        $ pagekite.py --list
    
        Disable or delete kites (--add re-enables)
        $ pagekite.py --disable OTHER-NAME.pagekite.me
        $ pagekite.py --remove NAME.pagekite.me
    """) MAN_FLAGS = ("""\ Flags are used to tune the behavior of a particular kite, for example by enabling access controls or specific features of the built-in HTTP server. """) MAN_FLAGS_COMMON = ("""\ +ip/1.2.3.4 __Enable connections only from this IP address. +ip/1.2.3 __Enable connections only from this /24 netblock. """) MAN_FLAGS_HTTP = ("""\ +password/name=pass Require a username and password (HTTP Basic Authentication) +rewritehost __Rewrite the incoming Host: header. +rewritehost=N __Replace Host: header value with N. +rawheaders __Do not rewrite (or add) any HTTP headers at all. +insecure __Allow access to phpMyAdmin, /admin, etc. (per kite). """) MAN_FLAGS_BUILTIN = ("""\ +indexes __Enable directory indexes. +indexes=all __Enable directory indexes including hidden (dot-) files. +hide __Obfuscate URLs of shared files. +cgi=list A list of extensions, for which files should be treated as CGI scripts (example: +cgi=cgi,pl,sh). """) MAN_OPTIONS = ("""\ The full power of pagekite.py lies in the numerous options which can be specified on the command line or in a configuration file (see below). Note that many options, especially the service and domain definitions, are additive and if given multiple options the program will attempt to obey them all. Options are processed in order and if they are not additive then the last option will override all preceding ones. Although pagekite.py accepts a great many options, most of the time the program defaults will Just Work. """) MAN_OPT_COMMON = ("""\ --clean __Skip loading the default configuration file. --signup __Interactively sign up for pagekite.net service. --defaults __Set defaults for use with pagekite.net service. --nocrashreport __Don't send anonymous crash reports to pagekite.net. """) MAN_OPT_BACKEND = ("""\ --shell __Run PageKite in an interactive shell. --nullui __Silent UI for scripting. Assumes Yes on all questions. --list __List all configured kites. --add __Add (or enable) the following kites, save config. --remove __Remove the following kites, save config. --disable __Disable the following kites, save config. --only __Disable all but the following kites, save config. --insecure __Allow access to phpMyAdmin, /admin, etc. (global). --local=ports __Configure for local serving only (no remote front-end). --watch=N __Display proxied data (higher N = more verbosity). --noproxy __Ignore system (or config file) proxy settings. --proxy=type:server:port,\ --socksify=server:port,\ --torify=server:port __ Connect to the front-ends using SSL, an HTTP proxy, a SOCKS proxy, or the Tor anonymity network. The type can be any of 'ssl', 'http' or 'socks5'. The server name can either be a plain hostname, user@hostname or user:password@hostname. For SSL connections the user part may be a path to a client cert PEM file. If multiple proxies are defined, they will be chained one after another. --service_on=proto:kitename:host:port:secret __ Explicit configuration for a service kite. Generally kites are created on the command-line using the service short-hand described above, but this syntax is used in the config file. --service_off=proto:kitename:host:port:secret __ Same as --service_on, except disabled by default. --service_cfg=..., --webpath=... __ These options are used in the configuration file to store service and flag settings (see above). These are both likely to change in the near future, so please just pretend you didn't notice them. --frontend=host:port __ Connect to the named front-end server. If this option is repeated, multiple connections will be made. --frontends=num:dns-name:port __ Choose num front-ends from the A records of a DNS domain name, using the given port number. Default behavior is to probe all addresses and use the fastest one. --nofrontend=ip:port __ Never connect to the named front-end server. This can be used to exclude some front-ends from auto-configuration. --fe_certname=domain __ Connect using SSL, accepting valid certs for this domain. If this option is repeated, any of the named certificates will be accepted, but the first will be preferred. --ca_certs=/path/to/file __ Path to your trusted root SSL certificates file. --dyndns=X __ Register changes with DynDNS provider X. X can either be simply the name of one of the 'built-in' providers, or a URL format string for ad-hoc updating. --all __Terminate early if any tunnels fail to register. --new __Don't attempt to connect to any kites' old front-ends. --fingerpath=P __Path recipe for the httpfinger back-end proxy. --noprobes __Reject all probes for service state. """) MAN_OPT_FRONTEND = ("""\ --isfrontend __Enable front-end operation. --domain=proto,proto2,pN:domain:secret __ Accept tunneling requests for the named protocols and specified domain, using the given secret. A * may be used as a wildcard for subdomains or protocols. --authdomain=auth-domain,\ --authdomain=target-domain:auth-domain __ Use auth-domain as a remote authentication server for the DNS-based authetication protocol. If no target-domain is given, use this as the default authentication method. --motd=/path/to/motd __ Send the contents of this file to new back-ends as a "message of the day". --host=hostname __Listen on the given hostname only. --ports=list __Listen on a comma-separated list of ports. --portalias=A:B __Report port A as port B to backends (because firewalls). --protos=list __Accept the listed protocols for tunneling. --rawports=list __ Listen for raw connections these ports. The string '%s' allows arbitrary ports in HTTP CONNECT. --accept_acl_file=/path/to/file __ Consult an external access control file before accepting an incoming connection. Quick'n'dirty for mitigating abuse. The format is one rule per line: `rule policy comment` where a rule is an IP or regexp and policy is 'allow' or 'deny'. --client_acl=policy:regexp,\ --tunnel_acl=policy:regexp __ Add a client connection or tunnel access control rule. Policies should be 'allow' or 'deny', the regular expression should be written to match IPv4 or IPv6 addresses. If defined, access rules are checkd in order and if none matches, incoming connections will be rejected. --tls_default=name __ Default name to use for SSL, if SNI (Server Name Indication) is missing from incoming HTTPS connections. --tls_endpoint=name:/path/to/file __ Terminate SSL/TLS for a name using key/cert from a file. """) MAN_OPT_SYSTEM = ("""\ --optfile=/path/to/file __ Read settings from file X. Default is ~/.pagekite.rc. --optdir=/path/to/directory __ Read settings from /path/to/directory/*.rc, in lexicographical order. --savefile=/path/to/file __ Saved settings will be written to this file. --save __Save the current configuration to the savefile. --settings __ Dump the current settings to STDOUT, formatted as a configuration file would be. --nozchunks __Disable zlib tunnel compression. --sslzlib __Enable zlib compression in OpenSSL. --buffers=N __Buffer at most N kB of data before blocking. --logfile=F __Log to file F, stdio means standard output. --daemonize __Run as a daemon. --runas=U:G __Set UID:GID after opening our listening sockets. --pidfile=P __Write PID to the named file. --errorurl=U __URL to redirect to when back-ends are not found. --selfsign __ Configure the built-in HTTP daemon for HTTPS, first generating a new self-signed certificate using openssl if necessary. --httpd=X:P,\ --httppass=X,\ --pemfile=X __ Configure the built-in HTTP daemon. These options are likely to change in the near future, please pretend you didn't see them. """) MAN_CONFIG_FILES = ("""\ If you are using pagekite.py as a command-line utility, it will load its configuration from a file in your home directory. The file is named .pagekite.rc on Unix systems (including Mac OS X), or pagekite.cfg on Windows. If you are using pagekite.py as a system-daemon which starts up when your computer boots, it is generally configured to load settings from /etc/pagekite.d/*.rc (in lexicographical order). In both cases, the configuration files contain one or more of the same options as are used on the command line, with the difference that at most one option may be present on each line, and the parser is more tolerant of white-space. The leading '--' may also be omitted for readability and blank lines and lines beginning with '#' are treated as comments. NOTE: When using -o, --optfile or --optdir on the command line, it is advisable to use --clean to suppress the default configuration. """) MAN_SECURITY = ("""\ Please keep in mind, that whenever exposing a server to the public Internet, it is important to think about security. Hacked webservers are frequently abused as part of virus, spam or phishing campaigns and in some cases security breaches can compromise the entire operating system. Some advice:
           * Switch PageKite off when not using it.
           * Use the built-in access controls and SSL encryption.
           * Leave the firewall enabled unless you have good reason not to.
           * Make sure you use good passwords everywhere.
           * Static content is very hard to hack!
           * Always, always make frequent backups of any important work.
    Note that as of version 0.5, pagekite.py includes a very basic request firewall, which attempts to prevent access to phpMyAdmin and other sensitive systems. If it gets in your way, the +insecure flag or --insecure option can be used to turn it off. For more, please visit: """) MAN_LICENSE = ("""\ Copyright 2010-2015, the Beanstalks Project ehf. and Bjarni R. Einarsson. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see: """) MAN_BUGS = ("""\ Using pagekite.py as a front-end relay with the native Python SSL module may result in poor performance. Please use the pyOpenSSL wrappers instead. """) MAN_SEE_ALSO = ("""\ lapcat(1), , """) MAN_CREDITS = ("""\
    - Bjarni R. Einarsson 
        - The Beanstalks Project ehf. 
        - The Rannis Technology Development Fund 
        - Joar Wandborg 
    - Luc-Pierre Terral """) MANUAL_TOC = ( ('SH', 'Name', MAN_NAME), ('SH', 'Synopsis', MAN_SYNOPSIS), ('SH', 'Description', MAN_DESCRIPTION), ('SH', 'Basic usage', MAN_EXAMPLES), ('SH', 'Services and kites', MAN_KITES), ('SH', 'Kite configuration', MAN_KITE_EXAMPLES), ('SH', 'Flags', MAN_FLAGS), ('SS', 'Common flags', MAN_FLAGS_COMMON), ('SS', 'HTTP protocol flags', MAN_FLAGS_HTTP), ('SS', 'Built-in HTTPD flags', MAN_FLAGS_BUILTIN), ('SH', 'Options', MAN_OPTIONS), ('SS', 'Common options', MAN_OPT_COMMON), ('SS', 'Back-end options', MAN_OPT_BACKEND), ('SS', 'Front-end options', MAN_OPT_FRONTEND), ('SS', 'System options', MAN_OPT_SYSTEM), ('SH', 'Configuration files', MAN_CONFIG_FILES), ('SH', 'Security', MAN_SECURITY), ('SH', 'Bugs', MAN_BUGS), ('SH', 'See Also', MAN_SEE_ALSO), ('SH', 'Credits', MAN_CREDITS), ('SH', 'Copyright and license', MAN_LICENSE), ) HELP_SHELL = ("""\ Press ENTER to fly your kites, CTRL+C to quit or give some arguments to accomplish a more specific task. """) HELP_KITES = ("""\ """) HELP_TOC = ( ('about', 'About PageKite', MAN_DESCRIPTION), ('basics', 'Basic usage examples', MAN_EXAMPLES), ('kites', 'Services and kites', MAN_KITES), ('config', 'Adding, disabling or removing kites', MAN_KITE_EXAMPLES), ('flags', 'Service flags', '\n'.join([MAN_FLAGS, MAN_FLAGS_COMMON, MAN_FLAGS_HTTP, MAN_FLAGS_BUILTIN])), ('files', 'Where are the config files?', MAN_CONFIG_FILES), ('security', 'A few words about security.', MAN_SECURITY), ('credits', 'License and credits', '\n'.join([MAN_LICENSE, 'CREDITS:', MAN_CREDITS])), ('manual', 'The complete manual. See also: http://pagekite.net/man/', None) ) def HELP(args): name = title = text = '' if args: what = args[0].strip().lower() for name, title, text in HELP_TOC: if name == what: break if name == 'manual': text = DOC() elif not text: text = ''.join([ 'Type `help TOPIC` to to read about one of these topics:\n\n', ''.join([' %-10.10s %s\n' % (n, t) for (n, t, x) in HELP_TOC]), '\n', HELP_SHELL ]) return unindent(clean_text(text)) def clean_text(text): return re.sub('', '`', re.sub('', '', text.replace(' __', ' '))) def unindent(text): return re.sub('(?m)^ ', '', text) def MINIDOC(): return ("""\ >>> Welcome to pagekite.py v%s! %s To sign up with PageKite.net or get advanced instructions: $ pagekite.py --signup $ pagekite.py --help If you request a kite which does not exist in your configuration file, the program will offer to help you sign up with https://pagekite.net/ and create it. Pick a name, any name!\ """) % (APPVER, clean_text(MAN_EXAMPLES)) def DOC(): doc = '' for h, section, text in MANUAL_TOC: doc += '%s\n\n%s\n' % (h == 'SH' and section.upper() or ' '+section, clean_text(text)) return doc def MAN(pname=None): man = ("""\ .\\" This man page is autogenerated from the pagekite.py built-in manual. .TH PAGEKITE "1" "%s" "https://pagekite.net/" "Awesome Commands" .nh .ad l """) % ts_to_iso(time.time()).split('T')[0] for h, section, text in MANUAL_TOC: man += ('.%s %s\n\n%s\n\n' ) % (h, h == 'SH' and section.upper() or section, re.sub('\n +', '\n', '\n'+text.strip()) .replace('\n--', '\n.TP\n\\fB--') .replace('\n+', '\n.TP\n\\fB+') .replace(' __', '\\fR\n') .replace('-', '\\-') .replace('
    ', '\n.nf\n').replace('
    ', '\n.fi\n') .replace('', '\\fB').replace('', '\\fR') .replace('', '\\fI').replace('', '\\fR') .replace('', '\\fI').replace('', '\\fR') .replace('', '\\fI').replace('', '\\fR') .replace('\\fR\\fR\n', '\\fR')) if pname: man = man.replace('pagekite.py', pname) return man def MARKDOWN(pname=None): mkd = '' for h, section, text in MANUAL_TOC: if h == 'SH': h = '##' else: h = '###' mkd += ('%s %s %s\n%s\n\n' ) % (h, section, h, re.sub('(|`)', '\\1', re.sub(' +
    ([A-Z0-9])', ' \n \\1', re.sub('\n ', '\n ', re.sub('\n ', '\n', '\n'+text.strip())) .replace(' __', '
    ') .replace('\n--', '\n * --') .replace('\n+', '\n * +') .replace('', '`').replace('', '`') .replace('', '`').replace('', '`')))) if pname: mkd = mkd.replace('pagekite.py', pname) return mkd if __name__ == '__main__': import sys if '--nopy' in sys.argv: pname = 'pagekite' else: pname = None if '--man' in sys.argv: print MAN(pname) elif '--markdown' in sys.argv: print MARKDOWN(pname) elif '--minidoc' in sys.argv: print MINIDOC() else: print DOC() pagekite-0.5.8a/debian/pagekite/usr/share/pyshared/pagekite/common.py0000644000175000017500000001066612610153251025225 0ustar brebre00000000000000""" Constants and global program state. """ ############################################################################## LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2015, the Beanstalks Project ehf. and Bjarni Runar Einarsson This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see: """ ############################################################################## import random import time PROTOVER = '0.8' APPVER = '0.5.8a' AUTHOR = 'Bjarni Runar Einarsson, http://bre.klaki.net/' WWWHOME = 'http://pagekite.net/' LICENSE_URL = 'http://www.gnu.org/licenses/agpl.html' MAGIC_PREFIX = '/~:PageKite:~/' MAGIC_PATH = '%sv%s' % (MAGIC_PREFIX, PROTOVER) MAGIC_PATHS = (MAGIC_PATH, '/Beanstalk~Magic~Beans/0.2') MAGIC_UUID = '%x-%x-%s' % (random.randint(0, 0xfffffff), time.time(), APPVER) SERVICE_PROVIDER = 'PageKite.net' SERVICE_DOMAINS = ('pagekite.me', '302.is', 'testing.is', 'kazz.am') SERVICE_DOMAINS_SIGNUP = ('pagekite.me',) SERVICE_XMLRPC = 'http://pagekite.net/xmlrpc/' SERVICE_TOS_URL = 'https://pagekite.net/humans.txt' SERVICE_CERTS = ['b5p.us', 'frontends.b5p.us', 'pagekite.net', 'pagekite.me', 'pagekite.com', 'pagekite.org', 'testing.is', '302.is'] DEFAULT_CHARSET = 'utf-8' DEFAULT_BUFFER_MAX = 1024 AUTH_ERRORS = '255.255.255.' AUTH_ERR_USER_UNKNOWN = '.0' AUTH_ERR_INVALID = '.1' AUTH_QUOTA_MAX = '255.255.254.255' VIRTUAL_PN = 'virtual' CATCHALL_HN = 'unknown' LOOPBACK_HN = 'loopback' LOOPBACK_FE = LOOPBACK_HN + ':1' LOOPBACK_BE = LOOPBACK_HN + ':2' LOOPBACK = {'FE': LOOPBACK_FE, 'BE': LOOPBACK_BE} # Re-evaluate our choice of frontends every 45-60 minutes. FE_PING_INTERVAL = (45 * 60) + random.randint(0, 900) PING_INTERVAL = 90 PING_INTERVAL_MOBILE = 1800 PING_INTERVAL_MAX = 1800 PING_GRACE_DEFAULT = 40 PING_GRACE_MIN = 5 WEB_POLICY_DEFAULT = 'default' WEB_POLICY_PUBLIC = 'public' WEB_POLICY_PRIVATE = 'private' WEB_POLICY_OTP = 'otp' WEB_POLICIES = (WEB_POLICY_DEFAULT, WEB_POLICY_PUBLIC, WEB_POLICY_PRIVATE, WEB_POLICY_OTP) WEB_INDEX_ALL = 'all' WEB_INDEX_ON = 'on' WEB_INDEX_OFF = 'off' WEB_INDEXTYPES = (WEB_INDEX_ALL, WEB_INDEX_ON, WEB_INDEX_OFF) BE_PROTO = 0 BE_PORT = 1 BE_DOMAIN = 2 BE_BHOST = 3 BE_BPORT = 4 BE_SECRET = 5 BE_STATUS = 6 BE_STATUS_REMOTE_SSL = 0x0010000 BE_STATUS_OK = 0x0001000 BE_STATUS_ERR_DNS = 0x0000100 BE_STATUS_ERR_BE = 0x0000010 BE_STATUS_ERR_TUNNEL = 0x0000001 BE_STATUS_ERR_ANY = 0x0000fff BE_STATUS_UNKNOWN = 0 BE_STATUS_DISABLED = 0x8000000 BE_STATUS_DISABLE_ONCE = 0x4000000 BE_INACTIVE = (BE_STATUS_DISABLED, BE_STATUS_DISABLE_ONCE) BE_NONE = ['', '', None, None, None, '', BE_STATUS_UNKNOWN] DYNDNS = { 'pagekite.net': ('http://up.pagekite.net/' '?hostname=%(domain)s&myip=%(ips)s&sign=%(sign)s'), 'beanstalks.net': ('http://up.b5p.us/' '?hostname=%(domain)s&myip=%(ips)s&sign=%(sign)s'), 'dyndns.org': ('https://%(user)s:%(pass)s@members.dyndns.org' '/nic/update?wildcard=NOCHG&backmx=NOCHG' '&hostname=%(domain)s&myip=%(ip)s'), 'no-ip.com': ('https://%(user)s:%(pass)s@dynupdate.no-ip.com' '/nic/update?hostname=%(domain)s&myip=%(ip)s'), } # Create our service-domain matching regexp import re SERVICE_DOMAIN_RE = re.compile('\.(' + '|'.join(SERVICE_DOMAINS) + ')$') SERVICE_SUBDOMAIN_RE = re.compile(r'^([A-Za-z0-9_-]+\.)*[A-Za-z0-9_-]+$') class ConfigError(Exception): """This error gets thrown on configuration errors.""" class ConnectError(Exception): """This error gets thrown on connection errors.""" class BugFoundError(Exception): """Throw this anywhere a bug is detected and we want a crash.""" ##[ Ugly fugly globals ]####################################################### # The global Yamon is used for measuring internal state for monitoring gYamon = None # Status of our buffers... buffered_bytes = [0] pagekite-0.5.8a/debian/pagekite/usr/share/pyshared/pagekite/logging.py0000644000175000017500000000617212603560755025375 0ustar brebre00000000000000""" Logging. """ ############################################################################## LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2015, the Beanstalks Project ehf. and Bjarni Runar Einarsson This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see: """ ############################################################################## import time import sys import compat, common from compat import * from common import * syslog = compat.syslog org_stdout = sys.stdout DEBUG_IO = False LOG = [] LOG_LINE = 0 LOG_LENGTH = 300 LOG_THRESHOLD = 256 * 1024 def LogValues(values, testtime=None): global LOG, LOG_LINE, LOG_LAST_TIME now = int(testtime or time.time()) words = [('ts', '%x' % now), ('t', '%s' % ts_to_iso(now)), ('ll', '%x' % LOG_LINE)] words.extend([(kv[0], ('%s' % kv[1]).replace('\t', ' ') .replace('\r', ' ') .replace('\n', ' ') .replace('; ', ', ') .strip()) for kv in values]) wdict = dict(words) LOG_LINE += 1 LOG.append(wdict) while len(LOG) > LOG_LENGTH: LOG[0:(LOG_LENGTH/10)] = [] return (words, wdict) def LogSyslog(values, wdict=None, words=None): if values: words, wdict = LogValues(values) if 'err' in wdict: syslog.syslog(syslog.LOG_ERR, '; '.join(['='.join(x) for x in words])) elif 'debug' in wdict: syslog.syslog(syslog.LOG_DEBUG, '; '.join(['='.join(x) for x in words])) else: syslog.syslog(syslog.LOG_INFO, '; '.join(['='.join(x) for x in words])) def LogToFile(values, wdict=None, words=None): if values: words, wdict = LogValues(values) try: global LogFile LogFile.write('; '.join(['='.join(x) for x in words])) LogFile.write('\n') except (OSError, IOError): # Avoid crashing if the disk fills up or something lame like that pass def LogToMemory(values, wdict=None, words=None): if values: LogValues(values) def FlushLogMemory(): global LOG for l in LOG: Log(None, wdict=l, words=[(w, l[w]) for w in l]) def LogError(msg, parms=None): emsg = [('err', msg)] if parms: emsg.extend(parms) Log(emsg) if common.gYamon: common.gYamon.vadd('errors', 1, wrap=1000000) def LogDebug(msg, parms=None): emsg = [('debug', msg)] if parms: emsg.extend(parms) Log(emsg) def LogInfo(msg, parms=None): emsg = [('info', msg)] if parms: emsg.extend(parms) Log(emsg) def ResetLog(): global LogFile, Log, org_stdout LogFile = org_stdout Log = LogToMemory ResetLog() pagekite-0.5.8a/debian/pagekite/usr/share/pyshared/pagekite-0.5.8a.egg-info/0000775000175000017500000000000012610153761025663 5ustar brebre00000000000000pagekite-0.5.8a/debian/pagekite/usr/share/pyshared/pagekite-0.5.8a.egg-info/PKG-INFO0000644000175000017500000000146112610153656026763 0ustar brebre00000000000000Metadata-Version: 1.0 Name: pagekite Version: 0.5.8a Summary: PageKite makes localhost servers visible to the world. Home-page: http://pagekite.org/ Author: Bjarni R. Einarsson Author-email: bre@pagekite.net License: AGPLv3+ Description: PageKite is a system for running publicly visible servers (generally web servers) on machines without a direct connection to the Internet, such as mobile devices or computers behind restrictive firewalls. PageKite works around NAT, firewalls and IP-address limitations by using a combination of tunnels and reverse proxies. Natively supported protocols: HTTP, HTTPS Any other TCP-based service, including SSH and VNC, may be exposed as well to clients supporting HTTP Proxies. Platform: UNKNOWN pagekite-0.5.8a/debian/pagekite/usr/share/pyshared/pagekite-0.5.8a.egg-info/top_level.txt0000644000175000017500000000001112610153656030406 0ustar brebre00000000000000pagekite pagekite-0.5.8a/debian/pagekite/usr/share/pyshared/pagekite-0.5.8a.egg-info/requires.txt0000644000175000017500000000002712610153656030263 0ustar brebre00000000000000SocksipyChain >= 2.0.15pagekite-0.5.8a/debian/pagekite/usr/share/pyshared/pagekite-0.5.8a.egg-info/dependency_links.txt0000644000175000017500000000000112610153656031732 0ustar brebre00000000000000 pagekite-0.5.8a/debian/pagekite/usr/share/pyshared/pagekite-0.5.8a.egg-info/SOURCES.txt0000644000175000017500000000713212610153661027547 0ustar brebre00000000000000COPYING HTTPD-PLAN.txt MANIFEST.in Makefile README.md TODO.md UI.txt __main__.py droiddemo.py pagekite_gtk.py pk setup.py debian/changelog debian/changelog.in debian/compat debian/control debian/control.in debian/copyright debian/copyright.in debian/dirs debian/docs debian/init.d debian/pagekite.debhelper.log debian/pagekite.logrotate debian/pagekite.manpages debian/pagekite.substvars debian/postinst debian/postrm debian/preinst debian/prerm debian/rules debian/pagekite/usr/lib/python2.7/site-packages/pagekite/__init__.py debian/pagekite/usr/lib/python2.7/site-packages/pagekite/__main__.py debian/pagekite/usr/lib/python2.7/site-packages/pagekite/android.py debian/pagekite/usr/lib/python2.7/site-packages/pagekite/common.py debian/pagekite/usr/lib/python2.7/site-packages/pagekite/compat.py debian/pagekite/usr/lib/python2.7/site-packages/pagekite/dropper.py debian/pagekite/usr/lib/python2.7/site-packages/pagekite/httpd.py debian/pagekite/usr/lib/python2.7/site-packages/pagekite/logging.py debian/pagekite/usr/lib/python2.7/site-packages/pagekite/logparse.py debian/pagekite/usr/lib/python2.7/site-packages/pagekite/manual.py debian/pagekite/usr/lib/python2.7/site-packages/pagekite/pk.py debian/pagekite/usr/lib/python2.7/site-packages/pagekite/yamond.py debian/pagekite/usr/lib/python2.7/site-packages/pagekite/proto/__init__.py debian/pagekite/usr/lib/python2.7/site-packages/pagekite/proto/conns.py debian/pagekite/usr/lib/python2.7/site-packages/pagekite/proto/filters.py debian/pagekite/usr/lib/python2.7/site-packages/pagekite/proto/parsers.py debian/pagekite/usr/lib/python2.7/site-packages/pagekite/proto/proto.py debian/pagekite/usr/lib/python2.7/site-packages/pagekite/proto/selectables.py debian/pagekite/usr/lib/python2.7/site-packages/pagekite/ui/__init__.py debian/pagekite/usr/lib/python2.7/site-packages/pagekite/ui/basic.py debian/pagekite/usr/lib/python2.7/site-packages/pagekite/ui/nullui.py debian/pagekite/usr/lib/python2.7/site-packages/pagekite/ui/remote.py debian/python-module-stampdir/pagekite doc/CREDITS.txt doc/DEV-HOWTO.md doc/HISTORY.txt doc/MANPAGE.md doc/README.md doc/REMOTEUI.md doc/header.txt doc/lapcat.1 doc/pagekite.1 etc/init.d/pagekite.debian etc/init.d/pagekite.fedora etc/logrotate.d/pagekite.debian etc/logrotate.d/pagekite.fedora etc/pagekite.d/10_account.rc etc/pagekite.d/20_frontends.rc etc/pagekite.d/80_httpd.rc.sample etc/pagekite.d/80_sshd.rc.sample etc/pagekite.d/accept.acl.sample etc/sysconfig/pagekite.fedora pagekite/__init__.py pagekite/__main__.py pagekite/android.py pagekite/common.py pagekite/compat.py pagekite/dropper.py pagekite/httpd.py pagekite/logging.py pagekite/logparse.py pagekite/manual.py pagekite/pk.py pagekite/yamond.py pagekite.egg-info/PKG-INFO pagekite.egg-info/SOURCES.txt pagekite.egg-info/dependency_links.txt pagekite.egg-info/requires.txt pagekite.egg-info/top_level.txt pagekite/proto/__init__.py pagekite/proto/conns.py pagekite/proto/filters.py pagekite/proto/parsers.py pagekite/proto/proto.py pagekite/proto/selectables.py pagekite/ui/__init__.py pagekite/ui/basic.py pagekite/ui/nullui.py pagekite/ui/remote.py rpm/pagekite.init rpm/pagekite.logrotate rpm/pagekite.sysconfig rpm/rpm-install.sh rpm/rpm-post.sh rpm/rpm-preun.sh rpm/rpm-setup.sh scripts/blackbox-test.sh scripts/breeder.py scripts/installer.sh scripts/lapcat scripts/mk-dropper.sh scripts/mk-self-signed.sh scripts/pagekite scripts/pagekite_gtk scripts/pagekite_test.py.old scripts/pkvnc scripts/self-signed.pem scripts/legacy-testing/pagekite-0.3.21.py scripts/legacy-testing/pagekite-0.4.6a.py scripts/legacy-testing/pagekite-0.5.6d.py scripts/tests/auth.rc scripts/tests/crypto.rc scripts/tests/proxy.rcpagekite-0.5.8a/debian/pagekite/usr/share/python-support/0000775000175000017500000000000012610153761023005 5ustar brebre00000000000000pagekite-0.5.8a/debian/pagekite/usr/share/python-support/pagekite.public0000644000175000017500000000224112610153667026000 0ustar brebre00000000000000pyversions=2.3- /usr/share/pyshared/pagekite-0.5.8a.egg-info/PKG-INFO /usr/share/pyshared/pagekite-0.5.8a.egg-info/SOURCES.txt /usr/share/pyshared/pagekite-0.5.8a.egg-info/dependency_links.txt /usr/share/pyshared/pagekite-0.5.8a.egg-info/requires.txt /usr/share/pyshared/pagekite-0.5.8a.egg-info/top_level.txt /usr/share/pyshared/pagekite/__init__.py /usr/share/pyshared/pagekite/__main__.py /usr/share/pyshared/pagekite/android.py /usr/share/pyshared/pagekite/common.py /usr/share/pyshared/pagekite/compat.py /usr/share/pyshared/pagekite/dropper.py /usr/share/pyshared/pagekite/httpd.py /usr/share/pyshared/pagekite/logging.py /usr/share/pyshared/pagekite/logparse.py /usr/share/pyshared/pagekite/manual.py /usr/share/pyshared/pagekite/pk.py /usr/share/pyshared/pagekite/proto/__init__.py /usr/share/pyshared/pagekite/proto/conns.py /usr/share/pyshared/pagekite/proto/filters.py /usr/share/pyshared/pagekite/proto/parsers.py /usr/share/pyshared/pagekite/proto/proto.py /usr/share/pyshared/pagekite/proto/selectables.py /usr/share/pyshared/pagekite/ui/basic.py /usr/share/pyshared/pagekite/ui/nullui.py /usr/share/pyshared/pagekite/ui/remote.py /usr/share/pyshared/pagekite/yamond.py pagekite-0.5.8a/debian/pagekite/usr/share/man/0000775000175000017500000000000012610153761020525 5ustar brebre00000000000000pagekite-0.5.8a/debian/pagekite/usr/share/man/man1/0000775000175000017500000000000012610153761021361 5ustar brebre00000000000000pagekite-0.5.8a/debian/pagekite/usr/share/man/man1/lapcat.1.gz0000644000175000017500000000211212610153662023320 0ustar brebre00000000000000‹UÁnã6½ë+¦9u»¶¼n la×ëØ‚­`4=PÒÈbL“IÙ«OêoôË:$åläÐ @lrøæÍ›7“øñþ×Ï»Ìî’évÃ_† µª*÷)Š â ׯBÃ4; E=€Ur7€©U+JȘP°Æøè-J–/÷³i¶X¯^¾Àuôñ4Óæ Ë•ôÊÖ¨`ÚçP','`é¥üùó‡ÿ;þÅÙ–I:M2ÃůŸÆãá§ÏÃËñ…‡K2CåSKeØš(™¥Ä5JkYrÃrPw %`ÏUÅuw¡ãw#X "D¬,8¾¼êžïòp׃U“T!–‘äšïjK,õŽKÓÓªÞÒª¸Tk¸®øJ¯ns}¾%<Ôè !×Èö!À4p%¯_ÈcÀCc;88µzQ‡¦Á‚W¼x–òG3£x;÷ÆŠk fáqKUxu 99 ¤Z}ï`Z+EÉ ¡Š³VJ¨£È?ß>¬Öév±â(Q¼YÀ_ càïèªVÆ^ÃU£´½îŸý1ÛN7‹Ôy4ŠÓ4z¬nÂëÇ*r ‰–¾ß#É®y‡5Á"¶ EdÓt´H¬/ƒ“).bÎÄ–­`\âÐÅ«SÇ#Ž¢¹:y÷½xO‰ÑXê 75–P"%+M0hAU [¦çBÃG¦»(:;ShHÊñˆ®Š“Òû—IœmØ‘qÁr.¸íâ^’µWƒ„L#€á±·Á,˜¤Ä¼%ôÖ6-q?‡„¨>„æJÂAé·ÁýnM’GÑ24šTÔ4†LD”ÑöÙµòF~V%šIßÿmó,K{;ø*ºätM:¹ï¾,‚ú©¥ƒ˜ˆ1‰yB¦Ð›¯7Núoš[KZçÜ<1-9lb˜qÉ´1Ôß«ÚÚf2ÑÐÆ{Áö<&´ÑµÊŒèÝ “DQìMDr>¹J±®bÏ3¥‰ýÊ©ZbÅÍðžNȹ›ÓõêËâö~“øо,þ¼›MÀC¿mê‡s6ƒd¹];Ög ZÞƒgŽÏèJïF×ïœû¬çœéÃfq;ÏÚT5]Ø}ÿþnû¿+…+éuÉð²d´ä 4N ¹M—Ç˸]ÝC✨€Ž€þ[76—®/‚ü¦£3Ël=q0̈í×ö H¨s§+MûΨʺ}6NµÞ’þ˜º_ÔLî°7-lK>mIEnƒÃX­á[²Ù$«ìapv~·nThÎ¥³ƒ`ÎÛÿ¥·t pagekite-0.5.8a/debian/pagekite/usr/share/man/man1/pagekite.1.gz0000644000175000017500000001355012610153662023655 0ustar brebre00000000000000‹¤[msã6’þÎ_›½+Û‰,Ïd“­”owë4{Fí³ä›¤ÎWYˆ„$®)’Kö(ö·ßó4¾Èò${7U‰%hýút78¾¥æëÔªÎU©WFá³nêberSéÚ$jYU¯<~Hk£MšÕÇiÎIÎÆÑxþAÝLÞŸÿ0Ÿ«Wo^©Wß¼~óÝñ›×Çoþ€/ëº.íéÉI 0ÎM}‚ß'OƣΊ (%öU4Î×ÑX'*‹Æ³êjòñ<Š¢vÝÇ×ãïÆßku¬>ꣲ"ÖÙº°µ²¦z4•Ue³ÈÒ8ÛªÇÔ¦‹ÌDBgöÓÕõÍl:­ûåÛ@î~y«þû~9½?¾?.Ê:-r‹ŸþG~#½46ò_eøq®7í¤¯—™^Éx·Ä»óÙÙíôf>½¾Â*7Xâî˜ÌTvkk³QË¢RæsYØ4_‘h»{Ò ¨ ²:rçPÓ¼6˜5VjZ“؆‡Á®"Ç! ñ`Ʀe†z2‹–VQE3lí%ÞŒ”ÎêuÑ¬Öø „u¾Uó³›ûã…&å²*ê".2ëÌç7œøy;î±Él|‘暬VH5yn2<ÈUîÑÈÔÔ0º4¹…Fd^ê¸Æ]?ã_c¡ˆ8[R`r^Ôj­±!yé$©Œu«“Ų6¹²Íâo†4 ¥·°[Îy’ŠVŒTšÇY“@n‘^­H%ñeZ™',êHnš¬NË rÐ[aþê;ĺp¬U¥7*Ý`È̳jQÔke õ αãT>;a¾Zèøáþƒ^E\ƒO*³)ÀÑW°Ê¼vÏóî…ñøŠ=@e.À5œâÑäWlF»Ê¯3[ø£‰x u°J$ètFTöïM?€³Au£ešwæˆë¢òãö>U–¨Oi‚PEˆ5e£ìZWÔ|LŒ T™^•¨ÂØYÐÛÉlz¦îfp#° x‚eôV6ÔXìy¤V๥ÜéMàLZñŸ~ÿúDŽ„M;=þµóWô#ãÖùlàæ…;Q¶4qºÄ¬e‘%àâH¹óaã¢Ò-:èERUmÄO4üZ½>y¶Šú:Íó´ÔïœË¼§Ï$ßõ |5^×›L©çº_$ðûׯ_˨/i –Çá}tš+' î⾃RæÄ£Á‘­]CÏV›\½s{yÆïñ2¼+>¿ý¯éÙùL3\Ð)Ï¡8­gƒ‰Ó,v–ÞÔ)ª7‹‘zZ› n¶Žð,¸C®î8ðä­‚Mm©OTT¨ÞùgM›+lÇêÊœbG¡g£öÇαŽ"íu‡:£E}@w¨q×nG‰ZÀAWé‚–Ö‡8TU–æâ– <}Já 0v–í PÅÇjºÄzíw.T™¿7Æ2j?­ÓxݹCUF'ÛÈ|NÁç4,›/ÓUãÌP6.oè59ÃàxaÙv$Áé*zJ³L-ÄeoJ.ɹ´RGÜ…TakºÊyÞ¦äºl¦gwû=¸Á8³'u"…ÁKðõcÐÖNXX–c-ã7äM;Âú¿ ›µ<¾?&GÖ”Úá…«B!uµ.©5"9¿>·wE:Ž ´æÝŸ¨uÖŸ®zTô“{n{RzWˆûÔ!#d’I¦!GóS”fˆÊz‚Ê­½™ˆ&Y†dÃë´·\—·“GÁúÝ_SÇcuX§¹G^NoCÆêPÐ3= @mWÂÝ*?d¼;Çs ›ïxÿÒä.Pë4I(“&qäbDG{M„Òþ#ÉëŲ±1ýïÝí¥+ÀÓÌÎÝÌvj¼Jý–|á'N1Ÿ‘óX—ÒТFpñ© >5ƒ3ùÎÞOáJ‰|åÐÛÿ)18—ùþ•ÙÈ®±Ê‘Gñ×’P·l2Bæ'SíAsY* Í›©ŠÆ¶QL¶í è»x‰>'Í%»|†f­¡kƒÖpW…ØTMâ²_ *#ô%}$ñÒ$Ã)6Ä{Ñ pöš07|tƒÓìi‘t8…TM<š¸¤%–R‹…‘õ6¢øJ]û9$9Ôq‹Ã€~!LØÊ˜UÔÁåš’vÈKDd¨*j“¦$ FÔ¯ÈEi&¡°›ÂÄ(s­VT†~ TwN=ªÓ¬Ò`ƒuËÿ¥ÁPä„C?ïIuÈ8gâîéüì!-·u€¡§½GÒã>!âM`͎д`Ž‹Òú8³™=ZíqÚMa`ûãÒg‰â •V^Ä•¶ëÊ0‰tÞ1?àМعȷZ€ŒQnór=ºÁ!bþ³Ÿ‘vm²¬ÏÈ[ ø®$•³NÓõJÆw »mÒ¾(Àc Ÿ»©Ù¹He¬&ÖÂt­ú‰q>-“ „»v%N¨¿«KþÀ AœÆãøÁV–Ó%Ž8ôtäêB#ÕDŒ”%úvtÔñKi€Ád8ArÉ·Ý"šÕ‘‚aߣcƒÇ7ç]8P¬„p…¢2㦄r“Œ\$ 5Œx­ù+ã¢ÒËZâmÁ# UÂ{õŸ‹Å­'•y&­q°SX{4Vn–Š–>„°¿Y°ß–G‚€ ’íeÃŒ^îhóþFÜV3Ú³r’¤ A´“R%~rAœûS>gú®W¡„’žoñ¶-ÕEÑ '¥ºXŠÎ¢éöM5Ó)ÝþM…l pßo*ÉmÛXlr]°†ÛŽ8¿¶#9Á–b8ëÊï®f±ûÒZg “:sÏ÷Σɶô‘º8Y™G ¾zà+]Äy®…PBœ›ÝسÃ{)6ôuÅ>M`Ù¯ò_ Œ;u8Ø»5JÚ¦ÏX¾-pt_gwä뀟ì"»újÑ,’ð+Rþ4ë@¯(†›KE‰^R”Oºƒ’ËCš~ÌkLäâøø¤xÜ•˜Y.@'Örœ U®üW'´SéÆ ©FÖU#…ñª(jeú;yîÛ’mžäô+®Y)A±\ë`ÿ»mN½ƒÚ<¦LÔ~„¿þq'(Zöý¶Qà9ÂHãsÐVÆZ2€zí“á]G¶–þ™øùu#‹I¤Ž>ܹîпAsSmRibÁÉÀù ¤\Bÿu©ÓŒlªüù†©€ysIK—Èöõ™T%B¨"ë+õÓ#¯LÕóÅ7¡Þ$ƒM§¥Q¡éK âftŸÐbÞ…~ c{Ð^Z».ñ•'’»„xXƒ>ºà>_ΧR {XßéÙlQš}öÖZWGòÿoFåUp€aÈ0NÄ"¼ˆ(vßÙ±-[œ9…Ú£¯Æ‡BIäÈ>÷€n¨èD}Д8i¬ÁÞ’XWìTT‘mŽŠ”uÛ…vt­©×ƒSò‡ÞÁ»þÃîÀZW+œõxÀ€ÙÑ‹;?»ÍúCÊ¢ýÎ5¡#L3\gí¸p‘±ìÛ»ŠÂ·‘wÿ®Ù¯Eì,¿1õºØqSØf²ë¢üoÑÌø¾~,Vûjyj[°AlõÞ5@£WH¹CT6£·¯†köй`ã+Sn(‡ŒN#:dŽÜm`Q!íë{©»££ð$ú ý´Pm”éÏ"è@trúŸn¥Ôábó„ç“Ooyvž[N}¸0®ÍÖ^ºæ“"Íg{í‹c©€ÎdDI‚… ùWé§=ÇçDŒØMTØ”C»Éù냳„¼+£«EZWºÚºqʤÙÏ»:?›ï˜–ìügg?û€¶7È!n[QÄ\ê»UŽt|Ø–qª´0K¢Ö.¦ë?HL¾=؉þ’ø‘?m™‚3½'qù¶HúWÇK©K+[ƨVYvÙËÓŒ£RK•ôgù+<+i­a=¹KTäQ7‡ª˜\ëªã5ôø-)àï:HêÜVHf#¿#îÄ%1ñÚÄÏËÇ9E%t»Ö$âö¡øJÂç.äª3û³w}Ë;×8-¾Ä_[º„~Ä¥gWSu8sÞùŠc¦yÒö²ØJO­Ä*A²]Ç æ1ëÛÙóíÀA”EšïìçtŸ­t@»:™_Î|º-{v¡òÁlO¤–àu f®ˆ´#àÇ/é­ÑI— i1Ê»4$•ËMÿ8éz¸UŒ©ãÝe`–»«´¦–Ú;öä+·Õ ÊÌç4f¿\§¬ç‰Òì¤×úÑ|éˆ3<ï-Ô¨§ÏmX{Nx€0Iéå†{È›Â~v‹n}ÑÈfSè´›‰ÙüÝõÝ|äÁuа·/2zò¦¹›íý¯›üÁîToÉÒEð¼Áè-|Ø °™ Ûi6Êo½9´ÞëÒäÐÕÁôE³\º6k¯|J:oå{©Ò¢¹Roˆ¥„êÝ¿4¯%Ú j½«žd/zq¾X‘W† :Ò©…þ±¾ £á,€¢ó„€±h격‡er €–§¿˜^Bxì ëòMÞ¢‚;o½ï»}°ér7}wú~úΗç¸åN3;‰ëòEJ³ :Ò¤w¸›Žè'iI߀ä0ßm'!ñ,ª¦ÊzÛs˜›IÚälŠŸŸØˆë£5שƒ–5y²«©Ù’­(RêÕÔŸß”ðüW%îpä³cE[ÂrD˜H’®ý5L´½kÃÂd´O.Ó!F À‘숄+é%¾§žom$åƒ!ŒiŸ•fÓc÷ÆÁ‹‡seåB\[*‹ÚR™zV*óu²=%2áB}löaxgJ]L/åêáÔÝGru?ϤA'ÔˆÛÕVë4Këíˆ7–èå"¶(ñÅîV »Ò^‰Z³nÓº`_Lw#läô¥Éa ξËÓϾ‹4ÜÝ#ø¨cu=S?±^ÉÍ?Õ•A9÷Sš'@ŸãßxZ·J°_I†NÀÚ”‘¨¸¿ßµÍ3-/ŠÚ ?˜/µµê^_¦!\ 8jã’©ãî½€$%Qí JløLsWK!y;z¡Pëð[…»$ #Ÿ0Y°:ju­W÷Ý{!3\Å„äè`y™ÚÝ#ðž6’\õËçÔôá 9øÅèxnvúd©[r¡ŸfQd`G/Á^y×À–:6^? á"ëÚ׬ W·± @’Ä.IH0L/D7å’õ"Óùƒ,ìŠîÓ¬Ò\œ¥œêàwrøîÒG@ëÔ¨ÇÕõüü”ùD¹·Z(Ò·úþÓ»`e/W#§.:y ÝS‡û·8—õö¦”˜øò%€q¸]|vw;ÿÄW0œ_x0¦¤õöIÏ£K!¶}C‡ªî¦mÞ¾šn$Í}é6wõOIçl«LX¾…®Q³U³”âM^Ã&$6K? êø˜V »¥ÞH i-)™Þ”®Ü¿±€¡fëÑaAµ¨¨]þê«@h„/T³*A/Cp† VÍH‰ŒÍ©\¯Œ¾R3¨ô´½&P,—.š1|9±§ˆï_©;»ëÇw/r¿,°ÂNªméÄó•º4Ö…Þ·ðÕä ´×fWEÁ’Ú¹¿"JòÊeá@ªŠŒ D«(Ô­¤˜>«qê8ÔU(?>Æ•¸£5$õ/6ÉžôÖòÒ5ÿºWi‚¼$’7¥ë.äÛžH÷Ôß°ì®iI±Ó¼7zvƒ½{ÛBv#¯\D¾¶×2fä®/²ú¶އ•÷5ôÅÜ¥]ÉË$ÖÝ òÁÂ%Š)ÑBÝ]ÍÅYGá®uÿ®]$¬Î„û×ð¼ƒv$u«\n²/—Ь ïgÛ¸ÌwêSõǽ¯€Ñ®ÁГ Î'ö¯‚ÜÉuÕ»CT¯ò*/»t:—{Óêf[¯±Wha´)Vè5áC$Ób#ˆ¦f*Aÿ¹xZï0B›§Üzè F—j#0£†m}͹š\ή±ÑL—€Y‡o„ÿèßNiÏYT«“?^`€?ïÙíù»é|^{¹?Voÿ¦«A 8o7Ðÿ0¶ÃÄZ0ö²‰ïoRös°\¬ÚÍO·Ó÷æònÈåôìüjÆWΊr[¥«u­¾yýæõý1_-tÊûÒ‘é€öpðÙËXL ›iËúI^ù c¡~Ò³|·hxaª¯-@•Òå–o›€Æ•á"6íç÷WwjBˆP„–½ºqom\Â׿VúÛ]ìZ:ÙÒºà&f~ê‚©‚µQè òû°Ž§&=¢C]GbÓÎ>ÝÝZÞÔoçí9ww¼¶Õ½F pn,ío°†e“I[.ú4@Ê éü¤>Mno'WóŸþ],1Є÷>Øêâņ˜FÚÏoÏ>`üäíô’áÛ¾˜Î¯ÎÙ{¹¾Uu3¹OÏî.'·ÿ;…€Ð ÿ`W=…`Hûw rÁ5 ÖºKI6øÀS‘À˜„ª«ÐÊÇLШ5_PIt„q¡lÆA"lÚØ8ö3¬PòFz^)8Ûç@Œe..‚Buƒi;pagekite-0.5.8a/debian/pagekite/usr/bin/0000775000175000017500000000000012610153761017420 5ustar brebre00000000000000pagekite-0.5.8a/debian/pagekite/usr/bin/lapcat0000755000175000017500000003743512610153646020626 0ustar brebre00000000000000#!/usr/bin/python __DOC__ = """\ # Copyright 2011, Bjarni R. Einarsson # License: AGPLv3 # # lapcat: Location Aware Proxy Chooser And Tunneler # a.k.a. Netcat for your Laptop. # # This is a netcat-like tool for opening up a TCP connection to some port # on some host, where the connection strategy depends on where you are. # # Requirements: # Python 2.x or 3.x # PySocksipyChain, # ############################################################################## # # For example, say we want 'ssh homeserver' to behave like so: # # - When at home, connect directly (fast!) # - At work, use the local HTTP Proxy and PageKite (fast!) # - From anywhere else, use a Tor hidden service (private!) # # With lapcat, this is possible by defining the following rules in a file # named ~/.lapcat/homeserver (use lapcat -N to generate network IDs). # # [home] # if network = 10.1.2.254/aa:bb:cc:dd:ee:ff # host = homeserver.local # chain = none # priority = 1 # # [work] # if network = 192.168.55.254/gw:ma:ca:dd:re:ss # host = homeserver.pagekite.me # chain = http:proxy.corp:8080, http:homeserver.pagekite.me:443 # priority = 1 # # [default] # host = 12345123451234512345.onion # chain = socks5:localhost:9050 # priority = 100 # # Then add the following to ~/.ssh/config # # Host homeserver homeserver.pagekite.me # CheckHostIP no # ProxyCommand /path/to/lapcat homeserver 22 # """ import getopt, os, select, socket, subprocess, sys import sockschain as socks def DebugPrint(text): sys.stderr.write(text+'\n') sys.stderr.flush() global TRACE global DEBUG TRACE = DEBUG = False TRACE = False SYS_CONF_DIR = '/etc/lapcat' USER_CONF_DIR = '~/.lapcat' IMPORT_KEYWORD = 'import' DEFAULT_RULE = 'default' DEFAULT_CHAIN = 'default' V_ACTIVE = 'active' V_CHAIN = 'chain' V_DEFAULT_CHAIN = 'default chain' V_FINAL = 'final' V_HOST = 'host' V_PORT = 'port' V_PRIORITY = 'priority' V_TEST_COMMAND = 'test command' V_TEST_HOST = 'if host' V_TEST_PORT = 'if port' V_TEST_NETWORK = 'if network' VARIABLE_DEFAULTS = { V_ACTIVE: True, V_CHAIN: DEFAULT_CHAIN, V_DEFAULT_CHAIN: None, V_HOST: '%h', V_PORT: '%p', V_FINAL: False, V_TEST_COMMAND: None, V_TEST_HOST: None, V_TEST_PORT: None, V_TEST_NETWORK: None, V_PRIORITY: 100 } def Run(argv): return subprocess.Popen(argv, stdout=subprocess.PIPE ).communicate()[0].decode().splitlines() def RunTest(command): try: if DEBUG: DEBUG("Running: %s" % command) retcode = subprocess.call(command, shell=True) if DEBUG: if retcode < 0: DEBUG("Child was terminated by signal: %s" % -retcode) else: DEBUG("Child returned: %s" % retcode) return (retcode == 0) except OSError: if DEBUG: DEBUG("Execution failed: %s" % (sys.exc_info(), )) return False def GetNetworkId(): # FIXME: This probably only works on Linux/IPv4 ! gateway = 'unknown' for line in Run(['netstat', '-rn']): if line.startswith('0.0.0.0'): gateway = line.split()[1].lower() network = 'unknown' if gateway != 'unknown': for line in Run(['arp', '-n', gateway]): if line.lower().startswith(gateway): network = line.split()[2].lower() if DEBUG: DEBUG("Network is: %s/%s" % (gateway, network)) return '%s/%s' % (gateway, network) class LapCatConfig(object): def __init__(self, hostname, portnum, network): self.hostname = hostname self.portnum = str(int(portnum)) self.network = network self.rules = {DEFAULT_RULE: {}} self.rules[DEFAULT_RULE].update(VARIABLE_DEFAULTS) def sysConfig(self, name=None): return os.path.join(SYS_CONF_DIR, name or self.hostname) def userConfig(self, name=None): return os.path.join(os.path.expanduser(USER_CONF_DIR), name or self.hostname) def globalConfigs(self): """List all global configuration files, in order of preference.""" configs = [] for order, dirn in ( ('0', SYS_CONF_DIR), ('1', os.path.expanduser(USER_CONF_DIR)) ): try: for fn in os.listdir(dirn): try: pri, rest = fn.split('-', 1) pri = '%3.3d-%s' % (int(pri), order) configs.append((pri, os.path.join(dirn, fn))) except ValueError: pass except: if DEBUG: DEBUG("%s: %s" % (dirn, sys.exc_info())) configs.sort(key=lambda k: k[0]) if DEBUG: DEBUG('Configs are: %s' % configs) return [cfg[1] for cfg in configs] def load(self, filename=None, require=False, wildcards=False): """Load and parse a rule configuration file.""" filename = filename or self.userConfig() try: fd = open(filename, 'r') if DEBUG: DEBUG("Loading: %s" % filename) except: fd = None if wildcards: filedir = os.path.dirname(filename) parts = os.path.basename(filename).split('.') while len(parts) > 0: parts[0] = '_ANY_' try: filename = os.path.join(filedir, '.'.join(parts)) fd = open(filename, 'r') if DEBUG: DEBUG("Loading: %s" % filename) break except: parts.pop(0) if not fd: if not require: return self raise section = self.rules[DEFAULT_RULE] count = 0 for line in fd: count += 1 line = line.strip() if line == '' or line.startswith('#'): pass elif line.startswith('[') and line.endswith(']'): secname = line[1:-1] if secname == '': raise ValueError(('%s(line=%s): Null section') % (filename, count)) elif secname not in self.rules: self.rules[secname] = {} section = self.rules[secname] elif line.startswith(IMPORT_KEYWORD): files = [self.sysConfig(name=line[len(IMPORT_KEYWORD)+1:]), self.userConfig(name=line[len(IMPORT_KEYWORD)+1:])] loaded = False for fn in files: try: self.load(filename=fn, require=True) loaded = True except IOError: pass if not loaded: raise ValueError(('%s(line=%s): File not found, tried: %s' ) % (filename, count, files)) elif '=' in line: var, value = line.split('=') var = var.strip().lower() if var not in VARIABLE_DEFAULTS: raise ValueError(('%s(line=%s): Unknown variable: %s' ) % (filename, count, var)) value = value.strip() if value.lower() in ('true', 'yes'): value = True elif value.lower() in ('false', 'no'): value = False section[var] = value else: raise ValueError(('%s(line=%s): Invalid line') % (filename, count)) return self def configure(self): """Load all the rules pertaining to this host:port.""" for config in self.globalConfigs(): self.load(filename=config, require=True) self.load(filename=self.sysConfig(), require=False, wildcards=True) self.load(filename=self.userConfig(), require=False, wildcards=True) return self def ruleOrder(self): """Calculate the order in which to evaluate our rules.""" keys = [r for r in self.rules] keys.sort(key=lambda rule: int(self.rules[rule].get(V_PRIORITY, 999))) if DEBUG: DEBUG('Rule order: %s' % keys) return keys def test(self, rule): """Test whether a particular rule matches.""" if not (rule.get(V_ACTIVE, True) or rule.get(V_DEFAULT_CHAIN, False)): return False try: hosts = (rule.get(V_TEST_HOST, '') or self.hostname).lower().split(', ') if self.hostname.lower() not in hosts: return False ports = (rule.get(V_TEST_PORT, '') or self.portnum).lower().split(', ') if self.portnum not in ports: return False ntwks = (rule.get(V_TEST_NETWORK, '') or self.network).split(', ') if self.network not in ntwks: return False if rule.get(V_TEST_COMMAND, False): return RunTest(rule[V_TEST_COMMAND]) else: return True except: return False def connect(self): """Connect to the host:port.""" rules = self.ruleOrder() for ruleName in rules: rule = self.rules[ruleName] if self.test(rule): if rule.get(V_DEFAULT_CHAIN, False): if DEBUG: DEBUG("Configuring default proxy chain: %s" % rule) socks.setdefaultproxy() for proxy in rule[V_DEFAULT_CHAIN].split(', '): socks.adddefaultproxy(*socks.parseproxy(proxy)) if rule.get(V_CHAIN, False) and rule.get(V_ACTIVE, True): try: host = (rule.get(V_HOST, '') or self.hostname ).replace('%h', self.hostname) port = (rule.get(V_PORT, '') or self.portnum ).replace('%p', self.portnum) sock = socks.socksocket(socket.AF_INET, socket.SOCK_STREAM) for proxy in rule.get(V_CHAIN, DEFAULT_CHAIN).split(', '): sock.addproxy(*socks.parseproxy(proxy.strip() .replace('%h', host) .replace('%p', port))) sock.connect((host, int(port))) if DEBUG: DEBUG('Connected! [%s]' % ruleName) return sock except: if DEBUG: DEBUG('connect(%s) failed: %s' % (ruleName, sys.exc_info())) if rule.get(V_FINAL, False): raise IOError("Connect failed at: %s" % ruleName) raise IOError("Connect failed, tried: %s" % rules) def NetCat(host, port, input_fd, output_fd): try: network = GetNetworkId() socks.netcat(LapCatConfig(host, port, network).configure().connect(), input_fd, output_fd) except IOError: DebugPrint('%s' % (sys.exc_info(), )) sys.exit(1) def SetProcTitle(title): try: import setproctitle setproctitle.setproctitle(title) except: pass def HttpProxy(input_fd, output_fd): try: # Get the initial request request = '' loops = 1024 while not (loops < 1 or request.endswith('\n\n') or request.endswith('\r\n\r\n')): request += os.read(input_fd.fileno(), 1) loops -= 1 if TRACE: TRACE('<<< Got request (l:%s):\n%s<<<\n' % (1024-loops, request)) # If it is a HTTP CONNECT, we connect directly. words = request.split() if (len(words) >= 3 and words[0].upper() == 'CONNECT' and words[2].upper().startswith('HTTP/')): output_fd.write('HTTP/1.1 200 Tunnel established\r\n\r\n') output_fd.flush() host, port = words[1].split(':') if DEBUG: DEBUG('Using native lapcat connection to %s:%s' % (host, port)) SetProcTitle('lapcat: %s:%s' % (host, port)) NetCat(host, port, input_fd, output_fd) # Otherwise, forward this to a real HTTP Proxy for processing. elif len(words) > 2: if DEBUG: DEBUG('Connecting via. lapcat-http-proxy') host = 'lapcat-http-proxy' network = GetNetworkId() SetProcTitle('lapcat: %s' % words[1]) conn = LapCatConfig(host, 0, network).configure().connect() conn.sendall(request) socks.netcat(conn, input_fd, output_fd) except (ValueError, IOError): DebugPrint('%s' % (sys.exc_info(), )) sys.exit(1) class FileWrapper(object): def __init__(self, sock): self.sock = sock def flush(self): pass def close(self): return self.sock.close() def write(self, data): return self.sock.send(data) def fileno(self): return self.sock.fileno() def ForkAndListen(outfmt, baseport=0, tries=1, loop=False, relative=False): for t in range(0, tries): try: try: srv = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) except: srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) srv.bind(('', baseport+t)) break except: srv = None srv.listen(3) if relative: sys.stdout.write((outfmt+'\n') % (srv.getsockname()[1]-baseport)) else: sys.stdout.write((outfmt+'\n') % srv.getsockname()[1]) sys.stdout.flush() os.close(sys.stdout.fileno()) os.close(sys.stdin.fileno()) if not loop and os.fork() != 0: os._exit(0) while True: # Wait for a connection... i, o, e = select.select([srv], [], [], 15) if srv in i: client, address = srv.accept() if DEBUG: DEBUG('Accepted: %s' % (address, )) if not (loop and (os.fork() != 0)): srv.close() fw = FileWrapper(client) return fw, fw client.close() elif not loop: # Or die? os._exit(0) if __name__ == '__main__': opts, args = getopt.getopt(sys.argv[1:], 'hl:NPRtvV:', ['listen=', 'tc', 'tp', 'tf=', 'vnc', 'rdp']) if len(args) == 1 and ':' in args[0]: args = args[0].split(':') use_sysdefaults = True mode, portadd, inlinefmt, inlineargs = 'netcat', 0, '', {} for opt, arg in opts: if '-V' == opt: opt = '-v' sys.stderr = open(arg, 'a') if '-v' == opt: if DEBUG and socks.DEBUG: TRACE = DebugPrint if DEBUG: socks.DEBUG = DebugPrint DEBUG = DebugPrint if '-N' == opt: mode = 'networkid' elif '-P' == opt: mode = 'httpproxy' elif '-R' == opt: use_sysdefaults = False else: if mode not in ('netcat', 'httpproxy'): mode = 'invalid' break elif '-t' == opt: inlinefmt = '127.0.0.1 %d' elif '--tc' == opt: inlinefmt = '127.0.0.1:%d' elif '--tp' == opt: inlinefmt = '%d' elif '--tf' == opt: inlinefmt = arg elif '--rdp' == opt: inlinefmt = '127.0.0.1:%d' if len(args) == 1: args.append('3389') elif '--vnc' == opt: inlinefmt, portadd = '127.0.0.1:%d', 5900 inlineargs = {'baseport': 5900, 'tries': 20, 'relative': True} if len(args) == 1: args.append('0') elif opt in ('-l', '--listen'): inlinefmt = '127.0.0.1:%d' inlineargs = {'baseport': int(arg), 'tries': 1, 'loop': True} if use_sysdefaults: socks.usesystemdefaults() # Set up the listener, if necessary... if inlinefmt and ((mode == 'netcat' and len(args) == 2) or (mode == 'httpproxy')): fin, fout = ForkAndListen(inlinefmt, **inlineargs) else: fin, fout = sys.stdin, sys.stdout # Do proxy stuff! if mode == 'netcat' and len(args) == 2: NetCat(args[0], portadd + int(args[1].replace(':', '')), fin, fout) elif mode == 'httpproxy' and len(args) == 0: HttpProxy(fin, fout) # Or print information! elif mode == 'networkid' and len(args) == 0: print('%s' % GetNetworkId()) elif len(args) == 1 and args[0] in ('-h', '--help'): DebugPrint(__DOC__) else: print(( '%(p)s: Location Aware Proxy Chooser And Tunneler / NetCat for Laptops\n' '\n' 'Usage: %(p)s [-v [-v]] host port # Connect to host:port\n' ' %(p)s <-t|--tc|--tp> host port # Inline proxy mode\n' ' %(p)s --tf= host port # Inline proxy mode\n' ' %(p)s --rdp host [port] # Inline RDP proxy mode\n' ' %(p)s --vnc host [screen] # Inline VNC proxy mode\n' ' %(p)s -l port host port # Local port <=> host proxy\n' ' %(p)s -N # Show current network ID\n' ' %(p)s -P # Behave like an HTTP Proxy\n' ' %(p)s -h # Print instructions\n' '\n' 'To use with ssh, add to ~/.ssh/config:\n' ' ProxyCommand %(fp)s %%h %%p\n' ' CheckHostIP no\n' '\n' 'Inline use examples:\n' ' $ vncviewer `%(p)s --vnc hostname`\n' ' $ rdesktop `%(p)s --rdp homebox.pagekite.me`\n' ' $ irssi -c localhost -p `%(p)s --tp irc.freenode.net 6667`\n' ) % {'fp': os.path.abspath(sys.argv[0]), 'p': os.path.basename(sys.argv[0])}) sys.exit(100) pagekite-0.5.8a/debian/pagekite/usr/bin/pagekite0000755000175000017500000001410112610153646021134 0ustar brebre00000000000000#!/usr/bin/python """ This is the pagekite.py Main() function. """ ############################################################################## LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2015, the Beanstalks Project ehf. and Bjarni Runar Einarsson This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see: """ ############################################################################## import sys from pagekite import pk from pagekite import httpd if __name__ == "__main__": if hasattr(sys.stdout, 'isatty') and sys.stdout.isatty(): import pagekite.ui.basic uiclass = pagekite.ui.basic.BasicUi else: import pagekite.ui.nullui uiclass = pagekite.ui.nullui.NullUi pk.Main(pk.PageKite, pk.Configure, uiclass=uiclass, http_handler=httpd.UiRequestHandler, http_server=httpd.UiHttpServer) ############################################################################## CERTS="""\ StartCom Ltd. ============= -----BEGIN CERTIFICATE----- MIIFFjCCBH+gAwIBAgIBADANBgkqhkiG9w0BAQQFADCBsDELMAkGA1UEBhMCSUwxDzANBgNVBAgT BklzcmFlbDEOMAwGA1UEBxMFRWlsYXQxFjAUBgNVBAoTDVN0YXJ0Q29tIEx0ZC4xGjAYBgNVBAsT EUNBIEF1dGhvcml0eSBEZXAuMSkwJwYDVQQDEyBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1dGhv cml0eTEhMB8GCSqGSIb3DQEJARYSYWRtaW5Ac3RhcnRjb20ub3JnMB4XDTA1MDMxNzE3Mzc0OFoX DTM1MDMxMDE3Mzc0OFowgbAxCzAJBgNVBAYTAklMMQ8wDQYDVQQIEwZJc3JhZWwxDjAMBgNVBAcT BUVpbGF0MRYwFAYDVQQKEw1TdGFydENvbSBMdGQuMRowGAYDVQQLExFDQSBBdXRob3JpdHkgRGVw LjEpMCcGA1UEAxMgRnJlZSBTU0wgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxITAfBgkqhkiG9w0B CQEWEmFkbWluQHN0YXJ0Y29tLm9yZzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA7YRgACOe yEpRKSfeOqE5tWmrCbIvNP1h3D3TsM+x18LEwrHkllbEvqoUDufMOlDIOmKdw6OsWXuO7lUaHEe+ o5c5s7XvIywI6Nivcy+5yYPo7QAPyHWlLzRMGOh2iCNJitu27Wjaw7ViKUylS7eYtAkUEKD4/mJ2 IhULpNYILzUCAwEAAaOCAjwwggI4MA8GA1UdEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgHmMB0GA1Ud DgQWBBQcicOWzL3+MtUNjIExtpidjShkjTCB3QYDVR0jBIHVMIHSgBQcicOWzL3+MtUNjIExtpid jShkjaGBtqSBszCBsDELMAkGA1UEBhMCSUwxDzANBgNVBAgTBklzcmFlbDEOMAwGA1UEBxMFRWls YXQxFjAUBgNVBAoTDVN0YXJ0Q29tIEx0ZC4xGjAYBgNVBAsTEUNBIEF1dGhvcml0eSBEZXAuMSkw JwYDVQQDEyBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEhMB8GCSqGSIb3DQEJARYS YWRtaW5Ac3RhcnRjb20ub3JnggEAMB0GA1UdEQQWMBSBEmFkbWluQHN0YXJ0Y29tLm9yZzAdBgNV HRIEFjAUgRJhZG1pbkBzdGFydGNvbS5vcmcwEQYJYIZIAYb4QgEBBAQDAgAHMC8GCWCGSAGG+EIB DQQiFiBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAyBglghkgBhvhCAQQEJRYjaHR0 cDovL2NlcnQuc3RhcnRjb20ub3JnL2NhLWNybC5jcmwwKAYJYIZIAYb4QgECBBsWGWh0dHA6Ly9j ZXJ0LnN0YXJ0Y29tLm9yZy8wOQYJYIZIAYb4QgEIBCwWKmh0dHA6Ly9jZXJ0LnN0YXJ0Y29tLm9y Zy9pbmRleC5waHA/YXBwPTExMTANBgkqhkiG9w0BAQQFAAOBgQBscSXhnjSRIe/bbL0BCFaPiNhB OlP1ct8nV0t2hPdopP7rPwl+KLhX6h/BquL/lp9JmeaylXOWxkjHXo0Hclb4g4+fd68p00UOpO6w NnQt8M2YI3s3S9r+UZjEHjQ8iP2ZO1CnwYszx8JSFhKVU2Ui77qLzmLbcCOxgN8aIDjnfg== -----END CERTIFICATE----- StartCom Certification Authority ================================ -----BEGIN CERTIFICATE----- MIIHyTCCBbGgAwIBAgIBATANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJJTDEWMBQGA1UEChMN U3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmlu ZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDYwOTE3MTk0 NjM2WhcNMzYwOTE3MTk0NjM2WjB9MQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRk LjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMg U3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw ggIKAoICAQDBiNsJvGxGfHiflXu1M5DycmLWwTYgIiRezul38kMKogZkpMyONvg45iPwbm2xPN1y o4UcodM9tDMr0y+v/uqwQVlntsQGfQqedIXWeUyAN3rfOQVSWff0G0ZDpNKFhdLDcfN1YjS6LIp/ Ho/u7TTQEceWzVI9ujPW3U3eCztKS5/CJi/6tRYccjV3yjxd5srhJosaNnZcAdt0FCX+7bWgiA/d eMotHweXMAEtcnn6RtYTKqi5pquDSR3l8u/d5AGOGAqPY1MWhWKpDhk6zLVmpsJrdAfkK+F2PrRt 2PZE4XNiHzvEvqBTViVsUQn3qqvKv3b9bZvzndu/PWa8DFaqr5hIlTpL36dYUNk4dalb6kMMAv+Z 6+hsTXBbKWWc3apdzK8BMewM69KN6Oqce+Zu9ydmDBpI125C4z/eIT574Q1w+2OqqGwaVLRcJXrJ osmLFqa7LH4XXgVNWG4SHQHuEhANxjJ/GP/89PrNbpHoNkm+Gkhpi8KWTRoSsmkXwQqQ1vp5Iki/ untp+HDH+no32NgN0nZPV/+Qt+OR0t3vwmC3Zzrd/qqc8NSLf3Iizsafl7b4r4qgEKjZ+xjGtrVc UjyJthkqcwEKDwOzEmDyei+B26Nu/yYwl/WL3YlXtq09s68rxbd2AvCl1iuahhQqcvbjM4xdCUsT 37uMdBNSSwIDAQABo4ICUjCCAk4wDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAa4wHQYDVR0OBBYE FE4L7xqkQFulF2mHMMo0aEPQQa7yMGQGA1UdHwRdMFswLKAqoCiGJmh0dHA6Ly9jZXJ0LnN0YXJ0 Y29tLm9yZy9zZnNjYS1jcmwuY3JsMCugKaAnhiVodHRwOi8vY3JsLnN0YXJ0Y29tLm9yZy9zZnNj YS1jcmwuY3JsMIIBXQYDVR0gBIIBVDCCAVAwggFMBgsrBgEEAYG1NwEBATCCATswLwYIKwYBBQUH AgEWI2h0dHA6Ly9jZXJ0LnN0YXJ0Y29tLm9yZy9wb2xpY3kucGRmMDUGCCsGAQUFBwIBFilodHRw Oi8vY2VydC5zdGFydGNvbS5vcmcvaW50ZXJtZWRpYXRlLnBkZjCB0AYIKwYBBQUHAgIwgcMwJxYg U3RhcnQgQ29tbWVyY2lhbCAoU3RhcnRDb20pIEx0ZC4wAwIBARqBl0xpbWl0ZWQgTGlhYmlsaXR5 LCByZWFkIHRoZSBzZWN0aW9uICpMZWdhbCBMaW1pdGF0aW9ucyogb2YgdGhlIFN0YXJ0Q29tIENl cnRpZmljYXRpb24gQXV0aG9yaXR5IFBvbGljeSBhdmFpbGFibGUgYXQgaHR0cDovL2NlcnQuc3Rh cnRjb20ub3JnL3BvbGljeS5wZGYwEQYJYIZIAYb4QgEBBAQDAgAHMDgGCWCGSAGG+EIBDQQrFilT dGFydENvbSBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTANBgkqhkiG9w0BAQUFAAOC AgEAFmyZ9GYMNPXQhV59CuzaEE44HF7fpiUFS5Eyweg78T3dRAlbB0mKKctmArexmvclmAk8jhvh 3TaHK0u7aNM5Zj2gJsfyOZEdUauCe37Vzlrk4gNXcGmXCPleWKYK34wGmkUWFjgKXlf2Ysd6AgXm vB618p70qSmD+LIU424oh0TDkBreOKk8rENNZEXO3SipXPJzewT4F+irsfMuXGRuczE6Eri8sxHk fY+BUZo7jYn0TZNmezwD7dOaHZrzZVD1oNB1ny+v8OqCQ5j4aZyJecRDjkZy42Q2Eq/3JR44iZB3 fsNrarnDy0RLrHiQi+fHLB5LEUTINFInzQpdn4XBidUaePKVEFMy3YCEZnXZtWgo+2EuvoSoOMCZ EoalHmdkrQYuL6lwhceWD3yJZfWOQ1QOq92lgDmUYMA0yZZwLKMS9R9Ie70cfmu3nZD0Ijuu+Pwq yvqCUqDvr0tVk+vBtfAii6w0TiYiBKGHLHVKt+V9E9e4DGTANtLJL4YSjCMJwRuCO3NJo2pXh5Tl 1njFmUNj403gdy3hZZlyaQQaRwnmDwFWJPsfvw55qVguucQJAX6Vum0ABj6y6koQOdjQK/W/7HW/ lwLFCRsI3FU34oH7N4RDYiDK51ZLZer+bMEkkyShNOsF/5oirpt9P/FlUQqmMGqz9IgcgA38coro g14= -----END CERTIFICATE----- """ pagekite-0.5.8a/debian/pagekite/DEBIAN/0000775000175000017500000000000012610153761016761 5ustar brebre00000000000000pagekite-0.5.8a/debian/pagekite/DEBIAN/prerm0000755000175000017500000000274512610153671020042 0ustar brebre00000000000000#!/bin/sh # prerm script for pagekite # # see: dh_installdeb(1) set -e # summary of how this script can be called: # * `remove' # * `upgrade' # * `failed-upgrade' # * `remove' `in-favour' # * `deconfigure' `in-favour' # `removing' # # for details, see http://www.debian.org/doc/debian-policy/ or # the debian-policy package if dpkg-maintscript-helper supports rm_conffile 2>/dev/null; then dpkg-maintscript-helper rm_conffile /etc/pagekite.rc 0.3.9-1 -- "$@" dpkg-maintscript-helper rm_conffile /etc/pagekite/pagekite.rc -- "$@" dpkg-maintscript-helper rm_conffile /etc/pagekite/local.rc -- "$@" fi case "$1" in remove|upgrade|deconfigure) ;; failed-upgrade) ;; *) echo "prerm called with unknown argument \`$1'" >&2 exit 1 ;; esac # dh_installdeb will replace this with shell code automatically # generated by other debhelper scripts. # Automatically added by dh_pysupport if which update-python-modules >/dev/null 2>&1; then update-python-modules -c pagekite.public fi # End automatically added section # Automatically added by dh_installinit if [ -x "/etc/init.d/pagekite" ] || [ -e "/etc/init/pagekite.conf" ]; then invoke-rc.d pagekite stop || exit $? fi # End automatically added section exit 0 pagekite-0.5.8a/debian/pagekite/DEBIAN/control0000644000175000017500000000167512610153673020375 0ustar brebre00000000000000Package: pagekite Source: pagekite-0.5.8a Version: 0.5.8a-0pagekite Architecture: all Maintainer: PageKite Packaging Team Installed-Size: 529 Depends: python (>= 2.3), python-support (>= 0.90.0), daemon (>= 0.6), python (<< 3.0), python-socksipychain (>= 2.0.15), python-openssl Section: net Priority: optional Homepage: https://pagekite.net/ Description: Make localhost servers publicly visible. PageKite is a system for running publicly visible servers (generally web servers) on machines without a direct connection to the Internet, such as mobile devices or computers behind restrictive firewalls. PageKite works around NAT, firewalls and IP-address limitations by using a combination of tunnels and reverse proxies. . Natively supported protocols: HTTP, HTTPS Partially supported protocols: IRC, Finger . Any other TCP-based service, including SSH and VNC, may be exposed as well to clients supporting HTTP Proxies. pagekite-0.5.8a/debian/pagekite/DEBIAN/preinst0000755000175000017500000000170112610153671020370 0ustar brebre00000000000000#!/bin/sh # preinst script for pagekite # # see: dh_installdeb(1) set -e # summary of how this script can be called: # * `install' # * `install' # * `upgrade' # * `abort-upgrade' # for details, see http://www.debian.org/doc/debian-policy/ or # the debian-policy package if dpkg-maintscript-helper supports rm_conffile 2>/dev/null; then dpkg-maintscript-helper rm_conffile /etc/pagekite.rc 0.3.9-1 -- "$@" dpkg-maintscript-helper rm_conffile /etc/pagekite/pagekite.rc -- "$@" dpkg-maintscript-helper rm_conffile /etc/pagekite/local.rc -- "$@" fi case "$1" in install|upgrade) ;; abort-upgrade) ;; *) echo "preinst called with unknown argument \`$1'" >&2 exit 1 ;; esac # dh_installdeb will replace this with shell code automatically # generated by other debhelper scripts. exit 0 pagekite-0.5.8a/debian/pagekite/DEBIAN/postinst0000755000175000017500000000474412610153671020601 0ustar brebre00000000000000#!/bin/sh # postinst script for pagekite # # see: dh_installdeb(1) set -e # summary of how this script can be called: # * `configure' # * `abort-upgrade' # * `abort-remove' `in-favour' # # * `abort-remove' # * `abort-deconfigure' `in-favour' # `removing' # # for details, see http://www.debian.org/doc/debian-policy/ or # the debian-policy package if dpkg-maintscript-helper supports rm_conffile 2>/dev/null; then dpkg-maintscript-helper rm_conffile /etc/pagekite.rc 0.3.9-1 -- "$@" dpkg-maintscript-helper rm_conffile /etc/pagekite/pagekite.rc -- "$@" dpkg-maintscript-helper rm_conffile /etc/pagekite/local.rc -- "$@" fi case "$1" in configure) [ -e /etc/pagekite/local.rc ] \ && mv /etc/pagekite/local.rc /etc/pagekite.d/99_old_local.rc [ -e /etc/pagekite/pagekite.rc ] \ && mv /etc/pagekite/pagekite.rc /etc/pagekite.d/89_old_pagekite.rc [ -e /etc/pagekite/local.rc.dpkg-bak ] \ && mv /etc/pagekite/local.rc.dpkg-bak /etc/pagekite.d/99_old_local.rc [ -e /etc/pagekite/pagekite.rc.dpkg-bak ] \ && mv /etc/pagekite/pagekite.rc.dpkg-bak /etc/pagekite.d/89_old_pagekite.rc chmod 644 /etc/pagekite.d/* || true chmod 600 /etc/pagekite.d/[019]* || true [ -d /etc/pagekite ] && rmdir /etc/pagekite || true ;; abort-upgrade|abort-remove|abort-deconfigure) ;; *) echo "postinst called with unknown argument \`$1'" >&2 exit 1 ;; esac # This has to happen before the default debhelper stuff, because CDBS gets # the order wrong. if which update-python-modules >/dev/null 2>&1; then update-python-modules pagekite >/dev/null 2>&1 || true fi # dh_installdeb will replace this with shell code automatically # generated by other debhelper scripts. # Automatically added by dh_installinit if [ -x "/etc/init.d/pagekite" ] || [ -e "/etc/init/pagekite.conf" ]; then if [ ! -e "/etc/init/pagekite.conf" ]; then update-rc.d pagekite defaults >/dev/null fi invoke-rc.d pagekite start || exit $? fi # End automatically added section # Automatically added by dh_pysupport if which update-python-modules >/dev/null 2>&1; then update-python-modules pagekite.public fi # End automatically added section exit 0 pagekite-0.5.8a/debian/pagekite/DEBIAN/md5sums0000644000175000017500000000530412610153673020303 0ustar brebre000000000000003229763261060ba404dbdff1bae1190e usr/bin/lapcat 57c9364ed7db9d799f730ce3fb3e38c8 usr/bin/pagekite 55e7b8cb55b0cc3efdde9ee52cce4c3b usr/share/doc/pagekite/COPYING.gz 70d3e66bf70cd647359c03413988b6af usr/share/doc/pagekite/CREDITS.txt f3d79fdf4798017cb42dfd9aa06164ab usr/share/doc/pagekite/HISTORY.txt.gz 0c5bfa386f21350bdc5d334040f13fdc usr/share/doc/pagekite/README.md.gz f572fc5ae01acb54fdd812420ea8a8eb usr/share/doc/pagekite/changelog.Debian.gz cffa7d88bad426052fce523facd89726 usr/share/doc/pagekite/copyright ef213d06895f01be5022a6861c95cc5e usr/share/man/man1/lapcat.1.gz 34e15c4367c70f501a92f5c2e8de1b60 usr/share/man/man1/pagekite.1.gz 3b43e885cf7bb9dd70bb50c1ce7f550c usr/share/pyshared/pagekite-0.5.8a.egg-info/PKG-INFO 464170b3fbac96f45e505889640a4b2c usr/share/pyshared/pagekite-0.5.8a.egg-info/SOURCES.txt 68b329da9893e34099c7d8ad5cb9c940 usr/share/pyshared/pagekite-0.5.8a.egg-info/dependency_links.txt f6a677e95904305e3defa9d79406d9a2 usr/share/pyshared/pagekite-0.5.8a.egg-info/requires.txt 1034b12b956c6507dbc20a3072687be3 usr/share/pyshared/pagekite-0.5.8a.egg-info/top_level.txt 447e48bab042eb95390199ebc3540809 usr/share/pyshared/pagekite/__init__.py af184ecd0291936ab6f061c8a94a0e3b usr/share/pyshared/pagekite/__main__.py 2636e790ecd4deb14a7f99408eb30a48 usr/share/pyshared/pagekite/android.py fdabad443c11a6c11614968b4963ea60 usr/share/pyshared/pagekite/common.py 48d47e0fa66d91deae0cb9ffb1859983 usr/share/pyshared/pagekite/compat.py 04c5426bcc53b525ca426bf8a8115067 usr/share/pyshared/pagekite/dropper.py 6b4e82c70969f39ad22c49d226b05660 usr/share/pyshared/pagekite/httpd.py 9bbe05122735c6de0b3ae8d702906a4d usr/share/pyshared/pagekite/logging.py 9cb3cace33a87b243ba3b39017d4da50 usr/share/pyshared/pagekite/logparse.py e19498c042d1a115fc086c075775c4fe usr/share/pyshared/pagekite/manual.py ca533b8b4ab2f0e88f82ce9f50641c2e usr/share/pyshared/pagekite/pk.py 5db65c211cdfb5f16c997519688a46a0 usr/share/pyshared/pagekite/proto/__init__.py 1bcd5896bc2eb0641385e895e3e64174 usr/share/pyshared/pagekite/proto/conns.py 498fc736cb94ff174964f2a76cb99a0d usr/share/pyshared/pagekite/proto/filters.py c078fd4cf1b2c2e7a68535ea61bab326 usr/share/pyshared/pagekite/proto/parsers.py 993428e7106de6104ee89526be699546 usr/share/pyshared/pagekite/proto/proto.py 2845b1ae0259d8c0507b465848558dc0 usr/share/pyshared/pagekite/proto/selectables.py 1497d9bf45c73b4808485e76b301e1b2 usr/share/pyshared/pagekite/ui/basic.py 2eef89218646da113b70d6cb00a59179 usr/share/pyshared/pagekite/ui/nullui.py 5453f2485198dafa6a3fcb959f7325e6 usr/share/pyshared/pagekite/ui/remote.py 592ee65d3edec93c4f84479bda327229 usr/share/pyshared/pagekite/yamond.py 7807b0b8fe32e8fbe617499fcaaa159d usr/share/python-support/pagekite.public pagekite-0.5.8a/debian/pagekite/DEBIAN/postrm0000755000175000017500000000306512610153671020235 0ustar brebre00000000000000#!/bin/sh # postrm script for pagekite # # see: dh_installdeb(1) set -e # summary of how this script can be called: # * `remove' # * `purge' # * `upgrade' # * `failed-upgrade' # * `abort-install' # * `abort-install' # * `abort-upgrade' # * `disappear' # # for details, see http://www.debian.org/doc/debian-policy/ or # the debian-policy package if dpkg-maintscript-helper supports rm_conffile 2>/dev/null; then dpkg-maintscript-helper rm_conffile /etc/pagekite.rc 0.3.9-1 -- "$@" dpkg-maintscript-helper rm_conffile /etc/pagekite/pagekite.rc -- "$@" dpkg-maintscript-helper rm_conffile /etc/pagekite/local.rc -- "$@" fi case "$1" in purge) rm -rf /var/log/pagekite ;; remove|upgrade|failed-upgrade|abort-install|abort-upgrade|disappear) ;; *) echo "postrm called with unknown argument \`$1'" >&2 exit 1 ;; esac # dh_installdeb will replace this with shell code automatically # generated by other debhelper scripts. # Automatically added by dh_installinit if [ "$1" = "purge" ] ; then update-rc.d pagekite remove >/dev/null fi # In case this system is running systemd, we make systemd reload the unit files # to pick up changes. if [ -d /run/systemd/system ] ; then systemctl --system daemon-reload >/dev/null || true fi # End automatically added section exit 0 pagekite-0.5.8a/debian/pagekite/DEBIAN/conffiles0000644000175000017500000000026212610153671020652 0ustar brebre00000000000000/etc/logrotate.d/pagekite /etc/pagekite.d/80_sshd.rc.sample /etc/pagekite.d/80_httpd.rc.sample /etc/pagekite.d/10_account.rc /etc/pagekite.d/20_frontends.rc /etc/init.d/pagekite pagekite-0.5.8a/debian/pagekite/etc/0000775000175000017500000000000012610153761016612 5ustar brebre00000000000000pagekite-0.5.8a/debian/pagekite/etc/logrotate.d/0000775000175000017500000000000012610153761021034 5ustar brebre00000000000000pagekite-0.5.8a/debian/pagekite/etc/logrotate.d/pagekite0000644000175000017500000000032312610153664022546 0ustar brebre00000000000000/var/log/pagekite/pagekite.log { daily su daemon daemon missingok rotate 7 postrotate [ ! -f /var/run/pagekite.pid ] || kill -HUP `cat /var/run/pagekite.pid` endscript compress notifempty nocreate } pagekite-0.5.8a/debian/pagekite/etc/pagekite.d/0000775000175000017500000000000012610153761020625 5ustar brebre00000000000000pagekite-0.5.8a/debian/pagekite/etc/pagekite.d/80_sshd.rc.sample0000644000175000017500000000025212610153667023705 0ustar brebre00000000000000#################################[ This file is placed in the Public Domain. ]# # Expose the local SSH daemon service_on = raw/22:@kitename : localhost:22 : @kitesecret pagekite-0.5.8a/debian/pagekite/etc/pagekite.d/80_httpd.rc.sample0000644000175000017500000000101212610153667024062 0ustar brebre00000000000000#################################[ This file is placed in the Public Domain. ]# # Expose the local HTTPD service_on = http:@kitename : localhost:80 : @kitesecret # # Uncomment the following to globally DISABLE the request firewall. Do this # if you are sure you know what you are doing, for more details please see # # #insecure # # To disable the firewall for one kite at a time, use lines like this:: # #service_cfg = KITENAME.pagekite.me/80 : insecure : True # pagekite-0.5.8a/debian/pagekite/etc/pagekite.d/10_account.rc0000644000175000017500000000034412610153667023113 0ustar brebre00000000000000#################################[ This file is placed in the Public Domain. ]# # Replace the following with your account details. kitename = NAME.pagekite.me kitesecret = YOURSECRET # Delete this line! abort_not_configured pagekite-0.5.8a/debian/pagekite/etc/pagekite.d/20_frontends.rc0000644000175000017500000000076612610153667023472 0ustar brebre00000000000000#################################[ This file is placed in the Public Domain. ]# # Front-end selection # # Front-ends accept incoming requests on your behalf and forward them to # your PageKite, which in turn forwards them to the actual server. You # probably need at least one, the service defaults will choose one for you. # Use the pagekite.net service defaults. defaults # If you want to use your own, use something like: # frontend = hostname:port # or: # frontends = COUNT:dnsname:port pagekite-0.5.8a/debian/pagekite/etc/init.d/0000775000175000017500000000000012610153761017777 5ustar brebre00000000000000pagekite-0.5.8a/debian/pagekite/etc/init.d/pagekite0000755000175000017500000001151512603542201021510 0ustar brebre00000000000000#! /bin/sh ### BEGIN INIT INFO # Provides: pagekite # Required-Start: $remote_fs $syslog $named # Required-Stop: $remote_fs $syslog $named # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: PageKite system service # Description: PageKite makes localhost servers publicly visible. ### END INIT INFO # Authors: Bjarni R. Einarsson # Hrafnkell Eiriksson # Do NOT "set -e" # PATH should only include /usr/* if it runs after the mountnfs.sh script PATH=/sbin:/usr/sbin:/bin:/usr/bin DESC="PageKite system service" NAME=pagekite RUNAS=daemon:daemon DAEMON=/usr/bin/$NAME WRAPPER=/usr/bin/daemon PIDFILE=/var/run/$NAME.pid LOGFILE=/var/log/$NAME/$NAME.log WRAPPER_PIDFILE=$PIDFILE.wrapper WRAPPER_ARGS="--noconfig --unsafe --respawn --delay=60 --name=$NAME" DAEMON_ARGS="--clean \ --runas=$RUNAS \ --logfile=$LOGFILE \ --optdir=/etc/$NAME.d" SCRIPTNAME=/etc/init.d/$NAME # Exit if the package is not installed [ -x "$DAEMON" ] || exit 0 # Exit if package is unconfigured grep -c ^abort_not_configured /etc/pagekite.d/10_account.rc \ 2>/dev/null >/dev/null && exit 0 # Read configuration variable file if it is present [ -r /etc/default/$NAME ] && . /etc/default/$NAME # Load the VERBOSE setting and other rcS variables . /lib/init/vars.sh # Define LSB log_* functions. # Depend on lsb-base (>= 3.0-6) to ensure that this file is present. . /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 touch $LOGFILE chown $RUNAS $(dirname $LOGFILE) $LOGFILE if [ -x $WRAPPER ]; then start-stop-daemon --quiet --pidfile $WRAPPER_PIDFILE --test --start \ --startas $WRAPPER > /dev/null \ || return 1 start-stop-daemon \ --quiet --pidfile $WRAPPER_PIDFILE --start --startas $WRAPPER -- \ --pidfile $WRAPPER_PIDFILE $WRAPPER_ARGS -- $DAEMON \ --pidfile $PIDFILE $DAEMON_ARGS --noloop \ || return 2 else start-stop-daemon --quiet --pidfile $PIDFILE --test --start \ --startas $DAEMON > /dev/null \ || return 1 start-stop-daemon \ --quiet --pidfile $PIDFILE --start --startas $DAEMON -- \ --pidfile $PIDFILE --daemonize $DAEMON_ARGS \ || return 2 fi # Add code here, if necessary, that waits for the process to be ready # to handle requests from services started subsequently which depend # on this one. As a last resort, sleep for some time. } # # 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 if [ -e $WRAPPER_PIDFILE ]; then start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 \ --pidfile $WRAPPER_PIDFILE else WRAPPERS=$(ps axw |grep $WRAPPER |grep $DAEMON \ |grep $LOGFILE |cut -b1-5) if [ "$WRAPPERS" = "" ]; then start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 \ --pidfile $PIDFILE else kill $WRAPPERS start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 \ --pidfile $PIDFILE --oknodo fi fi RETVAL="$?" [ "$RETVAL" = 2 ] && return 2 # Many daemons don't delete their pidfiles when they exit. rm -f $PIDFILE $WRAPPER_PIDFILE return "$RETVAL" } case "$1" in start) [ "$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 $? ;; #reload|force-reload) # # If do_reload() is not implemented then leave this commented out # and leave 'force-reload' as an alias for 'restart'. # #log_daemon_msg "Reloading $DESC" "$NAME" #do_reload #log_end_msg $? #;; restart|force-reload) # # If the "reload" option is implemented then remove the # 'force-reload' alias # 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: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2 exit 3 ;; esac : pagekite-0.5.8a/debian/init.d0000777000175000017500000000000012610153616022460 2../etc/init.d/pagekite.debianustar brebre00000000000000pagekite-0.5.8a/debian/pagekite.postrm.debhelper0000644000175000017500000000052712610153663021241 0ustar brebre00000000000000# Automatically added by dh_installinit if [ "$1" = "purge" ] ; then update-rc.d pagekite remove >/dev/null fi # In case this system is running systemd, we make systemd reload the unit files # to pick up changes. if [ -d /run/systemd/system ] ; then systemctl --system daemon-reload >/dev/null || true fi # End automatically added section pagekite-0.5.8a/debian/postinst0000664000175000017500000000405412603542200016047 0ustar brebre00000000000000#!/bin/sh # postinst script for pagekite # # see: dh_installdeb(1) set -e # summary of how this script can be called: # * `configure' # * `abort-upgrade' # * `abort-remove' `in-favour' # # * `abort-remove' # * `abort-deconfigure' `in-favour' # `removing' # # for details, see http://www.debian.org/doc/debian-policy/ or # the debian-policy package if dpkg-maintscript-helper supports rm_conffile 2>/dev/null; then dpkg-maintscript-helper rm_conffile /etc/pagekite.rc 0.3.9-1 -- "$@" dpkg-maintscript-helper rm_conffile /etc/pagekite/pagekite.rc -- "$@" dpkg-maintscript-helper rm_conffile /etc/pagekite/local.rc -- "$@" fi case "$1" in configure) [ -e /etc/pagekite/local.rc ] \ && mv /etc/pagekite/local.rc /etc/pagekite.d/99_old_local.rc [ -e /etc/pagekite/pagekite.rc ] \ && mv /etc/pagekite/pagekite.rc /etc/pagekite.d/89_old_pagekite.rc [ -e /etc/pagekite/local.rc.dpkg-bak ] \ && mv /etc/pagekite/local.rc.dpkg-bak /etc/pagekite.d/99_old_local.rc [ -e /etc/pagekite/pagekite.rc.dpkg-bak ] \ && mv /etc/pagekite/pagekite.rc.dpkg-bak /etc/pagekite.d/89_old_pagekite.rc chmod 644 /etc/pagekite.d/* || true chmod 600 /etc/pagekite.d/[019]* || true [ -d /etc/pagekite ] && rmdir /etc/pagekite || true ;; abort-upgrade|abort-remove|abort-deconfigure) ;; *) echo "postinst called with unknown argument \`$1'" >&2 exit 1 ;; esac # This has to happen before the default debhelper stuff, because CDBS gets # the order wrong. if which update-python-modules >/dev/null 2>&1; then update-python-modules pagekite >/dev/null 2>&1 || true fi # dh_installdeb will replace this with shell code automatically # generated by other debhelper scripts. #DEBHELPER# exit 0 pagekite-0.5.8a/debian/files0000644000175000017500000000005712610153673015275 0ustar brebre00000000000000pagekite_0.5.8a-0pagekite_all.deb net optional pagekite-0.5.8a/debian/changelog.in0000664000175000017500000000047212603542200016520 0ustar brebre00000000000000pagekite-@VERSION@ (@VERSION@-0pagekite) unstable; urgency=low * Automatic package build note. -- PageKite Packaging Team @DATE@ pagekite-0.4.0 (0.4.0-0) unstable; urgency=low * Initial release -- PageKite Packaging Team Fri, 29 Jul 2011 22:39:51 +0000 pagekite-0.5.8a/debian/changelog0000664000175000017500000000051512610153616016120 0ustar brebre00000000000000pagekite-0.5.8a (0.5.8a-0pagekite) unstable; urgency=low * Automatic package build note. -- PageKite Packaging Team Fri, 16 Oct 2015 11:55:10 +0100 pagekite-0.4.0 (0.4.0-0) unstable; urgency=low * Initial release -- PageKite Packaging Team Fri, 29 Jul 2011 22:39:51 +0000 pagekite-0.5.8a/debian/pagekite.logrotate0000777000175000017500000000000012610153616026120 2../etc/logrotate.d/pagekite.debianustar brebre00000000000000pagekite-0.5.8a/debian/postrm0000664000175000017500000000235112603542200015506 0ustar brebre00000000000000#!/bin/sh # postrm script for pagekite # # see: dh_installdeb(1) set -e # summary of how this script can be called: # * `remove' # * `purge' # * `upgrade' # * `failed-upgrade' # * `abort-install' # * `abort-install' # * `abort-upgrade' # * `disappear' # # for details, see http://www.debian.org/doc/debian-policy/ or # the debian-policy package if dpkg-maintscript-helper supports rm_conffile 2>/dev/null; then dpkg-maintscript-helper rm_conffile /etc/pagekite.rc 0.3.9-1 -- "$@" dpkg-maintscript-helper rm_conffile /etc/pagekite/pagekite.rc -- "$@" dpkg-maintscript-helper rm_conffile /etc/pagekite/local.rc -- "$@" fi case "$1" in purge) rm -rf /var/log/pagekite ;; remove|upgrade|failed-upgrade|abort-install|abort-upgrade|disappear) ;; *) echo "postrm called with unknown argument \`$1'" >&2 exit 1 ;; esac # dh_installdeb will replace this with shell code automatically # generated by other debhelper scripts. #DEBHELPER# exit 0 pagekite-0.5.8a/pagekite/0000775000175000017500000000000012610153761014615 5ustar brebre00000000000000pagekite-0.5.8a/pagekite/__init__.py0000775000175000017500000000163512603542201016727 0ustar brebre00000000000000############################################################################## LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2015, the Beanstalks Project ehf. and Bjarni Runar Einarsson This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see: """ ############################################################################## pagekite-0.5.8a/pagekite/compat.py0000775000175000017500000000746312603542201016460 0ustar brebre00000000000000""" Compatibility hacks to work around differences between Python versions. """ ############################################################################## LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2015, the Beanstalks Project ehf. and Bjarni Runar Einarsson This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see: """ ############################################################################## import common from common import * # System logging on Unix try: import syslog except ImportError: class mockSyslog: def openlog(*args): raise ConfigError('No Syslog on this machine') def syslog(*args): raise ConfigError('No Syslog on this machine') LOG_DAEMON = 0 LOG_DEBUG = 0 LOG_ERROR = 0 LOG_PID = 0 syslog = mockSyslog() # Backwards compatibility for old Pythons. import socket rawsocket = socket.socket if not 'SHUT_RD' in dir(socket): socket.SHUT_RD = 0 socket.SHUT_WR = 1 socket.SHUT_RDWR = 2 try: import datetime ts_to_date = datetime.datetime.fromtimestamp def ts_to_iso(ts=None): return datetime.datetime.fromtimestamp(ts).isoformat() except ImportError: ts_to_date = str ts_to_iso = str try: sorted([1, 2, 3]) except: def sorted(l): tmp = l[:] tmp.sort() return tmp try: sum([1, 2, 3]) except: def sum(l): s = 0 for v in l: s += v return s try: from urlparse import parse_qs, urlparse except ImportError, e: from cgi import parse_qs from urlparse import urlparse try: import hashlib def sha1hex(data): hl = hashlib.sha1() hl.update(data) return hl.hexdigest().lower() except ImportError: import sha def sha1hex(data): return sha.new(data).hexdigest().lower() common.MAGIC_UUID = sha1hex(common.MAGIC_UUID) try: from traceback import format_exc except ImportError: import traceback import StringIO def format_exc(): sio = StringIO.StringIO() traceback.print_exc(file=sio) return sio.getvalue() # Old Pythons lack rsplit def rsplit(ch, data): parts = data.split(ch) if (len(parts) > 2): tail = parts.pop(-1) return (ch.join(parts), tail) else: return parts # SSL/TLS strategy: prefer pyOpenSSL, as it comes with built-in Context # objects. If that fails, look for Python 2.6+ native ssl support and # create a compatibility wrapper. If both fail, bomb with a ConfigError # when the user tries to enable anything SSL-related. # import sockschain socks = sockschain if socks.HAVE_PYOPENSSL: SSL = socks.SSL SEND_ALWAYS_BUFFERS = False SEND_MAX_BYTES = 16 * 1024 TUNNEL_SOCKET_BLOCKS = False elif socks.HAVE_SSL: SSL = socks.SSL SEND_ALWAYS_BUFFERS = True SEND_MAX_BYTES = 4 * 1024 TUNNEL_SOCKET_BLOCKS = True # Workaround for http://bugs.python.org/issue8240 else: SEND_ALWAYS_BUFFERS = False SEND_MAX_BYTES = 16 * 1024 TUNNEL_SOCKET_BLOCKS = False class SSL(object): TLSv1_METHOD = 0 SSLv23_METHOD = 0 class Error(Exception): pass class SysCallError(Exception): pass class WantReadError(Exception): pass class WantWriteError(Exception): pass class ZeroReturnError(Exception): pass class Context(object): def __init__(self, method): raise ConfigError('Neither pyOpenSSL nor python 2.6+ ' 'ssl modules found!') pagekite-0.5.8a/pagekite/yamond.py0000775000175000017500000001341712603542202016461 0ustar brebre00000000000000""" This is a class implementing a flexible metric-store and an HTTP thread for browsing the numbers. """ ############################################################################## LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2015, the Beanstalks Project ehf. and Bjarni Runar Einarsson This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see: """ ############################################################################## import getopt import os import random import re import select import socket import struct import sys import threading import time import traceback import urllib import BaseHTTPServer try: from urlparse import parse_qs, urlparse except Exception, e: from cgi import parse_qs from urlparse import urlparse class YamonRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): def do_yamon_vars(self): self.send_response(200) self.send_header('Content-Type', 'text/plain') self.send_header('Cache-Control', 'no-cache') self.end_headers() self.wfile.write(self.server.yamond.render_vars_text()) def do_heapy(self): from guppy import hpy self.send_response(200) self.send_header('Content-Type', 'text/plain') self.send_header('Cache-Control', 'no-cache') self.end_headers() self.wfile.write(hpy().heap()) def do_404(self): self.send_response(404) self.send_header('Content-Type', 'text/html') self.end_headers() self.wfile.write('

    404: What? Where? Cannot find it!

    ') def do_root(self): self.send_response(200) self.send_header('Content-Type', 'text/html') self.end_headers() self.wfile.write('

    Hello!

    ') def handle_path(self, path, query): if path == '/vars.txt': self.do_yamon_vars() elif path == '/heap.txt': self.do_heapy() elif path == '/': self.do_root() else: self.do_404() def do_GET(self): (scheme, netloc, path, params, query, frag) = urlparse(self.path) qs = parse_qs(query) return self.handle_path(path, query) class YamonHttpServer(BaseHTTPServer.HTTPServer): def __init__(self, yamond, handler): BaseHTTPServer.HTTPServer.__init__(self, yamond.sspec, handler) self.yamond = yamond class YamonD(threading.Thread): """Handle HTTP in a separate thread.""" def __init__(self, sspec, server=YamonHttpServer, handler=YamonRequestHandler): threading.Thread.__init__(self) self.lock = threading.Lock() self.server = server self.handler = handler self.sspec = sspec self.httpd = None self.running = False self.values = {} self.lists = {} self.views = {} def vmax(self, var, value): try: self.lock.acquire() if value > self.values[var]: self.values[var] = value finally: self.lock.release() def vscale(self, var, ratio, add=0): try: self.lock.acquire() if var not in self.values: self.values[var] = 0 self.values[var] *= ratio self.values[var] += add finally: self.lock.release() def vset(self, var, value): try: self.lock.acquire() self.values[var] = value finally: self.lock.release() def vadd(self, var, value, wrap=None): try: self.lock.acquire() if var not in self.values: self.values[var] = 0 self.values[var] += value if wrap is not None and self.values[var] >= wrap: self.values[var] -= wrap finally: self.lock.release() def vmin(self, var, value): try: self.lock.acquire() if value < self.values[var]: self.values[var] = value finally: self.lock.release() def vdel(self, var): try: self.lock.acquire() if var in self.values: del self.values[var] finally: self.lock.release() def lcreate(self, listn, elems): try: self.lock.acquire() self.lists[listn] = [elems, 0, ['' for x in xrange(0, elems)]] finally: self.lock.release() def ladd(self, listn, value): try: self.lock.acquire() lst = self.lists[listn] lst[2][lst[1]] = value lst[1] += 1 lst[1] %= lst[0] finally: self.lock.release() def render_vars_text(self, view=None): if view: if view == 'heapy': from guppy import hpy return hpy().heap() else: values, lists = self.views[view] else: values, lists = self.values, self.lists data = [] for var in values: data.append('%s: %s\n' % (var, values[var])) for lname in lists: (elems, offset, lst) = lists[lname] l = lst[offset:] l.extend(lst[:offset]) data.append('%s: %s\n' % (lname, ' '.join(['%s' % (x, ) for x in l]))) data.sort() return ''.join(data) def quit(self): if self.httpd: self.running = False urllib.urlopen('http://%s:%s/exiting/' % self.sspec, proxies={}).readlines() def run(self): self.httpd = self.server(self, self.handler) self.sspec = self.httpd.server_address self.running = True while self.running: self.httpd.handle_request() if __name__ == '__main__': yd = YamonD(('', 0)) yd.vset('bjarni', 100) yd.lcreate('foo', 2) yd.ladd('foo', 1) yd.ladd('foo', 2) yd.ladd('foo', 3) yd.run() pagekite-0.5.8a/pagekite/logparse.py0000775000175000017500000001315112603542201017000 0ustar brebre00000000000000""" A basic tool for processing and parsing the Pagekite logs. This class doesn't actually do anything much, it's meant for subclassing. """ ############################################################################## LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2015, the Beanstalks Project ehf. and Bjarni Runar Einarsson This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see: """ ############################################################################## import os import sys import time class PageKiteLogParser(object): def __init__(self): pass def ParseLine(self, line, data=None): try: if data is None: data = {} for word in line.split('; '): key, val = word.split('=', 1); data[key] = val return data except Exception: return {'raw': '%s' % line} def ProcessData(self, data): print '%s' % data def ProcessLine(self, line, data=None): self.ProcessData(self.ParseLine(line, data)) def Follow(self, fd, filename): # Record last position... pos = fd.tell() try: if os.stat(filename).st_size < pos: # Re-open log-file if it's been rotated/trucated new_fd = open(filename, 'r') fd.close() return new_fd except (OSError, IOError), e: # Failed to stat or open new file, just try again later. pass # Sleep a bit and then try to read some more time.sleep(1) fd.seek(pos) return fd def ReadLog(self, filename=None, after=None, follow=False): if filename is not None: fd = open(filename, 'r') else: fd = sys.stdin first = True while first or follow: for line in fd: if line.endswith('\n'): data = self.ParseLine(line.strip()) if after is None or ('ts' in data and int(data['ts'], 16) > after): self.ProcessData(data) else: fd.seek(fd.tell() - len(line)) break if follow: fd = self.Follow(fd, filename) first = False def ReadSyslog(self, filename, pname='pagekite.py', after=None, follow=False): fd = open(filename, 'r') tag = ' %s[' % pname first = True while first or follow: for line in fd: if line.endswith('\n'): try: parts = line.split(':', 3) if parts[2].find(tag) > -1: data = self.ParseLine(parts[3].strip()) if after is None or int(data['ts'], 16) > after: self.ProcessData(data) except ValueError, e: pass else: fd.seek(fd.tell() - len(line)) break if follow: fd = self.Follow(fd, filename) first = False class PageKiteLogTracker(PageKiteLogParser): def __init__(self): PageKiteLogParser.__init__(self) self.streams = {} def ProcessRestart(self, data): # Program just restarted, discard streams state. self.streams = {} def ProcessBandwidthRead(self, stream, data): stream['read'] += int(data['read']) def ProcessBandwidthWrote(self, stream, data): stream['wrote'] += int(data['wrote']) def ProcessError(self, stream, data): stream['err'] = data['err'] def ProcessEof(self, stream, data): del self.streams[stream['id']] def ProcessNewStream(self, stream, data): self.streams[stream['id']] = stream stream['read'] = 0 stream['wrote'] = 0 def ProcessData(self, data): if 'id' in data: # This is info about a specific stream... sid = data['id'] if 'proto' in data and 'domain' in data and sid not in self.streams: self.ProcessNewStream(data, data) if sid in self.streams: stream = self.streams[sid] if 'err' in data: self.ProcessError(stream, data) if 'read' in data: self.ProcessBandwidthRead(stream, data) if 'wrote' in data: self.ProcessBandwidthWrote(stream, data) if 'eof' in data: self.ProcessEof(stream, data) elif 'started' in data and 'version' in data: self.ProcessRestart(data) class DebugPKLT(PageKiteLogTracker): def ProcessRestart(self, data): PageKiteLogTracker.ProcessRestart(self, data) print 'RESTARTED %s' % data def ProcessNewStream(self, stream, data): PageKiteLogTracker.ProcessNewStream(self, stream, data) print '[%s] NEW %s' % (stream['id'], data) def ProcessBandwidthRead(self, stream, data): PageKiteLogTracker.ProcessBandwidthRead(self, stream, data) print '[%s] BWR %s' % (stream['id'], data) def ProcessBandwidthWrote(self, stream, data): PageKiteLogTracker.ProcessBandwidthWrote(self, stream, data) print '[%s] BWW %s' % (stream['id'], data) def ProcessError(self, stream, data): PageKiteLogTracker.ProcessError(self, stream, data) print '[%s] ERR %s' % (stream['id'], data) def ProcessEof(self, stream, data): PageKiteLogTracker.ProcessEof(self, stream, data) print '[%s] EOF %s' % (stream['id'], data) if __name__ == '__main__': sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0) if len(sys.argv) > 2: DebugPKLT().ReadSyslog(sys.argv[1], pname=sys.argv[2]) else: DebugPKLT().ReadLog(sys.argv[1]) pagekite-0.5.8a/pagekite/ui/0000775000175000017500000000000012610153761015232 5ustar brebre00000000000000pagekite-0.5.8a/pagekite/ui/__init__.py0000664000175000017500000000000012603542202017323 0ustar brebre00000000000000pagekite-0.5.8a/pagekite/ui/remote.py0000775000175000017500000003556512603542202017112 0ustar brebre00000000000000""" This is a user interface class which communicates over a pipe or socket. """ ############################################################################## LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2015, the Beanstalks Project ehf. and Bjarni Runar Einarsson This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see: """ ############################################################################## import re import sys import time import threading from pagekite.compat import * from pagekite.common import * from pagekite.proto.conns import Tunnel from nullui import NullUi class RemoteUi(NullUi): """Stdio based user interface for interacting with other processes.""" DAEMON_FRIENDLY = True ALLOWS_INPUT = True WANTS_STDERR = True EMAIL_RE = re.compile(r'^[a-z0-9!#$%&\'\*\+\/=?^_`{|}~-]+' '(?:\.[a-z0-9!#$%&\'*+/=?^_`{|}~-]+)*@' '(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)*' '(?:[a-zA-Z]{2,4}|museum)$') def __init__(self, welcome=None, wfile=sys.stderr, rfile=sys.stdin): NullUi.__init__(self, welcome=welcome, wfile=wfile, rfile=rfile) self.CLEAR = '' self.NORM = self.WHITE = self.GREY = self.GREEN = self.YELLOW = '' self.BLUE = self.RED = self.MAGENTA = self.CYAN = '' def StartListingBackEnds(self): self.wfile.write('begin_be_list\n') def EndListingBackEnds(self): self.wfile.write('end_be_list\n') def NotifyBE(self, bid, be, has_ssl, dpaths, is_builtin=False, fingerprint=None, now=None): domain = be[BE_DOMAIN] port = be[BE_PORT] proto = be[BE_PROTO] prox = (proto == 'raw') and ' (HTTP proxied)' or '' if proto == 'raw' and port in ('22', 22): proto = 'ssh' url = '%s://%s%s' % (proto, domain, port and (':%s' % port) or '') message = (' be_status:' ' status=%x; bid=%s; domain=%s; port=%s; proto=%s;' ' bhost=%s; bport=%s%s%s%s' '\n') % (be[BE_STATUS], bid, domain, port, proto, be[BE_BHOST], be[BE_BPORT], has_ssl and '; ssl=1' or '', is_builtin and '; builtin=1' or '', fingerprint and ('; fingerprint=%s' % fingerprint) or '') self.wfile.write(message) for path in dpaths: message = (' be_path: domain=%s; port=%s; path=%s; policy=%s; src=%s\n' ) % (domain, port or 80, path, dpaths[path][0], dpaths[path][1]) self.wfile.write(message) def Notify(self, message, prefix=' ', popup=False, color=None, now=None, alignright=''): message = '%s' % message self.wfile.write('notify: %s\n' % message) def NotifyMOTD(self, frontend, message): self.wfile.write('motd: %s %s\n' % (frontend, message.replace('\n', ' '))) def Status(self, tag, message=None, color=None): self.status_tag = tag self.status_msg = '%s' % (message or self.status_msg) if message: self.wfile.write('status_msg: %s\n' % message) if tag: self.wfile.write('status_tag: %s\n' % tag) def Welcome(self, pre=None): self.wfile.write('welcome: %s\n' % (pre or '').replace('\n', ' ')) def StartWizard(self, title): self.wfile.write('start_wizard: %s\n' % title) def Retry(self): self.tries -= 1 if self.tries < 0: raise Exception('Too many tries') return self.tries def EndWizard(self, quietly=False): self.wfile.write('end_wizard: %s\n' % (quietly and 'quietly' or 'done')) def Spacer(self): pass def AskEmail(self, question, default=None, pre=[], wizard_hint=False, image=None, back=None, welcome=True): while self.Retry(): self.wfile.write('begin_ask_email\n') if pre: self.wfile.write(' preamble: %s\n' % '\n'.join(pre).replace('\n', ' ')) if default: self.wfile.write(' default: %s\n' % default) self.wfile.write(' question: %s\n' % (question or '').replace('\n', ' ')) self.wfile.write(' expect: email\n') self.wfile.write('end_ask_email\n') answer = self.rfile.readline().strip() if self.EMAIL_RE.match(answer): return answer if back is not None and answer == 'back': return back def AskLogin(self, question, default=None, email=None, pre=None, wizard_hint=False, image=None, back=None): while self.Retry(): self.wfile.write('begin_ask_login\n') if pre: self.wfile.write(' preamble: %s\n' % '\n'.join(pre).replace('\n', ' ')) if email: self.wfile.write(' default: %s\n' % email) self.wfile.write(' question: %s\n' % (question or '').replace('\n', ' ')) self.wfile.write(' expect: email\n') self.wfile.write(' expect: password\n') self.wfile.write('end_ask_login\n') answer_email = self.rfile.readline().strip() if back is not None and answer_email == 'back': return back answer_pass = self.rfile.readline().strip() if back is not None and answer_pass == 'back': return back if self.EMAIL_RE.match(answer_email) and answer_pass: return (answer_email, answer_pass) def AskYesNo(self, question, default=None, pre=[], yes='Yes', no='No', wizard_hint=False, image=None, back=None): while self.Retry(): self.wfile.write('begin_ask_yesno\n') if yes: self.wfile.write(' yes: %s\n' % yes) if no: self.wfile.write(' no: %s\n' % no) if pre: self.wfile.write(' preamble: %s\n' % '\n'.join(pre).replace('\n', ' ')) if default: self.wfile.write(' default: %s\n' % default) self.wfile.write(' question: %s\n' % (question or '').replace('\n', ' ')) self.wfile.write(' expect: yesno\n') self.wfile.write('end_ask_yesno\n') answer = self.rfile.readline().strip().lower() if back is not None and answer == 'back': return back if answer in ('y', 'n'): return (answer == 'y') if answer == str(default).lower(): return default def AskKiteName(self, domains, question, pre=[], default=None, wizard_hint=False, image=None, back=None): while self.Retry(): self.wfile.write('begin_ask_kitename\n') if pre: self.wfile.write(' preamble: %s\n' % '\n'.join(pre).replace('\n', ' ')) for domain in domains: self.wfile.write(' domain: %s\n' % domain) if default: self.wfile.write(' default: %s\n' % default) self.wfile.write(' question: %s\n' % (question or '').replace('\n', ' ')) self.wfile.write(' expect: kitename\n') self.wfile.write('end_ask_kitename\n') answer = self.rfile.readline().strip().lower() if back is not None and answer == 'back': return back if answer: for d in domains: if answer.endswith(d) or answer.endswith(d): return answer return answer+domains[0] def AskBackends(self, kitename, protos, ports, rawports, question, pre=[], default=None, wizard_hint=False, image=None, back=None): while self.Retry(): self.wfile.write('begin_ask_backends\n') if pre: self.wfile.write(' preamble: %s\n' % '\n'.join(pre).replace('\n', ' ')) count = 0 if self.server_info: protos = self.server_info[Tunnel.S_PROTOS] ports = self.server_info[Tunnel.S_PORTS] rawports = self.server_info[Tunnel.S_RAW_PORTS] self.wfile.write(' kitename: %s\n' % kitename) self.wfile.write(' protos: %s\n' % ', '.join(protos)) self.wfile.write(' ports: %s\n' % ', '.join(ports)) self.wfile.write(' rawports: %s\n' % ', '.join(rawports)) if default: self.wfile.write(' default: %s\n' % default) self.wfile.write(' question: %s\n' % (question or '').replace('\n', ' ')) self.wfile.write(' expect: backends\n') self.wfile.write('end_ask_backends\n') answer = self.rfile.readline().strip().lower() if back is not None and answer == 'back': return back return answer def AskMultipleChoice(self, choices, question, pre=[], default=None, wizard_hint=False, image=None, back=None): while self.Retry(): self.wfile.write('begin_ask_multiplechoice\n') if pre: self.wfile.write(' preamble: %s\n' % '\n'.join(pre).replace('\n', ' ')) count = 0 for choice in choices: count += 1 self.wfile.write(' choice_%d: %s\n' % (count, choice)) if default: self.wfile.write(' default: %s\n' % default) self.wfile.write(' question: %s\n' % (question or '').replace('\n', ' ')) self.wfile.write(' expect: choice_index\n') self.wfile.write('end_ask_multiplechoice\n') answer = self.rfile.readline().strip().lower() try: ch = int(answer) if ch > 0 and ch <= len(choices): return ch except: pass if back is not None and answer == 'back': return back def Tell(self, lines, error=False, back=None): dialog = error and 'error' or 'message' self.wfile.write('tell_%s: %s\n' % (dialog, ' '.join(lines))) def Working(self, message): self.wfile.write('working: %s\n' % message) class PageKiteThread(threading.Thread): def __init__(self, startup_args=None, debug=False): threading.Thread.__init__(self) self.pk = None self.pk_readlock = threading.Condition() self.gui_readlock = threading.Condition() self.debug = debug self.reset() def reset(self): self.pk_incoming = [] self.pk_eof = False self.gui_incoming = '' self.gui_eof = False # These routines are used by the PageKite UI, to communicate with us... def readline(self): try: self.pk_readlock.acquire() while (not self.pk_incoming) and (not self.pk_eof): self.pk_readlock.wait() if self.pk_incoming: line = self.pk_incoming.pop(0) else: line = '' if self.debug: print '>>PK>> %s' % line.strip() return line finally: self.pk_readlock.release() def write(self, data): if self.debug: print '>>GUI>> %s' % data.strip() try: self.gui_readlock.acquire() if data: self.gui_incoming += data else: self.gui_eof = True self.gui_readlock.notify() finally: self.gui_readlock.release() # And these are used by the GUI, to communicate with PageKite. def recv(self, bytecount): try: self.gui_readlock.acquire() while (len(self.gui_incoming) < bytecount) and (not self.gui_eof): self.gui_readlock.wait() data = self.gui_incoming[0:bytecount] self.gui_incoming = self.gui_incoming[bytecount:] return data finally: self.gui_readlock.release() def send(self, data): if not data.endswith('\n') and data != '': raise ValueError('Please always send whole lines') if self.debug: print '< """ ############################################################################# import re import sys import time from nullui import NullUi from pagekite.common import * HTML_BR_RE = re.compile(r'<(br|/p|/li|/tr|/h\d)>\s*') HTML_LI_RE = re.compile(r'
  • \s*') HTML_NBSP_RE = re.compile(r' ') HTML_TAGS_RE = re.compile(r'<[^>\s][^>]*>') def clean_html(text): return HTML_LI_RE.sub(' * ', HTML_NBSP_RE.sub('_', HTML_BR_RE.sub('\n', text))) def Q(text): return HTML_TAGS_RE.sub('', clean_html(text)) class BasicUi(NullUi): """Stdio based user interface.""" DAEMON_FRIENDLY = False WANTS_STDERR = True EMAIL_RE = re.compile(r'^[a-z0-9!#$%&\'\*\+\/=?^_`{|}~-]+' '(?:\.[a-z0-9!#$%&\'*+/=?^_`{|}~-]+)*@' '(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)*' '(?:[a-zA-Z]{2,4}|museum)$') def Notify(self, message, prefix=' ', popup=False, color=None, now=None, alignright=''): now = int(now or time.time()) color = color or self.NORM # We suppress duplicates that are either new or still on the screen. keys = self.notify_history.keys() if len(keys) > 20: for key in keys: if self.notify_history[key] < now-300: del self.notify_history[key] message = '%s' % message if message not in self.notify_history: # Display the time now and then. if (not alignright and (now >= (self.last_tick + 60)) and (len(message) < 68)): try: self.last_tick = now d = datetime.datetime.fromtimestamp(now) alignright = '[%2.2d:%2.2d]' % (d.hour, d.minute) except: pass # Fails on Python 2.2 if not now or now > 0: self.notify_history[message] = now msg = '\r%s %s%s%s%s%s\n' % ((prefix * 3)[0:3], color, message, self.NORM, ' ' * (75-len(message)-len(alignright)), alignright) self.wfile.write(msg) self.Status(self.status_tag, self.status_msg) def NotifyMOTD(self, frontend, motd_message): lc = 1 self.Notify(' ') for line in Q(motd_message).splitlines(): self.Notify((line.strip() or ' ' * (lc+2)), prefix=' ++', color=self.WHITE) lc += 1 self.Notify(' ' * (lc+2), alignright='[MOTD from %s]' % frontend) self.Notify(' ') def Status(self, tag, message=None, color=None): self.status_tag = tag self.status_col = color or self.status_col or self.NORM self.status_msg = '%s' % (message or self.status_msg) if not self.in_wizard: message = self.status_msg msg = ('\r << pagekite.py [%s]%s %s%s%s\r%s' ) % (tag, ' ' * (8-len(tag)), self.status_col, message[:52], ' ' * (52-len(message)), self.NORM) self.wfile.write(msg) if tag == 'exiting': self.wfile.write('\n') def Welcome(self, pre=None): if self.in_wizard: self.wfile.write('%s%s%s' % (self.CLEAR, self.WHITE, self.in_wizard)) if self.welcome: self.wfile.write('%s\r%s\n' % (self.NORM, Q(self.welcome))) self.welcome = None if self.in_wizard and self.wizard_tell: self.wfile.write('\n%s\r' % self.NORM) for line in self.wizard_tell: self.wfile.write('*** %s\n' % Q(line)) self.wizard_tell = None if pre: self.wfile.write('\n%s\r' % self.NORM) for line in pre: self.wfile.write(' %s\n' % Q(line)) self.wfile.write('\n%s\r' % self.NORM) def StartWizard(self, title): self.Welcome() banner = '>>> %s' % title banner = ('%s%s[CTRL+C = Cancel]\n') % (banner, ' ' * (62-len(banner))) self.in_wizard = banner self.tries = 200 def Retry(self): self.tries -= 1 return self.tries def EndWizard(self, quietly=False): if self.wizard_tell: self.Welcome() self.in_wizard = None if sys.platform in ('win32', 'os2', 'os2emx') and not quietly: self.wfile.write('\n<<< press ENTER to continue >>>\n') self.rfile.readline() def Spacer(self): self.wfile.write('\n') def Readline(self): line = self.rfile.readline() if line: return line.strip() else: raise IOError('EOF') def AskEmail(self, question, default=None, pre=[], wizard_hint=False, image=None, back=None, welcome=True): if welcome: self.Welcome(pre) while self.Retry(): self.wfile.write(' => %s ' % (Q(question), )) answer = self.Readline() if default and answer == '': return default if self.EMAIL_RE.match(answer.lower()): return answer if back is not None and answer == 'back': return back raise Exception('Too many tries') def AskLogin(self, question, default=None, email=None, pre=None, wizard_hint=False, image=None, back=None): self.Welcome(pre) def_email, def_pass = default or (email, None) self.wfile.write(' %s\n' % (Q(question), )) if not email: email = self.AskEmail('Your e-mail:', default=def_email, back=back, welcome=False) if email == back: return back import getpass self.wfile.write(' => ') return (email, getpass.getpass() or def_pass) def AskYesNo(self, question, default=None, pre=[], yes='yes', no='no', wizard_hint=False, image=None, back=None): self.Welcome(pre) yn = ((default is True) and '[Y/n]' ) or ((default is False) and '[y/N]' ) or ('[y/n]') while self.Retry(): self.wfile.write(' => %s %s ' % (Q(question), yn)) answer = self.Readline().lower() if default is not None and answer == '': answer = default and 'y' or 'n' if back is not None and answer.startswith('b'): return back if answer in ('y', 'n'): return (answer == 'y') raise Exception('Too many tries') def AskQuestion(self, question, pre=[], default=None, prompt=' =>', wizard_hint=False, image=None, back=None): self.Welcome(pre) self.wfile.write('%s %s ' % (prompt, Q(question))) return self.Readline() def AskKiteName(self, domains, question, pre=[], default=None, wizard_hint=False, image=None, back=None): self.Welcome(pre) if len(domains) == 1: self.wfile.write(('\n (Note: the ending %s will be added for you.)' ) % domains[0]) else: self.wfile.write('\n Please use one of the following domains:\n') for domain in domains: self.wfile.write('\n *%s' % domain) self.wfile.write('\n') while self.Retry(): self.wfile.write('\n => %s ' % Q(question)) answer = self.Readline().lower() if back is not None and answer == 'back': return back elif len(domains) == 1: answer = answer.replace(domains[0], '') if answer and SERVICE_SUBDOMAIN_RE.match(answer): return answer+domains[0] else: for domain in domains: if answer.endswith(domain): answer = answer.replace(domain, '') if answer and SERVICE_SUBDOMAIN_RE.match(answer): return answer+domain self.wfile.write(' (Please only use characters A-Z, 0-9, - and _.)') raise Exception('Too many tries') def AskMultipleChoice(self, choices, question, pre=[], default=None, wizard_hint=False, image=None, back=None): self.Welcome(pre) for i in range(0, len(choices)): self.wfile.write((' %s %d) %s\n' ) % ((default==i+1) and '*' or ' ', i+1, choices[i])) self.wfile.write('\n') while self.Retry(): d = default and (', default=%d' % default) or '' self.wfile.write(' => %s [1-%d%s] ' % (Q(question), len(choices), d)) try: answer = self.Readline().strip() if back is not None and answer.startswith('b'): return back choice = int(answer or default) if choice > 0 and choice <= len(choices): return choice except (ValueError, IndexError): pass raise Exception('Too many tries') def Tell(self, lines, error=False, back=None): if self.in_wizard: self.wizard_tell = lines else: self.Welcome() for line in lines: self.wfile.write(' %s\n' % line) if error: self.wfile.write('\n') return True def Working(self, message): if self.in_wizard: pending_messages = self.wizard_tell or [] self.wizard_tell = pending_messages + [message+' ...'] self.Welcome() self.wizard_tell = pending_messages + [message+' ... done.'] else: self.Tell([message]) return True pagekite-0.5.8a/pagekite/ui/nullui.py0000775000175000017500000002231212603542202017111 0ustar brebre00000000000000""" This is a basic "Null" user interface which does nothing at all. """ ############################################################################## LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2015, the Beanstalks Project ehf. and Bjarni Runar Einarsson This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see: """ ############################################################################## import sys from pagekite.compat import * from pagekite.common import * import pagekite.logging as logging class NullUi(object): """This is a UI that always returns default values or raises errors.""" DAEMON_FRIENDLY = True ALLOWS_INPUT = False WANTS_STDERR = False REJECTED_REASONS = { 'quota': 'You are out of quota', 'nodays': 'Your subscription has expired', 'noquota': 'You are out of quota', 'noconns': 'You are flying too many kites', 'unauthorized': 'Invalid account or shared secret' } def __init__(self, welcome=None, wfile=sys.stderr, rfile=sys.stdin): if sys.platform[:3] in ('win', 'os2'): self.CLEAR = '\n\n%s\n\n' % ('=' * 79) self.NORM = self.WHITE = self.GREY = self.GREEN = self.YELLOW = '' self.BLUE = self.RED = self.MAGENTA = self.CYAN = '' else: self.CLEAR = '\033[H\033[J' self.NORM = '\033[0m' self.WHITE = '\033[1m' self.GREY = '\033[0m' #'\033[30;1m' self.RED = '\033[31;1m' self.GREEN = '\033[32;1m' self.YELLOW = '\033[33;1m' self.BLUE = '\033[34;1m' self.MAGENTA = '\033[35;1m' self.CYAN = '\033[36;1m' self.wfile = wfile self.rfile = rfile self.welcome = welcome self.Reset() self.Splash() def Reset(self): self.in_wizard = False self.wizard_tell = None self.last_tick = 0 self.notify_history = {} self.status_tag = '' self.status_col = self.NORM self.status_msg = '' self.tries = 200 self.server_info = None def Splash(self): pass def Welcome(self): pass def StartWizard(self, title): pass def EndWizard(self, quietly=False): pass def Spacer(self): pass def Browse(self, url): import webbrowser self.Tell(['Opening %s in your browser...' % url]) webbrowser.open(url) def DefaultOrFail(self, question, default): if default is not None: return default raise ConfigError('Unanswerable question: %s' % question) def AskLogin(self, question, default=None, email=None, wizard_hint=False, image=None, back=None): return self.DefaultOrFail(question, default) def AskEmail(self, question, default=None, pre=None, wizard_hint=False, image=None, back=None): return self.DefaultOrFail(question, default) def AskYesNo(self, question, default=None, pre=None, yes='Yes', no='No', wizard_hint=False, image=None, back=None): return self.DefaultOrFail(question, default) def AskQuestion(self, question, pre=[], default=None, prompt=None, wizard_hint=False, image=None, back=None): return self.DefaultOrFail(question, default) def AskKiteName(self, domains, question, pre=[], default=None, wizard_hint=False, image=None, back=None): return self.DefaultOrFail(question, default) def AskMultipleChoice(self, choices, question, pre=[], default=None, wizard_hint=False, image=None, back=None): return self.DefaultOrFail(question, default) def AskBackends(self, kitename, protos, ports, rawports, question, pre=[], default=None, wizard_hint=False, image=None, back=None): return self.DefaultOrFail(question, default) def Working(self, message): pass def Tell(self, lines, error=False, back=None): if error: logging.LogError(' '.join(lines)) raise ConfigError(' '.join(lines)) else: logging.Log([('message', ' '.join(lines))]) return True def Notify(self, message, prefix=' ', popup=False, color=None, now=None, alignright=''): if popup: logging.Log([('info', '%s%s%s' % (message, alignright and ' ' or '', alignright))]) def NotifyMOTD(self, frontend, message): pass def NotifyKiteRejected(self, proto, domain, reason, crit=False): if reason in self.REJECTED_REASONS: reason = self.REJECTED_REASONS[reason] self.Notify('REJECTED: %s:%s (%s)' % (proto, domain, reason), prefix='!', color=(crit and self.RED or self.YELLOW)) def NotifyList(self, prefix, items, color): items = items[:] while items: show = [] while items and len(prefix) + len(' '.join(show)) < 65: show.append(items.pop(0)) self.Notify(' - %s: %s' % (prefix, ' '.join(show)), color=color) def NotifyServer(self, obj, server_info): self.server_info = server_info self.Notify('Connecting to front-end %s ...' % server_info[obj.S_NAME], color=self.GREY) self.NotifyList('Protocols', server_info[obj.S_PROTOS], self.GREY) self.NotifyList('Ports', server_info[obj.S_PORTS], self.GREY) if 'raw' in server_info[obj.S_PROTOS]: self.NotifyList('Raw ports', server_info[obj.S_RAW_PORTS], self.GREY) def NotifyQuota(self, quota, q_days, q_conns): qMB = 1024 msg = 'Quota: You have %.2f MB' % (float(quota) / qMB) if q_days is not None: msg += ', %d days' % q_days if q_conns is not None: msg += ' and %d connections' % q_conns self.Notify(msg + ' left.', prefix=(int(quota) < qMB) and '!' or ' ', color=self.MAGENTA) def NotifyFlyingFE(self, proto, port, domain, be=None): self.Notify(('Flying: %s://%s%s/' ) % (proto, domain, port and ':'+port or ''), prefix='~<>', color=self.CYAN) def StartListingBackEnds(self): pass def EndListingBackEnds(self): pass def NotifyBE(self, bid, be, has_ssl, dpaths, is_builtin=False, fingerprint=None): domain, port, proto = be[BE_DOMAIN], be[BE_PORT], be[BE_PROTO] prox = (proto == 'raw') and ' (HTTP proxied)' or '' if proto == 'raw' and port in ('22', 22): proto = 'ssh' if has_ssl and proto == 'http': proto = 'https' url = '%s://%s%s' % (proto, domain, port and (':%s' % port) or '') if be[BE_STATUS] == BE_STATUS_UNKNOWN: return if be[BE_STATUS] & BE_STATUS_OK: if be[BE_STATUS] & BE_STATUS_ERR_ANY: status = 'Trying' color = self.YELLOW prefix = ' ' else: status = 'Flying' color = self.CYAN prefix = '~<>' else: return if is_builtin: backend = 'builtin HTTPD' else: backend = '%s:%s' % (be[BE_BHOST], be[BE_BPORT]) self.Notify(('%s %s as %s/%s' ) % (status, backend, url, prox), prefix=prefix, color=color) if status == 'Flying': for dp in sorted(dpaths.keys()): self.Notify(' - %s%s' % (url, dp), color=self.BLUE) if fingerprint and proto.startswith('https'): self.Notify(' - Fingerprint=%s' % fingerprint, color=self.WHITE) self.Notify((' IMPORTANT: For maximum security, use a secure channel' ' to inform your'), color=self.YELLOW) self.Notify(' guests what fingerprint to expect.', color=self.YELLOW) def Status(self, tag, message=None, color=None): pass def ExplainError(self, error, title, subject=None): if error == 'pleaselogin': self.Tell([title, '', 'You already have an account. Log in to continue.' ], error=True) elif error == 'email': self.Tell([title, '', 'Invalid e-mail address. Please try again?' ], error=True) elif error == 'honey': self.Tell([title, '', 'Hmm. Somehow, you triggered the spam-filter.' ], error=True) elif error in ('domaintaken', 'domain', 'subdomain'): self.Tell([title, '', 'Sorry, that domain (%s) is unavailable.' % subject, '', 'If you registered it already, perhaps you need to log on with', 'a different e-mail address?', '' ], error=True) elif error == 'checkfailed': self.Tell([title, '', 'That domain (%s) is not correctly set up.' % subject ], error=True) elif error == 'network': self.Tell([title, '', 'There was a problem communicating with %s.' % subject, '', 'Please verify that you have a working' ' Internet connection and try again!' ], error=True) else: self.Tell([title, 'Error code: %s' % error, 'Try again later?' ], error=True) pagekite-0.5.8a/pagekite/dropper.py0000775000175000017500000000317512603542201016644 0ustar brebre00000000000000""" This is a "dropper template". A dropper is a single-purpose PageKite back-end connector which embeds its own configuration. """ ############################################################################## LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2015, the Beanstalks Project ehf. and Bjarni Runar Einarsson This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see: """ ############################################################################## import sys import pagekite.pk as pk import pagekite.httpd as httpd if __name__ == "__main__": kn = '@KITENAME@' ss = '@SECRET@' if len(sys.argv) == 1: sys.argv.extend([ '--daemonize', '--runas=nobody', '--logfile=/tmp/pagekite-%s.log' % kn, ]) sys.argv[1:1] = [ '--clean', '--noloop', '--nocrashreport', '--defaults', '--kitename=%s' % kn, '--kitesecret=%s' % ss, '--all' ] sys.argv.extend('@ARGS@'.split()) pk.Main(pk.PageKite, pk.Configure, http_handler=httpd.UiRequestHandler, http_server=httpd.UiHttpServer) pagekite-0.5.8a/pagekite/proto/0000775000175000017500000000000012610153761015760 5ustar brebre00000000000000pagekite-0.5.8a/pagekite/proto/__init__.py0000664000175000017500000000173012603542202020064 0ustar brebre00000000000000""" These are the PageKite protocol handling classes. """ ############################################################################## LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2015, the Beanstalks Project ehf. and Bjarni Runar Einarsson This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see: """ ############################################################################## pagekite-0.5.8a/pagekite/proto/filters.py0000775000175000017500000001603512603542202020004 0ustar brebre00000000000000""" These are filters placed at the end of a tunnel for watching or modifying the traffic. """ ############################################################################## LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2015, the Beanstalks Project ehf. and Bjarni Runar Einarsson This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see: """ ############################################################################## import re import time from pagekite.compat import * class TunnelFilter: """Base class for watchers/filters for data going in/out of Tunnels.""" IDLE_TIMEOUT = 1800 def __init__(self, ui): self.sid = {} self.ui = ui def clean_idle_sids(self, now=None): now = now or time.time() for sid in self.sid.keys(): if self.sid[sid]['_ts'] < now - self.IDLE_TIMEOUT: del self.sid[sid] def filter_set_sid(self, sid, info): now = time.time() if sid not in self.sid: self.sid[sid] = {} self.sid[sid].update(info) self.sid[sid]['_ts'] = now self.clean_idle_sids(now=now) def filter_data_in(self, tunnel, sid, data): if sid not in self.sid: self.sid[sid] = {} self.sid[sid]['_ts'] = time.time() return data def filter_data_out(self, tunnel, sid, data): if sid not in self.sid: self.sid[sid] = {} self.sid[sid]['_ts'] = time.time() return data class TunnelWatcher(TunnelFilter): """Base class for watchers/filters for data going in/out of Tunnels.""" def __init__(self, ui, watch_level=0): TunnelFilter.__init__(self, ui) self.watch_level = watch_level def format_data(self, data, level): if '\r\n\r\n' in data: head, tail = data.split('\r\n\r\n', 1) output = self.format_data(head, level) output[-1] += '\\r\\n' output.append('\\r\\n') if tail: output.extend(self.format_data(tail, level)) return output else: output = data.encode('string_escape').replace('\\n', '\\n\n') if output.count('\\') > 0.15*len(output): if level > 2: output = [['', '']] count = 0 for d in data: output[-1][0] += '%2.2x' % ord(d) output[-1][1] += '%c' % ((ord(d) > 31 and ord(d) < 127) and d or '.') count += 1 if (count % 2) == 0: output[-1][0] += ' ' if (count % 20) == 0: output.append(['', '']) return ['%-50s %s' % (l[0], l[1]) for l in output] else: return ['<< Binary bytes: %d >>' % len(data)] else: return output.strip().splitlines() def now(self): return ts_to_iso(int(10*time.time())/10.0 ).replace('T', ' ').replace('00000', '') def filter_data_in(self, tunnel, sid, data): if data and self.watch_level[0] > 0: self.ui.Notify('===[ INCOMING @ %s ]===' % self.now(), color=self.ui.WHITE, prefix=' __') for line in self.format_data(data, self.watch_level[0]): self.ui.Notify(line, prefix=' <=', now=-1, color=self.ui.GREEN) return TunnelFilter.filter_data_in(self, tunnel, sid, data) def filter_data_out(self, tunnel, sid, data): if data and self.watch_level[0] > 1: self.ui.Notify('===[ OUTGOING @ %s ]===' % self.now(), color=self.ui.WHITE, prefix=' __') for line in self.format_data(data, self.watch_level[0]): self.ui.Notify(line, prefix=' =>', now=-1, color=self.ui.BLUE) return TunnelFilter.filter_data_out(self, tunnel, sid, data) class HttpHeaderFilter(TunnelFilter): """Filter that adds X-Forwarded-For and X-Forwarded-Proto to requests.""" HTTP_HEADER = re.compile('(?ism)^(([A-Z]+) ([^\n]+) HTTP/\d+\.\d+\s*)$') DISABLE = 'rawheaders' def filter_data_in(self, tunnel, sid, data): info = self.sid.get(sid) if (info and info.get('proto') in ('http', 'http2', 'http3', 'websocket') and not info.get(self.DISABLE, False)): # FIXME: Check content-length and skip bodies entirely http_hdr = self.HTTP_HEADER.search(data) if http_hdr: data = self.filter_header_data_in(http_hdr, data, info) return TunnelFilter.filter_data_in(self, tunnel, sid, data) def filter_header_data_in(self, http_hdr, data, info): clean_headers = [ r'(?mi)^(X-(PageKite|Forwarded)-(For|Proto|Port):)' ] add_headers = [ 'X-Forwarded-For: %s' % info.get('remote_ip', 'unknown'), 'X-Forwarded-Proto: %s' % (info.get('using_tls') and 'https' or 'http'), 'X-PageKite-Port: %s' % info.get('port', 0) ] if info.get('rewritehost', False): add_headers.append('Host: %s' % info.get('rewritehost')) clean_headers.append(r'(?mi)^(Host:)') if http_hdr.group(1).upper() in ('POST', 'PUT'): # FIXME: This is a bit ugly add_headers.append('Connection: close') clean_headers.append(r'(?mi)^(Connection|Keep-Alive):') info['rawheaders'] = True for hdr_re in clean_headers: data = re.sub(hdr_re, 'X-Old-\\1', data) return re.sub(self.HTTP_HEADER, '\\1\n%s\r' % '\r\n'.join(add_headers), data) class HttpSecurityFilter(HttpHeaderFilter): """Filter that blocks known-to-be-dangerous requests.""" DISABLE = 'trusted' HTTP_DANGER = re.compile('(?ism)^((get|post|put|patch|delete) ' # xampp paths, anything starting with /adm* '((?:/+(?:xampp/|security/|licenses/|webalizer/|server-(?:status|info)|adm)' '|[^\n]*/' # WordPress admin pages '(?:wp-admin/(?!admin-ajax|css/)|wp-config\.php' # Hackzor tricks '|system32/|\.\.|\.ht(?:access|pass)' # phpMyAdmin and similar tools '|(?:php|sql)?my(?:sql)?(?:adm|manager)' # Setup pages for common PHP tools '|(?:adm[^\n]*|install[^\n]*|setup)\.php)' ')[^\n]*)' ' HTTP/\d+\.\d+\s*)$') REJECT = 'PAGEKITE_REJECT_' def filter_header_data_in(self, http_hdr, data, info): danger = self.HTTP_DANGER.search(data) if danger: self.ui.Notify('BLOCKED: %s %s' % (danger.group(2), danger.group(3)), color=self.ui.RED, prefix='***') self.ui.Notify('See https://pagekite.net/support/security/ for more' ' details.') return self.REJECT+data else: return data pagekite-0.5.8a/pagekite/proto/selectables.py0000775000175000017500000006571612603560755020651 0ustar brebre00000000000000""" Selectables are low level base classes which cooperate with our select-loop. """ ############################################################################## LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2015, the Beanstalks Project ehf. and Bjarni Runar Einarsson This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see: """ ############################################################################## import errno import struct import threading import time import zlib from pagekite.compat import * from pagekite.common import * import pagekite.logging as logging import pagekite.compat as compat import pagekite.common as common def obfuIp(ip): quads = ('%s' % ip).replace(':', '.').split('.') return '~%s' % '.'.join([q for q in quads[-2:]]) SELECTABLE_LOCK = threading.Lock() SELECTABLE_ID = 0 SELECTABLES = {} def getSelectableId(what): global SELECTABLES, SELECTABLE_ID, SELECTABLE_LOCK try: SELECTABLE_LOCK.acquire() count = 0 while SELECTABLE_ID in SELECTABLES: SELECTABLE_ID += 1 SELECTABLE_ID %= 0x10000 if (SELECTABLE_ID % 0x00800) == 0: logging.LogDebug('Selectable map: %s' % (SELECTABLES, )) count += 1 if count > 0x10001: raise ValueError('Too many conns!') SELECTABLES[SELECTABLE_ID] = what return SELECTABLE_ID finally: SELECTABLE_LOCK.release() class Selectable(object): """A wrapper around a socket, for use with select.""" HARMLESS_ERRNOS = (errno.EINTR, errno.EAGAIN, errno.ENOMEM, errno.EBUSY, errno.EDEADLK, errno.EWOULDBLOCK, errno.ENOBUFS, errno.EALREADY) def __init__(self, fd=None, address=None, on_port=None, maxread=16*1024, ui=None, tracked=True, bind=None, backlog=100): self.fd = None try: self.SetFD(fd or rawsocket(socket.AF_INET6, socket.SOCK_STREAM), six=True) if bind: self.fd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.fd.bind(bind) self.fd.listen(backlog) self.fd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) except: self.SetFD(fd or rawsocket(socket.AF_INET, socket.SOCK_STREAM)) if bind: self.fd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.fd.bind(bind) self.fd.listen(backlog) self.fd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.address = address self.on_port = on_port self.created = self.bytes_logged = time.time() self.last_activity = 0 self.dead = False self.ui = ui # Quota-related stuff self.quota = None self.q_conns = None self.q_days = None # Read-related variables self.maxread = maxread self.read_bytes = self.all_in = 0 self.read_eof = False self.peeking = False self.peeked = 0 # Write-related variables self.wrote_bytes = self.all_out = 0 self.write_blocked = '' self.write_speed = 102400 self.write_eof = False self.write_retry = None # Flow control v1 self.throttle_until = (time.time() - 1) self.max_read_speed = 96*1024 # Flow control v2 self.acked_kb_delta = 0 # Compression stuff self.lock = threading.Lock() self.zw = None self.zlevel = 1 self.zreset = False # Logging self.alt_id = None self.countas = 'selectables_live' self.sid = self.gsid = getSelectableId(self.countas) if address: addr = address or ('x.x.x.x', 'x') self.log_id = 's%x/%s:%s' % (self.sid, obfuIp(addr[0]), addr[1]) else: self.log_id = 's%x' % self.sid if common.gYamon: common.gYamon.vadd(self.countas, 1) common.gYamon.vadd('selectables', 1) def CountAs(self, what): if common.gYamon: common.gYamon.vadd(self.countas, -1) common.gYamon.vadd(what, 1) self.countas = what global SELECTABLES SELECTABLES[self.gsid] = '%s %s' % (self.countas, self) def Cleanup(self, close=True): self.peeked = self.zw = '' self.Die(discard_buffer=True) if close: if self.fd: if logging.DEBUG_IO: self.LogDebug('Closing FD: %s' % self) self.fd.close() self.fd = None if not self.dead: self.dead = True self.CountAs('selectables_dead') if close: self.LogTraffic(final=True) def __del__(self): try: if common.gYamon: common.gYamon.vadd(self.countas, -1) common.gYamon.vadd('selectables', -1) except AttributeError: pass try: global SELECTABLES del SELECTABLES[self.gsid] except (KeyError, TypeError): pass def __str__(self): return '%s: %s<%s%s%s>' % (self.log_id, self.__class__, self.read_eof and '-' or 'r', self.write_eof and '-' or 'w', len(self.write_blocked)) def __html__(self): try: peer = self.fd.getpeername() sock = self.fd.getsockname() except: peer = ('x.x.x.x', 'x') sock = ('x.x.x.x', 'x') return ('Outgoing ZChunks: %s
    ' 'Buffered bytes: %s
    ' 'Remote address: %s
    ' 'Local address: %s
    ' 'Bytes in / out: %s / %s
    ' 'Created: %s
    ' 'Status: %s
    ' '\n') % (self.zw and ('level %d' % self.zlevel) or 'off', len(self.write_blocked), self.dead and '-' or (obfuIp(peer[0]), peer[1]), self.dead and '-' or (obfuIp(sock[0]), sock[1]), self.all_in + self.read_bytes, self.all_out + self.wrote_bytes, time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(self.created)), self.dead and 'dead' or 'alive') def ResetZChunks(self): if self.zw: self.zreset = True self.zw = zlib.compressobj(self.zlevel) def EnableZChunks(self, level=1): self.zlevel = level self.zw = zlib.compressobj(level) def SetFD(self, fd, six=False): if self.fd: self.fd.close() self.fd = fd if fd: self.fd.setblocking(0) try: if six: self.fd.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) # This hurts mobile devices, let's try living without it #self.fd.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) #self.fd.setsockopt(socket.SOL_TCP, socket.TCP_KEEPIDLE, 60) #self.fd.setsockopt(socket.SOL_TCP, socket.TCP_KEEPCNT, 10) #self.fd.setsockopt(socket.SOL_TCP, socket.TCP_KEEPINTVL, 1) except: pass def SetConn(self, conn): self.SetFD(conn.fd) self.log_id = conn.log_id self.read_bytes = conn.read_bytes self.wrote_bytes = conn.wrote_bytes def Log(self, values): if self.log_id: values.append(('id', self.log_id)) logging.Log(values) def LogError(self, error, params=None): values = params or [] if self.log_id: values.append(('id', self.log_id)) logging.LogError(error, values) def LogDebug(self, message, params=None): values = params or [] if self.log_id: values.append(('id', self.log_id)) logging.LogDebug(message, values) def LogInfo(self, message, params=None): values = params or [] if self.log_id: values.append(('id', self.log_id)) logging.LogInfo(message, values) def LogTrafficStatus(self, final=False): if self.ui: self.ui.Status('traffic') def LogTraffic(self, final=False): if self.wrote_bytes or self.read_bytes: now = time.time() self.all_out += self.wrote_bytes self.all_in += self.read_bytes self.LogTrafficStatus(final) if common.gYamon: common.gYamon.vadd("bytes_all", self.wrote_bytes + self.read_bytes, wrap=1000000000) if final: self.Log([('wrote', '%d' % self.wrote_bytes), ('wbps', '%d' % self.write_speed), ('read', '%d' % self.read_bytes), ('eof', '1')]) else: self.Log([('wrote', '%d' % self.wrote_bytes), ('wbps', '%d' % self.write_speed), ('read', '%d' % self.read_bytes)]) self.bytes_logged = now self.wrote_bytes = self.read_bytes = 0 elif final: self.Log([('eof', '1')]) global SELECTABLES SELECTABLES[self.gsid] = '%s %s' % (self.countas, self) def SayHello(self): pass def ProcessData(self, data): self.LogError('Selectable::ProcessData: Should be overridden!') return False def ProcessEof(self): global SELECTABLES SELECTABLES[self.gsid] = '%s %s' % (self.countas, self) if self.read_eof and self.write_eof and not self.write_blocked: self.Cleanup() return False return True def ProcessEofRead(self): self.read_eof = True self.LogError('Selectable::ProcessEofRead: Should be overridden!') return False def ProcessEofWrite(self): self.write_eof = True self.LogError('Selectable::ProcessEofWrite: Should be overridden!') return False def EatPeeked(self, eat_bytes=None, keep_peeking=False): if not self.peeking: return if eat_bytes is None: eat_bytes = self.peeked discard = '' while len(discard) < eat_bytes: try: discard += self.fd.recv(eat_bytes - len(discard)) except socket.error, (errno, msg): self.LogInfo('Error reading (%d/%d) socket: %s (errno=%s)' % ( eat_bytes, self.peeked, msg, errno)) time.sleep(0.1) if logging.DEBUG_IO: print '===[ ATE %d PEEKED BYTES ]===\n' % eat_bytes self.peeked -= eat_bytes self.peeking = keep_peeking return def ReadData(self, maxread=None): if self.read_eof: return False now = time.time() maxread = maxread or self.maxread flooded = self.Flooded(now) if flooded > self.max_read_speed and not self.acked_kb_delta: # FIXME: This is v1 flow control, kill it when 0.4.7 is "everywhere" last = self.throttle_until # Disable local throttling for really slow connections; remote # throttles (trigged by blocked sockets) still work. if self.max_read_speed > 1024: self.AutoThrottle() maxread = 1024 if now > last and self.all_in > 2*self.max_read_speed: self.max_read_speed *= 1.25 self.max_read_speed += maxread try: if self.peeking: data = self.fd.recv(maxread, socket.MSG_PEEK) self.peeked = len(data) if logging.DEBUG_IO: print '<== PEEK =[%s]==(\n%s)==' % (self, data[:160]) else: data = self.fd.recv(maxread) if logging.DEBUG_IO: print ('<== IN =[%s @ %dbps]==(\n%s)==' ) % (self, self.max_read_speed, data[:160]) except (SSL.WantReadError, SSL.WantWriteError), err: return True except IOError, err: if err.errno not in self.HARMLESS_ERRNOS: self.LogDebug('Error reading socket: %s (%s)' % (err, err.errno)) return False else: return True except (SSL.Error, SSL.ZeroReturnError, SSL.SysCallError), err: self.LogDebug('Error reading socket (SSL): %s' % err) return False except socket.error, (errno, msg): if errno in self.HARMLESS_ERRNOS: return True else: self.LogInfo('Error reading socket: %s (errno=%s)' % (msg, errno)) return False self.last_activity = now if data is None or data == '': self.read_eof = True if logging.DEBUG_IO: print '<== IN =[%s]==(EOF)==' % self return self.ProcessData('') else: if not self.peeking: self.read_bytes += len(data) if self.acked_kb_delta: self.acked_kb_delta += (len(data)/1024) if self.read_bytes > logging.LOG_THRESHOLD: self.LogTraffic() return self.ProcessData(data) def Flooded(self, now=None): delta = ((now or time.time()) - self.created) if delta >= 1: flooded = self.read_bytes + self.all_in flooded -= self.max_read_speed * 0.95 * delta return flooded else: return 0 def RecordProgress(self, skb, bps): if skb >= 0: all_read = (self.all_in + self.read_bytes) / 1024 if self.acked_kb_delta: self.acked_kb_delta = max(1, all_read - skb) self.LogDebug('Delta is: %d' % self.acked_kb_delta) elif bps >= 0: self.Throttle(max_speed=bps, remote=True) def Throttle(self, max_speed=None, remote=False, delay=0.2): if max_speed: self.max_read_speed = max_speed flooded = max(-1, self.Flooded()) if self.max_read_speed: delay = min(10, max(0.1, flooded/self.max_read_speed)) if flooded < 0: delay = 0 if delay: ot = self.throttle_until self.throttle_until = time.time() + delay if ((self.throttle_until - ot) > 30 or (int(ot) != int(self.throttle_until) and delay > 8)): self.LogInfo('Throttled %.1fs until %x (flood=%d, bps=%s, %s)' % ( delay, self.throttle_until, flooded, self.max_read_speed, remote and 'remote' or 'local')) return True def AutoThrottle(self, max_speed=None, remote=False, delay=0.2): return self.Throttle(max_speed, remote, delay) def Send(self, data, try_flush=False, activity=False, just_buffer=False, allow_blocking=False): self.write_speed = int((self.wrote_bytes + self.all_out) / max(1, (time.time() - self.created))) # If we're already blocked, just buffer unless explicitly asked to flush. if ((just_buffer) or ((not try_flush) and (len(self.write_blocked) > 0 or compat.SEND_ALWAYS_BUFFERS))): self.write_blocked += str(''.join(data)) return True sending = ''.join([self.write_blocked, str(''.join(data))]) self.write_blocked = '' sent_bytes = 0 if sending: try: want_send = self.write_retry or min(len(sending), SEND_MAX_BYTES) sent_bytes = self.fd.send(sending[:want_send]) if logging.DEBUG_IO: print ('==> OUT =[%s: %d/%d bytes]==(\n%s)==' ) % (self, sent_bytes, want_send, sending[:min(160, sent_bytes)]) self.wrote_bytes += sent_bytes self.write_retry = None except (SSL.WantWriteError, SSL.WantReadError), err: if logging.DEBUG_IO: print '=== WRITE SSL RETRY: =[%s: %s bytes]==' % (self, want_send) self.write_retry = want_send except IOError, err: if err.errno not in self.HARMLESS_ERRNOS: self.LogInfo('Error sending: %s' % err) self.ProcessEofWrite() return False else: if logging.DEBUG_IO: print '=== WRITE HICCUP: =[%s: %s bytes]==' % (self, want_send) self.write_retry = want_send except socket.error, (errno, msg): if errno not in self.HARMLESS_ERRNOS: self.LogInfo('Error sending: %s (errno=%s)' % (msg, errno)) self.ProcessEofWrite() return False else: if logging.DEBUG_IO: print '=== WRITE HICCUP: =[%s: %s bytes]==' % (self, want_send) self.write_retry = want_send except (SSL.Error, SSL.ZeroReturnError, SSL.SysCallError), err: self.LogInfo('Error sending (SSL): %s' % err) self.ProcessEofWrite() return False except AttributeError: # This has been seen in the wild, is most likely some sort of # race during shutdown. :-( self.LogInfo('AttributeError, self.fd=%s' % self.fd) self.ProcessEofWrite() return False if activity: self.last_activity = time.time() self.write_blocked = sending[sent_bytes:] if self.wrote_bytes >= logging.LOG_THRESHOLD: self.LogTraffic() if self.write_eof and not self.write_blocked: self.ProcessEofWrite() return True def SendChunked(self, data, compress=True, zhistory=None, just_buffer=False): rst = '' if self.zreset: self.zreset = False rst = 'R' # Stop compressing streams that just get bigger. if zhistory and (zhistory[0] < zhistory[1]): compress = False try: try: if self.lock: self.lock.acquire() sdata = ''.join(data) if self.zw and compress and len(sdata) > 64: try: zdata = self.zw.compress(sdata) + self.zw.flush(zlib.Z_SYNC_FLUSH) if zhistory: zhistory[0] = len(sdata) zhistory[1] = len(zdata) return self.Send(['%xZ%x%s\r\n' % (len(sdata), len(zdata), rst), zdata], activity=False, try_flush=(not just_buffer), just_buffer=just_buffer) except zlib.error: logging.LogError('Error compressing, resetting ZChunks.') self.ResetZChunks() return self.Send(['%x%s\r\n' % (len(sdata), rst), sdata], activity=False, try_flush=(not just_buffer), just_buffer=just_buffer) except UnicodeDecodeError: logging.LogError('UnicodeDecodeError in SendChunked, wtf?') return False finally: if self.lock: self.lock.release() def Flush(self, loops=50, wait=False, allow_blocking=False): while (loops != 0 and len(self.write_blocked) > 0 and self.Send([], try_flush=True, activity=False, allow_blocking=allow_blocking)): if wait and len(self.write_blocked) > 0: time.sleep(0.1) logging.LogDebug('Flushing...') loops -= 1 if self.write_blocked: return False return True def IsReadable(s, now): return (s.fd and (not s.read_eof) and (s.acked_kb_delta < 64) # FIXME and (s.throttle_until <= now)) def IsBlocked(s): return (s.fd and (len(s.write_blocked) > 0)) def IsDead(s): return (s.read_eof and s.write_eof and not s.write_blocked) def Die(self, discard_buffer=False): if discard_buffer: self.write_blocked = '' self.read_eof = self.write_eof = True return True class LineParser(Selectable): """A Selectable which parses the input as lines of text.""" def __init__(self, fd=None, address=None, on_port=None, ui=None, tracked=True): Selectable.__init__(self, fd, address, on_port, ui=ui, tracked=tracked) self.leftovers = '' def __html__(self): return Selectable.__html__(self) def Cleanup(self, close=True): Selectable.Cleanup(self, close=close) self.leftovers = '' def ProcessData(self, data): lines = (self.leftovers+data).splitlines(True) self.leftovers = '' while lines: line = lines.pop(0) if line.endswith('\n'): if self.ProcessLine(line, lines) is False: return False else: if not self.peeking: self.leftovers += line if self.read_eof: return self.ProcessEofRead() return True def ProcessLine(self, line, lines): self.LogError('LineParser::ProcessLine: Should be overridden!') return False TLS_CLIENTHELLO = '%c' % 026 SSL_CLIENTHELLO = '\x80' MINECRAFT_HANDSHAKE = '%c' % (0x02, ) FLASH_POLICY_REQ = '' # FIXME: XMPP support class MagicProtocolParser(LineParser): """A Selectable which recognizes HTTP, TLS or XMPP preambles.""" def __init__(self, fd=None, address=None, on_port=None, ui=None): LineParser.__init__(self, fd, address, on_port, ui=ui, tracked=False) self.leftovers = '' self.might_be_tls = True self.is_tls = False self.my_tls = False def __html__(self): return ('Detected TLS: %s
    ' '%s') % (self.is_tls, LineParser.__html__(self)) # FIXME: DEPRECATE: Make this all go away, switch to CONNECT. def ProcessMagic(self, data): args = {} try: prefix, words, data = data.split('\r\n', 2) for arg in words.split('; '): key, val = arg.split('=', 1) args[key] = val self.EatPeeked(eat_bytes=len(prefix)+2+len(words)+2) except ValueError, e: return True try: port = 'port' in args and args['port'] or None if port: self.on_port = int(port) except ValueError, e: return False proto = 'proto' in args and args['proto'] or None if proto in ('http', 'http2', 'http3', 'websocket'): return LineParser.ProcessData(self, data) domain = 'domain' in args and args['domain'] or None if proto == 'https': return self.ProcessTls(data, domain) if proto == 'raw' and domain: return self.ProcessProto(data, 'raw', domain) return False def ProcessData(self, data): # Uncomment when adding support for new protocols: # #self.LogDebug(('DATA: >%s<' # ) % ' '.join(['%2.2x' % ord(d) for d in data])) if data.startswith(MAGIC_PREFIX): return self.ProcessMagic(data) if self.might_be_tls: self.might_be_tls = False if not (data.startswith(TLS_CLIENTHELLO) or data.startswith(SSL_CLIENTHELLO)): self.EatPeeked() # FIXME: These only work if the full policy request or minecraft # handshake are present in the first data packet. if data.startswith(FLASH_POLICY_REQ): return self.ProcessFlashPolicyRequest(data) if data.startswith(MINECRAFT_HANDSHAKE): user, server, port = self.GetMinecraftInfo(data) if user and server: return self.ProcessProto(data, 'minecraft', server) return LineParser.ProcessData(self, data) self.is_tls = True if self.is_tls: return self.ProcessTls(data) else: self.EatPeeked() return LineParser.ProcessData(self, data) def GetMsg(self, data): mtype, ml24, mlen = struct.unpack('>BBH', data[0:4]) mlen += ml24 * 0x10000 return mtype, data[4:4+mlen], data[4+mlen:] def GetClientHelloExtensions(self, msg): # Ugh, so many magic numbers! These are accumulated sizes of # the different fields we are ignoring in the TLS headers. slen = struct.unpack('>B', msg[34])[0] cslen = struct.unpack('>H', msg[35+slen:37+slen])[0] cmlen = struct.unpack('>B', msg[37+slen+cslen])[0] extofs = 34+1+2+1+2+slen+cslen+cmlen if extofs < len(msg): return msg[extofs:] return None def GetSniNames(self, extensions): names = [] while extensions: etype, elen = struct.unpack('>HH', extensions[0:4]) if etype == 0: # OK, we found an SNI extension, get the list. namelist = extensions[6:4+elen] while namelist: ntype, nlen = struct.unpack('>BH', namelist[0:3]) if ntype == 0: names.append(namelist[3:3+nlen].lower()) namelist = namelist[3+nlen:] extensions = extensions[4+elen:] return names def GetSni(self, data): hello, vmajor, vminor, mlen = struct.unpack('>BBBH', data[0:5]) data = data[5:] sni = [] while data: mtype, msg, data = self.GetMsg(data) if mtype == 1: # ClientHello! sni.extend(self.GetSniNames(self.GetClientHelloExtensions(msg))) return sni def GetMinecraftInfo(self, data): try: (packet, version, unlen) = struct.unpack('>bbh', data[0:4]) unlen *= 2 (hnlen, ) = struct.unpack('>h', data[4+unlen:6+unlen]) hnlen *= 2 (port, ) = struct.unpack('>i', data[6+unlen+hnlen:10+unlen+hnlen]) uname = data[4:4+unlen].decode('utf_16_be').encode('utf-8') sname = data[6+unlen:6+hnlen+unlen].decode('utf_16_be').encode('utf-8') return uname, sname, port except: return None, None, None def ProcessFlashPolicyRequest(self, data): self.LogError('MagicProtocolParser::ProcessFlashPolicyRequest: Should be overridden!') return False def ProcessTls(self, data, domain=None): self.LogError('MagicProtocolParser::ProcessTls: Should be overridden!') return False def ProcessProto(self, data, proto, domain): self.LogError('MagicProtocolParser::ProcessProto: Should be overridden!') return False class ChunkParser(Selectable): """A Selectable which parses the input as chunks.""" def __init__(self, fd=None, address=None, on_port=None, ui=None): Selectable.__init__(self, fd, address, on_port, ui=ui) self.want_cbytes = 0 self.want_bytes = 0 self.compressed = False self.header = '' self.chunk = '' self.zr = zlib.decompressobj() def __html__(self): return Selectable.__html__(self) def Cleanup(self, close=True): Selectable.Cleanup(self, close=close) self.zr = self.chunk = self.header = None def ProcessData(self, data): loops = 1500 result = more = True while result and more and (loops > 0): loops -= 1 if self.peeking: self.want_cbytes = 0 self.want_bytes = 0 self.header = '' self.chunk = '' if self.want_bytes == 0: self.header += (data or '') if self.header.find('\r\n') < 0: if self.read_eof: return self.ProcessEofRead() return True try: size, data = self.header.split('\r\n', 1) self.header = '' if size.endswith('R'): self.zr = zlib.decompressobj() size = size[0:-1] if 'Z' in size: csize, zsize = size.split('Z') self.compressed = True self.want_cbytes = int(csize, 16) self.want_bytes = int(zsize, 16) else: self.compressed = False self.want_bytes = int(size, 16) except ValueError, err: self.LogError('ChunkParser::ProcessData: %s' % err) self.Log([('bad_data', data)]) return False if self.want_bytes == 0: return False process = data[:self.want_bytes] data = more = data[self.want_bytes:] self.chunk += process self.want_bytes -= len(process) if self.want_bytes == 0: if self.compressed: try: cchunk = self.zr.decompress(self.chunk) except zlib.error: cchunk = '' if len(cchunk) != self.want_cbytes: result = self.ProcessCorruptChunk(self.chunk) else: result = self.ProcessChunk(cchunk) else: result = self.ProcessChunk(self.chunk) self.chunk = '' if result and more: self.LogError('Unprocessed data: %s' % data) raise BugFoundError('Too much data') elif self.read_eof: return self.ProcessEofRead() and result else: return result def ProcessCorruptChunk(self, chunk): self.LogError('ChunkParser::ProcessData: ProcessCorruptChunk not overridden!') return False def ProcessChunk(self, chunk): self.LogError('ChunkParser::ProcessData: ProcessChunk not overridden!') return False pagekite-0.5.8a/pagekite/proto/conns.py0000775000175000017500000017334712607425760017503 0ustar brebre00000000000000""" These are the Connection classes, relatively high level classes that handle incoming or outgoing network connections. """ ############################################################################## LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2015, the Beanstalks Project ehf. and Bjarni Runar Einarsson This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see: """ ############################################################################## import socket import sys import threading import time import traceback from pagekite.compat import * from pagekite.common import * import pagekite.common as common import pagekite.logging as logging from filters import HttpSecurityFilter from selectables import * from parsers import * from proto import * class Tunnel(ChunkParser): """A Selectable representing a PageKite tunnel.""" S_NAME = 0 S_PORTS = 1 S_RAW_PORTS = 2 S_PROTOS = 3 S_ADD_KITES = 4 S_IS_MOBILE = 5 def __init__(self, conns): ChunkParser.__init__(self, ui=conns.config.ui) self.server_info = ['x.x.x.x:x', [], [], [], False, False] self.Init(conns) # We want to be sure to read the entire chunk at once, including # headers to save cycles, so we double the size we're willing to # read here. self.maxread *= 2 def Init(self, conns): self.conns = conns self.users = {} self.remote_ssl = {} self.zhistory = {} self.backends = {} self.last_ping = 0 self.weighted_rtt = -1 self.using_tls = False self.filters = [] def Cleanup(self, close=True): if self.users: for sid in self.users.keys(): self.CloseStream(sid) ChunkParser.Cleanup(self, close=close) self.Init(None) def __html__(self): return ('Server name: %s
    ' '%s') % (self.server_info[self.S_NAME], ChunkParser.__html__(self)) def LogTrafficStatus(self, final=False): if self.ui: if final: message = 'Disconnected from: %s' % self.server_info[self.S_NAME] self.ui.Status('down', color=self.ui.GREY, message=message) else: self.ui.Status('traffic') def GetKiteRequests(self, parse): requests = [] for prefix in ('X-Beanstalk', 'X-PageKite'): for bs in parse.Header(prefix): # X-PageKite: proto:my.domain.com:token:signature proto, domain, srand, token, sign = bs.split(':') requests.append((proto.lower(), domain.lower(), srand, token, sign, prefix)) return requests def _FrontEnd(conn, body, conns): """This is what the front-end does when a back-end requests a new tunnel.""" self = Tunnel(conns) try: for prefix in ('X-Beanstalk', 'X-PageKite'): for feature in conn.parser.Header(prefix+'-Features'): if not conns.config.disable_zchunks: if feature == 'ZChunks': self.EnableZChunks(level=1) elif feature == 'AddKites': self.server_info[self.S_ADD_KITES] = True elif feature == 'Mobile': self.server_info[self.S_IS_MOBILE] = True # Track which versions we see in the wild. version = 'old' for v in conn.parser.Header(prefix+'-Version'): version = v if common.gYamon: common.gYamon.vadd('version-%s' % version, 1, wrap=10000000) for replace in conn.parser.Header(prefix+'-Replace'): if replace in self.conns.conns_by_id: repl = self.conns.conns_by_id[replace] self.LogInfo('Disconnecting old tunnel: %s' % repl) repl.Die(discard_buffer=True) requests = self.GetKiteRequests(conn.parser) except Exception, err: self.LogError('Discarding connection: %s' % err) self.Cleanup() return None except socket.error, err: self.LogInfo('Discarding connection: %s' % err) self.Cleanup() return None self.last_activity = time.time() self.CountAs('backends_live') self.SetConn(conn) if requests: conns.auth().check(requests[:], conn, lambda r, l: self.AuthCallback(conn, r, l)) return self def RecheckQuota(self, conns, when=None): if when is None: when = time.time() if (self.quota and self.quota[0] is not None and self.quota[1] and (self.quota[2] < when-900)): self.quota[2] = when self.LogDebug('Rechecking: %s' % (self.quota, )) conns.auth().check(self.quota[1], self, lambda r, l: self.QuotaCallback(conns, r, l)) def ProcessAuthResults(self, results, duplicates_ok=False, add_tunnels=True): ok = [] bad = [] if not self.conns: # This can be delayed until the connecting client gives up, which # means we may have already called Die(). In that case, just abort. return True ok_results = ['X-PageKite-OK'] bad_results = ['X-PageKite-Invalid'] if duplicates_ok is True: ok_results.extend(['X-PageKite-Duplicate']) elif duplicates_ok is False: bad_results.extend(['X-PageKite-Duplicate']) for r in results: if r[0] in ok_results: ok.append(r[1]) elif r[0] in bad_results: bad.append(r[1]) elif r[0] == 'X-PageKite-SessionID': self.conns.SetAltId(self, r[1]) logi = [] if self.server_info[self.S_IS_MOBILE]: logi.append(('mobile', 'True')) if self.server_info[self.S_ADD_KITES]: logi.append(('add_kites', 'True')) if bad: for backend in bad: if backend in self.backends: del self.backends[backend] proto, domain, srand = backend.split(':') self.Log([('BE', 'Dead'), ('proto', proto), ('domain', domain)] + logi) self.conns.CloseTunnel(proto, domain, self) if add_tunnels: for backend in ok: if backend not in self.backends: self.backends[backend] = 1 proto, domain, srand = backend.split(':') self.Log([('BE', 'Live'), ('proto', proto), ('domain', domain)] + logi) self.conns.Tunnel(proto, domain, self) if not ok: if self.server_info[self.S_ADD_KITES] and not bad: self.LogDebug('No tunnels configured, idling...') self.conns.SetIdle(self, 60) else: self.LogDebug('No tunnels configured, closing connection.') self.Die() return True def QuotaCallback(self, conns, results, log_info): # Report new values to the back-end... unless they are mobile. if self.quota and (self.quota[0] >= 0): if not self.server_info[self.S_IS_MOBILE]: self.SendQuota() self.ProcessAuthResults(results, duplicates_ok=True, add_tunnels=False) for r in results: if r[0] in ('X-PageKite-OK', 'X-PageKite-Duplicate'): return self # Nothing is OK anymore, give up and shut down the tunnel. self.Log(log_info) self.LogInfo('Ran out of quota or account deleted, closing tunnel.') self.Die() return self def AuthCallback(self, conn, results, log_info): if log_info: logging.Log(log_info) output = [HTTP_ResponseHeader(200, 'OK'), HTTP_Header('Transfer-Encoding', 'chunked'), HTTP_Header('X-PageKite-Features', 'AddKites'), HTTP_Header('X-PageKite-Protos', ', '.join(['%s' % p for p in self.conns.config.server_protos])), HTTP_Header('X-PageKite-Ports', ', '.join( ['%s' % self.conns.config.server_portalias.get(p, p) for p in self.conns.config.server_ports]))] if not self.conns.config.disable_zchunks: output.append(HTTP_Header('X-PageKite-Features', 'ZChunks')) if self.conns.config.server_raw_ports: output.append( HTTP_Header('X-PageKite-Raw-Ports', ', '.join(['%s' % p for p in self.conns.config.server_raw_ports]))) for r in results: output.append('%s: %s\r\n' % r) output.append(HTTP_StartBody()) if not self.Send(output, activity=False, just_buffer=True): conn.LogDebug('No tunnels configured, closing connection (send failed).') self.Die(discard_buffer=True) return self if conn.quota and conn.quota[0]: self.quota = conn.quota self.Log([('BE-Quota', self.quota[0])]) if self.ProcessAuthResults(results): self.conns.Add(self) else: self.Die() return self def ChunkAuthCallback(self, results, log_info): if log_info: logging.Log(log_info) if self.ProcessAuthResults(results): output = ['NOOP: 1\r\n'] for r in results: output.append('%s: %s\r\n' % r) output.append('\r\n!') self.SendChunked(''.join(output), compress=False, just_buffer=True) def _RecvHttpHeaders(self, fd=None): data = '' fd = fd or self.fd while not data.endswith('\r\n\r\n') and not data.endswith('\n\n'): try: buf = fd.recv(1) except: # This is sloppy, but the back-end will just connect somewhere else # instead, so laziness here should be fine. buf = None if buf is None or buf == '': self.LogDebug('Remote end closed connection.') return None data += buf self.read_bytes += len(buf) if logging.DEBUG_IO: print '<== IN (headers) =[%s]==(\n%s)==' % (self, data) return data def _Connect(self, server, conns, tokens=None): if self.fd: self.fd.close() sspec = rsplit(':', server) if len(sspec) < 2: sspec = (sspec[0], 443) # Use chained SocksiPy to secure our communication. socks.DEBUG = (logging.DEBUG_IO or socks.DEBUG) and logging.LogDebug sock = socks.socksocket() if socks.HAVE_SSL: chain = ['default'] if self.conns.config.fe_anon_tls_wrap: chain.append('ssl-anon!%s!%s' % (sspec[0], sspec[1])) if self.conns.config.fe_certname: chain.append('http!%s!%s' % (sspec[0], sspec[1])) chain.append('ssl!%s!443' % ','.join(self.conns.config.fe_certname)) for hop in chain: sock.addproxy(*socks.parseproxy(hop)) self.SetFD(sock) try: self.fd.settimeout(20.0) # Missing in Python 2.2 except: self.fd.setblocking(1) self.LogDebug('Connecting to %s:%s' % (sspec[0], sspec[1])) self.fd.connect((sspec[0], int(sspec[1]))) replace_sessionid = self.conns.config.servers_sessionids.get(server, None) if (not self.Send(HTTP_PageKiteRequest(server, conns.config.backends, tokens, nozchunks=conns.config.disable_zchunks, replace=replace_sessionid), activity=False, try_flush=True, allow_blocking=False) or not self.Flush(wait=True, allow_blocking=False)): self.LogDebug('Failed to send kite request, closing.') return None, None data = self._RecvHttpHeaders() if not data: self.LogDebug('Failed to parse kite response, closing.') return None, None self.fd.setblocking(0) parse = HttpLineParser(lines=data.splitlines(), state=HttpLineParser.IN_RESPONSE) return data, parse def CheckForTokens(self, parse): tcount = 0 tokens = {} for request in parse.Header('X-PageKite-SignThis'): proto, domain, srand, token = request.split(':') tokens['%s:%s' % (proto, domain)] = token tcount += 1 return tcount, tokens def ParsePageKiteCapabilities(self, parse): for portlist in parse.Header('X-PageKite-Ports'): self.server_info[self.S_PORTS].extend(portlist.split(', ')) for portlist in parse.Header('X-PageKite-Raw-Ports'): self.server_info[self.S_RAW_PORTS].extend(portlist.split(', ')) for protolist in parse.Header('X-PageKite-Protos'): self.server_info[self.S_PROTOS].extend(protolist.split(', ')) if not self.conns.config.disable_zchunks: for feature in parse.Header('X-PageKite-Features'): if feature == 'ZChunks': self.EnableZChunks(level=9) elif feature == 'AddKites': self.server_info[self.S_ADD_KITES] = True elif feature == 'Mobile': self.server_info[self.S_IS_MOBILE] = True def HandlePageKiteResponse(self, parse): config = self.conns.config have_kites = 0 have_kite_info = None sname = self.server_info[self.S_NAME] config.ui.NotifyServer(self, self.server_info) for misc in parse.Header('X-PageKite-Misc'): args = parse_qs(misc) logdata = [('FE', sname)] for arg in args: logdata.append((arg, args[arg][0])) logging.Log(logdata) if 'motd' in args and args['motd'][0]: config.ui.NotifyMOTD(sname, args['motd'][0]) # FIXME: Really, we should keep track of quota dimensions for # each kite. At the moment that isn't even reported... for quota in parse.Header('X-PageKite-Quota'): self.quota = [float(quota), None, None] self.Log([('FE', sname), ('quota', quota)]) for quota in parse.Header('X-PageKite-QConns'): self.q_conns = float(quota) self.Log([('FE', sname), ('q_conns', quota)]) for quota in parse.Header('X-PageKite-QDays'): self.q_days = float(quota) self.Log([('FE', sname), ('q_days', quota)]) if self.quota and self.quota[0] is not None: config.ui.NotifyQuota(self.quota[0], self.q_days, self.q_conns) invalid_reasons = {} for request in parse.Header('X-PageKite-Invalid-Why'): # This is future-compatible, in that we can add more fields later. details = request.split(';') invalid_reasons[details[0]] = details[1] for request in parse.Header('X-PageKite-Invalid'): have_kite_info = True proto, domain, srand = request.split(':') reason = invalid_reasons.get(request, 'unknown') self.Log([('FE', sname), ('err', 'Rejected'), ('proto', proto), ('reason', reason), ('domain', domain)]) config.ui.NotifyKiteRejected(proto, domain, reason, crit=True) config.SetBackendStatus(domain, proto, add=BE_STATUS_ERR_TUNNEL) for request in parse.Header('X-PageKite-Duplicate'): have_kite_info = True proto, domain, srand = request.split(':') self.Log([('FE', self.server_info[self.S_NAME]), ('err', 'Duplicate'), ('proto', proto), ('domain', domain)]) config.ui.NotifyKiteRejected(proto, domain, 'duplicate') config.SetBackendStatus(domain, proto, add=BE_STATUS_ERR_TUNNEL) ssl_available = {} for request in parse.Header('X-PageKite-SSL-OK'): ssl_available[request] = True for request in parse.Header('X-PageKite-OK'): have_kite_info = True have_kites += 1 proto, domain, srand = request.split(':') self.conns.Tunnel(proto, domain, self) status = BE_STATUS_OK if request in ssl_available: status |= BE_STATUS_REMOTE_SSL self.remote_ssl[(proto, domain)] = True self.Log([('FE', sname), ('proto', proto), ('domain', domain), ('ssl', (request in ssl_available))]) config.SetBackendStatus(domain, proto, add=status) return have_kite_info and have_kites def _BackEnd(server, backends, require_all, conns): """This is the back-end end of a tunnel.""" self = Tunnel(conns) self.backends = backends self.require_all = require_all self.server_info[self.S_NAME] = server abort = True try: try: data, parse = self._Connect(server, conns) except: logging.LogError('Error in connect: %s' % format_exc()) raise if data and parse: # Collect info about front-end capabilities, for interactive config self.ParsePageKiteCapabilities(parse) for sessionid in parse.Header('X-PageKite-SessionID'): conns.SetAltId(self, sessionid) conns.config.servers_sessionids[server] = sessionid tryagain, tokens = self.CheckForTokens(parse) if tryagain: if self.server_info[self.S_ADD_KITES]: request = PageKiteRequestHeaders(server, conns.config.backends, tokens) abort = not self.SendChunked(('NOOP: 1\r\n%s\r\n\r\n!' ) % ''.join(request), compress=False, just_buffer=True) data = parse = None else: try: data, parse = self._Connect(server, conns, tokens) except: logging.LogError('Error in connect: %s' % format_exc()) raise if data and parse: kites = self.HandlePageKiteResponse(parse) abort = (kites is None) or (kites < 1) except socket.error: self.Cleanup() return None except Exception, e: self.LogError('Server response parsing failed: %s' % e) self.Cleanup() return None if abort: return None conns.Add(self) self.CountAs('frontends_live') self.last_activity = time.time() return self FrontEnd = staticmethod(_FrontEnd) BackEnd = staticmethod(_BackEnd) def Send(self, data, try_flush=False, activity=False, just_buffer=False, allow_blocking=True): try: if TUNNEL_SOCKET_BLOCKS and allow_blocking and not just_buffer: if self.fd is not None: self.fd.setblocking(1) return ChunkParser.Send(self, data, try_flush=try_flush, activity=activity, just_buffer=just_buffer, allow_blocking=allow_blocking) finally: if TUNNEL_SOCKET_BLOCKS and allow_blocking and not just_buffer: if self.fd is not None: self.fd.setblocking(0) def SendData(self, conn, data, sid=None, host=None, proto=None, port=None, chunk_headers=None): sid = int(sid or conn.sid) if conn: self.users[sid] = conn if not sid in self.zhistory: self.zhistory[sid] = [0, 0] # Pass outgoing data through any defined filters for f in self.filters: try: data = f.filter_data_out(self, sid, data) except: logging.LogError(('Ignoring error in filter_out %s: %s' ) % (f, format_exc())) sending = ['SID: %s\r\n' % sid] if proto: sending.append('Proto: %s\r\n' % proto) if host: sending.append('Host: %s\r\n' % host) if port: porti = int(port) if self.conns and (porti in self.conns.config.server_portalias): sending.append('Port: %s\r\n' % self.conns.config.server_portalias[porti]) else: sending.append('Port: %s\r\n' % port) if chunk_headers: for ch in chunk_headers: sending.append('%s: %s\r\n' % ch) sending.append('\r\n') sending.append(data) return self.SendChunked(sending, zhistory=self.zhistory[sid]) def SendStreamEof(self, sid, write_eof=False, read_eof=False): return self.SendChunked('SID: %s\r\nEOF: 1%s%s\r\n\r\nBye!' % (sid, (write_eof or not read_eof) and 'W' or '', (read_eof or not write_eof) and 'R' or ''), compress=False) def EofStream(self, sid, eof_type='WR'): if sid in self.users and self.users[sid] is not None: write_eof = (-1 != eof_type.find('W')) read_eof = (-1 != eof_type.find('R')) self.users[sid].ProcessTunnelEof(read_eof=(read_eof or not write_eof), write_eof=(write_eof or not read_eof)) def CloseStream(self, sid, stream_closed=False): if sid in self.users: stream = self.users[sid] del self.users[sid] if not stream_closed and stream is not None: stream.CloseTunnel(tunnel_closed=True) if sid in self.zhistory: del self.zhistory[sid] def ResetRemoteZChunks(self): return self.SendChunked('NOOP: 1\r\nZRST: 1\r\n\r\n!', compress=False, just_buffer=True) def TriggerPing(self): when = time.time() - PING_GRACE_MIN - PING_INTERVAL_MAX self.last_ping = self.last_activity = when def SendPing(self): now = time.time() self.last_ping = int(now) self.LogDebug("Ping", [('host', self.server_info[self.S_NAME])]) return self.SendChunked('NOOP: 1\r\nPING: %.3f\r\n\r\n!' % now, compress=False, just_buffer=True) def ProcessPong(self, pong): try: rtt = int(1000*(time.time()-float(pong))) if self.weighted_rtt < 0: self.weighted_rtt = rtt else: self.weighted_rtt = (self.weighted_rtt + rtt)/2 self.Log([('host', self.server_info[self.S_NAME]), ('rtt', '%d' % rtt), ('wrtt', '%d' % self.weighted_rtt)]) if common.gYamon: common.gYamon.ladd('tunnel_rtt', rtt) common.gYamon.ladd('tunnel_wrtt', self.weighted_rtt) except ValueError: pass def SendPong(self, data): if (self.conns.config.isfrontend and self.quota and (self.quota[0] >= 0)): # May as well make ourselves useful! return self.SendQuota(pong=data[:64]) else: return self.SendChunked('NOOP: 1\r\nPONG: %s\r\n\r\n!' % data[:64], compress=False, just_buffer=True) def SendQuota(self, pong=''): if pong: pong = 'PONG: %s\r\n' % pong if self.q_days is not None: return self.SendChunked(('NOOP: 1\r\n%sQuota: %s\r\nQDays: %s\r\nQConns: %s\r\n\r\n!' ) % (pong, self.quota[0], self.q_days, self.q_conns), compress=False, just_buffer=True) else: return self.SendChunked(('NOOP: 1\r\n%sQuota: %s\r\n\r\n!' ) % (pong, self.quota[0]), compress=False, just_buffer=True) def SendProgress(self, sid, conn, throttle=False): # FIXME: Optimize this away unless meaningful progress has been made? msg = ('NOOP: 1\r\n' 'SID: %s\r\n' 'SKB: %d\r\n') % (sid, (conn.all_out + conn.wrote_bytes)/1024) throttle = throttle and ('SPD: %d\r\n' % conn.write_speed) or '' return self.SendChunked('%s%s\r\n!' % (msg, throttle), compress=False, just_buffer=True) def ProcessCorruptChunk(self, data): self.ResetRemoteZChunks() return True def Probe(self, host): for bid in self.conns.config.backends: be = self.conns.config.backends[bid] if be[BE_DOMAIN] == host: bhost, bport = (be[BE_BHOST], be[BE_BPORT]) # FIXME: Should vary probe by backend type if self.conns.config.Ping(bhost, int(bport)) > 2: return False return True def AutoThrottle(self, max_speed=None, remote=False, delay=0.2): # Never throttle tunnels. return True def ProgressTo(self, parse): try: sid = int(parse.Header('SID')[0]) bps = int((parse.Header('SPD') or [-1])[0]) skb = int((parse.Header('SKB') or [-1])[0]) if sid in self.users: self.users[sid].RecordProgress(skb, bps) except: logging.LogError(('Tunnel::ProgressTo: That made no sense! %s' ) % format_exc()) return True # If a tunnel goes down, we just go down hard and kill all our connections. def ProcessEofRead(self): self.Die() return False def ProcessEofWrite(self): return self.ProcessEofRead() def ProcessChunkQuotaInfo(self, parse): new_quota = 0 if parse.Header('QDays'): self.q_days = new_quota = int(parse.Header('QDays')) if parse.Header('QConns'): self.q_conns = new_quota = int(parse.Header('QConns')) if parse.Header('Quota'): new_quota = 1 if self.quota: self.quota[0] = int(parse.Header('Quota')[0]) else: self.quota = [int(parse.Header('Quota')[0]), None, None] if new_quota: self.conns.config.ui.NotifyQuota(self.quota[0], self.q_days, self.q_conns) def ProcessChunkDirectives(self, parse): if parse.Header('PONG'): self.ProcessPong(parse.Header('PONG')[0]) if parse.Header('PING'): return self.SendPong(parse.Header('PING')[0]) if parse.Header('ZRST') and not self.ResetZChunks(): return False if parse.Header('SPD') or parse.Header('SKB'): if not self.ProgressTo(parse): return False if parse.Header('NOOP'): return True return None def FilterIncoming(self, sid, data=None, info=None): """Pass incoming data through filters, if we have any.""" for f in self.filters: try: if sid and info: f.filter_set_sid(sid, info) if data is not None: data = f.filter_data_in(self, sid, data) except: logging.LogError(('Ignoring error in filter_in %s: %s' ) % (f, format_exc())) return data def GetChunkDestination(self, parse): return ((parse.Header('Proto') or [''])[0].lower(), (parse.Header('Port') or [''])[0].lower(), (parse.Header('Host') or [''])[0].lower(), (parse.Header('RIP') or [''])[0].lower(), (parse.Header('RPort') or [''])[0].lower(), (parse.Header('RTLS') or [''])[0].lower()) def ReplyToProbe(self, proto, sid, host): if self.conns.config.no_probes: what, reply = 'rejected', HTTP_NoFeConnection(proto) elif self.Probe(host): what, reply = 'good', HTTP_GoodBeConnection(proto) else: what, reply = 'back-end down', HTTP_NoBeConnection(proto) self.LogDebug('Responding to probe for %s: %s' % (host, what)) return self.SendChunked('SID: %s\r\n\r\n%s' % (sid, reply)) def ConnectBE(self, sid, proto, port, host, rIp, rPort, rTLS, data): conn = UserConn.BackEnd(proto, host, sid, self, port, remote_ip=rIp, remote_port=rPort, data=data) if self.filters: if conn: rewritehost = conn.config.get('rewritehost') if rewritehost is True: rewritehost = conn.backend[BE_BHOST] else: rewritehost = False data = self.FilterIncoming(sid, data, info={ 'proto': proto, 'port': port, 'host': host, 'remote_ip': rIp, 'remote_port': rPort, 'using_tls': rTLS, 'be_host': conn and conn.backend[BE_BHOST], 'be_port': conn and conn.backend[BE_BPORT], 'trusted': conn and (conn.security or conn.config.get('insecure', False)), 'rawheaders': conn and conn.config.get('rawheaders', False), 'rewritehost': rewritehost }) if proto in ('http', 'http2', 'http3', 'websocket'): if conn and data.startswith(HttpSecurityFilter.REJECT): # Pretend we need authentication for dangerous URLs conn.Die() conn, data, code = False, '', 500 else: code = (conn is None) and 503 or 401 if not conn: # conn is None means we have no back-end. # conn is False means authentication is required. if not self.SendChunked('SID: %s\r\n\r\n%s' % (sid, HTTP_Unavailable('be', proto, host, frame_url=self.conns.config.error_url, code=code )), just_buffer=True): return False, False else: conn = None elif conn and proto == 'httpfinger': # Rewrite a finger request to HTTP. try: firstline, rest = data.split('\n', 1) if conn.config.get('rewritehost', False): rewritehost = conn.backend[BE_BHOST] else: rewritehost = host if '%s' in self.conns.config.finger_path: args = (firstline.strip(), rIp, rewritehost, rest) else: args = (rIp, rewritehost, rest) data = ('GET '+self.conns.config.finger_path+' HTTP/1.1\r\n' 'X-Forwarded-For: %s\r\n' 'Connection: close\r\n' 'Host: %s\r\n\r\n%s') % args except Exception, e: self.LogError('Error formatting HTTP-Finger: %s' % e) conn.Die() conn = None elif not conn and proto == 'https': if not self.SendChunked('SID: %s\r\n\r\n%s' % (sid, TLS_Unavailable(unavailable=True)), just_buffer=True): return False, False if conn: self.users[sid] = conn if proto == 'httpfinger': conn.fd.setblocking(1) conn.Send(data, try_flush=True, allow_blocking=False) or conn.Flush(wait=True, allow_blocking=False) self._RecvHttpHeaders(fd=conn.fd) conn.fd.setblocking(0) data = '' return conn, data def ProcessKiteUpdates(self, parse): # Look for requests for new tunnels if self.conns.config.isfrontend: requests = self.GetKiteRequests(parse) if requests: self.conns.auth().check(requests[:], self, lambda r, l: self.ChunkAuthCallback(r, l)) # Look for responses to requests for new tunnels tryagain, tokens = self.CheckForTokens(parse) if tryagain: server = self.server_info[self.S_NAME] backends = { } for bid in tokens: backends[bid] = self.conns.config.backends[bid] request = '\r\n'.join(PageKiteRequestHeaders(server, backends, tokens)) self.SendChunked('NOOP: 1\r\n%s\r\n\r\n!' % request, compress=False, just_buffer=True) kites = self.HandlePageKiteResponse(parse) if (kites is not None) and (kites < 1): self.Die() def ProcessChunk(self, data): # First, we process the chunk headers. try: headers, data = data.split('\r\n\r\n', 1) parse = HttpLineParser(lines=headers.splitlines(), state=HttpLineParser.IN_HEADERS) # Process PING/NOOP/etc: may result in a short-circuit. rv = self.ProcessChunkDirectives(parse) if rv is not None: # Update quota and kite information if necessary: this data is # always sent along with a NOOP, so checking for it here is safe. self.ProcessChunkQuotaInfo(parse) self.ProcessKiteUpdates(parse) return rv sid = int(parse.Header('SID')[0]) eof = parse.Header('EOF') except: logging.LogError(('Tunnel::ProcessChunk: Corrupt chunk: %s' ) % format_exc()) return False # EOF stream? if eof: self.EofStream(sid, eof[0]) return True # Headers done, not EOF: let's get the other end of this connection. if sid in self.users: # Either from pre-existing connections... conn = self.users[sid] if self.filters: data = self.FilterIncoming(sid, data) else: # ... or we connect to a back-end. proto, port, host, rIp, rPort, rTLS = self.GetChunkDestination(parse) if proto and host: # Probe requests are handled differently (short circuit) if proto.startswith('probe'): return self.ReplyToProbe(proto, sid, host) conn, data = self.ConnectBE(sid, proto, port, host, rIp, rPort, rTLS, data) if conn is False: return False else: conn = None # Send the data or shut down. if conn: if data and not conn.Send(data, try_flush=True): # If that failed something is wrong, but we'll let the outer # select/epoll loop catch and handle it. pass if len(conn.write_blocked) > 0 and conn.created < time.time()-3: return self.SendProgress(sid, conn, throttle=True) else: # No connection? Close this stream. self.CloseStream(sid) return self.SendStreamEof(sid) and self.Flush() return True class LoopbackTunnel(Tunnel): """A Tunnel which just loops back to this process.""" def __init__(self, conns, which, backends): Tunnel.__init__(self, conns) if self.fd: self.fd = None self.weighted_rtt = -1000 self.lock = None self.backends = backends self.require_all = True self.server_info[self.S_NAME] = LOOPBACK[which] self.other_end = None self.which = which self.buffer_count = 0 self.CountAs('loopbacks_live') if which == 'FE': for d in backends.keys(): if backends[d][BE_BHOST]: proto, domain = d.split(':') self.conns.Tunnel(proto, domain, self) self.Log([('FE', self.server_info[self.S_NAME]), ('proto', proto), ('domain', domain)]) def __str__(self): return '%s %s' % (Tunnel.__str__(self), self.which) def Cleanup(self, close=True): Tunnel.Cleanup(self, close=close) other = self.other_end self.other_end = None if other and other.other_end: other.Cleanup(close=close) def Linkup(self, other): """Links two LoopbackTunnels together.""" self.other_end = other other.other_end = self return other def _Loop(conns, backends): """Creates a loop, returning the back-end tunnel object.""" return LoopbackTunnel(conns, 'FE', backends ).Linkup(LoopbackTunnel(conns, 'BE', backends)) Loop = staticmethod(_Loop) # FIXME: This is a zero-length tunnel, but the code relies in some places # on the tunnel having a length. We really need a pipe here, or # things will go horribly wrong now and then. For now we hack this by # separating Write and Flush and looping back only on Flush. def Send(self, data, try_flush=False, activity=False, just_buffer=True, allow_blocking=True): if self.write_blocked: data = [self.write_blocked] + data self.write_blocked = '' joined_data = ''.join(data) if try_flush or (len(joined_data) > 10240) or (self.buffer_count >= 100): if logging.DEBUG_IO: print '|%s| %s \n|%s| %s' % (self.which, self, self.which, data) self.buffer_count = 0 return self.other_end.ProcessData(joined_data) else: self.buffer_count += 1 self.write_blocked = joined_data return True class UserConn(Selectable): """A Selectable representing a user's connection.""" def __init__(self, address, ui=None): Selectable.__init__(self, address=address, ui=ui) self.Reset() def Reset(self): self.tunnel = None self.conns = None self.backend = BE_NONE[:] self.config = {} self.security = None def Cleanup(self, close=True): if close: self.CloseTunnel() Selectable.Cleanup(self, close=close) self.Reset() def ConnType(self): if self.backend[BE_BHOST]: return 'BE=%s:%s' % (self.backend[BE_BHOST], self.backend[BE_BPORT]) else: return 'FE' def __str__(self): return '%s %s' % (Selectable.__str__(self), self.ConnType()) def __html__(self): return ('Tunnel: %s
    ' '%s') % (self.tunnel and self.tunnel.sid or '', escape_html('%s' % (self.tunnel or '')), Selectable.__html__(self)) def IsReadable(self, now): if self.tunnel and self.tunnel.IsBlocked(): return False return Selectable.IsReadable(self, now) def CloseTunnel(self, tunnel_closed=False): tunnel, self.tunnel = self.tunnel, None if tunnel and not tunnel_closed: tunnel.SendStreamEof(self.sid, write_eof=True, read_eof=True) tunnel.CloseStream(self.sid, stream_closed=True) self.ProcessTunnelEof(read_eof=True, write_eof=True) def _FrontEnd(conn, address, proto, host, on_port, body, conns): # This is when an external user connects to a server and requests a # web-page. We have to give it to them! self = UserConn(address, ui=conns.config.ui) self.conns = conns self.SetConn(conn) if ':' in host: host, port = host.split(':', 1) self.proto = proto self.host = host # If the listening port is an alias for another... if int(on_port) in conns.config.server_portalias: on_port = conns.config.server_portalias[int(on_port)] # Try and find the right tunnel. We prefer proto/port specifications first, # then the just the proto. If the protocol is WebSocket and no tunnel is # found, look for a plain HTTP tunnel. if proto.startswith('probe'): protos = ['http', 'https', 'websocket', 'raw', 'irc', 'finger', 'httpfinger'] ports = conns.config.server_ports[:] ports.extend(conns.config.server_aliasport.keys()) ports.extend([x for x in conns.config.server_raw_ports if x != VIRTUAL_PN]) else: protos = [proto] ports = [on_port] if proto == 'websocket': protos.append('http') elif proto == 'http': protos.extend(['http2', 'http3']) tunnels = None for p in protos: for prt in ports: if not tunnels: tunnels = conns.Tunnel('%s-%s' % (p, prt), host) if not tunnels: tunnels = conns.Tunnel(p, host) if not tunnels: tunnels = conns.Tunnel(protos[0], CATCHALL_HN) if self.address: chunk_headers = [('RIP', self.address[0]), ('RPort', self.address[1])] if conn.my_tls: chunk_headers.append(('RTLS', 1)) if tunnels: if len(tunnels) > 1: tunnels.sort(key=lambda t: t.weighted_rtt) self.tunnel = tunnels[0] if (self.tunnel and self.tunnel.SendData(self, ''.join(body), host=host, proto=proto, port=on_port, chunk_headers=chunk_headers) and self.conns): self.Log([('domain', self.host), ('on_port', on_port), ('proto', self.proto), ('is', 'FE')]) self.conns.Add(self) if proto.startswith('http'): self.conns.TrackIP(address[0], host) # FIXME: Use the tracked data to detect & mitigate abuse? return self else: self.LogDebug('No back-end', [('on_port', on_port), ('proto', self.proto), ('domain', self.host), ('is', 'FE')]) self.Cleanup(close=False) return None def _BackEnd(proto, host, sid, tunnel, on_port, remote_ip=None, remote_port=None, data=None): # This is when we open a backend connection, because a user asked for it. self = UserConn(None, ui=tunnel.conns.config.ui) self.sid = sid self.proto = proto self.host = host self.conns = tunnel.conns self.tunnel = tunnel failure = None # Try and find the right back-end. We prefer proto/port specifications # first, then the just the proto. If the protocol is WebSocket and no # tunnel is found, look for a plain HTTP tunnel. Fallback hosts can # be registered using the http2/3/4 protocols. backend = None if proto == 'http': protos = [proto, 'http2', 'http3'] elif proto.startswith('probe'): protos = ['http', 'http2', 'http3'] elif proto == 'websocket': protos = [proto, 'http', 'http2', 'http3'] else: protos = [proto] for p in protos: if not backend: p_p = '%s-%s' % (p, on_port) backend, be = self.conns.config.GetBackendServer(p_p, host) if not backend: backend, be = self.conns.config.GetBackendServer(p, host) if not backend: backend, be = self.conns.config.GetBackendServer(p, CATCHALL_HN) if backend: break logInfo = [ ('on_port', on_port), ('proto', proto), ('domain', host), ('is', 'BE') ] if remote_ip: logInfo.append(('remote_ip', remote_ip)) # Strip off useless IPv6 prefix, if this is an IPv4 address. if remote_ip.startswith('::ffff:') and ':' not in remote_ip[7:]: remote_ip = remote_ip[7:] if not backend or not backend[0]: self.ui.Notify(('%s - %s://%s:%s (FAIL: no server)' ) % (remote_ip or 'unknown', proto, host, on_port), prefix='?', color=self.ui.YELLOW) else: http_host = '%s/%s' % (be[BE_DOMAIN], be[BE_PORT] or '80') self.backend = be self.config = host_config = self.conns.config.be_config.get(http_host, {}) # Access control interception: check remote IP addresses first. ip_keys = [k for k in host_config if k.startswith('ip/')] if ip_keys: k1 = 'ip/%s' % remote_ip k2 = '.'.join(k1.split('.')[:-1]) if not (k1 in host_config or k2 in host_config): self.ui.Notify(('%s - %s://%s:%s (IP ACCESS DENIED)' ) % (remote_ip or 'unknown', proto, host, on_port), prefix='!', color=self.ui.YELLOW) logInfo.append(('forbidden-ip', '%s' % remote_ip)) backend = None else: self.security = 'ip' # Access control interception: check for HTTP Basic authentication. user_keys = [k for k in host_config if k.startswith('password/')] if user_keys: user, pwd, fail = None, None, True if proto in ('websocket', 'http', 'http2', 'http3'): parse = HttpLineParser(lines=data.splitlines()) auth = parse.Header('Authorization') try: (how, ab64) = auth[0].strip().split() if how.lower() == 'basic': user, pwd = base64.decodestring(ab64).split(':') except: user = auth user_key = 'password/%s' % user if user and user_key in host_config: if host_config[user_key] == pwd: fail = False if fail: if logging.DEBUG_IO: print '=== REQUEST\n%s\n===' % data self.ui.Notify(('%s - %s://%s:%s (USER ACCESS DENIED)' ) % (remote_ip or 'unknown', proto, host, on_port), prefix='!', color=self.ui.YELLOW) logInfo.append(('forbidden-user', '%s' % user)) backend = None failure = '' else: self.security = 'password' if not backend: logInfo.append(('err', 'No back-end')) self.Log(logInfo) self.Cleanup(close=False) return failure try: self.SetFD(rawsocket(socket.AF_INET, socket.SOCK_STREAM)) try: self.fd.settimeout(2.0) # Missing in Python 2.2 except: self.fd.setblocking(1) sspec = list(backend) if len(sspec) == 1: sspec.append(80) self.fd.connect(tuple(sspec)) self.fd.setblocking(0) except socket.error, err: logInfo.append(('socket_error', '%s' % err)) self.ui.Notify(('%s - %s://%s:%s (FAIL: %s:%s is down)' ) % (remote_ip or 'unknown', proto, host, on_port, sspec[0], sspec[1]), prefix='!', color=self.ui.YELLOW) self.Log(logInfo) self.Cleanup(close=False) return None sspec = (sspec[0], sspec[1]) be_name = (sspec == self.conns.config.ui_sspec) and 'builtin' or ('%s:%s' % sspec) self.ui.Status('serving') self.ui.Notify(('%s < %s://%s:%s (%s)' ) % (remote_ip or 'unknown', proto, host, on_port, be_name)) self.Log(logInfo) self.conns.Add(self) return self FrontEnd = staticmethod(_FrontEnd) BackEnd = staticmethod(_BackEnd) def Shutdown(self, direction): try: if self.fd: if 'sock_shutdown' in dir(self.fd): # This is a pyOpenSSL socket, which has incompatible shutdown. if direction == socket.SHUT_RD: self.fd.shutdown() else: self.fd.sock_shutdown(direction) else: self.fd.shutdown(direction) except Exception, e: pass def ProcessTunnelEof(self, read_eof=False, write_eof=False): rv = True if write_eof and not self.read_eof: rv = self.ProcessEofRead(tell_tunnel=False) and rv if read_eof and not self.write_eof: rv = self.ProcessEofWrite(tell_tunnel=False) and rv return rv def ProcessEofRead(self, tell_tunnel=True): self.read_eof = True self.Shutdown(socket.SHUT_RD) if tell_tunnel and self.tunnel: self.tunnel.SendStreamEof(self.sid, read_eof=True) return self.ProcessEof() def ProcessEofWrite(self, tell_tunnel=True): self.write_eof = True if not self.write_blocked: self.Shutdown(socket.SHUT_WR) if tell_tunnel and self.tunnel: self.tunnel.SendStreamEof(self.sid, write_eof=True) if (self.conns and self.ConnType() == 'FE' and (not self.read_eof)): self.conns.SetIdle(self, 120) return self.ProcessEof() def Send(self, data, try_flush=False, activity=True, just_buffer=False, allow_blocking=True): rv = Selectable.Send(self, data, try_flush=try_flush, activity=activity, just_buffer=just_buffer, allow_blocking=allow_blocking) if self.write_eof and not self.write_blocked: self.Shutdown(socket.SHUT_WR) elif try_flush or not self.write_blocked: if self.tunnel: self.tunnel.SendProgress(self.sid, self) return rv def ProcessData(self, data): if not self.tunnel: self.LogError('No tunnel! %s' % self) return False if not self.tunnel.SendData(self, data): self.LogDebug('Send to tunnel failed') return False # Back off if tunnel is stuffed. if self.tunnel and len(self.tunnel.write_blocked) > 1024000: # FIXME: think about this... self.Throttle(delay=(len(self.tunnel.write_blocked)-204800)/max(50000, self.tunnel.write_speed)) if self.read_eof: return self.ProcessEofRead() return True class UnknownConn(MagicProtocolParser): """This class is a connection which we're not sure what is yet.""" def __init__(self, fd, address, on_port, conns): MagicProtocolParser.__init__(self, fd, address, on_port, ui=conns.config.ui) self.peeking = True # Set up our parser chain. self.parsers = [HttpLineParser] if IrcLineParser.PROTO in conns.config.server_protos: self.parsers.append(IrcLineParser) if FingerLineParser.PROTO in conns.config.server_protos: self.parsers.append(FingerLineParser) self.parser = MagicLineParser(parsers=self.parsers) self.conns = conns self.conns.Add(self) self.conns.SetIdle(self, 10) self.sid = -1 self.host = None self.proto = None self.said_hello = False self.bad_loops = 0 def Cleanup(self, close=True): MagicProtocolParser.Cleanup(self, close=close) self.conns = self.parser = None def SayHello(self): if self.said_hello: return False else: self.said_hello = True if self.on_port in (25, 125, 2525): # FIXME: We don't actually support SMTP yet and 125 is bogus. self.Send(['220 ready ESMTP PageKite Magic Proxy\n'], try_flush=True) return True def __str__(self): return '%s (%s/%s:%s)' % (MagicProtocolParser.__str__(self), (self.proto or '?'), (self.on_port or '?'), (self.host or '?')) # Any sort of EOF just means give up: if we haven't figured out what # kind of connnection this is yet, we won't without more data. def ProcessEofRead(self): self.Die(discard_buffer=True) return self.ProcessEof() def ProcessEofWrite(self): self.Die(discard_buffer=True) return self.ProcessEof() def ProcessLine(self, line, lines): if not self.parser: return True if self.parser.Parse(line) is False: return False if not self.parser.ParsedOK(): return True self.parser = self.parser.last_parser if self.parser.protocol == HttpLineParser.PROTO: # HTTP has special cases, including CONNECT etc. return self.ProcessParsedHttp(line, lines) else: return self.ProcessParsedMagic(self.parser.PROTOS, line, lines) def ProcessParsedMagic(self, protos, line, lines): if (self.conns and self.conns.config.CheckTunnelAcls(self.address, conn=self)): for proto in protos: if UserConn.FrontEnd(self, self.address, proto, self.parser.domain, self.on_port, self.parser.lines + lines, self.conns) is not None: self.Cleanup(close=False) return True self.Send([self.parser.ErrorReply(port=self.on_port)], try_flush=True) self.Cleanup() return False def ProcessParsedHttp(self, line, lines): done = False if self.parser.method == 'PING': self.Send('PONG %s\r\n\r\n' % self.parser.path) self.read_eof = self.write_eof = done = True self.fd.close() elif self.parser.method == 'CONNECT': if self.parser.path.lower().startswith('pagekite:'): if not self.conns.config.CheckTunnelAcls(self.address, conn=self): self.Send(HTTP_ConnectBad(code=403, status='Forbidden'), try_flush=True) return False if Tunnel.FrontEnd(self, lines, self.conns) is None: self.Send(HTTP_ConnectBad(), try_flush=True) return False done = True else: try: connect_parser = self.parser chost, cport = connect_parser.path.split(':', 1) cport = int(cport) chost = chost.lower() sid1 = ':%s' % chost sid2 = '-%s:%s' % (cport, chost) tunnels = self.conns.tunnels if not self.conns.config.CheckClientAcls(self.address, conn=self): self.Send(HTTP_Unavailable('fe', 'raw', chost, code=403, status='Forbidden', frame_url=self.conns.config.error_url), try_flush=True) return False # These allow explicit CONNECTs to direct http(s) or raw backends. # If no match is found, we throw an error. if cport in (80, 8080): if (('http'+sid1) in tunnels) or ( ('http'+sid2) in tunnels) or ( ('http2'+sid1) in tunnels) or ( ('http2'+sid2) in tunnels) or ( ('http3'+sid1) in tunnels) or ( ('http3'+sid2) in tunnels): (self.on_port, self.host) = (cport, chost) self.parser = HttpLineParser() self.Send(HTTP_ConnectOK(), try_flush=True) return True whost = chost if '.' in whost: whost = '*.' + '.'.join(whost.split('.')[1:]) if cport == 443: if (('https'+sid1) in tunnels) or ( ('https'+sid2) in tunnels) or ( chost in self.conns.config.tls_endpoints) or ( whost in self.conns.config.tls_endpoints): (self.on_port, self.host) = (cport, chost) self.parser = HttpLineParser() self.Send(HTTP_ConnectOK(), try_flush=True) return self.ProcessTls(''.join(lines), chost) if (cport in self.conns.config.server_raw_ports or VIRTUAL_PN in self.conns.config.server_raw_ports): for raw in ('raw', 'finger'): if ((raw+sid1) in tunnels) or ((raw+sid2) in tunnels): (self.on_port, self.host) = (cport, chost) self.parser = HttpLineParser() self.Send(HTTP_ConnectOK(), try_flush=True) return self.ProcessProto(''.join(lines), raw, self.host) self.Send(HTTP_ConnectBad(), try_flush=True) return False except ValueError: pass if (not done and self.parser.method == 'POST' and self.parser.path in MAGIC_PATHS): # FIXME: DEPRECATE: Make this go away! if not self.conns.config.CheckTunnelAcls(self.address, conn=self): self.Send(HTTP_ConnectBad(code=403, status='Forbidden'), try_flush=True) return False if Tunnel.FrontEnd(self, lines, self.conns) is None: self.Send(HTTP_ConnectBad(), try_flush=True) return False done = True if not done: if not self.host: hosts = self.parser.Header('Host') if hosts: self.host = hosts[0].lower() else: self.Send(HTTP_Response(400, 'Bad request', ['

    400 Bad request

    ', '

    Invalid request, no Host: found.

    ', '\n'], trackable=True)) return False if self.parser.path.startswith(MAGIC_PREFIX): try: self.host = self.parser.path.split('/')[2] if self.parser.path.endswith('.json'): self.proto = 'probe.json' else: self.proto = 'probe' except ValueError: pass if self.proto is None: self.proto = 'http' upgrade = self.parser.Header('Upgrade') if 'websocket' in self.conns.config.server_protos: if upgrade and upgrade[0].lower() == 'websocket': self.proto = 'websocket' if not self.conns.config.CheckClientAcls(self.address, conn=self): self.Send(HTTP_Unavailable('fe', self.proto, self.host, code=403, status='Forbidden', frame_url=self.conns.config.error_url), try_flush=True) return False address = self.address if int(self.on_port) in self.conns.config.server_portalias: xfwdf = self.parser.Header('X-Forwarded-For') if xfwdf and address[0] == '127.0.0.1': address = (xfwdf[0], address[1]) done = True if UserConn.FrontEnd(self, address, self.proto, self.host, self.on_port, self.parser.lines + lines, self.conns) is None: if self.proto.startswith('probe'): self.Send(HTTP_NoFeConnection(self.proto), try_flush=True) else: self.Send(HTTP_Unavailable('fe', self.proto, self.host, frame_url=self.conns.config.error_url), try_flush=True) return False # We are done! self.Cleanup(close=False) return True def ProcessTls(self, data, domain=None): if (not self.conns or not self.conns.config.CheckClientAcls(self.address, conn=self)): self.Send(TLS_Unavailable(forbidden=True), try_flush=True) return False if domain: domains = [domain] else: try: domains = self.GetSni(data) if not domains: domains = [self.conns.config.tls_default] if domains[0]: self.LogDebug('No SNI - trying: %s' % domains[0]) else: domains = None except: # Probably insufficient data, just True and assume we'll have # better luck on the next round... but with a timeout. self.bad_loops += 1 if self.bad_loops < 25: self.LogDebug('Error in ProcessTLS, will time out in 120 seconds.') self.conns.SetIdle(self, 120) return True else: self.LogDebug('Persistent error in ProcessTLS, aborting.') self.Send(TLS_Unavailable(unavailable=True), try_flush=True) return False if domains and domains[0] is not None: if UserConn.FrontEnd(self, self.address, 'https', domains[0], self.on_port, [data], self.conns) is not None: # We are done! self.EatPeeked() self.Cleanup(close=False) return True else: # If we know how to terminate the TLS/SSL, do so! ctx = self.conns.config.GetTlsEndpointCtx(domains[0]) if ctx: self.fd = socks.SSL_Connect(ctx, self.fd, accepted=True, server_side=True) self.peeking = False self.is_tls = False self.my_tls = True self.conns.SetIdle(self, 120) return True else: self.Send(TLS_Unavailable(unavailable=True), try_flush=True) return False self.Send(TLS_Unavailable(unavailable=True), try_flush=True) return False def ProcessFlashPolicyRequest(self, data): # FIXME: Should this be configurable? self.LogDebug('Sending friendly response to Flash Policy Request') self.Send('\n' ' \n' '\n', try_flush=True) return False def ProcessProto(self, data, proto, domain): if (not self.conns or not self.conns.config.CheckClientAcls(self.address, conn=self)): return False if UserConn.FrontEnd(self, self.address, proto, domain, self.on_port, [data], self.conns) is None: return False # We are done! self.Cleanup(close=False) return True class UiConn(LineParser): STATE_PASSWORD = 0 STATE_LIVE = 1 def __init__(self, fd, address, on_port, conns): LineParser.__init__(self, fd=fd, address=address, on_port=on_port) self.state = self.STATE_PASSWORD self.conns = conns self.conns.Add(self) self.lines = [] self.qc = threading.Condition() self.challenge = sha1hex('%s%8.8x' % (globalSecret(), random.randint(0, 0x7FFFFFFD)+1)) self.expect = signToken(token=self.challenge, secret=self.conns.config.ConfigSecret(), payload=self.challenge, length=1000) self.LogDebug('Expecting: %s' % self.expect) self.Send('PageKite? %s\r\n' % self.challenge) def readline(self): self.qc.acquire() while not self.lines: self.qc.wait() line = self.lines.pop(0) self.qc.release() return line def write(self, data): self.conns.config.ui_wfile.write(data) self.Send(data) def Cleanup(self): self.conns.config.ui.wfile = self.conns.config.ui_wfile self.conns.config.ui.rfile = self.conns.config.ui_rfile self.lines = self.conns.config.ui_conn = None self.conns = None LineParser.Cleanup(self) def Disconnect(self): self.Send('Goodbye') self.Cleanup() def ProcessLine(self, line, lines): if self.state == self.STATE_LIVE: self.qc.acquire() self.lines.append(line) self.qc.notify() self.qc.release() return True elif self.state == self.STATE_PASSWORD: if line.strip() == self.expect: if self.conns.config.ui_conn: self.conns.config.ui_conn.Disconnect() self.conns.config.ui_conn = self self.conns.config.ui.wfile = self self.conns.config.ui.rfile = self self.state = self.STATE_LIVE self.Send('OK!\r\n') return True else: self.Send('Sorry.\r\n') return False else: return False class RawConn(Selectable): """This class is a raw/timed connection.""" def __init__(self, fd, address, on_port, conns): Selectable.__init__(self, fd, address, on_port) self.my_tls = False self.is_tls = False domain = conns.LastIpDomain(address[0]) if domain and UserConn.FrontEnd(self, address, 'raw', domain, on_port, [], conns): self.Cleanup(close=False) else: self.Cleanup() class Listener(Selectable): """This class listens for incoming connections and accepts them.""" def __init__(self, host, port, conns, backlog=100, connclass=UnknownConn, quiet=False, acl=None): Selectable.__init__(self, bind=(host, port), backlog=backlog) self.Log([('listen', '%s:%s' % (host, port))]) if not quiet: conns.config.ui.Notify(' - Listening on %s:%s' % (host or '*', port)) self.acl = acl self.acl_match = None self.connclass = connclass self.port = port self.conns = conns self.conns.Add(self) self.CountAs('listeners_live') def __str__(self): return '%s port=%s' % (Selectable.__str__(self), self.port) def __html__(self): return '

    Listening on port %s for %s

    ' % (self.port, self.connclass) def check_acl(self, ipaddr, default=True): if self.acl: try: ipaddr = '%s' % ipaddr lc = 0 for line in open(self.acl, 'r'): line = line.lower().strip() lc += 1 if line.startswith('#') or not line: continue try: words = line.split() pattern, rule = words[:2] reason = ' '.join(words[2:]) if ipaddr == pattern: self.acl_match = (lc, pattern, rule, reason) return bool('allow' in rule) elif re.compile(pattern).match(ipaddr): self.acl_match = (lc, pattern, rule, reason) return bool('allow' in rule) except IndexError: self.LogDebug('Invalid line %d in ACL %s' % (lc, self.acl)) except: self.LogDebug('Failed to read/parse %s' % self.acl) self.acl_match = (0, '.*', default and 'allow' or 'reject', 'Default') return default def ReadData(self, maxread=None): try: self.last_activity = time.time() client, address = self.fd.accept() if client: if self.check_acl(address[0]): log_info = [('accept', '%s:%s' % (obfuIp(address[0]), address[1]))] uc = self.connclass(client, address, self.port, self.conns) else: log_info = [('reject', '%s:%s' % (obfuIp(address[0]), address[1]))] client.close() if self.acl: log_info.extend([('acl_line', '%s' % self.acl_match[0]), ('reason', self.acl_match[3])]) self.Log(log_info) return True except IOError, err: self.LogDebug('Listener::ReadData: error: %s (%s)' % (err, err.errno)) except socket.error, (errno, msg): self.LogInfo('Listener::ReadData: error: %s (errno=%s)' % (msg, errno)) except Exception, e: self.LogDebug('Listener::ReadData: %s' % e) return True pagekite-0.5.8a/pagekite/proto/parsers.py0000775000175000017500000001666312603542202020022 0ustar brebre00000000000000""" Protocol parsers for classifying incoming network connections. """ ############################################################################## LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2015, the Beanstalks Project ehf. and Bjarni Runar Einarsson This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see: """ ############################################################################## from pagekite.compat import * from pagekite.common import * import pagekite.logging as logging HTTP_METHODS = ['OPTIONS', 'CONNECT', 'GET', 'HEAD', 'POST', 'PUT', 'TRACE', 'PROPFIND', 'PROPPATCH', 'MKCOL', 'DELETE', 'COPY', 'MOVE', 'LOCK', 'UNLOCK', 'PING', 'PATCH'] HTTP_VERSIONS = ['HTTP/1.0', 'HTTP/1.1'] class BaseLineParser(object): """Base protocol parser class.""" PROTO = 'unknown' PROTOS = ['unknown'] PARSE_UNKNOWN = -2 PARSE_FAILED = -1 PARSE_OK = 100 def __init__(self, lines=None, state=PARSE_UNKNOWN, proto=PROTO): self.state = state self.protocol = proto self.lines = [] self.domain = None self.last_parser = self if lines is not None: for line in lines: if not self.Parse(line): break def ParsedOK(self): return (self.state == self.PARSE_OK) def Parse(self, line): self.lines.append(line) return False def ErrorReply(self, port=None): return '' class MagicLineParser(BaseLineParser): """Parse an unknown incoming connection request, line-by-line.""" PROTO = 'magic' def __init__(self, lines=None, state=BaseLineParser.PARSE_UNKNOWN, parsers=[]): self.parsers = [p() for p in parsers] BaseLineParser.__init__(self, lines, state, self.PROTO) if self.last_parser == self: self.last_parser = self.parsers[-1] def ParsedOK(self): return self.last_parser.ParsedOK() def Parse(self, line): BaseLineParser.Parse(self, line) self.last_parser = self.parsers[-1] for p in self.parsers[:]: if not p.Parse(line): self.parsers.remove(p) elif p.ParsedOK(): self.last_parser = p self.domain = p.domain self.protocol = p.protocol self.state = p.state self.parsers = [p] break if not self.parsers: logging.LogDebug('No more parsers!') return (len(self.parsers) > 0) class HttpLineParser(BaseLineParser): """Parse an HTTP request, line-by-line.""" PROTO = 'http' PROTOS = ['http'] IN_REQUEST = 11 IN_HEADERS = 12 IN_BODY = 13 IN_RESPONSE = 14 def __init__(self, lines=None, state=IN_REQUEST, testbody=False): self.method = None self.path = None self.version = None self.code = None self.message = None self.headers = [] self.body_result = testbody BaseLineParser.__init__(self, lines, state, self.PROTO) def ParseResponse(self, line): self.version, self.code, self.message = line.split() if not self.version.upper() in HTTP_VERSIONS: logging.LogDebug('Invalid version: %s' % self.version) return False self.state = self.IN_HEADERS return True def ParseRequest(self, line): self.method, self.path, self.version = line.split() if not self.method in HTTP_METHODS: logging.LogDebug('Invalid method: %s' % self.method) return False if not self.version.upper() in HTTP_VERSIONS: logging.LogDebug('Invalid version: %s' % self.version) return False self.state = self.IN_HEADERS return True def ParseHeader(self, line): if line in ('', '\r', '\n', '\r\n'): self.state = self.IN_BODY return True header, value = line.split(':', 1) if value and value.startswith(' '): value = value[1:] self.headers.append((header.lower(), value)) return True def ParseBody(self, line): # Could be overridden by subclasses, for now we just play dumb. return self.body_result def ParsedOK(self): return (self.state == self.IN_BODY) def Parse(self, line): BaseLineParser.Parse(self, line) try: if (self.state == self.IN_RESPONSE): return self.ParseResponse(line) elif (self.state == self.IN_REQUEST): return self.ParseRequest(line) elif (self.state == self.IN_HEADERS): return self.ParseHeader(line) elif (self.state == self.IN_BODY): return self.ParseBody(line) except ValueError, err: logging.LogDebug('Parse failed: %s, %s, %s' % (self.state, err, self.lines)) self.state = BaseLineParser.PARSE_FAILED return False def Header(self, header): return [h[1].strip() for h in self.headers if h[0] == header.lower()] class FingerLineParser(BaseLineParser): """Parse an incoming Finger request, line-by-line.""" PROTO = 'finger' PROTOS = ['finger', 'httpfinger'] WANT_FINGER = 71 def __init__(self, lines=None, state=WANT_FINGER): BaseLineParser.__init__(self, lines, state, self.PROTO) def ErrorReply(self, port=None): if port == 79: return ('PageKite wants to know, what domain?\n' 'Try: finger user+domain@domain\n') else: return '' def Parse(self, line): BaseLineParser.Parse(self, line) if ' ' in line: return False if '+' in line: arg0, self.domain = line.strip().split('+', 1) elif '@' in line: arg0, self.domain = line.strip().split('@', 1) if self.domain: self.state = BaseLineParser.PARSE_OK self.lines[-1] = '%s\n' % arg0 return True else: self.state = BaseLineParser.PARSE_FAILED return False class IrcLineParser(BaseLineParser): """Parse an incoming IRC connection, line-by-line.""" PROTO = 'irc' PROTOS = ['irc'] WANT_USER = 61 def __init__(self, lines=None, state=WANT_USER): self.seen = [] BaseLineParser.__init__(self, lines, state, self.PROTO) def ErrorReply(self): return ':pagekite 451 :IRC Gateway requires user@HOST or nick@HOST\n' def Parse(self, line): BaseLineParser.Parse(self, line) if line in ('\n', '\r\n'): return True if self.state == IrcLineParser.WANT_USER: try: ocmd, arg = line.strip().split(' ', 1) cmd = ocmd.lower() self.seen.append(cmd) args = arg.split(' ') if cmd == 'pass': pass elif cmd in ('user', 'nick'): if '@' in args[0]: parts = args[0].split('@') self.domain = parts[-1] arg0 = '@'.join(parts[:-1]) elif 'nick' in self.seen and 'user' in self.seen and not self.domain: raise Error('No domain found') if self.domain: self.state = BaseLineParser.PARSE_OK self.lines[-1] = '%s %s %s\n' % (ocmd, arg0, ' '.join(args[1:])) else: self.state = BaseLineParser.PARSE_FAILED except Exception, err: logging.LogDebug('Parse failed: %s, %s, %s' % (self.state, err, self.lines)) self.state = BaseLineParser.PARSE_FAILED return (self.state != BaseLineParser.PARSE_FAILED) pagekite-0.5.8a/pagekite/proto/proto.py0000775000175000017500000002527212603542202017502 0ustar brebre00000000000000""" PageKite protocol and HTTP protocol related code and constants. """ ############################################################################## LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2015, the Beanstalks Project ehf. and Bjarni Runar Einarsson This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see: """ ############################################################################## import base64 import os import random import struct import time from pagekite.compat import * from pagekite.common import * import pagekite.logging as logging gSecret = None def globalSecret(): global gSecret if not gSecret: # This always works... gSecret = '%8.8x%s%8.8x' % (random.randint(0, 0x7FFFFFFE), time.time(), random.randint(0, 0x7FFFFFFE)) # Next, see if we can augment that with some real randomness. try: newSecret = sha1hex(open('/dev/urandom').read(64) + gSecret) gSecret = newSecret logging.LogDebug('Seeded signatures using /dev/urandom, hooray!') except: try: newSecret = sha1hex(os.urandom(64) + gSecret) gSecret = newSecret logging.LogDebug('Seeded signatures using os.urandom(), hooray!') except: logging.LogInfo('WARNING: Seeding signatures with time.time() and random.randint()') return gSecret TOKEN_LENGTH=36 def signToken(token=None, secret=None, payload='', timestamp=None, length=TOKEN_LENGTH): """ This will generate a random token with a signature which could only have come from this server. If a token is provided, it is re-signed so the original can be compared with what we would have generated, for verification purposes. If a timestamp is provided it will be embedded in the signature to a resolution of 10 minutes, and the signature will begin with the letter 't' Note: This is only as secure as random.randint() is random. """ if not secret: secret = globalSecret() if not token: token = sha1hex('%s%8.8x' % (globalSecret(), random.randint(0, 0x7FFFFFFD)+1)) if timestamp: tok = 't' + token[1:] ts = '%x' % int(timestamp/600) return tok[0:8] + sha1hex(secret + payload + ts + tok[0:8])[0:length-8] else: return token[0:8] + sha1hex(secret + payload + token[0:8])[0:length-8] def checkSignature(sign='', secret='', payload=''): """ Check a signature for validity. When using timestamped signatures, we only accept signatures from the current and previous windows. """ if sign[0] == 't': ts = int(time.time()) for window in (0, 1): valid = signToken(token=sign, secret=secret, payload=payload, timestamp=(ts-(window*600))) if sign == valid: return True return False else: valid = signToken(token=sign, secret=secret, payload=payload) return sign == valid def PageKiteRequestHeaders(server, backends, tokens=None, testtoken=None): req = [] tokens = tokens or {} for d in backends.keys(): if (backends[d][BE_BHOST] and backends[d][BE_SECRET] and backends[d][BE_STATUS] not in BE_INACTIVE): # A stable (for replay on challenge) but unguessable salt. my_token = sha1hex(globalSecret() + server + backends[d][BE_SECRET] )[:TOKEN_LENGTH] # This is the challenge (salt) from the front-end, if any. server_token = d in tokens and tokens[d] or '' # Our payload is the (proto, name) combined with both salts data = '%s:%s:%s' % (d, my_token, server_token) # Sign the payload with the shared secret (random salt). sign = signToken(secret=backends[d][BE_SECRET], payload=data, token=testtoken) req.append('X-PageKite: %s:%s\r\n' % (data, sign)) return req def HTTP_PageKiteRequest(server, backends, tokens=None, nozchunks=False, tls=False, testtoken=None, replace=None): req = ['CONNECT PageKite:1 HTTP/1.0\r\n', 'X-PageKite-Features: AddKites\r\n', 'X-PageKite-Version: %s\r\n' % APPVER] if not nozchunks: req.append('X-PageKite-Features: ZChunks\r\n') if replace: req.append('X-PageKite-Replace: %s\r\n' % replace) if tls: req.append('X-PageKite-Features: TLS\r\n') req.extend(PageKiteRequestHeaders(server, backends, tokens=tokens, testtoken=testtoken)) req.append('\r\n') return ''.join(req) def HTTP_ResponseHeader(code, title, mimetype='text/html'): if mimetype.startswith('text/') and ';' not in mimetype: mimetype += ('; charset=%s' % DEFAULT_CHARSET) return ('HTTP/1.1 %s %s\r\nContent-Type: %s\r\nPragma: no-cache\r\n' 'Expires: 0\r\nCache-Control: no-store\r\nConnection: close' '\r\n') % (code, title, mimetype) def HTTP_Header(name, value): return '%s: %s\r\n' % (name, value) def HTTP_StartBody(): return '\r\n' def HTTP_ConnectOK(): return 'HTTP/1.0 200 Connection Established\r\n\r\n' def HTTP_ConnectBad(code=503, status='Unavailable'): return 'HTTP/1.0 %s %s\r\n\r\n' % (code, status) def HTTP_Response(code, title, body, mimetype='text/html', headers=None, trackable=False): data = [HTTP_ResponseHeader(code, title, mimetype)] if headers: data.extend(headers) if trackable: data.extend('X-PageKite-UUID: %s\r\n' % MAGIC_UUID) data.extend([HTTP_StartBody(), ''.join(body)]) return ''.join(data) def HTTP_NoFeConnection(proto): if proto.endswith('.json'): (mt, content) = ('application/json', '{"pagekite-status": "down-fe"}') else: (mt, content) = ('image/gif', base64.decodestring( 'R0lGODlhCgAKAMQCAN4hIf/+/v///+EzM+AuLvGkpORISPW+vudgYOhiYvKpqeZY' 'WPbAwOdaWup1dfOurvW7u++Rkepycu6PjwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAACH5BAEAAAIALAAAAAAKAAoAAAUtoCAcyEA0jyhEQOs6AuPO' 'QJHQrjEAQe+3O98PcMMBDAdjTTDBSVSQEmGhEIUAADs=')) return HTTP_Response(200, 'OK', content, mimetype=mt, headers=[HTTP_Header('X-PageKite-Status', 'Down-FE'), HTTP_Header('Access-Control-Allow-Origin', '*')]) def HTTP_NoBeConnection(proto): if proto.endswith('.json'): (mt, content) = ('application/json', '{"pagekite-status": "down-be"}') else: (mt, content) = ('image/gif', base64.decodestring( 'R0lGODlhCgAKAPcAAI9hE6t2Fv/GAf/NH//RMf/hd7u6uv/mj/ntq8XExMbFxc7N' 'zc/Ozv/xwfj31+jn5+vq6v///////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAACH5BAEAABIALAAAAAAKAAoAAAhDACUIlBAgwMCDARo4MHiQ' '4IEGDAcGKAAAAESEBCoiiBhgQEYABzYK7OiRQIEDBgMIEDCgokmUKlcOKFkgZcGb' 'BSUEBAA7')) return HTTP_Response(200, 'OK', content, mimetype=mt, headers=[HTTP_Header('X-PageKite-Status', 'Down-BE'), HTTP_Header('Access-Control-Allow-Origin', '*')]) def HTTP_GoodBeConnection(proto): if proto.endswith('.json'): (mt, content) = ('application/json', '{"pagekite-status": "ok"}') else: (mt, content) = ('image/gif', base64.decodestring( 'R0lGODlhCgAKANUCAEKtP0StQf8AAG2/a97w3qbYpd/x3mu/aajZp/b79vT69Mnn' 'yK7crXTDcqraqcfmxtLr0VG0T0ivRpbRlF24Wr7jveHy4Pv9+53UnPn8+cjnx4LI' 'gNfu1v///37HfKfZpq/crmG6XgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' 'AAAAAAAAAAAAAAAAACH5BAEAAAIALAAAAAAKAAoAAAZIQIGAUDgMEASh4BEANAGA' 'xRAaaHoYAAPCCZUoOIDPAdCAQhIRgJGiAG0uE+igAMB0MhYoAFmtJEJcBgILVU8B' 'GkpEAwMOggJBADs=')) return HTTP_Response(200, 'OK', content, mimetype=mt, headers=[HTTP_Header('X-PageKite-Status', 'OK'), HTTP_Header('Access-Control-Allow-Origin', '*')]) def HTTP_Unavailable(where, proto, domain, comment='', frame_url=None, code=503, status='Unavailable', headers=None): if code == 401: headers = headers or [] headers.append(HTTP_Header('WWW-Authenticate', 'Basic realm=PageKite')) message = ''.join(['

    Sorry! (', where, ')

    ', '

    The ', proto.upper(),' ', 'PageKite for ', domain, ' is unavailable at the moment.

    ', '

    Please try again later.

    ']) if frame_url: if '?' in frame_url: frame_url += '&where=%s&proto=%s&domain=%s' % (where.upper(), proto, domain) return HTTP_Response(code, status, ['', '', '', message, '', '\n'], headers=headers) else: return HTTP_Response(code, status, ['', message, '\n'], headers=headers) def TLS_Unavailable(forbidden=False, unavailable=False): """Generate a TLS alert record aborting this connectin.""" # FIXME: Should we really be ignoring forbidden and unavailable? # Unfortunately, Chrome/ium only honors code 49, any other code will # cause it to transparently retry with SSLv3. So although this is a # bit misleading, this is what we send... return struct.pack('>BBBBBBB', 0x15, 3, 3, 0, 2, 2, 49) # 49 = Access denied pagekite-0.5.8a/pagekite/__main__.py0000775000175000017500000001410512603560755016722 0ustar brebre00000000000000#!/usr/bin/env python """ This is the pagekite.py Main() function. """ ############################################################################## LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2015, the Beanstalks Project ehf. and Bjarni Runar Einarsson This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see: """ ############################################################################## import sys from pagekite import pk from pagekite import httpd if __name__ == "__main__": if hasattr(sys.stdout, 'isatty') and sys.stdout.isatty(): import pagekite.ui.basic uiclass = pagekite.ui.basic.BasicUi else: import pagekite.ui.nullui uiclass = pagekite.ui.nullui.NullUi pk.Main(pk.PageKite, pk.Configure, uiclass=uiclass, http_handler=httpd.UiRequestHandler, http_server=httpd.UiHttpServer) ############################################################################## CERTS="""\ StartCom Ltd. ============= -----BEGIN CERTIFICATE----- MIIFFjCCBH+gAwIBAgIBADANBgkqhkiG9w0BAQQFADCBsDELMAkGA1UEBhMCSUwxDzANBgNVBAgT BklzcmFlbDEOMAwGA1UEBxMFRWlsYXQxFjAUBgNVBAoTDVN0YXJ0Q29tIEx0ZC4xGjAYBgNVBAsT EUNBIEF1dGhvcml0eSBEZXAuMSkwJwYDVQQDEyBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1dGhv cml0eTEhMB8GCSqGSIb3DQEJARYSYWRtaW5Ac3RhcnRjb20ub3JnMB4XDTA1MDMxNzE3Mzc0OFoX DTM1MDMxMDE3Mzc0OFowgbAxCzAJBgNVBAYTAklMMQ8wDQYDVQQIEwZJc3JhZWwxDjAMBgNVBAcT BUVpbGF0MRYwFAYDVQQKEw1TdGFydENvbSBMdGQuMRowGAYDVQQLExFDQSBBdXRob3JpdHkgRGVw LjEpMCcGA1UEAxMgRnJlZSBTU0wgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxITAfBgkqhkiG9w0B CQEWEmFkbWluQHN0YXJ0Y29tLm9yZzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA7YRgACOe yEpRKSfeOqE5tWmrCbIvNP1h3D3TsM+x18LEwrHkllbEvqoUDufMOlDIOmKdw6OsWXuO7lUaHEe+ o5c5s7XvIywI6Nivcy+5yYPo7QAPyHWlLzRMGOh2iCNJitu27Wjaw7ViKUylS7eYtAkUEKD4/mJ2 IhULpNYILzUCAwEAAaOCAjwwggI4MA8GA1UdEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgHmMB0GA1Ud DgQWBBQcicOWzL3+MtUNjIExtpidjShkjTCB3QYDVR0jBIHVMIHSgBQcicOWzL3+MtUNjIExtpid jShkjaGBtqSBszCBsDELMAkGA1UEBhMCSUwxDzANBgNVBAgTBklzcmFlbDEOMAwGA1UEBxMFRWls YXQxFjAUBgNVBAoTDVN0YXJ0Q29tIEx0ZC4xGjAYBgNVBAsTEUNBIEF1dGhvcml0eSBEZXAuMSkw JwYDVQQDEyBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEhMB8GCSqGSIb3DQEJARYS YWRtaW5Ac3RhcnRjb20ub3JnggEAMB0GA1UdEQQWMBSBEmFkbWluQHN0YXJ0Y29tLm9yZzAdBgNV HRIEFjAUgRJhZG1pbkBzdGFydGNvbS5vcmcwEQYJYIZIAYb4QgEBBAQDAgAHMC8GCWCGSAGG+EIB DQQiFiBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAyBglghkgBhvhCAQQEJRYjaHR0 cDovL2NlcnQuc3RhcnRjb20ub3JnL2NhLWNybC5jcmwwKAYJYIZIAYb4QgECBBsWGWh0dHA6Ly9j ZXJ0LnN0YXJ0Y29tLm9yZy8wOQYJYIZIAYb4QgEIBCwWKmh0dHA6Ly9jZXJ0LnN0YXJ0Y29tLm9y Zy9pbmRleC5waHA/YXBwPTExMTANBgkqhkiG9w0BAQQFAAOBgQBscSXhnjSRIe/bbL0BCFaPiNhB OlP1ct8nV0t2hPdopP7rPwl+KLhX6h/BquL/lp9JmeaylXOWxkjHXo0Hclb4g4+fd68p00UOpO6w NnQt8M2YI3s3S9r+UZjEHjQ8iP2ZO1CnwYszx8JSFhKVU2Ui77qLzmLbcCOxgN8aIDjnfg== -----END CERTIFICATE----- StartCom Certification Authority ================================ -----BEGIN CERTIFICATE----- MIIHyTCCBbGgAwIBAgIBATANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJJTDEWMBQGA1UEChMN U3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmlu ZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDYwOTE3MTk0 NjM2WhcNMzYwOTE3MTk0NjM2WjB9MQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRk LjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMg U3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw ggIKAoICAQDBiNsJvGxGfHiflXu1M5DycmLWwTYgIiRezul38kMKogZkpMyONvg45iPwbm2xPN1y o4UcodM9tDMr0y+v/uqwQVlntsQGfQqedIXWeUyAN3rfOQVSWff0G0ZDpNKFhdLDcfN1YjS6LIp/ Ho/u7TTQEceWzVI9ujPW3U3eCztKS5/CJi/6tRYccjV3yjxd5srhJosaNnZcAdt0FCX+7bWgiA/d eMotHweXMAEtcnn6RtYTKqi5pquDSR3l8u/d5AGOGAqPY1MWhWKpDhk6zLVmpsJrdAfkK+F2PrRt 2PZE4XNiHzvEvqBTViVsUQn3qqvKv3b9bZvzndu/PWa8DFaqr5hIlTpL36dYUNk4dalb6kMMAv+Z 6+hsTXBbKWWc3apdzK8BMewM69KN6Oqce+Zu9ydmDBpI125C4z/eIT574Q1w+2OqqGwaVLRcJXrJ osmLFqa7LH4XXgVNWG4SHQHuEhANxjJ/GP/89PrNbpHoNkm+Gkhpi8KWTRoSsmkXwQqQ1vp5Iki/ untp+HDH+no32NgN0nZPV/+Qt+OR0t3vwmC3Zzrd/qqc8NSLf3Iizsafl7b4r4qgEKjZ+xjGtrVc UjyJthkqcwEKDwOzEmDyei+B26Nu/yYwl/WL3YlXtq09s68rxbd2AvCl1iuahhQqcvbjM4xdCUsT 37uMdBNSSwIDAQABo4ICUjCCAk4wDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAa4wHQYDVR0OBBYE FE4L7xqkQFulF2mHMMo0aEPQQa7yMGQGA1UdHwRdMFswLKAqoCiGJmh0dHA6Ly9jZXJ0LnN0YXJ0 Y29tLm9yZy9zZnNjYS1jcmwuY3JsMCugKaAnhiVodHRwOi8vY3JsLnN0YXJ0Y29tLm9yZy9zZnNj YS1jcmwuY3JsMIIBXQYDVR0gBIIBVDCCAVAwggFMBgsrBgEEAYG1NwEBATCCATswLwYIKwYBBQUH AgEWI2h0dHA6Ly9jZXJ0LnN0YXJ0Y29tLm9yZy9wb2xpY3kucGRmMDUGCCsGAQUFBwIBFilodHRw Oi8vY2VydC5zdGFydGNvbS5vcmcvaW50ZXJtZWRpYXRlLnBkZjCB0AYIKwYBBQUHAgIwgcMwJxYg U3RhcnQgQ29tbWVyY2lhbCAoU3RhcnRDb20pIEx0ZC4wAwIBARqBl0xpbWl0ZWQgTGlhYmlsaXR5 LCByZWFkIHRoZSBzZWN0aW9uICpMZWdhbCBMaW1pdGF0aW9ucyogb2YgdGhlIFN0YXJ0Q29tIENl cnRpZmljYXRpb24gQXV0aG9yaXR5IFBvbGljeSBhdmFpbGFibGUgYXQgaHR0cDovL2NlcnQuc3Rh cnRjb20ub3JnL3BvbGljeS5wZGYwEQYJYIZIAYb4QgEBBAQDAgAHMDgGCWCGSAGG+EIBDQQrFilT dGFydENvbSBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTANBgkqhkiG9w0BAQUFAAOC AgEAFmyZ9GYMNPXQhV59CuzaEE44HF7fpiUFS5Eyweg78T3dRAlbB0mKKctmArexmvclmAk8jhvh 3TaHK0u7aNM5Zj2gJsfyOZEdUauCe37Vzlrk4gNXcGmXCPleWKYK34wGmkUWFjgKXlf2Ysd6AgXm vB618p70qSmD+LIU424oh0TDkBreOKk8rENNZEXO3SipXPJzewT4F+irsfMuXGRuczE6Eri8sxHk fY+BUZo7jYn0TZNmezwD7dOaHZrzZVD1oNB1ny+v8OqCQ5j4aZyJecRDjkZy42Q2Eq/3JR44iZB3 fsNrarnDy0RLrHiQi+fHLB5LEUTINFInzQpdn4XBidUaePKVEFMy3YCEZnXZtWgo+2EuvoSoOMCZ EoalHmdkrQYuL6lwhceWD3yJZfWOQ1QOq92lgDmUYMA0yZZwLKMS9R9Ie70cfmu3nZD0Ijuu+Pwq yvqCUqDvr0tVk+vBtfAii6w0TiYiBKGHLHVKt+V9E9e4DGTANtLJL4YSjCMJwRuCO3NJo2pXh5Tl 1njFmUNj403gdy3hZZlyaQQaRwnmDwFWJPsfvw55qVguucQJAX6Vum0ABj6y6koQOdjQK/W/7HW/ lwLFCRsI3FU34oH7N4RDYiDK51ZLZer+bMEkkyShNOsF/5oirpt9P/FlUQqmMGqz9IgcgA38coro g14= -----END CERTIFICATE----- """ pagekite-0.5.8a/pagekite/android.py0000775000175000017500000001415012603542201016604 0ustar brebre00000000000000""" This is the main function for the Android version of PageKite. """ ############################################################################# LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2015, the Beanstalks Project ehf. and Bjarni Runar Einarsson This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see: """ ############################################################################# import sys import pagekite.pk as pk import pagekite.httpd as httpd def Configure(pkobj): pkobj.rcfile = "/sdcard/pagekite.cfg" pkobj.enable_sslzlib = True pk.Configure(pkobj) if __name__ == "__main__": if sys.stdout.isatty(): import pagekite.ui.basic uiclass = pagekite.ui.basic.BasicUi else: uiclass = pk.NullUi pk.Main(pk.PageKite, Configure, uiclass=uiclass, http_handler=httpd.UiRequestHandler, http_server=httpd.UiHttpServer) ############################################################################## CERTS="""\ StartCom Ltd. ============= -----BEGIN CERTIFICATE----- MIIFFjCCBH+gAwIBAgIBADANBgkqhkiG9w0BAQQFADCBsDELMAkGA1UEBhMCSUwxDzANBgNVBAgT BklzcmFlbDEOMAwGA1UEBxMFRWlsYXQxFjAUBgNVBAoTDVN0YXJ0Q29tIEx0ZC4xGjAYBgNVBAsT EUNBIEF1dGhvcml0eSBEZXAuMSkwJwYDVQQDEyBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1dGhv cml0eTEhMB8GCSqGSIb3DQEJARYSYWRtaW5Ac3RhcnRjb20ub3JnMB4XDTA1MDMxNzE3Mzc0OFoX DTM1MDMxMDE3Mzc0OFowgbAxCzAJBgNVBAYTAklMMQ8wDQYDVQQIEwZJc3JhZWwxDjAMBgNVBAcT BUVpbGF0MRYwFAYDVQQKEw1TdGFydENvbSBMdGQuMRowGAYDVQQLExFDQSBBdXRob3JpdHkgRGVw LjEpMCcGA1UEAxMgRnJlZSBTU0wgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxITAfBgkqhkiG9w0B CQEWEmFkbWluQHN0YXJ0Y29tLm9yZzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA7YRgACOe yEpRKSfeOqE5tWmrCbIvNP1h3D3TsM+x18LEwrHkllbEvqoUDufMOlDIOmKdw6OsWXuO7lUaHEe+ o5c5s7XvIywI6Nivcy+5yYPo7QAPyHWlLzRMGOh2iCNJitu27Wjaw7ViKUylS7eYtAkUEKD4/mJ2 IhULpNYILzUCAwEAAaOCAjwwggI4MA8GA1UdEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgHmMB0GA1Ud DgQWBBQcicOWzL3+MtUNjIExtpidjShkjTCB3QYDVR0jBIHVMIHSgBQcicOWzL3+MtUNjIExtpid jShkjaGBtqSBszCBsDELMAkGA1UEBhMCSUwxDzANBgNVBAgTBklzcmFlbDEOMAwGA1UEBxMFRWls YXQxFjAUBgNVBAoTDVN0YXJ0Q29tIEx0ZC4xGjAYBgNVBAsTEUNBIEF1dGhvcml0eSBEZXAuMSkw JwYDVQQDEyBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEhMB8GCSqGSIb3DQEJARYS YWRtaW5Ac3RhcnRjb20ub3JnggEAMB0GA1UdEQQWMBSBEmFkbWluQHN0YXJ0Y29tLm9yZzAdBgNV HRIEFjAUgRJhZG1pbkBzdGFydGNvbS5vcmcwEQYJYIZIAYb4QgEBBAQDAgAHMC8GCWCGSAGG+EIB DQQiFiBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAyBglghkgBhvhCAQQEJRYjaHR0 cDovL2NlcnQuc3RhcnRjb20ub3JnL2NhLWNybC5jcmwwKAYJYIZIAYb4QgECBBsWGWh0dHA6Ly9j ZXJ0LnN0YXJ0Y29tLm9yZy8wOQYJYIZIAYb4QgEIBCwWKmh0dHA6Ly9jZXJ0LnN0YXJ0Y29tLm9y Zy9pbmRleC5waHA/YXBwPTExMTANBgkqhkiG9w0BAQQFAAOBgQBscSXhnjSRIe/bbL0BCFaPiNhB OlP1ct8nV0t2hPdopP7rPwl+KLhX6h/BquL/lp9JmeaylXOWxkjHXo0Hclb4g4+fd68p00UOpO6w NnQt8M2YI3s3S9r+UZjEHjQ8iP2ZO1CnwYszx8JSFhKVU2Ui77qLzmLbcCOxgN8aIDjnfg== -----END CERTIFICATE----- StartCom Certification Authority ================================ -----BEGIN CERTIFICATE----- MIIHyTCCBbGgAwIBAgIBATANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJJTDEWMBQGA1UEChMN U3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmlu ZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDYwOTE3MTk0 NjM2WhcNMzYwOTE3MTk0NjM2WjB9MQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRk LjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMg U3RhcnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw ggIKAoICAQDBiNsJvGxGfHiflXu1M5DycmLWwTYgIiRezul38kMKogZkpMyONvg45iPwbm2xPN1y o4UcodM9tDMr0y+v/uqwQVlntsQGfQqedIXWeUyAN3rfOQVSWff0G0ZDpNKFhdLDcfN1YjS6LIp/ Ho/u7TTQEceWzVI9ujPW3U3eCztKS5/CJi/6tRYccjV3yjxd5srhJosaNnZcAdt0FCX+7bWgiA/d eMotHweXMAEtcnn6RtYTKqi5pquDSR3l8u/d5AGOGAqPY1MWhWKpDhk6zLVmpsJrdAfkK+F2PrRt 2PZE4XNiHzvEvqBTViVsUQn3qqvKv3b9bZvzndu/PWa8DFaqr5hIlTpL36dYUNk4dalb6kMMAv+Z 6+hsTXBbKWWc3apdzK8BMewM69KN6Oqce+Zu9ydmDBpI125C4z/eIT574Q1w+2OqqGwaVLRcJXrJ osmLFqa7LH4XXgVNWG4SHQHuEhANxjJ/GP/89PrNbpHoNkm+Gkhpi8KWTRoSsmkXwQqQ1vp5Iki/ untp+HDH+no32NgN0nZPV/+Qt+OR0t3vwmC3Zzrd/qqc8NSLf3Iizsafl7b4r4qgEKjZ+xjGtrVc UjyJthkqcwEKDwOzEmDyei+B26Nu/yYwl/WL3YlXtq09s68rxbd2AvCl1iuahhQqcvbjM4xdCUsT 37uMdBNSSwIDAQABo4ICUjCCAk4wDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAa4wHQYDVR0OBBYE FE4L7xqkQFulF2mHMMo0aEPQQa7yMGQGA1UdHwRdMFswLKAqoCiGJmh0dHA6Ly9jZXJ0LnN0YXJ0 Y29tLm9yZy9zZnNjYS1jcmwuY3JsMCugKaAnhiVodHRwOi8vY3JsLnN0YXJ0Y29tLm9yZy9zZnNj YS1jcmwuY3JsMIIBXQYDVR0gBIIBVDCCAVAwggFMBgsrBgEEAYG1NwEBATCCATswLwYIKwYBBQUH AgEWI2h0dHA6Ly9jZXJ0LnN0YXJ0Y29tLm9yZy9wb2xpY3kucGRmMDUGCCsGAQUFBwIBFilodHRw Oi8vY2VydC5zdGFydGNvbS5vcmcvaW50ZXJtZWRpYXRlLnBkZjCB0AYIKwYBBQUHAgIwgcMwJxYg U3RhcnQgQ29tbWVyY2lhbCAoU3RhcnRDb20pIEx0ZC4wAwIBARqBl0xpbWl0ZWQgTGlhYmlsaXR5 LCByZWFkIHRoZSBzZWN0aW9uICpMZWdhbCBMaW1pdGF0aW9ucyogb2YgdGhlIFN0YXJ0Q29tIENl cnRpZmljYXRpb24gQXV0aG9yaXR5IFBvbGljeSBhdmFpbGFibGUgYXQgaHR0cDovL2NlcnQuc3Rh cnRjb20ub3JnL3BvbGljeS5wZGYwEQYJYIZIAYb4QgEBBAQDAgAHMDgGCWCGSAGG+EIBDQQrFilT dGFydENvbSBGcmVlIFNTTCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTANBgkqhkiG9w0BAQUFAAOC AgEAFmyZ9GYMNPXQhV59CuzaEE44HF7fpiUFS5Eyweg78T3dRAlbB0mKKctmArexmvclmAk8jhvh 3TaHK0u7aNM5Zj2gJsfyOZEdUauCe37Vzlrk4gNXcGmXCPleWKYK34wGmkUWFjgKXlf2Ysd6AgXm vB618p70qSmD+LIU424oh0TDkBreOKk8rENNZEXO3SipXPJzewT4F+irsfMuXGRuczE6Eri8sxHk fY+BUZo7jYn0TZNmezwD7dOaHZrzZVD1oNB1ny+v8OqCQ5j4aZyJecRDjkZy42Q2Eq/3JR44iZB3 fsNrarnDy0RLrHiQi+fHLB5LEUTINFInzQpdn4XBidUaePKVEFMy3YCEZnXZtWgo+2EuvoSoOMCZ EoalHmdkrQYuL6lwhceWD3yJZfWOQ1QOq92lgDmUYMA0yZZwLKMS9R9Ie70cfmu3nZD0Ijuu+Pwq yvqCUqDvr0tVk+vBtfAii6w0TiYiBKGHLHVKt+V9E9e4DGTANtLJL4YSjCMJwRuCO3NJo2pXh5Tl 1njFmUNj403gdy3hZZlyaQQaRwnmDwFWJPsfvw55qVguucQJAX6Vum0ABj6y6koQOdjQK/W/7HW/ lwLFCRsI3FU34oH7N4RDYiDK51ZLZer+bMEkkyShNOsF/5oirpt9P/FlUQqmMGqz9IgcgA38coro g14= -----END CERTIFICATE----- """ pagekite-0.5.8a/pagekite/httpd.py0000775000175000017500000010677412603542201016325 0ustar brebre00000000000000""" This is the pagekite.py built-in HTTP server. """ ############################################################################## LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2015, the Beanstalks Project ehf. and Bjarni Runar Einarsson This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see: """ ############################################################################## import base64 import cgi from cgi import escape as escape_html import os import re import socket import sys import tempfile import threading import time import traceback import urllib import SocketServer from CGIHTTPServer import CGIHTTPRequestHandler from SimpleXMLRPCServer import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler import Cookie from pagekite.common import * from pagekite.compat import * import pagekite.common as common import pagekite.logging as logging import pagekite.proto.selectables as selectables import sockschain as socks ##[ Conditional imports & compatibility magic! ]############################### try: import datetime ts_to_date = datetime.datetime.fromtimestamp except ImportError: ts_to_date = str try: sorted([1, 2, 3]) except: def sorted(l): tmp = l[:] tmp.sort() return tmp # Different Python 2.x versions complain about deprecation depending on # where we pull these from. try: from urlparse import parse_qs, urlparse except ImportError, e: from cgi import parse_qs from urlparse import urlparse try: import hashlib def sha1hex(data): hl = hashlib.sha1() hl.update(data) return hl.hexdigest().lower() except ImportError: import sha def sha1hex(data): return sha.new(data).hexdigest().lower() ##[ PageKite HTTPD code starts here! ]######################################### class AuthError(Exception): pass def fmt_size(count): if count > 2*(1024*1024*1024): return '%dGB' % (count / (1024*1024*1024)) if count > 2*(1024*1024): return '%dMB' % (count / (1024*1024)) if count > 2*(1024): return '%dKB' % (count / 1024) return '%dB' % count class CGIWrapper(CGIHTTPRequestHandler): def __init__(self, request, path_cgi): self.path = path_cgi self.cgi_info = (os.path.dirname(path_cgi), os.path.basename(path_cgi)) self.request = request self.server = request.server self.command = request.command self.headers = request.headers self.client_address = ('unknown', 0) self.rfile = request.rfile self.wfile = tempfile.TemporaryFile() def translate_path(self, path): return path def send_response(self, code, message): self.wfile.write('X-Response-Code: %s\r\n' % code) self.wfile.write('X-Response-Message: %s\r\n' % message) def send_error(self, code, message): return self.send_response(code, message) def Run(self): self.run_cgi() self.wfile.seek(0) return self.wfile class UiRequestHandler(SimpleXMLRPCRequestHandler): # Make all paths/endpoints legal, we interpret them below. rpc_paths = ( ) E403 = { 'code': '403', 'msg': 'Missing', 'mimetype': 'text/html', 'title': '403 Not found', 'body': '

    File or directory not found. Sorry!

    ' } E404 = { 'code': '404', 'msg': 'Not found', 'mimetype': 'text/html', 'title': '404 Not found', 'body': '

    File or directory not found. Sorry!

    ' } ROBOTSTXT = { 'code': '200', 'msg': 'OK', 'mimetype': 'text/plain', 'body': ('User-agent: *\n' 'Disallow: /\n' '# pagekite.py default robots.txt\n') } MIME_TYPES = { '3gp': 'video/3gpp', 'aac': 'audio/aac', 'atom': 'application/atom+xml', 'avi': 'video/avi', 'bmp': 'image/bmp', 'bz2': 'application/x-bzip2', 'c': 'text/plain', 'cpp': 'text/plain', 'css': 'text/css', 'conf': 'text/plain', 'cfg': 'text/plain', 'dtd': 'application/xml-dtd', 'doc': 'application/msword', 'gif': 'image/gif', 'gz': 'application/x-gzip', 'h': 'text/plain', 'hpp': 'text/plain', 'htm': 'text/html', 'html': 'text/html', 'hqx': 'application/mac-binhex40', 'java': 'text/plain', 'jar': 'application/java-archive', 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'js': 'application/javascript', 'json': 'application/json', 'jsonp': 'application/javascript', 'log': 'text/plain', 'md': 'text/plain', 'midi': 'audio/x-midi', 'mov': 'video/quicktime', 'mpeg': 'video/mpeg', 'mp2': 'audio/mpeg', 'mp3': 'audio/mpeg', 'm4v': 'video/mp4', 'mp4': 'video/mp4', 'm4a': 'audio/mp4', 'ogg': 'audio/vorbis', 'pdf': 'application/pdf', 'ps': 'application/postscript', 'pl': 'text/plain', 'png': 'image/png', 'ppt': 'application/vnd.ms-powerpoint', 'py': 'text/plain', 'pyw': 'text/plain', 'pk-shtml': 'text/html', 'pk-js': 'application/javascript', 'rc': 'text/plain', 'rtf': 'application/rtf', 'rss': 'application/rss+xml', 'sgml': 'text/sgml', 'sh': 'text/plain', 'shtml': 'text/plain', 'svg': 'image/svg+xml', 'swf': 'application/x-shockwave-flash', 'tar': 'application/x-tar', 'tgz': 'application/x-tar', 'tiff': 'image/tiff', 'txt': 'text/plain', 'wav': 'audio/wav', 'xml': 'application/xml', 'xls': 'application/vnd.ms-excel', 'xrdf': 'application/xrds+xml','zip': 'application/zip', 'DEFAULT': 'application/octet-stream' } TEMPLATE_RAW = ('%(body)s') TEMPLATE_JSONP = ('window.pkData = %s;') TEMPLATE_HTML = ('\n' '\n' '%(title)s - %(prog)s v%(ver)s\n' '\n' '

    %(title)s

    \n' '
    %(body)s
    \n' '\n' '\n') def setup(self): self.suppress_body = False if self.server.enable_ssl: self.connection = self.request self.rfile = socket._fileobject(self.request, "rb", self.rbufsize) self.wfile = socket._fileobject(self.request, "wb", self.wbufsize) else: SimpleXMLRPCRequestHandler.setup(self) def log_message(self, format, *args): logging.Log([('uireq', format % args)]) def send_header(self, header, value): self.wfile.write('%s: %s\r\n' % (header, value)) def end_headers(self): self.wfile.write('\r\n') def sendStdHdrs(self, header_list=[], cachectrl='private', mimetype='text/html'): if mimetype.startswith('text/') and ';' not in mimetype: mimetype += ('; charset=%s' % DEFAULT_CHARSET) self.send_header('Cache-Control', cachectrl) self.send_header('Content-Type', mimetype) for header in header_list: self.send_header(header[0], header[1]) self.end_headers() def sendChunk(self, chunk): if self.chunked: if logging.DEBUG_IO: print '<== SENDING CHUNK ===\n%s\n' % chunk self.wfile.write('%x\r\n' % len(chunk)) self.wfile.write(chunk) self.wfile.write('\r\n') else: if logging.DEBUG_IO: print '<== SENDING ===\n%s\n' % chunk self.wfile.write(chunk) def sendEof(self): if self.chunked and not self.suppress_body: self.wfile.write('0\r\n\r\n') def sendResponse(self, message, code=200, msg='OK', mimetype='text/html', header_list=[], chunked=False, length=None): self.log_request(code, message and len(message) or '-') self.wfile.write('HTTP/1.1 %s %s\r\n' % (code, msg)) if code == 401: self.send_header('WWW-Authenticate', 'Basic realm=PK%d' % (time.time()/3600)) self.chunked = chunked if chunked: self.send_header('Transfer-Encoding', 'chunked') else: if length: self.send_header('Content-Length', length) elif not chunked: self.send_header('Content-Length', len(message or '')) self.sendStdHdrs(header_list=header_list, mimetype=mimetype) if message and not self.suppress_body: self.sendChunk(message) def needPassword(self): if self.server.pkite.ui_password: return True userkeys = [k for k in self.host_config.keys() if k.startswith('password/')] return userkeys def checkUsernamePasswordAuth(self, username, password): userkey = 'password/%s' % username if userkey in self.host_config: if self.host_config[userkey] == password: return if (self.server.pkite.ui_password and password == self.server.pkite.ui_password): return if self.needPassword(): raise AuthError("Invalid password") def checkRequestAuth(self, scheme, netloc, path, qs): if self.needPassword(): raise AuthError("checkRequestAuth not implemented") def checkPostAuth(self, scheme, netloc, path, qs, posted): if self.needPassword(): raise AuthError("checkPostAuth not implemented") def performAuthChecks(self, scheme, netloc, path, qs): try: auth = self.headers.get('authorization') if auth: (how, ab64) = auth.strip().split() if how.lower() == 'basic': (username, password) = base64.decodestring(ab64).split(':') self.checkUsernamePasswordAuth(username, password) return True self.checkRequestAuth(scheme, netloc, path, qs) return True except (ValueError, KeyError, AuthError), e: logging.LogDebug('HTTP Auth failed: %s' % e) else: logging.LogDebug('HTTP Auth failed: Unauthorized') self.sendResponse('

    Unauthorized

    \n', code=401, msg='Forbidden') return False def performPostAuthChecks(self, scheme, netloc, path, qs, posted): try: self.checkPostAuth(scheme, netloc, path, qs, posted) return True except AuthError: self.sendResponse('

    Unauthorized

    \n', code=401, msg='Forbidden') return False def do_UNSUPPORTED(self): self.sendResponse('Unsupported request method.\n', code=503, msg='Sorry', mimetype='text/plain') # Misc methods we don't support (yet) def do_OPTIONS(self): self.do_UNSUPPORTED() def do_DELETE(self): self.do_UNSUPPORTED() def do_PUT(self): self.do_UNSUPPORTED() def getHostInfo(self): http_host = self.headers.get('HOST', self.headers.get('host', 'unknown')) if http_host == 'unknown' or (http_host.startswith('localhost:') and http_host.replace(':', '/') not in self.server.pkite.be_config): http_host = None for bid in sorted(self.server.pkite.backends.keys()): be = self.server.pkite.backends[bid] if (be[BE_BPORT] == self.server.pkite.ui_sspec[1] and be[BE_STATUS] not in BE_INACTIVE): http_host = '%s:%s' % (be[BE_DOMAIN], be[BE_PORT] or 80) if not http_host: if self.server.pkite.be_config.keys(): http_host = sorted(self.server.pkite.be_config.keys() )[0].replace('/', ':') else: http_host = 'unknown' self.http_host = http_host self.host_config = self.server.pkite.be_config.get((':' in http_host and http_host or http_host+':80' ).replace(':', '/'), {}) def do_GET(self, command='GET'): (scheme, netloc, path, params, query, frag) = urlparse(self.path) qs = parse_qs(query) self.getHostInfo() self.post_data = None self.command = command if not self.performAuthChecks(scheme, netloc, path, qs): return try: return self.handleHttpRequest(scheme, netloc, path, params, query, frag, qs, None) except socket.error: pass except Exception, e: logging.Log([('err', 'GET error at %s: %s' % (path, e))]) if logging.DEBUG_IO: print '=== ERROR\n%s\n===' % format_exc() self.sendResponse('

    Internal Error

    \n', code=500, msg='Error') def do_HEAD(self): self.suppress_body = True self.do_GET(command='HEAD') def do_POST(self, command='POST'): (scheme, netloc, path, params, query, frag) = urlparse(self.path) qs = parse_qs(query) self.getHostInfo() self.command = command if not self.performAuthChecks(scheme, netloc, path, qs): return posted = None self.post_data = tempfile.TemporaryFile() self.old_rfile = self.rfile try: # First, buffer the POST data to a file... clength = cleft = int(self.headers.get('content-length')) while cleft > 0: rbytes = min(64*1024, cleft) self.post_data.write(self.rfile.read(rbytes)) cleft -= rbytes # Juggle things so the buffering is invisble. self.post_data.seek(0) self.rfile = self.post_data ctype, pdict = cgi.parse_header(self.headers.get('content-type')) if ctype == 'multipart/form-data': self.post_data.seek(0) posted = cgi.parse_multipart(self.rfile, pdict) elif ctype == 'application/x-www-form-urlencoded': if clength >= 50*1024*1024: raise Exception(("Refusing to parse giant posted query " "string (%s bytes).") % clength) posted = cgi.parse_qs(self.rfile.read(clength), 1) elif self.host_config.get('xmlrpc', False): # We wrap the XMLRPC request handler in _BEGIN/_END in order to # expose the request environment to the RPC functions. RCI = self.server.RCI return RCI._END(SimpleXMLRPCRequestHandler.do_POST(RCI._BEGIN(self))) self.post_data.seek(0) except socket.error: pass except Exception, e: logging.Log([('err', 'POST error at %s: %s' % (path, e))]) self.sendResponse('

    Internal Error

    \n', code=500, msg='Error') self.rfile = self.old_rfile self.post_data = None return if not self.performPostAuthChecks(scheme, netloc, path, qs, posted): return try: return self.handleHttpRequest(scheme, netloc, path, params, query, frag, qs, posted) except socket.error: pass except Exception, e: logging.Log([('err', 'POST error at %s: %s' % (path, e))]) self.sendResponse('

    Internal Error

    \n', code=500, msg='Error') self.rfile = self.old_rfile self.post_data = None def openCGI(self, full_path, path, shtml_vars): cgi_file = CGIWrapper(self, full_path).Run() lines = cgi_file.read(32*1024).splitlines(True) if '\r\n' in lines: lines = lines[0:lines.index('\r\n')+1] elif '\n' in lines: lines = lines[0:lines.index('\n')+1] else: lines.append('') header_list = [] response_code = 200 response_message = 'OK' response_mimetype = 'text/html' for line in lines[:-1]: key, val = line.strip().split(': ', 1) if key == 'X-Response-Code': response_code = val elif key == 'X-Response-Message': response_message = val elif key.lower() == 'content-type': response_mimetype = val elif key.lower() == 'location': response_code = 302 header_list.append((key, val)) else: header_list.append((key, val)) self.sendResponse(None, code=response_code, msg=response_message, mimetype=response_mimetype, chunked=True, header_list=header_list) cgi_file.seek(sum([len(l) for l in lines])) return cgi_file def renderIndex(self, full_path, files=None): files = files or [(f, os.path.join(full_path, f)) for f in sorted(os.listdir(full_path))] # Remove dot-files and PageKite metadata files if self.host_config.get('indexes') != WEB_INDEX_ALL: files = [f for f in files if not (f[0].startswith('.') or f[0].startswith('_pagekite'))] fhtml = [''] if files: for (fn, fpath) in files: fmimetype = self.getMimeType(fn) try: fsize = os.path.getsize(fpath) or '' except OSError: fsize = 0 ops = [ ] if os.path.isdir(fpath): fclass = ['dir'] if not fn.endswith('/'): fn += '/' qfn = urllib.quote(fn) else: qfn = urllib.quote(fn) fn = os.path.basename(fn) fclass = ['file'] ops.append('download') if (fmimetype.startswith('text/') or (fmimetype == 'application/octet-stream' and fsize < 512000)): ops.append('view') (unused, ext) = os.path.splitext(fn) if ext: fclass.append(ext.replace('.', 'ext_')) fclass.append('mime_%s' % fmimetype.replace('/', '_')) ophtml = ', '.join([('%s' ) % (op, qfn, op, qfn, op) for op in sorted(ops)]) try: mtime = full_path and int(os.path.getmtime(fpath) or time.time()) except OSError: mtime = int(time.time()) fhtml.append(('' '' '' '' '' '' ) % (' '.join(fclass), ophtml, fsize, str(ts_to_date(mtime)), qfn, fn.replace('<', '<'), )) else: fhtml.append('') fhtml.append('
    %s%s%s%s
    empty
    ') return ''.join(fhtml) def sendStaticPath(self, path, mimetype, shtml_vars=None): pkite = self.server.pkite is_shtml, is_cgi, is_dir = False, False, False index_list = None try: path = urllib.unquote(path) if path.find('..') >= 0: raise IOError("Evil") paths = pkite.ui_paths def_paths = paths.get('*', {}) http_host = self.http_host if ':' not in http_host: http_host += ':80' host_paths = paths.get(http_host.replace(':', '/'), {}) path_parts = path.split('/') path_rest = [] full_path = '' root_path = '' while len(path_parts) > 0 and not full_path: pf = '/'.join(path_parts) pd = pf+'/' m = None if pf in host_paths: m = host_paths[pf] elif pd in host_paths: m = host_paths[pd] elif pf in def_paths: m = def_paths[pf] elif pd in def_paths: m = def_paths[pd] if m: policy = m[0] root_path = m[1] full_path = os.path.join(root_path, *path_rest) else: path_rest.insert(0, path_parts.pop()) if full_path: is_dir = os.path.isdir(full_path) else: if not self.host_config.get('indexes', False): return False if self.host_config.get('hide', False): return False # Generate pseudo-index ipath = path if not ipath.endswith('/'): ipath += '/' plen = len(ipath) index_list = [(p[plen:], host_paths[p][1]) for p in sorted(host_paths.keys()) if p.startswith(ipath)] if not index_list: return False full_path = '' mimetype = 'text/html' is_dir = True if is_dir and not path.endswith('/'): self.sendResponse('\n', code=302, msg='Moved', header_list=[ ('Location', '%s/' % path) ]) return True indexes = ['index.html', 'index.htm', '_pagekite.html'] dynamic_suffixes = [] if self.host_config.get('pk-shtml'): indexes[0:0] = ['index.pk-shtml'] dynamic_suffixes = ['.pk-shtml', '.pk-js'] cgi_suffixes = [] cgi_config = self.host_config.get('cgi', False) if cgi_config: if cgi_config == True: cgi_config = 'cgi' for suffix in cgi_config.split(','): indexes[0:0] = ['index.%s' % suffix] cgi_suffixes.append('.%s' % suffix) for index in indexes: ipath = os.path.join(full_path, index) if os.path.exists(ipath): mimetype = 'text/html' full_path = ipath is_dir = False break self.chunked = False rf_stat = rf_size = None if full_path: if is_dir: mimetype = 'text/html' rf_size = rf = None rf_stat = os.stat(full_path) else: for s in dynamic_suffixes: if full_path.endswith(s): is_shtml = True for s in cgi_suffixes: if full_path.endswith(s): is_cgi = True if not is_shtml and not is_cgi: shtml_vars = None rf = open(full_path, "rb") try: rf_stat = os.fstat(rf.fileno()) rf_size = rf_stat.st_size except: self.chunked = True except (IOError, OSError), e: return False headers = [ ] if rf_stat and not (is_dir or is_shtml or is_cgi): # ETags for static content: we trust the file-system. etag = sha1hex(':'.join(['%s' % s for s in [full_path, rf_stat.st_mode, rf_stat.st_ino, rf_stat.st_dev, rf_stat.st_nlink, rf_stat.st_uid, rf_stat.st_gid, rf_stat.st_size, int(rf_stat.st_mtime), int(rf_stat.st_ctime)]]))[0:24] if etag == self.headers.get('if-none-match', None): rf.close() self.sendResponse('', code=304, msg='Not Modified', mimetype=mimetype) return True else: headers.append(('ETag', etag)) # FIXME: Support ranges for resuming aborted transfers? if is_cgi: self.chunked = True rf = self.openCGI(full_path, path, shtml_vars) else: self.sendResponse(None, mimetype=mimetype, length=rf_size, chunked=self.chunked or (shtml_vars is not None), header_list=headers) chunk_size = (is_shtml and 1024 or 16) * 1024 if rf: while not self.suppress_body: data = rf.read(chunk_size) if data == "": break if is_shtml and shtml_vars: self.sendChunk(data % shtml_vars) else: self.sendChunk(data) rf.close() elif shtml_vars and not self.suppress_body: shtml_vars['title'] = '//%s%s' % (shtml_vars['http_host'], path) if self.host_config.get('indexes') in (True, WEB_INDEX_ON, WEB_INDEX_ALL): shtml_vars['body'] = self.renderIndex(full_path, files=index_list) else: shtml_vars['body'] = ('

    Directory listings disabled and ' 'index.html not found.

    ') self.sendChunk(self.TEMPLATE_HTML % shtml_vars) self.sendEof() return True def getMimeType(self, path): try: ext = path.split('.')[-1].lower() except IndexError: ext = 'DIRECTORY' if ext in self.MIME_TYPES: return self.MIME_TYPES[ext] return self.MIME_TYPES['DEFAULT'] def add_kite(self, path, qs): if path.find(self.server.secret) == -1: return {'mimetype': 'text/plain', 'body': 'Invalid secret'} pass def handleHttpRequest(self, scheme, netloc, path, params, query, frag, qs, posted): data = { 'prog': self.server.pkite.progname, 'mimetype': self.getMimeType(path), 'hostname': socket.gethostname() or 'Your Computer', 'http_host': self.http_host, 'query_string': query, 'code': 200, 'body': '', 'msg': 'OK', 'now': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), 'ver': APPVER } for key in self.headers.keys(): data['http_'+key.lower()] = self.headers.get(key) if 'download' in qs: data['mimetype'] = 'application/octet-stream' # Would be nice to set Content-Disposition too. elif 'view' in qs: data['mimetype'] = 'text/plain' data['method'] = data.get('http_x-pagekite-proto', 'http').lower() if 'http_cookie' in data: cookies = Cookie.SimpleCookie(data['http_cookie']) else: cookies = {} # Do we expose the built-in console? console = self.host_config.get('console', False) if path == self.host_config.get('yamon', False): if common.gYamon: data['body'] = common.gYamon.render_vars_text(qs.get('view', [None])[0]) else: data['body'] = '' elif console and path.startswith('/_pagekite/logout/'): parts = path.split('/') location = parts[3] or ('%s://%s/' % (data['method'], data['http_host'])) self.sendResponse('\n', code=302, msg='Moved', header_list=[ ('Set-Cookie', 'pkite_token=; path=/'), ('Location', location) ]) return elif console and path.startswith('/_pagekite/login/'): parts = path.split('/', 4) token = parts[3] location = parts[4] or ('%s://%s/_pagekite/' % (data['method'], data['http_host'])) if query: location += '?' + query if token == self.server.secret: self.sendResponse('\n', code=302, msg='Moved', header_list=[ ('Set-Cookie', 'pkite_token=%s; path=/' % token), ('Location', location) ]) return else: logging.LogDebug("Invalid token, %s != %s" % (token, self.server.secret)) data.update(self.E404) elif console and path.startswith('/_pagekite/'): if not ('pkite_token' in cookies and cookies['pkite_token'].value == self.server.secret): self.sendResponse('

    Forbidden

    \n', code=403, msg='Forbidden') return if path == '/_pagekite/': if not self.sendStaticPath('%s/control.pk-shtml' % console, 'text/html', shtml_vars=data): self.sendResponse('

    Not found

    \n', code=404, msg='Missing') return elif path.startswith('/_pagekite/quitquitquit/'): self.sendResponse('

    Kaboom

    \n', code=500, msg='Asplode') self.wfile.flush() os._exit(2) elif path.startswith('/_pagekite/add_kite/'): data.update(self.add_kite(path, qs)) elif path.endswith('/pagekite.rc'): data.update({'mimetype': 'application/octet-stream', 'body': '\n'.join(self.server.pkite.GenerateConfig())}) elif path.endswith('/pagekite.rc.txt'): data.update({'mimetype': 'text/plain', 'body': '\n'.join(self.server.pkite.GenerateConfig())}) elif path.endswith('/pagekite.cfg'): data.update({'mimetype': 'application/octet-stream', 'body': '\r\n'.join(self.server.pkite.GenerateConfig())}) else: data.update(self.E403) else: if self.sendStaticPath(path, data['mimetype'], shtml_vars=data): return if path == '/robots.txt': data.update(self.ROBOTSTXT) else: data.update(self.E404) if data['mimetype'] in ('application/octet-stream', 'text/plain'): response = self.TEMPLATE_RAW % data elif path.endswith('.jsonp'): response = self.TEMPLATE_JSONP % (data, ) else: response = self.TEMPLATE_HTML % data self.sendResponse(response, msg=data['msg'], code=data['code'], mimetype=data['mimetype'], chunked=False) self.sendEof() class RemoteControlInterface(object): ACL_OPEN = '' ACL_READ = 'r' ACL_WRITE = 'w' def __init__(self, httpd, pkite, conns): self.httpd = httpd self.pkite = pkite self.conns = conns self.modified = False self.lock = threading.Lock() self.request = None # For now, nobody gets ACL_WRITE self.auth_tokens = {httpd.secret: self.ACL_READ} # Channels are in-memory logs which can be tailed over XML-RPC. # Javascript apps can create these for implementing chat etc. self.channels = {'LOG': {'access': self.ACL_READ, 'tokens': self.auth_tokens, 'data': logging.LOG}} def _BEGIN(self, request_object): self.lock.acquire() self.request = request_object return request_object def _END(self, rv=None): if self.request: self.request = None self.lock.release() return rv def connections(self, auth_token): if (not self.request.host_config.get('console', False) or self.ACL_READ not in self.auth_tokens.get(auth_token, self.ACL_OPEN)): raise AuthError('Unauthorized') return [{'sid': c.sid, 'dead': c.dead, 'html': c.__html__()} for c in self.conns.conns] def add_kite(self, auth_token, kite_domain, kite_proto): if (not self.request.host_config.get('console', False) or self.ACL_WRITE not in self.auth_tokens.get(auth_token, self.ACL_OPEN)): raise AuthError('Unauthorized') pass def get_kites(self, auth_token): if (not self.request.host_config.get('console', False) or self.ACL_READ not in self.auth_tokens.get(auth_token, self.ACL_OPEN)): raise AuthError('Unauthorized') kites = [] for bid in self.pkite.backends: proto, domain = bid.split(':') fe_proto = proto.split('-') kite_info = { 'id': bid, 'domain': domain, 'fe_proto': fe_proto[0], 'fe_port': (len(fe_proto) > 1) and fe_proto[1] or '', 'fe_secret': self.pkite.backends[bid][BE_SECRET], 'be_proto': self.pkite.backends[bid][BE_PROTO], 'backend': self.pkite.backends[bid][BE_BACKEND], 'fe_list': [{'name': fe.server_name, 'tls': fe.using_tls, 'sid': fe.sid} for fe in self.conns.Tunnel(proto, domain)] } kites.append(kite_info) return kites def add_kite(self, auth_token, proto, fe_port, fe_domain, be_port, be_domain, shared_secret): if (not self.request.host_config.get('console', False) or self.ACL_WRITE not in self.auth_tokens.get(auth_token, self.ACL_OPEN)): raise AuthError('Unauthorized') # FIXME def remove_kite(self, auth_token, kite_id): if (not self.request.host_config.get('console', False) or self.ACL_WRITE not in self.auth_tokens.get(auth_token, self.ACL_OPEN)): raise AuthError('Unauthorized') if kite_id in self.pkite.backends: del self.pkite.backends[kite_id] logging.Log([('reconfigured', '1'), ('removed', kite_id)]) self.modified = True return self.get_kites(auth_token) def mk_channel(self, auth_token, channel): if not self.request.host_config.get('channels', False): raise AuthError('Unauthorized') chid = '%s/%s' % (self.request.http_host, channel) if chid in self.channels: raise Error('Exists') else: self.channels[chid] = {'access': self.ACL_WRITE, 'tokens': {auth_token: self.ACL_WRITE}, 'data': []} return self.append_channel(auth_token, channel, {'created': channel}) def get_channel(self, auth_token, channel): if not self.request.host_config.get('channels', False): raise AuthError('Unauthorized') chan = self.channels.get('%s/%s' % (self.request.http_host, channel), self.channels.get(channel, {})) req = chan.get('access', self.ACL_WRITE) if req not in chan.get('tokens', self.auth_tokens).get(auth_token, self.ACL_OPEN): raise AuthError('Unauthorized') return chan.get('data', []) def append_channel(self, auth_token, channel, values): data = self.get_channel(auth_token, channel) global LOG_LINE values.update({'ts': '%x' % time.time(), 'll': '%x' % LOG_LINE}) LOG_LINE += 1 data.append(values) return values def get_channel_after(self, auth_token, channel, last_seen, timeout): data = self.get_channel(auth_token, channel) last_seen = int(last_seen, 16) # line at the remote end, then we've restarted and should send everything. if (last_seen == 0) or (LOG_LINE < last_seen): return data # FIXME: LOG_LINE global for all channels? Is that suck? # We are about to get sleepy, so release our environment lock. self._END() # If our internal LOG_LINE counter is less than the count of the last seen # Else, wait at least one second, AND wait for a new line to be added to # the log (or the timeout to expire). time.sleep(1) last_ll = data[-1]['ll'] while (timeout > 0) and (data[-1]['ll'] == last_ll): time.sleep(1) timeout -= 1 # Return everything the client hasn't already seen. return [ll for ll in data if int(ll['ll'], 16) > last_seen] class UiHttpServer(SocketServer.ThreadingMixIn, SimpleXMLRPCServer): def __init__(self, sspec, pkite, conns, handler=UiRequestHandler, ssl_pem_filename=None): SimpleXMLRPCServer.__init__(self, sspec, handler) self.pkite = pkite self.conns = conns self.secret = pkite.ConfigSecret() self.server_name = sspec[0] self.server_port = sspec[1] if ssl_pem_filename: ctx = socks.SSL.Context(socks.SSL.TLSv1_METHOD) ctx.set_ciphers('HIGH:-aNULL:-eNULL:-PSK:RC4-SHA:RC4-MD5') ctx.use_privatekey_file (ssl_pem_filename) ctx.use_certificate_chain_file(ssl_pem_filename) self.socket = socks.SSL_Connect(ctx, socket.socket(self.address_family, self.socket_type), server_side=True) self.server_bind() self.server_activate() self.enable_ssl = True else: self.enable_ssl = False try: from pagekite import yamond gYamon = common.gYamon = yamond.YamonD(sspec) gYamon.vset('started', int(time.time())) gYamon.vset('version', APPVER) gYamon.vset('httpd_ssl_enabled', self.enable_ssl) gYamon.vset('errors', 0) gYamon.lcreate("tunnel_rtt", 100) gYamon.lcreate("tunnel_wrtt", 100) gYamon.lists['buffered_bytes'] = [1, 0, common.buffered_bytes] gYamon.views['selectables'] = (selectables.SELECTABLES, { 'idle': [0, 0, self.conns.idle], 'conns': [0, 0, self.conns.conns] }) except: pass self.RCI = RemoteControlInterface(self, pkite, conns) self.register_introspection_functions() self.register_instance(self.RCI) def finish_request(self, request, client_address): try: SimpleXMLRPCServer.finish_request(self, request, client_address) except (socket.error, socks.SSL.ZeroReturnError, socks.SSL.Error): pass pagekite-0.5.8a/pagekite/pk.py0000775000175000017500000036250012607426200015607 0ustar brebre00000000000000""" This is what is left of the original monolithic pagekite.py. This is slowly being refactored into smaller sub-modules. """ ############################################################################## LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2015, the Beanstalks Project ehf. and Bjarni Runar Einarsson This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see: """ ############################################################################## import base64 import cgi from cgi import escape as escape_html import errno import getopt import httplib import os import random import re import select import socket import struct import sys import tempfile import threading import time import traceback import urllib import xmlrpclib import zlib import SocketServer from CGIHTTPServer import CGIHTTPRequestHandler from SimpleXMLRPCServer import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler import Cookie from compat import * from common import * import compat import logging OPT_FLAGS = 'o:O:S:H:P:X:L:ZI:fA:R:h:p:aD:U:NE:' OPT_ARGS = ['noloop', 'clean', 'nopyopenssl', 'nossl', 'nocrashreport', 'nullui', 'remoteui', 'uiport=', 'help', 'settings', 'optfile=', 'optdir=', 'savefile=', 'friendly', 'shell', 'signup', 'list', 'add', 'only', 'disable', 'remove', 'save', 'service_xmlrpc=', 'controlpanel', 'controlpass', 'httpd=', 'pemfile=', 'httppass=', 'errorurl=', 'webpath=', 'logfile=', 'daemonize', 'nodaemonize', 'runas=', 'pidfile=', 'isfrontend', 'noisfrontend', 'settings', 'defaults', 'local=', 'domain=', 'auththreads=', 'authdomain=', 'motd=', 'register=', 'host=', 'noupgradeinfo', 'upgradeinfo=', 'ports=', 'protos=', 'portalias=', 'rawports=', 'tls_legacy', 'tls_default=', 'tls_endpoint=', 'selfsign', 'fe_certname=', 'jakenoia', 'ca_certs=', 'kitename=', 'kitesecret=', 'fingerpath=', 'backend=', 'define_backend=', 'be_config=', 'insecure', 'service_on=', 'service_off=', 'service_cfg=', 'tunnel_acl=', 'client_acl=', 'accept_acl_file=', 'frontend=', 'nofrontend=', 'frontends=', 'torify=', 'socksify=', 'proxy=', 'noproxy', 'new', 'all', 'noall', 'dyndns=', 'nozchunks', 'sslzlib', 'buffers=', 'noprobes', 'debugio', 'watch=', # DEPRECATED: 'reloadfile=', 'autosave', 'noautosave', 'webroot=', 'webaccess=', 'webindexes=', 'delete_backend='] # Enable system proxies # This will all fail if we don't have PySocksipyChain available. # FIXME: Move this code somewhere else? socks.usesystemdefaults() socks.wrapmodule(sys.modules[__name__]) if socks.HAVE_SSL: # Secure connections to pagekite.net in SSL tunnels. def_hop = socks.parseproxy('default') https_hop = socks.parseproxy(('httpcs!%s!443' ) % ','.join(['pagekite.net']+SERVICE_CERTS)) for dest in ('pagekite.net', 'up.pagekite.net', 'up.b5p.us'): socks.setproxy(dest, *def_hop) socks.addproxy(dest, *socks.parseproxy('http!%s!443' % dest)) socks.addproxy(dest, *https_hop) else: # FIXME: Should scream and shout about lack of security. pass ##[ PageKite.py code starts here! ]############################################ from proto.proto import * from proto.parsers import * from proto.selectables import * from proto.filters import * from proto.conns import * from ui.nullui import NullUi # FIXME: This could easily be a pool of threads to let us handle more # than one incoming request at a time. class AuthThread(threading.Thread): """Handle authentication work in a separate thread.""" #daemon = True def __init__(self, conns): threading.Thread.__init__(self) self.qc = threading.Condition() self.jobs = [] self.conns = conns def check(self, requests, conn, callback): self.qc.acquire() self.jobs.append((requests, conn, callback)) self.qc.notify() self.qc.release() def quit(self): self.qc.acquire() self.keep_running = False self.qc.notify() self.qc.release() try: self.join() except RuntimeError: pass def run(self): self.keep_running = True while self.keep_running: try: self._run() except Exception, e: logging.LogError('AuthThread died: %s' % e) time.sleep(5) logging.LogDebug('AuthThread: done') def _run(self): self.qc.acquire() while self.keep_running: now = int(time.time()) if not self.jobs: (requests, conn, callback) = None, None, None self.qc.wait() else: (requests, conn, callback) = self.jobs.pop(0) if logging.DEBUG_IO: print '=== AUTH REQUESTS\n%s\n===' % requests self.qc.release() quotas = [] q_conns = [] q_days = [] results = [] log_info = [] session = '%x:%s:' % (now, globalSecret()) for request in requests: try: proto, domain, srand, token, sign, prefix = request except: logging.LogError('Invalid request: %s' % (request, )) continue what = '%s:%s:%s' % (proto, domain, srand) session += what if not token or not sign: # Send a challenge. Our challenges are time-stamped, so we can # put stict bounds on possible replay attacks (20 minutes atm). results.append(('%s-SignThis' % prefix, '%s:%s' % (what, signToken(payload=what, timestamp=now)))) else: # This is a bit lame, but we only check the token if the quota # for this connection has never been verified. (quota, days, conns, reason ) = self.conns.config.GetDomainQuota(proto, domain, srand, token, sign, check_token=(conn.quota is None)) duplicates = self.conns.Tunnel(proto, domain) if not quota: if not reason: reason = 'quota' results.append(('%s-Invalid' % prefix, what)) results.append(('%s-Invalid-Why' % prefix, '%s;%s' % (what, reason))) log_info.extend([('rejected', domain), ('quota', quota), ('reason', reason)]) elif duplicates: # Duplicates... is the old one dead? Trigger a ping. for conn in duplicates: conn.TriggerPing() results.append(('%s-Duplicate' % prefix, what)) log_info.extend([('rejected', domain), ('duplicate', 'yes')]) else: results.append(('%s-OK' % prefix, what)) quotas.append((quota, request)) if conns: q_conns.append(conns) if days: q_days.append(days) if (proto.startswith('http') and self.conns.config.GetTlsEndpointCtx(domain)): results.append(('%s-SSL-OK' % prefix, what)) results.append(('%s-SessionID' % prefix, '%x:%s' % (now, sha1hex(session)))) results.append(('%s-Misc' % prefix, urllib.urlencode({ 'motd': (self.conns.config.motd_message or ''), }))) for upgrade in self.conns.config.upgrade_info: results.append(('%s-Upgrade' % prefix, ';'.join(upgrade))) if quotas: min_qconns = min(q_conns or [0]) if q_conns and min_qconns: results.append(('%s-QConns' % prefix, min_qconns)) min_qdays = min(q_days or [0]) if q_days and min_qdays: results.append(('%s-QDays' % prefix, min_qdays)) nz_quotas = [qp for qp in quotas if qp[0] and qp[0] > 0] if nz_quotas: quota = min(nz_quotas)[0] conn.quota = [quota, [qp[1] for qp in nz_quotas], time.time()] results.append(('%s-Quota' % prefix, quota)) elif requests: if not conn.quota: conn.quota = [None, requests, time.time()] else: conn.quota[2] = time.time() if logging.DEBUG_IO: print '=== AUTH RESULTS\n%s\n===' % results callback(results, log_info) self.qc.acquire() self.buffering = 0 self.qc.release() ##[ Selectables ]############################################################## class Connections(object): """A container for connections (Selectables), config and tunnel info.""" def __init__(self, config): self.config = config self.ip_tracker = {} self.idle = [] self.conns = [] self.conns_by_id = {} self.tunnels = {} self.auth_pool = [] def start(self, auth_threads=None, auth_thread_count=1): self.auth_pool = auth_threads or [] while len(self.auth_pool) < auth_thread_count: self.auth_pool.append(AuthThread(self)) for th in self.auth_pool: th.start() def Add(self, conn): self.conns.append(conn) def auth(self): return self.auth_pool[random.randint(0, len(self.auth_pool)-1)] def SetAltId(self, conn, new_id): if conn.alt_id and conn.alt_id in self.conns_by_id: del self.conns_by_id[conn.alt_id] if new_id: self.conns_by_id[new_id] = conn conn.alt_id = new_id def SetIdle(self, conn, seconds): self.idle.append((time.time() + seconds, conn.last_activity, conn)) def TrackIP(self, ip, domain): tick = '%d' % (time.time()/12) if tick not in self.ip_tracker: deadline = int(tick)-10 for ot in self.ip_tracker.keys(): if int(ot) < deadline: del self.ip_tracker[ot] self.ip_tracker[tick] = {} if ip not in self.ip_tracker[tick]: self.ip_tracker[tick][ip] = [1, domain] else: self.ip_tracker[tick][ip][0] += 1 self.ip_tracker[tick][ip][1] = domain def LastIpDomain(self, ip): domain = None for tick in sorted(self.ip_tracker.keys()): if ip in self.ip_tracker[tick]: domain = self.ip_tracker[tick][ip][1] return domain def Remove(self, conn, retry=True): try: if conn.alt_id and conn.alt_id in self.conns_by_id: del self.conns_by_id[conn.alt_id] if conn in self.conns: self.conns.remove(conn) rmp = [] for elc in self.idle: if elc[-1] == conn: rmp.append(elc) for elc in rmp: self.idle.remove(elc) for tid, tunnels in self.tunnels.items(): if conn in tunnels: tunnels.remove(conn) if not tunnels: del self.tunnels[tid] except (ValueError, KeyError): # Let's not asplode if another thread races us for this. logging.LogError('Failed to remove %s: %s' % (conn, format_exc())) if retry: return self.Remove(conn, retry=False) def IdleConns(self): return [p[-1] for p in self.idle] def Sockets(self): return [s.fd for s in self.conns] def Readable(self): # FIXME: This is O(n) now = time.time() return [s.fd for s in self.conns if s.IsReadable(now)] def Blocked(self): # FIXME: This is O(n) # Magic side-effect: update buffered byte counter blocked = [s for s in self.conns if s.IsBlocked()] common.buffered_bytes[0] = sum([len(s.write_blocked) for s in blocked]) return [s.fd for s in blocked] def DeadConns(self): return [s for s in self.conns if s.IsDead()] def CleanFds(self): evil = [] for s in self.conns: try: i, o, e = select.select([s.fd], [s.fd], [s.fd], 0) except: evil.append(s) for s in evil: logging.LogDebug('Removing broken Selectable: %s' % s) s.Cleanup() self.Remove(s) def Connection(self, fd): for conn in self.conns: if conn.fd == fd: return conn return None def TunnelServers(self): servers = {} for tid in self.tunnels: for tunnel in self.tunnels[tid]: server = tunnel.server_info[tunnel.S_NAME] if server is not None: servers[server] = 1 return servers.keys() def CloseTunnel(self, proto, domain, conn): tid = '%s:%s' % (proto, domain) if tid in self.tunnels: if conn in self.tunnels[tid]: self.tunnels[tid].remove(conn) if not self.tunnels[tid]: del self.tunnels[tid] def CheckIdleConns(self, now): active = [] for elc in self.idle: expire, last_activity, conn = elc if conn.last_activity > last_activity: active.append(elc) elif expire < now: logging.LogDebug('Killing idle connection: %s' % conn) conn.Die(discard_buffer=True) elif conn.created < now - 1: conn.SayHello() for pair in active: self.idle.remove(pair) def Tunnel(self, proto, domain, conn=None): tid = '%s:%s' % (proto, domain) if conn is not None: if tid not in self.tunnels: self.tunnels[tid] = [] self.tunnels[tid].append(conn) if tid in self.tunnels: return self.tunnels[tid] else: try: dparts = domain.split('.')[1:] while len(dparts) > 1: wild_tid = '%s:*.%s' % (proto, '.'.join(dparts)) if wild_tid in self.tunnels: return self.tunnels[wild_tid] dparts = dparts[1:] except: pass return [] class HttpUiThread(threading.Thread): """Handle HTTP UI in a separate thread.""" daemon = True def __init__(self, pkite, conns, server=None, handler=None, ssl_pem_filename=None): threading.Thread.__init__(self) if not (server and handler): self.serve = False self.httpd = None return self.ui_sspec = pkite.ui_sspec self.httpd = server(self.ui_sspec, pkite, conns, handler=handler, ssl_pem_filename=ssl_pem_filename) self.httpd.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.ui_sspec = pkite.ui_sspec = (self.ui_sspec[0], self.httpd.socket.getsockname()[1]) self.serve = True def quit(self): self.serve = False try: knock = rawsocket(socket.AF_INET, socket.SOCK_STREAM) knock.connect(self.ui_sspec) knock.close() except IOError: pass try: self.join() except RuntimeError: try: if self.httpd and self.httpd.socket: self.httpd.socket.close() except IOError: pass def run(self): while self.serve: try: self.httpd.handle_request() except KeyboardInterrupt: self.serve = False except Exception, e: logging.LogInfo('HTTP UI caught exception: %s' % e) if self.httpd: self.httpd.socket.close() logging.LogDebug('HttpUiThread: done') class UiCommunicator(threading.Thread): """Listen for interactive commands.""" def __init__(self, config, conns): threading.Thread.__init__(self) self.looping = False self.config = config self.conns = conns logging.LogDebug('UiComm: Created') def run(self): self.looping = True while self.looping: if not self.config or not self.config.ui.ALLOWS_INPUT: time.sleep(1) continue line = '' try: i, o, e = select.select([self.config.ui.rfile], [], [], 1) if not i: continue except: pass if self.config: line = self.config.ui.rfile.readline().strip() if line: self.Parse(line) logging.LogDebug('UiCommunicator: done') def Reconnect(self): if self.config.tunnel_manager: self.config.ui.Status('reconfig') self.config.tunnel_manager.CloseTunnels() self.config.tunnel_manager.HurryUp() def Parse(self, line): try: command, args = line.split(': ', 1) logging.LogDebug('UiComm: %s(%s)' % (command, args)) if args.lower() == 'none': args = None elif args.lower() == 'true': args = True elif args.lower() == 'false': args = False if command == 'exit': self.config.keep_looping = False self.config.main_loop = False elif command == 'restart': self.config.keep_looping = False self.config.main_loop = True elif command == 'config': command = 'change settings' self.config.Configure(['--%s' % args]) elif command == 'enablekite': command = 'enable kite' if args and args in self.config.backends: self.config.backends[args][BE_STATUS] = BE_STATUS_UNKNOWN self.Reconnect() else: raise Exception('No such kite: %s' % args) elif command == 'disablekite': command = 'disable kite' if args and args in self.config.backends: self.config.backends[args][BE_STATUS] = BE_STATUS_DISABLED self.Reconnect() else: raise Exception('No such kite: %s' % args) elif command == 'delkite': command = 'remove kite' if args and args in self.config.backends: del self.config.backends[args] self.Reconnect() else: raise Exception('No such kite: %s' % args) elif command == 'addkite': command = 'create new kite' args = (args or '').strip().split() or [''] if self.config.RegisterNewKite(kitename=args[0], autoconfigure=True, ask_be=True): self.Reconnect() elif command == 'save': command = 'save configuration' self.config.SaveUserConfig(quiet=(args == 'quietly')) except ValueError: logging.LogDebug('UiComm: bogus: %s' % line) except SystemExit: self.config.keep_looping = False self.config.main_loop = False except: logging.LogDebug('UiComm: failed %s' % (sys.exc_info(), )) self.config.ui.Tell(['Oops!', '', 'Failed to %s, details:' % command, '', '%s' % (sys.exc_info(), )], error=True) def quit(self): self.looping = False self.conns = None try: self.join() except RuntimeError: pass class TunnelManager(threading.Thread): """Create new tunnels as necessary or kill idle ones.""" daemon = True def __init__(self, pkite, conns): threading.Thread.__init__(self) self.pkite = pkite self.conns = conns def CheckTunnelQuotas(self, now): for tid in self.conns.tunnels: for tunnel in self.conns.tunnels[tid]: tunnel.RecheckQuota(self.conns, when=now) def PingTunnels(self, now): dead = {} for tid in self.conns.tunnels: for tunnel in self.conns.tunnels[tid]: pings = PING_INTERVAL if tunnel.server_info[tunnel.S_IS_MOBILE]: pings = PING_INTERVAL_MOBILE grace = max(PING_GRACE_DEFAULT, len(tunnel.write_blocked)/(tunnel.write_speed or 0.001)) if tunnel.last_activity == 0: pass elif tunnel.last_ping < now - PING_GRACE_MIN: if tunnel.last_activity < tunnel.last_ping-(PING_GRACE_MIN+grace): dead['%s' % tunnel] = tunnel elif tunnel.last_activity < now-pings: tunnel.SendPing() elif random.randint(0, 10*pings) == 0: tunnel.SendPing() for tunnel in dead.values(): logging.Log([('dead', tunnel.server_info[tunnel.S_NAME])]) tunnel.Die(discard_buffer=True) def CloseTunnels(self): close = [] for tid in self.conns.tunnels: for tunnel in self.conns.tunnels[tid]: close.append(tunnel) for tunnel in close: logging.Log([('closing', tunnel.server_info[tunnel.S_NAME])]) tunnel.Die(discard_buffer=True) def quit(self): self.keep_running = False try: self.join() except RuntimeError: pass def run(self): self.keep_running = True self.explained = False while self.keep_running: try: self._run() except Exception, e: logging.LogError('TunnelManager died: %s' % e) if logging.DEBUG_IO: traceback.print_exc(file=sys.stderr) time.sleep(5) logging.LogDebug('TunnelManager: done') def DoFrontendWork(self): self.CheckTunnelQuotas(time.time()) self.pkite.LoadMOTD() # FIXME: Front-ends should close dead back-end tunnels. for tid in self.conns.tunnels: proto, domain = tid.split(':') if '-' in proto: proto, port = proto.split('-') else: port = '' self.pkite.ui.NotifyFlyingFE(proto, port, domain) def ListBackEnds(self): self.pkite.ui.StartListingBackEnds() for bid in self.pkite.backends: be = self.pkite.backends[bid] # Do we have auto-SSL at the front-end? protoport, domain = bid.split(':', 1) tunnels = self.conns.Tunnel(protoport, domain) if be[BE_PROTO] in ('http', 'http2', 'http3') and tunnels: has_ssl = True for t in tunnels: if (protoport, domain) not in t.remote_ssl: has_ssl = False else: has_ssl = False # Get list of webpaths... domainp = '%s/%s' % (domain, be[BE_PORT] or '80') if (self.pkite.ui_sspec and be[BE_BHOST] == self.pkite.ui_sspec[0] and be[BE_BPORT] == self.pkite.ui_sspec[1]): builtin = True dpaths = self.pkite.ui_paths.get(domainp, {}) else: builtin = False dpaths = {} self.pkite.ui.NotifyBE(bid, be, has_ssl, dpaths, is_builtin=builtin, fingerprint=(builtin and self.pkite.ui_pemfingerprint)) self.pkite.ui.EndListingBackEnds() def UpdateUiStatus(self, problem, connecting): tunnel_count = len(self.pkite.conns and self.pkite.conns.TunnelServers() or []) tunnel_total = len(self.pkite.servers) if tunnel_count == 0: if self.pkite.isfrontend: self.pkite.ui.Status('idle', message='Waiting for back-ends.') elif tunnel_total == 0: self.pkite.ui.Notify('It looks like your Internet connection might be ' 'down! Will retry soon.') self.pkite.ui.Status('down', color=self.pkite.ui.GREY, message='No kites ready to fly. Waiting...') elif connecting == 0: self.pkite.ui.Status('down', color=self.pkite.ui.RED, message='Not connected to any front-ends, will retry...') elif tunnel_count < tunnel_total: self.pkite.ui.Status('flying', color=self.pkite.ui.YELLOW, message=('Only connected to %d/%d front-ends, will retry...' ) % (tunnel_count, tunnel_total)) elif problem: self.pkite.ui.Status('flying', color=self.pkite.ui.YELLOW, message='DynDNS updates may be incomplete, will retry...') else: self.pkite.ui.Status('flying', color=self.pkite.ui.GREEN, message='Kites are flying and all is well.') def _run(self): self.check_interval = 5 loop_count = 0 last_log = 0 while self.keep_running: loop_count += 1 now = time.time() if (now - last_log) >= (60 * 15): # Report liveness/state roughly once every 15 minutes logging.LogDebug('TunnelManager: loop #%d, interval=%s' % (loop_count, self.check_interval)) last_log = now # Reconnect if necessary, randomized exponential fallback. problem, connecting = self.pkite.CreateTunnels(self.conns) if problem or connecting: logging.LogDebug('TunnelManager: problem=%s, connecting=%s' % (problem, connecting)) incr = int(1+random.random()*self.check_interval) self.check_interval = min(60, self.check_interval + incr) time.sleep(1) else: self.check_interval = 5 # Make sure tunnels are really alive. if self.pkite.isfrontend: self.DoFrontendWork() self.PingTunnels(time.time()) # FIXME: This is constant noise, instead there should be a # command which requests this stuff. self.ListBackEnds() self.UpdateUiStatus(problem, connecting) for i in xrange(0, self.check_interval): if self.keep_running: time.sleep(1) if i > self.check_interval: break if self.pkite.isfrontend: self.conns.CheckIdleConns(time.time()) def HurryUp(self): self.check_interval = 0 def SecureCreate(path): fd = open(path, 'w') try: os.chmod(path, 0600) except OSError: pass return fd def CreateSelfSignedCert(pem_path, ui): ui.Notify('Creating a 2048-bit self-signed TLS certificate ...', prefix='-', color=ui.YELLOW) workdir = tempfile.mkdtemp() def w(fn): return os.path.join(workdir, fn) os.system(('openssl genrsa -out %s 2048') % w('key')) os.system(('openssl req -batch -new -key %s -out %s' ' -subj "/CN=PageKite/O=Self-Hosted/OU=Website"' ) % (w('key'), w('csr'))) os.system(('openssl x509 -req -days 3650 -in %s -signkey %s -out %s' ) % (w('csr'), w('key'), w('crt'))) pem = SecureCreate(pem_path) pem.write(open(w('key')).read()) pem.write('\n') pem.write(open(w('crt')).read()) pem.close() for fn in ['key', 'csr', 'crt']: os.remove(w(fn)) os.rmdir(workdir) ui.Notify('Saved certificate to: %s' % pem_path, prefix='-', color=ui.YELLOW) class PageKite(object): """Configuration and master select loop.""" def __init__(self, ui=None, http_handler=None, http_server=None): self.progname = ((sys.argv[0] or 'pagekite.py').split('/')[-1] .split('\\')[-1]) self.ui = ui or NullUi() self.ui_request_handler = http_handler self.ui_http_server = http_server self.ResetConfiguration() def ResetConfiguration(self): self.isfrontend = False self.upgrade_info = [] self.auth_threads = 1 self.auth_domain = None self.auth_domains = {} self.motd = None self.motd_message = None self.server_host = '' self.server_ports = [80] self.server_raw_ports = [] self.server_portalias = {} self.server_aliasport = {} self.server_protos = ['http', 'http2', 'http3', 'https', 'websocket', 'irc', 'finger', 'httpfinger', 'raw', 'minecraft'] self.accept_acl_file = None self.tunnel_acls = [] self.client_acls = [] self.tls_legacy = False self.tls_default = None self.tls_endpoints = {} self.fe_certname = [] self.fe_anon_tls_wrap = False self.service_provider = SERVICE_PROVIDER self.service_xmlrpc = SERVICE_XMLRPC self.daemonize = False self.pidfile = None self.logfile = None self.setuid = None self.setgid = None self.ui_httpd = None self.ui_sspec_cfg = None self.ui_sspec = None self.ui_socket = None self.ui_password = None self.ui_pemfile = None self.ui_pemfingerprint = None self.ui_magic_file = '.pagekite.magic' self.ui_paths = {} self.insecure = False self.be_config = {} self.disable_zchunks = False self.enable_sslzlib = False self.buffer_max = DEFAULT_BUFFER_MAX self.error_url = None self.finger_path = '/~%s/.finger' self.tunnel_manager = None self.client_mode = 0 self.proxy_servers = [] self.no_proxy = False self.require_all = False self.no_probes = False self.servers = [] self.servers_manual = [] self.servers_never = [] self.servers_auto = None self.servers_new_only = False self.servers_no_ping = False self.servers_preferred = [] self.servers_sessionids = {} self.dns_cache = {} self.ping_cache = {} self.last_frontend_choice = 0 self.kitename = '' self.kitesecret = '' self.dyndns = None self.last_updates = [] self.backends = {} # These are the backends we want tunnels for. self.conns = None self.last_loop = 0 self.keep_looping = True self.main_loop = True self.watch_level = [None] self.crash_report_url = '%scgi-bin/crashes.pl' % WWWHOME self.rcfile_recursion = 0 self.rcfiles_loaded = [] self.savefile = None self.added_kites = False self.ui_wfile = sys.stderr self.ui_rfile = sys.stdin self.ui_port = None self.ui_conn = None self.ui_comm = None self.save = 0 self.shell = False self.kite_add = False self.kite_only = False self.kite_disable = False self.kite_remove = False # Searching for our configuration file! We prefer the documented # 'standard' locations, but if nothing is found there and something local # exists, use that instead. try: if sys.platform[:3] in ('win', 'os2'): self.rcfile = os.path.join(os.path.expanduser('~'), 'pagekite.cfg') self.devnull = 'nul' else: # Everything else self.rcfile = os.path.join(os.path.expanduser('~'), '.pagekite.rc') self.devnull = '/dev/null' except Exception, e: # The above stuff may fail in some cases, e.g. on Android in SL4A. self.rcfile = 'pagekite.cfg' self.devnull = '/dev/null' # Look for CA Certificates. If we don't find them in the host OS, # we assume there might be something good in the program itself. self.ca_certs_default = '/etc/ssl/certs/ca-certificates.crt' if not os.path.exists(self.ca_certs_default): self.ca_certs_default = sys.argv[0] self.ca_certs = self.ca_certs_default ACL_SHORTHAND = { 'localhost': '((::ffff:)?127\..*|::1)', 'any': '.*' } def CheckAcls(self, acls, address, which, conn=None): if not acls: return True for policy, pattern in acls: if re.match(self.ACL_SHORTHAND.get(pattern, pattern)+'$', address[0]): if (policy.lower() == 'allow'): return True else: if conn: conn.LogError(('%s rejected by %s ACL: %s:%s' ) % (address[0], which, policy, pattern)) return False if conn: conn.LogError('%s rejected by default %s ACL' % (address[0], which)) return False def CheckClientAcls(self, address, conn=None): return self.CheckAcls(self.client_acls, address, 'client', conn) def CheckTunnelAcls(self, address, conn=None): return self.CheckAcls(self.tunnel_acls, address, 'tunnel', conn) def SetLocalSettings(self, ports): self.isfrontend = True self.servers_auto = None self.servers_manual = [] self.servers_never = [] self.server_ports = ports self.backends = self.ArgToBackendSpecs('http:localhost:localhost:builtin:-') def SetServiceDefaults(self, clobber=True, check=False): def_dyndns = (DYNDNS['pagekite.net'], {'user': '', 'pass': ''}) def_frontends = (1, 'frontends.b5p.us', 443) def_ca_certs = sys.argv[0] def_fe_certs = ['b5p.us'] + [c for c in SERVICE_CERTS if c != 'b5p.us'] def_error_url = 'https://pagekite.net/offline/?' if check: return (self.dyndns == def_dyndns and self.servers_auto == def_frontends and self.error_url == def_error_url and self.ca_certs == def_ca_certs and (sorted(self.fe_certname) == sorted(def_fe_certs) or not socks.HAVE_SSL)) else: self.dyndns = (not clobber and self.dyndns) or def_dyndns self.servers_auto = (not clobber and self.servers_auto) or def_frontends self.error_url = (not clobber and self.error_url) or def_error_url self.ca_certs = def_ca_certs if socks.HAVE_SSL: for cert in def_fe_certs: if cert not in self.fe_certname: self.fe_certname.append(cert) return True def GenerateConfig(self, safe=False): config = [ '###[ Current settings for pagekite.py v%s. ]#########' % APPVER, '#', '## NOTE: This file may be rewritten/reordered by pagekite.py.', '#', '', ] if not self.kitename: for be in self.backends.values(): if not self.kitename or len(self.kitename) < len(be[BE_DOMAIN]): self.kitename = be[BE_DOMAIN] self.kitesecret = be[BE_SECRET] new = not (self.kitename or self.kitesecret or self.backends) def p(vfmt, value, dval): return '%s%s' % (value and value != dval and ('', vfmt % value) or ('# ', vfmt % dval)) if self.kitename or self.kitesecret or new: config.extend([ '##[ Default kite and account details ]##', p('kitename = %s', self.kitename, 'NAME'), p('kitesecret = %s', self.kitesecret, 'SECRET'), '' ]) if self.SetServiceDefaults(check=True): config.extend([ '##[ Front-end settings: use pagekite.net defaults ]##', 'defaults', '' ]) if self.servers_manual or self.servers_never: config.append('##[ Manual front-ends ]##') for server in sorted(self.servers_manual): config.append('frontend=%s' % server) for server in sorted(self.servers_never): config.append('nofrontend=%s' % server) config.append('') else: if not self.servers_auto and not self.servers_manual: new = True config.extend([ '##[ Use this to just use pagekite.net defaults ]##', '# defaults', '' ]) config.append('##[ Custom front-end and dynamic DNS settings ]##') if self.servers_auto: config.append('frontends = %d:%s:%d' % self.servers_auto) if self.servers_manual: for server in sorted(self.servers_manual): config.append('frontend = %s' % server) if self.servers_never: for server in sorted(self.servers_never): config.append('nofrontend = %s' % server) if not self.servers_auto and not self.servers_manual: new = True config.append('# frontends = N:hostname:port') config.append('# frontend = hostname:port') config.append('# nofrontend = hostname:port # never connect') for server in self.fe_certname: config.append('fe_certname = %s' % server) if self.ca_certs != self.ca_certs_default: config.append('ca_certs = %s' % self.ca_certs) if self.dyndns: provider, args = self.dyndns for prov in sorted(DYNDNS.keys()): if DYNDNS[prov] == provider and prov != 'beanstalks.net': args['prov'] = prov if 'prov' not in args: args['prov'] = provider if args['pass']: config.append('dyndns = %(user)s:%(pass)s@%(prov)s' % args) elif args['user']: config.append('dyndns = %(user)s@%(prov)s' % args) else: config.append('dyndns = %(prov)s' % args) else: new = True config.extend([ '# dyndns = pagekite.net OR', '# dyndns = user:pass@dyndns.org OR', '# dyndns = user:pass@no-ip.com' , '#', p('errorurl = %s', self.error_url, 'http://host/page/'), p('fingerpath = %s', self.finger_path, '/~%s/.finger'), '', ]) config.append('') if self.ui_sspec or self.ui_password or self.ui_pemfile: config.extend([ '##[ Built-in HTTPD settings ]##', p('httpd = %s:%s', self.ui_sspec_cfg, ('host', 'port')) ]) if self.ui_password: config.append('httppass=%s' % self.ui_password) if self.ui_pemfile: config.append('pemfile=%s' % self.ui_pemfile) for http_host in sorted(self.ui_paths.keys()): for path in sorted(self.ui_paths[http_host].keys()): up = self.ui_paths[http_host][path] config.append('webpath = %s:%s:%s:%s' % (http_host, path, up[0], up[1])) config.append('') config.append('##[ Back-ends and local services ]##') bprinted = 0 for bid in sorted(self.backends.keys()): be = self.backends[bid] proto, domain = bid.split(':') if be[BE_BHOST]: be_spec = (be[BE_BHOST], be[BE_BPORT]) be_spec = ((be_spec == self.ui_sspec) and 'localhost:builtin' or ('%s:%s' % be_spec)) fe_spec = ('%s:%s' % (proto, (domain == self.kitename) and '@kitename' or domain)) secret = ((be[BE_SECRET] == self.kitesecret) and '@kitesecret' or be[BE_SECRET]) config.append(('%s = %-33s: %-18s: %s' ) % ((be[BE_STATUS] == BE_STATUS_DISABLED ) and 'service_off' or 'service_on ', fe_spec, be_spec, secret)) bprinted += 1 if bprinted == 0: config.append('# No back-ends! How boring!') config.append('') for http_host in sorted(self.be_config.keys()): for key in sorted(self.be_config[http_host].keys()): config.append(('service_cfg = %-30s: %-15s: %s' ) % (http_host, key, self.be_config[http_host][key])) config.append('') if bprinted == 0: new = True config.extend([ '##[ Back-end service examples ... ]##', '#', '# service_on = http:YOU.pagekite.me:localhost:80:SECRET', '# service_on = ssh:YOU.pagekite.me:localhost:22:SECRET', '# service_on = http/8080:YOU.pagekite.me:localhost:8080:SECRET', '# service_on = https:YOU.pagekite.me:localhost:443:SECRET', '# service_on = websocket:YOU.pagekite.me:localhost:8080:SECRET', '# service_on = minecraft:YOU.pagekite.me:localhost:8080:SECRET', '#', '# service_off = http:YOU.pagekite.me:localhost:4545:SECRET', '' ]) config.extend([ '##[ Allow risky known-to-be-risky incoming HTTP requests? ]##', (self.insecure) and 'insecure' or '# insecure', '' ]) if self.isfrontend or new: config.extend([ '##[ Front-end Options ]##', (self.isfrontend and 'isfrontend' or '# isfrontend') ]) comment = ((not self.isfrontend) and '# ' or '') config.extend([ p('host = %s', self.isfrontend and self.server_host, 'machine.domain.com'), '%sports = %s' % (comment, ','.join(['%s' % x for x in sorted(self.server_ports)] or [])), '%sprotos = %s' % (comment, ','.join(['%s' % x for x in sorted(self.server_protos)] or [])) ]) for pa in self.server_portalias: config.append('portalias = %s:%s' % (int(pa), int(self.server_portalias[pa]))) config.extend([ '%srawports = %s' % (comment or (not self.server_raw_ports) and '# ' or '', ','.join(['%s' % x for x in sorted(self.server_raw_ports)] or [VIRTUAL_PN])), p('auththreads = %s', self.isfrontend and self.auth_threads, 1), p('authdomain = %s', self.isfrontend and self.auth_domain, 'foo.com'), p('motd = %s', self.isfrontend and self.motd, '/path/to/motd.txt') ]) for d in sorted(self.auth_domains.keys()): config.append('authdomain=%s:%s' % (d, self.auth_domains[d])) dprinted = 0 for bid in sorted(self.backends.keys()): be = self.backends[bid] if not be[BE_BHOST]: config.append('domain = %s:%s' % (bid, be[BE_SECRET])) dprinted += 1 if not dprinted: new = True config.extend([ '# domain = http:*.pagekite.me:SECRET1', '# domain = http,https,websocket:THEM.pagekite.me:SECRET2', ]) eprinted = 0 config.extend([ '', '##[ Domains we terminate SSL/TLS for natively, with key/cert-files ]##' ]) for ep in sorted(self.tls_endpoints.keys()): config.append('tls_endpoint = %s:%s' % (ep, self.tls_endpoints[ep][0])) eprinted += 1 if eprinted == 0: new = True config.append('# tls_endpoint = DOMAIN:PEM_FILE') config.extend([ p('tls_default = %s', self.tls_default, 'DOMAIN'), p('tls_legacy = %s', self.tls_legacy, False), '', ]) config.extend([ '##[ Proxy-chain settings ]##', (self.no_proxy and 'noproxy' or '# noproxy'), ]) for proxy in self.proxy_servers: config.append('proxy = %s' % proxy) if not self.proxy_servers: config.extend([ '# socksify = host:port', '# torify = host:port', '# proxy = ssl:/path/to/client-cert.pem@host,CommonName:port', '# proxy = http://user:password@host:port/', '# proxy = socks://user:password@host:port/' ]) config.extend([ '', '##[ Front-end access controls (default=deny, if configured) ]##', p('accept_acl_file = %s', self.accept_acl_file, '/path/to/file'), ]) for policy, pattern in self.client_acls: config.append('client_acl=%s:%s' % (policy, pattern)) if not self.client_acls: config.append('# client_acl=[allow|deny]:IP-regexp') for policy, pattern in self.tunnel_acls: config.append('tunnel_acl=%s:%s' % (policy, pattern)) if not self.tunnel_acls: config.append('# tunnel_acl=[allow|deny]:IP-regexp') config.extend([ '', '', '###[ Anything below this line can usually be ignored. ]#########', '', '##[ Miscellaneous settings ]##', p('logfile = %s', self.logfile, '/path/to/file'), p('buffers = %s', self.buffer_max, DEFAULT_BUFFER_MAX), (self.servers_new_only is True) and 'new' or '# new', (self.require_all and 'all' or '# all'), (self.no_probes and 'noprobes' or '# noprobes'), (self.crash_report_url and '# nocrashreport' or 'nocrashreport'), p('savefile = %s', safe and self.savefile, '/path/to/savefile'), '', ]) if self.daemonize or self.setuid or self.setgid or self.pidfile or new: config.extend([ '##[ Systems administration settings ]##', (self.daemonize and 'daemonize' or '# daemonize') ]) if self.setuid and self.setgid: config.append('runas = %s:%s' % (self.setuid, self.setgid)) elif self.setuid: config.append('runas = %s' % self.setuid) else: new = True config.append('# runas = uid:gid') config.append(p('pidfile = %s', self.pidfile, '/path/to/file')) config.extend([ '', '###[ End of pagekite.py configuration ]#########', 'END', '' ]) if not new: config = [l for l in config if not l.startswith('# ')] clean_config = [] for i in range(0, len(config)-1): if i > 0 and (config[i].startswith('#') or config[i] == ''): if config[i+1] != '' or clean_config[-1].startswith('#'): clean_config.append(config[i]) else: clean_config.append(config[i]) clean_config.append(config[-1]) return clean_config else: return config def ConfigSecret(self, new=False): # This method returns a stable secret for the lifetime of this process. # # The secret depends on the active configuration as, reported by # GenerateConfig(). This lets external processes generate the same # secret and use the remote-control APIs as long as they can read the # *entire* config (which contains all the sensitive bits anyway). # if self.ui_httpd and self.ui_httpd.httpd and not new: return self.ui_httpd.httpd.secret else: return sha1hex('\n'.join(self.GenerateConfig())) def LoginPath(self, goto): return '/_pagekite/login/%s/%s' % (self.ConfigSecret(), goto) def LoginUrl(self, goto=''): return 'http%s://%s%s' % (self.ui_pemfile and 's' or '', '%s:%s' % self.ui_sspec, self.LoginPath(goto)) def ListKites(self): self.ui.welcome = '>>> ' + self.ui.WHITE + 'Your kites:' + self.ui.NORM message = [] for bid in sorted(self.backends.keys()): be = self.backends[bid] be_be = (be[BE_BHOST], be[BE_BPORT]) backend = (be_be == self.ui_sspec) and 'builtin' or '%s:%s' % be_be fe_port = be[BE_PORT] or '' frontend = '%s://%s%s%s' % (be[BE_PROTO], be[BE_DOMAIN], fe_port and ':' or '', fe_port) if be[BE_STATUS] == BE_STATUS_DISABLED: color = self.ui.GREY status = '(disabled)' else: color = self.ui.NORM status = (be[BE_PROTO] == 'raw') and '(HTTP proxied)' or '' message.append(''.join([color, backend, ' ' * (19-len(backend)), frontend, ' ' * (42-len(frontend)), status])) message.append(self.ui.NORM) self.ui.Tell(message) def PrintSettings(self, safe=False): print '\n'.join(self.GenerateConfig(safe=safe)) def SaveUserConfig(self, quiet=False): self.savefile = self.savefile or self.rcfile try: fd = SecureCreate(self.savefile) fd.write('\n'.join(self.GenerateConfig(safe=True))) fd.close() if not quiet: self.ui.Tell(['Settings saved to: %s' % self.savefile]) self.ui.Spacer() logging.Log([('saved', 'Settings saved to: %s' % self.savefile)]) except Exception, e: if logging.DEBUG_IO: traceback.print_exc(file=sys.stderr) self.ui.Tell(['Could not save to %s: %s' % (self.savefile, e)], error=True) self.ui.Spacer() def FallDown(self, message, help=True, longhelp=False, noexit=False): if self.conns and self.conns.auth_pool: for th in self.conns.auth_pool: th.quit() if self.ui_httpd: self.ui_httpd.quit() if self.ui_comm: self.ui_comm.quit() if self.tunnel_manager: self.tunnel_manager.quit() self.keep_looping = False for fd in (self.conns and self.conns.Sockets() or []): try: fd.close() except (IOError, OSError, TypeError, AttributeError): pass self.conns = self.ui_httpd = self.ui_comm = self.tunnel_manager = None try: os.dup2(sys.stderr.fileno(), sys.stdout.fileno()) except: pass print if help or longhelp: import manual print longhelp and manual.DOC() or manual.MINIDOC() print '***' elif not noexit: self.ui.Status('exiting', message=(message or 'Good-bye!')) if message: print 'Error: %s' % message if logging.DEBUG_IO: traceback.print_exc(file=sys.stderr) if not noexit: self.main_loop = False sys.exit(1) def GetTlsEndpointCtx(self, domain): if domain in self.tls_endpoints: return self.tls_endpoints[domain][1] parts = domain.split('.') # Check for wildcards ... if len(parts) > 2: parts[0] = '*' domain = '.'.join(parts) if domain in self.tls_endpoints: return self.tls_endpoints[domain][1] return None def SetBackendStatus(self, domain, proto='', add=None, sub=None): match = '%s:%s' % (proto, domain) for bid in self.backends: if bid == match or (proto == '' and bid.endswith(match)): status = self.backends[bid][BE_STATUS] if add: self.backends[bid][BE_STATUS] |= add if sub and (status & sub): self.backends[bid][BE_STATUS] -= sub logging.Log([('bid', bid), ('status', '0x%x' % self.backends[bid][BE_STATUS])]) def GetBackendData(self, proto, domain, recurse=True): backend = '%s:%s' % (proto.lower(), domain.lower()) if backend in self.backends: if self.backends[backend][BE_STATUS] not in BE_INACTIVE: return self.backends[backend] if recurse: dparts = domain.split('.') while len(dparts) > 1: dparts = dparts[1:] data = self.GetBackendData(proto, '.'.join(['*'] + dparts), recurse=False) if data: return data return None def GetBackendServer(self, proto, domain, recurse=True): backend = self.GetBackendData(proto, domain) or BE_NONE bhost, bport = (backend[BE_BHOST], backend[BE_BPORT]) if bhost == '-' or not bhost: return None, None return (bhost, bport), backend def IsSignatureValid(self, sign, secret, proto, domain, srand, token): return checkSignature(sign=sign, secret=secret, payload='%s:%s:%s:%s' % (proto, domain, srand, token)) def LookupDomainQuota(self, lookup): if not lookup.endswith('.'): lookup += '.' if logging.DEBUG_IO: print '=== AUTH LOOKUP\n%s\n===' % lookup (hn, al, ips) = socket.gethostbyname_ex(lookup) if logging.DEBUG_IO: print 'hn=%s\nal=%s\nips=%s\n' % (hn, al, ips) # Extract auth error and extended quota info from CNAME replies if al: error, hg, hd, hc, junk = hn.split('.', 4) q_days = int(hd, 16) q_conns = int(hc, 16) else: error = q_days = q_conns = None # If not an authentication error, quota should be encoded as an IP. ip = ips[0] if ip.startswith(AUTH_ERRORS): if not error and (ip.endswith(AUTH_ERR_USER_UNKNOWN) or ip.endswith(AUTH_ERR_INVALID)): error = 'unauthorized' else: o = [int(x) for x in ip.split('.')] return ((((o[0]*256 + o[1])*256 + o[2])*256 + o[3]), q_days, q_conns, None) # Errors on real errors are final. if not ip.endswith(AUTH_ERR_USER_UNKNOWN): return (None, q_days, q_conns, error) # User unknown, fall through to local test. return (-1, q_days, q_conns, error) def GetDomainQuota(self, protoport, domain, srand, token, sign, recurse=True, check_token=True): if '-' in protoport: try: proto, port = protoport.split('-', 1) if proto == 'raw': port_list = self.server_raw_ports else: port_list = self.server_ports porti = int(port) if porti in self.server_aliasport: porti = self.server_aliasport[porti] if porti not in port_list and VIRTUAL_PN not in port_list: logging.LogInfo('Unsupported port request: %s (%s:%s)' % (porti, protoport, domain)) return (None, None, None, 'port') except ValueError: logging.LogError('Invalid port request: %s:%s' % (protoport, domain)) return (None, None, None, 'port') else: proto, port = protoport, None if proto not in self.server_protos: logging.LogInfo('Invalid proto request: %s:%s' % (protoport, domain)) return (None, None, None, 'proto') data = '%s:%s:%s' % (protoport, domain, srand) auth_error_type = None if ((not token) or (not check_token) or checkSignature(sign=token, payload=data)): secret = (self.GetBackendData(protoport, domain) or BE_NONE)[BE_SECRET] if not secret: secret = (self.GetBackendData(proto, domain) or BE_NONE)[BE_SECRET] if secret: if self.IsSignatureValid(sign, secret, protoport, domain, srand, token): return (-1, None, None, None) elif not self.auth_domain: logging.LogError('Invalid signature for: %s (%s)' % (domain, protoport)) return (None, None, None, auth_error_type or 'signature') if self.auth_domain: adom = self.auth_domain for dom in self.auth_domains: if domain.endswith('.%s' % dom): adom = self.auth_domains[dom] try: lookup = '.'.join([srand, token, sign, protoport, domain.replace('*', '_any_'), adom]) (rv, qd, qc, auth_error_type) = self.LookupDomainQuota(lookup) if rv is None or rv >= 0: return (rv, qd, qc, auth_error_type) except Exception, e: # Lookup failed, fail open. logging.LogError('Quota lookup failed: %s' % e) return (-2, None, None, None) logging.LogInfo('No authentication found for: %s (%s)' % (domain, protoport)) return (None, None, None, auth_error_type or 'unauthorized') def ConfigureFromFile(self, filename=None, data=None): if not filename: filename = self.rcfile if self.rcfile_recursion > 25: raise ConfigError('Nested too deep: %s' % filename) self.rcfiles_loaded.append(filename) optfile = data or open(filename) args = [] for line in optfile: line = line.strip() if line and not line.startswith('#'): if line.startswith('END'): break if not line.startswith('-'): line = '--%s' % line args.append(re.sub(r'\s*:\s*', ':', re.sub(r'\s*=\s*', '=', line))) self.rcfile_recursion += 1 self.Configure(args) self.rcfile_recursion -= 1 return self def ConfigureFromDirectory(self, dirname): for fn in sorted(os.listdir(dirname)): if not fn.startswith('.') and fn.endswith('.rc'): self.ConfigureFromFile(os.path.join(dirname, fn)) def HelpAndExit(self, longhelp=False): import manual print longhelp and manual.DOC() or manual.MINIDOC() sys.exit(0) def AddNewKite(self, kitespec, status=BE_STATUS_UNKNOWN, secret=None): new_specs = self.ArgToBackendSpecs(kitespec, status, secret) self.backends.update(new_specs) req = {} for server in self.conns.TunnelServers(): req[server] = '\r\n'.join(PageKiteRequestHeaders(server, new_specs, {})) for tid, tunnels in self.conns.tunnels.iteritems(): for tunnel in tunnels: server_name = tunnel.server_info[tunnel.S_NAME] if server_name in req: tunnel.SendChunked('NOOP: 1\r\n%s\r\n\r\n!' % req[server_name], compress=False) del req[server_name] def ArgToBackendSpecs(self, arg, status=BE_STATUS_UNKNOWN, secret=None): protos, fe_domain, be_host, be_port = '', '', '', '' # Interpret the argument into a specification of what we want. parts = arg.split(':') if len(parts) == 5: protos, fe_domain, be_host, be_port, secret = parts elif len(parts) == 4: protos, fe_domain, be_host, be_port = parts elif len(parts) == 3: protos, fe_domain, be_port = parts elif len(parts) == 2: if (parts[1] == 'builtin') or ('.' in parts[0] and os.path.exists(parts[1])): fe_domain, be_port = parts[0], parts[1] protos = 'http' else: try: fe_domain, be_port = parts[0], '%s' % int(parts[1]) protos = 'http' except: be_port = '' protos, fe_domain = parts elif len(parts) == 1: fe_domain = parts[0] else: return {} # Allow http:// as a common typo instead of http: fe_domain = fe_domain.replace('/', '').lower() # Allow easy referencing of built-in HTTPD if be_port == 'builtin': self.BindUiSspec() be_host, be_port = self.ui_sspec # Specs define what we are searching for... specs = [] if protos: for proto in protos.replace('/', '-').lower().split(','): if proto == 'ssh': specs.append(['raw', '22', fe_domain, be_host, be_port or '22', secret]) else: if '-' in proto: proto, port = proto.split('-') else: if len(parts) == 1: port = '*' else: port = '' specs.append([proto, port, fe_domain, be_host, be_port, secret]) else: specs = [[None, '', fe_domain, be_host, be_port, secret]] backends = {} # For each spec, search through the existing backends and copy matches # or just shared secrets for partial matches. for proto, port, fdom, bhost, bport, sec in specs: matches = 0 for bid in self.backends: be = self.backends[bid] if fdom and fdom != be[BE_DOMAIN]: continue if not sec and be[BE_SECRET]: sec = be[BE_SECRET] if proto and (proto != be[BE_PROTO]): continue if bhost and (bhost.lower() != be[BE_BHOST]): continue if bport and (int(bport) != be[BE_BHOST]): continue if port and (port != '*') and (int(port) != be[BE_PORT]): continue backends[bid] = be[:] backends[bid][BE_STATUS] = status matches += 1 if matches == 0: proto = (proto or 'http') bhost = (bhost or 'localhost') bport = (bport or (proto in ('http', 'httpfinger', 'websocket') and 80) or (proto == 'irc' and 6667) or (proto == 'https' and 443) or (proto == 'minecraft' and 25565) or (proto == 'finger' and 79)) if port: bid = '%s-%d:%s' % (proto, int(port), fdom) else: bid = '%s:%s' % (proto, fdom) backends[bid] = BE_NONE[:] backends[bid][BE_PROTO] = proto backends[bid][BE_PORT] = port and int(port) or '' backends[bid][BE_DOMAIN] = fdom backends[bid][BE_BHOST] = bhost.lower() backends[bid][BE_BPORT] = int(bport) backends[bid][BE_SECRET] = sec backends[bid][BE_STATUS] = status return backends def BindUiSspec(self, force=False): # Create the UI thread if self.ui_httpd and self.ui_httpd.httpd: if not force: return self.ui_sspec self.ui_httpd.httpd.socket.close() self.ui_sspec = self.ui_sspec or ('localhost', 0) self.ui_httpd = HttpUiThread(self, self.conns, handler=self.ui_request_handler, server=self.ui_http_server, ssl_pem_filename = self.ui_pemfile) return self.ui_sspec def LoadMOTD(self): if self.motd: try: f = open(self.motd, 'r') self.motd_message = ''.join(f.readlines()).strip()[:8192] f.close() except (OSError, IOError): pass def SetPem(self, filename): self.ui_pemfile = filename try: p = os.popen('openssl x509 -noout -fingerprint -in %s' % filename, 'r') data = p.read().strip() p.close() self.ui_pemfingerprint = data.split('=')[1] except (OSError, ValueError): pass def Configure(self, argv): self.conns = self.conns or Connections(self) opts, args = getopt.getopt(argv, OPT_FLAGS, OPT_ARGS) for opt, arg in opts: if opt in ('-o', '--optfile'): self.ConfigureFromFile(arg) elif opt in ('-O', '--optdir'): self.ConfigureFromDirectory(arg) elif opt in ('-S', '--savefile'): if self.savefile: raise ConfigError('Multiple save-files!') self.savefile = arg elif opt == '--shell': self.shell = True elif opt == '--save': self.save = True elif opt == '--only': self.save = self.kite_only = True if self.kite_remove or self.kite_add or self.kite_disable: raise ConfigError('One change at a time please!') elif opt == '--add': self.save = self.kite_add = True if self.kite_remove or self.kite_only or self.kite_disable: raise ConfigError('One change at a time please!') elif opt == '--remove': self.save = self.kite_remove = True if self.kite_add or self.kite_only or self.kite_disable: raise ConfigError('One change at a time please!') elif opt == '--disable': self.save = self.kite_disable = True if self.kite_add or self.kite_only or self.kite_remove: raise ConfigError('One change at a time please!') elif opt == '--list': pass elif opt in ('-I', '--pidfile'): self.pidfile = arg elif opt in ('-L', '--logfile'): self.logfile = arg elif opt in ('-Z', '--daemonize'): self.daemonize = True if not self.ui.DAEMON_FRIENDLY: self.ui = NullUi() elif opt in ('-U', '--runas'): import pwd import grp parts = arg.split(':') if len(parts) > 1: self.setuid, self.setgid = (pwd.getpwnam(parts[0])[2], grp.getgrnam(parts[1])[2]) else: self.setuid = pwd.getpwnam(parts[0])[2] self.main_loop = False elif opt in ('-X', '--httppass'): self.ui_password = arg elif opt in ('-P', '--pemfile'): self.SetPem(arg) elif opt in ('--selfsign', ): pf = self.rcfile.replace('.rc', '.pem').replace('.cfg', '.pem') if not os.path.exists(pf): CreateSelfSignedCert(pf, self.ui) self.SetPem(pf) elif opt in ('-H', '--httpd'): parts = arg.split(':') host = parts[0] or 'localhost' if len(parts) > 1: self.ui_sspec = self.ui_sspec_cfg = (host, int(parts[1])) else: self.ui_sspec = self.ui_sspec_cfg = (host, 0) elif opt == '--nowebpath': host, path = arg.split(':', 1) if host in self.ui_paths and path in self.ui_paths[host]: del self.ui_paths[host][path] elif opt == '--webpath': host, path, policy, fpath = arg.split(':', 3) # Defaults... path = path or os.path.normpath(fpath) host = host or '*' policy = policy or WEB_POLICY_DEFAULT if policy not in WEB_POLICIES: raise ConfigError('Policy must be one of: %s' % WEB_POLICIES) elif os.path.isdir(fpath): if not path.endswith('/'): path += '/' hosti = self.ui_paths.get(host, {}) hosti[path] = (policy or 'public', os.path.abspath(fpath)) self.ui_paths[host] = hosti elif opt == '--tls_default': self.tls_default = arg elif opt == '--tls_legacy': self.tls_legacy = True elif opt == '--tls_endpoint': name, pemfile = arg.split(':', 1) ctx = socks.MakeBestEffortSSLContext(legacy=self.tls_legacy) ctx.use_privatekey_file(pemfile) ctx.use_certificate_chain_file(pemfile) self.tls_endpoints[name] = (pemfile, ctx) elif opt in ('-D', '--dyndns'): if arg.startswith('http'): self.dyndns = (arg, {'user': '', 'pass': ''}) elif '@' in arg: splits = arg.split('@') provider = splits.pop() usrpwd = '@'.join(splits) if provider in DYNDNS: provider = DYNDNS[provider] if ':' in usrpwd: usr, pwd = usrpwd.split(':', 1) self.dyndns = (provider, {'user': usr, 'pass': pwd}) else: self.dyndns = (provider, {'user': usrpwd, 'pass': ''}) elif arg: if arg in DYNDNS: arg = DYNDNS[arg] self.dyndns = (arg, {'user': '', 'pass': ''}) else: self.dyndns = None elif opt in ('-p', '--ports'): self.server_ports = [int(x) for x in arg.split(',')] elif opt == '--portalias': port, alias = arg.split(':') self.server_portalias[int(port)] = int(alias) self.server_aliasport[int(alias)] = int(port) elif opt == '--protos': self.server_protos = [x.lower() for x in arg.split(',')] elif opt == '--rawports': self.server_raw_ports = [(x == VIRTUAL_PN and x or int(x)) for x in arg.split(',')] elif opt in ('-h', '--host'): self.server_host = arg elif opt == '--auththreads': self.auth_threads = int(arg) elif opt in ('-A', '--authdomain'): if ':' in arg: d, a = arg.split(':') self.auth_domains[d.lower()] = a if not self.auth_domain: self.auth_domain = a else: self.auth_domains = {} self.auth_domain = arg elif opt == '--motd': self.motd = arg self.LoadMOTD() elif opt == '--noupgradeinfo': self.upgrade_info = [] elif opt == '--upgradeinfo': version, tag, md5, human_url, file_url = arg.split(';') self.upgrade_info.append((version, tag, md5, human_url, file_url)) elif opt in ('-f', '--isfrontend'): self.isfrontend = True logging.LOG_THRESHOLD *= 4 elif opt in ('-a', '--all'): self.require_all = True elif opt in ('-N', '--new'): self.servers_new_only = True elif opt == '--accept_acl_file': self.accept_acl_file = arg elif opt == '--client_acl': policy, pattern = arg.split(':', 1) self.client_acls.append((policy, pattern)) elif opt == '--tunnel_acl': policy, pattern = arg.split(':', 1) self.tunnel_acls.append((policy, pattern)) elif opt in ('--noproxy', ): self.no_proxy = True self.proxy_servers = [] socks.setdefaultproxy() elif opt in ('--proxy', '--socksify', '--torify'): if opt == '--proxy': socks.adddefaultproxy(*socks.parseproxy(arg)) else: (host, port) = arg.rsplit(':', 1) socks.adddefaultproxy(socks.PROXY_TYPE_SOCKS5, host, int(port)) if not self.proxy_servers: # Make DynDNS updates go via the proxy. socks.wrapmodule(urllib) self.proxy_servers = [arg] else: self.proxy_servers.append(arg) if opt == '--torify': self.servers_new_only = True # Disable initial DNS lookups (leaks) self.servers_no_ping = True # Disable front-end pings self.crash_report_url = None # Disable crash reports # This increases the odds of unrelated requests getting lumped # together in the tunnel, which makes traffic analysis harder. compat.SEND_ALWAYS_BUFFERS = True elif opt == '--ca_certs': self.ca_certs = arg elif opt == '--jakenoia': self.fe_anon_tls_wrap = True elif opt == '--fe_certname': if arg == '': self.fe_certname = [] else: cert = arg.lower() if cert not in self.fe_certname: self.fe_certname.append(cert) elif opt == '--service_xmlrpc': self.service_xmlrpc = arg elif opt == '--frontend': self.servers_manual.append(arg) elif opt == '--nofrontend': self.servers_never.append(arg) elif opt == '--frontends': count, domain, port = arg.split(':') self.servers_auto = (int(count), domain, int(port)) elif opt in ('--errorurl', '-E'): self.error_url = arg elif opt == '--fingerpath': self.finger_path = arg elif opt == '--kitename': self.kitename = arg elif opt == '--kitesecret': self.kitesecret = arg elif opt in ('--service_on', '--service_off', '--backend', '--define_backend'): if opt in ('--backend', '--service_on'): status = BE_STATUS_UNKNOWN else: status = BE_STATUS_DISABLED bes = self.ArgToBackendSpecs(arg.replace('@kitesecret', self.kitesecret) .replace('@kitename', self.kitename), status=status) for bid in bes: if bid in self.backends: raise ConfigError("Same service/domain defined twice: %s" % bid) if not self.kitename: self.kitename = bes[bid][BE_DOMAIN] self.kitesecret = bes[bid][BE_SECRET] self.backends.update(bes) elif opt in ('--be_config', '--service_cfg'): host, key, val = arg.split(':', 2) if key.startswith('user/'): key = key.replace('user/', 'password/') hostc = self.be_config.get(host, {}) hostc[key] = {'True': True, 'False': False, 'None': None}.get(val, val) self.be_config[host] = hostc elif opt == '--domain': protos, domain, secret = arg.split(':') if protos in ('*', ''): protos = ','.join(self.server_protos) for proto in protos.split(','): bid = '%s:%s' % (proto, domain) if bid in self.backends: raise ConfigError("Same service/domain defined twice: %s" % bid) self.backends[bid] = BE_NONE[:] self.backends[bid][BE_PROTO] = proto self.backends[bid][BE_DOMAIN] = domain self.backends[bid][BE_SECRET] = secret self.backends[bid][BE_STATUS] = BE_STATUS_UNKNOWN elif opt == '--insecure': self.insecure = True elif opt == '--noprobes': self.no_probes = True elif opt == '--nofrontend': self.isfrontend = False elif opt == '--nodaemonize': self.daemonize = False elif opt == '--noall': self.require_all = False elif opt == '--nozchunks': self.disable_zchunks = True elif opt == '--nullui': self.ui = NullUi() elif opt == '--remoteui': import pagekite.ui.remote self.ui = pagekite.ui.remote.RemoteUi() elif opt == '--uiport': self.ui_port = int(arg) elif opt == '--sslzlib': self.enable_sslzlib = True elif opt == '--watch': self.watch_level[0] = int(arg) elif opt == '--debugio': logging.DEBUG_IO = True elif opt == '--buffers': self.buffer_max = int(arg) elif opt == '--nocrashreport': self.crash_report_url = None elif opt == '--noloop': self.main_loop = False elif opt == '--local': self.SetLocalSettings([int(p) for p in arg.split(',')]) if not 'localhost' in args: args.append('localhost') elif opt == '--defaults': self.SetServiceDefaults() elif opt in ('--clean', '--nopyopenssl', '--nossl', '--settings', '--signup', '--friendly'): # These are handled outside the main loop, we just ignore them. pass elif opt in ('--webroot', '--webaccess', '--webindexes', '--noautosave', '--autosave', '--reloadfile', '--delete_backend'): # FIXME: These are deprecated, we should probably warn the user. pass elif opt == '--help': self.HelpAndExit(longhelp=True) elif opt == '--controlpanel': import webbrowser webbrowser.open(self.LoginUrl()) sys.exit(0) elif opt == '--controlpass': print self.ConfigSecret() sys.exit(0) else: self.HelpAndExit() # Make sure these are configured before we try and do XML-RPC stuff. socks.DEBUG = (logging.DEBUG_IO or socks.DEBUG) and logging.LogDebug if self.ca_certs: socks.setdefaultcertfile(self.ca_certs) # Handle the user-friendly argument stuff and simple registration. return self.ParseFriendlyBackendSpecs(args) def ParseFriendlyBackendSpecs(self, args): just_these_backends = {} just_these_webpaths = {} just_these_be_configs = {} argsets = [] while 'AND' in args: argsets.append(args[0:args.index('AND')]) args[0:args.index('AND')+1] = [] if args: argsets.append(args) for args in argsets: # Extract the config options first... be_config = [p for p in args if p.startswith('+')] args = [p for p in args if not p.startswith('+')] fe_spec = (args.pop().replace('@kitesecret', self.kitesecret) .replace('@kitename', self.kitename)) if os.path.exists(fe_spec): raise ConfigError('Is a local file: %s' % fe_spec) be_paths = [] be_path_prefix = '' if len(args) == 0: be_spec = '' elif len(args) == 1: if '*' in args[0] or '?' in args[0]: if sys.platform[:3] in ('win', 'os2'): be_paths = [args[0]] be_spec = 'builtin' elif os.path.exists(args[0]): be_paths = [args[0]] be_spec = 'builtin' else: be_spec = args[0] else: be_spec = 'builtin' be_paths = args[:] be_proto = 'http' # A sane default... if be_spec == '': be = None else: be = be_spec.replace('/', '').split(':') if be[0].lower() in ('http', 'http2', 'http3', 'https', 'httpfinger', 'finger', 'ssh', 'irc'): be_proto = be.pop(0) if len(be) < 2: be.append({'http': '80', 'http2': '80', 'http3': '80', 'https': '443', 'irc': '6667', 'httpfinger': '80', 'finger': '79', 'ssh': '22'}[be_proto]) if len(be) > 2: raise ConfigError('Bad back-end definition: %s' % be_spec) if len(be) < 2: try: if be[0] != 'builtin': int(be[0]) be = ['localhost', be[0]] except ValueError: raise ConfigError('`%s` should be a file, directory, port or ' 'protocol' % be_spec) # Extract the path prefix from the fe_spec fe_urlp = fe_spec.split('/', 3) if len(fe_urlp) == 4: fe_spec = '/'.join(fe_urlp[:3]) be_path_prefix = '/' + fe_urlp[3] fe = fe_spec.replace('/', '').split(':') if len(fe) == 3: fe = ['%s-%s' % (fe[0], fe[2]), fe[1]] elif len(fe) == 2: try: fe = ['%s-%s' % (be_proto, int(fe[1])), fe[0]] except ValueError: pass elif len(fe) == 1 and be: fe = [be_proto, fe[0]] # Do our own globbing on Windows if sys.platform[:3] in ('win', 'os2'): import glob new_paths = [] for p in be_paths: new_paths.extend(glob.glob(p)) be_paths = new_paths for f in be_paths: if not os.path.exists(f): raise ConfigError('File or directory not found: %s' % f) spec = ':'.join(fe) if be: spec += ':' + ':'.join(be) specs = self.ArgToBackendSpecs(spec) just_these_backends.update(specs) spec = specs[specs.keys()[0]] http_host = '%s/%s' % (spec[BE_DOMAIN], spec[BE_PORT] or '80') if be_config: # Map the +foo=bar values to per-site config settings. host_config = just_these_be_configs.get(http_host, {}) for cfg in be_config: if '=' in cfg: key, val = cfg[1:].split('=', 1) elif cfg.startswith('+no'): key, val = cfg[3:], False else: key, val = cfg[1:], True if ':' in key: raise ConfigError('Please do not use : in web config keys.') if key.startswith('user/'): key = key.replace('user/', 'password/') host_config[key] = val just_these_be_configs[http_host] = host_config if be_paths: host_paths = just_these_webpaths.get(http_host, {}) host_config = just_these_be_configs.get(http_host, {}) rand_seed = '%s:%x' % (specs[specs.keys()[0]][BE_SECRET], time.time()/3600) first = (len(host_paths.keys()) == 0) or be_path_prefix paranoid = host_config.get('hide', False) set_root = host_config.get('root', True) if len(be_paths) == 1: skip = len(os.path.dirname(be_paths[0])) else: skip = len(os.path.dirname(os.path.commonprefix(be_paths)+'X')) for path in be_paths: phead, ptail = os.path.split(path) if paranoid: if path.endswith('/'): path = path[0:-1] webpath = '%s/%s' % (sha1hex(rand_seed+os.path.dirname(path))[0:9], os.path.basename(path)) elif (first and set_root and os.path.isdir(path)): webpath = '' elif (os.path.isdir(path) and not path.startswith('.') and not os.path.isabs(path)): webpath = path[skip:] + '/' elif path == '.': webpath = '' else: webpath = path[skip:] while webpath.endswith('/.'): webpath = webpath[:-2] host_paths[(be_path_prefix + '/' + webpath).replace('///', '/' ).replace('//', '/') ] = (WEB_POLICY_DEFAULT, os.path.abspath(path)) first = False just_these_webpaths[http_host] = host_paths need_registration = {} for be in just_these_backends.values(): if not be[BE_SECRET]: if self.kitesecret and be[BE_DOMAIN] == self.kitename: be[BE_SECRET] = self.kitesecret elif not self.kite_remove and not self.kite_disable: need_registration[be[BE_DOMAIN]] = True for domain in need_registration: if '.' not in domain: raise ConfigError('Not valid domain: %s' % domain) for domain in need_registration: result = self.RegisterNewKite(kitename=domain) if not result: raise ConfigError("Not sure what to do with %s, giving up." % domain) # Update the secrets... rdom, rsecret = result for be in just_these_backends.values(): if be[BE_DOMAIN] == domain: be[BE_SECRET] = rsecret # Update the kite names themselves, if they changed. if rdom != domain: for bid in just_these_backends.keys(): nbid = bid.replace(':'+domain, ':'+rdom) if nbid != bid: just_these_backends[nbid] = just_these_backends[bid] just_these_backends[nbid][BE_DOMAIN] = rdom del just_these_backends[bid] if just_these_backends.keys(): if self.kite_add: self.backends.update(just_these_backends) elif self.kite_remove: try: for bid in just_these_backends: be = self.backends[bid] if be[BE_PROTO] in ('http', 'http2', 'http3'): http_host = '%s/%s' % (be[BE_DOMAIN], be[BE_PORT] or '80') if http_host in self.ui_paths: del self.ui_paths[http_host] if http_host in self.be_config: del self.be_config[http_host] del self.backends[bid] except KeyError: raise ConfigError('No such kite: %s' % bid) elif self.kite_disable: try: for bid in just_these_backends: self.backends[bid][BE_STATUS] = BE_STATUS_DISABLED except KeyError: raise ConfigError('No such kite: %s' % bid) elif self.kite_only: for be in self.backends.values(): be[BE_STATUS] = BE_STATUS_DISABLED self.backends.update(just_these_backends) else: # Nothing explictly requested: 'only' behavior with a twist; # If kites are new, don't make disables persist on save. for be in self.backends.values(): be[BE_STATUS] = (need_registration and BE_STATUS_DISABLE_ONCE or BE_STATUS_DISABLED) self.backends.update(just_these_backends) self.ui_paths.update(just_these_webpaths) self.be_config.update(just_these_be_configs) return self def GetServiceXmlRpc(self): service = self.service_xmlrpc return xmlrpclib.ServerProxy(self.service_xmlrpc, None, None, False) def _KiteInfo(self, kitename): is_service_domain = kitename and SERVICE_DOMAIN_RE.search(kitename) is_subdomain_of = is_cname_for = is_cname_ready = False secret = None for be in self.backends.values(): if be[BE_SECRET] and (be[BE_DOMAIN] == kitename): secret = be[BE_SECRET] if is_service_domain: parts = kitename.split('.') if '-' in parts[0]: parts[0] = '-'.join(parts[0].split('-')[1:]) is_subdomain_of = '.'.join(parts) elif len(parts) > 3: is_subdomain_of = '.'.join(parts[1:]) elif kitename: try: (hn, al, ips) = socket.gethostbyname_ex(kitename) if hn != kitename and SERVICE_DOMAIN_RE.search(hn): is_cname_for = hn except: pass return (secret, is_subdomain_of, is_service_domain, is_cname_for, is_cname_ready) def RegisterNewKite(self, kitename=None, first=False, ask_be=False, autoconfigure=False): registered = False if kitename: (secret, is_subdomain_of, is_service_domain, is_cname_for, is_cname_ready) = self._KiteInfo(kitename) if secret: self.ui.StartWizard('Updating kite: %s' % kitename) registered = True else: self.ui.StartWizard('Creating kite: %s' % kitename) else: if first: self.ui.StartWizard('Create your first kite') else: self.ui.StartWizard('Creating a new kite') is_subdomain_of = is_service_domain = False is_cname_for = is_cname_ready = False # This is the default... be_specs = ['http:%s:localhost:80'] service = self.GetServiceXmlRpc() service_accounts = {} if self.kitename and self.kitesecret: service_accounts[self.kitename] = self.kitesecret for be in self.backends.values(): if SERVICE_DOMAIN_RE.search(be[BE_DOMAIN]): if be[BE_DOMAIN] == is_cname_for: is_cname_ready = True if be[BE_SECRET] not in service_accounts.values(): service_accounts[be[BE_DOMAIN]] = be[BE_SECRET] service_account_list = service_accounts.keys() if registered: state = ['choose_backends'] if service_account_list: state = ['choose_kite_account'] else: state = ['use_service_question'] history = [] def Goto(goto, back_skips_current=False): if not back_skips_current: history.append(state[0]) state[0] = goto def Back(): if history: state[0] = history.pop(-1) else: Goto('abort') register = is_cname_for or kitename account = email = None while 'end' not in state: try: if 'use_service_question' in state: ch = self.ui.AskYesNo('Use the PageKite.net service?', pre=['Welcome to PageKite!', '', 'Please answer a few quick questions to', 'create your first kite.', '', 'By continuing, you agree to play nice', 'and abide by the Terms of Service at:', '- %s' % (SERVICE_TOS_URL, SERVICE_TOS_URL)], default=True, back=-1, no='Abort') if ch is True: self.SetServiceDefaults(clobber=False) if not kitename: Goto('service_signup_email') elif is_cname_for and is_cname_ready: register = kitename Goto('service_signup_email') elif is_service_domain: register = is_cname_for or kitename if is_subdomain_of: # FIXME: Shut up if parent is already in local config! Goto('service_signup_is_subdomain') else: Goto('service_signup_email') else: Goto('service_signup_bad_domain') else: Goto('manual_abort') elif 'service_login_email' in state: p = None while not email or not p: (email, p) = self.ui.AskLogin('Please log on ...', pre=[ 'By logging on to %s,' % self.service_provider, 'you will be able to use this kite', 'with your pre-existing account.' ], email=email, back=(email, False)) if email and p: try: self.ui.Working('Logging on to your account') service_accounts[email] = service.getSharedSecret(email, p) # FIXME: Should get the list of preconfigured kites via. RPC # so we don't try to create something that already # exists? Or should the RPC not just not complain? account = email Goto('create_kite') except: email = p = None self.ui.Tell(['Login failed! Try again?'], error=True) if p is False: Back() break elif ('service_signup_is_subdomain' in state): ch = self.ui.AskYesNo('Use this name?', pre=['%s is a sub-domain.' % kitename, '', 'NOTE: This process will fail if you', 'have not already registered the parent', 'domain, %s.' % is_subdomain_of], default=True, back=-1) if ch is True: if account: Goto('create_kite') elif email: Goto('service_signup') else: Goto('service_signup_email') elif ch is False: Goto('service_signup_kitename') else: Back() elif ('service_signup_bad_domain' in state or 'service_login_bad_domain' in state): if is_cname_for: alternate = is_cname_for ch = self.ui.AskYesNo('Create both?', pre=['%s is a CNAME for %s.' % (kitename, is_cname_for)], default=True, back=-1) else: alternate = kitename.split('.')[-2]+'.'+SERVICE_DOMAINS[0] ch = self.ui.AskYesNo('Try to create %s instead?' % alternate, pre=['Sorry, %s is not a valid service domain.' % kitename], default=True, back=-1) if ch is True: register = alternate Goto(state[0].replace('bad_domain', 'email')) elif ch is False: register = alternate = kitename = False Goto('service_signup_kitename', back_skips_current=True) else: Back() elif 'service_signup_email' in state: email = self.ui.AskEmail('What is your e-mail address?', pre=['We need to be able to contact you', 'now and then with news about the', 'service and your account.', '', 'Your details will be kept private.'], back=False) if email and register: Goto('service_signup') elif email: Goto('service_signup_kitename') else: Back() elif ('service_signup_kitename' in state or 'service_ask_kitename' in state): try: self.ui.Working('Fetching list of available domains') domains = service.getAvailableDomains('', '') except: domains = ['.%s' % x for x in SERVICE_DOMAINS_SIGNUP] ch = self.ui.AskKiteName(domains, 'Name this kite:', pre=['Your kite name becomes the public name', 'of your personal server or web-site.', '', 'Names are provided on a first-come,', 'first-serve basis. You can create more', 'kites with different names later on.'], back=False) if ch: kitename = register = ch (secret, is_subdomain_of, is_service_domain, is_cname_for, is_cname_ready) = self._KiteInfo(ch) if secret: self.ui.StartWizard('Updating kite: %s' % kitename) registered = True else: self.ui.StartWizard('Creating kite: %s' % kitename) Goto('choose_backends') else: Back() elif 'choose_backends' in state: if ask_be and autoconfigure: skip = False ch = self.ui.AskBackends(kitename, ['http'], ['80'], [], 'Enable which service?', back=False, pre=[ 'You control which of your files or servers', 'PageKite exposes to the Internet. ', ], default=','.join(be_specs)) if ch: be_specs = ch.split(',') else: skip = ch = True if ch: if registered: Goto('create_kite', back_skips_current=skip) elif is_subdomain_of: Goto('service_signup_is_subdomain', back_skips_current=skip) elif account: Goto('create_kite', back_skips_current=skip) elif email: Goto('service_signup', back_skips_current=skip) else: Goto('service_signup_email', back_skips_current=skip) else: Back() elif 'service_signup' in state: try: self.ui.Working('Signing up') details = service.signUp(email, register) if details.get('secret', False): service_accounts[email] = details['secret'] self.ui.AskYesNo('Continue?', pre=[ 'Your kite is ready to fly!', '', 'Note: To complete the signup process,', 'check your e-mail (and spam folders) for', 'activation instructions. You can give', 'PageKite a try first, but un-activated', 'accounts are disabled after %d minutes.' % details['timeout'], ], yes='Finish', no=False, default=True) self.ui.EndWizard() if autoconfigure: for be_spec in be_specs: self.backends.update(self.ArgToBackendSpecs( be_spec % register, secret=details['secret'])) self.added_kites = True return (register, details['secret']) else: error = details.get('error', 'unknown') except IOError: error = 'network' except: error = '%s' % (sys.exc_info(), ) if error == 'pleaselogin': self.ui.ExplainError(error, 'Signup failed!', subject=email) Goto('service_login_email', back_skips_current=True) elif error == 'email': self.ui.ExplainError(error, 'Signup failed!', subject=register) Goto('service_login_email', back_skips_current=True) elif error in ('domain', 'domaintaken', 'subdomain'): self.ui.ExplainError(error, 'Invalid domain!', subject=register) register, kitename = None, None Goto('service_signup_kitename', back_skips_current=True) elif error == 'network': self.ui.ExplainError(error, 'Network error!', subject=self.service_provider) Goto('service_signup', back_skips_current=True) else: self.ui.ExplainError(error, 'Unknown problem!') print 'FIXME! Error is %s' % error Goto('abort') elif 'choose_kite_account' in state: choices = service_account_list[:] choices.append('Use another service provider') justdoit = (len(service_account_list) == 1) if justdoit: ch = 1 else: ch = self.ui.AskMultipleChoice(choices, 'Register with', pre=['Choose an account for this kite:'], default=1) account = choices[ch-1] if ch == len(choices): Goto('manual_abort') elif kitename: Goto('choose_backends', back_skips_current=justdoit) else: Goto('service_ask_kitename', back_skips_current=justdoit) elif 'create_kite' in state: secret = service_accounts[account] subject = None cfgs = {} result = {} error = None try: if registered and kitename and secret: pass elif is_cname_for and is_cname_ready: self.ui.Working('Creating your kite') subject = kitename result = service.addCnameKite(account, secret, kitename) time.sleep(2) # Give the service side a moment to replicate... else: self.ui.Working('Creating your kite') subject = register result = service.addKite(account, secret, register) time.sleep(2) # Give the service side a moment to replicate... for be_spec in be_specs: cfgs.update(self.ArgToBackendSpecs(be_spec % register, secret=secret)) if is_cname_for == register and 'error' not in result: subject = kitename result.update(service.addCnameKite(account, secret, kitename)) error = result.get('error', None) if not error: for be_spec in be_specs: cfgs.update(self.ArgToBackendSpecs(be_spec % kitename, secret=secret)) except Exception, e: error = '%s' % e if error: self.ui.ExplainError(error, 'Kite creation failed!', subject=subject) Goto('abort') else: self.ui.Tell(['Success!']) self.ui.EndWizard() if autoconfigure: self.backends.update(cfgs) self.added_kites = True return (register or kitename, secret) elif 'manual_abort' in state: if self.ui.Tell(['Aborted!', '', 'Please manually add information about your', 'kites and front-ends to the configuration file:', '', ' %s' % self.rcfile], error=True, back=False) is False: Back() else: self.ui.EndWizard() if self.ui.ALLOWS_INPUT: return None sys.exit(0) elif 'abort' in state: self.ui.EndWizard() if self.ui.ALLOWS_INPUT: return None sys.exit(0) else: raise ConfigError('Unknown state: %s' % state) except KeyboardInterrupt: sys.stderr.write('\n') if history: Back() else: raise KeyboardInterrupt() self.ui.EndWizard() return None def CheckConfig(self): if self.ui_sspec: self.BindUiSspec() if not self.servers_manual and not self.servers_auto and not self.isfrontend: if not self.servers and not self.ui.ALLOWS_INPUT: raise ConfigError('Nothing to do! List some servers, or run me as one.') return self def CheckAllTunnels(self, conns): missing = [] for backend in self.backends: proto, domain = backend.split(':') if not conns.Tunnel(proto, domain): missing.append(domain) if missing: self.FallDown('No tunnel for %s' % missing, help=False) TMP_UUID_MAP = { '2400:8900::f03c:91ff:feae:ea35:443': '106.187.99.46:443', '2a01:7e00::f03c:91ff:fe96:234:443': '178.79.140.143:443', '2600:3c03::f03c:91ff:fe96:2bf:443': '50.116.52.206:443', '2600:3c01::f03c:91ff:fe96:257:443': '173.230.155.164:443', '69.164.211.158:443': '50.116.52.206:443', } def Ping(self, host, port): cid = uuid = '%s:%s' % (host, port) if self.servers_no_ping: return (0, uuid) while ((cid not in self.ping_cache) or (len(self.ping_cache[cid]) < 2) or (time.time()-self.ping_cache[cid][0][0] > 60)): start = time.time() try: try: if ':' in host: fd = socks.socksocket(socket.AF_INET6, socket.SOCK_STREAM) else: fd = socks.socksocket(socket.AF_INET, socket.SOCK_STREAM) except: fd = socks.socksocket(socket.AF_INET, socket.SOCK_STREAM) try: fd.settimeout(3.0) # Missing in Python 2.2 except: fd.setblocking(1) fd.connect((host, port)) fd.send('HEAD / HTTP/1.0\r\n\r\n') data = fd.recv(1024) fd.close() except Exception, e: logging.LogDebug('Ping %s:%s failed: %s' % (host, port, e)) return (100000, uuid) elapsed = (time.time() - start) try: uuid = data.split('X-PageKite-UUID: ')[1].split()[0] except: uuid = self.TMP_UUID_MAP.get(uuid, uuid) if cid not in self.ping_cache: self.ping_cache[cid] = [] elif len(self.ping_cache[cid]) > 10: self.ping_cache[cid][8:] = [] self.ping_cache[cid][0:0] = [(time.time(), (elapsed, uuid))] window = min(3, len(self.ping_cache[cid])) pingval = sum([e[1][0] for e in self.ping_cache[cid][:window]])/window uuid = self.ping_cache[cid][0][1][1] logging.LogDebug(('Pinged %s:%s: %f [win=%s, uuid=%s]' ) % (host, port, pingval, window, uuid)) return (pingval, uuid) def GetHostIpAddrs(self, host): rv = [] try: info = socket.getaddrinfo(host, 0, socket.AF_UNSPEC, socket.SOCK_STREAM) rv = [i[4][0] for i in info] except AttributeError: rv = socket.gethostbyname_ex(host)[2] return rv def CachedGetHostIpAddrs(self, host): now = int(time.time()) if host in self.dns_cache: # FIXME: This number (900) is 3x the pagekite.net service DNS TTL, which # should be about right. BUG: nothing keeps those two numbers in sync! # This number must be larger, or we prematurely disconnect frontends. for exp in [t for t in self.dns_cache[host] if t < now-900]: del self.dns_cache[host][exp] else: self.dns_cache[host] = {} try: self.dns_cache[host][now] = self.GetHostIpAddrs(host) except: logging.LogDebug('DNS lookup failed for %s' % host) ips = {} for ipaddrs in self.dns_cache[host].values(): for ip in ipaddrs: ips[ip] = 1 return ips.keys() def GetActiveBackends(self): active = [] for bid in self.backends: (proto, bdom) = bid.split(':') if (self.backends[bid][BE_STATUS] not in BE_INACTIVE and self.backends[bid][BE_SECRET] and not bdom.startswith('*')): active.append(bid) return active def ChooseFrontEnds(self): self.servers = [] self.servers_preferred = [] self.last_frontend_choice = time.time() servers_all = {} servers_pref = {} # Enable internal loopback if self.isfrontend: need_loopback = False for be in self.backends.values(): if be[BE_BHOST]: need_loopback = True if need_loopback: servers_all['loopback'] = servers_pref['loopback'] = LOOPBACK_FE # Convert the hostnames into IP addresses... def sping(server): (host, port) = server.split(':') ipaddrs = self.CachedGetHostIpAddrs(host) if ipaddrs: ptime, uuid = self.Ping(ipaddrs[0], int(port)) server = '%s:%s' % (ipaddrs[0], port) if server not in self.servers_never: servers_all[uuid] = servers_pref[uuid] = server threads, deadline = [], time.time() + 5 for server in self.servers_manual: threads.append(threading.Thread(target=sping, args=(server,))) threads[-1].daemon = True threads[-1].start() for t in threads: t.join(max(0.1, deadline - time.time())) # Lookup and choose from the auto-list (and our old domain). if self.servers_auto: (count, domain, port) = self.servers_auto # First, check for old addresses and always connect to those. selected = {} if not self.servers_new_only: def bping(bid): (proto, bdom) = bid.split(':') for ip in self.CachedGetHostIpAddrs(bdom): # FIXME: What about IPv6 localhost? if not ip.startswith('127.'): server = '%s:%s' % (ip, port) if server not in self.servers_never: servers_all[self.Ping(ip, int(port))[1]] = server threads, deadline = [], time.time() + 5 for bid in self.GetActiveBackends(): threads.append(threading.Thread(target=bping, args=(bid,))) threads[-1].daemon = True threads[-1].start() for t in threads: t.join(max(0.1, deadline - time.time())) try: pings = [] ips = [ip for ip in self.CachedGetHostIpAddrs(domain) if ('%s:%s' % (ip, port)) not in self.servers_never] def iping(ip): pings.append(self.Ping(ip, port)) threads, deadline = [], time.time() + 5 for ip in ips: threads.append(threading.Thread(target=iping, args=(ip,))) threads[-1].daemon = True threads[-1].start() for t in threads: t.join(max(0.1, deadline - time.time())) except Exception, e: logging.LogDebug('Unreachable: %s, %s' % (domain, e)) ips = pings = [] while count > 0 and ips and pings: mIdx = pings.index(min(pings)) if pings[mIdx][0] > 60: # This is worthless data, abort. break else: count -= 1 uuid = pings[mIdx][1] server = '%s:%s' % (ips[mIdx], port) if uuid not in servers_all: servers_all[uuid] = server if uuid not in servers_pref: servers_pref[uuid] = ips[mIdx] del pings[mIdx] del ips[mIdx] self.servers = servers_all.values() self.servers_preferred = servers_pref.values() logging.LogDebug('Preferred: %s' % ', '.join(self.servers_preferred)) def ConnectFrontend(self, conns, server): self.ui.Status('connect', color=self.ui.YELLOW, message='Front-end connect: %s' % server) tun = Tunnel.BackEnd(server, self.backends, self.require_all, conns) if tun: tun.filters.append(HttpHeaderFilter(self.ui)) if not self.insecure: tun.filters.append(HttpSecurityFilter(self.ui)) if self.watch_level[0] is not None: tun.filters.append(TunnelWatcher(self.ui, self.watch_level)) logging.Log([('connect', server)]) return True else: logging.LogInfo('Failed to connect', [('FE', server)]) self.ui.Notify('Failed to connect to %s' % server, prefix='!', color=self.ui.YELLOW) return False def DisconnectFrontend(self, conns, server): logging.Log([('disconnect', server)]) kill = [] for bid in conns.tunnels: for tunnel in conns.tunnels[bid]: if (server == tunnel.server_info[tunnel.S_NAME] and tunnel.countas.startswith('frontend')): kill.append(tunnel) for tunnel in kill: if len(tunnel.users.keys()) < 1: tunnel.Die() return kill and True or False def CreateTunnels(self, conns): live_servers = conns.TunnelServers() failures = 0 connections = 0 if len(self.GetActiveBackends()) > 0: if self.last_frontend_choice < time.time()-FE_PING_INTERVAL: self.servers = [] if not self.servers or len(self.servers) > len(live_servers): self.ChooseFrontEnds() else: self.servers_preferred = [] self.servers = [] if not self.servers: logging.LogDebug('Not sure which servers to contact, making no changes.') return 0, 0 threads, deadline = [], time.time() + 120 def connect_in_thread(conns, server, state): try: state[1] = self.ConnectFrontend(conns, server) except (IOError, OSError): state[1] = False for server in self.servers: if server not in live_servers: if server == LOOPBACK_FE: loop = LoopbackTunnel.Loop(conns, self.backends) loop.filters.append(HttpHeaderFilter(self.ui)) if not self.insecure: loop.filters.append(HttpSecurityFilter(self.ui)) else: state = [None, None] state[0] = threading.Thread(target=connect_in_thread, args=(conns, server, state)) state[0].daemon = True state[0].start() threads.append(state) for thread, result in threads: thread.join(max(0.1, deadline - time.time())) for thread, result in threads: # This will treat timeouts both as connections AND failures if result is not False: connections += 1 if result is not True: failures += 1 for server in live_servers: if server not in self.servers and server not in self.servers_preferred: if self.DisconnectFrontend(conns, server): connections += 1 if self.dyndns: ddns_fmt, ddns_args = self.dyndns domains = {} for bid in self.backends.keys(): proto, domain = bid.split(':') if domain not in domains: domains[domain] = (self.backends[bid][BE_SECRET], []) if bid in conns.tunnels: ips, bips = [], [] for tunnel in conns.tunnels[bid]: ip = rsplit(':', tunnel.server_info[tunnel.S_NAME])[0] if not ip == LOOPBACK_HN and not tunnel.read_eof: if not self.servers_preferred or ip in self.servers_preferred: ips.append(ip) else: bips.append(ip) for ip in (ips or bips): if ip not in domains[domain]: domains[domain][1].append(ip) updates = {} for domain, (secret, ips) in domains.iteritems(): if ips: iplist = ','.join(ips) payload = '%s:%s' % (domain, iplist) args = {} args.update(ddns_args) args.update({ 'domain': domain, 'ip': ips[0], 'ips': iplist, 'sign': signToken(secret=secret, payload=payload, length=100) }) # FIXME: This may fail if different front-ends support different # protocols. In practice, this should be rare. updates[payload] = ddns_fmt % args last_updates = self.last_updates self.last_updates = [] for update in updates: if update in last_updates: # Was successful last time, no point in doing it again. self.last_updates.append(update) else: domain, ips = update.split(':', 1) try: self.ui.Status('dyndns', color=self.ui.YELLOW, message='Updating DNS for %s...' % domain) # FIXME: If the network misbehaves, can this stall forever? result = ''.join(urllib.urlopen(updates[update]).readlines()) if result.startswith('good') or result.startswith('nochg'): logging.Log([('dyndns', result), ('data', update)]) self.SetBackendStatus(update.split(':')[0], sub=BE_STATUS_ERR_DNS) self.last_updates.append(update) # Success! Make sure we remember these IP were live. if domain not in self.dns_cache: self.dns_cache[domain] = {} self.dns_cache[domain][int(time.time())] = ips.split(',') else: logging.LogInfo('DynDNS update failed: %s' % result, [('data', update)]) self.SetBackendStatus(update.split(':')[0], add=BE_STATUS_ERR_DNS) failures += 1 except Exception, e: logging.LogInfo('DynDNS update failed: %s' % e, [('data', update)]) if logging.DEBUG_IO: traceback.print_exc(file=sys.stderr) self.SetBackendStatus(update.split(':')[0], add=BE_STATUS_ERR_DNS) # Hmm, the update may have succeeded - assume the "worst". self.dns_cache[domain][int(time.time())] = ips.split(',') failures += 1 return failures, connections def LogTo(self, filename, close_all=True, dont_close=[]): if filename == 'memory': logging.Log = logging.LogToMemory filename = self.devnull elif filename == 'syslog': logging.Log = logging.LogSyslog filename = self.devnull compat.syslog.openlog(self.progname, syslog.LOG_PID, syslog.LOG_DAEMON) else: logging.Log = logging.LogToFile if filename in ('stdio', 'stdout'): try: logging.LogFile = os.fdopen(sys.stdout.fileno(), 'w', 0) except: logging.LogFile = sys.stdout else: try: logging.LogFile = fd = open(filename, "a", 0) os.dup2(fd.fileno(), sys.stdout.fileno()) if not self.ui.WANTS_STDERR: os.dup2(fd.fileno(), sys.stdin.fileno()) os.dup2(fd.fileno(), sys.stderr.fileno()) except Exception, e: raise ConfigError('%s' % e) def Daemonize(self): # Fork once... if os.fork() != 0: os._exit(0) # Fork twice... os.setsid() if os.fork() != 0: os._exit(0) def ProcessWritable(self, oready): if logging.DEBUG_IO: print '\n=== Ready for Write: %s' % [o and o.fileno() or '' for o in oready] for osock in oready: if osock: conn = self.conns.Connection(osock) if conn and not conn.Send([], try_flush=True): conn.Die(discard_buffer=True) def ProcessReadable(self, iready, throttle): if logging.DEBUG_IO: print '\n=== Ready for Read: %s' % [i and i.fileno() or None for i in iready] for isock in iready: if isock is not None: conn = self.conns.Connection(isock) if conn and not (conn.fd and conn.ReadData(maxread=throttle)): conn.Die(discard_buffer=True) def ProcessDead(self, epoll=None): for conn in self.conns.DeadConns(): if epoll and conn.fd: try: epoll.unregister(conn.fd) except (IOError, TypeError): pass conn.Cleanup() self.conns.Remove(conn) def Select(self, epoll, waittime): iready = oready = eready = None isocks, osocks = self.conns.Readable(), self.conns.Blocked() try: if isocks or osocks: iready, oready, eready = select.select(isocks, osocks, [], waittime) else: # Windoes does not seem to like empty selects, so we do this instead. time.sleep(waittime/2) except KeyboardInterrupt: raise except: logging.LogError('Error in select(%s/%s): %s' % (isocks, osocks, format_exc())) self.conns.CleanFds() self.last_loop -= 1 now = time.time() if not iready and not oready: if (isocks or osocks) and (now < self.last_loop + 1): logging.LogError('Spinning, pausing ...') time.sleep(0.1) return None, iready, oready, eready def Epoll(self, epoll, waittime): fdc = {} now = time.time() evs = [] broken = False try: bbc = 0 for c in self.conns.conns: fd, mask = c.fd, 0 if not c.IsDead(): if c.IsBlocked(): bbc += len(c.write_blocked) mask |= select.EPOLLOUT if c.IsReadable(now): mask |= select.EPOLLIN if mask: try: fdc[fd.fileno()] = fd except socket.error: # If this fails, then the socket has HUPed, however we need to # bypass epoll to make sure that's reflected in iready below. bid = 'dead-%d' % len(evs) fdc[bid] = fd evs.append((bid, select.EPOLLHUP)) # Trigger removal of c.fd, if it was still in the epoll. fd, mask = None, 0 if mask: try: epoll.modify(fd, mask) except IOError: try: epoll.register(fd, mask) except (IOError, TypeError): evs.append((fd, select.EPOLLHUP)) # Error == HUP else: try: epoll.unregister(c.fd) # Important: Use c.fd, not fd! except (IOError, TypeError): # Failing to unregister is OK, ignore pass common.buffered_bytes[0] = bbc evs.extend(epoll.poll(waittime)) except (IOError, OSError): broken = 'in poll' except KeyboardInterrupt: epoll.close() raise rmask = select.EPOLLIN | select.EPOLLHUP iready = [fdc.get(e[0]) for e in evs if e[1] & rmask] oready = [fdc.get(e[0]) for e in evs if e[1] & select.EPOLLOUT] if not broken and ((None in iready) or (None in oready)): broken = 'unknown FDs' if broken: logging.LogError('Epoll appears to be broken (%s), recreating' % broken) try: epoll.close() except (IOError, OSError, TypeError, AttributeError): pass epoll = select.epoll() return epoll, iready, oready, [] def CreatePollObject(self): try: epoll = select.epoll() mypoll = self.Epoll except: epoll = None mypoll = self.Select return epoll, mypoll def Loop(self): self.conns.start(auth_thread_count=self.auth_threads) if self.ui_httpd: self.ui_httpd.start() if self.tunnel_manager: self.tunnel_manager.start() if self.ui_comm: self.ui_comm.start() epoll, mypoll = self.CreatePollObject() self.last_barf = self.last_loop = time.time() logging.LogDebug('Entering main %s loop' % (epoll and 'epoll' or 'select')) loop_count = 0 while self.keep_looping: epoll, iready, oready, eready = mypoll(epoll, 1.1) now = time.time() if oready: self.ProcessWritable(oready) if common.buffered_bytes[0] < 1024 * self.buffer_max: throttle = None else: logging.LogDebug("FIXME: Nasty pause to let buffers clear!") time.sleep(0.1) throttle = 1024 if iready: self.ProcessReadable(iready, throttle) self.ProcessDead(epoll) self.last_loop = now loop_count += 1 if now - self.last_barf > (logging.DEBUG_IO and 15 or 600): self.last_barf = now if epoll: epoll.close() epoll, mypoll = self.CreatePollObject() logging.LogDebug('Loop #%d, selectable map: %s' % (loop_count, SELECTABLES)) if epoll: epoll.close() def Start(self, howtoquit='CTRL+C = Stop'): conns = self.conns = self.conns or Connections(self) # If we are going to spam stdout with ugly crap, then there is no point # attempting the fancy stuff. This also makes us backwards compatible # for the most part. if self.logfile == 'stdio': if not self.ui.DAEMON_FRIENDLY: self.ui = NullUi() # Announce that we've started up! self.ui.Status('startup', message='Starting up...') self.ui.Notify(('Hello! This is %s v%s.' ) % (self.progname, APPVER), prefix='>', color=self.ui.GREEN, alignright='[%s]' % howtoquit) config_report = [('started', sys.argv[0]), ('version', APPVER), ('platform', sys.platform), ('argv', ' '.join(sys.argv[1:])), ('ca_certs', self.ca_certs)] for optf in self.rcfiles_loaded: config_report.append(('optfile_%s' % optf, 'ok')) logging.Log(config_report) if not socks.HAVE_SSL: self.ui.Notify('SECURITY WARNING: No SSL support was found, tunnels are insecure!', prefix='!', color=self.ui.WHITE) self.ui.Notify('Please install either pyOpenSSL or python-ssl.', prefix='!', color=self.ui.WHITE) # Create global secret self.ui.Status('startup', message='Collecting entropy for a secure secret...') logging.LogInfo('Collecting entropy for a secure secret.') globalSecret() self.ui.Status('startup', message='Starting up...') # Create the UI Communicator self.ui_comm = UiCommunicator(self, conns) try: # Set up our listeners if we are a server. if self.isfrontend: self.ui.Notify('This is a PageKite front-end server.') for port in self.server_ports: Listener(self.server_host, port, conns, acl=self.accept_acl_file) for port in self.server_raw_ports: if port != VIRTUAL_PN and port > 0: Listener(self.server_host, port, conns, connclass=RawConn, acl=self.accept_acl_file) if self.ui_port: Listener('127.0.0.1', self.ui_port, conns, connclass=UiConn, acl=self.accept_acl_file) # Create the Tunnel Manager self.tunnel_manager = TunnelManager(self, conns) except Exception, e: self.LogTo('stdio') logging.FlushLogMemory() if logging.DEBUG_IO: traceback.print_exc(file=sys.stderr) raise ConfigError('Configuring listeners: %s ' % e) # Configure logging if self.logfile: keep_open = [s.fd.fileno() for s in conns.conns] if self.ui_httpd: keep_open.append(self.ui_httpd.httpd.socket.fileno()) self.LogTo(self.logfile, dont_close=keep_open) elif not (hasattr(sys.stdout, 'isatty') and sys.stdout.isatty()): # Preserve sane behavior when not run at the console. self.LogTo('stdio') # Flush in-memory log, if necessary logging.FlushLogMemory() # Set up SIGHUP handler. if self.logfile: try: import signal def reopen(x,y): if self.logfile: self.LogTo(self.logfile, close_all=False) logging.LogDebug('SIGHUP received, reopening: %s' % self.logfile) signal.signal(signal.SIGHUP, reopen) except Exception: logging.LogError('Warning: signal handler unavailable, logrotate will not work.') # Disable compression in OpenSSL if socks.HAVE_SSL and not self.enable_sslzlib: socks.DisableSSLCompression() # Daemonize! if self.daemonize: self.Daemonize() # Create PID file if self.pidfile: pf = open(self.pidfile, 'w') pf.write('%s\n' % os.getpid()) pf.close() # Do this after creating the PID and log-files. if self.daemonize: os.chdir('/') # Drop privileges, if we have any. if self.setgid: os.setgid(self.setgid) if self.setuid: os.setuid(self.setuid) if self.setuid or self.setgid: logging.Log([('uid', os.getuid()), ('gid', os.getgid())]) # Make sure we have what we need if self.require_all: self.CreateTunnels(conns) self.CheckAllTunnels(conns) # Finally, run our select loop. self.Loop() self.ui.Status('exiting', message='Stopping...') logging.Log([('stopping', 'pagekite.py')]) if self.ui_httpd: self.ui_httpd.quit() if self.ui_comm: self.ui_comm.quit() if self.tunnel_manager: self.tunnel_manager.quit() if self.conns: if self.conns.auth_pool: for th in self.conns.auth_pool: th.quit() for conn in self.conns.conns: conn.Cleanup() ##[ Main ]##################################################################### def Main(pagekite, configure, uiclass=NullUi, progname=None, appver=APPVER, http_handler=None, http_server=None): crashes = 0 shell_mode = None while True: ui = uiclass() logging.ResetLog() pk = pagekite(ui=ui, http_handler=http_handler, http_server=http_server) try: try: try: configure(pk) except SystemExit, status: sys.exit(status) except Exception, e: if logging.DEBUG_IO: raise raise ConfigError(e) shell_mode = shell_mode or pk.shell if shell_mode is not True: pk.Start() except (ConfigError, getopt.GetoptError), msg: pk.FallDown(msg, help=(not shell_mode), noexit=shell_mode) if shell_mode: shell_mode = 'more' except KeyboardInterrupt, msg: pk.FallDown(None, help=False, noexit=True) if shell_mode: shell_mode = 'auto' else: return except SystemExit, status: if shell_mode: shell_mode = 'more' else: sys.exit(status) except Exception, msg: traceback.print_exc(file=sys.stderr) if pk.crash_report_url: try: print 'Submitting crash report to %s' % pk.crash_report_url logging.LogDebug(''.join(urllib.urlopen(pk.crash_report_url, urllib.urlencode({ 'platform': sys.platform, 'appver': APPVER, 'crash': format_exc() })).readlines())) except Exception, e: print 'FAILED: %s' % e pk.FallDown(msg, help=False, noexit=pk.main_loop) crashes = min(9, crashes+1) if shell_mode: crashes = 0 try: sys.argv[1:] = Shell(pk, ui, shell_mode) shell_mode = 'more' except (KeyboardInterrupt, IOError, OSError): ui.Status('quitting') print return elif not pk.main_loop: return # Exponential fall-back. logging.LogDebug('Restarting in %d seconds...' % (2 ** crashes)) time.sleep(2 ** crashes) def Shell(pk, ui, shell_mode): import manual try: ui.Reset() if shell_mode != 'more': ui.StartWizard('The PageKite Shell') pre = [ 'Press ENTER to fly your kites or CTRL+C to quit. Or, type some', 'arguments to and try other things. Type `help` for help.' ] else: pre = '' prompt = os.path.basename(sys.argv[0]) while True: rv = ui.AskQuestion(prompt, prompt=' $', back=False, pre=pre ).strip().split() ui.EndWizard(quietly=True) while rv and rv[0] in ('pagekite.py', prompt): rv.pop(0) if rv and rv[0] == 'help': ui.welcome = '>>> ' + ui.WHITE + ' '.join(rv) + ui.NORM ui.Tell(manual.HELP(rv[1:]).splitlines()) pre = [] elif rv and rv[0] == 'quit': raise KeyboardInterrupt() else: if rv and rv[0] in OPT_ARGS: rv[0] = '--'+rv[0] return rv finally: ui.EndWizard(quietly=True) print def Configure(pk): if '--appver' in sys.argv: print '%s' % APPVER sys.exit(0) if '--clean' not in sys.argv and '--help' not in sys.argv: if os.path.exists(pk.rcfile): pk.ConfigureFromFile() friendly_mode = (('--friendly' in sys.argv) or (sys.platform[:3] in ('win', 'os2', 'dar'))) if friendly_mode and hasattr(sys.stdout, 'isatty') and sys.stdout.isatty(): pk.shell = (len(sys.argv) < 2) and 'auto' pk.Configure(sys.argv[1:]) if '--settings' in sys.argv: pk.PrintSettings(safe=True) sys.exit(0) if not pk.backends.keys() and (not pk.kitesecret or not pk.kitename): if '--signup' in sys.argv or friendly_mode: pk.RegisterNewKite(autoconfigure=True, first=True) if friendly_mode: pk.save = True pk.CheckConfig() if pk.added_kites: if (pk.save or pk.ui.AskYesNo('Save settings to %s?' % pk.rcfile, default=(len(pk.backends.keys()) > 0))): pk.SaveUserConfig() pk.servers_new_only = 'Once' elif pk.save: pk.SaveUserConfig(quiet=True) if ('--list' in sys.argv or pk.kite_add or pk.kite_remove or pk.kite_only or pk.kite_disable): pk.ListKites() sys.exit(0) pagekite-0.5.8a/pagekite/manual.py0000775000175000017500000005257512603542201016456 0ustar brebre00000000000000#!/usr/bin/env python """ The program manual! """ import re import time from common import * from compat import ts_to_iso MAN_NAME = ("""\ pagekite.py v%s - Make localhost servers publicly visible """ % APPVER) MAN_SYNOPSIS = ("""\ pagekite.py [--options] [service] kite-name [+flags] """) MAN_DESCRIPTION = ("""\ PageKite is a system for exposing localhost servers to the public Internet. It is most commonly used to make local web servers or SSH servers publicly visible, although almost any TCP-based protocol can work if the client knows how to use an HTTP proxy. PageKite uses a combination of tunnels and reverse proxies to compensate for the fact that localhost usually does not have a public IP address and is often subject to adverse network conditions, including aggressive firewalls and multiple layers of NAT. This program implements both ends of the tunnel: the local "back-end" and the remote "front-end" reverse-proxy relay. For convenience, pagekite.py also includes a basic HTTP server for quickly exposing files and directories to the World Wide Web for casual sharing and collaboration. """) MAN_EXAMPLES = ("""\
    Basic usage, gives http://localhost:80/ a public name:
        $ pagekite.py NAME.pagekite.me
    
        To expose specific folders, files or use alternate local ports:
        $ pagekite.py /a/path/ NAME.pagekite.me +indexes  # built-in HTTPD
        $ pagekite.py *.html   NAME.pagekite.me           # built-in HTTPD
        $ pagekite.py 3000     NAME.pagekite.me           # HTTPD on 3000
    
        To expose multiple local servers (SSH and HTTP):
        $ pagekite.py ssh://NAME.pagekite.me AND 3000 NAME.pagekite.me
    """) MAN_KITES = ("""\ The most comman usage of pagekite.py is as a back-end, where it is used to expose local services to the outside world. Examples of services are: a local HTTP server, a local SSH server, a folder or a file. A service is exposed by describing it on the command line, along with the desired public kite name. If a kite name is requested which does not already exist in the configuration file and program is run interactively, the user will be prompted and given the option of signing up and/or creating a new kite using the pagekite.net service. Multiple services and kites can be specified on a single command-line, separated by the word 'AND' (note capital letters are required). This may cause problems if you have many files and folders by that name, but that should be relatively rare. :-) """) MAN_KITE_EXAMPLES = ("""\ The options --list, --add, --disable and \ --remove can be used to manipulate the kites and service definitions in your configuration file, if you prefer not to edit it by hand. Examples:
    Adding new kites
        $ pagekite.py --add /a/path/ NAME.pagekite.me +indexes
        $ pagekite.py --add 80 OTHER-NAME.pagekite.me
    
        To display the current configuration
        $ pagekite.py --list
    
        Disable or delete kites (--add re-enables)
        $ pagekite.py --disable OTHER-NAME.pagekite.me
        $ pagekite.py --remove NAME.pagekite.me
    """) MAN_FLAGS = ("""\ Flags are used to tune the behavior of a particular kite, for example by enabling access controls or specific features of the built-in HTTP server. """) MAN_FLAGS_COMMON = ("""\ +ip
    /1.2.3.4 __Enable connections only from this IP address. +ip
    /1.2.3 __Enable connections only from this /24 netblock. """) MAN_FLAGS_HTTP = ("""\ +password/name=pass Require a username and password (HTTP Basic Authentication) +rewritehost __Rewrite the incoming Host: header. +rewritehost=N __Replace Host: header value with N. +rawheaders __Do not rewrite (or add) any HTTP headers at all. +insecure __Allow access to phpMyAdmin, /admin, etc. (per kite). """) MAN_FLAGS_BUILTIN = ("""\ +indexes __Enable directory indexes. +indexes=all __Enable directory indexes including hidden (dot-) files. +hide __Obfuscate URLs of shared files. +cgi=list A list of extensions, for which files should be treated as CGI scripts (example: +cgi=cgi,pl,sh). """) MAN_OPTIONS = ("""\ The full power of pagekite.py lies in the numerous options which can be specified on the command line or in a configuration file (see below). Note that many options, especially the service and domain definitions, are additive and if given multiple options the program will attempt to obey them all. Options are processed in order and if they are not additive then the last option will override all preceding ones. Although pagekite.py accepts a great many options, most of the time the program defaults will Just Work. """) MAN_OPT_COMMON = ("""\ --clean __Skip loading the default configuration file. --signup __Interactively sign up for pagekite.net service. --defaults __Set defaults for use with pagekite.net service. --nocrashreport __Don't send anonymous crash reports to pagekite.net. """) MAN_OPT_BACKEND = ("""\ --shell __Run PageKite in an interactive shell. --nullui __Silent UI for scripting. Assumes Yes on all questions. --list __List all configured kites. --add __Add (or enable) the following kites, save config. --remove __Remove the following kites, save config. --disable __Disable the following kites, save config. --only __Disable all but the following kites, save config. --insecure __Allow access to phpMyAdmin, /admin, etc. (global). --local=ports __Configure for local serving only (no remote front-end). --watch=N __Display proxied data (higher N = more verbosity). --noproxy __Ignore system (or config file) proxy settings. --proxy=type:server:port,\ --socksify=server:port,\ --torify=server:port __ Connect to the front-ends using SSL, an HTTP proxy, a SOCKS proxy, or the Tor anonymity network. The type can be any of 'ssl', 'http' or 'socks5'. The server name can either be a plain hostname, user@hostname or user:password@hostname. For SSL connections the user part may be a path to a client cert PEM file. If multiple proxies are defined, they will be chained one after another. --service_on=proto:kitename:host:port:secret __ Explicit configuration for a service kite. Generally kites are created on the command-line using the service short-hand described above, but this syntax is used in the config file. --service_off=proto:kitename:host:port:secret __ Same as --service_on, except disabled by default. --service_cfg=..., --webpath=... __ These options are used in the configuration file to store service and flag settings (see above). These are both likely to change in the near future, so please just pretend you didn't notice them. --frontend=host:port __ Connect to the named front-end server. If this option is repeated, multiple connections will be made. --frontends=num:dns-name:port __ Choose num front-ends from the A records of a DNS domain name, using the given port number. Default behavior is to probe all addresses and use the fastest one. --nofrontend=ip:port __ Never connect to the named front-end server. This can be used to exclude some front-ends from auto-configuration. --fe_certname=domain __ Connect using SSL, accepting valid certs for this domain. If this option is repeated, any of the named certificates will be accepted, but the first will be preferred. --ca_certs=/path/to/file __ Path to your trusted root SSL certificates file. --dyndns=X __ Register changes with DynDNS provider X. X can either be simply the name of one of the 'built-in' providers, or a URL format string for ad-hoc updating. --all __Terminate early if any tunnels fail to register. --new __Don't attempt to connect to any kites' old front-ends. --fingerpath=P __Path recipe for the httpfinger back-end proxy. --noprobes __Reject all probes for service state. """) MAN_OPT_FRONTEND = ("""\ --isfrontend __Enable front-end operation. --domain=proto,proto2,pN:domain:secret __ Accept tunneling requests for the named protocols and specified domain, using the given secret. A * may be used as a wildcard for subdomains or protocols. --authdomain=auth-domain,\ --authdomain=target-domain:auth-domain __ Use auth-domain as a remote authentication server for the DNS-based authetication protocol. If no target-domain is given, use this as the default authentication method. --motd=/path/to/motd __ Send the contents of this file to new back-ends as a "message of the day". --host=hostname __Listen on the given hostname only. --ports=list __Listen on a comma-separated list of ports. --portalias=A:B __Report port A as port B to backends (because firewalls). --protos=list __Accept the listed protocols for tunneling. --rawports=list __ Listen for raw connections these ports. The string '%s' allows arbitrary ports in HTTP CONNECT. --accept_acl_file=/path/to/file __ Consult an external access control file before accepting an incoming connection. Quick'n'dirty for mitigating abuse. The format is one rule per line: `rule policy comment` where a rule is an IP or regexp and policy is 'allow' or 'deny'. --client_acl=policy:regexp,\ --tunnel_acl=policy:regexp __ Add a client connection or tunnel access control rule. Policies should be 'allow' or 'deny', the regular expression should be written to match IPv4 or IPv6 addresses. If defined, access rules are checkd in order and if none matches, incoming connections will be rejected. --tls_default=name __ Default name to use for SSL, if SNI (Server Name Indication) is missing from incoming HTTPS connections. --tls_endpoint=name:/path/to/file __ Terminate SSL/TLS for a name using key/cert from a file. """) MAN_OPT_SYSTEM = ("""\ --optfile=/path/to/file __ Read settings from file X. Default is ~/.pagekite.rc. --optdir=/path/to/directory __ Read settings from /path/to/directory/*.rc, in lexicographical order. --savefile=/path/to/file __ Saved settings will be written to this file. --save __Save the current configuration to the savefile. --settings __ Dump the current settings to STDOUT, formatted as a configuration file would be. --nozchunks __Disable zlib tunnel compression. --sslzlib __Enable zlib compression in OpenSSL. --buffers=N __Buffer at most N kB of data before blocking. --logfile=F __Log to file F, stdio means standard output. --daemonize __Run as a daemon. --runas=U:G __Set UID:GID after opening our listening sockets. --pidfile=P __Write PID to the named file. --errorurl=U __URL to redirect to when back-ends are not found. --selfsign __ Configure the built-in HTTP daemon for HTTPS, first generating a new self-signed certificate using openssl if necessary. --httpd=X:P,\ --httppass=X,\ --pemfile=X __ Configure the built-in HTTP daemon. These options are likely to change in the near future, please pretend you didn't see them. """) MAN_CONFIG_FILES = ("""\ If you are using pagekite.py as a command-line utility, it will load its configuration from a file in your home directory. The file is named .pagekite.rc on Unix systems (including Mac OS X), or pagekite.cfg on Windows. If you are using pagekite.py as a system-daemon which starts up when your computer boots, it is generally configured to load settings from /etc/pagekite.d/*.rc (in lexicographical order). In both cases, the configuration files contain one or more of the same options as are used on the command line, with the difference that at most one option may be present on each line, and the parser is more tolerant of white-space. The leading '--' may also be omitted for readability and blank lines and lines beginning with '#' are treated as comments. NOTE: When using -o, --optfile or --optdir on the command line, it is advisable to use --clean to suppress the default configuration. """) MAN_SECURITY = ("""\ Please keep in mind, that whenever exposing a server to the public Internet, it is important to think about security. Hacked webservers are frequently abused as part of virus, spam or phishing campaigns and in some cases security breaches can compromise the entire operating system. Some advice:
           * Switch PageKite off when not using it.
           * Use the built-in access controls and SSL encryption.
           * Leave the firewall enabled unless you have good reason not to.
           * Make sure you use good passwords everywhere.
           * Static content is very hard to hack!
           * Always, always make frequent backups of any important work.
    Note that as of version 0.5, pagekite.py includes a very basic request firewall, which attempts to prevent access to phpMyAdmin and other sensitive systems. If it gets in your way, the +insecure flag or --insecure option can be used to turn it off. For more, please visit: """) MAN_LICENSE = ("""\ Copyright 2010-2015, the Beanstalks Project ehf. and Bjarni R. Einarsson. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see: """) MAN_BUGS = ("""\ Using pagekite.py as a front-end relay with the native Python SSL module may result in poor performance. Please use the pyOpenSSL wrappers instead. """) MAN_SEE_ALSO = ("""\ lapcat(1), , """) MAN_CREDITS = ("""\
    - Bjarni R. Einarsson 
        - The Beanstalks Project ehf. 
        - The Rannis Technology Development Fund 
        - Joar Wandborg 
    - Luc-Pierre Terral """) MANUAL_TOC = ( ('SH', 'Name', MAN_NAME), ('SH', 'Synopsis', MAN_SYNOPSIS), ('SH', 'Description', MAN_DESCRIPTION), ('SH', 'Basic usage', MAN_EXAMPLES), ('SH', 'Services and kites', MAN_KITES), ('SH', 'Kite configuration', MAN_KITE_EXAMPLES), ('SH', 'Flags', MAN_FLAGS), ('SS', 'Common flags', MAN_FLAGS_COMMON), ('SS', 'HTTP protocol flags', MAN_FLAGS_HTTP), ('SS', 'Built-in HTTPD flags', MAN_FLAGS_BUILTIN), ('SH', 'Options', MAN_OPTIONS), ('SS', 'Common options', MAN_OPT_COMMON), ('SS', 'Back-end options', MAN_OPT_BACKEND), ('SS', 'Front-end options', MAN_OPT_FRONTEND), ('SS', 'System options', MAN_OPT_SYSTEM), ('SH', 'Configuration files', MAN_CONFIG_FILES), ('SH', 'Security', MAN_SECURITY), ('SH', 'Bugs', MAN_BUGS), ('SH', 'See Also', MAN_SEE_ALSO), ('SH', 'Credits', MAN_CREDITS), ('SH', 'Copyright and license', MAN_LICENSE), ) HELP_SHELL = ("""\ Press ENTER to fly your kites, CTRL+C to quit or give some arguments to accomplish a more specific task. """) HELP_KITES = ("""\ """) HELP_TOC = ( ('about', 'About PageKite', MAN_DESCRIPTION), ('basics', 'Basic usage examples', MAN_EXAMPLES), ('kites', 'Services and kites', MAN_KITES), ('config', 'Adding, disabling or removing kites', MAN_KITE_EXAMPLES), ('flags', 'Service flags', '\n'.join([MAN_FLAGS, MAN_FLAGS_COMMON, MAN_FLAGS_HTTP, MAN_FLAGS_BUILTIN])), ('files', 'Where are the config files?', MAN_CONFIG_FILES), ('security', 'A few words about security.', MAN_SECURITY), ('credits', 'License and credits', '\n'.join([MAN_LICENSE, 'CREDITS:', MAN_CREDITS])), ('manual', 'The complete manual. See also: http://pagekite.net/man/', None) ) def HELP(args): name = title = text = '' if args: what = args[0].strip().lower() for name, title, text in HELP_TOC: if name == what: break if name == 'manual': text = DOC() elif not text: text = ''.join([ 'Type `help TOPIC` to to read about one of these topics:\n\n', ''.join([' %-10.10s %s\n' % (n, t) for (n, t, x) in HELP_TOC]), '\n', HELP_SHELL ]) return unindent(clean_text(text)) def clean_text(text): return re.sub('', '`', re.sub('', '', text.replace(' __', ' '))) def unindent(text): return re.sub('(?m)^ ', '', text) def MINIDOC(): return ("""\ >>> Welcome to pagekite.py v%s! %s To sign up with PageKite.net or get advanced instructions: $ pagekite.py --signup $ pagekite.py --help If you request a kite which does not exist in your configuration file, the program will offer to help you sign up with https://pagekite.net/ and create it. Pick a name, any name!\ """) % (APPVER, clean_text(MAN_EXAMPLES)) def DOC(): doc = '' for h, section, text in MANUAL_TOC: doc += '%s\n\n%s\n' % (h == 'SH' and section.upper() or ' '+section, clean_text(text)) return doc def MAN(pname=None): man = ("""\ .\\" This man page is autogenerated from the pagekite.py built-in manual. .TH PAGEKITE "1" "%s" "https://pagekite.net/" "Awesome Commands" .nh .ad l """) % ts_to_iso(time.time()).split('T')[0] for h, section, text in MANUAL_TOC: man += ('.%s %s\n\n%s\n\n' ) % (h, h == 'SH' and section.upper() or section, re.sub('\n +', '\n', '\n'+text.strip()) .replace('\n--', '\n.TP\n\\fB--') .replace('\n+', '\n.TP\n\\fB+') .replace(' __', '\\fR\n') .replace('-', '\\-') .replace('
    ', '\n.nf\n').replace('
    ', '\n.fi\n') .replace('', '\\fB').replace('', '\\fR') .replace('', '\\fI').replace('', '\\fR') .replace('', '\\fI').replace('', '\\fR') .replace('', '\\fI').replace('', '\\fR') .replace('\\fR\\fR\n', '\\fR')) if pname: man = man.replace('pagekite.py', pname) return man def MARKDOWN(pname=None): mkd = '' for h, section, text in MANUAL_TOC: if h == 'SH': h = '##' else: h = '###' mkd += ('%s %s %s\n%s\n\n' ) % (h, section, h, re.sub('(|`)', '\\1', re.sub(' +
    ([A-Z0-9])', ' \n \\1', re.sub('\n ', '\n ', re.sub('\n ', '\n', '\n'+text.strip())) .replace(' __', '
    ') .replace('\n--', '\n * --') .replace('\n+', '\n * +') .replace('', '`').replace('', '`') .replace('', '`').replace('', '`')))) if pname: mkd = mkd.replace('pagekite.py', pname) return mkd if __name__ == '__main__': import sys if '--nopy' in sys.argv: pname = 'pagekite' else: pname = None if '--man' in sys.argv: print MAN(pname) elif '--markdown' in sys.argv: print MARKDOWN(pname) elif '--minidoc' in sys.argv: print MINIDOC() else: print DOC() pagekite-0.5.8a/pagekite/common.py0000775000175000017500000001066612610153251016465 0ustar brebre00000000000000""" Constants and global program state. """ ############################################################################## LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2015, the Beanstalks Project ehf. and Bjarni Runar Einarsson This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see: """ ############################################################################## import random import time PROTOVER = '0.8' APPVER = '0.5.8a' AUTHOR = 'Bjarni Runar Einarsson, http://bre.klaki.net/' WWWHOME = 'http://pagekite.net/' LICENSE_URL = 'http://www.gnu.org/licenses/agpl.html' MAGIC_PREFIX = '/~:PageKite:~/' MAGIC_PATH = '%sv%s' % (MAGIC_PREFIX, PROTOVER) MAGIC_PATHS = (MAGIC_PATH, '/Beanstalk~Magic~Beans/0.2') MAGIC_UUID = '%x-%x-%s' % (random.randint(0, 0xfffffff), time.time(), APPVER) SERVICE_PROVIDER = 'PageKite.net' SERVICE_DOMAINS = ('pagekite.me', '302.is', 'testing.is', 'kazz.am') SERVICE_DOMAINS_SIGNUP = ('pagekite.me',) SERVICE_XMLRPC = 'http://pagekite.net/xmlrpc/' SERVICE_TOS_URL = 'https://pagekite.net/humans.txt' SERVICE_CERTS = ['b5p.us', 'frontends.b5p.us', 'pagekite.net', 'pagekite.me', 'pagekite.com', 'pagekite.org', 'testing.is', '302.is'] DEFAULT_CHARSET = 'utf-8' DEFAULT_BUFFER_MAX = 1024 AUTH_ERRORS = '255.255.255.' AUTH_ERR_USER_UNKNOWN = '.0' AUTH_ERR_INVALID = '.1' AUTH_QUOTA_MAX = '255.255.254.255' VIRTUAL_PN = 'virtual' CATCHALL_HN = 'unknown' LOOPBACK_HN = 'loopback' LOOPBACK_FE = LOOPBACK_HN + ':1' LOOPBACK_BE = LOOPBACK_HN + ':2' LOOPBACK = {'FE': LOOPBACK_FE, 'BE': LOOPBACK_BE} # Re-evaluate our choice of frontends every 45-60 minutes. FE_PING_INTERVAL = (45 * 60) + random.randint(0, 900) PING_INTERVAL = 90 PING_INTERVAL_MOBILE = 1800 PING_INTERVAL_MAX = 1800 PING_GRACE_DEFAULT = 40 PING_GRACE_MIN = 5 WEB_POLICY_DEFAULT = 'default' WEB_POLICY_PUBLIC = 'public' WEB_POLICY_PRIVATE = 'private' WEB_POLICY_OTP = 'otp' WEB_POLICIES = (WEB_POLICY_DEFAULT, WEB_POLICY_PUBLIC, WEB_POLICY_PRIVATE, WEB_POLICY_OTP) WEB_INDEX_ALL = 'all' WEB_INDEX_ON = 'on' WEB_INDEX_OFF = 'off' WEB_INDEXTYPES = (WEB_INDEX_ALL, WEB_INDEX_ON, WEB_INDEX_OFF) BE_PROTO = 0 BE_PORT = 1 BE_DOMAIN = 2 BE_BHOST = 3 BE_BPORT = 4 BE_SECRET = 5 BE_STATUS = 6 BE_STATUS_REMOTE_SSL = 0x0010000 BE_STATUS_OK = 0x0001000 BE_STATUS_ERR_DNS = 0x0000100 BE_STATUS_ERR_BE = 0x0000010 BE_STATUS_ERR_TUNNEL = 0x0000001 BE_STATUS_ERR_ANY = 0x0000fff BE_STATUS_UNKNOWN = 0 BE_STATUS_DISABLED = 0x8000000 BE_STATUS_DISABLE_ONCE = 0x4000000 BE_INACTIVE = (BE_STATUS_DISABLED, BE_STATUS_DISABLE_ONCE) BE_NONE = ['', '', None, None, None, '', BE_STATUS_UNKNOWN] DYNDNS = { 'pagekite.net': ('http://up.pagekite.net/' '?hostname=%(domain)s&myip=%(ips)s&sign=%(sign)s'), 'beanstalks.net': ('http://up.b5p.us/' '?hostname=%(domain)s&myip=%(ips)s&sign=%(sign)s'), 'dyndns.org': ('https://%(user)s:%(pass)s@members.dyndns.org' '/nic/update?wildcard=NOCHG&backmx=NOCHG' '&hostname=%(domain)s&myip=%(ip)s'), 'no-ip.com': ('https://%(user)s:%(pass)s@dynupdate.no-ip.com' '/nic/update?hostname=%(domain)s&myip=%(ip)s'), } # Create our service-domain matching regexp import re SERVICE_DOMAIN_RE = re.compile('\.(' + '|'.join(SERVICE_DOMAINS) + ')$') SERVICE_SUBDOMAIN_RE = re.compile(r'^([A-Za-z0-9_-]+\.)*[A-Za-z0-9_-]+$') class ConfigError(Exception): """This error gets thrown on configuration errors.""" class ConnectError(Exception): """This error gets thrown on connection errors.""" class BugFoundError(Exception): """Throw this anywhere a bug is detected and we want a crash.""" ##[ Ugly fugly globals ]####################################################### # The global Yamon is used for measuring internal state for monitoring gYamon = None # Status of our buffers... buffered_bytes = [0] pagekite-0.5.8a/pagekite/logging.py0000775000175000017500000000617212603560755016635 0ustar brebre00000000000000""" Logging. """ ############################################################################## LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2015, the Beanstalks Project ehf. and Bjarni Runar Einarsson This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see: """ ############################################################################## import time import sys import compat, common from compat import * from common import * syslog = compat.syslog org_stdout = sys.stdout DEBUG_IO = False LOG = [] LOG_LINE = 0 LOG_LENGTH = 300 LOG_THRESHOLD = 256 * 1024 def LogValues(values, testtime=None): global LOG, LOG_LINE, LOG_LAST_TIME now = int(testtime or time.time()) words = [('ts', '%x' % now), ('t', '%s' % ts_to_iso(now)), ('ll', '%x' % LOG_LINE)] words.extend([(kv[0], ('%s' % kv[1]).replace('\t', ' ') .replace('\r', ' ') .replace('\n', ' ') .replace('; ', ', ') .strip()) for kv in values]) wdict = dict(words) LOG_LINE += 1 LOG.append(wdict) while len(LOG) > LOG_LENGTH: LOG[0:(LOG_LENGTH/10)] = [] return (words, wdict) def LogSyslog(values, wdict=None, words=None): if values: words, wdict = LogValues(values) if 'err' in wdict: syslog.syslog(syslog.LOG_ERR, '; '.join(['='.join(x) for x in words])) elif 'debug' in wdict: syslog.syslog(syslog.LOG_DEBUG, '; '.join(['='.join(x) for x in words])) else: syslog.syslog(syslog.LOG_INFO, '; '.join(['='.join(x) for x in words])) def LogToFile(values, wdict=None, words=None): if values: words, wdict = LogValues(values) try: global LogFile LogFile.write('; '.join(['='.join(x) for x in words])) LogFile.write('\n') except (OSError, IOError): # Avoid crashing if the disk fills up or something lame like that pass def LogToMemory(values, wdict=None, words=None): if values: LogValues(values) def FlushLogMemory(): global LOG for l in LOG: Log(None, wdict=l, words=[(w, l[w]) for w in l]) def LogError(msg, parms=None): emsg = [('err', msg)] if parms: emsg.extend(parms) Log(emsg) if common.gYamon: common.gYamon.vadd('errors', 1, wrap=1000000) def LogDebug(msg, parms=None): emsg = [('debug', msg)] if parms: emsg.extend(parms) Log(emsg) def LogInfo(msg, parms=None): emsg = [('info', msg)] if parms: emsg.extend(parms) Log(emsg) def ResetLog(): global LogFile, Log, org_stdout LogFile = org_stdout Log = LogToMemory ResetLog() pagekite-0.5.8a/HTTPD-PLAN.txt0000664000175000017500000000450012603542200015167 0ustar brebre00000000000000## How the HTTPD is designed ## The pagekite.py HTTPD does: 1. XML-RPC for anything to do with controlling pagekite.py, so the interface can be shared between an AJAX Web-UI and a normal GUI, or even accessed remotely. Look into serving CORS headers to allow direct integration with pagekite.net/home/. 2. Static file server. 3. Embedded static files (for the default UI). 4. /vars.txt for monitoring. Make optional? 5. Access controls for backends. ### Sharing files ### Aside from the embedded static stuff, the served static content should be dynamically created at runtime (but persisted to disk?). Should support arbitrary http://vhost/path -> /local/fs/path mappings. ### Access controls ### Shared files and directories *could* have access controls based on: * One-off passwords * Username/password pairs * OpenID, Facebook connect, Twitter * Autogenerated obscure URLs * Guest IP address * SSL certificates * Time (files could expire) * Access counts (files could expire) * Banning/allowing bot traffic ### Access controls for backends ### We should be able to limit access to backend resources using some/all of the above methods. Granting access should/could become an interactive process. ### Presenting human readable log info ### The HTTP UI should give human readable information about visits to a site. This means adding visual cues such as grouping, pictures and colors so it is easy to see that the same person is browsing your site. It should be possible to use clever mapping of IP addresses to colors, to make networks more recognizable: For example: a.b.c.d => rgb( (a+b+d)/3, (b+c+d)/3, (c+a+d)/3) def ip2rgb(ip): # This basically gives each /24 it own hue, and then uses the final octet # to tune the brightness. We avoid pitch-black and very bright colors. o = [int(p) for p in ip.split('.')] v = 55+( (19*o[3] + o[3]) % 180) return '#%2.2x%2.2x%2.2x' % ( (v*o[0])/256, (v*o[1])/256, (v*o[2])/256 ) for ip in ['10.1.2.3', '10.0.0.100', '127.0.0.1', '255.255.255.255', '178.79.140.143', '69.164.211.158', '173.230.155.164', '192.168.1.100', '192.168.1.1', '192.168.1.200', '192.168.1.56']: print ('%s' ) % (ip2rgb(ip), ip) 10.1.2.3 => #030103 192.168.1.1 => #5a2a30 127.0.0.1 => #200020 pagekite-0.5.8a/droiddemo.py0000775000175000017500000001143312603542201015342 0ustar brebre00000000000000#!/usr/bin/python -u # # droiddemo.py, Copyright 2010-2013, The Beanstalks Project ehf. # http://beanstalks-project.net/ # # This is a proof-of-concept PageKite enabled HTTP server for Android. # It has been developed and tested in the SL4A Python environment. # DOMAIN='phone.bre.pagekite.me' SECRET='ba4e5430' SOURCE='/sdcard/sl4a/scripts/droiddemo.py' # ############################################################################# # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # ############################################################################# # import android import pagekite import os from urllib import unquote try: from urlparse import parse_qs, urlparse except Exception, e: from cgi import parse_qs from urlparse import urlparse class UiRequestHandler(pagekite.UiRequestHandler): CAMERA_PATH = '/sdcard/dcim/.thumbnails' HOME = ('\n' '\n' '\n' '

    Android photos!

    \n' '\n' '
    source code' '| kite status\n' '') def listFiles(self): mtimes = {} for item in os.listdir(self.CAMERA_PATH): iname = '%s/%s' % (self.CAMERA_PATH, item) if iname.endswith('.jpg'): mtimes[iname] = os.path.getmtime(iname) files = mtimes.keys() files.sort(lambda x,y: cmp(mtimes[x], mtimes[y])) return files def do_GET(self): (scheme, netloc, path, params, query, frag) = urlparse(self.path) p = unquote(path) if p.endswith('.jpg') and p.startswith(self.CAMERA_PATH) and ('..' not in p): try: jpgfile = open(p) self.send_response(200) self.send_header('Content-Type', 'image/jpeg') self.send_header('Content-Length', '%s' % os.path.getsize(p)) self.send_header('Cache-Control', 'max-age: 36000') self.send_header('Expires', 'Sat, 1 Jan 2011 12:00:00 GMT') self.send_header('Last-Modified', 'Wed, 1 Sep 2011 12:00:00 GMT') self.end_headers() data = jpgfile.read() while data: try: sent = self.wfile.write(data[0:15000]) data = data[15000:] except Exception: pass return except Exception, e: print '%s' % e pass if path == '/latest-image.txt': flist = self.listFiles() self.begin_headers(200, 'text/plain') self.end_headers() self.wfile.write(flist[-1]) return elif path == '/droiddemo.py': try: pyfile = open(SOURCE) self.begin_headers(200, 'text/plain') self.end_headers() self.wfile.write(pyfile.read().replace(SECRET, 'mysecret')) except IOError, e: self.begin_headers(404, 'text/plain') self.end_headers() self.wfile.write('Could not read %s: %s' % (SOURCE, e)) return elif path == '/': self.begin_headers(200, 'text/html') self.end_headers() self.wfile.write(self.HOME) return return pagekite.UiRequestHandler.do_GET(self) class DroidKite(pagekite.PageKite): def __init__(self, droid): pagekite.PageKite.__init__(self) self.droid = droid self.ui_request_handler = UiRequestHandler def Start(host, secret): ds = DroidKite(android.Android()) ds.Configure(['--defaults', '--httpd=localhost:9999', '--backend=http:%s:localhost:9999:%s' % (host, secret)]) ds.Start() Start(DOMAIN, SECRET) pagekite-0.5.8a/etc/0000775000175000017500000000000012610153761013577 5ustar brebre00000000000000pagekite-0.5.8a/etc/logrotate.d/0000775000175000017500000000000012610153761016021 5ustar brebre00000000000000pagekite-0.5.8a/etc/logrotate.d/pagekite.fedora0000664000175000017500000000030112603542201020757 0ustar brebre00000000000000/var/log/pagekite/pagekite.log { daily missingok rotate 7 postrotate [ ! -f /var/run/pagekite.pid ] || kill -HUP `cat /var/run/pagekite.pid` endscript compress notifempty nocreate } pagekite-0.5.8a/etc/logrotate.d/pagekite.debian0000664000175000017500000000032312603542201020745 0ustar brebre00000000000000/var/log/pagekite/pagekite.log { daily su daemon daemon missingok rotate 7 postrotate [ ! -f /var/run/pagekite.pid ] || kill -HUP `cat /var/run/pagekite.pid` endscript compress notifempty nocreate } pagekite-0.5.8a/etc/pagekite.d/0000775000175000017500000000000012610153761015612 5ustar brebre00000000000000pagekite-0.5.8a/etc/pagekite.d/80_sshd.rc.sample0000664000175000017500000000025212603542201020660 0ustar brebre00000000000000#################################[ This file is placed in the Public Domain. ]# # Expose the local SSH daemon service_on = raw/22:@kitename : localhost:22 : @kitesecret pagekite-0.5.8a/etc/pagekite.d/80_httpd.rc.sample0000664000175000017500000000101212603542201021035 0ustar brebre00000000000000#################################[ This file is placed in the Public Domain. ]# # Expose the local HTTPD service_on = http:@kitename : localhost:80 : @kitesecret # # Uncomment the following to globally DISABLE the request firewall. Do this # if you are sure you know what you are doing, for more details please see # # #insecure # # To disable the firewall for one kite at a time, use lines like this:: # #service_cfg = KITENAME.pagekite.me/80 : insecure : True # pagekite-0.5.8a/etc/pagekite.d/accept.acl.sample0000664000175000017500000000147712603542201021014 0ustar brebre00000000000000# This is a sample ACL file for use with --accept_acl_file. # # This is a file for use on frontend relays to restrict access. Note # that this effects both tunnels and client connections and is really # only intended for blacklisting abusive clients on a temporary basis. # # To enable these rules, rename the file and add the following to one # of the `/etc/pagekite.d/*.rc` files: # # accept_acl_file = /etc/pagekite.d/accept.acl # # For more routine access control, use `client_acl` or `tunnel_acl` # in the main configuration file (or on the command line). # # This example rejects incoming connections from localhost and allows # all others. Lines are processed in order, terminating on first match. 127.* deny Localhost is banned (IPv4) ::1 deny Localhost is banned (IPv6) # The default is to allow connections pagekite-0.5.8a/etc/pagekite.d/10_account.rc0000664000175000017500000000034412603542201020066 0ustar brebre00000000000000#################################[ This file is placed in the Public Domain. ]# # Replace the following with your account details. kitename = NAME.pagekite.me kitesecret = YOURSECRET # Delete this line! abort_not_configured pagekite-0.5.8a/etc/pagekite.d/20_frontends.rc0000664000175000017500000000076612603542201020445 0ustar brebre00000000000000#################################[ This file is placed in the Public Domain. ]# # Front-end selection # # Front-ends accept incoming requests on your behalf and forward them to # your PageKite, which in turn forwards them to the actual server. You # probably need at least one, the service defaults will choose one for you. # Use the pagekite.net service defaults. defaults # If you want to use your own, use something like: # frontend = hostname:port # or: # frontends = COUNT:dnsname:port pagekite-0.5.8a/etc/sysconfig/0000775000175000017500000000000012610153761015603 5ustar brebre00000000000000pagekite-0.5.8a/etc/sysconfig/pagekite.fedora0000664000175000017500000000015112603542201020544 0ustar brebre00000000000000OPTIONS="--optdir=/etc/pagekite.d" PK_UID=daemon PK_GID=daemon PK_LOGFILE=/var/log/pagekite/pagekite.log pagekite-0.5.8a/etc/init.d/0000775000175000017500000000000012610153761014764 5ustar brebre00000000000000pagekite-0.5.8a/etc/init.d/pagekite.fedora0000775000175000017500000000372212603542201017737 0ustar brebre00000000000000#!/bin/bash # # pagekite Startup script for the PageKite background service # # chkconfig: - 85 15 # description: PageKite makes localhost servers publicly visible. # processname: pagekite # config: /etc/pagekite.d/50_daemonize.rc # config: /etc/pagekite.d/10_account.rc # pidfile: /var/run/pagekite.pid # Source function library. . /etc/rc.d/init.d/functions if [ -f /etc/sysconfig/pagekite ]; then . /etc/sysconfig/pagekite fi # Start PageKite in the C locale by default. PK_LANG=${PK_LANG-"C"} # Path to the server binary, and short-form for messages. pk=/usr/bin/pagekite prog=pagekite pidfile=${PIDFILE-/var/run/pagekite.pid} lockfile=${LOCKFILE-/var/lock/subsys/pagekite} RETVAL=0 # Exit if package is unconfigured grep -c ^abort_not_configured /etc/pagekite.d/10_account.rc \ 2>/dev/null >/dev/null && exit 0 # Check for 0.3-style configuration check03x () { CONFFILE=/etc/pagekite/local.rc # FIXME true } start() { echo -n $"Starting $prog: " check03x || exit 1 touch $PK_LOGFILE chown -R $PK_UID:$PK_GID $(dirname $PK_LOGFILE) LANG=$PK_LANG daemon --pidfile=${pidfile} \ $pk --clean \ --runas=$PK_UID:$PK_GID \ --logfile=$PK_LOGFILE \ --pidfile=${pidfile} $OPTIONS \ --daemonize RETVAL=$? echo [ $RETVAL = 0 ] && touch ${lockfile} return $RETVAL } stop() { echo -n $"Stopping $prog: " killproc -p ${pidfile} $pk RETVAL=$? echo [ $RETVAL = 0 ] && rm -f ${lockfile} ${pidfile} } # See how we were called. case "$1" in start) start ;; stop) stop ;; status) status -p ${pidfile} $pk RETVAL=$? ;; restart|reload) stop start ;; condrestart) if [ -f ${pidfile} ] ; then stop start fi ;; *) echo $"Usage: $prog {start|stop|restart|condrestart|reload|status}" exit 1 esac exit $RETVAL pagekite-0.5.8a/etc/init.d/pagekite.debian0000775000175000017500000001151512603542201017720 0ustar brebre00000000000000#! /bin/sh ### BEGIN INIT INFO # Provides: pagekite # Required-Start: $remote_fs $syslog $named # Required-Stop: $remote_fs $syslog $named # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: PageKite system service # Description: PageKite makes localhost servers publicly visible. ### END INIT INFO # Authors: Bjarni R. Einarsson # Hrafnkell Eiriksson # Do NOT "set -e" # PATH should only include /usr/* if it runs after the mountnfs.sh script PATH=/sbin:/usr/sbin:/bin:/usr/bin DESC="PageKite system service" NAME=pagekite RUNAS=daemon:daemon DAEMON=/usr/bin/$NAME WRAPPER=/usr/bin/daemon PIDFILE=/var/run/$NAME.pid LOGFILE=/var/log/$NAME/$NAME.log WRAPPER_PIDFILE=$PIDFILE.wrapper WRAPPER_ARGS="--noconfig --unsafe --respawn --delay=60 --name=$NAME" DAEMON_ARGS="--clean \ --runas=$RUNAS \ --logfile=$LOGFILE \ --optdir=/etc/$NAME.d" SCRIPTNAME=/etc/init.d/$NAME # Exit if the package is not installed [ -x "$DAEMON" ] || exit 0 # Exit if package is unconfigured grep -c ^abort_not_configured /etc/pagekite.d/10_account.rc \ 2>/dev/null >/dev/null && exit 0 # Read configuration variable file if it is present [ -r /etc/default/$NAME ] && . /etc/default/$NAME # Load the VERBOSE setting and other rcS variables . /lib/init/vars.sh # Define LSB log_* functions. # Depend on lsb-base (>= 3.0-6) to ensure that this file is present. . /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 touch $LOGFILE chown $RUNAS $(dirname $LOGFILE) $LOGFILE if [ -x $WRAPPER ]; then start-stop-daemon --quiet --pidfile $WRAPPER_PIDFILE --test --start \ --startas $WRAPPER > /dev/null \ || return 1 start-stop-daemon \ --quiet --pidfile $WRAPPER_PIDFILE --start --startas $WRAPPER -- \ --pidfile $WRAPPER_PIDFILE $WRAPPER_ARGS -- $DAEMON \ --pidfile $PIDFILE $DAEMON_ARGS --noloop \ || return 2 else start-stop-daemon --quiet --pidfile $PIDFILE --test --start \ --startas $DAEMON > /dev/null \ || return 1 start-stop-daemon \ --quiet --pidfile $PIDFILE --start --startas $DAEMON -- \ --pidfile $PIDFILE --daemonize $DAEMON_ARGS \ || return 2 fi # Add code here, if necessary, that waits for the process to be ready # to handle requests from services started subsequently which depend # on this one. As a last resort, sleep for some time. } # # 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 if [ -e $WRAPPER_PIDFILE ]; then start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 \ --pidfile $WRAPPER_PIDFILE else WRAPPERS=$(ps axw |grep $WRAPPER |grep $DAEMON \ |grep $LOGFILE |cut -b1-5) if [ "$WRAPPERS" = "" ]; then start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 \ --pidfile $PIDFILE else kill $WRAPPERS start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 \ --pidfile $PIDFILE --oknodo fi fi RETVAL="$?" [ "$RETVAL" = 2 ] && return 2 # Many daemons don't delete their pidfiles when they exit. rm -f $PIDFILE $WRAPPER_PIDFILE return "$RETVAL" } case "$1" in start) [ "$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 $? ;; #reload|force-reload) # # If do_reload() is not implemented then leave this commented out # and leave 'force-reload' as an alias for 'restart'. # #log_daemon_msg "Reloading $DESC" "$NAME" #do_reload #log_end_msg $? #;; restart|force-reload) # # If the "reload" option is implemented then remove the # 'force-reload' alias # 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: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2 exit 3 ;; esac : pagekite-0.5.8a/UI.txt0000664000175000017500000000561712603542200014103 0ustar brebre00000000000000## UI 2.0 ## UNFINISHED IN GUI: * Paste to Web * Share file/folder behavior on Windows (.lnk instead of symlinks) * Sharing dialog: * Cropper * Title * Password * Description * Kite list: * Toggling should disable kite * Add / edit kites: need UI * PageKite Log display * Verbose log toggle NEEDS POLISH: * Config File editor BUILT IN WEBSERVER WORK: * Javier's everything! ## Add / Edit UI / Main window ## Use cases: - Want to create a new kite - Want to add services to a kite - Want to edit a kite service - Want to remove a kite service - Want to stop sharing something Kite view: KITE SERVICE SERVERS b.pagekite.me www (port 80) localhost:80 [up] [edit] [del] www (default) built-in [down] [edit] [del] ssh (HTTP proxied localhost:22 [down] [edit] [del] [new service] [new kite] Sharing view: KITE URL PATH LOCAL PATH EXPIRES b.pagekite.me / /home/bre/PageKite never [open] [del] ## UI! ## Indicator icon: down / connecting / flying / traffic Main menu: 15 day trial Buy buy buy -------------------- Pagekites: - bre.pagekite.me > [x] WWW (PageKite) - www.fnord.com Open in Browser - b.pagekite.me --------------------------- [ ] Secure Shell (SSH) [ ] Share Desktop (VNC/RDP) --------------------------- New Kite Settings Delete Kite -------------------- Connections ... > Portals: - SSH: bre.pagekite.me - VNC: bre.pagekite.me > Open - RDP: askja.ok.is Remove - 3349: magicserv:2341 Add Portal -------------------- Hosts: - bjarni - fooxbar Add Host -------------------- [ ] Lapcat HTTP Proxy (localhost:7669) -------------------- Shared items: - /foo/bar/baz > Open in Browser - Pasted Text Copy Link - Pasted Image Stop Sharing Paste to Web Share Folder or File -------------------- Advanced > View Log Config File Connect ... Help > About PageKite On-line support Quit ## When Sharing ## 1. One of: - Create screenshot - File/Directory chooser - Grab data from clipboard 2. DISPLAY A PREVIEW If screenshot, allow cropping If multiple kites, choose from a dropdown Choose expiration from a dropdown ADVANCED: x Make URL private x Require password: [ ... ]