| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320 |
- package imguploader
- import (
- "bytes"
- "context"
- "crypto/hmac"
- "crypto/sha256"
- "encoding/base64"
- "encoding/xml"
- "fmt"
- "io"
- "io/ioutil"
- "mime"
- "net/http"
- "net/url"
- "os"
- "path"
- "sort"
- "strconv"
- "strings"
- "time"
- "github.com/grafana/grafana/pkg/log"
- "github.com/grafana/grafana/pkg/util"
- )
- type AzureBlobUploader struct {
- account_name string
- account_key string
- container_name string
- log log.Logger
- }
- func NewAzureBlobUploader(account_name string, account_key string, container_name string) *AzureBlobUploader {
- return &AzureBlobUploader{
- account_name: account_name,
- account_key: account_key,
- container_name: container_name,
- log: log.New("azureBlobUploader"),
- }
- }
- // Receive path of image on disk and return azure blob url
- func (az *AzureBlobUploader) Upload(ctx context.Context, imageDiskPath string) (string, error) {
- // setup client
- blob := NewStorageClient(az.account_name, az.account_key)
- file, err := os.Open(imageDiskPath)
- if err != nil {
- return "", err
- }
- randomFileName := util.GetRandomString(30) + ".png"
- // upload image
- az.log.Debug("Uploading image to azure_blob", "conatiner_name", az.container_name, "blob_name", randomFileName)
- resp, err := blob.FileUpload(az.container_name, randomFileName, file)
- if err != nil {
- return "", err
- }
- if resp.StatusCode > 400 && resp.StatusCode < 600 {
- body, _ := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20))
- aerr := &Error{
- Code: resp.StatusCode,
- Status: resp.Status,
- Body: body,
- Header: resp.Header,
- }
- aerr.parseXML()
- resp.Body.Close()
- return "", aerr
- }
- if err != nil {
- return "", err
- }
- url := fmt.Sprintf("https://%s.blob.core.windows.net/%s/%s", az.account_name, az.container_name, randomFileName)
- return url, nil
- }
- // --- AZURE LIBRARY
- type Blobs struct {
- XMLName xml.Name `xml:"EnumerationResults"`
- Items []Blob `xml:"Blobs>Blob"`
- }
- type Blob struct {
- Name string `xml:"Name"`
- Property Property `xml:"Properties"`
- }
- type Property struct {
- LastModified string `xml:"Last-Modified"`
- Etag string `xml:"Etag"`
- ContentLength int `xml:"Content-Length"`
- ContentType string `xml:"Content-Type"`
- BlobType string `xml:"BlobType"`
- LeaseStatus string `xml:"LeaseStatus"`
- }
- type Error struct {
- Code int
- Status string
- Body []byte
- Header http.Header
- AzureCode string
- }
- func (e *Error) Error() string {
- return fmt.Sprintf("status %d: %s", e.Code, e.Body)
- }
- func (e *Error) parseXML() {
- var xe xmlError
- _ = xml.NewDecoder(bytes.NewReader(e.Body)).Decode(&xe)
- e.AzureCode = xe.Code
- }
- type xmlError struct {
- XMLName xml.Name `xml:"Error"`
- Code string
- Message string
- }
- const ms_date_layout = "Mon, 02 Jan 2006 15:04:05 GMT"
- const version = "2017-04-17"
- var client = &http.Client{}
- type StorageClient struct {
- Auth *Auth
- Transport http.RoundTripper
- }
- func (c *StorageClient) transport() http.RoundTripper {
- if c.Transport != nil {
- return c.Transport
- }
- return http.DefaultTransport
- }
- func NewStorageClient(account, accessKey string) *StorageClient {
- return &StorageClient{
- Auth: &Auth{
- account,
- accessKey,
- },
- Transport: nil,
- }
- }
- func (c *StorageClient) absUrl(format string, a ...interface{}) string {
- part := fmt.Sprintf(format, a...)
- return fmt.Sprintf("https://%s.blob.core.windows.net/%s", c.Auth.Account, part)
- }
- func copyHeadersToRequest(req *http.Request, headers map[string]string) {
- for k, v := range headers {
- req.Header[k] = []string{v}
- }
- }
- func (c *StorageClient) FileUpload(container, blobName string, body io.Reader) (*http.Response, error) {
- blobName = escape(blobName)
- extension := strings.ToLower(path.Ext(blobName))
- contentType := mime.TypeByExtension(extension)
- buf := new(bytes.Buffer)
- buf.ReadFrom(body)
- req, err := http.NewRequest(
- "PUT",
- c.absUrl("%s/%s", container, blobName),
- buf,
- )
- if err != nil {
- return nil, err
- }
- copyHeadersToRequest(req, map[string]string{
- "x-ms-blob-type": "BlockBlob",
- "x-ms-date": time.Now().UTC().Format(ms_date_layout),
- "x-ms-version": version,
- "Accept-Charset": "UTF-8",
- "Content-Type": contentType,
- "Content-Length": strconv.Itoa(buf.Len()),
- })
- c.Auth.SignRequest(req)
- return c.transport().RoundTrip(req)
- }
- func escape(content string) string {
- content = url.QueryEscape(content)
- // the Azure's behavior uses %20 to represent whitespace instead of + (plus)
- content = strings.Replace(content, "+", "%20", -1)
- // the Azure's behavior uses slash instead of + %2F
- content = strings.Replace(content, "%2F", "/", -1)
- return content
- }
- type Auth struct {
- Account string
- Key string
- }
- func (a *Auth) SignRequest(req *http.Request) {
- strToSign := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s",
- strings.ToUpper(req.Method),
- tryget(req.Header, "Content-Encoding"),
- tryget(req.Header, "Content-Language"),
- tryget(req.Header, "Content-Length"),
- tryget(req.Header, "Content-MD5"),
- tryget(req.Header, "Content-Type"),
- tryget(req.Header, "Date"),
- tryget(req.Header, "If-Modified-Since"),
- tryget(req.Header, "If-Match"),
- tryget(req.Header, "If-None-Match"),
- tryget(req.Header, "If-Unmodified-Since"),
- tryget(req.Header, "Range"),
- a.canonicalizedHeaders(req),
- a.canonicalizedResource(req),
- )
- decodedKey, _ := base64.StdEncoding.DecodeString(a.Key)
- sha256 := hmac.New(sha256.New, []byte(decodedKey))
- sha256.Write([]byte(strToSign))
- signature := base64.StdEncoding.EncodeToString(sha256.Sum(nil))
- copyHeadersToRequest(req, map[string]string{
- "Authorization": fmt.Sprintf("SharedKey %s:%s", a.Account, signature),
- })
- }
- func tryget(headers map[string][]string, key string) string {
- // We default to empty string for "0" values to match server side behavior when generating signatures.
- if len(headers[key]) > 0 { // && headers[key][0] != "0" { //&& key != "Content-Length" {
- return headers[key][0]
- }
- return ""
- }
- //
- // The following is copied ~95% verbatim from:
- // http://github.com/loldesign/azure/ -> core/core.go
- //
- /*
- Based on Azure docs:
- Link: http://msdn.microsoft.com/en-us/library/windowsazure/dd179428.aspx#Constructing_Element
- 1) Retrieve all headers for the resource that begin with x-ms-, including the x-ms-date header.
- 2) Convert each HTTP header name to lowercase.
- 3) Sort the headers lexicographically by header name, in ascending order. Note that each header may appear only once in the string.
- 4) Unfold the string by replacing any breaking white space with a single space.
- 5) Trim any white space around the colon in the header.
- 6) Finally, append a new line character to each canonicalized header in the resulting list. Construct the CanonicalizedHeaders string by concatenating all headers in this list into a single string.
- */
- func (a *Auth) canonicalizedHeaders(req *http.Request) string {
- var buffer bytes.Buffer
- for key, value := range req.Header {
- lowerKey := strings.ToLower(key)
- if strings.HasPrefix(lowerKey, "x-ms-") {
- if buffer.Len() == 0 {
- buffer.WriteString(fmt.Sprintf("%s:%s", lowerKey, value[0]))
- } else {
- buffer.WriteString(fmt.Sprintf("\n%s:%s", lowerKey, value[0]))
- }
- }
- }
- splitted := strings.Split(buffer.String(), "\n")
- sort.Strings(splitted)
- return strings.Join(splitted, "\n")
- }
- /*
- Based on Azure docs
- Link: http://msdn.microsoft.com/en-us/library/windowsazure/dd179428.aspx#Constructing_Element
- 1) Beginning with an empty string (""), append a forward slash (/), followed by the name of the account that owns the resource being accessed.
- 2) Append the resource's encoded URI path, without any query parameters.
- 3) Retrieve all query parameters on the resource URI, including the comp parameter if it exists.
- 4) Convert all parameter names to lowercase.
- 5) Sort the query parameters lexicographically by parameter name, in ascending order.
- 6) URL-decode each query parameter name and value.
- 7) Append each query parameter name and value to the string in the following format, making sure to include the colon (:) between the name and the value:
- parameter-name:parameter-value
- 8) If a query parameter has more than one value, sort all values lexicographically, then include them in a comma-separated list:
- parameter-name:parameter-value-1,parameter-value-2,parameter-value-n
- 9) Append a new line character (\n) after each name-value pair.
- Rules:
- 1) Avoid using the new line character (\n) in values for query parameters. If it must be used, ensure that it does not affect the format of the canonicalized resource string.
- 2) Avoid using commas in query parameter values.
- */
- func (a *Auth) canonicalizedResource(req *http.Request) string {
- var buffer bytes.Buffer
- buffer.WriteString(fmt.Sprintf("/%s%s", a.Account, req.URL.Path))
- queries := req.URL.Query()
- for key, values := range queries {
- sort.Strings(values)
- buffer.WriteString(fmt.Sprintf("\n%s:%s", key, strings.Join(values, ",")))
- }
- splitted := strings.Split(buffer.String(), "\n")
- sort.Strings(splitted)
- return strings.Join(splitted, "\n")
- }
|