diff --git a/cmd/derper/depaware.txt b/cmd/derper/depaware.txt index fad4c9a08..286af04e7 100644 --- a/cmd/derper/depaware.txt +++ b/cmd/derper/depaware.txt @@ -78,7 +78,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa tailscale.com/util/lineread from tailscale.com/hostinfo+ tailscale.com/util/mak from tailscale.com/syncs tailscale.com/util/singleflight from tailscale.com/net/dnscache - L tailscale.com/util/strs from tailscale.com/hostinfo + tailscale.com/util/strs from tailscale.com/hostinfo+ W 💣 tailscale.com/util/winutil from tailscale.com/hostinfo+ tailscale.com/version from tailscale.com/derp+ tailscale.com/version/distro from tailscale.com/hostinfo+ diff --git a/cmd/derper/mesh.go b/cmd/derper/mesh.go index 24ea83e5f..a6e6be3f4 100644 --- a/cmd/derper/mesh.go +++ b/cmd/derper/mesh.go @@ -17,6 +17,7 @@ import ( "tailscale.com/derp/derphttp" "tailscale.com/types/key" "tailscale.com/types/logger" + "tailscale.com/util/strs" ) func startMesh(s *derp.Server) error { @@ -50,8 +51,7 @@ func startMeshWithHost(s *derp.Server, host string) error { } var d net.Dialer var r net.Resolver - if port == "443" && strings.HasSuffix(host, ".tailscale.com") { - base := strings.TrimSuffix(host, ".tailscale.com") + if base, ok := strs.CutSuffix(host, ".tailscale.com"); ok && port == "443" { subCtx, cancel := context.WithTimeout(ctx, 2*time.Second) defer cancel() vpcHost := base + "-vpc.tailscale.com" diff --git a/cmd/tailscale/cli/debug.go b/cmd/tailscale/cli/debug.go index 3952b8ccb..bbd4508c5 100644 --- a/cmd/tailscale/cli/debug.go +++ b/cmd/tailscale/cli/debug.go @@ -37,6 +37,7 @@ import ( "tailscale.com/tailcfg" "tailscale.com/types/key" "tailscale.com/types/logger" + "tailscale.com/util/strs" ) var debugCmd = &ffcli.Command{ @@ -228,9 +229,8 @@ func runDebug(ctx context.Context, args []string) error { e.Encode(wfs) return nil } - delete := strings.HasPrefix(debugArgs.file, "delete:") - if delete { - return localClient.DeleteWaitingFile(ctx, strings.TrimPrefix(debugArgs.file, "delete:")) + if name, ok := strs.CutPrefix(debugArgs.file, "delete:"); ok { + return localClient.DeleteWaitingFile(ctx, name) } rc, size, err := localClient.GetWaitingFile(ctx, debugArgs.file) if err != nil { diff --git a/cmd/tailscale/cli/file.go b/cmd/tailscale/cli/file.go index 6986f58ca..b19ee4d5d 100644 --- a/cmd/tailscale/cli/file.go +++ b/cmd/tailscale/cli/file.go @@ -30,6 +30,7 @@ import ( "tailscale.com/net/tsaddr" "tailscale.com/tailcfg" "tailscale.com/util/quarantine" + "tailscale.com/util/strs" "tailscale.com/version" ) @@ -77,10 +78,10 @@ func runCp(ctx context.Context, args []string) error { return errors.New("usage: tailscale file cp :") } files, target := args[:len(args)-1], args[len(args)-1] - if !strings.HasSuffix(target, ":") { + target, ok := strs.CutSuffix(target, ":") + if !ok { return fmt.Errorf("final argument to 'tailscale file cp' must end in colon") } - target = strings.TrimSuffix(target, ":") hadBrackets := false if strings.HasPrefix(target, "[") && strings.HasSuffix(target, "]") { hadBrackets = true diff --git a/cmd/tailscale/cli/up.go b/cmd/tailscale/cli/up.go index f8d2ec10a..d1a7db41a 100644 --- a/cmd/tailscale/cli/up.go +++ b/cmd/tailscale/cli/up.go @@ -33,6 +33,7 @@ import ( "tailscale.com/tailcfg" "tailscale.com/types/logger" "tailscale.com/types/preftype" + "tailscale.com/util/strs" "tailscale.com/version" "tailscale.com/version/distro" ) @@ -166,8 +167,7 @@ type upArgsT struct { func (a upArgsT) getAuthKey() (string, error) { v := a.authKeyOrFile - if strings.HasPrefix(v, "file:") { - file := strings.TrimPrefix(v, "file:") + if file, ok := strs.CutPrefix(v, "file:"); ok { b, err := os.ReadFile(file) if err != nil { return "", err diff --git a/cmd/tailscale/depaware.txt b/cmd/tailscale/depaware.txt index 27927ec79..006d5ddd2 100644 --- a/cmd/tailscale/depaware.txt +++ b/cmd/tailscale/depaware.txt @@ -108,7 +108,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep tailscale.com/util/multierr from tailscale.com/control/controlhttp tailscale.com/util/quarantine from tailscale.com/cmd/tailscale/cli tailscale.com/util/singleflight from tailscale.com/net/dnscache - L tailscale.com/util/strs from tailscale.com/hostinfo + tailscale.com/util/strs from tailscale.com/hostinfo+ W 💣 tailscale.com/util/winutil from tailscale.com/hostinfo+ tailscale.com/version from tailscale.com/cmd/tailscale/cli+ tailscale.com/version/distro from tailscale.com/cmd/tailscale/cli+ diff --git a/derp/derphttp/derphttp_client.go b/derp/derphttp/derphttp_client.go index f0448d250..6c56e3096 100644 --- a/derp/derphttp/derphttp_client.go +++ b/derp/derphttp/derphttp_client.go @@ -24,7 +24,6 @@ import ( "net/netip" "net/url" "runtime" - "strings" "sync" "time" @@ -39,6 +38,7 @@ import ( "tailscale.com/tailcfg" "tailscale.com/types/key" "tailscale.com/types/logger" + "tailscale.com/util/strs" ) // Client is a DERP-over-HTTP client. @@ -1028,9 +1028,10 @@ var ErrClientClosed = errors.New("derphttp.Client closed") func parseMetaCert(certs []*x509.Certificate) (serverPub key.NodePublic, serverProtoVersion int) { for _, cert := range certs { - if cn := cert.Subject.CommonName; strings.HasPrefix(cn, "derpkey") { + // Look for derpkey prefix added by initMetacert() on the server side. + if pubHex, ok := strs.CutPrefix(cert.Subject.CommonName, "derpkey"); ok { var err error - serverPub, err = key.ParseNodePublicUntyped(mem.S(strings.TrimPrefix(cn, "derpkey"))) + serverPub, err = key.ParseNodePublicUntyped(mem.S(pubHex)) if err == nil && cert.SerialNumber.BitLen() <= 8 { // supports up to version 255 return serverPub, int(cert.SerialNumber.Int64()) } diff --git a/disco/disco_test.go b/disco/disco_test.go index 25070d581..84fa440cd 100644 --- a/disco/disco_test.go +++ b/disco/disco_test.go @@ -8,11 +8,11 @@ import ( "fmt" "net/netip" "reflect" - "strings" "testing" "go4.org/mem" "tailscale.com/types/key" + "tailscale.com/util/strs" ) func TestMarshalAndParse(t *testing.T) { @@ -72,10 +72,10 @@ func TestMarshalAndParse(t *testing.T) { t.Run(tt.name, func(t *testing.T) { foo := []byte("foo") got := string(tt.m.AppendMarshal(foo)) - if !strings.HasPrefix(got, "foo") { + got, ok := strs.CutPrefix(got, "foo") + if !ok { t.Fatalf("didn't start with foo: got %q", got) } - got = strings.TrimPrefix(got, "foo") gotHex := fmt.Sprintf("% x", got) if gotHex != tt.want { diff --git a/ipn/ipnlocal/peerapi.go b/ipn/ipnlocal/peerapi.go index 382b4cadc..c5cf2749f 100644 --- a/ipn/ipnlocal/peerapi.go +++ b/ipn/ipnlocal/peerapi.go @@ -166,13 +166,13 @@ func (s *peerAPIServer) hasFilesWaiting() bool { if strings.HasSuffix(name, partialSuffix) { continue } - if strings.HasSuffix(name, deletedSuffix) { // for Windows + tests + if name, ok := strs.CutSuffix(name, deletedSuffix); ok { // for Windows + tests // After we're done looping over files, then try // to delete this file. Don't do it proactively, // as the OS may return "foo.jpg.deleted" before "foo.jpg" // and we don't want to delete the ".deleted" file before // enumerating to the "foo.jpg" file. - defer tryDeleteAgain(filepath.Join(s.rootDir, strings.TrimSuffix(name, deletedSuffix))) + defer tryDeleteAgain(filepath.Join(s.rootDir, name)) continue } if de.Type().IsRegular() { @@ -225,11 +225,11 @@ func (s *peerAPIServer) WaitingFiles() (ret []apitype.WaitingFile, err error) { if strings.HasSuffix(name, partialSuffix) { continue } - if strings.HasSuffix(name, deletedSuffix) { // for Windows + tests + if name, ok := strs.CutSuffix(name, deletedSuffix); ok { // for Windows + tests if deleted == nil { deleted = map[string]bool{} } - deleted[strings.TrimSuffix(name, deletedSuffix)] = true + deleted[name] = true continue } if de.Type().IsRegular() { diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go index 700be77f1..89366aa41 100644 --- a/ipn/localapi/localapi.go +++ b/ipn/localapi/localapi.go @@ -670,7 +670,11 @@ func (h *Handler) serveFiles(w http.ResponseWriter, r *http.Request) { http.Error(w, "file access denied", http.StatusForbidden) return } - suffix := strings.TrimPrefix(r.URL.EscapedPath(), "/localapi/v0/files/") + suffix, ok := strs.CutPrefix(r.URL.EscapedPath(), "/localapi/v0/files/") + if !ok { + http.Error(w, "misconfigured", http.StatusInternalServerError) + return + } if suffix == "" { if r.Method != "GET" { http.Error(w, "want GET to list files", 400) @@ -774,7 +778,11 @@ func (h *Handler) serveFilePut(w http.ResponseWriter, r *http.Request) { return } - upath := strings.TrimPrefix(r.URL.EscapedPath(), "/localapi/v0/file-put/") + upath, ok := strs.CutPrefix(r.URL.EscapedPath(), "/localapi/v0/file-put/") + if !ok { + http.Error(w, "misconfigured", http.StatusInternalServerError) + return + } stableIDStr, filenameEscaped, ok := strings.Cut(upath, "/") if !ok { http.Error(w, "bogus URL", 400) @@ -1159,7 +1167,8 @@ func (h *Handler) serveProfiles(w http.ResponseWriter, r *http.Request) { } suffix, ok := strs.CutPrefix(r.URL.EscapedPath(), "/localapi/v0/profiles/") if !ok { - panic("misconfigured") + http.Error(w, "misconfigured", http.StatusInternalServerError) + return } if suffix == "" { switch r.Method { diff --git a/net/dns/resolvconffile/resolvconffile.go b/net/dns/resolvconffile/resolvconffile.go index 8ddc71357..a1b334aa3 100644 --- a/net/dns/resolvconffile/resolvconffile.go +++ b/net/dns/resolvconffile/resolvconffile.go @@ -21,6 +21,7 @@ import ( "strings" "tailscale.com/util/dnsname" + "tailscale.com/util/strs" ) // Path is the canonical location of resolv.conf. @@ -68,8 +69,7 @@ func Parse(r io.Reader) (*Config, error) { line, _, _ = strings.Cut(line, "#") // remove any comments line = strings.TrimSpace(line) - if strings.HasPrefix(line, "nameserver") { - s := strings.TrimPrefix(line, "nameserver") + if s, ok := strs.CutPrefix(line, "nameserver"); ok { nameserver := strings.TrimSpace(s) if len(nameserver) == len(s) { return nil, fmt.Errorf("missing space after \"nameserver\" in %q", line) @@ -82,8 +82,7 @@ func Parse(r io.Reader) (*Config, error) { continue } - if strings.HasPrefix(line, "search") { - s := strings.TrimPrefix(line, "search") + if s, ok := strs.CutPrefix(line, "search"); ok { domain := strings.TrimSpace(s) if len(domain) == len(s) { // No leading space?! diff --git a/net/dns/resolver/tsdns.go b/net/dns/resolver/tsdns.go index 74d7fd0e7..3a9684ebd 100644 --- a/net/dns/resolver/tsdns.go +++ b/net/dns/resolver/tsdns.go @@ -34,6 +34,7 @@ import ( "tailscale.com/util/clientmetric" "tailscale.com/util/cloudenv" "tailscale.com/util/dnsname" + "tailscale.com/util/strs" "tailscale.com/wgengine/monitor" ) @@ -1215,8 +1216,7 @@ func (r *Resolver) respond(query []byte) ([]byte, error) { // unARPA maps from "4.4.8.8.in-addr.arpa." to "8.8.4.4", etc. func unARPA(a string) (ipStr string, ok bool) { const suf4 = ".in-addr.arpa." - if strings.HasSuffix(a, suf4) { - s := strings.TrimSuffix(a, suf4) + if s, ok := strs.CutSuffix(a, suf4); ok { // Parse and reverse octets. ip, err := netip.ParseAddr(s) if err != nil || !ip.Is4() { diff --git a/tsweb/tsweb.go b/tsweb/tsweb.go index ec9388191..31c0cef87 100644 --- a/tsweb/tsweb.go +++ b/tsweb/tsweb.go @@ -32,6 +32,7 @@ import ( "tailscale.com/metrics" "tailscale.com/net/tsaddr" "tailscale.com/types/logger" + "tailscale.com/util/strs" "tailscale.com/version" ) @@ -740,7 +741,7 @@ func structTypeSortedFields(t reflect.Type) []sortedStructField { // removed. func removeTypePrefixes(s string) string { for _, prefix := range prefixesToTrim { - if trimmed := strings.TrimPrefix(s, prefix); trimmed != s { + if trimmed, ok := strs.CutPrefix(s, prefix); ok { return trimmed } } diff --git a/version/cmdname.go b/version/cmdname.go index ea5932197..e6aebc8ca 100644 --- a/version/cmdname.go +++ b/version/cmdname.go @@ -15,6 +15,8 @@ import ( "path" "path/filepath" "strings" + + "tailscale.com/util/strs" ) // CmdName returns either the base name of the current binary @@ -41,9 +43,8 @@ func cmdName(exe string) string { // v is like: // "path\ttailscale.com/cmd/tailscale\nmod\ttailscale.com\t(devel)\t\ndep\tgithub.com/apenwarr/fixconsole\tv0.0.0-20191012055117-5a9f6489cc29\th1:muXWUcay7DDy1/hEQWrYlBy+g0EuwT70sBHg65SeUc4=\ndep\tgithub.... for _, line := range strings.Split(info, "\n") { - if strings.HasPrefix(line, "path\t") { - goPkg := strings.TrimPrefix(line, "path\t") // like "tailscale.com/cmd/tailscale" - ret = path.Base(goPkg) // goPkg is always forward slashes; use path, not filepath + if goPkg, ok := strs.CutPrefix(line, "path\t"); ok { // like "tailscale.com/cmd/tailscale" + ret = path.Base(goPkg) // goPkg is always forward slashes; use path, not filepath break } }