├── .github └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── resty └── ipmatcher.lua ├── rockspec ├── lua-resty-ipmatcher-0.2-0.rockspec ├── lua-resty-ipmatcher-0.3-0.rockspec ├── lua-resty-ipmatcher-0.4-0.rockspec ├── lua-resty-ipmatcher-0.5-0.rockspec ├── lua-resty-ipmatcher-0.6-0.rockspec ├── lua-resty-ipmatcher-0.6.1-0.rockspec └── lua-resty-ipmatcher-master-0.rockspec └── t ├── IP.pm ├── lib └── ljson.lua └── sanity.t /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - "master" 7 | paths: 8 | - 'rockspec/**' 9 | 10 | jobs: 11 | release: 12 | name: Release 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v2 17 | 18 | - name: Install Lua 19 | uses: leafo/gh-actions-lua@v8 20 | 21 | - name: Install Luarocks 22 | uses: leafo/gh-actions-luarocks@v4 23 | 24 | - name: Extract release name 25 | id: release_env 26 | shell: bash 27 | run: | 28 | title="${{ github.event.head_commit.message }}" 29 | re="^feat: release v*(\S+)" 30 | if [[ $title =~ $re ]]; then 31 | v=v${BASH_REMATCH[1]} 32 | echo "##[set-output name=version;]${v}" 33 | echo "##[set-output name=version_withou_v;]${BASH_REMATCH[1]}" 34 | else 35 | echo "commit format is not correct" 36 | exit 1 37 | fi 38 | 39 | - name: Create Release 40 | uses: actions/create-release@v1 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | with: 44 | tag_name: ${{ steps.release_env.outputs.version }} 45 | release_name: ${{ steps.release_env.outputs.version }} 46 | draft: false 47 | prerelease: false 48 | 49 | - name: Upload to luarocks 50 | env: 51 | LUAROCKS_TOKEN: ${{ secrets.LUAROCKS_TOKEN }} 52 | run: | 53 | luarocks install dkjson 54 | luarocks upload rockspec/lua-resty-ipmatcher-${{ steps.release_env.outputs.version_withou_v }}-0.rockspec --api-key=${LUAROCKS_TOKEN} 55 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | test: 11 | runs-on: "ubuntu-20.04" 12 | env: 13 | OPENRESTY_PREFIX: "/usr/local/openresty" 14 | 15 | steps: 16 | - name: Check out code 17 | uses: actions/checkout@v2 18 | 19 | - name: Linux Get dependencies 20 | run: sudo apt install -y cpanminus build-essential libncurses5-dev libreadline-dev libssl-dev perl luarocks 21 | 22 | - name: Linux Before install 23 | run: | 24 | sudo cpanm --notest Test::Nginx > build.log 2>&1 || (cat build.log && exit 1) 25 | sudo luarocks install luacheck 26 | 27 | - name: Linux Install 28 | run: | 29 | wget -qO - https://openresty.org/package/pubkey.gpg | sudo apt-key add - 30 | sudo apt-get -y install software-properties-common 31 | sudo add-apt-repository -y "deb http://openresty.org/package/ubuntu $(lsb_release -sc) main" 32 | sudo apt-get update 33 | sudo apt-get install openresty 34 | 35 | - name: Linux Script 36 | run: | 37 | export PATH=$OPENRESTY_PREFIX/nginx/sbin:$PATH 38 | make test 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Lua sources 2 | luac.out 3 | 4 | # luarocks build files 5 | *.src.rock 6 | *.zip 7 | *.tar.gz 8 | 9 | # Object files 10 | *.o 11 | *.os 12 | *.ko 13 | *.obj 14 | *.elf 15 | 16 | # Precompiled Headers 17 | *.gch 18 | *.pch 19 | 20 | # Libraries 21 | *.lib 22 | *.a 23 | *.la 24 | *.lo 25 | *.def 26 | *.exp 27 | 28 | # Shared objects (inc. Windows DLLs) 29 | *.dll 30 | *.so 31 | *.so.* 32 | *.dylib 33 | 34 | # Executables 35 | *.exe 36 | *.out 37 | *.app 38 | *.i*86 39 | *.x86_64 40 | *.hex 41 | 42 | # 43 | t/servroot 44 | go 45 | -------------------------------------------------------------------------------- /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 [yyyy] [name of copyright owner] 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | INST_PREFIX ?= /usr 2 | INST_LIBDIR ?= $(INST_PREFIX)/lib/lua/5.1 3 | INST_LUADIR ?= $(INST_PREFIX)/share/lua/5.1 4 | INSTALL ?= install 5 | 6 | 7 | ### lint: Lint Lua source code 8 | .PHONY: lint 9 | lint: 10 | luacheck -q resty 11 | 12 | ### test: Run test suite. Use test=... for specific tests 13 | .PHONY: test 14 | test: 15 | TEST_NGINX_LOG_LEVEL=info \ 16 | prove -I. -I../test-nginx/lib -r t/ 17 | 18 | 19 | ### install: Install the library to runtime 20 | .PHONY: install 21 | install: 22 | $(INSTALL) -d $(INST_LUADIR)/resty/ 23 | $(INSTALL) resty/*.lua $(INST_LUADIR)/resty/ 24 | 25 | 26 | ### help: Show Makefile rules 27 | .PHONY: help 28 | help: 29 | @echo Makefile rules: 30 | @echo 31 | @grep -E '^### [-A-Za-z0-9_]+:' Makefile | sed 's/###/ /' 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Name 2 | 3 | High-performance IP address matching for OpenResty Lua. 4 | 5 | # Table of Contents 6 | 7 | - [Name](#name) 8 | - [Table of Contents](#table-of-contents) 9 | - [Synopsis](#synopsis) 10 | - [Methods](#methods) 11 | - [ipmatcher.new](#ipmatchernew) 12 | - [Usage](#usage) 13 | - [Example](#example) 14 | - [ipmatcher.new\_with\_value](#ipmatchernew_with_value) 15 | - [Usage](#usage-1) 16 | - [Example](#example-1) 17 | - [ip:match](#ipmatch) 18 | - [Usage](#usage-2) 19 | - [Example](#example-2) 20 | - [ip:match\_bin](#ipmatch_bin) 21 | - [Usage](#usage-3) 22 | - [Example](#example-3) 23 | - [ipmatcher.parse\_ipv4](#ipmatcherparse_ipv4) 24 | - [ipmatcher.parse\_ipv6](#ipmatcherparse_ipv6) 25 | - [Installation](#installation) 26 | - [From LuaRocks](#from-luarocks) 27 | - [From Source](#from-source) 28 | 29 | # Synopsis 30 | 31 | ```lua 32 | location / { 33 | content_by_lua_block { 34 | local ipmatcher = require("resty.ipmatcher") 35 | local ip = ipmatcher.new({ 36 | "127.0.0.1", 37 | "192.168.0.0/16", 38 | "::1", 39 | "fe80::/32", 40 | }) 41 | 42 | ngx.say(ip:match("127.0.0.1")) 43 | ngx.say(ip:match("192.168.1.100")) 44 | ngx.say(ip:match("::1")) 45 | } 46 | } 47 | ``` 48 | 49 | [Back to TOC](#table-of-contents) 50 | 51 | # Methods 52 | 53 | ## ipmatcher.new 54 | 55 | Creates a new hash table to store IP addresses. 56 | 57 | ### Usage 58 | 59 | `ips` is a list of IPv4 or IPv6 IP addresses in a CIDR format (`{ip1, ip2, ip3, ...}`). 60 | 61 | ```lua 62 | ok, err = ipmatcher.new(ips) 63 | ``` 64 | 65 | Returns `nil` and the error if it fails to create a new `ipmatcher` instance. 66 | 67 | ### Example 68 | 69 | ```lua 70 | local ip, err = ipmatcher.new({ 71 | "127.0.0.1", "192.168.0.0/16", "::1", "fe80::/16", 72 | }) 73 | ``` 74 | 75 | [Back to TOC](#table-of-contents) 76 | 77 | ## ipmatcher.new_with_value 78 | 79 | Creates a new hash table to store IP addresses and corresponding values. 80 | 81 | ### Usage 82 | 83 | `ips` is a list of key-value pairs (`{[ip1] = val1, [ip2] = val2, ...}`), where each key is an IP address string (CIDR format for IPv4 and IPv6). 84 | 85 | ```lua 86 | matcher, err = ipmatcher.new_with_value(ips) 87 | ``` 88 | 89 | Returns `nil` and the error if it fails to create a new `ipmatcher` instance. 90 | 91 | ### Example 92 | 93 | ```lua 94 | local ip, err = ipmatcher.new_with_value({ 95 | ["127.0.0.1"] = {info = "a"}, 96 | ["192.168.0.0/16"] = {info = "b"}, 97 | }) 98 | local data, err = ip:match("192.168.0.1") 99 | print(data.info) -- "b" 100 | ``` 101 | 102 | If the IP address matches multiple values, the returned value can be either one of the values: 103 | 104 | ```lua 105 | local ip, err = ipmatcher.new_with_value({ 106 | ["192.168.0.1"] = {info = "a"}, 107 | ["192.168.0.0/16"] = {info = "b"}, 108 | }) 109 | local data, err = ip:match("192.168.0.1") 110 | print(data.info) -- "a" or "b" 111 | ``` 112 | 113 | [Back to TOC](#table-of-contents) 114 | 115 | ## ip:match 116 | 117 | Checks if an IP address exists in the specified IP list. 118 | 119 | ### Usage 120 | 121 | `ip` is an IP address string. 122 | 123 | ```lua 124 | ok, err = ip:match(ip) 125 | ``` 126 | 127 | Returns `true` or `value` if the specified `ip` exists in the list. Returns `false` if the `ip` does not exist in the list. And returns `false` and an error message if the IP address is invalid. 128 | 129 | ### Example 130 | 131 | ```lua 132 | local ip, err = ipmatcher.new({ 133 | "127.0.0.1", "192.168.0.0/16", "::1", "fe80::/16", 134 | }) 135 | 136 | local ok, err = ip:match("127.0.0.1") -- true 137 | ``` 138 | 139 | [Back to TOC](#table-of-contents) 140 | 141 | ## ip:match_bin 142 | 143 | Checks if an IP address in binary format exists in the specified IP list. 144 | 145 | ### Usage 146 | 147 | `bin_ip` is an IP address in binary format. 148 | 149 | ```lua 150 | ok, err = ip:match_bin(bin_ip) 151 | ``` 152 | 153 | Returns `true` if the specified `bin_ip` exists in the list. Returns `false` if it does not exist. Returns `nil` and an error message if `bin_ip` is invalid. 154 | 155 | ### Example 156 | 157 | ```lua 158 | local ok, err = ip:match_bin(ngx.var.binary_remote_addr) 159 | ``` 160 | 161 | [Back to TOC](#table-of-contents) 162 | 163 | ## ipmatcher.parse_ipv4 164 | 165 | Tries to parse an IPv4 address to a host byte order FFI `uint32_t` type integer. 166 | 167 | ```lua 168 | ipmatcher.parse_ipv4(ip) 169 | ``` 170 | 171 | Returns `false` if the IP address is invalid. 172 | 173 | [Back to TOC](#table-of-contents) 174 | 175 | ## ipmatcher.parse_ipv6 176 | 177 | Tries to parse an IPv6 address to a table with four host byte order FF1 `uint32_t` type integer. The IP address can be wrapped in square brackets like `[::1]`. 178 | 179 | ```lua 180 | ipmatcher.parse_ipv6(ip) 181 | ``` 182 | 183 | Returns a `false` if the ip is not a valid IPv6 address. 184 | 185 | [Back to TOC](#table-of-contents) 186 | 187 | # Installation 188 | 189 | ## From LuaRocks 190 | 191 | ```shell 192 | luarocks install lua-resty-ipmatcher 193 | ``` 194 | 195 | ## From Source 196 | 197 | ```shell 198 | make install 199 | ``` 200 | 201 | [Back to TOC](#table-of-contents) 202 | -------------------------------------------------------------------------------- /resty/ipmatcher.lua: -------------------------------------------------------------------------------- 1 | local base = require("resty.core.base") 2 | local bit = require("bit") 3 | local clear_tab = require("table.clear") 4 | local nkeys = require("table.nkeys") 5 | local new_tab = base.new_tab 6 | local find_str = string.find 7 | local tonumber = tonumber 8 | local ipairs = ipairs 9 | local pairs = pairs 10 | local ffi = require "ffi" 11 | local ffi_cdef = ffi.cdef 12 | local ffi_copy = ffi.copy 13 | local ffi_new = ffi.new 14 | local C = ffi.C 15 | local insert_tab = table.insert 16 | local sort_tab = table.sort 17 | local string = string 18 | local setmetatable=setmetatable 19 | local type = type 20 | local error = error 21 | local str_sub = string.sub 22 | local str_byte = string.byte 23 | local cur_level = ngx.config.subsystem == "http" and 24 | require "ngx.errlog" .get_sys_filter_level() 25 | 26 | local AF_INET = 2 27 | local AF_INET6 = 10 28 | if ffi.os == "OSX" then 29 | AF_INET6 = 30 30 | elseif ffi.os == "BSD" then 31 | AF_INET6 = 28 32 | elseif ffi.os == "Windows" then 33 | AF_INET6 = 23 34 | end 35 | 36 | 37 | local _M = {_VERSION = 0.3} 38 | 39 | 40 | ffi_cdef[[ 41 | int inet_pton(int af, const char * restrict src, void * restrict dst); 42 | uint32_t ntohl(uint32_t netlong); 43 | ]] 44 | 45 | 46 | local parse_ipv4 47 | do 48 | local inet = ffi_new("unsigned int [1]") 49 | 50 | function parse_ipv4(ip) 51 | if not ip then 52 | return false 53 | end 54 | 55 | if C.inet_pton(AF_INET, ip, inet) ~= 1 then 56 | return false 57 | end 58 | 59 | return C.ntohl(inet[0]) 60 | end 61 | end 62 | _M.parse_ipv4 = parse_ipv4 63 | 64 | local parse_bin_ipv4 65 | do 66 | local inet = ffi_new("unsigned int [1]") 67 | 68 | function parse_bin_ipv4(ip) 69 | if not ip or #ip ~= 4 then 70 | return false 71 | end 72 | 73 | ffi_copy(inet, ip, 4) 74 | return C.ntohl(inet[0]) 75 | end 76 | end 77 | 78 | local parse_ipv6 79 | do 80 | local inets = ffi_new("unsigned int [4]") 81 | 82 | function parse_ipv6(ip) 83 | if not ip then 84 | return false 85 | end 86 | 87 | if str_byte(ip, 1, 1) == str_byte('[') 88 | and str_byte(ip, #ip) == str_byte(']') then 89 | 90 | -- strip square brackets around IPv6 literal if present 91 | ip = str_sub(ip, 2, #ip - 1) 92 | end 93 | 94 | if C.inet_pton(AF_INET6, ip, inets) ~= 1 then 95 | return false 96 | end 97 | 98 | local inets_arr = new_tab(4, 0) 99 | for i = 0, 3 do 100 | insert_tab(inets_arr, C.ntohl(inets[i])) 101 | end 102 | return inets_arr 103 | end 104 | end 105 | _M.parse_ipv6 = parse_ipv6 106 | 107 | local parse_bin_ipv6 108 | do 109 | local inets = ffi_new("unsigned int [4]") 110 | 111 | function parse_bin_ipv6(ip) 112 | if not ip or #ip ~= 16 then 113 | return false 114 | end 115 | 116 | ffi_copy(inets, ip, 16) 117 | local inets_arr = new_tab(4, 0) 118 | for i = 0, 3 do 119 | insert_tab(inets_arr, C.ntohl(inets[i])) 120 | end 121 | return inets_arr 122 | end 123 | end 124 | 125 | 126 | local mt = {__index = _M} 127 | 128 | 129 | local ngx_log = ngx.log 130 | local ngx_INFO = ngx.INFO 131 | local function log_info(...) 132 | if cur_level and ngx_INFO > cur_level then 133 | return 134 | end 135 | 136 | return ngx_log(ngx_INFO, ...) 137 | end 138 | 139 | 140 | local function split_ip(ip_addr_org) 141 | local idx = find_str(ip_addr_org, "/", 1, true) 142 | if not idx then 143 | return ip_addr_org 144 | end 145 | 146 | local ip_addr = str_sub(ip_addr_org, 1, idx - 1) 147 | local ip_addr_mask = str_sub(ip_addr_org, idx + 1) 148 | return ip_addr, tonumber(ip_addr_mask) 149 | end 150 | _M.split_ip = split_ip 151 | 152 | 153 | local idxs = {} 154 | local function gen_ipv6_idxs(inets_ipv6, mask) 155 | clear_tab(idxs) 156 | 157 | for _, inet in ipairs(inets_ipv6) do 158 | local valid_mask = mask 159 | if valid_mask > 32 then 160 | valid_mask = 32 161 | end 162 | 163 | if valid_mask == 32 then 164 | insert_tab(idxs, inet) 165 | else 166 | insert_tab(idxs, bit.rshift(inet, 32 - valid_mask)) 167 | end 168 | 169 | mask = mask - 32 170 | if mask <= 0 then 171 | break 172 | end 173 | end 174 | 175 | return idxs 176 | end 177 | 178 | 179 | local function cmp(x, y) 180 | return x > y 181 | end 182 | 183 | 184 | local function new(ips, with_value) 185 | if not ips or type(ips) ~= "table" then 186 | error("missing valid ip argument", 2) 187 | end 188 | 189 | local parsed_ipv4s = {} 190 | local parsed_ipv4s_mask = {} 191 | local ipv4_match_all_value 192 | 193 | local parsed_ipv6s = {} 194 | local parsed_ipv6s_mask = {} 195 | local ipv6_values = {} 196 | local ipv6s_values_idx = 1 197 | local ipv6_match_all_value 198 | 199 | local iter = with_value and pairs or ipairs 200 | for a, b in iter(ips) do 201 | local ip_addr_org, value 202 | if with_value then 203 | ip_addr_org = a 204 | value = b 205 | 206 | else 207 | ip_addr_org = b 208 | value = true 209 | end 210 | 211 | local ip_addr, ip_addr_mask = split_ip(ip_addr_org) 212 | 213 | local inet_ipv4 = parse_ipv4(ip_addr) 214 | if inet_ipv4 then 215 | ip_addr_mask = ip_addr_mask or 32 216 | if ip_addr_mask == 32 then 217 | parsed_ipv4s[inet_ipv4] = value 218 | 219 | elseif ip_addr_mask == 0 then 220 | ipv4_match_all_value = value 221 | 222 | else 223 | local valid_inet_addr = bit.rshift(inet_ipv4, 32 - ip_addr_mask) 224 | 225 | parsed_ipv4s_mask[ip_addr_mask] = parsed_ipv4s_mask[ip_addr_mask] or {} 226 | parsed_ipv4s_mask[ip_addr_mask][valid_inet_addr] = value 227 | log_info("ipv4 mask: ", ip_addr_mask, 228 | " valid inet: ", valid_inet_addr) 229 | end 230 | 231 | goto continue 232 | end 233 | 234 | local inets_ipv6 = parse_ipv6(ip_addr) 235 | if inets_ipv6 then 236 | ip_addr_mask = ip_addr_mask or 128 237 | if ip_addr_mask == 128 then 238 | parsed_ipv6s[ip_addr] = value 239 | 240 | elseif ip_addr_mask == 0 then 241 | ipv6_match_all_value = value 242 | end 243 | 244 | parsed_ipv6s[ip_addr_mask] = parsed_ipv6s[ip_addr_mask] or {} 245 | 246 | local inets_idxs = gen_ipv6_idxs(inets_ipv6, ip_addr_mask) 247 | local node = parsed_ipv6s[ip_addr_mask] 248 | for i, inet in ipairs(inets_idxs) do 249 | if i == #inets_idxs then 250 | if with_value then 251 | ipv6_values[ipv6s_values_idx] = value 252 | node[inet] = ipv6s_values_idx 253 | ipv6s_values_idx = ipv6s_values_idx + 1 254 | else 255 | node[inet] = true 256 | end 257 | end 258 | node[inet] = node[inet] or {} 259 | node = node[inet] 260 | end 261 | 262 | parsed_ipv6s_mask[ip_addr_mask] = true 263 | 264 | goto continue 265 | end 266 | 267 | if not inet_ipv4 and not inets_ipv6 then 268 | return nil, "invalid ip address: " .. ip_addr 269 | end 270 | 271 | ::continue:: 272 | end 273 | 274 | local ipv4_mask_arr = new_tab(nkeys(parsed_ipv4s_mask), 0) 275 | local i = 1 276 | for k, _ in pairs(parsed_ipv4s_mask) do 277 | ipv4_mask_arr[i] = k 278 | i = i + 1 279 | end 280 | 281 | sort_tab(ipv4_mask_arr, cmp) 282 | 283 | local ipv6_mask_arr = new_tab(nkeys(parsed_ipv6s_mask), 0) 284 | 285 | i = 1 286 | for k, _ in pairs(parsed_ipv6s_mask) do 287 | ipv6_mask_arr[i] = k 288 | i = i + 1 289 | end 290 | 291 | sort_tab(ipv6_mask_arr, cmp) 292 | 293 | return setmetatable({ 294 | ipv4 = parsed_ipv4s, 295 | ipv4_mask = parsed_ipv4s_mask, 296 | ipv4_mask_arr = ipv4_mask_arr, 297 | ipv4_match_all_value = ipv4_match_all_value, 298 | 299 | ipv6 = parsed_ipv6s, 300 | ipv6_mask = parsed_ipv6s_mask, 301 | ipv6_mask_arr = ipv6_mask_arr, 302 | ipv6_values = ipv6_values, 303 | ipv6_match_all_value = ipv6_match_all_value, 304 | }, mt) 305 | end 306 | 307 | function _M.new(ips) 308 | return new(ips, false) 309 | end 310 | 311 | function _M.new_with_value(ips) 312 | return new(ips, true) 313 | end 314 | 315 | 316 | local function match_ipv4(self, ip) 317 | local ipv4s = self.ipv4 318 | local value = ipv4s[ip] 319 | if value ~= nil then 320 | return value 321 | end 322 | 323 | local ipv4_mask = self.ipv4_mask 324 | if self.ipv4_match_all_value ~= nil then 325 | return self.ipv4_match_all_value -- match any ip 326 | end 327 | 328 | for _, mask in ipairs(self.ipv4_mask_arr) do 329 | local valid_inet_addr = bit.rshift(ip, 32 - mask) 330 | 331 | log_info("ipv4 mask: ", mask, 332 | " valid inet: ", valid_inet_addr) 333 | 334 | value = ipv4_mask[mask][valid_inet_addr] 335 | if value ~= nil then 336 | return value 337 | end 338 | end 339 | 340 | return false 341 | end 342 | 343 | local function match_ipv6(self, ip) 344 | local ipv6s = self.ipv6 345 | if self.ipv6_match_all_value ~= nil then 346 | return self.ipv6_match_all_value -- match any ip 347 | end 348 | 349 | for _, mask in ipairs(self.ipv6_mask_arr) do 350 | local node = ipv6s[mask] 351 | local inet_idxs = gen_ipv6_idxs(ip, mask) 352 | for _, inet in ipairs(inet_idxs) do 353 | if not node[inet] then 354 | break 355 | else 356 | node = node[inet] 357 | if node == true then 358 | return true 359 | end 360 | if type(node) == "number" then 361 | -- fetch with the ipv6s_values_idx 362 | return self.ipv6_values[node] 363 | end 364 | end 365 | end 366 | end 367 | 368 | return false 369 | end 370 | 371 | function _M.match(self, ip) 372 | local inet_ipv4 = parse_ipv4(ip) 373 | if inet_ipv4 then 374 | return match_ipv4(self, inet_ipv4) 375 | end 376 | 377 | local inets_ipv6 = parse_ipv6(ip) 378 | if not inets_ipv6 then 379 | return false, "invalid ip address, not ipv4 and ipv6" 380 | end 381 | 382 | local ipv6s = self.ipv6 383 | local value = ipv6s[ip] 384 | if value ~= nil then 385 | return value 386 | end 387 | 388 | return match_ipv6(self, inets_ipv6) 389 | end 390 | 391 | 392 | function _M.match_bin(self, bin_ip) 393 | local inet_ipv4 = parse_bin_ipv4(bin_ip) 394 | if inet_ipv4 then 395 | return match_ipv4(self, inet_ipv4) 396 | end 397 | 398 | local inets_ipv6 = parse_bin_ipv6(bin_ip) 399 | if not inets_ipv6 then 400 | return false, "invalid ip address, not ipv4 and ipv6" 401 | end 402 | 403 | return match_ipv6(self, inets_ipv6) 404 | end 405 | 406 | 407 | return _M 408 | -------------------------------------------------------------------------------- /rockspec/lua-resty-ipmatcher-0.2-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua-resty-ipmatcher" 2 | version = "0.2-0" 3 | source = { 4 | url = "git://github.com/iresty/lua-resty-ipmatcher", 5 | tag = "v0.2", 6 | } 7 | 8 | description = { 9 | summary = "High performance match IP address for OpenResty Lua.", 10 | homepage = "https://github.com/iresty/lua-resty-ipmatcher", 11 | license = "Apache License 2.0", 12 | maintainer = "Yuansheng Wang " 13 | } 14 | 15 | 16 | build = { 17 | type = "builtin", 18 | modules = { 19 | ["resty.ipmatcher"] = "resty/ipmatcher.lua" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /rockspec/lua-resty-ipmatcher-0.3-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua-resty-ipmatcher" 2 | version = "0.3-0" 3 | source = { 4 | url = "git://github.com/iresty/lua-resty-ipmatcher", 5 | tag = "v0.3", 6 | } 7 | 8 | description = { 9 | summary = "High performance match IP address for OpenResty Lua.", 10 | homepage = "https://github.com/iresty/lua-resty-ipmatcher", 11 | license = "Apache License 2.0", 12 | maintainer = "Yuansheng Wang " 13 | } 14 | 15 | build = { 16 | type = "builtin", 17 | modules = { 18 | ["resty.ipmatcher"] = "resty/ipmatcher.lua" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /rockspec/lua-resty-ipmatcher-0.4-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua-resty-ipmatcher" 2 | version = "0.4-0" 3 | source = { 4 | url = "git://github.com/iresty/lua-resty-ipmatcher", 5 | tag = "v0.4", 6 | } 7 | 8 | description = { 9 | summary = "High performance match IP address for Lua(OpenResty).", 10 | homepage = "https://github.com/iresty/lua-resty-ipmatcher", 11 | license = "Apache License 2.0", 12 | maintainer = "Yuansheng Wang " 13 | } 14 | 15 | build = { 16 | type = "builtin", 17 | modules = { 18 | ["resty.ipmatcher"] = "resty/ipmatcher.lua" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /rockspec/lua-resty-ipmatcher-0.5-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua-resty-ipmatcher" 2 | version = "0.5-0" 3 | source = { 4 | url = "git://github.com/iresty/lua-resty-ipmatcher", 5 | tag = "v0.5", 6 | } 7 | 8 | description = { 9 | summary = "High performance match IP address for Lua(OpenResty).", 10 | homepage = "https://github.com/iresty/lua-resty-ipmatcher", 11 | license = "Apache License 2.0", 12 | maintainer = "Yuansheng Wang " 13 | } 14 | 15 | build = { 16 | type = "builtin", 17 | modules = { 18 | ["resty.ipmatcher"] = "resty/ipmatcher.lua" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /rockspec/lua-resty-ipmatcher-0.6-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua-resty-ipmatcher" 2 | version = "0.6-0" 3 | source = { 4 | url = "git://github.com/iresty/lua-resty-ipmatcher", 5 | tag = "v0.6", 6 | } 7 | 8 | description = { 9 | summary = "High performance match IP address for Lua(OpenResty).", 10 | homepage = "https://github.com/iresty/lua-resty-ipmatcher", 11 | license = "Apache License 2.0", 12 | maintainer = "Yuansheng Wang " 13 | } 14 | 15 | build = { 16 | type = "builtin", 17 | modules = { 18 | ["resty.ipmatcher"] = "resty/ipmatcher.lua" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /rockspec/lua-resty-ipmatcher-0.6.1-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua-resty-ipmatcher" 2 | version = "0.6.1-0" 3 | source = { 4 | url = "git://github.com/api7/lua-resty-ipmatcher", 5 | tag = "v0.6.1", 6 | } 7 | 8 | description = { 9 | summary = "High performance match IP address for Lua(OpenResty).", 10 | homepage = "https://github.com/api7/lua-resty-ipmatcher", 11 | license = "Apache License 2.0", 12 | maintainer = "Yuansheng Wang " 13 | } 14 | 15 | build = { 16 | type = "builtin", 17 | modules = { 18 | ["resty.ipmatcher"] = "resty/ipmatcher.lua" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /rockspec/lua-resty-ipmatcher-master-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua-resty-ipmatcher" 2 | version = "master-0" 3 | source = { 4 | url = "git://github.com/iresty/lua-resty-ipmatcher", 5 | branch = "master", 6 | } 7 | 8 | description = { 9 | summary = "High performance match IP address for Lua(OpenResty).", 10 | homepage = "https://github.com/iresty/lua-resty-ipmatcher", 11 | license = "Apache License 2.0", 12 | maintainer = "Yuansheng Wang " 13 | } 14 | 15 | build = { 16 | type = "builtin", 17 | modules = { 18 | ["resty.ipmatcher"] = "resty/ipmatcher.lua" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /t/IP.pm: -------------------------------------------------------------------------------- 1 | package t::IP; 2 | 3 | use lib 'lib'; 4 | 5 | use Test::Nginx::Socket::Lua::Stream -Base; 6 | 7 | repeat_each(2); 8 | log_level('info'); 9 | no_long_string(); 10 | no_shuffle(); 11 | 12 | my $pwd = `pwd`; 13 | chomp $pwd; 14 | 15 | add_block_preprocessor(sub { 16 | my ($block) = @_; 17 | 18 | my $http_config = $block->http_config // ''; 19 | $http_config = <<_EOC_; 20 | lua_package_path '$pwd/t/lib/?.lua;$pwd/?.lua;\$prefix/?.lua;;'; 21 | lua_package_cpath '$pwd/?.so;;'; 22 | 23 | $http_config 24 | _EOC_ 25 | 26 | $block->set_value("http_config", $http_config); 27 | }); 28 | 29 | 1; 30 | -------------------------------------------------------------------------------- /t/lib/ljson.lua: -------------------------------------------------------------------------------- 1 | local ngx_null = ngx.null 2 | local tostring = tostring 3 | local gsub = string.gsub 4 | local sort = table.sort 5 | local pairs = pairs 6 | local ipairs = ipairs 7 | local concat = table.concat 8 | local type = type 9 | local new_tab = require("table.new") 10 | 11 | local _M = {version = 0.1} 12 | 13 | local metachars = { 14 | ['\t'] = '\\t', 15 | ["\\"] = "\\\\", 16 | ['"'] = '\\"', 17 | ['\r'] = '\\r', 18 | ['\n'] = '\\n', 19 | } 20 | 21 | local function encode_str(s) 22 | -- XXX we will rewrite this when string.buffer is implemented 23 | -- in LuaJIT 2.1 because string.gsub cannot be JIT compiled. 24 | return gsub(s, '["\\\r\n\t]', metachars) 25 | end 26 | 27 | local function is_arr(t) 28 | local exp = 1 29 | for k, _ in pairs(t) do 30 | if k ~= exp then 31 | return nil 32 | end 33 | exp = exp + 1 34 | end 35 | return exp - 1 36 | end 37 | 38 | local encode 39 | 40 | encode = function (v) 41 | if v == nil or v == ngx_null then 42 | return "null" 43 | end 44 | 45 | local typ = type(v) 46 | if typ == 'string' then 47 | return '"' .. encode_str(v) .. '"' 48 | end 49 | 50 | if typ == 'number' or typ == 'boolean' then 51 | return tostring(v) 52 | end 53 | 54 | if typ == 'table' then 55 | local n = is_arr(v) 56 | if n then 57 | local bits = new_tab(n, 0) 58 | for i, elem in ipairs(v) do 59 | bits[i] = encode(elem) 60 | end 61 | return "[" .. concat(bits, ",") .. "]" 62 | end 63 | 64 | local keys = {} 65 | local i = 0 66 | for key, _ in pairs(v) do 67 | i = i + 1 68 | keys[i] = key 69 | end 70 | sort(keys) 71 | 72 | local bits = new_tab(0, i) 73 | i = 0 74 | for _, key in ipairs(keys) do 75 | i = i + 1 76 | bits[i] = encode(key) .. ":" .. encode(v[key]) 77 | end 78 | return "{" .. concat(bits, ",") .. "}" 79 | end 80 | 81 | return '"<' .. typ .. '>"' 82 | end 83 | _M.encode = encode 84 | 85 | return _M 86 | -------------------------------------------------------------------------------- /t/sanity.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et fdm=marker: 2 | 3 | use t::IP 'no_plan'; 4 | 5 | repeat_each(1); 6 | run_tests(); 7 | 8 | 9 | __DATA__ 10 | 11 | === TEST 1: ipv4 address 12 | --- config 13 | location /t { 14 | content_by_lua_block { 15 | local ip = require("resty.ipmatcher").new({ 16 | "127.0.0.1", 17 | "127.0.0.2", 18 | "192.168.0.0/16", 19 | }) 20 | 21 | ngx.say(ip:match("127.0.0.1")) 22 | ngx.say(ip:match("127.0.0.2")) 23 | ngx.say(ip:match("127.0.0.3")) 24 | ngx.say(ip:match("192.168.1.1")) 25 | ngx.say(ip:match("192.168.1.100")) 26 | ngx.say(ip:match("192.100.1.100")) 27 | } 28 | } 29 | --- request 30 | GET /t 31 | --- no_error_log 32 | [error] 33 | --- response_body 34 | true 35 | true 36 | false 37 | true 38 | true 39 | false 40 | 41 | 42 | 43 | === TEST 2: ipv6 address 44 | --- config 45 | location /t { 46 | content_by_lua_block { 47 | local ip = require("resty.ipmatcher").new({ 48 | "::1", 49 | "fe80::/32", 50 | }) 51 | 52 | ngx.say(ip:match("::1")) 53 | ngx.say(ip:match("::2")) 54 | ngx.say(ip:match("fe80::")) 55 | ngx.say(ip:match("fe80:1::")) 56 | 57 | ngx.say(ip:match("127.0.0.1")) 58 | } 59 | } 60 | --- request 61 | GET /t 62 | --- no_error_log 63 | [error] 64 | --- response_body 65 | true 66 | false 67 | true 68 | false 69 | false 70 | 71 | 72 | 73 | === TEST 3: invalid ip address 74 | --- config 75 | location /t { 76 | content_by_lua_block { 77 | local ip, err = require("resty.ipmatcher").new({ 78 | "127.0.0.ffff", 79 | }) 80 | 81 | ngx.say("ip: ", ip) 82 | ngx.say("err:", err) 83 | } 84 | } 85 | --- request 86 | GET /t 87 | --- no_error_log 88 | [error] 89 | --- response_body 90 | ip: nil 91 | err:invalid ip address: 127.0.0.ffff 92 | 93 | 94 | 95 | === TEST 4: invalid ip address 96 | --- config 97 | location /t { 98 | content_by_lua_block { 99 | local ip = require("resty.ipmatcher").new({ 100 | "127.0.0.1", 101 | }) 102 | 103 | local ok, err = ip:match("127.0.0.ffff") 104 | ngx.say("ok: ", ok) 105 | ngx.say("err:", err) 106 | } 107 | } 108 | --- request 109 | GET /t 110 | --- no_error_log 111 | [error] 112 | --- response_body 113 | ok: false 114 | err:invalid ip address, not ipv4 and ipv6 115 | 116 | 117 | 118 | === TEST 5: ipv6 address (short mask) 119 | --- config 120 | location /t { 121 | content_by_lua_block { 122 | local ip = require("resty.ipmatcher").new({ 123 | "fe80::/8", 124 | }) 125 | 126 | ngx.say(ip:match("fe81::")) 127 | ngx.say(ip:match("ff80::")) 128 | } 129 | } 130 | --- request 131 | GET /t 132 | --- no_error_log 133 | [error] 134 | --- response_body 135 | true 136 | false 137 | 138 | 139 | 140 | === TEST 6: parse ipv6 address 141 | --- config 142 | location /t { 143 | content_by_lua_block { 144 | local cases = { 145 | {ip = "127.0.0.ffff"}, 146 | {ip = ""}, 147 | {ip = "["}, 148 | {ip = "[]"}, 149 | {ip = "[:1:]"}, 150 | {ip = "[::1x"}, 151 | {ip = "127.0.0.1"}, 152 | } 153 | for _, case in ipairs(cases) do 154 | local valid = require("resty.ipmatcher").parse_ipv6(case.ip) 155 | if valid then 156 | ngx.log(ngx.ERR, "expect invalid IPv6 ", case.ip) 157 | end 158 | end 159 | 160 | local cases = { 161 | {ip = "::1"}, 162 | {ip = "[::1]"}, 163 | {ip = "ff80::"}, 164 | } 165 | for _, case in ipairs(cases) do 166 | local valid = require("resty.ipmatcher").parse_ipv6(case.ip) 167 | if not valid then 168 | ngx.log(ngx.ERR, "expect IPv6 ", case.ip) 169 | end 170 | end 171 | } 172 | } 173 | --- request 174 | GET /t 175 | --- no_error_log 176 | [error] 177 | 178 | 179 | 180 | === TEST 7: match binary ip 181 | This test requires building Nginx with --with-http_realip_module 182 | --- config 183 | location /foo { 184 | set_real_ip_from 127.0.0.1; 185 | content_by_lua_block { 186 | ngx.log(ngx.INFO, ngx.var.http_x_real_ip, " ", ngx.var.binary_remote_addr) 187 | ngx.print(ngx.var.binary_remote_addr) 188 | } 189 | } 190 | location /t { 191 | content_by_lua_block { 192 | local function get_bin_ip(ip) 193 | local sock = ngx.socket.tcp() 194 | sock:settimeout(500) 195 | 196 | local ok, err = sock:connect("127.0.0.1", $TEST_NGINX_SERVER_PORT) 197 | if not ok then 198 | ngx.log(ngx.ERR, "failed to connect: ", err) 199 | return 200 | end 201 | 202 | local req = "GET /foo HTTP/1.0\r\nHost: test.com\r\nConnection: close\r\nX-Real-IP:" .. ip .. "\r\n\r\n" 203 | local bytes, err = sock:send(req) 204 | if not bytes then 205 | ngx.log(ngx.ERR, "failed to send http request: ", err) 206 | return 207 | end 208 | 209 | -- skip http header 210 | while true do 211 | local data, err, _ = sock:receive('*l') 212 | if err then 213 | ngx.log(ngx.ERR, 'unexpected error occurs when receiving http head: ' .. err) 214 | return 215 | end 216 | if #data == 0 then -- read last line of head 217 | break 218 | end 219 | end 220 | 221 | local data, err = sock:receive('*a') 222 | sock:close() 223 | if not data then 224 | ngx.log(ngx.ERR, "failed to receive body: ", err) 225 | end 226 | return data 227 | end 228 | 229 | local ip = require("resty.ipmatcher").new({ 230 | "127.0.0.1", 231 | "192.168.0.0/16", 232 | "::1", 233 | "fe80::/8", 234 | }) 235 | local cases = { 236 | {ip = "127.0.0.1", matched = true}, 237 | {ip = "127.0.0.2", matched = false}, 238 | {ip = "192.168.0.22", matched = true}, 239 | {ip = "182.168.0.22", matched = false}, 240 | {ip = "::1", matched = true}, 241 | {ip = "::2", matched = false}, 242 | {ip = "fe80::1", matched = true}, 243 | } 244 | for _, case in ipairs(cases) do 245 | local res = ip:match_bin(get_bin_ip(case.ip)) 246 | if res ~= case.matched then 247 | ngx.say("unexpected result for ", case.ip) 248 | end 249 | end 250 | ngx.say("ok") 251 | } 252 | } 253 | --- request 254 | GET /t 255 | --- no_error_log 256 | [error] 257 | --- response_body 258 | ok 259 | 260 | 261 | 262 | === TEST 8: zero mask 263 | --- config 264 | location /t { 265 | content_by_lua_block { 266 | local ip = require("resty.ipmatcher").new({ 267 | "::/0", 268 | "0.0.0.0/0", 269 | }) 270 | 271 | ngx.say(ip:match("127.0.0.1")) 272 | ngx.say(ip:match("0.0.0.0")) 273 | ngx.say(ip:match("fe81::")) 274 | ngx.say(ip:match("ff80::")) 275 | } 276 | } 277 | --- request 278 | GET /t 279 | --- no_error_log 280 | [error] 281 | --- response_body 282 | true 283 | true 284 | true 285 | true 286 | 287 | 288 | 289 | === TEST 9: ipv6 special notation 290 | --- config 291 | location /t { 292 | content_by_lua_block { 293 | local ip = require("resty.ipmatcher").new({ 294 | "2001:0db8:85a3:0000:0000:8a2e:0370:7334", 295 | }) 296 | ngx.say(ip:match("2001:0db8:85a3:0000:0000:8a2e:0370:7334")) 297 | -- zero in some occasions can be omitted 298 | ngx.say(ip:match("2001:db8:85a3:0:0:8a2e:370:7334")) 299 | ngx.say(ip:match("2001:db8:85a3::8a2e:370:7334")) 300 | 301 | local ip = require("resty.ipmatcher").new({ 302 | "::ffff:192.0.2.128", 303 | }) 304 | ngx.say(ip:match("::ffff:192.0.2.128")) 305 | -- ipv4-mapped address 306 | ngx.say(ip:match("::ffff:c000:0280")) 307 | } 308 | } 309 | --- request 310 | GET /t 311 | --- no_error_log 312 | [error] 313 | --- response_body 314 | true 315 | true 316 | true 317 | true 318 | true 319 | 320 | 321 | 322 | === TEST 10: match with new_with_value 323 | --- config 324 | location /t { 325 | content_by_lua_block { 326 | local ip = require("resty.ipmatcher").new_with_value({ 327 | ["127.0.0.1"] = 1, 328 | ["192.168.0.0/16"] = "2", 329 | ["::1"] = 3, 330 | ["fe80::/32"] = {value = 4}, 331 | ["fe81::/32"] = false, 332 | }) 333 | 334 | ngx.say(ip:match("127.0.0.1")) 335 | ngx.say(ip:match("127.0.0.2")) 336 | ngx.say(ip:match("192.168.1.1")) 337 | ngx.say(ip:match("192.168.1.100")) 338 | ngx.say(ip:match("192.100.1.100")) 339 | ngx.say(ip:match("::1")) 340 | ngx.say(ip:match("::2")) 341 | ngx.say(ip:match("fe80::").value) 342 | ngx.say(ip:match("fe80:1::")) 343 | ngx.say(ip:match("fe81::")) 344 | } 345 | } 346 | --- request 347 | GET /t 348 | --- no_error_log 349 | [error] 350 | --- response_body 351 | 1 352 | false 353 | 2 354 | 2 355 | false 356 | 3 357 | false 358 | 4 359 | false 360 | false 361 | 362 | 363 | 364 | === TEST 11: new_with_value and zero mask 365 | --- config 366 | location /t { 367 | content_by_lua_block { 368 | local ip = require("resty.ipmatcher").new_with_value({ 369 | ["::/0"] = 1, 370 | ["0.0.0.0/0"] = 2, 371 | }) 372 | 373 | ngx.say(ip:match("127.0.0.1")) 374 | ngx.say(ip:match("0.0.0.0")) 375 | ngx.say(ip:match("fe81::")) 376 | ngx.say(ip:match("ff80::")) 377 | } 378 | } 379 | --- request 380 | GET /t 381 | --- no_error_log 382 | [error] 383 | --- response_body 384 | 2 385 | 2 386 | 1 387 | 1 388 | 389 | 390 | 391 | === TEST 12: bug: ipv4 address overrided with the same mask 392 | --- config 393 | location /t { 394 | content_by_lua_block { 395 | local ip = require("resty.ipmatcher").new({ 396 | "192.168.0.0/16", 397 | "192.0.0.0/16", 398 | }) 399 | 400 | ngx.say(ip:match("192.168.1.1")) 401 | ngx.say(ip:match("192.0.1.100")) 402 | } 403 | } 404 | --- request 405 | GET /t 406 | --- no_error_log 407 | [error] 408 | --- response_body 409 | true 410 | true 411 | 412 | 413 | 414 | === TEST 13: bug fixing: same ipv6 prefix with the same mask 415 | --- config 416 | location /t { 417 | content_by_lua_block { 418 | local ip = require("resty.ipmatcher").new({ 419 | '2409:8928:6a00::/39', 420 | '2409:8928:a000::/39', -- 2409:8928:a000:: - 2409:8928:a1ff:ffff:ffff:ffff:ffff:ffff 421 | }) 422 | 423 | ngx.say(ip:match("2409:8928:6a00:2a57:1:1:d823:4521")) 424 | ngx.say(ip:match("2409:8928:6a01::")) 425 | ngx.say(ip:match("2409:8928:a0f8:2a57:1:1:d823:4521")) 426 | ngx.say(ip:match("2409:8928:a100::")) 427 | ngx.say(ip:match("2409:8928:a200::")) 428 | } 429 | } 430 | --- request 431 | GET /t 432 | --- no_error_log 433 | [error] 434 | --- response_body 435 | true 436 | true 437 | true 438 | true 439 | false 440 | 441 | 442 | 443 | === TEST 14: accurate match with new_with_value 444 | --- config 445 | location /t { 446 | content_by_lua_block { 447 | local ip = require("resty.ipmatcher").new_with_value({ 448 | ["192.168.1.1/24"] = "level3", 449 | ["192.168.1.1/16"] = "level2", 450 | ["192.168.1.1/8"] = "level1", 451 | }) 452 | 453 | ngx.say(ip:match("192.168.1.2")) 454 | ngx.say(ip:match("192.168.2.2")) 455 | ngx.say(ip:match("192.2.2.2")) 456 | 457 | local ip = require("resty.ipmatcher").new_with_value({ 458 | ["192.168.1.1/16"] = "level2", 459 | ["192.168.1.1/8"] = "level1", 460 | ["192.168.1.1/24"] = "level3", 461 | }) 462 | 463 | ngx.say(ip:match("192.168.1.2")) 464 | ngx.say(ip:match("192.168.2.2")) 465 | ngx.say(ip:match("192.2.2.2")) 466 | 467 | local ip = require("resty.ipmatcher").new_with_value({ 468 | ["192.168.1.1/8"] = "level1", 469 | ["192.168.1.1/16"] = "level2", 470 | ["192.168.1.1/24"] = "level3", 471 | }) 472 | 473 | ngx.say(ip:match("192.168.1.2")) 474 | ngx.say(ip:match("192.168.2.2")) 475 | ngx.say(ip:match("192.2.2.2")) 476 | } 477 | } 478 | --- request 479 | GET /t 480 | --- no_error_log 481 | [error] 482 | --- response_body 483 | level3 484 | level2 485 | level1 486 | level3 487 | level2 488 | level1 489 | level3 490 | level2 491 | level1 492 | --------------------------------------------------------------------------------