Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add proxy support for smapi commands #434 #465

Merged
merged 5 commits into from
May 15, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ node_modules
docs
scripts
*.md
coverage
.nyc_output/
191 changes: 135 additions & 56 deletions lib/clients/http-client.js
Original file line number Diff line number Diff line change
@@ -1,70 +1,93 @@
const R = require("ramda");
const requestLib = require("request");
"use strict";
import axios from "axios";
import {Url, parse} from "url";

const DynamicConfig = require("../utils/dynamic-config");
const logger = require("../utils/logger-utility");
const urlUtils = require("../utils/url-utils");
const stringUtils = require("../utils/string-utils");
const CONSTANTS = require("../utils/constants");

module.exports = {
request,
putByUrl,
};
import {userAgent} from "../utils/dynamic-config";
import {getInstance as loggerInstance} from "../utils/logger-utility";
import {isValidUrl} from "../utils/url-utils";
import {isNonBlankString} from "../utils/string-utils";
import {HTTP_REQUEST} from "../utils/constants";

/**
* Core CLI request function with User-Agent setting.
* Core CLI request function with User-Agent setting and proxy support.
*
* @param {object} options request options object
* @param {string} operation operation name for the request
* @param {boolean} doDebug define if debug info is needed
* @param {function} callback
*/
function request(options, operation, doDebug, callback) {
export function request(options, operation, doDebug, callback) {
// Validation of input parameters
const requestOptions = R.clone(options);
if (typeof operation !== "string" || !operation.trim()) {
process.nextTick(() => {
callback("[Fatal]: CLI request must have a non-empty operation name.");
});
return;
}
if (!urlUtils.isValidUrl(requestOptions.url)) {
if (!isValidUrl(options.url)) {
process.nextTick(() => {
callback(`[Fatal]: Invalid URL:${requestOptions.url}. CLI request must call with valid url.`);
callback(`[Fatal]: Invalid URL:${options.url}. CLI request must call with valid url.`);
});
return;
}

const proxyUrl = process.env.ASK_CLI_PROXY;
if (stringUtils.isNonBlankString(proxyUrl)) {
requestOptions.proxy = proxyUrl;
let proxyConfig = {};
try {
proxyConfig = getProxyConfigurations();
} catch (err) {
return callback(err.message);
}

const requestOptions = {
method: options.method || "GET",
url: options.url,
headers: options.headers || {},
data: options.body,
...(options.responseType ? {responseType: options.responseType} : {}),
...proxyConfig,
};

// Set user-agent for each CLI request
if (!requestOptions.headers) {
requestOptions.headers = {};
}
requestOptions.headers["User-Agent"] = DynamicConfig.userAgent;
requestOptions.headers["User-Agent"] = userAgent;

// Make request
requestLib(requestOptions, (error, response) => {
if (doDebug) {
logger.getInstance().debug(debugContentForResponse(operation, error, response));
}
if (error) {
return callback(`Failed to make request to ${operation}.\nError response: ${error}`);
}
if (!response) {
return callback(`Failed to make request to ${operation}.\nPlease make sure "${requestOptions.url}" is responding.`);
}
if (!response.statusCode) {
return callback(`Failed to access the statusCode from the request to ${operation}.`);
}
return callback(null, response);
});
}
return axios.request(requestOptions)
.then((response) => {
if (doDebug) {
loggerInstance().debug(debugContentForResponse(operation, null, response));
}
if (!response) {
return callback({
errorMessage :`The request to ${operation}, failed.\nPlease make sure "${requestOptions.url}" is responding.`,
});
}
if (!response.status) {
return callback({
errorMessage :`Failed to access the statusCode from the request to ${operation}.`,
});
}
return callback(null, {
statusCode: response.status,
...(response.data ? {body: response.data} : {}),
...(response.headers ? {headers: response.headers} : {}),
});
})
.catch((error) => {
const response = error ? error.response || {} : {};
error.statusCode ||= response.status;

if (doDebug) {
loggerInstance().debug(debugContentForResponse(operation, error, response));
}

return callback({
errorMessage : `The request to ${requestOptions.url} failed. Client Error: ${error}`,
statusCode: response.status,
...(response.data ? {body: response.data} : {}),
...(response.headers ? {headers: response.headers} : {}),
}, response);
});
}
/**
* HTTP client's upload method
* @param {String} url
Expand All @@ -73,10 +96,10 @@ function request(options, operation, doDebug, callback) {
* @param {Boolean} doDebug
* @param {Function} callback
*/
function putByUrl(url, payload, operation, doDebug, callback) {
export function putByUrl(url, payload, operation, doDebug, callback) {
const options = {
url,
method: CONSTANTS.HTTP_REQUEST.VERB.PUT,
method: HTTP_REQUEST.VERB.PUT,
headers: {},
body: payload,
};
Expand All @@ -92,21 +115,77 @@ function putByUrl(url, payload, operation, doDebug, callback) {
* @param {object} response
*/
function debugContentForResponse(operation, error, response) {
return {
const debugContent = {
activity: operation,
error,
"request-id": response.headers["x-amzn-requestid"] || null,
request: {
method: response.request.method,
url: response.request.href,
headers: response.request.headers,
body: response.request.body,
},
response: {
statusCode: response.statusCode,
statusMessage: response.statusMessage,
headers: response.headers,
},
body: response.body,
};
if (response) {
debugContent["response"] = {
...(response.status ? { statusCode: response.status } : {}),
...(response.statusText ? { statusMessage: response.statusText } : {}),
...(response.headers ? { headers: response.headers } : {}),
};
debugContent["request-id"] = response.headers ? (debugContent["request-id"] = response.headers["x-amzn-requestid"] || null) : null;
const requestConfig = response.config || {};
if (response.request) {
debugContent["request"] = {
method: requestConfig.method,
url: requestConfig.url,
headers: response.request._headers || requestConfig.headers,
body: requestConfig.data,
};
}
if (response.data) {
debugContent["body"] = response.data;
}
}
return debugContent;
}

/**
* If the env variable ASK_CLI_PROXY is set, returns the axios proxy object to append to the requestOptions
* as defined here https://www.npmjs.com/package/axios#request-config
* Otherwise returns an empty object {}
* @returns {Record} axios proxy configurations
* @throws {Error} if the ASK_CLI_PROXY is not a valid URL
*/
function getProxyConfigurations() {
const proxyEnv = process.env.ASK_CLI_PROXY;
const configuration = {};
if (proxyEnv && isNonBlankString(proxyEnv)) {
if (!isValidUrl(proxyEnv)) {
throw new Error(`[Fatal]: Invalid Proxy setting URL: ${proxyEnv}. Reset ASK_CLI_PROXY env variable with a valid proxy url.`);
}
const proxyUrl = parse(proxyEnv);
configuration.proxy = {
protocol: proxyUrl.protocol.replace(":", ""),
host: proxyUrl.hostname,
...(proxyUrl.port ? {port: proxyUrl.port} : {}),
...getAuthFromUrlObject(proxyUrl),
};
}
return configuration;
}

/**
* Gets the auth part of the specified Url and returns an axios auth object to append to the proxy / request object
* as defined here https://www.npmjs.com/package/axios#request-config
* Otherwise returns an empty object {}
* @param {Url} url
* @returns {Record} axios proxy auth configurations
*/
function getAuthFromUrlObject(url) {
const auth = {};
if (url.auth) {
const authSplit = url.auth.split(":");
const authBody = {};
if (authSplit.length > 0) {
authBody.username = authSplit[0];
}
if (authSplit.length > 1) {
authBody.password = authSplit[1];
}
auth.auth = authBody;
}
return auth;
}
38 changes: 19 additions & 19 deletions lib/clients/lwa-auth-code-client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@ module.exports = class LWAAuthCodeClient {
body,
json: !!body,
};
httpClient.request(options, "GET_ACCESS_TOKEN", this.config.doDebug, (err, response) => {
if (err) {
return callback(err);
httpClient.request(options, "GET_ACCESS_TOKEN", this.config.doDebug, (requestError, requestResponse) => {
if (requestError) {
return callback(requestError.errorMessage || requestError);
}
const tokenBody = R.clone(response.body);
const tokenBody = R.clone(requestResponse.body);
if (tokenBody.error) {
return callback(new CliError(tokenBody.error));
}
Expand All @@ -57,32 +57,32 @@ module.exports = class LWAAuthCodeClient {
*/
refreshToken(token, callback) {
const url = new URL(this.config.tokenPath, this.config.tokenHost);
const body = {
grant_type: "refresh_token",
refresh_token: token.refresh_token,
client_id: this.config.clientId,
client_secret: this.config.clientConfirmation,
};
const options = {
url: `${url}`,
headers: {"content-type": "application/json"},
method: "POST",
body,
json: !!body,
body: {
grant_type: "refresh_token",
refresh_token: token.refresh_token,
client_id: this.config.clientId,
client_secret: this.config.clientConfirmation,
},
json: true,
};
httpClient.request(options, "GET_ACCESS_TOKEN_USING_REFRESH_TOKEN", this.config.doDebug, (err, response) => {
if (err) {
return callback(err);
httpClient.request(options, "GET_ACCESS_TOKEN_USING_REFRESH_TOKEN", this.config.doDebug, (requestError, requestResponse) => {
if (requestError) {
return callback(requestError.message || requestError);
}
const responseErr = R.view(R.lensPath(["body", "error"]), response);
const responseErr = R.view(R.lensPath(["body", "error"]), requestResponse);
if (stringUtils.isNonBlankString(responseErr)) {
return callback(`Refresh LWA tokens failed, please run "ask configure" to manually update your tokens. Error: ${responseErr}.`);
}
const expiresIn = R.view(R.lensPath(["body", "expires_in"]), response);
const expiresIn = R.view(R.lensPath(["body", "expires_in"]), requestResponse);
if (!expiresIn) {
return callback(`Received invalid response body from LWA without "expires_in":\n${jsonView.toString(response.body)}`);
return callback(`Received invalid response body from LWA without "expires_in":\n${jsonView.toString(requestResponse.body)}`);
}

const tokenBody = R.clone(response.body);
const tokenBody = R.clone(requestResponse.body);
if (tokenBody.error) {
return callback(new CliError(tokenBody.error));
}
Expand Down
20 changes: 14 additions & 6 deletions lib/clients/smapi-client/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import querystring from "querystring";
import AuthorizationController from "../../controllers/authorization-controller";
import DynamicConfig from "../../utils/dynamic-config";
import httpClient from "../http-client";
import * as httpClient from "../http-client";

import accountLinkingApi from "./resources/account-linking";
import catalogApi from "./resources/catalog";
Expand Down Expand Up @@ -162,12 +162,17 @@ export class SmapiClientLateBound {
body: payload,
json: !!payload,
};
httpClient.request(requestOptions, apiName, configuration.doDebug, (reqErr: any, reqResponse: SmapiResponse<T>) => {
if (reqErr) {
return callback(reqErr);
httpClient.request(requestOptions, apiName, configuration.doDebug, (requestError: any, requestResponse: SmapiResponse<T>) => {
if (requestError && requestError.statusCode ) {
return _normalizeSmapiResponse<T>(requestError, (normalizeErr, smapiResponse) => {
return callback(normalizeErr || smapiResponse, smapiResponse || null);
});
}
_normalizeSmapiResponse<T>(reqResponse, (normalizeErr, smapiResponse) => {
callback(normalizeErr, normalizeErr ? null : smapiResponse);
if (requestError) {
return callback(requestError.errorMessage || requestError);
}
return _normalizeSmapiResponse<T>(requestResponse, (normalizeError, smapiResponse) => {
return callback(normalizeError, normalizeError ? null : smapiResponse);
});
});
});
Expand Down Expand Up @@ -235,12 +240,14 @@ export interface SmapiResponseObject<T = Record<string, any>> {
statusCode: number;
body: T;
headers: any[];
message?: string;
}

export interface SmapiResponseError<E = {}> {
statusCode: number;
body: E & {message: string};
headers: any[];
message?: string;
}

export function isSmapiError<T, E>(response: SmapiResponse<T, E>): response is SmapiResponseError<E> {
Expand All @@ -264,6 +271,7 @@ function _normalizeSmapiResponse<T>(reqResponse: SmapiResponse<T>, callback: (er
statusCode: reqResponse.statusCode,
body: parsedResponseBody,
headers: reqResponse.headers,
...(reqResponse.message? {message: reqResponse.message} : {})
});
}

Expand Down