New design; New features
This commit is contained in:
parent
6ce657d587
commit
6e9a553884
18 changed files with 4303 additions and 3604 deletions
|
@ -5,5 +5,10 @@ out
|
||||||
gen
|
gen
|
||||||
|
|
||||||
|
|
||||||
./easyrsa
|
easyrsa
|
||||||
|
ccd
|
||||||
werf.yaml
|
werf.yaml
|
||||||
|
frontend/node_modules
|
||||||
|
openvpn-web-ui
|
||||||
|
openvpn-ui
|
||||||
|
openvpn-admin
|
||||||
|
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1,2 +1,6 @@
|
||||||
easyrsa
|
easyrsa
|
||||||
|
ccd
|
||||||
openvpn-web-ui
|
openvpn-web-ui
|
||||||
|
openvpn-ui
|
||||||
|
openvpn-admin
|
||||||
|
frontend/node_modules
|
|
@ -1,14 +1,14 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
set -x
|
||||||
EASY_RSA_LOC="/etc/openvpn/easyrsa"
|
EASY_RSA_LOC="/etc/openvpn/easyrsa"
|
||||||
SERVER_CERT="${EASY_RSA_LOC}/pki/issued/server.crt"
|
SERVER_CERT="${EASY_RSA_LOC}/pki/issued/server.crt"
|
||||||
cd $EASY_RSA_LOC
|
cd $EASY_RSA_LOC
|
||||||
if [ -e "$SERVER_CERT" ]; then
|
if [ -e "$SERVER_CERT" ]; then
|
||||||
echo "found existing certs - reusing"
|
echo "found existing certs - reusing"
|
||||||
else
|
else
|
||||||
cp -R /usr/share/easy-rsa/* $EASY_RSA_LOC
|
|
||||||
easyrsa init-pki
|
easyrsa init-pki
|
||||||
echo "ca\n" | easyrsa build-ca nopass
|
cp -R /usr/share/easy-rsa/* $EASY_RSA_LOC/pki
|
||||||
|
echo "ca" | easyrsa build-ca nopass
|
||||||
easyrsa build-server-full server nopass
|
easyrsa build-server-full server nopass
|
||||||
easyrsa gen-dh
|
easyrsa gen-dh
|
||||||
openvpn --genkey --secret ./pki/ta.key
|
openvpn --genkey --secret ./pki/ta.key
|
||||||
|
@ -27,5 +27,7 @@ cp -f /etc/openvpn/setup/openvpn.conf /etc/openvpn/openvpn.conf
|
||||||
[ -d /etc/openvpn/certs/pki ] && chmod 755 /etc/openvpn/certs/pki
|
[ -d /etc/openvpn/certs/pki ] && chmod 755 /etc/openvpn/certs/pki
|
||||||
[ -f /etc/openvpn/certs/pki/crl.pem ] && chmod 644 /etc/openvpn/certs/pki/crl.pem
|
[ -f /etc/openvpn/certs/pki/crl.pem ] && chmod 644 /etc/openvpn/certs/pki/crl.pem
|
||||||
|
|
||||||
|
mkdir -p /etc/openvpn/ccd
|
||||||
|
|
||||||
openvpn --config /etc/openvpn/openvpn.conf --client-config-dir /etc/openvpn/ccd
|
openvpn --config /etc/openvpn/openvpn.conf --client-config-dir /etc/openvpn/ccd
|
||||||
|
|
||||||
|
|
25
.werffiles/openvpn.conf
Normal file
25
.werffiles/openvpn.conf
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
server 172.16.100.0 255.255.255.0
|
||||||
|
verb 3
|
||||||
|
tls-server
|
||||||
|
ca /etc/openvpn/easyrsa/pki/ca.crt
|
||||||
|
key /etc/openvpn/easyrsa/pki/private/server.key
|
||||||
|
cert /etc/openvpn/easyrsa/pki/issued/server.crt
|
||||||
|
dh /etc/openvpn/easyrsa/pki/dh.pem
|
||||||
|
crl-verify /etc/openvpn/easyrsa/pki/crl.pem
|
||||||
|
tls-auth /etc/openvpn/easyrsa/pki/ta.key
|
||||||
|
key-direction 0
|
||||||
|
cipher AES-128-CBC
|
||||||
|
management 127.0.0.1 8989
|
||||||
|
keepalive 10 60
|
||||||
|
persist-key
|
||||||
|
persist-tun
|
||||||
|
topology subnet
|
||||||
|
proto tcp
|
||||||
|
port 1194
|
||||||
|
dev tun0
|
||||||
|
status /tmp/openvpn-status.log
|
||||||
|
user nobody
|
||||||
|
group nogroup
|
||||||
|
push "topology subnet"
|
||||||
|
push "route-metric 9999"
|
||||||
|
push "dhcp-option DNS 1.1.1.1"
|
|
@ -1,7 +1,7 @@
|
||||||
FROM golang:1.14.2-alpine3.11 AS backend-builder
|
FROM golang:1.14.2-alpine3.11 AS backend-builder
|
||||||
COPY . /app
|
COPY . /app
|
||||||
RUN apk --no-cache add build-base git gcc
|
#RUN apk --no-cache add build-base git gcc
|
||||||
RUN cd /app && env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags='-extldflags "-static" -s -w' -o openvpn-ui
|
RUN cd /app && env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags='-extldflags "-static" -s -w' -o openvpn-admin
|
||||||
|
|
||||||
FROM node:14.2-alpine3.11 AS frontend-builder
|
FROM node:14.2-alpine3.11 AS frontend-builder
|
||||||
COPY frontend/ /app
|
COPY frontend/ /app
|
||||||
|
@ -9,8 +9,10 @@ RUN cd /app && npm install && npm run build
|
||||||
|
|
||||||
FROM alpine:3.11
|
FROM alpine:3.11
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=backend-builder /app/openvpn-ui /app
|
COPY --from=backend-builder /app/openvpn-admin /app
|
||||||
COPY --from=frontend-builder /app/static /app/static
|
COPY --from=frontend-builder /app/static /app/static
|
||||||
|
COPY client.conf.tpl /app/client.conf.tpl
|
||||||
|
COPY ccd.tpl /app/ccd.tpl
|
||||||
RUN echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing/" >> /etc/apk/repositories && \
|
RUN echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing/" >> /etc/apk/repositories && \
|
||||||
apk add --update bash easy-rsa && \
|
apk add --update bash easy-rsa && \
|
||||||
ln -s /usr/share/easy-rsa/easyrsa /usr/local/bin && \
|
ln -s /usr/share/easy-rsa/easyrsa /usr/local/bin && \
|
||||||
|
|
7
Dockerfile.openvpn
Normal file
7
Dockerfile.openvpn
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
FROM alpine:3.11
|
||||||
|
RUN echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing/" >> /etc/apk/repositories && \
|
||||||
|
apk add --update bash openvpn easy-rsa && \
|
||||||
|
ln -s /usr/share/easy-rsa/easyrsa /usr/local/bin && \
|
||||||
|
rm -rf /tmp/* /var/tmp/* /var/cache/apk/* /var/cache/distfiles/*
|
||||||
|
COPY .werffiles /etc/openvpn/setup
|
||||||
|
RUN chmod +x /etc/openvpn/setup/configure.sh
|
2
build.sh
2
build.sh
|
@ -1,3 +1,3 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
go build .
|
env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags='-extldflags "-static" -s -w' -o openvpn-admin
|
||||||
|
|
6
ccd.tpl
Normal file
6
ccd.tpl
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{{- if (ne .ClientAddress "dynamic") }}
|
||||||
|
ifconfig-push {{ .ClientAddress }} 255.255.255.255
|
||||||
|
{{- end }}
|
||||||
|
{{- range $route := .CustomRoutes }}
|
||||||
|
push "route {{ $route.Address }} {{ $route.Mask }}" # {{ $route.Description }}
|
||||||
|
{{- end }}
|
|
@ -1,23 +1,26 @@
|
||||||
version: '3'
|
version: '3'
|
||||||
|
|
||||||
volumes:
|
|
||||||
ovpn_data:
|
|
||||||
ovpn_config:
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
openvpn:
|
openvpn:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.openvpn
|
||||||
image: openvpn:local
|
image: openvpn:local
|
||||||
command: /etc/openvpn/setup/configure.sh
|
command: /etc/openvpn/setup/configure.sh
|
||||||
|
cap_add:
|
||||||
|
- NET_ADMIN
|
||||||
ports:
|
ports:
|
||||||
- 1194:1194
|
- 1194:1194
|
||||||
volumes:
|
volumes:
|
||||||
- ovpn_data:/etc/openvpn/easyrsa
|
- ./easyrsa:/etc/openvpn/easyrsa
|
||||||
|
- ./ccd:/etc/openvpn/ccd
|
||||||
openvpn-admin:
|
openvpn-admin:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
image: openvpn-admin:local
|
image: openvpn-admin:local
|
||||||
command: /app/openvpn-ui
|
command: /app/openvpn-admin --debug --ovpn.network="172.16.100.0/22" --mgmt.host="openvpn"
|
||||||
ports:
|
ports:
|
||||||
- 8080:8080
|
- 8080:8080
|
||||||
volumes:
|
volumes:
|
||||||
- ovpn_data:/mnt/easyrsa
|
- ./easyrsa:/mnt/easyrsa
|
||||||
|
- ./ccd:/mnt/ccd
|
|
@ -1,6 +1,16 @@
|
||||||
{
|
{
|
||||||
"presets": [
|
"presets": [
|
||||||
["env", { "modules": false }],
|
[
|
||||||
"stage-3"
|
"@babel/preset-env",
|
||||||
|
{
|
||||||
|
"modules": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
"@babel/plugin-syntax-dynamic-import",
|
||||||
|
"@babel/plugin-syntax-import-meta",
|
||||||
|
"@babel/plugin-proposal-class-properties",
|
||||||
|
"@babel/plugin-proposal-json-strings"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
6723
frontend/package-lock.json
generated
6723
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "openvpn-easyrsa-web-ui",
|
"name": "openvpn-admin",
|
||||||
"description": "A Vue.js project",
|
"description": "Vue.js admin ui for openvpn and easyrsa",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1a",
|
||||||
"author": "vitaliy.snurnitsin@gmail.com",
|
"author": "vitaliy.snurnitsin@gmail.com",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
@ -10,9 +10,10 @@
|
||||||
"build": "cross-env NODE_ENV=production webpack --progress --hide-modules"
|
"build": "cross-env NODE_ENV=production webpack --progress --hide-modules"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.18.0",
|
"axios": "^0.19.2",
|
||||||
"vue": "^2.5.17",
|
"vue": "^2.6.12",
|
||||||
"vue-clipboard2": "^0.2.1"
|
"vue-clipboard2": "^0.2.1",
|
||||||
|
"vue-good-table": "^2.21.1"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"> 1%",
|
"> 1%",
|
||||||
|
@ -20,18 +21,23 @@
|
||||||
"not ie <= 8"
|
"not ie <= 8"
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-core": "^6.26.3",
|
"@babel/core": "^7.8.6",
|
||||||
"babel-loader": "^7.1.5",
|
"@babel/plugin-proposal-class-properties": "^7.0.0",
|
||||||
"babel-preset-env": "^1.7.0",
|
"@babel/plugin-proposal-json-strings": "^7.0.0",
|
||||||
"babel-preset-stage-3": "^6.24.1",
|
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
|
||||||
"cross-env": "^5.2.0",
|
"@babel/plugin-syntax-import-meta": "^7.0.0",
|
||||||
"css-loader": "^0.28.7",
|
"@babel/preset-env": "^7.0.0",
|
||||||
"file-loader": "^1.1.4",
|
"babel-loader": "^8.0.0",
|
||||||
"node-sass": "^4.9.3",
|
"cross-env": "^7.0.0",
|
||||||
"sass-loader": "^6.0.6",
|
"css-loader": "^3.4.2",
|
||||||
"vue-loader": "^13.7.3",
|
"file-loader": "^5.1.0",
|
||||||
"vue-template-compiler": "^2.5.17",
|
"node-sass": "^4.13.1",
|
||||||
"webpack": "^3.12.0",
|
"sass-loader": "^8.0.2",
|
||||||
"webpack-dev-server": "^2.11.3"
|
"terser-webpack-plugin": "^2.3.5",
|
||||||
|
"vue-loader": "^15.9.0",
|
||||||
|
"vue-template-compiler": "^2.6.11",
|
||||||
|
"webpack": "^4.42.0",
|
||||||
|
"webpack-cli": "^3.3.11",
|
||||||
|
"webpack-dev-server": "^3.10.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import VueClipboard from 'vue-clipboard2'
|
import VueClipboard from 'vue-clipboard2'
|
||||||
|
import VueGoodTablePlugin from 'vue-good-table'
|
||||||
|
|
||||||
|
import 'vue-good-table/dist/vue-good-table.css'
|
||||||
|
|
||||||
Vue.use(VueClipboard)
|
Vue.use(VueClipboard)
|
||||||
|
Vue.use(VueGoodTablePlugin)
|
||||||
|
|
||||||
var axios_cfg = function(url, data='', type='form') {
|
var axios_cfg = function(url, data='', type='form') {
|
||||||
if (data == '') {
|
if (data == '') {
|
||||||
|
@ -24,132 +28,238 @@ var axios_cfg = function(url, data='', type='form') {
|
||||||
data: data,
|
data: data,
|
||||||
headers: { 'Content-Type': 'multipart/form-data' }
|
headers: { 'Content-Type': 'multipart/form-data' }
|
||||||
};
|
};
|
||||||
|
} else if (type == 'json') {
|
||||||
|
return {
|
||||||
|
method: 'post',
|
||||||
|
url: url,
|
||||||
|
data: data,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
new Vue({
|
new Vue({
|
||||||
el: '#app',
|
el: '#app',
|
||||||
data: {
|
data: {
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
label: 'Name',
|
||||||
|
field: 'Identity',
|
||||||
|
// filterable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Account Status',
|
||||||
|
field: 'AccountStatus',
|
||||||
|
filterable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Expiration Date',
|
||||||
|
field: 'ExpirationDate',
|
||||||
|
type: 'date',
|
||||||
|
dateInputFormat: 'yyyy-MM-dd HH:mm:ss',
|
||||||
|
dateOutputFormat: 'yyyy-MM-dd HH:mm:ss',
|
||||||
|
formatFn: function (value) {
|
||||||
|
return value != "" ? value : ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Revocation Date',
|
||||||
|
field: 'RevocationDate',
|
||||||
|
type: 'date',
|
||||||
|
dateInputFormat: 'yyyy-MM-dd HH:mm:ss',
|
||||||
|
dateOutputFormat: 'yyyy-MM-dd HH:mm:ss',
|
||||||
|
formatFn: function (value) {
|
||||||
|
return value != "" ? value : ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Actions',
|
||||||
|
field: 'actions',
|
||||||
|
sortable: false,
|
||||||
|
tdClass: 'text-right',
|
||||||
|
globalSearchDisabled: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
rows: [],
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
name: 'u-revoke',
|
||||||
|
label: 'Revoke',
|
||||||
|
showWhenStatus: 'Active'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'u-unrevoke',
|
||||||
|
label: 'Unrevoke',
|
||||||
|
showWhenStatus: 'Revoked'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'u-show-config',
|
||||||
|
label: 'Show config',
|
||||||
|
showWhenStatus: 'Active'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'u-download-config',
|
||||||
|
label: 'Download config',
|
||||||
|
showWhenStatus: 'Active'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'u-edit-ccd',
|
||||||
|
label: 'Edit routes',
|
||||||
|
showWhenStatus: 'Active'
|
||||||
|
}
|
||||||
|
],
|
||||||
u: {
|
u: {
|
||||||
ctxTop: '0',
|
|
||||||
ctxLeft: '0',
|
|
||||||
ctxVisible: false,
|
|
||||||
ctxMenuItems: { 'u-revoke': 'Revoke', 'u-unrevoke': 'Unrevoke', 'u-show-config': 'Show config', 'u-edit-ccd': "Edit routes"},
|
|
||||||
columns: [],
|
|
||||||
data: {},
|
|
||||||
name: '',
|
|
||||||
newUserName: '',
|
newUserName: '',
|
||||||
|
// newUserPassword: 'nopass',
|
||||||
|
newUserCreateError: '',
|
||||||
modalNewUserVisible: false,
|
modalNewUserVisible: false,
|
||||||
modalShowConfigVisible: false,
|
modalShowConfigVisible: false,
|
||||||
openvpnConfig: ''
|
modalShowCcdVisible: false,
|
||||||
|
openvpnConfig: '',
|
||||||
|
ccd: {
|
||||||
|
Name: '',
|
||||||
|
ClientAddress: '',
|
||||||
|
CustomRoutes: []
|
||||||
|
},
|
||||||
|
newRoute: {},
|
||||||
|
ccdApplyStatus: "",
|
||||||
|
ccdApplyStatusMessage: "",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
u: function () {
|
// u: function () {
|
||||||
this.u.columns = Object.keys(this.u.data[0]) //.reverse()
|
// this.u.columns = Object.keys(this.u.data[0]) //.reverse()
|
||||||
}
|
// }
|
||||||
},
|
},
|
||||||
mounted: function () {
|
mounted: function () {
|
||||||
this.u_get_data()
|
this.u_get_data()
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
var _this = this
|
var _this = this
|
||||||
this.$root.$on('u-revoke', function (msg) {
|
_this.$root.$on('u-revoke', function (msg) {
|
||||||
var data = new URLSearchParams();
|
var data = new URLSearchParams();
|
||||||
data.append('username', _this.u.name);
|
data.append('username', _this.username);
|
||||||
axios.request(axios_cfg('api/user/revoke', data, 'form'))
|
axios.request(axios_cfg('api/user/revoke', data, 'form'))
|
||||||
.then(function(response) {
|
.then(function(response) {
|
||||||
console.log(response.data);
|
|
||||||
_this.u_get_data();
|
_this.u_get_data();
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
this.$root.$on('u-unrevoke', function () {
|
_this.$root.$on('u-unrevoke', function () {
|
||||||
var data = new URLSearchParams();
|
var data = new URLSearchParams();
|
||||||
data.append('username', _this.u.name);
|
data.append('username', _this.username);
|
||||||
axios.request(axios_cfg('api/user/unrevoke', data, 'form'))
|
axios.request(axios_cfg('api/user/unrevoke', data, 'form'))
|
||||||
.then(function(response) {
|
.then(function(response) {
|
||||||
console.log(response.data);
|
|
||||||
_this.u_get_data();
|
_this.u_get_data();
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
this.$root.$on('u-show-config', function () {
|
_this.$root.$on('u-show-config', function () {
|
||||||
this.u.modalShowConfigVisible = true;
|
_this.u.modalShowConfigVisible = true;
|
||||||
var data = new URLSearchParams();
|
var data = new URLSearchParams();
|
||||||
data.append('username', _this.u.name);
|
data.append('username', _this.username);
|
||||||
axios.request(axios_cfg('api/user/showconfig', data, 'form'))
|
axios.request(axios_cfg('api/user/config/show', data, 'form'))
|
||||||
.then(function(response) {
|
.then(function(response) {
|
||||||
_this.u.openvpnConfig = response.data;
|
_this.u.openvpnConfig = response.data;
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
this.$root.$on('u-edit-ccd', function () {
|
_this.$root.$on('u-download-config', function () {
|
||||||
this.u.modalShowCcdVisible = true;
|
|
||||||
var data = new URLSearchParams();
|
var data = new URLSearchParams();
|
||||||
data.append('username', _this.u.name);
|
data.append('username', _this.username);
|
||||||
axios.request(axios_cfg('api/user/ccd/list', data, 'form'))
|
axios.request(axios_cfg('api/user/config/show', data, 'form'))
|
||||||
.then(function(response) {
|
.then(function(response) {
|
||||||
_this.u.ccds = response.data;
|
const blob = new Blob([response.data], { type: 'text/plain' })
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = URL.createObjectURL(blob)
|
||||||
|
link.download = _this.username + ".ovpn"
|
||||||
|
link.click()
|
||||||
|
URL.revokeObjectURL(link.href)
|
||||||
|
}).catch(console.error);
|
||||||
|
})
|
||||||
|
_this.$root.$on('u-edit-ccd', function () {
|
||||||
|
_this.u.modalShowCcdVisible = true;
|
||||||
|
var data = new URLSearchParams();
|
||||||
|
data.append('username', _this.username);
|
||||||
|
axios.request(axios_cfg('api/user/ccd', data, 'form'))
|
||||||
|
.then(function(response) {
|
||||||
|
_this.u.ccd = response.data;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
_this.$root.$on('u-disconnect-use', function () {
|
||||||
|
_this.u.modalShowCcdVisible = true;
|
||||||
|
var data = new URLSearchParams();
|
||||||
|
data.append('username', _this.username);
|
||||||
|
axios.request(axios_cfg('api/user/disconnect', data, 'form'))
|
||||||
|
.then(function(response) {
|
||||||
|
_this.u.ccd = response.data;
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
uCtxStyle: function () {
|
customAddressEnabled: function () {
|
||||||
return {
|
return this.u.ccd.ClientAddress == "dynamic"
|
||||||
'top': this.u.ctxTop + 'px',
|
},
|
||||||
'left': this.u.ctxLeft + 'px'
|
ccdApplyStatusCssClass: function () {
|
||||||
}
|
return this.u.ccdApplyStatus == 200 ? "alert-success" : "alert-danger"
|
||||||
}
|
},
|
||||||
|
modalNewUserDisplay: function () {
|
||||||
|
return this.u.modalNewUserVisible ? {display: 'flex'} : {}
|
||||||
|
},
|
||||||
|
modalShowConfigDisplay: function () {
|
||||||
|
return this.u.modalShowConfigVisible ? {display: 'flex'} : {}
|
||||||
|
},
|
||||||
|
modalShowCcdDisplay: function () {
|
||||||
|
return this.u.modalShowCcdVisible ? {display: 'flex'} : {}
|
||||||
|
},
|
||||||
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
copyTextArea: function (e) {
|
rowStyleClassFn: function(row) {
|
||||||
e.clipboardData.setData("text/plain", this.u.openvpnConfig);
|
return row.ConnectionStatus == '' ? '' : 'active-row';
|
||||||
},
|
},
|
||||||
u_ctx_click: function (e) {
|
rowActionFn: function(e) {
|
||||||
this.$root.$emit(e.target.dataset.name)
|
this.username = e.target.dataset.username;
|
||||||
this.u_ctx_hide()
|
this.$root.$emit(e.target.dataset.name);
|
||||||
},
|
|
||||||
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() {
|
u_get_data: function() {
|
||||||
var _this = this;
|
var _this = this;
|
||||||
axios.request(axios_cfg('api/users/list'))
|
axios.request(axios_cfg('api/users/list'))
|
||||||
.then(function(response) {
|
.then(function(response) {
|
||||||
_this.u.data = response.data
|
_this.rows = response.data;
|
||||||
});
|
|
||||||
},
|
|
||||||
u_get_ccd: function() {
|
|
||||||
var _this = this;
|
|
||||||
axios.request(axios_cfg('api/user/ccd'))
|
|
||||||
.then(function(response) {
|
|
||||||
_this.u.data = response.data
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
create_user: function() {
|
create_user: function() {
|
||||||
var _this = this;
|
var _this = this;
|
||||||
|
|
||||||
|
_this.u.newUserCreateError = "";
|
||||||
|
|
||||||
var data = new URLSearchParams();
|
var data = new URLSearchParams();
|
||||||
data.append('username', this.u.newUserName);
|
data.append('username', _this.u.newUserName);
|
||||||
|
// data.append('password', this.u.newUserPassword);
|
||||||
|
|
||||||
axios.request(axios_cfg('api/user/create', data, 'form'))
|
axios.request(axios_cfg('api/user/create', data, 'form'))
|
||||||
.then(function(response) {
|
.then(function(response) {
|
||||||
console.log(response.data);
|
|
||||||
_this.u_get_data();
|
_this.u_get_data();
|
||||||
|
_this.u.modalNewUserVisible = false;
|
||||||
_this.u.newUserName = '';
|
_this.u.newUserName = '';
|
||||||
|
// _this.u.newUserPassword = 'nopass';
|
||||||
|
})
|
||||||
|
.catch(function(error) {
|
||||||
|
_this.u.newUserCreateError = error.response.data;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
ccd_apply: function() {
|
ccd_apply: function() {
|
||||||
var _this = this;
|
var _this = this;
|
||||||
var data = new URLSearchParams();
|
|
||||||
data.append('username', this.u.newUserName);
|
_this.u.ccdApplyStatus = "";
|
||||||
axios.request(axios_cfg('api/user/ccd/apply', data, 'form'))
|
_this.u.ccdApplyStatusMessage = "";
|
||||||
|
|
||||||
|
axios.request(axios_cfg('api/user/ccd/apply', JSON.stringify(_this.u.ccd), 'json'))
|
||||||
.then(function(response) {
|
.then(function(response) {
|
||||||
console.log(response.data);
|
_this.u.ccdApplyStatus = 200;
|
||||||
_this.u_get_data();
|
_this.u.ccdApplyStatusMessage = response.data;
|
||||||
_this.u.newUserName = '';
|
})
|
||||||
|
.catch(function(error) {
|
||||||
|
_this.u.ccdApplyStatus = error.response.status;
|
||||||
|
_this.u.ccdApplyStatusMessage = error.response.data;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-wrapper {
|
.modal-wrapper {
|
||||||
display: flex;
|
display: none;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
@ -36,25 +36,16 @@ body {
|
||||||
margin: auto 0;
|
margin: auto 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-new-user {
|
.static-address-label {
|
||||||
display: flex;
|
margin: 0.1em 1em 0.1em 0.1em;
|
||||||
flex-direction: row;
|
|
||||||
background-color: #eaeaea;
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-new-user-el-margin {
|
.modal-el-margin {
|
||||||
margin-left: 0.1rem;
|
margin: 0.1rem;
|
||||||
margin-right: 0.1rem;
|
|
||||||
margin-top: 0.1rem;
|
|
||||||
margin-bottom: 0.1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-show-config {
|
.new-user-btn {
|
||||||
display: flex;
|
margin-right: 2rem;
|
||||||
flex-direction: column;
|
|
||||||
background-color: #eaeaea;
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-show-config-txt-box {
|
.modal-show-config-txt-box {
|
||||||
|
|
|
@ -1,65 +1,135 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>openvpn-admin</title>
|
<title>openvpn-admin</title>
|
||||||
<link rel="stylesheet" href="css/normalize.css">
|
<link rel="stylesheet" href="css/normalize.css">
|
||||||
<link rel="stylesheet" href="css/bootstrap.min.css">
|
<link rel="stylesheet" href="css/bootstrap.min.css">
|
||||||
<link rel="stylesheet" href="css/style.css">
|
<link rel="stylesheet" href="css/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app" @click.left.stop="u_ctx_hide">
|
<div id="app">
|
||||||
|
|
||||||
<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">Account status</th>-->
|
||||||
|
<!-- <th scope="col">Expiration date</th>-->
|
||||||
|
<!-- <th scope="col">Revocation date</th>-->
|
||||||
|
<!--<!– <th scope="col">Connection status</th>–>-->
|
||||||
|
<!-- </tr>-->
|
||||||
|
<!-- </thead>-->
|
||||||
|
<!-- <tbody>-->
|
||||||
|
<!-- <tr v-for="row in u.data" :data-name="row.Identity" v-bind:style="row.ConnectionStatus" @contextmenu.prevent="u_ctx_show">-->
|
||||||
|
<!-- <td>{{ row.Identity }}</td>-->
|
||||||
|
<!-- <td>{{ row.AccountStatus }}</td>-->
|
||||||
|
<!-- <td>{{ row.ExpirationDate }}</td>-->
|
||||||
|
<!-- <td>{{ row.RevocationDate }}</td>-->
|
||||||
|
<!--<!– <td>{{ row.ConnectionStatus }}</td>–>-->
|
||||||
|
<!-- </tr>-->
|
||||||
|
<!-- </tbody>-->
|
||||||
|
<!-- </table>-->
|
||||||
|
<vue-good-table
|
||||||
|
:columns="columns"
|
||||||
|
:rows="rows"
|
||||||
|
:line-numbers="true"
|
||||||
|
:row-style-class="rowStyleClassFn"
|
||||||
|
:search-options="{ enabled: true}" >
|
||||||
|
<div slot="table-actions">
|
||||||
<button type="button" class="btn btn-sm btn-success el-square" v-on:click.stop="u.modalNewUserVisible=true">Add user</button>
|
<button type="button" class="btn btn-sm btn-success el-square" v-on:click.stop="u.modalNewUserVisible=true">Add user</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div slot="emptystate">
|
||||||
|
This will show up when there are no rows
|
||||||
|
</div>
|
||||||
|
<template slot="table-row" slot-scope="props">
|
||||||
|
<span v-if="props.column.field == 'actions'">
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-success el-square modal-el-margin"
|
||||||
|
type="button"
|
||||||
|
:title="action.label"
|
||||||
|
:data-username="props.row.Identity"
|
||||||
|
:data-name="action.name"
|
||||||
|
:data-text="action.label"
|
||||||
|
@click.left.stop="rowActionFn"
|
||||||
|
v-for="action in actions"
|
||||||
|
v-if="action.showWhenStatus == props.row.AccountStatus">
|
||||||
|
{{ action.label }}
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</vue-good-table>
|
||||||
|
|
||||||
<div class="dropdown-menu dropdown-custom" :style="uCtxStyle" v-show="u.ctxVisible">
|
<!-- <div class="d-flex justify-content-md-end">-->
|
||||||
<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>
|
<!-- <button type="button" class="btn btn-sm btn-success el-square new-user-btn" v-on:click.stop="u.ctxVisible=false;u.modalNewUserVisible=true">Add user</button>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
|
||||||
|
<div class="modal-wrapper" v-if="u.modalNewUserVisible" v-bind:style="modalNewUserDisplay">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4>Add new user </h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="text" class="form-control el-square modal-el-margin" placeholder="Username [_a-zA-Z0-9\.-]" v-model="u.newUserName">
|
||||||
|
<!-- <input type="password" class="form-control el-square modal-el-margin" minlength="6" autocomplete="off" placeholder="Password [_a-zA-Z0-9\.-]" v-model="u.newUserPassword">-->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table class="table table-bordered table-hover">
|
<div class="modal-footer justify-content-center" v-if="u.newUserCreateError.length > 0">
|
||||||
<thead class="thead-dark">
|
<div class="alert alert-danger" role="alert" >
|
||||||
<tr>
|
{{ u.newUserCreateError }}
|
||||||
<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>
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
<div class="modal-wrapper" v-if="u.modalShowConfigVisible">
|
<button type="button" class="btn btn-success el-square modal-el-margin" v-on:click.stop="create_user();">Create</button>
|
||||||
<div class="modal-show-config">
|
<button type="button" class="btn btn-primary el-square d-flex justify-content-sm-end modal-el-margin" v-on:click.stop="u.newUserName='';u.newUserPassword='nopass';u.modalNewUserVisible=false">Close</button>
|
||||||
<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>
|
||||||
<div class="row">
|
|
||||||
<pre class="modal-show-config-txt-box modal-new-user-el-margin">{{ u.openvpnConfig }}</pre>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-wrapper" v-if="u.modalShowCcdVisible">
|
<div class="modal-wrapper" v-if="u.modalShowConfigVisible" v-bind:style="modalShowConfigDisplay">
|
||||||
<div class="modal-ccd-config">
|
<div class="modal-dialog modal-lg">
|
||||||
<div class="row">
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4>ovpn config for {{ username }}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="d-flex">
|
||||||
|
<pre class="modal-show-config-txt-box">{{ u.openvpnConfig }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<div class="d-flex">
|
||||||
|
<button type="button" class="btn btn-success el-square modal-el-margin" v-clipboard:copy="u.openvpnConfig">Copy </button>
|
||||||
|
<button type="button" class="btn btn-primary el-square modal-el-margin" v-on:click.stop="u.openvpnConfig='';u.modalShowConfigVisible=false">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-wrapper" v-if="u.modalShowCcdVisible" v-bind:style="modalShowCcdDisplay">
|
||||||
|
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="input-group">
|
||||||
|
<h4 class="static-address-label ">Client "{{ username }}" static address</h4>
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<div class="input-group-text">
|
||||||
|
<input id="enable-static" type="checkbox" onchange="document.getElementById('staticAddress').disabled=!this.checked;" v-bind:checked="!customAddressEnabled">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input id="staticAddress" type="text" class="form-control" v-model="u.ccd.ClientAddress" placeholder="127.0.0.1" v-bind:disabled="customAddressEnabled">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="d-flex ">
|
||||||
<table class="table table-bordered table-hover">
|
<table class="table table-bordered table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -70,33 +140,40 @@
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="(route, index) in u.ccd">
|
<tr v-for="(customRoute, index) in u.ccd.CustomRoutes">
|
||||||
<td>{{ route.addr }}</td>
|
<td>{{ customRoute.Address }}</td>
|
||||||
<td>{{ route.mask }}</td>
|
<td>{{ customRoute.Mask }}</td>
|
||||||
<td>{{ route.desc }}</td>
|
<td>{{ customRoute.Description }}</td>
|
||||||
<td>
|
<td>
|
||||||
<!-- button type="button" class="btn btn-primary btn-sm el-square" v-on:click.stop="ccd_edit()">Edit</button -->
|
<button type="button" class="btn btn-primary btn-sm el-square modal-el-margin" v-on:click.stop="u.ccd.CustomRoutes.splice(index, 1)">Delete</button>
|
||||||
<button type="button" class="btn btn-primary btn-sm el-square" v-on:click.stop="ccd.splice(index, 1)">Delete</button>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><input type="text" v-model="u.newRoute.Address"/></td>
|
||||||
|
<td><input type="text" v-model="u.newRoute.Mask"/></td>
|
||||||
|
<td><input type="text" v-model="u.newRoute.Description"/></td>
|
||||||
|
<td>
|
||||||
|
<button type="button" class="btn btn-success el-square modal-el-margin" v-on:click.stop="u.ccd.CustomRoutes.push(u.newRoute);u.newRoute={};">Add</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
|
||||||
<input type="text" v-model="route.addr" />
|
|
||||||
<input type="text" v-model="route.mask" />
|
|
||||||
<input type="text" v-model="route.desc" />
|
|
||||||
<button type="button" class="btn btn-success el-square" v-on:click.stop="ccd.push(route)">Add new route</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="modal-footer justify-content-center" v-if="u.ccdApplyStatusMessage.length > 0">
|
||||||
<button type="button" class="btn btn-success el-square" v-on:click.stop="ccd_apply()">Save</button>
|
<div class="alert" v-bind:class="ccdApplyStatusCssClass" role="alert">
|
||||||
<button type="button" class="btn btn-danger el-square" v-on:click.stop="u.ccd=[];u.modalShowCcdVisible=false">Close</button>
|
{{ u.ccdApplyStatusMessage }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-success el-square modal-el-margin" v-on:click.stop="ccd_apply()">Save</button>
|
||||||
|
<button type="button" class="btn btn-primary el-square modal-el-margin" v-on:click.stop="u.ccd={Name:'',ClientAddress:'',CustomRoutes:[]};u.ccdApplyStatusMessage='';u.ccdApplyStatus='';u.modalShowCcdVisible=false">Close</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<script src="dist/build.js"></script>
|
<script src="dist/build.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
<!-- n['flag'], n['expiration_date'], n['revocation_date'], n['serial_number'], n['filename'], n['distinguished_name'] -->
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
var path = require('path')
|
var path = require('path')
|
||||||
var webpack = require('webpack')
|
var webpack = require('webpack')
|
||||||
|
const TerserPlugin = require('terser-webpack-plugin');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
entry: './src/main.js',
|
entry: './src/main.js',
|
||||||
|
@ -95,11 +96,8 @@ if (process.env.NODE_ENV === 'production') {
|
||||||
NODE_ENV: '"production"'
|
NODE_ENV: '"production"'
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
new webpack.optimize.UglifyJsPlugin({
|
new TerserPlugin({
|
||||||
sourceMap: true,
|
sourceMap: true
|
||||||
compress: {
|
|
||||||
warnings: false
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
new webpack.LoaderOptionsPlugin({
|
new webpack.LoaderOptionsPlugin({
|
||||||
minimize: true
|
minimize: true
|
||||||
|
|
413
main.go
413
main.go
|
@ -8,34 +8,33 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"time"
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
// "reflect"
|
|
||||||
"bufio"
|
"bufio"
|
||||||
"net"
|
"net"
|
||||||
// "io"
|
|
||||||
// "encoding/binary"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"gopkg.in/alecthomas/kingpin.v2"
|
"gopkg.in/alecthomas/kingpin.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
listenHost = kingpin.Flag("listen.host","host for openvpn-admin").Default("127.0.0.1").String()
|
listenHost = kingpin.Flag("listen.host","host for openvpn-admin").Default("0.0.0.0").String()
|
||||||
listenPort = kingpin.Flag("listen.port","port for openvpn-admin").Default("8080").String()
|
listenPort = kingpin.Flag("listen.port","port for openvpn-admin").Default("8080").String()
|
||||||
easyrsaPath = kingpin.Flag("easyrsa.path", "path to easyrsa dir").Default("/etc/openvpn/easyrsa").String()
|
openvpnServerHost = kingpin.Flag("ovpn.host","host for openvpn server").Default("127.0.0.1").String()
|
||||||
indexTxtPath = kingpin.Flag("easyrsa.index-path", "path to easyrsa index file.").Default("/etc/openvpn/easyrsa/pki/index.txt").String()
|
openvpnServerPort = kingpin.Flag("ovpn.port","port for openvpn server").Default("7777").String()
|
||||||
ccdCustom = kingpin.Flag("ccd.custom", "enable or disable custom routes").Default("false").Bool()
|
openvpnNetwork = kingpin.Flag("ovpn.network","network for openvpn server").Default("172.16.100.0/24").String()
|
||||||
ccdDir = kingpin.Flag("ccd.path", "path to client-config-dir").Default("/etc/openvpn/ccd").String()
|
mgmtListenHost = kingpin.Flag("mgmt.host","host for mgmt").Default("127.0.0.1").String()
|
||||||
|
mgmtListenPort = kingpin.Flag("mgmt.port","port for mgmt").Default("8989").String()
|
||||||
|
easyrsaDirPath = kingpin.Flag("easyrsa.path", "path to easyrsa dir").Default("/mnt/easyrsa").String()
|
||||||
|
indexTxtPath = kingpin.Flag("easyrsa.index-path", "path to easyrsa index file.").Default("/mnt/easyrsa/pki/index.txt").String()
|
||||||
|
ccdDir = kingpin.Flag("ccd.path", "path to client-config-dir").Default("/mnt/ccd").String()
|
||||||
staticPath = kingpin.Flag("static.path", "path to static dir").Default("./static").String()
|
staticPath = kingpin.Flag("static.path", "path to static dir").Default("./static").String()
|
||||||
|
debug = kingpin.Flag("debug", "Enable debug mode.").Default("false").Bool()
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
usernameRegexp = `^([a-zA-Z0-9_.-])+$`
|
usernameRegexp = `^([a-zA-Z0-9_.-])+$`
|
||||||
openvpnServerHost = "127.0.0.1"
|
|
||||||
openvpnServerPort = "7777"
|
|
||||||
mgmtListenHost = "127.0.0.1"
|
|
||||||
mgmtListenPort = "7788"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type openvpnClientConfig struct {
|
type openvpnClientConfig struct {
|
||||||
|
@ -47,14 +46,24 @@ type openvpnClientConfig struct {
|
||||||
TLS string
|
TLS string
|
||||||
}
|
}
|
||||||
|
|
||||||
type ccdLine struct {
|
type openvpnClient struct {
|
||||||
addr string `json:"addr"`
|
Identity string `json:"Identity"`
|
||||||
mask string `json:"mask"`
|
AccountStatus string `json:"AccountStatus"`
|
||||||
desc string `json:"desc"`
|
ExpirationDate string `json:"ExpirationDate"`
|
||||||
|
RevocationDate string `json:"RevocationDate"`
|
||||||
|
ConnectionStatus string `json:"ConnectionStatus"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ccdFile struct {
|
type ccdRoute struct {
|
||||||
lines []ccdLine
|
Address string `json:"Address"`
|
||||||
|
Mask string `json:"Mask"`
|
||||||
|
Description string `json:"Description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Ccd struct {
|
||||||
|
User string `json:"User"`
|
||||||
|
ClientAddress string `json:"ClientAddress"`
|
||||||
|
CustomRoutes []ccdRoute `json:"CustomRoutes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type indexTxtLine struct {
|
type indexTxtLine struct {
|
||||||
|
@ -80,13 +89,21 @@ type clientStatus struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func userListHandler(w http.ResponseWriter, r *http.Request) {
|
func userListHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
userList, _ := json.Marshal(indexTxtParser(fRead(*indexTxtPath)))
|
usersList, _ := json.Marshal(usersList())
|
||||||
fmt.Fprintf(w, "%s", userList)
|
fmt.Fprintf(w, "%s", usersList)
|
||||||
}
|
}
|
||||||
|
|
||||||
func userCreateHandler(w http.ResponseWriter, r *http.Request) {
|
func userCreateHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
r.ParseForm()
|
r.ParseForm()
|
||||||
fmt.Fprintf(w, "%s", userCreate(r.FormValue("username")))
|
userCreated, userCreateStatus := userCreate(r.FormValue("username"))
|
||||||
|
|
||||||
|
if userCreated {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
fmt.Fprintf(w, userCreateStatus)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
http.Error(w, userCreateStatus, http.StatusUnprocessableEntity)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func userRevokeHandler(w http.ResponseWriter, r *http.Request) {
|
func userRevokeHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -101,20 +118,42 @@ func userUnrevokeHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
func userShowConfigHandler(w http.ResponseWriter, r *http.Request) {
|
func userShowConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
r.ParseForm()
|
r.ParseForm()
|
||||||
fmt.Printf("username: %v\n%s\n", r.PostForm, r.FormValue("username"))
|
|
||||||
fmt.Fprintf(w, "%s", renderClientConfig(r.FormValue("username")))
|
fmt.Fprintf(w, "%s", renderClientConfig(r.FormValue("username")))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func userDisconnectHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.ParseForm()
|
||||||
|
// fmt.Fprintf(w, "%s", userDisconnect(r.FormValue("username")))
|
||||||
|
fmt.Fprintf(w, "%s", r.FormValue("username"))
|
||||||
|
}
|
||||||
|
|
||||||
func userShowCcdHandler(w http.ResponseWriter, r *http.Request) {
|
func userShowCcdHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
r.ParseForm()
|
r.ParseForm()
|
||||||
fmt.Printf("username: %v\n%s\n", r.PostForm, r.FormValue("username"))
|
ccd, _ := json.Marshal(getCcd(r.FormValue("username")))
|
||||||
fmt.Fprintf(w, "%s", renderCcdConfig(r.FormValue("username")))
|
fmt.Fprintf(w, "%s", ccd)
|
||||||
}
|
}
|
||||||
|
|
||||||
func userApplyCcdHandler(w http.ResponseWriter, r *http.Request) {
|
func userApplyCcdHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
r.ParseForm()
|
var ccd Ccd
|
||||||
fmt.Printf("username: %v\n%s\n", r.PostForm, r.FormValue("username"))
|
if r.Body == nil {
|
||||||
fmt.Fprintf(w, "%s", ccdFileModify(r.FormValue("username"),ccdFileParser(r.FormValue("ccd"))))
|
http.Error(w, "Please send a request body", 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&ccd)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ccdApplied, applyStatus := modifyCcd(ccd)
|
||||||
|
|
||||||
|
if ccdApplied {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
fmt.Fprintf(w, applyStatus)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
http.Error(w, applyStatus, http.StatusUnprocessableEntity)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@ -122,34 +161,26 @@ func main() {
|
||||||
|
|
||||||
fmt.Println("Bind: http://" + *listenHost + ":" + *listenPort)
|
fmt.Println("Bind: http://" + *listenHost + ":" + *listenPort)
|
||||||
|
|
||||||
fs := http.FileServer(http.Dir(*staticPath))
|
fs := CacheControlWrapper(http.FileServer(http.Dir(*staticPath)))
|
||||||
|
|
||||||
http.Handle("/", fs)
|
http.Handle("/", fs)
|
||||||
http.HandleFunc("/api/users/list", userListHandler)
|
http.HandleFunc("/api/users/list", userListHandler)
|
||||||
http.HandleFunc("/api/user/create", userCreateHandler)
|
http.HandleFunc("/api/user/create", userCreateHandler)
|
||||||
http.HandleFunc("/api/user/revoke", userRevokeHandler)
|
http.HandleFunc("/api/user/revoke", userRevokeHandler)
|
||||||
http.HandleFunc("/api/user/unrevoke", userUnrevokeHandler)
|
http.HandleFunc("/api/user/unrevoke", userUnrevokeHandler)
|
||||||
http.HandleFunc("/api/user/showconfig", userShowConfigHandler)
|
http.HandleFunc("/api/user/config/show", userShowConfigHandler)
|
||||||
http.HandleFunc("/api/user/ccd/list", userShowCcdHandler)
|
http.HandleFunc("/api/user/disconnect", userDisconnectHandler)
|
||||||
|
http.HandleFunc("/api/user/ccd", userShowCcdHandler)
|
||||||
http.HandleFunc("/api/user/ccd/apply", userApplyCcdHandler)
|
http.HandleFunc("/api/user/ccd/apply", userApplyCcdHandler)
|
||||||
|
|
||||||
log.Fatal(http.ListenAndServe(*listenHost + ":" + *listenPort, nil))
|
log.Fatal(http.ListenAndServe(*listenHost + ":" + *listenPort, nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
func fRead(path string) string {
|
func CacheControlWrapper(h http.Handler) http.Handler {
|
||||||
content, err := ioutil.ReadFile(path)
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if err != nil {
|
w.Header().Set("Cache-Control", "max-age=2592000") // 30 days
|
||||||
log.Fatal(err)
|
h.ServeHTTP(w, r)
|
||||||
}
|
})
|
||||||
|
|
||||||
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 {
|
func indexTxtParser(txt string) []indexTxtLine {
|
||||||
|
@ -169,7 +200,6 @@ func indexTxtParser(txt string) []indexTxtLine {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return indexTxt
|
return indexTxt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,159 +208,211 @@ func renderIndexTxt(data []indexTxtLine) string {
|
||||||
for _, line := range data {
|
for _, line := range data {
|
||||||
switch {
|
switch {
|
||||||
case line.Flag == "V":
|
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)
|
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":
|
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)
|
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":
|
// case line.flag == "E":
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (indexTxt)
|
return indexTxt
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderClientConfig(username string) string {
|
func renderClientConfig(username string) string {
|
||||||
if checkUserExist(username) {
|
if checkUserExist(username) {
|
||||||
conf := openvpnClientConfig{}
|
conf := openvpnClientConfig{}
|
||||||
conf.Host = openvpnServerHost
|
conf.Host = *openvpnServerHost
|
||||||
conf.Port = openvpnServerPort
|
conf.Port = *openvpnServerPort
|
||||||
conf.CA = fRead(*easyrsaPath + "/pki/ca.crt")
|
conf.CA = fRead(*easyrsaDirPath + "/pki/ca.crt")
|
||||||
conf.Cert = fRead(*easyrsaPath + "/pki/issued/" + username + ".crt")
|
conf.Cert = fRead(*easyrsaDirPath + "/pki/issued/" + username + ".crt")
|
||||||
conf.Key = fRead(*easyrsaPath + "/pki/private/" + username + ".key")
|
conf.Key = fRead(*easyrsaDirPath + "/pki/private/" + username + ".key")
|
||||||
conf.TLS = fRead(*easyrsaPath + "/pki/ta.key")
|
conf.TLS = fRead(*easyrsaDirPath + "/pki/ta.key")
|
||||||
|
|
||||||
t, _ := template.ParseFiles("client.conf.tpl")
|
t, _ := template.ParseFiles("client.conf.tpl")
|
||||||
var tmp bytes.Buffer
|
var tmp bytes.Buffer
|
||||||
t.Execute(&tmp, conf)
|
t.Execute(&tmp, conf)
|
||||||
// fmt.Printf("%+v\n", err)
|
|
||||||
fmt.Printf("%+v\n", tmp.String())
|
fmt.Printf("%+v\n", tmp.String())
|
||||||
return (fmt.Sprintf("%+v\n", tmp.String()))
|
return (fmt.Sprintf("%+v\n", tmp.String()))
|
||||||
}
|
}
|
||||||
fmt.Printf("User \"%s\" not found", username)
|
fmt.Printf("User \"%s\" not found", username)
|
||||||
return (fmt.Sprintf("User \"%s\" not found", username))
|
return fmt.Sprintf("User \"%s\" not found", username)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ccdFileParser(txt string) ccdFile {
|
func parseCcd(username string) Ccd {
|
||||||
ccdFile := ccdFile{}
|
ccd := Ccd{}
|
||||||
|
ccd.User = username
|
||||||
|
ccd.ClientAddress = "dynamic"
|
||||||
|
ccd.CustomRoutes = []ccdRoute{}
|
||||||
|
|
||||||
txtLinesArray := strings.Split(txt, "\n")
|
txtLinesArray := strings.Split(fRead(*ccdDir + "/" + username), "\n")
|
||||||
|
|
||||||
for _, v := range txtLinesArray {
|
for _, v := range txtLinesArray {
|
||||||
str := strings.Fields(v)
|
str := strings.Fields(v)
|
||||||
if len(str) > 0 {
|
if len(str) > 0 {
|
||||||
switch {
|
switch {
|
||||||
case strings.HasPrefix(str[0], "ifconfig-push"):
|
case strings.HasPrefix(str[0], "ifconfig-push"):
|
||||||
ccdFile.lines = append(ccdFile.lines, ccdLine{addr: str[2], mask: str[3], desc: "Client Address"})
|
ccd.ClientAddress = str[1]
|
||||||
case strings.HasPrefix(str[0], "push"):
|
case strings.HasPrefix(str[0], "push"):
|
||||||
ccdFile.lines = append(ccdFile.lines, ccdLine{addr: str[2], mask: str[3], desc: strings.Join(str[4:], "")})
|
ccd.CustomRoutes = append(ccd.CustomRoutes, ccdRoute{Address: strings.Trim(str[2], "\""), Mask: strings.Trim(str[3], "\""), Description: strings.Trim(strings.Join(str[4:], ""), "#")})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ccdFile
|
return ccd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func modifyCcd(ccd Ccd) (bool, string) {
|
||||||
|
|
||||||
func renderCcdConfig(username string) string {
|
if fCreate(*ccdDir + "/" + ccd.User) {
|
||||||
if checkCcdExist(username) {
|
ccdValid, ccdErr := validateCcd(ccd)
|
||||||
ccdFileParser(fRead(*ccdDir + "/" + username))
|
if ccdErr != "" {
|
||||||
|
return false, ccdErr
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("ccd for user \"%s\" not found", username)
|
if ccdValid {
|
||||||
return (fmt.Sprintf("ccd for user \"%s\" not found", username))
|
t, _ := template.ParseFiles("ccd.tpl")
|
||||||
}
|
var tmp bytes.Buffer
|
||||||
|
t.Execute(&tmp, ccd)
|
||||||
|
fWrite(*ccdDir + "/" + ccd.User, tmp.String())
|
||||||
func ccdFileModify(username string, ccdFile ccdFile) bool {
|
return true, "ccd updated successfully"
|
||||||
if checkCcdExist(username) {
|
|
||||||
}
|
}
|
||||||
return true
|
}
|
||||||
|
|
||||||
|
return false, "something goes wrong"
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://community.openvpn.net/openvpn/ticket/623
|
func validateCcd(ccd Ccd) (bool, string) {
|
||||||
func crlFix() {
|
ccdErr := ""
|
||||||
os.Chmod(*easyrsaPath + "/pki", 0755)
|
|
||||||
err := os.Chmod(*easyrsaPath + "/pki/crl.pem", 0640)
|
if ccd.ClientAddress != "dynamic" {
|
||||||
|
_, ovpnNet, err := net.ParseCIDR(*openvpnNetwork)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ! checkStaticAddressIsFree(ccd.ClientAddress, ccd.User) {
|
||||||
|
ccdErr = fmt.Sprintf("ClientAddress \"%s\" already assigned to another user", ccd.ClientAddress)
|
||||||
|
if *debug {
|
||||||
|
log.Printf("ERROR: Modify ccd for user %s: %s", ccd.User, ccdErr)
|
||||||
|
}
|
||||||
|
return false, ccdErr
|
||||||
|
}
|
||||||
|
|
||||||
|
if net.ParseIP(ccd.ClientAddress) == nil {
|
||||||
|
ccdErr = fmt.Sprintf("ClientAddress \"%s\" not a valid IP address", ccd.ClientAddress)
|
||||||
|
if *debug {
|
||||||
|
log.Printf("ERROR: Modify ccd for user %s: %s", ccd.User, ccdErr)
|
||||||
|
}
|
||||||
|
return false, ccdErr
|
||||||
|
}
|
||||||
|
|
||||||
|
if ! ovpnNet.Contains(net.ParseIP(ccd.ClientAddress)) {
|
||||||
|
ccdErr = fmt.Sprintf("ClientAddress \"%s\" not belongs to openvpn server network", ccd.ClientAddress)
|
||||||
|
if *debug {
|
||||||
|
log.Printf("ERROR: Modify ccd for user %s: %s", ccdErr)
|
||||||
|
}
|
||||||
|
return false, ccdErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, route := range ccd.CustomRoutes {
|
||||||
|
if net.ParseIP(route.Address) == nil {
|
||||||
|
ccdErr = fmt.Sprintf("CustomRoute.Address \"%s\" must be a valid IP address", route.Address)
|
||||||
|
if *debug {
|
||||||
|
log.Printf("ERROR: Modify ccd for user %s: %s", ccdErr)
|
||||||
|
}
|
||||||
|
return false, ccdErr
|
||||||
|
}
|
||||||
|
|
||||||
|
if net.ParseIP(route.Mask) == nil {
|
||||||
|
ccdErr = fmt.Sprintf("CustomRoute.Mask \"%s\" must be a valid IP address", route.Mask)
|
||||||
|
if *debug {
|
||||||
|
log.Printf("ERROR: Modify ccd for user %s: %s", ccd.User, ccdErr)
|
||||||
|
}
|
||||||
|
return false, ccdErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, ccdErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func runBash(script string) string {
|
func getCcd(username string) Ccd {
|
||||||
fmt.Println(script)
|
ccd := Ccd{}
|
||||||
cmd := exec.Command("bash", "-c", script)
|
ccd.User = username
|
||||||
stdout, err := cmd.CombinedOutput()
|
ccd.ClientAddress = "dynamic"
|
||||||
if err != nil {
|
ccd.CustomRoutes = []ccdRoute{}
|
||||||
return (fmt.Sprint(err) + " : " + string(stdout))
|
|
||||||
|
if fCreate(*ccdDir + "/" + username) {
|
||||||
|
ccd = parseCcd(username)
|
||||||
}
|
}
|
||||||
return (string(stdout))
|
return ccd
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkStaticAddressIsFree(staticAddress string, username string) bool {
|
||||||
|
o := runBash(fmt.Sprintf("grep -rl %s %s | grep -vx %s/%s | wc -l", staticAddress, *ccdDir, *ccdDir, username))
|
||||||
|
|
||||||
|
if strings.TrimSpace(o) == "0" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateUsername(username string) bool {
|
func validateUsername(username string) bool {
|
||||||
var validUsername = regexp.MustCompile(usernameRegexp)
|
var validUsername = regexp.MustCompile(usernameRegexp)
|
||||||
return (validUsername.MatchString(username))
|
return validUsername.MatchString(username)
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkUserExist(username string) bool {
|
func checkUserExist(username string) bool {
|
||||||
for _, u := range indexTxtParser(fRead(*indexTxtPath)) {
|
for _, u := range indexTxtParser(fRead(*indexTxtPath)) {
|
||||||
if u.DistinguishedName == ("/CN=" + username) {
|
if u.DistinguishedName == ("/CN=" + username) {
|
||||||
return (true)
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (false)
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkCcdExist(username string) bool {
|
func usersList() []openvpnClient {
|
||||||
if *ccdCustom {
|
users := []openvpnClient{}
|
||||||
if _, err := os.Stat(*ccdDir + "/" + username); err == nil {
|
|
||||||
return (true)
|
|
||||||
} else if os.IsNotExist(err) {
|
|
||||||
fmt.Printf("ccd for user \"%s\" not found", username)
|
|
||||||
return (false)
|
|
||||||
} else {
|
|
||||||
fmt.Printf("Something goes wrong during checking ccd for user \"%s\"", username)
|
|
||||||
fmt.Printf("err: %s", err)
|
|
||||||
return (false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func usersList() []string {
|
|
||||||
users := []string{}
|
|
||||||
for _, line := range indexTxtParser(fRead(*indexTxtPath)) {
|
for _, line := range indexTxtParser(fRead(*indexTxtPath)) {
|
||||||
users = append(users, line.Identity)
|
if line.Identity != "server" {
|
||||||
|
switch {
|
||||||
|
case line.Flag == "V":
|
||||||
|
users = append(users, openvpnClient{Identity: line.Identity, ExpirationDate: indexTxtDateToHumanReadable(line.ExpirationDate), AccountStatus: "Active"})
|
||||||
|
case line.Flag == "R":
|
||||||
|
users = append(users, openvpnClient{Identity: line.Identity, RevocationDate: indexTxtDateToHumanReadable(line.RevocationDate), ExpirationDate: indexTxtDateToHumanReadable(line.ExpirationDate), AccountStatus: "Revoked"})
|
||||||
|
case line.Flag == "E":
|
||||||
|
users = append(users, openvpnClient{Identity: line.Identity, RevocationDate: indexTxtDateToHumanReadable(line.RevocationDate), ExpirationDate: indexTxtDateToHumanReadable(line.ExpirationDate), AccountStatus: "Expired"})
|
||||||
|
|
||||||
}
|
}
|
||||||
return (users)
|
}
|
||||||
|
}
|
||||||
|
return users
|
||||||
}
|
}
|
||||||
|
|
||||||
func userCreate(username string) string {
|
func userCreate(username string) (bool, string) {
|
||||||
|
// TODO: add password for user cert . priority=low
|
||||||
if validateUsername(username) == false {
|
if validateUsername(username) == false {
|
||||||
fmt.Printf("Username \"%s\" incorrect, you can use only %s\n", username, usernameRegexp)
|
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))
|
return false, fmt.Sprintf("Username \"%s\" incorrect, you can use only %s", username, usernameRegexp)
|
||||||
}
|
}
|
||||||
if checkUserExist(username) {
|
if checkUserExist(username) {
|
||||||
fmt.Printf("User \"%s\" already exists\n", username)
|
fmt.Printf("User \"%s\" already exists\n", username)
|
||||||
return (fmt.Sprintf("User \"%s\" already exists\n", username))
|
return false, fmt.Sprintf("User \"%s\" already exists", username)
|
||||||
}
|
}
|
||||||
o := runBash(fmt.Sprintf("date +%%Y-%%m-%%d\\ %%H:%%M:%%S && cd %s && ./easyrsa build-client-full %s nopass", *easyrsaPath, username))
|
o := runBash(fmt.Sprintf("date +%%Y-%%m-%%d\\ %%H:%%M:%%S && cd %s && easyrsa build-client-full %s nopass", *easyrsaDirPath, username))
|
||||||
fmt.Println(o)
|
fmt.Println(o)
|
||||||
return ("")
|
return true, fmt.Sprintf("User \"%s\" created", username)
|
||||||
}
|
}
|
||||||
|
|
||||||
func userRevoke(username string) string {
|
func userRevoke(username string) string {
|
||||||
if checkUserExist(username) {
|
if checkUserExist(username) {
|
||||||
// check certificate valid flag 'V'
|
// 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))
|
o := runBash(fmt.Sprintf("date +%%Y-%%m-%%d\\ %%H:%%M:%%S && cd %s && echo yes | easyrsa revoke %s && easyrsa gen-crl", *easyrsaDirPath, username))
|
||||||
crlFix()
|
crlFix()
|
||||||
return (fmt.Sprintln(o))
|
return fmt.Sprintln(o)
|
||||||
}
|
}
|
||||||
fmt.Printf("User \"%s\" not found", username)
|
fmt.Printf("User \"%s\" not found", username)
|
||||||
return (fmt.Sprintf("User \"%s\" not found", username))
|
return fmt.Sprintf("User \"%s\" not found", username)
|
||||||
}
|
}
|
||||||
|
|
||||||
func userUnrevoke(username string) string {
|
func userUnrevoke(username string) string {
|
||||||
|
@ -339,24 +421,45 @@ func userUnrevoke(username string) string {
|
||||||
usersFromIndexTxt := indexTxtParser(fRead(*indexTxtPath))
|
usersFromIndexTxt := indexTxtParser(fRead(*indexTxtPath))
|
||||||
for i := range usersFromIndexTxt {
|
for i := range usersFromIndexTxt {
|
||||||
if usersFromIndexTxt[i].DistinguishedName == ("/CN=" + username) {
|
if usersFromIndexTxt[i].DistinguishedName == ("/CN=" + username) {
|
||||||
|
if usersFromIndexTxt[i].Flag == "R" {
|
||||||
usersFromIndexTxt[i].Flag = "V"
|
usersFromIndexTxt[i].Flag = "V"
|
||||||
usersFromIndexTxt[i].RevocationDate = ""
|
usersFromIndexTxt[i].RevocationDate = ""
|
||||||
|
o := runBash(fmt.Sprintf("cd %s && cp pki/revoked/certs_by_serial/%s.crt pki/issued/%s.crt", *easyrsaDirPath, usersFromIndexTxt[i].SerialNumber, username))
|
||||||
|
fmt.Println(o)
|
||||||
|
o = runBash(fmt.Sprintf("cd %s && cp pki/revoked/certs_by_serial/%s.crt pki/certs_by_serial/%s.pem", *easyrsaDirPath, usersFromIndexTxt[i].SerialNumber, usersFromIndexTxt[i].SerialNumber))
|
||||||
|
fmt.Println(o)
|
||||||
|
o = runBash(fmt.Sprintf("cd %s && cp pki/revoked/private_by_serial/%s.key pki/private/%s.key", *easyrsaDirPath, usersFromIndexTxt[i].SerialNumber, username))
|
||||||
|
fmt.Println(o)
|
||||||
|
o = runBash(fmt.Sprintf("cd %s && cp pki/revoked/reqs_by_serial/%s.req pki/reqs/%s.req", *easyrsaDirPath, usersFromIndexTxt[i].SerialNumber, username))
|
||||||
|
fmt.Println(o)
|
||||||
|
fWrite(*indexTxtPath, renderIndexTxt(usersFromIndexTxt))
|
||||||
|
fmt.Print(renderIndexTxt(usersFromIndexTxt))
|
||||||
|
o = runBash(fmt.Sprintf("cd %s && easyrsa gen-crl", *easyrsaDirPath))
|
||||||
|
fmt.Println(o)
|
||||||
|
crlFix()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
fWrite(*indexTxtPath, renderIndexTxt(usersFromIndexTxt))
|
fWrite(*indexTxtPath, renderIndexTxt(usersFromIndexTxt))
|
||||||
fmt.Print(renderIndexTxt(usersFromIndexTxt))
|
fmt.Print(renderIndexTxt(usersFromIndexTxt))
|
||||||
crlFix()
|
crlFix()
|
||||||
return (fmt.Sprintf("{\"msg\":\"User %s successfully unrevoked\"}", username))
|
return fmt.Sprintf("{\"msg\":\"User %s successfully unrevoked\"}", username)
|
||||||
}
|
}
|
||||||
return (fmt.Sprintf("{\"msg\":\"User \"%s\" not found\"}", username))
|
return fmt.Sprintf("{\"msg\":\"User \"%s\" not found\"}", username)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: add ability to change password for user cert . priority=low
|
||||||
|
// func userChangePassword(username string, newPassword string) bool {
|
||||||
|
//
|
||||||
|
// return false
|
||||||
|
// }
|
||||||
|
|
||||||
func ovpnMgmtRead(conn net.Conn) string {
|
func ovpnMgmtRead(conn net.Conn) string {
|
||||||
buf := make([]byte, 32768)
|
buf := make([]byte, 32768)
|
||||||
len, _ := conn.Read(buf)
|
len, _ := conn.Read(buf)
|
||||||
s := string(buf[:len])
|
s := string(buf[:len])
|
||||||
return (s)
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func mgmtConnectedUsersParser(text string) []clientStatus {
|
func mgmtConnectedUsersParser(text string) []clientStatus {
|
||||||
|
@ -397,11 +500,11 @@ func mgmtConnectedUsersParser(text string) []clientStatus {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (u)
|
return u
|
||||||
}
|
}
|
||||||
|
|
||||||
func mgmtKillUserConnection(username string) {
|
func mgmtKillUserConnection(username string) {
|
||||||
conn, _ := net.Dial("tcp", mgmtListenHost+":"+mgmtListenPort)
|
conn, _ := net.Dial("tcp", *mgmtListenHost+":"+*mgmtListenPort)
|
||||||
ovpnMgmtRead(conn) // read welcome message
|
ovpnMgmtRead(conn) // read welcome message
|
||||||
conn.Write([]byte(fmt.Sprintf("kill %s\n", username)))
|
conn.Write([]byte(fmt.Sprintf("kill %s\n", username)))
|
||||||
fmt.Printf("%v", ovpnMgmtRead(conn))
|
fmt.Printf("%v", ovpnMgmtRead(conn))
|
||||||
|
@ -409,10 +512,66 @@ func mgmtKillUserConnection(username string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func mgmtGetActiveClients() []clientStatus {
|
func mgmtGetActiveClients() []clientStatus {
|
||||||
conn, _ := net.Dial("tcp", mgmtListenHost+":"+mgmtListenPort)
|
conn, _ := net.Dial("tcp", *mgmtListenHost+":"+*mgmtListenPort)
|
||||||
ovpnMgmtRead(conn) // read welcome message
|
ovpnMgmtRead(conn) // read welcome message
|
||||||
conn.Write([]byte("status\n"))
|
conn.Write([]byte("status\n"))
|
||||||
activeClients := mgmtConnectedUsersParser(ovpnMgmtRead(conn))
|
activeClients := mgmtConnectedUsersParser(ovpnMgmtRead(conn))
|
||||||
conn.Close()
|
conn.Close()
|
||||||
return (activeClients)
|
return activeClients
|
||||||
|
}
|
||||||
|
|
||||||
|
func indexTxtDateToHumanReadable(datetime string) string {
|
||||||
|
layout := "060102150405Z"
|
||||||
|
t, err := time.Parse(layout, datetime)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
return t.Format("2006-01-02 15:04:05")
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://community.openvpn.net/openvpn/ticket/623
|
||||||
|
func crlFix() {
|
||||||
|
os.Chmod(*easyrsaDirPath + "/pki", 0755)
|
||||||
|
err := os.Chmod(*easyrsaDirPath + "/pki/crl.pem", 0640)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fRead(path string) string {
|
||||||
|
content, err := ioutil.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
return string(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fCreate(path string) bool {
|
||||||
|
var _, err = os.Stat(path)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
var file, err = os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func fWrite(path, content string) {
|
||||||
|
err := ioutil.WriteFile(path, []byte(content), 0644)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
21
werf.yaml
21
werf.yaml
|
@ -27,7 +27,7 @@ ansible:
|
||||||
- build-base
|
- build-base
|
||||||
- gcc
|
- gcc
|
||||||
- name: Build backend
|
- name: Build backend
|
||||||
command: go build -ldflags='-extldflags "-static" -s -w' -o openvpn-ui
|
command: go build -ldflags='-extldflags "-static" -s -w' -o openvpn-admin
|
||||||
environment:
|
environment:
|
||||||
CGO_ENABLED: 0
|
CGO_ENABLED: 0
|
||||||
GOOS: linux
|
GOOS: linux
|
||||||
|
@ -64,13 +64,24 @@ image: openvpn-admin
|
||||||
from: alpine:3.11
|
from: alpine:3.11
|
||||||
import:
|
import:
|
||||||
- artifact: backend-builder
|
- artifact: backend-builder
|
||||||
add: /app/openvpn-ui
|
add: /app/openvpn-admin
|
||||||
to: /usr/bin/openvpn-ui
|
to: /usr/bin/openvpn-admin
|
||||||
before: setup
|
before: setup
|
||||||
- artifact: frontend-builder
|
- artifact: frontend-builder
|
||||||
add: /app/static
|
add: /app/static
|
||||||
to: /app/static
|
to: /app/static
|
||||||
before: setup
|
before: setup
|
||||||
|
git:
|
||||||
|
- add: /client.conf.tpl
|
||||||
|
to: /app/client.conf.tpl
|
||||||
|
stageDependencies:
|
||||||
|
setup:
|
||||||
|
- "*"
|
||||||
|
- add: /ccd.tpl
|
||||||
|
to: /app/ccd.tpl
|
||||||
|
stageDependencies:
|
||||||
|
setup:
|
||||||
|
- "*"
|
||||||
ansible:
|
ansible:
|
||||||
install:
|
install:
|
||||||
- name: Install packages
|
- name: Install packages
|
||||||
|
@ -88,8 +99,8 @@ ansible:
|
||||||
image: openvpn
|
image: openvpn
|
||||||
from: alpine:3.11
|
from: alpine:3.11
|
||||||
git:
|
git:
|
||||||
- add: /.werffiles/configure.sh
|
- add: /.werffiles/
|
||||||
to: /etc/openvpn/setup/configure.sh
|
to: /etc/openvpn/setup/
|
||||||
stageDependencies:
|
stageDependencies:
|
||||||
install:
|
install:
|
||||||
- "*"
|
- "*"
|
||||||
|
|
Loading…
Reference in a new issue