mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-03 01:01:56 +08:00
Go: fix lint (#16533)
### Summary as title. --------- Signed-off-by: Jin Hai <haijin.chn@gmail.com>
This commit is contained in:
@@ -52,7 +52,7 @@ func genID32() string {
|
||||
// webhookPayloadKey is the unexported context key RunAgent reads to
|
||||
// inject root["webhook_payload"]. Only the AgentService.RunAgentWithWebhook
|
||||
// public wrapper sets it; the chat / agent-run paths leave it absent so
|
||||
// existing callers see no behaviour change.
|
||||
// existing callers see no behavior change.
|
||||
//
|
||||
// We deliberately do NOT surface the payload as a new RunAgent parameter
|
||||
// — keeping the public signature stable means existing tests
|
||||
@@ -256,8 +256,8 @@ func toAgentItem(c *dao.UserCanvasListItem) *AgentItem {
|
||||
// ListAgents returns agent canvases visible to userID.
|
||||
// Mirrors Python agent_api.list_agents — validates owner_ids against joined tenants,
|
||||
// then delegates to the DAO.
|
||||
func (s *AgentService) ListAgents(userID string, keywords string, page, pageSize int, orderby string, desc bool, ownerIDs []string, canvasCategory, canvasType string, tags []string) (*ListAgentsResponse, common.ErrorCode, error) {
|
||||
// Build the set of tenant IDs the user is authorised to query.
|
||||
func (s *AgentService) ListAgents(userID string, keywords string, page, pageSize int, orderBy string, desc bool, ownerIDs []string, canvasCategory, canvasType string, tags []string) (*ListAgentsResponse, common.ErrorCode, error) {
|
||||
// Build the set of tenant IDs the user is authorized to query.
|
||||
tenantIDs, err := s.userTenantDAO.GetTenantIDsByUserID(userID)
|
||||
if err != nil {
|
||||
return nil, common.CodeServerError, fmt.Errorf("failed to get tenant IDs: %w", err)
|
||||
@@ -288,7 +288,7 @@ func (s *AgentService) ListAgents(userID string, keywords string, page, pageSize
|
||||
userID,
|
||||
page,
|
||||
pageSize,
|
||||
orderby,
|
||||
orderBy,
|
||||
desc,
|
||||
keywords,
|
||||
canvasCategory,
|
||||
@@ -329,10 +329,10 @@ func (s *AgentService) CreateAgent(ctx context.Context, req *CreateAgentRequest)
|
||||
return nil, common.CodeArgumentError, errors.New("create agent: nil request")
|
||||
}
|
||||
if req.DSL == nil {
|
||||
return nil, common.CodeArgumentError, errors.New("No DSL data in request.")
|
||||
return nil, common.CodeArgumentError, errors.New("no DSL data in request")
|
||||
}
|
||||
if req.Title == nil || strings.TrimSpace(*req.Title) == "" {
|
||||
return nil, common.CodeArgumentError, errors.New("No title in request.")
|
||||
return nil, common.CodeArgumentError, errors.New("no title in request")
|
||||
}
|
||||
title := strings.TrimSpace(*req.Title)
|
||||
req.Title = &title
|
||||
@@ -376,7 +376,7 @@ func (s *AgentService) CreateAgent(ctx context.Context, req *CreateAgentRequest)
|
||||
// handler layer can map every "not yours" case to the same 404 envelope
|
||||
// — see plan §4.8 IDOR mitigation.
|
||||
//
|
||||
// DAO-error sanitisation (v3.5.2 follow-up): the raw userTenantDAO and
|
||||
// DAO-error sanitization (v3.5.2 follow-up): the raw userTenantDAO and
|
||||
// canvasDAO errors are wrapped with ErrAgentStorageError so mapAgentError
|
||||
// classifies them as CodeServerError (500) with a sanitized message —
|
||||
// the original DAO error string (DSN, table name, gorm stack frame)
|
||||
@@ -385,7 +385,7 @@ func (s *AgentService) CreateAgent(ctx context.Context, req *CreateAgentRequest)
|
||||
//
|
||||
// This is the FIRST storage access path RunAgent hits, so leaving the
|
||||
// raw errors here would have left a DAO-string leak in the very first
|
||||
// hop — the earlier af2ac2eda + 804854a5e commits only sanitised the
|
||||
// hop — the earlier af2ac2eda + 804854a5e commits only sanitized the
|
||||
// version-read path, missing the canvas-access path.
|
||||
func (s *AgentService) loadCanvasForUser(ctx context.Context, userID, canvasID string) (*entity.UserCanvas, error) {
|
||||
if canvasID == "" {
|
||||
@@ -466,7 +466,7 @@ func (s *AgentService) UpdateAgent(ctx context.Context, userID, canvasID string,
|
||||
// extra GET.
|
||||
//
|
||||
// Reset does NOT create a new user_canvas_version row — that mirrors
|
||||
// the Python behaviour and UpdateAgent: versions are owned by
|
||||
// the Python behavior and UpdateAgent: versions are owned by
|
||||
// PublishAgent. It also does NOT touch the in-flight run state of any
|
||||
// currently executing canvas session; that is owned by the Python task
|
||||
// executor and is out of scope for the Go port.
|
||||
@@ -474,7 +474,7 @@ func (s *AgentService) UpdateAgent(ctx context.Context, userID, canvasID string,
|
||||
// Errors propagate the same way as GetAgent: a missing canvas, or a
|
||||
// canvas that the user has no access to, surfaces as
|
||||
// dao.ErrUserCanvasNotFound so mapAgentError emits the same 404 the
|
||||
// Python handler does for "canvas not found.".
|
||||
// Python handler does for "canvas not found."
|
||||
func (s *AgentService) ResetAgent(ctx context.Context, userID, canvasID string) (entity.JSONMap, error) {
|
||||
row, err := s.loadCanvasForUser(ctx, userID, canvasID)
|
||||
if err != nil {
|
||||
@@ -538,13 +538,13 @@ type PublishAgentRequest struct {
|
||||
// overwritten (§2.9); the parent canvas DSL/title/description/release
|
||||
// fields are updated atomically with the new version row.
|
||||
func (s *AgentService) PublishAgent(ctx context.Context, userID, canvasID string, req *PublishAgentRequest) (*entity.UserCanvasVersion, error) {
|
||||
canvas, err := s.loadCanvasForUser(ctx, userID, canvasID)
|
||||
canvasInstance, err := s.loadCanvasForUser(ctx, userID, canvasID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dsl := canvas.DSL
|
||||
title := canvas.Title
|
||||
description := canvas.Description
|
||||
dsl := canvasInstance.DSL
|
||||
title := canvasInstance.Title
|
||||
description := canvasInstance.Description
|
||||
if req != nil {
|
||||
if req.DSL != nil {
|
||||
dsl = dslpkg.NormalizeForCanvas(req.DSL)
|
||||
@@ -563,15 +563,15 @@ func (s *AgentService) PublishAgent(ctx context.Context, userID, canvasID string
|
||||
Description: description,
|
||||
DSL: dsl,
|
||||
}
|
||||
if err := dao.DB.Transaction(func(tx *gorm.DB) error {
|
||||
if err := s.versionDAO.CreateTx(tx, row); err != nil {
|
||||
if err = dao.DB.Transaction(func(tx *gorm.DB) error {
|
||||
if err = s.versionDAO.CreateTx(tx, row); err != nil {
|
||||
return fmt.Errorf("publish agent %s: insert version: %w", canvasID, err)
|
||||
}
|
||||
canvas.DSL = dsl
|
||||
canvas.Title = title
|
||||
canvas.Description = description
|
||||
canvas.Release = true
|
||||
if err := s.canvasDAO.UpdateTx(tx, canvas); err != nil {
|
||||
canvasInstance.DSL = dsl
|
||||
canvasInstance.Title = title
|
||||
canvasInstance.Description = description
|
||||
canvasInstance.Release = true
|
||||
if err = s.canvasDAO.UpdateTx(tx, canvasInstance); err != nil {
|
||||
return fmt.Errorf("publish agent %s: update parent: %w", canvasID, err)
|
||||
}
|
||||
return nil
|
||||
@@ -766,7 +766,7 @@ func (s *AgentService) RunAgent(ctx context.Context, userID, canvasID, sessionID
|
||||
}
|
||||
// Webhook payload injection. Only RunAgentWithWebhook sets this
|
||||
// context value; the chat / agent-run paths leave it nil so the
|
||||
// existing surface is unchanged. The Begin component reads
|
||||
// existing surface is unchanged. The 'BEGIN' component reads
|
||||
// inputs["webhook_payload"] and writes it to state.Sys so
|
||||
// downstream components can read sys.webhook_payload.
|
||||
if payload, ok := ctx.Value(webhookPayloadKey{}).(map[string]any); ok && payload != nil {
|
||||
@@ -804,7 +804,7 @@ func (s *AgentService) RunAgent(ctx context.Context, userID, canvasID, sessionID
|
||||
// drives.
|
||||
//
|
||||
// Phase 4.4 V2: this is the real Compile/Invoke path. The previous
|
||||
// V1 echo placeholder returned a synthesised answer without ever
|
||||
// V1 echo placeholder returned a synthesized answer without ever
|
||||
// calling canvas.Compile — every RunAgent invocation pretended to
|
||||
// run the canvas. The V2 body actually compiles the DSL, attaches
|
||||
// the CanvasState to ctx via runtime.WithState (so component bodies
|
||||
@@ -817,10 +817,10 @@ func (s *AgentService) RunAgent(ctx context.Context, userID, canvasID, sessionID
|
||||
// safely and, when both versionRow and dsl are empty, fall back to
|
||||
// a graceful "no published version" placeholder so the SSE surface
|
||||
// still flows (TestRunAgent_NoVersionPublishedPlaceholder pins this
|
||||
// behaviour). The placeholder is written into state.Outputs under
|
||||
// behavior). The placeholder is written into state.Outputs under
|
||||
// (cpn="answer", bucket="answer") so the answer extraction in
|
||||
// first-pass lookup picks it up; the same trick the V1 placeholder
|
||||
// used (the v3.5.2 fix landed this and we keep it).
|
||||
// used (the v3.5.2 fix landed this, and we keep it).
|
||||
//
|
||||
// Resume path: Runner.Run injects (__resume_interrupt_id__,
|
||||
// __resume_data__) into root when userInput arrives on a session
|
||||
@@ -1001,7 +1001,7 @@ func (s *AgentService) buildRunFunc(canvasID string, versionRow *entity.UserCanv
|
||||
}
|
||||
// On a resume, the user input is the resume payload for the
|
||||
// previously-paused UserFillUp node — it does NOT represent
|
||||
// a fresh sys.query. The begin node writes inputs["query"]
|
||||
// a fresh sys.query. The 'BEGIN' node writes inputs["query"]
|
||||
// straight into state.Sys["query"] (begin.go:76), and
|
||||
// UserFillUp:Menu's initialUserFillUpData reads sys.query
|
||||
// back to drive the menu's initial-input fast path. If we
|
||||
|
||||
@@ -175,7 +175,7 @@ func (m *MinioStorage) Get(bucket, fnm string, tenantID ...string) ([]byte, erro
|
||||
for i := 0; i < 2; i++ {
|
||||
obj, err := m.client.GetObject(ctx, bucket, fnm, minio.GetObjectOptions{})
|
||||
if err != nil {
|
||||
common.Warn("Failed to get object", zap.String("bucket", bucket), zap.String("key", fnm), zap.Error(err))
|
||||
common.Warn("failed to get object", zap.String("bucket", bucket), zap.String("key", fnm), zap.Error(err))
|
||||
m.reconnect()
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
@@ -183,8 +183,8 @@ func (m *MinioStorage) Get(bucket, fnm string, tenantID ...string) ([]byte, erro
|
||||
defer obj.Close()
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if _, err := buf.ReadFrom(obj); err != nil {
|
||||
common.Warn("Failed to read object data", zap.String("bucket", bucket), zap.String("key", fnm), zap.Error(err))
|
||||
if _, err = buf.ReadFrom(obj); err != nil {
|
||||
common.Warn("failed to read object data", zap.String("bucket", bucket), zap.String("key", fnm), zap.Error(err))
|
||||
m.reconnect()
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
|
||||
@@ -128,7 +128,7 @@ func callToolStreamableHTTP(ctx context.Context, endpoint string, headers map[st
|
||||
return nil, formatMCPError("initialize", initRes.Error)
|
||||
}
|
||||
|
||||
if _, _, err := streamableSend(ctx, client, endpoint, sessionID, headers, jsonRPCRequest{
|
||||
if _, _, err = streamableSend(ctx, client, endpoint, sessionID, headers, jsonRPCRequest{
|
||||
JSONRPC: jsonRPCVersion,
|
||||
Method: "notifications/initialized",
|
||||
}, false); err != nil {
|
||||
@@ -137,7 +137,7 @@ func callToolStreamableHTTP(ctx context.Context, endpoint string, headers map[st
|
||||
|
||||
var argsAny any
|
||||
if len(args) > 0 {
|
||||
if err := json.Unmarshal(args, &argsAny); err != nil {
|
||||
if err = json.Unmarshal(args, &argsAny); err != nil {
|
||||
return nil, fmt.Errorf("mcp tools/call: arguments are not valid JSON: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,7 +295,8 @@ func streamableSend(ctx context.Context, client *http.Client, endpoint, sessionI
|
||||
sessionID = sid
|
||||
}
|
||||
if strings.Contains(contentType, "text/event-stream") {
|
||||
r, err := readJSONRPCFromSSE(resp.Body, payload.ID)
|
||||
var r *jsonRPCResponse
|
||||
r, err = readJSONRPCFromSSE(resp.Body, payload.ID)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
@@ -398,7 +399,7 @@ func fetchToolsSSE(ctx context.Context, endpoint string, headers map[string]stri
|
||||
// Register the waiter BEFORE issuing the POST so a fast server that
|
||||
// pushes its response before our wait() call doesn't drop the delivery.
|
||||
initWaiter := pending.register(0)
|
||||
if err := postOnce(jsonRPCRequest{JSONRPC: jsonRPCVersion, ID: 0, Method: "initialize", Params: initializeParams()}); err != nil {
|
||||
if err = postOnce(jsonRPCRequest{JSONRPC: jsonRPCVersion, ID: 0, Method: "initialize", Params: initializeParams()}); err != nil {
|
||||
pending.cancel(0)
|
||||
return nil, err
|
||||
}
|
||||
@@ -409,11 +410,11 @@ func fetchToolsSSE(ctx context.Context, endpoint string, headers map[string]stri
|
||||
if initRes.Error != nil {
|
||||
return nil, formatMCPError("initialize", initRes.Error)
|
||||
}
|
||||
if err := postOnce(jsonRPCRequest{JSONRPC: jsonRPCVersion, Method: "notifications/initialized"}); err != nil {
|
||||
if err = postOnce(jsonRPCRequest{JSONRPC: jsonRPCVersion, Method: "notifications/initialized"}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
listWaiter := pending.register(1)
|
||||
if err := postOnce(jsonRPCRequest{JSONRPC: jsonRPCVersion, ID: 1, Method: "tools/list"}); err != nil {
|
||||
if err = postOnce(jsonRPCRequest{JSONRPC: jsonRPCVersion, ID: 1, Method: "tools/list"}); err != nil {
|
||||
pending.cancel(1)
|
||||
return nil, err
|
||||
}
|
||||
@@ -445,11 +446,13 @@ func waitForEndpoint(ctx context.Context, stream *sseReader, base string) (strin
|
||||
if ref == "" {
|
||||
return "", errors.New("MCP SSE endpoint event has empty data")
|
||||
}
|
||||
baseURL, err := url.Parse(base)
|
||||
var baseURL *url.URL
|
||||
baseURL, err = url.Parse(base)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("parse MCP SSE base url: %w", err)
|
||||
}
|
||||
rel, err := url.Parse(ref)
|
||||
var rel *url.URL
|
||||
rel, err = url.Parse(ref)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("parse MCP SSE endpoint data: %w", err)
|
||||
}
|
||||
@@ -717,21 +720,21 @@ func parseToolsResult(raw json.RawMessage) ([]Tool, error) {
|
||||
return nil, fmt.Errorf("parse tools result: %w", err)
|
||||
}
|
||||
tools := make([]Tool, 0, len(envelope.Tools))
|
||||
for _, raw := range envelope.Tools {
|
||||
name, _ := raw["name"].(string)
|
||||
for _, rawMap := range envelope.Tools {
|
||||
name, _ := rawMap["name"].(string)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
desc, _ := raw["description"].(string)
|
||||
desc, _ := rawMap["description"].(string)
|
||||
var schema map[string]interface{}
|
||||
if s, ok := raw["inputSchema"].(map[string]interface{}); ok {
|
||||
if s, ok := rawMap["inputSchema"].(map[string]interface{}); ok {
|
||||
schema = s
|
||||
}
|
||||
tools = append(tools, Tool{
|
||||
Name: name,
|
||||
Description: desc,
|
||||
InputSchema: schema,
|
||||
Raw: raw,
|
||||
Raw: rawMap,
|
||||
})
|
||||
}
|
||||
return tools, nil
|
||||
@@ -748,7 +751,7 @@ func formatMCPError(method string, e *jsonRPCError) error {
|
||||
// when a low-level connection fails (authentication / network).
|
||||
func mapMCPConnectionError(err error) error {
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
return errors.New("Timeout connecting to MCP server")
|
||||
return errors.New("timeout connecting to MCP server")
|
||||
}
|
||||
return fmt.Errorf("Connection failed (possibly due to auth error). Please check authentication settings first: %v", err)
|
||||
return fmt.Errorf("connection failed (possibly due to auth error). Please check authentication settings first: %v", err)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
// - "oauth2": vanilla OAuth 2.0 authorization-code flow with a
|
||||
// provider-supplied /userinfo endpoint
|
||||
// - "oidc": OAuth 2.0 + OIDC discovery via .well-known/openid-configuration
|
||||
// - "github": OAuth 2.0 plus GitHub's split user / emails endpoints
|
||||
// - "GitHub": OAuth 2.0 plus GitHub's split user / emails endpoints
|
||||
//
|
||||
// Note on OIDC ID-token validation: the Python OIDCClient verifies the
|
||||
// id_token signature against the discovered JWKS and pulls extra claims out
|
||||
@@ -107,7 +107,7 @@ func NewClient(cfg Config) (Client, error) {
|
||||
case "github":
|
||||
return newGitHubClient(cfg)
|
||||
default:
|
||||
return nil, fmt.Errorf("Unsupported type: %s", t)
|
||||
return nil, fmt.Errorf("unsupported type: %s", t)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,23 +169,23 @@ func (c *oauthClient) ExchangeCodeForToken(ctx context.Context, code string) (*T
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.cfg.TokenURL, strings.NewReader(form.Encode()))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to exchange authorization code for token: %w", err)
|
||||
return nil, fmt.Errorf("failed to exchange authorization code for token: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to exchange authorization code for token: %w", err)
|
||||
return nil, fmt.Errorf("failed to exchange authorization code for token: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to exchange authorization code for token: %w", err)
|
||||
return nil, fmt.Errorf("failed to exchange authorization code for token: %w", err)
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
return nil, fmt.Errorf("Failed to exchange authorization code for token: HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
|
||||
return nil, fmt.Errorf("failed to exchange authorization code for token: HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
|
||||
}
|
||||
|
||||
token := &TokenResponse{}
|
||||
@@ -198,11 +198,11 @@ func (c *oauthClient) ExchangeCodeForToken(ctx context.Context, code string) (*T
|
||||
token.IDToken = values.Get("id_token")
|
||||
token.Scope = values.Get("scope")
|
||||
} else {
|
||||
return nil, fmt.Errorf("Failed to exchange authorization code for token: parse response: %w", jerr)
|
||||
return nil, fmt.Errorf("failed to exchange authorization code for token: parse response: %w", jerr)
|
||||
}
|
||||
}
|
||||
if token.AccessToken == "" {
|
||||
return nil, fmt.Errorf("Failed to exchange authorization code for token: empty access_token")
|
||||
return nil, fmt.Errorf("failed to exchange authorization code for token: empty access_token")
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
@@ -211,11 +211,11 @@ func (c *oauthClient) ExchangeCodeForToken(ctx context.Context, code string) (*T
|
||||
// Mirrors OAuthClient.fetch_user_info / normalize_user_info.
|
||||
func (c *oauthClient) FetchUserInfo(ctx context.Context, accessToken, idToken string) (*UserInfo, error) {
|
||||
if c.cfg.UserinfoURL == "" {
|
||||
return nil, fmt.Errorf("Failed to fetch user info: userinfo_url is required")
|
||||
return nil, fmt.Errorf("failed to fetch user info: userinfo_url is required")
|
||||
}
|
||||
raw, err := c.fetchUserinfoRaw(ctx, c.cfg.UserinfoURL, accessToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to fetch user info: %w", err)
|
||||
return nil, fmt.Errorf("failed to fetch user info: %w", err)
|
||||
}
|
||||
return normalizeUserInfo(raw), nil
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ func TestAuthorizationURLPreservesExistingQuery(t *testing.T) {
|
||||
|
||||
func TestNewClientUnsupportedType(t *testing.T) {
|
||||
_, err := NewClient(Config{Type: "saml"})
|
||||
if err == nil || !strings.Contains(err.Error(), "Unsupported type") {
|
||||
if err == nil || !strings.Contains(err.Error(), "unsupported type") {
|
||||
t.Fatalf("expected unsupported-type error, got %v", err)
|
||||
}
|
||||
}
|
||||
@@ -306,7 +306,7 @@ func TestNewOAuthClientMissingFields(t *testing.T) {
|
||||
|
||||
func TestNewOIDCMissingIssuer(t *testing.T) {
|
||||
_, err := NewClient(Config{Type: "oidc"})
|
||||
if err == nil || !strings.Contains(err.Error(), "Missing issuer") {
|
||||
t.Errorf("expected Missing issuer error, got %v", err)
|
||||
if err == nil || !strings.Contains(err.Error(), "missing issuer") {
|
||||
t.Errorf("expected missing issuer error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,11 +52,11 @@ func newGitHubClient(cfg Config) (*gitHubClient, error) {
|
||||
func (c *gitHubClient) FetchUserInfo(ctx context.Context, accessToken, idToken string) (*UserInfo, error) {
|
||||
raw, err := c.fetchUserinfoRaw(ctx, c.cfg.UserinfoURL, accessToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to fetch github user info: %w", err)
|
||||
return nil, fmt.Errorf("failed to fetch github user info: %w", err)
|
||||
}
|
||||
email, err := c.fetchPrimaryEmail(ctx, accessToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to fetch github user info: %w", err)
|
||||
return nil, fmt.Errorf("failed to fetch github user info: %w", err)
|
||||
}
|
||||
if email != "" {
|
||||
raw["email"] = email
|
||||
|
||||
@@ -42,11 +42,11 @@ type oidcClient struct {
|
||||
|
||||
func newOIDCClient(cfg Config) (*oidcClient, error) {
|
||||
if cfg.Issuer == "" {
|
||||
return nil, fmt.Errorf("Missing issuer in configuration.")
|
||||
return nil, fmt.Errorf("missing issuer in configuration")
|
||||
}
|
||||
meta, err := loadOIDCMetadata(cfg.Issuer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to fetch OIDC metadata: %w", err)
|
||||
return nil, fmt.Errorf("failed to fetch OIDC metadata: %w", err)
|
||||
}
|
||||
if meta.Issuer != "" {
|
||||
cfg.Issuer = meta.Issuer
|
||||
|
||||
@@ -145,9 +145,9 @@ func randomStringFromAlphabet(alphabet string, length int) (string, error) {
|
||||
return "", fmt.Errorf("random string length must be positive")
|
||||
}
|
||||
out := make([]byte, length)
|
||||
max := big.NewInt(int64(len(alphabet)))
|
||||
maxInt := big.NewInt(int64(len(alphabet)))
|
||||
for i := 0; i < length; i++ {
|
||||
n, err := rand.Int(rand.Reader, max)
|
||||
n, err := rand.Int(rand.Reader, maxInt)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read random byte: %w", err)
|
||||
}
|
||||
|
||||
@@ -14,16 +14,6 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
// Minimal SMTP sender for transactional email (forgot-password OTP, etc).
|
||||
// Mirrors api/utils/web_utils.py:send_email_html on the Python side and
|
||||
// uses the same conf/service_conf.yaml `smtp` block so a single config
|
||||
// powers both backends.
|
||||
//
|
||||
// The config is passed in as a parameter rather than read via
|
||||
// server.GetConfig() — internal/server already imports internal/utility
|
||||
// (via variable.go), so importing server from here would close an
|
||||
// import cycle. The SMTPConfig type lives in internal/common for the
|
||||
// same reason.
|
||||
package utility
|
||||
|
||||
import (
|
||||
@@ -44,7 +34,7 @@ import (
|
||||
type SMTPNotConfiguredError struct{}
|
||||
|
||||
func (SMTPNotConfiguredError) Error() string {
|
||||
return "smtp is not configured"
|
||||
return "SMTP is not configured"
|
||||
}
|
||||
|
||||
// SMTPInsecureAuthError is returned when authentication is requested over
|
||||
@@ -53,7 +43,7 @@ func (SMTPNotConfiguredError) Error() string {
|
||||
type SMTPInsecureAuthError struct{}
|
||||
|
||||
func (SMTPInsecureAuthError) Error() string {
|
||||
return "smtp authentication refused over plaintext connection (set mail_use_ssl or mail_use_tls)"
|
||||
return "SMTP authentication refused over plaintext connection (set mail_use_ssl or mail_use_tls)"
|
||||
}
|
||||
|
||||
// SendResetCodeEmail delivers the password-reset OTP email. It is the Go
|
||||
@@ -92,7 +82,7 @@ func SendResetCodeEmail(cfg common.SMTPConfig, toEmail, otp string, ttlMinutes i
|
||||
|
||||
msg := buildPlainEmail(fromHeader, toEmail, subject, body)
|
||||
if err := sendMail(cfg, fromAddr, toEmail, msg); err != nil {
|
||||
common.Warn("smtp send failed",
|
||||
common.Warn("SMTP send failed",
|
||||
zap.String("to", toEmail),
|
||||
zap.String("server", cfg.MailServer),
|
||||
zap.Int("port", cfg.MailPort),
|
||||
@@ -140,17 +130,17 @@ func sendMail(cfg common.SMTPConfig, from, to string, msg []byte) error {
|
||||
}
|
||||
conn, err := tls.Dial("tcp", addr, tlsCfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("smtp tls dial: %w", err)
|
||||
return fmt.Errorf("SMTP tls dial: %w", err)
|
||||
}
|
||||
client, err := smtp.NewClient(conn, cfg.MailServer)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return fmt.Errorf("smtp client init: %w", err)
|
||||
return fmt.Errorf("SMTP client init: %w", err)
|
||||
}
|
||||
defer client.Quit()
|
||||
if cfg.MailUsername != "" {
|
||||
if err := client.Auth(auth); err != nil {
|
||||
return fmt.Errorf("smtp auth: %w", err)
|
||||
if err = client.Auth(auth); err != nil {
|
||||
return fmt.Errorf("SMTP auth: %w", err)
|
||||
}
|
||||
}
|
||||
return deliverMail(client, from, to, msg)
|
||||
@@ -159,7 +149,7 @@ func sendMail(cfg common.SMTPConfig, from, to string, msg []byte) error {
|
||||
// STARTTLS (typical port 587) or plain (auth refused above).
|
||||
client, err := smtp.Dial(addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("smtp dial: %w", err)
|
||||
return fmt.Errorf("SMTP dial: %w", err)
|
||||
}
|
||||
defer client.Quit()
|
||||
if cfg.MailUseTLS {
|
||||
@@ -167,12 +157,12 @@ func sendMail(cfg common.SMTPConfig, from, to string, msg []byte) error {
|
||||
ServerName: cfg.MailServer,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
if err := client.StartTLS(tlsCfg); err != nil {
|
||||
return fmt.Errorf("smtp starttls: %w", err)
|
||||
if err = client.StartTLS(tlsCfg); err != nil {
|
||||
return fmt.Errorf("SMTP starttls: %w", err)
|
||||
}
|
||||
if cfg.MailUsername != "" {
|
||||
if err := client.Auth(auth); err != nil {
|
||||
return fmt.Errorf("smtp auth: %w", err)
|
||||
if err = client.Auth(auth); err != nil {
|
||||
return fmt.Errorf("SMTP auth: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -182,26 +172,26 @@ func sendMail(cfg common.SMTPConfig, from, to string, msg []byte) error {
|
||||
|
||||
func deliverMail(client *smtp.Client, from, to string, msg []byte) error {
|
||||
if err := client.Mail(from); err != nil {
|
||||
return fmt.Errorf("smtp mail-from: %w", err)
|
||||
return fmt.Errorf("SMTP mail-from: %w", err)
|
||||
}
|
||||
if err := client.Rcpt(to); err != nil {
|
||||
return fmt.Errorf("smtp rcpt-to: %w", err)
|
||||
return fmt.Errorf("SMTP rcpt-to: %w", err)
|
||||
}
|
||||
w, err := client.Data()
|
||||
if err != nil {
|
||||
return fmt.Errorf("smtp data: %w", err)
|
||||
return fmt.Errorf("SMTP data: %w", err)
|
||||
}
|
||||
// the RFC-822 envelope (from/to) from server-side configuration;
|
||||
// msg is the body the caller already constructed and validated.
|
||||
// Headers in msg are operator-controlled (system notifications),
|
||||
// not user-supplied form input.
|
||||
// codeql[go/email-injection] False positive: deliverMail builds
|
||||
if _, err := w.Write(msg); err != nil {
|
||||
if _, err = w.Write(msg); err != nil {
|
||||
w.Close()
|
||||
return fmt.Errorf("smtp write: %w", err)
|
||||
return fmt.Errorf("SMTP write: %w", err)
|
||||
}
|
||||
if err := w.Close(); err != nil {
|
||||
return fmt.Errorf("smtp close: %w", err)
|
||||
if err = w.Close(); err != nil {
|
||||
return fmt.Errorf("SMTP close: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -63,39 +63,39 @@ func allowAnyHost() bool {
|
||||
//
|
||||
// Mirrors common/ssrf_guard.py:assert_url_is_safe.
|
||||
func AssertURLSafe(rawURL string) (hostname, resolvedIP string, err error) {
|
||||
parsed, perr := url.Parse(strings.TrimSpace(rawURL))
|
||||
if perr != nil {
|
||||
return "", "", fmt.Errorf("Invalid url.")
|
||||
parsed, err := url.Parse(strings.TrimSpace(rawURL))
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("invalid url")
|
||||
}
|
||||
|
||||
scheme := strings.ToLower(parsed.Scheme)
|
||||
if !schemeAllowed(scheme) {
|
||||
sorted := append([]string(nil), AllowedURLSchemes...)
|
||||
sort.Strings(sorted)
|
||||
return "", "", fmt.Errorf("Disallowed URL scheme: '%s'. Only %v are allowed.", scheme, sorted)
|
||||
return "", "", fmt.Errorf("disallowed URL scheme: '%s'. Only %v are allowed", scheme, sorted)
|
||||
}
|
||||
|
||||
hostname = parsed.Hostname()
|
||||
if hostname == "" {
|
||||
return "", "", fmt.Errorf("URL is missing a host.")
|
||||
return "", "", fmt.Errorf("URL is missing a host")
|
||||
}
|
||||
|
||||
allowAny := allowAnyHost()
|
||||
addrs, err := LookupHost(hostname)
|
||||
addresses, err := LookupHost(hostname)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("Could not resolve hostname '%s': %v", hostname, err)
|
||||
return "", "", fmt.Errorf("could not resolve hostname '%s': %v", hostname, err)
|
||||
}
|
||||
if len(addrs) == 0 {
|
||||
return "", "", fmt.Errorf("Hostname '%s' resolved to no addresses.", hostname)
|
||||
if len(addresses) == 0 {
|
||||
return "", "", fmt.Errorf("hostname '%s' resolved to no addresses", hostname)
|
||||
}
|
||||
|
||||
for _, addr := range addrs {
|
||||
for _, addr := range addresses {
|
||||
ip := net.ParseIP(addr)
|
||||
if ip == nil {
|
||||
return "", "", fmt.Errorf("Could not parse resolved address '%s' for hostname '%s'.", addr, hostname)
|
||||
return "", "", fmt.Errorf("could not parse resolved address '%s' for hostname '%s'", addr, hostname)
|
||||
}
|
||||
if !allowAny && !isGlobalIP(effectiveIP(ip)) {
|
||||
return "", "", fmt.Errorf("URL resolves to a non-public address (%s), which is not allowed.", ip.String())
|
||||
return "", "", fmt.Errorf("URL resolves to a non-public address (%s), which is not allowed", ip.String())
|
||||
}
|
||||
if resolvedIP == "" {
|
||||
resolvedIP = ip.String()
|
||||
|
||||
@@ -89,7 +89,7 @@ func TestAssertURLSafe(t *testing.T) {
|
||||
name: "disallowed scheme ftp",
|
||||
url: "ftp://example.com/",
|
||||
ips: []string{"93.184.216.34"},
|
||||
want: want{errSubstr: "Disallowed URL scheme"},
|
||||
want: want{errSubstr: "disallowed URL scheme"},
|
||||
},
|
||||
{
|
||||
name: "missing host",
|
||||
@@ -100,7 +100,7 @@ func TestAssertURLSafe(t *testing.T) {
|
||||
name: "resolution fails",
|
||||
url: "http://nosuchhost.test/x",
|
||||
err: "no such host",
|
||||
want: want{errSubstr: "Could not resolve"},
|
||||
want: want{errSubstr: "could not resolve"},
|
||||
},
|
||||
{
|
||||
name: "all addresses must be public",
|
||||
|
||||
@@ -129,7 +129,7 @@ func urlSafeB64Encode(data []byte) string {
|
||||
return strings.TrimRight(encoded, "=")
|
||||
}
|
||||
|
||||
// generateSecretKey generates a 32-byte hex string (equivalent to Python's secrets.token_hex(32))
|
||||
// GenerateSecretKey generates a 32-byte hex string (equivalent to Python's secrets.token_hex(32))
|
||||
func GenerateSecretKey() (string, error) {
|
||||
bytes := make([]byte, 32) // 32 bytes = 256 bits
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
|
||||
@@ -51,7 +51,8 @@ func getRAGFlowVersionInternal() string {
|
||||
dir := filepath.Dir(exePath)
|
||||
for i := 0; i < 5; i++ { // Try up to 5 levels up
|
||||
versionPath := filepath.Join(dir, "VERSION")
|
||||
if data, err := os.ReadFile(versionPath); err == nil {
|
||||
var data []byte
|
||||
if data, err = os.ReadFile(versionPath); err == nil {
|
||||
return strings.TrimSpace(string(data))
|
||||
}
|
||||
parent := filepath.Dir(dir)
|
||||
|
||||
Reference in New Issue
Block a user