From 5af66f1a96ccd557619ec58d43bbb52289518784 Mon Sep 17 00:00:00 2001 From: Yota Hamada Date: Tue, 10 Dec 2024 22:45:22 +0900 Subject: [PATCH] [#736] add TLS skip verification option for remote node (#739) --- docs/source/config_remote.rst | 3 ++ internal/config/config.go | 1 + internal/frontend/dag/handler.go | 75 +++++++++++++++++++++----------- 3 files changed, 53 insertions(+), 26 deletions(-) diff --git a/docs/source/config_remote.rst b/docs/source/config_remote.rst index 73442a174..75352a735 100644 --- a/docs/source/config_remote.rst +++ b/docs/source/config_remote.rst @@ -31,6 +31,9 @@ Create ``admin.yaml`` in ``$HOME/.config/dagu/`` to configure remote nodes. Exam isAuthToken: true # Enable API token (optional) authToken: "your-secret-token" # API token value (optional) + # TLS settings + skipTLSVerify: false # Skip TLS verification (optional) + Using Remote Nodes ----------------- Once configured, remote nodes can be selected from the dropdown menu in the top right corner of the UI. This allows you to: diff --git a/internal/config/config.go b/internal/config/config.go index 1e2a5df34..3f0517db6 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -73,6 +73,7 @@ type RemoteNode struct { BasicAuthPassword string // Basic auth password IsAuthToken bool // Enable auth token for API AuthToken string // Auth token for API + SkipTLSVerify bool // Skip TLS verification } type TLS struct { diff --git a/internal/frontend/dag/handler.go b/internal/frontend/dag/handler.go index bd3de45f1..86d052765 100644 --- a/internal/frontend/dag/handler.go +++ b/internal/frontend/dag/handler.go @@ -16,6 +16,7 @@ package dag import ( + "crypto/tls" "encoding/json" "errors" "fmt" @@ -24,6 +25,7 @@ import ( "os" "sort" "strings" + "time" "github.com/dagu-org/dagu/internal/client" "github.com/dagu-org/dagu/internal/config" @@ -223,10 +225,6 @@ func (h *Handler) doRemoteProxy(body any, originalReq *http.Request, node config method := originalReq.Method var bodyJSON io.Reader if body != nil { - // Forward the request body if needed - // originalReq.Body is a ReadCloser; ensure we can read it only once. - // Typically, you'd buffer it or ensure it's reusable. - // For simplicity, let's assume we can read it directly. data, err := json.Marshal(body) if err != nil { return h.responderWithCodedError(&codedError{ @@ -263,11 +261,21 @@ func (h *Handler) doRemoteProxy(body any, originalReq *http.Request, node config } } - client := &http.Client{} + // Create a custom transport that skips certificate verification + transport := &http.Transport{ + TLSClientConfig: &tls.Config{ + // Allow insecure TLS connections if the remote node is configured to skip verification + // This may be necessary for some enterprise setups + InsecureSkipVerify: node.SkipTLSVerify, // nolint:gosec + }, + } + + client := &http.Client{ + Transport: transport, + Timeout: 30 * time.Second, // Add a reasonable timeout + } + resp, err := client.Do(req) - defer func() { - _ = resp.Body.Close() - }() // Ensure we close the response body if err != nil { return h.responderWithCodedError(&codedError{ Code: 502, @@ -275,28 +283,21 @@ func (h *Handler) doRemoteProxy(body any, originalReq *http.Request, node config Message: swag.String(fmt.Sprintf("failed to send request to remote node: %v", err)), }}) } - defer resp.Body.Close() - // If not status 200, return an error - if resp.StatusCode < 200 || resp.StatusCode > 299 { - // Try to decode remote error if it's JSON - var remoteErr models.APIError - if err := json.NewDecoder(resp.Body).Decode(&remoteErr); err == nil && remoteErr.Message != nil { - return h.responderWithCodedError(&codedError{ - Code: resp.StatusCode, - APIError: &remoteErr, - }) - } - // If we cannot decode a proper error, return a generic one - payload := &models.APIError{ - Message: swag.String(fmt.Sprintf("remote node responded with status %d", resp.StatusCode)), - } + if resp == nil { return h.responderWithCodedError(&codedError{ - Code: resp.StatusCode, - APIError: payload, - }) + Code: 502, + APIError: &models.APIError{ + Message: swag.String("received nil response from remote node"), + }}) } + defer func() { + if resp.Body != nil { + resp.Body.Close() + } + }() + respData, err := io.ReadAll(resp.Body) if err != nil { return h.responderWithCodedError(&codedError{ @@ -306,6 +307,28 @@ func (h *Handler) doRemoteProxy(body any, originalReq *http.Request, node config }}) } + // If not status 200, try to parse the error response + if resp.StatusCode < 200 || resp.StatusCode > 299 { + // Only try to decode JSON if we actually got some response data + if len(respData) > 0 { + var remoteErr models.APIError + if err := json.Unmarshal(respData, &remoteErr); err == nil && remoteErr.Message != nil { + return h.responderWithCodedError(&codedError{ + Code: resp.StatusCode, + APIError: &remoteErr, + }) + } + } + // If we can't decode a proper error or have no data, return a generic one + payload := &models.APIError{ + Message: swag.String(fmt.Sprintf("remote node responded with status %d", resp.StatusCode)), + } + return h.responderWithCodedError(&codedError{ + Code: resp.StatusCode, + APIError: payload, + }) + } + return middleware.ResponderFunc(func(w http.ResponseWriter, _ runtime.Producer) { w.WriteHeader(resp.StatusCode) _, _ = w.Write(respData)