pax_global_header00006660000000000000000000000064137405656430014527gustar00rootroot0000000000000052 comment=e8ee3dbbde3bff35e3183c03fcc3c0adbfa3d068 PyPagekite-1.5.2.201011/000077500000000000000000000000001374056564300143215ustar00rootroot00000000000000PyPagekite-1.5.2.201011/.gitignore000066400000000000000000000003101374056564300163030ustar00rootroot00000000000000*.pyc *.pyo *.orig *-tmp.py build/ dist/ pagekite.egg-info doc/pagekite.1 .pybuild .header .SELF sockschain scripts/breeder.py deb/*.log deb/*.debhelper deb/*.substvars deb/files deb/pagekite/ debian PyPagekite-1.5.2.201011/COPYING000066400000000000000000001033301374056564300153540ustar00rootroot00000000000000 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 . PyPagekite-1.5.2.201011/HTTPD-PLAN.txt000066400000000000000000000045001374056564300164740ustar00rootroot00000000000000## 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 PyPagekite-1.5.2.201011/MANIFEST.in000066400000000000000000000004301374056564300160540ustar00rootroot00000000000000include pk *.py *.md *.txt *.sample *.1 Makefile COPYING recursive-include pagekite *.py recursive-include scripts * recursive-include doc * recursive-include rpm * recursive-include gui * recursive-include etc * exclude *.pyc exclude socks.py exclude sockschain.py exclude .SELF PyPagekite-1.5.2.201011/Makefile000066400000000000000000000121201374056564300157550ustar00rootroot00000000000000# Makefile for building combined pagekite.py files. export PYTHONPATH := . BREED_PAGEKITE = /usr/lib/python2.7/dist-packages/six.py \ pagekite/__init__.py \ pagekite/common.py \ pagekite/compat.py \ pagekite/logging.py \ pagekite/manual.py \ pagekite/proto/__init__.py \ pagekite/proto/ws_abnf.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 defaults.cfg @./scripts/breeder.py --compress --header .header \ defaults.cfg 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 untested: pagekite tools doc/MANPAGE.md dev .header defaults.cfg @./scripts/breeder.py --compress --header .header \ defaults.cfg sockschain $(BREED_PAGEKITE) \ pagekite/__main__.py \ >pagekite-tmp.py @chmod +x pagekite-tmp.py @mv pagekite-tmp.py dist/pagekite-`python setup.py --version`.py @ls -l dist/pagekite-*.py gtk: pagekite tools dev .header defaults.cfg @./scripts/breeder.py --gtk-images --compress --header .header \ defaults.cfg 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 defaults.cfg @./scripts/breeder.py --compress --header .header \ defaults.cfg 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 @python -m pagekite.manual --nopy --markdown >doc/MANPAGE.md doc/pagekite.1: pagekite pagekite/manual.py @python -m pagekite.manual --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 \ --requires=python-six VERSION=`python setup.py --version` DEB_VERSION=`head -n1 debian/changelog | sed -e "s+.*(\(.*\)).*+\1+"` .debprep: @ln -sf deb debian if [ "x$(VERSION)" != "x$(DEB_VERSION)" ] ; \ then \ dch --maintmaint --newversion $(VERSION) --urgency=low \ --distribution=unstable "New release." ; \ fi .targz: @python setup.py sdist .deb: .debprep @debuild -i -us -uc @mv ../pagekite_*.deb dist/ .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: [ -e debian ] && debuild clean || true @rm -vf sockschain *.py[co] */*.py[co] */*/*.py[co] scripts/breeder.py .SELF @rm -vf .appver pagekite-tmp.py MANIFEST setup.cfg pagekite_gtk.py @rm -vrf debian *.egg-info .header doc/pagekite.1 build/ PyPagekite-1.5.2.201011/README.md000066400000000000000000000010361374056564300156000ustar00rootroot00000000000000# 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: PyPagekite-1.5.2.201011/TODO.md000066400000000000000000000056511374056564300154170ustar00rootroot00000000000000# 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") PyPagekite-1.5.2.201011/UI.txt000066400000000000000000000056171374056564300154100ustar00rootroot00000000000000## 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: [ ... ] PyPagekite-1.5.2.201011/__main__.py000077500000000000000000000010521374056564300164140ustar00rootroot00000000000000#!/usr/bin/env python from __future__ import absolute_import 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) PyPagekite-1.5.2.201011/contrib/000077500000000000000000000000001374056564300157615ustar00rootroot00000000000000PyPagekite-1.5.2.201011/contrib/.gitignore000066400000000000000000000000001374056564300177370ustar00rootroot00000000000000PyPagekite-1.5.2.201011/contrib/lua/000077500000000000000000000000001374056564300165425ustar00rootroot00000000000000PyPagekite-1.5.2.201011/contrib/lua/README000066400000000000000000000017701374056564300174270ustar00rootroot00000000000000pagekite.lua version 0.3, 4/8/2011 by Stelios Mersinas (steliosm@gmail.com) This is a backend client for PageKite protocol/service implemented in Lua targeting at small embedded systems that are able to run Lua. INSTALLATION NOTES You need to have recent Lua version installed along with the Lua-socket module. Extract the archive which will create a pagekite directory. Rename the dot-config file to .config. Edit the .config file to suit your setup (private PageKite front-end or public Pagekite front-end). Run start.sh and you should be OK. a log file named log will be crated to display the status of the client, mainly crashes and restarts. You can set the option debug = 1 in the config file to have a more detailed output about what the client is doing. NEW FEATURES v0.3: Supports tunnel status check using Ping requests. The shell script will restart the service in case of a segfault (had a few during developing and testing the client). BUG FIXES Did you find a bug? E-mail me! PyPagekite-1.5.2.201011/contrib/lua/README.md000066400000000000000000000003221374056564300200160ustar00rootroot00000000000000## PageKite in LUA ## This is a basic implementation of a PageKite back-end in the LUA programming language. It was contributed by Stelios Mersinas in July 2011 and is still a work in progress at this time. PyPagekite-1.5.2.201011/contrib/lua/dot-config.sample000066400000000000000000000005421374056564300217770ustar00rootroot00000000000000 -- -- Pagekite Back-end configuration file -- -- Front-End server pk_site = '' pk_server = '' pk_server_port = pk_server_token = '' -- Web Server proxy_web = "127.0.0.1" proxy_web_port = 80 -- Varius settings debug = 0 server_reconnect = 1 PyPagekite-1.5.2.201011/contrib/lua/lib/000077500000000000000000000000001374056564300173105ustar00rootroot00000000000000PyPagekite-1.5.2.201011/contrib/lua/lib/shalib.lua000066400000000000000000000774231374056564300212720ustar00rootroot00000000000000-- -- SHA-1 secure hash computation, and HMAC-SHA1 signature computation, -- in pure Lua (tested on Lua 5.1) -- -- Latest version always at: http://regex.info/blog/lua/sha1 -- -- Copyright 2009 Jeffrey Friedl -- jfriedl@yahoo.com -- http://regex.info/blog/ -- -- -- Version 1 [May 28, 2009] -- -- -- Lua is a pathetic, horrid, turd of a language. Not only doesn't it have -- bitwise integer operators like OR and AND, it doesn't even have integers -- (and those, relatively speaking, are its good points). Yet, this -- implements the SHA-1 digest hash in pure Lua. While coding it, I felt as -- if I were chiseling NAND gates out of rough blocks of silicon. Those not -- already familiar with this woeful language may, upon seeing this code, -- throw up in their own mouth. -- -- It's not super fast.... a 10k-byte message takes about 2 seconds on a -- circa-2008 mid-level server, but it should be plenty adequate for short -- messages, such as is often needed during authentication handshaking. -- -- Algorithm: http://www.itl.nist.gov/fipspubs/fip180-1.htm -- -- This file creates four entries in the global namespace: -- -- local hash_as_hex = sha1(message) -- returns a hex string -- local hash_as_data = sha1_binary(message) -- returns raw bytes -- -- local hmac_as_hex = hmac_sha1(key, message) -- hex string -- local hmac_as_data = hmac_sha1_binary(key, message) -- raw bytes -- -- Pass sha1() a string, and it returns a hash as a 40-character hex string. -- For example, the call -- -- local hash = sha1 "http://regex.info/blog/" -- -- puts the 40-character string -- -- "7f103bf600de51dfe91062300c14738b32725db5" -- -- into the variable 'hash' -- -- Pass sha1_hmac() a key and a message, and it returns the signature as a -- 40-byte hex string. -- -- -- The two "_binary" versions do the same, but return the 20-byte string of raw data -- that the 40-byte hex strings represent. -- ------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------ -- -- Return a W32 object for the number zero -- local function ZERO() return { false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, } end local hex_to_bits = { ["0"] = { false, false, false, false }, ["1"] = { false, false, false, true }, ["2"] = { false, false, true, false }, ["3"] = { false, false, true, true }, ["4"] = { false, true, false, false }, ["5"] = { false, true, false, true }, ["6"] = { false, true, true, false }, ["7"] = { false, true, true, true }, ["8"] = { true, false, false, false }, ["9"] = { true, false, false, true }, ["A"] = { true, false, true, false }, ["B"] = { true, false, true, true }, ["C"] = { true, true, false, false }, ["D"] = { true, true, false, true }, ["E"] = { true, true, true, false }, ["F"] = { true, true, true, true }, ["a"] = { true, false, true, false }, ["b"] = { true, false, true, true }, ["c"] = { true, true, false, false }, ["d"] = { true, true, false, true }, ["e"] = { true, true, true, false }, ["f"] = { true, true, true, true }, } -- -- Given a string of 8 hex digits, return a W32 object representing that number -- local function from_hex(hex) assert(type(hex) == 'string') assert(hex:match('^[0123456789abcdefABCDEF]+$')) assert(#hex == 8) local W32 = { } for letter in hex:gmatch('.') do local b = hex_to_bits[letter] assert(b) table.insert(W32, 1, b[1]) table.insert(W32, 1, b[2]) table.insert(W32, 1, b[3]) table.insert(W32, 1, b[4]) end return W32 end local function COPY(old) local W32 = { } for k,v in pairs(old) do W32[k] = v end return W32 end local function ADD(first, ...) local a = COPY(first) local C, b, sum for v = 1, select('#', ...) do b = select(v, ...) C = 0 for i = 1, #a do sum = (a[i] and 1 or 0) + (b[i] and 1 or 0) + C if sum == 0 then a[i] = false C = 0 elseif sum == 1 then a[i] = true C = 0 elseif sum == 2 then a[i] = false C = 1 else a[i] = true C = 1 end end -- we drop any ending carry end return a end local function XOR(first, ...) local a = COPY(first) local b for v = 1, select('#', ...) do b = select(v, ...) for i = 1, #a do a[i] = a[i] ~= b[i] end end return a end local function AND(a, b) local c = ZERO() for i = 1, #a do -- only need to set true bits; other bits remain false if a[i] and b[i] then c[i] = true end end return c end local function OR(a, b) local c = ZERO() for i = 1, #a do -- only need to set true bits; other bits remain false if a[i] or b[i] then c[i] = true end end return c end local function OR3(a, b, c) local d = ZERO() for i = 1, #a do -- only need to set true bits; other bits remain false if a[i] or b[i] or c[i] then d[i] = true end end return d end local function NOT(a) local b = ZERO() for i = 1, #a do -- only need to set true bits; other bits remain false if not a[i] then b[i] = true end end return b end local function ROTATE(bits, a) local b = COPY(a) while bits > 0 do bits = bits - 1 table.insert(b, 1, table.remove(b)) end return b end local binary_to_hex = { ["0000"] = "0", ["0001"] = "1", ["0010"] = "2", ["0011"] = "3", ["0100"] = "4", ["0101"] = "5", ["0110"] = "6", ["0111"] = "7", ["1000"] = "8", ["1001"] = "9", ["1010"] = "a", ["1011"] = "b", ["1100"] = "c", ["1101"] = "d", ["1110"] = "e", ["1111"] = "f", } function asHEX(a) local hex = "" local i = 1 while i < #a do local binary = (a[i + 3] and '1' or '0') .. (a[i + 2] and '1' or '0') .. (a[i + 1] and '1' or '0') .. (a[i + 0] and '1' or '0') hex = binary_to_hex[binary] .. hex i = i + 4 end return hex end local x67452301 = from_hex("67452301") local xEFCDAB89 = from_hex("EFCDAB89") local x98BADCFE = from_hex("98BADCFE") local x10325476 = from_hex("10325476") local xC3D2E1F0 = from_hex("C3D2E1F0") local x5A827999 = from_hex("5A827999") local x6ED9EBA1 = from_hex("6ED9EBA1") local x8F1BBCDC = from_hex("8F1BBCDC") local xCA62C1D6 = from_hex("CA62C1D6") function sha1(msg) assert(type(msg) == 'string') assert(#msg < 0x7FFFFFFF) -- have no idea what would happen if it were large local H0 = x67452301 local H1 = xEFCDAB89 local H2 = x98BADCFE local H3 = x10325476 local H4 = xC3D2E1F0 local msg_len_in_bits = #msg * 8 local first_append = string.char(0x80) -- append a '1' bit plus seven '0' bits local non_zero_message_bytes = #msg +1 +8 -- the +1 is the appended bit 1, the +8 are for the final appended length local current_mod = non_zero_message_bytes % 64 local second_append = "" if current_mod ~= 0 then second_append = string.rep(string.char(0), 64 - current_mod) end -- now to append the length as a 64-bit number. local B1, R1 = math.modf(msg_len_in_bits / 0x01000000) local B2, R2 = math.modf( 0x01000000 * R1 / 0x00010000) local B3, R3 = math.modf( 0x00010000 * R2 / 0x00000100) local B4 = 0x00000100 * R3 local L64 = string.char( 0) .. string.char( 0) .. string.char( 0) .. string.char( 0) -- high 32 bits .. string.char(B1) .. string.char(B2) .. string.char(B3) .. string.char(B4) -- low 32 bits msg = msg .. first_append .. second_append .. L64 assert(#msg % 64 == 0) --local fd = io.open("/tmp/msg", "wb") --fd:write(msg) --fd:close() local chunks = #msg / 64 local W = { } local start, A, B, C, D, E, f, K, TEMP local chunk = 0 while chunk < chunks do -- -- break chunk up into W[0] through W[15] -- start = chunk * 64 + 1 chunk = chunk + 1 for t = 0, 15 do W[t] = from_hex(string.format("%02x%02x%02x%02x", msg:byte(start, start + 3))) start = start + 4 end -- -- build W[16] through W[79] -- for t = 16, 79 do -- For t = 16 to 79 let Wt = S1(Wt-3 XOR Wt-8 XOR Wt-14 XOR Wt-16). W[t] = ROTATE(1, XOR(W[t-3], W[t-8], W[t-14], W[t-16])) end A = H0 B = H1 C = H2 D = H3 E = H4 for t = 0, 79 do if t <= 19 then -- (B AND C) OR ((NOT B) AND D) f = OR(AND(B, C), AND(NOT(B), D)) K = x5A827999 elseif t <= 39 then -- B XOR C XOR D f = XOR(B, C, D) K = x6ED9EBA1 elseif t <= 59 then -- (B AND C) OR (B AND D) OR (C AND D f = OR3(AND(B, C), AND(B, D), AND(C, D)) K = x8F1BBCDC else -- B XOR C XOR D f = XOR(B, C, D) K = xCA62C1D6 end -- TEMP = S5(A) + ft(B,C,D) + E + Wt + Kt; TEMP = ADD(ROTATE(5, A), f, E, W[t], K) --E = D;   D = C;    C = S30(B);   B = A;   A = TEMP; E = D D = C C = ROTATE(30, B) B = A A = TEMP --printf("t = %2d: %s %s %s %s %s", t, A:HEX(), B:HEX(), C:HEX(), D:HEX(), E:HEX()) end -- Let H0 = H0 + A, H1 = H1 + B, H2 = H2 + C, H3 = H3 + D, H4 = H4 + E. H0 = ADD(H0, A) H1 = ADD(H1, B) H2 = ADD(H2, C) H3 = ADD(H3, D) H4 = ADD(H4, E) end return asHEX(H0) .. asHEX(H1) .. asHEX(H2) .. asHEX(H3) .. asHEX(H4) end local function hex_to_binary(hex) return hex:gsub('..', function(hexval) return string.char(tonumber(hexval, 16)) end) end function sha1_binary(msg) return hex_to_binary(sha1(msg)) end local xor_with_0x5c = { [string.char( 0)] = string.char( 92), [string.char( 1)] = string.char( 93), [string.char( 2)] = string.char( 94), [string.char( 3)] = string.char( 95), [string.char( 4)] = string.char( 88), [string.char( 5)] = string.char( 89), [string.char( 6)] = string.char( 90), [string.char( 7)] = string.char( 91), [string.char( 8)] = string.char( 84), [string.char( 9)] = string.char( 85), [string.char( 10)] = string.char( 86), [string.char( 11)] = string.char( 87), [string.char( 12)] = string.char( 80), [string.char( 13)] = string.char( 81), [string.char( 14)] = string.char( 82), [string.char( 15)] = string.char( 83), [string.char( 16)] = string.char( 76), [string.char( 17)] = string.char( 77), [string.char( 18)] = string.char( 78), [string.char( 19)] = string.char( 79), [string.char( 20)] = string.char( 72), [string.char( 21)] = string.char( 73), [string.char( 22)] = string.char( 74), [string.char( 23)] = string.char( 75), [string.char( 24)] = string.char( 68), [string.char( 25)] = string.char( 69), [string.char( 26)] = string.char( 70), [string.char( 27)] = string.char( 71), [string.char( 28)] = string.char( 64), [string.char( 29)] = string.char( 65), [string.char( 30)] = string.char( 66), [string.char( 31)] = string.char( 67), [string.char( 32)] = string.char(124), [string.char( 33)] = string.char(125), [string.char( 34)] = string.char(126), [string.char( 35)] = string.char(127), [string.char( 36)] = string.char(120), [string.char( 37)] = string.char(121), [string.char( 38)] = string.char(122), [string.char( 39)] = string.char(123), [string.char( 40)] = string.char(116), [string.char( 41)] = string.char(117), [string.char( 42)] = string.char(118), [string.char( 43)] = string.char(119), [string.char( 44)] = string.char(112), [string.char( 45)] = string.char(113), [string.char( 46)] = string.char(114), [string.char( 47)] = string.char(115), [string.char( 48)] = string.char(108), [string.char( 49)] = string.char(109), [string.char( 50)] = string.char(110), [string.char( 51)] = string.char(111), [string.char( 52)] = string.char(104), [string.char( 53)] = string.char(105), [string.char( 54)] = string.char(106), [string.char( 55)] = string.char(107), [string.char( 56)] = string.char(100), [string.char( 57)] = string.char(101), [string.char( 58)] = string.char(102), [string.char( 59)] = string.char(103), [string.char( 60)] = string.char( 96), [string.char( 61)] = string.char( 97), [string.char( 62)] = string.char( 98), [string.char( 63)] = string.char( 99), [string.char( 64)] = string.char( 28), [string.char( 65)] = string.char( 29), [string.char( 66)] = string.char( 30), [string.char( 67)] = string.char( 31), [string.char( 68)] = string.char( 24), [string.char( 69)] = string.char( 25), [string.char( 70)] = string.char( 26), [string.char( 71)] = string.char( 27), [string.char( 72)] = string.char( 20), [string.char( 73)] = string.char( 21), [string.char( 74)] = string.char( 22), [string.char( 75)] = string.char( 23), [string.char( 76)] = string.char( 16), [string.char( 77)] = string.char( 17), [string.char( 78)] = string.char( 18), [string.char( 79)] = string.char( 19), [string.char( 80)] = string.char( 12), [string.char( 81)] = string.char( 13), [string.char( 82)] = string.char( 14), [string.char( 83)] = string.char( 15), [string.char( 84)] = string.char( 8), [string.char( 85)] = string.char( 9), [string.char( 86)] = string.char( 10), [string.char( 87)] = string.char( 11), [string.char( 88)] = string.char( 4), [string.char( 89)] = string.char( 5), [string.char( 90)] = string.char( 6), [string.char( 91)] = string.char( 7), [string.char( 92)] = string.char( 0), [string.char( 93)] = string.char( 1), [string.char( 94)] = string.char( 2), [string.char( 95)] = string.char( 3), [string.char( 96)] = string.char( 60), [string.char( 97)] = string.char( 61), [string.char( 98)] = string.char( 62), [string.char( 99)] = string.char( 63), [string.char(100)] = string.char( 56), [string.char(101)] = string.char( 57), [string.char(102)] = string.char( 58), [string.char(103)] = string.char( 59), [string.char(104)] = string.char( 52), [string.char(105)] = string.char( 53), [string.char(106)] = string.char( 54), [string.char(107)] = string.char( 55), [string.char(108)] = string.char( 48), [string.char(109)] = string.char( 49), [string.char(110)] = string.char( 50), [string.char(111)] = string.char( 51), [string.char(112)] = string.char( 44), [string.char(113)] = string.char( 45), [string.char(114)] = string.char( 46), [string.char(115)] = string.char( 47), [string.char(116)] = string.char( 40), [string.char(117)] = string.char( 41), [string.char(118)] = string.char( 42), [string.char(119)] = string.char( 43), [string.char(120)] = string.char( 36), [string.char(121)] = string.char( 37), [string.char(122)] = string.char( 38), [string.char(123)] = string.char( 39), [string.char(124)] = string.char( 32), [string.char(125)] = string.char( 33), [string.char(126)] = string.char( 34), [string.char(127)] = string.char( 35), [string.char(128)] = string.char(220), [string.char(129)] = string.char(221), [string.char(130)] = string.char(222), [string.char(131)] = string.char(223), [string.char(132)] = string.char(216), [string.char(133)] = string.char(217), [string.char(134)] = string.char(218), [string.char(135)] = string.char(219), [string.char(136)] = string.char(212), [string.char(137)] = string.char(213), [string.char(138)] = string.char(214), [string.char(139)] = string.char(215), [string.char(140)] = string.char(208), [string.char(141)] = string.char(209), [string.char(142)] = string.char(210), [string.char(143)] = string.char(211), [string.char(144)] = string.char(204), [string.char(145)] = string.char(205), [string.char(146)] = string.char(206), [string.char(147)] = string.char(207), [string.char(148)] = string.char(200), [string.char(149)] = string.char(201), [string.char(150)] = string.char(202), [string.char(151)] = string.char(203), [string.char(152)] = string.char(196), [string.char(153)] = string.char(197), [string.char(154)] = string.char(198), [string.char(155)] = string.char(199), [string.char(156)] = string.char(192), [string.char(157)] = string.char(193), [string.char(158)] = string.char(194), [string.char(159)] = string.char(195), [string.char(160)] = string.char(252), [string.char(161)] = string.char(253), [string.char(162)] = string.char(254), [string.char(163)] = string.char(255), [string.char(164)] = string.char(248), [string.char(165)] = string.char(249), [string.char(166)] = string.char(250), [string.char(167)] = string.char(251), [string.char(168)] = string.char(244), [string.char(169)] = string.char(245), [string.char(170)] = string.char(246), [string.char(171)] = string.char(247), [string.char(172)] = string.char(240), [string.char(173)] = string.char(241), [string.char(174)] = string.char(242), [string.char(175)] = string.char(243), [string.char(176)] = string.char(236), [string.char(177)] = string.char(237), [string.char(178)] = string.char(238), [string.char(179)] = string.char(239), [string.char(180)] = string.char(232), [string.char(181)] = string.char(233), [string.char(182)] = string.char(234), [string.char(183)] = string.char(235), [string.char(184)] = string.char(228), [string.char(185)] = string.char(229), [string.char(186)] = string.char(230), [string.char(187)] = string.char(231), [string.char(188)] = string.char(224), [string.char(189)] = string.char(225), [string.char(190)] = string.char(226), [string.char(191)] = string.char(227), [string.char(192)] = string.char(156), [string.char(193)] = string.char(157), [string.char(194)] = string.char(158), [string.char(195)] = string.char(159), [string.char(196)] = string.char(152), [string.char(197)] = string.char(153), [string.char(198)] = string.char(154), [string.char(199)] = string.char(155), [string.char(200)] = string.char(148), [string.char(201)] = string.char(149), [string.char(202)] = string.char(150), [string.char(203)] = string.char(151), [string.char(204)] = string.char(144), [string.char(205)] = string.char(145), [string.char(206)] = string.char(146), [string.char(207)] = string.char(147), [string.char(208)] = string.char(140), [string.char(209)] = string.char(141), [string.char(210)] = string.char(142), [string.char(211)] = string.char(143), [string.char(212)] = string.char(136), [string.char(213)] = string.char(137), [string.char(214)] = string.char(138), [string.char(215)] = string.char(139), [string.char(216)] = string.char(132), [string.char(217)] = string.char(133), [string.char(218)] = string.char(134), [string.char(219)] = string.char(135), [string.char(220)] = string.char(128), [string.char(221)] = string.char(129), [string.char(222)] = string.char(130), [string.char(223)] = string.char(131), [string.char(224)] = string.char(188), [string.char(225)] = string.char(189), [string.char(226)] = string.char(190), [string.char(227)] = string.char(191), [string.char(228)] = string.char(184), [string.char(229)] = string.char(185), [string.char(230)] = string.char(186), [string.char(231)] = string.char(187), [string.char(232)] = string.char(180), [string.char(233)] = string.char(181), [string.char(234)] = string.char(182), [string.char(235)] = string.char(183), [string.char(236)] = string.char(176), [string.char(237)] = string.char(177), [string.char(238)] = string.char(178), [string.char(239)] = string.char(179), [string.char(240)] = string.char(172), [string.char(241)] = string.char(173), [string.char(242)] = string.char(174), [string.char(243)] = string.char(175), [string.char(244)] = string.char(168), [string.char(245)] = string.char(169), [string.char(246)] = string.char(170), [string.char(247)] = string.char(171), [string.char(248)] = string.char(164), [string.char(249)] = string.char(165), [string.char(250)] = string.char(166), [string.char(251)] = string.char(167), [string.char(252)] = string.char(160), [string.char(253)] = string.char(161), [string.char(254)] = string.char(162), [string.char(255)] = string.char(163), } local xor_with_0x36 = { [string.char( 0)] = string.char( 54), [string.char( 1)] = string.char( 55), [string.char( 2)] = string.char( 52), [string.char( 3)] = string.char( 53), [string.char( 4)] = string.char( 50), [string.char( 5)] = string.char( 51), [string.char( 6)] = string.char( 48), [string.char( 7)] = string.char( 49), [string.char( 8)] = string.char( 62), [string.char( 9)] = string.char( 63), [string.char( 10)] = string.char( 60), [string.char( 11)] = string.char( 61), [string.char( 12)] = string.char( 58), [string.char( 13)] = string.char( 59), [string.char( 14)] = string.char( 56), [string.char( 15)] = string.char( 57), [string.char( 16)] = string.char( 38), [string.char( 17)] = string.char( 39), [string.char( 18)] = string.char( 36), [string.char( 19)] = string.char( 37), [string.char( 20)] = string.char( 34), [string.char( 21)] = string.char( 35), [string.char( 22)] = string.char( 32), [string.char( 23)] = string.char( 33), [string.char( 24)] = string.char( 46), [string.char( 25)] = string.char( 47), [string.char( 26)] = string.char( 44), [string.char( 27)] = string.char( 45), [string.char( 28)] = string.char( 42), [string.char( 29)] = string.char( 43), [string.char( 30)] = string.char( 40), [string.char( 31)] = string.char( 41), [string.char( 32)] = string.char( 22), [string.char( 33)] = string.char( 23), [string.char( 34)] = string.char( 20), [string.char( 35)] = string.char( 21), [string.char( 36)] = string.char( 18), [string.char( 37)] = string.char( 19), [string.char( 38)] = string.char( 16), [string.char( 39)] = string.char( 17), [string.char( 40)] = string.char( 30), [string.char( 41)] = string.char( 31), [string.char( 42)] = string.char( 28), [string.char( 43)] = string.char( 29), [string.char( 44)] = string.char( 26), [string.char( 45)] = string.char( 27), [string.char( 46)] = string.char( 24), [string.char( 47)] = string.char( 25), [string.char( 48)] = string.char( 6), [string.char( 49)] = string.char( 7), [string.char( 50)] = string.char( 4), [string.char( 51)] = string.char( 5), [string.char( 52)] = string.char( 2), [string.char( 53)] = string.char( 3), [string.char( 54)] = string.char( 0), [string.char( 55)] = string.char( 1), [string.char( 56)] = string.char( 14), [string.char( 57)] = string.char( 15), [string.char( 58)] = string.char( 12), [string.char( 59)] = string.char( 13), [string.char( 60)] = string.char( 10), [string.char( 61)] = string.char( 11), [string.char( 62)] = string.char( 8), [string.char( 63)] = string.char( 9), [string.char( 64)] = string.char(118), [string.char( 65)] = string.char(119), [string.char( 66)] = string.char(116), [string.char( 67)] = string.char(117), [string.char( 68)] = string.char(114), [string.char( 69)] = string.char(115), [string.char( 70)] = string.char(112), [string.char( 71)] = string.char(113), [string.char( 72)] = string.char(126), [string.char( 73)] = string.char(127), [string.char( 74)] = string.char(124), [string.char( 75)] = string.char(125), [string.char( 76)] = string.char(122), [string.char( 77)] = string.char(123), [string.char( 78)] = string.char(120), [string.char( 79)] = string.char(121), [string.char( 80)] = string.char(102), [string.char( 81)] = string.char(103), [string.char( 82)] = string.char(100), [string.char( 83)] = string.char(101), [string.char( 84)] = string.char( 98), [string.char( 85)] = string.char( 99), [string.char( 86)] = string.char( 96), [string.char( 87)] = string.char( 97), [string.char( 88)] = string.char(110), [string.char( 89)] = string.char(111), [string.char( 90)] = string.char(108), [string.char( 91)] = string.char(109), [string.char( 92)] = string.char(106), [string.char( 93)] = string.char(107), [string.char( 94)] = string.char(104), [string.char( 95)] = string.char(105), [string.char( 96)] = string.char( 86), [string.char( 97)] = string.char( 87), [string.char( 98)] = string.char( 84), [string.char( 99)] = string.char( 85), [string.char(100)] = string.char( 82), [string.char(101)] = string.char( 83), [string.char(102)] = string.char( 80), [string.char(103)] = string.char( 81), [string.char(104)] = string.char( 94), [string.char(105)] = string.char( 95), [string.char(106)] = string.char( 92), [string.char(107)] = string.char( 93), [string.char(108)] = string.char( 90), [string.char(109)] = string.char( 91), [string.char(110)] = string.char( 88), [string.char(111)] = string.char( 89), [string.char(112)] = string.char( 70), [string.char(113)] = string.char( 71), [string.char(114)] = string.char( 68), [string.char(115)] = string.char( 69), [string.char(116)] = string.char( 66), [string.char(117)] = string.char( 67), [string.char(118)] = string.char( 64), [string.char(119)] = string.char( 65), [string.char(120)] = string.char( 78), [string.char(121)] = string.char( 79), [string.char(122)] = string.char( 76), [string.char(123)] = string.char( 77), [string.char(124)] = string.char( 74), [string.char(125)] = string.char( 75), [string.char(126)] = string.char( 72), [string.char(127)] = string.char( 73), [string.char(128)] = string.char(182), [string.char(129)] = string.char(183), [string.char(130)] = string.char(180), [string.char(131)] = string.char(181), [string.char(132)] = string.char(178), [string.char(133)] = string.char(179), [string.char(134)] = string.char(176), [string.char(135)] = string.char(177), [string.char(136)] = string.char(190), [string.char(137)] = string.char(191), [string.char(138)] = string.char(188), [string.char(139)] = string.char(189), [string.char(140)] = string.char(186), [string.char(141)] = string.char(187), [string.char(142)] = string.char(184), [string.char(143)] = string.char(185), [string.char(144)] = string.char(166), [string.char(145)] = string.char(167), [string.char(146)] = string.char(164), [string.char(147)] = string.char(165), [string.char(148)] = string.char(162), [string.char(149)] = string.char(163), [string.char(150)] = string.char(160), [string.char(151)] = string.char(161), [string.char(152)] = string.char(174), [string.char(153)] = string.char(175), [string.char(154)] = string.char(172), [string.char(155)] = string.char(173), [string.char(156)] = string.char(170), [string.char(157)] = string.char(171), [string.char(158)] = string.char(168), [string.char(159)] = string.char(169), [string.char(160)] = string.char(150), [string.char(161)] = string.char(151), [string.char(162)] = string.char(148), [string.char(163)] = string.char(149), [string.char(164)] = string.char(146), [string.char(165)] = string.char(147), [string.char(166)] = string.char(144), [string.char(167)] = string.char(145), [string.char(168)] = string.char(158), [string.char(169)] = string.char(159), [string.char(170)] = string.char(156), [string.char(171)] = string.char(157), [string.char(172)] = string.char(154), [string.char(173)] = string.char(155), [string.char(174)] = string.char(152), [string.char(175)] = string.char(153), [string.char(176)] = string.char(134), [string.char(177)] = string.char(135), [string.char(178)] = string.char(132), [string.char(179)] = string.char(133), [string.char(180)] = string.char(130), [string.char(181)] = string.char(131), [string.char(182)] = string.char(128), [string.char(183)] = string.char(129), [string.char(184)] = string.char(142), [string.char(185)] = string.char(143), [string.char(186)] = string.char(140), [string.char(187)] = string.char(141), [string.char(188)] = string.char(138), [string.char(189)] = string.char(139), [string.char(190)] = string.char(136), [string.char(191)] = string.char(137), [string.char(192)] = string.char(246), [string.char(193)] = string.char(247), [string.char(194)] = string.char(244), [string.char(195)] = string.char(245), [string.char(196)] = string.char(242), [string.char(197)] = string.char(243), [string.char(198)] = string.char(240), [string.char(199)] = string.char(241), [string.char(200)] = string.char(254), [string.char(201)] = string.char(255), [string.char(202)] = string.char(252), [string.char(203)] = string.char(253), [string.char(204)] = string.char(250), [string.char(205)] = string.char(251), [string.char(206)] = string.char(248), [string.char(207)] = string.char(249), [string.char(208)] = string.char(230), [string.char(209)] = string.char(231), [string.char(210)] = string.char(228), [string.char(211)] = string.char(229), [string.char(212)] = string.char(226), [string.char(213)] = string.char(227), [string.char(214)] = string.char(224), [string.char(215)] = string.char(225), [string.char(216)] = string.char(238), [string.char(217)] = string.char(239), [string.char(218)] = string.char(236), [string.char(219)] = string.char(237), [string.char(220)] = string.char(234), [string.char(221)] = string.char(235), [string.char(222)] = string.char(232), [string.char(223)] = string.char(233), [string.char(224)] = string.char(214), [string.char(225)] = string.char(215), [string.char(226)] = string.char(212), [string.char(227)] = string.char(213), [string.char(228)] = string.char(210), [string.char(229)] = string.char(211), [string.char(230)] = string.char(208), [string.char(231)] = string.char(209), [string.char(232)] = string.char(222), [string.char(233)] = string.char(223), [string.char(234)] = string.char(220), [string.char(235)] = string.char(221), [string.char(236)] = string.char(218), [string.char(237)] = string.char(219), [string.char(238)] = string.char(216), [string.char(239)] = string.char(217), [string.char(240)] = string.char(198), [string.char(241)] = string.char(199), [string.char(242)] = string.char(196), [string.char(243)] = string.char(197), [string.char(244)] = string.char(194), [string.char(245)] = string.char(195), [string.char(246)] = string.char(192), [string.char(247)] = string.char(193), [string.char(248)] = string.char(206), [string.char(249)] = string.char(207), [string.char(250)] = string.char(204), [string.char(251)] = string.char(205), [string.char(252)] = string.char(202), [string.char(253)] = string.char(203), [string.char(254)] = string.char(200), [string.char(255)] = string.char(201), } local blocksize = 64 -- 512 bits function hmac_sha1(key, text) assert(type(key) == 'string', "key passed to hmac_sha1 should be a string") assert(type(text) == 'string', "text passed to hmac_sha1 should be a string") if #key > blocksize then key = sha1_binary(key) end local key_xord_with_0x36 = key:gsub('.', xor_with_0x36) .. string.rep(string.char(0x36), blocksize - #key) local key_xord_with_0x5c = key:gsub('.', xor_with_0x5c) .. string.rep(string.char(0x5c), blocksize - #key) return sha1(key_xord_with_0x5c .. sha1_binary(key_xord_with_0x36 .. text)) end function hmac_sha1_binary(key, text) return hex_to_binary(hmac_sha1(key, text)) end PyPagekite-1.5.2.201011/contrib/lua/pagekite.lua000066400000000000000000000313751374056564300210470ustar00rootroot00000000000000#!/usr/bin/lua -- -- Pagekite(.net) back-end client in Lua -- by Stelios Mersinas (steliosm@gmail.com) -- v0.3 -- -- -- Changelog: -- -- 0.3 -- Supports tunnel status check using PING packets. -- A start.sh scripts re-starts the service if needed -- -- Load the needed modules require "socket" require "os" require "math" -- load the SHA1 encryption library dofile "lib/shalib.lua" -- -- Read the config file -- dofile ".config" -- -- Functions Part -- function string.trim(str) return (string.gsub(str, "^%s*(.-)%s*$", "%1")) end function string.explode(str, sep) -- Split a string based on sep value. -- Return a table back local pos, t = 1, {} if #sep == 0 or #str == 0 then return end for s, e in function() return string.find(str, sep, pos) end do table.insert(t, string.trim(string.sub(str, pos, s-1))) pos = e+1 end table.insert(t, string.trim(string.sub(str, pos))) return t end function sleep (my_sec) -- A function to make Lua sleep for a bit :-) -- It's an ugly hack actually. os.execute("sleep " .. tonumber(my_sec)) end function ConnectToServer (my_server, my_port) -- Create a socket and connect to server/port given. -- In case of a connection problem, conn will be nil and status will hold the error message local conn, status = socket.connect (my_server, my_port) -- Check weather we got a connection to set the timeout option if conn then -- Set a socket timeout value. This will allow us to skip blobking for ever on socket:receive() conn:settimeout (30) end -- Return the socket object return conn, status end function PingServer (fe_socket) -- Do a PING request to the server to check if the tunnel is up. -- The server should response with a NOOP header. -- In case the connection is down, then the socket:receive() will timeout if debug then print ( os.date("%c"), "[PingServer] Ping?") end local ping = false -- Build the Ping frame request local my_chunk = "NOOP: 1\r\nPING: 1\r\n\r\n!" local my_chunk_length = string.len(my_chunk) local my_frame = string.format ("%x", my_chunk_length) .. "\r\n" .. my_chunk -- Send the Ping request to the server fe_socket:send ( my_frame ) -- Listen for the reply frame_header, status, partial = fe_socket:receive("*l") if frame_header ~= nil then -- Socket didn't timeout, the connection seems to be alive. -- Read the rest of the frame data and look for a NOOP reply local frame_size = tonumber (frame_header, 16) local frame_data, status, partial = fe_socket:receive (frame_size) -- Check for the NOOP header if string.find ( frame_data, "NOOP") then if debug then print ( os.date("%c"), "[PingServer] Pong!") end ping = true end else ping = false end -- Return the result return ping end function PhaseOne () -- -- Make a connection to PageKite server and send a challenge request. -- This is the first phase of the authecation handshakng procedure. -- -- Define the variable to hold the server's response local my_challenge_string, my_session_id = "" -- Generate a random string using the math.random() functionstring.sub ( string.sub ( math.random(), 3 ) .. string.sub ( math.random(), 3 ) -- and cut a 36-bytes long string for the BSalt field and a 8 byte long string for the salt. math.randomseed ( os.time() ) local my_random_bsalt = string.sub ( string.sub ( math.random(), 3 ) .. string.sub ( math.random(), 3 ) .. string.sub ( math.random(), 3 ), 1, 36) local my_random_salt = string.sub ( string.sub ( math.random(), 3 ) .. string.sub ( math.random(), 3 ), 1, 8) -- Sign the data string (Sig) using the shared secret contained in the .config file local my_data = string.format ("http:%s:%s", pk_site, my_random_bsalt) --local my_signed_string = string.sub("87654321" .. sha1(pk_server_token .. my_data .. ":87654321"), 1, 36) local my_signed_string = string.sub(my_random_salt .. sha1(pk_server_token .. my_data .. ":" .. my_random_salt), 1, 36) -- Create the Phase-One authentication request local my_request = "CONNECT PageKite:1 HTTP/1.0\r\n" .. "X-PageKite-Version: 0.3.21\r\n" .. string.format ( "X-PageKite: http:%s:%s::%s", pk_site, my_random_bsalt, my_signed_string ) .. "\r\n\r\n" -- Make a connection to the Front-end server local remote_conn, error = ConnectToServer (pk_server, pk_server_port) -- Check if the connection was sucessful if remote_conn then -- We have a connection, send the authentication string remote_conn:send ( my_request ) -- Read the response for the server. Look for the challenge header while true do local data, status, partial = remote_conn:receive ("*l") -- Check if the remote port is closed if status == "closed" then break end -- Check for the challenge header if data ~= "" then header = string.explode(data, ":") -- Look for the Challenge request if header[1] == "X-PageKite-SignThis" then -- Challenge string found my_challenge_string = string.sub(data,22) end -- Look for the Session ID if header[1] == "X-PageKite-SessionID" then -- Session ID header found my_session_id = string.sub(data,23) end end end -- Close the socket - should already be closed by the server remote_conn:close() -- Return the values back return my_challenge_string, my_session_id else -- Error connecting to remote server. Return nil and the error message return nil, error end end function PhaseTwo (my_challenge_header, my_session_id) -- -- Reply to the challenge request and setup the tunnel -- -- Create challenge reply local my_random_salt = string.sub ( string.sub ( math.random(), 3 ) .. string.sub ( math.random(), 3 ), 1, 8) local my_challenge_reply = my_random_salt .. sha1 (pk_server_token .. my_challenge_header .. my_random_salt) -- Build the response local my_challenge_response = "CONNECT PageKite:1 HTTP/1.0\r\n" .. "X-PageKite-Version: 0.3.21\r\n" .. string.format ("X-PageKite-Replace: %s", my_session_id) .. "\r\n" .. string.format ("X-PageKite: %s:%s", my_challenge_header, string.sub(my_challenge_reply,1,36)) .. "\r\n\r\n" -- Connect to the server and send the reply back local remote_conn, error = ConnectToServer (pk_server, pk_server_port) -- Check if got connected and send the reply back if remote_conn then -- Send the response back remote_conn:send ( my_challenge_response ) -- Read the response for the server. Look for the OK header while true do data, status, partial = remote_conn:receive ("*l") -- Check if the remote port is closed if status == "closed" then break end if data ~= "" then header = string.explode(data, ":") end if header[1] == "X-PageKite-OK" then -- OK header found, set a flag ok_flag = 1 end if data == "" and ok_flag == 1 then -- Final line, exit the loop break end end -- Tunnel is configured, return the socket object. return remote_conn else -- Problem connecting to server and setting up the tunnel. -- Send a nil value and the error message. return nil, error end end function HTTP_handler ( my_chunk ) -- Make an HTTP request to the server, push the data and get a reply back local http_reply, http_conn local remote_conn, error = ConnectToServer (proxy_web, proxy_web_port) if debug then print (os.date("%c"), "[HTTP Response] Making Proxy connection to web server") end -- Check if we got connected to the web server if remote_conn then -- We have a connection, send the request remote_conn:send(my_chunk) remote_conn:send("\r\n") -- Get the reply back from the web server -- Note: Busyboxe's httpd server supports HTTP/0.9, so the connection closes after each request. http_reply = remote_conn:receive("*a") else -- Problem connecting, send error message back http_reply = nil end -- Return the data return http_reply end function RequestHandler ( my_frame_data ) -- Parse the frame and send a reply back -- -- Find out the request type (currently only HTTP requests are supported) local chunk, frame_length, frame_response, session_id local http_request = "" local protocol_response, request_is_http for request_line in my_frame_data:gmatch("[^\r\n]+") do -- Look for the SID header to get the size of data comming if string.find (request_line, "SID:") then -- Get the SID and store it frame_header = string.explode(request_line, ":") session_id = frame_header[2] end -- Check if this is an HTTP request if string.find (request_line, "GET /") then request_is_http = true -- http_request = request_line end -- Add the HTTP headers to a buffer if request_is_http then if request_line == "" then request_line = "\r\n" end http_request = http_request .. request_line .. "\r\n" end -- End For loop end -- Call the protocol handler if request_is_http ~= nil then -- Call the HTTP handler if debug then print (os.date("%c"), "[RequestHandler] Calling HTTP Response handler") end protocol_response = HTTP_handler ( http_request ) else if debug then print (os.date("%c"), "[RequestHandler] Request type is UNKNOWN ") end protocol_response = nil end -- Check if we got a response back if protocol_response ~= nil then -- Create the response frame chunk = "SID: " .. session_id .. "\r\n" .. "\r\n" .. protocol_response -- Calculate frame size frame_length = string.len (chunk) frame_response = string.format ("%x",frame_length) .. "\r\n" .. chunk -- Create the EOF chunk as well eof_chunk = "SID: " .. session_id .. "\r\n" .. "EOF: RW" .. "\r\n\r\n" eof_length = string.len (eof_chunk) eof_frame = string.format ("%x", eof_length) .. "\r\n" .. eof_chunk frame_response = frame_response .. eof_frame else -- Send an empty frame frame_response = nil end -- Send the frame back return frame_response end function PageKite ( fe_socket ) -- This is the protocol handler. -- It's responsible to receive and send data over the network connection. local frame_size, frame_data, frame_response -- Start a big loop - look for frames arriving or for a closed remote connection if debug then print ( os.date("%c"), "[PageKite] Waitting for data...") end while true do -- Loop over the socket and keep reading data until it closes -- The first line should contain the frame size local frame_header, status, partial = fe_socket:receive ("*l") if status == "closed" then -- Remote peer closed the connection! if debug then print (os.date("%c"), "[PageKite] Remote connection closed! Exiting...") end break end if status == "timeout" then -- Do a ping to the server to check the link status if not PingServer ( fe_socket ) then break end end if frame_header ~= nil then -- Get the first line of the frame. This should be the size of the chunk. frame_size = tonumber(frame_header, 16) if debug then print (os.date("%c"), "[PageKite] Incoming frame " .. frame_size .. " (0x" .. frame_header ..") bytes long") end -- Read the rest of the request to a buffer frame_data, status, partial = fe_socket:receive (frame_size) -- Call the Request Handler and get back a frame to send to the server if debug then print (os.date("%c"), "[PageKite] Calling RequestHandler") end frame_response = RequestHandler (frame_data) -- Send the request back to the front end -- If there is no response frame do not send anything if frame_response ~= nil then if debug then print (os.date("%c"), "[PageKite] Sending reply to FrontEnd") end fe_socket:send ( frame_response ) end end end end -- -- Main Part -- -- Run at least once and loop only if the user has defined server_reconnect as true. repeat -- Start PhaseOne authentication process -- Get the challenge response back to proceed to the second phase. challenge, session_id = PhaseOne () -- Check if we have a challenge string. if challenge then -- Phase One was completed successful. Move to phase 2 remote_conn, error = PhaseTwo (challenge, session_id) -- Check if we passed Phase Two authentication if remote_conn then -- Authenticated with server. print ("Ready.") -- Get the PageKite protocol handler PageKite (remote_conn) else -- Couldn't connect to remote server print ("Error connecting to remote server: " .. error) end end -- Try to reconnect to server after sleeping for 10 seconds if debug then print (os.date("%c"), "Reconnecting to server...") end -- Make a small pause before reconnecting to the server sleep (10) -- Loop over until server_reconnect == 0 PyPagekite-1.5.2.201011/contrib/lua/start.sh000077500000000000000000000005611374056564300202400ustar00rootroot00000000000000#!/bin/sh my_date=`date +"%d/%m/%Y %H:%M"` # Log starting the service echo "[$my_date] Starting service" > ./log # Loop over to restart the client on case of a crash while [ -f /home/pagekite/pagekite.lua ] do lua ./pagekite.lua sleep 10 # Log the crash my_date=`date +"%d/%m/%Y %H:%M"` echo "[$my_date] Service crashed! Restarting service" >> ./log done PyPagekite-1.5.2.201011/deb/000077500000000000000000000000001374056564300150535ustar00rootroot00000000000000PyPagekite-1.5.2.201011/deb/changelog000066400000000000000000000004661374056564300167330ustar00rootroot00000000000000pagekite (0.5.6d) unstable; urgency=low * Automatic package build note. -- PageKite Packaging Team Fri, 14 Mar 2014 07:57:38 +0530 pagekite (0.4.0-0) unstable; urgency=low * Initial release -- PageKite Packaging Team Fri, 29 Jul 2011 22:39:51 +0000 PyPagekite-1.5.2.201011/deb/compat000066400000000000000000000000021374056564300162510ustar00rootroot000000000000009 PyPagekite-1.5.2.201011/deb/control000066400000000000000000000021061374056564300164550ustar00rootroot00000000000000Source: pagekite Section: net Priority: optional Maintainer: PageKite Packaging Team Build-Depends: debhelper (>= 9), python (>= 2.4) X-Python-Version: >= 2.3, << 3.0 Standards-Version: 3.9.5 Homepage: https://pagekite.net/ Package: pagekite Section: net Architecture: all Depends: ${misc:Depends}, daemon (>= 0.6), python (>= 2.3), python (<< 3.0), python-six, python-socksipychain (>= 2.1.2), 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. PyPagekite-1.5.2.201011/deb/copyright000066400000000000000000001056661374056564300170240ustar00rootroot00000000000000Format: http://dep.debian.net/deps/dep5 Upstream-Name: pagekite Source: https://github.com/pagekite/PyPagekite/ Files: * Copyright: 2013 Bjarni Runar Einarsson 2013 The Beanstalks Project ehf. License: AGPL-3+ 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". License: AGPL-3+ 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 . PyPagekite-1.5.2.201011/deb/dirs000066400000000000000000000000401374056564300157310ustar00rootroot00000000000000var/log/pagekite etc/pagekite.d PyPagekite-1.5.2.201011/deb/docs000066400000000000000000000000561374056564300157270ustar00rootroot00000000000000doc/CREDITS.txt doc/HISTORY.txt doc/README.md PyPagekite-1.5.2.201011/deb/pagekite.init000077700000000000000000000000001374056564300246342../etc/init.d/pagekite.debianustar00rootroot00000000000000PyPagekite-1.5.2.201011/deb/pagekite.logrotate000077700000000000000000000000001374056564300267262../etc/logrotate.d/pagekite.debianustar00rootroot00000000000000PyPagekite-1.5.2.201011/deb/postinst000066400000000000000000000035271374056564300166700ustar00rootroot00000000000000#!/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/*.rc* || true chmod 600 /etc/pagekite.d/[019]*rc* || true [ -d /etc/pagekite ] && rmdir /etc/pagekite || true ;; abort-upgrade|abort-remove|abort-deconfigure) ;; *) echo "WARNING: postinst called with unknown argument \`$1'" >&2 ;; esac # dh_installdeb will replace this with shell code automatically # generated by other debhelper scripts. #DEBHELPER# exit 0 PyPagekite-1.5.2.201011/deb/postrm000066400000000000000000000023431374056564300163240ustar00rootroot00000000000000#!/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 "WARNING: postrm called with unknown argument \`$1'" >&2 ;; esac # dh_installdeb will replace this with shell code automatically # generated by other debhelper scripts. #DEBHELPER# exit 0 PyPagekite-1.5.2.201011/deb/preinst000066400000000000000000000017141374056564300164650ustar00rootroot00000000000000#!/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 PyPagekite-1.5.2.201011/deb/prerm000066400000000000000000000022071374056564300161240ustar00rootroot00000000000000#!/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 PyPagekite-1.5.2.201011/deb/rules000077500000000000000000000012131374056564300161300ustar00rootroot00000000000000#!/usr/bin/make -f %: dh $@ --with python2 --buildsystem=pybuild override_dh_installman: make doc/pagekite.1 dh_installman doc/*.? # After the basic Python installation, add files for /etc/ override_dh_auto_install: PYBUILD_INSTALL_ARGS="--install-lib={install_dir}" dh_auto_install --buildsystem=pybuild 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/ override_dh_builddeb: dh_builddeb -- -Zgzip PyPagekite-1.5.2.201011/deb/source/000077500000000000000000000000001374056564300163535ustar00rootroot00000000000000PyPagekite-1.5.2.201011/deb/source/format000066400000000000000000000000151374056564300175620ustar00rootroot000000000000003.0 (native) PyPagekite-1.5.2.201011/defaults.cfg000066400000000000000000000001551374056564300166120ustar00rootroot00000000000000#[ PageKite.py bundle default settings ]# #defaults #service_on = http : KITENAME : localhost : 80 : SECRET PyPagekite-1.5.2.201011/dist/000077500000000000000000000000001374056564300152645ustar00rootroot00000000000000PyPagekite-1.5.2.201011/dist/.gitignore000066400000000000000000000000001374056564300172420ustar00rootroot00000000000000PyPagekite-1.5.2.201011/doc/000077500000000000000000000000001374056564300150665ustar00rootroot00000000000000PyPagekite-1.5.2.201011/doc/Apache-Redirect.conf000066400000000000000000000017271374056564300206640ustar00rootroot00000000000000# This is an Apache configuration fragment which enables a redirect-only # VirtualHost, listening on Port 88. This can be used in combination with # the following pagekite.py configuration to redirect all incoming HTTP # requests to HTTPS. # # # /etc/pagekite.d/80_force_https.rc # service_on = http-80 : @kitename : localhost : 88 : @kitesecret # # Note: if you want to enforce HTTPS it is also worth enlisting the help # of the browsers themselves by advertising a Strict-Transport-Security # header. Add the following to your content VirtualHosts: # # Header always set Strict-Transport-Security "max-age=604800" env=nocache # # Before using this, please run `a2enmod rewrite` or uncomment the # following line: # # LoadModule rewrite_module /usr/lib/apache2/modules/mod_rewrite.so Listen 127.0.0.1:88 RewriteEngine On RewriteOptions Inherit RewriteCond %{HTTP_HOST} ^([^:]+) RewriteRule ^(.*)$ https://%1$1 [L,R=302] PyPagekite-1.5.2.201011/doc/CREDITS.txt000066400000000000000000000003471374056564300167300ustar00rootroot00000000000000## CREDITS ## FamFamFam Silk Icons by Mark James Joar Wandborg Luc-Pierre Terral ## AUTHORS ## Bjarni Rúnar Einarsson Már Örlygsson PyPagekite-1.5.2.201011/doc/DEV-HOWTO.md000066400000000000000000000013231374056564300167230ustar00rootroot00000000000000## 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/`. PyPagekite-1.5.2.201011/doc/HISTORY.txt000066400000000000000000000375051374056564300170020ustar00rootroot00000000000000Version history - highlights ============================ v1.5.2.201011 ------------- - Fix the bundled CA certificate for Sectigo v1.5.2.200725 ------------- - Make --ca_certs no longer part of --defaults - Disable CA certificate checking in the distant future - Try harder to survive OpenSSL write-retry errors on backend v1.5.2.200603 ------------- - Fix some TLS connection instability on old/embedded devices - Fix bad select() behaviour (Windows) v1.5.2.200531 ------------- - Update bundled CA Root certs - Avoid hanging TunnelManager if DNS updates never complete - Remove automated crash reports, it is a privacy leak nobody uses - Python3 fixes for built-in HTTPD - Further narrow the disconnect/keepalive logic v1.5.2.200513 ------------- - Remove obsolete finger, httpfinger and Minecraft protocols - Add working front-end support for XMPP tunnels - Make the auto-keepalive logic simpler and hopefully more robust - Depend on pySocksipychain 2.1.2+, to pick up SSL fixes - The single-file pagekite.py bundle now supports Python 2.7 and 3.x - Document the forgotten `unknown` backend-of-last-resort feature v1.5.1.200424 ------------- - This merges release (v1.0.1): performance and efficiency! - Create ping.pagekite fast-path in dedicated thread - Make select loop timing and read sizes configurable, tweak defaults - Remove 0.4.x flow-control, fix major bugs in current flow control code - Fix locking-related deadlocks under PyPy - Added --watchdog=N, to self-reap locked up processes - Disabled old ssl workarounds on modern versions of Python (broke PyPy) v1.5.0.200327 ------------- - Allow loading frontend IP list from a file instead of using DNS - Avoid crash if getsockname() fails - Python 3 bugfix: Fixed text wizard UI - Support for Python 3.8, more misc Python 3 fixes v1.5.0.191126 ------------- - FIRST RELEASE which supports Python 3; default is still 2.7 though. - Bump versions: New Dev Is Happening! Also, breaking compatibility... - Dropped protocols: finger, httpfinger, minecraft - Dropped support: Python 2.6 and below probably no longer work. - Fix a few minor buglets - Added experimental support for relay connections over websockets - Debian: improve .deb config samples - Improve errors/feedback if config is unwritable during signup, add, etc. - Added +proxyproto: HAProxy PROXY (v1) protocol support at the backend - Added --loglevel=N, massage logging & defaults to be more user friendly v1.0.0.190225 2019.02.25 ------------- - Call this 1.0, change versioning schemes. We're pretty stable! - UI: Made relay capability description and quota report less confusing - BE: Many refinements to the front-end relay selection algorithm - BE: Implement exponential fallback for DDNS update failures - BE: Fix IP address leak in BE offline page, fix &-encoding in URL - FE: Add --ratelimit_ips= , for eliminating phishing once and for all - FE: Augmented --authdomain= so it supports external authentication apps - FE: Added --authfail_closed, to avoid failing open if tunnel auth is broken - FE: Implement --overload= and friends. - Debian: Make it easy to use alternate Python for daemon (see init script) - HTTPD: Add support for file uploads, new flags: +uploads and +ul_filenames - HTTPD: Implement a PhotoBackup server, new flags: +photobackup v0.5.9.3 2018.01.24 -------- - Fix problems with setting CA cert location within wizard - Add workaround to cope with broken CA cert configurations v0.5.9.2 2018.01.23 -------- - Improve UI and provide debugging hints when failing to connect - Remove incorrect use of assert() v0.5.9.1 2017.12.09 (same as 0.5.9a) -------- - Adjust tunnel ping frequency on disconnect, to cope with bad firewalls - Add --keepalive for manual tunnel ping frequency configuration - Workaround Debian (and others) distrusting StartCOM issued certificates - Allow multiple --errorurl arguments for per-domain customization - Fix loopback (local backends on a frontend) v0.5.9 2016.11.18 ------ - CRITICAL: Fix how we load CA Certificates - Add --fe_nocertcheck for insecure (obfuscation-only) TLS connections - Add --whitelabel and --whitelabels for auto-configuring default settings for users of the pagekite.net white-label service - Remove --jakenoia, it wasn't documented anywhere and didn't really work - Advertise relay overload, consider during "Ping" evaluation - Create vipagekite helper for safely editing configs v0.5.8f 2016.11.18 ------- - CRITICAL: Fix how we load CA Certificates (backport from v0.5.9) - Minor back-ported bugfixes v0.5.6f 2016.11.18 ------- - CRITICAL: Fix how we load CA Certificates (backport from v0.5.9) v0.5.8e 2016.03.02 ------- - Fix dynamic DNS update bug which would advertise too many IPs - Enable versioned DNS frontend lookups in default settings - Fix server ping logic (broken in 0.5.8a) v0.5.8b 2016.02.16 ------- - Fix SSL3_WRITE_PENDING errors with recent OpenSSL versions - Make signup e-mail regexp less strict (rely on server to check) - Change iframe links to use https:// by default 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. PyPagekite-1.5.2.201011/doc/MANPAGE.md000066400000000000000000000523561374056564300164730ustar00rootroot00000000000000## Name ## pagekite - 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. * +proxyproto Use HAProxy's PROXY Protocol (v1) to relay IPs etc. * +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. * +uploads Accept file uploads. * +uploads=`RE` Accept uploads to paths matching regexp RE. * +ul_filenames=`P` Upload naming policy. P = overwrite, keep or rename * +cgi=`list` A list of extensions, for which files should be treated as CGI scripts (example: `+cgi=cgi,pl,sh`). * +photobackup=`password` Enable built-in PhotoBackup server with the given password. See https://photobackup.github.io/ for details. ## 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. * --whitelabel=D Set defaults for pagekite.net white-labels. * --whitelabels=D Set defaults for pagekite.net white-labels (with TLS). * --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. The kitename `unknown`, if allowed by the front-end, represents a backend of last resort for requests with no other match. * --authdomain=`DNS-suffix`, --authdomain=`/path/to/app`, --authdomain=`kite-domain`:`DNS-suffix`, --authdomain=`kite-domain`:`/path/to/app`
Use `DNS-suffix` for remote DNS-based authentication of incoming tunnel requests, or invoke an external application for this purpose. If no kite-domain is given, use this as the default authentication method. See the section below on tunnel authentication for further details. In order for the app path to be recognized as such, it must contain at least one / character. * --auththreads=`N`
Start N threads to process auth requests. Default is 1. * --authfail_closed
If authentication fails, reject tunnel requests. The default is to fail open and allow tunnels if the auth checks are broken. * --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. * --frontends=`num`:`@/path/to/file`:`port`
Same as above, except the IP address list will be loaded from a file (and reloaded periodically), instead of using DNS. * --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. * --fe_nocertcheck
Connect using SSL/TLS, but do not verify the remote certificate. This is largely insecure but still thwarts passive attacks and prevents routers and firewalls from corrupting the PageKite tunnel. * --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. * --keepalive=`N`
Force traffic over idle tunnels every N seconds, to cope with firewalls that kill idle TCP connections. Backend only: if set to "auto" (the default), the interval will be adjusted automatically in response to disconnects. * --all Terminate early if any tunnels fail to register. * --new Don't attempt to connect to any kites' old front-ends. * --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. This is for static configurations, for dynamic access controls use the `--authdomain` mechanism. The domain `unknown`, if configured, represents a backend of last resort for incoming requests with no other match. * --authdomain=`DNS-suffix`, --authdomain=`/path/to/app`, --authdomain=`kite-domain`:`DNS-suffix`, --authdomain=`kite-domain`:`/path/to/app`
Use `DNS-suffix` for remote DNS-based authentication of incoming tunnel requests, or invoke an external application for this purpose. If no kite-domain is given, use this as the default authentication method. See the section below on tunnel authentication for further details. In order for the app path to be recognized as such, it must contain at least one / character. * --auththreads=`N`
Start N threads to process auth requests. Default is 1. * --authfail_closed
If authentication fails, reject tunnel requests. The default is to fail open and allow tunnels if the auth checks are broken. * --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. * --overload=`baseline`, --overload_cpu=`fraction, 0-1`, --overload_mem=`fraction, 0-1`
Enable "overload" calculations, which cause the front-end to recommend back-ends go elsewhere if possible, once connection counts go above a certain number. The baseline is the initial overload level, but it will be adjusted dynamically based on load average (CPU use) and memory usage. This will really only work well on Linux and if PageKite is the only thing happening on the machine. Setting both fractions to 0 disables dynamic scaling. * --overload_file=`/path/to/baseline/file`
Path to a file, the contents of which overrides all overload calculations. This can be used to manage load calculations using an external process (or by hand, e.g. to prepare for maintenance). Note that overload must specify a non-zero baseline, otherwise this setting is ignored. * --ratelimit_ips=`IPs/seconds`, --ratelimit_ips=`kitename`:`IPs/seconds`
Limit how many different clients (IPs) can request data from a tunnel within a given window of time, e.g. 5/3600. This is useful as either a crude form of DDoS mitigation, or as a mechanism to make public kite services unusable for phishing. Note that limits are enforced per-tunnel (not per kite), and tunnels serving multiple kites will use the settings of the strictest kite. Limits apply to subdomains as well. A single IP may be counted more than once if request headers (such as User-Agent) differ. * --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. * --nopyopenssl Avoid use of the pyOpenSSL library (not in config file) * --nossl Avoid use SSL entirely (not allowed in config file) * --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. * --errorurl=`D:U` Custom error URL for domain D. * --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: ## Tunnel Request Authentication ## When running pagekite as a front-end relay, you can enable dynamic authentication of incoming tunnel requests in two ways. One uses a DNS-based protocol for delegating authentication to a remote server. The nice thing about this, is relays can be deployed without any direct access to your user account databases - in particular, a zero-knowlege challenge/response protocol is used which means the relay never sees the shared secret used to authenticate the kite. The second method delegates authentication to an external app; this external app can be written in any language you like, as long as it implements the following command-line arguments:
  --capabilities     Print a list of capabilities to STDOUT and exit
  --server           Run as a "server", reading queries on STDIN and
                  sending one-line replies to STDOUT.
  --auth     Return JSON formatted auth and quota details
  --zk-auth   Implement the DNS-based zero-knowlege protocol
The recognized capabilities are SERVER, ZK-AUTH and AUTH. One of AUTH or ZK-AUTH is required. The JSON `--auth` responses should be dictionaries which have at least one element, `secret` or `error`. The secret is the shared secret to be used to authenticate the tunnel. The dictionary may also contain advisory quota values (`quota_kb`, `quota_days` and `quota_conns`), and IP rate limiting parameters (`ips_per_sec-ips` and `ips_per_sec-secs`). The source distribution of pagekite includes a script named `demo_auth_app.py` which implements this protocol. ## 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-2020, 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: PyPagekite-1.5.2.201011/doc/README.md000066400000000000000000001115001374056564300163430ustar00rootroot00000000000000# 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. Some front-end relay functionality requires Python 2.6 or better. `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 (If you would like even more flexibility in how you configure which domains you provide service to, consult the [man page](MANPAGE.md) for information about the `--authdomain=` flag and external authentication apps.) 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 the "unknown" back-end as a catch-all for any unrecongized domains, by using the special hostname "unknown". This can either be a local back-end, using the `--service_on` line (remember to specify a protocol), or added as a domain using `--domain` and served over a tunnel like any other kite. 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-2020, 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-2020, 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) ] PyPagekite-1.5.2.201011/doc/REMOTEUI.md000066400000000000000000000113571374056564300166500ustar00rootroot00000000000000# 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. PyPagekite-1.5.2.201011/doc/header.txt000066400000000000000000000014031374056564300170550ustar00rootroot00000000000000# 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-2020, 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. PyPagekite-1.5.2.201011/doc/lapcat.1000066400000000000000000000040031374056564300164110ustar00rootroot00000000000000.\" 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. PyPagekite-1.5.2.201011/droiddemo.py000077500000000000000000000114711374056564300166500ustar00rootroot00000000000000#!/usr/bin/python -u from __future__ import absolute_import from __future__ import print_function # # 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 six from six.moves.urllib import unquote from six.moves.urllib.parse import urlparse import android import pagekite import os 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 = list(six.iterkeys(mtimes)) files.sort(key=lambda iname: mtimes[iname]) 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 as 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 as 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) PyPagekite-1.5.2.201011/etc/000077500000000000000000000000001374056564300150745ustar00rootroot00000000000000PyPagekite-1.5.2.201011/etc/init.d/000077500000000000000000000000001374056564300162615ustar00rootroot00000000000000PyPagekite-1.5.2.201011/etc/init.d/pagekite.debian000077500000000000000000000124671374056564300212330ustar00rootroot00000000000000#! /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 # Choose the best available Python interpretor PYTHON="python" [ -x "/usr/bin/python2" ] && PYTHON="/usr/bin/python2" [ -x "/usr/bin/python2.7" ] && PYTHON="/usr/bin/python2.7" # This will allow pypy to work, but we leave it up to the brave admin to # enable pypy by setting PYTHON=/usr/bin/pypy in /etc/default/pagekite. if [ -d /usr/lib/python2.7/dist-packages/pagekite/ ]; then export PYTHONPATH=/usr/lib/python2.7/dist-packages/ export PYTHONDONTWRITEBYTECODE=1 fi # 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 -- \ $PYTHON $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 $PYTHON -- \ $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 : PyPagekite-1.5.2.201011/etc/init.d/pagekite.fedora000077500000000000000000000037221374056564300212430ustar00rootroot00000000000000#!/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 PyPagekite-1.5.2.201011/etc/logrotate.d/000077500000000000000000000000001374056564300173165ustar00rootroot00000000000000PyPagekite-1.5.2.201011/etc/logrotate.d/pagekite.debian000066400000000000000000000003231374056564300222510ustar00rootroot00000000000000/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 } PyPagekite-1.5.2.201011/etc/logrotate.d/pagekite.fedora000066400000000000000000000003011374056564300222630ustar00rootroot00000000000000/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 } PyPagekite-1.5.2.201011/etc/pagekite.d/000077500000000000000000000000001374056564300171075ustar00rootroot00000000000000PyPagekite-1.5.2.201011/etc/pagekite.d/10_account.rc000066400000000000000000000003441374056564300213720ustar00rootroot00000000000000#################################[ 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 PyPagekite-1.5.2.201011/etc/pagekite.d/20_frontends.rc000066400000000000000000000007661374056564300217510ustar00rootroot00000000000000#################################[ 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 PyPagekite-1.5.2.201011/etc/pagekite.d/80_httpd.rc.sample000066400000000000000000000013461374056564300223530ustar00rootroot00000000000000#################################[ This file is placed in the Public Domain. ]# # Expose the local HTTPD service_on = http:@kitename : localhost:80 : @kitesecret # If you have TLS/SSL configured locally, uncomment this to enable end-to-end # TLS encryption instead of relying on the wild-card certificate at the relay. # #service_on = https:@kitename : localhost:443 : @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 # PyPagekite-1.5.2.201011/etc/pagekite.d/80_sshd.rc.sample000066400000000000000000000002521374056564300221640ustar00rootroot00000000000000#################################[ This file is placed in the Public Domain. ]# # Expose the local SSH daemon service_on = raw/22:@kitename : localhost:22 : @kitesecret PyPagekite-1.5.2.201011/etc/pagekite.d/accept.acl.sample000066400000000000000000000016331374056564300223120ustar00rootroot00000000000000# 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 blocking abusive clients on a temporary basis. # # WARNING: This is inefficient and slow. Every line added to this file # has a cost. # # 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 PyPagekite-1.5.2.201011/etc/sysconfig/000077500000000000000000000000001374056564300171005ustar00rootroot00000000000000PyPagekite-1.5.2.201011/etc/sysconfig/pagekite.fedora000066400000000000000000000001511374056564300220500ustar00rootroot00000000000000OPTIONS="--optdir=/etc/pagekite.d" PK_UID=daemon PK_GID=daemon PK_LOGFILE=/var/log/pagekite/pagekite.log PyPagekite-1.5.2.201011/gui/000077500000000000000000000000001374056564300151055ustar00rootroot00000000000000PyPagekite-1.5.2.201011/gui/background.jpg000066400000000000000000000153401374056564300177310ustar00rootroot00000000000000JFIFHHC  !"$"$C+"M !1AQa"Tq#2d4BRbers&3U6CEt ?;m̾JEp-ddAp[us/ko`Kne*}P`+o|\ꄪdZ۫kJ@_T _0%W2*ZW m̾JEp-ddAp[us/ko`Kne*}P`+o|\ꄪdZ۫kJ@_T _0%W2*ZW m̾JEp-ddAp[us/ko`Kne*}P`+o|\ꄪdZ]U1OT]2f*? W~b AAUߘ ,b0c=PPUw+o!T]2f*? W~b AAUߘ ,b0c=PPUw+o!T]2f*? W~b AAUߘ --& > a@)0PP& > a@)0PP& > a@)0PP& > a@)0PP& > a@)0PP& `.@=PY~;@_T, J/n` OT_%E0' /gh[3IQe@-IzAe RTY~=P sRzv)*,)=PY~;@_T, J/n` OT_ e.h,ހAEhzP @zY;i@&Yd^Pf@[7d 7e{iAlރe.h=݀B籭{׉= f]v=tܮ +PS uN}먼N9*Y;i@&Yd^Pf@[7d 7e{iAlރe.h,ހAEhzP @zY;i@&YXK(_-AT8lJXK(_-AT8lKat ٔTǁU7#9\+RP 9 Ɖ cdfc9 +\jS++0ͷ(-%Rl, Y `U/ _E%Rl, Y `U/ _E%Rl,A`(Q u'fR,6*+]rgMF*+T Uj 9r9G&'-+0KyLǣڴrbe%fjD,X A`-ޠVrkMF/Mo'&R+~+9mZjo>5 Fj`s9G5hJ6;R=8!@W QUE@UETTTPTTQP[ZvgNm`Iٞi5?UqA*A*TssVLO2ҳ l~(sq0{sVLLGmG5TTPTTQPǝcU] GrTF_{vɸy*jqK@ * * ( R)!@VvkMF/OvkNm`cUUVUUZP  s{VLOlfD7a1YIlz^s!@.r5-TkUZ"bb&+fUUkS:؀@9fE2،GjaNYhڭ[ OolF#صE( ER .B!HP ))RvkMF/Ovi!6&5UUj;5n@vY:+pzPK6PRvk1SvkMF/O5mUU[TV *A莹,N*raixђ/Ek9\ܪ@ڱ=LטbVPKH[9-}((Tm𽩱 ŻrL6ZQNQ8Qbsyr[P7D`ᒘIXq'I7-ʇ8cZ91 bi񚍦%[PP啎/ս 7#ZaNYX[xp|zDb㝘''j:+>Yz.i%V#荭kO*2E@fś3b^]y܃Ҟ+VuV6YSOZf»]Ѝ[Y**"*3~77ÉqWZڪQӗeMqQjEKQx8"%;`kHIVB\fJI/EMIjU[T/_a14d[8b[QQZ7u8n:2e&*j)4[iIW5?"7PK] M"/x, ȬsWz*Ss7BrlU_ jd\@&Ea޸'˽!WFw>y!3$HZ$G+qb+B=ZֹTFUW5\-F'*FJ:1h(wvґVua5: x,x zl[85IF|WUtW=U긪`aنSUݠczG 4ۢHyK-[\+͖7t)yDūCs^ƽG5ͪ*b )[8'sv+oiUUi(}V%PoEGٞPv_$8dsSәo hp:zZi%d]wE~wkF݊1yWy2\'9Jv.GoҮc^ػf#Q:+k{Ƨ'ecÙ1 jȭG7p9 SzBȚ2+HUS ( qH٣aFLmRrYcM2DU{qUP>{iI,I8nYLyc;UJWLO/ӓZN,QWV. KFg㘄] 9@@բ1:ҎjO2r^ن;Šܧ~4ף!]-j󳽠t^8ګQiz>!DlXL =b>%N !roΈatnzeu~,o֌^K3)q9~z.P ZYy٤V~Exޙd_gr5U@'[IA`*:L;D]b~30e֡rD3 iǁS0%!MBZ#QS_ )6#u_ Ox (EJ-1::J17n@5~]izm[+ i|HUzzj2qt+cg,{њVԽ q96%@?ڟLQ\M'Q ]vi!&UP5ߣ+Ӻ4k[&#Տ@/.shāζkKѕy?ѯu>;3`XNOU[]tK3:7+9硵=iT+jv䮁_Cj}MC֝Np !FhIDR5h^&A@3UƿUʔGR?Y_c=dK/̞26 Kg8[2DM~ruҋ?Ro%(S%6Fؔԭ*7GS[*jZik'?ܧ{.S0π[ܯ-H!i&HnG5ymY!r@ o DDo3WS֪5q3O>i#`#ã2v3JcvGgP@PyPagekite-1.5.2.201011/gui/dreki.png000066400000000000000000000660471374056564300167260ustar00rootroot00000000000000PNG  IHDR|BusRGBbKGD pHYs  tIME ,Zb IDATxweu;RNȁ0$)RhJ^FҌ۳5Y֒,FǒLI&MJL$H$r$RF{70^UBªFՋ~wo{_c6erl6lpa۰ m؆mn6a۰ ܆mn6lp ۰ m6lpa۰ m؆mn6a ?nyF{[?@>ŶVͼmbp3K\{="*'?IT܆`-V6#x6brYYfϹ?> {[NO! [Z= hH3O8>XW[~ܕfb@'6v@5@~]dK 6@Jdb !U4T t@8Ja!%W}_1p?01V﹝c:]﵊lP4E(lⲿڣg .I:-V {\u9mxÜ1s !q  z9R)$E!ED%E'~JlGВYW.~?k0dP0@HRO}d]GjVJy'%Fkxi*x:k JINl P>Vsc/ TI(C8}"@kۥiʐFBHyFk@ yv^ΔF}wVl%?2-W4DR{M'Tתھu8}&(y<* 5ʤXshɩszk^V;wߋRTme ǟЃy<]H!wͯ[MCE zDanajk|^\f;~ :R+SxA'DINfk7-o{Kl"py98f똸Z[M4~8Β[Cf, `EGﻏ;@Jf[׼r K:ኼM*?Ŧ׽ Ulrx[3ykURlZ7?}^~S CRm(!1;,u:|y綾"d"w%6xP9 İp!Dw;}n{? 6R}7D0zٕL^}坻VۏSkM? "* hcHҔ0}c&Ꭓ94 &MWnjtӭ| |o~/E-14~9%14׾+zaۧOͣxtq8ȴ(BƨӍK:AEXc+.痯rkas~ІnS^_%4UPںdw}ŗ1qyvֱc~I2#eP)zYPJ2c@BzesR=_̦Zھ U>x s5g,g}ewbp/< p6䫯g>{ |Q=}fN7a Z[@P.5DkLṌֈ0<ȿyP3B&ڭUE`Ao{O멿Kvý[_Z{|&~ #^,ey8=\ܴsk@wuZS ,v10Ex綾grʎ̼edQ ªv0 \s0p㬥Cz˽sowt>1\a똸Zz:='O# 5 "՚@JJEBp`n4Im> ;#e_{YoIWh-=ɽYizIFQzKaS,R`%)D<1DJQto6ɋ90Ŗ$GGibk+.VjHT'BLַ*~瞸ӝ\99]k#I)2LK2&F+ >Os|WPfo;aΦp(;,Mߔ"2f[-n~KEpY9sC&!8sr9o?r̺>.Fȗԭ'=n֖^Q-H3pu{~hHhk~jN$9b$ zA_8" ZjR!`R"=j}ˊh.guF@ jWRrA)*! l07k$)ȣ|NN\zׇ5$}oyڡ;|8vzN?CIAoK+N/hc֒C;+߳w3; 7+IDyOhJÃOZ/BZ\k`#J29REIH]8=yA8Mׅh PpK5BӔYZk12 >pvqob>z?;wV-~/'n$tz)YI,#rFTJYn(E<Fjei~U<'@9 qη(x8ёRz3E("(*@LFѰ anyU5@v!vXt>>e-zILQR9##JuIꐦ9FJD\_@zsuŬSE4~!QjBK]A 0`RJ?Nz)TZ˟}ϚEM`M7gKºuK.mKɽl#|O=hė?9"cRBK ~youLIX}afzN߯D"ȴfT\tA@*$ה1^H*^y{yTkHn (⼉ n<{/̜u9Ow]S:-yuwpX;RszY[n @8(G7xs`[y"(im=6}z0Q(IRmQJGZ)"I5i" bHTc(%R.U)RԛQRPBnܽt_*>}hHQ A6Я )*vvg?O2g_E7C>ӚiMvJ2R)_Υ B;`U/Mx;z᧹6;C/O2ʥ$(hZ?VKn,8 2^ƥ4Sc=Kuνl.VV۴[]< g79۶ aUu0o`dy|Jc}H|V Ť~xy^|m?>P )9%GRkXf( 8{&z%cv_.6Tr{럠 Ṋ[ qpT1F*eIH%FJJI)ZߔK0\ q"oxul2NP*1019JH[:'GӐ[cqJA۶={G~08Xn29ZhJÍ\ț>8 jӜ]et}?KM쟸h+x:KZSC`# 4GIA(0DHڇYrJqfyR*IbTưizvOKVW[s`qv={(=ȌၣG'8Nl2{v4Iֱs oв=2r_:/N\ iwjdK2&G,&o"p()v.n|2}s_xP)|zSk uI@BTJ!TS)yR`#׆jR@n-qSVxL9RᜣRCI/XC h4fs/BpܸwNO?7z Z߁pRɹ9Ew?x|ŗQ.A[Tz *A_)䓿w*PrmEYA:(Td&*LZ{ZLkJ蛖e!JD?I)h39^+X~6k,#c%넡de; f,<;rm|0 ɵ=S>}8$cccJaCI5~xf]Sbky]1ר6LJ,n<+ΥLsI +<)F*N.oɑ*6~((J@ ^ĕ7~7^ryZw*KqkojKMLRRP)dR#P(Be((2%ۦzVjrQ(pԪ%q(er1,koWxc++仱֋+ )B" Y\1a%pߧ\s'BfFFu+(-=֢-66 %;"ɉYƦq^.9M7h3mx4ff$&ny;7s?q=^z66,Z )PK|LX]7_!'ܭ ^唢<ψ2PIs)_vW^m8r$jn $P^SaMD(;ϭ{6Gi6;(uޫ-4=xG=lCi{%H)Yʺ\oo|U\k'oc kbb܀rhkugma~zWOGSJ;sγJ%.ٳ4Ӝ_!8 lGI6C sG_v\‘/HHo#_A)ϿFUI8FD j)$ |[rbQh$jgo bcޝ4}۪bi JL11&#F|n !Vʼnc/ay 8 Z+€C'Ormv~7ko~3`?{õ[dz)o,5V8f׮N^Gl6Mէ15=Fe$F:qRֱxâc IDAT,uk+ƟLxaH׾Z>ss]\O h{)#wkƔ+N֝3]s"yB(6MjanwJTIbZ-y 26RdiNۧ缇6fk\B lH')i!T}MЦ)_~K*tr^JGR"׆$ՌUN2y$ՅҢ!4qST5I1B rmj:*Bm+9IAX<~Ýw%p7-PouBr%)"7;zhwz/5! $Q"p,4bK%&2nѱKMp^',f2I?AIIRZK\ u($(lQ-W0&ZuJ\b$VAβZ⪭5NR_dg*l\8]MS\\@H)-#}8oRM)͎dJ2R0>Zhu^J\ ]bfdq-)Jilq @3\uɸ[mZ' '2ߨLpFk-'b}I ઋM/itMc4Z,ֻL )^hAȲ*۫gȵZ1ưy8KRD-Bf N` P,PRR 0$rQTC(9T!`YΞ,o"b0a!l<%$BH p $3c8']aZ.(stcݑ4(F(5nQYZZTJQriu˜mS@yYRK]Xtd ^p>>W<Ě4 ժ^9Z)qgsӌ{;Bn}+)Bq$˹`$P%&*,,.Rpjn0~[8IpFTgQaiy)>X6U$NXX}", HSg4%< $R@F() #f% 333O V$5h~ľX⒌+X[h $ֹ!dsQ):eRA :"?Gku{V|>wWn*x %@ ~܎zoklsv9O]Sbf]5GeYN'0ֲw#1'THҤh~&M6aplE:i:DAH"6> 0<-hk#jpg LNr_,ټ\-PP LUIRBRRXA)e4Zر.)d UkXiwDMqʁgn@3.A`{-2q[ 8SCr/: Y%z׆v=_?%pL8X]GI]^(cJJq)["'Iq&jqHzIk4iZ'FGF$3$Ia-]\>h|>8|iq \G (A(QU T$ɆRjeWa(`iJSFrX(DaJ׵΂Wykі 䅃rߗ)UFkn*w?+ OÛߩP)-ӣ4{˜Z3=96٥:'< %sN/%61ke)ӇN"G`ff8vz ͼn76u{T9H\.W8qk?{`R8B08)ψ"S b`A[RohwLTZ\q7g\)=oGs3>fpgBl!ύJ1g*:ҲA ?K0*gđ/7J)ŏO|I gT1꧱21REXmvW$eG%C8x-[,1.:njgu.qj#52KK2@_x`3 Ι"Ӻ4r~X`hqZkrxJX"AM:׾8^Nkv͎J^Xm\ܹauTTX$ɷBa#6jQ,UTBgKuA O;Vfp/xH}on+u/.*~co?RrY(xb/ݢy?h(N;9D=zgm,v P?^en!yY^Zfiq$LOMt yFYNSz0k,ZK ڢa%.?GC !Pk5ţ~cR)Q(pV$}.:amv:͈.5h勍ag| Z1V5ȉh˯ד(!qb#a; )ӽ`[Yu_?b&r&7o~bzHzg `JJmyK6Bϯ X ɵv98 34󺯲RR֑V &SV V+MFO!'kc|0RiCf1`Ъ0d)QBElX\\e^ߪ*Vp5g+dPDZ`;|q0[ rME$(Ο,.`EB_\~ԗ cO=jA?y %W^f7pՖvpH!Yiwַ[`vߙV) Eu:vndVeѦWWb;4R+sa܁]d^?t{ +d 7I>` ;`*i CGt!y/e C$ nq-YNXwrCy2U?B)9B5b ofƃ A!EPŐvrg EguYC?pο}WWď/<ﶆs4N.a􄯀Zsx3f{=r4ϱ178RכֿAGf\u!O<8AXgiNѢcMR DQL9i9C(D:$'KsVQuaZ8$I{>\;_цQ,MX]]UoЙ["[GV%i;.TDžNckz=8Bs7H %J b[AiҬx}ȴ&ӆ,7kp`N͒ \8u*Oګv>_sh ;mgRwY7SJ%;x#sXm"KS8&2.8,5x n gmݛ)JY?C%ÀfvB:$##B%PA@026FerrfNmFfh$Է4AaqPX/Rʔ+b[!Xc9q8[e@dkdy!]Yֹ! AEH'r)/UÐ*@/Ru@])rjG59-+!x*]~=\}0kUԱ:6Gi Rpn96YjKqLxXl' N,,Fs\1&9S2{I B@KR!5ybڭA瞋]NH$abLF{=JqFc )^z}ee1-MVZ: ޭ Yw~߯+~uvݽgCqp~sp[(/Ͷ,@$Eq7An^\`l]{MNkuqE}'0LXXm>NJ)[Ƽ.fz|d9lWΙ)r{:6RJzIJf3{UH v(Ɍj$ Z>f\"Ks(D gfsRT@6zZ~B@*疙V~JKCY%,(y=Dq1n Mxuz`EAPLűv,O)?"i^x³ 8@4xoߍ+3j:Gpv *kc2Vo[0?{|J!U:BR)E\qNn\B9{_{?Xc,y.ۺ^Ɖ'X[Qok-##aia^'hV0h4ꬬYZZ@B4u:u$IXxzj0]%շހ&y*7$s=?vbу*Α)@+?jC?́ gk߽n'[BpҰ538`-7laE;[-~B $L1ZvJS*7@Ffj~?Hm{Pڝ>z:NXa X{E䊋R' | Rv>k<7f'L#:465QN =r]ȼ~q}/u@oy>ME$$xЃ ,WyPqYΓ%sւ"_rMlEM<~dR Y*f͝bX\;~2k0efY)>ٺm3\G T$RUj5V 8Fc KcD,{4?ih~+)aEy%/u% Bߓ5Zz@fJ8 HQHIs@ [T;^(Jyz0] En5^կ;?TNHέץY s$N-2R.1Jקjs߱&z]Vwls 6U@bY OXsr6w`͊s 2VCؾyyqon5rüW30w a(.yŲ\i OQ(1ƱsΥڼUW=5#a5L0R i'9cUBX\F3b}^T.x-f~jiۦqںǟ>ou1zٹ;L (t,*3\yl΁FMS:6obiԘϹrJNQ&G疇΀KpH=*8+:y!pmW8m+i}.+rU3L]Q["/Ow%pi!)f.gBaXnMn%ιѾh\R2c CH( 1jq@f RgO߰wa C7κ9U{Y !%%"8sdXCt]<*_vt9o?Eb{GW|nS/'/$) PK{q)G{?XPp.$>seDhH)<"Y> odca(0t}O䝉jcKu ]k(rE3wp֧I" m AjWMl׼Gf߿prB`Cg9)T8{l4+E hx-+t3y"ݰÅ+˱[3+ohӍG/p=wW+E3I{>PYcp`HɄuQ*^/wOk(ȴHCk?6t&u֝iķ2՛E;2^z efT /V(ơiUsMnMpp<^Nr,;Mc[6! őoclrww0@\w 64K!&u$ip1LkclzCgx6 M˺5Hf,^ЀJJ C]ӱ3tt'1p%;oJ O>sK.\9Q>*B IDAT<YdZk*LMu1=d" Nt yV԰T 3NXqZ"BOMRMqiѦ(+2 .D̽?Nƹqi=KqaȌ?0u 3sǪnyV3yKMK+RJ2q; J]Bxq_L8*:ʃs~df:˧&clQ!#Vgs.feݳggg4N;d)c>~.32:cG]0\*q*W";y7rΜ'ޒ3,7l*~ޗ=5\<.;cBlOy3TEv~TBj.ddB G缣I6"2RvK:6,/Q:,qbft.S bƎI]d"(1YZ/$q7s; )O_;a&5)s~QPRo[aqnڭfchDrLZst ζ 2ISD$)Ag7eIQ,S"ciXmtzh\_'rt']$dV_88YÝ0El$ͻ<^>D)9fb|Z^U!9ʂUæC"j֎θhS J"w> N1vm@٥#6q;x`PY]ei-II+ZqxyA ဈIIIZ\ &:!$Hx4AEYbz:1:NQEpHK"8X\l6sQd} ɯ>!3 dewQRdQ٘aAq: 4<& %b b/#]ouBpJQMJ(rϦ!hiUɪiɴ"V3@U3B߱[b_H MXjg^[hڞ;s.jͺnͨ3|+;n#[]Bz()RqRw"ز7v+U8G:n}yvf\htƎeDzy錪ȩr|¤jĕVqp>MoRE I]C4NG_!֓G!Lgi{g83gU뎃UQRcZq,Éc{'Cjp6mOg &?A2+ک;GIX˥~fJHkyRkSy4^rDlBk3eŀ=2!,tZՍKAr\Jt3wǙafxvv&'(%z˗}/S[o7|p:ÐdYVuKmx'Y"xo ,ccwP>ش\Q(ZP-&UJw'TBEqi(x̪8fr|.MZrz{ٝYRy$0stfCM;gsV̧2*rKEyA2Ӥʱ^iz|qH8l:Agy} cah I`ftvM׳[&Ui]^G 6eg 2)yɹ-?\3mMeHW2'%2fh'àۆIVH)ϳu8wjAmcO_!{7&JSw룈yJ=y?8]zŶJ ̈́EYu(9ڂ]س;xehL܏VeD+2{lcwiΐeMQf2}J‘ gzw` heh2S33t2ͳD:Z⚇ov\3$TZ iq.s;'yd&"G%JE]MagI^?jn/&t[qph !n4=5u2e;@%11ozKѻqta]\:8b:ͦaZD 8X66lNM=>Q2q]cׅDǓ x8|p.ptyә]3qNf<,*pݘ sYiZ&U4=iMB0Jތ$y=ƫ[ú- HUΈ|E麸NtE"7`ZHؽ=#N ZDCaI;VuOeq~w4O༏MRl>,!ı0qP 'F@gƎ,5YE^ŀvpi'xG<2c&{ 6v5]O5++Furf< tZIZ%&(ķ>Nvy")t r ´*QӜD1V dNmϐ:>ZBE+]<` n:)%eGMqeVwQŨhL R!, Ʉz9 +Tkt:t]C=WN$0ֳ,"ZZL%:t jtq-*;$'kc2atI|y1-1ơ`]ng9`Zޝ\74]Ϥ,X7pUwZtVp]t3)(hlU:}lnw3-{+iB`>-qγ44kg>0Vu+bJյp'A8Uc k:ѺK+Isngʴ}z$dqӿ'U`^:nE'8o s ]Ylۋ6cIA"-eYr*])CZ9"~{1+u8mvSΞ~o>49G:RǠz˴!qTϘ3}<"J!2z9W&U~-\͸iFl`< Cug蝧N|O<b̘-C}Hdq1yϺQ#L qB̨eل6q>gӒŔ\+궧"ڨV.[f1Tcfnp3tƦyZ c IԸUR"W}ϥ%eViZjlZQhlZd^j4c`2e;F\ i4ʲf$p F%ӓ>TYJC _X&;&k'C?{l*22GJItlL>Eo5.zlzCƮ~1k5 4}xDI%Ry!Č6)%ySھ7YjRfQnNՆ ⼧l~D(Ԇ:V!Vu;g$:úYGoH \-mӍG<)$>TUMK3'NRi$+kl'c93$e4- r`ci"LIvl~D^7i]Ay'2M^Z=l+G6MGklMٚ v>PˑGYJX2UC q0J&Eܥ^9X%@,lbVa]`{1eh~Z IX5Eo UY$e̘DBLc`i4G:ꐈ@nq%dYAc:B`8Z_G갢2<˦cnh`ՃDMeۙvB2-3NGD 6zQq,nR4I.lp0mὧLYU6:m<[UY5JvƑOgiT͌<]VU'&ENg4mS|6aZ5:$sѦ'oQ%d\Frq*~B[Rvwd*}"DvkQ:6q'Rmug+˯ڎ{;>Agɋ"#{k6aSYdӪ˪eA&g|>JQ 1/ll^Zʲ'utΐe ! x,˯̀( ? *^uC:B*$XKQpByFe5]ۡ o gYF۴Eٴ B)Loȋwx$gYQe%6x$_}.H3lo-rMO%:/h6A!<w=MvlͧlukMR ʪi`SoN&VH$eh:˴AA뜾oAh3+kkX3U.%W-|&|k y K^J{աCtP82.<X*7RҴB|:ԂbӚtcdtYbUcCZBY{{KY -X̦dJƽq41@tS9JJ֍e6ppzg k {$?Z"ypjk>D^M]? sτ>G, @:˰ƌ*("B!Cj3cby :S9YMײ-ϙ^>$c'jg OowE{l w,s BPN*J}ht2k[P G8b<,EYIP*! ,E 69H~~{nCΌ3\ BR( ؄k[I31p9RE bM&2^pyI5)i*qrzg)5J|a SPΘ(oL}k@EkPX]ـ %e& BZoɼA)"2@@@UVuMH%GMK;\i>nUɥ_,/x/`F8P]iDyB^dT}Tsޣp/b >7e+#:3p&G )U׉{Qԇ@T 3MkIϙBdSY:2|#uKWp>n}VL^}UYUjf :]0N/%X!|gx!CGWc儮ky/׾BJ8 |Sfb22>s!?bߵ٬X, !pk/)<}v\ 81;R2IG% $zӊ\Cc"*F@9=pp>9D,}c0>5q2VYJ#E\-äR3<+!JȓL=czx3W̏<_aEJfĘ~WyKs/.ⳏg[,i6KYUdY}&eC}bi6L<$Fq<`$/7虌K IDATְj(άtDeztOKfO_&%:ϫMì,FigciIH jh&9Y"㽿Q5b=E5ҵRB?RfIQ}GUN88CJEmGϰ^/)mP:ꮵmKUMRҶ MS{Yw_cs7.jdy3\bN<}Im&~G'Y>xqv&3feƦ3YnZ꺡(+T]OI6 ZGXzNW2>ޠtNeWj^HsLGos_+NsNY軎t3s>e4l6ʕK8EQƣ3Mx>'A&^HDQoꬩ[&ӊqV[ RH1]JsTUÏ_ L˨>d g{zƳM3l#t3qP[ eUb 8_sc^ſfu,9:/</ oœE8<|T[>{;F9w/30wE^٬X%deڦmk[z[Ǐ`'uiM6:yԨkG@J gItRw&9擜 ΍ұ,cRdrѺ*K,e\_uƐ+I qY˼] ]`CW.?6k.O>Et^ʷ ! ۔xػ<|3^/QR1X.^Txg! =qϾ3|Ⱦ1c2XGFTH2͢Xo,:9vPVT"9yl:kgLmkV⑘7os֛ Eaz=!(JĹ]Y`|[;?ۏ}|'8HA]~ [VNA*d}$8 oE(35| >LzMQ8ђɤ[P dy5=Re]CQ8gp^ ([;opmw~_KFRtklm(wo>GH!Q /^#t!>lG$.r^Ȉ:)KGi>šڎ(aQmRMJ~fi6U)˒u! @ ;KYٽ[o n3ٴpHG(;ٴwBB +rH8 #8 ̥}f)B(  5 Ofxol SPYalYt1o~7o{/OPg'+Μ}(ʭkqyG_cExT !L=:pF?/籀>'y)JgVyA4l/ !H&UYPdUSd(9 ڦmhvqƟE|=χOЬ.%+ q%'s#H5O]C g!@; 3rjg;[s0]l6ٛx/?@loijp]Li]/{k-w_~o1CUYV(ޡ2ƣe[{Ȳ)wXGL U.ZKJo{Ʒ;@.^ZxLg jBQTVGL&|Os]\r^ݯ}?*q).2lEd8`h-AdYbN\;*NT%o~ \`&r5#\a>[o^[tػ~7==<{'ORR IZ*u'MȢo|isO8sS:sϢd: *'pכk$'o\BȎɋ/|s y1g^Zr[ gj#st=~ҺԦh)[m]|0?o<+Y^\ w3&JMMSwvv_r=\o|YRU ^whQׯc׋oz]u=_upׯy~kΣPIENDB`PyPagekite-1.5.2.201011/gui/icons-127/000077500000000000000000000000001374056564300165275ustar00rootroot00000000000000PyPagekite-1.5.2.201011/gui/icons-127/pk-active.png000066400000000000000000000222261374056564300211240ustar00rootroot00000000000000PNG  IHDRƽbKGDԂ pHYsHHFk> vpAgGg$#IDATx}Yp\יwν $ \DDFؔ5F3#yRN*L%JRedjRyIU\qDVٱ-ɤ(3En zޓh @$[H=;s / =5Q3+QT?#~' tVLK?3_Rd$'ÎZҜBt%ap?* Hr@fп @P ɉŚN!ZAt30ءkm5 vBA(X\DWeț|uyZ$Z׷`8 cBc.F`@j9* ^C ;qÉG=vL74&㻰!IR+qK`jZ eĉVU2,?9 t*- v31efC¸bqwM?Ko!0 SN+ VZ3qh ڨK@J;@u%#f PAcXఁ;`}9Ah-ZG *뾀DP lJ}Uu y'`+WgׁyTR e 9^je:yҿP}j9 ci1VH29 c&?`N[z`>ŏYV>s`=k CX!I=$l1 {YC[LMHUj_V'fWI xs͕5Cf5#LoU3lҼ4]JX ,EZW0W ._1f ߍZmHiy͏C~_|gBtF0$CFlŶ""w^bZ}8ё yq,ND]Bb]Z.h6Wk셬x՟UsV] p8:T9,\ >Bd6BNHiĂ+:|`p\*-ޕ{ Iկ؎%H]d1oZ%@wb㝈 Axj1$b_fc)4"x-L o| ֯xeے|2)&bw  ` ,bS CAdQ4b~Dǰz"^F ,{@5ڠ/xH/kѽ b0 q(K$OXkPH@, [45$bQ݈u#ꚄSIT$;=7> 6 |ĿG-3xMgrAkބ"sHֵ]=D2JC^A^Z4q6BcY@ד굈^QȏM!v T>,' x #_VLە|1=ȱl`2BnH3NHct <6 3Lo$ jU*gY #p#l?xToyMHZ+lR+XØ9fG!:{FtQO ,ժ{=T LEBIyyßOe uH?g_"jG/]yڛ;jˊn6 9)燎e#'']45Dg]. [~ \euP[[Ao08xA(W-K7/;U}"_UAʲM> a:#BHNض NB?CAd"A0O ġ>Dd w$)j2m6RI5x+767Mrx@SJ(H[)WVp9і>0 X4"?}"s׎Jʀ~X&!+y yiё Ct CAG7Vk2koJ)-)/5rؤSB !y>!kNk ""J۟#qB=Cf*@@Kބ ۉO.D³}$w#<#D}F`)@Jt&eDsź涶Ҋ bit#%G-ٙ@ùh=t%.=YQO $%8J!QO;vT^^e=`0_WO)Nm :`k+}"^1-3?DN&U?>r sئi$H7j WG(G)SG%'V`gVB < !xAaE弭q%H5L~#$+x=U|ZS3grkD u<9$ZU0Lĥ_Yeop.ZK2sز7X^tG9?L)B~<_pލ)/`?w" &3zIk0~%ܲBzG*!Yܽb+.>^RVǮl[zb|SSU̽O$'ԫ`SiU4ZO?:L%Ȝй%획 xcsZuzچHRI>ID enoԀ[gCؘkkl,C)kt ;1i+0;5Ж?O\$I% CZZqsdKѽm ksrBkB_\VF)5"CG!~V@M`)<a.%ϖQQhX9j/sYRV-Iplފز?-+?hNWnollFOuk>P黰!unC6$`Hh=BhQII &z30"yrlI>~!5'`k~$%o|?{3S!TY[]KKNWprWEZ9ރTR2Y~ 5يOWVڰ)n׃ 9Ou Ғ(IҔV݋ 5G >1h|0!UwLD8κn?ds'!k5&Jt0:φkvP 04kUlG߯4hqOd㳡gSY8Zd_n`$#\(bitz}E]CC|=ws9`N/ẟ~A(1Yv+в !`1c,呙ѩ񙢒3oNw8\U[2,Jp>C3 П#$l? C/3m_3#wx|1w;-1DQ=ANy=yܜ;G_yeHH|xSK'=ʅ **B׃k>a8] ,btu{E~`l-%f,b ~o.,̸}^ok'k2]^\ʚ{*lBj` ^` @9 kKs:>Tlɓ,IVFX,D"Rs:SsK^'{Vѡ*1Z >j+,l "*?Xh+ྣUt$$1UȪ DqDo0(U,VA!Ɍ1)B9Of˳ 5-=ҲCIRv2>IOwblL%)ʐͪpeިCzѤ']l=vE;a6kߺT)*W~C=;T?tnD4gl~  ,]pC\ "`R=1GZvRU =.ٷOM*h3~7cCC;^D;!9E gAo gR+Z:k>1dPZݟUWWX:NbBRMVXXx.zܿukɓׅ oj%yѳr6 ӑBtqQ ܃F}t@rs|ŮE$k(#z\knbb)ǙUUޑeY'`WtCn}S:>FP?O9y;z[z6>pleyF(Dn*[z'vuwA!`ݥu :l|hm}}PMbB^5T9@8]Aԯ ˧E^]: @ͥ?xƆ{~~~)NRZ(` '$EXݻ m'O^((,g]e+E=f3]qM_Nxgy+/?sرKw!>yƇoOs_wʫ :M1U_ `G ~zOkUR,Bg%{@ yyy$Cum=0`6cw*eKS6]M--9Q 喣Gk߼\#yc{0'= =.2 Y]4edIP8,#5mGqPdJn!cT P\pA+|o3)f ^@~οXэ3LD|L<|:,))Y6eI7ÇKo+>y=fxT |Œc{!gUB^E\Ape9iLR7@~ԕOnS@p-Jfju#W'`Plln~I.8 :''`4*gUFewM!6ŸOx~_|Hj% )4~GM>S6ȥ> H1|mMM[ժـ,esXIe)@"w17~  xP[xx@TK733$E7=r싫W3YOg͂^Ahy6[kiiU @R=(! ^EE')6E+Ҭ~DjJZ}ܻsҠןjP7FqW{U9Rl0OA'qfQ'FGJ $in %?'srRzGjWtHLyVO2DQt!iPJLScg}E:/-.uJxs퍍u|h$DC<;wB OQxQee)8>EQtcS`d6W8~ә65z|ܼ6{CC ޵' ȊD{Z|N};In=hE>F.-%6 s[(̽r\ W63XX^YY IcICptO'|M+޺~d B.2-쯬<|P!2gJKdY^rӲ.9޹ؾ1Bjruئ&]ʷ}˖V}>7c,]\^&_9q= ޮ+kj7Dd Pȍ86bvT&BXf-+ZJj7 2P66I37׿}u/ي_N!$E~y!g†XܔզC qvY_4i'b TVT[E{⢋ɲBB:>QmqKt"Xj-/FsAB S׮_3?y<уn3&0Q=ssw˗zzB)봲3O_-nHf %IZ؍Nel\ZZ^^RqB^dz7>19 9A?RoO&+ܶwلVk> GEqlWC^nnϿBWb}{ddl?!%)fftvt$IMf Wwx@CG*1]`(}̙0R >2s+t(K 띟qI+Upe_Kw]ի䳀?bZmRZUQZZRrqvnNhW ߻7ZZRr֒te/ ]V6yĉBu&y'Och$ Iҭ.] Bq%jS:LR L}^_ӧ~wbc,=Avww?@vP#(*]A˴}=hq خhJ!TUVݵM@s_X\ 3YY1 /,, 9F '!(tG_B2xb3k ҷ 7|H$D(F }߿ۏ?~47?6rHAG? !,A{ƍ裁x0Fg ;">)b }g\0ȗWjө_o) |{`$_IzQ _2 Q~Uzt%E#j_V@2.NjQQ 5N}9ge"?}^ UYt }G#%tEXtdate:create2011-03-23T08:49:11+01:00]~%tEXtdate:modify2011-03-23T08:48:27+01:00tEXtSoftwareAdobe ImageReadyqe<IENDB`PyPagekite-1.5.2.201011/gui/icons-127/pk-idle.png000066400000000000000000000212011374056564300205560ustar00rootroot00000000000000PNG  IHDRƽsRGBbKGD pHYs  tIME -b+ IDATx}ioY{NmIIDBI^$m_OM@$ H K>_!A>dLOw&sn-mDp'XU䃊r,KfI b=y[{[p">>?у>Ǩ`?"51N49a5:hd#RY#=ݴ?>h$#Hokw4}u`h S3-rZ+vwƘ^=yK6Fcj !H!7N柬,GK iPi|l-v[M}=\c^"0)  u/1?x''-951EǃAx${X115j_!et~UY9 wC^lCѷ0FBBcX_`*쟀|Gzlá'X6PK3#."!حHD/ҩ74e^D{B==c'}t/!uNUAhd8Evjd';jgQ)a fT#Bvl0?VJ8P'~#N\\Q `0ǰ o` Cf8Yje`=c`({~4s)6 Cv .AI }!ϻ@!?ELدoGo81 3rƒn2MS7hL]5#go* E B>~XzS(1=t$#q>t8 "cH1!R8@]ŝb1͞hz؈"ۄ\C37qʨ>R,s6X1yH?? _XXc8KNj8OĆN爝nјS`w~ޏhL}YI81@㑨R,u,MQ5J!<E细ncj42[Ȧ8O8Bp!3dʣK?K㑁 Kc~l?}ˊBQ{Ρ}.eo_gB }Ywogx03?({\HT][=Eᾑp_3mo:&MU ;1x{: iBHJe`5g4!;1??EL 2-Se>KUT][:sn#&|K"+ʋs/ EХ6駭Zoi,/NN?xv<ˤO4򶮓8uTUaH`n/(f8ؐ &lVr?[3M_I:섐dr3J[]fi2>1u]oBu"ZkjAj6xIW+b![5[7^j6Q ~cL=WYCSU6j[HTMiw l覮k|SU"o7$ysZNJuAy^J&Yk 3ĭr\6KɚI[zq"53ZuPT]If.6ֶ+R.6 v>uUŸ{_EUG4MGÑ;?&gaoU|ER3jOlhU5є3,ebczm;_yI`o[ݺX_`1{!8}=ەrN_ת{SEjF>-"Sbiz]TYQk !WiZuPk oQoY9ZL:}k]C#ە)j[qRq<-@XhRSU,o뵕\ڪFNQڤu>ם-UPb㾓!dI_hiaĶQ)ӷBT]'($J|}e\LUxZE5"|_G/s=YB7-޾\ț}]2HfY۷Z#$)o4rZC|Z*kUBHtNcu:^1HD(獐μHk '~q8ny/&6J\[Ư+m N#oSN$Y7M9+fUPת 5`eP(dx ǍQ51麾655?ΙFf[$x0ˋOT]֍ǖ} eO `쀘ZVuZIT|_nKەVVz ?Մ(*pCCC==}}"]i:doK-ZMSE^Sá"~kwg5\EN|d>1BT(Ųl˩P/WxZAaV';}|#g{z~8t!w{9CB ^{-UD 7~BCLz\;F|* t]~RNnWv*jk{v;ӈaccc@`p\wڵG {`ȇfH$5-ӷU<:]9Q"S}nuaҽo|]5~7a&g\ 21av˗/pf4aB^FSdrhG/XL[>!m@lYjz@֍S7d2;Y뀶L"p#dXc|h񂵝Ag 1ޯ vFT?= QWV--Gˍin7H$\h4t:CǍr1Bh !@agډ9ΩʢqB@[u-Pj^}.Ȓ-mb|[ "%0M䤯` t:/s Bh!4j\pʪM>r>{ޘn8iFɞH4s8#,ˎ4@%0C}>Z&(S*&[m{g'ץR@,6?Vmm,uq˃a@ 7KsMg,[}f Cй$ y؂(oY{ϤUQH?K@o>^766ʄm{;1ekR顮u>$缮,Ƈv]2MgT]{nBmwL]%a'>Ȳ+{-u$_UA`9Jf N:K1T_ϕ3N,_󻮺lvio n~^u#S#hJۍ߽/ҫ9ESY)Nv@tmiC7O(R#T?=}}(r\* |Is/\3EG-惴S>N蠉yYg3?R{k=N5_>() b~;gQi޷Rm,;v1x\C:5v 6jB*z`g45? dZhg&FnN̿6hS+i_ bo-͈?}cw8;5ҽɍ\Yj6s|Bs$Ik`Q^F0샽eջ_J_cCMY &*IQ-YV0x<> gfM{hGj.2T'TZS !QӵZmϯnmmmNݼ(굈yp$ Bhڈ[խjU~z7[Ncl;@kUQ3F#J+Jr\Ea<}yr؊:<22d2Yocz=a-ZtNvT{nOj/_tlBQ5ME1JQ*KRhpp&yVVu:7*K[X]K&bm,k Fu﵉JCn>fE[5ME)6͢(R- EA;v|.6 +"V]^*˲kL#$F+a^@f.b!(JFuA6kJX,n RRS^٩;`^40k_y<(j"򣞞6w$_dI,/{zx%X_C {t6a{ssaXkkkMŨ5𽩄M::={,3~=a`. ͕ۼ~ \zaiV]n{B*e~?ƛl6[!ۄbmh:%$cn'EQhdd/~q'?Ilsf@,\i1KKOM׽|hocO4 S.Jc2V z6胺x[YbwΝH(J\,ޠ(* B7nmRG@Ϭ=EݴyH$?r;BpTB}I#|fcҫ,*uoݖcnYo.ށ>v8 Ì4(j!iB} Ƣ{ sssۗ&&:Ng`` 2m1f8~˱@{e^61 gؿѣY@ |tOO}h4t.kfݠ("Bh [D"y2,:Q#YE!#D=4w-UiH]X4LJp_/0>I/@ e\.Ν;h4znEnR55ْeٱd2l QZ]}^PuBFÙL-Ԫ31 [{0=ޯ *o`.}u{40. &nvr+E fkQM D&&~xnYX&I6ZvgV˱@;3ŠM뺮7z.gV{kk0c EcacJe!^|qӛfS~rc-[ E}ut05v&ԔJ|zxq=;KbCRqB6S^SSSr&:]d23H̰,;huxXfdM]ES^D4S|\.7z{{KY3 w… gϞ;1o=a,h4DT,dty(b6$]^+Ԫ%S2iJImgL T?eM}CBgϞ-V~ |$I2<2Mӗ-,I3|~mς(܈b"@GXa5?.[/\m˒ iΚ&Gk -a>zpΝQ|H$2p8=v\A㸿]h<,tϤK^|࠸fu]EYΖl&a)MTLڶ-P(V<<ϥ/?y;I./޼9GQ v_xuy~#+M-B6UxsځI'PO:}UU˦DϩcD>rmm<RV\ Lx/^QaS[%U4.ZqY~fIOU r&&&F`;AWV5M:nT, 5&Rx^ԖV߫NOS |j?ՃiUQҦW-Udh|]|Fm^CW,oz~`$*rB% ?tժ4!LiR?,,,,#;\"1\2%krO8t4 !DsGɱd2)ݻ4]h]wti:Nʲ\1lgn2z=7 > l6)rcd2|?=N7Rtq~zd%^92uf2맚1 o_v^Mc_[]z5IQԥ#U7sGϪj>ќq#cRRXV߼y4M8hv4Y0- F 4G&h~&MӪJHR$UMv?vAb2?Jc* ^x<_LR"8ҽ&{Q,eR5Y[) wl(i Yprto:zÞ7%I'sܺvmASRT!DE1g,ˆ]t/_ܾ<95I7׿[ZZ+eSj\ӎr}_ODQ0ncCе+<5˲Eije˥lVM똮:ЭtZN[̼aU*t:=luM}L&3,K$ͩVIwl6Fz{V0˲>η~;T*UIROAe_$ JϜPIdB0Bۗ/_n=4jcoCLxgt_'Jeiuu_f^OM?:|Sș3[+W*|rvDGN3/FAQB >}_hQGpAh4rBј[^^޽{I_5(y#tHΜ1V4Mӟz[A?Y1}П^=ztERA?dUkkvkTAGe_oZ"A'砟K?@IENDB`PyPagekite-1.5.2.201011/gui/icons-127/pk-traffic.png000066400000000000000000000370341374056564300212720ustar00rootroot00000000000000PNG  IHDRƽsRGBbKGD pHYs  tIME d IDATxY$Wz&Ě^,EӭnmF3x6 ɰgc/1` ,խV"&$ŭX^w52s̛̌6[Uu"3#odf|ٍ~y ˽ϼYh. 9̓$_5\M@< +_Bit?)g5~ȸzS¶ ʧ퇽[x=k]!vֿ![TQB""htӲiϭ//$?}9;9V8@"B|CK4.H$O<?\\AqC!j7/F=tN#y<~A%h (l/Ɵ?oPGK@DdDؕߴL{Izo#RY_E-Th :h[;( y9EOVs{0{| yvdR=KVoa?ww#MDȌ´l1EJL?Qr*G;Zu}ۑ keV!*Ysfv+:Y#WSa3GҾhpr'\ӂS3ŝL $0BQٯEs\3y;wJö=[Sr*`Cz_AZ'ss,I!Px꟢4V||VBuŊ_XEL)GHLc+K҉QA7XD,t[ #=d PW/B>-' ˿'g^=^/~zƟzGuϞΊ"jTS…kABPH1Ja18eHEpDzAл߭~k,xOrHdS $D19=;SȊ_):axl` pih4pFj[!GG/X4 =& ?@u.X*CpuS?~NLשtv4?T) 1!RC:-<I~<7փZu' H~Zp翃;F \=P)#Tg^3 ),*_A'>,<3-AO/d)]CڏV1{`U4DkTFP;e$u(!11=T)H+кZ 2)xQ/o@wQݳk :žqDcy^ʁU@G1oSy@hXwL mנHL ]A2t0X•x !bCE`=t:D?Ǝg='Q='P!/,n ۈT f @$(@W4Q0]hD6( &cݿʧ_M[w K |^?0ovE JUf6 e "7"`h=8C].@%*bL@5p҇C@}oA~˻_6Q3 )F8FNT^TJ5@jt+3tpn06P.  1 I@$UXPDhX3?<ס^0览8_pPloXOq+ONJi/!]&!@'D$*gU0B}zk5g ZxH4M?XW>4:wa!x F3h),8< oqJ #ыb ! \e|̙x0$"03I" ks`v̬Yz ;{ mݍ䯣R_[Ua|/#4{(fTum!^ 9a zd Bά21 k7 XՃ ̶ 0IUGkWn-ީ_Mq{x ,,8H8@;twS"n#87 3R!=7;64в(u ~$61U!:[w >C'\ul7,=!:6]؏_j#ֽ̟gέ>?_gl ()lPHl!$i;%0 DYįv SHf?P߅Cėng_ۑܡKQ9|S {P-!,BU@Agn < D99ʻ m%p7PbBUk"NGm4Cڅ=`zv7,FbA{Мc83Q0 [z !X6,@/Lx6+p=h K{UdmLoy=4Y*Ao5nH|h=^:=ۃji,Rj=ӳ ]o6 4IcVG\8KQu!PJ Em%O_B@ $m'憟s K.x mМX٧~<<`Bq V.L3kD*%0/kp*i5V|Rg^o֋/A^79_CێЛ<]6L4&4+VB^'J/+Oh?>X&"FxΚFޝro"I Ac4@rLtd:W:.(n:2&\j6 .CZ0'½7PlkGl9 !L=υBU")|30>;oha?#8prςluJRHF$\iJ,5nb B9>LyJ[ z+7 Ѿ-1|xG|,/OCDZklF[C.7t @Jv ]"liN}"E=ᨎ;Et[Qafy eWZÏ\3џ}̃| VW.A?#caj~FY^u9\Ea6ޏy: 粆1z`s&`ua ?J' |S bXDa4VgD3?mvmo!~ Tg;D%&/|<}f;}O-.Zy $ ҝ,Y!}}̃=30nMJD}&S(N] t~7)zh$*DCXAśkTGPḡKoit`D T;po:>o g9>Rg|rϺS{HZ"1s~bxLm~}>P!)= $2O,y(a;n8g=`cۖ׺!{5 >/oW<[潣[\GJn\m]+3"+a,a䖸1lBjLrҗ ~ 42`r\$Hug}ԍM*`[IvFo{+O>#uݦL| γu﵆-#s9"|b쾹GwW;jPrj!5 <Ī6Y,]Ls?ƤE a9Ej\/'qqc%<;Ϫ JG*Lp%ece*:7(+,<}W߯]3EXOZ-L>w,L1qggnM .bʚ&yy3%X$h 2jOs#5R ,y&#~ G(;(YuhHu caDF@'1aK"RWΝ+)ݻWlHaFvҼ_XCM-_?mj' ?Ɔaka} l ֭ PYZ*n=,4 !Eˆ d`8W1tF?ZwS} X҆+}H.Z.DCFLND!D-!6;q$ہG|GT=?w̅ D92,pY1Gݥ,ø%SO'}QdM 6VW1L̀2):ji:Oc H ¾ HL tb ]7Pњ[ =)$!A.:]:Q)"bcذNQrJSĉ{7_W[Nqˑ41(NQOcXƀX|vh/s9ԊEDP6MS7L1L c6X`kmj>;)%K !+ʻRy ,o/Ui@8 K$T՘4!Or츋kg6.b{lV$b" MAaz*:4.h(}85gS3g^dֱ6Z)(U)@`R bv-MݖhU,ubĎeKn&VIqL':B{Ht6S&jȻ4 (贪hެ"JMV, ҂vq\(fVǩ4G7RN6VWA7.A~K~-h츥b籯d{?]ʾ{{\m{Gu8Qs2)*( K,BQ'ȸ^5C; dQZ3n*X,`ڛ˟j#ʪ+"ٜ ya1*IHJ5:ꭢZPB .UJ `V+'{!=D4ӿ N&8\$l$`NmXQꘪ@H^"U@ !8L(Mr y PlKs-1(HBl9H?3x͌{a'(Jdb@*0̃bk nu 1o Pn@(8AjafCn"2,d)QD"7H" ^vfHp=-}aU] J;~޸kߊZ+̼AHo=e$w7!2\ ߷`9XZ#Ռ$ 5)3lX+ ® X R hױ:6l,0Ӱ k(=Yw5ElW.^l1˷CPʃ7w=0CiF R5wH^S@A3aޯMv HT>w?S;E8_];д5Hya6y!Fuu#w>/!*sM|m|q59_e|k0ڃ 9{Ā@MCfx| MD}|ng}=LZmgIn7KQ A jbDt*;ݫ2X[>'n2b ǃ&N*p9w ]x}hMTof!.ݫ*s'O٘[*?|B~\ۏl AT7^yˤ } /@M|kkmj*=cDc>誇ŁQkhn" +]*J;Ǹq~8ML.zmۥ~Q`"Ba8lƘB`84jL .?׿_yGƌ;[f0a\&?teۖ%iT|"B&6$<1- TXC~''-o%s t7>Lhl=snI!`<0iE᣽.o g"HޫO}Do%#:lLst:Ճ֮Xnyp~o #Є`ɌnP q>< =<F½w.Sѿ]&i:T Maΰ?oPZ|\<75>h1 :w6TFyU!hqx]½EΗ}\t'?ٔHGmќ!d#<8ȇ,LQRqb7)~%q"͂-2,S6 =kFE<69s ~;JbԲmnƀA`K%ۑp <( ׅCps۴Q˺x~;Wk;He>:W\[}@10T 0RަIDATdcȔ"!R! 3331ꟂhīhQtq6FS_EEÓ>,r -ՠz yWXYjh"*WKjqʖcyҖ-0Q䫴0FqLA1;je3P$+'*/|*G88Pzƀ {H9:WG`"(!FČaWf@+H]m+]Ao&z"5Y~#p*#Mo2 1 ^$q[zuSJ)9sWY ~kX`fL=]%YZM2U=Tʮd6DFRFd/1JM%HB Bf1-d*.8~8_0ʾi$%bqkI u6@FqFWJ#Jz3VJ%ao4+d HjWyuoZ\ {{8gnnn_R[vS ׅ{%&@;U嚘*=J0g L̅b x˒HR8y<~{Rakgc KÍ6(TY6TE!Pz`d D Ìe߅2ז"ҌJٵw/u%IX 6FMxcG29X󃂐~HS٣a -}B$`KBɕB,_[fV݋{s Ҡyۓ+""Cbb(˩ڪӹ{"Tm׎iw]@K!hԺ/5ŵxcWkȓ6#eź@3*E,Ζ! iZNd~mtC5ظ dn3^b羫j p[ǎg]R7,|ϙضeYD*()./TM(/0bҭaYPj侞M(޳T^/Rfg^wns O/[vji#h?~|AKnZ JeY ױH{Ifъbװ:|wxBM g6%T"qk7;W86o:.i64DsSW~KWM*[ϖ%(Lz BPIT+Fl r:>DB|tCųXnnw+vp龣\./I3<Ͷs<}zKK^ZkH\ -!h @Ѽ}}@Dp,kۨx6\Kpz A'Npu򍂎Ǿ & n1~r?Ďnr{)(][LUK^wkKTJ6DDl5m.ȌƕF_|boM \ eFշ`c^rch{$IO6)ry_ӽjWo'?9s"҃&iY~EN4!}!S+ HId[23T CCLQphЈ)bױP-#`Y#yjo{\i7{{^uiyKNDu(ƻ 6;U !ԯa6Ɠ?#=VHyz\Gٳƥf'/kouC~_$IWڶ}~˪:s_<$l6{.O=ϑJ)}6LB X !$[7<ƓC.@^qQlHb$qb`Jzf':u? ywVl3gFZ<$BۻCDK^?w*/\YrMZwϢ6C0VH.]ԩU0CK`c~ʃV k-7D8kW{AfFk=|$Mo:6U}ſя[Ԫ|#{zz_Zb~BlˇHR0*^TRܺnʭv`X[z0 _É? Ͼ-kb?\|8TVF9"ˎ /0-1q="UgeY~3<džcK(cZX3elTŅ%a+ӫ띟w8Xy~l#s~oϞ;v|Ҡ1m^nW'ڭ_;wYvaYhm <.Ow<5 2u+p$ayK tR7_0Jk xn1? 33jc{[ƛƕ]Lxݕ;]553gjVu!-Kxv$B%0.0gERZpmG^mtVp&M_(|}7/m'>1wBϲRTj?|| ?ao[ƶ%{mD)UڲlB (шSf FΠ:2 DZ0[17óQ FZf'^cFj Ix13wmDD>9ֳ3}>:?ݗZ\^^_n`c3 1(gCsS>gJ%ƥkkF42J}ev;2t&$wϞk?y1,Kv;`[OWRH< *~ϱΚ [Fk4Q(Jigx g#f<f n=wş/'0/kjntYcz$k<縎lD3 '!WXRVq1UqXI"hֺA|ۋߍcp!x zϧl''C~Fq'_~ᥗ^~vEVV,l´qJTJkmWSV*=i ojL̓Q kݠ uz;^0NO~eA_|96Zn9{?{|k-gU״~{EJjbZ2<-\Gga#|[*Q A|6 AhW?sX|}NF>N[ՓOݿWNMcLa릥8܌*C7LY dcI^  A"_-%+J[Q F^ژw)18n2h -tl6/?3gVxXq}\\N ou__rtwˮ\ov,Vc%BdóqBA/V; WMxag $888?`NAdmx___{8;7~AO 4΍~&={l%) ڹ0m1U~Jxoh͝gn,V{nkW˯]wlxّ'p|l6o5V=+N^.t{dtBЃN\4JgY<4сٙbnl'+|GBʬK'Slѵkkkx;7kkFE0'z$ `~=fQm+@ZF[oz:=)]=)K32CpWFZ~K̞hUYmvԔ) HSvfsjY[]~?\x/̿OH b6zIl2 [#٬on8}uuxO=wnu'c^lU}+F&f1a86stH~h.M5fk];Pxhw#\Z\_?5^ϞՓ8  |7z1^? |uow:@A){^oNO iEc`sDp& gNW{73 (J4=szpsim\>\9r؃~+Iq;v_Y]ؘ݂*~x[W^m`^lH0yơE$C \ JhyEI Zҩ`;8lZ3-D<2d 3}8wh~7_usiu C.)ts9V[ Cܑ1бU*lۓWO.VneE2 I` I@Oz mwN:p~ߐ[ZZz^{?/\X|eo? L*|1rG{ $.)t]y8?4A̛fqbQr4VWW?x_~ҥqq{:Aј;DR[=S T.븽^XUSHǹ?cq_E=Ix'ۛr@W-n/mVK`n^jDwHD俕o&|{k_`攈,cLlK/}OOk&ݶ`):_lq\Ȥ' Ψ3y~7?k:ucjĉwsSF17vF[q,mb1vfGsXl |RO/u٦wDܤ;qnGzJcXLѿYŅ|^ mw f.Gøwgn$]wM½,‡H͘>ϒ2Apl`tsbK8 On1Wp3}+K웱jIDWqj|^HL>Iz,͢6 5onx+yi'ApmCo? 7IENDB`PyPagekite-1.5.2.201011/gui/icons-16/000077500000000000000000000000001374056564300164445ustar00rootroot00000000000000PyPagekite-1.5.2.201011/gui/icons-16/pk-active.png000066400000000000000000000017621374056564300210430ustar00rootroot00000000000000PNG  IHDRabKGD7A pHYsHHFk> vpAg\ƭIDAT8}Ou_AЀ@0%Hp&dŃ;y_0u`L01ƃӠl6aD(Y`q-mPڧOE,{_N#W2[X=1dF@Y!fBR0`X'0TZKĎ01L)ZZ9ݲӐHgޡZKS*ҦJV3j؍'UwwI3iAX2%֯kQ^HG뙁 +P腳t~qDUɧu5 ~K f u &B%ҷL6nw.|0Jh|3FO:DT0"u:vsqn,<޸0_"w q73D2MTDƜ`hCFi+"J2ٹ3CU` ڔ] )8G@%u[OKQJOz}yGH׃6z#yaRGrlid4kYm!^۟kBU'_kk_1C' PIJ2m_s"{Xsc'/o)Ĕ`&Pa25sƘmHԋcߜ.>t n,sI40\fz*Y%:LdTl1{AJvPr}20яLM!IH)/2 "DQv1MDNO@_Z 8~&Zk_ <H)'*rysJ޾x֎hn%DQ@8ܻ1F1ws_%;f}u{ƘQ'`0njkkՕ;x Zb^Zնׇ+/dHf4%tEXtdate:create2011-04-18T12:32:45+00:00ƺ%tEXtdate:modify2011-04-18T12:32:45+00:00IENDB`PyPagekite-1.5.2.201011/gui/icons-16/pk-traffic.png000066400000000000000000000022011374056564300211730ustar00rootroot00000000000000PNG  IHDRagAMA asRGB cHRMz&u0`:pQ<bKGDԂ pHYs   vpAg\ƭaIDAT8MKlTU9޹amOP X(.ƭMHtB ! /b!A JgP[L{gnop[{[ AеǙ3 EXb+s>d?=9Z> C@> %SBJ7U@ ,/^t\7p.H#ndG >@:7PְyCYFs0bl62 ˽|ŞZNH)|ɭYSߠ m 24I946AKs/CJ\e&sX!3mϗ6)L_Y'󜒬DEEln/puFn\bυ?BS {J.w=Of0h@"tbofk%˺hrcW`))}()c4)+KJfC(F F)\ pˊO|[K$sU+5<++wrnNe GH4pБ2Mmcp߾OE[i2ej}%]#NBfԚM7օuؑ#N6Y-hhITtq֏IEI. c8Ƃ0{p%@q<urTWS0959c6\[-u_S|B,XgRR?L:G7l1c7nW=[0:BʺRy*ˇN9s8`%qQ[F>kzm1x} 9<;22 2+SMY͆sÛf%tEXtdate:create2011-04-18T13:17:19+00:00*)n%tEXtdate:modify2011-04-18T13:17:19+00:00[tIENDB`PyPagekite-1.5.2.201011/jsui/000077500000000000000000000000001374056564300152735ustar00rootroot00000000000000PyPagekite-1.5.2.201011/jsui/control.pk-shtml000066400000000000000000000250521374056564300204400ustar00rootroot00000000000000 PageKite Status on %(hostname)s (%(http_host)s)

Your pagekites protocol front-ends server

Download current configuration for Windows or Linux / OS X (view).

Sharing

asdf lakdsjf;lkaj ;laskdjfl;ksj a;lksdjf ;akldjf ;lkjasdflkj alkj

About X

This is your PageKite Status on %(hostname)s.

Note that this page is on your computer, but some of the links may take you to the on-line service at PageKite.net.

Activity


PyPagekite-1.5.2.201011/jsui/icons/000077500000000000000000000000001374056564300164065ustar00rootroot00000000000000PyPagekite-1.5.2.201011/jsui/icons/add.png000066400000000000000000000013351374056564300176460ustar00rootroot00000000000000PNG  IHDRagAMA7tEXtSoftwareAdobe ImageReadyqe<oIDAT8˥Ka[/Y()%X(olNۖskn.-h;8fEP"jïMGˈ}yພ羹$I.tulu AX:𼂒ZHh1DnZJOJB{Z?`2`S=N$ő=;a &jw qJG#<"N2h8޵`6xցn_+ ~Zto}`x%XЛ͈ hXѿƻ/}BJ_G&|Qr-6Aރ EL⬡\U3:WUh[C6+ 6.f *K͸ܝFq ou4܄?d|XҥMvD` *_[ #A20liR|xq`4w=\uQ m+G|%$5Թ5RO*YGMUO Gqj4ְ(X& s1c˭(LVf RdjQ '-1ATA>U j4,pV"4L$e@.ArBY a~myY])Q8tNLܞt2"I o=CSd)__AF(IENDB`PyPagekite-1.5.2.201011/jsui/icons/delete.png000066400000000000000000000013131374056564300203540ustar00rootroot00000000000000PNG  IHDRagAMA7tEXtSoftwareAdobe ImageReadyqe<]IDAT8˥KSa[nQP2wܦγL[,biaA\Cv_2MlZFjסNMjmkʷ`&.#z<ϓ bVPT3%I{GqRivȅ tz#E6EddJ`DR2<]N ;4Ѿ;m>78ɀQe6LIt殷cq!z |v j/Xi@ %1|hl !|! Y#uUNw]˼ H3u t]E>k%IfoRD:0`~ | (r on3oG0!$V *[W0_-+ dW&2ZfMFVJpiF&B > Rg- ~ CmڴER ឫ p5ްy+21Kawh` #aZ񽞆TZoLѓ`"(?'ˎJvKކ|:G9[aw82 Jw f'ymzsӘTsw__ιIrIENDB`PyPagekite-1.5.2.201011/jsui/icons/edit.png000066400000000000000000000013121374056564300200360ustar00rootroot00000000000000PNG  IHDRagAMA7tEXtSoftwareAdobe ImageReadyqe<\IDAT8˥KSQU/"/HAL͕bkbmiIbk֦Ӷvcnwwv݊4 s=9(eSM57=@`Ճ\CY4yQo%(nVEdsڑ5lEXN2o;F!~m#o T祹  j0w EXd2ORd- ~mcW"}ˆ{-p8FNpK"@7cc)g- %>ǥ(gId^C.D:XuEphe{ib1D X4&euQ!qAT"RӨޛ/gHdS v0ʂOg6apωFKޠN0Bd{gFdr I@_9Z:s7վG8L   1 ݥ˱>u8 ѐ՛Ln:Uf( aPz')p!Ԩ$'fF$lE k\+5ݸuLkެ83k=!;L[IENDB`PyPagekite-1.5.2.201011/jsui/index.pk-shtml000066400000000000000000000010621374056564300200620ustar00rootroot00000000000000 %(http_host)s - %(prog)s


%(method)s://%(http_host)s/

[ control panel ]

Carried to the Clouds by PageKite.

PyPagekite-1.5.2.201011/jsui/js/000077500000000000000000000000001374056564300157075ustar00rootroot00000000000000PyPagekite-1.5.2.201011/jsui/js/jquery.cookie.js000066400000000000000000000102261374056564300210350ustar00rootroot00000000000000/** * Cookie plugin * * Copyright (c) 2006 Klaus Hartl (stilbuero.de) * Dual licensed under the MIT and GPL licenses: * http://www.opensource.org/licenses/mit-license.php * http://www.gnu.org/licenses/gpl.html * */ /** * Create a cookie with the given name and value and other optional parameters. * * @example $.cookie('the_cookie', 'the_value'); * @desc Set the value of a cookie. * @example $.cookie('the_cookie', 'the_value', { expires: 7, path: '/', domain: 'jquery.com', secure: true }); * @desc Create a cookie with all available options. * @example $.cookie('the_cookie', 'the_value'); * @desc Create a session cookie. * @example $.cookie('the_cookie', null); * @desc Delete a cookie by passing null as value. Keep in mind that you have to use the same path and domain * used when the cookie was set. * * @param String name The name of the cookie. * @param String value The value of the cookie. * @param Object options An object literal containing key/value pairs to provide optional cookie attributes. * @option Number|Date expires Either an integer specifying the expiration date from now on in days or a Date object. * If a negative value is specified (e.g. a date in the past), the cookie will be deleted. * If set to null or omitted, the cookie will be a session cookie and will not be retained * when the the browser exits. * @option String path The value of the path atribute of the cookie (default: path of page that created the cookie). * @option String domain The value of the domain attribute of the cookie (default: domain of page that created the cookie). * @option Boolean secure If true, the secure attribute of the cookie will be set and the cookie transmission will * require a secure protocol (like HTTPS). * @type undefined * * @name $.cookie * @cat Plugins/Cookie * @author Klaus Hartl/klaus.hartl@stilbuero.de */ /** * Get the value of a cookie with the given name. * * @example $.cookie('the_cookie'); * @desc Get the value of a cookie. * * @param String name The name of the cookie. * @return The value of the cookie. * @type String * * @name $.cookie * @cat Plugins/Cookie * @author Klaus Hartl/klaus.hartl@stilbuero.de */ jQuery.cookie = function(name, value, options) { if (typeof value != 'undefined') { // name and value given, set cookie options = options || {}; if (value === null) { value = ''; options.expires = -1; } var expires = ''; if (options.expires && (typeof options.expires == 'number' || options.expires.toUTCString)) { var date; if (typeof options.expires == 'number') { date = new Date(); date.setTime(date.getTime() + (options.expires * 24 * 60 * 60 * 1000)); } else { date = options.expires; } expires = '; expires=' + date.toUTCString(); // use expires attribute, max-age is not supported by IE } // CAUTION: Needed to parenthesize options.path and options.domain // in the following expressions, otherwise they evaluate to undefined // in the packed version for some reason... var path = options.path ? '; path=' + (options.path) : ''; var domain = options.domain ? '; domain=' + (options.domain) : ''; var secure = options.secure ? '; secure' : ''; document.cookie = [name, '=', encodeURIComponent(value), expires, path, domain, secure].join(''); } else { // only name given, get cookie var cookieValue = null; if (document.cookie && document.cookie != '') { var cookies = document.cookie.split(';'); for (var i = 0; i < cookies.length; i++) { var cookie = jQuery.trim(cookies[i]); // Does this cookie string begin with the name we want? if (cookie.substring(0, name.length + 1) == (name + '=')) { cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); break; } } } return cookieValue; } };PyPagekite-1.5.2.201011/jsui/js/rpc.js000066400000000000000000001155471374056564300170460ustar00rootroot00000000000000/* * JSON/XML-RPC Client * Version: 0.8.0.2 (2007-12-06) * Copyright: 2007, Weston Ruter * License: GNU General Public License, Free Software Foundation * * * Original inspiration for the design of this implementation is from jsolait, from which * are taken the "ServiceProxy" name and the interface for synchronous method calls. * * See the following specifications: * - XML-RPC: * - JSON-RPC 1.0: * - JSON-RPC 1.1 (draft): * * Usage: * var service = new rpc.ServiceProxy("/app/service", { * asynchronous: true, //default: true * sanitize: true, //default: true * methods: ['greet'], //default: null (synchronous introspection populates) * protocol: 'JSON-RPC', //default: JSON-RPC * }); * service.greet({ * params:{name:"World"}, * onSuccess:function(message){ * alert(message); * }, * onException:function(e){ * alert("Unable to greet because: " + e); * return true; * } * }); * * If you create the service proxy with asynchronous set to false you may execute * the previous as follows: * * try { * var message = service.greet("World"); * alert(message); * } * catch(e){ * alert("Unable to greet because: " + e); * } * * Finally, if the URL provided is on a site that violates the same origin policy, * then you may only create an asynchronous proxy, the resultant data may not be * sanitized, and you must provide the methods yourself. In order to obtain the * method response, the JSON-RPC server must be provided the name of a callback * function which will be generated in the JavaScript (json-in-script) response. The HTTP GET * parameter for passing the callback function is currently non-standardized and so * varies from server to server. Create a service proxy with the option * 'callbackParamName' in order to specify the callback function name parameter; * the default is 'JSON-response-callback', as used by associated JSON/XML-RPC * Server project. For example, getting Google Calendar data: * * var gcalService = new rpc.ServiceProxy("http://www.google.com/calendar/feeds/myemail%40gmail.com/public", { * asynchronous: true, //true (default) required, otherwise error raised * sanitize: false, //explicit false required, otherwise error raised * methods: ['full'] //explicit list required, otherwise error raised * callbackParamName: 'callback' * }); * gcalService.full({ * params:{ * alt:'json-in-script' //required for this to work * 'start-min':new Date() //automatically converted to ISO8601 * //other Google Calendar parameters * }, * onSuccess:function(json){ * json.feed.entry.each(function(entry){ * //do something * }); * } * }); */ var rpc = { version:"0.8.0.2", requestCount: 0 }; rpc.ServiceProxy = function(serviceURL, options){ //if(typeof Prototype == 'undefined') // throw Error("The RPC client currently requires the use of Prototype."); this.__serviceURL = serviceURL; //Determine if accessing the server would violate the same origin policy this.__isCrossSite = false; var urlParts = this.__serviceURL.match(/^(\w+:)\/\/([^\/]+?)(?::(\d+))?(?:$|\/)/); if(urlParts){ this.__isCrossSite = ( location.protocol != urlParts[1] || document.domain != urlParts[2] || location.port != (urlParts[3] || "") ); } //Set other default options var providedMethodList; this.__isAsynchronous = true; this.__isResponseSanitized = true; this.__authUsername = null; this.__authPassword = null; this.__callbackParamName = 'JSON-response-callback'; this.__protocol = 'JSON-RPC'; this.__dateEncoding = 'ISO8601'; // ("@timestamp@" || "@ticks@") || "classHinting" || "ASP.NET" this.__decodeISO8601 = true; //JSON only //Get the provided options if(options instanceof Object){ if(options.asynchronous !== undefined){ this.__isAsynchronous = !!options.asynchronous; if(!this.__isAsynchronous && this.__isCrossSite) throw Error("It is not possible to establish a synchronous connection to a cross-site RPC service."); } if(options.sanitize != undefined) this.__isResponseSanitized = !!options.sanitize; if(options.user != undefined) this.__authUsername = options.user; if(options.password != undefined) this.__authPassword = options.password; if(options.callbackParamName != undefined) this.__callbackParamName = options.callbackParamName; if(String(options.protocol).toUpperCase() == 'XML-RPC') this.__protocol = 'XML-RPC'; if(options.dateEncoding != undefined) this.__dateEncoding = options.dateEncoding; if(options.decodeISO8601 != undefined) this.__decodeISO8601 = !!options.decodeISO8601; providedMethodList = options.methods; } if(this.__isCrossSite){ if(this.__isResponseSanitized){ throw Error("You are attempting to access a service on another site, and the JSON data returned " + "by cross-site requests cannot be sanitized. You must therefore explicitly set the " + "'sanitize' option to false (it is true by default) in order to proceed with making " + "potentially insecure cross-site rpc calls."); } else if(this.__protocol == 'XML-RPC') throw Error("Unable to use the XML-RPC protocol to access services on other domains."); } //Obtain the list of methods made available by the server if(this.__isCrossSite && !providedMethodList) throw Error("You must manually supply the service's method names since auto-introspection is not permitted for cross-site services."); if(providedMethodList) this.__methodList = providedMethodList; else { //Introspection must be performed synchronously var async = this.__isAsynchronous; this.__isAsynchronous = false; this.__methodList = this.__callMethod("system.listMethods", []); this.__isAsynchronous = async; } this.__methodList.push('system.listMethods'); this.__methodList.push('system.describe'); //Create local "wrapper" functions which reference the methods obtained above for(var methodName, i = 0; methodName = this.__methodList[i]; i++){ //Make available the received methods in the form of chained property lists (eg. "parent.child.methodName") var methodObject = this; var propChain = methodName.split(/\./); for(var j = 0; j+1 < propChain.length; j++){ if(!methodObject[propChain[j]]) methodObject[propChain[j]] = {}; methodObject = methodObject[propChain[j]]; } //Create a wrapper to this.__callMethod with this instance and this methodName bound var wrapper = (function(instance, methodName){ var call = {instance:instance, methodName:methodName}; //Pass parameters into closure return function(){ if(call.instance.__isAsynchronous){ if(arguments.length == 1 && arguments[0] instanceof Object){ call.instance.__callMethod(call.methodName, arguments[0].params, arguments[0].onSuccess, arguments[0].onException, arguments[0].onComplete); } else { call.instance.__callMethod(call.methodName, arguments[0], arguments[1], arguments[2], arguments[3]); } return undefined; } else return call.instance.__callMethod(call.methodName, rpc.toArray(arguments)); }; })(this, methodName); methodObject[propChain[propChain.length-1]] = wrapper; } }; rpc.setAsynchronous = function(serviceProxy, isAsynchronous){ if(!isAsynchronous && serviceProxy.__isCrossSite) throw Error("It is not possible to establish a synchronous connection to a cross-site RPC service."); serviceProxy.__isAsynchronous = !!isAsynchronous; }; rpc.ServiceProxy.prototype.__callMethod = function(methodName, params, successHandler, exceptionHandler, completeHandler){ rpc.requestCount++; //Verify that successHandler, exceptionHandler, and completeHandler are functions if(this.__isAsynchronous){ if(successHandler && typeof successHandler != 'function') throw Error('The asynchronous onSuccess handler callback function you provided is invalid; the value you provided (' + successHandler.toString() + ') is of type "' + typeof(successHandler) + '".'); if(exceptionHandler && typeof exceptionHandler != 'function') throw Error('The asynchronous onException handler callback function you provided is invalid; the value you provided (' + exceptionHandler.toString() + ') is of type "' + typeof(exceptionHandler) + '".'); if(completeHandler && typeof completeHandler != 'function') throw Error('The asynchronous onComplete handler callback function you provided is invalid; the value you provided (' + completeHandler.toString() + ') is of type "' + typeof(completeHandler) + '".'); } try { //Assign the provided callback function to the response lookup table if(this.__isAsynchronous || this.__isCrossSite){ rpc.pendingRequests[String(rpc.requestCount)] = { //method:methodName, onSuccess:successHandler, onException:exceptionHandler, onComplete:completeHandler }; } //Asynchronous cross-domain call (JSON-in-Script) ----------------------------------------------------- if(this.__isCrossSite){ //then this.__isAsynchronous is implied //Create an ad hoc function specifically for this cross-site request; this is necessary because it is // not possible pass an JSON-RPC request object with an id over HTTP Get requests. rpc.callbacks['r' + String(rpc.requestCount)] = (function(instance, id){ var call = {instance: instance, id: id}; //Pass parameter into closure return function(response){ if(response instanceof Object && (response.result || response.error)){ response.id = call.id; instance.__doCallback(response); } else {//Allow data without response wrapper (i.e. GData) instance.__doCallback({id: call.id, result: response}); } } })(this, rpc.requestCount); //rpc.callbacks['r' + String(rpc.requestCount)] = new Function("response", 'response.id = ' + rpc.requestCount + '; this.__doCallback(response);'); //Make the request by adding a SCRIPT element to the page var script = document.createElement('script'); script.setAttribute('type', 'text/javascript'); var src = this.__serviceURL + '/' + methodName + '?' + this.__callbackParamName + '=rpc.callbacks.r' + (rpc.requestCount); if(params) src += '&' + rpc.toQueryString(params); script.setAttribute('src', src); script.setAttribute('id', 'rpc' + rpc.requestCount); var head = document.getElementsByTagName('head')[0]; rpc.pendingRequests[rpc.requestCount].scriptElement = script; head.appendChild(script); return undefined; } //Calls made with XMLHttpRequest ------------------------------------------------------------ else { //Obtain and verify the parameters if(params){ if(!(params instanceof Object) || params instanceof Date) //JSON-RPC 1.1 allows params to be a hash not just an array throw Error('When making asynchronous calls, the parameters for the method must be passed as an array (or a hash); the value you supplied (' + String(params) + ') is of type "' + typeof(params) + '".'); //request.params = params; } //Prepare the XML-RPC request var request,postData; if(this.__protocol == 'XML-RPC'){ if(!(params instanceof Array)) throw Error("Unable to pass associative arrays to XML-RPC services."); var xml = ['' + methodName + '']; if(params){ xml.push(''); for(var i = 0; i < params.length; i++) xml.push('' + this.__toXMLRPC(params[i]) + ''); xml.push(''); } xml.push(''); postData = xml.join(''); //request = new Document(); //var methodCallEl = document.createElement('methodCall'); //var methodNameEl = document.createElement('methodName'); //methodNameEl.appendChild(document.createTextNode(methodName)); //methodCallEl.appendChild(methodNameEl); //if(params){ // var paramsEl = document.createElement('params'); // for(var i = 0; i < params.length; i++){ // var paramEl = document.createElement('param'); // paramEl.appendChild(this.__toXMLRPC(params[i])); // paramsEl.appendChild(paramEl); // } // methodCallEl.appendChild(paramsEl); //} //request.appendChild(methodCallEl); //postData = request.serializeXML(); } //Prepare the JSON-RPC request else { request = { version:"1.1", method:methodName, id:rpc.requestCount }; if(params) request.params = params; postData = this.__toJSON(request); } //XMLHttpRequest chosen (over Ajax.Request) because it propogates uncaught exceptions var xhr; if(window.XMLHttpRequest) xhr = new XMLHttpRequest(); else if(window.ActiveXObject){ try { xhr = new ActiveXObject('Msxml2.XMLHTTP'); } catch(err){ xhr = new ActiveXObject('Microsoft.XMLHTTP'); } } xhr.open('POST', this.__serviceURL, this.__isAsynchronous, this.__authUsername, this.__authPassword); if(this.__protocol == 'XML-RPC'){ xhr.setRequestHeader('Content-Type', 'text/xml'); xhr.setRequestHeader('Accept', 'text/xml'); } else { xhr.setRequestHeader('Content-Type', 'application/json'); xhr.setRequestHeader('Accept', 'application/json'); } //Asynchronous same-domain call ----------------------------------------------------- if(this.__isAsynchronous){ //Send the request xhr.send(postData); //Handle the response var instance = this; var requestInfo = {id:rpc.requestCount}; //for XML-RPC since the 'request' object cannot contain request ID xhr.onreadystatechange = function(){ //QUESTION: Why can't I use this.readyState? if(xhr.readyState == 4){ //XML-RPC if(instance.__protocol == 'XML-RPC'){ var response = instance.__getXMLRPCResponse(xhr, requestInfo.id); instance.__doCallback(response); } //JSON-RPC else { var response = instance.__evalJSON(xhr.responseText, instance.__isResponseSanitized); if(!response.id) response.id = requestInfo.id; instance.__doCallback(response); } } }; return undefined; } //Synchronous same-domain call ----------------------------------------------------- else { //Send the request xhr.send(postData); var response; if(this.__protocol == 'XML-RPC') response = this.__getXMLRPCResponse(xhr, rpc.requestCount); else response = this.__evalJSON(xhr.responseText, this.__isResponseSanitized); //Note that this error must be caught with a try/catch block instead of by passing a onException callback if(response.error) throw Error('Unable to call "' + methodName + '". Server responsed with error (code ' + response.error.code + '): ' + response.error.message); this.__upgradeValuesFromJSON(response); return response.result; } } } catch(err){ //err.locationCode = PRE-REQUEST Cleint var isCaught = false; if(exceptionHandler) isCaught = exceptionHandler(err); //add error location if(completeHandler) completeHandler(); if(!isCaught) throw err; } }; //This acts as a lookup table for the response callback to execute the user-defined // callbacks and to clean up after a request rpc.pendingRequests = {}; //Ad hoc cross-site callback functions keyed by request ID; when a cross-site request // is made, a function is created rpc.callbacks = {}; //Called by asychronous calls when their responses have loaded rpc.ServiceProxy.prototype.__doCallback = function(response){ if(typeof response != 'object') throw Error('The server did not respond with a response object.'); if(!response.id) throw Error('The server did not respond with the required response id for asynchronous calls.'); if(!rpc.pendingRequests[response.id]) throw Error('Fatal error with RPC code: no ID "' + response.id + '" found in pendingRequests.'); //Remove the SCRIPT element from the DOM tree for cross-site (JSON-in-Script) requests if(rpc.pendingRequests[response.id].scriptElement){ var script = rpc.pendingRequests[response.id].scriptElement; script.parentNode.removeChild(script); } //Remove the ad hoc cross-site callback function if(rpc.callbacks[response.id]) delete rpc.callbacks['r' + response.id]; var uncaughtExceptions = []; //Handle errors returned by the server if(response.error !== undefined){ var err = new Error(response.error.message); err.code = response.error.code; //err.locationCode = SERVER if(rpc.pendingRequests[response.id].onException){ try{ if(!rpc.pendingRequests[response.id].onException(err)) uncaughtExceptions.push(err); } catch(err2){ //If the onException handler also fails uncaughtExceptions.push(err); uncaughtExceptions.push(err2); } } else uncaughtExceptions.push(err); } //Process the valid result else if(response.result !== undefined){ //iterate over all values and substitute date strings with Date objects //Note that response.result is not passed because the values contained // need to be modified by reference, and the only way to do so is // but accessing an object's properties. Thus an extra level of // abstraction allows for accessing all of the results members by reference. this.__upgradeValuesFromJSON(response); if(rpc.pendingRequests[response.id].onSuccess){ try { rpc.pendingRequests[response.id].onSuccess(response.result); } //If the onSuccess callback itself fails, then call the onException handler as above catch(err){ //err3.locationCode = CLIENT; if(rpc.pendingRequests[response.id].onException){ try { if(!rpc.pendingRequests[response.id].onException(err)) uncaughtExceptions.push(err); } catch(err2){ //If the onException handler also fails uncaughtExceptions.push(err); uncaughtExceptions.push(err2); } } else uncaughtExceptions.push(err); } } } //Call the onComplete handler try { if(rpc.pendingRequests[response.id].onComplete) rpc.pendingRequests[response.id].onComplete(response); } catch(err){ //If the onComplete handler fails //err3.locationCode = CLIENT; if(rpc.pendingRequests[response.id].onException){ try { if(!rpc.pendingRequests[response.id].onException(err)) uncaughtExceptions.push(err); } catch(err2){ //If the onException handler also fails uncaughtExceptions.push(err); uncaughtExceptions.push(err2); } } else uncaughtExceptions.push(err); } delete rpc.pendingRequests[response.id]; //Merge any exception raised by onComplete into the previous one(s) and throw it if(uncaughtExceptions.length){ var code; var message = 'There ' + (uncaughtExceptions.length == 1 ? 'was 1 uncaught exception' : 'were ' + uncaughtExceptions.length + ' uncaught exceptions') + ': '; for(var i = 0; i < uncaughtExceptions.length; i++){ if(i) message += "; "; message += uncaughtExceptions[i].message; if(uncaughtExceptions[i].code) code = uncaughtExceptions[i].code; } var err = new Error(message); err.code = code; throw err; } }; /******************************************************************************************* * JSON-RPC Specific Functions ******************************************************************************************/ rpc.ServiceProxy.prototype.__toJSON = function(value){ switch(typeof value){ case 'number': return isFinite(value) ? value.toString() : 'null'; case 'boolean': return value.toString(); case 'string': //Taken from Ext JSON.js var specialChars = { "\b": '\\b', "\t": '\\t', "\n": '\\n', "\f": '\\f', "\r": '\\r', '"' : '\\"', "\\": '\\\\', "/" : '\/' }; return '"' + value.replace(/([\x00-\x1f\\"])/g, function(a, b) { var c = specialChars[b]; if(c) return c; c = b.charCodeAt(); //return "\\u00" + Math.floor(c / 16).toString(16) + (c % 16).toString(16); return '\\u00' + rpc.zeroPad(c.toString(16)); }) + '"'; case 'object': if(value === null) return 'null'; else if(value instanceof Array){ var json = ['[']; //Ext's JSON.js reminds me that Array.join is faster than += in MSIE for(var i = 0; i < value.length; i++){ if(i) json.push(','); json.push(this.__toJSON(value[i])); } json.push(']'); return json.join(''); } else if(value instanceof Date){ switch(this.__dateEncoding){ case 'classHinting': //{"__jsonclass__":["constructor", [param1,...]], "prop1": ...} return '{"__jsonclass__":["Date",[' + value.valueOf() + ']]}'; case '@timestamp@': case '@ticks@': return '"@' + value.valueOf() + '@"'; case 'ASP.NET': return '"\\/Date(' + value.valueOf() + ')\\/"'; default: return '"' + rpc.dateToISO8601(value) + '"'; } } else if(value instanceof Number || value instanceof String || value instanceof Boolean) return this.__toJSON(value.valueOf()); else { var useHasOwn = {}.hasOwnProperty ? true : false; //From Ext's JSON.js var json = ['{']; for(var key in value){ if(!useHasOwn || value.hasOwnProperty(key)){ if(json.length > 1) json.push(','); json.push(this.__toJSON(key) + ':' + this.__toJSON(value[key])); } } json.push('}'); return json.join(''); } //case 'undefined': //case 'function': //case 'unknown': //default: } throw new TypeError('Unable to convert the value of type "' + typeof(value) + '" to JSON.'); //(' + String(value) + ') }; rpc.isJSON = function(string){ //from Prototype String.isJSON() var testStr = string.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, ''); return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(testStr); }; rpc.ServiceProxy.prototype.__evalJSON = function(json, sanitize){ //from Prototype String.evalJSON() //Remove security comment delimiters json = json.replace(/^\/\*-secure-([\s\S]*)\*\/\s*$/, "$1"); var err; try { if(!sanitize || rpc.isJSON(json)) return eval('(' + json + ')'); } catch(e){err = e;} throw new SyntaxError('Badly formed JSON string: ' + json + " ... " + (err ? err.message : '')); }; //This function iterates over the properties of the passed object and converts them // into more appropriate data types, i.e. ISO8601 strings are converted to Date objects. rpc.ServiceProxy.prototype.__upgradeValuesFromJSON = function(obj){ var matches, useHasOwn = {}.hasOwnProperty ? true : false; for(var key in obj){ if(!useHasOwn || obj.hasOwnProperty(key)){ //Parse date strings if(typeof obj[key] == 'string'){ //ISO8601 if(this.__decodeISO8601 && (matches = obj[key].match(/^(?:(\d\d\d\d)-(\d\d)(?:-(\d\d)(?:T(\d\d)(?::(\d\d)(?::(\d\d)(?:\.(\d+))?)?)?)?)?)$/))){ obj[key] = new Date(0); if(matches[1]) obj[key].setUTCFullYear(parseInt(matches[1])); if(matches[2]) obj[key].setUTCMonth(parseInt(matches[2]-1)); if(matches[3]) obj[key].setUTCDate(parseInt(matches[3])); if(matches[4]) obj[key].setUTCHours(parseInt(matches[4])); if(matches[5]) obj[key].setUTCMinutes(parseInt(matches[5])); if(matches[6]) obj[key].setUTCMilliseconds(parseInt(matches[6])); } //@timestamp@ / @ticks@ else if(matches = obj[key].match(/^@(\d+)@$/)){ obj[key] = new Date(parseInt(matches[1])) } //ASP.NET else if(matches = obj[key].match(/^\/Date\((\d+)\)\/$/)){ obj[key] = new Date(parseInt(matches[1])) } } else if(obj[key] instanceof Object){ //JSON 1.0 Class Hinting: {"__jsonclass__":["constructor", [param1,...]], "prop1": ...} if(obj[key].__jsonclass__ instanceof Array){ //console.info('good1'); if(obj[key].__jsonclass__[0] == 'Date'){ //console.info('good2'); if(obj[key].__jsonclass__[1] instanceof Array && obj[key].__jsonclass__[1][0]) obj[key] = new Date(obj[key].__jsonclass__[1][0]); else obj[key] = new Date(); } } else this.__upgradeValuesFromJSON(obj[key]); } } } }; /******************************************************************************************* * XML-RPC Specific Functions ******************************************************************************************/ rpc.ServiceProxy.prototype.__toXMLRPC = function(value){ var xml = ['']; switch(typeof value){ case 'number': if(!isFinite(value)) xml.push(''); else if(parseInt(value) == Math.ceil(value)){ xml.push(''); xml.push(value.toString()); xml.push(''); } else { xml.push(''); xml.push(value.toString()); xml.push(''); } break; case 'boolean': xml.push(''); xml.push(value ? '1' : '0'); xml.push(''); break; case 'string': xml.push(''); xml.push(value.replace(/[<>&]/, function(ch){ })); //escape for XML! xml.push(''); break; case 'object': if(value === null) xml.push(''); else if(value instanceof Array){ xml.push(''); for(var i = 0; i < value.length; i++) xml.push(this.__toXMLRPC(value[i])); xml.push(''); } else if(value instanceof Date){ xml.push('' + rpc.dateToISO8601(value) + ''); } else if(value instanceof Number || value instanceof String || value instanceof Boolean) return rpc.dateToISO8601(value.valueOf()); else { xml.push(''); var useHasOwn = {}.hasOwnProperty ? true : false; //From Ext's JSON.js for(var key in value){ if(!useHasOwn || value.hasOwnProperty(key)){ xml.push(''); xml.push('' + key + ''); //Excape XML! xml.push(this.__toXMLRPC(value[key])); xml.push(''); } } xml.push(''); } break; //case 'undefined': //case 'function': //case 'unknown': default: throw new TypeError('Unable to convert the value of type "' + typeof(value) + '" to XML-RPC.'); //(' + String(value) + ') } xml.push(''); return xml.join(''); }; //rpc.ServiceProxy.prototype.toXMLRPC = function(value){ //documentNode // var valueEl = document.createElement('value'); // //var xml = ['']; // switch(typeof value){ // case 'number': // if(!isFinite(value)) // //xml.push(''); // valueEl.appendChild(document.createElement('nil')); // //else if(parseInt(value) == Math.ceil(value)){ // // var intEl = document.createElement('int'); // // intEl.appendChild(document.createTextNode(value.toString())); // // valueEl.appendChild(intEl); // // //xml.push(''); // // //xml.push(value.toString()); // // //xml.push(''); // //} // //else { // // var doubleEl = document.createElement('double'); // // doubleEl.appendChild(document.createTextNode(value.toString())); // // valueEl.appendChild(doubleEl); // // //xml.push(''); // // //xml.push(value.toString()); // // //xml.push(''); // //} // else { // var numEl = document.createElement(parseInt(value) == Math.ceil(value) ? 'int' : 'double'); // numEl.appendChild(document.createTextNode(value.toString())); // valueEl.appendChild(numEl); // } // return valueEl; // case 'boolean': // var boolEl = document.createElement('boolean'); // boolEl.appendChild(document.createTextNode(value ? '1' : '0')); // valueEl.appendChild(boolEl); // return valueEl; // //xml.push(''); // //xml.push(value ? '1' : '0'); // //xml.push(''); // case 'string': // var stringEl = document.createElement('string'); // stringEl.appendChild(document.createTextNode(value)); // valueEl.appendChild(stringEl); // return valueEl; // case 'object': // if(value === null) // valueEl.appendChild(document.createElement('nil')); // else if(value instanceof Array){ // var arrayEl = document.createElement('array'); // var dataEl = document.createElement('data'); // for(var i = 0; i < value.length; i++) // dataEl.appendChild(this.__toXMLRPC(value[i])); // arrayEl.appendChild(dataEl); // valueEl.appendChild(arrayEl); // } // else if(value instanceof Date){ // var dateEl = document.createElement('datetime.ISO8601'); // dateEl.appendChild(document.createTextNode(rpc.dateToISO8601(value))); // valueEl.appendChild(dateEl); // } // else if(value instanceof Number || value instanceof String || value instanceof Boolean) // return rpc.dateToISO8601(value.valueOf()); // else { // var structEl = document.createElement('struct'); // var useHasOwn = {}.hasOwnProperty ? true : false; //From Ext's JSON.js // for(var key in value){ // if(!useHasOwn || value.hasOwnProperty(key)){ // var memberEl = document.createElement('member'); // var nameEl = document.createElement('name') // nameEl.appendChild(document.createTextNode(key)); // memberEl.appendChild(nameEl); // memberEl.appendChild(this.__toXMLRPC(value[key])); // structEl.appendChild(memberEl); // } // } // valueEl.appendChild(structEl); // } // return valueEl; // //case 'undefined': // //case 'function': // //case 'unknown': // //default: // } // throw new TypeError('Unable to convert the value of type "' + typeof(value) + '" to XML-RPC.'); //(' + String(value) + ') //}; rpc.ServiceProxy.prototype.__parseXMLRPC = function(valueEl){ if(valueEl.childNodes.length == 1 && valueEl.childNodes.item(0).nodeType == 3) { return valueEl.childNodes.item(0).nodeValue; } for(var i = 0; i < valueEl.childNodes.length; i++){ if(valueEl.childNodes.item(i).nodeType == 1){ var typeEL = valueEl.childNodes.item(i); switch(typeEL.nodeName.toLowerCase()){ case 'i4': case 'int': //An integer is a 32-bit signed number. You can include a plus or minus at the // beginning of a string of numeric characters. Leading zeros are collapsed. // Whitespace is not permitted. Just numeric characters preceeded by a plus or minus. var intVal = parseInt(typeEL.firstChild.nodeValue); if(isNaN(intVal)) throw Error("XML-RPC Parse Error: The value provided as an integer '" + typeEL.firstChild.nodeValue + "' is invalid."); return intVal; case 'double': //There is no representation for infinity or negative infinity or "not a number". // At this time, only decimal point notation is allowed, a plus or a minus, // followed by any number of numeric characters, followed by a period and any // number of numeric characters. Whitespace is not allowed. The range of // allowable values is implementation-dependent, is not specified. var floatVal = parseFloat(typeEL.firstChild.nodeValue); if(isNaN(floatVal)) throw Error("XML-RPC Parse Error: The value provided as a double '" + typeEL.firstChild.nodeValue + "' is invalid."); return floatVal; case 'boolean': if(typeEL.firstChild.nodeValue != '0' && typeEL.firstChild.nodeValue != '1') throw Error("XML-RPC Parse Error: The value provided as a boolean '" + typeEL.firstChild.nodeValue + "' is invalid."); return Boolean(parseInt(typeEL.firstChild.nodeValue)); case 'string': if(!typeEL.firstChild) return ""; return typeEL.firstChild.nodeValue; case 'datetime.iso8601': var matches, date = new Date(0); if(matches = typeEL.firstChild.nodeValue.match(/^(?:(\d\d\d\d)-(\d\d)(?:-(\d\d)(?:T(\d\d)(?::(\d\d)(?::(\d\d)(?:\.(\d+))?)?)?)?)?)$/)){ if(matches[1]) date.setUTCFullYear(parseInt(matches[1])); if(matches[2]) date.setUTCMonth(parseInt(matches[2]-1)); if(matches[3]) date.setUTCDate(parseInt(matches[3])); if(matches[4]) date.setUTCHours(parseInt(matches[4])); if(matches[5]) date.setUTCMinutes(parseInt(matches[5])); if(matches[6]) date.setUTCMilliseconds(parseInt(matches[6])); return date; } throw Error("XML-RPC Parse Error: The provided value does not match ISO8601."); case 'base64': throw Error("Not able to parse base64 data yet."); //return base64_decode(typeEL.firstChild.nodeValue); case 'nil': return null; case 'struct': //A contains s and each contains a and a . var obj = {}; for(var memberEl, j = 0; memberEl = typeEL.childNodes.item(j); j++){ if(memberEl.nodeType == 1 && memberEl.nodeName == 'member'){ var name = ''; valueEl = null; for(var child, k = 0; child = memberEl.childNodes.item(k); k++){ if(child.nodeType == 1){ if(child.nodeName == 'name') name = child.firstChild.nodeValue; else if(child.nodeName == 'value') valueEl = child; } } //s can be recursive, any may contain a or // any other type, including an , described below. if(name && valueEl) obj[name] = this.__parseXMLRPC(valueEl); } } return obj; case 'array': //An contains a single element, which can contain any number of s. var arr = []; var dataEl = typeEL.firstChild; while(dataEl && (dataEl.nodeType != 1 || dataEl.nodeName != 'data')) dataEl = dataEl.nextSibling; if(!dataEl) new Error("XML-RPC Parse Error: Expected 'data' element as sole child element of 'array'."); valueEl = dataEl.firstChild; while(valueEl){ if(valueEl.nodeType == 1){ //s can be recursive, any value may contain an or // any other type, including a , described above. if(valueEl.nodeName == 'value') arr.push(this.__parseXMLRPC(valueEl)); else throw Error("XML-RPC Parse Error: Illegal element child '" + valueEl.nodeName + "' of an array's 'data' element."); } valueEl = valueEl.nextSibling; } return arr; default: throw Error("XML-RPC Parse Error: Illegal element '" + typeEL.nodeName + "' child of the 'value' element."); } } } return ''; } rpc.ServiceProxy.prototype.__getXMLRPCResponse = function(xhr, id){ var response = {}; if(!xhr.responseXML) throw Error("Malformed XML document."); var doc = xhr.responseXML.documentElement; if(doc.nodeName != 'methodResponse') throw Error("Invalid XML-RPC document."); var valueEl = doc.getElementsByTagName('value')[0]; if(valueEl.parentNode.nodeName == 'param' && valueEl.parentNode.parentNode.nodeName == 'params') { response.result = this.__parseXMLRPC(valueEl); } else if(valueEl.parentNode.nodeName == 'fault'){ var fault = this.__parseXMLRPC(valueEl); response.error = { code: fault.faultCode, message: fault.faultString }; } else throw Error("Invalid XML-RPC document."); if(!response.result && !response.error) throw Error("Malformed XML-RPC methodResponse document."); response.id = id; //XML-RPC cannot pass and return request IDs return response; }; /******************************************************************************************* * Other helper functions ******************************************************************************************/ //Takes an array or hash and coverts it into a query string, converting dates to ISO8601 // and throwing an exception if nested hashes or nested arrays appear. rpc.toQueryString = function(params){ if(!(params instanceof Object || params instanceof Array) || params instanceof Date) throw Error('You must supply either an array or object type to convert into a query string. You supplied: ' + params.constructor); var str = ''; var useHasOwn = {}.hasOwnProperty ? true : false; for(var key in params){ if(useHasOwn && params.hasOwnProperty(key)){ //Process an array if(params[key] instanceof Array){ for(var i = 0; i < params[key].length; i++){ if(str) str += '&'; str += encodeURIComponent(key) + "="; if(params[key][i] instanceof Date) str += encodeURIComponent(rpc.dateToISO8601(params[key][i])); else if(params[key][i] instanceof Object) throw Error('Unable to pass nested arrays nor objects as parameters while in making a cross-site request. The object in question has this constructor: ' + params[key][i].constructor); else str += encodeURIComponent(String(params[key][i])); } } else { if(str) str += '&'; str += encodeURIComponent(key) + "="; if(params[key] instanceof Date) str += encodeURIComponent(rpc.dateToISO8601(params[key])); else if(params[key] instanceof Object) throw Error('Unable to pass objects as parameters while in making a cross-site request. The object in question has this constructor: ' + params[key].constructor); else str += encodeURIComponent(String(params[key])); } } } return str; }; //Converts an iterateable value into an array; similar to Prototype's $A function rpc.toArray = function(value){ //if(value && value.length){ if(value instanceof Array) return value; var array = []; for(var i = 0; i < value.length; i++) array.push(value[i]); return array; //} //throw Error("Unable to convert to an array the value: " + String(value)); }; //Returns an ISO8601 string *in UTC* for the provided date (Prototype's Date.toJSON() returns localtime) rpc.dateToISO8601 = function(date){ //var jsonDate = date.toJSON(); //return jsonDate.substring(1, jsonDate.length-1); //strip double quotes return date.getUTCFullYear() + '-' + rpc.zeroPad(date.getUTCMonth()+1) + '-' + rpc.zeroPad(date.getUTCDate()) + 'T' + rpc.zeroPad(date.getUTCHours()) + ':' + rpc.zeroPad(date.getUTCMinutes()) + ':' + rpc.zeroPad(date.getUTCSeconds()) + '.' + //Prototype's Date.toJSON() method does not include milliseconds rpc.zeroPad(date.getUTCMilliseconds(), 3); }; rpc.zeroPad = function(value, width){ if(!width) width = 2; value = (value == undefined ? '' : String(value)) while(value.length < width) value = '0' + value; return value; };PyPagekite-1.5.2.201011/pagekite/000077500000000000000000000000001374056564300161125ustar00rootroot00000000000000PyPagekite-1.5.2.201011/pagekite/__init__.py000077500000000000000000000016351374056564300202330ustar00rootroot00000000000000############################################################################## LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2020, 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: """ ############################################################################## PyPagekite-1.5.2.201011/pagekite/__main__.py000077500000000000000000000202061374056564300202070ustar00rootroot00000000000000#!/usr/bin/env python """ This is the pagekite.py Main() function. """ ############################################################################## from __future__ import absolute_import LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2020, 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="""\ -----BEGIN CERTIFICATE----- MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMTE5 MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgT EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR 6FSS0gpWsawNJN3Fz0RndJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8X pz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZFGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC 9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+5eNu/Nio5JIk2kNrYrhV /erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pGx8cgoLEf Zd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z +pUX2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7w qP/0uK3pN/u6uPQLOvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZah SL0896+1DSJMwBGB7FY79tOi4lu3sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVIC u9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+CGCe01a60y1Dma/RMhnEw6abf Fobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5WdYgGq/yapiq crxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB /wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvl wFTPoCWOAvn9sKIN9SCYPBMtrFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM 4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+nq6PK7o9mfjYcwlYRm6mnPTXJ9OV 2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSgtZx8jb8uk2Intzna FxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwWsRqZ CuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiK boHGhfKppC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmcke jkk9u+UJueBPSZI9FoJAzMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yL S0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb QOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl 0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB NVOFBkpdn627G190 -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ 0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ 3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq 4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/ MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw 7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69 ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5 JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj YXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezEL MAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE BwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMM GEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP ADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQua BtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe 3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4 YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR rOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cm ez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQU oBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF MAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v QUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29t b2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG9w0BAQUF AAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q GE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2 G9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi l2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3 smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg== -----END CERTIFICATE----- """ PyPagekite-1.5.2.201011/pagekite/android.py000077500000000000000000000142211374056564300201070ustar00rootroot00000000000000""" This is the main function for the Android version of PageKite. """ ############################################################################# from __future__ import absolute_import LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2020, 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----- """ PyPagekite-1.5.2.201011/pagekite/common.py000077500000000000000000000136161374056564300177660ustar00rootroot00000000000000""" Constants and global program state. """ ############################################################################## from __future__ import absolute_import LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2020, 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 = '1.5.2.201011' AUTHOR = 'Bjarni Runar Einarsson, http://bre.klaki.net/' WWWHOME = 'https://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), int(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'] # Places to search for the CA Certificate bundle OS_CA_CERTS = ( "/etc/pki/tls/certs/ca-bundle.crt", # Fedora/RHEL "/etc/ssl/certs/ca-certificates.crt", # Debian/Ubuntu/Gentoo etc. "/etc/ssl/ca-bundle.pem", # OpenSUSE "/etc/pki/tls/cacert.pem", # OpenELEC "/etc/ssl/cert.pem", # OpenBSD "/usr/local/share/certs/ca-root-nss.crt", # FreeBSD/DragonFly "/usr/local/etc/openssl/cert.pem", # OS X (Homebrew) "/opt/local/etc/openssl/cert.pem", # OS X (Ports?) "/system/etc/security/cacerts") # Android CURL_CA_CERTS = 'https://curl.haxx.se/ca/cacert.pem' 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} # This is how many bytes we are willing to read per cycle. MAX_READ_BYTES = (16 * 1024) - 128 # Under 16kB, because OpenSSL MAX_READ_TUNNEL_X = 3.1 # 3x above, + fudge factor # Higher values save CPU and prevent individual tunnels # from hogging all our resources, but hurt latency and # reduce per-tunnel throughput. SELECT_LOOP_MIN_MS = 5 # Re-evaluate our choice of frontends every 45-60 minutes. FE_PING_INTERVAL = (45 * 60) + random.randint(0, 900) # This is a global count of disconnect errors; we use this # to adjust the ping interval over time. DISCONNECTS = [] PING_INTERVAL_MIN = 20 PING_INTERVAL = 116 # Not quite 2 minutes... :-) PING_INTERVAL_DEFAULT = 116 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'), 'whitelabel': ('http://dnsup.%s/' '?hostname=%%(domain)s&myip=%%(ips)s&sign=%%(sign)s'), 'whitelabels': ('https://dnsup.%s/' '?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] PyPagekite-1.5.2.201011/pagekite/compat.py000077500000000000000000000104121374056564300177500ustar00rootroot00000000000000""" Compatibility hacks to work around differences between Python versions. """ ############################################################################## from __future__ import absolute_import LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2020, 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 six.moves.urllib.parse import parse_qs, urlparse from . 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 import datetime ts_to_date = datetime.datetime.fromtimestamp def ts_to_iso(ts=None): return datetime.datetime.utcfromtimestamp(ts).isoformat() if sys.version_info < (3,): def b(data): return data def s(data): if isinstance(data, unicode): return data.encode('utf-8') return str(data) def u(data): if isinstance(data, unicode): return data return data.decode('utf-8') else: # We are using the latin-1 encoding here, on the assumption that # the string contains binary data we do not want to modify. import codecs def b(data): if isinstance(data, bytes): return data return codecs.latin_1_encode(data)[0] def s(data): if isinstance(data, str): return data return str(data, 'iso-8859-1') def u(data): if isinstance(data, str): return data return str(data, 'utf-8') import base64 import hashlib def sha1hex(data): return hashlib.sha1(b(data)).hexdigest().lower() def sha1b64(data): return base64.b64encode(hashlib.sha1(b(data)).digest()) def sha256b64(data): return base64.b64encode(hashlib.sha256(b(data)).digest()) try: from traceback import format_exc except ImportError: import traceback from six import StringIO def format_exc(): sio = StringIO() traceback.print_exc(file=sio) return sio.getvalue() try: from Queue import Queue except ImportError: from queue import Queue # 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 tuple(sys.version_info) >= (2, 7, 13): SSL = socks.SSL SEND_ALWAYS_BUFFERS = False SEND_MAX_BYTES = (16 * 1024) - 64 # Under 16kB to avoid WANT_WRITE errors 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!') class WithableStub(object): def __enter__(self): pass def __exit__(self, et, ev, tb): pass # Only calculate this just once MAGIC_UUID_SHA1 = sha1hex(MAGIC_UUID) PyPagekite-1.5.2.201011/pagekite/dropper.py000077500000000000000000000032461374056564300201470ustar00rootroot00000000000000""" This is a "dropper template". A dropper is a single-purpose PageKite back-end connector which embeds its own configuration. """ ############################################################################## from __future__ import absolute_import LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2020, 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) PyPagekite-1.5.2.201011/pagekite/httpd.py000077500000000000000000001211671374056564300176220ustar00rootroot00000000000000""" This is the pagekite.py built-in HTTP server. """ from __future__ import absolute_import from __future__ import print_function ############################################################################## LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2020, 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 six.moves import socketserver from six.moves.CGIHTTPServer import CGIHTTPRequestHandler from six.moves.urllib.parse import parse_qs, quote, unquote, urlparse from six.moves import http_cookies from six.moves.xmlrpc_server import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler import base64 import cgi import datetime try: from html import escape as escape_html except ImportError: from cgi import escape as escape_html import hashlib import os import re import socket import sys import tempfile import threading import time import traceback 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 ts_to_date = datetime.datetime.fromtimestamp def sha1hex(data): return hashlib.sha1(b(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(b('X-Response-Code: %s\r\n' % code)) self.wfile.write(b('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 = ( ) E_PB = { 'code': 400, 'msg': 'Failed', 'mimetype': 'text/html', 'title': 'PhotoBackup Error', 'body': '

PhotoBackup Error

' } E401 = { 'code': '401', 'msg': 'Forbidden', 'mimetype': 'text/html', 'title': '401 Forbidden', 'body': '

Access Denied. Sorry!

' } E403 = { 'code': '403', 'msg': 'Forbidden', 'mimetype': 'text/html', 'title': '403 Forbidden', 'body': '

Access Denied. Sorry!

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

File or directory not found. Sorry!

' } E500 = { 'code': '500', 'msg': 'Internal Error', 'mimetype': 'text/html', 'title': '500 Internal Error', 'body': '

Something is misconfigured or broken. 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(b('%s: %s\r\n' % (header, value))) def end_headers(self): self.wfile.write(b('\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('Connection', 'close') 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(b('%x\r\n' % len(chunk))) self.wfile.write(b(chunk)) self.wfile.write(b('\r\n')) else: if logging.DEBUG_IO: print('<== SENDING ===\n%s\n' % chunk) self.wfile.write(b(chunk)) def sendEof(self): if self.chunked and not self.suppress_body: self.wfile.write(b('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(b('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 allowUploads(self, full_path): uploads = self.host_config.get('uploads', False) return (uploads and ((uploads is True) or re.match(uploads, full_path))) 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) as 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 KeyboardInterrupt: raise except Exception as 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 ctype, pdict = cgi.parse_header(self.headers.get('content-type')) if (not (ctype == 'text/xml' and self.host_config.get('xmlrpc')) and 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 if ctype.lower() == 'multipart/form-data': self.post_data.seek(0) posted = cgi.FieldStorage( fp=self.post_data, headers=self.headers, environ={'REQUEST_METHOD': command, 'CONTENT_TYPE': ctype}) elif ctype.lower() == '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): with self.server.RCI.lock: return SimpleXMLRPCRequestHandler.do_POST(self) self.post_data.seek(0) except socket.error: pass except KeyboardInterrupt: raise except Exception as 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 KeyboardInterrupt: raise except Exception as e: logging.Log([('err', 'Error handling POST 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 = quote(fn) else: qfn = 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 convertPaths(self, path): path = unquote(path) if path.find('..') >= 0: raise IOError("Evil") paths = self.server.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()) return host_paths, full_path def handleFileUpload(self, path, uploaded, data=None, shtml_vars=None, subdir=None): host_paths, full_path = self.convertPaths(path) if not (full_path and os.path.isdir(full_path) and (data or self.allowUploads(full_path))): return False try: if not isinstance(uploaded, list): uploaded = [uploaded] for upload in uploaded: fn = os.path.basename( hasattr(upload, 'filename') and upload.filename or 'file.dat') name_policy = self.host_config.get('ul_filenames', 'keep') if name_policy not in ('keep', 'overwrite'): ext = ('.' in fn and fn.split('.')[-1] or 'dat') fn = 'upload-%x.%s' % (int(time.time()), ext) if subdir: full_path = os.path.join(full_path, subdir) if not os.path.exists(full_path): os.mkdir(full_path) target = os.path.join(full_path, fn) count = 1 while os.path.exists(target) and name_policy != 'overwrite': if '.' in fn: bn, ext = fn.rsplit('.', 1) else: bn, ext = fn, '' target = os.path.join(full_path, bn) target += '_%d' % count if ext: target += '.%s' % ext count += 1 fd = open(target, 'wb') fd.write(data or upload.value) fd.close() return True except KeyboardInterrupt: raise except: return False def handlePhotoBackup(self, path, posted, shtml_vars=None): password = self.host_config.get('photobackup', False) host_paths, full_path = self.convertPaths('/') # This allows the user to store just the SHA512 in their PageKite # config file. Users with exactly 128 char passwords are screwed. if password and (len(password) != 128): password = hashlib.sha512(b(password)).hexdigest() userpass = ('password' in posted and posted['password']) if isinstance(userpass, list): userpass = userpass[0] else: userpass = userpass.value if path == '/test': if not password: shtml_vars.update(self.E401) elif str(userpass) != password: shtml_vars.update(self.E403) elif not full_path or not os.path.isdir(full_path): shtml_vars.update(self.E500) else: self.sendResponse('OK', mimetype='text/plain') self.sendEof() return True elif path == '/': filesize = ('filesize' in posted and posted['filesize'].value) album = ('album' in posted and posted['album'].value) photo = ('upfile' in posted and posted['upfile']) photo_data = ((photo not in (None, False)) and photo.value or '') if album and ( (':' in album) or ('/' in album) or ('\\' in album) or (album[:1] == '.')): raise ValueError('Illegal album name') shtml_vars.update(self.E_PB) if not filesize: shtml_vars['code'] = 400 elif photo in (None, False): shtml_vars['code'] = 401 elif str(userpass) != password: shtml_vars.update(self.E403) elif len(photo_data) != int(filesize): shtml_vars['code'] = 411 elif self.handleFileUpload('/', photo, data=photo_data, subdir=album, shtml_vars=shtml_vars): self.sendResponse('OK', mimetype='text/plain') self.sendEof() return True else: shtml_vars['code'] = 500 else: shtml_vars.update(self.E404) return False def sendStaticPath(self, path, mimetype, shtml_vars=None): is_shtml, is_cgi, is_dir = False, False, False index_list = None try: host_paths, full_path = self.convertPaths(path) 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 KeyboardInterrupt: raise except: self.chunked = True except (IOError, OSError) as 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(u(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.

') if is_dir and self.allowUploads(full_path): shtml_vars['body'] += ( '

' '' '

') 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 = http_cookies.SimpleCookie(data['http_cookie']) else: cookies = {} # Do we expose the built-in console? console = self.host_config.get('console', False) # Are we implementing the PhotoBackup protocol? photobackup = self.host_config.get('photobackup', False) if path == self.host_config.get('yamon', False): if qs.get('view', [None])[0] == 'conns': from pagekite.pk import Watchdog llines = [] Watchdog.DumpConnState(self.server.pkite.conns, logfunc=llines.append) data['body'] = '\n'.join(llines) + '\n' elif common.gYamon: self.server.pkite.Overloaded(yamon=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 photobackup and (posted is not None) and (path in '/', '/test'): if self.handlePhotoBackup(path, posted, shtml_vars=data): return elif (posted is not None) and 'upload' in posted: if self.handleFileUpload(path, posted['upload'], shtml_vars=data): if self.sendStaticPath(path, data['mimetype'], shtml_vars=data): return 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' ACL_BOTH = 'rw' def __init__(self, httpd, pkite, conns): self.httpd = httpd self.pkite = pkite self.conns = conns self.modified = False self.lock = threading.RLock() self.request = None self.auth_tokens = {httpd.secret: self.ACL_READ} if self.pkite.ui_password: self.auth_tokens[httpd.secret] = self.ACL_BOTH self.auth_tokens[self.pkite.ui_password] = self.ACL_BOTH # 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 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 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_BHOST], 'fe_list': [{'name': fe.server_info[0], '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 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 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' % int(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_cipher_list('HIGH:!MEDIUM:!LOW:!aNULL:!NULL:!SHA') 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('version_python', sys.version.replace('\n', ' ')) 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] except KeyboardInterrupt: raise 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 def shutdown_request(self, *args): try: return SimpleXMLRPCServer.shutdown_request(self, *args) except TypeError: return SimpleXMLRPCServer.close_request(self, *args) PyPagekite-1.5.2.201011/pagekite/logging.py000077500000000000000000000106471374056564300201250ustar00rootroot00000000000000""" Logging. """ from __future__ import absolute_import ############################################################################## LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2020, 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 threading import time import sys from . 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 LOG_LOCK = threading.Lock() LOG_LEVEL_NONE = 1 LOG_LEVEL_ERR = 2 LOG_LEVEL_WARN = 3 LOG_LEVEL_INFO = 4 LOG_LEVEL_MACH = 5 LOG_LEVEL_DEBUG = 6 LOG_LEVEL_DEFAULT = LOG_LEVEL_INFO LOG_LEVELS = { 'none': LOG_LEVEL_NONE, 'err': LOG_LEVEL_ERR, 'errors': LOG_LEVEL_ERR, 'warn': LOG_LEVEL_WARN, 'warnings': LOG_LEVEL_WARN, 'info': LOG_LEVEL_INFO, 'mach': LOG_LEVEL_MACH, 'machine': LOG_LEVEL_MACH, 'debug': LOG_LEVEL_DEBUG, 'full': LOG_LEVEL_DEBUG, 'all': LOG_LEVEL_DEBUG, 0: 'none', LOG_LEVEL_NONE: 'none', LOG_LEVEL_ERR: 'err', LOG_LEVEL_WARN: 'warn', LOG_LEVEL_INFO: 'info', LOG_LEVEL_MACH: 'mach', LOG_LEVEL_DEBUG: 'debug'} LOG_LEVEL_DEFNAME = LOG_LEVELS[LOG_LEVEL_DEFAULT] LOG_LEVEL = LOG_LEVEL_DEFAULT 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, level=LOG_LEVEL_INFO): global LOG_LEVEL if level > LOG_LEVEL: return if values: words, wdict = LogValues(values) if level <= LOG_LEVEL_ERR or ('err' in wdict): syslog.syslog(syslog.LOG_ERR, '; '.join(['='.join(x) for x in words])) elif level <= LOG_LEVEL_INFO: syslog.syslog(syslog.LOG_INFO, '; '.join(['='.join(x) for x in words])) else: syslog.syslog(syslog.LOG_DEBUG, '; '.join(['='.join(x) for x in words])) def LogToFile(values, wdict=None, words=None, level=LOG_LEVEL_INFO): global LOG_LEVEL if level > LOG_LEVEL: return if values: words, wdict = LogValues(values) try: global LogFile with LOG_LOCK: LogFile.write('; '.join(['='.join(x) for x in words])) LogFile.write('\n') LogFile.flush() except (OSError, IOError): # Avoid crashing if the disk fills up or something lame like that pass def LogToMemory(values, wdict=None, words=None, level=LOG_LEVEL_INFO): global LOG_LEVEL if values and (level <= LOG_LEVEL): with LOG_LOCK: LogValues(values) def FlushLogMemory(): global LOG for l in LOG: Log(None, wdict=l, words=[(w, l[w]) for w in l], level=LOG_LEVEL) def LogError(msg, parms=None): emsg = [('err', msg)] if parms: emsg.extend(parms) Log(emsg, level=LOG_LEVEL_ERR) if common.gYamon: common.gYamon.vadd('errors', 1, wrap=1000000) def LogWarning(msg, parms=None): emsg = [('warn', msg)] if parms: emsg.extend(parms) Log(emsg, level=LOG_LEVEL_WARN) def LogDebug(msg, parms=None): emsg = [('debug', msg)] if parms: emsg.extend(parms) Log(emsg, level=LOG_LEVEL_DEBUG) def LogInfo(msg, parms=None): emsg = [('info', msg)] if parms: emsg.extend(parms) Log(emsg, level=LOG_LEVEL_INFO) def ResetLog(): global LogFile, Log, org_stdout LogFile = org_stdout Log = LogToMemory ResetLog() PyPagekite-1.5.2.201011/pagekite/logparse.py000077500000000000000000000133041374056564300203040ustar00rootroot00000000000000""" A basic tool for processing and parsing the Pagekite logs. This class doesn't actually do anything much, it's meant for subclassing. """ from __future__ import absolute_import from __future__ import print_function ############################################################################## LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2020, 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) as 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 as 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]) PyPagekite-1.5.2.201011/pagekite/manual.py000077500000000000000000000707431374056564300177570ustar00rootroot00000000000000#!/usr/bin/env python """ The program manual! """ from __future__ import absolute_import from __future__ import print_function import os import re import time from .common import * from .compat import ts_to_iso MAN_NAME = ("""\ pagekite.py - Make localhost servers publicly visible """) 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. +proxyproto __Use HAProxy's PROXY Protocol (v1) to relay IPs etc. +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. +uploads __Accept file uploads. +uploads=RE __Accept uploads to paths matching regexp RE. +ul_filenames=P __Upload naming policy. P = overwrite, keep or rename +cgi=list A list of extensions, for which files should be treated as CGI scripts (example: +cgi=cgi,pl,sh). +photobackup=password Enable built-in PhotoBackup server with the given password. See https://photobackup.github.io/ for details. """) 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. --whitelabel=D __Set defaults for pagekite.net white-labels. --whitelabels=D __Set defaults for pagekite.net white-labels (with TLS). --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. The kitename `unknown`, if allowed by the front-end, represents a backend of last resort for requests with no other match. --authdomain=DNS-suffix,\ --authdomain=/path/to/app,\ --authdomain=kite-domain:DNS-suffix,\ --authdomain=kite-domain:/path/to/app __ Use DNS-suffix for remote DNS-based authentication of incoming tunnel requests, or invoke an external application for this purpose. If no kite-domain is given, use this as the default authentication method. See the section below on tunnel authentication for further details. In order for the app path to be recognized as such, it must contain at least one / character. --auththreads=N __ Start N threads to process auth requests. Default is 1. --authfail_closed __ If authentication fails, reject tunnel requests. The default is to fail open and allow tunnels if the auth checks are broken. --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. --frontends=num:@/path/to/file:port __ Same as above, except the IP address list will be loaded from a file (and reloaded periodically), instead of using DNS. --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. --fe_nocertcheck __ Connect using SSL/TLS, but do not verify the remote certificate. This is largely insecure but still thwarts passive attacks and prevents routers and firewalls from corrupting the PageKite tunnel. --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. --keepalive=N __ Force traffic over idle tunnels every N seconds, to cope with firewalls that kill idle TCP connections. Backend only: if set to "auto" (the default), the interval will be adjusted automatically in response to disconnects. --all __Terminate early if any tunnels fail to register. --new __Don't attempt to connect to any kites' old front-ends. --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. This is for static configurations, for dynamic access controls use the `--authdomain` mechanism. The domain `unknown`, if configured, represents a backend of last resort for incoming requests with no other match. --authdomain=DNS-suffix,\ --authdomain=/path/to/app,\ --authdomain=kite-domain:DNS-suffix,\ --authdomain=kite-domain:/path/to/app __ Use DNS-suffix for remote DNS-based authentication of incoming tunnel requests, or invoke an external application for this purpose. If no kite-domain is given, use this as the default authentication method. See the section below on tunnel authentication for further details. In order for the app path to be recognized as such, it must contain at least one / character. --auththreads=N __ Start N threads to process auth requests. Default is 1. --authfail_closed __ If authentication fails, reject tunnel requests. The default is to fail open and allow tunnels if the auth checks are broken. --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. --overload=baseline,\ --overload_cpu=fraction, 0-1,\ --overload_mem=fraction, 0-1 __ Enable "overload" calculations, which cause the front-end to recommend back-ends go elsewhere if possible, once connection counts go above a certain number. The baseline is the initial overload level, but it will be adjusted dynamically based on load average (CPU use) and memory usage. This will really only work well on Linux and if PageKite is the only thing happening on the machine. Setting both fractions to 0 disables dynamic scaling. --overload_file=/path/to/baseline/file __ Path to a file, the contents of which overrides all overload calculations. This can be used to manage load calculations using an external process (or by hand, e.g. to prepare for maintenance). Note that overload must specify a non-zero baseline, otherwise this setting is ignored. --ratelimit_ips=IPs/seconds,\ --ratelimit_ips=kitename:IPs/seconds __ Limit how many different clients (IPs) can request data from a tunnel within a given window of time, e.g. 5/3600. This is useful as either a crude form of DDoS mitigation, or as a mechanism to make public kite services unusable for phishing. Note that limits are enforced per-tunnel (not per kite), and tunnels serving multiple kites will use the settings of the strictest kite. Limits apply to subdomains as well. A single IP may be counted more than once if request headers (such as User-Agent) differ. --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. --nopyopenssl __Avoid use of the pyOpenSSL library (not in config file) --nossl __Avoid use SSL entirely (not allowed in config file) --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. --errorurl=D:U __Custom error URL for domain D. --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_TUNNEL_AUTH = ("""\ When running pagekite.py as a front-end relay, you can enable dynamic authentication of incoming tunnel requests in two ways. One uses a DNS-based protocol for delegating authentication to a remote server. The nice thing about this, is relays can be deployed without any direct access to your user account databases - in particular, a zero-knowlege challenge/response protocol is used which means the relay never sees the shared secret used to authenticate the kite. The second method delegates authentication to an external app; this external app can be written in any language you like, as long as it implements the following command-line arguments:
      --capabilities     Print a list of capabilities to STDOUT and exit
      --server           Run as a "server", reading queries on STDIN and
                         sending one-line replies to STDOUT.
      --auth     Return JSON formatted auth and quota details
      --zk-auth   Implement the DNS-based zero-knowlege protocol
    
The recognized capabilities are SERVER, ZK-AUTH and AUTH. One of AUTH or ZK-AUTH is required. The JSON `--auth` responses should be dictionaries which have at least one element, `secret` or `error`. The secret is the shared secret to be used to authenticate the tunnel. The dictionary may also contain advisory quota values (`quota_kb`, `quota_days` and `quota_conns`), and IP rate limiting parameters (`ips_per_sec-ips` and `ips_per_sec-secs`). The source distribution of pagekite.py includes a script named `demo_auth_app.py` which implements this protocol. """) MAN_LICENSE = ("""\ Copyright 2010-2020, 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', 'Tunnel Request Authentication', MAN_TUNNEL_AUTH), ('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): lastchange = float(os.environ.get('SOURCE_DATE_EPOCH', os.path.getmtime(sys.argv[0]))) 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(lastchange).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()) PyPagekite-1.5.2.201011/pagekite/pk.py000077500000000000000000004731471374056564300171210ustar00rootroot00000000000000""" This is what is left of the original monolithic pagekite.py. This is slowly being refactored into smaller sub-modules. """ from __future__ import absolute_import from __future__ import division from __future__ import print_function ############################################################################## LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2020, 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 six from six.moves import range from six.moves import xmlrpc_client from six.moves.urllib.request import URLopener, urlopen from six.moves.urllib.parse import urlencode from six.moves.xmlrpc_server import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler import base64 import cgi import copy try: from html import escape as escape_html except ImportError: from cgi import escape as escape_html import errno import gc import getopt import getpass import os import random import re import select import socket import struct import sys import tempfile import threading import time import traceback import zlib from .compat import * from .common import * from . import compat from . import common from . import logging try: import urllib.request as urllib_request # Python 3 except ImportError: import urllib as urllib_request # Python 2 # This allows us to run, degraded, on Python < 2.6. try: import subprocess import json except ImportError: subprocess = json = None 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', 'whitelabel=', 'whitelabels=', 'local=', 'domain=', 'auththreads=', 'authdomain=', 'authfail_closed', 'motd=', 'register=', 'host=', 'noupgradeinfo', 'upgradeinfo=', 'ports=', 'protos=', 'portalias=', 'rawports=', 'tls_legacy', 'tls_default=', 'tls_endpoint=', 'selfsign', 'fe_certname=', 'fe_nocertcheck', 'ca_certs=', 'kitename=', 'kitesecret=', 'backend=', 'define_backend=', 'be_config=', 'insecure', 'ratelimit_ips=', 'max_read_bytes=', 'select_loop_min_ms=', 'service_on=', 'service_off=', 'service_cfg=', 'tunnel_acl=', 'client_acl=', 'accept_acl_file=', 'frontend=', 'nofrontend=', 'frontends=', 'keepalive=', 'torify=', 'socksify=', 'proxy=', 'noproxy', 'new', 'all', 'noall', 'dyndns=', 'nozchunks', 'sslzlib', 'wschunks', 'buffers=', 'noprobes', 'debugio', 'watch=', 'loglevel=', 'watchdog=', 'overload=', 'overload_cpu=', 'overload_mem=', 'overload_file=', # 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 otherwise cleartext connections to pagekite.net in SSL tunnels. def_hop = socks.parseproxy('default') for dest in ('pagekite.net', 'up.pagekite.net', 'up.b5p.us'): https_hop = socks.parseproxy( 'httpcs!%s!443' % ','.join([dest]+SERVICE_CERTS)) socks.setproxy(dest, *def_hop) 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 class AuthApp(object): def __init__(self, app_path): assert(subprocess is not None) self.app_path = app_path self.capabilities = [cap.upper() for cap in subprocess.check_output([app_path, '--capabilities']).split() if cap] if 'SERVER' in self.capabilities: self.lock = threading.Lock() self.server = subprocess.Popen([app_path, '--server'], stdin=subprocess.PIPE, stdout=subprocess.PIPE) else: self.lock = WithableStub() self.server = None def _q(self, args): if self.server is not None: with self.lock: self.server.stdin.write(' '.join(args) + '\n') self.server.stdin.flush() return self.server.stdout.readline().strip() else: return subprocess.check_output([self.app_path] + args).strip() def auth(self, domain): return json.loads(self._q(['--auth', domain])) def zk_auth(self, query): r = json.loads(self._q(['--zk-auth', query])) return (r['hostname'], r.get('alias', ''), r.get('ips', [''])) def supports_zk_auth(self): return ('ZK-AUTH' in self.capabilities) def supports_auth(self): return ('AUTH' in self.capabilities) class Watchdog(threading.Thread): """Kill the app if it locks up.""" daemon = True def __init__(self, timeout): threading.Thread.__init__(self) self.pid = os.getpid() self.conns = [] self.timeout = timeout self.updated = time.time() self.locks = {} @classmethod def DumpConnState(cls, conns, close=False, logfunc=None): for fpc in copy.copy(conns.ping_helper.clients): try: if close: (logfunc or logging.LogError)('Closing FPC %s' % (fpc,)) fpc[1].close() else: (logfunc or logging.LogInfo)('FastPing: %s' % (fpc,)) except: pass for conn in copy.copy(conns.conns): try: if close: (logfunc or logging.LogError)('Closing %s' % conn) conn.fd.close() else: (logfunc or logging.LogInfo)('Conn %s' % conn) except: pass def patpatpat(self): self.updated = time.time() def run(self): import signal if self.timeout: self.timeout = max(15, self.timeout) # Lower than this won't work! if common.gYamon and self.timeout: common.gYamon.vset('watchdog', self.timeout) failed = 5 # Log happy message after first sleep worries = 0 last_update = self.updated while self.timeout and (failed < 10) and (worries < self.timeout): time.sleep(self.timeout / 10.0) if self.updated == last_update: failed += 1 worries += 1 logging.LogInfo('Watchdog is worried (timeout=%ds, failures=%d/10, worries=%.1f/%d)' % (self.timeout, failed, worries, self.timeout)) if common.gYamon: common.gYamon.vadd('watchdog_worried', 1) if failed in (1, 6): os.kill(self.pid, signal.SIGUSR1) else: if failed: logging.LogInfo('Watchdog is happy (timeout=%ds)' % self.timeout) failed = 0 worries *= 0.9 last_update = self.updated if self.timeout: try: for lock_name, lock in self.locks.iteritems(): logging.LogDebug('Lock %s %s' % ( lock_name, lock.acquire(blocking=False) and 'is free' or 'is LOCKED')) self.DumpConnState(self.conns, close=True) finally: logging.LogError('Watchdog is sad: kill -INT %s' % self.pid) os.kill(self.pid, signal.SIGINT) time.sleep(2) logging.LogError('Watchdog is sad: kill -9 %s' % self.pid) os.kill(self.pid, 9) 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 self.qtime = 0.250 # A decent initial estimate def check(self, requests, conn, callback): with self.qc: self.jobs.append((requests, conn, callback)) self.qc.notify() def quit(self): with self.qc: self.keep_running = False self.qc.notify() try: self.join() except RuntimeError: pass def run(self): self.keep_running = True while self.keep_running: try: self._run() except Exception as e: logging.LogError('AuthThread died: %s' % e) time.sleep(5) logging.LogDebug('AuthThread: done') def _run(self): with self.qc: 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 = [] ip_limits = [] 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: # Note: These 15 seconds are a magic number which should be well # below the timeout in proto.conns.Tunnel._Connect(). if ((not self.conns.config.authfail_closed) and len(self.jobs) >= (15 / self.qtime)): # Float division logging.LogWarning('Quota lookup skipped, over 15s worth of jobs queued') (quota, days, conns, ipc, ips, reason) = ( -2, None, None, None, None, None) else: # This is a bit lame, but we only check the token if the quota # for this connection has never been verified. t0 = time.time() (quota, days, conns, ipc, ips, reason) = ( self.conns.config.GetDomainQuota( proto, domain, srand, token, sign, check_token=(conn.quota is None))) elapsed = (time.time() - t0) self.qtime = max(0.2, (0.9 * self.qtime) + (0.1 * elapsed)) 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 not ipc: try: ipc, ips = self.conns.config.GetDefaultIPsPerSecond(domain) except ValueError: pass if ipc and ips: ip_limits.append((float(ipc)/ips, ipc, ips)) # Float division 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, 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)) min_ip_limits = min(ip_limits or [(0, None, None)])[1:] if ip_limits and min_ip_limits[0]: results.append(('%s-IPsPerSec' % prefix, '%s/%s' % min_ip_limits)) 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 ##[ Selectables ]############################################################## class Connections(object): """A container for connections (Selectables), config and tunnel info.""" def __init__(self, config): self.config = config self.ip_tracker = {} self.lock = threading.RLock() self.idle = [] self.conns = [] self.conns_by_id = {} self.tunnels = {} self.auth_pool = [] self.ping_helper = FastPingHelper(self) self.ping_helper.start() 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): with self.lock: self.conns.append(conn) def auth(self): if common.gYamon: common.gYamon.vset('auth_threads', len(self.auth_pool)) common.gYamon.vset('auth_thread_qtime', sum([at.qtime for at in self.auth_pool] ) / (len(self.auth_pool) or 1)) # Float division common.gYamon.vset('auth_thread_jobs', sum([len(at.jobs) for at in self.auth_pool])) return self.auth_pool[random.randint(0, len(self.auth_pool)-1)] def SetAltId(self, conn, new_id): with self.lock: 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): with self.lock: self.idle.append((time.time() + seconds, conn.last_activity, conn)) def TrackIP(self, ip, domain): tick = '%d' % (time.time()//12) with self.lock: if tick not in self.ip_tracker: deadline = int(tick)-10 for ot in list(six.iterkeys(self.ip_tracker)): 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 with self.lock: _keys = sorted(self.ip_tracker.keys()) for tick in _keys: if ip in self.ip_tracker[tick]: domain = self.ip_tracker[tick][ip][1] return domain def Remove(self, conn, retry=True): try: with self.lock: 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 list(six.iteritems(self.tunnels)): 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): with self.lock: return [p[-1] for p in self.idle] def Sockets(self): with self.lock: return [s.fd for s in self.conns] def Readable(self): # FIXME: This is O(n) now = time.time() with self.lock: 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 with self.lock: 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): with self.lock: return [s for s in self.conns if s.IsDead()] def CleanFds(self): evil = [] with self.lock: 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.LogWarning('Removing broken Selectable: %s' % s) s.Cleanup() self.Remove(s) def Connection(self, fd): with self.lock: for conn in self.conns: if conn.fd == fd: return conn return None def TunnelServers(self): servers = {} with self.lock: 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 list(six.iterkeys(servers)) def CloseTunnel(self, proto, domain, conn): with self.lock: 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 = [] with self.lock: _idle = copy.copy(self.idle) for elc in _idle: expire, last_activity, conn = elc if conn.last_activity > last_activity: active.append(elc) elif expire < now: logging.LogInfo('Killing idle connection', [('conn', '%s' % conn)]) conn.Die(discard_buffer=True) elif conn.created < now - 1: conn.SayHello() with self.lock: for pair in active: if pair in self.idle: self.idle.remove(pair) def Tunnel(self, proto, domain, conn=None): with self.lock: 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 as 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.""" daemon = True 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 = {} # If we keep getting disconnected, maybe we have a nasty firewall # and should ping more frequently. Disabled at the frontend! cutoff = time.time() - (48 * 3600) common.DISCONNECTS = [c for c in common.DISCONNECTS[-50:] if c > cutoff] if (len(common.DISCONNECTS) >= 3 and not self.pkite.isfrontend and not self.pkite.keepalive): badness = (len(common.DISCONNECTS)-3)/10.0 new_interval = max( (1-badness) * common.PING_INTERVAL_DEFAULT, common.PING_INTERVAL_MIN) if common.PING_INTERVAL != new_interval: common.PING_INTERVAL = new_interval logging.LogInfo('TunnelManager: adjusted ping interval, PI=%s, DC=%s' % (common.PING_INTERVAL, len(common.DISCONNECTS))) for tid in self.conns.tunnels: for tunnel in self.conns.tunnels[tid]: if tunnel.server_info[tunnel.S_IS_MOBILE]: pings = int(self.pkite.keepalive or min(common.PING_INTERVAL_MOBILE, common.PING_INTERVAL)) else: pings = int(self.pkite.keepalive or common.PING_INTERVAL) grace = max(PING_GRACE_DEFAULT, len(tunnel.write_blocked)/(tunnel.write_speed or 0.001)) # Float division 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 list(six.itervalues(dead)): logging.Log([('dead', tunnel.server_info[tunnel.S_NAME])]) tunnel.Die(discard_buffer=True) if (dead and not self.pkite.isfrontend and common.PING_INTERVAL > common.PING_INTERVAL_DEFAULT): common.DISCONNECTS.append(time.time()) common.PING_INTERVAL = common.PING_INTERVAL_DEFAULT logging.LogInfo('TunnelManager: adjusted ping interval, PI=%s, DC=%s' % (common.PING_INTERVAL, len(common.DISCONNECTS))) 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 as e: logging.LogError('TunnelManager died: %s' % (format_exc(),)) time.sleep(5) logging.LogDebug('TunnelManager: done') def DoFrontendWork(self, loop_count): self.CheckTunnelQuotas(time.time()) self.pkite.LoadMOTD() # Update our idea of what it means to be overloaded. if self.pkite.overload and (1 == loop_count % 20): self.pkite.CalculateOverload(yamon=common.gYamon) # 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.Status('down', color=self.pkite.ui.GREY, message='No kites ready to fly. Waiting...') self.pkite.ui.Notify('It looks like your Internet connection might ' 'be down! Will retry soon.', color=self.pkite.ui.YELLOW) self.pkite.ui.Notify( ' - Check whether you can ping pagekite.net or google.com') if self.pkite.servers_auto: hostname = self.pkite.servers_auto[1] self.pkite.ui.Notify( ' - Check whether `%s` can be looked up in DNS' % hostname) for hostport in self.pkite.servers_manual: hostname = hostport.split(':')[0] self.pkite.ui.Notify( ' - Check whether `%s` can be looked up in DNS' % hostname) elif connecting == 0: self.pkite.ui.Status('down', color=self.pkite.ui.RED, message='Not connected to any front-end relays, will retry...') elif tunnel_count < tunnel_total: self.pkite.ui.Status('flying', color=self.pkite.ui.YELLOW, message=('Only connected to %d/%d front-end relays, 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, DC=%d, PI=%d' % (loop_count, self.check_interval, len(common.DISCONNECTS), common.PING_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, DC=%s, PI=%d' % (problem, connecting, len(common.DISCONNECTS), (self.pkite.keepalive or common.PING_INTERVAL))) incr = int(1+random.random()*self.check_interval) self.check_interval = min(60, self.check_interval + incr) time.sleep(1) else: # No servers probably means we don't have a working Internet # connection, so try not to be too frantic. self.check_interval = self.pkite.servers and 5 or 30 # Make sure tunnels are really alive. if self.pkite.isfrontend: self.DoFrontendWork(loop_count=loop_count) 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 range(0, (self.check_interval // 5)): if self.keep_running: time.sleep(self.check_interval // 5) if i > (self.check_interval // 5): 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, 0o600) 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.pyfile = os.path.abspath(sys.argv[0]) 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.auth_apps = {} self.authfail_closed = False 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', 'raw', 'xmpp'] 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 = [] # # This will automatically disable TLS certificate checks after # Dec 1 00:00:00 2028 GMT, one month before the current bundled # USERTrust certificate expires. We also bundle Comodo and ISRG # roots which last longer, but this is our conservative cutoff. # Context: https://pagekite.net/2020-05-31/TLS_Certificate_Bug # # FIXME: Update this when bundled certs get updated! # self.fe_nocertcheck = (time.time() >= 1859241600) 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.ratelimit_ips = {} self.be_config = {} self.disable_zchunks = False self.websocket_chunks = False self.enable_sslzlib = False self.buffer_max = DEFAULT_BUFFER_MAX self.error_url = None self.error_urls = {} 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_connected = {} self.servers_errored = {} self.servers_preferred = [] self.servers_sessionids = {} self.keepalive = None self.dns_cache = {} self.ping_cache = {} self.last_frontend_choice = 0 self.kitename = '' self.kitesecret = '' self.dyndns = None self.last_updates = [] self.postpone_ddns_updates = [0, 0] 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.watchdog = None self.overload = None self.overload_cpu = 0.75 self.overload_mem = 0.85 self.overload_file = None self.overload_current = None self.overload_membase = None 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 as e: # The above stuff may fail in some cases, e.g. on Android in SL4A. self.rcfile = 'pagekite.cfg' self.devnull = '/dev/null' self.ca_certs_default = self.ca_certs = None self.SetDefaultCACerts() def SetDefaultCACerts(self, **kwargs): # 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. if self.ca_certs_default != self.pyfile: self.ca_certs_default = self.FindCACerts(**kwargs) self.ca_certs = self.ca_certs_default def FindCACerts(self, use_curl_bundle=False): # Search a bunch of paths, preferring the biggest/newest bundle found biggest, newest, found = 0, 0, None own_pemfile = "%s.pem" % '.'.join(self.rcfile.split('.')[:-1]) for path in list(OS_CA_CERTS) + [own_pemfile]: if os.path.exists(path): # We consider all bundles over 200k to be the same size... size = min(200000, os.stat(path).st_size) mtime = os.stat(path).st_mtime if size > biggest: # Choose the biggest bundle! found, biggest, newest = path, size, mtime elif size == biggest and mtime > newest: # Choose the freshest bundle! found, newest = path, mtime if use_curl_bundle and ((not found) or ((found == own_pemfile) and (newest < time.time() - 365*24*3600))): # No bundle found or bundle old, download a new one from the cURL site. try: URLopener().retrieve(CURL_CA_CERTS, filename=own_pemfile) return self.FindCACerts(use_curl_bundle=False) except: pass if found: return found return self.pyfile # Fall back to distributed CA certs 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 APPVER_DNS(self, tld): appver_without_patchlevel = '.'.join(APPVER.split('.')[:3]) return ('fe4_%s.' + tld) % re.sub(r'[^\d]', '', appver_without_patchlevel) def SetServiceDefaults(self, clobber=True, check=False): def_dyndns = (DYNDNS['pagekite.net'], {'user': '', 'pass': ''}) def_frontends = (1, self.APPVER_DNS('b5p.us'), 443) 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 (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 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 SetWhitelabelDefaults(self, wld, secure=False, clobber=True, check=False): def_dyndns = (DYNDNS[secure and 'whitelabels' or 'whitelabel'] % wld, {'user': '', 'pass': ''}) def_frontends = (1, self.APPVER_DNS(wld), 443) def_fe_certs = ['fe.%s' % wld, wld] + [c for c in SERVICE_CERTS if c != wld] def_error_url = 'http%s://www.%s/offline/?' % (secure and 's' or '', wld) if check: return (self.dyndns == def_dyndns and self.servers_auto == def_frontends and self.error_url == def_error_url 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 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'), '' ]) kite_tld = None if self.kitename: kite_tld = '.'.join(self.kitename.split('.')[-2:]) def addManualFrontends(): if self.servers_manual or self.servers_never: config.append('') 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) if self.SetServiceDefaults(check=True): config.extend([ '##[ Front-end settings: use pagekite.net defaults ]##', 'defaults', ]) addManualFrontends() elif (kite_tld and self.SetWhitelabelDefaults(kite_tld, secure=False, check=True)): config.extend([ '##[ Front-end settings: use %s defaults ]##' % kite_tld, 'whitelabel = %s' % kite_tld, '' ]) addManualFrontends() elif (kite_tld and self.SetWhitelabelDefaults(kite_tld, secure=True, check=True)): config.extend([ '##[ Front-end settings: use %s defaults ]##' % kite_tld, 'whitelabels = %s' % kite_tld, '' ]) addManualFrontends() 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.fe_nocertcheck: config.append('fe_nocertcheck') 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/'), '', ]) if self.ca_certs != self.ca_certs_default: config.append('ca_certs = %s' % self.ca_certs) if self.keepalive != None: config.append('keepalive = %d' % self.keepalive) for dom in sorted(self.error_urls.keys()): config.append('errorurl = %s:%s' % (dom, self.error_urls[dom])) 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_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'), (self.authfail_closed and 'authfail_closed' or '# authfail_closed # Tunnel auth fails OPEN without this'), 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/' ]) if self.isfrontend or self.overload or self.overload_file: com = (not self.overload) and '# ' or '' config.extend([ '', '##[ Overload settings ]##', p('overload = %s', self.overload, ('max-backends (int)',)), p('overload_file = %s', self.overload_file, ('/path/to/max/backends/file',)), com + ('overload_mem = %-5s # 0=fixed' % self.overload_cpu), com + ('overload_cpu = %-5s # 0=fixed' % self.overload_mem) ]) config.extend([ '', '##[ Front-end access controls (default=deny, if configured) ]##', p('accept_acl_file = %s', self.accept_acl_file, '/path/to/file'), ]) for d in sorted(self.ratelimit_ips.keys()): if d == '*': config.append('ratelimit_ips = %s' % self.ratelimit_ips[d]) else: config.append('ratelimit_ips = %s:%s' % (d, self.ratelimit_ips[d])) 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('loglevel = %s', logging.LOG_LEVELS.get(logging.LOG_LEVEL, logging.LOG_LEVEL_DEBUG), logging.LOG_LEVEL_DEFNAME), 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'), p('savefile = %s', safe and self.savefile, '/path/to/savefile'), ]) if common.MAX_READ_BYTES != 16*1024: config.append('max_read_bytes = %sx%.3f' % (common.MAX_READ_BYTES, common.MAX_READ_TUNNEL_X)) if common.SELECT_LOOP_MIN_MS != 5: config.append('select_loop_min_ms = %s' % common.SELECT_LOOP_MIN_MS) config.append('') 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, username=None): # 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). # # FIXME: This runs too early! Before the config is fully loaded! BOO. # if self.ui_httpd and self.ui_httpd.httpd and not new: return self.ui_httpd.httpd.secret elif not self.kitesecret and not self.ui_password and not self.backends: # Our config has no secrets, generate a completely random one return u(sha256b64(globalSecret())[:24]).replace('/', '_') else: config = [username or getpass.getuser()] + self.GenerateConfig() return u(sha256b64('\n'.join(config))[:24]).replace('/', '_') 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 CanSaveConfig(self, savefile=None, _raise=None): savefile = savefile or self.savefile or self.rcfile try: if os.path.exists(savefile): open(savefile, 'r+').close() else: open(savefile, 'w').close() # FIXME: Python3.3 adds mode=x, use it! os.remove(savefile) except (IOError, OSError): if _raise is not None: raise _raise("Could not write to: %s" % savefile) return False return savefile 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 as 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: from . 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' % int(self.backends[bid][BE_STATUS]))], level=logging.LOG_LEVEL_MACH) 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 GetAuthApp(self, command): auth_app = self.auth_apps.get(command) if auth_app is None: self.auth_apps[command] = AuthApp(command) return self.auth_apps[command] def LookupDomainQuota(self, srand, token, sign, protoport, domain, adom): if '/' in adom: auth_app = self.GetAuthApp(adom) if not auth_app.supports_zk_auth(): auth = auth_app.auth(domain) secret = auth.get('secret') if not secret: # We cannot validate: the auth app was unavailable or broken. raise ValueError('Auth app provided no secret for: %s' % domain) elif self.IsSignatureValid(sign, secret, protoport, domain, srand, token): return (auth.get('quota_kb', -2), # None or Zero would deny access auth.get('quota_days'), auth.get('quota_conns'), auth.get('ips_per_sec-ips'), auth.get('ips_per_sec-secs'), auth.get('reason', auth.get('error'))) else: logging.LogError('Invalid signature for: %s (%s)' % (domain, protoport)) return (None, None, None, None, None, 'signature') else: auth_app = None lookup = '.'.join([srand, token, sign, protoport, domain, adom]) if not lookup.endswith('.'): lookup += '.' if logging.DEBUG_IO: print('=== AUTH LOOKUP\n%s\n===' % lookup) if auth_app: (hn, al, iplist) = auth_app.zk_auth(lookup) else: (hn, al, iplist) = socket.gethostbyname_ex(lookup) if logging.DEBUG_IO: print('hn=%s\nal=%s\niplist=%s\n' % (hn, al, iplist)) # 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) ipc = ips = None if '-' in error: try: error, ipc, ips = error.split('-')[:3] # More fields may come later ipc = int(ipc, 16) ips = int(ips, 16) except ValueError: pass else: error = q_days = q_conns = ipc = ips = None # If not an authentication error, quota should be encoded as an IP. ip = iplist[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, ipc, ips, None) # Errors on real errors are final. if not ip.endswith(AUTH_ERR_USER_UNKNOWN): return (None, q_days, q_conns, ipc, ips, error) # User unknown, fall through to local test. return (-1, q_days, q_conns, ipc, ips, 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.LogWarning('Unsupported port request: %s (%s:%s)' % (porti, protoport, domain)) return (None, None, None, None, None, 'port') except ValueError: logging.LogError('Invalid port request: %s:%s' % (protoport, domain)) return (None, None, None, None, None, 'port') else: proto, port = protoport, None if proto not in self.server_protos: logging.LogWarning('Invalid proto request: %s:%s' % (protoport, domain)) return (None, None, 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, None, None) elif not (self.auth_domain or self.auth_domains): logging.LogError('Invalid signature for: %s (%s)' % (domain, protoport)) return (None, None, None, None, None, auth_error_type or 'signature') if self.auth_domain or self.auth_domains: adom = '' adom_keys = list(six.iterkeys(self.auth_domains)) adom_keys.sort(key=lambda k: (len(k), k)) # Longest match will win for dom in adom_keys: if domain.endswith('.' + dom): adom = self.auth_domains[dom] if not adom: adom = self.auth_domain if adom: try: return self.LookupDomainQuota(srand, token, sign, protoport, domain.replace('*', '_any_'), adom) except Exception as e: # Lookup failed, fail open. if logging.DEBUG_IO: traceback.print_exc(file=sys.stderr) logging.LogError('Quota lookup failed: %s' % e) if not self.authfail_closed: return (-2, None, None, None, None, None) logging.LogWarning('No authentication found for: %s (%s)' % (domain, protoport)) return (None, None, None, None, None, auth_error_type or 'unauthorized') def _get_overload_factor(self): if common.gYamon is not None: return ( common.gYamon.values.get('backends_live', 0) + common.gYamon.values.get('selectables_live', 1)) or 1 return (len(self.conns.tunnels) or 1) def CalculateOverload(self, cload=None, yamon=None): # Check overload file first, it overrides everything if self.overload_file: try: with open(self.overload_file, 'r') as fd: new_overload = int(fd.read().strip()) if new_overload != self.overload_current: self.overload_current = new_overload logging.LogInfo( ('Overload level is now %d (from %s)' ) % (self.overload_current, self.overload_file)) return except (OSError, IOError, ValueError): pass # FIXME: This is almost certainly linux specific. # FIXME: There are too many magic numbers in here. try: # If both are disabled, just bail out. if not (self.overload_cpu or self.overload_mem): return # Check system load. with open('/proc/loadavg', 'r') as fd: loadavg = float(fd.read().strip().split()[1]) meminfo = {} with open('/proc/meminfo', 'r') as fd: for line in fd: try: key, val = line.lower().split(':') meminfo[key] = int(val.strip().split()[0]) except ValueError: pass # Figure out how much RAM is available memfree = meminfo.get('memavailable') if not memfree: memfree = meminfo.get('memfree', 0) + meminfo.get('cached', 0) # Record baseline memory usage if this is our first run if not self.overload_membase: self.overload_membase = float(meminfo['memtotal']) - memfree # Sanity checks... are these really necessary? self.overload_membase = max(50000, self.overload_membase) self.overload_membase = min(self.overload_membase, 0.9 * meminfo['memtotal']) # Check internal load, abort if load is low anyway. cload = cload or self._get_overload_factor() if cload < 50: return # Calculate the implied unit cost of every live connection memtotal = float(meminfo['memtotal'] - self.overload_membase) munit = max(32, float(memtotal - memfree) / cload) # 32KB/conn=optimism! # Float division lunit = max(0.10, loadavg) / cload # Calculate overload factors based on the unit costs moverload = int(self.overload_mem * float(memtotal) / munit) # Integer division loverload = int(self.overload_cpu / lunit) # Integer division # Choose a new overload value. new_overload = int(max( # Dynamic load scaling can reduce our overload from the baseline # as well as raise it, but only up to a point. self.overload // 2, # This hack lets us disable memory or CPU overload checks # with --overload_cpu=0 or --overload_mem=0. min(moverload or loverload, loverload or moverload))) # Smooth things out a little bit... self.overload_current = int( (0.8 * self.overload_current) + (0.2 * new_overload)) logging.LogInfo( ('Overload level is now %d' ' (bml=%d/%d/%d; cml=%d/%d/%.4f free=%d/%d loadavg=%.2f)' ) % (self.overload_current, self.overload, moverload, loverload, cload, munit, lunit, memfree, memtotal, loadavg)) if yamon is not None: yamon.vset('overload_unit_mem', munit) yamon.vset('overload_unit_cpu', lunit) except (IOError, OSError, ValueError, KeyError, TypeError): pass def Overloaded(self, yamon=None): if not self.overload_current or not self.conns: return False cload = self._get_overload_factor() if yamon is not None: yamon.vset('overload_threshold', self.overload_current) yamon.vset('overload_headroom', max(0, self.overload_current - cload)) yamon.vset('overload', float(cload) / self.overload_current) # Float division return (cload >= self.overload_current) 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): from . 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 six.iteritems(self.conns.tunnels): 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', 'websocket') and 80) or (proto == 'irc' and 6667) or (proto == 'xmpp' and 5222) or (proto == 'https' and 443)) 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() if self.ui_httpd: self.ui_httpd.quit() 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: with open(self.motd, 'r') as f: self.motd_message = ''.join(f.readlines()).strip()[:8192] 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 GetDefaultIPsPerSecond(self, dom=None, limit=None): if dom: parts = dom.split('.') while len(parts) > 1: pdom = '.'.join(parts) if pdom not in self.ratelimit_ips: parts.pop(0) else: dom = pdom break ips, secs = (limit or self.ratelimit_ips.get(dom or '*', '')).split('/') return int(ips), int(secs) 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 = self.CanSaveConfig(savefile=arg, _raise=ConfigError) elif opt == '--shell': self.shell = True elif opt == '--save': self.save = self.CanSaveConfig(_raise=ConfigError) and True elif opt == '--only': self.kite_only = True if self.kite_remove or self.kite_add or self.kite_disable: raise ConfigError('One change at a time please!') self.save = self.CanSaveConfig(_raise=ConfigError) and True elif opt == '--add': self.kite_add = True if self.kite_remove or self.kite_only or self.kite_disable: raise ConfigError('One change at a time please!') self.save = self.CanSaveConfig(_raise=ConfigError) and True elif opt == '--remove': self.kite_remove = True if self.kite_add or self.kite_only or self.kite_disable: raise ConfigError('One change at a time please!') self.save = self.CanSaveConfig(_raise=ConfigError) and True elif opt == '--disable': self.kite_disable = True if self.kite_add or self.kite_only or self.kite_remove: raise ConfigError('One change at a time please!') self.save = self.CanSaveConfig(_raise=ConfigError) and True elif opt == '--list': pass elif opt in ('-I', '--pidfile'): self.pidfile = arg elif opt in ('-L', '--logfile'): self.logfile = arg elif opt == '--loglevel': try: logging.LOG_LEVEL = int(arg) except ValueError: logging.LOG_LEVEL = logging.LOG_LEVELS[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'): if self.ui_httpd: self.ui_httpd.quit() self.ui_httpd = None if arg.lower() in ('off', 'none', 'disabled'): self.ui_sspec = None else: 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 else: self.auth_domains = {} self.auth_domain = arg elif opt == '--authfail_closed': self.authfail_closed = True 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 == '--keepalive': if arg == 'auto': self.keepalive = None else: self.keepalive = max(PING_INTERVAL_MIN, int(arg)) 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 == '--ratelimit_ips': if ':' in arg: which, limit = arg.split(':') else: which, limit = '*', arg self.GetDefaultIPsPerSecond(None, limit.strip()) # ValueErrors if bad self.ratelimit_ips[which.strip()] = limit.strip() elif opt == '--max_read_bytes': if 'x' in arg: base, tmul = arg.split('x') common.MAX_READ_BYTES = max(1024, int(base)) common.MAX_READ_TUNNEL_X = max(1, float(tmul)) else: common.MAX_READ_BYTES = max(1024, int(arg)) elif opt == '--select_loop_min_ms': common.SELECT_LOOP_MIN_MS = max(0, min(int(arg), 100)) 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_request) 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 # 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': if arg == 'auto': self.SetDefaultCACerts(use_curl_bundle=True) else: self.ca_certs = arg 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 == '--fe_nocertcheck': self.fe_nocertcheck = True 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'): if ':http' in arg: dom, url = arg.split(':', 1) self.error_urls[dom] = url else: self.error_url = 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 == '--wschunks': self.websocket_chunks = 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 == '--watchdog': self.watchdog = Watchdog(int(arg)) elif opt == '--overload': self.overload_current = self.overload = int(arg) elif opt == '--overload_file': self.overload_file = arg elif opt == '--overload_cpu': self.overload_cpu = max(0, float(arg)) elif opt == '--overload_mem': self.overload_mem = max(0, min(float(arg), 1.5)) elif opt == '--debugio': NullUi.CLEAR = self.ui.CLEAR = '' logging.DEBUG_IO = True elif opt == '--buffers': self.buffer_max = int(arg) elif opt == '--nocrashreport': pass # Legacy 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 == '--whitelabel': self.SetWhitelabelDefaults(arg, secure=False) elif opt == '--whitelabels': self.SetWhitelabelDefaults(arg, secure=True) 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', 'ssh', 'irc', 'xmpp'): be_proto = be.pop(0) if len(be) < 2: be.append({'http': '80', 'http2': '80', 'http3': '80', 'https': '443', 'irc': '6667', 'xmpp': '5222', '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[list(six.iterkeys(specs))[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[list(six.iterkeys(specs))[0]][BE_SECRET], int(time.time()//3600)) first = (len(list(six.iterkeys(host_paths))) == 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 list(six.itervalues(just_these_backends)): 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 list(six.iterkeys(just_these_backends)): 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 list(six.iterkeys(just_these_backends)): 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 try: import http.client socks.wrapmodule(http.client) except ImportError: pass return xmlrpc_client.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): self.CanSaveConfig(_raise=ConfigError) 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'] if self.ca_certs == self.ca_certs_default: # We're using the defaults, but the defaults might be lame so we # reset them here, allowing for downloading the cURL bundle. self.SetDefaultCACerts(use_curl_bundle=True) 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 = list(six.iterkeys(service_accounts)) 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=True) socks.setdefaultcertfile(self.ca_certs) 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 at %s' % (sys.exc_info(), format_exc()) 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 as 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 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, overload_ms=250, bias=None, wanted_by=None): cid = uuid = '%s:%s' % (host, port) if cid in self.servers_never: return (999999, uuid) if self.servers_no_ping: return (bias, 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(b('HEAD /ping HTTP/1.1\r\nHost: ping.pagekite\r\n\r\n')) data = s(fd.recv(1024)) fd.close() if not data.startswith('HTTP/1.1 503 Unavailable'): raise Exception() except Exception as e: logging.LogDebug('Ping %s:%s failed: %s' % (host, port, e)) return (999999, uuid) elapsed = (time.time() - start) try: uuid = data.split('X-PageKite-UUID: ')[1].split()[0] except: uuid = self.TMP_UUID_MAP.get(uuid, uuid) try: if overload_ms and data.index('X-PageKite-Overloaded:') >= 0: # Simulate slowness: How much depends on what context this relay # is in (in use, preferred, forced, ...), the default is 250ms # which will bump us to a less ideal geographic region, but not # all the way around the planet. elapsed += (overload_ms / 1000.0) except ValueError: pass 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([elapsed[1][0] for elapsed in self.ping_cache[cid][:window]] )/window # Float division uuid = self.ping_cache[cid][0][1][1] biased = max(0.01, (bias is None) and pingval or bias(pingval)) info = [ ('FE', '%s:%s' % (host, port)), ('http_ping_ms', '%d' % (1000 * biased)), ('win', window), ('uuid', uuid)] if pingval != biased: info.append(('unbiased_ms', '%d' % (1000 * pingval))) if wanted_by: info.append(('wanted_by', wanted_by)) logging.Log(info) return (biased, uuid) def GetHostIpAddrs(self, host): rv = [] if host[:1] == '@': try: with open(host[1:], 'r') as fd: for line in (l.strip() for l in fd): if line and line[:1] not in ('#', ';'): rv.append(line) logging.LogDebug('Loaded %d IPs from %s' % (len(rv), host[1:])) except: logging.LogDebug('Failed to load IPs from %s' % host[1:]) raise else: 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) # If the network is down, don't count disconnects... cutoff = now - 120 while common.DISCONNECTS and (common.DISCONNECTS[-1] > cutoff): common.DISCONNECTS.pop(-1) ips = {} for ipaddrs in self.dns_cache[host].values(): for ip in ipaddrs: ips[ip] = 1 return list(six.iterkeys(ips)) def GetActiveBackends(self, include_loopback=False): active = [] for bid in self.backends: (proto, bdom) = bid.split(':') if (self.backends[bid][BE_STATUS] not in BE_INACTIVE and (include_loopback or self.backends[bid][BE_SECRET]) and not bdom.startswith('*')): active.append(bid) return active def ChooseFrontEnds(self, live_servers, periodic=False): self.servers = [] self.servers_preferred = [] self.last_frontend_choice = time.time() now = time.time() servers_all = {} servers_pref = {} pinged = {} # This calculates a ping penalty, based on when we last had problems # connecting to this server, and a bonus if we already have a live # connection to this server. wanted_conns = self.servers_auto and self.servers_auto[0] or 1 def server_bias(server, base=0): if server not in live_servers: # If we already have >wanted live connections, pretend all the other # servers are very far away. This dampens the sloshing when many # of the front-end relays are in an overloaded state. This also # avoids us connecting to too many relays if both our current relay # and the dynamic DNS updater are overloaded. base += max(0, (len(live_servers) - wanted_conns) * 0.200) # Further avoid servers that had problems. We also check this for # servers that are live, in case we've detected the tunnel is just # really slow. seconds_since_error = (now - self.servers_errored.get(server, 0)) error_penalty = max(0, (1800 - seconds_since_error) / 1800) # Float division return lambda p: (p + base + error_penalty) # Increase our ping interval slightly unless it has been reduced # to the minimum: that means our connection is crap and we should # just leave it be. if (periodic and not self.keepalive and not self.isfrontend and not common.DISCONNECTS and common.PING_INTERVAL >= common.PING_INTERVAL_DEFAULT and common.PING_INTERVAL < common.PING_INTERVAL_MAX): common.PING_INTERVAL = int(min(common.PING_INTERVAL + 15, common.PING_INTERVAL_MAX)) logging.LogDebug('TunnelManager: adjusted ping interval, PI=%s' % common.PING_INTERVAL) # Process the manually requested servers first (--frontend= lines); these # are always added (and likely preferred) as long as they respond at all. # We may still check the wider pool for faster relays, so these are not # guaranteed to be in DNS. To force that, the user should not specify a # --frontends pool at all, and maybe use --new to ignore DNS too. def sping(server): (host, port) = server.split(':') ipaddrs = self.CachedGetHostIpAddrs(host) if ipaddrs: server = '%s:%s' % (ipaddrs[0], port) pingtime, uuid = self.Ping(ipaddrs[0], int(port), overload_ms=125, bias=server_bias(server), wanted_by='config') pinged[ipaddrs[0]] = (pingtime, uuid) if pingtime < 60: servers_all[uuid] = server if pingtime < 0.250: 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: (wanted_conns, domain, port) = self.servers_auto try: # First, check for old addresses and always connect to those. selected = {} if not self.servers_new_only: def bping(): for bid in self.GetActiveBackends(): (proto, bdom) = bid.split(':') for ip in self.CachedGetHostIpAddrs(bdom): # FIXME: What about IPv6 localhost? if not ip.startswith('127.') and ip not in pinged: server = '%s:%s' % (ip, port) pingtime, uuid = self.Ping(ip, int(port), overload_ms=50, bias=server_bias(server), wanted_by='DNS') pinged[ip] = (pingtime, uuid) servers_all[uuid] = server if pingtime < 0.055 and len(servers_pref) < wanted_conns: # If we have a relay in DNS with a nice low ping time, mark # it as preferred and potentially skip pinging the pool. servers_pref[uuid] = server threads, deadline = [], time.time() + 5 threads.append(threading.Thread(target=bping, args=set())) threads[-1].daemon = True threads[-1].start() for t in threads: t.join(max(0.1, deadline - time.time())) # Optimization: If we already have a long enough list of preferred # relays, skip the rest of the pings. In practice # this means a combination of --frontend and --frontends arguments # will treat the frontends pool as a fallback if the preferred ones # are unavailable. if len(servers_pref) < wanted_conns: ips = [i for i in self.CachedGetHostIpAddrs(domain) if i not in pinged] def iping(ip): # We add 50ms to these pings, to add stability and implement # a preference for the servers that are already in DNS. This # number matches the overload_ms value above, which means if # our server-in-DNS is overloaded, we'll initiate a move # elsewhere if an unbiased ping would recommend we do so. server = '%s:%s' % (ip, port) pinged[ip] = self.Ping(ip, int(port), overload_ms=250, bias=server_bias(server, base=0.05)) threads, deadline = [], time.time() + 5 for ip in ips: if ip not in pinged: 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 as e: logging.LogDebug('Unreachable: %s, %s' % (domain, e)) # Evaluate ping results, mark fastest N servers as preferred pings = [list(ping) + [ip] for ip, ping in six.iteritems(pinged)] while pings and len(servers_pref) < wanted_conns: mIdx = pings.index(min(pings)) if pings[mIdx][0] > 60: # This is worthless data, abort. break else: wanted_conns -= 1 ptime, uuid, ip = pings[mIdx] server = '%s:%s' % (ip, port) if uuid not in servers_all: servers_all[uuid] = server if uuid not in servers_pref: servers_pref[uuid] = server del pings[mIdx] # 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: # Note: Add to servers_pref to keep from getting disconnected servers_all['loopback'] = servers_pref['loopback'] = LOOPBACK_FE nvr = self.servers_never self.servers = [v for v in servers_all.values() if v not in nvr] self.servers_preferred = [v for v in servers_pref.values() if v not in nvr] 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(HaproxyProtocolFilter(self.ui)) 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: if tun is False: logging.LogInfo('Rejected', [('FE', server)]) self.ui.Notify('Rejected by %s' % server, prefix='!', color=self.ui.YELLOW) else: logging.LogInfo('Failed to connect', [('FE', server)]) self.ui.Notify('Failed to connect to %s' % server, prefix='!', color=self.ui.YELLOW) for line in logging.LOG[-5:]: if 'err' in line and 'ssl' in line['err'].lower(): self.ui.Notify('Unable to verify SSL certificates!') self.ui.Notify(socks.HAVE_PYOPENSSL and ' - Using pyOpenSSL wrapper, good.' or ' - Using standard Python ssl: try installing pyOpenSSL?') self.ui.Notify(' - CA certificates loaded: %s' % self.ca_certs) for dom in self.fe_certname: self.ui.Notify(' - Would accept a certificate for: %s' % dom) self.ui.Notify(' - Check your system clock (dates matter)') self.ui.Notify( ' - Beware firewalls that intercept outgoing SSL/TLS connections!') self.ui.Notify( ' - Danger Zone: use --fe_nocertcheck to connect insecurely.') # Dammit, if we know what the problem is, just fix it. if (self.ca_certs != self.pyfile and 'b5p.us' in (self.servers_auto or ['', ''])[1]): logging.LogInfo('Reconfiguring', [('ca_certs', self.pyfile)]) self.ui.Notify('Reconfiguring to use internal CA certificates', prefix="!", color=self.ui.RED) self.ca_certs = self.pyfile socks.setdefaultcertfile(self.ca_certs) return tun # False or None def DisconnectFrontend(self, conns, 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(list(six.iterkeys(tunnel.users))) < 1: logging.Log([('disconnect', server)]) tunnel.Die() return kill and True or False def CreateTunnels(self, conns): live_servers = conns.TunnelServers() failures = 0 connections = 0 now = time.time() # We re-evaluate our choices more frequently, if we have many # tunnel connections open. This speeds up the process of dropping # old conns, hopefully reducing the load on the relays. fec_interval = max(900, FE_PING_INTERVAL // max(1, len(live_servers))) if len(self.GetActiveBackends(include_loopback=True)) > 0: if (not self.servers) or len(self.servers) > len(live_servers): self.ChooseFrontEnds(live_servers) elif self.last_frontend_choice < (time.time() - fec_interval): self.servers = [] self.ChooseFrontEnds(live_servers, periodic=True) 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: self.servers_connected[server] = time.time() state[1] = self.ConnectFrontend(conns, server) except (IOError, OSError): state[1] = None for server in self.servers: if server not in live_servers: if server == LOOPBACK_FE: loop = LoopbackTunnel.Loop(conns, self.backends) loop.filters.append(HaproxyProtocolFilter(self.ui)) loop.filters.append(HttpHeaderFilter(self.ui)) if not self.insecure: loop.filters.append(HttpSecurityFilter(self.ui)) elif server not in self.servers_never: if now - self.servers_connected.get(server, 0) < 60: connections += 1 # Assume this one may still be in flight else: if server in self.servers_errored: del self.servers_errored[server] state = [None, None, server] 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, server in threads: thread.join(max(0.1, deadline - time.time())) for thread, result, server in threads: # This will treat timeouts/errors both as connections AND failures. # We record the errors, as input into the chooser. if result is not False: connections += 1 if result is not True: failures += 1 if result is None: if server in self.servers_connected: del self.servers_connected[server] self.servers_errored[server] = time.time() for server in live_servers: if (server not in self.servers and server not in self.servers_preferred): # Never disconnect after less than 10 minutes. if self.servers_connected.get(server, 0) < (time.time() - 600): if self.DisconnectFrontend(conns, server): if server in self.servers_connected: del self.servers_connected[server] connections += 1 if self.dyndns and ([time.time(), 0] > self.postpone_ddns_updates): ddns_fmt, ddns_args = self.dyndns domains = {} for bid in list(six.iterkeys(self.backends)): 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]: srv = tunnel.server_info[tunnel.S_NAME] ip = srv.rsplit(':', 1)[0] if not ip == LOOPBACK_HN and not tunnel.read_eof: if (not self.servers_preferred) or srv in self.servers_preferred: ips.append(ip) else: bips.append(ip) for ip in (ips or bips): if ip not in domains[domain][1]: domains[domain][1].append(ip) updates = {} for domain, (secret, ips) in six.iteritems(domains): if ips: # NOTE: Here it would be tempting to skip updates if we already # see correct results in DNS. We avoid this temptation, # because always updating DNS will resolve and mitigate # harms caused by stale DNS caches. The DDNS service just # has to deal with the load. 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) }) # Note: This may be wrong if different front-ends support different # protocols. Unfortunately, that isn't easily solvable. updates[payload] = ddns_fmt % args failed_updates = [] planned_updates = sorted(updates.values()) last_updates = sorted(self.last_updates) if last_updates != planned_updates: self.last_updates = [] for update in updates: if update in last_updates: self.last_updates.append(update) def _dnsup(results, update_url): try: results.append(''.join( s(l) for l in urlopen(update_url).readlines())) except: results.append('err: %s' % (format_exc(),)) for update in updates: if update in last_updates: continue domain, ips = update.split(':', 1) try: self.ui.Status('dyndns', color=self.ui.YELLOW, message='Updating DNS for %s...' % domain) r = [] _up = threading.Thread(target=_dnsup, args=(r, updates[update])) _up.daemon = True _up.start() _up.join(timeout=5) result = r[0] if r else 'timed out' if result.startswith('good') or result.startswith('nochg'): logging.Log([('dyndns', result), ('data', update)]) self.SetBackendStatus(domain, 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: failed_updates.append(domain) logging.LogInfo('DynDNS update failed: %s' % result, [('data', update)]) except Exception as e: failed_updates.append(update.split(':')[0]) logging.LogInfo('DynDNS update failed: %s' % e, [('data', update)]) if logging.DEBUG_IO: traceback.print_exc(file=sys.stderr) # Hmm, the update may have succeeded - assume the "worst". if domain not in self.dns_cache: self.dns_cache[domain] = {} self.dns_cache[domain][int(time.time())] = ips.split(',') # Avoid hammering broken services. break if failed_updates: for domain in failed_updates: self.SetBackendStatus(domain, add=BE_STATUS_ERR_DNS) failures += 1 # Exponential fallback for DDNS updates, up to at most half an hour. self.postpone_ddns_updates[1] += 1 self.postpone_ddns_updates[0] = int( time.time() + (56 * (2 ** min(5, self.postpone_ddns_updates[1])))) logging.LogInfo('DynDNS updates postponed until ts>%x (errors=%d)' % tuple(self.postpone_ddns_updates)) else: self.postpone_ddns_updates = [0, 0] # DDNS updates being postponed counts as at least one failure. if self.dyndns and self.postpone_ddns_updates[1]: failures = min(1, failures) 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') except: logging.LogFile = sys.stdout else: try: logging.LogFile = fd = open(filename, "a") 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 as 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 = [] 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) # Float division 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: with self.conns.lock: clist = copy.copy(self.conns.conns) bbc = 0 for c in clist: 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 try: fdc[fd] = fd # Our synthetic events need this 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) evs.append((fd, select.EPOLLIN)) # We might have missed events 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() if self.watchdog: self.watchdog.conns = self.conns.conns try: self.watchdog.locks['httpd.RCI.lock'] = self.ui_httpd.httpd.RCI.lock except AttributeError: pass if common.gYamon: self.watchdog.locks['YamonD.lock'] = common.gYamon.lock # FIXME: Add the AuthApp locks? for i in range(0, len(self.conns.auth_pool)): lock_name = 'conns.auth_pool[%d].qc' % i self.watchdog.locks[lock_name] = self.conns.auth_pool[i].qc self.watchdog.locks.update({ 'Connections.lock': self.conns.lock, 'SELECTABLE_LOCK': SELECTABLE_LOCK}) self.watchdog.start() epoll, mypoll = self.CreatePollObject() 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.10) 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 # This delay does things! # Pro: # - Reduce overhead by batching IO events together # Mixed: # - Along with Tunnel.maxread, this caps the per-stream/tunnel # bandwidth. The default SELECT_LOOP_MIN_MS=5, combined with # a MAX_READ_BYTES=16 (doubled for tunnels) lets us read from # the socket 200x/second: 200 * 32kB =~ 6MB/s. This is the # MAXIMUM outgoing bandwidth of any live tunnel, limiting # how much load any single connection can generate. Total # incoming bandwidth per-conn is half that. # Con: # - Adds latency # if self.isfrontend: snooze = max(0, (now + common.SELECT_LOOP_MIN_MS/1000.0) - time.time()) if snooze: if oready: snooze /= 2 time.sleep(snooze) else: snooze = 0 if 0 == (loop_count % (5 if logging.DEBUG_IO else 250)): logging.LogDebug('Loop #%d (i=%d, o=%d, e=%d, s=%.3fs) v%s' % (loop_count, len(iready), len(oready), len(eready), snooze, APPVER)) if self.watchdog: self.watchdog.patpatpat() 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', self.pyfile), ('version', APPVER), ('platform', sys.platform), ('python', sys.version.replace('\n', ' ')), ('argv', ' '.join(sys.argv[1:])), ('ca_certs', self.ca_certs), ('send_always_buffers', SEND_ALWAYS_BUFFERS), ('tunnel_socket_blocks', TUNNEL_SOCKET_BLOCKS)] for optf in self.rcfiles_loaded: config_report.append(('optfile_%s' % optf, 'ok')) logging.Log(config_report, level=logging.LOG_LEVEL) 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.LogDebug('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 as 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() # Report status of built-in HTTPD, update secret if self.ui_httpd and self.ui_httpd.httpd: httpd_sspec = '%s:%s' % self.ui_httpd.ui_sspec conf_secret = self.ConfigSecret() logging.Log([('builtin_httpd', httpd_sspec), ('secret', conf_secret)]) self.ui.Notify( 'Built-in HTTPD is on %s, secret=%s' % (httpd_sspec, u(conf_secret))) # 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.LogWarning( 'Warning: signal handler unavailable, logrotate will not work.') # Set up SIGUSR1 handler. try: import signal def dumpconns(x,y): logging.LogInfo('SIGUSR1 received, dumping conn state') Watchdog.DumpConnState(self.conns) signal.signal(signal.SIGUSR1, dumpconns) except Exception: logging.LogError('Warning: signal handler unavailable, kill -USR1 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: with open(self.pidfile, 'w') as pf: pf.write('%s\n' % os.getpid()) # 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_comm: self.ui_comm.quit() ##[ 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 as status: sys.exit(status) except Exception as e: if logging.DEBUG_IO: raise raise ConfigError(e) shell_mode = shell_mode or pk.shell if shell_mode is True: pk.FallDown('', help=False, noexit=True) else: pk.Start() except (ConfigError, getopt.GetoptError) as msg: pk.FallDown(msg, help=(not shell_mode), noexit=shell_mode) if shell_mode: shell_mode = 'more' except KeyboardInterrupt as msg: if shell_mode: pk.FallDown(None, help=False, noexit=True) shell_mode = 'auto' else: pk.ui.Status('exiting', message='Good-bye!') return except SystemExit as status: if shell_mode: shell_mode = 'more' else: sys.exit(status) except Exception as msg: crash_msg = format_exc() logging.LogDebug('Crashed: %s' % crash_msg) sys.stderr.write('Crashed: %s\n' % crash_msg) 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): from . 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: try: pk.ConfigureFromFile('.SELF/defaults.cfg') except (OSError, IOError): pass 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.stdin, 'isatty') and sys.stdin.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 list(six.iterkeys(pk.backends)) 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 = pk.CanSaveConfig(_raise=ConfigError) and True pk.CheckConfig() if pk.added_kites: if (pk.save or pk.ui.AskYesNo('Save settings to %s?' % pk.rcfile, default=(len(list(six.iterkeys(pk.backends))) > 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) PyPagekite-1.5.2.201011/pagekite/proto/000077500000000000000000000000001374056564300172555ustar00rootroot00000000000000PyPagekite-1.5.2.201011/pagekite/proto/__init__.py000066400000000000000000000017301374056564300213670ustar00rootroot00000000000000""" These are the PageKite protocol handling classes. """ ############################################################################## LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2020, 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: """ ############################################################################## PyPagekite-1.5.2.201011/pagekite/proto/conns.py000077500000000000000000002214641374056564300207630ustar00rootroot00000000000000""" These are the Connection classes, relatively high level classes that handle incoming or outgoing network connections. """ from __future__ import absolute_import from __future__ import division from __future__ import print_function ############################################################################## LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2020, 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 six import socket import sys import threading import time 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 * SMTP_PORTS = (25, 465, 587, 2525) 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 S_VERSION = 6 S_WEBSOCKET = 7 def __init__(self, conns): ChunkParser.__init__(self, ui=conns.config.ui) if conns.config.websocket_chunks: self.PrepareWebsockets() self.server_info = ['x.x.x.x:x', [], [], [], False, False, None, False] self.Init(conns) 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 = [] self.ip_limits = None self.maxread = int(common.MAX_READ_BYTES * common.MAX_READ_TUNNEL_X) def Cleanup(self, close=True): if self.users: for sid in list(six.iterkeys(self.users)): 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 RejectTraffic(self, client_conn, address, host): # This function allows the tunnel to reject an incoming connection # based on the remote address and the requested host. For now we # only know how to discriminate by remote IP. return self.RejectRemoteIP(client_conn, str(address[0]), host) or False def RejectRemoteIP(self, client_conn, ip, host): if not self.ip_limits: return False if len(self.ip_limits) == 1: whitelist = self.ip_limits[0] delta = maxips = seen = None else: whitelist = None delta, maxips, seen = self.ip_limits # Do we have a whitelist-only policy for this tunnel? if whitelist: for prefix in whitelist: if ip.startswith(prefix): return False self.LogError('Rejecting connection from unrecognized IP') return 'not_whitelisted' # Do we have a delta/maxips policy? if delta and maxips: # Since IP addresses are often shared, we try to differentiate browsers # based on few of the request headers as well. We don't track cookies # since they're mutated by the site itself, which would lead to false # positives here. client = ip log_info = [] if hasattr(client_conn, 'parser'): if hasattr(client_conn.parser, 'Header'): client = sha1hex('/'.join([ip] + (client_conn.parser.Header('User-Agent') or []) + (client_conn.parser.Header('Accept-Language') or []))) if hasattr(client_conn.parser, 'method'): log_info.append( (str(client_conn.parser.method), str(client_conn.parser.path))) now = time.time() if client in seen: seen[client] = now return False for seen_ip in list(six.iterkeys(seen)): if seen[seen_ip] < now - delta: del seen[seen_ip] if len(seen) >= maxips: self.LogError('Rejecting connection from new client', [('client', client[:12]), ('ips_per_sec', '%d/%ds' % (maxips, delta)), ('domain', host)] + log_info) return 'ips_per_sec' else: seen[client] = now return False # All else is allowed return False def ProcessPageKiteHeaders(self, parser): for prefix in ('X-Beanstalk', 'X-PageKite'): for feature in parser.Header(prefix+'-Features'): if feature == 'ZChunks': if not self.conns.config.disable_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 parser.Header(prefix+'-Version'): version = v if common.gYamon: common.gYamon.vadd('version-%s' % version, 1, wrap=10000000) self.server_info[self.S_VERSION] = version for replace in 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) def _FrontEnd(conn, body, conns): """This is what the front-end does when a back-end requests a new tunnel.""" self = Tunnel(conns) try: if 'websocket' in conn.parser.Header('Upgrade'): self.server_info[self.S_ADD_KITES] = True self.server_info[self.S_WEBSOCKET] = ( ''.join(conn.parser.Header('Sec-WebSocket-Key')) or True) self.ProcessPageKiteHeaders(conn.parser) requests = self.GetKiteRequests(conn.parser) except Exception as err: self.LogError('Discarding connection: %s' % err) self.Cleanup() return None except socket.error as err: self.LogInfo('Discarding connection: %s' % err) self.Cleanup() return None try: ips, seconds = conns.config.GetDefaultIPsPerSecond() self.UpdateIP_Limits(ips, seconds) except ValueError: pass 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)) elif self.server_info[self.S_WEBSOCKET]: self.AuthCallback(conn, [], []) 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 self.server_info[self.S_WEBSOCKET]: logi.append(('websocket', 'True')) if self.server_info[self.S_VERSION]: logi.append(('version', self.server_info[self.S_VERSION])) 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, level=logging.LOG_LEVEL_MACH) self.conns.CloseTunnel(proto, domain, self) # Update IP rate limits, if necessary first = True for r in results: if r[0] in ('X-PageKite-IPsPerSec',): ips, seconds = [int(x) for x in r[1].split('/')] self.UpdateIP_Limits(ips, seconds, force=first) first = False if first: for backend in ok: try: proto, domain, srand = backend.split(':') ips, seconds = self.conns.config.GetDefaultIPsPerSecond(domain) self.UpdateIP_Limits(ips, seconds) except ValueError: pass if add_tunnels: if self.ip_limits and len(self.ip_limits) > 2: logi.append(('ips_per_sec', '%d/%ds' % (self.ip_limits[1], self.ip_limits[0]))) 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, level=logging.LOG_LEVEL_MACH) 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.LogWarning('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.LogWarning('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) if self.server_info[self.S_WEBSOCKET]: output = [HTTP_WebsocketResponse(self.server_info[self.S_WEBSOCKET])] extras = [] else: output = [HTTP_ResponseHeader(200, 'OK'), HTTP_Header('Transfer-Encoding', 'chunked')] extras = output if not self.conns.config.disable_zchunks: output.append(HTTP_Header('X-PageKite-Features', 'ZChunks')) extras.extend([ HTTP_Header('X-PageKite-Features', 'WebSockets'), 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 self.conns.config.server_raw_ports: extras.append( HTTP_Header('X-PageKite-Raw-Ports', ', '.join(['%s' % p for p in self.conns.config.server_raw_ports]))) for r in results: extras.append('%s: %s\r\n' % r) output.append(HTTP_StartBody()) if not self.Send(output, activity=False): 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.server_info[self.S_WEBSOCKET]: self.EnableWebsockets() self.SendChunked('NOOP: 1\r\n%s\r\n!' % ''.join(extras)) self.conns.Add(self) elif 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, level=logging.LOG_LEVEL_MACH) 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 = s(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 = server.rsplit(':', 1) 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: pp = socks.parseproxy chain = [pp('default')] if self.conns.config.fe_nocertcheck: chain.append([socks.PROXY_TYPE_SSL_WEAK, sspec[0], int(sspec[1])]) elif self.conns.config.fe_certname: chain.append(pp('http!%s!%s' % (sspec[0], sspec[1]))) chain.append(pp('ssl!%s!443' % ','.join(self.conns.config.fe_certname))) for hop in chain: sock.addproxy(*hop) self.SetFD(sock) try: # Note: This value is a magic number which should correlate with # bounds on auth thread queue length, set in AuthThread._run(). self.fd.settimeout(30.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, websocket_key=self.websocket_key), activity=False, try_flush=True, allow_blocking=False) or not self.Flush(wait=True, allow_blocking=False)): self.LogError('Failed to send kite request, closing.') raise IOError('Failed to send kite request, closing.') data = self._RecvHttpHeaders() if not data: self.LogError('Failed to parse kite response, closing.') raise IOError('Failed to parse kite response, closing.') self.fd.setblocking(0) parse = HttpLineParser(lines=data.splitlines(), state=HttpLineParser.IN_RESPONSE) return data, parse def CheckForTokens(self, parse): tcount = 0 tokens = {} if parse: 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 UpdateIP_Limits(self, ips, seconds, force=False): if self.ip_limits and len(self.ip_limits) > 2 and not force: new_rate = float(ips)/(seconds or 1) # Float division old_rate = float(self.ip_limits[1] or 9999)/(self.ip_limits[0] or 1) # Float division if new_rate < old_rate: self.ip_limits[0] = seconds self.ip_limits[1] = ips else: self.ip_limits = [(seconds or 1), ips, {}] 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) logged = 0 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, level=logging.LOG_LEVEL_MACH) if 'motd' in args and args['motd'][0]: config.ui.NotifyMOTD(sname, args['motd'][0]) logged += 1 # FIXME: Really, we should keep track of quota dimensions for # each kite. At the moment that isn't even reported... quota_log = [] for quota in parse.Header('X-PageKite-Quota'): self.quota = [float(quota), None, None] quota_log.append(('quota_bw', quota)) for quota in parse.Header('X-PageKite-QConns'): self.q_conns = float(quota) quota_log.append(('quota_conns', quota)) for quota in parse.Header('X-PageKite-QDays'): self.q_days = float(quota) quota_log.append(('quota_days', quota)) for quota in parse.Header('X-PageKite-IPsPerSec'): quota_log.append(('ips_per_sec', quota)) try: config.ui.NotifyIPsPerSec(*[int(i) for i in quota.split('/')]) except ValueError: pass if quota_log: self.Log([('FE', sname)] + quota_log) logged += 1 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] logged += 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)], level=logging.LOG_LEVEL_WARN) config.ui.NotifyKiteRejected(proto, domain, reason, crit=True) config.SetBackendStatus(domain, proto, add=BE_STATUS_ERR_TUNNEL) logged += 1 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)], level=logging.LOG_LEVEL_WARN) config.ui.NotifyKiteRejected(proto, domain, 'duplicate') config.SetBackendStatus(domain, proto, add=BE_STATUS_ERR_TUNNEL) logged += 1 ssl_available = {} for request in parse.Header('X-PageKite-SSL-OK'): ssl_available[request] = True logged += 1 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))], level=logging.LOG_LEVEL_INFO) config.SetBackendStatus(domain, proto, add=status) logged += 1 if logged: if self.quota and self.quota[0] is not None: config.ui.NotifyQuota(self.quota[0], self.q_days, self.q_conns) # Also log the server capabilities logging.Log([ ('FE', sname), ('ports', ','.join(self.server_info[self.S_PORTS])), ('protocols', ','.join(self.server_info[self.S_PROTOS])), ('raw_ports', ','.join(self.server_info[self.S_RAW_PORTS] or []))]) 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) if conns and not conns.config.isfrontend: self.ExtendSSLRetryDelays() 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 for upgrade in parse.Header('Upgrade'): if upgrade.lower() == 'websocket': self.EnableWebsockets() abort = data = parse = False 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 as e: self.LogError('Connect failed: %s' % e) self.Cleanup() return None if abort: return False 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: if 'data_out' in f.FILTERS: try: data = f.filter_data_out(self, sid, data) except: logging.LogWarning(('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') # Small amounts of data we just send... if len(data) <= 1024: sending.append(data) return self.SendChunked(sending, zhistory=self.zhistory.get(sid)) # Larger amounts we break into fragments at the FE, to work around bugs # in some of our small-buffered embedded clients. We aim for roughly # one fragment per packet, assuming an MTU of 1500 bytes. We use # much larger fragments at the back-end, relays can be assumed to # be up-to-date and larger chunks saves CPU and improves throughput. frag_size = self.conns.config.isfrontend and 1024 or (self.maxread+1024) sending.append('') frag_size = max(frag_size, 1400-len(''.join(sending))) first = True while data or first: sending[-1] = data[:frag_size] if not self.SendChunked(sending, zhistory=self.zhistory.get(sid)): return False data = data[frag_size:] if first: sending = ['SID: %s\r\n' % sid, '\r\n', ''] frag_size = max(frag_size, 1400-len(''.join(sending))) first = False return True 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.Log([ ('FE', self.server_info[self.S_NAME]), ('pinged_tunnel', '@%.4f' % now)], level=logging.LOG_LEVEL_DEBUG) return self.SendChunked('NOOP: 1\r\nPING: %.4f\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 = int(0.9 * self.weighted_rtt + 0.1 * rtt) sname = self.server_info[self.S_NAME] log_info = [('FE', sname), ('tunnel_ping_ms', '%d' % rtt), ('tunnel_ping_wrtt', '%d' % self.weighted_rtt)] if self.weighted_rtt > 2500: # Magic number: 2.5 seconds is a long time! if not self.conns.config.isfrontend: # If the weighted RTT is this high, then we've had poor connectivity # for quite some time. Set things in motion to try another relay. self.conns.config.servers_errored[sname] = time.time() self.conns.config.last_frontend_choice = 0 # Avoid re-triggering again right away self.weighted_rtt = 0 log_info.append(('flagged', 'Flagged relay as broken')) self.Log(log_info, level=( logging.LOG_LEVEL_WARN if ('flagged' in log_info) else logging.LOG_LEVEL_INFO)) 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): msg = ('NOOP: 1\r\n' 'SID: %s\r\n' 'SKB: %d\r\n\r\n') % (sid, (conn.all_out + conn.wrote_bytes)/1024) return self.SendChunked(msg, 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 ProgressTo(self, parse): try: sid = int(parse.Header('SID')[0]) skb = int((parse.Header('SKB') or [-1])[0]) if sid in self.users: self.users[sid].RecordProgress(skb) 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, connecting=False): """Pass incoming data through filters, if we have any.""" for f in self.filters: if 'data_in' in f.FILTERS or (connecting and 'connected' in f.FILTERS): try: if sid and info: f.filter_set_sid(sid, info) if connecting and 'connected' in f.FILTERS: data = f.filter_connected(self, sid, data) if data is not None: data = f.filter_data_in(self, sid, data) except: logging.LogWarning(('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), 'proxyproto': conn and conn.config.get('proxyproto', False), 'rewritehost': rewritehost }, connecting=True) 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, self.HTTP_Unavail( self.conns.config, 'be', proto, host, code=code )), just_buffer=True): return False, False else: 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 return conn, data def ProcessKiteUpdates(self, parse): # Look for requests for new tunnels if self.conns.config.isfrontend: self.ProcessPageKiteHeaders(parse) requests = self.GetKiteRequests(parse) if requests: self.conns.auth().check(requests[:], self, lambda r, l: self.ChunkAuthCallback(r, l)) else: self.ParsePageKiteCapabilities(parse) # 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 = ''.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 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.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 list(six.iterkeys(backends)): 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 ''), quote=False) if PY3 else 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! try: self = UserConn(address, ui=conns.config.ui) except (ValueError, IOError, OSError): conn.LogError('Unable to create new connection object!') return None self.conns = conns self.SetConn(conn) if ':' in host: host, port = host.split(':', 1) self.proto = oproto = proto self.host = StripEncodedIP(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', 'xmpp'] 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.extend(['http', 'http2', 'http3']) elif proto == 'http': protos.extend(['http2', 'http3']) tunnels = [] for p in protos: for prt in ports: if not tunnels: tunnels = conns.Tunnel('%s-%s' % (p, prt), host) if tunnels: self.proto = proto = p if not tunnels: tunnels = conns.Tunnel(p, host) if tunnels: self.proto = proto = p if not tunnels: tunnels = conns.Tunnel(protos[0], CATCHALL_HN) if tunnels: self.proto = proto = protos[0] if self.address: chunk_headers = [('RIP', self.address[0]), ('RPort', self.address[1])] if conn.my_tls: chunk_headers.append(('RTLS', 1)) if len(tunnels) > 1: tunnels.sort(key=lambda t: t.weighted_rtt) for tun in tunnels: rejection = tun.RejectTraffic(conn, address, host) if rejection and hasattr(conn, 'error_details'): conn.error_details['rejected'] = rejection else: self.tunnel = tun break if (self.tunnel and self.tunnel.SendData(self, ''.join(body), host=host, proto=proto, port=on_port, chunk_headers=chunk_headers) and self.conns): log_info = [('domain', self.host), ('on_port', on_port), ('proto', self.proto), ('is', 'FE')] if oproto != proto: log_info.append(('sniffed_proto', proto)) self.Log(log_info) self.conns.Add(self) if proto in ('http', 'http2', 'http3', 'websocket'): self.conns.TrackIP(address[0], host) # FIXME: Use the tracked data to detect & mitigate abuse? return self 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. try: self = UserConn(None, ui=tunnel.conns.config.ui) except (ValueError, IOError, OSError): tunnel.LogDebug('Unable to create new connection object!') return None 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')] # 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 remote_ip: logInfo.append(('remote_ip', remote_ip)) 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' # Parse things! if proto in ('websocket', 'http', 'http2', 'http3'): http_parse = HttpLineParser(lines=data.splitlines()) logInfo[0:0] = [(http_parse.method, http_parse.path)] else: http_parse = 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 http_parse: auth = http_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, level=logging.LOG_LEVEL_ERR) 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 as 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, level=logging.LOG_LEVEL_ERR) 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 as 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 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 self.sid = -1 self.host = None self.proto = None self.said_hello = False self.bad_loops = 0 self.error_details = {} # Set up our parser chain. self.parsers = [HttpLineParser] if IrcLineParser.PROTO in conns.config.server_protos: self.parsers.append(IrcLineParser) self.parser = MagicLineParser(parsers=self.parsers) self.conns = conns self.conns.Add(self) self.conns.SetIdle(self, 10) 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 SMTP_PORTS: 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 = StripEncodedIP(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(self.HTTP_Unavail( self.conns.config, 'fe', 'raw', chost, code=403, status='Forbidden', other_details=self.error_details), 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',): 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 == 'GET' and self.parser.path in MAGIC_PATHS and 'v1.pagekite.org' in self.parser.Header('Sec-WebSocket-Protocol') and 'websocket' in self.parser.Header('Upgrade')): 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 = StripEncodedIP(hosts[0].lower()) else: self.Send(HTTP_Response(400, 'Bad request', ['

400 Bad request

', '

Invalid request, no Host: found.

', '\n'], trackable=True, overloaded=self.conns.config.Overloaded())) return False if self.parser.path.startswith(MAGIC_PREFIX): try: self.host = StripEncodedIP(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(self.HTTP_Unavail( self.conns.config, 'fe', self.proto, self.host, code=403, status='Forbidden', other_details=self.error_details), try_flush=True) self.Cleanup(close=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(self.HTTP_Unavail( self.conns.config, 'fe', self.proto, self.host, overloaded=self.conns.config.Overloaded(), other_details=self.error_details ), try_flush=True) self.Cleanup(close=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 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): with self.qc: while not self.lines: self.qc.wait() line = self.lines.pop(0) 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: with self.qc: self.lines.append(line) self.qc.notify() 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 FastPingHelper(threading.Thread): def __init__(self, conns): threading.Thread.__init__(self) self.daemon = True self.lock = threading.Lock() self.conns = conns self.config = conns.config self.clients = [] self.rejection = None self.overloaded = False self.waiting = True self.sleeptime = 0.03 self.fast_pinged = [] self.next_pinglog = time.time() + 1 self.wq = Queue() self.up_rejection() def up_rejection(self): self.overloaded = self.config.Overloaded() self.rejection = HTTP_Unavailable('fe', 'http', 'ping.pagekite', overloaded=self.overloaded, advertise=False) def add_client(self, client, addr, handler): client.setblocking(0) with self.lock: self.clients.append((time.time(), client, addr, handler)) if self.waiting: self.wq.put(1) def run_once(self): now = time.time() with self.lock: _clients, self.clients = self.clients, [] for ts, client, addr, handler in _clients: try: data = client.recv(64, socket.MSG_PEEK) except: data = None try: if data: if '\nHost: ping.pagekite' in data: client.send(self.rejection) client.close() self.fast_pinged.append(obfuIp(addr[0])) else: handler(client, addr) elif ts > (now-5): with self.lock: self.clients.append((ts, client, addr, handler)) else: logging.LogDebug('Timeout, dropping ' + obfuIp(addr[0])) client.close() except IOError: logging.LogDebug('IOError, dropping ' + obfuIp(addr[0])) # No action: just let the client get garbage collected except: logging.LogDebug('Error in FastPing: ' + format_exc()) if now > self.next_pinglog: logging.LogDebug('Fast ping %s %d clients: %s' % ( 'discouraged' if self.overloaded else 'welcomed', len(self.fast_pinged), ', '.join(self.fast_pinged))) self.fast_pinged = [] self.up_rejection() self.next_pinglog = now + 1 self.sleeptime = max(0, (now + 0.015) - time.time()) def run_until(self, deadline): try: while (time.time() + self.sleeptime) < deadline and self.clients: with self.lock: self.waiting = True while not self.wq.empty(): self.wq.get() self.waiting = False time.sleep(self.sleeptime) self.run_once() except: logging.LogError('FastPingHelper crashed: ' + format_exc()) def run(self): while True: try: while True: with self.lock: self.waiting = True while not self.clients or not self.wq.empty(): self.wq.get() self.waiting = False time.sleep(self.sleeptime) self.run_once() except: logging.LogError('FastPingHelper crashed: ' + format_exc()) time.sleep(1) 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 and os.path.exists(self.acl): try: ipaddr = '%s' % ipaddr lc = 0 with open(self.acl, 'r') as fd: for line in fd: 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: %s' % (self.acl, format_exc())) self.acl_match = (0, '.*', default and 'allow' or 'reject', 'Default') return default def HandleClient(self, client, address): log_info = [('port', self.port)] 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 += [('acl_line', '%s' % self.acl_match[0]), ('reason', self.acl_match[3])] self.Log(log_info) return True def ReadData(self, maxread=None): try: self.sstate = 'accept' self.last_activity = time.time() client, address = self.fd.accept() if self.port not in SMTP_PORTS: while client: try: self.conns.ping_helper.add_client(client, address, self.HandleClient) client, address = self.fd.accept() except IOError: client = None elif client: self.sstate = 'client' self.HandleClient(client, address) self.sstate = (self.dead and 'dead' or 'idle') return True except IOError as err: self.sstate += '/ioerr=%s' % (err.errno,) self.LogDebug('Listener::ReadData: error: %s (%s)' % (err, err.errno)) except socket.error as e: (errno, msg) = e self.sstate += '/sockerr=%s' % (errno,) self.LogInfo('Listener::ReadData: error: %s (errno=%s)' % (msg, errno)) except Exception as e: self.sstate += '/exc' self.LogDebug('Listener::ReadData: %s' % e) return True PyPagekite-1.5.2.201011/pagekite/proto/filters.py000077500000000000000000000203341374056564300213040ustar00rootroot00000000000000""" These are filters placed at the end of a tunnel for watching or modifying the traffic. """ ############################################################################## from __future__ import absolute_import LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2020, 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 six import re import time import pagekite.logging as logging from pagekite.compat import * class TunnelFilter: """Base class for watchers/filters for data going in/out of Tunnels.""" FILTERS = ('connected', 'data_in', 'data_out') 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 list(six.iterkeys(self.sid)): 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_connected(self, tunnel, sid, data): if sid not in self.sid: self.sid[sid] = {} self.sid[sid]['_ts'] = time.time() return data 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.""" FILTERS = ('data_in', 'data_out') 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 / %s ]===' % (self.now(), sid), 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 / %s ]===' % (self.now(), sid), 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 HaproxyProtocolFilter(TunnelFilter): """Filter prefixes the HAProxy PROXY protocol info to requests.""" FILTERS = ('connected') ENABLE = 'proxyproto' def filter_connected(self, tunnel, sid, data): info = self.sid.get(sid) if info: if not info.get(self.ENABLE, False): pass elif info[self.ENABLE] in ("1", True): remote_ip = info['remote_ip'] if '.' in remote_ip: remote_ip = remote_ip.rsplit(':', 1)[1] data = 'PROXY TCP%s %s 0.0.0.0 %s %s\r\n%s' % ( '4' if ('.' in remote_ip) else '6', remote_ip, info['remote_port'], info['port'], data or '') else: logging.LogError( 'FIXME: Unimplemented PROXY protocol v%s\n' % info[self.ENABLE]) return TunnelFilter.filter_connected(self, tunnel, sid, data) class HttpHeaderFilter(TunnelFilter): """Filter that adds X-Forwarded-For and X-Forwarded-Proto to requests.""" FILTERS = ('data_in') 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 PyPagekite-1.5.2.201011/pagekite/proto/parsers.py000077500000000000000000000154421374056564300213170ustar00rootroot00000000000000""" Protocol parsers for classifying incoming network connections. """ ############################################################################## from __future__ import absolute_import LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2020, 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 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'] EMBEDDED_IP_RE = re.compile(r'^0x(?:[0-9a-fA-F]{8,8}|[0-9a-fA-F]{32,32})--') def StripEncodedIP(domain): if domain: return EMBEDDED_IP_RE.sub('', str(domain)) return domain 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(' ', 2) 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 as err: logging.LogDebug('HTTP 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 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 as err: logging.LogDebug('IRC parse failed: %s, %s, %s' % (self.state, err, self.lines)) self.state = BaseLineParser.PARSE_FAILED return (self.state != BaseLineParser.PARSE_FAILED) PyPagekite-1.5.2.201011/pagekite/proto/proto.py000077500000000000000000000310111374056564300207710ustar00rootroot00000000000000""" PageKite protocol and HTTP protocol related code and constants. """ ############################################################################## from __future__ import absolute_import from __future__ import division LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2020, 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 six 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: with open('/dev/urandom', 'rb') as fd: newSecret = sha1hex(fd.read(64) + gSecret) gSecret = newSecret logging.LogDebug('Seeded signatures using /dev/urandom, hooray!') except: try: newSecret = sha1hex(s(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) # Integer division 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, replace=None): req = ['X-PageKite-Version: %s\r\n' % APPVER] if replace: req.append('X-PageKite-Replace: %s\r\n' % replace) tokens = tokens or {} for d in list(six.iterkeys(backends)): 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, websocket_key=None): if websocket_key is not None: key = base64.b64encode(websocket_key).strip() req = ['GET %s HTTP/1.1\r\n' % MAGIC_PATH, 'Upgrade: websocket\r\n', 'Connection: Upgrade\r\n', 'Sec-WebSocket-Key: %s\r\n' % key, 'Sec-WebSocket-Protocol: v1.pagekite.org\r\n', 'Sec-WebSocket-Version: 13\r\n'] else: 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 tls: req.append('X-PageKite-Features: TLS\r\n') req.extend( PageKiteRequestHeaders(server, backends, tokens=tokens, testtoken=testtoken,replace=replace)) req.append('\r\n') return ''.join(req) def HTTP_WebsocketResponse(ws_key): signed_key = sha1b64(ws_key.strip() + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11') return ('HTTP/1.1 101 Switching Protocols\r\n' 'Upgrade: websocket\r\nConnection: upgrade\r\n' 'Sec-WebSocket-Accept: %s\r\nSec-WebSocket-Protocol: v1.pagekite.org\r\n' % signed_key) def HTTP_ResponseHeader(code, title, mimetype='text/html', first_headers=None): if mimetype.startswith('text/') and ';' not in mimetype: mimetype += ('; charset=%s' % DEFAULT_CHARSET) return ('HTTP/1.1 %s %s\r\n%sContent-Type: %s\r\nPragma: no-cache\r\n' 'Expires: 0\r\nCache-Control: no-store\r\nConnection: close' '\r\n') % (code, title, ''.join(first_headers or []), 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, overloaded=False): if overloaded or trackable: headers = headers or [] if trackable: # Put this first... headers = [ HTTP_Header('X-PageKite-UUID', MAGIC_UUID_SHA1) ] + headers if overloaded: # No, put this first! headers = [HTTP_Header('X-PageKite-Overloaded', 'Sorry')] + headers return ''.join([ HTTP_ResponseHeader(code, title, mimetype, first_headers=headers), HTTP_StartBody(), ''.join(body)]) 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, overloaded=False, advertise=True, relay_sockname=None, other_details=None): if advertise: label = "PageKite" whatis = ''.join(['', label, '']) else: label = "Connection" whatis = "connection" if code == 401: headers = headers or [] headers.append(HTTP_Header('WWW-Authenticate', 'Basic realm=%s' % label)) message = ''.join(['

Sorry! (', where, ')

', '

The ', proto.upper(), ' ', whatis, ' 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)) if relay_sockname is not None: frame_url += ('&relay=%s' % relay_sockname[0]) for key, val in (other_details or {}).items(): frame_url += ('&%s=%s' % (key, val)) return HTTP_Response(code, status, ['', '', '', message, '', '\n'], headers=headers, trackable=True, overloaded=overloaded) else: return HTTP_Response(code, status, ['', message, '\n'], headers=headers, trackable=True, overloaded=overloaded) 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 PyPagekite-1.5.2.201011/pagekite/proto/selectables.py000077500000000000000000000711071374056564300221260ustar00rootroot00000000000000""" Selectables are low level base classes which cooperate with our select-loop. """ from __future__ import absolute_import from __future__ import division from __future__ import print_function ############################################################################## LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2020, 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 os import re import struct import threading import time import zlib from pagekite.compat import * from pagekite.common import * from pagekite.proto.proto import HTTP_Unavailable from pagekite.proto.ws_abnf import ABNF 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.RLock() # threading.Lock() will deadlock on pypy! SELECTABLE_ID = 0 SELECTABLES = set([]) def getSelectableId(what): global SELECTABLES, SELECTABLE_ID, SELECTABLE_LOCK with SELECTABLE_LOCK: count = 0 SELECTABLE_ID += 1 SELECTABLE_ID %= 0x20000 while SELECTABLE_ID in SELECTABLES: SELECTABLE_ID += 1 SELECTABLE_ID %= 0x20000 count += 1 if count > 0x20000: raise ValueError('Too many conns!') SELECTABLES.add(SELECTABLE_ID) return SELECTABLE_ID 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=None, ui=None, tracked=True, bind=None, backlog=100): self.fd = None try: if bind and bind[0] and re.match(r'^\d+\.\d+\.\d+\.\d+$', bind[0]): raise ValueError('Avoid INET6 for IPv4 hosts') 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.lock = threading.RLock() 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 or common.MAX_READ_BYTES self.read_bytes = self.all_in = 0 self.read_eof = False self.peeking = False self.peeked = 0 self.retry_delays = [0.0, 0.02, 0.05] # Write-related variables self.wrote_bytes = self.all_out = 0 self.write_blocked = '' self.write_speed = 102400 self.write_eof = False # Flow control v2 self.acked_kb_delta = 0 # Compression stuff self.zw = None self.zlevel = 1 self.zreset = False # This is the default, until we switch to Websockets... self.use_websocket = False self.ws_zero_mask = False # Logging self.sstate = 'new' 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 ExtendSSLRetryDelays(self): self.LogDebug('Extended SSL Write retries on %s' % self) self.retry_delays = [0.0, 0.02, 0.05, 0.30, 0.70, 1.5, 2.0] def CountAs(self, what): with self.lock: if common.gYamon: common.gYamon.vadd(self.countas, -1) common.gYamon.vadd(what, 1) self.countas = what 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.sstate = 'dead' self.CountAs('selectables_dead') if close: self.LogTraffic(final=True) try: global SELECTABLES, SELECTABLE_LOCK with SELECTABLE_LOCK: SELECTABLES.remove(self.gsid) except KeyError: pass def __del__(self): # Important: This can run at random times, especially under pypy, so all # locks must be re-entrant (RLock), otherwise we deadlock. try: with self.lock: if common.gYamon and self.countas: common.gYamon.vadd(self.countas, -1) common.gYamon.vadd('selectables', -1) self.countas = None except AttributeError: pass def __str__(self): return '%s: %s<%s|%s%s%s>' % (self.log_id, self.__class__, self.sstate, self.read_eof and '-' or 'r', self.write_eof and '-' or 'w', len(self.write_blocked)) def __html__(self): try: peer = self.fd.getpeername() except: peer = ('x.x.x.x', 'x') try: sock = self.fd.getsockname() except: 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.sstate) def ResetZChunks(self): with self.lock: if self.zw: self.zreset = True self.zw = zlib.compressobj(self.zlevel) def EnableZChunks(self, level=1): with self.lock: self.zlevel = level self.zw = zlib.compressobj(level) def EnableWebsockets(self): with self.lock: self.use_websocket = True self.ws_zero_mask = ( hasattr(self.fd, 'get_cipher_name') or hasattr(self.fd, 'getpeercert')) 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, level=logging.LOG_LEVEL_DEFAULT): if self.log_id: values.append(('id', self.log_id)) logging.Log(values, level=level) def LogError(self, error, params=None): values = params or [] if self.log_id: values.extend([('id', self.log_id), ('s', self.sstate)]) logging.LogError(error, values) def LogDebug(self, message, params=None): values = params or [] if self.log_id: values.extend([('id', self.log_id), ('s', self.sstate)]) logging.LogDebug(message, values) def LogWarning(self, warning, params=None): values = params or [] if self.log_id: values.append(('id', self.log_id)) logging.LogWarning(warning, values) def LogInfo(self, message, params=None): values = params or [] if self.log_id: values.extend([('id', self.log_id), ('s', self.sstate)]) 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) log_info = [('wrote', '%d' % self.wrote_bytes), ('wbps', '%d' % self.write_speed), ('read', '%d' % self.read_bytes)] if self.acked_kb_delta: log_info.append(('delta', '%d' % self.acked_kb_delta)) if final: log_info.append(('eof', '1')) self.Log(log_info) self.bytes_logged = now self.wrote_bytes = self.read_bytes = 0 elif final: self.Log([('eof', '1')], level=logging.LOG_LEVEL_MACH) 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: bytecount = eat_bytes - len(discard) self.sstate = 'eat(%d)' % bytecount discard += self.fd.recv(bytecount) except socket.error as err: (errno, msg) = err.args 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.sstate = 'ate(%d)' % 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 try: if self.peeking: self.sstate = 'peek(%d)' % maxread data = s(self.fd.recv(maxread, socket.MSG_PEEK)) self.peeked = len(data) if logging.DEBUG_IO: print('<== PEEK =[%s]==(\n%s)==' % (self, data[:320])) else: self.sstate = 'read(%d)' % maxread data = s(self.fd.recv(maxread)) if logging.DEBUG_IO: print('<== IN =[%s]==(\n%s)==' % (self, data[:160])) self.sstate = 'data(%d)' % len(data) except (SSL.WantReadError, SSL.WantWriteError): self.sstate += '/SSL.WRE' return True except IOError as err: self.sstate += '/ioerr=%s' % (err.errno,) 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) as err: self.sstate += '/SSL.Error' self.LogDebug('Error reading socket (SSL): %s' % err) return False except socket.error as err: (errno, msg) = err.args self.sstate += '/sockerr=%s' % (err.errno,) if errno in self.HARMLESS_ERRNOS: return True else: self.LogInfo('Error reading socket: %s (errno=%s)' % (msg, errno)) return False try: self.last_activity = now if data is None or data == '': self.sstate += '/EOF' 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) finally: self.sstate = (self.dead and 'dead' or 'idle') def RecordProgress(self, skb): if skb >= 0: all_read = (self.all_in + self.read_bytes) // 1024 self.acked_kb_delta = max(1, all_read - skb) def Send(self, data, try_flush=False, activity=False, just_buffer=False, allow_blocking=False): global SEND_MAX_BYTES self.write_speed = int((self.wrote_bytes + self.all_out) / max(1, (time.time() - self.created))) # Integer division # 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 pending = ''.join([self.write_blocked, str(''.join(data))]) self.write_blocked = '' if pending: try: sent = None send_bytes = min(len(pending), SEND_MAX_BYTES) send_buffer = b(pending[:send_bytes]) self.sstate = 'send(%d)' % (send_bytes) for try_wait in self.retry_delays: try: sent = self.fd.send(send_buffer) if logging.DEBUG_IO: print(('==> OUT =[%s: %d/%d bytes]==(\n%s)==' ) % (self, sent, send_bytes, send_buffer[:min(320, sent)])) self.wrote_bytes += sent break except (SSL.WantWriteError, SSL.WantReadError) as err: SEND_MAX_BYTES = min(4096, SEND_MAX_BYTES) # Maybe this will help? if logging.DEBUG_IO: print('=== WRITE SSL RETRY: =[%s: %s bytes]==' % (self, send_bytes)) self.sstate = 'send/SSL.WRE(%d,%.1f)' % (send_bytes, try_wait) time.sleep(try_wait) if sent is None: self.sstate += '/retries' self.LogInfo( 'Error sending: Too many SSL write retries (SEND_MAX_BYTES=%d)' % SEND_MAX_BYTES) self.ProcessEofWrite() return False except IOError as err: self.sstate += '/ioerr=%s' % (err.errno,) 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, send_bytes)) except socket.error as err: (errno, msg) = err.args self.sstate += '/sockerr=%s' % (errno,) 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, send_bytes)) except (SSL.Error, SSL.ZeroReturnError, SSL.SysCallError) as err: self.sstate += '/SSL.Error' self.LogInfo( 'Error sending (SSL, SEND_MAX_BYTES=%d): %s' % (SEND_MAX_BYTES, err)) SEND_MAX_BYTES = min(4096, SEND_MAX_BYTES) # Maybe this will help? self.ProcessEofWrite() return False except AttributeError: self.sstate += '/AttrError' # 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 self.write_blocked = pending[sent:] if activity: self.last_activity = time.time() if self.wrote_bytes >= logging.LOG_THRESHOLD: self.LogTraffic() if self.write_eof and not self.write_blocked: self.ProcessEofWrite() self.sstate = (self.dead and 'dead' or 'idle') return True def SendChunked(self, data, compress=True, zhistory=None, just_buffer=False): if self.use_websocket: with self.lock: return self.Send( [ABNF.create_frame( ''.join(data), ABNF.OPCODE_BINARY, 1, self.ws_zero_mask).format()], activity=False, try_flush=(not just_buffer), just_buffer=just_buffer) 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 with self.lock: try: sdata = ''.join(data) if self.zw and compress and len(sdata) > 64: try: zdata = s(self.zw.compress(b(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 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 < (3 * s.maxread/1024))) 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 def HTTP_Unavail(self, config, where, proto, host, **kwargs): kwargs['frame_url'] = config.error_url if self.fd and where in ('FE', 'fe'): try: kwargs['relay_sockname'] = self.fd.getsockname() except: kwargs['relay_sockname'] = None # Do we have a more specific error URL for this domain? This is a # white-label feature, for folks not wanting to hit the PageKite.net # servers at all. In case of a match, we also disable mention of # PageKite itself in the HTML boilerplate. dparts = host.split(':')[0].split('.') while dparts: fu = config.error_urls.get('.'.join(dparts), None) if fu is not None: kwargs['frame_url'] = fu kwargs['advertise'] = False break dparts.pop(0) return HTTP_Unavailable(where, proto, host, **kwargs) 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' % 0o26 SSL_CLIENTHELLO = '\x80' XML_PREAMBLE = ']+\sto=([^\s>]+)[^>]*>") 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)) def ProcessData(self, data): # Uncomment when adding support for new protocols: # #print(('DATA: >%s<' # ) % ' '.join(['%2.2x' % ord(d) for d in data])) if self.might_be_tls: self.might_be_tls = False if not (data.startswith(TLS_CLIENTHELLO) or data.startswith(SSL_CLIENTHELLO)): self.EatPeeked() # Only works if the full request is in the first data packet. if data.startswith(XML_PREAMBLE): server = self.GetXMPPServer(data) if server: return self.ProcessProto(data, 'xmpp', 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 GetXMPPServer(self, data): match = XMPP_REGEXP.search(data) if match and match.group(1): server = match.group(1) if server[:1] in ('"', "'"): server = server[1:-1] if '@' in server: server = server.split('@')[-1] return server else: return None 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 = None self.websocket_key = None def ProcessData(self, *args, **kwargs): if self.use_websocket: return self.ProcessWebsocketData(*args, **kwargs) else: return self.ProcessPageKiteData(*args, **kwargs) 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 PrepareWebsockets(self): self.websocket_key = os.urandom(16) def ProcessWebsocketData(self, data): loops = 150 happy = more = True while happy and more and (loops > 0): loops -= 1 if self.peeking: self.header = '' self.chunk = '' self.header += (data or '') try: ws_frame, data = ABNF.parse(self.header) more = data and (len(data) > 0) except ValueError as err: self.LogError('ChunkParser::ProcessData: %s' % err) self.Log([('bad_data', data)]) return False if ws_frame and ws_frame.length == len(ws_frame.data): # We have a complete frame, process it! self.header = '' happy = self.ProcessChunk(ws_frame.data) if ws_frame.data else True else: if self.read_eof: return self.ProcessEofRead() # Frame is incomplete, but there were no errors: we're done for now. return True if happy and more: self.LogError('Unprocessed data: %s' % data) raise BugFoundError('Too much data') elif self.read_eof: return self.ProcessEofRead() and happy else: return happy return False # Not reached def ProcessPageKiteData(self, data): loops = 150 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 as 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: if not self.zr: self.zr = zlib.decompressobj() cchunk = s(self.zr.decompress(b(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 PyPagekite-1.5.2.201011/pagekite/proto/ws_abnf.py000066400000000000000000000250031374056564300212460ustar00rootroot00000000000000""" websocket - WebSocket client library for Python Copyright (C) 2010 Hiroki Ohtani(liris) This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335 USA """ import array import os import struct import six from threading import Lock try: if six.PY3: import numpy else: numpy = None except ImportError: numpy = None try: # If wsaccel is available we use compiled routines to mask data. if not numpy: from wsaccel.xormask import XorMaskerSimple def _mask(_m, _d): return XorMaskerSimple(_m).process(_d) except ImportError: # wsaccel is not available, we rely on python implementations. def _mask(_m, _d): for i in range(len(_d)): _d[i] ^= _m[i % 4] if six.PY3: return _d.tobytes() else: return _d.tostring() __all__ = [ 'ABNF', 'continuous_frame', 'frame_buffer', 'STATUS_NORMAL', 'STATUS_GOING_AWAY', 'STATUS_PROTOCOL_ERROR', 'STATUS_UNSUPPORTED_DATA_TYPE', 'STATUS_STATUS_NOT_AVAILABLE', 'STATUS_ABNORMAL_CLOSED', 'STATUS_INVALID_PAYLOAD', 'STATUS_POLICY_VIOLATION', 'STATUS_MESSAGE_TOO_BIG', 'STATUS_INVALID_EXTENSION', 'STATUS_UNEXPECTED_CONDITION', 'STATUS_BAD_GATEWAY', 'STATUS_TLS_HANDSHAKE_ERROR', 'WebSocketException', 'WebSocketProtocolException' ] # closing frame status codes. STATUS_NORMAL = 1000 STATUS_GOING_AWAY = 1001 STATUS_PROTOCOL_ERROR = 1002 STATUS_UNSUPPORTED_DATA_TYPE = 1003 STATUS_STATUS_NOT_AVAILABLE = 1005 STATUS_ABNORMAL_CLOSED = 1006 STATUS_INVALID_PAYLOAD = 1007 STATUS_POLICY_VIOLATION = 1008 STATUS_MESSAGE_TOO_BIG = 1009 STATUS_INVALID_EXTENSION = 1010 STATUS_UNEXPECTED_CONDITION = 1011 STATUS_BAD_GATEWAY = 1014 STATUS_TLS_HANDSHAKE_ERROR = 1015 # A mask that does nothing ZERO_MASK = "\0\0\0\0" VALID_CLOSE_STATUS = ( STATUS_NORMAL, STATUS_GOING_AWAY, STATUS_PROTOCOL_ERROR, STATUS_UNSUPPORTED_DATA_TYPE, STATUS_INVALID_PAYLOAD, STATUS_POLICY_VIOLATION, STATUS_MESSAGE_TOO_BIG, STATUS_INVALID_EXTENSION, STATUS_UNEXPECTED_CONDITION, STATUS_BAD_GATEWAY, ) class WebSocketNeedMoreDataException(Exception): pass class WebSocketException(IOError): pass class WebSocketProtocolException(WebSocketException): pass class ABNF(object): """ ABNF frame class. see http://tools.ietf.org/html/rfc5234 and http://tools.ietf.org/html/rfc6455#section-5.2 """ # operation code values. OPCODE_CONT = 0x0 OPCODE_TEXT = 0x1 OPCODE_BINARY = 0x2 OPCODE_CLOSE = 0x8 OPCODE_PING = 0x9 OPCODE_PONG = 0xa # available operation code value tuple OPCODES = (OPCODE_CONT, OPCODE_TEXT, OPCODE_BINARY, OPCODE_CLOSE, OPCODE_PING, OPCODE_PONG) # opcode human readable string OPCODE_MAP = { OPCODE_CONT: "cont", OPCODE_TEXT: "text", OPCODE_BINARY: "binary", OPCODE_CLOSE: "close", OPCODE_PING: "ping", OPCODE_PONG: "pong" } # data length threshold. LENGTH_7 = 0x7e LENGTH_16 = 1 << 16 LENGTH_63 = 1 << 63 def __init__(self, fin=0, rsv1=0, rsv2=0, rsv3=0, opcode=OPCODE_TEXT, mask=1, data="", zero_mask=False): """ Constructor for ABNF. please check RFC for arguments. """ self.fin = fin self.rsv1 = rsv1 self.rsv2 = rsv2 self.rsv3 = rsv3 self.opcode = opcode self.mask = mask self.data = data or "" self.length = len(self.data) if zero_mask: self.get_mask_key = lambda c: ZERO_MASK else: self.get_mask_key = os.urandom def validate(self): """ validate the ABNF frame. """ if self.rsv1 or self.rsv2 or self.rsv3: raise WebSocketProtocolException("rsv is not implemented, yet") if self.opcode not in ABNF.OPCODES: raise WebSocketProtocolException("Invalid opcode %r", self.opcode) if self.opcode == ABNF.OPCODE_PING and not self.fin: raise WebSocketProtocolException("Invalid ping frame.") if self.opcode == ABNF.OPCODE_CLOSE: l = len(self.data) if not l: return if l == 1 or l >= 126: raise WebSocketProtocolException("Invalid close frame.") code = 256 * \ six.byte2int(self.data[0:1]) + six.byte2int(self.data[1:2]) if not self._is_valid_close_status(code): raise WebSocketProtocolException("Invalid close opcode.") @staticmethod def _is_valid_close_status(code): return code in VALID_CLOSE_STATUS or (3000 <= code < 5000) def __str__(self): return "fin=" + str(self.fin) \ + " opcode=" + str(self.opcode) \ + " data=" + str(self.data) @staticmethod def create_frame(data, opcode, fin=1, zero_mask=False): """ create frame to send text, binary and other data. data: data to send. This is string value(byte array). if opcode is OPCODE_TEXT and this value is unicode, data value is converted into unicode string, automatically. opcode: operation code. please see OPCODE_XXX. fin: fin flag. if set to 0, create continue fragmentation. """ if opcode == ABNF.OPCODE_TEXT and isinstance(data, six.text_type): data = data.encode("utf-8") # mask must be set if send data from client return ABNF(fin, 0, 0, 0, opcode, 1, data, zero_mask) def format(self): """ format this object to string(byte array) to send data to server. """ if any(x not in (0, 1) for x in [self.fin, self.rsv1, self.rsv2, self.rsv3]): raise ValueError("not 0 or 1") if self.opcode not in ABNF.OPCODES: raise ValueError("Invalid OPCODE") length = len(self.data) if length >= ABNF.LENGTH_63: raise ValueError("data is too long") frame_header = chr(self.fin << 7 | self.rsv1 << 6 | self.rsv2 << 5 | self.rsv3 << 4 | self.opcode) if length < ABNF.LENGTH_7: frame_header += chr(self.mask << 7 | length) frame_header = six.b(frame_header) elif length < ABNF.LENGTH_16: frame_header += chr(self.mask << 7 | 0x7e) frame_header = six.b(frame_header) frame_header += struct.pack("!H", length) else: frame_header += chr(self.mask << 7 | 0x7f) frame_header = six.b(frame_header) frame_header += struct.pack("!Q", length) if not self.mask: return frame_header + self.data else: mask_key = self.get_mask_key(4) return frame_header + mask_key + ABNF.mask(mask_key, self.data) @staticmethod def mask(mask_key, data): """ mask or unmask data. Just do xor for each byte mask_key: 4 byte string(byte). data: data to mask/unmask. """ if data is None: data = "" if mask_key == ZERO_MASK: return data if isinstance(mask_key, six.text_type): mask_key = six.b(mask_key) if isinstance(data, six.text_type): data = six.b(data) if len(data) < 1: return data if numpy: origlen = len(data) _mask_key = mask_key[3] << 24 | mask_key[2] << 16 | mask_key[1] << 8 | mask_key[0] # We need data to be a multiple of four... data += bytes(" " * (4 - (len(data) % 4)), "us-ascii") a = numpy.frombuffer(data, dtype="uint32") masked = numpy.bitwise_xor(a, [_mask_key]).astype("uint32") if len(data) > origlen: return masked.tobytes()[:origlen] return masked.tobytes() else: _m = array.array("B", mask_key) _d = array.array("B", data) return _mask(_m, _d) def parse_header(self, data): try: b1 = data[0] b2 = data[1] except IndexError: raise WebSocketNeedMoreDataException() if six.PY2: b1 = ord(b1) b2 = ord(b2) self.fin = b1 >> 7 & 1 self.rsv1 = b1 >> 6 & 1 self.rsv2 = b1 >> 5 & 1 self.rsv3 = b1 >> 4 & 1 self.opcode = b1 & 0xf has_mask = b2 >> 7 & 1 length_bits = b2 & 0x7f return has_mask, length_bits, data[2:] def parse_length(self, length_bits, data): if length_bits == 0x7e: if len(data) < 2: raise WebSocketNeedMoreDataException() self.length = struct.unpack("!H", data[:2])[0] return data[2:] elif length_bits == 0x7f: if len(data) < 8: raise WebSocketNeedMoreDataException() self.length = struct.unpack("!Q", data[:8])[0] return data[8:] else: self.length = length_bits return data def parse_mask(self, has_mask, data): if has_mask: if len(data) < 4: raise WebSocketNeedMoreDataException() self.mask = data[:4] return data[4:] else: self.mask = "" return data def parse_data(self, data): payload = data[:self.length] if len(payload) == self.length: self.data = ABNF.mask(self.mask, payload) self.validate() return data[self.length:] @classmethod def parse(cls, all_data): self = cls() try: has_mask, length_bits, data = self.parse_header(all_data) data = self.parse_length(length_bits, data) data = self.parse_mask(has_mask, data) data = self.parse_data(data) return self, data except WebSocketNeedMoreDataException: return None, "" PyPagekite-1.5.2.201011/pagekite/ui/000077500000000000000000000000001374056564300165275ustar00rootroot00000000000000PyPagekite-1.5.2.201011/pagekite/ui/__init__.py000066400000000000000000000000001374056564300206260ustar00rootroot00000000000000PyPagekite-1.5.2.201011/pagekite/ui/basic.py000077500000000000000000000225111374056564300201660ustar00rootroot00000000000000""" This is the "basic" text-mode user interface class. """ ############################################################################# from __future__ import absolute_import LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2020, 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 six 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,16})$') 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 = list(six.iterkeys(self.notify_history)) 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.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.write(msg) if tag == 'exiting': self.write('\n') def Welcome(self, pre=None): if self.in_wizard: self.write('%s%s%s' % (self.CLEAR, self.WHITE, self.in_wizard)) if self.welcome: self.write('%s\r%s\n' % (self.NORM, Q(self.welcome))) self.welcome = None if self.in_wizard and self.wizard_tell: self.write('\n%s\r' % self.NORM) for line in self.wizard_tell: self.write('*** %s\n' % Q(line)) self.wizard_tell = None if pre: self.write('\n%s\r' % self.NORM) for line in pre: self.write(' %s\n' % Q(line)) self.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.write('\n<<< press ENTER to continue >>>\n') self.rfile.readline() def Spacer(self): self.write('\n') self.wfile.flush() 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.write(' => %s ' % (Q(question), )) self.wfile.flush() 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.write(' %s\n' % (Q(question), )) self.wfile.flush() if not email: email = self.AskEmail('Your e-mail:', default=def_email, back=back, welcome=False) if email == back: return back import getpass self.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.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.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.write(('\n (Note: the ending %s will be added for you.)' ) % domains[0]) else: self.write('\n Please use one of the following domains:\n') for domain in domains: self.write('\n *%s' % domain) self.write('\n') while self.Retry(): self.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.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.write((' %s %d) %s\n' ) % ((default==i+1) and '*' or ' ', i+1, choices[i])) self.write('\n') while self.Retry(): d = default and (', default=%d' % default) or '' self.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.write(' %s\n' % line) if error: self.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 PyPagekite-1.5.2.201011/pagekite/ui/nullui.py000077500000000000000000000237201374056564300204200ustar00rootroot00000000000000""" This is a basic "Null" user interface which does nothing at all. """ ############################################################################## from __future__ import absolute_import from __future__ import division LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2020, 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 if hasattr(self.wfile, 'buffer'): self.wfile = self.wfile.buffer self.Reset() self.Splash() def write(self, data): self.wfile.write(b(data)) self.wfile.flush() 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 relay %s ...' % server_info[obj.S_NAME], color=self.GREY) self.Notify( ' - Relay supports %d protocols on %d public ports.' % (len(server_info[obj.S_PROTOS]), len(server_info[obj.S_PORTS])), color=self.GREY) if 'raw' in server_info[obj.S_PROTOS]: self.Notify( ' - Raw TCP/IP (HTTP proxied) kites are available.', color=self.GREY) self.Notify( ' - To enable more logging, add option: --logfile=/path/to/logfile', color=self.GREY) def NotifyQuota(self, quota, q_days, q_conns): q, qMB = [], float(quota) / 1024 # Float division if qMB < 1024: q.append('%.2f MB' % qMB) if q_days is not None and q_days < 400: q.append('%d days' % q_days) if q_conns is not None and q_conns < 10: q.append('%s tunnels' % q_conns) if not q: q = ['plenty of time and bandwidth'] self.Notify('Quota: You have %s left.' % ', '.join(q), prefix=(int(quota) < qMB) and '!' or ' ', color=self.MAGENTA) def NotifyIPsPerSec(self, ips, secs): self.Notify( 'Abuse/DDOS protection: Relaying traffic for up to %d clients per %ds.' % (ips, secs), prefix=' ', 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) PyPagekite-1.5.2.201011/pagekite/ui/remote.py000077500000000000000000000346461374056564300204140ustar00rootroot00000000000000""" This is a user interface class which communicates over a pipe or socket. """ from __future__ import absolute_import from __future__ import print_function ############################################################################## LICENSE = """\ This file is part of pagekite.py. Copyright 2010-2020, 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.write('begin_be_list\n') def EndListingBackEnds(self): self.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') % (int(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.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.write(message) def Notify(self, message, prefix=' ', popup=False, color=None, now=None, alignright=''): message = '%s' % message self.write('notify: %s\n' % message) def NotifyMOTD(self, frontend, message): self.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.write('status_msg: %s\n' % message) if tag: self.write('status_tag: %s\n' % tag) def Welcome(self, pre=None): self.write('welcome: %s\n' % (pre or '').replace('\n', ' ')) def StartWizard(self, title): self.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.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.write('begin_ask_email\n') if pre: self.write(' preamble: %s\n' % '\n'.join(pre).replace('\n', ' ')) if default: self.write(' default: %s\n' % default) self.write(' question: %s\n' % (question or '').replace('\n', ' ')) self.write(' expect: email\n') self.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.write('begin_ask_login\n') if pre: self.write(' preamble: %s\n' % '\n'.join(pre).replace('\n', ' ')) if email: self.write(' default: %s\n' % email) self.write(' question: %s\n' % (question or '').replace('\n', ' ')) self.write(' expect: email\n') self.write(' expect: password\n') self.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.write('begin_ask_yesno\n') if yes: self.write(' yes: %s\n' % yes) if no: self.write(' no: %s\n' % no) if pre: self.write(' preamble: %s\n' % '\n'.join(pre).replace('\n', ' ')) if default: self.write(' default: %s\n' % default) self.write(' question: %s\n' % (question or '').replace('\n', ' ')) self.write(' expect: yesno\n') self.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.write('begin_ask_kitename\n') if pre: self.write(' preamble: %s\n' % '\n'.join(pre).replace('\n', ' ')) for domain in domains: self.write(' domain: %s\n' % domain) if default: self.write(' default: %s\n' % default) self.write(' question: %s\n' % (question or '').replace('\n', ' ')) self.write(' expect: kitename\n') self.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.write('begin_ask_backends\n') if pre: self.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.write(' kitename: %s\n' % kitename) self.write(' protos: %s\n' % ', '.join(protos)) self.write(' ports: %s\n' % ', '.join(ports)) self.write(' rawports: %s\n' % ', '.join(rawports)) if default: self.write(' default: %s\n' % default) self.write(' question: %s\n' % (question or '').replace('\n', ' ')) self.write(' expect: backends\n') self.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.write('begin_ask_multiplechoice\n') if pre: self.write(' preamble: %s\n' % '\n'.join(pre).replace('\n', ' ')) count = 0 for choice in choices: count += 1 self.write(' choice_%d: %s\n' % (count, choice)) if default: self.write(' default: %s\n' % default) self.write(' question: %s\n' % (question or '').replace('\n', ' ')) self.write(' expect: choice_index\n') self.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.write('tell_%s: %s\n' % (dialog, ' '.join(lines))) def Working(self, message): self.write('working: %s\n' % message) class PageKiteThread(threading.Thread): daemon = True 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): with self.pk_readlock: 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 def write(self, data): if self.debug: print('>>GUI>> %s' % data.strip()) with self.gui_readlock: if data: self.gui_incoming += data else: self.gui_eof = True self.gui_readlock.notify() # And these are used by the GUI, to communicate with PageKite. def recv(self, bytecount): with self.gui_readlock: 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 def send(self, data): if not data.endswith('\n') and data != '': raise ValueError('Please always send whole lines') if self.debug: print('< """ ############################################################################## from six.moves import range from six.moves import BaseHTTPServer from six.moves.urllib.request import urlopen from six.moves.urllib.parse import parse_qs, urlparse import getopt import os import random import re import select import socket import struct import sys import threading import time import traceback 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.""" daemon = True 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 = {} self.views = {} # Important: threading.Lock() will deadlock pypy and generally we want # to avoid locking. The methods below only hold this lock # if they are adding/removing elements from our dicts and # lists. For mutating existing values we either just accept # things getting overwritten or rely on the GIL. self.lock = threading.RLock() def vmax(self, var, value): # Unlocked, since we don't change the size of self.values if value > self.values[var]: self.values[var] = value def vmin(self, var, value): # Unlocked, since we don't change the size of self.values if value < self.values[var]: self.values[var] = value def vscale(self, var, ratio, add=0): if var not in self.values: with self.lock: self.values[var] = self.values.get(var, 0) # Unlocked, since we don't change the size of self.values self.values[var] *= ratio self.values[var] += add def vset(self, var, value): with self.lock: self.values[var] = value def vadd(self, var, value, wrap=None): if var not in self.values: with self.lock: self.values[var] = self.values.get(var, 0) # We assume the GIL will guarantee these do sane things self.values[var] += value if wrap: self.values[var] %= wrap def vdel(self, var): if var in self.values: with self.lock: del self.values[var] def lcreate(self, listn, elems): with self.lock: self.lists[listn] = [elems, 0, ['' for x in range(0, elems)]] def ladd(self, listn, value): with self.lock: lst = self.lists[listn] lst[2][lst[1]] = value lst[1] += 1 lst[1] %= lst[0] 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])) if var == 'started': data.append( 'started_days_ago: %.3f\n' % ((time.time() - values[var]) / 86400)) 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]))) try: slist = sorted([float(i) for i in l if i]) if len(slist) >= 10: data.append('%s_m50: %.2f\n' % (lname, slist[int(len(slist) * 0.5)])) data.append('%s_m90: %.2f\n' % (lname, slist[int(len(slist) * 0.9)])) data.append('%s_avg: %.2f\n' % (lname, sum(slist) / len(slist))) except (ValueError, TypeError, IndexError, ZeroDivisionError): pass data.sort() return ''.join(data) def quit(self): if self.httpd: self.running = False 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() PyPagekite-1.5.2.201011/pk000077700000000000000000000000001374056564300205342pagekite/__main__.pyustar00rootroot00000000000000PyPagekite-1.5.2.201011/rpm/000077500000000000000000000000001374056564300151175ustar00rootroot00000000000000PyPagekite-1.5.2.201011/rpm/pagekite.init000077500000000000000000000035651374056564300176110ustar00rootroot00000000000000#!/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 PyPagekite-1.5.2.201011/rpm/pagekite.logrotate000077500000000000000000000003071374056564300206350ustar00rootroot00000000000000/var/log/pagekite/pagekite.log { missingok notifempty size 100k create 0644 root root postrotate /sbin/service pagekite condrestart > /dev/null 2>&1 || : endscript } PyPagekite-1.5.2.201011/rpm/pagekite.sysconfig000077500000000000000000000000611374056564300206360ustar00rootroot00000000000000# Should pagekite run in daemon mode? DAEMON=yes PyPagekite-1.5.2.201011/rpm/rpm-install.sh000077500000000000000000000022251374056564300177210ustar00rootroot00000000000000# 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 PyPagekite-1.5.2.201011/rpm/rpm-post.sh000077500000000000000000000004421374056564300172370ustar00rootroot00000000000000# 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 PyPagekite-1.5.2.201011/rpm/rpm-preun.sh000077500000000000000000000006451374056564300174100ustar00rootroot00000000000000 (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 PyPagekite-1.5.2.201011/rpm/rpm-setup.sh000077500000000000000000000002771374056564300174200ustar00rootroot00000000000000#!/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 PyPagekite-1.5.2.201011/scripts/000077500000000000000000000000001374056564300160105ustar00rootroot00000000000000PyPagekite-1.5.2.201011/scripts/blackbox-test.sh000077500000000000000000000132101374056564300211060ustar00rootroot00000000000000#!/bin/bash # # Primitive black-box test for pagekite.py # export PATH=.:$PATH export http_proxy= PKB=$1 PKF=$2 [ "$PKF" = "-" ] && PKF="$PKB" shift shift LOG="/tmp/pk-test.log" PKARGS="$*" PKA="--clean --debugio --ca_certs=$0" PORT=12000 let PORT="$PORT+($$%10000)" [ "$PKF" = "" ] && { echo "Usage: $0 /path/to/pagekite.py [global pagekite options]" exit 1 } echo -n "Testing versions: $($PKB --clean --appver)/$($PKF --clean --appver) ($PKARGS)" HAVE_TLS=" (SSL Enabled)" $PKB --clean $PKARGS "--tls_endpoint=a:$0" --settings >/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----- PyPagekite-1.5.2.201011/scripts/demo_auth_app.py000077500000000000000000000067061374056564300212030ustar00rootroot00000000000000#!/usr/bin/python2 -u from __future__ import absolute_import # # This is a trivial demo auth server, which just approves any requests # it sees, while printing debug information to STDERR. # # This code is in the public domain, feel free to adapt to your needs. # import getopt import json import os import sys # By default we advertise only the AUTH method; ZK-AUTH is not listed. # Roughly 50% of the time we'll offer SERVER mode as well. CAPABILITIES = (('SERVER ' if (os.getpid() % 2 == 0) else '') + 'AUTH') def Auth(domain): # This method simply returns a dictionary of quota values, along with # the shared secret in the clear. Pagekite.py takes care of challenging # and authenticating the user. # # Note that the quota values are mostly advisory; it is assumed that # accounting happens elsewhere and Pagekite.py is mostly just relaying # values back to the user as information. # # Exception #1: a quota_kb value of zero means the user exists but no # longer has access to the service (ran out of quota). Negative or # missing quota_kb values mean "no bandwidth quota, just grant access." # # Exception #2: The IPs-per-second rate limits are enforced by # pagekite.py, using the semantics of the --ratelimit_ips option. # # Valid rejection reasons that will be understood by the connector # include: "unauthorized", "quota", "nodays", "noquota", "noconns" # # Note that rejection reasons are ignored unless quota_kb is 0. # return { 'secret': 'testing', # Important! This is used for authentication. 'quota_kb': 10240, # If set, 0 means "out of quota"; else advisory 'quota_days': 24, # Advisory 'quota_conns': 5, # Advisory 'reason': 'reason', # If set, explains why user is "out of quota" 'ips_per_sec-ips': 1, # Inform PageKite what IP rate limits apply 'ips_per_sec-secs': 900} # ... to this tunnel's incoming traffic. def ZkAuth(domain): # This usually does nothing, adjust CAPABILITIES above if you want to # take this code path. # # In reality this method would both decode the incoming "auth domain" # string and verify the signed challenge against a shared secret, and # then reformat the returned quota values as a dynamic DNS response... # return {'hostname': domain, 'alias': '', 'ips': ['0.0.255.255']} def P(string): # Delete the sys.stderr line if you're not debugging. sys.stderr.write('>> ' + string + '\n') print(string) def ProcessArgs(args, server=False): o, a = getopt.getopt(args, 'a:z:', ([] if server else ['capabilities', 'server']) + ['auth=', 'zk-auth=']) for opt, arg in o: sys.stderr.write('<< %s=%s\n' % (opt, arg)) if opt == '--capabilities': P(CAPABILITIES) return if opt == '--server': ServerLoop() return if opt in ('-z', '--zk-auth'): P(json.dumps(ZkAuth(arg), indent=None)) return if opt in ('-a', '--auth'): P(json.dumps(Auth(arg), indent=None)) return def ServerLoop(): while True: line = sys.stdin.readline() if not line: return args = line.strip().split() if args and not args[0][:2] == '--': args[0] = '--' + args[0] ProcessArgs(args, server=True) if __name__ == '__main__': ProcessArgs(sys.argv[1:]) PyPagekite-1.5.2.201011/scripts/installer.sh000077500000000000000000000056511374056564300203530ustar00rootroot00000000000000#!/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 <
    # 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)
    
    PyPagekite-1.5.2.201011/scripts/legacy-testing/000077500000000000000000000000001374056564300207275ustar00rootroot00000000000000PyPagekite-1.5.2.201011/scripts/legacy-testing/pagekite-0.3.21.py000077500000000000000000004123451374056564300236250ustar00rootroot00000000000000#!/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 PyPagekite-1.5.2.201011/scripts/legacy-testing/pagekite-0.4.6a.py000077500000000000000000011521031374056564300237040ustar00rootroot00000000000000#!/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# PyPagekite-1.5.2.201011/scripts/legacy-testing/pagekite-0.5.6d.py000077500000000000000000004774351374056564300237310ustar00rootroot00000000000000#!/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("""\ eNq1V1lz28gRfsev6FgPIr00dNi1lVLiraIkSGaFIlk8oiibLdQQbBJjARjszIA0/32+wcHDlq2kkuU DMehr+u7GmzdvvBuV5sLKuUyk3VIsomdDVtFG6WcSWhXZghZyuWTNWcSG5mw3zBmNtjZWGa1ZG6ky43 tvIOvk//rz+r2bYDAJ6CNB+L+8aSwNLWXChGcutCW1xHPFz9Kyn2992JJvtVzFli7PL87f4e99h2zMd M0iM1YksG2k1WeOLHG89EnAuuvPQmeSxkUmNAUS/8aozKuuy7VaaZG6G5eamYxa2o3QfEVbVVAkMtK8 kMZqOS8sFLNO5JnSlCp4besA8CBrz2lhWafGKe1e6H4wI+o6zyq654y1SGhUzBMZUV9GnBkmAQUcxMS 8oPm25LuDGt6kVoPuXIAQPpV1iCXwugkJvW9uqqV1CGq1hHWaa1K5Y2pD3a2XCLvn87+1fG/ggmRWyo xVDntiSIOFG5kkSAwqDC+LpEMEUqLH3vTTcDb1uoMneuyOx93B9OkvoEXeAM1rriTJNE8kBMMcLTKkI LR+CMY3n0Dfve71e9Mnp/hdbzoIJhPvbjimLo2642nvZtbvjmk0G4+Gk8AnmjCXEp1jf+zXZRkgzd6C rZAJstd7QjgNNEsWqIE1I6wRyzX0EhQhqxpfvirbE4nKVqWZYNj7Efr1lpQp2yHDSJ+/xtbmV2dnm83 GX2WFr/TqLKlEmLNf/ohqgqMVaiZSaYr0XmqV1meqMW89zzuhydZYTilRq5WEIUDPMvnFs3p75VFDar YGBB5/iTi31CuBgdZKO5ooEcbAv9HzpCRzMKIFL5F0nAHQeiv0yrSvSAuJaNyobClXJXvrdKCo4nI3l w5MRRTLjE/bOzHV7f+LlP7wPrztBg/DAZrL+R4UXM/ujyDBeIyEO4SMerf1e6UGXvamttqlD6/RRZHP C+M8fNBdXd4pZFjVPJF2jTshgK2nxaY6QWZ18GuMLFOHTiefZtNwfHvqynAhdatCt52Ha4aaolHxAPj o7Lj4hrIEX3pfBRg9ha1MGRBrQqtCBwBhA/d3B5dI7oD2muZeFaCKRRrVsubjQGXcrnJAsy109poQML V9MMNbqbBw6ctpdqQXGtQOBNYa0hhlwMeL1q8XHbrs0PvfGpFXtb41PqnVtGkOAcmvV781r76jaLUPj QB0L79IfyAcyEay2aWSS4W1C2NSYRzup4+0PrzC7C4oq7XQCaae4SZG5Uv4u+nsMC94CmNhJyBaya95 vyd7J/E4LWJh4kTOG8ticRHzlxZiIGoD4wQW1lS+w9dOixO/yF2sKuJDK4GCkIVcIfqttp+oDevvBr0 pmFh8X4fGe7HwM95UmBev8Kr+5z9073s34WxWVnYj8Btc+zgaVouI5yj0RqcqXUPo/WPdd4x70ATjNV v1hrVNe0mtJm+kS+mGzG8OtXN3Ev0c8IrR7UkfwXbkarz7K7ZrkRTs7D+h4b4bUeKM0Qbj2HpOjerYi uIO7bzr1i5T9QHhN3h3BzpUK+GsVRK06Re6bIoJAxYMJdzPVd56d3GkE/j9z0o2nJ2SwVFwYvgooCVB OaIm/bNpf+JKHBm12l5hyDJmMuXbISYM0B23Osly2GFnLYfxvJCJfYeCw6Cw/MVCjpq7TdD4bjKXu8z SLQMdzD71XBZoveRe+j//RBnaOPYCYxJUdF6GDasepESaXQ8SX/X6jRZ5zroUPldQwAnv4JjOK4XE4c iCnE1cb0RYozRiKrlcxDkTc6y82NMwxzCQYd07zW5lW/jeyeEEMVEsZOaVx3qC1CC5rN78T92/B+Hoa TjCXj3pO+/i0dD6ODtIMLgNu/3H7tMkvJ7d3QXjCSjuBMLRYB+6/wivn6aBQ1z8TG/p4vzyA5DT2WAQ 9MPJ8OZvwTS87uO55/U4Odbjv9RgqosXFfjw6v2Ok07oEZ809ReNC269gM2LFfKyDHS5hEljCv7z5Yd zp3CVgX+UR5pVCVa3qlysawaA9eX78CHABn27GxlI+fXF18BKQrX2BGXbcWs9KgLgAwKsJzciSV6le8 QKPmax+I8IHzU+vF6l/CeW5XFZwa+S1qV57IyqJ4ahzKQNw5bhZNmhlBGwxY6CXlwC6w+iXVfADuXe9 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("""\ 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 """)) 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# PyPagekite-1.5.2.201011/scripts/legacy-testing/pagekite-0.5.8a.py000077500000000000000000005045421374056564300237160ustar00rootroot00000000000000#!/usr/bin/python # vim: set fileencoding=utf-8 : # 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.8a) # 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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 5rlwajf8GxnApuQ== """)) m = sys.modules["sockschain"] = imp.new_module("sockschain") m.__file__ = "sockschain/__init__.py" m.open = __comb_open exec __FILES[".SELF/sockschain/__init__.py"] in m.__dict__ ############################################################################### __FILES[".SELF/pagekite/__init__.py"] = zlib.decompress(__b64d("""\ eNqtk0+P2jAQxe/+FE/spZVo2LbqZftHCgh2I1FASdAKqReTTBLvGjuyHVC+/U5gVxwqtZfNwZbtmef fG09ubt7zE8tkNl9lc/zEaDT6I/JGeVRKE3hupQuwFc81PatAUdtHYmbb3qm6Cfhy+/n2Ew/fxggNYU rS+CD1s8fG2ScqAqipIkhTYvoknVFIOyMd5opH760Rl+taZ2snD8ONlSOCt1U4SUd36G2HQho4KpUPT u27wGBhkJxYh4MtVdUPG50pyYmBIpA7+AF6WOB+tQXiqiJncU+GnNTYdHutCixVQcYTJAMMO76hEvv+ nLdgDJG9YmBhWV4GZc0YpPjc4UjO8xpf3256VRuDsT7IMJA72HZI+si4vdAyXPOiv51fDZZQ5qzZ2Jb 9NKzGDk9Ka+wJnaeq02OAQ4HHJH9Yb3MRr3Z4jNM0XuW77xwbGsvHdKSLkjq0WrEw23HShH6g/j1PZw 8cH0+TZZLvBvBFkq/mWSYW6xQxNnGaJ7PtMk6x2aabdTaPgIzorDgU9t91rc4P5EiUFKTSnj3v+Dk9k +kSjTwSP2tB6shcEgV31Vst/6stpLamPtvkhGsdmS+pYGwYwxO3z48mhPZuMjmdTlFtusi6eqIvEn7y S3DDi5v3/ZteAL58GPI= """)) m = sys.modules["pagekite"] = imp.new_module("pagekite") m.__file__ = "pagekite/__init__.py" m.open = __comb_open exec __FILES[".SELF/pagekite/__init__.py"] in m.__dict__ ############################################################################### __FILES[".SELF/pagekite/common.py"] = zlib.decompress(__b64d("""\ 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 """)) m = sys.modules["pagekite.common"] = imp.new_module("pagekite.common") m.__file__ = "pagekite/common.py" m.open = __comb_open sys.modules["pagekite"].__setattr__("common", m) exec __FILES[".SELF/pagekite/common.py"] in m.__dict__ ############################################################################### __FILES[".SELF/pagekite/compat.py"] = zlib.decompress(__b64d("""\ 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 9f06Y7929+pq3R8xRUJ6nDpsuYPWB69fwN13cV6 """)) m = sys.modules["pagekite.compat"] = imp.new_module("pagekite.compat") m.__file__ = "pagekite/compat.py" m.open = __comb_open sys.modules["pagekite"].__setattr__("compat", m) exec __FILES[".SELF/pagekite/compat.py"] in m.__dict__ ############################################################################### __FILES[".SELF/pagekite/logging.py"] = zlib.decompress(__b64d("""\ eNq1VlFv2zgMftevIDoMsXc+N9lu99BdB6Rd0gbIkiJJbyiyoFBj2dGqWIakJM2/HynFTdcB67W4+cE WKfLjJ4qUfHBwwPq6KGRZpOwAhVf/68P6vdPOYNyBY0Dwr2yykBZyqQTgt+LGgc7xW4hb6URabVN2qq utkcXCwdtmq/knvt4n4BYCTgQvrePq1sKF0d/E3IFY5CnwMoOTb9yUEkarkhvoSHxbq0sWwlVGF4YvK WJuhACrc7fhRhzBVq9gzkswIpPWGXmzckjMEeShNrDUmcy3pFiVmTCMWDhhlpZIkwBng0uAdp4Lo+FM lMJwBRerGyXn0JdzUVoBHAmQxi5EBjdb79dFGmy8owFdjfDcSV0mICTOG1gLY1GGd3WkHVoCSCvijpg b0BU5xUh3yxR3e7/055XvF5iBLD3mQle4ngWi4Qo3Uim4EbCyIl+pBABNAb70JufDywlrD67gS3s0ag 8mVx/Q1i00Tou1CEhyWSmJwLgcw0u3JdafO6PTc7Rvn/T6vckVEe/2JoPOeMy6wxG04aI9mvROL/vtE Vxcji6G404KMBbCI1Jif53X3G+QESwTjktlcc1XuJ0WmakMFnwtcFvnQq6RF4c5VlWdyyexGVe6LPwy 0WGfR+TXy6HULgErsHz+WThXHR0ebjabtChXqTbFoQoQ9vDj7+gmTLTGnnFyKeqx3VpWj+d6WXFkh98 lln9u9HKng53Fm3slGuyVDFGULrBLg3kaZIYrurYuo80+pkhpEBj71Dm5PLvuDVHd5QpTxvrDMxSmMx pc93sDavlmEDqDs8k5iu+aQTE5H3XG58P+J9S9ff83vIFW8+1fDHcyBzyL/uVqJWy09h/sfWEdLfh4o EsRHzGAQukb3DSESqCOthu1x5PrSe9zB61KvUF8WbqoRqAapG9KryiO0WijTWaJd9RwtpFA4/VdA16T b5zg9P2D0ziL05amnb12+lpaHZHhY0ul9kA1vXhWB0vFnRNlFk2j2/W0OUvQIYCi2JrFqRGV4nMRNb5 SxAY04ofov3geeJoXe5Yv9fwA5Jk8w5XOowp3wXfy7ZpOpbDlM78xmZxT0dEn8okj7X1t/XEMrSCnvK oon97Bey7oelGijHA2ho+wr8Ejzw3lafMo2qsPW814FooXDYxwK1NCCJrADrcuzrFvjPvi9LO+MpOwv fsqlfluPSHqQziM9bjO4+DREMY0KBXeLniGXty1ZLSTiH1nNMKEY+bTb1qW0bRxvBvdhaTeeSCKO/PF LhRFyMTNqvjPMXyjPzOKFU+A9gbd4TMw69xPdBe39vfk3pltsK0PF11QsFAwYZxuDP6kRM9IxU+u2F8 +RXdzUTmI8M4zRpsEekM/iAOFV9Bea5nB3HC7wB8zWg5dXHiH39LPk7Kwqugws3opnLdQHI83JW/Dhe 5RKm7tg9R9Fnhbbl+QvJ+z5UG7amUXOLfDfXQwo0AJUZQQFO+Rol1EH13VoafRJgE13cxCGjfkpWb7j ffJiZa2SOifcfmAq0BlOL6pcRJA0R+1Mg+WR96iPnO9yp8jyIQmYhZsw32YFlccP4HsD6p0zbPMx9CG rokWMje8Om41/bNn+oma6wmmoQFfwnUXpFfm+okYEk1eHGIkrHCk+2FTQyUnNKDf0PrHgN0XOUZ+rA7 NVhcfY3tkxr4Dt2qz6g== """)) m = sys.modules["pagekite.logging"] = imp.new_module("pagekite.logging") m.__file__ = "pagekite/logging.py" m.open = __comb_open sys.modules["pagekite"].__setattr__("logging", m) exec __FILES[".SELF/pagekite/logging.py"] in m.__dict__ ############################################################################### __FILES[".SELF/pagekite/manual.py"] = zlib.decompress(__b64d("""\ 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 """)) m = sys.modules["pagekite.manual"] = imp.new_module("pagekite.manual") m.__file__ = "pagekite/manual.py" m.open = __comb_open sys.modules["pagekite"].__setattr__("manual", m) exec __FILES[".SELF/pagekite/manual.py"] in m.__dict__ ############################################################################### __FILES[".SELF/pagekite/proto/__init__.py"] = zlib.decompress(__b64d("""\ eNq1k8tu2zAQRff8igt30wKunLboJn0AimEnQl3bkGUEBrqhpZHEhCYFkrKhv8/QTpBFgXbTckGCj7k 8dzgcjUaiaMkTpCOElrCWDf1QgdA5G2xpNVppKq1Mg1JL78knYsRRb/5pE4tsOltuZvgGFv/FTMqjVp rAYyddgK15bOiR0ZJuSMTUdoNTTRvw8erD1XvuPo/PBm5IGh+kfvRYO/tAZQC1dQJ2gZsH6YxC3hvpM FPce2+NuFzHhhsnD/HG2hHB2zqcOC3XGGyPUho4qpQPTu17zo8KUXJiHQ62UvUQF3pTkRORIpA7+Agd J7hdboG0rslZ3JIhJzXW/V6rEgtVkon5Z4C44luqsB/OcXPGEJtnDMwty8ugrBmDFO87HMl5nuPTy03 PamMw1lsZIrmD7WLQO8YdhJbhNS753fmrwQrKnDVb28XSYDV2eFJaY0/oPdW9HgN8FLjPirvVthDpco f7NM/TZbH7wmdDa3mbjnRRUodOKxZmO06aMETqn7N8esfn05tskRW7CD7PiuVssxHzVY4U6zQvsul2k eZYb/P1ajNLgA1dqjUm9s95rc8P5EhUFKTSXL1ix8/pmUxXXNtH4mctSR2ZS6LkqnrJ5V+1hdSWv0W0 yQGveWS+rIaxYQxPXD5f2xC668nkdDoljekT65qJvkj4yff/8ZvEE63uK/U= """)) m = sys.modules["pagekite.proto"] = imp.new_module("pagekite.proto") m.__file__ = "pagekite/proto/__init__.py" m.open = __comb_open sys.modules["pagekite"].__setattr__("proto", m) exec __FILES[".SELF/pagekite/proto/__init__.py"] in m.__dict__ ############################################################################### __FILES[".SELF/pagekite/proto/proto.py"] = zlib.decompress(__b64d("""\ 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 O/4d+9DCXjsfYDcKUoOAau41Cr8D2aNpdg= """)) m = sys.modules["pagekite.proto.proto"] = imp.new_module("pagekite.proto.proto") m.__file__ = "pagekite/proto/proto.py" m.open = __comb_open sys.modules["pagekite.proto"].__setattr__("proto", m) exec __FILES[".SELF/pagekite/proto/proto.py"] in m.__dict__ ############################################################################### __FILES[".SELF/pagekite/proto/parsers.py"] = zlib.decompress(__b64d("""\ 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 """)) m = sys.modules["pagekite.proto.parsers"] = imp.new_module("pagekite.proto.parsers") m.__file__ = "pagekite/proto/parsers.py" m.open = __comb_open sys.modules["pagekite.proto"].__setattr__("parsers", m) exec __FILES[".SELF/pagekite/proto/parsers.py"] in m.__dict__ ############################################################################### __FILES[".SELF/pagekite/proto/selectables.py"] = zlib.decompress(__b64d("""\ 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 """)) m = sys.modules["pagekite.proto.selectables"] = imp.new_module("pagekite.proto.selectables") m.__file__ = "pagekite/proto/selectables.py" m.open = __comb_open sys.modules["pagekite.proto"].__setattr__("selectables", m) exec __FILES[".SELF/pagekite/proto/selectables.py"] in m.__dict__ ############################################################################### __FILES[".SELF/pagekite/proto/filters.py"] = zlib.decompress(__b64d("""\ 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 """)) m = sys.modules["pagekite.proto.filters"] = imp.new_module("pagekite.proto.filters") m.__file__ = "pagekite/proto/filters.py" m.open = __comb_open sys.modules["pagekite.proto"].__setattr__("filters", m) exec __FILES[".SELF/pagekite/proto/filters.py"] in m.__dict__ ############################################################################### __FILES[".SELF/pagekite/proto/conns.py"] = zlib.decompress(__b64d("""\ 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 """)) m = sys.modules["pagekite.proto.conns"] = imp.new_module("pagekite.proto.conns") m.__file__ = "pagekite/proto/conns.py" m.open = __comb_open sys.modules["pagekite.proto"].__setattr__("conns", m) exec __FILES[".SELF/pagekite/proto/conns.py"] in m.__dict__ ############################################################################### __FILES[".SELF/pagekite/ui/__init__.py"] = zlib.decompress(__b64d("""\ eNoDAAAAAAE= """)) m = sys.modules["pagekite.ui"] = imp.new_module("pagekite.ui") m.__file__ = "pagekite/ui/__init__.py" m.open = __comb_open sys.modules["pagekite"].__setattr__("ui", m) exec __FILES[".SELF/pagekite/ui/__init__.py"] in m.__dict__ ############################################################################### __FILES[".SELF/pagekite/ui/nullui.py"] = zlib.decompress(__b64d("""\ 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 """)) m = sys.modules["pagekite.ui.nullui"] = imp.new_module("pagekite.ui.nullui") m.__file__ = "pagekite/ui/nullui.py" m.open = __comb_open sys.modules["pagekite.ui"].__setattr__("nullui", m) exec __FILES[".SELF/pagekite/ui/nullui.py"] in m.__dict__ ############################################################################### __FILES[".SELF/pagekite/ui/basic.py"] = zlib.decompress(__b64d("""\ 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 """)) m = sys.modules["pagekite.ui.basic"] = imp.new_module("pagekite.ui.basic") m.__file__ = "pagekite/ui/basic.py" m.open = __comb_open sys.modules["pagekite.ui"].__setattr__("basic", m) exec __FILES[".SELF/pagekite/ui/basic.py"] in m.__dict__ ############################################################################### __FILES[".SELF/pagekite/ui/remote.py"] = zlib.decompress(__b64d("""\ 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 """)) m = sys.modules["pagekite.ui.remote"] = imp.new_module("pagekite.ui.remote") m.__file__ = "pagekite/ui/remote.py" m.open = __comb_open sys.modules["pagekite.ui"].__setattr__("remote", m) exec __FILES[".SELF/pagekite/ui/remote.py"] in m.__dict__ ############################################################################### __FILES[".SELF/pagekite/yamond.py"] = zlib.decompress(__b64d("""\ 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 """)) m = sys.modules["pagekite.yamond"] = imp.new_module("pagekite.yamond") m.__file__ = "pagekite/yamond.py" m.open = __comb_open sys.modules["pagekite"].__setattr__("yamond", m) exec __FILES[".SELF/pagekite/yamond.py"] in m.__dict__ ############################################################################### __FILES[".SELF/pagekite/httpd.py"] = zlib.decompress(__b64d("""\ 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 """)) m = sys.modules["pagekite.httpd"] = imp.new_module("pagekite.httpd") m.__file__ = "pagekite/httpd.py" m.open = __comb_open sys.modules["pagekite"].__setattr__("httpd", m) exec __FILES[".SELF/pagekite/httpd.py"] in m.__dict__ ############################################################################### __FILES[".SELF/pagekite/pk.py"] = zlib.decompress(__b64d("""\ eNrMvet620ayKPpfTwHHxweATVGSb5NwDZORJTrWF1nSFqU42Qo/bpAEJYwpggFAS5pZs5/91KXvaJC Uk5l1stZYBNBdfauurqquyzfffLN1cZOVAfz/3U1S4d9ZOq2CfBpUN2mQF9l1Nk9mwW0+z2dZdZONg0 VynX7OqrS9eGiryuUsv5s9BKM0m18HRTpNxlVepJMgm1d5UN4ms1laBOVytH2bT5aztGxvfQNNP/1T/ 9s6PjronfR7QTcA4L9x36bZLMUOLpKCRmX1/iBfPMAIb6rg5e7e7jb886ZF436XJvOySmafy+CsyP+e jqsgvZm2g2Q+Cd79PSnmWXC+nCdF0IPZKcoyn29xc4sivy6SW2xxWqRpUObT6i4p0k7wkC+DcTKHyZl kZVVko2UFHasQ5E5ewARPsukDvljOJ2mxhb2o0uK2lEsR/HhyGQT702la5MGP6TwtYFnOlqMZLMlxNk 7nZRok0AF8U97A1I8eqN576MZWX3QjeJ8D+KTK8nkrSGE9YVW+pEUJz8Er2ZKA1oLVDyLACeh5EeQLr BRDdx+2Zkml67XrI9cDRAQgmDf5AsZDGFYFd9lsBqgSLMt0upy1ggCKBsGno4sPp5cXW/snvwaf9s/P 908ufv0vKFvd5PA5/ZIypOx2McsAMAynSObVA/b6Y+/84AOU3393dHx08St2/P3RxUmv3996f3oe7Ad n++cXRweXx/vnwdnl+dlpv9cOgn6aEkSc2NXzOqUFKtKtSVol2Qywd+tXWM4SejabBDfJlxSWdZxmX6 BfSTAGrJJzuRb2VjLLYcvgMKGCnkfo39E0mOdVKyhTQJ+/3lTVorOzc3d3176eL9t5cb0zYxDlzvf/j t0EE53DnhklZfr2tXwaX2db0yK/xR+BeJeW42RByMe/hjfV7UxWSItinsuH67QCPJJPOKJZNpKPeSl/ wbpO8lv1lMpfZTqDnaie8vHnVD9VxdL49qCAVentAomAer4p0mQCZEq9yG71xyIZp6Nk/Fm+WBYzo4v 3t7NiMTZe/AN/y4c+daifFrAzeJIOfjz6cHFxxq/kdImX5+nvy7SsPsBYZ7J8H5E7/eXj8fnZgV2p/q VlvXOgyaby/HOWbokVy28XuP/403P1Eki7fimXmcrKp1l+fY0TtrV1enYxfH+8/2MfaGyYd047/c6Hz lnnl85x538fdab7nfPOTWfRSQ47l52TXiekCvvnVP4qhCMkzxdhKwjHMyCw+GMOewVIw7wsZ/yofoyL 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 HRiOsJ64rtLkBzHa6zSQArkPF/aP0+Aw== """)) m = sys.modules["pagekite.pk"] = imp.new_module("pagekite.pk") m.__file__ = "pagekite/pk.py" m.open = __comb_open sys.modules["pagekite"].__setattr__("pk", m) exec __FILES[".SELF/pagekite/pk.py"] in m.__dict__ ############################################################################### #!/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----- """ #EOF# PyPagekite-1.5.2.201011/scripts/mk-dropper.sh000077500000000000000000000015171374056564300204330ustar00rootroot00000000000000#!/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 PyPagekite-1.5.2.201011/scripts/mk-self-signed.sh000077500000000000000000000016601374056564300211570ustar00rootroot00000000000000#!/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 PyPagekite-1.5.2.201011/scripts/pagekite000077700000000000000000000000001374056564300236152../pagekite/__main__.pyustar00rootroot00000000000000PyPagekite-1.5.2.201011/scripts/pagekite_gtk000077500000000000000000001710211374056564300203760ustar00rootroot00000000000000#!/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----- """ PyPagekite-1.5.2.201011/scripts/pagekite_test.py.old000077500000000000000000000516111374056564300217760ustar00rootroot00000000000000#!/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() PyPagekite-1.5.2.201011/scripts/pkvnc000077500000000000000000000034331374056564300170620ustar00rootroot00000000000000#!/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 PyPagekite-1.5.2.201011/scripts/qemu-rpi.sh000077500000000000000000000006771374056564300201200ustar00rootroot00000000000000#!/bin/bash IMAGE_IMG=2019-04-08-raspbian-stretch.img IMAGE_ZIP=2019-04-08-raspbian-stretch.zip IMAGE_URL=http://downloads.raspberrypi.org/raspbian/images/raspbian-2019-04-09/$IMAGE_ZIP set -e mkdir -p ~/tmp/rpi_qemu_vm cd ~/tmp/rpi_qemu_vm [ -e $IMAGE_ZIP ] || wget $IMAGE_URL [ -e $IMAGE_IMG ] || unzip $IMAGE_ZIP # See: https://github.com/lukechilds/dockerpi docker run -it -v $(pwd)/$IMAGE_IMG:/sdcard/filesystem.img lukechilds/dockerpi:vm PyPagekite-1.5.2.201011/scripts/vipagekite000077500000000000000000000124351374056564300200730ustar00rootroot00000000000000#!/bin/bash # # This is a helper script for performing safe, versioned edits of the # PageKite configuration. # # Since PageKite is often used to facilitate remote access, editing the # config can be quite, dangerous as it may break the remote access itself. # # This script will test the syntax of the pagekite config file, warning # the user if it fails to parse and offering remedies. It also tries to # detect if the user has changed anything that might effect their SSH # configuration and warn about that too. # # TODO: # - Add a watcher that watches the pagekite log file and reverts the # config if it doesn't appear to make a working connection within # a certain time-frame. # - Add end-to-end probes, revert if they fail (note this can only # work in environments with permissive firewalls, so the above # TODO should have priority). # set -e # Default settings export VISUAL=${VISUAL:-vi} export PAGER=${PAGER:-less} export DIFFER=${DIFFER:-diff} export RESTART_AFTER=${RESTART_AFTER:-15} # Edit ~/.pagekite.rc by default [ "$1" = "" ] && exec "$0" ~/.pagekite.rc # We can only handle edits to one file at a time [ "$2" != "" ] && { echo "ERROR: Please only edit one file at a time" exit 1 } # Figure out what we're doing... FN="$1" DN="$(cd $(dirname "$FN") && pwd)" if [ "$DN" = "/etc/pagekite.d" ]; then GD="$DN" else GD="$DN/.pagekite.git" fi # Make sure we have the power [ "$DN" != "/etc/pagekite.d" -o "$(id -u)" = "0" ] || exec sudo "$0" "$@" # Make sure our prerequisites are installed export PK=$(which pagekite || which pagekite.py) export PK=${PAGEKITE:-pagekite} for T in sha1sum git $DIFFER $PAGER $VISUAL $PK; do which "$T" >/dev/null || { echo "ERROR: Please install $T"; exit 1 } done # Before we edit anything, parse the config so we can detect changes. export OLD_CONFIG=$(tempfile) export NEW_CONFIG=$(tempfile) cleanup() { rm -f "$OLD_CONFIG" "$NEW_CONFIG" } trap cleanup EXIT if [ "$DN" = "/etc/pagekite.d" ]; then $PK --clean --optdir=$DN --settings >"$OLD_CONFIG" else $PK --clean --optfile="$FN" --settings >"$OLD_CONFIG" fi # Check to see if we're tunneling SSH. If we are, we will want to take # special care when restarting. SSHKITE=$(grep -e raw-22: "$OLD_CONFIG" ||true) if [ "$SSHKITE" != "" ]; then SSHKITE=$(grep -e raw-22: -e ^kitename -e ^kitesecret "$OLD_CONFIG" \ |sha1sum) fi # This is the edit loop; keep editing and testing the file until it # passes our tests or the user asks to quit anyway. E=1 while [ 1 ]; do [ "$E" = 1 ] && $VISUAL $FN if [ "$DN" = "/etc/pagekite.d" ]; then $PK --clean --optdir=$DN --settings >$NEW_CONFIG 2>&1 && E=0 || E=1 if [ "$E" != 1 -a "$SSHKITE" != "" ]; then SSHKITE2=$(grep -e raw-22: -e ^kitename -e ^kitesecret "$NEW_CONFIG" \ |sha1sum) if [ "$SSHKITE" != "$SSHKITE2" ]; then echo echo "WARNING: Your SSH configuration may have CHANGED." echo E=2 fi fi else $PK --clean --optfile=$FN --settings >$NEW_CONFIG 2>&1 && E=0 || E=1 fi if [ "$E" = 0 ]; then break fi if [ "$E" = 1 ]; then echo echo "ERROR: Configuration failed to parse; a typo or syntax error?" echo fi echo " E) Edit again" if [ -d "$GD/.git" ]; then echo " U) Undo edits (revert file)" fi echo echo " D) Display differences between current and previous config" echo " P) Parse and display complete config (or errors)" echo echo " C) Keep changes, continue with restart/commit" echo " Q) Keep changes, quit vipagekite" echo echo -n "Your choice [E]: " read CHOICE case "$CHOICE" in c|C|continue) # Continue break ;; d|D|diff) # Show what changed clear $DIFFER -u -b "$OLD_CONFIG" "$NEW_CONFIG" |$PAGER echo E=0 ;; e|E|edit|"") # Loop again, edit again E=1 ;; p|P|print) # Display current parsed config clear cat "$NEW_CONFIG" |$PAGER E=0 ;; q|Q|quit) # Quit exit ;; u|U|undo) # Undo edits if [ "$DN" = "$GD" ]; then git checkout "$FN" else cp -va "$GD/$(basename "$FN")" "$FN" fi exit ;; *) # Default: Just re-test file and redisplay choices E=0 ;; esac done # Offer to restart the PageKite service if [ "$DN" = "/etc/pagekite.d" ]; then echo while [ 1 ]; do echo -n "Would you like to restart the pagekite service? [y/n] " read YN case "$YN" in y|Y|yes|Yes|YES) ( sleep $RESTART_AFTER systemctl restart pagekite \ || service pagekite restart \ || /etc/rc2.d/S*pagekite restart # TODO: Watch the log for signs of success or failure # TODO: Run e2e tests? ) /dev/null 2>&1 & echo "OK, scheduled a restart for $RESTART_AFTER seconds from now." break ;; n|N|no|No|NO) break ;; esac done echo fi # Make sure we have a place for version control mkdir "$GD" 2>/dev/null || true [ ! -d "$GD/.git" ] && { cd "$GD" git init } [ ! -d "$GD/.git" ] && { echo "ERROR: Could not initialize git repository in $GD" exit 2 } # Save our results so we can revert later if [ "$GD" != "$DN" ]; then cp -va "$FN" "$GD" fi cd "$GD" git add * "$(basename "$FN")" git commit -m 'vipagekite auto-commit' PyPagekite-1.5.2.201011/setup.py000077500000000000000000000024771374056564300160500ustar00rootroot00000000000000#!/usr/bin/python from __future__ import absolute_import from __future__ import division 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=os.getenv( 'PAGEKITE_VERSION', APPVER.replace('github', 'dev%d' % (120*int(time.time()/120)))), # Integer division 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', 'scripts/vipagekite'], install_requires=['six', 'SocksipyChain >= 2.1.2'] )