Initial
This commit is contained in:
parent
0b2512fc15
commit
9f4c4e2c5c
21 changed files with 9715 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
easyrsa
|
||||||
|
openvpn-web-ui
|
3
build.sh
Executable file
3
build.sh
Executable file
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
go build .
|
26
client.conf.tpl
Normal file
26
client.conf.tpl
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
remote {{ .Host }} {{ .Port }} tcp
|
||||||
|
verb 4
|
||||||
|
client
|
||||||
|
nobind
|
||||||
|
dev tun
|
||||||
|
cipher AES-128-CBC
|
||||||
|
key-direction 1
|
||||||
|
#redirect-gateway def1
|
||||||
|
tls-client
|
||||||
|
remote-cert-tls server
|
||||||
|
# for update resolv.conf on ubuntu
|
||||||
|
#script-security 2 system
|
||||||
|
#up /etc/openvpn/update-resolv-conf
|
||||||
|
#down /etc/openvpn/update-resolv-conf
|
||||||
|
<cert>
|
||||||
|
{{ .Cert -}}
|
||||||
|
</cert>
|
||||||
|
<key>
|
||||||
|
{{ .Key -}}
|
||||||
|
</key>
|
||||||
|
<ca>
|
||||||
|
{{ .CA -}}
|
||||||
|
</ca>
|
||||||
|
<tls-auth>
|
||||||
|
{{ .TLS -}}
|
||||||
|
</tls-auth>
|
6
frontend/.babelrc
Normal file
6
frontend/.babelrc
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"presets": [
|
||||||
|
["env", { "modules": false }],
|
||||||
|
"stage-3"
|
||||||
|
]
|
||||||
|
}
|
9
frontend/.editorconfig
Normal file
9
frontend/.editorconfig
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
12
frontend/.gitignore
vendored
Normal file
12
frontend/.gitignore
vendored
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
.DS_Store
|
||||||
|
node_modules/
|
||||||
|
static/dist/
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
7
frontend/build.sh
Executable file
7
frontend/build.sh
Executable file
|
@ -0,0 +1,7 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
image="node:14.2-alpine3.11"
|
||||||
|
uid="$(id -u $USER)"
|
||||||
|
|
||||||
|
docker run -u $uid -w /app -v $(pwd):/app $image npm i && \
|
||||||
|
docker run -u $uid -w /app -v $(pwd):/app $image npm run build
|
8407
frontend/package-lock.json
generated
Normal file
8407
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
37
frontend/package.json
Normal file
37
frontend/package.json
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
{
|
||||||
|
"name": "openvpn-easyrsa-web-ui",
|
||||||
|
"description": "A Vue.js project",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "vitaliy.snurnitsin@gmail.com",
|
||||||
|
"license": "MIT",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "cross-env NODE_ENV=development webpack-dev-server --hot",
|
||||||
|
"build": "cross-env NODE_ENV=production webpack --progress --hide-modules"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^0.18.0",
|
||||||
|
"vue": "^2.5.17",
|
||||||
|
"vue-clipboard2": "^0.2.1"
|
||||||
|
},
|
||||||
|
"browserslist": [
|
||||||
|
"> 1%",
|
||||||
|
"last 2 versions",
|
||||||
|
"not ie <= 8"
|
||||||
|
],
|
||||||
|
"devDependencies": {
|
||||||
|
"babel-core": "^6.26.3",
|
||||||
|
"babel-loader": "^7.1.5",
|
||||||
|
"babel-preset-env": "^1.7.0",
|
||||||
|
"babel-preset-stage-3": "^6.24.1",
|
||||||
|
"cross-env": "^5.2.0",
|
||||||
|
"css-loader": "^0.28.7",
|
||||||
|
"file-loader": "^1.1.4",
|
||||||
|
"node-sass": "^4.9.3",
|
||||||
|
"sass-loader": "^6.0.6",
|
||||||
|
"vue-loader": "^13.7.3",
|
||||||
|
"vue-template-compiler": "^2.5.17",
|
||||||
|
"webpack": "^3.12.0",
|
||||||
|
"webpack-dev-server": "^2.11.3"
|
||||||
|
}
|
||||||
|
}
|
129
frontend/src/main.js
Normal file
129
frontend/src/main.js
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
import Vue from 'vue';
|
||||||
|
import axios from 'axios';
|
||||||
|
import VueClipboard from 'vue-clipboard2'
|
||||||
|
|
||||||
|
Vue.use(VueClipboard)
|
||||||
|
|
||||||
|
var axios_cfg = function(url, data='', type='form') {
|
||||||
|
if (data == '') {
|
||||||
|
return {
|
||||||
|
method: 'get',
|
||||||
|
url: url
|
||||||
|
};
|
||||||
|
} else if (type == 'form') {
|
||||||
|
return {
|
||||||
|
method: 'post',
|
||||||
|
url: url,
|
||||||
|
data: data,
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
||||||
|
};
|
||||||
|
} else if (type == 'file') {
|
||||||
|
return {
|
||||||
|
method: 'post',
|
||||||
|
url: url,
|
||||||
|
data: data,
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
el: '#app',
|
||||||
|
data: {
|
||||||
|
u: {
|
||||||
|
ctxTop: '0',
|
||||||
|
ctxLeft: '0',
|
||||||
|
ctxVisible: false,
|
||||||
|
ctxMenuItems: { 'u-revoke': 'Revoke', 'u-unrevoke': 'Unrevoke', 'u-show-config': 'Show config'},
|
||||||
|
columns: [],
|
||||||
|
data: {},
|
||||||
|
name: '',
|
||||||
|
newUserName: '',
|
||||||
|
modalNewUserVisible: false,
|
||||||
|
modalShowConfigVisible: false,
|
||||||
|
openvpnConfig: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
u: function () {
|
||||||
|
this.u.columns = Object.keys(this.u.data[0]) //.reverse()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted: function () {
|
||||||
|
this.u_get_data()
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
var _this = this
|
||||||
|
this.$root.$on('u-revoke', function (msg) {
|
||||||
|
var data = new URLSearchParams();
|
||||||
|
data.append('username', _this.u.name);
|
||||||
|
axios.request(axios_cfg('api/user/revoke', data, 'form'))
|
||||||
|
.then(function(response) {
|
||||||
|
console.log(response.data);
|
||||||
|
_this.u_get_data();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
this.$root.$on('u-unrevoke', function () {
|
||||||
|
var data = new URLSearchParams();
|
||||||
|
data.append('username', _this.u.name);
|
||||||
|
axios.request(axios_cfg('api/user/unrevoke', data, 'form'))
|
||||||
|
.then(function(response) {
|
||||||
|
console.log(response.data);
|
||||||
|
_this.u_get_data();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
this.$root.$on('u-show-config', function () {
|
||||||
|
this.u.modalShowConfigVisible = true;
|
||||||
|
var data = new URLSearchParams();
|
||||||
|
data.append('username', _this.u.name);
|
||||||
|
axios.request(axios_cfg('api/user/showconfig', data, 'form'))
|
||||||
|
.then(function(response) {
|
||||||
|
_this.u.openvpnConfig = response.data;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
uCtxStyle: function () {
|
||||||
|
return {
|
||||||
|
'top': this.u.ctxTop + 'px',
|
||||||
|
'left': this.u.ctxLeft + 'px'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
copyTextArea: function (e) {
|
||||||
|
e.clipboardData.setData("text/plain", this.u.openvpnConfig);
|
||||||
|
},
|
||||||
|
u_ctx_click: function (e) {
|
||||||
|
this.$root.$emit(e.target.dataset.name)
|
||||||
|
this.u_ctx_hide()
|
||||||
|
},
|
||||||
|
u_ctx_hide: function () {
|
||||||
|
this.u.ctxVisible = false
|
||||||
|
},
|
||||||
|
u_ctx_show: function (e) {
|
||||||
|
this.u.name = e.target.parentElement.dataset.name
|
||||||
|
this.u.ctxTop = e.pageY
|
||||||
|
this.u.ctxLeft = e.pageX
|
||||||
|
this.u.ctxVisible = true
|
||||||
|
},
|
||||||
|
u_get_data: function() {
|
||||||
|
var _this = this;
|
||||||
|
axios.request(axios_cfg('api/users/list'))
|
||||||
|
.then(function(response) {
|
||||||
|
_this.u.data = response.data
|
||||||
|
});
|
||||||
|
},
|
||||||
|
create_user: function() {
|
||||||
|
var _this = this;
|
||||||
|
var data = new URLSearchParams();
|
||||||
|
data.append('username', this.u.newUserName);
|
||||||
|
axios.request(axios_cfg('api/user/create', data, 'form'))
|
||||||
|
.then(function(response) {
|
||||||
|
console.log(response.data);
|
||||||
|
_this.u_get_data();
|
||||||
|
_this.u.newUserName = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
7
frontend/static/css/bootstrap.min.css
vendored
Normal file
7
frontend/static/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend/static/css/bootstrap.min.css.map
Normal file
1
frontend/static/css/bootstrap.min.css.map
Normal file
File diff suppressed because one or more lines are too long
7
frontend/static/css/bootstrap.min.css_
Normal file
7
frontend/static/css/bootstrap.min.css_
Normal file
File diff suppressed because one or more lines are too long
461
frontend/static/css/normalize.css
vendored
Normal file
461
frontend/static/css/normalize.css
vendored
Normal file
|
@ -0,0 +1,461 @@
|
||||||
|
/*! normalize.css v5.0.0 | MIT License | github.com/necolas/normalize.css */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Change the default font family in all browsers (opinionated).
|
||||||
|
* 2. Correct the line height in all browsers.
|
||||||
|
* 3. Prevent adjustments of font size after orientation changes in
|
||||||
|
* IE on Windows Phone and in iOS.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Document
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-family: sans-serif; /* 1 */
|
||||||
|
line-height: 1.15; /* 2 */
|
||||||
|
-ms-text-size-adjust: 100%; /* 3 */
|
||||||
|
-webkit-text-size-adjust: 100%; /* 3 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sections
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the margin in all browsers (opinionated).
|
||||||
|
*/
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct display in IE 9-.
|
||||||
|
*/
|
||||||
|
|
||||||
|
article,
|
||||||
|
aside,
|
||||||
|
footer,
|
||||||
|
header,
|
||||||
|
nav,
|
||||||
|
section {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct the font size and margin on `h1` elements within `section` and
|
||||||
|
* `article` contexts in Chrome, Firefox, and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
margin: 0.67em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grouping content
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct display in IE 9-.
|
||||||
|
* 1. Add the correct display in IE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
figcaption,
|
||||||
|
figure,
|
||||||
|
main { /* 1 */
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct margin in IE 8.
|
||||||
|
*/
|
||||||
|
|
||||||
|
figure {
|
||||||
|
margin: 1em 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Add the correct box sizing in Firefox.
|
||||||
|
* 2. Show the overflow in Edge and IE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
hr {
|
||||||
|
box-sizing: content-box; /* 1 */
|
||||||
|
height: 0; /* 1 */
|
||||||
|
overflow: visible; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||||
|
* 2. Correct the odd `em` font sizing in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
pre {
|
||||||
|
font-family: monospace, monospace; /* 1 */
|
||||||
|
font-size: 1em; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Text-level semantics
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Remove the gray background on active links in IE 10.
|
||||||
|
* 2. Remove gaps in links underline in iOS 8+ and Safari 8+.
|
||||||
|
*/
|
||||||
|
|
||||||
|
a {
|
||||||
|
background-color: transparent; /* 1 */
|
||||||
|
-webkit-text-decoration-skip: objects; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the outline on focused links when they are also active or hovered
|
||||||
|
* in all browsers (opinionated).
|
||||||
|
*/
|
||||||
|
|
||||||
|
a:active,
|
||||||
|
a:hover {
|
||||||
|
outline-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Remove the bottom border in Firefox 39-.
|
||||||
|
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
abbr[title] {
|
||||||
|
border-bottom: none; /* 1 */
|
||||||
|
text-decoration: underline; /* 2 */
|
||||||
|
text-decoration: underline dotted; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent the duplicate application of `bolder` by the next rule in Safari 6.
|
||||||
|
*/
|
||||||
|
|
||||||
|
b,
|
||||||
|
strong {
|
||||||
|
font-weight: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct font weight in Chrome, Edge, and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
b,
|
||||||
|
strong {
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||||
|
* 2. Correct the odd `em` font sizing in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
code,
|
||||||
|
kbd,
|
||||||
|
samp {
|
||||||
|
font-family: monospace, monospace; /* 1 */
|
||||||
|
font-size: 1em; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct font style in Android 4.3-.
|
||||||
|
*/
|
||||||
|
|
||||||
|
dfn {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct background and color in IE 9-.
|
||||||
|
*/
|
||||||
|
|
||||||
|
mark {
|
||||||
|
background-color: #ff0;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct font size in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent `sub` and `sup` elements from affecting the line height in
|
||||||
|
* all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
sub,
|
||||||
|
sup {
|
||||||
|
font-size: 75%;
|
||||||
|
line-height: 0;
|
||||||
|
position: relative;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub {
|
||||||
|
bottom: -0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
sup {
|
||||||
|
top: -0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Embedded content
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct display in IE 9-.
|
||||||
|
*/
|
||||||
|
|
||||||
|
audio,
|
||||||
|
video {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct display in iOS 4-7.
|
||||||
|
*/
|
||||||
|
|
||||||
|
audio:not([controls]) {
|
||||||
|
display: none;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the border on images inside links in IE 10-.
|
||||||
|
*/
|
||||||
|
|
||||||
|
img {
|
||||||
|
border-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide the overflow in IE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
svg:not(:root) {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Forms
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Change the font styles in all browsers (opinionated).
|
||||||
|
* 2. Remove the margin in Firefox and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
optgroup,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
font-family: sans-serif; /* 1 */
|
||||||
|
font-size: 100%; /* 1 */
|
||||||
|
line-height: 1.15; /* 1 */
|
||||||
|
margin: 0; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the overflow in IE.
|
||||||
|
* 1. Show the overflow in Edge.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
input { /* 1 */
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the inheritance of text transform in Edge, Firefox, and IE.
|
||||||
|
* 1. Remove the inheritance of text transform in Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
select { /* 1 */
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`
|
||||||
|
* controls in Android 4.
|
||||||
|
* 2. Correct the inability to style clickable types in iOS and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
html [type="button"], /* 1 */
|
||||||
|
[type="reset"],
|
||||||
|
[type="submit"] {
|
||||||
|
-webkit-appearance: button; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the inner border and padding in Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button::-moz-focus-inner,
|
||||||
|
[type="button"]::-moz-focus-inner,
|
||||||
|
[type="reset"]::-moz-focus-inner,
|
||||||
|
[type="submit"]::-moz-focus-inner {
|
||||||
|
border-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore the focus styles unset by the previous rule.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button:-moz-focusring,
|
||||||
|
[type="button"]:-moz-focusring,
|
||||||
|
[type="reset"]:-moz-focusring,
|
||||||
|
[type="submit"]:-moz-focusring {
|
||||||
|
outline: 1px dotted ButtonText;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the border, margin, and padding in all browsers (opinionated).
|
||||||
|
*/
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
border: 1px solid #c0c0c0;
|
||||||
|
margin: 0 2px;
|
||||||
|
padding: 0.35em 0.625em 0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the text wrapping in Edge and IE.
|
||||||
|
* 2. Correct the color inheritance from `fieldset` elements in IE.
|
||||||
|
* 3. Remove the padding so developers are not caught out when they zero out
|
||||||
|
* `fieldset` elements in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
legend {
|
||||||
|
box-sizing: border-box; /* 1 */
|
||||||
|
color: inherit; /* 2 */
|
||||||
|
display: table; /* 1 */
|
||||||
|
max-width: 100%; /* 1 */
|
||||||
|
padding: 0; /* 3 */
|
||||||
|
white-space: normal; /* 1 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Add the correct display in IE 9-.
|
||||||
|
* 2. Add the correct vertical alignment in Chrome, Firefox, and Opera.
|
||||||
|
*/
|
||||||
|
|
||||||
|
progress {
|
||||||
|
display: inline-block; /* 1 */
|
||||||
|
vertical-align: baseline; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the default vertical scrollbar in IE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Add the correct box sizing in IE 10-.
|
||||||
|
* 2. Remove the padding in IE 10-.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type="checkbox"],
|
||||||
|
[type="radio"] {
|
||||||
|
box-sizing: border-box; /* 1 */
|
||||||
|
padding: 0; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct the cursor style of increment and decrement buttons in Chrome.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type="number"]::-webkit-inner-spin-button,
|
||||||
|
[type="number"]::-webkit-outer-spin-button {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the odd appearance in Chrome and Safari.
|
||||||
|
* 2. Correct the outline style in Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type="search"] {
|
||||||
|
-webkit-appearance: textfield; /* 1 */
|
||||||
|
outline-offset: -2px; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the inner padding and cancel buttons in Chrome and Safari on macOS.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type="search"]::-webkit-search-cancel-button,
|
||||||
|
[type="search"]::-webkit-search-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the inability to style clickable types in iOS and Safari.
|
||||||
|
* 2. Change font properties to `inherit` in Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
::-webkit-file-upload-button {
|
||||||
|
-webkit-appearance: button; /* 1 */
|
||||||
|
font: inherit; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Interactive
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Add the correct display in IE 9-.
|
||||||
|
* 1. Add the correct display in Edge, IE, and Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
details, /* 1 */
|
||||||
|
menu {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Add the correct display in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
summary {
|
||||||
|
display: list-item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scripting
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct display in IE 9-.
|
||||||
|
*/
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct display in IE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
template {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hidden
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct display in IE 10-.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
65
frontend/static/css/style.css
Normal file
65
frontend/static/css/style.css
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
overflow-y: scroll;
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: 'Roboto', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-custom {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-square {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
transition: opacity .3s ease;
|
||||||
|
z-index: 999;
|
||||||
|
position: fixed;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
margin: auto 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-new-user {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
background-color: #eaeaea;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-new-user-el-margin {
|
||||||
|
margin-left: 0.1rem;
|
||||||
|
margin-right: 0.1rem;
|
||||||
|
margin-top: 0.1rem;
|
||||||
|
margin-bottom: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-show-config {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: #eaeaea;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-show-config-txt-box {
|
||||||
|
/* width: 50rem; */
|
||||||
|
max-height: 30rem;
|
||||||
|
background-color: #ffffff;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
BIN
frontend/static/favicon.ico
Normal file
BIN
frontend/static/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 318 B |
64
frontend/static/index.html
Normal file
64
frontend/static/index.html
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>openvpn-admin</title>
|
||||||
|
<link rel="stylesheet" href="css/normalize.css">
|
||||||
|
<link rel="stylesheet" href="css/bootstrap.min.css">
|
||||||
|
<link rel="stylesheet" href="css/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app" @click.left.stop="u_ctx_hide">
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button type="button" class="btn btn-sm btn-success el-square" v-on:click.stop="u.modalNewUserVisible=true">Add user</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dropdown-menu dropdown-custom" :style="uCtxStyle" v-show="u.ctxVisible">
|
||||||
|
<button class="dropdown-item" type="button" :data-name="name" :data-text="text" @click.left.stop="u_ctx_click" v-for="text, name in u.ctxMenuItems">{{text}}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="table table-bordered table-hover">
|
||||||
|
<thead class="thead-dark">
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Name</th>
|
||||||
|
<th scope="col">Flag</th>
|
||||||
|
<th scope="col">Expiration date</th>
|
||||||
|
<th scope="col">Revocation date</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="row in u.data" :data-name="row.Identity" :style="row.connection_status" @contextmenu.prevent="u_ctx_show">
|
||||||
|
<td>{{ row.Identity }}</td>
|
||||||
|
<td>{{ row.Flag }}</td>
|
||||||
|
<td>{{ row.ExpirationDate }}</td>
|
||||||
|
<td>{{ row.RevocationDate }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="modal-wrapper" v-if="u.modalNewUserVisible">
|
||||||
|
<div class="modal-new-user">
|
||||||
|
<input type="text" class="form-control el-square modal-new-user-el-margin" placeholder="User name [_a-zA-Z0-9\.-]" v-model="u.newUserName">
|
||||||
|
<button type="button" class="btn btn-success el-square modal-new-user-el-margin" v-on:click.stop="create_user();u.modalNewUserVisible=false">Create</button>
|
||||||
|
<button type="button" class="btn btn-success el-square modal-new-user-el-margin" v-on:click.stop="u.newUserName='';u.modalNewUserVisible=false">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-wrapper" v-if="u.modalShowConfigVisible">
|
||||||
|
<div class="modal-show-config">
|
||||||
|
<div class="row">
|
||||||
|
<button type="button" class="btn btn-success el-square modal-new-user-el-margin" v-clipboard:copy="u.openvpnConfig">Copy</button>
|
||||||
|
<button type="button" class="btn btn-success el-square modal-new-user-el-margin" v-on:click.stop="u.openvpnConfig='';u.modalShowConfigVisible=false">Cancel</button>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<pre class="modal-show-config-txt-box modal-new-user-el-margin">{{ u.openvpnConfig }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<script src="dist/build.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
<!-- n['flag'], n['expiration_date'], n['revocation_date'], n['serial_number'], n['filename'], n['distinguished_name'] -->
|
108
frontend/webpack.config.js
Normal file
108
frontend/webpack.config.js
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
var path = require('path')
|
||||||
|
var webpack = require('webpack')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
entry: './src/main.js',
|
||||||
|
output: {
|
||||||
|
path: path.resolve(__dirname, './static/dist'),
|
||||||
|
publicPath: '/dist/',
|
||||||
|
filename: 'build.js'
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.css$/,
|
||||||
|
use: [
|
||||||
|
'vue-style-loader',
|
||||||
|
'css-loader'
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.scss$/,
|
||||||
|
use: [
|
||||||
|
'vue-style-loader',
|
||||||
|
'css-loader',
|
||||||
|
'sass-loader'
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.sass$/,
|
||||||
|
use: [
|
||||||
|
'vue-style-loader',
|
||||||
|
'css-loader',
|
||||||
|
'sass-loader?indentedSyntax'
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.vue$/,
|
||||||
|
loader: 'vue-loader',
|
||||||
|
options: {
|
||||||
|
loaders: {
|
||||||
|
// Since sass-loader (weirdly) has SCSS as its default parse mode, we map
|
||||||
|
// the "scss" and "sass" values for the lang attribute to the right configs here.
|
||||||
|
// other preprocessors should work out of the box, no loader config like this necessary.
|
||||||
|
'scss': [
|
||||||
|
'vue-style-loader',
|
||||||
|
'css-loader',
|
||||||
|
'sass-loader'
|
||||||
|
],
|
||||||
|
'sass': [
|
||||||
|
'vue-style-loader',
|
||||||
|
'css-loader',
|
||||||
|
'sass-loader?indentedSyntax'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
// other vue-loader options go here
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.js$/,
|
||||||
|
loader: 'babel-loader',
|
||||||
|
exclude: /node_modules/
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(png|jpg|gif|svg)$/,
|
||||||
|
loader: 'file-loader',
|
||||||
|
options: {
|
||||||
|
name: '[name].[ext]?[hash]'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'vue$': 'vue/dist/vue.esm.js'
|
||||||
|
},
|
||||||
|
extensions: ['*', '.js', '.vue', '.json']
|
||||||
|
},
|
||||||
|
devServer: {
|
||||||
|
historyApiFallback: true,
|
||||||
|
noInfo: true,
|
||||||
|
overlay: true
|
||||||
|
},
|
||||||
|
performance: {
|
||||||
|
hints: false
|
||||||
|
},
|
||||||
|
devtool: '#eval-source-map'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
module.exports.devtool = '#source-map'
|
||||||
|
// http://vue-loader.vuejs.org/en/workflow/production.html
|
||||||
|
module.exports.plugins = (module.exports.plugins || []).concat([
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
'process.env': {
|
||||||
|
NODE_ENV: '"production"'
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
new webpack.optimize.UglifyJsPlugin({
|
||||||
|
sourceMap: true,
|
||||||
|
compress: {
|
||||||
|
warnings: false
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
new webpack.LoaderOptionsPlugin({
|
||||||
|
minimize: true
|
||||||
|
})
|
||||||
|
])
|
||||||
|
}
|
20
get-easyrsa-end-gen-certs.sh
Executable file
20
get-easyrsa-end-gen-certs.sh
Executable file
|
@ -0,0 +1,20 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if [ ! -d easyrsa ]; then
|
||||||
|
mkdir easyrsa
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd easyrsa
|
||||||
|
|
||||||
|
if [ ! -f easyrsa ]; then
|
||||||
|
curl -sL https://github.com/OpenVPN/easy-rsa/releases/download/v3.0.6/EasyRSA-unix-v3.0.6.tgz | tar -xzv --strip-components=1 -C .
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -d pki ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
./easyrsa init-pki
|
||||||
|
echo "ca\n" | ./easyrsa build-ca nopass
|
||||||
|
./easyrsa build-server-full server nopass
|
||||||
|
./easyrsa build-client-full client nopass
|
3
go.mod
Normal file
3
go.mod
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
module openvpn-web-ui
|
||||||
|
|
||||||
|
go 1.14
|
341
main.go
Normal file
341
main.go
Normal file
|
@ -0,0 +1,341 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
// "reflect"
|
||||||
|
"bufio"
|
||||||
|
"net"
|
||||||
|
// "io"
|
||||||
|
// "encoding/binary"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
easyrsaPath = "easyrsa"
|
||||||
|
indexTxtPath = easyrsaPath + "/pki/index.txt"
|
||||||
|
usernameRegexp = `^([a-zA-Z0-9_.-])+$`
|
||||||
|
openvpnServerHost = "127.0.0.1"
|
||||||
|
openvpnServerPort = "7777"
|
||||||
|
listenHost = "127.0.0.1"
|
||||||
|
listenPort = "8080"
|
||||||
|
mgmtListenHost = "127.0.0.1"
|
||||||
|
mgmtListenPort = "7788"
|
||||||
|
)
|
||||||
|
|
||||||
|
type openvpnClientConfig struct {
|
||||||
|
Host string
|
||||||
|
Port string
|
||||||
|
CA string
|
||||||
|
Cert string
|
||||||
|
Key string
|
||||||
|
TLS string
|
||||||
|
}
|
||||||
|
|
||||||
|
type indexTxtLine struct {
|
||||||
|
Flag string
|
||||||
|
ExpirationDate string
|
||||||
|
RevocationDate string
|
||||||
|
SerialNumber string
|
||||||
|
Filename string
|
||||||
|
DistinguishedName string
|
||||||
|
Identity string
|
||||||
|
}
|
||||||
|
|
||||||
|
type clientStatus struct {
|
||||||
|
CommonName string
|
||||||
|
RealAddress string
|
||||||
|
BytesReceived string
|
||||||
|
BytesSent string
|
||||||
|
ConnectedSince string
|
||||||
|
VirtualAddress string
|
||||||
|
LastRef string
|
||||||
|
ConnectedSinceFormated string
|
||||||
|
LastRefFormated string
|
||||||
|
}
|
||||||
|
|
||||||
|
func userListHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userList, _ := json.Marshal(indexTxtParser(fRead(indexTxtPath)))
|
||||||
|
fmt.Fprintf(w, "%s", userList)
|
||||||
|
}
|
||||||
|
|
||||||
|
func userCreateHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.ParseForm()
|
||||||
|
fmt.Fprintf(w, "%s", userCreate(r.FormValue("username")))
|
||||||
|
}
|
||||||
|
|
||||||
|
func userRevokeHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.ParseForm()
|
||||||
|
fmt.Fprintf(w, "%s", userRevoke(r.FormValue("username")))
|
||||||
|
}
|
||||||
|
|
||||||
|
func userUnrevokeHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.ParseForm()
|
||||||
|
fmt.Fprintf(w, "%s", userUnrevoke(r.FormValue("username")))
|
||||||
|
}
|
||||||
|
|
||||||
|
func userShowConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.ParseForm()
|
||||||
|
fmt.Printf("username: %v\n%s\n", r.PostForm, r.FormValue("username"))
|
||||||
|
fmt.Fprintf(w, "%s", renderClientConfig(r.FormValue("username")))
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
fmt.Println("Bind: http://" + listenHost + ":" + listenPort)
|
||||||
|
// usersFromIndexTxt := indexTxtParser(fRead(indexTxtPath))
|
||||||
|
// fRead(indexTxtPath)
|
||||||
|
// fWrite("hello", "hi")
|
||||||
|
// renderIndexTxt(indexTxtParser(fRead(indexTxtPath)))
|
||||||
|
// renderClientConfig("asd")
|
||||||
|
// crlFix()
|
||||||
|
// fmt.Println(reflect.TypeOf(indexTxtConf))
|
||||||
|
// fmt.Print(userUnrevoke("asd"))
|
||||||
|
// renderIndexTxt(usersFromIndexTxt)
|
||||||
|
// x := getActiveClients()
|
||||||
|
// fmt.Printf("%#v", x)
|
||||||
|
// killUserConnection("x")
|
||||||
|
fs := http.FileServer(http.Dir("./frontend/static"))
|
||||||
|
|
||||||
|
http.Handle("/", fs)
|
||||||
|
http.HandleFunc("/api/users/list", userListHandler)
|
||||||
|
http.HandleFunc("/api/user/create", userCreateHandler)
|
||||||
|
http.HandleFunc("/api/user/revoke", userRevokeHandler)
|
||||||
|
http.HandleFunc("/api/user/unrevoke", userUnrevokeHandler)
|
||||||
|
http.HandleFunc("/api/user/showconfig", userShowConfigHandler)
|
||||||
|
log.Fatal(http.ListenAndServe(listenHost+":"+listenPort, nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func fRead(path string) string {
|
||||||
|
content, err := ioutil.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fWrite(path, content string) {
|
||||||
|
err := ioutil.WriteFile(path, []byte(content), 0644)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func indexTxtParser(txt string) []indexTxtLine {
|
||||||
|
indexTxt := []indexTxtLine{}
|
||||||
|
|
||||||
|
txtLinesArray := strings.Split(txt, "\n")
|
||||||
|
|
||||||
|
for _, v := range txtLinesArray {
|
||||||
|
str := strings.Fields(v)
|
||||||
|
if len(str) > 0 {
|
||||||
|
switch {
|
||||||
|
// case strings.HasPrefix(str[0], "E"):
|
||||||
|
case strings.HasPrefix(str[0], "V"):
|
||||||
|
indexTxt = append(indexTxt, indexTxtLine{Flag: str[0], ExpirationDate: str[1], SerialNumber: str[2], Filename: str[3], DistinguishedName: str[4], Identity: str[4][strings.Index(str[4], "=")+1:]})
|
||||||
|
case strings.HasPrefix(str[0], "R"):
|
||||||
|
indexTxt = append(indexTxt, indexTxtLine{Flag: str[0], ExpirationDate: str[1], RevocationDate: str[2], SerialNumber: str[3], Filename: str[4], DistinguishedName: str[5], Identity: str[5][strings.Index(str[5], "=")+1:]})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return indexTxt
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderIndexTxt(data []indexTxtLine) string {
|
||||||
|
indexTxt := ""
|
||||||
|
for _, line := range data {
|
||||||
|
switch {
|
||||||
|
case line.Flag == "V":
|
||||||
|
// if line.distinguishedName != "/CN=server" {
|
||||||
|
// fmt.Printf("%s\t%s\t\t%s\t%s\t%s\n", line.flag, line.expirationDate, line.serialNumber, line.filename, line.distinguishedName)
|
||||||
|
indexTxt += fmt.Sprintf("%s\t%s\t\t%s\t%s\t%s\n", line.Flag, line.ExpirationDate, line.SerialNumber, line.Filename, line.DistinguishedName)
|
||||||
|
// }
|
||||||
|
case line.Flag == "R":
|
||||||
|
// if line.distinguishedName != "/CN=server" {
|
||||||
|
// fmt.Printf("%s\t%s\t%s\t%s\t%s\t%s\n", line.flag, line.expirationDate, line.revocationDate, line.serialNumber, line.filename, line.distinguishedName)
|
||||||
|
indexTxt += fmt.Sprintf("%s\t%s\t%s\t%s\t%s\t%s\n", line.Flag, line.ExpirationDate, line.RevocationDate, line.SerialNumber, line.Filename, line.DistinguishedName)
|
||||||
|
// }
|
||||||
|
// case line.flag == "E":
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (indexTxt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderClientConfig(username string) string {
|
||||||
|
if checkUserExist(username) {
|
||||||
|
conf := openvpnClientConfig{}
|
||||||
|
conf.Host = openvpnServerHost
|
||||||
|
conf.Port = openvpnServerPort
|
||||||
|
conf.CA = fRead(easyrsaPath + "/pki/ca.crt")
|
||||||
|
conf.Cert = fRead(easyrsaPath + "/pki/issued/" + username + ".crt")
|
||||||
|
conf.Key = fRead(easyrsaPath + "/pki/private/" + username + ".key")
|
||||||
|
conf.TLS = fRead(easyrsaPath + "/pki/ta.key")
|
||||||
|
t, _ := template.ParseFiles("client.conf.tpl")
|
||||||
|
var tmp bytes.Buffer
|
||||||
|
t.Execute(&tmp, conf)
|
||||||
|
// fmt.Printf("%+v\n", err)
|
||||||
|
fmt.Printf("%+v\n", tmp.String())
|
||||||
|
return (fmt.Sprintf("%+v\n", tmp.String()))
|
||||||
|
}
|
||||||
|
fmt.Printf("User \"%s\" not found", username)
|
||||||
|
return (fmt.Sprintf("User \"%s\" not found", username))
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://community.openvpn.net/openvpn/ticket/623
|
||||||
|
func crlFix() {
|
||||||
|
os.Chmod(easyrsaPath+"/pki", 0755)
|
||||||
|
err := os.Chmod(easyrsaPath+"/pki/crl.pem", 0640)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runBash(script string) string {
|
||||||
|
fmt.Println(script)
|
||||||
|
cmd := exec.Command("bash", "-c", script)
|
||||||
|
stdout, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return (fmt.Sprint(err) + " : " + string(stdout))
|
||||||
|
}
|
||||||
|
return (string(stdout))
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateUsername(username string) bool {
|
||||||
|
var validUsername = regexp.MustCompile(usernameRegexp)
|
||||||
|
return (validUsername.MatchString(username))
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkUserExist(username string) bool {
|
||||||
|
for _, u := range indexTxtParser(fRead(indexTxtPath)) {
|
||||||
|
if u.DistinguishedName == ("/CN=" + username) {
|
||||||
|
return (true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func usersList() []string {
|
||||||
|
users := []string{}
|
||||||
|
for _, line := range indexTxtParser(fRead(indexTxtPath)) {
|
||||||
|
users = append(users, line.Identity)
|
||||||
|
}
|
||||||
|
return (users)
|
||||||
|
}
|
||||||
|
|
||||||
|
func userCreate(username string) string {
|
||||||
|
if validateUsername(username) == false {
|
||||||
|
fmt.Printf("Username \"%s\" incorrect, you can use only %s\n", username, usernameRegexp)
|
||||||
|
return (fmt.Sprintf("Username \"%s\" incorrect, you can use only %s\n", username, usernameRegexp))
|
||||||
|
}
|
||||||
|
if checkUserExist(username) {
|
||||||
|
fmt.Printf("User \"%s\" already exists\n", username)
|
||||||
|
return (fmt.Sprintf("User \"%s\" already exists\n", username))
|
||||||
|
}
|
||||||
|
o := runBash(fmt.Sprintf("date +%%Y-%%m-%%d\\ %%H:%%M:%%S && cd %s && ./easyrsa build-client-full %s nopass", easyrsaPath, username))
|
||||||
|
fmt.Println(o)
|
||||||
|
return ("")
|
||||||
|
}
|
||||||
|
|
||||||
|
func userRevoke(username string) string {
|
||||||
|
if checkUserExist(username) {
|
||||||
|
// check certificate valid flag 'V'
|
||||||
|
o := runBash(fmt.Sprintf("date +%%Y-%%m-%%d\\ %%H:%%M:%%S && cd %s && echo yes | ./easyrsa revoke %s && ./easyrsa gen-crl", easyrsaPath, username))
|
||||||
|
crlFix()
|
||||||
|
return (fmt.Sprintln(o))
|
||||||
|
}
|
||||||
|
fmt.Printf("User \"%s\" not found", username)
|
||||||
|
return (fmt.Sprintf("User \"%s\" not found", username))
|
||||||
|
}
|
||||||
|
|
||||||
|
func userUnrevoke(username string) string {
|
||||||
|
if checkUserExist(username) {
|
||||||
|
// check certificate revoked flag 'R'
|
||||||
|
usersFromIndexTxt := indexTxtParser(fRead(indexTxtPath))
|
||||||
|
for i := range usersFromIndexTxt {
|
||||||
|
if usersFromIndexTxt[i].DistinguishedName == ("/CN=" + username) {
|
||||||
|
usersFromIndexTxt[i].Flag = "V"
|
||||||
|
usersFromIndexTxt[i].RevocationDate = ""
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fWrite(indexTxtPath, renderIndexTxt(usersFromIndexTxt))
|
||||||
|
fmt.Print(renderIndexTxt(usersFromIndexTxt))
|
||||||
|
crlFix()
|
||||||
|
return (fmt.Sprintf("{\"msg\":\"User %s successfully unrevoked\"}", username))
|
||||||
|
}
|
||||||
|
return (fmt.Sprintf("{\"msg\":\"User \"%s\" not found\"}", username))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ovpnMgmtRead(conn net.Conn) string {
|
||||||
|
buf := make([]byte, 32768)
|
||||||
|
len, _ := conn.Read(buf)
|
||||||
|
s := string(buf[:len])
|
||||||
|
return (s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mgmtConnectedUsersParser(text string) []clientStatus {
|
||||||
|
u := []clientStatus{}
|
||||||
|
isClientList := false
|
||||||
|
isRouteTable := false
|
||||||
|
scanner := bufio.NewScanner(strings.NewReader(text))
|
||||||
|
for scanner.Scan() {
|
||||||
|
txt := scanner.Text()
|
||||||
|
if regexp.MustCompile(`^Common Name,Real Address,Bytes Received,Bytes Sent,Connected Since$`).MatchString(txt) {
|
||||||
|
isClientList = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if regexp.MustCompile(`^ROUTING TABLE$`).MatchString(txt) {
|
||||||
|
isClientList = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if regexp.MustCompile(`^Virtual Address,Common Name,Real Address,Last Ref$`).MatchString(txt) {
|
||||||
|
isRouteTable = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if regexp.MustCompile(`^GLOBAL STATS$`).MatchString(txt) {
|
||||||
|
// isRouteTable = false // ineffectual assignment to isRouteTable (ineffassign)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if isClientList {
|
||||||
|
user := strings.Split(txt, ",")
|
||||||
|
u = append(u, clientStatus{CommonName: user[0], RealAddress: user[1], BytesReceived: user[2], BytesSent: user[3], ConnectedSince: user[4]})
|
||||||
|
}
|
||||||
|
if isRouteTable {
|
||||||
|
user := strings.Split(txt, ",")
|
||||||
|
for i := range u {
|
||||||
|
if u[i].CommonName == user[1] {
|
||||||
|
u[i].VirtualAddress = user[0]
|
||||||
|
u[i].LastRef = user[3]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (u)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mgmtKillUserConnection(username string) {
|
||||||
|
conn, _ := net.Dial("tcp", mgmtListenHost+":"+mgmtListenPort)
|
||||||
|
ovpnMgmtRead(conn) // read welcome message
|
||||||
|
conn.Write([]byte(fmt.Sprintf("kill %s\n", username)))
|
||||||
|
fmt.Printf("%v", ovpnMgmtRead(conn))
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func mgmtGetActiveClients() []clientStatus {
|
||||||
|
conn, _ := net.Dial("tcp", mgmtListenHost+":"+mgmtListenPort)
|
||||||
|
ovpnMgmtRead(conn) // read welcome message
|
||||||
|
conn.Write([]byte("status\n"))
|
||||||
|
activeClients := mgmtConnectedUsersParser(ovpnMgmtRead(conn))
|
||||||
|
conn.Close()
|
||||||
|
return (activeClients)
|
||||||
|
}
|
Loading…
Reference in a new issue