├── .gitignore ├── GeoLite2-City.mmdb ├── Gopkg.lock ├── Gopkg.toml ├── LICENSE ├── README.md ├── _config.yml ├── api ├── api.go ├── baidu.go ├── base.go ├── pconline.go ├── sina.go └── taobao.go ├── config.toml ├── config └── config.go ├── es └── es.go ├── geolite2 └── geolite2.go ├── ip2region.db ├── ip2region ├── ip2region.go └── lib.go ├── ipd.go ├── middleware ├── auth.go ├── cors.go ├── json_logger.go └── sentry.go ├── qqwry.dat ├── qqwry └── qqwry.go ├── server └── server.go └── utils ├── utils.go └── utils_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | ipd 14 | ipd.exe 15 | vendor/ -------------------------------------------------------------------------------- /GeoLite2-City.mmdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xin053/ipd/b6cbe2a60190cdb8b6ff9c9423e8aede290967d7/GeoLite2-City.mmdb -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | name = "github.com/BurntSushi/toml" 6 | packages = ["."] 7 | revision = "b26d9c308763d68093482582cea63d69be07a0f0" 8 | version = "v0.3.0" 9 | 10 | [[projects]] 11 | name = "github.com/allegro/bigcache" 12 | packages = [ 13 | ".", 14 | "queue" 15 | ] 16 | revision = "f31987a23e44c5121ef8c8b2f2ea2e8ffa37b068" 17 | version = "v1.1.0" 18 | 19 | [[projects]] 20 | branch = "master" 21 | name = "github.com/axgle/mahonia" 22 | packages = ["."] 23 | revision = "3358181d7394e26beccfae0ffde05193ef3be33a" 24 | 25 | [[projects]] 26 | name = "github.com/certifi/gocertifi" 27 | packages = ["."] 28 | revision = "deb3ae2ef2610fde3330947281941c562861188b" 29 | version = "2018.01.18" 30 | 31 | [[projects]] 32 | branch = "master" 33 | name = "github.com/getsentry/raven-go" 34 | packages = ["."] 35 | revision = "263040ce1a362270b5897a5982572ddc1fe807be" 36 | 37 | [[projects]] 38 | branch = "master" 39 | name = "github.com/gin-contrib/sse" 40 | packages = ["."] 41 | revision = "22d885f9ecc78bf4ee5d72b937e4bbcdc58e8cae" 42 | 43 | [[projects]] 44 | name = "github.com/gin-gonic/gin" 45 | packages = [ 46 | ".", 47 | "binding", 48 | "render" 49 | ] 50 | revision = "d459835d2b077e44f7c9b453505ee29881d5d12d" 51 | version = "v1.2" 52 | 53 | [[projects]] 54 | name = "github.com/golang/protobuf" 55 | packages = ["proto"] 56 | revision = "925541529c1fa6821df4e44ce2723319eb2be768" 57 | version = "v1.0.0" 58 | 59 | [[projects]] 60 | name = "github.com/json-iterator/go" 61 | packages = ["."] 62 | revision = "ca39e5af3ece67bbcda3d0f4f56a8e24d9f2dad4" 63 | version = "1.1.3" 64 | 65 | [[projects]] 66 | branch = "master" 67 | name = "github.com/mailru/easyjson" 68 | packages = [ 69 | ".", 70 | "buffer", 71 | "jlexer", 72 | "jwriter" 73 | ] 74 | revision = "8b799c424f57fa123fc63a99d6383bc6e4c02578" 75 | 76 | [[projects]] 77 | name = "github.com/mattn/go-isatty" 78 | packages = ["."] 79 | revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39" 80 | version = "v0.0.3" 81 | 82 | [[projects]] 83 | name = "github.com/modern-go/concurrent" 84 | packages = ["."] 85 | revision = "bacd9c7ef1dd9b15be4a9909b8ac7a4e313eec94" 86 | version = "1.0.3" 87 | 88 | [[projects]] 89 | name = "github.com/modern-go/reflect2" 90 | packages = ["."] 91 | revision = "1df9eeb2bb81f327b96228865c5687bc2194af3f" 92 | version = "1.0.0" 93 | 94 | [[projects]] 95 | name = "github.com/olivere/elastic" 96 | packages = [ 97 | ".", 98 | "config", 99 | "uritemplates" 100 | ] 101 | revision = "d6362604399c7af560b54f048b4fcfbdd6eff293" 102 | version = "v6.1.14" 103 | 104 | [[projects]] 105 | name = "github.com/oschwald/geoip2-golang" 106 | packages = ["."] 107 | revision = "7118115686e16b77967cdbf55d1b944fe14ad312" 108 | version = "v1.2.1" 109 | 110 | [[projects]] 111 | name = "github.com/oschwald/maxminddb-golang" 112 | packages = ["."] 113 | revision = "c5bec84d1963260297932a1b7a1753c8420717a7" 114 | version = "v1.3.0" 115 | 116 | [[projects]] 117 | name = "github.com/pkg/errors" 118 | packages = ["."] 119 | revision = "645ef00459ed84a119197bfb8d8205042c6df63d" 120 | version = "v0.8.0" 121 | 122 | [[projects]] 123 | name = "github.com/sirupsen/logrus" 124 | packages = ["."] 125 | revision = "c155da19408a8799da419ed3eeb0cb5db0ad5dbc" 126 | version = "v1.0.5" 127 | 128 | [[projects]] 129 | name = "github.com/ugorji/go" 130 | packages = ["codec"] 131 | revision = "b4c50a2b199d93b13dc15e78929cfb23bfdf21ab" 132 | version = "v1.1.1" 133 | 134 | [[projects]] 135 | branch = "master" 136 | name = "golang.org/x/crypto" 137 | packages = ["ssh/terminal"] 138 | revision = "2b6c08872f4b66da917bb4ce98df4f0307330f78" 139 | 140 | [[projects]] 141 | branch = "master" 142 | name = "golang.org/x/sys" 143 | packages = [ 144 | "unix", 145 | "windows" 146 | ] 147 | revision = "79b0c6888797020a994db17c8510466c72fe75d9" 148 | 149 | [[projects]] 150 | name = "gopkg.in/go-playground/validator.v8" 151 | packages = ["."] 152 | revision = "5f1438d3fca68893a817e4a66806cea46a9e4ebf" 153 | version = "v8.18.2" 154 | 155 | [[projects]] 156 | name = "gopkg.in/yaml.v2" 157 | packages = ["."] 158 | revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183" 159 | version = "v2.2.1" 160 | 161 | [solve-meta] 162 | analyzer-name = "dep" 163 | analyzer-version = 1 164 | inputs-digest = "e492cfd1ad6091bfcf21edf4dc4653cfc7cf17a0b8a1b7cdfe393d229824155d" 165 | solver-name = "gps-cdcl" 166 | solver-version = 1 167 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | 2 | # Gopkg.toml example 3 | # 4 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md 5 | # for detailed Gopkg.toml documentation. 6 | # 7 | # required = ["github.com/user/thing/cmd/thing"] 8 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 9 | # 10 | # [[constraint]] 11 | # name = "github.com/user/project" 12 | # version = "1.0.0" 13 | # 14 | # [[constraint]] 15 | # name = "github.com/user/project2" 16 | # branch = "dev" 17 | # source = "github.com/myfork/project2" 18 | # 19 | # [[override]] 20 | # name = "github.com/x/y" 21 | # version = "2.4.0" 22 | 23 | [[constraint]] 24 | name = "github.com/gin-gonic/gin" 25 | version = "1.2.0" 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 xin053 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ipd 2 | 3 | 批量ip反查服务, 批量获取ip的地理位置信息,包括国家,省份,城市,ISP以及地理位置,支持国内外ip,目前仅支持ipv4. 4 | 5 | 基于[`gin框架`](https://github.com/gin-gonic/gin)构建, 默认端口`6789`,可在`config.toml`中修改服务端口以及其他配置项 6 | 7 | ## 从源代码构建 8 | 9 | 1. 下载包管理器[`dep`](https://github.com/golang/dep) 10 | 11 | ```shell 12 | go get -u github.com/golang/dep/cmd/dep 13 | ``` 14 | 15 | **确保`dep`在环境变量`PATH`中** 16 | 2. 下载[`ipd`](https://github.com/xin053/ipd)源码 17 | 18 | ```shell 19 | export GOPATH=`pwd` 20 | go get -d github.com/golang/xin053/ipd 21 | ``` 22 | 23 | 3. 安装依赖 24 | 25 | ```shell 26 | dep ensure 27 | ``` 28 | 29 | 4. 构建`ipd`可执行文件 30 | 31 | ```shell 32 | cd github.com/golang/xin053/ipd 33 | go build ipd.go 34 | ``` 35 | 36 | 或者 37 | 38 | ```shell 39 | go build -ldflags "-w -s" ipd.go 40 | ``` 41 | 42 | 5. 使用 43 | 44 | ```shell 45 | # 运行 46 | ./ipd 47 | # 停止 48 | kill -2 pid 49 | ``` 50 | 51 | ## 使用 52 | 53 | `ipd`提供四种ip反查的方式: 54 | 55 | 1. 通过[纯真ip数据库](http://www.cz88.net/fox/ipdat.shtml)(目前更新到`2018-09-25`) 56 | 57 | [*纯真ip数据库官网*](http://www.cz88.net/fox/ipdat.shtml),下载可能需要翻墙,下载后安装会释放出`qqwry.dat`文件,也可以使用 [`UniExtract2`](https://github.com/Bioruebe/UniExtract2) 直接解压出文件 58 | 59 | 2. 通过[GeoLite2数据库](https://dev.maxmind.com/zh-hans/geoip/geoip2/geolite2-%E5%BC%80%E6%BA%90%E6%95%B0%E6%8D%AE%E5%BA%93/) 60 | 3. 通过[ip2region数据库](https://github.com/lionsoul2014/ip2region),启动服务时会自动从github下载最新的数据库文件(*最优先*) 61 | 4. 通过公开的 REST API方式, 目前支持四种接口: 62 | 63 | 1. 淘宝ip查询接口:`http://ip.taobao.com/service/getIpInfo.php?ip=` 64 | 2. 新浪ip查询接口:`http://int.dpool.sina.com.cn/iplookup/iplookup.php?format=json&ip=` 65 | 3. 太平洋ip查询接口:`http://whois.pconline.com.cn/ipJson.jsp?ip=` 66 | 4. 百度ip查询接口:`http://api.map.baidu.com/location/ip?ak=yourAK&ip=` 67 | 68 | ### API 69 | 70 | 1. 从公共API获取ip信息 71 | 72 | **接口** 73 | 74 | `POST /v1/api` 75 | 76 | **请求头** 77 | 78 | 1. `Authorization` = `thisisaveryimportantkey`(key可在`config.go`中配置) 79 | 2. `Content-Type` = `application/json` 80 | 81 | **Body** 82 | 83 | ```json 84 | { 85 | "ip": 86 | ["111.111.111.111", "8.8.8.8"] 87 | } 88 | ``` 89 | 90 | **返回** 91 | 92 | ```json 93 | [ 94 | { 95 | "ip": "8.8.8.8", 96 | "country": "美国", 97 | "region": "", 98 | "city": "", 99 | "isp": "Google公共DNS", 100 | "geo_x": 0, 101 | "geo_y": 0 102 | }, 103 | { 104 | "ip": "111.111.111.111", 105 | "country": "", 106 | "region": "", 107 | "city": "", 108 | "isp": "日本东京市KDDI通信公司", 109 | "geo_x": 0, 110 | "geo_y": 0 111 | } 112 | ] 113 | ``` 114 | 115 | 2. 从纯真ip数据库获取ip信息 116 | 117 | **接口** 118 | 119 | `POST /v1/db` 120 | 121 | **请求头** 122 | 123 | 1. `Authorization` = `thisisaveryimportantkey`(key可在`config.go`中配置) 124 | 2. `Content-Type` = `application/json` 125 | 126 | **Body** 127 | 128 | ```json 129 | { 130 | "ip": 131 | ["111.111.111.111", "8.8.8.8"] 132 | } 133 | ``` 134 | 135 | **返回** 136 | 137 | ```json 138 | [ 139 | { 140 | "ip": "8.8.8.8", 141 | "country": "美国", 142 | "region": "", 143 | "city": "", 144 | "isp": "Google公共DNS", 145 | "geo_x": 0, 146 | "geo_y": 0 147 | }, 148 | { 149 | "ip": "111.111.111.111", 150 | "country": "", 151 | "region": "", 152 | "city": "", 153 | "isp": "日本东京市KDDI通信公司", 154 | "geo_x": 0, 155 | "geo_y": 0 156 | } 157 | ] 158 | ``` 159 | 160 | 3. 从GeoLite2数据库获取ip信息 161 | 162 | **接口** 163 | 164 | `POST /v1/db2` 165 | 166 | **请求头** 167 | 168 | 1. `Authorization` = `thisisaveryimportantkey`(key可在`config.go`中配置) 169 | 2. `Content-Type` = `application/json` 170 | 171 | **Body** 172 | 173 | ```json 174 | { 175 | "ip": 176 | ["111.111.111.111", "8.8.8.8"] 177 | } 178 | ``` 179 | 180 | **返回** 181 | 182 | ```json 183 | [ 184 | { 185 | "ip": "8.8.8.8", 186 | "country": "美国", 187 | "region": "", 188 | "city": "", 189 | "isp": "Google公共DNS", 190 | "geo_x": 0, 191 | "geo_y": 0 192 | }, 193 | { 194 | "ip": "111.111.111.111", 195 | "country": "", 196 | "region": "", 197 | "city": "", 198 | "isp": "日本东京市KDDI通信公司", 199 | "geo_x": 0, 200 | "geo_y": 0 201 | } 202 | ] 203 | ``` 204 | 205 | 4. 从ip2region数据库获取ip信息 206 | 207 | **接口** 208 | 209 | `POST /v1/db3` 210 | 211 | **请求头** 212 | 213 | 1. `Authorization` = `thisisaveryimportantkey`(key可在`config.go`中配置) 214 | 2. `Content-Type` = `application/json` 215 | 216 | **Body** 217 | 218 | ```json 219 | { 220 | "ip": 221 | ["111.111.111.111", "8.8.8.8"] 222 | } 223 | ``` 224 | 225 | **返回** 226 | 227 | ```json 228 | [ 229 | { 230 | "ip": "8.8.8.8", 231 | "country": "美国", 232 | "region": "", 233 | "city": "", 234 | "isp": "Google公共DNS", 235 | "geo_x": 0, 236 | "geo_y": 0 237 | }, 238 | { 239 | "ip": "111.111.111.111", 240 | "country": "", 241 | "region": "", 242 | "city": "", 243 | "isp": "日本东京市KDDI通信公司", 244 | "geo_x": 0, 245 | "geo_y": 0 246 | } 247 | ] 248 | ``` 249 | 250 | 5. 整合以上四种方式获取ip信息,先异步查ip2region数据库(默认的主数据库),查不到的ip再查纯真, 再查GeoLite2数据库,最后通过api查询 251 | 252 | 主数据库可在配置文件中配置(`config.toml`中的`request_order`配置) 253 | 254 | **接口** 255 | 256 | `POST /v1/ip` 257 | 258 | **请求头** 259 | 260 | 1. `Authorization` = `thisisaveryimportantkey`(key可在`config.go`中配置) 261 | 2. `Content-Type` = `application/json` 262 | 263 | **Body** 264 | 265 | ```json 266 | { 267 | "ip": 268 | ["111.111.111.111", "8.8.8.8"] 269 | } 270 | ``` 271 | 272 | **返回** 273 | 274 | ```json 275 | [ 276 | { 277 | "ip": "8.8.8.8", 278 | "country": "美国", 279 | "region": "", 280 | "city": "", 281 | "isp": "Google公共DNS", 282 | "geo_x": 0, 283 | "geo_y": 0 284 | }, 285 | { 286 | "ip": "111.111.111.111", 287 | "country": "", 288 | "region": "", 289 | "city": "", 290 | "isp": "日本东京市KDDI通信公司", 291 | "geo_x": 0, 292 | "geo_y": 0 293 | } 294 | ] 295 | ``` 296 | 297 | ## 项目结构 298 | 299 | ``` 300 | api\ 301 | |api.go # api 方式查询 ip 信息主文件 302 | |baidu.go # 百度 ip API 服务解析 303 | |base.go # 通用接口 304 | |pconline.go # 太平洋 ip API 服务解析 305 | |sina.go # 新浪 ip API 服务解析 306 | |taobao.go # 淘宝 ip API 服务解析 307 | config\ 308 | |config.go # 从 config.toml 读取配置以及其他配置 309 | es\ 310 | |es.go # elasticsearch 存储相关 311 | geolite2\ 312 | |geolite2.go # db2 方式查询 ip 信息主文件 313 | ip2region\ 314 | |ip2region # db3 方式查询 ip 信息主文件 315 | |lib # 解析 ip2region.db 的库文件 316 | middleware\ 317 | |auth.go # 简单授权验证中间件 318 | |cors.go # cors 跨域中间件 319 | |json_logger.go # 日志服务中间件 320 | |sentry.go # sentry 服务 321 | qqwry\ 322 | |qqwry.go # db 方式查询 ip 信息主文件 323 | server\ 324 | |server.go # /v1/ip 接口主文件 325 | utils\ 326 | |utils_test.go # 工具包测试 327 | |utils.go # 工具包 328 | config.toml # ipd 服务使用的配置文件 329 | GeoLite2-City.mmdb # geolite2 数据库二进制文件 330 | Gopkg.lock # dep 包管理器 lock 文件 331 | Gopkg.toml # dep 包管理器 toml 文件 332 | ip2region.db # ip2region 数据库二进制文件 333 | ipd.go # ipd 服务 main 包 334 | qqwry.dat # 纯真 ip 数据库二进制文件 335 | README.md # README 336 | ``` 337 | 338 | ## 构建自己的ip信息数据库 339 | 340 | ipd服务支持将查询过的ip信息添加到elasticsearch数据库,作后续其他的使用.默认启用elasticsearch存储 341 | 342 | 1. 安装[elasticsearch](https://www.elastic.co/products/elasticsearch),建议安装最新版 343 | 2. 修改`config.toml`中的`elasticsearch`段的`url`, 如果不想使用存储ip功能,将`elasticsearch`段注释即可 344 | 345 | ## 其他事项 346 | 347 | 1. 程序默认使用sentry服务, 修改`config.go`中的`dsn`以使用自己的sentry,也可以注释掉`config.toml`中的`sentry`段来禁用sentry 348 | 2. 更多设置请查看`config.toml`的备注说明 349 | 350 | ## TODO 351 | 352 | - [ ] 添加es接口,直接从es查询ip数据 353 | - [ ] 增加精准查询接口(多种查询方式同时 goroutine,获取接口分析取最优) 354 | - [x] 添加缓存机制 -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /api/api.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "math/rand" 5 | "sync" 6 | "time" 7 | 8 | "github.com/gin-gonic/gin" 9 | log "github.com/sirupsen/logrus" 10 | "github.com/xin053/ipd/config" 11 | "github.com/xin053/ipd/server" 12 | "github.com/xin053/ipd/utils" 13 | ) 14 | 15 | var apiList = []API{&TaoBao{}, &Sina{}, &BaiDu{}, &Pconline{}} 16 | 17 | // IPdAPI struct implement server.Server for api method 18 | type IPdAPI struct{} 19 | 20 | // FromAPI get ip information from public IP API 21 | func FromAPI(c *gin.Context) { 22 | server.FindIPs(c, &IPdAPI{}) 23 | } 24 | 25 | // FindIP find information about a single IP 26 | func (i *IPdAPI) FindIP(ip string, ch chan config.IPWithGeo, wg *sync.WaitGroup) *config.IPWithGeo { 27 | if wg != nil { 28 | defer wg.Done() 29 | } 30 | 31 | ipWithGeo := &config.IPWithGeo{} 32 | ipWithGeo.IP = ip 33 | 34 | count := 0 35 | var randomAPI API 36 | randomAPI = apiList[rand.Intn(len(apiList))] 37 | result, err := randomAPI.Request(ip) 38 | 39 | for err != nil { 40 | log.Error(err) 41 | count++ 42 | if count >= 3 { 43 | return ipWithGeo 44 | } 45 | 46 | randomAPI = apiList[rand.Intn(len(apiList))] 47 | result, err = randomAPI.Request(ip) 48 | } 49 | 50 | ipInfo := result.JSON() 51 | ipWithGeo = utils.AddGeo(&ipInfo) 52 | if ch != nil { 53 | ch <- *ipWithGeo 54 | } 55 | return ipWithGeo 56 | } 57 | 58 | // Register register server 59 | func (i *IPdAPI) Register(routine bool) { 60 | server.Register(i, routine) 61 | } 62 | 63 | func init() { 64 | rand.Seed(time.Now().UnixNano()) 65 | } 66 | -------------------------------------------------------------------------------- /api/baidu.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net/http" 7 | 8 | "github.com/json-iterator/go" 9 | "github.com/xin053/ipd/config" 10 | ) 11 | 12 | type BaiDu struct { 13 | IP string `json:"ip"` 14 | Country string `json:"country"` 15 | Region string `json:"province"` 16 | City string `json:"city"` 17 | ISP string `json:"isp"` 18 | } 19 | 20 | func (b *BaiDu) Name() string { 21 | return "百度" 22 | } 23 | 24 | func (b *BaiDu) Url() string { 25 | return fmt.Sprintf("http://api.map.baidu.com/location/ip?ak=%s&ip=", config.BaiDuAK) 26 | } 27 | 28 | func (b *BaiDu) Request(ip string) (API, error) { 29 | url := b.Url() + ip 30 | req, err := http.NewRequest("GET", url, nil) 31 | if err != nil { 32 | return nil, err 33 | } 34 | req.Header.Add("Referer", config.BaiDuReferer) 35 | 36 | client := http.Client{ 37 | Timeout: config.APITimeOut, 38 | } 39 | 40 | resp, err := client.Do(req) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | defer resp.Body.Close() 46 | 47 | body, err := ioutil.ReadAll(resp.Body) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | baidu := BaiDu{} 53 | json.Unmarshal([]byte(jsoniter.Get(body, "content", "address_detail").ToString()), &baidu) 54 | baidu.IP = ip 55 | return &baidu, nil 56 | } 57 | 58 | func (b *BaiDu) JSON() config.IPInfo { 59 | return config.IPInfo{b.IP, b.Country, b.Region, b.City, b.ISP} 60 | } 61 | -------------------------------------------------------------------------------- /api/base.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/json-iterator/go" 5 | "github.com/xin053/ipd/config" 6 | ) 7 | 8 | var json = jsoniter.ConfigCompatibleWithStandardLibrary 9 | 10 | // API ip api interface 11 | type API interface { 12 | // Name get ip api name 13 | Name() string 14 | 15 | //Url get ip api url 16 | Url() string 17 | 18 | //Request get ip api result 19 | Request(ip string) (API, error) 20 | 21 | //JSON IPInfo 22 | JSON() config.IPInfo 23 | } 24 | -------------------------------------------------------------------------------- /api/pconline.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "io/ioutil" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/axgle/mahonia" 9 | "github.com/xin053/ipd/config" 10 | ) 11 | 12 | type Pconline struct { 13 | IP string `json:"ip"` 14 | Country string `json:"country"` 15 | Region string `json:"pro"` 16 | City string `json:"city"` 17 | ISP string `json:"addr"` 18 | } 19 | 20 | func (p *Pconline) Name() string { 21 | return "太平洋" 22 | } 23 | 24 | func (p *Pconline) Url() string { 25 | return "http://whois.pconline.com.cn/ipJson.jsp?ip=" 26 | } 27 | 28 | func (p *Pconline) Request(ip string) (API, error) { 29 | url := p.Url() + ip 30 | client := http.Client{ 31 | Timeout: config.APITimeOut, 32 | } 33 | resp, err := client.Get(url) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | defer resp.Body.Close() 39 | 40 | body, err := ioutil.ReadAll(resp.Body) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | result := strings.Split(strings.Split(string(body[:]), "(")[2], ")")[0] 46 | 47 | enc := mahonia.NewDecoder("gbk") 48 | result = enc.ConvertString(result) 49 | 50 | pconline := Pconline{} 51 | json.Unmarshal([]byte(result), &pconline) 52 | 53 | pconline.IP = ip 54 | if pconline.ISP != "" && len(strings.Split(pconline.ISP, " ")) == 2 { 55 | pconline.ISP = strings.Split(pconline.ISP, " ")[1] 56 | } else { 57 | pconline.ISP = "" 58 | } 59 | return &pconline, nil 60 | } 61 | 62 | func (p *Pconline) JSON() config.IPInfo { 63 | return config.IPInfo{p.IP, p.Country, p.Region, p.City, p.ISP} 64 | } 65 | -------------------------------------------------------------------------------- /api/sina.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "io/ioutil" 5 | "net/http" 6 | 7 | "github.com/xin053/ipd/config" 8 | ) 9 | 10 | type Sina struct { 11 | IP string `json:"ip"` 12 | Country string `json:"country"` 13 | Region string `json:"province"` 14 | City string `json:"city"` 15 | ISP string `json:"isp"` 16 | } 17 | 18 | func (s *Sina) Name() string { 19 | return "新浪" 20 | } 21 | 22 | func (s *Sina) Url() string { 23 | return "http://int.dpool.sina.com.cn/iplookup/iplookup.php?format=json&ip=" 24 | } 25 | 26 | func (s *Sina) Request(ip string) (API, error) { 27 | url := s.Url() + ip 28 | client := http.Client{ 29 | Timeout: config.APITimeOut, 30 | } 31 | resp, err := client.Get(url) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | defer resp.Body.Close() 37 | 38 | body, err := ioutil.ReadAll(resp.Body) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | sina := Sina{} 44 | json.Unmarshal(body, &sina) 45 | sina.IP = ip 46 | return &sina, nil 47 | } 48 | 49 | func (s *Sina) JSON() config.IPInfo { 50 | return config.IPInfo{s.IP, s.Country, s.Region, s.City, s.ISP} 51 | } 52 | -------------------------------------------------------------------------------- /api/taobao.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "io/ioutil" 5 | "net/http" 6 | 7 | "github.com/json-iterator/go" 8 | "github.com/xin053/ipd/config" 9 | ) 10 | 11 | type TaoBao struct { 12 | IP string `json:"ip"` 13 | Country string `json:"country"` 14 | Region string `json:"region"` 15 | City string `json:"city"` 16 | ISP string `json:"isp"` 17 | } 18 | 19 | func (t *TaoBao) Name() string { 20 | return "淘宝" 21 | } 22 | 23 | func (t *TaoBao) Url() string { 24 | return "http://ip.taobao.com/service/getIpInfo.php?ip=" 25 | } 26 | 27 | func (t *TaoBao) Request(ip string) (API, error) { 28 | url := t.Url() + ip 29 | client := http.Client{ 30 | Timeout: config.APITimeOut, 31 | } 32 | resp, err := client.Get(url) 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | defer resp.Body.Close() 38 | 39 | body, err := ioutil.ReadAll(resp.Body) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | taoBao := TaoBao{} 45 | json.Unmarshal([]byte(jsoniter.Get(body, "data").ToString()), &taoBao) 46 | taoBao.IP = ip 47 | return &taoBao, nil 48 | } 49 | 50 | func (t *TaoBao) JSON() config.IPInfo { 51 | return config.IPInfo{t.IP, t.Country, t.Region, t.City, t.ISP} 52 | } 53 | -------------------------------------------------------------------------------- /config.toml: -------------------------------------------------------------------------------- 1 | # config file for ipd server 2 | 3 | [ipd] 4 | # ipd server port 5 | port = "6789" 6 | # buffer size used for goroutine querying ip data 7 | # set it to the max ip numbers in one request 8 | chanel_buffer = 200 9 | # used for authorization, each request should have this http header 10 | auth_key = "thisisaveryimportantkey" 11 | # timeout for public ip querying API 12 | api_timeout = "3s" 13 | # your baidu ip server AK 14 | baidu_AK = "dXkvR03fdzCvUvfb69Ztnlr6UBMk7Azy" 15 | # this can be set at baidu map console, and your request should have 16 | # http hearder Referer set to this value 17 | baidu_referer = "www.mygoipapi.com" 18 | # request order, the first one will be used asynchronously, the rest will just use a for loop statement 19 | # if you don't know what this means, you should not change this 20 | request_order = ["ip2region", "chunzhen", "geolite2", "api"] 21 | 22 | [sentry] 23 | # when your don't want to use sentry, please comment this block 24 | # sentry DSN 25 | dsn = "https://3d94eafaf06c4626aa6fec57a5e78176:4d1da97ba2e14289b34d0aa5e09ef49b@sentry.io/1200974" 26 | 27 | [elasticsearch] 28 | # when your don't want to use sentry, please comment this block 29 | # elasticsearch url 30 | url = "http://127.0.0.1:9200" 31 | # elasticsearch index 32 | index = "ipinfo" 33 | 34 | [ip2region] 35 | # https://github.com/lionsoul2014/ip2region 36 | # ip2region.db download url 37 | url = "https://github.com/lionsoul2014/ip2region/raw/master/data/ip2region.db" 38 | 39 | [cache] 40 | # use bigcache lib 41 | # expiration time for every entry 42 | expiration = "12h" 43 | # max entry size 44 | max_entry = 1000 45 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "time" 7 | 8 | "github.com/BurntSushi/toml" 9 | "github.com/allegro/bigcache" 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | type tomlConfig struct { 14 | IPd ipd `toml:"ipd"` 15 | Sentry sentry `toml:"sentry"` 16 | Elasticsearch elasticsearch `toml:"elasticsearch"` 17 | IP2Region ip2Region `toml:"ip2region"` 18 | Cache cache `toml:"cache"` 19 | } 20 | 21 | type ipd struct { 22 | Port string `toml:"port"` 23 | IPChanelBuf int `toml:"chanel_buffer"` 24 | AuthKey string `toml:"auth_key"` 25 | APITimeOut duration `toml:"api_timeout"` 26 | BaiDuAK string `toml:"baidu_AK"` 27 | BaiDuReferer string `toml:"baidu_referer"` 28 | RequestOrder []string `toml:"request_order"` 29 | } 30 | 31 | type sentry struct { 32 | SentryDSN string `toml:"dsn"` 33 | } 34 | 35 | type elasticsearch struct { 36 | ESURL string `toml:"url"` 37 | ESIndex string `toml:"index"` 38 | } 39 | 40 | type ip2Region struct { 41 | URL string `toml:"url"` 42 | } 43 | 44 | type cache struct { 45 | Expiration duration `toml:"expiration"` 46 | MaxEntry int `toml:"max_entry"` 47 | } 48 | 49 | type duration struct { 50 | time.Duration 51 | } 52 | 53 | func (d *duration) UnmarshalText(text []byte) error { 54 | var err error 55 | d.Duration, err = time.ParseDuration(string(text)) 56 | return err 57 | } 58 | 59 | var ( 60 | // UseSentry whether ipd server use sentry or not 61 | UseSentry bool = true 62 | // SentryDSN sentry DSN 63 | SentryDSN string = "https://3d94eafaf06c4626aa6fec57a5e78176:4d1da97ba2e14289b34d0aa5e09ef49b@sentry.io/1200974" 64 | // Port ipd server port 65 | Port string = "6789" 66 | // IPChanelBuf chanel buffer size for FindIP go routine 67 | IPChanelBuf int = 200 68 | // AuthKey used for authorization, should not be changed 69 | AuthKey string = "thisisaveryimportantkey" 70 | // APITimeOut ip api timeout 71 | APITimeOut time.Duration = time.Duration(3) * time.Second 72 | // BaiDuAK baidu ak key 73 | BaiDuAK string = "dXkvR03fdzCvUvfb69Ztnlr6UBMk7Azy" 74 | // BaiDuReferer baidu api referer 75 | BaiDuReferer string = "www.mygoipapi.com" 76 | // RequestOrder request order 77 | RequestOrder []string = []string{"ip2region", "chunzhen", "geolite2", "api"} 78 | // IP2RegionURL url to download ip2region.db file 79 | IP2RegionURL string = "https://github.com/lionsoul2014/ip2region/raw/master/data/ip2region.db" 80 | // Expiration cache expiration time 81 | Expiration time.Duration = time.Duration(10) * time.Minute 82 | // MaxEntry max entry size 83 | MaxEntry int = 1000 84 | // bigcache cache client 85 | Cache *bigcache.BigCache 86 | // StoreES store ip information in elasticsearch or not 87 | StoreES bool = true 88 | // ESURL elasticsearch url 89 | ESURL string = "http://127.0.0.1:9200" 90 | // ESIndex elasticsearch index used to store the ip information 91 | ESIndex string = "ipinfo" 92 | //ESMapping ipinfo mapping 93 | ESMapping string = ` 94 | { 95 | "settings": { 96 | "index": { 97 | "refresh_interval": "5s" 98 | } 99 | }, 100 | "mappings": { 101 | "_doc": { 102 | "properties":{ 103 | "time":{ 104 | "type":"date", 105 | "ignore_malformed": true, 106 | "format": "yyyy-MM-dd HH:mm:ss.SSS||yyyy-MM-dd HH:mm:ss" 107 | }, 108 | "ip":{ 109 | "type":"ip" 110 | }, 111 | "country":{ 112 | "type":"keyword" 113 | }, 114 | "region":{ 115 | "type":"keyword" 116 | }, 117 | "city":{ 118 | "type":"keyword" 119 | }, 120 | "isp":{ 121 | "type":"keyword" 122 | }, 123 | "location":{ 124 | "type":"geo_point", 125 | "ignore_malformed": true 126 | } 127 | } 128 | } 129 | } 130 | }` 131 | ) 132 | 133 | // IP ip struct 134 | type IP struct { 135 | IP []string `json:"ip"` 136 | } 137 | 138 | // IPInfo ipinfo struct 139 | type IPInfo struct { 140 | IP string `json:"ip"` 141 | Country string `json:"country"` 142 | Region string `json:"region"` 143 | City string `json:"city"` 144 | ISP string `json:"isp"` 145 | } 146 | 147 | // Geo geo struct 148 | type Geo struct { 149 | GeoX float64 `json:"geo_x"` 150 | GeoY float64 `json:"geo_y"` 151 | } 152 | 153 | //IPWithGeo combine IPInfo and Geo 154 | type IPWithGeo struct { 155 | IPInfo 156 | Geo 157 | } 158 | 159 | // GeoMap geo map 160 | var GeoMap = map[string]Geo{ 161 | "甘肃": {103.73, 36.03}, 162 | "青海": {101.74, 36.56}, 163 | "四川": {104.06, 30.67}, 164 | "河北": {114.48, 38.03}, 165 | "云南": {102.73, 25.04}, 166 | "贵州": {106.71, 26.57}, 167 | "湖北": {114.31, 30.52}, 168 | "河南": {113.65, 34.76}, 169 | "山东": {117, 36.65}, 170 | "江苏": {118.78, 32.04}, 171 | "安徽": {117.27, 31.86}, 172 | "浙江": {120.19, 30.26}, 173 | "江西": {115.89, 28.68}, 174 | "福建": {119.3, 26.08}, 175 | "广东": {113.23, 23.16}, 176 | "湖南": {113, 28.21}, 177 | "海南": {110.35, 20.02}, 178 | "辽宁": {123.38, 41.8}, 179 | "吉林": {125.35, 43.88}, 180 | "黑龙江": {126.63, 45.75}, 181 | "山西": {112.53, 37.87}, 182 | "陕西": {108.95, 34.27}, 183 | "台湾": {121.30, 25.03}, 184 | "北京": {116.46, 39.92}, 185 | "上海": {121.48, 31.22}, 186 | "重庆": {106.54, 29.59}, 187 | "天津": {117.2, 39.13}, 188 | "内蒙古": {111.65, 40.82}, 189 | "广西": {108.33, 22.84}, 190 | "西藏": {91.11, 29.97}, 191 | "宁夏": {106.27, 38.47}, 192 | "新疆": {87.68, 43.77}, 193 | "香港": {114.17, 22.28}, 194 | "澳门": {113.54, 22.19}, 195 | // 常用省 196 | "海门": {121.15, 31.89}, 197 | "鄂尔多斯": {109.781327, 39.608266}, 198 | "招远": {120.38, 37.35}, 199 | "舟山": {122.207216, 29.985295}, 200 | "齐齐哈尔": {123.97, 47.33}, 201 | "盐城": {120.13, 33.38}, 202 | "赤峰": {118.87, 42.28}, 203 | "青岛": {120.33, 36.07}, 204 | "乳山": {121.52, 36.89}, 205 | "金昌": {102.188043, 38.520089}, 206 | "泉州": {118.58, 24.93}, 207 | "莱西": {120.53, 36.86}, 208 | "日照": {119.46, 35.42}, 209 | "胶南": {119.97, 35.88}, 210 | "南通": {121.05, 32.08}, 211 | "拉萨": {91.11, 29.97}, 212 | "云浮": {112.02, 22.93}, 213 | "梅州": {116.1, 24.55}, 214 | "文登": {122.05, 37.2}, 215 | "攀枝花": {101.718637, 26.582347}, 216 | "威海": {122.1, 37.5}, 217 | "承德": {117.93, 40.97}, 218 | "厦门": {118.1, 24.46}, 219 | "汕尾": {115.375279, 22.786211}, 220 | "潮州": {116.63, 23.68}, 221 | "丹东": {124.37, 40.13}, 222 | "太仓": {121.1, 31.45}, 223 | "曲靖": {103.79, 25.51}, 224 | "烟台": {121.39, 37.52}, 225 | "福州": {119.3, 26.08}, 226 | "瓦房店": {121.979603, 39.627114}, 227 | "即墨": {120.45, 36.38}, 228 | "抚顺": {123.97, 41.97}, 229 | "玉溪": {102.52, 24.35}, 230 | "张家口": {114.87, 40.82}, 231 | "阳泉": {113.57, 37.85}, 232 | "莱州": {119.942327, 37.177017}, 233 | "湖州": {120.1, 30.86}, 234 | "汕头": {116.69, 23.39}, 235 | "昆山": {120.95, 31.39}, 236 | "宁波": {121.56, 29.86}, 237 | "湛江": {110.359377, 21.270708}, 238 | "揭阳": {116.35, 23.55}, 239 | "荣成": {122.41, 37.16}, 240 | "连云港": {119.16, 34.59}, 241 | "葫芦岛": {120.836932, 40.711052}, 242 | "常熟": {120.74, 31.64}, 243 | "东莞": {113.75, 23.04}, 244 | "河源": {114.68, 23.73}, 245 | "淮安": {119.15, 33.5}, 246 | "泰州": {119.9, 32.49}, 247 | "南宁": {108.33, 22.84}, 248 | "营口": {122.18, 40.65}, 249 | "惠州": {114.4, 23.09}, 250 | "江阴": {120.26, 31.91}, 251 | "蓬莱": {120.75, 37.8}, 252 | "韶关": {113.62, 24.84}, 253 | "嘉峪关": {98.289152, 39.77313}, 254 | "广州": {113.23, 23.16}, 255 | "延安": {109.47, 36.6}, 256 | "太原": {112.53, 37.87}, 257 | "清远": {113.01, 23.7}, 258 | "中山": {113.38, 22.52}, 259 | "昆明": {102.73, 25.04}, 260 | "寿光": {118.73, 36.86}, 261 | "盘锦": {122.070714, 41.119997}, 262 | "长治": {113.08, 36.18}, 263 | "深圳": {114.07, 22.62}, 264 | "珠海": {113.52, 22.3}, 265 | "宿迁": {118.3, 33.96}, 266 | "咸阳": {108.72, 34.36}, 267 | "铜川": {109.11, 35.09}, 268 | "平度": {119.97, 36.77}, 269 | "佛山": {113.11, 23.05}, 270 | "海口": {110.35, 20.02}, 271 | "江门": {113.06, 22.61}, 272 | "章丘": {117.53, 36.72}, 273 | "肇庆": {112.44, 23.05}, 274 | "大连": {121.62, 38.92}, 275 | "临汾": {111.5, 36.08}, 276 | "吴江": {120.63, 31.16}, 277 | "石嘴山": {106.39, 39.04}, 278 | "沈阳": {123.38, 41.8}, 279 | "苏州": {120.62, 31.32}, 280 | "茂名": {110.88, 21.68}, 281 | "嘉兴": {120.76, 30.77}, 282 | "长春": {125.35, 43.88}, 283 | "胶州": {120.03336, 36.264622}, 284 | "银川": {106.27, 38.47}, 285 | "张家港": {120.555821, 31.875428}, 286 | "三门峡": {111.19, 34.76}, 287 | "锦州": {121.15, 41.13}, 288 | "南昌": {115.89, 28.68}, 289 | "柳州": {109.4, 24.33}, 290 | "三亚": {109.511909, 18.252847}, 291 | "自贡": {104.778442, 29.33903}, 292 | "阳江": {111.95, 21.85}, 293 | "泸州": {105.39, 28.91}, 294 | "西宁": {101.74, 36.56}, 295 | "宜宾": {104.56, 29.77}, 296 | "呼和浩特": {111.65, 40.82}, 297 | "成都": {104.06, 30.67}, 298 | "大同": {113.3, 40.12}, 299 | "镇江": {119.44, 32.2}, 300 | "桂林": {110.28, 25.29}, 301 | "张家界": {110.479191, 29.117096}, 302 | "宜兴": {119.82, 31.36}, 303 | "北海": {109.12, 21.49}, 304 | "西安": {108.95, 34.27}, 305 | "金坛": {119.56, 31.74}, 306 | "东营": {118.49, 37.46}, 307 | "牡丹江": {129.58, 44.6}, 308 | "遵义": {106.9, 27.7}, 309 | "绍兴": {120.58, 30.01}, 310 | "扬州": {119.42, 32.39}, 311 | "常州": {119.95, 31.79}, 312 | "潍坊": {119.1, 36.62}, 313 | "台州": {121.420757, 28.656386}, 314 | "南京": {118.78, 32.04}, 315 | "滨州": {118.03, 37.36}, 316 | "贵阳": {106.71, 26.57}, 317 | "无锡": {120.29, 31.59}, 318 | "本溪": {123.73, 41.3}, 319 | "克拉玛依": {84.77, 45.59}, 320 | "渭南": {109.5, 34.52}, 321 | "马鞍山": {118.48, 31.56}, 322 | "宝鸡": {107.15, 34.38}, 323 | "焦作": {113.21, 35.24}, 324 | "句容": {119.16, 31.95}, 325 | "徐州": {117.2, 34.26}, 326 | "衡水": {115.72, 37.72}, 327 | "包头": {110, 40.58}, 328 | "绵阳": {104.73, 31.48}, 329 | "乌鲁木齐": {87.68, 43.77}, 330 | "枣庄": {117.57, 34.86}, 331 | "杭州": {120.19, 30.26}, 332 | "淄博": {118.05, 36.78}, 333 | "鞍山": {122.85, 41.12}, 334 | "溧阳": {119.48, 31.43}, 335 | "库尔勒": {86.06, 41.68}, 336 | "安阳": {114.35, 36.1}, 337 | "开封": {114.35, 34.79}, 338 | "济南": {117, 36.65}, 339 | "德阳": {104.37, 31.13}, 340 | "温州": {120.65, 28.01}, 341 | "九江": {115.97, 29.71}, 342 | "邯郸": {114.47, 36.6}, 343 | "临安": {119.72, 30.23}, 344 | "兰州": {103.73, 36.03}, 345 | "沧州": {116.83, 38.33}, 346 | "临沂": {118.35, 35.05}, 347 | "南充": {106.110698, 30.837793}, 348 | "富阳": {119.95, 30.07}, 349 | "泰安": {117.13, 36.18}, 350 | "诸暨": {120.23, 29.71}, 351 | "郑州": {113.65, 34.76}, 352 | "哈尔滨": {126.63, 45.75}, 353 | "聊城": {115.97, 36.45}, 354 | "芜湖": {118.38, 31.33}, 355 | "唐山": {118.02, 39.63}, 356 | "平顶山": {113.29, 33.75}, 357 | "邢台": {114.48, 37.05}, 358 | "德州": {116.29, 37.45}, 359 | "济宁": {116.59, 35.38}, 360 | "荆州": {112.239741, 30.335165}, 361 | "宜昌": {111.3, 30.7}, 362 | "义乌": {120.06, 29.32}, 363 | "丽水": {119.92, 28.45}, 364 | "洛阳": {112.44, 34.7}, 365 | "秦皇岛": {119.57, 39.95}, 366 | "株洲": {113.16, 27.83}, 367 | "石家庄": {114.48, 38.03}, 368 | "莱芜": {117.67, 36.19}, 369 | "常德": {111.69, 29.05}, 370 | "保定": {115.48, 38.85}, 371 | "湘潭": {112.91, 27.87}, 372 | "金华": {119.64, 29.12}, 373 | "岳阳": {113.09, 29.37}, 374 | "长沙": {113, 28.21}, 375 | "衢州": {118.88, 28.97}, 376 | "廊坊": {116.7, 39.53}, 377 | "菏泽": {115.480656, 35.23375}, 378 | "合肥": {117.27, 31.86}, 379 | "武汉": {114.31, 30.52}, 380 | "大庆": {125.03, 46.58}, 381 | } 382 | 383 | func init() { 384 | var err error 385 | dir, err := filepath.Abs(filepath.Dir(os.Args[0])) 386 | if err != nil { 387 | log.Error(err) 388 | } 389 | var config tomlConfig 390 | if _, err = toml.DecodeFile(filepath.Join(dir, "config.toml"), &config); err != nil { 391 | log.Error(err) 392 | } 393 | 394 | Port = config.IPd.Port 395 | IPChanelBuf = config.IPd.IPChanelBuf 396 | AuthKey = config.IPd.AuthKey 397 | APITimeOut = config.IPd.APITimeOut.Duration 398 | BaiDuAK = config.IPd.BaiDuAK 399 | BaiDuReferer = config.IPd.BaiDuReferer 400 | IP2RegionURL = config.IP2Region.URL 401 | RequestOrder = config.IPd.RequestOrder 402 | 403 | // cache init 404 | c := bigcache.DefaultConfig(Expiration) 405 | c.MaxEntrySize = MaxEntry 406 | Cache, err = bigcache.NewBigCache(c) 407 | if err != nil { 408 | log.Fatal(err) 409 | } 410 | 411 | if config.Sentry.SentryDSN == "" { 412 | UseSentry = false 413 | } else { 414 | SentryDSN = config.Sentry.SentryDSN 415 | } 416 | 417 | if config.Elasticsearch.ESURL == "" { 418 | StoreES = false 419 | } else { 420 | ESURL = config.Elasticsearch.ESURL 421 | ESIndex = config.Elasticsearch.ESIndex 422 | } 423 | } 424 | -------------------------------------------------------------------------------- /es/es.go: -------------------------------------------------------------------------------- 1 | package es 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/olivere/elastic" 8 | log "github.com/sirupsen/logrus" 9 | "github.com/xin053/ipd/config" 10 | ) 11 | 12 | var ( 13 | ctx context.Context 14 | Client *elastic.Client 15 | ) 16 | 17 | type ESIPInfo struct { 18 | Time string `json:"time"` 19 | IP string `json:"ip"` 20 | Country string `json:"country"` 21 | Region string `json:"region"` 22 | City string `json:"city"` 23 | ISP string `json:"isp"` 24 | Location struct { 25 | Lon float64 `json:"lon"` 26 | Lat float64 `json:"lat"` 27 | } `json:"location"` 28 | } 29 | 30 | // Store store these ip information in elasticsearch. 31 | func Store(ipWithGeos ...config.IPWithGeo) { 32 | termQuerys := []elastic.Query{} 33 | bulkRequest := Client.Bulk() 34 | indexReqs := []elastic.BulkableRequest{} 35 | for _, ipWithGeo := range ipWithGeos { 36 | termQuery := elastic.NewTermQuery("ip", ipWithGeo.IP) 37 | termQuerys = append(termQuerys, termQuery) 38 | 39 | esIPInfo := ESIPInfo{ 40 | Time: time.Now().Format("2006-01-02 15:04:05.999"), 41 | IP: ipWithGeo.IP, 42 | Country: ipWithGeo.Country, 43 | Region: ipWithGeo.Region, 44 | City: ipWithGeo.City, 45 | ISP: ipWithGeo.ISP, 46 | } 47 | esIPInfo.Location.Lon = ipWithGeo.GeoX 48 | esIPInfo.Location.Lat = ipWithGeo.GeoY 49 | 50 | indexReq := elastic.NewBulkIndexRequest().Index(config.ESIndex).Type("_doc").Doc(esIPInfo) 51 | indexReqs = append(indexReqs, indexReq) 52 | } 53 | 54 | if len(indexReqs) == 0 { 55 | return 56 | } 57 | 58 | bulkRequest = bulkRequest.Add(indexReqs...) 59 | // first, delete this ip info in elasticsearch 60 | q := elastic.NewBoolQuery().Should(termQuerys...) 61 | // Search with a term query 62 | _, err := Client.DeleteByQuery(). 63 | Index(config.ESIndex). // search in index "ipinfo" 64 | Query(q). // return all results, but ... 65 | // Pretty(true). // pretty print request and response JSON 66 | Do(ctx) // execute 67 | if err != nil { 68 | log.Error(err) 69 | return 70 | } 71 | // then, we send a bulk request. 72 | _, err = bulkRequest.Do(ctx) 73 | if err != nil { 74 | log.Error(err) 75 | return 76 | } 77 | return 78 | } 79 | 80 | func init() { 81 | if config.StoreES { 82 | ctx = context.Background() 83 | var err error 84 | Client, err = elastic.NewClient(elastic.SetURL(config.ESURL)) 85 | if err != nil && config.StoreES { 86 | log.Error(err) 87 | } 88 | 89 | exists, err := Client.IndexExists(config.ESIndex).Do(ctx) 90 | if err != nil { 91 | log.Error(err) 92 | } 93 | 94 | if !exists { 95 | log.Infof("elasticsearch index %s does not exits, and will be created now.", config.ESIndex) 96 | _, err := Client.CreateIndex(config.ESIndex).BodyString(config.ESMapping).Do(ctx) 97 | if err != nil { 98 | log.Error(err) 99 | } else { 100 | log.Infof("elasticsearch index %s has been created.", config.ESIndex) 101 | } 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /geolite2/geolite2.go: -------------------------------------------------------------------------------- 1 | package geolite2 2 | 3 | import ( 4 | "net" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | "sync" 9 | 10 | "github.com/gin-gonic/gin" 11 | "github.com/oschwald/geoip2-golang" 12 | log "github.com/sirupsen/logrus" 13 | "github.com/xin053/ipd/config" 14 | "github.com/xin053/ipd/server" 15 | ) 16 | 17 | var ( 18 | DB2 *geoip2.Reader 19 | ) 20 | 21 | // IPdDb2 struct implement server.Server for geolite2 method 22 | type IPdDb2 struct{} 23 | 24 | // FromDb2 get ip information from Geolite2 database 25 | func FromDb2(c *gin.Context) { 26 | server.FindIPs(c, &IPdDb2{}) 27 | } 28 | 29 | // FindIP find information about a single IP 30 | func (i *IPdDb2) FindIP(ip string, ch chan config.IPWithGeo, wg *sync.WaitGroup) *config.IPWithGeo { 31 | if wg != nil { 32 | defer wg.Done() 33 | } 34 | 35 | ipWithGeo := &config.IPWithGeo{} 36 | ipWithGeo.IP = ip 37 | 38 | record, err := DB2.City(net.ParseIP(ip)) 39 | if err != nil { 40 | log.Error(err) 41 | return ipWithGeo 42 | } 43 | 44 | if len(record.Subdivisions) == 0 { 45 | return ipWithGeo 46 | } 47 | region := record.Subdivisions[0].Names["zh-CN"] 48 | region = strings.Replace(region, "省", "", -1) 49 | region = strings.Replace(region, "市", "", -1) 50 | region = strings.Replace(region, "自治区", "", -1) 51 | ipWithGeo = &config.IPWithGeo{ 52 | config.IPInfo{ 53 | IP: ip, 54 | Country: record.Country.Names["zh-CN"], 55 | Region: region, 56 | City: record.City.Names["zh-CN"], 57 | }, 58 | config.Geo{ 59 | GeoX: record.Location.Longitude, 60 | GeoY: record.Location.Latitude, 61 | }, 62 | } 63 | 64 | if ch != nil { 65 | ch <- *ipWithGeo 66 | } 67 | return ipWithGeo 68 | } 69 | 70 | // Register register server 71 | func (i *IPdDb2) Register(routine bool) { 72 | server.Register(i, routine) 73 | } 74 | 75 | func init() { 76 | dir, err := filepath.Abs(filepath.Dir(os.Args[0])) 77 | if err != nil { 78 | log.Error(err) 79 | } 80 | // 打开ip数据库文件, 通过监听 ctrl+c, kill -2等信号来关闭文件 81 | DB2, err = geoip2.Open(filepath.Join(dir, "GeoLite2-City.mmdb")) 82 | if err != nil { 83 | log.Error(err) 84 | return 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /ip2region.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xin053/ipd/b6cbe2a60190cdb8b6ff9c9423e8aede290967d7/ip2region.db -------------------------------------------------------------------------------- /ip2region/ip2region.go: -------------------------------------------------------------------------------- 1 | package ip2region 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | "sync" 10 | 11 | "github.com/gin-gonic/gin" 12 | log "github.com/sirupsen/logrus" 13 | "github.com/xin053/ipd/config" 14 | "github.com/xin053/ipd/server" 15 | "github.com/xin053/ipd/utils" 16 | ) 17 | 18 | var ( 19 | DB3 *Ip2Region 20 | ) 21 | 22 | // IPdDb3 struct implement server.Server for ip2region method 23 | type IPdDb3 struct{} 24 | 25 | // FromDb3 get ip information from ip2region database 26 | func FromDb3(c *gin.Context) { 27 | server.FindIPs(c, &IPdDb3{}) 28 | } 29 | 30 | // FindIP find information about a single IP 31 | func (i *IPdDb3) FindIP(ip string, ch chan config.IPWithGeo, wg *sync.WaitGroup) *config.IPWithGeo { 32 | if wg != nil { 33 | defer wg.Done() 34 | } 35 | 36 | ipWithGeo := &config.IPWithGeo{} 37 | ipWithGeo.IP = ip 38 | 39 | result, err := DB3.MemorySearch(ip) 40 | if err != nil { 41 | return ipWithGeo 42 | } 43 | 44 | ipInfo := config.IPInfo{ 45 | IP: ip, 46 | Country: strings.Replace(result.Country, "0", "", -1), 47 | Region: strings.Replace(result.Province, "0", "", -1), 48 | City: strings.Replace(result.City, "0", "", -1), 49 | ISP: strings.Replace(result.ISP, "0", "", -1), 50 | } 51 | 52 | ipWithGeo = utils.AddGeo(&ipInfo) 53 | if ch != nil { 54 | ch <- *ipWithGeo 55 | } 56 | return ipWithGeo 57 | } 58 | 59 | // Register register server 60 | func (i *IPdDb3) Register(routine bool) { 61 | server.Register(i, routine) 62 | } 63 | 64 | func init() { 65 | dir, err := filepath.Abs(filepath.Dir(os.Args[0])) 66 | if err != nil { 67 | log.Error(err) 68 | } 69 | os.Chdir(dir) 70 | // download ip2region.db from github 71 | log.Info("Initializing...please wait...") 72 | log.Info("downloading ip2region.db from github...") 73 | res, err := http.Get(config.IP2RegionURL) 74 | if err != nil { 75 | log.Warn("download ip2region.db from github failed, you should take a look at this.") 76 | log.Info("now, we will use ip2region.db at the workplace.") 77 | log.Error(err) 78 | } else { 79 | // remove ip2region.db if exits 80 | os.Remove("ip2region.db") 81 | f, err := os.Create("ip2region.db") 82 | if err != nil { 83 | log.Warn("creat ip2region.db file failed, you should take a look at this.") 84 | log.Error(err) 85 | } 86 | io.Copy(f, res.Body) 87 | log.Info("download finished, you can go on") 88 | } 89 | 90 | // 打开ip数据库文件, 通过监听 ctrl+c, kill -2等信号来关闭文件 91 | DB3, err = New(filepath.Join(dir, "ip2region.db")) 92 | if err != nil { 93 | log.Error(err) 94 | return 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /ip2region/lib.go: -------------------------------------------------------------------------------- 1 | package ip2region 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | "strconv" 7 | "io/ioutil" 8 | "errors" 9 | ) 10 | 11 | const ( 12 | INDEX_BLOCK_LENGTH = 12 13 | TOTAL_HEADER_LENGTH = 4096 14 | ) 15 | 16 | var err error 17 | var ipInfo IpInfo 18 | 19 | type Ip2Region struct { 20 | // db file handler 21 | dbFileHandler *os.File 22 | 23 | 24 | //header block info 25 | 26 | headerSip []int64 27 | headerPtr []int64 28 | headerLen int64 29 | 30 | // super block index info 31 | firstIndexPtr int64 32 | lastIndexPtr int64 33 | totalBlocks int64 34 | 35 | // for memory mode only 36 | // the original db binary string 37 | 38 | dbBinStr []byte 39 | dbFile string 40 | } 41 | 42 | type IpInfo struct { 43 | CityId int64 44 | Country string 45 | Region string 46 | Province string 47 | City string 48 | ISP string 49 | } 50 | 51 | func (ip IpInfo)String() string { 52 | return strconv.FormatInt(ip.CityId, 10) + "|" + ip.Country + "|" + ip.Region + "|" + ip.Province + "|" + ip.City + "|" + ip.ISP 53 | } 54 | 55 | func getIpInfo(cityId int64, line []byte) IpInfo { 56 | 57 | lineSlice := strings.Split(string(line), "|") 58 | ipInfo := IpInfo{} 59 | length := len(lineSlice) 60 | ipInfo.CityId = cityId 61 | if length < 5 { 62 | for i := 0; i <= 5 - length; i++ { 63 | lineSlice = append(lineSlice, "") 64 | } 65 | } 66 | 67 | ipInfo.Country = lineSlice[0] 68 | ipInfo.Region = lineSlice[1] 69 | ipInfo.Province = lineSlice[2] 70 | ipInfo.City = lineSlice[3] 71 | ipInfo.ISP = lineSlice[4] 72 | return ipInfo 73 | } 74 | 75 | func New(path string) (*Ip2Region, error) { 76 | 77 | file, err := os.Open(path) 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | return &Ip2Region{ 83 | dbFile:path, 84 | dbFileHandler:file, 85 | }, nil 86 | } 87 | 88 | func (this *Ip2Region) Close() { 89 | this.dbFileHandler.Close() 90 | } 91 | 92 | func (this *Ip2Region) MemorySearch(ipStr string) (ipInfo IpInfo, err error) { 93 | ipInfo = IpInfo{} 94 | 95 | if this.totalBlocks == 0 { 96 | this.dbBinStr, err = ioutil.ReadFile(this.dbFile) 97 | 98 | if err != nil { 99 | 100 | return ipInfo, err 101 | } 102 | 103 | this.firstIndexPtr = getLong(this.dbBinStr, 0) 104 | this.lastIndexPtr = getLong(this.dbBinStr, 4) 105 | this.totalBlocks = (this.lastIndexPtr - this.firstIndexPtr) / INDEX_BLOCK_LENGTH + 1 106 | } 107 | 108 | ip, err := ip2long(ipStr) 109 | if err != nil { 110 | return ipInfo, err 111 | } 112 | 113 | h := this.totalBlocks 114 | var dataPtr, l int64; 115 | for (l <= h) { 116 | 117 | m := (l + h) >> 1 118 | p := this.firstIndexPtr + m * INDEX_BLOCK_LENGTH 119 | sip := getLong(this.dbBinStr, p) 120 | if ip < sip { 121 | h = m - 1 122 | } else { 123 | eip := getLong(this.dbBinStr, p + 4) 124 | if ip > eip { 125 | l = m + 1 126 | } else { 127 | dataPtr = getLong(this.dbBinStr, p + 8) 128 | break; 129 | } 130 | } 131 | } 132 | if dataPtr == 0 { 133 | return ipInfo, errors.New("not found") 134 | } 135 | 136 | dataLen := ((dataPtr >> 24) & 0xFF) 137 | dataPtr = (dataPtr & 0x00FFFFFF); 138 | ipInfo = getIpInfo(getLong(this.dbBinStr, dataPtr), this.dbBinStr[(dataPtr) + 4:dataPtr + dataLen]) 139 | return ipInfo, nil 140 | 141 | } 142 | 143 | func (this *Ip2Region)BinarySearch(ipStr string) (ipInfo IpInfo, err error) { 144 | ipInfo = IpInfo{} 145 | if this.totalBlocks == 0 { 146 | this.dbFileHandler.Seek(0, 0) 147 | superBlock := make([]byte, 8) 148 | this.dbFileHandler.Read(superBlock) 149 | this.firstIndexPtr = getLong(superBlock, 0) 150 | this.lastIndexPtr = getLong(superBlock, 4) 151 | this.totalBlocks = (this.lastIndexPtr - this.firstIndexPtr) / INDEX_BLOCK_LENGTH + 1 152 | } 153 | 154 | var l, dataPtr, p int64 155 | 156 | h := this.totalBlocks 157 | 158 | ip, err := ip2long(ipStr) 159 | 160 | if err != nil { 161 | return 162 | } 163 | 164 | for (l <= h) { 165 | m := (l + h) >> 1 166 | 167 | p = m * INDEX_BLOCK_LENGTH 168 | 169 | _, err = this.dbFileHandler.Seek(this.firstIndexPtr + p, 0) 170 | if err != nil { 171 | return 172 | } 173 | 174 | buffer := make([]byte, INDEX_BLOCK_LENGTH) 175 | _, err = this.dbFileHandler.Read(buffer) 176 | 177 | if err != nil { 178 | 179 | } 180 | sip := getLong(buffer, 0) 181 | if ip < sip { 182 | h = m - 1 183 | } else { 184 | eip := getLong(buffer, 4) 185 | if ip > eip { 186 | l = m + 1 187 | } else { 188 | dataPtr = getLong(buffer, 8) 189 | break; 190 | } 191 | } 192 | 193 | } 194 | 195 | if dataPtr == 0 { 196 | err = errors.New("not found") 197 | return 198 | } 199 | 200 | dataLen := ((dataPtr >> 24) & 0xFF) 201 | dataPtr = (dataPtr & 0x00FFFFFF); 202 | 203 | this.dbFileHandler.Seek(dataPtr, 0) 204 | data := make([]byte, dataLen) 205 | this.dbFileHandler.Read(data) 206 | ipInfo = getIpInfo(getLong(data, 0), data[4:dataLen]) 207 | err = nil 208 | return 209 | } 210 | 211 | func (this *Ip2Region) BtreeSearch(ipStr string) (ipInfo IpInfo, err error) { 212 | ipInfo = IpInfo{} 213 | ip, err := ip2long(ipStr) 214 | 215 | if this.headerLen == 0 { 216 | this.dbFileHandler.Seek(8, 0) 217 | 218 | buffer := make([]byte, TOTAL_HEADER_LENGTH) 219 | this.dbFileHandler.Read(buffer) 220 | var idx int64; 221 | for i := 0; i < TOTAL_HEADER_LENGTH; i += 8 { 222 | startIp := getLong(buffer, int64(i)) 223 | dataPar := getLong(buffer, int64(i + 4)) 224 | if dataPar == 0 { 225 | break 226 | } 227 | 228 | this.headerSip = append(this.headerSip, startIp) 229 | this.headerPtr = append(this.headerPtr, dataPar) 230 | idx ++; 231 | } 232 | 233 | this.headerLen = idx 234 | } 235 | 236 | var l, sptr, eptr int64 237 | h := this.headerLen 238 | 239 | for l <= h { 240 | m := int64(l + h) >> 1 241 | if m < this.headerLen { 242 | if ip == this.headerSip[m] { 243 | if m > 0 { 244 | sptr = this.headerPtr[m - 1] 245 | eptr = this.headerPtr[m] 246 | } else { 247 | sptr = this.headerPtr[m] 248 | eptr = this.headerPtr[m + 1] 249 | } 250 | break 251 | } 252 | if ip < this.headerSip[m] { 253 | if m == 0 { 254 | sptr = this.headerPtr[m] 255 | eptr = this.headerPtr[m + 1] 256 | break 257 | } else if ip > this.headerSip[m - 1] { 258 | sptr = this.headerPtr[m - 1] 259 | eptr = this.headerPtr[m] 260 | break 261 | } 262 | h = m - 1 263 | } else { 264 | if m == this.headerLen - 1 { 265 | sptr = this.headerPtr[m - 1] 266 | eptr = this.headerPtr[m] 267 | break 268 | } else if ip <= this.headerSip[m + 1] { 269 | sptr = this.headerPtr[m ] 270 | eptr = this.headerPtr[m + 1] 271 | break 272 | } 273 | l = m + 1 274 | } 275 | } 276 | 277 | } 278 | 279 | if sptr == 0 { 280 | err = errors.New("not found") 281 | return 282 | } 283 | 284 | blockLen := eptr - sptr 285 | this.dbFileHandler.Seek(sptr, 0) 286 | index := make([]byte, blockLen + INDEX_BLOCK_LENGTH) 287 | this.dbFileHandler.Read(index) 288 | var dataptr int64 289 | h = blockLen / INDEX_BLOCK_LENGTH 290 | l = 0 291 | 292 | for l <= h { 293 | m := int64(l + h) >> 1 294 | p := m * INDEX_BLOCK_LENGTH 295 | sip := getLong(index, p) 296 | if ip < sip { 297 | h = m - 1; 298 | } else { 299 | eip := getLong(index, p + 4) 300 | if ip > eip { 301 | l = m + 1 302 | } else { 303 | dataptr = getLong(index, p + 8) 304 | break 305 | } 306 | } 307 | } 308 | 309 | if dataptr == 0 { 310 | err = errors.New("not found") 311 | return 312 | } 313 | 314 | dataLen := (dataptr >> 24) & 0xFF 315 | dataPtr := dataptr & 0x00FFFFFF 316 | 317 | this.dbFileHandler.Seek(dataPtr, 0) 318 | data := make([]byte, dataLen) 319 | this.dbFileHandler.Read(data) 320 | ipInfo = getIpInfo(getLong(data, 0), data[4:]) 321 | return 322 | } 323 | 324 | func getLong(b []byte, offset int64) int64 { 325 | 326 | val := (int64(b[offset ]) | 327 | int64(b[offset + 1]) << 8 | 328 | int64(b[offset + 2]) << 16 | 329 | int64(b[offset + 3]) << 24) 330 | 331 | return val 332 | 333 | } 334 | 335 | func ip2long(IpStr string) (int64, error) { 336 | bits := strings.Split(IpStr, ".") 337 | if len(bits) != 4 { 338 | return 0, errors.New("ip format error") 339 | } 340 | 341 | var sum int64 342 | for i, n := range bits { 343 | bit, _ := strconv.ParseInt(n, 10, 64) 344 | sum += bit << uint(24 - 8 * i) 345 | } 346 | 347 | return sum, nil 348 | } 349 | 350 | -------------------------------------------------------------------------------- /ipd.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "os/signal" 6 | "syscall" 7 | 8 | "github.com/getsentry/raven-go" 9 | "github.com/gin-gonic/gin" 10 | log "github.com/sirupsen/logrus" 11 | "github.com/xin053/ipd/api" 12 | "github.com/xin053/ipd/config" 13 | "github.com/xin053/ipd/es" 14 | "github.com/xin053/ipd/geolite2" 15 | "github.com/xin053/ipd/ip2region" 16 | "github.com/xin053/ipd/middleware" 17 | "github.com/xin053/ipd/qqwry" 18 | "github.com/xin053/ipd/server" 19 | ) 20 | 21 | var router *gin.Engine 22 | 23 | func main() { 24 | gin.SetMode(gin.ReleaseMode) 25 | router = gin.Default() 26 | 27 | router.Use(middleware.JSONLogMiddleware()) 28 | router.Use(middleware.AuthRequired()) 29 | router.Use(middleware.CORS(middleware.CORSOptions{})) 30 | if config.UseSentry { 31 | router.Use(middleware.Sentry(raven.DefaultClient, false)) 32 | } 33 | 34 | initRouters() 35 | 36 | log.Info("Service starting on port " + config.Port) 37 | 38 | signals := make(chan os.Signal) 39 | signal.Notify(signals, syscall.SIGINT) 40 | go func() { 41 | <-signals 42 | geolite2.DB2.Close() 43 | ip2region.DB3.Close() 44 | es.Client.Stop() 45 | log.Info("closing databases, preparing exit...bye") 46 | os.Exit(0) 47 | }() 48 | 49 | router.Run(":" + config.Port) 50 | } 51 | 52 | func initRouters() { 53 | v1 := router.Group("v1") 54 | { 55 | v1.POST("/db", qqwry.FromDb) 56 | v1.POST("/db2", geolite2.FromDb2) 57 | v1.POST("/db3", ip2region.FromDb3) 58 | v1.POST("/api", api.FromAPI) 59 | v1.POST("/ip", server.GetIP) 60 | } 61 | 62 | serverList := map[string]server.Server{ 63 | "ip2region": &ip2region.IPdDb3{}, 64 | "chunzhen": &qqwry.IPdDb{}, 65 | "geolite2": &geolite2.IPdDb2{}, 66 | "api": &api.IPdAPI{}, 67 | } 68 | // register server 69 | if len(config.RequestOrder) > 0 { 70 | serverList[config.RequestOrder[0]].Register(true) 71 | for _, server := range config.RequestOrder[1:len(config.RequestOrder)] { 72 | serverList[server].Register(false) 73 | } 74 | } 75 | } 76 | 77 | func init() { 78 | if config.UseSentry { 79 | raven.SetDSN(config.SentryDSN) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /middleware/auth.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/xin053/ipd/config" 6 | ) 7 | 8 | //AuthRequired Authorization middleware 9 | func AuthRequired() gin.HandlerFunc { 10 | return func(c *gin.Context) { 11 | authHeader := c.Request.Header.Get("Authorization") 12 | if authHeader == "" || authHeader != config.AuthKey { 13 | c.AbortWithStatus(401) 14 | return 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /middleware/cors.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | type CORSOptions struct { 6 | Origin string 7 | } 8 | 9 | // CORS middleware from https://github.com/gin-gonic/gin/issues/29#issuecomment-89132826 10 | func CORS(options CORSOptions) gin.HandlerFunc { 11 | return func(c *gin.Context) { 12 | c.Writer.Header().Set("Access-Control-Allow-Origin", "*") // allow any origin domain 13 | if options.Origin != "" { 14 | c.Writer.Header().Set("Access-Control-Allow-Origin", options.Origin) 15 | } 16 | c.Writer.Header().Set("Access-Control-Max-Age", "86400") 17 | c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE") 18 | c.Writer.Header().Set("Access-Control-Allow-Headers", "Origin, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization") 19 | c.Writer.Header().Set("Access-Control-Expose-Headers", "Content-Length") 20 | c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") 21 | 22 | if c.Request.Method == "OPTIONS" { 23 | c.AbortWithStatus(200) 24 | } else { 25 | c.Next() 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /middleware/json_logger.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | // "github.com/evalphobia/logrus_sentry" 8 | "github.com/gin-gonic/gin" 9 | log "github.com/sirupsen/logrus" 10 | // "github.com/xin053/ipd/config" 11 | "github.com/xin053/ipd/utils" 12 | ) 13 | 14 | // JSONLogMiddleware logs a gin HTTP request in JSON format, with some additional custom key/values 15 | func JSONLogMiddleware() gin.HandlerFunc { 16 | return func(c *gin.Context) { 17 | // Start timer 18 | start := time.Now() 19 | 20 | // Process Request 21 | c.Next() 22 | 23 | // Stop timer 24 | duration := fmt.Sprintf("%fms", utils.GetDurationInMillseconds(start)) 25 | 26 | entry := log.WithFields(log.Fields{ 27 | "client_ip": utils.GetClientIP(c), 28 | "duration": duration, 29 | "method": c.Request.Method, 30 | "path": c.Request.RequestURI, 31 | "status": c.Writer.Status(), 32 | }) 33 | 34 | statusCode := c.Writer.Status() 35 | switch { 36 | case statusCode >= 500: 37 | entry.Error(c.Errors.String()) 38 | case statusCode >= 400: 39 | entry.Warn("You should pay attention to this request IP.") 40 | default: 41 | entry.Info("") 42 | } 43 | } 44 | } 45 | 46 | // func init() { 47 | // if config.UseSentry { 48 | // hook, err := logrus_sentry.NewSentryHook(config.SentryDSN, []log.Level{ 49 | // log.PanicLevel, 50 | // log.FatalLevel, 51 | // log.ErrorLevel, 52 | // }) 53 | // hook.Timeout = 5 * time.Second 54 | 55 | // if err == nil { 56 | // log.AddHook(hook) 57 | // } 58 | // } 59 | // } 60 | -------------------------------------------------------------------------------- /middleware/sentry.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "runtime/debug" 7 | 8 | "github.com/getsentry/raven-go" 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | func Sentry(client *raven.Client, onlyCrashes bool) gin.HandlerFunc { 13 | 14 | return func(c *gin.Context) { 15 | defer func() { 16 | flags := map[string]string{ 17 | "endpoint": c.Request.RequestURI, 18 | } 19 | if rval := recover(); rval != nil { 20 | debug.PrintStack() 21 | rvalStr := fmt.Sprint(rval) 22 | client.CaptureMessage(rvalStr, flags, raven.NewException(errors.New(rvalStr), raven.NewStacktrace(2, 3, nil)), 23 | raven.NewHttp(c.Request)) 24 | c.AbortWithStatus(500) 25 | } 26 | if !onlyCrashes { 27 | for _, item := range c.Errors { 28 | client.CaptureMessage(item.Error(), flags, &raven.Message{ 29 | Message: item.Error(), 30 | Params: []interface{}{item.Meta}, 31 | }, 32 | raven.NewHttp(c.Request)) 33 | } 34 | } 35 | }() 36 | 37 | c.Next() 38 | } 39 | } -------------------------------------------------------------------------------- /qqwry.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xin053/ipd/b6cbe2a60190cdb8b6ff9c9423e8aede290967d7/qqwry.dat -------------------------------------------------------------------------------- /qqwry/qqwry.go: -------------------------------------------------------------------------------- 1 | package qqwry 2 | 3 | import ( 4 | "encoding/binary" 5 | "io/ioutil" 6 | "net" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | "sync" 11 | 12 | "github.com/axgle/mahonia" 13 | "github.com/gin-gonic/gin" 14 | log "github.com/sirupsen/logrus" 15 | "github.com/xin053/ipd/config" 16 | "github.com/xin053/ipd/server" 17 | "github.com/xin053/ipd/utils" 18 | ) 19 | 20 | const ( 21 | INDEX_LEN = 7 22 | REDIRECT_MODE_1 = 0x01 23 | REDIRECT_MODE_2 = 0x02 24 | ) 25 | 26 | var gqqwry *QQwry 27 | 28 | type QQwry struct { 29 | buff []byte 30 | start uint32 31 | end uint32 32 | } 33 | 34 | // IPdDb struct implement server.Server for chunzhen method 35 | type IPdDb struct{} 36 | 37 | func NewQQwry(file string) (qqwry *QQwry) { 38 | qqwry = &QQwry{} 39 | f, e := os.Open(file) 40 | if e != nil { 41 | log.Error(e) 42 | return nil 43 | } 44 | defer f.Close() 45 | qqwry.buff, e = ioutil.ReadAll(f) 46 | if e != nil { 47 | log.Error(e) 48 | return nil 49 | } 50 | qqwry.start = binary.LittleEndian.Uint32(qqwry.buff[:4]) 51 | qqwry.end = binary.LittleEndian.Uint32(qqwry.buff[4:8]) 52 | return qqwry 53 | } 54 | 55 | // FromDb get ip information from chunzhen ip database 56 | func FromDb(c *gin.Context) { 57 | server.FindIPs(c, &IPdDb{}) 58 | } 59 | 60 | // FindIP find information about a single IP 61 | func (i *IPdDb) FindIP(ip string, ch chan config.IPWithGeo, wg *sync.WaitGroup) *config.IPWithGeo { 62 | if wg != nil { 63 | defer wg.Done() 64 | } 65 | 66 | ipWithGeo := &config.IPWithGeo{} 67 | ipWithGeo.IP = ip 68 | if gqqwry.buff == nil { 69 | return ipWithGeo 70 | } 71 | 72 | var country []byte 73 | var area []byte 74 | ip1 := net.ParseIP(ip) 75 | if ip1 == nil { 76 | return ipWithGeo 77 | } 78 | offset := gqqwry.searchRecord(binary.BigEndian.Uint32(ip1.To4())) 79 | if offset <= 0 { 80 | return ipWithGeo 81 | } 82 | mode := gqqwry.readMode(offset + 4) 83 | if mode == REDIRECT_MODE_1 { 84 | countryOffset := gqqwry.readUint32FromByte3(offset + 5) 85 | 86 | mode = gqqwry.readMode(countryOffset) 87 | if mode == REDIRECT_MODE_2 { 88 | c := gqqwry.readUint32FromByte3(countryOffset + 1) 89 | country = gqqwry.readString(c) 90 | countryOffset += 4 91 | area = gqqwry.readArea(countryOffset) 92 | 93 | } else { 94 | country = gqqwry.readString(countryOffset) 95 | countryOffset += uint32(len(country) + 1) 96 | area = gqqwry.readArea(countryOffset) 97 | } 98 | 99 | } else if mode == REDIRECT_MODE_2 { 100 | countryOffset := gqqwry.readUint32FromByte3(offset + 5) 101 | country = gqqwry.readString(countryOffset) 102 | area = gqqwry.readArea(offset + 8) 103 | } 104 | enc := mahonia.NewDecoder("gbk") 105 | region := enc.ConvertString(string(country)) 106 | 107 | ipInfo := config.IPInfo{IP: ip} 108 | ipInfo.ISP = enc.ConvertString(string(area)) 109 | 110 | if strings.Contains(region, "市") || strings.Contains(region, "省") || strings.Contains(region, "区") { 111 | ipInfo.Country = "中国" 112 | if strings.Contains(region, "省") { 113 | if s := strings.Split(region, "省"); len(s) == 2 { 114 | ipInfo.Region = s[0] 115 | ipInfo.City = s[1] 116 | } else { 117 | ipInfo.Region = s[0] 118 | } 119 | } 120 | } else { 121 | ipInfo.Country = region 122 | } 123 | 124 | ipWithGeo = utils.AddGeo(&ipInfo) 125 | if ch != nil { 126 | ch <- *ipWithGeo 127 | } 128 | return ipWithGeo 129 | } 130 | 131 | // Register register server 132 | func (i *IPdDb) Register(routine bool) { 133 | server.Register(i, routine) 134 | } 135 | 136 | func (q *QQwry) readUint32FromByte3(offset uint32) uint32 { 137 | return byte3ToUInt32(q.buff[offset : offset+3]) 138 | } 139 | 140 | func (q *QQwry) readMode(offset uint32) byte { 141 | return q.buff[offset : offset+1][0] 142 | } 143 | 144 | func (q *QQwry) readString(offset uint32) []byte { 145 | 146 | i := 0 147 | for { 148 | 149 | if q.buff[int(offset)+i] == 0 { 150 | break 151 | } else { 152 | i++ 153 | } 154 | 155 | } 156 | return q.buff[offset : int(offset)+i] 157 | } 158 | 159 | func (q *QQwry) readArea(offset uint32) []byte { 160 | mode := q.readMode(offset) 161 | if mode == REDIRECT_MODE_1 || mode == REDIRECT_MODE_2 { 162 | areaOffset := q.readUint32FromByte3(offset + 1) 163 | if areaOffset == 0 { 164 | return []byte("") 165 | } else { 166 | return q.readString(areaOffset) 167 | } 168 | } else { 169 | return q.readString(offset) 170 | } 171 | } 172 | 173 | func (q *QQwry) getRecord(offset uint32) []byte { 174 | return q.buff[offset : offset+INDEX_LEN] 175 | } 176 | 177 | func (q *QQwry) getIPFromRecord(buf []byte) uint32 { 178 | return binary.LittleEndian.Uint32(buf[:4]) 179 | } 180 | 181 | func (q *QQwry) getAddrFromRecord(buf []byte) uint32 { 182 | return byte3ToUInt32(buf[4:7]) 183 | } 184 | 185 | func (q *QQwry) searchRecord(ip uint32) uint32 { 186 | 187 | start := q.start 188 | end := q.end 189 | 190 | // log.Printf("len info %v, %v ---- %v, %v", start, end, hex.EncodeToString(header[:4]), hex.EncodeToString(header[4:])) 191 | for { 192 | mid := q.getMiddleOffset(start, end) 193 | buf := q.getRecord(mid) 194 | _ip := q.getIPFromRecord(buf) 195 | 196 | // log.Printf(">> %v, %v, %v -- %v", start, mid, end, hex.EncodeToString(buf[:4])) 197 | 198 | if end-start == INDEX_LEN { 199 | //log.Printf(">> %v, %v, %v -- %v", start, mid, end, hex.EncodeToString(buf[:4])) 200 | offset := q.getAddrFromRecord(buf) 201 | buf = q.getRecord(mid + INDEX_LEN) 202 | if ip < q.getIPFromRecord(buf) { 203 | return offset 204 | } else { 205 | return 0 206 | } 207 | } 208 | 209 | // 找到的比较大,向前移 210 | if _ip > ip { 211 | end = mid 212 | } else if _ip < ip { // 找到的比较小,向后移 213 | start = mid 214 | } else if _ip == ip { 215 | return byte3ToUInt32(buf[4:7]) 216 | } 217 | 218 | } 219 | } 220 | 221 | func (q *QQwry) getMiddleOffset(start uint32, end uint32) uint32 { 222 | records := ((end - start) / INDEX_LEN) >> 1 223 | return start + records*INDEX_LEN 224 | } 225 | 226 | func byte3ToUInt32(data []byte) uint32 { 227 | i := uint32(data[0]) & 0xff 228 | i |= (uint32(data[1]) << 8) & 0xff00 229 | i |= (uint32(data[2]) << 16) & 0xff0000 230 | return i 231 | } 232 | 233 | func init() { 234 | dir, err := filepath.Abs(filepath.Dir(os.Args[0])) 235 | if err != nil { 236 | log.Error(err) 237 | } 238 | gqqwry = NewQQwry(filepath.Join(dir, "qqwry.dat")) 239 | } 240 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "net" 5 | "sync" 6 | 7 | "github.com/allegro/bigcache" 8 | "github.com/gin-gonic/gin" 9 | jsoniter "github.com/json-iterator/go" 10 | log "github.com/sirupsen/logrus" 11 | "github.com/xin053/ipd/config" 12 | "github.com/xin053/ipd/es" 13 | ) 14 | 15 | var ( 16 | json = jsoniter.ConfigCompatibleWithStandardLibrary 17 | ServerList = [2][]Server{} 18 | ) 19 | 20 | // Server you can register your own ip recognition service by implement this interface 21 | type Server interface { 22 | // FindIP find information about a single IP 23 | FindIP(ip string, ch chan config.IPWithGeo, wg *sync.WaitGroup) *config.IPWithGeo 24 | // Register register server 25 | Register(routine bool) 26 | } 27 | 28 | // Register register server, only one server can set the routine param to true 29 | func Register(server Server, routine bool) { 30 | if routine { 31 | if len(ServerList[0]) == 1 { 32 | log.Warn("you already have a server set to true at ServerList, this server will be set false.") 33 | ServerList[1] = append(ServerList[1], server) 34 | return 35 | } 36 | ServerList[0] = append(ServerList[0], server) 37 | } else { 38 | ServerList[1] = append(ServerList[1], server) 39 | } 40 | } 41 | 42 | func solveIP(ips []string) ([]string, []config.IPWithGeo) { 43 | var ipWithGeoCache = []config.IPWithGeo{} 44 | var ipList []string 45 | for _, ip := range ips { 46 | if net.ParseIP(ip) == nil { 47 | log.Warnf("'%s' is not a valid ip, you should pay attention to this ip", ip) 48 | continue 49 | } 50 | if entry, err := config.Cache.Get(ip); err != nil { 51 | if err, ok := err.(*bigcache.EntryNotFoundError); ok { 52 | } else { 53 | log.Error(err) 54 | } 55 | } else { 56 | var data config.IPWithGeo 57 | if err := json.Unmarshal(entry, &data); err != nil { 58 | log.Error(err) 59 | } 60 | ipWithGeoCache = append(ipWithGeoCache, data) 61 | continue 62 | } 63 | ipList = append(ipList, ip) 64 | } 65 | return ipList, ipWithGeoCache 66 | } 67 | 68 | func cacheIP(ipWithGeoList []config.IPWithGeo) { 69 | for _, ipWithGeo := range ipWithGeoList { 70 | ipWithGeoJSON, err := json.Marshal(ipWithGeo) 71 | if err != nil { 72 | log.Error(err) 73 | } 74 | config.Cache.Set(ipWithGeo.IP, ipWithGeoJSON) 75 | } 76 | } 77 | 78 | // FindIPs get information about a list of ip by server asynchronously 79 | func FindIPs(c *gin.Context, server Server) { 80 | var ipJSON config.IP 81 | err := c.BindJSON(&ipJSON) 82 | if err != nil { 83 | log.Error("missing ip params or format incorrect") 84 | return 85 | } 86 | 87 | ipList, ipWithGeoCache := solveIP(ipJSON.IP) 88 | 89 | var wg sync.WaitGroup 90 | var ch = make(chan config.IPWithGeo, config.IPChanelBuf) 91 | wg.Add(len(ipList)) 92 | for _, ip := range ipList { 93 | go server.FindIP(ip, ch, &wg) 94 | } 95 | wg.Wait() 96 | close(ch) 97 | 98 | var ipWithGeoList = []config.IPWithGeo{} 99 | for ipWithGeo := range ch { 100 | ipWithGeoList = append(ipWithGeoList, ipWithGeo) 101 | } 102 | 103 | message, err := json.Marshal(append(ipWithGeoCache, ipWithGeoList...)) 104 | if err != nil { 105 | log.Error(err) 106 | return 107 | } 108 | 109 | c.String(200, string(message)) 110 | 111 | // cache these ip 112 | go cacheIP(ipWithGeoList) 113 | 114 | // store these ip 115 | if config.StoreES { 116 | go es.Store(ipWithGeoList...) 117 | } 118 | } 119 | 120 | // GetIP get information about a list of ip by ServerList 121 | // first, ip2region asynchronously; then chunzhe; then geoip2; at last, query by api 122 | func GetIP(c *gin.Context) { 123 | var ipJSON config.IP 124 | err := c.BindJSON(&ipJSON) 125 | if err != nil { 126 | log.Error("missing ip params or format incorrect") 127 | return 128 | } 129 | 130 | ipList, ipWithGeoCache := solveIP(ipJSON.IP) 131 | 132 | var wg sync.WaitGroup 133 | var ch = make(chan config.IPWithGeo, config.IPChanelBuf) 134 | wg.Add(len(ipList)) 135 | ipMap := map[string]bool{} 136 | for _, ip := range ipList { 137 | ipMap[ip] = false 138 | go ServerList[0][0].FindIP(ip, ch, &wg) 139 | } 140 | wg.Wait() 141 | close(ch) 142 | 143 | var ipWithGeoList = []config.IPWithGeo{} 144 | for ipWithGeo := range ch { 145 | ipMap[ipWithGeo.IP] = true 146 | if ipWithGeo.GeoX == 0 && ipWithGeo.GeoY == 0 { 147 | ipWithGeo = *fromList1(ipWithGeo.IP) 148 | } 149 | ipWithGeoList = append(ipWithGeoList, ipWithGeo) 150 | } 151 | 152 | // things left 153 | for ip, exits := range ipMap { 154 | if !exits { 155 | ipWithGeo := fromList1(ip) 156 | ipWithGeoList = append(ipWithGeoList, *ipWithGeo) 157 | } 158 | } 159 | 160 | message, err := json.Marshal(append(ipWithGeoCache, ipWithGeoList...)) 161 | if err != nil { 162 | log.Error(err) 163 | return 164 | } 165 | 166 | c.String(200, string(message)) 167 | 168 | // cache these ip 169 | go cacheIP(ipWithGeoList) 170 | 171 | // store these ip 172 | if config.StoreES { 173 | go es.Store(ipWithGeoList...) 174 | } 175 | } 176 | 177 | func fromList1(ip string) *config.IPWithGeo { 178 | ipWithGeo := &config.IPWithGeo{} 179 | ipWithGeo.IP = ip 180 | for _, server := range ServerList[1] { 181 | ipWithGeo = server.FindIP(ip, nil, nil) 182 | if ipWithGeo.GeoX != 0 || ipWithGeo.GeoY != 0 { 183 | return ipWithGeo 184 | } 185 | } 186 | return ipWithGeo 187 | } 188 | -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "strings" 5 | "time" 6 | 7 | "github.com/gin-gonic/gin" 8 | log "github.com/sirupsen/logrus" 9 | "github.com/xin053/ipd/config" 10 | ) 11 | 12 | // GetDurationInMillseconds takes a start time and returns a duration in milliseconds 13 | func GetDurationInMillseconds(start time.Time) float64 { 14 | end := time.Now() 15 | duration := end.Sub(start) 16 | milliseconds := float64(duration) / float64(time.Millisecond) 17 | rounded := float64(int(milliseconds*100+0.5)) / 100 18 | return rounded 19 | } 20 | 21 | // GetClientIP gets the correct IP for the end client instead of the proxy 22 | func GetClientIP(c *gin.Context) string { 23 | // first check the X-Forwarded-For header 24 | requester := c.Request.Header.Get("X-Forwarded-For") 25 | // if empty, check the Real-IP header 26 | if len(requester) == 0 { 27 | requester = c.Request.Header.Get("X-Real-IP") 28 | } 29 | // if the requester is still empty, use the hard-coded address from the socket 30 | if len(requester) == 0 { 31 | requester = c.Request.RemoteAddr 32 | } 33 | 34 | // if requester is a comma delimited list, take the first one 35 | // (this happens when proxied via elastic load balancer then again through nginx) 36 | if strings.Contains(requester, ",") { 37 | requester = strings.Split(requester, ",")[0] 38 | } 39 | 40 | return requester 41 | } 42 | 43 | // EmptyStrings whether all the given strings are empty or not 44 | func EmptyStrings(s ...string) bool { 45 | for _, i := range s { 46 | if i != "" { 47 | return false 48 | } 49 | } 50 | return true 51 | } 52 | 53 | // AddGeo add geo information to IPInfo struct 54 | func AddGeo(ipInfo *config.IPInfo) *config.IPWithGeo { 55 | ipInfo.City = strings.Replace(ipInfo.City, "市", "", -1) 56 | if geo, ok := config.GeoMap[ipInfo.City]; !ok { 57 | // get geo info by Region 58 | ipInfo.Region = strings.Replace(ipInfo.Region, "省", "", -1) 59 | ipInfo.Region = strings.Replace(ipInfo.Region, "市", "", -1) 60 | ipInfo.Region = strings.Replace(ipInfo.Region, "自治区", "", -1) 61 | if geo, ok := config.GeoMap[ipInfo.Region]; !ok { 62 | log.Warn("can not get geo information of: ", ipInfo.Region, ipInfo.Country, ipInfo.IP) 63 | if ipInfo.Country == "中国" || strings.ToLower(ipInfo.Country) == "china" { 64 | log.Warn("this chinese ip has no region or city: ", ipInfo.IP) 65 | } 66 | return &config.IPWithGeo{*ipInfo, config.Geo{0, 0}} 67 | } else { 68 | ipInfo.Country = "中国" 69 | return &config.IPWithGeo{*ipInfo, geo} 70 | } 71 | log.Warn("can not get geo information of: ", ipInfo.City, ipInfo.Region, ipInfo.Country, ipInfo.IP) 72 | return &config.IPWithGeo{*ipInfo, config.Geo{0, 0}} 73 | } else { 74 | // get geo info by city 75 | ipInfo.Country = "中国" 76 | ipInfo.Region = strings.Replace(ipInfo.Region, "省", "", -1) 77 | ipInfo.Region = strings.Replace(ipInfo.Region, "市", "", -1) 78 | ipInfo.Region = strings.Replace(ipInfo.Region, "自治区", "", -1) 79 | return &config.IPWithGeo{*ipInfo, geo} 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /utils/utils_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/xin053/ipd/config" 8 | ) 9 | 10 | func TestEmptyStrings(t *testing.T) { 11 | type args struct { 12 | s []string 13 | } 14 | tests := []struct { 15 | name string 16 | args args 17 | want bool 18 | }{ 19 | { 20 | "test1", 21 | args{[]string{"", "123"}}, 22 | false, 23 | }, 24 | { 25 | "test2", 26 | args{[]string{"", "", "", ""}}, 27 | true, 28 | }, 29 | } 30 | for _, tt := range tests { 31 | t.Run(tt.name, func(t *testing.T) { 32 | if got := EmptyStrings(tt.args.s...); got != tt.want { 33 | t.Errorf("EmptyStrings() = %v, want %v", got, tt.want) 34 | } 35 | }) 36 | } 37 | } 38 | 39 | func TestAddGeo(t *testing.T) { 40 | type args struct { 41 | ipInfo *config.IPInfo 42 | } 43 | tests := []struct { 44 | name string 45 | args args 46 | want config.IPWithGeo 47 | }{ 48 | { 49 | "test1", 50 | args{&config.IPInfo{ 51 | "183.238.58.120", 52 | "中国", 53 | "广东", 54 | "", 55 | "", 56 | }}, 57 | config.IPWithGeo{ 58 | config.IPInfo{ 59 | "183.238.58.120", 60 | "中国", 61 | "广东", 62 | "", 63 | "", 64 | }, 65 | config.Geo{ 66 | 113.23, 67 | 23.16, 68 | }, 69 | }, 70 | }, 71 | { 72 | "test2", 73 | args{&config.IPInfo{ 74 | "183.238.58.120", 75 | "", 76 | "", 77 | "广州", 78 | "", 79 | }}, 80 | config.IPWithGeo{ 81 | config.IPInfo{ 82 | "183.238.58.120", 83 | "中国", 84 | "", 85 | "广州", 86 | "", 87 | }, 88 | config.Geo{ 89 | 113.23, 90 | 23.16, 91 | }, 92 | }, 93 | }, 94 | } 95 | for _, tt := range tests { 96 | t.Run(tt.name, func(t *testing.T) { 97 | if got := AddGeo(tt.args.ipInfo); !reflect.DeepEqual(*got, tt.want) { 98 | t.Errorf("AddGeo() = %v, want %v", *got, tt.want) 99 | } 100 | }) 101 | } 102 | } 103 | --------------------------------------------------------------------------------