Skip to content

Commit

Permalink
[KODO-13155] 添加服务端文件签名校验 (#532)
Browse files Browse the repository at this point in the history
* 添加服务端文件校验

* 补充 config test

* 优化 README

* 添加一些注释、说明、以及部分实现调整

* 添加 0 size 的文件测试

* 提升兼容性、优化 README

* 更新注释文档

* 更新版本号

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

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "qiniu-js",
"jsName": "qiniu",
"version": "3.3.3",
"version": "3.4.0",
"private": false,
"description": "Javascript SDK for Qiniu Resource (Cloud) Storage AP",
"main": "lib/index.js",
Expand Down
7 changes: 5 additions & 2 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,17 @@ export function uploadChunk(
key: string | null | undefined,
index: number,
uploadInfo: UploadInfo,
options: Partial<utils.RequestOptions>
options: Partial<utils.RequestOptions & { md5: string }>
): utils.Response<UploadChunkData> {
const bucket = utils.getPutPolicy(token).bucketName
const url = getBaseUrl(bucket, key, uploadInfo) + `/${index}`
const headers = utils.getHeadersForChunkUpload(token)
if (options.md5) headers['Content-MD5'] = options.md5

return utils.request<UploadChunkData>(url, {
...options,
method: 'PUT',
headers: utils.getHeadersForChunkUpload(token)
headers
})
}

Expand Down
2 changes: 2 additions & 0 deletions src/upload/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export interface Extra {
export interface InternalConfig {
/** 是否开启 cdn 加速 */
useCdnDomain: boolean
/** 是否开启服务端校验 */
checkByServer: boolean
/** 是否对分片进行 md5校验 */
checkByMD5: boolean
/** 强制直传 */
Expand Down
7 changes: 7 additions & 0 deletions src/upload/direct.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { CRC32 } from '../utils/crc32'

import { direct } from '../api'

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

if (this.config.checkByServer) {
const crcSign = await CRC32.file(this.file)
formData.append('crc32', crcSign.toString())
}

if (this.putExtra.customVars) {
this.logger.info('init customVars.')
const { customVars } = this.putExtra
Expand Down
1 change: 1 addition & 0 deletions src/upload/resume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ export default class Resume extends Base {

const requestOptions = {
body: chunk,
md5: this.config.checkByServer ? md5 : undefined,
onProgress,
onCreate: (xhr: XMLHttpRequest) => this.addXhr(xhr)
}
Expand Down
6 changes: 6 additions & 0 deletions src/utils/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ describe('test config ', () => {
uphost: [],
retryCount: 3,
checkByMD5: false,
checkByServer: false,
forceDirect: false,
useCdnDomain: true,
concurrentRequestLimit: 3,
Expand All @@ -23,6 +24,7 @@ describe('test config ', () => {
uphost: [],
retryCount: 3,
checkByMD5: false,
checkByServer: false,
forceDirect: false,
useCdnDomain: true,
concurrentRequestLimit: 3,
Expand All @@ -38,6 +40,7 @@ describe('test config ', () => {
uphost: regionUphostMap[region.z0].cdnUphost,
retryCount: 3,
checkByMD5: false,
checkByServer: false,
forceDirect: false,
useCdnDomain: true,
concurrentRequestLimit: 3,
Expand All @@ -52,6 +55,7 @@ describe('test config ', () => {
uphost: ['test'],
retryCount: 3,
checkByMD5: false,
checkByServer: false,
forceDirect: false,
useCdnDomain: true,
concurrentRequestLimit: 3,
Expand All @@ -67,6 +71,7 @@ describe('test config ', () => {
uphost: ['test'],
retryCount: 3,
checkByMD5: false,
checkByServer: false,
forceDirect: false,
useCdnDomain: true,
concurrentRequestLimit: 3,
Expand All @@ -82,6 +87,7 @@ describe('test config ', () => {
uphost: regionUphostMap[region.z0].srcUphost,
retryCount: 3,
checkByMD5: false,
checkByServer: false,
forceDirect: false,
useCdnDomain: false,
concurrentRequestLimit: 3,
Expand Down
1 change: 1 addition & 0 deletions src/utils/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export function normalizeUploadConfig(config?: Partial<Config>, logger?: Logger)
checkByMD5: false,
forceDirect: false,
useCdnDomain: true,
checkByServer: false,
concurrentRequestLimit: 3,
chunkSize: DEFAULT_CHUNK_SIZE,

Expand Down
25 changes: 25 additions & 0 deletions src/utils/crc32.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { CRC32 } from './crc32'
import { MB } from './helper'

function mockFile(size = 4, name = 'mock.jpg', type = 'image/jpg'): File {
if (size >= 1024) throw new Error('the size is set too large.')

const blob = new Blob(['1'.repeat(size * MB)], { type })
return new File([blob], name)
}

describe('test crc32', async () => {
test('file', async () => {
const crc32One = new CRC32()
await expect(crc32One.file(mockFile(0))).resolves.toEqual(0)

const crc32Two = new CRC32()
await expect(crc32Two.file(mockFile(0.5))).resolves.toEqual(1610895105)

const crc32Three = new CRC32()
await expect(crc32Three.file(mockFile(1))).resolves.toEqual(3172987001)

const crc32Four = new CRC32()
await expect(crc32Four.file(mockFile(2))).resolves.toEqual(847982614)
})
})
90 changes: 90 additions & 0 deletions src/utils/crc32.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/* eslint-disable no-bitwise */

import { MB } from './helper'

/**
* 以下 class 实现参考
* https://github.com/Stuk/jszip/blob/d4702a70834bd953d4c2d0bc155fad795076631a/lib/crc32.js
* 该实现主要针对大文件优化、对计算的值进行了 `>>> 0` 运算(为与服务端保持一致)
*/
export class CRC32 {
private crc = -1
private table = this.makeTable()

private makeTable() {
const table = new Array<number>()
for (let i = 0; i < 256; i++) {
let t = i
for (let j = 0; j < 8; j++) {
if (t & 1) {
// IEEE 标准
t = (t >>> 1) ^ 0xEDB88320
} else {
t >>>= 1
}
}
table[i] = t
}

return table
}

private append(data: Uint8Array) {
let crc = this.crc
for (let offset = 0; offset < data.byteLength; offset++) {
crc = (crc >>> 8) ^ this.table[(crc ^ data[offset]) & 0xFF]
}
this.crc = crc
}

private compute() {
return (this.crc ^ -1) >>> 0
}

private async readAsUint8Array(file: File | Blob): Promise<Uint8Array> {
if (typeof file.arrayBuffer === 'function') {
return new Uint8Array(await file.arrayBuffer())
}

return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => {
if (reader.result == null) {
reject()
return
}

if (typeof reader.result === 'string') {
reject()
return
}

resolve(new Uint8Array(reader.result))
}
reader.readAsArrayBuffer(file)
})
}

async file(file: File): Promise<number> {
if (file.size <= MB) {
this.append(await this.readAsUint8Array(file))
return this.compute()
}

const count = Math.ceil(file.size / MB)
for (let index = 0; index < count; index++) {
const start = index * MB
const end = index === (count - 1) ? file.size : start + MB
// eslint-disable-next-line no-await-in-loop
const chuck = await this.readAsUint8Array(file.slice(start, end))
this.append(new Uint8Array(chuck))
}

return this.compute()
}

static file(file: File): Promise<number> {
const crc = new CRC32()
return crc.file(file)
}
}
3 changes: 3 additions & 0 deletions test/demo1/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
var token = res.uptoken;
var domain = res.domain;
var config = {
checkByServer: true,
checkByMD5: true,
forceDirect: false,
useCdnDomain: true,
disableStatisticsReport: false,
retryCount: 6,
Expand Down

0 comments on commit c6fea03

Please sign in to comment.