├── .gitignore ├── GeoIP.mmdb ├── LICENSE ├── README.md ├── conf.toml ├── config └── conf.go ├── go.mod ├── golua ├── clone.go ├── cryto.go ├── html.go ├── loader.go ├── log.go ├── luavm.go ├── random.go ├── re.go ├── request.go ├── response.go ├── time.go └── utils.go ├── html ├── default_404.html └── etc_shadow.html ├── iploc ├── detail.go ├── indexes.go ├── ip.go ├── iploc.go ├── parser.go └── resource.go ├── juggler.go ├── logger └── printer.go ├── pics ├── juggler-1.gif ├── juggler-2.gif ├── juggler-waf.jpg └── juggler.jpg ├── qqwry-0.dat ├── scripts ├── default.lua └── juggler.test.com.lua └── web ├── kafka.go ├── logging.go ├── router.go ├── struct.go └── web.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /GeoIP.mmdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/C4o/Juggler/9a6a06276c37ff3aa5201b5d82abc3eb45fc5576/GeoIP.mmdb -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Juggler - 一个也许能骗到黑客的系统 2 | 3 | ## 应用场景 4 | 现在很多WAF拦截了恶意请求之后,直接返回一些特殊告警页面(之前有看到t00ls上有看图识WAF)或一些状态码(403或者500啥的)。 5 | 6 | 但是实际上返不返回特殊响应都不会有啥实际作用,反而会给攻击者显而易见的提示。 7 | 8 | 但是如果返回的内容跟业务返回一致的话,就能让攻击者很难察觉到已经被策略拦截了。 9 | 10 | 场景一:攻击者正在暴力破解某登陆口 11 | ``` 12 | 发现登陆成功是 13 | {"successcode":0,"result":{"ReturnCode":0}} 14 | 登陆失败是 15 | {"errorcode":1,"error":"用户名密码不匹配","result":{"ReturnCode":0}} 16 | ``` 17 | 那现在我们可以这么做 18 | ``` 19 | 1. 触发规则后持续返回错误状态码,让黑客觉得自己的字典不大行。 20 | 2. 返回一个特定的cookie,当waf匹配到该cookie后,将请求导流到某web蜜罐跟黑客深入交流。 21 | ``` 22 | 23 | 场景二:攻击者正在尝试找xss 24 | 25 | 我们可以这么做 26 | ``` 27 | 例如: 28 | 1. 不管攻击者怎么来,检测后都返回去去除了攻击者payload的请求的响应。 29 | 2. 攻击者payload是alert(xxxx),那不管系统有无漏,我们返回一个弹框xxx。 30 | (当然前提是我们能识别payload的语法是否正确,也不能把攻击者当傻子骗。) 31 | ``` 32 | 33 | 肯定有人会觉得,我们WAF强的不行,直接拦截就行,不整这些花里胡哨的,那这可以的。 34 | 35 | 但是相对于直接的拦截给攻击者告警,混淆视听,消费攻击者的精力,让攻击者怀疑自己,这样是不是更加狡猾?这也正是项目取名的由来,juggler,耍把戏的人。 36 | 37 | 当然,上面需求实现的前提,是前方有一个强有力的WAF,只有在攻击请求被检出后,攻击请求才能到达我们的拦截欺骗中心,否则一切都是扯犊子。 38 | 39 | ``` 40 | 项目思路来自我的领导们,并且简单的应用已经在线上有了很长一段时间的应用,我只是思路的实现者。 41 | 项目已在线上运行一年多,每日处理攻击请求过亿。 42 | 43 | juggler本质上是一个lua插件化的web服务器,类似openresty(大言不惭哈哈); 44 | 基于gin进行的开发,其实就是将*gin.Context以lua的userdata放入lua虚拟机,所以可以通过lua脚本进行请求处理。 45 | ``` 46 | 47 | ### 性能 48 | 49 | 跟gin进行对比,性能损失大概10%。 50 | 51 | 虽然每个请求的真实处理还是在golang中完成,但是每个请求的一些临时变量都会在lua虚拟机走一遍。 52 | 53 | gin逻辑 54 | ```go 55 | func handler(c *gin.Context) { 56 | c.String(200, "host of this request is %s", c.Request.Host) 57 | } 58 | ``` 59 | juggler逻辑 60 | ```lua 61 | local var = rock.var 62 | local resp = rock.resp 63 | 64 | resp.string(200, "host of this request is %s", var.host) 65 | ``` 66 | 67 | ### 使用方式 68 | 69 | 项目流程图 70 | ![image](pics/juggler.jpg) 71 | 72 | 示例插件 73 | 74 | ```lua 75 | -- juggler.test.com.lua 76 | -- 文件名juggler.test.com.lua 当攻击请求的业务域名是juggler.test.com时匹配该插件 77 | local var = rock.var 78 | local resp = rock.resp 79 | local crypto = require("crypto") 80 | local time = require("time") 81 | local re = require("re") 82 | local log = rock.log 83 | local ERR = rock.ERROR 84 | 85 | -- 通过var内的参数,匹配每一个攻击请求中的http参数 86 | if var.rule == "sqli" then 87 | -- 满足条件后直接返回格式化字符串,使用内置方法每次回显不同的32位随机md5值 88 | resp.string(200, "Congratulation!Password hash is %s.", crypto.randomMD5(32)) 89 | -- 在日志文件中打印日志 90 | log(ERR, "found sqli attack in %d", time.format()) 91 | return 92 | end 93 | 94 | -- 使用正则匹配某个路径,与规则匹配并用 95 | if var.rule == "xss" and re.match(var.uri, "^/admin/") then 96 | -- 设置响应体类型 97 | resp.set_header("Content-Type", "text/html; charset=utf-8") 98 | -- 添加响应头Date,内容是正常服务器产生的内容 99 | resp.set_header("Date", time.server_date()) 100 | -- 只响应状态码,不响应内容 101 | resp.status(403) 102 | return 103 | end 104 | 105 | if var.rule == "lfi_shadow" then 106 | -- 使用预存文件etc_shadow.html进行内容回显,状态码200 107 | resp.html(200, "etc_shadow") 108 | return 109 | end 110 | 111 | if var.rule == "rce" then 112 | resp.set_header("Content-Type", "text/html; charset=utf-8") 113 | -- 在响应中set_cookie 114 | resp.set_cookie("sessionid", "admin_session", 6000, "/", var.host, true, true) 115 | -- 克隆固定页面回显,缓存内容,不会每次都克隆 116 | resp.clone(200, "https://duxiaofa.baidu.com/detail?searchType=statute&from=aladdin_28231&originquery=%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8%E6%B3%95&count=79&cid=f66f830e45c0490d589f1de2fe05e942_law") 117 | return 118 | end 119 | 120 | -- 不匹配任何规则时,返回默认404内容 121 | resp.set_header("Content-Type", "text/html; charset=utf-8") 122 | resp.html(404, "default_404") 123 | return 124 | ``` 125 | 126 | ## 特点 127 | 128 | ### 插件编写灵活 129 | 130 | 插件支持lua所有常见语法,暂时不支持goto。可以调用预先注册好的变量、函数、模块。 131 | 132 | 实现个很简单的响应状态码404的页面。 133 | 134 | ```lua 135 | local var = rock.var 136 | local resp = rock.resp 137 | 138 | resp.set_header("Content-Type", "text/html; charset=utf-8") 139 | resp.string(404, "this is 404 page! your host is %s, your ip is %s.", var.host, var.addr) 140 | ``` 141 | 142 | ### 插件和响应文件被动式更新 143 | 144 | 加载时会初始化所有的插件和响应内容文件。 145 | 146 | ![image](pics/juggler-1.gif) 147 | 148 | 使用inotify来监听文件行为,实现被动式的更新,不用写那个多主动轮询的for循环。 149 | 150 | ![image](pics/juggler-2.gif) 151 | 152 | ### 丰富三方插件库可自行定义 153 | 154 | juggler中的lua插件除了lua本身的一些变量,其他的都是由golang实现后注册进lua虚拟机供lua进行调用的。 155 | 156 | 例如定义一个生成随机数的golang-lua函数 157 | 158 | ```go 159 | // L是lua虚拟机 160 | var L *lua.LState 161 | // 注册这个模块 162 | L.PreloadModule("random", luaRandom) 163 | 164 | // golang中定义的可在lua中使用的生成随机数方法 165 | var randFns = map[string]lua.LGFunction{ 166 | "rint" : rint, 167 | } 168 | 169 | func rint(L *lua.LState) int { 170 | 171 | rand.Seed(time.Now().UnixNano()) 172 | L.Push(lua.LNumber(rand.Intn(L.CheckInt(1))+1)) 173 | return 1 174 | } 175 | ``` 176 | 177 | lua使用方式 178 | 179 | ```lua 180 | local random = require("random") 181 | print(random.rint(3)) 182 | -- 每次运行会打印 0,1,2 中的一个 183 | ``` 184 | 185 | ## 已实现需求 186 | 187 | ### 每个请求可操作的变量和函数 188 | 189 | #### 1. 项目全局根变量:rock 190 | 191 | 项目所有的变量的,类型是table。 192 | 193 | #### 2. http请求:rock.var 194 | 195 | 包含了部分的当前请求的参数,具体参数见golua/request.go,已经覆盖了常见的参数了 196 | 197 | ```go 198 | case "host": 199 | L.Push(lua.LString(r.Host)) 200 | case "status": 201 | L.Push(lua.LNumber(w.Status())) 202 | case "xff": 203 | L.Push(lua.LString(r.Header.Get("x-forwarded-for"))) 204 | case "rule": 205 | L.Push(lua.LString(r.Header.Get("rule"))) 206 | case "size": 207 | L.Push(lua.LNumber(w.Size())) 208 | case "method": 209 | L.Push(lua.LString(r.Method)) 210 | case "uri": 211 | L.Push(lua.LString(r.URL.Path)) 212 | case "app": 213 | L.Push(lua.LString(r.Header.Get("x-Rock-APP"))) 214 | case "addr": 215 | L.Push(lua.LString(r.Header.Get("x-real-ip"))) 216 | case "saddr": 217 | L.Push(lua.LString(r.RemoteAddr)) 218 | case "query": 219 | L.Push(lua.LString(r.URL.RawQuery)) 220 | case "ref": 221 | L.Push(lua.LString(r.Referer())) 222 | case "ua": 223 | L.Push(lua.LString(r.UserAgent())) 224 | case "ltime": 225 | L.Push(lua.LNumber(time.Now().Unix())) 226 | default: 227 | L.Push(lua.LNil) 228 | ``` 229 | 230 | #### 3. http响应:rock.resp 231 | 232 | 处理响应,通过每个请求的*gin.Context存在userdata的值进行操作 233 | 234 | ```lua 235 | local resp = rock.resp 236 | 237 | -- 参数是状态码number类型,无返回 238 | resp.status(200) 239 | -- *gin.Context响应回显状态码 240 | 241 | -- 参数是 状态码number类型、响应体是格式化字符串string类型、任意类型,无返回 242 | resp.string(200, "return a string..%s", "xx") 243 | -- *gin.Context响应回显状态码,并返回格式化字符串 244 | 245 | -- 参数是 状态码number类型、响应体文件名是string类型、任意类型,无返回 246 | -- 第二个参数对应的文件在项目html目录下,如输入juggler_404,那么实际内容就是juggler_404.html 247 | -- 如果找不到该文件,就返回default_404.html的内容,所有内容会在第一次加载后缓存进内存 248 | resp.html(200, "juggler_404") 249 | -- *gin.Context响应回显状态码,和缓存页面内容(实际上也是格式化字符串) 250 | 251 | -- 参数是 状态码number类型,url是string类型 252 | -- 第二个参数是可以进行克隆的url,比如不方便直接存,那可以直接克隆,内容会在克隆完成一次后缓存进内存 253 | resp.clone(200, "http://juggler.test.com/uri?p=1") 254 | -- *gin.Context响应克隆出来的内容 255 | 256 | -- 参数是 头的key和头的值,都是string类型,没有返回 257 | resp.set_header("Content-Type", "text/html; charset=utf-8") 258 | -- *gin.Context设置头参数 259 | 260 | -- 参数是 cookie的key和值,都是string类型,生命周期number类型,作用路径和作用域名是string类型,secure和httponly是布尔类型,没有返回 261 | resp.set_cookie("sessionid", "admin_session", 6000, "/", var.host, true, true) 262 | -- *gin.Context设置cookie 263 | ``` 264 | 265 | ### 内置模块、函数和对应需求 266 | 267 | #### 1. 正则匹配:re 268 | 269 | 统一拦截规则可能会需要根据不同uri区分子业务来返回对应的欺骗页面 270 | 271 | re中实现缓存,所以性能优于golang原生(虽然golang中的正则匹配性能一直被诟病 272 | 273 | ```lua 274 | local var = rock.var 275 | local re = require("re") 276 | 277 | -- 参数是 待匹配字符串、正则匹配语法,返回bool类型 278 | local res = re.match(var.uri, "^/admin/") 279 | -- 输入 true或者false 280 | ``` 281 | 282 | #### 2. 时间相关:time 283 | 284 | 服务器的Date时间特定格式、使用unix时间戳计算、日志打印格式化时间 285 | 286 | ```lua 287 | local time = require("time") 288 | 289 | -- 没有参数,返回number类型 290 | local zero = time.zero 291 | -- 输出 1590829200,主要用来做差值算余数 292 | 293 | -- 没有参数,返回string类型 294 | local server_date = time.server_date() 295 | -- 输出 Mon, 06 Jul 2020 15:28:49 GMT 296 | 297 | -- 没有参数,返回string类型 298 | local format_time = time.format() 299 | -- 输出 2020-07-06 15:30:14 300 | ``` 301 | 302 | #### 3. 加密:crypto 303 | 304 | 业务上会有些接口,每次报错会返回一个随机md5,为了完全仿真,我们返回的数据的md5必然也要随机 305 | 306 | ```lua 307 | local crypto = require("crypto") 308 | 309 | -- 参数是string类型,返回string类型 310 | local md5sum = crypto.md5sum("123") 311 | -- 输出 202cb962ac59075b964b07152d234b70 312 | 313 | -- 参数是16或32,number类型,返回string类型 314 | local randomMD5 = crypto.randomMD5(16) 315 | -- 输出一个随机的对应长度的md5 316 | ``` 317 | 318 | #### 4. 随机数:random 319 | 320 | lua中使用随机数对table内容进行随机筛选,由于lua自带的随机数函数太不随机,所以自己实现 321 | 322 | ```lua 323 | local random = require("random") 324 | 325 | -- 参数是随机数范围,返回number类型 326 | local ri = random.rint(3) 327 | -- 输出 0,1,2 中的一个 328 | ``` 329 | 330 | #### 5. 日志打印:log、ERROR、DEBUG、INFO 331 | 332 | ERROR、DEBUG、INFO都是日志等级 333 | 334 | ```lua 335 | local log = rock.log 336 | local ERR = rock.ERROR 337 | 338 | -- 参数是 日志等级(number类型)、格式化字符串(string类型)、若干个填入内容(任意类型),没有返回 339 | log(ERR, "this is a err msg.") 340 | -- 日志中输出 2020/07/06 15:32:43 [error] this is a err msg. 341 | ``` 342 | 343 | ## 联动WAF使用 344 | 345 | ![image](pics/juggler-waf.jpg) 346 | 347 | ## 本项目在现实中的应用 348 | 349 | ### WAF体系 350 | 351 | 本项目为拦截图中的拦截欺骗中心,接收并处理所有恶意请求。 352 | 353 | ![image](https://p3.ssl.qhimg.com/t015b7079b7b1839010.png) 354 | 355 | ### 日志分析风控系统 356 | 357 | 项目地址:[https://github.com/C4o/FBI-Analyzer](https://github.com/C4o/FBI-Analyzer) 358 | 359 | ### 实时日志传输模块 360 | 361 | 项目地址:[https://github.com/C4o/LogFarmer](https://github.com/C4o/LogFarmer) 362 | 363 | ## 404StarLink 2.0 - Galaxy 364 | ![](https://github.com/knownsec/404StarLink-Project/raw/master/logo.png) 365 | 366 | Juggler 是 404Team [星链计划2.0](https://github.com/knownsec/404StarLink2.0-Galaxy)中的一环,如果对Juggler有任何疑问又或是想要找小伙伴交流,可以参考星链计划的加群方式。 367 | 368 | - [https://github.com/knownsec/404StarLink2.0-Galaxy#community](https://github.com/knownsec/404StarLink2.0-Galaxy#community) -------------------------------------------------------------------------------- /conf.toml: -------------------------------------------------------------------------------- 1 | ############################# 2 | ### kafka相关配置 3 | [Kafka] 4 | topic = "intercept" 5 | num = 10 6 | thread = 20 7 | addr = "192.168.1.1:9092" 8 | on = false 9 | ############################# 10 | [Other] 11 | ### 节点信息 12 | local = "127.0.0.1" 13 | ### 日志等级 14 | debug = 10 15 | ### 虚拟机个数 16 | vmnum = 5 17 | -------------------------------------------------------------------------------- /config/conf.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "os" 7 | "time" 8 | 9 | "github.com/BurntSushi/toml" 10 | ) 11 | 12 | var Cfg Config 13 | 14 | type Kafka struct { 15 | Addr string 16 | Thread int 17 | Num int 18 | Topic string 19 | // 开关,决定是否传到kafka里 20 | On bool 21 | } 22 | 23 | type Other struct { 24 | // 拦截节点地址 25 | Local string 26 | // 日志等级 27 | Debug int 28 | // lua虚拟机个数 29 | VMNum int 30 | } 31 | 32 | type Config struct { 33 | // kafka配置 34 | Kafka Kafka 35 | Other Other 36 | } 37 | 38 | func (cfg *Config) Load(ConfPath string) error { 39 | 40 | if tomlFile, err := ioutil.ReadFile(ConfPath); err == nil { 41 | *cfg = Config{} 42 | _, err := toml.Decode(string(tomlFile), &cfg) 43 | if err != nil { 44 | log.Printf("error in decode toml : %v", err) 45 | } 46 | log.Println("[info] configuration update.") 47 | return nil 48 | } else { 49 | log.Printf("[error] error in open file : %v", err) 50 | return err 51 | } 52 | } 53 | 54 | func (cfg *Config) Monitor(ConfPath string) { 55 | 56 | var last int64 57 | var f os.FileInfo 58 | var err error 59 | 60 | if f, err = os.Stat(ConfPath); err == nil { 61 | last = f.ModTime().Unix() 62 | s1 := time.NewTicker(1 * time.Second) 63 | defer func() { 64 | s1.Stop() 65 | }() 66 | for { 67 | select { 68 | case <-s1.C: 69 | if f, err = os.Stat(ConfPath); err == nil { 70 | if last != f.ModTime().Unix() { 71 | cfg.Load(ConfPath) 72 | last = f.ModTime().Unix() 73 | } 74 | } else { 75 | log.Printf("stat file error : %v", err) 76 | } 77 | } 78 | } 79 | } else { 80 | log.Printf("stat file error : %v", err) 81 | } 82 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module Juggler 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/BurntSushi/toml v1.4.0 7 | github.com/IBM/sarama v1.45.0 8 | github.com/fsnotify/fsnotify v1.8.0 9 | github.com/gin-contrib/gzip v1.2.2 10 | github.com/gin-gonic/gin v1.10.0 11 | github.com/google/btree v1.1.3 12 | github.com/json-iterator/go v1.1.12 13 | github.com/oschwald/geoip2-golang v1.11.0 14 | github.com/yuin/gopher-lua v1.1.1 15 | ) 16 | 17 | require ( 18 | github.com/bytedance/sonic v1.12.7 // indirect 19 | github.com/bytedance/sonic/loader v0.2.2 // indirect 20 | github.com/cloudwego/base64x v0.1.4 // indirect 21 | github.com/davecgh/go-spew v1.1.1 // indirect 22 | github.com/eapache/go-resiliency v1.7.0 // indirect 23 | github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect 24 | github.com/eapache/queue v1.1.0 // indirect 25 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect 26 | github.com/gin-contrib/sse v1.0.0 // indirect 27 | github.com/go-playground/locales v0.14.1 // indirect 28 | github.com/go-playground/universal-translator v0.18.1 // indirect 29 | github.com/go-playground/validator/v10 v10.24.0 // indirect 30 | github.com/goccy/go-json v0.10.4 // indirect 31 | github.com/golang/snappy v0.0.4 // indirect 32 | github.com/hashicorp/errwrap v1.0.0 // indirect 33 | github.com/hashicorp/go-multierror v1.1.1 // indirect 34 | github.com/hashicorp/go-uuid v1.0.3 // indirect 35 | github.com/jcmturner/aescts/v2 v2.0.0 // indirect 36 | github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect 37 | github.com/jcmturner/gofork v1.7.6 // indirect 38 | github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect 39 | github.com/jcmturner/rpc/v2 v2.0.3 // indirect 40 | github.com/klauspost/compress v1.17.11 // indirect 41 | github.com/klauspost/cpuid/v2 v2.2.9 // indirect 42 | github.com/kr/text v0.2.0 // indirect 43 | github.com/leodido/go-urn v1.4.0 // indirect 44 | github.com/mattn/go-isatty v0.0.20 // indirect 45 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 46 | github.com/modern-go/reflect2 v1.0.2 // indirect 47 | github.com/oschwald/maxminddb-golang v1.13.0 // indirect 48 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 49 | github.com/pierrec/lz4/v4 v4.1.22 // indirect 50 | github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect 51 | github.com/rogpeppe/go-internal v1.14.0 // indirect 52 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 53 | github.com/ugorji/go/codec v1.2.12 // indirect 54 | golang.org/x/arch v0.13.0 // indirect 55 | golang.org/x/crypto v0.32.0 // indirect 56 | golang.org/x/net v0.34.0 // indirect 57 | golang.org/x/sys v0.29.0 // indirect 58 | golang.org/x/text v0.21.0 // indirect 59 | google.golang.org/protobuf v1.36.2 // indirect 60 | gopkg.in/yaml.v3 v3.0.1 // indirect 61 | ) 62 | -------------------------------------------------------------------------------- /golua/clone.go: -------------------------------------------------------------------------------- 1 | package golua 2 | 3 | import ( 4 | "Juggler/logger" 5 | "github.com/gin-gonic/gin" 6 | lua "github.com/yuin/gopher-lua" 7 | "io/ioutil" 8 | "net/http" 9 | ) 10 | 11 | var ( 12 | cloneSites = make(map[string][]byte) 13 | client = &http.Client{} 14 | ) 15 | 16 | func clone(L *lua.LState) int { 17 | 18 | var content []byte 19 | var ok bool 20 | var url string 21 | status := L.CheckInt(1) 22 | url = L.CheckString(2) 23 | content, ok = cloneSites[url] 24 | if !ok { 25 | go goCloneSite(url) 26 | content = LuaPool.Htmls["default"] 27 | } 28 | L.Context().(*gin.Context).String(status, string(content)) 29 | return 0 30 | } 31 | 32 | func goCloneSite(url string) { 33 | 34 | var err error 35 | var resp *http.Response 36 | var body []byte 37 | req, err := http.NewRequest("GET", url, nil) //建立一个请求 38 | if err != nil { 39 | logger.Printer(logger.ERROR, "cannot new http request to %s , error is %v", url, err) 40 | return 41 | } 42 | req.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") 43 | req.Header.Add("Accept-Language", "ja,zh-CN;q=0.8,zh;q=0.6") 44 | req.Header.Add("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:12.0) Gecko/20100101 Firefox/12.0") 45 | resp, err = client.Do(req) //提交 46 | defer resp.Body.Close() 47 | if err != nil { 48 | logger.Printer(logger.ERROR, "cannot send http request to %s , error is %v", url, err) 49 | return 50 | } 51 | 52 | body, err = ioutil.ReadAll(resp.Body) 53 | if err != nil { 54 | logger.Printer(logger.ERROR, "cannot read http response of %s , error is %v", url, err) 55 | return 56 | } 57 | cloneSites[url] = body 58 | } 59 | -------------------------------------------------------------------------------- /golua/cryto.go: -------------------------------------------------------------------------------- 1 | package golua 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | "fmt" 7 | "time" 8 | 9 | lua "github.com/yuin/gopher-lua" 10 | ) 11 | 12 | var cryptoFns = map[string]lua.LGFunction{ 13 | "md5sum": md5sum, 14 | "randomMD5": randomMD5, 15 | "b64encode": b64encode, 16 | "b64decode": b64decode, 17 | } 18 | 19 | func md5sum(L *lua.LState) int { 20 | 21 | h := md5.New() 22 | h.Write([]byte(fmt.Sprintf("%+v", L.CheckAny(1)))) 23 | L.Push(lua.LString(hex.EncodeToString(h.Sum(nil)))) 24 | return 1 25 | } 26 | 27 | func randomMD5(L *lua.LState) int { 28 | 29 | h := md5.New() 30 | h.Write([]byte(fmt.Sprintf("x%d", time.Now().Nanosecond()))) 31 | if L.CheckInt(1) == 16 { 32 | L.Push(lua.LString(hex.EncodeToString(h.Sum(nil))[8:24])) 33 | } else { 34 | L.Push(lua.LString(hex.EncodeToString(h.Sum(nil)))) 35 | } 36 | 37 | return 1 38 | } 39 | 40 | func b64encode(L *lua.LState) int { 41 | 42 | return 0 43 | } 44 | 45 | func b64decode(L *lua.LState) int { 46 | 47 | return 0 48 | } -------------------------------------------------------------------------------- /golua/html.go: -------------------------------------------------------------------------------- 1 | package golua 2 | 3 | import ( 4 | "Juggler/logger" 5 | "github.com/fsnotify/fsnotify" 6 | "github.com/gin-gonic/gin" 7 | "io/ioutil" 8 | 9 | lua "github.com/yuin/gopher-lua" 10 | ) 11 | 12 | func (pl *LStatePool) LoadHtml(path string) error { 13 | 14 | if path == "" { 15 | path = "html/" 16 | } else { 17 | pl.HPath = path 18 | } 19 | // 初始加载自定义相应内容 20 | fileList, err := ioutil.ReadDir(pl.HPath) 21 | if err != nil { 22 | logger.Printer(logger.ERROR, "load htmls in %s error : %v", pl.HPath, err) 23 | return err 24 | } 25 | for _, fileInfo := range fileList { 26 | name := fileInfo.Name() 27 | LuaPool.Htmls[name[:len(name)-5]], err = ioutil.ReadFile(pl.HPath+name) 28 | if err == nil { 29 | logger.Printer(logger.INFO, "update html file %s successfully.", name) 30 | } else { 31 | delete(LuaPool.Htmls, name[:len(name)-5]) 32 | logger.Printer(logger.ERROR, "read html file %s error : %v", name, err) 33 | } 34 | } 35 | go LuaPool.MonitorHtml() 36 | return nil 37 | } 38 | 39 | func (pl *LStatePool) MonitorHtml() { 40 | 41 | var watcher *fsnotify.Watcher 42 | var event fsnotify.Event 43 | var err error 44 | // 检测插件文件是否变化 45 | watcher, err = fsnotify.NewWatcher() 46 | if err != nil { 47 | logger.Printer(logger.ERROR, "new inotify watcher error: %v", err) 48 | } 49 | defer watcher.Close() 50 | watcher.Add(pl.HPath) 51 | for { 52 | select { 53 | case event =<- watcher.Events: 54 | if event.Op&fsnotify.Write == fsnotify.Write { 55 | if event.Name[len(event.Name)-5:] == ".html" { 56 | LuaPool.Htmls[event.Name[len(pl.HPath):len(event.Name)-5]], err = ioutil.ReadFile(event.Name) 57 | if err == nil { 58 | logger.Printer(logger.INFO, "update html file %s successfully.", event.Name) 59 | } else { 60 | delete(LuaPool.Htmls, event.Name[len(pl.HPath):len(event.Name)-5]) 61 | logger.Printer(logger.ERROR, "read html file %s error : %v", event.Name, err) 62 | } 63 | } else { 64 | logger.Printer(logger.ERROR, "%s is not end with .html!", event.Name) 65 | } 66 | } 67 | if event.Op&fsnotify.Remove == fsnotify.Remove { 68 | delete(LuaPool.Htmls, event.Name[len(pl.HPath):len(event.Name)-5]) 69 | logger.Printer(logger.INFO, "delete html file %s", event.Name) 70 | } 71 | } 72 | } 73 | } 74 | 75 | func getHtml(L *lua.LState) int { 76 | 77 | var html []byte 78 | var ok bool 79 | key := L.CheckString(2) 80 | if html, ok = LuaPool.Htmls[key]; !ok { 81 | logger.Printer(logger.ERROR, "cannot load file %s.", key) 82 | html = LuaPool.Htmls["default_404"] 83 | } 84 | n := L.GetTop() 85 | buf := make([]interface{}, n-2) 86 | for i := 3; i < n+1; i++ { 87 | buf[i-3] = L.CheckAny(i) 88 | } 89 | L.Context().(*gin.Context).String(L.CheckInt(1), string(html), buf...) 90 | return 0 91 | } -------------------------------------------------------------------------------- /golua/loader.go: -------------------------------------------------------------------------------- 1 | package golua 2 | 3 | import ( 4 | "Juggler/logger" 5 | 6 | "github.com/gin-gonic/gin" 7 | lua "github.com/yuin/gopher-lua" 8 | ) 9 | 10 | // 注册Request到lua虚拟机 11 | func registerRock(L *lua.LState) { 12 | 13 | var rmt = L.NewTypeMetatable("req") 14 | var hmt = L.NewTypeMetatable("html") 15 | var reqTb, ginTb, htmlTb = &lua.LTable{}, &lua.LTable{}, &lua.LTable{} 16 | // 设置req的表和元表 17 | L.SetFuncs(rmt, map[string]lua.LGFunction{"__index": getReqVar}) 18 | reqTb.Metatable = rmt 19 | ginTb.RawSet(lua.LString("var"), reqTb) 20 | // 设置logging方法和日志等级 21 | ginTb.RawSet(lua.LString("log"), L.NewFunction(logging)) 22 | ginTb.RawSet(lua.LString("ERROR"), lua.LNumber(logger.ERROR)) 23 | ginTb.RawSet(lua.LString("DEBUG"), lua.LNumber(logger.DEBUG)) 24 | ginTb.RawSet(lua.LString("INFO"), lua.LNumber(logger.INFO)) 25 | // 设置resp方法 26 | ginTb.RawSet(lua.LString("resp"), L.SetFuncs(L.NewTable(), respFns)) 27 | // 设置req方法 28 | ginTb.RawSet(lua.LString("req"), L.SetFuncs(L.NewTable(), reqFns)) 29 | htmlTb.Metatable = hmt 30 | ginTb.RawSet(lua.LString("html"), htmlTb) 31 | // 设置全局顶级变量table 32 | L.SetGlobal("rock", ginTb) 33 | } 34 | 35 | // 注册gin.context的userdata区域并填充日志数据 36 | func registerGinContextUserData(L *lua.LState, c *gin.Context) { 37 | // 直接回传*gin.Context,类型就是context 38 | L.SetContext(c) 39 | } 40 | 41 | // 注册re相关方法到lua虚拟机 42 | func luaRe(L *lua.LState) int { 43 | // 注册方法 44 | mod := L.SetFuncs(L.NewTable(), reFns) 45 | L.Push(mod) 46 | return 1 47 | } 48 | 49 | // 注册time相关方法到lua虚拟机 50 | func luaTime(L *lua.LState) int { 51 | // 注册方法 52 | mod := L.SetFuncs(L.NewTable(), timeFns) 53 | mod.RawSet(lua.LString("zero"), lua.LNumber(1590829200)) 54 | L.Push(mod) 55 | return 1 56 | } 57 | 58 | // 注册加密相关方法 59 | func luaCrypto(L *lua.LState) int { 60 | // 注册方法 61 | mod := L.SetFuncs(L.NewTable(), cryptoFns) 62 | L.Push(mod) 63 | return 1 64 | } 65 | 66 | // 注册随机数方法 67 | func luaRandom(L *lua.LState) int { 68 | mod := L.SetFuncs(L.NewTable(), randFns) 69 | L.Push(mod) 70 | return 1 71 | } 72 | -------------------------------------------------------------------------------- /golua/log.go: -------------------------------------------------------------------------------- 1 | package golua 2 | 3 | import ( 4 | "Juggler/logger" 5 | "bytes" 6 | 7 | lua "github.com/yuin/gopher-lua" 8 | ) 9 | 10 | func logging(L *lua.LState) int { 11 | 12 | buf := new(bytes.Buffer) 13 | n := L.GetTop() 14 | 15 | for i := 2; i < n+1; i++ { 16 | buf.WriteString(L.CheckString(i)) 17 | buf.WriteString(" ")} 18 | 19 | logger.Printer(L.CheckInt(1), buf.String()) 20 | return 0 21 | } 22 | -------------------------------------------------------------------------------- /golua/luavm.go: -------------------------------------------------------------------------------- 1 | package golua 2 | 3 | import ( 4 | "Juggler/config" 5 | "Juggler/logger" 6 | "bufio" 7 | "errors" 8 | "io/ioutil" 9 | "os" 10 | 11 | "github.com/fsnotify/fsnotify" 12 | "github.com/gin-gonic/gin" 13 | lua "github.com/yuin/gopher-lua" 14 | "github.com/yuin/gopher-lua/ast" 15 | "github.com/yuin/gopher-lua/parse" 16 | ) 17 | 18 | var ( 19 | LuaPool *LStatePool 20 | workerVM = 1 21 | functionVM = 0 22 | ) 23 | 24 | // lua虚拟机池 25 | type LStatePool struct { 26 | HPath string 27 | Htmls map[string][]byte 28 | FnVM *lua.LState 29 | Luas string 30 | Fns map[string]*lua.LFunction 31 | VMs chan *lua.LState 32 | } 33 | 34 | // 拿走channel中的虚拟机 35 | func (pl *LStatePool) get() *lua.LState { 36 | n := len(pl.VMs) 37 | if n == 0 { 38 | return pl.new(workerVM) 39 | } 40 | x := <- pl.VMs 41 | return x 42 | } 43 | 44 | // 把用完的虚拟机放回channel 45 | func (pl *LStatePool) put(L *lua.LState) { 46 | pl.VMs <- L 47 | } 48 | 49 | // 新建虚拟机,注册若干预设定变量和模块 50 | func (pl *LStatePool) new(vmType int) *lua.LState { 51 | 52 | switch vmType { 53 | case functionVM: 54 | logger.Printer(logger.INFO, "fnVM new state") 55 | case workerVM: 56 | logger.Printer(logger.INFO, "workerVM new state") 57 | } 58 | L := lua.NewState() 59 | registerRock(L) 60 | L.PreloadModule("re", luaRe) 61 | L.PreloadModule("time", luaTime) 62 | L.PreloadModule("crypto", luaCrypto) 63 | L.PreloadModule("random", luaRandom) 64 | return L 65 | } 66 | 67 | // 结束后关闭所有channel中的虚拟机 68 | func (pl *LStatePool) Shutdown() { 69 | for L := range pl.VMs { 70 | L.Close() 71 | } 72 | } 73 | 74 | // 初始化若干个虚拟机放入channel待用 75 | func (pl *LStatePool) Init(s string) error { 76 | 77 | if s == "" { 78 | LuaPool.Luas = "scripts/" 79 | } else { 80 | if s[len(s)-1:] != "/" { 81 | logger.Printer(logger.ERROR, "%s plugins path must be end with '/' !", s) 82 | return errors.New("plugins path error.") 83 | } 84 | LuaPool.Luas = s 85 | } 86 | logger.Printer(logger.INFO, "vmnum is %d, init of struct...", config.Cfg.Other.VMNum) 87 | pl.FnVM = pl.new(functionVM) 88 | go pl.loadPlugins() 89 | for i := 0; i < config.Cfg.Other.VMNum; i++ { 90 | pl.VMs <- pl.new(workerVM) 91 | } 92 | return nil 93 | } 94 | 95 | // 获取方法供使用 96 | func (pl *LStatePool) getFn(host string) *lua.LFunction { 97 | 98 | if fn, ok := pl.Fns[host]; ok { 99 | return fn 100 | } 101 | return pl.Fns["default"] 102 | } 103 | 104 | // 加载和规则 105 | func (pl *LStatePool) loadPlugins() { 106 | 107 | var watcher *fsnotify.Watcher 108 | var event fsnotify.Event 109 | 110 | // 初始加载插件 111 | fileList, err := ioutil.ReadDir(pl.Luas) 112 | if err != nil { 113 | logger.Printer(logger.ERROR, "plugin path %s read error : %v", pl.Luas, err) 114 | os.Exit(1) 115 | } 116 | for _, fileInfo := range fileList { 117 | name := fileInfo.Name() 118 | if name[len(name)-4:] == ".lua" { 119 | pl.compileLua(pl.Luas+name,name[:len(name)-4]) 120 | } else { 121 | logger.Printer(logger.ERROR, "%s is not end with .lua!", name) 122 | } 123 | } 124 | // 检测插件文件是否变化 125 | watcher, err = fsnotify.NewWatcher() 126 | if err != nil { 127 | logger.Printer(logger.ERROR, "new inotify watcher error: %v", err) 128 | } 129 | defer watcher.Close() 130 | watcher.Add(pl.Luas) 131 | for { 132 | select { 133 | case event =<- watcher.Events: 134 | if event.Op&fsnotify.Write == fsnotify.Write { 135 | if event.Name[len(event.Name)-4:] == ".lua" { 136 | pl.compileLua(event.Name, event.Name[len(pl.Luas):len(event.Name)-4]) 137 | } else { 138 | logger.Printer(logger.ERROR, "%s is not end with .lua!", event.Name) 139 | } 140 | } 141 | if event.Op&fsnotify.Remove == fsnotify.Remove { 142 | delete(pl.Fns, event.Name[len(pl.Luas):len(event.Name)-4]) 143 | logger.Printer(logger.INFO, "delete plugins %s", event.Name) 144 | } 145 | } 146 | } 147 | } 148 | 149 | // 编译lua脚本并缓存方法 150 | func (pl *LStatePool) compileLua(filepath, host string) { 151 | 152 | var err error 153 | var file *os.File 154 | var chunk []ast.Stmt 155 | var proto *lua.FunctionProto 156 | 157 | file, err = os.OpenFile(filepath, os.O_RDONLY, 0444) 158 | if err != nil { 159 | logger.Printer(logger.ERROR, "script %s not found! error is %v.", filepath, err) 160 | return 161 | } 162 | defer file.Close() 163 | chunk, err = parse.Parse(bufio.NewReader(file), filepath) 164 | if err != nil { 165 | logger.Printer(logger.ERROR, "parse script %s failed for %v.", filepath, err) 166 | return 167 | } 168 | proto, err = lua.Compile(chunk, filepath) 169 | if err != nil { 170 | logger.Printer(logger.ERROR, "compile script %s failed for %v.", filepath, err) 171 | return 172 | } 173 | pl.Fns[host] = pl.FnVM.NewFunctionFromProto(proto) 174 | logger.Printer(logger.INFO, "update plugins %s successfull", host) 175 | return 176 | } 177 | 178 | // 每个请求的worker 179 | func LuaWorker(c *gin.Context) { 180 | 181 | var err error 182 | L := LuaPool.get() 183 | defer LuaPool.put(L) 184 | // 每次将gin.context存入userdata待使用 185 | registerGinContextUserData(L, c) 186 | L.Push(LuaPool.getFn(c.Request.Host)) 187 | err = L.PCall(0, lua.MultRet, nil) 188 | if err != nil { 189 | logger.Printer(logger.ERROR, "%v", err) 190 | } 191 | } -------------------------------------------------------------------------------- /golua/random.go: -------------------------------------------------------------------------------- 1 | package golua 2 | 3 | import ( 4 | lua "github.com/yuin/gopher-lua" 5 | "math/rand" 6 | "time" 7 | ) 8 | 9 | var randFns = map[string]lua.LGFunction{ 10 | "rint" : rint, 11 | } 12 | 13 | func rint(L *lua.LState) int { 14 | 15 | rand.Seed(time.Now().UnixNano()) 16 | L.Push(lua.LNumber(rand.Intn(L.CheckInt(1))+1)) 17 | return 1 18 | } -------------------------------------------------------------------------------- /golua/re.go: -------------------------------------------------------------------------------- 1 | package golua 2 | 3 | import ( 4 | "regexp" 5 | 6 | "github.com/yuin/gopher-lua" 7 | ) 8 | 9 | var reFns = map[string]lua.LGFunction{ 10 | "find": find, 11 | "match": match, 12 | } 13 | 14 | var cacheRec = make(map[string]*regexp.Regexp) 15 | 16 | // 缓存re 17 | func compile(repr string) (rec *regexp.Regexp, err error) { 18 | 19 | rec, err = regexp.Compile(repr) 20 | cacheRec[repr] = rec 21 | return rec, err 22 | } 23 | 24 | // 匹配结果返回 string, error 25 | func find(L *lua.LState) int { 26 | 27 | var err error 28 | var rec *regexp.Regexp 29 | var ok bool 30 | str := L.CheckString(1) 31 | repr := L.CheckString(2) 32 | if rec, ok = cacheRec[repr]; ok { 33 | L.Push(lua.LString(rec.FindString(str))) 34 | L.Push(lua.LNil) 35 | return 2 36 | } 37 | rec, err = compile(repr) 38 | if err != nil { 39 | L.Push(lua.LString("")) 40 | pushErr(L, err) 41 | } else { 42 | L.Push(lua.LString(rec.FindString(str))) 43 | L.Push(lua.LNil) 44 | } 45 | return 2 46 | } 47 | 48 | // 匹配结果返回 bool, error 49 | func match(L *lua.LState) int { 50 | 51 | var err error 52 | var rec *regexp.Regexp 53 | var ok bool 54 | str := L.CheckString(1) 55 | repr := L.CheckString(2) 56 | if rec, ok = cacheRec[repr]; ok { 57 | L.Push(lua.LBool(rec.MatchString(str))) 58 | L.Push(lua.LNil) 59 | return 2 60 | } 61 | rec, err = compile(repr) 62 | if err != nil { 63 | L.Push(lua.LBool(false)) 64 | pushErr(L, err) 65 | } else { 66 | L.Push(lua.LBool(rec.MatchString(str))) 67 | L.Push(lua.LNil) 68 | } 69 | return 2 70 | } 71 | 72 | -------------------------------------------------------------------------------- /golua/request.go: -------------------------------------------------------------------------------- 1 | package golua 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/gin-gonic/gin" 7 | lua "github.com/yuin/gopher-lua" 8 | ) 9 | 10 | var reqFns = map[string]lua.LGFunction{ 11 | 12 | } 13 | 14 | func getHeader(L *lua.LState) int { 15 | 16 | L.Push(lua.LString(L.Context().(*gin.Context).GetHeader(L.CheckString(1)))) 17 | return 1 18 | } 19 | 20 | func getReqVar(L *lua.LState) int { 21 | 22 | access, _ := L.Context().(*gin.Context) 23 | //logger.Printer(logger.ERROR, "ok is : %v", ok) 24 | r := access.Request 25 | w := access.Writer 26 | _ = L.CheckAny(1) 27 | switch L.CheckString(2) { 28 | case "host": 29 | L.Push(lua.LString(r.Host)) 30 | case "status": 31 | L.Push(lua.LNumber(w.Status())) 32 | case "xff": 33 | L.Push(lua.LString(r.Header.Get("x-forwarded-for"))) 34 | case "rule": 35 | L.Push(lua.LString(r.Header.Get("rule"))) 36 | case "size": 37 | L.Push(lua.LNumber(w.Size())) 38 | case "method": 39 | L.Push(lua.LString(r.Method)) 40 | case "uri": 41 | L.Push(lua.LString(r.URL.Path)) 42 | case "app": 43 | L.Push(lua.LString(r.Header.Get("x-Rock-APP"))) 44 | case "addr": 45 | L.Push(lua.LString(r.Header.Get("x-real-ip"))) 46 | case "saddr": 47 | L.Push(lua.LString(r.RemoteAddr)) 48 | case "query": 49 | L.Push(lua.LString(r.URL.RawQuery)) 50 | case "ref": 51 | L.Push(lua.LString(r.Referer())) 52 | case "ua": 53 | L.Push(lua.LString(r.UserAgent())) 54 | case "ltime": 55 | L.Push(lua.LNumber(time.Now().Unix())) 56 | default: 57 | L.Push(lua.LNil) 58 | } 59 | return 1 60 | } 61 | -------------------------------------------------------------------------------- /golua/response.go: -------------------------------------------------------------------------------- 1 | package golua 2 | 3 | import ( 4 | "Juggler/logger" 5 | 6 | "github.com/gin-gonic/gin" 7 | lua "github.com/yuin/gopher-lua" 8 | ) 9 | 10 | var respFns = map[string]lua.LGFunction{ 11 | "string": respString, 12 | "status": respStatus, 13 | "html": getHtml, 14 | "clone": clone, 15 | "set_header": respSetHeader, 16 | "set_cookie": respSetCookie, 17 | } 18 | 19 | func respString(L *lua.LState) int { 20 | 21 | status := L.CheckInt(1) 22 | format := L.CheckString(2) 23 | n := L.GetTop() 24 | buf := make([]interface{}, n-2) 25 | for i := 3; i < n+1; i++ { 26 | buf[i-3] = L.CheckAny(i) 27 | } 28 | L.Context().(*gin.Context).String(status, format, buf...) 29 | return 0 30 | } 31 | 32 | func respStatus(L *lua.LState) int { 33 | 34 | logger.Printer(logger.ERROR, "%d", L.GetTop()) 35 | L.Context().(*gin.Context).Status(L.CheckInt(1)) 36 | return 0 37 | } 38 | 39 | func respSetHeader(L *lua.LState) int { 40 | 41 | L.Context().(*gin.Context).Header(L.CheckString(1), L.CheckString(2)) 42 | return 0 43 | } 44 | 45 | func respSetCookie(L *lua.LState) int { 46 | 47 | L.Context().(*gin.Context).SetCookie( 48 | L.CheckString(1), L.CheckString(2), L.CheckInt(3), L.CheckString(4), 49 | L.CheckString(5), L.CheckBool(6), L.CheckBool(7)) 50 | return 0 51 | } 52 | -------------------------------------------------------------------------------- /golua/time.go: -------------------------------------------------------------------------------- 1 | package golua 2 | 3 | import ( 4 | "time" 5 | 6 | lua "github.com/yuin/gopher-lua" 7 | ) 8 | 9 | var timeFns = map[string]lua.LGFunction{ 10 | "unix": tunix, 11 | "format": tformat, 12 | "server_date": server_date, 13 | } 14 | 15 | func tunix(L *lua.LState) int { 16 | 17 | L.Push(lua.LNumber(time.Now().Unix())) 18 | return 1 19 | } 20 | 21 | func tformat(L *lua.LState) int { 22 | 23 | L.Push(lua.LString(time.Now().Format("2006-01-02 15:04:05"))) 24 | return 1 25 | } 26 | 27 | func server_date(L *lua.LState) int { 28 | 29 | L.Push(lua.LString(time.Now().Format("Mon, 02 Jan 2006 15:04:05 GMT"))) 30 | return 1 31 | } 32 | 33 | -------------------------------------------------------------------------------- /golua/utils.go: -------------------------------------------------------------------------------- 1 | package golua 2 | 3 | import ( 4 | jsoniter "github.com/json-iterator/go" 5 | lua "github.com/yuin/gopher-lua" 6 | ) 7 | 8 | var json = jsoniter.ConfigCompatibleWithStandardLibrary 9 | 10 | func pushErr(L *lua.LState, err error) { 11 | 12 | if err == nil { 13 | L.Push(lua.LNil) 14 | } else { 15 | L.Push(lua.LString(err.Error())) 16 | } 17 | } 18 | 19 | func typeCheck(it interface{}) lua.LValue { 20 | 21 | var ok bool 22 | if it == nil { 23 | return lua.LNil 24 | } else if _, ok = it.(string); ok { 25 | return lua.LString(it.(string)) 26 | } else if _, ok = it.(int); ok { 27 | return lua.LNumber(it.(int)) 28 | } else { 29 | return lua.LNumber(1) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /html/default_404.html: -------------------------------------------------------------------------------- 1 | This is a default page. -------------------------------------------------------------------------------- /html/etc_shadow.html: -------------------------------------------------------------------------------- 1 | root:$6$kcgcu794R0VP3fDL$aYN8XUbtWvZ4QQtT2xVW.N2CgE3YLPdtnprAAtKZUgNdq8itUJEN6NoYQDarLUevcDCWrxMVId8b18ujwST1b0::0:99999:7::: 2 | bin:*:17632:0:99999:7::: 3 | daemon:*:17632:0:99999:7::: -------------------------------------------------------------------------------- /iploc/detail.go: -------------------------------------------------------------------------------- 1 | package iploc 2 | 3 | var provincePrefix = map[int32]map[int32]int{'北': {'京': 0}, '天': {'津': 0}, '河': {'北': 0, '南': 0}, '山': {'东': 0, '西': 0}, '内': {'蒙': 1}, '辽': {'宁': 0}, '吉': {'宁': 0}, '黑': {'龙': 1}, '上': {'海': 0}, '江': {'苏': 0, '西': 0}, '浙': {'江': 0}, '安': {'徽': 0}, '福': {'福': 0}, '湖': {'南': 0, '北': 0}, '广': {'东': 0, '西': 0}, '海': {'南': 0}, '重': {'庆': 0}, '四': {'川': 0}, '贵': {'州': 0}, '云': {'南': 0}, '西': {'藏': 0}, '陕': {'西': 0}, '甘': {'肃': 0}, '青': {'海': 0}, '宁': {'夏': 0}, '新': {'疆': 0}, '香': {'港': 0}, '澳': {'门': 0}, '台': {'湾': 0}} 4 | var adEndSuffix = [2]map[int32]int{{'市': 1, '州': 1, '区': 1, '盟': 1}, {'县': 2, '市': 2, '旗': 2}} 5 | 6 | type Detail struct { 7 | IP IP 8 | Start IP 9 | End IP 10 | Location 11 | } 12 | 13 | func (detail Detail) String() string { 14 | return detail.Location.String() 15 | } 16 | 17 | func (detail Detail) Bytes() []byte { 18 | return detail.Location.Bytes() 19 | } 20 | 21 | func (detail Detail) InIP(ip IP) bool { 22 | return detail.Start.Compare(ip) < 1 && detail.End.Compare(ip) > -1 23 | } 24 | 25 | func (detail Detail) In(rawIP string) bool { 26 | ip, err := ParseIP(rawIP) 27 | if err != nil { 28 | return false 29 | } 30 | return detail.InIP(ip) 31 | } 32 | 33 | func (detail Detail) InUint(uintIP uint32) bool { 34 | return detail.InIP(ParseUintIP(uintIP)) 35 | } 36 | 37 | func (detail *Detail) fill() *Detail { 38 | if detail.Region == "N/A" { 39 | return detail 40 | } 41 | 42 | var ( 43 | rs = []rune(detail.Country) 44 | s [2][]rune 45 | p map[int32]int 46 | ok bool 47 | i, n int 48 | ) 49 | 50 | if p, ok = provincePrefix[rs[0]]; ok { 51 | i, ok = p[rs[1]] 52 | i += 2 53 | } 54 | if !ok { 55 | return detail 56 | } 57 | 58 | detail.Country = "中国" 59 | detail.Province = string(rs[:i]) 60 | 61 | if i >= len(rs) { 62 | if rs[0] == '北' || rs[0] == '天' || rs[0] == '上' || rs[0] == '重' { 63 | detail.City = string(rs[:i-1]) 64 | } 65 | return detail 66 | } 67 | 68 | if rs[i] == '市' { 69 | i++ 70 | detail.City = string(rs[:i]) 71 | } else if rs[i] == '省' { 72 | i++ 73 | } 74 | 75 | for ; i < len(rs); i++ { 76 | s[n] = append(s[n], rs[i]) 77 | if _, ok = adEndSuffix[n][rs[i]]; ok { 78 | if rs[i] != '市' && i+1 < len(rs) && rs[i+1] == '市' { 79 | continue 80 | } 81 | n++ 82 | } 83 | if n > 1 { 84 | break 85 | } 86 | } 87 | 88 | if detail.City != "" { 89 | detail.County = string(s[0]) 90 | } else { 91 | detail.City = string(s[0]) 92 | detail.County = string(s[1]) 93 | } 94 | 95 | return detail 96 | } 97 | 98 | type Location struct { 99 | Country string 100 | Region string 101 | Province string 102 | City string 103 | County string 104 | raw string 105 | } 106 | 107 | func (location Location) String() string { 108 | return location.raw 109 | } 110 | 111 | func (location Location) Bytes() []byte { 112 | return []byte(location.raw) 113 | } 114 | 115 | func parseLocation(country, region []byte) Location { 116 | location := Location{ 117 | Country: string(country), 118 | Region: string(region), 119 | } 120 | location.raw = location.Country 121 | if region != nil { 122 | location.raw += " " + location.Region 123 | } 124 | return location 125 | } 126 | -------------------------------------------------------------------------------- /iploc/indexes.go: -------------------------------------------------------------------------------- 1 | package iploc 2 | 3 | import ( 4 | "encoding/gob" 5 | 6 | "github.com/google/btree" 7 | ) 8 | 9 | type dataItem = btree.Item 10 | 11 | // 索引时 12 | // 0: ip start 13 | // 1: ip end 14 | // 2: country position 15 | // 3: region position 16 | // 无索引时 17 | // 2: ip end position 18 | // 3: n/a 19 | type indexItem [4]uint32 20 | 21 | func (i indexItem) Less(than btree.Item) bool { 22 | // 默认降序 1:End < 0:Start 23 | if v, ok := than.(indexItem); ok { 24 | return i[1] < v[0] 25 | 26 | } 27 | return i[1] < than.(indexItemAscend)[0] 28 | } 29 | 30 | // indexItem 升序 31 | type indexItemAscend [4]uint32 32 | 33 | func (i indexItemAscend) Less(than btree.Item) bool { 34 | // 升序 0:End < 0:Start 35 | if v, ok := than.(indexItem); ok { 36 | return i[0] < v[0] 37 | 38 | } 39 | return i[0] < than.(indexItemAscend)[0] 40 | } 41 | 42 | type indexes struct { 43 | index *btree.BTree 44 | indexMid indexItem 45 | locations map[uint32][]byte 46 | } 47 | 48 | func (idx *indexes) indexOf(u uint32) (hit indexItem) { 49 | // 对比中间值决定高低顺序,提升查询速度 50 | if u > idx.indexMid[1] { 51 | idx.index.DescendLessOrEqual(indexItem{0, u}, func(i btree.Item) bool { 52 | hit = i.(indexItem) 53 | return false 54 | }) 55 | } else if u < idx.indexMid[0] { 56 | idx.index.AscendGreaterOrEqual(indexItemAscend{u}, func(i btree.Item) bool { 57 | hit = i.(indexItem) 58 | return false 59 | }) 60 | } else { 61 | hit = idx.indexMid 62 | } 63 | return 64 | } 65 | 66 | func (idx *indexes) getLocation(i, j uint32) Location { 67 | return parseLocation(idx.locations[i], idx.locations[j]) 68 | } 69 | 70 | func newIndexes(p *Parser) *indexes { 71 | idx := &indexes{ 72 | index: btree.New(10), 73 | } 74 | idx.locations = make(map[uint32][]byte) 75 | 76 | var ( 77 | item indexItem 78 | raw LocationRaw 79 | mid = int(p.Count()) >> 1 80 | has bool 81 | ) 82 | 83 | p.IndexRange(func(i int, start, end, pos uint32) bool { 84 | item = indexItem{start, end, pos} 85 | raw = p.ReadLocationRaw(int64(pos)) 86 | if raw.Text[0] != nil { 87 | if _, has = idx.locations[raw.Pos[0]]; !has { 88 | idx.locations[raw.Pos[0]] = raw.Text[0] 89 | } 90 | } 91 | if raw.Text[1] != nil { 92 | if _, has = idx.locations[raw.Pos[1]]; !has { 93 | idx.locations[raw.Pos[1]] = raw.Text[1] 94 | } 95 | } 96 | item[2] = raw.Pos[0] 97 | item[3] = raw.Pos[1] 98 | if i == mid { 99 | idx.indexMid = item 100 | } 101 | idx.index.ReplaceOrInsert(item) 102 | return true 103 | }) 104 | return idx 105 | } 106 | 107 | func init() { 108 | gob.Register([][4]uint32{}) 109 | gob.Register(map[uint32][]byte{}) 110 | } 111 | -------------------------------------------------------------------------------- /iploc/ip.go: -------------------------------------------------------------------------------- 1 | package iploc 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | const ( 12 | IPLen = 4 13 | ) 14 | 15 | type IP [IPLen]byte 16 | 17 | func (ip IP) Bytes() []byte { 18 | return ip[:] 19 | } 20 | 21 | func (ip IP) ReverseBytes() []byte { 22 | var b [4]byte 23 | copy(b[:], ip[:]) 24 | for i, j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1 { 25 | b[i], b[j] = b[j], b[i] 26 | } 27 | return b[:] 28 | } 29 | 30 | func (ip IP) String() string { 31 | return fmt.Sprintf("%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]) 32 | } 33 | 34 | func (ip IP) Uint() uint32 { 35 | return binary.BigEndian.Uint32(ip[:]) 36 | } 37 | 38 | // Compare like bytes.Compare 39 | // The result will be 0 if == a, -1 if < a, and +1 if > a. 40 | func (ip IP) Compare(a IP) int { 41 | return bytes.Compare(ip[:], a[:]) 42 | } 43 | 44 | func ParseIP(s string) (ip IP, err error) { 45 | var b = make([]byte, 0, IPLen) 46 | var d uint64 47 | for i, v := range strings.Split(s, ".") { 48 | if d, err = strconv.ParseUint(v, 10, 8); err != nil { 49 | err = fmt.Errorf("invalid IP address %s", s) 50 | return 51 | } 52 | b = append(b, byte(d)) 53 | if i == 3 { 54 | break 55 | } 56 | } 57 | if len(b) == 0 { 58 | err = fmt.Errorf("invalid IP address %s", s) 59 | return 60 | } 61 | 62 | // filling,e.g. 127.1 -> 127.0.0.1 63 | // copy to array, right padding 64 | copy(ip[:], b) 65 | if padding := IPLen - len(b); padding > 0 { 66 | // left padding 67 | if lastIndex := len(b) - 1; b[lastIndex] > 0 { 68 | ip[lastIndex], ip[3] = 0, ip[lastIndex] 69 | } 70 | } 71 | return 72 | } 73 | 74 | func ParseBytesIP(b []byte) (ip IP) { 75 | copy(ip[:], b) 76 | return 77 | } 78 | 79 | func ParseUintIP(u uint32) (ip IP) { 80 | binary.BigEndian.PutUint32(ip[:], u) 81 | return 82 | } 83 | -------------------------------------------------------------------------------- /iploc/iploc.go: -------------------------------------------------------------------------------- 1 | package iploc 2 | 3 | import ( 4 | "bytes" 5 | "compress/zlib" 6 | "fmt" 7 | "io/ioutil" 8 | "runtime" 9 | ) 10 | 11 | const Version = "1.0" 12 | 13 | type rangeIterator func(i int, start, end IP) bool 14 | 15 | // Find shorthand for iploc.Open.Find 16 | // not be preload file and without indexed 17 | func Find(qqwrySrc, rawIP string) (*Detail, error) { 18 | loc, err := OpenWithoutIndexes(qqwrySrc) 19 | if err != nil { 20 | return nil, err 21 | } 22 | defer loc.parser.Close() 23 | return loc.Find(rawIP), nil 24 | } 25 | 26 | // Open 生成索引,查询速度快 27 | func Open(qqwrySrc string) (loc *Locator, err error) { 28 | loc = &Locator{} 29 | var parser *Parser 30 | if parser, err = NewParser(qqwrySrc, true); err != nil { 31 | return nil, err 32 | } 33 | loc.indexes = newIndexes(parser) 34 | loc.count = int(parser.count) 35 | return 36 | } 37 | 38 | // OpenWithoutIndexes 无索引,不预载文件,打开速度快,但查询速度慢 39 | func OpenWithoutIndexes(qqwrySrc string) (loc *Locator, err error) { 40 | loc = &Locator{} 41 | if loc.parser, err = NewParser(qqwrySrc, false); err != nil { 42 | return nil, err 43 | } 44 | loc.count = int(loc.parser.count) 45 | return 46 | } 47 | 48 | func Load(b []byte) (loc *Locator, err error) { 49 | buf := bytes.NewReader(b) 50 | r, err := zlib.NewReader(buf) 51 | if err != nil { 52 | return nil, err 53 | } 54 | b, err = ioutil.ReadAll(r) 55 | if err != nil { 56 | return nil, err 57 | } 58 | loc = &Locator{} 59 | res := &resource{data: b} 60 | var parser *Parser 61 | if parser, err = NewParserRes(res, uint32(len(res.data))); err != nil { 62 | return nil, err 63 | } 64 | loc.indexes = newIndexes(parser) 65 | loc.count = int(parser.count) 66 | return 67 | } 68 | 69 | func LoadWithoutIndexes(b []byte) (loc *Locator, err error) { 70 | buf := bytes.NewReader(b) 71 | r, err := zlib.NewReader(buf) 72 | if err != nil { 73 | return nil, err 74 | } 75 | b, err = ioutil.ReadAll(r) 76 | if err != nil { 77 | return nil, err 78 | } 79 | loc = &Locator{} 80 | res := &resource{data:b} 81 | if loc.parser, err = NewParserRes(res, uint32(len(res.data))); err != nil { 82 | return nil, err 83 | } 84 | loc.count = int(loc.parser.count) 85 | return 86 | } 87 | 88 | type Locator struct { 89 | parser *Parser 90 | indexes *indexes 91 | count int 92 | } 93 | 94 | // Close close the file descriptor, if there is no preload 95 | func (loc *Locator) Close() error { 96 | if loc.parser != nil { 97 | return loc.parser.Close() 98 | } 99 | return nil 100 | } 101 | 102 | func (loc *Locator) Count() int { 103 | return loc.count 104 | } 105 | 106 | func (loc *Locator) FindIP(ip IP) *Detail { 107 | return loc.find(ip) 108 | } 109 | 110 | func (loc *Locator) FindUint(uintIP uint32) *Detail { 111 | return loc.find(ParseUintIP(uintIP)) 112 | } 113 | 114 | func (loc *Locator) Find(rawIP string) *Detail { 115 | ip, err := ParseIP(rawIP) 116 | if err != nil { 117 | return nil 118 | } 119 | return loc.find(ip) 120 | } 121 | 122 | func (loc *Locator) Range(iterator rangeIterator) { 123 | if loc.indexes != nil { 124 | var n int 125 | loc.indexes.index.AscendRange(nil, nil, func(i dataItem) bool { 126 | n++ 127 | return iterator(n, ParseUintIP(i.(indexItem)[0]), ParseUintIP(i.(indexItem)[1])) 128 | }) 129 | } else { 130 | loc.parser.IndexRange(func(i int, start, end, pos uint32) bool { 131 | return iterator(i, ParseUintIP(start), ParseUintIP(end)) 132 | }) 133 | } 134 | } 135 | 136 | func (loc *Locator) find(ip IP) *Detail { 137 | defer func() { 138 | if r := recover(); r != nil { 139 | buf := make([]byte, 1<<16) 140 | buf = buf[:runtime.Stack(buf, false)] 141 | fmt.Printf("iploc > panic: %v\n%s", r, buf) 142 | } 143 | }() 144 | 145 | hit := loc.seek(ip) 146 | detail := &Detail{ 147 | IP: ip, 148 | Start: ParseUintIP(hit[0]), 149 | End: ParseUintIP(hit[1]), 150 | Location: loc.getLocation(hit), 151 | } 152 | return detail.fill() 153 | } 154 | 155 | func (loc *Locator) seek(ip IP) (hit indexItem) { 156 | if loc.indexes != nil { 157 | hit = loc.indexes.indexOf(ip.Uint()) 158 | } else { 159 | var low, mid, high uint32 160 | var index int64 161 | var start, end IP 162 | high = loc.parser.Count() - 1 163 | for low <= high { 164 | mid = (low + high) >> 1 165 | index = int64(loc.parser.min + mid*indexBlockSize) 166 | start = ParseBytesIP(loc.parser.ReadBytes(index, ipByteSize)) 167 | if ip.Compare(start) < 0 { 168 | high = mid - 1 169 | } else { 170 | index = loc.parser.ReadPosition(index + ipByteSize) 171 | end = ParseBytesIP(loc.parser.ReadBytes(index, ipByteSize)) 172 | if ip.Compare(end) > 0 { 173 | low = mid + 1 174 | } else { 175 | hit = indexItem{start.Uint(), end.Uint(), uint32(index), 0} 176 | break 177 | } 178 | } 179 | } 180 | } 181 | return 182 | } 183 | 184 | func (loc *Locator) getLocation(item indexItem) Location { 185 | if loc.indexes != nil { 186 | return loc.indexes.getLocation(item[2], item[3]) 187 | } 188 | return loc.parser.digLocation(int64(item[2])) 189 | } 190 | -------------------------------------------------------------------------------- /iploc/parser.go: -------------------------------------------------------------------------------- 1 | package iploc 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "os" 9 | ) 10 | 11 | const ( 12 | indexBlockSize = 7 13 | ipByteSize = 4 14 | positionByteSize = 3 15 | 16 | terminatorFlag = 0x00 17 | redirectAll = 0x01 18 | redirectPart = 0x02 19 | ) 20 | 21 | /* 22 | 23 | 原始下载地址 http://www.cz88.net/fox/ipdat.shtml 24 | 原版qqwry.dat, 字节序: LittleEndian 编码字符集: GBK 25 | 使用转换工具 iploc-conv 转换为UTF-8 26 | 文件内容结构, 所有偏移位置都是3字节的绝对偏移 27 | <文件头 8字节|数据区|索引区 7字节的倍数> 28 | [文件头] 索引区开始位置(4字节)|索引区结束位置(4字节) 29 | [数据区] 结束IP(4字节)|国家地区数据 30 | *国家地区数据 31 | 通常是 国家数据(0x00结尾)|地区数据(0x00结尾) 32 | 当国家地区数据的首字节是 0x01 或 0x02时为重定向模式,后3字节为偏移位置 33 | 34 | 0x01 国家和地区都重定向, 偏移位置(3字节) 35 | 0x02 国家重定向, 偏移位置(3字节)|地区数据 36 | 无重定向 国家数据(0x00结尾)|地区数据 37 | 38 | 地区数据可能还有一次重定向(0x02开头) 39 | 40 | [索引区] 起始IP(4字节)|偏移位置(3字节) = 每条索引 7字节 41 | */ 42 | 43 | type Parser struct { 44 | res resReadCloser 45 | min uint32 46 | max uint32 47 | total uint32 48 | count uint32 49 | size uint32 50 | } 51 | 52 | type indexIterator func(i int, start, end, pos uint32) bool 53 | 54 | type LocationRaw struct { 55 | Text [2][]byte 56 | Pos [2]uint32 57 | Mode [2]byte 58 | } 59 | 60 | func NewParser(qqwrySrc string, preload bool) (*Parser, error) { 61 | var ( 62 | err error 63 | size uint32 64 | fd *os.File 65 | b []byte 66 | res resReadCloser 67 | ) 68 | 69 | if preload { 70 | if b, err = ioutil.ReadFile(qqwrySrc); err != nil { 71 | return nil, err 72 | } 73 | size = uint32(len(b)) 74 | res = &resource{data: b} 75 | } else { 76 | if fd, err = os.OpenFile(qqwrySrc, os.O_RDONLY, 0400); err != nil { 77 | return nil, err 78 | } 79 | fi, err := fd.Stat() 80 | if err != nil { 81 | return nil, err 82 | } 83 | size = uint32(fi.Size()) 84 | res = fd 85 | } 86 | return NewParserRes(res, size) 87 | } 88 | 89 | func NewParserRes(res resReadCloser, size uint32) (*Parser, error) { 90 | if res == nil { 91 | return nil, fmt.Errorf("nil resource") 92 | } 93 | var ( 94 | p = &Parser{res: res} 95 | b []byte 96 | n int 97 | err error 98 | errInvalidDat = fmt.Errorf("invalid IP dat file") 99 | ) 100 | b = make([]byte, ipByteSize*2) 101 | if n, err = p.res.ReadAt(b, 0); err != nil || n != ipByteSize*2 { 102 | return nil, errInvalidDat 103 | } 104 | 105 | p.min = binary.LittleEndian.Uint32(b[:ipByteSize]) 106 | p.max = binary.LittleEndian.Uint32(b[ipByteSize:]) 107 | if (p.max-p.min)%indexBlockSize != 0 || size != p.max+indexBlockSize { 108 | return nil, errInvalidDat 109 | } 110 | p.total = (p.max - p.min) / indexBlockSize 111 | p.count = (p.max-p.min)/indexBlockSize + 1 112 | p.size = size 113 | return p, nil 114 | } 115 | 116 | func (p *Parser) Close() error { 117 | return p.res.Close() 118 | } 119 | 120 | func (p *Parser) Count() uint32 { 121 | return p.count 122 | } 123 | 124 | func (p *Parser) Size() uint32 { 125 | return p.size 126 | } 127 | 128 | func (p *Parser) Reader() io.Reader { 129 | return p.res 130 | } 131 | 132 | // (*Parser) ReadByte 读取1字节,用来识别重定向模式 133 | func (p *Parser) ReadByte(pos int64) byte { 134 | b := make([]byte, 1) 135 | n, err := p.res.ReadAt(b, pos) 136 | if err != nil || n != 1 { 137 | panic("ReadByte damaged DAT files, position: " + fmt.Sprint(pos)) 138 | } 139 | return b[0] 140 | } 141 | 142 | // (*Parser) ReadBytes 读取n字节并翻转 143 | func (p *Parser) ReadBytes(pos, n int64) (b []byte) { 144 | b = make([]byte, n) 145 | i, err := p.res.ReadAt(b, pos) 146 | if err != nil || int64(i) != n { 147 | panic("ReadBytes damaged DAT files, position: " + fmt.Sprint(pos)) 148 | } 149 | // reverse bytes 150 | for i, j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1 { 151 | b[i], b[j] = b[j], b[i] 152 | } 153 | return 154 | } 155 | 156 | // (*Parser) ReadPosition 读取3字节的偏移位置 157 | func (p *Parser) ReadPosition(offset int64) int64 { 158 | b := p.ReadBytes(offset, positionByteSize) 159 | // left padding to the 32 bits 160 | if i := 4 - len(b); i > 0 { 161 | b = append(make([]byte, i), b...) 162 | } 163 | // bytes has been reversed, so it won't use binary.LittleEndian 164 | return int64(binary.BigEndian.Uint32(b)) 165 | } 166 | 167 | // (*Parser) ReadText 读取国家地区数据,以0x00结尾 168 | func (p *Parser) ReadText(offset int64) ([]byte, int) { 169 | if uint32(offset) >= p.min { 170 | return nil, 0 171 | } 172 | var s []byte 173 | var b byte 174 | for { 175 | b = p.ReadByte(offset) 176 | if b != terminatorFlag { 177 | s = append(s, b) 178 | } else if len(s) > 0 { 179 | break 180 | } 181 | offset++ 182 | } 183 | return s, len(s) 184 | } 185 | 186 | func (p *Parser) ReadString(offset int64) (string, int) { 187 | s, n := p.ReadText(offset) 188 | return string(s), n 189 | } 190 | 191 | // (*Parser) ReadRegion 读取地区数据,处理可能的重定向 192 | func (p *Parser) ReadRegion(offset int64) (s []byte) { 193 | switch p.ReadByte(offset) { 194 | case redirectPart: 195 | s, _ = p.ReadText(p.ReadPosition(offset + 1)) 196 | default: 197 | s, _ = p.ReadText(offset) 198 | } 199 | return 200 | } 201 | 202 | func (p *Parser) ReadRegionString(offset int64) string { 203 | return string(p.ReadRegion(offset)) 204 | } 205 | 206 | func (p *Parser) digLocation(offset int64) (location Location) { 207 | var n int 208 | switch p.ReadByte(offset + ipByteSize) { 209 | case redirectAll: 210 | offset = p.ReadPosition(offset + ipByteSize + 1) 211 | switch p.ReadByte(offset) { 212 | case redirectPart: 213 | location.Country, _ = p.ReadString(p.ReadPosition(offset + 1)) 214 | location.Region = p.ReadRegionString(offset + 1 + positionByteSize) 215 | default: 216 | location.Country, n = p.ReadString(offset) 217 | // +1, skip 1 bytes 0x00 218 | location.Region = p.ReadRegionString(offset + 1 + int64(n)) 219 | } 220 | case redirectPart: 221 | location.Country, _ = p.ReadString(p.ReadPosition(offset + ipByteSize + 1)) 222 | location.Region = p.ReadRegionString(offset + ipByteSize + 1 + positionByteSize) 223 | default: 224 | location.Country, n = p.ReadString(offset + ipByteSize) 225 | // +1, skip 1 bytes 0x00 226 | location.Region = p.ReadRegionString(offset + ipByteSize + 1 + int64(n)) 227 | } 228 | location.raw = location.Country 229 | if location.Region != "" { 230 | location.raw += " " + location.Region 231 | } 232 | return 233 | } 234 | 235 | func (p *Parser) readRegionRaw(offset int64) (s []byte, pos uint32, mode byte) { 236 | switch p.ReadByte(offset) { 237 | case redirectPart: 238 | pos = uint32(p.ReadPosition(offset + 1)) 239 | mode = redirectPart 240 | default: 241 | s, _ = p.ReadText(offset) 242 | } 243 | return 244 | } 245 | 246 | // ReadLocationRaw 用于导出或索引 247 | func (p *Parser) ReadLocationRaw(offset int64) (raw LocationRaw) { 248 | var n int 249 | raw.Mode[0] = p.ReadByte(offset + ipByteSize) 250 | switch raw.Mode[0] { 251 | case redirectAll: 252 | offset = p.ReadPosition(offset + ipByteSize + 1) 253 | switch p.ReadByte(offset) { 254 | case redirectPart: 255 | raw.Mode[0] = redirectPart 256 | raw.Pos[0] = uint32(p.ReadPosition(offset + 1)) 257 | raw.Text[1], raw.Pos[1], raw.Mode[1] = p.readRegionRaw(offset + 1 + positionByteSize) 258 | if raw.Text[1] != nil { 259 | raw.Pos[1] = uint32(offset + 1 + positionByteSize) 260 | } 261 | default: 262 | raw.Pos[0] = uint32(offset) 263 | _, n = p.ReadText(offset) 264 | _, raw.Pos[1], _ = p.readRegionRaw(offset + 1 + int64(n)) 265 | if raw.Pos[1] == 0 { 266 | raw.Pos[1] = uint32(offset + 1 + int64(n)) 267 | } 268 | } 269 | case redirectPart: 270 | raw.Pos[0] = uint32(p.ReadPosition(offset + ipByteSize + 1)) 271 | raw.Text[1], raw.Pos[1], raw.Mode[1] = p.readRegionRaw(offset + ipByteSize + 1 + positionByteSize) 272 | if raw.Text[1] != nil { 273 | raw.Pos[1] = uint32(offset + ipByteSize + 1 + positionByteSize) 274 | } 275 | default: 276 | raw.Pos[0] = uint32(offset + ipByteSize) 277 | raw.Mode[0] = 0x00 278 | raw.Text[0], n = p.ReadText(offset + ipByteSize) 279 | raw.Text[1], raw.Pos[1], raw.Mode[1] = p.readRegionRaw(offset + ipByteSize + 1 + int64(n)) 280 | if raw.Text[1] != nil { 281 | raw.Pos[1] = uint32(offset + ipByteSize + 1 + int64(n)) 282 | } 283 | } 284 | return 285 | } 286 | 287 | // (*Parser) IndexRange 288 | // calls the iterator for every index within the range (i, start, end, Pos) 289 | // until iterator returns false. 290 | func (p *Parser) IndexRange(iterator indexIterator) { 291 | var ( 292 | count = int(p.count) 293 | index, pos int64 294 | ) 295 | for i := 0; i < count; i++ { 296 | index = int64(p.min) + indexBlockSize*int64(i) 297 | pos = p.ReadPosition(index + 4) 298 | if !iterator( 299 | i+1, 300 | ParseBytesIP(p.ReadBytes(index, ipByteSize)).Uint(), 301 | ParseBytesIP(p.ReadBytes(pos, ipByteSize)).Uint(), 302 | uint32(pos), 303 | ) { 304 | break 305 | } 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /iploc/resource.go: -------------------------------------------------------------------------------- 1 | package iploc 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | ) 7 | 8 | type resReadCloser interface { 9 | Read(b []byte) (n int, err error) 10 | ReadAt(b []byte, off int64) (n int, err error) 11 | Close() error 12 | } 13 | 14 | type resource struct { 15 | data []byte 16 | seek int64 17 | } 18 | 19 | func (res *resource) Read(b []byte) (n int, err error) { 20 | if len(b) == 0 { 21 | return 22 | } 23 | if max := len(res.data); len(b) > max { 24 | b = b[:max] 25 | } 26 | n, err = res.ReadAt(b, res.seek) 27 | res.seek += int64(n) 28 | return 29 | } 30 | 31 | func (res *resource) ReadAt(b []byte, off int64) (n int, err error) { 32 | if off < 0 { 33 | return 0, fmt.Errorf("negative offset: %d", off) 34 | } else if len(b) == 0 || off >= int64(len(res.data)) { 35 | return 0, nil 36 | } 37 | copy(b, res.data[off:]) 38 | if int64(len(b))+off > int64(len(res.data)) { 39 | return int(int64(len(res.data)) - off), io.EOF 40 | } 41 | return len(b), nil 42 | } 43 | 44 | func (res *resource) Close() error { 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /juggler.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "Juggler/config" 5 | "Juggler/golua" 6 | "Juggler/logger" 7 | "Juggler/web" 8 | "flag" 9 | "fmt" 10 | lua "github.com/yuin/gopher-lua" 11 | "strconv" 12 | ) 13 | 14 | var versionInfo = `[*] 更新说明 15 | [+] 2020年5月14日 16 | 增加geoip数据 17 | 允许通过-p参数进行端口指定 18 | 允许通过-l参数进行日志文件路径指定 19 | 允许通过-c参数进行配置文件路径指定 20 | 21 | [+] 2020年6月20日 22 | 把toml配置文件替换成lua插件模式 23 | 24 | [+] 2020年6月25日 25 | 增加time库和crypto库 26 | 使用kafka做消息缓冲入ES,使用gzip压缩减少网络io 27 | 通过lua虚拟机进行web请求和响应的基本处理 28 | 29 | [+] 2020年6月30日 30 | 可以通过lua插件自定义返回内容,响应格式包括 31 | 1.只状态码 32 | 2.固定格式化字符串+状态码 33 | 3.格式化文件内容+状态码 34 | 4.克隆站内容+状态码 35 | 响应体内容缓存进内存减少文件读取io 36 | 37 | [+] 2020年7月1日 38 | 响应体使用gzip减少网络io 39 | 将原有的拦截中心toml配置全部转化成lua脚本 40 | ` 41 | 42 | func main() { 43 | 44 | var err error 45 | v := flag.Bool("v", false, "版本参数") 46 | p := flag.String("p", "8888", "端口参数 默认端口8888") 47 | l := flag.String("l", "is.log", "日志路径参数 默认是./is.log") 48 | c := flag.String("c", "conf.toml", "配置文件路径参数 默认是./conf.toml") 49 | s := flag.String("s", "scripts/", "插件路径参数 默认是./scripts/") 50 | r := flag.String("r", "html/", "响应内容文件路径参数 默认是./html/ 且自定义路径必须以/结尾") 51 | g := flag.Int("g", web.DefaultGzip, "响应内容gzip压缩参数 默认DefaultGzip 0是不压缩 1是默认压缩 2是极致压缩") 52 | k := flag.Int("k", web.KafkaOpen, "kafka线程启动开关参数 默认开 0是开 其他都是关") 53 | // 初始化应用参数 54 | flag.Parse() 55 | // 初始化日志打印器 56 | if *l == "" { 57 | err = logger.NewPrinter("is.log") 58 | } else { 59 | err = logger.NewPrinter(*l) 60 | } 61 | if err != nil { 62 | fmt.Printf("new printer error : %v", err) 63 | return 64 | } 65 | // 初始化配置,现在只有kafka配置 66 | config.Cfg = config.Config{} 67 | var cp string 68 | if *c == "" { 69 | cp = "conf.toml" 70 | } else { 71 | cp = *c 72 | } 73 | if config.Cfg.Load(cp) != nil { 74 | logger.Printer(logger.ERROR, "config init error.") 75 | } 76 | go config.Cfg.Monitor(cp) 77 | // 打印当前版本信息 78 | if *v { 79 | fmt.Println(versionInfo) 80 | return 81 | } 82 | // 初始化监听端口,默认8888 83 | var port string 84 | if *p == "" { 85 | port = ":8888" 86 | } else { 87 | _, err := strconv.Atoi(*p) 88 | if err != nil { 89 | logger.Printer(logger.ERROR, "bad input : %v", err) 90 | port = ":8888" 91 | } else { 92 | port = ":" + *p 93 | } 94 | } 95 | // 初始化插件路径 96 | golua.LuaPool = &golua.LStatePool{ 97 | VMs: make(chan *lua.LState, 10240), 98 | Htmls: make(map[string][]byte), 99 | Fns: make(map[string]*lua.LFunction), 100 | } 101 | if err = golua.LuaPool.Init(*s); err != nil { 102 | return 103 | } 104 | // 初始化相应内容表 105 | if err = golua.LuaPool.LoadHtml(*r); err != nil { 106 | logger.Printer(logger.ERROR, "load htmls in %s error : %v", *r, err) 107 | return 108 | } 109 | defer golua.LuaPool.Shutdown() 110 | 111 | if *k == 0 { 112 | web.KAccess.Thread() 113 | go web.KAccess.Start() 114 | } 115 | 116 | web.WebServer(port, *g) 117 | } 118 | -------------------------------------------------------------------------------- /logger/printer.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "Juggler/config" 5 | "fmt" 6 | "log" 7 | "os" 8 | ) 9 | 10 | var ( 11 | ERROR = 0 12 | INFO = 2 13 | DEBUG = 4 14 | ) 15 | 16 | func NewPrinter(LogPath string) error { 17 | 18 | output, err := os.OpenFile(LogPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) 19 | if err != nil { 20 | return err 21 | } 22 | log.SetOutput(output) 23 | 24 | return nil 25 | } 26 | 27 | func Printer(level int, format string, v ...interface{}) { 28 | 29 | // 如果等级一样,就只打印该等级而没有其他等级日志 30 | if level == config.Cfg.Other.Debug { 31 | logging(level, format, v...) 32 | } else { 33 | // 如果等级不一样,就把小于配置文件等级的所有日志等级全部打印 34 | if config.Cfg.Other.Debug > level { 35 | logging(level, format, v...) 36 | } 37 | } 38 | } 39 | 40 | func logging(level int, format string, v ...interface{}) { 41 | 42 | switch level { 43 | case 0: 44 | log.Println(fmt.Sprintf("[error] "+format, v...)) 45 | case 2: 46 | log.Println(fmt.Sprintf("[info] "+format, v...)) 47 | case 4: 48 | log.Println(fmt.Sprintf("[debug] "+format, v...)) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /pics/juggler-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/C4o/Juggler/9a6a06276c37ff3aa5201b5d82abc3eb45fc5576/pics/juggler-1.gif -------------------------------------------------------------------------------- /pics/juggler-2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/C4o/Juggler/9a6a06276c37ff3aa5201b5d82abc3eb45fc5576/pics/juggler-2.gif -------------------------------------------------------------------------------- /pics/juggler-waf.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/C4o/Juggler/9a6a06276c37ff3aa5201b5d82abc3eb45fc5576/pics/juggler-waf.jpg -------------------------------------------------------------------------------- /pics/juggler.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/C4o/Juggler/9a6a06276c37ff3aa5201b5d82abc3eb45fc5576/pics/juggler.jpg -------------------------------------------------------------------------------- /qqwry-0.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/C4o/Juggler/9a6a06276c37ff3aa5201b5d82abc3eb45fc5576/qqwry-0.dat -------------------------------------------------------------------------------- /scripts/default.lua: -------------------------------------------------------------------------------- 1 | -- 当攻击请求无法匹配到特定插件时,匹配默认插件 2 | local resp = rock.resp 3 | 4 | resp.set_header("Content-Type", "text/html; charset=utf-8") 5 | resp.html(404, "default_404") 6 | return -------------------------------------------------------------------------------- /scripts/juggler.test.com.lua: -------------------------------------------------------------------------------- 1 | -- 文件名juggler.test.com.lua 当攻击请求的业务域名是juggler.test.com时匹配该插件 2 | local var = rock.var 3 | local resp = rock.resp 4 | local crypto = require("crypto") 5 | local time = require("time") 6 | local re = require("re") 7 | local log = rock.log 8 | local ERR = rock.ERROR 9 | 10 | -- 通过var内的参数,匹配每一个攻击请求中的http参数 11 | if var.rule == "sqli" then 12 | -- 满足条件后直接返回格式化字符串,使用内置方法每次回显不同的32位随机md5值 13 | resp.string(200, "Congratulation!Password hash is %s.", crypto.randomMD5(32)) 14 | -- 在日志文件中打印日志 15 | log(ERR, "found sqli attack in %d", time.format()) 16 | return 17 | end 18 | 19 | -- 使用正则匹配某个路径,与规则匹配并用 20 | if var.rule == "xss" and re.match(var.uri, "^/admin/") then 21 | -- 设置响应体类型 22 | resp.set_header("Content-Type", "text/html; charset=utf-8") 23 | -- 添加响应头Date,内容是正常服务器产生的内容 24 | resp.set_header("Date", time.server_date()) 25 | -- 只响应状态码,不响应内容 26 | resp.status(403) 27 | return 28 | end 29 | 30 | -- 文件读取漏洞检测到就返回一个假的逼真的对应内容 31 | if var.rule == "lfi_shadow" then 32 | -- 使用预存文件etc_shadow.html进行内容回显,状态码200 33 | resp.html(200, "etc_shadow") 34 | return 35 | end 36 | 37 | -- 使用waf来匹配特定cookie用来后续把黑客引流到蜜罐 38 | if var.rule == "rce" then 39 | resp.set_header("Content-Type", "text/html; charset=utf-8") 40 | -- 在响应中set_cookie 41 | resp.set_cookie("sessionid", "admin_session", 6000, "/", var.host, true, true) 42 | -- 克隆固定页面回显,缓存内容,不会每次都克隆 43 | resp.clone(200, "https://duxiaofa.baidu.com/detail?searchType=statute&from=aladdin_28231&originquery=%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8%E6%B3%95&count=79&cid=f66f830e45c0490d589f1de2fe05e942_law") 44 | return 45 | end 46 | 47 | -- 不匹配任何规则时,返回默认404内容 48 | resp.set_header("Content-Type", "text/html; charset=utf-8") 49 | resp.html(404, "default_404") 50 | return -------------------------------------------------------------------------------- /web/kafka.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | conf "Juggler/config" 5 | "Juggler/logger" 6 | jsoniter "github.com/json-iterator/go" 7 | 8 | //"context" 9 | "errors" 10 | "fmt" 11 | "time" 12 | 13 | "github.com/IBM/sarama" 14 | //"golang.org/x/time/rate" 15 | ) 16 | 17 | type KafkaAccess struct { 18 | Size int64 19 | Path string 20 | Chan chan Requests 21 | First int64 22 | Last int64 // 上次发送信息 23 | Count int // 数据总量 24 | } 25 | 26 | var ( 27 | KAccess = KafkaAccess{Chan: make(chan Requests, 40960*2)} 28 | Address []string 29 | Num int 30 | Thread int 31 | json = jsoniter.ConfigCompatibleWithStandardLibrary 32 | KafkaOpen = 0 33 | ) 34 | 35 | func (access *KafkaAccess) Send(data []*sarama.ProducerMessage, client sarama.SyncProducer) { 36 | 37 | //发送消息 38 | err := client.SendMessages(data) 39 | if err != nil { 40 | logger.Printer(logger.ERROR, "send message failed: %v", err) 41 | return 42 | } 43 | //logger.Printer(logger.INFO, "Send %d data to kafka in %d", len(data), time.Now().Unix()) 44 | } 45 | 46 | // 读取channel并发送 47 | func (access *KafkaAccess) RChan(id int) { 48 | 49 | //logger.Printer(logger.INFO, "id is : %d\n", id) 50 | //kafka初始化配置 51 | config := sarama.NewConfig() 52 | config.Producer.RequiredAcks = sarama.WaitForAll 53 | config.Producer.Partitioner = sarama.NewRandomPartitioner 54 | config.Producer.Return.Successes = true 55 | //新增lz4压缩方式 56 | config.Producer.Compression = sarama.CompressionGZIP 57 | //生产者 58 | client, err := sarama.NewSyncProducer(Address, config) 59 | if err != nil { 60 | logger.Printer(logger.ERROR, "producer close,err: %v", err) 61 | return 62 | } 63 | 64 | defer client.Close() 65 | s10 := time.NewTicker(20 * time.Second) 66 | defer func() { 67 | s10.Stop() 68 | }() 69 | 70 | // 可从配置文件里读 71 | count := 0 72 | access.Last = time.Now().Unix() 73 | data := make([]*sarama.ProducerMessage, Num) 74 | 75 | //l := rate.NewLimiter(rate.Limit(config.Conf.Limit/config.Conf.Thread), 1000) 76 | //c, _ := context.WithCancel(context.TODO()) 77 | for { 78 | //l.Wait(c) 79 | select { 80 | case raw := <-access.Chan: 81 | jstr, err := json.Marshal(raw) 82 | //logger.Printer(logger.DEBUG, "recv jstr : %v", jstr) 83 | if err == nil { 84 | msg := &sarama.ProducerMessage{} 85 | msg.Topic = conf.Cfg.Kafka.Topic 86 | msg.Value = sarama.StringEncoder(fmt.Sprintf("%s", jstr)) 87 | data[count] = msg 88 | count += 1 89 | if count == Num { 90 | access.Count += count 91 | count = 0 92 | access.Send(data, client) 93 | data = make([]*sarama.ProducerMessage, Num) 94 | } 95 | } else { 96 | logger.Printer(logger.ERROR, "json marshal in kafka data, error : %v", err) 97 | } 98 | case <-s10.C: 99 | if count != 0 { 100 | tmp := count 101 | count = 0 102 | access.Send(data[:tmp], client) 103 | access.Count += tmp 104 | data = make([]*sarama.ProducerMessage, Num) 105 | access.Last = time.Now().Unix() 106 | } 107 | } 108 | } 109 | } 110 | 111 | // 判断topic是否存在 112 | func (access *KafkaAccess) Topic() error { 113 | 114 | var err error 115 | var cli sarama.Client 116 | var topics []string 117 | 118 | cli, err = sarama.NewClient(Address, sarama.NewConfig()) 119 | defer func() { 120 | err = cli.Close() 121 | if err != nil { 122 | logger.Printer(logger.ERROR, "close kafka client error, %v", err) 123 | } 124 | }() 125 | if err != nil { 126 | logger.Printer(logger.ERROR, "create new client err : %v", err) 127 | return err 128 | } 129 | topics, err = cli.Topics() 130 | if err != nil { 131 | logger.Printer(logger.ERROR, "get topics err : %v", err) 132 | return err 133 | } 134 | for _, t := range topics { 135 | if t == conf.Cfg.Kafka.Topic { 136 | return nil 137 | } 138 | } 139 | return errors.New("no topic : " + conf.Cfg.Kafka.Topic) 140 | } 141 | 142 | // 启动线程 143 | func (access *KafkaAccess) Thread() { 144 | 145 | Thread = conf.Cfg.Kafka.Thread 146 | 147 | //logger.Printer(logger.DEBUG, "%d new thread start.", Thread) 148 | for i := 0; i < Thread; i++ { 149 | go access.RChan(i) 150 | } 151 | } 152 | 153 | // 初始化 154 | func (access *KafkaAccess) Start() { 155 | 156 | //s5 := time.NewTicker(5 * time.Second) 157 | //defer func() { 158 | //s5.Stop() 159 | //}() 160 | 161 | //for { 162 | //select { 163 | //case <-s5.C: 164 | Address = []string{conf.Cfg.Kafka.Addr} 165 | Num = conf.Cfg.Kafka.Num 166 | 167 | access.First = time.Now().Unix() 168 | access.Last = time.Now().Unix() 169 | access.Count = 0 170 | var err error 171 | 172 | if err = access.Topic(); err != nil { 173 | s10 := time.NewTicker(10 * time.Second) 174 | defer func() { 175 | s10.Stop() 176 | }() 177 | for { 178 | select { 179 | case <-s10.C: 180 | if err = access.Topic(); err == nil { 181 | break 182 | } 183 | logger.Printer(logger.ERROR, "kafka get topic err, %v", err) 184 | } 185 | 186 | } 187 | } 188 | //} 189 | //} 190 | } 191 | 192 | 193 | -------------------------------------------------------------------------------- /web/logging.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "Juggler/config" 5 | "Juggler/logger" 6 | "encoding/base64" 7 | "net" 8 | "os" 9 | "time" 10 | 11 | "github.com/gin-gonic/gin" 12 | "github.com/oschwald/geoip2-golang" 13 | ) 14 | 15 | type Buff struct { 16 | Ch chan Requests 17 | Size int 18 | File *os.File 19 | } 20 | 21 | func Logging() gin.HandlerFunc { 22 | 23 | //logger.Printer(logger.ERROR, "logging") 24 | return func(c *gin.Context) { 25 | 26 | var err error 27 | var body []byte 28 | var record *geoip2.City 29 | geodata := GeoData{} 30 | r := c.Request 31 | w := c.Writer 32 | 33 | req := Requests{ 34 | TimeStamp: time.Now().Format("02/Jan/2006:15:04:05 +0800"), 35 | Saddr: r.RemoteAddr, 36 | Method: r.Method, 37 | Host: r.Host, 38 | UA: r.UserAgent(), 39 | URI: r.URL.Path, 40 | Query: r.URL.RawQuery, 41 | Rule: r.Header.Get("rule"), 42 | XFF: r.Header.Get("x-forwarded-for"), 43 | REF: r.Referer(), 44 | Addr: r.Header.Get("x-real-ip"), 45 | APP: r.Header.Get("x-Rock-APP"), 46 | Headers: r.Header, 47 | Status: w.Status(), 48 | Size: w.Size(), 49 | } 50 | 51 | body, err = c.GetRawData() 52 | //logger.Printer(logger.DEBUG, "addr is %v", req.Addr) 53 | if err != nil { 54 | logger.Printer(logger.ERROR, "get raw data error : %v", err) 55 | } else { 56 | // 获取地理位置信息 57 | if len(req.Addr) > 0 { 58 | loc := Loc.Find(req.Addr) 59 | req.LRegion = loc.Region 60 | req.LCountry = loc.Country 61 | req.LProvince = loc.Province 62 | req.LCity = loc.City 63 | } 64 | req.Body = base64.StdEncoding.EncodeToString(body) 65 | req.Local = config.Cfg.Other.Local 66 | 67 | record, err = GeoFile.City(net.ParseIP(req.Addr)) 68 | if err != nil { 69 | logger.Printer(logger.ERROR, "get geoip data error : %v", err) 70 | } else { 71 | geodata.Lat = record.Location.Latitude 72 | geodata.Lon = record.Location.Longitude 73 | req.Location = geodata 74 | //fmt.Printf("geodata : %v", geodata) 75 | } 76 | // 如果开关开启,就往kafka的channel里传 77 | if config.Cfg.Kafka.On { 78 | KAccess.Chan <- req 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /web/router.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "Juggler/golua" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | // gin路由处理函数 10 | func handler(c *gin.Context) { 11 | 12 | golua.LuaWorker(c) 13 | 14 | //c.String(200, string(cjpl)) 15 | } 16 | 17 | -------------------------------------------------------------------------------- /web/struct.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | type Requests struct { 4 | TimeStamp string `json:"timestamp"` // 访问时间 5 | Saddr string `json:"saddr"` // 来源地址,一般是WAF地址 6 | Host string `json:"host"` // host头,区分业务 7 | UA string `json:"ua"` // UA头 8 | URI string `json:"uri"` // uri 9 | Query string `json:"query"` // uri里的查询参数 10 | Rule string `json:"rule"` // 匹配上的规则,便于分类拦截 11 | XFF string `json:"xff"` // X-Forwarded-For 12 | REF string `json:"ref"` // referer 13 | Addr string `json:"addr"` // X-Real-IP 14 | Method string `json:"method"` // 请求方式 15 | APP string `json:"app"` // 所属应用 16 | Headers map[string][]string `json:"headers"` // 完整头数据 17 | Status int `json:"status"` // 响应状态码 18 | Size int `json:"size"` // 响应包长度 19 | Body string `json:"body"` // post body全包 20 | LRegion string `json:"region"` // 组织 21 | LCountry string `json:"country"` // 国家 22 | LProvince string `json:"province"` // 省份 23 | LCity string `json:"city"` // 城市 24 | Local string `json:"localip"` // 拦截中心地址 25 | Location GeoData `json:"Location"` // 经纬度 26 | } 27 | 28 | type GeoData struct { 29 | Lat float64 `json:"lat"` 30 | Lon float64 `json:"lon"` 31 | } 32 | -------------------------------------------------------------------------------- /web/web.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "Juggler/iploc" 5 | "Juggler/logger" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/gin-contrib/gzip" 9 | "github.com/oschwald/geoip2-golang" 10 | ) 11 | 12 | var ( 13 | GeoFile *geoip2.Reader 14 | Loc *iploc.Locator 15 | NoGzip = 0 16 | DefaultGzip = 1 17 | BestGzip = 2 18 | ) 19 | 20 | func WebServer(addr string, gzipType int) { 21 | 22 | var err error 23 | // 初始化纯真数据库 24 | Loc, err = iploc.Open("qqwry-0.dat") 25 | if err != nil { 26 | logger.Printer(logger.ERROR, "open qqwry.dat error : %v", err) 27 | return 28 | } 29 | // 初始化geoip数据,方便kibana做攻击来源热力图 30 | GeoFile, err = geoip2.Open("GeoIP.mmdb") 31 | if err != nil { 32 | logger.Printer(logger.ERROR, "open GeoIP.mmdb error : %v", err) 33 | return 34 | } 35 | defer GeoFile.Close() 36 | 37 | // 启动gin 38 | r := gin.New() 39 | gin.SetMode(gin.ReleaseMode) 40 | switch gzipType { 41 | case NoGzip: 42 | r.Use(gzip.Gzip(gzip.NoCompression)) 43 | case DefaultGzip: 44 | r.Use(gzip.Gzip(gzip.DefaultCompression)) 45 | case BestGzip: 46 | r.Use(gzip.Gzip(gzip.BestCompression)) 47 | } 48 | r.Use(gin.Recovery(), Logging()) 49 | r.NoRoute(handler) 50 | //r.GET("/", Handler) 51 | r.Run(addr) 52 | } 53 | --------------------------------------------------------------------------------