Skip to content

Commit c6fea03

Browse files
authored
[KODO-13155] 添加服务端文件签名校验 (#532)
* 添加服务端文件校验 * 补充 config test * 优化 README * 添加一些注释、说明、以及部分实现调整 * 添加 0 size 的文件测试 * 提升兼容性、优化 README * 更新注释文档 * 更新版本号 * update
1 parent b65d484 commit c6fea03

File tree

12 files changed

+145
-6
lines changed

12 files changed

+145
-6
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,8 @@ qiniu.compressImage(file, options).then(data => {
215215
* config.region: 选择上传域名区域;当为 `null``undefined` 时,自动分析上传域名区域,当指定了 `uphost` 时,此设置项无效。
216216
* config.retryCount: 上传自动重试次数(整体重试次数,而不是某个分片的重试次数);默认 3 次(即上传失败后最多重试两次)。
217217
* config.concurrentRequestLimit: 分片上传的并发请求量,`number`,默认为3;因为浏览器本身也会限制最大并发量,所以最大并发量与浏览器有关。
218-
* config.checkByMD5: 是否开启 MD5 校验,为布尔值;在断点续传时,开启 MD5 校验会将已上传的分片与当前分片进行 MD5 值比对,若不一致,则重传该分片,避免使用错误的分片。读取分片内容并计算 MD5 需要花费一定的时间,因此会稍微增加断点续传时的耗时,默认为 false,不开启。
218+
* config.checkByServer: 是否开启服务端文件签名校验,为布尔值;开启后在文件上传时会计算本地的文件签名,服务端会根据本地的签名与接收到的数据的签名进行比对,如果不相同、则说明文件可能存在问题,此时会返回错误(`code`: 406),默认为 `false`,不开启。
219+
* config.checkByMD5: 是否开启 `MD5` 校验,为布尔值;在断点续传时,开启 `MD5` 校验会将已上传的分片与当前分片进行 `MD5` 值比对,若不一致,则重传该分片,避免使用错误的分片。读取分片内容并计算 `MD5` 需要花费一定的时间,因此会稍微增加断点续传时的耗时,默认为 `false`,不开启。
219220
* config.forceDirect: 是否上传全部采用直传方式,为布尔值;为 `true` 时则上传方式全部为直传 form 方式,禁用断点续传,默认 `false`
220221
* config.chunkSize: `number`,分片上传时每片的大小,必须为正整数,单位为 `MB`,且最大不能超过 1024,默认值 4。因为 chunk 数最大 10000,所以如果文件以你所设的 `chunkSize` 进行分片并且 chunk 数超过 10000,我们会把你所设的 `chunkSize` 扩大二倍,如果仍不符合则继续扩大,直到符合条件。
221222
* config.debugLogLevel: `INFO` | `WARN` | `ERROR` | `OFF`,允许程序在控制台输出日志,默认为 `OFF`,不输出任何日志,本功能仅仅用于本地调试,不建议在线上环境开启。

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "qiniu-js",
33
"jsName": "qiniu",
4-
"version": "3.3.3",
4+
"version": "3.4.0",
55
"private": false,
66
"description": "Javascript SDK for Qiniu Resource (Cloud) Storage AP",
77
"main": "lib/index.js",

src/api/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,14 +76,17 @@ export function uploadChunk(
7676
key: string | null | undefined,
7777
index: number,
7878
uploadInfo: UploadInfo,
79-
options: Partial<utils.RequestOptions>
79+
options: Partial<utils.RequestOptions & { md5: string }>
8080
): utils.Response<UploadChunkData> {
8181
const bucket = utils.getPutPolicy(token).bucketName
8282
const url = getBaseUrl(bucket, key, uploadInfo) + `/${index}`
83+
const headers = utils.getHeadersForChunkUpload(token)
84+
if (options.md5) headers['Content-MD5'] = options.md5
85+
8386
return utils.request<UploadChunkData>(url, {
8487
...options,
8588
method: 'PUT',
86-
headers: utils.getHeadersForChunkUpload(token)
89+
headers
8790
})
8891
}
8992

src/upload/base.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ export interface Extra {
2626
export interface InternalConfig {
2727
/** 是否开启 cdn 加速 */
2828
useCdnDomain: boolean
29+
/** 是否开启服务端校验 */
30+
checkByServer: boolean
2931
/** 是否对分片进行 md5校验 */
3032
checkByMD5: boolean
3133
/** 强制直传 */

src/upload/direct.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { CRC32 } from '../utils/crc32'
2+
13
import { direct } from '../api'
24

35
import Base from './base'
@@ -15,6 +17,11 @@ export default class Direct extends Base {
1517
}
1618
formData.append('fname', this.putExtra.fname)
1719

20+
if (this.config.checkByServer) {
21+
const crcSign = await CRC32.file(this.file)
22+
formData.append('crc32', crcSign.toString())
23+
}
24+
1825
if (this.putExtra.customVars) {
1926
this.logger.info('init customVars.')
2027
const { customVars } = this.putExtra

src/upload/resume.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ export default class Resume extends Base {
153153

154154
const requestOptions = {
155155
body: chunk,
156+
md5: this.config.checkByServer ? md5 : undefined,
156157
onProgress,
157158
onCreate: (xhr: XMLHttpRequest) => this.addXhr(xhr)
158159
}

src/utils/config.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ describe('test config ', () => {
99
uphost: [],
1010
retryCount: 3,
1111
checkByMD5: false,
12+
checkByServer: false,
1213
forceDirect: false,
1314
useCdnDomain: true,
1415
concurrentRequestLimit: 3,
@@ -23,6 +24,7 @@ describe('test config ', () => {
2324
uphost: [],
2425
retryCount: 3,
2526
checkByMD5: false,
27+
checkByServer: false,
2628
forceDirect: false,
2729
useCdnDomain: true,
2830
concurrentRequestLimit: 3,
@@ -38,6 +40,7 @@ describe('test config ', () => {
3840
uphost: regionUphostMap[region.z0].cdnUphost,
3941
retryCount: 3,
4042
checkByMD5: false,
43+
checkByServer: false,
4144
forceDirect: false,
4245
useCdnDomain: true,
4346
concurrentRequestLimit: 3,
@@ -52,6 +55,7 @@ describe('test config ', () => {
5255
uphost: ['test'],
5356
retryCount: 3,
5457
checkByMD5: false,
58+
checkByServer: false,
5559
forceDirect: false,
5660
useCdnDomain: true,
5761
concurrentRequestLimit: 3,
@@ -67,6 +71,7 @@ describe('test config ', () => {
6771
uphost: ['test'],
6872
retryCount: 3,
6973
checkByMD5: false,
74+
checkByServer: false,
7075
forceDirect: false,
7176
useCdnDomain: true,
7277
concurrentRequestLimit: 3,
@@ -82,6 +87,7 @@ describe('test config ', () => {
8287
uphost: regionUphostMap[region.z0].srcUphost,
8388
retryCount: 3,
8489
checkByMD5: false,
90+
checkByServer: false,
8591
forceDirect: false,
8692
useCdnDomain: false,
8793
concurrentRequestLimit: 3,

src/utils/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export function normalizeUploadConfig(config?: Partial<Config>, logger?: Logger)
1212
checkByMD5: false,
1313
forceDirect: false,
1414
useCdnDomain: true,
15+
checkByServer: false,
1516
concurrentRequestLimit: 3,
1617
chunkSize: DEFAULT_CHUNK_SIZE,
1718

src/utils/crc32.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { CRC32 } from './crc32'
2+
import { MB } from './helper'
3+
4+
function mockFile(size = 4, name = 'mock.jpg', type = 'image/jpg'): File {
5+
if (size >= 1024) throw new Error('the size is set too large.')
6+
7+
const blob = new Blob(['1'.repeat(size * MB)], { type })
8+
return new File([blob], name)
9+
}
10+
11+
describe('test crc32', async () => {
12+
test('file', async () => {
13+
const crc32One = new CRC32()
14+
await expect(crc32One.file(mockFile(0))).resolves.toEqual(0)
15+
16+
const crc32Two = new CRC32()
17+
await expect(crc32Two.file(mockFile(0.5))).resolves.toEqual(1610895105)
18+
19+
const crc32Three = new CRC32()
20+
await expect(crc32Three.file(mockFile(1))).resolves.toEqual(3172987001)
21+
22+
const crc32Four = new CRC32()
23+
await expect(crc32Four.file(mockFile(2))).resolves.toEqual(847982614)
24+
})
25+
})

src/utils/crc32.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/* eslint-disable no-bitwise */
2+
3+
import { MB } from './helper'
4+
5+
/**
6+
* 以下 class 实现参考
7+
* https://github.com/Stuk/jszip/blob/d4702a70834bd953d4c2d0bc155fad795076631a/lib/crc32.js
8+
* 该实现主要针对大文件优化、对计算的值进行了 `>>> 0` 运算(为与服务端保持一致)
9+
*/
10+
export class CRC32 {
11+
private crc = -1
12+
private table = this.makeTable()
13+
14+
private makeTable() {
15+
const table = new Array<number>()
16+
for (let i = 0; i < 256; i++) {
17+
let t = i
18+
for (let j = 0; j < 8; j++) {
19+
if (t & 1) {
20+
// IEEE 标准
21+
t = (t >>> 1) ^ 0xEDB88320
22+
} else {
23+
t >>>= 1
24+
}
25+
}
26+
table[i] = t
27+
}
28+
29+
return table
30+
}
31+
32+
private append(data: Uint8Array) {
33+
let crc = this.crc
34+
for (let offset = 0; offset < data.byteLength; offset++) {
35+
crc = (crc >>> 8) ^ this.table[(crc ^ data[offset]) & 0xFF]
36+
}
37+
this.crc = crc
38+
}
39+
40+
private compute() {
41+
return (this.crc ^ -1) >>> 0
42+
}
43+
44+
private async readAsUint8Array(file: File | Blob): Promise<Uint8Array> {
45+
if (typeof file.arrayBuffer === 'function') {
46+
return new Uint8Array(await file.arrayBuffer())
47+
}
48+
49+
return new Promise((resolve, reject) => {
50+
const reader = new FileReader()
51+
reader.onload = () => {
52+
if (reader.result == null) {
53+
reject()
54+
return
55+
}
56+
57+
if (typeof reader.result === 'string') {
58+
reject()
59+
return
60+
}
61+
62+
resolve(new Uint8Array(reader.result))
63+
}
64+
reader.readAsArrayBuffer(file)
65+
})
66+
}
67+
68+
async file(file: File): Promise<number> {
69+
if (file.size <= MB) {
70+
this.append(await this.readAsUint8Array(file))
71+
return this.compute()
72+
}
73+
74+
const count = Math.ceil(file.size / MB)
75+
for (let index = 0; index < count; index++) {
76+
const start = index * MB
77+
const end = index === (count - 1) ? file.size : start + MB
78+
// eslint-disable-next-line no-await-in-loop
79+
const chuck = await this.readAsUint8Array(file.slice(start, end))
80+
this.append(new Uint8Array(chuck))
81+
}
82+
83+
return this.compute()
84+
}
85+
86+
static file(file: File): Promise<number> {
87+
const crc = new CRC32()
88+
return crc.file(file)
89+
}
90+
}

test/demo1/main.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
var token = res.uptoken;
33
var domain = res.domain;
44
var config = {
5+
checkByServer: true,
6+
checkByMD5: true,
7+
forceDirect: false,
58
useCdnDomain: true,
69
disableStatisticsReport: false,
710
retryCount: 6,

0 commit comments

Comments
 (0)