openvpn
Version:
openvpn is a easy to use module that exposes endpoints to configure openvpn on any linux system
387 lines (345 loc) • 14.7 kB
text/coffeescript
# validation is used by other modules
fileops = require 'fileops'
validate = require('json-schema').validate
exec = require('child_process').exec
uuid = require 'node-uuid'
@db = db =
server: require('dirty') '/tmp/openvpnservers.db'
client: require('dirty') '/tmp/openvpnclients.db'
user: require('dirty') '/tmp/openvpnusers.db'
db.user.on 'load', ->
console.log 'loaded openvpnusers.db'
db.user.forEach (key,val) ->
console.log 'found ' + key
@lookup = lookup = (id) ->
console.log "looking up user ID: #{id}"
entry = db.user.get id
if entry
if userschema?
console.log 'performing schema validation on retrieved user entry'
result = validate entry, userschema
console.log result
return new Error "Invalid user retrieved: #{result.errors}" unless result.valid
return entry
else
return new Error "No such user ID: #{id}"
clientSchema =
name: "openvpn"
type: "object"
additionalProperties: false
properties:
pull: {"type":"boolean", "required":true}
'tls-client': {"type":"boolean", "required":true}
dev: {"type":"string", "required":true}
proto: {"type":"string", "required":true}
ca: {"type":"string", "required":true}
dh: {"type":"string", "required":true}
cert: {"type":"string", "required":true}
key: {"type":"string", "required":true}
remote: {"type":"string", "required":true}
cipher: {"type":"string", "required":false}
'tls-cipher': {"type":"string", "required":false}
route:
items: { type: "string" }
push:
items: { type: "string" }
'persist-key': {"type":"boolean", "required":false}
'persist-tun': {"type":"boolean", "required":false}
status: {"type":"string", "required":false}
'comp-lzo': {"type":"string", "required":false}
verb: {"type":"number", "required":false}
mlock: {"type":"boolean", "required":false}
userSchema =
name: "openvpn"
type: "object"
additionalProperties: false
properties:
id: { type: "string", required: true }
email: { type: "string", required: false}
cname: { type: "string", required: false}
push:
items: { type: "string" }
# testing openvpn validation with test schema
serverSchema =
name: "openvpn"
type: "object"
additionalProperties: false
properties:
port: {"type":"number", "required":true}
dev: {"type":"string", "required":true}
proto: {"type":"string", "required":true}
ca: {"type":"string", "required":true}
dh: {"type":"string", "required":true}
cert: {"type":"string", "required":true}
key: {"type":"string", "required":true}
server: {"type":"string", "required":true}
'script-security': {"type":"string", "required":false}
multihome: {"type":"boolean", "required":false}
management: {"type":"string", "required":false}
cipher: {"type":"string", "required":false}
'tls-cipher': {"type":"string", "required":false}
auth: {"type":"string", "required":false}
topology: {"type":"string", "required":false}
'route-gateway': {"type":"string", "required":false}
'client-config-dir': {"type":"string", "required":false}
'ccd-exclusive': {"type":"boolean", "required":false}
'client-to-client': {"type":"boolean", "required":false}
route:
items: { type: "string" }
push:
items: { type: "string" }
'max-clients': {"type":"number", "required":false}
'persist-key': {"type":"boolean", "required":false}
'persist-tun': {"type":"boolean", "required":false}
status: {"type":"string", "required":false}
keepalive: {"type":"string", "required":false}
'comp-lzo': {"type":"string", "required":false}
sndbuf: {"type":"number", "required":false}
rcvbuf: {"type":"number", "required":false}
txqueuelen: {"type":"number", "required":false}
'replay-window': {"type":"string", "required":false}
verb: {"type":"number", "required":false}
mlock: {"type":"boolean", "required":false}
class vpnlib
constructor: ->
console.log 'vpnlib initialized'
@clientdb = db.client
@serverdb = db.server
@serverdb.on 'load', ->
console.log 'loaded openvpnserver.db'
@forEach (key,val) ->
console.log 'found ' + key
@clientdb.on 'load', ->
console.log 'loaded openvpnclient.db'
@forEach (key,val) ->
console.log 'found ' + key
console.log 'dbs ' + @clientdb + @serverdb
getCcdPath: (entry) ->
console.log entry.config
return entry.config["client-config-dir"]
getServerEntryByID: (id) ->
entry = @serverdb.get id
if entry
return entry
else
return new Error "Invalid ID posting! #{id}"
getMgmtPort: (entry) ->
console.log 'entry is ' + entry.config
console.log 'management ip port is ' + entry.config.management
port = entry.config.management.split(" ")
return port[1]
getStatusFile: (entry) ->
console.log 'status file is ' + entry.status
return entry.config.status
new: (config) ->
instance = {}
instance.id = uuid.v4()
instance.config = config
#instance.config.id ?= uuid.v4()
return instance
configvpn: (instance, filename, idb, callback) ->
console.log 'idb is ' + idb
service = "openvpn"
config = ''
for key, val of instance.config
switch (typeof val)
when "object"
if val instanceof Array
for i in val
config += "#{key} #{i}\n" if key is "route"
config += "#{key} \"#{i}\"\n" if key is "push"
when "number", "string"
config += key + ' ' + val + "\n"
when "boolean"
config += key + "\n"
console.log 'writing vpn config onto file' + filename
fileops.createFile filename, (result) ->
return new Error "Unable to create configuration file #{filename}!" if result instanceof Error
fileops.updateFile filename, config
exec "touch /config/#{service}/on"
try
idb.set instance.id, instance, ->
console.log "#{instance.id} added to OpenVPN service configuration"
callback({result:true})
catch err
console.log err
callback(err)
addUser: (body, filename, callback) ->
service = "openvpn"
config = ''
for key, val of body
switch (typeof val)
when "object"
if val instanceof Array
for i in val
config += "#{key} #{i}\n" if key is "iroute"
config += "#{key} \"#{i}\"\n" if key is "push"
id = body.id
fileops.createFile filename, (err) ->
return new Error "Unable to create configuration file #{filename}!" if err instanceof Error
fileops.updateFile filename, config
try
'''
TODO: implement a module to act on service
'''
exec "svcs #{service.description.name} sync"
db.user.set id, body, ->
console.log "#{id} added to OpenVPN service configuration"
console.log body
callback({result: true })
catch err
callback(err)
delInstance: (id, idb, filename, callback) ->
entry = idb.get id
console.log 'filename to be removed ' + filename
#spawnvpn takes care of killing openvpn instance.
#To keep it generic, we need to call service module to stop this process
#service module should have mapping with id to process id
fileops.removeFile filename, (err) =>
console.log 'result of removing file ' + err
unless err instanceof Error
idb.rm id, =>
console.log "removed VPN client ID: #{id}"
callback(true)
else
error = new Error "Unable to delete the instance #{id}! #{err}" if err instanceof Error
callback (error)
delUser: (userid, ccdpath, callback) ->
entry = db.user.get userid
try
throw new Error "user does not exist!" unless entry
if entry.email
file = entry.email
else
file = entry.cname
filename = "#{ccdpath}" + "/#{file}"
console.log "removing user config on #{filename}..."
fileops.fileExists filename, (exists) ->
if not exists
console.log 'file removed already'
err = new Error "user is already removed!"
callback(err)
else
console.log 'remove the file'
fileops.removeFile filename, (err) ->
if err
callback(err)
else
console.log 'removed file'
db.user.rm userid, ->
console.log "removed VPN user ID: #{userid}"
callback(true)
catch err
callback(err)
listServers: ->
res = {"servers":[]}
@serverdb.forEach (key,val) ->
console.log 'found server ' + key
res.servers.push val
console.log 'listing'
return res.servers
listClientByID: (key) ->
entry = @clientdb.get key
return new Error "Entry with the given key #{key} does not exist" unless entry
return entry
listClients: ->
res = {"clients":[]}
@clientdb.forEach (key,val) ->
console.log 'found client ' + key
res.clients.push val unless key == "management"
console.log 'listing'
return res.clients
getInfo: (port, filename, id, callback) ->
console.log 'in getInfo'
res =
id: id
users: []
connections: []
db.user.forEach (key,val) ->
console.log 'found ' + key
res.users.push val
# TODO: should retrieve the openvpn configuration and inspect "management" and "status" property
Lazy = require 'lazy'
status = new Lazy
status
.lines
.map(String)
.filter (line) ->
not (
/^OpenVPN/.test(line) or
/^Updated/.test(line) or
/^Common/.test(line) or
/^ROUTING/.test(line) or
/^Virtual/.test(line) or
/^GLOBAL/.test(line) or
/^UNDEF/.test(line) or
/^END/.test(line) or
/^Max bcast/.test(line))
.map (line) ->
#console.log "lazy: #{line}"
return line.trim().split ','
.forEach (fields) ->
switch fields.length
when 5
res.connections.push {
cname: fields[0]
remote: fields[1]
received: fields[2]
sent: fields[3]
since: fields[4]
}
when 4
for conn in res.connections
if conn.cname is fields[1]
conn.ip = fields[0]
.join =>
console.log res
callback(res)
console.log "checking for live connections..."
# OPENVPN MGMT API v1
net = require 'net'
conn = net.connect port, '127.0.0.1', ->
console.log 'connection to openvpn mgmt successful!'
response = ''
@setEncoding 'ascii'
@on 'prompt', =>
@write "status\n"
@on 'response', =>
console.log "response: #{response}"
status.emit 'end'
@write "exit\n"
@end
@on 'data', (data) =>
console.log "read: "+data+"\n"
if /^>/.test(data)
@emit 'prompt'
else
response += data
status.emit 'data',data
if /^END$/gm.test(response)
@emit 'response'
@on 'end', =>
console.log 'connection to openvpn mgmt ended!'
status.emit 'end'
@end
# When we CANNOT make a connection to OPENVPN MGMT port, we fallback to checking file
conn.on 'error', (error) ->
console.log error
statusfile = filename # hard-coded for now...
console.log "failling back to processing #{statusfile}..."
#statusfile = "openvpn-status.log" # hard-coded for now...
fs = require 'fs'
stream = fs.createReadStream statusfile, encoding: 'utf8'
stream.on 'open', ->
console.log "sending #{statusfile} to lazy status..."
stream.on 'data', (data) ->
status.emit 'data',data
stream.on 'end', ->
status.emit 'end'
stream.on 'error', (error) ->
console.log error
status.emit 'end'
module.exports = vpnlib
module.exports.clientSchema = clientSchema
module.exports.serverSchema = serverSchema
module.exports.userSchema = userSchema