Commit d352c213 by Arve Knudsen Committed by GitHub

API: Recognize MSSQL data source URLs (#25629)

* API: Recognize MSSQL URLs

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>

* Move MSSQL URL validation into mssql package

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>
parent e6a5e880
...@@ -4,46 +4,59 @@ import ( ...@@ -4,46 +4,59 @@ import (
"fmt" "fmt"
"net/url" "net/url"
"regexp" "regexp"
"strings"
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/tsdb/mssql"
) )
var logger = log.New("datasource") var logger = log.New("datasource")
// URLValidationError represents an error from validating a data source URL. // URLValidationError represents an error from validating a data source URL.
type URLValidationError struct { type URLValidationError struct {
error Err error
url string URL string
} }
// Error returns the error message. // Error returns the error message.
func (e URLValidationError) Error() string { func (e URLValidationError) Error() string {
return fmt.Sprintf("Validation of data source URL %q failed: %s", e.url, e.error.Error()) return fmt.Sprintf("Validation of data source URL %q failed: %s", e.URL, e.Err.Error())
} }
// Unwrap returns the wrapped error. // Unwrap returns the wrapped error.
func (e URLValidationError) Unwrap() error { func (e URLValidationError) Unwrap() error {
return e.error return e.Err
} }
// reURL is a regexp to detect if a URL specifies the protocol. We match also strings where the actual protocol is // reURL is a regexp to detect if a URL specifies the protocol. We match also strings where the actual protocol is
// missing (i.e., "://"), in order to catch these as invalid when parsing. // missing (i.e., "://"), in order to catch these as invalid when parsing.
var reURL = regexp.MustCompile("^[^:]*://") var reURL = regexp.MustCompile("^[^:]*://")
// ValidateURL validates a data source URL. // ValidateURL validates a data source's URL.
// //
// If successful, the valid URL object is returned, otherwise an error is returned. // The data source's type and URL must be provided. If successful, the valid URL object is returned, otherwise an
func ValidateURL(urlStr string) (*url.URL, error) { // error is returned.
// Make sure the URL starts with a protocol specifier, so parsing is unambiguous func ValidateURL(typeName, urlStr string) (*url.URL, error) {
if !reURL.MatchString(urlStr) { var u *url.URL
logger.Debug( var err error
"Data source URL doesn't specify protocol, so prepending it with http:// in order to make it unambiguous") switch strings.ToLower(typeName) {
urlStr = fmt.Sprintf("http://%s", urlStr) case "mssql":
u, err = mssql.ParseURL(urlStr)
default:
logger.Debug("Applying default URL parsing for this data source type", "type", typeName, "url", urlStr)
// Make sure the URL starts with a protocol specifier, so parsing is unambiguous
if !reURL.MatchString(urlStr) {
logger.Debug(
"Data source URL doesn't specify protocol, so prepending it with http:// in order to make it unambiguous",
"type", typeName, "url", urlStr)
urlStr = fmt.Sprintf("http://%s", urlStr)
}
u, err = url.Parse(urlStr)
} }
u, err := url.Parse(urlStr)
if err != nil { if err != nil {
return nil, URLValidationError{error: err, url: urlStr} return nil, URLValidationError{Err: err, URL: urlStr}
} }
return u, nil return u, nil
......
...@@ -9,12 +9,15 @@ import ( ...@@ -9,12 +9,15 @@ import (
"github.com/grafana/grafana/pkg/api/datasource" "github.com/grafana/grafana/pkg/api/datasource"
"github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/datasource/wrapper" "github.com/grafana/grafana/pkg/plugins/datasource/wrapper"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
) )
var datasourcesLogger = log.New("datasources")
func GetDataSources(c *models.ReqContext) Response { func GetDataSources(c *models.ReqContext) Response {
query := models.GetDataSourcesQuery{OrgId: c.OrgId} query := models.GetDataSourcesQuery{OrgId: c.OrgId}
...@@ -127,9 +130,11 @@ func DeleteDataSourceByName(c *models.ReqContext) Response { ...@@ -127,9 +130,11 @@ func DeleteDataSourceByName(c *models.ReqContext) Response {
return Success("Data source deleted") return Success("Data source deleted")
} }
func validateURL(u string) Response { func validateURL(tp string, u string) Response {
if u != "" { if u != "" {
if _, err := datasource.ValidateURL(u); err != nil { if _, err := datasource.ValidateURL(tp, u); err != nil {
datasourcesLogger.Error("Received invalid data source URL as part of data source command",
"url", u)
return Error(400, fmt.Sprintf("Validation error, invalid URL: %q", u), err) return Error(400, fmt.Sprintf("Validation error, invalid URL: %q", u), err)
} }
} }
...@@ -138,8 +143,9 @@ func validateURL(u string) Response { ...@@ -138,8 +143,9 @@ func validateURL(u string) Response {
} }
func AddDataSource(c *models.ReqContext, cmd models.AddDataSourceCommand) Response { func AddDataSource(c *models.ReqContext, cmd models.AddDataSourceCommand) Response {
datasourcesLogger.Debug("Received command to add data source", "url", cmd.Url)
cmd.OrgId = c.OrgId cmd.OrgId = c.OrgId
if resp := validateURL(cmd.Url); resp != nil { if resp := validateURL(cmd.Type, cmd.Url); resp != nil {
return resp return resp
} }
...@@ -161,9 +167,10 @@ func AddDataSource(c *models.ReqContext, cmd models.AddDataSourceCommand) Respon ...@@ -161,9 +167,10 @@ func AddDataSource(c *models.ReqContext, cmd models.AddDataSourceCommand) Respon
} }
func UpdateDataSource(c *models.ReqContext, cmd models.UpdateDataSourceCommand) Response { func UpdateDataSource(c *models.ReqContext, cmd models.UpdateDataSourceCommand) Response {
datasourcesLogger.Debug("Received command to update data source", "url", cmd.Url)
cmd.OrgId = c.OrgId cmd.OrgId = c.OrgId
cmd.Id = c.ParamsInt64(":id") cmd.Id = c.ParamsInt64(":id")
if resp := validateURL(cmd.Url); resp != nil { if resp := validateURL(cmd.Type, cmd.Url); resp != nil {
return resp return resp
} }
......
...@@ -73,7 +73,7 @@ func (lw *logWrapper) Write(p []byte) (n int, err error) { ...@@ -73,7 +73,7 @@ func (lw *logWrapper) Write(p []byte) (n int, err error) {
// NewDataSourceProxy creates a new Datasource proxy // NewDataSourceProxy creates a new Datasource proxy
func NewDataSourceProxy(ds *models.DataSource, plugin *plugins.DataSourcePlugin, ctx *models.ReqContext, func NewDataSourceProxy(ds *models.DataSource, plugin *plugins.DataSourcePlugin, ctx *models.ReqContext,
proxyPath string, cfg *setting.Cfg) (*DataSourceProxy, error) { proxyPath string, cfg *setting.Cfg) (*DataSourceProxy, error) {
targetURL, err := datasource.ValidateURL(ds.Url) targetURL, err := datasource.ValidateURL(ds.Type, ds.Url)
if err != nil { if err != nil {
return nil, err return nil, err
} }
......
...@@ -11,6 +11,7 @@ import ( ...@@ -11,6 +11,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/grafana/grafana/pkg/api/datasource"
"github.com/grafana/grafana/pkg/components/securejsondata" "github.com/grafana/grafana/pkg/components/securejsondata"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
...@@ -583,11 +584,62 @@ func TestNewDataSourceProxy_ProtocolLessURL(t *testing.T) { ...@@ -583,11 +584,62 @@ func TestNewDataSourceProxy_ProtocolLessURL(t *testing.T) {
} }
cfg := setting.Cfg{} cfg := setting.Cfg{}
plugin := plugins.DataSourcePlugin{} plugin := plugins.DataSourcePlugin{}
_, err := NewDataSourceProxy(&ds, &plugin, &ctx, "api/method", &cfg) _, err := NewDataSourceProxy(&ds, &plugin, &ctx, "api/method", &cfg)
require.NoError(t, err) require.NoError(t, err)
} }
// Test wth MSSQL type data sources.
func TestNewDataSourceProxy_MSSQL(t *testing.T) {
ctx := models.ReqContext{
Context: &macaron.Context{
Req: macaron.Request{},
},
SignedInUser: &models.SignedInUser{OrgRole: models.ROLE_EDITOR},
}
tcs := []struct {
description string
url string
err error
}{
{
description: "Valid ODBC URL",
url: `localhost\instance:1433`,
},
{
description: "Invalid ODBC URL",
url: `localhost\instance::1433`,
err: datasource.URLValidationError{
Err: fmt.Errorf(`unrecognized MSSQL URL format: "localhost\\instance::1433"`),
URL: `localhost\instance::1433`,
},
},
}
for _, tc := range tcs {
t.Run(tc.description, func(t *testing.T) {
cfg := setting.Cfg{}
plugin := plugins.DataSourcePlugin{}
ds := models.DataSource{
Type: "mssql",
Url: tc.url,
}
p, err := NewDataSourceProxy(&ds, &plugin, &ctx, "api/method", &cfg)
if tc.err == nil {
require.NoError(t, err)
assert.Equal(t, &url.URL{
Scheme: "sqlserver",
Host: ds.Url,
}, p.targetUrl)
} else {
require.Error(t, err)
assert.Equal(t, tc.err, err)
}
})
}
}
type CloseNotifierResponseRecorder struct { type CloseNotifierResponseRecorder struct {
*httptest.ResponseRecorder *httptest.ResponseRecorder
closeChan chan bool closeChan chan bool
......
...@@ -3,17 +3,18 @@ package mssql ...@@ -3,17 +3,18 @@ package mssql
import ( import (
"database/sql" "database/sql"
"fmt" "fmt"
"net/url"
"regexp"
"strconv" "strconv"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
_ "github.com/denisenkom/go-mssqldb" _ "github.com/denisenkom/go-mssqldb"
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/tsdb" "github.com/grafana/grafana/pkg/tsdb"
"github.com/grafana/grafana/pkg/tsdb/sqleng" "github.com/grafana/grafana/pkg/tsdb/sqleng"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/util/errutil"
"xorm.io/core" "xorm.io/core"
) )
...@@ -21,9 +22,9 @@ func init() { ...@@ -21,9 +22,9 @@ func init() {
tsdb.RegisterTsdbQueryEndpoint("mssql", newMssqlQueryEndpoint) tsdb.RegisterTsdbQueryEndpoint("mssql", newMssqlQueryEndpoint)
} }
func newMssqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, error) { var logger = log.New("tsdb.mssql")
logger := log.New("tsdb.mssql")
func newMssqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, error) {
cnnstr, err := generateConnectionString(datasource) cnnstr, err := generateConnectionString(datasource)
if err != nil { if err != nil {
return nil, err return nil, err
...@@ -46,12 +47,46 @@ func newMssqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoin ...@@ -46,12 +47,46 @@ func newMssqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoin
return sqleng.NewSqlQueryEndpoint(&config, &queryResultTransformer, newMssqlMacroEngine(), logger) return sqleng.NewSqlQueryEndpoint(&config, &queryResultTransformer, newMssqlMacroEngine(), logger)
} }
// ParseURL tries to parse an MSSQL URL string into a URL object.
func ParseURL(u string) (*url.URL, error) {
logger.Debug("Parsing MSSQL URL", "url", u)
// Recognize ODBC connection strings like host\instance:1234
reODBC := regexp.MustCompile(`^[^\\:]+(?:\\[^:]+)?(?::\d+)?$`)
var host string
switch {
case reODBC.MatchString(u):
logger.Debug("Recognized as ODBC URL format", "url", u)
host = u
default:
logger.Debug("Couldn't recognize as valid MSSQL URL", "url", u)
return nil, fmt.Errorf("unrecognized MSSQL URL format: %q", u)
}
return &url.URL{
Scheme: "sqlserver",
Host: host,
}, nil
}
func generateConnectionString(datasource *models.DataSource) (string, error) { func generateConnectionString(datasource *models.DataSource) (string, error) {
addr, err := util.SplitHostPortDefault(datasource.Url, "localhost", "1433") var addr util.NetworkAddress
if err != nil { if datasource.Url != "" {
return "", errutil.Wrapf(err, "Invalid data source URL '%s'", datasource.Url) u, err := ParseURL(datasource.Url)
if err != nil {
return "", err
}
addr, err = util.SplitHostPortDefault(u.Host, "localhost", "1433")
if err != nil {
return "", err
}
} else {
addr = util.NetworkAddress{
Host: "localhost",
Port: "1433",
}
} }
logger.Debug("Generating connection string", "url", datasource.Url, "host", addr.Host, "port", addr.Port)
encrypt := datasource.JsonData.Get("encrypt").MustString("false") encrypt := datasource.JsonData.Get("encrypt").MustString("false")
connStr := fmt.Sprintf("server=%s;port=%s;database=%s;user id=%s;password=%s;", connStr := fmt.Sprintf("server=%s;port=%s;database=%s;user id=%s;password=%s;",
addr.Host, addr.Host,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment