mirror of
https://git.mirrors.martin98.com/https://github.com/cyberman54/curl
synced 2026-04-03 20:43:15 +08:00
fix: validate status and some security
This commit is contained in:
50
src/index.ts
50
src/index.ts
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
109
src/util.ts
109
src/util.ts
@@ -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));
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user