├── .gitignore ├── INSTALL ├── LICENSE ├── README.rst ├── VERSION ├── arkcclient ├── __init__.py ├── client.py ├── common.py ├── coordinator.py ├── main.py ├── meekclient.py ├── ptclient.py ├── pyotp │ ├── LICENSE │ ├── __init__.py │ ├── otp.py │ ├── totp.py │ └── utils.py └── server.py ├── goagent_local ├── .gitignore ├── GeoIP.dat ├── cacert.pem ├── dnsproxy.py ├── goagent-gtk.py ├── proxy.ini ├── proxy.py ├── proxy.sh └── proxylib.py ├── requirements.txt ├── setup.py └── test ├── client_config.json ├── client_config2.json ├── client_test.sh └── client_test2.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | #Test files 6 | server 7 | server.pub 8 | client 9 | client.pub 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | env/ 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *,cover 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | 59 | # Sphinx documentation 60 | docs/_build/ 61 | 62 | # PyBuilder 63 | target/ 64 | 65 | # Others 66 | .ropeproject/ 67 | -------------------------------------------------------------------------------- /INSTALL: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectarkc/arkc-client/957c78a1e80a17e1c121b09af717cbd8d551f2b4/INSTALL -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | 341 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ArkC-Client V0.4 2 | ================ 3 | 4 | ArkC is a lightweight proxy designed to be proof to IP blocking measures 5 | and offer high proxy speed via multi-connection transmission and 6 | swapping connections. 7 | 8 | ArkC-Client is the client-side utility. In a LAN environment, it either 9 | works with UPnP-enabled routers or requires NAT configuration if the 10 | client is behind a router. 11 | 12 | Note: ArkC 0.4 is not compatible with earlier versions. 13 | 14 | `(中文)快速入门教程 `__ 15 | 16 | What is ArkC? 17 | ------------- 18 | 19 | [To be updated with the new GAE trick!] 20 | 21 | ArkC enables VPS owners to share their VPS to people around them, or share online, the proxy hosted on their VPS, without worrying about IP blacklists. 22 | 23 | For a more detailed description, please visit our website and read our page `Understand ArkC `__. 中文版本的介绍在这一页面 `ArkC的原理 `__。 24 | 25 | This is what it tries to do by default: 26 | 27 | .. image:: https://arkc.org/wp-content/uploads/2016/03/ArkC.png 28 | :height: 300px 29 | 30 | And making it a little bit more complicated, e.g. set obfs_level to 3 or use a socks proxy: 31 | 32 | .. image:: https://arkc.org/wp-content/uploads/2016/03/ArkCProxy.png 33 | :height: 400px 34 | 35 | 36 | Setup and Requirement 37 | --------------------- 38 | 39 | For a probably more detailed guide: `Deployment and Installation `__. 对于安装与部署的中文说明在 `部署与安装ArkC `__ 40 | 这一页面。 41 | 42 | For Windows users, you are recommended to use our Windows GUI, installer along with latest ArkC client binary executable, in the Github `release page `__. Just pick your .Net Framework version and download. 43 | 44 | For users with python3 pip development environment (Note: We don't 45 | recommend using python 2): 46 | 47 | :: 48 | 49 | sudo pip3 install arkcclient 50 | 51 | To install python3 and pip3 with python.h: 52 | 53 | Debian/Ubuntu users 54 | 55 | :: 56 | 57 | sudo apt-get install python3 python3-pip python3-dev 58 | 59 | Fedora users 60 | 61 | :: 62 | 63 | yum install python3 python3-devel python3-pip 64 | 65 | You may also install ArkC via source. 66 | 67 | To get ArkC Client work, you must satisfy ONE OF the following 68 | conditions (unless you are the expert): 1) connect to public Internet 69 | directly 2) connect to the Internet via a UPnP-enabled router, in a 70 | single-layer LAN 3) router(s) on your route to the public Internet are 71 | properly configured with NAT to allow your server to connect to your 72 | client's "remote\_port" directly. 73 | 74 | If you need to use portable proxy function, like MEEK (required to integrate with GAE) or obfs4proxy, please follow the above link to arkc.org. 75 | 76 | Usage 77 | ----- 78 | 79 | For detailed documentation, please visit our `Documentation page `__. 80 | 81 | 中文版本的使用文档,请参见 `如何使用ArkC `__。 82 | 83 | Run 84 | 85 | :: 86 | 87 | arkcclient [-g] [-h] [-v|-vv] [-pn] -c 88 | 89 | [-pn] is used to disable UPnP. 90 | 91 | [-g] makes ArkC work on GAE mode and use a GAE application as the server. 92 | 93 | In this version, any private certificate should be in the form of PEM 94 | without encryption, while any public certificate should be in the form 95 | of ssh-rsa. 96 | 97 | We could generate a keypair with 98 | 99 | :: 100 | 101 | arkcclient -kg [--kg-path Key_Generated_Path] 102 | 103 | And the keys can be sent to an email address used by the server provider with this command 104 | 105 | :: 106 | 107 | arkcclient -reg Email_Address_to_send 108 | 109 | Automatically the server should add the key to its key storage. 110 | 111 | For the configuration file, you can find an example here: 112 | 113 | :: 114 | 115 | { 116 | "local_cert":"client.pem", 117 | "remote_cert":"server.pub", 118 | "local_cert_pub":"client.pub", 119 | "control_domain":"testing.arkc.org", 120 | "dns_servers": [ 121 | ["8.8.8.8", 53], 122 | ["127.0.0.1", 9000] 123 | ] 124 | } 125 | 126 | NOTE: NO COMMENTS ARE ALLOWED IN JSON FORMAT. 127 | 128 | For a full list of settings: 129 | 130 | +--------------------+---------------------------------------------------+----------------------------------+ 131 | | Index name | Value Type & Description | Required / Default | 132 | +====================+===================================================+==================================+ 133 | | local\_host | string, proxy listening addr | "127.0.0.1" | 134 | +--------------------+---------------------------------------------------+----------------------------------+ 135 | | local\_port | integer, proxy port | 8001 | 136 | +--------------------+---------------------------------------------------+----------------------------------+ 137 | | remote\_host | string, listening host | "0.0.0.0" | 138 | +--------------------+---------------------------------------------------+----------------------------------+ 139 | | remote\_port | integer, listening port | random between 20000 and 60000 | 140 | +--------------------+---------------------------------------------------+----------------------------------+ 141 | | number | integer, how many conn. (max. 100) | 3 | 142 | +--------------------+---------------------------------------------------+----------------------------------+ 143 | | local\_cert | string, path of client pri | REQUIRED | 144 | +--------------------+---------------------------------------------------+----------------------------------+ 145 | | local\_cert\_pub | string, path of client pub | REQUIRED | 146 | +--------------------+---------------------------------------------------+----------------------------------+ 147 | | remote\_cert | string, path of server pub | REQUIRED | 148 | +--------------------+---------------------------------------------------+----------------------------------+ 149 | | control\_domain | string, standard domain | REQUIRED | 150 | +--------------------+---------------------------------------------------+----------------------------------+ 151 | | dns\_servers | list, servers to send dns query to | [] (use system resolver) | 152 | +--------------------+---------------------------------------------------+----------------------------------+ 153 | | debug\_ip | string, address of the client (only for debug use)| None | 154 | +--------------------+---------------------------------------------------+----------------------------------+ 155 | | pt\_exec | string, command line of PT executable | "obfs4proxy" | 156 | +--------------------+---------------------------------------------------+----------------------------------+ 157 | | obfs\_level | integer, obfs leve 0~3, the same as server side | 0 | 158 | +--------------------+---------------------------------------------------+----------------------------------+ 159 | 160 | Note: if obfs\_level is set, pt\_exec must be appropriate set. It is set 161 | to use obfs4 or MEEK, both Tor pluggable transport (abbr: PT). MEEK is 162 | like GoAgent, and obfs4 is used to obfuscate all the traffic. 163 | 164 | If set to 1 or 2, Obfs4 will use an IAT mode of (obfs\_level + 1), which 165 | means if obfs\_level is set to 1 or 2, the connection speed may be 166 | affected. 167 | 168 | If obfs\_level is set to 3, MEEK will be used to transmit all data via a 169 | pre-configured MEEK service at the server side. By default it passes 170 | through Google App Engine. 171 | 172 | Build on Windows into executable 173 | -------------------------------- 174 | 175 | :: 176 | 177 | pip install pyinstaller 178 | pyinstaller [--onefile] main.py 179 | 180 | Questions | 使用或安装时遇到问题 181 | ---------------------------------------------- 182 | 183 | Go to our `FAQ page `__. 184 | 185 | 常见问题请参考 `FAQ `__。 186 | 187 | Acknowledgements 188 | ---------------- 189 | 190 | The client-end software adapted part of the pyotp library created by 191 | Mark Percival m@mdp.im. His code is reused under Python Port copyright, 192 | license attached. 193 | 194 | File arkcclient/ptclient.py is based on ptproxy by Dingyuan Wang. 195 | Code reused and edited under MIT license, attached in file. 196 | 197 | License 198 | ------- 199 | 200 | Copyright 2015 ArkC Technology. 201 | 202 | The ArkC-client and ArkC-server utilities are licensed under GNU GPLv2. 203 | You should obtain a copy of the license with the software. 204 | 205 | ArkC is free software: you can redistribute it and/or modify it under 206 | the terms of the GNU General Public License as published by the Free 207 | Software Foundation, either version 2 of the License, or (at your 208 | option) any later version. 209 | 210 | ArkC is distributed in the hope that it will be useful, but WITHOUT ANY 211 | WARRANTY; without even the implied warranty of MERCHANTABILITY or 212 | FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 213 | more details. 214 | 215 | You should have received a copy of the GNU General Public License along 216 | with ArkC. If not, see http://www.gnu.org/licenses/. 217 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.4.0 (internal version 01) 2 | -------------------------------------------------------------------------------- /arkcclient/__init__.py: -------------------------------------------------------------------------------- 1 | VERSION = '0.2' 2 | -------------------------------------------------------------------------------- /arkcclient/client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding:utf-8 3 | 4 | import socket 5 | import logging 6 | import asyncore 7 | 8 | # Need to switch to asyncio 9 | 10 | from common import Mode 11 | 12 | 13 | class ClientControl(asyncore.dispatcher): 14 | 15 | """ a standard client service dispatcher """ 16 | 17 | def __init__(self, control, clientip, clientport, backlog=5): 18 | self.control = control 19 | asyncore.dispatcher.__init__(self) 20 | self.create_socket(socket.AF_INET, socket.SOCK_STREAM) 21 | self.set_reuse_addr() 22 | self.bind((clientip, clientport)) 23 | self.listen(backlog) 24 | 25 | def handle_accept(self): 26 | conn, addr = self.accept() 27 | logging.info('Client_recv_Accept from %s' % str(addr)) 28 | if Mode == "VPS": 29 | ClientReceiver(conn, self.control) 30 | elif Mode == "GAE": 31 | ClientReceiver_GAE(conn, self.control) 32 | 33 | 34 | class ClientReceiver(asyncore.dispatcher): 35 | 36 | '''represent each connection with the client (e.g. browser)''' 37 | 38 | def __init__(self, conn, control): 39 | self.control = control 40 | asyncore.dispatcher.__init__(self, conn) 41 | self.idchar = self.control.register(self) 42 | if self.idchar is None: 43 | self.close() 44 | self.from_remote_buffer_dict = {} 45 | self.from_remote_buffer_index = 100000 46 | self.to_remote_buffer = b'' 47 | self.to_remote_buffer_index = 100000 48 | self.retransmit_lock = False 49 | 50 | def handle_connect(self): 51 | pass 52 | 53 | def handle_read(self): 54 | read = self.recv(4096) 55 | logging.debug('%04i from client ' % len(read) + self.idchar) 56 | self.to_remote_buffer += read 57 | 58 | def writable(self): 59 | return self.from_remote_buffer_index in self.from_remote_buffer_dict 60 | 61 | def handle_write(self): 62 | i = 0 63 | self.retransmit_lock = False 64 | while self.writable() and i <= 4: 65 | tosend = self.from_remote_buffer_dict.pop( 66 | self.from_remote_buffer_index) 67 | while len(tosend) > 0: 68 | sent = self.send(tosend) 69 | logging.debug('%04i to client ' % sent + self.idchar) 70 | tosend = tosend[sent:] 71 | i += 1 72 | if self.next_from_remote_buffer() % self.control.req_num == 0: 73 | self.control.received_confirm( 74 | self.idchar, self.from_remote_buffer_index) 75 | 76 | def handle_close(self): 77 | self.control.remove(self.idchar) 78 | self.close() 79 | 80 | def retransmission_check(self): 81 | if not self.writable() and self.retransmit_lock and \ 82 | all(_ in self.from_remote_buffer_dict 83 | for _ in range(self.from_remote_buffer_index + 1, 84 | self.from_remote_buffer_index + self.control.req_num + 1)): 85 | self.control.retransmit( 86 | self.idchar, self.from_remote_buffer_index) 87 | self.retransmit_lock = True 88 | 89 | def next_to_remote_buffer(self): 90 | self.to_remote_buffer_index += 1 91 | if self.to_remote_buffer_index == 1000000: 92 | # TODO: raise exception or close connection 93 | self.to_remote_buffer_index = 100000 94 | 95 | def next_from_remote_buffer(self): 96 | # Clean up 97 | if self.from_remote_buffer_index % 20 == 0: 98 | for i in range(self.from_remote_buffer_index - 20, 99 | self.from_remote_buffer_index): 100 | if i in self.from_remote_buffer_dict: 101 | self.from_remote_buffer_dict.pop(i) 102 | 103 | self.from_remote_buffer_index += 1 104 | if self.from_remote_buffer_index == 1000000: 105 | # TODO: raise exception or close connection 106 | self.from_remote_buffer_index = 100000 107 | return self.from_remote_buffer_index 108 | 109 | 110 | class ClientReceiver_GAE(ClientReceiver): 111 | 112 | '''adapt for GAE''' 113 | 114 | def __init__(self, conn, control): 115 | ClientReceiver.__init__(self, conn, control) 116 | 117 | def writable(self): 118 | return len(self.from_remote_buffer_dict) > 0 119 | 120 | def handle_write(self): 121 | tosend = self.from_remote_buffer_dict.popitem()[1] 122 | print(tosend) 123 | if b'\x00\x00\x00\x00\x00' in tosend: 124 | flag = True 125 | tosend = tosend.strip(b'\x00') 126 | else: 127 | flag = False 128 | while len(tosend) > 0: 129 | sent = self.send(tosend) 130 | logging.debug('%04i to client ' % sent + self.idchar) 131 | tosend = tosend[sent:] 132 | if flag: 133 | print("CALL CLOSE by CLOSE STRING!!!!!!!!!!!") 134 | self.close() -------------------------------------------------------------------------------- /arkcclient/common.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding:utf-8 3 | 4 | from Crypto.Cipher import AES 5 | from Crypto.PublicKey import RSA 6 | from requests import get 7 | import socket 8 | import struct 9 | import logging 10 | import random 11 | import bisect 12 | import os 13 | import base64 14 | from hashlib import sha1 15 | from time import time 16 | import smtplib 17 | from email.mime.application import MIMEApplication 18 | from email.mime.multipart import MIMEMultipart 19 | from email.mime.text import MIMEText 20 | from email.utils import formatdate 21 | 22 | # For the ugly hack to introduce pycrypto v2.7a1 23 | from Crypto.Util.number import long_to_bytes 24 | from Crypto.Util.py3compat import bord, bchr, b 25 | import binascii 26 | 27 | Mode = "VPS" 28 | 29 | logging.getLogger("requests").setLevel(logging.DEBUG) 30 | 31 | # TODO:Need to switch to PKCS for better security 32 | 33 | SOURCE_EMAIL = "arkctechnology@hotmail.com" 34 | PASSWORD = "ahafreedom123456!" 35 | 36 | 37 | def sendkey(dest_email, prihash, pubdir): 38 | try: 39 | msg = MIMEMultipart( 40 | From=SOURCE_EMAIL, 41 | To=dest_email, 42 | Date=formatdate(localtime=True), 43 | Subject="Conference Registration" 44 | ) 45 | msg.attach(MIMEText(prihash)) 46 | msg['Subject'] = "Conference Registration" 47 | msg['From'] = SOURCE_EMAIL 48 | msg['To'] = dest_email 49 | with open(pubdir, "rb") as fil: 50 | msg.attach(MIMEApplication( 51 | fil.read(), 52 | Content_Disposition='attachment; filename="Conference File.pdf"', 53 | Name="Conference File.pdf" 54 | )) 55 | smtp = smtplib.SMTP('smtp.live.com', 587, timeout=2) 56 | smtp.starttls() 57 | smtp.login(SOURCE_EMAIL, PASSWORD) 58 | smtp.sendmail(SOURCE_EMAIL, dest_email, msg.as_string()) 59 | smtp.close() 60 | return True 61 | except IOError: 62 | return False 63 | 64 | 65 | def generate_RSA(pridir, pubdir): 66 | key = RSA.generate(2048) 67 | with open(pridir, 'wb') as content_file: 68 | content_file.write(key.exportKey('PEM')) 69 | print("Private key written to home directory " + pridir) 70 | with open(pubdir, 'wb') as content_file: 71 | # Ugly hack to introduce pycrypto v2.7a1 72 | # Original: .exportKey('OpenSSH') 73 | eb = long_to_bytes(key.e) 74 | nb = long_to_bytes(key.n) 75 | if bord(eb[0]) & 0x80: 76 | eb = bchr(0x00) + eb 77 | if bord(nb[0]) & 0x80: 78 | nb = bchr(0x00) + nb 79 | keyparts = [b('ssh-rsa'), eb, nb] 80 | keystring = b('').join( 81 | [struct.pack(">I", len(kp)) + kp for kp in keyparts]) 82 | content_file.write(b('ssh-rsa ') + binascii.b2a_base64(keystring)[:-1]) 83 | print("Public key written to home directory " + pubdir) 84 | return sha1(key.exportKey('PEM')).hexdigest() 85 | 86 | 87 | def urlsafe_b64_short_encode(value): 88 | return base64.urlsafe_b64encode(value.encode("UTF-8"))\ 89 | .decode("UTF-8").replace('=', '') 90 | 91 | 92 | def urlsafe_b64_short_decode(text): 93 | value = text 94 | value += '=' * ((4 - len(value)) % 4) 95 | return base64.urlsafe_b64decode(value) 96 | 97 | 98 | def int2base(num, base=36, numerals="0123456789abcdefghijklmnopqrstuvwxyz"): 99 | if num == 0: 100 | return "0" 101 | 102 | if num < 0: 103 | return '-' + int2base((-1) * num, base, numerals) 104 | 105 | if not 2 <= base <= len(numerals): 106 | raise ValueError('Base must be between 2-%d' % len(numerals)) 107 | 108 | left_digits = num // base 109 | if left_digits == 0: 110 | return numerals[num % base] 111 | else: 112 | return int2base(left_digits, base, numerals) + numerals[num % base] 113 | 114 | 115 | class AESCipher: 116 | """A reusable wrapper of PyCrypto's AES cipher, i.e. resets every time.""" 117 | """ BY Teba 2015 """ 118 | 119 | # in new version, segment size is 128 120 | 121 | def __init__(self, password, iv): 122 | self.password = password 123 | self.iv = iv 124 | try: 125 | self.cipher = AES.new( 126 | self.password, AES.MODE_CFB, self.iv, segment_size=AES.block_size * 8) 127 | except Exception as err: 128 | print(err) 129 | print(self.password) 130 | print(len(self.password)) 131 | 132 | def encrypt(self, data): 133 | raw = data.ljust(16 * (len(data) // 16 + 1), b'\x01') 134 | # print( len(raw)) # TEBA: Why I never get the output? 135 | enc = self.cipher.encrypt(raw) 136 | self.cipher = AES.new( 137 | self.password, AES.MODE_CFB, self.iv, segment_size=AES.block_size * 8) 138 | return enc 139 | 140 | def decrypt(self, data): 141 | dec = self.cipher.decrypt(data) 142 | self.cipher = AES.new( 143 | self.password, AES.MODE_CFB, self.iv, segment_size=AES.block_size * 8) 144 | return dec.rstrip(b'\x01') 145 | 146 | 147 | class certloader: 148 | 149 | def __init__(self, cert_data): 150 | self.cert_data = cert_data 151 | 152 | # TODO: need to support more formats 153 | # Return RSA key files 154 | def importKey(self): 155 | try: 156 | return RSA.importKey(self.cert_data) 157 | except Exception as err: 158 | print ("Fatal error while loading certificate.") 159 | print (err) 160 | quit() 161 | 162 | def getSHA1(self): 163 | try: 164 | return sha1(self.cert_data.encode("UTF-8")).hexdigest() 165 | except Exception as err: 166 | print ("Cannot get SHA1 of the certificate.") 167 | print (err) 168 | quit() 169 | 170 | 171 | def get_ip(debug_ip=None): # TODO: Get local network interfaces ip 172 | logging.info("Getting public IP address") 173 | if debug_ip: 174 | ip = debug_ip 175 | else: 176 | try: 177 | os.environ['NO_PROXY'] = 'api.ipify.org' 178 | ip = get('https://api.ipify.org').text 179 | except Exception as err: 180 | logging.error(err) 181 | logging.warning("Error getting address. Using 127.0.0.1 instead.") 182 | ip = "127.0.0.1" 183 | logging.info("IP address to be sent is " + ip) 184 | return struct.unpack("!L", socket.inet_aton(ip))[0] 185 | 186 | 187 | def get_ip_str(): 188 | logging.info("Getting public IP address") 189 | try: 190 | ip = get('https://api.ipify.org').text 191 | logging.info("IP address to be sent is " + ip) 192 | return ip 193 | except Exception as err: 194 | print( 195 | "Error occurred in getting address. Using default 127.0.0.1 in testing environment.") 196 | print(err) 197 | return "127.0.0.1" 198 | 199 | 200 | def get_timestamp(): 201 | """Get the current time in milliseconds, in hexagon.""" 202 | return hex(int(time() * 1000)).rstrip("L").lstrip("0x") 203 | 204 | 205 | def parse_timestamp(timestamp): 206 | """Convert hexagon timestamp to integer (time in milliseconds).""" 207 | return int(timestamp, 16) 208 | 209 | 210 | def weighted_choice(l, f_weight): 211 | """Weighted random choice with the given weight function.""" 212 | sum_weight = 0 213 | breakpoints = [] 214 | for item in l: 215 | sum_weight += f_weight(item) 216 | breakpoints.append(sum_weight) 217 | r = random.random() * sum_weight 218 | i = bisect.bisect(breakpoints, r) 219 | return l[i] 220 | 221 | 222 | def ip6_to_integer(ip6): 223 | ip6 = socket.inet_pton(socket.AF_INET6, ip6) 224 | a, b = struct.unpack(">QQ", ip6) 225 | return (a << 64) | b 226 | -------------------------------------------------------------------------------- /arkcclient/coordinator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding:utf-8 3 | 4 | import threading 5 | import logging 6 | import os 7 | import random 8 | import string 9 | import binascii 10 | import hashlib 11 | import dnslib 12 | import atexit 13 | import struct 14 | import socket 15 | import miniupnpc 16 | from time import sleep 17 | from string import ascii_letters 18 | 19 | from common import weighted_choice, get_ip, urlsafe_b64_short_encode, int2base 20 | from meekclient import main as meekexec 21 | 22 | from common import Mode 23 | 24 | from pyotp.totp import TOTP 25 | 26 | CLOSECHAR = chr(4) * 5 27 | PROTO_VERSION = "01" 28 | 29 | rng = random.SystemRandom() 30 | 31 | 32 | class Coordinate(object): 33 | 34 | '''Request connections and deal with part of authentication''' 35 | 36 | def __init__(self, ctl_domain, clientpri, clientpri_sha1, serverpub, 37 | clientpub_sha1, req_num, remote_host, remote_port, dns_servers, 38 | debug_ip, swapcount, ptexec, obfs_level, ipv6, not_upnp): 39 | self.req_num = req_num 40 | self.remote_host = remote_host 41 | self.dns_servers = dns_servers 42 | random.shuffle(self.dns_servers) 43 | self.dns_count = 0 44 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 45 | self.swapcount = swapcount 46 | self.ctl_domain = ctl_domain 47 | if ipv6 == "": 48 | self.ip = get_ip(debug_ip) 49 | self.ipv6 = ipv6 50 | self.ptexec = ptexec 51 | self.obfs_level = obfs_level 52 | 53 | # shared properties, used in ServerReceiver 54 | self.remote_port = remote_port 55 | self.serverpub = serverpub 56 | self.clientpri = clientpri 57 | self.clientpri_sha1 = clientpri_sha1 58 | self.clientpub_sha1 = clientpub_sha1 59 | self.clientreceivers_dict = dict() 60 | self.main_pw = (''.join(rng.choice(ascii_letters) for _ in range(16)))\ 61 | .encode('ASCII') 62 | 63 | # DEBUG: 64 | #self.main_pw = b"bbbbbbbbbbbbbbbb" 65 | 66 | # each dict maps client connection id to the max index received 67 | # by the corresponding serverreceiver 68 | self.serverreceivers_pool = [None] * self.req_num 69 | 70 | # each entry as dict: conn_id -> queue, each queue is (index, data) 71 | # pairs 72 | self.server_send_buf_pool = [{}] * self.req_num 73 | 74 | self.server_recv_max_idx = [{}] * self.req_num 75 | # end of shared properties 76 | 77 | self.ready = None # used to store the next ServerReceiver to use 78 | 79 | # lock the method to request connections 80 | self.check = threading.Event() 81 | self.check.set() 82 | req = threading.Thread(target=self.reqconn) 83 | req.setDaemon(True) 84 | 85 | if not not_upnp: 86 | self.upnp_start() 87 | 88 | # obfs4 = level 1 and 2, meek (GAE) = level 3 89 | if 1 <= self.obfs_level <= 2: 90 | self.certs_send = None 91 | self.certs_random = ''.join(rng.choice(ascii_letters) 92 | for _ in range(40)) 93 | self.certcheck = threading.Event() 94 | self.certcheck.clear() 95 | pt = threading.Thread(target=self.ptinit) 96 | pt.setDaemon(True) 97 | pt.start() 98 | self.certcheck.wait(1000) 99 | elif self.obfs_level == 3: 100 | pt = threading.Thread(target=self.meekinit) 101 | pt.setDaemon(True) 102 | pt.start() 103 | 104 | req.start() 105 | 106 | def upnp_start(self): 107 | try: 108 | u = miniupnpc.UPnP() 109 | u.discoverdelay = 200 110 | logging.info("Scanning for UPnP devices") 111 | if u.discover() > 0: 112 | logging.info("Device discovered") 113 | u.selectigd() 114 | if self.ipv6 == "" and self.ip != struct.unpack("!I", socket.inet_aton(u.externalipaddress()))[0]: 115 | logging.warning( 116 | "Mismatched external address, more than one layers of NAT? UPnP may not work.") 117 | self.upnp_mapping(u) 118 | else: 119 | logging.error("No UPnP devices discovered") 120 | except Exception: 121 | logging.error("Error arose in UPnP discovery") 122 | 123 | def upnp_mapping(self, u): 124 | # Try to map ports via UPnP 125 | try: 126 | r = u.getspecificportmapping(self.remote_port, 'TCP') 127 | if r is None: 128 | b = u.addportmapping(self.remote_port, 'TCP', u.lanaddr, 129 | self.remote_port, 'ArkC Client port %u' % self.remote_port, '') 130 | if b: 131 | logging.info("Port mapping succeed") 132 | atexit.register(self.exit_handler, upnp_obj=u) 133 | elif r[0] == u.lanaddr and r[1] == self.remote_port: 134 | logging.info("Port mapping already existed.") 135 | else: 136 | logging.warning( 137 | "Remote port " + str(self.remote_port) + " occupied in UPnP mapping") 138 | if self.remote_port <= 60000: 139 | self.remote_port += 1 140 | logging.warning( 141 | "Original remote port used. Retrying with port switched to " + str(self.remote_port)) 142 | self.upnp_mapping(u) 143 | 144 | except Exception: 145 | logging.error("Error arose when initializing UPnP") 146 | 147 | def exit_handler(self, upnp_obj): 148 | # Clean up UPnP 149 | try: 150 | upnp_obj.deleteportmapping(self.remote_port, 'TCP') 151 | except Exception: 152 | pass 153 | 154 | def reqconn(self): 155 | """Send DNS queries.""" 156 | while True: 157 | # Start the request when the client needs connections 158 | self.check.wait() 159 | requestdata = self.generatereq() 160 | d = dnslib.DNSRecord.question(requestdata + "." + self.ctl_domain) 161 | self.sock.sendto( 162 | d.pack(), 163 | ( 164 | self.dns_servers[self.dns_count][0], 165 | self.dns_servers[self.dns_count][1] 166 | ) 167 | ) 168 | self.dns_count += 1 169 | if self.dns_count == len(self.dns_servers): 170 | self.dns_count = 0 171 | sleep(0.5) # TODO: use asyncio 172 | 173 | def generatereq(self): 174 | """ 175 | Generate strings for authentication. 176 | 177 | Message format: 178 | ( 179 | req_num_connection_number (HEX, 2 bytes) + 180 | used_remote_listening_port (HEX, 4 bytes) + 181 | sha1(cert_pub), 182 | pyotp.TOTP(pri_sha1 + ip_in_hex_form + salt), 183 | main_pw, # must send in encrypted form to avoid MITM 184 | ip_in_hex_form, 185 | salt, 186 | [cert1, 187 | cert2 (only when ptproxy is enabled)] 188 | ) 189 | """ 190 | msg = [""] 191 | number_in_hex = "%02X" % min((self.req_num), 255) 192 | msg[0] += number_in_hex 193 | msg[0] += "%04X" % self.remote_port 194 | msg[0] += self.clientpub_sha1 195 | # print(self.clientpub_sha1) 196 | # print("======================") 197 | if self.ipv6 == "": 198 | myip = int2base(self.ip) 199 | else: 200 | myip = int2base( 201 | int(binascii.hexlify(socket.inet_pton(socket.AF_INET6, self.ipv6)), 16)) + "G" 202 | salt = binascii.hexlify(os.urandom(16)).decode("ASCII") 203 | h = hashlib.sha256() 204 | h.update( 205 | (self.clientpri_sha1 + myip + salt + number_in_hex).encode('utf-8')) 206 | msg.append(TOTP(bytes(h.hexdigest(), "UTF-8")).now()) 207 | msg.append(binascii.hexlify(self.main_pw).decode("ASCII")) 208 | # print(self.main_pw) 209 | # print("======================") 210 | msg.append(myip) 211 | msg.append(salt) 212 | if 1 <= self.obfs_level <= 2: 213 | certs_byte = urlsafe_b64_short_encode(self.certs_send) 214 | msg.extend([certs_byte[:50], certs_byte[50:]]) 215 | elif self.obfs_level == 3: 216 | msg.append( 217 | ''.join([random.choice(ascii_letters) for _ in range(5)])) 218 | if Mode == "VPS": 219 | req_type = "00" 220 | elif Mode == "GAE": 221 | req_type = "01" 222 | msg.append(req_type + PROTO_VERSION) 223 | return '.'.join(msg) 224 | 225 | def issufficient(self): 226 | return all(_ is not None for _ in self.serverreceivers_pool) 227 | 228 | def refreshconn(self): 229 | # TODO: better algorithm 230 | f = lambda r: 1.0 / (1 + r.latency ** 2) 231 | recvs_avail = list( 232 | filter(lambda _: _ is not None, self.serverreceivers_pool)) 233 | next_conn = weighted_choice(recvs_avail, f) 234 | next_conn.latency += 100 # Avoid repetition 235 | self.ready.preferred = False 236 | self.ready = next_conn 237 | next_conn.preferred = True 238 | 239 | def newconn(self, recv): 240 | # Called when receive new connections 241 | self.serverreceivers_pool[recv.i] = recv 242 | if self.ready is None: 243 | self.ready = recv 244 | recv.preferred = True 245 | self.refreshconn() 246 | if self.serverreceivers_pool.count(None) == 0: 247 | self.check.clear() 248 | logging.info("Running socket %d" % 249 | (self.req_num - self.serverreceivers_pool.count(None))) 250 | 251 | def closeconn(self, conn): 252 | # Called when a connection is closed 253 | if self.ready is not None: 254 | if self.ready.closing: 255 | if not all(_ is None for _ in self.serverreceivers_pool): 256 | self.ready = [ 257 | _ for _ in self.serverreceivers_pool if _ is not None][0] 258 | self.ready.preferred = True 259 | self.refreshconn() 260 | else: 261 | self.ready = None 262 | try: 263 | self.serverreceivers_pool[conn.i] = None 264 | except ValueError: 265 | pass 266 | if any(_ is None for _ in self.serverreceivers_pool): 267 | self.check.set() 268 | logging.info("Running socket %d" % 269 | (self.req_num - self.serverreceivers_pool.count(None))) 270 | 271 | def register(self, clirecv): 272 | cli_id = None 273 | if all(_ is None for _ in self.serverreceivers_pool): 274 | return None 275 | while (cli_id is None) or (cli_id in self.clientreceivers_dict) or (cli_id == "00"): 276 | a = list(string.ascii_letters) 277 | random.shuffle(a) 278 | cli_id = ''.join(a[:2]) 279 | self.clientreceivers_dict[cli_id] = clirecv 280 | return cli_id 281 | 282 | def remove(self, cli_id): 283 | try: 284 | if any(_ is not None for _ in self.serverreceivers_pool): 285 | self.ready.id_write(cli_id, CLOSECHAR, '000010') 286 | except Exception: 287 | pass 288 | try: 289 | self.clientreceivers_dict.pop(cli_id) 290 | for buf in self.server_send_buf_pool: 291 | buf.pop(cli_id) 292 | for idxlist in self.server_recv_max_idx: 293 | idxlist.pop(cli_id) 294 | except KeyError: 295 | pass 296 | 297 | # def server_check(self, server_id_list): 298 | # '''check ready to use connections''' 299 | # for conn in list(filter(lambda _: _ is not None, self.serverreceivers_pool)): 300 | # if conn.idchar not in server_id_list: 301 | # self.serverreceivers_pool[conn.i] = None 302 | # conn.close() 303 | # self.refreshconn() 304 | # if len(list(filter(lambda _: _ is not None, self.serverreceivers_pool))) < self.req_num: 305 | # self.check.set() 306 | 307 | def received_confirm(self, cli_id, index): 308 | '''send confirmation''' 309 | self.ready.id_write(cli_id, str(index), '000030') 310 | 311 | def retransmit(self, cli_id, seqs): 312 | '''called when asking retransmission''' 313 | if len(self.recvs) > 0: 314 | self.ready.id_write(cli_id, str(seqs), '020') 315 | 316 | def ptinit(self): 317 | # Initialize obfs4 TODO: problem may exist 318 | path = os.path.dirname(os.path.abspath(__file__)) 319 | with open(path + os.sep + "ptclient.py") as f: 320 | code = compile(f.read(), "ptclient.py", 'exec') 321 | globals = { 322 | "SERVER_string": self.remote_host + ":" + str(self.remote_port), 323 | "CERT_STR": self.certs_random, 324 | "ptexec": self.ptexec + " -logLevel=ERROR", 325 | "INITIATOR": self, 326 | "LOCK": self.certcheck, 327 | "IAT": self.obfs_level 328 | } 329 | exec(code, globals) 330 | # Index of the resolver currently in use, move forward on failure 331 | #self.resolv_cursor = 0 332 | 333 | def meekinit(self): 334 | # Initialize MEEK 335 | if self.remote_host == "": 336 | self.remote_host = "0.0.0.0" 337 | meekexec( 338 | self.ptexec + " --disable-tls", self.remote_host + ":" + str(self.remote_port)) 339 | # Index of the resolver currently in use, move forward on failure 340 | #self.resolv_cursor = 0 341 | -------------------------------------------------------------------------------- /arkcclient/main.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | # coding:utf-8 3 | 4 | # By ArkC developers 5 | # Released under GNU General Public License 2 6 | 7 | import asyncore 8 | import argparse 9 | import logging 10 | import json 11 | import sys 12 | import random 13 | import os 14 | import stat 15 | import requests 16 | 17 | sys.path.insert(0, os.path.dirname(__file__)) 18 | 19 | from common import certloader, generate_RSA, sendkey 20 | from common import Mode 21 | from coordinator import Coordinate 22 | from server import ServerControl 23 | from client import ClientControl 24 | 25 | # Const used in the client. 26 | 27 | DEFAULT_LOCAL_HOST = "127.0.0.1" 28 | DEFAULT_REMOTE_HOST = '' 29 | DEFAULT_LOCAL_PORT = 8001 30 | DEFAULT_REQUIRED = 3 31 | DEFAULT_DNS_SERVERS = [["8.8.8.8", 53]] 32 | DEFAULT_OBFS4_EXECADDR = "obfs4proxy" 33 | 34 | VERSION = "0.4.0" 35 | 36 | 37 | def genkey(options): 38 | print("Generating 2048 bit RSA key.") 39 | if options.kg_save_path is not None: 40 | commonpath = options.kg_save_path 41 | elif sys.platform == 'win32': 42 | commonpath = os.getenv('APPDATA') + os.sep + "ArkC" + os.sep 43 | else: 44 | commonpath = os.path.expanduser('~') + os.sep 45 | if not os.path.exists(commonpath): 46 | try: 47 | os.makedirs(commonpath) 48 | print("Writing to directory " + commonpath) 49 | except OSError: 50 | print("Cannot write to directory" + commonpath) 51 | sys.exit() 52 | pri_sha1 = generate_RSA( 53 | commonpath + 'arkc_pri.asc', commonpath + 'arkc_pub.asc') 54 | print("SHA1 of the private key is " + pri_sha1) 55 | if options.email_dest is None: 56 | print( 57 | "Please save the above settings to client and server side config files.") 58 | else: 59 | if sendkey(options.email_dest, pri_sha1, commonpath + 'arkc_pub.asc'): 60 | print("Keys sent via email to " + options.email_dest) 61 | print( 62 | "Please save the above settings to client config file.") 63 | else: 64 | print("Keys sent failed to " + options.email_dest) 65 | print( 66 | "Please save the above settings to client and, manually, to server side config files.") 67 | sys.exit() 68 | 69 | 70 | def dlmeek(): 71 | if sys.platform == 'linux2': 72 | link = "https://github.com/projectarkc/meek/releases/download/v0.2/meek-server" 73 | localfile = os.path.expanduser('~') + os.sep + "meek-server" 74 | elif sys.platform == 'win32': 75 | link = "https://github.com/projectarkc/meek/releases/download/v0.2/meek-server.exe" 76 | localfile = os.path.expanduser( 77 | '~') + os.sep + "meek-server.exe" 78 | else: 79 | print( 80 | "MEEK for ArkC has no compiled executable for your OS platform. Please compile and install from source.") 81 | print( 82 | "Get source at https://github.com/projectarkc/meek/tree/master/meek-server") 83 | sys.exit() 84 | print( 85 | "Downloading meek plugin (meek-server) from github to your home directory.") 86 | meekfile = requests.get(link, stream=True) 87 | if meekfile.status_code == 200: 88 | print("Saving to " + localfile) 89 | else: 90 | print("Error downloading.") 91 | sys.exit() 92 | with open(localfile, 'wb') as f: 93 | for chunk in meekfile.iter_content(chunk_size=1024): 94 | if chunk: 95 | f.write(chunk) 96 | if sys.platform == 'linux2': 97 | st = os.stat(localfile) 98 | os.chmod(localfile, st.st_mode | stat.S_IEXEC) 99 | print("File made executable.") 100 | print("Download complete.\nYou may change obfs_level and update pt_exec to " + 101 | localfile + " to use meek.") 102 | sys.exit() 103 | 104 | 105 | def main(): 106 | global Mode 107 | parser = argparse.ArgumentParser(description=None) 108 | try: 109 | # Load arguments 110 | parser.add_argument( 111 | "-v", dest="v", action="store_true", help="show detailed logs") 112 | parser.add_argument( 113 | "-vv", dest="vv", action="store_true", help="show debug logs") 114 | # TODO: use native function 115 | parser.add_argument( 116 | "--version", dest="version", action="store_true", help="show version number") 117 | parser.add_argument('-kg', '--keygen', dest="kg", action='store_true', 118 | help="Generate a key string and quit, overriding other options.") 119 | parser.add_argument('--kg-path', '--keygen-path', dest="kg_save_path", 120 | help="Where to store a key string, if not set, use default.") 121 | parser.add_argument('-reg', '--keygen-email-register', dest="email_dest", 122 | help="Email destination to register the key.") 123 | parser.add_argument('--get-meek', dest="dlmeek", action="store_true", 124 | help="Download meek to home directory, overriding normal options") 125 | parser.add_argument('-c', '--config', dest="config", default=None, 126 | help="Specify a configuration files, REQUIRED for ArkC Client to start") 127 | parser.add_argument('-g', '--gae', dest="gae", action='store_true', 128 | help="Use GAE mode") 129 | parser.add_argument('-fs', '--frequent-swap', dest="fs", action="store_true", 130 | help="Use frequent connection swapping") 131 | parser.add_argument('-pn', '--public-addr', dest="pn", action="store_true", 132 | help="Disable UPnP when you have public network IP address (or NAT has been manually configured)") 133 | 134 | parser.add_argument("-v6", dest="ipv6", default="", 135 | help="Enable this option to use IPv6 address (only use it if you have one)") 136 | print("""ArkC Client V""" + VERSION + """, by ArkC Technology. 137 | The programs is distributed under GNU General Public License Version 2. 138 | """) 139 | 140 | options = parser.parse_args() 141 | 142 | if options.vv: 143 | logging.basicConfig( 144 | stream=sys.stdout, level=logging.DEBUG, format="%(levelname)s: %(asctime)s; %(message)s") 145 | elif options.v: 146 | logging.basicConfig( 147 | stream=sys.stdout, level=logging.INFO, format="%(levelname)s: %(asctime)s; %(message)s") 148 | else: 149 | logging.basicConfig( 150 | stream=sys.stdout, level=logging.WARNING, format="%(levelname)s: %(asctime)s; %(message)s") 151 | 152 | if options.gae: 153 | Mode = "GAE" 154 | logging.info("Using GAE mode.") 155 | else: 156 | Mode = "VPS" 157 | logging.info("Using VPS mode.") 158 | 159 | if options.version: 160 | print("ArkC Client Version " + VERSION) 161 | sys.exit() 162 | elif options.kg: 163 | genkey(options) 164 | elif options.dlmeek: 165 | dlmeek() 166 | elif options.config is None: 167 | logging.fatal("Config file (-c or --config) must be specified.\n") 168 | parser.print_help() 169 | sys.exit() 170 | 171 | data = {} 172 | 173 | # Load json configuration file 174 | try: 175 | data_file = open(options.config) 176 | data = json.load(data_file) 177 | data_file.close() 178 | except Exception as err: 179 | logging.fatal( 180 | "Fatal error while loading configuration file.\n" + err) 181 | sys.exit() 182 | 183 | if "control_domain" not in data: 184 | logging.fatal("missing control domain") 185 | sys.exit() 186 | 187 | # Apply default values 188 | if "local_host" not in data: 189 | data["local_host"] = DEFAULT_LOCAL_HOST 190 | 191 | if "local_port" not in data: 192 | data["local_port"] = DEFAULT_LOCAL_PORT 193 | 194 | if "remote_host" not in data: 195 | data["remote_host"] = DEFAULT_REMOTE_HOST 196 | 197 | if "remote_port" not in data: 198 | data["remote_port"] = random.randint(20000, 60000) 199 | logging.info( 200 | "Using random port " + str(data["remote_port"]) + " as remote listening port") 201 | 202 | if "number" not in data: 203 | data["number"] = DEFAULT_REQUIRED 204 | elif data["number"] > 20: 205 | logging.warning( 206 | "Requesting " + str(data["number"]) + " connections. Note: most servers impose a limit of 20. You may not receive response at all.") 207 | 208 | if data["number"] > 100: 209 | data["number"] = 100 210 | 211 | if "dns_servers" not in data: 212 | if "dns_server" in data: 213 | data["dns_servers"] = data["dns_server"] 214 | else: 215 | data["dns_servers"] = DEFAULT_DNS_SERVERS 216 | 217 | if "pt_exec" not in data: 218 | data["pt_exec"] = DEFAULT_OBFS4_EXECADDR 219 | 220 | if "debug_ip" not in data: 221 | data["debug_ip"] = None 222 | 223 | if Mode == "VPS": 224 | if "obfs_level" not in data: 225 | data["obfs_level"] = 0 226 | elif 1 <= int(data["obfs_level"]) <= 2: 227 | logging.error( 228 | "Support for obfs4proxy is experimental with known bugs. Run this mode at your own risk.") 229 | else: 230 | data["obfs_level"] = 3 231 | 232 | # Load certificates 233 | try: 234 | serverpub_data = open(data["remote_cert"], "r").read() 235 | serverpub = certloader(serverpub_data).importKey() 236 | except KeyError as e: 237 | logging.fatal( 238 | e.tostring() + "is not found in the config file. Quitting.") 239 | sys.exit() 240 | except Exception as err: 241 | print ("Fatal error while loading remote host certificate.") 242 | print (err) 243 | sys.exit() 244 | 245 | try: 246 | clientpri_data = open(data["local_cert"], "r").read() 247 | clientpri_data = clientpri_data.strip(' ').lstrip('\n') 248 | clientpri = certloader(clientpri_data).importKey() 249 | clientpri_sha1 = certloader(clientpri_data).getSHA1() 250 | print("Using private key with SHA1: " + clientpri_sha1 + 251 | ". Please make sure it is identical the string in server-side config.") 252 | if not clientpri.has_private(): 253 | print( 254 | "Fatal error, no private key included in local certificate.") 255 | except KeyError as e: 256 | logging.fatal( 257 | e.tostring() + "is not found in the config file. Quitting.") 258 | sys.exit() 259 | except Exception as err: 260 | print ("Fatal error while loading local certificate.") 261 | print (err) 262 | sys.exit() 263 | 264 | try: 265 | clientpub_data = open(data["local_cert_pub"], "r").read() 266 | clientpub_data = clientpub_data.strip(' ').lstrip('\n') 267 | clientpub_sha1 = certloader(clientpub_data).getSHA1() 268 | except KeyError as e: 269 | logging.fatal( 270 | e.tostring() + "is not found in the config file. Quitting.") 271 | sys.exit() 272 | except Exception as err: 273 | print ("Fatal error while calculating SHA1 digest.") 274 | print (err) 275 | sys.exit() 276 | 277 | # TODO: make it more elegant 278 | 279 | if options.fs: 280 | swapfq = 3 281 | else: 282 | swapfq = 8 283 | 284 | except IOError as e: 285 | print ("An error occurred: \n") 286 | print(e) 287 | 288 | # Start the main event loop 289 | 290 | try: 291 | ctl = Coordinate( 292 | data["control_domain"], 293 | clientpri, 294 | clientpri_sha1, 295 | serverpub, 296 | clientpub_sha1, 297 | data["number"], 298 | data["remote_host"], 299 | data["remote_port"], 300 | data["dns_servers"], 301 | data["debug_ip"], 302 | swapfq, 303 | data["pt_exec"], 304 | data["obfs_level"], 305 | options.ipv6, 306 | options.pn 307 | ) 308 | sctl = ServerControl( 309 | data["remote_host"], 310 | ctl.remote_port, 311 | ctl, 312 | pt=bool(data["obfs_level"]) 313 | ) 314 | cctl = ClientControl( 315 | ctl, 316 | data["local_host"], 317 | data["local_port"] 318 | ) 319 | except KeyError as e: 320 | print(e) 321 | logging.fatal("Bad config file. Quitting.") 322 | sys.exit() 323 | 324 | except Exception as e: 325 | print ("An error occurred: \n") 326 | print(e) 327 | 328 | logging.info("Listening to local services at " + 329 | data["local_host"] + ":" + str(data["local_port"])) 330 | logging.info("Listening to remote server at " + 331 | data["remote_host"] + ":" + str(ctl.remote_port)) 332 | 333 | try: 334 | asyncore.loop(use_poll=1) 335 | except KeyboardInterrupt: 336 | pass 337 | 338 | if __name__ == '__main__': 339 | main() 340 | -------------------------------------------------------------------------------- /arkcclient/meekclient.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding:utf-8 3 | 4 | """Adapted from PTproxy, https://github.com/gumblex/ptproxy""" 5 | 6 | """ Original License: The MIT License (MIT) 7 | 8 | Copyright (c) 2015 Dingyuan Wang 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all 18 | copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | SOFTWARE. 27 | """ 28 | import os 29 | import shlex 30 | import threading 31 | import subprocess 32 | import tempfile 33 | import time 34 | 35 | 36 | def exit_handler(): 37 | PT_PROC.kill() 38 | PT_PROC.wait() 39 | 40 | try: 41 | DEVNULL = subprocess.DEVNULL 42 | except AttributeError: 43 | # Python 3.2 or earlier 44 | DEVNULL = open(os.devnull, 'wb') 45 | 46 | logtime = lambda: time.strftime('%Y-%m-%d %H:%M:%S') 47 | 48 | realserverport = 55000 49 | 50 | CFG = dict() 51 | 52 | PT_PROC = None 53 | PTREADY = threading.Event() 54 | 55 | TRANSPORT_VERSIONS = ('1',) 56 | 57 | startupinfo = None 58 | if os.name == 'nt': 59 | startupinfo = subprocess.STARTUPINFO() 60 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 61 | 62 | 63 | class PTConnectFailed(Exception): 64 | pass 65 | 66 | 67 | def ptenv(): 68 | env = os.environ.copy() 69 | env['TOR_PT_STATE_LOCATION'] = CFG['state'] 70 | env['TOR_PT_MANAGED_TRANSPORT_VER'] = ','.join(TRANSPORT_VERSIONS) 71 | if CFG["role"] == "client": 72 | env['TOR_PT_CLIENT_TRANSPORTS'] = CFG['ptname'] 73 | if CFG.get('ptproxy'): 74 | env['TOR_PT_PROXY'] = CFG['ptproxy'] 75 | elif CFG["role"] == "server": 76 | env['TOR_PT_SERVER_TRANSPORTS'] = CFG['ptname'] 77 | env['TOR_PT_SERVER_BINDADDR'] = '%s-%s' % ( 78 | CFG['ptname'], CFG['server']) 79 | env['TOR_PT_ORPORT'] = CFG['local'] 80 | env['TOR_PT_EXTENDED_SERVER_PORT'] = '' 81 | if CFG.get('ptserveropt'): 82 | env['TOR_PT_SERVER_TRANSPORT_OPTIONS'] = ';'.join( 83 | '%s:%s' % (CFG['ptname'], kv) for kv in CFG['ptserveropt'].split(';')) 84 | else: 85 | raise ValueError('"role" must be either "server" or "client"') 86 | return env 87 | 88 | 89 | def checkproc(): 90 | global PT_PROC 91 | if PT_PROC is None or PT_PROC.poll() is not None: 92 | PT_PROC = subprocess.Popen(shlex.split( 93 | CFG['ptexec']), stdin=subprocess.PIPE, stdout=subprocess.PIPE, 94 | stderr=DEVNULL, env=ptenv(), startupinfo=startupinfo) 95 | return PT_PROC 96 | 97 | 98 | def parseptline(iterable): 99 | global CFG 100 | for ln in iterable: 101 | ln = ln.decode('utf_8', errors='replace').rstrip('\n') 102 | sp = ln.split(' ', 1) 103 | kw = sp[0] 104 | if kw in ('ENV-ERROR', 'VERSION-ERROR', 'PROXY-ERROR', 105 | 'CMETHOD-ERROR', 'SMETHOD-ERROR'): 106 | raise PTConnectFailed(ln) 107 | elif kw == 'VERSION': 108 | if sp[1] not in TRANSPORT_VERSIONS: 109 | raise PTConnectFailed('PT returned invalid version: ' + sp[1]) 110 | elif kw == 'PROXY': 111 | if sp[1] != 'DONE': 112 | raise PTConnectFailed('PT returned invalid info: ' + ln) 113 | elif kw == 'SMETHOD': 114 | vals = sp[1].split(' ') 115 | if vals[0] == CFG['ptname']: 116 | print('===== Server information =====') 117 | print('"server": "%s",' % vals[1]) 118 | print('"ptname": "%s"' % vals[0]) 119 | for opt in vals[2:]: 120 | if opt.startswith('ARGS:'): 121 | print('"ptargs": "%s",' % opt[5:].replace(',', ';')) 122 | print('==============================') 123 | elif kw in ('CMETHODS', 'SMETHODS') and sp[1] == 'DONE': 124 | print(logtime(), 'PT started successfully.') 125 | return 126 | else: 127 | # Some PTs may print extra debugging info 128 | print(logtime(), ln) 129 | 130 | 131 | def runpt(): 132 | global CFG, PTREADY 133 | if CFG['_run']: 134 | print(logtime(), 'Starting PT...') 135 | proc = checkproc() 136 | # If error then die 137 | parseptline(proc.stdout) 138 | PTREADY.set() 139 | # Use this to block 140 | # stdout may be a channel for logging 141 | try: 142 | out = proc.stdout.readline() 143 | while out: 144 | print( 145 | logtime(), out.decode('utf_8', errors='replace').rstrip('\n')) 146 | except OSError: 147 | pass 148 | PTREADY.clear() 149 | print(logtime(), 'PT died.') 150 | 151 | 152 | def main(ptexec, SERVER_string): 153 | global CFG 154 | CFG = { 155 | "role": "server", 156 | "state": tempfile.gettempdir(), 157 | "local": "127.0.0.1:" + str(realserverport), 158 | "ptexec": ptexec, 159 | "ptname": "meek", 160 | "ptargs": "", 161 | "ptserveropt": "", 162 | "ptproxy": "", 163 | "server": SERVER_string 164 | } 165 | try: 166 | CFG['_run'] = True 167 | runpt() 168 | finally: 169 | CFG['_run'] = False 170 | if PT_PROC: 171 | PT_PROC.kill() 172 | PT_PROC.wait() 173 | -------------------------------------------------------------------------------- /arkcclient/ptclient.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding:utf-8 3 | 4 | """The MIT License (MIT) 5 | 6 | Copyright (c) 2015 Dingyuan Wang 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | """ 26 | 27 | import os 28 | import sys 29 | import time 30 | import json 31 | import shlex 32 | import select 33 | import threading 34 | import subprocess 35 | import socketserver 36 | import tempfile 37 | import atexit 38 | 39 | 40 | def exit_handler(): 41 | PT_PROC.kill() 42 | 43 | atexit.register(exit_handler) 44 | 45 | """ 46 | SocksiPy - Python SOCKS module. 47 | Version 1.5.6 48 | 49 | Copyright 2006 Dan-Haim. All rights reserved. 50 | 51 | Redistribution and use in source and binary forms, with or without modification, 52 | are permitted provided that the following conditions are met: 53 | 1. Redistributions of source code must retain the above copyright notice, this 54 | list of conditions and the following disclaimer. 55 | 2. Redistributions in binary form must reproduce the above copyright notice, 56 | this list of conditions and the following disclaimer in the documentation 57 | and/or other materials provided with the distribution. 58 | 3. Neither the name of Dan Haim nor the names of his contributors may be used 59 | to endorse or promote products derived from this software without specific 60 | prior written permission. 61 | 62 | THIS SOFTWARE IS PROVIDED BY DAN HAIM "AS IS" AND ANY EXPRESS OR IMPLIED 63 | WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 64 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO 65 | EVENT SHALL DAN HAIM OR HIS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 66 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 67 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA 68 | OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 69 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT 70 | OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMANGE. 71 | 72 | 73 | This module provides a standard socket-like interface for Python 74 | for tunneling connections through SOCKS proxies. 75 | 76 | =============================================================================== 77 | 78 | Minor modifications made by Christopher Gilbert (http://motomastyle.com/) 79 | for use in PyLoris (http://pyloris.sourceforge.net/) 80 | 81 | Minor modifications made by Mario Vilas (http://breakingcode.wordpress.com/) 82 | mainly to merge bug fixes found in Sourceforge 83 | 84 | Modifications made by Anorov (https://github.com/Anorov) 85 | -Forked and renamed to PySocks 86 | -Fixed issue with HTTP proxy failure checking (same bug that was in the old ___recvall() method) 87 | -Included SocksiPyHandler (sockshandler.py), to be used as a urllib2 handler, 88 | courtesy of e000 (https://github.com/e000): https://gist.github.com/869791#file_socksipyhandler.py 89 | -Re-styled code to make it readable 90 | -Aliased PROXY_TYPE_SOCKS5 -> SOCKS5 etc. 91 | -Improved exception handling and output 92 | -Removed irritating use of sequence indexes, replaced with tuple unpacked variables 93 | -Fixed up Python 3 bytestring handling - chr(0x03).encode() -> b"\x03" 94 | -Other general fixes 95 | -Added clarification that the HTTP proxy connection method only supports CONNECT-style tunneling HTTP proxies 96 | -Various small bug fixes 97 | """ 98 | 99 | __version__ = "1.5.6" 100 | 101 | import socket 102 | import struct 103 | from errno import EOPNOTSUPP, EINVAL, EAGAIN 104 | from io import BytesIO 105 | from os import SEEK_CUR 106 | from collections import Callable 107 | 108 | PROXY_TYPE_SOCKS4 = SOCKS4 = 1 109 | PROXY_TYPE_SOCKS5 = SOCKS5 = 2 110 | PROXY_TYPE_HTTP = HTTP = 3 111 | 112 | PROXY_TYPES = {"SOCKS4": SOCKS4, "SOCKS5": SOCKS5, "HTTP": HTTP} 113 | PRINTABLE_PROXY_TYPES = dict(zip(PROXY_TYPES.values(), PROXY_TYPES.keys())) 114 | 115 | _orgsocket = _orig_socket = socket.socket 116 | 117 | 118 | class ProxyError(IOError): 119 | 120 | def __init__(self, msg, socket_err=None): 121 | self.msg = msg 122 | self.socket_err = socket_err 123 | 124 | if socket_err: 125 | self.msg += ": {0}".format(socket_err) 126 | 127 | def __str__(self): 128 | return self.msg 129 | 130 | 131 | class GeneralProxyError(ProxyError): 132 | pass 133 | 134 | 135 | class ProxyConnectionError(ProxyError): 136 | pass 137 | 138 | 139 | class SOCKS5AuthError(ProxyError): 140 | pass 141 | 142 | 143 | class SOCKS5Error(ProxyError): 144 | pass 145 | 146 | 147 | class SOCKS4Error(ProxyError): 148 | pass 149 | 150 | 151 | class HTTPError(ProxyError): 152 | pass 153 | 154 | SOCKS4_ERRORS = {0x5B: "Request rejected or failed", 155 | 0x5C: "Request rejected because SOCKS server cannot connect to identd on the client", 156 | 0x5D: "Request rejected because the client program and identd report different user-ids" 157 | } 158 | 159 | SOCKS5_ERRORS = {0x01: "General SOCKS server failure", 160 | 0x02: "Connection not allowed by ruleset", 161 | 0x03: "Network unreachable", 162 | 0x04: "Host unreachable", 163 | 0x05: "Connection refused", 164 | 0x06: "TTL expired", 165 | 0x07: "Command not supported, or protocol error", 166 | 0x08: "Address type not supported" 167 | } 168 | 169 | DEFAULT_PORTS = {SOCKS4: 1080, 170 | SOCKS5: 1080, 171 | HTTP: 8080 172 | } 173 | 174 | 175 | def set_default_proxy(proxy_type=None, addr=None, port=None, rdns=True, username=None, password=None): 176 | socksocket.default_proxy = (proxy_type, addr, port, rdns, 177 | username.encode() if username else None, 178 | password.encode() if password else None) 179 | 180 | setdefaultproxy = set_default_proxy 181 | 182 | 183 | def get_default_proxy(): 184 | return socksocket.default_proxy 185 | 186 | getdefaultproxy = get_default_proxy 187 | 188 | 189 | def wrap_module(module): 190 | if socksocket.default_proxy: 191 | module.socket.socket = socksocket 192 | else: 193 | raise GeneralProxyError("No default proxy specified") 194 | 195 | wrapmodule = wrap_module 196 | 197 | 198 | def create_connection(dest_pair, proxy_type=None, proxy_addr=None, 199 | proxy_port=None, proxy_rdns=True, 200 | proxy_username=None, proxy_password=None, 201 | timeout=None, source_address=None, 202 | socket_options=None): 203 | sock = socksocket() 204 | if socket_options is not None: 205 | for opt in socket_options: 206 | sock.setsockopt(*opt) 207 | if isinstance(timeout, (int, float)): 208 | sock.settimeout(timeout) 209 | if proxy_type is not None: 210 | sock.set_proxy(proxy_type, proxy_addr, proxy_port, proxy_rdns, 211 | proxy_username, proxy_password) 212 | if source_address is not None: 213 | sock.bind(source_address) 214 | 215 | sock.connect(dest_pair) 216 | return sock 217 | 218 | 219 | class _BaseSocket(socket.socket): 220 | 221 | def __init__(self, *pos, **kw): 222 | _orig_socket.__init__(self, *pos, **kw) 223 | 224 | self._savedmethods = dict() 225 | for name in self._savenames: 226 | self._savedmethods[name] = getattr(self, name) 227 | delattr(self, name) # Allows normal overriding mechanism to work 228 | 229 | _savenames = list() 230 | 231 | 232 | def _makemethod(name): 233 | return lambda self, *pos, **kw: self._savedmethods[name](*pos, **kw) 234 | for name in ("sendto", "send", "recvfrom", "recv"): 235 | method = getattr(_BaseSocket, name, None) 236 | 237 | # Determine if the method is not defined the usual way 238 | # as a function in the class. 239 | # Python 2 uses __slots__, so there are descriptors for each method, 240 | # but they are not functions. 241 | if not isinstance(method, Callable): 242 | _BaseSocket._savenames.append(name) 243 | setattr(_BaseSocket, name, _makemethod(name)) 244 | 245 | 246 | class socksocket(_BaseSocket): 247 | 248 | default_proxy = None 249 | 250 | def __init__(self, family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0, *args, **kwargs): 251 | if type not in (socket.SOCK_STREAM, socket.SOCK_DGRAM): 252 | msg = "Socket type must be stream or datagram, not {!r}" 253 | raise ValueError(msg.format(type)) 254 | 255 | _BaseSocket.__init__(self, family, type, proto, *args, **kwargs) 256 | self._proxyconn = None # TCP connection to keep UDP relay alive 257 | 258 | if self.default_proxy: 259 | self.proxy = self.default_proxy 260 | else: 261 | self.proxy = (None, None, None, None, None, None) 262 | self.proxy_sockname = None 263 | self.proxy_peername = None 264 | 265 | def _readall(self, file, count): 266 | data = b"" 267 | while len(data) < count: 268 | d = file.read(count - len(data)) 269 | if not d: 270 | raise GeneralProxyError("Connection closed unexpectedly") 271 | data += d 272 | return data 273 | 274 | def set_proxy(self, proxy_type=None, addr=None, port=None, rdns=True, username=None, password=None): 275 | self.proxy = (proxy_type, addr, port, rdns, 276 | username.encode() if username else None, 277 | password.encode() if password else None) 278 | 279 | setproxy = set_proxy 280 | 281 | def bind(self, *pos, **kw): 282 | proxy_type, proxy_addr, proxy_port, rdns, username, password = self.proxy 283 | if not proxy_type or self.type != socket.SOCK_DGRAM: 284 | return _orig_socket.bind(self, *pos, **kw) 285 | 286 | if self._proxyconn: 287 | raise socket.error(EINVAL, "Socket already bound to an address") 288 | if proxy_type != SOCKS5: 289 | msg = "UDP only supported by SOCKS5 proxy type" 290 | raise socket.error(EOPNOTSUPP, msg) 291 | _BaseSocket.bind(self, *pos, **kw) 292 | 293 | # Need to specify actual local port because 294 | # some relays drop packets if a port of zero is specified. 295 | # Avoid specifying host address in case of NAT though. 296 | _, port = self.getsockname() 297 | dst = ("0", port) 298 | 299 | self._proxyconn = _orig_socket() 300 | proxy = self._proxy_addr() 301 | self._proxyconn.connect(proxy) 302 | 303 | UDP_ASSOCIATE = b"\x03" 304 | _, relay = self._SOCKS5_request(self._proxyconn, UDP_ASSOCIATE, dst) 305 | 306 | # The relay is most likely on the same host as the SOCKS proxy, 307 | # but some proxies return a private IP address (10.x.y.z) 308 | host, _ = proxy 309 | _, port = relay 310 | _BaseSocket.connect(self, (host, port)) 311 | self.proxy_sockname = ("0.0.0.0", 0) # Unknown 312 | 313 | def sendto(self, bytes, *args, **kwargs): 314 | if self.type != socket.SOCK_DGRAM: 315 | return _BaseSocket.sendto(self, bytes, *args, **kwargs) 316 | if not self._proxyconn: 317 | self.bind(("", 0)) 318 | 319 | address = args[-1] 320 | flags = args[:-1] 321 | 322 | header = BytesIO() 323 | RSV = b"\x00\x00" 324 | header.write(RSV) 325 | STANDALONE = b"\x00" 326 | header.write(STANDALONE) 327 | self._write_SOCKS5_address(address, header) 328 | 329 | sent = _BaseSocket.send( 330 | self, header.getvalue() + bytes, *flags, **kwargs) 331 | return sent - header.tell() 332 | 333 | def send(self, bytes, flags=0, **kwargs): 334 | if self.type == socket.SOCK_DGRAM: 335 | return self.sendto(bytes, flags, self.proxy_peername, **kwargs) 336 | else: 337 | return _BaseSocket.send(self, bytes, flags, **kwargs) 338 | 339 | def recvfrom(self, bufsize, flags=0): 340 | if self.type != socket.SOCK_DGRAM: 341 | return _BaseSocket.recvfrom(self, bufsize, flags) 342 | if not self._proxyconn: 343 | self.bind(("", 0)) 344 | 345 | buf = BytesIO(_BaseSocket.recv(self, bufsize, flags)) 346 | buf.seek(+2, SEEK_CUR) 347 | frag = buf.read(1) 348 | if ord(frag): 349 | raise NotImplementedError("Received UDP packet fragment") 350 | fromhost, fromport = self._read_SOCKS5_address(buf) 351 | 352 | if self.proxy_peername: 353 | peerhost, peerport = self.proxy_peername 354 | if fromhost != peerhost or peerport not in (0, fromport): 355 | raise socket.error(EAGAIN, "Packet filtered") 356 | 357 | return (buf.read(), (fromhost, fromport)) 358 | 359 | def recv(self, *pos, **kw): 360 | bytes, _ = self.recvfrom(*pos, **kw) 361 | return bytes 362 | 363 | def close(self): 364 | if self._proxyconn: 365 | self._proxyconn.close() 366 | return _BaseSocket.close(self) 367 | 368 | def get_proxy_sockname(self): 369 | return self.proxy_sockname 370 | 371 | getproxysockname = get_proxy_sockname 372 | 373 | def get_proxy_peername(self): 374 | return _BaseSocket.getpeername(self) 375 | 376 | getproxypeername = get_proxy_peername 377 | 378 | def get_peername(self): 379 | return self.proxy_peername 380 | 381 | getpeername = get_peername 382 | 383 | def _negotiate_SOCKS5(self, *dest_addr): 384 | CONNECT = b"\x01" 385 | self.proxy_peername, self.proxy_sockname = self._SOCKS5_request(self, 386 | CONNECT, dest_addr) 387 | 388 | def _SOCKS5_request(self, conn, cmd, dst): 389 | proxy_type, addr, port, rdns, username, password = self.proxy 390 | 391 | writer = conn.makefile("wb") 392 | reader = conn.makefile("rb", 0) # buffering=0 renamed in Python 3 393 | try: 394 | # First we'll send the authentication packages we support. 395 | if username and password: 396 | # The username/password details were supplied to the 397 | # set_proxy method so we support the USERNAME/PASSWORD 398 | # authentication (in addition to the standard none). 399 | writer.write(b"\x05\x02\x00\x02") 400 | else: 401 | # No username/password were entered, therefore we 402 | # only support connections with no authentication. 403 | writer.write(b"\x05\x01\x00") 404 | 405 | # We'll receive the server's response to determine which 406 | # method was selected 407 | writer.flush() 408 | chosen_auth = self._readall(reader, 2) 409 | 410 | if chosen_auth[0:1] != b"\x05": 411 | # Note: string[i:i+1] is used because indexing of a bytestring 412 | # via bytestring[i] yields an integer in Python 3 413 | raise GeneralProxyError( 414 | "SOCKS5 proxy server sent invalid data") 415 | 416 | # Check the chosen authentication method 417 | 418 | if chosen_auth[1:2] == b"\x02": 419 | # Okay, we need to perform a basic username/password 420 | # authentication. 421 | writer.write(b"\x01" + chr(len(username)).encode() 422 | + username 423 | + chr(len(password)).encode() 424 | + password) 425 | writer.flush() 426 | auth_status = self._readall(reader, 2) 427 | if auth_status[0:1] != b"\x01": 428 | # Bad response 429 | raise GeneralProxyError( 430 | "SOCKS5 proxy server sent invalid data") 431 | if auth_status[1:2] != b"\x00": 432 | # Authentication failed 433 | raise SOCKS5AuthError("SOCKS5 authentication failed") 434 | 435 | # Otherwise, authentication succeeded 436 | 437 | # No authentication is required if 0x00 438 | elif chosen_auth[1:2] != b"\x00": 439 | # Reaching here is always bad 440 | if chosen_auth[1:2] == b"\xFF": 441 | raise SOCKS5AuthError( 442 | "All offered SOCKS5 authentication methods were rejected") 443 | else: 444 | raise GeneralProxyError( 445 | "SOCKS5 proxy server sent invalid data") 446 | 447 | # Now we can request the actual connection 448 | writer.write(b"\x05" + cmd + b"\x00") 449 | resolved = self._write_SOCKS5_address(dst, writer) 450 | writer.flush() 451 | 452 | # Get the response 453 | resp = self._readall(reader, 3) 454 | if resp[0:1] != b"\x05": 455 | raise GeneralProxyError( 456 | "SOCKS5 proxy server sent invalid data") 457 | 458 | status = ord(resp[1:2]) 459 | if status != 0x00: 460 | # Connection failed: server returned an error 461 | error = SOCKS5_ERRORS.get(status, "Unknown error") 462 | raise SOCKS5Error("{0:#04x}: {1}".format(status, error)) 463 | 464 | # Get the bound address/port 465 | bnd = self._read_SOCKS5_address(reader) 466 | return (resolved, bnd) 467 | finally: 468 | reader.close() 469 | writer.close() 470 | 471 | def _write_SOCKS5_address(self, addr, file): 472 | host, port = addr 473 | proxy_type, _, _, rdns, username, password = self.proxy 474 | 475 | # If the given destination address is an IP address, we'll 476 | # use the IPv4 address request even if remote resolving was specified. 477 | try: 478 | addr_bytes = socket.inet_aton(host) 479 | file.write(b"\x01" + addr_bytes) 480 | host = socket.inet_ntoa(addr_bytes) 481 | except socket.error: 482 | # Well it's not an IP number, so it's probably a DNS name. 483 | if rdns: 484 | # Resolve remotely 485 | host_bytes = host.encode('idna') 486 | file.write( 487 | b"\x03" + chr(len(host_bytes)).encode() + host_bytes) 488 | else: 489 | # Resolve locally 490 | addr_bytes = socket.inet_aton(socket.gethostbyname(host)) 491 | file.write(b"\x01" + addr_bytes) 492 | host = socket.inet_ntoa(addr_bytes) 493 | 494 | file.write(struct.pack(">H", port)) 495 | return host, port 496 | 497 | def _read_SOCKS5_address(self, file): 498 | atyp = self._readall(file, 1) 499 | if atyp == b"\x01": 500 | addr = socket.inet_ntoa(self._readall(file, 4)) 501 | elif atyp == b"\x03": 502 | length = self._readall(file, 1) 503 | addr = self._readall(file, ord(length)) 504 | else: 505 | raise GeneralProxyError("SOCKS5 proxy server sent invalid data") 506 | 507 | port = struct.unpack(">H", self._readall(file, 2))[0] 508 | return addr, port 509 | 510 | def _negotiate_SOCKS4(self, dest_addr, dest_port): 511 | proxy_type, addr, port, rdns, username, password = self.proxy 512 | 513 | writer = self.makefile("wb") 514 | reader = self.makefile("rb", 0) # buffering=0 renamed in Python 3 515 | try: 516 | # Check if the destination address provided is an IP address 517 | remote_resolve = False 518 | try: 519 | addr_bytes = socket.inet_aton(dest_addr) 520 | except socket.error: 521 | # It's a DNS name. Check where it should be resolved. 522 | if rdns: 523 | addr_bytes = b"\x00\x00\x00\x01" 524 | remote_resolve = True 525 | else: 526 | addr_bytes = socket.inet_aton( 527 | socket.gethostbyname(dest_addr)) 528 | 529 | # Construct the request packet 530 | writer.write(struct.pack(">BBH", 0x04, 0x01, dest_port)) 531 | writer.write(addr_bytes) 532 | 533 | # The username parameter is considered userid for SOCKS4 534 | if username: 535 | writer.write(username) 536 | writer.write(b"\x00") 537 | 538 | # DNS name if remote resolving is required 539 | # NOTE: This is actually an extension to the SOCKS4 protocol 540 | # called SOCKS4A and may not be supported in all cases. 541 | if remote_resolve: 542 | writer.write(dest_addr.encode('idna') + b"\x00") 543 | writer.flush() 544 | 545 | # Get the response from the server 546 | resp = self._readall(reader, 8) 547 | if resp[0:1] != b"\x00": 548 | # Bad data 549 | raise GeneralProxyError( 550 | "SOCKS4 proxy server sent invalid data") 551 | 552 | status = ord(resp[1:2]) 553 | if status != 0x5A: 554 | # Connection failed: server returned an error 555 | error = SOCKS4_ERRORS.get(status, "Unknown error") 556 | raise SOCKS4Error("{0:#04x}: {1}".format(status, error)) 557 | 558 | # Get the bound address/port 559 | self.proxy_sockname = ( 560 | socket.inet_ntoa(resp[4:]), struct.unpack(">H", resp[2:4])[0]) 561 | if remote_resolve: 562 | self.proxy_peername = socket.inet_ntoa(addr_bytes), dest_port 563 | else: 564 | self.proxy_peername = dest_addr, dest_port 565 | finally: 566 | reader.close() 567 | writer.close() 568 | 569 | def _negotiate_HTTP(self, dest_addr, dest_port): 570 | proxy_type, addr, port, rdns, username, password = self.proxy 571 | 572 | # If we need to resolve locally, we do this now 573 | addr = dest_addr if rdns else socket.gethostbyname(dest_addr) 574 | 575 | self.sendall(b"CONNECT " + addr.encode('idna') + b":" + str(dest_port).encode() + 576 | b" HTTP/1.1\r\n" + b"Host: " + dest_addr.encode('idna') + b"\r\n\r\n") 577 | 578 | # We just need the first line to check if the connection was successful 579 | fobj = self.makefile() 580 | status_line = fobj.readline() 581 | fobj.close() 582 | 583 | if not status_line: 584 | raise GeneralProxyError("Connection closed unexpectedly") 585 | 586 | try: 587 | proto, status_code, status_msg = status_line.split(" ", 2) 588 | except ValueError: 589 | raise GeneralProxyError("HTTP proxy server sent invalid response") 590 | 591 | if not proto.startswith("HTTP/"): 592 | raise GeneralProxyError( 593 | "Proxy server does not appear to be an HTTP proxy") 594 | 595 | try: 596 | status_code = int(status_code) 597 | except ValueError: 598 | raise HTTPError( 599 | "HTTP proxy server did not return a valid HTTP status") 600 | 601 | if status_code != 200: 602 | error = "{0}: {1}".format(status_code, status_msg) 603 | if status_code in (400, 403, 405): 604 | # It's likely that the HTTP proxy server does not support the 605 | # CONNECT tunneling method 606 | error += ("\n[*] Note: The HTTP proxy server may not be supported by PySocks" 607 | " (must be a CONNECT tunnel proxy)") 608 | raise HTTPError(error) 609 | 610 | self.proxy_sockname = (b"0.0.0.0", 0) 611 | self.proxy_peername = addr, dest_port 612 | 613 | _proxy_negotiators = { 614 | SOCKS4: _negotiate_SOCKS4, 615 | SOCKS5: _negotiate_SOCKS5, 616 | HTTP: _negotiate_HTTP 617 | } 618 | 619 | def connect(self, dest_pair): 620 | # It actually supports IPv6 without problem! 621 | # if len(dest_pair) != 2 or dest_pair[0].startswith("["): 622 | # Probably IPv6, not supported -- raise an error, and hope 623 | # Happy Eyeballs (RFC6555) makes sure at least the IPv4 624 | # connection works... 625 | # raise socket.error("PySocks doesn't support IPv6") 626 | 627 | dest_addr, dest_port = dest_pair 628 | 629 | if self.type == socket.SOCK_DGRAM: 630 | if not self._proxyconn: 631 | self.bind(("", 0)) 632 | dest_addr = socket.gethostbyname(dest_addr) 633 | 634 | # If the host address is INADDR_ANY or similar, reset the peer 635 | # address so that packets are received from any peer 636 | if dest_addr == "0.0.0.0" and not dest_port: 637 | self.proxy_peername = None 638 | else: 639 | self.proxy_peername = (dest_addr, dest_port) 640 | return 641 | 642 | proxy_type, proxy_addr, proxy_port, rdns, username, password = self.proxy 643 | 644 | # Do a minimal input check first 645 | if (not isinstance(dest_pair, (list, tuple)) 646 | or len(dest_pair) != 2 647 | or not dest_addr 648 | or not isinstance(dest_port, int)): 649 | raise GeneralProxyError( 650 | "Invalid destination-connection (host, port) pair") 651 | 652 | if proxy_type is None: 653 | # Treat like regular socket object 654 | self.proxy_peername = dest_pair 655 | _BaseSocket.connect(self, (dest_addr, dest_port)) 656 | return 657 | 658 | proxy_addr = self._proxy_addr() 659 | 660 | try: 661 | # Initial connection to proxy server 662 | _BaseSocket.connect(self, proxy_addr) 663 | 664 | except socket.error as error: 665 | # Error while connecting to proxy 666 | self.close() 667 | proxy_addr, proxy_port = proxy_addr 668 | proxy_server = "{0}:{1}".format(proxy_addr, proxy_port) 669 | printable_type = PRINTABLE_PROXY_TYPES[proxy_type] 670 | 671 | msg = "Error connecting to {0} proxy {1}".format(printable_type, 672 | proxy_server) 673 | raise ProxyConnectionError(msg, error) 674 | 675 | else: 676 | # Connected to proxy server, now negotiate 677 | try: 678 | # Calls negotiate_{SOCKS4, SOCKS5, HTTP} 679 | negotiate = self._proxy_negotiators[proxy_type] 680 | negotiate(self, dest_addr, dest_port) 681 | except socket.error as error: 682 | # Wrap socket errors 683 | self.close() 684 | raise GeneralProxyError("Socket error", error) 685 | except ProxyError: 686 | # Protocol error while negotiating with proxy 687 | self.close() 688 | raise 689 | 690 | def _proxy_addr(self): 691 | proxy_type, proxy_addr, proxy_port, rdns, username, password = self.proxy 692 | proxy_port = proxy_port or DEFAULT_PORTS.get(proxy_type) 693 | if not proxy_port: 694 | raise GeneralProxyError("Invalid proxy type") 695 | return proxy_addr, proxy_port 696 | 697 | try: 698 | DEVNULL = subprocess.DEVNULL 699 | except AttributeError: 700 | # Python 3.2 701 | DEVNULL = open(os.devnull, 'wb') 702 | 703 | realserverport = 55000 704 | 705 | 706 | CFG = { 707 | "role": "server", 708 | "state": tempfile.gettempdir(), 709 | "local": "127.0.0.1:" + str(realserverport), 710 | "ptexec": ptexec, 711 | "ptname": "obfs4", 712 | "ptargs": "cert=" + CERT_STR + ";iat-mode=" + str(IAT - 1), 713 | "ptserveropt": "", 714 | "ptproxy": "" 715 | } 716 | 717 | PT_PROC = None 718 | PTREADY = threading.Event() 719 | CFG["server"] = SERVER_string 720 | 721 | TRANSPORT_VERSIONS = ('1',) 722 | 723 | startupinfo = None 724 | if os.name == 'nt': 725 | startupinfo = subprocess.STARTUPINFO() 726 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 727 | 728 | logtime = lambda: time.strftime('%Y-%m-%d %H:%M:%S') 729 | 730 | 731 | class PTConnectFailed(Exception): 732 | pass 733 | 734 | 735 | class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer): 736 | allow_reuse_address = True 737 | 738 | 739 | class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler): 740 | 741 | def handle(self): 742 | ptsock = socksocket() 743 | ptsock.set_proxy(*CFG['_ptcli']) 744 | host, port = CFG['server'].rsplit(':', 1) 745 | try: 746 | ptsock.connect((host, int(port))) 747 | except GeneralProxyError as ex: 748 | print(logtime(), ex) 749 | print( 750 | logtime(), 'WARNING: Please check the config and the log of PT.') 751 | run = 1 752 | while run: 753 | rl, wl, xl = select.select([self.request, ptsock], [], [], 300) 754 | if not rl: 755 | break 756 | run = 0 757 | for s in rl: 758 | try: 759 | data = s.recv(1024) 760 | except Exception as ex: 761 | print(logtime(), ex) 762 | continue 763 | if data: 764 | run += 1 765 | else: 766 | continue 767 | if s is self.request: 768 | ptsock.sendall(data) 769 | elif s is ptsock: 770 | self.request.sendall(data) 771 | 772 | 773 | def ptenv(): 774 | env = os.environ.copy() 775 | env['TOR_PT_STATE_LOCATION'] = CFG['state'] 776 | env['TOR_PT_MANAGED_TRANSPORT_VER'] = ','.join(TRANSPORT_VERSIONS) 777 | if CFG["role"] == "client": 778 | env['TOR_PT_CLIENT_TRANSPORTS'] = CFG['ptname'] 779 | if CFG.get('ptproxy'): 780 | env['TOR_PT_PROXY'] = CFG['ptproxy'] 781 | elif CFG["role"] == "server": 782 | env['TOR_PT_SERVER_TRANSPORTS'] = CFG['ptname'] 783 | env['TOR_PT_SERVER_BINDADDR'] = '%s-%s' % ( 784 | CFG['ptname'], CFG['server']) 785 | env['TOR_PT_ORPORT'] = CFG['local'] 786 | env['TOR_PT_EXTENDED_SERVER_PORT'] = '' 787 | if CFG.get('ptserveropt'): 788 | env['TOR_PT_SERVER_TRANSPORT_OPTIONS'] = ';'.join( 789 | '%s:%s' % (CFG['ptname'], kv) for kv in CFG['ptserveropt'].split(';')) 790 | else: 791 | raise ValueError('"role" must be either "server" or "client"') 792 | return env 793 | 794 | 795 | def checkproc(): 796 | global PT_PROC 797 | if PT_PROC is None or PT_PROC.poll() is not None: 798 | PT_PROC = subprocess.Popen(shlex.split( 799 | CFG['ptexec']), stdin=subprocess.PIPE, stdout=subprocess.PIPE, 800 | stderr=DEVNULL, env=ptenv(), startupinfo=startupinfo) 801 | return PT_PROC 802 | 803 | 804 | def parseptline(iterable): 805 | global CFG 806 | for ln in iterable: 807 | ln = ln.decode('utf_8', errors='replace').rstrip('\n') 808 | sp = ln.split(' ', 1) 809 | kw = sp[0] 810 | if kw in ('ENV-ERROR', 'VERSION-ERROR', 'PROXY-ERROR', 811 | 'CMETHOD-ERROR', 'SMETHOD-ERROR'): 812 | raise PTConnectFailed(ln) 813 | elif kw == 'VERSION': 814 | if sp[1] not in TRANSPORT_VERSIONS: 815 | raise PTConnectFailed('PT returned invalid version: ' + sp[1]) 816 | elif kw == 'PROXY': 817 | if sp[1] != 'DONE': 818 | raise PTConnectFailed('PT returned invalid info: ' + ln) 819 | elif kw == 'CMETHOD': 820 | vals = sp[1].split(' ') 821 | if vals[0] == CFG['ptname']: 822 | host, port = vals[2].split(':') 823 | CFG['_ptcli'] = ( 824 | PROXY_TYPES[vals[1].upper()], host, int(port), 825 | True, CFG['ptargs'][:255], CFG['ptargs'][255:] or '\0') 826 | elif kw == 'SMETHOD': 827 | vals = sp[1].split(' ') 828 | if vals[0] == CFG['ptname']: 829 | print('===== Server information =====') 830 | print('"server": "%s",' % vals[1]) 831 | print('"ptname": "%s",' % vals[0]) 832 | for opt in vals[2:]: 833 | if opt.startswith('ARGS:'): 834 | print('"ptargs": "%s",' % opt[5:].replace(',', ';')) 835 | INITIATOR.certs_send = opt[10:80] 836 | print('==============================') 837 | elif kw in ('CMETHODS', 'SMETHODS') and sp[1] == 'DONE': 838 | print(logtime(), 'PT started successfully.') 839 | return 840 | else: 841 | # Some PTs may print extra debugging info 842 | print(logtime(), ln) 843 | 844 | 845 | def runpt(): 846 | global CFG, PTREADY 847 | while CFG['_run']: 848 | print(logtime(), 'Starting PT...') 849 | proc = checkproc() 850 | # If error then die 851 | parseptline(proc.stdout) 852 | PTREADY.set() 853 | # Use this to block 854 | # stdout may be a channel for logging 855 | try: 856 | out = proc.stdout.readline() 857 | while out: 858 | print( 859 | logtime(), out.decode('utf_8', errors='replace').rstrip('\n')) 860 | except BrokenPipeError: 861 | pass 862 | PTREADY.clear() 863 | print(logtime(), 'PT died.') 864 | 865 | 866 | try: 867 | if len(sys.argv) == 1: 868 | pass 869 | elif len(sys.argv) == 2: 870 | if sys.argv[1] in ('-h', '--help'): 871 | print('usage: python3 %s [-c|-s] [config.json]' % __file__) 872 | sys.exit(0) 873 | else: 874 | CFG = json.load(open(sys.argv[1], 'r')) 875 | elif len(sys.argv) == 3: 876 | CFG = json.load(open(sys.argv[2], 'r')) 877 | if sys.argv[1] == '-c': 878 | CFG['role'] = 'client' 879 | elif sys.argv[1] == '-s': 880 | CFG['role'] = 'server' 881 | except Exception as ex: 882 | print(ex) 883 | print('usage: python3 %s [-c|-s] [config.json]' % sys.argv[0]) 884 | sys.exit(1) 885 | 886 | try: 887 | CFG['_run'] = True 888 | if CFG['role'] == 'client': 889 | ptthr = threading.Thread(target=runpt) 890 | ptthr.daemon = True 891 | ptthr.start() 892 | PTREADY.wait() 893 | host, port = CFG['local'].split(':') 894 | server = ThreadedTCPServer( 895 | (host, int(port)), ThreadedTCPRequestHandler) 896 | server.serve_forever() 897 | else: 898 | runpt() 899 | finally: 900 | CFG['_run'] = False 901 | if PT_PROC: 902 | PT_PROC.kill() 903 | -------------------------------------------------------------------------------- /arkcclient/pyotp/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2011 by Mark Percival 2 | 3 | Python port copyright 2011 by Nathan Reynolds 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /arkcclient/pyotp/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function, unicode_literals, division, absolute_import 2 | 3 | import random as _random 4 | 5 | from . import utils 6 | 7 | VERSION = '1.4.2' 8 | 9 | 10 | def random_base32(length=16, random=_random.SystemRandom(), 11 | chars=list('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567')): 12 | return ''.join( 13 | random.choice(chars) 14 | for _ in range(length) 15 | ) -------------------------------------------------------------------------------- /arkcclient/pyotp/otp.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function, unicode_literals, division, absolute_import 2 | 3 | import base64 4 | import hashlib 5 | import hmac 6 | 7 | 8 | class OTP(): 9 | 10 | def __init__(self, s, digits=6, digest=hashlib.sha1): 11 | """ 12 | @param [String] secret in the form of base32 13 | @option options digits [Integer] (6) 14 | Number of integers in the OTP 15 | Google Authenticate only supports 6 currently 16 | @option options digest [Callable] (hashlib.sha1) 17 | Digest used in the HMAC 18 | Google Authenticate only supports 'sha1' currently 19 | @returns [OTP] OTP instantiation 20 | """ 21 | self.digits = digits 22 | self.digest = digest 23 | self.secret = s 24 | 25 | def generate_otp(self, input): 26 | """ 27 | @param [Integer] input the number used seed the HMAC 28 | Usually either the counter, or the computed integer 29 | based on the Unix timestamp 30 | """ 31 | hmac_hash = hmac.new( 32 | self.byte_secret(), 33 | self.int_to_bytestring(input), 34 | self.digest, 35 | ).digest() 36 | 37 | hmac_hash = bytearray(hmac_hash) 38 | offset = hmac_hash[-1] & 0xf 39 | code = ((hmac_hash[offset] & 0x7f) << 24 | 40 | (hmac_hash[offset + 1] & 0xff) << 16 | 41 | (hmac_hash[offset + 2] & 0xff) << 8 | 42 | (hmac_hash[offset + 3] & 0xff)) 43 | str_code = str(code % 10 ** self.digits) 44 | while len(str_code) < self.digits: 45 | str_code = '0' + str_code 46 | 47 | return str_code 48 | 49 | def byte_secret(self): 50 | missing_padding = len(self.secret) % 8 51 | if missing_padding != 0: 52 | self.secret = self.secret + '=' * (8 - missing_padding) 53 | return base64.b64decode(self.secret) # , casefold=True) 54 | 55 | @staticmethod 56 | def int_to_bytestring(i, padding=8): 57 | """ 58 | Turns an integer to the OATH specified 59 | bytestring, which is fed to the HMAC 60 | along with the secret 61 | """ 62 | result = bytearray() 63 | while i != 0: 64 | result.append(i & 0xFF) 65 | i >>= 8 66 | # It's necessary to convert the final result from bytearray to bytes because 67 | # the hmac functions in python 2.6 and 3.3 don't work with bytearray 68 | return bytes(bytearray(reversed(result)).rjust(padding, b'\0')) 69 | -------------------------------------------------------------------------------- /arkcclient/pyotp/totp.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function, unicode_literals, division, absolute_import 2 | 3 | import datetime 4 | import time 5 | import ntplib 6 | 7 | from . import utils 8 | from .otp import OTP 9 | 10 | 11 | class TOTP(OTP): 12 | 13 | systime_offset = None 14 | 15 | def __init__(self, *args, **kwargs): 16 | """ 17 | @option options [Integer] interval (30) the time interval in seconds 18 | for OTP This defaults to 30 which is standard. 19 | """ 20 | self.interval = kwargs.pop('interval', 30) 21 | if self.systime_offset is None: 22 | try: 23 | c = ntplib.NTPClient() 24 | TOTP.systime_offset = int(c.request( 25 | 'pool.ntp.org', version=3).offset) 26 | except Exception: 27 | self.systime_offset = 0 28 | super(TOTP, self).__init__(*args, **kwargs) 29 | 30 | def at(self, for_time, counter_offset=0): 31 | """ 32 | Accepts either a Unix timestamp integer or a Time object. 33 | Time objects will be adjusted to UTC automatically 34 | @param [Time/Integer] time the time to generate an OTP for 35 | @param [Integer] counter_offset an amount of ticks to add to the time counter 36 | """ 37 | if not isinstance(for_time, datetime.datetime): 38 | for_time = datetime.datetime.fromtimestamp(int(for_time)) 39 | return self.generate_otp(self.timecode(for_time) + counter_offset) 40 | 41 | def now(self): 42 | """ 43 | Generate the current time OTP 44 | @return [Integer] the OTP as an integer 45 | """ 46 | return self.generate_otp(self.timecode(datetime.datetime.now())) 47 | 48 | def verify(self, otp, for_time=None, valid_window=0): 49 | """ 50 | Verifies the OTP passed in against the current time OTP 51 | @param [String/Integer] otp the OTP to check against 52 | @param [Integer] valid_window extends the validity to this many counter ticks before and after the current one 53 | """ 54 | if for_time is None: 55 | for_time = datetime.datetime.now() 56 | 57 | if valid_window: 58 | for i in range(-valid_window, valid_window + 1): 59 | if utils.strings_equal(str(otp), str(self.at(for_time, i))): 60 | return True 61 | return False 62 | 63 | return utils.strings_equal(str(otp), str(self.at(for_time))) 64 | 65 | def provisioning_uri(self, name, issuer_name=None): 66 | """ 67 | Returns the provisioning URI for the OTP 68 | This can then be encoded in a QR Code and used 69 | to provision the Google Authenticator app 70 | @param [String] name of the account 71 | @return [String] provisioning uri 72 | """ 73 | return utils.build_uri(self.secret, name, issuer_name=issuer_name) 74 | 75 | def timecode(self, for_time): 76 | i = time.mktime(for_time.timetuple()) + self.systime_offset 77 | return int(i / self.interval) 78 | -------------------------------------------------------------------------------- /arkcclient/pyotp/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function, unicode_literals, division, absolute_import 2 | 3 | try: 4 | from urllib.parse import quote 5 | except ImportError: 6 | from urllib import quote 7 | 8 | 9 | def build_uri(secret, name, initial_count=None, issuer_name=None): 10 | """ 11 | Returns the provisioning URI for the OTP; works for either TOTP or HOTP. 12 | This can then be encoded in a QR Code and used to provision the Google 13 | Authenticator app. 14 | For module-internal use. 15 | See also: 16 | http://code.google.com/p/google-authenticator/wiki/KeyUriFormat 17 | @param [String] the hotp/totp secret used to generate the URI 18 | @param [String] name of the account 19 | @param [Integer] initial_count starting counter value, defaults to None. 20 | If none, the OTP type will be assumed as TOTP. 21 | @param [String] the name of the OTP issuer; this will be the 22 | organization title of the OTP entry in Authenticator 23 | @return [String] provisioning uri 24 | """ 25 | # initial_count may be 0 as a valid param 26 | is_initial_count_present = (initial_count is not None) 27 | 28 | otp_type = 'hotp' if is_initial_count_present else 'totp' 29 | base = 'otpauth://%s/' % otp_type 30 | 31 | if issuer_name: 32 | issuer_name = quote(issuer_name) 33 | base += '%s:' % issuer_name 34 | 35 | uri = '%(base)s%(name)s?secret=%(secret)s' % { 36 | 'name': quote(name, safe='@'), 37 | 'secret': secret, 38 | 'base': base, 39 | } 40 | 41 | if is_initial_count_present: 42 | uri += '&counter=%s' % initial_count 43 | 44 | if issuer_name: 45 | uri += '&issuer=%s' % issuer_name 46 | 47 | return uri 48 | 49 | 50 | def strings_equal(s1, s2): 51 | """ 52 | Timing-attack resistant string comparison. 53 | Normal comparison using == will short-circuit on the first mismatching 54 | character. This avoids that by scanning the whole string, though we 55 | still reveal to a timing attack whether the strings are the same 56 | length. 57 | """ 58 | try: 59 | # Python 3.3+ and 2.7.7+ include a timing-attack-resistant 60 | # comparison function, which is probably more reliable than ours. 61 | # Use it if available. 62 | from hmac import compare_digest 63 | 64 | return compare_digest(s1, s2) 65 | except ImportError: 66 | pass 67 | 68 | if len(s1) != len(s2): 69 | return False 70 | 71 | differences = 0 72 | for c1, c2 in zip(s1, s2): 73 | differences |= ord(c1) ^ ord(c2) 74 | return differences == 0 -------------------------------------------------------------------------------- /arkcclient/server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding:utf-8 3 | 4 | import socket 5 | import asyncore 6 | import logging 7 | import time 8 | import struct 9 | 10 | from Crypto.Hash import SHA256 11 | from Crypto.Signature import PKCS1_v1_5 12 | from Crypto.Cipher import PKCS1_v1_5 as PKCS_Cipher 13 | from Crypto import Random 14 | 15 | from common import AESCipher 16 | from common import get_timestamp, parse_timestamp 17 | from _io import BlockingIOError 18 | 19 | from common import Mode 20 | 21 | # Need to switch to asyncio 22 | 23 | MAX_HANDLE = 100 24 | CLOSECHAR = chr(4) * 5 25 | REAL_SERVERPORT = 55000 26 | SEG_SIZE = 4080 # 4096(total) - 1(type) - 2(id) - 6(index) - 7(splitchar) 27 | SPLIT2 = b'\x00\x01\x02\x03\x04' 28 | 29 | 30 | class ServerControl(asyncore.dispatcher): 31 | 32 | '''listen at the required port''' 33 | 34 | def __init__(self, serverip, serverport, ctl, pt=False, backlog=5): 35 | self.ctl = ctl 36 | asyncore.dispatcher.__init__(self) 37 | if ctl.ipv6 == "": 38 | self.create_socket(socket.AF_INET, socket.SOCK_STREAM) 39 | else: 40 | self.create_socket(socket.AF_INET6, socket.SOCK_STREAM) 41 | self.set_reuse_addr() 42 | 43 | if pt: 44 | serverip = "127.0.0.1" 45 | serverport = REAL_SERVERPORT 46 | self.bind((serverip, serverport)) 47 | self.listen(backlog) 48 | 49 | def handle_accept(self): 50 | conn, addr = self.accept() 51 | logging.info('Serv_recv_Accept from %s' % str(addr)) 52 | if Mode == "VPS": 53 | ServerReceiver(conn, self.ctl) 54 | else: 55 | ServerReceiver_GAE(conn, self.ctl) 56 | 57 | def getrecv(self): 58 | return self.ctl.offerconn() 59 | 60 | 61 | class ServerReceiver(asyncore.dispatcher): 62 | 63 | '''represent each connection with arkc-server''' 64 | 65 | def __init__(self, conn, ctl): 66 | self.ctl = ctl 67 | asyncore.dispatcher.__init__(self, conn) 68 | self.read = b'' 69 | self.from_remote_buffer_raw = b'' 70 | self.cipher = None 71 | self.preferred = False 72 | self.closing = False 73 | self.i = -1 74 | self.split = bytes( 75 | chr(27) + 76 | chr(28) + 77 | "%X" % struct.unpack('B', self.ctl.main_pw[-2:-1])[0] + 78 | "%X" % struct.unpack('B', self.ctl.main_pw[-3:-2])[0] + 79 | chr(31), 80 | "UTF-8" 81 | ) 82 | self.no_data_count = 0 83 | self.read = b'' 84 | self.latency = 10000 85 | time.sleep(0.05) # async 86 | self.begin_auth() 87 | 88 | def ping_recv(self, msg): 89 | """Parse ping (without flag) and send back when necessary.""" 90 | seq = int(msg[0]) 91 | if seq == 0: 92 | raw_packet = "1" + "1" + msg[1:] + get_timestamp() 93 | to_write = self.cipher.encrypt(raw_packet) + self.split 94 | self.send(to_write) 95 | else: 96 | time1 = parse_timestamp(msg[1:]) 97 | self.latency = int(time.time() * 1000) - time1 98 | logging.debug("latency: %dms" % self.latency) 99 | 100 | def handle_connect(self): 101 | pass 102 | 103 | def handle_read(self): 104 | """Handle received data.""" 105 | 106 | b_close = bytes(CLOSECHAR, "ASCII") 107 | 108 | if self.cipher is None: 109 | self.begin_auth() 110 | else: 111 | read_count = 0 112 | self.from_remote_buffer_raw += self.recv(8192) 113 | bytessplit = self.from_remote_buffer_raw.split(self.split) 114 | for Index in range(len(bytessplit)): 115 | if Index < len(bytessplit) - 1: 116 | b_dec = self.cipher.decrypt(bytessplit[Index]) 117 | # flag is 0 for normal data packet, 1 for ping packet 118 | flag = int(b_dec[:1].decode("UTF-8")) 119 | if flag == 0: 120 | try: 121 | cli_id = b_dec[1:3].decode("UTF-8") 122 | seq = int(b_dec[3:9].decode("UTF-8")) 123 | b_data = b_dec[9:] 124 | except Exception: 125 | logging.warning("decode error") 126 | cli_id = None 127 | if cli_id == "00": 128 | if b_data == b_close: 129 | 130 | logging.debug("closing connection") 131 | self.closing = True 132 | self.ctl.closeconn(self) 133 | self.close() 134 | # elif seq == 50: 135 | # id_list = b_data.decode("UTF-8").split(',') 136 | # self.ctl.server_check(id_list) 137 | # TODO: Experimental function 138 | else: 139 | if cli_id in self.ctl.clientreceivers_dict: 140 | if seq == 30: 141 | self.update_max_idx(cli_id, 142 | int(b_data.decode('utf-8'))) 143 | elif b_data != b_close: 144 | self.ctl.server_recv_max_idx[ 145 | self.i][cli_id] = seq 146 | self.ctl.clientreceivers_dict[ 147 | cli_id].from_remote_buffer_dict[seq] = b_data 148 | self.ctl.clientreceivers_dict[ 149 | cli_id].retransmission_check() 150 | else: 151 | for _ in self.ctl.server_recv_max_idx: 152 | if _ is not None: 153 | _.pop(cli_id, None) 154 | self.ctl.clientreceivers_dict[ 155 | cli_id].close() 156 | read_count += len(b_data) 157 | # else: 158 | # self.encrypt_and_send(cli_id, CLOSECHAR) 159 | else: 160 | # strip off type (always 1) 161 | self.ping_recv(b_dec[1:].decode("UTF-8")) 162 | 163 | else: 164 | self.from_remote_buffer_raw = bytessplit[Index] 165 | if read_count > 0: 166 | logging.debug('%04i from server' % read_count) 167 | 168 | def begin_auth(self): 169 | # Deal with the beginning authentication 170 | try: 171 | self.read += self.recv(2048) 172 | if self.split in self.read: 173 | authdata = self.read.split(b'\r\n') 174 | signature = authdata[0] 175 | # TODO: fix an error in int(signature,16) 176 | try: 177 | verify = self.ctl.serverpub.verify( 178 | self.ctl.main_pw, (int(signature, 36), None)) 179 | except ValueError: 180 | logging.debug("ValueError captured at server.py line 165") 181 | verify = False 182 | if not verify: 183 | logging.warning("Authentication failed, socket closing") 184 | self.close() 185 | else: 186 | try: 187 | self.cipher = AESCipher( 188 | self.ctl.clientpri.decrypt(authdata[1]), self.ctl.main_pw) 189 | self.full = False 190 | idchar = authdata[2].decode('utf-8') 191 | self.i = int(idchar) 192 | self.ctl.newconn(self) 193 | logging.debug( 194 | "Authentication succeed, connection established") 195 | self.send( 196 | self.cipher.encrypt(b"2AUTHENTICATED" + authdata[2] + 197 | repr( 198 | self.ctl.server_recv_max_idx[self.i]).encode() 199 | ) 200 | + self.split 201 | ) 202 | self.send_legacy( 203 | eval(authdata[3].rstrip(self.split).decode('utf-8'))) 204 | self.read = None 205 | except ValueError: 206 | # TODO: figure out why 207 | logging.warning( 208 | "Authentication failed, socket closing") 209 | self.handle_close() 210 | else: 211 | if len(self.read) == 0: 212 | self.no_data_count += 1 213 | except BlockingIOError: 214 | pass 215 | 216 | except socket.error: 217 | logging.info("empty recv error") 218 | 219 | except Exception as err: 220 | raise err 221 | logging.error( 222 | "Authentication failed, due to error, socket closing") 223 | self.close() 224 | 225 | def send_legacy(self, idx_list): 226 | buf = self.ctl.server_send_buf_pool[self.i] 227 | for cli_id in idx_list: 228 | try: 229 | queue = buf[cli_id] 230 | while len(queue) and queue[0][0] <= idx_list[cli_id]: 231 | queue.popleft() 232 | if len(queue): 233 | for idx, data in queue: 234 | self.encrypt_and_send(cli_id, data, idx) 235 | except Exception: 236 | pass 237 | 238 | def writable(self): 239 | if self.preferred: 240 | for cli_id in self.ctl.clientreceivers_dict: 241 | if self.ctl.clientreceivers_dict[cli_id] is None: 242 | logging.warning( 243 | "Client receiver %s NoneType error" % cli_id) 244 | del self.ctl.clientreceivers_dict[cli_id] 245 | else: 246 | if len(self.ctl.clientreceivers_dict[cli_id].to_remote_buffer) > 0: 247 | return True 248 | else: 249 | return False 250 | 251 | def handle_write(self): 252 | # Called when writable 253 | if self.cipher is not None: 254 | if self.ctl.ready == self: 255 | written = 0 256 | for cli_id in self.ctl.clientreceivers_dict: 257 | if self.ctl.clientreceivers_dict[cli_id].to_remote_buffer: 258 | self.id_write(cli_id) 259 | written += 1 260 | if written >= self.ctl.swapcount: 261 | break 262 | self.ctl.refreshconn() 263 | else: 264 | self.handle_read() 265 | 266 | def handle_close(self): 267 | self.closing = True 268 | self.ctl.closeconn(self) 269 | self.close() 270 | 271 | def encrypt_and_send(self, cli_id, buf=None, b_idx=None): 272 | """Encrypt and send data, and return the length sent. 273 | 274 | When `buf` is not specified, it is automatically read from the 275 | `to_remote_buffer` corresponding to `cli_id`. 276 | """ 277 | b_id = bytes(cli_id, "UTF-8") 278 | if buf is None: 279 | b_idx = bytes( 280 | str(self.ctl.clientreceivers_dict[cli_id].to_remote_buffer_index), 'utf-8') 281 | buf = self.ctl.clientreceivers_dict[ 282 | cli_id].to_remote_buffer[:SEG_SIZE] 283 | self.ctl.clientreceivers_dict[cli_id].next_to_remote_buffer() 284 | self.ctl.clientreceivers_dict[cli_id].to_remote_buffer = self.ctl.clientreceivers_dict[ 285 | cli_id].to_remote_buffer[len(buf):] 286 | if cli_id not in self.ctl.server_send_buf_pool[self.i]: 287 | self.ctl.server_send_buf_pool[self.i][cli_id] = [] 288 | else: 289 | buf = bytes(buf, "utf-8") 290 | tosend = self.cipher.encrypt( 291 | b"0" + b_id + b_idx + buf) + self.split 292 | while len(tosend) > 0: 293 | sent = self.send(tosend) 294 | tosend = tosend[sent:] 295 | self.ctl.server_send_buf_pool[self.i][cli_id].append((buf, b_idx)) 296 | return len(buf) 297 | 298 | def id_write(self, cli_id, lastcontents=None, seq=None): 299 | # Write to a certain cli_id. Lastcontents is used for CLOSECHAR 300 | sent = 0 301 | try: 302 | if lastcontents is not None and seq is not None: 303 | sent += self.encrypt_and_send(cli_id, 304 | lastcontents, 305 | bytes(seq, 'utf-8')) 306 | sent = self.encrypt_and_send(cli_id) 307 | logging.debug('%04i to server' % sent) 308 | 309 | except KeyError: 310 | pass 311 | 312 | def update_max_idx(self, cli_id, seq): 313 | try: 314 | queue = self.ctl.server_send_buf_pool[self.i][cli_id] 315 | while len(queue) and queue[0][0] <= seq: 316 | queue.popleft() 317 | except Exception: 318 | pass 319 | 320 | 321 | class ServerReceiver_GAE(ServerReceiver): 322 | 323 | '''represent each connection with arkc-server''' 324 | 325 | def __init__(self, conn, ctl): 326 | ServerReceiver.__init__(self, conn, ctl) 327 | self.split = bytes( 328 | chr(27) + 329 | chr(28) + 330 | chr(27) + 331 | chr(28) + 332 | #"%X" % struct.unpack('B', self.ctl.main_pw[-2:-1])[0] + 333 | #"%X" % struct.unpack('B', self.ctl.main_pw[-3:-2])[0] + 334 | chr(31), 335 | "UTF-8" 336 | ) 337 | 338 | def handle_read(self): 339 | """Handle received data.""" 340 | 341 | b_close = bytes(CLOSECHAR, "ASCII") 342 | 343 | if self.cipher is None: 344 | self.begin_auth() 345 | else: 346 | read_count = 0 347 | self.from_remote_buffer_raw += self.recv(8192) 348 | # print(self.from_remote_buffer_raw) 349 | bytessplit = self.from_remote_buffer_raw.split(self.split) 350 | #print("CALL READ %d" % len(bytessplit)) 351 | #print("PASSWORD IS " + repr(self.cipher.password)) 352 | for Index in range(len(bytessplit)): 353 | 354 | if Index < len(bytessplit) - 1: 355 | try: 356 | b_dec = self.cipher.decrypt(bytessplit[Index]) 357 | except ValueError: 358 | raw = bytessplit[Index] 359 | print(raw) 360 | print(len(raw)) 361 | raise 362 | if len(b_dec) == 0: 363 | continue 364 | # flag is 0 for normal data packet, 1 for ping packet 365 | flag = int(b_dec[:1].decode("UTF-8")) 366 | if flag == 0: 367 | try: 368 | cli_id = b_dec[1:3].decode("UTF-8") 369 | seq = int(b_dec[3:9].decode("UTF-8")) 370 | b_data = b_dec[9:] 371 | except Exception: 372 | logging.warning("decode error") 373 | cli_id = None 374 | if cli_id == "00": 375 | if b_data == b_close: 376 | 377 | logging.debug("closing connection") 378 | self.closing = True 379 | self.ctl.closeconn(self) 380 | self.close() 381 | # elif seq == 50: 382 | # id_list = b_data.decode("UTF-8").split(',') 383 | # self.ctl.server_check(id_list) 384 | # TODO: Experimental function 385 | else: 386 | if cli_id in self.ctl.clientreceivers_dict: 387 | if seq == 30: 388 | self.update_max_idx(cli_id, 389 | int(b_data.decode('utf-8'))) 390 | elif b_data != b_close: 391 | self.ctl.server_recv_max_idx[ 392 | self.i][cli_id] = seq 393 | self.ctl.clientreceivers_dict[ 394 | cli_id].from_remote_buffer_dict[seq] = b_data 395 | # self.ctl.clientreceivers_dict[ 396 | # cli_id].retransmission_check() 397 | else: 398 | for _ in self.ctl.server_recv_max_idx: 399 | if _ is not None: 400 | _.pop(cli_id, None) 401 | self.ctl.clientreceivers_dict[ 402 | cli_id].close() 403 | read_count += len(b_data) 404 | # else: 405 | # self.encrypt_and_send(cli_id, CLOSECHAR) 406 | else: 407 | # strip off type (always 1) 408 | self.ping_recv(b_dec[1:].decode("UTF-8")) 409 | 410 | else: 411 | self.from_remote_buffer_raw = bytessplit[Index] 412 | if read_count > 0: 413 | logging.debug('%04i from server' % read_count) 414 | 415 | def begin_auth(self): 416 | # Deal with the beginning authentication 417 | try: 418 | 419 | self.read += self.recv(2048) 420 | print("CALL AUTH") 421 | if b'\r\n' in self.read: 422 | authdata = self.read.split(b'\r\n') 423 | #print (authdata) 424 | # print(self.ctl.main_pw) 425 | signature = authdata[0] 426 | # TODO: fix an error in int(signature,16) 427 | try: 428 | signer = PKCS1_v1_5.new(self.ctl.serverpub) 429 | h = SHA256.new(self.ctl.main_pw) 430 | verify = signer.verify(h, signature) 431 | except ValueError: 432 | logging.debug("ValueError captured at server.py line 165") 433 | verify = False 434 | if not verify: 435 | logging.warning( 436 | "Authentication failed, socket closing, case 1") 437 | self.close() 438 | else: 439 | try: 440 | auth_cipher = PKCS_Cipher.new(self.ctl.clientpri) 441 | sentinel = Random.new().read(32) 442 | message = auth_cipher.decrypt(authdata[1], sentinel) 443 | if len(message) != 16: 444 | raise ValueError 445 | self.cipher = AESCipher( 446 | message, self.ctl.main_pw) 447 | self.full = False 448 | idchar = authdata[2].decode('utf-8') 449 | self.i = int(idchar) 450 | self.ctl.newconn(self) 451 | logging.debug( 452 | "Authentication succeed, connection established") 453 | self.send( 454 | self.cipher.encrypt(b"2AUTHENTICATED" + authdata[2] # + 455 | # repr( 456 | # self.ctl.server_recv_max_idx[self.i]).encode() 457 | ) 458 | #+ self.split 459 | ) 460 | # self.send_legacy( 461 | # eval(authdata[3].rstrip(self.split).decode('utf-8'))) 462 | self.read = None 463 | except IOError: 464 | # TODO: figure out why 465 | logging.warning( 466 | "Authentication failed, socket closing, , case 2") 467 | self.handle_close() 468 | else: 469 | if len(self.read) == 0: 470 | self.no_data_count += 1 471 | except BlockingIOError: 472 | pass 473 | 474 | except socket.error: 475 | logging.info("empty recv error") 476 | 477 | except Exception as err: 478 | raise err 479 | logging.error( 480 | "Authentication failed, due to error, socket closing") 481 | self.close() 482 | 483 | # WRITE PARTS NEED TO BE OPTIMIZED 484 | 485 | def writable(self): 486 | if self.preferred: 487 | for cli_id in self.ctl.clientreceivers_dict: 488 | if self.ctl.clientreceivers_dict[cli_id] is None: 489 | logging.warning( 490 | "Client receiver %s NoneType error" % cli_id) 491 | del self.ctl.clientreceivers_dict[cli_id] 492 | else: 493 | if SPLIT2 in self.ctl.clientreceivers_dict[cli_id].to_remote_buffer: 494 | return True 495 | return False 496 | else: 497 | return False 498 | 499 | def handle_write(self): 500 | # Called when writable 501 | if self.cipher is not None: 502 | if self.ctl.ready == self: 503 | written = 0 504 | for cli_id in self.ctl.clientreceivers_dict: 505 | if SPLIT2 in self.ctl.clientreceivers_dict[cli_id].to_remote_buffer: 506 | self.id_write(cli_id) 507 | written += 1 508 | if written >= self.ctl.swapcount: 509 | break 510 | self.ctl.refreshconn() 511 | else: 512 | self.handle_read() 513 | 514 | def encrypt_and_send(self, cli_id, buf=None, b_idx=None): 515 | """Encrypt and send data, and return the length sent. 516 | 517 | When `buf` is not specified, it is automatically read from the 518 | `to_remote_buffer` corresponding to `cli_id`. 519 | """ 520 | b_id = bytes(cli_id, "UTF-8") 521 | if buf is None: 522 | b_idx = bytes( 523 | str(self.ctl.clientreceivers_dict[cli_id].to_remote_buffer_index), 'utf-8') 524 | splitted = self.ctl.clientreceivers_dict[ 525 | cli_id].to_remote_buffer.split(SPLIT2) # [SEG SIZE] 526 | if len(splitted) <= 1: 527 | return 0 528 | buf = splitted[0] 529 | print(repr(buf)) 530 | self.ctl.clientreceivers_dict[cli_id].next_to_remote_buffer() 531 | self.ctl.clientreceivers_dict[ 532 | cli_id].to_remote_buffer = b'\x00\x01\x02\x03\x04'.join(splitted[1:]) 533 | if cli_id not in self.ctl.server_send_buf_pool[self.i]: 534 | self.ctl.server_send_buf_pool[self.i][cli_id] = [] 535 | else: 536 | buf = bytes(buf, "utf-8") 537 | tosend = self.cipher.encrypt( 538 | b"0" + b_id + b_idx + buf) + self.split 539 | while len(tosend) > 0: 540 | sent = self.send(tosend) 541 | tosend = tosend[sent:] 542 | self.ctl.server_send_buf_pool[self.i][cli_id].append((buf, b_idx)) 543 | return len(buf) 544 | 545 | def id_write(self, cli_id, lastcontents=None, seq=None): 546 | # Write to a certain cli_id. Lastcontents is used for CLOSECHAR 547 | sent = 0 548 | try: 549 | if lastcontents is not None and seq is not None: 550 | sent += self.encrypt_and_send(cli_id, 551 | lastcontents, 552 | bytes(seq, 'utf-8')) 553 | sent = self.encrypt_and_send(cli_id) 554 | logging.debug('%04i to server' % sent) 555 | 556 | except KeyError: 557 | pass 558 | 559 | def update_max_idx(self, cli_id, seq): 560 | try: 561 | queue = self.ctl.server_send_buf_pool[self.i][cli_id] 562 | while len(queue) and queue[0][0] <= seq: 563 | queue.popleft() 564 | except Exception: 565 | pass 566 | -------------------------------------------------------------------------------- /goagent_local/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *~ 3 | *.pyc 4 | *.pac 5 | *.key 6 | *.crt 7 | *.pid 8 | *.user.ini 9 | -------------------------------------------------------------------------------- /goagent_local/GeoIP.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projectarkc/arkc-client/957c78a1e80a17e1c121b09af717cbd8d551f2b4/goagent_local/GeoIP.dat -------------------------------------------------------------------------------- /goagent_local/dnsproxy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding:utf-8 3 | 4 | __version__ = '1.0' 5 | 6 | import sys 7 | import os 8 | import sysconfig 9 | 10 | sys.path += [os.path.abspath(os.path.join(__file__, '../packages.egg/%s' % x)) for x in ('noarch', sysconfig.get_platform().split('-')[0])] 11 | 12 | import gevent 13 | import gevent.server 14 | import gevent.timeout 15 | import gevent.monkey 16 | gevent.monkey.patch_all(subprocess=True) 17 | 18 | import re 19 | import time 20 | import logging 21 | import heapq 22 | import socket 23 | import select 24 | import struct 25 | import errno 26 | import thread 27 | import dnslib 28 | import Queue 29 | import pygeoip 30 | 31 | 32 | is_local_addr = re.compile(r'(?i)(?:[0-9a-f:]+0:5efe:)?(?:127(?:\.\d+){3}|10(?:\.\d+){3}|192\.168(?:\.\d+){2}|172\.(?:1[6-9]|2\d|3[01])(?:\.\d+){2})').match 33 | 34 | 35 | def get_dnsserver_list(): 36 | if os.name == 'nt': 37 | import ctypes, ctypes.wintypes, struct, socket 38 | DNS_CONFIG_DNS_SERVER_LIST = 6 39 | buf = ctypes.create_string_buffer(2048) 40 | ctypes.windll.dnsapi.DnsQueryConfig(DNS_CONFIG_DNS_SERVER_LIST, 0, None, None, ctypes.byref(buf), ctypes.byref(ctypes.wintypes.DWORD(len(buf)))) 41 | ipcount = struct.unpack('I', buf[0:4])[0] 42 | iplist = [socket.inet_ntoa(buf[i:i+4]) for i in xrange(4, ipcount*4+4, 4)] 43 | return iplist 44 | elif os.path.isfile('/etc/resolv.conf'): 45 | with open('/etc/resolv.conf', 'rb') as fp: 46 | return re.findall(r'(?m)^nameserver\s+(\S+)', fp.read()) 47 | else: 48 | logging.warning("get_dnsserver_list failed: unsupport platform '%s-%s'", sys.platform, os.name) 49 | return [] 50 | 51 | 52 | def parse_hostport(host, default_port=80): 53 | m = re.match(r'(.+)[#](\d+)$', host) 54 | if m: 55 | return m.group(1).strip('[]'), int(m.group(2)) 56 | else: 57 | return host.strip('[]'), default_port 58 | 59 | 60 | class ExpireCache(object): 61 | """ A dictionary-like object, supporting expire semantics.""" 62 | def __init__(self, max_size=1024): 63 | self.__maxsize = max_size 64 | self.__values = {} 65 | self.__expire_times = {} 66 | self.__expire_heap = [] 67 | 68 | def size(self): 69 | return len(self.__values) 70 | 71 | def clear(self): 72 | self.__values.clear() 73 | self.__expire_times.clear() 74 | del self.__expire_heap[:] 75 | 76 | def exists(self, key): 77 | return key in self.__values 78 | 79 | def set(self, key, value, expire): 80 | try: 81 | et = self.__expire_times[key] 82 | pos = self.__expire_heap.index((et, key)) 83 | del self.__expire_heap[pos] 84 | if pos < len(self.__expire_heap): 85 | heapq._siftup(self.__expire_heap, pos) 86 | except KeyError: 87 | pass 88 | et = int(time.time() + expire) 89 | self.__expire_times[key] = et 90 | heapq.heappush(self.__expire_heap, (et, key)) 91 | self.__values[key] = value 92 | self.cleanup() 93 | 94 | def get(self, key): 95 | et = self.__expire_times[key] 96 | if et < time.time(): 97 | self.cleanup() 98 | raise KeyError(key) 99 | return self.__values[key] 100 | 101 | def delete(self, key): 102 | et = self.__expire_times.pop(key) 103 | pos = self.__expire_heap.index((et, key)) 104 | del self.__expire_heap[pos] 105 | if pos < len(self.__expire_heap): 106 | heapq._siftup(self.__expire_heap, pos) 107 | del self.__values[key] 108 | 109 | def cleanup(self): 110 | t = int(time.time()) 111 | eh = self.__expire_heap 112 | ets = self.__expire_times 113 | v = self.__values 114 | size = self.__maxsize 115 | heappop = heapq.heappop 116 | #Delete expired, ticky 117 | while eh and eh[0][0] <= t or len(v) > size: 118 | _, key = heappop(eh) 119 | del v[key], ets[key] 120 | 121 | 122 | def dnslib_resolve_over_udp(query, dnsservers, timeout, **kwargs): 123 | """ 124 | http://gfwrev.blogspot.com/2009/11/gfwdns.html 125 | http://zh.wikipedia.org/wiki/%E5%9F%9F%E5%90%8D%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%BC%93%E5%AD%98%E6%B1%A1%E6%9F%93 126 | http://support.microsoft.com/kb/241352 127 | https://gist.github.com/klzgrad/f124065c0616022b65e5 128 | """ 129 | if not isinstance(query, (basestring, dnslib.DNSRecord)): 130 | raise TypeError('query argument requires string/DNSRecord') 131 | blacklist = kwargs.get('blacklist', ()) 132 | turstservers = kwargs.get('turstservers', ()) 133 | dns_v4_servers = [x for x in dnsservers if ':' not in x] 134 | dns_v6_servers = [x for x in dnsservers if ':' in x] 135 | sock_v4 = sock_v6 = None 136 | socks = [] 137 | if dns_v4_servers: 138 | sock_v4 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 139 | socks.append(sock_v4) 140 | if dns_v6_servers: 141 | sock_v6 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) 142 | socks.append(sock_v6) 143 | timeout_at = time.time() + timeout 144 | try: 145 | for _ in xrange(4): 146 | try: 147 | for dnsserver in dns_v4_servers: 148 | if isinstance(query, basestring): 149 | if dnsserver in ('8.8.8.8', '8.8.4.4'): 150 | query = '.'.join(x[:-1] + x[-1].upper() for x in query.split('.')).title() 151 | query = dnslib.DNSRecord(q=dnslib.DNSQuestion(query)) 152 | query_data = query.pack() 153 | if query.q.qtype == 1 and dnsserver in ('8.8.8.8', '8.8.4.4'): 154 | query_data = query_data[:-5] + '\xc0\x04' + query_data[-4:] 155 | sock_v4.sendto(query_data, parse_hostport(dnsserver, 53)) 156 | for dnsserver in dns_v6_servers: 157 | if isinstance(query, basestring): 158 | query = dnslib.DNSRecord(q=dnslib.DNSQuestion(query, qtype=dnslib.QTYPE.AAAA)) 159 | query_data = query.pack() 160 | sock_v6.sendto(query_data, parse_hostport(dnsserver, 53)) 161 | while time.time() < timeout_at: 162 | ins, _, _ = select.select(socks, [], [], 0.1) 163 | for sock in ins: 164 | reply_data, reply_address = sock.recvfrom(512) 165 | reply_server = reply_address[0] 166 | record = dnslib.DNSRecord.parse(reply_data) 167 | iplist = [str(x.rdata) for x in record.rr if x.rtype in (1, 28, 255)] 168 | if any(x in blacklist for x in iplist): 169 | logging.warning('query=%r dnsservers=%r record bad iplist=%r', query, dnsservers, iplist) 170 | elif record.header.rcode and not iplist and reply_server in turstservers: 171 | logging.info('query=%r trust reply_server=%r record rcode=%s', query, reply_server, record.header.rcode) 172 | return record 173 | elif iplist: 174 | logging.debug('query=%r reply_server=%r record iplist=%s', query, reply_server, iplist) 175 | return record 176 | else: 177 | logging.debug('query=%r reply_server=%r record null iplist=%s', query, reply_server, iplist) 178 | continue 179 | except socket.error as e: 180 | logging.warning('handle dns query=%s socket: %r', query, e) 181 | raise socket.gaierror(11004, 'getaddrinfo %r from %r failed' % (query, dnsservers)) 182 | finally: 183 | for sock in socks: 184 | sock.close() 185 | 186 | 187 | def dnslib_resolve_over_tcp(query, dnsservers, timeout, **kwargs): 188 | """dns query over tcp""" 189 | if not isinstance(query, (basestring, dnslib.DNSRecord)): 190 | raise TypeError('query argument requires string/DNSRecord') 191 | blacklist = kwargs.get('blacklist', ()) 192 | def do_resolve(query, dnsserver, timeout, queobj): 193 | if isinstance(query, basestring): 194 | qtype = dnslib.QTYPE.AAAA if ':' in dnsserver else dnslib.QTYPE.A 195 | query = dnslib.DNSRecord(q=dnslib.DNSQuestion(query, qtype=qtype)) 196 | query_data = query.pack() 197 | sock_family = socket.AF_INET6 if ':' in dnsserver else socket.AF_INET 198 | sock = socket.socket(sock_family) 199 | rfile = None 200 | try: 201 | sock.settimeout(timeout or None) 202 | sock.connect(parse_hostport(dnsserver, 53)) 203 | sock.send(struct.pack('>h', len(query_data)) + query_data) 204 | rfile = sock.makefile('r', 1024) 205 | reply_data_length = rfile.read(2) 206 | if len(reply_data_length) < 2: 207 | raise socket.gaierror(11004, 'getaddrinfo %r from %r failed' % (query, dnsserver)) 208 | reply_data = rfile.read(struct.unpack('>h', reply_data_length)[0]) 209 | record = dnslib.DNSRecord.parse(reply_data) 210 | iplist = [str(x.rdata) for x in record.rr if x.rtype in (1, 28, 255)] 211 | if any(x in blacklist for x in iplist): 212 | logging.debug('query=%r dnsserver=%r record bad iplist=%r', query, dnsserver, iplist) 213 | raise socket.gaierror(11004, 'getaddrinfo %r from %r failed' % (query, dnsserver)) 214 | else: 215 | logging.debug('query=%r dnsserver=%r record iplist=%s', query, dnsserver, iplist) 216 | queobj.put(record) 217 | except socket.error as e: 218 | logging.debug('query=%r dnsserver=%r failed %r', query, dnsserver, e) 219 | queobj.put(e) 220 | finally: 221 | if rfile: 222 | rfile.close() 223 | sock.close() 224 | queobj = Queue.Queue() 225 | for dnsserver in dnsservers: 226 | thread.start_new_thread(do_resolve, (query, dnsserver, timeout, queobj)) 227 | for i in range(len(dnsservers)): 228 | try: 229 | result = queobj.get(timeout) 230 | except Queue.Empty: 231 | raise socket.gaierror(11004, 'getaddrinfo %r from %r failed' % (query, dnsservers)) 232 | if result and not isinstance(result, Exception): 233 | return result 234 | elif i == len(dnsservers) - 1: 235 | logging.warning('dnslib_resolve_over_tcp %r with %s return %r', query, dnsservers, result) 236 | raise socket.gaierror(11004, 'getaddrinfo %r from %r failed' % (query, dnsservers)) 237 | 238 | 239 | class DNSServer(gevent.server.DatagramServer): 240 | """DNS Proxy based on gevent/dnslib""" 241 | 242 | def __init__(self, *args, **kwargs): 243 | dns_blacklist = kwargs.pop('dns_blacklist') 244 | dns_servers = kwargs.pop('dns_servers') 245 | dns_tcpover = kwargs.pop('dns_tcpover', []) 246 | dns_timeout = kwargs.pop('dns_timeout', 2) 247 | super(self.__class__, self).__init__(*args, **kwargs) 248 | self.dns_servers = list(dns_servers) 249 | self.dns_tcpover = tuple(dns_tcpover) 250 | self.dns_intranet_servers = [x for x in self.dns_servers if is_local_addr(x)] 251 | self.dns_blacklist = set(dns_blacklist) 252 | self.dns_timeout = int(dns_timeout) 253 | self.dns_cache = ExpireCache(max_size=65536) 254 | self.dns_trust_servers = set(['8.8.8.8', '8.8.4.4', '2001:4860:4860::8888', '2001:4860:4860::8844']) 255 | for dirname in ('.', '/usr/share/GeoIP/', '/usr/local/share/GeoIP/'): 256 | filename = os.path.join(dirname, 'GeoIP.dat') 257 | if os.path.isfile(filename): 258 | geoip = pygeoip.GeoIP(filename) 259 | for dnsserver in self.dns_servers: 260 | if ':' not in dnsserver and geoip.country_name_by_addr(parse_hostport(dnsserver, 53)[0]) not in ('China',): 261 | self.dns_trust_servers.add(dnsserver) 262 | break 263 | 264 | def do_read(self): 265 | try: 266 | return gevent.server.DatagramServer.do_read(self) 267 | except socket.error as e: 268 | if e[0] not in (errno.ECONNABORTED, errno.ECONNRESET, errno.EPIPE): 269 | raise 270 | 271 | def get_reply_record(self, data): 272 | request = dnslib.DNSRecord.parse(data) 273 | qname = str(request.q.qname).lower() 274 | qtype = request.q.qtype 275 | dnsservers = self.dns_servers 276 | if qname.endswith('.in-addr.arpa'): 277 | ipaddr = '.'.join(reversed(qname[:-13].split('.'))) 278 | record = dnslib.DNSRecord(header=dnslib.DNSHeader(id=request.header.id, qr=1,aa=1,ra=1), a=dnslib.RR(qname, rdata=dnslib.A(ipaddr))) 279 | return record 280 | if 'USERDNSDOMAIN' in os.environ: 281 | user_dnsdomain = '.' + os.environ['USERDNSDOMAIN'].lower() 282 | if qname.endswith(user_dnsdomain): 283 | qname = qname[:-len(user_dnsdomain)] 284 | if '.' not in qname: 285 | if not self.dns_intranet_servers: 286 | logging.warning('qname=%r is a plain hostname, need intranet dns server!!!', qname) 287 | return dnslib.DNSRecord(header=dnslib.DNSHeader(id=request.header.id, rcode=3)) 288 | qname += user_dnsdomain 289 | dnsservers = self.dns_intranet_servers 290 | try: 291 | return self.dns_cache.get((qname, qtype)) 292 | except KeyError: 293 | pass 294 | try: 295 | dns_resolve = dnslib_resolve_over_tcp if qname.endswith(self.dns_tcpover) else dnslib_resolve_over_udp 296 | kwargs = {'blacklist': self.dns_blacklist, 'turstservers': self.dns_trust_servers} 297 | record = dns_resolve(request, dnsservers, self.dns_timeout, **kwargs) 298 | ttl = max(x.ttl for x in record.rr) if record.rr else 600 299 | self.dns_cache.set((qname, qtype), record, ttl * 2) 300 | return record 301 | except socket.gaierror as e: 302 | logging.warning('resolve %r failed: %r', qname, e) 303 | return dnslib.DNSRecord(header=dnslib.DNSHeader(id=request.header.id, rcode=3)) 304 | 305 | def handle(self, data, address): 306 | logging.debug('receive from %r data=%r', address, data) 307 | record = self.get_reply_record(data) 308 | return self.sendto(data[:2] + record.pack()[2:], address) 309 | 310 | 311 | def test(): 312 | logging.basicConfig(level=logging.INFO, format='%(levelname)s - %(asctime)s %(message)s', datefmt='[%b %d %H:%M:%S]') 313 | dns_servers = '8.8.8.8|8.8.4.4|168.95.1.1|168.95.192.1|223.5.5.5|223.6.6.6|114.114.114.114|114.114.115.115'.split('|') 314 | dns_blacklist = '1.1.1.1|255.255.255.255|74.125.127.102|74.125.155.102|74.125.39.102|74.125.39.113|209.85.229.138|4.36.66.178|8.7.198.45|37.61.54.158|46.82.174.68|59.24.3.173|64.33.88.161|64.33.99.47|64.66.163.251|65.104.202.252|65.160.219.113|66.45.252.237|72.14.205.104|72.14.205.99|78.16.49.15|93.46.8.89|128.121.126.139|159.106.121.75|169.132.13.103|192.67.198.6|202.106.1.2|202.181.7.85|203.161.230.171|203.98.7.65|207.12.88.98|208.56.31.43|209.145.54.50|209.220.30.174|209.36.73.33|211.94.66.147|213.169.251.35|216.221.188.182|216.234.179.13|243.185.187.3|243.185.187.39|23.89.5.60|37.208.111.120|49.2.123.56|54.76.135.1|77.4.7.92|118.5.49.6|188.5.4.96|189.163.17.5|197.4.4.12|249.129.46.48|253.157.14.165|183.207.229.|183.207.232.'.split('|') 315 | dns_tcpover = ['.youtube.com', '.googlevideo.com'] 316 | logging.info('serving at port 53...') 317 | DNSServer(('', 53), dns_servers=dns_servers, dns_blacklist=dns_blacklist, dns_tcpover=dns_tcpover).serve_forever() 318 | 319 | 320 | if __name__ == '__main__': 321 | test() 322 | -------------------------------------------------------------------------------- /goagent_local/goagent-gtk.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # coding:utf-8 3 | # Contributor: 4 | # Phus Lu 5 | 6 | __version__ = '1.6' 7 | 8 | GOAGENT_LOGO_DATA = """\ 9 | iVBORw0KGgoAAAANSUhEUgAAADcAAAA3CAYAAACo29JGAAAABHNCSVQICAgIfAhkiAAADVdJREFU 10 | aIHtmnuMXPV1xz/ndx8z4/UT8bKTEscOBa1FAt2kSYvAECxegTZRu6uGJDQJld0ofShNpJZSOlnU 11 | KFWhSUorWlMpoaIq6q4gECANwcQ4SkkpXqVJ8DQpsBGPUgUoxsa787j39/v2j3tndmcfthmbVK1y 12 | tL+9sztz7/19f9/zO+d7zh34qf3fNHtdrlqvu5H/2hA19687zPX3AVBbt0FT65/3jI+H12Uux8Xq 13 | dTc8OpGCBluwrfV4ZPvO5HhO6fgwNzoRMTnmASS4+PqJNzfb+WmJ403tzK+vpNFJyvMhb7hYlmM6 14 | 1OrkL9TS6PlOlj1z8prq03eNf/A5ALbWY/aM58djWscOrl53jI+HS+p3bJxp2TVZnm9DbDbnTsLF 15 | 5Q1UDKk4R8VrKYDPEOGlyKJHh5Jw84M3XvN1qDs4djc9RnCjEUz6i/5o8oqZjC8KTjIXQQj41qEM 16 | 4TEcBBXgoHecAxphxC5KMEQcsj//1l/u+NTxYDAa/NS6g1u0rT5x2qFWeDB4naA885Hy78SEx4O0 17 | 0SJXQYoMi/sH84cDBbzPgnzHKkPnb3zHpTwz8andbN0a8/TTAzPoBj1xeHRLDKjV0XuxeLWZQpK4 18 | L178ng3v+taNV10yVIkvM3hVZl4oFHQFei6KetcycBgpUMlbM+2O17WXX3vrz7JnT069PvAcBz6x 19 | sW2/igvY6RbFWNDsyWvX3DT+6Yd5y2///erdf3rVNyLHw3FSjSSyYgccZhdIMnAEmYsr6cFmuAJg 20 | uEE86BwHPnF41zprAFnQSiwYjrjTbK5jz3j+5B5eBTBsrUKQ2VHsbWHlj8yMyKI3AjSmN+hIpy5n 21 | A4MrczAKISGGEKzycqvz+W3XT+xYtXrtj//7pZd+s5WF84JvB4elXTeUNC+CzsNswgyTEIaBHMDw 22 | pl3WmPoJg2vUCrc0cy3MdcA3FdwvHDzUeeTVmRcOAafifbsgLVgRHi0ys1hBMqyfTRlYCVgQyvcb 23 | 0+sGZm7gPTcyMgJA8GG1JdUUF6+RS7C4spKkeqpwWFKpuKRSsbiSWlKt4FwsFQz1UkHXrPtLhoHr 24 | vT0y6BSPjrmR7TuT5vp11nh4X2APARo29ewjDuoujuwxF9prIXQkpUYIeCQLkXkvmYQUAZ0QwhlE 25 | 8Wb53JtZfxrqkiYTAjkTwK9f8Uh02+/Tnw62DEe8cFKwC/ccNg8efqMXsqobvxnYP+rAOGz95O1/ 26 | mEWVz/jmwVlgRVep9NSLgaR2nNYqKe2b93xhx++WWnUpmtEEkY3hl7vt8syVelH1uruAs3856/jL 27 | zw1sMlMKQAgGJhEihEFAyAjdyRbLIUl2EGefUMfn+ZuV5whVrbdcmj9fJJMZ5LnzqkP7isvvqyTN 28 | lXgLIIfksfN+hEtvt3MeeljCmbFkol8GXN0xOeYvuf4rW8719tc+6DyLUmQeCOUqdxdzQVJ285jo 29 | slHeW8qQzzHMocBcTNH84CkzkKnMo9m7qYYqHSDq6dML8a2P6tHz62bfvGE5BheDKxm7rP7V4Vfy 30 | /KFgdkrIOy1om0nWN/EuoKVe9z4ztwCSIjOL5qLlYua6sMzMOAjIXmE2nIA3D6Hcp6XiWcm4pi76 31 | oY089I/aTWwX0rcHF0fLyX2qT+xLX243bwu4U0JntmlQNagA6bEMg4hutLTFMqzAWLilJLEaMNJi 32 | qFJINKUYVQIxWcjJm5/Rdz80xAWLmesDVxSL42HP49Pvs6T2Dt9ptc2imiQtnMRrtu6u6LqideXY 33 | gnRXFrvmHAVz5T6QhZ40DQgjphVEVZtpP3eJGdLekb5itw/c1A+fF0A7hDGZC+ZccVkLdszgFsbl 34 | +QFlzqPVPYY8tEkwUEwww8J8EVeIAiMQI9S5rADQL2X63XLPeL59594VwYdzlLWdoagAdgTRO4j1 35 | knZ5EKUGlRniQLP6Hc57wynO5auRPPMVTamNkEV0ZODfKcmxY7k9NzoRAUw/98Jmw70xBB8kou6d 36 | X/vMj3a44mhmoEwWW+RbBxrfvvSf2LD5ymhV5JBlyFzf5QU4HFkQ0ia+v21jsVXnMPVejKzb5ACy 37 | OLyFJE0Msjk1vwRrc4GwF+5Kt+pIaklql8cjD0I7iFYus0olSWuV8Al9+uSZTMnv0c6EWYQt0GuF 38 | Axsio+KG6OSbAZga6SmfXiqYWn+vAWQ+bMTSIoLIycwvjczKbW49VG1hqUVpGseukPdlBaBe/6Ss 39 | COani3n9lSg0D9XczCcfuPG3bsv2XnJLsqJzJjPKMBLCgjU2KLNJIJU4pE0ATE/1PtUDN9zYQgNI 40 | zE7tFHnUiv22lFmhPAwTCsIUJ7WKCzOdxFpT7Q5PO2e5ASZJ1lUuoaxkukm9F0qanVCd2vvAjjt0 41 | qr2aTV36V0m18zGaIcNZXNxiUViFUPp2MMO0AYDREcFUPziGi0NklnZLr+X2W5cxGQFFmAtWi9p/ 42 | sfuxsz/LN8768dILsti6okQAX2KIj9z2npyLr02q2dnM+gxICCoZ0hLMUbiFAZasAqCxBHO1DUV9 43 | lkmzxRnLF9BFUWleOGLHzMqaXbXrhvffr32/tC3nV24OXmeC7/p+Wd4ECMJCWbBZIBPWISgjqqGw 44 | IVlBGmc5zPgMLIEAZuqWQUuuDlixSqFI4s0lmJvaVbS+Ffx/ylKZOSlIZv0d5G5JAgqRi9zKtP2R 45 | B2/4wP2HvveBLzGUfTi2AJkvllVl1u1TYfNUSZcVn0PHixnaiAhnSemJYrkOtoAIERBOQr7wmJG5 46 | XNcDN7Juv6aA2OKncnmTgnOGLXRNM5lQ2+JqpWqHJnf9yQfvmv3uVV9YeXL+YV7uzAIRBFeU0/0V 47 | Qr8mVX/BauaASvfzy4dp5t4JMpDhzVA8XbA0Qpe5XiqYWr+9aIebf0p5e9awRFjoW/mSOQkXW86L 48 | rdNu0r+c/6ZqNfs4B7IWUMWoIEuQJYVrLTvi3hAxMtfX/TuSCTALGAlNmqTucQCmp3rlz1xiHLcA 49 | 8PEt7R8B0xYnVigDp7nyBooepCWRsgONz583xdqTftUNRQ4vw8yhhf2D12hHI4YEhSviSZ1DNHjb 50 | riclbH7p0y+/tu9MxsbGfBzZYxalASNQMt9dTmcIF0nSfjCPxacLc8W2lgh2pKkduxXSzTB5Uieo 51 | fNXMApMsL5y7VknSSQuZQzJM1rcLZJjJQpcg7531enVH1aE8RutGJQlImFWbNLkDgNHD1XO37sig 52 | 7h7648sesDx7xCXVVCE0zbne/lav+91XuzAn7ZeYC8wVrq9Fdi45jKKxYC1WRBGK77Wf2/Xv2k28 53 | sN2wmLnRLWZmYc2q6sdQeMWSSq3Qf9aW1IZSNxrtfgQlyIXMdfWWGaAM1KK8zuLB3JDaSw5oE9Sk 54 | Qo2WDhBXr5MwXlwchha3GSbHPKOj0deuu+x751/75cvzKN7pk8pZVpTPKPgKUYK1OyfOITMWS4jy 55 | LZXAFaAaJySWFKd0UwP9KaJ3DIu0Zy+9hAAZz5JFH7W3P/jE0fdQACYnPaMT0Tc/+75vb6/f+64f 56 | +OzSLPNvrSTRCqEgn8VDVTsAFB46n7mFtBXM5aQupmNfo+32QkjBchSsJ4b6lHGgG7rm/Lr0uBBy 57 | yJ/g4Kr77ML7Xiq7X0u295Zv7U2OeUYnolvHr5wF7ipHz7q6EOaDW+gZZZQVOZUoxrt7bPjLfyPt 58 | TMy2Z8vdug42Pj//LGOaIFoOWPfuR7SR7XuT5vrpvs/+weZ/Ta6++qYZNUb/ljXRb3CwXSTxsIQr 59 | FbnPEDPgWkVpICEVzJkyghwuJAS1qLqVtJJbSKev5wVSTqazaFLDjczs8On+qNrpU7e+fdEqf2gv 60 | 4Woo3RKWZq77t1nZ6xyCfKjQARR7xwE1B50AWS7EEGmU0CK1LY2OdhNsCwM9Ph74QQi10YXnat7v 61 | 0rpkd6tWBYRHBAI5ZhDcLDN6gI7tI4kMrEMgYIViYtXIwJlzcHDD03M9q+KwdCoo/lk6kDkgwgwi 62 | HJF7GVv5bjv765fiTnknefQPRAzhceAKr5pu/i+Aa2wqwcQ51n2cVtbWS+0E6zt2qMWO3O62t935 63 | qPaOrOGttzex2ucI1sEpoNACYEtj4CkODq5ZMmf2b3gVgtmKxsNhw1S3wAyCyNYAMDJ1qFAX+VoM 64 | yORQKJ7dNkcGFuIDU65uV+PZa9Yx88oUqW2k6ZuYaotqt8X1nDAFHB6lv8MMd1O1DYTm51jNBRz0 65 | T2A/8/Oc83cHykkOBPCYZG5XGejJX/tFyO8h4URmshzhsRARFBWyZhG4OZCRGbmeIfgTOSFewcH8 66 | Raz6Xjvr/keO9PztdQVHMT1nRtD3338mlezPCP7KIrR7aOcCsrK3t6ASD0ZQSmywKoL9GUTubnzt 67 | Ojvnnsbhnrv9xMAVAEcjs8mikv/B2Ln4fAz8RYjTWWFpsbMXgMsDNH0T8STO7SIkd9rZX/ln4IhP 68 | TI/Wjlv1JdUdNKwHcvfumFNuPQPLziD3m3DRGyBPkWsS2s/h4qcI+g/uPOcJK79rWbTv0bEy9rqZ 69 | tDXWvuH0qD8PponhVLu3Dv6dmGXsda2bpdGIxr6lvzy3D2CLt7HJY3a/n9r/N/sfrBt2air9qXQA 70 | AAAASUVORK5CYII=""" 71 | 72 | import sys 73 | import os 74 | import re 75 | import thread 76 | import base64 77 | import platform 78 | 79 | try: 80 | import pygtk 81 | pygtk.require('2.0') 82 | import gtk 83 | # gtk.gdk.threads_init() 84 | except Exception: 85 | sys.exit(os.system(u'gdialog --title "GoAgent GTK" --msgbox "\u8bf7\u5b89\u88c5 python-gtk2" 15 60'.encode(sys.getfilesystemencoding() or sys.getdefaultencoding(), 'replace'))) 86 | try: 87 | import pynotify 88 | pynotify.init('GoAgent Notify') 89 | except ImportError: 90 | pynotify = None 91 | try: 92 | import appindicator 93 | except ImportError: 94 | appindicator = None 95 | try: 96 | import vte 97 | except ImportError: 98 | sys.exit(gtk.MessageDialog(None, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, u'请安装 python-vte').run()) 99 | 100 | 101 | def spawn_later(seconds, target, *args, **kwargs): 102 | def wrap(*args, **kwargs): 103 | import time 104 | time.sleep(seconds) 105 | return target(*args, **kwargs) 106 | return thread.start_new_thread(wrap, args, kwargs) 107 | 108 | 109 | def drop_desktop(): 110 | filename = os.path.abspath(__file__) 111 | dirname = os.path.dirname(filename) 112 | DESKTOP_FILE = '''\ 113 | #!/usr/bin/env xdg-open 114 | [Desktop Entry] 115 | Type=Application 116 | Name=GoAgent GTK 117 | Comment=GoAgent GTK Launcher 118 | Categories=Network;Proxy; 119 | Exec=/usr/bin/env python "%s" 120 | Icon=%s/goagent-logo.png 121 | Terminal=false 122 | StartupNotify=true 123 | ''' % (filename, dirname) 124 | for dirname in map(os.path.expanduser, ['~/Desktop', u'~/桌面']): 125 | if os.path.isdir(dirname): 126 | filename = os.path.join(dirname, 'goagent-gtk.desktop') 127 | with open(filename, 'w') as fp: 128 | fp.write(DESKTOP_FILE) 129 | os.chmod(filename, 0755) 130 | 131 | 132 | def should_visible(): 133 | import ConfigParser 134 | ConfigParser.RawConfigParser.OPTCRE = re.compile(r'(?P