Commit 61b764e4 authored by mehdi's avatar mehdi

Adding device connection status & changing commonName format from uid to username

parent 0509f0c7
......@@ -29,8 +29,8 @@ ADD app /app/code/app
# Somehow postinstall is not run automatically when building docker image
RUN npm run postinstall
ADD start.sh server.js openvpn-conf.sh /app/code/
RUN chmod +x start.sh openvpn-conf.sh
ADD start.sh server.js openvpn-conf.sh openvpn-on-client-connect.sh openvpn-on-client-disconnect.sh /app/code/
RUN chmod +x start.sh openvpn-conf.sh openvpn-on-client-connect.sh openvpn-on-client-disconnect.sh
RUN mkdir -p /app/data
......
......@@ -92,3 +92,15 @@ form {
color: #a94442;
font-weight: 700;
}
.status-indicator {
height: 10px;
width: 10px;
background-color: #db4033;
border-radius: 50%;
border: 1px solid #666;
}
.status-indicator.connected {
background-color: #2dc841;
}
......@@ -128,17 +128,27 @@
<table class="table table-hover table-condensed">
<thead>
<tr>
<th></th>
<th>Status</th>
<th>Name</th>
<th>Created</th>
<th>IP</th>
<th style="text-align: right;">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="entry in entries">
<th><i class="fa fa-key fa-2"></i></th>
<th>
<div class="status-indicator" :class="{connected: entry.connected && entry.connected.ip && entry.connected.vpnIp}"></div>
</th>
<th><a>{{ entry.name }}</a></th>
<th><span data-toggle="tooltip" :title="prettyFullDate(entry.created)">{{prettyDate(entry.created)}}</span></th>
<th>
<span v-if="entry.connected && entry.connected.ip && entry.connected.vpnIp">
VPN: {{entry.connected.vpnIp}}<br>
Remote: {{entry.connected.ip}}
</span>
<span v-else>--</span>
</th>
<th style="text-align: right;">
<button class="btn btn-default btn-sm" type="button" aria-expanded="false" @click="revokeKeyAsk(entry.name)"
data-toggle="tooltip" title="Revoke">
......
......@@ -32,4 +32,8 @@ persist-tun
verb 3
mute 20
status /run/openvpn-status.log
# Hooks to update server status
script-security 2
client-connect /app/code/openvpn-on-client-connect.sh
client-disconnect /app/code/openvpn-on-client-disconnect.sh
"
#!/usr/bin/env bash
curl 'http://127.0.0.1:3000/api/onConnect/' -X POST \
--data-urlencode "cn=${common_name}" \
--data-urlencode "vpnIp=${ifconfig_pool_remote_ip}" \
--data-urlencode "ip=${trusted_ip}" \
--data-urlencode "token@/run/admin-token"
#!/usr/bin/env bash
curl 'http://127.0.0.1:3000/api/onDisconnect/' -X POST \
--data-urlencode "cn=${common_name}" \
--data-urlencode "token@/run/admin-token"
......@@ -8,6 +8,7 @@ const path = require('path')
const compression = require('compression')
const session = require('express-session')
const lastMile = require('connect-lastmile')
const bodyParser = require('body-parser')
const fs = require('fs')
const openvpn = require('./src/openvpn')
const CloudronStrategy = require('passport-cloudron')
......@@ -16,6 +17,8 @@ const LokiStore = require('connect-loki')(session)
const app = express()
const router = new express.Router()
const urlEncodedParser = bodyParser.urlencoded({ extended: true })
const cleanProfile = profile => ({
id: profile.id,
username: profile.username,
......@@ -81,6 +84,8 @@ app.use('/logout', (req, res) => {
req.logout()
res.redirect('/')
})
router.post('/api/onConnect/', urlEncodedParser, openvpn.onClientConnect)
router.post('/api/onDisconnect/', urlEncodedParser, openvpn.onClientDisconnect)
router.get('/api/list/', isAuthenticated, openvpn.list)
router.put('/api/key/*', isAuthenticated, openvpn.createKey)
router.get('/api/key/*', isAuthenticated, openvpn.getKey)
......
......@@ -70,6 +70,8 @@ persist-tun
verb 3
`
const connectedClients = {}
const exec = ({tag, file, args, wantedCode = 0}) => new Promise((resolve, reject) => {
assert.strictEqual(typeof tag, 'string')
assert.strictEqual(typeof file, 'string')
......@@ -107,7 +109,7 @@ const exec = ({tag, file, args, wantedCode = 0}) => new Promise((resolve, reject
})
})
const cleanUserName = userName => userName.toLowerCase().replace(/[^-a-z0-9]+/g, '')
const cleanUserName = userName => userName.toLowerCase().replace(/[^A-Za-z0-9.]+/g, '')
const cleanDeviceName = deviceName => deviceName.replace(/[^A-Za-z0-9\-_]+/g, '')
......@@ -120,17 +122,23 @@ const stat = promisify(fs.stat)
const rm = promisify(fs.unlink)
const readFile = promisify(fs.readFile)
const ADMIN_TOKEN = fs.readFileSync('/run/admin-token', 'utf8')
const listUserKeys = (user) => readdir(keyDir)
.then(files => files.filter(file => file.startsWith(cleanUserName(user) + '-') && file.endsWith('.key')))
.then(files => files.filter(file => file.startsWith(cleanUserName(user) + ':') && file.endsWith('.key')))
.then(files => Promise.all(files.map(file => stat(path.join(keyDir, file))))
.then(stats => files.map((name, i) => ({
name: name.substring(cleanUserName(user).length + 1, name.length).replace(/\.key/, ''),
created: stats[i].birthtime
})))
.then(stats => files.map((fileName, i) => {
const deviceName = fileName.substring(cleanUserName(user).length + 1, fileName.length).replace(/\.key/, '')
return {
name: deviceName,
created: stats[i].birthtime,
connected: (connectedClients[user] && connectedClients[user][deviceName]) || false // object if exists, false if undefined or null
}
}))
)
const list = (req, res, next) => {
const user = req.user && req.user.id ? req.user.id : 'anonymous'
const user = req.user && req.user.username ? req.user.username : 'anonymous'
if (!user) return next(new HttpError(401, 'Unidentified'))
listUserKeys(user)
.then(list => res.status(222).send({entries: list}))
......@@ -138,7 +146,7 @@ const list = (req, res, next) => {
}
const createKey = (req, res, next) => {
const user = req.user && req.user.id ? req.user.id : 'anonymous'
const user = req.user && req.user.username ? req.user.username : 'anonymous'
const deviceName = decodeURIComponent(req.params[0])
if (!user) return next(new HttpError(401, 'Unidentified'))
......@@ -150,7 +158,7 @@ const createKey = (req, res, next) => {
return exec({
tag: 'createUserKey',
file: path.join(baseDir, 'pkitool'),
args: [`${cleanUserName(user)}-${deviceName}`]
args: [`${cleanUserName(user)}:${deviceName}`]
})
.then(() => res.status(201).send({created: deviceName}))
})
......@@ -158,7 +166,7 @@ const createKey = (req, res, next) => {
}
const getKey = (req, res, next) => {
const user = req.user && req.user.id ? cleanUserName(req.user.id) : 'anonymous'
const user = req.user && req.user.username ? cleanUserName(req.user.username) : 'anonymous'
const deviceName = decodeURIComponent(req.params[0])
const format = req.query['format'] || 'conf'
const zip = !(req.query['zip'] === 'false') // default to true
......@@ -186,8 +194,8 @@ const getKey = (req, res, next) => {
listUserKeys(user)
.then(list => {
if (!list.map(e => e.name).includes(deviceName)) return next(new HttpError(404, 'Not Found'))
const certFile = `${user}-${deviceName}.crt`
const keyFile = `${user}-${deviceName}.key`
const certFile = `${user}:${deviceName}.crt`
const keyFile = `${user}:${deviceName}.key`
if (zip) {
res.header('Content-Type', 'application/zip')
......@@ -206,7 +214,7 @@ const getKey = (req, res, next) => {
archive.file(path.join(keyDir, keyFile), {name: internalPathPrefix + keyFile})
archive.file(path.join(keyDir, 'ta.key'), {name: internalPathPrefix + 'ta.key'})
archive.append(
clientConfFile({keyName: `${user}-${deviceName}`}),
clientConfFile({keyName: `${user}:${deviceName}`}),
{name: internalPathPrefix + 'config.' + configExt}
)
archive.finalize()
......@@ -233,7 +241,7 @@ const getKey = (req, res, next) => {
}
const revokeKey = (req, res, next) => {
const user = req.user && req.user.id ? cleanUserName(req.user.id) : 'anonymous'
const user = req.user && req.user.username ? cleanUserName(req.user.username) : 'anonymous'
const deviceName = decodeURIComponent(req.params[0])
if (!user) return next(new HttpError(401, 'Unidentified'))
......@@ -246,15 +254,52 @@ const revokeKey = (req, res, next) => {
return exec({
tag: 'revokeUserKey',
file: path.join(baseDir, 'revoke-full'),
args: [`${cleanUserName(user)}-${deviceName}`],
args: [`${cleanUserName(user)}:${deviceName}`],
wantedCode: 2
})
.then(() => rm(path.join(keyDir, `${cleanUserName(user)}-${deviceName}.key`)))
.then(() => rm(path.join(keyDir, `${cleanUserName(user)}:${deviceName}.key`)))
.then(() => res.status(200).send({revoked: deviceName}))
})
.catch(error => next(new HttpError(500, error)))
}
const onClientConnect = (req, res, next) => {
const cn = req.body['cn']
const ip = req.body['ip']
const vpnIp = req.body['vpnIp']
const token = req.body['token']
if (token !== ADMIN_TOKEN) return next(new HttpError(401, 'Unauthorized'))
if (!cn || !ip || !vpnIp) return next(new HttpError(409, 'Invalid Request'))
const match = /^([A-Za-z0-9.]+):([A-Za-z0-9\-_]+)$/.exec(cn)
if (!match) return next(new HttpError(409, 'Invalid Request'))
const [, user, deviceName] = match
if (!connectedClients[user]) connectedClients[user] = {}
connectedClients[user][deviceName] = {ip, vpnIp}
return res.status(200).send({connected: {user, deviceName, ip, vpnIp}})
}
const onClientDisconnect = (req, res, next) => {
const cn = req.body['cn']
const token = req.body['token']
if (token !== ADMIN_TOKEN) return next(new HttpError(401, 'Unauthorized'))
if (!cn) return next(new HttpError(409, 'Invalid Request'))
const match = /^([A-Za-z0-9.]+):([A-Za-z0-9\-_]+)$/.exec(cn)
if (!match) return next(new HttpError(409, 'Invalid Request'))
const [, user, deviceName] = match
if (connectedClients[user] && connectedClients[user][deviceName]) {
delete connectedClients[user][deviceName]
}
return res.status(200).send({disconnected: {user, deviceName}})
}
const isRunning = () => {
try {
return childProcess.execSync('supervisorctl status openvpn')
......@@ -270,5 +315,7 @@ module.exports = {
createKey,
getKey,
revokeKey,
isRunning
isRunning,
onClientConnect,
onClientDisconnect
}
......@@ -9,6 +9,9 @@ if [ ! -f /app/data/session.secret ]; then
dd if=/dev/urandom bs=256 count=1 | base64 > /app/data/session.secret
fi
# Generate random management token for admin api
dd if=/dev/urandom bs=256 count=1 | base64 > /run/admin-token
export EASY_RSA="/app/code/easyrsa/"
export OPENSSL="openssl"
export PKCS11TOOL="pkcs11-tool"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment