├── .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