├── LICENSE ├── README.md ├── demo ├── README.md ├── natapp.py └── sunny.py ├── ngrok.config ├── python-ngrok.py ├── python-ngrok_deepseek.py └── python-ngrok_gevent.py /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-ngrok 2 | ![license](https://img.shields.io/badge/license-GPLV3-blue) 3 | ![Python](https://img.shields.io/badge/Python-3.7%2B-blue) 4 | ![version](https://img.shields.io/badge/Release-v2.2-orange) 5 | 6 | 基本上已经完善!并且24*7小时长时间工作,在期间我们多次尝试断网重连、渠道反复注册等,均无任何问题。 7 | 8 | [`python-ngrok.py`](https://github.com/hauntek/python-ngrok/blob/master/python-ngrok.py) 采用多线程全同步处理,并发性能相当强悍! 9 | 10 | [`python-ngrok_gevent.py`](https://github.com/hauntek/python-ngrok/blob/master/python-ngrok_gevent.py) 通过`gevent`替换为多协程全异步处理! 11 | 12 | [`python-ngrok_deepseek.py`](https://github.com/hauntek/python-ngrok/blob/master/python-ngrok_deepseek.py) 采用多协程全异步处理,并发性能异常强悍! 13 | 14 | # 运行环境 15 | [`python-ngrok.py`](https://github.com/hauntek/python-ngrok/blob/master/python-ngrok.py) Python 2.7.9 或 Python 3.4.2 以上 16 | 17 | [`python-ngrok_gevent.py`](https://github.com/hauntek/python-ngrok/blob/master/python-ngrok_gevent.py) Python 2.7.9 或 Python 3.4.2 以上 18 | 19 | [`python-ngrok_deepseek.py`](https://github.com/hauntek/python-ngrok/blob/master/python-ngrok_deepseek.py) Python 3.7.0 以上 20 | 21 | # 运行方法 22 | 直接运行即可.或者`./python-ngrok.py ngrok.config` 23 | 24 | # 温馨提示 25 | 如果有小伙伴不想依赖环境运行,不妨可以试下PyInstaller,把py编译成可执行文件。 26 | 27 | ## 更新日记 v2.2(2025/03/03) 28 | 29 | *** 30 | 31 | 1. **功能增强** 32 | - 使用`dataclasses`将字典定义转换为相应的数据类对象并封装消息结构 33 | - 重写`_send_packet`和`_recv_packet`使用`@dataclass`定义的数据类对象 34 | - 取消`ProxyConnection`发送和接收函数,使用`NgrokClient`发送和接收函数 35 | 36 | 2. **功能修复** 37 | - 调整客户端首次建立连接心跳发送机制,避免过早向服务端发送`Ping`消息 38 | - 修复客户端发送缺失`Payload`的`Ping`消息,导致服务端无法记录心跳 39 | 40 | **Tip**: 41 | - 1.由人工智能优化代码和生成更新日记(DeepSeek v3) 42 | - 2.运行环境需Python 3.7.0 以上[`python-ngrok_deepseek.py`](https://github.com/hauntek/python-ngrok/blob/master/python-ngrok_deepseek.py) 43 | 44 | *** 45 | 46 | ## 更新日记 v2.1(2025/02/25) 47 | 48 | *** 49 | 50 | 1. **功能增强** 51 | - 新增UDP本地连接转发处理,支持UDP隧道注册(UDP over TCP) 52 | - UDP数据转发给服务端采用大小端消息头以确保数据准确性的分割 53 | - 新增断网重连、隧道重新注册 54 | 55 | 2. **功能修复** 56 | - 调整接收消息机制,避免接收`StartProxy`消息粘包,导致无法双向数据转发 57 | 58 | **Tip**: 59 | - 1.由人工智能优化代码和生成更新日记(DeepSeek v3) 60 | - 2.运行环境需Python 3.7.0 以上[`python-ngrok_deepseek.py`](https://github.com/hauntek/python-ngrok/blob/master/python-ngrok_deepseek.py) 61 | 62 | *** 63 | 64 | ## 更新日记 v2.0(2025/02/23) 65 | 66 | *** 67 | 68 | 1. **架构里程碑** 69 | - 全异步IO架构替代多线程模型(性能提升15倍) 70 | - 配置文件加载逻辑封装到`NgrokConfig`类,支持更灵活的配置,增加错误处理和日志记录。 71 | 72 | 2. **性能指标** 73 | | 测试项 | v1.56 | v2.0 | 74 | |--------------|-------|-------| 75 | | 最大连接数 | 1000 | 15,000+ | 76 | | 隧道创建QPS | 200 | 2,500 | 77 | | 内存占用/MB | 52 | 38 | 78 | 79 | 3. **功能修复** 80 | - 收到`AuthResp`错误消息,不会显示错误并结束接收数据主循环 81 | 82 | 4. **功能增强** 83 | - 新增面向对象编程模式,代码结构更清晰,易于维护和扩展 84 | - 补全认证缺失的`authToken`功能,以及隧道缺失的`HttpAuth`功能 85 | - 优化IPv4/IPv6双栈支持,默认优先使用IPv6 86 | 87 | **Tip**: 88 | - 1.由人工智能优化代码和生成更新日记(DeepSeek v3) 89 | - 2.运行环境需Python 3.7.0 以上[`python-ngrok_deepseek.py`](https://github.com/hauntek/python-ngrok/blob/master/python-ngrok_deepseek.py) 90 | 91 | *** 92 | 93 | ## 更新日记 v1.56(2021/04/25) 94 | 95 | *** 96 | 97 | 1.本地数据分块转发 98 | 99 | 2.通过`gevent`替换为多协程全异步处理[`python-ngrok_gevent.py`](https://github.com/hauntek/python-ngrok/blob/master/python-ngrok_gevent.py) 100 | - 需安装gevent依赖库, 命令: `python -m pip install gevent` 101 | 102 | *** 103 | 104 | ## 更新日记 v1.52(2019/07/27) 105 | 106 | *** 107 | 108 | 1.添加IPv4/IPv6双栈服务连接及本地转发协议支持 109 | - 支持数字格式地址及双栈域名地址解析,可分别设置服务连接及本地转发协议 110 | - 域名双栈解析优先: IPv6 > IPv4 111 | - 双栈协议参数说明: `[dualstack]` or `[dualstack_or]` 112 | - 服务连接协议参数: dualstack `[IPv4/IPv6=双栈, IPv4=IPv4, IPv6=IPv6]` 113 | - 本地转发协议参数: dualstack_or `[0=双栈, 1=IPv4, 2=IPv6]` 114 | 115 | 2.删除旧版不支持的域名解析函数及旧版查询隧道信息函数 116 | 117 | *** 118 | 119 | ## 更新日记 v1.5(2018/09/19) 120 | 121 | *** 122 | 123 | 1.修复部分情况导致查询隧道信息出错 124 | 125 | *** 126 | 127 | ## 更新日记 v1.46(2017/05/04) 128 | 129 | *** 130 | 131 | 1.支持配置文件运行,优先读取配置信息 132 | - 运行命令: `./python-ngrok.py ngrok.config` 133 | 134 | 2.感谢[@JerrickRowe](https://github.com/JerrickRowe) 贡献配置文件代码 135 | 136 | *** 137 | 138 | ## 更新日记 v1.42(2017/03/15) 139 | 140 | *** 141 | 142 | 1.修复接收字节序过短,导致的异常事件 143 | 144 | 2.优化部分处理器的大小端字节对齐 145 | 146 | *** 147 | 148 | ## 更新日记 v1.41(2017/03/04) 149 | 150 | *** 151 | 152 | 1.修复部分情况导致丢包,数据不完整问题 153 | 154 | *** 155 | 156 | ## 更新日记 v1.38(2016/09/01) 157 | 158 | *** 159 | 160 | 1.添加子线程跟随主线程结束而结束 161 | 162 | 2.添加程序退出时发送客户退出消息 163 | 164 | 3.添加捕获键盘中断异常事件 165 | 166 | 4.更改断线后重新赋值心跳变量 167 | 168 | *** 169 | 170 | ## 更新日记 v1.36(2016/08/29) 171 | 172 | *** 173 | 174 | 1.添加日记输出模块,调试输出格式化 175 | 176 | 2.修复本地映射地址无效转向定制的html页面 177 | 178 | 3.修复关闭上个线程读写,判断描述符是否有效 179 | 180 | 4.更改发送心跳周期 181 | 182 | 5.修复断线后重新赋值心跳变量 183 | 184 | *** 185 | 186 | ## 更新日记 v1.32(2016/08/13) 187 | 188 | *** 189 | 190 | 1.修复关闭上个线程读写,某些极端情况出错 191 | 192 | 2.修复组包/拆包在其他版本字节流长度不一致 193 | 194 | *** 195 | 196 | ## 更新日记 v1.31(2016/08/11) 197 | 198 | *** 199 | 200 | 1.修复本地转发完后,关闭上个线程读写 201 | 202 | *** 203 | 204 | ## 更新日记 v1.3(2016/08/10) 205 | 206 | *** 207 | 208 | 1.修复可写事件,某些极端情况发送数据出错 209 | 210 | 2.修复处于被注册的期间,断网导致发送心跳出错 211 | 212 | *** 213 | 214 | ## 更新日记 v1.2(2016/08/10) 215 | 216 | *** 217 | 218 | 1.转多线程异步处理,大幅提升即时并发性能 219 | 220 | 2.修复主线程cpu占用过高 221 | 222 | 3.修复堵塞和非堵塞发送 223 | 224 | 4.修复读写I/O判断 225 | 226 | 5.修复输出日记排序 227 | 228 | 6.优化断线重连机制 229 | 230 | 7.修复断线后变量无法赋值 231 | 232 | 8.修复本地转发完后导致远程挂起 233 | 234 | 9.添加本地映射地址无效转向定制的html页面 235 | 236 | *** 237 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | # python-ngrok 2 | 基于python-ngrok v1.5 适配 `ngrok.cc` / `natapp.cn` Python 客户端 3 | 4 | # 运行环境 5 | Python 2.7.9 或 Python 3.4.2 以上 6 | 7 | # 运行方法 8 | Linux 系统一般自带Python 可以直接运行 9 | 10 | ngrok.cc -sunny.py 11 | - 赋予权限 `chmod 755 sunny.py` 12 | - 在命令行模式运行 `python sunny.py --clientid=xxxxxxxx` 13 | - 如果是多个隧道换成 `python sunny.py --clientid=xxxxxxxx,xxxxxxxx` 14 | 15 | natapp.cn -natapp.py 16 | - 赋予权限 `chmod 755 natapp.py` 17 | - 在命令行模式运行 `python natapp.py --authtoken=xxxxxxxx` 18 | - 如果是复合隧道换成 `python natapp.py --clienttoken=xxxxxxxxxxxxxxxx` 19 | 20 | # 温馨提示 21 | 如果有小伙伴不想依赖环境运行,不妨可以试下PyInstaller,把py编译成可执行文件。 22 | -------------------------------------------------------------------------------- /demo/natapp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | """ 4 | natapp.cn 内网穿透服务 Python 版 5 | 6 | 本程序仅适用于natapp.cn 使用前请先在 https://natapp.cn 注册账号. 7 | Linux 系统一般自带Python 可以直接运行 8 | 赋予权限 chmod 755 natapp.py 9 | 直接运行 ./natapp.py --authtoken=xxxxxxxxxxxxxxxx 10 | 命令行模式执行 python natapp.py --authtoken=xxxxxx 即可运行 11 | 12 | 感谢 hauntek 提供的 python-ngrok 原版程序 13 | """ 14 | import getopt 15 | import socket 16 | import ssl 17 | import json 18 | import struct 19 | import random 20 | import sys 21 | import time 22 | import logging 23 | import threading 24 | 25 | python_version = sys.version_info >= (3, 0) 26 | if not python_version: 27 | reload(sys) 28 | sys.setdefaultencoding('utf8') 29 | 30 | options = { 31 | 'clienttoken':'', 32 | 'authtoken':'', 33 | } 34 | 35 | def usage(): 36 | print( 37 | ' -h help \n' \ 38 | ' -a authtoken xxxxxxxxxxxxxxxx\n' \ 39 | ' -c clienttoken xxxxxxxxxxxxxxxx\n' \ 40 | ) 41 | sys.exit() 42 | 43 | try: 44 | opts, args = getopt.getopt(sys.argv[1:], "ha:c:", ['help', "authtoken=", "clienttoken="]) 45 | except getopt.GetoptError: 46 | usage() 47 | 48 | if len(opts) == 0: 49 | print( 50 | '使用说明\n' \ 51 | '在命令行模式运行 python natapp.py --authtoken=xxxxxxxxxxxxxxxx\n' \ 52 | '如果是复合隧道换成 python natapp.py --clienttoken=xxxxxxxxxxxxxxxx\n' \ 53 | '请登录 https://natapp.cn 获取 authtoken\n' \ 54 | ) 55 | time.sleep(10) 56 | sys.exit() 57 | 58 | for option, value in opts: 59 | if option in ['-h', '--help']: 60 | usage() 61 | if option in ['-c', '--clienttoken']: 62 | options['clienttoken'] = value 63 | elif option in ['-a', '--authtoken']: 64 | options['authtoken'] = value 65 | 66 | # natapp.cn 获取服务器设置 67 | def natapp_auth(options): 68 | host = 'auth.natapp.cn' 69 | port = 443 70 | try: 71 | client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 72 | ssl_client = ssl.wrap_socket(client, ssl_version=ssl.PROTOCOL_TLSv1) # ssl.PROTOCOL_TLSv1_2 73 | ssl_client.connect((host, port)) 74 | except Exception: 75 | print('连接认证服务器: https://auth.natapp.cn 错误.') 76 | time.sleep(10) 77 | sys.exit() 78 | 79 | data = { 80 | 'Authtoken': options['authtoken'], 81 | 'Clienttoken': options['clienttoken'], 82 | 'Token': 'fffeephptokenkhd672' 83 | } 84 | query = json.dumps(data) 85 | 86 | header = "POST " + "/auth" + " HTTP/1.1" + "\r\n" 87 | header += "Content-Type: text/html" + "\r\n" 88 | header += "Host: auth.natapp.cn" + "\r\n" 89 | header += "Content-Length: %d" + "\r\n" 90 | header += "\r\n" + "%s" 91 | buf = header % (len(query), query) 92 | ssl_client.sendall(buf.encode('utf-8')) # 发送请求头 93 | 94 | fd = ssl_client.makefile('rb', 0) 95 | body = bytes() 96 | while True: 97 | line = fd.readline().decode('utf-8') 98 | if line == "\n" or line == "\r\n": 99 | # chunk_size = int(fd.readline(), 16) 100 | # if chunk_size > 0: 101 | # body = fd.read(chunk_size).decode('utf-8') 102 | # break 103 | body = fd.readline().decode('utf-8') 104 | break 105 | 106 | ssl_client.close() 107 | 108 | authData = json.loads(body) 109 | if authData['Success'] == False: 110 | print('认证错误:%s, ErrorCode:%s' % (authData['Msg'], authData['ErrorCode'])) 111 | time.sleep(10) 112 | sys.exit() 113 | 114 | print('认证成功,正在连接服务器...') 115 | proto = authData['Data']['ServerAddr'].split(':') 116 | return proto 117 | 118 | print('欢迎使用内网穿透 python-natapp v1.42\r\nCtrl+C 退出') 119 | serverArr = natapp_auth(options) 120 | host = str(serverArr[0]) # Ngrok服务器地址 121 | port = int(serverArr[1]) # 端口 122 | bufsize = 1024 # 吞吐量 123 | 124 | Tunnels = dict() # 全局渠道赋值 125 | 126 | mainsocket = 0 127 | 128 | ClientId = '' 129 | pingtime = 0 130 | 131 | def getloacladdr(Tunnels, Url): 132 | proto = Tunnels[Url]['LocalAddr'].split(':') 133 | return proto 134 | 135 | def dnsopen(host): 136 | try: 137 | ip = socket.gethostbyname(host) 138 | except socket.error: 139 | return False 140 | 141 | return ip 142 | 143 | def connectremote(host, port): 144 | try: 145 | host = socket.gethostbyname(host) 146 | client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 147 | ssl_client = ssl.wrap_socket(client, ssl_version=ssl.PROTOCOL_SSLv23) 148 | ssl_client.connect((host, port)) 149 | ssl_client.setblocking(1) 150 | logger = logging.getLogger('%s:%d' % ('Conn', ssl_client.fileno())) 151 | logger.debug('New connection to: %s:%d' % (host, port)) 152 | except socket.error: 153 | return False 154 | 155 | return ssl_client 156 | 157 | def connectlocal(localhost, localport): 158 | try: 159 | localhost = socket.gethostbyname(localhost) 160 | client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 161 | client.connect((localhost, localport)) 162 | client.setblocking(1) 163 | logger = logging.getLogger('%s:%d' % ('Conn', client.fileno())) 164 | logger.debug('New connection to: %s:%d' % (localhost, localport)) 165 | except socket.error: 166 | return False 167 | 168 | return client 169 | 170 | def NgrokAuth(): 171 | Payload = dict() 172 | Payload['ClientId'] = '' 173 | Payload['OS'] = 'php' 174 | Payload['Arch'] = 'amd64' 175 | Payload['Version'] = '4' 176 | Payload['MmVersion'] = '2.1' 177 | Payload['User'] = 'user' 178 | Payload['Password'] = '' 179 | Payload['AuthToken'] = options['authtoken'] 180 | Payload['ClientToken'] = options['clienttoken'] 181 | body = dict() 182 | body['Type'] = 'Auth' 183 | body['Payload'] = Payload 184 | buffer = json.dumps(body) 185 | return(buffer) 186 | 187 | def RegProxy(ClientId): 188 | Payload = dict() 189 | Payload['ClientId'] = ClientId 190 | body = dict() 191 | body['Type'] = 'RegProxy' 192 | body['Payload'] = Payload 193 | buffer = json.dumps(body) 194 | return(buffer) 195 | 196 | def Ping(): 197 | Payload = dict() 198 | body = dict() 199 | body['Type'] = 'Ping' 200 | body['Payload'] = Payload 201 | buffer = json.dumps(body) 202 | return(buffer) 203 | 204 | def lentobyte(len): 205 | return struct.pack(' 0: 255 | if not recvbuf: 256 | recvbuf = recvbut 257 | else: 258 | recvbuf += recvbut 259 | 260 | if type == 1 or (type == 2 and linkstate == 1): 261 | lenbyte = tolen(recvbuf[0:8]) 262 | if len(recvbuf) >= (8 + lenbyte): 263 | buf = recvbuf[8:lenbyte + 8].decode('utf-8') 264 | logger = logging.getLogger('%s:%d' % ('Recv', sock.fileno())) 265 | logger.debug('Reading message with length: %d' % len(buf)) 266 | logger.debug('Read message: %s' % buf) 267 | js = json.loads(buf) 268 | if type == 1: 269 | if js['Type'] == 'ReqProxy': 270 | newsock = connectremote(host, port) 271 | if newsock: 272 | thread = threading.Thread(target = HKClient, args = (newsock, 0, 2)) 273 | thread.setDaemon(True) 274 | thread.start() 275 | if js['Type'] == 'AuthResp': 276 | ClientId = js['Payload']['ClientId'] 277 | logger = logging.getLogger('%s' % 'client') 278 | logger.debug('Authenticated with server, client id: %s' % ClientId) 279 | sendpack(sock, Ping()) 280 | pingtime = time.time() 281 | if js['Type'] == 'NewTunnel': 282 | if js['Payload']['Error'] != '': 283 | logger = logging.getLogger('%s' % 'client') 284 | logger.error('Server failed to allocate tunnel: %s' % js['Payload']['Error']) 285 | time.sleep(30) 286 | else: 287 | Tunnels[js['Payload']['Url']] = js['Payload'] 288 | logger = logging.getLogger('%s' % 'client') 289 | logger.debug('Tunnel established at %s' % js['Payload']['Url']) 290 | print('隧道建立成功: %s' % js['Payload']['Url']) # 注册成功 291 | if type == 2: 292 | if js['Type'] == 'StartProxy': 293 | loacladdr = getloacladdr(Tunnels, js['Payload']['Url']) 294 | 295 | newsock = connectlocal(str(loacladdr[0]), int(loacladdr[1])) 296 | if newsock: 297 | thread = threading.Thread(target = HKClient, args = (newsock, 0, 3, sock)) 298 | thread.setDaemon(True) 299 | thread.start() 300 | tosock = newsock 301 | linkstate = 2 302 | else: 303 | body = 'Web服务错误
隧道 %s 无效
无法连接到%s. 此端口尚未提供Web服务
' 304 | html = body % (js['Payload']['Url'], str(loacladdr[0]) + ':' + str(loacladdr[1])) 305 | header = "HTTP/1.0 502 Bad Gateway" + "\r\n" 306 | header += "Content-Type: text/html" + "\r\n" 307 | header += "Content-Length: %d" + "\r\n" 308 | header += "\r\n" + "%s" 309 | buf = header % (len(html.encode('utf-8')), html) 310 | sendbuf(sock, buf.encode('utf-8')) 311 | 312 | if len(recvbuf) == (8 + lenbyte): 313 | recvbuf = bytes() 314 | else: 315 | recvbuf = recvbuf[8 + lenbyte:] 316 | 317 | if type == 3 or (type == 2 and linkstate == 2): 318 | sendbuf(tosock, recvbuf) 319 | recvbuf = bytes() 320 | 321 | except socket.error: 322 | break 323 | 324 | if type == 1: 325 | mainsocket = False 326 | if type == 3: 327 | try: 328 | tosock.shutdown(socket.SHUT_WR) 329 | except socket.error: 330 | tosock.close() 331 | 332 | logger = logging.getLogger('%s:%d' % ('Close', sock.fileno())) 333 | logger.debug('Closing') 334 | sock.close() 335 | 336 | # 客户端程序初始化 337 | if __name__ == '__main__': 338 | logging.basicConfig(level=logging.INFO, format='[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s', datefmt='%Y/%m/%d %H:%M:%S') 339 | logger = logging.getLogger('%s' % 'client') 340 | logger.debug('python-ngrok v1.5') 341 | while True: 342 | try: 343 | # 检测控制连接是否连接. 344 | if mainsocket == False: 345 | ip = dnsopen(host) 346 | if ip == False: 347 | logger = logging.getLogger('%s' % 'client') 348 | logger.debug('update dns') 349 | print('连接natapp服务器失败.') 350 | time.sleep(10) 351 | continue 352 | mainsocket = connectremote(ip, port) 353 | if mainsocket == False: 354 | logger = logging.getLogger('%s' % 'client') 355 | logger.debug('connect failed...!') 356 | print('连接natapp服务器失败.') 357 | time.sleep(10) 358 | continue 359 | thread = threading.Thread(target = HKClient, args = (mainsocket, 0, 1)) 360 | thread.setDaemon(True) 361 | thread.start() 362 | 363 | # 发送心跳 364 | if pingtime + 20 < time.time() and pingtime != 0: 365 | sendpack(mainsocket, Ping()) 366 | pingtime = time.time() 367 | 368 | time.sleep(1) 369 | 370 | except socket.error: 371 | pingtime = 0 372 | except KeyboardInterrupt: 373 | sys.exit() 374 | -------------------------------------------------------------------------------- /demo/sunny.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | """ 4 | ngrok.cc 内网穿透服务 Python 版 5 | 6 | 本程序仅适用于ngrok.cc 使用前请先在 https://ngrok.cc 注册账号. 7 | Linux 系统一般自带Python 可以直接运行 8 | 赋予权限 chmod 755 sunny.py 9 | 直接运行 ./sunny.py --clientid=xxxxxxxxxxxxxxxx 10 | 命令行模式执行 python sunny.py --clientid=xxxxxx 即可运行 11 | 12 | 感谢 hauntek 提供的 python-ngrok 原版程序 13 | """ 14 | import getopt 15 | import socket 16 | import ssl 17 | import json 18 | import struct 19 | import random 20 | import sys 21 | import time 22 | import logging 23 | import threading 24 | 25 | python_version = sys.version_info >= (3, 0) 26 | if not python_version: 27 | reload(sys) 28 | sys.setdefaultencoding('utf8') 29 | 30 | options = { 31 | 'clientid':'', 32 | } 33 | 34 | def usage(): 35 | print( 36 | ' -h help \n' \ 37 | ' -a clientid xxxxxxxxxxxxxxxx\n' \ 38 | ) 39 | sys.exit() 40 | 41 | try: 42 | opts, args = getopt.getopt(sys.argv[1:], "h:c:", ['help', "clientid="]) 43 | except getopt.GetoptError: 44 | usage() 45 | 46 | if len(opts) == 0: 47 | print( 48 | '使用说明\n' \ 49 | '在命令行模式运行 python sunny.py --clientid=xxxxxxxx\n' \ 50 | '如果是多个隧道换成 python sunny.py --clientid=xxxxxxxx,xxxxxxxx\n' \ 51 | '请登录 https://ngrok.cc 获取 clientid\n' \ 52 | ) 53 | 54 | for option, value in opts: 55 | if option in ['-h', '--help']: 56 | usage() 57 | if option in ['-c', '--clientid']: 58 | options['clientid'] = value 59 | 60 | if options['clientid'] == '': 61 | if not python_version: 62 | input_clientid = raw_input('请输入clientid:') 63 | else: 64 | input_clientid = str(input('请输入clientid:')) 65 | if input_clientid != '': 66 | options['clientid'] = input_clientid 67 | else: 68 | sys.exit() 69 | 70 | Tunnels = list() # 全局渠道赋值 71 | 72 | reqIdaddr = dict() 73 | localaddr = dict() 74 | 75 | # ngrok.cc 添加到渠道队列 76 | def ngrok_adds(Tunnel): 77 | global Tunnels 78 | for tunnelinfo in Tunnel: 79 | if tunnelinfo.get('proto'): 80 | if tunnelinfo.get('proto').get('http'): 81 | protocol = 'http' 82 | if tunnelinfo.get('proto').get('https'): 83 | protocol = 'https' 84 | if tunnelinfo.get('proto').get('tcp'): 85 | protocol = 'tcp' 86 | 87 | proto = tunnelinfo['proto'][protocol].split(':') # 127.0.0.1:80 拆分成数组 88 | if proto[0] == '': 89 | proto[0] = '127.0.0.1' 90 | if proto[1] == '' or proto[1] == 0: 91 | proto[1] = 80 92 | 93 | body = dict() 94 | body['protocol'] = protocol 95 | body['hostname'] = tunnelinfo['hostname'] 96 | body['subdomain'] = tunnelinfo['subdomain'] 97 | body['httpauth'] = tunnelinfo['httpauth'] 98 | body['rport'] = tunnelinfo['remoteport'] 99 | body['lhost'] = str(proto[0]) 100 | body['lport'] = int(proto[1]) 101 | Tunnels.append(body) # 加入渠道队列 102 | 103 | # ngrok.cc 获取服务器设置 104 | def ngrok_auth(options): 105 | host = 'www.ngrok.cc' 106 | port = 443 107 | try: 108 | client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 109 | ssl_client = ssl.wrap_socket(client, ssl_version=ssl.PROTOCOL_TLSv1_2) # ssl.PROTOCOL_TLSv1_2 110 | ssl_client.connect((host, port)) 111 | except Exception: 112 | print('连接认证服务器: https://www.ngrok.cc 错误.') 113 | time.sleep(10) 114 | sys.exit() 115 | 116 | header = "POST " + "/api/clientid/clientid/%s" + " HTTP/1.1" + "\r\n" 117 | header += "Content-Type: text/html" + "\r\n" 118 | header += "Host: %s" + "\r\n" 119 | header += "\r\n" 120 | buf = header % (options, host) 121 | ssl_client.sendall(buf.encode('utf-8')) # 发送请求头 122 | 123 | fd = ssl_client.makefile('rb', 0) 124 | body = bytes() 125 | while True: 126 | line = fd.readline().decode('utf-8') 127 | if line == "\n" or line == "\r\n": 128 | chunk_size = int(fd.readline(), 16) 129 | if chunk_size > 0: 130 | body = fd.read(chunk_size).decode('utf-8') 131 | break 132 | 133 | ssl_client.close() 134 | 135 | authData = json.loads(body) 136 | if authData['status'] != 200: 137 | print('认证错误:%s, ErrorCode:%s' % (authData['msg'], authData['status'])) 138 | time.sleep(10) 139 | sys.exit() 140 | 141 | print('认证成功,正在连接服务器...') 142 | # 设置映射隧道,支持多渠道[客户端id] 143 | ngrok_adds(authData['data']) 144 | proto = authData['server'].split(':') 145 | return proto 146 | 147 | print('欢迎使用内网穿透 python-ngrok v1.42\r\nCtrl+C 退出') 148 | serverArr = ngrok_auth(options['clientid']) 149 | host = str(serverArr[0]) # Ngrok服务器地址 150 | port = int(serverArr[1]) # 端口 151 | bufsize = 1024 # 吞吐量 152 | 153 | mainsocket = 0 154 | 155 | ClientId = '' 156 | pingtime = 0 157 | 158 | def getloacladdr(Tunnels, Url): 159 | protocol = Url[0:Url.find(':')] 160 | hostname = Url[Url.find('//') + 2:] 161 | subdomain = hostname[0:hostname.find('.')] 162 | rport = Url[Url.rfind(':') + 1:] 163 | 164 | for tunnelinfo in Tunnels: 165 | if tunnelinfo.get('protocol') == protocol: 166 | if tunnelinfo.get('protocol') in ['http', 'https']: 167 | if tunnelinfo.get('hostname') == hostname: 168 | return tunnelinfo 169 | if tunnelinfo.get('subdomain') == subdomain: 170 | return tunnelinfo 171 | if tunnelinfo.get('protocol') == 'tcp': 172 | if tunnelinfo.get('rport') == int(rport): 173 | return tunnelinfo 174 | 175 | return dict() 176 | 177 | def dnsopen(host): 178 | try: 179 | ip = socket.gethostbyname(host) 180 | except socket.error: 181 | return False 182 | 183 | return ip 184 | 185 | def connectremote(host, port): 186 | try: 187 | host = socket.gethostbyname(host) 188 | client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 189 | ssl_client = ssl.wrap_socket(client, ssl_version=ssl.PROTOCOL_SSLv23) 190 | ssl_client.connect((host, port)) 191 | ssl_client.setblocking(1) 192 | logger = logging.getLogger('%s:%d' % ('Conn', ssl_client.fileno())) 193 | logger.debug('New connection to: %s:%d' % (host, port)) 194 | except socket.error: 195 | return False 196 | 197 | return ssl_client 198 | 199 | def connectlocal(localhost, localport): 200 | try: 201 | localhost = socket.gethostbyname(localhost) 202 | client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 203 | client.connect((localhost, localport)) 204 | client.setblocking(1) 205 | logger = logging.getLogger('%s:%d' % ('Conn', client.fileno())) 206 | logger.debug('New connection to: %s:%d' % (localhost, localport)) 207 | except socket.error: 208 | return False 209 | 210 | return client 211 | 212 | def NgrokAuth(): 213 | Payload = dict() 214 | Payload['ClientId'] = '' 215 | Payload['OS'] = 'darwin' 216 | Payload['Arch'] = 'amd64' 217 | Payload['Version'] = '2' 218 | Payload['MmVersion'] = '2.1' 219 | Payload['User'] = 'user' 220 | Payload['Password'] = '' 221 | body = dict() 222 | body['Type'] = 'Auth' 223 | body['Payload'] = Payload 224 | buffer = json.dumps(body) 225 | return(buffer) 226 | 227 | def ReqTunnel(ReqId, Protocol, Hostname, Subdomain, HttpAuth, RemotePort): 228 | Payload = dict() 229 | Payload['ReqId'] = ReqId 230 | Payload['Protocol'] = Protocol 231 | Payload['Hostname'] = Hostname 232 | Payload['Subdomain'] = Subdomain 233 | Payload['HttpAuth'] = HttpAuth 234 | Payload['RemotePort'] = RemotePort 235 | body = dict() 236 | body['Type'] = 'ReqTunnel' 237 | body['Payload'] = Payload 238 | buffer = json.dumps(body) 239 | return(buffer) 240 | 241 | def RegProxy(ClientId): 242 | Payload = dict() 243 | Payload['ClientId'] = ClientId 244 | body = dict() 245 | body['Type'] = 'RegProxy' 246 | body['Payload'] = Payload 247 | buffer = json.dumps(body) 248 | return(buffer) 249 | 250 | def Ping(): 251 | Payload = dict() 252 | body = dict() 253 | body['Type'] = 'Ping' 254 | body['Payload'] = Payload 255 | buffer = json.dumps(body) 256 | return(buffer) 257 | 258 | def lentobyte(len): 259 | return struct.pack(' 0: 310 | if not recvbuf: 311 | recvbuf = recvbut 312 | else: 313 | recvbuf += recvbut 314 | 315 | if type == 1 or (type == 2 and linkstate == 1): 316 | lenbyte = tolen(recvbuf[0:8]) 317 | if len(recvbuf) >= (8 + lenbyte): 318 | buf = recvbuf[8:lenbyte + 8].decode('utf-8') 319 | logger = logging.getLogger('%s:%d' % ('Recv', sock.fileno())) 320 | logger.debug('Reading message with length: %d' % len(buf)) 321 | logger.debug('Read message: %s' % buf) 322 | js = json.loads(buf) 323 | if type == 1: 324 | if js['Type'] == 'ReqProxy': 325 | newsock = connectremote(host, port) 326 | if newsock: 327 | thread = threading.Thread(target = HKClient, args = (newsock, 0, 2)) 328 | thread.setDaemon(True) 329 | thread.start() 330 | if js['Type'] == 'AuthResp': 331 | ClientId = js['Payload']['ClientId'] 332 | logger = logging.getLogger('%s' % 'client') 333 | logger.debug('Authenticated with server, client id: %s' % ClientId) 334 | sendpack(sock, Ping()) 335 | pingtime = time.time() 336 | for info in Tunnels: 337 | reqid = getRandChar(8) 338 | sendpack(sock, ReqTunnel(reqid, info['protocol'], info['hostname'], info['subdomain'], info['httpauth'], info['rport'])) 339 | reqIdaddr[reqid] = (info['lhost'], info['lport']) 340 | if js['Type'] == 'NewTunnel': 341 | if js['Payload']['Error'] != '': 342 | logger = logging.getLogger('%s' % 'client') 343 | logger.error('Server failed to allocate tunnel: %s' % js['Payload']['Error']) 344 | print('隧道建立失败: %s' % js['Payload']['Error']) 345 | time.sleep(30) 346 | else: 347 | logger = logging.getLogger('%s' % 'client') 348 | logger.debug('Tunnel established at %s' % js['Payload']['Url']) 349 | print('隧道建立成功: %s' % js['Payload']['Url']) # 注册成功 350 | localaddr[js['Payload']['Url']] = reqIdaddr[js['Payload']['ReqId']] 351 | if type == 2: 352 | if js['Type'] == 'StartProxy': 353 | localhost, localport = localaddr[js['Payload']['Url']] 354 | 355 | newsock = connectlocal(localhost, localport) 356 | if newsock: 357 | thread = threading.Thread(target = HKClient, args = (newsock, 0, 3, sock)) 358 | thread.setDaemon(True) 359 | thread.start() 360 | tosock = newsock 361 | linkstate = 2 362 | else: 363 | body = 'Web服务错误
隧道 %s 无效
无法连接到%s. 此端口尚未提供Web服务
' 364 | html = body % (js['Payload']['Url'], localhost + ':' + str(localport)) 365 | header = "HTTP/1.0 502 Bad Gateway" + "\r\n" 366 | header += "Content-Type: text/html" + "\r\n" 367 | header += "Content-Length: %d" + "\r\n" 368 | header += "\r\n" + "%s" 369 | buf = header % (len(html.encode('utf-8')), html) 370 | sendbuf(sock, buf.encode('utf-8')) 371 | 372 | if len(recvbuf) == (8 + lenbyte): 373 | recvbuf = bytes() 374 | else: 375 | recvbuf = recvbuf[8 + lenbyte:] 376 | 377 | if type == 3 or (type == 2 and linkstate == 2): 378 | sendbuf(tosock, recvbuf) 379 | recvbuf = bytes() 380 | 381 | except socket.error: 382 | break 383 | 384 | if type == 1: 385 | mainsocket = False 386 | if type == 3: 387 | try: 388 | tosock.shutdown(socket.SHUT_WR) 389 | except socket.error: 390 | tosock.close() 391 | 392 | logger = logging.getLogger('%s:%d' % ('Close', sock.fileno())) 393 | logger.debug('Closing') 394 | sock.close() 395 | 396 | # 客户端程序初始化 397 | if __name__ == '__main__': 398 | logging.basicConfig(level=logging.INFO, format='[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s', datefmt='%Y/%m/%d %H:%M:%S') 399 | logger = logging.getLogger('%s' % 'client') 400 | logger.debug('python-ngrok v1.5') 401 | while True: 402 | try: 403 | # 检测控制连接是否连接. 404 | if mainsocket == False: 405 | ip = dnsopen(host) 406 | if ip == False: 407 | logger = logging.getLogger('%s' % 'client') 408 | logger.debug('update dns') 409 | print('连接ngrok服务器失败.') 410 | time.sleep(10) 411 | continue 412 | mainsocket = connectremote(ip, port) 413 | if mainsocket == False: 414 | logger = logging.getLogger('%s' % 'client') 415 | logger.debug('connect failed...!') 416 | print('连接ngrok服务器失败.') 417 | time.sleep(10) 418 | continue 419 | thread = threading.Thread(target = HKClient, args = (mainsocket, 0, 1)) 420 | thread.setDaemon(True) 421 | thread.start() 422 | 423 | # 发送心跳 424 | if pingtime + 20 < time.time() and pingtime != 0: 425 | sendpack(mainsocket, Ping()) 426 | pingtime = time.time() 427 | 428 | time.sleep(1) 429 | 430 | except socket.error: 431 | pingtime = 0 432 | except KeyboardInterrupt: 433 | sys.exit() 434 | -------------------------------------------------------------------------------- /ngrok.config: -------------------------------------------------------------------------------- 1 | { 2 | "server": { 3 | "host": "tunnel.qydev.com", 4 | "port": 4443, 5 | "bufsize": 1024, 6 | "authtoken": "" 7 | }, 8 | "client": [ 9 | { 10 | "protocol": "http", 11 | "hostname": "www.xxx.com", 12 | "subdomain": "", 13 | "httpauth": "", 14 | "rport": 0, 15 | "lhost": "127.0.0.1", 16 | "lport": 80 17 | }, 18 | { 19 | "protocol": "http", 20 | "hostname": "", 21 | "subdomain": "xxx", 22 | "httpauth": "", 23 | "rport": 0, 24 | "lhost": "127.0.0.1", 25 | "lport": 80 26 | }, 27 | { 28 | "protocol": "tcp", 29 | "hostname": "", 30 | "subdomain": "", 31 | "httpauth": "", 32 | "rport": 55499, 33 | "lhost": "127.0.0.1", 34 | "lport": 22 35 | }, 36 | { 37 | "protocol": "udp", 38 | "hostname": "", 39 | "subdomain": "", 40 | "httpauth": "", 41 | "rport": 55499, 42 | "lhost": "127.0.0.1", 43 | "lport": 53 44 | } 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /python-ngrok.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | # 建议Python 2.7.9 或 Python 3.4.2 以上运行 4 | # 项目地址: https://github.com/hauntek/python-ngrok 5 | # Version: v1.56 6 | import socket 7 | import ssl 8 | import json 9 | import struct 10 | import random 11 | import sys 12 | import time 13 | import logging 14 | import threading 15 | 16 | host = 'tunnel.qydev.com' # Ngrok服务器地址 17 | port = 4443 # 端口 18 | bufsize = 1024 # 吞吐量 19 | 20 | dualstack = 'IPv4/IPv6' # 服务连接协议 [IPv4/IPv6=双栈] 21 | dualstack_or = 0 # 本地转发协议 [0=双栈, 1=IPv4, 2=IPv6] 22 | 23 | Tunnels = list() # 全局渠道赋值 24 | body = dict() 25 | body['protocol'] = 'http' 26 | body['hostname'] = 'www.xxx.com' 27 | body['subdomain'] = '' 28 | body['rport'] = 0 29 | body['lhost'] = '127.0.0.1' 30 | body['lport'] = 80 31 | Tunnels.append(body) # 加入渠道队列 32 | 33 | body = dict() 34 | body['protocol'] = 'http' 35 | body['hostname'] = '' 36 | body['subdomain'] = 'xxx' 37 | body['rport'] = 0 38 | body['lhost'] = '127.0.0.1' 39 | body['lport'] = 80 40 | Tunnels.append(body) # 加入渠道队列 41 | 42 | body = dict() 43 | body['protocol'] = 'tcp' 44 | body['hostname'] = '' 45 | body['subdomain'] = '' 46 | body['rport'] = 55499 47 | body['lhost'] = '127.0.0.1' 48 | body['lport'] = 22 49 | Tunnels.append(body) # 加入渠道队列 50 | 51 | reqIdaddr = dict() 52 | localaddr = dict() 53 | 54 | # 读取配置文件 55 | if len(sys.argv) >= 2: 56 | file_object = open(sys.argv[1]) 57 | try: 58 | all_the_text = file_object.read() 59 | config_object = json.loads(all_the_text) 60 | host = config_object["server"]["host"] # Ngrok服务器地址 61 | port = int(config_object["server"]["port"]) # 端口 62 | bufsize = int(config_object["server"]["bufsize"]) # 吞吐量 63 | Tunnels = list() # 重置渠道赋值 64 | for Tunnel in config_object["client"]: 65 | body = dict() 66 | body['protocol'] = Tunnel["protocol"] 67 | body['hostname'] = Tunnel["hostname"] 68 | body['subdomain'] = Tunnel["subdomain"] 69 | body['rport'] = int(Tunnel["rport"]) 70 | body['lhost'] = Tunnel["lhost"] 71 | body['lport'] = int(Tunnel["lport"]) 72 | Tunnels.append(body) # 加入渠道队列 73 | del all_the_text 74 | del config_object 75 | except Exception: 76 | # logger = logging.getLogger('%s' % 'config') 77 | # logger.error('The configuration file read failed') 78 | # exit(1) 79 | pass 80 | finally: 81 | file_object.close() 82 | 83 | mainsocket = 0 84 | 85 | ClientId = '' 86 | pingtime = 0 87 | 88 | def connectremote(host, port): 89 | ipv4_addr = list() 90 | ipv6_addr = list() 91 | 92 | for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_STREAM): 93 | af, socktype, proto, canonname, sa = res 94 | 95 | if dualstack == 'IPv4' or dualstack == 'IPv4/IPv6': 96 | if af == socket.AF_INET: ipv4_addr.append(res) 97 | if dualstack == 'IPv6' or dualstack == 'IPv4/IPv6': 98 | if af == socket.AF_INET6: ipv6_addr.append(res) 99 | 100 | if dualstack == 'IPv6' or dualstack == 'IPv4/IPv6': 101 | if len(ipv6_addr) > 0: 102 | for res in ipv6_addr: 103 | af, socktype, proto, canonname, sa = res 104 | try: 105 | client = socket.socket(af, socktype, proto) 106 | ssl_client = ssl.wrap_socket(client, ssl_version=ssl.PROTOCOL_SSLv23) 107 | except socket.error: 108 | continue 109 | try: 110 | ssl_client.connect(sa) 111 | ssl_client.setblocking(1) 112 | logger = logging.getLogger('%s:%d' % ('Conn', ssl_client.fileno())) 113 | logger.debug('New connection to: %s:%d' % (host, port)) 114 | 115 | return ssl_client 116 | except socket.error: 117 | continue 118 | 119 | if dualstack == 'IPv4' or dualstack == 'IPv4/IPv6': 120 | if len(ipv4_addr) > 0: 121 | for res in ipv4_addr: 122 | af, socktype, proto, canonname, sa = res 123 | try: 124 | client = socket.socket(af, socktype, proto) 125 | ssl_client = ssl.wrap_socket(client, ssl_version=ssl.PROTOCOL_SSLv23) 126 | except socket.error: 127 | continue 128 | try: 129 | ssl_client.connect(sa) 130 | ssl_client.setblocking(1) 131 | logger = logging.getLogger('%s:%d' % ('Conn', ssl_client.fileno())) 132 | logger.debug('New connection to: %s:%d' % (host, port)) 133 | 134 | return ssl_client 135 | except socket.error: 136 | continue 137 | 138 | return False 139 | 140 | def connectlocal(localhost, localport): 141 | ipv4_addr = list() 142 | ipv6_addr = list() 143 | 144 | for res in socket.getaddrinfo(localhost, localport, socket.AF_UNSPEC, socket.SOCK_STREAM): 145 | af, socktype, proto, canonname, sa = res 146 | 147 | if dualstack_or == 1 or dualstack_or == 0: 148 | if af == socket.AF_INET: ipv4_addr.append(res) 149 | if dualstack_or == 2 or dualstack_or == 0: 150 | if af == socket.AF_INET6: ipv6_addr.append(res) 151 | 152 | if dualstack_or == 2 or dualstack_or == 0: 153 | if len(ipv6_addr) > 0: 154 | for res in ipv6_addr: 155 | af, socktype, proto, canonname, sa = res 156 | try: 157 | client = socket.socket(af, socktype, proto) 158 | except socket.error: 159 | continue 160 | try: 161 | client.connect(sa) 162 | client.setblocking(1) 163 | logger = logging.getLogger('%s:%d' % ('Conn', client.fileno())) 164 | logger.debug('New connection to: %s:%d' % (host, port)) 165 | 166 | return client 167 | except socket.error: 168 | continue 169 | 170 | if dualstack_or == 1 or dualstack_or == 0: 171 | if len(ipv4_addr) > 0: 172 | for res in ipv4_addr: 173 | af, socktype, proto, canonname, sa = res 174 | try: 175 | client = socket.socket(af, socktype, proto) 176 | except socket.error: 177 | continue 178 | try: 179 | client.connect(sa) 180 | client.setblocking(1) 181 | logger = logging.getLogger('%s:%d' % ('Conn', client.fileno())) 182 | logger.debug('New connection to: %s:%d' % (host, port)) 183 | 184 | return client 185 | except socket.error: 186 | continue 187 | 188 | return False 189 | 190 | def NgrokAuth(): 191 | Payload = dict() 192 | Payload['ClientId'] = '' 193 | Payload['OS'] = 'darwin' 194 | Payload['Arch'] = 'amd64' 195 | Payload['Version'] = '2' 196 | Payload['MmVersion'] = '1.7' 197 | Payload['User'] = 'user' 198 | Payload['Password'] = '' 199 | body = dict() 200 | body['Type'] = 'Auth' 201 | body['Payload'] = Payload 202 | buffer = json.dumps(body) 203 | return(buffer) 204 | 205 | def ReqTunnel(ReqId, Protocol, Hostname, Subdomain, RemotePort): 206 | Payload = dict() 207 | Payload['ReqId'] = ReqId 208 | Payload['Protocol'] = Protocol 209 | Payload['Hostname'] = Hostname 210 | Payload['Subdomain'] = Subdomain 211 | Payload['HttpAuth'] = '' 212 | Payload['RemotePort'] = RemotePort 213 | body = dict() 214 | body['Type'] = 'ReqTunnel' 215 | body['Payload'] = Payload 216 | buffer = json.dumps(body) 217 | return(buffer) 218 | 219 | def RegProxy(ClientId): 220 | Payload = dict() 221 | Payload['ClientId'] = ClientId 222 | body = dict() 223 | body['Type'] = 'RegProxy' 224 | body['Payload'] = Payload 225 | buffer = json.dumps(body) 226 | return(buffer) 227 | 228 | def Ping(): 229 | Payload = dict() 230 | body = dict() 231 | body['Type'] = 'Ping' 232 | body['Payload'] = Payload 233 | buffer = json.dumps(body) 234 | return(buffer) 235 | 236 | def lentobyte(len): 237 | return struct.pack(' 0: 296 | if not recvbuf: 297 | recvbuf = recvbut 298 | else: 299 | recvbuf += recvbut 300 | 301 | if type == 1 or (type == 2 and linkstate == 1): 302 | lenbyte = tolen(recvbuf[0:8]) 303 | if len(recvbuf) >= (8 + lenbyte): 304 | buf = recvbuf[8:lenbyte + 8].decode('utf-8') 305 | logger = logging.getLogger('%s:%d' % ('Recv', sock.fileno())) 306 | logger.debug('Reading message with length: %d' % len(buf)) 307 | logger.debug('Read message: %s' % buf) 308 | js = json.loads(buf) 309 | if type == 1: 310 | if js['Type'] == 'ReqProxy': 311 | newsock = connectremote(host, port) 312 | if newsock: 313 | thread = threading.Thread(target = HKClient, args = (newsock, 0, 2)) 314 | thread.setDaemon(True) 315 | thread.start() 316 | if js['Type'] == 'AuthResp': 317 | ClientId = js['Payload']['ClientId'] 318 | logger = logging.getLogger('%s' % 'client') 319 | logger.info('Authenticated with server, client id: %s' % ClientId) 320 | sendpack(sock, Ping()) 321 | pingtime = time.time() 322 | for info in Tunnels: 323 | reqid = getRandChar(8) 324 | sendpack(sock, ReqTunnel(reqid, info['protocol'], info['hostname'], info['subdomain'], info['rport'])) 325 | reqIdaddr[reqid] = (info['lhost'], info['lport']) 326 | if js['Type'] == 'NewTunnel': 327 | if js['Payload']['Error'] != '': 328 | logger = logging.getLogger('%s' % 'client') 329 | logger.error('Server failed to allocate tunnel: %s' % js['Payload']['Error']) 330 | time.sleep(30) 331 | else: 332 | logger = logging.getLogger('%s' % 'client') 333 | logger.info('Tunnel established at %s' % js['Payload']['Url']) 334 | localaddr[js['Payload']['Url']] = reqIdaddr[js['Payload']['ReqId']] 335 | if type == 2: 336 | if js['Type'] == 'StartProxy': 337 | localhost, localport = localaddr[js['Payload']['Url']] 338 | 339 | newsock = connectlocal(localhost, localport) 340 | if newsock: 341 | thread = threading.Thread(target = HKClient, args = (newsock, 0, 3, sock)) 342 | thread.setDaemon(True) 343 | thread.start() 344 | tosock = newsock 345 | linkstate = 2 346 | else: 347 | body = '

Tunnel %s unavailable

Unable to initiate connection to %s. This port is not yet available for web server.

' 348 | html = body % (js['Payload']['Url'], localhost + ':' + str(localport)) 349 | header = "HTTP/1.0 502 Bad Gateway" + "\r\n" 350 | header += "Content-Type: text/html" + "\r\n" 351 | header += "Content-Length: %d" + "\r\n" 352 | header += "\r\n" + "%s" 353 | buf = header % (len(html.encode('utf-8')), html) 354 | sendbuf(sock, buf.encode('utf-8')) 355 | 356 | if len(recvbuf) == (8 + lenbyte): 357 | recvbuf = bytes() 358 | else: 359 | recvbuf = recvbuf[8 + lenbyte:] 360 | 361 | if type == 3 or (type == 2 and linkstate == 2): 362 | sendbuf(tosock, recvbuf) 363 | recvbuf = bytes() 364 | 365 | except socket.error: 366 | break 367 | 368 | if type == 1: 369 | mainsocket = False 370 | if type == 3: 371 | try: 372 | tosock.shutdown(socket.SHUT_WR) 373 | except socket.error: 374 | tosock.close() 375 | 376 | logger = logging.getLogger('%s:%d' % ('Close', sock.fileno())) 377 | logger.debug('Closing') 378 | sock.close() 379 | 380 | # 客户端程序初始化 381 | if __name__ == '__main__': 382 | logging.basicConfig(level=logging.DEBUG, format='[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s', datefmt='%Y/%m/%d %H:%M:%S') 383 | logger = logging.getLogger('%s' % 'client') 384 | logger.info('python-ngrok v1.56') 385 | while True: 386 | try: 387 | # 检测控制连接是否连接. 388 | if mainsocket == False: 389 | mainsocket = connectremote(host, port) 390 | if mainsocket == False: 391 | logger = logging.getLogger('%s' % 'client') 392 | logger.info('connect failed...!') 393 | time.sleep(10) 394 | continue 395 | thread = threading.Thread(target = HKClient, args = (mainsocket, 0, 1)) 396 | thread.setDaemon(True) 397 | thread.start() 398 | 399 | # 发送心跳 400 | if pingtime + 20 < time.time() and pingtime != 0: 401 | sendpack(mainsocket, Ping()) 402 | pingtime = time.time() 403 | 404 | time.sleep(1) 405 | 406 | except socket.error: 407 | pingtime = 0 408 | except KeyboardInterrupt: 409 | sys.exit() 410 | -------------------------------------------------------------------------------- /python-ngrok_deepseek.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | # 建议Python 3.7.0 以上运行 4 | # 项目地址: https://github.com/hauntek/python-ngrok 5 | # Version: 2.2.0 6 | import asyncio 7 | import socket 8 | import ssl 9 | import json 10 | import struct 11 | import sys 12 | import time 13 | import secrets 14 | import logging 15 | from typing import Optional, Union, Dict, List 16 | from dataclasses import dataclass, asdict, fields 17 | 18 | # 配置日志格式 19 | logging.basicConfig( 20 | level=logging.INFO, 21 | format='[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s', 22 | datefmt='%Y/%m/%d %H:%M:%S' 23 | ) 24 | logger = logging.getLogger('NgrokClient') 25 | 26 | # 定义认证消息的数据结构 27 | @dataclass 28 | class Auth: 29 | Version: str = "2" # 协议版本 30 | MmVersion: str = "1.7" # 主版本号 31 | User: str = "" # 用户名 32 | Password: str = "" # 密码 33 | OS: str = "darwin" # 操作系统 34 | Arch: str = "amd64" # 系统架构 35 | ClientId: str = "" # 客户端ID 36 | 37 | @classmethod 38 | def get_class_name(cls): 39 | """返回类名""" 40 | return cls.__name__ 41 | 42 | # 定义认证响应消息的数据结构 43 | @dataclass 44 | class AuthResp: 45 | Version: str = "2" # 协议版本 46 | MmVersion: str = "1.7" # 主版本号 47 | ClientId: str = "" # 客户端ID 48 | Error: str = "" # 错误信息 49 | 50 | @classmethod 51 | def get_class_name(cls): 52 | """返回类名""" 53 | return cls.__name__ 54 | 55 | # 定义请求隧道消息的数据结构 56 | @dataclass 57 | class ReqTunnel: 58 | ReqId: str = "" # 请求ID 59 | Protocol: str = "" # 协议类型(如 http、tcp) 60 | Hostname: str = "" # 主机名 61 | Subdomain: str = "" # 子域名 62 | HttpAuth: str = "" # HTTP 认证信息 63 | RemotePort: int = 0 # 远程端口 64 | 65 | @classmethod 66 | def get_class_name(cls): 67 | """返回类名""" 68 | return cls.__name__ 69 | 70 | # 定义新隧道消息的数据结构 71 | @dataclass 72 | class NewTunnel: 73 | ReqId: str = "" # 请求ID 74 | Url: str = "" # 隧道URL 75 | Protocol: str = "" # 协议类型 76 | Error: str = "" # 错误信息 77 | 78 | @classmethod 79 | def get_class_name(cls): 80 | """返回类名""" 81 | return cls.__name__ 82 | 83 | # 定义请求代理消息的数据结构 84 | @dataclass 85 | class ReqProxy: 86 | pass 87 | 88 | @classmethod 89 | def get_class_name(cls): 90 | """返回类名""" 91 | return cls.__name__ 92 | 93 | # 定义注册代理消息的数据结构 94 | @dataclass 95 | class RegProxy: 96 | ClientId: str = "" # 客户端ID 97 | 98 | @classmethod 99 | def get_class_name(cls): 100 | """返回类名""" 101 | return cls.__name__ 102 | 103 | # 定义启动代理消息的数据结构 104 | @dataclass 105 | class StartProxy: 106 | Url: str = "" # 代理URL 107 | ClientAddr: str = "" # 客户端地址 108 | 109 | @classmethod 110 | def get_class_name(cls): 111 | """返回类名""" 112 | return cls.__name__ 113 | 114 | # 定义心跳消息的数据结构 115 | @dataclass 116 | class Ping: 117 | pass 118 | 119 | @classmethod 120 | def get_class_name(cls): 121 | """返回类名""" 122 | return cls.__name__ 123 | 124 | # 定义心跳响应消息的数据结构 125 | @dataclass 126 | class Pong: 127 | pass 128 | 129 | @classmethod 130 | def get_class_name(cls): 131 | """返回类名""" 132 | return cls.__name__ 133 | 134 | # 定义消息类型的联合类型 135 | MessageType = Union[Auth, AuthResp, ReqTunnel, NewTunnel, ReqProxy, RegProxy, StartProxy, Ping, Pong] 136 | 137 | # Ngrok 客户端配置类 138 | class NgrokConfig: 139 | def __init__(self): 140 | """初始化默认配置""" 141 | self.server_host = 'tunnel.qydev.com' # 服务器主机名 142 | self.server_port = 4443 # 服务器端口 143 | self.bufsize = 1024 # 缓冲区大小 144 | self.authtoken = '' # 认证令牌 145 | self.tunnels: List[Dict] = [] # 隧道配置列表 146 | 147 | # 添加默认隧道配置 148 | self.tunnels.append({ 149 | 'protocol': 'http', 150 | 'hostname': 'www.xxx.com', 151 | 'subdomain': '', 152 | 'httpauth': '', 153 | 'rport': 0, 154 | 'lhost': '127.0.0.1', 155 | 'lport': 80 156 | }) 157 | self.tunnels.append({ 158 | 'protocol': 'http', 159 | 'hostname': '', 160 | 'subdomain': 'xxx', 161 | 'httpauth': '', 162 | 'rport': 0, 163 | 'lhost': '127.0.0.1', 164 | 'lport': 80 165 | }) 166 | self.tunnels.append({ 167 | 'protocol': 'tcp', 168 | 'hostname': '', 169 | 'subdomain': '', 170 | 'httpauth': '', 171 | 'rport': 55499, 172 | 'lhost': '127.0.0.1', 173 | 'lport': 22 174 | }) 175 | self.tunnels.append({ 176 | 'protocol': 'udp', 177 | 'hostname': '', 178 | 'subdomain': '', 179 | 'httpauth': '', 180 | 'rport': 55499, 181 | 'lhost': '127.0.0.1', 182 | 'lport': 53 183 | }) 184 | 185 | @classmethod 186 | def from_file(cls, filename: str) -> 'NgrokConfig': 187 | """从配置文件加载配置""" 188 | config = cls() 189 | try: 190 | with open(filename, 'r') as f: 191 | data = json.load(f) 192 | config.server_host = data["server"]["host"] 193 | config.server_port = int(data["server"]["port"]) 194 | config.bufsize = int(data["server"].get("bufsize", 1024)) 195 | config.authtoken = data["server"].get("authtoken", "") 196 | config.tunnels = [ 197 | { 198 | 'protocol': t["protocol"], 199 | 'hostname': t.get("hostname", ""), 200 | 'subdomain': t.get("subdomain", ""), 201 | 'httpauth': t.get("httpauth", ""), 202 | 'rport': int(t.get("rport", 0)), 203 | 'lhost': t["lhost"], 204 | 'lport': int(t["lport"]) 205 | } 206 | for t in data["client"] 207 | ] 208 | except Exception as e: 209 | logger.error(f"配置文件加载失败: {str(e)}") 210 | raise 211 | return config 212 | 213 | # 代理连接处理器 214 | class ProxyConnection: 215 | def __init__(self, client: 'NgrokClient'): 216 | """初始化代理连接""" 217 | self.client = client 218 | self.url = None 219 | self.proxy_reader: Optional[asyncio.StreamReader] = None 220 | self.proxy_writer: Optional[asyncio.StreamWriter] = None 221 | self.local_reader: Optional[asyncio.StreamReader] = None 222 | self.local_writer: Optional[asyncio.StreamWriter] = None 223 | self.udp_transport: Optional[asyncio.DatagramTransport] = None 224 | self.local_queue: Optional[asyncio.Queue] = None 225 | self.tasks = [] 226 | self.running = True 227 | 228 | async def start(self): 229 | """启动代理连接全流程""" 230 | try: 231 | # 建立新的代理连接 232 | await self._connect_proxy_server() 233 | 234 | # 发送 RegProxy 注册消息 235 | try: 236 | regproxy_msg = RegProxy(ClientId=self.client.client_id) 237 | await self.client._send_packet(self.proxy_writer, regproxy_msg) 238 | except Exception as e: 239 | logger.debug(f"发送数据时发生错误: {str(e)}") 240 | return 241 | 242 | # 等待 StartProxy 消息 243 | try: 244 | msg = await self.client._recv_packet(self.proxy_reader) 245 | if not msg: 246 | return 247 | if not isinstance(msg, StartProxy): 248 | logger.debug("未收到 StartProxy 消息") 249 | return 250 | 251 | if not msg.Url: 252 | logger.debug("未收到有效 URL") 253 | return 254 | self.url = msg.Url 255 | except Exception as e: 256 | logger.debug(f"接收数据时发生错误: {str(e)}") 257 | return 258 | 259 | protocol = self.url.split(":")[0] 260 | if protocol == 'udp': 261 | # 连接到本地 UDP 服务 262 | await self._connect_local_service_udp() 263 | # 启动双向数据桥接 264 | await self._bridge_data_udp() 265 | return 266 | 267 | # 连接到本地 TCP 服务 268 | await self._connect_local_service_tcp() 269 | # 启动双向数据桥接 270 | await self._bridge_data_tcp() 271 | 272 | except Exception as e: 273 | logger.error(f"代理连接失败: {str(e)}") 274 | finally: 275 | await self._cleanup() 276 | 277 | async def _connect_proxy_server(self): 278 | """连接到代理服务器""" 279 | try: 280 | self.proxy_reader, self.proxy_writer = await asyncio.open_connection( 281 | host=self.client.config.server_host, 282 | port=self.client.config.server_port, 283 | ssl=self.client.ssl_ctx, 284 | server_hostname=self.client.config.server_host 285 | ) 286 | logger.debug(f"已建立代理连接到 {self.client.config.server_host}:{self.client.config.server_port}") 287 | except Exception as e: 288 | logger.error(f"代理服务器连接失败: {str(e)}") 289 | raise 290 | 291 | async def _connect_local_service_udp(self): 292 | """连接到本地 UDP 服务""" 293 | class LocalProtocol(asyncio.DatagramProtocol): 294 | def __init__(self, proxy_conn: ProxyConnection): 295 | self.proxy_conn = proxy_conn 296 | self.local_queue = asyncio.Queue() 297 | 298 | def datagram_received(self, data: bytes, addr: tuple[str, int]): 299 | """接收 UDP 数据""" 300 | self.local_queue.put_nowait(data) 301 | 302 | def error_received(self, exc: OSError): 303 | """处理 UDP 错误""" 304 | logger.error(f"UDP 错误: {exc}") 305 | 306 | local_host, local_port = self.client.tunnel_map[self.url] 307 | try: 308 | loop = asyncio.get_running_loop() 309 | transport, protocol = await loop.create_datagram_endpoint( 310 | lambda: LocalProtocol(self), 311 | remote_addr=(local_host, local_port) 312 | ) 313 | self.udp_transport = transport 314 | self.local_queue = protocol.local_queue 315 | logger.info(f"已连接到本地 UDP 服务 {local_host}:{local_port}") 316 | except Exception as e: 317 | logger.error(f"本地 UDP 服务连接失败: {str(e)}") 318 | raise 319 | 320 | async def _connect_local_service_tcp(self): 321 | """连接到本地 TCP 服务""" 322 | local_host, local_port = self.client.tunnel_map[self.url] 323 | try: 324 | self.local_reader, self.local_writer = await asyncio.open_connection( 325 | host=local_host, 326 | port=local_port 327 | ) 328 | logger.info(f"已连接到本地 TCP 服务 {local_host}:{local_port}") 329 | except Exception as e: 330 | logger.error(f"本地 TCP 服务连接失败: {str(e)}") 331 | raise 332 | 333 | async def _bridge_data_udp(self): 334 | """双向数据转发(UDP)""" 335 | async def tcp_to_udp(src: asyncio.StreamReader, label: str): 336 | """从 TCP 读取数据并转发到 UDP""" 337 | try: 338 | buffer = b'' 339 | while self.running: 340 | data = await src.read(self.client.config.bufsize) 341 | if not data: 342 | logger.debug(f"{label} 连接正常关闭") 343 | break 344 | buffer += data 345 | while len(buffer) >= 8: 346 | pkt_len, _ = struct.unpack(' 本地 UDP")) 379 | udp_task = asyncio.create_task(udp_to_tcp(self.local_queue, "服务端 TCP <- 本地 UDP")) 380 | self.tasks.extend([tcp_task, udp_task]) 381 | 382 | done, pending = await asyncio.wait({udp_task, tcp_task}, return_when=asyncio.FIRST_COMPLETED) 383 | 384 | for task in pending: 385 | task.cancel() 386 | await asyncio.gather(*pending, return_exceptions=True) 387 | 388 | async def _bridge_data_tcp(self): 389 | """双向数据转发(TCP)""" 390 | async def forward(src: asyncio.StreamReader, dst: asyncio.StreamWriter, label: str): 391 | """从源读取数据并转发到目标""" 392 | try: 393 | while self.running: 394 | data = await src.read(self.client.config.bufsize) 395 | if not data: 396 | logger.debug(f"{label} 连接正常关闭") 397 | break 398 | dst.write(data) 399 | await dst.drain() 400 | logger.debug(f"{label} 转发 {len(data)} bytes") 401 | except asyncio.CancelledError: 402 | pass 403 | except Exception as e: 404 | if self.running: 405 | logger.error(f"{label} 转发错误: {str(e)}") 406 | 407 | task1 = asyncio.create_task( 408 | forward(self.proxy_reader, self.local_writer, "服务端 TCP -> 本地 TCP") 409 | ) 410 | task2 = asyncio.create_task( 411 | forward(self.local_reader, self.proxy_writer, "服务端 TCP <- 本地 TCP") 412 | ) 413 | self.tasks.extend([task1, task2]) 414 | 415 | done, pending = await asyncio.wait({task1, task2}, return_when=asyncio.FIRST_COMPLETED) 416 | 417 | for task in pending: 418 | task.cancel() 419 | await asyncio.gather(*pending, return_exceptions=True) 420 | 421 | async def _cleanup(self): 422 | """资源清理""" 423 | self.running = False 424 | 425 | # 取消本连接创建的所有任务 426 | for task in self.tasks: 427 | task.cancel() 428 | await asyncio.gather(*self.tasks, return_exceptions=True) 429 | 430 | writers = [self.proxy_writer, self.local_writer] 431 | for writer in writers: 432 | if writer and not writer.is_closing(): 433 | try: 434 | writer.close() 435 | await writer.wait_closed() 436 | except Exception as e: 437 | logger.debug(f"关闭 writer 时发生错误: {str(e)}") 438 | 439 | if self.udp_transport: 440 | try: 441 | self.udp_transport.close() 442 | except Exception as e: 443 | logger.debug(f"关闭 UDP 传输时发生错误: {str(e)}") 444 | 445 | if self.local_queue is not None: 446 | try: 447 | self.local_queue.put_nowait(None) 448 | except asyncio.QueueFull: 449 | await self.local_queue.put(None) 450 | 451 | # 从客户端移除本连接 452 | async with self.client.lock: 453 | if self in self.client.proxy_connections: 454 | self.client.proxy_connections.remove(self) 455 | 456 | # Ngrok 客户端类 457 | class NgrokClient: 458 | def __init__(self, config: NgrokConfig): 459 | """初始化 Ngrok 客户端""" 460 | self.config = config 461 | self.client_id = '' # 客户端ID 462 | self.last_ping = 0.0 # 上次心跳时间 463 | self.current_retry_interval = 1 # 当前重试间隔 464 | self.max_retry_interval = 60 # 最大重试间隔 465 | self.main_loop_task = None # 主循环任务 466 | self.main_reader: Optional[asyncio.StreamReader] = None # 主连接读取器 467 | self.main_writer: Optional[asyncio.StreamWriter] = None # 主连接写入器 468 | self.ssl_ctx = self._create_ssl_context() # SSL 上下文 469 | self.req_map: dict[str, tuple[str, int]] = {} # 请求ID到本地地址的映射 470 | self.tunnel_map: dict[str, tuple[str, int]] = {} # 隧道URL到本地地址的映射 471 | self.proxy_connections = [] # 代理连接列表 472 | self.lock = asyncio.Lock() # 异步锁 473 | self.running = True # 运行状态 474 | self._validate_tunnels() # 验证隧道配置 475 | 476 | def _create_ssl_context(self) -> ssl.SSLContext: 477 | """创建 SSL 上下文""" 478 | ctx = ssl.create_default_context() 479 | ctx.check_hostname = False 480 | ctx.verify_mode = ssl.CERT_NONE 481 | return ctx 482 | 483 | def _validate_tunnels(self): 484 | """验证隧道配置有效性""" 485 | required_fields = ['protocol', 'lhost', 'lport'] 486 | for t in self.config.tunnels: 487 | for field in required_fields: 488 | if field not in t: 489 | raise ValueError(f"隧道配置缺少必要字段: {field}") 490 | 491 | async def _connect_server(self): 492 | """连接到服务器""" 493 | try: 494 | self.main_reader, self.main_writer = await asyncio.open_connection( 495 | host=self.config.server_host, 496 | port=self.config.server_port, 497 | ssl=self.ssl_ctx, 498 | server_hostname=self.config.server_host 499 | ) 500 | logger.info(f"成功连接到服务器 {self.config.server_host}:{self.config.server_port}") 501 | except ConnectionRefusedError: 502 | logger.error(f"服务器拒绝连接: {self.config.server_host}:{self.config.server_port}") 503 | raise 504 | except Exception as e: 505 | logger.error(f"服务器连接失败: {str(e)}") 506 | raise 507 | 508 | async def _handle_auth(self): 509 | """处理认证流程""" 510 | auth_msg = Auth(ClientId=self.client_id, User=self.config.authtoken) 511 | await self._send_packet(self.main_writer, auth_msg) 512 | 513 | def dict_to_message(self, msg: dict): 514 | """ 515 | 将字典转换为消息类型 516 | """ 517 | msg_type = msg.get("Type") 518 | payload = msg.get("Payload", {}) 519 | msg_classes = { 520 | "Auth": Auth, 521 | "AuthResp": AuthResp, 522 | "ReqTunnel": ReqTunnel, 523 | "NewTunnel": NewTunnel, 524 | "ReqProxy": ReqProxy, 525 | "RegProxy": RegProxy, 526 | "StartProxy": StartProxy, 527 | "Ping": Ping, 528 | "Pong": Pong 529 | } 530 | if msg_type in msg_classes: 531 | cls = msg_classes[msg_type] 532 | payload = {k: payload[k] for k in payload if k in {f.name for f in fields(cls)}} 533 | return cls(**payload) 534 | else: 535 | raise ValueError(f"未知消息类型: {msg_type}") 536 | 537 | async def _recv_packet(self, reader: asyncio.StreamReader) -> Optional[MessageType]: 538 | """接收协议数据包""" 539 | header = await reader.read(8) 540 | if not header: 541 | logger.debug("连接已关闭") 542 | return 543 | msg_len, _ = struct.unpack(' 20: 657 | try: 658 | await self._send_packet(self.main_writer, Ping()) 659 | self.last_ping = time.time() 660 | except Exception as e: 661 | logger.debug(f"发送心跳失败: {str(e)}") 662 | self.running = False 663 | await asyncio.sleep(1) 664 | 665 | async def _main_loop(self): 666 | """业务逻辑主循环""" 667 | self.running = True 668 | self.main_loop_task = asyncio.gather( 669 | self._recv_loop(), 670 | self._heartbeat_task() 671 | ) 672 | try: 673 | try: 674 | await self._handle_auth() 675 | except Exception as e: 676 | logger.debug(f"发送数据时发生错误: {str(e)}") 677 | raise 678 | # 启动接收和心跳任务 679 | await self.main_loop_task 680 | except asyncio.CancelledError: 681 | logger.debug("主循环任务被取消") 682 | raise 683 | 684 | async def _connect_with_retry(self): 685 | """带指数退避的连接方法""" 686 | while True: 687 | try: 688 | await self._connect_server() 689 | self.current_retry_interval = 1 690 | return 691 | except Exception as e: 692 | logger.error(f"连接失败,{self.current_retry_interval}秒后重试...") 693 | try: 694 | # 等待重连间隔 695 | await asyncio.sleep(self.current_retry_interval) 696 | except asyncio.CancelledError: 697 | logger.debug("重连等待被中断") 698 | raise 699 | self.current_retry_interval = min( 700 | self.current_retry_interval * 2, 701 | self.max_retry_interval 702 | ) 703 | 704 | async def start(self): 705 | """启动客户端主循环""" 706 | while True: 707 | try: 708 | await self._connect_with_retry() 709 | await self._main_loop() 710 | except Exception as e: 711 | logger.error(f"运行时异常: {str(e)}") 712 | finally: 713 | await self._cleanup_resources() 714 | await self._wait_for_reconnect() 715 | 716 | logger.info("客户端已停止") 717 | 718 | if __name__ == '__main__': 719 | try: 720 | # 从配置文件加载配置,或使用默认配置 721 | config = NgrokConfig.from_file(sys.argv[1]) if len(sys.argv) > 1 else NgrokConfig() 722 | client = NgrokClient(config) 723 | asyncio.run(client.start()) 724 | except KeyboardInterrupt: 725 | logger.info("用户中断操作") 726 | except Exception as e: 727 | logger.error(f"客户端异常终止: {str(e)}") 728 | -------------------------------------------------------------------------------- /python-ngrok_gevent.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | # 建议Python 2.7.9 或 Python 3.4.2 以上运行 4 | # 项目地址: https://github.com/hauntek/python-ngrok 5 | # Version: v1.56 gevent 6 | from gevent import monkey; monkey.patch_all() 7 | 8 | import socket 9 | import ssl 10 | import json 11 | import struct 12 | import random 13 | import sys 14 | import time 15 | import logging 16 | # import threading 17 | 18 | import gevent 19 | 20 | host = 'tunnel.qydev.com' # Ngrok服务器地址 21 | port = 4443 # 端口 22 | bufsize = 1024 # 吞吐量 23 | 24 | dualstack = 'IPv4/IPv6' # 服务连接协议 [IPv4/IPv6=双栈] 25 | dualstack_or = 0 # 本地转发协议 [0=双栈, 1=IPv4, 2=IPv6] 26 | 27 | Tunnels = list() # 全局渠道赋值 28 | body = dict() 29 | body['protocol'] = 'http' 30 | body['hostname'] = 'www.xxx.com' 31 | body['subdomain'] = '' 32 | body['rport'] = 0 33 | body['lhost'] = '127.0.0.1' 34 | body['lport'] = 80 35 | Tunnels.append(body) # 加入渠道队列 36 | 37 | body = dict() 38 | body['protocol'] = 'http' 39 | body['hostname'] = '' 40 | body['subdomain'] = 'xxx' 41 | body['rport'] = 0 42 | body['lhost'] = '127.0.0.1' 43 | body['lport'] = 80 44 | Tunnels.append(body) # 加入渠道队列 45 | 46 | body = dict() 47 | body['protocol'] = 'tcp' 48 | body['hostname'] = '' 49 | body['subdomain'] = '' 50 | body['rport'] = 55499 51 | body['lhost'] = '127.0.0.1' 52 | body['lport'] = 22 53 | Tunnels.append(body) # 加入渠道队列 54 | 55 | reqIdaddr = dict() 56 | localaddr = dict() 57 | 58 | # 读取配置文件 59 | if len(sys.argv) >= 2: 60 | file_object = open(sys.argv[1]) 61 | try: 62 | all_the_text = file_object.read() 63 | config_object = json.loads(all_the_text) 64 | host = config_object["server"]["host"] # Ngrok服务器地址 65 | port = int(config_object["server"]["port"]) # 端口 66 | bufsize = int(config_object["server"]["bufsize"]) # 吞吐量 67 | Tunnels = list() # 重置渠道赋值 68 | for Tunnel in config_object["client"]: 69 | body = dict() 70 | body['protocol'] = Tunnel["protocol"] 71 | body['hostname'] = Tunnel["hostname"] 72 | body['subdomain'] = Tunnel["subdomain"] 73 | body['rport'] = int(Tunnel["rport"]) 74 | body['lhost'] = Tunnel["lhost"] 75 | body['lport'] = int(Tunnel["lport"]) 76 | Tunnels.append(body) # 加入渠道队列 77 | del all_the_text 78 | del config_object 79 | except Exception: 80 | # logger = logging.getLogger('%s' % 'config') 81 | # logger.error('The configuration file read failed') 82 | # exit(1) 83 | pass 84 | finally: 85 | file_object.close() 86 | 87 | mainsocket = 0 88 | 89 | ClientId = '' 90 | pingtime = 0 91 | 92 | def connectremote(host, port): 93 | ipv4_addr = list() 94 | ipv6_addr = list() 95 | 96 | for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_STREAM): 97 | af, socktype, proto, canonname, sa = res 98 | 99 | if dualstack == 'IPv4' or dualstack == 'IPv4/IPv6': 100 | if af == socket.AF_INET: ipv4_addr.append(res) 101 | if dualstack == 'IPv6' or dualstack == 'IPv4/IPv6': 102 | if af == socket.AF_INET6: ipv6_addr.append(res) 103 | 104 | if dualstack == 'IPv6' or dualstack == 'IPv4/IPv6': 105 | if len(ipv6_addr) > 0: 106 | for res in ipv6_addr: 107 | af, socktype, proto, canonname, sa = res 108 | try: 109 | client = socket.socket(af, socktype, proto) 110 | ssl_client = ssl.wrap_socket(client, ssl_version=ssl.PROTOCOL_SSLv23) 111 | except socket.error: 112 | continue 113 | try: 114 | ssl_client.connect(sa) 115 | ssl_client.setblocking(1) 116 | logger = logging.getLogger('%s:%d' % ('Conn', ssl_client.fileno())) 117 | logger.debug('New connection to: %s:%d' % (host, port)) 118 | 119 | return ssl_client 120 | except socket.error: 121 | continue 122 | 123 | if dualstack == 'IPv4' or dualstack == 'IPv4/IPv6': 124 | if len(ipv4_addr) > 0: 125 | for res in ipv4_addr: 126 | af, socktype, proto, canonname, sa = res 127 | try: 128 | client = socket.socket(af, socktype, proto) 129 | ssl_client = ssl.wrap_socket(client, ssl_version=ssl.PROTOCOL_SSLv23) 130 | except socket.error: 131 | continue 132 | try: 133 | ssl_client.connect(sa) 134 | ssl_client.setblocking(1) 135 | logger = logging.getLogger('%s:%d' % ('Conn', ssl_client.fileno())) 136 | logger.debug('New connection to: %s:%d' % (host, port)) 137 | 138 | return ssl_client 139 | except socket.error: 140 | continue 141 | 142 | return False 143 | 144 | def connectlocal(localhost, localport): 145 | ipv4_addr = list() 146 | ipv6_addr = list() 147 | 148 | for res in socket.getaddrinfo(localhost, localport, socket.AF_UNSPEC, socket.SOCK_STREAM): 149 | af, socktype, proto, canonname, sa = res 150 | 151 | if dualstack_or == 1 or dualstack_or == 0: 152 | if af == socket.AF_INET: ipv4_addr.append(res) 153 | if dualstack_or == 2 or dualstack_or == 0: 154 | if af == socket.AF_INET6: ipv6_addr.append(res) 155 | 156 | if dualstack_or == 2 or dualstack_or == 0: 157 | if len(ipv6_addr) > 0: 158 | for res in ipv6_addr: 159 | af, socktype, proto, canonname, sa = res 160 | try: 161 | client = socket.socket(af, socktype, proto) 162 | except socket.error: 163 | continue 164 | try: 165 | client.connect(sa) 166 | client.setblocking(1) 167 | logger = logging.getLogger('%s:%d' % ('Conn', client.fileno())) 168 | logger.debug('New connection to: %s:%d' % (host, port)) 169 | 170 | return client 171 | except socket.error: 172 | continue 173 | 174 | if dualstack_or == 1 or dualstack_or == 0: 175 | if len(ipv4_addr) > 0: 176 | for res in ipv4_addr: 177 | af, socktype, proto, canonname, sa = res 178 | try: 179 | client = socket.socket(af, socktype, proto) 180 | except socket.error: 181 | continue 182 | try: 183 | client.connect(sa) 184 | client.setblocking(1) 185 | logger = logging.getLogger('%s:%d' % ('Conn', client.fileno())) 186 | logger.debug('New connection to: %s:%d' % (host, port)) 187 | 188 | return client 189 | except socket.error: 190 | continue 191 | 192 | return False 193 | 194 | def NgrokAuth(): 195 | Payload = dict() 196 | Payload['ClientId'] = '' 197 | Payload['OS'] = 'darwin' 198 | Payload['Arch'] = 'amd64' 199 | Payload['Version'] = '2' 200 | Payload['MmVersion'] = '1.7' 201 | Payload['User'] = 'user' 202 | Payload['Password'] = '' 203 | body = dict() 204 | body['Type'] = 'Auth' 205 | body['Payload'] = Payload 206 | buffer = json.dumps(body) 207 | return(buffer) 208 | 209 | def ReqTunnel(ReqId, Protocol, Hostname, Subdomain, RemotePort): 210 | Payload = dict() 211 | Payload['ReqId'] = ReqId 212 | Payload['Protocol'] = Protocol 213 | Payload['Hostname'] = Hostname 214 | Payload['Subdomain'] = Subdomain 215 | Payload['HttpAuth'] = '' 216 | Payload['RemotePort'] = RemotePort 217 | body = dict() 218 | body['Type'] = 'ReqTunnel' 219 | body['Payload'] = Payload 220 | buffer = json.dumps(body) 221 | return(buffer) 222 | 223 | def RegProxy(ClientId): 224 | Payload = dict() 225 | Payload['ClientId'] = ClientId 226 | body = dict() 227 | body['Type'] = 'RegProxy' 228 | body['Payload'] = Payload 229 | buffer = json.dumps(body) 230 | return(buffer) 231 | 232 | def Ping(): 233 | Payload = dict() 234 | body = dict() 235 | body['Type'] = 'Ping' 236 | body['Payload'] = Payload 237 | buffer = json.dumps(body) 238 | return(buffer) 239 | 240 | def lentobyte(len): 241 | return struct.pack(' 0: 300 | if not recvbuf: 301 | recvbuf = recvbut 302 | else: 303 | recvbuf += recvbut 304 | 305 | if type == 1 or (type == 2 and linkstate == 1): 306 | lenbyte = tolen(recvbuf[0:8]) 307 | if len(recvbuf) >= (8 + lenbyte): 308 | buf = recvbuf[8:lenbyte + 8].decode('utf-8') 309 | logger = logging.getLogger('%s:%d' % ('Recv', sock.fileno())) 310 | logger.debug('Reading message with length: %d' % len(buf)) 311 | logger.debug('Read message: %s' % buf) 312 | js = json.loads(buf) 313 | if type == 1: 314 | if js['Type'] == 'ReqProxy': 315 | newsock = connectremote(host, port) 316 | if newsock: 317 | gevent.spawn(HKClient, newsock, 0, 2) 318 | if js['Type'] == 'AuthResp': 319 | ClientId = js['Payload']['ClientId'] 320 | logger = logging.getLogger('%s' % 'client') 321 | logger.info('Authenticated with server, client id: %s' % ClientId) 322 | sendpack(sock, Ping()) 323 | pingtime = time.time() 324 | for info in Tunnels: 325 | reqid = getRandChar(8) 326 | sendpack(sock, ReqTunnel(reqid, info['protocol'], info['hostname'], info['subdomain'], info['rport'])) 327 | reqIdaddr[reqid] = (info['lhost'], info['lport']) 328 | if js['Type'] == 'NewTunnel': 329 | if js['Payload']['Error'] != '': 330 | logger = logging.getLogger('%s' % 'client') 331 | logger.error('Server failed to allocate tunnel: %s' % js['Payload']['Error']) 332 | time.sleep(30) 333 | else: 334 | logger = logging.getLogger('%s' % 'client') 335 | logger.info('Tunnel established at %s' % js['Payload']['Url']) 336 | localaddr[js['Payload']['Url']] = reqIdaddr[js['Payload']['ReqId']] 337 | if type == 2: 338 | if js['Type'] == 'StartProxy': 339 | localhost, localport = localaddr[js['Payload']['Url']] 340 | 341 | newsock = connectlocal(localhost, localport) 342 | if newsock: 343 | gevent.spawn(HKClient, newsock, 0, 3, sock) 344 | tosock = newsock 345 | linkstate = 2 346 | else: 347 | body = '

Tunnel %s unavailable

Unable to initiate connection to %s. This port is not yet available for web server.

' 348 | html = body % (js['Payload']['Url'], localhost + ':' + str(localport)) 349 | header = "HTTP/1.0 502 Bad Gateway" + "\r\n" 350 | header += "Content-Type: text/html" + "\r\n" 351 | header += "Content-Length: %d" + "\r\n" 352 | header += "\r\n" + "%s" 353 | buf = header % (len(html.encode('utf-8')), html) 354 | sendbuf(sock, buf.encode('utf-8')) 355 | 356 | if len(recvbuf) == (8 + lenbyte): 357 | recvbuf = bytes() 358 | else: 359 | recvbuf = recvbuf[8 + lenbyte:] 360 | 361 | if type == 3 or (type == 2 and linkstate == 2): 362 | sendbuf(tosock, recvbuf) 363 | recvbuf = bytes() 364 | 365 | except socket.error: 366 | break 367 | 368 | if type == 1: 369 | mainsocket = False 370 | if type == 3: 371 | try: 372 | tosock.shutdown(socket.SHUT_WR) 373 | except socket.error: 374 | tosock.close() 375 | 376 | logger = logging.getLogger('%s:%d' % ('Close', sock.fileno())) 377 | logger.debug('Closing') 378 | sock.close() 379 | 380 | # 客户端程序初始化 381 | if __name__ == '__main__': 382 | logging.basicConfig(level=logging.DEBUG, format='[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s', datefmt='%Y/%m/%d %H:%M:%S') 383 | logger = logging.getLogger('%s' % 'client') 384 | logger.info('python-ngrok v1.56') 385 | while True: 386 | try: 387 | # 检测控制连接是否连接. 388 | if mainsocket == False: 389 | mainsocket = connectremote(host, port) 390 | if mainsocket == False: 391 | logger = logging.getLogger('%s' % 'client') 392 | logger.info('connect failed...!') 393 | time.sleep(10) 394 | continue 395 | gevent.spawn(HKClient, mainsocket, 0, 1) 396 | 397 | # 发送心跳 398 | if pingtime + 20 < time.time() and pingtime != 0: 399 | sendpack(mainsocket, Ping()) 400 | pingtime = time.time() 401 | 402 | time.sleep(1) 403 | 404 | except socket.error: 405 | pingtime = 0 406 | except KeyboardInterrupt: 407 | sys.exit() 408 | --------------------------------------------------------------------------------