github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/cli/client_url.go (about) 1 // Copyright 2018 The Cockroach Authors. 2 // 3 // Use of this software is governed by the Business Source License 4 // included in the file licenses/BSL.txt. 5 // 6 // As of the Change Date specified in that file, in accordance with 7 // the Business Source License, use of this software will be governed 8 // by the Apache License, Version 2.0, included in the file 9 // licenses/APL.txt. 10 11 package cli 12 13 import ( 14 "fmt" 15 "net" 16 "net/url" 17 "os" 18 "path/filepath" 19 "strings" 20 21 "github.com/cockroachdb/cockroach/pkg/cli/cliflags" 22 "github.com/cockroachdb/cockroach/pkg/security" 23 "github.com/cockroachdb/errors" 24 "github.com/spf13/cobra" 25 ) 26 27 // This file implements the parsing of the client --url flag. 28 // 29 // This aims to offer consistent UX between uses the "combined" --url 30 // flag and the "discrete" separate flags --host / --port / etc. 31 // 32 // The flow of data between flags, configuration variables and usage 33 // by client commands goes as follows: 34 // 35 // flags parser 36 // / \ 37 // .-----' `-------. 38 // | | 39 // --url --host, --port, etc 40 // | | 41 // | | 42 // urlParser.Set() | 43 // (this file) | 44 // | | 45 // `-------. .-------' 46 // \ / 47 // sqlCtx/cliCtx/baseCtx 48 // | 49 // / \ 50 // .-------' `--------. 51 // | | 52 // | | 53 // non-SQL makeClientConnURL() 54 // commands (this file) 55 // (quit, init, etc) | 56 // SQL commands 57 // (user, zone, etc) 58 // 59 60 type urlParser struct { 61 cmd *cobra.Command 62 cliCtx *cliContext 63 64 // sslStrict, when set to true, requires that the SSL file paths in 65 // a URL clearly map to a certificate directory and restricts the 66 // set of supported SSL modes to just "disable" and "require". 67 // 68 // This is set for all non-SQL client commands, which only support 69 // the insecure boolean and certs-dir with maximum SSL validation. 70 sslStrict bool 71 } 72 73 func (u urlParser) String() string { return "" } 74 75 func (u urlParser) Type() string { 76 return "postgresql://[user[:passwd]@]host[:port]/[db][?parameters...]" 77 } 78 79 func (u urlParser) Set(v string) error { 80 return u.setInternal(v, true /* warn */) 81 } 82 83 func (u urlParser) setInternal(v string, warn bool) error { 84 parsedURL, err := url.Parse(v) 85 if err != nil { 86 return err 87 } 88 89 // General URL format compatibility check. 90 // 91 // The canonical PostgreSQL URL scheme is "postgresql", however our 92 // own client commands also accept "postgres" which is the scheme 93 // registered/supported by lib/pq. Internally, lib/pq supports 94 // both. 95 if parsedURL.Scheme != "postgresql" && parsedURL.Scheme != "postgres" { 96 return fmt.Errorf(`URL scheme must be "postgresql", not "%s"`, parsedURL.Scheme) 97 } 98 99 if parsedURL.Opaque != "" { 100 return fmt.Errorf("unknown URL format: %s", v) 101 } 102 103 cliCtx := u.cliCtx 104 fl := flagSetForCmd(u.cmd) 105 106 // If user name / password information is available, forward it to 107 // --user. We store the password for later re-collection by 108 // makeClientConnURL(). 109 if parsedURL.User != nil { 110 f := fl.Lookup(cliflags.User.Name) 111 if f == nil { 112 // A client which does not support --user will also not use 113 // makeClientConnURL(), so we can ignore/forget about the 114 // information. We do not produce an error however, so that a 115 // user can readily copy-paste the URL produced by `cockroach 116 // start` even if the client command does not accept a username. 117 if warn { 118 fmt.Fprintf(stderr, 119 "warning: --url specifies user/password, but command %q does not accept user/password details - details ignored\n", 120 u.cmd.Name()) 121 } 122 } else { 123 if err := f.Value.Set(parsedURL.User.Username()); err != nil { 124 return errors.Wrapf(err, "extracting user") 125 } 126 if pw, pwSet := parsedURL.User.Password(); pwSet { 127 cliCtx.sqlConnPasswd = pw 128 } 129 } 130 } 131 132 // If some host/port information is available, forward it to 133 // --host / --port. 134 if parsedURL.Host != "" { 135 prevHost, prevPort := cliCtx.clientConnHost, cliCtx.clientConnPort 136 if err := u.cmd.Flags().Set(cliflags.ClientHost.Name, parsedURL.Host); err != nil { 137 return errors.Wrapf(err, "extracting host/port") 138 } 139 // Fill in previously set values for each component that wasn't specified. 140 if cliCtx.clientConnHost == "" { 141 cliCtx.clientConnHost = prevHost 142 } 143 if cliCtx.clientConnPort == "" { 144 cliCtx.clientConnPort = prevPort 145 } 146 } 147 148 // If a database path is available, forward it to --database. 149 if parsedURL.Path != "" { 150 dbPath := strings.TrimLeft(parsedURL.Path, "/") 151 f := fl.Lookup(cliflags.Database.Name) 152 if f == nil { 153 // A client which does not support --database does not need this 154 // bit of information, so we can ignore/forget about it. We do 155 // not produce an error however, so that a user can readily 156 // copy-paste an URL they picked up from another tool (a GUI 157 // tool for example). 158 if warn { 159 fmt.Fprintf(stderr, 160 "warning: --url specifies database %q, but command %q does not accept a database name - database name ignored\n", 161 dbPath, u.cmd.Name()) 162 } 163 } else { 164 if err := f.Value.Set(dbPath); err != nil { 165 return errors.Wrapf(err, "extracting database name") 166 } 167 } 168 } 169 170 // If some query options are available, try to decompose/capture as 171 // much as possible. Anything not decomposed will be accumulated in 172 // cliCtx.extraConnURLOptions. 173 if parsedURL.RawQuery != "" { 174 options, err := url.ParseQuery(parsedURL.RawQuery) 175 if err != nil { 176 return err 177 } 178 179 // If the URL specifies host/port as query args, we're having to do 180 // with a unix socket. In that case, we don't want to populate 181 // the host field in the URL. 182 if options.Get("host") != "" { 183 cliCtx.clientConnHost = "" 184 cliCtx.clientConnPort = "" 185 } 186 187 cliCtx.extraConnURLOptions = options 188 189 switch sslMode := options.Get("sslmode"); sslMode { 190 case "", "disable": 191 if err := fl.Set(cliflags.ClientInsecure.Name, "true"); err != nil { 192 return errors.Wrapf(err, "setting insecure connection based on --url") 193 } 194 case "require", "verify-ca", "verify-full": 195 if sslMode != "verify-full" && u.sslStrict { 196 return fmt.Errorf("command %q only supports sslmode=disable or sslmode=verify-full", u.cmd.Name()) 197 } 198 if err := fl.Set(cliflags.ClientInsecure.Name, "false"); err != nil { 199 return errors.Wrapf(err, "setting secure connection based on --url") 200 } 201 202 if u.sslStrict { 203 // The "sslStrict" flag means the client command is using our 204 // certificate manager instead of the certificate handler in 205 // lib/pq. 206 // 207 // Our certificate manager is peculiar in that it requires 208 // every file in the same directory (the "certs dir") and also 209 // the files to be named after a fixed naming convention. 210 // 211 // Meanwhile, the URL format for security flags consists 212 // of 3 options (sslrootcert, sslcert, sslkey) that *may* 213 // refer to arbitrary files in arbitrary directories. 214 // Regular SQL drivers are fine with that (including lib/pq) 215 // but our cert manager definitely not (or, at least, not yet). 216 // 217 // So here we have to reverse-engineer the parameters needed 218 // for the certificate manager from the URL and verify that 219 // they conform to the restrictions of our cert manager. There 220 // are three things that need to happen: 221 // 222 // - if the flag --certs-dir is not specified in the command 223 // line, we need to derive a path for the certificate 224 // directory from the URL options; our cert manager needs 225 // this as input. 226 // 227 // - we must verify that all 3 url options that determine 228 // files refer to the same directory; our cert manager does 229 // not know how to work otherwise. 230 // 231 // - we must also verify that the 3 options specify a file 232 // name that is compatible with our cert manager (namely, 233 // "ca.crt", "client.USERNAME.crt" and 234 // "client.USERNAME.key"). 235 // 236 237 candidateCertsDir := "" 238 foundCertsDir := false 239 if fl.Lookup(cliflags.CertsDir.Name).Changed { 240 // If a --certs-dir flag was preceding --url, we want to 241 // check that the paths inside the URL match the value of 242 // that explicit --certs-dir. 243 // 244 // If --certs-dir was not specified, we'll pick up 245 // the first directory encountered below. 246 candidateCertsDir = cliCtx.SSLCertsDir 247 candidateCertsDir = os.ExpandEnv(candidateCertsDir) 248 candidateCertsDir, err = filepath.Abs(candidateCertsDir) 249 if err != nil { 250 return err 251 } 252 } 253 254 // tryCertsDir digs into the SSL URL options to extract a valid 255 // certificate directory. It also checks that the file names are those 256 // expected by the certificate manager. 257 tryCertsDir := func(optName, expectedFilename string) error { 258 opt := options.Get(optName) 259 if opt == "" { 260 // Option not set: nothing to do. 261 return nil 262 } 263 264 // Check the expected base file name. 265 base := filepath.Base(opt) 266 if base != expectedFilename { 267 return fmt.Errorf("invalid file name for %q: expected %q, got %q", optName, expectedFilename, base) 268 } 269 270 // Extract the directory part. 271 dir := filepath.Dir(opt) 272 dir, err = filepath.Abs(dir) 273 if err != nil { 274 return err 275 } 276 if candidateCertsDir != "" { 277 // A certificate directory has already been found in a previous option; 278 // check that the new option uses the same. 279 if candidateCertsDir != dir { 280 return fmt.Errorf("non-homogeneous certificate directory: %s=%q, expected %q", optName, opt, candidateCertsDir) 281 } 282 } else { 283 // First time seeing a directory, remember it. 284 candidateCertsDir = dir 285 foundCertsDir = true 286 } 287 288 return nil 289 } 290 291 userName := security.RootUser 292 if cliCtx.sqlConnUser != "" { 293 userName = cliCtx.sqlConnUser 294 } 295 if err := tryCertsDir("sslrootcert", security.CACertFilename()); err != nil { 296 return err 297 } 298 if err := tryCertsDir("sslcert", security.ClientCertFilename(userName)); err != nil { 299 return err 300 } 301 if err := tryCertsDir("sslkey", security.ClientKeyFilename(userName)); err != nil { 302 return err 303 } 304 305 if foundCertsDir { 306 if err := fl.Set(cliflags.CertsDir.Name, candidateCertsDir); err != nil { 307 return errors.Wrapf(err, "extracting certificate directory") 308 } 309 } 310 } 311 default: 312 return fmt.Errorf( 313 "unsupported sslmode=%s (supported: disable, require, verify-ca, verify-full)", sslMode) 314 } 315 } 316 317 return nil 318 } 319 320 // makeClientConnURL constructs a connection URL from the parsed options. 321 // Do not call this function before command-line argument parsing has completed: 322 // this initializes the certificate manager with the configured --certs-dir. 323 func (cliCtx *cliContext) makeClientConnURL() (url.URL, error) { 324 netHost := "" 325 if cliCtx.clientConnHost != "" || cliCtx.clientConnPort != "" { 326 netHost = net.JoinHostPort(cliCtx.clientConnHost, cliCtx.clientConnPort) 327 } 328 pgurl := url.URL{ 329 Scheme: "postgresql", 330 Host: netHost, 331 Path: cliCtx.sqlConnDBName, 332 } 333 334 if cliCtx.sqlConnUser != "" { 335 if cliCtx.sqlConnPasswd != "" { 336 pgurl.User = url.UserPassword(cliCtx.sqlConnUser, cliCtx.sqlConnPasswd) 337 } else { 338 pgurl.User = url.User(cliCtx.sqlConnUser) 339 } 340 } 341 342 opts := url.Values{} 343 for k, v := range cliCtx.extraConnURLOptions { 344 opts[k] = v 345 } 346 347 if netHost != "" { 348 // Only add TLS parameters when using a network connection. 349 userName := cliCtx.sqlConnUser 350 if userName == "" { 351 userName = security.RootUser 352 } 353 if err := cliCtx.LoadSecurityOptions(opts, userName); err != nil { 354 return url.URL{}, err 355 } 356 } 357 358 pgurl.RawQuery = opts.Encode() 359 return pgurl, nil 360 }