feat(transports): add plain XML-RPC as last fallback (#65)

This commit is contained in:
Julien Fontanet 2017-05-31 18:05:46 +02:00 committed by GitHub
parent 781ffa5574
commit e65dd15edc
5 changed files with 151 additions and 62 deletions

View File

@ -0,0 +1,3 @@
import makeError from 'make-error'
export const UnsupportedTransport = makeError('UnsupportedTransport')

View File

@ -1,29 +1,36 @@
import { InvalidJson } from 'json-rpc-protocol'
import jsonRpc from './json-rpc'
import xmlRpc from './xml-rpc'
import xmlRpcJson from './xml-rpc-json'
import { UnsupportedTransport } from './_utils'
const factories = [ jsonRpc, xmlRpcJson, xmlRpc ]
const { length } = factories
export default opts => {
let call = (jsonRpcCall =>
(method, args) => jsonRpcCall(method, args).then(
result => {
// JSON-RPC seems to be working, remove the test
call = jsonRpcCall
let i = 0
return result
},
error => {
if (error instanceof InvalidJson) {
// fallback to XML-RPC
call = xmlRpc(opts)
let call
function create () {
const current = factories[i++](opts)
if (i < length) {
const currentI = i
call = (method, args) => current(method, args).catch(
error => {
if (error instanceof UnsupportedTransport) {
if (currentI === i) { // not changed yet
create()
}
return call(method, args)
}
return call(method, args)
throw error
}
throw error
}
)
)(jsonRpc(opts))
)
} else {
call = current
}
}
create()
return (method, args) => call(method, args)
}

View File

@ -1,6 +1,8 @@
import httpRequestPlus from 'http-request-plus'
import { format, parse } from 'json-rpc-protocol'
import { UnsupportedTransport } from './_utils'
export default ({ allowUnauthorized, url }) => {
return (method, args) => httpRequestPlus.post(url, {
rejectUnauthorized: !allowUnauthorized,
@ -10,12 +12,27 @@ export default ({ allowUnauthorized, url }) => {
'Content-Type': 'application/json'
},
path: '/jsonrpc'
}).readAll('utf8').then(text => {
const response = parse(text)
if (response.type === 'response') {
return response.result
}
}).readAll('utf8').then(
text => {
let response
try {
response = parse(text)
} catch (error) {
throw new UnsupportedTransport()
}
throw response.error
})
if (response.type === 'response') {
return response.result
}
throw response.error
},
error => {
if (error.response !== undefined) { // HTTP error
throw new UnsupportedTransport()
}
throw error
}
)
}

View File

@ -0,0 +1,97 @@
import { createClient, createSecureClient } from 'xmlrpc'
import { promisify } from 'promise-toolbox'
import { UnsupportedTransport } from './_utils'
const logError = error => {
if (error.res) {
console.error(
'XML-RPC Error: %s (response status %s)',
error.message,
error.res.statusCode
)
console.error('%s', error.body)
}
throw error
}
const SPECIAL_CHARS = {
'\r': '\\r',
'\t': '\\t'
}
const SPECIAL_CHARS_RE = new RegExp(
Object.keys(SPECIAL_CHARS).join('|'),
'g'
)
const parseResult = result => {
const status = result.Status
// Return the plain result if it does not have a valid XAPI
// format.
if (status === undefined) {
return result
}
if (status !== 'Success') {
throw result.ErrorDescription
}
const value = result.Value
// XAPI returns an empty string (invalid JSON) for an empty
// result.
if (value === '') {
return ''
}
try {
return JSON.parse(value)
} catch (error) {
// XAPI JSON sometimes contains invalid characters.
if (!(error instanceof SyntaxError)) {
throw error
}
}
let replaced = false
const fixedValue = value.replace(SPECIAL_CHARS_RE, match => {
replaced = true
return SPECIAL_CHARS[match]
})
if (replaced) {
try {
return JSON.parse(fixedValue)
} catch (error) {
if (!(error instanceof SyntaxError)) {
throw error
}
}
}
throw new UnsupportedTransport()
}
export default ({
allowUnauthorized,
url: { hostname, path, port, protocol }
}) => {
const client = (
protocol === 'https:'
? createSecureClient
: createClient
)({
host: hostname,
path: '/json',
port,
rejectUnauthorized: !allowUnauthorized
})
const call = promisify(client.methodCall, client)
return (method, args) => call(method, args).then(
parseResult,
logError
)
}

View File

@ -14,15 +14,6 @@ const logError = error => {
throw error
}
const SPECIAL_CHARS = {
'\r': '\\r',
'\t': '\\t'
}
const SPECIAL_CHARS_RE = new RegExp(
Object.keys(SPECIAL_CHARS).join('|'),
'g'
)
const parseResult = result => {
const status = result.Status
@ -36,32 +27,7 @@ const parseResult = result => {
throw result.ErrorDescription
}
const value = result.Value
// XAPI returns an empty string (invalid JSON) for an empty
// result.
if (value === '') {
return ''
}
try {
return JSON.parse(value)
} catch (error) {
// XAPI JSON sometimes contains invalid characters.
if (error instanceof SyntaxError) {
let replaced
const fixedValue = value.replace(SPECIAL_CHARS_RE, match => {
replaced = true
return SPECIAL_CHARS[match]
})
if (replaced) {
return JSON.parse(fixedValue)
}
}
throw error
}
return result.Value
}
export default ({
@ -74,7 +40,6 @@ export default ({
: createClient
)({
host: hostname,
path: '/json',
port,
rejectUnauthorized: !allowUnauthorized
})