正在显示
5 个修改的文件
包含
503 行增加
和
20 行删除
1 | package aliyun | 1 | package aliyun |
2 | 2 | ||
3 | -import ( | ||
4 | - "github.com/astaxie/beego" | ||
5 | -) | ||
6 | - | ||
7 | /* | 3 | /* |
8 | AK AccessKey 完全访问权限 | 4 | AK AccessKey 完全访问权限 |
9 | STS (Security Token Service) 临时访问控制 | 5 | STS (Security Token Service) 临时访问控制 |
10 | */ | 6 | */ |
11 | - | ||
12 | -var ( | ||
13 | - Endpoint string = beego.AppConfig.String("end_point") | ||
14 | - BucketName string = beego.AppConfig.String("bucket_name") | ||
15 | - //BucketNameCdn string = beego.AppConfig.String("bucket_name_cdn") | ||
16 | - //EndpointCdn string = beego.AppConfig.String("end_point_cdn") | ||
17 | -) | ||
18 | - | ||
19 | -//type OssSts struct{ | ||
20 | -// | ||
21 | -//} | ||
22 | - | ||
23 | -//func NewSTS(){ | ||
24 | -// client,_ :=oos.NewClientWithAccessKey(RegionID,AccessKeyID,AccessKeySecret) | ||
25 | -// client.DoAction() | ||
26 | -//} | 7 | +type ossConfig struct { |
8 | + AccessID string | ||
9 | + AccessKey string | ||
10 | + RoleAcs string | ||
11 | + EndPoint string | ||
12 | + BuckName string | ||
13 | + CallbackUrl string | ||
14 | +} |
services/oss/config.go
0 → 100644
1 | +package oss |
services/oss/policy.go
0 → 100644
1 | +package oss |
services/oss/sts.go
0 → 100644
1 | +package oss | ||
2 | + | ||
3 | +import ( | ||
4 | + "crypto/hmac" | ||
5 | + "crypto/sha1" | ||
6 | + "crypto/tls" | ||
7 | + "encoding/base64" | ||
8 | + "io/ioutil" | ||
9 | + "net/http" | ||
10 | + "net/url" | ||
11 | + "time" | ||
12 | +) | ||
13 | + | ||
14 | +type AliyunStsClient struct { | ||
15 | + ChildAccountKeyId string | ||
16 | + ChildAccountSecret string | ||
17 | + RoleAcs string | ||
18 | +} | ||
19 | + | ||
20 | +func NewStsClient(key, secret, roleAcs string) *AliyunStsClient { | ||
21 | + return &AliyunStsClient{ | ||
22 | + ChildAccountKeyId: key, | ||
23 | + ChildAccountSecret: secret, | ||
24 | + RoleAcs: roleAcs, | ||
25 | + } | ||
26 | +} | ||
27 | + | ||
28 | +func (cli *AliyunStsClient) GenerateSignatureUrl(sessionName, durationSeconds string) (string, error) { | ||
29 | + assumeUrl := "SignatureVersion=1.0" | ||
30 | + assumeUrl += "&Format=JSON" | ||
31 | + assumeUrl += "&Timestamp=" + url.QueryEscape(time.Now().UTC().Format("2006-01-02T15:04:05Z")) | ||
32 | + assumeUrl += "&RoleArn=" + url.QueryEscape(cli.RoleAcs) | ||
33 | + assumeUrl += "&RoleSessionName=" + sessionName | ||
34 | + assumeUrl += "&AccessKeyId=" + cli.ChildAccountKeyId | ||
35 | + assumeUrl += "&SignatureMethod=HMAC-SHA1" | ||
36 | + assumeUrl += "&Version=2015-04-01" | ||
37 | + assumeUrl += "&Action=AssumeRole" | ||
38 | + assumeUrl += "&SignatureNonce=" + "TODO" | ||
39 | + assumeUrl += "&DurationSeconds=" + durationSeconds | ||
40 | + | ||
41 | + // 解析成V type | ||
42 | + signToString, err := url.ParseQuery(assumeUrl) | ||
43 | + if err != nil { | ||
44 | + return "", err | ||
45 | + } | ||
46 | + | ||
47 | + // URL顺序化 | ||
48 | + result := signToString.Encode() | ||
49 | + | ||
50 | + // 拼接 | ||
51 | + StringToSign := "GET" + "&" + "%2F" + "&" + url.QueryEscape(result) | ||
52 | + | ||
53 | + // HMAC | ||
54 | + hashSign := hmac.New(sha1.New, []byte(cli.ChildAccountSecret+"&")) | ||
55 | + hashSign.Write([]byte(StringToSign)) | ||
56 | + | ||
57 | + // 生成signature | ||
58 | + signature := base64.StdEncoding.EncodeToString(hashSign.Sum(nil)) | ||
59 | + | ||
60 | + // Url 添加signature | ||
61 | + assumeUrl = "https://sts.aliyuncs.com/?" + assumeUrl + "&Signature=" + url.QueryEscape(signature) | ||
62 | + | ||
63 | + return assumeUrl, nil | ||
64 | +} | ||
65 | + | ||
66 | +// 请求构造好的URL,获得授权信息 | ||
67 | +// 安全认证 HTTPS | ||
68 | +func (cli *AliyunStsClient) GetStsResponse(url string) ([]byte, error) { | ||
69 | + tr := &http.Transport{ | ||
70 | + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, | ||
71 | + } | ||
72 | + client := &http.Client{Transport: tr} | ||
73 | + | ||
74 | + resp, err := client.Get(url) | ||
75 | + if err != nil { | ||
76 | + return nil, err | ||
77 | + } | ||
78 | + defer resp.Body.Close() | ||
79 | + | ||
80 | + body, err := ioutil.ReadAll(resp.Body) | ||
81 | + | ||
82 | + return body, err | ||
83 | +} |
services/oss/valid.go
0 → 100644
1 | +package oss | ||
2 | + | ||
3 | +import ( | ||
4 | + "crypto" | ||
5 | + "crypto/md5" | ||
6 | + "crypto/rsa" | ||
7 | + "crypto/x509" | ||
8 | + "encoding/base64" | ||
9 | + "encoding/pem" | ||
10 | + "errors" | ||
11 | + "fmt" | ||
12 | + "io/ioutil" | ||
13 | + "net/http" | ||
14 | + "strconv" | ||
15 | + | ||
16 | + "github.com/astaxie/beego" | ||
17 | +) | ||
18 | + | ||
19 | +func ValidSignure(r *http.Request, requestBody []byte) bool { | ||
20 | + if r.Method != "POST" { | ||
21 | + beego.Error("request.Method!=post") | ||
22 | + return false | ||
23 | + } | ||
24 | + // Get PublicKey bytes | ||
25 | + bytePublicKey, err := getPublicKey(r) | ||
26 | + if err != nil { | ||
27 | + beego.Error(err) | ||
28 | + return false | ||
29 | + } | ||
30 | + // Get Authorization bytes : decode from Base64String | ||
31 | + byteAuthorization, err := getAuthorization(r) | ||
32 | + if err != nil { | ||
33 | + beego.Error(err) | ||
34 | + return false | ||
35 | + } | ||
36 | + // Get MD5 bytes from Newly Constructed Authrization String. | ||
37 | + byteMD5, err := getMD5FromNewAuthString(r, requestBody) | ||
38 | + if err != nil { | ||
39 | + beego.Error(err) | ||
40 | + return false | ||
41 | + } | ||
42 | + | ||
43 | + // VerifySignature and response to client | ||
44 | + if verifySignature(bytePublicKey, byteMD5, byteAuthorization) { | ||
45 | + return true | ||
46 | + } else { | ||
47 | + beego.Error("verifySignature return false") | ||
48 | + return false | ||
49 | + } | ||
50 | + | ||
51 | + return false | ||
52 | +} | ||
53 | + | ||
54 | +func handlerRequest(w http.ResponseWriter, r *http.Request) { | ||
55 | + if r.Method == "POST" { | ||
56 | + fmt.Println("\nHandle Post Request...") | ||
57 | + | ||
58 | + // Get PublicKey bytes | ||
59 | + bytePublicKey, err := getPublicKey(r) | ||
60 | + if err != nil { | ||
61 | + responseFailed(w) | ||
62 | + return | ||
63 | + } | ||
64 | + beego.Debug("bytePublicKey:", string(bytePublicKey)) | ||
65 | + // Get Authorization bytes : decode from Base64String | ||
66 | + byteAuthorization, err := getAuthorization(r) | ||
67 | + if err != nil { | ||
68 | + responseFailed(w) | ||
69 | + return | ||
70 | + } | ||
71 | + beego.Debug("byteAuthorization:", string(byteAuthorization)) | ||
72 | + // Get MD5 bytes from Newly Constructed Authrization String. | ||
73 | + byteMD5, err := getMD5FromNewAuthString(r, []byte{}) | ||
74 | + if err != nil { | ||
75 | + responseFailed(w) | ||
76 | + return | ||
77 | + } | ||
78 | + | ||
79 | + // VerifySignature and response to client | ||
80 | + if verifySignature(bytePublicKey, byteMD5, byteAuthorization) { | ||
81 | + | ||
82 | + // Do something you want accoding to callback_body ... | ||
83 | + | ||
84 | + // response OK : 200 | ||
85 | + responseSuccess(w) | ||
86 | + } else { | ||
87 | + // response FAILED : 400 | ||
88 | + responseFailed(w) | ||
89 | + } | ||
90 | + } | ||
91 | +} | ||
92 | + | ||
93 | +// getPublicKey : Get PublicKey bytes from Request.URL | ||
94 | +func getPublicKey(r *http.Request) ([]byte, error) { | ||
95 | + var bytePublicKey []byte | ||
96 | + | ||
97 | + // get PublicKey URL | ||
98 | + publicKeyURLBase64 := r.Header.Get("x-oss-pub-key-url") | ||
99 | + if publicKeyURLBase64 == "" { | ||
100 | + fmt.Println("GetPublicKey from Request header failed : No x-oss-pub-key-url field. ") | ||
101 | + return bytePublicKey, errors.New("no x-oss-pub-key-url field in Request header ") | ||
102 | + } | ||
103 | + publicKeyURL, _ := base64.StdEncoding.DecodeString(publicKeyURLBase64) | ||
104 | + // get PublicKey Content from URL | ||
105 | + responsePublicKeyURL, err := http.Get(string(publicKeyURL)) | ||
106 | + if err != nil { | ||
107 | + fmt.Printf("Get PublicKey Content from URL failed : %s \n", err.Error()) | ||
108 | + return bytePublicKey, err | ||
109 | + } | ||
110 | + defer responsePublicKeyURL.Body.Close() | ||
111 | + bytePublicKey, err = ioutil.ReadAll(responsePublicKeyURL.Body) | ||
112 | + if err != nil { | ||
113 | + fmt.Printf("Read PublicKey Content from URL failed : %s \n", err.Error()) | ||
114 | + return bytePublicKey, err | ||
115 | + } | ||
116 | + | ||
117 | + return bytePublicKey, nil | ||
118 | +} | ||
119 | + | ||
120 | +// getAuthorization : decode from Base64String | ||
121 | +func getAuthorization(r *http.Request) ([]byte, error) { | ||
122 | + var byteAuthorization []byte | ||
123 | + | ||
124 | + strAuthorizationBase64 := r.Header.Get("authorization") | ||
125 | + if strAuthorizationBase64 == "" { | ||
126 | + fmt.Println("Failed to get authorization field from request header. ") | ||
127 | + return byteAuthorization, errors.New("no authorization field in Request header") | ||
128 | + } | ||
129 | + byteAuthorization, _ = base64.StdEncoding.DecodeString(strAuthorizationBase64) | ||
130 | + | ||
131 | + return byteAuthorization, nil | ||
132 | +} | ||
133 | + | ||
134 | +// getMD5FromNewAuthString : Get MD5 bytes from Newly Constructed Authrization String. | ||
135 | +func getMD5FromNewAuthString(r *http.Request, requestBody []byte) ([]byte, error) { | ||
136 | + var byteMD5 []byte | ||
137 | + // Construct the New Auth String from URI+Query+Body | ||
138 | + _, err := ioutil.ReadAll(r.Body) | ||
139 | + r.Body.Close() | ||
140 | + if err != nil { | ||
141 | + fmt.Printf("Read Request Body failed : %s \n", err.Error()) | ||
142 | + return byteMD5, err | ||
143 | + } | ||
144 | + //strCallbackBody := string(bodyContent) | ||
145 | + strCallbackBody := string(requestBody) | ||
146 | + strURLPathDecode, errUnescape := unescapePath(r.URL.Path, encodePathSegment) | ||
147 | + if errUnescape != nil { | ||
148 | + fmt.Printf("url.PathUnescape failed : URL.Path=%s, error=%s \n", r.URL.Path, err.Error()) | ||
149 | + return byteMD5, errUnescape | ||
150 | + } | ||
151 | + | ||
152 | + // Generate New Auth String prepare for MD5 | ||
153 | + strAuth := "" | ||
154 | + if r.URL.RawQuery == "" { | ||
155 | + strAuth = fmt.Sprintf("%s\n%s", strURLPathDecode, strCallbackBody) | ||
156 | + } else { | ||
157 | + strAuth = fmt.Sprintf("%s?%s\n%s", strURLPathDecode, r.URL.RawQuery, strCallbackBody) | ||
158 | + } | ||
159 | + // Generate MD5 from the New Auth String | ||
160 | + md5Ctx := md5.New() | ||
161 | + md5Ctx.Write([]byte(strAuth)) | ||
162 | + byteMD5 = md5Ctx.Sum(nil) | ||
163 | + | ||
164 | + return byteMD5, nil | ||
165 | +} | ||
166 | + | ||
167 | +// verifySignature | ||
168 | +func verifySignature(bytePublicKey []byte, byteMd5 []byte, authorization []byte) bool { | ||
169 | + pubBlock, _ := pem.Decode(bytePublicKey) | ||
170 | + if pubBlock == nil { | ||
171 | + beego.Error("Failed to parse PEM block containing the public key") | ||
172 | + return false | ||
173 | + } | ||
174 | + pubInterface, err := x509.ParsePKIXPublicKey(pubBlock.Bytes) | ||
175 | + if (pubInterface == nil) || (err != nil) { | ||
176 | + beego.Error(fmt.Sprintf("x509.ParsePKIXPublicKey(publicKey) failed : %s \n", err.Error())) | ||
177 | + return false | ||
178 | + } | ||
179 | + pub := pubInterface.(*rsa.PublicKey) | ||
180 | + | ||
181 | + errorVerifyPKCS1v15 := rsa.VerifyPKCS1v15(pub, crypto.MD5, byteMd5, authorization) | ||
182 | + if errorVerifyPKCS1v15 != nil { | ||
183 | + beego.Error(fmt.Sprintf("Signature Verification is Failed : %s \n", errorVerifyPKCS1v15.Error())) | ||
184 | + return false | ||
185 | + } | ||
186 | + | ||
187 | + fmt.Printf("Signature Verification is Successful. \n") | ||
188 | + return true | ||
189 | +} | ||
190 | + | ||
191 | +// responseSuccess : Response 200 to client | ||
192 | +func responseSuccess(w http.ResponseWriter) { | ||
193 | + strResponseBody := "{\"Status\":\"OK\"}" | ||
194 | + w.Header().Set("Content-Type", "application/json") | ||
195 | + w.Header().Set("Content-Length", strconv.Itoa(len(strResponseBody))) | ||
196 | + w.WriteHeader(http.StatusOK) | ||
197 | + w.Write([]byte(strResponseBody)) | ||
198 | + fmt.Printf("\nPost Response : 200 OK . \n") | ||
199 | +} | ||
200 | + | ||
201 | +// responseFailed : Response 400 to client | ||
202 | +func responseFailed(w http.ResponseWriter) { | ||
203 | + w.WriteHeader(http.StatusBadRequest) | ||
204 | + fmt.Printf("\nPost Response : 400 BAD . \n") | ||
205 | +} | ||
206 | + | ||
207 | +func printByteArray(byteArrary []byte, arrName string) { | ||
208 | + fmt.Printf("printByteArray : ArrayName=%s, ArrayLength=%d \n", arrName, len(byteArrary)) | ||
209 | + for i := 0; i < len(byteArrary); i++ { | ||
210 | + fmt.Printf("%02x", byteArrary[i]) | ||
211 | + } | ||
212 | + fmt.Printf("printByteArray : End . \n") | ||
213 | +} | ||
214 | + | ||
215 | +// EscapeError Escape Error | ||
216 | +type EscapeError string | ||
217 | + | ||
218 | +func (e EscapeError) Error() string { | ||
219 | + return "invalid URL escape " + strconv.Quote(string(e)) | ||
220 | +} | ||
221 | + | ||
222 | +// InvalidHostError Invalid Host Error | ||
223 | +type InvalidHostError string | ||
224 | + | ||
225 | +func (e InvalidHostError) Error() string { | ||
226 | + return "invalid character " + strconv.Quote(string(e)) + " in host name" | ||
227 | +} | ||
228 | + | ||
229 | +type encoding int | ||
230 | + | ||
231 | +const ( | ||
232 | + encodePath encoding = 1 + iota | ||
233 | + encodePathSegment | ||
234 | + encodeHost | ||
235 | + encodeZone | ||
236 | + encodeUserPassword | ||
237 | + encodeQueryComponent | ||
238 | + encodeFragment | ||
239 | +) | ||
240 | + | ||
241 | +// unescapePath : unescapes a string; the mode specifies, which section of the URL string is being unescaped. | ||
242 | +func unescapePath(s string, mode encoding) (string, error) { | ||
243 | + | ||
244 | + // Count %, check that they're well-formed. | ||
245 | + mode = encodePathSegment | ||
246 | + n := 0 | ||
247 | + hasPlus := false | ||
248 | + for i := 0; i < len(s); { | ||
249 | + switch s[i] { | ||
250 | + case '%': | ||
251 | + n++ | ||
252 | + if i+2 >= len(s) || !ishex(s[i+1]) || !ishex(s[i+2]) { | ||
253 | + s = s[i:] | ||
254 | + if len(s) > 3 { | ||
255 | + s = s[:3] | ||
256 | + } | ||
257 | + return "", EscapeError(s) | ||
258 | + } | ||
259 | + // Per https://tools.ietf.org/html/rfc3986#page-21 | ||
260 | + // in the host component %-encoding can only be used | ||
261 | + // for non-ASCII bytes. | ||
262 | + // But https://tools.ietf.org/html/rfc6874#section-2 | ||
263 | + // introduces %25 being allowed to escape a percent sign | ||
264 | + // in IPv6 scoped-address literals. Yay. | ||
265 | + if mode == encodeHost && unhex(s[i+1]) < 8 && s[i:i+3] != "%25" { | ||
266 | + return "", EscapeError(s[i : i+3]) | ||
267 | + } | ||
268 | + if mode == encodeZone { | ||
269 | + // RFC 6874 says basically "anything goes" for zone identifiers | ||
270 | + // and that even non-ASCII can be redundantly escaped, | ||
271 | + // but it seems prudent to restrict %-escaped bytes here to those | ||
272 | + // that are valid host name bytes in their unescaped form. | ||
273 | + // That is, you can use escaping in the zone identifier but not | ||
274 | + // to introduce bytes you couldn't just write directly. | ||
275 | + // But Windows puts spaces here! Yay. | ||
276 | + v := unhex(s[i+1])<<4 | unhex(s[i+2]) | ||
277 | + if s[i:i+3] != "%25" && v != ' ' && shouldEscape(v, encodeHost) { | ||
278 | + return "", EscapeError(s[i : i+3]) | ||
279 | + } | ||
280 | + } | ||
281 | + i += 3 | ||
282 | + case '+': | ||
283 | + hasPlus = mode == encodeQueryComponent | ||
284 | + i++ | ||
285 | + default: | ||
286 | + if (mode == encodeHost || mode == encodeZone) && s[i] < 0x80 && shouldEscape(s[i], mode) { | ||
287 | + return "", InvalidHostError(s[i : i+1]) | ||
288 | + } | ||
289 | + i++ | ||
290 | + } | ||
291 | + } | ||
292 | + | ||
293 | + if n == 0 && !hasPlus { | ||
294 | + return s, nil | ||
295 | + } | ||
296 | + | ||
297 | + t := make([]byte, len(s)-2*n) | ||
298 | + j := 0 | ||
299 | + for i := 0; i < len(s); { | ||
300 | + switch s[i] { | ||
301 | + case '%': | ||
302 | + t[j] = unhex(s[i+1])<<4 | unhex(s[i+2]) | ||
303 | + j++ | ||
304 | + i += 3 | ||
305 | + case '+': | ||
306 | + if mode == encodeQueryComponent { | ||
307 | + t[j] = ' ' | ||
308 | + } else { | ||
309 | + t[j] = '+' | ||
310 | + } | ||
311 | + j++ | ||
312 | + i++ | ||
313 | + default: | ||
314 | + t[j] = s[i] | ||
315 | + j++ | ||
316 | + i++ | ||
317 | + } | ||
318 | + } | ||
319 | + return string(t), nil | ||
320 | +} | ||
321 | + | ||
322 | +// Please be informed that for now shouldEscape does not check all | ||
323 | +// reserved characters correctly. See golang.org/issue/5684. | ||
324 | +func shouldEscape(c byte, mode encoding) bool { | ||
325 | + // §2.3 Unreserved characters (alphanum) | ||
326 | + if 'A' <= c && c <= 'Z' || 'a' <= c && c <= 'z' || '0' <= c && c <= '9' { | ||
327 | + return false | ||
328 | + } | ||
329 | + | ||
330 | + if mode == encodeHost || mode == encodeZone { | ||
331 | + // §3.2.2 Host allows | ||
332 | + // sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" | ||
333 | + // as part of reg-name. | ||
334 | + // We add : because we include :port as part of host. | ||
335 | + // We add [ ] because we include [ipv6]:port as part of host. | ||
336 | + // We add < > because they're the only characters left that | ||
337 | + // we could possibly allow, and Parse will reject them if we | ||
338 | + // escape them (because hosts can't use %-encoding for | ||
339 | + // ASCII bytes). | ||
340 | + switch c { | ||
341 | + case '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=', ':', '[', ']', '<', '>', '"': | ||
342 | + return false | ||
343 | + } | ||
344 | + } | ||
345 | + | ||
346 | + switch c { | ||
347 | + case '-', '_', '.', '~': // §2.3 Unreserved characters (mark) | ||
348 | + return false | ||
349 | + | ||
350 | + case '$', '&', '+', ',', '/', ':', ';', '=', '?', '@': // §2.2 Reserved characters (reserved) | ||
351 | + // Different sections of the URL allow a few of | ||
352 | + // the reserved characters to appear unescaped. | ||
353 | + switch mode { | ||
354 | + case encodePath: // §3.3 | ||
355 | + // The RFC allows : @ & = + $ but saves / ; , for assigning | ||
356 | + // meaning to individual path segments. This package | ||
357 | + // only manipulates the path as a whole, so we allow those | ||
358 | + // last three as well. That leaves only ? to escape. | ||
359 | + return c == '?' | ||
360 | + | ||
361 | + case encodePathSegment: // §3.3 | ||
362 | + // The RFC allows : @ & = + $ but saves / ; , for assigning | ||
363 | + // meaning to individual path segments. | ||
364 | + return c == '/' || c == ';' || c == ',' || c == '?' | ||
365 | + | ||
366 | + case encodeUserPassword: // §3.2.1 | ||
367 | + // The RFC allows ';', ':', '&', '=', '+', '$', and ',' in | ||
368 | + // userinfo, so we must escape only '@', '/', and '?'. | ||
369 | + // The parsing of userinfo treats ':' as special so we must escape | ||
370 | + // that too. | ||
371 | + return c == '@' || c == '/' || c == '?' || c == ':' | ||
372 | + | ||
373 | + case encodeQueryComponent: // §3.4 | ||
374 | + // The RFC reserves (so we must escape) everything. | ||
375 | + return true | ||
376 | + | ||
377 | + case encodeFragment: // §4.1 | ||
378 | + // The RFC text is silent but the grammar allows | ||
379 | + // everything, so escape nothing. | ||
380 | + return false | ||
381 | + } | ||
382 | + } | ||
383 | + | ||
384 | + // Everything else must be escaped. | ||
385 | + return true | ||
386 | +} | ||
387 | + | ||
388 | +func ishex(c byte) bool { | ||
389 | + switch { | ||
390 | + case '0' <= c && c <= '9': | ||
391 | + return true | ||
392 | + case 'a' <= c && c <= 'f': | ||
393 | + return true | ||
394 | + case 'A' <= c && c <= 'F': | ||
395 | + return true | ||
396 | + } | ||
397 | + return false | ||
398 | +} | ||
399 | + | ||
400 | +func unhex(c byte) byte { | ||
401 | + switch { | ||
402 | + case '0' <= c && c <= '9': | ||
403 | + return c - '0' | ||
404 | + case 'a' <= c && c <= 'f': | ||
405 | + return c - 'a' + 10 | ||
406 | + case 'A' <= c && c <= 'F': | ||
407 | + return c - 'A' + 10 | ||
408 | + } | ||
409 | + return 0 | ||
410 | +} |
-
请 注册 或 登录 后发表评论