diff --git a/cmd/client/command/common.go b/cmd/client/command/common.go index 1efadcd654..c4d9e29e4b 100644 --- a/cmd/client/command/common.go +++ b/cmd/client/command/common.go @@ -19,9 +19,11 @@ package command import ( "bytes" + "crypto/tls" "fmt" "io" "net/http" + "strings" "github.com/megaease/easegress/pkg/util/codectool" "github.com/spf13/cobra" @@ -30,8 +32,10 @@ import ( type ( // GlobalFlags is the global flags for the whole client. GlobalFlags struct { - Server string - OutputFormat string + Server string + ForceTLS bool + InsecureSkipVerify bool + OutputFormat string } // APIErr is the standard return of error. @@ -112,10 +116,15 @@ const ( // MeshIngressURL is the mesh ingress path. MeshIngressURL = apiURL + "/mesh/ingresses/%s" + + // HTTPProtocal is prefix for HTTP protocal + HTTPProtocal = "http://" + // HTTPSProtocal is prefix for HTTPS protocal + HTTPSProtocal = "https://" ) func makeURL(urlTemplate string, a ...interface{}) string { - return "http://" + CommandlineGlobalFlags.Server + fmt.Sprintf(urlTemplate, a...) + return CommandlineGlobalFlags.Server + fmt.Sprintf(urlTemplate, a...) } func successfulStatusCode(code int) bool { @@ -132,26 +141,24 @@ func handleRequest(httpMethod string, url string, yamlBody []byte, cmd *cobra.Co } } - req, err := http.NewRequest(httpMethod, url, bytes.NewReader(jsonBody)) - if err != nil { - ExitWithError(err) + p := HTTPProtocal + if CommandlineGlobalFlags.ForceTLS { + p = HTTPSProtocal } - - resp, err := http.DefaultClient.Do(req) - if err != nil { - ExitWithErrorf("%s failed: %v", cmd.Short, err) + tr := http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: CommandlineGlobalFlags.InsecureSkipVerify}, } - defer resp.Body.Close() + client := &http.Client{Transport: &tr} + resp, body := doRequest(httpMethod, p+url, jsonBody, client, cmd) - body, err := io.ReadAll(resp.Body) - if err != nil { - ExitWithErrorf("%s failed: %v", cmd.Short, err) + msg := string(body) + if p == HTTPProtocal && resp.StatusCode == http.StatusBadRequest && strings.Contains(strings.ToUpper(msg), "HTTPS") { + resp, body = doRequest(httpMethod, HTTPSProtocal+url, jsonBody, client, cmd) } if !successfulStatusCode(resp.StatusCode) { - msg := string(body) apiErr := &APIErr{} - err = codectool.Unmarshal(body, apiErr) + err := codectool.Unmarshal(body, apiErr) if err == nil { msg = apiErr.Message } @@ -163,6 +170,24 @@ func handleRequest(httpMethod string, url string, yamlBody []byte, cmd *cobra.Co } } +func doRequest(httpMethod string, url string, jsonBody []byte, client *http.Client, cmd *cobra.Command) (*http.Response, []byte) { + req, err := http.NewRequest(httpMethod, url, bytes.NewReader(jsonBody)) + if err != nil { + ExitWithError(err) + } + resp, err := client.Do(req) + if err != nil { + ExitWithErrorf("%s failed: %v", cmd.Short, err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + ExitWithErrorf("%s failed: %v", cmd.Short, err) + } + return resp, body +} + func printBody(body []byte) { var output []byte switch CommandlineGlobalFlags.OutputFormat { diff --git a/cmd/client/main.go b/cmd/client/main.go index 99e6fd857d..404ceeb312 100644 --- a/cmd/client/main.go +++ b/cmd/client/main.go @@ -118,6 +118,10 @@ func main() { rootCmd.PersistentFlags().StringVar(&command.CommandlineGlobalFlags.Server, "server", "localhost:2381", "The address of the Easegress endpoint") + rootCmd.PersistentFlags().BoolVar(&command.CommandlineGlobalFlags.ForceTLS, + "force-tls", false, "Whether to forcibly use HTTPS, if not, client will auto upgrade to HTTPS on-demand") + rootCmd.PersistentFlags().BoolVar(&command.CommandlineGlobalFlags.InsecureSkipVerify, + "insecure-skip-verify", false, "Whether to verify the server's certificate chain and host name") rootCmd.PersistentFlags().StringVarP(&command.CommandlineGlobalFlags.OutputFormat, "output", "o", "yaml", "Output format(json, yaml)") diff --git a/pkg/api/server.go b/pkg/api/server.go index 3d8300f220..4563e2b5c7 100644 --- a/pkg/api/server.go +++ b/pkg/api/server.go @@ -83,8 +83,17 @@ func MustNewServer(opt *option.Options, cls cluster.Cluster, super *supervisor.S s.registerAPIs() go func() { - logger.Infof("api server running in %s", opt.APIAddr) - s.server.ListenAndServe() + var err error + if s.opt.TLS { + logger.Infof("api server (https) running in %s", opt.APIAddr) + err = s.server.ListenAndServeTLS(s.opt.CertFile, s.opt.KeyFile) + } else { + logger.Infof("api server running in %s", opt.APIAddr) + err = s.server.ListenAndServe() + } + if err != nil { + logger.Errorf("start api server failed: %v", err) + } }() return s diff --git a/pkg/option/option.go b/pkg/option/option.go index 36871d6e4f..562356009c 100644 --- a/pkg/option/option.go +++ b/pkg/option/option.go @@ -69,6 +69,9 @@ type Options struct { Name string `yaml:"name" env:"EG_NAME"` Labels map[string]string `yaml:"labels" env:"EG_LABELS"` APIAddr string `yaml:"api-addr"` + TLS bool `yaml:"tls"` + CertFile string `yaml:"cert-file"` + KeyFile string `yaml:"key-file"` Debug bool `yaml:"debug"` DisableAccessLog bool `yaml:"disable-access-log"` InitialObjectConfigFiles []string `yaml:"initial-object-config-files"` @@ -138,6 +141,9 @@ func New() *Options { opt.flags.BoolVar(&opt.UseStandaloneEtcd, "use-standalone-etcd", false, "Use standalone etcd instead of embedded .") addClusterVars(opt) opt.flags.StringVar(&opt.APIAddr, "api-addr", "localhost:2381", "Address([host]:port) to listen on for administration traffic.") + opt.flags.BoolVar(&opt.TLS, "tls", false, "Flag to use secure transport protocol(https).") + opt.flags.StringVar(&opt.CertFile, "cert-file", "", "Flag to set the certificate file for https.") + opt.flags.StringVar(&opt.KeyFile, "key-file", "", "Flag to set the private key file for https.") opt.flags.BoolVar(&opt.Debug, "debug", false, "Flag to set lowest log level from INFO downgrade DEBUG.") opt.flags.StringSliceVar(&opt.InitialObjectConfigFiles, "initial-object-config-files", nil, "List of configuration files for initial objects, these objects will be created at startup if not already exist.") opt.flags.StringVar(&opt.ObjectsDumpInterval, "objects-dump-interval", "", "The time interval to dump running objects config, for example: 30m") @@ -341,6 +347,9 @@ func (opt *Options) validate() error { if !opt.UseInitialCluster() && opt.MemberDir == "" { return fmt.Errorf("empty member-dir") } + if opt.TLS && (opt.CertFile == "" || opt.KeyFile == "") { + return fmt.Errorf("empty cert file or key file") + } // profile: nothing to validate