mirror of
https://github.com/grafana/grafana.git
synced 2024-11-28 19:54:10 -06:00
3689bb778c
See, $ gometalinter --disable-all --enable misspell --deadline 10m --vendor ./... pkg/api/dtos/alerting_test.go:32:13⚠️ "expectes" is a misspelling of "expects" (misspell) pkg/api/static/static.go:2:18⚠️ "Unknwon" is a misspelling of "Unknown" (misspell) pkg/components/imguploader/azureblobuploader.go:55:48⚠️ "conatiner" is a misspelling of "container" (misspell) pkg/login/ldap_settings.go:51:115⚠️ "compatability" is a misspelling of "compatibility" (misspell) pkg/middleware/auth_proxy_test.go:122:22⚠️ "Destory" is a misspelling of "Destroy" (misspell) pkg/middleware/logger.go:2:18⚠️ "Unknwon" is a misspelling of "Unknown" (misspell) pkg/services/notifications/codes.go:9:13⚠️ "Unknwon" is a misspelling of "Unknown" (misspell) pkg/services/session/mysql.go:170:3⚠️ "Destory" is a misspelling of "Destroy" (misspell) pkg/services/session/mysql.go:171:24⚠️ "Destory" is a misspelling of "Destroy" (misspell) pkg/services/session/session.go:95:4⚠️ "Destory" is a misspelling of "Destroy" (misspell) pkg/services/session/session.go:96:1⚠️ "Destory" is a misspelling of "Destroy" (misspell) pkg/services/session/session.go:167:25⚠️ "Destory" is a misspelling of "Destroy" (misspell) pkg/setting/setting.go:1:18⚠️ "Unknwon" is a misspelling of "Unknown" (misspell) pkg/tsdb/cloudwatch/cloudwatch.go:199:14⚠️ "resolutin" is a misspelling of "resolutions" (misspell) pkg/tsdb/cloudwatch/cloudwatch.go:270:15⚠️ "resolutin" is a misspelling of "resolutions" (misspell) pkg/tsdb/elasticsearch/response_parser.go:531:24⚠️ "Unkown" is a misspelling of "Unknown" (misspell) pkg/tsdb/elasticsearch/client/search_request.go:113:7⚠️ "initaite" is a misspelling of "initiate" (misspell) Note: Unknwon is a library name, and Destory a mysql typo.
321 lines
8.8 KiB
Go
321 lines
8.8 KiB
Go
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", "container_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, 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")
|
|
}
|