利用Wireshark和OSS的API文件簡單實現上傳和下載

wood23發表於2016-04-27

背景及目的

由於各個開發者使用的開發語言可能在官方SDK找不到相應的語言版本,就必須自主開發SDK。
本文根據wireshark和API文件,來簡單實現上傳和下載的請求,給需要自主開發的開發者提供一個簡單的示例。

準備工作

安裝wireshark

官網地址:https://www.wireshark.org/download.html
找到合適的平臺及版本,下載並安裝。

找到OSS的API文件

官網地址:https://help.aliyun.com/document_detail/oss/api-reference/abstract.html

準備開發環境

1. 這裡使用的是python 2.7, 並且使用requests庫。

http://cn.python-requests.org/zh_CN/latest/

2. 需要開通OSS,並且擁有一個bucket,同時需要獲取AccessKeyId和AccessKeySecret

實踐

基於OSS API文件,用python實現一個簡單的上傳和下載操作

上傳

1. 先看Put Object的API文件

https://help.aliyun.com/document_detail/oss/api-reference/object/PutObject.html

請求語法
PUT /ObjectName HTTP/1.1
Content-Length:ContentLength
Content-Type: ContentType
Host: BucketName.oss-cn-hangzhou.aliyuncs.com
Date: GMT Date
Authorization: SignatureValue

2. 構建類似的HTTP請求

BucketName是ali-beijing
Endpoint是oss-cn-beijing.aliyuncs.com
ObjectName是test.txt
將如下的程式碼保持檔案後執行

import requests
bucket = "ali-beijing"
objectname = "test.txt"
endpoint = "oss-cn-beijing.aliyuncs.com"
url = "http://%s.%s/%s" % (bucket, endpoint, objectname)
headers = {}
r = requests.put(url, data="hello", headers=headers)
print r.text
print r.status_code
print r.headers

3. 執行的同時,開啟wireshark來抓包,檢視請求

執行完畢後,停止抓包,檢視請求。
如圖所示:
Snip20160426_130

停止抓包後點選圖中紅框的”Protocol”,找到傳送的HTTP請求,然後點選“Analyze”->”Follow TCP Stream”,即可看到整個HTTP請求的內容。

可以看到最終的HTTP請求如下所示

PUT /test.txt HTTP/1.1
Host: ali-beijing.oss-cn-beijing.aliyuncs.com
Content-Length: 5
User-Agent: python-requests/2.5.1 CPython/2.7.10 Darwin/15.0.0
Connection: keep-alive
Accept: */*
Accept-Encoding: gzip, deflate

hello

HTTP/1.1 403 Forbidden
Server: AliyunOSS
Date: Tue, 26 Apr 2016 10:01:20 GMT
Content-Type: application/xml
Content-Length: 279
Connection: keep-alive
x-oss-request-id: 571F3C704FF4F07A6A0080A6

<?xml version="1.0" encoding="UTF-8"?>
<Error>
  <Code>AccessDenied</Code>
  <Message>You have no right to access this object because of bucket acl.</Message>
  <RequestId>571F3C704FF4F07A6A0080A6</RequestId>
  <HostId>ali-beijing.oss-cn-beijing.aliyuncs.com</HostId>
</Error>

經過和Put Object的協議對比,我們可以看到,請求的header中沒有加入Authorization,以及Date,也沒有Content-Type。由於bucket是私有許可權,沒有Authorization的認證資訊是無法對bucket進行寫入操作。所以需要加入簽名資訊。

4. 根據API文件描述的,加入簽名的資訊

簽名相關的文件見:
https://help.aliyun.com/document_detail/oss/api-reference/access-control/signature-header.html

#coding=utf-8
import requests, datetime, hmac, httplib, hashlib
from email.utils import formatdate
from urllib import quote
from base64 import b64encode

class OssRequest():
    def __init__(self,  endpoint, AccessKeyId, AccessKeySecret, bucket):
        self.endpoint = endpoint
        self.AccessKeyId = AccessKeyId
        self.AccessKeySecret = AccessKeySecret
        self.bucket = bucket
        self.objectname = ""
        self.subresource = ""
        self.VERB = ""

    def format_oss_headers(self, headers=None):
        map = {}
        for header, value in headers.iteritems():
            header = header.lower()
            if header.startswith("x-oss-"):
                map.setdefault(header, []).append(value)
        parts = []
        for key in sorted(map):
            parts.append("%s:%s
" % (key, ",".join(map[key])))
        return "".join(parts)

    def canonical_resource(self):
        resource = "/"
        if self.bucket:
            resource += self.bucket + "/"
        if self.objectname:
            resource += "%s" % self.objectname
        if self.subresource:
            resource += "?%s" % quote(self.subresource, "/")
        return resource

    def sign(self, headers=None):
        if not headers:
            headers = {}
        AuthString = "
".join(str(item_) for item_ in items) + "
"
        CanonicalizedOSSHeaders = self.format_oss_headers(headers)
        CanonicalizedResource = self.canonical_resource()
        AuthString = "".join((AuthString, CanonicalizedOSSHeaders, CanonicalizedResource))
        Signature = `%s` % (b64encode(hmac.new(AccessKeySecret, AuthString.encode("utf-8"), hashlib.sha1).digest()))
        return Signature

    def put(self, objectname):
        self.VERB = `PUT`
        self.objectname = objectname
        url = "http://%s.%s/%s" % (self.bucket, self.endpoint, self.objectname)
        headers = {`Date` : formatdate(None, usegmt=True)}
        Signature = self.sign(headers)
        headers[`Authorization`] = `OSS %s:%s` % (self.AccessKeyId, Signature)
        r = requests.put(url, data = "hello", headers=headers)
        print r.text
        print r.status_code
        print r.headers

if __name__ == "__main__":
    AccessKeyId = "替換成自己的AccessKeyId"
    AccessKeySecret = "替換成自己的AccessKeySecret"
    bucket = "ali-beijing"
    objectname = "test.txt"
    endpoint = "oss-cn-beijing.aliyuncs.com"
    a = OssRequest(endpoint, AccessKeyId, AccessKeySecret, bucket)
    a.put(objectname)

5. 再次在執行後,通過wireshark抓包觀察

同之前的抓包和觀察方法,可以看到,上傳成功了。

PUT /test.txt HTTP/1.1
Host: ali-beijing.oss-cn-beijing.aliyuncs.com
Content-Length: 5
Accept-Encoding: gzip, deflate
Accept: */*
User-Agent: python-requests/2.5.1 CPython/2.7.10 Darwin/15.0.0
Connection: keep-alive
Date: Tue, 26 Apr 2016 13:44:42 GMT
Content-Type: plain/text
Authorization: OSS testaliyun:1aUnxjJ4V/0+pTwzd7t9An3d10c=

helloHTTP/1.1 200 OK
Server: AliyunOSS
Date: Tue, 26 Apr 2016 13:44:42 GMT
Content-Length: 0
Connection: keep-alive
x-oss-request-id: 571F70CA4FF4F07A6A022212
ETag: "5D41402ABC4B2A76B9719D911017C592"
x-oss-hash-crc64ecma: 11177612005948864433

下載

1. 檢視Get Object的API文件

https://help.aliyun.com/document_detail/oss/api-reference/object/GetObject.html

GET /ObjectName HTTP/1.1
Host: BucketName.oss-cn-hangzhou.aliyuncs.com
Date: GMT Date
Authorization: SignatureValue
Range: bytes=ByteRange(可選)

2. 在上傳成功的基礎上實現下載

由於之前上傳Object已經成功,這裡只需要新增如下程式碼

省略和上傳一樣的程式碼
在def put(self, objectname):
函式下新增

    def get(self, objectname):
        self.VERB = `GET`
        self.objectname = objectname
        url = "http://%s.%s/%s" % (self.bucket, self.endpoint, self.objectname)
        headers = {`Date` : formatdate(None, usegmt=True)}
        Signature = self.sign(headers)
        headers[`Authorization`] = `OSS %s:%s` % (self.AccessKeyId, Signature)
        r = requests.get(url, headers=headers)
        print r.text
        print r.status_code
        print r.headers

呼叫的時候在a.put(objectname)下新增a.get(objectname)

3. 抓包觀察

GET /test.txt HTTP/1.1
Host: ali-beijing.oss-cn-beijing.aliyuncs.com
Accept-Encoding: gzip, deflate
Accept: */*
User-Agent: python-requests/2.5.1 CPython/2.7.10 Darwin/15.0.0
Connection: keep-alive
Date: Tue, 26 Apr 2016 14:16:32 GMT
Authorization: OSS testaliyun:ARRfi3zGoiGdrAjmM5lJ0o4LEBA=

HTTP/1.1 200 OK
Server: AliyunOSS
Date: Tue, 26 Apr 2016 14:16:32 GMT
Content-Type: plain/text
Content-Length: 5
Connection: keep-alive
x-oss-request-id: 571F78404FF4F07A6A023023
Accept-Ranges: bytes
ETag: "5D41402ABC4B2A76B9719D911017C592"
Last-Modified: Tue, 26 Apr 2016 13:44:42 GMT
x-oss-object-type: Normal
x-oss-hash-crc64ecma: 11177612005948864433
Cache-Control: max-age=86400

hello

以上是根據API文件,簡單實現的上傳和下載操作。
程式碼都是很簡單的,沒有異常的重試,也沒有考慮大檔案的上傳和下載。
主要目的是演示如何通過wireshark和API文件來構建HTTP 請求來實現OSS的相關介面。

常見問題

1. Content-MD5計算錯誤

以訊息內容為"123456789"來說,計算這個字串的Content-MD5

正確的計算方式:
標準中定義的演算法簡單點說就是:
1. 先計算MD5加密的二進位制陣列(128位)。
2. 再對這個二進位制進行base64編碼(而不是對32位字串編碼)。 

以Python為例子:
正確計算的程式碼為:
>>> import base64,hashlib
>>> hash = hashlib.md5()
>>> hash.update("0123456789")
>>> base64.b64encode(hash.digest())
`eB5eJF1ptWaXm4bijSPyxw==`

需要注意
正確的是:hash.digest(),計算出進位制陣列(128位)
>>> hash.digest()
`xx1e^$]ixb5fx97x9bx86xe2x8d#xf2xc7`

常見錯誤是直接對計算出的32位字串編碼進行base64編碼。
例如,錯誤的是:hash.hexdigest(),計算得到可見的32位字串編碼
>>> hash.hexdigest()
`781e5e245d69b566979b86e28d23f2c7`
錯誤的MD5值進行base64編碼後的結果:
>>> base64.b64encode(hash.hexdigest())
`NzgxZTVlMjQ1ZDY5YjU2Njk3OWI4NmUyOGQyM2YyYzc=`

2. 某些頭部沒有加入到簽名的計算中

例如x-oss-開頭的header沒有加入到簽名的計算中。

3. Content-Type設定不對

上傳Objec的時候沒有設定正確的Content-Type,導致瀏覽器等無法根據Content-Type進行預覽等處理。


相關文章