├── .busted ├── .editorconfig ├── .gitignore ├── .luacheckrc ├── .pongo └── pongorc ├── .travis.yml ├── CHANGELOG.md ├── INSTALL.txt ├── LICENSE ├── README.md ├── kong-plugin-request-transformer-1.3.2-0.rockspec ├── kong └── plugins │ └── request-transformer │ ├── access.lua │ ├── handler.lua │ ├── migrations │ ├── cassandra.lua │ ├── common.lua │ └── postgres.lua │ └── schema.lua ├── package.sh └── spec ├── 01-schema_spec.lua ├── 02-access_spec.lua └── 03-api_spec.lua /.busted: -------------------------------------------------------------------------------- 1 | return { 2 | default = { 3 | verbose = true, 4 | coverage = false, 5 | output = "gtest", 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | charset = utf-8 8 | 9 | [*.lua] 10 | indent_style = space 11 | indent_size = 2 12 | 13 | [kong/templates/nginx*] 14 | indent_style = space 15 | indent_size = 4 16 | 17 | [*.template] 18 | indent_style = space 19 | indent_size = 4 20 | 21 | [Makefile] 22 | indent_style = tab 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # servroot is typically the nginx/Kong workingdirectory when testing 2 | servroot 3 | 4 | # packed distribution format for LuaRocks 5 | *.rock 6 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | std = "ngx_lua" 2 | unused_args = false 3 | redefined = false 4 | max_line_length = false 5 | 6 | 7 | globals = { 8 | --"_KONG", 9 | "kong", 10 | --"ngx.IS_CLI", 11 | } 12 | 13 | 14 | not_globals = { 15 | "string.len", 16 | "table.getn", 17 | } 18 | 19 | 20 | ignore = { 21 | --"6.", -- ignore whitespace warnings 22 | } 23 | 24 | 25 | exclude_files = { 26 | "kong-ce/**/*.lua", 27 | --"spec-old-api/fixtures/invalid-module.lua", 28 | } 29 | 30 | 31 | --files["kong/plugins/ldap-auth/*.lua"] = { 32 | -- read_globals = { 33 | -- "bit.mod", 34 | -- "string.pack", 35 | -- "string.unpack", 36 | -- }, 37 | --} 38 | 39 | 40 | files["spec/**/*.lua"] = { 41 | std = "ngx_lua+busted", 42 | } 43 | -------------------------------------------------------------------------------- /.pongo/pongorc: -------------------------------------------------------------------------------- 1 | --postgres 2 | --cassandra 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: focal 2 | jobs: 3 | include: 4 | - name: Kong CE 2.0.x 5 | env: KONG_VERSION=2.0.x 6 | - name: Kong CE 2.1.x 7 | env: KONG_VERSION=2.1.x 8 | - name: Kong CE 2.2.x 9 | env: KONG_VERSION=2.2.x 10 | - name: Enterprise 2.1.4.x 11 | env: KONG_VERSION=2.1.4.x 12 | - name: Enterprise 2.2.0.x 13 | env: KONG_VERSION=2.2.0.x 14 | - name: Nightly EE-master 15 | env: KONG_VERSION=nightly-ee POSTGRES=latest CASSANDRA=latest 16 | env: 17 | global: 18 | # for Enterprise images 19 | - PULP_USERNAME=admin 20 | - secure: Pr2465dOELvOogXcqwzr8ytq1wjzROkt5jM7C4QB8eTkLzEWm922hs84o1E3cHJLcqCyKaY+4VCgaAGB0iHeV9Kc2J8yvROa3YdmI9AF8TqNNACumMPSsS2kZE5Hr/jXaP9JqRCa84ZDPCgGnXnFIi55GdosQh6oEYisdcflPAD+o61cx9fUsj/vHw/gSFb/9tZ8RR1yDI/qunc382495MnC+OuqUdeP5ZgXABrs1KI7C6cWZWGX+RwYn+E1HkjYkEr3CabBDMzKNvkBR2sWOw8BhmcOyIOZYPxlhOoxTnmcS8r4/bdglUJ/ZGFtONweobE+zYYv8zv6Li2CbQIPDMWz4u60TGYn65mEyPmqFU/jm0EbR9VJ9k3TDt3HElmSqGlgvRtND9BUp11hNQz+wCcVWyc3frJyiWBGG+nOxeN3tPiBWOL29A9f3FhVRDBMT70qZLGCIAGl4BtJvmQ/ExVoOKRWnS1XH12XYUR8vfQJqFqOsu8/hxZv+evagBul8ErJB4NZFEY87pYe9naVJUDfqBOHnxPV/QZicD2TvBecgWdk+/5RZMSQBVKYD8cLJwU6VlNC0QyQ6nQrXPlOxGlQaTuvjFI0cmpE+bJKFRfohkzA5BgNFHDJWVyEyZ7DVjMK74faPr+lqELZnBYgBSCMlhE4Nh6jtDmGo6FG1bY= 21 | # for Enterprise nightly images 22 | - DOCKER_USERNAME=kongcloudpull 23 | - secure: EzPrLUd0m7XxbQlnzzMHP9zQVngONbtwv0nvNp5JkDIY51DwgbnUG5cnIB6XGqeewIspMsoBQP8IZPjY2hd0ZZrf3SK8PYU6oBIA4eqm1auCMF6bnwgBMGpLmalfwt9STGNUbD5wbFhGUsU8lBBvUPovjAJm8BDwRNAm59NPxC6GymG/nygE/bPr4RP9FiyBQ2tqYOW1+J6BVx29+Djn2brYgCu8C+05xwt9KVSBWrItLg2V/45w8qPOpOSuFwnS3Y0l5pC1xh0cX9pBjLlKbbdufMm00yMQnb3iQ6h32QBoiu5NRWFq6zWKzZXE/oVNUqOMF+YeUzN2+XC3sExcrFVe26/8Ajh2N7DyNyRPuk/+W6igrNOP4iL6qqRomoOmKU6MBY9pYeFjNPuxw9pgJUPVfGGxLXS4dTbQJcvgu8YDqE/btZturgW1/H8EC5bF9mdSdVc6JdckmBhHHHcurWv8zzL1dYfKqdqWnyAbmtFjDaw8uhOxdjUZqENQS8gmIucRjLwBcqdrLhadYzFxPjI3EAeMMAM/UpT225+dMlGXU6j6Wnhq1/cyPYys9pikgiHGFSY4KdSzFfBIvCeEefcJNI0GRFsJugqfhdhHsMpyI/3VteGWP003WS1GD2dvcglxkUD4tHeBnYqf8XND4PX9yWR1DoBjzvlVJV84iLE= 24 | install: 25 | - echo "$DOCKER_KEY" | docker login -u "$DOCKER_USER" --password-stdin 26 | - git clone --single-branch https://github.com/Kong/kong-pongo ../kong-pongo 27 | - "../kong-pongo/pongo.sh up" 28 | - "../kong-pongo/pongo.sh build" 29 | script: 30 | - "../kong-pongo/pongo.sh lint" 31 | - "../kong-pongo/pongo.sh run" 32 | notifications: 33 | slack: 34 | if: branch = master AND type != pull_request 35 | on_success: change 36 | on_failure: always 37 | rooms: 38 | secure: H7cwgmVIQGH4zIjwUdPEmEBCbnP0K696lEjx2g2L35uj4r9qt/3iwLzNLURTmz/R5Jlo6NZypCL8Zcv4ujSuh7UR1P/ob53Ca8az9y0vZsjz68HRrQ1UM9CljEN5Y/8F2J69wtfjx52LNC6BGVov0IyXTnUI/cLPylbgmyJoguUKnlsFo1WYn357R6dNwHS5n6jKAgGETvkhO1QCZuS15YX0BGIQw4Wt1OV1b/1T9Gm7JGLz51VrGig4G+V8mem040hju+wxJpKAcwxMqBhB/onu88CQjYjpVN2vHY5WTEdWCPjCU+BBAMGeiKt1nJVr5GQKFFdhvr8gmECSi7QKOi14kt40+1YUbq3ZwemZyTcIucFlMkvaGvOvDl8dRbPAe3Vy8Yh7hQAnFHdlVyYyfr0Tw5qnJrpDLVspmSbCy3J+vsafrEhXKQAsOhyU0ANmyEt0tJiXPA5DMQph/oACF24GIzlARDDfFvknGlXjA4D1VCtVUy90OWtQPBoNBinLAth60P5wIGF0k6/LX1I6iv+sJyCFlnCagVtzJI51frCU3rpg5K5CLUMvD11kTiZ8v61IVQrCahUkWfHYHl5dy7iDb9TdYoKSj1vPee/IYt4bOlPsECSMshIXAsz3cZfn2RLJhOBPEWzpdPiobpjST1+NACJUkD5qRZQBw6gknfg= 39 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.3.2 2 | 3 | ### Fixed 4 | 5 | - Marked array fields with `default` value as `required` to prevent possible 6 | `nil` / `null` errors on runtime. 7 | - Keep configured name case on append/add headers, fixes #28 8 | 9 | ## 1.3.1 10 | 11 | ### Fixed 12 | 13 | - Fix sandbox environment check 14 | 15 | ## 1.3.0 16 | 17 | ### Added 18 | 19 | - Include the `type` function in template environment. 20 | Because headers may contain array elements such as duplicated headers, 21 | `type` is a useful function in these cases. 22 | 23 | ## 1.2.8 24 | 25 | ### Fixed 26 | 27 | - Accept '#' as a non-special template value 28 | 29 | ## 1.2.7 30 | 31 | ### Fixed 32 | 33 | - Fix the construction of the error message when a template throws a Lua error. 34 | [#25](https://github.com/Kong/kong-plugin-request-transformer/issues/25) 35 | 36 | ## 1.2.6 37 | 38 | ### Fixed 39 | 40 | - Correct short circuit conditional for query strings (#24) 41 | 42 | ## 1.2.5 43 | 44 | ### Fixed 45 | 46 | - Fix the error message that is displayed in error logs when the template 47 | is invalid (#13) 48 | 49 | ## 1.2.4 50 | 51 | ### Changed 52 | 53 | - Remove the no-longer supported `run_on` field from plugin config schema 54 | 55 | ### Fixed 56 | 57 | - None 58 | 59 | ## 1.2.3 60 | 61 | ### Changed 62 | 63 | - Allow rendering values stored in `kong.ctx.shared` from the template renderer environment 64 | 65 | ### Fixed 66 | 67 | - Fixed bug on adding a header with the same name as a removed one doesn't behave correctly 68 | 69 | ## 1.2.2 70 | 71 | ### Changed 72 | 73 | - Remove leftover `print` call from schema validator 74 | 75 | ### Fixed 76 | 77 | - Fix issue preventing JSON body transformation to be executed on empty body 78 | upon Content-Type rewrite to `application/json` 79 | [#1](https://github.com/Kong/kong-plugin-request-transformer/issues/1) 80 | 81 | ## 1.2.1 82 | 83 | ### Changed 84 | 85 | - Remove dependency to `BasePlugin` (not needed anymore) 86 | 87 | ## 0.35 88 | 89 | ### Changed 90 | 91 | - Convert to new dao 92 | 93 | ## 0.34.0 94 | 95 | ### Changed 96 | - Internal improvements 97 | 98 | ## 0.1.0 99 | 100 | - `pre-function` and `post-function` enterprise plugins added 101 | -------------------------------------------------------------------------------- /INSTALL.txt: -------------------------------------------------------------------------------- 1 | ========================================== 2 | Installation guide for Kong custom plugins 3 | ========================================== 4 | 5 | -------------------------------- 6 | | Kong version | 0.35.0 | 7 | |-----------------|------------| 8 | | Latest revision | 2019/04/14 | 9 | -------------------------------- 10 | 11 | Custom plugins for Kong consist of Lua source files that need to be in the file 12 | system of each of your Kong nodes. This guide will provide you with step-by-step 13 | instructions that will make a Kong node aware of your custom plugin(s). 14 | 15 | These steps should be applied to each node in your Kong cluster, so that the 16 | custom plugin(s) are available on each one of them. 17 | 18 | Prerequisite: Kong must be installed on the host. 19 | 20 | 21 | 1. Extract the custom plugin's sources 22 | ====================================== 23 | 24 | You were provided with an archive containing the sources and documentation for 25 | your plugin. You can extract it with: 26 | 27 | $ tar -xvf -.tar.gz 28 | 29 | Where is the name of the plugin, and its version 30 | number. 31 | 32 | The contents of this archive should be close to the following: 33 | 34 | $ tree 35 | 36 | ├── INSTALL.txt 37 | ├── README.md 38 | ├── kong 39 | │ └── plugins 40 | │ └── 41 | │ ├── handler.lua 42 | │ └── schema.lua 43 | └── -.rockspec 44 | 45 | * README.md is the documentation for this plugin: it covers topics such as 46 | its functionalities, configuration capabilities, usage examples, etc. 47 | * INSTALL.txt is this file. 48 | * `kong/plugins/` is a directory containing the Lua sources 49 | for this plugin. It contains at least 2 files: `handler.lua` and 50 | `schema.lua`. 51 | * `-.rockspec` is a file describing the Lua sources 52 | in case you wish to install this plugin via LuaRocks, as described later. 53 | 54 | 55 | 2. Install the custom plugin's sources 56 | ====================================== 57 | 58 | For a Kong node to be able to use the custom plugin, the custom plugin's Lua 59 | sources must be installed on your host's file system. There are two ways of 60 | doing so: via LuaRocks, or manually. Choose one of the two, and jump to 61 | section 3. 62 | 63 | 1. Via LuaRocks 64 | 65 | If the `luarocks` utility is installed in your system (this is likely the 66 | case if you used one of the official installation packages), you can 67 | install the Lua sources in your LuaRocks tree (a directory in which 68 | LuaRocks installs Lua modules). 69 | 70 | You can do so by changing the current directory to the extracted archive, 71 | where the rockspec file is: 72 | 73 | $ cd 74 | 75 | And then run the following: 76 | 77 | $ luarocks make 78 | 79 | This will install the Lua sources in `kong/plugins/` in your 80 | system's LuaRocks tree, where all the Kong sources are already present. 81 | 82 | 2. Manually 83 | 84 | A more conservative way of installing your plugin's sources is 85 | to avoid "polluting" the LuaRocks tree, and instead, point Kong 86 | to the directory containing them. 87 | 88 | This is done by tweaking the `lua_package_path` property of your Kong 89 | configuration. Under the hood, this property is an alias to the `LUA_PATH` 90 | variable of the Lua VM, if you are familiar with it. 91 | 92 | Those properties contain a semicolon-separated list of directories in 93 | which to search for Lua sources. It should be set like so in your Kong 94 | configuration file: 95 | 96 | lua_package_path = //?.lua;; 97 | 98 | Where: 99 | 100 | * `/` is the path to the directory containing the 101 | extracted archive. It should be the location of the `kong` directory 102 | from the archive. 103 | * `?` is a placeholder that will be replaced by 104 | `kong.plugins.` when Kong will try to load your plugin. Do 105 | not change it. 106 | * `;;` a placeholder for the "the default Lua path". Do not change it. 107 | 108 | Example: 109 | 110 | The plugin `something` being located on the file system such that the 111 | handler file is: 112 | 113 | /usr/local/kong/plugins/something/handler.lua 114 | 115 | The location of the `kong` directory is: /usr/local, hence the 116 | proper path setup would be: 117 | 118 | lua_package_path = /usr/local/?.lua;; 119 | 120 | Multiple plugins: 121 | 122 | If you wish to install two or more custom plugins this way, you can set 123 | the variable to something like: 124 | 125 | lua_package_path = /path/to/plugin1/?.lua;/path/to/plugin2/?.lua;; 126 | 127 | * `;` is the separator between directories. 128 | * `;;` still means "the default Lua path". 129 | 130 | Note: you can also set this property via its environment variable 131 | equivalent: `KONG_LUA_PACKAGE_PATH`. 132 | 133 | Reminder: regardless of which method you are using to install your plugin's 134 | sources, you must still do so for each node in your Kong nodes. 135 | 136 | 137 | 3. Instruct Kong to load your custom plugin 138 | =========================================== 139 | 140 | You must now add the custom plugin's name to the `plugins` list in your 141 | Kong configuration (on each Kong node): 142 | 143 | plugins = 144 | 145 | If you are using two or more custom plugins, insert commas in between, like so: 146 | 147 | plugins = plugin1,plugin2 148 | 149 | Note: you can also set this property via its environment variable equivalent: 150 | `KONG_PLUGINS`. 151 | 152 | Reminder: don't forget to update the `plugins` directive for each node 153 | in your Kong cluster. 154 | 155 | 156 | 4. Start Kong 157 | ============= 158 | 159 | You should now be able to start Kong without any issue. Consult your custom 160 | plugin's README.md file for instructions on how to enable/configure your plugin 161 | on an API or Consumer object. 162 | 163 | To make sure your plugin is being loaded by Kong, you can start Kong with a 164 | `debug` log level: 165 | 166 | log_level = debug 167 | 168 | or: 169 | 170 | KONG_LOG_LEVEL=debug 171 | 172 | Then, you should see the following log for each plugin being loaded: 173 | 174 | [debug] Loading plugin 175 | 176 | 177 | 5. Removing a plugin 178 | ==================== 179 | There are three steps to completely remove a plugin. 180 | 181 | 1. remove the plugin from your Kong api configuration. Make sure that it 182 | is no longer applied globally nor for any api or consumer. This has to be 183 | done only once for the entire Kong cluster, no restart/reload required. 184 | This step in itself will make that the plugin is no longer used. But 185 | it is still possible to re-apply the plugin. 186 | 187 | 2. remove the plugin from the `plugins` directive (on each Kong node). 188 | Make sure to have completed step 1 before doing so. After this step 189 | it will be impossible for anyone to re-apply the plugin to any Kong 190 | api, consumer, or even globally. This step requires to restart/reload the 191 | Kong node to take effect. 192 | 193 | 3. delete the plugin related files from each of the Kong nodes (for the 194 | paranoid only). Make sure to have completed step 2, including restarting/ 195 | reloading Kong, before deleting the files. If you used LuaRocks to install 196 | the plugin, you can do `luarocks remove ` to remove it. 197 | 198 | 199 | 6. Troubleshooting 200 | ================== 201 | 202 | Kong can fail to start because of a misconfigured custom plugin for several 203 | reasons: 204 | 205 | * "plugin is in use but not enabled" -> this error means that you configured a 206 | custom plugin from another node, and that the plugin configuration is in the 207 | database, but that the current node you are trying to start does not have it 208 | in its `plugins` directive. 209 | To resolve, add the plugin's name to the node's `plugins` directive. 210 | 211 | * "plugin is enabled but not installed" -> this means that the plugin's name 212 | is present in the `plugins` directive, but that Kong is unable to load 213 | the `handler.lua` source file from the file system. 214 | To resolve, make sure that the lua_package_path directive is properly set to 215 | load this plugin's Lua sources. 216 | 217 | * "no configuration schema found for plugin" -> the plugin is installed, 218 | enabled in `plugins`, but Kong is unable to load the `schema.lua` 219 | source file from the file system. 220 | To resolve, make sure that the `schema.lua` file is present alongside the 221 | plugin's `handler.lua` file. 222 | 223 | Feel free to contact for further troubleshooting. 224 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2019 Kong Inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kong request transformer plugin 2 | 3 | ## :warning: This plugin is now maintained as part of [Kong](https://github.com/Kong/kong). Please open Issues and PRs in that repository 4 | 5 | ## :open_book: For documentation, please visit https://docs.konghq.com/hub/kong-inc/request-transformer 6 | -------------------------------------------------------------------------------- /kong-plugin-request-transformer-1.3.2-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "kong-plugin-request-transformer" 2 | version = "1.3.2-0" 3 | 4 | source = { 5 | url = "git://github.com/Kong/kong-plugin-request-transformer", 6 | tag = "1.3.2" 7 | } 8 | 9 | supported_platforms = {"linux", "macosx"} 10 | description = { 11 | summary = "Kong Request Transformer Plugin", 12 | license = "Apache 2.0", 13 | } 14 | 15 | build = { 16 | type = "builtin", 17 | modules = { 18 | ["kong.plugins.request-transformer.migrations.cassandra"] = "kong/plugins/request-transformer/migrations/cassandra.lua", 19 | ["kong.plugins.request-transformer.migrations.postgres"] = "kong/plugins/request-transformer/migrations/postgres.lua", 20 | ["kong.plugins.request-transformer.migrations.common"] = "kong/plugins/request-transformer/migrations/common.lua", 21 | ["kong.plugins.request-transformer.handler"] = "kong/plugins/request-transformer/handler.lua", 22 | ["kong.plugins.request-transformer.access"] = "kong/plugins/request-transformer/access.lua", 23 | ["kong.plugins.request-transformer.schema"] = "kong/plugins/request-transformer/schema.lua", 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /kong/plugins/request-transformer/access.lua: -------------------------------------------------------------------------------- 1 | local multipart = require "multipart" 2 | local cjson = require "cjson" 3 | local pl_template = require "pl.template" 4 | local pl_tablex = require "pl.tablex" 5 | 6 | local table_insert = table.insert 7 | local get_uri_args = kong.request.get_query 8 | local set_uri_args = kong.service.request.set_query 9 | local clear_header = kong.service.request.clear_header 10 | local get_header = kong.request.get_header 11 | local set_header = kong.service.request.set_header 12 | local get_headers = kong.request.get_headers 13 | local set_headers = kong.service.request.set_headers 14 | local set_method = kong.service.request.set_method 15 | local set_path = kong.service.request.set_path 16 | local get_raw_body = kong.request.get_raw_body 17 | local set_raw_body = kong.service.request.set_raw_body 18 | local encode_args = ngx.encode_args 19 | local ngx_decode_args = ngx.decode_args 20 | local type = type 21 | local str_find = string.find 22 | local pcall = pcall 23 | local pairs = pairs 24 | local error = error 25 | local rawset = rawset 26 | local pl_copy_table = pl_tablex.deepcopy 27 | 28 | local _M = {} 29 | local template_cache = setmetatable( {}, { __mode = "k" }) 30 | local template_environment 31 | 32 | local DEBUG = ngx.DEBUG 33 | local CONTENT_LENGTH = "content-length" 34 | local CONTENT_TYPE = "content-type" 35 | local HOST = "host" 36 | local JSON, MULTI, ENCODED = "json", "multi_part", "form_encoded" 37 | local EMPTY = pl_tablex.readonly({}) 38 | 39 | 40 | local compile_opts = { 41 | escape = "\xff", -- disable '#' as a valid template escape 42 | } 43 | 44 | 45 | local function parse_json(body) 46 | if body then 47 | local status, res = pcall(cjson.decode, body) 48 | if status then 49 | return res 50 | end 51 | end 52 | end 53 | 54 | local function decode_args(body) 55 | if body then 56 | return ngx_decode_args(body) 57 | end 58 | return {} 59 | end 60 | 61 | local function get_content_type(content_type) 62 | if content_type == nil then 63 | return 64 | end 65 | if str_find(content_type:lower(), "application/json", nil, true) then 66 | return JSON 67 | elseif str_find(content_type:lower(), "multipart/form-data", nil, true) then 68 | return MULTI 69 | elseif str_find(content_type:lower(), "application/x-www-form-urlencoded", nil, true) then 70 | return ENCODED 71 | end 72 | end 73 | 74 | -- meta table for the sandbox, exposing lazily loaded values 75 | local __meta_environment = { 76 | __index = function(self, key) 77 | local lazy_loaders = { 78 | headers = function(self) 79 | return get_headers() or EMPTY 80 | end, 81 | query_params = function(self) 82 | return get_uri_args() or EMPTY 83 | end, 84 | uri_captures = function(self) 85 | return (ngx.ctx.router_matches or EMPTY).uri_captures or EMPTY 86 | end, 87 | shared = function(self) 88 | return ((kong or EMPTY).ctx or EMPTY).shared or EMPTY 89 | end, 90 | } 91 | local loader = lazy_loaders[key] 92 | if not loader then 93 | -- we don't have a loader, so just return nothing 94 | return 95 | end 96 | -- set the result on the table to not load again 97 | local value = loader() 98 | rawset(self, key, value) 99 | return value 100 | end, 101 | __newindex = function(self) 102 | error("This environment is read-only.") 103 | end, 104 | } 105 | 106 | template_environment = setmetatable({ 107 | -- here we can optionally add functions to expose to the sandbox, eg: 108 | -- tostring = tostring, -- for example 109 | -- because headers may contain array elements such as duplicated headers 110 | -- type is a useful function in these cases. See issue #25. 111 | type = type, 112 | }, __meta_environment) 113 | 114 | local function clear_environment(conf) 115 | rawset(template_environment, "headers", nil) 116 | rawset(template_environment, "query_params", nil) 117 | rawset(template_environment, "uri_captures", nil) 118 | rawset(template_environment, "shared", nil) 119 | end 120 | 121 | local function param_value(source_template, config_array) 122 | if not source_template or source_template == "" then 123 | return nil 124 | end 125 | 126 | -- find compiled templates for this plugin-configuration array 127 | local compiled_templates = template_cache[config_array] 128 | if not compiled_templates then 129 | compiled_templates = {} 130 | -- store it by `config_array` which is part of the plugin `conf` table 131 | -- it will be GC'ed at the same time as `conf` and hence invalidate the 132 | -- compiled templates here as well as the cache-table has weak-keys 133 | template_cache[config_array] = compiled_templates 134 | end 135 | 136 | -- Find or compile the specific template 137 | local compiled_template = compiled_templates[source_template] 138 | if not compiled_template then 139 | compiled_template = assert(pl_template.compile(source_template, compile_opts)) 140 | compiled_templates[source_template] = compiled_template 141 | end 142 | 143 | return compiled_template:render(template_environment) 144 | end 145 | 146 | local function iter(config_array) 147 | return function(config_array, i, previous_name, previous_value) 148 | i = i + 1 149 | local current_pair = config_array[i] 150 | if current_pair == nil then -- n + 1 151 | return nil 152 | end 153 | 154 | local current_name, current_value = current_pair:match("^([^:]+):*(.-)$") 155 | 156 | if current_value == "" then 157 | return i, current_name 158 | end 159 | 160 | local res, err = param_value(current_value, config_array) 161 | if err then 162 | return error("[request-transformer] failed to render the template " .. 163 | current_value .. ", error:" .. err) 164 | end 165 | 166 | kong.log.debug("[request-transformer] template `", current_value, 167 | "` rendered to `", res, "`") 168 | 169 | return i, current_name, res 170 | end, config_array, 0 171 | end 172 | 173 | local function append_value(current_value, value) 174 | local current_value_type = type(current_value) 175 | 176 | if current_value_type == "string" then 177 | return { current_value, value } 178 | elseif current_value_type == "table" then 179 | table_insert(current_value, value) 180 | return current_value 181 | else 182 | return { value } 183 | end 184 | end 185 | 186 | local function transform_headers(conf) 187 | local headers = get_headers() 188 | local headers_to_remove = {} 189 | 190 | headers.host = nil 191 | 192 | -- Remove header(s) 193 | for _, name, value in iter(conf.remove.headers) do 194 | name = name:lower() 195 | if headers[name] then 196 | headers[name] = nil 197 | headers_to_remove[name] = true 198 | end 199 | end 200 | 201 | -- Rename headers(s) 202 | for _, old_name, new_name in iter(conf.rename.headers) do 203 | old_name = old_name:lower() 204 | if headers[old_name] then 205 | local value = headers[old_name] 206 | headers[new_name] = value 207 | headers[old_name] = nil 208 | headers_to_remove[old_name] = true 209 | end 210 | end 211 | 212 | -- Replace header(s) 213 | for _, name, value in iter(conf.replace.headers) do 214 | name = name:lower() 215 | if headers[name] or name == HOST then 216 | headers[name] = value 217 | end 218 | end 219 | 220 | -- Add header(s) 221 | for _, name, value in iter(conf.add.headers) do 222 | if not headers[name] and name:lower() ~= HOST then 223 | headers[name] = value 224 | end 225 | end 226 | 227 | -- Append header(s) 228 | for _, name, value in iter(conf.append.headers) do 229 | local name_lc = name:lower() 230 | 231 | if name_lc ~= HOST and name ~= name_lc and headers[name] ~= nil then 232 | -- keep original content, use configd case 233 | -- note: the __index method of table returned by ngx.req.get_header 234 | -- is overwritten to check for lower case as well, see documentation 235 | -- for ngx.req.get_header to get more information 236 | -- effectively, it does this: headers[name] = headers[name] or headers[name_lc] 237 | headers[name] = headers[name] 238 | headers[name_lc] = nil 239 | end 240 | 241 | headers[name] = append_value(headers[name], value) 242 | end 243 | 244 | for name, _ in pairs(headers_to_remove) do 245 | clear_header(name) 246 | end 247 | 248 | set_headers(headers) 249 | end 250 | 251 | local function transform_querystrings(conf) 252 | 253 | if not (#conf.remove.querystring > 0 or #conf.rename.querystring > 0 or 254 | #conf.replace.querystring > 0 or #conf.add.querystring > 0 or 255 | #conf.append.querystring > 0) then 256 | return 257 | end 258 | 259 | local querystring = pl_copy_table(template_environment.query_params) 260 | 261 | -- Remove querystring(s) 262 | for _, name, value in iter(conf.remove.querystring) do 263 | querystring[name] = nil 264 | end 265 | 266 | -- Rename querystring(s) 267 | for _, old_name, new_name in iter(conf.rename.querystring) do 268 | local value = querystring[old_name] 269 | querystring[new_name] = value 270 | querystring[old_name] = nil 271 | end 272 | 273 | for _, name, value in iter(conf.replace.querystring) do 274 | if querystring[name] then 275 | querystring[name] = value 276 | end 277 | end 278 | 279 | -- Add querystring(s) 280 | for _, name, value in iter(conf.add.querystring) do 281 | if not querystring[name] then 282 | querystring[name] = value 283 | end 284 | end 285 | 286 | -- Append querystring(s) 287 | for _, name, value in iter(conf.append.querystring) do 288 | querystring[name] = append_value(querystring[name], value) 289 | end 290 | set_uri_args(querystring) 291 | end 292 | 293 | local function transform_json_body(conf, body, content_length) 294 | local removed, renamed, replaced, added, appended = false, false, false, false, false 295 | local content_length = (body and #body) or 0 296 | local parameters = parse_json(body) 297 | if parameters == nil then 298 | if content_length > 0 then 299 | return false, nil 300 | end 301 | parameters = {} 302 | end 303 | 304 | if content_length > 0 and #conf.remove.body > 0 then 305 | for _, name, value in iter(conf.remove.body) do 306 | parameters[name] = nil 307 | removed = true 308 | end 309 | end 310 | 311 | if content_length > 0 and #conf.rename.body > 0 then 312 | for _, old_name, new_name in iter(conf.rename.body) do 313 | local value = parameters[old_name] 314 | parameters[new_name] = value 315 | parameters[old_name] = nil 316 | renamed = true 317 | end 318 | end 319 | 320 | if content_length > 0 and #conf.replace.body > 0 then 321 | for _, name, value in iter(conf.replace.body) do 322 | if parameters[name] then 323 | parameters[name] = value 324 | replaced = true 325 | end 326 | end 327 | end 328 | 329 | if #conf.add.body > 0 then 330 | for _, name, value in iter(conf.add.body) do 331 | if not parameters[name] then 332 | parameters[name] = value 333 | added = true 334 | end 335 | end 336 | end 337 | 338 | if #conf.append.body > 0 then 339 | for _, name, value in iter(conf.append.body) do 340 | local old_value = parameters[name] 341 | parameters[name] = append_value(old_value, value) 342 | appended = true 343 | end 344 | end 345 | 346 | if removed or renamed or replaced or added or appended then 347 | return true, cjson.encode(parameters) 348 | end 349 | end 350 | 351 | local function transform_url_encoded_body(conf, body, content_length) 352 | local renamed, removed, replaced, added, appended = false, false, false, false, false 353 | local parameters = decode_args(body) 354 | 355 | if content_length > 0 and #conf.remove.body > 0 then 356 | for _, name, value in iter(conf.remove.body) do 357 | parameters[name] = nil 358 | removed = true 359 | end 360 | end 361 | 362 | if content_length > 0 and #conf.rename.body > 0 then 363 | for _, old_name, new_name in iter(conf.rename.body) do 364 | local value = parameters[old_name] 365 | parameters[new_name] = value 366 | parameters[old_name] = nil 367 | renamed = true 368 | end 369 | end 370 | 371 | if content_length > 0 and #conf.replace.body > 0 then 372 | for _, name, value in iter(conf.replace.body) do 373 | if parameters[name] then 374 | parameters[name] = value 375 | replaced = true 376 | end 377 | end 378 | end 379 | 380 | if #conf.add.body > 0 then 381 | for _, name, value in iter(conf.add.body) do 382 | if parameters[name] == nil then 383 | parameters[name] = value 384 | added = true 385 | end 386 | end 387 | end 388 | 389 | if #conf.append.body > 0 then 390 | for _, name, value in iter(conf.append.body) do 391 | local old_value = parameters[name] 392 | parameters[name] = append_value(old_value, value) 393 | appended = true 394 | end 395 | end 396 | 397 | if removed or renamed or replaced or added or appended then 398 | return true, encode_args(parameters) 399 | end 400 | end 401 | 402 | local function transform_multipart_body(conf, body, content_length, content_type_value) 403 | local removed, renamed, replaced, added, appended = false, false, false, false, false 404 | local parameters = multipart(body and body or "", content_type_value) 405 | 406 | if content_length > 0 and #conf.rename.body > 0 then 407 | for _, old_name, new_name in iter(conf.rename.body) do 408 | if parameters:get(old_name) then 409 | local value = parameters:get(old_name).value 410 | parameters:set_simple(new_name, value) 411 | parameters:delete(old_name) 412 | renamed = true 413 | end 414 | end 415 | end 416 | 417 | if content_length > 0 and #conf.remove.body > 0 then 418 | for _, name, value in iter(conf.remove.body) do 419 | parameters:delete(name) 420 | removed = true 421 | end 422 | end 423 | 424 | if content_length > 0 and #conf.replace.body > 0 then 425 | for _, name, value in iter(conf.replace.body) do 426 | if parameters:get(name) then 427 | parameters:delete(name) 428 | parameters:set_simple(name, value) 429 | replaced = true 430 | end 431 | end 432 | end 433 | 434 | if #conf.add.body > 0 then 435 | for _, name, value in iter(conf.add.body) do 436 | if not parameters:get(name) then 437 | parameters:set_simple(name, value) 438 | added = true 439 | end 440 | end 441 | end 442 | 443 | if removed or renamed or replaced or added or appended then 444 | return true, parameters:tostring() 445 | end 446 | end 447 | 448 | local function transform_body(conf) 449 | local content_type_value = get_header(CONTENT_TYPE) 450 | local content_type = get_content_type(content_type_value) 451 | if content_type == nil or #conf.rename.body < 1 and 452 | #conf.remove.body < 1 and #conf.replace.body < 1 and 453 | #conf.add.body < 1 and #conf.append.body < 1 then 454 | return 455 | end 456 | 457 | -- Call req_read_body to read the request body first 458 | local body = get_raw_body() 459 | local is_body_transformed = false 460 | local content_length = (body and #body) or 0 461 | 462 | if content_type == ENCODED then 463 | is_body_transformed, body = transform_url_encoded_body(conf, body, content_length) 464 | elseif content_type == MULTI then 465 | is_body_transformed, body = transform_multipart_body(conf, body, content_length, content_type_value) 466 | elseif content_type == JSON then 467 | is_body_transformed, body = transform_json_body(conf, body, content_length) 468 | end 469 | 470 | if is_body_transformed then 471 | set_raw_body(body) 472 | set_header(CONTENT_LENGTH, #body) 473 | end 474 | end 475 | 476 | local function transform_method(conf) 477 | if conf.http_method then 478 | set_method(conf.http_method:upper()) 479 | if conf.http_method == "GET" or conf.http_method == "HEAD" or conf.http_method == "TRACE" then 480 | local content_type_value = get_header(CONTENT_TYPE) 481 | local content_type = get_content_type(content_type_value) 482 | if content_type == ENCODED then 483 | -- Also put the body into querystring 484 | local body = get_raw_body() 485 | local parameters = decode_args(body) 486 | 487 | -- Append to querystring 488 | if type(parameters) == "table" and next(parameters) then 489 | local querystring = get_uri_args() 490 | for name, value in pairs(parameters) do 491 | if querystring[name] then 492 | if type(querystring[name]) == "table" then 493 | append_value(querystring[name], value) 494 | else 495 | querystring[name] = { querystring[name], value } 496 | end 497 | else 498 | querystring[name] = value 499 | end 500 | end 501 | 502 | set_uri_args(querystring) 503 | end 504 | end 505 | end 506 | end 507 | end 508 | 509 | local function transform_uri(conf) 510 | if conf.replace.uri then 511 | 512 | local res, err = param_value(conf.replace.uri, conf.replace) 513 | if err then 514 | error("[request-transformer] failed to render the template " .. 515 | tostring(conf.replace.uri) .. ", error:" .. err) 516 | end 517 | 518 | kong.log.debug(DEBUG, "[request-transformer] template `", conf.replace.uri, 519 | "` rendered to `", res, "`") 520 | 521 | if res then 522 | set_path(res) 523 | end 524 | end 525 | end 526 | 527 | function _M.execute(conf) 528 | clear_environment() 529 | transform_uri(conf) 530 | transform_method(conf) 531 | transform_headers(conf) 532 | transform_body(conf) 533 | transform_querystrings(conf) 534 | end 535 | 536 | return _M 537 | -------------------------------------------------------------------------------- /kong/plugins/request-transformer/handler.lua: -------------------------------------------------------------------------------- 1 | local access = require "kong.plugins.request-transformer.access" 2 | 3 | 4 | local RequestTransformerHandler = { 5 | VERSION = "1.3.2", 6 | PRIORITY = 801, 7 | } 8 | 9 | 10 | function RequestTransformerHandler:access(conf) 11 | access.execute(conf) 12 | end 13 | 14 | 15 | return RequestTransformerHandler 16 | -------------------------------------------------------------------------------- /kong/plugins/request-transformer/migrations/cassandra.lua: -------------------------------------------------------------------------------- 1 | local common = require "kong.plugins.request-transformer.migrations.common" 2 | 3 | 4 | return { 5 | { 6 | name = "2019-05-21-120000_request-transformer-advanced-rename", 7 | up = common.rt_rename, 8 | down = function() end, 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /kong/plugins/request-transformer/migrations/common.lua: -------------------------------------------------------------------------------- 1 | local utils = require "kong.tools.utils" 2 | 3 | 4 | local _M = {} 5 | 6 | 7 | function _M.rt_rename(_, _, dao) 8 | local plugins, err = dao.plugins:find_all( 9 | { name = "request-transformer-advanced" }) 10 | if err then 11 | return err 12 | end 13 | 14 | for i = 1, #plugins do 15 | local plugin = plugins[i] 16 | local _, err = dao.plugins:insert({ 17 | name = "request-transformer", 18 | api_id = plugin.api_id, 19 | consumer_id = plugin.consumer_id, 20 | enabled = plugin.enabled, 21 | config = utils.deep_copy(plugin.config), 22 | }) 23 | if err then 24 | return err 25 | end 26 | 27 | -- drop the old entry 28 | local _, err = dao.plugins:delete(plugin, { quite = true }) 29 | if err then 30 | return err 31 | end 32 | end 33 | end 34 | 35 | 36 | return _M 37 | 38 | -------------------------------------------------------------------------------- /kong/plugins/request-transformer/migrations/postgres.lua: -------------------------------------------------------------------------------- 1 | local common = require "kong.plugins.request-transformer.migrations.common" 2 | 3 | 4 | return { 5 | { 6 | name = "2017-11-28-120000_request-transformer-advanced-rename", 7 | up = common.rt_rename, 8 | down = function() end, 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /kong/plugins/request-transformer/schema.lua: -------------------------------------------------------------------------------- 1 | local pl_template = require "pl.template" 2 | local tx = require "pl.tablex" 3 | local typedefs = require "kong.db.schema.typedefs" 4 | local validate_header_name = require("kong.tools.utils").validate_header_name 5 | 6 | 7 | local compile_opts = { 8 | escape = "\xff", -- disable '#' as a valid template escape 9 | } 10 | 11 | 12 | -- entries must have colons to set the key and value apart 13 | local function check_for_value(entry) 14 | local name, value = entry:match("^([^:]+):*(.-)$") 15 | if not name or not value or value == "" then 16 | return false, "key '" ..name.. "' has no value" 17 | end 18 | 19 | local status, res, err = pcall(pl_template.compile, value, compile_opts) 20 | if not status or err then 21 | return false, "value '" .. value .. 22 | "' is not in supported format, error:" .. 23 | (status and res or err) 24 | end 25 | return true 26 | end 27 | 28 | 29 | local function validate_headers(pair, validate_value) 30 | local name, value = pair:match("^([^:]+):*(.-)$") 31 | if validate_header_name(name) == nil then 32 | return nil, string.format("'%s' is not a valid header", tostring(name)) 33 | end 34 | 35 | if validate_value then 36 | if validate_header_name(value) == nil then 37 | return nil, string.format("'%s' is not a valid header", tostring(value)) 38 | end 39 | end 40 | return true 41 | end 42 | 43 | 44 | local function validate_colon_headers(pair) 45 | return validate_headers(pair, true) 46 | end 47 | 48 | 49 | local strings_array = { 50 | type = "array", 51 | default = {}, 52 | required = true, 53 | elements = { type = "string" }, 54 | } 55 | 56 | 57 | local headers_array = { 58 | type = "array", 59 | default = {}, 60 | required = true, 61 | elements = { type = "string", custom_validator = validate_headers }, 62 | } 63 | 64 | 65 | local strings_array_record = { 66 | type = "record", 67 | fields = { 68 | { body = strings_array }, 69 | { headers = headers_array }, 70 | { querystring = strings_array }, 71 | }, 72 | } 73 | 74 | 75 | local colon_strings_array = { 76 | type = "array", 77 | default = {}, 78 | required = true, 79 | elements = { type = "string", custom_validator = check_for_value } 80 | } 81 | 82 | 83 | local colon_header_value_array = { 84 | type = "array", 85 | default = {}, 86 | required = true, 87 | elements = { type = "string", match = "^[^:]+:.*$", custom_validator = validate_headers }, 88 | } 89 | 90 | 91 | local colon_strings_array_record = { 92 | type = "record", 93 | fields = { 94 | { body = colon_strings_array }, 95 | { headers = colon_header_value_array }, 96 | { querystring = colon_strings_array }, 97 | }, 98 | } 99 | 100 | 101 | local colon_headers_array = { 102 | type = "array", 103 | default = {}, 104 | required = true, 105 | elements = { type = "string", match = "^[^:]+:.*$", custom_validator = validate_colon_headers }, 106 | } 107 | 108 | 109 | local colon_rename_strings_array_record = { 110 | type = "record", 111 | fields = { 112 | { body = colon_strings_array }, 113 | { headers = colon_headers_array }, 114 | { querystring = colon_strings_array }, 115 | }, 116 | } 117 | 118 | 119 | local colon_strings_array_record_plus_uri = tx.deepcopy(colon_strings_array_record) 120 | local uri = { uri = { type = "string" } } 121 | table.insert(colon_strings_array_record_plus_uri.fields, uri) 122 | 123 | 124 | return { 125 | name = "request-transformer", 126 | fields = { 127 | { config = { 128 | type = "record", 129 | fields = { 130 | { http_method = typedefs.http_method }, 131 | { remove = strings_array_record }, 132 | { rename = colon_rename_strings_array_record }, 133 | { replace = colon_strings_array_record_plus_uri }, 134 | { add = colon_strings_array_record }, 135 | { append = colon_strings_array_record }, 136 | } 137 | }, 138 | }, 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | PLUGIN=`basename "$PWD"` 6 | VERSION=`echo *.rockspec | sed "s/^.*-\([0-9.]*\.[0-9]*\.[0.-9]*-[0-9]*\)\.rockspec/\1/"` 7 | 8 | #------------------------------------------------------- 9 | # Remove existing archive directory and create a new one 10 | #------------------------------------------------------- 11 | rm -rf $PLUGIN || true 12 | rm -f $PLUGIN-$VERSION.tar.gz || true 13 | mkdir -p $PLUGIN 14 | 15 | #---------------------------------------------- 16 | # Copy files to be archived to archive directory 17 | #---------------------------------------------- 18 | cp -R ./kong $PLUGIN 19 | cp INSTALL.txt README.md LICENSE *.rockspec $PLUGIN 20 | 21 | #-------------- 22 | # Archive files 23 | #-------------- 24 | tar cvzf $PLUGIN-$VERSION.tar.gz $PLUGIN 25 | 26 | #------------------------- 27 | # Remove archive directory 28 | #------------------------- 29 | rm -rf $PLUGIN || true 30 | 31 | #------------------------- 32 | # Create a rock 33 | #------------------------- 34 | luarocks make 35 | luarocks pack $PLUGIN $VERSION 36 | -------------------------------------------------------------------------------- /spec/01-schema_spec.lua: -------------------------------------------------------------------------------- 1 | local request_transformer_schema = require "kong.plugins.request-transformer.schema" 2 | local v = require("spec.helpers").validate_plugin_config_schema 3 | 4 | describe("Plugin: request-transformer(schema)", function() 5 | it("validates http_method", function() 6 | local ok, err = v({ http_method = "GET" }, request_transformer_schema) 7 | assert.truthy(ok) 8 | assert.falsy(err) 9 | end) 10 | it("errors invalid http_method", function() 11 | local ok, err = v({ http_method = "HELLO!" }, request_transformer_schema) 12 | assert.falsy(ok) 13 | assert.equal("invalid value: HELLO!", err.config.http_method) 14 | end) 15 | it("validate regex pattern as value", function() 16 | local config = { 17 | add = { 18 | querystring = {"uri_param1:$(uri_captures.user1)", "uri_param2:$(uri_captures.user2)"}, 19 | } 20 | } 21 | local ok, err = v(config, request_transformer_schema) 22 | assert.truthy(ok) 23 | assert.is_nil(err) 24 | end) 25 | it("validate string as value", function() 26 | local config = { 27 | add = { 28 | querystring = {"uri_param1:$(uri_captures.user1)", "uri_param2:value"}, 29 | } 30 | } 31 | local ok, err = v(config, request_transformer_schema) 32 | assert.truthy(ok) 33 | assert.is_nil(err) 34 | end) 35 | it("error for missing value", function() 36 | local config = { 37 | add = { 38 | querystring = {"uri_param2:"}, 39 | } 40 | } 41 | local ok, err = v(config, request_transformer_schema) 42 | assert.falsy(ok) 43 | assert.not_nil(err) 44 | end) 45 | it("error for malformed regex pattern in value", function() 46 | local config = { 47 | add = { 48 | querystring = {"uri_param2:$(uri_captures user2)"}, 49 | } 50 | } 51 | local ok, err = v(config, request_transformer_schema) 52 | assert.falsy(ok) 53 | assert.not_nil(err) 54 | end) 55 | end) 56 | 57 | -------------------------------------------------------------------------------- /spec/02-access_spec.lua: -------------------------------------------------------------------------------- 1 | local admin_api = require "spec.fixtures.admin_api" 2 | local helpers = require "spec.helpers" 3 | local cjson = require "cjson" 4 | local pl_file = require "pl.file" 5 | 6 | 7 | local function count_log_lines(pattern) 8 | local cfg = helpers.test_conf 9 | local logs = pl_file.read(cfg.prefix .. "/" .. cfg.proxy_error_log) 10 | local _, count = logs:gsub(pattern, "") 11 | return count 12 | end 13 | 14 | 15 | for _, strategy in helpers.each_strategy() do 16 | describe("Plugin: request-transformer(access) [#" .. strategy .. "]", function() 17 | local client 18 | 19 | lazy_setup(function() 20 | local bp = helpers.get_db_utils(strategy, { 21 | "routes", 22 | "services", 23 | "plugins", 24 | }) 25 | 26 | local route1 = bp.routes:insert({ 27 | hosts = { "test1.test" } 28 | }) 29 | local route2 = bp.routes:insert({ 30 | hosts = { "test2.test" }, 31 | preserve_host = true, 32 | }) 33 | local route3 = bp.routes:insert({ 34 | hosts = { "test3.test" } 35 | }) 36 | local route4 = bp.routes:insert({ 37 | hosts = { "test4.test" } 38 | }) 39 | local route5 = bp.routes:insert({ 40 | hosts = { "test5.test" } 41 | }) 42 | local route6 = bp.routes:insert({ 43 | hosts = { "test6.test" } 44 | }) 45 | local route7 = bp.routes:insert({ 46 | hosts = { "test7.test" } 47 | }) 48 | local route8 = bp.routes:insert({ 49 | hosts = { "test8.test" } 50 | }) 51 | local route9 = bp.routes:insert({ 52 | hosts = { "test9.test" } 53 | }) 54 | local route10 = bp.routes:insert({ 55 | hosts = { "test10.test" }, 56 | paths = { "/requests/user1/(?P\\w+)/user2/(?P\\S+)" }, 57 | strip_path = false 58 | }) 59 | local route11 = bp.routes:insert({ 60 | hosts = { "test11.test" }, 61 | paths = { "/requests/user1/(?P\\w+)/user2/(?P\\S+)" } 62 | }) 63 | local route12 = bp.routes:insert({ 64 | hosts = { "test12.test" }, 65 | paths = { "/requests/" }, 66 | strip_path = false 67 | }) 68 | local route13 = bp.routes:insert({ 69 | hosts = { "test13.test" }, 70 | paths = { "/requests/user1/(?P\\w+)/user2/(?P\\S+)" } 71 | }) 72 | local route14 = bp.routes:insert({ 73 | hosts = { "test14.test" }, 74 | paths = { "/user1/(?P\\w+)/user2/(?P\\S+)" } 75 | }) 76 | local route15 = bp.routes:insert({ 77 | hosts = { "test15.test" }, 78 | paths = { "/requests/user1/(?\\w+)/user2/(?\\S+)" }, 79 | strip_path = false 80 | }) 81 | local route16 = bp.routes:insert({ 82 | hosts = { "test16.test" }, 83 | paths = { "/requests/user1/(?\\w+)/user2/(?\\S+)" }, 84 | strip_path = false 85 | }) 86 | local route17 = bp.routes:insert({ 87 | hosts = { "test17.test" }, 88 | paths = { "/requests/user1/(?\\w+)/user2/(?\\S+)" }, 89 | strip_path = false 90 | }) 91 | local route18 = bp.routes:insert({ 92 | hosts = { "test18.test" }, 93 | paths = { "/requests/user1/(?\\w+)/user2/(?\\S+)" }, 94 | strip_path = false 95 | }) 96 | local route19 = bp.routes:insert({ 97 | hosts = { "test19.test" }, 98 | paths = { "/requests/user1/(?\\w+)/user2/(?\\S+)" }, 99 | strip_path = false 100 | }) 101 | local route20 = bp.routes:insert({ 102 | hosts = { "test20.test" } 103 | }) 104 | local route21 = bp.routes:insert({ 105 | hosts = { "test21.test" } 106 | }) 107 | local route22 = bp.routes:insert({ 108 | hosts = { "test22.test" } 109 | }) 110 | local route23 = bp.routes:insert({ 111 | hosts = { "test23.test" }, 112 | paths = { "/request" } 113 | }) 114 | local route24 = bp.routes:insert({ 115 | hosts = { "test24.test" } 116 | }) 117 | local route25 = bp.routes:insert({ 118 | hosts = { "test25.test" } 119 | }) 120 | local route26 = bp.routes:insert({ 121 | hosts = { "test26.test" } 122 | }) 123 | 124 | local route27 = bp.routes:insert({ 125 | hosts = { "test27.test" } 126 | }) 127 | 128 | local route28 = bp.routes:insert({ 129 | hosts = { "test28.test" } 130 | }) 131 | 132 | bp.plugins:insert { 133 | route = { id = route1.id }, 134 | name = "request-transformer", 135 | config = { 136 | add = { 137 | headers = {"h1:v1", "h2:value:2"}, -- payload containing a colon 138 | querystring = {"q1:v1"}, 139 | body = {"p1:v1"} 140 | } 141 | } 142 | } 143 | bp.plugins:insert { 144 | route = { id = route2.id }, 145 | name = "request-transformer", 146 | config = { 147 | add = { 148 | headers = {"host:mark"} 149 | } 150 | } 151 | } 152 | bp.plugins:insert { 153 | route = { id = route3.id }, 154 | name = "request-transformer", 155 | config = { 156 | add = { 157 | headers = {"x-added:a1", "x-added2:b1", "x-added3:c2"}, 158 | querystring = {"query-added:newvalue", "p1:anything:1"}, -- payload containing a colon 159 | body = {"newformparam:newvalue"} 160 | }, 161 | remove = { 162 | headers = {"x-to-remove"}, 163 | querystring = {"toremovequery"} 164 | }, 165 | append = { 166 | headers = {"x-added:a2", "x-added:a3"}, 167 | querystring = {"p1:a2", "p2:b1"} 168 | }, 169 | replace = { 170 | headers = {"x-to-replace:false"}, 171 | querystring = {"toreplacequery:no"} 172 | } 173 | } 174 | } 175 | bp.plugins:insert { 176 | route = { id = route4.id }, 177 | name = "request-transformer", 178 | config = { 179 | remove = { 180 | headers = {"x-to-remove"}, 181 | querystring = {"q1"}, 182 | body = {"toremoveform"} 183 | } 184 | } 185 | } 186 | bp.plugins:insert { 187 | route = { id = route5.id }, 188 | name = "request-transformer", 189 | config = { 190 | replace = { 191 | headers = {"h1:v1"}, 192 | querystring = {"q1:v1"}, 193 | body = {"p1:v1"} 194 | } 195 | } 196 | } 197 | bp.plugins:insert { 198 | route = { id = route6.id }, 199 | name = "request-transformer", 200 | config = { 201 | append = { 202 | headers = {"h1:v1", "h1:v2", "h2:v1",}, 203 | querystring = {"q1:v1", "q1:v2", "q2:v1"}, 204 | body = {"p1:v1", "p1:v2", "p2:value:1"} -- payload containing a colon 205 | } 206 | } 207 | } 208 | bp.plugins:insert { 209 | route = { id = route7.id }, 210 | name = "request-transformer", 211 | config = { 212 | http_method = "POST" 213 | } 214 | } 215 | bp.plugins:insert { 216 | route = { id = route8.id }, 217 | name = "request-transformer", 218 | config = { 219 | http_method = "GET" 220 | } 221 | } 222 | 223 | bp.plugins:insert { 224 | route = { id = route9.id }, 225 | name = "request-transformer", 226 | config = { 227 | rename = { 228 | headers = {"x-to-rename:x-is-renamed"}, 229 | querystring = {"originalparam:renamedparam"}, 230 | body = {"originalparam:renamedparam"} 231 | } 232 | } 233 | } 234 | 235 | bp.plugins:insert { 236 | route = { id = route10.id }, 237 | name = "request-transformer", 238 | config = { 239 | add = { 240 | querystring = {"uri_param1:$(uri_captures.user1)", "uri_param2[some_index][1]:$(uri_captures.user2)"}, 241 | } 242 | } 243 | } 244 | 245 | bp.plugins:insert { 246 | route = { id = route11.id }, 247 | name = "request-transformer", 248 | config = { 249 | replace = { 250 | uri = "/requests/user2/$(uri_captures.user2)/user1/$(uri_captures.user1)", 251 | } 252 | } 253 | } 254 | 255 | bp.plugins:insert { 256 | route = { id = route12.id }, 257 | name = "request-transformer", 258 | config = { 259 | add = { 260 | querystring = {"uri_param1:$(uri_captures.user1 or 'default1')", "uri_param2:$(uri_captures.user2 or 'default2')"}, 261 | } 262 | } 263 | } 264 | 265 | bp.plugins:insert { 266 | route = { id = route13.id }, 267 | name = "request-transformer", 268 | config = { 269 | replace = { 270 | uri = "/requests/user2/$(10 * uri_captures.user1)", 271 | } 272 | } 273 | } 274 | 275 | bp.plugins:insert { 276 | route = { id = route14.id }, 277 | name = "request-transformer", 278 | config = { 279 | replace = { 280 | uri = "/requests$(uri_captures[0])", 281 | } 282 | } 283 | } 284 | 285 | bp.plugins:insert { 286 | route = { id = route15.id }, 287 | name = "request-transformer", 288 | config = { 289 | add = { 290 | querystring = {"uri_param1:$(uri_captures.user1)", "uri_param2:$(headers.host)"}, 291 | headers = {"x-test-header:$(query_params.q1)"} 292 | }, 293 | remove = { 294 | querystring = {"q1"}, 295 | } 296 | } 297 | } 298 | 299 | bp.plugins:insert { 300 | route = { id = route16.id }, 301 | name = "request-transformer", 302 | config = { 303 | replace = { 304 | querystring = {"q2:$(headers['x-remove-header'])"}, 305 | }, 306 | add = { 307 | querystring = {"q1:$(uri_captures.user1)"}, 308 | headers = {"x-test-header:$(headers['x-remove-header'])"} 309 | }, 310 | remove = { 311 | headers = {"x-remove-header"} 312 | }, 313 | } 314 | } 315 | 316 | bp.plugins:insert { 317 | route = { id = route17.id }, 318 | name = "request-transformer", 319 | config = { 320 | replace = { 321 | querystring = {"q2:$(headers['x-replace-header'])"}, 322 | headers = {"x-replace-header:the new value"} 323 | }, 324 | add = { 325 | querystring = {"q1:$(uri_captures.user1)"}, 326 | headers = {"x-test-header:$(headers['x-replace-header'])"} 327 | } 328 | } 329 | } 330 | 331 | bp.plugins:insert { 332 | route = { id = route18.id }, 333 | name = "request-transformer", 334 | config = { 335 | add = { 336 | querystring = {[[q1:$('$(uri_captures.user1)')]]}, 337 | } 338 | } 339 | } 340 | 341 | bp.plugins:insert { 342 | route = { id = route19.id }, 343 | name = "request-transformer", 344 | config = { 345 | add = { 346 | -- will trigger a runtime error 347 | querystring = {[[q1:$(ERROR())]]}, 348 | } 349 | } 350 | } 351 | 352 | bp.plugins:insert { 353 | route = { id = route20.id }, 354 | name = "request-transformer", 355 | config = { 356 | http_method = "POST", 357 | add = { 358 | headers = { 359 | "Content-Type:application/json" 360 | }, 361 | body = { "body:somecontent" } 362 | }, 363 | } 364 | } 365 | 366 | do 367 | -- 2 plugins: 368 | -- pre-function: plugin to inject a shared value in the kong.ctx.shared table 369 | -- transformer: pick up the injected value and add to the query string 370 | bp.plugins:insert { 371 | route = { id = route21.id }, 372 | name = "pre-function", 373 | config = { 374 | functions = { 375 | [[ 376 | kong.ctx.shared.my_version = "1.2.3" 377 | ]] 378 | }, 379 | } 380 | } 381 | bp.plugins:insert { 382 | route = { id = route21.id }, 383 | name = "request-transformer", 384 | config = { 385 | add = { 386 | querystring = {"shared_param1:$(shared.my_version)"}, 387 | } 388 | } 389 | } 390 | end 391 | bp.plugins:insert { 392 | route = { id = route22.id }, 393 | name = "request-transformer", 394 | config = { 395 | http_method = "POST", 396 | remove = { 397 | headers = { "Authorization" }, 398 | }, 399 | add = { 400 | headers = { "Authorization:Basic test" }, 401 | }, 402 | }, 403 | } 404 | 405 | bp.plugins:insert { 406 | route = { id = route23.id }, 407 | name = "request-transformer", 408 | config = {} 409 | } 410 | 411 | bp.plugins:insert { 412 | route = { id = route24.id }, 413 | name = "request-transformer", 414 | config = { 415 | add = { 416 | headers = { "x-user-agent:$(foo(headers[\"User-Agent\"]) == \"table\" and headers[\"User-Agent\"][1] or headers[\"User-Agent\"])", }, 417 | }, 418 | } 419 | } 420 | 421 | bp.plugins:insert { 422 | route = { id = route25.id }, 423 | name = "request-transformer", 424 | config = { 425 | add = { 426 | headers = { "X-Foo-Transformed:$(type(headers[\"X-Foo\"]) == \"table\" and headers[\"X-Foo\"][1] .. \"-first\" or headers[\"X-Foo\"])", }, 427 | }, 428 | } 429 | } 430 | 431 | bp.plugins:insert { 432 | route = { id = route26.id }, 433 | name = "request-transformer", 434 | config = { 435 | add = { 436 | headers = {"X-aDDed:a1", "x-aDDed2:b1", "x-Added3:c2"}, 437 | }, 438 | remove = { 439 | headers = {"X-To-Remove"}, 440 | }, 441 | append = { 442 | headers = {"X-aDDed:a2", "X-aDDed:a3"}, 443 | }, 444 | replace = { 445 | headers = {"x-to-Replace:false"}, 446 | } 447 | } 448 | } 449 | 450 | bp.plugins:insert { 451 | route = { id = route27.id }, 452 | name = "request-transformer", 453 | config = { 454 | replace = { 455 | uri = "/requests/tést", 456 | } 457 | } 458 | } 459 | 460 | -- transformer attempts to inject a value in ngx.ctx.shared, but that will result in an invalid template 461 | -- which provokes a failure 462 | bp.plugins:insert { 463 | route = { id = route28.id }, 464 | name = "request-transformer", 465 | config = { 466 | add = { 467 | headers = { "X-Write-Attempt:$(shared.written = true)" }, 468 | } 469 | } 470 | } 471 | 472 | assert(helpers.start_kong({ 473 | database = strategy, 474 | plugins = "bundled, request-transformer", 475 | nginx_conf = "spec/fixtures/custom_nginx.template", 476 | })) 477 | end) 478 | 479 | lazy_teardown(function() 480 | helpers.stop_kong() 481 | end) 482 | 483 | before_each(function() 484 | client = helpers.proxy_client() 485 | end) 486 | 487 | after_each(function() 488 | if client then client:close() end 489 | end) 490 | 491 | describe("http method", function() 492 | it("changes the HTTP method from GET to POST", function() 493 | local r = assert(client:send { 494 | method = "GET", 495 | path = "/request?hello=world&name=marco", 496 | headers = { 497 | host = "test7.test" 498 | } 499 | }) 500 | assert.response(r).has.status(200) 501 | local json = assert.response(r).has.jsonbody() 502 | assert.equal("POST", json.vars.request_method) 503 | assert.equal("world", json.uri_args.hello) 504 | assert.equal("marco", json.uri_args.name) 505 | end) 506 | it("changes the HTTP method from POST to GET", function() 507 | local r = assert(client:send { 508 | method = "POST", 509 | path = "/request?hello=world", 510 | body = { 511 | name = "marco" 512 | }, 513 | headers = { 514 | ["Content-Type"] = "application/x-www-form-urlencoded", 515 | host = "test8.test" 516 | } 517 | }) 518 | assert.response(r).has.status(200) 519 | local json = assert.response(r).has.jsonbody() 520 | assert.equal("GET", json.vars.request_method) 521 | assert.equal("world", json.uri_args.hello) 522 | assert.equal("marco", json.uri_args.name) 523 | end) 524 | it("changes the HTTP method from GET to POST and adds JSON body", function() 525 | local r = assert(client:send { 526 | method = "GET", 527 | path = "/request", 528 | headers = { 529 | host = "test20.test", 530 | } 531 | }) 532 | assert.response(r).has.status(200) 533 | local json = assert.response(r).has.jsonbody() 534 | assert.request(r).has.jsonbody() 535 | assert.equal("POST", json.vars.request_method) 536 | assert.is_nil(json.post_data.error) 537 | local header_content_type = assert.request(r).has.header("Content-Type") 538 | assert.equals("application/json", header_content_type) 539 | end) 540 | end) 541 | describe("remove", function() 542 | it("specified header", function() 543 | local r = assert(client:send { 544 | method = "GET", 545 | path = "/request", 546 | headers = { 547 | host = "test4.test", 548 | ["x-to-remove"] = "true", 549 | ["x-another-header"] = "true" 550 | } 551 | }) 552 | assert.response(r).has.status(200) 553 | assert.response(r).has.jsonbody() 554 | assert.request(r).has.no.header("x-to-remove") 555 | assert.request(r).has.header("x-another-header") 556 | end) 557 | it("parameters on url encoded form POST", function() 558 | local r = assert(client:send { 559 | method = "POST", 560 | path = "/request", 561 | body = { 562 | ["toremoveform"] = "yes", 563 | ["nottoremove"] = "yes" 564 | }, 565 | headers = { 566 | ["Content-Type"] = "application/x-www-form-urlencoded", 567 | host = "test4.test" 568 | } 569 | }) 570 | assert.response(r).has.status(200) 571 | assert.response(r).has.jsonbody() 572 | assert.request(r).has.no.formparam("toremoveform") 573 | local value = assert.request(r).has.formparam("nottoremove") 574 | assert.equals("yes", value) 575 | end) 576 | it("parameters from JSON body in POST", function() 577 | local r = assert(client:send { 578 | method = "POST", 579 | path = "/request", 580 | body = { 581 | ["toremoveform"] = "yes", 582 | ["nottoremove"] = "yes" 583 | }, 584 | headers = { 585 | host = "test4.test", 586 | ["content-type"] = "application/json" 587 | } 588 | }) 589 | assert.response(r).has.status(200) 590 | assert.response(r).has.jsonbody() 591 | local json = assert.request(r).has.jsonbody() 592 | assert.is_nil(json.params["toremoveform"]) 593 | assert.equals("yes", json.params["nottoremove"]) 594 | end) 595 | it("does not fail if JSON body is malformed in POST", function() 596 | local r = assert(client:send { 597 | method = "POST", 598 | path = "/request", 599 | body = "malformed json body", 600 | headers = { 601 | host = "test4.test", 602 | ["content-type"] = "application/json" 603 | } 604 | }) 605 | assert.response(r).has.status(200) 606 | local json = assert.response(r).has.jsonbody() 607 | assert.equals("json (error)", json.post_data.kind) 608 | assert.not_nil(json.post_data.error) 609 | end) 610 | it("does not fail if body is empty and content type is application/json in POST", function() 611 | local r = assert(client:send { 612 | method = "POST", 613 | path = "/request", 614 | body = {}, 615 | headers = { 616 | host = "test4.test", 617 | ["content-type"] = "application/json" 618 | } 619 | }) 620 | assert.response(r).has.status(200) 621 | local json = assert.response(r).has.jsonbody() 622 | assert.equals('{}', json.post_data.text) 623 | assert.equals("2", json.headers["content-length"]) 624 | end) 625 | it("does not fail if body is empty in POST", function() 626 | local r = assert(client:send { 627 | method = "POST", 628 | path = "/request", 629 | body = "", 630 | headers = { 631 | host = "test4.test" 632 | } 633 | }) 634 | assert.response(r).has.status(200) 635 | local json = assert.response(r).has.jsonbody() 636 | assert.same(cjson.null, json.post_data.params) 637 | assert.equal('', json.post_data.text) 638 | local value = assert.request(r).has.header("content-length") 639 | assert.equal("0", value) 640 | end) 641 | it("parameters on multipart POST", function() 642 | local r = assert(client:send { 643 | method = "POST", 644 | path = "/request", 645 | body = { 646 | ["toremoveform"] = "yes", 647 | ["nottoremove"] = "yes" 648 | }, 649 | headers = { 650 | ["Content-Type"] = "multipart/form-data", 651 | host = "test4.test" 652 | } 653 | }) 654 | assert.response(r).has.status(200) 655 | assert.response(r).has.jsonbody() 656 | assert.request(r).has.no.formparam("toremoveform") 657 | local value = assert.request(r).has.formparam("nottoremove") 658 | assert.equals("yes", value) 659 | end) 660 | it("args on GET if it exist", function() 661 | local r = assert( client:send { 662 | method = "GET", 663 | path = "/request", 664 | query = { 665 | q1 = "v1", 666 | q2 = "v2", 667 | }, 668 | body = { 669 | hello = "world" 670 | }, 671 | headers = { 672 | ["Content-Type"] = "application/x-www-form-urlencoded", 673 | host = "test4.test" 674 | } 675 | }) 676 | assert.response(r).has.status(200) 677 | assert.response(r).has.jsonbody() 678 | assert.request(r).has.no.queryparam("q1") 679 | local value = assert.request(r).has.queryparam("q2") 680 | assert.equals("v2", value) 681 | end) 682 | end) 683 | 684 | describe("rename", function() 685 | it("specified header", function() 686 | local r = assert(client:send { 687 | method = "GET", 688 | path = "/request", 689 | headers = { 690 | host = "test9.test", 691 | ["x-to-rename"] = "true", 692 | ["x-another-header"] = "true" 693 | } 694 | }) 695 | assert.response(r).has.status(200) 696 | assert.response(r).has.jsonbody() 697 | assert.request(r).has.no.header("x-to-rename") 698 | assert.request(r).has.header("x-is-renamed") 699 | assert.request(r).has.header("x-another-header") 700 | end) 701 | it("does not add as new header if header does not exist", function() 702 | local r = assert( client:send { 703 | method = "GET", 704 | path = "/request", 705 | body = {}, 706 | headers = { 707 | host = "test9.test", 708 | ["x-a-header"] = "true", 709 | } 710 | }) 711 | assert.response(r).has.status(200) 712 | assert.response(r).has.jsonbody() 713 | assert.request(r).has.no.header("renamedparam") 714 | local h_a_header = assert.request(r).has.header("x-a-header") 715 | assert.equals("true", h_a_header) 716 | end) 717 | it("specified parameters in url encoded body on POST", function() 718 | local r = assert(client:send { 719 | method = "POST", 720 | path = "/request", 721 | body = { 722 | originalparam = "yes", 723 | }, 724 | headers = { 725 | ["Content-Type"] = "application/x-www-form-urlencoded", 726 | host = "test9.test" 727 | } 728 | }) 729 | assert.response(r).has.status(200) 730 | assert.response(r).has.jsonbody() 731 | assert.request(r).has.no.formparam("originalparam") 732 | local value = assert.request(r).has.formparam("renamedparam") 733 | assert.equals("yes", value) 734 | end) 735 | it("does not add as new parameter in url encoded body if parameter does not exist on POST", function() 736 | local r = assert(client:send { 737 | method = "POST", 738 | path = "/request", 739 | body = { 740 | ["x-a-header"] = "true", 741 | }, 742 | headers = { 743 | ["Content-Type"] = "application/x-www-form-urlencoded", 744 | host = "test9.test" 745 | } 746 | }) 747 | assert.response(r).has.status(200) 748 | assert.request(r).has.no.formparam("renamedparam") 749 | local value = assert.request(r).has.formparam("x-a-header") 750 | assert.equals("true", value) 751 | end) 752 | it("parameters from JSON body in POST", function() 753 | local r = assert(client:send { 754 | method = "POST", 755 | path = "/request", 756 | body = { 757 | ["originalparam"] = "yes", 758 | ["nottorename"] = "yes" 759 | }, 760 | headers = { 761 | host = "test9.test", 762 | ["content-type"] = "application/json" 763 | } 764 | }) 765 | assert.response(r).has.status(200) 766 | assert.response(r).has.jsonbody() 767 | local json = assert.request(r).has.jsonbody() 768 | assert.is_nil(json.params["originalparam"]) 769 | assert.is_not_nil(json.params["renamedparam"]) 770 | assert.equals("yes", json.params["renamedparam"]) 771 | assert.equals("yes", json.params["nottorename"]) 772 | end) 773 | it("does not fail if JSON body is malformed in POST", function() 774 | local r = assert(client:send { 775 | method = "POST", 776 | path = "/request", 777 | body = "malformed json body", 778 | headers = { 779 | host = "test9.test", 780 | ["content-type"] = "application/json" 781 | } 782 | }) 783 | assert.response(r).has.status(200) 784 | local json = assert.response(r).has.jsonbody() 785 | assert.equals("json (error)", json.post_data.kind) 786 | assert.is_not_nil(json.post_data.error) 787 | end) 788 | it("parameters on multipart POST", function() 789 | local r = assert(client:send { 790 | method = "POST", 791 | path = "/request", 792 | body = { 793 | ["originalparam"] = "yes", 794 | ["nottorename"] = "yes" 795 | }, 796 | headers = { 797 | ["Content-Type"] = "multipart/form-data", 798 | host = "test9.test" 799 | } 800 | }) 801 | assert.response(r).has.status(200) 802 | assert.response(r).has.jsonbody() 803 | assert.request(r).has.no.formparam("originalparam") 804 | local value = assert.request(r).has.formparam("renamedparam") 805 | assert.equals("yes", value) 806 | local value2 = assert.request(r).has.formparam("nottorename") 807 | assert.equals("yes", value2) 808 | end) 809 | it("args on GET if it exists", function() 810 | local r = assert( client:send { 811 | method = "GET", 812 | path = "/request", 813 | query = { 814 | originalparam = "true", 815 | nottorename = "true", 816 | }, 817 | headers = { 818 | ["Content-Type"] = "application/x-www-form-urlencoded", 819 | host = "test9.test" 820 | } 821 | }) 822 | assert.response(r).has.status(200) 823 | assert.response(r).has.jsonbody() 824 | assert.request(r).has.no.queryparam("originalparam") 825 | local value1 = assert.request(r).has.queryparam("renamedparam") 826 | assert.equals("true", value1) 827 | local value2 = assert.request(r).has.queryparam("nottorename") 828 | assert.equals("true", value2) 829 | end) 830 | end) 831 | 832 | describe("rename", function() 833 | it("specified header", function() 834 | local r = assert(client:send { 835 | method = "GET", 836 | path = "/request", 837 | headers = { 838 | host = "test9.test", 839 | ["x-to-rename"] = "true", 840 | ["x-another-header"] = "true" 841 | } 842 | }) 843 | assert.response(r).has.status(200) 844 | assert.response(r).has.jsonbody() 845 | assert.request(r).has.no.header("x-to-rename") 846 | assert.request(r).has.header("x-is-renamed") 847 | assert.request(r).has.header("x-another-header") 848 | end) 849 | it("does not add as new header if header does not exist", function() 850 | local r = assert( client:send { 851 | method = "GET", 852 | path = "/request", 853 | body = {}, 854 | headers = { 855 | host = "test9.test", 856 | ["x-a-header"] = "true", 857 | } 858 | }) 859 | assert.response(r).has.status(200) 860 | assert.response(r).has.jsonbody() 861 | assert.request(r).has.no.header("renamedparam") 862 | local h_a_header = assert.request(r).has.header("x-a-header") 863 | assert.equals("true", h_a_header) 864 | end) 865 | it("specified parameters in url encoded body on POST", function() 866 | local r = assert(client:send { 867 | method = "POST", 868 | path = "/request", 869 | body = { 870 | originalparam = "yes", 871 | }, 872 | headers = { 873 | ["Content-Type"] = "application/x-www-form-urlencoded", 874 | host = "test9.test" 875 | } 876 | }) 877 | assert.response(r).has.status(200) 878 | assert.response(r).has.jsonbody() 879 | assert.request(r).has.no.formparam("originalparam") 880 | local value = assert.request(r).has.formparam("renamedparam") 881 | assert.equals("yes", value) 882 | end) 883 | it("does not add as new parameter in url encoded body if parameter does not exist on POST", function() 884 | local r = assert(client:send { 885 | method = "POST", 886 | path = "/request", 887 | body = { 888 | ["x-a-header"] = "true", 889 | }, 890 | headers = { 891 | ["Content-Type"] = "application/x-www-form-urlencoded", 892 | host = "test9.test" 893 | } 894 | }) 895 | assert.response(r).has.status(200) 896 | assert.request(r).has.no.formparam("renamedparam") 897 | local value = assert.request(r).has.formparam("x-a-header") 898 | assert.equals("true", value) 899 | end) 900 | it("parameters from JSON body in POST", function() 901 | local r = assert(client:send { 902 | method = "POST", 903 | path = "/request", 904 | body = { 905 | ["originalparam"] = "yes", 906 | ["nottorename"] = "yes" 907 | }, 908 | headers = { 909 | host = "test9.test", 910 | ["content-type"] = "application/json" 911 | } 912 | }) 913 | assert.response(r).has.status(200) 914 | assert.response(r).has.jsonbody() 915 | local json = assert.request(r).has.jsonbody() 916 | assert.is_nil(json.params["originalparam"]) 917 | assert.is_not_nil(json.params["renamedparam"]) 918 | assert.equals("yes", json.params["renamedparam"]) 919 | assert.equals("yes", json.params["nottorename"]) 920 | end) 921 | it("does not fail if JSON body is malformed in POST", function() 922 | local r = assert(client:send { 923 | method = "POST", 924 | path = "/request", 925 | body = "malformed json body", 926 | headers = { 927 | host = "test9.test", 928 | ["content-type"] = "application/json" 929 | } 930 | }) 931 | assert.response(r).has.status(200) 932 | local json = assert.response(r).has.jsonbody() 933 | assert.equals("json (error)", json.post_data.kind) 934 | assert.is_not_nil(json.post_data.error) 935 | end) 936 | it("parameters on multipart POST", function() 937 | local r = assert(client:send { 938 | method = "POST", 939 | path = "/request", 940 | body = { 941 | ["originalparam"] = "yes", 942 | ["nottorename"] = "yes" 943 | }, 944 | headers = { 945 | ["Content-Type"] = "multipart/form-data", 946 | host = "test9.test" 947 | } 948 | }) 949 | assert.response(r).has.status(200) 950 | assert.response(r).has.jsonbody() 951 | assert.request(r).has.no.formparam("originalparam") 952 | local value = assert.request(r).has.formparam("renamedparam") 953 | assert.equals("yes", value) 954 | local value2 = assert.request(r).has.formparam("nottorename") 955 | assert.equals("yes", value2) 956 | end) 957 | it("args on GET if it exists", function() 958 | local r = assert( client:send { 959 | method = "GET", 960 | path = "/request", 961 | query = { 962 | originalparam = "true", 963 | nottorename = "true", 964 | }, 965 | headers = { 966 | ["Content-Type"] = "application/x-www-form-urlencoded", 967 | host = "test9.test" 968 | } 969 | }) 970 | assert.response(r).has.status(200) 971 | assert.response(r).has.jsonbody() 972 | assert.request(r).has.no.queryparam("originalparam") 973 | local value1 = assert.request(r).has.queryparam("renamedparam") 974 | assert.equals("true", value1) 975 | local value2 = assert.request(r).has.queryparam("nottorename") 976 | assert.equals("true", value2) 977 | end) 978 | end) 979 | 980 | describe("replace", function() 981 | it("specified header if it exist", function() 982 | local r = assert( client:send { 983 | method = "GET", 984 | path = "/request", 985 | body = {}, 986 | headers = { 987 | host = "test5.test", 988 | h1 = "V", 989 | h2 = "v2", 990 | } 991 | }) 992 | assert.response(r).has.status(200) 993 | assert.response(r).has.jsonbody() 994 | local h_h1 = assert.request(r).has.header("h1") 995 | assert.equals("v1", h_h1) 996 | local h_h2 = assert.request(r).has.header("h2") 997 | assert.equals("v2", h_h2) 998 | end) 999 | it("does not add as new header if header does not exist", function() 1000 | local r = assert( client:send { 1001 | method = "GET", 1002 | path = "/request", 1003 | body = {}, 1004 | headers = { 1005 | host = "test5.test", 1006 | h2 = "v2", 1007 | } 1008 | }) 1009 | assert.response(r).has.status(200) 1010 | assert.response(r).has.jsonbody() 1011 | assert.request(r).has.no.header("h1") 1012 | local h_h2 = assert.request(r).has.header("h2") 1013 | assert.equals("v2", h_h2) 1014 | end) 1015 | it("specified parameters in url encoded body on POST", function() 1016 | local r = assert(client:send { 1017 | method = "POST", 1018 | path = "/request", 1019 | body = { 1020 | p1 = "v", 1021 | p2 = "v1", 1022 | }, 1023 | headers = { 1024 | ["Content-Type"] = "application/x-www-form-urlencoded", 1025 | host = "test5.test" 1026 | } 1027 | }) 1028 | assert.response(r).has.status(200) 1029 | local value = assert.request(r).has.formparam("p1") 1030 | assert.equals("v1", value) 1031 | local value = assert.request(r).has.formparam("p2") 1032 | assert.equals("v1", value) 1033 | end) 1034 | it("does not add as new parameter in url encoded body if parameter does not exist on POST", function() 1035 | local r = assert(client:send { 1036 | method = "POST", 1037 | path = "/request", 1038 | body = { 1039 | p2 = "v1", 1040 | }, 1041 | headers = { 1042 | ["Content-Type"] = "application/x-www-form-urlencoded", 1043 | host = "test5.test" 1044 | } 1045 | }) 1046 | assert.response(r).has.status(200) 1047 | assert.request(r).has.no.formparam("p1") 1048 | local value = assert.request(r).has.formparam("p2") 1049 | assert.equals("v1", value) 1050 | end) 1051 | it("specified parameters in json body on POST", function() 1052 | local r = assert(client:send { 1053 | method = "POST", 1054 | path = "/request", 1055 | body = { 1056 | p1 = "v", 1057 | p2 = "v1" 1058 | }, 1059 | headers = { 1060 | host = "test5.test", 1061 | ["content-type"] = "application/json" 1062 | } 1063 | }) 1064 | assert.response(r).has.status(200) 1065 | local json = assert.request(r).has.jsonbody() 1066 | assert.equals("v1", json.params.p1) 1067 | assert.equals("v1", json.params.p2) 1068 | end) 1069 | it("does not fail if JSON body is malformed in POST", function() 1070 | local r = assert(client:send { 1071 | method = "POST", 1072 | path = "/request", 1073 | body = "malformed json body", 1074 | headers = { 1075 | host = "test5.test", 1076 | ["content-type"] = "application/json" 1077 | } 1078 | }) 1079 | assert.response(r).has.status(200) 1080 | local json = assert.response(r).has.jsonbody() 1081 | assert.equal("json (error)", json.post_data.kind) 1082 | assert.is_not_nil(json.post_data.error) 1083 | end) 1084 | it("does not add as new parameter in json if parameter does not exist on POST", function() 1085 | local r = assert(client:send { 1086 | method = "POST", 1087 | path = "/request", 1088 | body = { 1089 | p2 = "v1", 1090 | }, 1091 | headers = { 1092 | host = "test5.test", 1093 | ["content-type"] = "application/json" 1094 | } 1095 | }) 1096 | assert.response(r).has.status(200) 1097 | local json = assert.request(r).has.jsonbody() 1098 | assert.is_nil(json.params.p1) 1099 | assert.equals("v1", json.params.p2) 1100 | end) 1101 | it("specified parameters on multipart POST", function() 1102 | local r = assert(client:send { 1103 | method = "POST", 1104 | path = "/request", 1105 | body = { 1106 | p1 = "v", 1107 | p2 = "v1", 1108 | }, 1109 | headers = { 1110 | ["Content-Type"] = "multipart/form-data", 1111 | host = "test5.test" 1112 | } 1113 | }) 1114 | assert.response(r).has.status(200) 1115 | assert.response(r).has.jsonbody() 1116 | local value = assert.request(r).has.formparam("p1") 1117 | assert.equals("v1", value) 1118 | local value2 = assert.request(r).has.formparam("p2") 1119 | assert.equals("v1", value2) 1120 | end) 1121 | it("does not add as new parameter if parameter does not exist on multipart POST", function() 1122 | local r = assert(client:send { 1123 | method = "POST", 1124 | path = "/request", 1125 | body = { 1126 | p2 = "v1", 1127 | }, 1128 | headers = { 1129 | ["Content-Type"] = "multipart/form-data", 1130 | host = "test5.test" 1131 | } 1132 | }) 1133 | assert.response(r).has.status(200) 1134 | assert.response(r).has.jsonbody() 1135 | 1136 | assert.request(r).has.no.formparam("p1") 1137 | 1138 | local value = assert.request(r).has.formparam("p2") 1139 | assert.equals("v1", value) 1140 | end) 1141 | it("args on POST if it exist", function() 1142 | local r = assert(client:send { 1143 | method = "POST", 1144 | path = "/request", 1145 | query = { 1146 | q1 = "v", 1147 | q2 = "v2", 1148 | }, 1149 | body = { 1150 | hello = "world" 1151 | }, 1152 | headers = { 1153 | ["Content-Type"] = "application/x-www-form-urlencoded", 1154 | host = "test5.test" 1155 | } 1156 | }) 1157 | assert.response(r).has.status(200) 1158 | local value = assert.request(r).has.queryparam("q1") 1159 | assert.equals("v1", value) 1160 | local value = assert.request(r).has.queryparam("q2") 1161 | assert.equals("v2", value) 1162 | end) 1163 | it("does not add new args on POST if it does not exist", function() 1164 | local r = assert(client:send { 1165 | method = "POST", 1166 | path = "/request", 1167 | query = { 1168 | q2 = "v2", 1169 | }, 1170 | body = { 1171 | hello = "world" 1172 | }, 1173 | headers = { 1174 | ["Content-Type"] = "application/x-www-form-urlencoded", 1175 | host = "test5.test" 1176 | } 1177 | }) 1178 | assert.response(r).has.status(200) 1179 | assert.request(r).has.no.queryparam("q1") 1180 | local value = assert.request(r).has.queryparam("q2") 1181 | assert.equals("v2", value) 1182 | end) 1183 | 1184 | pending("escape UTF-8 characters when replacing upstream path - enable after Kong 2.4", function() 1185 | local r = assert(client:send { 1186 | method = "GET", 1187 | path = "/requests/wrong_path", 1188 | headers = { 1189 | host = "test27.test" 1190 | } 1191 | }) 1192 | assert.response(r).has.status(200) 1193 | local body = assert(assert.response(r).has.jsonbody()) 1194 | assert.equals(helpers.mock_upstream_url .. 1195 | "/requests/t%C3%A9st", body.url) 1196 | end) 1197 | end) 1198 | 1199 | describe("add", function() 1200 | it("new headers", function() 1201 | local r = assert(client:send { 1202 | method = "GET", 1203 | path = "/request", 1204 | headers = { 1205 | host = "test1.test" 1206 | } 1207 | }) 1208 | assert.response(r).has.status(200) 1209 | assert.response(r).has.jsonbody() 1210 | local h_h1 = assert.request(r).has.header("h1") 1211 | assert.equals("v1", h_h1) 1212 | local h_h2 = assert.request(r).has.header("h2") 1213 | assert.equals("value:2", h_h2) 1214 | end) 1215 | it("does not change or append value if header already exists", function() 1216 | local r = assert(client:send { 1217 | method = "GET", 1218 | path = "/request", 1219 | headers = { 1220 | h1 = "v3", 1221 | host = "test1.test", 1222 | } 1223 | }) 1224 | assert.response(r).has.status(200) 1225 | assert.response(r).has.jsonbody() 1226 | local h_h1 = assert.request(r).has.header("h1") 1227 | assert.equals("v3", h_h1) 1228 | local h_h2 = assert.request(r).has.header("h2") 1229 | assert.equals("value:2", h_h2) 1230 | end) 1231 | it("new parameter in url encoded body on POST", function() 1232 | local r = assert(client:send { 1233 | method = "POST", 1234 | path = "/request", 1235 | body = { 1236 | hello = "world", 1237 | }, 1238 | headers = { 1239 | ["Content-Type"] = "application/x-www-form-urlencoded", 1240 | host = "test1.test" 1241 | } 1242 | }) 1243 | assert.response(r).has.status(200) 1244 | local value = assert.request(r).has.formparam("hello") 1245 | assert.equals("world", value) 1246 | local value = assert.request(r).has.formparam("p1") 1247 | assert.equals("v1", value) 1248 | end) 1249 | it("does not change or append value to parameter in url encoded body on POST when parameter exists", function() 1250 | local r = assert(client:send { 1251 | method = "POST", 1252 | path = "/request", 1253 | body = { 1254 | p1 = "should not change", 1255 | hello = "world", 1256 | }, 1257 | headers = { 1258 | ["Content-Type"] = "application/x-www-form-urlencoded", 1259 | host = "test1.test" 1260 | } 1261 | }) 1262 | assert.response(r).has.status(200) 1263 | local value = assert.request(r).has.formparam("p1") 1264 | assert.equals("should not change", value) 1265 | local value = assert.request(r).has.formparam("hello") 1266 | assert.equals("world", value) 1267 | end) 1268 | it("new parameter in JSON body on POST", function() 1269 | local r = assert(client:send { 1270 | method = "POST", 1271 | path = "/request", 1272 | body = { 1273 | hello = "world", 1274 | }, 1275 | headers = { 1276 | ["Content-Type"] = "application/json", 1277 | host = "test1.test" 1278 | } 1279 | }) 1280 | assert.response(r).has.status(200) 1281 | local params = assert.request(r).has.jsonbody().params 1282 | assert.equals("world", params.hello) 1283 | assert.equals("v1", params.p1) 1284 | end) 1285 | it("does not change or append value to parameter in JSON on POST when parameter exists", function() 1286 | local r = assert(client:send { 1287 | method = "POST", 1288 | path = "/request", 1289 | body = { 1290 | p1 = "this should not change", 1291 | hello = "world", 1292 | }, 1293 | headers = { 1294 | ["Content-Type"] = "application/json", 1295 | host = "test1.test" 1296 | } 1297 | }) 1298 | assert.response(r).has.status(200) 1299 | local params = assert.request(r).has.jsonbody().params 1300 | assert.equals("world", params.hello) 1301 | assert.equals("this should not change", params.p1) 1302 | end) 1303 | it("does not fail if JSON body is malformed in POST", function() 1304 | local r = assert(client:send { 1305 | method = "POST", 1306 | path = "/request", 1307 | body = "malformed json body", 1308 | headers = { 1309 | host = "test1.test", 1310 | ["content-type"] = "application/json" 1311 | } 1312 | }) 1313 | assert.response(r).has.status(200) 1314 | local json = assert.response(r).has.jsonbody() 1315 | assert.equal("json (error)", json.post_data.kind) 1316 | assert.is_not_nil(json.post_data.error) 1317 | end) 1318 | it("new parameter on multipart POST", function() 1319 | local r = assert(client:send { 1320 | method = "POST", 1321 | path = "/request", 1322 | body = {}, 1323 | headers = { 1324 | ["Content-Type"] = "multipart/form-data", 1325 | host = "test1.test" 1326 | } 1327 | }) 1328 | assert.response(r).has.status(200) 1329 | assert.response(r).has.jsonbody() 1330 | local value = assert.request(r).has.formparam("p1") 1331 | assert.equals("v1", value) 1332 | end) 1333 | it("does not change or append value to parameter on multipart POST when parameter exists", function() 1334 | local r = assert(client:send { 1335 | method = "POST", 1336 | path = "/request", 1337 | body = { 1338 | p1 = "this should not change", 1339 | hello = "world", 1340 | }, 1341 | headers = { 1342 | ["Content-Type"] = "multipart/form-data", 1343 | host = "test1.test" 1344 | }, 1345 | }) 1346 | assert.response(r).has.status(200) 1347 | assert.response(r).has.jsonbody() 1348 | local value = assert.request(r).has.formparam("p1") 1349 | assert.equals("this should not change", value) 1350 | 1351 | local value2 = assert.request(r).has.formparam("hello") 1352 | assert.equals("world", value2) 1353 | end) 1354 | it("new querystring on GET", function() 1355 | local r = assert( client:send { 1356 | method = "GET", 1357 | path = "/request", 1358 | query = { 1359 | q2 = "v2", 1360 | }, 1361 | headers = { 1362 | ["Content-Type"] = "application/x-www-form-urlencoded", 1363 | host = "test1.test" 1364 | } 1365 | }) 1366 | assert.response(r).has.status(200) 1367 | local value = assert.request(r).has.queryparam("q2") 1368 | assert.equals("v2", value) 1369 | local value = assert.request(r).has.queryparam("q1") 1370 | assert.equals("v1", value) 1371 | end) 1372 | it("does not change or append value to querystring on GET if querystring exists", function() 1373 | local r = assert(client:send { 1374 | method = "GET", 1375 | path = "/request", 1376 | query = { 1377 | q1 = "v2", 1378 | }, 1379 | headers = { 1380 | ["Content-Type"] = "application/x-www-form-urlencoded", 1381 | host = "test1.test" 1382 | } 1383 | }) 1384 | assert.response(r).has.status(200) 1385 | local value = assert.request(r).has.queryparam("q1") 1386 | assert.equals("v2", value) 1387 | end) 1388 | it("should not change the host header", function() 1389 | local r = assert(client:send { 1390 | method = "GET", 1391 | path = "/get", 1392 | headers = { 1393 | ["Content-Type"] = "application/json", 1394 | host = "test2.test" 1395 | } 1396 | }) 1397 | assert.response(r).has.status(200) 1398 | local json = assert.response(r).has.jsonbody() 1399 | local value = assert.has.header("host", json) 1400 | assert.equals("test2.test", value) 1401 | end) 1402 | end) 1403 | 1404 | describe("append ", function() 1405 | it("new header if header does not exists", function() 1406 | local r = assert(client:send { 1407 | method = "GET", 1408 | path = "/request", 1409 | headers = { 1410 | host = "test6.test" 1411 | } 1412 | }) 1413 | assert.response(r).has.status(200) 1414 | assert.response(r).has.jsonbody() 1415 | local h_h2 = assert.request(r).has.header("h2") 1416 | assert.equals("v1", h_h2) 1417 | end) 1418 | it("values to existing headers", function() 1419 | local r = assert( client:send { 1420 | method = "GET", 1421 | path = "/request", 1422 | headers = { 1423 | host = "test6.test" 1424 | } 1425 | }) 1426 | assert.response(r).has.status(200) 1427 | assert.response(r).has.jsonbody() 1428 | local h_h1 = assert.request(r).has.header("h1") 1429 | assert.same({"v1", "v2"}, h_h1) 1430 | end) 1431 | 1432 | it("can append a value with '#' (regression test for #29)", function() 1433 | local route = admin_api.routes:insert({ 1434 | hosts = { "test_append_hash.test" } 1435 | }) 1436 | admin_api.plugins:insert { 1437 | route = { id = route.id }, 1438 | name = "request-transformer", 1439 | config = { 1440 | append = { 1441 | headers = {"h1:v1", "h1:v2", "h1:#value_with_hash", "h2:v1",}, 1442 | querystring = {"q1:v1", "q1:v2", "q2:v1"}, 1443 | body = {"p1:v1", "p1:v2", "p2:value:1"} -- payload containing a colon 1444 | } 1445 | } 1446 | } 1447 | local r = assert( client:send { 1448 | method = "GET", 1449 | path = "/request", 1450 | headers = { 1451 | host = "test_append_hash.test" 1452 | } 1453 | }) 1454 | assert.response(r).has.status(200) 1455 | assert.response(r).has.jsonbody() 1456 | local h_h1 = assert.request(r).has.header("h1") 1457 | assert.same({"v1", "v2", "#value_with_hash"}, h_h1) 1458 | end) 1459 | 1460 | it("new querystring if querystring does not exists", function() 1461 | local r = assert(client:send { 1462 | method = "POST", 1463 | path = "/request", 1464 | body = { 1465 | hello = "world", 1466 | }, 1467 | headers = { 1468 | ["Content-Type"] = "application/x-www-form-urlencoded", 1469 | host = "test6.test" 1470 | } 1471 | }) 1472 | assert.response(r).has.status(200) 1473 | local value = assert.request(r).has.queryparam("q2") 1474 | assert.equals("v1", value) 1475 | end) 1476 | it("values to existing querystring", function() 1477 | local r = assert(client:send { 1478 | method = "POST", 1479 | path = "/request", 1480 | headers = { 1481 | ["Content-Type"] = "application/x-www-form-urlencoded", 1482 | host = "test6.test" 1483 | } 1484 | }) 1485 | assert.response(r).has.status(200) 1486 | local value = assert.request(r).has.queryparam("q1") 1487 | assert.are.same({"v1", "v2"}, value) 1488 | end) 1489 | it("new parameter in url encoded body on POST if it does not exist", function() 1490 | local r = assert( client:send { 1491 | method = "POST", 1492 | path = "/request", 1493 | headers = { 1494 | ["Content-Type"] = "application/x-www-form-urlencoded", 1495 | host = "test6.test" 1496 | } 1497 | }) 1498 | assert.response(r).has.status(200) 1499 | assert.response(r).has.jsonbody() 1500 | local value = assert.request(r).has.formparam("p1") 1501 | assert.same({"v1", "v2"}, value) 1502 | 1503 | local value2 = assert.request(r).has.formparam("p2") 1504 | assert.same("value:1", value2) 1505 | end) 1506 | it("values to existing parameter in url encoded body if parameter already exist on POST", function() 1507 | local r = assert(client:send { 1508 | method = "POST", 1509 | path = "/request", 1510 | body = { 1511 | p1 = "v0", 1512 | }, 1513 | headers = { 1514 | ["Content-Type"] = "application/x-www-form-urlencoded", 1515 | host = "test6.test" 1516 | } 1517 | }) 1518 | assert.response(r).has.status(200) 1519 | assert.response(r).has.jsonbody() 1520 | local value = assert.request(r).has.formparam("p1") 1521 | assert.same({"v0", "v1", "v2"}, value) 1522 | 1523 | local value2 = assert.request(r).has.formparam("p2") 1524 | assert.are.same("value:1", value2) 1525 | end) 1526 | it("does not fail if JSON body is malformed in POST", function() 1527 | local r = assert(client:send { 1528 | method = "POST", 1529 | path = "/request", 1530 | body = "malformed json body", 1531 | headers = { 1532 | host = "test6.test", 1533 | ["content-type"] = "application/json" 1534 | } 1535 | }) 1536 | assert.response(r).has.status(200) 1537 | local json = assert.response(r).has.jsonbody() 1538 | assert.equal("json (error)", json.post_data.kind) 1539 | assert.is_not_nil(json.post_data.error) 1540 | end) 1541 | it("does not change or append value to parameter on multipart POST", function() 1542 | local r = assert(client:send { 1543 | method = "POST", 1544 | path = "/request", 1545 | body = { 1546 | p1 = "This should not change", 1547 | }, 1548 | headers = { 1549 | ["Content-Type"] = "multipart/form-data", 1550 | host = "test6.test" 1551 | } 1552 | }) 1553 | assert.response(r).has.status(200) 1554 | assert.response(r).has.jsonbody() 1555 | local value = assert.request(r).has.formparam("p1") 1556 | assert.equals("This should not change", value) 1557 | end) 1558 | end) 1559 | 1560 | describe("remove, replace, add and append ", function() 1561 | it("removes a header", function() 1562 | local r = assert(client:send { 1563 | method = "GET", 1564 | path = "/request", 1565 | headers = { 1566 | host = "test3.test", 1567 | ["x-to-remove"] = "true", 1568 | } 1569 | }) 1570 | assert.response(r).has.status(200) 1571 | assert.response(r).has.jsonbody() 1572 | assert.request(r).has.no.header("x-to-remove") 1573 | end) 1574 | it("replaces value of header, if header exist", function() 1575 | local r = assert( client:send { 1576 | method = "GET", 1577 | path = "/request", 1578 | headers = { 1579 | host = "test3.test", 1580 | ["x-to-replace"] = "true", 1581 | } 1582 | }) 1583 | assert.response(r).has.status(200) 1584 | assert.response(r).has.jsonbody() 1585 | local hval = assert.request(r).has.header("x-to-replace") 1586 | assert.equals("false", hval) 1587 | end) 1588 | it("does not add new header if to be replaced header does not exist", function() 1589 | local r = assert(client:send { 1590 | method = "GET", 1591 | path = "/request", 1592 | headers = { 1593 | host = "test3.test", 1594 | } 1595 | }) 1596 | assert.response(r).has.status(200) 1597 | assert.response(r).has.jsonbody() 1598 | assert.request(r).has.no.header("x-to-replace") 1599 | end) 1600 | it("add new header if missing", function() 1601 | local r = assert(client:send { 1602 | method = "GET", 1603 | path = "/request", 1604 | headers = { 1605 | host = "test3.test", 1606 | } 1607 | }) 1608 | assert.response(r).has.status(200) 1609 | assert.response(r).has.jsonbody() 1610 | local hval = assert.request(r).has.header("x-added2") 1611 | assert.equals("b1", hval) 1612 | end) 1613 | it("does not add new header if it already exist", function() 1614 | local r = assert(client:send { 1615 | method = "GET", 1616 | path = "/request", 1617 | headers = { 1618 | host = "test3.test", 1619 | ["x-added3"] = "c1", 1620 | } 1621 | }) 1622 | assert.response(r).has.status(200) 1623 | assert.response(r).has.jsonbody() 1624 | local hval = assert.request(r).has.header("x-added3") 1625 | assert.equals("c1", hval) 1626 | end) 1627 | it("appends values to existing headers", function() 1628 | local r = assert(client:send { 1629 | method = "GET", 1630 | path = "/request", 1631 | headers = { 1632 | host = "test3.test", 1633 | } 1634 | }) 1635 | assert.response(r).has.status(200) 1636 | assert.response(r).has.jsonbody() 1637 | local hval = assert.request(r).has.header("x-added") 1638 | assert.same({"a1", "a2", "a3"}, hval) 1639 | end) 1640 | it("adds new parameters on POST when query string key missing", function() 1641 | local r = assert(client:send { 1642 | method = "POST", 1643 | path = "/request", 1644 | body = { 1645 | hello = "world", 1646 | }, 1647 | headers = { 1648 | host = "test3.test", 1649 | ["Content-Type"] = "application/x-www-form-urlencoded", 1650 | } 1651 | }) 1652 | assert.response(r).has.status(200) 1653 | local value = assert.request(r).has.queryparam("p2") 1654 | assert.equals("b1", value) 1655 | end) 1656 | it("removes parameters on GET", function() 1657 | local r = assert( client:send { 1658 | method = "GET", 1659 | path = "/request", 1660 | query = { 1661 | toremovequery = "yes", 1662 | nottoremove = "yes", 1663 | }, 1664 | body = { 1665 | hello = "world", 1666 | }, 1667 | headers = { 1668 | host = "test3.test", 1669 | ["Content-Type"] = "application/x-www-form-urlencoded", 1670 | } 1671 | }) 1672 | assert.response(r).has.status(200) 1673 | assert.response(r).has.jsonbody() 1674 | assert.request(r).has.no.queryparam("toremovequery") 1675 | local value = assert.request(r).has.queryparam("nottoremove") 1676 | assert.equals("yes", value) 1677 | end) 1678 | it("replaces parameters on GET", function() 1679 | local r = assert(client:send { 1680 | method = "GET", 1681 | path = "/request", 1682 | query = { 1683 | toreplacequery = "yes", 1684 | }, 1685 | body = { 1686 | hello = "world", 1687 | }, 1688 | headers = { 1689 | host = "test3.test", 1690 | ["Content-Type"] = "application/x-www-form-urlencoded", 1691 | } 1692 | }) 1693 | assert.response(r).has.status(200) 1694 | local value = assert.request(r).has.queryparam("toreplacequery") 1695 | assert.equals("no", value) 1696 | end) 1697 | it("does not add new parameter if to be replaced parameters does not exist on GET", function() 1698 | local r = assert( client:send { 1699 | method = "GET", 1700 | path = "/request", 1701 | headers = { 1702 | host = "test3.test", 1703 | ["Content-Type"] = "application/x-www-form-urlencoded", 1704 | } 1705 | }) 1706 | assert.response(r).has.status(200) 1707 | assert.request(r).has.no.formparam("toreplacequery") 1708 | end) 1709 | it("adds parameters on GET if it does not exist", function() 1710 | local r = assert(client:send { 1711 | method = "GET", 1712 | path = "/request", 1713 | headers = { 1714 | host = "test3.test", 1715 | ["Content-Type"] = "application/x-www-form-urlencoded", 1716 | } 1717 | }) 1718 | assert.response(r).has.status(200) 1719 | local value = assert.request(r).has.queryparam("query-added") 1720 | assert.equals("newvalue", value) 1721 | end) 1722 | it("does not add new parameter if to be added parameters already exist on GET", function() 1723 | local r = assert(client:send { 1724 | method = "GET", 1725 | path = "/request", 1726 | query = { 1727 | ["query-added"] = "oldvalue", 1728 | }, 1729 | headers = { 1730 | host = "test3.test", 1731 | ["Content-Type"] = "application/x-www-form-urlencoded", 1732 | } 1733 | }) 1734 | assert.response(r).has.status(200) 1735 | local value = assert.request(r).has.queryparam("query-added") 1736 | assert.equals("oldvalue", value) 1737 | end) 1738 | it("appends parameters on GET", function() 1739 | local r = assert(client:send { 1740 | method = "GET", 1741 | path = "/request", 1742 | query = { 1743 | q1 = "20", 1744 | }, 1745 | body = { 1746 | hello = "world", 1747 | }, 1748 | headers = { 1749 | host = "test3.test", 1750 | ["Content-Type"] = "application/x-www-form-urlencoded", 1751 | } 1752 | }) 1753 | assert.response(r).has.status(200) 1754 | local value = assert.request(r).has.queryparam("p1") 1755 | assert.equals("anything:1", value[1]) 1756 | assert.equals("a2", value[2]) 1757 | local value = assert.request(r).has.queryparam("q1") 1758 | assert.equals("20", value) 1759 | end) 1760 | 1761 | it("removes a header -- ignore case", function() 1762 | local r = assert(client:send { 1763 | method = "GET", 1764 | path = "/request", 1765 | headers = { 1766 | host = "test26.test", 1767 | ["x-to-remove"] = "true", 1768 | } 1769 | }) 1770 | assert.response(r).has.status(200) 1771 | assert.response(r).has.jsonbody() 1772 | assert.request(r).has.no.header("X-To-Remove") 1773 | end) 1774 | it("replaces value of header, if header exist -- don't change header case", function() 1775 | local r = assert( client:send { 1776 | method = "GET", 1777 | path = "/request", 1778 | headers = { 1779 | host = "test26.test", 1780 | ["x-to-replace"] = "true", 1781 | } 1782 | }) 1783 | assert.response(r).has.status(200) 1784 | assert.response(r).has.jsonbody() 1785 | local hval = assert.request(r).has.header("x-to-Replace") 1786 | assert.equals("false", hval) 1787 | end) 1788 | it("add new header if missing -- keep configured case", function() 1789 | local r = assert(client:send { 1790 | method = "GET", 1791 | path = "/request", 1792 | headers = { 1793 | host = "test26.test", 1794 | } 1795 | }) 1796 | assert.response(r).has.status(200) 1797 | assert.response(r).has.jsonbody() 1798 | local hval = assert.request(r).has.header("x-aDDed2") 1799 | assert.equals("b1", hval) 1800 | end) 1801 | it("does not add new header if it already exist -- keep request case", function() 1802 | local r = assert(client:send { 1803 | method = "GET", 1804 | path = "/request", 1805 | headers = { 1806 | host = "test26.test", 1807 | ["x-added3"] = "c1", 1808 | } 1809 | }) 1810 | assert.response(r).has.status(200) 1811 | assert.response(r).has.jsonbody() 1812 | local hval = assert.request(r).has.header("x-added3") 1813 | assert.equals("c1", hval) 1814 | end) 1815 | it("appends values to existing headers -- keep config case", function() 1816 | local r = assert(client:send { 1817 | method = "GET", 1818 | path = "/request", 1819 | headers = { 1820 | host = "test26.test", 1821 | ["X-addeD"] = "a0", 1822 | } 1823 | }) 1824 | assert.response(r).has.status(200) 1825 | assert.response(r).has.jsonbody() 1826 | local hval = assert.request(r).has.header("X-aDDed") 1827 | assert.same({"a0", "a2", "a3"}, hval) 1828 | end) 1829 | it("appends values to added headers -- keep 'add' config case", function() 1830 | local r = assert(client:send { 1831 | method = "GET", 1832 | path = "/request", 1833 | headers = { 1834 | host = "test26.test", 1835 | } 1836 | }) 1837 | assert.response(r).has.status(200) 1838 | assert.response(r).has.jsonbody() 1839 | local hval = assert.request(r).has.header("X-Added") 1840 | assert.same({"a1", "a2", "a3"}, hval) 1841 | end) 1842 | it("adds new parameters on POST when query string key missing", function() 1843 | local r = assert(client:send { 1844 | method = "POST", 1845 | path = "/request", 1846 | body = { 1847 | hello = "world", 1848 | }, 1849 | headers = { 1850 | host = "test3.test", 1851 | ["Content-Type"] = "application/x-www-form-urlencoded", 1852 | } 1853 | }) 1854 | assert.response(r).has.status(200) 1855 | local value = assert.request(r).has.queryparam("p2") 1856 | assert.equals("b1", value) 1857 | end) 1858 | it("removes parameters on GET", function() 1859 | local r = assert( client:send { 1860 | method = "GET", 1861 | path = "/request", 1862 | query = { 1863 | toremovequery = "yes", 1864 | nottoremove = "yes", 1865 | }, 1866 | body = { 1867 | hello = "world", 1868 | }, 1869 | headers = { 1870 | host = "test3.test", 1871 | ["Content-Type"] = "application/x-www-form-urlencoded", 1872 | } 1873 | }) 1874 | assert.response(r).has.status(200) 1875 | assert.response(r).has.jsonbody() 1876 | assert.request(r).has.no.queryparam("toremovequery") 1877 | local value = assert.request(r).has.queryparam("nottoremove") 1878 | assert.equals("yes", value) 1879 | end) 1880 | it("replaces parameters on GET", function() 1881 | local r = assert(client:send { 1882 | method = "GET", 1883 | path = "/request", 1884 | query = { 1885 | toreplacequery = "yes", 1886 | }, 1887 | body = { 1888 | hello = "world", 1889 | }, 1890 | headers = { 1891 | host = "test3.test", 1892 | ["Content-Type"] = "application/x-www-form-urlencoded", 1893 | } 1894 | }) 1895 | assert.response(r).has.status(200) 1896 | local value = assert.request(r).has.queryparam("toreplacequery") 1897 | assert.equals("no", value) 1898 | end) 1899 | it("does not add new parameter if to be replaced parameters does not exist on GET", function() 1900 | local r = assert( client:send { 1901 | method = "GET", 1902 | path = "/request", 1903 | headers = { 1904 | host = "test3.test", 1905 | ["Content-Type"] = "application/x-www-form-urlencoded", 1906 | } 1907 | }) 1908 | assert.response(r).has.status(200) 1909 | assert.request(r).has.no.formparam("toreplacequery") 1910 | end) 1911 | it("adds parameters on GET if it does not exist", function() 1912 | local r = assert(client:send { 1913 | method = "GET", 1914 | path = "/request", 1915 | headers = { 1916 | host = "test3.test", 1917 | ["Content-Type"] = "application/x-www-form-urlencoded", 1918 | } 1919 | }) 1920 | assert.response(r).has.status(200) 1921 | local value = assert.request(r).has.queryparam("query-added") 1922 | assert.equals("newvalue", value) 1923 | end) 1924 | it("does not add new parameter if to be added parameters already exist on GET", function() 1925 | local r = assert(client:send { 1926 | method = "GET", 1927 | path = "/request", 1928 | query = { 1929 | ["query-added"] = "oldvalue", 1930 | }, 1931 | headers = { 1932 | host = "test3.test", 1933 | ["Content-Type"] = "application/x-www-form-urlencoded", 1934 | } 1935 | }) 1936 | assert.response(r).has.status(200) 1937 | local value = assert.request(r).has.queryparam("query-added") 1938 | assert.equals("oldvalue", value) 1939 | end) 1940 | it("appends parameters on GET", function() 1941 | local r = assert(client:send { 1942 | method = "GET", 1943 | path = "/request", 1944 | query = { 1945 | q1 = "20", 1946 | }, 1947 | body = { 1948 | hello = "world", 1949 | }, 1950 | headers = { 1951 | host = "test3.test", 1952 | ["Content-Type"] = "application/x-www-form-urlencoded", 1953 | } 1954 | }) 1955 | assert.response(r).has.status(200) 1956 | local value = assert.request(r).has.queryparam("p1") 1957 | assert.equals("anything:1", value[1]) 1958 | assert.equals("a2", value[2]) 1959 | local value = assert.request(r).has.queryparam("q1") 1960 | assert.equals("20", value) 1961 | end) 1962 | end) 1963 | 1964 | 1965 | 1966 | describe("request rewrite using template", function() 1967 | it("template as querystring parameters on GET", function() 1968 | local r = assert(client:send { 1969 | method = "GET", 1970 | path = "/requests/user1/foo/user2/bar", 1971 | query = { 1972 | q1 = "20", 1973 | }, 1974 | body = { 1975 | hello = "world", 1976 | }, 1977 | headers = { 1978 | host = "test10.test", 1979 | ["Content-Type"] = "application/x-www-form-urlencoded", 1980 | } 1981 | }) 1982 | local body = assert.response(r).has.status(200) 1983 | local json_body = cjson.decode(body) 1984 | local value = assert.request(r).has.queryparam("uri_param1") 1985 | assert.equals("foo", value) 1986 | assert.equals("/requests/user1/foo/user2/bar?q1=20&uri_param1=foo&uri_param2%5Bsome_index%5D%5B1%5D=bar", 1987 | json_body.vars.request_uri) 1988 | end) 1989 | it("should update request path using hash", function() 1990 | local r = assert(client:send { 1991 | method = "GET", 1992 | path = "/requests/user1/foo/user2/bar", 1993 | headers = { 1994 | host = "test11.test", 1995 | } 1996 | }) 1997 | assert.response(r).has.status(200) 1998 | local body = assert(assert.response(r).has.jsonbody()) 1999 | assert.equals(helpers.mock_upstream_url .. 2000 | "/requests/user2/bar/user1/foo", body.url) 2001 | end) 2002 | it("should not add querystring if hash missing", function() 2003 | local r = assert(client:send { 2004 | method = "GET", 2005 | path = "/requests/", 2006 | query = { 2007 | q1 = "20", 2008 | }, 2009 | headers = { 2010 | host = "test12.test", 2011 | } 2012 | }) 2013 | assert.response(r).has.status(200) 2014 | assert.request(r).has.queryparam("q1") 2015 | local value = assert.request(r).has.queryparam("uri_param1") 2016 | assert.equals("default1", value) 2017 | value = assert.request(r).has.queryparam("uri_param2") 2018 | assert.equals("default2", value) 2019 | end) 2020 | it("should fail when uri template is not a proper expression", function() 2021 | local r = assert(client:send { 2022 | method = "GET", 2023 | path = "/requests/user1/foo/user2/bar", 2024 | headers = { 2025 | host = "test13.test", 2026 | } 2027 | }) 2028 | assert.response(r).has.status(500) 2029 | end) 2030 | it("should not fail when uri template rendered using index", function() 2031 | local r = assert(client:send { 2032 | method = "GET", 2033 | path = "/user1/foo/user2/bar", 2034 | headers = { 2035 | host = "test14.test", 2036 | } 2037 | }) 2038 | assert.response(r).has.status(200) 2039 | local body = assert(assert.response(r).has.jsonbody()) 2040 | assert.equals(helpers.mock_upstream_url .. 2041 | "/requests/user1/foo/user2/bar", body.url) 2042 | end) 2043 | it("validate using headers/req_querystring for rendering templates", 2044 | function() 2045 | local r = assert(client:send { 2046 | method = "GET", 2047 | path = "/requests/user1/foo/user2/bar", 2048 | query = { 2049 | q1 = "20", 2050 | }, 2051 | headers = { 2052 | host = "test15.test", 2053 | ["Content-Type"] = "application/x-www-form-urlencoded", 2054 | } 2055 | }) 2056 | assert.response(r).has.status(200) 2057 | assert.request(r).has.no.queryparam("q1") 2058 | local value = assert.request(r).has.queryparam("uri_param1") 2059 | assert.equals("foo", value) 2060 | value = assert.request(r).has.queryparam("uri_param2") 2061 | assert.equals("test15.test", value) 2062 | value = assert.request(r).has.header("x-test-header") 2063 | assert.equals("20", value) 2064 | end) 2065 | it("validate that removed header can be used as template", function() 2066 | local r = assert(client:send { 2067 | method = "GET", 2068 | path = "/requests/user1/foo/user2/bar", 2069 | query = { 2070 | q2 = "20", 2071 | }, 2072 | headers = { 2073 | host = "test16.test", 2074 | ["x-remove-header"] = "its a test", 2075 | ["Content-Type"] = "application/x-www-form-urlencoded", 2076 | } 2077 | }) 2078 | assert.response(r).has.status(200) 2079 | assert.request(r).has.no.header("x-remove-header") 2080 | local value = assert.request(r).has.queryparam("q1") 2081 | assert.equals("foo", value) 2082 | value = assert.request(r).has.queryparam("q2") 2083 | assert.equals("its a test", value) 2084 | value = assert.request(r).has.header("x-test-header") 2085 | assert.equals("its a test", value) 2086 | end) 2087 | it("validate template will be rendered with old value of replaced header", 2088 | function() 2089 | local r = assert(client:send { 2090 | method = "GET", 2091 | path = "/requests/user1/foo/user2/bar", 2092 | query = { 2093 | q2 = "20", 2094 | }, 2095 | headers = { 2096 | host = "test17.test", 2097 | ["x-replace-header"] = "the old value", 2098 | ["Content-Type"] = "application/x-www-form-urlencoded", 2099 | } 2100 | }) 2101 | assert.response(r).has.status(200) 2102 | local value = assert.request(r).has.queryparam("q1") 2103 | assert.equals("foo", value) 2104 | value = assert.request(r).has.queryparam("q2") 2105 | assert.equals("the old value", value) 2106 | value = assert.request(r).has.header("x-test-header") 2107 | assert.equals("the old value", value) 2108 | value = assert.request(r).has.header("x-replace-header") 2109 | assert.equals("the new value", value) 2110 | end) 2111 | it("validate template can be escaped", 2112 | function() 2113 | local r = assert(client:send { 2114 | method = "GET", 2115 | path = "/requests/user1/foo/user2/bar", 2116 | query = { 2117 | q2 = "20", 2118 | }, 2119 | headers = { 2120 | host = "test18.test", 2121 | ["x-replace-header"] = "the old value", 2122 | ["Content-Type"] = "application/x-www-form-urlencoded", 2123 | } 2124 | }) 2125 | assert.response(r).has.status(200) 2126 | local value = assert.request(r).has.queryparam("q1") 2127 | assert.equals([[$(uri_captures.user1)]], value) 2128 | value = assert.request(r).has.queryparam("q2") 2129 | assert.equals("20", value) 2130 | end) 2131 | it("rendering error (query) should fail when rendering errors out", function() 2132 | local r = assert(client:send { 2133 | method = "GET", 2134 | path = "/requests/user1/foo/user2/bar", 2135 | query = { 2136 | q2 = "20", 2137 | }, 2138 | headers = { 2139 | host = "test19.test", 2140 | ["x-replace-header"] = "the old value", 2141 | ["Content-Type"] = "application/x-www-form-urlencoded", 2142 | } 2143 | }) 2144 | assert.response(r).has.status(500) 2145 | end) 2146 | it("rendering error (header) is correctly propagated in error.log, issue #25", function() 2147 | local pattern = [[error:%[string "TMP"%]:4: attempt to call global 'foo' %(a nil value%)]] 2148 | local start_count = count_log_lines(pattern) 2149 | 2150 | local r = assert(client:send { 2151 | method = "GET", 2152 | path = "/", 2153 | headers = { 2154 | host = "test24.test", 2155 | } 2156 | }) 2157 | assert.response(r).has.status(500) 2158 | 2159 | helpers.wait_until(function() 2160 | local count = count_log_lines(pattern) 2161 | return count - start_count >= 1 -- Kong 2.2+ == 1, Pre 2.2 == 2 2162 | end, 5) 2163 | end) 2164 | it("type function is available in template environment", function() 2165 | local r = assert(client:send { 2166 | method = "GET", 2167 | path = "/request", 2168 | headers = { 2169 | host = "test25.test", 2170 | ["X-Foo"] = { "1", "2", }, 2171 | } 2172 | }) 2173 | assert.response(r).has.status(200) 2174 | assert.response(r).has.jsonbody() 2175 | local h_h1 = assert.request(r).has.header("X-Foo-Transformed") 2176 | assert.equals("1-first", h_h1) 2177 | end) 2178 | it("can inject a value from 'kong.ctx.shared'", function() 2179 | local r = assert(client:send { 2180 | method = "GET", 2181 | path = "/", 2182 | headers = { 2183 | host = "test21.test", 2184 | } 2185 | }) 2186 | assert.response(r).has.status(200) 2187 | local value = assert.request(r).has.queryparam("shared_param1") 2188 | assert.equals("1.2.3", value) 2189 | end) 2190 | it("cannot write a value in `kong.ctx.shared`", function() 2191 | local r = client:send { 2192 | method = "GET", 2193 | path = "/", 2194 | headers = { 2195 | host = "test28.test", 2196 | } 2197 | } 2198 | assert.response(r).has.status(500) 2199 | end) 2200 | end) 2201 | describe("remove then add header (regression test)", function() 2202 | it("header already exists in request", function() 2203 | local r = assert(client:send { 2204 | method = "GET", 2205 | path = "/", 2206 | headers = { 2207 | host = "test22.test", 2208 | ["Authorization"] = "Basic dGVzdDp0ZXN0", 2209 | ["Content-Type"] = "application/json", 2210 | } 2211 | }) 2212 | assert.response(r).has.status(200) 2213 | local value = assert.request(r).has.header("authorization") 2214 | assert.equals("Basic test", value) 2215 | end) 2216 | it("header does not exist in request", function() 2217 | local r = assert(client:send { 2218 | method = "GET", 2219 | path = "/", 2220 | headers = { 2221 | host = "test22.test", 2222 | ["Content-Type"] = "application/json", 2223 | } 2224 | }) 2225 | assert.response(r).has.status(200) 2226 | local value = assert.request(r).has.header("authorization") 2227 | assert.equals("Basic test", value) 2228 | end) 2229 | end) 2230 | describe("query parameters are not #urlencoded to upstream URL", function() 2231 | local expected = "$&+,/:;?@" 2232 | it("when plugin is not configured", function() 2233 | local r = assert(client:send { 2234 | method = "GET", 2235 | path = "/request?expected=" .. expected, -- Use inline to keep from getting URL encoded 2236 | headers = { 2237 | host = "test23.test" 2238 | } 2239 | }) 2240 | local res = assert.response(r).has.status(200) 2241 | local body = cjson.decode(res) 2242 | local expected_url = helpers.mock_upstream_url .. "/?expected=" .. expected 2243 | assert.equals(expected_url, body.url) 2244 | end) 2245 | end) 2246 | end) 2247 | end 2248 | -------------------------------------------------------------------------------- /spec/03-api_spec.lua: -------------------------------------------------------------------------------- 1 | local helpers = require "spec.helpers" 2 | local cjson = require "cjson" 3 | 4 | for _, strategy in helpers.each_strategy() do 5 | describe("Plugin: request-transformer (API) [#" .. strategy .. "]", function() 6 | local admin_client 7 | 8 | lazy_teardown(function() 9 | if admin_client then 10 | admin_client:close() 11 | end 12 | 13 | helpers.stop_kong() 14 | end) 15 | 16 | describe("POST", function() 17 | lazy_setup(function() 18 | helpers.get_db_utils(strategy, { 19 | "plugins", 20 | }) 21 | 22 | assert(helpers.start_kong({ 23 | database = strategy, 24 | plugins = "bundled, request-transformer", 25 | nginx_conf = "spec/fixtures/custom_nginx.template", 26 | })) 27 | admin_client = helpers.admin_client() 28 | end) 29 | 30 | describe("validate config parameters", function() 31 | it("remove succeeds without colons", function() 32 | local res = assert(admin_client:send { 33 | method = "POST", 34 | path = "/plugins", 35 | body = { 36 | name = "request-transformer", 37 | config = { 38 | remove = { 39 | headers = {"just_a_key"}, 40 | body = {"just_a_key"}, 41 | querystring = {"just_a_key"}, 42 | }, 43 | }, 44 | }, 45 | headers = { 46 | ["Content-Type"] = "application/json", 47 | }, 48 | }) 49 | assert.response(res).has.status(201) 50 | local body = assert.response(res).has.jsonbody() 51 | assert.equals("just_a_key", body.config.remove.headers[1]) 52 | assert.equals("just_a_key", body.config.remove.body[1]) 53 | assert.equals("just_a_key", body.config.remove.querystring[1]) 54 | end) 55 | it("add fails with missing colons for key/value separation", function() 56 | local res = assert(admin_client:send { 57 | method = "POST", 58 | path = "/plugins", 59 | body = { 60 | name = "request-transformer", 61 | config = { 62 | add = { 63 | headers = {"just_a_key"}, 64 | }, 65 | }, 66 | }, 67 | headers = { 68 | ["Content-Type"] = "application/json", 69 | }, 70 | }) 71 | local body = assert.response(res).has.status(400) 72 | local json = cjson.decode(body) 73 | local msg = { "invalid value: just_a_key" } 74 | local expected = { config = { add = { headers = msg } } } 75 | assert.same(expected, json["fields"]) 76 | end) 77 | it("replace fails with missing colons for key/value separation", function() 78 | local res = assert(admin_client:send { 79 | method = "POST", 80 | path = "/plugins", 81 | body = { 82 | name = "request-transformer", 83 | config = { 84 | replace = { 85 | headers = {"just_a_key"}, 86 | }, 87 | }, 88 | }, 89 | headers = { 90 | ["Content-Type"] = "application/json", 91 | }, 92 | }) 93 | local body = assert.response(res).has.status(400) 94 | local json = cjson.decode(body) 95 | local msg = { "invalid value: just_a_key" } 96 | local expected = { config = { replace = { headers = msg } } } 97 | assert.same(expected, json["fields"]) 98 | end) 99 | it("append fails with missing colons for key/value separation", function() 100 | local res = assert(admin_client:send { 101 | method = "POST", 102 | path = "/plugins", 103 | body = { 104 | name = "request-transformer", 105 | config = { 106 | append = { 107 | headers = {"just_a_key"}, 108 | }, 109 | }, 110 | }, 111 | headers = { 112 | ["Content-Type"] = "application/json", 113 | }, 114 | }) 115 | local body = assert.response(res).has.status(400) 116 | local json = cjson.decode(body) 117 | local msg = { "invalid value: just_a_key" } 118 | local expected = { config = { append = { headers = msg } } } 119 | assert.same(expected, json["fields"]) 120 | end) 121 | it("it does not allow null value for arrays", function() 122 | local res = assert(admin_client:send { 123 | method = "POST", 124 | path = "/plugins", 125 | body = { 126 | name = "request-transformer", 127 | config = { 128 | remove = { 129 | body = cjson.null, 130 | headers = cjson.null, 131 | querystring = cjson.null, 132 | }, 133 | rename = { 134 | body = cjson.null, 135 | headers = cjson.null, 136 | querystring = cjson.null, 137 | }, 138 | replace = { 139 | body = cjson.null, 140 | headers = cjson.null, 141 | querystring = cjson.null, 142 | }, 143 | add = { 144 | body = cjson.null, 145 | headers = cjson.null, 146 | querystring = cjson.null, 147 | }, 148 | append = { 149 | body = cjson.null, 150 | headers = cjson.null, 151 | querystring = cjson.null, 152 | }, 153 | }, 154 | }, 155 | headers = { 156 | ["Content-Type"] = "application/json", 157 | }, 158 | }) 159 | assert.response(res).has.status(400) 160 | local body = assert.response(res).has.jsonbody() 161 | assert.same({ 162 | remove = { 163 | body = "required field missing", 164 | headers = "required field missing", 165 | querystring = "required field missing", 166 | }, 167 | rename = { 168 | body = "required field missing", 169 | headers = "required field missing", 170 | querystring = "required field missing", 171 | }, 172 | replace = { 173 | body = "required field missing", 174 | headers = "required field missing", 175 | querystring = "required field missing", 176 | }, 177 | add = { 178 | body = "required field missing", 179 | headers = "required field missing", 180 | querystring = "required field missing", 181 | }, 182 | append = { 183 | body = "required field missing", 184 | headers = "required field missing", 185 | querystring = "required field missing", 186 | }, 187 | }, body.fields.config) 188 | end) 189 | 190 | end) 191 | end) 192 | end) 193 | end 194 | --------------------------------------------------------------------------------