fix: validate status and some security

This commit is contained in:
Wathanyu Phromma
2022-03-20 14:01:35 +07:00
parent 31d4a5f24b
commit a2c408c63d
387 changed files with 33710 additions and 4612 deletions

View File

@@ -1,29 +1,31 @@
import axios, { AxiosRequestConfig } from 'axios'
import config from './requestconf'
import * as core from '@actions/core'
import * as fs from 'fs'
import { sendRequestWithRetry } from './util'
import { AxiosRequestConfig } from "axios";
import config, { INPUT_CUSTOM_CONFIG_FILE } from "./requestconf";
import * as core from "@actions/core";
import * as fs from "fs";
import { sendRequestWithRetry } from "./util";
try {
if(core.getInput('custom-config')){
const configPath = core.getInput('custom-config');
const basePath = process.env.GITHUB_WORKSPACE;
const path = `${basePath}/${configPath}`;
core.info(`Path is ${path}`);
if(configPath.split('.').pop() !== 'json'){
throw new Error('Config must be json file')
}
if(!fs.existsSync(path)){
throw new Error('Config file not found, meybe you need to use action/checkout before this step or there is typo on file name')
}
let customConfig: AxiosRequestConfig = JSON.parse(fs.readFileSync(path).toString()) as AxiosRequestConfig;
sendRequestWithRetry(customConfig)
}else{
sendRequestWithRetry(config)
if (INPUT_CUSTOM_CONFIG_FILE) {
core.info(`Using custom axios config file`);
const basePath = process.env.GITHUB_WORKSPACE;
const path = `${basePath}/${INPUT_CUSTOM_CONFIG_FILE}`;
core.debug(`Path is ${path}`);
if ((INPUT_CUSTOM_CONFIG_FILE as string).split(".").pop() !== "json") {
throw new Error("Config must be json file");
}
if (!fs.existsSync(path)) {
throw new Error(
"Config file not found, meybe you need to use action/checkout before this step or there is typo on file name"
);
}
const customConfig: AxiosRequestConfig = JSON.parse(
fs.readFileSync(path).toString()
) as AxiosRequestConfig;
sendRequestWithRetry(customConfig);
} else {
core.info(`Using config from action params`);
sendRequestWithRetry(config);
}
} catch (err) {
core.setFailed(err.message);
core.setFailed((err as Error).message);
}

View File

@@ -1,17 +1,16 @@
import * as core from '@actions/core'
import * as util from './util'
import { AxiosResponse } from 'axios'
import { INPUT_LOG_RESPONSE } from "./requestconf";
import * as core from "@actions/core";
import * as util from "./util";
import { AxiosResponse } from "axios";
const setOutput = (res: void | AxiosResponse <any>) => {
if(!res){
throw new Error('No response from request')
}
util.validateStatusCode(res.status.toString());
if(core.getInput('is_debug') === 'true'){
core.info(util.buildOutput(res));
}
core.setOutput('response', util.buildOutput(res));
}
const setOutput = (res: void | AxiosResponse<any>) => {
if (!res) {
throw new Error("No response from request");
}
if (INPUT_LOG_RESPONSE) {
core.info(`Response: ${util.buildOutput(res)}`);
}
core.setOutput("response", util.buildOutput(res));
};
export default setOutput
export default setOutput;

View File

@@ -1,92 +1,142 @@
import axios from 'axios'
import * as core from '@actions/core'
import { AxiosRequestConfig, Method, AxiosBasicCredentials, AxiosProxyConfig} from 'axios'
import { getAcceptedStatusCodes, tryToParseJson } from "./util";
import * as core from "@actions/core";
import {
AxiosRequestConfig,
Method,
AxiosBasicCredentials,
AxiosProxyConfig,
AxiosRequestHeaders,
} from "axios";
import * as yaml from "js-yaml";
// builder for request config
// inputs
export const INPUT_BASIC_AUTH_TOKEN: Readonly<string> =
core.getInput("basic-auth-token");
export const INPUT_BEARER_TOKEN: Readonly<string> =
core.getInput("bearer-token");
export const INPUT_PROXY_URL: Readonly<string> = core.getInput("proxy-url");
export const INPUT_PROXY_AUTH_TOKEN: Readonly<string> =
core.getInput("proxy-auth-token");
export const INPUT_URL: Readonly<string> = core.getInput("url", {
required: true,
});
export const INPUT_HEADERS: Readonly<string> = core.getInput("headers");
export const INPUT_PARAMS: Readonly<string> = core.getInput("params");
export const INPUT_BODY: Readonly<string> = core.getInput("body");
export const INPUT_METHOD: Readonly<Method> = core.getInput("method") as Method;
export const INPUT_TIMEOUT: Readonly<string> = core.getInput("timeout");
export const INPUT_ACCEPT: Readonly<string> = core.getInput("accept");
export const INPUT_LOG_RESPONSE: Readonly<boolean> =
core.getBooleanInput("log-response");
export const INPUT_CUSTOM_CONFIG_FILE: Readonly<string> =
core.getInput("custom-config");
export const INPUT_RETRIES: Readonly<string> = core.getInput("retries");
const builder = {
basicAuth: (): AxiosBasicCredentials => {
let authArr: string[] = core.getInput('basic-auth').trim().split(':');
if(authArr.length !== 2){
throw new Error('basic-auth format is invalid. The valid format should be username:password.');
}
return {
username: authArr[0],
password: authArr[1]
}
},
bearerToken: (): string => {
return `Bearer ${core.getInput('bearer-token')}`;
},
proxy: (): AxiosProxyConfig => {
let proxy: AxiosProxyConfig;
if(core.getInput('proxy-url').includes('//')){
const proxyUrlArr: string[] = core.getInput('proxy-url').replace('//', '').trim().split(':');
if(proxyUrlArr.length !== 3){
throw new Error('proxy-url format is invalid. The valid format should be host:port.');
}
proxy = {
protocol: proxyUrlArr[0],
host: proxyUrlArr[1],
port: Number(proxyUrlArr[2])
}
}else{
const proxyUrlArr: string[] = core.getInput('proxy-url').trim().split(':');
if(proxyUrlArr.length !== 2){
throw new Error('proxy-url format is invalid. The valid format should be host:port.');
}
proxy = {
host: proxyUrlArr[0],
port: Number(proxyUrlArr[1])
}
}
if(core.getInput('proxy-auth')){
const proxyAuthArr: string[] = core.getInput('proxy-auth').trim().split(':');
if(proxyAuthArr.length !== 2){
throw new Error('proxy-auth format is invalid. The valid format should be username:password.');
}
proxy.auth = {
username: proxyAuthArr[0],
password: proxyAuthArr[1]
}
}
return proxy;
basicAuth: (): AxiosBasicCredentials => {
const basicAuthString: string = Buffer.from(
INPUT_BASIC_AUTH_TOKEN,
"base64"
).toString();
const basicAuthArr: string[] = basicAuthString.trim().split(":");
if (basicAuthArr.length !== 2) {
throw new Error(
"basic-auth-token format is invalid. The valid format should be username:password as base64."
);
}
}
return {
username: basicAuthArr[0],
password: basicAuthArr[1],
};
},
bearerToken: (): string => {
return `Bearer ${INPUT_BEARER_TOKEN}`;
},
proxy: (): AxiosProxyConfig => {
let proxy: AxiosProxyConfig;
if (INPUT_PROXY_URL.includes("//")) {
const proxyUrlArr: string[] = INPUT_PROXY_URL.replace("//", "")
.trim()
.split(":");
if (proxyUrlArr.length !== 3) {
throw new Error(
"proxy-url format is invalid. The valid format should be host:port."
);
}
proxy = {
protocol: proxyUrlArr[0],
host: proxyUrlArr[1],
port: Number(proxyUrlArr[2]),
};
} else {
const proxyUrlArr: string[] = INPUT_PROXY_URL.trim().split(":");
if (proxyUrlArr.length !== 2) {
throw new Error(
"proxy-url format is invalid. The valid format should be host:port."
);
}
proxy = {
host: proxyUrlArr[0],
port: Number(proxyUrlArr[1]),
};
}
if (INPUT_PROXY_AUTH_TOKEN) {
const proxyAuthString: string = Buffer.from(
INPUT_PROXY_AUTH_TOKEN,
"base64"
).toString();
const proxyAuthArr: string[] = proxyAuthString.trim().split(":");
if (proxyAuthArr.length !== 2) {
throw new Error(
"proxy-auth format is invalid. The valid format should be username:password as base64."
);
}
proxy.auth = {
username: proxyAuthArr[0],
password: proxyAuthArr[1],
};
}
return proxy;
},
};
// Request config
// Request config
const config: AxiosRequestConfig = {
url: core.getInput('url'),
method: core.getInput('method') as Method,
timeout: Number(core.getInput('timeout'))
url: INPUT_URL,
method: INPUT_METHOD,
timeout: Number(INPUT_TIMEOUT),
};
if (INPUT_BASIC_AUTH_TOKEN) {
config.auth = builder.basicAuth();
}
if(core.getInput('basic-auth')){
config.auth = builder.basicAuth()
if (INPUT_HEADERS) {
config.headers = tryToParseJson(INPUT_HEADERS) as AxiosRequestHeaders;
}
if(core.getInput('headers')){
config.headers = JSON.parse(core.getInput('headers'))
if (INPUT_PARAMS) {
config.params = tryToParseJson(INPUT_PARAMS);
}
if(core.getInput('params')){
config.params = JSON.parse(core.getInput('params'))
if (INPUT_BODY) {
config.data = tryToParseJson(INPUT_BODY);
}
if(core.getInput('body')){
config.data = core.getInput('body')
if (INPUT_BEARER_TOKEN) {
config.headers = { ...config.headers, Authorization: builder.bearerToken() };
}
if(core.getInput('bearer-token')){
config.headers = { ...config.headers, Authorization: builder.bearerToken() }
if (INPUT_PROXY_URL) {
config.proxy = builder.proxy();
}
if(core.getInput('proxy-url')){
config.proxy = builder.proxy()
if (INPUT_ACCEPT) {
const accepts = getAcceptedStatusCodes();
config.validateStatus = (status) => accepts.includes(status);
}
export default config
export default config;

View File

@@ -1,49 +1,72 @@
import * as core from '@actions/core'
import axios, { AxiosResponse, AxiosRequestConfig } from 'axios'
import setOutput from './output'
import { INPUT_ACCEPT, INPUT_RETRIES } from "./requestconf";
import * as core from "@actions/core";
import axios, { AxiosResponse, AxiosRequestConfig } from "axios";
import setOutput from "./output";
import * as rax from "retry-axios";
import * as yaml from "js-yaml";
export const validateStatusCode = (actualStatusCode: string): void => {
const acceptedStatusCode: string[] = core.getInput('accept')
.split(",").filter(x => x !== "")
.map(x => x.trim());
if (!acceptedStatusCode.includes(actualStatusCode)) {
throw new Error(`The accepted status code is ${acceptedStatusCode} but got ${actualStatusCode}`)
export const getAcceptedStatusCodes = (): number[] => {
const acceptedStatusCodes: string[] = (INPUT_ACCEPT as string)
.split(",")
.filter((x) => x !== "")
.map((x) => x.trim());
let output: number[] = [];
for (let acceptedStatusCode of acceptedStatusCodes) {
if (isNaN(Number(acceptedStatusCode))) {
throw new Error(`Accept status ${acceptedStatusCode} is invalid`);
}
}
output.push(Number(acceptedStatusCode));
}
return output;
};
export const buildOutput = (res: AxiosResponse<any>): string => {
return JSON.stringify({
"status_code": res.status,
"data": res.data,
"headers": res.headers
})
}
return JSON.stringify({
status_code: res.status,
data: res.data,
headers: res.headers,
});
};
export const tryToParseJson = (data: string): string | unknown => {
let output: string | unknown = data;
// try to parse json directly
try {
output = JSON.parse(data);
return output;
} catch {
// do nothing
}
// try to parse json from yaml
try {
output = yaml.load(data, { json: true });
return output;
} catch {
// do nothing
}
return data;
};
export const sendRequestWithRetry = async (config: AxiosRequestConfig) => {
var exit = false
var countRetry = 0
const retryArr: string[] = core.getInput('retry').split('/')
const numberOfRetry: number = Number(retryArr[0])
const backoff: number = Number(retryArr[1])
do {
try {
const res = await axios(config)
setOutput(res)
exit = true
} catch (err) {
countRetry += 1
if (countRetry <= numberOfRetry) {
core.info(`retry: ${countRetry}`)
await sleep(backoff * 1000)
} else {
exit = true
core.setFailed(err)
}
}
} while (!exit)
}
function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
const client = axios.create();
if (INPUT_RETRIES) {
if (isNaN(Number(INPUT_RETRIES))) {
throw new Error("retries should be number");
}
client.defaults.raxConfig = {
instance: client,
retry: Number(INPUT_RETRIES),
onRetryAttempt: (err) => {
const cfg = rax.getConfig(err);
core.info(`Retry attempt #${cfg?.currentRetryAttempt}`);
},
};
rax.attach(client);
}
client
.request(config)
.then((resp) => setOutput(resp))
.catch((err) => core.setFailed(err));
};