@eighty4/c2 0.0.2-2 → 0.0.2-26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/lib/attachments.spec.ts +49 -4
- package/lib/attachments.ts +22 -16
- package/lib/build.spec.ts +41 -44
- package/lib/build.ts +7 -42
- package/lib/c2.api.ts +1 -1
- package/lib/c2.bin.ts +18 -9
- package/lib/cli.spec.ts +35 -8
- package/lib/cli.ts +12 -1
- package/lib/expression.spec.ts +2 -2
- package/lib/expression.ts +1 -1
- package/lib/http.spec.ts +57 -0
- package/lib/http.ts +110 -0
- package/lib_js/attachments.js +16 -13
- package/lib_js/build.js +4 -33
- package/lib_js/c2.api.js +1 -1
- package/lib_js/c2.bin.js +17 -8
- package/lib_js/cli.js +10 -1
- package/lib_js/expression.js +1 -1
- package/lib_js/http.js +94 -0
- package/lib_types/attachments.d.ts +2 -1
- package/lib_types/attachments.d.ts.map +1 -1
- package/lib_types/build.d.ts.map +1 -1
- package/lib_types/c2.api.d.ts +1 -1
- package/lib_types/c2.api.d.ts.map +1 -1
- package/lib_types/cli.d.ts.map +1 -1
- package/lib_types/http.d.ts +18 -0
- package/lib_types/http.d.ts.map +1 -0
- package/package.json +13 -5
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ const userData: string = await buildUserData('./cloud_init_dir')
|
|
|
30
30
|
|
|
31
31
|
## Cloud Config data dir
|
|
32
32
|
|
|
33
|
-
Multiple user data are ordered by filenames and `01_` numbered prefixes help
|
|
33
|
+
Multiple user data are ordered by filenames and `01_` numbered prefixes help declare execution order.
|
|
34
34
|
|
|
35
35
|
```
|
|
36
36
|
ls ./cloud_init_dir
|
package/lib/attachments.spec.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { afterEach, beforeEach, expect, test } from 'bun:test'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
|
-
import { collectAttachments } from '#c2/attachments
|
|
4
|
-
import { makeFile, makeTempDir, removeDir } from '#c2/fs.testing
|
|
3
|
+
import { collectAttachments } from '#c2/attachments'
|
|
4
|
+
import { makeFile, makeTempDir, removeDir } from '#c2/fs.testing'
|
|
5
5
|
|
|
6
6
|
let tmpDir: string
|
|
7
7
|
|
|
@@ -9,18 +9,40 @@ beforeEach(async () => (tmpDir = await makeTempDir()))
|
|
|
9
9
|
|
|
10
10
|
afterEach(async () => await removeDir(tmpDir))
|
|
11
11
|
|
|
12
|
+
test('collect throws error when yml does not have #cloud-config comment', async () => {
|
|
13
|
+
await makeFile('init-cloud', 'whoopie', tmpDir)
|
|
14
|
+
await expect(() => collectAttachments(tmpDir)).toThrow(
|
|
15
|
+
'init-cloud is an unsupported file type',
|
|
16
|
+
)
|
|
17
|
+
})
|
|
18
|
+
|
|
12
19
|
test('collect cloud config yml', async () => {
|
|
13
|
-
await makeFile('init-cloud.yml', '
|
|
20
|
+
await makeFile('init-cloud.yml', '#cloud-config\nwhoopie', tmpDir)
|
|
14
21
|
expect(await collectAttachments(tmpDir)).toStrictEqual([
|
|
15
22
|
{
|
|
16
23
|
path: join(tmpDir, 'init-cloud.yml'),
|
|
17
|
-
content: '
|
|
24
|
+
content: '#cloud-config\nwhoopie',
|
|
18
25
|
filename: 'init-cloud.yml',
|
|
26
|
+
source: '#cloud-config\nwhoopie',
|
|
19
27
|
type: 'cloud-config',
|
|
20
28
|
},
|
|
21
29
|
])
|
|
22
30
|
})
|
|
23
31
|
|
|
32
|
+
test('collect throws error when yml does not have #cloud-config comment', async () => {
|
|
33
|
+
await makeFile('init-cloud.yml', 'whoopie', tmpDir)
|
|
34
|
+
await expect(() => collectAttachments(tmpDir)).toThrow(
|
|
35
|
+
'YAML cloud config must start with a #cloud-config comment',
|
|
36
|
+
)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('collect throws error when yaml does not have #cloud-config comment', async () => {
|
|
40
|
+
await makeFile('init-cloud.yaml', 'whoopie', tmpDir)
|
|
41
|
+
await expect(() => collectAttachments(tmpDir)).toThrow(
|
|
42
|
+
'YAML cloud config must start with a #cloud-config comment',
|
|
43
|
+
)
|
|
44
|
+
})
|
|
45
|
+
|
|
24
46
|
test('collect shell script', async () => {
|
|
25
47
|
await makeFile('init-cloud.sh', 'whoopie', tmpDir)
|
|
26
48
|
expect(await collectAttachments(tmpDir)).toStrictEqual([
|
|
@@ -28,9 +50,30 @@ test('collect shell script', async () => {
|
|
|
28
50
|
path: join(tmpDir, 'init-cloud.sh'),
|
|
29
51
|
content: 'whoopie',
|
|
30
52
|
filename: 'init-cloud.sh',
|
|
53
|
+
source: 'whoopie',
|
|
54
|
+
type: 'x-shellscript',
|
|
55
|
+
},
|
|
56
|
+
])
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('evals template expressions', async () => {
|
|
60
|
+
const resourceTmpDir = await makeTempDir()
|
|
61
|
+
await makeFile('whoopie', 'whoopie', resourceTmpDir)
|
|
62
|
+
await makeFile(
|
|
63
|
+
'init-cloud.sh',
|
|
64
|
+
`\${{ file('${resourceTmpDir}/whoopie') }}`,
|
|
65
|
+
tmpDir,
|
|
66
|
+
)
|
|
67
|
+
expect(await collectAttachments(tmpDir)).toStrictEqual([
|
|
68
|
+
{
|
|
69
|
+
path: tmpDir + '/init-cloud.sh',
|
|
70
|
+
content: 'whoopie',
|
|
71
|
+
filename: 'init-cloud.sh',
|
|
72
|
+
source: `\${{ file('${resourceTmpDir}/whoopie') }}`,
|
|
31
73
|
type: 'x-shellscript',
|
|
32
74
|
},
|
|
33
75
|
])
|
|
76
|
+
await removeDir(resourceTmpDir)
|
|
34
77
|
})
|
|
35
78
|
|
|
36
79
|
test('sorts attachments by filename', async () => {
|
|
@@ -41,12 +84,14 @@ test('sorts attachments by filename', async () => {
|
|
|
41
84
|
path: join(tmpDir, '01-init-cloud.sh'),
|
|
42
85
|
content: 'whoopie',
|
|
43
86
|
filename: '01-init-cloud.sh',
|
|
87
|
+
source: 'whoopie',
|
|
44
88
|
type: 'x-shellscript',
|
|
45
89
|
},
|
|
46
90
|
{
|
|
47
91
|
path: join(tmpDir, '02-init-cloud.sh'),
|
|
48
92
|
content: 'whoopie',
|
|
49
93
|
filename: '02-init-cloud.sh',
|
|
94
|
+
source: 'whoopie',
|
|
50
95
|
type: 'x-shellscript',
|
|
51
96
|
},
|
|
52
97
|
])
|
package/lib/attachments.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { evalTemplateExpressions } from '#c2/expression'
|
|
2
|
+
import { readDirListing, readToString } from '#c2/fs'
|
|
2
3
|
|
|
3
4
|
export type AttachmentType = 'cloud-config' | 'x-shellscript'
|
|
4
5
|
|
|
@@ -6,20 +7,24 @@ export interface Attachment {
|
|
|
6
7
|
path: string
|
|
7
8
|
content: string
|
|
8
9
|
filename: string
|
|
10
|
+
source: string
|
|
9
11
|
type: AttachmentType
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
export async function collectAttachments(
|
|
13
15
|
dir: string,
|
|
14
16
|
): Promise<Array<Attachment>> {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
const filenames = await readDirListing(dir)
|
|
18
|
+
const attachments = await Promise.all(
|
|
19
|
+
filenames.map(async filename => {
|
|
20
|
+
const path = `${dir}/${filename}`
|
|
21
|
+
const source = await readToString(path)
|
|
22
|
+
const type = resolveAttachmentType(filename, source)
|
|
23
|
+
const content = await evalTemplateExpressions(source)
|
|
24
|
+
return { content, filename, path, type, source }
|
|
25
|
+
}),
|
|
26
|
+
)
|
|
27
|
+
return attachments.sort(compareAttachmentFilenames)
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
function compareAttachmentFilenames(
|
|
@@ -33,17 +38,18 @@ function compareAttachmentFilenames(
|
|
|
33
38
|
|
|
34
39
|
export function resolveAttachmentType(
|
|
35
40
|
filename: string,
|
|
36
|
-
|
|
41
|
+
source: string,
|
|
37
42
|
): AttachmentType {
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
if (filename.endsWith('.yml') || filename.endsWith('.yaml')) {
|
|
44
|
+
if (!source.trim().startsWith('#cloud-config')) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
'YAML cloud config must start with a #cloud-config comment',
|
|
47
|
+
)
|
|
48
|
+
}
|
|
43
49
|
return 'cloud-config'
|
|
44
50
|
} else if (filename.endsWith('.sh')) {
|
|
45
51
|
return 'x-shellscript'
|
|
46
52
|
} else {
|
|
47
|
-
throw new Error(
|
|
53
|
+
throw new Error(`${filename} is an unsupported file type`)
|
|
48
54
|
}
|
|
49
55
|
}
|
package/lib/build.spec.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { afterEach, beforeEach, expect, test } from 'bun:test'
|
|
2
|
-
import { buildUserData } from '#c2/build
|
|
3
|
-
import { makeFile, makeTempDir, removeDir } from '#c2/fs.testing
|
|
2
|
+
import { buildUserData } from '#c2/build'
|
|
3
|
+
import { makeFile, makeTempDir, removeDir } from '#c2/fs.testing'
|
|
4
4
|
|
|
5
5
|
let tmpDir: string
|
|
6
6
|
|
|
@@ -11,9 +11,9 @@ beforeEach(async () => {
|
|
|
11
11
|
afterEach(async () => removeDir(tmpDir))
|
|
12
12
|
|
|
13
13
|
test('build user data of single file', async () => {
|
|
14
|
-
const initCloudYml = '
|
|
14
|
+
const initCloudYml = '#cloud-config\nwhoopie'
|
|
15
15
|
await makeFile('init-cloud.yml', initCloudYml, tmpDir)
|
|
16
|
-
expect(await buildUserData(tmpDir)).toStrictEqual(
|
|
16
|
+
expect(await buildUserData(tmpDir)).toStrictEqual(initCloudYml)
|
|
17
17
|
})
|
|
18
18
|
|
|
19
19
|
test('build user data of single file with template expression', async () => {
|
|
@@ -21,38 +21,37 @@ test('build user data of single file with template expression', async () => {
|
|
|
21
21
|
await makeFile('whoopie', 'whoopie', whoopie)
|
|
22
22
|
await makeFile(
|
|
23
23
|
'init-cloud.yml',
|
|
24
|
-
|
|
24
|
+
`#cloud-config\n\${{ file('${whoopie}/whoopie')}}`,
|
|
25
25
|
tmpDir,
|
|
26
26
|
)
|
|
27
|
-
expect(await buildUserData(tmpDir)).toStrictEqual('
|
|
27
|
+
expect(await buildUserData(tmpDir)).toStrictEqual('#cloud-config\nwhoopie')
|
|
28
28
|
await removeDir(whoopie)
|
|
29
29
|
})
|
|
30
30
|
|
|
31
31
|
test('build user data multipart message', async () => {
|
|
32
|
-
await makeFile('1-init-cloud.yml', '
|
|
32
|
+
await makeFile('1-init-cloud.yml', '#cloud-config\nwhoopie', tmpDir)
|
|
33
33
|
await makeFile('2-init-cloud.sh', 'cushion', tmpDir)
|
|
34
34
|
const boundary = Bun.randomUUIDv7()
|
|
35
35
|
expect(
|
|
36
36
|
await buildUserData(tmpDir, { attachmentBoundary: boundary }),
|
|
37
37
|
).toStrictEqual(
|
|
38
|
-
`Content-Type: multipart/mixed; boundary=${boundary}
|
|
39
|
-
MIME-Version: 1.0
|
|
40
|
-
Number-Attachments: 2
|
|
41
|
-
--${boundary}
|
|
42
|
-
Content-Type: text/cloud-config; charset="us-ascii"
|
|
43
|
-
|
|
44
|
-
Content-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
whoopie
|
|
48
|
-
--${boundary}
|
|
49
|
-
Content-Type: text/x-shellscript; charset="us-ascii"
|
|
50
|
-
|
|
51
|
-
Content-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
--${boundary}
|
|
38
|
+
`Content-Type: multipart/mixed; boundary=${boundary}\r
|
|
39
|
+
MIME-Version: 1.0\r
|
|
40
|
+
Number-Attachments: 2\r
|
|
41
|
+
--${boundary}\r
|
|
42
|
+
Content-Type: text/cloud-config; charset="us-ascii"\r
|
|
43
|
+
Content-Transfer-Encoding: 7bit\r
|
|
44
|
+
Content-Disposition: attachment; filename="1-init-cloud.yml"\r
|
|
45
|
+
\r
|
|
46
|
+
#cloud-config
|
|
47
|
+
whoopie\r
|
|
48
|
+
--${boundary}\r
|
|
49
|
+
Content-Type: text/x-shellscript; charset="us-ascii"\r
|
|
50
|
+
Content-Transfer-Encoding: 7bit\r
|
|
51
|
+
Content-Disposition: attachment; filename="2-init-cloud.sh"\r
|
|
52
|
+
\r
|
|
53
|
+
cushion\r
|
|
54
|
+
--${boundary}--\r
|
|
56
55
|
`,
|
|
57
56
|
)
|
|
58
57
|
})
|
|
@@ -62,7 +61,7 @@ test('build user data multipart with template expression', async () => {
|
|
|
62
61
|
await makeFile('whoopie', 'whoopie', whoopie)
|
|
63
62
|
await makeFile(
|
|
64
63
|
'1-init-cloud.yml',
|
|
65
|
-
|
|
64
|
+
`#cloud-config\n\${{ file('${whoopie}/whoopie')}}`,
|
|
66
65
|
tmpDir,
|
|
67
66
|
)
|
|
68
67
|
await makeFile('2-init-cloud.sh', 'cushion', tmpDir)
|
|
@@ -70,24 +69,22 @@ test('build user data multipart with template expression', async () => {
|
|
|
70
69
|
expect(
|
|
71
70
|
await buildUserData(tmpDir, { attachmentBoundary: boundary }),
|
|
72
71
|
).toStrictEqual(
|
|
73
|
-
`Content-Type: multipart/mixed; boundary=${boundary}
|
|
74
|
-
MIME-Version: 1.0
|
|
75
|
-
Number-Attachments: 2
|
|
76
|
-
--${boundary}
|
|
77
|
-
Content-Type: text/cloud-config; charset="us-ascii"
|
|
78
|
-
|
|
79
|
-
Content-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
Content-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
cushion
|
|
90
|
-
--${boundary}
|
|
72
|
+
`Content-Type: multipart/mixed; boundary=${boundary}\r
|
|
73
|
+
MIME-Version: 1.0\r
|
|
74
|
+
Number-Attachments: 2\r
|
|
75
|
+
--${boundary}\r
|
|
76
|
+
Content-Type: text/cloud-config; charset="us-ascii"\r
|
|
77
|
+
Content-Transfer-Encoding: 7bit\r
|
|
78
|
+
Content-Disposition: attachment; filename="1-init-cloud.yml"\r
|
|
79
|
+
\r
|
|
80
|
+
#cloud-config\nwhoopie\r
|
|
81
|
+
--${boundary}\r
|
|
82
|
+
Content-Type: text/x-shellscript; charset="us-ascii"\r
|
|
83
|
+
Content-Transfer-Encoding: 7bit\r
|
|
84
|
+
Content-Disposition: attachment; filename="2-init-cloud.sh"\r
|
|
85
|
+
\r
|
|
86
|
+
cushion\r
|
|
87
|
+
--${boundary}--\r
|
|
91
88
|
`,
|
|
92
89
|
)
|
|
93
90
|
})
|
package/lib/build.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { readToString } from '#c2/fs.ts'
|
|
1
|
+
import { collectAttachments } from '#c2/attachments'
|
|
2
|
+
import { MultipartMessage } from '#c2/http'
|
|
4
3
|
|
|
5
4
|
export type BuildUserDataOpts = {
|
|
6
5
|
attachmentBoundary?: string
|
|
@@ -15,45 +14,11 @@ export async function buildUserData(
|
|
|
15
14
|
case 0:
|
|
16
15
|
throw new Error(`nothing found in dir ${userDataDir}`)
|
|
17
16
|
case 1:
|
|
18
|
-
return
|
|
19
|
-
await readToString(attachments[0].path),
|
|
20
|
-
)
|
|
17
|
+
return attachments[0].content
|
|
21
18
|
default:
|
|
22
|
-
return
|
|
19
|
+
return new MultipartMessage(
|
|
20
|
+
attachments,
|
|
21
|
+
opts?.attachmentBoundary,
|
|
22
|
+
).toHTTP()
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
|
-
|
|
26
|
-
function createBoundary(): string {
|
|
27
|
-
return new Date().toISOString()
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
async function buildMultipartUserData(
|
|
31
|
-
attachments: Array<Attachment>,
|
|
32
|
-
boundary: string = createBoundary(),
|
|
33
|
-
): Promise<string> {
|
|
34
|
-
let result = `Content-Type: multipart/mixed; boundary=${boundary}
|
|
35
|
-
MIME-Version: 1.0
|
|
36
|
-
Number-Attachments: ${attachments.length}
|
|
37
|
-
--${boundary}
|
|
38
|
-
`
|
|
39
|
-
|
|
40
|
-
for (const attachment of attachments) {
|
|
41
|
-
let content: string
|
|
42
|
-
try {
|
|
43
|
-
content = await evalTemplateExpressions(attachment.content)
|
|
44
|
-
} catch (e: any) {
|
|
45
|
-
throw new Error(
|
|
46
|
-
`error templating ${attachment.filename}: ${e.message}`,
|
|
47
|
-
)
|
|
48
|
-
}
|
|
49
|
-
result += `Content-Type: text/${attachment.type}; charset="us-ascii"
|
|
50
|
-
MIME-Version: 1.0
|
|
51
|
-
Content-Transfer-Encoding: 7bit
|
|
52
|
-
Content-Disposition: attachment; filename="${attachment.filename}"
|
|
53
|
-
|
|
54
|
-
${content}
|
|
55
|
-
--${boundary}
|
|
56
|
-
`
|
|
57
|
-
}
|
|
58
|
-
return result
|
|
59
|
-
}
|
package/lib/c2.api.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { type BuildUserDataOpts, buildUserData } from '#c2/build
|
|
1
|
+
export { type BuildUserDataOpts, buildUserData } from '#c2/build'
|
package/lib/c2.bin.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { buildUserData } from '#c2/build
|
|
4
|
-
import { parseArgs, type ParsedArgs } from '#c2/cli
|
|
5
|
-
import { doesDirExist } from '#c2/fs
|
|
3
|
+
import { buildUserData } from '#c2/build'
|
|
4
|
+
import { parseArgs, type ParsedArgs } from '#c2/cli'
|
|
5
|
+
import { doesDirExist } from '#c2/fs'
|
|
6
|
+
import { startUserDataHttp } from '#c2/http'
|
|
6
7
|
|
|
7
8
|
let args: ParsedArgs | undefined
|
|
8
9
|
try {
|
|
@@ -21,17 +22,25 @@ if (!args || args.help) {
|
|
|
21
22
|
)
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
if (args.httpPort) {
|
|
25
|
-
errorExit('--http PORT is not yet implemented')
|
|
26
|
-
}
|
|
27
|
-
|
|
28
25
|
if (!(await doesDirExist(args.userDataDir))) {
|
|
29
26
|
errorExit(`${args.userDataDir} directory does not exist`)
|
|
30
27
|
}
|
|
31
28
|
|
|
29
|
+
let work: () => Promise<void>
|
|
30
|
+
|
|
31
|
+
if (typeof args.httpPort !== 'undefined') {
|
|
32
|
+
work = async () => {
|
|
33
|
+
startUserDataHttp(args.httpPort!, args.userDataDir)
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
work = async () => {
|
|
37
|
+
const userData = await buildUserData(args.userDataDir)
|
|
38
|
+
console.log(args.base64 ? btoa(userData) : userData)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
32
42
|
try {
|
|
33
|
-
|
|
34
|
-
console.log(args.base64 ? btoa(userData) : userData)
|
|
43
|
+
await work()
|
|
35
44
|
} catch (e: any) {
|
|
36
45
|
errorExit(e.message)
|
|
37
46
|
}
|
package/lib/cli.spec.ts
CHANGED
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
import { expect, test } from 'bun:test'
|
|
2
|
-
import { parseArgs } from '#c2/cli
|
|
2
|
+
import { parseArgs } from '#c2/cli'
|
|
3
3
|
|
|
4
|
-
test('parseArgs', () => {
|
|
4
|
+
test('parseArgs with ts entrypoint', () => {
|
|
5
5
|
expect(
|
|
6
6
|
parseArgs([
|
|
7
7
|
'/Users/who/.bun/bin/bun',
|
|
8
|
-
'/Users/who/user-data/c2.ts',
|
|
8
|
+
'/Users/who/user-data/lib/c2.bin.ts',
|
|
9
|
+
'user_data_dir',
|
|
10
|
+
]),
|
|
11
|
+
).toStrictEqual({
|
|
12
|
+
base64: false,
|
|
13
|
+
userDataDir: 'user_data_dir',
|
|
14
|
+
})
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('parseArgs with js entrypoint', () => {
|
|
18
|
+
expect(
|
|
19
|
+
parseArgs([
|
|
20
|
+
'/Users/who/.nvm/versions/node/v23.7.0/bin/node',
|
|
21
|
+
'/Users/who/user-data/lib_js/c2.bin.js',
|
|
9
22
|
'user_data_dir',
|
|
10
23
|
]),
|
|
11
24
|
).toStrictEqual({
|
|
@@ -16,7 +29,10 @@ test('parseArgs', () => {
|
|
|
16
29
|
|
|
17
30
|
test('parseArgs errors without USER_DATA_DIR', () => {
|
|
18
31
|
expect(() =>
|
|
19
|
-
parseArgs([
|
|
32
|
+
parseArgs([
|
|
33
|
+
'/Users/who/.bun/bin/bun',
|
|
34
|
+
'/Users/who/user-data/lib/c2.bin.ts',
|
|
35
|
+
]),
|
|
20
36
|
).toThrow()
|
|
21
37
|
})
|
|
22
38
|
|
|
@@ -24,7 +40,7 @@ test('parseArgs errors with extra USER_DATA_DIR', () => {
|
|
|
24
40
|
expect(() =>
|
|
25
41
|
parseArgs([
|
|
26
42
|
'/Users/who/.bun/bin/bun',
|
|
27
|
-
'/Users/who/user-data/c2.ts',
|
|
43
|
+
'/Users/who/user-data/lib/c2.bin.ts',
|
|
28
44
|
'user_data_dir',
|
|
29
45
|
'some_other_arg',
|
|
30
46
|
]),
|
|
@@ -35,7 +51,7 @@ test('parseArgs with --base64', () => {
|
|
|
35
51
|
expect(
|
|
36
52
|
parseArgs([
|
|
37
53
|
'/Users/who/.bun/bin/bun',
|
|
38
|
-
'/Users/who/user-data/c2.ts',
|
|
54
|
+
'/Users/who/user-data/lib/c2.bin.ts',
|
|
39
55
|
'--base64',
|
|
40
56
|
'user_data_dir',
|
|
41
57
|
]),
|
|
@@ -49,7 +65,7 @@ test('parseArgs with --http PORT', () => {
|
|
|
49
65
|
expect(
|
|
50
66
|
parseArgs([
|
|
51
67
|
'/Users/who/.bun/bin/bun',
|
|
52
|
-
'/Users/who/user-data/c2.ts',
|
|
68
|
+
'/Users/who/user-data/lib/c2.bin.ts',
|
|
53
69
|
'--http',
|
|
54
70
|
'6666',
|
|
55
71
|
'user_data_dir',
|
|
@@ -61,10 +77,21 @@ test('parseArgs with --http bunk', () => {
|
|
|
61
77
|
expect(() =>
|
|
62
78
|
parseArgs([
|
|
63
79
|
'/Users/who/.bun/bin/bun',
|
|
64
|
-
'/Users/who/user-data/c2.ts',
|
|
80
|
+
'/Users/who/user-data/lib/c2.bin.ts',
|
|
65
81
|
'--http',
|
|
66
82
|
'bunk',
|
|
67
83
|
'user_data_dir',
|
|
68
84
|
]),
|
|
69
85
|
).toThrow('--http bunk is not a valid http port')
|
|
70
86
|
})
|
|
87
|
+
|
|
88
|
+
test('parseArgs with --http without PORT', () => {
|
|
89
|
+
expect(() =>
|
|
90
|
+
parseArgs([
|
|
91
|
+
'/Users/who/.bun/bin/bun',
|
|
92
|
+
'/Users/who/user-data/lib/c2.bin.ts',
|
|
93
|
+
'user_data_dir',
|
|
94
|
+
'--http',
|
|
95
|
+
]),
|
|
96
|
+
).toThrow('--http did not include a PORT')
|
|
97
|
+
})
|
package/lib/cli.ts
CHANGED
|
@@ -12,7 +12,15 @@ export function parseArgs(args?: Array<string>): ParsedArgs {
|
|
|
12
12
|
args = process.argv
|
|
13
13
|
}
|
|
14
14
|
args = [...args]
|
|
15
|
-
|
|
15
|
+
let shifted
|
|
16
|
+
while ((shifted = args.shift())) {
|
|
17
|
+
if (
|
|
18
|
+
shifted.endsWith('/lib_js/c2.bin.js') ||
|
|
19
|
+
shifted.endsWith('/lib/c2.bin.ts')
|
|
20
|
+
) {
|
|
21
|
+
break
|
|
22
|
+
}
|
|
23
|
+
}
|
|
16
24
|
let base64 = false
|
|
17
25
|
let httpPort: number | undefined
|
|
18
26
|
let userData: Array<string> = []
|
|
@@ -34,6 +42,9 @@ export function parseArgs(args?: Array<string>): ParsedArgs {
|
|
|
34
42
|
userData.push(arg)
|
|
35
43
|
}
|
|
36
44
|
}
|
|
45
|
+
if (expectHttpPort) {
|
|
46
|
+
throw new Error('--http did not include a PORT')
|
|
47
|
+
}
|
|
37
48
|
switch (userData.length) {
|
|
38
49
|
case 1:
|
|
39
50
|
const userDataDir = userData[0]
|
package/lib/expression.spec.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { afterAll, afterEach, beforeEach, expect, test } from 'bun:test'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
|
-
import { evalTemplateExpressions } from '
|
|
4
|
-
import { makeFile, makeTempDir, removeDir } from '
|
|
3
|
+
import { evalTemplateExpressions } from './expression.ts'
|
|
4
|
+
import { makeFile, makeTempDir, removeDir } from './fs.testing.ts'
|
|
5
5
|
|
|
6
6
|
let files: Array<string> = []
|
|
7
7
|
let tmpDir: string
|
package/lib/expression.ts
CHANGED
package/lib/http.spec.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { beforeEach, expect, test } from 'bun:test'
|
|
2
|
+
import { MultipartMessage, MultipartAttachment } from '#c2/http'
|
|
3
|
+
|
|
4
|
+
let message: MultipartMessage
|
|
5
|
+
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
message = new MultipartMessage(
|
|
8
|
+
[
|
|
9
|
+
{
|
|
10
|
+
path: 'upgrades',
|
|
11
|
+
content: '#cloud-config\nupgrades',
|
|
12
|
+
filename: 'upgrades.yml',
|
|
13
|
+
source: '#cloud-config\nupgrades',
|
|
14
|
+
type: 'cloud-config',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
path: 'security',
|
|
18
|
+
content: 'security',
|
|
19
|
+
filename: 'security.sh',
|
|
20
|
+
source: 'security',
|
|
21
|
+
type: 'x-shellscript',
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
'BOUNDARY',
|
|
25
|
+
)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('multipart message creates cloud-init payload', () => {
|
|
29
|
+
expect(message.toHTTP()).toStrictEqual(
|
|
30
|
+
`Content-Type: multipart/mixed; boundary=BOUNDARY\r
|
|
31
|
+
MIME-Version: 1.0\r
|
|
32
|
+
Number-Attachments: 2\r
|
|
33
|
+
--BOUNDARY\r
|
|
34
|
+
Content-Type: text/cloud-config; charset="us-ascii"\r
|
|
35
|
+
Content-Transfer-Encoding: 7bit\r
|
|
36
|
+
Content-Disposition: attachment; filename="upgrades.yml"\r
|
|
37
|
+
\r
|
|
38
|
+
#cloud-config
|
|
39
|
+
upgrades\r
|
|
40
|
+
--BOUNDARY\r
|
|
41
|
+
Content-Type: text/x-shellscript; charset="us-ascii"\r
|
|
42
|
+
Content-Transfer-Encoding: 7bit\r
|
|
43
|
+
Content-Disposition: attachment; filename="security.sh"\r
|
|
44
|
+
\r
|
|
45
|
+
security\r
|
|
46
|
+
--BOUNDARY--\r
|
|
47
|
+
`,
|
|
48
|
+
)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('multipart message creates HTTP headers', () => {
|
|
52
|
+
expect(message.headers).toStrictEqual({
|
|
53
|
+
'Content-Type': 'multipart/mixed; boundary=BOUNDARY',
|
|
54
|
+
'MIME-Version': '1.0',
|
|
55
|
+
'Number-Attachments': '2',
|
|
56
|
+
})
|
|
57
|
+
})
|
package/lib/http.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
import { createServer, type ServerResponse } from 'node:http'
|
|
3
|
+
import { type Attachment, collectAttachments } from '#c2/attachments'
|
|
4
|
+
|
|
5
|
+
export function startUserDataHttp(port: number, userDataDir: string) {
|
|
6
|
+
const server = createServer((req, res) => {
|
|
7
|
+
console.log(req.method, req.url)
|
|
8
|
+
if (req.method !== 'GET') {
|
|
9
|
+
res.writeHead(405)
|
|
10
|
+
res.end()
|
|
11
|
+
} else if (req.url === '/user-data') {
|
|
12
|
+
sendUserData(res, userDataDir).then()
|
|
13
|
+
} else {
|
|
14
|
+
res.writeHead(
|
|
15
|
+
req.url === '/meta-data' || req.url === '/network-config'
|
|
16
|
+
? 200
|
|
17
|
+
: 404,
|
|
18
|
+
)
|
|
19
|
+
res.end()
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
server.listen(port, () => {
|
|
23
|
+
console.log(userDataDir, `up @ http://localhost:${port}/user-data`)
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function sendUserData(res: ServerResponse, userDataDir: string) {
|
|
28
|
+
try {
|
|
29
|
+
const message = new MultipartMessage(
|
|
30
|
+
await collectAttachments(userDataDir),
|
|
31
|
+
)
|
|
32
|
+
res.writeHead(200, message.headers)
|
|
33
|
+
res.write(message.responseBody())
|
|
34
|
+
} catch (e: any) {
|
|
35
|
+
console.error(500, '/user-data', e.message)
|
|
36
|
+
res.writeHead(500)
|
|
37
|
+
} finally {
|
|
38
|
+
res.end()
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class MultipartMessage {
|
|
43
|
+
static createBoundary(): string {
|
|
44
|
+
return randomUUID()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
readonly #attachments: Array<MultipartAttachment>
|
|
48
|
+
readonly #boundary: string
|
|
49
|
+
|
|
50
|
+
constructor(
|
|
51
|
+
attachments: Array<Attachment>,
|
|
52
|
+
boundary: string = MultipartMessage.createBoundary(),
|
|
53
|
+
) {
|
|
54
|
+
this.#attachments = attachments.map(a => new MultipartAttachment(a))
|
|
55
|
+
this.#boundary = boundary
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get attachments(): Array<MultipartAttachment> {
|
|
59
|
+
return this.#attachments
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
get boundary(): string {
|
|
63
|
+
return this.#boundary
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
get headers(): Record<string, string> {
|
|
67
|
+
return {
|
|
68
|
+
'Content-Type': `multipart/mixed; boundary=${this.#boundary}`,
|
|
69
|
+
'MIME-Version': '1.0',
|
|
70
|
+
'Number-Attachments': `${this.#attachments.length}`,
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
toHTTP(): string {
|
|
75
|
+
return [
|
|
76
|
+
`Content-Type: multipart/mixed; boundary=${this.#boundary}`,
|
|
77
|
+
'MIME-Version: 1.0',
|
|
78
|
+
`Number-Attachments: ${this.#attachments.length}`,
|
|
79
|
+
this.responseBody(),
|
|
80
|
+
].join('\r\n')
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
responseBody(): string {
|
|
84
|
+
return [
|
|
85
|
+
'--' + this.#boundary,
|
|
86
|
+
this.#attachments
|
|
87
|
+
.map(a => a.toHTTP())
|
|
88
|
+
.join('\r\n--' + this.#boundary + '\r\n'),
|
|
89
|
+
'--' + this.#boundary + '--\r\n',
|
|
90
|
+
].join('\r\n')
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export class MultipartAttachment {
|
|
95
|
+
#attachment: Attachment
|
|
96
|
+
|
|
97
|
+
constructor(attachment: Attachment) {
|
|
98
|
+
this.#attachment = attachment
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
toHTTP(): string {
|
|
102
|
+
return [
|
|
103
|
+
`Content-Type: text/${this.#attachment.type}; charset="us-ascii"`,
|
|
104
|
+
'Content-Transfer-Encoding: 7bit',
|
|
105
|
+
`Content-Disposition: attachment; filename="${this.#attachment.filename}"`,
|
|
106
|
+
'',
|
|
107
|
+
this.#attachment.content,
|
|
108
|
+
].join('\r\n')
|
|
109
|
+
}
|
|
110
|
+
}
|
package/lib_js/attachments.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { evalTemplateExpressions } from '#c2/expression';
|
|
2
|
+
import { readDirListing, readToString } from '#c2/fs';
|
|
2
3
|
export async function collectAttachments(dir) {
|
|
3
|
-
const
|
|
4
|
-
|
|
4
|
+
const filenames = await readDirListing(dir);
|
|
5
|
+
const attachments = await Promise.all(filenames.map(async (filename) => {
|
|
5
6
|
const path = `${dir}/${filename}`;
|
|
6
|
-
const
|
|
7
|
-
const type = resolveAttachmentType(filename,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
const source = await readToString(path);
|
|
8
|
+
const type = resolveAttachmentType(filename, source);
|
|
9
|
+
const content = await evalTemplateExpressions(source);
|
|
10
|
+
return { content, filename, path, type, source };
|
|
11
|
+
}));
|
|
12
|
+
return attachments.sort(compareAttachmentFilenames);
|
|
11
13
|
}
|
|
12
14
|
function compareAttachmentFilenames(a1, a2) {
|
|
13
15
|
if (a1.filename === a2.filename)
|
|
@@ -16,16 +18,17 @@ function compareAttachmentFilenames(a1, a2) {
|
|
|
16
18
|
return 1;
|
|
17
19
|
return -1;
|
|
18
20
|
}
|
|
19
|
-
export function resolveAttachmentType(filename,
|
|
20
|
-
if (filename.endsWith('.yml') ||
|
|
21
|
-
(
|
|
22
|
-
|
|
21
|
+
export function resolveAttachmentType(filename, source) {
|
|
22
|
+
if (filename.endsWith('.yml') || filename.endsWith('.yaml')) {
|
|
23
|
+
if (!source.trim().startsWith('#cloud-config')) {
|
|
24
|
+
throw new Error('YAML cloud config must start with a #cloud-config comment');
|
|
25
|
+
}
|
|
23
26
|
return 'cloud-config';
|
|
24
27
|
}
|
|
25
28
|
else if (filename.endsWith('.sh')) {
|
|
26
29
|
return 'x-shellscript';
|
|
27
30
|
}
|
|
28
31
|
else {
|
|
29
|
-
throw new Error(
|
|
32
|
+
throw new Error(`${filename} is an unsupported file type`);
|
|
30
33
|
}
|
|
31
34
|
}
|
package/lib_js/build.js
CHANGED
|
@@ -1,42 +1,13 @@
|
|
|
1
|
-
import { collectAttachments } from '#c2/attachments
|
|
2
|
-
import {
|
|
3
|
-
import { readToString } from '#c2/fs.ts';
|
|
1
|
+
import { collectAttachments } from '#c2/attachments';
|
|
2
|
+
import { MultipartMessage } from '#c2/http';
|
|
4
3
|
export async function buildUserData(userDataDir, opts) {
|
|
5
4
|
const attachments = await collectAttachments(userDataDir);
|
|
6
5
|
switch (attachments.length) {
|
|
7
6
|
case 0:
|
|
8
7
|
throw new Error(`nothing found in dir ${userDataDir}`);
|
|
9
8
|
case 1:
|
|
10
|
-
return
|
|
9
|
+
return attachments[0].content;
|
|
11
10
|
default:
|
|
12
|
-
return
|
|
11
|
+
return new MultipartMessage(attachments, opts?.attachmentBoundary).toHTTP();
|
|
13
12
|
}
|
|
14
13
|
}
|
|
15
|
-
function createBoundary() {
|
|
16
|
-
return new Date().toISOString();
|
|
17
|
-
}
|
|
18
|
-
async function buildMultipartUserData(attachments, boundary = createBoundary()) {
|
|
19
|
-
let result = `Content-Type: multipart/mixed; boundary=${boundary}
|
|
20
|
-
MIME-Version: 1.0
|
|
21
|
-
Number-Attachments: ${attachments.length}
|
|
22
|
-
--${boundary}
|
|
23
|
-
`;
|
|
24
|
-
for (const attachment of attachments) {
|
|
25
|
-
let content;
|
|
26
|
-
try {
|
|
27
|
-
content = await evalTemplateExpressions(attachment.content);
|
|
28
|
-
}
|
|
29
|
-
catch (e) {
|
|
30
|
-
throw new Error(`error templating ${attachment.filename}: ${e.message}`);
|
|
31
|
-
}
|
|
32
|
-
result += `Content-Type: text/${attachment.type}; charset="us-ascii"
|
|
33
|
-
MIME-Version: 1.0
|
|
34
|
-
Content-Transfer-Encoding: 7bit
|
|
35
|
-
Content-Disposition: attachment; filename="${attachment.filename}"
|
|
36
|
-
|
|
37
|
-
${content}
|
|
38
|
-
--${boundary}
|
|
39
|
-
`;
|
|
40
|
-
}
|
|
41
|
-
return result;
|
|
42
|
-
}
|
package/lib_js/c2.api.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { buildUserData } from '#c2/build
|
|
1
|
+
export { buildUserData } from '#c2/build';
|
package/lib_js/c2.bin.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { buildUserData } from '#c2/build
|
|
3
|
-
import { parseArgs } from '#c2/cli
|
|
4
|
-
import { doesDirExist } from '#c2/fs
|
|
2
|
+
import { buildUserData } from '#c2/build';
|
|
3
|
+
import { parseArgs } from '#c2/cli';
|
|
4
|
+
import { doesDirExist } from '#c2/fs';
|
|
5
|
+
import { startUserDataHttp } from '#c2/http';
|
|
5
6
|
let args;
|
|
6
7
|
try {
|
|
7
8
|
args = parseArgs();
|
|
@@ -16,15 +17,23 @@ if (!args || args.help) {
|
|
|
16
17
|
const required = (s) => `\u001b[1m${s}\u001b[0m`;
|
|
17
18
|
errorExit(`c2 ${optional('[[--base64] | [--http PORT]]')} ${required('USER_DATA_DIR')}`);
|
|
18
19
|
}
|
|
19
|
-
if (args.httpPort) {
|
|
20
|
-
errorExit('--http PORT is not yet implemented');
|
|
21
|
-
}
|
|
22
20
|
if (!(await doesDirExist(args.userDataDir))) {
|
|
23
21
|
errorExit(`${args.userDataDir} directory does not exist`);
|
|
24
22
|
}
|
|
23
|
+
let work;
|
|
24
|
+
if (typeof args.httpPort !== 'undefined') {
|
|
25
|
+
work = async () => {
|
|
26
|
+
startUserDataHttp(args.httpPort, args.userDataDir);
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
work = async () => {
|
|
31
|
+
const userData = await buildUserData(args.userDataDir);
|
|
32
|
+
console.log(args.base64 ? btoa(userData) : userData);
|
|
33
|
+
};
|
|
34
|
+
}
|
|
25
35
|
try {
|
|
26
|
-
|
|
27
|
-
console.log(args.base64 ? btoa(userData) : userData);
|
|
36
|
+
await work();
|
|
28
37
|
}
|
|
29
38
|
catch (e) {
|
|
30
39
|
errorExit(e.message);
|
package/lib_js/cli.js
CHANGED
|
@@ -3,7 +3,13 @@ export function parseArgs(args) {
|
|
|
3
3
|
args = process.argv;
|
|
4
4
|
}
|
|
5
5
|
args = [...args];
|
|
6
|
-
|
|
6
|
+
let shifted;
|
|
7
|
+
while ((shifted = args.shift())) {
|
|
8
|
+
if (shifted.endsWith('/lib_js/c2.bin.js') ||
|
|
9
|
+
shifted.endsWith('/lib/c2.bin.ts')) {
|
|
10
|
+
break;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
7
13
|
let base64 = false;
|
|
8
14
|
let httpPort;
|
|
9
15
|
let userData = [];
|
|
@@ -29,6 +35,9 @@ export function parseArgs(args) {
|
|
|
29
35
|
userData.push(arg);
|
|
30
36
|
}
|
|
31
37
|
}
|
|
38
|
+
if (expectHttpPort) {
|
|
39
|
+
throw new Error('--http did not include a PORT');
|
|
40
|
+
}
|
|
32
41
|
switch (userData.length) {
|
|
33
42
|
case 1:
|
|
34
43
|
const userDataDir = userData[0];
|
package/lib_js/expression.js
CHANGED
package/lib_js/http.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { createServer } from 'node:http';
|
|
3
|
+
import { collectAttachments } from '#c2/attachments';
|
|
4
|
+
export function startUserDataHttp(port, userDataDir) {
|
|
5
|
+
const server = createServer((req, res) => {
|
|
6
|
+
console.log(req.method, req.url);
|
|
7
|
+
if (req.method !== 'GET') {
|
|
8
|
+
res.writeHead(405);
|
|
9
|
+
res.end();
|
|
10
|
+
}
|
|
11
|
+
else if (req.url === '/user-data') {
|
|
12
|
+
sendUserData(res, userDataDir).then();
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
res.writeHead(req.url === '/meta-data' || req.url === '/network-config'
|
|
16
|
+
? 200
|
|
17
|
+
: 404);
|
|
18
|
+
res.end();
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
server.listen(port, () => {
|
|
22
|
+
console.log(userDataDir, `up @ http://localhost:${port}/user-data`);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
async function sendUserData(res, userDataDir) {
|
|
26
|
+
try {
|
|
27
|
+
const message = new MultipartMessage(await collectAttachments(userDataDir));
|
|
28
|
+
res.writeHead(200, message.headers);
|
|
29
|
+
res.write(message.responseBody());
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
console.error(500, '/user-data', e.message);
|
|
33
|
+
res.writeHead(500);
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
res.end();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export class MultipartMessage {
|
|
40
|
+
static createBoundary() {
|
|
41
|
+
return randomUUID();
|
|
42
|
+
}
|
|
43
|
+
#attachments;
|
|
44
|
+
#boundary;
|
|
45
|
+
constructor(attachments, boundary = MultipartMessage.createBoundary()) {
|
|
46
|
+
this.#attachments = attachments.map(a => new MultipartAttachment(a));
|
|
47
|
+
this.#boundary = boundary;
|
|
48
|
+
}
|
|
49
|
+
get attachments() {
|
|
50
|
+
return this.#attachments;
|
|
51
|
+
}
|
|
52
|
+
get boundary() {
|
|
53
|
+
return this.#boundary;
|
|
54
|
+
}
|
|
55
|
+
get headers() {
|
|
56
|
+
return {
|
|
57
|
+
'Content-Type': `multipart/mixed; boundary=${this.#boundary}`,
|
|
58
|
+
'MIME-Version': '1.0',
|
|
59
|
+
'Number-Attachments': `${this.#attachments.length}`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
toHTTP() {
|
|
63
|
+
return [
|
|
64
|
+
`Content-Type: multipart/mixed; boundary=${this.#boundary}`,
|
|
65
|
+
'MIME-Version: 1.0',
|
|
66
|
+
`Number-Attachments: ${this.#attachments.length}`,
|
|
67
|
+
this.responseBody(),
|
|
68
|
+
].join('\r\n');
|
|
69
|
+
}
|
|
70
|
+
responseBody() {
|
|
71
|
+
return [
|
|
72
|
+
'--' + this.#boundary,
|
|
73
|
+
this.#attachments
|
|
74
|
+
.map(a => a.toHTTP())
|
|
75
|
+
.join('\r\n--' + this.#boundary + '\r\n'),
|
|
76
|
+
'--' + this.#boundary + '--\r\n',
|
|
77
|
+
].join('\r\n');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export class MultipartAttachment {
|
|
81
|
+
#attachment;
|
|
82
|
+
constructor(attachment) {
|
|
83
|
+
this.#attachment = attachment;
|
|
84
|
+
}
|
|
85
|
+
toHTTP() {
|
|
86
|
+
return [
|
|
87
|
+
`Content-Type: text/${this.#attachment.type}; charset="us-ascii"`,
|
|
88
|
+
'Content-Transfer-Encoding: 7bit',
|
|
89
|
+
`Content-Disposition: attachment; filename="${this.#attachment.filename}"`,
|
|
90
|
+
'',
|
|
91
|
+
this.#attachment.content,
|
|
92
|
+
].join('\r\n');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -3,8 +3,9 @@ export interface Attachment {
|
|
|
3
3
|
path: string;
|
|
4
4
|
content: string;
|
|
5
5
|
filename: string;
|
|
6
|
+
source: string;
|
|
6
7
|
type: AttachmentType;
|
|
7
8
|
}
|
|
8
9
|
export declare function collectAttachments(dir: string): Promise<Array<Attachment>>;
|
|
9
|
-
export declare function resolveAttachmentType(filename: string,
|
|
10
|
+
export declare function resolveAttachmentType(filename: string, source: string): AttachmentType;
|
|
10
11
|
//# sourceMappingURL=attachments.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attachments.d.ts","sourceRoot":"","sources":["../lib/attachments.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"attachments.d.ts","sourceRoot":"","sources":["../lib/attachments.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,cAAc,GAAG,cAAc,GAAG,eAAe,CAAA;AAE7D,MAAM,WAAW,UAAU;IACvB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,cAAc,CAAA;CACvB;AAED,wBAAsB,kBAAkB,CACpC,GAAG,EAAE,MAAM,GACZ,OAAO,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAY5B;AAWD,wBAAgB,qBAAqB,CACjC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GACf,cAAc,CAahB"}
|
package/lib_types/build.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../lib/build.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../lib/build.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,iBAAiB,GAAG;IAC5B,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAC9B,CAAA;AAED,wBAAsB,aAAa,CAC/B,WAAW,EAAE,MAAM,EACnB,IAAI,CAAC,EAAE,iBAAiB,GACzB,OAAO,CAAC,MAAM,CAAC,CAajB"}
|
package/lib_types/c2.api.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { type BuildUserDataOpts, buildUserData } from '#c2/build
|
|
1
|
+
export { type BuildUserDataOpts, buildUserData } from '#c2/build';
|
|
2
2
|
//# sourceMappingURL=c2.api.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"c2.api.d.ts","sourceRoot":"","sources":["../lib/c2.api.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,iBAAiB,EAAE,aAAa,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"c2.api.d.ts","sourceRoot":"","sources":["../lib/c2.api.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,iBAAiB,EAAE,aAAa,EAAE,MAAM,WAAW,CAAA"}
|
package/lib_types/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../lib/cli.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAChB;IAAE,IAAI,EAAE,IAAI,CAAA;CAAE,GACd;IACI,IAAI,CAAC,EAAE,KAAK,CAAA;IACZ,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;CACtB,CAAA;AAEP,wBAAgB,SAAS,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,GAAG,UAAU,
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../lib/cli.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAChB;IAAE,IAAI,EAAE,IAAI,CAAA;CAAE,GACd;IACI,IAAI,CAAC,EAAE,KAAK,CAAA;IACZ,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;CACtB,CAAA;AAEP,wBAAgB,SAAS,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,GAAG,UAAU,CAiD1D"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type Attachment } from '#c2/attachments';
|
|
2
|
+
export declare function startUserDataHttp(port: number, userDataDir: string): void;
|
|
3
|
+
export declare class MultipartMessage {
|
|
4
|
+
#private;
|
|
5
|
+
static createBoundary(): string;
|
|
6
|
+
constructor(attachments: Array<Attachment>, boundary?: string);
|
|
7
|
+
get attachments(): Array<MultipartAttachment>;
|
|
8
|
+
get boundary(): string;
|
|
9
|
+
get headers(): Record<string, string>;
|
|
10
|
+
toHTTP(): string;
|
|
11
|
+
responseBody(): string;
|
|
12
|
+
}
|
|
13
|
+
export declare class MultipartAttachment {
|
|
14
|
+
#private;
|
|
15
|
+
constructor(attachment: Attachment);
|
|
16
|
+
toHTTP(): string;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=http.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../lib/http.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,UAAU,EAAsB,MAAM,iBAAiB,CAAA;AAErE,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,QAoBlE;AAiBD,qBAAa,gBAAgB;;IACzB,MAAM,CAAC,cAAc,IAAI,MAAM;gBAQ3B,WAAW,EAAE,KAAK,CAAC,UAAU,CAAC,EAC9B,QAAQ,GAAE,MAA0C;IAMxD,IAAI,WAAW,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAE5C;IAED,IAAI,QAAQ,IAAI,MAAM,CAErB;IAED,IAAI,OAAO,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAMpC;IAED,MAAM,IAAI,MAAM;IAShB,YAAY,IAAI,MAAM;CASzB;AAED,qBAAa,mBAAmB;;gBAGhB,UAAU,EAAE,UAAU;IAIlC,MAAM,IAAI,MAAM;CASnB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eighty4/c2",
|
|
3
|
-
"version": "0.0.2-
|
|
3
|
+
"version": "0.0.2-26",
|
|
4
4
|
"author": "Adam McKee <adam.be.g84d@gmail.com>",
|
|
5
5
|
"repository": "https://github.com/eighty4/c2",
|
|
6
6
|
"homepage": "https://github.com/eighty4/c2",
|
|
@@ -22,14 +22,22 @@
|
|
|
22
22
|
"bin": {
|
|
23
23
|
"c2": "./lib_js/c2.bin.js"
|
|
24
24
|
},
|
|
25
|
-
"
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"bun": "./lib_js/c2.api.js",
|
|
28
|
+
"node": "./lib/c2.api.ts"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
26
31
|
"imports": {
|
|
27
|
-
"#c2
|
|
28
|
-
|
|
32
|
+
"#c2/*": {
|
|
33
|
+
"bun": "./lib/*.ts",
|
|
34
|
+
"node": "./lib_js/*.js"
|
|
35
|
+
}
|
|
29
36
|
},
|
|
30
37
|
"types": "./lib_types",
|
|
31
38
|
"scripts": {
|
|
32
|
-
"build": "tsc",
|
|
39
|
+
"build": "tsc && chmod +x lib_js/c2.bin.js",
|
|
40
|
+
"fmt": "prettier --write .",
|
|
33
41
|
"fmtcheck": "prettier --check .",
|
|
34
42
|
"typecheck": "tsc --noEmit"
|
|
35
43
|
},
|