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
10 changes: 2 additions & 8 deletions defs/defs.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package defs
import (
"encoding/json"
"net"
"runtime"
"strings"
)

Expand All @@ -12,13 +11,8 @@ var (
BuildDate string
ProgName string
ProgVersion string
// UserAgent names the platform as well as the program, the way a browser
// does. A server's telemetry already stores this header, so reporting the
// operating system and architecture here is what lets an operator tell
// which kinds of machine are measuring against them without any change to
// what the client sends. It stays coarse deliberately: the kernel version
// or the hostname would identify the machine rather than describe it.
UserAgent = ProgName + "/" + ProgVersion + " (" + runtime.GOOS + "; " + runtime.GOARCH + ")"
// UserAgent names the platform as well as the program; see buildUserAgent.
UserAgent = buildUserAgent()
)

// GetIPResults represents the returned JSON from backend server's getIP.php endpoint
Expand Down
60 changes: 60 additions & 0 deletions defs/useragent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package defs

import (
"os"
"runtime"
"strings"
)

// buildUserAgent reports the program, its version, the platform, and on Linux
// the distribution ID. The Rust client sends the same shape.
func buildUserAgent() string {
platform := runtime.GOOS + "; " + runtime.GOARCH

if id := distributionID(); id != "" {
platform += "; " + id
}

return ProgName + "/" + ProgVersion + " (" + platform + ")"
}

// distributionID returns the ID from os-release, e.g. "openwrt", or "" where
// neither of its two standard locations exists.
func distributionID() string {
b, err := os.ReadFile("/etc/os-release")
if err != nil {
if b, err = os.ReadFile("/usr/lib/os-release"); err != nil {
return ""
}
}
return parseOsReleaseID(string(b))
}

// parseOsReleaseID extracts ID from os-release content, sanitised and capped:
// it ends up in a header built from a root-writable file.
func parseOsReleaseID(content string) string {
for _, line := range strings.Split(content, "\n") {
bare, ok := strings.CutPrefix(line, "ID=")
if !ok {
continue
}
bare = strings.Trim(strings.TrimSpace(bare), `"'`)

var b strings.Builder
for _, r := range strings.ToLower(bare) {
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') ||
r == '.' || r == '_' || r == '-' {
b.WriteRune(r)
}
if b.Len() >= 24 {
break
}
}

if b.Len() > 0 {
return b.String()
}
}

return ""
}
37 changes: 37 additions & 0 deletions defs/useragent_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package defs

import (
"strings"
"testing"
)

func TestParseOsReleaseID(t *testing.T) {
cases := []struct {
name string
content string
want string
}{
{"openwrt", "ID=\"openwrt\"\nVERSION_ID=\"24.10.1\"\n", "openwrt"},
{"unquoted", "ID=debian\nVERSION_ID=12\n", "debian"},
// ID_LIKE and VERSION_ID must not be mistaken for ID.
{"id_like first", "ID_LIKE=debian\nID=ubuntu\n", "ubuntu"},
{"version only", "VERSION_ID=1.0\n", ""},
{"empty", "", ""},
{"garbage id", "ID=\"???\"\n", ""},
{"uppercase and junk", "ID='OpenWrt!'\n", "openwrt"},
}

for _, c := range cases {
if got := parseOsReleaseID(c.content); got != c.want {
t.Errorf("%s: parseOsReleaseID = %q, want %q", c.name, got, c.want)
}
}
}

// The value lands in an HTTP header and comes from a root-writable file.
func TestParseOsReleaseIDIsBounded(t *testing.T) {
long := "ID=" + strings.Repeat("a", 200) + "\n"
if got := parseOsReleaseID(long); len(got) != 24 {
t.Errorf("parseOsReleaseID length = %d, want 24", len(got))
}
}