diff --git a/examples/features/https/README.md b/examples/features/https/README.md new file mode 100644 index 0000000..1ed2b10 --- /dev/null +++ b/examples/features/https/README.md @@ -0,0 +1,96 @@ +# HTTPS example + +This example demonstrates HTTPS Standard Service in tRPC-Go and provides two +ways to run the client: + +- Pure code example (see `client/`). +- YAML-only example (see `clientyaml/`). + +The server listens on `9443` with a self-signed certificate (demo only). + +## Layout + +``` +examples/features/https/ +├── server/ # Server (9443, https_no_protocol) +├── client/ # Pure code: enable TLS via WithTarget/WithTLS +└── clientyaml/ # YAML-only: enable TLS via YAML +``` + +## Start the server (9443) + +```bash +cd server +go run main.go -conf trpc_go.yaml +``` + +## Client options + +Pick either the pure code client or the YAML-only client. + +### Pure code ([client/](./client/)) + +Explicitly set backend target and TLS in code: + +- Case A: `https + ca_cert:none` (insecure). +- Case B: `http + ca_cert:none` (enable TLS via `ca_cert:none`). + +Run: + +```bash +cd ../client +# Code uses ip://127.0.0.1:9443 + WithTLS(..., "none", ...) +# You will see three logs: A success, B failure (no ca), C success (http+none) +go run main.go +``` + +Notes (code): + +- `protocol: https` alone does not enable TLS; you need a TLS signal: + - `WithTLS("", "", "none", "localhost")`, or + - YAML provides `ca_cert: "none"`/real CA, or + - Port inference (e.g., direct `:9443`). + +### YAML-only ([clientyaml/](./clientyaml/)) + +Three YAML files show common cases: + +- `trpc_go.yaml`: `protocol: https` + `ca_cert: "none"`. +- `trpc_go_no_ca.yaml`: `protocol: https`, no `ca_cert`, target port `9443`. +- `trpc_go_http_ca.yaml`: `protocol: http` + `ca_cert: "none"`. + +Run: + +```bash +cd ../clientyaml +# Case A: protocol:https, no ca_cert (port 9443) +go run main.go -conf trpc_go_no_ca.yaml +# Case B: protocol:http, ca_cert:"none" (port 9443) +go run main.go -conf trpc_go_http_ca.yaml +# Case C: protocol:https, ca_cert:"none" (port 9443) +go run main.go -conf trpc_go.yaml +``` + +Notes (YAML): + +- `dns://host` without port defaults to 80 (no TLS inference). + - Append `:443` (or your TLS port), or + - Provide `ca_cert: "none"`/real CA in YAML. +- Ensure the config is loaded: + - In-service: `trpc.NewServer()` loads automatically. + - Tools: see `clientyaml/main.go` using `LoadGlobalConfig` + `Setup`. + +## Expected + +- Case A (https, no ca_cert, 9443): success, body `https response body`, + header `reply: https-response-head`. +- Case B (http, ca_cert:none, 9443): success (TLS enabled by `ca_cert:none`). + +## FAQ + +- Why is `protocol: https` not enough by itself? + - TLS requires a signal (port, CA, or code `WithTLS`). +- Do I have to use 443? + - No. 9443 works in this demo; the key is the target is a TLS service. +- Production tips? + - Provide real `ca_cert` and `tls_server_name` instead of `none`. diff --git a/examples/features/https/client/main.go b/examples/features/https/client/main.go new file mode 100644 index 0000000..96d6a77 --- /dev/null +++ b/examples/features/https/client/main.go @@ -0,0 +1,162 @@ +// +// +// Tencent is pleased to support the open source community by making tRPC available. +// +// Copyright (C) 2023 Tencent. +// All rights reserved. +// +// If you have downloaded a copy of the tRPC source code from Tencent, +// please note that tRPC source code is licensed under the Apache 2.0 License, +// A copy of the Apache 2.0 License is included in this file. +// +// + +package main + +import ( + "context" + "net/http" + + "trpc.group/trpc-go/trpc-go/client" + "trpc.group/trpc-go/trpc-go/codec" + thttp "trpc.group/trpc-go/trpc-go/http" + "trpc.group/trpc-go/trpc-go/log" +) + +func main() { + // Test 1: protocol: https with ca_cert: "none" + log.Info("=== Test 1: protocol: https with ca_cert: none ===") + httpsWithCaCert() + + // Test 2: protocol: https without ca_cert (should work with explicit HTTPS) + log.Info("=== Test 2: protocol: https without ca_cert ===") + httpsWithoutCaCert() + + // Test 3: protocol: http with ca_cert: "none" (should also work) + log.Info("=== Test 3: protocol: http with ca_cert: none ===") + httpWithCaCert() +} + +// httpsWithCaCert sends an HTTPS POST request using explicit HTTPS protocol with ca_cert: "none". +func httpsWithCaCert() { + // Create a ClientProxy with target and TLS enabled via ca_cert: "none". + httpCli := thttp.NewClientProxy("trpc.app.server.stdhttps", + client.WithSerializationType(codec.SerializationTypeNoop), + client.WithCurrentSerializationType(codec.SerializationTypeNoop), + client.WithTarget("ip://127.0.0.1:9443"), + client.WithTLS("", "", "none", "localhost"), + ) + + // Create a ClientReqHeader with the specified HTTP method (POST) + reqHeader := &thttp.ClientReqHeader{ + Method: http.MethodPost, + } + + // Add a custom "request" header to the HTTP request header + reqHeader.AddHeader("request", "https-test-with-ca-cert") + + // Create ClientRspHeader to store the response header + rspHead := &thttp.ClientRspHeader{} + + // Create a Body containing the request data + req := &codec.Body{Data: []byte("Hello, I am HTTPS client with ca_cert!")} + + // Create an empty Body to store the response data + rsp := &codec.Body{} + + // Send a HTTPS POST request + if err := httpCli.Post(context.Background(), "/v1/hello", req, rsp, + client.WithReqHead(reqHeader), + client.WithRspHead(rspHead), + ); err != nil { + log.Errorf("Error getting HTTPS response with ca_cert: %v", err) + return + } + + // Get the "reply" field from the HTTP response header + replyHead := rspHead.Response.Header.Get("reply") + log.Infof("HTTPS with ca_cert - Data: \"%s\", Response head: \"%s\"", string(rsp.Data), replyHead) +} + +// httpsWithoutCaCert sends an HTTPS POST request using explicit HTTPS protocol without ca_cert. +// This should work if explicit HTTPS is properly implemented. +func httpsWithoutCaCert() { + // Create a ClientProxy with HTTPS but no ca_cert to test behavior. + httpCli := thttp.NewClientProxy("trpc.app.server.stdhttps-no-ca", + client.WithSerializationType(codec.SerializationTypeNoop), + client.WithCurrentSerializationType(codec.SerializationTypeNoop), + client.WithTarget("ip://127.0.0.1:9443"), + ) + + // Create a ClientReqHeader with the specified HTTP method (POST) + reqHeader := &thttp.ClientReqHeader{ + Method: http.MethodPost, + } + + // Add a custom "request" header to the HTTP request header + reqHeader.AddHeader("request", "https-test-without-ca-cert") + + // Create ClientRspHeader to store the response header + rspHead := &thttp.ClientRspHeader{} + + // Create a Body containing the request data + req := &codec.Body{Data: []byte("Hello, I am HTTPS client without ca_cert!")} + + // Create an empty Body to store the response data + rsp := &codec.Body{} + + // Send a HTTPS POST request + if err := httpCli.Post(context.Background(), "/v1/hello", req, rsp, + client.WithReqHead(reqHeader), + client.WithRspHead(rspHead), + ); err != nil { + log.Errorf("Error getting HTTPS response without ca_cert: %v", err) + return + } + + // Get the "reply" field from the HTTP response header + replyHead := rspHead.Response.Header.Get("reply") + log.Infof("HTTPS without ca_cert - Data: \"%s\", Response head: \"%s\"", string(rsp.Data), replyHead) +} + +// httpWithCaCert sends an HTTPS POST request using HTTP protocol with ca_cert: "none". +// This should work because ca_cert triggers HTTPS inference. +func httpWithCaCert() { + // Create a ClientProxy with HTTP protocol but TLS enabled via ca_cert: "none". + httpCli := thttp.NewClientProxy("trpc.app.server.stdhttps-http-ca", + client.WithSerializationType(codec.SerializationTypeNoop), + client.WithCurrentSerializationType(codec.SerializationTypeNoop), + client.WithTarget("ip://127.0.0.1:9443"), + client.WithTLS("", "", "none", "localhost"), + ) + + // Create a ClientReqHeader with the specified HTTP method (POST) + reqHeader := &thttp.ClientReqHeader{ + Method: http.MethodPost, + } + + // Add a custom "request" header to the HTTP request header + reqHeader.AddHeader("request", "http-test-with-ca-cert") + + // Create ClientRspHeader to store the response header + rspHead := &thttp.ClientRspHeader{} + + // Create a Body containing the request data + req := &codec.Body{Data: []byte("Hello, I am HTTP client with ca_cert!")} + + // Create an empty Body to store the response data + rsp := &codec.Body{} + + // Send a HTTPS POST request + if err := httpCli.Post(context.Background(), "/v1/hello", req, rsp, + client.WithReqHead(reqHeader), + client.WithRspHead(rspHead), + ); err != nil { + log.Errorf("Error getting HTTP response with ca_cert: %v", err) + return + } + + // Get the "reply" field from the HTTP response header + replyHead := rspHead.Response.Header.Get("reply") + log.Infof("HTTP with ca_cert - Data: \"%s\", Response head: \"%s\"", string(rsp.Data), replyHead) +} diff --git a/examples/features/https/client_with_cert_provider/main.go b/examples/features/https/client_with_cert_provider/main.go new file mode 100644 index 0000000..10b69a8 --- /dev/null +++ b/examples/features/https/client_with_cert_provider/main.go @@ -0,0 +1,241 @@ +// +// +// Tencent is pleased to support the open source community by making tRPC available. +// +// Copyright (C) 2023 Tencent. +// All rights reserved. +// +// If you have downloaded a copy of the tRPC source code from Tencent, +// please note that tRPC source code is licensed under the Apache 2.0 License, +// A copy of the Apache 2.0 License is included in this file. +// +// + +package main + +import ( + "context" + "fmt" + "net/http" + "os" + + "trpc.group/trpc-go/trpc-go/client" + "trpc.group/trpc-go/trpc-go/codec" + thttp "trpc.group/trpc-go/trpc-go/http" + "trpc.group/trpc-go/trpc-go/http/tls" + "trpc.group/trpc-go/trpc-go/log" +) + +// SimpleCertProvider is a minimal example of implementing a custom certificate provider +type SimpleCertProvider struct{} + +// LoadCertFile implements the CertificateProvider interface +func (s *SimpleCertProvider) LoadCertFile(certID string) ([]byte, error) { + log.Infof("SimpleCertProvider loading certificate file: certID=%q (empty=%v)", certID, certID == "") + + // Handle special IDs - simulate KMS/vault behavior + var actualPath string + switch certID { + case "": + log.Info("certID is empty, using default client certificate") + actualPath = "../../../../testdata/client.crt" + case "my-client-cert", "my-client-cert-2": + log.Infof("Recognized custom cert ID: %s, fetching from KMS...", certID) + actualPath = "../../../../testdata/client.crt" + default: + // Treat as file path + actualPath = certID + } + + // In this simple example, we just load from files + // In production, you would: + // 1. Fetch from your secure storage (KMS, vault, database, etc.) + // 2. Decrypt if necessary + // 3. Apply access control checks + // 4. Log audit events + // 5. Implement caching and rotation + + certPEM, err := os.ReadFile(actualPath) + if err != nil { + return nil, fmt.Errorf("failed to read cert from %s (mapped from %s): %w", actualPath, certID, err) + } + + // Here you could decrypt or transform the data before returning + // certPEM = decrypt(certPEM) + + log.Infof("Certificate loaded successfully via SimpleCertProvider (certID=%q, size=%d bytes)", certID, len(certPEM)) + return certPEM, nil +} + +// LoadKeyFile implements the CertificateProvider interface +func (s *SimpleCertProvider) LoadKeyFile(keyID string) ([]byte, error) { + log.Infof("SimpleCertProvider loading key file: keyID=%q (empty=%v)", keyID, keyID == "") + + // Handle special IDs - simulate KMS/vault behavior + var actualPath string + switch keyID { + case "": + log.Info("keyID is empty, using default client key") + actualPath = "../../../../testdata/client.key" + case "my-client-key", "my-client-key-2": + log.Infof("Recognized custom key ID: %s, fetching from KMS and decrypting...", keyID) + actualPath = "../../../../testdata/client.key" + default: + // Treat as file path + actualPath = keyID + } + + keyPEM, err := os.ReadFile(actualPath) + if err != nil { + return nil, fmt.Errorf("failed to read key from %s (mapped from %s): %w", actualPath, keyID, err) + } + + // Here you could decrypt the key + // keyPEM = decrypt(keyPEM) + + log.Infof("Key loaded successfully via SimpleCertProvider (keyID=%q, size=%d bytes)", keyID, len(keyPEM)) + return keyPEM, nil +} + +// KMSCertProvider demonstrates integration with Key Management Service +type KMSCertProvider struct { + kmsEndpoint string +} + +// LoadCertFile fetches certificate from KMS +func (k *KMSCertProvider) LoadCertFile(certID string) ([]byte, error) { + log.Infof("KMSCertProvider loading cert from %s: cert=%s", k.kmsEndpoint, certID) + + // In production, you would: + // 1. Call KMS API to fetch certificate + // 2. Authenticate using service credentials + // 3. Handle caching + + // For demonstration, fall back to file loading + certPEM, err := os.ReadFile(certID) + if err != nil { + return nil, fmt.Errorf("failed to fetch cert from KMS: %w", err) + } + + log.Infof("Certificate loaded successfully from KMS") + return certPEM, nil +} + +// LoadKeyFile fetches private key from KMS +func (k *KMSCertProvider) LoadKeyFile(keyID string) ([]byte, error) { + log.Infof("KMSCertProvider loading key from %s: key=%s", k.kmsEndpoint, keyID) + + // In production, you would: + // 1. Call KMS API to fetch and decrypt the key + // 2. The key might never leave the KMS in plaintext + // 3. Handle key rotation + + // For demonstration, fall back to file loading + keyPEM, err := os.ReadFile(keyID) + if err != nil { + return nil, fmt.Errorf("failed to fetch key from KMS: %w", err) + } + + log.Infof("Key loaded successfully from KMS") + return keyPEM, nil +} + +func init() { + // Register multiple certificate providers with different names + // This demonstrates the registry pattern where each provider has a unique name + + simpleProvider := &SimpleCertProvider{} + tls.RegisterCertificateProvider("simple", simpleProvider) + log.Info("Registered certificate provider: 'simple'") + + kmsProvider := &KMSCertProvider{kmsEndpoint: "https://kms.example.com"} + tls.RegisterCertificateProvider("kms", kmsProvider) + log.Info("Registered certificate provider: 'kms'") +} + +func main() { + log.Info("=== Testing Custom Certificate Provider with client.WithCertProvider ===") + log.Info("This example demonstrates using client.WithCertProvider option") + log.Info("Please start the HTTPS server first:") + log.Info(" cd ../server && go run main.go") + log.Info("") + + // Example 1: Using Simple Certificate Provider with normal paths + log.Info("--- Example 1: Using 'simple' Provider with normal paths ---") + makeHTTPSRequestWithOptions("simple", "../../../../testdata/client.crt", "../../../../testdata/client.key") + + // Example 2: Using Simple Certificate Provider with EMPTY paths + log.Info("\n--- Example 2: Using 'simple' Provider with EMPTY paths (provider handles default) ---") + makeHTTPSRequestWithOptions("simple", "", "") + + // Example 3: Using Simple Certificate Provider with MULTIPLE paths + log.Info("\n--- Example 3: Using 'simple' Provider with MULTIPLE paths (xxx:xxx) ---") + makeHTTPSRequestWithOptions("simple", + "../../../../testdata/client.crt:../../../../testdata/client.crt", + "../../../../testdata/client.key:../../../../testdata/client.key") + + // Example 4: Using Simple Certificate Provider with NON-FILE custom IDs (xxx:xxx) + log.Info("\n--- Example 4: Using 'simple' Provider with CUSTOM IDs (my-client-cert:my-client-cert-2) ---") + makeHTTPSRequestWithOptions("simple", + "my-client-cert:my-client-cert-2", + "my-client-key:my-client-key-2") + + // Example 5: Using KMS Certificate Provider + log.Info("\n--- Example 5: Using 'kms' Provider ---") + makeHTTPSRequestWithOptions("kms", "../../../../testdata/client.crt", "../../../../testdata/client.key") + + // Example 6: Using default (no provider) + log.Info("\n--- Example 6: Using default file loading (no provider) ---") + makeHTTPSRequestWithOptions("", "../../../../testdata/client.crt", "../../../../testdata/client.key") + + log.Info("\n=== All tests completed successfully! ===") +} + +func makeHTTPSRequestWithOptions(providerName, certFile, keyFile string) { + log.Infof("Creating HTTPS client with certFile=%q, keyFile=%q, provider=%q", certFile, keyFile, providerName) + + // Create HTTPS client with certificate provider option + opts := []client.Option{ + client.WithSerializationType(codec.SerializationTypeNoop), + client.WithCurrentSerializationType(codec.SerializationTypeNoop), + client.WithTarget("ip://127.0.0.1:9443"), + client.WithTLS( + certFile, + keyFile, + "none", + "localhost", + ), + } + + // Add provider option if specified + if providerName != "" { + log.Infof("Using certificate provider: %s", providerName) + opts = append(opts, client.WithCertProvider(providerName)) + } else { + log.Info("Using default file-based certificate loading") + } + + httpCli := thttp.NewClientProxy("trpc.app.server.stdhttps", opts...) + + reqHeader := &thttp.ClientReqHeader{ + Method: http.MethodPost, + } + reqHeader.AddHeader("request", "custom-cert-provider-test") + + rspHead := &thttp.ClientRspHeader{} + req := &codec.Body{Data: []byte("Hello from custom cert provider client!")} + rsp := &codec.Body{} + + // Send HTTPS POST request to the existing server + if err := httpCli.Post(context.Background(), "/v1/hello", req, rsp, + client.WithReqHead(reqHeader), + client.WithRspHead(rspHead), + ); err != nil { + log.Errorf("Request failed: %v", err) + return + } + + log.Infof("Request successful!") + log.Infof(" Response: %s", string(rsp.Data)) + log.Infof(" Reply header: %s", rspHead.Response.Header.Get("reply")) +} diff --git a/examples/features/https/client_with_provider.yaml b/examples/features/https/client_with_provider.yaml new file mode 100644 index 0000000..c4e0183 --- /dev/null +++ b/examples/features/https/client_with_provider.yaml @@ -0,0 +1,18 @@ +client: + service: + - name: trpc.app.server.stdhttps + target: ip://127.0.0.1:9443 + protocol: http_no_protocol + network: tcp + timeout: 1000 + tls_cert: ../../../testdata/client.crt + tls_key: ../../../testdata/client.key + ca_cert: none + tls_server_name: localhost + tls_cert_provider: simple # Use custom certificate provider + +plugins: + log: + default: + - writer: console + level: debug diff --git a/examples/features/https/clientyaml/main.go b/examples/features/https/clientyaml/main.go new file mode 100644 index 0000000..c36cc0e --- /dev/null +++ b/examples/features/https/clientyaml/main.go @@ -0,0 +1,86 @@ +// +// +// Tencent is pleased to support the open source community by making tRPC available. +// +// Copyright (C) 2023 Tencent. +// All rights reserved. +// +// If you have downloaded a copy of the tRPC source code from Tencent, +// please note that tRPC source code is licensed under the Apache 2.0 License, +// A copy of the Apache 2.0 License is included in this file. +// +// + +package main + +import ( + "context" + "flag" + "net/http" + + trpc "trpc.group/trpc-go/trpc-go" + "trpc.group/trpc-go/trpc-go/client" + "trpc.group/trpc-go/trpc-go/codec" + thttp "trpc.group/trpc-go/trpc-go/http" + "trpc.group/trpc-go/trpc-go/log" +) + +func main() { + // Parse config path from command line since the default config path is parsed in trpc.NewServer. + // We don't want to use new server here since it is just a client. + configPath := flag.String("conf", "./trpc_go.yaml", "config path") + flag.Parse() + + trpc.ServerConfigPath = *configPath + // Load YAML config and setup plugins. + if err := trpc.LoadGlobalConfig(trpc.ServerConfigPath); err != nil { + log.Errorf("load config error: %v", err) + return + } + if err := trpc.Setup(trpc.GlobalConfig()); err != nil { + log.Errorf("setup error: %v", err) + return + } + + // Iterate configured client services and invoke each one once. + cfg := trpc.GlobalConfig() + for _, s := range cfg.Client.Service { + // Choose the service name from YAML. + name := s.ServiceName + if name == "" { + name = s.Callee + } + if name == "" { + continue + } + + // Create HTTP client proxy relying solely on YAML. + httpCli := thttp.NewClientProxy(name, + client.WithSerializationType(codec.SerializationTypeNoop), + client.WithCurrentSerializationType(codec.SerializationTypeNoop), + ) + + // Prepare headers and bodies. + reqHeader := &thttp.ClientReqHeader{Method: http.MethodPost} + reqHeader.AddHeader("request", "yaml-only") + rspHead := &thttp.ClientRspHeader{} + req := &codec.Body{Data: []byte("Hello from YAML-only client.")} + rsp := &codec.Body{} + + // Invoke. + log.Infof("=== Invoke service: %s ===", name) + if err := httpCli.Post(context.Background(), "/v1/hello", req, rsp, + client.WithReqHead(reqHeader), + client.WithRspHead(rspHead), + ); err != nil { + log.Errorf("call %s failed: %v", name, err) + continue + } + + replyHead := "" + if rspHead.Response != nil { + replyHead = rspHead.Response.Header.Get("reply") + } + log.Infof("service %s ok, data: %q, reply head: %q", name, string(rsp.Data), replyHead) + } +} diff --git a/examples/features/https/clientyaml/trpc_go.yaml b/examples/features/https/clientyaml/trpc_go.yaml new file mode 100644 index 0000000..1e45ca7 --- /dev/null +++ b/examples/features/https/clientyaml/trpc_go.yaml @@ -0,0 +1,13 @@ +global: # global config. + namespace: development # environment type, two types: production and development. + env_name: test # environment name, names of multiple environments in informal settings. + +client: # configuration for client calls. + timeout: 1000 # maximum request processing time for all backends. + service: # configuration for a single backend. + - name: trpc.app.server.stdhttps # backend service name. + network: tcp # backend service network type, tcp or udp, configuration takes precedence. + protocol: https # application layer protocol, https for explicit HTTPS. + timeout: 800 # maximum request processing time in milliseconds. + target: ip://127.0.0.1:9443 # backend service address. + ca_cert: "none" # CA certificate, set to "none" for no verification. diff --git a/examples/features/https/clientyaml/trpc_go_http_ca.yaml b/examples/features/https/clientyaml/trpc_go_http_ca.yaml new file mode 100644 index 0000000..b7cb65d --- /dev/null +++ b/examples/features/https/clientyaml/trpc_go_http_ca.yaml @@ -0,0 +1,13 @@ +global: # global config. + namespace: development # environment type, two types: production and development. + env_name: test # environment name, names of multiple environments in informal settings. + +client: # configuration for client calls. + timeout: 1000 # maximum request processing time for all backends. + service: # configuration for a single backend. + - name: trpc.app.server.stdhttps-http-ca # backend service name. + network: tcp # backend service network type, tcp or udp, configuration takes precedence. + protocol: http # application layer protocol, http but with ca_cert. + timeout: 800 # maximum request processing time in milliseconds. + target: ip://127.0.0.1:9443 # backend service address. + ca_cert: "none" # CA certificate, set to "none" for no verification. diff --git a/examples/features/https/clientyaml/trpc_go_no_ca.yaml b/examples/features/https/clientyaml/trpc_go_no_ca.yaml new file mode 100644 index 0000000..715b886 --- /dev/null +++ b/examples/features/https/clientyaml/trpc_go_no_ca.yaml @@ -0,0 +1,13 @@ +global: # global config. + namespace: development # environment type, two types: production and development. + env_name: test # environment name, names of multiple environments in informal settings. + +client: # configuration for client calls. + timeout: 1000 # maximum request processing time for all backends. + service: # configuration for a single backend. + - name: trpc.app.server.stdhttps-no-ca # backend service name. + network: tcp # backend service network type, tcp or udp, configuration takes precedence. + protocol: https # application layer protocol, https for explicit HTTPS. + timeout: 800 # maximum request processing time in milliseconds. + target: ip://127.0.0.1:9443 # backend service address. + # No ca_cert specified - should work with explicit HTTPS diff --git a/examples/features/https/server/main.go b/examples/features/https/server/main.go new file mode 100644 index 0000000..a2ecb1a --- /dev/null +++ b/examples/features/https/server/main.go @@ -0,0 +1,144 @@ +// +// +// Tencent is pleased to support the open source community by making tRPC available. +// +// Copyright (C) 2023 Tencent. +// All rights reserved. +// +// If you have downloaded a copy of the tRPC source code from Tencent, +// please note that tRPC source code is licensed under the Apache 2.0 License, +// A copy of the Apache 2.0 License is included in this file. +// +// + +// Package main is the server main package for https demo. +package main + +import ( + "context" + "fmt" + "io" + "net/http" + "os" + + "trpc.group/trpc-go/trpc-go" + "trpc.group/trpc-go/trpc-go/codec" + "trpc.group/trpc-go/trpc-go/filter" + thttp "trpc.group/trpc-go/trpc-go/http" + "trpc.group/trpc-go/trpc-go/http/tls" + "trpc.group/trpc-go/trpc-go/log" +) + +// CustomCertProvider demonstrates custom provider for server-side certificates +type CustomCertProvider struct{} + +// LoadCertFile implements the CertificateProvider interface +func (c *CustomCertProvider) LoadCertFile(certID string) ([]byte, error) { + log.Infof("CustomCertProvider loading certificate file: certID=%q (empty=%v)", certID, certID == "") + + // Handle special IDs - simulate KMS/vault behavior + var actualPath string + switch certID { + case "": + log.Info("certID is empty, using default server certificate") + actualPath = "../../../../testdata/server.crt" + case "my-server-cert", "my-server-cert-2": + log.Infof("Recognized custom cert ID: %s, mapping to actual certificate", certID) + actualPath = "../../../../testdata/server.crt" + default: + // Treat as file path + actualPath = certID + } + + // In production, you would load from KMS, vault, encrypted storage, etc. + certPEM, err := os.ReadFile(actualPath) + if err != nil { + return nil, fmt.Errorf("failed to read cert from %s (mapped from %s): %w", actualPath, certID, err) + } + + log.Infof("Server certificate loaded successfully via CustomCertProvider (certID=%q, size=%d bytes)", certID, len(certPEM)) + return certPEM, nil +} + +// LoadKeyFile implements the CertificateProvider interface +func (c *CustomCertProvider) LoadKeyFile(keyID string) ([]byte, error) { + log.Infof("CustomCertProvider loading key file: keyID=%q (empty=%v)", keyID, keyID == "") + + // Handle special IDs - simulate KMS/vault behavior + var actualPath string + switch keyID { + case "": + log.Info("keyID is empty, using default server key") + actualPath = "../../../../testdata/server.key" + case "my-server-key", "my-server-key-2": + log.Infof("Recognized custom key ID: %s, mapping to actual key (decrypting...)", keyID) + actualPath = "../../../../testdata/server.key" + default: + // Treat as file path + actualPath = keyID + } + + // In production, you would decrypt the key from secure storage + keyPEM, err := os.ReadFile(actualPath) + if err != nil { + return nil, fmt.Errorf("failed to read key from %s (mapped from %s): %w", actualPath, keyID, err) + } + + log.Infof("Server key loaded successfully via CustomCertProvider (keyID=%q, size=%d bytes)", keyID, len(keyPEM)) + return keyPEM, nil +} + +func init() { + // Register custom certificate provider with a name + // The server will use this provider to load certificates + provider := &CustomCertProvider{} + tls.RegisterCertificateProvider("server-custom", provider) + log.Info("Custom certificate provider 'server-custom' registered") +} + +// handle is a function that processes HTTPS requests. +// Its implementation is consistent with the standard HTTP library. +func handle(w http.ResponseWriter, r *http.Request) error { + body, err := io.ReadAll(r.Body) + if err != nil { + log.Error(err) + return err + } + + log.Infof("Received HTTPS request: Method=%s, URL=%s, Body=%s", r.Method, r.URL.String(), string(body)) + + // Finally, use 'w' to send the response. + w.Header().Set("Content-type", "application/text") + w.Header().Set("reply", "https-response-head") + w.WriteHeader(http.StatusOK) + w.Write([]byte("https response body")) + return nil +} + +func main() { + filter.Register("info-request-head", infoReqHead, nil) + // Init server. Use -conf flag to specify config file: go run main.go -conf trpc_go_with_provider.yaml + s := trpc.NewServer() + + // Register the handle function for the "/v1/hello" endpoint. + thttp.HandleFunc("/v1/hello", handle) + + // When registering the NoProtocolService, the parameter passed must match the service name in the configuration. + // The service name here should be: s.Service("trpc.app.server.stdhttps"). + thttp.RegisterNoProtocolService(s.Service("trpc.app.server.stdhttps")) + + // Start serving and listening. + if err := s.Serve(); err != nil { + fmt.Println(err) + } +} + +func infoReqHead(ctx context.Context, req interface{}, next filter.ServerHandleFunc) (rsp interface{}, err error) { + msg := codec.Message(ctx) + rsp, err = next(ctx, req) + log.Info("ClientReqHead:", msg.ClientReqHead()) + log.Info("ClientRspHead:", msg.ClientRspHead()) + log.Info("ServerReqHead:", msg.ServerReqHead()) + log.Info("ServerRspHead:", msg.ServerRspHead()) + return rsp, err +} diff --git a/examples/features/https/server/trpc_go.yaml b/examples/features/https/server/trpc_go.yaml new file mode 100644 index 0000000..7b52de9 --- /dev/null +++ b/examples/features/https/server/trpc_go.yaml @@ -0,0 +1,15 @@ +global: # global config. + namespace: development # environment type, two types: production and development. + env_name: test # environment name, names of multiple environments in informal settings. + +server: # server configuration. + service: # business service configuration, can have multiple. + - name: trpc.app.server.stdhttps # the route name of the service. + ip: 127.0.0.1 # the service listening ip address, can use the placeholder ${ip}, choose one of ip and nic, priority ip. + port: 9443 # the service listening port, can use the placeholder ${port}. + network: tcp # the service listening network type. + protocol: https_no_protocol # the service application protocol. + timeout: 1000 # the service process timeout. + tls_cert: "../../../../testdata/server.crt" # TLS certificate file. + tls_key: "../../../../testdata/server.key" # TLS private key file. + filter: [info-request-head] diff --git a/examples/features/https/server/trpc_go_empty_path.yaml b/examples/features/https/server/trpc_go_empty_path.yaml new file mode 100644 index 0000000..2e91cfe --- /dev/null +++ b/examples/features/https/server/trpc_go_empty_path.yaml @@ -0,0 +1,16 @@ +server: + service: + - name: trpc.app.server.stdhttps + network: tcp + protocol: http_no_protocol + address: 127.0.0.1:9443 + timeout: 1000 + tls_cert: "" # Empty path - provider will handle + tls_key: "" # Empty path - provider will handle + tls_cert_provider: server-custom # Use custom certificate provider + +plugins: + log: + default: + - writer: console + level: debug diff --git a/examples/features/https/server/trpc_go_multi_path.yaml b/examples/features/https/server/trpc_go_multi_path.yaml new file mode 100644 index 0000000..143e59b --- /dev/null +++ b/examples/features/https/server/trpc_go_multi_path.yaml @@ -0,0 +1,16 @@ +server: + service: + - name: trpc.app.server.stdhttps + network: tcp + protocol: http_no_protocol + address: 127.0.0.1:9443 + timeout: 1000 + tls_cert: "my-server-cert:my-server-cert-2" # Non-file paths - provider will handle + tls_key: "my-server-key:my-server-key-2" # Non-file paths - provider will handle + tls_cert_provider: server-custom # Use custom certificate provider + +plugins: + log: + default: + - writer: console + level: debug diff --git a/examples/features/https/server/trpc_go_with_provider.yaml b/examples/features/https/server/trpc_go_with_provider.yaml new file mode 100644 index 0000000..f80df63 --- /dev/null +++ b/examples/features/https/server/trpc_go_with_provider.yaml @@ -0,0 +1,16 @@ +server: + service: + - name: trpc.app.server.stdhttps + network: tcp + protocol: http_no_protocol + address: 127.0.0.1:9443 + timeout: 1000 + tls_cert: ../../../../testdata/server.crt + tls_key: ../../../../testdata/server.key + tls_cert_provider: server-custom # Use custom certificate provider + +plugins: + log: + default: + - writer: console + level: debug diff --git a/examples/features/mtls/README.md b/examples/features/mtls/README.md new file mode 100644 index 0000000..b4f153a --- /dev/null +++ b/examples/features/mtls/README.md @@ -0,0 +1,43 @@ +## mTLS + +This example code demonstrates how to transmit the trpc protocol via mTLS. In this example, we specifically illustrate how the client and server configure and utilize certificates, private keys, and CA certificates to achieve secure mTLS transmission. + +## Usage + +- Start server. + +```bash +go run server/main.go -conf server/trpc_go.yaml +``` + +- Start client. + +```bash +go run client/main.go +``` + +- Server output + +```txt +2024-07-05 17:29:04.243 DEBUG common/common.go:39 SayHi recv req:msg:"test mTLS message" +``` + +- Client output + +```txt +2024-07-05 17:29:04.244 INFO client/main.go:29 get msg: Hi test mTLS message +``` + +## Explanation + +To adapt to sensitive scenarios such as databases and finance, the tRPC architecture provides Token-based Knocknock authentication and mTLS authentication methods for transmitting tRPC protocols. The specific implementation is to configure and use client certificates, private keys, and Certificate Authority (CA) certificates on both the client and server sides. + +### Server-side + +In order for the server to support mTLS authentication, configure `NewServer` +with the ServerOption `server.WithTLS()`, or use the server configuration +`trpc_go.yaml`. + +### Client-side + +Client side is similar to server side. diff --git a/examples/features/mtls/client/main.go b/examples/features/mtls/client/main.go new file mode 100644 index 0000000..c82dc19 --- /dev/null +++ b/examples/features/mtls/client/main.go @@ -0,0 +1,45 @@ +// +// +// Tencent is pleased to support the open source community by making tRPC available. +// +// Copyright (C) 2023 Tencent. +// All rights reserved. +// +// If you have downloaded a copy of the tRPC source code from Tencent, +// please note that tRPC source code is licensed under the Apache 2.0 License, +// A copy of the Apache 2.0 License is included in this file. +// +// + +// Package main is the client main package for mTLS demo. +package main + +import ( + "trpc.group/trpc-go/trpc-go" + "trpc.group/trpc-go/trpc-go/client" + "trpc.group/trpc-go/trpc-go/log" + pb "trpc.group/trpc-go/trpc-go/testdata/trpc/helloworld" +) + +func main() { + // Set up mTLS client options. + options := []client.Option{ + client.WithTarget("ip://localhost:8080"), + client.WithTLS( + "../../../testdata/client.crt", + "../../../testdata/client.key", + "../../../testdata/ca.pem", + "localhost", + ), + } + // new client + proxy := pb.NewGreeterClientProxy(options...) + ctx := trpc.BackgroundContext() + // start rpc call + rsp, err := proxy.SayHi(ctx, &pb.HelloRequest{Msg: "test mTLS message"}) + if err != nil { + log.ErrorContextf(ctx, "say hi err: %v", err) + return + } + log.InfoContextf(ctx, "get msg: %s", rsp.GetMsg()) +} diff --git a/examples/features/mtls/server/main.go b/examples/features/mtls/server/main.go new file mode 100644 index 0000000..a20a714 --- /dev/null +++ b/examples/features/mtls/server/main.go @@ -0,0 +1,34 @@ +// +// +// Tencent is pleased to support the open source community by making tRPC available. +// +// Copyright (C) 2023 Tencent. +// All rights reserved. +// +// If you have downloaded a copy of the tRPC source code from Tencent, +// please note that tRPC source code is licensed under the Apache 2.0 License, +// A copy of the Apache 2.0 License is included in this file. +// +// + +// Package main is the server main package for mTLS demo. +package main + +import ( + "fmt" + + "trpc.group/trpc-go/trpc-go" + "trpc.group/trpc-go/trpc-go/examples/features/common" + pb "trpc.group/trpc-go/trpc-go/testdata/trpc/helloworld" +) + +func main() { + // Create a server with TLS. + s := trpc.NewServer() + // Register service. + pb.RegisterGreeterService(s.Service("trpc.test.helloworld.Greeter"), &common.GreeterServerImpl{}) + // Serve and listen. + if err := s.Serve(); err != nil { + fmt.Println(err) + } +} diff --git a/examples/features/mtls/server/trpc_go.yaml b/examples/features/mtls/server/trpc_go.yaml new file mode 100644 index 0000000..ec62754 --- /dev/null +++ b/examples/features/mtls/server/trpc_go.yaml @@ -0,0 +1,23 @@ +global: # global config. + namespace: development # environment type, two types: production and development. + env_name: test # environment name, names of multiple environments in informal settings. + container_name: ${container_name} # container name, the placeholder is replaced by the actual container name by the operating platform. + local_ip: ${local_ip} # local ip, it is the container's ip in container and is local ip in physical machine or virtual machine. + +server: # server configuration. + app: test # business application name. + server: helloworld # server process name + bin_path: /usr/local/trpc/bin/ # paths to binary executables and framework configuration files. + conf_path: /usr/local/trpc/conf/ # paths to business configuration files. + data_path: /usr/local/trpc/data/ # paths to business data files. + service: # business service configuration, can have multiple. + - name: trpc.test.helloworld.Greeter # the route name of the service. + ip: 127.0.0.1 # the service listening ip address, can use the placeholder ${ip}, choose one of ip and nic, priority ip. + port: 8080 # the service listening port, can use the placeholder ${port}. + network: tcp # the service listening network type, tcp or udp. + protocol: trpc # application layer protocol, trpc or http. + timeout: 1000 # maximum request processing time in milliseconds. + idletime: 300000 # connection idle time in milliseconds. + tls_key: ../../../testdata/server.key # private key path + tls_cert: ../../../testdata/server.crt # Certificate path + ca_cert: ../../../testdata/ca.pem # ca certificate, used to verify the client certificate to more strictly identify the client's identity and restrict client access \ No newline at end of file