diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 21b49631c..0a2bf40dd 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -169,6 +169,23 @@ jobs: run: | go test ./... --tags apitests + - name: Run CLI tests and Integration Tests + if: ${{ vars.ZITI_SKIP_CLI_TESTS != 'true' }} + timeout-minutes: 10 + shell: bash + run: | + mkdir cli_tests_bin + go build -o cli_tests_bin ./... + ZITI_CLI_TEST_ZITI_BIN="$PWD/cli_tests_bin/ziti" go test ./... --tags cli_tests + + - name: Upload CLI test logs + if: failure() + uses: actions/upload-artifact@v4 + with: + name: cli-test-logs-${{ github.run_id }} + path: '**/ctrl-std*log' + retention-days: 5 + fablab-smoketest: name: Fablab Smoketest # not applicable to forks. shouldn't run on release build diff --git a/CHANGELOG.md b/CHANGELOG.md index 48de497af..252a69295 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,174 @@ +# Release 1.8.0 + +## What's New + +* controllers can now optionally bind APIs using a OpenZiti identity +* `ziti edge login` now supports the `--network-identity` flag to authenticate and establish connections through the Ziti overlay network +* `ziti edge login` now supports using a bearer token with `--token` for authentication. The token is expected to be + provided as just the JWT, not with the "Bearer " prefix +* identity configuration can now be loaded from files or environment variables for flexible deployment scenarios + +## Binding Controller APIs With Identity + +Controller APIs can now be bound to an OpenZiti overlay network identity, allowing secure communication through +the Ziti network. This is useful for scenarios where you want to expose controller APIs only through the overlay +network rather than on a standard network interface. + +### Configuration Structure + +A standard `bindPoint` configuration looks like this: +```text + bindPoints: + - interface: 127.0.0.1:18441 + address: 127.0.0.1:18441 +``` + +To bind controller APIs to an OpenZiti identity, add an additional `identity` block to your `bindPoints`. The +identity configuration specifies where to load the Ziti identity file and which service to bind it to: + +```text + bindPoints: + - interface: 127.0.0.1:18441 + address: 127.0.0.1:18441 + - identity: + file: "c:/temp/ctrl.testing/ctrl.identity.json" + service: "mgmt" +``` + +### Supported Configuration Options + +- `file`: Path to a Ziti identity JSON file containing the controller's identity and enrollment certificate +- `env`: Name of an environment variable containing a base64-encoded Ziti identity (alternative to `file`) +- `service`: The name of the Ziti service to bind the controller API to + +### Using Environment Variables + +For deployments where storing identity files on disk is not preferred, you can reference a base64-encoded +identity file from an environment variable. The environment variable should contain the base64-encoded contents +of the identity JSON file. + +For example, if an environment variable named `ZITI_CTRL_IDENTITY` contains a base64-encoded identity file: + +```text + bindPoints: + - interface: 127.0.0.1:18441 + address: 127.0.0.1:18441 + - identity: + env: ZITI_CTRL_IDENTITY + service: "mgmt" +``` + +### IPv6 Support + +Both IPv4 and IPv6 addresses are supported for standard bind points. IPv6 addresses should be specified in bracket +notation with a port number: + +```text + bindPoints: + - interface: "[::1]:18441" + address: "[::1]:18441" + - identity: + file: "/path/to/identity.json" + service: "mgmt" +``` + +## CLI Enhancements for Identity-Based Connections + +The `ziti edge login` command and REST client utilities have been enhanced to support identity-based connections +through the Ziti overlay network. + +### New `--network-identity` Flag for `ziti edge login` + +The `ziti edge login` command now includes a `--network-identity` flag that allows you to authenticate to a Ziti +controller through the overlay network using a Ziti identity: + +```bash +ziti edge login https://ziti.mgmt.apis.local:1280 \ + --username myuser \ + --password mypass \ + --network-identity /path/to/identity.json +``` + +This is useful when the controller is only accessible through the Ziti overlay network or when you want to ensure +all communication to the controller flows through the overlay for security purposes. + +### Identity Resolution Order + +When establishing connections, identities are resolved in the following order: + +1. **Command-line flag**: The `--network-identity` flag takes precedence +2. **Environment variable**: If `ZITI_CLI_NETWORK_ID` is set and contains a base64-encoded identity, it is used +3. **Cached identity file**: If a network identity was saved from a previous login in the Ziti config directory, it may be used + +This layered approach allows for flexibility in deployment scenarios: +- Development: Use command-line flags for quick testing +- Automation: Use environment variables in CI/CD pipelines +- Production: Cache identities securely for repeated access + +#### Dialing Modes When Authenticating + +The CLI supports two dialing modes: + +**Intercept-based Dialing (Default)** +By default, URLs are expected to leverage intercepts. Create a service with an appropriate intercept config and use +the intercept address when dialing. This is the standard mode for most use cases. For example, given a service with +the intercept `ziti.mgmt.apis.local` +```bash +ziti edge login https://ziti.mgmt.apis.local:1280 \ + --username myuser \ + --password mypass \ + --network-identity /path/to/identity.json +``` + +**Identity-aware Dialing (Addressable Terminators)** +To support addressable terminators-based dialing, specify a user in the URL. This activates dial-by-identity +functionality. The URL format should be `identity-to-dial@service-name-to-dial`. For example: +```bash +ziti edge login https://my-identity@my-service:1280 \ + --username myuser \ + --password mypass \ + --network-identity /path/to/identity.json +``` + +In this mode, the transport extracts the identity from the URL and uses it to establish a direct connection to +the specified service via the addressable terminator. + +## What's New + +* controllers can now optionally bind APIs using a OpenZiti identity + +## Binding Controller APIs With Identity + +It's now possible to bind controller APIs to an OpenZiti overlay network identity. To bind a given controller +API to an OpenZiti identity, add a section to the desired `bindPoint` section. For example a common `bindPoint` +configuration might look like: +```text + bindPoints: + - interface: 127.0.0.1:18441 + address: 127.0.0.1:18441 +``` +To bind any declared APIs to a given OpenZiti identity add an `identity` block: +```text + bindPoints: + - interface: 127.0.0.1:18441 + address: 127.0.0.1:18441 + - identity: + file: "c:/temp/ctrl.testing/clint.ctrl.json" + service: "mgmt" +``` +It's possible to refer to an environment variable for the identity file if desired. Add an environment variable with +the contents of the environment variable the identity file base64 encoded. For example if an environment is defined +with the name `ZITI_ID_EXAMPLE` and contains a base64 encoded identity file, the following `bindPoint` block can be used: +```text + bindPoints: + - interface: 127.0.0.1:18441 + address: 127.0.0.1:18441 + - identity: + env: ZITI_ID_EXAMPLE + service: "mgmt" +``` + + # Release 1.7.2 ## What's New @@ -138,7 +309,7 @@ Added support for dynamic service proxies with configurable binding and protocol This allows Edge Routers and Tunnelers to create proxy endpoints that can forward traffic for Ziti services. This differs from intercept.v1 in that intercept.v1 will intercept traffic on specified -IP ip addresses or DNS entries to forward to a service using tproxy or tun interface, +IP addresses or DNS entries to forward to a service using tproxy or tun interface, depending on implementation. A proxy on the other hand will just start a regular TCP/UDP listener on the configured port, diff --git a/common/health/handler.go b/common/health/handler.go index 62f28cbb7..7bebdaec7 100644 --- a/common/health/handler.go +++ b/common/health/handler.go @@ -20,7 +20,7 @@ import ( "encoding/json" "fmt" gosundheit "github.com/AppsFlyer/go-sundheit" - "github.com/openziti/xweb/v2" + "github.com/openziti/xweb/v3" "github.com/sirupsen/logrus" "net/http" "strings" diff --git a/controller/bindpoints/bindpoints.go b/controller/bindpoints/bindpoints.go new file mode 100644 index 000000000..f9d87613d --- /dev/null +++ b/controller/bindpoints/bindpoints.go @@ -0,0 +1,36 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package bindpoints + +import ( + "github.com/openziti/xweb/v3" +) + +// BindPointListenerFactory implements the xweb.BindPointListenerFactory. +// It provides a factory that generates xweb.BindPoints based on the provided config section +type BindPointListenerFactory struct { +} + +// New checks to see if this bindPoint is for overlay or underlay then calls the expected func. +// As of now there are only two types, OverlayBindPoint and UnderlayBindPoint +func (c *BindPointListenerFactory) New(conf map[interface{}]interface{}) (xweb.BindPoint, error) { + if conf["identity"] != nil { + return newOverlayBindPoint(conf) + } else { // only two options right now. underlay and overlay... + return newUnderlayBindPoint(conf) + } +} diff --git a/controller/bindpoints/overlay.go b/controller/bindpoints/overlay.go new file mode 100644 index 000000000..e95ae1d21 --- /dev/null +++ b/controller/bindpoints/overlay.go @@ -0,0 +1,156 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package bindpoints + +import ( + "crypto/tls" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "net" + "net/http" + "os" + "strings" + + "github.com/michaelquigley/pfxlog" + "github.com/openziti/identity" + "github.com/openziti/sdk-golang/ziti" + "github.com/openziti/xweb/v3" +) + +var _ xweb.BindPoint = (*OverlayBindPoint)(nil) + +// OverlayBindPoint represents the BindPointConfig when an identity is supplied as opposed to an address +type OverlayBindPoint struct { + Identity []byte //an openziti identity + Service string //name of the service to bind + ClientAuthType tls.ClientAuthType + Name string + Opts ziti.ListenOptions + cfg ziti.Config + ctx ziti.Context +} + +func (o OverlayBindPoint) BeforeHandler(next http.Handler) http.Handler { + return next +} +func (o OverlayBindPoint) AfterHandler(prev http.Handler) http.Handler { + return prev +} +func (o OverlayBindPoint) ServerAddress() string { + return o.Name +} +func newOverlayBindPoint(conf map[interface{}]interface{}) (OverlayBindPoint, error) { + o := OverlayBindPoint{} + + identVal, ok := conf["identity"] + if !ok { + return o, errors.New("missing identity section") + } + + identCfg, ok := identVal.(map[interface{}]interface{}) + if !ok { + return o, errors.New("identity config must be a map") + } + + if fileVal, ok := identCfg["file"].(string); ok { + data, err := os.ReadFile(fileVal) + if err != nil { + return o, err + } + o.Identity = data + } + + if envName, ok := identCfg["env"].(string); ok { + b64Id := os.Getenv(envName) + idReader := base64.NewDecoder(base64.StdEncoding, strings.NewReader(b64Id)) + data, err := io.ReadAll(idReader) + if err != nil { + return o, err + } + o.Identity = data + } + + if len(o.Identity) < 1 { + return o, errors.New("no identity configured: file or env required") + } + + if svc, ok := identCfg["service"].(string); ok { + o.Service = svc + } else { + return o, errors.New("service must be supplied when using an identity binding") + } + + if certRequired, ok := identCfg["tlsClientAuthenticationPolicy"].(string); ok { + switch strings.ToLower(certRequired) { + case "noclientcert": + o.ClientAuthType = tls.NoClientCert + case "requestclientcert": + o.ClientAuthType = tls.RequestClientCert + case "requireanyclientcert": + o.ClientAuthType = tls.RequireAnyClientCert + case "verifyclientcertifgiven": + o.ClientAuthType = tls.VerifyClientCertIfGiven + case "requireandverifyclientcert": + o.ClientAuthType = tls.RequireAndVerifyClientCert + default: + o.ClientAuthType = tls.VerifyClientCertIfGiven + } + } + if listenOptsCfg, ok := identCfg["listenOptions"]; ok { + optsCfg := listenOptsCfg.(map[interface{}]interface{}) + if asId, ok := optsCfg["bindUsingEdgeIdentity"].(bool); ok { + o.Opts.BindUsingEdgeIdentity = asId + } + } + + o.cfg = ziti.Config{} + unMarshallErr := json.Unmarshal(o.Identity, &o.cfg) + if unMarshallErr != nil { + return o, unMarshallErr + } + + if ctx, newCtxErr := ziti.NewContext(&o.cfg); newCtxErr != nil { + return o, newCtxErr + } else { + o.ctx = ctx + } + + return o, nil +} + +func (o OverlayBindPoint) Listener(_ string, tlsConfig *tls.Config) (net.Listener, error) { + var ln net.Listener + + ln, err := o.ctx.ListenWithOptions(o.Service, &o.Opts) + if err != nil { + return nil, fmt.Errorf("error listening on overlay: %s", err) + } + + ln = tls.NewListener(ln, tlsConfig) + if tlsConfig.ClientAuth < tls.RequestClientCert { + pfxlog.Logger().WithError(err).Warnf("The configured certificate verification method [%d] will not support mutual TLS", tlsConfig.ClientAuth) + } + + return ln, nil +} + +func (o OverlayBindPoint) Validate(_ identity.Identity) error { + return nil // much of the validation happens before this func is invoked in newOverlayBindPoint +} diff --git a/controller/bindpoints/underlay.go b/controller/bindpoints/underlay.go new file mode 100644 index 000000000..be3d7c618 --- /dev/null +++ b/controller/bindpoints/underlay.go @@ -0,0 +1,170 @@ +/* + Copyright NetFoundry Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package bindpoints + +import ( + gotls "crypto/tls" + goerrs "errors" + "fmt" + "net" + "net/http" + "strconv" + "strings" + + "github.com/openziti/identity" + transporttls "github.com/openziti/transport/v2/tls" + "github.com/openziti/xweb/v3" + "github.com/pkg/errors" +) + +const ( + ZitiCtrlAddressHeader = "ziti-ctrl-address" +) + +// UnderlayBindPoint represents the interface:port address of where a http.Server should listen for a ServerConfig and the public +// address that should be used to address it. +type UnderlayBindPoint struct { + InterfaceAddress string //: + Address string //: + NewAddress string //: sent out as a header for clients to alternatively swap to (ip -> hostname moves) +} + +func (u UnderlayBindPoint) BeforeHandler(next http.Handler) http.Handler { + return u.wrapSetCtrlAddressHeader(next) +} +func (u UnderlayBindPoint) AfterHandler(prev http.Handler) http.Handler { + return prev +} +func (u UnderlayBindPoint) Listener(serverName string, tlsConfig *gotls.Config) (net.Listener, error) { + ln, err := transporttls.ListenTLS(u.InterfaceAddress, serverName, tlsConfig) + if err != nil { + return nil, fmt.Errorf("error listening: %s", err) + } + + return ln, nil +} + +func (u UnderlayBindPoint) ServerAddress() string { + return u.Address +} + +func newUnderlayBindPoint(conf map[interface{}]interface{}) (xweb.BindPoint, error) { + u := UnderlayBindPoint{} + if v, ok := conf["interface"].(string); ok { + u.InterfaceAddress = v + } + if v, ok := conf["address"].(string); ok { + u.Address = v + } + if v, ok := conf["newAddress"].(string); ok { + u.NewAddress = v + } + return u, nil +} + +// Validate this configuration object. +func (u UnderlayBindPoint) Validate(id identity.Identity) error { + var errs []error + + // required + if err := validateHostPort(u.InterfaceAddress); err != nil { + errs = append(errs, fmt.Errorf("invalid interface address [%s]: %v", u.InterfaceAddress, err)) + } + + // required + if err := validateHostPort(u.Address); err != nil { + errs = append(errs, fmt.Errorf("invalid advertise address [%s]: %v", u.Address, err)) + } + + //optional + if u.NewAddress != "" { + if err := validateHostPort(u.NewAddress); err != nil { + errs = append(errs, fmt.Errorf("invalid new address [%s]: %v", u.NewAddress, err)) + } + } + + if h, _, err := net.SplitHostPort(u.Address); err == nil { + if ve := id.ValidFor(normalizeIp(h)); ve != nil { + errs = append(errs, fmt.Errorf("address not valid %s: %v", u.Address, ve)) + } + } + + return goerrs.Join(errs...) +} + +func normalizeIp(s string) string { + s = strings.Trim(s, "[]") + var ip net.IP + if i := strings.IndexByte(s, '%'); i != -1 { + ip = net.ParseIP(s[:i]) + } else { + ip = net.ParseIP(s) + } + if ip == nil { + return s + } else { + return ip.String() + } +} + +// wrapSetCtrlAddressHeader will check to see if the bindPoint is configured to advertise a "new address". If so +// the value is added to the ZitiCtrlAddressHeader which will be sent out on every response. Clients can check this +// header to be notified that the controller is or will be moving from one ip/hostname to another. When the +// new address value is set, both the old and new addresses should be valid as the clients will begin using the +// new address on their next connect. +func (u UnderlayBindPoint) wrapSetCtrlAddressHeader(handler http.Handler) http.Handler { + wrappedHandler := http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + if u.NewAddress != "" { + address := "https://" + u.NewAddress + writer.Header().Set(ZitiCtrlAddressHeader, address) + } + + handler.ServeHTTP(writer, request) + }) + + return wrappedHandler +} + +func validateHostPort(address string) error { + address = strings.TrimSpace(address) + + if address == "" { + return errors.New("must not be an empty string or unspecified") + } + + host, port, err := net.SplitHostPort(address) + + if err != nil { + return errors.Errorf("could not split host and port: %v", err) + } + + if host == "" { + return errors.New("host must be specified") + } + + if port == "" { + return errors.New("port must be specified") + } + + if port, err := strconv.ParseInt(port, 10, 32); err != nil { + return errors.New("invalid port, must be a integer") + } else if port < 1 || port > 65535 { + return errors.New("invalid port, must 1-65535") + } + + return nil +} diff --git a/controller/controller.go b/controller/controller.go index 2836c7256..6d8240056 100644 --- a/controller/controller.go +++ b/controller/controller.go @@ -22,11 +22,9 @@ import ( cryptoTls "crypto/tls" "crypto/x509" "encoding/json" - stderr "errors" "fmt" "math/big" "os" - "strings" "sync" "sync/atomic" @@ -41,12 +39,13 @@ import ( "github.com/openziti/storage/boltz" "github.com/openziti/transport/v2" "github.com/openziti/transport/v2/tls" - "github.com/openziti/xweb/v2" + "github.com/openziti/xweb/v3" "github.com/openziti/ziti/common/capabilities" "github.com/openziti/ziti/common/concurrency" fabricMetrics "github.com/openziti/ziti/common/metrics" "github.com/openziti/ziti/common/pb/ctrl_pb" "github.com/openziti/ziti/common/profiler" + "github.com/openziti/ziti/controller/bindpoints" "github.com/openziti/ziti/controller/command" "github.com/openziti/ziti/controller/config" "github.com/openziti/ziti/controller/db" @@ -102,6 +101,10 @@ type Controller struct { healthChecker gosundheit.Health } +func init() { + xweb.BindPointListenerFactoryRegistry = append(xweb.BindPointListenerFactoryRegistry, &bindpoints.BindPointListenerFactory{}) +} + func (c *Controller) GetPeerSigners() []*x509.Certificate { if c.raftController == nil || c.raftController.Mesh == nil { return nil @@ -235,17 +238,6 @@ func NewController(cfg *config.Config, versionProvider versions.VersionProvider) xwebInitialized: concurrency.NewInitState(), } xwebInstanceOptions := xweb.InstanceOptions{ - InstanceValidators: []xweb.InstanceValidator{func(config *xweb.InstanceConfig) error { - var errs []error - for i, serverConfig := range config.ServerConfigs { - for _, bp := range serverConfig.BindPoints { - if ve := serverConfig.Identity.ValidFor(strings.Split(bp.Address, ":")[0]); ve != nil { - errs = append(errs, fmt.Errorf("could not validate server at %s[%d]: %v", config.Options.DefaultConfigSection, i, ve)) - } - } - } - return stderr.Join(errs...) - }}, DefaultIdentity: c.config.Id, DefaultIdentitySection: xweb.DefaultIdentitySection, DefaultConfigSection: xweb.DefaultConfigSection, @@ -844,8 +836,8 @@ func (c *Controller) GetApiAddresses() (map[string][]event.ApiAddress, []byte) { for _, bindPoint := range serverConfig.BindPoints { for _, api := range serverConfig.APIs { apiData[api.Binding()] = append(apiData[api.Binding()], event.ApiAddress{ - Url: "https://" + bindPoint.Address + getApiPath(api.Binding()), //TODO: temp till xweb support reporting API paths - Version: "v1", //TODO: temp till xweb supports reporting versions via api.Version() + Url: "https://" + bindPoint.ServerAddress() + getApiPath(api.Binding()), //TODO: temp till xweb support reporting API paths + Version: "v1", //TODO: temp till xweb supports reporting versions via api.Version() }) } } diff --git a/controller/env/appenv.go b/controller/env/appenv.go index 49a12f9e1..f0fe009c2 100644 --- a/controller/env/appenv.go +++ b/controller/env/appenv.go @@ -49,7 +49,7 @@ import ( "github.com/openziti/metrics" "github.com/openziti/sdk-golang/ziti" "github.com/openziti/storage/boltz" - "github.com/openziti/xweb/v2" + "github.com/openziti/xweb/v3" "github.com/openziti/ziti/common" "github.com/openziti/ziti/common/cert" "github.com/openziti/ziti/common/eid" diff --git a/controller/internal/routes/version_router.go b/controller/internal/routes/version_router.go index c91074907..003ac0999 100644 --- a/controller/internal/routes/version_router.go +++ b/controller/internal/routes/version_router.go @@ -25,7 +25,7 @@ import ( clientInformational "github.com/openziti/edge-api/rest_client_api_server/operations/informational" managementInformational "github.com/openziti/edge-api/rest_management_api_server/operations/informational" "github.com/openziti/edge-api/rest_model" - "github.com/openziti/xweb/v2" + "github.com/openziti/xweb/v3" "github.com/openziti/ziti/common/build" "github.com/openziti/ziti/controller/env" "github.com/openziti/ziti/controller/internal/permissions" @@ -115,7 +115,7 @@ func (ir *VersionRouter) List(ae *env.AppEnv, rc *response.RequestContext) { } for _, bindPoint := range webListener.BindPoints { - apiBaseUrl := bindPoint.Address + apiBindingToPath(api.Binding()) + apiBaseUrl := bindPoint.ServerAddress() + apiBindingToPath(api.Binding()) apiToBaseUrls[api.Binding()][apiBaseUrl] = struct{}{} } } diff --git a/controller/webapis/client-api.go b/controller/webapis/client-api.go index ac78dd969..f95bc1508 100644 --- a/controller/webapis/client-api.go +++ b/controller/webapis/client-api.go @@ -26,7 +26,7 @@ import ( "github.com/openziti/edge-api/rest_client_api_client" "github.com/openziti/edge-api/rest_client_api_server" "github.com/openziti/edge-api/rest_management_api_server" - "github.com/openziti/xweb/v2" + "github.com/openziti/xweb/v3" "github.com/openziti/ziti/controller/api" "github.com/openziti/ziti/controller/apierror" "github.com/openziti/ziti/controller/env" @@ -61,7 +61,7 @@ func (factory ClientApiFactory) Validate(config *xweb.InstanceConfig) error { if !clientApiFound && api.Binding() == ClientApiBinding { for _, bindPoint := range webListener.BindPoints { - if bindPoint.Address == edgeConfig.Api.Address { + if bindPoint.ServerAddress() == edgeConfig.Api.Address { factory.appEnv.SetClientApiDefaultCertificate(webListener.Identity.ServerCert()[0]) clientApiFound = true break diff --git a/controller/webapis/controller-health.go b/controller/webapis/controller-health.go index af80c02b9..a7c74c7cd 100644 --- a/controller/webapis/controller-health.go +++ b/controller/webapis/controller-health.go @@ -20,7 +20,7 @@ import ( "encoding/json" "fmt" gosundheit "github.com/AppsFlyer/go-sundheit" - "github.com/openziti/xweb/v2" + "github.com/openziti/xweb/v3" "github.com/openziti/ziti/controller/env" "github.com/sirupsen/logrus" "net/http" diff --git a/controller/webapis/fabric-management-api.go b/controller/webapis/fabric-management-api.go index fb035f2dc..51fa45e97 100644 --- a/controller/webapis/fabric-management-api.go +++ b/controller/webapis/fabric-management-api.go @@ -26,7 +26,7 @@ import ( "github.com/openziti/channel/v4/websockets" "github.com/openziti/foundation/v2/concurrenz" "github.com/openziti/identity" - "github.com/openziti/xweb/v2" + "github.com/openziti/xweb/v3" "github.com/openziti/ziti/controller/api_impl" "github.com/openziti/ziti/controller/env" "github.com/openziti/ziti/controller/handler_mgmt" diff --git a/controller/webapis/management-api.go b/controller/webapis/management-api.go index 2ef2b69a3..2f9452ffb 100644 --- a/controller/webapis/management-api.go +++ b/controller/webapis/management-api.go @@ -20,7 +20,7 @@ import ( "fmt" "github.com/openziti/edge-api/rest_management_api_client" "github.com/openziti/edge-api/rest_management_api_server" - "github.com/openziti/xweb/v2" + "github.com/openziti/xweb/v3" "github.com/openziti/ziti/controller/api" "github.com/openziti/ziti/controller/apierror" "github.com/openziti/ziti/controller/env" diff --git a/controller/webapis/metrics-api.go b/controller/webapis/metrics-api.go index 3691f09d6..3dcee9b2f 100644 --- a/controller/webapis/metrics-api.go +++ b/controller/webapis/metrics-api.go @@ -28,7 +28,7 @@ import ( "github.com/michaelquigley/pfxlog" "github.com/openziti/identity" - "github.com/openziti/xweb/v2" + "github.com/openziti/xweb/v3" "github.com/openziti/ziti/controller/api_impl" "github.com/openziti/ziti/controller/network" ) diff --git a/controller/webapis/oidc-api.go b/controller/webapis/oidc-api.go index 161842179..94b03d678 100644 --- a/controller/webapis/oidc-api.go +++ b/controller/webapis/oidc-api.go @@ -21,12 +21,13 @@ import ( "crypto/rand" "encoding/hex" "fmt" - "github.com/openziti/identity" "net" "net/http" "strings" - "github.com/openziti/xweb/v2" + "github.com/openziti/identity" + + "github.com/openziti/xweb/v3" "github.com/openziti/ziti/controller/api" "github.com/openziti/ziti/controller/env" "github.com/openziti/ziti/controller/oidc_auth" @@ -188,7 +189,7 @@ func NewOidcApiHandler(serverConfig *xweb.ServerConfig, ae *env.AppEnv, options // getPossibleIssuers inspects the API server's identity and bind points for addresses, SAN DNS, and SAN IP entries // that denote valid issuers. It returns a list of hostname:port combinations as a slice. It handles converting // :443 to explicit and implicit ports for clients that may silently remove :443 -func getPossibleIssuers(id identity.Identity, bindPoints []*xweb.BindPointConfig) []oidc_auth.Issuer { +func getPossibleIssuers(id identity.Identity, bindPoints []xweb.BindPoint) []oidc_auth.Issuer { const ( DefaultTlsPort = "443" ) @@ -200,7 +201,7 @@ func getPossibleIssuers(id identity.Identity, bindPoints []*xweb.BindPointConfig portMap := map[string]struct{}{} for _, bindPoint := range bindPoints { - host, port, err := net.SplitHostPort(bindPoint.Address) + host, port, err := net.SplitHostPort(bindPoint.ServerAddress()) if err != nil { continue @@ -211,7 +212,7 @@ func getPossibleIssuers(id identity.Identity, bindPoints []*xweb.BindPointConfig issuerMap[host] = struct{}{} } - issuerMap[bindPoint.Address] = struct{}{} + issuerMap[bindPoint.ServerAddress()] = struct{}{} } var ports []string diff --git a/controller/webapis/oidc-api_test.go b/controller/webapis/oidc-api_test.go index 6e810c382..ae7030f61 100644 --- a/controller/webapis/oidc-api_test.go +++ b/controller/webapis/oidc-api_test.go @@ -24,13 +24,15 @@ import ( "crypto/x509/pkix" "encoding/pem" "fmt" - "github.com/openziti/identity" - "github.com/openziti/xweb/v2" - "github.com/stretchr/testify/require" "math/big" "net" "testing" "time" + + "github.com/openziti/identity" + "github.com/openziti/xweb/v3" + "github.com/openziti/ziti/controller/bindpoints" + "github.com/stretchr/testify/require" ) func Test_getPossibleIssuers(t *testing.T) { @@ -132,11 +134,11 @@ func Test_getPossibleIssuers(t *testing.T) { bindPoint2Address = "test2.example.com:443" ) - bindPoints := []*xweb.BindPointConfig{ - { + bindPoints := []xweb.BindPoint{ + &bindpoints.UnderlayBindPoint{ Address: bindPoint1Address, }, - { + &bindpoints.UnderlayBindPoint{ Address: bindPoint2Address, }, } diff --git a/controller/webapis/zac.go b/controller/webapis/zac.go index 7566414bc..dd3af9345 100644 --- a/controller/webapis/zac.go +++ b/controller/webapis/zac.go @@ -18,7 +18,7 @@ package webapis import ( "fmt" - "github.com/openziti/xweb/v2" + "github.com/openziti/xweb/v3" log "github.com/sirupsen/logrus" "strings" ) diff --git a/go.mod b/go.mod index 0f0b63232..96844f8cb 100644 --- a/go.mod +++ b/go.mod @@ -24,15 +24,15 @@ require ( github.com/gaissmai/extnetip v1.2.0 github.com/go-acme/lego/v4 v4.25.2 github.com/go-jose/go-jose/v4 v4.1.3 - github.com/go-openapi/errors v0.22.4 + github.com/go-openapi/errors v0.22.3 github.com/go-openapi/jsonpointer v0.22.1 - github.com/go-openapi/loads v0.23.2 - github.com/go-openapi/runtime v0.29.2 - github.com/go-openapi/spec v0.22.1 - github.com/go-openapi/strfmt v0.25.0 + github.com/go-openapi/loads v0.23.1 + github.com/go-openapi/runtime v0.29.0 + github.com/go-openapi/spec v0.22.0 + github.com/go-openapi/strfmt v0.24.0 github.com/go-openapi/swag v0.25.1 github.com/go-openapi/swag/jsonutils v0.25.1 - github.com/go-openapi/validate v0.25.1 + github.com/go-openapi/validate v0.25.0 github.com/go-resty/resty/v2 v2.16.5 github.com/go-viper/mapstructure/v2 v2.4.0 github.com/golang-jwt/jwt/v5 v5.3.0 @@ -72,7 +72,7 @@ require ( github.com/openziti/storage v0.4.31 github.com/openziti/transport/v2 v2.0.198 github.com/openziti/x509-claims v1.0.3 - github.com/openziti/xweb/v2 v2.3.4 + github.com/openziti/xweb/v3 v3.0.1 github.com/openziti/ziti-db-explorer v1.1.3 github.com/orcaman/concurrent-map/v2 v2.0.1 github.com/pkg/errors v0.9.1 @@ -92,13 +92,14 @@ require ( go.etcd.io/bbolt v1.4.3 go.uber.org/atomic v1.11.0 go4.org v0.0.0-20180809161055-417644f6feb5 - golang.org/x/crypto v0.44.0 + golang.org/x/crypto v0.43.0 golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 - golang.org/x/net v0.47.0 - golang.org/x/oauth2 v0.33.0 - golang.org/x/sync v0.18.0 - golang.org/x/sys v0.38.0 - golang.org/x/text v0.31.0 + golang.org/x/net v0.46.0 + golang.org/x/oauth2 v0.32.0 + golang.org/x/sync v0.17.0 + golang.org/x/sys v0.37.0 + golang.org/x/term v0.36.0 + golang.org/x/text v0.30.0 google.golang.org/protobuf v1.36.10 gopkg.in/AlecAivazis/survey.v1 v1.8.8 gopkg.in/resty.v1 v1.12.0 @@ -116,6 +117,7 @@ require ( github.com/antchfx/xpath v1.3.2 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/armon/go-metrics v0.4.1 // indirect + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/biogo/store v0.0.0-20200525035639-8c94ae1e7c9c // indirect github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect github.com/boltdb/bolt v1.3.1 // indirect @@ -133,8 +135,8 @@ require ( github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect - github.com/go-openapi/analysis v0.24.1 // indirect - github.com/go-openapi/jsonreference v0.21.3 // indirect + github.com/go-openapi/analysis v0.24.0 // indirect + github.com/go-openapi/jsonreference v0.21.2 // indirect github.com/go-openapi/swag/cmdutils v0.25.1 // indirect github.com/go-openapi/swag/conv v0.25.1 // indirect github.com/go-openapi/swag/fileutils v0.25.1 // indirect @@ -173,6 +175,7 @@ require ( github.com/openziti-incubator/cf v0.0.3 // indirect github.com/openziti/dilithium v0.3.5 // indirect github.com/openziti/go-term-markdown v1.0.1 // indirect + github.com/openziti/xweb/v2 v2.3.4 // indirect github.com/parallaxsecond/parsec-client-go v0.0.0-20221025095442-f0a77d263cf9 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pion/dtls/v3 v3.0.7 // indirect @@ -191,8 +194,8 @@ require ( github.com/spf13/afero v1.12.0 // indirect github.com/spf13/cast v1.7.1 // indirect github.com/subosito/gotenv v1.6.0 // indirect - github.com/tklauser/go-sysconf v0.3.16 // indirect - github.com/tklauser/numcpus v0.11.0 // indirect + github.com/tklauser/go-sysconf v0.3.15 // indirect + github.com/tklauser/numcpus v0.10.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect @@ -200,7 +203,7 @@ require ( github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/zitadel/logging v0.6.2 // indirect github.com/zitadel/schema v1.3.1 // indirect - go.mongodb.org/mongo-driver v1.17.6 // indirect + go.mongodb.org/mongo-driver v1.17.4 // indirect go.mozilla.org/pkcs7 v0.9.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel v1.38.0 // indirect @@ -208,9 +211,8 @@ require ( go.opentelemetry.io/otel/trace v1.38.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/mod v0.29.0 // indirect - golang.org/x/term v0.37.0 // indirect - golang.org/x/tools v0.38.0 // indirect + golang.org/x/mod v0.28.0 // indirect + golang.org/x/tools v0.37.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect nhooyr.io/websocket v1.8.17 // indirect ) diff --git a/go.sum b/go.sum index 8e71f7317..719616ac3 100644 --- a/go.sum +++ b/go.sum @@ -100,6 +100,8 @@ github.com/armon/go-metrics v0.3.8/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4 github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= @@ -225,22 +227,22 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= -github.com/go-openapi/analysis v0.24.1 h1:Xp+7Yn/KOnVWYG8d+hPksOYnCYImE3TieBa7rBOesYM= -github.com/go-openapi/analysis v0.24.1/go.mod h1:dU+qxX7QGU1rl7IYhBC8bIfmWQdX4Buoea4TGtxXY84= -github.com/go-openapi/errors v0.22.4 h1:oi2K9mHTOb5DPW2Zjdzs/NIvwi2N3fARKaTJLdNabaM= -github.com/go-openapi/errors v0.22.4/go.mod h1:z9S8ASTUqx7+CP1Q8dD8ewGH/1JWFFLX/2PmAYNQLgk= +github.com/go-openapi/analysis v0.24.0 h1:vE/VFFkICKyYuTWYnplQ+aVr45vlG6NcZKC7BdIXhsA= +github.com/go-openapi/analysis v0.24.0/go.mod h1:GLyoJA+bvmGGaHgpfeDh8ldpGo69fAJg7eeMDMRCIrw= +github.com/go-openapi/errors v0.22.3 h1:k6Hxa5Jg1TUyZnOwV2Lh81j8ayNw5VVYLvKrp4zFKFs= +github.com/go-openapi/errors v0.22.3/go.mod h1:+WvbaBBULWCOna//9B9TbLNGSFOfF8lY9dw4hGiEiKQ= github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk= github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM= -github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc= -github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4= -github.com/go-openapi/loads v0.23.2 h1:rJXAcP7g1+lWyBHC7iTY+WAF0rprtM+pm8Jxv1uQJp4= -github.com/go-openapi/loads v0.23.2/go.mod h1:IEVw1GfRt/P2Pplkelxzj9BYFajiWOtY2nHZNj4UnWY= -github.com/go-openapi/runtime v0.29.2 h1:UmwSGWNmWQqKm1c2MGgXVpC2FTGwPDQeUsBMufc5Yj0= -github.com/go-openapi/runtime v0.29.2/go.mod h1:biq5kJXRJKBJxTDJXAa00DOTa/anflQPhT0/wmjuy+0= -github.com/go-openapi/spec v0.22.1 h1:beZMa5AVQzRspNjvhe5aG1/XyBSMeX1eEOs7dMoXh/k= -github.com/go-openapi/spec v0.22.1/go.mod h1:c7aeIQT175dVowfp7FeCvXXnjN/MrpaONStibD2WtDA= -github.com/go-openapi/strfmt v0.25.0 h1:7R0RX7mbKLa9EYCTHRcCuIPcaqlyQiWNPTXwClK0saQ= -github.com/go-openapi/strfmt v0.25.0/go.mod h1:nNXct7OzbwrMY9+5tLX4I21pzcmE6ccMGXl3jFdPfn8= +github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU= +github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ= +github.com/go-openapi/loads v0.23.1 h1:H8A0dX2KDHxDzc797h0+uiCZ5kwE2+VojaQVaTlXvS0= +github.com/go-openapi/loads v0.23.1/go.mod h1:hZSXkyACCWzWPQqizAv/Ye0yhi2zzHwMmoXQ6YQml44= +github.com/go-openapi/runtime v0.29.0 h1:Y7iDTFarS9XaFQ+fA+lBLngMwH6nYfqig1G+pHxMRO0= +github.com/go-openapi/runtime v0.29.0/go.mod h1:52HOkEmLL/fE4Pg3Kf9nxc9fYQn0UsIWyGjGIJE9dkg= +github.com/go-openapi/spec v0.22.0 h1:xT/EsX4frL3U09QviRIZXvkh80yibxQmtoEvyqug0Tw= +github.com/go-openapi/spec v0.22.0/go.mod h1:K0FhKxkez8YNS94XzF8YKEMULbFrRw4m15i2YUht4L0= +github.com/go-openapi/strfmt v0.24.0 h1:dDsopqbI3wrrlIzeXRbqMihRNnjzGC+ez4NQaAAJLuc= +github.com/go-openapi/strfmt v0.24.0/go.mod h1:Lnn1Bk9rZjXxU9VMADbEEOo7D7CDyKGLsSKekhFr7s4= github.com/go-openapi/swag v0.25.1 h1:6uwVsx+/OuvFVPqfQmOOPsqTcm5/GkBhNwLqIR916n8= github.com/go-openapi/swag v0.25.1/go.mod h1:bzONdGlT0fkStgGPd3bhZf1MnuPkf2YAys6h+jZipOo= github.com/go-openapi/swag/cmdutils v0.25.1 h1:nDke3nAFDArAa631aitksFGj2omusks88GF1VwdYqPY= @@ -267,12 +269,8 @@ github.com/go-openapi/swag/typeutils v0.25.1 h1:rD/9HsEQieewNt6/k+JBwkxuAHktFtH3 github.com/go-openapi/swag/typeutils v0.25.1/go.mod h1:9McMC/oCdS4BKwk2shEB7x17P6HmMmA6dQRtAkSnNb8= github.com/go-openapi/swag/yamlutils v0.25.1 h1:mry5ez8joJwzvMbaTGLhw8pXUnhDK91oSJLDPF1bmGk= github.com/go-openapi/swag/yamlutils v0.25.1/go.mod h1:cm9ywbzncy3y6uPm/97ysW8+wZ09qsks+9RS8fLWKqg= -github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= -github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= -github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= -github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= -github.com/go-openapi/validate v0.25.1 h1:sSACUI6Jcnbo5IWqbYHgjibrhhmt3vR6lCzKZnmAgBw= -github.com/go-openapi/validate v0.25.1/go.mod h1:RMVyVFYte0gbSTaZ0N4KmTn6u/kClvAFp+mAVfS/DQc= +github.com/go-openapi/validate v0.25.0 h1:JD9eGX81hDTjoY3WOzh6WqxVBVl7xjsLnvDo1GL5WPU= +github.com/go-openapi/validate v0.25.0/go.mod h1:SUY7vKrN5FiwK6LyvSwKjDfLNirSfWwHNgxd2l29Mmw= github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM= github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -621,6 +619,8 @@ github.com/openziti/x509-claims v1.0.3 h1:HNdQ8Nf1agB3lBs1gahcO6zfkeS4S5xoQ2/PkY github.com/openziti/x509-claims v1.0.3/go.mod h1:Z0WIpBm6c4ecrpRKrou6Gk2wrLWxJO/+tuUwKh8VewE= github.com/openziti/xweb/v2 v2.3.4 h1:QFRyyvxBuBv9dRJVMW04gpD3dXo6L4vNs4PM3nlYtYM= github.com/openziti/xweb/v2 v2.3.4/go.mod h1:z1bKQh1mCFxXL9288jtdKzYeXYVXv1Em452jabM2gIM= +github.com/openziti/xweb/v3 v3.0.1 h1:Ey31+21XirlU7ld/Plmpy8YpDVOevSEyFBAH6FzX7wU= +github.com/openziti/xweb/v3 v3.0.1/go.mod h1:ljMB+Ne3bI3I5cItZ4H/bLIGuXvsnlsHVZOp1Z7AvqM= github.com/openziti/ziti-db-explorer v1.1.3 h1:9JER16MJzagtYPdGEhgDcw2p/BXNCVbf9IgA/sMB52w= github.com/openziti/ziti-db-explorer v1.1.3/go.mod h1:pMIMNJoTRSTbkO2e7cZWiBokA3jMdeiGAILP3QhU+v8= github.com/orcaman/concurrent-map/v2 v2.0.1 h1:jOJ5Pg2w1oeB6PeDurIYf6k9PQ+aTITr/6lP/L/zp6c= @@ -791,10 +791,10 @@ github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSW github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= github.com/teris-io/shortid v0.0.0-20201117134242-e59966efd125 h1:3SNcvBmEPE1YlB1JpVZouslJpI3GBNoiqW7+wb0Rz7w= github.com/teris-io/shortid v0.0.0-20201117134242-e59966efd125/go.mod h1:M8agBzgqHIhgj7wEn9/0hJUZcrvt9VY+Ln+S1I5Mha0= -github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= -github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= -github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= -github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= +github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= +github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= +github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= +github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= @@ -831,8 +831,8 @@ go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= -go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss= -go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw= +go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= go.mozilla.org/pkcs7 v0.9.0 h1:yM4/HS9dYv7ri2biPtxt8ikvB37a980dg69/pKmS+eI= go.mozilla.org/pkcs7 v0.9.0/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= @@ -883,8 +883,8 @@ golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= -golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= -golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -924,8 +924,8 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= -golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -974,8 +974,8 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -990,8 +990,8 @@ golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= -golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= +golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1005,8 +1005,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1083,13 +1083,13 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1099,8 +1099,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1162,8 +1162,8 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= -golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= +golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/jwtutil/jwtutil.go b/internal/jwtutil/jwtutil.go new file mode 100644 index 000000000..6d2340c16 --- /dev/null +++ b/internal/jwtutil/jwtutil.go @@ -0,0 +1,16 @@ +package jwtutil + +import "strings" + +func IsJwt(token string) bool { + parts := strings.Split(token, ".") + if len(parts) != 3 { + return false + } + for _, p := range parts { + if p == "" { + return false + } + } + return true +} diff --git a/quickstart/test/ha-test.sh b/quickstart/test/ha-test.sh index 1b7cb78f8..de646057c 100755 --- a/quickstart/test/ha-test.sh +++ b/quickstart/test/ha-test.sh @@ -26,7 +26,7 @@ function _wait_for_controller { )" != "200" ]]; do if (( elapsed >= timeout )); then echo "Timeout waiting for https://${advertised_host_port}" >&2 - exit 1 + return 1 fi echo "waiting for https://${advertised_host_port}" sleep 3 @@ -40,7 +40,7 @@ function _wait_for_controller { while ! "${BUILD_DIR}/ziti" edge login -u admin -p admin "${advertised_host_port}" -y; do if (( elapsed >= timeout )); then echo "Login failed after $timeout seconds, exiting." - exit 1 + return 1 fi echo "Login failed, retrying..." sleep 1 @@ -53,18 +53,18 @@ function _wait_for_controller { function _check_command() { if ! command -v "$1" &>/dev/null; then echo "ERROR: this script requires ${BINS[*]}, but '$1' is missing." >&2 - $1 + return 1 fi } function _wait_for_leader() { - local timeout=10 # Maximum wait time in seconds + local timeout=10 local elapsed=0 while [ "$elapsed" -lt "$timeout" ]; do if "${BUILD_DIR}/ziti" ops cluster list | awk -F'│' 'NR>3 {print $3, $5}' | grep -q "true"; then echo "Leader found" - return 0 # Success + return 0 fi echo "leader not found. waiting for leader..." sleep 1 @@ -72,7 +72,7 @@ function _wait_for_leader() { done echo "No leader found after $timeout seconds" - return 1 # Failure + return 1 } declare -a BINS=(awk grep jq "${BUILD_DIR}/ziti") @@ -91,8 +91,15 @@ done > >(while IFS= read -r line; do echo "inst1: $line"; done) 2>&1 & pid1=$! -_wait_for_controller "2001" -_wait_for_leader +if ! _wait_for_controller "2001"; then + echo "Controller 2001 failed to start" >&2 + exit 1 +fi + +if ! _wait_for_leader; then + echo "Leader not found after starting inst1" >&2 + exit 1 +fi "${BUILD_DIR}/ziti" edge quickstart join \ --ctrl-address="127.0.0.1" \ @@ -107,8 +114,16 @@ _wait_for_leader pid2=$! sleep 3 -_wait_for_controller "2002" -_wait_for_leader + +if ! _wait_for_controller "2002"; then + echo "Controller 2002 failed to start" >&2 + exit 1 +fi + +if ! _wait_for_leader; then + echo "Leader not found after starting inst2" >&2 + exit 1 +fi "${BUILD_DIR}/ziti" edge quickstart join \ --ctrl-address="127.0.0.1" \ @@ -123,7 +138,11 @@ _wait_for_leader pid3=$! sleep 3 -_wait_for_controller "2003" + +if ! _wait_for_controller "2003"; then + echo "Controller 2003 failed to start" >&2 + exit 1 +fi echo "=========================================================" echo "HA Cluster should now be online" @@ -131,8 +150,12 @@ echo "HA Cluster should now be online" echo "" echo "Building and running quickstart test" echo "=========================================================" -"${BUILD_DIR}/ziti" ops verify traffic -u admin -p admin --controller-url localhost:2001 -y \ - > >(while IFS= read -r line; do echo "traffic: $line"; done) 2>&1 + +if ! "${BUILD_DIR}/ziti" ops verify traffic -u admin -p admin --controller-url localhost:2001 -y \ + > >(while IFS= read -r line; do echo "traffic: $line"; done) 2>&1; then + echo "Traffic verification failed" >&2 + exit 1 +fi ZITI_CTRL_EDGE_ADVERTISED_ADDRESS=localhost \ ZITI_CTRL_EDGE_ADVERTISED_PORT=2001 \ diff --git a/router/router.go b/router/router.go index 3d6952131..7756aadec 100644 --- a/router/router.go +++ b/router/router.go @@ -48,7 +48,7 @@ import ( "github.com/openziti/metrics" "github.com/openziti/sdk-golang/xgress" "github.com/openziti/transport/v2" - "github.com/openziti/xweb/v2" + "github.com/openziti/xweb/v3" "github.com/openziti/ziti/common" "github.com/openziti/ziti/common/alert" "github.com/openziti/ziti/common/config" @@ -298,24 +298,32 @@ func (self *Router) createDataPlaneAdapter() xgress.DataPlaneAdapter { }) } -func (self *Router) ListenForShutdownSignal() { +func (self *Router) ListenForShutdownSignal(ctx context.Context) { ch := make(chan os.Signal, 1) signal.Notify(ch, os.Interrupt, syscall.SIGQUIT, syscall.SIGINT, syscall.SIGTERM) + defer signal.Stop(ch) - s := <-ch + pfxlog.Logger().Info("waiting for shutdown signal or context cancel") - if s == syscall.SIGQUIT { - fmt.Println("=== STACK DUMP BEGIN ===") - debugz.DumpStack() - fmt.Println("=== STACK DUMP CLOSE ===") + select { + case s := <-ch: + pfxlog.Logger().Infof("received signal: %v", s) + if s == syscall.SIGQUIT { + fmt.Println("=== STACK DUMP BEGIN ===") + debugz.DumpStack() + fmt.Println("=== STACK DUMP END ===") + } + case <-ctx.Done(): + pfxlog.Logger().Info("context cancelled, initiating shutdown") } log := pfxlog.Logger() - log.Info("shutting down ziti router") if err := self.Shutdown(); err != nil { - log.WithError(err).Info("error encountered during shutdown") + log.WithError(err).Error("error encountered during shutdown") + } else { + log.Info("shutdown complete") } } @@ -641,7 +649,7 @@ func (self *Router) registerComponents() error { var errs []error for i, serverConfig := range config.ServerConfigs { for _, bp := range serverConfig.BindPoints { - if ve := serverConfig.Identity.ValidFor(strings.Split(bp.Address, ":")[0]); ve != nil { + if ve := serverConfig.Identity.ValidFor(strings.Split(bp.ServerAddress(), ":")[0]); ve != nil { if config.Options.DefaultConfigSection != xweb.DefaultConfigSection { errs = append(errs, fmt.Errorf("could not validate server at %s[%d]: %v", config.Options.DefaultConfigSection, i, ve)) } else { diff --git a/ziti/cmd/ascode/ascode_test.go b/tests/cli_tests/ascode_test.go similarity index 85% rename from ziti/cmd/ascode/ascode_test.go rename to tests/cli_tests/ascode_test.go index 9ffe9141a..b7f858c40 100644 --- a/ziti/cmd/ascode/ascode_test.go +++ b/tests/cli_tests/ascode_test.go @@ -1,3 +1,5 @@ +//go:build cli_tests + /* Copyright NetFoundry Inc. @@ -14,87 +16,97 @@ limitations under the License. */ -package cmd +package cli_tests import ( "context" - "crypto/tls" "fmt" - "github.com/antchfx/jsonquery" - "github.com/michaelquigley/pfxlog" - "github.com/openziti/ziti/ziti/cmd/ascode/exporter" - "github.com/openziti/ziti/ziti/cmd/ascode/importer" - "github.com/openziti/ziti/ziti/run" - "github.com/stretchr/testify/assert" - "net/http" "os" + "path/filepath" "strings" "testing" "time" + + "github.com/antchfx/jsonquery" + "github.com/michaelquigley/pfxlog" + "github.com/openziti/ziti/tests/testutil" + "github.com/openziti/ziti/ziti/cmd/ascode/exporter" + "github.com/openziti/ziti/ziti/cmd/ascode/importer" + "github.com/stretchr/testify/assert" ) var log = pfxlog.Logger() func TestYamlUploadAndDownload(t *testing.T) { + zitiPath := os.Getenv("ZITI_CLI_TEST_ZITI_BIN") + if zitiPath == "" { + t.Fatalf("ZITI_CLI_TEST_ZITI_BIN not set") + } ctx, cancel := context.WithCancel(context.Background()) - cmdComplete := make(chan bool) - qsCmd := run.NewQuickStartCmd(os.Stdout, os.Stderr, ctx) - qsCmd.SetArgs([]string{}) - - go func() { - err := qsCmd.Execute() - if err != nil { - log.Fatal(err) - } - cmdComplete <- true - }() - - c := make(chan struct{}) - go waitForController("https://127.0.0.1:1280", c) - - select { - case <-c: - //completed normally - log.Info("controller online") - case <-time.After(30 * time.Second): - cancel() - panic("timed out waiting for controller") + baseDir := filepath.Join(os.TempDir(), "import-tests") + if me := os.MkdirAll(baseDir, 0755); me != nil { + t.Fatalf("failed creating baseDir dir: %v", baseDir) + } + testRunHome, err := os.MkdirTemp(baseDir, "test-run-*") + if err != nil { + t.Fatalf("failed creating temp dir: %v", err) } - performImport(t) - performAllTest(t) - performServiceAndConfigTest(t) - performIdentitiesTest(t) + // set ZITI_CONFIG_DIR so that anything here forth is not corrupting local stuff + _ = os.Setenv("ZITI_CONFIG_DIR", filepath.Join(testRunHome, ".config/ziti")) + overlay := testutil.CreateOverlay(t, ctx, 60*time.Second, testRunHome, "import", false) + targetDone := make(chan error) + go overlay.StartExternal(zitiPath, targetDone) + + defer func() { + if !t.Failed() { + // allow/ensure the processes to exit windows is a pain about rm'ing folders if not + errChan := make(chan error, 1) + + go func() { errChan <- overlay.Stop() }() + success := true + + if err := <-errChan; err != nil { + t.Logf("stop error: %v", err) + success = false + } + + if !success { + t.Logf("manual cleanup may be required at %s", testRunHome) + } else { + t.Logf("tests passed, removing temp dir at %s", testRunHome) + if rerr := os.RemoveAll(testRunHome); rerr != nil { + t.Logf("remove %s failed... **sigh**: %v", testRunHome, rerr) + } + } + } else { + t.Logf("tests failed, temp dir left intact at %s", testRunHome) + } + overlay.CleanupPids() + }() + startErr := overlay.WaitForControllerReady(30 * time.Second) + if startErr != nil { + log.Fatalf("start controller failed: %v", startErr) + } + + performImport(t, overlay.ControllerHostPort()) + performAllTest(t, overlay.ControllerHostPort()) + performServiceAndConfigTest(t, overlay.ControllerHostPort()) + performIdentitiesTest(t, overlay.ControllerHostPort()) cancel() //terminate the running ctrl/router - <-cmdComplete fmt.Println("Operation completed") } -func waitForController(ctrlUrl string, done chan struct{}) { - tr := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} - client := &http.Client{Transport: tr} - for { - r, e := client.Get(ctrlUrl) - if e != nil || r == nil || r.StatusCode != 200 { - time.Sleep(50 * time.Millisecond) - } else { - break - } - } - done <- struct{}{} - -} - -func performImport(t *testing.T) { +func performImport(t *testing.T, url string) { errWriter := strings.Builder{} uploadWriter := strings.Builder{} importCmd := importer.NewImportCmd(&uploadWriter, &errWriter) - importCmd.SetArgs([]string{"--input-format=yaml", "--yes", "--controller-url=localhost:1280", "--username=admin", "--password=admin", "./test.yaml"}) + importCmd.SetArgs([]string{"--input-format=yaml", "--yes", "--controller-url=" + url, "--username=admin", "--password=admin", "./test.yaml"}) err := importCmd.Execute() if err != nil { @@ -103,7 +115,7 @@ func performImport(t *testing.T) { } -func performAllTest(t *testing.T) { +func performAllTest(t *testing.T, url string) { // Create a temporary file in the default temporary directory tempFile, err := os.CreateTemp("", "ascode-output-*.json") @@ -114,7 +126,7 @@ func performAllTest(t *testing.T) { log.Info("output file: ", tempFile.Name()) exportCmd := exporter.NewExportCmd(os.Stdout, os.Stderr) - exportCmd.SetArgs([]string{"--output-format=json", "--yes", "--controller-url=localhost:1280", "--username=admin", "--password=admin", "--output-file=" + tempFile.Name(), "all"}) + exportCmd.SetArgs([]string{"--output-format=json", "--yes", "--controller-url=" + url, "--username=admin", "--password=admin", "--output-file=" + tempFile.Name(), "all"}) err = exportCmd.Execute() if err != nil { t.Fatal(err) @@ -271,7 +283,7 @@ func performAllTest(t *testing.T) { } -func performServiceAndConfigTest(t *testing.T) { +func performServiceAndConfigTest(t *testing.T, url string) { // Create a temporary file in the default temporary directory tempFile, err := os.CreateTemp("", "ascode-output-*.json") @@ -282,7 +294,7 @@ func performServiceAndConfigTest(t *testing.T) { log.Info("output file: ", tempFile.Name()) exportCmd := exporter.NewExportCmd(os.Stdout, os.Stderr) - exportCmd.SetArgs([]string{"--output-format=json", "--yes", "--controller-url=localhost:1280", "--username=admin", "--password=admin", "--output-file=" + tempFile.Name(), "service,config"}) + exportCmd.SetArgs([]string{"--output-format=json", "--yes", "--controller-url=" + url, "--username=admin", "--password=admin", "--output-file=" + tempFile.Name(), "service,config"}) err = exportCmd.Execute() if err != nil { t.Fatal(err) @@ -336,7 +348,7 @@ func performServiceAndConfigTest(t *testing.T) { } -func performIdentitiesTest(t *testing.T) { +func performIdentitiesTest(t *testing.T, url string) { // Create a temporary file in the default temporary directory tempFile, err := os.CreateTemp("", "ascode-output-*.json") @@ -347,7 +359,7 @@ func performIdentitiesTest(t *testing.T) { log.Info("output file: ", tempFile.Name()) exportCmd := exporter.NewExportCmd(os.Stdout, os.Stderr) - exportCmd.SetArgs([]string{"--output-format=json", "--yes", "--controller-url=localhost:1280", "--username=admin", "--password=admin", "--output-file=" + tempFile.Name(), "identity"}) + exportCmd.SetArgs([]string{"--output-format=json", "--yes", "--controller-url=" + url, "--username=admin", "--password=admin", "--output-file=" + tempFile.Name(), "identity"}) err = exportCmd.Execute() if err != nil { t.Fatal(err) diff --git a/tests/cli_tests/login_test.go b/tests/cli_tests/login_test.go new file mode 100644 index 000000000..24e55cd9a --- /dev/null +++ b/tests/cli_tests/login_test.go @@ -0,0 +1,579 @@ +//go:build cli_tests + +/* +Copyright NetFoundry Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package cli_tests + +import ( + "context" + "net/http" + "os" + gopath "path" + "path/filepath" + "runtime" + "strings" + "testing" + "time" + + "github.com/openziti/sdk-golang/ziti" + "github.com/openziti/ziti/tests/testutil" + "github.com/openziti/ziti/ziti/cmd" + "github.com/openziti/ziti/ziti/cmd/api" + "github.com/openziti/ziti/ziti/cmd/common" + "github.com/openziti/ziti/ziti/cmd/edge" + "github.com/openziti/ziti/ziti/util" + "github.com/stretchr/testify/require" +) + +type loginTestState struct { + homeDir string + zitiContext *ziti.Context + zitiTransport *http.Transport + commonOpts api.Options + externalZiti testutil.Overlay + controllerUnderTest testutil.Overlay +} + +func (s *loginTestState) removeZitiDir(t *testing.T) { + zitiDir, _ := util.ConfigDir() + if err := os.RemoveAll(zitiDir); err != nil { + t.Errorf("remove %s: %v", zitiDir, err) + t.Fail() + } + t.Logf("Removed ziti dir from: %s", zitiDir) +} + +func Test_LoginSuite(t *testing.T) { + zitiPath := os.Getenv("ZITI_CLI_TEST_ZITI_BIN") + if zitiPath == "" { + t.Fatalf("ZITI_CLI_TEST_ZITI_BIN not set") + } + if _, statErr := os.Stat(zitiPath); statErr != nil { + t.Fatalf("ziti binary not found at provided location %s: %v", zitiPath, statErr) + } + baseDir := filepath.Join(os.TempDir(), "cli-tests") + if me := os.MkdirAll(baseDir, 0755); me != nil { + t.Fatalf("failed creating baseDir dir: %v", baseDir) + } + testRunHome, mkdirErr := os.MkdirTemp(baseDir, "test-run-*") + if mkdirErr != nil { + t.Fatalf("failed creating temp dir: %v", mkdirErr) + } + // set ZITI_CONFIG_DIR so that anything here forth is not corrupting local stuff + cfgDir := filepath.Join(baseDir, ".config/ziti") + _ = os.Setenv("ZITI_CONFIG_DIR", cfgDir) + _ = os.RemoveAll(cfgDir) + externalCtx, externalCancel := context.WithCancel(context.Background()) + defer externalCancel() + ctrlUnderTestCtx, ctrlUnderTestCancel := context.WithCancel(context.Background()) + defer ctrlUnderTestCancel() + + testState := &loginTestState{ + homeDir: util.HomeDir(), + zitiContext: nil, + zitiTransport: nil, + externalZiti: testutil.CreateOverlay(t, externalCtx, 600*time.Second, testRunHome, "external", false), + controllerUnderTest: testutil.CreateOverlay(t, ctrlUnderTestCtx, 600*time.Second, testRunHome, "target", false), + commonOpts: api.Options{ + CommonOptions: common.CommonOptions{ + Out: os.Stdout, + Err: os.Stderr, + }, + }, + } + + defer func() { + if !t.Failed() { + // allow/ensure the processes to exit windows is a pain about rm'ing folders if not + errChan := make(chan error, 2) + + go func() { errChan <- testState.externalZiti.Stop() }() + go func() { errChan <- testState.controllerUnderTest.Stop() }() + success := true + for i := 0; i < 2; i++ { // Wait for both + if deferErr := <-errChan; deferErr != nil { + t.Logf("stop error: %v", deferErr) + success = false + } + } + if !success { + t.Logf("manual cleanup may be required at %s", testRunHome) + } else { + t.Logf("tests passed, removing temp dir at %s", testRunHome) + if rerr := os.RemoveAll(testRunHome); rerr != nil { + t.Logf("remove %s failed... **sigh**: %v", testRunHome, rerr) + } + } + } else { + t.Logf("tests failed, temp dir left intact at %s", testRunHome) + } + testState.externalZiti.CleanupPids() + testState.controllerUnderTest.CleanupPids() + }() + + extDone := make(chan error) + go testState.externalZiti.StartExternal(zitiPath, extDone) + targetDone := make(chan error) + go testState.controllerUnderTest.StartExternal(zitiPath, targetDone) + + exStartErr := testState.externalZiti.WaitForControllerReady(60 * time.Second) + if exStartErr != nil { + log.Fatalf("externalZiti start failed: %v", exStartErr) + } + + cutStartErr := testState.controllerUnderTest.WaitForControllerReady(60 * time.Second) + if cutStartErr != nil { + log.Fatalf("controllerUnderTest start failed: %v", cutStartErr) + } + + if lo, le := testState.controllerUnderTest.Login(); le != nil { + t.Fatalf("unable to login before running tests: %v", le) + } else { + testState.controllerUnderTest.ApiSession = lo.ApiSession + } + + require.NotEmpty(t, testState.controllerUnderTest.ApiSession) + require.NotEmpty(t, testState.controllerUnderTest.ApiSession.GetToken()) + + now := time.Now().Format("150405") + + if ae := testState.controllerUnderTest.CreateAdminIdentity(t, now, testRunHome); ae != nil { + t.Fatalf("unable to create controller admin: %v", ae) + } + + t.Log("====================================================================================") + t.Log("=========================== overlay ready. tests begin =============================") + t.Log("====================================================================================") + + testTimeout := 120 * time.Second + testDone := make(chan struct{}) + testTimer := time.NewTimer(testTimeout) + defer testTimer.Stop() + + go func() { + defer close(testDone) + // Just signal when time is up, don't call t methods + <-testTimer.C + }() + + if lr, le := testState.controllerUnderTest.Login(); le != nil { + t.Fatalf("unable to login before running tests: %v", le) + } else { + //set the valid token for reuse later: + require.NotEmpty(t, lr.ApiSession) + testState.controllerUnderTest.ApiSession = lr.ApiSession + } + + t.Run("login tests over underlay", testState.runLoginTests) + + t.Log("Cancelling controllerUnderTest to reconfigure for use with ziti") + + ctrlUnderTestCancel() + if se := testState.controllerUnderTest.Stop(); se != nil { + t.Fatalf("controllerUnderTest didn't stop? %v", se) + } + t.Log("Cancelling controllerUnderTest complete") + testState.loginTestsOverZiti(t, now, zitiPath) + externalCancel() + + t.Run("make sure any ziti instances are stopped", testState.externalZiti.EnsureAllPidsStopped) + t.Run("make sure any ziti instances are stopped", testState.controllerUnderTest.EnsureAllPidsStopped) +} + +func (s *loginTestState) runLoginTests(t *testing.T) { + //Authentication Methods + t.Run("correct password succeeds", s.testCorrectPasswordSucceeds) + t.Run("wrong password fails", s.testWrongPasswordFails) + t.Run("token based login", s.testTokenBasedLogin) + t.Run("client cert authentication - no ca", s.testClientCertAuthentication) + t.Run("identity file authentication", s.testIdentityFileAuthentication) + t.Run("external JWT authentication", s.testExternalJWTAuthentication) + t.Run("network identity zitified connection", s.testNetworkIdentityZitifiedConnection) + + // Edge Cases + t.Run("empty username", s.testEmptyUsername) + t.Run("empty password", s.testEmptyPassword) + t.Run("invalid controller URL", s.testInvalidControllerURL) + t.Run("non-existent username", s.testNonExistentUsername) + t.Run("controller unavailable", s.testControllerUnavailable) +} + +// Authentication Methods +func (s *loginTestState) testCorrectPasswordSucceeds(t *testing.T) { + opts := s.controllerUnderTest.NewTestLoginOpts() + + err := opts.Run() + require.NoError(t, err) + require.NotEmpty(t, opts.ApiSession) + t.Logf("Login successful, token: %s", opts.Token) + + // Verify we can create a management client + client, err := opts.NewManagementClient(false) + require.NoError(t, err) + require.NotNil(t, client) + require.NotEmpty(t, opts.ApiSession) +} + +func (s *loginTestState) testWrongPasswordFails(t *testing.T) { + opts := &edge.LoginOptions{ + Options: s.commonOpts, + Username: s.controllerUnderTest.Username, + Password: "wrong-password", + ControllerUrl: s.controllerUnderTest.ControllerHostPort(), + Yes: true, + IgnoreConfig: true, + NetworkId: s.controllerUnderTest.NetworkDialingIdFile, + } + + err := opts.Run() + require.Error(t, err, "login with wrong password should fail") +} + +func (s *loginTestState) testTokenBasedLogin(t *testing.T) { + opts := &edge.LoginOptions{ + Options: s.commonOpts, + ApiSession: s.controllerUnderTest.ApiSession, + ControllerUrl: s.controllerUnderTest.ControllerHostPort(), + IgnoreConfig: true, + NetworkId: s.controllerUnderTest.NetworkDialingIdFile, + } + + err := opts.Run() + require.NoError(t, err) + require.NotEmpty(t, opts.ApiSession) + require.NotEmpty(t, opts.ApiSession.GetToken()) + t.Logf("Login successful, token: %s", opts.ApiSession.GetToken()) +} + +func (s *loginTestState) testClientCertAuthentication(t *testing.T) { + // Setup common options + baseOpts := edge.LoginOptions{ + Options: s.commonOpts, + ControllerUrl: s.controllerUnderTest.ControllerHostPort(), + Yes: true, + IgnoreConfig: true, + ClientCert: s.controllerUnderTest.AdminCertFile, + ClientKey: s.controllerUnderTest.AdminKeyFile, + CaCert: s.controllerUnderTest.AdminCaFile, + NetworkId: s.controllerUnderTest.NetworkDialingIdFile, + } + + s.removeZitiDir(t) + t.Run("all present", func(t *testing.T) { + opts := baseOpts + + err := opts.Run() + require.NoError(t, err, "login with cert/key/ca when all present should succeed") + require.NotEmpty(t, opts.ApiSession) + t.Logf("Login successful, token: %s", opts.Token) + }) + + s.removeZitiDir(t) + t.Run("no cert", func(t *testing.T) { + opts := baseOpts + opts.ClientCert = "" + + err := opts.Run() + require.Error(t, err, "expected error when client cert is missing") + require.Contains(t, err.Error(), "username required but not provided") + }) + + s.removeZitiDir(t) + t.Run("no key", func(t *testing.T) { + opts := baseOpts + opts.ClientKey = "" + + err := opts.Run() + require.Error(t, err, "expected error when client key is missing") + require.Contains(t, err.Error(), "failed to read key") + }) + + s.removeZitiDir(t) + t.Run("no CA cert with yes flag", func(t *testing.T) { + opts := baseOpts + opts.CaCert = "" + opts.Yes = true + + err := opts.Run() + require.NoError(t, err, "expected success when CA cert is missing and IgnoreConfig is enabled and 'Yes' is true") + require.NotEmpty(t, opts.ApiSession) + t.Logf("Login successful, token: %s", opts.Token) + }) + + s.removeZitiDir(t) + t.Run("no CA cert without yes flag", func(t *testing.T) { + opts := baseOpts + opts.CaCert = "" + opts.Yes = false + + err := opts.Run() + require.Error(t, err, "expected error when CA cert is missing") + require.Contains(t, err.Error(), "Cannot accept certs - no terminal") + }) +} + +func (s *loginTestState) testIdentityFileAuthentication(t *testing.T) { + opts := &edge.LoginOptions{ + Options: s.commonOpts, + ControllerUrl: s.controllerUnderTest.ControllerHostPort(), + Yes: true, + IgnoreConfig: true, + File: s.controllerUnderTest.AdminIdFile, + NetworkId: s.controllerUnderTest.NetworkDialingIdFile, + } + + err := opts.Run() + require.NoError(t, err) + + client, err := opts.NewManagementClient(false) + require.NoError(t, err) + require.NotNil(t, client) + require.NotEmpty(t, opts.ApiSession) + t.Logf("Login successful, token: %s", opts.Token) +} + +func (s *loginTestState) testExternalJWTAuthentication(t *testing.T) { + // TODO: Generate valid JWT token + t.Skip("External JWT authentication requires JWT setup") +} + +func (s *loginTestState) testNetworkIdentityZitifiedConnection(t *testing.T) { + // TODO: Create network identity file + t.Skip("Network identity requires identity setup") +} + +// Edge Cases +func (s *loginTestState) testEmptyUsername(t *testing.T) { + opts := &edge.LoginOptions{ + Options: s.commonOpts, + Username: "", + Password: s.controllerUnderTest.Password, + ControllerUrl: s.controllerUnderTest.ControllerHostPort(), + Yes: true, + IgnoreConfig: true, + NetworkId: s.controllerUnderTest.NetworkDialingIdFile, + } + + err := opts.Run() + require.Error(t, err, "empty username should fail") + require.Contains(t, err.Error(), "username required but not provided") + t.Logf("Empty username correctly failed: %v", err) +} + +func (s *loginTestState) testEmptyPassword(t *testing.T) { + opts := &edge.LoginOptions{ + Options: s.commonOpts, + Username: s.controllerUnderTest.Username, + Password: "", + ControllerUrl: s.controllerUnderTest.ControllerHostPort(), + Yes: true, + IgnoreConfig: true, + NetworkId: s.controllerUnderTest.NetworkDialingIdFile, + } + + err := opts.Run() + require.Error(t, err, "empty password should fail") + require.Contains(t, err.Error(), "password required but not provided") + t.Logf("Empty password correctly failed: %v", err) +} + +func (s *loginTestState) testInvalidControllerURL(t *testing.T) { + hostErrors := []string{"i/o timeout", "no such host", "server misbehaving"} + t.Run("not-a-url", func(t *testing.T) { + opts := &edge.LoginOptions{Options: s.commonOpts, Username: s.controllerUnderTest.Username, Password: s.controllerUnderTest.Password, + ControllerUrl: "not-a-url", Yes: true, IgnoreConfig: true, + NetworkId: s.controllerUnderTest.NetworkDialingIdFile} + err := opts.Run() + require.Error(t, err) + require.True(t, + strings.Contains(err.Error(), hostErrors[0]) || + strings.Contains(err.Error(), hostErrors[1]) || + strings.Contains(err.Error(), hostErrors[2]) || + strings.Contains(err.Error(), "service 'not-a-url' not found"), + "Error %s not contained in host errors array: %v", err.Error(), hostErrors) + t.Logf("Invalid URL correctly failed: %v", err) + }) + + t.Run("http://[invalid", func(t *testing.T) { + opts := &edge.LoginOptions{ + Options: s.commonOpts, + Username: s.controllerUnderTest.Username, + Password: s.controllerUnderTest.Password, + ControllerUrl: "http://[invalid", + Yes: true, + IgnoreConfig: true, + NetworkId: s.controllerUnderTest.NetworkDialingIdFile, + } + err := opts.Run() + require.Error(t, err) + invurlmsg := "invalid controller URL" + parsemsg := "unable to parse controller url" + require.True(t, + strings.Contains(err.Error(), invurlmsg) || + strings.Contains(err.Error(), parsemsg), + `Error %s found but expected either: %s or %s`, err.Error(), invurlmsg, parsemsg) + t.Logf("Invalid URL correctly failed: %v", err) + }) + + t.Run("ftp://wrong-scheme.com", func(t *testing.T) { + opts := &edge.LoginOptions{Options: s.commonOpts, Username: s.controllerUnderTest.Username, Password: s.controllerUnderTest.Password, + ControllerUrl: "ftp://wrong-scheme.com", Yes: true, IgnoreConfig: true, + NetworkId: s.controllerUnderTest.NetworkDialingIdFile} + err := opts.Run() + require.Error(t, err) + require.True(t, + strings.Contains(err.Error(), hostErrors[0]) || + strings.Contains(err.Error(), hostErrors[1]) || + strings.Contains(err.Error(), hostErrors[2]) || + strings.Contains(err.Error(), "service 'ftp' not found"), + "Error %s not contained in host errors array: %v", err.Error(), hostErrors) + t.Logf("Invalid URL correctly failed: %v", err) + }) + + t.Run("https://non-existent-host-12345.local:9999", func(t *testing.T) { + opts := &edge.LoginOptions{Options: s.commonOpts, Username: s.controllerUnderTest.Username, Password: s.controllerUnderTest.Password, + ControllerUrl: "https://non-existent-host-12345.local:9999", Yes: true, IgnoreConfig: true, + NetworkId: s.controllerUnderTest.NetworkDialingIdFile} + err := opts.Run() + require.Error(t, err) + require.True(t, + strings.Contains(err.Error(), hostErrors[0]) || + strings.Contains(err.Error(), hostErrors[1]) || + strings.Contains(err.Error(), hostErrors[2]) || + strings.Contains(err.Error(), "service 'non-existent-host-12345.local' not found"), + "Error %s not contained in host errors array: %v", err.Error(), hostErrors) + t.Logf("Invalid URL correctly failed: %v", err) + }) +} + +func (s *loginTestState) testNonExistentUsername(t *testing.T) { + opts := &edge.LoginOptions{ + Options: s.commonOpts, + Username: "nonexistent-user-12345", + Password: s.controllerUnderTest.Password, + ControllerUrl: s.controllerUnderTest.ControllerHostPort(), + Yes: true, + IgnoreConfig: true, + NetworkId: s.controllerUnderTest.NetworkDialingIdFile, + } + + err := opts.Run() + require.Error(t, err, "non-existent username should fail") + t.Logf("Non-existent username correctly failed: %v", err) +} + +func (s *loginTestState) testControllerUnavailable(t *testing.T) { + expectedErr := "connection refused" + if runtime.GOOS == "windows" { //because of course it's different on linux/windows + expectedErr = "the target machine actively refused it" + } + opts := &edge.LoginOptions{ + Options: s.commonOpts, + Username: s.controllerUnderTest.Username, + Password: s.controllerUnderTest.Password, + ControllerUrl: "https://127.0.0.1:9999", + Yes: true, + IgnoreConfig: true, + NetworkId: s.controllerUnderTest.NetworkDialingIdFile, + } + + err := opts.Run() + require.Error(t, err, "unavailable controller should fail") + require.True(t, + strings.Contains(err.Error(), expectedErr) || + strings.Contains(err.Error(), "service '127.0.0.1' not found"), + "Expected error not found: %v", err.Error()) + + t.Logf("Unavailable controller correctly failed: %v", err) +} + +func (s *loginTestState) reconfigureTargetForZiti(pkiRoot string) error { + v2 := cmd.NewRootCommand(os.Stdin, os.Stdout, os.Stderr) + v2.SetArgs(strings.Split("pki create server --key-file server --pki-root "+pkiRoot+" --ip 127.0.0.1,::1 --dns localhost,mgmt,mgmt.ziti --ca-name intermediate-ca-quickstart --server-file mgmt.ziti", " ")) + if zitiCmdErr := v2.Execute(); zitiCmdErr != nil { + return zitiCmdErr + } + return nil +} + +func (s *loginTestState) loginTestsOverZiti(t *testing.T, now, zitiPath string) { + t.Run("login tests over ziti", func(t *testing.T) { + pkiRoot := gopath.Join(s.controllerUnderTest.Home, "pki") + if reconfErr := s.reconfigureTargetForZiti(pkiRoot); reconfErr != nil { + t.Fatalf("failed to reconfigure target: %v", reconfErr) + } + + if ie := s.externalZiti.CreateOverlayIdentities(t, now); ie != nil { + t.Fatalf("failed to initialize ziti transport for controllerUnderTest: %v", ie) + } + s.controllerUnderTest.NetworkDialingIdFile = s.externalZiti.NetworkDialingIdFile + s.controllerUnderTest.NetworkBindingIdFile = s.externalZiti.NetworkBindingIdFile + + controllerUnderTestCtx2, controllerUnderTestCancel := context.WithCancel(context.Background()) + defer controllerUnderTestCancel() + s.controllerUnderTest.Ctx = controllerUnderTestCtx2 + s.controllerUnderTest.ConfigFile = gopath.Join(s.controllerUnderTest.Home, "ctrl.yaml") + newServerCertPath := gopath.Join(s.controllerUnderTest.Home, "pki/intermediate-ca-quickstart/certs/mgmt.ziti.chain.pem") + if re := s.controllerUnderTest.ReplaceConfig(newServerCertPath); re != nil { + t.Fatalf("failed to replace config: %v", re) + } + + targetDone := make(chan error) + go s.controllerUnderTest.StartExternal(zitiPath, targetDone) + cutStartErr := s.controllerUnderTest.WaitForControllerReady(60 * time.Second) + if cutStartErr != nil { + log.Fatalf("controllerUnderTest start failed: %v", cutStartErr) + } + s.controllerUnderTest.ControllerAddress = "mgmt.ziti" + s.controllerUnderTest.ControllerPort = 443 + + s.runLoginTests(t) + + s.testZitiThenNot(t) + + controllerUnderTestCancel() + }) +} + +func (s *loginTestState) testZitiThenNot(t *testing.T) { + // this test should make sure that after logging in with a zitified login, a subsequent login to a non-zitified + // controller works as expected + opts := &edge.LoginOptions{ + Options: s.commonOpts, + Username: s.controllerUnderTest.Username, + Password: s.controllerUnderTest.Password, + ControllerUrl: s.controllerUnderTest.ControllerHostPort(), + Yes: true, + IgnoreConfig: false, + NetworkId: s.controllerUnderTest.NetworkDialingIdFile, + } + + err := opts.Run() + require.NoError(t, err, "overlay controller should login successfully") + + opts = &edge.LoginOptions{ + Options: s.commonOpts, + Username: s.externalZiti.Username, + Password: s.externalZiti.Password, + ControllerUrl: s.externalZiti.ControllerHostPort(), + Yes: true, + IgnoreConfig: false, + NetworkId: "", + } + + err = opts.Run() + require.NoError(t, err, "underlay controller should login successfully") +} diff --git a/tests/cli_tests/login_test_import.yml b/tests/cli_tests/login_test_import.yml new file mode 100644 index 000000000..c1fd5fedf --- /dev/null +++ b/tests/cli_tests/login_test_import.yml @@ -0,0 +1,125 @@ +{ + "authPolicies": [], + "certificateAuthorities": [], + "configTypes": [], + "configs": [ + { + "configType": "@intercept.v1", + "data": { + "addresses": [ + "mgmt.ziti" + ], + "portRanges": [ + { + "high": 443, + "low": 443 + } + ], + "protocols": [ + "tcp" + ] + }, + "name": "mgmt.interceptv1", + "tags": {} + } + ], + "edgeRouterPolicies": [ + { + "edgeRouterRoles": [ + "#public" + ], + "identityRoles": [ + "#all" + ], + "name": "all-endpoints-public-routers", + "semantic": "AnyOf", + "tags": {} + }, + { + "edgeRouterRoles": [ + "@router-quickstart" + ], + "identityRoles": [ + "@router-quickstart" + ], + "name": "edge-router-RoZMjWCQAx-system", + "semantic": "AnyOf", + "tags": {} + } + ], + "edgeRouters": [ + { + "appData": {}, + "disabled": false, + "hostname": "sg4", + "isTunnelerEnabled": true, + "name": "router-quickstart", + "noTraversal": false, + "roleAttributes": [ + "public" + ], + "tags": {}, + "unverifiedCertPem": null, + "unverifiedFingerprint": null + } + ], + "externalJwtSigners": [], + "identities": [ + ], + "postureChecks": [], + "serviceEdgeRouterPolicies": [ + { + "edgeRouterRoles": [ + "#all" + ], + "name": "all-routers-all-services", + "semantic": "AnyOf", + "serviceRoles": [ + "#all" + ], + "tags": {} + } + ], + "servicePolicies": [ + { + "identityRoles": [ + "#mgmtclients" + ], + "name": "mgmt.dial", + "postureCheckRoles": [], + "semantic": "AnyOf", + "serviceRoles": [ + "@mgmt" + ], + "tags": {}, + "type": "Dial" + }, + { + "identityRoles": [ + "#mgmtservers" + ], + "name": "mgmt.bind", + "postureCheckRoles": [], + "semantic": "AnyOf", + "serviceRoles": [ + "@mgmt" + ], + "tags": {}, + "type": "Bind" + } + ], + "services": [ + { + "configs": [ + "@mgmt.interceptv1" + ], + "encryptionRequired": true, + "name": "mgmt", + "roleAttributes": [ + "mgmtservers" + ], + "tags": {}, + "terminatorStrategy": "smartrouting" + } + ] +} \ No newline at end of file diff --git a/tests/cli_tests/run-cli-tests.sh b/tests/cli_tests/run-cli-tests.sh new file mode 100644 index 000000000..4a2c2152f --- /dev/null +++ b/tests/cli_tests/run-cli-tests.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +# raise exceptions +set -o errexit +set -o nounset +set -o pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR" && git rev-parse --show-toplevel)" + +cd "$ROOT_DIR" +echo "changed to root dir at: $ROOT_DIR" + +if [ -z "$ZITI_CLI_TEST_ZITI_BIN" ]; then + echo "building binary for use in tests" + mkdir -p cli_tests_bin + go build -o cli_tests_bin ./... + export ZITI_CLI_TEST_ZITI_BIN="$ROOT_DIR/cli_tests_bin/ziti" +else + echo "using pre-defined ZITI_CLI_TEST_ZITI_BIN at: $ZITI_CLI_TEST_ZITI_BIN" +fi + +echo "executing cli_tests from $PWD" +go test ./tests/cli_tests/... --tags cli_tests ${ZITI_CLI_TESTS_VERBOSE:-} +echo "cli_tests complete" \ No newline at end of file diff --git a/ziti/cmd/ascode/test.yaml b/tests/cli_tests/test.yaml similarity index 100% rename from ziti/cmd/ascode/test.yaml rename to tests/cli_tests/test.yaml diff --git a/tests/testutil/overlay.go b/tests/testutil/overlay.go new file mode 100644 index 000000000..2cc89554e --- /dev/null +++ b/tests/testutil/overlay.go @@ -0,0 +1,547 @@ +//go:build apitests || cli_tests + +/* +Copyright NetFoundry Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package testutil + +import ( + "context" + "errors" + "fmt" + "net" + "os" + "os/exec" + gopath "path" + "path/filepath" + "regexp" + "runtime" + "strings" + "sync" + "syscall" + "testing" + "time" + + "github.com/openziti/edge-api/rest_util" + edge_apis "github.com/openziti/sdk-golang/edge-apis" + "github.com/openziti/ziti/ziti/cmd" + "github.com/openziti/ziti/ziti/cmd/api" + "github.com/openziti/ziti/ziti/cmd/common" + "github.com/openziti/ziti/ziti/cmd/edge" + "github.com/openziti/ziti/ziti/cmd/ops" + "github.com/openziti/ziti/ziti/run" + "github.com/openziti/ziti/ziti/util" + "github.com/stretchr/testify/require" +) + +var commonOpts = api.Options{ + CommonOptions: common.CommonOptions{ + Out: os.Stdout, + Err: os.Stderr, + }, +} + +type loginCreds struct { + AdminCertFile string + AdminCaFile string + AdminKeyFile string + AdminIdFile string + ApiSession edge_apis.ApiSession +} + +type Overlay struct { + loginCreds + NetworkBindingIdFile string // a ziti identity file to use when starting the controller which will bind a given service over an overlay + NetworkDialingIdFile string // a ziti identity file used to dial mgmt services hosted/bound by a controller using a ziti overlay + t *testing.T + Ctx context.Context + StartTimeout time.Duration + Name string + extCmd *exec.Cmd + cmdDone chan error + pidsMutex *sync.Mutex + activePids []int + *run.QuickstartOpts +} + +func (o *Overlay) ControllerHostPort() string { + return fmt.Sprintf("https://%s:%d", o.ControllerAddress, o.ControllerPort) +} +func (o *Overlay) RouterHostPort() string { + return fmt.Sprintf("https://%s:%d", o.RouterAddress, o.RouterPort) +} + +func (o *Overlay) startArgs() []string { + args := []string{ + fmt.Sprintf("--home=%s", o.Home), + fmt.Sprintf("--ctrl-address=%s", o.ControllerAddress), + fmt.Sprintf("--ctrl-port=%d", o.ControllerPort), + fmt.Sprintf("--router-address=%s", o.RouterAddress), + fmt.Sprintf("--router-port=%d", o.RouterPort), + } + if o.Routerless { + args = append(args, "--no-router") + } + if o.ConfigureAndExit { + args = append(args, "--configure-and-exit") + } + return args +} + +func (o *Overlay) ReplaceConfig(newServerCertPath string) error { + content, err := os.ReadFile(o.QuickstartOpts.ConfigFile) + if err != nil { + return err + } + newContent := string(content) + + // remove edge-management section + reEdgeMgmt := regexp.MustCompile(`(?m)^ *- binding: edge-management[\s\S]+?options: \{\ }\n`) + newContent = reEdgeMgmt.ReplaceAllString(newContent, "") + + // replace "- binding: fabric" block with single "-" + reFabric := regexp.MustCompile(`(?m)- binding: fabric[\s\S]+?options: \{\ }\n {6}-`) + newContent = reFabric.ReplaceAllString(newContent, "-") + + reServerCert := regexp.MustCompile(`(?m)^( *)(server_cert:.*server.chain.pem")$`) + newContent = reServerCert.ReplaceAllString(newContent, "$1#$2\n${1}server_cert: "+newServerCertPath) + + newContent = newContent + ` + - name: secured-by-ziti-http + bindPoints: + - identity: + file: ` + o.NetworkBindingIdFile + ` + service: "mgmt" + serveTLS: true + apis: + - binding: edge-management + options: { } + - binding: fabric + options: { } + - binding: zac + options: + location: "/ctrl/zac/ziti-console-v3.12.5" + indexFile: index.html` + if we := os.WriteFile(o.QuickstartOpts.ConfigFile, []byte(newContent), 0644); we != nil { + return fmt.Errorf("failed to write new content to target controller file: %v", we) + } else { + fmt.Println("CHANGED FILE AT : " + o.QuickstartOpts.ConfigFile) + } + return nil +} + +func (o *Overlay) StartExternal(zitiPath string, done chan error) { + args := append([]string{"edge", "quickstart"}, o.startArgs()...) + fmt.Printf("%s overlay command: %s %s\n", o.Name, zitiPath, strings.Join(args, " ")) + o.extCmd = exec.CommandContext( + o.Ctx, + zitiPath, + args..., + ) + _ = os.Mkdir(o.Home, 0755) + stdoutFile, createErr1 := os.Create(filepath.Join(o.Home, "ctrl-stdout.log")) + if createErr1 != nil { + done <- createErr1 + } + stderrFile, createErr2 := os.Create(filepath.Join(o.Home, "ctrl-stderr.log")) + if createErr2 != nil { + done <- createErr2 + } + o.extCmd.Stdout = stdoutFile + o.extCmd.Stderr = stderrFile + + fmt.Printf("ctrl logs at: %s\n", filepath.Join(o.Home, "ctrl-stdout.log")) + if startErr := o.extCmd.Start(); startErr != nil { + done <- startErr + return + } + + fmt.Printf("started ziti quickstart (pid=%d)\n", o.extCmd.Process.Pid) + o.trackPid(o.extCmd.Process.Pid) + + go func() { + err := o.extCmd.Wait() + if errors.Is(err, context.Canceled) || (err != nil && strings.Contains(err.Error(), "signal killed")) { + err = nil + } + done <- err + }() +} + +func (o *Overlay) CreateAdminIdentity(t *testing.T, now, baseDir string) error { + if lr, le := o.Login(); le != nil { + return le + } else { + //set the valid token for reuse later: + require.NotEmpty(t, lr) + require.NotEmpty(t, lr.ApiSession) + require.NotEmpty(t, lr.ApiSession.GetToken()) + o.ApiSession = lr.ApiSession + } + adminIdName := fmt.Sprintf("test-admin-%s", now) + adminJwtPath := filepath.Join(baseDir, adminIdName+".jwt") + zitiCmd := edge.NewCmdEdge(os.Stdout, os.Stderr, common.NewOptionsProvider(os.Stdout, os.Stderr)) + zitiCmd.SetArgs(strings.Split("create identity "+adminIdName+" -o "+adminJwtPath+" --admin", " ")) + if zitiCmdErr := zitiCmd.Execute(); zitiCmdErr != nil { + t.Fatalf("unable to create identity: %v", zitiCmdErr) + } + zitiCmd.SetArgs([]string{"enroll", adminJwtPath}) + if zitiCmdErr := zitiCmd.Execute(); zitiCmdErr != nil { + t.Fatalf("unable to create identity: %v", zitiCmdErr) + } + o.AdminIdFile = strings.TrimSuffix(adminJwtPath, ".jwt") + ".json" + t.Logf("identity file should exist at: %v", o.AdminIdFile) + + unwrapCmd := ops.NewUnwrapIdentityFileCommand(os.Stdout, os.Stderr) + unwrapCmd.SetArgs([]string{o.AdminIdFile}) + unwrapCmdErr := unwrapCmd.Execute() + if unwrapCmdErr != nil { + t.Fatalf("unable to unwrap identity: %v", unwrapCmdErr) + } + o.AdminCertFile = strings.TrimSuffix(adminJwtPath, ".jwt") + ".cert" + o.AdminCaFile = strings.TrimSuffix(adminJwtPath, ".jwt") + ".ca" + o.AdminKeyFile = strings.TrimSuffix(adminJwtPath, ".jwt") + ".key" + t.Logf("certfile should exist at: %v", o.AdminCertFile) + t.Logf("caFile should exist at: %v", o.AdminCaFile) + t.Logf("keyFile should exist at: %v", o.AdminKeyFile) + return nil +} + +func (o *Overlay) CreateOverlayIdentities(t *testing.T, now string) error { + p := common.NewOptionsProvider(os.Stdout, os.Stderr) + if _, le := o.Login(); le != nil { + return le + } + + controllerIdName := fmt.Sprintf("controller-binder-%s", now) + controllerJwtPath := filepath.Join(o.Home, controllerIdName+".jwt") + zitiCmd1 := edge.NewCmdEdge(os.Stdout, os.Stderr, p) + zitiCmd1.SetArgs(strings.Split("create identity "+controllerIdName+" -o "+controllerJwtPath+" --admin -a mgmtservers", " ")) + if zitiCmdErr := zitiCmd1.Execute(); zitiCmdErr != nil { + t.Fatalf("unable to create identity: %v", zitiCmdErr) + } + zitiCmd1.SetArgs([]string{"enroll", controllerJwtPath}) + if zitiCmdErr := zitiCmd1.Execute(); zitiCmdErr != nil { + t.Fatalf("unable to create identity: %v", zitiCmdErr) + } + o.NetworkBindingIdFile = strings.TrimSuffix(controllerJwtPath, ".jwt") + ".json" + t.Logf("networkBindingIdFile should exist at: %v", o.NetworkBindingIdFile) + + clientIdName := fmt.Sprintf("controller-client-%s", now) + clientJwtPath := filepath.Join(o.Home, clientIdName+".jwt") + zitiCmd1 = edge.NewCmdEdge(os.Stdout, os.Stderr, p) + zitiCmd1.SetArgs(strings.Split("create identity "+clientIdName+" -o "+clientJwtPath+" -a mgmtclients", " ")) + if zitiCmdErr := zitiCmd1.Execute(); zitiCmdErr != nil { + t.Fatalf("unable to create identity: %v", zitiCmdErr) + } + zitiCmd1.SetArgs([]string{"enroll", clientJwtPath}) + if zitiCmdErr := zitiCmd1.Execute(); zitiCmdErr != nil { + t.Fatalf("unable to create identity: %v", zitiCmdErr) + } + o.NetworkDialingIdFile = strings.TrimSuffix(clientJwtPath, ".jwt") + ".json" + t.Logf("networkDialingIdFile should exist at: %v", o.NetworkDialingIdFile) + + cwd, _ := os.Getwd() + yamlToImport, _ := filepath.Abs(cwd + "/login_test_import.yml") + v2 := cmd.NewRootCommand(os.Stdin, os.Stdout, os.Stderr) + v2.SetArgs(strings.Split("ops import --username "+o.Username+" --password "+o.Password+" "+yamlToImport, " ")) + if v2Err := v2.Execute(); v2Err != nil { + t.Fatalf("unable to import zitified-login-test.yml: %v", v2Err) + } + return nil +} +func (o *Overlay) Stop() error { + if o.extCmd != nil && o.extCmd.Process != nil { + _ = o.extCmd.Process.Kill() + + // Poll until process is gone + for i := 0; i < 120; i++ { // 60 seconds total + time.Sleep(500 * time.Millisecond) + + // Try to find the process - if it fails, process is gone + if proc, err := os.FindProcess(o.extCmd.Process.Pid); err != nil || proc == nil { + o.closeFileHandles() + return nil + } + + // On Windows, FindProcess can succeed, send signal 0 to check + if err := o.extCmd.Process.Signal(syscall.Signal(0)); err != nil { + o.closeFileHandles() + return nil // Process is gone + } + } + + return fmt.Errorf("overlay %s did not exit after kill", o.Name) + } + return nil +} +func (o *Overlay) Login() (*edge.LoginOptions, error) { + initialLogin := &edge.LoginOptions{ + Options: commonOpts, + Username: o.Username, + Password: o.Password, + ControllerUrl: o.ControllerHostPort(), + Yes: true, + NetworkId: o.NetworkDialingIdFile, + } + ile := initialLogin.Run() + if ile == nil { + util.ReloadConfig() //every login really needs to call reload to flush/overwrite the cached client + } + return initialLogin, ile +} + +func (o *Overlay) closeFileHandles() { + if f, ok := o.extCmd.Stdout.(*os.File); ok { + if err := f.Close(); err != nil { + fmt.Println("failed to close stdout") + } + } + if f, ok := o.extCmd.Stderr.(*os.File); ok { + if err := f.Close(); err != nil { + fmt.Println("failed to close stderr") + } + } +} + +func (o *Overlay) NewTestLoginOpts() edge.LoginOptions { + return edge.LoginOptions{ + Options: commonOpts, + Username: o.Username, + Password: o.Password, + ControllerUrl: o.ControllerHostPort(), + Yes: true, + IgnoreConfig: false, + NetworkId: o.NetworkDialingIdFile, + } +} + +func (o *Overlay) trackPid(pid int) { + o.pidsMutex.Lock() + defer o.pidsMutex.Unlock() + o.activePids = append(o.activePids, pid) +} + +func (o *Overlay) CleanupPids() { + o.pidsMutex.Lock() + defer o.pidsMutex.Unlock() + + for _, pid := range o.activePids { + if !o.isPidRunning(pid) { + fmt.Printf("Process (pid=%d) already exited\n", pid) + continue + } + + fmt.Printf("Process (pid=%d) still running, killing...\n", pid) + proc, _ := os.FindProcess(pid) + if err := proc.Kill(); err != nil { + fmt.Printf("Failed to kill process (pid=%d): %v\n", pid, err) + } else { + fmt.Printf("Successfully killed process (pid=%d)\n", pid) + } + } +} + +func (o *Overlay) forceKillPids(pids []int) { + if runtime.GOOS != "windows" { + return + } + + for _, pid := range pids { + fmt.Printf("Force killing process (pid=%d)...\n", pid) + c := exec.Command("taskkill", "/F", "/PID", fmt.Sprintf("%d", pid)) + _ = c.Run() + } +} + +func (o *Overlay) EnsureAllPidsStopped(t *testing.T) { + running := o.getRunningPids() + if len(running) == 0 { + fmt.Printf("All processes stopped\n") + return + } + + fmt.Printf("Processes still running: %v, waiting 10o...\n", running) + time.Sleep(10 * time.Second) + + running = o.getRunningPids() + if len(running) > 0 { + fmt.Printf("Force killing remaining processes: %v\n", running) + o.forceKillPids(running) + + // Poll for up to 60s for processes to exit + start := time.Now() + for time.Since(start) < 60*time.Second { + running = o.getRunningPids() + if len(running) == 0 { + fmt.Printf("All processes stopped after force kill\n") + return + } + fmt.Printf("Processes still running after %.0fs: %v\n", time.Since(start).Seconds(), running) + time.Sleep(2 * time.Second) + } + + running = o.getRunningPids() + if len(running) > 0 { + t.Fatalf("Processes still running after 60s wait: %v", running) + } + } + + fmt.Printf("All processes stopped\n") +} + +func (o *Overlay) isPidRunning(pid int) bool { + if pid <= 0 { + return false + } + proc, err := os.FindProcess(pid) + if err != nil { + return false + } + err = proc.Signal(syscall.Signal(0)) + if err == nil { + return true + } + if err.Error() == "os: process already finished" { + return false + } + errno, ok := err.(syscall.Errno) + if !ok { + return false + } + switch errno { + case syscall.ESRCH: + return false + case syscall.EPERM: + return true + default: + // ignored + } + return false +} + +func (o *Overlay) getRunningPidsWindows(pids []int) []int { + if len(pids) == 0 { + return nil + } + + args := []string{"/NH"} + for _, pid := range pids { + args = append(args, "/FI", fmt.Sprintf("PID eq %d", pid)) + } + + c := exec.Command("tasklist", args...) + output, err := c.Output() + if err != nil { + return nil + } + + var running []int + outputStr := string(output) + for _, pid := range pids { + if strings.Contains(outputStr, fmt.Sprintf("%d", pid)) { + running = append(running, pid) + } + } + return running +} + +func (o *Overlay) getRunningPids() []int { + o.pidsMutex.Lock() + defer o.pidsMutex.Unlock() + + if len(o.activePids) == 0 { + return nil + } + + if runtime.GOOS == "windows" { + return o.getRunningPidsWindows(o.activePids) + } + + var running []int + for _, pid := range o.activePids { + if o.isPidRunning(pid) { + running = append(running, pid) + } + } + return running +} + +func (o *Overlay) WaitForControllerReady(timeout time.Duration) error { + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + fmt.Println("Waiting for controller to start... " + o.ControllerHostPort()) + _, err := rest_util.GetControllerWellKnownCas(o.ControllerHostPort()) + if err == nil { + return nil + } + case <-time.After(timeout): + return fmt.Errorf("timeout waiting for controller to become ready at %s", o.ControllerHostPort()) + } + } +} + +func (o *Overlay) WaitForRouterReady(timeout time.Duration) error { + if !o.Routerless { + routerReady := make(chan error) + o.WaitForRouter(timeout, routerReady) + + select { + case err := <-routerReady: + return err + case <-time.After(10 * time.Second): + return fmt.Errorf("timeout waiting for router to be ready at: %s", o.RouterHostPort()) + } + } + return nil +} + +func CreateOverlay(t *testing.T, ctx context.Context, startTimeout time.Duration, home string, name string, ha bool) Overlay { + o := Overlay{ + Name: name, + t: t, + Ctx: ctx, + StartTimeout: startTimeout, + cmdDone: make(chan error, 1), + QuickstartOpts: &run.QuickstartOpts{ + Home: gopath.Join(home, name), + ControllerAddress: "localhost", //helpers.GetCtrlAdvertisedAddress(), + ControllerPort: findAvailablePort(t), + RouterAddress: "localhost", //helpers.GetRouterAdvertisedAddress(), + RouterPort: findAvailablePort(t), + Routerless: false, + TrustDomain: name, + InstanceID: name, + IsHA: ha, + Username: "admin", + Password: "admin", + ConfigureAndExit: false, + }, + pidsMutex: &sync.Mutex{}, + } + + return o +} + +func findAvailablePort(t *testing.T) uint16 { + listener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + defer func() { _ = listener.Close() }() + return (uint16)(listener.Addr().(*net.TCPAddr).Port) +} diff --git a/ziti/cmd/ascode/exporter/exporter.go b/ziti/cmd/ascode/exporter/exporter.go index 7f506ea36..d5ae73f7f 100644 --- a/ziti/cmd/ascode/exporter/exporter.go +++ b/ziti/cmd/ascode/exporter/exporter.go @@ -20,6 +20,11 @@ import ( "bufio" "encoding/json" "errors" + "io" + "os" + "slices" + "strings" + "github.com/judedaryl/go-arrayutils" "github.com/michaelquigley/pfxlog" "github.com/openziti/edge-api/rest_management_api_client" @@ -33,10 +38,6 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" "gopkg.in/yaml.v3" - "io" - "os" - "slices" - "strings" ) var log = pfxlog.Logger() @@ -75,7 +76,7 @@ func NewExportCmd(out io.Writer, errOut io.Writer) *cobra.Command { Long: "Export all or comma separated list of selected entities.\n" + "Valid entities are: [all|ca/certificate-authority|identity|edge-router|service|config|config-type|service-policy|edge-router-policy|service-edge-router-policy|external-jwt-signer|auth-policy|posture-check] (default all)", Args: cobra.MinimumNArgs(0), - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { logLvl := logrus.InfoLevel if loginOpts.Verbose { @@ -97,11 +98,12 @@ func NewExportCmd(out io.Writer, errOut io.Writer) *cobra.Command { log.Fatalf("Invalid output format: %s", outputFormat) } - client, err := loginOpts.NewMgmtClient() - if err != nil { - log.Fatal(err) + mgmtClient, mgmtClientErr := loginOpts.NewManagementClient(true) + if mgmtClientErr != nil { + log.WithError(mgmtClientErr).Error("Error creating management client") + return mgmtClientErr } - exporter.Client = client + exporter.Client = mgmtClient.BaseClient.API.ZitiEdgeManagement var entities []string if len(args) > 0 { @@ -158,7 +160,7 @@ func NewExportCmd(out io.Writer, errOut io.Writer) *cobra.Command { WithField("bytes", bytes). Debug("Wrote data") } - + return nil }, Hidden: true, } diff --git a/ziti/cmd/ascode/importer/importer.go b/ziti/cmd/ascode/importer/importer.go index d65dcb35d..0342577ac 100644 --- a/ziti/cmd/ascode/importer/importer.go +++ b/ziti/cmd/ascode/importer/importer.go @@ -19,6 +19,12 @@ package importer import ( "encoding/json" "errors" + "fmt" + "io" + "maps" + "os" + "strings" + "github.com/judedaryl/go-arrayutils" "github.com/michaelquigley/pfxlog" "github.com/openziti/edge-api/rest_management_api_client" @@ -32,10 +38,6 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" "gopkg.in/yaml.v3" - "io" - "maps" - "os" - "strings" ) var log = pfxlog.Logger() @@ -94,7 +96,7 @@ func NewImportCmd(out io.Writer, errOut io.Writer) *cobra.Command { filename: args[0], }.read() if err != nil { - log.WithError(err).Fatal("error reading file") + return fmt.Errorf("error reading file: %v", err) } data := map[string][]interface{}{} @@ -111,11 +113,11 @@ func NewImportCmd(out io.Writer, errOut io.Writer) *cobra.Command { } importer.Data = data - client, err := loginOpts.NewMgmtClient() - if err != nil { - log.Fatal(err) + mgmtClient, mgmtClientErr := loginOpts.NewManagementClient(true) + if mgmtClientErr != nil { + return mgmtClientErr } - importer.Client = client + importer.Client = mgmtClient.BaseClient.API.ZitiEdgeManagement var entities []string if len(args) > 1 { diff --git a/ziti/cmd/create/create_config.go b/ziti/cmd/create/create_config.go index 359f78334..f8faab875 100644 --- a/ziti/cmd/create/create_config.go +++ b/ziti/cmd/create/create_config.go @@ -26,7 +26,7 @@ import ( "github.com/openziti/channel/v4" foundation "github.com/openziti/transport/v2" - fabXweb "github.com/openziti/xweb/v2" + fabXweb "github.com/openziti/xweb/v3" edge "github.com/openziti/ziti/controller/config" "github.com/sirupsen/logrus" "github.com/spf13/cobra" diff --git a/ziti/cmd/edge/login.go b/ziti/cmd/edge/login.go index e3309872c..964254a88 100644 --- a/ziti/cmd/edge/login.go +++ b/ziti/cmd/edge/login.go @@ -17,24 +17,11 @@ package edge import ( + "crypto" "crypto/tls" "crypto/x509" + "encoding/pem" "fmt" - "github.com/Jeffail/gabs" - "github.com/michaelquigley/pfxlog" - "github.com/openziti/edge-api/rest_client_api_client" - "github.com/openziti/edge-api/rest_management_api_client" - "github.com/openziti/edge-api/rest_util" - "github.com/openziti/foundation/v2/term" - edge_apis "github.com/openziti/sdk-golang/edge-apis" - "github.com/openziti/sdk-golang/ziti" - ziticobra "github.com/openziti/ziti/internal/cobra" - "github.com/openziti/ziti/ziti/cmd/api" - "github.com/openziti/ziti/ziti/cmd/common" - cmdhelper "github.com/openziti/ziti/ziti/cmd/helpers" - "github.com/openziti/ziti/ziti/util" - "github.com/pkg/errors" - "github.com/spf13/cobra" "io" "net/http" "net/url" @@ -42,6 +29,23 @@ import ( "path/filepath" "strings" "time" + + "github.com/Jeffail/gabs" + "github.com/michaelquigley/pfxlog" + "github.com/openziti/edge-api/rest_client_api_client" + "github.com/openziti/edge-api/rest_util" + "github.com/openziti/foundation/v2/term" + edge_apis "github.com/openziti/sdk-golang/edge-apis" + "github.com/openziti/sdk-golang/ziti" + ziticobra "github.com/openziti/ziti/internal/cobra" + "github.com/openziti/ziti/internal/jwtutil" + "github.com/openziti/ziti/ziti/cmd/api" + "github.com/openziti/ziti/ziti/cmd/common" + "github.com/openziti/ziti/ziti/constants" + "github.com/openziti/ziti/ziti/util" + "github.com/pkg/errors" + "github.com/spf13/cobra" + xterm "golang.org/x/term" ) // LoginOptions are the flags for login commands @@ -60,8 +64,24 @@ type LoginOptions struct { ExtJwtToken string File string ControllerUrl string - + ServiceName string + NetworkId string FileCertCreds *edge_apis.IdentityCredentials + ApiSession edge_apis.ApiSession + TotpCallback func(strings chan string) + + client http.Client + transport *http.Transport + caPool *x509.CertPool + cachedId *util.RestClientEdgeIdentity + mgmtClient *edge_apis.ManagementApiClient +} + +func (options *LoginOptions) GetClient() http.Client { + return options.client +} +func (options *LoginOptions) SetClient(c http.Client) { + options.client = c } const LoginFlagKey = "login" @@ -96,6 +116,10 @@ func AddLoginFlags(cmd *cobra.Command, options *LoginOptions) { addLoginAnnotation(cmd, "ext-jwt") cmd.Flags().StringVarP(&options.File, "file", "f", "", "An identity file to use for authentication") addLoginAnnotation(cmd, "file") + cmd.Flags().StringVarP(&options.ServiceName, "service", "s", "", "The service name to use. When set the file will be used to create a zitified connection") + addLoginAnnotation(cmd, "service") + cmd.Flags().StringVarP(&options.NetworkId, "network-identity", "n", "", "The identity to use to connect to the OpenZiti overlay") + addLoginAnnotation(cmd, "network-identity") options.AddCommonFlags(cmd) } @@ -113,7 +137,7 @@ func NewLoginCmd(out io.Writer, errOut io.Writer) *cobra.Command { Short: "logs into a Ziti Edge Controller instance", Long: `login allows the ziti command to establish a session with a Ziti Edge Controller, allowing more commands to be run against the controller.`, Args: cobra.RangeArgs(0, 1), - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { options.Cmd = cmd if len(args) > 0 { options.ControllerUrl = args[0] @@ -122,11 +146,11 @@ func NewLoginCmd(out io.Writer, errOut io.Writer) *cobra.Command { if options.extJwtFile != "" { auth, err := os.ReadFile(options.extJwtFile) if err != nil { - pfxlog.Logger().Fatal(err) + return err } options.ExtJwtToken = string(auth) } - cmdhelper.CheckErr(options.Run()) + return options.Run() }, SuggestFor: []string{}, } @@ -136,59 +160,39 @@ func NewLoginCmd(out io.Writer, errOut io.Writer) *cobra.Command { return cmd } -func (o *LoginOptions) newHttpClient() *http.Client { +func (o *LoginOptions) newHttpClient(tryCachedCreds bool) (http.Client, error) { if o.ControllerUrl != "" && o.Args == nil || len(o.Args) < 1 { o.Args = []string{o.ControllerUrl} } - // any error indicates there are probably no saved credentials. look for login information and use those - loginErr := o.Run() - if loginErr != nil { - pfxlog.Logger().Fatal(loginErr) - } - - caPool := x509.NewCertPool() - if o.CaCert != "" { - if _, cacertErr := os.Stat(o.CaCert); cacertErr == nil { - rootPemData, err := os.ReadFile(o.CaCert) - if err != nil { - pfxlog.Logger().Fatalf("error reading CA cert [%s]", o.CaCert) - } - caPool.AppendCertsFromPEM(rootPemData) - } else { - pfxlog.Logger().Warnf("CA cert not found [%s]", o.CaCert) + if tryCachedCreds { + // any error indicates there are probably no saved credentials. look for login information and use those + cached := *o + cached.PopulateFromCache() + cached.IgnoreConfig = true // don't overwrite when trying to login + loginErr := cached.Run() + if loginErr != nil { + return http.Client{}, loginErr } } - - return &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{ - RootCAs: caPool, - }, - }, + t, cte := o.createHttpTransport() + if cte != nil { + return http.Client{}, cte } + c := http.Client{ + Transport: t, + } + return c, nil } // NewClientApiClient returns a new management client for use with the controller using the set of login material provided func (o *LoginOptions) NewClientApiClient() (*rest_client_api_client.ZitiEdgeClient, error) { - httpClient := o.newHttpClient() - - c, e := rest_util.NewEdgeClientClientWithToken(httpClient, o.ControllerUrl, o.Token) - if e != nil { - pfxlog.Logger().Fatal(e) + nc, newClientErr := o.newHttpClient(true) + if newClientErr != nil { + return nil, newClientErr } - return c, nil -} -// NewMgmtClient returns a new management client for use with the controller using the set of login material provided -func (o *LoginOptions) NewMgmtClient() (*rest_management_api_client.ZitiEdgeManagement, error) { - httpClient := o.newHttpClient() - - c, e := rest_util.NewEdgeManagementClientWithToken(httpClient, o.ControllerUrl, o.Token) - if e != nil { - pfxlog.Logger().Fatal(e) - } - return c, nil + return rest_util.NewEdgeClientClientWithToken(&nc, o.ControllerUrl, o.Token) } // Run implements this command @@ -200,17 +204,21 @@ func (o *LoginOptions) Run() error { return cfgErr } + httpClient, newClientErr := o.newHttpClient(false) + if newClientErr != nil { + return newClientErr + } + o.client = httpClient + if o.File != "" { cfg, err := ziti.NewConfigFromFile(o.File) - if err != nil { return fmt.Errorf("could not read file %s: %w", o.File, err) } - if !o.IgnoreConfig { - idCredentials := edge_apis.NewIdentityCredentialsFromConfig(cfg.ID) - o.FileCertCreds = idCredentials - } + idCredentials := edge_apis.NewIdentityCredentialsFromConfig(cfg.ID) + o.FileCertCreds = idCredentials + ztAPI := cfg.ZtAPI // override with the first HA client API URL if defined @@ -223,15 +231,19 @@ func (o *LoginOptions) Run() error { return fmt.Errorf("could not parse ztAPI '%s' as a URL", ztAPI) } - host = parsedZtAPI.Host + if o.ControllerUrl == "" { + host = parsedZtAPI.Host + } else { + host = o.ControllerUrl + } } id := config.GetIdentity() if host == "" { - if o.ControllerUrl == "" { - if defaultId := config.EdgeIdentities[id]; defaultId != nil && !o.IgnoreConfig { - host = defaultId.Url + if strings.TrimPrefix(o.ControllerUrl, "https://") == "" { + if cachedCliConfig := config.EdgeIdentities[id]; cachedCliConfig != nil && !o.IgnoreConfig { + host = cachedCliConfig.Url o.Printf("Using controller url: %v from identity '%v' in config file: %v\n", host, id, configFile) } else { var err error @@ -247,16 +259,16 @@ func (o *LoginOptions) Run() error { } } - if !strings.HasPrefix(host, "http") { - host = "https://" + host - } + host = addHttpsIfNeeded(host) ctrlUrl, urlParseErr := url.Parse(host) if urlParseErr != nil { - return errors.Wrap(urlParseErr, "invalid controller URL") + return errors.New("invalid controller URL supplied") } - host = ctrlUrl.Scheme + "://" + ctrlUrl.Host + if ctrlUrl.Host == "" { + return errors.New("invalid controller URL supplied") + } if err := o.ConfigureCerts(host, ctrlUrl); err != nil { return err @@ -270,12 +282,12 @@ func (o *LoginOptions) Run() error { if ctrlUrl.Path == "" { if o.FileCertCreds != nil && o.FileCertCreds.CaPool != nil { - host = util.EdgeControllerGetManagementApiBasePathWithPool(host, o.FileCertCreds.CaPool) + host = util.EdgeControllerGetManagementApiBasePathWithPool(host, o.FileCertCreds.CaPool, &httpClient) } else { - host = util.EdgeControllerGetManagementApiBasePath(host, o.CaCert) + host = util.EdgeControllerGetManagementApiBasePath(host, o.CaCert, &httpClient) } - } else { - host = host + ctrlUrl.Path + hostUrl, _ := url.Parse(host) + o.ControllerUrl = o.ControllerUrl + hostUrl.Path } if o.Token != "" && o.Cmd != nil && !o.Cmd.Flag("read-only").Changed { @@ -283,86 +295,115 @@ func (o *LoginOptions) Run() error { o.Println("NOTE: When using --token the saved identity will be marked as read-only unless --read-only=false is provided") } - body := "{}" - if o.Token == "" && o.ClientCert == "" && o.ExtJwtToken == "" && o.FileCertCreds == nil { + dontHaveApiSession := !(o.ApiSession != nil && len(o.ApiSession.GetToken()) != 0) + if dontHaveApiSession && o.Token == "" && o.ClientCert == "" && o.ExtJwtToken == "" && o.FileCertCreds == nil { for o.Username == "" { - var err error - if defaultId := config.EdgeIdentities[id]; defaultId != nil && defaultId.Username != "" && !o.IgnoreConfig { - o.Username = defaultId.Username - o.Printf("Using username: %v from identity '%v' in config file: %v\n", o.Username, id, configFile) - } else if o.Username, err = term.Prompt("Enter username: "); err != nil { - return err + if xterm.IsTerminal(int(os.Stdin.Fd())) { + var err error + if cachedCliConfig := config.EdgeIdentities[id]; cachedCliConfig != nil && cachedCliConfig.Username != "" && !o.IgnoreConfig { + o.Username = cachedCliConfig.Username + o.Printf("Using username: %v from identity '%v' in config file: %v\n", o.Username, id, configFile) + } else if o.Username, err = term.Prompt("Enter username: "); err != nil { + return err + } + } else { + return errors.New("username required but not provided") } } if o.Password == "" { var err error - if o.Password, err = term.PromptPassword("Enter password: ", false); err != nil { - return err + if xterm.IsTerminal(int(os.Stdin.Fd())) { + if o.Password, err = term.PromptPassword("Enter password: ", false); err != nil { + return err + } + } else { + return errors.New("password required but not provided") } } container := gabs.New() _, _ = container.SetP(o.Username, "username") _, _ = container.SetP(o.Password, "password") - - body = container.String() } - if o.Token == "" { - jsonParsed, err := login(o, host, body) + caPool, caPoolErr := o.GetCaPool() + if caPoolErr != nil { + return caPoolErr + } else { + o.caPool = caPool + } - if err != nil { - return err - } - - if !jsonParsed.ExistsP("data.token") { - return fmt.Errorf("no session token returned from login request to %v. Received: %v", host, jsonParsed.String()) - } - - var ok bool - o.Token, ok = jsonParsed.Path("data.token").Data().(string) - - if !ok { - return fmt.Errorf("session token returned from login request to %v is not in the expected format. Received: %v", host, jsonParsed.String()) - } - - if !o.OutputJSONResponse { - o.Printf("Token: %v\n", o.Token) + t, e := o.createHttpTransport() + if e != nil { + return e + } else { + o.transport = t + nc, ncErr := o.newHttpClient(false) + if ncErr != nil { + return ncErr } + o.client = nc } o.ControllerUrl = host + + if o.Token == "" || dontHaveApiSession { + // if no token or api session, need to log in + adminClient, newMgmtClientErr := o.NewManagementClient(false) + if newMgmtClientErr != nil { + return newMgmtClientErr + } + s, le := o.Login() + if le != nil { + return le + } + if s == nil { + return fmt.Errorf("failed to login") + } else { + o.ApiSession = s + } + o.mgmtClient = adminClient + } + + var sess *edge_apis.ApiSessionJsonWrapper + if o.ApiSession != nil { + sess = &edge_apis.ApiSessionJsonWrapper{ + ApiSession: o.ApiSession, + } + } if !o.IgnoreConfig { loginIdentity := &util.RestClientEdgeIdentity{ - Url: host, - Username: o.Username, - Token: o.Token, - LoginTime: time.Now().Format(time.RFC3339), - CaCert: o.CaCert, - ReadOnly: o.ReadOnly, + Url: o.ControllerUrl, + Username: o.Username, + Token: "", // --use-api-session-- + LoginTime: time.Now().Format(time.RFC3339), + CaCert: o.CaCert, + ReadOnly: o.ReadOnly, + NetworkIdFile: o.NetworkId, + ApiSession: sess, } o.Printf("Saving identity '%v' to %v\n", id, configFile) config.EdgeIdentities[id] = loginIdentity - return util.PersistRestClientConfig(config) } return nil } func (o *LoginOptions) ConfigureCerts(host string, ctrlUrl *url.URL) error { - isServerTrusted, err := util.IsServerTrusted(host) + httpClient := o.GetClient() + isServerTrusted, err := util.IsServerTrusted(host, &httpClient) if err != nil { return err } if !isServerTrusted && o.CaCert == "" { - wellKnownCerts, certs, err := util.GetWellKnownCerts(host) + wellKnownCerts, certs, err := util.GetWellKnownCerts(host, httpClient) if err != nil { return errors.Wrapf(err, "unable to retrieve server certificate authority from %v", host) } - certsTrusted, err := util.AreCertsTrusted(host, wellKnownCerts) + certsTrusted, err := util.AreCertsTrusted(host, wellKnownCerts, httpClient) if err != nil { return err } @@ -370,7 +411,7 @@ func (o *LoginOptions) ConfigureCerts(host string, ctrlUrl *url.URL) error { return errors.New("server supplied certs not trusted by server, unable to continue") } - savedCerts, certFile, err := util.ReadCert(ctrlUrl.Hostname()) + savedCerts, certFile, err := util.ReadCert(ctrlUrl) if err != nil { return err } @@ -386,7 +427,7 @@ func (o *LoginOptions) ConfigureCerts(host string, ctrlUrl *url.URL) error { } } if replace { - _, err = util.WriteCert(o, ctrlUrl.Hostname(), wellKnownCerts) + _, err = util.WriteCert(o, ctrlUrl, wellKnownCerts) if err != nil { return err } @@ -403,7 +444,7 @@ func (o *LoginOptions) ConfigureCerts(host string, ctrlUrl *url.URL) error { } } if importCerts { - o.CaCert, err = util.WriteCert(o, ctrlUrl.Hostname(), wellKnownCerts) + o.CaCert, err = util.WriteCert(o, ctrlUrl, wellKnownCerts) if err != nil { return err } @@ -411,20 +452,15 @@ func (o *LoginOptions) ConfigureCerts(host string, ctrlUrl *url.URL) error { o.Println("WARNING: no certificate authority provided for server, continuing but login will likely fail") } } - } else if isServerTrusted && o.CaCert != "" { - override, err := o.askYesNo("Server certificate authority is already trusted. Are you sure you want to provide an additional CA [Y/N]: ") - if err != nil { - return err - } - if !override { - o.CaCert = "" - } } return nil } func (o *LoginOptions) askYesNo(prompt string) (bool, error) { + if o.Yes { + return true, nil + } filter := &yesNoFilter{} if _, err := o.ask(prompt, filter.Accept); err != nil { return false, err @@ -433,6 +469,14 @@ func (o *LoginOptions) askYesNo(prompt string) (bool, error) { } func (o *LoginOptions) ask(prompt string, f func(string) bool) (string, error) { + if o.Yes { + return "yes", nil + } + + if !xterm.IsTerminal(int(os.Stdin.Fd())) { + return "", errors.New("Cannot accept certs - no terminal") + } + for { val, err := term.Prompt(prompt) if err != nil { @@ -464,63 +508,343 @@ func (self *yesNoFilter) Accept(s string) bool { return false } -// EdgeControllerLogin will authenticate to the given Edge Controller -func login(o *LoginOptions, url string, authentication string) (*gabs.Container, error) { - client := util.NewClient() - cert := o.CaCert - out := o.Out - logJSON := o.OutputJSONResponse - timeout := o.Timeout - verbose := o.Verbose - method := "password" - if cert != "" { - client.SetRootCertificate(cert) +func (o *LoginOptions) terminatorId() string { + o.ControllerUrl = addHttpsIfNeeded(o.ControllerUrl) + curl, curle := url.Parse(o.ControllerUrl) + if curle != nil { + o.Printf("unable to parse controller url [%s]\n", o.ControllerUrl) + return "" } - authHeader := "" - if o.ExtJwtToken != "" { - method = "ext-jwt" - authHeader = "Bearer " + strings.TrimSpace(o.ExtJwtToken) - client.SetHeader("Authorization", authHeader) + return curl.User.Username() +} + +func (o *LoginOptions) createHttpTransport() (*http.Transport, error) { + // if cli param supplied - use it first + if o.NetworkId != "" { + t, e := util.NewZitifiedTransportFromFile(o.NetworkId, o.terminatorId()) + o.transport = t + return t, e + } + + // if env var set - use it + if zt, zte := util.ZitifiedTransportFromEnv(o.terminatorId()); zte != nil { + o.Printf("NetworkId found by env var [%s] but failed: %v\n", constants.ZitiCliNetworkIdVarName, zte) + return nil, zte } else { - if o.ClientCert != "" { - clientCert, err := tls.LoadX509KeyPair(o.ClientCert, o.ClientKey) - if err != nil { - return nil, fmt.Errorf("can't load client certificate: %s with key %s: %v", o.ClientCert, o.ClientKey, err) - } - client.SetCertificates(clientCert) - method = "cert" - } else if o.FileCertCreds != nil { - tlsCert := o.FileCertCreds.TlsCerts()[0] - client.SetCertificates(tlsCert) - method = "cert" + if zt != nil { + o.Printf("NetworkId found by env var [%s], zitified transport enabled\n", constants.ZitiCliNetworkIdVarName) + o.NetworkId = "" + o.transport = zt + return zt, nil } } - resp, err := client. - SetTimeout(time.Duration(timeout)*time.Second). - SetDebug(verbose). - R(). - SetQueryParam("method", method). - SetHeader("Content-Type", "application/json"). - SetBody(authentication). - Post(url + "/authenticate") - - if err != nil { - return nil, fmt.Errorf("unable to authenticate to %v. Error: %v", url, err) + caPool, caErr := o.GetCaPool() + if caErr != nil { + return nil, caErr } - - if resp.StatusCode() != http.StatusOK { - return nil, fmt.Errorf("unable to authenticate to %v. Status code: %v, Server returned: %v", url, resp.Status(), util.PrettyPrintResponse(resp)) - } - - if logJSON { - util.OutputJson(out, resp.Body()) - } - - jsonParsed, err := gabs.ParseJSON(resp.Body()) - if err != nil { - return nil, fmt.Errorf("unable to parse response from %v. Server returned: %v", url, resp.String()) - } - - return jsonParsed, nil + return &http.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: caPool, + }, + }, nil +} + +func (o *LoginOptions) PopulateFromCache() { + config, _, cfgErr := util.LoadRestClientConfig() + if cfgErr != nil { + return + } + + id := config.GetIdentity() + cachedCliConfig := config.EdgeIdentities[id] + if cachedCliConfig == nil { + return + } + if o.ControllerUrl == "" { + o.ControllerUrl = cachedCliConfig.Url + } + o.ControllerUrl = addHttpsIfNeeded(o.ControllerUrl) + if o.Username == "" { + o.Username = cachedCliConfig.Username + } + if o.Token == "" { + o.Token = cachedCliConfig.Token + } + if o.NetworkId == "" { + o.NetworkId = cachedCliConfig.NetworkIdFile + } + if o.CaCert == "" { + o.CaCert = cachedCliConfig.CaCert + } + if o.ApiSession == nil { + if cachedCliConfig.ApiSession != nil { + o.ApiSession = cachedCliConfig.ApiSession.ApiSession + } + } +} + +func NewFromCache(out io.Writer, eout io.Writer) LoginOptions { + o := LoginOptions{ + Options: api.Options{ + CommonOptions: common.CommonOptions{ + Out: out, + Err: eout, + }, + }, + } + o.PopulateFromCache() + return o +} + +func TryCachedCredsLogin(out io.Writer, eout io.Writer) (LoginOptions, error) { + o := NewFromCache(out, eout) + err := o.Run() + if err != nil { + return o, err + } else { + return o, nil + } +} + +func (o *LoginOptions) GetCaPool() (*x509.CertPool, error) { + caPool := x509.NewCertPool() + if o.CaCert != "" { + if _, cacertErr := os.Stat(o.CaCert); cacertErr == nil { + rootPemData, err := os.ReadFile(o.CaCert) + if err != nil { + pfxlog.Logger().Fatalf("error reading CA cert [%s]", o.CaCert) + } + caPool.AppendCertsFromPEM(rootPemData) + } else { + pfxlog.Logger().Warnf("CA cert not found [%s]", o.CaCert) + } + } + return caPool, nil +} + +func (o *LoginOptions) MergeUnsetFrom(cached LoginOptions) { + if o == nil { + return + } + + if o.Username == "" { + o.Username = cached.Username + } + if o.Password == "" { + o.Password = cached.Password + } + if o.Token == "" { + o.Token = cached.Token + } + if o.CaCert == "" { + o.CaCert = cached.CaCert + } + if o.ClientCert == "" { + o.ClientCert = cached.ClientCert + } + if o.ClientKey == "" { + o.ClientKey = cached.ClientKey + } + if o.ServiceName == "" { + o.ServiceName = cached.ServiceName + } + if o.extJwtFile == "" { + o.extJwtFile = cached.extJwtFile + } + if o.ExtJwtToken == "" { + o.ExtJwtToken = cached.ExtJwtToken + } + if o.File == "" { + o.File = cached.File + } + if o.ControllerUrl == "" { + o.ControllerUrl = cached.ControllerUrl + } + if o.NetworkId == "" { + o.NetworkId = cached.NetworkId + } + if o.FileCertCreds == nil { + o.FileCertCreds = cached.FileCertCreds + } + if o.ApiSession == nil { + o.ApiSession = cached.ApiSession + } + if o.transport == nil { + o.transport = cached.transport + } + if o.caPool == nil { + o.caPool = cached.caPool + } + if o.cachedId == nil { + o.cachedId = cached.cachedId + } + if o.mgmtClient == nil { + o.mgmtClient = cached.mgmtClient + } + if o.TotpCallback == nil { + o.TotpCallback = cached.TotpCallback + } + o.client = cached.client +} + +func (o *LoginOptions) NewManagementClient(useCachedCreds bool) (*edge_apis.ManagementApiClient, error) { + if useCachedCreds { + o.PopulateFromCache() + // any error indicates there are probably no saved credentials. look for login information and use those + cached := *o + cached.IgnoreConfig = true // don't overwrite when trying to login + loginErr := cached.Run() + if loginErr != nil { + return nil, loginErr + } + o.MergeUnsetFrom(cached) + _, sessErr := o.mgmtClient.AuthenticateWithPreviousSession(&edge_apis.EmptyCredentials{}, o.ApiSession) + if sessErr != nil { + return nil, sessErr + } + return o.mgmtClient, nil + } + + o.ControllerUrl = addHttpsIfNeeded(o.ControllerUrl) + ctrlUrl, _ := url.Parse(o.ControllerUrl) + if ctrlUrl.Path == "" { + resolvedUrl := util.EdgeControllerGetManagementApiBasePath(o.ControllerUrl, o.CaCert, &o.client) + hostUrl, _ := url.Parse(resolvedUrl) + o.ControllerUrl = o.ControllerUrl + hostUrl.Path + } + transport := &edge_apis.TlsAwareHttpTransport{Transport: o.transport} + o.client.Transport = transport + + o.mgmtClient = edge_apis.NewManagementApiClientWithConfig(&edge_apis.ApiClientConfig{ + ApiUrls: []*url.URL{ctrlUrl}, + CaPool: o.caPool, + TotpCodeProvider: edge_apis.NewTotpCodeProviderFromChStringFunc(o.TotpCallback), + Components: &edge_apis.Components{ + HttpClient: &o.client, + TlsAwareTransport: transport, + CaPool: o.caPool, + }, + }) + + o.mgmtClient.SetAllowOidcDynamicallyEnabled(true) + + return o.mgmtClient, nil +} + +func (o *LoginOptions) Login() (edge_apis.ApiSession, error) { + var authCreds edge_apis.Credentials + if o.Token != "" { + if jwtutil.IsJwt(o.Token) { + return edge_apis.NewApiSessionOidc(o.Token, ""), nil + } else { + return edge_apis.NewApiSessionLegacy(o.Token), nil + } + } else if o.ApiSession != nil { + return o.ApiSession, nil + } else if o.Username != "" && o.Password != "" { + authCreds = edge_apis.NewUpdbCredentials(o.Username, o.Password) + } else if o.ClientCert != "" || o.ClientKey != "" { + var key crypto.PrivateKey + if keyPEM, err := os.ReadFile(o.ClientKey); err != nil { + return nil, fmt.Errorf("failed to read key: %w", err) + } else { + keyBlock, _ := pem.Decode(keyPEM) + k, _ := x509.ParsePKCS1PrivateKey(keyBlock.Bytes) + key = k + } + + var cert *x509.Certificate + if certPEM, err := os.ReadFile(o.ClientCert); err != nil { + return nil, fmt.Errorf("failed to read cert: %w", err) + } else { + block, _ := pem.Decode(certPEM) + if block == nil { + return nil, fmt.Errorf("invalid cert pem") + } + c, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, fmt.Errorf("failed to parse cert: %w", err) + } + cert = c + } + + authCreds = edge_apis.NewCertCredentials([]*x509.Certificate{cert}, key) + } else if o.File != "" { + cfg, err := ziti.NewConfigFromFile(o.File) + if err != nil { + return nil, err + } + authCreds = edge_apis.NewIdentityCredentialsFromConfig(cfg.ID) + } else if o.extJwtFile != "" { + jwt, jwtErr := os.ReadFile(o.extJwtFile) + if jwtErr != nil { + return nil, fmt.Errorf("failed to read jwt file: %w", jwtErr) + } + authCreds = edge_apis.NewJwtCredentials(string(jwt)) + } + + s, err := o.mgmtClient.Authenticate(authCreds, nil) + if err != nil { + return nil, err + } + if s == nil { + return nil, fmt.Errorf("authentication failed for some reason but no error") + } + + return s, nil +} + +// EffectiveUrl will take all inputs and return the expected url of the controller or prompt the user to enter the url +// to use if insufficient information exists in the inputs provided +func (o *LoginOptions) EffectiveUrl() (string, error) { + // if provided use the url provided + if o.ControllerUrl != "" { + return addHttpsIfNeeded(o.ControllerUrl), nil + } + + // if using file-based auth and --file is provided, look into the file for the url + if o.File != "" { + return o.UrlFromFile() + } + + // if a cached id exists - use the url from that + if o.cachedId != nil { + return addHttpsIfNeeded(o.cachedId.Url), nil + } + + return "", nil +} + +func addHttpsIfNeeded(host string) string { + if !strings.HasPrefix(host, "http") { + host = "https://" + host + } + return host +} + +func (o *LoginOptions) UrlFromFile() (string, error) { + cfg, err := ziti.NewConfigFromFile(o.File) + if err != nil { + return "", fmt.Errorf("could not read file %s: %w", o.File, err) + } + + if o.FileCertCreds == nil { + idCredentials := edge_apis.NewIdentityCredentialsFromConfig(cfg.ID) + o.FileCertCreds = idCredentials + } + + ztAPI := cfg.ZtAPI + + // override with the first HA client API URL if defined + if len(cfg.ZtAPIs) > 0 { + ztAPI = cfg.ZtAPIs[0] + } + + parsedZtAPI, err := url.Parse(ztAPI) + if err != nil { + return "", fmt.Errorf("could not parse ztAPI '%s' as a URL", ztAPI) + } + + return addHttpsIfNeeded(parsedZtAPI.Host), nil } diff --git a/ziti/cmd/edge/login_test.go b/ziti/cmd/edge/login_test.go deleted file mode 100644 index f2c572d54..000000000 --- a/ziti/cmd/edge/login_test.go +++ /dev/null @@ -1 +0,0 @@ -package edge diff --git a/ziti/cmd/ops/verify/ext-jwt-signer/oidc/oidc.go b/ziti/cmd/ops/verify/ext-jwt-signer/oidc/oidc.go index 5cfd29597..225e55a28 100644 --- a/ziti/cmd/ops/verify/ext-jwt-signer/oidc/oidc.go +++ b/ziti/cmd/ops/verify/ext-jwt-signer/oidc/oidc.go @@ -23,15 +23,16 @@ import ( "encoding/json" "errors" "fmt" - "github.com/openziti/edge-api/rest_model" - "github.com/openziti/ziti/ziti/cmd/api" - "github.com/openziti/ziti/ziti/cmd/common" "io" "net/http" "net/url" "strings" "time" + "github.com/openziti/edge-api/rest_model" + "github.com/openziti/ziti/ziti/cmd/api" + "github.com/openziti/ziti/ziti/cmd/common" + "github.com/fatih/color" "github.com/google/uuid" "github.com/gorilla/securecookie" @@ -446,7 +447,7 @@ func NewOidcVerificationCmd(out io.Writer, errOut io.Writer, initialContext cont cmd.Flags().BoolVar(&opts.showRefreshToken, "refresh-token", false, "Display the full Refresh Token to the screen. Use caution.") cmd.Flags().BoolVar(&opts.showAccessToken, "access-token", false, "Display the full Access Token to the screen. Use caution.") cmd.Flags().StringVar(&opts.ControllerUrl, "controller-url", "", "The url of the controller") - cmd.Flags().StringSliceVarP(&opts.additionalScopes, "additional-scopes", "s", []string{}, "List of additional scopes to add") + cmd.Flags().StringSliceVarP(&opts.additionalScopes, "additional-scopes", "a", []string{}, "List of additional scopes to add") cmd.Flags().BoolVar(&opts.attemptAuth, "authenticate", false, "Also attempt to authenticate using the supplied ext-jwt-signer") cmd.Flags().StringVarP(&opts.RedirectURL, "redirect-url", "r", "http://localhost:20314/auth/callback", "The expected redirect URL to listen to") diff --git a/ziti/cmd/ops/verify/ops_verify_traffic.go b/ziti/cmd/ops/verify/ops_verify_traffic.go index 4c92cffdd..9a5863eb5 100644 --- a/ziti/cmd/ops/verify/ops_verify_traffic.go +++ b/ziti/cmd/ops/verify/ops_verify_traffic.go @@ -66,7 +66,7 @@ func NewVerifyTraffic(out io.Writer, errOut io.Writer) *cobra.Command { Use: "traffic", Short: "Verifies traffic", Long: "A tool to verify traffic can flow over the overlay properly. You must be authenticated to use this tool.", - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { logLvl := logrus.InfoLevel if t.verbose { logLvl = logrus.DebugLevel @@ -98,11 +98,11 @@ func NewVerifyTraffic(out io.Writer, errOut io.Writer) *cobra.Command { t.bindSPName = t.prefix + ".bind" t.dialSPName = t.prefix + ".dial" - var err error - t.client, err = t.loginOpts.NewMgmtClient() - if err != nil { - log.Fatal(err) + mgmtClient, mgmtClientErr := t.loginOpts.NewManagementClient(true) + if mgmtClientErr != nil { + return mgmtClientErr } + t.client = mgmtClient.BaseClient.API.ZitiEdgeManagement if t.cleanup { log.Info("attempting to cleanup based on parameters. this operation will disconnect the server if it's running.") @@ -121,6 +121,8 @@ func NewVerifyTraffic(out io.Writer, errOut io.Writer) *cobra.Command { } else { log.Fatal("no role supplied? should have defaulted to 'both'") } + + return nil }, } diff --git a/ziti/constants/constants.go b/ziti/constants/constants.go index 8f8951b73..ced2c34e9 100644 --- a/ziti/constants/constants.go +++ b/ziti/constants/constants.go @@ -141,4 +141,7 @@ const ( ZitiEdgeRouterCsrOUVarDescription = "The organization unit to use for router CSRs" ZitiRouterCsrSansDnsVarName = "ZITI_ROUTER_CSR_SANS_DNS" ZitiRouterCsrSansDnsVarDescription = "Additional DNS SAN of the router" + + ZitiCliNetworkIdVarName = "ZITI_CLI_NETWORK_ID" + ZitiCliNetworkIdVarDescription = "Necessary when using the CLI over a zitified transport" ) diff --git a/ziti/run/quickstart.go b/ziti/run/quickstart.go index ddff0374c..ee109fb3a 100644 --- a/ziti/run/quickstart.go +++ b/ziti/run/quickstart.go @@ -20,8 +20,6 @@ import ( "context" "crypto/tls" "fmt" - "github.com/openziti/ziti/ziti/cmd/edge" - "github.com/openziti/ziti/ziti/enroll" "io" "net" "net/http" @@ -35,6 +33,9 @@ import ( "syscall" "time" + "github.com/openziti/ziti/ziti/cmd/edge" + "github.com/openziti/ziti/ziti/enroll" + "github.com/google/uuid" "github.com/michaelquigley/pfxlog" "github.com/openziti/ziti/common/version" @@ -65,13 +66,16 @@ type QuickstartOpts struct { errOut io.Writer cleanOnExit bool TrustDomain string - isHA bool + IsHA bool InstanceID string ClusterMember string - joinCommand bool - verbose bool - nonVoter bool - routerless bool + Routerless bool + ConfigureAndExit bool + ConfigFile string + + joinCommand bool + verbose bool + nonVoter bool } func addCommonQuickstartFlags(cmd *cobra.Command, options *QuickstartOpts) { @@ -91,9 +95,10 @@ func addCommonQuickstartFlags(cmd *cobra.Command, options *QuickstartOpts) { cmd.Flags().Uint16Var(&options.ControllerPort, "ctrl-port", uint16(defaultCtrlPort), "sets the port to use for the control plane and API. current: "+currentCtrlPort) cmd.Flags().StringVar(&options.RouterAddress, "router-address", "", "sets the advertised address for the integrated router. current: "+currentRouterAddy) cmd.Flags().Uint16Var(&options.RouterPort, "router-port", uint16(defaultRouterPort), "sets the port to use for the integrated router. current: "+currentRouterPort) - cmd.Flags().BoolVar(&options.routerless, "no-router", false, "specifies the quickstart should not start a router") + cmd.Flags().BoolVar(&options.Routerless, "no-router", false, "specifies the quickstart should not start a router") cmd.Flags().BoolVar(&options.verbose, "verbose", false, "Show additional output.") + cmd.Flags().BoolVar(&options.ConfigureAndExit, "configure-and-exit", false, "Configures everything and then exits gracefully") } func addQuickstartHaFlags(cmd *cobra.Command, options *QuickstartOpts) { @@ -108,15 +113,12 @@ func NewQuickStartCmd(out io.Writer, errOut io.Writer, context context.Context) Use: "quickstart", Short: "runs a Controller and Router in quickstart mode", Long: "runs a Controller and Router in quickstart mode with a temporary directory; suitable for testing and development", - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { options.out = out options.errOut = errOut options.TrustDomain = "quickstart" options.InstanceID = "quickstart" - err := options.run(context) - if err != nil { - logrus.Fatal(err) - } + return options.run(context) }, } addCommonQuickstartFlags(cmd, options) @@ -134,7 +136,7 @@ func NewQuickStartHaCmd(out io.Writer, errOut io.Writer, context context.Context Run: func(cmd *cobra.Command, args []string) { options.out = out options.errOut = errOut - options.isHA = true + options.IsHA = true if options.TrustDomain == "" { options.TrustDomain = uuid.New().String() fmt.Println("Trust domain was not supplied. Using a random trust domain: " + options.TrustDomain) @@ -175,7 +177,7 @@ func NewQuickStartJoinClusterCmd(out io.Writer, errOut io.Writer, context contex } func (o *QuickstartOpts) cleanupHome() { - if o.cleanOnExit { + if o.cleanOnExit && !o.ConfigureAndExit { fmt.Println("Removing temp directory at: " + o.Home) _ = os.RemoveAll(o.Home) } else { @@ -195,7 +197,7 @@ func (o *QuickstartOpts) join(ctx context.Context) error { logrus.Fatalf("--cluster-member is required") } - o.isHA = true + o.IsHA = true o.joinCommand = true return o.run(ctx) } @@ -250,7 +252,7 @@ func (o *QuickstartOpts) run(ctx context.Context) error { o.InstanceID = uuid.New().String() } - ctrlYaml := path.Join(o.instHome(), "ctrl.yaml") + o.ConfigFile = path.Join(o.instHome(), "ctrl.yaml") routerName := "router-" + o.InstanceID //ZITI_HOME=/tmp ziti create config controller | grep -v "#" | sed -E 's/^ *$//g' | sed '/^$/d' @@ -280,14 +282,14 @@ func (o *QuickstartOpts) run(ctx context.Context) error { _ = os.MkdirAll(dbDir, 0o700) logrus.Debugf("made directory '%s'", dbDir) - o.createMinimalPki() + o.CreateMinimalPki() _ = os.Setenv("ZITI_HOME", o.instHome()) ctrl := create.NewCmdCreateConfigController() args := []string{ - fmt.Sprintf("--output=%s", ctrlYaml), + fmt.Sprintf("--output=%s", o.ConfigFile), } - if o.isHA { + if o.IsHA { args = append(args, "--clustered") } ctrl.SetArgs(args) @@ -296,12 +298,12 @@ func (o *QuickstartOpts) run(ctx context.Context) error { logrus.Fatal(err) } - if !o.isHA { + if !o.IsHA { initCmd := edgeSubCmd.NewEdgeInitializeCmd(version.GetCmdBuildInfo()) initCmd.SetArgs([]string{ fmt.Sprintf("--username=%s", o.Username), fmt.Sprintf("--password=%s", o.Password), - ctrlYaml, + o.ConfigFile, }) initErr := initCmd.Execute() if initErr != nil { @@ -310,12 +312,14 @@ func (o *QuickstartOpts) run(ctx context.Context) error { } } + curCtx, cancel := context.WithCancel(ctx) //used to cancel controller and router when configure-and-exit is selected fmt.Println("Starting controller...") go func() { runCtrl := NewRunControllerCmd() runCtrl.SetArgs([]string{ - ctrlYaml, + o.ConfigFile, }) + runCtrl.SetContext(curCtx) runCtrlErr := runCtrl.Execute() if runCtrlErr != nil { logrus.Fatal(runCtrlErr) @@ -323,23 +327,34 @@ func (o *QuickstartOpts) run(ctx context.Context) error { }() fmt.Println("Controller running...") - ctrlAddy := helpers.GetCtrlEdgeAdvertisedAddress() - ctrlPort := helpers.GetCtrlEdgeAdvertisedPort() - ctrlUrl := fmt.Sprintf("https://%s:%s", ctrlAddy, ctrlPort) + o.ControllerAddress = helpers.GetCtrlEdgeAdvertisedAddress() + + portStr := helpers.GetCtrlEdgeAdvertisedPort() + port, portErr := strconv.Atoi(portStr) + if portErr != nil { + cancel() + return fmt.Errorf("invalid controller port: %s", portStr) + } + o.ControllerPort = uint16(port) c := make(chan error) - timeout, _ := time.ParseDuration("30s") - go waitForController(ctrlUrl, c) + timeout := 30 * time.Second + + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) + defer stop() + go waitForController(ctx, o.ControllerHostPort(), c) + select { case <-c: //completed normally logrus.Info("Controller online. Continuing...") case <-time.After(timeout): o.cleanupHome() - return fmt.Errorf("timed out waiting for controller: %s", ctrlUrl) + cancel() + return fmt.Errorf("timed out waiting for controller: %s", o.ControllerHostPort()) } - if o.isHA { + if o.IsHA { p := common.NewOptionsProvider(o.out, o.errOut) fmt.Println("waiting three seconds for controller to become ready...") @@ -364,6 +379,7 @@ func (o *QuickstartOpts) run(ctx context.Context) error { time.Sleep(2 * time.Second) // Wait before retrying } else { fmt.Println("Max retries reached. Failing.") + cancel() return agentInitErr } } else { @@ -397,14 +413,16 @@ func (o *QuickstartOpts) run(ctx context.Context) error { logrus.Info("Add command successful. continuing...") case <-time.After(addTimeout): o.cleanupHome() + cancel() return fmt.Errorf("timed out adding to cluster") } } } erConfigFile := path.Join(o.instHome(), routerName+".yaml") - err := o.configureRouter(routerName, erConfigFile, ctrlUrl) + err := o.configureRouter(routerName, erConfigFile, o.ControllerHostPort()) if err != nil { + cancel() return err } o.runRouter(erConfigFile) @@ -412,21 +430,26 @@ func (o *QuickstartOpts) run(ctx context.Context) error { ch := make(chan os.Signal, 1) signal.Notify(ch, os.Interrupt, syscall.SIGQUIT, syscall.SIGINT, syscall.SIGTERM) - if !o.routerless { - r := make(chan struct{}) + if !o.Routerless { + r := make(chan error) timeout, _ = time.ParseDuration("30s") logrus.Infof("waiting for router at: %s:%d", o.RouterAddress, o.RouterPort) - go waitForRouter(o.RouterAddress, o.RouterPort, r) + go o.WaitForRouter(timeout, r) select { - case <-r: - //completed normally + case waitErr := <-r: + if waitErr != nil { + o.cleanupHome() + cancel() + return fmt.Errorf("router failed: %w", waitErr) + } case <-time.After(timeout): o.cleanupHome() + cancel() return fmt.Errorf("timed out waiting for router on port: %d", o.RouterPort) } } - if o.isHA { + if o.IsHA { go func() { time.Sleep(3 * time.Second) // output this after a bit... nextInstId := incrementStringSuffix(o.InstanceID) @@ -439,7 +462,7 @@ func (o *QuickstartOpts) run(ctx context.Context) error { fmt.Printf(" --router-port %d \\\n", o.RouterPort+1) fmt.Printf(" --home \"%s\" \\\n", o.Home) fmt.Printf(" --trust-domain=\"%s\" \\\n", o.TrustDomain) - fmt.Printf(" --cluster-member tls:%s:%s\\ \n", ctrlAddy, ctrlPort) + fmt.Printf(" --cluster-member tls:%s:%d\\ \n", o.ControllerAddress, o.ControllerPort) fmt.Printf(" --instance-id \"%s\"\n", nextInstId) fmt.Println("=======================================================================================") fmt.Println() @@ -458,6 +481,7 @@ func (o *QuickstartOpts) run(ctx context.Context) error { } o.cleanupHome() + cancel() return nil } @@ -472,14 +496,14 @@ func (o *QuickstartOpts) printDetails() { } func (o *QuickstartOpts) configureRouter(routerName string, configFile string, ctrlUrl string) error { - if o.routerless { + if o.Routerless { return nil } if !o.AlreadyInitialized { loginCmd := edge.NewLoginCmd(o.out, o.errOut) loginCmd.SetArgs([]string{ - ctrlUrl, + o.ControllerHostPort(), fmt.Sprintf("--username=%s", o.Username), fmt.Sprintf("--password=%s", o.Password), "-y", @@ -520,7 +544,7 @@ func (o *QuickstartOpts) configureRouter(routerName string, configFile string, c data := &create.ConfigTemplateValues{} data.PopulateConfigValues() - opts.IsHA = o.isHA + opts.IsHA = o.IsHA create.SetZitiRouterIdentity(&data.Router, routerName) erCfg := create.NewCmdCreateConfigRouterEdge(opts, data) erCfg.SetArgs([]string{ @@ -551,7 +575,7 @@ func (o *QuickstartOpts) configureRouter(routerName string, configFile string, c } func (o *QuickstartOpts) runRouter(configFile string) { - if o.routerless { + if o.Routerless { return } @@ -570,7 +594,7 @@ func (o *QuickstartOpts) runRouter(configFile string) { }() } -func (o *QuickstartOpts) createMinimalPki() { +func (o *QuickstartOpts) CreateMinimalPki() { where := path.Join(o.Home, "pki") fmt.Println("emitting a minimal PKI") @@ -660,31 +684,50 @@ func (o *QuickstartOpts) createMinimalPki() { } } -func waitForController(ctrlUrl string, done chan error) { +func waitForController(ctx context.Context, ctrlUrl string, done chan error) { + if !strings.HasPrefix(ctrlUrl, "https://") { + ctrlUrl = "https://" + ctrlUrl + } tr := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} client := &http.Client{Transport: tr} + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + for { - r, e := client.Get(ctrlUrl) - if e != nil || r == nil || r.StatusCode != 200 { + select { + case <-ctx.Done(): + done <- ctx.Err() + return + case <-ticker.C: + fmt.Printf("waiting for controller: %s\n", ctrlUrl) + default: + r, e := client.Get(ctrlUrl) + if e == nil && r != nil && r.StatusCode == 200 { + done <- nil + return + } time.Sleep(50 * time.Millisecond) - } else { - break } } - done <- nil } -func waitForRouter(address string, port uint16, done chan struct{}) { +func (o *QuickstartOpts) WaitForRouter(timeout time.Duration, done chan error) { for { - addr := net.JoinHostPort(address, strconv.Itoa(int(port))) - conn, err := net.DialTimeout("tcp", addr, 2*time.Second) - if err == nil { - _ = conn.Close() - fmt.Printf("Router is available on %s\n", addr) - close(done) + select { + case <-time.After(timeout): + done <- fmt.Errorf("router not available after %s at %s:%d", timeout, o.RouterAddress, o.RouterPort) return + default: + addr := net.JoinHostPort(o.RouterAddress, strconv.Itoa(int(o.RouterPort))) + conn, err := net.DialTimeout("tcp", addr, 2*time.Second) + if err == nil { + _ = conn.Close() + fmt.Printf("Router is available on %s:%d\n", o.RouterAddress, o.RouterPort) + done <- nil + return + } + time.Sleep(25 * time.Millisecond) } - time.Sleep(25 * time.Millisecond) } } @@ -700,7 +743,7 @@ func (o *QuickstartOpts) scopedName(name string) string { } func (o *QuickstartOpts) instHome() string { - if o.isHA { + if o.IsHA { return path.Join(o.Home, o.InstanceID) } return o.Home @@ -789,3 +832,54 @@ func incrementStringSuffix(input string) string { return strings.TrimSuffix(input, numStr) + incremented } + +func (o *QuickstartOpts) InitHA(p common.OptionsProvider) error { + maxRetries := 5 + for attempt := 1; attempt <= maxRetries; attempt++ { + fmt.Printf("initializing controller at port: %d\n", o.ControllerPort) + agentInitCmd := agentcli.NewAgentClusterInit(p) + pid := os.Getpid() + args := []string{ + o.Username, + o.Password, + o.Username, + fmt.Sprintf("--pid=%d", pid), + } + agentInitCmd.SetArgs(args) + + agentInitErr := agentInitCmd.Execute() + if agentInitErr != nil { + if attempt < maxRetries { + fmt.Println("initialization failed. waiting two seconds and trying again") + time.Sleep(2 * time.Second) // Wait before retrying + } else { + fmt.Println("Max retries reached. Failing.") + return agentInitErr + } + } else { + break + } + } + return nil +} + +func (o *QuickstartOpts) InitLegacy() error { + initCmd := edgeSubCmd.NewEdgeInitializeCmd(version.GetCmdBuildInfo()) + initCmd.SetArgs([]string{ + fmt.Sprintf("--username=%s", o.Username), + fmt.Sprintf("--password=%s", o.Password), + o.ConfigFile, + }) + initErr := initCmd.Execute() + if initErr != nil { + return initErr + } + return nil +} + +func (o *QuickstartOpts) ControllerHostPort() string { + return net.JoinHostPort(o.ControllerAddress, strconv.Itoa(int(o.ControllerPort))) +} +func (o *QuickstartOpts) RouterHostPort() string { + return net.JoinHostPort(o.RouterAddress, strconv.Itoa(int(o.RouterPort))) +} diff --git a/ziti/run/run_controller.go b/ziti/run/run_controller.go index c62b2e89d..d2b1e7130 100644 --- a/ziti/run/run_controller.go +++ b/ziti/run/run_controller.go @@ -17,13 +17,15 @@ package run import ( + "context" "fmt" - "github.com/openziti/ziti/controller/config" "net" "os" "os/signal" "syscall" + "github.com/openziti/ziti/controller/config" + "github.com/michaelquigley/pfxlog" "github.com/openziti/agent" "github.com/openziti/ziti/common/version" @@ -100,7 +102,7 @@ func (self *ControllerAction) Run(cmd *cobra.Command, args []string) { } } - go self.waitForShutdown() + go self.waitForShutdown(cmd.Context()) self.edgeController.Run() if err := self.fabricController.Run(); err != nil { @@ -108,13 +110,22 @@ func (self *ControllerAction) Run(cmd *cobra.Command, args []string) { } } -func (self *ControllerAction) waitForShutdown() { +func (self *ControllerAction) waitForShutdown(ctx context.Context) { ch := make(chan os.Signal, 1) signal.Notify(ch, os.Interrupt, syscall.SIGTERM) + defer signal.Stop(ch) - <-ch + pfxlog.Logger().Info("waiting for shutdown signal or context cancel") + + select { + case sig := <-ch: + pfxlog.Logger().Infof("received signal: %v", sig) + case <-ctx.Done(): + pfxlog.Logger().Info("context cancelled, shutting down") + } - pfxlog.Logger().Info("shutting down ziti-controller") self.edgeController.Shutdown() self.fabricController.Shutdown() + + pfxlog.Logger().Info("shutdown complete") } diff --git a/ziti/run/run_router.go b/ziti/run/run_router.go index 6ffdbd638..c385cfc9f 100644 --- a/ziti/run/run_router.go +++ b/ziti/run/run_router.go @@ -85,7 +85,7 @@ func (self *RouterAction) Run(cmd *cobra.Command, args []string) { r.RunCliAgent(self.CliAgentAddr, self.CliAgentAlias) } - go r.ListenForShutdownSignal() + go r.ListenForShutdownSignal(cmd.Context()) if err = r.Run(); err != nil { logrus.WithError(err).Fatal("error starting") diff --git a/ziti/util/certs.go b/ziti/util/certs.go index 7398b9e6b..74707fce8 100644 --- a/ziti/util/certs.go +++ b/ziti/util/certs.go @@ -7,7 +7,6 @@ import ( "encoding/base64" "encoding/pem" "fmt" - "github.com/openziti/ziti/ziti/cmd/common" "io" "net/http" "net/url" @@ -17,11 +16,26 @@ import ( "sort" "strings" + "github.com/openziti/ziti/ziti/cmd/common" + "github.com/fullsailor/pkcs7" "github.com/pkg/errors" ) -func WriteCert(p common.Printer, id string, cert []byte) (string, error) { +func urlToId(url *url.URL) string { + p := url.Port() + if p == "" { + if url.Scheme == "https" { + p = "443" + } else { + p = "80" + } + } + return url.Hostname() + "_" + p +} + +func WriteCert(p common.Printer, url *url.URL, cert []byte) (string, error) { + id := urlToId(url) cfgDir, err := ConfigDir() if err != nil { return "", err @@ -38,7 +52,8 @@ func WriteCert(p common.Printer, id string, cert []byte) (string, error) { return certFile, nil } -func ReadCert(id string) ([]byte, string, error) { +func ReadCert(url *url.URL) ([]byte, string, error) { + id := urlToId(url) cfgDir, err := ConfigDir() if err != nil { return nil, "", errors.Wrapf(err, "couldn't get config dir while reading cert for %v", id) @@ -58,8 +73,11 @@ func ReadCert(id string) ([]byte, string, error) { return result, certFile, nil } -func IsServerTrusted(host string) (bool, error) { - resp, err := http.DefaultClient.Get(fmt.Sprintf("%v/.well-known/est/cacerts", host)) +func IsServerTrusted(host string, client *http.Client) (bool, error) { + if client == nil { + client = http.DefaultClient + } + resp, err := client.Get(fmt.Sprintf("%v/.well-known/est/cacerts", host)) if err != nil { if ue, ok := err.(*url.Error); ok && (errors.As(ue.Err, &x509.UnknownAuthorityError{}) || strings.Contains(err.Error(), "x509")) { return false, nil @@ -70,25 +88,10 @@ func IsServerTrusted(host string) (bool, error) { return true, nil } -func AreCertsTrusted(host string, certs []byte) (bool, error) { - tlsConfig := &tls.Config{ - InsecureSkipVerify: true, - } +func AreCertsTrusted(host string, certs []byte, client http.Client) (bool, error) { + c := InsecureClient(&client, certs) - if tlsConfig.RootCAs == nil { - tlsConfig.RootCAs = x509.NewCertPool() - } - - tlsConfig.RootCAs.AppendCertsFromPEM(certs) - - transport := http.DefaultTransport.(*http.Transport).Clone() - transport.TLSClientConfig = tlsConfig - - client := http.Client{ - Transport: transport, - } - - resp, err := client.Get(fmt.Sprintf("%v/.well-known/est/cacerts", host)) + resp, err := c.Get(fmt.Sprintf("%v/.well-known/est/cacerts", host)) if err != nil { if ue, ok := err.(*url.Error); ok && errors.As(ue.Err, &x509.UnknownAuthorityError{}) { return false, nil @@ -99,16 +102,10 @@ func AreCertsTrusted(host string, certs []byte) (bool, error) { return true, nil } -func GetWellKnownCerts(host string) ([]byte, []*x509.Certificate, error) { - transport := http.DefaultTransport.(*http.Transport).Clone() - transport.TLSClientConfig = &tls.Config{ - InsecureSkipVerify: true, - } - client := http.Client{ - Transport: transport, - } +func GetWellKnownCerts(host string, client http.Client) ([]byte, []*x509.Certificate, error) { + c := InsecureClient(&client, nil) - resp, err := client.Get(fmt.Sprintf("%v/.well-known/est/cacerts", host)) + resp, err := c.Get(fmt.Sprintf("%v/.well-known/est/cacerts", host)) if err != nil { return nil, nil, err } @@ -198,3 +195,27 @@ func (self blockSort) Swap(i, j int) { self[i] = self[j] self[j] = tmp } + +func InsecureClient(client *http.Client, certs []byte) *http.Client { + tlsConfig := &tls.Config{ + RootCAs: x509.NewCertPool(), + InsecureSkipVerify: true, + } + + if len(certs) > 0 { + tlsConfig.RootCAs.AppendCertsFromPEM(certs) + } + + var t *http.Transport + if origTransport, ok := client.Transport.(*http.Transport); ok { + t = origTransport.Clone() + t.TLSClientConfig = tlsConfig + } else { + t = &http.Transport{TLSClientConfig: tlsConfig} + } + + c := &http.Client{ + Transport: t, + } + return c +} diff --git a/ziti/util/identities.go b/ziti/util/identities.go index 0fafbe28e..33ce68cda 100644 --- a/ziti/util/identities.go +++ b/ziti/util/identities.go @@ -17,13 +17,22 @@ import ( httptransport "github.com/go-openapi/runtime/client" "github.com/openziti/edge-api/rest_management_api_client" + edge_apis "github.com/openziti/sdk-golang/edge-apis" + "github.com/openziti/sdk-golang/ziti" "github.com/openziti/ziti/controller/env" fabric_rest_client "github.com/openziti/ziti/controller/rest_client" "github.com/openziti/ziti/ziti/cmd/common" + "github.com/openziti/ziti/ziti/constants" "github.com/pkg/errors" "gopkg.in/resty.v1" ) +var zitiCliContextCollection *ziti.CtxCollection + +func init() { + zitiCliContextCollection = ziti.NewSdkCollection() +} + type API string const ( @@ -50,6 +59,7 @@ func (self *RestClientConfig) GetIdentity() string { type RestClientIdentity interface { NewTlsClientConfig() (*tls.Config, error) NewClient(timeout time.Duration, verbose bool) (*resty.Client, error) + NewClientByTerminator(timeout time.Duration, verbose bool, terminator string) (*resty.Client, error) NewRequest(client *resty.Client) *resty.Request IsReadOnly() bool GetBaseUrlForApi(api API) (string, error) @@ -59,7 +69,11 @@ type RestClientIdentity interface { } func NewRequest(restClientIdentity RestClientIdentity, timeoutInSeconds int, verbose bool) (*resty.Request, error) { - client, err := restClientIdentity.NewClient(time.Duration(timeoutInSeconds)*time.Second, verbose) + return NewRequestByTerminator(restClientIdentity, timeoutInSeconds, verbose, "") +} + +func NewRequestByTerminator(restClientIdentity RestClientIdentity, timeoutInSeconds int, verbose bool, terminator string) (*resty.Request, error) { + client, err := restClientIdentity.NewClientByTerminator(time.Duration(timeoutInSeconds)*time.Second, verbose, terminator) if err != nil { return nil, err } @@ -67,12 +81,14 @@ func NewRequest(restClientIdentity RestClientIdentity, timeoutInSeconds int, ver } type RestClientEdgeIdentity struct { - Url string `json:"url"` - Username string `json:"username"` - Token string `json:"token"` - LoginTime string `json:"loginTime"` - CaCert string `json:"caCert,omitempty"` - ReadOnly bool `json:"readOnly"` + Url string `json:"url"` + Username string `json:"username"` + Token string `json:"token"` + LoginTime string `json:"loginTime"` + CaCert string `json:"caCert,omitempty"` + ReadOnly bool `json:"readOnly"` + NetworkIdFile string `json:"networkId"` + ApiSession *edge_apis.ApiSessionJsonWrapper `json:"apiSession"` } func (self *RestClientEdgeIdentity) IsReadOnly() bool { @@ -103,7 +119,40 @@ func (self *RestClientEdgeIdentity) NewTlsClientConfig() (*tls.Config, error) { } func (self *RestClientEdgeIdentity) NewClient(timeout time.Duration, verbose bool) (*resty.Client, error) { + return self.NewClientByTerminator(timeout, verbose, "") +} + +func (self *RestClientEdgeIdentity) NewClientByTerminator(timeout time.Duration, verbose bool, terminator string) (*resty.Client, error) { client := NewClient() + if ztFromEnv, ztFromEnvErr := ZitifiedTransportFromEnv(""); ztFromEnvErr != nil { + return nil, ztFromEnvErr + } else { + if ztFromEnv != nil { + if verbose { + client.Log.Printf("Using Ziti Transport from environment var: %s", constants.ZitiCliNetworkIdVarName) + } + client.GetClient().Transport = ztFromEnv + } else { + if self.NetworkIdFile != "" { + if ztFromFile, ztFromFileErr := NewZitifiedTransportFromFile(self.NetworkIdFile, terminator); ztFromFileErr != nil { + // ignore any error around the networkId file + if verbose { + client.Log.Printf("Ziti transport from cached file failed: %v", ztFromFileErr) + } + } else { + if verbose { + client.Log.Printf("Using Ziti transport from cached file: %s", self.NetworkIdFile) + } + client.GetClient().Transport = ztFromFile + } + } else { + if verbose { + client.Log.Printf("Using default http transport") + } + } + } + } + if self.CaCert != "" { client.SetRootCertificate(self.CaCert) } @@ -114,7 +163,19 @@ func (self *RestClientEdgeIdentity) NewClient(timeout time.Duration, verbose boo func (self *RestClientEdgeIdentity) NewRequest(client *resty.Client) *resty.Request { r := client.R() - r.SetHeader(env.ZitiSession, self.Token) + if self.ApiSession != nil && self.ApiSession.ApiSession != nil { + switch self.ApiSession.ApiSession.GetType() { + case edge_apis.ApiSessionTypeOidc: + authHeader := "Bearer " + strings.TrimSpace(string(self.ApiSession.ApiSession.GetToken())) + r.SetHeader("Authorization", authHeader) + case edge_apis.ApiSessionTypeLegacy: + r.SetHeader(env.ZitiSession, string(self.ApiSession.ApiSession.GetToken())) + default: + panic("unsupported api session type " + self.ApiSession.ApiSession.GetType()) + } + } else { + r.SetHeader(env.ZitiSession, self.Token) + } return r } @@ -145,9 +206,7 @@ func (self *RestClientEdgeIdentity) NewEdgeManagementClient(clientOpts ClientOpt clientRuntime := httptransport.NewWithClient(parsedHost.Host, rest_management_api_client.DefaultBasePath, rest_management_api_client.DefaultSchemes, httpClient) - clientRuntime.DefaultAuthentication = &EdgeManagementAuth{ - Token: self.Token, - } + clientRuntime.DefaultAuthentication = self.newEdgeAuth() return rest_management_api_client.New(clientRuntime, nil), nil } @@ -165,9 +224,7 @@ func (self *RestClientEdgeIdentity) NewFabricManagementClient(clientOpts ClientO clientRuntime := httptransport.NewWithClient(parsedHost.Host, fabric_rest_client.DefaultBasePath, fabric_rest_client.DefaultSchemes, httpClient) - clientRuntime.DefaultAuthentication = &EdgeManagementAuth{ - Token: self.Token, - } + clientRuntime.DefaultAuthentication = self.newEdgeAuth() return fabric_rest_client.New(clientRuntime, nil), nil } @@ -364,3 +421,17 @@ func newRestClientTransport(clientOpts ClientOpts, clientIdentity RestClientIden } return httpClient, nil } + +func (self *RestClientEdgeIdentity) newEdgeAuth() EdgeManagementAuth { + ea := EdgeManagementAuth{} + + if self.ApiSession != nil && self.ApiSession.ApiSession != nil { + ea.BearerToken = string(self.ApiSession.ApiSession.GetToken()) + } else if self.Token != "" { + ea.LegacyToken = self.Token + } else { + panic("no authentication mechanism set") + } + + return ea +} diff --git a/ziti/util/rest.go b/ziti/util/rest.go index d23efa4eb..cbefcbfb8 100644 --- a/ziti/util/rest.go +++ b/ziti/util/rest.go @@ -34,6 +34,7 @@ import ( "github.com/go-openapi/strfmt" "github.com/openziti/edge-api/rest_management_api_client" "github.com/openziti/edge-api/rest_model" + "github.com/openziti/ziti/controller/api" fabric_rest_client "github.com/openziti/ziti/controller/rest_client" "gopkg.in/resty.v1" ) @@ -47,6 +48,14 @@ func NewClient() *resty.Client { SetRedirectPolicy(resty.FlexibleRedirectPolicy(15)) } +func NewClientWithClient(client *http.Client) *resty.Client { + return resty. + NewWithClient(client). + SetTimeout(2 * time.Second). + SetRetryCount(5). + SetRedirectPolicy(resty.FlexibleRedirectPolicy(15)) +} + func PrettyPrintResponse(resp *resty.Response) string { out := resp.String() var prettyJSON bytes.Buffer @@ -132,17 +141,18 @@ func ControllerList(api API, path string, params url.Values, logJSON bool, out i return nil, err } - baseUrl, err := restClientIdentity.GetBaseUrlForApi(api) + baseUrlStr, err := restClientIdentity.GetBaseUrlForApi(api) if err != nil { return nil, err } - req, err := NewRequest(restClientIdentity, timeout, verbose) + baseUrl, _ := url.Parse(baseUrlStr) + req, err := NewRequestByTerminator(restClientIdentity, timeout, verbose, baseUrl.User.Username()) if err != nil { return nil, err } - queryUrl := baseUrl + "/" + path + queryUrl := strings.TrimRight(baseUrlStr, "/") + "/" + path if len(params) > 0 { queryUrl += "?" + params.Encode() @@ -265,11 +275,16 @@ func NewFabricManagementClient(clientOpts ClientOpts) (*fabric_rest_client.ZitiF } type EdgeManagementAuth struct { - Token string + LegacyToken string + BearerToken string } func (e EdgeManagementAuth) AuthenticateRequest(request openApiRuntime.ClientRequest, registry strfmt.Registry) error { - return request.SetHeaderParam("zt-session", e.Token) + if e.LegacyToken != "" { + return request.SetHeaderParam(api.ZitiSession, e.LegacyToken) + } else { + return request.SetHeaderParam("Authorization", "Bearer "+e.BearerToken) + } } // ControllerCreate will create entities of the given type in the given Edge Controller @@ -506,9 +521,13 @@ func EdgeControllerRequest(entityType string, out io.Writer, logJSON bool, timeo return jsonParsed, nil } -func EdgeControllerGetManagementApiBasePathWithPool(host string, caPool *x509.CertPool) string { - client := NewClient() - +func EdgeControllerGetManagementApiBasePathWithPool(host string, caPool *x509.CertPool, httpClient *http.Client) string { + var client *resty.Client + if httpClient == nil { + client = NewClient() + } else { + client = NewClientWithClient(httpClient) + } client.SetHostURL(host) if caPool != nil { @@ -593,8 +612,13 @@ func getManagementApiBasePath(host string, client *resty.Client) string { // determine the proper path that should be used to access the Edge Management API. Depending // on the version of the Edge Controller the API may be monolith on `/edge/` and `/` or split into // `/edge/management/` and `/edge/client/`. -func EdgeControllerGetManagementApiBasePath(host string, cert string) string { - client := NewClient() +func EdgeControllerGetManagementApiBasePath(host string, cert string, httpClient *http.Client) string { + var client *resty.Client + if httpClient == nil { + client = NewClient() + } else { + client = NewClientWithClient(httpClient) + } client.SetHostURL(host) diff --git a/ziti/util/transport.go b/ziti/util/transport.go new file mode 100644 index 000000000..54dfdd30f --- /dev/null +++ b/ziti/util/transport.go @@ -0,0 +1,113 @@ +// Package util provides utility functions for the Ziti CLI, including HTTP transport +// creation and configuration for communicating over Ziti networks. +package util + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "os" + "strings" + + "github.com/openziti/sdk-golang/ziti" + "github.com/openziti/ziti/ziti/constants" +) + +// NewZitifiedTransportFromSlice creates an HTTP transport configured to route +// connections through a Ziti network. The provided bytes should contain a JSON-encoded +// Ziti configuration. +// +// By default, urls are expected to leverage intercepts. Create a service and assign an appropriate +// intercept config and use the intercept address when dialing. +// +// To support addressable terminators-based dialing a user should be specified in the URL. This activates +// the dial-by-identity functionality. In this mode the url should be in the form of +// "identity-to-dial@service-name-to-dial". The transport uses the Proxy hook to extract user identity +// information from request URLs and passes it to Ziti dial operation via DialOptions. +// +// Returns an error if the configuration is invalid or Ziti context creation fails. +func NewZitifiedTransportFromSlice(bytes []byte, terminator string) (*http.Transport, error) { + cfg := &ziti.Config{} + if err := json.Unmarshal(bytes, &cfg); err != nil { + return nil, err + } + cfg.ConfigTypes = append(cfg.ConfigTypes, "all") + + zc, zce := ziti.NewContext(cfg) + if zce != nil { + return nil, fmt.Errorf("failed to create ziti context: %v", zce) + } + zitiCliContextCollection.Add(zc) + + if _, se := zc.GetServices(); se != nil { + return nil, fmt.Errorf("failed to get ziti services: %v", se) + } + + zitiTransport := http.DefaultTransport.(*http.Transport).Clone() + + opts := &ziti.DialOptions{ + Identity: terminator, + } + + zitiTransport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { + dialer := zitiCliContextCollection.NewDialerWithFallback(ctx, &net.Dialer{}) + if opts.Identity != "" { + hostParts := strings.Split(addr, ":") + return zc.DialWithOptions(hostParts[0], opts) + } else { + return dialer.Dial(network, addr) + } + } + + _, se := zc.GetServices() // loads all the services + if se != nil { + return nil, fmt.Errorf("failed to get ziti services: %v", se) + } + return zitiTransport, nil +} + +// ZitifiedTransportFromEnv creates a Ziti-enabled HTTP transport by reading a +// base64-encoded Ziti identity from the default environment variable +// (ZitiCliNetworkIdVarName from constants). +// +// Returns (nil, nil) if the environment variable is not set, or (transport, error) +// if there's an issue creating the transport. +func ZitifiedTransportFromEnv(terminator string) (*http.Transport, error) { + return ZitifiedTransportFromEnvByName(constants.ZitiCliNetworkIdVarName, terminator) +} + +// ZitifiedTransportFromEnvByName creates a Ziti-enabled HTTP transport by reading +// a base64-encoded Ziti identity from the specified environment variable. +// +// The environment variable should contain a base64-encoded Ziti configuration. +// Returns (nil, nil) if the environment variable is not set, or (transport, error) +// if there are issues with decoding or configuration creation. +func ZitifiedTransportFromEnvByName(envVarName string, terminator string) (*http.Transport, error) { + b64Zid := os.Getenv(envVarName) + if b64Zid == "" { + return nil, nil + } + idReader := base64.NewDecoder(base64.StdEncoding, strings.NewReader(b64Zid)) + data, err := io.ReadAll(idReader) + if err != nil { + return nil, fmt.Errorf("failed to read and decode ziti identity: %v", err) + } + return NewZitifiedTransportFromSlice(data, terminator) +} + +// NewZitifiedTransportFromFile creates a Ziti-enabled HTTP transport by reading +// a Ziti configuration from a file. The file should contain JSON-encoded Ziti +// configuration data. +// +// Returns an error if the file cannot be read or contains invalid configuration. +func NewZitifiedTransportFromFile(pathToFile string, terminator string) (*http.Transport, error) { + data, err := os.ReadFile(pathToFile) + if err != nil { + return nil, fmt.Errorf("failed to read ziti identity file %s: %v", pathToFile, err) + } + return NewZitifiedTransportFromSlice(data, terminator) +} diff --git a/zitirest/clients.go b/zitirest/clients.go index 8905535c4..b7e51f842 100644 --- a/zitirest/clients.go +++ b/zitirest/clients.go @@ -22,6 +22,14 @@ import ( "crypto/x509" "encoding/json" "fmt" + "io" + "net" + "net/http" + "net/url" + "strings" + "sync" + "time" + openApiRuntime "github.com/go-openapi/runtime" httptransport "github.com/go-openapi/runtime/client" "github.com/go-openapi/strfmt" @@ -34,17 +42,11 @@ import ( "github.com/openziti/edge-api/rest_model" "github.com/openziti/foundation/v2/concurrenz" "github.com/openziti/identity" + "github.com/openziti/ziti/controller/api" "github.com/openziti/ziti/controller/env" fabricRestClient "github.com/openziti/ziti/controller/rest_client" "github.com/openziti/ziti/ziti/util" "github.com/pkg/errors" - "io" - "net" - "net/http" - "net/url" - "strings" - "sync" - "time" ) type Clients struct { @@ -111,7 +113,7 @@ func (self *Clients) Authenticate(user, password string) error { } func (self *Clients) AuthenticateRequest(request openApiRuntime.ClientRequest, registry strfmt.Registry) error { - return request.SetHeaderParam("zt-session", self.token.Load()) + return request.SetHeaderParam(api.ZitiSession, self.token.Load()) } func (self *Clients) SetSessionToken(token string) { @@ -159,12 +161,12 @@ func (self *Clients) LoadWellKnownCerts() error { self.host = "https://" + self.host } - wellKnownCerts, _, err := util.GetWellKnownCerts(self.host) + wellKnownCerts, _, err := util.GetWellKnownCerts(self.host, http.Client{}) if err != nil { return errors.Wrapf(err, "unable to retrieve server certificate authority from %v", self.host) } - certsTrusted, err := util.AreCertsTrusted(self.host, wellKnownCerts) + certsTrusted, err := util.AreCertsTrusted(self.host, wellKnownCerts, http.Client{}) if err != nil { return errors.Wrapf(err, "unable to verify well known certs for host %v", self.host) }