Skip to content
Open
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
7 changes: 7 additions & 0 deletions common/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,13 @@ func (c *Client) RequestRaw(
return nil
}

// HTTPClient returns the underlying *http.Client used by this Client.
// This is useful when you need to make HTTP requests that don't require
// auth header injection but should share the same transport/TLS configuration.
func (c *Client) HTTPClient() *http.Client {
return c.client
}

// Do is a wrapper of http.Client.Do that injects the auth header in the request.
func (c *Client) Do(r *http.Request) (*http.Response, error) {
slog.DebugContext(r.Context(), "Request", "method", r.Method, "url", r.URL.String())
Expand Down
40 changes: 39 additions & 1 deletion common/client/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"log/slog"
"net/url"
"time"

"github.com/harness/mcp-server/common/client/dto"
Expand Down Expand Up @@ -99,5 +100,42 @@ func (l *LogService) GetDownloadLogsURL(ctx context.Context, scope dto.Scope, pl
return "", lastErr
}

return response.Link, nil
// Rewrite the download URL host to match the configured base URL.
// The API may return a pre-signed URL with a hardcoded host (e.g., app.harness.io)
// that differs from HARNESS_BASE_URL, causing TLS failures in environments with
// custom domains or corporate proxies.
rewrittenLink, err := rewriteDownloadURLHost(response.Link, l.LogServiceClient.BaseURL)
if err != nil {
slog.WarnContext(ctx, "Failed to rewrite download URL host, using original URL", "error", err)
return response.Link, nil
}

return rewrittenLink, nil
}

// rewriteDownloadURLHost replaces the host of a download URL with the host from the
// configured base URL, so that the download request routes through the same trusted
// endpoint as all other API calls.
func rewriteDownloadURLHost(downloadLink string, baseURL *url.URL) (string, error) {
if baseURL == nil {
return downloadLink, nil
}

parsed, err := url.Parse(downloadLink)
if err != nil {
return "", fmt.Errorf("failed to parse download URL: %w", err)
}

// Only rewrite if the hosts actually differ
if parsed.Host == baseURL.Host {
return downloadLink, nil
}

slog.Info("Rewriting download URL host to match configured base URL",
"original_host", parsed.Host,
"new_host", baseURL.Host)

parsed.Scheme = baseURL.Scheme
parsed.Host = baseURL.Host
return parsed.String(), nil
}
69 changes: 69 additions & 0 deletions common/client/logs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package client

import (
"net/url"
"testing"
)

func TestRewriteDownloadURLHost(t *testing.T) {
tests := []struct {
name string
downloadLink string
baseURL string
wantURL string
wantErr bool
}{
{
name: "rewrites app.harness.io to custom domain",
downloadLink: "https://app.harness.io/storage/harness-download/logs/abc123?token=xyz",
baseURL: "https://mycompany.harness.io",
wantURL: "https://mycompany.harness.io/storage/harness-download/logs/abc123?token=xyz",
},
{
name: "no rewrite when hosts match",
downloadLink: "https://app.harness.io/storage/logs/abc123",
baseURL: "https://app.harness.io",
wantURL: "https://app.harness.io/storage/logs/abc123",
},
{
name: "nil base URL returns original link",
downloadLink: "https://app.harness.io/storage/logs/abc123",
baseURL: "",
wantURL: "https://app.harness.io/storage/logs/abc123",
},
{
name: "preserves query parameters",
downloadLink: "https://app.harness.io/storage/logs?accountID=abc&prefix=key&X-Amz-Signature=sig123",
baseURL: "https://mycompany.harness.io",
wantURL: "https://mycompany.harness.io/storage/logs?accountID=abc&prefix=key&X-Amz-Signature=sig123",
},
{
name: "preserves path structure",
downloadLink: "https://app.harness.io/storage/harness-download/comp-log-service/deep/nested/path.zip",
baseURL: "https://custom.example.com",
wantURL: "https://custom.example.com/storage/harness-download/comp-log-service/deep/nested/path.zip",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var baseURL *url.URL
if tt.baseURL != "" {
var err error
baseURL, err = url.Parse(tt.baseURL)
if err != nil {
t.Fatalf("failed to parse base URL: %v", err)
}
}

got, err := rewriteDownloadURLHost(tt.downloadLink, baseURL)
if (err != nil) != tt.wantErr {
t.Errorf("rewriteDownloadURLHost() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.wantURL {
t.Errorf("rewriteDownloadURLHost() = %v, want %v", got, tt.wantURL)
}
})
}
}
11 changes: 9 additions & 2 deletions common/pkg/tools/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ func NewExtendedLogService(logService *client.LogService) *ExtendedLogService {
type DownloadLogsConfig struct {
MaxLogLines int // Enforce max lines (0 = use request parameter)
GetDownloadURL func(ctx context.Context, scope dto.Scope, planExecutionID string, logKey string) (string, error) // Custom URL fetching logic
HTTPClient *http.Client // HTTP client for downloading logs (inherits TLS/proxy config)
}

// DefaultDownloadLogsConfig returns the default configuration for downloading logs
Expand All @@ -67,6 +68,7 @@ func (l *ExtendedLogService) DefaultDownloadLogsConfig() *DownloadLogsConfig {
headers := map[string]string{}
return l.GetDownloadLogsURL(ctx, scope, planExecutionID, logKey, headers)
},
HTTPClient: l.LogService.LogServiceClient.HTTPClient(),
}
}

Expand Down Expand Up @@ -451,8 +453,13 @@ func DownloadExecutionLogsTool(config *config.McpServerConfig, client LogService
return mcp.NewToolResultError(fmt.Sprintf("failed to fetch log download URL: %v", err)), nil
}

// Download the logs into outputPath
resp, err := http.Get(logDownloadURL)
// Download the logs using the configured HTTP client (inherits TLS/proxy settings)
// instead of http.Get which uses the default client with no TLS customization.
httpClient := downloadLogsConfig.HTTPClient
if httpClient == nil {
httpClient = http.DefaultClient
}
resp, err := httpClient.Get(logDownloadURL)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("failed to download logs: %v", err)), nil
}
Expand Down