Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .github/workflows/prod.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Production
on:
push:
branches:
- main
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master

- uses: Azure/docker-login@v1
with:
login-server: sparrowprod.azurecr.io
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}

- run: |
docker build . -t sparrowprod.azurecr.io/sparrow-proxy:${{ github.run_number }}
docker push sparrowprod.azurecr.io/sparrow-proxy:${{ github.run_number }}
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- uses: richardrigutins/replace-in-files@v1
with:
files: "./deploymentManifests/deployment.yml"
search-text: '_BUILD__ID_'
replacement-text: '${{ github.run_number }}'

- uses: azure/[email protected]

- uses: Azure/k8s-set-context@v2
with:
kubeconfig: ${{ secrets.KUBE_CONFIG }}

- uses: Azure/k8s-deploy@v4
with:
action: deploy
namespace: sparrow-prod
manifests: |
./deploymentManifests/deployment.yml
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "sparrow-proxy",
"version": "2.33.0",
"version": "2.34.0",
"description": "",
"author": "",
"private": true,
Expand Down
16 changes: 7 additions & 9 deletions src/enum/httpRequest.enum.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,3 @@
export interface KeyWrapper {
key: string;
}

export interface ValueWrapper {
value: string;
}

export enum RequestDataTypeEnum {
JSON = "JSON",
XML = "XML",
Expand All @@ -30,4 +22,10 @@ export enum ResponseStatusCode {
ERROR = "Not Found",
}

export interface KeyValue extends KeyWrapper, ValueWrapper {}
export interface KeyValue {
key: string;
value: string;
checked?: boolean;
base?: string;
type?: "text" | "file";
}
77 changes: 54 additions & 23 deletions src/proxy/testflow/testflow.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,30 +109,60 @@ export class TestflowService {
}

private async validateUrl(targetUrl: string) {
let url: URL;

try {
const url = new URL(targetUrl);

// Resolve hostname to IPs
const addresses = await lookup(url.hostname, { all: true });

for (const addr of addresses) {
const ip = ipaddr.parse(addr.address);

// Block local, private, or reserved IPs
if (
ip.range() === 'linkLocal' || // 169.254.0.0/16 (Azure IMDS lives here)
ip.range() === 'loopback' || // 127.0.0.0/8
ip.range() === 'private' || // 10.x, 192.168.x, 172.16-31.x
ip.range() === 'reserved' // Other reserved ranges
) {
throw new BadRequestException(
`Access to internal IP addresses is not allowed: ${addr.address}`,
);
}
}
} catch (err) {
throw new BadRequestException('Invalid or disallowed URL');
url = new URL(targetUrl);
} catch (error) {
throw new BadRequestException('Invalid URL');
}

const hostname = url.hostname.toLowerCase();

const localhostPatterns = [
'localhost',
'127.0.0.1',
'0.0.0.0',
'::1', // IPv6 localhost
];

// Check for private IP ranges
const isPrivateIP = (
hostname.startsWith('192.168.') ||
hostname.startsWith('10.') ||
(hostname.startsWith('172.') &&
(() => {
const parts = hostname.split('.');
if (parts.length >= 2) {
const secondOctet = parseInt(parts[1], 10);
return secondOctet >= 16 && secondOctet <= 31;
}
return false;
})()
) ||
hostname.endsWith('.local') ||
hostname.includes('.local.')
);

const isLocalhost = localhostPatterns.includes(hostname) || isPrivateIP;

if (isLocalhost) {
throw new BadRequestException('This API is local and cannot run on the cloud. Deploy it to enable execution.');
}
}

private async validateFormData(
body: any,
contentType: string,
) {
if (contentType === 'multipart/form-data' && body) {
const formData = JSON.parse(body);
formData.find((item: any) => {
if (item?.type === 'file') {
throw new BadRequestException('This API includes form-data with file uploads that are stored locally and cannot run in scheduled/cloud execution. Remove or replace the files to enable execution.');
}
});
}
}

private async makeHttpRequest({
Expand All @@ -150,6 +180,7 @@ export class TestflowService {
}): Promise<{ status: string; data: any; headers: any }> {
try {
await this.validateUrl(url);
await this.validateFormData(body, contentType);
// Parse headers from stringified JSON
const parsedHeaders: Record<string, string> = {};
let headersArray;
Expand Down Expand Up @@ -329,7 +360,7 @@ export class TestflowService {
}
}
} catch (error: any) {
console.error('HTTP Service Error:', error);
// console.error('HTTP Service Error:', error);
throw new Error(error.message || 'Unknown error occurred');
}
}
Expand Down
8 changes: 4 additions & 4 deletions src/utils/decode-testflow.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import JSON5 from "json5"
import type { KeyValue} from "src/enum/httpRequest.enum";
import type { KeyValue } from "src/enum/httpRequest.enum";
import { RequestDataTypeEnum } from "src/enum/httpRequest.enum";

/**
Expand Down Expand Up @@ -96,13 +96,13 @@ class DecodeTestflow {
* Return only checked KeyValue entries (preserves order).
*/
private extractKeyValue = (
pairs?: Array<{ key: string; value: string; checked: boolean }>,
pairs?: KeyValue[],
): KeyValue[] => {
if (!Array.isArray(pairs)) return [];
const checkedPairs: KeyValue[] = [];
for (const pair of pairs) {
if (pair && pair.checked && pair.key) {
checkedPairs.push({ key: pair.key, value: String(pair.value || "") });
checkedPairs.push({ key: pair.key, type: pair.type, value: String(pair.value || "") });
}
}
return checkedPairs;
Expand Down Expand Up @@ -482,7 +482,7 @@ class DecodeTestflow {
String(field.value),
environmentVariables,
),
type: "text",
type: field.type,
});
});
}
Expand Down
Loading