Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,12 @@ darwin-arm64/sqlcmd
linux-amd64/sqlcmd
linux-arm64/sqlcmd
linux-s390x/sqlcmd

# Build artifacts in root
/sqlcmd
/sqlcmd_binary

# certificates used for local testing
*.der
*.pem
*.pfx
17 changes: 17 additions & 0 deletions cmd/sqlcmd/sqlcmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ type SQLCmdArguments struct {
ApplicationIntent string
EncryptConnection string
HostNameInCertificate string
ServerCertificate string
DriverLoggingLevel int
ExitOnError bool
ErrorSeverityLevel uint8
Expand Down Expand Up @@ -127,6 +128,15 @@ const (
removeControlCharacters = "remove-control-characters"
)

func encryptConnectionAllowsTLS(value string) bool {
switch strings.ToLower(value) {
case "s", "strict", "m", "mandatory", "true", "t", "yes", "1":
return true
default:
return false
}
}

// Validate arguments for settings not describe
func (a *SQLCmdArguments) Validate(c *cobra.Command) (err error) {
if a.ListServers != "" {
Expand All @@ -144,6 +154,8 @@ func (a *SQLCmdArguments) Validate(c *cobra.Command) (err error) {
err = mutuallyExclusiveError("-E", `-U/-P`)
case a.UseAad && len(a.AuthenticationMethod) > 0:
err = mutuallyExclusiveError("-G", "--authentication-method")
case len(a.HostNameInCertificate) > 0 && len(a.ServerCertificate) > 0:
err = mutuallyExclusiveError("-F", "-J")
Copy link

@dlevy-msft-sql dlevy-msft-sql Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is -F here still Format? #Resolved

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hostnameincertificate

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I look at sqlcmd -? it has

-F,--format
   Specifies the formatting for results

case a.PacketSize != 0 && (a.PacketSize < 512 || a.PacketSize > 32767):
err = localizer.Errorf(`'-a %#v': Packet size has to be a number between 512 and 32767.`, a.PacketSize)
// Ignore 0 even though it's technically an invalid input
Expand All @@ -157,6 +169,8 @@ func (a *SQLCmdArguments) Validate(c *cobra.Command) (err error) {
err = rangeParameterError("-y", fmt.Sprint(*a.VariableTypeWidth), 0, 8000, true)
case a.QueryTimeout < 0 || a.QueryTimeout > 65534:
err = rangeParameterError("-t", fmt.Sprint(a.QueryTimeout), 0, 65534, true)
case a.ServerCertificate != "" && !encryptConnectionAllowsTLS(a.EncryptConnection):
err = localizer.Errorf("The -J parameter requires encryption to be enabled (-N true, -N mandatory, or -N strict).")
}
}
if err != nil {
Expand Down Expand Up @@ -429,6 +443,8 @@ func setFlags(rootCmd *cobra.Command, args *SQLCmdArguments) {
rootCmd.Flags().StringVarP(&args.ApplicationIntent, applicationIntent, "K", "default", localizer.Sprintf("Declares the application workload type when connecting to a server. The only currently supported value is ReadOnly. If %s is not specified, the sqlcmd utility will not support connectivity to a secondary replica in an Always On availability group", localizer.ApplicationIntentFlagShort))
rootCmd.Flags().StringVarP(&args.EncryptConnection, encryptConnection, "N", "default", localizer.Sprintf("This switch is used by the client to request an encrypted connection"))
rootCmd.Flags().StringVarP(&args.HostNameInCertificate, "host-name-in-certificate", "F", "", localizer.Sprintf("Specifies the host name in the server certificate."))
rootCmd.Flags().StringVarP(&args.ServerCertificate, "server-certificate", "J", "", localizer.Sprintf("Specifies the path to a server certificate file (PEM, DER, or CER) to match against the server's TLS certificate. Use when encryption is enabled (-N true, -N mandatory, or -N strict) for certificate pinning instead of standard certificate validation."))
rootCmd.MarkFlagsMutuallyExclusive("host-name-in-certificate", "server-certificate")
// Can't use NoOptDefVal until this fix: https://github.com/spf13/cobra/issues/866
//rootCmd.Flags().Lookup(encryptConnection).NoOptDefVal = "true"
rootCmd.Flags().BoolVarP(&args.Vertical, "vertical", "", false, localizer.Sprintf("Prints the output in vertical format. This option sets the sqlcmd scripting variable %s to '%s'. The default is false", sqlcmd.SQLCMDFORMAT, "vert"))
Expand Down Expand Up @@ -721,6 +737,7 @@ func setConnect(connect *sqlcmd.ConnectSettings, args *SQLCmdArguments, vars *sq
connect.Encrypt = args.EncryptConnection
}
connect.HostNameInCertificate = args.HostNameInCertificate
connect.ServerCertificate = args.ServerCertificate
connect.PacketSize = args.PacketSize
connect.WorkstationName = args.WorkstationName
connect.LogLevel = args.DriverLoggingLevel
Expand Down
18 changes: 17 additions & 1 deletion cmd/sqlcmd/sqlcmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,18 @@ func TestValidCommandLineToArgsConversion(t *testing.T) {
{[]string{"-N", "s", "-F", "myserver.domain.com"}, func(args SQLCmdArguments) bool {
return args.EncryptConnection == "s" && args.HostNameInCertificate == "myserver.domain.com"
}},
{[]string{"-N", "s", "-J", "/path/to/cert.pem"}, func(args SQLCmdArguments) bool {
return args.EncryptConnection == "s" && args.ServerCertificate == "/path/to/cert.pem"
}},
{[]string{"-N", "strict", "-J", "/path/to/cert.der"}, func(args SQLCmdArguments) bool {
return args.EncryptConnection == "strict" && args.ServerCertificate == "/path/to/cert.der"
}},
{[]string{"-N", "m", "-J", "/path/to/cert.cer"}, func(args SQLCmdArguments) bool {
return args.EncryptConnection == "m" && args.ServerCertificate == "/path/to/cert.cer"
}},
{[]string{"-N", "true", "-J", "/path/to/cert2.pem"}, func(args SQLCmdArguments) bool {
return args.EncryptConnection == "true" && args.ServerCertificate == "/path/to/cert2.pem"
}},
}

for _, test := range commands {
Expand Down Expand Up @@ -154,14 +166,18 @@ func TestInvalidCommandLine(t *testing.T) {
{[]string{"-E", "-U", "someuser"}, "The -E and the -U/-P options are mutually exclusive."},
{[]string{"-L", "-q", `"select 1"`}, "The -L parameter can not be used in combination with other parameters."},
{[]string{"-i", "foo.sql", "-q", `"select 1"`}, "The i and the -Q/-q options are mutually exclusive."},
{[]string{"-r", "5"}, `'-r 5': Unexpected argument. Argument value has to be one of [0 1].`},
{[]string{"-r", "5"}, "'-r 5': Unexpected argument. Argument value has to be one of [0 1]."},
{[]string{"-w", "x"}, "'-w x': value must be greater than 8 and less than 65536."},
{[]string{"-y", "111111"}, "'-y 111111': value must be greater than or equal to 0 and less than or equal to 8000."},
{[]string{"-Y", "-2"}, "'-Y -2': value must be greater than or equal to 0 and less than or equal to 8000."},
{[]string{"-P"}, "'-P': Missing argument. Enter '-?' for help."},
{[]string{"-;"}, "';': Unknown Option. Enter '-?' for help."},
{[]string{"-t", "-2"}, "'-t -2': value must be greater than or equal to 0 and less than or equal to 65534."},
{[]string{"-N", "invalid"}, "'-N invalid': Unexpected argument. Argument value has to be one of [m[andatory] yes 1 t[rue] disable o[ptional] no 0 f[alse] s[trict]]."},
{[]string{"-J", "/path/to/cert.pem"}, "The -J parameter requires encryption to be enabled (-N true, -N mandatory, or -N strict)."},
{[]string{"-N", "optional", "-J", "/path/to/cert.pem"}, "The -J parameter requires encryption to be enabled (-N true, -N mandatory, or -N strict)."},
{[]string{"-N", "disable", "-J", "/path/to/cert.pem"}, "The -J parameter requires encryption to be enabled (-N true, -N mandatory, or -N strict)."},
{[]string{"-N", "strict", "-F", "myserver.domain.com", "-J", "/path/to/cert.pem"}, "The -F and the -J options are mutually exclusive."},
}

for _, test := range commands {
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ require (
github.com/docker/go-connections v0.4.0
github.com/golang-sql/sqlexp v0.1.0
github.com/google/uuid v1.6.0
github.com/microsoft/go-mssqldb v1.9.2
github.com/microsoft/go-mssqldb v1.9.6
github.com/opencontainers/image-spec v1.0.2
github.com/peterh/liner v1.2.2
github.com/pkg/errors v0.9.1
Expand Down Expand Up @@ -72,6 +72,7 @@ require (
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.26.0 // indirect
github.com/prometheus/procfs v0.6.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/spf13/afero v1.9.2 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -257,8 +257,8 @@ github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8Bz
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/microsoft/go-mssqldb v1.9.2 h1:nY8TmFMQOHpm2qVWo6y4I2mAmVdZqlGiMGAYt64Ibbs=
github.com/microsoft/go-mssqldb v1.9.2/go.mod h1:GBbW9ASTiDC+mpgWDGKdm3FnFLTUsLYN3iFL90lQ+PA=
github.com/microsoft/go-mssqldb v1.9.6 h1:1MNQg5UiSsokiPz3++K2KPx4moKrwIqly1wv+RyCKTw=
github.com/microsoft/go-mssqldb v1.9.6/go.mod h1:yYMPDufyoF2vVuVCUGtZARr06DKFIhMrluTcgWlXpr4=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
Expand Down Expand Up @@ -320,6 +320,8 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
Expand Down
5 changes: 5 additions & 0 deletions pkg/sqlcmd/connect.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ type ConnectSettings struct {
ChangePassword string
// The HostNameInCertificate is the name to use for the host in the certificate validation
HostNameInCertificate string
// ServerCertificate is the path to a certificate file to match against the server's TLS certificate
ServerCertificate string
}

func (c ConnectSettings) authenticationMethod() string {
Expand Down Expand Up @@ -150,6 +152,9 @@ func (connect ConnectSettings) ConnectionString() (connectionString string, err
if connect.HostNameInCertificate != "" {
query.Add(msdsn.HostNameInCertificate, connect.HostNameInCertificate)
}
if connect.ServerCertificate != "" {
query.Add(msdsn.ServerCertificate, connect.ServerCertificate)
}
if connect.LogLevel > 0 {
query.Add(msdsn.LogParam, fmt.Sprint(connect.LogLevel))
}
Expand Down
Loading