You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
256 lines
6.2 KiB
256 lines
6.2 KiB
package datacenter |
|
|
|
import ( |
|
"bytes" |
|
"context" |
|
"crypto/md5" |
|
"encoding/json" |
|
"fmt" |
|
"io" |
|
"net/http" |
|
"net/url" |
|
"sort" |
|
"strings" |
|
"time" |
|
|
|
"go-common/library/log" |
|
|
|
pkgerr "github.com/pkg/errors" |
|
"go-common/library/stat" |
|
"strconv" |
|
) |
|
|
|
/* |
|
访问数据平台的http client,处理了签名、接口监控等 |
|
*/ |
|
|
|
//ClientConfig client config |
|
type ClientConfig struct { |
|
Key string |
|
Secret string |
|
Dial time.Duration |
|
Timeout time.Duration |
|
KeepAlive time.Duration |
|
} |
|
|
|
//New new client |
|
func New(c *ClientConfig) *HttpClient { |
|
return &HttpClient{ |
|
client: &http.Client{}, |
|
conf: c, |
|
} |
|
} |
|
|
|
//HttpClient http client |
|
type HttpClient struct { |
|
client *http.Client |
|
conf *ClientConfig |
|
Debug bool |
|
} |
|
|
|
//Response response |
|
type Response struct { |
|
Code int `json:"code"` |
|
Msg string `json:"msg"` |
|
Result interface{} `json:"result"` |
|
} |
|
|
|
const ( |
|
keyAppKey = "appKey" |
|
keyAppID = "apiId" |
|
keyTimeStamp = "timestamp" |
|
keySign = "sign" |
|
keySignMethod = "signMethod" |
|
keyVersion = "version" |
|
//TimeStampFormat time format in second |
|
TimeStampFormat = "2006-01-02 15:04:05" |
|
) |
|
|
|
var ( |
|
clientStats = stat.HTTPClient |
|
) |
|
|
|
// Get issues a GET to the specified URL. |
|
func (client *HttpClient) Get(c context.Context, uri string, params url.Values, res interface{}) (err error) { |
|
req, err := client.NewRequest(http.MethodGet, uri, params) |
|
if err != nil { |
|
return |
|
} |
|
return client.Do(c, req, res) |
|
} |
|
|
|
// NewRequest new http request with method, uri, ip, values and headers. |
|
// TODO(zhoujiahui): param realIP should be removed later. |
|
func (client *HttpClient) NewRequest(method, uri string, params url.Values) (req *http.Request, err error) { |
|
signStr, err := client.sign(params) |
|
if err != nil { |
|
err = pkgerr.Wrapf(err, "uri:%s,params:%v", uri, params) |
|
return |
|
} |
|
params.Add(keySign, signStr) |
|
enc := params.Encode() |
|
ru := uri |
|
if enc != "" { |
|
ru = uri + "?" + enc |
|
} |
|
if method == http.MethodGet { |
|
req, err = http.NewRequest(http.MethodGet, ru, nil) |
|
} else { |
|
req, err = http.NewRequest(http.MethodPost, uri, strings.NewReader(enc)) |
|
} |
|
if err != nil { |
|
err = pkgerr.Wrapf(err, "method:%s,uri:%s", method, ru) |
|
return |
|
} |
|
const ( |
|
_contentType = "Content-Type" |
|
_urlencoded = "application/x-www-form-urlencoded" |
|
) |
|
if method == http.MethodPost { |
|
req.Header.Set(_contentType, _urlencoded) |
|
} |
|
|
|
return |
|
} |
|
|
|
// Do sends an HTTP request and returns an HTTP json response. |
|
func (client *HttpClient) Do(c context.Context, req *http.Request, res interface{}, v ...string) (err error) { |
|
var bs []byte |
|
if bs, err = client.Raw(c, req, v...); err != nil { |
|
return |
|
} |
|
if res != nil { |
|
if err = json.Unmarshal(bs, res); err != nil { |
|
err = pkgerr.Wrapf(err, "host:%s, url:%s, response:%s", req.URL.Host, realURL(req), string(bs)) |
|
} |
|
} |
|
return |
|
} |
|
|
|
//Raw get from url |
|
func (client *HttpClient) Raw(c context.Context, req *http.Request, v ...string) (bs []byte, err error) { |
|
var resp *http.Response |
|
var uri = fmt.Sprintf("%s://%s%s", req.URL.Scheme, req.Host, req.URL.Path) |
|
|
|
var now = time.Now() |
|
var code string |
|
defer func() { |
|
clientStats.Timing(uri, int64(time.Since(now)/time.Millisecond)) |
|
if code != "" { |
|
clientStats.Incr(uri, code) |
|
} |
|
}() |
|
req = req.WithContext(c) |
|
if resp, err = client.client.Do(req); err != nil { |
|
err = pkgerr.Wrapf(err, "host:%s, url:%s", req.URL.Host, realURL(req)) |
|
code = "failed" |
|
return |
|
} |
|
defer resp.Body.Close() |
|
if resp.StatusCode >= http.StatusBadRequest { |
|
err = pkgerr.Errorf("incorrect http status:%d host:%s, url:%s", resp.StatusCode, req.URL.Host, realURL(req)) |
|
code = strconv.Itoa(resp.StatusCode) |
|
return |
|
} |
|
if bs, err = readAll(resp.Body, 16*1024); err != nil { |
|
err = pkgerr.Wrapf(err, "host:%s, url:%s", req.URL.Host, realURL(req)) |
|
return |
|
} |
|
if client.Debug { |
|
log.Info("reqeust: host:%s, url:%s, response body:%s", req.URL.Host, realURL(req), string(bs)) |
|
} |
|
return |
|
} |
|
|
|
// sign calc appkey and appsecret sign. |
|
// see http://info.bilibili.co/pages/viewpage.action?pageId=5410881#id-%E6%95%B0%E6%8D%AE%E7%9B%98%EF%BC%8D%E5%AE%89%E5%85%A8%E8%AE%A4%E8%AF%81-%E4%BA%8C%E7%AD%BE%E5%90%8D%E7%AE%97%E6%B3%95 |
|
func (client *HttpClient) sign(params url.Values) (sign string, err error) { |
|
key := client.conf.Key |
|
secret := client.conf.Secret |
|
if params == nil { |
|
params = url.Values{} |
|
} |
|
|
|
params.Set(keyAppKey, key) |
|
params.Set(keyVersion, "1.0") |
|
if params.Get(keyTimeStamp) == "" { |
|
params.Set(keyTimeStamp, time.Now().Format(TimeStampFormat)) |
|
} |
|
params.Set(keySignMethod, "md5") |
|
|
|
var needSignParams = url.Values{} |
|
needSignParams.Add(keyAppKey, key) |
|
needSignParams.Add(keyTimeStamp, params.Get(keyTimeStamp)) |
|
needSignParams.Add(keyVersion, params.Get(keyVersion)) |
|
|
|
//tmp := params.Encode() |
|
var valueMap = map[string][]string(needSignParams) |
|
var buf bytes.Buffer |
|
// 开头与结尾加secret |
|
buf.Write([]byte(secret)) |
|
keys := make([]string, 0, len(valueMap)) |
|
for k := range valueMap { |
|
keys = append(keys, k) |
|
} |
|
sort.Strings(keys) |
|
for _, k := range keys { |
|
vs := valueMap[k] |
|
prefix := k |
|
buf.WriteString(prefix) |
|
for _, v := range vs { |
|
buf.WriteString(v) |
|
break |
|
} |
|
} |
|
|
|
buf.Write([]byte(secret)) |
|
var md5 = md5.New() |
|
md5.Write(buf.Bytes()) |
|
sign = fmt.Sprintf("%X", md5.Sum(nil)) |
|
return |
|
} |
|
|
|
// readAll reads from r until an error or EOF and returns the data it read |
|
// from the internal buffer allocated with a specified capacity. |
|
func readAll(r io.Reader, capacity int64) (b []byte, err error) { |
|
buf := bytes.NewBuffer(make([]byte, 0, capacity)) |
|
// If the buffer overflows, we will get bytes.ErrTooLarge. |
|
// Return that as an error. Any other panic remains. |
|
defer func() { |
|
e := recover() |
|
if e == nil { |
|
return |
|
} |
|
if panicErr, ok := e.(error); ok && panicErr == bytes.ErrTooLarge { |
|
err = panicErr |
|
} else { |
|
panic(e) |
|
} |
|
}() |
|
_, err = buf.ReadFrom(r) |
|
return buf.Bytes(), err |
|
} |
|
|
|
// realUrl return url with http://host/params. |
|
func realURL(req *http.Request) string { |
|
if req.Method == http.MethodGet { |
|
return req.URL.String() |
|
} else if req.Method == http.MethodPost { |
|
ru := req.URL.Path |
|
if req.Body != nil { |
|
rd, ok := req.Body.(io.Reader) |
|
if ok { |
|
buf := bytes.NewBuffer([]byte{}) |
|
buf.ReadFrom(rd) |
|
ru = ru + "?" + buf.String() |
|
} |
|
} |
|
return ru |
|
} |
|
return req.URL.Path |
|
} |
|
|
|
// SetTransport set client transport |
|
func (client *HttpClient) SetTransport(t http.RoundTripper) { |
|
client.client.Transport = t |
|
}
|
|
|