azureblobuploader.go 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. package imguploader
  2. import (
  3. "bytes"
  4. "context"
  5. "crypto/hmac"
  6. "crypto/sha256"
  7. "encoding/base64"
  8. "encoding/xml"
  9. "fmt"
  10. "io"
  11. "io/ioutil"
  12. "mime"
  13. "net/http"
  14. "net/url"
  15. "os"
  16. "path"
  17. "sort"
  18. "strconv"
  19. "strings"
  20. "time"
  21. "github.com/grafana/grafana/pkg/infra/log"
  22. "github.com/grafana/grafana/pkg/util"
  23. )
  24. type AzureBlobUploader struct {
  25. account_name string
  26. account_key string
  27. container_name string
  28. log log.Logger
  29. }
  30. func NewAzureBlobUploader(account_name string, account_key string, container_name string) *AzureBlobUploader {
  31. return &AzureBlobUploader{
  32. account_name: account_name,
  33. account_key: account_key,
  34. container_name: container_name,
  35. log: log.New("azureBlobUploader"),
  36. }
  37. }
  38. // Receive path of image on disk and return azure blob url
  39. func (az *AzureBlobUploader) Upload(ctx context.Context, imageDiskPath string) (string, error) {
  40. // setup client
  41. blob := NewStorageClient(az.account_name, az.account_key)
  42. file, err := os.Open(imageDiskPath)
  43. if err != nil {
  44. return "", err
  45. }
  46. defer file.Close()
  47. randomFileName := util.GetRandomString(30) + ".png"
  48. // upload image
  49. az.log.Debug("Uploading image to azure_blob", "container_name", az.container_name, "blob_name", randomFileName)
  50. resp, err := blob.FileUpload(az.container_name, randomFileName, file)
  51. if err != nil {
  52. return "", err
  53. }
  54. if resp.StatusCode > 400 && resp.StatusCode < 600 {
  55. body, _ := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20))
  56. aerr := &Error{
  57. Code: resp.StatusCode,
  58. Status: resp.Status,
  59. Body: body,
  60. Header: resp.Header,
  61. }
  62. aerr.parseXML()
  63. resp.Body.Close()
  64. return "", aerr
  65. }
  66. if err != nil {
  67. return "", err
  68. }
  69. url := fmt.Sprintf("https://%s.blob.core.windows.net/%s/%s", az.account_name, az.container_name, randomFileName)
  70. return url, nil
  71. }
  72. // --- AZURE LIBRARY
  73. type Blobs struct {
  74. XMLName xml.Name `xml:"EnumerationResults"`
  75. Items []Blob `xml:"Blobs>Blob"`
  76. }
  77. type Blob struct {
  78. Name string `xml:"Name"`
  79. Property Property `xml:"Properties"`
  80. }
  81. type Property struct {
  82. LastModified string `xml:"Last-Modified"`
  83. Etag string `xml:"Etag"`
  84. ContentLength int `xml:"Content-Length"`
  85. ContentType string `xml:"Content-Type"`
  86. BlobType string `xml:"BlobType"`
  87. LeaseStatus string `xml:"LeaseStatus"`
  88. }
  89. type Error struct {
  90. Code int
  91. Status string
  92. Body []byte
  93. Header http.Header
  94. AzureCode string
  95. }
  96. func (e *Error) Error() string {
  97. return fmt.Sprintf("status %d: %s", e.Code, e.Body)
  98. }
  99. func (e *Error) parseXML() {
  100. var xe xmlError
  101. _ = xml.NewDecoder(bytes.NewReader(e.Body)).Decode(&xe)
  102. e.AzureCode = xe.Code
  103. }
  104. type xmlError struct {
  105. XMLName xml.Name `xml:"Error"`
  106. Code string
  107. Message string
  108. }
  109. const ms_date_layout = "Mon, 02 Jan 2006 15:04:05 GMT"
  110. const version = "2017-04-17"
  111. type StorageClient struct {
  112. Auth *Auth
  113. Transport http.RoundTripper
  114. }
  115. func (c *StorageClient) transport() http.RoundTripper {
  116. if c.Transport != nil {
  117. return c.Transport
  118. }
  119. return http.DefaultTransport
  120. }
  121. func NewStorageClient(account, accessKey string) *StorageClient {
  122. return &StorageClient{
  123. Auth: &Auth{
  124. account,
  125. accessKey,
  126. },
  127. Transport: nil,
  128. }
  129. }
  130. func (c *StorageClient) absUrl(format string, a ...interface{}) string {
  131. part := fmt.Sprintf(format, a...)
  132. return fmt.Sprintf("https://%s.blob.core.windows.net/%s", c.Auth.Account, part)
  133. }
  134. func copyHeadersToRequest(req *http.Request, headers map[string]string) {
  135. for k, v := range headers {
  136. req.Header[k] = []string{v}
  137. }
  138. }
  139. func (c *StorageClient) FileUpload(container, blobName string, body io.Reader) (*http.Response, error) {
  140. blobName = escape(blobName)
  141. extension := strings.ToLower(path.Ext(blobName))
  142. contentType := mime.TypeByExtension(extension)
  143. buf := new(bytes.Buffer)
  144. buf.ReadFrom(body)
  145. req, err := http.NewRequest(
  146. "PUT",
  147. c.absUrl("%s/%s", container, blobName),
  148. buf,
  149. )
  150. if err != nil {
  151. return nil, err
  152. }
  153. copyHeadersToRequest(req, map[string]string{
  154. "x-ms-blob-type": "BlockBlob",
  155. "x-ms-date": time.Now().UTC().Format(ms_date_layout),
  156. "x-ms-version": version,
  157. "Accept-Charset": "UTF-8",
  158. "Content-Type": contentType,
  159. "Content-Length": strconv.Itoa(buf.Len()),
  160. })
  161. c.Auth.SignRequest(req)
  162. return c.transport().RoundTrip(req)
  163. }
  164. func escape(content string) string {
  165. content = url.QueryEscape(content)
  166. // the Azure's behavior uses %20 to represent whitespace instead of + (plus)
  167. content = strings.Replace(content, "+", "%20", -1)
  168. // the Azure's behavior uses slash instead of + %2F
  169. content = strings.Replace(content, "%2F", "/", -1)
  170. return content
  171. }
  172. type Auth struct {
  173. Account string
  174. Key string
  175. }
  176. func (a *Auth) SignRequest(req *http.Request) {
  177. 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",
  178. strings.ToUpper(req.Method),
  179. tryget(req.Header, "Content-Encoding"),
  180. tryget(req.Header, "Content-Language"),
  181. tryget(req.Header, "Content-Length"),
  182. tryget(req.Header, "Content-MD5"),
  183. tryget(req.Header, "Content-Type"),
  184. tryget(req.Header, "Date"),
  185. tryget(req.Header, "If-Modified-Since"),
  186. tryget(req.Header, "If-Match"),
  187. tryget(req.Header, "If-None-Match"),
  188. tryget(req.Header, "If-Unmodified-Since"),
  189. tryget(req.Header, "Range"),
  190. a.canonicalizedHeaders(req),
  191. a.canonicalizedResource(req),
  192. )
  193. decodedKey, _ := base64.StdEncoding.DecodeString(a.Key)
  194. sha256 := hmac.New(sha256.New, decodedKey)
  195. sha256.Write([]byte(strToSign))
  196. signature := base64.StdEncoding.EncodeToString(sha256.Sum(nil))
  197. copyHeadersToRequest(req, map[string]string{
  198. "Authorization": fmt.Sprintf("SharedKey %s:%s", a.Account, signature),
  199. })
  200. }
  201. func tryget(headers map[string][]string, key string) string {
  202. // We default to empty string for "0" values to match server side behavior when generating signatures.
  203. if len(headers[key]) > 0 { // && headers[key][0] != "0" { //&& key != "Content-Length" {
  204. return headers[key][0]
  205. }
  206. return ""
  207. }
  208. //
  209. // The following is copied ~95% verbatim from:
  210. // http://github.com/loldesign/azure/ -> core/core.go
  211. //
  212. /*
  213. Based on Azure docs:
  214. Link: http://msdn.microsoft.com/en-us/library/windowsazure/dd179428.aspx#Constructing_Element
  215. 1) Retrieve all headers for the resource that begin with x-ms-, including the x-ms-date header.
  216. 2) Convert each HTTP header name to lowercase.
  217. 3) Sort the headers lexicographically by header name, in ascending order. Note that each header may appear only once in the string.
  218. 4) Unfold the string by replacing any breaking white space with a single space.
  219. 5) Trim any white space around the colon in the header.
  220. 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.
  221. */
  222. func (a *Auth) canonicalizedHeaders(req *http.Request) string {
  223. var buffer bytes.Buffer
  224. for key, value := range req.Header {
  225. lowerKey := strings.ToLower(key)
  226. if strings.HasPrefix(lowerKey, "x-ms-") {
  227. if buffer.Len() == 0 {
  228. buffer.WriteString(fmt.Sprintf("%s:%s", lowerKey, value[0]))
  229. } else {
  230. buffer.WriteString(fmt.Sprintf("\n%s:%s", lowerKey, value[0]))
  231. }
  232. }
  233. }
  234. split := strings.Split(buffer.String(), "\n")
  235. sort.Strings(split)
  236. return strings.Join(split, "\n")
  237. }
  238. /*
  239. Based on Azure docs
  240. Link: http://msdn.microsoft.com/en-us/library/windowsazure/dd179428.aspx#Constructing_Element
  241. 1) Beginning with an empty string (""), append a forward slash (/), followed by the name of the account that owns the resource being accessed.
  242. 2) Append the resource's encoded URI path, without any query parameters.
  243. 3) Retrieve all query parameters on the resource URI, including the comp parameter if it exists.
  244. 4) Convert all parameter names to lowercase.
  245. 5) Sort the query parameters lexicographically by parameter name, in ascending order.
  246. 6) URL-decode each query parameter name and value.
  247. 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:
  248. parameter-name:parameter-value
  249. 8) If a query parameter has more than one value, sort all values lexicographically, then include them in a comma-separated list:
  250. parameter-name:parameter-value-1,parameter-value-2,parameter-value-n
  251. 9) Append a new line character (\n) after each name-value pair.
  252. Rules:
  253. 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.
  254. 2) Avoid using commas in query parameter values.
  255. */
  256. func (a *Auth) canonicalizedResource(req *http.Request) string {
  257. var buffer bytes.Buffer
  258. buffer.WriteString(fmt.Sprintf("/%s%s", a.Account, req.URL.Path))
  259. queries := req.URL.Query()
  260. for key, values := range queries {
  261. sort.Strings(values)
  262. buffer.WriteString(fmt.Sprintf("\n%s:%s", key, strings.Join(values, ",")))
  263. }
  264. split := strings.Split(buffer.String(), "\n")
  265. sort.Strings(split)
  266. return strings.Join(split, "\n")
  267. }