├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── README_EN.md ├── build_exec.sh ├── build_image.sh ├── cmd └── opennotrd │ └── main.go ├── coverage.sh ├── doc ├── README.md ├── assets │ ├── httpdemo.jpg │ ├── logo.jpg │ ├── logo.png │ ├── opennotr.jpg │ ├── qrcode.jpg │ ├── tcpdemo.jpg │ └── udpdemo.jpg └── zh │ ├── opennotr技术实现细节.md │ └── opennotr技术架构.md ├── docker-build ├── Dockerfile ├── docker-compose.yaml └── start.sh ├── etc ├── opennotr.yaml └── opennotrd.yaml ├── example └── README.md ├── go.mod ├── go.sum ├── internal ├── logs │ ├── README.md │ ├── alils │ │ ├── alils.go │ │ ├── config.go │ │ ├── log.pb.go │ │ ├── log_config.go │ │ ├── log_project.go │ │ ├── log_store.go │ │ ├── machine_group.go │ │ ├── request.go │ │ └── signature.go │ ├── color.go │ ├── color_windows.go │ ├── color_windows_test.go │ ├── conn.go │ ├── conn_test.go │ ├── console.go │ ├── console_test.go │ ├── es │ │ └── es.go │ ├── file.go │ ├── file_test.go │ ├── jianliao.go │ ├── log.go │ ├── logger.go │ ├── logger_test.go │ ├── multifile.go │ ├── multifile_test.go │ ├── slack.go │ ├── smtp.go │ └── smtp_test.go └── proto │ └── cs.go ├── opennotr ├── client.go ├── config.go └── main.go └── opennotrd ├── core ├── config.go ├── dhcp.go ├── net.go ├── resolver.go ├── server.go ├── session.go ├── tcpforward.go ├── tcpforward_test.go └── udpforward.go ├── plugin ├── dummy │ └── dummy.go ├── plugin.go ├── restyproxy │ └── restyproxy.go ├── tcpproxy │ └── tcpproxy.go └── udpproxy │ └── udpproxy.go ├── plugins.go ├── run.go └── test ├── httpecho ├── httpclient.go └── httpserver.go ├── plugin_test.go ├── tcpecho ├── tcpclient.go └── tcpserver.go ├── udpecho ├── client.go └── server.go └── websocket ├── wsclient.go └── wsserver.go /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | *.log 3 | *_temp 4 | *.prof 5 | *.pprof 6 | *.svg 7 | *.txt 8 | *.out -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.13 4 | before_install: 5 | install: 6 | - go get github.com/ICKelin/opennotr/opennotrd 7 | - go get github.com/ICKelin/opennotr/opennotr 8 | before_script: 9 | script: 10 | - cd $HOME/gopath/src/github.com/ICKelin/opennotr 11 | - chmod +x coverage.sh 12 | # - ./coverage.sh 13 | - chmod +x build_exec.sh 14 | - ./build_exec.sh 15 | 16 | after_success: 17 | - bash <(curl -s https://codecov.io/bash) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 ICKelin 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 |

2 | 3 |

4 | 5 | 6 |

7 | 8 | 9 | 10 | 11 | go report 12 | 13 | 14 | 15 | Build Status 16 | 17 | 18 | license 19 | 20 |

21 | 22 | [English](README_EN.md) | 简体中文 23 | 24 | ## 介绍 25 | 26 | **状态: Stable** 27 | 28 | **[在线试用](https://github.com/ICKelin/opennotr/wiki/opennotr%E5%9C%A8%E7%BA%BF%E4%BD%93%E9%AA%8C)** 29 | 30 | opennotr是一款开源的内网穿透软件,内部使用`透明代理`实现流量劫持,同时,基于虚拟IP构建虚拟局域网,整体技术与k8s的service技术类似,虚拟IP与service ip类似,只在opennotr的服务端所在的机器可以访问,在其他任何位置都无法访问该虚拟ip。 31 | 32 | opennotr支持多种协议,包括http,https,grpc,tcp,udp,为了实现http,https,grpc协议端口复用,opennotr引入了openresty作为网关,多个客户端不同域名可以共享http的80,https的443端口,不需要额外的端口。 33 | 34 | opennotr支持定制化插件,我们内置了http, https, grpc, tcp, udp代理,可以覆盖大部分场景,同时,opennotr允许自己开发协议插件,比如说,如果您希望使用apisix来作为前置的网关,您可以开发opennotr的apisix插件,opennotr会把一些基本信息传递给插件,其余功能均由插件自己进行。 35 | 36 | opennotr内置的协议也是以插件的形式存在的,只是默认导入到程序当中。 37 | 38 | ## 目录 39 | - [介绍](#介绍) 40 | - [功能特性](#功能特性) 41 | - [opennotr的技术原理](#opennotr的技术原理) 42 | - [如何开始使用](#如何开始使用) 43 | - [安装opennotrd](#安装opennotrd) 44 | - [运行opennotr](#运行opennotr) 45 | - [相关文章与视频](#相关文章和视频) 46 | - [插件开发](#插件开发) 47 | - [有问题怎么办](#有问题怎么办) 48 | - [关于作者](#关于作者) 49 | 50 | ## 功能特性 51 | 52 | - 支持多种协议,可覆盖大部分内网穿透场景 53 | - 支持定制化插件,opennotr提供一个虚拟局域网的基础功能,您开发的插件运行在这个局域网内 54 | - 引入openresty作为网关,网络处理性能问题可以得到保证 55 | - 支持动态域名解析,可支持引入coredns作为dns的nameserver。 56 | 57 | ## opennotr的技术原理 58 | 59 | ![opennotr.jpg](doc/assets/opennotr.jpg) 60 | 61 | 最下层是客户端所在的机器/设备,同时也作为虚拟局域网当中的一台机器,具备该局域网的IP地址`100.64.240.100` 62 | 63 | 最上层是服务端所在的机器,同时也作为虚拟局域网当中的网关,具备该局域网的IP地址`100.64.240.1`,opennotr提供能够在`Public Cloud`层可通过`100.64.240.100`访问`Device/PC behind Routed`的能力。在这个能力的基础之上构建整个内网穿透体系。 64 | 65 | 上图中,虚拟IP不存在任何网卡,只是内部虚拟出来的一个ip,便于流量劫持,opennotr通过透明代理,将访问虚拟ip的流量转到本机监听的端口,然后查找该虚拟ip对应的客户端长连接,将流量通过长连接下发到客户端,从而实现穿透。 66 | 67 | 上图中,openresty是基于我openresty开发的http网关,目的是支持动态upstream,可通过接口增删upstream,感兴趣的可以查看[resty-upstream](https://github.com/ICKelin/resty-upstream),当有客户端连接上来时,会调用`resty-upstream`提供的接口新增配置,当客户端断开连接时,会调用`resty-upstream`提供的删除接口删除配置。 68 | 69 | 从整个系统的层面,openresty的upstream的地址为opennotr客户端分配的lan地址,至于如何与客户端lan地址通信,这是opennotr处理的事情,对于openresty而言是透明的。 70 | 71 | 上图中,针对tcp和udp,opennotr倒是没有使用openresty的功能,而是自己开发的代理程序,当前也是集成在opennotrd程序当中,具体可参考以下两个文件。 72 | 73 | - [tcpproxy.go](https://github.com/ICKelin/opennotr/blob/master/opennotrd/plugin/tcpproxy/tcpproxy.go) 74 | - [udpproxy.go](https://github.com/ICKelin/opennotr/blob/master/opennotrd/plugin/udpproxy/udpproxy.go) 75 | 76 | 不使用openresty处理tcp和udp主要基于以下考虑: 77 | 78 | - openresty本身的stream功能并不能满足要求,这里的场景是事先不知道需要监听哪个端口,而openresty的server配置块需要预先知道监听的端口,否则需要重写配置再reload。 79 | 80 | - openresty的stream模块和http是分割的,原本使用http接口实现的动态upstream针对tcp并不生效,因此需要使用lua的tcp编程接受请求,作者自身对lua和openresty了解不深,不贸贸然行动。 81 | 82 | - 不使用openresty构建tcp/udp代理难度并不大,http使用openresty是为了端口复用,根据host区分,而tcp则没有类似的机制,也就是说,即使用openresty,也是单独的端口。 83 | 84 | 后续有兴趣的同学可以考虑将openresty完全换成golang实现,或者将golang的tcp/udp部分换成openresty。笔者注意到市面上已经有类似的实现,比如[apache apisix](https://github.com/apache/apisix)是使用的openresty实现的http,tcp,udp网关,这个是很不错的项目,我的[resty-upstream](https://github.com/ICKelin/resty-upstream)项目大部分都是从该项目学习而来 85 | 86 | [返回目录](#目录) 87 | 88 | ## 如何使用 89 | 90 | opennotr包括服务端程序`opennotrd`和客户端程序`opennotr` 91 | 92 | ### 安装opennotrd 93 | 94 | **强烈建议使用docker运行opennotrd** 95 | 96 | 为了运行opennotrd,您需要准备好程序运行的配置文件和tls证书等信息,以下为笔者的一个示例 97 | 98 | ``` 99 | root@iZwz97kfjnf78copv1ae65Z:/opt/data/opennotrd# tree 100 | . 101 | |-- cert ---------------------> 证书,秘钥目录 102 | | |-- upstream.crt 103 | | `-- upstream.key 104 | `-- notrd.yaml ---------------> opennotrd启动的配置文件 105 | 106 | 2 directories, 5 files 107 | ``` 108 | 109 | 这里主要关注`notrd.yaml`文件的内容,该文件是opennotrd运行时的配置文件. 110 | 111 | ```yml 112 | server: 113 | listen: ":10100" 114 | authKey: "client server exchange key" 115 | domain: "open.notr.tech" 116 | 117 | tcpforward: 118 | listen: ":4398" 119 | 120 | udpforward: 121 | listen: ":4399" 122 | 123 | dhcp: 124 | cidr: "100.64.242.1/24" 125 | ip: "100.64.242.1" 126 | 127 | plugin: 128 | tcp: | 129 | {} 130 | 131 | udp: | 132 | {} 133 | 134 | http: | 135 | { 136 | "adminUrl": "http://127.0.0.1:81/upstreams" 137 | } 138 | 139 | https: | 140 | { 141 | "adminUrl": "http://127.0.0.1:81/upstreams" 142 | } 143 | 144 | h2c: | 145 | { 146 | "adminUrl": "http://127.0.0.1:81/upstreams" 147 | } 148 | ``` 149 | 150 | 大部分情况下,上述配置只需要调整`server`区块相关配置,其他保留默认即可。 151 | 152 | 准备好配置之后运行以下命令即可开始启动: 153 | 154 | `docker run --privileged --net=host -v /opt/logs/opennotr:/opt/resty-upstream/logs -v /opt/data/opennotrd:/opt/conf -d ickelin/opennotr:v1.0.4` 155 | 156 | 需要配置volume,将主机目录`/opt/data/opennotrd`挂载到容器的`/opt/conf`当中,`/opt/data/opennotrd`为在上一步当中创建的配置文件,证书目录。 157 | 158 | 或者您也可以使用docker-compose启动 159 | 160 | ``` 161 | wget https://github.com/ICKelin/opennotr/blob/master/docker-build/docker-compose.yaml 162 | 163 | docker-compose up -d opennotrd 164 | ``` 165 | 166 | ### 运行opennotr 167 | opennotr的启动比较简单,首先需要准备配置. 168 | 169 | ```yaml 170 | serverAddr: "demo.notr.tech:10100" 171 | key: "client server exchange key" 172 | domain: "cloud.dahuizong.com" 173 | 174 | # 转发表信息 175 | # ports字段解析: 公网端口: 本机监听的端口 176 | forwards: 177 | - protocol: tcp 178 | ports: 179 | 2222: 2222 180 | 181 | - protocol: udp 182 | ports: 183 | 53: 53 184 | 185 | - protocol: http 186 | ports: 187 | 0: 8080 188 | 189 | - protocol: https 190 | ports: 191 | 0: 8081 192 | 193 | - protocol: h2c 194 | ports: 195 | 0: 50052 196 | 197 | ``` 198 | 199 | 配置准备好之后,启动opennotr即可 200 | 201 | `./opennotr -conf config.yaml` 202 | 203 | 204 | [返回目录](#目录) 205 | 206 | ### 相关文章和视频 207 | 208 | - [opennotr基本用法](https://www.zhihu.com/zvideo/1348958178885963776) 209 | - [opennotr进阶-使用域名](https://www.zhihu.com/zvideo/1357007720181293056) 210 | 211 | ## 插件开发 212 | 要开发opennotr支持的插件,您需要实现以下接口: 213 | 214 | ```golang 215 | 216 | // IPlugin defines plugin interface 217 | // Plugin should implements the IPlugin 218 | type IPlugin interface { 219 | // Setup calls at the begin of plugin system initialize 220 | // plugin system will pass the raw message to plugin's Setup function 221 | Setup(json.RawMessage) error 222 | 223 | // Close a proxy, it may be called by client's connection close 224 | StopProxy(item *PluginMeta) 225 | 226 | // Run a proxy, it may be called by client's connection established 227 | RunProxy(item *PluginMeta) error 228 | } 229 | 230 | ``` 231 | 232 | `Setup`函数负责初始化您的插件,由插件管理程序调用,开发者无需手动调用,参数是插件运行需要的配置,格式为`json`格式 233 | 234 | `RunProxy`和`StopProxy`也是由我们插件管理程序调用的,需要由开发者自己实现。 235 | 236 | 实现以上三个接口之后,需要在插件当中调用注册函数,将插件注册到系统当中,比如: 237 | 238 | ```golang 239 | package tcpproxy 240 | 241 | import ( 242 | "encoding/json" 243 | "fmt" 244 | 245 | "github.com/ICKelin/opennotr/opennotrd/plugin" 246 | ) 247 | 248 | func init() { 249 | plugin.Register("tcp", &TCPProxy{}) 250 | } 251 | 252 | type TCPProxy struct{} 253 | 254 | func (t *TCPProxy) Setup(config json.RawMessage) error { return nil } 255 | 256 | func (t *TCPProxy) StopProxy(item *plugin.PluginMeta) {} 257 | 258 | func (t *TCPProxy) RunProxy(item *plugin.PluginMeta) error { 259 | return fmt.Errorf("TODO://") 260 | } 261 | 262 | ``` 263 | 264 | 最后,需要在`opennotrd/plugins.go`当中导入您的插件所在的包。 265 | 266 | ```golang 267 | import ( 268 | // plugin import 269 | _ "github.com/ICKelin/opennotr/opennotrd/plugin/tcpproxy" 270 | ) 271 | ``` 272 | 273 | 示例插件可参考[tcpproxy.go](https://github.com/ICKelin/opennotr/blob/master/opennotrd/plugin/tcpproxy/tcpproxy.go) 274 | 275 | [返回目录](#目录) 276 | 277 | ## 有问题怎么办 278 | 279 | - [wiki](https://github.com/ICKelin/opennotr/wiki) 280 | - [查看文档](https://github.com/ICKelin/opennotr/tree/master/doc) 281 | - [提交issue](https://github.com/ICKelin/opennotr/issues) 282 | - [查看源码](https://github.com/ICKelin/opennotr) 283 | - [联系作者交流解决](#关于作者) 284 | 285 | 286 | [返回目录](#目录) 287 | 288 | ## 关于作者 289 | 一个爱好编程的人,网名叫ICKelin。对于以下任何问题,包括 290 | 291 | - 项目实现细节 292 | - 项目使用问题 293 | - 项目建议,代码问题 294 | - 案例分享 295 | - 技术交流 296 | 297 | 可加微信: zyj995139094 -------------------------------------------------------------------------------- /README_EN.md: -------------------------------------------------------------------------------- 1 | ## opennotr 2 | [![Build Status](https://travis-ci.org/ICKelin/opennotr.svg?branch=master)](https://travis-ci.org/ICKelin/opennotr) 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/ICKelin/opennotr)](https://goreportcard.com/report/github.com/ICKelin/opennotr) 4 | 5 | opennotr is a nat tranversal application base on `tproxy` and openresty. 6 | 7 | opennotr provides http, https, grpc, tcp and udp nat traversal. For http, https, grpc, opennotr supports multi client share the 80/443 ports, it maybe useful for wechat, facebook webhook debug. 8 | 9 | **Status: Stable** 10 | 11 | The technical architecture of opennotr 12 | 13 | ![opennotr](doc/assets/opennotr.jpg) 14 | 15 | Table of Contents 16 | ================= 17 | - [Features](#Features) 18 | - [Build](#build) 19 | - [Install](#Install) 20 | - [Plugin](#Plugin) 21 | - [Technology details](#Technology-details) 22 | - [Author](#Author) 23 | 24 | Features 25 | ========= 26 | opennotr provides these features: 27 | 28 | - Supports multi protocol, http, https, grpc, tcp, udp. 29 | - Multi client shares the same http, https, grpc port, for example: client A use `a.notr.tech` domain, client B use `b.notr.tech`, they can both use 80 port for http. Opennotr use openresty for dynamic upstream. 30 | - Dynamic dns support, opennotr use coredns and etcd for dynamic dns. 31 | - Support plugin 32 | 33 | [Back to TOC](#table-of-contents) 34 | 35 | Build 36 | ===== 37 | 38 | **Build binary:** 39 | 40 | `./build_exec.sh` 41 | 42 | The binary file will created in bin folder. 43 | 44 | **Build docker image:** 45 | 46 | `./build_image.sh` 47 | 48 | This scripts will run `build_exec.sh` and build an image name `opennotr` 49 | 50 | [Back to TOC](#table-of-contents) 51 | 52 | Install 53 | ========= 54 | 55 | **Install via docker-compose** 56 | 57 | 1. create configuration file 58 | 59 | `mkdir /opt/data/opennotrd` 60 | 61 | An example of configuration folder tree is: 62 | 63 | ``` 64 | root@iZwz97kfjnf78copv1ae65Z:/opt/data/opennotrd# tree 65 | . 66 | |-- cert ---------------------> cert folder 67 | | |-- upstream.crt 68 | | `-- upstream.key 69 | `-- notrd.yaml ---------------> opennotr config file 70 | 71 | 2 directories, 5 files 72 | ``` 73 | 74 | the cert folder MUST be created and the crt and key file MUST created too. 75 | 76 | ```yml 77 | server: 78 | listen: ":10100" 79 | authKey: "client server exchange key" 80 | domain: "open.notr.tech" 81 | tcplisten: ":4398" 82 | udplisten: ":4399" 83 | 84 | dhcp: 85 | cidr: "100.64.242.1/24" 86 | ip: "100.64.242.1" 87 | 88 | plugin: 89 | tcp: | 90 | { 91 | "PortMin": 10000, 92 | "PortMax": 20000 93 | } 94 | 95 | udp: | 96 | { 97 | "PortMin": 20000, 98 | "PortMax": 30000 99 | } 100 | 101 | http: | 102 | { 103 | "adminUrl": "http://127.0.0.1:81/upstreams" 104 | } 105 | 106 | https: | 107 | { 108 | "adminUrl": "http://127.0.0.1:81/upstreams" 109 | } 110 | 111 | h2c: | 112 | { 113 | "adminUrl": "http://127.0.0.1:81/upstreams" 114 | } 115 | ``` 116 | 117 | the only one configuration item you should change is `domain: "open.notr.tech"`, replace `open.notr.tech` with your own domain. 118 | 119 | 2. Run with docker 120 | 121 | `docker run --privileged --net=host -v /opt/logs/opennotr:/opt/resty-upstream/logs -v /opt/data/opennotrd:/opt/conf -d opennotr` 122 | 123 | Or use docker-compose 124 | 125 | 126 | ``` 127 | wget https://github.com/ICKelin/opennotr/blob/develop/docker-build/docker-compose.yaml 128 | 129 | docker-compose up -d opennotrd 130 | ``` 131 | 132 | **Run opennotr client** 133 | 134 | prepare config file`config.yaml` 135 | ```yaml 136 | serverAddr: "demo.notr.tech:10100" 137 | key: "client server exchange key" 138 | domain: "cloud.dahuizong.com" 139 | 140 | # forward table 141 | forwards: 142 | - protocol: tcp 143 | ports: 144 | # public port: local port 145 | 2222: 2222 146 | 147 | - protocol: udp 148 | ports: 149 | 53: 53 150 | 151 | - protocol: http 152 | ports: 153 | 0: 8080 154 | 155 | - protocol: https 156 | ports: 157 | 0: 8081 158 | 159 | - protocol: h2c 160 | ports: 161 | 0: 50052 162 | 163 | ``` 164 | 165 | and then you can run the opennotr client using `./opennotr -conf config.yaml` command 166 | 167 | [Back to TOC](#table-of-contents) 168 | 169 | Plugin 170 | ======= 171 | opennotr provide plugin interface for developer, Yes, tcp and udp are buildin plugins. 172 | 173 | For a new plugin, you should implement the IPlugin interface which contains RunProxy method. 174 | 175 | ```golang 176 | // IPlugin defines plugin interface 177 | // Plugin should implements the IPlugin 178 | type IPlugin interface { 179 | // Setup calls at the begin of plugin system initialize 180 | // plugin system will pass the raw message to plugin's Setup function 181 | Setup(json.RawMessage) error 182 | 183 | // Close a proxy, it may be called by client's connection close 184 | StopProxy(item *PluginMeta) 185 | 186 | // Run a proxy, it may be called by client's connection established 187 | RunProxy(item *PluginMeta) error 188 | } 189 | ``` 190 | 191 | And then implement the interface 192 | 193 | ```golang 194 | package tcpproxy 195 | 196 | import ( 197 | "encoding/json" 198 | "fmt" 199 | 200 | "github.com/ICKelin/opennotr/opennotrd/plugin" 201 | ) 202 | 203 | func init() { 204 | plugin.Register("tcp", &TCPProxy{}) 205 | } 206 | 207 | type TCPProxy struct{} 208 | 209 | func (t *TCPProxy) Setup(config json.RawMessage) error { return nil } 210 | 211 | func (t *TCPProxy) StopProxy(item *plugin.PluginMeta) {} 212 | 213 | func (t *TCPProxy) RunProxy(item *plugin.PluginMeta) error { 214 | return fmt.Errorf("TODO://") 215 | } 216 | ``` 217 | 218 | and then import the plugin package 219 | ```golang 220 | import ( 221 | // plugin import 222 | _ "github.com/ICKelin/opennotr/opennotrd/plugin/tcpproxy" 223 | ) 224 | ``` 225 | 226 | Technology details 227 | ================== 228 | 229 | - [opennotr architecture]() 230 | - [opennotr dynamic upstream implement]() 231 | - [opennotr vpn implement]() 232 | 233 | [Back to TOC](#table-of-contents) 234 | 235 | Author 236 | ====== 237 | A programer name ICKelin. -------------------------------------------------------------------------------- /build_exec.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | WORKSPACE=`pwd` 3 | BIN=$WORKSPACE/bin/ 4 | EXEC_PREFIX=opennotrd 5 | 6 | cd $WORKSPACE/opennotr 7 | 8 | echo 'building client...' 9 | GOOS=darwin go build -o $BIN/$EXEC_PREFIX_darwin_amd64 10 | GOOS=linux go build -o $BIN/$EXEC_PREFIX_linux_amd64 11 | GOARCH=arm GOOS=linux go build -o $BIN/$EXEC_PREFIX_arm 12 | GOARCH=arm64 GOOS=linux go build -o $BIN/$EXEC_PREFIX_arm64 13 | 14 | echo 'building server...' 15 | cd $WORKSPACE/cmd/opennotrd 16 | GOOS=linux go build -o $BIN/$EXEC_PREFIX_linux_amd64 17 | -------------------------------------------------------------------------------- /build_image.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ./build_exec.sh 3 | 4 | cd $WORKSPACE/docker-build 5 | docker build . -t opennotrd 6 | -------------------------------------------------------------------------------- /cmd/opennotrd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/ICKelin/opennotr/opennotrd" 4 | 5 | func main() { 6 | opennotrd.Run() 7 | } 8 | -------------------------------------------------------------------------------- /coverage.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | echo "" > coverage.txt 5 | 6 | for d in $(go list ./... | grep -v pkg/logs); do 7 | go test -race -coverprofile=profile.out -covermode=atomic $d -v 8 | if [ -f profile.out ]; then 9 | cat profile.out >> coverage.txt 10 | rm profile.out 11 | fi 12 | done -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | opennotr相关文档 2 | =============== 3 | 建设中. -------------------------------------------------------------------------------- /doc/assets/httpdemo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ICKelin/opennotr/c7fa36165ea9766f8065e593fb9dbc625c19cd31/doc/assets/httpdemo.jpg -------------------------------------------------------------------------------- /doc/assets/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ICKelin/opennotr/c7fa36165ea9766f8065e593fb9dbc625c19cd31/doc/assets/logo.jpg -------------------------------------------------------------------------------- /doc/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ICKelin/opennotr/c7fa36165ea9766f8065e593fb9dbc625c19cd31/doc/assets/logo.png -------------------------------------------------------------------------------- /doc/assets/opennotr.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ICKelin/opennotr/c7fa36165ea9766f8065e593fb9dbc625c19cd31/doc/assets/opennotr.jpg -------------------------------------------------------------------------------- /doc/assets/qrcode.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ICKelin/opennotr/c7fa36165ea9766f8065e593fb9dbc625c19cd31/doc/assets/qrcode.jpg -------------------------------------------------------------------------------- /doc/assets/tcpdemo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ICKelin/opennotr/c7fa36165ea9766f8065e593fb9dbc625c19cd31/doc/assets/tcpdemo.jpg -------------------------------------------------------------------------------- /doc/assets/udpdemo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ICKelin/opennotr/c7fa36165ea9766f8065e593fb9dbc625c19cd31/doc/assets/udpdemo.jpg -------------------------------------------------------------------------------- /doc/zh/opennotr技术实现细节.md: -------------------------------------------------------------------------------- 1 | ## opennotr技术实现细节 2 | 3 | ## 内部线程模型 4 | 5 | ## openresty动态upstream 6 | 7 | ## tcp/udp代理实现 8 | -------------------------------------------------------------------------------- /doc/zh/opennotr技术架构.md: -------------------------------------------------------------------------------- 1 | ## opennotr技术架构 2 | ![技术架构](../../opennotr.jpg) -------------------------------------------------------------------------------- /docker-build/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ickelin/resty-upstream:latest 2 | COPY opennotrd /opt/ 3 | COPY start.sh /opt/ 4 | RUN chmod +x /opt/start.sh && chmod +x /opt/opennotrd 5 | CMD /opt/start.sh 6 | -------------------------------------------------------------------------------- /docker-build/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | opennotrd: 4 | image: ickelin/opennotr:latest 5 | network_mode: host 6 | container_name: opennotrd 7 | restart: always 8 | privileged: true 9 | volumes: 10 | - /opt/logs/opennotr:/opt/resty-upstream/logs 11 | - /opt/data/opennotrd:/opt/conf 12 | environment: 13 | TIME_ZONE: Asia/Shanghai 14 | -------------------------------------------------------------------------------- /docker-build/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | /opt/openresty/bin/openresty -p /opt/resty-upstream -c /opt/conf/nginx-conf/nginx.conf 3 | /opt/opennotrd -conf /opt/conf/notrd.yaml 4 | -------------------------------------------------------------------------------- /etc/opennotr.yaml: -------------------------------------------------------------------------------- 1 | serverAddr: "demo.notr.tech:10100" 2 | key: "http://www.notr.tech" 3 | forwards: 4 | - protocol: tcp 5 | ports: 6 | 222: 2222 7 | 8 | - protocol: udp 9 | ports: 10 | 0: 53 11 | 12 | - protocol: http 13 | ports: 14 | 0: 8080 15 | 16 | - protocol: https 17 | ports: 18 | 0: 8081 19 | 20 | - protocol: h2c 21 | ports: 22 | 0: 50052 23 | -------------------------------------------------------------------------------- /etc/opennotrd.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | listen: ":10100" 3 | authKey: "client server exchange key" 4 | domain: "open.notr.tech" 5 | 6 | tcpforward: 7 | listen: ":4398" 8 | 9 | udpforward: 10 | listen: ":4399" 11 | 12 | dhcp: 13 | cidr: "100.64.242.1/24" 14 | ip: "100.64.242.1" 15 | 16 | # resolver: 17 | # etcdEndpoints: 18 | # - 127.0.0.1:2379 19 | 20 | plugin: 21 | tcp: | 22 | {} 23 | 24 | udp: | 25 | { 26 | "sessionTimeout": 30 27 | } 28 | 29 | http: | 30 | { 31 | "adminUrl": "http://127.0.0.1:81/upstreams" 32 | } 33 | 34 | https: | 35 | { 36 | "adminUrl": "http://127.0.0.1:81/upstreams" 37 | } 38 | 39 | h2c: | 40 | { 41 | "adminUrl": "http://127.0.0.1:81/upstreams" 42 | } 43 | dummy: | 44 | {} 45 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ICKelin/opennotr/c7fa36165ea9766f8065e593fb9dbc625c19cd31/example/README.md -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ICKelin/opennotr 2 | 3 | go 1.13 4 | 5 | replace google.golang.org/grpc => google.golang.org/grpc v1.26.0 6 | 7 | require ( 8 | github.com/astaxie/beego v1.12.3 9 | github.com/belogik/goes v0.0.0-20151229125003-e54d722c3aff 10 | github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58 11 | github.com/coreos/bbolt v1.3.2 // indirect 12 | github.com/coreos/etcd v3.3.22+incompatible 13 | github.com/coreos/go-semver v0.3.0 // indirect 14 | github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect 15 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f // indirect 16 | github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect 17 | github.com/dustin/go-humanize v1.0.0 // indirect 18 | github.com/gogo/protobuf v1.3.1 19 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect 20 | github.com/google/btree v1.0.0 // indirect 21 | github.com/google/uuid v1.1.1 // indirect 22 | github.com/gorilla/websocket v1.4.0 23 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4 // indirect 24 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect 25 | github.com/grpc-ecosystem/grpc-gateway v1.9.5 // indirect 26 | github.com/jonboulle/clockwork v0.1.0 // indirect 27 | github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect 28 | github.com/siddontang/go v0.0.0-20170517070808-cb568a3e5cc0 29 | github.com/soheilhy/cmux v0.1.4 // indirect 30 | github.com/stretchr/testify v1.5.1 // indirect 31 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 // indirect 32 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect 33 | github.com/xtaci/smux v2.0.1+incompatible 34 | go.etcd.io/bbolt v1.3.3 // indirect 35 | go.uber.org/zap v1.15.0 // indirect 36 | golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 // indirect 37 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b // indirect 38 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e // indirect 39 | golang.org/x/text v0.3.2 // indirect 40 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect 41 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4 // indirect 42 | google.golang.org/genproto v0.0.0-20200711021454-869866162049 // indirect 43 | google.golang.org/grpc v1.29.1 // indirect 44 | gopkg.in/yaml.v2 v2.3.0 45 | honnef.co/go/tools v0.0.1-2020.1.3 // indirect 46 | sigs.k8s.io/yaml v1.2.0 // indirect 47 | ) 48 | -------------------------------------------------------------------------------- /internal/logs/README.md: -------------------------------------------------------------------------------- 1 | ## logs 2 | logs is a Go logs manager. It can use many logs adapters. The repo is inspired by `database/sql` . 3 | 4 | 5 | ## How to install? 6 | 7 | go get github.com/astaxie/beego/logs 8 | 9 | 10 | ## What adapters are supported? 11 | 12 | As of now this logs support console, file,smtp and conn. 13 | 14 | 15 | ## How to use it? 16 | 17 | First you must import it 18 | 19 | import ( 20 | "github.com/astaxie/beego/logs" 21 | ) 22 | 23 | Then init a Log (example with console adapter) 24 | 25 | log := NewLogger(10000) 26 | log.SetLogger("console", "") 27 | 28 | > the first params stand for how many channel 29 | 30 | Use it like this: 31 | 32 | log.Trace("trace") 33 | log.Info("info") 34 | log.Warn("warning") 35 | log.Debug("debug") 36 | log.Critical("critical") 37 | 38 | 39 | ## File adapter 40 | 41 | Configure file adapter like this: 42 | 43 | log := NewLogger(10000) 44 | log.SetLogger("file", `{"filename":"test.log"}`) 45 | 46 | 47 | ## Conn adapter 48 | 49 | Configure like this: 50 | 51 | log := NewLogger(1000) 52 | log.SetLogger("conn", `{"net":"tcp","addr":":7020"}`) 53 | log.Info("info") 54 | 55 | 56 | ## Smtp adapter 57 | 58 | Configure like this: 59 | 60 | log := NewLogger(10000) 61 | log.SetLogger("smtp", `{"username":"beegotest@gmail.com","password":"xxxxxxxx","host":"smtp.gmail.com:587","sendTos":["xiemengjun@gmail.com"]}`) 62 | log.Critical("sendmail critical") 63 | time.Sleep(time.Second * 30) 64 | -------------------------------------------------------------------------------- /internal/logs/alils/alils.go: -------------------------------------------------------------------------------- 1 | package alils 2 | 3 | import ( 4 | "encoding/json" 5 | "strings" 6 | "sync" 7 | "time" 8 | 9 | "github.com/astaxie/beego/logs" 10 | "github.com/gogo/protobuf/proto" 11 | ) 12 | 13 | const ( 14 | // CacheSize set the flush size 15 | CacheSize int = 64 16 | // Delimiter define the topic delimiter 17 | Delimiter string = "##" 18 | ) 19 | 20 | // Config is the Config for Ali Log 21 | type Config struct { 22 | Project string `json:"project"` 23 | Endpoint string `json:"endpoint"` 24 | KeyID string `json:"key_id"` 25 | KeySecret string `json:"key_secret"` 26 | LogStore string `json:"log_store"` 27 | Topics []string `json:"topics"` 28 | Source string `json:"source"` 29 | Level int `json:"level"` 30 | FlushWhen int `json:"flush_when"` 31 | } 32 | 33 | // aliLSWriter implements LoggerInterface. 34 | // it writes messages in keep-live tcp connection. 35 | type aliLSWriter struct { 36 | store *LogStore 37 | group []*LogGroup 38 | withMap bool 39 | groupMap map[string]*LogGroup 40 | lock *sync.Mutex 41 | Config 42 | } 43 | 44 | // NewAliLS create a new Logger 45 | func NewAliLS() logs.Logger { 46 | alils := new(aliLSWriter) 47 | alils.Level = logs.LevelTrace 48 | return alils 49 | } 50 | 51 | // Init parse config and init struct 52 | func (c *aliLSWriter) Init(jsonConfig string) (err error) { 53 | 54 | json.Unmarshal([]byte(jsonConfig), c) 55 | 56 | if c.FlushWhen > CacheSize { 57 | c.FlushWhen = CacheSize 58 | } 59 | 60 | prj := &LogProject{ 61 | Name: c.Project, 62 | Endpoint: c.Endpoint, 63 | AccessKeyID: c.KeyID, 64 | AccessKeySecret: c.KeySecret, 65 | } 66 | 67 | c.store, err = prj.GetLogStore(c.LogStore) 68 | if err != nil { 69 | return err 70 | } 71 | 72 | // Create default Log Group 73 | c.group = append(c.group, &LogGroup{ 74 | Topic: proto.String(""), 75 | Source: proto.String(c.Source), 76 | Logs: make([]*Log, 0, c.FlushWhen), 77 | }) 78 | 79 | // Create other Log Group 80 | c.groupMap = make(map[string]*LogGroup) 81 | for _, topic := range c.Topics { 82 | 83 | lg := &LogGroup{ 84 | Topic: proto.String(topic), 85 | Source: proto.String(c.Source), 86 | Logs: make([]*Log, 0, c.FlushWhen), 87 | } 88 | 89 | c.group = append(c.group, lg) 90 | c.groupMap[topic] = lg 91 | } 92 | 93 | if len(c.group) == 1 { 94 | c.withMap = false 95 | } else { 96 | c.withMap = true 97 | } 98 | 99 | c.lock = &sync.Mutex{} 100 | 101 | return nil 102 | } 103 | 104 | // WriteMsg write message in connection. 105 | // if connection is down, try to re-connect. 106 | func (c *aliLSWriter) WriteMsg(when time.Time, msg string, level int) (err error) { 107 | 108 | if level > c.Level { 109 | return nil 110 | } 111 | 112 | var topic string 113 | var content string 114 | var lg *LogGroup 115 | if c.withMap { 116 | 117 | // Topic,LogGroup 118 | strs := strings.SplitN(msg, Delimiter, 2) 119 | if len(strs) == 2 { 120 | pos := strings.LastIndex(strs[0], " ") 121 | topic = strs[0][pos+1 : len(strs[0])] 122 | content = strs[0][0:pos] + strs[1] 123 | lg = c.groupMap[topic] 124 | } 125 | 126 | // send to empty Topic 127 | if lg == nil { 128 | content = msg 129 | lg = c.group[0] 130 | } 131 | } else { 132 | content = msg 133 | lg = c.group[0] 134 | } 135 | 136 | c1 := &LogContent{ 137 | Key: proto.String("msg"), 138 | Value: proto.String(content), 139 | } 140 | 141 | l := &Log{ 142 | Time: proto.Uint32(uint32(when.Unix())), 143 | Contents: []*LogContent{ 144 | c1, 145 | }, 146 | } 147 | 148 | c.lock.Lock() 149 | lg.Logs = append(lg.Logs, l) 150 | c.lock.Unlock() 151 | 152 | if len(lg.Logs) >= c.FlushWhen { 153 | c.flush(lg) 154 | } 155 | 156 | return nil 157 | } 158 | 159 | // Flush implementing method. empty. 160 | func (c *aliLSWriter) Flush() { 161 | 162 | // flush all group 163 | for _, lg := range c.group { 164 | c.flush(lg) 165 | } 166 | } 167 | 168 | // Destroy destroy connection writer and close tcp listener. 169 | func (c *aliLSWriter) Destroy() { 170 | } 171 | 172 | func (c *aliLSWriter) flush(lg *LogGroup) { 173 | 174 | c.lock.Lock() 175 | defer c.lock.Unlock() 176 | err := c.store.PutLogs(lg) 177 | if err != nil { 178 | return 179 | } 180 | 181 | lg.Logs = make([]*Log, 0, c.FlushWhen) 182 | } 183 | 184 | func init() { 185 | logs.Register(logs.AdapterAliLS, NewAliLS) 186 | } 187 | -------------------------------------------------------------------------------- /internal/logs/alils/config.go: -------------------------------------------------------------------------------- 1 | package alils 2 | 3 | const ( 4 | version = "0.5.0" // SDK version 5 | signatureMethod = "hmac-sha1" // Signature method 6 | 7 | // OffsetNewest stands for the log head offset, i.e. the offset that will be 8 | // assigned to the next message that will be produced to the shard. 9 | OffsetNewest = "end" 10 | // OffsetOldest stands for the oldest offset available on the logstore for a 11 | // shard. 12 | OffsetOldest = "begin" 13 | ) 14 | -------------------------------------------------------------------------------- /internal/logs/alils/log_config.go: -------------------------------------------------------------------------------- 1 | package alils 2 | 3 | // InputDetail define log detail 4 | type InputDetail struct { 5 | LogType string `json:"logType"` 6 | LogPath string `json:"logPath"` 7 | FilePattern string `json:"filePattern"` 8 | LocalStorage bool `json:"localStorage"` 9 | TimeFormat string `json:"timeFormat"` 10 | LogBeginRegex string `json:"logBeginRegex"` 11 | Regex string `json:"regex"` 12 | Keys []string `json:"key"` 13 | FilterKeys []string `json:"filterKey"` 14 | FilterRegex []string `json:"filterRegex"` 15 | TopicFormat string `json:"topicFormat"` 16 | } 17 | 18 | // OutputDetail define the output detail 19 | type OutputDetail struct { 20 | Endpoint string `json:"endpoint"` 21 | LogStoreName string `json:"logstoreName"` 22 | } 23 | 24 | // LogConfig define Log Config 25 | type LogConfig struct { 26 | Name string `json:"configName"` 27 | InputType string `json:"inputType"` 28 | InputDetail InputDetail `json:"inputDetail"` 29 | OutputType string `json:"outputType"` 30 | OutputDetail OutputDetail `json:"outputDetail"` 31 | 32 | CreateTime uint32 33 | LastModifyTime uint32 34 | 35 | project *LogProject 36 | } 37 | 38 | // GetAppliedMachineGroup returns applied machine group of this config. 39 | func (c *LogConfig) GetAppliedMachineGroup(confName string) (groupNames []string, err error) { 40 | groupNames, err = c.project.GetAppliedMachineGroups(c.Name) 41 | return 42 | } 43 | -------------------------------------------------------------------------------- /internal/logs/alils/log_store.go: -------------------------------------------------------------------------------- 1 | package alils 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "net/http/httputil" 9 | "strconv" 10 | 11 | lz4 "github.com/cloudflare/golz4" 12 | "github.com/gogo/protobuf/proto" 13 | ) 14 | 15 | // LogStore Store the logs 16 | type LogStore struct { 17 | Name string `json:"logstoreName"` 18 | TTL int 19 | ShardCount int 20 | 21 | CreateTime uint32 22 | LastModifyTime uint32 23 | 24 | project *LogProject 25 | } 26 | 27 | // Shard define the Log Shard 28 | type Shard struct { 29 | ShardID int `json:"shardID"` 30 | } 31 | 32 | // ListShards returns shard id list of this logstore. 33 | func (s *LogStore) ListShards() (shardIDs []int, err error) { 34 | h := map[string]string{ 35 | "x-sls-bodyrawsize": "0", 36 | } 37 | 38 | uri := fmt.Sprintf("/logstores/%v/shards", s.Name) 39 | r, err := request(s.project, "GET", uri, h, nil) 40 | if err != nil { 41 | return 42 | } 43 | 44 | buf, err := ioutil.ReadAll(r.Body) 45 | if err != nil { 46 | return 47 | } 48 | 49 | if r.StatusCode != http.StatusOK { 50 | errMsg := &errorMessage{} 51 | err = json.Unmarshal(buf, errMsg) 52 | if err != nil { 53 | err = fmt.Errorf("failed to list logstore") 54 | dump, _ := httputil.DumpResponse(r, true) 55 | fmt.Println(dump) 56 | return 57 | } 58 | err = fmt.Errorf("%v:%v", errMsg.Code, errMsg.Message) 59 | return 60 | } 61 | 62 | var shards []*Shard 63 | err = json.Unmarshal(buf, &shards) 64 | if err != nil { 65 | return 66 | } 67 | 68 | for _, v := range shards { 69 | shardIDs = append(shardIDs, v.ShardID) 70 | } 71 | return 72 | } 73 | 74 | // PutLogs put logs into logstore. 75 | // The callers should transform user logs into LogGroup. 76 | func (s *LogStore) PutLogs(lg *LogGroup) (err error) { 77 | body, err := proto.Marshal(lg) 78 | if err != nil { 79 | return 80 | } 81 | 82 | // Compresse body with lz4 83 | out := make([]byte, lz4.CompressBound(body)) 84 | n, err := lz4.Compress(body, out) 85 | if err != nil { 86 | return 87 | } 88 | 89 | h := map[string]string{ 90 | "x-sls-compresstype": "lz4", 91 | "x-sls-bodyrawsize": fmt.Sprintf("%v", len(body)), 92 | "Content-Type": "application/x-protobuf", 93 | } 94 | 95 | uri := fmt.Sprintf("/logstores/%v", s.Name) 96 | r, err := request(s.project, "POST", uri, h, out[:n]) 97 | if err != nil { 98 | return 99 | } 100 | 101 | buf, err := ioutil.ReadAll(r.Body) 102 | if err != nil { 103 | return 104 | } 105 | 106 | if r.StatusCode != http.StatusOK { 107 | errMsg := &errorMessage{} 108 | err = json.Unmarshal(buf, errMsg) 109 | if err != nil { 110 | err = fmt.Errorf("failed to put logs") 111 | dump, _ := httputil.DumpResponse(r, true) 112 | fmt.Println(dump) 113 | return 114 | } 115 | err = fmt.Errorf("%v:%v", errMsg.Code, errMsg.Message) 116 | return 117 | } 118 | return 119 | } 120 | 121 | // GetCursor gets log cursor of one shard specified by shardID. 122 | // The from can be in three form: a) unix timestamp in seccond, b) "begin", c) "end". 123 | // For more detail please read: http://gitlab.alibaba-inc.com/sls/doc/blob/master/api/shard.md#logstore 124 | func (s *LogStore) GetCursor(shardID int, from string) (cursor string, err error) { 125 | h := map[string]string{ 126 | "x-sls-bodyrawsize": "0", 127 | } 128 | 129 | uri := fmt.Sprintf("/logstores/%v/shards/%v?type=cursor&from=%v", 130 | s.Name, shardID, from) 131 | 132 | r, err := request(s.project, "GET", uri, h, nil) 133 | if err != nil { 134 | return 135 | } 136 | 137 | buf, err := ioutil.ReadAll(r.Body) 138 | if err != nil { 139 | return 140 | } 141 | 142 | if r.StatusCode != http.StatusOK { 143 | errMsg := &errorMessage{} 144 | err = json.Unmarshal(buf, errMsg) 145 | if err != nil { 146 | err = fmt.Errorf("failed to get cursor") 147 | dump, _ := httputil.DumpResponse(r, true) 148 | fmt.Println(dump) 149 | return 150 | } 151 | err = fmt.Errorf("%v:%v", errMsg.Code, errMsg.Message) 152 | return 153 | } 154 | 155 | type Body struct { 156 | Cursor string 157 | } 158 | body := &Body{} 159 | 160 | err = json.Unmarshal(buf, body) 161 | if err != nil { 162 | return 163 | } 164 | cursor = body.Cursor 165 | return 166 | } 167 | 168 | // GetLogsBytes gets logs binary data from shard specified by shardID according cursor. 169 | // The logGroupMaxCount is the max number of logGroup could be returned. 170 | // The nextCursor is the next curosr can be used to read logs at next time. 171 | func (s *LogStore) GetLogsBytes(shardID int, cursor string, 172 | logGroupMaxCount int) (out []byte, nextCursor string, err error) { 173 | 174 | h := map[string]string{ 175 | "x-sls-bodyrawsize": "0", 176 | "Accept": "application/x-protobuf", 177 | "Accept-Encoding": "lz4", 178 | } 179 | 180 | uri := fmt.Sprintf("/logstores/%v/shards/%v?type=logs&cursor=%v&count=%v", 181 | s.Name, shardID, cursor, logGroupMaxCount) 182 | 183 | r, err := request(s.project, "GET", uri, h, nil) 184 | if err != nil { 185 | return 186 | } 187 | 188 | buf, err := ioutil.ReadAll(r.Body) 189 | if err != nil { 190 | return 191 | } 192 | 193 | if r.StatusCode != http.StatusOK { 194 | errMsg := &errorMessage{} 195 | err = json.Unmarshal(buf, errMsg) 196 | if err != nil { 197 | err = fmt.Errorf("failed to get cursor") 198 | dump, _ := httputil.DumpResponse(r, true) 199 | fmt.Println(dump) 200 | return 201 | } 202 | err = fmt.Errorf("%v:%v", errMsg.Code, errMsg.Message) 203 | return 204 | } 205 | 206 | v, ok := r.Header["X-Sls-Compresstype"] 207 | if !ok || len(v) == 0 { 208 | err = fmt.Errorf("can't find 'x-sls-compresstype' header") 209 | return 210 | } 211 | if v[0] != "lz4" { 212 | err = fmt.Errorf("unexpected compress type:%v", v[0]) 213 | return 214 | } 215 | 216 | v, ok = r.Header["X-Sls-Cursor"] 217 | if !ok || len(v) == 0 { 218 | err = fmt.Errorf("can't find 'x-sls-cursor' header") 219 | return 220 | } 221 | nextCursor = v[0] 222 | 223 | v, ok = r.Header["X-Sls-Bodyrawsize"] 224 | if !ok || len(v) == 0 { 225 | err = fmt.Errorf("can't find 'x-sls-bodyrawsize' header") 226 | return 227 | } 228 | bodyRawSize, err := strconv.Atoi(v[0]) 229 | if err != nil { 230 | return 231 | } 232 | 233 | out = make([]byte, bodyRawSize) 234 | err = lz4.Uncompress(buf, out) 235 | if err != nil { 236 | return 237 | } 238 | 239 | return 240 | } 241 | 242 | // LogsBytesDecode decodes logs binary data retruned by GetLogsBytes API 243 | func LogsBytesDecode(data []byte) (gl *LogGroupList, err error) { 244 | 245 | gl = &LogGroupList{} 246 | err = proto.Unmarshal(data, gl) 247 | if err != nil { 248 | return 249 | } 250 | 251 | return 252 | } 253 | 254 | // GetLogs gets logs from shard specified by shardID according cursor. 255 | // The logGroupMaxCount is the max number of logGroup could be returned. 256 | // The nextCursor is the next curosr can be used to read logs at next time. 257 | func (s *LogStore) GetLogs(shardID int, cursor string, 258 | logGroupMaxCount int) (gl *LogGroupList, nextCursor string, err error) { 259 | 260 | out, nextCursor, err := s.GetLogsBytes(shardID, cursor, logGroupMaxCount) 261 | if err != nil { 262 | return 263 | } 264 | 265 | gl, err = LogsBytesDecode(out) 266 | if err != nil { 267 | return 268 | } 269 | 270 | return 271 | } 272 | -------------------------------------------------------------------------------- /internal/logs/alils/machine_group.go: -------------------------------------------------------------------------------- 1 | package alils 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "net/http/httputil" 9 | ) 10 | 11 | // MachineGroupAttribute define the Attribute 12 | type MachineGroupAttribute struct { 13 | ExternalName string `json:"externalName"` 14 | TopicName string `json:"groupTopic"` 15 | } 16 | 17 | // MachineGroup define the machine Group 18 | type MachineGroup struct { 19 | Name string `json:"groupName"` 20 | Type string `json:"groupType"` 21 | MachineIDType string `json:"machineIdentifyType"` 22 | MachineIDList []string `json:"machineList"` 23 | 24 | Attribute MachineGroupAttribute `json:"groupAttribute"` 25 | 26 | CreateTime uint32 27 | LastModifyTime uint32 28 | 29 | project *LogProject 30 | } 31 | 32 | // Machine define the Machine 33 | type Machine struct { 34 | IP string 35 | UniqueID string `json:"machine-uniqueid"` 36 | UserdefinedID string `json:"userdefined-id"` 37 | } 38 | 39 | // MachineList define the Machine List 40 | type MachineList struct { 41 | Total int 42 | Machines []*Machine 43 | } 44 | 45 | // ListMachines returns machine list of this machine group. 46 | func (m *MachineGroup) ListMachines() (ms []*Machine, total int, err error) { 47 | h := map[string]string{ 48 | "x-sls-bodyrawsize": "0", 49 | } 50 | 51 | uri := fmt.Sprintf("/machinegroups/%v/machines", m.Name) 52 | r, err := request(m.project, "GET", uri, h, nil) 53 | if err != nil { 54 | return 55 | } 56 | 57 | buf, err := ioutil.ReadAll(r.Body) 58 | if err != nil { 59 | return 60 | } 61 | 62 | if r.StatusCode != http.StatusOK { 63 | errMsg := &errorMessage{} 64 | err = json.Unmarshal(buf, errMsg) 65 | if err != nil { 66 | err = fmt.Errorf("failed to remove config from machine group") 67 | dump, _ := httputil.DumpResponse(r, true) 68 | fmt.Println(dump) 69 | return 70 | } 71 | err = fmt.Errorf("%v:%v", errMsg.Code, errMsg.Message) 72 | return 73 | } 74 | 75 | body := &MachineList{} 76 | err = json.Unmarshal(buf, body) 77 | if err != nil { 78 | return 79 | } 80 | 81 | ms = body.Machines 82 | total = body.Total 83 | 84 | return 85 | } 86 | 87 | // GetAppliedConfigs returns applied configs of this machine group. 88 | func (m *MachineGroup) GetAppliedConfigs() (confNames []string, err error) { 89 | confNames, err = m.project.GetAppliedConfigs(m.Name) 90 | return 91 | } 92 | -------------------------------------------------------------------------------- /internal/logs/alils/request.go: -------------------------------------------------------------------------------- 1 | package alils 2 | 3 | import ( 4 | "bytes" 5 | "crypto/md5" 6 | "fmt" 7 | "net/http" 8 | ) 9 | 10 | // request sends a request to SLS. 11 | func request(project *LogProject, method, uri string, headers map[string]string, 12 | body []byte) (resp *http.Response, err error) { 13 | 14 | // The caller should provide 'x-sls-bodyrawsize' header 15 | if _, ok := headers["x-sls-bodyrawsize"]; !ok { 16 | err = fmt.Errorf("Can't find 'x-sls-bodyrawsize' header") 17 | return 18 | } 19 | 20 | // SLS public request headers 21 | headers["Host"] = project.Name + "." + project.Endpoint 22 | headers["Date"] = nowRFC1123() 23 | headers["x-sls-apiversion"] = version 24 | headers["x-sls-signaturemethod"] = signatureMethod 25 | if body != nil { 26 | bodyMD5 := fmt.Sprintf("%X", md5.Sum(body)) 27 | headers["Content-MD5"] = bodyMD5 28 | 29 | if _, ok := headers["Content-Type"]; !ok { 30 | err = fmt.Errorf("Can't find 'Content-Type' header") 31 | return 32 | } 33 | } 34 | 35 | // Calc Authorization 36 | // Authorization = "SLS :" 37 | digest, err := signature(project, method, uri, headers) 38 | if err != nil { 39 | return 40 | } 41 | auth := fmt.Sprintf("SLS %v:%v", project.AccessKeyID, digest) 42 | headers["Authorization"] = auth 43 | 44 | // Initialize http request 45 | reader := bytes.NewReader(body) 46 | urlStr := fmt.Sprintf("http://%v.%v%v", project.Name, project.Endpoint, uri) 47 | req, err := http.NewRequest(method, urlStr, reader) 48 | if err != nil { 49 | return 50 | } 51 | for k, v := range headers { 52 | req.Header.Add(k, v) 53 | } 54 | 55 | // Get ready to do request 56 | resp, err = http.DefaultClient.Do(req) 57 | if err != nil { 58 | return 59 | } 60 | 61 | return 62 | } 63 | -------------------------------------------------------------------------------- /internal/logs/alils/signature.go: -------------------------------------------------------------------------------- 1 | package alils 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha1" 6 | "encoding/base64" 7 | "fmt" 8 | "net/url" 9 | "sort" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | // GMT location 15 | var gmtLoc = time.FixedZone("GMT", 0) 16 | 17 | // NowRFC1123 returns now time in RFC1123 format with GMT timezone, 18 | // eg. "Mon, 02 Jan 2006 15:04:05 GMT". 19 | func nowRFC1123() string { 20 | return time.Now().In(gmtLoc).Format(time.RFC1123) 21 | } 22 | 23 | // signature calculates a request's signature digest. 24 | func signature(project *LogProject, method, uri string, 25 | headers map[string]string) (digest string, err error) { 26 | var contentMD5, contentType, date, canoHeaders, canoResource string 27 | var slsHeaderKeys sort.StringSlice 28 | 29 | // SignString = VERB + "\n" 30 | // + CONTENT-MD5 + "\n" 31 | // + CONTENT-TYPE + "\n" 32 | // + DATE + "\n" 33 | // + CanonicalizedSLSHeaders + "\n" 34 | // + CanonicalizedResource 35 | 36 | if val, ok := headers["Content-MD5"]; ok { 37 | contentMD5 = val 38 | } 39 | 40 | if val, ok := headers["Content-Type"]; ok { 41 | contentType = val 42 | } 43 | 44 | date, ok := headers["Date"] 45 | if !ok { 46 | err = fmt.Errorf("Can't find 'Date' header") 47 | return 48 | } 49 | 50 | // Calc CanonicalizedSLSHeaders 51 | slsHeaders := make(map[string]string, len(headers)) 52 | for k, v := range headers { 53 | l := strings.TrimSpace(strings.ToLower(k)) 54 | if strings.HasPrefix(l, "x-sls-") { 55 | slsHeaders[l] = strings.TrimSpace(v) 56 | slsHeaderKeys = append(slsHeaderKeys, l) 57 | } 58 | } 59 | 60 | sort.Sort(slsHeaderKeys) 61 | for i, k := range slsHeaderKeys { 62 | canoHeaders += k + ":" + slsHeaders[k] 63 | if i+1 < len(slsHeaderKeys) { 64 | canoHeaders += "\n" 65 | } 66 | } 67 | 68 | // Calc CanonicalizedResource 69 | u, err := url.Parse(uri) 70 | if err != nil { 71 | return 72 | } 73 | 74 | canoResource += url.QueryEscape(u.Path) 75 | if u.RawQuery != "" { 76 | var keys sort.StringSlice 77 | 78 | vals := u.Query() 79 | for k := range vals { 80 | keys = append(keys, k) 81 | } 82 | 83 | sort.Sort(keys) 84 | canoResource += "?" 85 | for i, k := range keys { 86 | if i > 0 { 87 | canoResource += "&" 88 | } 89 | 90 | for _, v := range vals[k] { 91 | canoResource += k + "=" + v 92 | } 93 | } 94 | } 95 | 96 | signStr := method + "\n" + 97 | contentMD5 + "\n" + 98 | contentType + "\n" + 99 | date + "\n" + 100 | canoHeaders + "\n" + 101 | canoResource 102 | 103 | // Signature = base64(hmac-sha1(UTF8-Encoding-Of(SignString),AccessKeySecret)) 104 | mac := hmac.New(sha1.New, []byte(project.AccessKeySecret)) 105 | _, err = mac.Write([]byte(signStr)) 106 | if err != nil { 107 | return 108 | } 109 | digest = base64.StdEncoding.EncodeToString(mac.Sum(nil)) 110 | return 111 | } 112 | -------------------------------------------------------------------------------- /internal/logs/color.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 beego Author. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // +build !windows 16 | 17 | package logs 18 | 19 | import "io" 20 | 21 | type ansiColorWriter struct { 22 | w io.Writer 23 | mode outputMode 24 | } 25 | 26 | func (cw *ansiColorWriter) Write(p []byte) (int, error) { 27 | return cw.w.Write(p) 28 | } 29 | -------------------------------------------------------------------------------- /internal/logs/color_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 beego Author. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // +build windows 16 | 17 | package logs 18 | 19 | import ( 20 | "bytes" 21 | "io" 22 | "strings" 23 | "syscall" 24 | "unsafe" 25 | ) 26 | 27 | type ( 28 | csiState int 29 | parseResult int 30 | ) 31 | 32 | const ( 33 | outsideCsiCode csiState = iota 34 | firstCsiCode 35 | secondCsiCode 36 | ) 37 | 38 | const ( 39 | noConsole parseResult = iota 40 | changedColor 41 | unknown 42 | ) 43 | 44 | type ansiColorWriter struct { 45 | w io.Writer 46 | mode outputMode 47 | state csiState 48 | paramStartBuf bytes.Buffer 49 | paramBuf bytes.Buffer 50 | } 51 | 52 | const ( 53 | firstCsiChar byte = '\x1b' 54 | secondeCsiChar byte = '[' 55 | separatorChar byte = ';' 56 | sgrCode byte = 'm' 57 | ) 58 | 59 | const ( 60 | foregroundBlue = uint16(0x0001) 61 | foregroundGreen = uint16(0x0002) 62 | foregroundRed = uint16(0x0004) 63 | foregroundIntensity = uint16(0x0008) 64 | backgroundBlue = uint16(0x0010) 65 | backgroundGreen = uint16(0x0020) 66 | backgroundRed = uint16(0x0040) 67 | backgroundIntensity = uint16(0x0080) 68 | underscore = uint16(0x8000) 69 | 70 | foregroundMask = foregroundBlue | foregroundGreen | foregroundRed | foregroundIntensity 71 | backgroundMask = backgroundBlue | backgroundGreen | backgroundRed | backgroundIntensity 72 | ) 73 | 74 | const ( 75 | ansiReset = "0" 76 | ansiIntensityOn = "1" 77 | ansiIntensityOff = "21" 78 | ansiUnderlineOn = "4" 79 | ansiUnderlineOff = "24" 80 | ansiBlinkOn = "5" 81 | ansiBlinkOff = "25" 82 | 83 | ansiForegroundBlack = "30" 84 | ansiForegroundRed = "31" 85 | ansiForegroundGreen = "32" 86 | ansiForegroundYellow = "33" 87 | ansiForegroundBlue = "34" 88 | ansiForegroundMagenta = "35" 89 | ansiForegroundCyan = "36" 90 | ansiForegroundWhite = "37" 91 | ansiForegroundDefault = "39" 92 | 93 | ansiBackgroundBlack = "40" 94 | ansiBackgroundRed = "41" 95 | ansiBackgroundGreen = "42" 96 | ansiBackgroundYellow = "43" 97 | ansiBackgroundBlue = "44" 98 | ansiBackgroundMagenta = "45" 99 | ansiBackgroundCyan = "46" 100 | ansiBackgroundWhite = "47" 101 | ansiBackgroundDefault = "49" 102 | 103 | ansiLightForegroundGray = "90" 104 | ansiLightForegroundRed = "91" 105 | ansiLightForegroundGreen = "92" 106 | ansiLightForegroundYellow = "93" 107 | ansiLightForegroundBlue = "94" 108 | ansiLightForegroundMagenta = "95" 109 | ansiLightForegroundCyan = "96" 110 | ansiLightForegroundWhite = "97" 111 | 112 | ansiLightBackgroundGray = "100" 113 | ansiLightBackgroundRed = "101" 114 | ansiLightBackgroundGreen = "102" 115 | ansiLightBackgroundYellow = "103" 116 | ansiLightBackgroundBlue = "104" 117 | ansiLightBackgroundMagenta = "105" 118 | ansiLightBackgroundCyan = "106" 119 | ansiLightBackgroundWhite = "107" 120 | ) 121 | 122 | type drawType int 123 | 124 | const ( 125 | foreground drawType = iota 126 | background 127 | ) 128 | 129 | type winColor struct { 130 | code uint16 131 | drawType drawType 132 | } 133 | 134 | var colorMap = map[string]winColor{ 135 | ansiForegroundBlack: {0, foreground}, 136 | ansiForegroundRed: {foregroundRed, foreground}, 137 | ansiForegroundGreen: {foregroundGreen, foreground}, 138 | ansiForegroundYellow: {foregroundRed | foregroundGreen, foreground}, 139 | ansiForegroundBlue: {foregroundBlue, foreground}, 140 | ansiForegroundMagenta: {foregroundRed | foregroundBlue, foreground}, 141 | ansiForegroundCyan: {foregroundGreen | foregroundBlue, foreground}, 142 | ansiForegroundWhite: {foregroundRed | foregroundGreen | foregroundBlue, foreground}, 143 | ansiForegroundDefault: {foregroundRed | foregroundGreen | foregroundBlue, foreground}, 144 | 145 | ansiBackgroundBlack: {0, background}, 146 | ansiBackgroundRed: {backgroundRed, background}, 147 | ansiBackgroundGreen: {backgroundGreen, background}, 148 | ansiBackgroundYellow: {backgroundRed | backgroundGreen, background}, 149 | ansiBackgroundBlue: {backgroundBlue, background}, 150 | ansiBackgroundMagenta: {backgroundRed | backgroundBlue, background}, 151 | ansiBackgroundCyan: {backgroundGreen | backgroundBlue, background}, 152 | ansiBackgroundWhite: {backgroundRed | backgroundGreen | backgroundBlue, background}, 153 | ansiBackgroundDefault: {0, background}, 154 | 155 | ansiLightForegroundGray: {foregroundIntensity, foreground}, 156 | ansiLightForegroundRed: {foregroundIntensity | foregroundRed, foreground}, 157 | ansiLightForegroundGreen: {foregroundIntensity | foregroundGreen, foreground}, 158 | ansiLightForegroundYellow: {foregroundIntensity | foregroundRed | foregroundGreen, foreground}, 159 | ansiLightForegroundBlue: {foregroundIntensity | foregroundBlue, foreground}, 160 | ansiLightForegroundMagenta: {foregroundIntensity | foregroundRed | foregroundBlue, foreground}, 161 | ansiLightForegroundCyan: {foregroundIntensity | foregroundGreen | foregroundBlue, foreground}, 162 | ansiLightForegroundWhite: {foregroundIntensity | foregroundRed | foregroundGreen | foregroundBlue, foreground}, 163 | 164 | ansiLightBackgroundGray: {backgroundIntensity, background}, 165 | ansiLightBackgroundRed: {backgroundIntensity | backgroundRed, background}, 166 | ansiLightBackgroundGreen: {backgroundIntensity | backgroundGreen, background}, 167 | ansiLightBackgroundYellow: {backgroundIntensity | backgroundRed | backgroundGreen, background}, 168 | ansiLightBackgroundBlue: {backgroundIntensity | backgroundBlue, background}, 169 | ansiLightBackgroundMagenta: {backgroundIntensity | backgroundRed | backgroundBlue, background}, 170 | ansiLightBackgroundCyan: {backgroundIntensity | backgroundGreen | backgroundBlue, background}, 171 | ansiLightBackgroundWhite: {backgroundIntensity | backgroundRed | backgroundGreen | backgroundBlue, background}, 172 | } 173 | 174 | var ( 175 | kernel32 = syscall.NewLazyDLL("kernel32.dll") 176 | procSetConsoleTextAttribute = kernel32.NewProc("SetConsoleTextAttribute") 177 | procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo") 178 | defaultAttr *textAttributes 179 | ) 180 | 181 | func init() { 182 | screenInfo := getConsoleScreenBufferInfo(uintptr(syscall.Stdout)) 183 | if screenInfo != nil { 184 | colorMap[ansiForegroundDefault] = winColor{ 185 | screenInfo.WAttributes & (foregroundRed | foregroundGreen | foregroundBlue), 186 | foreground, 187 | } 188 | colorMap[ansiBackgroundDefault] = winColor{ 189 | screenInfo.WAttributes & (backgroundRed | backgroundGreen | backgroundBlue), 190 | background, 191 | } 192 | defaultAttr = convertTextAttr(screenInfo.WAttributes) 193 | } 194 | } 195 | 196 | type coord struct { 197 | X, Y int16 198 | } 199 | 200 | type smallRect struct { 201 | Left, Top, Right, Bottom int16 202 | } 203 | 204 | type consoleScreenBufferInfo struct { 205 | DwSize coord 206 | DwCursorPosition coord 207 | WAttributes uint16 208 | SrWindow smallRect 209 | DwMaximumWindowSize coord 210 | } 211 | 212 | func getConsoleScreenBufferInfo(hConsoleOutput uintptr) *consoleScreenBufferInfo { 213 | var csbi consoleScreenBufferInfo 214 | ret, _, _ := procGetConsoleScreenBufferInfo.Call( 215 | hConsoleOutput, 216 | uintptr(unsafe.Pointer(&csbi))) 217 | if ret == 0 { 218 | return nil 219 | } 220 | return &csbi 221 | } 222 | 223 | func setConsoleTextAttribute(hConsoleOutput uintptr, wAttributes uint16) bool { 224 | ret, _, _ := procSetConsoleTextAttribute.Call( 225 | hConsoleOutput, 226 | uintptr(wAttributes)) 227 | return ret != 0 228 | } 229 | 230 | type textAttributes struct { 231 | foregroundColor uint16 232 | backgroundColor uint16 233 | foregroundIntensity uint16 234 | backgroundIntensity uint16 235 | underscore uint16 236 | otherAttributes uint16 237 | } 238 | 239 | func convertTextAttr(winAttr uint16) *textAttributes { 240 | fgColor := winAttr & (foregroundRed | foregroundGreen | foregroundBlue) 241 | bgColor := winAttr & (backgroundRed | backgroundGreen | backgroundBlue) 242 | fgIntensity := winAttr & foregroundIntensity 243 | bgIntensity := winAttr & backgroundIntensity 244 | underline := winAttr & underscore 245 | otherAttributes := winAttr &^ (foregroundMask | backgroundMask | underscore) 246 | return &textAttributes{fgColor, bgColor, fgIntensity, bgIntensity, underline, otherAttributes} 247 | } 248 | 249 | func convertWinAttr(textAttr *textAttributes) uint16 { 250 | var winAttr uint16 251 | winAttr |= textAttr.foregroundColor 252 | winAttr |= textAttr.backgroundColor 253 | winAttr |= textAttr.foregroundIntensity 254 | winAttr |= textAttr.backgroundIntensity 255 | winAttr |= textAttr.underscore 256 | winAttr |= textAttr.otherAttributes 257 | return winAttr 258 | } 259 | 260 | func changeColor(param []byte) parseResult { 261 | screenInfo := getConsoleScreenBufferInfo(uintptr(syscall.Stdout)) 262 | if screenInfo == nil { 263 | return noConsole 264 | } 265 | 266 | winAttr := convertTextAttr(screenInfo.WAttributes) 267 | strParam := string(param) 268 | if len(strParam) <= 0 { 269 | strParam = "0" 270 | } 271 | csiParam := strings.Split(strParam, string(separatorChar)) 272 | for _, p := range csiParam { 273 | c, ok := colorMap[p] 274 | switch { 275 | case !ok: 276 | switch p { 277 | case ansiReset: 278 | winAttr.foregroundColor = defaultAttr.foregroundColor 279 | winAttr.backgroundColor = defaultAttr.backgroundColor 280 | winAttr.foregroundIntensity = defaultAttr.foregroundIntensity 281 | winAttr.backgroundIntensity = defaultAttr.backgroundIntensity 282 | winAttr.underscore = 0 283 | winAttr.otherAttributes = 0 284 | case ansiIntensityOn: 285 | winAttr.foregroundIntensity = foregroundIntensity 286 | case ansiIntensityOff: 287 | winAttr.foregroundIntensity = 0 288 | case ansiUnderlineOn: 289 | winAttr.underscore = underscore 290 | case ansiUnderlineOff: 291 | winAttr.underscore = 0 292 | case ansiBlinkOn: 293 | winAttr.backgroundIntensity = backgroundIntensity 294 | case ansiBlinkOff: 295 | winAttr.backgroundIntensity = 0 296 | default: 297 | // unknown code 298 | } 299 | case c.drawType == foreground: 300 | winAttr.foregroundColor = c.code 301 | case c.drawType == background: 302 | winAttr.backgroundColor = c.code 303 | } 304 | } 305 | winTextAttribute := convertWinAttr(winAttr) 306 | setConsoleTextAttribute(uintptr(syscall.Stdout), winTextAttribute) 307 | 308 | return changedColor 309 | } 310 | 311 | func parseEscapeSequence(command byte, param []byte) parseResult { 312 | if defaultAttr == nil { 313 | return noConsole 314 | } 315 | 316 | switch command { 317 | case sgrCode: 318 | return changeColor(param) 319 | default: 320 | return unknown 321 | } 322 | } 323 | 324 | func (cw *ansiColorWriter) flushBuffer() (int, error) { 325 | return cw.flushTo(cw.w) 326 | } 327 | 328 | func (cw *ansiColorWriter) resetBuffer() (int, error) { 329 | return cw.flushTo(nil) 330 | } 331 | 332 | func (cw *ansiColorWriter) flushTo(w io.Writer) (int, error) { 333 | var n1, n2 int 334 | var err error 335 | 336 | startBytes := cw.paramStartBuf.Bytes() 337 | cw.paramStartBuf.Reset() 338 | if w != nil { 339 | n1, err = cw.w.Write(startBytes) 340 | if err != nil { 341 | return n1, err 342 | } 343 | } else { 344 | n1 = len(startBytes) 345 | } 346 | paramBytes := cw.paramBuf.Bytes() 347 | cw.paramBuf.Reset() 348 | if w != nil { 349 | n2, err = cw.w.Write(paramBytes) 350 | if err != nil { 351 | return n1 + n2, err 352 | } 353 | } else { 354 | n2 = len(paramBytes) 355 | } 356 | return n1 + n2, nil 357 | } 358 | 359 | func isParameterChar(b byte) bool { 360 | return ('0' <= b && b <= '9') || b == separatorChar 361 | } 362 | 363 | func (cw *ansiColorWriter) Write(p []byte) (int, error) { 364 | var r, nw, first, last int 365 | if cw.mode != DiscardNonColorEscSeq { 366 | cw.state = outsideCsiCode 367 | cw.resetBuffer() 368 | } 369 | 370 | var err error 371 | for i, ch := range p { 372 | switch cw.state { 373 | case outsideCsiCode: 374 | if ch == firstCsiChar { 375 | cw.paramStartBuf.WriteByte(ch) 376 | cw.state = firstCsiCode 377 | } 378 | case firstCsiCode: 379 | switch ch { 380 | case firstCsiChar: 381 | cw.paramStartBuf.WriteByte(ch) 382 | break 383 | case secondeCsiChar: 384 | cw.paramStartBuf.WriteByte(ch) 385 | cw.state = secondCsiCode 386 | last = i - 1 387 | default: 388 | cw.resetBuffer() 389 | cw.state = outsideCsiCode 390 | } 391 | case secondCsiCode: 392 | if isParameterChar(ch) { 393 | cw.paramBuf.WriteByte(ch) 394 | } else { 395 | nw, err = cw.w.Write(p[first:last]) 396 | r += nw 397 | if err != nil { 398 | return r, err 399 | } 400 | first = i + 1 401 | result := parseEscapeSequence(ch, cw.paramBuf.Bytes()) 402 | if result == noConsole || (cw.mode == OutputNonColorEscSeq && result == unknown) { 403 | cw.paramBuf.WriteByte(ch) 404 | nw, err := cw.flushBuffer() 405 | if err != nil { 406 | return r, err 407 | } 408 | r += nw 409 | } else { 410 | n, _ := cw.resetBuffer() 411 | // Add one more to the size of the buffer for the last ch 412 | r += n + 1 413 | } 414 | 415 | cw.state = outsideCsiCode 416 | } 417 | default: 418 | cw.state = outsideCsiCode 419 | } 420 | } 421 | 422 | if cw.mode != DiscardNonColorEscSeq || cw.state == outsideCsiCode { 423 | nw, err = cw.w.Write(p[first:]) 424 | r += nw 425 | } 426 | 427 | return r, err 428 | } 429 | -------------------------------------------------------------------------------- /internal/logs/color_windows_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 beego Author. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // +build windows 16 | 17 | package logs 18 | 19 | import ( 20 | "bytes" 21 | "fmt" 22 | "syscall" 23 | "testing" 24 | ) 25 | 26 | var GetConsoleScreenBufferInfo = getConsoleScreenBufferInfo 27 | 28 | func ChangeColor(color uint16) { 29 | setConsoleTextAttribute(uintptr(syscall.Stdout), color) 30 | } 31 | 32 | func ResetColor() { 33 | ChangeColor(uint16(0x0007)) 34 | } 35 | 36 | func TestWritePlanText(t *testing.T) { 37 | inner := bytes.NewBufferString("") 38 | w := NewAnsiColorWriter(inner) 39 | expected := "plain text" 40 | fmt.Fprintf(w, expected) 41 | actual := inner.String() 42 | if actual != expected { 43 | t.Errorf("Get %q, want %q", actual, expected) 44 | } 45 | } 46 | 47 | func TestWriteParseText(t *testing.T) { 48 | inner := bytes.NewBufferString("") 49 | w := NewAnsiColorWriter(inner) 50 | 51 | inputTail := "\x1b[0mtail text" 52 | expectedTail := "tail text" 53 | fmt.Fprintf(w, inputTail) 54 | actualTail := inner.String() 55 | inner.Reset() 56 | if actualTail != expectedTail { 57 | t.Errorf("Get %q, want %q", actualTail, expectedTail) 58 | } 59 | 60 | inputHead := "head text\x1b[0m" 61 | expectedHead := "head text" 62 | fmt.Fprintf(w, inputHead) 63 | actualHead := inner.String() 64 | inner.Reset() 65 | if actualHead != expectedHead { 66 | t.Errorf("Get %q, want %q", actualHead, expectedHead) 67 | } 68 | 69 | inputBothEnds := "both ends \x1b[0m text" 70 | expectedBothEnds := "both ends text" 71 | fmt.Fprintf(w, inputBothEnds) 72 | actualBothEnds := inner.String() 73 | inner.Reset() 74 | if actualBothEnds != expectedBothEnds { 75 | t.Errorf("Get %q, want %q", actualBothEnds, expectedBothEnds) 76 | } 77 | 78 | inputManyEsc := "\x1b\x1b\x1b\x1b[0m many esc" 79 | expectedManyEsc := "\x1b\x1b\x1b many esc" 80 | fmt.Fprintf(w, inputManyEsc) 81 | actualManyEsc := inner.String() 82 | inner.Reset() 83 | if actualManyEsc != expectedManyEsc { 84 | t.Errorf("Get %q, want %q", actualManyEsc, expectedManyEsc) 85 | } 86 | 87 | expectedSplit := "split text" 88 | for _, ch := range "split \x1b[0m text" { 89 | fmt.Fprintf(w, string(ch)) 90 | } 91 | actualSplit := inner.String() 92 | inner.Reset() 93 | if actualSplit != expectedSplit { 94 | t.Errorf("Get %q, want %q", actualSplit, expectedSplit) 95 | } 96 | } 97 | 98 | type screenNotFoundError struct { 99 | error 100 | } 101 | 102 | func writeAnsiColor(expectedText, colorCode string) (actualText string, actualAttributes uint16, err error) { 103 | inner := bytes.NewBufferString("") 104 | w := NewAnsiColorWriter(inner) 105 | fmt.Fprintf(w, "\x1b[%sm%s", colorCode, expectedText) 106 | 107 | actualText = inner.String() 108 | screenInfo := GetConsoleScreenBufferInfo(uintptr(syscall.Stdout)) 109 | if screenInfo != nil { 110 | actualAttributes = screenInfo.WAttributes 111 | } else { 112 | err = &screenNotFoundError{} 113 | } 114 | return 115 | } 116 | 117 | type testParam struct { 118 | text string 119 | attributes uint16 120 | ansiColor string 121 | } 122 | 123 | func TestWriteAnsiColorText(t *testing.T) { 124 | screenInfo := GetConsoleScreenBufferInfo(uintptr(syscall.Stdout)) 125 | if screenInfo == nil { 126 | t.Fatal("Could not get ConsoleScreenBufferInfo") 127 | } 128 | defer ChangeColor(screenInfo.WAttributes) 129 | defaultFgColor := screenInfo.WAttributes & uint16(0x0007) 130 | defaultBgColor := screenInfo.WAttributes & uint16(0x0070) 131 | defaultFgIntensity := screenInfo.WAttributes & uint16(0x0008) 132 | defaultBgIntensity := screenInfo.WAttributes & uint16(0x0080) 133 | 134 | fgParam := []testParam{ 135 | {"foreground black ", uint16(0x0000 | 0x0000), "30"}, 136 | {"foreground red ", uint16(0x0004 | 0x0000), "31"}, 137 | {"foreground green ", uint16(0x0002 | 0x0000), "32"}, 138 | {"foreground yellow ", uint16(0x0006 | 0x0000), "33"}, 139 | {"foreground blue ", uint16(0x0001 | 0x0000), "34"}, 140 | {"foreground magenta", uint16(0x0005 | 0x0000), "35"}, 141 | {"foreground cyan ", uint16(0x0003 | 0x0000), "36"}, 142 | {"foreground white ", uint16(0x0007 | 0x0000), "37"}, 143 | {"foreground default", defaultFgColor | 0x0000, "39"}, 144 | {"foreground light gray ", uint16(0x0000 | 0x0008 | 0x0000), "90"}, 145 | {"foreground light red ", uint16(0x0004 | 0x0008 | 0x0000), "91"}, 146 | {"foreground light green ", uint16(0x0002 | 0x0008 | 0x0000), "92"}, 147 | {"foreground light yellow ", uint16(0x0006 | 0x0008 | 0x0000), "93"}, 148 | {"foreground light blue ", uint16(0x0001 | 0x0008 | 0x0000), "94"}, 149 | {"foreground light magenta", uint16(0x0005 | 0x0008 | 0x0000), "95"}, 150 | {"foreground light cyan ", uint16(0x0003 | 0x0008 | 0x0000), "96"}, 151 | {"foreground light white ", uint16(0x0007 | 0x0008 | 0x0000), "97"}, 152 | } 153 | 154 | bgParam := []testParam{ 155 | {"background black ", uint16(0x0007 | 0x0000), "40"}, 156 | {"background red ", uint16(0x0007 | 0x0040), "41"}, 157 | {"background green ", uint16(0x0007 | 0x0020), "42"}, 158 | {"background yellow ", uint16(0x0007 | 0x0060), "43"}, 159 | {"background blue ", uint16(0x0007 | 0x0010), "44"}, 160 | {"background magenta", uint16(0x0007 | 0x0050), "45"}, 161 | {"background cyan ", uint16(0x0007 | 0x0030), "46"}, 162 | {"background white ", uint16(0x0007 | 0x0070), "47"}, 163 | {"background default", uint16(0x0007) | defaultBgColor, "49"}, 164 | {"background light gray ", uint16(0x0007 | 0x0000 | 0x0080), "100"}, 165 | {"background light red ", uint16(0x0007 | 0x0040 | 0x0080), "101"}, 166 | {"background light green ", uint16(0x0007 | 0x0020 | 0x0080), "102"}, 167 | {"background light yellow ", uint16(0x0007 | 0x0060 | 0x0080), "103"}, 168 | {"background light blue ", uint16(0x0007 | 0x0010 | 0x0080), "104"}, 169 | {"background light magenta", uint16(0x0007 | 0x0050 | 0x0080), "105"}, 170 | {"background light cyan ", uint16(0x0007 | 0x0030 | 0x0080), "106"}, 171 | {"background light white ", uint16(0x0007 | 0x0070 | 0x0080), "107"}, 172 | } 173 | 174 | resetParam := []testParam{ 175 | {"all reset", defaultFgColor | defaultBgColor | defaultFgIntensity | defaultBgIntensity, "0"}, 176 | {"all reset", defaultFgColor | defaultBgColor | defaultFgIntensity | defaultBgIntensity, ""}, 177 | } 178 | 179 | boldParam := []testParam{ 180 | {"bold on", uint16(0x0007 | 0x0008), "1"}, 181 | {"bold off", uint16(0x0007), "21"}, 182 | } 183 | 184 | underscoreParam := []testParam{ 185 | {"underscore on", uint16(0x0007 | 0x8000), "4"}, 186 | {"underscore off", uint16(0x0007), "24"}, 187 | } 188 | 189 | blinkParam := []testParam{ 190 | {"blink on", uint16(0x0007 | 0x0080), "5"}, 191 | {"blink off", uint16(0x0007), "25"}, 192 | } 193 | 194 | mixedParam := []testParam{ 195 | {"both black, bold, underline, blink", uint16(0x0000 | 0x0000 | 0x0008 | 0x8000 | 0x0080), "30;40;1;4;5"}, 196 | {"both red, bold, underline, blink", uint16(0x0004 | 0x0040 | 0x0008 | 0x8000 | 0x0080), "31;41;1;4;5"}, 197 | {"both green, bold, underline, blink", uint16(0x0002 | 0x0020 | 0x0008 | 0x8000 | 0x0080), "32;42;1;4;5"}, 198 | {"both yellow, bold, underline, blink", uint16(0x0006 | 0x0060 | 0x0008 | 0x8000 | 0x0080), "33;43;1;4;5"}, 199 | {"both blue, bold, underline, blink", uint16(0x0001 | 0x0010 | 0x0008 | 0x8000 | 0x0080), "34;44;1;4;5"}, 200 | {"both magenta, bold, underline, blink", uint16(0x0005 | 0x0050 | 0x0008 | 0x8000 | 0x0080), "35;45;1;4;5"}, 201 | {"both cyan, bold, underline, blink", uint16(0x0003 | 0x0030 | 0x0008 | 0x8000 | 0x0080), "36;46;1;4;5"}, 202 | {"both white, bold, underline, blink", uint16(0x0007 | 0x0070 | 0x0008 | 0x8000 | 0x0080), "37;47;1;4;5"}, 203 | {"both default, bold, underline, blink", uint16(defaultFgColor | defaultBgColor | 0x0008 | 0x8000 | 0x0080), "39;49;1;4;5"}, 204 | } 205 | 206 | assertTextAttribute := func(expectedText string, expectedAttributes uint16, ansiColor string) { 207 | actualText, actualAttributes, err := writeAnsiColor(expectedText, ansiColor) 208 | if actualText != expectedText { 209 | t.Errorf("Get %q, want %q", actualText, expectedText) 210 | } 211 | if err != nil { 212 | t.Fatal("Could not get ConsoleScreenBufferInfo") 213 | } 214 | if actualAttributes != expectedAttributes { 215 | t.Errorf("Text: %q, Get 0x%04x, want 0x%04x", expectedText, actualAttributes, expectedAttributes) 216 | } 217 | } 218 | 219 | for _, v := range fgParam { 220 | ResetColor() 221 | assertTextAttribute(v.text, v.attributes, v.ansiColor) 222 | } 223 | 224 | for _, v := range bgParam { 225 | ChangeColor(uint16(0x0070 | 0x0007)) 226 | assertTextAttribute(v.text, v.attributes, v.ansiColor) 227 | } 228 | 229 | for _, v := range resetParam { 230 | ChangeColor(uint16(0x0000 | 0x0070 | 0x0008)) 231 | assertTextAttribute(v.text, v.attributes, v.ansiColor) 232 | } 233 | 234 | ResetColor() 235 | for _, v := range boldParam { 236 | assertTextAttribute(v.text, v.attributes, v.ansiColor) 237 | } 238 | 239 | ResetColor() 240 | for _, v := range underscoreParam { 241 | assertTextAttribute(v.text, v.attributes, v.ansiColor) 242 | } 243 | 244 | ResetColor() 245 | for _, v := range blinkParam { 246 | assertTextAttribute(v.text, v.attributes, v.ansiColor) 247 | } 248 | 249 | for _, v := range mixedParam { 250 | ResetColor() 251 | assertTextAttribute(v.text, v.attributes, v.ansiColor) 252 | } 253 | } 254 | 255 | func TestIgnoreUnknownSequences(t *testing.T) { 256 | inner := bytes.NewBufferString("") 257 | w := NewModeAnsiColorWriter(inner, OutputNonColorEscSeq) 258 | 259 | inputText := "\x1b[=decpath mode" 260 | expectedTail := inputText 261 | fmt.Fprintf(w, inputText) 262 | actualTail := inner.String() 263 | inner.Reset() 264 | if actualTail != expectedTail { 265 | t.Errorf("Get %q, want %q", actualTail, expectedTail) 266 | } 267 | 268 | inputText = "\x1b[=tailing esc and bracket\x1b[" 269 | expectedTail = inputText 270 | fmt.Fprintf(w, inputText) 271 | actualTail = inner.String() 272 | inner.Reset() 273 | if actualTail != expectedTail { 274 | t.Errorf("Get %q, want %q", actualTail, expectedTail) 275 | } 276 | 277 | inputText = "\x1b[?tailing esc\x1b" 278 | expectedTail = inputText 279 | fmt.Fprintf(w, inputText) 280 | actualTail = inner.String() 281 | inner.Reset() 282 | if actualTail != expectedTail { 283 | t.Errorf("Get %q, want %q", actualTail, expectedTail) 284 | } 285 | 286 | inputText = "\x1b[1h;3punended color code invalid\x1b3" 287 | expectedTail = inputText 288 | fmt.Fprintf(w, inputText) 289 | actualTail = inner.String() 290 | inner.Reset() 291 | if actualTail != expectedTail { 292 | t.Errorf("Get %q, want %q", actualTail, expectedTail) 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /internal/logs/conn.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 beego Author. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package logs 16 | 17 | import ( 18 | "encoding/json" 19 | "io" 20 | "net" 21 | "time" 22 | ) 23 | 24 | // connWriter implements LoggerInterface. 25 | // it writes messages in keep-live tcp connection. 26 | type connWriter struct { 27 | lg *logWriter 28 | innerWriter io.WriteCloser 29 | ReconnectOnMsg bool `json:"reconnectOnMsg"` 30 | Reconnect bool `json:"reconnect"` 31 | Net string `json:"net"` 32 | Addr string `json:"addr"` 33 | Level int `json:"level"` 34 | } 35 | 36 | // NewConn create new ConnWrite returning as LoggerInterface. 37 | func NewConn() Logger { 38 | conn := new(connWriter) 39 | conn.Level = LevelTrace 40 | return conn 41 | } 42 | 43 | // Init init connection writer with json config. 44 | // json config only need key "level". 45 | func (c *connWriter) Init(jsonConfig string) error { 46 | return json.Unmarshal([]byte(jsonConfig), c) 47 | } 48 | 49 | // WriteMsg write message in connection. 50 | // if connection is down, try to re-connect. 51 | func (c *connWriter) WriteMsg(when time.Time, msg string, level int) error { 52 | if level > c.Level { 53 | return nil 54 | } 55 | if c.needToConnectOnMsg() { 56 | err := c.connect() 57 | if err != nil { 58 | return err 59 | } 60 | } 61 | 62 | if c.ReconnectOnMsg { 63 | defer c.innerWriter.Close() 64 | } 65 | 66 | c.lg.println(when, msg) 67 | return nil 68 | } 69 | 70 | // Flush implementing method. empty. 71 | func (c *connWriter) Flush() { 72 | 73 | } 74 | 75 | // Destroy destroy connection writer and close tcp listener. 76 | func (c *connWriter) Destroy() { 77 | if c.innerWriter != nil { 78 | c.innerWriter.Close() 79 | } 80 | } 81 | 82 | func (c *connWriter) connect() error { 83 | if c.innerWriter != nil { 84 | c.innerWriter.Close() 85 | c.innerWriter = nil 86 | } 87 | 88 | conn, err := net.Dial(c.Net, c.Addr) 89 | if err != nil { 90 | return err 91 | } 92 | 93 | if tcpConn, ok := conn.(*net.TCPConn); ok { 94 | tcpConn.SetKeepAlive(true) 95 | } 96 | 97 | c.innerWriter = conn 98 | c.lg = newLogWriter(conn) 99 | return nil 100 | } 101 | 102 | func (c *connWriter) needToConnectOnMsg() bool { 103 | if c.Reconnect { 104 | c.Reconnect = false 105 | return true 106 | } 107 | 108 | if c.innerWriter == nil { 109 | return true 110 | } 111 | 112 | return c.ReconnectOnMsg 113 | } 114 | 115 | func init() { 116 | Register(AdapterConn, NewConn) 117 | } 118 | -------------------------------------------------------------------------------- /internal/logs/conn_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 beego Author. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package logs 16 | 17 | import ( 18 | "testing" 19 | ) 20 | 21 | func TestConn(t *testing.T) { 22 | log := NewLogger(1000) 23 | log.SetLogger("conn", `{"net":"tcp","addr":":7020"}`) 24 | log.Informational("informational") 25 | } 26 | -------------------------------------------------------------------------------- /internal/logs/console.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 beego Author. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package logs 16 | 17 | import ( 18 | "encoding/json" 19 | "os" 20 | "runtime" 21 | "time" 22 | ) 23 | 24 | // brush is a color join function 25 | type brush func(string) string 26 | 27 | // newBrush return a fix color Brush 28 | func newBrush(color string) brush { 29 | pre := "\033[" 30 | reset := "\033[0m" 31 | return func(text string) string { 32 | return pre + color + "m" + text + reset 33 | } 34 | } 35 | 36 | var colors = []brush{ 37 | newBrush("1;37"), // Emergency white 38 | newBrush("1;36"), // Alert cyan 39 | newBrush("1;35"), // Critical magenta 40 | newBrush("1;31"), // Error red 41 | newBrush("1;33"), // Warning yellow 42 | newBrush("1;32"), // Notice green 43 | newBrush("1;34"), // Informational blue 44 | newBrush("1;44"), // Debug Background blue 45 | } 46 | 47 | // consoleWriter implements LoggerInterface and writes messages to terminal. 48 | type consoleWriter struct { 49 | lg *logWriter 50 | Level int `json:"level"` 51 | Colorful bool `json:"color"` //this filed is useful only when system's terminal supports color 52 | } 53 | 54 | // NewConsole create ConsoleWriter returning as LoggerInterface. 55 | func NewConsole() Logger { 56 | cw := &consoleWriter{ 57 | lg: newLogWriter(os.Stdout), 58 | Level: LevelDebug, 59 | Colorful: runtime.GOOS != "windows", 60 | } 61 | return cw 62 | } 63 | 64 | // Init init console logger. 65 | // jsonConfig like '{"level":LevelTrace}'. 66 | func (c *consoleWriter) Init(jsonConfig string) error { 67 | if len(jsonConfig) == 0 { 68 | return nil 69 | } 70 | err := json.Unmarshal([]byte(jsonConfig), c) 71 | if runtime.GOOS == "windows" { 72 | c.Colorful = false 73 | } 74 | return err 75 | } 76 | 77 | // WriteMsg write message in console. 78 | func (c *consoleWriter) WriteMsg(when time.Time, msg string, level int) error { 79 | if level > c.Level { 80 | return nil 81 | } 82 | if c.Colorful { 83 | msg = colors[level](msg) 84 | } 85 | c.lg.println(when, msg) 86 | return nil 87 | } 88 | 89 | // Destroy implementing method. empty. 90 | func (c *consoleWriter) Destroy() { 91 | 92 | } 93 | 94 | // Flush implementing method. empty. 95 | func (c *consoleWriter) Flush() { 96 | 97 | } 98 | 99 | func init() { 100 | Register(AdapterConsole, NewConsole) 101 | } 102 | -------------------------------------------------------------------------------- /internal/logs/console_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 beego Author. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package logs 16 | 17 | import ( 18 | "testing" 19 | ) 20 | 21 | // Try each log level in decreasing order of priority. 22 | func testConsoleCalls(bl *BeeLogger) { 23 | bl.Emergency("emergency") 24 | bl.Alert("alert") 25 | bl.Critical("critical") 26 | bl.Error("error") 27 | bl.Warning("warning") 28 | bl.Notice("notice") 29 | bl.Informational("informational") 30 | bl.Debug("debug") 31 | } 32 | 33 | // Test console logging by visually comparing the lines being output with and 34 | // without a log level specification. 35 | func TestConsole(t *testing.T) { 36 | log1 := NewLogger(10000) 37 | log1.EnableFuncCallDepth(true) 38 | log1.SetLogger("console", "") 39 | testConsoleCalls(log1) 40 | 41 | log2 := NewLogger(100) 42 | log2.SetLogger("console", `{"level":3}`) 43 | testConsoleCalls(log2) 44 | } 45 | 46 | // Test console without color 47 | func TestConsoleNoColor(t *testing.T) { 48 | log := NewLogger(100) 49 | log.SetLogger("console", `{"color":false}`) 50 | testConsoleCalls(log) 51 | } 52 | -------------------------------------------------------------------------------- /internal/logs/es/es.go: -------------------------------------------------------------------------------- 1 | package es 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "net" 8 | "net/url" 9 | "time" 10 | 11 | "github.com/astaxie/beego/logs" 12 | "github.com/belogik/goes" 13 | ) 14 | 15 | // NewES return a LoggerInterface 16 | func NewES() logs.Logger { 17 | cw := &esLogger{ 18 | Level: logs.LevelDebug, 19 | } 20 | return cw 21 | } 22 | 23 | type esLogger struct { 24 | *goes.Connection 25 | DSN string `json:"dsn"` 26 | Level int `json:"level"` 27 | } 28 | 29 | // {"dsn":"http://localhost:9200/","level":1} 30 | func (el *esLogger) Init(jsonconfig string) error { 31 | err := json.Unmarshal([]byte(jsonconfig), el) 32 | if err != nil { 33 | return err 34 | } 35 | if el.DSN == "" { 36 | return errors.New("empty dsn") 37 | } else if u, err := url.Parse(el.DSN); err != nil { 38 | return err 39 | } else if u.Path == "" { 40 | return errors.New("missing prefix") 41 | } else if host, port, err := net.SplitHostPort(u.Host); err != nil { 42 | return err 43 | } else { 44 | conn := goes.NewConnection(host, port) 45 | el.Connection = conn 46 | } 47 | return nil 48 | } 49 | 50 | // WriteMsg will write the msg and level into es 51 | func (el *esLogger) WriteMsg(when time.Time, msg string, level int) error { 52 | if level > el.Level { 53 | return nil 54 | } 55 | 56 | vals := make(map[string]interface{}) 57 | vals["@timestamp"] = when.Format(time.RFC3339) 58 | vals["@msg"] = msg 59 | d := goes.Document{ 60 | Index: fmt.Sprintf("%04d.%02d.%02d", when.Year(), when.Month(), when.Day()), 61 | Type: "logs", 62 | Fields: vals, 63 | } 64 | _, err := el.Index(d, nil) 65 | return err 66 | } 67 | 68 | // Destroy is a empty method 69 | func (el *esLogger) Destroy() { 70 | 71 | } 72 | 73 | // Flush is a empty method 74 | func (el *esLogger) Flush() { 75 | 76 | } 77 | 78 | func init() { 79 | logs.Register(logs.AdapterEs, NewES) 80 | } 81 | -------------------------------------------------------------------------------- /internal/logs/file.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 beego Author. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package logs 16 | 17 | import ( 18 | "bytes" 19 | "encoding/json" 20 | "errors" 21 | "fmt" 22 | "io" 23 | "os" 24 | "path/filepath" 25 | "strconv" 26 | "strings" 27 | "sync" 28 | "time" 29 | ) 30 | 31 | // fileLogWriter implements LoggerInterface. 32 | // It writes messages by lines limit, file size limit, or time frequency. 33 | type fileLogWriter struct { 34 | sync.RWMutex // write log order by order and atomic incr maxLinesCurLines and maxSizeCurSize 35 | // The opened file 36 | Filename string `json:"filename"` 37 | fileWriter *os.File 38 | 39 | // Rotate at line 40 | MaxLines int `json:"maxlines"` 41 | maxLinesCurLines int 42 | 43 | // Rotate at size 44 | MaxSize int `json:"maxsize"` 45 | maxSizeCurSize int 46 | 47 | // Rotate daily 48 | Daily bool `json:"daily"` 49 | MaxDays int64 `json:"maxdays"` 50 | dailyOpenDate int 51 | dailyOpenTime time.Time 52 | 53 | Rotate bool `json:"rotate"` 54 | 55 | Level int `json:"level"` 56 | 57 | Perm string `json:"perm"` 58 | 59 | fileNameOnly, suffix string // like "project.log", project is fileNameOnly and .log is suffix 60 | } 61 | 62 | // newFileWriter create a FileLogWriter returning as LoggerInterface. 63 | func newFileWriter() Logger { 64 | w := &fileLogWriter{ 65 | Daily: true, 66 | MaxDays: 7, 67 | Rotate: true, 68 | Level: LevelTrace, 69 | Perm: "0660", 70 | } 71 | return w 72 | } 73 | 74 | // Init file logger with json config. 75 | // jsonConfig like: 76 | // { 77 | // "filename":"logs/beego.log", 78 | // "maxLines":10000, 79 | // "maxsize":1024, 80 | // "daily":true, 81 | // "maxDays":15, 82 | // "rotate":true, 83 | // "perm":"0600" 84 | // } 85 | func (w *fileLogWriter) Init(jsonConfig string) error { 86 | err := json.Unmarshal([]byte(jsonConfig), w) 87 | if err != nil { 88 | return err 89 | } 90 | if len(w.Filename) == 0 { 91 | return errors.New("jsonconfig must have filename") 92 | } 93 | w.suffix = filepath.Ext(w.Filename) 94 | w.fileNameOnly = strings.TrimSuffix(w.Filename, w.suffix) 95 | if w.suffix == "" { 96 | w.suffix = ".log" 97 | } 98 | err = w.startLogger() 99 | return err 100 | } 101 | 102 | // start file logger. create log file and set to locker-inside file writer. 103 | func (w *fileLogWriter) startLogger() error { 104 | file, err := w.createLogFile() 105 | if err != nil { 106 | return err 107 | } 108 | if w.fileWriter != nil { 109 | w.fileWriter.Close() 110 | } 111 | w.fileWriter = file 112 | return w.initFd() 113 | } 114 | 115 | func (w *fileLogWriter) needRotate(size int, day int) bool { 116 | return (w.MaxLines > 0 && w.maxLinesCurLines >= w.MaxLines) || 117 | (w.MaxSize > 0 && w.maxSizeCurSize >= w.MaxSize) || 118 | (w.Daily && day != w.dailyOpenDate) 119 | 120 | } 121 | 122 | // WriteMsg write logger message into file. 123 | func (w *fileLogWriter) WriteMsg(when time.Time, msg string, level int) error { 124 | if level > w.Level { 125 | return nil 126 | } 127 | h, d := formatTimeHeader(when) 128 | msg = string(h) + msg + "\n" 129 | if w.Rotate { 130 | w.RLock() 131 | if w.needRotate(len(msg), d) { 132 | w.RUnlock() 133 | w.Lock() 134 | if w.needRotate(len(msg), d) { 135 | if err := w.doRotate(when); err != nil { 136 | fmt.Fprintf(os.Stderr, "FileLogWriter(%q): %s\n", w.Filename, err) 137 | } 138 | } 139 | w.Unlock() 140 | } else { 141 | w.RUnlock() 142 | } 143 | } 144 | 145 | w.Lock() 146 | _, err := w.fileWriter.Write([]byte(msg)) 147 | if err == nil { 148 | w.maxLinesCurLines++ 149 | w.maxSizeCurSize += len(msg) 150 | } 151 | w.Unlock() 152 | return err 153 | } 154 | 155 | func (w *fileLogWriter) createLogFile() (*os.File, error) { 156 | // Open the log file 157 | perm, err := strconv.ParseInt(w.Perm, 8, 64) 158 | if err != nil { 159 | return nil, err 160 | } 161 | fd, err := os.OpenFile(w.Filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, os.FileMode(perm)) 162 | if err == nil { 163 | // Make sure file perm is user set perm cause of `os.OpenFile` will obey umask 164 | os.Chmod(w.Filename, os.FileMode(perm)) 165 | } 166 | return fd, err 167 | } 168 | 169 | func (w *fileLogWriter) initFd() error { 170 | fd := w.fileWriter 171 | fInfo, err := fd.Stat() 172 | if err != nil { 173 | return fmt.Errorf("get stat err: %s", err) 174 | } 175 | w.maxSizeCurSize = int(fInfo.Size()) 176 | w.dailyOpenTime = time.Now() 177 | w.dailyOpenDate = w.dailyOpenTime.Day() 178 | w.maxLinesCurLines = 0 179 | if w.Daily { 180 | go w.dailyRotate(w.dailyOpenTime) 181 | } 182 | if fInfo.Size() > 0 { 183 | count, err := w.lines() 184 | if err != nil { 185 | return err 186 | } 187 | w.maxLinesCurLines = count 188 | } 189 | return nil 190 | } 191 | 192 | func (w *fileLogWriter) dailyRotate(openTime time.Time) { 193 | y, m, d := openTime.Add(24 * time.Hour).Date() 194 | nextDay := time.Date(y, m, d, 0, 0, 0, 0, openTime.Location()) 195 | tm := time.NewTimer(time.Duration(nextDay.UnixNano() - openTime.UnixNano() + 100)) 196 | <-tm.C 197 | w.Lock() 198 | if w.needRotate(0, time.Now().Day()) { 199 | if err := w.doRotate(time.Now()); err != nil { 200 | fmt.Fprintf(os.Stderr, "FileLogWriter(%q): %s\n", w.Filename, err) 201 | } 202 | } 203 | w.Unlock() 204 | } 205 | 206 | func (w *fileLogWriter) lines() (int, error) { 207 | fd, err := os.Open(w.Filename) 208 | if err != nil { 209 | return 0, err 210 | } 211 | defer fd.Close() 212 | 213 | buf := make([]byte, 32768) // 32k 214 | count := 0 215 | lineSep := []byte{'\n'} 216 | 217 | for { 218 | c, err := fd.Read(buf) 219 | if err != nil && err != io.EOF { 220 | return count, err 221 | } 222 | 223 | count += bytes.Count(buf[:c], lineSep) 224 | 225 | if err == io.EOF { 226 | break 227 | } 228 | } 229 | 230 | return count, nil 231 | } 232 | 233 | // DoRotate means it need to write file in new file. 234 | // new file name like xx.2013-01-01.log (daily) or xx.001.log (by line or size) 235 | func (w *fileLogWriter) doRotate(logTime time.Time) error { 236 | // file exists 237 | // Find the next available number 238 | num := 1 239 | fName := "" 240 | 241 | _, err := os.Lstat(w.Filename) 242 | if err != nil { 243 | //even if the file is not exist or other ,we should RESTART the logger 244 | goto RESTART_LOGGER 245 | } 246 | 247 | if w.MaxLines > 0 || w.MaxSize > 0 { 248 | for ; err == nil && num <= 999; num++ { 249 | fName = w.fileNameOnly + fmt.Sprintf(".%s.%03d%s", logTime.Format("2006-01-02"), num, w.suffix) 250 | _, err = os.Lstat(fName) 251 | } 252 | } else { 253 | fName = fmt.Sprintf("%s.%s%s", w.fileNameOnly, w.dailyOpenTime.Format("2006-01-02"), w.suffix) 254 | _, err = os.Lstat(fName) 255 | for ; err == nil && num <= 999; num++ { 256 | fName = w.fileNameOnly + fmt.Sprintf(".%s.%03d%s", w.dailyOpenTime.Format("2006-01-02"), num, w.suffix) 257 | _, err = os.Lstat(fName) 258 | } 259 | } 260 | // return error if the last file checked still existed 261 | if err == nil { 262 | return fmt.Errorf("Rotate: Cannot find free log number to rename %s", w.Filename) 263 | } 264 | 265 | // close fileWriter before rename 266 | w.fileWriter.Close() 267 | 268 | // Rename the file to its new found name 269 | // even if occurs error,we MUST guarantee to restart new logger 270 | err = os.Rename(w.Filename, fName) 271 | if err != nil { 272 | goto RESTART_LOGGER 273 | } 274 | err = os.Chmod(fName, os.FileMode(0440)) 275 | // re-start logger 276 | RESTART_LOGGER: 277 | 278 | startLoggerErr := w.startLogger() 279 | go w.deleteOldLog() 280 | 281 | if startLoggerErr != nil { 282 | return fmt.Errorf("Rotate StartLogger: %s", startLoggerErr) 283 | } 284 | if err != nil { 285 | return fmt.Errorf("Rotate: %s", err) 286 | } 287 | return nil 288 | } 289 | 290 | func (w *fileLogWriter) deleteOldLog() { 291 | dir := filepath.Dir(w.Filename) 292 | filepath.Walk(dir, func(path string, info os.FileInfo, err error) (returnErr error) { 293 | defer func() { 294 | if r := recover(); r != nil { 295 | fmt.Fprintf(os.Stderr, "Unable to delete old log '%s', error: %v\n", path, r) 296 | } 297 | }() 298 | 299 | if info == nil { 300 | return 301 | } 302 | 303 | if !info.IsDir() && info.ModTime().Add(24*time.Hour*time.Duration(w.MaxDays)).Before(time.Now()) { 304 | if strings.HasPrefix(filepath.Base(path), filepath.Base(w.fileNameOnly)) && 305 | strings.HasSuffix(filepath.Base(path), w.suffix) { 306 | os.Remove(path) 307 | } 308 | } 309 | return 310 | }) 311 | } 312 | 313 | // Destroy close the file description, close file writer. 314 | func (w *fileLogWriter) Destroy() { 315 | w.fileWriter.Close() 316 | } 317 | 318 | // Flush flush file logger. 319 | // there are no buffering messages in file logger in memory. 320 | // flush file means sync file from disk. 321 | func (w *fileLogWriter) Flush() { 322 | w.fileWriter.Sync() 323 | } 324 | 325 | func init() { 326 | Register(AdapterFile, newFileWriter) 327 | } 328 | -------------------------------------------------------------------------------- /internal/logs/file_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 beego Author. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package logs 16 | 17 | import ( 18 | "bufio" 19 | "fmt" 20 | "io/ioutil" 21 | "os" 22 | "strconv" 23 | "testing" 24 | "time" 25 | ) 26 | 27 | func TestFilePerm(t *testing.T) { 28 | log := NewLogger(10000) 29 | // use 0666 as test perm cause the default umask is 022 30 | log.SetLogger("file", `{"filename":"test.log", "perm": "0666"}`) 31 | log.Debug("debug") 32 | log.Informational("info") 33 | log.Notice("notice") 34 | log.Warning("warning") 35 | log.Error("error") 36 | log.Alert("alert") 37 | log.Critical("critical") 38 | log.Emergency("emergency") 39 | file, err := os.Stat("test.log") 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | if file.Mode() != 0666 { 44 | t.Fatal("unexpected log file permission") 45 | } 46 | os.Remove("test.log") 47 | } 48 | 49 | func TestFile1(t *testing.T) { 50 | log := NewLogger(10000) 51 | log.SetLogger("file", `{"filename":"test.log"}`) 52 | log.Debug("debug") 53 | log.Informational("info") 54 | log.Notice("notice") 55 | log.Warning("warning") 56 | log.Error("error") 57 | log.Alert("alert") 58 | log.Critical("critical") 59 | log.Emergency("emergency") 60 | f, err := os.Open("test.log") 61 | if err != nil { 62 | t.Fatal(err) 63 | } 64 | b := bufio.NewReader(f) 65 | lineNum := 0 66 | for { 67 | line, _, err := b.ReadLine() 68 | if err != nil { 69 | break 70 | } 71 | if len(line) > 0 { 72 | lineNum++ 73 | } 74 | } 75 | var expected = LevelDebug + 1 76 | if lineNum != expected { 77 | t.Fatal(lineNum, "not "+strconv.Itoa(expected)+" lines") 78 | } 79 | os.Remove("test.log") 80 | } 81 | 82 | func TestFile2(t *testing.T) { 83 | log := NewLogger(10000) 84 | log.SetLogger("file", fmt.Sprintf(`{"filename":"test2.log","level":%d}`, LevelError)) 85 | log.Debug("debug") 86 | log.Info("info") 87 | log.Notice("notice") 88 | log.Warning("warning") 89 | log.Error("error") 90 | log.Alert("alert") 91 | log.Critical("critical") 92 | log.Emergency("emergency") 93 | f, err := os.Open("test2.log") 94 | if err != nil { 95 | t.Fatal(err) 96 | } 97 | b := bufio.NewReader(f) 98 | lineNum := 0 99 | for { 100 | line, _, err := b.ReadLine() 101 | if err != nil { 102 | break 103 | } 104 | if len(line) > 0 { 105 | lineNum++ 106 | } 107 | } 108 | var expected = LevelError + 1 109 | if lineNum != expected { 110 | t.Fatal(lineNum, "not "+strconv.Itoa(expected)+" lines") 111 | } 112 | os.Remove("test2.log") 113 | } 114 | 115 | func TestFileRotate_01(t *testing.T) { 116 | log := NewLogger(10000) 117 | log.SetLogger("file", `{"filename":"test3.log","maxlines":4}`) 118 | log.Debug("debug") 119 | log.Info("info") 120 | log.Notice("notice") 121 | log.Warning("warning") 122 | log.Error("error") 123 | log.Alert("alert") 124 | log.Critical("critical") 125 | log.Emergency("emergency") 126 | rotateName := "test3" + fmt.Sprintf(".%s.%03d", time.Now().Format("2006-01-02"), 1) + ".log" 127 | b, err := exists(rotateName) 128 | if !b || err != nil { 129 | os.Remove("test3.log") 130 | t.Fatal("rotate not generated") 131 | } 132 | os.Remove(rotateName) 133 | os.Remove("test3.log") 134 | } 135 | 136 | func TestFileRotate_02(t *testing.T) { 137 | fn1 := "rotate_day.log" 138 | fn2 := "rotate_day." + time.Now().Add(-24*time.Hour).Format("2006-01-02") + ".log" 139 | testFileRotate(t, fn1, fn2) 140 | } 141 | 142 | func TestFileRotate_03(t *testing.T) { 143 | fn1 := "rotate_day.log" 144 | fn := "rotate_day." + time.Now().Add(-24*time.Hour).Format("2006-01-02") + ".log" 145 | os.Create(fn) 146 | fn2 := "rotate_day." + time.Now().Add(-24*time.Hour).Format("2006-01-02") + ".001.log" 147 | testFileRotate(t, fn1, fn2) 148 | os.Remove(fn) 149 | } 150 | 151 | func TestFileRotate_04(t *testing.T) { 152 | fn1 := "rotate_day.log" 153 | fn2 := "rotate_day." + time.Now().Add(-24*time.Hour).Format("2006-01-02") + ".log" 154 | testFileDailyRotate(t, fn1, fn2) 155 | } 156 | 157 | func TestFileRotate_05(t *testing.T) { 158 | fn1 := "rotate_day.log" 159 | fn := "rotate_day." + time.Now().Add(-24*time.Hour).Format("2006-01-02") + ".log" 160 | os.Create(fn) 161 | fn2 := "rotate_day." + time.Now().Add(-24*time.Hour).Format("2006-01-02") + ".001.log" 162 | testFileDailyRotate(t, fn1, fn2) 163 | os.Remove(fn) 164 | } 165 | func TestFileRotate_06(t *testing.T) { //test file mode 166 | log := NewLogger(10000) 167 | log.SetLogger("file", `{"filename":"test3.log","maxlines":4}`) 168 | log.Debug("debug") 169 | log.Info("info") 170 | log.Notice("notice") 171 | log.Warning("warning") 172 | log.Error("error") 173 | log.Alert("alert") 174 | log.Critical("critical") 175 | log.Emergency("emergency") 176 | rotateName := "test3" + fmt.Sprintf(".%s.%03d", time.Now().Format("2006-01-02"), 1) + ".log" 177 | s, _ := os.Lstat(rotateName) 178 | if s.Mode() != 0440 { 179 | os.Remove(rotateName) 180 | os.Remove("test3.log") 181 | t.Fatal("rotate file mode error") 182 | } 183 | os.Remove(rotateName) 184 | os.Remove("test3.log") 185 | } 186 | func testFileRotate(t *testing.T, fn1, fn2 string) { 187 | fw := &fileLogWriter{ 188 | Daily: true, 189 | MaxDays: 7, 190 | Rotate: true, 191 | Level: LevelTrace, 192 | Perm: "0660", 193 | } 194 | fw.Init(fmt.Sprintf(`{"filename":"%v","maxdays":1}`, fn1)) 195 | fw.dailyOpenTime = time.Now().Add(-24 * time.Hour) 196 | fw.dailyOpenDate = fw.dailyOpenTime.Day() 197 | fw.WriteMsg(time.Now(), "this is a msg for test", LevelDebug) 198 | 199 | for _, file := range []string{fn1, fn2} { 200 | _, err := os.Stat(file) 201 | if err != nil { 202 | t.FailNow() 203 | } 204 | os.Remove(file) 205 | } 206 | fw.Destroy() 207 | } 208 | 209 | func testFileDailyRotate(t *testing.T, fn1, fn2 string) { 210 | fw := &fileLogWriter{ 211 | Daily: true, 212 | MaxDays: 7, 213 | Rotate: true, 214 | Level: LevelTrace, 215 | Perm: "0660", 216 | } 217 | fw.Init(fmt.Sprintf(`{"filename":"%v","maxdays":1}`, fn1)) 218 | fw.dailyOpenTime = time.Now().Add(-24 * time.Hour) 219 | fw.dailyOpenDate = fw.dailyOpenTime.Day() 220 | today, _ := time.ParseInLocation("2006-01-02", time.Now().Format("2006-01-02"), fw.dailyOpenTime.Location()) 221 | today = today.Add(-1 * time.Second) 222 | fw.dailyRotate(today) 223 | for _, file := range []string{fn1, fn2} { 224 | _, err := os.Stat(file) 225 | if err != nil { 226 | t.FailNow() 227 | } 228 | content, err := ioutil.ReadFile(file) 229 | if err != nil { 230 | t.FailNow() 231 | } 232 | if len(content) > 0 { 233 | t.FailNow() 234 | } 235 | os.Remove(file) 236 | } 237 | fw.Destroy() 238 | } 239 | 240 | func exists(path string) (bool, error) { 241 | _, err := os.Stat(path) 242 | if err == nil { 243 | return true, nil 244 | } 245 | if os.IsNotExist(err) { 246 | return false, nil 247 | } 248 | return false, err 249 | } 250 | 251 | func BenchmarkFile(b *testing.B) { 252 | log := NewLogger(100000) 253 | log.SetLogger("file", `{"filename":"test4.log"}`) 254 | for i := 0; i < b.N; i++ { 255 | log.Debug("debug") 256 | } 257 | os.Remove("test4.log") 258 | } 259 | 260 | func BenchmarkFileAsynchronous(b *testing.B) { 261 | log := NewLogger(100000) 262 | log.SetLogger("file", `{"filename":"test4.log"}`) 263 | log.Async() 264 | for i := 0; i < b.N; i++ { 265 | log.Debug("debug") 266 | } 267 | os.Remove("test4.log") 268 | } 269 | 270 | func BenchmarkFileCallDepth(b *testing.B) { 271 | log := NewLogger(100000) 272 | log.SetLogger("file", `{"filename":"test4.log"}`) 273 | log.EnableFuncCallDepth(true) 274 | log.SetLogFuncCallDepth(2) 275 | for i := 0; i < b.N; i++ { 276 | log.Debug("debug") 277 | } 278 | os.Remove("test4.log") 279 | } 280 | 281 | func BenchmarkFileAsynchronousCallDepth(b *testing.B) { 282 | log := NewLogger(100000) 283 | log.SetLogger("file", `{"filename":"test4.log"}`) 284 | log.EnableFuncCallDepth(true) 285 | log.SetLogFuncCallDepth(2) 286 | log.Async() 287 | for i := 0; i < b.N; i++ { 288 | log.Debug("debug") 289 | } 290 | os.Remove("test4.log") 291 | } 292 | 293 | func BenchmarkFileOnGoroutine(b *testing.B) { 294 | log := NewLogger(100000) 295 | log.SetLogger("file", `{"filename":"test4.log"}`) 296 | for i := 0; i < b.N; i++ { 297 | go log.Debug("debug") 298 | } 299 | os.Remove("test4.log") 300 | } 301 | -------------------------------------------------------------------------------- /internal/logs/jianliao.go: -------------------------------------------------------------------------------- 1 | package logs 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | "time" 9 | ) 10 | 11 | // JLWriter implements beego LoggerInterface and is used to send jiaoliao webhook 12 | type JLWriter struct { 13 | AuthorName string `json:"authorname"` 14 | Title string `json:"title"` 15 | WebhookURL string `json:"webhookurl"` 16 | RedirectURL string `json:"redirecturl,omitempty"` 17 | ImageURL string `json:"imageurl,omitempty"` 18 | Level int `json:"level"` 19 | } 20 | 21 | // newJLWriter create jiaoliao writer. 22 | func newJLWriter() Logger { 23 | return &JLWriter{Level: LevelTrace} 24 | } 25 | 26 | // Init JLWriter with json config string 27 | func (s *JLWriter) Init(jsonconfig string) error { 28 | return json.Unmarshal([]byte(jsonconfig), s) 29 | } 30 | 31 | // WriteMsg write message in smtp writer. 32 | // it will send an email with subject and only this message. 33 | func (s *JLWriter) WriteMsg(when time.Time, msg string, level int) error { 34 | if level > s.Level { 35 | return nil 36 | } 37 | 38 | text := fmt.Sprintf("%s %s", when.Format("2006-01-02 15:04:05"), msg) 39 | 40 | form := url.Values{} 41 | form.Add("authorName", s.AuthorName) 42 | form.Add("title", s.Title) 43 | form.Add("text", text) 44 | if s.RedirectURL != "" { 45 | form.Add("redirectUrl", s.RedirectURL) 46 | } 47 | if s.ImageURL != "" { 48 | form.Add("imageUrl", s.ImageURL) 49 | } 50 | 51 | resp, err := http.PostForm(s.WebhookURL, form) 52 | if err != nil { 53 | return err 54 | } 55 | defer resp.Body.Close() 56 | if resp.StatusCode != http.StatusOK { 57 | return fmt.Errorf("Post webhook failed %s %d", resp.Status, resp.StatusCode) 58 | } 59 | return nil 60 | } 61 | 62 | // Flush implementing method. empty. 63 | func (s *JLWriter) Flush() { 64 | } 65 | 66 | // Destroy implementing method. empty. 67 | func (s *JLWriter) Destroy() { 68 | } 69 | 70 | func init() { 71 | Register(AdapterJianLiao, newJLWriter) 72 | } 73 | -------------------------------------------------------------------------------- /internal/logs/log.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 beego Author. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package logs provide a general log interface 16 | // Usage: 17 | // 18 | // import "github.com/astaxie/beego/logs" 19 | // 20 | // log := NewLogger(10000) 21 | // log.SetLogger("console", "") 22 | // 23 | // > the first params stand for how many channel 24 | // 25 | // Use it like this: 26 | // 27 | // log.Trace("trace") 28 | // log.Info("info") 29 | // log.Warn("warning") 30 | // log.Debug("debug") 31 | // log.Critical("critical") 32 | // 33 | // more docs http://beego.me/docs/module/logs.md 34 | package logs 35 | 36 | import ( 37 | "fmt" 38 | "log" 39 | "os" 40 | "path" 41 | "runtime" 42 | "strconv" 43 | "strings" 44 | "sync" 45 | "time" 46 | ) 47 | 48 | // RFC5424 log message levels. 49 | const ( 50 | LevelEmergency = iota 51 | LevelAlert 52 | LevelCritical 53 | LevelError 54 | LevelWarning 55 | LevelNotice 56 | LevelInformational 57 | LevelDebug 58 | ) 59 | 60 | // levelLogLogger is defined to implement log.Logger 61 | // the real log level will be LevelEmergency 62 | const levelLoggerImpl = -1 63 | 64 | // Name for adapter with beego official support 65 | const ( 66 | AdapterConsole = "console" 67 | AdapterFile = "file" 68 | AdapterMultiFile = "multifile" 69 | AdapterMail = "smtp" 70 | AdapterConn = "conn" 71 | AdapterEs = "es" 72 | AdapterJianLiao = "jianliao" 73 | AdapterSlack = "slack" 74 | AdapterAliLS = "alils" 75 | ) 76 | 77 | // Legacy log level constants to ensure backwards compatibility. 78 | const ( 79 | LevelInfo = LevelInformational 80 | LevelTrace = LevelDebug 81 | LevelWarn = LevelWarning 82 | ) 83 | 84 | type newLoggerFunc func() Logger 85 | 86 | // Logger defines the behavior of a log provider. 87 | type Logger interface { 88 | Init(config string) error 89 | WriteMsg(when time.Time, msg string, level int) error 90 | Destroy() 91 | Flush() 92 | } 93 | 94 | var adapters = make(map[string]newLoggerFunc) 95 | var levelPrefix = [LevelDebug + 1]string{"[M] ", "[A] ", "[C] ", "[E] ", "[W] ", "[N] ", "[I] ", "[D] "} 96 | 97 | // Register makes a log provide available by the provided name. 98 | // If Register is called twice with the same name or if driver is nil, 99 | // it panics. 100 | func Register(name string, log newLoggerFunc) { 101 | if log == nil { 102 | panic("logs: Register provide is nil") 103 | } 104 | if _, dup := adapters[name]; dup { 105 | panic("logs: Register called twice for provider " + name) 106 | } 107 | adapters[name] = log 108 | } 109 | 110 | // BeeLogger is default logger in beego application. 111 | // it can contain several providers and log message into all providers. 112 | type BeeLogger struct { 113 | lock sync.Mutex 114 | level int 115 | init bool 116 | enableFuncCallDepth bool 117 | loggerFuncCallDepth int 118 | asynchronous bool 119 | msgChanLen int64 120 | msgChan chan *logMsg 121 | signalChan chan string 122 | wg sync.WaitGroup 123 | outputs []*nameLogger 124 | } 125 | 126 | const defaultAsyncMsgLen = 1e3 127 | 128 | type nameLogger struct { 129 | Logger 130 | name string 131 | } 132 | 133 | type logMsg struct { 134 | level int 135 | msg string 136 | when time.Time 137 | } 138 | 139 | var logMsgPool *sync.Pool 140 | 141 | // NewLogger returns a new BeeLogger. 142 | // channelLen means the number of messages in chan(used where asynchronous is true). 143 | // if the buffering chan is full, logger adapters write to file or other way. 144 | func NewLogger(channelLens ...int64) *BeeLogger { 145 | bl := new(BeeLogger) 146 | bl.level = LevelDebug 147 | bl.loggerFuncCallDepth = 2 148 | bl.msgChanLen = append(channelLens, 0)[0] 149 | if bl.msgChanLen <= 0 { 150 | bl.msgChanLen = defaultAsyncMsgLen 151 | } 152 | bl.signalChan = make(chan string, 1) 153 | bl.setLogger(AdapterConsole) 154 | return bl 155 | } 156 | 157 | // Async set the log to asynchronous and start the goroutine 158 | func (bl *BeeLogger) Async(msgLen ...int64) *BeeLogger { 159 | bl.lock.Lock() 160 | defer bl.lock.Unlock() 161 | if bl.asynchronous { 162 | return bl 163 | } 164 | bl.asynchronous = true 165 | if len(msgLen) > 0 && msgLen[0] > 0 { 166 | bl.msgChanLen = msgLen[0] 167 | } 168 | bl.msgChan = make(chan *logMsg, bl.msgChanLen) 169 | logMsgPool = &sync.Pool{ 170 | New: func() interface{} { 171 | return &logMsg{} 172 | }, 173 | } 174 | bl.wg.Add(1) 175 | go bl.startLogger() 176 | return bl 177 | } 178 | 179 | // SetLogger provides a given logger adapter into BeeLogger with config string. 180 | // config need to be correct JSON as string: {"interval":360}. 181 | func (bl *BeeLogger) setLogger(adapterName string, configs ...string) error { 182 | config := append(configs, "{}")[0] 183 | for _, l := range bl.outputs { 184 | if l.name == adapterName { 185 | return fmt.Errorf("logs: duplicate adaptername %q (you have set this logger before)", adapterName) 186 | } 187 | } 188 | 189 | log, ok := adapters[adapterName] 190 | if !ok { 191 | return fmt.Errorf("logs: unknown adaptername %q (forgotten Register?)", adapterName) 192 | } 193 | 194 | lg := log() 195 | err := lg.Init(config) 196 | if err != nil { 197 | fmt.Fprintln(os.Stderr, "logs.BeeLogger.SetLogger: "+err.Error()) 198 | return err 199 | } 200 | bl.outputs = append(bl.outputs, &nameLogger{name: adapterName, Logger: lg}) 201 | return nil 202 | } 203 | 204 | // SetLogger provides a given logger adapter into BeeLogger with config string. 205 | // config need to be correct JSON as string: {"interval":360}. 206 | func (bl *BeeLogger) SetLogger(adapterName string, configs ...string) error { 207 | bl.lock.Lock() 208 | defer bl.lock.Unlock() 209 | if !bl.init { 210 | bl.outputs = []*nameLogger{} 211 | bl.init = true 212 | } 213 | return bl.setLogger(adapterName, configs...) 214 | } 215 | 216 | // DelLogger remove a logger adapter in BeeLogger. 217 | func (bl *BeeLogger) DelLogger(adapterName string) error { 218 | bl.lock.Lock() 219 | defer bl.lock.Unlock() 220 | outputs := []*nameLogger{} 221 | for _, lg := range bl.outputs { 222 | if lg.name == adapterName { 223 | lg.Destroy() 224 | } else { 225 | outputs = append(outputs, lg) 226 | } 227 | } 228 | if len(outputs) == len(bl.outputs) { 229 | return fmt.Errorf("logs: unknown adaptername %q (forgotten Register?)", adapterName) 230 | } 231 | bl.outputs = outputs 232 | return nil 233 | } 234 | 235 | func (bl *BeeLogger) writeToLoggers(when time.Time, msg string, level int) { 236 | for _, l := range bl.outputs { 237 | err := l.WriteMsg(when, msg, level) 238 | if err != nil { 239 | fmt.Fprintf(os.Stderr, "unable to WriteMsg to adapter:%v,error:%v\n", l.name, err) 240 | } 241 | } 242 | } 243 | 244 | func (bl *BeeLogger) Write(p []byte) (n int, err error) { 245 | if len(p) == 0 { 246 | return 0, nil 247 | } 248 | // writeMsg will always add a '\n' character 249 | if p[len(p)-1] == '\n' { 250 | p = p[0 : len(p)-1] 251 | } 252 | // set levelLoggerImpl to ensure all log message will be write out 253 | err = bl.writeMsg(levelLoggerImpl, string(p)) 254 | if err == nil { 255 | return len(p), err 256 | } 257 | return 0, err 258 | } 259 | 260 | func (bl *BeeLogger) writeMsg(logLevel int, msg string, v ...interface{}) error { 261 | if !bl.init { 262 | bl.lock.Lock() 263 | bl.setLogger(AdapterConsole) 264 | bl.lock.Unlock() 265 | } 266 | 267 | if len(v) > 0 { 268 | msg = fmt.Sprintf(msg, v...) 269 | } 270 | when := time.Now() 271 | if bl.enableFuncCallDepth { 272 | _, file, line, ok := runtime.Caller(bl.loggerFuncCallDepth) 273 | if !ok { 274 | file = "???" 275 | line = 0 276 | } 277 | _, filename := path.Split(file) 278 | msg = "[" + filename + ":" + strconv.Itoa(line) + "] " + msg 279 | } 280 | 281 | //set level info in front of filename info 282 | if logLevel == levelLoggerImpl { 283 | // set to emergency to ensure all log will be print out correctly 284 | logLevel = LevelEmergency 285 | } else { 286 | msg = levelPrefix[logLevel] + msg 287 | } 288 | 289 | if bl.asynchronous { 290 | lm := logMsgPool.Get().(*logMsg) 291 | lm.level = logLevel 292 | lm.msg = msg 293 | lm.when = when 294 | bl.msgChan <- lm 295 | } else { 296 | bl.writeToLoggers(when, msg, logLevel) 297 | } 298 | return nil 299 | } 300 | 301 | // SetLevel Set log message level. 302 | // If message level (such as LevelDebug) is higher than logger level (such as LevelWarning), 303 | // log providers will not even be sent the message. 304 | func (bl *BeeLogger) SetLevel(l int) { 305 | bl.level = l 306 | } 307 | 308 | // SetLogFuncCallDepth set log funcCallDepth 309 | func (bl *BeeLogger) SetLogFuncCallDepth(d int) { 310 | bl.loggerFuncCallDepth = d 311 | } 312 | 313 | // GetLogFuncCallDepth return log funcCallDepth for wrapper 314 | func (bl *BeeLogger) GetLogFuncCallDepth() int { 315 | return bl.loggerFuncCallDepth 316 | } 317 | 318 | // EnableFuncCallDepth enable log funcCallDepth 319 | func (bl *BeeLogger) EnableFuncCallDepth(b bool) { 320 | bl.enableFuncCallDepth = b 321 | } 322 | 323 | // start logger chan reading. 324 | // when chan is not empty, write logs. 325 | func (bl *BeeLogger) startLogger() { 326 | gameOver := false 327 | for { 328 | select { 329 | case bm := <-bl.msgChan: 330 | bl.writeToLoggers(bm.when, bm.msg, bm.level) 331 | logMsgPool.Put(bm) 332 | case sg := <-bl.signalChan: 333 | // Now should only send "flush" or "close" to bl.signalChan 334 | bl.flush() 335 | if sg == "close" { 336 | for _, l := range bl.outputs { 337 | l.Destroy() 338 | } 339 | bl.outputs = nil 340 | gameOver = true 341 | } 342 | bl.wg.Done() 343 | } 344 | if gameOver { 345 | break 346 | } 347 | } 348 | } 349 | 350 | // Emergency Log EMERGENCY level message. 351 | func (bl *BeeLogger) Emergency(format string, v ...interface{}) { 352 | if LevelEmergency > bl.level { 353 | return 354 | } 355 | bl.writeMsg(LevelEmergency, format, v...) 356 | } 357 | 358 | // Alert Log ALERT level message. 359 | func (bl *BeeLogger) Alert(format string, v ...interface{}) { 360 | if LevelAlert > bl.level { 361 | return 362 | } 363 | bl.writeMsg(LevelAlert, format, v...) 364 | } 365 | 366 | // Critical Log CRITICAL level message. 367 | func (bl *BeeLogger) Critical(format string, v ...interface{}) { 368 | if LevelCritical > bl.level { 369 | return 370 | } 371 | bl.writeMsg(LevelCritical, format, v...) 372 | } 373 | 374 | // Error Log ERROR level message. 375 | func (bl *BeeLogger) Error(format string, v ...interface{}) { 376 | if LevelError > bl.level { 377 | return 378 | } 379 | bl.writeMsg(LevelError, format, v...) 380 | } 381 | 382 | // Warning Log WARNING level message. 383 | func (bl *BeeLogger) Warning(format string, v ...interface{}) { 384 | if LevelWarn > bl.level { 385 | return 386 | } 387 | bl.writeMsg(LevelWarn, format, v...) 388 | } 389 | 390 | // Notice Log NOTICE level message. 391 | func (bl *BeeLogger) Notice(format string, v ...interface{}) { 392 | if LevelNotice > bl.level { 393 | return 394 | } 395 | bl.writeMsg(LevelNotice, format, v...) 396 | } 397 | 398 | // Informational Log INFORMATIONAL level message. 399 | func (bl *BeeLogger) Informational(format string, v ...interface{}) { 400 | if LevelInfo > bl.level { 401 | return 402 | } 403 | bl.writeMsg(LevelInfo, format, v...) 404 | } 405 | 406 | // Debug Log DEBUG level message. 407 | func (bl *BeeLogger) Debug(format string, v ...interface{}) { 408 | if LevelDebug > bl.level { 409 | return 410 | } 411 | bl.writeMsg(LevelDebug, format, v...) 412 | } 413 | 414 | // Warn Log WARN level message. 415 | // compatibility alias for Warning() 416 | func (bl *BeeLogger) Warn(format string, v ...interface{}) { 417 | if LevelWarn > bl.level { 418 | return 419 | } 420 | bl.writeMsg(LevelWarn, format, v...) 421 | } 422 | 423 | // Info Log INFO level message. 424 | // compatibility alias for Informational() 425 | func (bl *BeeLogger) Info(format string, v ...interface{}) { 426 | if LevelInfo > bl.level { 427 | return 428 | } 429 | bl.writeMsg(LevelInfo, format, v...) 430 | } 431 | 432 | // Trace Log TRACE level message. 433 | // compatibility alias for Debug() 434 | func (bl *BeeLogger) Trace(format string, v ...interface{}) { 435 | if LevelDebug > bl.level { 436 | return 437 | } 438 | bl.writeMsg(LevelDebug, format, v...) 439 | } 440 | 441 | // Flush flush all chan data. 442 | func (bl *BeeLogger) Flush() { 443 | if bl.asynchronous { 444 | bl.signalChan <- "flush" 445 | bl.wg.Wait() 446 | bl.wg.Add(1) 447 | return 448 | } 449 | bl.flush() 450 | } 451 | 452 | // Close close logger, flush all chan data and destroy all adapters in BeeLogger. 453 | func (bl *BeeLogger) Close() { 454 | if bl.asynchronous { 455 | bl.signalChan <- "close" 456 | bl.wg.Wait() 457 | close(bl.msgChan) 458 | } else { 459 | bl.flush() 460 | for _, l := range bl.outputs { 461 | l.Destroy() 462 | } 463 | bl.outputs = nil 464 | } 465 | close(bl.signalChan) 466 | } 467 | 468 | // Reset close all outputs, and set bl.outputs to nil 469 | func (bl *BeeLogger) Reset() { 470 | bl.Flush() 471 | for _, l := range bl.outputs { 472 | l.Destroy() 473 | } 474 | bl.outputs = nil 475 | } 476 | 477 | func (bl *BeeLogger) flush() { 478 | if bl.asynchronous { 479 | for { 480 | if len(bl.msgChan) > 0 { 481 | bm := <-bl.msgChan 482 | bl.writeToLoggers(bm.when, bm.msg, bm.level) 483 | logMsgPool.Put(bm) 484 | continue 485 | } 486 | break 487 | } 488 | } 489 | for _, l := range bl.outputs { 490 | l.Flush() 491 | } 492 | } 493 | 494 | // beeLogger references the used application logger. 495 | var beeLogger = NewLogger() 496 | 497 | // GetBeeLogger returns the default BeeLogger 498 | func GetBeeLogger() *BeeLogger { 499 | return beeLogger 500 | } 501 | 502 | var beeLoggerMap = struct { 503 | sync.RWMutex 504 | logs map[string]*log.Logger 505 | }{ 506 | logs: map[string]*log.Logger{}, 507 | } 508 | 509 | // GetLogger returns the default BeeLogger 510 | func GetLogger(prefixes ...string) *log.Logger { 511 | prefix := append(prefixes, "")[0] 512 | if prefix != "" { 513 | prefix = fmt.Sprintf(`[%s] `, strings.ToUpper(prefix)) 514 | } 515 | beeLoggerMap.RLock() 516 | l, ok := beeLoggerMap.logs[prefix] 517 | if ok { 518 | beeLoggerMap.RUnlock() 519 | return l 520 | } 521 | beeLoggerMap.RUnlock() 522 | beeLoggerMap.Lock() 523 | defer beeLoggerMap.Unlock() 524 | l, ok = beeLoggerMap.logs[prefix] 525 | if !ok { 526 | l = log.New(beeLogger, prefix, 0) 527 | beeLoggerMap.logs[prefix] = l 528 | } 529 | return l 530 | } 531 | 532 | // Reset will remove all the adapter 533 | func Reset() { 534 | beeLogger.Reset() 535 | } 536 | 537 | // Async set the beelogger with Async mode and hold msglen messages 538 | func Async(msgLen ...int64) *BeeLogger { 539 | return beeLogger.Async(msgLen...) 540 | } 541 | 542 | // SetLevel sets the global log level used by the simple logger. 543 | func SetLevel(l int) { 544 | beeLogger.SetLevel(l) 545 | } 546 | 547 | var levelMap = map[string]int{ 548 | "debug": LevelDebug, 549 | "info": LevelInfo, 550 | "warn": LevelWarn, 551 | "error": LevelError, 552 | "critical": LevelCritical, 553 | } 554 | 555 | func Level(l string) { 556 | lvl, ok := levelMap[l] 557 | if !ok { 558 | lvl = LevelInfo 559 | } 560 | beeLogger.SetLevel(lvl) 561 | } 562 | 563 | func Init(path, level string, maxDay int64) { 564 | param := fmt.Sprintf(`{"filename": "%s", "maxdays": %d}`, path, maxDay) 565 | beeLogger.SetLogger(AdapterFile, param) 566 | beeLogger.SetLogFuncCallDepth(3) 567 | beeLogger.EnableFuncCallDepth(true) 568 | Level(level) 569 | } 570 | 571 | // EnableFuncCallDepth enable log funcCallDepth 572 | func EnableFuncCallDepth(b bool) { 573 | beeLogger.enableFuncCallDepth = b 574 | } 575 | 576 | // SetLogFuncCall set the CallDepth, default is 4 577 | func SetLogFuncCall(b bool) { 578 | beeLogger.EnableFuncCallDepth(b) 579 | beeLogger.SetLogFuncCallDepth(4) 580 | } 581 | 582 | // SetLogFuncCallDepth set log funcCallDepth 583 | func SetLogFuncCallDepth(d int) { 584 | beeLogger.loggerFuncCallDepth = d 585 | } 586 | 587 | // SetLogger sets a new logger. 588 | func SetLogger(adapter string, config ...string) error { 589 | return beeLogger.SetLogger(adapter, config...) 590 | } 591 | 592 | // Emergency logs a message at emergency level. 593 | func Emergency(f interface{}, v ...interface{}) { 594 | beeLogger.Emergency(formatLog(f, v...)) 595 | } 596 | 597 | // Alert logs a message at alert level. 598 | func Alert(f interface{}, v ...interface{}) { 599 | beeLogger.Alert(formatLog(f, v...)) 600 | } 601 | 602 | // Critical logs a message at critical level. 603 | func Critical(f interface{}, v ...interface{}) { 604 | beeLogger.Critical(formatLog(f, v...)) 605 | } 606 | 607 | // Error logs a message at error level. 608 | func Error(f interface{}, v ...interface{}) { 609 | beeLogger.Error(formatLog(f, v...)) 610 | } 611 | 612 | // Warning logs a message at warning level. 613 | func Warning(f interface{}, v ...interface{}) { 614 | beeLogger.Warn(formatLog(f, v...)) 615 | } 616 | 617 | // Warn compatibility alias for Warning() 618 | func Warn(f interface{}, v ...interface{}) { 619 | beeLogger.Warn(formatLog(f, v...)) 620 | } 621 | 622 | // Notice logs a message at notice level. 623 | func Notice(f interface{}, v ...interface{}) { 624 | beeLogger.Notice(formatLog(f, v...)) 625 | } 626 | 627 | // Informational logs a message at info level. 628 | func Informational(f interface{}, v ...interface{}) { 629 | beeLogger.Info(formatLog(f, v...)) 630 | } 631 | 632 | // Info compatibility alias for Warning() 633 | func Info(f interface{}, v ...interface{}) { 634 | beeLogger.Info(formatLog(f, v...)) 635 | } 636 | 637 | // Debug logs a message at debug level. 638 | func Debug(f interface{}, v ...interface{}) { 639 | beeLogger.Debug(formatLog(f, v...)) 640 | } 641 | 642 | // Trace logs a message at trace level. 643 | // compatibility alias for Warning() 644 | func Trace(f interface{}, v ...interface{}) { 645 | beeLogger.Trace(formatLog(f, v...)) 646 | } 647 | 648 | func formatLog(f interface{}, v ...interface{}) string { 649 | var msg string 650 | switch f.(type) { 651 | case string: 652 | msg = f.(string) 653 | if len(v) == 0 { 654 | return msg 655 | } 656 | if strings.Contains(msg, "%") && !strings.Contains(msg, "%%") { 657 | //format string 658 | } else { 659 | //do not contain format char 660 | msg += strings.Repeat(" %v", len(v)) 661 | } 662 | default: 663 | msg = fmt.Sprint(f) 664 | if len(v) == 0 { 665 | return msg 666 | } 667 | msg += strings.Repeat(" %v", len(v)) 668 | } 669 | return fmt.Sprintf(msg, v...) 670 | } 671 | -------------------------------------------------------------------------------- /internal/logs/logger.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 beego Author. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package logs 16 | 17 | import ( 18 | "fmt" 19 | "io" 20 | "os" 21 | "sync" 22 | "time" 23 | ) 24 | 25 | type logWriter struct { 26 | sync.Mutex 27 | writer io.Writer 28 | } 29 | 30 | func newLogWriter(wr io.Writer) *logWriter { 31 | return &logWriter{writer: wr} 32 | } 33 | 34 | func (lg *logWriter) println(when time.Time, msg string) { 35 | lg.Lock() 36 | h, _ := formatTimeHeader(when) 37 | lg.writer.Write(append(append(h, msg...), '\n')) 38 | lg.Unlock() 39 | } 40 | 41 | type outputMode int 42 | 43 | // DiscardNonColorEscSeq supports the divided color escape sequence. 44 | // But non-color escape sequence is not output. 45 | // Please use the OutputNonColorEscSeq If you want to output a non-color 46 | // escape sequences such as ncurses. However, it does not support the divided 47 | // color escape sequence. 48 | const ( 49 | _ outputMode = iota 50 | DiscardNonColorEscSeq 51 | OutputNonColorEscSeq 52 | ) 53 | 54 | // NewAnsiColorWriter creates and initializes a new ansiColorWriter 55 | // using io.Writer w as its initial contents. 56 | // In the console of Windows, which change the foreground and background 57 | // colors of the text by the escape sequence. 58 | // In the console of other systems, which writes to w all text. 59 | func NewAnsiColorWriter(w io.Writer) io.Writer { 60 | return NewModeAnsiColorWriter(w, DiscardNonColorEscSeq) 61 | } 62 | 63 | // NewModeAnsiColorWriter create and initializes a new ansiColorWriter 64 | // by specifying the outputMode. 65 | func NewModeAnsiColorWriter(w io.Writer, mode outputMode) io.Writer { 66 | if _, ok := w.(*ansiColorWriter); !ok { 67 | return &ansiColorWriter{ 68 | w: w, 69 | mode: mode, 70 | } 71 | } 72 | return w 73 | } 74 | 75 | // Cheap integer to fixed-width decimal ASCII. Give a negative width to avoid zero-padding. 76 | func itoa(buf *[]byte, i int, wid int) { 77 | // Assemble decimal in reverse order. 78 | var b [6]byte 79 | bp := len(b) - 1 80 | for i >= 10 || wid > 1 { 81 | wid-- 82 | q := i / 10 83 | b[bp] = byte('0' + i - q*10) 84 | bp-- 85 | i = q 86 | } 87 | // i < 10 88 | b[bp] = byte('0' + i) 89 | *buf = append(*buf, b[bp:]...) 90 | } 91 | 92 | func formatTimeHeader(when time.Time) ([]byte, int) { 93 | buf := make([]byte, 0, 27) 94 | year, month, day := when.Date() 95 | itoa(&buf, year, 4) 96 | buf = append(buf, '/') 97 | itoa(&buf, int(month), 2) 98 | buf = append(buf, '/') 99 | itoa(&buf, day, 2) 100 | buf = append(buf, ' ') 101 | hour, min, sec := when.Clock() 102 | itoa(&buf, hour, 2) 103 | buf = append(buf, ':') 104 | itoa(&buf, min, 2) 105 | buf = append(buf, ':') 106 | itoa(&buf, sec, 2) 107 | buf = append(buf, '.') 108 | itoa(&buf, when.Nanosecond()/1e3, 6) 109 | buf = append(buf, ' ') 110 | return buf, day 111 | } 112 | 113 | var ( 114 | green = string([]byte{27, 91, 57, 55, 59, 52, 50, 109}) 115 | white = string([]byte{27, 91, 57, 48, 59, 52, 55, 109}) 116 | yellow = string([]byte{27, 91, 57, 55, 59, 52, 51, 109}) 117 | red = string([]byte{27, 91, 57, 55, 59, 52, 49, 109}) 118 | blue = string([]byte{27, 91, 57, 55, 59, 52, 52, 109}) 119 | magenta = string([]byte{27, 91, 57, 55, 59, 52, 53, 109}) 120 | cyan = string([]byte{27, 91, 57, 55, 59, 52, 54, 109}) 121 | 122 | w32Green = string([]byte{27, 91, 52, 50, 109}) 123 | w32White = string([]byte{27, 91, 52, 55, 109}) 124 | w32Yellow = string([]byte{27, 91, 52, 51, 109}) 125 | w32Red = string([]byte{27, 91, 52, 49, 109}) 126 | w32Blue = string([]byte{27, 91, 52, 52, 109}) 127 | w32Magenta = string([]byte{27, 91, 52, 53, 109}) 128 | w32Cyan = string([]byte{27, 91, 52, 54, 109}) 129 | 130 | reset = string([]byte{27, 91, 48, 109}) 131 | ) 132 | 133 | // ColorByStatus return color by http code 134 | // 2xx return Green 135 | // 3xx return White 136 | // 4xx return Yellow 137 | // 5xx return Red 138 | func ColorByStatus(cond bool, code int) string { 139 | switch { 140 | case code >= 200 && code < 300: 141 | return map[bool]string{true: green, false: w32Green}[cond] 142 | case code >= 300 && code < 400: 143 | return map[bool]string{true: white, false: w32White}[cond] 144 | case code >= 400 && code < 500: 145 | return map[bool]string{true: yellow, false: w32Yellow}[cond] 146 | default: 147 | return map[bool]string{true: red, false: w32Red}[cond] 148 | } 149 | } 150 | 151 | // ColorByMethod return color by http code 152 | // GET return Blue 153 | // POST return Cyan 154 | // PUT return Yellow 155 | // DELETE return Red 156 | // PATCH return Green 157 | // HEAD return Magenta 158 | // OPTIONS return WHITE 159 | func ColorByMethod(cond bool, method string) string { 160 | switch method { 161 | case "GET": 162 | return map[bool]string{true: blue, false: w32Blue}[cond] 163 | case "POST": 164 | return map[bool]string{true: cyan, false: w32Cyan}[cond] 165 | case "PUT": 166 | return map[bool]string{true: yellow, false: w32Yellow}[cond] 167 | case "DELETE": 168 | return map[bool]string{true: red, false: w32Red}[cond] 169 | case "PATCH": 170 | return map[bool]string{true: green, false: w32Green}[cond] 171 | case "HEAD": 172 | return map[bool]string{true: magenta, false: w32Magenta}[cond] 173 | case "OPTIONS": 174 | return map[bool]string{true: white, false: w32White}[cond] 175 | default: 176 | return reset 177 | } 178 | } 179 | 180 | // Guard Mutex to guarantee atomic of W32Debug(string) function 181 | var mu sync.Mutex 182 | 183 | // W32Debug Helper method to output colored logs in Windows terminals 184 | func W32Debug(msg string) { 185 | mu.Lock() 186 | defer mu.Unlock() 187 | 188 | current := time.Now() 189 | w := NewAnsiColorWriter(os.Stdout) 190 | 191 | fmt.Fprintf(w, "[beego] %v %s\n", current.Format("2006/01/02 - 15:04:05"), msg) 192 | } 193 | -------------------------------------------------------------------------------- /internal/logs/logger_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 beego Author. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package logs 16 | 17 | import ( 18 | "bytes" 19 | "testing" 20 | "time" 21 | ) 22 | 23 | func TestFormatHeader_0(t *testing.T) { 24 | tm := time.Now() 25 | if tm.Year() >= 2100 { 26 | t.FailNow() 27 | } 28 | dur := time.Second 29 | for { 30 | if tm.Year() >= 2100 { 31 | break 32 | } 33 | h, _ := formatTimeHeader(tm) 34 | if tm.Format("2006/01/02 15:04:05 ") != string(h) { 35 | t.Log(tm) 36 | t.FailNow() 37 | } 38 | tm = tm.Add(dur) 39 | dur *= 2 40 | } 41 | } 42 | 43 | func TestFormatHeader_1(t *testing.T) { 44 | tm := time.Now() 45 | year := tm.Year() 46 | dur := time.Second 47 | for { 48 | if tm.Year() >= year+1 { 49 | break 50 | } 51 | h, _ := formatTimeHeader(tm) 52 | if tm.Format("2006/01/02 15:04:05 ") != string(h) { 53 | t.Log(tm) 54 | t.FailNow() 55 | } 56 | tm = tm.Add(dur) 57 | } 58 | } 59 | 60 | func TestNewAnsiColor1(t *testing.T) { 61 | inner := bytes.NewBufferString("") 62 | w := NewAnsiColorWriter(inner) 63 | if w == inner { 64 | t.Errorf("Get %#v, want %#v", w, inner) 65 | } 66 | } 67 | 68 | func TestNewAnsiColor2(t *testing.T) { 69 | inner := bytes.NewBufferString("") 70 | w1 := NewAnsiColorWriter(inner) 71 | w2 := NewAnsiColorWriter(w1) 72 | if w1 != w2 { 73 | t.Errorf("Get %#v, want %#v", w1, w2) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /internal/logs/multifile.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 beego Author. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package logs 16 | 17 | import ( 18 | "encoding/json" 19 | "time" 20 | ) 21 | 22 | // A filesLogWriter manages several fileLogWriter 23 | // filesLogWriter will write logs to the file in json configuration and write the same level log to correspond file 24 | // means if the file name in configuration is project.log filesLogWriter will create project.error.log/project.debug.log 25 | // and write the error-level logs to project.error.log and write the debug-level logs to project.debug.log 26 | // the rotate attribute also acts like fileLogWriter 27 | type multiFileLogWriter struct { 28 | writers [LevelDebug + 1 + 1]*fileLogWriter // the last one for fullLogWriter 29 | fullLogWriter *fileLogWriter 30 | Separate []string `json:"separate"` 31 | } 32 | 33 | var levelNames = [...]string{"emergency", "alert", "critical", "error", "warning", "notice", "info", "debug"} 34 | 35 | // Init file logger with json config. 36 | // jsonConfig like: 37 | // { 38 | // "filename":"logs/beego.log", 39 | // "maxLines":0, 40 | // "maxsize":0, 41 | // "daily":true, 42 | // "maxDays":15, 43 | // "rotate":true, 44 | // "perm":0600, 45 | // "separate":["emergency", "alert", "critical", "error", "warning", "notice", "info", "debug"], 46 | // } 47 | 48 | func (f *multiFileLogWriter) Init(config string) error { 49 | writer := newFileWriter().(*fileLogWriter) 50 | err := writer.Init(config) 51 | if err != nil { 52 | return err 53 | } 54 | f.fullLogWriter = writer 55 | f.writers[LevelDebug+1] = writer 56 | 57 | //unmarshal "separate" field to f.Separate 58 | json.Unmarshal([]byte(config), f) 59 | 60 | jsonMap := map[string]interface{}{} 61 | json.Unmarshal([]byte(config), &jsonMap) 62 | 63 | for i := LevelEmergency; i < LevelDebug+1; i++ { 64 | for _, v := range f.Separate { 65 | if v == levelNames[i] { 66 | jsonMap["filename"] = f.fullLogWriter.fileNameOnly + "." + levelNames[i] + f.fullLogWriter.suffix 67 | jsonMap["level"] = i 68 | bs, _ := json.Marshal(jsonMap) 69 | writer = newFileWriter().(*fileLogWriter) 70 | writer.Init(string(bs)) 71 | f.writers[i] = writer 72 | } 73 | } 74 | } 75 | 76 | return nil 77 | } 78 | 79 | func (f *multiFileLogWriter) Destroy() { 80 | for i := 0; i < len(f.writers); i++ { 81 | if f.writers[i] != nil { 82 | f.writers[i].Destroy() 83 | } 84 | } 85 | } 86 | 87 | func (f *multiFileLogWriter) WriteMsg(when time.Time, msg string, level int) error { 88 | if f.fullLogWriter != nil { 89 | f.fullLogWriter.WriteMsg(when, msg, level) 90 | } 91 | for i := 0; i < len(f.writers)-1; i++ { 92 | if f.writers[i] != nil { 93 | if level == f.writers[i].Level { 94 | f.writers[i].WriteMsg(when, msg, level) 95 | } 96 | } 97 | } 98 | return nil 99 | } 100 | 101 | func (f *multiFileLogWriter) Flush() { 102 | for i := 0; i < len(f.writers); i++ { 103 | if f.writers[i] != nil { 104 | f.writers[i].Flush() 105 | } 106 | } 107 | } 108 | 109 | // newFilesWriter create a FileLogWriter returning as LoggerInterface. 110 | func newFilesWriter() Logger { 111 | return &multiFileLogWriter{} 112 | } 113 | 114 | func init() { 115 | Register(AdapterMultiFile, newFilesWriter) 116 | } 117 | -------------------------------------------------------------------------------- /internal/logs/multifile_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 beego Author. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package logs 16 | 17 | import ( 18 | "bufio" 19 | "os" 20 | "strconv" 21 | "strings" 22 | "testing" 23 | ) 24 | 25 | func TestFiles_1(t *testing.T) { 26 | log := NewLogger(10000) 27 | log.SetLogger("multifile", `{"filename":"test.log","separate":["emergency", "alert", "critical", "error", "warning", "notice", "info", "debug"]}`) 28 | log.Debug("debug") 29 | log.Informational("info") 30 | log.Notice("notice") 31 | log.Warning("warning") 32 | log.Error("error") 33 | log.Alert("alert") 34 | log.Critical("critical") 35 | log.Emergency("emergency") 36 | fns := []string{""} 37 | fns = append(fns, levelNames[0:]...) 38 | name := "test" 39 | suffix := ".log" 40 | for _, fn := range fns { 41 | 42 | file := name + suffix 43 | if fn != "" { 44 | file = name + "." + fn + suffix 45 | } 46 | f, err := os.Open(file) 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | b := bufio.NewReader(f) 51 | lineNum := 0 52 | lastLine := "" 53 | for { 54 | line, _, err := b.ReadLine() 55 | if err != nil { 56 | break 57 | } 58 | if len(line) > 0 { 59 | lastLine = string(line) 60 | lineNum++ 61 | } 62 | } 63 | var expected = 1 64 | if fn == "" { 65 | expected = LevelDebug + 1 66 | } 67 | if lineNum != expected { 68 | t.Fatal(file, "has", lineNum, "lines not "+strconv.Itoa(expected)+" lines") 69 | } 70 | if lineNum == 1 { 71 | if !strings.Contains(lastLine, fn) { 72 | t.Fatal(file + " " + lastLine + " not contains the log msg " + fn) 73 | } 74 | } 75 | os.Remove(file) 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /internal/logs/slack.go: -------------------------------------------------------------------------------- 1 | package logs 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | "time" 9 | ) 10 | 11 | // SLACKWriter implements beego LoggerInterface and is used to send jiaoliao webhook 12 | type SLACKWriter struct { 13 | WebhookURL string `json:"webhookurl"` 14 | Level int `json:"level"` 15 | } 16 | 17 | // newSLACKWriter create jiaoliao writer. 18 | func newSLACKWriter() Logger { 19 | return &SLACKWriter{Level: LevelTrace} 20 | } 21 | 22 | // Init SLACKWriter with json config string 23 | func (s *SLACKWriter) Init(jsonconfig string) error { 24 | return json.Unmarshal([]byte(jsonconfig), s) 25 | } 26 | 27 | // WriteMsg write message in smtp writer. 28 | // it will send an email with subject and only this message. 29 | func (s *SLACKWriter) WriteMsg(when time.Time, msg string, level int) error { 30 | if level > s.Level { 31 | return nil 32 | } 33 | 34 | text := fmt.Sprintf("{\"text\": \"%s %s\"}", when.Format("2006-01-02 15:04:05"), msg) 35 | 36 | form := url.Values{} 37 | form.Add("payload", text) 38 | 39 | resp, err := http.PostForm(s.WebhookURL, form) 40 | if err != nil { 41 | return err 42 | } 43 | defer resp.Body.Close() 44 | if resp.StatusCode != http.StatusOK { 45 | return fmt.Errorf("Post webhook failed %s %d", resp.Status, resp.StatusCode) 46 | } 47 | return nil 48 | } 49 | 50 | // Flush implementing method. empty. 51 | func (s *SLACKWriter) Flush() { 52 | } 53 | 54 | // Destroy implementing method. empty. 55 | func (s *SLACKWriter) Destroy() { 56 | } 57 | 58 | func init() { 59 | Register(AdapterSlack, newSLACKWriter) 60 | } 61 | -------------------------------------------------------------------------------- /internal/logs/smtp.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 beego Author. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package logs 16 | 17 | import ( 18 | "crypto/tls" 19 | "encoding/json" 20 | "fmt" 21 | "net" 22 | "net/smtp" 23 | "strings" 24 | "time" 25 | ) 26 | 27 | // SMTPWriter implements LoggerInterface and is used to send emails via given SMTP-server. 28 | type SMTPWriter struct { 29 | Username string `json:"username"` 30 | Password string `json:"password"` 31 | Host string `json:"host"` 32 | Subject string `json:"subject"` 33 | FromAddress string `json:"fromAddress"` 34 | RecipientAddresses []string `json:"sendTos"` 35 | Level int `json:"level"` 36 | } 37 | 38 | // NewSMTPWriter create smtp writer. 39 | func newSMTPWriter() Logger { 40 | return &SMTPWriter{Level: LevelTrace} 41 | } 42 | 43 | // Init smtp writer with json config. 44 | // config like: 45 | // { 46 | // "username":"example@gmail.com", 47 | // "password:"password", 48 | // "host":"smtp.gmail.com:465", 49 | // "subject":"email title", 50 | // "fromAddress":"from@example.com", 51 | // "sendTos":["email1","email2"], 52 | // "level":LevelError 53 | // } 54 | func (s *SMTPWriter) Init(jsonconfig string) error { 55 | return json.Unmarshal([]byte(jsonconfig), s) 56 | } 57 | 58 | func (s *SMTPWriter) getSMTPAuth(host string) smtp.Auth { 59 | if len(strings.Trim(s.Username, " ")) == 0 && len(strings.Trim(s.Password, " ")) == 0 { 60 | return nil 61 | } 62 | return smtp.PlainAuth( 63 | "", 64 | s.Username, 65 | s.Password, 66 | host, 67 | ) 68 | } 69 | 70 | func (s *SMTPWriter) sendMail(hostAddressWithPort string, auth smtp.Auth, fromAddress string, recipients []string, msgContent []byte) error { 71 | client, err := smtp.Dial(hostAddressWithPort) 72 | if err != nil { 73 | return err 74 | } 75 | 76 | host, _, _ := net.SplitHostPort(hostAddressWithPort) 77 | tlsConn := &tls.Config{ 78 | InsecureSkipVerify: true, 79 | ServerName: host, 80 | } 81 | if err = client.StartTLS(tlsConn); err != nil { 82 | return err 83 | } 84 | 85 | if auth != nil { 86 | if err = client.Auth(auth); err != nil { 87 | return err 88 | } 89 | } 90 | 91 | if err = client.Mail(fromAddress); err != nil { 92 | return err 93 | } 94 | 95 | for _, rec := range recipients { 96 | if err = client.Rcpt(rec); err != nil { 97 | return err 98 | } 99 | } 100 | 101 | w, err := client.Data() 102 | if err != nil { 103 | return err 104 | } 105 | _, err = w.Write(msgContent) 106 | if err != nil { 107 | return err 108 | } 109 | 110 | err = w.Close() 111 | if err != nil { 112 | return err 113 | } 114 | 115 | return client.Quit() 116 | } 117 | 118 | // WriteMsg write message in smtp writer. 119 | // it will send an email with subject and only this message. 120 | func (s *SMTPWriter) WriteMsg(when time.Time, msg string, level int) error { 121 | if level > s.Level { 122 | return nil 123 | } 124 | 125 | hp := strings.Split(s.Host, ":") 126 | 127 | // Set up authentication information. 128 | auth := s.getSMTPAuth(hp[0]) 129 | 130 | // Connect to the server, authenticate, set the sender and recipient, 131 | // and send the email all in one step. 132 | contentType := "Content-Type: text/plain" + "; charset=UTF-8" 133 | mailmsg := []byte("To: " + strings.Join(s.RecipientAddresses, ";") + "\r\nFrom: " + s.FromAddress + "<" + s.FromAddress + 134 | ">\r\nSubject: " + s.Subject + "\r\n" + contentType + "\r\n\r\n" + fmt.Sprintf(".%s", when.Format("2006-01-02 15:04:05")) + msg) 135 | 136 | return s.sendMail(s.Host, auth, s.FromAddress, s.RecipientAddresses, mailmsg) 137 | } 138 | 139 | // Flush implementing method. empty. 140 | func (s *SMTPWriter) Flush() { 141 | } 142 | 143 | // Destroy implementing method. empty. 144 | func (s *SMTPWriter) Destroy() { 145 | } 146 | 147 | func init() { 148 | Register(AdapterMail, newSMTPWriter) 149 | } 150 | -------------------------------------------------------------------------------- /internal/logs/smtp_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 beego Author. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package logs 16 | 17 | import ( 18 | "testing" 19 | "time" 20 | ) 21 | 22 | func TestSmtp(t *testing.T) { 23 | log := NewLogger(10000) 24 | log.SetLogger("smtp", `{"username":"beegotest@gmail.com","password":"xxxxxxxx","host":"smtp.gmail.com:587","sendTos":["xiemengjun@gmail.com"]}`) 25 | log.Critical("sendmail critical") 26 | time.Sleep(time.Second * 30) 27 | } 28 | -------------------------------------------------------------------------------- /internal/proto/cs.go: -------------------------------------------------------------------------------- 1 | package proto 2 | 3 | import ( 4 | "encoding/binary" 5 | "encoding/json" 6 | "io" 7 | "net" 8 | ) 9 | 10 | const ( 11 | CmdAuth = iota 12 | CmdHeartbeat 13 | CmdData 14 | ) 15 | 16 | type S2CHeartbeat struct { 17 | } 18 | 19 | type C2SHeartbeat struct{} 20 | 21 | type C2SAuth struct { 22 | Key string `json:"key" yaml:"key"` 23 | Domain string `json:"domain" yaml:"domain"` 24 | Forward []ForwardItem `json:"forwards" yaml:"forwards"` // request forwards, not real, it depends on opennotrd 25 | } 26 | 27 | type ForwardItem struct { 28 | // forward protocol. eg: tcp, udp, https, http 29 | Protocol string `json:"protocol" yaml:"protocol"` 30 | 31 | // forward ports 32 | // key is the port opennotrd listen 33 | // value is local port 34 | Ports map[int]string `json:"ports" yaml:"ports"` 35 | 36 | // local ip, default is 127.0.0.1 37 | // the traffic will be forward to $LocalIP:$LocalPort 38 | // for example: 127.0.0.1:8080. 192.168.31.65:8080 39 | LocalIP string `json:"localIP" yaml:"localIP"` 40 | 41 | // raw config pass to server 42 | RawConfig string `json:"rawConfig" yaml:"rawConfig"` 43 | } 44 | 45 | type S2CAuth struct { 46 | Domain string `json:"domain"` // uniq domain for opennotr 47 | Vip string `json:"vip"` // vip for opennotr 48 | ProxyInfos []*ProxyTuple `json:"proxyInfos"` // real proxy table 49 | } 50 | 51 | type ProxyTuple struct { 52 | Protocol string 53 | FromPort string 54 | ToPort string 55 | } 56 | 57 | type ProxyProtocol struct { 58 | Protocol string `json:"protocol"` 59 | SrcIP string `json:"sip"` 60 | SrcPort string `json:"sport"` 61 | DstIP string `json:"dip"` 62 | DstPort string `json:"dport"` 63 | } 64 | 65 | // 1字节版本 66 | // 1字节命令 67 | // 2字节长度 68 | type Header [4]byte 69 | 70 | func (h Header) Version() int { 71 | return int(h[0]) 72 | } 73 | 74 | func (h Header) Cmd() int { 75 | return int(h[1]) 76 | } 77 | 78 | func (h Header) Bodylen() int { 79 | return (int(h[2]) << 8) + int(h[3]) 80 | } 81 | 82 | func Read(conn net.Conn) (Header, []byte, error) { 83 | h := Header{} 84 | _, err := io.ReadFull(conn, h[:]) 85 | if err != nil { 86 | return h, nil, err 87 | } 88 | 89 | bodylen := h.Bodylen() 90 | if bodylen <= 0 { 91 | return h, nil, nil 92 | } 93 | 94 | body := make([]byte, bodylen) 95 | _, err = io.ReadFull(conn, body) 96 | if err != nil { 97 | return h, nil, err 98 | } 99 | 100 | return h, body, nil 101 | } 102 | 103 | func Write(conn net.Conn, cmd int, body []byte) error { 104 | bodylen := make([]byte, 2) 105 | binary.BigEndian.PutUint16(bodylen, uint16(len(body))) 106 | 107 | hdr := []byte{0x01, byte(cmd)} 108 | hdr = append(hdr, bodylen...) 109 | 110 | writebody := make([]byte, 0) 111 | writebody = append(writebody, hdr...) 112 | writebody = append(writebody, body...) 113 | _, err := conn.Write(writebody) 114 | return err 115 | } 116 | 117 | func WriteJSON(conn net.Conn, cmd int, obj interface{}) error { 118 | body, err := json.Marshal(obj) 119 | if err != nil { 120 | return err 121 | } 122 | return Write(conn, cmd, body) 123 | } 124 | 125 | func ReadJSON(conn net.Conn, obj interface{}) error { 126 | _, body, err := Read(conn) 127 | if err != nil { 128 | return err 129 | } 130 | 131 | return json.Unmarshal(body, obj) 132 | } 133 | -------------------------------------------------------------------------------- /opennotr/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/binary" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "log" 9 | "net" 10 | "sync" 11 | "time" 12 | 13 | "github.com/ICKelin/opennotr/internal/proto" 14 | "github.com/xtaci/smux" 15 | ) 16 | 17 | type Client struct { 18 | srv string 19 | key string 20 | domain string 21 | forwards []proto.ForwardItem 22 | udppool sync.Pool 23 | tcppool sync.Pool 24 | } 25 | 26 | func NewClient(cfg *Config) *Client { 27 | return &Client{ 28 | srv: cfg.ServerAddr, 29 | key: cfg.Key, 30 | domain: cfg.Domain, 31 | forwards: cfg.Forwards, 32 | tcppool: sync.Pool{ 33 | New: func() interface{} { 34 | return make([]byte, 4096) 35 | }, 36 | }, 37 | udppool: sync.Pool{ 38 | New: func() interface{} { 39 | return make([]byte, 64*1024) 40 | }, 41 | }, 42 | } 43 | } 44 | 45 | func (c *Client) Run() { 46 | for { 47 | conn, err := net.Dial("tcp", c.srv) 48 | if err != nil { 49 | log.Println(err) 50 | time.Sleep(time.Second * 3) 51 | continue 52 | } 53 | 54 | c2sauth := &proto.C2SAuth{ 55 | Key: c.key, 56 | Domain: c.domain, 57 | Forward: c.forwards, 58 | } 59 | 60 | err = proto.WriteJSON(conn, proto.CmdAuth, c2sauth) 61 | if err != nil { 62 | log.Println(err) 63 | time.Sleep(time.Second * 3) 64 | continue 65 | } 66 | 67 | auth := proto.S2CAuth{} 68 | err = proto.ReadJSON(conn, &auth) 69 | if err != nil { 70 | log.Println(err) 71 | time.Sleep(time.Second * 3) 72 | continue 73 | } 74 | 75 | log.Println("connect success") 76 | log.Println("vhost:", auth.Vip) 77 | log.Println("domain:", auth.Domain) 78 | for _, item := range auth.ProxyInfos { 79 | fromaddr := fmt.Sprintf("%s:%s", auth.Domain, item.FromPort) 80 | if len(item.FromPort) == 0 { 81 | fromaddr = auth.Domain 82 | } 83 | log.Printf("%s://%s => 127.0.0.1:%s\n", item.Protocol, fromaddr, item.ToPort) 84 | } 85 | 86 | mux, err := smux.Client(conn, nil) 87 | if err != nil { 88 | log.Println(err) 89 | time.Sleep(time.Second * 3) 90 | continue 91 | } 92 | 93 | for { 94 | stream, err := mux.AcceptStream() 95 | if err != nil { 96 | log.Println(err) 97 | break 98 | } 99 | 100 | go c.handleStream(stream) 101 | } 102 | 103 | log.Println("reconnecting") 104 | time.Sleep(time.Second * 3) 105 | } 106 | } 107 | 108 | func (c *Client) handleStream(stream *smux.Stream) { 109 | lenbuf := make([]byte, 2) 110 | _, err := stream.Read(lenbuf) 111 | if err != nil { 112 | log.Println(err) 113 | stream.Close() 114 | return 115 | } 116 | 117 | bodylen := binary.BigEndian.Uint16(lenbuf) 118 | buf := make([]byte, bodylen) 119 | nr, err := io.ReadFull(stream, buf) 120 | if err != nil { 121 | log.Println(err) 122 | stream.Close() 123 | return 124 | } 125 | 126 | proxyProtocol := proto.ProxyProtocol{} 127 | err = json.Unmarshal(buf[:nr], &proxyProtocol) 128 | if err != nil { 129 | log.Println("unmarshal fail: ", err) 130 | return 131 | } 132 | switch proxyProtocol.Protocol { 133 | case "tcp": 134 | c.tcpProxy(stream, &proxyProtocol) 135 | case "udp": 136 | c.udpProxy(stream, &proxyProtocol) 137 | } 138 | } 139 | 140 | func (c *Client) tcpProxy(stream *smux.Stream, p *proto.ProxyProtocol) { 141 | addr := fmt.Sprintf("%s:%s", p.DstIP, p.DstPort) 142 | remoteConn, err := net.DialTimeout("tcp", addr, time.Second*10) 143 | if err != nil { 144 | log.Println(err) 145 | stream.Close() 146 | return 147 | } 148 | 149 | go func() { 150 | defer remoteConn.Close() 151 | defer stream.Close() 152 | obj := c.tcppool.Get() 153 | buf := obj.([]byte) 154 | defer c.tcppool.Put(buf) 155 | 156 | io.CopyBuffer(remoteConn, stream, buf) 157 | }() 158 | 159 | defer remoteConn.Close() 160 | defer stream.Close() 161 | obj := c.tcppool.Get() 162 | buf := obj.([]byte) 163 | defer c.tcppool.Put(buf) 164 | io.CopyBuffer(stream, remoteConn, buf) 165 | } 166 | 167 | func (c *Client) udpProxy(stream *smux.Stream, p *proto.ProxyProtocol) { 168 | addr := fmt.Sprintf("%s:%s", p.DstIP, p.DstPort) 169 | raddr, err := net.ResolveUDPAddr("udp", addr) 170 | if err != nil { 171 | log.Println(err) 172 | stream.Close() 173 | return 174 | } 175 | 176 | remoteConn, err := net.DialUDP("udp", nil, raddr) 177 | if err != nil { 178 | log.Println(err) 179 | return 180 | } 181 | 182 | go func() { 183 | defer remoteConn.Close() 184 | defer stream.Close() 185 | hdr := make([]byte, 2) 186 | for { 187 | 188 | _, err := io.ReadFull(stream, hdr) 189 | if err != nil { 190 | log.Println("read stream fail: ", err) 191 | break 192 | } 193 | nlen := binary.BigEndian.Uint16(hdr) 194 | buf := make([]byte, nlen) 195 | nr, err := io.ReadFull(stream, buf) 196 | if err != nil { 197 | log.Println("read stream body fail: ", err) 198 | break 199 | } 200 | 201 | remoteConn.Write(buf[:nr]) 202 | } 203 | }() 204 | 205 | defer remoteConn.Close() 206 | defer stream.Close() 207 | obj := c.udppool.Get() 208 | buf := obj.([]byte) 209 | defer c.udppool.Put(buf) 210 | for { 211 | nr, err := remoteConn.Read(buf) 212 | if err != nil { 213 | log.Println(err) 214 | break 215 | } 216 | 217 | bytes := encode(buf[:nr]) 218 | _, err = stream.Write(bytes) 219 | if err != nil { 220 | log.Println(err) 221 | break 222 | } 223 | } 224 | } 225 | 226 | func encode(raw []byte) []byte { 227 | buf := make([]byte, 2) 228 | binary.BigEndian.PutUint16(buf, uint16(len(raw))) 229 | buf = append(buf, raw...) 230 | return buf 231 | } 232 | -------------------------------------------------------------------------------- /opennotr/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | 7 | "github.com/ICKelin/opennotr/internal/proto" 8 | "gopkg.in/yaml.v2" 9 | ) 10 | 11 | type Config struct { 12 | ServerAddr string `yaml:"serverAddr"` 13 | Key string `yaml:"key"` 14 | Domain string `yaml:"domain"` 15 | Forwards []proto.ForwardItem `yaml:"forwards"` 16 | } 17 | 18 | func ParseConfig(path string) (*Config, error) { 19 | cnt, err := ioutil.ReadFile(path) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | var cfg Config 25 | err = yaml.Unmarshal(cnt, &cfg) 26 | return &cfg, err 27 | } 28 | 29 | func (c *Config) String() string { 30 | cnt, _ := json.Marshal(c) 31 | return string(cnt) 32 | } 33 | -------------------------------------------------------------------------------- /opennotr/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | ) 7 | 8 | func main() { 9 | confpath := flag.String("conf", "", "config file path") 10 | flag.Parse() 11 | 12 | cfg, err := ParseConfig(*confpath) 13 | if err != nil { 14 | log.Println(err) 15 | return 16 | } 17 | 18 | cli := NewClient(cfg) 19 | cli.Run() 20 | } 21 | -------------------------------------------------------------------------------- /opennotrd/core/config.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | 7 | "gopkg.in/yaml.v2" 8 | ) 9 | 10 | type Config struct { 11 | ServerConfig ServerConfig `yaml:"server"` 12 | DHCPConfig DHCPConfig `yaml:"dhcp"` 13 | ResolverConfig ResolverConfig `yaml:"resolver"` 14 | TCPForwardConfig TCPForwardConfig `yaml:"tcpforward"` 15 | UDPForwardConfig UDPForwardConfig `yaml:"udpforward"` 16 | Plugins map[string]string `yaml:"plugin"` 17 | } 18 | 19 | type ServerConfig struct { 20 | ListenAddr string `yaml:"listen"` 21 | AuthKey string `yaml:"authKey"` 22 | Domain string `yaml:"domain"` 23 | } 24 | 25 | type TCPForwardConfig struct { 26 | ListenAddr string `yaml:"listen"` 27 | ReadTimeout int `yaml:"readTimeout"` 28 | WriteTimeout int `yaml:"writeTimeout"` 29 | } 30 | 31 | type UDPForwardConfig struct { 32 | ListenAddr string `yaml:"listen"` 33 | ReadTimeout int `yaml:"readTimeout"` 34 | WriteTimeout int `yaml:"writeTimeout"` 35 | SessionTimeout int `yaml:"sessionTimeout"` 36 | } 37 | 38 | type DHCPConfig struct { 39 | Cidr string `yaml:"cidr"` 40 | IP string `yaml:"ip"` 41 | } 42 | 43 | type ResolverConfig struct { 44 | EtcdEndpoints []string `yaml:"etcdEndpoints"` 45 | } 46 | 47 | func ParseConfig(path string) (*Config, error) { 48 | cnt, err := ioutil.ReadFile(path) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | var cfg Config 54 | err = yaml.Unmarshal(cnt, &cfg) 55 | return &cfg, err 56 | } 57 | 58 | func (c *Config) String() string { 59 | cnt, _ := json.Marshal(c) 60 | return string(cnt) 61 | } 62 | -------------------------------------------------------------------------------- /opennotrd/core/dhcp.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "sync" 7 | ) 8 | 9 | type DHCP struct { 10 | rw sync.Mutex 11 | cidr string 12 | localIP string 13 | free map[string]struct{} 14 | inuse map[string]struct{} 15 | } 16 | 17 | func NewDHCP(cidr string) (*DHCP, error) { 18 | begin, end, err := getIPRange(cidr) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | free := make(map[string]struct{}) 24 | inused := make(map[string]struct{}) 25 | for i := begin + 1; i < end; i++ { 26 | free[toIP(i)] = struct{}{} 27 | } 28 | 29 | return &DHCP{ 30 | free: free, 31 | inuse: inused, 32 | cidr: cidr, 33 | localIP: toIP(begin), 34 | }, nil 35 | } 36 | 37 | func (g *DHCP) GetCIDR() string { 38 | return g.cidr 39 | } 40 | 41 | func (g *DHCP) SelectIP() (string, error) { 42 | g.rw.Lock() 43 | defer g.rw.Unlock() 44 | 45 | for ip := range g.free { 46 | delete(g.free, ip) 47 | g.inuse[ip] = struct{}{} 48 | return ip, nil 49 | } 50 | 51 | return "", fmt.Errorf("no available ip") 52 | } 53 | 54 | func (g *DHCP) ReleaseIP(ip string) { 55 | g.rw.Lock() 56 | defer g.rw.Unlock() 57 | 58 | g.free[ip] = struct{}{} 59 | delete(g.inuse, ip) 60 | } 61 | 62 | func getIPRange(cidr string) (int32, int32, error) { 63 | ip, mask, err := net.ParseCIDR(cidr) 64 | if err != nil { 65 | return -1, -1, err 66 | } 67 | 68 | ipv4 := ip.To4() 69 | if ipv4 == nil { 70 | return -1, -1, fmt.Errorf("parse cidr fail") 71 | } 72 | 73 | one, _ := mask.Mask.Size() 74 | begin := (int32(ipv4[0]) << 24) + (int32(ipv4[1]) << 16) + (int32(ipv4[2]) << 8) + int32(ipv4[3]) 75 | end := begin | (1<<(32-one) - 1) 76 | 77 | return begin, end, nil 78 | } 79 | 80 | // int32 ip地址转换为string 81 | func toIP(iip int32) string { 82 | return fmt.Sprintf("%d.%d.%d.%d", byte(iip>>24), byte(iip>>16), byte(iip>>8), byte(iip)) 83 | } 84 | -------------------------------------------------------------------------------- /opennotrd/core/net.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "encoding/binary" 5 | "encoding/json" 6 | "fmt" 7 | "net" 8 | "syscall" 9 | 10 | "github.com/ICKelin/opennotr/internal/proto" 11 | ) 12 | 13 | func checksumAdd(buf []byte, seed uint32) uint32 { 14 | sum := seed 15 | for i, l := 0, len(buf); i < l; i += 2 { 16 | j := i + 1 17 | if j == l { 18 | sum += uint32(buf[i]) << 8 19 | break 20 | } 21 | sum += uint32(buf[i])<<8 | uint32(buf[j]) 22 | } 23 | return sum 24 | } 25 | 26 | func checksumWrapper(seed uint32) uint16 { 27 | sum := seed 28 | for sum > 0xffff { 29 | sum = (sum >> 16) + (sum & 0xffff) 30 | } 31 | csum := ^uint16(sum) 32 | 33 | // RFC 768 34 | if csum == 0 { 35 | csum = 0xffff 36 | } 37 | return csum 38 | } 39 | 40 | func CheckSum(buf []byte) uint16 { 41 | return checksumWrapper(checksumAdd(buf, 0)) 42 | } 43 | 44 | func sendUDPViaRaw(fd int, src, dst *net.UDPAddr, payload []byte) error { 45 | iplen, ulen := uint16(28+len(payload)), uint16(8+len(payload)) 46 | if iplen > 65535 { 47 | return fmt.Errorf("too big packet") 48 | } 49 | 50 | // UDP checksum: sip + dip + udp-head + payload + PROTO + ulen 51 | data := make([]byte, iplen) 52 | data[9] = syscall.IPPROTO_UDP 53 | copy(data[12:16], src.IP.To4()) 54 | copy(data[16:20], dst.IP.To4()) 55 | data[20] = byte(src.Port >> 8) 56 | data[21] = byte(src.Port) 57 | data[22] = byte(dst.Port >> 8) 58 | data[23] = byte(dst.Port) 59 | data[24] = byte(ulen >> 8) 60 | data[25] = byte(ulen) 61 | copy(data[28:], payload) 62 | 63 | uc := checksumWrapper(checksumAdd(data, uint32(ulen))) 64 | data[26] = byte(uc >> 8) 65 | data[27] = byte(uc) 66 | 67 | data[0] = 0x45 68 | data[2] = byte(iplen >> 8) 69 | data[3] = byte(iplen) 70 | data[6] = 0x40 71 | data[8] = 64 72 | ipc := CheckSum(data[:20]) 73 | data[10] = byte(ipc >> 8) 74 | data[11] = byte(ipc) 75 | 76 | addr := syscall.SockaddrInet4{Port: dst.Port} 77 | copy(addr.Addr[:], data[16:20]) 78 | return syscall.Sendto(fd, data, 0, &addr) 79 | } 80 | 81 | func encode(raw []byte) []byte { 82 | buf := make([]byte, 2) 83 | binary.BigEndian.PutUint16(buf, uint16(len(raw))) 84 | buf = append(buf, raw...) 85 | return buf 86 | } 87 | 88 | func encodeProxyProtocol(protocol, sip, sport, dip, dport string) []byte { 89 | proxyProtocol := &proto.ProxyProtocol{ 90 | Protocol: protocol, 91 | SrcIP: sip, 92 | SrcPort: sport, 93 | DstIP: dip, 94 | DstPort: dport, 95 | } 96 | 97 | body, _ := json.Marshal(proxyProtocol) 98 | return encode(body) 99 | } 100 | -------------------------------------------------------------------------------- /opennotrd/core/resolver.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "strings" 8 | "time" 9 | 10 | "github.com/coreos/etcd/clientv3" 11 | ) 12 | 13 | type record struct { 14 | Host string `json:"host"` 15 | } 16 | 17 | type Resolver struct { 18 | endpoints []string 19 | cli *clientv3.Client 20 | } 21 | 22 | func NewResolve(endpoints []string) (*Resolver, error) { 23 | cli, err := clientv3.New(clientv3.Config{ 24 | Endpoints: endpoints, 25 | DialTimeout: time.Minute * 1, 26 | }) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | return &Resolver{ 32 | endpoints: endpoints, 33 | cli: cli, 34 | }, nil 35 | } 36 | 37 | func (r *Resolver) ApplyDomain(domain, ip string) error { 38 | sp := strings.Split(domain, ".") 39 | if len(sp) == 0 { 40 | return fmt.Errorf("invalid domain: %s", domain) 41 | } 42 | 43 | key := "/skydns" 44 | for i := len(sp) - 1; i >= 0; i-- { 45 | key = fmt.Sprintf("%s/%s", key, sp[i]) 46 | } 47 | 48 | value := &record{Host: ip} 49 | b, err := json.Marshal(value) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | _, err = r.cli.Put(context.Background(), key, string(b)) 55 | return err 56 | } 57 | -------------------------------------------------------------------------------- /opennotrd/core/server.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net" 7 | "net/http" 8 | "strings" 9 | "time" 10 | 11 | "github.com/ICKelin/opennotr/internal/logs" 12 | "github.com/ICKelin/opennotr/internal/proto" 13 | "github.com/ICKelin/opennotr/opennotrd/plugin" 14 | "github.com/xtaci/smux" 15 | ) 16 | 17 | type Server struct { 18 | cfg ServerConfig 19 | addr string 20 | authKey string 21 | domain string 22 | publicIP string 23 | 24 | // dhcp manager select/release ip for client 25 | dhcp *DHCP 26 | 27 | // call stream proxy for dynamic add/del tcp/udp proxy 28 | pluginMgr *plugin.PluginManager 29 | 30 | // resolver writes domains to etcd and it will be used by coredns 31 | resolver *Resolver 32 | 33 | // sess manager is the model of client session 34 | sessMgr *SessionManager 35 | } 36 | 37 | func NewServer(cfg ServerConfig, 38 | dhcp *DHCP, 39 | resolver *Resolver) *Server { 40 | return &Server{ 41 | cfg: cfg, 42 | addr: cfg.ListenAddr, 43 | authKey: cfg.AuthKey, 44 | domain: cfg.Domain, 45 | publicIP: publicIP(), 46 | dhcp: dhcp, 47 | pluginMgr: plugin.DefaultPluginManager(), 48 | resolver: resolver, 49 | sessMgr: GetSessionManager(), 50 | } 51 | } 52 | 53 | func (s *Server) ListenAndServe() error { 54 | listener, err := net.Listen("tcp", s.addr) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | for { 60 | conn, err := listener.Accept() 61 | if err != nil { 62 | return err 63 | } 64 | 65 | go s.onConn(conn) 66 | } 67 | } 68 | 69 | func (s *Server) onConn(conn net.Conn) { 70 | defer conn.Close() 71 | 72 | // auth key verify 73 | // currently we use auth key which configured in notrd.yaml 74 | auth := proto.C2SAuth{} 75 | err := proto.ReadJSON(conn, &auth) 76 | if err != nil { 77 | logs.Error("bad request, authorize fail: %v %v", err, auth) 78 | return 79 | } 80 | 81 | if auth.Key != s.authKey { 82 | logs.Error("verify key fail") 83 | return 84 | } 85 | 86 | // if client without domain 87 | // generate random domain base on time nano 88 | if len(auth.Domain) <= 0 { 89 | auth.Domain = fmt.Sprintf("%s.%s", randomDomain(time.Now().UnixNano()), s.domain) 90 | } 91 | 92 | // select a virtual ip for client. 93 | // a virtual ip is the ip address which can be use in our system 94 | // but cannot be used by other networks 95 | vip, err := s.dhcp.SelectIP() 96 | if err != nil { 97 | logs.Error("dhcp select ip fail: %v", err) 98 | return 99 | } 100 | 101 | // dynamic dns, write domain=>ip map to etcd 102 | // coredns will read records from etcd and reply to dns client 103 | if s.resolver != nil { 104 | err = s.resolver.ApplyDomain(auth.Domain, publicIP()) 105 | if err != nil { 106 | logs.Error("resolve domain fail: %v", err) 107 | return 108 | } 109 | } 110 | 111 | logs.Info("select vip: %s", vip) 112 | logs.Info("select domain: %s", auth.Domain) 113 | 114 | // create forward 115 | // 0.0.0.0:$publicPort => $vip:$localPort 116 | // 1. for from address, we listen 0.0.0.0:$publicPort 117 | // 2. for to address, we use $vip:$localPort 118 | // the vip is the virtual lan ip address 119 | // Domain is only use for restyproxy 120 | proxyInfos := make([]*proto.ProxyTuple, 0) 121 | for _, forward := range auth.Forward { 122 | for publicPort, localPort := range forward.Ports { 123 | item := &plugin.PluginMeta{ 124 | Protocol: forward.Protocol, 125 | From: fmt.Sprintf("0.0.0.0:%d", publicPort), 126 | To: fmt.Sprintf("%s:%s", vip, localPort), 127 | Domain: auth.Domain, 128 | RecycleSignal: make(chan struct{}), 129 | Ctx: forward.RawConfig, 130 | } 131 | 132 | p, err := s.pluginMgr.AddProxy(item) 133 | if err != nil { 134 | logs.Error("add proxy fail: %v", err) 135 | return 136 | } 137 | proxyInfos = append(proxyInfos, &proto.ProxyTuple{ 138 | Protocol: forward.Protocol, 139 | FromPort: p.FromPort, 140 | ToPort: p.ToPort, 141 | }) 142 | defer s.pluginMgr.DelProxy(item) 143 | } 144 | } 145 | 146 | reply := &proto.S2CAuth{ 147 | Vip: vip, 148 | Domain: auth.Domain, 149 | ProxyInfos: proxyInfos, 150 | } 151 | 152 | err = proto.WriteJSON(conn, proto.CmdAuth, reply) 153 | if err != nil { 154 | logs.Error("write json fail: %v", err) 155 | return 156 | } 157 | 158 | mux, err := smux.Server(conn, nil) 159 | if err != nil { 160 | logs.Error("smux server fail:%v", err) 161 | return 162 | } 163 | 164 | sess := newSession(mux, vip) 165 | s.sessMgr.AddSession(vip, sess) 166 | defer s.sessMgr.DeleteSession(vip) 167 | 168 | rttInterval := time.NewTicker(time.Millisecond * 500) 169 | for range rttInterval.C { 170 | if mux.IsClosed() { 171 | logs.Info("session %v close", sess.conn.RemoteAddr().String()) 172 | return 173 | } 174 | 175 | } 176 | } 177 | 178 | // randomDomain generate random domain for client 179 | func randomDomain(num int64) string { 180 | const ALPHABET = "123456789abcdefghijklmnopqrstuvwxyz" 181 | const BASE = int64(len(ALPHABET)) 182 | rs := "" 183 | for num > 0 { 184 | rs += string(ALPHABET[num%BASE]) 185 | num = num / BASE 186 | } 187 | 188 | return rs 189 | } 190 | 191 | // get public 192 | func publicIP() string { 193 | resp, err := http.Get("http://ipv4.icanhazip.com") 194 | if err != nil { 195 | logs.Error("get public ip fail: %v", err) 196 | panic(err) 197 | } 198 | 199 | defer resp.Body.Close() 200 | content, err := ioutil.ReadAll(resp.Body) 201 | if err != nil { 202 | panic(err) 203 | } 204 | 205 | str := string(content) 206 | idx := strings.LastIndex(str, "\n") 207 | return str[:idx] 208 | } 209 | -------------------------------------------------------------------------------- /opennotrd/core/session.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/xtaci/smux" 7 | ) 8 | 9 | var sessionMgr = &SessionManager{} 10 | 11 | // SessionManager defines the session info add/delete/get actions 12 | type SessionManager struct { 13 | sessions sync.Map 14 | } 15 | 16 | // GetSessionManager returs the singleton of session manager 17 | func GetSessionManager() *SessionManager { 18 | return sessionMgr 19 | } 20 | 21 | // Session defines each opennotr_client to opennotr_server connection 22 | type Session struct { 23 | conn *smux.Session 24 | rxbytes uint64 25 | txbytes uint64 26 | } 27 | 28 | func newSession(conn *smux.Session, vip string) *Session { 29 | return &Session{ 30 | conn: conn, 31 | } 32 | } 33 | 34 | func (mgr *SessionManager) AddSession(vip string, sess *Session) { 35 | mgr.sessions.Store(vip, sess) 36 | } 37 | 38 | func (mgr *SessionManager) GetSession(vip string) *Session { 39 | val, ok := mgr.sessions.Load(vip) 40 | if !ok { 41 | return nil 42 | } 43 | return val.(*Session) 44 | } 45 | 46 | func (mgr *SessionManager) DeleteSession(vip string) { 47 | mgr.sessions.Delete(vip) 48 | } 49 | -------------------------------------------------------------------------------- /opennotrd/core/tcpforward.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "io" 5 | "net" 6 | "sync" 7 | "syscall" 8 | "time" 9 | 10 | "github.com/ICKelin/opennotr/internal/logs" 11 | ) 12 | 13 | var ( 14 | // default tcp timeout(read, write), 10 seconds 15 | defaultTCPTimeout = 10 16 | ) 17 | 18 | type TCPForward struct { 19 | listenAddr string 20 | // writeTimeout defines the tcp connection write timeout in second 21 | // default value set to 10 seconds 22 | writeTimeout time.Duration 23 | 24 | // readTimeout defines the tcp connection write timeout in second 25 | // default value set to 10 seconds 26 | readTimeout time.Duration 27 | 28 | // the session manager is the global session manager 29 | // it stores opennotr_client to opennotr_server connection 30 | sessMgr *SessionManager 31 | } 32 | 33 | func NewTCPForward(cfg TCPForwardConfig) *TCPForward { 34 | tcpReadTimeout := cfg.ReadTimeout 35 | if tcpReadTimeout <= 0 { 36 | tcpReadTimeout = defaultTCPTimeout 37 | } 38 | 39 | tcpWriteTimeout := cfg.WriteTimeout 40 | if tcpWriteTimeout <= 0 { 41 | tcpWriteTimeout = int(defaultTCPTimeout) 42 | } 43 | return &TCPForward{ 44 | listenAddr: cfg.ListenAddr, 45 | writeTimeout: time.Duration(tcpWriteTimeout) * time.Second, 46 | readTimeout: time.Duration(tcpReadTimeout) * time.Second, 47 | sessMgr: GetSessionManager(), 48 | } 49 | } 50 | 51 | func (f *TCPForward) Listen() (net.Listener, error) { 52 | listener, err := net.Listen("tcp", f.listenAddr) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | // set socket with ip transparent option 58 | file, err := listener.(*net.TCPListener).File() 59 | if err != nil { 60 | return nil, err 61 | } 62 | defer file.Close() 63 | 64 | err = syscall.SetsockoptInt(int(file.Fd()), syscall.SOL_IP, syscall.IP_TRANSPARENT, 1) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | return listener, nil 70 | } 71 | 72 | func (f *TCPForward) Serve(listener net.Listener) error { 73 | for { 74 | conn, err := listener.Accept() 75 | if err != nil { 76 | logs.Error("accept fail: %v", err) 77 | break 78 | } 79 | 80 | go f.forwardTCP(conn) 81 | } 82 | 83 | return nil 84 | } 85 | 86 | func (f *TCPForward) forwardTCP(conn net.Conn) { 87 | defer conn.Close() 88 | 89 | dip, dport, _ := net.SplitHostPort(conn.LocalAddr().String()) 90 | sip, sport, _ := net.SplitHostPort(conn.RemoteAddr().String()) 91 | 92 | sess := f.sessMgr.GetSession(dip) 93 | if sess == nil { 94 | logs.Error("no route to host: %s", dip) 95 | return 96 | } 97 | 98 | stream, err := sess.conn.OpenStream() 99 | if err != nil { 100 | logs.Error("open stream fail: %v", err) 101 | return 102 | } 103 | defer stream.Close() 104 | 105 | // todo rewrite to client configuration 106 | targetIP := "127.0.0.1" 107 | bytes := encodeProxyProtocol("tcp", sip, sport, targetIP, dport) 108 | stream.SetWriteDeadline(time.Now().Add(f.writeTimeout)) 109 | _, err = stream.Write(bytes) 110 | stream.SetWriteDeadline(time.Time{}) 111 | if err != nil { 112 | logs.Error("stream write fail: %v", err) 113 | return 114 | } 115 | 116 | wg := &sync.WaitGroup{} 117 | wg.Add(1) 118 | defer wg.Wait() 119 | 120 | go func() { 121 | defer wg.Done() 122 | defer stream.Close() 123 | defer conn.Close() 124 | buf := make([]byte, 4096) 125 | io.CopyBuffer(stream, conn, buf) 126 | }() 127 | 128 | // todo: optimize mem alloc 129 | // one session will cause 4KB + 4KB buffer for io copy 130 | // and two goroutine 4KB mem used 131 | buf := make([]byte, 4096) 132 | io.CopyBuffer(conn, stream, buf) 133 | } 134 | -------------------------------------------------------------------------------- /opennotrd/core/tcpforward_test.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net" 7 | "net/http" 8 | _ "net/http/pprof" 9 | "os" 10 | "sync" 11 | "testing" 12 | "time" 13 | 14 | "github.com/xtaci/smux" 15 | ) 16 | 17 | func init() { 18 | go http.ListenAndServe("127.0.0.1:6060", nil) 19 | } 20 | 21 | // client -----> tproxy | opennotr server <------ opennotr client 22 | 23 | var backendAddr = "127.0.0.1:8522" 24 | var serverAddr = "127.0.0.1:8521" 25 | var tproxyAddr = "127.0.0.1:8520" 26 | var vip = "100.64.240.10" 27 | 28 | type mockConn struct { 29 | net.Conn 30 | addr mockAddr 31 | } 32 | 33 | type mockAddr struct{} 34 | 35 | func (addr mockAddr) Network() string { 36 | return "tcp" 37 | } 38 | 39 | func (addr mockAddr) String() string { 40 | return "100.64.240.10:8522" 41 | } 42 | 43 | func (c *mockConn) LocalAddr() net.Addr { 44 | return c.addr 45 | } 46 | 47 | func runBackend() { 48 | conn, err := net.Dial("tcp", serverAddr) 49 | if err != nil { 50 | panic(err) 51 | } 52 | defer conn.Close() 53 | sess, err := smux.Client(conn, nil) 54 | if err != nil { 55 | panic(err) 56 | } 57 | defer sess.Close() 58 | 59 | for { 60 | stream, err := sess.AcceptStream() 61 | if err != nil { 62 | panic(err) 63 | } 64 | 65 | go func() { 66 | defer stream.Close() 67 | buf := make([]byte, len("ping\n")) 68 | for { 69 | nr, err := stream.Read(buf) 70 | if err != nil { 71 | break 72 | } 73 | stream.Write(buf[:nr]) 74 | } 75 | }() 76 | } 77 | } 78 | 79 | func runserver(listener net.Listener) { 80 | for { 81 | conn, err := listener.Accept() 82 | if err != nil { 83 | break 84 | } 85 | 86 | go func() { 87 | sess, err := smux.Server(conn, nil) 88 | if err != nil { 89 | panic(err) 90 | } 91 | 92 | sessMgr := GetSessionManager() 93 | sessMgr.AddSession(vip, &Session{conn: sess}) 94 | fmt.Println("add session: ", vip) 95 | }() 96 | } 97 | } 98 | 99 | func runtproxy(tcpfw *TCPForward, listener net.Listener) { 100 | for { 101 | conn, err := listener.Accept() 102 | if err != nil { 103 | break 104 | } 105 | 106 | go func() { 107 | // forward test 108 | mConn := &mockConn{} 109 | mConn.Conn = conn 110 | tcpfw.forwardTCP(mConn) 111 | }() 112 | } 113 | } 114 | 115 | func TestTCPForward(t *testing.T) { 116 | // listen tproxy 117 | tcpfw := NewTCPForward(TCPForwardConfig{ 118 | ListenAddr: tproxyAddr, 119 | }) 120 | listener, err := tcpfw.Listen() 121 | if err != nil { 122 | t.Error(err) 123 | return 124 | } 125 | // defer listener.Close() 126 | 127 | srvlistener, err := net.Listen("tcp", serverAddr) 128 | if err != nil { 129 | t.Error(err) 130 | return 131 | } 132 | defer srvlistener.Close() 133 | 134 | go runBackend() 135 | go runserver(srvlistener) 136 | go runtproxy(tcpfw, listener) 137 | // wait for session created 138 | time.Sleep(time.Second * 1) 139 | conn, err := net.Dial("tcp", tproxyAddr) 140 | if err != nil { 141 | t.FailNow() 142 | } 143 | defer conn.Close() 144 | 145 | go func() { 146 | defer conn.Close() 147 | for i := 0; i < 10; i++ { 148 | conn.Write([]byte("ping\n")) 149 | time.Sleep(time.Second * 1) 150 | } 151 | fmt.Println("connection close") 152 | }() 153 | 154 | buf := make([]byte, 128) 155 | c := 0 156 | for { 157 | nr, err := conn.Read(buf) 158 | if err != nil { 159 | break 160 | } 161 | fmt.Printf("receive %d %s\n", c+1, string(buf[:nr])) 162 | c += 1 163 | } 164 | } 165 | 166 | func benchmark(t *testing.B, nconn int) { 167 | // listen tproxy 168 | tcpfw := NewTCPForward(TCPForwardConfig{ 169 | ListenAddr: tproxyAddr, 170 | }) 171 | listener, err := tcpfw.Listen() 172 | if err != nil { 173 | t.Error(err) 174 | return 175 | } 176 | // defer listener.Close() 177 | 178 | srvlistener, err := net.Listen("tcp", serverAddr) 179 | if err != nil { 180 | t.Error(err) 181 | return 182 | } 183 | defer srvlistener.Close() 184 | 185 | go runBackend() 186 | go runserver(srvlistener) 187 | go runtproxy(tcpfw, listener) 188 | 189 | // wait for session created 190 | time.Sleep(time.Second * 1) 191 | wg := sync.WaitGroup{} 192 | wg.Add(nconn) 193 | defer wg.Wait() 194 | for i := 0; i < nconn; i++ { 195 | go func() { 196 | defer wg.Done() 197 | conn, err := net.Dial("tcp", tproxyAddr) 198 | if err != nil { 199 | t.FailNow() 200 | } 201 | defer conn.Close() 202 | 203 | go func() { 204 | defer conn.Close() 205 | for i := 0; i < 10; i++ { 206 | conn.Write([]byte("ping\n")) 207 | time.Sleep(time.Second * 1) 208 | } 209 | }() 210 | fp, _ := os.Open(os.DevNull) 211 | defer fp.Close() 212 | io.Copy(fp, conn) 213 | }() 214 | } 215 | } 216 | 217 | func Benchmark1K(b *testing.B) { 218 | benchmark(b, 1024) 219 | } 220 | 221 | func Benchmark2K(b *testing.B) { 222 | benchmark(b, 1024*2) 223 | } 224 | 225 | func Benchmark4K(b *testing.B) { 226 | benchmark(b, 1024*4) 227 | } 228 | 229 | func Benchmark8K(b *testing.B) { 230 | benchmark(b, 1024*8) 231 | } 232 | 233 | func Benchmark10K(b *testing.B) { 234 | benchmark(b, 1024*10) 235 | } 236 | 237 | func Benchmark14K(b *testing.B) { 238 | benchmark(b, 1024*14) 239 | } 240 | -------------------------------------------------------------------------------- /opennotrd/core/udpforward.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | "io" 8 | "net" 9 | "sync" 10 | "syscall" 11 | "time" 12 | "unsafe" 13 | 14 | "github.com/ICKelin/opennotr/internal/logs" 15 | "github.com/xtaci/smux" 16 | ) 17 | 18 | var ( 19 | // default udp timeout(read, write)(seconds) 20 | defaultUDPTimeout = 10 21 | 22 | // default udp session timeout(seconds) 23 | defaultUDPSessionTimeout = 30 24 | ) 25 | 26 | type udpSession struct { 27 | stream *smux.Stream 28 | lastActive time.Time 29 | } 30 | 31 | type UDPForward struct { 32 | listenAddr string 33 | sessionTimeout int 34 | readTimeout time.Duration 35 | writeTimeout time.Duration 36 | rawfd int 37 | 38 | // the session manager is the global session manager 39 | // it stores opennotr_client to opennotr_server connection 40 | sessMgr *SessionManager 41 | 42 | // udpSessions stores each client forward stream 43 | // the purpose of udpSession is to reuse stream 44 | udpSessions map[string]*udpSession 45 | udpsessLock sync.Mutex 46 | } 47 | 48 | func NewUDPForward(cfg UDPForwardConfig) *UDPForward { 49 | readTimeout := cfg.ReadTimeout 50 | if readTimeout <= 0 { 51 | readTimeout = defaultUDPTimeout 52 | } 53 | 54 | writeTimeout := cfg.WriteTimeout 55 | if writeTimeout <= 0 { 56 | writeTimeout = defaultUDPTimeout 57 | } 58 | 59 | sessionTimeout := cfg.SessionTimeout 60 | if sessionTimeout <= 0 { 61 | sessionTimeout = defaultUDPSessionTimeout 62 | } 63 | 64 | return &UDPForward{ 65 | listenAddr: cfg.ListenAddr, 66 | readTimeout: time.Duration(readTimeout) * time.Second, 67 | writeTimeout: time.Duration(writeTimeout) * time.Second, 68 | sessionTimeout: sessionTimeout, 69 | sessMgr: GetSessionManager(), 70 | udpSessions: make(map[string]*udpSession), 71 | } 72 | } 73 | 74 | // Listen listens a udp port, since that we use tproxy to 75 | // redirect traffic to this listened udp port 76 | // so the socket should set to ip transparent option 77 | func (f *UDPForward) Listen() (*net.UDPConn, error) { 78 | laddr, err := net.ResolveUDPAddr("udp", f.listenAddr) 79 | if err != nil { 80 | logs.Error("resolve udp fail: %v", err) 81 | return nil, err 82 | } 83 | 84 | lconn, err := net.ListenUDP("udp", laddr) 85 | if err != nil { 86 | return nil, err 87 | } 88 | 89 | // set socket with ip transparent option 90 | file, err := lconn.File() 91 | if err != nil { 92 | lconn.Close() 93 | return nil, err 94 | } 95 | defer file.Close() 96 | 97 | err = syscall.SetsockoptInt(int(file.Fd()), syscall.SOL_IP, syscall.IP_TRANSPARENT, 1) 98 | if err != nil { 99 | lconn.Close() 100 | return nil, err 101 | } 102 | 103 | // set socket with recv origin dst option 104 | err = syscall.SetsockoptInt(int(file.Fd()), syscall.SOL_IP, syscall.IP_RECVORIGDSTADDR, 1) 105 | if err != nil { 106 | return nil, err 107 | } 108 | 109 | // create raw socket fd 110 | // we use rawsocket to send udp packet back to client. 111 | rawfd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_RAW, syscall.IPPROTO_RAW) 112 | if err != nil || rawfd < 0 { 113 | logs.Error("call socket fail: %v", err) 114 | return nil, err 115 | } 116 | 117 | err = syscall.SetsockoptInt(rawfd, syscall.IPPROTO_IP, syscall.IP_HDRINCL, 1) 118 | if err != nil { 119 | return nil, err 120 | } 121 | 122 | f.rawfd = rawfd 123 | return lconn, nil 124 | } 125 | 126 | func (f *UDPForward) Serve(lconn *net.UDPConn) error { 127 | go f.recyeleSession() 128 | buf := make([]byte, 64*1024) 129 | oob := make([]byte, 1024) 130 | for { 131 | // udp is not connect oriented, it should use read message 132 | // and read the origin dst ip and port from msghdr 133 | nr, oobn, _, raddr, err := lconn.ReadMsgUDP(buf, oob) 134 | if err != nil { 135 | logs.Error("read from udp fail: %v", err) 136 | break 137 | } 138 | 139 | origindst, err := f.getOriginDst(oob[:oobn]) 140 | if err != nil { 141 | logs.Error("get origin dst fail: %v", err) 142 | continue 143 | } 144 | 145 | dip, dport, _ := net.SplitHostPort(origindst.String()) 146 | sip, sport, _ := net.SplitHostPort(raddr.String()) 147 | 148 | key := fmt.Sprintf("%s:%s:%s:%s", sip, sport, dip, dport) 149 | 150 | f.udpsessLock.Lock() 151 | udpsess := f.udpSessions[key] 152 | if udpsess != nil { 153 | udpsess.lastActive = time.Now() 154 | f.udpsessLock.Unlock() 155 | } else { 156 | f.udpsessLock.Unlock() 157 | sess := f.sessMgr.GetSession(dip) 158 | if sess == nil { 159 | logs.Error("no route to host: %s", dip) 160 | continue 161 | } 162 | 163 | stream, err := sess.conn.OpenStream() 164 | if err != nil { 165 | logs.Error("open stream fail: %v", err) 166 | continue 167 | } 168 | 169 | udpsess = &udpSession{stream, time.Now()} 170 | f.udpsessLock.Lock() 171 | f.udpSessions[key] = udpsess 172 | f.udpsessLock.Unlock() 173 | 174 | targetIP := "127.0.0.1" 175 | bytes := encodeProxyProtocol("udp", sip, sport, targetIP, dport) 176 | stream.SetWriteDeadline(time.Now().Add(f.writeTimeout)) 177 | _, err = stream.Write(bytes) 178 | stream.SetWriteDeadline(time.Time{}) 179 | if err != nil { 180 | logs.Error("stream write fail: %v", err) 181 | continue 182 | } 183 | 184 | go f.forwardUDP(stream, key, origindst, raddr) 185 | } 186 | 187 | stream := udpsess.stream 188 | 189 | bytes := encode(buf[:nr]) 190 | stream.SetWriteDeadline(time.Now().Add(f.writeTimeout)) 191 | _, err = stream.Write(bytes) 192 | stream.SetWriteDeadline(time.Time{}) 193 | if err != nil { 194 | logs.Error("stream write fail: %v", err) 195 | } 196 | } 197 | return nil 198 | } 199 | 200 | // forwardUDP reads from stream and write to tofd via rawsocket 201 | func (f *UDPForward) forwardUDP(stream *smux.Stream, sessionKey string, fromaddr, toaddr *net.UDPAddr) { 202 | defer stream.Close() 203 | defer func() { 204 | f.udpsessLock.Lock() 205 | delete(f.udpSessions, sessionKey) 206 | f.udpsessLock.Unlock() 207 | }() 208 | 209 | hdr := make([]byte, 2) 210 | for { 211 | nr, err := stream.Read(hdr) 212 | if err != nil { 213 | if err != io.EOF { 214 | logs.Error("read stream fail %v", err) 215 | } 216 | break 217 | } 218 | if nr != 2 { 219 | logs.Error("invalid bodylen: %d", nr) 220 | continue 221 | } 222 | 223 | nlen := binary.BigEndian.Uint16(hdr) 224 | buf := make([]byte, nlen) 225 | stream.SetReadDeadline(time.Now().Add(f.readTimeout)) 226 | _, err = io.ReadFull(stream, buf) 227 | stream.SetReadDeadline(time.Time{}) 228 | if err != nil { 229 | logs.Error("read stream body fail: %v", err) 230 | break 231 | } 232 | 233 | err = sendUDPViaRaw(f.rawfd, fromaddr, toaddr, buf) 234 | if err != nil { 235 | logs.Error("send via raw socket fail: %v", err) 236 | } 237 | 238 | f.udpsessLock.Lock() 239 | udpsess := f.udpSessions[sessionKey] 240 | if udpsess != nil { 241 | udpsess.lastActive = time.Now() 242 | } 243 | f.udpsessLock.Unlock() 244 | } 245 | } 246 | 247 | func (f *UDPForward) recyeleSession() { 248 | tick := time.NewTicker(time.Second * 5) 249 | for range tick.C { 250 | f.udpsessLock.Lock() 251 | for k, s := range f.udpSessions { 252 | if time.Now().Sub(s.lastActive).Seconds() > float64(f.sessionTimeout) { 253 | logs.Warn("remove udp %v session, lastActive: %v", k, s.lastActive) 254 | delete(f.udpSessions, k) 255 | } 256 | } 257 | f.udpsessLock.Unlock() 258 | } 259 | } 260 | 261 | func (f *UDPForward) getOriginDst(hdr []byte) (*net.UDPAddr, error) { 262 | msgs, err := syscall.ParseSocketControlMessage(hdr) 263 | if err != nil { 264 | return nil, err 265 | } 266 | 267 | var origindst *net.UDPAddr 268 | for _, msg := range msgs { 269 | if msg.Header.Level == syscall.SOL_IP && 270 | msg.Header.Type == syscall.IP_RECVORIGDSTADDR { 271 | originDstRaw := &syscall.RawSockaddrInet4{} 272 | err := binary.Read(bytes.NewReader(msg.Data), binary.LittleEndian, originDstRaw) 273 | if err != nil { 274 | logs.Error("read origin dst fail: %v", err) 275 | continue 276 | } 277 | 278 | // only support for ipv4 279 | if originDstRaw.Family == syscall.AF_INET { 280 | pp := (*syscall.RawSockaddrInet4)(unsafe.Pointer(originDstRaw)) 281 | p := (*[2]byte)(unsafe.Pointer(&pp.Port)) 282 | origindst = &net.UDPAddr{ 283 | IP: net.IPv4(pp.Addr[0], pp.Addr[1], pp.Addr[2], pp.Addr[3]), 284 | Port: int(p[0])<<8 + int(p[1]), 285 | } 286 | } 287 | } 288 | } 289 | 290 | if origindst == nil { 291 | return nil, fmt.Errorf("get origin dst fail") 292 | } 293 | 294 | return origindst, nil 295 | } 296 | -------------------------------------------------------------------------------- /opennotrd/plugin/dummy/dummy.go: -------------------------------------------------------------------------------- 1 | package dummy 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/ICKelin/opennotr/internal/logs" 7 | "github.com/ICKelin/opennotr/opennotrd/plugin" 8 | ) 9 | 10 | func init() { 11 | plugin.Register("dummy", &DummyPlugin{}) 12 | } 13 | 14 | type DummyPlugin struct{} 15 | 16 | func (d *DummyPlugin) Setup(cfg json.RawMessage) error { 17 | return nil 18 | } 19 | 20 | func (d *DummyPlugin) RunProxy(meta *plugin.PluginMeta) (*plugin.ProxyTuple, error) { 21 | logs.Info("dummy plugin client config: %v", meta.Ctx) 22 | return &plugin.ProxyTuple{}, nil 23 | } 24 | 25 | func (d *DummyPlugin) StopProxy(meta *plugin.PluginMeta) {} 26 | -------------------------------------------------------------------------------- /opennotrd/plugin/plugin.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "sync" 7 | 8 | "github.com/ICKelin/opennotr/internal/logs" 9 | ) 10 | 11 | var pluginMgr = &PluginManager{ 12 | routes: make(map[string]*PluginMeta), 13 | plugins: make(map[string]IPlugin), 14 | } 15 | 16 | // ProxyTuple defineds plugins real proxy address 17 | type ProxyTuple struct { 18 | Protocol string 19 | FromPort string 20 | ToPort string 21 | } 22 | 23 | // PluginMeta defineds data that the plugins needs 24 | // these members are filled by server.go 25 | type PluginMeta struct { 26 | // plugin register protocol 27 | // eg: tcp, udp, http, http2, h2c 28 | Protocol string 29 | 30 | // From specific local listener address of plugin 31 | // browser or other clients will connect to this address 32 | // it's no use for restyproxy plugin. 33 | From string 34 | 35 | // To specific VIP:port of our VPN peer node. 36 | // For example: 37 | // our VPN virtual lan cidr is 100.64.100.1/24 38 | // the connected VPN client's VPN lan ip is 100.64.100.10/24 39 | // and it wants to export 8080 as http port, so the $To is 40 | // 100.64.100.10:8080 41 | To string 42 | 43 | // Domain specific the domain of our VPN peer node. 44 | // It could be empty 45 | Domain string 46 | 47 | // Data you want to passto plugin 48 | // Reserve 49 | Ctx interface{} 50 | RecycleSignal chan struct{} 51 | } 52 | 53 | func (item *PluginMeta) identify() string { 54 | return fmt.Sprintf("%s:%s:%s", item.Protocol, item.From, item.Domain) 55 | } 56 | 57 | // IPlugin defines plugin interface 58 | // Plugin should implements the IPlugin 59 | type IPlugin interface { 60 | // Setup calls at the begin of plugin system initialize 61 | // plugin system will pass the raw message to plugin's Setup function 62 | Setup(json.RawMessage) error 63 | 64 | // Close a proxy, it may be called by client's connection close 65 | StopProxy(item *PluginMeta) 66 | 67 | // Run a proxy, it may be called by client's connection established 68 | RunProxy(item *PluginMeta) (*ProxyTuple, error) 69 | } 70 | 71 | type PluginManager struct { 72 | mu sync.Mutex 73 | 74 | // routes stores proxier of localAddress 75 | // key: pluginMeta.identify() 76 | // value: pluginMeta 77 | routes map[string]*PluginMeta 78 | 79 | // plugins store plugin information 80 | // by call plugin.Register function. 81 | // key: protocol, eg: tcp, udp 82 | // value: plugin implement 83 | plugins map[string]IPlugin 84 | } 85 | 86 | func DefaultPluginManager() *PluginManager { 87 | return pluginMgr 88 | } 89 | 90 | func Register(protocol string, p IPlugin) { 91 | pluginMgr.plugins[protocol] = p 92 | } 93 | 94 | func Setup(plugins map[string]string) error { 95 | for protocol, cfg := range plugins { 96 | logs.Info("setup for %s with configuration:\n%s", protocol, cfg) 97 | plug, ok := pluginMgr.plugins[protocol] 98 | if !ok { 99 | logs.Error("protocol %s not register", protocol) 100 | return fmt.Errorf("protocol %s not register", protocol) 101 | } 102 | 103 | err := plug.Setup([]byte(cfg)) 104 | if err != nil { 105 | logs.Error("setup protocol %s fail: %v", protocol, err) 106 | return err 107 | } 108 | } 109 | 110 | return nil 111 | } 112 | 113 | func (p *PluginManager) AddProxy(item *PluginMeta) (*ProxyTuple, error) { 114 | p.mu.Lock() 115 | defer p.mu.Unlock() 116 | key := item.identify() 117 | if _, ok := p.routes[key]; ok { 118 | return nil, fmt.Errorf("port %s is in used", key) 119 | } 120 | 121 | plug, ok := p.plugins[item.Protocol] 122 | if !ok { 123 | return nil, fmt.Errorf("proxy %s not register", item.Protocol) 124 | } 125 | 126 | tuple, err := plug.RunProxy(item) 127 | if err != nil { 128 | logs.Error("run proxy fail: %v", err) 129 | return nil, err 130 | } 131 | p.routes[key] = item 132 | return tuple, nil 133 | } 134 | 135 | func (p *PluginManager) DelProxy(item *PluginMeta) { 136 | p.mu.Lock() 137 | defer p.mu.Unlock() 138 | key := item.identify() 139 | 140 | plug, ok := p.plugins[item.Protocol] 141 | if ok { 142 | plug.StopProxy(item) 143 | } 144 | 145 | delete(p.routes, key) 146 | } 147 | -------------------------------------------------------------------------------- /opennotrd/plugin/restyproxy/restyproxy.go: -------------------------------------------------------------------------------- 1 | package restyproxy 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "net" 9 | "net/http" 10 | "time" 11 | 12 | "github.com/ICKelin/opennotr/internal/logs" 13 | "github.com/ICKelin/opennotr/opennotrd/plugin" 14 | ) 15 | 16 | var restyAdminUrl string 17 | 18 | func init() { 19 | plugin.Register("http", &RestyProxy{}) 20 | plugin.Register("https", &RestyProxy{}) 21 | plugin.Register("h2c", &RestyProxy{}) 22 | } 23 | 24 | type AddUpstreamBody struct { 25 | Scheme string `json:"scheme"` 26 | Host string `json:"host"` 27 | IP string `json:"ip"` 28 | Port string `json:"port"` 29 | } 30 | 31 | type RestyConfig struct { 32 | RestyAdminUrl string `json:"adminUrl"` 33 | } 34 | 35 | type RestyProxy struct { 36 | cfg RestyConfig 37 | } 38 | 39 | func (p *RestyProxy) Setup(config json.RawMessage) error { 40 | var cfg = RestyConfig{} 41 | err := json.Unmarshal([]byte(config), &cfg) 42 | if err != nil { 43 | return err 44 | } 45 | p.cfg = cfg 46 | return nil 47 | } 48 | 49 | func (p *RestyProxy) StopProxy(item *plugin.PluginMeta) { 50 | p.sendDeleteReq(item.Domain, item.Protocol) 51 | } 52 | 53 | func (p *RestyProxy) RunProxy(item *plugin.PluginMeta) (*plugin.ProxyTuple, error) { 54 | vip, port, err := net.SplitHostPort(item.To) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | req := &AddUpstreamBody{ 60 | Scheme: item.Protocol, 61 | Host: item.Domain, 62 | IP: vip, 63 | Port: port, 64 | } 65 | 66 | go p.sendPostReq(req) 67 | 68 | _, toPort, _ := net.SplitHostPort(item.To) 69 | return &plugin.ProxyTuple{ 70 | Protocol: item.Protocol, 71 | ToPort: toPort, 72 | }, nil 73 | } 74 | 75 | func (p *RestyProxy) sendPostReq(body interface{}) { 76 | cli := http.Client{ 77 | Timeout: time.Second * 10, 78 | } 79 | 80 | buf, _ := json.Marshal(body) 81 | br := bytes.NewBuffer(buf) 82 | 83 | req, err := http.NewRequest("POST", p.cfg.RestyAdminUrl, br) 84 | if err != nil { 85 | logs.Error("request %v fail: %v", body, err) 86 | return 87 | } 88 | 89 | resp, err := cli.Do(req) 90 | if err != nil { 91 | logs.Error("request %v fail: %v", body, err) 92 | return 93 | } 94 | defer resp.Body.Close() 95 | 96 | cnt, err := ioutil.ReadAll(resp.Body) 97 | if err != nil { 98 | logs.Error("request %v fail: %v", body, err) 99 | return 100 | } 101 | logs.Info("set upstream %v reply: %s", body, string(cnt)) 102 | } 103 | 104 | func (p *RestyProxy) sendDeleteReq(host, scheme string) { 105 | cli := http.Client{ 106 | Timeout: time.Second * 10, 107 | } 108 | url := fmt.Sprintf("%s?host=%s&scheme=%s", p.cfg.RestyAdminUrl, host, scheme) 109 | req, err := http.NewRequest("DELETE", url, nil) 110 | if err != nil { 111 | logs.Error("delete host %s fail: %v", host, err) 112 | return 113 | } 114 | 115 | resp, err := cli.Do(req) 116 | if err != nil { 117 | logs.Error("delete host %s fail: %v", host, err) 118 | return 119 | } 120 | defer resp.Body.Close() 121 | 122 | cnt, err := ioutil.ReadAll(resp.Body) 123 | if err != nil { 124 | logs.Error("delete host %s fail: %v", host, err) 125 | return 126 | } 127 | 128 | logs.Info("delete upstream reply: %s", string(cnt)) 129 | } 130 | -------------------------------------------------------------------------------- /opennotrd/plugin/tcpproxy/tcpproxy.go: -------------------------------------------------------------------------------- 1 | package tcpproxy 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "net" 7 | "sync" 8 | "time" 9 | 10 | "github.com/ICKelin/opennotr/internal/logs" 11 | "github.com/ICKelin/opennotr/opennotrd/plugin" 12 | ) 13 | 14 | func init() { 15 | plugin.Register("tcp", &TCPProxy{}) 16 | } 17 | 18 | type TCPProxy struct{} 19 | 20 | func (t *TCPProxy) Setup(config json.RawMessage) error { return nil } 21 | 22 | func (t *TCPProxy) StopProxy(item *plugin.PluginMeta) { 23 | select { 24 | case item.RecycleSignal <- struct{}{}: 25 | default: 26 | } 27 | } 28 | 29 | // RunProxy runs a tcp server and proxy to item.To 30 | // RunProxy may change item.From address to the real listenner address 31 | func (t *TCPProxy) RunProxy(item *plugin.PluginMeta) (*plugin.ProxyTuple, error) { 32 | from, to := item.From, item.To 33 | lis, err := net.Listen("tcp", from) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | fin := make(chan struct{}) 39 | go func() { 40 | select { 41 | case <-item.RecycleSignal: 42 | logs.Info("receive recycle signal for %s", from) 43 | lis.Close() 44 | case <-fin: 45 | return 46 | } 47 | }() 48 | 49 | go func() { 50 | defer lis.Close() 51 | defer close(fin) 52 | 53 | sess := &sync.Map{} 54 | defer func() { 55 | sess.Range(func(k, v interface{}) bool { 56 | if conn, ok := v.(net.Conn); ok { 57 | conn.Close() 58 | } 59 | return true 60 | }) 61 | }() 62 | 63 | for { 64 | conn, err := lis.Accept() 65 | if err != nil { 66 | logs.Error("accept fail: %v", err) 67 | break 68 | } 69 | 70 | go func() { 71 | sess.Store(conn.RemoteAddr().String(), conn) 72 | defer sess.Delete(conn.RemoteAddr().String()) 73 | t.doProxy(conn, to) 74 | }() 75 | } 76 | }() 77 | 78 | _, fromPort, _ := net.SplitHostPort(lis.Addr().String()) 79 | _, toPort, _ := net.SplitHostPort(item.To) 80 | 81 | return &plugin.ProxyTuple{ 82 | Protocol: item.Protocol, 83 | FromPort: fromPort, 84 | ToPort: toPort, 85 | }, nil 86 | } 87 | 88 | func (t *TCPProxy) doProxy(conn net.Conn, to string) { 89 | defer conn.Close() 90 | 91 | toconn, err := net.DialTimeout("tcp", to, time.Second*10) 92 | if err != nil { 93 | logs.Error("dial fail: %v", err) 94 | return 95 | } 96 | defer toconn.Close() 97 | 98 | wg := &sync.WaitGroup{} 99 | wg.Add(2) 100 | 101 | go func() { 102 | defer wg.Done() 103 | buf := make([]byte, 1500) 104 | io.CopyBuffer(toconn, conn, buf) 105 | }() 106 | 107 | go func() { 108 | defer wg.Done() 109 | buf := make([]byte, 1500) 110 | io.CopyBuffer(conn, toconn, buf) 111 | }() 112 | wg.Wait() 113 | } 114 | -------------------------------------------------------------------------------- /opennotrd/plugin/udpproxy/udpproxy.go: -------------------------------------------------------------------------------- 1 | package udpproxy 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "net" 7 | "sync" 8 | "time" 9 | 10 | "github.com/ICKelin/opennotr/internal/logs" 11 | "github.com/ICKelin/opennotr/opennotrd/plugin" 12 | ) 13 | 14 | // default timeout for udp session 15 | var defaultTimeout = 30 16 | 17 | func init() { 18 | plugin.Register("udp", &UDPProxy{}) 19 | } 20 | 21 | type config struct { 22 | // session timeout(second) 23 | SessionTimeout int `json:"sessionTimeout"` 24 | } 25 | 26 | type UDPProxy struct { 27 | cfg config 28 | } 29 | 30 | func (p *UDPProxy) Setup(rawMessage json.RawMessage) error { 31 | var cfg config 32 | err := json.Unmarshal(rawMessage, &cfg) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | if cfg.SessionTimeout <= 0 { 38 | cfg.SessionTimeout = defaultTimeout 39 | } 40 | p.cfg = cfg 41 | return nil 42 | } 43 | 44 | func (p *UDPProxy) StopProxy(item *plugin.PluginMeta) { 45 | select { 46 | case item.RecycleSignal <- struct{}{}: 47 | default: 48 | } 49 | } 50 | 51 | func (p *UDPProxy) RunProxy(item *plugin.PluginMeta) (*plugin.ProxyTuple, error) { 52 | from := item.From 53 | laddr, err := net.ResolveUDPAddr("udp", from) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | lis, err := net.ListenUDP("udp", laddr) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | go p.doProxy(lis, item) 64 | 65 | _, fromPort, _ := net.SplitHostPort(lis.LocalAddr().String()) 66 | _, toPort, _ := net.SplitHostPort(item.To) 67 | 68 | return &plugin.ProxyTuple{ 69 | Protocol: item.Protocol, 70 | FromPort: fromPort, 71 | ToPort: toPort, 72 | }, nil 73 | } 74 | 75 | func (p *UDPProxy) doProxy(lis *net.UDPConn, item *plugin.PluginMeta) { 76 | defer lis.Close() 77 | 78 | from := item.From 79 | ctx, cancel := context.WithCancel(context.Background()) 80 | defer cancel() 81 | 82 | // receive this signal and close the listener 83 | // the listener close will force lis.ReadFromUDP loop break 84 | // then close all the client socket and end udpCopy 85 | go func() { 86 | select { 87 | case <-item.RecycleSignal: 88 | logs.Info("receive recycle signal for %s", from) 89 | lis.Close() 90 | case <-ctx.Done(): 91 | return 92 | } 93 | }() 94 | 95 | // sess store all backend connection 96 | // key: client address 97 | // value: *net.UDPConn 98 | sess := sync.Map{} 99 | 100 | // sessionTimeout store all session key active time 101 | // the purpose of this is to avoid too session without expired 102 | sessionTimeout := sync.Map{} 103 | 104 | // close all backend sockets 105 | // this action may end udpCopy 106 | defer func() { 107 | sess.Range(func(k, v interface{}) bool { 108 | if conn, ok := v.(*net.UDPConn); ok { 109 | conn.Close() 110 | } 111 | return true 112 | }) 113 | }() 114 | 115 | go func() { 116 | timeout := p.cfg.SessionTimeout 117 | interval := timeout / 2 118 | if interval <= 0 { 119 | interval = timeout 120 | } 121 | tick := time.NewTicker(time.Second * time.Duration(interval)) 122 | for range tick.C { 123 | sessionTimeout.Range(func(k, v interface{}) bool { 124 | lastActiveAt, ok := v.(time.Time) 125 | if !ok { 126 | return true 127 | } 128 | 129 | if time.Now().Sub(lastActiveAt).Seconds() > float64(timeout) { 130 | sess.Delete(k) 131 | } 132 | return true 133 | }) 134 | } 135 | }() 136 | 137 | var buf = make([]byte, 64*1024) 138 | for { 139 | nr, raddr, err := lis.ReadFromUDP(buf) 140 | if err != nil { 141 | logs.Error("read from udp fail: %v", err) 142 | break 143 | } 144 | 145 | key := raddr.String() 146 | val, ok := sess.Load(key) 147 | if !ok { 148 | backendAddr, err := net.ResolveUDPAddr("udp", item.To) 149 | if err != nil { 150 | logs.Error("resolve udp fail: %v", err) 151 | break 152 | } 153 | 154 | backendConn, err := net.DialUDP("udp", nil, backendAddr) 155 | if err != nil { 156 | logs.Error("dial udp fail: %v", err) 157 | break 158 | } 159 | sess.Store(key, backendConn) 160 | sessionTimeout.Store(key, time.Now()) 161 | 162 | // read from $to address and write to $from address 163 | go p.udpCopy(lis, backendConn, raddr) 164 | } 165 | 166 | val, ok = sess.Load(key) 167 | if !ok { 168 | continue 169 | } 170 | 171 | sessionTimeout.Store(key, time.Now()) 172 | // read from $from address and write to $to address 173 | val.(*net.UDPConn).Write(buf[:nr]) 174 | } 175 | } 176 | 177 | func (p *UDPProxy) udpCopy(dst, src *net.UDPConn, toaddr *net.UDPAddr) { 178 | defer src.Close() 179 | buf := make([]byte, 64*1024) 180 | for { 181 | nr, _, err := src.ReadFromUDP(buf) 182 | if err != nil { 183 | logs.Error("read from udp fail: %v", err) 184 | break 185 | } 186 | 187 | _, err = dst.WriteToUDP(buf[:nr], toaddr) 188 | if err != nil { 189 | logs.Error("write to udp fail: %v", err) 190 | break 191 | } 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /opennotrd/plugins.go: -------------------------------------------------------------------------------- 1 | package opennotrd 2 | 3 | import ( 4 | // plugin import 5 | _ "github.com/ICKelin/opennotr/opennotrd/plugin/dummy" 6 | _ "github.com/ICKelin/opennotr/opennotrd/plugin/restyproxy" 7 | _ "github.com/ICKelin/opennotr/opennotrd/plugin/tcpproxy" 8 | _ "github.com/ICKelin/opennotr/opennotrd/plugin/udpproxy" 9 | ) 10 | -------------------------------------------------------------------------------- /opennotrd/run.go: -------------------------------------------------------------------------------- 1 | package opennotrd 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | 7 | "github.com/ICKelin/opennotr/internal/logs" 8 | "github.com/ICKelin/opennotr/opennotrd/core" 9 | "github.com/ICKelin/opennotr/opennotrd/plugin" 10 | ) 11 | 12 | func Run() { 13 | confpath := flag.String("conf", "", "config file path") 14 | flag.Parse() 15 | 16 | cfg, err := core.ParseConfig(*confpath) 17 | if err != nil { 18 | fmt.Println(err) 19 | return 20 | } 21 | 22 | logs.Init("opennotrd.log", "info", 10) 23 | logs.Info("config: %v", cfg) 24 | 25 | // create dhcp manager 26 | // dhcp Select/Release ip for opennotr client 27 | dhcp, err := core.NewDHCP(cfg.DHCPConfig.Cidr) 28 | if err != nil { 29 | logs.Error("new dhcp module fail: %v", err) 30 | return 31 | } 32 | 33 | // setup all plugin base on plugin json configuration 34 | err = plugin.Setup(cfg.Plugins) 35 | if err != nil { 36 | logs.Error("setup plugin fail: %v", err) 37 | return 38 | } 39 | 40 | // initial resolver 41 | // currently resolver use coredns and etcd 42 | // our resolver just write DOMAIN => VIP record to etcd 43 | var resolver *core.Resolver 44 | if len(cfg.ResolverConfig.EtcdEndpoints) > 0 { 45 | resolver, err = core.NewResolve(cfg.ResolverConfig.EtcdEndpoints) 46 | if err != nil { 47 | logs.Error("new resolve fail: %v", err) 48 | return 49 | } 50 | } 51 | 52 | // up local tcp,udp service 53 | // we use tproxy to route traffic to the tcp port and udp port here. 54 | tcpfw := core.NewTCPForward(cfg.TCPForwardConfig) 55 | listener, err := tcpfw.Listen() 56 | if err != nil { 57 | logs.Error("listen tproxy tcp fail: %v", err) 58 | return 59 | } 60 | 61 | go tcpfw.Serve(listener) 62 | 63 | udpfw := core.NewUDPForward(cfg.UDPForwardConfig) 64 | lconn, err := udpfw.Listen() 65 | if err != nil { 66 | logs.Error("listen tproxy udp fail: %v", err) 67 | return 68 | } 69 | go udpfw.Serve(lconn) 70 | 71 | // server provides tcp server for opennotr client 72 | s := core.NewServer(cfg.ServerConfig, dhcp, resolver) 73 | fmt.Println(s.ListenAndServe()) 74 | } 75 | -------------------------------------------------------------------------------- /opennotrd/test/httpecho/httpclient.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "time" 9 | ) 10 | 11 | func main() { 12 | remoteAddr := flag.String("r", "", "remote address") 13 | flag.Parse() 14 | 15 | url := fmt.Sprintf("http://%s/echo", *remoteAddr) 16 | for i := 0; i < 100; i++ { 17 | beg := time.Now() 18 | rsp, err := http.Get(url) 19 | if err != nil { 20 | break 21 | } 22 | cnt, err := ioutil.ReadAll(rsp.Body) 23 | if err != nil { 24 | fmt.Println(err) 25 | break 26 | } 27 | 28 | rsp.Body.Close() 29 | fmt.Printf("echo http packet %d %s rtt %dms\n", i+1, string(cnt), time.Now().Sub(beg).Milliseconds()) 30 | time.Sleep(time.Second * 1) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /opennotrd/test/httpecho/httpserver.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "net/http" 4 | 5 | func main() { 6 | http.HandleFunc("/echo", func(w http.ResponseWriter, r *http.Request) { 7 | w.Write([]byte(r.RemoteAddr)) 8 | }) 9 | http.ListenAndServe(":8080", nil) 10 | } 11 | -------------------------------------------------------------------------------- /opennotrd/test/plugin_test.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | "time" 7 | 8 | "github.com/ICKelin/opennotr/opennotrd/plugin" 9 | _ "github.com/ICKelin/opennotr/opennotrd/plugin/tcpproxy" 10 | ) 11 | 12 | var listener net.Listener 13 | 14 | func init() { 15 | 16 | } 17 | 18 | func runTCPServer(bufsize int) net.Listener { 19 | lis, err := net.Listen("tcp", "127.0.0.1:2345") 20 | if err != nil { 21 | panic(err) 22 | } 23 | 24 | go func() { 25 | defer lis.Close() 26 | for { 27 | conn, err := lis.Accept() 28 | if err != nil { 29 | break 30 | } 31 | 32 | go onconn(conn, bufsize) 33 | } 34 | }() 35 | return lis 36 | } 37 | 38 | func onconn(conn net.Conn, bufsize int) { 39 | defer conn.Close() 40 | buf := make([]byte, bufsize) 41 | nr, _ := conn.Read(buf) 42 | conn.Write(buf[:nr]) 43 | } 44 | 45 | func runEcho(t *testing.T, bufsize, numconn int) { 46 | item := &plugin.PluginMeta{ 47 | Protocol: "tcp", 48 | From: "127.0.0.1:1234", 49 | To: "127.0.0.1:2345", 50 | RecycleSignal: make(chan struct{}), 51 | } 52 | err := plugin.DefaultPluginManager().AddProxy(item) 53 | if err != nil { 54 | t.Error(err) 55 | return 56 | } 57 | defer plugin.DefaultPluginManager().DelProxy(item) 58 | 59 | lis := runTCPServer(bufsize) 60 | 61 | // client 62 | for i := 0; i < numconn; i++ { 63 | go func() { 64 | buf := make([]byte, bufsize) 65 | conn, err := net.Dial("tcp", "127.0.0.1:1234") 66 | if err != nil { 67 | t.Error(err) 68 | return 69 | } 70 | defer conn.Close() 71 | for { 72 | conn.Write(buf) 73 | conn.Read(buf) 74 | } 75 | }() 76 | } 77 | tick := time.NewTicker(time.Second * 60) 78 | <-tick.C 79 | lis.Close() 80 | time.Sleep(time.Second) 81 | } 82 | 83 | func TestTCPEcho128B(t *testing.T) { 84 | numconn := 128 85 | bufsize := 1024 86 | runEcho(t, bufsize, numconn) 87 | } 88 | -------------------------------------------------------------------------------- /opennotrd/test/tcpecho/tcpclient.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "net" 7 | "time" 8 | ) 9 | 10 | func main() { 11 | remoteAddr := flag.String("r", "", "remote address") 12 | flag.Parse() 13 | 14 | buf := make([]byte, 1024) 15 | for i := 0; i < 100; i++ { 16 | beg := time.Now() 17 | 18 | conn, err := net.Dial("tcp", *remoteAddr) 19 | if err != nil { 20 | fmt.Println(err) 21 | break 22 | } 23 | 24 | conn.Write(buf) 25 | conn.Read(buf) 26 | conn.Close() 27 | fmt.Printf("echo tcp packet %d rtt %dms\n", i+1, time.Now().Sub(beg).Milliseconds()) 28 | time.Sleep(time.Second * 1) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /opennotrd/test/tcpecho/tcpserver.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "net" 7 | ) 8 | 9 | func main() { 10 | localAddress := flag.String("l", "", "local address") 11 | flag.Parse() 12 | 13 | listener, err := net.Listen("tcp", *localAddress) 14 | if err != nil { 15 | fmt.Println(err) 16 | return 17 | } 18 | defer listener.Close() 19 | 20 | for { 21 | conn, err := listener.Accept() 22 | if err != nil { 23 | break 24 | } 25 | 26 | go func() { 27 | defer conn.Close() 28 | buf := make([]byte, 1024) 29 | nr, err := conn.Read(buf) 30 | if err != nil { 31 | fmt.Println(err) 32 | return 33 | } 34 | 35 | conn.Write(buf[:nr]) 36 | }() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /opennotrd/test/udpecho/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "net" 7 | "time" 8 | ) 9 | 10 | func main() { 11 | remoteAddr := flag.String("r", "", "remote address") 12 | flag.Parse() 13 | 14 | rconn, err := net.Dial("udp", *remoteAddr) 15 | if err != nil { 16 | fmt.Println(err) 17 | return 18 | } 19 | 20 | buf := make([]byte, 1024) 21 | for i := 0; i < 100; i++ { 22 | beg := time.Now() 23 | _, err := rconn.Write(buf) 24 | if err != nil { 25 | fmt.Println(err) 26 | break 27 | } 28 | 29 | rconn.Read(buf) 30 | fmt.Printf("echo udp packet %d rtt %dms\n", i+1, time.Now().Sub(beg).Milliseconds()) 31 | time.Sleep(time.Second * 1) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /opennotrd/test/udpecho/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "net" 7 | ) 8 | 9 | func main() { 10 | localAddress := flag.String("l", "", "local address") 11 | flag.Parse() 12 | 13 | laddr, err := net.ResolveUDPAddr("udp", *localAddress) 14 | if err != nil { 15 | fmt.Println(err) 16 | return 17 | } 18 | 19 | lconn, err := net.ListenUDP("udp", laddr) 20 | if err != nil { 21 | fmt.Println(err) 22 | return 23 | } 24 | defer lconn.Close() 25 | 26 | buf := make([]byte, 64*1024) 27 | for { 28 | nr, raddr, err := lconn.ReadFromUDP(buf) 29 | if err != nil { 30 | fmt.Println(err) 31 | break 32 | } 33 | lconn.WriteToUDP(buf[:nr], raddr) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /opennotrd/test/websocket/wsclient.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/gorilla/websocket" 9 | ) 10 | 11 | func main() { 12 | raddr := flag.String("r", "", "remote address") 13 | flag.Parse() 14 | buf := make([]byte, 1024) 15 | for i := 0; i < 100; i++ { 16 | beg := time.Now() 17 | conn, _, err := websocket.DefaultDialer.Dial(*raddr, nil) 18 | if err != nil { 19 | fmt.Println(err) 20 | return 21 | } 22 | 23 | defer conn.Close() 24 | conn.WriteMessage(websocket.BinaryMessage, buf) 25 | conn.ReadMessage() 26 | fmt.Printf("echo websocket packet %d rtt %dms\n", i+1, time.Now().Sub(beg).Milliseconds()) 27 | time.Sleep(time.Second * 1) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /opennotrd/test/websocket/wsserver.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/siddontang/go/websocket" 9 | ) 10 | 11 | func main() { 12 | localAddress := flag.String("l", "", "local address") 13 | flag.Parse() 14 | 15 | http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) { 16 | conn, err := websocket.Upgrade(w, r, r.Header) 17 | if err != nil { 18 | fmt.Println(err) 19 | return 20 | } 21 | defer conn.Close() 22 | 23 | msgType, msg, err := conn.Read() 24 | if err != nil { 25 | fmt.Println(err) 26 | return 27 | } 28 | conn.WriteMessage(msgType, msg) 29 | }) 30 | http.ListenAndServe(*localAddress, nil) 31 | } 32 | --------------------------------------------------------------------------------