Skip to content

Commit 788495f

Browse files
authored
fix(sandbox): support conditional request injections (#452)
1 parent 5d5076c commit 788495f

7 files changed

Lines changed: 198 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## 7.15.3
44
- 新增沙箱(Sandbox)模块,支持沙箱实例、文件系统、命令执行、Git、PTY、模板、资源挂载、请求注入等能力
55
- 新增沙箱模块的 TypeScript 类型声明、示例和测试覆盖
6+
- 沙箱请求注入支持 `baseUrl` 路径前缀匹配和 `ifHeaders``ifQueries` 条件匹配
67

78
## 7.15.2
89
- 对象存储,修复 pfop pipeline 参数无效问题

examples/sandbox_injection_rules.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ runExample(() => {
2323
name: ruleName,
2424
injection: {
2525
type: 'http',
26-
baseUrl: 'https://httpbin.org',
26+
baseUrl: 'https://httpbin.org/bearer',
27+
ifHeaders: {
28+
'X-Sandbox-Example': 'qiniu-nodejs-sdk'
29+
},
2730
headers: {
2831
Authorization: `Bearer ${env('QINIU_SANDBOX_HTTP_INJECTION_TOKEN', 'real_token')}`
2932
}
@@ -38,7 +41,10 @@ runExample(() => {
3841
name: `${ruleName}-updated`,
3942
injection: {
4043
type: 'http',
41-
baseUrl: 'https://httpbin.org',
44+
baseUrl: 'https://httpbin.org/bearer',
45+
ifHeaders: {
46+
'X-Sandbox-Example': 'qiniu-nodejs-sdk'
47+
},
4248
headers: {
4349
Authorization: `Bearer ${env('QINIU_SANDBOX_HTTP_INJECTION_TOKEN', 'updated_token')}`,
4450
'X-Sandbox-Example': 'qiniu-nodejs-sdk'
@@ -64,7 +70,7 @@ runExample(() => {
6470
});
6571
}).then(created => {
6672
sandbox = created;
67-
return sandbox.commands.run('curl --max-time 20 -sSL https://httpbin.org/bearer -H "Authorization: Bearer fake_token"', {
73+
return sandbox.commands.run('curl --max-time 20 -sSL https://httpbin.org/bearer -H "Authorization: Bearer fake_token" -H "X-Sandbox-Example: qiniu-nodejs-sdk"', {
6874
timeout: 30000
6975
});
7076
}).then(result => {

examples/sandbox_request_injections.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ runExample(() => {
1212
injections: [
1313
{
1414
type: 'http',
15-
base_url: 'https://httpbin.org',
15+
baseUrl: 'https://httpbin.org/bearer',
16+
ifHeaders: {
17+
'X-Sandbox-Example': 'qiniu-nodejs-sdk'
18+
},
1619
headers: {
1720
Authorization: `Bearer ${env('QINIU_SANDBOX_HTTP_INJECTION_TOKEN', 'real_token')}`
1821
}
@@ -23,7 +26,7 @@ runExample(() => {
2326
}
2427
}).then(created => {
2528
sandbox = created;
26-
return sandbox.commands.run('curl --max-time 20 -sSL https://httpbin.org/bearer -H "Authorization: Bearer fake_token"', {
29+
return sandbox.commands.run('curl --max-time 20 -sSL https://httpbin.org/bearer -H "Authorization: Bearer fake_token" -H "X-Sandbox-Example: qiniu-nodejs-sdk"', {
2730
timeout: 30000
2831
});
2932
}).then(result => {

index.d.ts

Lines changed: 84 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,85 @@ export declare namespace sandbox {
7070

7171
type SandboxResource = GitRepositoryResource | KodoResource;
7272

73+
type InjectionConditions = {[key: string]: string};
74+
75+
interface InjectionMatchOptions {
76+
base_url?: string;
77+
baseUrl?: string;
78+
if_headers?: InjectionConditions;
79+
ifHeaders?: InjectionConditions;
80+
if_queries?: InjectionConditions;
81+
ifQueries?: InjectionConditions;
82+
}
83+
84+
interface InjectionById {
85+
type: 'id';
86+
id?: string;
87+
ruleID?: string;
88+
ruleId?: string;
89+
injectionRuleID?: string;
90+
}
91+
92+
interface InjectionRuleReference {
93+
injectionRuleID: string;
94+
}
95+
96+
interface HttpInjection extends InjectionMatchOptions {
97+
type: 'http';
98+
headers?: {[key: string]: string};
99+
}
100+
101+
interface OpenaiInjection extends InjectionMatchOptions {
102+
type: 'openai';
103+
api_key?: string;
104+
apiKey?: string;
105+
}
106+
107+
interface AnthropicInjection extends InjectionMatchOptions {
108+
type: 'anthropic';
109+
api_key?: string;
110+
apiKey?: string;
111+
}
112+
113+
interface GeminiInjection extends InjectionMatchOptions {
114+
type: 'gemini';
115+
api_key?: string;
116+
apiKey?: string;
117+
}
118+
119+
interface QiniuInjection extends InjectionMatchOptions {
120+
type: 'qiniu';
121+
api_key?: string;
122+
apiKey?: string;
123+
}
124+
125+
interface GithubInjection extends InjectionMatchOptions {
126+
type: 'github';
127+
token: string;
128+
}
129+
130+
type Injection = HttpInjection | OpenaiInjection | AnthropicInjection | GeminiInjection | QiniuInjection | GithubInjection;
131+
type SandboxInjection = InjectionById | InjectionRuleReference | Injection;
132+
133+
interface InjectionRule {
134+
ruleID?: string;
135+
id?: string;
136+
name: string;
137+
createdAt?: string | Date;
138+
updatedAt?: string | Date;
139+
injection: Injection;
140+
}
141+
142+
interface CreateInjectionRuleOptions {
143+
name: string;
144+
injection: Injection;
145+
}
146+
147+
interface UpdateInjectionRuleOptions {
148+
name?: string;
149+
injection?: Injection;
150+
}
151+
73152
interface SandboxCreateOptions extends SandboxClientOptions {
74153
template?: string;
75154
templateID?: string;
@@ -84,7 +163,7 @@ export declare namespace sandbox {
84163
envVars?: {[key: string]: string};
85164
envs?: {[key: string]: string};
86165
mcp?: any;
87-
injections?: any[];
166+
injections?: SandboxInjection[];
88167
resources?: SandboxResource[];
89168
client?: SandboxClient;
90169
}
@@ -403,10 +482,10 @@ export declare namespace sandbox {
403482
deleteTemplateTags(options?: any): Promise<null>;
404483
getTemplateByAlias(alias: string): Promise<any>;
405484

406-
listInjectionRules(): Promise<any>;
407-
createInjectionRule(options?: any): Promise<any>;
408-
getInjectionRule(ruleID: string): Promise<any>;
409-
updateInjectionRule(ruleID: string, options?: any): Promise<any>;
485+
listInjectionRules(): Promise<InjectionRule[]>;
486+
createInjectionRule(options: CreateInjectionRuleOptions): Promise<InjectionRule>;
487+
getInjectionRule(ruleID: string): Promise<InjectionRule>;
488+
updateInjectionRule(ruleID: string, options: UpdateInjectionRuleOptions): Promise<InjectionRule>;
410489
deleteInjectionRule(ruleID: string): Promise<null>;
411490

412491
create(options?: SandboxCreateOptions): Promise<any>;

qiniu/sandbox/client.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,14 @@ function normalizeInjection (injection) {
6464
normalized.base_url = normalized.baseUrl;
6565
delete normalized.baseUrl;
6666
}
67+
if (normalized.ifHeaders !== undefined && normalized.if_headers === undefined) {
68+
normalized.if_headers = normalized.ifHeaders;
69+
delete normalized.ifHeaders;
70+
}
71+
if (normalized.ifQueries !== undefined && normalized.if_queries === undefined) {
72+
normalized.if_queries = normalized.ifQueries;
73+
delete normalized.ifQueries;
74+
}
6775
if (normalized.ruleId !== undefined && normalized.ruleID === undefined) {
6876
normalized.ruleID = normalized.ruleId;
6977
delete normalized.ruleId;
@@ -172,6 +180,9 @@ SandboxClient.prototype._request = function (method, path, options) {
172180
const body = options.body;
173181
const hasBody = body !== undefined && body !== null;
174182
const headers = this._headers(options.authType);
183+
if (!hasBody && (method === 'GET' || method === 'HEAD')) {
184+
delete headers['Content-Type'];
185+
}
175186
const urllibOptions = {
176187
method,
177188
headers,

test/sandbox_client.test.js

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ describe('test sandbox client module', function () {
3737
type: 'qiniu',
3838
apiKey: 'ak',
3939
baseUrl: 'https://example.com',
40+
ifHeaders: {
41+
'X-Provider': 'qiniu'
42+
},
43+
ifQueries: {
44+
model: 'default'
45+
},
4046
ruleId: 'rule_1'
4147
}
4248
]
@@ -62,6 +68,12 @@ describe('test sandbox client module', function () {
6268
type: 'qiniu',
6369
api_key: 'ak',
6470
base_url: 'https://example.com',
71+
if_headers: {
72+
'X-Provider': 'qiniu'
73+
},
74+
if_queries: {
75+
model: 'default'
76+
},
6577
ruleID: 'rule_1'
6678
}
6779
]
@@ -386,7 +398,14 @@ describe('test sandbox client module', function () {
386398
name: 'openai',
387399
injection: {
388400
type: 'openai',
389-
apiKey: 'secret'
401+
apiKey: 'secret',
402+
baseUrl: 'https://api.openai.com/v1/*',
403+
ifHeaders: {
404+
'X-Use-Injected-Key': 'true'
405+
},
406+
ifQueries: {
407+
source: 'sdk-test'
408+
}
390409
}
391410
}).then(() => {
392411
fixture.requests[0].method.should.eql('POST');
@@ -397,7 +416,14 @@ describe('test sandbox client module', function () {
397416
name: 'openai',
398417
injection: {
399418
type: 'openai',
400-
api_key: 'secret'
419+
api_key: 'secret',
420+
base_url: 'https://api.openai.com/v1/*',
421+
if_headers: {
422+
'X-Use-Injected-Key': 'true'
423+
},
424+
if_queries: {
425+
source: 'sdk-test'
426+
}
401427
}
402428
});
403429
}).then(() => closeServer(fixture.server), err => {
@@ -408,6 +434,38 @@ describe('test sandbox client module', function () {
408434
});
409435
});
410436

437+
it('uses the signing default content type for Qiniu-auth GET requests without a body', function () {
438+
return startServer((req, res) => {
439+
res.statusCode = 200;
440+
res.setHeader('Content-Type', 'application/json');
441+
res.end(JSON.stringify({
442+
id: 'rule_1',
443+
name: 'openai',
444+
injection: {
445+
type: 'openai'
446+
}
447+
}));
448+
}).then(fixture => {
449+
const client = new qiniu.sandbox.SandboxClient({
450+
endpoint: fixture.endpoint,
451+
mac: new qiniu.auth.digest.Mac('ak', 'sk', {
452+
disableQiniuTimestampSignature: true
453+
})
454+
});
455+
456+
return client.getInjectionRule('rule_1').then(() => {
457+
fixture.requests[0].method.should.eql('GET');
458+
fixture.requests[0].url.should.eql('/injection-rules/rule_1');
459+
should(fixture.requests[0].headers.authorization).startWith('Qiniu ak:');
460+
fixture.requests[0].headers['content-type'].should.eql('application/x-www-form-urlencoded');
461+
}).then(() => closeServer(fixture.server), err => {
462+
return closeServer(fixture.server).then(() => {
463+
throw err;
464+
});
465+
});
466+
});
467+
});
468+
411469
it('maps sandbox lifecycle and metrics APIs', function () {
412470
return startServer((req, res) => {
413471
res.setHeader('Content-Type', 'application/json');

test/sandbox_types.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,38 @@ async function useSandboxTypes () {
1919
mount_path: '/workspace/repo',
2020
authorization_token: 'token'
2121
};
22+
const qiniuInjection: qiniu.sandbox.QiniuInjection = {
23+
type: 'qiniu',
24+
apiKey: 'sandbox-qiniu-ai-key',
25+
baseUrl: 'https://api.qnaigc.com/v1/*',
26+
ifHeaders: {
27+
'X-Use-Injected-Key': 'true'
28+
},
29+
ifQueries: {
30+
model: 'qiniu-default'
31+
}
32+
};
33+
const githubInjection: qiniu.sandbox.GithubInjection = {
34+
type: 'github',
35+
token: 'github-token',
36+
base_url: 'https://api.github.com/repos/qiniu/*',
37+
if_headers: {
38+
Accept: 'application/vnd.github+json'
39+
},
40+
if_queries: {
41+
per_page: '100'
42+
}
43+
};
2244

2345
const sandbox = await qiniu.Sandbox.create('base', {
2446
client,
2547
resources: [kodoResource, gitResource],
48+
injections: [
49+
{ type: 'id', id: 'rule_1' },
50+
{ injectionRuleID: 'rule_2' },
51+
qiniuInjection,
52+
githubInjection
53+
],
2654
network: {
2755
allowOut: [qiniu.sandbox.ALL_TRAFFIC]
2856
}

0 commit comments

Comments
 (0)