├── 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 2 F x ATTR x x
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------