From 22e7ffae75918fd5a653bd66b48d223ed2211acb Mon Sep 17 00:00:00 2001 From: xueqingz Date: Mon, 6 May 2024 05:47:10 +0000 Subject: [PATCH 1/2] CP-47357: Add a Go JSON-RPC client file Signed-off-by: xueqingz --- .../sdk-gen/go/autogen/src/jsonrpc_client.go | 225 ++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 ocaml/sdk-gen/go/autogen/src/jsonrpc_client.go diff --git a/ocaml/sdk-gen/go/autogen/src/jsonrpc_client.go b/ocaml/sdk-gen/go/autogen/src/jsonrpc_client.go new file mode 100644 index 00000000000..c7448479428 --- /dev/null +++ b/ocaml/sdk-gen/go/autogen/src/jsonrpc_client.go @@ -0,0 +1,225 @@ +package xenapi + +import ( + "bytes" + "context" + "crypto/tls" + "crypto/x509" + "encoding/json" + "errors" + "fmt" + "io" + "log" + "net/http" + "os" + "reflect" + "strings" + "time" +) + +type Request struct { + JSONRPC string `json:"jsonrpc"` + Method string `json:"method"` + Params interface{} `json:"params,omitempty"` + ID int `json:"id"` +} + +type ResponseError struct { + Code int `json:"code"` + Message string `json:"message"` + Data interface{} `json:"data,omitempty"` +} + +type Response struct { + JSONRPC string `json:"jsonrpc"` + Result interface{} `json:"result,omitempty"` + Error *ResponseError `json:"error,omitempty"` + ID int `json:"id"` +} + +func paramsParse(params ...interface{}) interface{} { + var finalParams interface{} + finalParams = params + if len(params) == 1 { + if params[0] != nil { + var typeOf reflect.Type + typeOf = reflect.TypeOf(params[0]) + for typeOf != nil && typeOf.Kind() == reflect.Ptr { + typeOf = typeOf.Elem() + } + typeArr := []reflect.Kind{reflect.Struct, reflect.Array, reflect.Slice, reflect.Interface, reflect.Map} + if typeOf != nil { + for _, value := range typeArr { + if value == typeOf.Kind() { + finalParams = params[0] + break + } + } + } + } + } + return finalParams +} + +type rpcClient struct { + endpoint string + httpClient *http.Client + headers map[string]string +} + +func (client *rpcClient) newRequest(ctx context.Context, req interface{}) (*http.Request, error) { + dataByte, err := json.Marshal(req) + if err != nil { + return nil, fmt.Errorf("error marshaling request: %w", err) + } + + request, err := http.NewRequestWithContext(ctx, http.MethodPost, client.endpoint, bytes.NewReader(dataByte)) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + request.Header.Set("Content-Type", "application/json; charset=utf-8") + request.Header.Set("Accept", "application/json") + request.Header.Set("User-Agent", "XenAPI/" + APIVersionLatest.String()) + for k, v := range client.headers { + request.Header.Set(k, v) + } + + return request, nil +} + +func convertUnhandledJSONData(jsonBytes []byte) []byte { + jsonString := string(jsonBytes) + jsonString = strings.ReplaceAll(jsonString, ":Infinity", ":\"+Inf\"") + jsonString = strings.ReplaceAll(jsonString, ":-Infinity", ":\"-Inf\"") + jsonString = strings.ReplaceAll(jsonString, ":NaN", ":\"NaN\"") + + return []byte(jsonString) +} + +func (client *rpcClient) call(ctx context.Context, methodName string, params ...interface{}) (*Response, error) { + request := &Request{ + ID: 0, + Method: methodName, + Params: paramsParse(params...), + JSONRPC: "2.0", + } + + httpRequest, err := client.newRequest(ctx, request) + if err != nil { + return nil, fmt.Errorf("could not create request for %v() : %w", request.Method, err) + } + + httpResponse, err := client.httpClient.Do(httpRequest) + if err != nil { + return nil, fmt.Errorf("call %v() on %v. Error making http request: %w", request.Method, httpRequest.URL.Redacted(), err) + } + defer httpResponse.Body.Close() + + var rpcResponse *Response + body, err := io.ReadAll(httpResponse.Body) + if err != nil { + return nil, fmt.Errorf("call %v() on %v status code: %v. Could not read response body: %w", request.Method, httpRequest.URL.Redacted(), httpResponse.StatusCode, err) + } + body = convertUnhandledJSONData(body) + err = json.Unmarshal(body, &rpcResponse) + if err != nil && !errors.Is(err, io.EOF) { + return nil, fmt.Errorf("call %v() on %v status code: %v. Could not decode response body: %w", request.Method, httpRequest.URL.Redacted(), httpResponse.StatusCode, err) + } + + if rpcResponse == nil { + return nil, fmt.Errorf("call %v() on %v status code: %v. Response missing", request.Method, httpRequest.URL.Redacted(), httpResponse.StatusCode) + } + + return rpcResponse, nil +} + +func (client *rpcClient) sendCall(methodName string, params ...interface{}) (result interface{}, err error) { + response, err := client.call(context.Background(), methodName, params...) + if err != nil { + return + } + + if response.Error != nil { + errString := fmt.Sprintf("API error: code %d, message %s", response.Error.Code, response.Error.Message) + if response.Error.Data != nil { + errString += fmt.Sprintf(", data %v", response.Error.Data) + } + err = errors.New(errString) + return + } + + result = response.Result + return +} + +type SecureOpts struct { + ServerCert string + ClientCert string + ClientKey string +} + +type ClientOpts struct { + URL string + SecureOpts *SecureOpts + Timeout int + Headers map[string]string +} + +func newJSONRPCClient(opts *ClientOpts) *rpcClient { + client := &rpcClient{ + endpoint: fmt.Sprintf("%s%s", opts.URL, "/jsonrpc"), + httpClient: &http.Client{}, + headers: make(map[string]string), + } + + if strings.HasPrefix(opts.URL, "https://") { + skipVerify := true + caCertPool := x509.NewCertPool() + certs := []tls.Certificate{} + if opts.SecureOpts != nil { + skipVerify = false + if opts.SecureOpts.ServerCert != "" { + caCert, err := os.ReadFile(opts.SecureOpts.ServerCert) + if err != nil { + log.Fatal(err) + } + caCertPool.AppendCertsFromPEM(caCert) + } + if opts.SecureOpts.ClientCert != "" || opts.SecureOpts.ClientKey != "" { + if opts.SecureOpts.ClientCert == "" { + log.Fatal(errors.New("missing client certificate")) + } + if opts.SecureOpts.ClientKey == "" { + log.Fatal(errors.New("missing client private key")) + } + cert, err := tls.LoadX509KeyPair(opts.SecureOpts.ClientCert, opts.SecureOpts.ClientKey) + if err != nil { + log.Fatal(err) + } + certs = []tls.Certificate{cert} + } + } + tlsConfig := &tls.Config{ + RootCAs: caCertPool, + Certificates: certs, + InsecureSkipVerify: skipVerify, // #nosec + } + transport := &http.Transport{ + TLSClientConfig: tlsConfig, + } + client.httpClient.Transport = transport + } + + if opts.Timeout != 0 { + client.httpClient.Timeout = time.Duration(opts.Timeout) * time.Second + } + + if opts.Headers != nil { + for k, v := range opts.Headers { + client.headers[k] = v + } + } + + return client +} From 890ae8d47303b6737632772a4b1e2c8e3f70af97 Mon Sep 17 00:00:00 2001 From: xueqingz Date: Tue, 14 May 2024 10:18:40 +0000 Subject: [PATCH 2/2] CP-47357: fix review issues Signed-off-by: xueqingz --- .../sdk-gen/go/autogen/src/jsonrpc_client.go | 53 ++++++++----------- 1 file changed, 21 insertions(+), 32 deletions(-) diff --git a/ocaml/sdk-gen/go/autogen/src/jsonrpc_client.go b/ocaml/sdk-gen/go/autogen/src/jsonrpc_client.go index c7448479428..b4dd2d601e5 100644 --- a/ocaml/sdk-gen/go/autogen/src/jsonrpc_client.go +++ b/ocaml/sdk-gen/go/autogen/src/jsonrpc_client.go @@ -11,8 +11,8 @@ import ( "io" "log" "net/http" + "net/url" "os" - "reflect" "strings" "time" ) @@ -37,30 +37,6 @@ type Response struct { ID int `json:"id"` } -func paramsParse(params ...interface{}) interface{} { - var finalParams interface{} - finalParams = params - if len(params) == 1 { - if params[0] != nil { - var typeOf reflect.Type - typeOf = reflect.TypeOf(params[0]) - for typeOf != nil && typeOf.Kind() == reflect.Ptr { - typeOf = typeOf.Elem() - } - typeArr := []reflect.Kind{reflect.Struct, reflect.Array, reflect.Slice, reflect.Interface, reflect.Map} - if typeOf != nil { - for _, value := range typeArr { - if value == typeOf.Kind() { - finalParams = params[0] - break - } - } - } - } - } - return finalParams -} - type rpcClient struct { endpoint string httpClient *http.Client @@ -80,7 +56,7 @@ func (client *rpcClient) newRequest(ctx context.Context, req interface{}) (*http request.Header.Set("Content-Type", "application/json; charset=utf-8") request.Header.Set("Accept", "application/json") - request.Header.Set("User-Agent", "XenAPI/" + APIVersionLatest.String()) + request.Header.Set("User-Agent", "XenAPI/"+APIVersionLatest.String()) for k, v := range client.headers { request.Header.Set(k, v) } @@ -101,7 +77,7 @@ func (client *rpcClient) call(ctx context.Context, methodName string, params ... request := &Request{ ID: 0, Method: methodName, - Params: paramsParse(params...), + Params: params, JSONRPC: "2.0", } @@ -173,25 +149,32 @@ func newJSONRPCClient(opts *ClientOpts) *rpcClient { headers: make(map[string]string), } - if strings.HasPrefix(opts.URL, "https://") { + u, err := url.Parse(opts.URL) + if err != nil { + log.Fatal(err) + } + if strings.Compare(u.Scheme, "https") == 0 { skipVerify := true caCertPool := x509.NewCertPool() certs := []tls.Certificate{} if opts.SecureOpts != nil { - skipVerify = false if opts.SecureOpts.ServerCert != "" { + skipVerify = false caCert, err := os.ReadFile(opts.SecureOpts.ServerCert) if err != nil { log.Fatal(err) } - caCertPool.AppendCertsFromPEM(caCert) + ok := caCertPool.AppendCertsFromPEM(caCert) + if !ok { + log.Fatal("failed to parse CA certificate") + } } if opts.SecureOpts.ClientCert != "" || opts.SecureOpts.ClientKey != "" { if opts.SecureOpts.ClientCert == "" { - log.Fatal(errors.New("missing client certificate")) + log.Fatal("missing client certificate") } if opts.SecureOpts.ClientKey == "" { - log.Fatal(errors.New("missing client private key")) + log.Fatal("missing client private key") } cert, err := tls.LoadX509KeyPair(opts.SecureOpts.ClientCert, opts.SecureOpts.ClientKey) if err != nil { @@ -204,6 +187,12 @@ func newJSONRPCClient(opts *ClientOpts) *rpcClient { RootCAs: caCertPool, Certificates: certs, InsecureSkipVerify: skipVerify, // #nosec + CipherSuites: []uint16{ + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + }, + MinVersion: tls.VersionTLS12, + PreferServerCipherSuites: true, } transport := &http.Transport{ TLSClientConfig: tlsConfig,