This commit is contained in:
Vitaliy Snurnitsin 2020-05-15 02:13:33 +03:00
parent 0b2512fc15
commit 9f4c4e2c5c
21 changed files with 9715 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
easyrsa
openvpn-web-ui

3
build.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
go build .

26
client.conf.tpl Normal file
View 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
View File

@ -0,0 +1,6 @@
{
"presets": [
["env", { "modules": false }],
"stage-3"
]
}

9
frontend/.editorconfig Normal file
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

37
frontend/package.json Normal file
View 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
View 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

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

461
frontend/static/css/normalize.css vendored Normal file
View 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;
}

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

View 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
View 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
View 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
View File

@ -0,0 +1,3 @@
module openvpn-web-ui
go 1.14

341
main.go Normal file
View 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)
}