2017-11-17 16:58:24 -05:00
|
|
|
<!DOCTYPE html>
|
2017-11-15 12:31:02 -05:00
|
|
|
|
|
|
|
<html lang="en">
|
2017-11-17 09:53:06 -05:00
|
|
|
<head>
|
|
|
|
<meta charset="utf-8">
|
|
|
|
|
|
|
|
<title>OpenVPN to ONC</title>
|
|
|
|
<meta name="description" content="Convert OpenVPN config files to ONC files">
|
|
|
|
|
2018-05-19 10:08:34 -04:00
|
|
|
<style>
|
|
|
|
#output {
|
|
|
|
background-color: lightgray;
|
|
|
|
}
|
|
|
|
</style>
|
|
|
|
<script>
|
|
|
|
|
2018-07-02 10:05:12 -04:00
|
|
|
/**
|
|
|
|
* Register the function `handler` to be called when the `Convert` button is
|
|
|
|
* pressed.
|
|
|
|
*/
|
2018-07-02 09:36:31 -04:00
|
|
|
function setHandler () {
|
2018-05-19 10:08:34 -04:00
|
|
|
let clickButton = document.getElementById('clickbutton')
|
|
|
|
clickButton.addEventListener('click', handler, false)
|
|
|
|
}
|
|
|
|
|
2018-07-02 10:05:12 -04:00
|
|
|
/**
|
|
|
|
* Read parameters and pass them to the `main` function. This function is
|
|
|
|
* called when the `Convert` button is clicked.
|
|
|
|
*/
|
2018-07-02 09:36:31 -04:00
|
|
|
function handler () {
|
2018-05-19 10:08:34 -04:00
|
|
|
let selectedFile = document.getElementById('inputopenvpn').files[0]
|
|
|
|
let certificates = document.getElementById('inputcertificates').files
|
|
|
|
let connName = document.getElementById('connname').value
|
|
|
|
let output = document.getElementById('output')
|
|
|
|
main(connName, selectedFile, certificates, output)
|
|
|
|
}
|
|
|
|
|
2018-07-02 10:05:12 -04:00
|
|
|
/**
|
|
|
|
* Read, convert and print result. This function calls other functions
|
|
|
|
* to first read everything, then convert it and finally print the result.
|
|
|
|
*
|
|
|
|
* The function is `async` because it uses `await` when reading files.
|
|
|
|
*
|
|
|
|
* @param {String} connName Name of the connection
|
2018-07-09 06:03:02 -04:00
|
|
|
* @param {File} ovpnFile File object for the ovpn file
|
2018-07-02 10:05:12 -04:00
|
|
|
* @param {Array} certificateFiles List of file objects for the certificates
|
|
|
|
* @param {Object} output HTML element where the output should go
|
|
|
|
*/
|
2018-07-09 06:03:02 -04:00
|
|
|
async function main (connName, ovpnFile, certificateFiles, output) {
|
2018-05-19 10:08:34 -04:00
|
|
|
if (connName === '') {
|
|
|
|
alert('Please specify a name for the connection.')
|
|
|
|
return
|
|
|
|
}
|
2018-07-15 06:44:42 -04:00
|
|
|
console.log(ovpnFile.size + ' bytes')
|
2018-07-09 06:03:02 -04:00
|
|
|
let ovpnContent = await readFile(ovpnFile)
|
|
|
|
let [ovpn, keys] = parseOvpn(ovpnContent)
|
2018-05-19 10:08:34 -04:00
|
|
|
console.log(ovpn)
|
|
|
|
for (const certificateFile of certificateFiles) {
|
|
|
|
keys[certificateFile.name] = await readFile(certificateFile)
|
|
|
|
}
|
|
|
|
let onc = convert(connName, ovpn, keys)
|
|
|
|
output.value = JSON.stringify(onc, null, 2)
|
|
|
|
}
|
|
|
|
|
2018-07-02 10:05:12 -04:00
|
|
|
/**
|
|
|
|
* Return a promise to read a file as text.
|
|
|
|
*
|
|
|
|
* @param {File} file A file object
|
|
|
|
*
|
|
|
|
* @return {Promise} A promise with the contents of the file
|
|
|
|
*/
|
2018-07-02 09:36:31 -04:00
|
|
|
function readFile (file) {
|
2018-05-19 10:08:34 -04:00
|
|
|
return new Promise(resolve => {
|
|
|
|
let reader = new FileReader()
|
2018-07-02 09:36:31 -04:00
|
|
|
reader.onload = e => {
|
2018-05-19 10:08:34 -04:00
|
|
|
// callback and remove windows-style newlines
|
|
|
|
resolve(e.target.result.replace(/\r/g, ''))
|
2018-07-02 09:36:31 -04:00
|
|
|
}
|
2018-05-19 10:08:34 -04:00
|
|
|
// start reading
|
|
|
|
reader.readAsText(file)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2018-07-02 10:05:12 -04:00
|
|
|
/**
|
2018-07-09 06:03:02 -04:00
|
|
|
* Parse an OVPN file. Extract all the key-value pairs and keys
|
2018-07-02 10:05:12 -04:00
|
|
|
* that are written inside XML tags.
|
|
|
|
*
|
|
|
|
* The key-value pairs are written into an object. The keys are also
|
|
|
|
* written into an object with the the XML tag name as the key.
|
|
|
|
*
|
|
|
|
* @param {String} str The contents of the ovpn file as a string.
|
|
|
|
*
|
|
|
|
* @return {Array} An array that contains the key-value pairs and
|
|
|
|
* the keys.
|
|
|
|
*/
|
2018-07-09 06:03:02 -04:00
|
|
|
function parseOvpn (str) {
|
2018-05-19 10:08:34 -04:00
|
|
|
let ovpn = {}
|
|
|
|
let keys = {}
|
2018-07-09 06:03:02 -04:00
|
|
|
// define regexes for properties, opening xml tag and closing xml tag
|
|
|
|
const reProperty = /^([^ ]+)( (.*))?$/i
|
|
|
|
const reXmlOpen = /^<([^\/].*)>$/i
|
|
|
|
const reXmlClose = /^<\/(.*)>$/i
|
|
|
|
|
|
|
|
// temporary variables for handling xml tags
|
2018-05-19 10:08:34 -04:00
|
|
|
let xmlTag = ''
|
|
|
|
let inXml = false
|
|
|
|
let xmlContent = ''
|
2018-07-09 06:03:02 -04:00
|
|
|
|
2018-05-19 10:08:34 -04:00
|
|
|
let lines = str.split(/[\r\n]+/g)
|
|
|
|
|
|
|
|
for (let line of lines) {
|
2018-07-09 06:03:02 -04:00
|
|
|
// skip line if it is empty or begins with '#' or ';'
|
2018-05-19 10:08:34 -04:00
|
|
|
if (!line || line.match(/^\s*[;#]/)) continue
|
2018-07-09 06:03:02 -04:00
|
|
|
if (inXml) { // an XML tag was opened and hasn't been closed yet
|
|
|
|
const xmlMatch = line.match(reXmlClose)
|
2018-05-19 10:08:34 -04:00
|
|
|
if (!xmlMatch) {
|
2018-07-09 06:03:02 -04:00
|
|
|
// no closing tag -> add content to `xmlContent`
|
2018-05-19 10:08:34 -04:00
|
|
|
xmlContent += line + '\n'
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
const tag = xmlMatch[1]
|
|
|
|
if (tag !== xmlTag) {
|
2018-07-09 06:03:02 -04:00
|
|
|
alert('Cannot parse ovpn file.')
|
2018-05-19 10:08:34 -04:00
|
|
|
throw 'bad xml tag'
|
|
|
|
}
|
2018-07-09 06:03:02 -04:00
|
|
|
// closing tag was found
|
|
|
|
// make sure the tag name and the contents are safe
|
|
|
|
const name = makeSafe(xmlTag)
|
|
|
|
const value = makeSafe(xmlContent)
|
|
|
|
// store everything and reset the xml variables
|
2018-05-19 10:08:34 -04:00
|
|
|
keys[name] = value
|
|
|
|
ovpn[name] = name
|
|
|
|
xmlContent = ''
|
|
|
|
inXml = false
|
|
|
|
continue
|
|
|
|
}
|
2018-07-09 06:03:02 -04:00
|
|
|
const xmlMatch = line.match(reXmlOpen)
|
2018-05-19 10:08:34 -04:00
|
|
|
if (xmlMatch) {
|
2018-07-09 06:03:02 -04:00
|
|
|
// an xml tag was opened
|
2018-05-19 10:08:34 -04:00
|
|
|
inXml = true
|
|
|
|
xmlTag = xmlMatch[1]
|
|
|
|
continue
|
|
|
|
}
|
2018-07-09 06:03:02 -04:00
|
|
|
// check if the line contains a property
|
|
|
|
const match = line.match(reProperty)
|
2018-05-19 10:08:34 -04:00
|
|
|
if (!match) continue
|
2018-07-09 06:03:02 -04:00
|
|
|
// make sure everything is safe and then store it
|
|
|
|
const key = makeSafe(match[1])
|
|
|
|
const value = match[2] ? makeSafe((match[3] || '')) : true
|
2018-05-19 10:08:34 -04:00
|
|
|
ovpn[key] = value
|
|
|
|
}
|
|
|
|
|
|
|
|
return [ovpn, keys]
|
|
|
|
}
|
|
|
|
|
|
|
|
function isQuoted (val) {
|
|
|
|
return ((val.charAt(0) === '"' && val.slice(-1) === '"') ||
|
2018-07-02 09:36:31 -04:00
|
|
|
(val.charAt(0) === "'" && val.slice(-1) === "'"))
|
2018-05-19 10:08:34 -04:00
|
|
|
}
|
|
|
|
|
2018-07-09 06:03:02 -04:00
|
|
|
function makeSafe (val, doUnesc) {
|
2018-05-19 10:08:34 -04:00
|
|
|
val = (val || '').trim()
|
|
|
|
if (isQuoted(val)) {
|
|
|
|
// remove the single quotes before calling JSON.parse
|
|
|
|
if (val.charAt(0) === "'") {
|
|
|
|
val = val.substr(1, val.length - 2)
|
|
|
|
}
|
|
|
|
try { val = JSON.parse(val) } catch (_) {}
|
|
|
|
} else {
|
|
|
|
// walk the val to find the first not-escaped ; character
|
|
|
|
var esc = false
|
|
|
|
var unesc = ''
|
|
|
|
for (var i = 0, l = val.length; i < l; i++) {
|
|
|
|
var c = val.charAt(i)
|
|
|
|
if (esc) {
|
|
|
|
if ('\\;#'.indexOf(c) !== -1) {
|
|
|
|
unesc += c
|
|
|
|
} else {
|
|
|
|
unesc += '\\' + c
|
|
|
|
}
|
|
|
|
esc = false
|
|
|
|
} else if (';#'.indexOf(c) !== -1) {
|
|
|
|
break
|
|
|
|
} else if (c === '\\') {
|
|
|
|
esc = true
|
|
|
|
} else {
|
|
|
|
unesc += c
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (esc) {
|
|
|
|
unesc += '\\'
|
|
|
|
}
|
|
|
|
return unesc
|
|
|
|
}
|
|
|
|
return val
|
|
|
|
}
|
|
|
|
|
|
|
|
const oncBasics = {
|
|
|
|
'Type': 'UnencryptedConfiguration',
|
|
|
|
'Certificates': [],
|
|
|
|
'NetworkConfigurations': []
|
|
|
|
}
|
2018-07-09 06:03:02 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Convert the parsed OVPN file to ONC structure
|
|
|
|
*
|
|
|
|
* @param {string} name Name of the connection
|
|
|
|
* @param {Object} ovpn The parsed OVPN file
|
|
|
|
* @param {Object} keys Strings with keys, indexed by key name
|
|
|
|
* @return {Object} The converted ONC structure
|
|
|
|
*/
|
2018-07-02 09:36:31 -04:00
|
|
|
function convert (name, ovpn, keys) {
|
2018-05-19 10:08:34 -04:00
|
|
|
if (!ovpn.client) {
|
|
|
|
console.warn('Is this a server file?')
|
|
|
|
}
|
|
|
|
let params = {}
|
|
|
|
|
|
|
|
// Add certificates
|
|
|
|
let certs = []
|
|
|
|
let [cas, caGuids] = createCerts(keys, ovpn['ca'], 'Authority')
|
|
|
|
params['ServerCARefs'] = caGuids
|
|
|
|
certs = certs.concat(cas)
|
|
|
|
let [clientCerts, clientCertGuids] = createCerts(keys, ovpn['cert'], 'Client')
|
|
|
|
if (clientCerts[0]) {
|
|
|
|
params['ClientCertType'] = 'Ref'
|
|
|
|
params['ClientCertRef'] = clientCertGuids[0]
|
|
|
|
certs.push(clientCerts[0])
|
|
|
|
} else {
|
|
|
|
params['ClientCertType'] = 'None'
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add parameters
|
|
|
|
let remote = ovpn.remote.split(' ')
|
|
|
|
const host = remote[0]
|
|
|
|
if (remote[1]) params['Port'] = remote[1]
|
|
|
|
if (ovpn['auth-user-pass']) params['UserAuthenticationType'] = 'Password'
|
|
|
|
if (ovpn['comp-lzo'] && ovpn['comp-lzo'] !== 'no') {
|
|
|
|
params['CompLZO'] = 'true'
|
|
|
|
} else {
|
|
|
|
params['CompLZO'] = 'false'
|
|
|
|
}
|
|
|
|
if (ovpn['persist-key']) params['SaveCredentials'] = true
|
|
|
|
if (ovpn['tls-auth']) {
|
|
|
|
let authKey = ovpn['tls-auth'].split(' ')
|
|
|
|
let keyString = keys[authKey[0]]
|
|
|
|
if (!keyString) {
|
|
|
|
alert("Please provide the file '" + authKey[0] + "' in 'Certificates and keys'")
|
|
|
|
}
|
|
|
|
params['TLSAuthContents'] = convertKey(keyString)
|
|
|
|
if (authKey[1]) params['KeyDirection'] = authKey[1]
|
|
|
|
}
|
|
|
|
if (ovpn['verify-x509-name']) {
|
|
|
|
params['VerifyX509'] = {
|
|
|
|
'Name': ovpn['verify-x509-name']
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// set parameters if they exist in the ovpn config
|
2018-07-02 09:36:31 -04:00
|
|
|
let conditionalSet = (ovpnName, oncName, type = 'str') => {
|
2018-05-19 10:08:34 -04:00
|
|
|
if (ovpn[ovpnName]) {
|
|
|
|
const raw = ovpn[ovpnName]
|
|
|
|
let value
|
|
|
|
switch (type) {
|
|
|
|
case 'int':
|
|
|
|
value = Number(raw)
|
|
|
|
break
|
|
|
|
default:
|
|
|
|
value = raw
|
|
|
|
}
|
|
|
|
params[oncName] = value
|
|
|
|
}
|
|
|
|
}
|
|
|
|
conditionalSet('port', 'Port', 'int')
|
|
|
|
conditionalSet('proto', 'Proto')
|
|
|
|
conditionalSet('key-direction', 'KeyDirection')
|
|
|
|
conditionalSet('remote-cert-tls', 'RemoteCertTLS')
|
|
|
|
conditionalSet('cipher', 'Cipher')
|
|
|
|
conditionalSet('auth', 'Auth')
|
|
|
|
conditionalSet('auth-retry', 'AuthRetry')
|
|
|
|
conditionalSet('reneg-sec', 'RenegSec', 'int')
|
|
|
|
|
|
|
|
// Put together network configuration
|
|
|
|
let config = {
|
|
|
|
'GUID': `{${uuidv4()}}`,
|
|
|
|
'Name': name,
|
|
|
|
'Type': 'VPN',
|
|
|
|
'VPN': {
|
|
|
|
'Type': 'OpenVPN',
|
|
|
|
'Host': host,
|
|
|
|
'OpenVPN': params
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Put everything together
|
|
|
|
let onc = Object.assign({}, oncBasics) // create copy
|
|
|
|
onc.NetworkConfigurations = [config]
|
|
|
|
onc.Certificates = certs
|
|
|
|
return onc
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create UUID (from Stackoverflow).
|
|
|
|
*/
|
2018-07-02 09:36:31 -04:00
|
|
|
function uuidv4 () {
|
|
|
|
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
|
|
|
|
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
|
|
|
|
)
|
2018-05-19 10:08:34 -04:00
|
|
|
}
|
2018-07-09 06:03:02 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Replace newlines with explicit `\n` and filter out comments
|
|
|
|
*/
|
2018-07-02 09:36:31 -04:00
|
|
|
function convertKey (key) {
|
2018-05-19 10:08:34 -04:00
|
|
|
let lines = key.split(/\n/g)
|
|
|
|
let out = ''
|
|
|
|
for (let line of lines) {
|
|
|
|
// filter out empty lines and lines with comments
|
|
|
|
if (!line || line.match(/^\s*[;#]/)) continue
|
|
|
|
out += line + '\n'
|
|
|
|
}
|
|
|
|
return out
|
|
|
|
}
|
2018-07-09 06:03:02 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Find all certificates in a string and extract them
|
|
|
|
*/
|
2018-07-02 09:36:31 -04:00
|
|
|
function extractCas (str) {
|
2018-05-19 10:08:34 -04:00
|
|
|
let splits = str.replace(/\n/g, '').split('-----BEGIN CERTIFICATE-----')
|
|
|
|
console.log(splits)
|
|
|
|
let cas = []
|
|
|
|
for (const s of splits) {
|
|
|
|
if (s.includes('-----END CERTIFICATE-----')) {
|
|
|
|
cas.push(s.split('-----END CERTIFICATE-----')[0])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return cas
|
|
|
|
}
|
|
|
|
|
2018-07-02 09:36:31 -04:00
|
|
|
function createCerts (keys, certName, certType) {
|
2018-05-19 10:08:34 -04:00
|
|
|
let certs = []
|
|
|
|
let certGuids = []
|
|
|
|
if (certName) {
|
|
|
|
let cert = keys[certName]
|
|
|
|
if (!cert) {
|
|
|
|
alert("Please provide the file '" + certName + "' in 'Certificates and keys'")
|
|
|
|
}
|
|
|
|
let rawCerts = extractCas(cert)
|
|
|
|
for (const cert of rawCerts) {
|
|
|
|
const guid = `{${uuidv4()}}`
|
|
|
|
certGuids.push(guid)
|
|
|
|
certs.push({
|
|
|
|
'GUID': guid,
|
|
|
|
'Type': certType,
|
|
|
|
'X509': cert
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return [certs, certGuids]
|
|
|
|
}
|
|
|
|
</script>
|
2017-11-17 09:53:06 -05:00
|
|
|
</head>
|
|
|
|
|
2018-05-19 10:08:34 -04:00
|
|
|
<body onload="setHandler()">
|
2017-11-17 09:53:06 -05:00
|
|
|
<div>
|
|
|
|
<h1>ovpn2onc</h1>
|
|
|
|
<ul>
|
2018-07-02 10:05:12 -04:00
|
|
|
<li><label for="connname">Name for connection (can be chosen freely):</label> <input type="text" id="connname"></li>
|
|
|
|
<li><label for="inputopenvpn">OpenVPN config file (*.ovpn):</label> <input type="file" id="inputopenvpn"></li>
|
2018-05-19 10:08:34 -04:00
|
|
|
<li><label for="inputcertificates">Certificates and keys (can be multiple files):</label> <input type="file" id="inputcertificates" multiple></li>
|
2017-11-17 09:53:06 -05:00
|
|
|
</ul>
|
|
|
|
<button id="clickbutton" type="button">Convert</button>
|
|
|
|
</div>
|
|
|
|
<div>
|
2018-05-19 10:08:34 -04:00
|
|
|
<p>Output (copy this into a new file and load it in ChromeOS)</p>
|
|
|
|
<textarea readonly id="output" wrap="off" rows="20" cols="100"></textarea>
|
2017-11-17 09:53:06 -05:00
|
|
|
</div>
|
|
|
|
</body>
|
2017-11-15 12:31:02 -05:00
|
|
|
</html>
|