Skip to content

Commit c617cb4

Browse files
Merge pull request #37 from sparrowapp-dev/development
feat: add production workflow
2 parents 52f26a9 + 19ef68b commit c617cb4

File tree

5 files changed

+110
-37
lines changed

5 files changed

+110
-37
lines changed

.github/workflows/prod.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: Production
2+
on:
3+
push:
4+
branches:
5+
- main
6+
workflow_dispatch:
7+
jobs:
8+
build:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- uses: actions/checkout@master
12+
13+
- uses: Azure/docker-login@v1
14+
with:
15+
login-server: sparrowprod.azurecr.io
16+
username: ${{ secrets.REGISTRY_USERNAME }}
17+
password: ${{ secrets.REGISTRY_PASSWORD }}
18+
19+
- run: |
20+
docker build . -t sparrowprod.azurecr.io/sparrow-proxy:${{ github.run_number }}
21+
docker push sparrowprod.azurecr.io/sparrow-proxy:${{ github.run_number }}
22+
deploy:
23+
needs: build
24+
runs-on: ubuntu-latest
25+
steps:
26+
- uses: actions/checkout@master
27+
- uses: richardrigutins/replace-in-files@v1
28+
with:
29+
files: "./deploymentManifests/deployment.yml"
30+
search-text: '_BUILD__ID_'
31+
replacement-text: '${{ github.run_number }}'
32+
33+
- uses: azure/[email protected]
34+
35+
- uses: Azure/k8s-set-context@v2
36+
with:
37+
kubeconfig: ${{ secrets.KUBE_CONFIG }}
38+
39+
- uses: Azure/k8s-deploy@v4
40+
with:
41+
action: deploy
42+
namespace: sparrow-prod
43+
manifests: |
44+
./deploymentManifests/deployment.yml

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "sparrow-proxy",
3-
"version": "2.33.0",
3+
"version": "2.34.0",
44
"description": "",
55
"author": "",
66
"private": true,

src/enum/httpRequest.enum.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,3 @@
1-
export interface KeyWrapper {
2-
key: string;
3-
}
4-
5-
export interface ValueWrapper {
6-
value: string;
7-
}
8-
91
export enum RequestDataTypeEnum {
102
JSON = "JSON",
113
XML = "XML",
@@ -30,4 +22,10 @@ export enum ResponseStatusCode {
3022
ERROR = "Not Found",
3123
}
3224

33-
export interface KeyValue extends KeyWrapper, ValueWrapper {}
25+
export interface KeyValue {
26+
key: string;
27+
value: string;
28+
checked?: boolean;
29+
base?: string;
30+
type?: "text" | "file";
31+
}

src/proxy/testflow/testflow.service.ts

Lines changed: 54 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -109,30 +109,60 @@ export class TestflowService {
109109
}
110110

111111
private async validateUrl(targetUrl: string) {
112+
let url: URL;
113+
112114
try {
113-
const url = new URL(targetUrl);
114-
115-
// Resolve hostname to IPs
116-
const addresses = await lookup(url.hostname, { all: true });
117-
118-
for (const addr of addresses) {
119-
const ip = ipaddr.parse(addr.address);
120-
121-
// Block local, private, or reserved IPs
122-
if (
123-
ip.range() === 'linkLocal' || // 169.254.0.0/16 (Azure IMDS lives here)
124-
ip.range() === 'loopback' || // 127.0.0.0/8
125-
ip.range() === 'private' || // 10.x, 192.168.x, 172.16-31.x
126-
ip.range() === 'reserved' // Other reserved ranges
127-
) {
128-
throw new BadRequestException(
129-
`Access to internal IP addresses is not allowed: ${addr.address}`,
130-
);
131-
}
132-
}
133-
} catch (err) {
134-
throw new BadRequestException('Invalid or disallowed URL');
115+
url = new URL(targetUrl);
116+
} catch (error) {
117+
throw new BadRequestException('Invalid URL');
135118
}
119+
120+
const hostname = url.hostname.toLowerCase();
121+
122+
const localhostPatterns = [
123+
'localhost',
124+
'127.0.0.1',
125+
'0.0.0.0',
126+
'::1', // IPv6 localhost
127+
];
128+
129+
// Check for private IP ranges
130+
const isPrivateIP = (
131+
hostname.startsWith('192.168.') ||
132+
hostname.startsWith('10.') ||
133+
(hostname.startsWith('172.') &&
134+
(() => {
135+
const parts = hostname.split('.');
136+
if (parts.length >= 2) {
137+
const secondOctet = parseInt(parts[1], 10);
138+
return secondOctet >= 16 && secondOctet <= 31;
139+
}
140+
return false;
141+
})()
142+
) ||
143+
hostname.endsWith('.local') ||
144+
hostname.includes('.local.')
145+
);
146+
147+
const isLocalhost = localhostPatterns.includes(hostname) || isPrivateIP;
148+
149+
if (isLocalhost) {
150+
throw new BadRequestException('This API is local and cannot run on the cloud. Deploy it to enable execution.');
151+
}
152+
}
153+
154+
private async validateFormData(
155+
body: any,
156+
contentType: string,
157+
) {
158+
if (contentType === 'multipart/form-data' && body) {
159+
const formData = JSON.parse(body);
160+
formData.find((item: any) => {
161+
if (item?.type === 'file') {
162+
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.');
163+
}
164+
});
165+
}
136166
}
137167

138168
private async makeHttpRequest({
@@ -150,6 +180,7 @@ export class TestflowService {
150180
}): Promise<{ status: string; data: any; headers: any }> {
151181
try {
152182
await this.validateUrl(url);
183+
await this.validateFormData(body, contentType);
153184
// Parse headers from stringified JSON
154185
const parsedHeaders: Record<string, string> = {};
155186
let headersArray;
@@ -329,7 +360,7 @@ export class TestflowService {
329360
}
330361
}
331362
} catch (error: any) {
332-
console.error('HTTP Service Error:', error);
363+
// console.error('HTTP Service Error:', error);
333364
throw new Error(error.message || 'Unknown error occurred');
334365
}
335366
}

src/utils/decode-testflow.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
22
import JSON5 from "json5"
3-
import type { KeyValue} from "src/enum/httpRequest.enum";
3+
import type { KeyValue } from "src/enum/httpRequest.enum";
44
import { RequestDataTypeEnum } from "src/enum/httpRequest.enum";
55

66
/**
@@ -96,13 +96,13 @@ class DecodeTestflow {
9696
* Return only checked KeyValue entries (preserves order).
9797
*/
9898
private extractKeyValue = (
99-
pairs?: Array<{ key: string; value: string; checked: boolean }>,
99+
pairs?: KeyValue[],
100100
): KeyValue[] => {
101101
if (!Array.isArray(pairs)) return [];
102102
const checkedPairs: KeyValue[] = [];
103103
for (const pair of pairs) {
104104
if (pair && pair.checked && pair.key) {
105-
checkedPairs.push({ key: pair.key, value: String(pair.value || "") });
105+
checkedPairs.push({ key: pair.key, type: pair.type, value: String(pair.value || "") });
106106
}
107107
}
108108
return checkedPairs;
@@ -482,7 +482,7 @@ class DecodeTestflow {
482482
String(field.value),
483483
environmentVariables,
484484
),
485-
type: "text",
485+
type: field.type,
486486
});
487487
});
488488
}

0 commit comments

Comments
 (0)