@felipedsvit/s3node 0.1.2
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/LICENSE +21 -0
- package/README.md +143 -0
- package/bin/s3node.js +101 -0
- package/docs/plan.md +340 -0
- package/package.json +50 -0
- package/src/auth/chunked.js +277 -0
- package/src/auth/credentials.js +41 -0
- package/src/auth/sigv4.js +293 -0
- package/src/errors.js +65 -0
- package/src/features/cors.js +115 -0
- package/src/features/encryption.js +201 -0
- package/src/features/formdata.js +169 -0
- package/src/features/lifecycle.js +175 -0
- package/src/features/notifications.js +161 -0
- package/src/features/policy.js +0 -0
- package/src/features/postpolicy.js +150 -0
- package/src/features/tagging.js +64 -0
- package/src/handlers/bucket.js +197 -0
- package/src/handlers/config.js +129 -0
- package/src/handlers/index.js +6 -0
- package/src/handlers/multipart.js +109 -0
- package/src/handlers/object.js +211 -0
- package/src/handlers/post.js +106 -0
- package/src/handlers/service.js +10 -0
- package/src/handlers/shared.js +84 -0
- package/src/http.js +243 -0
- package/src/index.js +30 -0
- package/src/router.js +128 -0
- package/src/server.js +282 -0
- package/src/storage/blobs.js +135 -0
- package/src/storage/metadata.js +616 -0
- package/src/storage/store.js +548 -0
- package/src/util/bytes.js +35 -0
- package/src/util/crc.js +54 -0
- package/src/util/hash.js +64 -0
- package/src/xml.js +205 -0
package/src/xml.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { S3Error } from './errors.js'
|
|
2
|
+
|
|
3
|
+
export const S3_XMLNS = 'http://s3.amazonaws.com/doc/2006-03-01/'
|
|
4
|
+
|
|
5
|
+
const ESCAPES = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Element text needs only &, < and > escaped. S3 leaves quotes and apostrophes
|
|
9
|
+
* literal in element content, and matching that keeps keys byte-identical to
|
|
10
|
+
* what clients sent.
|
|
11
|
+
*/
|
|
12
|
+
export function escapeXml(value) {
|
|
13
|
+
if (value === null || value === undefined) return ''
|
|
14
|
+
return String(value).replace(/[&<>]/g, (c) => ESCAPES[c])
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Attribute values additionally need quotes escaped. */
|
|
18
|
+
export function escapeAttribute(value) {
|
|
19
|
+
if (value === null || value === undefined) return ''
|
|
20
|
+
return String(value).replace(/[&<>"']/g, (c) => ESCAPES[c])
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Build an element. `children` may be a string (text) or pre-rendered markup. */
|
|
24
|
+
export function el(name, children, attrs) {
|
|
25
|
+
const attr = attrs
|
|
26
|
+
? Object.entries(attrs)
|
|
27
|
+
.filter(([, v]) => v !== undefined && v !== null)
|
|
28
|
+
.map(([k, v]) => ` ${k}="${escapeAttribute(v)}"`)
|
|
29
|
+
.join('')
|
|
30
|
+
: ''
|
|
31
|
+
if (children === undefined || children === null || children === '') return `<${name}${attr}/>`
|
|
32
|
+
return `<${name}${attr}>${children}</${name}>`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function text(name, value) {
|
|
36
|
+
if (value === undefined || value === null) return ''
|
|
37
|
+
return `<${name}>${escapeXml(value)}</${name}>`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function document(rootName, body) {
|
|
41
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n<${rootName} xmlns="${S3_XMLNS}">${body}</${rootName}>`
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function errorDocument({ code, message, resource, requestId, hostId }) {
|
|
45
|
+
return (
|
|
46
|
+
'<?xml version="1.0" encoding="UTF-8"?>\n<Error>' +
|
|
47
|
+
text('Code', code) +
|
|
48
|
+
text('Message', message) +
|
|
49
|
+
text('Resource', resource) +
|
|
50
|
+
text('RequestId', requestId) +
|
|
51
|
+
text('HostId', hostId) +
|
|
52
|
+
'</Error>'
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* ------------------------------------------------------------------ *
|
|
57
|
+
* Parsing
|
|
58
|
+
*
|
|
59
|
+
* Deliberately minimal: no DTD, no entity declarations, no CDATA, no
|
|
60
|
+
* external references. That makes XXE and billion-laughs impossible by
|
|
61
|
+
* construction rather than by configuration (docs/plan.md section 7).
|
|
62
|
+
* ------------------------------------------------------------------ */
|
|
63
|
+
|
|
64
|
+
const NAMED_ENTITIES = { lt: '<', gt: '>', amp: '&', quot: '"', apos: "'" }
|
|
65
|
+
const NAME_RE = /[A-Za-z_:][A-Za-z0-9._:-]*/y
|
|
66
|
+
|
|
67
|
+
function decodeEntities(raw) {
|
|
68
|
+
return raw.replace(/&(#x?[0-9A-Fa-f]+|[A-Za-z]+);/g, (match, body) => {
|
|
69
|
+
if (body[0] === '#') {
|
|
70
|
+
const code = body[1] === 'x' || body[1] === 'X'
|
|
71
|
+
? Number.parseInt(body.slice(2), 16)
|
|
72
|
+
: Number.parseInt(body.slice(1), 10)
|
|
73
|
+
if (!Number.isFinite(code) || code < 0 || code > 0x10ffff) {
|
|
74
|
+
throw new S3Error('MalformedXML', 'Invalid character reference')
|
|
75
|
+
}
|
|
76
|
+
return String.fromCodePoint(code)
|
|
77
|
+
}
|
|
78
|
+
const named = NAMED_ENTITIES[body]
|
|
79
|
+
if (named === undefined) throw new S3Error('MalformedXML', `Unsupported entity &${body};`)
|
|
80
|
+
return named
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function parseXml(input, { maxSize = 1024 * 1024, maxDepth = 32, maxNodes = 100_000 } = {}) {
|
|
85
|
+
const src = Buffer.isBuffer(input) ? input.toString('utf8') : String(input)
|
|
86
|
+
if (src.length > maxSize) throw new S3Error('MalformedXML', 'XML body too large')
|
|
87
|
+
if (/<!\s*(DOCTYPE|ENTITY|ATTLIST|ELEMENT|NOTATION)/i.test(src)) {
|
|
88
|
+
throw new S3Error('MalformedXML', 'Document type declarations are not accepted')
|
|
89
|
+
}
|
|
90
|
+
if (src.includes('<![CDATA[')) throw new S3Error('MalformedXML', 'CDATA sections are not accepted')
|
|
91
|
+
|
|
92
|
+
let pos = 0
|
|
93
|
+
let nodes = 0
|
|
94
|
+
|
|
95
|
+
const fail = (msg) => { throw new S3Error('MalformedXML', msg) }
|
|
96
|
+
|
|
97
|
+
const skipTrivia = () => {
|
|
98
|
+
for (;;) {
|
|
99
|
+
while (pos < src.length && /\s/.test(src[pos])) pos++
|
|
100
|
+
if (src.startsWith('<?', pos)) {
|
|
101
|
+
const end = src.indexOf('?>', pos)
|
|
102
|
+
if (end === -1) fail('Unterminated processing instruction')
|
|
103
|
+
pos = end + 2
|
|
104
|
+
continue
|
|
105
|
+
}
|
|
106
|
+
if (src.startsWith('<!--', pos)) {
|
|
107
|
+
const end = src.indexOf('-->', pos)
|
|
108
|
+
if (end === -1) fail('Unterminated comment')
|
|
109
|
+
pos = end + 3
|
|
110
|
+
continue
|
|
111
|
+
}
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const readName = () => {
|
|
117
|
+
NAME_RE.lastIndex = pos
|
|
118
|
+
const m = NAME_RE.exec(src)
|
|
119
|
+
if (!m) fail('Expected element name')
|
|
120
|
+
pos = NAME_RE.lastIndex
|
|
121
|
+
return m[0]
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const readAttributes = () => {
|
|
125
|
+
const attrs = {}
|
|
126
|
+
for (;;) {
|
|
127
|
+
while (pos < src.length && /\s/.test(src[pos])) pos++
|
|
128
|
+
if (src[pos] === '>' || src.startsWith('/>', pos)) return attrs
|
|
129
|
+
const name = readName()
|
|
130
|
+
while (pos < src.length && /\s/.test(src[pos])) pos++
|
|
131
|
+
if (src[pos] !== '=') fail('Expected "=" in attribute')
|
|
132
|
+
pos++
|
|
133
|
+
while (pos < src.length && /\s/.test(src[pos])) pos++
|
|
134
|
+
const quote = src[pos]
|
|
135
|
+
if (quote !== '"' && quote !== "'") fail('Unquoted attribute value')
|
|
136
|
+
const end = src.indexOf(quote, pos + 1)
|
|
137
|
+
if (end === -1) fail('Unterminated attribute value')
|
|
138
|
+
attrs[name] = decodeEntities(src.slice(pos + 1, end))
|
|
139
|
+
pos = end + 1
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const parseElement = (depth) => {
|
|
144
|
+
if (depth > maxDepth) fail('XML nesting too deep')
|
|
145
|
+
if (++nodes > maxNodes) fail('XML has too many nodes')
|
|
146
|
+
if (src[pos] !== '<') fail('Expected "<"')
|
|
147
|
+
pos++
|
|
148
|
+
const name = readName()
|
|
149
|
+
const attrs = readAttributes()
|
|
150
|
+
if (src.startsWith('/>', pos)) {
|
|
151
|
+
pos += 2
|
|
152
|
+
return { name, attrs, text: '', children: [] }
|
|
153
|
+
}
|
|
154
|
+
if (src[pos] !== '>') fail('Malformed start tag')
|
|
155
|
+
pos++
|
|
156
|
+
|
|
157
|
+
const children = []
|
|
158
|
+
let textParts = ''
|
|
159
|
+
for (;;) {
|
|
160
|
+
if (pos >= src.length) fail('Unexpected end of document')
|
|
161
|
+
if (src.startsWith('</', pos)) {
|
|
162
|
+
pos += 2
|
|
163
|
+
const closing = readName()
|
|
164
|
+
if (closing !== name) fail(`Mismatched closing tag </${closing}>`)
|
|
165
|
+
while (pos < src.length && /\s/.test(src[pos])) pos++
|
|
166
|
+
if (src[pos] !== '>') fail('Malformed end tag')
|
|
167
|
+
pos++
|
|
168
|
+
return { name, attrs, text: decodeEntities(textParts).trim(), children }
|
|
169
|
+
}
|
|
170
|
+
if (src.startsWith('<!--', pos)) {
|
|
171
|
+
const end = src.indexOf('-->', pos)
|
|
172
|
+
if (end === -1) fail('Unterminated comment')
|
|
173
|
+
pos = end + 3
|
|
174
|
+
continue
|
|
175
|
+
}
|
|
176
|
+
if (src[pos] === '<') {
|
|
177
|
+
children.push(parseElement(depth + 1))
|
|
178
|
+
continue
|
|
179
|
+
}
|
|
180
|
+
const next = src.indexOf('<', pos)
|
|
181
|
+
if (next === -1) fail('Unexpected end of document')
|
|
182
|
+
textParts += src.slice(pos, next)
|
|
183
|
+
pos = next
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
skipTrivia()
|
|
188
|
+
if (pos >= src.length) fail('Empty XML document')
|
|
189
|
+
const root = parseElement(0)
|
|
190
|
+
skipTrivia()
|
|
191
|
+
if (pos !== src.length) fail('Trailing content after root element')
|
|
192
|
+
return root
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function childrenNamed(node, name) {
|
|
196
|
+
return node ? node.children.filter((c) => c.name === name) : []
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function childNamed(node, name) {
|
|
200
|
+
return node ? node.children.find((c) => c.name === name) : undefined
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function childText(node, name) {
|
|
204
|
+
return childNamed(node, name)?.text
|
|
205
|
+
}
|