diff --git a/LICENSE b/LICENSE index 14b035e..ae5ae84 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015-2023 Kenneth Shaw +Copyright (c) 2015-2024 Kenneth Shaw Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index ba42ecd..7ca2ea4 100644 --- a/README.md +++ b/README.md @@ -1,72 +1,29 @@ -# omahaproxy +# verhist -Package `omahaproxy` provides a simple client to retrieve data from [Omaha -Proxy][omahaproxy]. +Package `verhist` provides a simple client to retrieve the latest release +versions of Chrome using the [version history API][verhist]. -[omahaproxy]: https://omahaproxy.appspot.com +[verhist]: https://developer.chrome.com/docs/web-platform/versionhistory/guide + +Can also be used to build the latest user agent for Chrome. ## Example ```go -// _example/example.go -package main +package verhist_test import ( "context" - "flag" "fmt" - "os" - "runtime" - "github.com/chromedp/omahaproxy" + "github.com/chromedp/verhist" ) -func main() { - platform := "linux" - switch runtime.GOOS { - case "windows": - platform = "win" - if runtime.GOARCH == "amd64" { - platform += "64" - } - case "darwin": - platform = "mac" - if runtime.GOARCH == "aarch64" { - platform += "_arm64" - } - } - verbose := flag.Bool("v", false, "verbose") - osstr := flag.String("os", platform, "os") - channel := flag.String("channel", "stable", "channel") - flag.Parse() - if err := run(context.Background(), *verbose, *osstr, *channel); err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n", err) - os.Exit(1) - } -} - -func run(ctx context.Context, verbose bool, os, channel string) error { - // enable verbose - var opts []omahaproxy.Option - if verbose { - opts = append(opts, omahaproxy.WithLogf(fmt.Printf)) - } - // create client - cl := omahaproxy.New(opts...) - // retrieve recent - releases, err := cl.Recent(ctx) - if err != nil { - return err - } - for _, release := range releases { - fmt.Printf("os: %s channel: %s version: %s\n", release.OS, release.Channel, release.Version) - } - // show latest - ver, err := cl.Latest(ctx, os, channel) +func Example() { + userAgent, err := verhist.UserAgent(context.Background(), "linux", "stable") if err != nil { - return err + panic(err) } - fmt.Printf("latest: %s\n", ver) - return nil + fmt.Println(userAgent) } ``` diff --git a/_example/example.go b/_example/example.go deleted file mode 100644 index 78f5bc0..0000000 --- a/_example/example.go +++ /dev/null @@ -1,61 +0,0 @@ -// _example/example.go -package main - -import ( - "context" - "flag" - "fmt" - "os" - "runtime" - - "github.com/chromedp/omahaproxy" -) - -func main() { - platform := "linux" - switch runtime.GOOS { - case "windows": - platform = "win" - if runtime.GOARCH == "amd64" { - platform += "64" - } - case "darwin": - platform = "mac" - if runtime.GOARCH == "aarch64" { - platform += "_arm64" - } - } - verbose := flag.Bool("v", false, "verbose") - osstr := flag.String("os", platform, "os") - channel := flag.String("channel", "stable", "channel") - flag.Parse() - if err := run(context.Background(), *verbose, *osstr, *channel); err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n", err) - os.Exit(1) - } -} - -func run(ctx context.Context, verbose bool, os, channel string) error { - // enable verbose - var opts []omahaproxy.Option - if verbose { - opts = append(opts, omahaproxy.WithLogf(fmt.Printf)) - } - // create client - cl := omahaproxy.New(opts...) - // retrieve recent - releases, err := cl.Recent(ctx) - if err != nil { - return err - } - for _, release := range releases { - fmt.Printf("os: %s channel: %s version: %s\n", release.OS, release.Channel, release.Version) - } - // show latest - ver, err := cl.Latest(ctx, os, channel) - if err != nil { - return err - } - fmt.Printf("latest: %s\n", ver) - return nil -} diff --git a/client.go b/client.go index 659f1bc..f85c68c 100644 --- a/client.go +++ b/client.go @@ -1,28 +1,36 @@ -package omahaproxy +package verhist import ( - "bytes" "context" - "encoding/csv" "encoding/json" + "errors" "fmt" - "io" - "io/ioutil" "net/http" - "time" + "net/url" + "strings" "github.com/kenshaw/httplog" ) +// https://versionhistory.googleapis.com/v1/chrome/platforms/all/channels/all/versions/ +// https://versionhistory.googleapis.com/v1/chrome/platforms/all/channels/all/versions/all/releases?filter=endtime%3E2023-01-01T00:00:00Z + +// https://developer.chrome.com/docs/web-platform/versionhistory/guide +// https://developer.chrome.com/docs/web-platform/versionhistory/reference +// https://developer.chrome.com/docs/web-platform/versionhistory/examples + // DefaultTransport is the default transport. var DefaultTransport = http.DefaultTransport -// Client is a omaha proxy client. +// BaseURL is the base URL. +var BaseURL = "https://versionhistory.googleapis.com" + +// Client is a version history client. type Client struct { Transport http.RoundTripper } -// New creates a new omaha proxy client. +// New creates a new version history client. func New(opts ...Option) *Client { cl := &Client{ Transport: DefaultTransport, @@ -33,151 +41,107 @@ func New(opts ...Option) *Client { return cl } -// get retrieves data from the url. -func (cl *Client) get(ctx context.Context, urlstr string) ([]byte, error) { - req, err := http.NewRequest("GET", urlstr, nil) - if err != nil { - return nil, err +// Versions returns the versions for the os, channel. +func (cl *Client) Versions(ctx context.Context, os, channel string, q ...string) ([]Version, error) { + if len(q) == 0 { + q = []string{ + "order_by", "version desc", + } } - // retrieve and decode - httpClient := &http.Client{Transport: cl.Transport} - res, err := httpClient.Do(req.WithContext(ctx)) - if err != nil { + res := new(VersionsResponse) + if err := grab(ctx, BaseURL+"/v1/chrome/platforms/"+os+"/channels/"+channel+"/versions", cl.Transport, res, q...); err != nil { return nil, err } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return nil, fmt.Errorf("could not retrieve %s (status: %d)", urlstr, res.StatusCode) - } - return ioutil.ReadAll(res.Body) + return res.Versions, nil } -// Recent retrieves the recent release history from the omaha proxy. -func (cl *Client) Recent(ctx context.Context) ([]Release, error) { - buf, err := cl.get(ctx, "https://omahaproxy.appspot.com/history") - if err != nil { - return nil, err +// UserAgent builds the user agent for the os, channel. +func (cl *Client) UserAgent(ctx context.Context, os, channel string) (string, error) { + versions, err := cl.Versions(ctx, os, channel) + switch { + case err != nil: + return "", err + case len(versions) == 0: + return "", errors.New("no versions returned") } - r := csv.NewReader(bytes.NewReader(buf)) - r.FieldsPerRecord = 4 - r.TrimLeadingSpace = true - var history []Release - var i int -loop: - for { - row, err := r.Read() - switch { - case err != nil && err == io.EOF: - break loop - case err != nil: - return nil, err - } - i++ - if i == 1 { - continue - } - timestamp, err := time.Parse("2006-01-02 15:04:05.999999999", row[3]) - if err != nil { - return nil, err - } - history = append(history, Release{ - OS: row[0], - Channel: row[1], - Version: row[2], - Timestamp: timestamp, - }) - } - return history, nil + return versions[0].UserAgent(os), nil } -// Entries retrieves latest version entries from the omaha proxy. -func (cl *Client) Entries(ctx context.Context) ([]VersionEntry, error) { - buf, err := cl.get(ctx, "https://omahaproxy.appspot.com/json") - if err != nil { - return nil, err - } - dec := json.NewDecoder(bytes.NewReader(buf)) - dec.DisallowUnknownFields() - var entries []VersionEntry - if err := dec.Decode(&entries); err != nil { - return nil, err +// Option is a version history client option. +type Option func(*Client) + +// WithTransport is a version history client option to set the http transport. +func WithTransport(transport http.RoundTripper) Option { + return func(cl *Client) { + cl.Transport = transport } - return entries, nil } -// Latest returns the latest version for the provided os and channel from the -// omaha proxy. -func (cl *Client) Latest(ctx context.Context, os, channel string) (Version, error) { - entries, err := cl.Entries(ctx) - if err != nil { - return Version{}, err - } - for _, entry := range entries { - if entry.OS != os { - continue - } - for _, v := range entry.Versions { - if v.Channel == channel { - return v, nil - } - } +// WithLogf is a version history client option to set a log handler for HTTP +// requests and responses. +func WithLogf(logf interface{}, opts ...httplog.Option) Option { + return func(cl *Client) { + cl.Transport = httplog.NewPrefixedRoundTripLogger(cl.Transport, logf, opts...) } - return Version{}, fmt.Errorf("could not find latest version for channel %s (%s)", channel, os) } -// Release holds browser release information. -type Release struct { - OS string - Channel string - Version string - Timestamp time.Time +// VersionsResponse wraps the versions API response. +type VersionsResponse struct { + Versions []Version `json:"versions,omitempty"` + NextPageToken string `json:"nextPageToken,omitempty"` } -// Version wraps browser version information. +// Version contains information about a chrome release. type Version struct { - BranchCommit string `json:"branch_commit"` - BranchBasePosition string `json:"branch_base_position"` - SkiaCommit string `json:"skia_commit"` - V8Version string `json:"v8_version"` - PreviousVersion string `json:"previous_version"` - V8Commit string `json:"v8_commit"` - TrueBranch string `json:"true_branch"` - PreviousReldate string `json:"previous_reldate"` - BranchBaseCommit string `json:"branch_base_commit"` - Version string `json:"version"` - CurrentReldate string `json:"current_reldate"` - CurrentVersion string `json:"current_version"` - OS string `json:"os"` - Channel string `json:"channel"` - ChromiumCommit string `json:"chromium_commit"` -} - -// String satisfies the fmt.Stringer interface. -func (v Version) String() string { - return fmt.Sprintf("Chromium %s (v8: %s, os: %s, channel: %s)", v.Version, v.V8Version, v.OS, v.Channel) -} - -// VersionEntry is a OS version entry detailing the available browser -// version entries. -type VersionEntry struct { - OS string `json:"os"` - Versions []Version `json:"versions"` + Name string `json:"name,omitempty"` + Version string `json:"version,omitempty"` } -// Option is a omaha proxy client option. -type Option func(*Client) - -// WithTransport is a omaha proxy client option to set the http transport. -func WithTransport(transport http.RoundTripper) Option { - return func(cl *Client) { - cl.Transport = transport +// UserAgent builds the user agent for the +func (ver Version) UserAgent(os string) string { + typ := "Windows NT 10.0; Win64; x64" + switch strings.ToLower(os) { + case "linux": + typ = "X11; Linux x86_64" + case "mac", "mac_arm64": + typ = "Macintosh; Intel Mac OS X 10_15_7" + } + v := "120.0.0.0" + if i := strings.Index(ver.Version, "."); i != -1 { + v = ver.Version[:i] + ".0.0.0" } + return fmt.Sprintf("Mozilla/5.0 (%s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36", typ, v) } -// WithLogf is a omaha proxy client option to set a log handler for HTTP -// requests and responses. -func WithLogf(logf interface{}, opts ...httplog.Option) Option { - return func(cl *Client) { - cl.Transport = httplog.NewPrefixedRoundTripLogger(cl.Transport, logf, opts...) +// grab grabs the url and json decodes it. +func grab(ctx context.Context, urlstr string, transport http.RoundTripper, v interface{}, q ...string) error { + if len(q)%2 != 0 { + return errors.New("invalid query") + } + z := make(url.Values) + for i := 0; i < len(q); i += 2 { + z.Add(q[i], q[i+1]) + } + s := z.Encode() + if s != "" { + s = "?" + s + } + req, err := http.NewRequestWithContext(ctx, "GET", urlstr+s, nil) + if err != nil { + return err } + cl := &http.Client{ + Transport: transport, + } + res, err := cl.Do(req) + if err != nil { + return err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return fmt.Errorf("could not retrieve %s (status: %d)", urlstr, res.StatusCode) + } + dec := json.NewDecoder(res.Body) + dec.DisallowUnknownFields() + return dec.Decode(v) } diff --git a/client_test.go b/client_test.go index f66b263..1daca7f 100644 --- a/client_test.go +++ b/client_test.go @@ -1,28 +1,19 @@ -package omahaproxy +package verhist import ( "context" "testing" ) -func TestRecent(t *testing.T) { +func TestUserAgent(t *testing.T) { t.Parallel() cl := New(WithLogf(t.Logf)) - releases, err := cl.Recent(context.Background()) - if err != nil { + userAgent, err := cl.UserAgent(context.Background(), "linux", "stable") + switch { + case err != nil: t.Fatalf("expected no error, got: %v", err) + case userAgent == "": + t.Errorf("expected non-empty user agent") } - for _, release := range releases { - t.Logf("os: %s channel: %s version: %s\n", release.OS, release.Channel, release.Version) - } -} - -func TestLatest(t *testing.T) { - t.Parallel() - cl := New(WithLogf(t.Logf)) - ver, err := cl.Latest(context.Background(), "linux", "stable") - if err != nil { - t.Fatalf("expected no error, got: %v", err) - } - t.Logf("latest: %v", ver) + t.Logf("user agent: %v", userAgent) } diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..d229148 --- /dev/null +++ b/example_test.go @@ -0,0 +1,18 @@ +package verhist_test + +import ( + "context" + "fmt" + + "github.com/chromedp/verhist" +) + +func Example() { + userAgent, err := verhist.UserAgent(context.Background(), "linux", "stable") + if err != nil { + panic(err) + } + fmt.Println(userAgent != "") + // Output: + // true +} diff --git a/go.mod b/go.mod index 2e9afc3..2fd1f1b 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/chromedp/omahaproxy +module github.com/chromedp/verhist go 1.20 diff --git a/omahaproxy.go b/omahaproxy.go deleted file mode 100644 index df96774..0000000 --- a/omahaproxy.go +++ /dev/null @@ -1,25 +0,0 @@ -// Package omahaproxy provides a client and utilities for working with the -// Chrome Omaha Proxy. -// -// See: https://omahaproxy.appspot.com -package omahaproxy - -import ( - "context" -) - -// Recent returns the recent release information from the omaha proxy. -func Recent(ctx context.Context, opts ...Option) ([]Release, error) { - return New(opts...).Recent(ctx) -} - -// Entries returns the latest version entries from the omaha proxy. -func Entries(ctx context.Context, opts ...Option) ([]VersionEntry, error) { - return New(opts...).Entries(ctx) -} - -// Latest retrieves the latest version for the specified os and channel from -// the omaha proxy. -func Latest(ctx context.Context, os, channel string, opts ...Option) (Version, error) { - return New(opts...).Latest(ctx, os, channel) -} diff --git a/verhist.go b/verhist.go new file mode 100644 index 0000000..3bd036a --- /dev/null +++ b/verhist.go @@ -0,0 +1,19 @@ +// Package verhist provides a client and utilities for working with the +// Chrome version history API. +// +// See: https://developer.chrome.com/docs/web-platform/versionhistory/guide +package verhist + +import ( + "context" +) + +// Versions returns the versions for the os, channel. +func Versions(ctx context.Context, os, channel string, opts ...Option) ([]Version, error) { + return New(opts...).Versions(ctx, os, channel) +} + +// UserAgent builds the user agent for the os, channel. +func UserAgent(ctx context.Context, os, channel string, opts ...Option) (string, error) { + return New(opts...).UserAgent(ctx, os, channel) +}