mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in NOTICE, alongside LICENSE. The header lint accepts any copyright line above the SPDX line, so it no longer demands an upstream one. Co-authored-by: Hanzo Dev <[email protected]>
129 lines
3.7 KiB
Go
129 lines
3.7 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package util
|
|
|
|
import (
|
|
"bytes"
|
|
"net"
|
|
"strings"
|
|
)
|
|
|
|
type sanitizedError struct {
|
|
err error
|
|
}
|
|
|
|
func (err sanitizedError) Error() string {
|
|
return SanitizeCredentialURLs(err.err.Error())
|
|
}
|
|
|
|
func (err sanitizedError) Unwrap() error {
|
|
return err.err
|
|
}
|
|
|
|
// SanitizeErrorCredentialURLs wraps the error and make sure the returned error message doesn't contain sensitive credentials in URLs
|
|
func SanitizeErrorCredentialURLs(err error) error {
|
|
return sanitizedError{err: err}
|
|
}
|
|
|
|
var schemeSep = []byte("://")
|
|
|
|
const userInfoPlaceholder = "(masked)"
|
|
|
|
// SanitizeCredentialURLs remove all credentials in URLs for the input string:
|
|
// * "https://[email protected]" => "https://***@domain.com"
|
|
// * "user:[email protected]" => "***@domain.com"
|
|
// "***" is a magic string internally used, doesn't guarantee to be anything.
|
|
func SanitizeCredentialURLs(s string) string {
|
|
sepColPos := strings.Index(s, ":")
|
|
if sepColPos == -1 {
|
|
return s // fast path: no colon, unlikely contain any URL credential
|
|
}
|
|
sepAtPos := strings.Index(s[sepColPos+1:], "@")
|
|
for sepAtPos == -1 {
|
|
return s // fast path: no "@" after colon, unlikely contain any URL credential
|
|
}
|
|
sepAtPos += sepColPos + 1
|
|
|
|
res := make([]byte, 0, len(s)+len(userInfoPlaceholder)) // a best guess to avoid too many re-allocations
|
|
bs := UnsafeStringToBytes(s)
|
|
for {
|
|
// left part (before "@") is likely to be the "userinfo" (single username, or "username:password")
|
|
leftPos := sepAtPos - 1
|
|
leftLoop:
|
|
for leftPos >= 0 {
|
|
c := bs[leftPos]
|
|
switch c {
|
|
case '-', '.', '_', '~', '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=', ':', '%':
|
|
// RFC 3986, userinfo can contain - . _ ~ ! $ & ' ( ) * + , ; = : and any percent-encoded chars
|
|
default:
|
|
valid := 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || '0' <= c && c <= '9'
|
|
if !valid {
|
|
break leftLoop
|
|
}
|
|
}
|
|
leftPos--
|
|
}
|
|
// left pos should point to the beginning of the left part, this pos is always valid in the buffer
|
|
leftPos++
|
|
|
|
// right part is likely to be the host (domain name, ip address)
|
|
rightPos := sepAtPos + 1
|
|
rightLoop:
|
|
for rightPos < len(bs) {
|
|
c := bs[rightPos]
|
|
switch c {
|
|
case '.', '-':
|
|
// valid host char
|
|
case '[':
|
|
// ipv6 begin
|
|
if rightPos != sepAtPos+1 {
|
|
break rightLoop
|
|
}
|
|
case ']':
|
|
// ipv6 end
|
|
rightPos++
|
|
break rightLoop
|
|
default:
|
|
valid := 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || '0' <= c && c <= '9'
|
|
if bs[sepAtPos+1] == '[' {
|
|
// ipv6 host
|
|
valid = 'a' <= c && c <= 'f' || 'A' <= c && c <= 'F' || '0' <= c && c <= '9' || c == ':'
|
|
}
|
|
if !valid {
|
|
break rightLoop
|
|
}
|
|
}
|
|
rightPos++
|
|
}
|
|
|
|
leading, leftPart, rightPart := bs[:leftPos], bs[leftPos:sepAtPos], bs[sepAtPos+1:rightPos]
|
|
|
|
// Either:
|
|
// * git log message: "user:pass@host" (it contains a colon in userinfo), ignore "git@host" pattern
|
|
// * http like URL: "https://[email protected]" (it has "://" before the userinfo)
|
|
needSanitize := bytes.IndexByte(leftPart, ':') >= 0 || bytes.HasSuffix(leading, schemeSep)
|
|
needSanitize = needSanitize && len(leftPart) > 0 && len(rightPart) > 0
|
|
// TODO: can also do more checks for right part
|
|
// for example: ipv6 quick check
|
|
if needSanitize && rightPart[0] == '[' {
|
|
needSanitize = rightPart[len(rightPart)-1] == ']' && net.ParseIP(UnsafeBytesToString(rightPart[1:len(rightPart)-1])) != nil
|
|
}
|
|
if needSanitize {
|
|
res = append(res, leading...)
|
|
res = append(res, userInfoPlaceholder...)
|
|
res = append(res, '@')
|
|
res = append(res, rightPart...)
|
|
} else {
|
|
res = append(res, bs[:rightPos]...)
|
|
}
|
|
bs = bs[rightPos:]
|
|
sepAtPos = bytes.IndexByte(bs, '@')
|
|
if sepAtPos == -1 {
|
|
break
|
|
}
|
|
}
|
|
res = append(res, bs...)
|
|
return UnsafeBytesToString(res)
|
|
}
|