UNPKG

14.8 kBJavaScriptView Raw
1/**
2 * Copyright 2016-2018 F5 Networks, Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17'use strict';
18
19const Logger = require('./logger');
20const http = require('http');
21const https = require('https');
22const q = require('q');
23
24/**
25 * Creates an IControl object
26 * @class
27 *
28 * @param {Object} [options] - Options for the creation.
29 * @param {String} [options.host] - IP Address to connect to. Default 127.0.0.1
30 * @param {Number} [options.port] - Port to use. Default 443.
31 * @param {String} [options.user] - User to use for auth. Default admin.
32 * @param {String} [options.password] - Password to use for auth. Default admin.
33 * @param {String} [options.authToken] - Auth token to use rather than user and password.
34 * @param {String} [options.basePath] - Base path to prepend to paths for all requests. Default /mgmt
35 * @param {Boolean} [options.strict] - Whether or not to validate SSL certificates.
36 * @param {Object} [options.logger] - Logger to use. Or, pass loggerOptions to get your own logger.
37 * @param {Object} [options.loggerOptions] - Options for the logger.
38 * See {@link module:logger.getLogger} for details.
39 */
40function IControl(options) {
41 const opts = options || {};
42
43 const logger = options ? options.logger : undefined;
44 let loggerOptions = options ? options.loggerOptions : undefined;
45
46 if (logger) {
47 this.logger = logger;
48 } else {
49 loggerOptions = loggerOptions || { logLevel: 'none' };
50 loggerOptions.module = module;
51 this.logger = Logger.getLogger(loggerOptions);
52 }
53
54 // Set default options
55 this.host = opts.host || '127.0.0.1';
56 this.port = opts.port || 443;
57 this.user = opts.user || 'admin';
58 this.password = opts.password || 'admin';
59 this.authToken = opts.authToken;
60 this.basePath = opts.basePath || getBasePath.call(this, opts);
61 this.strict = typeof opts.strict !== 'undefined' ? opts.strict : true;
62
63 if (this.host !== 'localhost' || this.port !== 8100) {
64 this.auth = `${this.user}:${this.password}`;
65 }
66
67 this.http = http;
68 this.https = https;
69}
70
71/**
72 * Executes a list (GET) request
73 *
74 * @param {String} path - Path for the request.
75 * @param {Object} [options] - Options for the request.
76 * @param {Boolean} [options.noWait] - Don't wait for a response. Default false (wait for a response).
77 * @param {Object} [options.headers] - Headers to use in the request. Default
78 * {
79 * 'Content-Type': 'application/json'
80 * }
81 *
82 * @returns {Promise} A promise which is resolved with the results of the request
83 * or rejected if an error occurs. If the response is JSON that
84 * has 'items' in it, only the items are returned.
85 */
86IControl.prototype.list = function list(path, options) {
87 return this.request('GET', path, undefined, options);
88};
89
90/**
91 * Executes a create (POST) request
92 *
93 * @param {String} path - Path for the request.
94 * @param {Object} [body] - Body of the request.
95 * @param {Object} [options] - Options for the request.
96 * @param {Boolean} [options.noWait] - Don't wait for a response. Default false (wait for a response).
97 * @param {Object} [options.headers] - Headers to use in the request. Default
98 * {
99 * 'Content-Type': 'application/json'
100 * }
101 *
102 * @returns {Promise} A promise which is resolved with the results of the request
103 * or rejected if an error occurs.
104 */
105IControl.prototype.create = function create(path, body, options) {
106 return this.request('POST', path, body, options);
107};
108
109/**
110 * Executes a modify (PATCH) request
111 *
112 * @param {String} path - Path for the request.
113 * @param {Object} [body] - Body of the request.
114 * @param {Object} [options] - Options for the request.
115 * @param {Boolean} [options.noWait] - Don't wait for a response. Default false (wait for a response).
116 * @param {Object} [options.headers] - Headers to use in the request. Default
117 * {
118 * 'Content-Type': 'application/json'
119 * }
120 *
121 * @returns {Promise} A promise which is resolved with the results of the request
122 * or rejected if an error occurs.
123 */
124IControl.prototype.modify = function modify(path, body, options) {
125 return this.request('PATCH', path, body, options);
126};
127
128
129/**
130 * Executes a replace (PUT) request
131 *
132 * @param {String} path - Path for the request.
133 * @param {Object} [body] - Body of the request.
134 * @param {Object} [options] - Options for the request.
135 * @param {Boolean} [options.noWait] - Don't wait for a response. Default false (wait for a response).
136 * @param {Object} [options.headers] - Headers to use in the request. Default
137 * {
138 * 'Content-Type': 'application/json'
139 * }
140 *
141 * @returns {Promise} A promise which is resolved with the results of the request
142 * or rejected if an error occurs.
143 */
144IControl.prototype.replace = function replace(path, body, options) {
145 return this.request('PUT', path, body, options);
146};
147
148/**
149 * Executes a delete (DELETE) request
150 *
151 * @param {String} path - Path for the request.
152 * @param {Object} [body] - Body of the request.
153 * @param {Object} [options] - Options for the request.
154 * @param {Boolean} [options.noWait] - Don't wait for a response. Default false (wait for a response).
155 * @param {Object} [options.headers] - Headers to use in the request. Default
156 * {
157 * 'Content-Type': 'application/json'
158 * }
159 *
160 * @returns {Promise} A promise which is resolved with the results of the request
161 * or rejected if an error occurs. If the response is JSON that
162 * has 'items' in it, only the items are returned.
163 */
164IControl.prototype.delete = function deletex(path, body, options) {
165 return this.request('DELETE', path, body, options);
166};
167
168IControl.prototype.setAuthToken = function setAuthToken(authToken) {
169 this.authToken = authToken;
170};
171
172IControl.prototype.setRefreshToken = function setRefreshToken(refreshToken) {
173 this.refreshToken = refreshToken;
174};
175
176/**
177 * Executes a request
178 *
179 * @param {String} method - HTTP method for the request.
180 * @param {String} path - Path for the request.
181 * @param {Object} [body] - Body of the request.
182 * @param {Object} [options] - Options for the request.
183 * @param {Boolean} [options.noWait] - Don't wait for a response. Default false (wait for a response).
184 * @param {Object} [options.headers] - Headers to use in the request.
185 *
186 * {
187 * 'Content-Type': 'application/json'
188 * }
189 *
190 * @param {Object} [existingDeferred] - An existing deferred to use instead of creating a new one. Used
191 * for recursion (for retrying the request).
192 *
193 * @param {Boolean} [options.noRefresh] - Do not try to refresh the auth token if the request gets a 401
194 *
195 * @returns {Promise} A promise which is resolved with the results of the request
196 * or rejected if an error occurs.
197 */
198IControl.prototype.request = function request(method, path, body, options, existingDeferred) {
199 const deferred = existingDeferred || q.defer();
200 const requestOptions = {
201 method,
202 hostname: this.host,
203 port: this.port,
204 path: this.basePath + path,
205 rejectUnauthorized: this.strict,
206 headers: {
207 'Content-Type': 'application/json'
208 }
209 };
210
211 if (this.authToken) {
212 requestOptions.headers['X-F5-Auth-Token'] = this.authToken;
213 } else {
214 requestOptions.auth = this.auth;
215 }
216
217 const noWait = options ? options.noWait : undefined;
218 const headers = options ? options.headers : undefined;
219 const noRefresh = options ? options.noRefresh : undefined;
220
221 let mungedBody = body;
222
223 // Add any headers that were specified
224 if (headers) {
225 Object.keys(headers).forEach((header) => {
226 if (header.toLowerCase() === 'content-type') {
227 requestOptions.headers['Content-Type'] = headers[header];
228 } else {
229 requestOptions.headers[header] = headers[header];
230 }
231 });
232 }
233
234 if (requestOptions.headers['Content-Type'] === 'application/json') {
235 if (mungedBody) {
236 mungedBody = JSON.stringify(body);
237 }
238 }
239
240 if (mungedBody) {
241 requestOptions.headers['Content-Length'] = Buffer.byteLength(mungedBody);
242 }
243
244 const responseHandler = noWait ? undefined : (response) => {
245 let totalResponse = '';
246 response.on('data', (chunk) => {
247 totalResponse += chunk;
248 });
249
250 response.on('end', () => {
251 const responseHeaders = response.headers;
252 let parsedResponse;
253 let contentType;
254
255 if (responseHeaders['content-type']
256 && responseHeaders['content-type'].indexOf('application/json') !== -1
257 ) {
258 contentType = 'application/json';
259 try {
260 parsedResponse = JSON.parse(totalResponse || '{}');
261 } catch (err) {
262 deferred.reject(new Error(`Unable to parse JSON response: ${err.message}`));
263 return;
264 }
265
266 if (method === 'GET' && parsedResponse.items) {
267 parsedResponse = parsedResponse.items;
268 }
269 } else {
270 parsedResponse = totalResponse;
271 }
272
273 if (response.statusCode >= 300) {
274 if (response.statusCode === 401 && this.authToken && !noRefresh) {
275 const retryOptions = {};
276 Object.assign(retryOptions, options);
277 retryOptions.noRefresh = true;
278
279 // BIG-IQ requests using Token Auth are required to originate from the same IP address.
280 // Requests from a client may originate from multiple IP addresses (ex: SNAT pools).
281 // Fall back to Basic Auth if BIG-IQ reports the token was issued to another client.
282 const invalidClient =
283 'Tokens are only valid to be used by the client of which they were issued.';
284 if (parsedResponse.message && parsedResponse.message.indexOf(invalidClient) > -1) {
285 this.logger.debug('auth token authentication failed, trying basic auth');
286
287 this.authToken = undefined;
288 this.refreshToken = undefined;
289
290 this.request(method, path, body, retryOptions, deferred);
291 } else if (this.refreshToken) {
292 // Our auth token expired - try to refresh
293 this.logger.debug('auth token expired, refreshing');
294 refreshAuthToken.call(this)
295 .then(() => {
296 this.request(method, path, body, retryOptions, deferred);
297 })
298 .catch((err) => {
299 this.logger.info('auth token expired and refresh failed');
300 deferred.reject(err);
301 });
302 }
303 } else {
304 const error = parseError.call(this, response, parsedResponse, contentType);
305 deferred.reject(error);
306 }
307 } else {
308 deferred.resolve(parsedResponse);
309 }
310 });
311 };
312
313 const httpRequest = getRequest.call(this, requestOptions, responseHandler);
314
315 if (mungedBody) {
316 httpRequest.write(mungedBody);
317 }
318
319 httpRequest.on('error', (err) => {
320 if (!noWait) {
321 deferred.reject(err);
322 }
323 });
324
325 httpRequest.end();
326
327 if (noWait) {
328 deferred.resolve();
329 }
330
331 return deferred.promise;
332};
333
334function parseError(response, parsedResponse, contentType) {
335 let error;
336
337 if (contentType === 'application/json') {
338 if (parsedResponse.code && parsedResponse.message) {
339 error = new Error(parsedResponse.message);
340 error.code = parsedResponse.code;
341 } else {
342 error = new Error(JSON.stringify(parsedResponse));
343 error.code = response.statusCode;
344 }
345 } else {
346 error = new Error(parsedResponse);
347 error.code = response.statusCode;
348 }
349
350 return error;
351}
352
353function refreshAuthToken() {
354 return this.create(
355 '/shared/authn/exchange',
356 {
357 refreshToken: {
358 token: this.refreshToken
359 }
360 }
361 )
362 .then((response) => {
363 if (response && response.token && response.token.token) {
364 this.setAuthToken(response.token.token);
365 this.setRefreshToken(response.refreshToken.token);
366 return q();
367 }
368 return q.reject(new Error('Did not receive auth token while refreshing'));
369 })
370 .catch((err) => {
371 return q.reject(err);
372 });
373}
374
375function getBasePath(options) {
376 if (options.host === 'localhost' && options.port === 8100) {
377 return '';
378 }
379 return '/mgmt';
380}
381
382function getRequest(requestOptions, responseHandler) {
383 if (this.host === 'localhost' && this.port === 8100) {
384 return this.http.request(requestOptions, responseHandler);
385 }
386 return this.https.request(requestOptions, responseHandler);
387}
388
389module.exports = IControl;