Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#736] add TLS skip verification option for remote node #739

Merged
merged 1 commit into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/source/config_remote.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
75 changes: 49 additions & 26 deletions internal/frontend/dag/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package dag

import (
"crypto/tls"
"encoding/json"
"errors"
"fmt"
Expand All @@ -24,6 +25,7 @@ import (
"os"
"sort"
"strings"
"time"

"github.com/dagu-org/dagu/internal/client"
"github.com/dagu-org/dagu/internal/config"
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -263,40 +261,43 @@ 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,
APIError: &models.APIError{
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{
Expand All @@ -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)
Expand Down
Loading