├── Dockerfile ├── LICENSE ├── README.md ├── conf ├── conf.d │ ├── demo.conf │ └── gw.conf └── nginx.conf ├── docker-compose.yml └── lua ├── access_check.lua ├── configs.lua ├── init_by_lua.lua ├── jwt_token.lua ├── mq.lua ├── my_verify.lua ├── plugin ├── auth │ └── auth_check.lua ├── limit │ └── limit_req.lua └── logs │ └── init_log.lua ├── redis.lua ├── resty ├── .DS_Store ├── ._.DS_Store ├── evp.lua ├── hmac.lua ├── http.lua ├── http_headers.lua ├── jwt-validators.lua ├── jwt.lua └── rabbitmqstomp.lua ├── test ├── login.lua ├── lua.sql ├── mysql.lua └── register.lua ├── tools.lua └── upstream.lua /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openresty/openresty:1.15.8.1-1-centos 2 | 3 | MAINTAINER "shenshuo<191715030@qq.com>" 4 | 5 | ENV LANG en_US.UTF-8 6 | # 同步时间 7 | ENV TZ=Asia/Shanghai 8 | RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone 9 | 10 | COPY . /usr/local/openresty/nginx/ 11 | VOLUME /var/log/ 12 | VOLUME /usr/local/openresty/nginx/logs/ 13 | EXPOSE 80 14 | CMD ["/usr/bin/openresty", "-g", "daemon off;"] -------------------------------------------------------------------------------- /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 | # API网关项目介绍 2 | API网关系统,是基于openresty + Lua开发的一套API网关系统,主要功能如下: 3 | 4 | - API鉴权 5 | 6 | - API 限速 7 | 8 | - 日志记录 9 | 10 | - 白名单 (未完成) 11 | 12 | - 熔断 (未完成) 13 | 14 | # 一、服务部署 15 | #### openresty 编译安装 16 | ``` 17 | wget https://openresty.org/download/openresty-1.13.6.2.tar.gz 18 | tar zxf openresty-1.13.6.2.tar.gz && cd openresty-1.13.6.2 19 | ./configure --prefix=/usr/local/openresty-1.13.6.2 \ 20 | --with-luajit --with-http_stub_status_module \ 21 | --with-pcre --with-pcre-jit 22 | gmake && gmake install 23 | ln -s /usr/local/openresty-1.13.6.2/ /usr/local/openresty 24 | ln -s /usr/local/openresty/bin/resty /usr/bin/resty 25 | ``` 26 | 27 | #### yum安装 28 | ```bash 29 | # yum部署 30 | yum install yum-utils 31 | yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo 32 | yum install openresty 33 | yum install openresty-resty 34 | ``` 35 | ##### 代码部署 36 | ```bash 37 | \cp -arp api-gateway/* /usr/local/openresty/nginx/ 38 | ``` 39 | 40 | # 二、 修改配置 41 | ##### 文件 /usr/local/openresty/nginx/conf/nginx.conf 42 | - 修改 resolver 172.16.0.21; 为resolver DNS服务器。 43 | ##### 文件 /usr/local/openresty/nginx/conf/conf.d/gw.conf 44 | - 修改 lua_code_cache on; 线上环境设置为on 45 | - 修改 server_name 为你的网关域名 46 | ##### 文件 /usr/local/openresty/nginx/lua/configs.lua 47 | - token_secret 为你的令牌的密钥 和登录JWT 服务的key一致 48 | - rewrite_cache_url 刷新权限到redis接口 49 | - rewrite_cache_token 为获取权限的令牌 50 | #### - login_url 当token 无效或者过期 跳转的登录页面 51 | - limit_conf 并发 限制默认即可 如有需求下面有详细介绍 52 | - rewrite_conf 注册API 下面有详解 53 | 54 | 55 | 56 | # 三、使用配置,注册API 57 | > 要接入API网关系统,则要先进行注册,注册方式如下: 58 | 59 | ​ a、配置文件configs.lua中的rewrite_conf 60 | 61 | ​ b、POST注册接口(暂无) 62 | 63 | 注册示例如下: 64 | 上级nginx 配置示例 65 | ``` 66 | server { 67 | listen 80; 68 | server_name demo.opendevops.cn 69 | access_log /var/log/nginx/ops-demo_access.log; 70 | error_log /var/log/nginx/ops-demo_error.log; 71 | 72 | location / { 73 | root /var/www/admin-front; 74 | index index.html index.htm; 75 | try_files $uri $uri/ /index.html; 76 | expires 3d; 77 | } 78 | location /api { 79 | proxy_redirect off; 80 | proxy_read_timeout 600; 81 | proxy_http_version 1.1; 82 | proxy_set_header Upgrade $http_upgrade; 83 | proxy_set_header Connection "upgrade"; 84 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 85 | 86 | add_header 'Access-Control-Allow-Origin' '*'; 87 | proxy_pass http://gw.opendevops.cn; 88 | } 89 | location ~ /(.svn|.git|admin|manage|.sh|.bash)$ { 90 | return 403; 91 | } 92 | } 93 | ``` 94 | - 通过 api代理到api网关, 网关会把URI的第一位删除, 第二位是服务标示,之后的才是真正后端的API的URI地址,当然权限还是会从服务标示开始匹配 95 | ```lua 96 | gw_domain_name = 'gw.opendevops.cn' 97 | 98 | rewrite_conf = { 99 | [gw_domain_name] = { 100 | rewrite_urls = { 101 | { 102 | uri = "/cmdb", 103 | rewrite_upstream = "172.16.80.12:8000" 104 | }, 105 | { 106 | uri = "/task", 107 | rewrite_upstream = "172.16.0.223:8900" 108 | }, 109 | { 110 | uri = "/cron", 111 | rewrite_upstream = "172.16.0.223:9900" 112 | }, 113 | { 114 | uri = "/mg", 115 | rewrite_upstream = "172.16.0.223:9800" 116 | }, 117 | { 118 | uri = "/accounts", 119 | rewrite_upstream = "172.16.0.223:9800" 120 | }, 121 | } 122 | } 123 | } 124 | ``` 125 | 126 | 127 | 128 | 如上可以看到,注册了的服务【cron】【mg】【accounts】 129 | accounts 做过处理 不用经过鉴权 130 | 131 | 132 | 133 | # 四、API鉴权权限 134 | 135 | 在configs.lua文件中配置redis信息和刷新redis权限接口信息,此信息由【权限系统】提供 136 | 137 | 权限验证步骤如下: 138 | 139 | - 获取cook信息,得到auth_key 140 | - 根据私钥及加密算法解密auth_key 141 | - 得到用户ID 142 | - 获取当前uri及method 143 | - redis中查询用户id的权限列表进行匹配 144 | - 匹配不通过则rewrite login 145 | 146 | 147 | 148 | 在这里来测试 devops服务的job接口 149 | 150 | ​ 原接口地址:http://mg.opendevops.cn/xxxx/ 151 | 152 | ​ 现接口地址:http://gw.opendevops.cn/mg/xxxx/ 153 | 154 | 测试: 155 | 156 | ​ 首次访问 http://gw.opendevops.cn/mg/xxxx/ 会返回 401错误,表示未登路 157 | http://gw.opendevops.cn/accounts/login/ 158 | 使用post 模拟登录 159 | ``` 160 | { 161 | "username":"ss", 162 | "password":"shenshuo", 163 | "dynamic":"010073" 164 | } 165 | ``` 166 | ​ 登录成功,再次访问进行uri鉴权,鉴权成功则如下: 167 | http://gw.opendevops.cn/mg/v2/sysconfig/settings/STORAGE/ 168 | ``` 169 | { 170 | "code": 0, 171 | "msg": "获取配置成功", 172 | "data": {} 173 | } 174 | ``` 175 | 176 | 177 | # 四、API限速 178 | 179 | 在configs.lua文件中配置limit,配置示例如下 180 | 181 | ```lua 182 | limit_conf = { 183 | rate = 5, --限制ip每分钟只能调用n*60次接口 184 | burst = 10, --桶容量,用于平滑处理,最大接收请求次数 185 | } 186 | ``` 187 | 188 | 次配置为每秒5个并发请求,并临时允许超出10个请求并平滑处理掉: 189 | 190 | 测试:(最好先关闭权限验证,方便测试) 191 | 192 | ```shell 193 | ab -c 100 -n 1000 http://gw.opendevops.cn/cron/v1/cron/log/ 194 | ``` 195 | 可以看到,差不多有21个请求是成功的 196 | ```bash 197 | Document Path: /cron/v1/cron/log/ 198 | Document Length: 11852 bytes 199 | 200 | Concurrency Level: 100 201 | Time taken for tests: 3.982 seconds 202 | Complete requests: 1000 203 | Failed requests: 979 204 | ``` 205 | 206 | 再试试 并发5个请求 如下: 207 | ```shell 208 | ab -c 5 -n 1000 http://gw.opendevops.cn/cron/v1/cron/log/ 209 | ``` 210 | ```bash 211 | Document Path: /cron/v1/cron/log/ 212 | Document Length: 11852 bytes 213 | 214 | Concurrency Level: 5 215 | Time taken for tests: 199.811 seconds 216 | Complete requests: 1000 217 | Failed requests: 0 218 | Write errors: 0 219 | ``` 220 | 221 | 222 | 223 | # 五、日志记录 224 | 225 | 在configs.lua文件中配置log地址及redis channel 226 | 227 | - get请求日志会访日本地log 228 | - 非get请求会发送给redis channel 需要自己接受记录 229 | 230 | ```bash 231 | [root@CentOS7-Shinezone /var/log]#tailf gw.log 232 | {"time":"2018-09-19 10:44:48","uri":"\/devops\/api\/v1.0\/job\/","login_ip":"172.16.0.121","method":"GET"} 233 | {"time":"2018-09-19 10:44:48","uri":"\/devops\/api\/v1.0\/job\/","login_ip":"172.16.0.121","method":"GET"} 234 | ``` 235 | 236 | ``` 237 | [root@CentOS7-Shinezone /var/log]#redis-cli -h 127.0.0.1 -p 6379 238 | 127.0.0.1:6379> SUBSCRIBE gw 239 | Reading messages... (press Ctrl-C to quit) 240 | 1) "subscribe" 241 | 2) "gw" 242 | 3) (integer) 1 243 | 244 | 245 | 1) "message" 246 | 2) "gw" 247 | 3) "{\"time\":\"2018-09-19 10:48:52\",\"uri\":\"\\/devops\\/api\\/v1.0\\/job\\/\",\"login_ip\":\"172.16.80.12\",\"method\":\"POST\"}" 248 | ``` 249 | 250 | # docker 部署 251 | 252 | **配置修改参考上述内容** 253 | 254 | ``` 255 | #删除前端的配置文件 256 | mv conf/conf.d/demo.conf conf/conf.d/demo.conf-bak 257 | 258 | #bulid镜像 259 | docker build . -t gateway_image 260 | 261 | #启动 262 | docker-compose up -d 263 | ``` 264 | **使用docker部署启动之后端口为8888,防止单机部署造成端口冲突,如果想修改端口请修改`docker-compose.yml`文件。** 265 | 266 | **默认域名:`http://gw.opendevops.cn:8888` 如果需要修改域名请修改`conf/conf.d/gw.conf`文件。** 267 | 268 | ## License 269 | 270 | Everything is [GPL v3.0](https://www.gnu.org/licenses/gpl-3.0.html). -------------------------------------------------------------------------------- /conf/conf.d/demo.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name demo.opendevops.cn; 4 | access_log /var/log/nginx/f_access.log; 5 | error_log /var/log/nginx/f_error.log; 6 | root /var/www/codo; 7 | 8 | location / { 9 | root /var/www/codo; 10 | index index.html index.htm; 11 | try_files $uri $uri/ /index.html; 12 | } 13 | 14 | location /api { 15 | ### ws 支持 16 | proxy_http_version 1.1; 17 | proxy_set_header Upgrade $http_upgrade; 18 | proxy_set_header Connection "upgrade"; 19 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 20 | 21 | add_header 'Access-Control-Allow-Origin' '*'; 22 | proxy_pass http://gw.opendevops.cn; 23 | } 24 | 25 | location ~ /(.svn|.git|admin|manage|.sh|.bash)$ { 26 | return 403; 27 | } 28 | } -------------------------------------------------------------------------------- /conf/conf.d/gw.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name gw.opendevops.cn; 4 | lua_need_request_body on; # 开启获取body数据记录日志 5 | 6 | location / { 7 | ### ws 支持 8 | proxy_http_version 1.1; 9 | proxy_set_header Upgrade $http_upgrade; 10 | proxy_set_header Connection "upgrade"; 11 | ### 12 | proxy_redirect off; 13 | proxy_read_timeout 600; 14 | 15 | ### 获取真实IP 16 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 17 | 18 | access_by_lua_file lua/access_check.lua; 19 | set $my_upstream $my_upstream; 20 | proxy_pass http://$my_upstream; 21 | 22 | ### 跨域 23 | add_header Access-Control-Allow-Methods *; 24 | add_header Access-Control-Max-Age 3600; 25 | add_header Access-Control-Allow-Credentials true; 26 | add_header Access-Control-Allow-Origin $http_origin; 27 | add_header Access-Control-Allow-Headers $http_access_control_request_headers; 28 | if ($request_method = OPTIONS){ 29 | return 204;} 30 | } 31 | location ~ .*\.(sh|bash|py|sql)$ { 32 | return 403; 33 | } 34 | 35 | location ~* ^/(image.*|admin.*|manage.*)/ { 36 | return 403; 37 | } 38 | } -------------------------------------------------------------------------------- /conf/nginx.conf: -------------------------------------------------------------------------------- 1 | user root; 2 | worker_processes auto; 3 | worker_rlimit_nofile 51200; 4 | error_log logs/error.log; 5 | events { 6 | use epoll; 7 | worker_connections 51024; 8 | } 9 | http { 10 | #设置默认lua搜索路径 11 | lua_package_path '$prefix/lua/?.lua;/blah/?.lua;;'; 12 | lua_code_cache on; #线上环境设置为on, off时可以热加载lua文件 13 | lua_shared_dict user_info 1m; 14 | lua_shared_dict my_limit_conn_store 100m; #100M可以放1.6M个键值对 15 | include mime.types; #代理静态文件 16 | 17 | client_header_buffer_size 64k; 18 | large_client_header_buffers 4 64k; 19 | 20 | gzip on; 21 | gzip_min_length 1k; 22 | gzip_comp_level 2; 23 | gzip_types text/plain application/x-javascript text/css application/xml text/javascript; 24 | gzip_vary on; 25 | gzip_buffers 4 16k; 26 | gzip_http_version 1.1; 27 | 28 | init_by_lua_file lua/init_by_lua.lua; # nginx启动时就会执行 29 | include ./conf.d/*.conf; # lua生成upstream 30 | resolver 172.16.0.21; # 内部DNS 31 | } 32 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | gateway: 2 | restart: unless-stopped 3 | image: gateway_image 4 | volumes: 5 | - /var/log/:/var/log/ 6 | - /usr/local/openresty/nginx/logs/:/usr/local/openresty/nginx/logs/ 7 | - /sys/fs/cgroup:/sys/fs/cgroup 8 | ports: 9 | - "8888:80" -------------------------------------------------------------------------------- /lua/access_check.lua: -------------------------------------------------------------------------------- 1 | local limit_req = require "plugin.limit.limit_req" 2 | local auth_check = require "plugin.auth.auth_check" 3 | local init_log = require "plugin.logs.init_log" 4 | local upstream = require "upstream" 5 | local tools = require "tools" 6 | 7 | limit_req.incoming() --限速,限制每秒请求数 8 | 9 | -- 获取访问URI 10 | local url_path_list = tools.split(ngx.var.request_uri, '/') 11 | local svc_code = url_path_list[2] -- 去第二位 12 | -- 13 | table.remove(url_path_list,1) 14 | local real_new_uri = tools.list_to_str(url_path_list,'/') 15 | -- 16 | -- 不用鉴权的URI 17 | if svc_code == 'accounts' or svc_code == 'favicon.ico' then 18 | ngx.log(ngx.ERR, 'acc-->>>', svc_code) 19 | else 20 | -- 获取真实的URI 21 | auth_check.check(real_new_uri) --权限验证 22 | init_log.send(real_new_uri) --记录日志 23 | end 24 | upstream.set(real_new_uri) --匹配upstream -------------------------------------------------------------------------------- /lua/configs.lua: -------------------------------------------------------------------------------- 1 | json = require("cjson") 2 | 3 | --mysql_config = { 4 | -- host = "127.0.0.1", 5 | -- port = 3306, 6 | -- database = "lua", 7 | -- user = "root", 8 | -- password = "", 9 | -- max_packet_size = 1024 * 1024 10 | --} 11 | 12 | redis_config = { 13 | host = '172.16.0.223', 14 | --host = '172.16.0.121', 15 | port = 6379, 16 | auth_pwd = '123456', 17 | db = 8, 18 | alive_time = 3600 * 24 * 7, 19 | channel = 'gw' 20 | } 21 | 22 | --mq_conf = { 23 | -- host = '172.16.0.121', 24 | -- port = 5672, 25 | -- username = 'sz', 26 | -- password = '123456', 27 | -- vhost = '/' 28 | --} 29 | 30 | token_secret = "pXFb4i%*834gfdh96(3df&%18iodGq4ODQyMzc4lz7yI6ImF1dG" 31 | logs_file = '/var/log/gw.log' 32 | 33 | --刷新权限到redis接口 34 | rewrite_cache_url = 'http://mg.opendevops.cn:8010/v2/accounts/verify/' 35 | rewrite_cache_token = '8b888a62-3edb-4920-b446-697a472b4001' 36 | 37 | --并发限流配置 38 | limit_conf = { 39 | rate = 10, --限制ip每分钟只能调用n*60次接口 40 | burst = 10, --桶容量,用于平滑处理,最大接收请求次数 41 | } 42 | 43 | --upstream匹配规则 44 | gw_domain_name = 'gw.opendevops.cn' 45 | 46 | rewrite_conf = { 47 | [gw_domain_name] = { 48 | rewrite_urls = { 49 | { 50 | uri = "/dns", 51 | rewrite_upstream = "dns.opendevops.cn:8060" 52 | }, 53 | { 54 | uri = "/cmdb2", 55 | rewrite_upstream = "cmdb2.opendevops.cn:8050" 56 | }, 57 | { 58 | uri = "/tools", 59 | rewrite_upstream = "tools.opendevops.cn:8040" 60 | }, 61 | { 62 | uri = "/kerrigan", 63 | rewrite_upstream = "kerrigan.opendevops.cn:8030" 64 | }, 65 | { 66 | uri = "/cmdb", 67 | rewrite_upstream = "cmdb.opendevops.cn:8002" 68 | }, 69 | { 70 | uri = "/k8s", 71 | rewrite_upstream = "k8s.opendevops.cn:8001" 72 | }, 73 | { 74 | uri = "/task", 75 | rewrite_upstream = "task.opendevops.cn:8020" 76 | }, 77 | { 78 | uri = "/cron", 79 | rewrite_upstream = "cron.opendevops.cn:9900" 80 | }, 81 | { 82 | uri = "/mg", 83 | rewrite_upstream = "mg.opendevops.cn:8010" 84 | }, 85 | { 86 | uri = "/accounts", 87 | rewrite_upstream = "mg.opendevops.cn:8010" 88 | }, 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /lua/init_by_lua.lua: -------------------------------------------------------------------------------- 1 | config = require "configs" -------------------------------------------------------------------------------- /lua/jwt_token.lua: -------------------------------------------------------------------------------- 1 | module("jwt_token", package.seeall) 2 | local jwt = require "resty.jwt" 3 | 4 | function encode_auth_token(uid) 5 | local jwt_token = jwt:sign( 6 | token_secret, 7 | { 8 | header={typ="JWT", alg="HS256"}, 9 | payload={ 10 | foo="bar", 11 | data={ 12 | user_id=uid, 13 | } 14 | } 15 | } 16 | ) 17 | return jwt_token 18 | end 19 | 20 | 21 | function decode_auth_token(auth_token) 22 | local load_token = jwt:load_jwt( 23 | auth_token, 24 | token_secret 25 | ) 26 | return load_token 27 | end 28 | 29 | function decode_auth_token_verify(auth_token) 30 | local load_token = jwt:verify( 31 | token_secret, 32 | auth_token 33 | ) 34 | return load_token 35 | end 36 | --local jwt_token = encode_auth_token(token_secret,1) 37 | --local load_token = decode_auth_token('eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1MzUxOTMwMzksIm5iZiI6MTUzNTEwNjYxOSwiaWF0IjoxNTM1MTA2NjI5LCJpc3MiOiJhdXRoOiBzcyIsInN1YiI6Im15IHRva2VuIiwiaWQiOiIxNTYxODcxODA2MCIsImRhdGEiOnsidXNlcl9pZCI6IjE0IiwidXNlcm5hbWUiOiJ5YW5nbWluZ3dlaSIsIm5pY2tuYW1lIjoiXHU2NzY4XHU5NGVkXHU1YTAxIn19.GucrQnWIVsWL-0nTqef5eLFAVzBRjsuUp_L9oasRGRQ') 38 | --ngx.say(json.encode(load_token)) -------------------------------------------------------------------------------- /lua/mq.lua: -------------------------------------------------------------------------------- 1 | local rabbitmq = require "resty.rabbitmqstomp" 2 | local _M = {} 3 | 4 | 5 | function _M.send() 6 | -- send log to mq 7 | local opts = { username = "guest", 8 | password = "guest", 9 | vhost = "/" } 10 | 11 | local mq, err = rabbitmq:new(opts) 12 | if not mq then 13 | ngx.log(ngx.ERR, "cannot new mq-------->") 14 | return 15 | end 16 | ngx.log(ngx.ERR, "send mq-22222------->") 17 | mq:set_timeout(10000) 18 | 19 | local ok, err = mq:connect('127.0.0.1',61613) 20 | 21 | if not ok then 22 | ngx.log(ngx.ERR, "cannot connect mq-------->"..err) 23 | return 24 | end 25 | ngx.log(ngx.ERR, "connect mq ok ------->") 26 | 27 | local msg = {key="value1", key2="value2"} 28 | 29 | local headers = {} 30 | -- 消息发送到哪里 /exchange/交换机名称/routing_key名称 31 | headers["destination"] = "/exchange/test/binding" 32 | headers["receipt"] = "msg#1" 33 | headers["app-id"] = "luaresty" 34 | -- 是否持久化 35 | headers["persistent"] = "true" 36 | -- 消息格式 37 | headers["content-type"] = "application/json" 38 | 39 | local ok, err = mq:send(json.encode(data), headers) 40 | if not ok then 41 | ngx.log(ngx.ERR, "cannot send mq ------->") 42 | return 43 | end 44 | ngx.log(ngx.INFO, "Published: " .. msg) 45 | 46 | -- -- 消息保持长连接,第一个参数表示连接超时时间,第二个参数是表示连接池大小 47 | -- -- 由于 rabbitmq 连接建立比较耗时,所以保持连接池是非常必要的 48 | -- local ok, err = mq:set_keepalive(10000, 500) 49 | -- if not ok then 50 | -- ngx.log(ngx.ERR, err) 51 | -- return 52 | -- end 53 | 54 | 55 | end 56 | 57 | 58 | function _M.subscribe( self, channel ) 59 | -- send log to redis 60 | local redis, err = redis_c:new() 61 | if not redis then 62 | return nil, err 63 | end 64 | 65 | local ok, err = self:connect_mod(redis) 66 | if not ok or err then 67 | return nil, err 68 | end 69 | 70 | local res, err = redis:subscribe(channel) 71 | if not res then 72 | return nil, err 73 | end 74 | 75 | local function do_read_func ( do_read ) 76 | if do_read == nil or do_read == true then 77 | res, err = redis:read_reply() 78 | if not res then 79 | return nil, err 80 | end 81 | return res 82 | end 83 | 84 | redis:unsubscribe(channel) 85 | self.set_keepalive_mod(redis) 86 | return 87 | end 88 | 89 | return do_read_func 90 | end 91 | 92 | 93 | return _M -------------------------------------------------------------------------------- /lua/my_verify.lua: -------------------------------------------------------------------------------- 1 | module("my_verify", package.seeall) 2 | --local mysql = require('mysql') 3 | local my_cache = require("redis") 4 | local http = require "resty.http" 5 | 6 | --function write_permission(user_id) 7 | -- local db = mysql.connect() 8 | -- if db == false then 9 | -- ngx.say('[Error] Mysql连接失败!') 10 | -- return 11 | -- end 12 | -- 13 | -- local select_sql = string.format("SELECT a.id,a.permission from permission a ,role_permission b,role c,user_role d,account e WHERE a.id=b.permission_id and c.id=b.role_id and d.role_id=c.id and d.user_id=e.uid and e.uid=%s;",user_id) 14 | -- local res, err, errno, sqlstate = db:query(select_sql) 15 | -- if not res then 16 | -- ngx.say("select error : ", err, " , errno : ", errno, " , sqlstate : ", sqlstate) 17 | -- return close_db(db) 18 | -- end 19 | -- 20 | -- local permissions={} 21 | -- for i, row in ipairs(res) do 22 | -- for name, value in pairs(row) do 23 | -- if name == "permission" then 24 | -- table.insert(permissions, 1, value) 25 | -- my_cache.sadd(user_id, value) 26 | -- end 27 | -- end 28 | -- end 29 | -- return permissions 30 | --end 31 | 32 | --function get_permission(user_id,uri) 33 | -- my_verify = my_cache.smembers(user_id) 34 | -- for k,v in ipairs(my_verify) do 35 | -- -- ngx.log(ngx.ERR,'line----->',v) 36 | -- local is_exit = string.find(uri,"^"..v) 37 | -- if is_exit == 1 then 38 | -- return true 39 | -- end 40 | -- end 41 | -- return false 42 | --end 43 | 44 | 45 | -- 对接权限系统的redis 46 | function get_verify(user_id, uri, method) 47 | local my_verify = my_cache.smembers(user_id .. method) 48 | local all_verify = my_cache.smembers(user_id .. 'ALL') 49 | 50 | for k, v in ipairs(my_verify) do 51 | --ngx.log(ngx.ERR,'line----->',v) 52 | local is_exit = string.find(uri, "^" .. v) 53 | if is_exit == 1 then 54 | return true 55 | end 56 | end 57 | 58 | for k, v in ipairs(all_verify) do 59 | --ngx.log(ngx.ERR,'line----->',v) 60 | local is_exit = string.find(uri, "^" .. v) 61 | if is_exit == 1 then 62 | return true 63 | end 64 | end 65 | return false 66 | end 67 | 68 | function write_verify(user_id, is_superuser) 69 | local httpc = http.new() 70 | local res, err = httpc:request_uri(rewrite_cache_url, 71 | { 72 | method = "POST", 73 | body = json.encode({ 74 | user_id = user_id, 75 | is_superuser = is_superuser, 76 | secret_key = rewrite_cache_token 77 | }), 78 | headers = { 79 | ["Content-Type"] = "application/json" 80 | } 81 | }) 82 | if not res then 83 | ngx.say("failed to request: ", err) 84 | return 85 | end 86 | if 200 ~= res.status then 87 | ngx.exit(res.status) 88 | end 89 | end 90 | 91 | --local permissions = get_permission(28,'/home1') 92 | --ngx.say(json.encode(permissions)) 93 | 94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /lua/plugin/auth/auth_check.lua: -------------------------------------------------------------------------------- 1 | local jwt = require('jwt_token') 2 | local my_verify = require('my_verify') 3 | local user_info = ngx.shared.user_info 4 | 5 | local _M = {} 6 | 7 | function _M.check(real_new_uri) 8 | -- 获取cook 9 | local auth_key = ngx.var.cookie_auth_key 10 | 11 | if auth_key == nil then 12 | local arg = ngx.req.get_uri_args() 13 | if arg ~= nil then 14 | for k, v in pairs(arg) do 15 | if k == 'auth_key' then 16 | auth_key = v 17 | end 18 | end 19 | else 20 | ngx.exit(ngx.HTTP_UNAUTHORIZED) 21 | return 22 | end 23 | end 24 | 25 | if auth_key == nil then 26 | ngx.exit(ngx.HTTP_UNAUTHORIZED) 27 | return 28 | end 29 | 30 | -- 31 | -- local auth_key = ngx.var.cookie_auth_key 32 | -- 33 | -- if auth_key == nil then 34 | -- ngx.exit(ngx.HTTP_UNAUTHORIZED) 35 | -- return 36 | -- end 37 | 38 | -- 解密auth_key 39 | local load_token = jwt.decode_auth_token_verify(auth_key) 40 | -- ngx.log(ngx.ERR,'load_token--->',json.encode(load_token)) 41 | 42 | -- 鉴定token是否正常 43 | if load_token.verified == false then 44 | ngx.log(ngx.ERR, "Invalid token: " .. load_token.reason) 45 | ngx.exit(ngx.HTTP_UNAUTHORIZED) 46 | end 47 | 48 | -- 获得用户id 49 | local user_id = load_token.payload.data.user_id 50 | local is_superuser = load_token.payload.data.is_superuser 51 | -- ngx.log(ngx.ERR, 'user_id--->', user_id) 52 | user_info['username'] = load_token.payload.data.username 53 | user_info['nickname'] = load_token.payload.data.nickname 54 | 55 | -- ngx.log(ngx.ERR, 'is_superuser--->>>>>>>>>>>>>', load_token.payload.data.is_superuser) 56 | -- 获取当前uri 57 | local uri = real_new_uri 58 | -- local uri = ngx.var.request_uri 59 | -- ngx.log(ngx.ERR,'auth_check_uri--->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>', uri) 60 | -- ngx.say('uri---> ',uri) 61 | 62 | -- 获取请求方法 63 | local method = ngx.req.get_method() 64 | 65 | -- 根据用户id获取权限列表(从权限系统redis获取) 66 | local is_permission = my_verify.get_verify(user_id, uri, method) 67 | if is_permission ~= true then 68 | -- 第一次没有就先刷新下redis 69 | my_verify.write_verify(user_id, is_superuser) 70 | local is_permission = my_verify.get_verify(user_id, uri, method) 71 | if is_permission ~= true then 72 | my_verify.write_verify(user_id, is_superuser) 73 | ngx.exit(ngx.HTTP_FORBIDDEN) 74 | return 75 | end 76 | end 77 | 78 | --- - 根据用户id获取权限列表(本地测试redis) 79 | -- local is_permission = my_verify.get_permission(user_id,uri) 80 | ---- ngx.say('is_permission---> ',is_permission) 81 | -- if is_permission ~= true then 82 | -- -- 第一次没有就先刷新下redis 83 | -- my_verify.write_permission(user_id) 84 | -- local is_permission = my_verify.get_permission(user_id,uri) 85 | -- if is_permission ~= true then 86 | -- my_verify.write_permission(user_id) 87 | -- ngx.say('没有权限访问该URI') 88 | -- return 89 | -- end 90 | -- end 91 | end 92 | 93 | return _M -------------------------------------------------------------------------------- /lua/plugin/limit/limit_req.lua: -------------------------------------------------------------------------------- 1 | local limit_req = require "resty.limit.req" 2 | 3 | -- 限制 ip 每分钟只能调用 5*60 次 接口(平滑处理请求,即每秒放过5个请求) 4 | -- 超过部分进入桶中等待,(桶容量为60),如果桶也满了,则进行限流 5 | local lim, err = limit_req.new("my_limit_conn_store",limit_conf.rate,limit_conf.burst) 6 | 7 | if not lim then --没定义共享字典 8 | ngx.log(ngx.ERR, "failed to instantiate a resty.limit.conn object: ", err) 9 | return ngx.exit(500) 10 | end 11 | 12 | local _M = {} 13 | 14 | function _M.incoming() 15 | -- 对于内部重定向或子请求,不进行限制。因为这些并不是真正对外的请求。 16 | if ngx.req.is_internal() then 17 | return 18 | end 19 | 20 | local key = ngx.var.binary_remote_addr 21 | local delay, err = lim:incoming(key, true) 22 | if not delay then 23 | if err == "rejected" then 24 | return ngx.exit(503) 25 | end 26 | ngx.log(ngx.ERR, "failed to limit req: ", err) 27 | return ngx.exit(500) 28 | end 29 | -- 此方法返回,当前请求需要delay秒后才会被处理,和他前面对请求数 30 | -- 所以此处对桶中请求进行延时处理,让其排队等待,就是应用了漏桶算法 31 | if delay >= 0.001 then 32 | ngx.sleep(delay) 33 | end 34 | end 35 | 36 | return _M 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /lua/plugin/logs/init_log.lua: -------------------------------------------------------------------------------- 1 | local user_info = ngx.shared.user_info 2 | local my_cache = require("redis") 3 | 4 | local _M = {} 5 | 6 | function _M.send() 7 | -- 记录日志操作 8 | local method = ngx.req.get_method() 9 | local postargs = ngx.req.get_body_data() --str 10 | -- local postargs = ngx.req.get_post_args() --table 11 | local data = { 12 | username = user_info.username, 13 | nickname = user_info.nickname, 14 | login_ip = ngx.var.proxy_add_x_forwarded_for, 15 | method = method, 16 | uri = ngx.var.request_uri, 17 | data = postargs, 18 | time = os.date('%Y-%m-%d %H:%M:%S') 19 | } 20 | 21 | -- data['time'] = os.date('%Y-%m-%d %H:%M:%S') 22 | local new_data = json.encode(data) 23 | 24 | local this_file = io.open(logs_file, "a+") 25 | this_file:write(new_data .. '\n') 26 | this_file:close() 27 | 28 | if method ~= "GET" then 29 | my_cache.publish(redis_config.channel, new_data) 30 | end 31 | end 32 | 33 | return _M -------------------------------------------------------------------------------- /lua/redis.lua: -------------------------------------------------------------------------------- 1 | module("redis", package.seeall) 2 | local redis = require "resty.redis" 3 | local aes = require "resty.aes" 4 | local str = require "resty.string" 5 | 6 | local function connect() 7 | local red = redis:new() 8 | red:set_timeout(1000) 9 | local ok, err = red:connect(redis_config['host'], redis_config['port']) 10 | if not ok then 11 | return false 12 | end 13 | red:auth(redis_config['auth_pwd']) 14 | ok, err = red:select(redis_config['db']) 15 | if not ok then 16 | return false 17 | end 18 | return red 19 | end 20 | 21 | function add_token(token, raw_token) 22 | local red = connect() 23 | if red == false then 24 | return false 25 | end 26 | 27 | local ok, err = red:setex(token, redis_config['alive_time'], raw_token) 28 | if not ok then 29 | return false 30 | end 31 | return true 32 | end 33 | 34 | -- 用户权限信息写入redis 35 | function sadd(k, v) 36 | local red = connect() 37 | if red == false then 38 | return false 39 | end 40 | 41 | local ok, err = red:sadd(k,v) 42 | if not ok then 43 | return false 44 | end 45 | return true 46 | end 47 | 48 | -- 读取用户权限信息 49 | function smembers(k) 50 | local red = connect() 51 | if red == false then 52 | return false 53 | end 54 | local res, err = red:smembers(k) 55 | if not res then 56 | return false 57 | end 58 | return res 59 | end 60 | 61 | function del_token(token) 62 | local red = connect() 63 | if red == false then 64 | return 65 | end 66 | red:del(token) 67 | end 68 | 69 | function has_token(token) 70 | local red = connect() 71 | if red == false then 72 | return false 73 | end 74 | 75 | local res, err = red:get(token) 76 | if not res then 77 | return false 78 | end 79 | return res 80 | end 81 | 82 | -- 发布消息 83 | function publish(channel,msg) 84 | local red = connect() 85 | if red == false then 86 | return false 87 | end 88 | 89 | local ok, err = red:publish(channel,msg) 90 | if not ok then 91 | return false 92 | end 93 | return true 94 | end 95 | 96 | 97 | -- 根据uid生成token,不用了 98 | function gen_token(uid) 99 | local rawtoken = uid .. " " .. ngx.now() 100 | local aes_128_cbc_md5 = aes:new("friends_secret_key") 101 | local encrypted = aes_128_cbc_md5:encrypt(rawtoken) 102 | local token = str.to_hex(encrypted) 103 | return token, rawtoken 104 | end 105 | 106 | -------------------------------------------------------------------------------- /lua/resty/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ss1917/api-gateway/312467a7aebbf8bb6c4d584080d4a3890b5756a4/lua/resty/.DS_Store -------------------------------------------------------------------------------- /lua/resty/._.DS_Store: -------------------------------------------------------------------------------- 1 | Mac OS X  2Fx ATTRxx -------------------------------------------------------------------------------- /lua/resty/evp.lua: -------------------------------------------------------------------------------- 1 | -- Copyright (C) by Daniel Hiltgen (daniel.hiltgen@docker.com) 2 | 3 | 4 | local ffi = require "ffi" 5 | local _C = ffi.C 6 | local _M = { _VERSION = "0.0.2" } 7 | 8 | 9 | local CONST = { 10 | SHA256_DIGEST = "SHA256", 11 | SHA512_DIGEST = "SHA512", 12 | } 13 | _M.CONST = CONST 14 | 15 | 16 | -- Reference: https://wiki.openssl.org/index.php/EVP_Signing_and_Verifying 17 | ffi.cdef[[ 18 | // Error handling 19 | unsigned long ERR_get_error(void); 20 | const char * ERR_reason_error_string(unsigned long e); 21 | 22 | // Basic IO 23 | typedef struct bio_st BIO; 24 | typedef struct bio_method_st BIO_METHOD; 25 | BIO_METHOD *BIO_s_mem(void); 26 | BIO * BIO_new(BIO_METHOD *type); 27 | int BIO_puts(BIO *bp,const char *buf); 28 | void BIO_vfree(BIO *a); 29 | int BIO_write(BIO *b, const void *buf, int len); 30 | 31 | // RSA 32 | typedef struct rsa_st RSA; 33 | int RSA_size(const RSA *rsa); 34 | void RSA_free(RSA *rsa); 35 | typedef int pem_password_cb(char *buf, int size, int rwflag, void *userdata); 36 | RSA * PEM_read_bio_RSAPrivateKey(BIO *bp, RSA **rsa, pem_password_cb *cb, 37 | void *u); 38 | RSA * PEM_read_bio_RSAPublicKey(BIO *bp, RSA **rsa, pem_password_cb *cb, 39 | void *u); 40 | 41 | // EVP PKEY 42 | typedef struct evp_pkey_st EVP_PKEY; 43 | typedef struct engine_st ENGINE; 44 | EVP_PKEY *EVP_PKEY_new(void); 45 | int EVP_PKEY_set1_RSA(EVP_PKEY *pkey,RSA *key); 46 | EVP_PKEY *EVP_PKEY_new_mac_key(int type, ENGINE *e, 47 | const unsigned char *key, int keylen); 48 | void EVP_PKEY_free(EVP_PKEY *key); 49 | int i2d_RSA(RSA *a, unsigned char **out); 50 | 51 | // PUBKEY 52 | EVP_PKEY *PEM_read_bio_PUBKEY(BIO *bp, EVP_PKEY **x, 53 | pem_password_cb *cb, void *u); 54 | 55 | // X509 56 | typedef struct x509_st X509; 57 | X509 *PEM_read_bio_X509(BIO *bp, X509 **x, pem_password_cb *cb, void *u); 58 | EVP_PKEY * X509_get_pubkey(X509 *x); 59 | void X509_free(X509 *a); 60 | void EVP_PKEY_free(EVP_PKEY *key); 61 | int i2d_X509(X509 *a, unsigned char **out); 62 | X509 *d2i_X509_bio(BIO *bp, X509 **x); 63 | 64 | // X509 store 65 | typedef struct x509_store_st X509_STORE; 66 | typedef struct X509_crl_st X509_CRL; 67 | X509_STORE *X509_STORE_new(void ); 68 | int X509_STORE_add_cert(X509_STORE *ctx, X509 *x); 69 | // Use this if we want to load the certs directly from a variables 70 | int X509_STORE_add_crl(X509_STORE *ctx, X509_CRL *x); 71 | int X509_STORE_load_locations (X509_STORE *ctx, 72 | const char *file, const char *dir); 73 | void X509_STORE_free(X509_STORE *v); 74 | 75 | // X509 store context 76 | typedef struct x509_store_ctx_st X509_STORE_CTX; 77 | X509_STORE_CTX *X509_STORE_CTX_new(void); 78 | int X509_STORE_CTX_init(X509_STORE_CTX *ctx, X509_STORE *store, 79 | X509 *x509, void *chain); 80 | int X509_verify_cert(X509_STORE_CTX *ctx); 81 | void X509_STORE_CTX_cleanup(X509_STORE_CTX *ctx); 82 | int X509_STORE_CTX_get_error(X509_STORE_CTX *ctx); 83 | const char *X509_verify_cert_error_string(long n); 84 | void X509_STORE_CTX_free(X509_STORE_CTX *ctx); 85 | 86 | // EVP Sign/Verify 87 | typedef struct env_md_ctx_st EVP_MD_CTX; 88 | typedef struct env_md_st EVP_MD; 89 | typedef struct evp_pkey_ctx_st EVP_PKEY_CTX; 90 | const EVP_MD *EVP_get_digestbyname(const char *name); 91 | EVP_MD_CTX *EVP_MD_CTX_create(void); 92 | void EVP_MD_CTX_destroy(EVP_MD_CTX *ctx); 93 | int EVP_DigestInit_ex(EVP_MD_CTX *ctx, const EVP_MD *type, ENGINE *impl); 94 | int EVP_DigestSignInit(EVP_MD_CTX *ctx, EVP_PKEY_CTX **pctx, 95 | const EVP_MD *type, ENGINE *e, EVP_PKEY *pkey); 96 | int EVP_DigestUpdate(EVP_MD_CTX *ctx,const void *d, 97 | size_t cnt); 98 | int EVP_DigestSignFinal(EVP_MD_CTX *ctx, 99 | unsigned char *sigret, size_t *siglen); 100 | 101 | int EVP_DigestVerifyInit(EVP_MD_CTX *ctx, EVP_PKEY_CTX **pctx, 102 | const EVP_MD *type, ENGINE *e, EVP_PKEY *pkey); 103 | int EVP_DigestVerifyFinal(EVP_MD_CTX *ctx, 104 | unsigned char *sig, size_t siglen); 105 | 106 | // Fingerprints 107 | int X509_digest(const X509 *data,const EVP_MD *type, 108 | unsigned char *md, unsigned int *len); 109 | 110 | ]] 111 | 112 | 113 | local function _err(ret) 114 | local code = _C.ERR_get_error() 115 | if code == 0 then 116 | return ret, "Zero error code (null arguments?)" 117 | end 118 | return ret, ffi.string(_C.ERR_reason_error_string(code)) 119 | end 120 | 121 | 122 | local RSASigner = {} 123 | _M.RSASigner = RSASigner 124 | 125 | --- Create a new RSASigner 126 | -- @param pem_private_key A private key string in PEM format 127 | -- @returns RSASigner, err_string 128 | function RSASigner.new(self, pem_private_key) 129 | local bio = _C.BIO_new(_C.BIO_s_mem()) 130 | ffi.gc(bio, _C.BIO_vfree) 131 | if _C.BIO_puts(bio, pem_private_key) < 0 then 132 | return _err() 133 | end 134 | 135 | -- TODO might want to support password protected private keys... 136 | local rsa = _C.PEM_read_bio_RSAPrivateKey(bio, nil, nil, nil) 137 | ffi.gc(rsa, _C.RSA_free) 138 | 139 | local evp_pkey = _C.EVP_PKEY_new() 140 | if not evp_pkey then 141 | return _err() 142 | end 143 | ffi.gc(evp_pkey, _C.EVP_PKEY_free) 144 | if _C.EVP_PKEY_set1_RSA(evp_pkey, rsa) ~= 1 then 145 | return _err() 146 | end 147 | self.evp_pkey = evp_pkey 148 | return self, nil 149 | end 150 | 151 | 152 | --- Sign a message 153 | -- @param message The message to sign 154 | -- @param digest_name The digest format to use (e.g., "SHA256") 155 | -- @returns signature, error_string 156 | function RSASigner.sign(self, message, digest_name) 157 | local buf = ffi.new("unsigned char[?]", 1024) 158 | local len = ffi.new("size_t[1]", 1024) 159 | 160 | local ctx = _C.EVP_MD_CTX_create() 161 | if not ctx then 162 | return _err() 163 | end 164 | ffi.gc(ctx, _C.EVP_MD_CTX_destroy) 165 | 166 | local md = _C.EVP_get_digestbyname(digest_name) 167 | if not md then 168 | return _err() 169 | end 170 | 171 | if _C.EVP_DigestInit_ex(ctx, md, nil) ~= 1 then 172 | return _err() 173 | end 174 | local ret = _C.EVP_DigestSignInit(ctx, nil, md, nil, self.evp_pkey) 175 | if ret ~= 1 then 176 | return _err() 177 | end 178 | if _C.EVP_DigestUpdate(ctx, message, #message) ~= 1 then 179 | return _err() 180 | end 181 | if _C.EVP_DigestSignFinal(ctx, buf, len) ~= 1 then 182 | return _err() 183 | end 184 | return ffi.string(buf, len[0]), nil 185 | end 186 | 187 | 188 | 189 | local RSAVerifier = {} 190 | _M.RSAVerifier = RSAVerifier 191 | 192 | 193 | --- Create a new RSAVerifier 194 | -- @param key_source An instance of Cert or PublicKey used for verification 195 | -- @returns RSAVerifier, error_string 196 | function RSAVerifier.new(self, key_source) 197 | if not key_source then 198 | return nil, "You must pass in an key_source for a public key" 199 | end 200 | local evp_public_key = key_source.public_key 201 | self.evp_pkey = evp_public_key 202 | return self, nil 203 | end 204 | 205 | --- Verify a message is properly signed 206 | -- @param message The original message 207 | -- @param the signature to verify 208 | -- @param digest_name The digest type that was used to sign 209 | -- @returns bool, error_string 210 | function RSAVerifier.verify(self, message, sig, digest_name) 211 | local md = _C.EVP_get_digestbyname(digest_name) 212 | if not md then 213 | return _err(false) 214 | end 215 | 216 | local ctx = _C.EVP_MD_CTX_create() 217 | if not ctx then 218 | return _err(false) 219 | end 220 | ffi.gc(ctx, _C.EVP_MD_CTX_destroy) 221 | 222 | if _C.EVP_DigestInit_ex(ctx, md, nil) ~= 1 then 223 | return _err(false) 224 | end 225 | 226 | local ret = _C.EVP_DigestVerifyInit(ctx, nil, md, nil, self.evp_pkey) 227 | if ret ~= 1 then 228 | return _err(false) 229 | end 230 | if _C.EVP_DigestUpdate(ctx, message, #message) ~= 1 then 231 | return _err(false) 232 | end 233 | local sig_bin = ffi.new("unsigned char[?]", #sig) 234 | ffi.copy(sig_bin, sig, #sig) 235 | if _C.EVP_DigestVerifyFinal(ctx, sig_bin, #sig) == 1 then 236 | return true, nil 237 | else 238 | return false, "Verification failed" 239 | end 240 | end 241 | 242 | 243 | local Cert = {} 244 | _M.Cert = Cert 245 | 246 | 247 | --- Create a new Certificate object 248 | -- @param payload A PEM or DER format X509 certificate 249 | -- @returns Cert, error_string 250 | function Cert.new(self, payload) 251 | if not payload then 252 | return nil, "Must pass a PEM or binary DER cert" 253 | end 254 | local bio = _C.BIO_new(_C.BIO_s_mem()) 255 | ffi.gc(bio, _C.BIO_vfree) 256 | local x509 257 | if payload:find('-----BEGIN') then 258 | if _C.BIO_puts(bio, payload) < 0 then 259 | return _err() 260 | end 261 | x509 = _C.PEM_read_bio_X509(bio, nil, nil, nil) 262 | else 263 | if _C.BIO_write(bio, payload, #payload) < 0 then 264 | return _err() 265 | end 266 | x509 = _C.d2i_X509_bio(bio, nil) 267 | end 268 | if not x509 then 269 | return _err() 270 | end 271 | ffi.gc(x509, _C.X509_free) 272 | self.x509 = x509 273 | local public_key, err = self:get_public_key() 274 | if not public_key then 275 | return nil, err 276 | end 277 | 278 | ffi.gc(public_key, _C.EVP_PKEY_free) 279 | 280 | self.public_key = public_key 281 | return self, nil 282 | end 283 | 284 | 285 | --- Retrieve the DER format of the certificate 286 | -- @returns Binary DER format 287 | function Cert.get_der(self) 288 | local bufp = ffi.new("unsigned char *[1]") 289 | local len = _C.i2d_X509(self.x509, bufp) 290 | if len < 0 then 291 | return _err() 292 | end 293 | local der = ffi.string(bufp[0], len) 294 | return der, nil 295 | end 296 | 297 | --- Retrieve the cert fingerprint 298 | -- @param digest_name the Type of digest to use (e.g., "SHA256") 299 | -- @returns fingerprint_string 300 | function Cert.get_fingerprint(self, digest_name) 301 | local md = _C.EVP_get_digestbyname(digest_name) 302 | if not md then 303 | return _err() 304 | end 305 | local buf = ffi.new("unsigned char[?]", 32) 306 | local len = ffi.new("unsigned int[1]", 32) 307 | if _C.X509_digest(self.x509, md, buf, len) ~= 1 then 308 | return _err() 309 | end 310 | local raw = ffi.string(buf, len[0]) 311 | local t = {} 312 | raw:gsub('.', function (c) table.insert(t, string.format('%02X', string.byte(c))) end) 313 | return table.concat(t, ":"), nil 314 | end 315 | 316 | --- Retrieve the public key from the CERT 317 | -- @returns An OpenSSL EVP PKEY object representing the public key 318 | function Cert.get_public_key(self) 319 | local evp_pkey = _C.X509_get_pubkey(self.x509) 320 | if not evp_pkey then 321 | return _err() 322 | end 323 | 324 | return evp_pkey, nil 325 | end 326 | 327 | --- Verify the Certificate is trusted 328 | -- @param trusted_cert_file File path to a list of PEM encoded trusted certificates 329 | -- @return bool, error_string 330 | function Cert.verify_trust(self, trusted_cert_file) 331 | local store = _C.X509_STORE_new() 332 | if not store then 333 | return _err(false) 334 | end 335 | ffi.gc(store, _C.X509_STORE_free) 336 | if _C.X509_STORE_load_locations(store, trusted_cert_file, nil) ~=1 then 337 | return _err(false) 338 | end 339 | 340 | local ctx = _C.X509_STORE_CTX_new() 341 | if not store then 342 | return _err(false) 343 | end 344 | ffi.gc(ctx, _C.X509_STORE_CTX_free) 345 | if _C.X509_STORE_CTX_init(ctx, store, self.x509, nil) ~= 1 then 346 | return _err(false) 347 | end 348 | 349 | if _C.X509_verify_cert(ctx) ~= 1 then 350 | local code = _C.X509_STORE_CTX_get_error(ctx) 351 | local msg = ffi.string(_C.X509_verify_cert_error_string(code)) 352 | _C.X509_STORE_CTX_cleanup(ctx) 353 | return false, msg 354 | end 355 | _C.X509_STORE_CTX_cleanup(ctx) 356 | return true, nil 357 | 358 | end 359 | 360 | local PublicKey = {} 361 | _M.PublicKey = PublicKey 362 | 363 | --- Create a new PublicKey object 364 | -- 365 | -- If a PEM fornatted key is provided, the key must start with 366 | -- 367 | -- ----- BEGIN PUBLIC KEY ----- 368 | -- 369 | -- @param payload A PEM or DER format public key file 370 | -- @return PublicKey, error_string 371 | function PublicKey.new(self, payload) 372 | if not payload then 373 | return nil, "Must pass a PEM or binary DER public key" 374 | end 375 | local bio = _C.BIO_new(_C.BIO_s_mem()) 376 | ffi.gc(bio, _C.BIO_vfree) 377 | local pkey 378 | if payload:find('-----BEGIN') then 379 | if _C.BIO_puts(bio, payload) < 0 then 380 | return _err() 381 | end 382 | pkey = _C.PEM_read_bio_PUBKEY(bio, nil, nil, nil) 383 | else 384 | if _C.BIO_write(bio, payload, #payload) < 0 then 385 | return _err() 386 | end 387 | pkey = _C.d2i_PUBKEY_bio(bio, nil) 388 | end 389 | if not pkey then 390 | return _err() 391 | end 392 | ffi.gc(pkey, _C.EVP_PKEY_free) 393 | self.public_key = pkey 394 | return self, nil 395 | end 396 | 397 | 398 | return _M 399 | -------------------------------------------------------------------------------- /lua/resty/hmac.lua: -------------------------------------------------------------------------------- 1 | 2 | local str_util = require "resty.string" 3 | local to_hex = str_util.to_hex 4 | local ffi = require "ffi" 5 | local ffi_new = ffi.new 6 | local ffi_str = ffi.string 7 | local ffi_gc = ffi.gc 8 | local ffi_typeof = ffi.typeof 9 | local C = ffi.C 10 | local setmetatable = setmetatable 11 | local error = error 12 | 13 | 14 | local _M = { _VERSION = '0.02' } 15 | 16 | local mt = { __index = _M } 17 | 18 | 19 | ffi.cdef[[ 20 | typedef struct engine_st ENGINE; 21 | typedef struct evp_pkey_ctx_st EVP_PKEY_CTX; 22 | typedef struct env_md_ctx_st EVP_MD_CTX; 23 | typedef struct env_md_st EVP_MD; 24 | 25 | struct env_md_ctx_st 26 | { 27 | const EVP_MD *digest; 28 | ENGINE *engine; 29 | unsigned long flags; 30 | void *md_data; 31 | EVP_PKEY_CTX *pctx; 32 | int (*update)(EVP_MD_CTX *ctx,const void *data,size_t count); 33 | }; 34 | 35 | struct env_md_st 36 | { 37 | int type; 38 | int pkey_type; 39 | int md_size; 40 | unsigned long flags; 41 | int (*init)(EVP_MD_CTX *ctx); 42 | int (*update)(EVP_MD_CTX *ctx,const void *data,size_t count); 43 | int (*final)(EVP_MD_CTX *ctx,unsigned char *md); 44 | int (*copy)(EVP_MD_CTX *to,const EVP_MD_CTX *from); 45 | int (*cleanup)(EVP_MD_CTX *ctx); 46 | 47 | int (*sign)(int type, const unsigned char *m, unsigned int m_length, unsigned char *sigret, unsigned int *siglen, void *key); 48 | int (*verify)(int type, const unsigned char *m, unsigned int m_length, const unsigned char *sigbuf, unsigned int siglen, void *key); 49 | int required_pkey_type[5]; 50 | int block_size; 51 | int ctx_size; 52 | int (*md_ctrl)(EVP_MD_CTX *ctx, int cmd, int p1, void *p2); 53 | }; 54 | 55 | typedef struct hmac_ctx_st 56 | { 57 | const EVP_MD *md; 58 | EVP_MD_CTX md_ctx; 59 | EVP_MD_CTX i_ctx; 60 | EVP_MD_CTX o_ctx; 61 | unsigned int key_length; 62 | unsigned char key[128]; 63 | } HMAC_CTX; 64 | 65 | //OpenSSL 1.0 66 | void HMAC_CTX_init(HMAC_CTX *ctx); 67 | void HMAC_CTX_cleanup(HMAC_CTX *ctx); 68 | 69 | //OpenSSL 1.1 70 | HMAC_CTX *HMAC_CTX_new(void); 71 | void HMAC_CTX_free(HMAC_CTX *ctx); 72 | 73 | int HMAC_Init_ex(HMAC_CTX *ctx, const void *key, int len, const EVP_MD *md, ENGINE *impl); 74 | int HMAC_Update(HMAC_CTX *ctx, const unsigned char *data, size_t len); 75 | int HMAC_Final(HMAC_CTX *ctx, unsigned char *md, unsigned int *len); 76 | 77 | const EVP_MD *EVP_md5(void); 78 | const EVP_MD *EVP_sha1(void); 79 | const EVP_MD *EVP_sha256(void); 80 | const EVP_MD *EVP_sha512(void); 81 | ]] 82 | 83 | local buf = ffi_new("unsigned char[64]") 84 | local res_len = ffi_new("unsigned int[1]") 85 | local ctx_ptr_type = ffi_typeof("HMAC_CTX[1]") 86 | local hashes = { 87 | MD5 = C.EVP_md5(), 88 | SHA1 = C.EVP_sha1(), 89 | SHA256 = C.EVP_sha256(), 90 | SHA512 = C.EVP_sha512() 91 | } 92 | 93 | local ctx_new, ctx_free 94 | local openssl11, e = pcall(function () 95 | local ctx = C.HMAC_CTX_new() 96 | C.HMAC_CTX_free(ctx) 97 | end) 98 | if openssl11 then 99 | ctx_new = function () 100 | return C.HMAC_CTX_new() 101 | end 102 | ctx_free = function (ctx) 103 | C.HMAC_CTX_free(ctx) 104 | end 105 | else 106 | ctx_new = function () 107 | local ctx = ffi_new(ctx_ptr_type) 108 | C.HMAC_CTX_init(ctx) 109 | return ctx 110 | end 111 | ctx_free = function (ctx) 112 | C.HMAC_CTX_cleanup(ctx) 113 | end 114 | end 115 | 116 | 117 | _M.ALGOS = hashes 118 | 119 | 120 | function _M.new(self, key, hash_algo) 121 | local ctx = ctx_new() 122 | 123 | local _hash_algo = hash_algo or hashes.md5 124 | 125 | if C.HMAC_Init_ex(ctx, key, #key, _hash_algo, nil) == 0 then 126 | return nil 127 | end 128 | 129 | ffi_gc(ctx, ctx_free) 130 | 131 | return setmetatable({ _ctx = ctx }, mt) 132 | end 133 | 134 | 135 | function _M.update(self, s) 136 | return C.HMAC_Update(self._ctx, s, #s) == 1 137 | end 138 | 139 | 140 | function _M.final(self, s, hex_output) 141 | 142 | if s ~= nil then 143 | if C.HMAC_Update(self._ctx, s, #s) == 0 then 144 | return nil 145 | end 146 | end 147 | 148 | if C.HMAC_Final(self._ctx, buf, res_len) == 1 then 149 | if hex_output == true then 150 | return to_hex(ffi_str(buf, res_len[0])) 151 | end 152 | return ffi_str(buf, res_len[0]) 153 | end 154 | 155 | return nil 156 | end 157 | 158 | 159 | function _M.reset(self) 160 | return C.HMAC_Init_ex(self._ctx, nil, 0, nil, nil) == 1 161 | end 162 | 163 | return _M 164 | -------------------------------------------------------------------------------- /lua/resty/http.lua: -------------------------------------------------------------------------------- 1 | local http_headers = require "resty.http_headers" 2 | 3 | local ngx = ngx 4 | local ngx_socket_tcp = ngx.socket.tcp 5 | local ngx_req = ngx.req 6 | local ngx_req_socket = ngx_req.socket 7 | local ngx_req_get_headers = ngx_req.get_headers 8 | local ngx_req_get_method = ngx_req.get_method 9 | local str_lower = string.lower 10 | local str_upper = string.upper 11 | local str_find = string.find 12 | local str_sub = string.sub 13 | local tbl_concat = table.concat 14 | local tbl_insert = table.insert 15 | local ngx_encode_args = ngx.encode_args 16 | local ngx_re_match = ngx.re.match 17 | local ngx_re_gmatch = ngx.re.gmatch 18 | local ngx_re_sub = ngx.re.sub 19 | local ngx_re_gsub = ngx.re.gsub 20 | local ngx_re_find = ngx.re.find 21 | local ngx_log = ngx.log 22 | local ngx_DEBUG = ngx.DEBUG 23 | local ngx_ERR = ngx.ERR 24 | local ngx_var = ngx.var 25 | local ngx_print = ngx.print 26 | local ngx_header = ngx.header 27 | local co_yield = coroutine.yield 28 | local co_create = coroutine.create 29 | local co_status = coroutine.status 30 | local co_resume = coroutine.resume 31 | local setmetatable = setmetatable 32 | local tonumber = tonumber 33 | local tostring = tostring 34 | local unpack = unpack 35 | local rawget = rawget 36 | local select = select 37 | local ipairs = ipairs 38 | local pairs = pairs 39 | local pcall = pcall 40 | local type = type 41 | 42 | 43 | -- http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.1 44 | local HOP_BY_HOP_HEADERS = { 45 | ["connection"] = true, 46 | ["keep-alive"] = true, 47 | ["proxy-authenticate"] = true, 48 | ["proxy-authorization"] = true, 49 | ["te"] = true, 50 | ["trailers"] = true, 51 | ["transfer-encoding"] = true, 52 | ["upgrade"] = true, 53 | ["content-length"] = true, -- Not strictly hop-by-hop, but Nginx will deal 54 | -- with this (may send chunked for example). 55 | } 56 | 57 | 58 | -- Reimplemented coroutine.wrap, returning "nil, err" if the coroutine cannot 59 | -- be resumed. This protects user code from inifite loops when doing things like 60 | -- repeat 61 | -- local chunk, err = res.body_reader() 62 | -- if chunk then -- <-- This could be a string msg in the core wrap function. 63 | -- ... 64 | -- end 65 | -- until not chunk 66 | local co_wrap = function(func) 67 | local co = co_create(func) 68 | if not co then 69 | return nil, "could not create coroutine" 70 | else 71 | return function(...) 72 | if co_status(co) == "suspended" then 73 | return select(2, co_resume(co, ...)) 74 | else 75 | return nil, "can't resume a " .. co_status(co) .. " coroutine" 76 | end 77 | end 78 | end 79 | end 80 | 81 | 82 | -- Returns a new table, recursively copied from the one given. 83 | -- 84 | -- @param table table to be copied 85 | -- @return table 86 | local function tbl_copy(orig) 87 | local orig_type = type(orig) 88 | local copy 89 | if orig_type == "table" then 90 | copy = {} 91 | for orig_key, orig_value in next, orig, nil do 92 | copy[tbl_copy(orig_key)] = tbl_copy(orig_value) 93 | end 94 | else -- number, string, boolean, etc 95 | copy = orig 96 | end 97 | return copy 98 | end 99 | 100 | 101 | local _M = { 102 | _VERSION = '0.12', 103 | } 104 | _M._USER_AGENT = "lua-resty-http/" .. _M._VERSION .. " (Lua) ngx_lua/" .. ngx.config.ngx_lua_version 105 | 106 | local mt = { __index = _M } 107 | 108 | 109 | local HTTP = { 110 | [1.0] = " HTTP/1.0\r\n", 111 | [1.1] = " HTTP/1.1\r\n", 112 | } 113 | 114 | 115 | local DEFAULT_PARAMS = { 116 | method = "GET", 117 | path = "/", 118 | version = 1.1, 119 | } 120 | 121 | 122 | function _M.new(_) 123 | local sock, err = ngx_socket_tcp() 124 | if not sock then 125 | return nil, err 126 | end 127 | return setmetatable({ sock = sock, keepalive = true }, mt) 128 | end 129 | 130 | 131 | function _M.set_timeout(self, timeout) 132 | local sock = self.sock 133 | if not sock then 134 | return nil, "not initialized" 135 | end 136 | 137 | return sock:settimeout(timeout) 138 | end 139 | 140 | 141 | function _M.set_timeouts(self, connect_timeout, send_timeout, read_timeout) 142 | local sock = self.sock 143 | if not sock then 144 | return nil, "not initialized" 145 | end 146 | 147 | return sock:settimeouts(connect_timeout, send_timeout, read_timeout) 148 | end 149 | 150 | 151 | function _M.ssl_handshake(self, ...) 152 | local sock = self.sock 153 | if not sock then 154 | return nil, "not initialized" 155 | end 156 | 157 | self.ssl = true 158 | 159 | return sock:sslhandshake(...) 160 | end 161 | 162 | 163 | function _M.connect(self, ...) 164 | local sock = self.sock 165 | if not sock then 166 | return nil, "not initialized" 167 | end 168 | 169 | self.host = select(1, ...) 170 | self.port = select(2, ...) 171 | 172 | -- If port is not a number, this is likely a unix domain socket connection. 173 | if type(self.port) ~= "number" then 174 | self.port = nil 175 | end 176 | 177 | self.keepalive = true 178 | 179 | return sock:connect(...) 180 | end 181 | 182 | 183 | function _M.set_keepalive(self, ...) 184 | local sock = self.sock 185 | if not sock then 186 | return nil, "not initialized" 187 | end 188 | 189 | if self.keepalive == true then 190 | return sock:setkeepalive(...) 191 | else 192 | -- The server said we must close the connection, so we cannot setkeepalive. 193 | -- If close() succeeds we return 2 instead of 1, to differentiate between 194 | -- a normal setkeepalive() failure and an intentional close(). 195 | local res, err = sock:close() 196 | if res then 197 | return 2, "connection must be closed" 198 | else 199 | return res, err 200 | end 201 | end 202 | end 203 | 204 | 205 | function _M.get_reused_times(self) 206 | local sock = self.sock 207 | if not sock then 208 | return nil, "not initialized" 209 | end 210 | 211 | return sock:getreusedtimes() 212 | end 213 | 214 | 215 | function _M.close(self) 216 | local sock = self.sock 217 | if not sock then 218 | return nil, "not initialized" 219 | end 220 | 221 | return sock:close() 222 | end 223 | 224 | 225 | local function _should_receive_body(method, code) 226 | if method == "HEAD" then return nil end 227 | if code == 204 or code == 304 then return nil end 228 | if code >= 100 and code < 200 then return nil end 229 | return true 230 | end 231 | 232 | 233 | function _M.parse_uri(_, uri, query_in_path) 234 | if query_in_path == nil then query_in_path = true end 235 | 236 | local m, err = ngx_re_match(uri, [[^(?:(http[s]?):)?//([^:/\?]+)(?::(\d+))?([^\?]*)\??(.*)]], "jo") 237 | 238 | if not m then 239 | if err then 240 | return nil, "failed to match the uri: " .. uri .. ", " .. err 241 | end 242 | 243 | return nil, "bad uri: " .. uri 244 | else 245 | -- If the URI is schemaless (i.e. //example.com) try to use our current 246 | -- request scheme. 247 | if not m[1] then 248 | local scheme = ngx_var.scheme 249 | if scheme == "http" or scheme == "https" then 250 | m[1] = scheme 251 | else 252 | return nil, "schemaless URIs require a request context: " .. uri 253 | end 254 | end 255 | 256 | if m[3] then 257 | m[3] = tonumber(m[3]) 258 | else 259 | if m[1] == "https" then 260 | m[3] = 443 261 | else 262 | m[3] = 80 263 | end 264 | end 265 | if not m[4] or "" == m[4] then m[4] = "/" end 266 | 267 | if query_in_path and m[5] and m[5] ~= "" then 268 | m[4] = m[4] .. "?" .. m[5] 269 | m[5] = nil 270 | end 271 | 272 | return m, nil 273 | end 274 | end 275 | 276 | 277 | local function _format_request(params) 278 | local version = params.version 279 | local headers = params.headers or {} 280 | 281 | local query = params.query or "" 282 | if type(query) == "table" then 283 | query = "?" .. ngx_encode_args(query) 284 | elseif query ~= "" and str_sub(query, 1, 1) ~= "?" then 285 | query = "?" .. query 286 | end 287 | 288 | -- Initialize request 289 | local req = { 290 | str_upper(params.method), 291 | " ", 292 | params.path, 293 | query, 294 | HTTP[version], 295 | -- Pre-allocate slots for minimum headers and carriage return. 296 | true, 297 | true, 298 | true, 299 | } 300 | local c = 6 -- req table index it's faster to do this inline vs table.insert 301 | 302 | -- Append headers 303 | for key, values in pairs(headers) do 304 | if type(values) ~= "table" then 305 | values = {values} 306 | end 307 | 308 | key = tostring(key) 309 | for _, value in pairs(values) do 310 | req[c] = key .. ": " .. tostring(value) .. "\r\n" 311 | c = c + 1 312 | end 313 | end 314 | 315 | -- Close headers 316 | req[c] = "\r\n" 317 | 318 | return tbl_concat(req) 319 | end 320 | 321 | 322 | local function _receive_status(sock) 323 | local line, err = sock:receive("*l") 324 | if not line then 325 | return nil, nil, nil, err 326 | end 327 | 328 | return tonumber(str_sub(line, 10, 12)), tonumber(str_sub(line, 6, 8)), str_sub(line, 14) 329 | end 330 | 331 | 332 | local function _receive_headers(sock) 333 | local headers = http_headers.new() 334 | 335 | repeat 336 | local line, err = sock:receive("*l") 337 | if not line then 338 | return nil, err 339 | end 340 | 341 | local m, err = ngx_re_match(line, "([^:\\s]+):\\s*(.*)", "jo") 342 | if err then ngx_log(ngx_ERR, err) end 343 | 344 | if not m then 345 | break 346 | end 347 | 348 | local key = m[1] 349 | local val = m[2] 350 | if headers[key] then 351 | if type(headers[key]) ~= "table" then 352 | headers[key] = { headers[key] } 353 | end 354 | tbl_insert(headers[key], tostring(val)) 355 | else 356 | headers[key] = tostring(val) 357 | end 358 | until ngx_re_find(line, "^\\s*$", "jo") 359 | 360 | return headers, nil 361 | end 362 | 363 | 364 | local function _chunked_body_reader(sock, default_chunk_size) 365 | return co_wrap(function(max_chunk_size) 366 | local remaining = 0 367 | local length 368 | max_chunk_size = max_chunk_size or default_chunk_size 369 | 370 | repeat 371 | -- If we still have data on this chunk 372 | if max_chunk_size and remaining > 0 then 373 | 374 | if remaining > max_chunk_size then 375 | -- Consume up to max_chunk_size 376 | length = max_chunk_size 377 | remaining = remaining - max_chunk_size 378 | else 379 | -- Consume all remaining 380 | length = remaining 381 | remaining = 0 382 | end 383 | else -- This is a fresh chunk 384 | 385 | -- Receive the chunk size 386 | local str, err = sock:receive("*l") 387 | if not str then 388 | co_yield(nil, err) 389 | end 390 | 391 | length = tonumber(str, 16) 392 | 393 | if not length then 394 | co_yield(nil, "unable to read chunksize") 395 | end 396 | 397 | if max_chunk_size and length > max_chunk_size then 398 | -- Consume up to max_chunk_size 399 | remaining = length - max_chunk_size 400 | length = max_chunk_size 401 | end 402 | end 403 | 404 | if length > 0 then 405 | local str, err = sock:receive(length) 406 | if not str then 407 | co_yield(nil, err) 408 | end 409 | 410 | max_chunk_size = co_yield(str) or default_chunk_size 411 | 412 | -- If we're finished with this chunk, read the carriage return. 413 | if remaining == 0 then 414 | sock:receive(2) -- read \r\n 415 | end 416 | else 417 | -- Read the last (zero length) chunk's carriage return 418 | sock:receive(2) -- read \r\n 419 | end 420 | 421 | until length == 0 422 | end) 423 | end 424 | 425 | 426 | local function _body_reader(sock, content_length, default_chunk_size) 427 | return co_wrap(function(max_chunk_size) 428 | max_chunk_size = max_chunk_size or default_chunk_size 429 | 430 | if not content_length and max_chunk_size then 431 | -- We have no length, but wish to stream. 432 | -- HTTP 1.0 with no length will close connection, so read chunks to the end. 433 | repeat 434 | local str, err, partial = sock:receive(max_chunk_size) 435 | if not str and err == "closed" then 436 | co_yield(partial, err) 437 | end 438 | 439 | max_chunk_size = tonumber(co_yield(str) or default_chunk_size) 440 | if max_chunk_size and max_chunk_size < 0 then max_chunk_size = nil end 441 | 442 | if not max_chunk_size then 443 | ngx_log(ngx_ERR, "Buffer size not specified, bailing") 444 | break 445 | end 446 | until not str 447 | 448 | elseif not content_length then 449 | -- We have no length but don't wish to stream. 450 | -- HTTP 1.0 with no length will close connection, so read to the end. 451 | co_yield(sock:receive("*a")) 452 | 453 | elseif not max_chunk_size then 454 | -- We have a length and potentially keep-alive, but want everything. 455 | co_yield(sock:receive(content_length)) 456 | 457 | else 458 | -- We have a length and potentially a keep-alive, and wish to stream 459 | -- the response. 460 | local received = 0 461 | repeat 462 | local length = max_chunk_size 463 | if received + length > content_length then 464 | length = content_length - received 465 | end 466 | 467 | if length > 0 then 468 | local str, err = sock:receive(length) 469 | if not str then 470 | co_yield(nil, err) 471 | end 472 | received = received + length 473 | 474 | max_chunk_size = tonumber(co_yield(str) or default_chunk_size) 475 | if max_chunk_size and max_chunk_size < 0 then max_chunk_size = nil end 476 | 477 | if not max_chunk_size then 478 | ngx_log(ngx_ERR, "Buffer size not specified, bailing") 479 | break 480 | end 481 | end 482 | 483 | until length == 0 484 | end 485 | end) 486 | end 487 | 488 | 489 | local function _no_body_reader() 490 | return nil 491 | end 492 | 493 | 494 | local function _read_body(res) 495 | local reader = res.body_reader 496 | 497 | if not reader then 498 | -- Most likely HEAD or 304 etc. 499 | return nil, "no body to be read" 500 | end 501 | 502 | local chunks = {} 503 | local c = 1 504 | 505 | local chunk, err 506 | repeat 507 | chunk, err = reader() 508 | 509 | if err then 510 | return nil, err, tbl_concat(chunks) -- Return any data so far. 511 | end 512 | if chunk then 513 | chunks[c] = chunk 514 | c = c + 1 515 | end 516 | until not chunk 517 | 518 | return tbl_concat(chunks) 519 | end 520 | 521 | 522 | local function _trailer_reader(sock) 523 | return co_wrap(function() 524 | co_yield(_receive_headers(sock)) 525 | end) 526 | end 527 | 528 | 529 | local function _read_trailers(res) 530 | local reader = res.trailer_reader 531 | if not reader then 532 | return nil, "no trailers" 533 | end 534 | 535 | local trailers = reader() 536 | setmetatable(res.headers, { __index = trailers }) 537 | end 538 | 539 | 540 | local function _send_body(sock, body) 541 | if type(body) == 'function' then 542 | repeat 543 | local chunk, err, partial = body() 544 | 545 | if chunk then 546 | local ok, err = sock:send(chunk) 547 | 548 | if not ok then 549 | return nil, err 550 | end 551 | elseif err ~= nil then 552 | return nil, err, partial 553 | end 554 | 555 | until chunk == nil 556 | elseif body ~= nil then 557 | local bytes, err = sock:send(body) 558 | 559 | if not bytes then 560 | return nil, err 561 | end 562 | end 563 | return true, nil 564 | end 565 | 566 | 567 | local function _handle_continue(sock, body) 568 | local status, version, reason, err = _receive_status(sock) --luacheck: no unused 569 | if not status then 570 | return nil, nil, err 571 | end 572 | 573 | -- Only send body if we receive a 100 Continue 574 | if status == 100 then 575 | local ok, err = sock:receive("*l") -- Read carriage return 576 | if not ok then 577 | return nil, nil, err 578 | end 579 | _send_body(sock, body) 580 | end 581 | return status, version, err 582 | end 583 | 584 | 585 | function _M.send_request(self, params) 586 | -- Apply defaults 587 | setmetatable(params, { __index = DEFAULT_PARAMS }) 588 | 589 | local sock = self.sock 590 | local body = params.body 591 | local headers = http_headers.new() 592 | 593 | local params_headers = params.headers 594 | if params_headers then 595 | -- We assign one by one so that the metatable can handle case insensitivity 596 | -- for us. You can blame the spec for this inefficiency. 597 | for k, v in pairs(params_headers) do 598 | headers[k] = v 599 | end 600 | end 601 | 602 | -- Ensure minimal headers are set 603 | if type(body) == 'string' and not headers["Content-Length"] then 604 | headers["Content-Length"] = #body 605 | end 606 | if not headers["Host"] then 607 | if (str_sub(self.host, 1, 5) == "unix:") then 608 | return nil, "Unable to generate a useful Host header for a unix domain socket. Please provide one." 609 | end 610 | -- If we have a port (i.e. not connected to a unix domain socket), and this 611 | -- port is non-standard, append it to the Host heaer. 612 | if self.port then 613 | if self.ssl and self.port ~= 443 then 614 | headers["Host"] = self.host .. ":" .. self.port 615 | elseif not self.ssl and self.port ~= 80 then 616 | headers["Host"] = self.host .. ":" .. self.port 617 | else 618 | headers["Host"] = self.host 619 | end 620 | else 621 | headers["Host"] = self.host 622 | end 623 | end 624 | if not headers["User-Agent"] then 625 | headers["User-Agent"] = _M._USER_AGENT 626 | end 627 | if params.version == 1.0 and not headers["Connection"] then 628 | headers["Connection"] = "Keep-Alive" 629 | end 630 | 631 | params.headers = headers 632 | 633 | -- Format and send request 634 | local req = _format_request(params) 635 | ngx_log(ngx_DEBUG, "\n", req) 636 | local bytes, err = sock:send(req) 637 | 638 | if not bytes then 639 | return nil, err 640 | end 641 | 642 | -- Send the request body, unless we expect: continue, in which case 643 | -- we handle this as part of reading the response. 644 | if headers["Expect"] ~= "100-continue" then 645 | local ok, err, partial = _send_body(sock, body) 646 | if not ok then 647 | return nil, err, partial 648 | end 649 | end 650 | 651 | return true 652 | end 653 | 654 | 655 | function _M.read_response(self, params) 656 | local sock = self.sock 657 | 658 | local status, version, reason, err 659 | 660 | -- If we expect: continue, we need to handle this, sending the body if allowed. 661 | -- If we don't get 100 back, then status is the actual status. 662 | if params.headers["Expect"] == "100-continue" then 663 | local _status, _version, _err = _handle_continue(sock, params.body) 664 | if not _status then 665 | return nil, _err 666 | elseif _status ~= 100 then 667 | status, version, err = _status, _version, _err -- luacheck: no unused 668 | end 669 | end 670 | 671 | -- Just read the status as normal. 672 | if not status then 673 | status, version, reason, err = _receive_status(sock) 674 | if not status then 675 | return nil, err 676 | end 677 | end 678 | 679 | 680 | local res_headers, err = _receive_headers(sock) 681 | if not res_headers then 682 | return nil, err 683 | end 684 | 685 | -- keepalive is true by default. Determine if this is correct or not. 686 | local ok, connection = pcall(str_lower, res_headers["Connection"]) 687 | if ok then 688 | if (version == 1.1 and str_find(connection, "close", 1, true)) or 689 | (version == 1.0 and not str_find(connection, "keep-alive", 1, true)) then 690 | self.keepalive = false 691 | end 692 | else 693 | -- no connection header 694 | if version == 1.0 then 695 | self.keepalive = false 696 | end 697 | end 698 | 699 | local body_reader = _no_body_reader 700 | local trailer_reader, err 701 | local has_body = false 702 | 703 | -- Receive the body_reader 704 | if _should_receive_body(params.method, status) then 705 | local ok, encoding = pcall(str_lower, res_headers["Transfer-Encoding"]) 706 | if ok and version == 1.1 and encoding == "chunked" then 707 | body_reader, err = _chunked_body_reader(sock) 708 | has_body = true 709 | else 710 | 711 | local ok, length = pcall(tonumber, res_headers["Content-Length"]) 712 | if ok then 713 | body_reader, err = _body_reader(sock, length) 714 | has_body = true 715 | end 716 | end 717 | end 718 | 719 | if res_headers["Trailer"] then 720 | trailer_reader, err = _trailer_reader(sock) 721 | end 722 | 723 | if err then 724 | return nil, err 725 | else 726 | return { 727 | status = status, 728 | reason = reason, 729 | headers = res_headers, 730 | has_body = has_body, 731 | body_reader = body_reader, 732 | read_body = _read_body, 733 | trailer_reader = trailer_reader, 734 | read_trailers = _read_trailers, 735 | } 736 | end 737 | end 738 | 739 | 740 | function _M.request(self, params) 741 | params = tbl_copy(params) -- Take by value 742 | local res, err = self:send_request(params) 743 | if not res then 744 | return res, err 745 | else 746 | return self:read_response(params) 747 | end 748 | end 749 | 750 | 751 | function _M.request_pipeline(self, requests) 752 | requests = tbl_copy(requests) -- Take by value 753 | 754 | for _, params in ipairs(requests) do 755 | if params.headers and params.headers["Expect"] == "100-continue" then 756 | return nil, "Cannot pipeline request specifying Expect: 100-continue" 757 | end 758 | 759 | local res, err = self:send_request(params) 760 | if not res then 761 | return res, err 762 | end 763 | end 764 | 765 | local responses = {} 766 | for i, params in ipairs(requests) do 767 | responses[i] = setmetatable({ 768 | params = params, 769 | response_read = false, 770 | }, { 771 | -- Read each actual response lazily, at the point the user tries 772 | -- to access any of the fields. 773 | __index = function(t, k) 774 | local res, err 775 | if t.response_read == false then 776 | res, err = _M.read_response(self, t.params) 777 | t.response_read = true 778 | 779 | if not res then 780 | ngx_log(ngx_ERR, err) 781 | else 782 | for rk, rv in pairs(res) do 783 | t[rk] = rv 784 | end 785 | end 786 | end 787 | return rawget(t, k) 788 | end, 789 | }) 790 | end 791 | return responses 792 | end 793 | 794 | 795 | function _M.request_uri(self, uri, params) 796 | params = tbl_copy(params or {}) -- Take by value 797 | 798 | local parsed_uri, err = self:parse_uri(uri, false) 799 | if not parsed_uri then 800 | return nil, err 801 | end 802 | 803 | local scheme, host, port, path, query = unpack(parsed_uri) 804 | if not params.path then params.path = path end 805 | if not params.query then params.query = query end 806 | 807 | -- See if we should use a proxy to make this request 808 | local proxy_uri = self:get_proxy_uri(scheme, host) 809 | 810 | -- Make the connection either through the proxy or directly 811 | -- to the remote host 812 | local c, err 813 | 814 | if proxy_uri then 815 | c, err = self:connect_proxy(proxy_uri, scheme, host, port) 816 | else 817 | c, err = self:connect(host, port) 818 | end 819 | 820 | if not c then 821 | return nil, err 822 | end 823 | 824 | if proxy_uri then 825 | if scheme == "http" then 826 | -- When a proxy is used, the target URI must be in absolute-form 827 | -- (RFC 7230, Section 5.3.2.). That is, it must be an absolute URI 828 | -- to the remote resource with the scheme, host and an optional port 829 | -- in place. 830 | -- 831 | -- Since _format_request() constructs the request line by concatenating 832 | -- params.path and params.query together, we need to modify the path 833 | -- to also include the scheme, host and port so that the final form 834 | -- in conformant to RFC 7230. 835 | if port == 80 then 836 | params.path = scheme .. "://" .. host .. path 837 | else 838 | params.path = scheme .. "://" .. host .. ":" .. port .. path 839 | end 840 | end 841 | 842 | if scheme == "https" then 843 | -- don't keep this connection alive as the next request could target 844 | -- any host and re-using the proxy tunnel for that is not possible 845 | self.keepalive = false 846 | end 847 | 848 | -- self:connect_uri() set the host and port to point to the proxy server. As 849 | -- the connection to the proxy has been established, set the host and port 850 | -- to point to the actual remote endpoint at the other end of the tunnel to 851 | -- ensure the correct Host header added to the requests. 852 | self.host = host 853 | self.port = port 854 | end 855 | 856 | if scheme == "https" then 857 | local verify = true 858 | 859 | if params.ssl_verify == false then 860 | verify = false 861 | end 862 | 863 | local ok, err = self:ssl_handshake(nil, host, verify) 864 | if not ok then 865 | self:close() 866 | return nil, err 867 | end 868 | 869 | end 870 | 871 | local res, err = self:request(params) 872 | if not res then 873 | self:close() 874 | return nil, err 875 | end 876 | 877 | local body, err = res:read_body() 878 | if not body then 879 | self:close() 880 | return nil, err 881 | end 882 | 883 | res.body = body 884 | 885 | if params.keepalive == false then 886 | local ok, err = self:close() 887 | if not ok then 888 | ngx_log(ngx_ERR, err) 889 | end 890 | 891 | else 892 | local ok, err = self:set_keepalive(params.keepalive_timeout, params.keepalive_pool) 893 | if not ok then 894 | ngx_log(ngx_ERR, err) 895 | end 896 | 897 | end 898 | 899 | return res, nil 900 | end 901 | 902 | 903 | function _M.get_client_body_reader(_, chunksize, sock) 904 | chunksize = chunksize or 65536 905 | 906 | if not sock then 907 | local ok, err 908 | ok, sock, err = pcall(ngx_req_socket) 909 | 910 | if not ok then 911 | return nil, sock -- pcall err 912 | end 913 | 914 | if not sock then 915 | if err == "no body" then 916 | return nil 917 | else 918 | return nil, err 919 | end 920 | end 921 | end 922 | 923 | local headers = ngx_req_get_headers() 924 | local length = headers.content_length 925 | local encoding = headers.transfer_encoding 926 | if length then 927 | return _body_reader(sock, tonumber(length), chunksize) 928 | elseif encoding and str_lower(encoding) == 'chunked' then 929 | -- Not yet supported by ngx_lua but should just work... 930 | return _chunked_body_reader(sock, chunksize) 931 | else 932 | return nil 933 | end 934 | end 935 | 936 | 937 | function _M.proxy_request(self, chunksize) 938 | return self:request({ 939 | method = ngx_req_get_method(), 940 | path = ngx_re_gsub(ngx_var.uri, "\\s", "%20", "jo") .. ngx_var.is_args .. (ngx_var.query_string or ""), 941 | body = self:get_client_body_reader(chunksize), 942 | headers = ngx_req_get_headers(), 943 | }) 944 | end 945 | 946 | 947 | function _M.proxy_response(_, response, chunksize) 948 | if not response then 949 | ngx_log(ngx_ERR, "no response provided") 950 | return 951 | end 952 | 953 | ngx.status = response.status 954 | 955 | -- Filter out hop-by-hop headeres 956 | for k, v in pairs(response.headers) do 957 | if not HOP_BY_HOP_HEADERS[str_lower(k)] then 958 | ngx_header[k] = v 959 | end 960 | end 961 | 962 | local reader = response.body_reader 963 | repeat 964 | local chunk, err = reader(chunksize) 965 | if err then 966 | ngx_log(ngx_ERR, err) 967 | break 968 | end 969 | 970 | if chunk then 971 | local res, err = ngx_print(chunk) 972 | if not res then 973 | ngx_log(ngx_ERR, err) 974 | break 975 | end 976 | end 977 | until not chunk 978 | end 979 | 980 | 981 | function _M.set_proxy_options(self, opts) 982 | self.proxy_opts = tbl_copy(opts) -- Take by value 983 | end 984 | 985 | 986 | function _M.get_proxy_uri(self, scheme, host) 987 | if not self.proxy_opts then 988 | return nil 989 | end 990 | 991 | -- Check if the no_proxy option matches this host. Implementation adapted 992 | -- from lua-http library (https://github.com/daurnimator/lua-http) 993 | if self.proxy_opts.no_proxy then 994 | if self.proxy_opts.no_proxy == "*" then 995 | -- all hosts are excluded 996 | return nil 997 | end 998 | 999 | local no_proxy_set = {} 1000 | -- wget allows domains in no_proxy list to be prefixed by "." 1001 | -- e.g. no_proxy=.mit.edu 1002 | for host_suffix in ngx_re_gmatch(self.proxy_opts.no_proxy, "\\.?([^,]+)") do 1003 | no_proxy_set[host_suffix[1]] = true 1004 | end 1005 | 1006 | -- From curl docs: 1007 | -- matched as either a domain which contains the hostname, or the 1008 | -- hostname itself. For example local.com would match local.com, 1009 | -- local.com:80, and www.local.com, but not www.notlocal.com. 1010 | -- 1011 | -- Therefore, we keep stripping subdomains from the host, compare 1012 | -- them to the ones in the no_proxy list and continue until we find 1013 | -- a match or until there's only the TLD left 1014 | repeat 1015 | if no_proxy_set[host] then 1016 | return nil 1017 | end 1018 | 1019 | -- Strip the next level from the domain and check if that one 1020 | -- is on the list 1021 | host = ngx_re_sub(host, "^[^.]+\\.", "") 1022 | until not ngx_re_find(host, "\\.") 1023 | end 1024 | 1025 | if scheme == "http" and self.proxy_opts.http_proxy then 1026 | return self.proxy_opts.http_proxy 1027 | end 1028 | 1029 | if scheme == "https" and self.proxy_opts.https_proxy then 1030 | return self.proxy_opts.https_proxy 1031 | end 1032 | 1033 | return nil 1034 | end 1035 | 1036 | 1037 | function _M.connect_proxy(self, proxy_uri, scheme, host, port) 1038 | -- Parse the provided proxy URI 1039 | local parsed_proxy_uri, err = self:parse_uri(proxy_uri, false) 1040 | if not parsed_proxy_uri then 1041 | return nil, err 1042 | end 1043 | 1044 | -- Check that the scheme is http (https is not supported for 1045 | -- connections between the client and the proxy) 1046 | local proxy_scheme = parsed_proxy_uri[1] 1047 | if proxy_scheme ~= "http" then 1048 | return nil, "protocol " .. proxy_scheme .. " not supported for proxy connections" 1049 | end 1050 | 1051 | -- Make the connection to the given proxy 1052 | local proxy_host, proxy_port = parsed_proxy_uri[2], parsed_proxy_uri[3] 1053 | local c, err = self:connect(proxy_host, proxy_port) 1054 | if not c then 1055 | return nil, err 1056 | end 1057 | 1058 | if scheme == "https" then 1059 | -- Make a CONNECT request to create a tunnel to the destination through 1060 | -- the proxy. The request-target and the Host header must be in the 1061 | -- authority-form of RFC 7230 Section 5.3.3. See also RFC 7231 Section 1062 | -- 4.3.6 for more details about the CONNECT request 1063 | local destination = host .. ":" .. port 1064 | local res, err = self:request({ 1065 | method = "CONNECT", 1066 | path = destination, 1067 | headers = { 1068 | ["Host"] = destination 1069 | } 1070 | }) 1071 | 1072 | if not res then 1073 | return nil, err 1074 | end 1075 | 1076 | if res.status < 200 or res.status > 299 then 1077 | return nil, "failed to establish a tunnel through a proxy: " .. res.status 1078 | end 1079 | end 1080 | 1081 | return c, nil 1082 | end 1083 | 1084 | 1085 | return _M 1086 | -------------------------------------------------------------------------------- /lua/resty/http_headers.lua: -------------------------------------------------------------------------------- 1 | local rawget, rawset, setmetatable = 2 | rawget, rawset, setmetatable 3 | 4 | local str_lower = string.lower 5 | 6 | local _M = { 7 | _VERSION = '0.12', 8 | } 9 | 10 | 11 | -- Returns an empty headers table with internalised case normalisation. 12 | function _M.new() 13 | local mt = { 14 | normalised = {}, 15 | } 16 | 17 | mt.__index = function(t, k) 18 | return rawget(t, mt.normalised[str_lower(k)]) 19 | end 20 | 21 | mt.__newindex = function(t, k, v) 22 | local k_normalised = str_lower(k) 23 | 24 | -- First time seeing this header field? 25 | if not mt.normalised[k_normalised] then 26 | -- Create a lowercased entry in the metatable proxy, with the value 27 | -- of the given field case 28 | mt.normalised[k_normalised] = k 29 | 30 | -- Set the header using the given field case 31 | rawset(t, k, v) 32 | else 33 | -- We're being updated just with a different field case. Use the 34 | -- normalised metatable proxy to give us the original key case, and 35 | -- perorm a rawset() to update the value. 36 | rawset(t, mt.normalised[k_normalised], v) 37 | end 38 | end 39 | 40 | return setmetatable({}, mt) 41 | end 42 | 43 | 44 | return _M 45 | -------------------------------------------------------------------------------- /lua/resty/jwt-validators.lua: -------------------------------------------------------------------------------- 1 | local _M = {_VERSION="0.1.5"} 2 | 3 | --[[ 4 | This file defines "validators" to be used in validating a spec. A "validator" is simply a function with 5 | a signature that matches: 6 | 7 | function(val, claim, jwt_json) 8 | 9 | This function returns either true or false. If a validator needs to give more information on why it failed, 10 | then it can also raise an error (which will be used in the "reason" part of the validated jwt_obj). If a 11 | validator returns nil, then it is assumed to have passed (same as returning true) and that you just forgot 12 | to actually return a value. 13 | 14 | There is a special claim name of "__jwt" that can be used to validate the entire jwt_obj. 15 | 16 | "val" is the value being tested. It may be nil if the claim doesn't exist in the jwt_obj. If the function 17 | is being called for the "__jwt" claim, then "val" will contain a deep clone of the full jwt object. 18 | 19 | "claim" is the claim that is being tested. It is passed in just in case a validator needs to do additional 20 | checks. It will be the string "__jwt" if the validator is being called for the entire jwt_object. 21 | 22 | "jwt_json" is a json-encoded representation of the full object that is being tested. It will never be nil, 23 | and can always be decoded using cjson.decode(jwt_json). 24 | ]]-- 25 | 26 | 27 | --[[ 28 | A function which will define a validator. It creates both "opt_" and required (non-"opt_") 29 | versions. The function that is passed in is the *optional* version. 30 | ]]-- 31 | local function define_validator(name, fx) 32 | _M["opt_" .. name] = fx 33 | _M[name] = function(...) return _M.chain(_M.required(), fx(...)) end 34 | end 35 | 36 | -- Validation messages 37 | local messages = { 38 | nil_validator = "Cannot create validator for nil %s.", 39 | wrong_type_validator = "Cannot create validator for non-%s %s.", 40 | empty_table_validator = "Cannot create validator for empty table %s.", 41 | wrong_table_type_validator = "Cannot create validator for non-%s table %s.", 42 | required_claim = "'%s' claim is required.", 43 | wrong_type_claim = "'%s' is malformed. Expected to be a %s.", 44 | missing_claim = "Missing one of claims - [ %s ]." 45 | } 46 | 47 | -- Local function to make sure that a value is non-nil or raises an error 48 | local function ensure_not_nil(v, e, ...) 49 | return v ~= nil and v or error(string.format(e, ...), 0) 50 | end 51 | 52 | -- Local function to make sure that a value is the given type 53 | local function ensure_is_type(v, t, e, ...) 54 | return type(v) == t and v or error(string.format(e, ...), 0) 55 | end 56 | 57 | -- Local function to make sure that a value is a (non-empty) table 58 | local function ensure_is_table(v, e, ...) 59 | ensure_is_type(v, "table", e, ...) 60 | return ensure_not_nil(next(v), e, ...) 61 | end 62 | 63 | -- Local function to make sure all entries in the table are the given type 64 | local function ensure_is_table_type(v, t, e, ...) 65 | if v ~= nil then 66 | ensure_is_table(v, e, ...) 67 | for _,val in ipairs(v) do 68 | ensure_is_type(val, t, e, ...) 69 | end 70 | end 71 | return v 72 | end 73 | 74 | -- Local function to ensure that a number is non-negative (positive or 0) 75 | local function ensure_is_non_negative(v, e, ...) 76 | if v ~= nil then 77 | ensure_is_type(v, "number", e, ...) 78 | if v >= 0 then 79 | return v 80 | else 81 | error(string.format(e, ...), 0) 82 | end 83 | end 84 | end 85 | 86 | -- A local function which returns simple equality 87 | local function equality_function(val, check) 88 | return val == check 89 | end 90 | 91 | -- A local function which returns string match 92 | local function string_match_function(val, pattern) 93 | return string.match(val, pattern) ~= nil 94 | end 95 | 96 | --[[ 97 | A local function which returns truth on existence of check in vals. 98 | Adopted from auth0/nginx-jwt table_contains by @twistedstream 99 | ]]-- 100 | local function table_contains_function(vals, check) 101 | for _, val in pairs(vals) do 102 | if val == check then return true end 103 | end 104 | return false 105 | end 106 | 107 | 108 | -- A local function which returns numeric greater than comparison 109 | local function greater_than_function(val, check) 110 | return val > check 111 | end 112 | 113 | -- A local function which returns numeric greater than or equal comparison 114 | local function greater_than_or_equal_function(val, check) 115 | return val >= check 116 | end 117 | 118 | -- A local function which returns numeric less than comparison 119 | local function less_than_function(val, check) 120 | return val < check 121 | end 122 | 123 | -- A local function which returns numeric less than or equal comparison 124 | local function less_than_or_equal_function(val, check) 125 | return val <= check 126 | end 127 | 128 | 129 | --[[ 130 | Returns a validator that chains the given functions together, one after 131 | another - as long as they keep passing their checks. 132 | ]]-- 133 | function _M.chain(...) 134 | local chain_functions = {...} 135 | for _, fx in ipairs(chain_functions) do 136 | ensure_is_type(fx, "function", messages.wrong_type_validator, "function", "chain_function") 137 | end 138 | 139 | return function(val, claim, jwt_json) 140 | for _, fx in ipairs(chain_functions) do 141 | if fx(val, claim, jwt_json) == false then 142 | return false 143 | end 144 | end 145 | return true 146 | end 147 | end 148 | 149 | --[[ 150 | Returns a validator that returns false if a value doesn't exist. If 151 | the value exists and a chain_function is specified, then the value of 152 | chain_function(val, claim, jwt_json) 153 | will be returned, otherwise, true will be returned. This allows for 154 | specifying that a value is both required *and* it must match some 155 | additional check. This function will be used in the "required_*" shortcut 156 | functions for simplification. 157 | ]]-- 158 | function _M.required(chain_function) 159 | if chain_function ~= nil then 160 | return _M.chain(_M.required(), chain_function) 161 | end 162 | 163 | return function(val, claim, jwt_json) 164 | ensure_not_nil(val, messages.required_claim, claim) 165 | return true 166 | end 167 | end 168 | 169 | --[[ 170 | Returns a validator which errors with a message if *NONE* of the given claim 171 | keys exist. It is expected that this function is used against a full jwt object. 172 | The claim_keys must be a non-empty table of strings. 173 | ]]-- 174 | function _M.require_one_of(claim_keys) 175 | ensure_not_nil(claim_keys, messages.nil_validator, "claim_keys") 176 | ensure_is_type(claim_keys, "table", messages.wrong_type_validator, "table", "claim_keys") 177 | ensure_is_table(claim_keys, messages.empty_table_validator, "claim_keys") 178 | ensure_is_table_type(claim_keys, "string", messages.wrong_table_type_validator, "string", "claim_keys") 179 | 180 | return function(val, claim, jwt_json) 181 | ensure_is_type(val, "table", messages.wrong_type_claim, claim, "table") 182 | ensure_is_type(val.payload, "table", messages.wrong_type_claim, claim .. ".payload", "table") 183 | 184 | for i, v in ipairs(claim_keys) do 185 | if val.payload[v] ~= nil then return true end 186 | end 187 | 188 | error(string.format(messages.missing_claim, table.concat(claim_keys, ", ")), 0) 189 | end 190 | end 191 | 192 | --[[ 193 | Returns a validator that checks if the result of calling the given function for 194 | the tested value and the check value returns true. The value of check_val and 195 | check_function cannot be nil. The optional name is used for error messages and 196 | defaults to "check_value". The optional check_type is used to make sure that 197 | the check type matches and defaults to type(check_val). The first parameter 198 | passed to check_function will *never* be nil (check succeeds if value is nil). 199 | Use the required version to fail on nil. If the check_function raises an 200 | error, that will be appended to the error message. 201 | ]]-- 202 | define_validator("check", function(check_val, check_function, name, check_type) 203 | name = name or "check_val" 204 | ensure_not_nil(check_val, messages.nil_validator, name) 205 | 206 | ensure_not_nil(check_function, messages.nil_validator, "check_function") 207 | ensure_is_type(check_function, "function", messages.wrong_type_validator, "function", "check_function") 208 | 209 | check_type = check_type or type(check_val) 210 | return function(val, claim, jwt_json) 211 | if val == nil then return true end 212 | 213 | ensure_is_type(val, check_type, messages.wrong_type_claim, claim, check_type) 214 | return check_function(val, check_val) 215 | end 216 | end) 217 | 218 | 219 | --[[ 220 | Returns a validator that checks if a value exactly equals the given check_value. 221 | If the value is nil, then this check succeeds. The value of check_val cannot be 222 | nil. 223 | ]]-- 224 | define_validator("equals", function(check_val) 225 | return _M.opt_check(check_val, equality_function, "check_val") 226 | end) 227 | 228 | 229 | --[[ 230 | Returns a validator that checks if a value matches the given pattern. The value 231 | of pattern must be a string. 232 | ]]-- 233 | define_validator("matches", function (pattern) 234 | ensure_is_type(pattern, "string", messages.wrong_type_validator, "string", "pattern") 235 | return _M.opt_check(pattern, string_match_function, "pattern", "string") 236 | end) 237 | 238 | 239 | --[[ 240 | Returns a validator which calls the given function for each of the given values 241 | and the tested value. If any of these calls return true, then this function 242 | returns true. The value of check_values must be a non-empty table with all the 243 | same types, and the value of check_function must not be nil. The optional name 244 | is used for error messages and defaults to "check_values". The optional 245 | check_type is used to make sure that the check type matches and defaults to 246 | type(check_values[1]) - the table type. 247 | ]]-- 248 | define_validator("any_of", function(check_values, check_function, name, check_type, table_type) 249 | name = name or "check_values" 250 | ensure_not_nil(check_values, messages.nil_validator, name) 251 | ensure_is_type(check_values, "table", messages.wrong_type_validator, "table", name) 252 | ensure_is_table(check_values, messages.empty_table_validator, name) 253 | 254 | table_type = table_type or type(check_values[1]) 255 | ensure_is_table_type(check_values, table_type, messages.wrong_table_type_validator, table_type, name) 256 | 257 | ensure_not_nil(check_function, messages.nil_validator, "check_function") 258 | ensure_is_type(check_function, "function", messages.wrong_type_validator, "function", "check_function") 259 | 260 | check_type = check_type or table_type 261 | return _M.opt_check(check_values, function(v1, v2) 262 | for i, v in ipairs(v2) do 263 | if check_function(v1, v) then return true end 264 | end 265 | return false 266 | end, name, check_type) 267 | end) 268 | 269 | 270 | --[[ 271 | Returns a validator that checks if a value exactly equals any of the given values. 272 | ]]-- 273 | define_validator("equals_any_of", function(check_values) 274 | return _M.opt_any_of(check_values, equality_function, "check_values") 275 | end) 276 | 277 | 278 | --[[ 279 | Returns a validator that checks if a value matches any of the given patterns. 280 | ]]-- 281 | define_validator("matches_any_of", function(patterns) 282 | return _M.opt_any_of(patterns, string_match_function, "patterns", "string", "string") 283 | end) 284 | 285 | --[[ 286 | Returns a validator that checks if a value of expected type string exists in any of the given values. 287 | The value of check_values must be a non-empty table with all the same types. 288 | The optional name is used for error messages and defaults to "check_values". 289 | ]]-- 290 | define_validator("contains_any_of", function(check_values, name) 291 | return _M.opt_any_of(check_values, table_contains_function, name, "table", "string") 292 | end) 293 | 294 | --[[ 295 | Returns a validator that checks how a value compares (numerically) to a given 296 | check_value. The value of check_val cannot be nil and must be a number. 297 | ]]-- 298 | define_validator("greater_than", function(check_val) 299 | ensure_is_type(check_val, "number", messages.wrong_type_validator, "number", "check_val") 300 | return _M.opt_check(check_val, greater_than_function, "check_val", "number") 301 | end) 302 | define_validator("greater_than_or_equal", function(check_val) 303 | ensure_is_type(check_val, "number", messages.wrong_type_validator, "number", "check_val") 304 | return _M.opt_check(check_val, greater_than_or_equal_function, "check_val", "number") 305 | end) 306 | define_validator("less_than", function(check_val) 307 | ensure_is_type(check_val, "number", messages.wrong_type_validator, "number", "check_val") 308 | return _M.opt_check(check_val, less_than_function, "check_val", "number") 309 | end) 310 | define_validator("less_than_or_equal", function(check_val) 311 | ensure_is_type(check_val, "number", messages.wrong_type_validator, "number", "check_val") 312 | return _M.opt_check(check_val, less_than_or_equal_function, "check_val", "number") 313 | end) 314 | 315 | 316 | --[[ 317 | A function to set the leeway (in seconds) used for is_not_before and is_not_expired. The 318 | default is to use 0 seconds 319 | ]]-- 320 | local system_leeway = 0 321 | function _M.set_system_leeway(leeway) 322 | ensure_is_type(leeway, "number", "leeway must be a non-negative number") 323 | ensure_is_non_negative(leeway, "leeway must be a non-negative number") 324 | system_leeway = leeway 325 | end 326 | 327 | 328 | --[[ 329 | A function to set the system clock used for is_not_before and is_not_expired. The 330 | default is to use ngx.now 331 | ]]-- 332 | local system_clock = ngx.now 333 | function _M.set_system_clock(clock) 334 | ensure_is_type(clock, "function", "clock must be a function") 335 | -- Check that clock returns the correct value 336 | local t = clock() 337 | ensure_is_type(t, "number", "clock function must return a non-negative number") 338 | ensure_is_non_negative(t, "clock function must return a non-negative number") 339 | system_clock = clock 340 | end 341 | 342 | -- Local helper function for date validation 343 | local function validate_is_date(val, claim, jwt_json) 344 | ensure_is_non_negative(val, messages.wrong_type_claim, claim, "positive numeric value") 345 | return true 346 | end 347 | 348 | -- Local helper for date formatting 349 | local function format_date_on_error(date_check_function, error_msg) 350 | ensure_is_type(date_check_function, "function", messages.wrong_type_validator, "function", "date_check_function") 351 | ensure_is_type(error_msg, "string", messages.wrong_type_validator, "string", error_msg) 352 | return function(val, claim, jwt_json) 353 | local ret = date_check_function(val, claim, jwt_json) 354 | if ret == false then 355 | error(string.format("'%s' claim %s %s", claim, error_msg, ngx.http_time(val)), 0) 356 | end 357 | return true 358 | end 359 | end 360 | 361 | --[[ 362 | Returns a validator that checks if the current time is not before the tested value 363 | within the system's leeway. This means that: 364 | val <= (system_clock() + system_leeway). 365 | ]]-- 366 | define_validator("is_not_before", function() 367 | return format_date_on_error( 368 | _M.chain(validate_is_date, 369 | function(val) 370 | return val and less_than_or_equal_function(val, (system_clock() + system_leeway)) 371 | end), 372 | "not valid until" 373 | ) 374 | end) 375 | 376 | 377 | --[[ 378 | Returns a validator that checks if the current time is not equal to or after the 379 | tested value within the system's leeway. This means that: 380 | val > (system_clock() - system_leeway). 381 | ]]-- 382 | define_validator("is_not_expired", function() 383 | return format_date_on_error( 384 | _M.chain(validate_is_date, 385 | function(val) 386 | return val and greater_than_function(val, (system_clock() - system_leeway)) 387 | end), 388 | "expired at" 389 | ) 390 | end) 391 | 392 | --[[ 393 | Returns a validator that checks if the current time is the same as the tested value 394 | within the system's leeway. This means that: 395 | val >= (system_clock() - system_leeway) and val <= (system_clock() + system_leeway). 396 | ]]-- 397 | define_validator("is_at", function() 398 | local now = system_clock() 399 | return format_date_on_error( 400 | _M.chain(validate_is_date, 401 | function(val) 402 | local now = system_clock() 403 | return val and 404 | greater_than_or_equal_function(val, now - system_leeway) and 405 | less_than_or_equal_function(val, now + system_leeway) 406 | end), 407 | "is only valid at" 408 | ) 409 | end) 410 | 411 | 412 | return _M 413 | -------------------------------------------------------------------------------- /lua/resty/jwt.lua: -------------------------------------------------------------------------------- 1 | local cjson = require "cjson.safe" 2 | 3 | local aes = require "resty.aes" 4 | local evp = require "resty.evp" 5 | local hmac = require "resty.hmac" 6 | local resty_random = require "resty.random" 7 | 8 | local _M = {_VERSION="0.1.11"} 9 | local mt = {__index=_M} 10 | 11 | local string_match= string.match 12 | local string_rep = string.rep 13 | local string_format = string.format 14 | local string_sub = string.sub 15 | local string_byte = string.byte 16 | local string_char = string.char 17 | local table_concat = table.concat 18 | local ngx_encode_base64 = ngx.encode_base64 19 | local ngx_decode_base64 = ngx.decode_base64 20 | local cjson_encode = cjson.encode 21 | local cjson_decode = cjson.decode 22 | local tostring = tostring 23 | 24 | -- define string constants to avoid string garbage collection 25 | local str_const = { 26 | invalid_jwt= "invalid jwt string", 27 | regex_join_msg = "%s.%s", 28 | regex_join_delim = "([^%s]+)", 29 | regex_split_dot = "%.", 30 | regex_jwt_join_str = "%s.%s.%s", 31 | raw_underscore = "raw_", 32 | dash = "-", 33 | empty = "", 34 | dotdot = "..", 35 | table = "table", 36 | plus = "+", 37 | equal = "=", 38 | underscore = "_", 39 | slash = "/", 40 | header = "header", 41 | typ = "typ", 42 | JWT = "JWT", 43 | JWE = "JWE", 44 | payload = "payload", 45 | signature = "signature", 46 | encrypted_key = "encrypted_key", 47 | alg = "alg", 48 | enc = "enc", 49 | kid = "kid", 50 | exp = "exp", 51 | nbf = "nbf", 52 | iss = "iss", 53 | full_obj = "__jwt", 54 | AES = "AES", 55 | cbc = "cbc", 56 | x5c = "x5c", 57 | x5u = 'x5u', 58 | HS256 = "HS256", 59 | HS512 = "HS512", 60 | RS256 = "RS256", 61 | RS512 = "RS512", 62 | A128CBC_HS256 = "A128CBC-HS256", 63 | A256CBC_HS512 = "A256CBC-HS512", 64 | DIR = "dir", 65 | reason = "reason", 66 | verified = "verified", 67 | number = "number", 68 | string = "string", 69 | funct = "function", 70 | boolean = "boolean", 71 | table = "table", 72 | valid = "valid", 73 | valid_issuers = "valid_issuers", 74 | lifetime_grace_period = "lifetime_grace_period", 75 | require_nbf_claim = "require_nbf_claim", 76 | require_exp_claim = "require_exp_claim", 77 | internal_error = "internal error", 78 | everything_awesome = "everything is awesome~ :p" 79 | } 80 | 81 | -- @function split string 82 | local function split_string(str, delim, maxNb) 83 | local result = {} 84 | local sep = string_format(str_const.regex_join_delim, delim) 85 | for m in str:gmatch(sep) do 86 | result[#result+1]=m 87 | end 88 | return result 89 | end 90 | 91 | -- @function is nil or boolean 92 | -- @return true if param is nil or true or false; false otherwise 93 | local function is_nil_or_boolean(arg_value) 94 | if arg_value == nil then 95 | return true 96 | end 97 | 98 | if type(arg_value) ~= str_const.boolean then 99 | return false 100 | end 101 | 102 | return true 103 | end 104 | 105 | --@function get the row part 106 | --@param part_name 107 | --@param jwt_obj 108 | local function get_raw_part(part_name, jwt_obj) 109 | local raw_part = jwt_obj[str_const.raw_underscore .. part_name] 110 | if raw_part == nil then 111 | local part = jwt_obj[part_name] 112 | if part == nil then 113 | error({reason="missing part " .. part_name}) 114 | end 115 | raw_part = _M:jwt_encode(part) 116 | end 117 | return raw_part 118 | end 119 | 120 | 121 | --@function decrypt payload 122 | --@param secret_key to decrypt the payload 123 | --@param encrypted payload 124 | --@param encryption algorithm 125 | --@param iv which was generated while encrypting the payload 126 | --@return decrypted payloaf 127 | local function decrypt_payload(secret_key, encrypted_payload, enc, iv_in ) 128 | local decrypted_payload 129 | if enc == str_const.A128CBC_HS256 then 130 | local aes_128_cbc_with_iv = assert(aes:new(secret_key, str_const.AES, aes.cipher(128,str_const.cbc), {iv=iv_in} )) 131 | decrypted_payload= aes_128_cbc_with_iv:decrypt(encrypted_payload) 132 | elseif enc == str_const.A256CBC_HS512 then 133 | local aes_256_cbc_with_iv = assert(aes:new(secret_key, str_const.AES, aes.cipher(256,str_const.cbc), {iv=iv_in} )) 134 | decrypted_payload= aes_256_cbc_with_iv:decrypt(encrypted_payload) 135 | 136 | else 137 | return nil, "unsupported enc: " .. enc 138 | end 139 | if not decrypted_payload then 140 | return nil, "invalid secret key" 141 | end 142 | return decrypted_payload 143 | end 144 | 145 | -- @function : encrypt payload using given secret 146 | -- @param secret key to encrypt 147 | -- @param algortim to use for encryption 148 | -- @message : data to be encrypted. It could be lua table or string 149 | local function encrypt_payload(secret_key, message, enc ) 150 | 151 | if enc == str_const.A128CBC_HS256 then 152 | local iv_rand = resty_random.bytes(16,true) 153 | local aes_128_cbc_with_iv = assert(aes:new(secret_key, str_const.AES, aes.cipher(128,str_const.cbc), {iv=iv_rand} )) 154 | local encrypted = aes_128_cbc_with_iv:encrypt(message) 155 | return encrypted, iv_rand 156 | 157 | elseif enc == str_const.A256CBC_HS512 then 158 | local iv_rand = resty_random.bytes(16,true) 159 | local aes_256_cbc_with_iv = assert(aes:new(secret_key, str_const.AES, aes.cipher(256,str_const.cbc), {iv=iv_rand} )) 160 | local encrypted = aes_256_cbc_with_iv:encrypt(message) 161 | return encrypted, iv_rand 162 | 163 | else 164 | return nil, nil , "unsupported enc: " .. enc 165 | end 166 | end 167 | 168 | --@function hmac_digest : generate hmac digest based on key for input message 169 | --@param mac_key 170 | --@param input message 171 | --@return hmac digest 172 | local function hmac_digest(enc, mac_key, message) 173 | if enc == str_const.A128CBC_HS256 then 174 | return hmac:new(mac_key, hmac.ALGOS.SHA256):final(message) 175 | elseif enc == str_const.A256CBC_HS512 then 176 | return hmac:new(mac_key, hmac.ALGOS.SHA512):final(message) 177 | else 178 | error({reason="unsupported enc: " .. enc}) 179 | end 180 | end 181 | 182 | --@function dervice keys: it generates key if null based on encryption algorithm 183 | --@param encryption type 184 | --@param secret key 185 | --@return secret key, mac key and encryption key 186 | local function derive_keys(enc, secret_key) 187 | local mac_key_len, enc_key_len = 16, 16 188 | 189 | if enc == str_const.A128CBC_HS256 then 190 | mac_key_len, enc_key_len = 16, 16 191 | elseif enc == str_const.A256CBC_HS512 then 192 | mac_key_len, enc_key_len = 32, 32 193 | end 194 | 195 | local secret_key_len = mac_key_len + enc_key_len 196 | 197 | if not secret_key then 198 | secret_key = resty_random.bytes(secret_key_len, true) 199 | end 200 | 201 | if #secret_key ~= secret_key_len then 202 | error({reason="The pre-shared content key must be ".. secret_key_len}) 203 | end 204 | 205 | local mac_key = string_sub(secret_key, 1, mac_key_len) 206 | local enc_key = string_sub(secret_key, enc_key_len + 1) 207 | return secret_key, mac_key, enc_key 208 | end 209 | 210 | --@function parse_jwe 211 | --@param pre-shared key 212 | --@encoded-header 213 | local function parse_jwe(preshared_key, encoded_header, encoded_encrypted_key, encoded_iv, encoded_cipher_text, encoded_auth_tag) 214 | 215 | 216 | local header = _M:jwt_decode(encoded_header, true) 217 | if not header then 218 | error({reason="invalid header: " .. encoded_header}) 219 | end 220 | 221 | local key, mac_key, enc_key = derive_keys(header.enc, preshared_key) 222 | 223 | -- use preshared key if given otherwise decrypt the encoded key 224 | if not preshared_key then 225 | local encrypted_key = _M:jwt_decode(encoded_encrypted_key) 226 | if header.alg == str_const.DIR then 227 | error({reason="preshared key must not ne null"}) 228 | else -- implement algorithm to decrypt the key 229 | error({reason="invalid algorithm: " .. header.alg}) 230 | end 231 | end 232 | 233 | local cipher_text = _M:jwt_decode(encoded_cipher_text) 234 | local iv = _M:jwt_decode(encoded_iv) 235 | 236 | local basic_jwe = { 237 | internal = { 238 | encoded_header = encoded_header, 239 | cipher_text = cipher_text, 240 | key = key, 241 | iv = iv 242 | }, 243 | header=header, 244 | signature=_M:jwt_decode(encoded_auth_tag) 245 | } 246 | 247 | local json_payload, err = decrypt_payload(enc_key, cipher_text, header.enc, iv) 248 | if not json_payload then 249 | basic_jwe.reason = err 250 | 251 | else 252 | basic_jwe.payload = cjson_decode(json_payload) 253 | basic_jwe.internal.json_payload=json_payload 254 | end 255 | return basic_jwe 256 | end 257 | 258 | -- @function parse_jwt 259 | -- @param encoded header 260 | -- @param encoded 261 | -- @param signature 262 | -- @return jwt table 263 | local function parse_jwt(encoded_header, encoded_payload, signature) 264 | local header = _M:jwt_decode(encoded_header, true) 265 | if not header then 266 | error({reason="invalid header: " .. encoded_header}) 267 | end 268 | 269 | local payload = _M:jwt_decode(encoded_payload, true) 270 | if not payload then 271 | error({reason="invalid payload: " .. encoded_payload}) 272 | end 273 | 274 | local basic_jwt = { 275 | raw_header=encoded_header, 276 | raw_payload=encoded_payload, 277 | header=header, 278 | payload=payload, 279 | signature=signature 280 | } 281 | return basic_jwt 282 | 283 | end 284 | 285 | -- @function parse token - this can be JWE or JWT token 286 | -- @param token string 287 | -- @return jwt/jwe tables 288 | local function parse(secret, token_str) 289 | local tokens = split_string(token_str, str_const.regex_split_dot) 290 | local num_tokens = #tokens 291 | if num_tokens == 3 then 292 | return parse_jwt(tokens[1], tokens[2], tokens[3]) 293 | elseif num_tokens == 4 then 294 | return parse_jwe(secret, tokens[1], "", tokens[2], tokens[3], tokens[4]) 295 | elseif num_tokens == 5 then 296 | return parse_jwe(secret, tokens[1], tokens[2], tokens[3], tokens[4], tokens[5]) 297 | else 298 | error({reason=str_const.invalid_jwt}) 299 | end 300 | end 301 | 302 | 303 | --@function jwt encode : it converts into base64 encoded string. if input is a table, it convets into 304 | -- json before converting to base64 string 305 | --@param payloaf 306 | --@return base64 encoded payloaf 307 | function _M.jwt_encode(self, ori) 308 | if type(ori) == str_const.table then 309 | ori = cjson_encode(ori) 310 | end 311 | return ngx.encode_base64(ori):gsub(str_const.plus, str_const.dash):gsub(str_const.slash, str_const.underscore):gsub(str_const.equal, str_const.empty) 312 | end 313 | 314 | 315 | 316 | --@function jwt decode : decode bas64 encoded string 317 | function _M.jwt_decode(self, b64_str, json_decode) 318 | b64_str = b64_str:gsub(str_const.dash, str_const.plus):gsub(str_const.underscore, str_const.slash) 319 | 320 | local reminder = #b64_str % 4 321 | if reminder > 0 then 322 | b64_str = b64_str .. string_rep(str_const.equal, 4 - reminder) 323 | end 324 | local data = ngx_decode_base64(b64_str) 325 | if not data then 326 | return nil 327 | end 328 | if json_decode then 329 | data = cjson_decode(data) 330 | end 331 | return data 332 | end 333 | 334 | --- Initialize the trusted certs 335 | -- During RS256 verify, we'll make sure the 336 | -- cert was signed by one of these 337 | function _M.set_trusted_certs_file(self, filename) 338 | self.trusted_certs_file = filename 339 | end 340 | _M.trusted_certs_file = nil 341 | 342 | --- Set a whitelist of allowed algorithms 343 | -- E.g., jwt:set_alg_whitelist({RS256=1,HS256=1}) 344 | -- 345 | -- @param algorithms - A table with keys for the supported algorithms 346 | -- If the table is non-nil, during 347 | -- verify, the alg must be in the table 348 | function _M.set_alg_whitelist(self, algorithms) 349 | self.alg_whitelist = algorithms 350 | end 351 | 352 | _M.alg_whitelist = nil 353 | 354 | 355 | --- Returns the list of default validations that will be 356 | --- applied upon the verification of a jwt. 357 | function _M.get_default_validation_options(self, jwt_obj) 358 | return { 359 | [str_const.require_exp_claim]=jwt_obj[exp] ~= nil, 360 | [str_const.require_nbf_claim]=jwt_obj[nbf] ~= nil 361 | } 362 | end 363 | 364 | --- Set a function used to retrieve the content of x5u urls 365 | -- 366 | -- @param retriever_function - A pointer to a function. This function should be 367 | -- defined to accept three string parameters. First one 368 | -- will be the value of the 'x5u' attribute. Second 369 | -- one will be the value of the 'iss' attribute, would 370 | -- it be defined in the jwt. Third one will be the value 371 | -- of the 'iss' attribute, would it be defined in the jwt. 372 | -- This function should return the matching certificate. 373 | function _M.set_x5u_content_retriever(self, retriever_function) 374 | if type(retriever_function) ~= str_const.funct then 375 | error("'retriever_function' is expected to be a function", 0) 376 | end 377 | self.x5u_content_retriever = retriever_function 378 | end 379 | 380 | _M.x5u_content_retriever = nil 381 | 382 | -- https://tools.ietf.org/html/rfc7516#appendix-B.3 383 | -- TODO: do it in lua way 384 | local function binlen(s) 385 | if type(s) ~= 'string' then return end 386 | 387 | local len = 8 * #s 388 | 389 | return string_char(len / 0x0100000000000000 % 0x100) 390 | .. string_char(len / 0x0001000000000000 % 0x100) 391 | .. string_char(len / 0x0000010000000000 % 0x100) 392 | .. string_char(len / 0x0000000100000000 % 0x100) 393 | .. string_char(len / 0x0000000001000000 % 0x100) 394 | .. string_char(len / 0x0000000000010000 % 0x100) 395 | .. string_char(len / 0x0000000000000100 % 0x100) 396 | .. string_char(len / 0x0000000000000001 % 0x100) 397 | end 398 | 399 | --@function sign jwe payload 400 | --@param secret key : if used pre-shared or RSA key 401 | --@param jwe payload 402 | --@return jwe token 403 | local function sign_jwe(secret_key, jwt_obj) 404 | local header = jwt_obj.header 405 | local enc = header.enc 406 | 407 | local key, mac_key, enc_key = derive_keys(enc, secret_key) 408 | local json_payload = cjson_encode(jwt_obj.payload) 409 | local cipher_text, iv, err = encrypt_payload(enc_key, json_payload, enc) 410 | if err then 411 | error({reason="error while encrypting payload. Error: " .. err}) 412 | end 413 | local alg = header.alg 414 | 415 | if alg ~= str_const.DIR then 416 | error({reason="unsupported alg: " .. tostring(alg)}) 417 | end 418 | -- remove type 419 | if header.typ then 420 | header.typ = nil 421 | end 422 | local encoded_header = _M:jwt_encode(header) 423 | 424 | local encoded_header_length = binlen(encoded_header) 425 | local mac_input = table_concat({encoded_header , iv, cipher_text , encoded_header_length}) 426 | local mac = hmac_digest(enc, mac_key, mac_input) 427 | -- TODO: implement logic for creating enc key and mac key and then encrypt key 428 | local encrypted_key 429 | if alg == str_const.DIR then 430 | encrypted_key = "" 431 | else 432 | error({reason="unsupported alg: " .. alg}) 433 | end 434 | local auth_tag = string_sub(mac, 1, #mac/2) 435 | local jwe_table = {encoded_header, _M:jwt_encode(encrypted_key), _M:jwt_encode(iv), 436 | _M:jwt_encode(cipher_text), _M:jwt_encode(auth_tag)} 437 | return table_concat(jwe_table, ".", 1, 5) 438 | end 439 | 440 | --@function get_secret_str : returns the secret if it is a string, or the result of a function 441 | --@param either the string secret or a function that takes a string parameter and returns a string or nil 442 | --@param jwt payload 443 | --@return the secret as a string or as a function 444 | local function get_secret_str(secret_or_function, jwt_obj) 445 | if type(secret_or_function) == str_const.funct then 446 | -- Only use with hmac algorithms 447 | local alg = jwt_obj[str_const.header][str_const.alg] 448 | if alg ~= str_const.HS256 and alg ~= str_const.HS512 then 449 | error({reason="secret function can only be used with hmac alg: " .. alg}) 450 | end 451 | 452 | -- Pull out the kid value from the header 453 | local kid_val = jwt_obj[str_const.header][str_const.kid] 454 | if kid_val == nil then 455 | error({reason="secret function specified without kid in header"}) 456 | end 457 | 458 | -- Call the function 459 | return secret_or_function(kid_val) or error({reason="function returned nil for kid: " .. kid_val}) 460 | elseif type(secret_or_function) == str_const.string then 461 | -- Just return the string 462 | return secret_or_function 463 | else 464 | -- Throw an error 465 | error({reason="invalid secret type (must be string or function)"}) 466 | end 467 | end 468 | 469 | --@function sign : create a jwt/jwe signature from jwt_object 470 | --@param secret key 471 | --@param jwt/jwe payload 472 | function _M.sign(self, secret_key, jwt_obj) 473 | -- header typ check 474 | local typ = jwt_obj[str_const.header][str_const.typ] 475 | -- Optional header typ check [See http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-25#section-5.1] 476 | if typ ~= nil then 477 | if typ ~= str_const.JWT and typ ~= str_const.JWE then 478 | error({reason="invalid typ: " .. typ}) 479 | end 480 | end 481 | 482 | if typ == str_const.JWE or jwt_obj.header.enc then 483 | return sign_jwe(secret_key, jwt_obj) 484 | end 485 | -- header alg check 486 | local raw_header = get_raw_part(str_const.header, jwt_obj) 487 | local raw_payload = get_raw_part(str_const.payload, jwt_obj) 488 | local message = string_format(str_const.regex_join_msg, raw_header , raw_payload) 489 | 490 | local alg = jwt_obj[str_const.header][str_const.alg] 491 | local signature = "" 492 | if alg == str_const.HS256 then 493 | local secret_str = get_secret_str(secret_key, jwt_obj) 494 | signature = hmac:new(secret_str, hmac.ALGOS.SHA256):final(message) 495 | elseif alg == str_const.HS512 then 496 | local secret_str = get_secret_str(secret_key, jwt_obj) 497 | signature = hmac:new(secret_str, hmac.ALGOS.SHA512):final(message) 498 | elseif alg == str_const.RS256 then 499 | local signer, err = evp.RSASigner:new(secret_key) 500 | if not signer then 501 | error({reason="signer error: " .. err}) 502 | end 503 | signature = signer:sign(message, evp.CONST.SHA256_DIGEST) 504 | else 505 | error({reason="unsupported alg: " .. alg}) 506 | end 507 | -- return full jwt string 508 | return string_format(str_const.regex_join_msg, message , _M:jwt_encode(signature)) 509 | 510 | end 511 | 512 | --@function load jwt 513 | --@param jwt string token 514 | --@param secret 515 | function _M.load_jwt(self, jwt_str, secret) 516 | local success, ret = pcall(parse, secret, jwt_str) 517 | if not success then 518 | return { 519 | valid=false, 520 | verified=false, 521 | reason=ret[str_const.reason] or str_const.invalid_jwt 522 | } 523 | end 524 | 525 | local jwt_obj = ret 526 | jwt_obj[str_const.verified] = false 527 | jwt_obj[str_const.valid] = true 528 | return jwt_obj 529 | end 530 | 531 | --@function verify jwe object 532 | --@param secret 533 | --@param jwt object 534 | --@return jwt object with reason whether verified or not 535 | local function verify_jwe_obj(secret, jwt_obj) 536 | local key, mac_key, enc_key = derive_keys(jwt_obj.header.enc, jwt_obj.internal.key) 537 | local encoded_header = jwt_obj.internal.encoded_header 538 | 539 | local encoded_header_length = binlen(encoded_header) 540 | local mac_input = table_concat({encoded_header , jwt_obj.internal.iv, jwt_obj.internal.cipher_text , encoded_header_length}) 541 | local mac = hmac_digest(jwt_obj.header.enc, mac_key, mac_input) 542 | local auth_tag = string_sub(mac, 1, #mac/2) 543 | 544 | if auth_tag ~= jwt_obj.signature then 545 | jwt_obj[str_const.reason] = "signature mismatch: " .. 546 | tostring(jwt_obj[str_const.signature]) 547 | 548 | end 549 | jwt_obj.internal = nil 550 | jwt_obj.signature = nil 551 | 552 | if not jwt_obj[str_const.reason] then 553 | jwt_obj[str_const.verified] = true 554 | jwt_obj[str_const.reason] = str_const.everything_awesome 555 | end 556 | 557 | return jwt_obj 558 | end 559 | 560 | --@function extract certificate 561 | --@param jwt object 562 | --@return decoded certificate 563 | local function extract_certificate(jwt_obj, x5u_content_retriever) 564 | local x5c = jwt_obj[str_const.header][str_const.x5c] 565 | if x5c ~= nil and x5c[1] ~= nil then 566 | -- TODO Might want to add support for intermediaries that we 567 | -- don't have in our trusted chain (items 2... if present) 568 | local cert_str = ngx_decode_base64(x5c[1]) 569 | if not cert_str then 570 | jwt_obj[str_const.reason] = "Malformed x5c header" 571 | end 572 | 573 | return cert_str 574 | end 575 | 576 | local x5u = jwt_obj[str_const.header][str_const.x5u] 577 | if x5u ~= nil then 578 | -- TODO Ensure the url starts with https:// 579 | -- cf. https://tools.ietf.org/html/rfc7517#section-4.6 580 | 581 | if x5u_content_retriever == nil then 582 | jwt_obj[str_const.reason] = "No function has been provided to retrieve the content pointed at by the 'x5u'." 583 | return nil 584 | end 585 | 586 | -- TODO Maybe validate the url against an optional list whitelisted url prefixes? 587 | -- cf. https://news.ycombinator.com/item?id=9302394 588 | 589 | local iss = jwt_obj[str_const.payload][str_const.iss] 590 | local kid = jwt_obj[str_const.header][str_const.kid] 591 | local success, ret = pcall(x5u_content_retriever, x5u, iss, kid) 592 | 593 | if not success then 594 | jwt_obj[str_const.reason] = "An error occured while invoking the x5u_content_retriever function." 595 | return nil 596 | end 597 | 598 | return ret 599 | end 600 | 601 | -- TODO When both x5c and x5u are defined, the implementation should 602 | -- ensure their content match 603 | -- cf. https://tools.ietf.org/html/rfc7517#section-4.6 604 | 605 | jwt_obj[str_const.reason] = "Unsupported RS256 key model" 606 | return nil 607 | -- TODO - Implement jwk and kid based models... 608 | end 609 | 610 | local function get_claim_spec_from_legacy_options(self, options) 611 | local claim_spec = { } 612 | local jwt_validators = require "resty.jwt-validators" 613 | 614 | if options[str_const.valid_issuers] ~= nil then 615 | claim_spec[str_const.iss] = jwt_validators.equals_any_of(options[str_const.valid_issuers]) 616 | end 617 | 618 | if options[str_const.lifetime_grace_period] ~= nil then 619 | jwt_validators.set_system_leeway(options[str_const.lifetime_grace_period] or 0) 620 | 621 | -- If we have a leeway set, then either an NBF or an EXP should also exist requireds are added below 622 | if options[str_const.require_nbf_claim] ~= true and options[str_const.require_exp_claim] ~= true then 623 | claim_spec[str_const.full_obj] = jwt_validators.require_one_of({ str_const.nbf, str_const.exp }) 624 | end 625 | end 626 | 627 | if not is_nil_or_boolean(options[str_const.require_nbf_claim]) then 628 | error(string.format("'%s' validation option is expected to be a boolean.", str_const.require_nbf_claim), 0) 629 | end 630 | 631 | if not is_nil_or_boolean(options[str_const.require_exp_claim]) then 632 | error(string.format("'%s' validation option is expected to be a boolean.", str_const.require_exp_claim), 0) 633 | end 634 | 635 | if options[str_const.lifetime_grace_period] ~= nil or options[str_const.require_nbf_claim] ~= nil or options[str_const.require_exp_claim] ~= nil then 636 | if options[str_const.require_nbf_claim] == true then 637 | claim_spec[str_const.nbf] = jwt_validators.is_not_before() 638 | else 639 | claim_spec[str_const.nbf] = jwt_validators.opt_is_not_before() 640 | end 641 | 642 | if options[str_const.require_exp_claim] == true then 643 | claim_spec[str_const.exp] = jwt_validators.is_not_expired() 644 | else 645 | claim_spec[str_const.exp] = jwt_validators.opt_is_not_expired() 646 | end 647 | end 648 | 649 | return claim_spec 650 | end 651 | 652 | local function is_legacy_validation_options(options) 653 | 654 | -- Validation options MUST be a table 655 | if type(options) ~= str_const.table then 656 | return false 657 | end 658 | 659 | -- Validation options MUST have at least one of these, and must ONLY have these 660 | local legacy_options = { } 661 | legacy_options[str_const.valid_issuers]=1 662 | legacy_options[str_const.lifetime_grace_period]=1 663 | legacy_options[str_const.require_nbf_claim]=1 664 | legacy_options[str_const.require_exp_claim]=1 665 | 666 | local is_legacy = false 667 | for k in pairs(options) do 668 | if legacy_options[k] ~= nil then 669 | is_legacy = true 670 | else 671 | return false 672 | end 673 | end 674 | return is_legacy 675 | end 676 | 677 | -- Validates the claims for the given (parsed) object 678 | local function validate_claims(self, jwt_obj, ...) 679 | local claim_specs = {...} 680 | if #claim_specs == 0 then 681 | table.insert(claim_specs, _M:get_default_validation_options(jwt_obj)) 682 | end 683 | 684 | if jwt_obj[str_const.reason] ~= nil then 685 | return false 686 | end 687 | 688 | -- Encode the current jwt_obj and use it when calling the individual validation functions 689 | local jwt_json = cjson_encode(jwt_obj) 690 | 691 | -- Validate all our specs 692 | for _, claim_spec in ipairs(claim_specs) do 693 | if is_legacy_validation_options(claim_spec) then 694 | claim_spec = get_claim_spec_from_legacy_options(self, claim_spec) 695 | end 696 | for claim, fx in pairs(claim_spec) do 697 | if type(fx) ~= str_const.funct then 698 | error("Claim spec value must be a function - see jwt-validators.lua for helper functions", 0) 699 | end 700 | 701 | local val = claim == str_const.full_obj and cjson_decode(jwt_json) or jwt_obj.payload[claim] 702 | local success, ret = pcall(fx, val, claim, jwt_json) 703 | if not success then 704 | jwt_obj[str_const.reason] = ret.reason or string.gsub(ret, "^.-:%d-: ", "") 705 | return false 706 | elseif ret == false then 707 | jwt_obj[str_const.reason] = string.format("Claim '%s' ('%s') returned failure", claim, val) 708 | return false 709 | end 710 | end 711 | end 712 | 713 | -- Everything was good 714 | return true 715 | end 716 | 717 | --@function verify jwt object 718 | --@param secret 719 | --@param jwt_object 720 | --@leeway 721 | --@return verified jwt payload or jwt object with error code 722 | function _M.verify_jwt_obj(self, secret, jwt_obj, ...) 723 | if not jwt_obj.valid then 724 | return jwt_obj 725 | end 726 | 727 | -- validate any claims that have been passed in 728 | if not validate_claims(self, jwt_obj, ...) then 729 | return jwt_obj 730 | end 731 | 732 | -- if jwe, invoked verify jwe 733 | if jwt_obj[str_const.header][str_const.enc] then 734 | return verify_jwe_obj(secret, jwt_obj) 735 | end 736 | 737 | local alg = jwt_obj[str_const.header][str_const.alg] 738 | 739 | local jwt_str = string_format(str_const.regex_jwt_join_str, jwt_obj.raw_header , jwt_obj.raw_payload , jwt_obj.signature) 740 | 741 | if self.alg_whitelist ~= nil then 742 | if self.alg_whitelist[alg] == nil then 743 | return {verified=false, reason="whitelist unsupported alg: " .. alg} 744 | end 745 | end 746 | 747 | if alg == str_const.HS256 or alg == str_const.HS512 then 748 | local success, ret = pcall(_M.sign, self, secret, jwt_obj) 749 | if not success then 750 | -- syntax check 751 | jwt_obj[str_const.reason] = ret[str_const.reason] or str_const.internal_error 752 | elseif jwt_str ~= ret then 753 | -- signature check 754 | jwt_obj[str_const.reason] = "signature mismatch: " .. jwt_obj[str_const.signature] 755 | end 756 | elseif alg == str_const.RS256 or alg == str_const.RS512 then 757 | local cert, err 758 | if self.trusted_certs_file ~= nil then 759 | local cert_str = extract_certificate(jwt_obj, self.x5u_content_retriever) 760 | if not cert_str then 761 | return jwt_obj 762 | end 763 | cert, err = evp.Cert:new(cert_str) 764 | if not cert then 765 | jwt_obj[str_const.reason] = "Unable to extract signing cert from JWT: " .. err 766 | return jwt_obj 767 | end 768 | -- Try validating against trusted CA's, then a cert passed as secret 769 | local trusted, err = cert:verify_trust(self.trusted_certs_file) 770 | if not trusted then 771 | jwt_obj[str_const.reason] = "Cert used to sign the JWT isn't trusted: " .. err 772 | return jwt_obj 773 | end 774 | elseif secret ~= nil then 775 | local err 776 | if secret:find("CERTIFICATE") then 777 | cert, err = evp.Cert:new(secret) 778 | elseif secret:find("PUBLIC KEY") then 779 | cert, err = evp.PublicKey:new(secret) 780 | end 781 | if not cert then 782 | jwt_obj[str_const.reason] = "Decode secret is not a valid cert/public key: " .. (err and err or secret) 783 | return jwt_obj 784 | end 785 | else 786 | jwt_obj[str_const.reason] = "No trusted certs loaded" 787 | return jwt_obj 788 | end 789 | local verifier, err = evp.RSAVerifier:new(cert) 790 | if not verifier then 791 | -- Internal error case, should not happen... 792 | jwt_obj[str_const.reason] = "Failed to build verifier " .. err 793 | return jwt_obj 794 | end 795 | 796 | -- assemble jwt parts 797 | local raw_header = get_raw_part(str_const.header, jwt_obj) 798 | local raw_payload = get_raw_part(str_const.payload, jwt_obj) 799 | 800 | local message =string_format(str_const.regex_join_msg, raw_header , raw_payload) 801 | local sig = _M:jwt_decode(jwt_obj[str_const.signature], false) 802 | 803 | if not sig then 804 | jwt_obj[str_const.reason] = "Wrongly encoded signature" 805 | return jwt_obj 806 | end 807 | 808 | local verified = false 809 | local err = "verify error: reason unknown" 810 | 811 | if alg == str_const.RS256 then 812 | verified, err = verifier:verify(message, sig, evp.CONST.SHA256_DIGEST) 813 | elseif alg == str_const.RS512 then 814 | verified, err = verifier:verify(message, sig, evp.CONST.SHA512_DIGEST) 815 | end 816 | if not verified then 817 | jwt_obj[str_const.reason] = err 818 | end 819 | else 820 | jwt_obj[str_const.reason] = "Unsupported algorithm " .. alg 821 | end 822 | 823 | if not jwt_obj[str_const.reason] then 824 | jwt_obj[str_const.verified] = true 825 | jwt_obj[str_const.reason] = str_const.everything_awesome 826 | end 827 | return jwt_obj 828 | 829 | end 830 | 831 | 832 | function _M.verify(self, secret, jwt_str, ...) 833 | local jwt_obj = _M.load_jwt(self, jwt_str, secret) 834 | if not jwt_obj.valid then 835 | return {verified=false, reason=jwt_obj[str_const.reason]} 836 | end 837 | return _M.verify_jwt_obj(self, secret, jwt_obj, ...) 838 | 839 | end 840 | 841 | return _M 842 | -------------------------------------------------------------------------------- /lua/resty/rabbitmqstomp.lua: -------------------------------------------------------------------------------- 1 | -- lua-resty-rabbitmqstomp: Opinionated RabbitMQ (STOMP) client lib 2 | -- Copyright (C) 2013 Rohit 'bhaisaab' Yadav, Wingify 3 | -- Opensourced at Wingify in New Delhi under the MIT License 4 | 5 | local byte = string.byte 6 | local concat = table.concat 7 | local error = error 8 | local find = string.find 9 | local gsub = string.gsub 10 | local insert = table.insert 11 | local len = string.len 12 | local pairs = pairs 13 | local setmetatable = setmetatable 14 | local sub = string.sub 15 | local tcp = ngx.socket.tcp 16 | 17 | module(...) 18 | 19 | _VERSION = "0.1" 20 | 21 | local mt = { __index = _M } 22 | 23 | local LF = "\x0a" 24 | local EOL = "\x0d\x0a" 25 | local NULL_BYTE = "\x00" 26 | local STATE_CONNECTED = 1 27 | local STATE_COMMAND_SENT = 2 28 | 29 | 30 | function new(self, opts) 31 | local sock, err = tcp() 32 | if not sock then 33 | return nil, err 34 | end 35 | 36 | if opts == nil then 37 | opts = {username = "guest", password = "guest", vhost = "/", trailing_lf = true} 38 | end 39 | 40 | return setmetatable({ sock = sock, opts = opts}, mt) 41 | 42 | end 43 | 44 | 45 | function set_timeout(self, timeout) 46 | local sock = self.sock 47 | if not sock then 48 | return nil, "not initialized" 49 | end 50 | 51 | return sock:settimeout(timeout) 52 | end 53 | 54 | 55 | function _build_frame(self, command, headers, body) 56 | local frame = {command, EOL} 57 | 58 | if body then 59 | headers["content-length"] = len(body) 60 | end 61 | 62 | for key, value in pairs(headers) do 63 | insert(frame, key) 64 | insert(frame, ":") 65 | insert(frame, value) 66 | insert(frame, EOL) 67 | end 68 | 69 | insert(frame, EOL) 70 | 71 | if body then 72 | insert(frame, body) 73 | end 74 | 75 | insert(frame, NULL_BYTE) 76 | insert(frame, EOL) 77 | return concat(frame, "") 78 | end 79 | 80 | 81 | function _send_frame(self, frame) 82 | local sock = self.sock 83 | if not sock then 84 | return nil, "not initialized" 85 | end 86 | return sock:send(frame) 87 | end 88 | 89 | 90 | function _receive_frame(self) 91 | local sock = self.sock 92 | if not sock then 93 | return nil, "not initialized" 94 | end 95 | local resp = nil 96 | if self.opts.trailing_lf == nil or self.opts.trailing_lf == true then 97 | resp = sock:receiveuntil(NULL_BYTE .. LF, {inclusive = true}) 98 | else 99 | resp = sock:receiveuntil(NULL_BYTE, {inclusive = true}) 100 | end 101 | local data, err, partial = resp() 102 | return data, err 103 | end 104 | 105 | 106 | function _login(self) 107 | 108 | local headers = {} 109 | headers["accept-version"] = "1.2" 110 | headers["login"] = self.opts.username 111 | headers["passcode"] = self.opts.password 112 | headers["host"] = self.opts.vhost 113 | 114 | local ok, err = _send_frame(self, _build_frame(self, "CONNECT", headers, nil)) 115 | if not ok then 116 | return nil, err 117 | end 118 | 119 | local frame, err = _receive_frame(self) 120 | if not frame then 121 | return nil, err 122 | end 123 | 124 | -- We successfully received a frame, but it was an ERROR frame 125 | if sub( frame, 1, len( 'ERROR' ) ) == 'ERROR' then 126 | return nil, frame 127 | end 128 | 129 | self.state = STATE_CONNECTED 130 | return frame 131 | end 132 | 133 | 134 | function _logout(self) 135 | local sock = self.sock 136 | if not sock then 137 | self.state = nil 138 | return nil, "not initialized" 139 | end 140 | 141 | if self.state == STATE_CONNECTED then 142 | -- Graceful shutdown 143 | local headers = {} 144 | headers["receipt"] = "disconnect" 145 | sock:send(_build_frame(self, "DISCONNECT", headers, nil)) 146 | sock:receive("*a") 147 | end 148 | self.state = nil 149 | return sock:close() 150 | end 151 | 152 | 153 | function connect(self, ...) 154 | 155 | local sock = self.sock 156 | 157 | if not sock then 158 | return nil, "not initialized" 159 | end 160 | 161 | local ok, err = sock:connect(...) 162 | 163 | if not ok then 164 | return nil, "failed to connect: " .. err 165 | end 166 | 167 | local reused = sock:getreusedtimes() 168 | if reused and reused > 0 then 169 | self.state = STATE_CONNECTED 170 | return 1 171 | end 172 | 173 | return _login(self) 174 | 175 | end 176 | 177 | 178 | function send(self, msg, headers) 179 | local ok, err = _send_frame(self, _build_frame(self, "SEND", headers, msg)) 180 | if not ok then 181 | return nil, err 182 | end 183 | 184 | if headers["receipt"] ~= nil then 185 | return _receive_frame(self) 186 | end 187 | return ok, err 188 | end 189 | 190 | 191 | function subscribe(self, headers) 192 | return _send_frame(self, _build_frame(self, "SUBSCRIBE", headers)) 193 | end 194 | 195 | 196 | function unsubscribe(self, headers) 197 | return _send_frame(self, _build_frame(self, "UNSUBSCRIBE", headers)) 198 | end 199 | 200 | 201 | function receive(self) 202 | local data, err = _receive_frame(self) 203 | if not data then 204 | return nil, err 205 | end 206 | local idx = find(data, "\n\n", 1) 207 | return sub(data, idx + 2) 208 | end 209 | 210 | 211 | function set_keepalive(self, ...) 212 | local sock = self.sock 213 | if not sock then 214 | return nil, "not initialized" 215 | end 216 | 217 | if self.state ~= STATE_CONNECTED then 218 | return nil, "cannot be reused in the current connection state: " 219 | .. (self.state or "nil") 220 | end 221 | 222 | self.state = nil 223 | return sock:setkeepalive(...) 224 | end 225 | 226 | 227 | function get_reused_times(self) 228 | local sock = self.sock 229 | if not sock then 230 | return nil, "not initialized" 231 | end 232 | 233 | return sock:getreusedtimes() 234 | end 235 | 236 | 237 | function close(self) 238 | return _logout(self) 239 | end 240 | 241 | 242 | local class_mt = { 243 | -- to prevent use of casual module global variables 244 | __newindex = function (table, key, val) 245 | error('attempt to write to undeclared variable "' .. key .. '"') 246 | end 247 | } 248 | 249 | setmetatable(_M, class_mt) 250 | -------------------------------------------------------------------------------- /lua/test/login.lua: -------------------------------------------------------------------------------- 1 | local mysql = require('mysql') 2 | local jwt = require('jwt_token') 3 | local my_verify = require('my_verify') 4 | 5 | function login1(pargs) 6 | if pargs.username == nil or pargs.password == nil then 7 | ngx.say('[Error] 字段[username][password]不能为空!') 8 | return 9 | end 10 | 11 | local db = mysql.connect() 12 | if db == false then 13 | ngx.say('[Error] Mysql连接失败!') 14 | return 15 | end 16 | 17 | local res, err, errno, sqlstate = db:query("select uid from account where username=\'".. pargs.username .."\' and password=\'".. pargs.password .."\' limit 1", 1) 18 | if not res then 19 | ngx.say("select error : ", err, " , errno : ", errno, " , sqlstate : ", sqlstate) 20 | return close_db(db) 21 | end 22 | 23 | if res[1] == nil then 24 | ngx.say('[Error] 用户名或密码错误!') 25 | ngx.exit(ngx.HTTP_FORBIDDEN) 26 | 27 | end 28 | 29 | local uid = res[1].uid 30 | 31 | -- 根据uid生成token 32 | --local token, rawtoken = tokentool.gen_token(uid) 33 | local token = jwt.encode_auth_token(uid) 34 | 35 | -- token写入cookie 36 | ngx.header['Set-Cookie'] = 'auth_key='.. token ..'; path=/; Expires=' .. ngx.cookie_time(ngx.time() + 60 * 90) -- 设置Cookie过期时间为90分钟 37 | 38 | -- 用户权限信息写入redis,key为用户id 39 | my_verify.write_permission(uid) 40 | 41 | ngx.say('[Success] 登录成功! [Token] '..token) 42 | -- local ret = tokentool.add_token(token, rawtoken) 43 | -- if ret == true then 44 | -- ngx.say(token) 45 | -- else 46 | -- ngx.say('[Error] Token 写入redis失败!') 47 | -- ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) 48 | -- end 49 | end 50 | 51 | 52 | local postargs = json.decode(ngx.req.get_body_data()) 53 | login1(postargs) 54 | 55 | --[[ 56 | http://172.16.0.121/api/login 57 | { 58 | "username":"yangmv", 59 | "password":"123456" 60 | } 61 | --]] 62 | -------------------------------------------------------------------------------- /lua/test/lua.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE account( 2 | uid INTEGER NOT NULL auto_increment, 3 | username VARCHAR (64), 4 | password VARCHAR(64), 5 | email VARCHAR(128), 6 | PRIMARY KEY (uid), 7 | UNIQUE KEY email(email) 8 | ); 9 | 10 | CREATE TABLE role( 11 | `id` int(11) NOT NULL AUTO_INCREMENT , 12 | `name` varchar(255) CHARACTER SET latin5 NULL DEFAULT NULL , 13 | PRIMARY KEY (`id`) 14 | ) 15 | ENGINE=InnoDB 16 | DEFAULT CHARACTER SET=latin1 COLLATE=latin1_swedish_ci 17 | AUTO_INCREMENT=2 18 | ROW_FORMAT=COMPACT 19 | ; 20 | 21 | CREATE TABLE permission( 22 | `id` int(11) NOT NULL AUTO_INCREMENT , 23 | `permission` varchar(255) CHARACTER SET latin1 COLLATE latin1_swedish_ci NULL DEFAULT NULL , 24 | PRIMARY KEY (`id`) 25 | ) 26 | ENGINE=InnoDB 27 | DEFAULT CHARACTER SET=latin1 COLLATE=latin1_swedish_ci 28 | AUTO_INCREMENT=3 29 | ROW_FORMAT=COMPACT 30 | ; 31 | 32 | CREATE TABLE user_role( 33 | `id` int(11) NOT NULL AUTO_INCREMENT , 34 | `user_id` int(11) NULL DEFAULT NULL , 35 | `role_id` int(11) NULL DEFAULT NULL , 36 | PRIMARY KEY (`id`) 37 | ) 38 | ENGINE=InnoDB 39 | DEFAULT CHARACTER SET=latin1 COLLATE=latin1_swedish_ci 40 | AUTO_INCREMENT=2 41 | ROW_FORMAT=COMPACT 42 | ; 43 | 44 | 45 | CREATE TABLE role_permission( 46 | `id` int(11) NOT NULL AUTO_INCREMENT , 47 | `role_id` int(11) NULL DEFAULT NULL , 48 | `permission_id` int(11) NULL DEFAULT NULL , 49 | PRIMARY KEY (`id`) 50 | ) 51 | ENGINE=InnoDB 52 | DEFAULT CHARACTER SET=latin1 COLLATE=latin1_swedish_ci 53 | AUTO_INCREMENT=3 54 | ROW_FORMAT=COMPACT 55 | ; -------------------------------------------------------------------------------- /lua/test/mysql.lua: -------------------------------------------------------------------------------- 1 | module("mysql", package.seeall) 2 | local mysql = require "resty.mysql" 3 | 4 | -- connect to mysql; 5 | function connect() 6 | local db, err = mysql:new() 7 | if not db then 8 | return false 9 | end 10 | db:set_timeout(1000) 11 | 12 | local ok, err, errno, sqlstate = db:connect{ 13 | host = "127.0.0.1", 14 | port = 3306, 15 | database = "lua", 16 | user = "root", 17 | password = "", 18 | max_packet_size = 1024 * 1024 } 19 | 20 | if not ok then 21 | return false 22 | end 23 | return db 24 | end 25 | 26 | 27 | -------------------------------------------------------------------------------- /lua/test/register.lua: -------------------------------------------------------------------------------- 1 | local mysql = require('mysql') 2 | local json = require("cjson") 3 | 4 | function register1(pargs) 5 | -- 字段不能为空 6 | ngx.log(ngx.ERR,'username---->',pargs.username) 7 | if pargs.username == nil or pargs.email == nil or pargs.password == nil then 8 | ngx.say('[Error] 字段[username][email][password]不能为空!') 9 | return 10 | end 11 | 12 | local db = mysql.connect() 13 | if db == false then 14 | ngx.say('[Error] Mysql连接失败!') 15 | return 16 | end 17 | 18 | local res, err, errno, sqlstate = db:query("insert into account(username, password, email) " 19 | .. "values (\'".. pargs.username .."\',\'".. pargs.password .."\',\'".. pargs.email .."\')") 20 | if not res then 21 | ngx.say('[Error] 用户注册失败!') 22 | return 23 | end 24 | end 25 | 26 | -- post args 27 | local postargs = json.decode(ngx.req.get_body_data()) 28 | register1(postargs) 29 | 30 | 31 | --[[ 32 | http://172.16.0.121/api/register 33 | { 34 | "username":"yangmv", 35 | "email":"yangmv@qq.com", 36 | "password":"123456" 37 | } 38 | --]] -------------------------------------------------------------------------------- /lua/tools.lua: -------------------------------------------------------------------------------- 1 | module("tools", package.seeall) 2 | function split(s, p) 3 | -- 字符串切割 4 | local rt = {} 5 | string.gsub(s, '[^' .. p .. ']+', function(w) 6 | rt[#rt + 1] = w 7 | --table.insert(rt, w) 8 | end ) 9 | return rt 10 | end 11 | 12 | function list_to_str(list,x) 13 | local st = '' 14 | for i,v in ipairs(list) do 15 | st = st..x..v 16 | end 17 | st = st..x 18 | return st 19 | end 20 | 21 | function match(s,p) 22 | local s_end = string.sub(s,-1,-1) 23 | local p_end = string.sub(p,-1,-1) 24 | if s_end ~= '/' then 25 | s = s..'/' 26 | end 27 | if p_end ~= '/' then 28 | p = p..'/' 29 | end 30 | local ret = string.match(s,'^'..p) 31 | return ret 32 | end 33 | -------------------------------------------------------------------------------- /lua/upstream.lua: -------------------------------------------------------------------------------- 1 | local tools = require "tools" 2 | --local host = ngx.var.http_host 3 | local host = gw_domain_name 4 | --local uri = ngx.var.uri 5 | 6 | local _M = {} 7 | function _M.set(real_new_uri) 8 | local uri = real_new_uri 9 | -- ngx.log(ngx.ERR, "uri-------->"..uri) 10 | if uri == '/nginx-logo.png' or uri == '/poweredby.png' then 11 | return 12 | end 13 | 14 | local url_path_list = tools.split(uri, '/') 15 | local svc_code = url_path_list[1] 16 | 17 | local default_upstream = 'None' 18 | if rewrite_conf[host] ~= nil then 19 | local data = {} 20 | local key_data = {} 21 | for i, elem in ipairs(rewrite_conf[host]['rewrite_urls']) do 22 | --local ret = tools.match(uri,elem['uri']) 23 | --if ret then 24 | local ret = tools.match('/'..svc_code,elem['uri']) 25 | if '/'..svc_code == elem['uri'] then 26 | data [string.len(elem['uri'])] = elem['rewrite_upstream'] 27 | end 28 | end 29 | 30 | if next(data) ~= nil then 31 | for k,v in pairs(data) do 32 | --ngx.log(ngx.ERR, "k---->"..k,v) 33 | table.insert(key_data,k) 34 | end 35 | table.sort(key_data) --排序 36 | local new_key = key_data[#key_data] --取最后一个key 37 | default_upstream = data[new_key] 38 | -- ngx.log(ngx.ERR, "default_upstream----> "..default_upstream) 39 | end 40 | end 41 | 42 | if default_upstream ~= "None" then 43 | ngx.var.my_upstream = default_upstream 44 | table.remove(url_path_list,1) 45 | local new_uri = tools.list_to_str(url_path_list,'/') 46 | local real_url_path_list = tools.split(new_uri, '?') 47 | local real_uri = real_url_path_list[1] 48 | ngx.log(ngx.ERR,'real_uri-------->',real_uri) 49 | -- ngx.log(ngx.ERR,'new_uri-------->',new_uri) 50 | ngx.req.set_uri(real_uri, false) 51 | else 52 | return ngx.exit(404) 53 | end 54 | 55 | end 56 | return _M --------------------------------------------------------------------------------