Skip to content

Commit e6d87f9

Browse files
line-oduncdrum
authored andcommitted
feat(connection): align connection options
Both REST and XML-RPC now have the same set of default options. The boolean option `secure` can still be used to control XMLRPC client, but `protocol` is the preferred way. This allows to use the same set of options for both client constructors. Add test to ensure legacy option is still working.
1 parent 6399491 commit e6d87f9

File tree

2 files changed

+131
-78
lines changed

2 files changed

+131
-78
lines changed

components/connection.js

Lines changed: 124 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -8,116 +8,163 @@ const promisedMethodCall = require('./promisedMethodCall')
88

99
/**
1010
* @typedef {Object} NodeExistConnectionOptions
11+
* @prop {{user:string, pass:string}} [basic_auth] database user credentials, default: {"user":"guest","pass":"guest"}
12+
* @prop {"http:"|"https:"} [protocol] "http:" or "https:", default: "https:"
1113
* @prop {string} [host] database host, default: "localhost"
1214
* @prop {string} [port] database port, default: "8443"
13-
* @prop {boolean} [secure] use HTTPS? default: true
14-
* @prop {boolean} [rejectUnauthorized] enforce valid SSL connection, default: true
1515
* @prop {string} [path] path to XMLRPC, default: "/exist/xmlrpc"
16-
* @prop {{user:string, pass:string}} [basic_auth] database user credentials, default: {"user":"guest","pass":"guest"}
16+
* @prop {boolean} [rejectUnauthorized] enforce valid SSL certs, default: true for remote hosts
17+
*/
18+
19+
/**
20+
* @typedef {Object} MergedOptions
21+
* @prop {{user:string, pass:string}} basic_auth database user credentials
22+
* @prop {"http:"|"https:"} protocol "http:" or "https:"
23+
* @prop {string} host database host
24+
* @prop {string} port database port
25+
* @prop {string} path path to XMLRPC, default: "/exist/xmlrpc"
26+
* @prop {boolean} [rejectUnauthorized] enforce valid SSL certs, if https: is used
27+
*/
28+
29+
/**
30+
* Default REST endpoint
31+
* @type {string}
1732
*/
33+
const defaultRestEndpoint = '/exist/rest'
34+
35+
/**
36+
* Default XML-RPC endpoint
37+
* @type {string}
38+
*/
39+
const defaultXmlrpcEndpoint = '/exist/xmlrpc'
1840

1941
/**
2042
* Default connection options
2143
* @type {NodeExistConnectionOptions}
2244
*/
23-
const defaultRPCoptions = {
24-
host: 'localhost',
25-
port: '8443',
26-
path: '/exist/xmlrpc',
45+
const defaultConnectionOptions = {
2746
basic_auth: {
2847
user: 'guest',
2948
pass: 'guest'
30-
}
31-
}
32-
33-
const defaultRestOptions = {
34-
host: 'localhost',
49+
},
3550
protocol: 'https:',
36-
port: '8443',
37-
path: '/exist/rest',
38-
basic_auth: {
39-
user: 'guest',
40-
pass: 'guest'
41-
}
42-
}
43-
44-
function isLocalDB (host) {
45-
return (
46-
host === 'localhost' ||
47-
host === '127.0.0.1' ||
48-
host === '[::1]'
49-
)
51+
host: 'localhost',
52+
port: '8443'
5053
}
5154

52-
function useSecureConnection (options) {
53-
if (options && 'secure' in options) {
54-
return Boolean(options.secure)
55+
/**
56+
* get REST client
57+
* @param {NodeExistConnectionOptions} [options] connection options
58+
* @returns {got} Extended HTTP client instance
59+
*/
60+
async function restConnection (options) {
61+
const { got } = await import('got')
62+
/* eslint camelcase: "off" */
63+
const { basic_auth, protocol, host, port, path, rejectUnauthorized } = mergeOptions(defaultRestEndpoint, options)
64+
65+
const prefixUrl = protocol + '//' + host + (port ? ':' + port : '') + path
66+
67+
const httpClientOptions = {
68+
prefixUrl,
69+
headers: {
70+
'user-agent': 'node-exist',
71+
authorization: basicAuth(basic_auth)
72+
},
73+
https: { rejectUnauthorized }
5574
}
56-
return true
75+
76+
return got.extend(httpClientOptions)
5777
}
5878

59-
function basicAuth (name, pass) {
60-
const payload = pass ? `${name}:${pass}` : name
79+
/**
80+
* Basic authorization header value
81+
* @prop {{user:string, pass:string}} auth database user credentials
82+
* @returns {string} header value
83+
*/
84+
function basicAuth (auth) {
85+
const payload = auth.pass ? `${auth.user}:${auth.pass}` : auth.user
6186
return 'Basic ' + Buffer.from(payload).toString('base64')
6287
}
6388

6489
/**
6590
* Connect to database via XML-RPC
66-
* @param {NodeExistConnectionOptions} options
91+
* @param {NodeExistConnectionOptions} [options] connection options
6792
* @returns {XMLRPCClient} XMLRPC-client
6893
*/
6994
function connect (options) {
70-
const _options = assign({}, defaultRPCoptions, options)
71-
delete _options.secure // prevent pollution of XML-RPC options
72-
73-
let client
74-
if (useSecureConnection(options)) {
75-
// allow invalid and self-signed certificates on localhost, if not explicitly
76-
// enforced by setting options.rejectUnauthorized to true
77-
_options.rejectUnauthorized = ('rejectUnauthorized' in _options)
78-
? _options.rejectUnauthorized
79-
: !isLocalDB(_options.host)
80-
81-
client = xmlrpc.createSecureClient(_options)
82-
} else {
83-
if (!isLocalDB(_options.host)) {
84-
console.warn('Connecting to DB using an unencrypted channel.')
85-
}
86-
client = xmlrpc.createClient(_options)
87-
}
95+
const mergedOptions = mergeOptions(defaultXmlrpcEndpoint, options)
96+
const client = getXMLRPCClient(mergedOptions)
8897
client.promisedMethodCall = promisedMethodCall(client)
8998
return client
9099
}
91100

92-
async function restConnection (options) {
93-
const { got } = await import('got')
94-
const _options = assign({}, defaultRestOptions, options)
95-
const authorization = basicAuth(_options.basic_auth.user, _options.basic_auth.pass)
101+
/**
102+
*
103+
* @param {MergedOptions} options
104+
* @returns {XMLRPCClient} XMLRPC-client
105+
*/
106+
function getXMLRPCClient (options) {
107+
if (useSecureConnection(options.protocol)) {
108+
return xmlrpc.createSecureClient(options)
109+
}
110+
return xmlrpc.createClient(options)
111+
}
112+
113+
/**
114+
* Merge options with defaults
115+
*
116+
* Allow invalid and self-signed certificates on localhost,
117+
* if not explicitly set to be enforced.
118+
* @param {string} path default endpoint
119+
* @param {NodeExistConnectionOptions} [options] given options
120+
* @returns {MergedOptions} merged options
121+
*/
122+
function mergeOptions (path, options) {
123+
const mergedOptions = assign({ path }, defaultConnectionOptions, options)
96124

97-
const rejectUnauthorized = ('rejectUnauthorized' in _options)
98-
? _options.rejectUnauthorized
99-
: !isLocalDB(_options.host)
125+
// compatibility for older setups
126+
if ('secure' in mergedOptions) {
127+
mergedOptions.protocol = mergedOptions.secure ? 'https:' : 'http:'
128+
delete mergedOptions.secure // remove legacy option
129+
}
100130

101-
if (!isLocalDB(_options.host) && _options.protocol === 'http') {
102-
console.warn('Connecting to remote DB using an unencrypted channel.')
131+
const isLocalDb = checkIfLocalHost(mergedOptions.host)
132+
const isSecureClient = useSecureConnection(mergedOptions.protocol)
133+
if (isLocalDb && isSecureClient && !('rejectUnauthorized' in mergedOptions)) {
134+
mergedOptions.rejectUnauthorized = false
103135
}
104136

105-
const port = _options.port ? ':' + _options.port : ''
106-
const path = _options.path.startsWith('/') ? _options.path : '/' + _options.path
107-
const prefixUrl = `${_options.protocol}//${_options.host}${port}${path}`
108-
109-
const client = got.extend(
110-
{
111-
prefixUrl,
112-
headers: {
113-
'user-agent': 'node-exist',
114-
authorization
115-
},
116-
https: { rejectUnauthorized }
137+
if (!isLocalDb) {
138+
if (!isSecureClient) {
139+
console.warn('Connecting to remote DB using an unencrypted channel.')
140+
}
141+
if (!mergedOptions.rejectUnauthorized) {
142+
console.warn('Connecting to remote DB allowing invalid certificate.')
117143
}
144+
}
145+
return mergedOptions
146+
}
147+
148+
/**
149+
* Is the host considered a local host
150+
* @param {string} host hostname
151+
* @returns {boolean} true, if host is local
152+
*/
153+
function checkIfLocalHost (host) {
154+
return (
155+
host === 'localhost' ||
156+
host === '127.0.0.1' || // TODO: 127.0.1.1 is also local
157+
host === '[::1]' // TODO: match all ipv6 addresses considered local
118158
)
159+
}
119160

120-
return client
161+
/**
162+
* SSL or not?
163+
* @param {string} protocol must end in colon
164+
* @returns {boolean} true, if encrypted connection
165+
*/
166+
function useSecureConnection (protocol) {
167+
return protocol === 'https:'
121168
}
122169

123170
/**
@@ -144,10 +191,9 @@ function readOptionsFromEnv () {
144191
throw new Error('Unknown protocol: "' + protocol + '"!')
145192
}
146193

147-
environmentOptions.secure = protocol === 'https:'
194+
environmentOptions.protocol = protocol
148195
environmentOptions.host = hostname
149196
environmentOptions.port = port
150-
environmentOptions.protocol = protocol
151197
}
152198

153199
return environmentOptions
@@ -157,6 +203,7 @@ module.exports = {
157203
connect,
158204
readOptionsFromEnv,
159205
restConnection,
160-
defaultRPCoptions,
161-
defaultRestOptions
206+
defaultConnectionOptions,
207+
defaultXmlrpcEndpoint,
208+
defaultRestEndpoint
162209
}

spec/tests/index.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,13 @@ test('create connection with default settings', function (t) {
5959
t.end()
6060
})
6161

62-
test('create connection using http://', function (t) {
62+
test('create connection using http:', function (t) {
63+
const db = connect({ protocol: 'http:', port: 8080 })
64+
t.equal(db.client.isSecure, false, 'insecure client used')
65+
t.end()
66+
})
67+
68+
test('create insecure client using legacy option', function (t) {
6369
const db = connect({ secure: false, port: 8080 })
6470
t.equal(db.client.isSecure, false, 'insecure client used')
6571
t.end()

0 commit comments

Comments
 (0)